//go:build windows package compose import ( "context" "errors" "os" "os/exec" "path/filepath" "testing" "time" "golang.org/x/sys/windows" ) func TestOwnedProcessUsesCreationTimeJobAttribute(t *testing.T) { if procThreadAttributeJobList == 0 { t.Fatal("owned Windows process must carry a creation-time job-list attribute") } cmd := exec.Command("cmd.exe") configureOwnedProcess(cmd) if cmd.SysProcAttr != nil && cmd.SysProcAttr.CreationFlags&windows.CREATE_SUSPENDED != 0 { t.Fatal("owned Windows process must not rely on a post-start assignment race") } } func TestOwnedJobPolicyKillsDescendantsOnClose(t *testing.T) { if windows.JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE == 0 { t.Fatal("owned job must use kill-on-close policy") } } // TestWindowsProcessHelper is both a test helper and the child process used by the runtime // ownership regression below. The child writes a marker only if it outlives its parent job. func TestWindowsProcessHelper(t *testing.T) { mode := os.Getenv("THOTHCTL_WINDOWS_HELPER") if mode == "" { return } marker := os.Getenv("THOTHCTL_WINDOWS_MARKER") if mode == "child" { time.Sleep(2 * time.Second) _ = os.WriteFile(marker, []byte("survived"), 0o600) return } child := exec.Command(os.Args[0], "-test.run=TestWindowsProcessHelper", "--") child.Env = append(os.Environ(), "THOTHCTL_WINDOWS_HELPER=child", "THOTHCTL_WINDOWS_MARKER="+marker) if err := child.Start(); err != nil { t.Fatal(err) } if mode == "exit" { return } if mode == "cancel" { for { time.Sleep(time.Second) } } for { _, _ = os.Stdout.Write([]byte("overflow\n")) time.Sleep(time.Millisecond) } } func TestOwnedProcessTerminatesDescendantOnOverflowCancelAndParentExit(t *testing.T) { for _, mode := range []string{"overflow", "cancel", "exit"} { t.Run(mode, func(t *testing.T) { marker := filepath.Join(t.TempDir(), "descendant-marker") t.Setenv("THOTHCTL_WINDOWS_HELPER", mode) t.Setenv("THOTHCTL_WINDOWS_MARKER", marker) ctx := context.Background() var cancel context.CancelFunc if mode == "cancel" { ctx, cancel = context.WithCancel(ctx) } result := make(chan error, 1) go func() { _, err := runBoundedWindows(ctx, os.Args[0], []string{"-test.run=TestWindowsProcessHelper", "--"}, nil, CaptureLimits{StdoutBytes: 1024, StderrBytes: 1024}) result <- err }() if mode == "cancel" { time.Sleep(100 * time.Millisecond) cancel() } select { case err := <-result: if mode == "overflow" && !errors.Is(err, ErrOutputLimit) { t.Fatalf("overflow error = %v, want ErrOutputLimit", err) } if mode == "cancel" && !errors.Is(err, context.Canceled) { t.Fatalf("cancel error = %v, want context.Canceled", err) } case <-time.After(5 * time.Second): t.Fatal("owned process did not terminate") } time.Sleep(150 * time.Millisecond) if _, err := os.Stat(marker); !errors.Is(err, os.ErrNotExist) { t.Fatalf("descendant marker error = %v; descendant survived %s termination", err, mode) } }) } }