Compare commits

..

No commits in common. "main" and "1.1.0" have entirely different histories.
main ... 1.1.0

22 changed files with 152 additions and 236 deletions

View file

@ -2,7 +2,7 @@
[![built with nix](https://img.shields.io/static/v1?logo=nixos&logoColor=white&label=&message=Built%20with%20Nix&color=41439a)](https://builtwithnix.org) [![built with nix](https://img.shields.io/static/v1?logo=nixos&logoColor=white&label=&message=Built%20with%20Nix&color=41439a)](https://builtwithnix.org)
[![pipeline status](https://gitlab.com/TECHNOFAB/nixtest/badges/main/pipeline.svg)](https://gitlab.com/TECHNOFAB/nixtest/-/commits/main) [![pipeline status](https://gitlab.com/TECHNOFAB/nixtest/badges/main/pipeline.svg)](https://gitlab.com/TECHNOFAB/nixtest/-/commits/main)
![License: MIT](https://img.shields.io/gitlab/license/technofab/nixtest) ![License: MIT](https://img.shields.io/gitlab/license/technofab/nix-gitlab-ci)
[![Latest Release](https://gitlab.com/TECHNOFAB/nixtest/-/badges/release.svg)](https://gitlab.com/TECHNOFAB/nixtest/-/releases) [![Latest Release](https://gitlab.com/TECHNOFAB/nixtest/-/badges/release.svg)](https://gitlab.com/TECHNOFAB/nixtest/-/releases)
[![Support me](https://img.shields.io/badge/Support-me-green)](https://tec.tf/#support) [![Support me](https://img.shields.io/badge/Support-me-green)](https://tec.tf/#support)
[![Docs](https://img.shields.io/badge/Read-Docs-green)](https://nixtest.projects.tf) [![Docs](https://img.shields.io/badge/Read-Docs-green)](https://nixtest.projects.tf)

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,
ImpureEnv: appCfg.ImpureEnv, PureEnv: appCfg.PureEnv,
} }
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
--impure Don\'t unset all env vars before running script tests --pure 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)

BIN
docs/images/favicon.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

BIN
docs/images/logo.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

View file

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" fill="none"><rect width="100" height="100" fill="#222" rx="12"/><rect width="100" height="100" fill="url(#a)" rx="12"/><path fill="#3FB950" d="M18 54.7a3 3 0 0 1 4.3 0l17 17c.5.4 1 .4 1.5 0L82.5 30a3 3 0 0 1 4.2 0l2 1.9a3 3 0 0 1 0 4.2L46.2 78.7l-4 4a3 3 0 0 1-4.2 0L16 60.9a3 3 0 0 1 0-4.3l2-1.9Z"/><path fill="url(#b)" d="m89 66.6-5.3 9.6H71l6.3 11-2.7 4.8h-5.5l-9-15.8H53l9.6-9.6H89Zm-76-4.8 1 1.2 6 5.9-3.5 6-5.5-9.4 2-3.7Zm3-5 .1-.2-.1.2Zm11.4-1.1-3-3.1a6 6 0 0 0-8.5 0l-1.8 1.8H4.7L2 49.6l2.7-4.8h18l6.5-11.2H40l-12.6 22ZM62.8 18l6.4-11h5.4l2.7 4.8-9 15.7 4.4 7.9-6.9 6.9L45.8 7 56.5 7l6.2 11.1Z"/><path fill="url(#c)" d="m54.9 91.9-10.7.1-3.1-5.5a6 6 0 0 0 3.2-1.7l4-4 .2-.2L55 92Zm-19.1-7.2L31.7 92h-5.3l-2.8-4.8L29 78l6.9 6.8ZM47 78l-.8.8-4 4-.1.1.1-.1 4-4 .8-.8ZM40.2 72Zm.5-.2Zm.3-4.5-1 .9-11.7-11.7 3.3-6L41 67.3Zm54.3-22.6 2.7 4.7-2.7 4.8H77.5l-6.3 11.2h-7.6l20.8-20.7h11Zm-54.8-22 12.7.1 5.4 9.5H12l5.3-9.5h12.5l-6.2-11L26.2 7h5.5l8.8 15.8Zm44.3 3.4a6 6 0 0 0-2.4.4l1.3-2.4 1.1 2Z"/><defs><linearGradient id="b" x1="76.7" x2="59.1" y1="47.2" y2="17.1" gradientUnits="userSpaceOnUse"><stop offset=".2" stop-color="#7EB1DD"/></linearGradient><linearGradient id="c" x1="63.4" x2="81.3" y1="70.3" y2="40.6" gradientUnits="userSpaceOnUse"><stop offset="1" stop-color="#5277C3"/></linearGradient><radialGradient id="a" cx="0" cy="0" r="1" gradientTransform="matrix(0 50 -50 0 50 50)" gradientUnits="userSpaceOnUse"><stop offset=".5" stop-color="#091A3D"/><stop offset="1" stop-color="#000819"/></radialGradient></defs></svg>

Before

Width:  |  Height:  |  Size: 1.6 KiB

View file

@ -1,3 +0,0 @@
# Options
{% include 'options.md' %}

View file

@ -1,15 +0,0 @@
.md-header__button.md-logo {
margin: 0;
padding-top: .2rem;
padding-bottom: .2rem;
}
[dir="ltr"] .md-header__title {
margin-left: 0;
}
.md-header__button.md-logo img,
.md-header__button.md-logo svg {
height: 2rem;
}

View file

@ -96,10 +96,10 @@ Examples:
name = "script-test"; name = "script-test";
type = "script"; type = "script";
script = script =
# there are two modes, "default"/"pure" and "impure" # there are two modes, "default"/"impure" and "pure"
# 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 (using --impure). # and are available to the test
# to make it more reproducible and cleaner, the default is pure # to make it more reproducible and cleaner, use --pure to switch to 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:
# #

22
flake.lock generated
View file

@ -169,6 +169,21 @@
"type": "github" "type": "github"
} }
}, },
"mkdocs-material-umami": {
"locked": {
"lastModified": 1745840856,
"narHash": "sha256-1Ad1JTMQMP6YsoIKAA+SBCE15qWrYkGue9/lXOLnu9I=",
"owner": "technofab",
"repo": "mkdocs-material-umami",
"rev": "3ac9b194450f6b779c37b8d16fec640198e5cd0a",
"type": "gitlab"
},
"original": {
"owner": "technofab",
"repo": "mkdocs-material-umami",
"type": "gitlab"
}
},
"nix": { "nix": {
"inputs": { "inputs": {
"flake-compat": [ "flake-compat": [
@ -240,11 +255,11 @@
"nix-mkdocs": { "nix-mkdocs": {
"locked": { "locked": {
"dir": "lib", "dir": "lib",
"lastModified": 1757055638, "lastModified": 1745841841,
"narHash": "sha256-KHYSkEreFe4meXzSdEbknC/HwaQSNClQkc8vzHlAsMM=", "narHash": "sha256-297zPQbUlc7ZAYDoaD6mCmQxCC3Tr4YOKekRF1ArZ7g=",
"owner": "technofab", "owner": "technofab",
"repo": "nixmkdocs", "repo": "nixmkdocs",
"rev": "7840a5febdbeaf2da90babf6c94b3d0929d2bf74", "rev": "c7e3c3b13ded25818e9789938387bba6f2cde690",
"type": "gitlab" "type": "gitlab"
}, },
"original": { "original": {
@ -353,6 +368,7 @@
"inputs": { "inputs": {
"devenv": "devenv", "devenv": "devenv",
"flake-parts": "flake-parts_2", "flake-parts": "flake-parts_2",
"mkdocs-material-umami": "mkdocs-material-umami",
"nix-devtools": "nix-devtools", "nix-devtools": "nix-devtools",
"nix-gitlab-ci": "nix-gitlab-ci", "nix-gitlab-ci": "nix-gitlab-ci",
"nix-mkdocs": "nix-mkdocs", "nix-mkdocs": "nix-mkdocs",

109
flake.nix
View file

@ -17,7 +17,6 @@
perSystem = { perSystem = {
lib, lib,
pkgs, pkgs,
self',
config, config,
... ...
}: { }: {
@ -37,12 +36,9 @@
}; };
devenv.shells.default = { devenv.shells.default = {
containers = pkgs.lib.mkForce {}; containers = pkgs.lib.mkForce {};
packages = with pkgs; [gore go-junit-report]; packages = with pkgs; [gopls gore go-junit-report];
languages.go = { languages.go.enable = true;
enable = true;
enableHardeningWorkaround = true;
};
pre-commit.hooks = { pre-commit.hooks = {
treefmt = { treefmt = {
@ -66,48 +62,81 @@
}; };
}; };
docs."default".config = { doc = {
path = ./docs; path = ./docs;
material = { deps = pp: [
enable = true; pp.mkdocs-material
colors = { (pp.callPackage inputs.mkdocs-material-umami {})
primary = "green"; ];
accent = "light green";
};
umami = {
enable = true;
src = "https://analytics.tf/umami";
siteId = "716d1869-9342-4b62-a770-e15d2d5c807d";
domains = ["nixtest.projects.tf"];
};
};
macros = {
enable = true;
includeDir = toString self'.packages.optionsDocs;
};
config = { config = {
site_name = "Nixtest"; site_name = "Nixtest";
site_url = "https://nixtest.projects.tf";
repo_name = "TECHNOFAB/nixtest"; repo_name = "TECHNOFAB/nixtest";
repo_url = "https://gitlab.com/TECHNOFAB/nixtest"; repo_url = "https://gitlab.com/TECHNOFAB/nixtest";
extra_css = ["style.css"]; edit_uri = "edit/main/docs/";
theme = { theme = {
name = "material";
features = ["content.code.copy" "content.action.edit"];
icon.repo = "simple/gitlab"; icon.repo = "simple/gitlab";
logo = "images/logo.svg"; logo = "images/logo.png";
favicon = "images/logo.svg"; favicon = "images/favicon.png";
palette = [
{
scheme = "default";
media = "(prefers-color-scheme: light)";
primary = "green";
accent = "light green";
toggle = {
icon = "material/brightness-7";
name = "Switch to dark mode";
}; };
}
{
scheme = "slate";
media = "(prefers-color-scheme: dark)";
primary = "green";
accent = "light green";
toggle = {
icon = "material/brightness-4";
name = "Switch to light mode";
};
}
];
};
plugins = ["search" "material-umami"];
nav = [ nav = [
{"Introduction" = "index.md";} {"Introduction" = "index.md";}
{"Usage" = "usage.md";} {"Usage" = "usage.md";}
{"Reference" = "reference.md";} {"Reference" = "reference.md";}
{"CLI" = "cli.md";} {"CLI" = "cli.md";}
{"Example Configs" = "examples.md";} {"Example Configs" = "examples.md";}
{"Options" = "options.md";}
]; ];
markdown_extensions = [ markdown_extensions = [
"pymdownx.superfences" "pymdownx.superfences"
"admonition" "admonition"
]; ];
extra.analytics = {
provider = "umami";
site_id = "716d1869-9342-4b62-a770-e15d2d5c807d";
src = "https://analytics.tf/umami";
domains = "nixtest.projects.tf";
feedback = {
title = "Was this page helpful?";
ratings = [
{
icon = "material/thumb-up-outline";
name = "This page is helpful";
data = "good";
note = "Thanks for your feedback!";
}
{
icon = "material/thumb-down-outline";
name = "This page could be improved";
data = "bad";
note = "Thanks for your feedback! Please leave feedback by creating an issue :)";
}
];
};
};
}; };
}; };
@ -180,8 +209,7 @@
packages = let packages = let
ntlib = import ./lib {inherit pkgs lib;}; ntlib = import ./lib {inherit pkgs lib;};
doclib = inputs.nix-mkdocs.lib {inherit lib pkgs;}; in {
in rec {
default = pkgs.callPackage ./package.nix {}; default = pkgs.callPackage ./package.nix {};
tests = ntlib.mkNixtest { tests = ntlib.mkNixtest {
modules = ntlib.autodiscover {dir = ./tests;}; modules = ntlib.autodiscover {dir = ./tests;};
@ -189,24 +217,6 @@
inherit pkgs ntlib; inherit pkgs ntlib;
}; };
}; };
optionsDoc = doclib.mkOptionDocs {
module = {
_module.args.pkgs = pkgs;
imports = [
ntlib.module
];
};
roots = [
{
url = "https://gitlab.com/TECHNOFAB/nixtest/-/blob/main/lib";
path = toString ./lib;
}
];
};
optionsDocs = pkgs.runCommand "options-docs" {} ''
mkdir -p $out
ln -s ${optionsDoc} $out/options.md
'';
}; };
}; };
}; };
@ -222,6 +232,7 @@
nix-gitlab-ci.url = "gitlab:technofab/nix-gitlab-ci/2.0.1?dir=lib"; nix-gitlab-ci.url = "gitlab:technofab/nix-gitlab-ci/2.0.1?dir=lib";
nix-devtools.url = "gitlab:technofab/nix-devtools?dir=lib"; nix-devtools.url = "gitlab:technofab/nix-devtools?dir=lib";
nix-mkdocs.url = "gitlab:technofab/nixmkdocs?dir=lib"; nix-mkdocs.url = "gitlab:technofab/nixmkdocs?dir=lib";
mkdocs-material-umami.url = "gitlab:technofab/mkdocs-material-umami";
}; };
nixConfig = { nixConfig = {

2
go.mod
View file

@ -1,6 +1,6 @@
module gitlab.com/technofab/nixtest module gitlab.com/technofab/nixtest
go 1.23.0 go 1.24.2
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
ImpureEnv bool PureEnv 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.ImpureEnv, "impure", false, "Don't unset all env vars before running script tests") flag.BoolVar(&cfg.PureEnv, "pure", false, "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",
"--impure", "--pure",
"--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.ImpureEnv { if !cfg.PureEnv {
t.Errorf("ImpureEnv: got %v, want true", cfg.ImpureEnv) t.Errorf("PureEnv: got %v, want true", cfg.PureEnv)
} }
} }

View file

@ -3,7 +3,6 @@ package nix
import ( import (
"bytes" "bytes"
"encoding/json" "encoding/json"
"fmt"
"os" "os"
"os/exec" "os/exec"
"strings" "strings"
@ -15,7 +14,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, impureEnv bool) (exitCode int, stdout string, stderr string, err error) BuildAndRunScript(derivation string, pureEnv bool) (exitCode int, stdout string, stderr string, err error)
} }
type DefaultService struct { type DefaultService struct {
@ -73,29 +72,21 @@ 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, impureEnv bool) (exitCode int, stdout string, stderr string, err error) { func (s *DefaultService) BuildAndRunScript(derivation string, pureEnv 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 impureEnv { if pureEnv {
cmdArgs = []string{"bash", path}
} else {
cmdArgs = append([]string{"env", "-i"}, "bash", path) cmdArgs = append([]string{"env", "-i"}, "bash", path)
} else {
cmdArgs = []string{"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
impureEnv bool pureEnv 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 impure", "script.drv#sh", true, mockScriptPath, "", "0", "Success pure", "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.impureEnv) exitCode, stdout, stderr, err := service.BuildAndRunScript(tt.derivation, tt.pureEnv)
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
ImpureEnv bool PureEnv 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.ImpureEnv) exitCode, stdout, stderrStr, err := r.nixService.BuildAndRunScript(spec.Script, r.config.PureEnv)
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, impureEnv bool) (exitCode int, stdout string, stderr string, err error) BuildAndRunScriptFunc func(derivation string, pureEnv 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, impureEnv bool) (int, string, string, error) { mNix.BuildAndRunScriptFunc = func(derivation string, pureEnv 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, impureEnv bool) (int, string, string, error) { mNix.BuildAndRunScriptFunc = func(derivation string, pureEnv 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, impureEnv bool) (int, string, string, error) { return 0, "", "", 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.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

@ -39,11 +39,7 @@ in rec {
}: let }: let
files = builtins.readDir dir; files = builtins.readDir dir;
matchingFiles = builtins.filter (name: builtins.match pattern name != null) (builtins.attrNames files); matchingFiles = builtins.filter (name: builtins.match pattern name != null) (builtins.attrNames files);
imports = map (file: imports = map (file: /${dir}/${file}) matchingFiles;
if builtins.isString dir
then (builtins.unsafeDiscardStringContext dir) + "/${file}"
else /${dir}/${file})
matchingFiles;
in { in {
inherit imports; inherit imports;
# automatically set the base so test filepaths are easier to read # automatically set the base so test filepaths are easier to read

View file

@ -3,18 +3,7 @@
lib, lib,
... ...
}: let }: let
inherit inherit (lib) mkOptionType mkOption types;
(lib)
mkOptionType
mkOption
types
filterAttrs
isType
removePrefix
assertMsg
generators
literalExpression
;
nixtest-lib = import ./default.nix {inherit pkgs lib;}; nixtest-lib = import ./default.nix {inherit pkgs lib;};
@ -27,26 +16,14 @@
unset = { unset = {
_type = "unset"; _type = "unset";
}; };
isUnset = isType "unset"; isUnset = lib.isType "unset";
unsetOr = typ:
(types.either unsetType typ)
// {
inherit (typ) description getSubOptions;
};
mkUnsetOption = opts:
mkOption (opts
// {
type = unsetOr opts.type;
default = opts.default or unset;
defaultText = literalExpression "unset";
});
filterUnset = value: filterUnset = value:
if builtins.isAttrs value && !builtins.hasAttr "_type" value if builtins.isAttrs value && !builtins.hasAttr "_type" value
then let then let
filteredAttrs = builtins.mapAttrs (n: v: filterUnset v) value; filteredAttrs = builtins.mapAttrs (n: v: filterUnset v) value;
in in
filterAttrs (name: value: (!isUnset value)) filteredAttrs lib.filterAttrs (name: value: (!isUnset value)) filteredAttrs
else if builtins.isList value else if builtins.isList value
then builtins.filter (elem: !isUnset elem) (map filterUnset value) then builtins.filter (elem: !isUnset elem) (map filterUnset value)
else value; else value;
@ -58,31 +35,25 @@
... ...
}: { }: {
options = { options = {
pos = mkUnsetOption { pos = mkOption {
type = types.attrs; type = types.either types.attrs unsetType;
description = ''
Position of test, use `__curPos` for automatic insertion of current position.
'';
default = pos; default = pos;
apply = val: apply = val:
if isUnset val if isUnset val
then val then val
else let else let
fileRelative = removePrefix testsBase val.file; fileRelative = lib.removePrefix testsBase val.file;
in "${fileRelative}:${toString val.line}"; in "${fileRelative}:${toString val.line}";
}; };
type = mkOption { type = mkOption {
type = types.enum ["unit" "snapshot" "script"]; type = types.enum ["unit" "snapshot" "script"];
description = ''
Type of test, has to be one of "unit", "snapshot" or "script".
'';
default = "unit"; default = "unit";
apply = value: apply = value:
assert assertMsg (value != "script" || !isUnset config.script) assert lib.assertMsg (value != "script" || !isUnset config.script)
"test '${config.name}' as type 'script' requires 'script' to be set"; "test '${config.name}' as type 'script' requires 'script' to be set";
assert assertMsg (value != "unit" || !isUnset config.expected) assert lib.assertMsg (value != "unit" || !isUnset config.expected)
"test '${config.name}' as type 'unit' requires 'expected' to be set"; "test '${config.name}' as type 'unit' requires 'expected' to be set";
assert assertMsg ( assert lib.assertMsg (
let let
actualIsUnset = isUnset config.actual; actualIsUnset = isUnset config.actual;
actualDrvIsUnset = isUnset config.actualDrv; actualDrvIsUnset = isUnset config.actualDrv;
@ -95,66 +66,43 @@
}; };
name = mkOption { name = mkOption {
type = types.str; type = types.str;
description = ''
Name of this test.
'';
}; };
description = mkUnsetOption { description = mkOption {
type = types.str; type = types.either types.str unsetType;
description = '' default = unset;
Short description of the test.
'';
}; };
format = mkOption { format = mkOption {
type = types.enum ["json" "pretty"]; type = types.enum ["json" "pretty"];
description = ''
Which format to use for serializing arbitrary values.
Required since this config is serialized to JSON for passing it to Nixtest, so no Nix-values can be used directly.
- `json`: serializes the data to json using `builtins.toJSON`
- `pretty`: serializes the data to a "pretty" format using `lib.generators.toPretty`
'';
default = "json"; default = "json";
}; };
expected = mkUnsetOption { expected = mkOption {
type = types.anything; type = types.anything;
description = '' default = unset;
Expected value of the test. Remember, the values are serialized (see [here](#suitesnametestsformat)).
'';
apply = val: apply = val:
if isUnset val || config.format == "json" if isUnset val || config.format == "json"
then val then val
else generators.toPretty {} val; else lib.generators.toPretty {} val;
}; };
actual = mkUnsetOption { actual = mkOption {
type = types.anything; type = types.anything;
description = '' default = unset;
Actual value of the test. Remember, the values are serialized (see [here](#suitesnametestsformat)).
'';
apply = val: apply = val:
if isUnset val || config.format == "json" if isUnset val || config.format == "json"
then val then val
else generators.toPretty {} val; else lib.generators.toPretty {} val;
}; };
actualDrv = mkUnsetOption { actualDrv = mkOption {
type = types.package; type = types.either types.package unsetType;
description = '' default = unset;
Actual value of the test, but as a derivation.
Nixtest will build this derivation when running the test, then compare the contents of the
resulting file to the [`expected`](#suitesnametestsexpected) value.
'';
apply = val: apply = val:
# keep unset value # keep unset value
if isUnset val if isUnset val
then val then val
else builtins.unsafeDiscardStringContext (val.drvPath or ""); else builtins.unsafeDiscardStringContext (val.drvPath or "");
}; };
script = mkUnsetOption { script = mkOption {
type = types.str; type = types.either types.str unsetType;
description = '' default = unset;
Script to run for the test.
Nixtest will run this, failing the test if it exits with a non-zero exit code.
'';
apply = val: apply = val:
if isUnset val if isUnset val
then val then val
@ -174,19 +122,11 @@
options = { options = {
name = mkOption { name = mkOption {
type = types.str; type = types.str;
description = ''
Name of the suite, uses attrset name by default.
'';
default = name; default = name;
defaultText = literalExpression name;
}; };
pos = mkUnsetOption { pos = mkOption {
type = types.attrs; type = types.either types.attrs unsetType;
description = '' default = unset;
Position for tests, use `__curPos` for automatic insertion of current position.
This will set `pos` for every test of this suite, useful if the suite's tests are all in a single file.
'';
example = literalExpression "__curPos";
}; };
tests = mkOption { tests = mkOption {
type = types.listOf (types.submoduleWith { type = types.listOf (types.submoduleWith {
@ -196,30 +136,20 @@
inherit testsBase; inherit testsBase;
}; };
}); });
description = ''
Define tests of this suite here.
'';
default = []; default = [];
}; };
}; };
}; };
nixtestSubmodule = {config, ...}: { nixtestSubmodule = {config, ...}: {
_file = ./module.nix;
options = { options = {
base = mkOption { base = mkOption {
description = "Base directory of the tests, will be removed from the test file path";
type = types.str; type = types.str;
description = ''
Base directory of the tests, will be removed from the test file path.
This makes it possible to show the relative path from the git repo, instead of ugly Nix store paths.
'';
default = ""; default = "";
}; };
skip = mkOption { skip = mkOption {
type = types.str; type = types.str;
description = ''
Tests to skip, is passed to Nixtest's `--skip` param.
'';
default = ""; default = "";
}; };
suites = mkOption { suites = mkOption {
@ -229,22 +159,12 @@
testsBase = config.base; testsBase = config.base;
}; };
}); });
description = ''
Define your test suites here, every test belongs to a suite.
'';
default = {}; default = {};
apply = suites: apply = suites:
map ( map (
n: filterUnset (builtins.removeAttrs suites.${n} ["pos"]) n: filterUnset (builtins.removeAttrs suites.${n} ["pos"])
) )
(builtins.attrNames suites); (builtins.attrNames suites);
example = {
"Suite A".tests = [
{
name = "Some Test";
}
];
};
}; };
finalConfigJson = mkOption { finalConfigJson = mkOption {

View file

@ -35,6 +35,16 @@ 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

@ -13,10 +13,6 @@
actual = ntlib.helpers.toPrettyFile (ntlib.autodiscover { actual = ntlib.helpers.toPrettyFile (ntlib.autodiscover {
dir = ./fixtures; dir = ./fixtures;
}); });
# tests if strings with store path context work
actualDirString = ntlib.helpers.toPrettyFile (ntlib.autodiscover {
dir = "${./fixtures}";
});
in in
# sh # sh
'' ''
@ -24,9 +20,6 @@
${ntlib.helpers.scriptHelpers} ${ntlib.helpers.scriptHelpers}
assert_file_contains ${actual} "sample_test.nix" "should find sample_test.nix" assert_file_contains ${actual} "sample_test.nix" "should find sample_test.nix"
assert_file_contains ${actual} "base = \"/nix/store/.*-source/tests/fixtures/\"" "should set base to fixtures dir" assert_file_contains ${actual} "base = \"/nix/store/.*-source/tests/fixtures/\"" "should set base to fixtures dir"
assert_file_contains ${actualDirString} "sample_test.nix" "should find sample_test.nix"
assert_file_contains ${actualDirString} "base = \"/nix/store/.*-fixtures/\"" "should set base to fixtures dir"
''; '';
} }
{ {
@ -36,7 +29,7 @@
binary = binary =
(ntlib.mkBinary { (ntlib.mkBinary {
nixtests = "stub"; nixtests = "stub";
extraParams = "--impure"; extraParams = "--pure";
}) })
+ "/bin/nixtests:run"; + "/bin/nixtests:run";
in in
@ -45,7 +38,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} "--impure" "should contain --impure arg" assert_file_contains ${binary} "--pure" "should contain --pure 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,23 +63,21 @@
in in
# sh # sh
'' ''
${ntlib.helpers.path [pkgs.gnugrep pkgs.mktemp pkgs.coreutils]} ${ntlib.helpers.path [pkgs.gnugrep pkgs.mktemp]}
${ntlib.helpers.scriptHelpers} ${ntlib.helpers.scriptHelpers}
export SSL_CERT_FILE=${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt
export NIX_SSL_CERT_FILE=${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt
cp -r ${./../snapshots} snapshots
TMPDIR=$(tmpdir)
# start without nix & env binaries to expect errors # start without nix & env binaries to expect errors
run "${binary} --junit=junit.xml" run "${binary} --pure --junit=$TMPDIR/junit.xml"
assert "$exit_code -eq 2" "should exit 2" assert "$exit_code -eq 2" "should exit 2"
assert "-f junit.xml" "should create junit.xml" assert "-f $TMPDIR/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} --junit=junit2.xml" run "${binary} --pure --junit=$TMPDIR/junit2.xml"
assert "$exit_code -eq 2" "should exit 2" assert "$exit_code -eq 2" "should exit 2"
assert "-f junit2.xml" "should create junit2.xml" assert "-f $TMPDIR/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"