mirror of
https://gitlab.com/TECHNOFAB/nixtest.git
synced 2025-12-12 10:10:09 +01:00
391 lines
14 KiB
Go
391 lines
14 KiB
Go
|
|
package runner
|
||
|
|
|
||
|
|
import (
|
||
|
|
"errors"
|
||
|
|
"fmt"
|
||
|
|
"os"
|
||
|
|
"path/filepath"
|
||
|
|
"strings"
|
||
|
|
"testing"
|
||
|
|
"time"
|
||
|
|
|
||
|
|
apperrors "gitlab.com/technofab/nixtest/internal/errors"
|
||
|
|
"gitlab.com/technofab/nixtest/internal/types"
|
||
|
|
)
|
||
|
|
|
||
|
|
// --- Mock Service Implementations ---
|
||
|
|
|
||
|
|
type mockNixService struct {
|
||
|
|
BuildDerivationFunc func(derivation string) (string, error)
|
||
|
|
BuildAndParseJSONFunc func(derivation string) (any, error)
|
||
|
|
BuildAndRunScriptFunc func(derivation string, pureEnv bool) (exitCode int, stdout string, stderr string, err error)
|
||
|
|
}
|
||
|
|
|
||
|
|
func (m *mockNixService) BuildDerivation(d string) (string, error) {
|
||
|
|
if m.BuildDerivationFunc == nil {
|
||
|
|
panic("mockNixService.BuildDerivationFunc not set")
|
||
|
|
}
|
||
|
|
return m.BuildDerivationFunc(d)
|
||
|
|
}
|
||
|
|
func (m *mockNixService) BuildAndParseJSON(d string) (any, error) {
|
||
|
|
if m.BuildAndParseJSONFunc == nil {
|
||
|
|
panic("mockNixService.BuildAndParseJSONFunc not set")
|
||
|
|
}
|
||
|
|
return m.BuildAndParseJSONFunc(d)
|
||
|
|
}
|
||
|
|
func (m *mockNixService) BuildAndRunScript(d string, p bool) (int, string, string, error) {
|
||
|
|
if m.BuildAndRunScriptFunc == nil {
|
||
|
|
panic("mockNixService.BuildAndRunScriptFunc not set")
|
||
|
|
}
|
||
|
|
return m.BuildAndRunScriptFunc(d, p)
|
||
|
|
}
|
||
|
|
|
||
|
|
type mockSnapshotService struct {
|
||
|
|
GetPathFunc func(snapshotDir string, testName string) string
|
||
|
|
CreateFileFunc func(filePath string, data any) error
|
||
|
|
LoadFileFunc func(filePath string) (any, error)
|
||
|
|
StatFunc func(name string) (os.FileInfo, error)
|
||
|
|
}
|
||
|
|
|
||
|
|
func (m *mockSnapshotService) GetPath(sDir string, tName string) string {
|
||
|
|
if m.GetPathFunc == nil { // provide a default if not overridden
|
||
|
|
return filepath.Join(sDir, strings.ToLower(strings.ReplaceAll(tName, " ", "_"))+".snap.json")
|
||
|
|
}
|
||
|
|
return m.GetPathFunc(sDir, tName)
|
||
|
|
}
|
||
|
|
func (m *mockSnapshotService) CreateFile(fp string, d any) error {
|
||
|
|
if m.CreateFileFunc == nil {
|
||
|
|
panic("mockSnapshotService.CreateFileFunc not set")
|
||
|
|
}
|
||
|
|
return m.CreateFileFunc(fp, d)
|
||
|
|
}
|
||
|
|
func (m *mockSnapshotService) LoadFile(fp string) (any, error) {
|
||
|
|
if m.LoadFileFunc == nil {
|
||
|
|
panic("mockSnapshotService.LoadFileFunc not set")
|
||
|
|
}
|
||
|
|
return m.LoadFileFunc(fp)
|
||
|
|
}
|
||
|
|
func (m *mockSnapshotService) Stat(n string) (os.FileInfo, error) {
|
||
|
|
if m.StatFunc == nil {
|
||
|
|
panic("mockSnapshotService.StatFunc not set")
|
||
|
|
}
|
||
|
|
return m.StatFunc(n)
|
||
|
|
}
|
||
|
|
|
||
|
|
// mockFileInfo for snapshot.Stat
|
||
|
|
type mockFileInfo struct {
|
||
|
|
name string
|
||
|
|
isDir bool
|
||
|
|
modTime time.Time
|
||
|
|
size int64
|
||
|
|
mode os.FileMode
|
||
|
|
}
|
||
|
|
|
||
|
|
func (m mockFileInfo) Name() string { return m.name }
|
||
|
|
func (m mockFileInfo) IsDir() bool { return m.isDir }
|
||
|
|
func (m mockFileInfo) ModTime() time.Time { return m.modTime }
|
||
|
|
func (m mockFileInfo) Size() int64 { return m.size }
|
||
|
|
func (m mockFileInfo) Mode() os.FileMode { return m.mode }
|
||
|
|
func (m mockFileInfo) Sys() any { return nil }
|
||
|
|
|
||
|
|
// --- Test Cases ---
|
||
|
|
|
||
|
|
func TestNewRunner(t *testing.T) {
|
||
|
|
mockNix := &mockNixService{}
|
||
|
|
mockSnap := &mockSnapshotService{}
|
||
|
|
tests := []struct {
|
||
|
|
name string
|
||
|
|
cfg Config
|
||
|
|
wantErr bool
|
||
|
|
skipPattern string
|
||
|
|
}{
|
||
|
|
{"Valid config, no skip", Config{NumWorkers: 1}, false, ""},
|
||
|
|
{"Valid config, valid skip", Config{NumWorkers: 1, SkipPattern: "Test.*"}, false, "Test.*"},
|
||
|
|
{"Invalid skip pattern", Config{NumWorkers: 1, SkipPattern: "[invalid"}, true, ""},
|
||
|
|
}
|
||
|
|
for _, tt := range tests {
|
||
|
|
t.Run(tt.name, func(t *testing.T) {
|
||
|
|
r, err := New(tt.cfg, mockNix, mockSnap)
|
||
|
|
if (err != nil) != tt.wantErr {
|
||
|
|
t.Errorf("New() error = %v, wantErr %v", err, tt.wantErr)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
if err == nil {
|
||
|
|
if tt.skipPattern == "" && r.skipRegex != nil {
|
||
|
|
t.Errorf("Expected nil skipRegex, got %v", r.skipRegex)
|
||
|
|
}
|
||
|
|
if tt.skipPattern != "" && (r.skipRegex == nil || r.skipRegex.String() != tt.skipPattern) {
|
||
|
|
t.Errorf("Expected skipRegex %q, got %v", tt.skipPattern, r.skipRegex)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
})
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestRunner_executeTest(t *testing.T) {
|
||
|
|
tempDir := t.TempDir() // used for snapshotDir in runnerConfig
|
||
|
|
|
||
|
|
tests := []struct {
|
||
|
|
name string
|
||
|
|
spec types.TestSpec
|
||
|
|
runnerConfig Config
|
||
|
|
setupMockServices func(t *testing.T, mockNix *mockNixService, mockSnap *mockSnapshotService, spec types.TestSpec, cfg Config)
|
||
|
|
wantStatus types.TestStatus
|
||
|
|
wantErrMsgContains string
|
||
|
|
wantActual string
|
||
|
|
wantExpected string
|
||
|
|
}{
|
||
|
|
// --- Invalid ---
|
||
|
|
{
|
||
|
|
name: "Invalid test type",
|
||
|
|
spec: types.TestSpec{Name: "Invalid", Type: "invalid"},
|
||
|
|
runnerConfig: Config{},
|
||
|
|
setupMockServices: func(t *testing.T, mNix *mockNixService, mSnap *mockSnapshotService, s types.TestSpec, c Config) {
|
||
|
|
// No service calls expected
|
||
|
|
},
|
||
|
|
wantStatus: types.StatusError,
|
||
|
|
},
|
||
|
|
// --- Skip ---
|
||
|
|
{
|
||
|
|
name: "Skip test due to pattern",
|
||
|
|
spec: types.TestSpec{Name: "SkipThisTest", Type: types.TestTypeUnit},
|
||
|
|
runnerConfig: Config{SkipPattern: "SkipThis.*"},
|
||
|
|
setupMockServices: func(t *testing.T, mNix *mockNixService, mSnap *mockSnapshotService, s types.TestSpec, c Config) {
|
||
|
|
// No service calls expected
|
||
|
|
},
|
||
|
|
wantStatus: types.StatusSkipped,
|
||
|
|
},
|
||
|
|
// --- Unit Tests ---
|
||
|
|
{
|
||
|
|
name: "Unit test success",
|
||
|
|
spec: types.TestSpec{Name: "UnitSuccess", Type: types.TestTypeUnit, Expected: "hello", Actual: "hello"},
|
||
|
|
runnerConfig: Config{},
|
||
|
|
setupMockServices: func(t *testing.T, mNix *mockNixService, mSnap *mockSnapshotService, s types.TestSpec, c Config) {},
|
||
|
|
wantStatus: types.StatusSuccess,
|
||
|
|
},
|
||
|
|
{
|
||
|
|
name: "Unit test failure",
|
||
|
|
spec: types.TestSpec{Name: "UnitFail", Type: types.TestTypeUnit, Expected: map[string]int{"a": 1}, Actual: map[string]int{"a": 2}},
|
||
|
|
runnerConfig: Config{},
|
||
|
|
setupMockServices: func(t *testing.T, mNix *mockNixService, mSnap *mockSnapshotService, s types.TestSpec, c Config) {},
|
||
|
|
wantStatus: types.StatusFailure,
|
||
|
|
wantExpected: "{\n \"a\": 1\n}",
|
||
|
|
wantActual: "{\n \"a\": 2\n}",
|
||
|
|
},
|
||
|
|
{
|
||
|
|
name: "Unit test success with ActualDrv",
|
||
|
|
spec: types.TestSpec{Name: "UnitActualDrvSuccess", Type: types.TestTypeUnit, Expected: map[string]any{"key": "val"}, ActualDrv: "drv.actual"},
|
||
|
|
runnerConfig: Config{},
|
||
|
|
setupMockServices: func(t *testing.T, mNix *mockNixService, mSnap *mockSnapshotService, s types.TestSpec, c Config) {
|
||
|
|
mNix.BuildAndParseJSONFunc = func(derivation string) (any, error) {
|
||
|
|
if derivation == "drv.actual" {
|
||
|
|
return map[string]any{"key": "val"}, nil
|
||
|
|
}
|
||
|
|
return nil, fmt.Errorf("unexpected drv: %s", derivation)
|
||
|
|
}
|
||
|
|
},
|
||
|
|
wantStatus: types.StatusSuccess,
|
||
|
|
},
|
||
|
|
{
|
||
|
|
name: "Unit test error (ActualDrv build fail)",
|
||
|
|
spec: types.TestSpec{Name: "UnitActualDrvError", Type: types.TestTypeUnit, Expected: "any", ActualDrv: "drv.actual.fail"},
|
||
|
|
runnerConfig: Config{},
|
||
|
|
setupMockServices: func(t *testing.T, mNix *mockNixService, mSnap *mockSnapshotService, s types.TestSpec, c Config) {
|
||
|
|
mNix.BuildAndParseJSONFunc = func(derivation string) (any, error) {
|
||
|
|
return nil, &apperrors.NixBuildError{Derivation: "drv.actual.fail", Err: errors.New("build failed")}
|
||
|
|
}
|
||
|
|
},
|
||
|
|
wantStatus: types.StatusError,
|
||
|
|
wantErrMsgContains: "failed to build/parse actualDrv drv.actual.fail: nix build for drv.actual.fail failed: build failed",
|
||
|
|
},
|
||
|
|
// --- Snapshot Tests ---
|
||
|
|
{
|
||
|
|
name: "Snapshot test success (existing snapshot match)",
|
||
|
|
spec: types.TestSpec{Name: "SnapSuccess", Type: types.TestTypeSnapshot, Actual: map[string]any{"data": "match"}},
|
||
|
|
runnerConfig: Config{SnapshotDir: tempDir},
|
||
|
|
setupMockServices: func(t *testing.T, mNix *mockNixService, mSnap *mockSnapshotService, s types.TestSpec, c Config) {
|
||
|
|
snapPath := mSnap.GetPath(c.SnapshotDir, s.Name)
|
||
|
|
mSnap.StatFunc = func(name string) (os.FileInfo, error) {
|
||
|
|
if name == snapPath {
|
||
|
|
return mockFileInfo{name: filepath.Base(snapPath)}, nil
|
||
|
|
}
|
||
|
|
return nil, os.ErrNotExist
|
||
|
|
}
|
||
|
|
mSnap.LoadFileFunc = func(filePath string) (any, error) {
|
||
|
|
if filePath == snapPath {
|
||
|
|
return map[string]any{"data": "match"}, nil
|
||
|
|
}
|
||
|
|
return nil, os.ErrNotExist
|
||
|
|
}
|
||
|
|
},
|
||
|
|
wantStatus: types.StatusSuccess,
|
||
|
|
},
|
||
|
|
{
|
||
|
|
name: "Snapshot test update (snapshot created, no prior)",
|
||
|
|
spec: types.TestSpec{Name: "SnapUpdateNew", Type: types.TestTypeSnapshot, Actual: map[string]any{"data": "new"}},
|
||
|
|
runnerConfig: Config{SnapshotDir: tempDir, UpdateSnapshots: true},
|
||
|
|
setupMockServices: func(t *testing.T, mNix *mockNixService, mSnap *mockSnapshotService, s types.TestSpec, c Config) {
|
||
|
|
snapPath := mSnap.GetPath(c.SnapshotDir, s.Name)
|
||
|
|
mSnap.CreateFileFunc = func(filePath string, data any) error {
|
||
|
|
if filePath == snapPath {
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
return fmt.Errorf("unexpected create path: %s", filePath)
|
||
|
|
}
|
||
|
|
mSnap.StatFunc = func(name string) (os.FileInfo, error) {
|
||
|
|
if name == snapPath {
|
||
|
|
return mockFileInfo{name: filepath.Base(snapPath)}, nil
|
||
|
|
}
|
||
|
|
return nil, os.ErrNotExist
|
||
|
|
}
|
||
|
|
mSnap.LoadFileFunc = func(filePath string) (any, error) {
|
||
|
|
if filePath == snapPath {
|
||
|
|
return s.Actual, nil
|
||
|
|
}
|
||
|
|
return nil, os.ErrNotExist
|
||
|
|
}
|
||
|
|
},
|
||
|
|
wantStatus: types.StatusSuccess,
|
||
|
|
},
|
||
|
|
// --- Script Tests ---
|
||
|
|
{
|
||
|
|
name: "Script test success (exit 0)",
|
||
|
|
spec: types.TestSpec{Name: "ScriptSuccess", Type: types.TestTypeScript, Script: "script.sh"},
|
||
|
|
runnerConfig: 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) {
|
||
|
|
return 0, "stdout", "stderr", nil
|
||
|
|
}
|
||
|
|
},
|
||
|
|
wantStatus: types.StatusSuccess,
|
||
|
|
},
|
||
|
|
{
|
||
|
|
name: "Script test failure (exit non-0)",
|
||
|
|
spec: types.TestSpec{Name: "ScriptFail", Type: types.TestTypeScript, Script: "script.sh"},
|
||
|
|
runnerConfig: 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) {
|
||
|
|
return 1, "out on fail", "err on fail", nil
|
||
|
|
}
|
||
|
|
},
|
||
|
|
wantStatus: types.StatusFailure,
|
||
|
|
wantErrMsgContains: "[exit code 1]\n[stdout]\nout on fail\n[stderr]\nerr on fail",
|
||
|
|
},
|
||
|
|
}
|
||
|
|
|
||
|
|
for _, tt := range tests {
|
||
|
|
t.Run(tt.name, func(t *testing.T) {
|
||
|
|
mockNixSvc := &mockNixService{}
|
||
|
|
mockSnapSvc := &mockSnapshotService{}
|
||
|
|
|
||
|
|
tt.setupMockServices(t, mockNixSvc, mockSnapSvc, tt.spec, tt.runnerConfig)
|
||
|
|
|
||
|
|
r, err := New(tt.runnerConfig, mockNixSvc, mockSnapSvc)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("New() failed: %v", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
result := r.executeTest(tt.spec)
|
||
|
|
|
||
|
|
if result.Status != tt.wantStatus {
|
||
|
|
t.Errorf("executeTest() status = %s, want %s. ErrorMsg: %s", result.Status, tt.wantStatus, result.ErrorMessage)
|
||
|
|
}
|
||
|
|
if tt.wantErrMsgContains != "" && !strings.Contains(result.ErrorMessage, tt.wantErrMsgContains) {
|
||
|
|
t.Errorf("executeTest() ErrorMessage = %q, want to contain %q", result.ErrorMessage, tt.wantErrMsgContains)
|
||
|
|
}
|
||
|
|
if result.Status == types.StatusFailure {
|
||
|
|
if tt.wantExpected != "" && result.Expected != tt.wantExpected {
|
||
|
|
t.Errorf("executeTest() Expected diff string mismatch.\nGot:\n%s\nWant:\n%s", result.Expected, tt.wantExpected)
|
||
|
|
}
|
||
|
|
if tt.wantActual != "" && result.Actual != tt.wantActual {
|
||
|
|
t.Errorf("executeTest() Actual diff string mismatch.\nGot:\n%s\nWant:\n%s", result.Actual, tt.wantActual)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if result.Duration <= 0 && result.Status != types.StatusSkipped {
|
||
|
|
t.Errorf("executeTest() Duration = %v, want > 0", result.Duration)
|
||
|
|
}
|
||
|
|
})
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestRunner_RunTests(t *testing.T) {
|
||
|
|
mockNixSvc := &mockNixService{}
|
||
|
|
mockSnapSvc := &mockSnapshotService{}
|
||
|
|
|
||
|
|
mockNixSvc.BuildAndParseJSONFunc = func(derivation string) (any, error) { return "parsed", nil }
|
||
|
|
mockNixSvc.BuildAndRunScriptFunc = func(derivation string, pureEnv bool) (int, string, string, error) { return 0, "", "", nil }
|
||
|
|
mockSnapSvc.StatFunc = func(name string) (os.FileInfo, error) { return mockFileInfo{}, nil }
|
||
|
|
mockSnapSvc.LoadFileFunc = func(filePath string) (any, error) { return "snapshot", nil }
|
||
|
|
mockSnapSvc.CreateFileFunc = func(filePath string, data any) error { return nil }
|
||
|
|
|
||
|
|
suites := []types.SuiteSpec{
|
||
|
|
{Name: "Suite1", Tests: []types.TestSpec{
|
||
|
|
{Name: "S1_Test1_Pass", Type: types.TestTypeUnit, Actual: "a", Expected: "a"},
|
||
|
|
{Name: "S1_Test2_Fail", Type: types.TestTypeUnit, Actual: "a", Expected: "b"},
|
||
|
|
}},
|
||
|
|
{Name: "Suite2", Tests: []types.TestSpec{
|
||
|
|
{Name: "S2_Test1_Pass", Type: types.TestTypeUnit, Actual: "c", Expected: "c"},
|
||
|
|
{Name: "S2_Test2_SkipThis", Type: types.TestTypeUnit, Actual: "d", Expected: "d"},
|
||
|
|
}},
|
||
|
|
}
|
||
|
|
|
||
|
|
runnerCfg := Config{NumWorkers: 2, SkipPattern: ".*SkipThis.*"}
|
||
|
|
testRunner, err := New(runnerCfg, mockNixSvc, mockSnapSvc)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("New() failed: %v", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
results := testRunner.RunTests(suites)
|
||
|
|
|
||
|
|
totalTestsProcessed := 0
|
||
|
|
suite1Results, ok1 := results["Suite1"]
|
||
|
|
if !ok1 {
|
||
|
|
t.Fatalf("Missing results for Suite1")
|
||
|
|
}
|
||
|
|
totalTestsProcessed += len(suite1Results)
|
||
|
|
|
||
|
|
suite2Results, ok2 := results["Suite2"]
|
||
|
|
if !ok2 {
|
||
|
|
t.Fatalf("Missing results for Suite2")
|
||
|
|
}
|
||
|
|
totalTestsProcessed += len(suite2Results)
|
||
|
|
|
||
|
|
if totalTestsProcessed != 4 {
|
||
|
|
t.Errorf("RunTests() processed %d tests, want 4", totalTestsProcessed)
|
||
|
|
}
|
||
|
|
|
||
|
|
// Check statuses
|
||
|
|
foundS1T1, foundS1T2, foundS2T1, foundS2T2 := false, false, false, false
|
||
|
|
for _, res := range suite1Results {
|
||
|
|
if res.Spec.Name == "S1_Test1_Pass" {
|
||
|
|
foundS1T1 = true
|
||
|
|
if res.Status != types.StatusSuccess {
|
||
|
|
t.Errorf("S1_Test1_Pass status %s, want Success", res.Status)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if res.Spec.Name == "S1_Test2_Fail" {
|
||
|
|
foundS1T2 = true
|
||
|
|
if res.Status != types.StatusFailure {
|
||
|
|
t.Errorf("S1_Test2_Fail status %s, want Failure", res.Status)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
for _, res := range suite2Results {
|
||
|
|
if res.Spec.Name == "S2_Test1_Pass" {
|
||
|
|
foundS2T1 = true
|
||
|
|
if res.Status != types.StatusSuccess {
|
||
|
|
t.Errorf("S2_Test1_Pass status %s, want Success", res.Status)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if res.Spec.Name == "S2_Test2_SkipThis" {
|
||
|
|
foundS2T2 = true
|
||
|
|
if res.Status != types.StatusSkipped {
|
||
|
|
t.Errorf("S2_Test2_SkipThis status %s, want Skipped", res.Status)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if !foundS1T1 || !foundS1T2 || !foundS2T1 || !foundS2T2 {
|
||
|
|
t.Errorf("Not all tests were found in results map. S1T1:%v, S1T2:%v, S2T1:%v, S2T2:%v", foundS1T1, foundS1T2, foundS2T1, foundS2T2)
|
||
|
|
}
|
||
|
|
}
|