mirror of
https://gitlab.com/TECHNOFAB/nixtest.git
synced 2025-12-12 02:00:18 +01:00
refactor: split into packages and add tests
This commit is contained in:
parent
fd58344ca7
commit
11117e0c0e
28 changed files with 2736 additions and 636 deletions
110
internal/nix/service.go
Normal file
110
internal/nix/service.go
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
package nix
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
apperrors "gitlab.com/technofab/nixtest/internal/errors"
|
||||
)
|
||||
|
||||
// Service defines operations related to Nix
|
||||
type Service interface {
|
||||
BuildDerivation(derivation string) (string, error)
|
||||
BuildAndParseJSON(derivation string) (any, error)
|
||||
BuildAndRunScript(derivation string, pureEnv bool) (exitCode int, stdout string, stderr string, err error)
|
||||
}
|
||||
|
||||
type DefaultService struct {
|
||||
commandExecutor func(command string, args ...string) *exec.Cmd
|
||||
}
|
||||
|
||||
func NewDefaultService() *DefaultService {
|
||||
return &DefaultService{commandExecutor: exec.Command}
|
||||
}
|
||||
|
||||
// BuildDerivation builds a Nix derivation and returns the output path
|
||||
func (s *DefaultService) BuildDerivation(derivation string) (string, error) {
|
||||
cmd := s.commandExecutor(
|
||||
"nix",
|
||||
"build",
|
||||
derivation+"^*",
|
||||
"--print-out-paths",
|
||||
"--no-link",
|
||||
)
|
||||
var stdout, stderr bytes.Buffer
|
||||
cmd.Stdout = &stdout
|
||||
cmd.Stderr = &stderr
|
||||
|
||||
err := cmd.Run()
|
||||
if err != nil {
|
||||
return "", &apperrors.NixBuildError{Derivation: derivation, Stderr: stderr.String(), Err: err}
|
||||
}
|
||||
|
||||
path := strings.TrimSpace(stdout.String())
|
||||
if path == "" {
|
||||
return "", &apperrors.NixNoOutputPathError{Derivation: derivation, Stderr: stderr.String()}
|
||||
}
|
||||
return path, nil
|
||||
}
|
||||
|
||||
// BuildAndParseJSON builds a derivation and parses its output file as JSON
|
||||
func (s *DefaultService) BuildAndParseJSON(derivation string) (any, error) {
|
||||
path, err := s.BuildDerivation(derivation)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, &apperrors.FileReadError{Path: path, Err: err}
|
||||
}
|
||||
|
||||
var result any
|
||||
err = json.Unmarshal(data, &result)
|
||||
if err != nil {
|
||||
return nil, &apperrors.JSONUnmarshalError{Source: path, Err: err}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// 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) {
|
||||
exitCode = -1
|
||||
path, err := s.BuildDerivation(derivation)
|
||||
if err != nil {
|
||||
return exitCode, "", "", err
|
||||
}
|
||||
|
||||
var cmdArgs []string
|
||||
if pureEnv {
|
||||
cmdArgs = append([]string{"env", "-i"}, "bash", path)
|
||||
} else {
|
||||
cmdArgs = []string{"bash", path}
|
||||
}
|
||||
|
||||
cmd := s.commandExecutor(cmdArgs[0], cmdArgs[1:]...)
|
||||
var outBuf, errBuf bytes.Buffer
|
||||
cmd.Stdout = &outBuf
|
||||
cmd.Stderr = &errBuf
|
||||
|
||||
if err = cmd.Start(); err != nil {
|
||||
return exitCode, "", "", &apperrors.ScriptExecutionError{Path: path, Err: err}
|
||||
}
|
||||
|
||||
runErr := cmd.Wait()
|
||||
stdout = outBuf.String()
|
||||
stderr = errBuf.String()
|
||||
|
||||
if runErr != nil {
|
||||
if exitErr, ok := runErr.(*exec.ExitError); ok {
|
||||
return exitErr.ExitCode(), stdout, stderr, nil
|
||||
}
|
||||
return exitCode, stdout, stderr, &apperrors.ScriptExecutionError{Path: path, Err: runErr}
|
||||
}
|
||||
|
||||
return 0, stdout, stderr, nil
|
||||
}
|
||||
305
internal/nix/service_test.go
Normal file
305
internal/nix/service_test.go
Normal file
|
|
@ -0,0 +1,305 @@
|
|||
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
|
||||
pureEnv 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 pure", "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.pureEnv)
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue