From 8968af90ccba5dbda15000a86a86abf9458a82b2 Mon Sep 17 00:00:00 2001 From: ruinivist Date: Mon, 16 Mar 2026 21:45:16 +0000 Subject: [PATCH] feat: testdir resolution --- internal/config/config.go | 10 +- internal/config/config_test.go | 16 +-- internal/miro/testdir.go | 86 ++++++++++++ internal/miro/testdir_test.go | 241 +++++++++++++++++++++++++++++++++ 4 files changed, 336 insertions(+), 17 deletions(-) create mode 100644 internal/miro/testdir.go create mode 100644 internal/miro/testdir_test.go diff --git a/internal/config/config.go b/internal/config/config.go index 5ae971a..66bb129 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -9,8 +9,7 @@ import ( ) type Config struct { - TestDir string - HasTestDir bool + TestDir string } type tomlConfig struct { @@ -21,11 +20,11 @@ type tomlMiroConfig struct { TestDir string `toml:"test_dir"` } -// ReadConfig reads miro.toml and reports whether [miro].test_dir was explicitly set. +// ReadConfig reads miro.toml. func ReadConfig(path string) (Config, error) { var raw tomlConfig - meta, err := toml.DecodeFile(path, &raw) + _, err := toml.DecodeFile(path, &raw) if err != nil { if errors.Is(err, os.ErrNotExist) { return Config{}, err @@ -34,7 +33,6 @@ func ReadConfig(path string) (Config, error) { } return Config{ - TestDir: raw.Miro.TestDir, - HasTestDir: meta.IsDefined("miro", "test_dir"), + TestDir: raw.Miro.TestDir, }, nil } diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 5541009..13edbca 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -11,19 +11,16 @@ func TestReadConfig(t *testing.T) { name string content string wantDir string - wantHasDir bool wantReadErr bool }{ { - name: "with test dir", - content: "[miro]\ntest_dir = \"custom/suite\"\n", - wantDir: "custom/suite", - wantHasDir: true, + name: "with test dir", + content: "[miro]\ntest_dir = \"custom/suite\"\n", + wantDir: "custom/suite", }, { - name: "without test dir", - content: "[miro]\n", - wantHasDir: false, + name: "without test dir", + content: "[miro]\n", }, { name: "invalid toml", @@ -52,9 +49,6 @@ func TestReadConfig(t *testing.T) { if got.TestDir != tt.wantDir { t.Fatalf("ReadConfig() TestDir = %q, want %q", got.TestDir, tt.wantDir) } - if got.HasTestDir != tt.wantHasDir { - t.Fatalf("ReadConfig() HasTestDir = %v, want %v", got.HasTestDir, tt.wantHasDir) - } }) } } diff --git a/internal/miro/testdir.go b/internal/miro/testdir.go new file mode 100644 index 0000000..4aaa274 --- /dev/null +++ b/internal/miro/testdir.go @@ -0,0 +1,86 @@ +package miro + +import ( + "errors" + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" + + miroconfig "miro/internal/config" +) + +var fallbackTestDirs = []string{ + "e2e", + filepath.Join("test", "e2e"), + filepath.Join("tests", "e2e"), + "miro", + filepath.Join("test", "miro"), + filepath.Join("tests", "miro"), +} + +// ResolveTestDir resolves the project test directory from config or conventions. +func ResolveTestDir() (string, error) { + cwd, err := os.Getwd() + if err != nil { + return "", fmt.Errorf("failed to get working directory: %v", err) + } + + root := projectRoot(cwd) + return resolveTestDirFromRoot(root) +} + +func resolveTestDirFromRoot(root string) (string, error) { + candidates, err := testDirCandidates(root) + if err != nil { + return "", err + } + + for _, candidate := range candidates { + info, err := os.Stat(candidate) + if err == nil && info.IsDir() { + return candidate, nil + } + if err != nil && !errors.Is(err, os.ErrNotExist) { + return "", fmt.Errorf("failed to check test directory %s: %v", candidate, err) + } + } + + return "", errors.New("test_dir not configured and none of the expected test directories exist") +} + +// returns the git root if in a git repo else current pwd +func projectRoot(cwd string) string { + out, err := exec.Command("git", "rev-parse", "--show-toplevel").CombinedOutput() + if err != nil { + return cwd + } + + root := strings.TrimSpace(string(out)) + if root == "" { + return cwd + } + return filepath.Clean(root) +} + +// list of e2e test directory candidates. uses from config if it exists +// else uses fallback paths +func testDirCandidates(root string) ([]string, error) { + configPath := filepath.Join(root, "miro.toml") + + cfg, err := miroconfig.ReadConfig(configPath) + if err == nil && cfg.TestDir != "" { + return []string{filepath.Join(root, cfg.TestDir)}, nil + } + if err != nil && !errors.Is(err, os.ErrNotExist) { + return nil, err + } + + candidates := make([]string, 0, len(fallbackTestDirs)) + for _, candidate := range fallbackTestDirs { + candidates = append(candidates, filepath.Join(root, candidate)) + } + + return candidates, nil +} diff --git a/internal/miro/testdir_test.go b/internal/miro/testdir_test.go new file mode 100644 index 0000000..7b4c95f --- /dev/null +++ b/internal/miro/testdir_test.go @@ -0,0 +1,241 @@ +package miro + +import ( + "os" + "os/exec" + "path/filepath" + "strings" + "testing" +) + +func TestResolveTestDirConfigOverride(t *testing.T) { + root := t.TempDir() + configured := filepath.Join(root, "custom", "suite") + mustMkdirAll(t, configured) + mustMkdirAll(t, filepath.Join(root, "e2e")) + writeFile(t, filepath.Join(root, "miro.toml"), "[miro]\ntest_dir = \"custom/suite\"\n") + + got := withWorkingDir(t, root, func() string { + path, err := ResolveTestDir() + if err != nil { + t.Fatalf("ResolveTestDir() error = %v", err) + } + return path + }) + + if got != configured { + t.Fatalf("ResolveTestDir() = %q, want %q", got, configured) + } +} + +func TestResolveTestDirFallsBackWhenConfigOmitsTestDir(t *testing.T) { + root := t.TempDir() + want := filepath.Join(root, "e2e") + mustMkdirAll(t, want) + writeFile(t, filepath.Join(root, "miro.toml"), "[miro]\n") + + got := withWorkingDir(t, root, func() string { + path, err := ResolveTestDir() + if err != nil { + t.Fatalf("ResolveTestDir() error = %v", err) + } + return path + }) + + if got != want { + t.Fatalf("ResolveTestDir() = %q, want %q", got, want) + } +} + +func TestResolveTestDirFallsBackWhenConfiguredPathEmpty(t *testing.T) { + root := t.TempDir() + want := filepath.Join(root, "e2e") + mustMkdirAll(t, want) + writeFile(t, filepath.Join(root, "miro.toml"), "[miro]\ntest_dir = \"\"\n") + + got := withWorkingDir(t, root, func() string { + path, err := ResolveTestDir() + if err != nil { + t.Fatalf("ResolveTestDir() error = %v", err) + } + return path + }) + + if got != want { + t.Fatalf("ResolveTestDir() = %q, want %q", got, want) + } +} + +func TestResolveTestDirMalformedConfigFails(t *testing.T) { + root := t.TempDir() + writeFile(t, filepath.Join(root, "miro.toml"), "[miro]\ntest_dir = [\n") + + err := withWorkingDir(t, root, func() error { + _, err := ResolveTestDir() + return err + }) + + if err == nil { + t.Fatal("ResolveTestDir() error = nil, want error") + } + if !strings.Contains(err.Error(), "failed to read") { + t.Fatalf("ResolveTestDir() error = %q, want read failure", err) + } +} + +func TestResolveTestDirConfiguredMissingDirectoryFails(t *testing.T) { + root := t.TempDir() + writeFile(t, filepath.Join(root, "miro.toml"), "[miro]\ntest_dir = \"missing\"\n") + + err := withWorkingDir(t, root, func() error { + _, err := ResolveTestDir() + return err + }) + + if err == nil { + t.Fatal("ResolveTestDir() error = nil, want error") + } + if err.Error() != "test_dir not configured and none of the expected test directories exist" { + t.Fatalf("ResolveTestDir() error = %q, want generic missing-dir error", err) + } +} + +func TestResolveTestDirConfiguredFileFails(t *testing.T) { + root := t.TempDir() + writeFile(t, filepath.Join(root, "case.txt"), "hello\n") + writeFile(t, filepath.Join(root, "miro.toml"), "[miro]\ntest_dir = \"case.txt\"\n") + + err := withWorkingDir(t, root, func() error { + _, err := ResolveTestDir() + return err + }) + + if err == nil { + t.Fatal("ResolveTestDir() error = nil, want error") + } + if err.Error() != "test_dir not configured and none of the expected test directories exist" { + t.Fatalf("ResolveTestDir() error = %q, want generic missing-dir error", err) + } +} + +func TestResolveTestDirFallbackOrder(t *testing.T) { + root := t.TempDir() + lower := filepath.Join(root, "tests", "miro") + higher := filepath.Join(root, "tests", "e2e") + mustMkdirAll(t, lower) + mustMkdirAll(t, higher) + + got := withWorkingDir(t, root, func() string { + path, err := ResolveTestDir() + if err != nil { + t.Fatalf("ResolveTestDir() error = %v", err) + } + return path + }) + + if got != higher { + t.Fatalf("ResolveTestDir() = %q, want %q", got, higher) + } +} + +func TestResolveTestDirFallbackToMiroDirectories(t *testing.T) { + root := t.TempDir() + want := filepath.Join(root, "test", "miro") + mustMkdirAll(t, want) + + got := withWorkingDir(t, root, func() string { + path, err := ResolveTestDir() + if err != nil { + t.Fatalf("ResolveTestDir() error = %v", err) + } + return path + }) + + if got != want { + t.Fatalf("ResolveTestDir() = %q, want %q", got, want) + } +} + +func TestResolveTestDirUsesGitRoot(t *testing.T) { + root := t.TempDir() + mustGitInit(t, root) + want := filepath.Join(root, "e2e") + mustMkdirAll(t, want) + subdir := filepath.Join(root, "nested", "dir") + mustMkdirAll(t, subdir) + + got := withWorkingDir(t, subdir, func() string { + path, err := ResolveTestDir() + if err != nil { + t.Fatalf("ResolveTestDir() error = %v", err) + } + return path + }) + + if got != want { + t.Fatalf("ResolveTestDir() = %q, want %q", got, want) + } +} + +func TestResolveTestDirFailsWhenNothingDetected(t *testing.T) { + root := t.TempDir() + + err := withWorkingDir(t, root, func() error { + _, err := ResolveTestDir() + return err + }) + + if err == nil { + t.Fatal("ResolveTestDir() error = nil, want error") + } + if err.Error() != "test_dir not configured and none of the expected test directories exist" { + t.Fatalf("ResolveTestDir() error = %q, want generic missing-dir error", err) + } +} + +func withWorkingDir[T any](t *testing.T, dir string, fn func() T) T { + t.Helper() + + wd, err := os.Getwd() + if err != nil { + t.Fatalf("Getwd() error = %v", err) + } + if err := os.Chdir(dir); err != nil { + t.Fatalf("Chdir(%q) error = %v", dir, err) + } + t.Cleanup(func() { + if err := os.Chdir(wd); err != nil { + t.Fatalf("restore working directory: %v", err) + } + }) + + return fn() +} + +func writeFile(t *testing.T, path, content string) { + t.Helper() + + mustMkdirAll(t, filepath.Dir(path)) + if err := os.WriteFile(path, []byte(content), 0o644); err != nil { + t.Fatalf("WriteFile(%q) error = %v", path, err) + } +} + +func mustMkdirAll(t *testing.T, path string) { + t.Helper() + + if err := os.MkdirAll(path, 0o755); err != nil { + t.Fatalf("MkdirAll(%q) error = %v", path, err) + } +} + +func mustGitInit(t *testing.T, dir string) { + t.Helper() + + cmd := exec.Command("git", "init") + cmd.Dir = dir + out, err := cmd.CombinedOutput() + if err != nil { + t.Fatalf("git init: %v\n%s", err, out) + } +}