nixbuild/stub_test.go
Yonah 69c6128ff5
exec: replace global state with interface
This is cleaner, and finally enables writing tests for the nix invoking functions.
2025-07-18 13:40:46 +09:00

137 lines
3.4 KiB
Go

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 }