Files
opentf/internal/command/arguments/show_test.go
Martin Atkins 6a27c82bb4 tofu show: -module=DIR mode, for showing just a single module
We previously added the -config mode for showing the entire assembled
configuration tree, including the content of any descendent modules, but
that mode requires first running "tofu init" to install all of the
provider and module dependencies of the configuration.

This new -module=DIR mode returns a subset of the same JSON representation
for only a single module that can be generated without first installing
any dependencies, making this mode more appropriate for situations like
generating documentation for a single module when importing it into the
OpenTofu Registry. The registry generation process does not want to endure
the overhead of installing other providers and modules when all it actually
needs is metadata about the top-level declarations in the module.

To minimize the risk to the already-working full-config JSON representation
while still reusing most of its code, the implementation details of package
jsonconfig are a little awkward here. Since this code changes relatively
infrequently and is implementing an external interface subject to
compatibility constraints, and since this new behavior is relatively
marginal and intended primarily for our own OpenTofu Registry purposes,
this is a pragmatic tradeoff that is hopefully compensated for well enough
by the code comments that aim to explain what's going on for the benefit
of future maintainers. If we _do_ find ourselves making substantial changes
to this code at a later date then we can consider a more significant
restructure of the code at that point; the weird stuff is intentionally
encapsulated inside package jsonconfig so it can change later without
changing any callers.

Signed-off-by: Martin Atkins <mart@degeneration.co.uk>
2025-07-10 13:18:26 -07:00

320 lines
7.5 KiB
Go

// Copyright (c) The OpenTofu Authors
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) 2023 HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package arguments
import (
"reflect"
"testing"
"github.com/davecgh/go-spew/spew"
"github.com/opentofu/opentofu/internal/tfdiags"
)
func TestParseShow_valid(t *testing.T) {
testCases := map[string]struct {
args []string
want *Show
}{
"no options at all": {
nil,
&Show{
TargetType: ShowState,
TargetArg: "",
ViewType: ViewHuman,
},
},
"json with no other options": {
[]string{"-json"},
&Show{
TargetType: ShowState,
TargetArg: "",
ViewType: ViewJSON,
},
},
"latest state snapshot": {
[]string{"-state"},
&Show{
TargetType: ShowState,
TargetArg: "",
ViewType: ViewHuman,
},
},
"latest state snapshot, JSON": {
[]string{"-state", "-json"},
&Show{
TargetType: ShowState,
TargetArg: "",
ViewType: ViewJSON,
},
},
"saved plan file": {
[]string{"-plan=tfplan"},
&Show{
TargetType: ShowPlan,
TargetArg: "tfplan",
ViewType: ViewHuman,
},
},
"saved plan file, JSON": {
[]string{"-plan=tfplan", "-json"},
&Show{
TargetType: ShowPlan,
TargetArg: "tfplan",
ViewType: ViewJSON,
},
},
"legacy positional argument": {
[]string{"foo"},
&Show{
TargetType: ShowUnknownType, // caller must inspect "foo" to decide the type
TargetArg: "foo",
ViewType: ViewHuman,
},
},
"json with legacy positional argument": {
[]string{"-json", "foo"},
&Show{
TargetType: ShowUnknownType, // caller must inspect "foo" to decide the type
TargetArg: "foo",
ViewType: ViewJSON,
},
},
"configuration with json": {
[]string{"-config", "-json"},
&Show{
TargetType: ShowConfig,
TargetArg: "",
ViewType: ViewJSON,
},
},
"module with json": {
[]string{"-module=foo", "-json"},
&Show{
TargetType: ShowModule,
TargetArg: "foo",
ViewType: ViewJSON,
},
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
got, diags := ParseShow(tc.args)
got.Vars = nil
if len(diags) > 0 {
t.Fatalf("unexpected diags: %v", diags)
}
if *got != *tc.want {
t.Fatalf("unexpected result\n got: %#v\nwant: %#v", got, tc.want)
}
})
}
}
func TestParseShow_invalid(t *testing.T) {
testCases := map[string]struct {
args []string
want *Show
wantDiags tfdiags.Diagnostics
}{
"unknown option": {
[]string{"-boop"},
&Show{
TargetType: ShowState,
ViewType: ViewHuman,
},
tfdiags.Diagnostics{
tfdiags.Sourceless(
tfdiags.Error,
"Failed to parse command-line options",
"flag provided but not defined: -boop",
),
},
},
"positional arguments with state target selection": {
[]string{"-state", "bar"},
&Show{
ViewType: ViewHuman,
},
tfdiags.Diagnostics{
tfdiags.Sourceless(
tfdiags.Error,
"Unexpected command line arguments",
"This command does not expect any positional arguments when using a target-selection option.",
),
},
},
"positional arguments with planfile target selection": {
[]string{"-plan=foo", "bar"},
&Show{
ViewType: ViewHuman,
},
tfdiags.Diagnostics{
tfdiags.Sourceless(
tfdiags.Error,
"Unexpected command line arguments",
"This command does not expect any positional arguments when using a target-selection option.",
),
},
},
"conflicting target selection options": {
[]string{"-state", "-plan=foo"},
&Show{
TargetType: ShowPlan,
TargetArg: "foo",
ViewType: ViewHuman,
},
tfdiags.Diagnostics{
tfdiags.Sourceless(
tfdiags.Error,
"Conflicting object types to show",
"The -state, -plan=FILENAME, -config, and -module=DIR options are mutually-exclusive, to specify which kind of object to show.",
),
},
},
"too many arguments in legacy mode": {
[]string{"bar", "baz"},
&Show{
ViewType: ViewHuman,
},
tfdiags.Diagnostics{
tfdiags.Sourceless(
tfdiags.Error,
"Too many command line arguments",
"Expected at most one positional argument for the legacy positional argument mode.",
),
},
},
"too many arguments in legacy mode, json": {
[]string{"-json", "bar", "baz"},
&Show{
ViewType: ViewJSON,
},
tfdiags.Diagnostics{
tfdiags.Sourceless(
tfdiags.Error,
"Too many command line arguments",
"Expected at most one positional argument for the legacy positional argument mode.",
),
},
},
"configuration without json": {
[]string{"-config"},
&Show{
ViewType: ViewNone,
},
tfdiags.Diagnostics{
tfdiags.Sourceless(
tfdiags.Error,
"JSON output required for configuration",
"The -config option requires -json to be specified.",
),
},
},
"configuration with state": {
[]string{"-config", "-state", "-json"},
&Show{
TargetType: ShowConfig,
TargetArg: "",
ViewType: ViewJSON,
},
tfdiags.Diagnostics{
tfdiags.Sourceless(
tfdiags.Error,
"Conflicting object types to show",
"The -state, -plan=FILENAME, -config, and -module=DIR options are mutually-exclusive, to specify which kind of object to show.",
),
},
},
"configuration with plan": {
[]string{"-config", "-plan=tfplan", "-json"},
&Show{
TargetType: ShowConfig,
TargetArg: "",
ViewType: ViewJSON,
},
tfdiags.Diagnostics{
tfdiags.Sourceless(
tfdiags.Error,
"Conflicting object types to show",
"The -state, -plan=FILENAME, -config, and -module=DIR options are mutually-exclusive, to specify which kind of object to show.",
),
},
},
"module without json": {
[]string{"-module=foo"},
&Show{
ViewType: ViewNone,
},
tfdiags.Diagnostics{
tfdiags.Sourceless(
tfdiags.Error,
"JSON output required for module",
"The -module=DIR option requires -json to be specified.",
),
},
},
"module with state": {
[]string{"-module=foo", "-state", "-json"},
&Show{
TargetType: ShowModule,
TargetArg: "foo",
ViewType: ViewJSON,
},
tfdiags.Diagnostics{
tfdiags.Sourceless(
tfdiags.Error,
"Conflicting object types to show",
"The -state, -plan=FILENAME, -config, and -module=DIR options are mutually-exclusive, to specify which kind of object to show.",
),
},
},
"module with plan": {
[]string{"-module=foo", "-plan=tfplan", "-json"},
&Show{
TargetType: ShowModule,
TargetArg: "foo",
ViewType: ViewJSON,
},
tfdiags.Diagnostics{
tfdiags.Sourceless(
tfdiags.Error,
"Conflicting object types to show",
"The -state, -plan=FILENAME, -config, and -module=DIR options are mutually-exclusive, to specify which kind of object to show.",
),
},
},
"module with config": {
[]string{"-module=foo", "-config", "-json"},
&Show{
TargetType: ShowModule,
TargetArg: "foo",
ViewType: ViewJSON,
},
tfdiags.Diagnostics{
tfdiags.Sourceless(
tfdiags.Error,
"Conflicting object types to show",
"The -state, -plan=FILENAME, -config, and -module=DIR options are mutually-exclusive, to specify which kind of object to show.",
),
},
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
got, gotDiags := ParseShow(tc.args)
got.Vars = nil
if *got != *tc.want {
t.Fatalf("unexpected result\n got: %#v\nwant: %#v", got, tc.want)
}
if !reflect.DeepEqual(gotDiags, tc.wantDiags) {
t.Errorf("wrong result\ngot: %s\nwant: %s", spew.Sdump(gotDiags), spew.Sdump(tc.wantDiags))
}
})
}
}