157 lines
4.7 KiB
Go
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)
|
|
}
|
|
}
|