package main import ( "bytes" "fmt" "io/fs" "os" "os/exec" "path/filepath" "strings" "testing" ) func TestMultiModuleSelectorMapsSelectorsAndTags(t *testing.T) { example := filepath.Join(cliRepositoryRoot(t), "examples", "multi-module") helper := filepath.Join(example, "select-module.sh") for _, test := range []struct { mode string value string want string }{ {mode: "selector", value: "root", want: "root\tv[01].*\t.\treleasekit-root.json"}, {mode: "selector", value: "ckconfig", want: "ckconfig\tintegrations/ckconfig/v*\tintegrations/ckconfig\treleasekit-ckconfig.json"}, {mode: "tag", value: "integrations/ckconfig/v1.2.3", want: "ckconfig\tintegrations/ckconfig/v*\tintegrations/ckconfig\treleasekit-ckconfig.json"}, {mode: "tag", value: "integrations/core/v2.3.4", want: "core-v2\tintegrations/core/v2.*\tintegrations/core/v2\treleasekit-core-v2.json"}, {mode: "tag", value: "v1.2.3", want: "root\tv[01].*\t.\treleasekit-root.json"}, } { t.Run(test.mode+" "+test.value, func(t *testing.T) { got := runModuleSelector(t, helper, test.mode, test.value, "") if strings.TrimSpace(got) != test.want { t.Fatalf("selector output = %q, want %q", got, test.want) } }) } mapPath := filepath.Join(t.TempDir(), "release modules.tsv") if err := os.WriteFile(mapPath, []byte("one\tfeature/*\tmodules/one\tprofiles/one.json\ntwo\tfeature/v1.*\tmodules/two\tprofiles/two.json\n"), 0o600); err != nil { t.Fatal(err) } if output := runModuleSelectorError(t, helper, "selector", "absent", mapPath); !strings.Contains(output, "no module map row") { t.Fatalf("unknown selector error = %q", output) } if output := runModuleSelectorError(t, helper, "tag", "feature/v1.2.3", mapPath); !strings.Contains(output, "ambiguous") { t.Fatalf("ambiguous event error = %q", output) } if err := os.WriteFile(mapPath, []byte("space\tfeature/*\tintegrations/ck config\tprofiles/core release.json\n"), 0o600); err != nil { t.Fatal(err) } got := runModuleSelector(t, helper, "tag", "feature/v1.2.3", mapPath) want := "space\tfeature/*\tintegrations/ck config\tprofiles/core release.json" if strings.TrimSpace(got) != want { t.Fatalf("paths with spaces = %q, want %q", got, want) } } func TestMultiModuleLauncherUsesSelectedModuleAndProfile(t *testing.T) { example := filepath.Join(cliRepositoryRoot(t), "examples", "multi-module") repository := newCLIRepository(t) copyExampleTree(t, example, repository) rootModule := "module example.com/multi-module-consumer\n\ngo 1.25.0\n\nrequire gitea.maximumdirect.net/eric/ReleaseKit v0.2.197\n" selectedModule := "module example.com/multi-module-consumer/integrations/ckconfig\n\ngo 1.25.0\n\nrequire gitea.maximumdirect.net/eric/ReleaseKit v0.2.198\n" writeRepositoryFile(t, repository, "go.mod", rootModule) writeRepositoryFile(t, repository, "integrations/ckconfig/go.mod", selectedModule) spaceModule := "module example.com/multi-module-consumer/integrations/ckconfig\n\ngo 1.25.0\n\nrequire gitea.maximumdirect.net/eric/ReleaseKit v0.2.299\n" writeRepositoryFile(t, repository, "integrations/ck config/go.mod", spaceModule) writeRepositoryFile(t, repository, "profiles/core release.json", `{"product":"Example Consumer Integration","module_dir":"integrations/ck config"}`) cliGitRun(t, repository, "add", ".") cliGitRun(t, repository, "commit", "-m", "prepare multi-module launcher fixture") mapPath := filepath.Join(t.TempDir(), "release modules.tsv") if err := os.WriteFile(mapPath, []byte("with space\tfeature/*\tintegrations/ck config\tprofiles/core release.json\n"), 0o600); err != nil { t.Fatal(err) } shimDir := t.TempDir() goLog := filepath.Join(t.TempDir(), "go commands") argsLog := filepath.Join(t.TempDir(), "tool arguments") shim := `#!/bin/sh set -eu printf 'cwd=%s args=%s\n' "$PWD" "$*" >> "$RELEASEKIT_TEST_GO_LOG" modfile= output= previous= for argument in "$@"; do case "$argument" in -modfile=*) modfile=${argument#-modfile=} ;; esac if [ "$previous" = -o ]; then output=$argument; fi previous=$argument done if [ -n "$modfile" ]; then cat "$modfile" >> "$RELEASEKIT_TEST_GO_LOG"; fi if [ "$1" = build ]; then cat > "$output" <<'TOOL' #!/bin/sh printf '<%s>\n' "$@" > "$RELEASEKIT_TEST_TOOL_ARGS" TOOL chmod +x "$output" fi ` if err := os.WriteFile(filepath.Join(shimDir, "go"), []byte(shim), 0o700); err != nil { t.Fatal(err) } t.Setenv("PATH", shimDir+string(os.PathListSeparator)+os.Getenv("PATH")) t.Setenv("RELEASEKIT_MODULE_MAP", mapPath) t.Setenv("RELEASEKIT_TEST_GO_LOG", goLog) t.Setenv("RELEASEKIT_TEST_TOOL_ARGS", argsLog) launcher := filepath.Join(repository, "release.sh") command := exec.Command(launcher, "with space", "v1.2.4") var stdout, stderr bytes.Buffer command.Stdout = &stdout command.Stderr = &stderr if err := command.Run(); err != nil { t.Fatalf("launcher error: %v stdout=%q stderr=%q", err, stdout.String(), stderr.String()) } goCommands, err := os.ReadFile(goLog) if err != nil { t.Fatal(err) } wantModule := filepath.Join(repository, "integrations", "ck config") if !strings.Contains(string(goCommands), "cwd="+wantModule) || !strings.Contains(string(goCommands), "v0.2.299") || strings.Contains(string(goCommands), "v0.2.197") { t.Fatalf("tool bootstrap did not use selected module metadata: %s", goCommands) } toolArgs, err := os.ReadFile(argsLog) if err != nil { t.Fatal(err) } for _, expected := range []string{"", "<--repo>", "<" + repository + ">", "<--config>", "", "<--expected-module>", "", ""} { if !strings.Contains(string(toolArgs), expected) { t.Errorf("publication arguments %q omit %q", toolArgs, expected) } } if strings.Contains(string(goCommands), repository+"/.releasekit-bootstrap.") { t.Fatalf("temporary tool metadata was placed inside the repository: %s", goCommands) } } func TestMultiModuleLauncherBuildsToolFromSelectedDependency(t *testing.T) { example := filepath.Join(cliRepositoryRoot(t), "examples", "multi-module") repository := newCLIRepository(t) copyExampleTree(t, example, repository) writeRepositoryFile(t, repository, "go.mod", "module example.com/multi-module-consumer\n\ngo 1.25.0\n\nrequire gitea.maximumdirect.net/eric/ReleaseKit v0.2.197\n") writeRepositoryFile(t, repository, "integrations/ckconfig/go.mod", "module example.com/multi-module-consumer/integrations/ckconfig\n\ngo 1.25.0\n\nrequire gitea.maximumdirect.net/eric/ReleaseKit v0.2.198\n") cliGitRun(t, repository, "add", ".") cliGitRun(t, repository, "commit", "-m", "prepare selected tool module") proxy := writeReleaseKitModuleProxy(t) moduleCache := t.TempDir() t.Setenv("GOMODCACHE", moduleCache) t.Cleanup(func() { _ = filepath.WalkDir(moduleCache, func(path string, entry fs.DirEntry, err error) error { if err == nil && entry.IsDir() { _ = os.Chmod(path, 0o700) } return nil }) }) t.Setenv("GOPROXY", "file://"+filepath.ToSlash(proxy)) t.Setenv("GOSUMDB", "off") t.Setenv("GOTOOLCHAIN", "local") moduleBefore, err := os.ReadFile(filepath.Join(repository, "integrations", "ckconfig", "go.mod")) if err != nil { t.Fatal(err) } command := exec.Command(filepath.Join(repository, "release.sh"), "ckconfig", "v1.2.3") output, err := command.CombinedOutput() if err != nil { t.Fatalf("launcher error: %v output=%q", err, output) } for _, expected := range []string{ "tool-version=v0.2.198 helper=tool-only dependency", `"publish"`, `"--repo"`, `"` + repository + `"`, `"--config"`, `"releasekit-ckconfig.json"`, `"--expected-module"`, `"integrations/ckconfig"`, `"v1.2.3"`, } { if !strings.Contains(string(output), expected) { t.Errorf("launcher output %q omits %q", output, expected) } } if strings.Contains(string(output), "tool-version=v0.2.197") { t.Fatalf("launcher built the root-selected ReleaseKit version: %s", output) } moduleAfter, err := os.ReadFile(filepath.Join(repository, "integrations", "ckconfig", "go.mod")) if err != nil || !bytes.Equal(moduleAfter, moduleBefore) { t.Fatalf("selected module go.mod changed: got %q want %q error=%v", moduleAfter, moduleBefore, err) } } func runModuleSelector(t *testing.T, helper, mode, value, mapPath string) string { t.Helper() command := exec.Command("sh", helper, mode, value) command.Env = os.Environ() if mapPath != "" { command.Env = append(command.Env, "RELEASEKIT_MODULE_MAP="+mapPath) } output, err := command.CombinedOutput() if err != nil { t.Fatalf("selector %s %q: %v: %s", mode, value, err, output) } return string(output) } func runModuleSelectorError(t *testing.T, helper, mode, value, mapPath string) string { t.Helper() command := exec.Command("sh", helper, mode, value) command.Env = append(os.Environ(), "RELEASEKIT_MODULE_MAP="+mapPath) output, err := command.CombinedOutput() if err == nil { t.Fatalf("selector %s %q unexpectedly succeeded: %s", mode, value, output) } return string(output) } func copyExampleTree(t *testing.T, source, destination string) { t.Helper() err := filepath.WalkDir(source, func(path string, entry fs.DirEntry, err error) error { if err != nil { return err } relative, err := filepath.Rel(source, path) if err != nil { return err } if relative == "." { return nil } target := filepath.Join(destination, relative) if entry.IsDir() { return os.MkdirAll(target, 0o700) } contents, err := os.ReadFile(path) if err != nil { return err } info, err := entry.Info() if err != nil { return err } mode := info.Mode().Perm() if err := os.WriteFile(target, contents, mode); err != nil { return err } return os.Chmod(target, mode) }) if err != nil { t.Fatal(fmt.Errorf("copy multi-module example: %w", err)) } }