blob: 0d45df5dc71dfc136c383811ff7a09e13dfded42 [file]
// Copyright 2026 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package cmd
import (
"slices"
"strings"
"testing"
)
// TODO(adonovan): turn this into an integration test. Tests of
// internal helper functions are not robust nor do they reflect actual
// application behavior when state (e.g. flags) is involved.
//
// Also, check that the correct usage message is generated.
func TestNormalize(t *testing.T) {
tests := []struct {
name string
args []string
wantCmd string
wantGlobalArgs []string
wantArgs []string
wantErr string
}{
// ==========================================================
// Implicit & Explicit Serve
// ==========================================================
{
name: "VSCodeGo_Default",
args: []string{},
wantCmd: "serve",
},
{
name: "GlobalFlagDefaultsToServe",
args: []string{"-v"},
wantCmd: "serve",
wantGlobalArgs: []string{"-v"},
},
{
name: "VSCodeGo_RPCTrace",
args: []string{"-rpc.trace"},
wantCmd: "serve",
wantArgs: []string{"-rpc.trace"},
},
{
name: "VSCodeGo_RPCTraceServe",
args: []string{"-rpc.trace", "serve"},
wantCmd: "serve",
wantArgs: []string{"-rpc.trace"},
},
{
name: "XTools_ServeDebug",
args: []string{"serve", "-debug=localhost:6060"},
wantCmd: "serve",
wantArgs: []string{"-debug=localhost:6060"},
},
{
name: "GlobalFlagBeforeExplicitServe",
args: []string{"-otel=http://localhost:4318", "serve"},
wantCmd: "serve",
wantGlobalArgs: []string{"-otel=http://localhost:4318"},
},
// ==========================================================
// Non-Serve Subcommands
// ==========================================================
{
name: "OtherCases_Version",
args: []string{"version"},
wantCmd: "version",
},
{
name: "XTools_Help",
args: []string{"help"},
wantCmd: "help",
},
{
name: "XTools_References",
args: []string{"references", "./gopls/main.go:35:8"},
wantCmd: "references",
wantArgs: []string{"./gopls/main.go:35:8"},
},
{
name: "ReferencesWithRemoteDebugStrict",
args: []string{"references", "-remote.debug=:0", "./gopls/main.go:35:8"},
wantCmd: "references",
wantArgs: []string{"-remote.debug=:0", "./gopls/main.go:35:8"},
},
{
name: "ModernRemoteSessionsStrict_Valid",
args: []string{"remote", "sessions", "-remote=localhost:12345"},
wantCmd: "remote",
wantArgs: []string{"sessions", "-remote=localhost:12345"},
},
{
name: "ModernRemoteDebugStrict_Valid",
args: []string{"remote", "debug", "-remote=localhost:8082", "localhost:8083"},
wantCmd: "remote",
wantArgs: []string{"debug", "-remote=localhost:8082", "localhost:8083"},
},
{
name: "VSCodeGo_Vulncheck",
args: []string{"vulncheck", "--", "-mode=convert", "-show=color"},
wantCmd: "vulncheck",
wantArgs: []string{"--", "-mode=convert", "-show=color"},
},
{
name: "OtherCases_VerboseExecuteServe",
args: []string{"-v", "execute", "serve"},
wantCmd: "execute",
wantGlobalArgs: []string{"-v"},
wantArgs: []string{"serve"},
},
{
name: "OtherCases_UnknownAppFlag",
args: []string{"-nope", "execute"},
wantErr: "unknown flag: -nope",
},
// ==========================================================
// Edge cases
// ==========================================================
{
name: "AppFlagWithDoubleDash",
args: []string{"--verbose", "check", "foo.go"},
wantCmd: "check",
wantGlobalArgs: []string{"--verbose"},
wantArgs: []string{"foo.go"},
},
{
name: "AppFlagWithTripleDash",
args: []string{"---foo"},
wantErr: "unknown flag: ---foo",
},
{
name: "ServeFlagWithSpaceArg",
args: []string{"-listen", "localhost:3000"},
wantCmd: "serve",
wantArgs: []string{"-listen", "localhost:3000"},
},
{
name: "LogfileWithSpaceServe",
args: []string{"-logfile", "serve"},
wantCmd: "serve",
wantArgs: []string{"-logfile", "serve"},
},
{
name: "GlobalAndServeFlagsHoist",
args: []string{"-listen=localhost:3000", "-v"},
wantCmd: "serve",
wantGlobalArgs: []string{"-v"},
wantArgs: []string{"-listen=localhost:3000"},
},
{
name: "GlobalAndServeFlagsHoistMixed",
args: []string{"-listen", "localhost:3000", "-v"},
wantCmd: "serve",
wantGlobalArgs: []string{"-v"},
wantArgs: []string{"-listen", "localhost:3000"},
},
{
name: "DoubleDashPositional",
args: []string{"--", "foo"},
wantCmd: "serve",
wantArgs: []string{"foo"},
},
{
name: "DoubleDashFlag",
args: []string{"--", "-v"},
wantCmd: "serve",
wantArgs: []string{"-v"},
},
{
name: "UnknownFlagInServe",
args: []string{"-unknown"},
wantErr: "unknown flag: -unknown",
},
{
name: "GlobalFlagWithSpaceArgBeforeCheck",
args: []string{"-otel", "http://localhost", "check", "file.go"},
wantCmd: "check",
wantGlobalArgs: []string{"-otel", "http://localhost"},
wantArgs: []string{"file.go"},
},
{
name: "ImplicitServePositional",
args: []string{"-listen=localhost:3000", "foo"},
wantErr: `unknown command "foo"`, // consistent with gopls@v0.20.0
},
{
name: "RemoteFlagBeforeServeCompat",
args: []string{"-remote=auto", "serve"},
wantCmd: "serve",
wantGlobalArgs: []string{"-remote=auto"},
},
{
name: "RemoteFlagDefaultsToServe",
args: []string{"-remote=auto"},
wantCmd: "serve",
wantGlobalArgs: []string{"-remote=auto"},
},
{
name: "RemoteFlagsDefaultToServe",
args: []string{"-remote=auto", "-remote.debug=localhost:8080"},
wantCmd: "serve",
wantGlobalArgs: []string{"-remote=auto", "-remote.debug=localhost:8080"},
},
{
name: "RepeatedSubcommandFlags",
args: []string{"-listen", "localhost:3000", "-listen", "localhost:4000"},
wantCmd: "serve",
wantArgs: []string{"-listen", "localhost:3000", "-listen", "localhost:4000"},
},
{
name: "BooleanFlagInlineTrue",
args: []string{"-v=true"},
wantCmd: "serve",
wantGlobalArgs: []string{"-v=true"},
},
{
name: "GlobalFlagTrailingEqual",
args: []string{"-v="},
wantCmd: "serve",
wantGlobalArgs: []string{"-v="},
},
{
name: "ServeFlagTrailingEqual",
args: []string{"-listen="},
wantCmd: "serve",
wantArgs: []string{"-listen="},
},
{
name: "EmptyArgumentImplicitServe",
args: []string{""},
wantErr: `unknown command ""`,
},
{
name: "EmptyArgumentAsGlobalFlagValue",
args: []string{"-otel", "", "check"},
wantCmd: "check",
wantGlobalArgs: []string{"-otel", ""},
},
{
name: "RepeatedExplicitServeSubcommand",
args: []string{"serve", "serve"},
wantCmd: "serve",
wantArgs: []string{"serve"},
},
{
name: "GlobalHelpFlagDefaultsToServe",
args: []string{"-help"},
wantCmd: "serve",
wantGlobalArgs: []string{"-help"},
},
// ==========================================================
// Regression Tests: Reflection-based flag separation & misplaced flags
// ==========================================================
{
name: "SeparateProfileFlagFromServeFlag",
args: []string{"-profile.cpu=cpu.prof", "-listen=localhost:8080"},
wantCmd: "serve",
wantGlobalArgs: []string{"-profile.cpu=cpu.prof"},
wantArgs: []string{"-listen=localhost:8080"},
},
{
name: "SeparateVeryVerboseAndProfileFromServeFlags",
args: []string{"-vv", "-listen", "localhost:8080", "-profile.mem=mem.prof"},
wantCmd: "serve",
wantGlobalArgs: []string{"-vv", "-profile.mem=mem.prof"},
wantArgs: []string{"-listen", "localhost:8080"},
},
{
name: "MisplacedServeListenFlagBeforeCheck",
args: []string{"-listen=localhost:8080", "check", "file.go"},
wantCmd: "check",
wantErr: "flag -listen belongs to subcommand serve",
},
{
name: "MisplacedServeLogfileFlagBeforeVersion",
args: []string{"-logfile=gopls.log", "version"},
wantCmd: "version",
wantErr: "flag -logfile belongs to subcommand serve",
},
// ==========================================================
// Error Scenarios
// ==========================================================
{
name: "MissingGlobalFlagValue",
args: []string{"-otel"},
wantErr: "flag needs an argument",
},
{
name: "MissingServeFlagValue",
args: []string{"-listen"},
wantErr: "flag needs an argument",
},
{
name: "SubcommandFlagBeforeSubcommandFail",
args: []string{"-listen=:0", "references", "./gopls/main.go:35:8"},
wantCmd: "references",
wantErr: "flag -listen belongs to subcommand serve",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
app := newApplication() // Ensure test isolation
t.Logf("> gopls %v", strings.Join(tc.args, " "))
subApp, gotGlobalArgs, gotArgs, err := normalize(app, tc.args)
if tc.wantErr != "" {
if err == nil {
t.Fatalf("expected error containing %q, got nil", tc.wantErr)
}
if !strings.Contains(err.Error(), tc.wantErr) {
t.Errorf("err = %v, want error containing %q", err, tc.wantErr)
}
if tc.wantCmd != "" && subApp != nil && subApp.Name() != tc.wantCmd {
t.Errorf("normalize() cmd = %v, want %v on error", subApp.Name(), tc.wantCmd)
}
return
}
if err != nil {
t.Fatalf("dispatch failed: %v", err)
}
if subApp.Name() != tc.wantCmd {
t.Errorf("normalize() cmd = %v, want %v", subApp.Name(), tc.wantCmd)
}
if !slices.Equal(gotGlobalArgs, tc.wantGlobalArgs) {
t.Errorf("normalize() globalArgs = %v, want %v", gotGlobalArgs, tc.wantGlobalArgs)
}
if !slices.Equal(gotArgs, tc.wantArgs) {
t.Errorf("normalize() args = %v, want %v", gotArgs, tc.wantArgs)
}
})
}
}