JOJ3/internal/executor/sandbox/executor_test.go
张泊明518370910136 79a64efbfd
Some checks failed
build / build (push) Successful in 1m22s
build / trigger-build-image (push) Has been skipped
build / build (pull_request) Failing after 44s
build / trigger-build-image (pull_request) Has been skipped
test: cover stage and executor reliability regressions
2026-08-02 01:51:04 -07:00

130 lines
3.3 KiB
Go

package sandbox
import (
"context"
"errors"
"math"
"testing"
"time"
"github.com/criyle/go-judge/pb"
"github.com/joint-online-judge/JOJ3/internal/stage"
"google.golang.org/grpc"
"google.golang.org/protobuf/types/known/emptypb"
)
type fakeExecutorClient struct {
pb.ExecutorClient
exec func(context.Context, *pb.Request) (*pb.Response, error)
fileDelete func(context.Context, *pb.FileID) (*emptypb.Empty, error)
}
func (f fakeExecutorClient) Exec(
ctx context.Context, req *pb.Request, _ ...grpc.CallOption,
) (*pb.Response, error) {
return f.exec(ctx, req)
}
func (f fakeExecutorClient) FileDelete(
ctx context.Context, id *pb.FileID, _ ...grpc.CallOption,
) (*emptypb.Empty, error) {
return f.fileDelete(ctx, id)
}
func TestExecRPCTimeout(t *testing.T) {
tests := []struct {
name string
cmds []stage.Cmd
want time.Duration
}{
{name: "margin only", want: rpcTimeoutMargin},
{
name: "largest clock limit",
cmds: []stage.Cmd{{ClockLimit: uint64(time.Minute)}, {ClockLimit: uint64(2 * time.Minute)}},
want: 2*time.Minute + rpcTimeoutMargin,
},
{
name: "cpu limit fallback",
cmds: []stage.Cmd{{CPULimit: uint64(time.Minute)}},
want: 2*time.Minute + rpcTimeoutMargin,
},
{
name: "clock limit takes precedence over cpu limit",
cmds: []stage.Cmd{{ClockLimit: uint64(time.Minute), CPULimit: uint64(10 * time.Minute)}},
want: time.Minute + rpcTimeoutMargin,
},
{
name: "cpu multiplication overflow",
cmds: []stage.Cmd{{CPULimit: math.MaxUint64}},
want: time.Duration(math.MaxInt64),
},
{
name: "duration overflow",
cmds: []stage.Cmd{{ClockLimit: math.MaxUint64}},
want: time.Duration(math.MaxInt64),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := execRPCTimeout(tt.cmds); got != tt.want {
t.Fatalf("execRPCTimeout() = %v, want %v", got, tt.want)
}
})
}
}
func TestRunAppliesComputedRPCDeadline(t *testing.T) {
var remaining time.Duration
client := fakeExecutorClient{
exec: func(ctx context.Context, _ *pb.Request) (*pb.Response, error) {
deadline, ok := ctx.Deadline()
if !ok {
t.Fatal("Exec context has no deadline")
}
remaining = time.Until(deadline)
return &pb.Response{}, nil
},
}
executor := &Sandbox{
execClient: client,
cachedMap: make(map[string]string),
}
want := 2*time.Minute + rpcTimeoutMargin
_, err := executor.Run(context.Background(), []stage.Cmd{{
Args: []string{"true"},
CPULimit: uint64(time.Minute),
}})
if err != nil {
t.Fatal(err)
}
if remaining > want || remaining < want-time.Second {
t.Fatalf("RPC deadline remaining = %v, want approximately %v", remaining, want)
}
}
func TestCleanupJoinsDeleteErrorsAndClearsCache(t *testing.T) {
deleteErr := errors.New("delete failed")
deleted := 0
client := fakeExecutorClient{
fileDelete: func(context.Context, *pb.FileID) (*emptypb.Empty, error) {
deleted++
return nil, deleteErr
},
}
executor := &Sandbox{
execClient: client,
cachedMap: map[string]string{"one": "1", "two": "2"},
}
err := executor.Cleanup(context.Background())
if !errors.Is(err, deleteErr) {
t.Fatalf("Cleanup() error = %v, want delete error", err)
}
if deleted != 2 {
t.Fatalf("FileDelete called %d times, want 2", deleted)
}
if len(executor.cachedMap) != 0 {
t.Fatalf("cached files not cleared: %v", executor.cachedMap)
}
}