Compare commits

...

2 commits

12 changed files with 42 additions and 43 deletions

View file

@ -60,7 +60,7 @@ func main() {
SnapshotDir: appCfg.SnapshotDir, SnapshotDir: appCfg.SnapshotDir,
UpdateSnapshots: appCfg.UpdateSnapshots, UpdateSnapshots: appCfg.UpdateSnapshots,
SkipPattern: appCfg.SkipPattern, SkipPattern: appCfg.SkipPattern,
PureEnv: appCfg.PureEnv, ImpureEnv: appCfg.ImpureEnv,
} }
testRunner, err := runner.New(runnerCfg, nixService, snapshotService) testRunner, err := runner.New(runnerCfg, nixService, snapshotService)
if err != nil { if err != nil {

View file

@ -4,7 +4,7 @@
Usage of nixtest: Usage of nixtest:
--junit string Path to generate JUNIT report to, leave empty to disable --junit string Path to generate JUNIT report to, leave empty to disable
--no-color Disable coloring --no-color Disable coloring
--pure Unset all env vars before running script tests --impure Don\'t unset all env vars before running script tests
-s, --skip string Regular expression to skip tests (e.g., 'test-.*|.*-b') -s, --skip string Regular expression to skip tests (e.g., 'test-.*|.*-b')
--snapshot-dir string Directory where snapshots are stored (default "./snapshots") --snapshot-dir string Directory where snapshots are stored (default "./snapshots")
-f, --tests string Path to JSON file containing tests (required) -f, --tests string Path to JSON file containing tests (required)

View file

@ -96,10 +96,10 @@ Examples:
name = "script-test"; name = "script-test";
type = "script"; type = "script";
script = script =
# there are two modes, "default"/"impure" and "pure" # there are two modes, "default"/"pure" and "impure"
# in impure mode all env variables etc. from your current session are kept # in impure mode all env variables etc. from your current session are kept
# and are available to the test # and are available to the test (using --impure).
# to make it more reproducible and cleaner, use --pure to switch to pure # to make it more reproducible and cleaner, the default is pure
# mode which will unset all env variables before running the test. That # mode which will unset all env variables before running the test. That
# requires you to set PATH yourself then: # requires you to set PATH yourself then:
# #

2
go.mod
View file

@ -1,6 +1,6 @@
module gitlab.com/technofab/nixtest module gitlab.com/technofab/nixtest
go 1.24.2 go 1.23.0
require ( require (
github.com/akedrou/textdiff v0.1.0 github.com/akedrou/textdiff v0.1.0

View file

@ -16,7 +16,7 @@ type AppConfig struct {
JunitPath string JunitPath string
UpdateSnapshots bool UpdateSnapshots bool
SkipPattern string SkipPattern string
PureEnv bool ImpureEnv bool
NoColor bool NoColor bool
} }
@ -29,7 +29,7 @@ func Load() AppConfig {
flag.StringVar(&cfg.JunitPath, "junit", "", "Path to generate JUNIT report to, leave empty to disable") flag.StringVar(&cfg.JunitPath, "junit", "", "Path to generate JUNIT report to, leave empty to disable")
flag.BoolVarP(&cfg.UpdateSnapshots, "update-snapshots", "u", false, "Update all snapshots") flag.BoolVarP(&cfg.UpdateSnapshots, "update-snapshots", "u", false, "Update all snapshots")
flag.StringVarP(&cfg.SkipPattern, "skip", "s", "", "Regular expression to skip tests (e.g., 'test-.*|.*-b')") flag.StringVarP(&cfg.SkipPattern, "skip", "s", "", "Regular expression to skip tests (e.g., 'test-.*|.*-b')")
flag.BoolVar(&cfg.PureEnv, "pure", false, "Unset all env vars before running script tests") flag.BoolVar(&cfg.ImpureEnv, "impure", false, "Don't unset all env vars before running script tests")
flag.BoolVar(&cfg.NoColor, "no-color", false, "Disable coloring") flag.BoolVar(&cfg.NoColor, "no-color", false, "Disable coloring")
helpRequested := flag.BoolP("help", "h", false, "Show this menu") helpRequested := flag.BoolP("help", "h", false, "Show this menu")

View file

@ -64,7 +64,7 @@ func TestLoad_CustomValues(t *testing.T) {
"--junit", "report.xml", "--junit", "report.xml",
"-u", "-u",
"--skip", "specific-test", "--skip", "specific-test",
"--pure", "--impure",
"--no-color", "--no-color",
} }
pflag.CommandLine = pflag.NewFlagSet(os.Args[0], pflag.ExitOnError) // Reset flags pflag.CommandLine = pflag.NewFlagSet(os.Args[0], pflag.ExitOnError) // Reset flags
@ -83,7 +83,7 @@ func TestLoad_CustomValues(t *testing.T) {
if cfg.SkipPattern != "specific-test" { if cfg.SkipPattern != "specific-test" {
t.Errorf("SkipPattern: got %s, want specific-test", cfg.SkipPattern) t.Errorf("SkipPattern: got %s, want specific-test", cfg.SkipPattern)
} }
if !cfg.PureEnv { if !cfg.ImpureEnv {
t.Errorf("PureEnv: got %v, want true", cfg.PureEnv) t.Errorf("ImpureEnv: got %v, want true", cfg.ImpureEnv)
} }
} }

View file

@ -3,6 +3,7 @@ package nix
import ( import (
"bytes" "bytes"
"encoding/json" "encoding/json"
"fmt"
"os" "os"
"os/exec" "os/exec"
"strings" "strings"
@ -14,7 +15,7 @@ import (
type Service interface { type Service interface {
BuildDerivation(derivation string) (string, error) BuildDerivation(derivation string) (string, error)
BuildAndParseJSON(derivation string) (any, error) BuildAndParseJSON(derivation string) (any, error)
BuildAndRunScript(derivation string, pureEnv bool) (exitCode int, stdout string, stderr string, err error) BuildAndRunScript(derivation string, impureEnv bool) (exitCode int, stdout string, stderr string, err error)
} }
type DefaultService struct { type DefaultService struct {
@ -72,21 +73,29 @@ func (s *DefaultService) BuildAndParseJSON(derivation string) (any, error) {
} }
// BuildAndRunScript builds a derivation and runs it as a script // BuildAndRunScript builds a derivation and runs it as a script
func (s *DefaultService) BuildAndRunScript(derivation string, pureEnv bool) (exitCode int, stdout string, stderr string, err error) { func (s *DefaultService) BuildAndRunScript(derivation string, impureEnv bool) (exitCode int, stdout string, stderr string, err error) {
exitCode = -1 exitCode = -1
path, err := s.BuildDerivation(derivation) path, err := s.BuildDerivation(derivation)
if err != nil { if err != nil {
return exitCode, "", "", err return exitCode, "", "", err
} }
// run scripts in a temporary directory
tempDir, err := os.MkdirTemp("", "nixtest-script-")
if err != nil {
return exitCode, "", "", &apperrors.ScriptExecutionError{Path: path, Err: fmt.Errorf("failed to create temporary directory: %w", err)}
}
defer os.RemoveAll(tempDir)
var cmdArgs []string var cmdArgs []string
if pureEnv { if impureEnv {
cmdArgs = append([]string{"env", "-i"}, "bash", path)
} else {
cmdArgs = []string{"bash", path} cmdArgs = []string{"bash", path}
} else {
cmdArgs = append([]string{"env", "-i"}, "bash", path)
} }
cmd := s.commandExecutor(cmdArgs[0], cmdArgs[1:]...) cmd := s.commandExecutor(cmdArgs[0], cmdArgs[1:]...)
cmd.Dir = tempDir
var outBuf, errBuf bytes.Buffer var outBuf, errBuf bytes.Buffer
cmd.Stdout = &outBuf cmd.Stdout = &outBuf
cmd.Stderr = &errBuf cmd.Stderr = &errBuf

View file

@ -232,7 +232,7 @@ func TestDefaultService_BuildAndRunScript(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
derivation string derivation string
pureEnv bool impureEnv bool
mockBuildDrvOutput string mockBuildDrvOutput string
mockBuildDrvError string mockBuildDrvError string
mockBuildDrvExitCode string mockBuildDrvExitCode string
@ -252,7 +252,7 @@ func TestDefaultService_BuildAndRunScript(t *testing.T) {
0, "Hello", "ErrOut", false, nil, "", 0, "Hello", "ErrOut", false, nil, "",
}, },
{ {
"Success pure", "script.drv#sh", true, mockScriptPath, "", "0", "Success impure", "script.drv#sh", true, mockScriptPath, "", "0",
"Hello", "ErrOut", "0", "Hello", "ErrOut", "0",
0, "Hello", "ErrOut", false, nil, "", 0, "Hello", "ErrOut", false, nil, "",
}, },
@ -277,7 +277,7 @@ func TestDefaultService_BuildAndRunScript(t *testing.T) {
os.Setenv("MOCK_SCRIPT_STDERR", tt.mockScriptStderr) os.Setenv("MOCK_SCRIPT_STDERR", tt.mockScriptStderr)
os.Setenv("MOCK_SCRIPT_EXIT_CODE", tt.mockScriptExitCode) os.Setenv("MOCK_SCRIPT_EXIT_CODE", tt.mockScriptExitCode)
exitCode, stdout, stderr, err := service.BuildAndRunScript(tt.derivation, tt.pureEnv) exitCode, stdout, stderr, err := service.BuildAndRunScript(tt.derivation, tt.impureEnv)
if (err != nil) != tt.wantErr { if (err != nil) != tt.wantErr {
t.Fatalf("BuildAndRunScript() error = %v, wantErr %v", err, tt.wantErr) t.Fatalf("BuildAndRunScript() error = %v, wantErr %v", err, tt.wantErr)

View file

@ -32,7 +32,7 @@ type Config struct {
SnapshotDir string SnapshotDir string
UpdateSnapshots bool UpdateSnapshots bool
SkipPattern string SkipPattern string
PureEnv bool ImpureEnv bool
} }
func New(cfg Config, nixService nix.Service, snapService snapshot.Service) (*Runner, error) { func New(cfg Config, nixService nix.Service, snapService snapshot.Service) (*Runner, error) {
@ -181,7 +181,7 @@ func (r *Runner) handleUnitTest(result *types.TestResult, spec types.TestSpec, a
// handleScriptTest processes script type tests // handleScriptTest processes script type tests
func (r *Runner) handleScriptTest(result *types.TestResult, spec types.TestSpec) { func (r *Runner) handleScriptTest(result *types.TestResult, spec types.TestSpec) {
exitCode, stdout, stderrStr, err := r.nixService.BuildAndRunScript(spec.Script, r.config.PureEnv) exitCode, stdout, stderrStr, err := r.nixService.BuildAndRunScript(spec.Script, r.config.ImpureEnv)
if err != nil { if err != nil {
result.Status = types.StatusError result.Status = types.StatusError
result.ErrorMessage = fmt.Sprintf("[system] failed to run script derivation %s: %v", spec.Script, err) result.ErrorMessage = fmt.Sprintf("[system] failed to run script derivation %s: %v", spec.Script, err)

View file

@ -18,7 +18,7 @@ import (
type mockNixService struct { type mockNixService struct {
BuildDerivationFunc func(derivation string) (string, error) BuildDerivationFunc func(derivation string) (string, error)
BuildAndParseJSONFunc func(derivation string) (any, error) BuildAndParseJSONFunc func(derivation string) (any, error)
BuildAndRunScriptFunc func(derivation string, pureEnv bool) (exitCode int, stdout string, stderr string, err error) BuildAndRunScriptFunc func(derivation string, impureEnv bool) (exitCode int, stdout string, stderr string, err error)
} }
func (m *mockNixService) BuildDerivation(d string) (string, error) { func (m *mockNixService) BuildDerivation(d string) (string, error) {
@ -253,7 +253,7 @@ func TestRunner_executeTest(t *testing.T) {
spec: types.TestSpec{Name: "ScriptSuccess", Type: types.TestTypeScript, Script: "script.sh"}, spec: types.TestSpec{Name: "ScriptSuccess", Type: types.TestTypeScript, Script: "script.sh"},
runnerConfig: Config{}, runnerConfig: Config{},
setupMockServices: func(t *testing.T, mNix *mockNixService, mSnap *mockSnapshotService, s types.TestSpec, c Config) { setupMockServices: func(t *testing.T, mNix *mockNixService, mSnap *mockSnapshotService, s types.TestSpec, c Config) {
mNix.BuildAndRunScriptFunc = func(derivation string, pureEnv bool) (int, string, string, error) { mNix.BuildAndRunScriptFunc = func(derivation string, impureEnv bool) (int, string, string, error) {
return 0, "stdout", "stderr", nil return 0, "stdout", "stderr", nil
} }
}, },
@ -264,7 +264,7 @@ func TestRunner_executeTest(t *testing.T) {
spec: types.TestSpec{Name: "ScriptFail", Type: types.TestTypeScript, Script: "script.sh"}, spec: types.TestSpec{Name: "ScriptFail", Type: types.TestTypeScript, Script: "script.sh"},
runnerConfig: Config{}, runnerConfig: Config{},
setupMockServices: func(t *testing.T, mNix *mockNixService, mSnap *mockSnapshotService, s types.TestSpec, c Config) { setupMockServices: func(t *testing.T, mNix *mockNixService, mSnap *mockSnapshotService, s types.TestSpec, c Config) {
mNix.BuildAndRunScriptFunc = func(derivation string, pureEnv bool) (int, string, string, error) { mNix.BuildAndRunScriptFunc = func(derivation string, impureEnv bool) (int, string, string, error) {
return 1, "out on fail", "err on fail", nil return 1, "out on fail", "err on fail", nil
} }
}, },
@ -313,7 +313,7 @@ func TestRunner_RunTests(t *testing.T) {
mockSnapSvc := &mockSnapshotService{} mockSnapSvc := &mockSnapshotService{}
mockNixSvc.BuildAndParseJSONFunc = func(derivation string) (any, error) { return "parsed", nil } mockNixSvc.BuildAndParseJSONFunc = func(derivation string) (any, error) { return "parsed", nil }
mockNixSvc.BuildAndRunScriptFunc = func(derivation string, pureEnv bool) (int, string, string, error) { return 0, "", "", nil } mockNixSvc.BuildAndRunScriptFunc = func(derivation string, impureEnv bool) (int, string, string, error) { return 0, "", "", nil }
mockSnapSvc.StatFunc = func(name string) (os.FileInfo, error) { return mockFileInfo{}, nil } mockSnapSvc.StatFunc = func(name string) (os.FileInfo, error) { return mockFileInfo{}, nil }
mockSnapSvc.LoadFileFunc = func(filePath string) (any, error) { return "snapshot", nil } mockSnapSvc.LoadFileFunc = func(filePath string) (any, error) { return "snapshot", nil }
mockSnapSvc.CreateFileFunc = func(filePath string, data any) error { return nil } mockSnapSvc.CreateFileFunc = func(filePath string, data any) error { return nil }

View file

@ -35,16 +35,6 @@ function assert_file_not_contains() {
} }
} }
function tmpdir() {
dir=$(mktemp -d)
trap "rm -rf $dir" EXIT
echo -n "$dir"
}
function tmpfile() {
file=$(mktemp)
trap "rm -f $file" EXIT
echo -n "$file"
}
function run() { function run() {
output=$($@ 2>&1) output=$($@ 2>&1)
exit_code=$? exit_code=$?

View file

@ -36,7 +36,7 @@
binary = binary =
(ntlib.mkBinary { (ntlib.mkBinary {
nixtests = "stub"; nixtests = "stub";
extraParams = "--pure"; extraParams = "--impure";
}) })
+ "/bin/nixtests:run"; + "/bin/nixtests:run";
in in
@ -45,7 +45,7 @@
${ntlib.helpers.path [pkgs.gnugrep]} ${ntlib.helpers.path [pkgs.gnugrep]}
${ntlib.helpers.scriptHelpers} ${ntlib.helpers.scriptHelpers}
assert_file_contains ${binary} "nixtest" "should contain nixtest" assert_file_contains ${binary} "nixtest" "should contain nixtest"
assert_file_contains ${binary} "--pure" "should contain --pure arg" assert_file_contains ${binary} "--impure" "should contain --impure arg"
assert_file_contains ${binary} "--tests=stub" "should contain --tests arg" assert_file_contains ${binary} "--tests=stub" "should contain --tests arg"
run "${binary} --help" run "${binary} --help"
@ -70,21 +70,21 @@
in in
# sh # sh
'' ''
${ntlib.helpers.path [pkgs.gnugrep pkgs.mktemp]} ${ntlib.helpers.path [pkgs.gnugrep pkgs.mktemp pkgs.coreutils]}
${ntlib.helpers.scriptHelpers} ${ntlib.helpers.scriptHelpers}
cp -r ${./../snapshots} snapshots
TMPDIR=$(tmpdir)
# start without nix & env binaries to expect errors # start without nix & env binaries to expect errors
run "${binary} --pure --junit=$TMPDIR/junit.xml" run "${binary} --junit=junit.xml"
assert "$exit_code -eq 2" "should exit 2" assert "$exit_code -eq 2" "should exit 2"
assert "-f $TMPDIR/junit.xml" "should create junit.xml" assert "-f junit.xml" "should create junit.xml"
assert_contains "$output" "executable file not found" "nix should not be found in pure mode" assert_contains "$output" "executable file not found" "nix should not be found in pure mode"
# now add required deps # now add required deps
${ntlib.helpers.pathAdd [pkgs.nix pkgs.coreutils]} ${ntlib.helpers.pathAdd [pkgs.nix pkgs.coreutils]}
run "${binary} --pure --junit=$TMPDIR/junit2.xml" run "${binary} --junit=junit2.xml"
assert "$exit_code -eq 2" "should exit 2" assert "$exit_code -eq 2" "should exit 2"
assert "-f $TMPDIR/junit2.xml" "should create junit2.xml" assert "-f junit2.xml" "should create junit2.xml"
assert_not_contains "$output" "executable file not found" "nix should now exist" assert_not_contains "$output" "executable file not found" "nix should now exist"
assert_contains "$output" "suite-one" "should contain suite-one" assert_contains "$output" "suite-one" "should contain suite-one"
assert_contains "$output" "8/11 (1 SKIPPED)" "should be 8/11 total" assert_contains "$output" "8/11 (1 SKIPPED)" "should be 8/11 total"