JOJ3/internal/executor/local/executor_test.go
张泊明518370910136 e5de5b2bd6
Some checks failed
build / trigger-build-image (push) Blocked by required conditions
build / build (push) Has been cancelled
build / build (pull_request) Successful in 2m7s
build / trigger-build-image (pull_request) Has been skipped
test: expand reliability regression coverage
2026-08-02 04:28:24 -07:00

157 lines
4.7 KiB
Go

package local
import (
"bytes"
"context"
"errors"
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
"time"
"github.com/joint-online-judge/JOJ3/internal/stage"
)
func stringPtr(s string) *string { return &s }
func TestRunCapturesIOAndCopyOut(t *testing.T) {
dir := t.TempDir()
output := filepath.Join(dir, "artifact")
results, err := (&Local{}).Run(context.Background(), []stage.Cmd{{
Args: []string{"/bin/sh", "-c", "read value; printf '%s:%s' \"$MARK\" \"$value\"; printf artifact > \"$1\"", "sh", output},
Env: []string{"MARK=env"},
Stdin: &stage.CmdFile{Content: stringPtr("input\n")},
Stdout: &stage.CmdFile{Name: stringPtr("stdout")},
CopyOut: []string{output, filepath.Join(dir, "optional?")},
}})
if err != nil {
t.Fatal(err)
}
if len(results) != 1 || results[0].Status != stage.StatusAccepted {
t.Fatalf("Run() results = %+v", results)
}
if got := results[0].Files["stdout"]; got != "env:input" {
t.Fatalf("stdout = %q", got)
}
if got := results[0].Files[output]; got != "artifact" {
t.Fatalf("copy-out = %q", got)
}
}
func TestGenerateResultClassifiesErrors(t *testing.T) {
result := (&Local{}).generateResult(errors.New("start failed"), nil, -time.Second,
stage.Cmd{}, bytes.Buffer{}, bytes.Buffer{}, false)
if result.Status != stage.StatusInternalError || result.ExitStatus != -1 || result.RunTime != 0 {
t.Fatalf("generateResult() = %+v", result)
}
cmd := exec.Command("/bin/sh", "-c", "exit 7")
err := cmd.Run()
result = (&Local{}).generateResult(err, cmd.ProcessState, time.Millisecond,
stage.Cmd{}, bytes.Buffer{}, bytes.Buffer{}, false)
if result.Status != stage.StatusNonzeroExitStatus || result.ExitStatus != 7 {
t.Fatalf("exit result = %+v", result)
}
}
func TestRunReportsRequiredCopyOutError(t *testing.T) {
missing := filepath.Join(t.TempDir(), "missing")
results, err := (&Local{}).Run(context.Background(), []stage.Cmd{{
Args: []string{"/bin/true"}, CopyOut: []string{missing},
}})
if err != nil {
t.Fatal(err)
}
if len(results) != 1 || results[0].Status != stage.StatusFileError || results[0].Error == "" {
t.Fatalf("Run() results = %+v", results)
}
}
func TestRunReadsStdinFileAndRunsMultipleCommands(t *testing.T) {
input := filepath.Join(t.TempDir(), "stdin")
if err := os.WriteFile(input, []byte("from-file"), 0o600); err != nil {
t.Fatal(err)
}
results, err := (&Local{}).Run(context.Background(), []stage.Cmd{
{Args: []string{"/bin/true"}},
{
Args: []string{"/bin/cat"},
Stdin: &stage.CmdFile{Src: &input},
Stdout: &stage.CmdFile{Name: stringPtr("stdout")},
},
})
if err != nil {
t.Fatal(err)
}
if len(results) != 2 || results[1].Files["stdout"] != "from-file" {
t.Fatalf("Run() results = %+v", results)
}
}
func TestRunSetupErrors(t *testing.T) {
_, err := (&Local{}).Run(context.Background(), []stage.Cmd{{
Args: []string{"/bin/cat"}, Stdin: &stage.CmdFile{Src: stringPtr(filepath.Join(t.TempDir(), "missing"))},
}})
if err == nil || !strings.Contains(err.Error(), "failed to open stdin file") {
t.Fatalf("stdin error = %v", err)
}
_, err = (&Local{}).Run(context.Background(), []stage.Cmd{{Args: []string{filepath.Join(t.TempDir(), "missing")}}})
if err == nil || !strings.Contains(err.Error(), "failed to start command") {
t.Fatalf("start error = %v", err)
}
}
func TestRunClockTimeout(t *testing.T) {
results, err := (&Local{}).Run(context.Background(), []stage.Cmd{{
Args: []string{"/bin/sleep", "1"}, ClockLimit: uint64(20 * time.Millisecond),
}})
if err != nil {
t.Fatal(err)
}
if len(results) != 1 || results[0].Status != stage.StatusTimeLimitExceeded {
t.Fatalf("Run() results = %+v", results)
}
}
func TestRunRejectsEmptyArgs(t *testing.T) {
_, err := (&Local{}).Run(context.Background(), []stage.Cmd{{}})
if err == nil || err.Error() != "command args must not be empty" {
t.Fatalf("Run() error = %v", err)
}
}
func TestRunCancellationKillsProcessGroup(t *testing.T) {
dir := t.TempDir()
ready := filepath.Join(dir, "ready")
marker := filepath.Join(dir, "marker")
ctx, cancel := context.WithCancel(context.Background())
done := make(chan error, 1)
go func() {
_, err := (&Local{}).Run(ctx, []stage.Cmd{{
Args: []string{"/bin/sh", "-c", "touch \"$1\"; (sleep 0.3; touch \"$2\") & wait", "sh", ready, marker},
}})
done <- err
}()
deadline := time.Now().Add(2 * time.Second)
for {
if _, err := os.Stat(ready); err == nil {
break
}
if time.Now().After(deadline) {
t.Fatal("command did not start")
}
time.Sleep(10 * time.Millisecond)
}
cancel()
if err := <-done; !errors.Is(err, context.Canceled) {
t.Fatalf("Run() error = %v, want context canceled", err)
}
time.Sleep(500 * time.Millisecond)
if _, err := os.Stat(marker); !errors.Is(err, os.ErrNotExist) {
t.Fatalf("child process survived cancellation: %v", err)
}
}