commit c1c19c324d67dbcd482ed9cd3dac05f74a042a98 Author: technofab Date: Sat May 3 22:05:29 2025 +0200 chore: initial prototype diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..f990172 --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +use flake . --impure --accept-flake-config diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..247d87e --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.direnv/ +.devenv/ +result +.pre-commit-config.yaml +*.xml diff --git a/cmd/nixtest/display.go b/cmd/nixtest/display.go new file mode 100644 index 0000000..6b06c2a --- /dev/null +++ b/cmd/nixtest/display.go @@ -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() +} diff --git a/cmd/nixtest/junit.go b/cmd/nixtest/junit.go new file mode 100644 index 0000000..74dee7d --- /dev/null +++ b/cmd/nixtest/junit.go @@ -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 +} diff --git a/cmd/nixtest/main.go b/cmd/nixtest/main.go new file mode 100644 index 0000000..18a9a6b --- /dev/null +++ b/cmd/nixtest/main.go @@ -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) + } +} diff --git a/cmd/nixtest/utils.go b/cmd/nixtest/utils.go new file mode 100644 index 0000000..515a938 --- /dev/null +++ b/cmd/nixtest/utils.go @@ -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 +} diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..2a73032 --- /dev/null +++ b/flake.lock @@ -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 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..6ed14d6 --- /dev/null +++ b/flake.nix @@ -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=" + ]; + }; +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..c6cfd29 --- /dev/null +++ b/go.mod @@ -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 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..cc4692d --- /dev/null +++ b/go.sum @@ -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= diff --git a/lib/default.nix b/lib/default.nix new file mode 100644 index 0000000..dfa69f1 --- /dev/null +++ b/lib/default.nix @@ -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 + ''; +} diff --git a/lib/flake.nix b/lib/flake.nix new file mode 100644 index 0000000..7aa48f8 --- /dev/null +++ b/lib/flake.nix @@ -0,0 +1,6 @@ +{ + outputs = inputs: { + lib = import ./.; + flakeModule = import ./flakeModule.nix; + }; +} diff --git a/lib/flakeModule.nix b/lib/flakeModule.nix new file mode 100644 index 0000000..12d900f --- /dev/null +++ b/lib/flakeModule.nix @@ -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} "$@" + ''; + }; + } + ); +} diff --git a/package.nix b/package.nix new file mode 100644 index 0000000..7ac87cc --- /dev/null +++ b/package.nix @@ -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"; +} diff --git a/snapshots/obj-snapshot.snap.json b/snapshots/obj-snapshot.snap.json new file mode 100644 index 0000000..cf7bd6e --- /dev/null +++ b/snapshots/obj-snapshot.snap.json @@ -0,0 +1 @@ +{"bye":"world"} diff --git a/snapshots/snapshot-test.snap.json b/snapshots/snapshot-test.snap.json new file mode 100644 index 0000000..60bc259 --- /dev/null +++ b/snapshots/snapshot-test.snap.json @@ -0,0 +1 @@ +"test" \ No newline at end of file