Skip to content

Commit e907442

Browse files
RoseSecurityautofix-ci[bot]coderabbitai[bot]
authored
feat: add spinner UI to validation commands (#1951)
* feat: add spinner UI to validation commands Integrate spinner UI feedback into authentication, EditorConfig, and YAML schema validation commands. This enhances user experience by providing visual progress indicators during validation processes. * [autofix.ci] apply automated fixes * feat: output validation success for scripts/pipelines Add explicit success messages to authentication and EditorConfig validation commands for improved script and pipeline integration. Update test snapshots to reflect new output. * fix: improve error handling in validation commands Refactor error reporting in authentication and editorconfig validation commands. Now, errors are printed directly from the returned error object, and editorconfig validation only prints errors if present, otherwise logs the error. This ensures clearer and more accurate error messages for users and scripts. * chore: address godot linting Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * feat: improve EditorConfig validation UX and error handling Refactor EditorConfig validation to use the UI package for output, providing clearer success and error messages. Add specific error constants for EditorConfig validation failures, version mismatches, and file retrieval issues. Update error wrapping for better context. * refactor: use ui package for auth validate output Replaced utils markdown output with ui.Info, ui.Error, and ui.Success for consistent messaging in the auth validate command. This improves clarity and aligns output with other commands using the ui package. * feat: remove success messages from validation commands Removed final success output from authentication and editorconfig validation commands to streamline script and pipeline usage. * feat: update validation and describe config output snapshots Update test snapshots for authentication validation and configuration describe commands to reflect new output, including pager usage, validation messages, and improved error reporting. * fix(tests): expect validation messages on stderr Update test cases to expect validation success messages on stderr instead of stdout for authentication and editorconfig validation commands. This aligns test expectations with recent output changes. ``` * test: improve test robustness for env and version differences Update test cases to ignore or normalize output that varies by environment, pager settings, or tool versions (e.g., Terraform). This reduces false negatives in CI and local runs by filtering out lines and normalizing values that are not relevant to test logic. * feat: improve auth config validation test coverage Add checks for orphaned continuation lines in authentication configuration validation tests to ensure more robust error detection. * test(cmd): add unit tests for editorconfig validation logic Add comprehensive unit tests for the editorconfig validation command, including dry-run logic, version checking, and integration with atmos configuration. Tests cover config path parsing, flag overrides, and various config scenarios to ensure robust behavior. --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
1 parent e483668 commit e907442

File tree

31 files changed

+333
-58
lines changed

31 files changed

+333
-58
lines changed

cmd/auth_validate.go

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ import (
1010
cfg "github.com/cloudposse/atmos/pkg/config"
1111
log "github.com/cloudposse/atmos/pkg/logger"
1212
"github.com/cloudposse/atmos/pkg/schema"
13-
u "github.com/cloudposse/atmos/pkg/utils"
13+
"github.com/cloudposse/atmos/pkg/ui"
14+
"github.com/cloudposse/atmos/pkg/ui/spinner"
1415
)
1516

1617
// authValidateCmd validates the auth configuration.
@@ -28,7 +29,7 @@ func executeAuthValidateCommand(cmd *cobra.Command, args []string) error {
2829
// Get verbose flag
2930
verbose := viper.GetBool("auth.validate.verbose")
3031
if verbose {
31-
u.PrintfMarkdown("**Validating authentication configuration...**\n")
32+
ui.Info("Validating authentication configuration...")
3233
}
3334

3435
// Load atmos config
@@ -40,14 +41,19 @@ func executeAuthValidateCommand(cmd *cobra.Command, args []string) error {
4041
// Create validator
4142
validator := validation.NewValidator()
4243

43-
// Validate auth configuration
44-
if err := validator.ValidateAuthConfig(&atmosConfig.Auth); err != nil {
45-
u.PrintfMarkdown("**❌ Authentication configuration validation failed:**\n")
46-
u.PrintfMarkdown("%s\n", err.Error())
44+
// Validate auth configuration with spinner.
45+
err = spinner.ExecWithSpinner(
46+
"Validating authentication configuration...",
47+
"Authentication configuration is valid",
48+
func() error {
49+
return validator.ValidateAuthConfig(&atmosConfig.Auth)
50+
},
51+
)
52+
if err != nil {
53+
ui.Error(fmt.Sprintf("Authentication configuration validation failed: %v", err))
4754
return err
4855
}
4956

50-
u.PrintfMarkdown("**✅ Authentication configuration is valid**\n")
5157
return nil
5258
}
5359

cmd/validate_editorconfig.go

Lines changed: 33 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ import (
1515
errUtils "github.com/cloudposse/atmos/errors"
1616
log "github.com/cloudposse/atmos/pkg/logger"
1717
"github.com/cloudposse/atmos/pkg/schema"
18+
"github.com/cloudposse/atmos/pkg/ui"
19+
"github.com/cloudposse/atmos/pkg/ui/spinner"
1820
u "github.com/cloudposse/atmos/pkg/utils"
1921
"github.com/cloudposse/atmos/pkg/version"
2022
)
@@ -163,33 +165,53 @@ func runMainLogic() {
163165
errUtils.CheckErrorPrintAndExit(err, "", "")
164166
}
165167

166-
filePaths, err := files.GetFiles(config)
167-
errUtils.CheckErrorPrintAndExit(err, "", "")
168-
168+
// Dry-run mode - just list files without spinner.
169169
if config.DryRun {
170+
filePaths, err := files.GetFiles(config)
171+
errUtils.CheckErrorPrintAndExit(err, "", "")
170172
for _, file := range filePaths {
171173
log.Info(file)
172174
}
173175
return
174176
}
175177

176-
errors := validation.ProcessValidation(filePaths, config)
177-
log.Debug("Files checked", "count", len(filePaths))
178-
errorCount := er.GetErrorCount(errors)
179-
if errorCount != 0 {
180-
er.PrintErrors(errors, config)
178+
var filePaths []string
179+
var validationErrors []er.ValidationErrors
180+
181+
err := spinner.ExecWithSpinner(
182+
"Validating EditorConfig...",
183+
"EditorConfig validation passed",
184+
func() error {
185+
var err error
186+
filePaths, err = files.GetFiles(config)
187+
if err != nil {
188+
return fmt.Errorf(errUtils.ErrWrapFormat, errUtils.ErrEditorConfigGetFiles, err)
189+
}
190+
validationErrors = validation.ProcessValidation(filePaths, config)
191+
log.Debug("Files checked", "count", len(filePaths))
192+
if er.GetErrorCount(validationErrors) != 0 {
193+
return errUtils.ErrEditorConfigValidationFailed
194+
}
195+
return nil
196+
},
197+
)
198+
if err != nil {
199+
if len(validationErrors) > 0 {
200+
er.PrintErrors(validationErrors, config)
201+
} else {
202+
ui.Error(fmt.Sprintf("Validation failed: %v", err))
203+
}
181204
errUtils.Exit(1)
182205
}
183-
u.PrintMessage("No errors found")
184206
}
185207

186208
func checkVersion(config config.Config) error {
187209
if !utils.FileExists(config.Path) || config.Version == "" {
188210
return nil
189211
}
190212
if config.Version != version.Version {
191-
return fmt.Errorf("version mismatch: binary=%s, config=%s",
192-
version.Version, config.Version)
213+
return fmt.Errorf("%w: binary=%s, config=%s",
214+
errUtils.ErrEditorConfigVersionMismatch, version.Version, config.Version)
193215
}
194216

195217
return nil

cmd/validate_editorconfig_test.go

Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,12 @@ package cmd
33
import (
44
"testing"
55

6+
"github.com/editorconfig-checker/editorconfig-checker/v3/pkg/config"
67
"github.com/spf13/cobra"
78
"github.com/stretchr/testify/assert"
89
"github.com/stretchr/testify/require"
10+
11+
"github.com/cloudposse/atmos/pkg/schema"
912
)
1013

1114
// TestParseConfigPaths tests the pure parseConfigPaths function.
@@ -70,3 +73,213 @@ func TestInitConfig(t *testing.T) {
7073
// Call function with no assertions - test passes if no panic occurs.
7174
initializeConfig(editorConfigCmd)
7275
}
76+
77+
// TestRunMainLogicDryRun tests the dry-run path in runMainLogic.
78+
func TestRunMainLogicDryRun(t *testing.T) {
79+
// Save original state.
80+
originalConfig := currentConfig
81+
originalCliConfig := cliConfig
82+
defer func() {
83+
currentConfig = originalConfig
84+
cliConfig = originalCliConfig
85+
}()
86+
87+
// Create a minimal config for dry-run mode.
88+
cfg := config.NewConfig([]string{})
89+
cfg.DryRun = true
90+
currentConfig = cfg
91+
cliConfig = config.Config{DryRun: true}
92+
93+
// This should not panic and should list files (if any match).
94+
// In dry-run mode, runMainLogic just lists files without validation.
95+
runMainLogic()
96+
}
97+
98+
// TestCheckVersion tests the version checking logic.
99+
func TestCheckVersion(t *testing.T) {
100+
tests := []struct {
101+
name string
102+
config config.Config
103+
expectError bool
104+
}{
105+
{
106+
name: "no config file exists",
107+
config: config.Config{
108+
Path: "/nonexistent/path",
109+
Version: "",
110+
},
111+
expectError: false,
112+
},
113+
{
114+
name: "empty version in config",
115+
config: config.Config{
116+
Path: ".",
117+
Version: "",
118+
},
119+
expectError: false,
120+
},
121+
{
122+
name: "version mismatch",
123+
config: config.Config{
124+
Path: ".", // Current directory exists
125+
Version: "999.999.999",
126+
},
127+
expectError: true,
128+
},
129+
}
130+
131+
for _, tt := range tests {
132+
t.Run(tt.name, func(t *testing.T) {
133+
err := checkVersion(tt.config)
134+
if tt.expectError {
135+
assert.Error(t, err)
136+
} else {
137+
assert.NoError(t, err)
138+
}
139+
})
140+
}
141+
}
142+
143+
// TestReplaceAtmosConfigInConfig tests that atmos config values are properly applied.
144+
func TestReplaceAtmosConfigInConfig(t *testing.T) {
145+
// Reset module-level variables before test.
146+
originalConfigFilePaths := configFilePaths
147+
originalTmpExclude := tmpExclude
148+
originalInitEditorConfig := initEditorConfig
149+
originalCliConfig := cliConfig
150+
defer func() {
151+
configFilePaths = originalConfigFilePaths
152+
tmpExclude = originalTmpExclude
153+
initEditorConfig = originalInitEditorConfig
154+
cliConfig = originalCliConfig
155+
}()
156+
157+
tests := []struct {
158+
name string
159+
atmosConfig schema.AtmosConfiguration
160+
flagChanged map[string]bool
161+
setup func(*cobra.Command)
162+
validate func(t *testing.T)
163+
}{
164+
{
165+
name: "applies config file paths from atmos config",
166+
atmosConfig: schema.AtmosConfiguration{
167+
Validate: schema.Validate{
168+
EditorConfig: schema.EditorConfig{
169+
ConfigFilePaths: []string{".custom-editorconfig"},
170+
},
171+
},
172+
},
173+
validate: func(t *testing.T) {
174+
assert.Equal(t, []string{".custom-editorconfig"}, configFilePaths)
175+
},
176+
},
177+
{
178+
name: "applies exclude patterns from atmos config",
179+
atmosConfig: schema.AtmosConfiguration{
180+
Validate: schema.Validate{
181+
EditorConfig: schema.EditorConfig{
182+
Exclude: []string{"vendor/**", "node_modules/**"},
183+
},
184+
},
185+
},
186+
validate: func(t *testing.T) {
187+
assert.Equal(t, "vendor/**,node_modules/**", tmpExclude)
188+
},
189+
},
190+
{
191+
name: "applies init flag from atmos config",
192+
atmosConfig: schema.AtmosConfiguration{
193+
Validate: schema.Validate{
194+
EditorConfig: schema.EditorConfig{
195+
Init: true,
196+
},
197+
},
198+
},
199+
validate: func(t *testing.T) {
200+
assert.True(t, initEditorConfig)
201+
},
202+
},
203+
{
204+
name: "applies ignore defaults from atmos config",
205+
atmosConfig: schema.AtmosConfiguration{
206+
Validate: schema.Validate{
207+
EditorConfig: schema.EditorConfig{
208+
IgnoreDefaults: true,
209+
},
210+
},
211+
},
212+
validate: func(t *testing.T) {
213+
assert.True(t, cliConfig.IgnoreDefaults)
214+
},
215+
},
216+
{
217+
name: "applies dry run from atmos config",
218+
atmosConfig: schema.AtmosConfiguration{
219+
Validate: schema.Validate{
220+
EditorConfig: schema.EditorConfig{
221+
DryRun: true,
222+
},
223+
},
224+
},
225+
validate: func(t *testing.T) {
226+
assert.True(t, cliConfig.DryRun)
227+
},
228+
},
229+
{
230+
name: "applies disable flags from atmos config",
231+
atmosConfig: schema.AtmosConfiguration{
232+
Validate: schema.Validate{
233+
EditorConfig: schema.EditorConfig{
234+
DisableTrimTrailingWhitespace: true,
235+
DisableEndOfLine: true,
236+
DisableInsertFinalNewline: true,
237+
DisableIndentation: true,
238+
DisableIndentSize: true,
239+
DisableMaxLineLength: true,
240+
},
241+
},
242+
},
243+
validate: func(t *testing.T) {
244+
assert.True(t, cliConfig.Disable.TrimTrailingWhitespace)
245+
assert.True(t, cliConfig.Disable.EndOfLine)
246+
assert.True(t, cliConfig.Disable.InsertFinalNewline)
247+
assert.True(t, cliConfig.Disable.Indentation)
248+
assert.True(t, cliConfig.Disable.IndentSize)
249+
assert.True(t, cliConfig.Disable.MaxLineLength)
250+
},
251+
},
252+
{
253+
name: "applies no color from atmos config",
254+
atmosConfig: schema.AtmosConfiguration{
255+
Settings: schema.AtmosSettings{
256+
Terminal: schema.Terminal{
257+
NoColor: true,
258+
},
259+
},
260+
},
261+
validate: func(t *testing.T) {
262+
assert.True(t, cliConfig.NoColor)
263+
},
264+
},
265+
}
266+
267+
for _, tt := range tests {
268+
t.Run(tt.name, func(t *testing.T) {
269+
// Reset state.
270+
configFilePaths = nil
271+
tmpExclude = ""
272+
initEditorConfig = false
273+
274+
cmd := &cobra.Command{}
275+
addPersistentFlags(cmd)
276+
277+
if tt.setup != nil {
278+
tt.setup(cmd)
279+
}
280+
281+
replaceAtmosConfigInConfig(cmd, tt.atmosConfig)
282+
tt.validate(t)
283+
})
284+
}
285+
}

errors/errors.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -408,6 +408,11 @@ var (
408408
// Validation errors.
409409
ErrValidationFailed = errors.New("validation failed")
410410

411+
// EditorConfig validation errors.
412+
ErrEditorConfigValidationFailed = errors.New("EditorConfig validation failed")
413+
ErrEditorConfigVersionMismatch = errors.New("EditorConfig version mismatch")
414+
ErrEditorConfigGetFiles = errors.New("failed to get files for EditorConfig validation")
415+
411416
// Global/Stack-level section errors.
412417
ErrInvalidVarsSection = errors.New("invalid vars section")
413418
ErrInvalidSettingsSection = errors.New("invalid settings section")

0 commit comments

Comments
 (0)