Skip to content

Commit 37d8330

Browse files
committed
test: add integration tests for version command recursive module support
Adds comprehensive integration tests that verify: - Recursive mode populates modules array with per-directory plugins - Top-level plugins field contains deduplicated plugins across all modules - Deduplication works correctly (aws in root + module1 = 1 entry) - Different plugins across modules are included (aws + google = 2 entries) - Plugins are sorted by name, then version - Non-recursive mode maintains backwards compatibility (no modules field) - Text format output works correctly Tests use real plugin downloads via --init to validate actual behavior.
1 parent c0bf07d commit 37d8330

File tree

2 files changed

+323
-6
lines changed

2 files changed

+323
-6
lines changed

cmd/version.go

Lines changed: 71 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
package cmd
22

33
import (
4+
"cmp"
45
"context"
56
"encoding/json"
67
"fmt"
78
"log"
89
"maps"
910
"slices"
11+
"strings"
1012
"time"
1113

1214
"github.com/spf13/afero"
@@ -21,11 +23,18 @@ const (
2123

2224
// VersionOutput is the JSON output structure for version command
2325
type VersionOutput struct {
24-
Version string `json:"version"`
25-
Plugins []PluginVersion `json:"plugins"`
26-
UpdateCheckEnabled bool `json:"update_check_enabled"`
27-
UpdateAvailable bool `json:"update_available"`
28-
LatestVersion string `json:"latest_version,omitempty"`
26+
Version string `json:"version"`
27+
Plugins []PluginVersion `json:"plugins,omitempty"`
28+
Modules []ModuleVersionOutput `json:"modules,omitempty"`
29+
UpdateCheckEnabled bool `json:"update_check_enabled"`
30+
UpdateAvailable bool `json:"update_available"`
31+
LatestVersion string `json:"latest_version,omitempty"`
32+
}
33+
34+
// ModuleVersionOutput represents plugins for a specific module
35+
type ModuleVersionOutput struct {
36+
Path string `json:"path"`
37+
Plugins []PluginVersion `json:"plugins"`
2938
}
3039

3140
// PluginVersion represents a plugin's name and version
@@ -116,10 +125,15 @@ func (cli *CLI) printVersion(opts Options) int {
116125
}
117126

118127
func (cli *CLI) printVersionJSON(opts Options, updateInfo *versioncheck.UpdateInfo) int {
128+
workingDirs, err := findWorkingDirs(opts)
129+
if err != nil {
130+
cli.formatter.Print(tflint.Issues{}, fmt.Errorf("Failed to find workspaces; %w", err), map[string][]byte{})
131+
return ExitCodeError
132+
}
133+
119134
// Build output
120135
output := VersionOutput{
121136
Version: tflint.Version.String(),
122-
Plugins: getPluginVersions(opts),
123137
UpdateCheckEnabled: versioncheck.Enabled(),
124138
}
125139

@@ -130,6 +144,57 @@ func (cli *CLI) printVersionJSON(opts Options, updateInfo *versioncheck.UpdateIn
130144
}
131145
}
132146

147+
// Handle multiple working directories for --recursive
148+
if opts.Recursive && len(workingDirs) > 1 {
149+
// Track all unique plugins across modules
150+
pluginMap := make(map[string]PluginVersion)
151+
152+
for _, wd := range workingDirs {
153+
var plugins []PluginVersion
154+
err := cli.withinChangedDir(wd, func() error {
155+
plugins = getPluginVersions(opts)
156+
return nil
157+
})
158+
if err != nil {
159+
log.Printf("[ERROR] Failed to get plugins for %s: %s", wd, err)
160+
continue
161+
}
162+
163+
// Add to modules output
164+
output.Modules = append(output.Modules, ModuleVersionOutput{
165+
Path: wd,
166+
Plugins: plugins,
167+
})
168+
169+
// Accumulate unique plugins
170+
for _, plugin := range plugins {
171+
key := plugin.Name + "@" + plugin.Version
172+
pluginMap[key] = plugin
173+
}
174+
}
175+
176+
// Convert map to sorted slice for consistent output
177+
for _, plugin := range pluginMap {
178+
output.Plugins = append(output.Plugins, plugin)
179+
}
180+
slices.SortFunc(output.Plugins, func(a, b PluginVersion) int {
181+
return cmp.Or(
182+
strings.Compare(a.Name, b.Name),
183+
strings.Compare(a.Version, b.Version),
184+
)
185+
})
186+
} else {
187+
// Single directory mode (backwards compatible)
188+
err := cli.withinChangedDir(workingDirs[0], func() error {
189+
output.Plugins = getPluginVersions(opts)
190+
return nil
191+
})
192+
if err != nil {
193+
cli.formatter.Print(tflint.Issues{}, err, map[string][]byte{})
194+
return ExitCodeError
195+
}
196+
}
197+
133198
// Marshal and print JSON
134199
jsonBytes, err := json.MarshalIndent(output, "", " ")
135200
if err != nil {
Lines changed: 252 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
1+
package main
2+
3+
import (
4+
"bytes"
5+
"encoding/json"
6+
"os"
7+
"path/filepath"
8+
"strings"
9+
"testing"
10+
11+
"github.com/google/go-cmp/cmp"
12+
"github.com/google/go-cmp/cmp/cmpopts"
13+
"github.com/terraform-linters/tflint/cmd"
14+
"github.com/terraform-linters/tflint/tflint"
15+
)
16+
17+
func TestVersionRecursiveWithPlugins(t *testing.T) {
18+
// Disable the bundled plugin because os.Executable() returns go(1) in tests
19+
tflint.DisableBundledPlugin = true
20+
t.Cleanup(func() {
21+
tflint.DisableBundledPlugin = false
22+
})
23+
24+
// Create test directory structure
25+
tmpDir := t.TempDir()
26+
pluginDir := filepath.Join(tmpDir, ".tflint.d")
27+
t.Setenv("TFLINT_PLUGIN_DIR", pluginDir)
28+
29+
module1 := filepath.Join(tmpDir, "module1")
30+
module2 := filepath.Join(tmpDir, "module2")
31+
32+
if err := os.MkdirAll(module1, 0755); err != nil {
33+
t.Fatal(err)
34+
}
35+
if err := os.MkdirAll(module2, 0755); err != nil {
36+
t.Fatal(err)
37+
}
38+
39+
// Root config: aws plugin
40+
rootConfig := `
41+
plugin "aws" {
42+
enabled = true
43+
version = "0.21.1"
44+
source = "github.com/terraform-linters/tflint-ruleset-aws"
45+
}
46+
`
47+
if err := os.WriteFile(filepath.Join(tmpDir, ".tflint.hcl"), []byte(rootConfig), 0644); err != nil {
48+
t.Fatal(err)
49+
}
50+
51+
// Module 1: aws plugin (duplicate)
52+
module1Config := `
53+
plugin "aws" {
54+
enabled = true
55+
version = "0.21.1"
56+
source = "github.com/terraform-linters/tflint-ruleset-aws"
57+
}
58+
`
59+
if err := os.WriteFile(filepath.Join(module1, ".tflint.hcl"), []byte(module1Config), 0644); err != nil {
60+
t.Fatal(err)
61+
}
62+
63+
// Module 2: google plugin (different)
64+
module2Config := `
65+
plugin "google" {
66+
enabled = true
67+
version = "0.21.0"
68+
source = "github.com/terraform-linters/tflint-ruleset-google"
69+
}
70+
`
71+
if err := os.WriteFile(filepath.Join(module2, ".tflint.hcl"), []byte(module2Config), 0644); err != nil {
72+
t.Fatal(err)
73+
}
74+
75+
t.Chdir(tmpDir)
76+
77+
// First, run init to install plugins
78+
outStream, errStream := new(bytes.Buffer), new(bytes.Buffer)
79+
cli, err := cmd.NewCLI(outStream, errStream)
80+
if err != nil {
81+
t.Fatal(err)
82+
}
83+
84+
exitCode := cli.Run([]string{"tflint", "--recursive", "--init"})
85+
if exitCode != cmd.ExitCodeOK {
86+
t.Fatalf("init failed with exit code %d\nstdout: %s\nstderr: %s", exitCode, outStream.String(), errStream.String())
87+
}
88+
89+
// Now run version command
90+
outStream.Reset()
91+
errStream.Reset()
92+
cli, err = cmd.NewCLI(outStream, errStream)
93+
if err != nil {
94+
t.Fatal(err)
95+
}
96+
97+
t.Setenv("TFLINT_DISABLE_VERSION_CHECK", "1")
98+
exitCode = cli.Run([]string{"tflint", "--recursive", "--version", "--format=json"})
99+
if exitCode != cmd.ExitCodeOK {
100+
t.Fatalf("version failed with exit code %d\nstdout: %s\nstderr: %s", exitCode, outStream.String(), errStream.String())
101+
}
102+
103+
var output cmd.VersionOutput
104+
if err := json.Unmarshal(outStream.Bytes(), &output); err != nil {
105+
t.Fatalf("failed to unmarshal JSON: %s\noutput: %s", err, outStream.String())
106+
}
107+
108+
// Verify modules are present (3 directories: ., module1, module2)
109+
if len(output.Modules) != 3 {
110+
t.Errorf("expected 3 modules, got %d: %+v", len(output.Modules), output.Modules)
111+
}
112+
113+
// Verify module paths
114+
var gotPaths []string
115+
for _, mod := range output.Modules {
116+
gotPaths = append(gotPaths, mod.Path)
117+
}
118+
119+
expectedPaths := []string{".", "module1", "module2"}
120+
opts := []cmp.Option{
121+
cmpopts.SortSlices(func(a, b string) bool { return a < b }),
122+
}
123+
124+
if diff := cmp.Diff(expectedPaths, gotPaths, opts...); diff != "" {
125+
t.Errorf("module paths mismatch (-want +got):\n%s", diff)
126+
}
127+
128+
// Verify deduplicated plugins list contains both aws and google
129+
if len(output.Plugins) != 2 {
130+
t.Errorf("expected 2 deduplicated plugins (aws, google), got %d: %+v", len(output.Plugins), output.Plugins)
131+
}
132+
133+
foundAWS := false
134+
foundGoogle := false
135+
for _, p := range output.Plugins {
136+
if p.Name == "ruleset.aws" && p.Version == "0.21.1" {
137+
foundAWS = true
138+
}
139+
if p.Name == "ruleset.google" && p.Version == "0.21.0" {
140+
foundGoogle = true
141+
}
142+
}
143+
144+
if !foundAWS {
145+
t.Errorf("expected aws plugin in deduplicated list, got: %+v", output.Plugins)
146+
}
147+
if !foundGoogle {
148+
t.Errorf("expected google plugin in deduplicated list, got: %+v", output.Plugins)
149+
}
150+
151+
// Verify plugins are sorted by name
152+
if len(output.Plugins) >= 2 {
153+
if output.Plugins[0].Name > output.Plugins[1].Name {
154+
t.Errorf("plugins should be sorted by name, got: %+v", output.Plugins)
155+
}
156+
}
157+
}
158+
159+
func TestVersionNonRecursive(t *testing.T) {
160+
tflint.DisableBundledPlugin = true
161+
t.Cleanup(func() {
162+
tflint.DisableBundledPlugin = false
163+
})
164+
165+
tmpDir := t.TempDir()
166+
pluginDir := filepath.Join(tmpDir, ".tflint.d")
167+
t.Setenv("TFLINT_PLUGIN_DIR", pluginDir)
168+
169+
config := `
170+
plugin "aws" {
171+
enabled = true
172+
version = "0.21.1"
173+
source = "github.com/terraform-linters/tflint-ruleset-aws"
174+
}
175+
`
176+
if err := os.WriteFile(filepath.Join(tmpDir, ".tflint.hcl"), []byte(config), 0644); err != nil {
177+
t.Fatal(err)
178+
}
179+
180+
t.Chdir(tmpDir)
181+
182+
// Init
183+
outStream, errStream := new(bytes.Buffer), new(bytes.Buffer)
184+
cli, err := cmd.NewCLI(outStream, errStream)
185+
if err != nil {
186+
t.Fatal(err)
187+
}
188+
189+
exitCode := cli.Run([]string{"tflint", "--init"})
190+
if exitCode != cmd.ExitCodeOK {
191+
t.Fatalf("init failed with exit code %d\nstdout: %s\nstderr: %s", exitCode, outStream.String(), errStream.String())
192+
}
193+
194+
// Version (non-recursive)
195+
outStream.Reset()
196+
errStream.Reset()
197+
cli, err = cmd.NewCLI(outStream, errStream)
198+
if err != nil {
199+
t.Fatal(err)
200+
}
201+
202+
t.Setenv("TFLINT_DISABLE_VERSION_CHECK", "1")
203+
exitCode = cli.Run([]string{"tflint", "--version", "--format=json"})
204+
if exitCode != cmd.ExitCodeOK {
205+
t.Fatalf("version failed with exit code %d\nstdout: %s\nstderr: %s", exitCode, outStream.String(), errStream.String())
206+
}
207+
208+
var output cmd.VersionOutput
209+
if err := json.Unmarshal(outStream.Bytes(), &output); err != nil {
210+
t.Fatalf("failed to unmarshal JSON: %s\noutput: %s", err, outStream.String())
211+
}
212+
213+
// Non-recursive mode should NOT have modules field
214+
if len(output.Modules) != 0 {
215+
t.Errorf("non-recursive mode should not have modules, got: %+v", output.Modules)
216+
}
217+
218+
// Should have plugins field
219+
if len(output.Plugins) != 1 {
220+
t.Errorf("expected 1 plugin, got %d: %+v", len(output.Plugins), output.Plugins)
221+
}
222+
223+
if output.Plugins[0].Name != "ruleset.aws" || output.Plugins[0].Version != "0.21.1" {
224+
t.Errorf("expected aws plugin 0.21.1, got: %+v", output.Plugins[0])
225+
}
226+
}
227+
228+
func TestVersionTextFormat(t *testing.T) {
229+
tmpDir := t.TempDir()
230+
if err := os.WriteFile(filepath.Join(tmpDir, ".tflint.hcl"), []byte{}, 0644); err != nil {
231+
t.Fatal(err)
232+
}
233+
234+
t.Chdir(tmpDir)
235+
236+
outStream, errStream := new(bytes.Buffer), new(bytes.Buffer)
237+
cli, err := cmd.NewCLI(outStream, errStream)
238+
if err != nil {
239+
t.Fatal(err)
240+
}
241+
242+
t.Setenv("TFLINT_DISABLE_VERSION_CHECK", "1")
243+
exitCode := cli.Run([]string{"tflint", "--version"})
244+
if exitCode != cmd.ExitCodeOK {
245+
t.Fatalf("expected exit code %d, got %d\nstderr: %s", cmd.ExitCodeOK, exitCode, errStream.String())
246+
}
247+
248+
output := outStream.String()
249+
if !strings.Contains(output, "TFLint version") {
250+
t.Errorf("output should contain 'TFLint version', got: %s", output)
251+
}
252+
}

0 commit comments

Comments
 (0)