exec: replace global state with interface

This is cleaner, and finally enables writing tests for the nix invoking functions.
This commit is contained in:
Yonah 2025-07-17 16:53:00 +09:00
parent aa4bbbc2fe
commit 69c6128ff5
Signed by: yonah
SSH Key Fingerprint: SHA256:vnQvK8+XXH9Tbni2AV1a/8qdVK/zPcXw52GM0ruQvwA
34 changed files with 324496 additions and 258903 deletions

View File

@ -2,42 +2,28 @@ package nixbuild
import (
"context"
"errors"
"io"
"iter"
)
// Build builds all entries yielded by installables.
func Build(ctx context.Context, installables iter.Seq[string]) error {
c, cancel := context.WithCancel(ctx)
func Build(ctx Context, installables iter.Seq[string]) error {
c, cancel := context.WithCancel(ctx.Unwrap())
defer cancel()
stdout, stderr := ctx.Streams()
cmd := Nix(c, CommandBuild,
cmd := ctx.Nix(c, CommandBuild,
FlagKeepGoing, FlagNoLink, FlagStdin)
if Stdout != nil {
cmd.Stdout = Stdout
if stdout != nil {
cmd.Stdout = stdout
cmd.Args = append(cmd.Args, FlagPrintBuildLogs)
} else {
cmd.Args = append(cmd.Args, FlagQuiet)
}
if Stderr != nil {
cmd.Stderr = Stderr
if stderr != nil {
cmd.Stderr = stderr
cmd.Args = append(cmd.Args, FlagVerbose)
}
ir, iw := io.Pipe()
cmd.Stdin = ir
if err := cmd.Start(); err != nil {
return err
}
if _, err := WriteStdin(iw, installables); err != nil {
return errors.Join(err, cmd.Wait())
}
if err := iw.Close(); err != nil {
return errors.Join(err, cmd.Wait())
}
return cmd.Wait()
_, err := ctx.WriteStdin(cmd, installables, nil)
return err
}

93
build_test.go Normal file
View File

@ -0,0 +1,93 @@
package nixbuild_test
import (
"errors"
"io"
"os"
"slices"
"strings"
"syscall"
"testing"
"git.gensokyo.uk/yonah/nixbuild"
"hakurei.app/command"
)
func init() {
stubCommandInit = append(stubCommandInit, func(c command.Command) {
var (
flagBuildKeepGoing bool
flagBuildNoLink bool
flagBuildStdin bool
flagBuildQuiet bool
flagBuildPBL bool
flagBuildVerbose bool
)
c.NewCommand(nixbuild.CommandBuild, "emit samples for various `nix build` cases", func(args []string) error {
switch {
default:
return os.ErrInvalid
case len(args) == 7 && slices.Equal(args[1:], []string{
nixbuild.FlagOption, nixbuild.OptionEvalCache, nixbuild.ValueFalse,
nixbuild.FlagDryRun, nixbuild.FlagPrintBuildLogs, nixbuild.FlagDebug,
}): // InstantiatedEvaluator
return stubInstantiatedEvaluator(args)
case flagBuildKeepGoing, flagBuildNoLink, flagBuildStdin: // Build
switch {
default:
return syscall.ENOTRECOVERABLE
case flagBuildQuiet && !flagBuildPBL && !flagBuildVerbose:
return checkStdin(os.Stdin, "quiet")
case !flagBuildQuiet && flagBuildPBL && !flagBuildVerbose:
return checkStdin(os.Stdin, "logs")
case flagBuildQuiet && !flagBuildPBL && flagBuildVerbose:
return checkStdin(os.Stdin, "quiet", "verbose")
case !flagBuildQuiet && flagBuildPBL && flagBuildVerbose:
return checkStdin(os.Stdin, "logs", "verbose")
}
}
}).
Flag(&flagBuildKeepGoing, trimFlagName(nixbuild.FlagKeepGoing), command.BoolFlag(false), nixbuild.FlagKeepGoing).
Flag(&flagBuildNoLink, trimFlagName(nixbuild.FlagNoLink), command.BoolFlag(false), nixbuild.FlagNoLink).
Flag(&flagBuildStdin, trimFlagName(nixbuild.FlagStdin), command.BoolFlag(false), nixbuild.FlagStdin).
Flag(&flagBuildQuiet, trimFlagName(nixbuild.FlagQuiet), command.BoolFlag(false), nixbuild.FlagQuiet).
Flag(&flagBuildPBL, trimFlagName(nixbuild.FlagPrintBuildLogs), command.BoolFlag(false), nixbuild.FlagPrintBuildLogs).
Flag(&flagBuildVerbose, trimFlagName(nixbuild.FlagVerbose), command.BoolFlag(false), nixbuild.FlagVerbose)
})
}
func TestBuild(t *testing.T) {
stubNixCommand(t)
check := func(stdout, stderr io.Writer, v ...string) {
t.Run(strings.Join(v, " "), func(t *testing.T) {
if err := nixbuild.Build(
newStubContext(t.Context(), nil, stdout, stderr),
slices.Values(v),
); err != nil {
t.Errorf("Build: error = %v", err)
}
})
}
check(nil, nil, "quiet")
check(os.Stdout, nil, "logs")
check(nil, os.Stderr, "quiet", "verbose")
check(os.Stdout, os.Stderr, "logs", "verbose")
}
func TestBuildBadCommand(t *testing.T) {
wantErr := os.ErrNotExist
breakNixCommand(t)
if err := nixbuild.Build(
nixbuild.New(t.Context(), nil, nil, nil),
nil,
); !errors.Is(err, wantErr) {
t.Errorf("Build: error = %v, want %v", err, wantErr)
}
}

View File

@ -5,6 +5,7 @@ import (
"encoding/json"
"errors"
"fmt"
"io"
"log"
"os"
"os/signal"
@ -20,7 +21,9 @@ type commandHandlerError string
func (c commandHandlerError) Error() string { return string(c) }
func main() {
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
var ctx nixbuild.Context
nixCtx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
var (
@ -31,10 +34,13 @@ func main() {
c := command.New(os.Stderr, log.Printf, "nixbuild", func(args []string) error {
log.SetFlags(0)
log.SetPrefix("nixbuild: ")
nixbuild.Stdout = os.Stdout
var stderr io.Writer
if flagVerbose {
nixbuild.Stderr = os.Stderr
stderr = os.Stderr
}
ctx = nixbuild.New(nixCtx, nil, os.Stdout, stderr)
return nil
}).
Flag(&flagNixOS, "nixos", command.BoolFlag(false), "Interpret input as NixOS flake installable").

22
context.go Normal file
View File

@ -0,0 +1,22 @@
package nixbuild
import (
"context"
"io"
"iter"
"os/exec"
)
// Context holds configuration and environment information for interacting with nix.
type Context interface {
// Streams returns the stdout and stderr writers held by this [Context].
Streams() (stdout, stderr io.Writer)
// Nix returns the [exec.Cmd] struct to execute a nix command.
Nix(ctx context.Context, arg ...string) *exec.Cmd
// WriteStdin calls [WriteStdin] for [exec.Cmd]. The function f points to is called if [WriteStdin] succeeds.
WriteStdin(cmd *exec.Cmd, installables iter.Seq[string], f func() error) (int, error)
// Unwrap returns the stored [context.Context]
Unwrap() context.Context
}

View File

@ -3,8 +3,6 @@ package nixbuild
import (
"context"
"encoding/json"
"errors"
"io"
"iter"
)
@ -48,41 +46,24 @@ type (
)
// DerivationShow returns a [DerivationMap] describing all entries yielded by installables.
func DerivationShow(ctx context.Context, installables iter.Seq[string]) (DerivationMap, error) {
c, cancel := context.WithCancel(ctx)
func DerivationShow(ctx Context, installables iter.Seq[string]) (DerivationMap, error) {
c, cancel := context.WithCancel(ctx.Unwrap())
defer cancel()
_, stderr := ctx.Streams()
var decoder *json.Decoder
cmd := Nix(c, CommandDerivation, CommandDerivationShow,
cmd := ctx.Nix(c, CommandDerivation, CommandDerivationShow,
FlagStdin)
ir, iw := io.Pipe()
cmd.Stdin = ir
or, ow := io.Pipe()
cmd.Stdout = ow
if Stderr != nil {
cmd.Stderr = Stderr
}
cmd.Cancel = func() error { _ = ow.Close(); return cmd.Process.Kill() }
if err := cmd.Start(); err != nil {
if r, err := cmd.StdoutPipe(); err != nil {
return nil, err
} else {
decoder = json.NewDecoder(r)
}
if stderr != nil {
cmd.Stderr = stderr
}
done := make(chan error)
var v DerivationMap
go func() { done <- json.NewDecoder(or).Decode(&v) }()
if _, err := WriteStdin(iw, installables); err != nil {
return nil, errors.Join(err, cmd.Wait())
}
if err := iw.Close(); err != nil {
return nil, errors.Join(err, cmd.Wait())
}
if err := cmd.Wait(); err != nil {
// deferred cancel closes pipe
return nil, err
}
err := <-done
_, err := ctx.WriteStdin(cmd, installables, func() error { return decoder.Decode(&v) })
return v, err
}

View File

@ -1,11 +0,0 @@
package nixbuild_test
import _ "embed"
var (
//go:embed testdata/derivation/show_getchoo_atlas.json
getchooAtlasShow []byte
//go:embed testdata/derivation/collect_getchoo_atlas
getchooAtlasCollective string
)

View File

@ -1,11 +0,0 @@
package nixbuild_test
import _ "embed"
var (
//go:embed testdata/derivation/show_getchoo_glados.json
getchooGladosShow []byte
//go:embed testdata/derivation/collect_getchoo_glados
getchooGladosCollective string
)

View File

@ -1,11 +0,0 @@
package nixbuild_test
import _ "embed"
var (
//go:embed testdata/derivation/show_pluiedev_pappardelle.json
pluiedevPappardelleShow []byte
//go:embed testdata/derivation/collect_pluiedev_pappardelle
pluiedevPappardelleCollective string
)

View File

@ -2,35 +2,111 @@ package nixbuild_test
import (
"encoding/json"
"os"
"os/exec"
"reflect"
"slices"
"strings"
"syscall"
"testing"
"git.gensokyo.uk/yonah/nixbuild"
"hakurei.app/command"
)
func TestCollectFromDerivations(t *testing.T) {
testCases := []struct {
name string
show []byte
want string
}{
{"getchoo atlas", getchooAtlasShow, getchooAtlasCollective},
{"getchoo glados", getchooGladosShow, getchooGladosCollective},
{"pluiedev pappardelle", pluiedevPappardelleShow, pluiedevPappardelleCollective},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
var derivations nixbuild.DerivationMap
if err := json.Unmarshal(tc.show, &derivations); err != nil {
t.Fatalf("cannot unmarshal test data: %v", err)
func init() {
stubCommandInit = append(stubCommandInit, func(c command.Command) {
c.NewCommand(nixbuild.CommandDerivation, "emit samples for various `nix derivation` cases", func(args []string) error {
if len(args) == 0 {
return syscall.ENOSYS
}
got := nixbuild.CollectFromDerivations(derivations)
want := strings.Split(strings.TrimSpace(tc.want), "\n")
if !slices.Equal(got, want) {
t.Errorf("CollectFromDerivations:\n%s, want\n%s",
strings.Join(got, "\n"), strings.Join(want, "\n"))
switch args[0] {
default:
return syscall.EINVAL
case nixbuild.CommandDerivationShow:
if len(args) != 2 {
return syscall.ENOSYS
}
switch args[1] {
default:
return syscall.EINVAL
case nixbuild.FlagStdin:
var testName string
if want, err := nixbuild.ReadStdin(os.Stdin); err != nil {
return err
} else {
for n, w := range instWant {
if slices.Equal(w, want) {
testName = n
break
}
}
if testName == "" {
return syscall.ENOSYS
}
}
if sample, ok := drvShow[testName]; !ok {
return syscall.ENOSYS
} else {
_, err := os.Stdout.Write(sample)
return err
}
}
}
})
})
}
func TestDerivationShow(t *testing.T) {
stubNixCommand(t)
testCases := []struct {
name string
}{
{"getchoo atlas"},
{"getchoo glados"},
{"pluiedev pappardelle"},
}
for _, tc := range testCases {
got, err := nixbuild.DerivationShow(
newStubContext(t.Context(), nil, os.Stdout, os.Stderr),
slices.Values(instWant[tc.name]),
)
if err != nil {
t.Fatalf("DerivationShow: error = %v", err)
}
var want nixbuild.DerivationMap
if err = json.Unmarshal(drvShow[tc.name], &want); err != nil {
t.Fatalf("cannot unmarshal: %v", err)
}
if !reflect.DeepEqual(got, want) {
t.Fatalf("DerivationShow: %#v, want %#v", got, want)
}
}
}
func TestDerivationShowAlreadySet(t *testing.T) {
stubNixCommand(t)
if _, err := nixbuild.DerivationShow(
newStubContextCommand(func(cmd *exec.Cmd) { cmd.Stdout = os.Stdout }, t.Context(), nil, os.Stdout, os.Stderr),
nil,
); err == nil {
t.Errorf("DerivationShow unexpectedly succeeded")
}
}
func BenchmarkShowUnmarshal(b *testing.B) {
var v nixbuild.DerivationMap
data := drvShow["pluiedev pappardelle"]
for b.Loop() {
if err := json.Unmarshal(data, &v); err != nil {
b.Fatalf("Unmarshal: error = %v", err)
}
}
}

68
exec.go Normal file
View File

@ -0,0 +1,68 @@
package nixbuild
import (
"context"
"errors"
"io"
"iter"
"os/exec"
)
// Nix is the name of the nix program.
var Nix = "nix"
type nix struct {
name string
ctx context.Context
extra []string
stdout, stderr io.Writer
}
func (n *nix) Unwrap() context.Context { return n.ctx }
func (n *nix) Streams() (stdout, stderr io.Writer) { return n.stdout, n.stderr }
const (
ExtraExperimentalFeatures = "--extra-experimental-features"
ExperimentalFeaturesFlakes = "nix-command flakes"
)
/*
New returns a new [Context].
A non-nil stderr implies verbose.
Streams will not be connected for commands outputting JSON.
*/
func New(ctx context.Context, extraArgs []string, stdout, stderr io.Writer) Context {
return &nix{
name: Nix,
ctx: ctx,
// since flakes are supposedly experimental
extra: append(extraArgs, ExtraExperimentalFeatures, ExperimentalFeaturesFlakes),
stdout: stdout,
stderr: stderr,
}
}
func (n *nix) Nix(ctx context.Context, arg ...string) *exec.Cmd {
return exec.CommandContext(ctx, n.name, append(n.extra, arg...)...)
}
func (n *nix) WriteStdin(cmd *exec.Cmd, installables iter.Seq[string], f func() error) (int, error) {
w, err := cmd.StdinPipe()
if err != nil {
return 0, err
}
if err = cmd.Start(); err != nil {
return 0, err
}
count, writeErr := WriteStdin(w, installables)
closeErr := w.Close()
var fErr error
if f != nil && writeErr == nil && closeErr == nil {
fErr = f()
}
return count, errors.Join(writeErr, closeErr, fErr, cmd.Wait())
}

37
exec_test.go Normal file
View File

@ -0,0 +1,37 @@
package nixbuild_test
import (
"errors"
"os"
"os/exec"
"slices"
"syscall"
"testing"
"git.gensokyo.uk/yonah/nixbuild"
)
func TestNixWriteStdin(t *testing.T) {
ctx := nixbuild.New(t.Context(), nil, os.Stdout, os.Stderr)
t.Run("already set", func(t *testing.T) {
cmd := exec.CommandContext(t.Context(), "/proc/nonexistent")
cmd.Stdin = os.Stdin
if _, err := ctx.WriteStdin(cmd, nil, nil); err == nil {
t.Fatal("WriteStdinCommand unexpectedly succeeded")
}
})
t.Run("f returns error", func(t *testing.T) {
stubNixCommand(t)
ctx := newStubContext(t.Context(), nil, os.Stdout, os.Stderr)
cmd := ctx.Nix(t.Context(), "true")
if _, err := ctx.WriteStdin(
cmd,
slices.Values(make([]string, 0)),
func() error { return syscall.ENOSYS },
); !errors.Is(err, syscall.ENOSYS) {
t.Fatalf("WriteStdinCommand: error = %v, want %v", err, syscall.ENOSYS)
}
})
}

View File

@ -1,10 +1,10 @@
package nixbuild
import (
"context"
"bufio"
"io"
"iter"
"os/exec"
"slices"
"strings"
)
@ -73,21 +73,6 @@ const (
ValueFalse = "false"
)
const (
nix = "nix"
nixExtraExperimentalFeatures = "--extra-experimental-features"
nixExperimentalFeaturesFlakes = "nix-command flakes"
)
// since flakes are supposedly experimental
var nixEnableFlakes = []string{nixExtraExperimentalFeatures, nixExperimentalFeaturesFlakes}
// Nix returns the [exec.Cmd] struct to execute a nix command.
func Nix(ctx context.Context, arg ...string) *exec.Cmd {
return exec.CommandContext(ctx, nix, append(nixEnableFlakes, arg...)...)
}
const (
nixosSuffix0 = "#nixosConfigurations."
nixosSuffix1 = ".config.system.build.toplevel"
@ -112,3 +97,23 @@ func WriteStdin(w io.Writer, installables iter.Seq[string]) (int, error) {
}
return count, nil
}
const readStdinInitialCap = 1 << 10
// ReadStdin is the inverse of [WriteStdin].
func ReadStdin(r io.Reader) ([]string, error) {
collective := make([]string, 0, readStdinInitialCap)
scanner := bufio.NewScanner(r)
for scanner.Scan() {
if len(collective) == cap(collective) {
// grow the buffer exponentially to minimise copies
collective = slices.Grow(collective, cap(collective))
}
p := scanner.Text()
if strings.HasSuffix(p, ".drv^*") {
p = p[:len(p)-2]
}
collective = append(collective, p)
}
return collective, scanner.Err()
}

View File

@ -1,32 +1,60 @@
package nixbuild_test
import (
"errors"
"slices"
"strings"
"syscall"
"testing"
"git.gensokyo.uk/yonah/nixbuild"
)
func TestNix(t *testing.T) {
func TestStdin(t *testing.T) {
testCases := []struct {
name string
arg []string
want []string
col []string
raw string
}{
{"build", []string{"build"},
[]string{"nix", "--extra-experimental-features", "nix-command flakes", "build"}},
{"build workflow", []string{"build", `--out-link "result"`, "--print-out-paths", "--print-build-logs"},
[]string{"nix", "--extra-experimental-features", "nix-command flakes", "build", `--out-link "result"`, "--print-out-paths", "--print-build-logs"}},
{"getchoo atlas", getchooAtlasCollective, getchooAtlasStdin},
{"getchoo glados", getchooGladosCollective, getchooGladosStdin},
{"pluiedev pappardelle", pluiedevPappardelleCollective, pluiedevPappardelleStdin},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
got := nixbuild.Nix(t.Context(), tc.arg...)
if !slices.Equal(got.Args, tc.want) {
t.Errorf("Nix: %#v, want %#v",
got.Args, tc.want)
}
t.Run("write", func(t *testing.T) {
w := new(strings.Builder)
if _, err := nixbuild.WriteStdin(w, slices.Values(tc.col)); err != nil {
t.Fatalf("cannot write: %v", err)
}
if w.String() != tc.raw {
t.Errorf("WriteStdin:\n%s\nwant:\n%s", w.String(), tc.raw)
}
})
t.Run("read", func(t *testing.T) {
r := strings.NewReader(tc.raw)
got, err := nixbuild.ReadStdin(r)
if err != nil {
t.Fatalf("cannot read: %v", err)
}
if !slices.Equal(got, tc.col) {
t.Errorf("ReadStdin: %#v, want %#v", got, tc.col)
}
})
})
}
t.Run("write error", func(t *testing.T) {
n, err := nixbuild.WriteStdin(errorWriter{}, slices.Values([]string{"/nix/store"}))
if n != 0 {
panic("unreachable")
}
if !errors.Is(err, syscall.EIO) {
t.Errorf("WriteStdin: error = %v, want %v", err, syscall.EIO)
}
})
}
func TestInstallable(t *testing.T) {

View File

@ -19,7 +19,7 @@ const (
instantiatedFields = 2
// a reasonable starting buffer capacity: pretty close to average of test cases
instantiatedInitialCap = 1 << 13
instantiatedInitialCap = 1 << 15
)
const (
@ -60,6 +60,8 @@ func (m *MalformedInstantiatedError) Is(err error) bool {
// InstantiatedDecoder interprets the verbose output of `nix build` and collects instantiated derivations.
type InstantiatedDecoder struct {
err error
pt func(string)
drv string
scanner *bufio.Scanner
}
@ -67,44 +69,65 @@ type InstantiatedDecoder struct {
func (d *InstantiatedDecoder) Err() error { return errors.Join(d.err, d.scanner.Err()) }
// NewInstantiatedDecoder returns a [InstantiatedDecoder] struct for collecting derivations from r.
func NewInstantiatedDecoder(r io.Reader) *InstantiatedDecoder {
return &InstantiatedDecoder{scanner: bufio.NewScanner(r)}
// Skipped lines are written to w.
func NewInstantiatedDecoder(r io.Reader, w io.Writer) *InstantiatedDecoder {
passthrough := func(string) {}
if w != nil {
passthrough = func(v string) { _, _ = w.Write([]byte(v + "\n")) }
}
return &InstantiatedDecoder{scanner: bufio.NewScanner(r), pt: passthrough}
}
// Scan advances the decoder to the next instantiated path, skipping all unrelated lines in-between.
func (d *InstantiatedDecoder) Scan() bool {
if d.err != nil {
return false
}
rescan:
if !d.scanner.Scan() {
return false
}
v := d.scanner.Text()
if !strings.HasPrefix(v, instantiatedPrefix) {
// write skipped lines to stderr
d.pt(v)
goto rescan
}
fields := strings.SplitN(v, instantiatedSeparator, instantiatedFields)
if len(fields) != instantiatedFields {
// no more than a single -> is expected
d.err = &MalformedInstantiatedError{v, InstantiatedBadFields}
return false
}
// very basic validation here: the output format is not fully understood
if len(fields[1]) < 3 || fields[1][0] != '\'' || fields[1][len(fields[1])-1] != '\'' {
d.err = &MalformedInstantiatedError{v, InstantiatedUnexpectedQuotes}
return false
}
drv := fields[1][1 : len(fields[1])-1]
if !path.IsAbs(drv) {
d.err = &MalformedInstantiatedError{v, InstantiatedNotAbsolute}
return false
}
d.drv = drv
return true
}
// Text returns the most recent instantiated path generated by a call to [InstantiatedDecoder.Scan]
// as a newly allocated string holding its bytes.
func (d *InstantiatedDecoder) Text() string { return d.drv }
// Instantiated returns a non-reusable iterator over instantiated derivations collected from [io.Reader].
func (d *InstantiatedDecoder) Instantiated() iter.Seq[string] {
return func(yield func(string) bool) {
for d.scanner.Scan() {
v := d.scanner.Text()
if !strings.HasPrefix(v, instantiatedPrefix) {
// write skipped lines to stderr
if Stderr != nil {
_, _ = Stderr.Write([]byte(v + "\n"))
}
continue
}
fields := strings.SplitN(v, instantiatedSeparator, instantiatedFields)
if len(fields) != instantiatedFields {
// no more than a single -> is expected
d.err = &MalformedInstantiatedError{v, InstantiatedBadFields}
break
}
// very basic validation here: the output format is not fully understood
if len(fields[1]) < 3 || fields[1][0] != '\'' || fields[1][len(fields[1])-1] != '\'' {
d.err = &MalformedInstantiatedError{v, InstantiatedUnexpectedQuotes}
break
}
drv := fields[1][1 : len(fields[1])-1]
if !path.IsAbs(drv) {
d.err = &MalformedInstantiatedError{v, InstantiatedNotAbsolute}
break
}
if !yield(drv) {
break
for d.Scan() {
if !yield(d.Text()) {
return
}
}
}
@ -147,10 +170,10 @@ func (e *InstantiatedEvaluator) Err() error {
}
// NewInstantiatedEvaluator initialises an [InstantiatedEvaluator] struct and its underlying nix process.
func NewInstantiatedEvaluator(ctx context.Context, installable string) (*InstantiatedEvaluator, error) {
c, cancel := context.WithCancel(ctx)
func NewInstantiatedEvaluator(ctx Context, installable string) (*InstantiatedEvaluator, error) {
c, cancel := context.WithCancel(ctx.Unwrap())
e := &InstantiatedEvaluator{
cmd: Nix(c, CommandBuild, installable,
cmd: ctx.Nix(c, CommandBuild, installable,
// 'instantiated' messages are only emitted when actually evaluating something
FlagOption, OptionEvalCache, ValueFalse,
// do not actually build anything
@ -160,12 +183,16 @@ func NewInstantiatedEvaluator(ctx context.Context, installable string) (*Instant
),
}
e.cmd.Stdout = Stdout
stdout, stderr := ctx.Streams()
e.cmd.Stdout = stdout
// verbose output ends up on stderr in the current nix implementation
er, ew := io.Pipe()
e.cmd.Stderr = ew
e.InstantiatedDecoder = NewInstantiatedDecoder(er)
if r, err := e.cmd.StderrPipe(); err != nil {
cancel()
return nil, err
} else {
e.InstantiatedDecoder = NewInstantiatedDecoder(r, stderr)
}
if err := e.cmd.Start(); err != nil {
cancel()
@ -173,13 +200,13 @@ func NewInstantiatedEvaluator(ctx context.Context, installable string) (*Instant
return nil, err
}
e.waitMu.Lock()
go func() { e.waitErr = e.cmd.Wait(); cancel(); _ = ew.Close(); e.waitMu.Unlock() }()
go func() { e.waitErr = e.cmd.Wait(); cancel(); e.waitMu.Unlock() }()
return e, nil
}
// EvalInstantiated calls the underlying [InstantiatedDecoder.Decode] of a new [InstantiatedEvaluator].
func EvalInstantiated(ctx context.Context, installable string) ([]string, error) {
func EvalInstantiated(ctx Context, installable string) ([]string, error) {
evaluator, err := NewInstantiatedEvaluator(ctx, installable)
if err != nil {
return nil, err

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -2,8 +2,9 @@ package nixbuild_test
import (
"errors"
"io"
"os"
"runtime"
"os/exec"
"slices"
"strings"
"testing"
@ -11,24 +12,21 @@ import (
"git.gensokyo.uk/yonah/nixbuild"
)
func TestDecodeInstantiated(t *testing.T) {
func TestInstantiated(t *testing.T) {
stubNixCommand(t)
testCases := []struct {
name string
out string
want []string
wantErr error
}{
{"bad fields", segmentPrefix + `instantiated 'config.sub-948ae97' ` + segmentSuffix, nil,
&nixbuild.MalformedInstantiatedError{Type: nixbuild.InstantiatedBadFields}},
{"unexpected quotes left", segmentPrefix + `instantiated 'config.sub-948ae97' -> /n'` + segmentSuffix, nil,
&nixbuild.MalformedInstantiatedError{Type: nixbuild.InstantiatedUnexpectedQuotes}},
{"unexpected quotes right", segmentPrefix + `instantiated 'config.sub-948ae97' -> '/n` + segmentSuffix, nil,
&nixbuild.MalformedInstantiatedError{Type: nixbuild.InstantiatedUnexpectedQuotes}},
{"unexpected quotes short", segmentPrefix + `instantiated 'config.sub-948ae97' -> ''` + segmentSuffix, nil,
&nixbuild.MalformedInstantiatedError{Type: nixbuild.InstantiatedUnexpectedQuotes}},
{"not absolute", segmentPrefix + `instantiated 'config.sub-948ae97' -> ' '` + segmentSuffix, nil,
&nixbuild.MalformedInstantiatedError{Type: nixbuild.InstantiatedNotAbsolute}},
{"good segment", segmentPrefix + segmentBody + segmentSuffix, []string{
{"bad fields", nil, &nixbuild.MalformedInstantiatedError{Type: nixbuild.InstantiatedBadFields}},
{"unexpected quotes left", nil, &nixbuild.MalformedInstantiatedError{Type: nixbuild.InstantiatedUnexpectedQuotes}},
{"unexpected quotes right", nil, &nixbuild.MalformedInstantiatedError{Type: nixbuild.InstantiatedUnexpectedQuotes}},
{"unexpected quotes short", nil, &nixbuild.MalformedInstantiatedError{Type: nixbuild.InstantiatedUnexpectedQuotes}},
{"not absolute", nil, &nixbuild.MalformedInstantiatedError{Type: nixbuild.InstantiatedNotAbsolute}},
{"good segment", []string{
"/nix/store/3zilrlmq7r6rpzfd94mwss32b62yinj5-bootstrap-stage0-stdenv-linux.drv",
"/nix/store/7yfwy95p6lcdpljdajs5aw10h6q0sfx0-update-autotools-gnu-config-scripts-hook.drv",
"/nix/store/bamwxswxacs3cjdcydv0z7bj22d7g2kc-config.guess-948ae97.drv",
@ -37,125 +35,175 @@ func TestDecodeInstantiated(t *testing.T) {
"/nix/store/ysp83x9nrks28zkblqmnc1s1kb68dr69-gnu-config-2024-01-01.drv",
}, nil},
{"getchoo atlas", getchooAtlasOut, getchooAtlasInstantiated, nil},
{"getchoo glados", getchooGladosOut, getchooGladosInstantiated, nil},
{"pluiedev pappardelle", pluiedevPappardelleOut, pluiedevPappardelleInstantiated, nil},
{"getchoo atlas", getchooAtlasInstantiated, nil},
{"getchoo glados", getchooGladosInstantiated, nil},
{"pluiedev pappardelle", pluiedevPappardelleInstantiated, nil},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
var stderr io.Writer
if tc.wantErr != nil {
w := nixbuild.Stderr
nixbuild.Stderr = os.Stderr
t.Cleanup(func() { nixbuild.Stderr = w })
stderr = os.Stderr
}
sample := instSample[tc.name]
stderr := strings.NewReader(tc.out)
got, err := nixbuild.NewInstantiatedDecoder(stderr).Decode()
if !errors.Is(err, tc.wantErr) {
t.Fatalf("DecodeInstantiated: error = %v, want %v", err, tc.wantErr)
}
if tc.wantErr != nil {
t.Logf("DecodeInstantiated: error = %v", err)
return
}
if !slices.Equal(got, tc.want) {
t.Errorf("DecodeInstantiated: %#v, want %#v", got, tc.want)
}
})
}
t.Run("stop early", func(t *testing.T) {
want := []string{
"/nix/store/gyks6vvl7x0gq214ldjhi3w4rg37nh8i-zlib-1.3.1.tar.gz.drv",
"/nix/store/bamwxswxacs3cjdcydv0z7bj22d7g2kc-config.guess-948ae97.drv",
"/nix/store/nbsdqpfzh1jlpmh95s69b3iivfcvv3lh-config.sub-948ae97.drv",
}
decoder := nixbuild.NewInstantiatedDecoder(strings.NewReader(segmentPrefix + segmentBody + segmentSuffix))
counter := 3
got := make([]string, 0, counter)
for drv := range decoder.Instantiated() {
got = append(got, drv)
counter--
if counter == 0 {
break
}
}
if !slices.Equal(got, want) {
t.Errorf("Instantiated: %#v, want %#v", got, want)
}
})
t.Run("errors", func(t *testing.T) {
t.Run("bad type", func(t *testing.T) {
badErr := errors.New("")
if errors.Is(new(nixbuild.MalformedInstantiatedError), badErr) {
t.Error("unexpected MalformedInstantiatedError equivalence")
}
})
t.Run("nil", func(t *testing.T) {
if errors.Is(new(nixbuild.MalformedInstantiatedError), (*nixbuild.MalformedInstantiatedError)(nil)) {
t.Error("unexpected MalformedInstantiatedError equivalence")
}
})
t.Run("unreachable", func(t *testing.T) {
defer func() {
wantPanic := "unreachable"
if r := recover(); r != wantPanic {
t.Errorf("Error: panic = %q, want %q", r, wantPanic)
t.Run("decoder", func(t *testing.T) {
out := strings.NewReader(sample)
decoder := nixbuild.NewInstantiatedDecoder(out, stderr)
got, err := decoder.Decode()
if !errors.Is(err, tc.wantErr) {
t.Fatalf("Decode: error = %v, want %v", err, tc.wantErr)
}
}()
_ = (&nixbuild.MalformedInstantiatedError{Type: -1}).Error()
if tc.wantErr != nil {
t.Logf("Decode: error = %v", err)
t.Run("scan after error", func(t *testing.T) {
if decoder.Scan() {
t.Fatalf("Scan unexpectedly succeeded on faulted decoder")
}
})
return
}
if !slices.Equal(got, tc.want) {
t.Errorf("Decode: %#v, want %#v", got, tc.want)
}
})
t.Run("evaluator", func(t *testing.T) {
ctx := newStubContext(t.Context(), nil, os.Stdout, stderr)
got, err := nixbuild.EvalInstantiated(ctx, tc.name)
if !errors.Is(err, tc.wantErr) {
t.Fatalf("EvalInstantiated: error = %v, want %v", err, tc.wantErr)
}
if tc.wantErr != nil {
t.Logf("EvalInstantiated: error = %v", err)
return
}
if !slices.Equal(got, tc.want) {
t.Errorf("EvalInstantiated: %#v, want %#v", got, tc.want)
}
})
})
}
}
func stubInstantiatedEvaluator(args []string) error {
_, _ = os.Stderr.Write([]byte(instSample[args[0]]))
return nil
}
func TestInstantiatedDecoderStopEarly(t *testing.T) {
want := []string{
"/nix/store/gyks6vvl7x0gq214ldjhi3w4rg37nh8i-zlib-1.3.1.tar.gz.drv",
"/nix/store/bamwxswxacs3cjdcydv0z7bj22d7g2kc-config.guess-948ae97.drv",
"/nix/store/nbsdqpfzh1jlpmh95s69b3iivfcvv3lh-config.sub-948ae97.drv",
}
decoder := nixbuild.NewInstantiatedDecoder(strings.NewReader(segmentPrefix+segmentBody+segmentSuffix), os.Stderr)
counter := 3
got := make([]string, 0, counter)
for drv := range decoder.Instantiated() {
got = append(got, drv)
counter--
if counter == 0 {
break
}
}
if !slices.Equal(got, want) {
t.Errorf("Instantiated: %#v, want %#v", got, want)
}
}
func TestInstantiatedEvaluatorBadCommand(t *testing.T) {
wantErr := os.ErrNotExist
breakNixCommand(t)
if _, err := nixbuild.EvalInstantiated(
nixbuild.New(t.Context(), nil, os.Stdout, os.Stderr),
"",
); !errors.Is(err, wantErr) {
t.Errorf("EvalInstantiated: error = %v, want %v", err, wantErr)
}
}
func TestInstantiatedEvaluatorAlreadySet(t *testing.T) {
stubNixCommand(t)
if _, err := nixbuild.EvalInstantiated(
newStubContextCommand(func(cmd *exec.Cmd) { cmd.Stderr = os.Stderr }, t.Context(), nil, os.Stdout, os.Stderr),
"",
); err == nil {
t.Errorf("EvalInstantiated unexpectedly succeeded")
}
}
func TestMalformedInstantiatedError(t *testing.T) {
t.Run("bad type", func(t *testing.T) {
badErr := errors.New("")
if errors.Is(new(nixbuild.MalformedInstantiatedError), badErr) {
t.Error("unexpected MalformedInstantiatedError equivalence")
}
})
t.Run("nil", func(t *testing.T) {
if errors.Is(new(nixbuild.MalformedInstantiatedError), (*nixbuild.MalformedInstantiatedError)(nil)) {
t.Error("unexpected MalformedInstantiatedError equivalence")
}
})
t.Run("unreachable", func(t *testing.T) {
defer func() {
wantPanic := "unreachable"
if r := recover(); r != wantPanic {
t.Errorf("Error: panic = %q, want %q", r, wantPanic)
}
}()
_ = (&nixbuild.MalformedInstantiatedError{Type: -1}).Error()
})
}
const (
segmentPrefix = `evaluating file '/nix/store/vdzlppvrdkz9rv14q4j02g9kpjbww2ww-source/pkgs/development/libraries/zlib/default.nix'
evaluating file '/nix/store/vdzlppvrdkz9rv14q4j02g9kpjbww2ww-source/pkgs/build-support/fetchurl/boot.nix'
performing daemon worker op: 7
instantiated 'zlib-1.3.1.tar.gz' -> '/nix/store/gyks6vvl7x0gq214ldjhi3w4rg37nh8i-zlib-1.3.1.tar.gz.drv'
source path '/nix/store/vdzlppvrdkz9rv14q4j02g9kpjbww2ww-source/pkgs/build-support/setup-hooks/update-autotools-gnu-config-scripts.sh' is uncacheable
copying '/nix/store/vdzlppvrdkz9rv14q4j02g9kpjbww2ww-source/pkgs/build-support/setup-hooks/update-autotools-gnu-config-scripts.sh' to the store...
performing daemon worker op: 7
copied '/nix/store/vdzlppvrdkz9rv14q4j02g9kpjbww2ww-source/pkgs/build-support/setup-hooks/update-autotools-gnu-config-scripts.sh' to '/nix/store/96rvfw5vlv1hwwm9sdxhdkkpjyym6p2x-update-autotools-gnu-config-scripts.sh'
copied source '/nix/store/vdzlppvrdkz9rv14q4j02g9kpjbww2ww-source/pkgs/build-support/setup-hooks/update-autotools-gnu-config-scripts.sh' -> '/nix/store/96rvfw5vlv1hwwm9sdxhdkkpjyym6p2x-update-autotools-gnu-config-scripts.sh'
evaluating file '/nix/store/vdzlppvrdkz9rv14q4j02g9kpjbww2ww-source/pkgs/by-name/gn/gnu-config/package.nix'
performing daemon worker op: 7
instantiated 'config.guess-948ae97' -> '/nix/store/bamwxswxacs3cjdcydv0z7bj22d7g2kc-config.guess-948ae97.drv'
performing daemon worker op: 7
`
segmentBody = `instantiated 'config.sub-948ae97' -> '/nix/store/nbsdqpfzh1jlpmh95s69b3iivfcvv3lh-config.sub-948ae97.drv'`
segmentSuffix = `
performing daemon worker op: 7
instantiated 'gnu-config-2024-01-01' -> '/nix/store/ysp83x9nrks28zkblqmnc1s1kb68dr69-gnu-config-2024-01-01.drv'
performing daemon worker op: 7
instantiated 'bootstrap-stage0-stdenv-linux' -> '/nix/store/3zilrlmq7r6rpzfd94mwss32b62yinj5-bootstrap-stage0-stdenv-linux.drv'
performing daemon worker op: 7
instantiated 'update-autotools-gnu-config-scripts-hook' -> '/nix/store/7yfwy95p6lcdpljdajs5aw10h6q0sfx0-update-autotools-gnu-config-scripts-hook.drv'
source path '/nix/store/vdzlppvrdkz9rv14q4j02g9kpjbww2ww-source/pkgs/build-support/bintools-wrapper/ld-wrapper.sh' is uncacheable
copying '/nix/store/vdzlppvrdkz9rv14q4j02g9kpjbww2ww-source/pkgs/build-support/bintools-wrapper/ld-wrapper.sh' to the store...`
)
func BenchmarkDecodeInstantiated(b *testing.B) {
stderr := strings.NewReader(pluiedevPappardelleOut)
var v []string
for i := 0; i < b.N; i++ {
v, _ = nixbuild.NewInstantiatedDecoder(stderr).Decode()
runtime.KeepAlive(v)
func benchmarkInstantiatedDecoder(b *testing.B, out string) {
decoder := nixbuild.NewInstantiatedDecoder(strings.NewReader(out), nil)
for b.Loop() {
retry:
if !decoder.Scan() {
b.StopTimer()
if err := decoder.Err(); err != nil {
b.Fatalf("Decode: error = %v", err)
}
decoder = nixbuild.NewInstantiatedDecoder(strings.NewReader(out), nil)
b.StartTimer()
goto retry
}
b.StopTimer()
if !strings.HasPrefix(decoder.Text(), "/nix/store") {
b.Fatalf("Text: unexpected prefix: %s", decoder.Text())
}
b.StartTimer()
}
}
func BenchmarkDecodeInstantiatedCopy(b *testing.B) {
stderr := strings.NewReader(getchooAtlasOut)
var v []string
for i := 0; i < b.N; i++ {
v, _ = nixbuild.NewInstantiatedDecoder(stderr).Decode()
runtime.KeepAlive(v)
func BenchmarkInstantiatedDecoder(b *testing.B) {
benchmarkInstantiatedDecoder(b, pluiedevPappardelleOut)
}
func benchmarkInstantiated(b *testing.B, out string, want []string) {
for b.Loop() {
v, _ := nixbuild.NewInstantiatedDecoder(strings.NewReader(out), nil).Decode()
b.StopTimer()
if !slices.Equal(v, want) {
b.Fatalf("Decode: %#v, want %#v", v, want)
}
b.StartTimer()
}
}
func BenchmarkInstantiated(b *testing.B) {
/* 27750 raw, 10729 deduplicated */
benchmarkInstantiated(b, getchooGladosOut, getchooGladosInstantiated)
}
func BenchmarkInstantiatedCopy(b *testing.B) {
/* 40177 raw, 10685 deduplicated */
benchmarkInstantiated(b, pluiedevPappardelleOut, pluiedevPappardelleInstantiated)
}

8
io.go
View File

@ -1,8 +0,0 @@
package nixbuild
import "io"
var (
Stdout io.Writer = nil
Stderr io.Writer = nil
)

View File

@ -0,0 +1,24 @@
package nixbuild_test
import _ "embed"
// github:getchoo/borealis#atlas
var (
//go:embed testdata/getchoo_atlas
getchooAtlasOut string
getchooAtlasInstantiated = sampleSplitPaths(getchooAtlasInstantiatedRaw)
//go:embed testdata/instantiated/getchoo_atlas
getchooAtlasInstantiatedRaw string
//go:embed testdata/derivation/show_getchoo_atlas.json
getchooAtlasShow []byte
getchooAtlasCollective = sampleSplitPaths(getchooAtlasCollectiveRaw)
//go:embed testdata/derivation/collect_getchoo_atlas
getchooAtlasCollectiveRaw string
//go:embed testdata/format/stdin_getchoo_atlas
getchooAtlasStdin string
)

View File

@ -0,0 +1,24 @@
package nixbuild_test
import _ "embed"
// github:getchoo/borealis#glados
var (
//go:embed testdata/getchoo_glados
getchooGladosOut string
getchooGladosInstantiated = sampleSplitPaths(getchooGladosInstantiatedRaw)
//go:embed testdata/instantiated/getchoo_glados
getchooGladosInstantiatedRaw string
//go:embed testdata/derivation/show_getchoo_glados.json
getchooGladosShow []byte
getchooGladosCollective = sampleSplitPaths(getchooGladosCollectiveRaw)
//go:embed testdata/derivation/collect_getchoo_glados
getchooGladosCollectiveRaw string
//go:embed testdata/format/stdin_getchoo_glados
getchooGladosStdin string
)

View File

@ -0,0 +1,26 @@
package nixbuild_test
import (
_ "embed"
)
// git+https://tangled.sh/@pluie.me/flake#pappardelle
var (
//go:embed testdata/pluiedev_pappardelle
pluiedevPappardelleOut string
pluiedevPappardelleInstantiated = sampleSplitPaths(pluiedevPappardelleInstantiatedRaw)
//go:embed testdata/instantiated/pluiedev_pappardelle
pluiedevPappardelleInstantiatedRaw string
//go:embed testdata/derivation/show_pluiedev_pappardelle.json
pluiedevPappardelleShow []byte
pluiedevPappardelleCollective = sampleSplitPaths(pluiedevPappardelleCollectiveRaw)
//go:embed testdata/derivation/collect_pluiedev_pappardelle
pluiedevPappardelleCollectiveRaw string
//go:embed testdata/format/stdin_pluiedev_pappardelle
pluiedevPappardelleStdin string
)

65
sample_test.go Normal file
View File

@ -0,0 +1,65 @@
package nixbuild_test
import "strings"
var instSample = map[string]string{
"bad fields": segmentPrefix + `instantiated 'config.sub-948ae97' ` + segmentSuffix,
"unexpected quotes left": segmentPrefix + `instantiated 'config.sub-948ae97' -> /n'` + segmentSuffix,
"unexpected quotes right": segmentPrefix + `instantiated 'config.sub-948ae97' -> '/n` + segmentSuffix,
"unexpected quotes short": segmentPrefix + `instantiated 'config.sub-948ae97' -> ''` + segmentSuffix,
"not absolute": segmentPrefix + `instantiated 'config.sub-948ae97' -> ' '` + segmentSuffix,
"good segment": segmentPrefix + segmentBody + segmentSuffix,
"getchoo atlas": getchooAtlasOut,
"getchoo glados": getchooGladosOut,
"pluiedev pappardelle": pluiedevPappardelleOut,
}
var instWant = map[string][]string{
"getchoo atlas": getchooAtlasInstantiated,
"getchoo glados": getchooGladosInstantiated,
"pluiedev pappardelle": pluiedevPappardelleInstantiated,
}
var collectWant = map[string][]string{
"getchoo atlas": getchooAtlasCollective,
"getchoo glados": getchooGladosCollective,
"pluiedev pappardelle": pluiedevPappardelleCollective,
}
var drvShow = map[string][]byte{
"getchoo atlas": getchooAtlasShow,
"getchoo glados": getchooGladosShow,
"pluiedev pappardelle": pluiedevPappardelleShow,
}
const (
segmentPrefix = `evaluating file '/nix/store/vdzlppvrdkz9rv14q4j02g9kpjbww2ww-source/pkgs/development/libraries/zlib/default.nix'
evaluating file '/nix/store/vdzlppvrdkz9rv14q4j02g9kpjbww2ww-source/pkgs/build-support/fetchurl/boot.nix'
performing daemon worker op: 7
instantiated 'zlib-1.3.1.tar.gz' -> '/nix/store/gyks6vvl7x0gq214ldjhi3w4rg37nh8i-zlib-1.3.1.tar.gz.drv'
source path '/nix/store/vdzlppvrdkz9rv14q4j02g9kpjbww2ww-source/pkgs/build-support/setup-hooks/update-autotools-gnu-config-scripts.sh' is uncacheable
copying '/nix/store/vdzlppvrdkz9rv14q4j02g9kpjbww2ww-source/pkgs/build-support/setup-hooks/update-autotools-gnu-config-scripts.sh' to the store...
performing daemon worker op: 7
copied '/nix/store/vdzlppvrdkz9rv14q4j02g9kpjbww2ww-source/pkgs/build-support/setup-hooks/update-autotools-gnu-config-scripts.sh' to '/nix/store/96rvfw5vlv1hwwm9sdxhdkkpjyym6p2x-update-autotools-gnu-config-scripts.sh'
copied source '/nix/store/vdzlppvrdkz9rv14q4j02g9kpjbww2ww-source/pkgs/build-support/setup-hooks/update-autotools-gnu-config-scripts.sh' -> '/nix/store/96rvfw5vlv1hwwm9sdxhdkkpjyym6p2x-update-autotools-gnu-config-scripts.sh'
evaluating file '/nix/store/vdzlppvrdkz9rv14q4j02g9kpjbww2ww-source/pkgs/by-name/gn/gnu-config/package.nix'
performing daemon worker op: 7
instantiated 'config.guess-948ae97' -> '/nix/store/bamwxswxacs3cjdcydv0z7bj22d7g2kc-config.guess-948ae97.drv'
performing daemon worker op: 7
`
segmentBody = `instantiated 'config.sub-948ae97' -> '/nix/store/nbsdqpfzh1jlpmh95s69b3iivfcvv3lh-config.sub-948ae97.drv'`
segmentSuffix = `
performing daemon worker op: 7
instantiated 'gnu-config-2024-01-01' -> '/nix/store/ysp83x9nrks28zkblqmnc1s1kb68dr69-gnu-config-2024-01-01.drv'
performing daemon worker op: 7
instantiated 'bootstrap-stage0-stdenv-linux' -> '/nix/store/3zilrlmq7r6rpzfd94mwss32b62yinj5-bootstrap-stage0-stdenv-linux.drv'
performing daemon worker op: 7
instantiated 'update-autotools-gnu-config-scripts-hook' -> '/nix/store/7yfwy95p6lcdpljdajs5aw10h6q0sfx0-update-autotools-gnu-config-scripts-hook.drv'
source path '/nix/store/vdzlppvrdkz9rv14q4j02g9kpjbww2ww-source/pkgs/build-support/bintools-wrapper/ld-wrapper.sh' is uncacheable
copying '/nix/store/vdzlppvrdkz9rv14q4j02g9kpjbww2ww-source/pkgs/build-support/bintools-wrapper/ld-wrapper.sh' to the store...`
)
func sampleSplitPaths(s string) []string { return strings.Split(strings.TrimSpace(s), "\n") }

136
stub_test.go Normal file
View File

@ -0,0 +1,136 @@
package nixbuild_test
import (
"context"
"errors"
"fmt"
"io"
"os"
"os/exec"
"slices"
"strings"
"syscall"
"testing"
"git.gensokyo.uk/yonah/nixbuild"
"hakurei.app/command"
)
const (
runAsNixStub = "TEST_RUN_AS_NIX_STUB"
)
var (
stubExtraArgs = []string{
"-test.run=TestNixStub",
"--",
}
)
// stubNixCommand causes all nix command invocations to invoke the stub for the current test.
func stubNixCommand(t *testing.T) {
n := nixbuild.Nix
nixbuild.Nix = os.Args[0]
t.Cleanup(func() { nixbuild.Nix = n })
cur, ok := os.LookupEnv(runAsNixStub)
if err := os.Setenv(runAsNixStub, "1"); err != nil {
t.Fatalf("cannot setenv: %v", err)
}
t.Cleanup(func() {
if !ok {
if err := os.Unsetenv(runAsNixStub); err != nil {
t.Fatalf("cannot unsetenv: %v", err)
}
return
}
if err := os.Setenv(runAsNixStub, cur); err != nil {
t.Fatalf("cannot setenv: %v", err)
}
})
}
// newStubContext creates a context for use with the nix command stub.
func newStubContext(ctx context.Context, extraArgs []string, stdout, stderr io.Writer) nixbuild.Context {
return nixbuild.New(ctx, append(stubExtraArgs, extraArgs...), stdout, stderr)
}
type stubContextCommand struct {
f func(*exec.Cmd)
nixbuild.Context
}
func (s *stubContextCommand) Nix(ctx context.Context, arg ...string) *exec.Cmd {
cmd := s.Context.Nix(ctx, arg...)
if s.f != nil {
s.f(cmd)
}
return cmd
}
// newStubContext creates a context for use with the nix command stub with a function injected into [nixbuild.Context.Nix].
func newStubContextCommand(f func(*exec.Cmd), ctx context.Context, extraArgs []string, stdout, stderr io.Writer) nixbuild.Context {
return &stubContextCommand{f, newStubContext(ctx, extraArgs, stdout, stderr)}
}
// breakNixCommand makes all nix invocations fail for the current test.
func breakNixCommand(t *testing.T) {
n := nixbuild.Nix
nixbuild.Nix = "/proc/nonexistent"
t.Cleanup(func() { nixbuild.Nix = n })
}
type stubCommandInitFunc func(c command.Command)
var stubCommandInit []stubCommandInitFunc
// this test mocks the nix command
func TestNixStub(t *testing.T) {
if os.Getenv(runAsNixStub) != "1" {
return
}
var (
flagExtraExperimentalFeatures string
)
c := command.New(os.Stderr, t.Logf, "nix", func(args []string) error {
if flagExtraExperimentalFeatures != nixbuild.ExperimentalFeaturesFlakes {
t.Fatalf("%s: %q, want %q",
nixbuild.ExtraExperimentalFeatures, flagExtraExperimentalFeatures, nixbuild.ExperimentalFeaturesFlakes)
return syscall.ENOTRECOVERABLE
}
return nil
}).
Flag(&flagExtraExperimentalFeatures, trimFlagName(nixbuild.ExtraExperimentalFeatures), command.StringFlag(""),
fmt.Sprintf("expects exactly %q", nixbuild.ExperimentalFeaturesFlakes))
c.Command("true", command.UsageInternal, func([]string) error { return nil })
for _, f := range stubCommandInit {
f(c)
}
c.MustParse(os.Args[len(stubExtraArgs)+1:], func(err error) {
if err != nil {
t.Fatal(err.Error())
}
})
}
// checkStdin checks whether entries read from r is equivalent to want.
func checkStdin(r io.Reader, want ...string) error {
if got, err := nixbuild.ReadStdin(r); err != nil {
return err
} else if !slices.Equal(got, want) {
return errors.New(fmt.Sprintf("got build %#v, want %#v", got, want))
}
return nil
}
func trimFlagName(n string) string { return strings.TrimPrefix(n, "--") }
// errorWriter unconditionally returns a non-nil error
type errorWriter struct{}
func (errorWriter) Write([]byte) (int, error) { return 0, syscall.EIO }

15013
testdata/format/stdin_getchoo_atlas vendored Normal file

File diff suppressed because it is too large Load Diff

25109
testdata/format/stdin_getchoo_glados vendored Normal file

File diff suppressed because it is too large Load Diff

24847
testdata/format/stdin_pluiedev_pappardelle vendored Normal file

File diff suppressed because it is too large Load Diff

37432
testdata/getchoo_atlas vendored Normal file

File diff suppressed because it is too large Load Diff

84832
testdata/getchoo_glados vendored Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

108599
testdata/pluiedev_pappardelle vendored Normal file

File diff suppressed because it is too large Load Diff

46
util_test.go Normal file
View File

@ -0,0 +1,46 @@
package nixbuild_test
import (
"encoding/json"
"slices"
"strings"
"testing"
"git.gensokyo.uk/yonah/nixbuild"
)
func TestCollectFromDerivations(t *testing.T) {
testCases := []struct {
name string
}{
{"getchoo atlas"},
{"getchoo glados"},
{"pluiedev pappardelle"},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
var derivations nixbuild.DerivationMap
if err := json.Unmarshal(drvShow[tc.name], &derivations); err != nil {
t.Fatalf("cannot unmarshal test data: %v", err)
}
got := nixbuild.CollectFromDerivations(derivations)
want := collectWant[tc.name]
if !slices.Equal(got, want) {
t.Errorf("CollectFromDerivations:\n%s, want\n%s",
strings.Join(got, "\n"), strings.Join(want, "\n"))
}
})
}
t.Run("edge cases", func(t *testing.T) {
// this exclusively tests edge cases for nil values and buffer growing, so the data doesn't have to make sense
want := []string{"", "big"}
got := nixbuild.CollectFromDerivations(nixbuild.DerivationMap{
"nil": nil,
"big": &nixbuild.Derivation{InputSources: make([]string, 1<<18)},
})
if !slices.Equal(got, want) {
t.Errorf("CollectFromDerivations: %#v, want %#v", got, want)
}
})
}