diff --git a/cmd/root_test.go b/cmd/root_test.go index a7b9ccf..a394d12 100644 --- a/cmd/root_test.go +++ b/cmd/root_test.go @@ -239,6 +239,99 @@ func TestRunTest(t *testing.T) { }) } +func TestRunTestScopedDirectory(t *testing.T) { + addFakeRecordDependencies(t, "script", "bwrap", "bash") + t.Setenv("FAKE_SCRIPT_ECHO_STDIN", "1") + + root := t.TempDir() + writeScenarioFixtures(t, filepath.Join(root, "e2e", "a"), "echo one\n", "echo one\n") + writeScenarioFixtures(t, filepath.Join(root, "e2e", "nested", "b"), "echo two\n", "echo two\n") + writeScenarioFixtures(t, filepath.Join(root, "e2e", "nested", "c"), "echo three\n", "echo three\n") + + withWorkingDir(t, root, func() { + initStdout, initStderr := captureOutput(t, func() { + if got := Run([]string{"init"}); got != 0 { + t.Fatalf("Run() code = %d, want %d", got, 0) + } + }) + if initStdout != prefixed("Done initialising...\n") { + t.Fatalf("stdout = %q, want %q", initStdout, prefixed("Done initialising...\n")) + } + if initStderr != "" { + t.Fatalf("stderr = %q, want empty", initStderr) + } + + stdout, stderr := captureOutput(t, func() { + if got := Run([]string{"test", "nested"}); got != 0 { + t.Fatalf("Run() code = %d, want %d", got, 0) + } + }) + + for _, want := range []string{ + "RUN nested/b", + "PASS nested/b", + "RUN nested/c", + "PASS nested/c", + "Summary: total=2 passed=2 failed=0", + } { + if !strings.Contains(stdout, want) { + t.Fatalf("stdout = %q, want substring %q", stdout, want) + } + } + if strings.Contains(stdout, "RUN a") { + t.Fatalf("stdout = %q, want scoped run to exclude scenario outside nested", stdout) + } + if stderr != "" { + t.Fatalf("stderr = %q, want empty", stderr) + } + }) +} + +func TestRunTestScopedLeafDirectory(t *testing.T) { + addFakeRecordDependencies(t, "script", "bwrap", "bash") + t.Setenv("FAKE_SCRIPT_ECHO_STDIN", "1") + + root := t.TempDir() + writeScenarioFixtures(t, filepath.Join(root, "e2e", "a"), "echo one\n", "echo one\n") + writeScenarioFixtures(t, filepath.Join(root, "e2e", "nested", "b"), "echo two\n", "echo two\n") + + withWorkingDir(t, root, func() { + initStdout, initStderr := captureOutput(t, func() { + if got := Run([]string{"init"}); got != 0 { + t.Fatalf("Run() code = %d, want %d", got, 0) + } + }) + if initStdout != prefixed("Done initialising...\n") { + t.Fatalf("stdout = %q, want %q", initStdout, prefixed("Done initialising...\n")) + } + if initStderr != "" { + t.Fatalf("stderr = %q, want empty", initStderr) + } + + stdout, stderr := captureOutput(t, func() { + if got := Run([]string{"test", filepath.Join("nested", "b")}); got != 0 { + t.Fatalf("Run() code = %d, want %d", got, 0) + } + }) + + for _, want := range []string{ + "RUN nested/b", + "PASS nested/b", + "Summary: total=1 passed=1 failed=0", + } { + if !strings.Contains(stdout, want) { + t.Fatalf("stdout = %q, want substring %q", stdout, want) + } + } + if strings.Contains(stdout, "RUN a") { + t.Fatalf("stdout = %q, want scoped run to exclude scenario outside nested/b", stdout) + } + if stderr != "" { + t.Fatalf("stderr = %q, want empty", stderr) + } + }) +} + func TestRunTestReturnsOneWhenScenarioMismatches(t *testing.T) { addFakeRecordDependencies(t, "script", "bwrap", "bash") t.Setenv("FAKE_SCRIPT_ECHO_STDIN", "1") @@ -401,6 +494,26 @@ func TestRunRecordMissingPath(t *testing.T) { } } +func TestRunTestExtraArgs(t *testing.T) { + addFakeRecordDependencies(t, "script", "bwrap", "bash") + + stdout, stderr := captureOutput(t, func() { + if got := Run([]string{"test", "a", "b"}); got != 1 { + t.Fatalf("Run() code = %d, want %d", got, 1) + } + }) + + if stdout != "" { + t.Fatalf("stdout = %q, want empty", stdout) + } + if !strings.Contains(stderr, "accepts at most 1 arg(s), received 2") { + t.Fatalf("stderr = %q, want extra-arg error", stderr) + } + if !strings.Contains(stderr, "Usage:") || !strings.Contains(stderr, "miro test [path]") { + t.Fatalf("stderr = %q, want test usage", stderr) + } +} + func TestRunInitExtraArgs(t *testing.T) { addFakeRecordDependencies(t, "script", "bwrap", "bash") diff --git a/cmd/test.go b/cmd/test.go index 77e7055..78c04a2 100644 --- a/cmd/test.go +++ b/cmd/test.go @@ -1,6 +1,8 @@ package cmd import ( + "path/filepath" + "miro/internal/miro" "github.com/spf13/cobra" @@ -8,10 +10,15 @@ import ( func newTestCommand() *cobra.Command { return &cobra.Command{ - Use: "test", - Args: cobra.NoArgs, - RunE: func(_ *cobra.Command, _ []string) error { - return miro.RunTests() + Use: "test [path]", + Args: cobra.MaximumNArgs(1), + RunE: func(_ *cobra.Command, args []string) error { + path := "" + if len(args) == 1 { + path = filepath.Clean(args[0]) + } + + return miro.RunTests(path) }, } } diff --git a/internal/miro/test.go b/internal/miro/test.go index 854c237..e7b23cc 100644 --- a/internal/miro/test.go +++ b/internal/miro/test.go @@ -45,14 +45,14 @@ func (e TestSuiteFailedError) Error() string { } // RunTests replays all scenarios under the configured test directory. -func RunTests() error { - return runTests(testIO{ +func RunTests(path string) error { + return runTests(path, testIO{ out: os.Stdout, err: os.Stderr, }) } -func runTests(tio testIO) error { +func runTests(path string, tio testIO) error { root, err := currentProjectRoot() if err != nil { return err @@ -68,17 +68,22 @@ func runTests(tio testIO) error { return fmt.Errorf("failed to resolve test directory: %v", err) } + discoveryRoot, err := resolveTestDiscoveryRoot(testDir, path) + if err != nil { + return err + } + shellPath, err := resolveRecordShell(testDir) if err != nil { return err } - scenarios, err := discoverTestScenarios(testDir) + scenarios, err := discoverTestScenarios(discoveryRoot, testDir) if err != nil { return err } if len(scenarios) == 0 { - return fmt.Errorf("no test scenarios found in %q", testDir) + return fmt.Errorf("no test scenarios found in %q", discoveryRoot) } summary := testSummary{total: len(scenarios)} @@ -110,10 +115,34 @@ func runTests(tio testIO) error { return nil } -func discoverTestScenarios(testDir string) ([]testScenario, error) { +func resolveTestDiscoveryRoot(testDir, path string) (string, error) { + if path == "" { + return testDir, nil + } + + target, err := resolvePathWithinTestDir(testDir, path, "test") + if err != nil { + return "", err + } + + info, err := os.Stat(target) + if err != nil { + if os.IsNotExist(err) { + return "", fmt.Errorf("test path %q does not exist", target) + } + return "", fmt.Errorf("failed to check test path %q: %v", target, err) + } + if !info.IsDir() { + return "", fmt.Errorf("test path %q is not a directory", target) + } + + return target, nil +} + +func discoverTestScenarios(discoveryRoot, displayRoot string) ([]testScenario, error) { fixturesByDir := map[string]testFixtureFiles{} - if err := filepath.WalkDir(testDir, func(path string, d fs.DirEntry, walkErr error) error { + if err := filepath.WalkDir(discoveryRoot, func(path string, d fs.DirEntry, walkErr error) error { if walkErr != nil { return walkErr } @@ -137,12 +166,12 @@ func discoverTestScenarios(testDir string) ([]testScenario, error) { return nil }); err != nil { - return nil, fmt.Errorf("failed to scan test directory %q: %v", testDir, err) + return nil, fmt.Errorf("failed to scan test directory %q: %v", discoveryRoot, err) } scenarios := make([]testScenario, 0, len(fixturesByDir)) for dir, files := range fixturesByDir { - relPath, err := filepath.Rel(testDir, dir) + relPath, err := filepath.Rel(displayRoot, dir) if err != nil { return nil, fmt.Errorf("failed to resolve scenario path for %q: %v", dir, err) } diff --git a/internal/miro/test_test.go b/internal/miro/test_test.go index b3644b0..9e8c2c0 100644 --- a/internal/miro/test_test.go +++ b/internal/miro/test_test.go @@ -2,7 +2,6 @@ package miro import ( "bytes" - "errors" "path/filepath" "strings" "testing" @@ -15,7 +14,7 @@ func TestDiscoverTestScenariosFindsNestedFixturesAndSorts(t *testing.T) { writeFile(t, filepath.Join(testDir, "shell.sh"), "#!/bin/sh\n") writeFile(t, filepath.Join(testDir, "notes.txt"), "ignore me\n") - got, err := discoverTestScenarios(testDir) + got, err := discoverTestScenarios(testDir, testDir) if err != nil { t.Fatalf("discoverTestScenarios() error = %v", err) } @@ -45,7 +44,7 @@ func TestDiscoverTestScenariosRejectsMissingOutFixture(t *testing.T) { testDir := filepath.Join(t.TempDir(), "e2e") writeFile(t, filepath.Join(testDir, "broken", "in"), "echo broken\n") - _, err := discoverTestScenarios(testDir) + _, err := discoverTestScenarios(testDir, testDir) if err == nil { t.Fatal("discoverTestScenarios() error = nil, want error") } @@ -58,7 +57,7 @@ func TestDiscoverTestScenariosRejectsMissingInFixture(t *testing.T) { testDir := filepath.Join(t.TempDir(), "e2e") writeFile(t, filepath.Join(testDir, "broken", "out"), "echo broken\n") - _, err := discoverTestScenarios(testDir) + _, err := discoverTestScenarios(testDir, testDir) if err == nil { t.Fatal("discoverTestScenarios() error = nil, want error") } @@ -133,47 +132,72 @@ func TestReplayScenarioFailsWhenCompareMarkerMissing(t *testing.T) { } } -func TestRunTestsRunsFullSuiteAndSummarizesFailures(t *testing.T) { - addFakeRecordDependencies(t, "script") - t.Setenv("FAKE_SCRIPT_ECHO_STDIN", "1") +func TestDiscoverTestScenariosUsesDisplayRootForRelativePaths(t *testing.T) { + testDir := filepath.Join(t.TempDir(), "e2e") + scopedDir := filepath.Join(testDir, "nested") + writeScenarioFixtures(t, filepath.Join(scopedDir, "b"), "echo two\n", "echo two\n") + got, err := discoverTestScenarios(scopedDir, testDir) + if err != nil { + t.Fatalf("discoverTestScenarios() error = %v", err) + } + if len(got) != 1 { + t.Fatalf("len(discoverTestScenarios()) = %d, want 1", len(got)) + } + if got[0].relPath != filepath.Join("nested", "b") { + t.Fatalf("scenario relPath = %q, want %q", got[0].relPath, filepath.Join("nested", "b")) + } +} + +func TestResolveTestDiscoveryRootRejectsMissingPath(t *testing.T) { + testDir := filepath.Join(t.TempDir(), "e2e") + mustMkdirAll(t, testDir) + + _, err := resolveTestDiscoveryRoot(testDir, "missing") + if err == nil { + t.Fatal("resolveTestDiscoveryRoot() error = nil, want error") + } + if !strings.Contains(err.Error(), `does not exist`) { + t.Fatalf("resolveTestDiscoveryRoot() error = %q, want missing-path error", err.Error()) + } +} + +func TestResolveTestDiscoveryRootRejectsFile(t *testing.T) { root := t.TempDir() testDir := filepath.Join(root, "e2e") + writeFile(t, filepath.Join(testDir, "case.txt"), "hello\n") + + err := withWorkingDir(t, root, func() error { + _, err := resolveTestDiscoveryRoot(testDir, filepath.Join("e2e", "case.txt")) + return err + }) + if err == nil { + t.Fatal("resolveTestDiscoveryRoot() error = nil, want error") + } + if !strings.Contains(err.Error(), `is not a directory`) { + t.Fatalf("resolveTestDiscoveryRoot() error = %q, want directory error", err.Error()) + } +} + +func TestRunTestsScopedRunEmptyDirectoryFails(t *testing.T) { + root := t.TempDir() + testDir := filepath.Join(root, "e2e") + emptyDir := filepath.Join(testDir, "empty") writeFile(t, filepath.Join(root, "miro.toml"), validConfigContent("e2e")) mustWriteRecordShell(t, testDir) - writeScenarioFixtures(t, filepath.Join(testDir, "a"), "echo one\n", "echo one\n") - writeScenarioFixtures(t, filepath.Join(testDir, "b"), "echo two\n", "different output\n") + mustMkdirAll(t, emptyDir) - var stdout bytes.Buffer - var stderr bytes.Buffer err := withWorkingDir(t, root, func() error { - return runTests(testIO{ - out: &stdout, - err: &stderr, + return runTests("empty", testIO{ + out: &bytes.Buffer{}, + err: &bytes.Buffer{}, }) }) - - var suiteErr TestSuiteFailedError - if !errors.As(err, &suiteErr) { - t.Fatalf("runTests() error = %v, want TestSuiteFailedError", err) + if err == nil { + t.Fatal("runTests() error = nil, want error") } - if suiteErr.Failed != 1 { - t.Fatalf("TestSuiteFailedError.Failed = %d, want 1", suiteErr.Failed) - } - - for _, want := range []string{ - "RUN a", - "PASS a", - "RUN b", - "FAIL b: output differed", - "Summary: total=2 passed=1 failed=1", - } { - if !strings.Contains(stdout.String(), want) { - t.Fatalf("stdout = %q, want substring %q", stdout.String(), want) - } - } - if stderr.String() != "" { - t.Fatalf("stderr = %q, want empty", stderr.String()) + if !strings.Contains(err.Error(), `no test scenarios found in "`) || !strings.Contains(err.Error(), filepath.Join("e2e", "empty")) { + t.Fatalf("runTests() error = %q, want empty-directory error", err.Error()) } }