diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e3972f57..43aa3da0 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -28,6 +28,7 @@ jobs: uses: codecov/codecov-action@v4 with: token: ${{ secrets.CODECOV_TOKEN }} + disable_search: true file: ./coverage-cmd.out flags: cmd fail_ci_if_error: true @@ -36,6 +37,7 @@ jobs: uses: codecov/codecov-action@v4 with: token: ${{ secrets.CODECOV_TOKEN }} + disable_search: true file: ./coverage-module.out flags: module fail_ci_if_error: true diff --git a/cmd/scenarigo/cmd/plugin/build.go b/cmd/scenarigo/cmd/plugin/build.go index fc7760ea..d81cc8c2 100644 --- a/cmd/scenarigo/cmd/plugin/build.go +++ b/cmd/scenarigo/cmd/plugin/build.go @@ -6,6 +6,8 @@ import ( "errors" "fmt" "go/build" + goversion "go/version" + "io" "io/fs" "os" "os/exec" @@ -34,43 +36,59 @@ const ( var ( goVer string toolchain string - tip bool goMinVer string versionTooHighErrorRegexp *regexp.Regexp ) func init() { - goVer = runtime.Version() - toolchain = goVer - if strings.HasPrefix(goVer, "devel ") { - // gotip - goVer = strings.Split(strings.TrimPrefix(goVer, "devel "), "-")[0] - toolchain = "local" - tip = true - } - e := strings.Split(strings.TrimPrefix(goVer, "go"), ".") - if len(e) < 2 { - panic(fmt.Sprintf("%q is invalid Go version", goVer)) - } + goVer, toolchain = parseGoVersion(runtime.Version()) goMinVer = "1.21.2" versionTooHighErrorRegexp = regexp.MustCompile(versionTooHighErrorPattern) } -var buildCmd = &cobra.Command{ - Use: "build", - Short: "build plugins", - Long: strings.Trim(` +func parseGoVersion(ver string) (string, string) { + tc := ver + // gotip + if strings.HasPrefix(ver, "devel ") { + ver = strings.Split(strings.TrimPrefix(ver, "devel "), "-")[0] + tc = "local" + } + // go installed with homebrew (e.g., go1.23.2 X:rangefunc) + if !goversion.IsValid(ver) { + if v := strings.Split(ver, " ")[0]; goversion.IsValid(v) { + ver = v + tc = v + } else { + tc = "local" + } + } + return ver, tc +} + +var verbose bool + +func newBuildCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "build", + Short: "build plugins", + Long: strings.Trim(` Builds plugins based on the configuration file. This command requires go command in $PATH. `, "\n"), - Args: cobra.ExactArgs(0), - RunE: buildRun, - SilenceErrors: true, - SilenceUsage: true, + Args: cobra.ExactArgs(0), + RunE: buildRun, + SilenceErrors: true, + SilenceUsage: true, + } + cmd.Flags().BoolVarP(&verbose, "verbose", "v", false, "print verbose log") + return cmd } -var warnColor = color.New(color.Bold, color.FgYellow) +var ( + warnColor = color.New(color.Bold, color.FgYellow) + debugColor = color.New(color.Bold) +) type retriableError struct { reason string @@ -103,6 +121,12 @@ func (o *overrideModule) requireReplace() (*modfile.Require, string, *modfile.Re } func buildRun(cmd *cobra.Command, args []string) error { + runtimeVersion := runtime.Version() + debugLog(cmd.OutOrStderr(), "scenarigo was built with %s", runtimeVersion) + if !goversion.IsValid(goVer) { + warnLog(cmd.OutOrStderr(), "failed to parse the Go version that built scenarigo: %s", runtimeVersion) + } + cfg, err := config.Load() if err != nil { return fmt.Errorf("failed to load config: %w", err) @@ -115,6 +139,8 @@ func buildRun(cmd *cobra.Command, args []string) error { if err != nil { return err } + debugLog(cmd.OutOrStderr(), "found go command: %s", goCmd) + debugLog(cmd.OutOrStderr(), "set GOTOOLCHAIN=%s", toolchain) pbs := make([]*pluginBuilder, 0, cfg.Plugins.Len()) pluginModules := map[string]*overrideModule{} @@ -506,11 +532,7 @@ func executeWithEnvs(ctx context.Context, envs []string, wd, name string, args . var stdout bytes.Buffer var stderr bytes.Buffer cmd := exec.CommandContext(ctx, name, args...) - if tip { - envs = append(envs, "GOTOOLCHAIN=local") - } else { - envs = append(envs, fmt.Sprintf("GOTOOLCHAIN=%s", goVer)) - } + envs = append(envs, fmt.Sprintf("GOTOOLCHAIN=%s", toolchain)) cmd.Env = append(os.Environ(), envs...) if wd != "" { cmd.Dir = wd @@ -525,18 +547,26 @@ func executeWithEnvs(ctx context.Context, envs []string, wd, name string, args . func (pb *pluginBuilder) updateGoMod(cmd *cobra.Command, goCmd string, overrideKeys []string, overrides map[string]*overrideModule) error { if err := pb.editGoMod(cmd, goCmd, func(gomod *modfile.File) error { - switch compareVers(gomod.Go.Version, goVer) { + if toolchain == "local" { + if gomod.Toolchain != nil { + warnLog(cmd.OutOrStdout(), "%s: remove toolchain by scenarigo", pb.name) + gomod.DropToolchainStmt() + } + return nil + } + + switch compareVers(gomod.Go.Version, toolchain) { case -1: // go.mod < scenarigo go version if gomod.Toolchain == nil { - fmt.Fprintf(cmd.OutOrStdout(), "%s: %s: add toolchain %s by scenarigo\n", warnColor.Sprint("WARN"), pb.name, goVer) - } else if gomod.Toolchain.Name != goVer { - fmt.Fprintf(cmd.OutOrStdout(), "%s: %s: change toolchain %s ==> %s by scenarigo\n", warnColor.Sprint("WARN"), pb.name, gomod.Toolchain.Name, goVer) + warnLog(cmd.OutOrStdout(), "%s: add toolchain %s by scenarigo", pb.name, toolchain) + } else if gomod.Toolchain.Name != toolchain { + warnLog(cmd.OutOrStdout(), "%s: change toolchain %s ==> %s by scenarigo", pb.name, gomod.Toolchain.Name, toolchain) } - if err := gomod.AddToolchainStmt(goVer); err != nil { + if err := gomod.AddToolchainStmt(toolchain); err != nil { return fmt.Errorf("%s: %w", pb.gomodPath, err) } case 1: // go.mod > scenarigo go version - return fmt.Errorf("%s: go: go.mod requires go >= %s (running go %s)", pb.gomodPath, gomod.Go.Version, goVer) + return fmt.Errorf("%s: go: go.mod requires go >= %s (scenarigo was built with %s)", pb.gomodPath, gomod.Go.Version, toolchain) } return nil }); err != nil { @@ -692,22 +722,22 @@ func printUpdatedRequires(cmd *cobra.Command, name string, overrides map[string] if !diff.new.Indirect { if o := overrides[k]; o != nil { _, requiredBy, _, _ := o.requireReplace() - fmt.Fprintf(cmd.OutOrStdout(), "%s: %s: add require %s %s by %s\n", warnColor.Sprint("WARN"), name, k, diff.new.Mod.Version, requiredBy) + warnLog(cmd.OutOrStdout(), "%s: add require %s %s by %s", name, k, diff.new.Mod.Version, requiredBy) } else { - fmt.Fprintf(cmd.OutOrStdout(), "%s: %s: add require %s %s\n", warnColor.Sprint("WARN"), name, k, diff.new.Mod.Version) + warnLog(cmd.OutOrStdout(), "%s: add require %s %s", name, k, diff.new.Mod.Version) } } case diff.new.Mod.Path == "": if !diff.old.Indirect { - fmt.Fprintf(cmd.OutOrStdout(), "%s: %s: remove require %s %s\n", warnColor.Sprint("WARN"), name, k, diff.old.Mod.Version) + warnLog(cmd.OutOrStdout(), "%s: remove require %s %s", name, k, diff.old.Mod.Version) } case diff.old.Mod.Version != diff.new.Mod.Version: if !diff.old.Indirect || !diff.new.Indirect { if o := overrides[k]; o != nil { _, requiredBy, _, _ := o.requireReplace() - fmt.Fprintf(cmd.OutOrStdout(), "%s: %s: change require %s %s ==> %s by %s\n", warnColor.Sprint("WARN"), name, k, diff.old.Mod.Version, diff.new.Mod.Version, requiredBy) + warnLog(cmd.OutOrStdout(), "%s: change require %s %s ==> %s by %s", name, k, diff.old.Mod.Version, diff.new.Mod.Version, requiredBy) } else { - fmt.Fprintf(cmd.OutOrStdout(), "%s: %s: change require %s %s ==> %s\n", warnColor.Sprint("WARN"), name, k, diff.old.Mod.Version, diff.new.Mod.Version) + warnLog(cmd.OutOrStdout(), "%s: change require %s %s ==> %s", name, k, diff.old.Mod.Version, diff.new.Mod.Version) } } } @@ -745,21 +775,21 @@ func printUpdatedReplaces(cmd *cobra.Command, name string, overrides map[string] if replace != nil { by = replaceBy } - fmt.Fprintf(cmd.OutOrStdout(), "%s: %s: add replace %s => %s by %s\n", warnColor.Sprint("WARN"), name, replacePathVersion(k, diff.new.Old.Version), replacePathVersion(diff.new.New.Path, diff.new.New.Version), by) + warnLog(cmd.OutOrStdout(), "%s: add replace %s => %s by %s", name, replacePathVersion(k, diff.new.Old.Version), replacePathVersion(diff.new.New.Path, diff.new.New.Version), by) } else { - fmt.Fprintf(cmd.OutOrStdout(), "%s: %s: add replace %s => %s\n", warnColor.Sprint("WARN"), name, replacePathVersion(k, diff.new.Old.Version), replacePathVersion(diff.new.New.Path, diff.new.New.Version)) + warnLog(cmd.OutOrStdout(), "%s: add replace %s => %s", name, replacePathVersion(k, diff.new.Old.Version), replacePathVersion(diff.new.New.Path, diff.new.New.Version)) } case diff.new.Old.Path == "": - fmt.Fprintf(cmd.OutOrStdout(), "%s: %s: remove replace %s => %s\n", warnColor.Sprint("WARN"), name, replacePathVersion(k, diff.old.Old.Version), replacePathVersion(diff.old.New.Path, diff.old.New.Version)) + warnLog(cmd.OutOrStdout(), "%s: remove replace %s => %s", name, replacePathVersion(k, diff.old.Old.Version), replacePathVersion(diff.old.New.Path, diff.old.New.Version)) case diff.old.New.Path != diff.new.New.Path || diff.old.New.Version != diff.new.New.Version: if o := overrides[k]; o != nil { _, by, replace, replaceBy := o.requireReplace() if replace != nil { by = replaceBy } - fmt.Fprintf(cmd.OutOrStdout(), "%s: %s: change replace %s => %s ==> %s => %s by %s\n", warnColor.Sprint("WARN"), name, replacePathVersion(k, diff.old.Old.Version), replacePathVersion(diff.old.New.Path, diff.old.New.Version), replacePathVersion(k, diff.new.Old.Version), replacePathVersion(diff.new.New.Path, diff.new.New.Version), by) + warnLog(cmd.OutOrStdout(), "%s: change replace %s => %s ==> %s => %s by %s", name, replacePathVersion(k, diff.old.Old.Version), replacePathVersion(diff.old.New.Path, diff.old.New.Version), replacePathVersion(k, diff.new.Old.Version), replacePathVersion(diff.new.New.Path, diff.new.New.Version), by) } else { - fmt.Fprintf(cmd.OutOrStdout(), "%s: %s: change replace %s => %s ==> %s => %s\n", warnColor.Sprint("WARN"), name, replacePathVersion(k, diff.old.Old.Version), replacePathVersion(diff.old.New.Path, diff.old.New.Version), replacePathVersion(k, diff.new.Old.Version), replacePathVersion(diff.new.New.Path, diff.new.New.Version)) + warnLog(cmd.OutOrStdout(), "%s: change replace %s => %s ==> %s => %s", name, replacePathVersion(k, diff.old.Old.Version), replacePathVersion(diff.old.New.Path, diff.old.New.Version), replacePathVersion(k, diff.new.Old.Version), replacePathVersion(diff.new.New.Path, diff.new.New.Version)) } } } @@ -889,3 +919,14 @@ func asVersionTooHighError(err error) (bool, *versionTooHighError) { } return false, nil } + +func warnLog(w io.Writer, format string, a ...any) { + fmt.Fprintf(w, fmt.Sprintf("%s: %s\n", warnColor.Sprint("WARN"), format), a...) +} + +func debugLog(w io.Writer, format string, a ...any) { + if !verbose { + return + } + fmt.Fprintf(w, fmt.Sprintf("%s: %s\n", debugColor.Sprint("DEBUG"), format), a...) +} diff --git a/cmd/scenarigo/cmd/plugin/build_test.go b/cmd/scenarigo/cmd/plugin/build_test.go index baff2b9c..90cfd97d 100644 --- a/cmd/scenarigo/cmd/plugin/build_test.go +++ b/cmd/scenarigo/cmd/plugin/build_test.go @@ -32,6 +32,7 @@ import ( var ( bash string echo string + _ = newBuildCmd() ) func init() { @@ -46,6 +47,47 @@ func init() { } } +func TestParseGoVersion(t *testing.T) { + tests := map[string]struct { + version string + expect string + expectToolchain string + }{ + "go1.2.3": { + version: "go1.2.3", + expect: "go1.2.3", + expectToolchain: "go1.2.3", + }, + "devel go1.24-76f320836": { + version: "devel go1.24-76f320836", + expect: "go1.24", + expectToolchain: "local", + }, + "go1.23.2 X:rangefunc": { + version: "go1.23.2 X:rangefunc", + expect: "go1.23.2", + expectToolchain: "go1.23.2", + }, + "invalid": { + version: "invalid", + expect: "invalid", + expectToolchain: "local", + }, + } + for name, test := range tests { + test := test + t.Run(name, func(t *testing.T) { + got, gotToolchain := parseGoVersion(test.version) + if got != test.expect { + t.Errorf("expect %s but got %s", test.expect, got) + } + if gotToolchain != test.expectToolchain { + t.Errorf("expect toolchain %s but got %s", test.expectToolchain, gotToolchain) + } + }) + } +} + func TestBuild(t *testing.T) { goVersion := strings.TrimPrefix(goVer, "go") pluginCode := `package main @@ -1200,6 +1242,11 @@ func TestFindGoCmd(t *testing.T) { func TestUpdateGoMod(t *testing.T) { goVersion := strings.TrimPrefix(goVer, "go") + gomodToolchain := toolchain + if toolchain == "local" { + gomodToolchain = "default" + } + t.Run("success", func(t *testing.T) { tests := map[string]struct { gomod string @@ -1227,9 +1274,12 @@ go 1.21 go 1.21 -toolchain go%s -`, goVersion), - expectStdout: fmt.Sprintf("WARN: test.so: add toolchain go%s by scenarigo\n", goVersion), +toolchain %s +`, toolchain), + expectStdout: tipOut( + "", + fmt.Sprintf("WARN: test.so: add toolchain %s by scenarigo\n", gomodToolchain), + ), }, "change toolchain directive": { gomod: `module plugin_module @@ -1242,9 +1292,12 @@ toolchain go1.21.1 go 1.21 -toolchain go%s -`, goVersion), - expectStdout: fmt.Sprintf("WARN: test.so: change toolchain go1.21.1 ==> go%s by scenarigo\n", goVersion), +toolchain %s +`, toolchain), + expectStdout: tipOut( + "WARN: test.so: remove toolchain by scenarigo\n", + fmt.Sprintf("WARN: test.so: change toolchain go1.21.1 ==> %s by scenarigo\n", gomodToolchain), + ), }, "do nothing (no requires)": { gomod: fmt.Sprintf(`module plugin_module @@ -1815,6 +1868,9 @@ replace google.golang.org/grpc v1.46.0 => google.golang.org/grpc v1.40.0 if err != nil { t.Fatalf("failed read go.mod: %s", err) } + if toolchain == "local" { + test.expect = strings.ReplaceAll(test.expect, "\ntoolchain local\n", "") + } if got := string(b); got != test.expect { dmp := diffmatchpatch.New() diffs := dmp.DiffMain(test.expect, got, false) @@ -1986,6 +2042,11 @@ import ( }) t.Run("failure", func(t *testing.T) { + tooHighError := fmt.Sprintf("go.mod requires go >= 100.0.0 (scenarigo was built with %s)", goVer) + if toolchain == "local" { + tooHighError = fmt.Sprintf(`failed to edit toolchain directive: "go mod tidy" failed: go: go.mod requires go >= 100.0.0 (running go %s; GOTOOLCHAIN=local)`, strings.TrimPrefix(goVer, "go")) + } + tests := map[string]struct { gomod string src string @@ -1997,7 +2058,7 @@ import ( go 100.0.0 `, - expect: fmt.Sprintf("go.mod requires go >= 100.0.0 (running go %s)", goVer), + expect: tooHighError, }, } for name, test := range tests { @@ -2179,3 +2240,10 @@ func createExecutable(t *testing.T, path, stdout string) { t.Fatalf("failed to write %s: %s", path, err) } } + +func tipOut(tip, s string) string { + if toolchain == "local" { + return tip + } + return s +} diff --git a/cmd/scenarigo/cmd/plugin/plugin.go b/cmd/scenarigo/cmd/plugin/plugin.go index 72daa925..fa6c9195 100644 --- a/cmd/scenarigo/cmd/plugin/plugin.go +++ b/cmd/scenarigo/cmd/plugin/plugin.go @@ -3,5 +3,5 @@ package plugin import "github.com/spf13/cobra" func Commands() []*cobra.Command { - return []*cobra.Command{buildCmd, listCmd} + return []*cobra.Command{newBuildCmd(), listCmd} } diff --git a/go.mod b/go.mod index 1b143cb3..2d21ec89 100644 --- a/go.mod +++ b/go.mod @@ -1,8 +1,6 @@ module github.com/zoncoen/scenarigo -go 1.21.2 - -toolchain go1.22.3 +go 1.22.0 require ( carvel.dev/ytt v0.48.0 diff --git a/logger/logger.go b/logger/logger.go index 248310c3..6cd8014a 100644 --- a/logger/logger.go +++ b/logger/logger.go @@ -55,7 +55,7 @@ func (l *logger) Info(msg string, kvs ...interface{}) { if l.level > LogLevelInfo { return } - l.logger.Printf(bold.Sprintf("[INFO] %q%s", msg, flatten(kvs...))) + l.logger.Print(bold.Sprintf("[INFO] %q%s", msg, flatten(kvs...))) } // Error logs an error, with the given message and key/value pairs. @@ -63,7 +63,7 @@ func (l *logger) Error(err error, msg string, kvs ...interface{}) { if l.level > LogLevelError { return } - l.logger.Printf(red.Sprintf(`[ERROR] %q "error"=%q%s`, msg, err, flatten(kvs...))) + l.logger.Print(red.Sprintf(`[ERROR] %q "error"=%q%s`, msg, err, flatten(kvs...))) } func flatten(kvs ...interface{}) string { diff --git a/logger/logger_test.go b/logger/logger_test.go index a0f65944..e8c24a22 100644 --- a/logger/logger_test.go +++ b/logger/logger_test.go @@ -33,6 +33,15 @@ func TestLogger(t *testing.T) { }, expect: strings.TrimPrefix(` [ERROR] "error msg" "error"="omg" "count"="2" +`, "\n"), + }, + "no value": { + level: LogLevelAll, + f: func(l Logger) { + l.Info("info msg", "count") + }, + expect: strings.TrimPrefix(` +[INFO] "info msg" "count"="" `, "\n"), }, "none": { diff --git a/reporter/context.go b/reporter/context.go index 6d7c0dd9..f6f09bd0 100644 --- a/reporter/context.go +++ b/reporter/context.go @@ -118,6 +118,13 @@ func (c *testContext) release() { c.startParallel <- true // Pick a waiting test to be run. } +func (c *testContext) print(a ...any) (int, error) { + if c.w == nil { + return 0, nil + } + return fmt.Fprint(c.w, a...) +} + func (c *testContext) printf(format string, a ...interface{}) (int, error) { if c.w == nil { return 0, nil diff --git a/reporter/context_test.go b/reporter/context_test.go new file mode 100644 index 00000000..f53b8471 --- /dev/null +++ b/reporter/context_test.go @@ -0,0 +1,17 @@ +package reporter + +import "testing" + +func TestContextPrint(t *testing.T) { + c := &testContext{} + if i, err := c.print("test"); err != nil { + t.Fatal(err) + } else if i != 0 { + t.Fatalf("expect 0 but got %d", i) + } + if i, err := c.printf("%s", "test"); err != nil { + t.Fatal(err) + } else if i != 0 { + t.Fatalf("expect 0 but got %d", i) + } +} diff --git a/reporter/reporter.go b/reporter/reporter.go index 8ec89cdd..7f26f743 100644 --- a/reporter/reporter.go +++ b/reporter/reporter.go @@ -253,7 +253,7 @@ func (r *reporter) printTestSummary() { if !r.context.enabledTestSummary { return } - _, _ = r.context.printf(r.context.testSummary.String(r.context.noColor)) + _, _ = r.context.print(r.context.testSummary.String(r.context.noColor)) } func (r *reporter) appendChildren(children ...*reporter) { @@ -446,7 +446,7 @@ func printReport(r *reporter) { results := collectOutput(r) r.context.printf("%s\n", strings.Join(results, "\n")) if r.Failed() && !r.testing { - r.context.printf(r.failColor().Sprintln("FAIL")) + r.context.print(r.failColor().Sprintln("FAIL")) } } diff --git a/reporter/test_summary.go b/reporter/test_summary.go index ae8c67af..a38727a8 100644 --- a/reporter/test_summary.go +++ b/reporter/test_summary.go @@ -52,7 +52,7 @@ func (s *testSummary) String(noColor bool) string { passedText := s.passColor(noColor).Sprintf("%d passed", s.passedCount) failedText := s.failColor(noColor).Sprintf("%d failed", len(s.failed)) skippedText := s.skipColor(noColor).Sprintf("%d skipped", s.skippedCount) - failedFiles := s.failColor(noColor).Sprintf(s.failedFiles()) + failedFiles := s.failColor(noColor).Sprint(s.failedFiles()) return fmt.Sprintf( "\n%s: %s, %s, %s\n\n%s", totalText, passedText, failedText, skippedText, failedFiles,