diff --git a/apps/iris/src/service/build/unit.go b/apps/iris/src/service/build/unit.go index eedf3a9363..46df9afd8f 100644 --- a/apps/iris/src/service/build/unit.go +++ b/apps/iris/src/service/build/unit.go @@ -76,8 +76,8 @@ func (bu *BuildUnit) Setup( Dir: bu.Dir, Language: bu.ParsedLang, }) - // compileErr: sandbox service itself failed (e.g., timeout, internal error) - // compileResult.ExecResult.ErrorCode != 0: sandbox ran but user code has errors + // compileErr: compiler could not be started or the sandbox service failed + // compileResult.ExecResult.StatusCode != RUN_SUCCESS: compiler exited unsuccessfully or timed out if compileErr != nil { return &BuildUnitError{ Unit: bu.Name, diff --git a/apps/iris/src/service/build/unit_test.go b/apps/iris/src/service/build/unit_test.go index 3d991b381a..b22d4756a1 100644 --- a/apps/iris/src/service/build/unit_test.go +++ b/apps/iris/src/service/build/unit_test.go @@ -1,9 +1,12 @@ package build import ( + "errors" + "path/filepath" "testing" "time" + "github.com/skkuding/codedang/apps/iris/src/service/file" "github.com/skkuding/codedang/apps/iris/src/service/sandbox" "github.com/skkuding/codedang/apps/iris/src/service/sandbox/judger" "github.com/stretchr/testify/assert" @@ -98,3 +101,57 @@ func TestBuildUnitRunSerializesSameUnit(t *testing.T) { <-done assert.False(t, concurrent, "same BuildUnit entered the sandbox concurrently") } + +type compilingSandbox struct { + *blockingSandbox + baseDir string + result sandbox.CompileResult + err error +} + +func (s *compilingSandbox) MakeSrcPath(dir string, _ sandbox.Language) (string, error) { + return filepath.Join(s.baseDir, dir, "main.c"), nil +} + +func (s *compilingSandbox) Compile(sandbox.CompileRequest) (sandbox.CompileResult, error) { + return s.result, s.err +} + +func TestBuildUnitSetupClassifiesCompileFailure(t *testing.T) { + for _, tc := range []struct { + name string + result sandbox.CompileResult + err error + isUserError bool + }{ + { + name: "compiler rejected source", + result: sandbox.CompileResult{ + ExecResult: sandbox.ExecResult{StatusCode: sandbox.COMPILE_ERROR}, + ErrOutput: "syntax error", + }, + isUserError: true, + }, + { + name: "compiler could not start", + err: errors.New("compiler execution failed"), + }, + } { + t.Run(tc.name, func(t *testing.T) { + baseDir := t.TempDir() + unit := &BuildUnit{Code: "invalid source", Language: string(sandbox.CPP)} + fake := &compilingSandbox{ + blockingSandbox: newBlockingSandbox(), + baseDir: baseDir, + result: tc.result, + err: tc.err, + } + + buildErr := unit.Setup(0, 1, file.NewFileManager(baseDir), fake) + + require.Error(t, buildErr) + assert.Equal(t, "compile", buildErr.Phase) + assert.Equal(t, tc.isUserError, buildErr.IsUserError) + }) + } +} diff --git a/apps/iris/src/service/sandbox/judger/compiler.go b/apps/iris/src/service/sandbox/judger/compiler.go index 2567dfb7f7..2992e2120a 100644 --- a/apps/iris/src/service/sandbox/judger/compiler.go +++ b/apps/iris/src/service/sandbox/judger/compiler.go @@ -1,10 +1,9 @@ package judger import ( - "bytes" - "fmt" - "context" + "errors" + "fmt" "os" "os/exec" "time" @@ -40,16 +39,6 @@ func (c *compiler) Compile(dto sandbox.CompileRequest) (sandbox.CompileResult, e return sandbox.CompileResult{}, err } - if execResult.StatusCode == sandbox.StatusCode(SYSTEM_ERROR) { - c.logger.Log(logger.ERROR, fmt.Sprintf("Compile failed: %+v", execResult)) - data, err := c.file.ReadFile(constants.COMPILE_LOG_PATH) - if err != nil { - return sandbox.CompileResult{}, fmt.Errorf("failed to read output file: %w", err) - } - c.logger.Log(logger.ERROR, fmt.Sprintf("Compile Log: %s", string(data))) - return sandbox.CompileResult{}, fmt.Errorf("system error: %v", execResult) - } - compileResult := sandbox.CompileResult{} compileResult.ExecResult = execResult @@ -71,7 +60,7 @@ func (c *compiler) compileExec(args ExecArgs) (sandbox.ExecResult, error) { outputFile, err := os.Create(args.OutputPath) if err != nil { return sandbox.ExecResult{ - StatusCode: sandbox.StatusCode(SYSTEM_ERROR), + StatusCode: sandbox.SERVER_ERROR, }, err } defer outputFile.Close() @@ -81,9 +70,8 @@ func (c *compiler) compileExec(args ExecArgs) (sandbox.ExecResult, error) { cmd := exec.CommandContext(ctx, args.ExePath, args.Args...) cmd.Env = append(cmd.Env, env) - - var stderr bytes.Buffer - cmd.Stderr = &stderr + cmd.Stdout = outputFile + cmd.Stderr = outputFile startTime := time.Now() err = cmd.Run() @@ -95,9 +83,16 @@ func (c *compiler) compileExec(args ExecArgs) (sandbox.ExecResult, error) { } if err != nil { + var exitErr *exec.ExitError + if errors.As(err, &exitErr) && exitErr.ExitCode() >= 0 { + return sandbox.ExecResult{ + ExitCode: exitErr.ExitCode(), + StatusCode: sandbox.COMPILE_ERROR, + }, nil + } return sandbox.ExecResult{ - StatusCode: sandbox.StatusCode(SYSTEM_ERROR), - }, fmt.Errorf("%s", stderr.String()) + StatusCode: sandbox.SERVER_ERROR, + }, fmt.Errorf("compiler execution failed: %w", err) } realTimeSpentMS := int(time.Since(startTime).Milliseconds()) diff --git a/apps/iris/src/service/sandbox/judger/compiler_test.go b/apps/iris/src/service/sandbox/judger/compiler_test.go new file mode 100644 index 0000000000..697244826c --- /dev/null +++ b/apps/iris/src/service/sandbox/judger/compiler_test.go @@ -0,0 +1,75 @@ +package judger + +import ( + "os" + "path/filepath" + "testing" + + "github.com/skkuding/codedang/apps/iris/src/common/constants" + "github.com/skkuding/codedang/apps/iris/src/service/file" + "github.com/skkuding/codedang/apps/iris/src/service/sandbox" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +type fixedCompileArgs struct { + sandbox.LangConfig[JudgerConfig, ExecArgs] + args ExecArgs +} + +func (f fixedCompileArgs) ToCompileExecArgs(string, sandbox.Language) (ExecArgs, error) { + return f.args, nil +} + +func TestCompileReturnsCompilerFailureAndDiagnostic(t *testing.T) { + baseDir := t.TempDir() + dir := "source" + require.NoError(t, os.Mkdir(filepath.Join(baseDir, dir), 0700)) + c := &compiler{ + langConfig: fixedCompileArgs{args: ExecArgs{ + ExePath: "/bin/sh", + Args: []string{"-c", "printf 'syntax error\\n' >&2; exit 1"}, + OutputPath: filepath.Join(baseDir, dir, constants.COMPILE_OUT_FILE), + MaxRealTime: 1000, + }}, + file: file.NewFileManager(baseDir), + } + + result, err := c.Compile(sandbox.CompileRequest{Dir: dir, Language: sandbox.CPP}) + + require.NoError(t, err) + assert.Equal(t, sandbox.COMPILE_ERROR, result.ExecResult.StatusCode) + assert.Equal(t, 1, result.ExecResult.ExitCode) + assert.Equal(t, "syntax error\n", result.ErrOutput) +} + +func TestCompileExecClassifiesCompilerExit(t *testing.T) { + outputPath := filepath.Join(t.TempDir(), "compile.out") + c := &compiler{} + + result, err := c.compileExec(ExecArgs{ + ExePath: "/bin/sh", + Args: []string{"-c", "printf 'syntax error\\n' >&2; exit 1"}, + OutputPath: outputPath, + MaxRealTime: 1000, + }) + + require.NoError(t, err) + assert.Equal(t, sandbox.COMPILE_ERROR, result.StatusCode) + assert.Equal(t, 1, result.ExitCode) + output, err := os.ReadFile(outputPath) + require.NoError(t, err) + assert.Equal(t, "syntax error\n", string(output)) +} + +func TestCompileExecReportsStartFailure(t *testing.T) { + c := &compiler{} + result, err := c.compileExec(ExecArgs{ + ExePath: filepath.Join(t.TempDir(), "missing-compiler"), + OutputPath: filepath.Join(t.TempDir(), "compile.out"), + MaxRealTime: 1000, + }) + + require.Error(t, err) + assert.Equal(t, sandbox.SERVER_ERROR, result.StatusCode) +}