chore: initial prototype

This commit is contained in:
technofab 2025-05-03 22:05:29 +02:00
commit c1c19c324d
16 changed files with 1099 additions and 0 deletions

1
.envrc Normal file
View file

@ -0,0 +1 @@
use flake . --impure --accept-flake-config

5
.gitignore vendored Normal file
View file

@ -0,0 +1,5 @@
.direnv/
.devenv/
result
.pre-commit-config.yaml
*.xml

78
cmd/nixtest/display.go Normal file
View file

@ -0,0 +1,78 @@
package main
import (
"fmt"
"os"
"strings"
"github.com/jedib0t/go-pretty/v6/table"
"github.com/jedib0t/go-pretty/v6/text"
"github.com/rs/zerolog/log"
)
func printErrors(results Results) {
for _, suiteResults := range results {
for _, result := range suiteResults {
if result.Success {
continue
}
fmt.Println(text.FgRed.Sprintf("⚠ Test \"%s\" failed:", result.Name))
for line := range strings.Lines(result.Error) {
fmt.Printf("%s %s", text.FgRed.Sprint("|"), line)
}
if result.Error == "" {
fmt.Printf("- no output -")
}
fmt.Printf("\n\n")
}
}
}
func printSummary(results Results, successCount int, totalCount int) {
t := table.NewWriter()
t.SetStyle(table.StyleLight)
t.SetOutputMirror(os.Stdout)
t.AppendHeader(table.Row{"Test", "Duration", "Pass", "File"})
log.Info().Msg("Summary:")
for suite, suiteResults := range results {
suiteTotal := len(suiteResults)
suiteSuccess := 0
for _, res := range suiteResults {
if res.Success {
suiteSuccess++
}
}
t.AppendRow(table.Row{
text.Bold.Sprint(suite),
"",
fmt.Sprintf("%d/%d", suiteSuccess, suiteTotal),
"",
})
for _, res := range suiteResults {
symbol := "❌"
if res.Success {
symbol = "✅"
}
t.AppendRow([]any{
res.Name,
fmt.Sprintf("%s", res.Duration),
symbol,
res.Pos,
})
}
t.AppendSeparator()
}
t.AppendFooter(table.Row{
text.Bold.Sprint("TOTAL"),
"",
fmt.Sprintf("%d/%d", successCount, totalCount),
"",
})
t.Render()
}

115
cmd/nixtest/junit.go Normal file
View file

@ -0,0 +1,115 @@
package main
import (
"encoding/xml"
"fmt"
"os"
"strings"
"time"
)
type JUnitReport struct {
XMLName xml.Name `xml:"testsuite"`
Name string `xml:"name,attr"`
Tests int `xml:"tests,attr"`
Failures int `xml:"failures,attr"`
Time string `xml:"time,attr"` // in seconds
Suites []JUnitTestSuite `xml:"testsuite"`
}
type JUnitTestSuite struct {
XMLName xml.Name `xml:"testsuite"`
Name string `xml:"name,attr"`
Tests int `xml:"tests,attr"`
Failures int `xml:"failures,attr"`
Time string `xml:"time,attr"` // in seconds
TestCases []JUnitCase `xml:"testcase"`
}
type JUnitCase struct {
Name string `xml:"name,attr"`
Classname string `xml:"classname,attr"`
Time string `xml:"time,attr"` // in seconds
File string `xml:"file,attr,omitempty"`
Line string `xml:"line,attr,omitempty"`
Failure *string `xml:"failure,omitempty"`
Error *string `xml:"error,omitempty"`
}
func GenerateJUnitReport(name string, results Results) (string, error) {
report := JUnitReport{
Name: name,
Tests: 0,
Failures: 0,
Suites: []JUnitTestSuite{},
}
totalDuration := time.Duration(0)
for suiteName, suiteResults := range results {
suite := JUnitTestSuite{
Name: suiteName,
Tests: len(suiteResults),
Failures: 0,
TestCases: []JUnitCase{},
}
suiteDuration := time.Duration(0)
for _, result := range suiteResults {
durationSeconds := fmt.Sprintf("%.3f", result.Duration.Seconds())
totalDuration += result.Duration
suiteDuration += result.Duration
testCase := JUnitCase{
Name: result.Name,
Classname: suiteName, // Use suite name as classname
Time: durationSeconds,
}
if result.Pos != "" {
pos := strings.Split(result.Pos, ":")
testCase.File = pos[0]
testCase.Line = pos[1]
}
if !result.Success {
suite.Failures++
report.Failures++
testCase.Failure = &result.Error
}
suite.TestCases = append(suite.TestCases, testCase)
}
suite.Time = fmt.Sprintf("%.3f", suiteDuration.Seconds())
report.Suites = append(report.Suites, suite)
report.Tests += len(suiteResults)
}
report.Time = fmt.Sprintf("%.3f", totalDuration.Seconds())
output, err := xml.MarshalIndent(report, "", " ")
if err != nil {
return "", fmt.Errorf("failed to marshal XML: %w", err)
}
return xml.Header + string(output), nil
}
func GenerateJunitFile(path string, results Results) error {
res, err := GenerateJUnitReport("nixtest", results)
if err != nil {
return fmt.Errorf("failed to generate junit report: %w", err)
}
file, err := os.Create(*junitPath)
if err != nil {
return fmt.Errorf("failed to create file: %w", err)
}
defer file.Close()
_, err = file.WriteString(res)
if err != nil {
return fmt.Errorf("failed to write to file: %w", err)
}
return nil
}

270
cmd/nixtest/main.go Normal file
View file

@ -0,0 +1,270 @@
package main
import (
"encoding/json"
"errors"
"fmt"
"os"
"os/exec"
"path"
"reflect"
"strings"
"sync"
"time"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"github.com/sergi/go-diff/diffmatchpatch"
flag "github.com/spf13/pflag"
)
type SuiteSpec struct {
Name string `json:"name"`
Tests []TestSpec `json:"tests"`
}
type TestSpec struct {
Type string `json:"type"`
Name string `json:"name"`
Description string `json:"description"`
Expected any `json:"expected,omitempty"`
Actual any `json:"actual,omitempty"`
ActualDrv string `json:"actualDrv,omitempty"`
Pos string `json:"pos,omitempty"`
Suite string
}
type TestResult struct {
Name string
Success bool
Error string
Duration time.Duration
Pos string
Suite string
}
type Results map[string][]TestResult
func buildAndParse(variable string) (any, error) {
cmd := exec.Command(
"nix",
"build",
variable+"^*",
"--print-out-paths",
"--no-link",
)
output, err := cmd.Output()
if err != nil {
return nil, err
}
path := strings.TrimSpace(string(output))
data, err := os.ReadFile(path)
if err != nil {
return nil, err
}
var result any
err = json.Unmarshal(data, &result)
if err != nil {
return nil, err
}
return result, nil
}
func runTest(spec TestSpec) TestResult {
startTime := time.Now()
result := TestResult{
Name: spec.Name,
Pos: spec.Pos,
Suite: spec.Suite,
Success: false,
Error: "",
}
var actual any
var expected any
if spec.Type == "snapshot" {
actual = spec.Actual
filePath := path.Join(
*snapshotDir,
fmt.Sprintf("%s.snap.json", strings.ToLower(spec.Name)),
)
if *updateSnapshots {
createSnapshot(filePath, actual)
}
if _, err := os.Stat(filePath); errors.Is(err, os.ErrNotExist) {
result.Error = "No Snapshot exists yet"
goto end
}
var err error
expected, err = ParseFile[any](filePath)
if err != nil {
result.Error = fmt.Sprintf("[system] failed to parse snapshot: %v", err.Error())
goto end
}
} else if spec.Type == "unit" {
if spec.ActualDrv != "" {
var err error
actual, err = buildAndParse(spec.ActualDrv)
if err != nil {
result.Error = fmt.Sprintf("[system] failed to parse drv output: %v", err.Error())
goto end
}
} else {
actual = spec.Actual
}
expected = spec.Expected
} else {
log.Panic().Str("type", spec.Type).Msg("Invalid test type")
}
if reflect.DeepEqual(actual, expected) {
result.Success = true
} else {
dmp := diffmatchpatch.New()
text1, err := json.MarshalIndent(actual, "", " ")
if err != nil {
result.Error = fmt.Sprintf("[system] failed to json marshal 'actual': %v", err.Error())
goto end
}
text2, err := json.MarshalIndent(expected, "", " ")
if err != nil {
result.Error = fmt.Sprintf("[system] failed to json marshal 'expected': %v", err.Error())
goto end
}
diffs := dmp.DiffMain(string(text1), string(text2), false)
result.Error = fmt.Sprintf("Mismatch:\n%s", dmp.DiffPrettyText(diffs))
}
end:
result.Duration = time.Since(startTime)
return result
}
func createSnapshot(filePath string, actual any) error {
jsonData, err := json.Marshal(actual)
if err != nil {
return err
}
err = os.MkdirAll(path.Dir(filePath), 0777)
if err != nil {
return err
}
err = os.WriteFile(filePath, jsonData, 0644)
if err != nil {
return err
}
return nil
}
// worker to process TestSpec items
func worker(jobs <-chan TestSpec, results chan<- TestResult, wg *sync.WaitGroup) {
defer wg.Done()
for spec := range jobs {
results <- runTest(spec)
}
}
var (
numWorkers *int = flag.Int("workers", 4, "Amount of tests to run in parallel")
testsFile *string = flag.String("tests", "", "Path to JSON file containing tests")
snapshotDir *string = flag.String(
"snapshot-dir", "./snapshots", "Directory where snapshots are stored",
)
junitPath *string = flag.String(
"junit", "", "Path to generate JUNIT report to, leave empty to disable",
)
updateSnapshots *bool = flag.Bool("update-snapshots", false, "Update all snapshots")
)
func main() {
flag.Parse()
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr})
log.Info().
Int("workers", *numWorkers).
Msg("Starting nixtest")
if _, err := os.Stat(*testsFile); errors.Is(err, os.ErrNotExist) {
log.Error().Str("file", *testsFile).Msg("Tests file does not exist")
os.Exit(1)
}
parsedSpecs, err := ParseFile[[]SuiteSpec](*testsFile)
if err != nil {
log.Error().Err(err).Msg("Failed to load tests from file")
os.Exit(1)
}
totalTests := 0
for _, suite := range parsedSpecs {
totalTests += len(suite.Tests)
}
log.Info().
Int("suites", len(parsedSpecs)).
Int("tests", totalTests).
Msg("Discovered suites")
jobsChan := make(chan TestSpec, totalTests)
resultsChan := make(chan TestResult, totalTests)
var wg sync.WaitGroup
for i := 1; i <= *numWorkers; i++ {
wg.Add(1)
go worker(jobsChan, resultsChan, &wg)
}
for _, suite := range parsedSpecs {
for _, test := range suite.Tests {
test.Suite = suite.Name
jobsChan <- test
}
}
close(jobsChan)
wg.Wait()
close(resultsChan)
results := map[string][]TestResult{}
successCount := 0
for r := range resultsChan {
results[r.Suite] = append(results[r.Suite], r)
if r.Success {
successCount++
}
}
if *junitPath != "" {
err = GenerateJunitFile(*junitPath, results)
if err != nil {
log.Error().Err(err).Msg("Failed to generate junit file")
} else {
log.Info().Str("path", *junitPath).Msg("Generated Junit report")
}
}
// print errors/logs of failed tests
printErrors(results)
// show table summary
printSummary(results, successCount, totalTests)
if successCount != totalTests {
os.Exit(2)
}
}

24
cmd/nixtest/utils.go Normal file
View file

@ -0,0 +1,24 @@
package main
import (
"encoding/json"
"fmt"
"os"
)
func ParseFile[T any](filePath string) (result T, err error) {
file, err := os.Open(filePath)
if err != nil {
return result, fmt.Errorf("failed to open file %s: %w", filePath, err)
}
defer file.Close()
decoder := json.NewDecoder(file)
err = decoder.Decode(&result)
if err != nil {
return result, fmt.Errorf("failed to decode JSON from file %s: %w", filePath, err)
}
return result, nil
}

345
flake.lock generated Normal file
View file

@ -0,0 +1,345 @@
{
"nodes": {
"cachix": {
"inputs": {
"devenv": [
"devenv"
],
"flake-compat": [
"devenv"
],
"git-hooks": [
"devenv"
],
"nixpkgs": "nixpkgs"
},
"locked": {
"lastModified": 1742042642,
"narHash": "sha256-D0gP8srrX0qj+wNYNPdtVJsQuFzIng3q43thnHXQ/es=",
"owner": "cachix",
"repo": "cachix",
"rev": "a624d3eaf4b1d225f918de8543ed739f2f574203",
"type": "github"
},
"original": {
"owner": "cachix",
"ref": "latest",
"repo": "cachix",
"type": "github"
}
},
"devenv": {
"inputs": {
"cachix": "cachix",
"flake-compat": "flake-compat",
"git-hooks": "git-hooks",
"nix": "nix",
"nixpkgs": "nixpkgs_3"
},
"locked": {
"lastModified": 1746189866,
"narHash": "sha256-3sTvuSVBFcXbqg26Qcw/ENJ1s36jtzEcZ0mHqLqvWRA=",
"owner": "cachix",
"repo": "devenv",
"rev": "5fc592d45dd056035e0fd5000893a21609c35526",
"type": "github"
},
"original": {
"owner": "cachix",
"repo": "devenv",
"type": "github"
}
},
"flake-compat": {
"flake": false,
"locked": {
"lastModified": 1733328505,
"narHash": "sha256-NeCCThCEP3eCl2l/+27kNNK7QrwZB1IJCrXfrbv5oqU=",
"owner": "edolstra",
"repo": "flake-compat",
"rev": "ff81ac966bb2cae68946d5ed5fc4994f96d0ffec",
"type": "github"
},
"original": {
"owner": "edolstra",
"repo": "flake-compat",
"type": "github"
}
},
"flake-parts": {
"inputs": {
"nixpkgs-lib": [
"devenv",
"nix",
"nixpkgs"
]
},
"locked": {
"lastModified": 1712014858,
"narHash": "sha256-sB4SWl2lX95bExY2gMFG5HIzvva5AVMJd4Igm+GpZNw=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "9126214d0a59633752a136528f5f3b9aa8565b7d",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"repo": "flake-parts",
"type": "github"
}
},
"flake-parts_2": {
"inputs": {
"nixpkgs-lib": "nixpkgs-lib"
},
"locked": {
"lastModified": 1743550720,
"narHash": "sha256-hIshGgKZCgWh6AYJpJmRgFdR3WUbkY04o82X05xqQiY=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "c621e8422220273271f52058f618c94e405bb0f5",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"repo": "flake-parts",
"type": "github"
}
},
"git-hooks": {
"inputs": {
"flake-compat": [
"devenv"
],
"gitignore": "gitignore",
"nixpkgs": [
"devenv",
"nixpkgs"
]
},
"locked": {
"lastModified": 1742649964,
"narHash": "sha256-DwOTp7nvfi8mRfuL1escHDXabVXFGT1VlPD1JHrtrco=",
"owner": "cachix",
"repo": "git-hooks.nix",
"rev": "dcf5072734cb576d2b0c59b2ac44f5050b5eac82",
"type": "github"
},
"original": {
"owner": "cachix",
"repo": "git-hooks.nix",
"type": "github"
}
},
"gitignore": {
"inputs": {
"nixpkgs": [
"devenv",
"git-hooks",
"nixpkgs"
]
},
"locked": {
"lastModified": 1709087332,
"narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=",
"owner": "hercules-ci",
"repo": "gitignore.nix",
"rev": "637db329424fd7e46cf4185293b9cc8c88c95394",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"repo": "gitignore.nix",
"type": "github"
}
},
"libgit2": {
"flake": false,
"locked": {
"lastModified": 1697646580,
"narHash": "sha256-oX4Z3S9WtJlwvj0uH9HlYcWv+x1hqp8mhXl7HsLu2f0=",
"owner": "libgit2",
"repo": "libgit2",
"rev": "45fd9ed7ae1a9b74b957ef4f337bc3c8b3df01b5",
"type": "github"
},
"original": {
"owner": "libgit2",
"repo": "libgit2",
"type": "github"
}
},
"nix": {
"inputs": {
"flake-compat": [
"devenv"
],
"flake-parts": "flake-parts",
"libgit2": "libgit2",
"nixpkgs": "nixpkgs_2",
"nixpkgs-23-11": [
"devenv"
],
"nixpkgs-regression": [
"devenv"
],
"pre-commit-hooks": [
"devenv"
]
},
"locked": {
"lastModified": 1745930071,
"narHash": "sha256-bYyjarS3qSNqxfgc89IoVz8cAFDkF9yPE63EJr+h50s=",
"owner": "domenkozar",
"repo": "nix",
"rev": "b455edf3505f1bf0172b39a735caef94687d0d9c",
"type": "github"
},
"original": {
"owner": "domenkozar",
"ref": "devenv-2.24",
"repo": "nix",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1733212471,
"narHash": "sha256-M1+uCoV5igihRfcUKrr1riygbe73/dzNnzPsmaLCmpo=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "55d15ad12a74eb7d4646254e13638ad0c4128776",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs-lib": {
"locked": {
"lastModified": 1743296961,
"narHash": "sha256-b1EdN3cULCqtorQ4QeWgLMrd5ZGOjLSLemfa00heasc=",
"owner": "nix-community",
"repo": "nixpkgs.lib",
"rev": "e4822aea2a6d1cdd36653c134cacfd64c97ff4fa",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "nixpkgs.lib",
"type": "github"
}
},
"nixpkgs_2": {
"locked": {
"lastModified": 1717432640,
"narHash": "sha256-+f9c4/ZX5MWDOuB1rKoWj+lBNm0z0rs4CK47HBLxy1o=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "88269ab3044128b7c2f4c7d68448b2fb50456870",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "release-24.05",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_3": {
"locked": {
"lastModified": 1733477122,
"narHash": "sha256-qamMCz5mNpQmgBwc8SB5tVMlD5sbwVIToVZtSxMph9s=",
"owner": "cachix",
"repo": "devenv-nixpkgs",
"rev": "7bd9e84d0452f6d2e63b6e6da29fe73fac951857",
"type": "github"
},
"original": {
"owner": "cachix",
"ref": "rolling",
"repo": "devenv-nixpkgs",
"type": "github"
}
},
"nixpkgs_4": {
"locked": {
"lastModified": 1746152631,
"narHash": "sha256-zBuvmL6+CUsk2J8GINpyy8Hs1Zp4PP6iBWSmZ4SCQ/s=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "032bc6539bd5f14e9d0c51bd79cfe9a055b094c3",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_5": {
"locked": {
"lastModified": 1745377448,
"narHash": "sha256-jhZDfXVKdD7TSEGgzFJQvEEZ2K65UMiqW5YJ2aIqxMA=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "507b63021ada5fee621b6ca371c4fca9ca46f52c",
"type": "github"
},
"original": {
"owner": "nixos",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"devenv": "devenv",
"flake-parts": "flake-parts_2",
"nixpkgs": "nixpkgs_4",
"systems": "systems",
"treefmt-nix": "treefmt-nix"
}
},
"systems": {
"locked": {
"lastModified": 1689347949,
"narHash": "sha256-12tWmuL2zgBgZkdoB6qXZsgJEH9LR3oUgpaQq2RbI80=",
"owner": "nix-systems",
"repo": "default-linux",
"rev": "31732fcf5e8fea42e59c2488ad31a0e651500f68",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default-linux",
"type": "github"
}
},
"treefmt-nix": {
"inputs": {
"nixpkgs": "nixpkgs_5"
},
"locked": {
"lastModified": 1746216483,
"narHash": "sha256-4h3s1L/kKqt3gMDcVfN8/4v2jqHrgLIe4qok4ApH5x4=",
"owner": "numtide",
"repo": "treefmt-nix",
"rev": "29ec5026372e0dec56f890e50dbe4f45930320fd",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "treefmt-nix",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

100
flake.nix Normal file
View file

@ -0,0 +1,100 @@
{
outputs = {
flake-parts,
systems,
...
} @ inputs:
flake-parts.lib.mkFlake {inherit inputs;} {
imports = [
inputs.devenv.flakeModule
inputs.treefmt-nix.flakeModule
./lib/flakeModule.nix
];
systems = import systems;
flake = {};
perSystem = {
pkgs,
config,
...
}: {
treefmt = {
projectRootFile = "flake.nix";
programs = {
alejandra.enable = true;
mdformat.enable = true;
gofmt.enable = true;
};
};
devenv.shells.default = {
containers = pkgs.lib.mkForce {};
packages = [pkgs.gopls pkgs.gore];
languages.go.enable = true;
pre-commit.hooks = {
treefmt = {
enable = true;
packageOverrides.treefmt = config.treefmt.build.wrapper;
};
convco.enable = true;
};
};
testSuites = {
"suite-one" = [
{
name = "test-one";
# required to figure out file and line, but optional
pos = __curPos;
expected = 1;
actual = 1;
}
{
name = "fail";
expected = 0;
actual = "meow";
}
{
name = "snapshot-test";
type = "snapshot";
pos = __curPos;
actual = "test";
}
];
"other-suite" = [
{
name = "obj-snapshot";
type = "snapshot";
actual = {hello = "world";};
}
];
};
packages.default = pkgs.callPackage ./package.nix {};
};
};
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
# flake & devenv related
flake-parts.url = "github:hercules-ci/flake-parts";
systems.url = "github:nix-systems/default-linux";
devenv.url = "github:cachix/devenv";
treefmt-nix.url = "github:numtide/treefmt-nix";
};
nixConfig = {
extra-substituters = [
"https://cache.nixos.org/"
"https://nix-community.cachix.org"
"https://devenv.cachix.org"
];
extra-trusted-public-keys = [
"cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY="
"nix-community.cachix.org-1:mB9FSh9qf2dCimDSUo8Zy7bkq5CX+/rkCWyvRCYg3Fs="
"devenv.cachix.org-1:w1cLUi8dv3hnoSPGAuibQv+f9TZLr6cv/Hm9XgU50cw="
];
};
}

22
go.mod Normal file
View file

@ -0,0 +1,22 @@
module gitlab.com/technofab/testnix
go 1.24.2
require (
github.com/rs/zerolog v1.34.0
github.com/spf13/pflag v1.0.6
)
require (
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
golang.org/x/text v0.22.0 // indirect
)
require (
github.com/jedib0t/go-pretty/v6 v6.6.7
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/sergi/go-diff v1.3.1
golang.org/x/sys v0.32.0 // indirect
)

43
go.sum Normal file
View file

@ -0,0 +1,43 @@
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/jedib0t/go-pretty/v6 v6.6.7 h1:m+LbHpm0aIAPLzLbMfn8dc3Ht8MW7lsSO4MPItz/Uuo=
github.com/jedib0t/go-pretty/v6 v6.6.7/go.mod h1:YwC5CE4fJ1HFUDeivSV1r//AmANFHyqczZk+U6BDALU=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=

31
lib/default.nix Normal file
View file

@ -0,0 +1,31 @@
{pkgs, ...}: {
mkTest = {
type ? "unit",
name,
description ? "",
expected ? null,
actual ? null,
actualDrv ? null,
pos ? null,
}: {
inherit type name description expected actual;
actualDrv = actualDrv.drvPath or "";
pos =
if pos == null
then ""
else "${pos.file}:${toString pos.line}:${toString pos.column}";
};
mkSuite = name: tests: {
inherit name tests;
};
exportSuites = suites: let
suitesList =
if builtins.isList suites
then suites
else [suites];
testsMapped = builtins.toJSON suitesList;
in
pkgs.runCommand "tests.json" {} ''
echo '${testsMapped}' > $out
'';
}

6
lib/flake.nix Normal file
View file

@ -0,0 +1,6 @@
{
outputs = inputs: {
lib = import ./.;
flakeModule = import ./flakeModule.nix;
};
}

41
lib/flakeModule.nix Normal file
View file

@ -0,0 +1,41 @@
{
flake-parts-lib,
lib,
...
}: let
inherit (lib) mkOption types;
in {
options.perSystem = flake-parts-lib.mkPerSystemOption (
{
config,
pkgs,
...
}: let
nixtests-lib = import ./. {inherit pkgs;};
in {
options.testSuites = mkOption {
type = types.attrsOf (types.listOf types.attrs);
default = {};
};
config.legacyPackages = rec {
"nixtests" = let
suites = map (suiteName: let
tests = builtins.getAttr suiteName config.testSuites;
in
nixtests-lib.mkSuite
suiteName
(map (test: nixtests-lib.mkTest test) tests))
(builtins.attrNames config.testSuites);
in
nixtests-lib.exportSuites suites;
"nixtests:run" = let
program = pkgs.callPackage ./../package.nix {};
in
pkgs.writeShellScriptBin "nixtests:run" ''
${program}/bin/nixtest --tests=${nixtests} "$@"
'';
};
}
);
}

16
package.nix Normal file
View file

@ -0,0 +1,16 @@
{buildGoModule, ...}:
buildGoModule {
name = "nixtest";
src =
# filter everything except for cmd/ and go.mod, go.sum
builtins.filterSource (
path: type:
builtins.match ".*(/cmd/?.*|/go\.(mod|sum))$"
path
!= null
)
./.;
subPackages = ["cmd/nixtest"];
vendorHash = "sha256-H0KiuTqY2cxsUvqoxWAHKHjdfsBHjYkqxdYgTY0ftes=";
meta.mainProgram = "nixtest";
}

View file

@ -0,0 +1 @@
{"bye":"world"}

View file

@ -0,0 +1 @@
"test"