mirror of
https://gitlab.com/TECHNOFAB/nixtest.git
synced 2025-12-11 01:30:11 +01:00
chore: initial prototype
This commit is contained in:
commit
c1c19c324d
16 changed files with 1099 additions and 0 deletions
1
.envrc
Normal file
1
.envrc
Normal file
|
|
@ -0,0 +1 @@
|
|||
use flake . --impure --accept-flake-config
|
||||
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
.direnv/
|
||||
.devenv/
|
||||
result
|
||||
.pre-commit-config.yaml
|
||||
*.xml
|
||||
78
cmd/nixtest/display.go
Normal file
78
cmd/nixtest/display.go
Normal 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
115
cmd/nixtest/junit.go
Normal 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
270
cmd/nixtest/main.go
Normal 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
24
cmd/nixtest/utils.go
Normal 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
345
flake.lock
generated
Normal 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
100
flake.nix
Normal 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
22
go.mod
Normal 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
43
go.sum
Normal 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
31
lib/default.nix
Normal 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
6
lib/flake.nix
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
outputs = inputs: {
|
||||
lib = import ./.;
|
||||
flakeModule = import ./flakeModule.nix;
|
||||
};
|
||||
}
|
||||
41
lib/flakeModule.nix
Normal file
41
lib/flakeModule.nix
Normal 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
16
package.nix
Normal 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";
|
||||
}
|
||||
1
snapshots/obj-snapshot.snap.json
Normal file
1
snapshots/obj-snapshot.snap.json
Normal file
|
|
@ -0,0 +1 @@
|
|||
{"bye":"world"}
|
||||
1
snapshots/snapshot-test.snap.json
Normal file
1
snapshots/snapshot-test.snap.json
Normal file
|
|
@ -0,0 +1 @@
|
|||
"test"
|
||||
Loading…
Add table
Add a link
Reference in a new issue