Skip to content

Commit 3b1d6af

Browse files
pfrederiksenclaude
andcommitted
fix: Address PR review issues - lint errors, tests, and coverage
This commit fixes all issues identified in the PR review: ## Lint Fixes - Fix errcheck violations in completion.go by using RunE and checking errors - All Gen*Completion calls now properly return and check errors ## Bug Fixes - Fix global state pollution in report.go color.NoColor - Save original value and restore via defer - Prevents color settings from persisting across function calls - Critical for library usage and test reliability - Fix ApplyConfigDefaults to handle empty OutputFormat - Check for both "" and "report" as default values - Config values now properly apply when CLI uses defaults ## Test Coverage Improvements - Add comprehensive TestParseHCL with 13 test cases - Boolean, number, string primitives - Objects, nested objects - Lists of strings and objects - Multiple top-level attributes - Invalid HCL and empty HCL - All cases verify paths are set - Add comprehensive TestCLIOptions_ApplyConfigDefaults with 8 test cases - Empty config no-op - Config defaults when CLI empty - CLI precedence over config - Ignore paths deduplication and merging - Array keys merging - Boolean, string, and numeric defaults - All merge scenarios covered - Add integration tests using testdata files - TestParseHCL_Integration uses testdata/hcl/*.hcl files - Verifies real-world HCL parsing - Addresses unused testdata concern ## Coverage Results - Total coverage: 84.5% (above 80% threshold) - parse package: 82.2% - internal/cli: 91.8% - internal/config: 100.0% - All packages above or near 80% All tests passing, all lint checks passing, coverage threshold met. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent 52b187b commit 3b1d6af

File tree

5 files changed

+501
-6
lines changed

5 files changed

+501
-6
lines changed

cmd/configdiff/completion.go

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -51,17 +51,19 @@ PowerShell:
5151
DisableFlagsInUseLine: true,
5252
ValidArgs: []string{"bash", "zsh", "fish", "powershell"},
5353
Args: cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs),
54-
Run: func(cmd *cobra.Command, args []string) {
54+
RunE: func(cmd *cobra.Command, args []string) error {
55+
var err error
5556
switch args[0] {
5657
case "bash":
57-
cmd.Root().GenBashCompletion(os.Stdout)
58+
err = cmd.Root().GenBashCompletion(os.Stdout)
5859
case "zsh":
59-
cmd.Root().GenZshCompletion(os.Stdout)
60+
err = cmd.Root().GenZshCompletion(os.Stdout)
6061
case "fish":
61-
cmd.Root().GenFishCompletion(os.Stdout, true)
62+
err = cmd.Root().GenFishCompletion(os.Stdout, true)
6263
case "powershell":
63-
cmd.Root().GenPowerShellCompletionWithDesc(os.Stdout)
64+
err = cmd.Root().GenPowerShellCompletionWithDesc(os.Stdout)
6465
}
66+
return err
6567
},
6668
}
6769

internal/cli/options.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ func (c *CLIOptions) ApplyConfigDefaults(cfg *config.Config) {
122122
}
123123

124124
// Apply string defaults if not set
125-
if c.OutputFormat == "report" && cfg.OutputFormat != "" {
125+
if (c.OutputFormat == "" || c.OutputFormat == "report") && cfg.OutputFormat != "" {
126126
c.OutputFormat = cfg.OutputFormat
127127
}
128128

internal/cli/options_test.go

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ package cli
22

33
import (
44
"testing"
5+
6+
"github.com/pfrederiksen/configdiff/internal/config"
57
)
68

79
func TestCLIOptions_ToLibraryOptions(t *testing.T) {
@@ -170,3 +172,195 @@ func TestCLIOptions_Validate(t *testing.T) {
170172
})
171173
}
172174
}
175+
func TestCLIOptions_ApplyConfigDefaults(t *testing.T) {
176+
tests := []struct {
177+
name string
178+
opts CLIOptions
179+
config *config.Config
180+
want CLIOptions
181+
}{
182+
{
183+
name: "empty config no-op",
184+
opts: CLIOptions{
185+
OutputFormat: "report",
186+
},
187+
config: &config.Config{},
188+
want: CLIOptions{
189+
OutputFormat: "report",
190+
},
191+
},
192+
{
193+
name: "apply config defaults when CLI empty",
194+
opts: CLIOptions{},
195+
config: &config.Config{
196+
IgnorePaths: []string{"/status"},
197+
NumericStrings: true,
198+
BoolStrings: true,
199+
StableOrder: true,
200+
OutputFormat: "compact",
201+
MaxValueLength: 50,
202+
NoColor: true,
203+
},
204+
want: CLIOptions{
205+
IgnorePaths: []string{"/status"},
206+
NumericStrings: true,
207+
BoolStrings: true,
208+
StableOrder: true,
209+
OutputFormat: "compact",
210+
MaxValueLength: 50,
211+
NoColor: true,
212+
},
213+
},
214+
{
215+
name: "CLI values take precedence",
216+
opts: CLIOptions{
217+
IgnorePaths: []string{"/metadata"},
218+
NumericStrings: true,
219+
OutputFormat: "json",
220+
MaxValueLength: 100,
221+
},
222+
config: &config.Config{
223+
IgnorePaths: []string{"/status"},
224+
NumericStrings: false,
225+
OutputFormat: "compact",
226+
MaxValueLength: 50,
227+
},
228+
want: CLIOptions{
229+
IgnorePaths: []string{"/metadata", "/status"}, // Merged
230+
NumericStrings: true, // CLI wins (already set)
231+
OutputFormat: "json", // CLI wins
232+
MaxValueLength: 100, // CLI wins
233+
},
234+
},
235+
{
236+
name: "merge ignore paths deduplication",
237+
opts: CLIOptions{
238+
IgnorePaths: []string{"/metadata", "/status"},
239+
},
240+
config: &config.Config{
241+
IgnorePaths: []string{"/status", "/timestamp"},
242+
},
243+
want: CLIOptions{
244+
IgnorePaths: []string{"/metadata", "/status", "/timestamp"},
245+
},
246+
},
247+
{
248+
name: "merge array keys",
249+
opts: CLIOptions{
250+
ArrayKeys: []string{"/containers=name"},
251+
},
252+
config: &config.Config{
253+
ArrayKeys: map[string]string{
254+
"/volumes": "name",
255+
"/ports": "port",
256+
},
257+
},
258+
want: CLIOptions{
259+
ArrayKeys: []string{"/containers=name", "/volumes=name", "/ports=port"},
260+
},
261+
},
262+
{
263+
name: "boolean flags - config applies when CLI is false",
264+
opts: CLIOptions{
265+
NumericStrings: false,
266+
BoolStrings: false,
267+
StableOrder: false,
268+
NoColor: false,
269+
},
270+
config: &config.Config{
271+
NumericStrings: true,
272+
BoolStrings: true,
273+
StableOrder: true,
274+
NoColor: true,
275+
},
276+
want: CLIOptions{
277+
NumericStrings: true,
278+
BoolStrings: true,
279+
StableOrder: true,
280+
NoColor: true,
281+
},
282+
},
283+
{
284+
name: "string defaults - config applies when CLI is default",
285+
opts: CLIOptions{
286+
OutputFormat: "report", // Default value
287+
},
288+
config: &config.Config{
289+
OutputFormat: "compact",
290+
},
291+
want: CLIOptions{
292+
OutputFormat: "compact",
293+
},
294+
},
295+
{
296+
name: "numeric defaults - config applies when CLI is zero",
297+
opts: CLIOptions{
298+
MaxValueLength: 0, // Default value
299+
},
300+
config: &config.Config{
301+
MaxValueLength: 100,
302+
},
303+
want: CLIOptions{
304+
MaxValueLength: 100,
305+
},
306+
},
307+
}
308+
309+
for _, tt := range tests {
310+
t.Run(tt.name, func(t *testing.T) {
311+
opts := tt.opts
312+
opts.ApplyConfigDefaults(tt.config)
313+
314+
// Check arrays with element comparison (order-independent)
315+
if !containsAll(opts.IgnorePaths, tt.want.IgnorePaths) {
316+
t.Errorf("IgnorePaths = %v, want to contain all of %v", opts.IgnorePaths, tt.want.IgnorePaths)
317+
}
318+
319+
// Check array keys
320+
if len(opts.ArrayKeys) != len(tt.want.ArrayKeys) {
321+
t.Errorf("ArrayKeys length = %d, want %d", len(opts.ArrayKeys), len(tt.want.ArrayKeys))
322+
}
323+
324+
// Check boolean flags
325+
if opts.NumericStrings != tt.want.NumericStrings {
326+
t.Errorf("NumericStrings = %v, want %v", opts.NumericStrings, tt.want.NumericStrings)
327+
}
328+
if opts.BoolStrings != tt.want.BoolStrings {
329+
t.Errorf("BoolStrings = %v, want %v", opts.BoolStrings, tt.want.BoolStrings)
330+
}
331+
if opts.StableOrder != tt.want.StableOrder {
332+
t.Errorf("StableOrder = %v, want %v", opts.StableOrder, tt.want.StableOrder)
333+
}
334+
if opts.NoColor != tt.want.NoColor {
335+
t.Errorf("NoColor = %v, want %v", opts.NoColor, tt.want.NoColor)
336+
}
337+
338+
// Check string options
339+
if opts.OutputFormat != tt.want.OutputFormat {
340+
t.Errorf("OutputFormat = %v, want %v", opts.OutputFormat, tt.want.OutputFormat)
341+
}
342+
343+
// Check numeric options
344+
if opts.MaxValueLength != tt.want.MaxValueLength {
345+
t.Errorf("MaxValueLength = %v, want %v", opts.MaxValueLength, tt.want.MaxValueLength)
346+
}
347+
})
348+
}
349+
}
350+
351+
// containsAll checks if actual contains all elements from expected
352+
func containsAll(actual, expected []string) bool {
353+
if len(actual) < len(expected) {
354+
return false
355+
}
356+
expectedMap := make(map[string]bool)
357+
for _, e := range expected {
358+
expectedMap[e] = true
359+
}
360+
for _, a := range actual {
361+
if expectedMap[a] {
362+
delete(expectedMap, a)
363+
}
364+
}
365+
return len(expectedMap) == 0
366+
}

0 commit comments

Comments
 (0)