package xfer
import (
"os"
"os/exec"
"path/filepath"
"testing"
"time"
)
// The store child's stderr must be an *os.File, and the forge must own
// the reader.
//
// This is a structural assertion on purpose. The forge log gained the
// child's diagnostics by assigning a plain io.Writer to cmd.Stderr.
// os/exec connects an *os.File to the child directly, but copies
// anything else on a goroutine that Wait() blocks on, in
// awaitGoroutines, until every write end of that pipe is closed.
// Process.Kill() closes the direct child's; a grandchild holding the
// inherited descriptor keeps its own, and Wait() never returns.
//
// That is not a slow test, it is a stopped forge. Close() and
// Unregister() hold f.mu across the wait, ensure() and ServeHTTP take
// the same mutex, so one removed repository wedges sync for every
// repository. It hung two CI runs at the ten-minute timeout, and the
// goroutine dump named awaitGoroutines under Fronter.Close.
//
// Asserted structurally rather than by racing a grandchild: a
// synthetic child that leaves one holding stderr did NOT reproduce the
// stall reliably here, and a guard that passes while the bug is
// present is worse than none. The type of Stderr is the invariant that
// actually prevents it, it is what regressed, and it cannot pass by
// luck.
func TestTheChildStderrIsAFileNotACopiedWriter(t *testing.T) {
if _, err := exec.LookPath("zit"); err != nil {
t.Skip("zit not on PATH")
}
dir := t.TempDir()
zrun(t, dir, "init")
f := New()
defer f.Close()
f.Register("alpha", dir)
b, err := f.ensure("alpha")
if err != nil {
t.Fatal(err)
}
if _, ok := b.cmd.Stderr.(*os.File); !ok {
t.Fatalf("FAIL: cmd.Stderr is %T, not *os.File — os/exec will copy it on a "+
"goroutine and Wait() will block in awaitGoroutines until every write "+
"end is closed, which a grandchild never does", b.cmd.Stderr)
}
if b.errR == nil {
t.Error("FAIL: the forge does not hold the read end, so stop() cannot end the reading")
}
}
// And stopping releases both, promptly.
func TestStoppingTheChildReleasesItAndItsReader(t *testing.T) {
if _, err := exec.LookPath("zit"); err != nil {
t.Skip("zit not on PATH")
}
dir := t.TempDir()
zrun(t, dir, "init")
f := New()
defer f.Close()
f.Register("alpha", dir)
b, err := f.ensure("alpha")
if err != nil {
t.Fatal(err)
}
// The store is real, so the child has something to say and the
// reader has something to read before it is closed under it.
_ = filepath.Join(dir, ".zit")
done := make(chan struct{})
go func() { b.stop(); close(done) }()
select {
case <-done:
case <-time.After(30 * time.Second):
t.Fatal("FAIL: stop() did not return — the child's stderr is being waited on")
}
if b.cmd != nil {
t.Error("FAIL: stop() left the command in place")
}
if b.errR != nil {
t.Error("FAIL: stop() left the stderr reader open")
}
}