From 5f722cf3c08601877ac2ce17d7572824b6278004 Mon Sep 17 00:00:00 2001 From: Harsh Kapse Date: Sat, 10 Oct 2026 14:54:11 +0530 Subject: [PATCH] test(e2e): isolate test suites from host shell and git configs Developer shell variables (DIFFM_*, PAGER, COLUMNS) and host Git settings (GIT_DIR, ~/.gitconfig) leaked into test processes, causing tests to fail or produce unexpected output depending on the local setup. Sanitize user settings at TestMain entry across test packages, and isolate e2e CLI subprocesses so host Git configurations cannot interfere. - e2e: strip user config in TestMain and filter env in-place - e2e: point GIT_CONFIG_GLOBAL to devnull and set GIT_CONFIG_NOSYSTEM - e2e: add regression test verifying CLI runs with invalid host Git pointers - cmd: clear user config and pager vars while keeping DIFFM_TEST_* flags - git: clear GIT_DIR, GIT_WORK_TREE, and GIT_INDEX_FILE in TestMain - pager: clear DIFFM_NO_PAGER, GIT_PAGER, and PAGER in TestMain --- cmd/diff_test.go | 18 ++++++++++ internal/git/git_test.go | 8 +++++ internal/pager/pager_test.go | 8 +++++ tests/e2e/cli_test.go | 68 +++++++++++++++++++++++++++++++++++- 4 files changed, 101 insertions(+), 1 deletion(-) diff --git a/cmd/diff_test.go b/cmd/diff_test.go index 2ae6d90f..052dd8ff 100644 --- a/cmd/diff_test.go +++ b/cmd/diff_test.go @@ -13,6 +13,24 @@ import ( "github.com/spf13/cobra" ) +// TestMain clears user config and pager env vars so host settings don't break cmd tests. +func TestMain(m *testing.M) { + for _, env := range os.Environ() { + if k, _, ok := strings.Cut(env, "="); ok { + if strings.HasPrefix(k, "DIFFM_") && !strings.HasPrefix(k, "DIFFM_TEST_") { + _ = os.Unsetenv(k) + continue + } + switch k { + case "PAGER", "GIT_PAGER", "COLUMNS", "NO_COLOR", "CLICOLOR", "CLICOLOR_FORCE", + "GIT_DIR", "GIT_WORK_TREE", "GIT_INDEX_FILE": + _ = os.Unsetenv(k) + } + } + } + os.Exit(m.Run()) +} + func TestRootCmdFlags(t *testing.T) { flags := []struct { name string diff --git a/internal/git/git_test.go b/internal/git/git_test.go index 5af4b462..0dc454d1 100644 --- a/internal/git/git_test.go +++ b/internal/git/git_test.go @@ -9,6 +9,14 @@ import ( "testing" ) +// TestMain clears Git pointer env vars so the host repo state doesn't leak into tests. +func TestMain(m *testing.M) { + for _, k := range []string{"GIT_DIR", "GIT_WORK_TREE", "GIT_INDEX_FILE"} { + _ = os.Unsetenv(k) + } + os.Exit(m.Run()) +} + func TestIsGitRepository(t *testing.T) { cwd, err := os.Getwd() if err != nil { diff --git a/internal/pager/pager_test.go b/internal/pager/pager_test.go index e8817d8b..d69497f7 100644 --- a/internal/pager/pager_test.go +++ b/internal/pager/pager_test.go @@ -9,6 +9,14 @@ import ( "testing" ) +// TestMain clears pager env vars so host settings don't change test expectations. +func TestMain(m *testing.M) { + for _, k := range []string{"DIFFM_NO_PAGER", "GIT_PAGER", "PAGER"} { + _ = os.Unsetenv(k) + } + os.Exit(m.Run()) +} + func TestPager_Disabled(t *testing.T) { p, w := Start(true) if p != nil { diff --git a/tests/e2e/cli_test.go b/tests/e2e/cli_test.go index d9b4a3e9..a457597a 100644 --- a/tests/e2e/cli_test.go +++ b/tests/e2e/cli_test.go @@ -14,6 +14,7 @@ import ( "os/exec" "path/filepath" "runtime" + "slices" "strings" "testing" @@ -43,9 +44,27 @@ func TestMain(m *testing.M) { panic("failed to build diffm: " + string(out) + ": " + err.Error()) } + sanitizeTestEnv() os.Exit(m.Run()) } +// sanitizeTestEnv strips user config and pager env vars so shell settings don't bleed into e2e runs. +func sanitizeTestEnv() { + for _, env := range os.Environ() { + if k, _, ok := strings.Cut(env, "="); ok { + if strings.HasPrefix(k, "DIFFM_") && !strings.HasPrefix(k, "DIFFM_TEST_") { + _ = os.Unsetenv(k) + continue + } + switch k { + case "PAGER", "GIT_PAGER", "COLUMNS", "NO_COLOR", "CLICOLOR", "CLICOLOR_FORCE", + "GIT_DIR", "GIT_WORK_TREE", "GIT_INDEX_FILE": + _ = os.Unsetenv(k) + } + } + } +} + // testdataDir returns the absolute path to tests/testdata/. func testdataDir(t *testing.T) string { t.Helper() @@ -81,9 +100,27 @@ func fixtureFiles(t *testing.T, name string) (string, string) { return oldPath, newPath } -// runDiffm runs the diffm binary with the given args. +// isolatedEnv strips Git repo pointers and blocks host git configs so subprocesses run clean. +func isolatedEnv() []string { + env := slices.DeleteFunc(os.Environ(), func(kv string) bool { + k, _, ok := strings.Cut(kv, "=") + if !ok { + return true + } + switch k { + case "GIT_DIR", "GIT_WORK_TREE", "GIT_INDEX_FILE", "GIT_CONFIG_GLOBAL", "GIT_CONFIG_NOSYSTEM": + return true + default: + return false + } + }) + return append(env, "GIT_CONFIG_GLOBAL="+os.DevNull, "GIT_CONFIG_NOSYSTEM=1") +} + +// runDiffm runs the diffm binary with the given args in an isolated environment. func runDiffm(args ...string) (stdout, stderr string, err error) { cmd := exec.Command(binaryPath, args...) + cmd.Env = isolatedEnv() var outBuf, errBuf strings.Builder cmd.Stdout = &outBuf cmd.Stderr = &errBuf @@ -536,6 +573,35 @@ func TestCLI_EnvVarDefaults(t *testing.T) { } } +func TestCLI_SubprocessEnvironmentIsolation(t *testing.T) { + t.Setenv("GIT_DIR", "/invalid/git/dir") + t.Setenv("GIT_WORK_TREE", "/invalid/work/tree") + t.Setenv("GIT_INDEX_FILE", "/invalid/index/file") + + env := isolatedEnv() + for _, kv := range env { + k, _, _ := strings.Cut(kv, "=") + if k == "GIT_DIR" || k == "GIT_WORK_TREE" || k == "GIT_INDEX_FILE" { + t.Errorf("isolatedEnv leaked %s", k) + } + } + if !slices.Contains(env, "GIT_CONFIG_GLOBAL="+os.DevNull) { + t.Errorf("isolatedEnv missing GIT_CONFIG_GLOBAL=%s", os.DevNull) + } + if !slices.Contains(env, "GIT_CONFIG_NOSYSTEM=1") { + t.Error("isolatedEnv missing GIT_CONFIG_NOSYSTEM=1") + } + + oldPath, newPath := fixtureFiles(t, sampleFixture(t)) + stdout, stderr, err := runDiffm(oldPath, newPath) + if err != nil { + t.Fatalf("runDiffm failed under poisoned Git env: %v\nstderr: %s", err, stderr) + } + if stdout == "" { + t.Fatal("expected non-empty diff output under isolated env") + } +} + func TestCLI_GitMode_TypoFailFast(t *testing.T) { // In the current git repo, diffm with a typo file should fail fast _, stderr, err := runDiffm("HEAD", "nonexistent_typo_file_12345.go")