nixtest/internal/nix/service_test.go

306 lines
9.4 KiB
Go
Raw Normal View History

package nix
import (
"encoding/json"
"errors"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
apperrors "gitlab.com/technofab/nixtest/internal/errors"
)
func TestHelperProcess(t *testing.T) {
if os.Getenv("GO_WANT_HELPER_PROCESS") != "1" {
return
}
defer os.Exit(0)
args := os.Args
for len(args) > 0 && args[0] != "--" {
args = args[1:]
}
if len(args) == 0 {
fmt.Fprintln(os.Stderr, "No command after --")
os.Exit(1)
}
args = args[1:]
cmd, params := args[0], args[1:]
switch cmd {
case "nix":
if len(params) > 0 && params[0] == "build" {
mockOutput := os.Getenv("MOCK_NIX_BUILD_OUTPUT")
mockError := os.Getenv("MOCK_NIX_BUILD_ERROR")
mockExitCode := os.Getenv("MOCK_NIX_BUILD_EXIT_CODE")
if mockError != "" {
fmt.Fprintln(os.Stderr, mockError)
}
if mockExitCode != "" && mockExitCode != "0" {
os.Exit(1) // simplified exit for helper
}
if mockError == "" && (mockExitCode == "" || mockExitCode == "0") {
fmt.Fprintln(os.Stdout, mockOutput)
}
}
case "bash", "env":
scriptPath := params[0]
if cmd == "env" && len(params) > 2 {
scriptPath = params[2]
}
if _, err := os.Stat(scriptPath); err != nil && !os.IsNotExist(err) {
fmt.Fprintf(os.Stderr, "mocked script: script path %s could not be statted: %v\n", scriptPath, err)
os.Exit(3)
}
fmt.Fprint(os.Stdout, os.Getenv("MOCK_SCRIPT_STDOUT"))
fmt.Fprint(os.Stderr, os.Getenv("MOCK_SCRIPT_STDERR"))
if code := os.Getenv("MOCK_SCRIPT_EXIT_CODE"); code != "" && code != "0" {
os.Exit(5) // custom exit for script failure
}
default:
fmt.Fprintf(os.Stderr, "mocked command: unknown command %s\n", cmd)
os.Exit(126)
}
}
// mockExecCommand configures the DefaultService to use the test helper
func mockExecCommandForService(service *DefaultService) {
service.commandExecutor = func(command string, args ...string) *exec.Cmd {
cs := []string{"-test.run=TestHelperProcess", "--", command}
cs = append(cs, args...)
cmd := exec.Command(os.Args[0], cs...)
cmd.Env = []string{"GO_WANT_HELPER_PROCESS=1"}
cmd.Env = append(cmd.Env, os.Environ()...)
return cmd
}
}
func TestDefaultService_BuildDerivation(t *testing.T) {
service := NewDefaultService()
mockExecCommandForService(service) // configure service to use helper
tests := []struct {
name string
derivation string
mockOutput string
mockError string
mockExitCode string
wantPath string
wantErr bool
wantErrType any
wantErrMsgContains string
}{
{"Success", "some.drv#attr", "/nix/store/mock-path", "", "0", "/nix/store/mock-path", false, nil, ""},
{
"Nix command error", "error.drv#attr", "", "nix error details", "1", "", true,
(*apperrors.NixBuildError)(nil), "nix error details",
},
{
"Nix command success but no output path", "empty.drv#attr", "", "", "0", "", true,
(*apperrors.NixNoOutputPathError)(nil), "produced no output path",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
os.Setenv("MOCK_NIX_BUILD_OUTPUT", tt.mockOutput)
os.Setenv("MOCK_NIX_BUILD_ERROR", tt.mockError)
os.Setenv("MOCK_NIX_BUILD_EXIT_CODE", tt.mockExitCode)
defer func() {
os.Unsetenv("MOCK_NIX_BUILD_OUTPUT")
os.Unsetenv("MOCK_NIX_BUILD_ERROR")
os.Unsetenv("MOCK_NIX_BUILD_EXIT_CODE")
}()
gotPath, err := service.BuildDerivation(tt.derivation)
if (err != nil) != tt.wantErr {
t.Fatalf("BuildDerivation() error = %v, wantErr %v", err, tt.wantErr)
}
if tt.wantErr {
if tt.wantErrType != nil && !errors.As(err, &tt.wantErrType) {
t.Errorf("BuildDerivation() error type = %T, want %T", err, tt.wantErrType)
}
if tt.wantErrMsgContains != "" && !strings.Contains(err.Error(), tt.wantErrMsgContains) {
t.Errorf("BuildDerivation() error = %q, want error containing %q", err.Error(), tt.wantErrMsgContains)
}
}
if !tt.wantErr && gotPath != tt.wantPath {
t.Errorf("BuildDerivation() gotPath = %v, want %v", gotPath, tt.wantPath)
}
})
}
}
func TestDefaultService_BuildAndParseJSON(t *testing.T) {
service := NewDefaultService()
mockExecCommandForService(service)
tempDir := t.TempDir()
mockDrvOutputPath := filepath.Join(tempDir, "drv_output.json")
tests := []struct {
name string
derivation string
mockBuildOutput string
mockJSONContent string
mockBuildError string
mockBuildExitCode string
want any
wantErr bool
wantErrType any
wantErrMsgContains string
}{
{
"Success", "some.drv#json", mockDrvOutputPath, `{"key": "value"}`, "", "0",
map[string]any{"key": "value"}, false, nil, "",
},
{
"BuildDerivation fails", "error.drv#json", "", "", "nix build error", "1",
nil, true, (*apperrors.NixBuildError)(nil), "nix build error",
},
{
"ReadFile fails", "readfail.drv#json", "/nonexistent/path/output.json", "", "", "0",
nil, true, (*apperrors.FileReadError)(nil), "failed to read file",
},
{
"Unmarshal fails", "badjson.drv#json", mockDrvOutputPath, `{"key": "value"`, "", "0",
nil, true, (*apperrors.JSONUnmarshalError)(nil), "failed to unmarshal JSON",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
os.Setenv("MOCK_NIX_BUILD_OUTPUT", tt.mockBuildOutput)
os.Setenv("MOCK_NIX_BUILD_ERROR", tt.mockBuildError)
os.Setenv("MOCK_NIX_BUILD_EXIT_CODE", tt.mockBuildExitCode)
if tt.mockJSONContent != "" && tt.mockBuildOutput == mockDrvOutputPath {
if err := os.WriteFile(mockDrvOutputPath, []byte(tt.mockJSONContent), 0644); err != nil {
t.Fatalf("Failed to write mock JSON content: %v", err)
}
defer os.Remove(mockDrvOutputPath)
}
got, err := service.BuildAndParseJSON(tt.derivation)
if (err != nil) != tt.wantErr {
t.Fatalf("BuildAndParseJSON() error = %v, wantErr %v", err, tt.wantErr)
}
if tt.wantErr {
if tt.wantErrType != nil && !errors.As(err, &tt.wantErrType) {
t.Errorf("BuildAndParseJSON() error type = %T, want %T", err, tt.wantErrType)
}
if tt.wantErrMsgContains != "" && !strings.Contains(err.Error(), tt.wantErrMsgContains) {
t.Errorf("BuildAndParseJSON() error = %q, want error containing %q", err.Error(), tt.wantErrMsgContains)
}
}
if !tt.wantErr && !jsonDeepEqual(got, tt.want) {
t.Errorf("BuildAndParseJSON() got = %v, want %v", got, tt.want)
}
})
}
}
func jsonDeepEqual(a, b any) bool {
if a == nil && b == nil {
return true
}
if a == nil || b == nil {
return false
}
jsonA, _ := json.Marshal(a)
jsonB, _ := json.Marshal(b)
return string(jsonA) == string(jsonB)
}
func TestDefaultService_BuildAndRunScript(t *testing.T) {
service := NewDefaultService()
mockExecCommandForService(service)
tempDir := t.TempDir()
mockScriptPath := filepath.Join(tempDir, "mock_script.sh")
if err := os.WriteFile(mockScriptPath, []byte("#!/bin/bash\necho hello"), 0755); err != nil {
t.Fatalf("Failed to create dummy mock script: %v", err)
}
tests := []struct {
name string
derivation string
impureEnv bool
mockBuildDrvOutput string
mockBuildDrvError string
mockBuildDrvExitCode string
mockScriptStdout string
mockScriptStderr string
mockScriptExitCode string
wantExitCode int
wantStdout string
wantStderr string
wantErr bool
wantErrType any
wantErrMsgContains string
}{
{
"Success", "script.drv#sh", false, mockScriptPath, "", "0",
"Hello", "ErrOut", "0",
0, "Hello", "ErrOut", false, nil, "",
},
{
"Success impure", "script.drv#sh", true, mockScriptPath, "", "0",
"Hello", "ErrOut", "0",
0, "Hello", "ErrOut", false, nil, "",
},
{
"Script fails (non-zero exit)", "fail.drv#sh", false, mockScriptPath, "", "0",
"Out", "Err", "custom", // helper uses 5 for script failure
5, "Out", "Err", false, nil, "", // error is nil, non-zero exit code
},
{
"BuildDerivation fails", "buildfail.drv#sh", false, "", "nix error", "1",
"", "", "",
-1, "", "", true, (*apperrors.NixBuildError)(nil), "nix error",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
os.Setenv("MOCK_NIX_BUILD_OUTPUT", tt.mockBuildDrvOutput)
os.Setenv("MOCK_NIX_BUILD_ERROR", tt.mockBuildDrvError)
os.Setenv("MOCK_NIX_BUILD_EXIT_CODE", tt.mockBuildDrvExitCode)
os.Setenv("MOCK_SCRIPT_STDOUT", tt.mockScriptStdout)
os.Setenv("MOCK_SCRIPT_STDERR", tt.mockScriptStderr)
os.Setenv("MOCK_SCRIPT_EXIT_CODE", tt.mockScriptExitCode)
exitCode, stdout, stderr, err := service.BuildAndRunScript(tt.derivation, tt.impureEnv)
if (err != nil) != tt.wantErr {
t.Fatalf("BuildAndRunScript() error = %v, wantErr %v", err, tt.wantErr)
}
if tt.wantErr {
if tt.wantErrType != nil && !errors.As(err, &tt.wantErrType) {
t.Errorf("BuildAndRunScript() error type = %T, want %T", err, tt.wantErrType)
}
if tt.wantErrMsgContains != "" && !strings.Contains(err.Error(), tt.wantErrMsgContains) {
t.Errorf("BuildAndRunScript() error = %q, want error containing %q", err.Error(), tt.wantErrMsgContains)
}
} else {
if exitCode != tt.wantExitCode {
t.Errorf("BuildAndRunScript() exitCode = %v, want %v", exitCode, tt.wantExitCode)
}
if stdout != tt.wantStdout {
t.Errorf("BuildAndRunScript() stdout = %q, want %q", stdout, tt.wantStdout)
}
if stderr != tt.wantStderr {
t.Errorf("BuildAndRunScript() stderr = %q, want %q", stderr, tt.wantStderr)
}
}
})
}
}