Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ and uses [semantic versioning](https://semver.org/).
- **One Redis subscription per gateway** — SSE clients share a single pattern subscription instead of one subscription each. A client that falls behind is closed so it reconnects and replays from the event log; no event is dropped silently.
- **Honest `Retry-After`** — when the queue is full, `429` carries the time the queue needs to fall under the admission limit at the measured completion rate (1 to 60 s), not a fixed 10.
- **Leaner JVM** — `java` and `kotlin` start with the serial collector and shared class data, which also lowers the thread count of each run.
- **Rust edition 2021** — `rust` compiles with `--edition=2021` instead of rustc's default 2015, so `try_into`, `TryFrom` and `FromIterator` work without imports. `compiler_flags: ["--edition=2018"]` still works: it replaces the default rather than adding a second `--edition`, which rustc rejects.

## [0.1.0] — 2026-10-04

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -500,7 +500,7 @@ Cancels an active or queued submission. Marks the submission record as `failed`
| `javascript` | Node.js 24 LTS | No | 2.0 s | 512 MB |
| `sqlite` | SQLite 3.45 (`sqlite3 -batch`) | No | 2.0 s | 512 MB |
| `bash` | Bash | No | 2.0 s | 256 MB |
| `rust` | rustc (`-O`) | Yes | 2.0 s | 512 MB |
| `rust` | rustc 1.85 (`-O --edition=2021`) | Yes | 2.0 s | 512 MB |
| `csharp` | Mono (`mcs`, `mono`) | Yes | 2.0 s | 512 MB (Min 128 MB) |
| `php` | PHP CLI | No | 2.0 s | 512 MB |
| `ruby` | Ruby | No | 2.0 s | 512 MB |
Expand Down
73 changes: 51 additions & 22 deletions internal/sandbox/language.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
package sandbox

import (
"slices"
"strings"
)

// StdoutMax and StderrMax are the per-test output caps sent to go-judge.
// They default to 8 MiB and 1 MiB respectively and can be overridden at
// startup via AC_SANDBOX_STDOUT_MAX_BYTES / AC_SANDBOX_STDERR_MAX_BYTES.
Expand Down Expand Up @@ -63,12 +68,17 @@ func ClockLimit(cpu uint64) uint64 {
type LangConfig struct {
Compiled bool
CompileArgs []string
CompileEnv []string
CompileCPU uint64 // nanoseconds
CompileMem uint64 // bytes
CompileProc uint64
SourcePath string
BinaryPath string // output path for compiled languages
// CompileDefaults are flags appended after CompileArgs unless a caller
// flag has the same name (the text before '='), which then replaces it.
// rustc rejects an option given twice, so a caller's --edition=2018 must
// replace the default edition, not follow it.
CompileDefaults []string
CompileEnv []string
CompileCPU uint64 // nanoseconds
CompileMem uint64 // bytes
CompileProc uint64
SourcePath string
BinaryPath string // output path for compiled languages

RunArgs []string
RunEnv []string
Expand Down Expand Up @@ -217,17 +227,20 @@ var Languages = map[string]LangConfig{
"rust": {
Compiled: true,
CompileArgs: []string{"/usr/bin/rustc", "-O", "-o", "/w/a.out", "/w/main.rs"},
CompileEnv: []string{"PATH=/usr/bin:/bin", "HOME=/tmp"},
CompileCPU: 20_000_000_000,
CompileMem: 536_870_912,
CompileProc: 50,
SourcePath: "/w/main.rs",
BinaryPath: "/w/a.out",
RunArgs: []string{"/w/a.out"},
RunEnv: []string{"PATH=/usr/bin:/bin"},
RunCPU: 2_000_000_000,
RunMem: 536_870_912,
RunProc: 10,
// Without --edition rustc compiles as edition 2015, where TryFrom,
// TryInto and FromIterator are not in the prelude.
CompileDefaults: []string{"--edition=2021"},
CompileEnv: []string{"PATH=/usr/bin:/bin", "HOME=/tmp"},
CompileCPU: 20_000_000_000,
CompileMem: 536_870_912,
CompileProc: 50,
SourcePath: "/w/main.rs",
BinaryPath: "/w/a.out",
RunArgs: []string{"/w/a.out"},
RunEnv: []string{"PATH=/usr/bin:/bin"},
RunCPU: 2_000_000_000,
RunMem: 536_870_912,
RunProc: 10,
},
"typescript": {
Compiled: false,
Expand Down Expand Up @@ -265,11 +278,7 @@ func BuildCompileCmd(lang LangConfig, sourceCode string) Cmd {

// BuildCompileCmdWithOptions builds the go-judge Cmd for compilation supporting extra files and compiler flags.
func BuildCompileCmdWithOptions(lang LangConfig, sourceCode string, extraFiles map[string]string, compilerFlags []string) Cmd {
args := make([]string, 0, len(lang.CompileArgs)+len(compilerFlags))
args = append(args, lang.CompileArgs...)
if len(compilerFlags) > 0 {
args = append(args, compilerFlags...)
}
args := compileArgs(lang, compilerFlags)

copyIn := make(map[string]CopyIn, 1+len(extraFiles))
copyIn[lang.SourcePath] = FileContent(sourceCode)
Expand All @@ -293,6 +302,26 @@ func BuildCompileCmdWithOptions(lang LangConfig, sourceCode string, extraFiles m
}
}

// compileArgs returns CompileArgs, then each CompileDefaults flag the caller
// did not override, then the caller's flags.
func compileArgs(lang LangConfig, compilerFlags []string) []string {
args := make([]string, 0, len(lang.CompileArgs)+len(lang.CompileDefaults)+len(compilerFlags))
args = append(args, lang.CompileArgs...)
for _, def := range lang.CompileDefaults {
name := flagName(def)
if !slices.ContainsFunc(compilerFlags, func(f string) bool { return flagName(f) == name }) {
args = append(args, def)
}
}
return append(args, compilerFlags...)
}

// flagName is a flag without its "=value" part: "--edition=2021" → "--edition".
func flagName(flag string) string {
name, _, _ := strings.Cut(flag, "=")
return name
}

// BuildCacheSourceCmd builds a no-op go-judge Cmd that uploads interpreted source
// into the file cache. The returned fileID can then be used in BuildRunCmd to avoid
// sending the full source text in every test Cmd of a batch.
Expand Down
59 changes: 58 additions & 1 deletion internal/sandbox/language_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package sandbox

import "testing"
import (
"slices"
"testing"
)

func TestEffectiveDefaultsWithoutLimits(t *testing.T) {
lang := Languages["cpp"]
Expand Down Expand Up @@ -30,3 +33,57 @@ func TestBuildRunCmdSetsLimitsAndClock(t *testing.T) {
t.Fatalf("cpu=%d mem=%d clock=%d", cmd.CPULimit, cmd.MemoryLimit, cmd.ClockLimit)
}
}

func TestBuildCompileCmdRustDefaults(t *testing.T) {
rustc := []string{"/usr/bin/rustc", "-O", "-o", "/w/a.out", "/w/main.rs"}
tests := []struct {
name string
flags []string
want []string
}{
{"no flags compile as edition 2021", nil, append(slices.Clone(rustc), "--edition=2021")},
{"caller edition replaces the default", []string{"--edition=2018"}, append(slices.Clone(rustc), "--edition=2018")},
{"caller 2021 is not repeated", []string{"--edition=2021"}, append(slices.Clone(rustc), "--edition=2021")},
// rustc accepts -O twice, so a caller -O keeps the default edition.
{"other flags keep the default", []string{"-O"}, append(slices.Clone(rustc), "--edition=2021", "-O")},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := BuildCompileCmdWithOptions(Languages["rust"], "fn main(){}", nil, tt.flags).Args
if !slices.Equal(got, tt.want) {
t.Fatalf("args = %q, want %q", got, tt.want)
}
})
}
}

// A language without CompileDefaults gets its args and the caller's flags, as before.
func TestBuildCompileCmdWithoutDefaults(t *testing.T) {
cpp := Languages["cpp"]
got := BuildCompileCmdWithOptions(cpp, "int main(){}", nil, []string{"-O3"}).Args
want := append(slices.Clone(cpp.CompileArgs), "-O3")
if !slices.Equal(got, want) {
t.Fatalf("args = %q, want %q", got, want)
}
}

// No allowed caller flag may repeat a default: rustc fails with "Option
// 'edition' given more than once" when --edition appears twice.
func TestBuildCompileCmdNeverRepeatsAllowedFlag(t *testing.T) {
for language, allowed := range AllowedCompilerFlags {
lang, ok := Languages[language]
if !ok || len(lang.CompileDefaults) == 0 {
continue
}
for flag := range allowed {
args := BuildCompileCmdWithOptions(lang, "", nil, []string{flag}).Args
seen := map[string]bool{}
for _, arg := range args[len(lang.CompileArgs):] {
if seen[flagName(arg)] {
t.Errorf("%s with %q: %q repeats %s", language, flag, args, flagName(arg))
}
seen[flagName(arg)] = true
}
}
}
}
141 changes: 141 additions & 0 deletions internal/worker/rust_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
package worker

import (
"context"
"encoding/json"
"log/slog"
"net/http"
"net/http/httptest"
"slices"
"strings"
"sync"
"testing"
"time"

"github.com/stjosephsplacements/AetherCode-Execution-Engine/internal/model"
"github.com/stjosephsplacements/AetherCode-Execution-Engine/internal/sandbox"
)

const rustSolution = `use std::io::{self, Read};
fn main() {
let mut s = String::new();
io::stdin().read_to_string(&mut s).unwrap();
let n: i64 = s.trim().parse().unwrap();
let small: u8 = n.try_into().unwrap_or(255);
println!("{}", small);
}
`

// rustJudge is a fake go-judge for Rust jobs. Unit tests never run real
// submissions, so it answers the compile request with compileResult (adding
// a cached binary ID when the compile is accepted) and every run Cmd with
// stdout "42". It records every request it receives.
func rustJudge(t *testing.T, compileResult sandbox.Result) (*Worker, func() []sandbox.Request) {
t.Helper()
var mu sync.Mutex
var reqs []sandbox.Request
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var req sandbox.Request
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
mu.Lock()
reqs = append(reqs, req)
mu.Unlock()
res := make([]sandbox.Result, len(req.Cmd))
for i, cmd := range req.Cmd {
if slices.Contains(cmd.CopyOutCached, "/w/a.out") {
res[i] = compileResult
if compileResult.Status == sandbox.StatusAccepted {
res[i].FileIDs = map[string]string{"/w/a.out": "rust-bin"}
}
continue
}
res[i] = sandbox.Result{Status: sandbox.StatusAccepted, Files: map[string]string{"stdout": "42\n"}}
}
_ = json.NewEncoder(w).Encode(res)
}))
t.Cleanup(srv.Close)

w := New(nil, sandbox.NewClient(srv.URL, "dummy"), nil, nil, 10*time.Second, 1, 5*time.Minute)
return w, func() []sandbox.Request {
mu.Lock()
defer mu.Unlock()
return append([]sandbox.Request(nil), reqs...)
}
}

// A Rust job compiles main.rs as edition 2021 and runs the cached binary for
// every test.
func TestRustCompilesAsEdition2021AndRunsTests(t *testing.T) {
w, reqs := rustJudge(t, sandbox.Result{Status: sandbox.StatusAccepted})
lang := sandbox.Languages["rust"]

compiled, err := w.compileWithOptions(context.Background(), lang, rustSolution, nil, nil)
if err != nil {
t.Fatalf("compile: %v", err)
}
if compiled.Status != sandbox.StatusAccepted || compiled.FileIDs["/w/a.out"] != "rust-bin" {
t.Fatalf("compile result = %+v, want accepted with the cached binary", compiled)
}

tests := []testData{{Input: "42", ExpectedOutput: "42"}, {Input: "42", ExpectedOutput: "42"}}
_, _, _, passed, verdict, err := w.runTestsBatched(context.Background(), "job-rust", lang, rustSolution, tests, "rust-bin", sandbox.RunLimits{}, nil, slog.Default())
if err != nil {
t.Fatalf("runTestsBatched: %v", err)
}
if passed != 2 || verdict != model.VerdictAccepted {
t.Fatalf("passed=%d verdict=%v, want 2 Accepted", passed, verdict)
}

got := reqs()
if len(got) != 2 {
t.Fatalf("want a compile request and one test batch, got %d requests", len(got))
}
compile := got[0].Cmd[0]
if compile.Args[0] != "/usr/bin/rustc" || !slices.Contains(compile.Args, "--edition=2021") {
t.Errorf("compile args = %q, want rustc with --edition=2021", compile.Args)
}
if src := compile.CopyIn["/w/main.rs"].Content; src == nil || *src != rustSolution {
t.Errorf("compile did not copy the source to /w/main.rs")
}
for i, cmd := range got[1].Cmd {
if !slices.Equal(cmd.Args, lang.RunArgs) {
t.Errorf("test %d args = %q, want %q", i, cmd.Args, lang.RunArgs)
}
if id := cmd.CopyIn["/w/a.out"].FileID; id == nil || *id != "rust-bin" {
t.Errorf("test %d does not run the compiled binary", i)
}
}
}

// A caller's --edition replaces the default instead of following it: rustc
// fails with "Option 'edition' given more than once" otherwise.
func TestRustCallerEditionReplacesDefault(t *testing.T) {
w, reqs := rustJudge(t, sandbox.Result{Status: sandbox.StatusAccepted})

if _, err := w.compileWithOptions(context.Background(), sandbox.Languages["rust"], rustSolution, nil, []string{"--edition=2018"}); err != nil {
t.Fatalf("compile: %v", err)
}
args := reqs()[0].Cmd[0].Args
editions := slices.DeleteFunc(slices.Clone(args), func(a string) bool { return !strings.HasPrefix(a, "--edition") })
if !slices.Equal(editions, []string{"--edition=2018"}) {
t.Fatalf("compile args = %q, want exactly one --edition=2018", args)
}
}

// A Rust compile failure comes back as a non-accepted result carrying
// rustc's diagnostics, which process turns into a Compilation Error verdict.
func TestRustCompileErrorKeepsDiagnostics(t *testing.T) {
stderr := "error[E0308]: mismatched types\n"
w, _ := rustJudge(t, sandbox.Result{Status: sandbox.StatusNonzeroExit, ExitStatus: 1, Files: map[string]string{"stderr": stderr}})

res, err := w.compileWithOptions(context.Background(), sandbox.Languages["rust"], `fn main() { let x: i32 = "nope"; }`, nil, nil)
if err != nil {
t.Fatalf("compile: %v", err)
}
if res.Status == sandbox.StatusAccepted || res.Files["stderr"] != stderr {
t.Fatalf("compile result = %+v, want a failure with rustc's stderr", res)
}
}
Loading