-
Notifications
You must be signed in to change notification settings - Fork 12
Implement Cobra v1.10.0 improvements: context support, declarative flag validation, and lifecycle hooks #882
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,11 +2,14 @@ package cmd | |
|
|
||
| import ( | ||
| "bytes" | ||
| "context" | ||
| "encoding/json" | ||
| "os" | ||
| "path/filepath" | ||
| "testing" | ||
| "time" | ||
|
|
||
| "github.com/spf13/cobra" | ||
| "github.com/stretchr/testify/assert" | ||
| "github.com/stretchr/testify/require" | ||
|
|
||
|
|
@@ -77,13 +80,9 @@ func TestRunRequiresConfigSource(t *testing.T) { | |
| configStdin = origConfigStdin | ||
| }) | ||
|
|
||
| t.Run("no config source provided", func(t *testing.T) { | ||
| configFile = "" | ||
| configStdin = false | ||
| err := preRun(nil, nil) | ||
| require.Error(t, err, "Expected error when neither --config nor --config-stdin is provided") | ||
| assert.Contains(t, err.Error(), "configuration source required", "Error should mention configuration source required") | ||
| }) | ||
| // Note: The validation for "one of config or config-stdin is required" is now | ||
| // handled by Cobra's MarkFlagsOneRequired, which validates at command execution time, | ||
| // not in preRun. Therefore, preRun should pass validation as long as at least one is set. | ||
|
|
||
| t.Run("config file provided", func(t *testing.T) { | ||
| configFile = "test.toml" | ||
|
|
@@ -138,14 +137,8 @@ func TestPreRunValidation(t *testing.T) { | |
| assert.NoError(t, err) | ||
| }) | ||
|
|
||
| t.Run("validation fails without config source", func(t *testing.T) { | ||
| configFile = "" | ||
| configStdin = false | ||
| verbosity = 0 | ||
| err := preRun(nil, nil) | ||
| require.Error(t, err) | ||
| assert.Contains(t, err.Error(), "configuration source required") | ||
| }) | ||
| // Note: validation for "one of config or config-stdin is required" is now | ||
| // handled by Cobra's MarkFlagsOneRequired, so preRun doesn't check this anymore | ||
|
|
||
| t.Run("verbosity level 1 does not set DEBUG", func(t *testing.T) { | ||
| // Save and clear DEBUG env var | ||
|
|
@@ -499,3 +492,66 @@ func TestWriteGatewayConfig(t *testing.T) { | |
| assert.Contains(t, output, DefaultListenPort) | ||
| }) | ||
| } | ||
|
|
||
| // TestContextCancellation tests that context cancellation works properly | ||
| func TestContextCancellation(t *testing.T) { | ||
| t.Run("context with timeout", func(t *testing.T) { | ||
| ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond) | ||
| defer cancel() | ||
|
Comment on lines
+496
to
+500
|
||
|
|
||
| // Wait for context to be done | ||
| <-ctx.Done() | ||
|
|
||
| // Verify context was cancelled due to timeout | ||
| assert.Equal(t, context.DeadlineExceeded, ctx.Err()) | ||
| }) | ||
|
|
||
| t.Run("context with cancellation", func(t *testing.T) { | ||
| ctx, cancel := context.WithCancel(context.Background()) | ||
|
|
||
| // Cancel immediately | ||
| cancel() | ||
|
|
||
| // Wait for context to be done | ||
| <-ctx.Done() | ||
|
|
||
| // Verify context was cancelled | ||
| assert.Equal(t, context.Canceled, ctx.Err()) | ||
| }) | ||
| } | ||
|
|
||
| // TestFlagValidationGroups tests that flag validation groups work correctly | ||
| func TestFlagValidationGroups(t *testing.T) { | ||
| // Note: This tests that the flag validation groups are registered correctly. | ||
| // Actual validation is performed by Cobra during command execution. | ||
| t.Run("mutually exclusive flags registered", func(t *testing.T) { | ||
| // Create a new root command to test | ||
| cmd := &cobra.Command{ | ||
| Use: "test", | ||
| } | ||
| registerCoreFlags(cmd) | ||
|
|
||
| // Verify flags are registered | ||
| assert.NotNil(t, cmd.Flags().Lookup("routed")) | ||
| assert.NotNil(t, cmd.Flags().Lookup("unified")) | ||
| assert.NotNil(t, cmd.Flags().Lookup("config")) | ||
| assert.NotNil(t, cmd.Flags().Lookup("config-stdin")) | ||
| }) | ||
| } | ||
|
|
||
| // TestVersionTemplate tests that custom version template is set | ||
| func TestVersionTemplate(t *testing.T) { | ||
| t.Run("version template is set", func(t *testing.T) { | ||
| // The version template should be set during init | ||
| // We can verify the version command works by checking it's not empty | ||
| assert.NotEmpty(t, rootCmd.Version, "Version should be set") | ||
| }) | ||
| } | ||
|
|
||
| // TestPostRunCleanup tests that postRun cleanup is called | ||
| func TestPostRunCleanup(t *testing.T) { | ||
| t.Run("postRun is registered", func(t *testing.T) { | ||
| // Verify that postRun hook is set | ||
| assert.NotNil(t, rootCmd.PersistentPostRun, "PersistentPostRun should be set") | ||
| }) | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -502,14 +502,15 @@ func TestBinaryInvocation_NoConfigRequired(t *testing.T) { | |||||
| require.Error(t, err) | ||||||
|
|
||||||
| outputStr := string(output) | ||||||
| // Should contain the error message about requiring config | ||||||
| if !bytes.Contains(output, []byte("configuration source required")) { | ||||||
| t.Errorf("Expected 'configuration source required' error message, got: %s", outputStr) | ||||||
| // Should contain the error message about requiring at least one of the flags | ||||||
| // Note: Cobra's MarkFlagsOneRequired produces a different error message than manual validation | ||||||
| if !bytes.Contains(output, []byte("at least one of the flags in the group [config config-stdin] is required")) { | ||||||
| t.Errorf("Expected 'at least one of the flags in the group [config config-stdin] is required' error message, got: %s", outputStr) | ||||||
| } | ||||||
|
|
||||||
| // Should mention both --config and --config-stdin | ||||||
| if !bytes.Contains(output, []byte("--config")) || !bytes.Contains(output, []byte("--config-stdin")) { | ||||||
| t.Errorf("Expected error message to mention both --config and --config-stdin, got: %s", outputStr) | ||||||
| if !bytes.Contains(output, []byte("config")) || !bytes.Contains(output, []byte("config-stdin")) { | ||||||
|
||||||
| if !bytes.Contains(output, []byte("config")) || !bytes.Contains(output, []byte("config-stdin")) { | |
| if !bytes.Contains(output, []byte("--config")) || !bytes.Contains(output, []byte("--config-stdin")) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
unifiedServer.Close()is called both viadefer unifiedServer.Close()and again inside the shutdown goroutine.UnifiedServer.Close()delegates tolauncher.Close(), which callssessionPool.Stop()and will block up to 1s on subsequent calls, so this can introduce an unnecessary delay and makes shutdown behavior harder to reason about. Prefer ensuring shutdown happens exactly once (e.g., remove the goroutine call and rely on the deferred Close, or switch to an idempotent shutdown method such asInitiateShutdown()guarded bysync.Once).