diff --git a/cli/e2e/e2e_test.go b/cli/e2e/e2e_test.go new file mode 100644 index 0000000..2bc2d40 --- /dev/null +++ b/cli/e2e/e2e_test.go @@ -0,0 +1,63 @@ +//go:build e2e + +package e2e_test + +import ( + "os" + "os/exec" + "path/filepath" + "testing" + + "github.com/rogpeppe/go-internal/testscript" +) + +var sameBinary string + +func TestMain(m *testing.M) { + tmpDir, err := os.MkdirTemp("", "same-e2e-*") + if err != nil { + panic(err) + } + + sameBinary = filepath.Join(tmpDir, "same") + + //nolint:gosec // Building binary with static arguments, not user input + cmd := exec.Command("nix", "develop", "-c", "go", "build", "-o", sameBinary, "./cli/cmd/same") + cmd.Dir = filepath.Join("..", "..") + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + if err := cmd.Run(); err != nil { + panic("failed to build same binary: " + err.Error()) + } + + exitCode := m.Run() + + _ = os.RemoveAll(tmpDir) + + os.Exit(exitCode) +} + +func TestScripts(t *testing.T) { + testscript.Run(t, testscript.Params{ + Dir: "testdata", + Setup: setupE2E, + }) +} + +func setupE2E(env *testscript.Env) error { + env.Setenv("NO_COLOR", "1") + env.Setenv("CI", "true") + + binDir := filepath.Dir(sameBinary) + currentPath := env.Getenv("PATH") + env.Setenv("PATH", binDir+string(os.PathListSeparator)+currentPath) + + homeDir := filepath.Join(env.WorkDir, ".home") + if err := os.MkdirAll(homeDir, 0o750); err != nil { + return err + } + env.Setenv("HOME", homeDir) + + return nil +} diff --git a/cli/e2e/testdata/basic_execution.txtar b/cli/e2e/testdata/basic_execution.txtar new file mode 100644 index 0000000..5ae54b6 --- /dev/null +++ b/cli/e2e/testdata/basic_execution.txtar @@ -0,0 +1,12 @@ +# Basic task execution test +exec same run --ci hello +stderr '\[hello\] Starting\.\.\.' +stdout '\[hello\] Hello, World!' +stderr '\[hello\] ✓ Completed' + +-- same.yaml -- +version: "1" +project: test +tasks: + hello: + cmd: ["echo", "Hello, World!"] diff --git a/cli/e2e/testdata/cache_hit.txtar b/cli/e2e/testdata/cache_hit.txtar new file mode 100644 index 0000000..feda0c1 --- /dev/null +++ b/cli/e2e/testdata/cache_hit.txtar @@ -0,0 +1,19 @@ +# First run builds from scratch +exec same run --ci greet +stderr '\[greet\] ✓ Completed' +! stderr 'Cached' + +# Second run hits cache +exec same run --ci greet +stderr '\[greet\] ⚡ Cached' + +-- same.yaml -- +version: "1" +project: test +tasks: + greet: + cmd: ["echo", "Greetings!"] + input: ["input.txt"] + +-- input.txt -- +Hello diff --git a/cli/e2e/testdata/cache_invalidation.txtar b/cli/e2e/testdata/cache_invalidation.txtar new file mode 100644 index 0000000..04b65dd --- /dev/null +++ b/cli/e2e/testdata/cache_invalidation.txtar @@ -0,0 +1,26 @@ +# First run +exec same run --ci process +stderr '\[process\] ✓ Completed' +exists .same/store + +# Modify input +cp data2.txt data.txt + +# Second run rebuilds +exec same run --ci process +stderr '\[process\] Starting\.\.\.' +stderr '\[process\] ✓ Completed' +! stderr 'Cached' + +-- same.yaml -- +version: "1" +project: test +tasks: + process: + input: ["data.txt"] + cmd: ["cat", "data.txt"] + +-- data.txt -- +original +-- data2.txt -- +modified diff --git a/cli/e2e/testdata/clean.txtar b/cli/e2e/testdata/clean.txtar new file mode 100644 index 0000000..86e9225 --- /dev/null +++ b/cli/e2e/testdata/clean.txtar @@ -0,0 +1,30 @@ +# Run task to populate cache +exec same run --ci task1 +exists .same/store + +# Clean build cache (default) +exec same clean +! exists .same/store + +# Verify cache directories may still exist +# (Nix cache is managed separately) + +# Repopulate +exec same run --ci task1 +exists .same/store + +# Clean all - removes build store and attempts to clean cache +exec same clean --all +! exists .same/store +# Note: cache directory structure may remain even after clean --all + +-- same.yaml -- +version: "1" +project: test +tasks: + task1: + cmd: ["echo", "test"] + input: ["file.txt"] + +-- file.txt -- +content diff --git a/cli/e2e/testdata/dependency_order.txtar b/cli/e2e/testdata/dependency_order.txtar new file mode 100644 index 0000000..c498139 --- /dev/null +++ b/cli/e2e/testdata/dependency_order.txtar @@ -0,0 +1,27 @@ +# Verify topological execution order +exec same run --ci final +stderr '\[setup\] Starting\.\.\.' +stderr '\[setup\] ✓ Completed' +stderr '\[build\] Starting\.\.\.' +stderr '\[build\] ✓ Completed' +stderr '\[final\] Starting\.\.\.' +stderr '\[final\] ✓ Completed' + +-- same.yaml -- +version: "1" +project: test +tasks: + setup: + cmd: ["sh", "-c", "echo 'Setup complete' > setup.txt"] + target: ["setup.txt"] + + build: + cmd: ["sh", "-c", "cat setup.txt && echo 'Build complete' > build.txt"] + input: ["setup.txt"] + target: ["build.txt"] + dependsOn: ["setup"] + + final: + cmd: ["sh", "-c", "cat build.txt && echo 'Final step'"] + input: ["build.txt"] + dependsOn: ["build"] diff --git a/cli/e2e/testdata/error_circular_deps.txtar b/cli/e2e/testdata/error_circular_deps.txtar new file mode 100644 index 0000000..11bb127 --- /dev/null +++ b/cli/e2e/testdata/error_circular_deps.txtar @@ -0,0 +1,17 @@ +# Circular dependency should be detected +! exec same run --ci a +# Command should fail with non-zero exit code + +-- same.yaml -- +version: "1" +project: test +tasks: + a: + cmd: ["echo", "A"] + dependsOn: ["b"] + b: + cmd: ["echo", "B"] + dependsOn: ["c"] + c: + cmd: ["echo", "C"] + dependsOn: ["a"] diff --git a/cli/e2e/testdata/error_invalid_config.txtar b/cli/e2e/testdata/error_invalid_config.txtar new file mode 100644 index 0000000..f98a966 --- /dev/null +++ b/cli/e2e/testdata/error_invalid_config.txtar @@ -0,0 +1,9 @@ +# Invalid config should fail with error +! exec same run --ci task1 +stderr 'failed to load configuration' + +-- same.yaml -- +version: "1" +project: test +tasks: + invalid syntax here diff --git a/cli/e2e/testdata/error_missing_file.txtar b/cli/e2e/testdata/error_missing_file.txtar new file mode 100644 index 0000000..43c5848 --- /dev/null +++ b/cli/e2e/testdata/error_missing_file.txtar @@ -0,0 +1,3 @@ +# No same.yaml present +! exec same run --ci anything +stderr 'could not find samefile or workfile' diff --git a/cli/e2e/testdata/error_unknown_task.txtar b/cli/e2e/testdata/error_unknown_task.txtar new file mode 100644 index 0000000..ec534fa --- /dev/null +++ b/cli/e2e/testdata/error_unknown_task.txtar @@ -0,0 +1,9 @@ +! exec same run --ci nonexistent +# Command should fail with non-zero exit code + +-- same.yaml -- +version: "1" +project: test +tasks: + real: + cmd: ["echo", "exists"] diff --git a/cli/e2e/testdata/hermetic_env.txtar b/cli/e2e/testdata/hermetic_env.txtar new file mode 100644 index 0000000..5e756a5 --- /dev/null +++ b/cli/e2e/testdata/hermetic_env.txtar @@ -0,0 +1,22 @@ +# Set host variable +env TEST_VAR=from_host + +# Task without env should not see it +exec same run --ci no-env +! stdout 'from_host' + +# Task with explicit env should see it +exec same run --ci with-env +stdout 'custom_value' + +-- same.yaml -- +version: "1" +project: test +tasks: + no-env: + cmd: ["sh", "-c", "echo TEST_VAR=${TEST_VAR:-empty}"] + + with-env: + cmd: ["sh", "-c", "echo TEST_VAR=$TEST_VAR"] + environment: + TEST_VAR: "custom_value" diff --git a/cli/e2e/testdata/nix_hermetic.txtar b/cli/e2e/testdata/nix_hermetic.txtar new file mode 100644 index 0000000..e1a14f2 --- /dev/null +++ b/cli/e2e/testdata/nix_hermetic.txtar @@ -0,0 +1,22 @@ +# Multiple tools in isolated environment +# TODO: Re-enable when Nix tool resolution is fully implemented +skip 'Nix hermetic environments not yet implemented' + +exec same run --ci multi-tool +! stderr 'operation failed' + +-- same.work.yaml -- +version: "1" +root: "." +tools: + go: go@1.25.4 + lint: golangci-lint@2.7.2 +projects: ["."] + +-- same.yaml -- +version: "1" +project: test +tasks: + multi-tool: + cmd: ["sh", "-c", "go version && golangci-lint version"] + tools: ["go", "lint"] diff --git a/cli/e2e/testdata/nix_tool_resolution.txtar b/cli/e2e/testdata/nix_tool_resolution.txtar new file mode 100644 index 0000000..c624fc1 --- /dev/null +++ b/cli/e2e/testdata/nix_tool_resolution.txtar @@ -0,0 +1,21 @@ +# Task requires Go tool from Nix +# TODO: Re-enable when Nix tool resolution is fully implemented +skip 'Nix tool resolution not yet implemented' + +exec same run --ci check-go +! stderr 'operation failed' + +-- same.work.yaml -- +version: "1" +root: "." +tools: + go: go@1.25.4 +projects: ["."] + +-- same.yaml -- +version: "1" +project: test +tasks: + check-go: + cmd: ["go", "version"] + tools: ["go"] diff --git a/cli/e2e/testdata/no_cache_flag.txtar b/cli/e2e/testdata/no_cache_flag.txtar new file mode 100644 index 0000000..b5f0d61 --- /dev/null +++ b/cli/e2e/testdata/no_cache_flag.txtar @@ -0,0 +1,23 @@ +# First run +exec same run --ci task +stderr '\[task\] ✓ Completed' + +# Cached run +exec same run --ci task +stderr 'Cached' + +# Force rebuild with --no-cache +exec same run --ci --no-cache task +stderr '\[task\] Starting' +! stderr 'Cached' + +-- same.yaml -- +version: "1" +project: test +tasks: + task: + cmd: ["echo", "running"] + input: ["data.txt"] + +-- data.txt -- +stable diff --git a/cli/e2e/testdata/workspace.txtar b/cli/e2e/testdata/workspace.txtar new file mode 100644 index 0000000..adee4db --- /dev/null +++ b/cli/e2e/testdata/workspace.txtar @@ -0,0 +1,30 @@ +# Run task from sub-project +cd proj1 +exec same run --ci build +stderr '\[build\] ✓ Completed' + +# Run task from another project +cd ../proj2 +exec same run --ci test +stderr '\[test\] ✓ Completed' + +-- same.work.yaml -- +version: "1" +root: "." +projects: + - "proj1" + - "proj2" + +-- proj1/same.yaml -- +version: "1" +project: proj1 +tasks: + build: + cmd: ["echo", "Building proj1"] + +-- proj2/same.yaml -- +version: "1" +project: proj2 +tasks: + test: + cmd: ["echo", "Testing proj2"] diff --git a/cli/go.mod b/cli/go.mod index 02b4a60..32d2feb 100644 --- a/cli/go.mod +++ b/cli/go.mod @@ -9,6 +9,7 @@ require ( github.com/creack/pty v1.1.24 github.com/grindlemire/graft v0.2.3 github.com/muesli/termenv v0.16.0 + github.com/rogpeppe/go-internal v1.14.1 github.com/spf13/cobra v1.10.2 github.com/stretchr/testify v1.11.1 github.com/vito/midterm v0.2.3 @@ -18,6 +19,7 @@ require ( go.trai.ch/zerr v0.2.1 go.uber.org/mock v0.6.0 golang.org/x/sync v0.19.0 + golang.org/x/term v0.39.0 gopkg.in/yaml.v3 v3.0.1 ) @@ -51,7 +53,6 @@ require ( go.opentelemetry.io/otel/metric v1.39.0 // indirect golang.org/x/mod v0.31.0 // indirect golang.org/x/sys v0.40.0 // indirect - golang.org/x/term v0.39.0 // indirect golang.org/x/text v0.3.8 // indirect golang.org/x/tools v0.40.0 // indirect ) diff --git a/cli/go.sum b/cli/go.sum index d037c16..0f6002b 100644 --- a/cli/go.sum +++ b/cli/go.sum @@ -109,8 +109,6 @@ golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= -golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY= diff --git a/cli/same.yaml b/cli/same.yaml index 30a987b..5ddb01a 100644 --- a/cli/same.yaml +++ b/cli/same.yaml @@ -38,3 +38,10 @@ tasks: target: ["bin/same"] tools: ["go"] dependsOn: ["generate"] + + # Run E2E tests + e2e-test: + input: ["e2e", "cmd", "internal"] + cmd: ["go", "test", "-v", "-tags=e2e", "./e2e/..."] + tools: ["go"] + dependsOn: ["generate"] diff --git a/flake.nix b/flake.nix index 0970aa9..d83c05f 100644 --- a/flake.nix +++ b/flake.nix @@ -43,7 +43,7 @@ inherit version; src = ./cli; - vendorHash = "sha256-dtmW7rFQfLBww7mNMPTkyG3SvkcU2En+bHY2BTA8y4w="; + vendorHash = "sha256-O9y+DIxt8YcqlP499Ns5ECHEWV2IENy6nAH25Leh1AI="; env.CGO_ENABLED = 0;