Skip to content

Your first task

This walks through a realistic request: compile a C program once, then run it on two inputs in parallel. It assumes Faber is running as in the quick start with API_KEY set.

A program that reads two integers and prints their sum:

#include <stdio.h>
int main(void) {
int a, b;
if (scanf("%d %d", &a, &b) != 2) return 1;
printf("%d\n", a + b);
return 0;
}

Save this as request.json:

{
"stop_on_failure": true,
"steps": [
{
"cmd": "gcc",
"args": ["-O2", "-o", "main", "main.c"],
"files": {
"main.c": "#include <stdio.h>\n\nint main(void) {\n int a, b;\n if (scanf(\"%d %d\", &a, &b) != 2) return 1;\n printf(\"%d\\n\", a + b);\n return 0;\n}\n"
}
},
[
{ "cmd": "./main", "stdin": "1 2\n", "sandbox_profile": "native_v2" },
{ "cmd": "./main", "stdin": "40 2\n", "sandbox_profile": "native_v2" }
]
]
}

and send it:

Terminal window
curl -s http://localhost:3000/api/v1/execute \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
--data @request.json | jq

What each part does:

  • steps: two steps that run in order. The first is a single task (an object); the second is a parallel step (an array) whose tasks run at the same time.
  • cmd and args: the program and its arguments. gcc has no /, so it is looked up on PATH inside the sandbox. ./main is relative to the working directory.
  • files: files to create before the task starts, keyed by path relative to the workspace /faber. Values here are plain strings (UTF-8). You can also send base64 content, mark a file executable, or reference an uploaded file; see the file store.
  • stdin: written to the task’s standard input, which is then closed.
  • sandbox_profile: the seccomp policy. The compiler runs under the default (compile_v2, which allows starting processes). The compiled program runs under native_v2, which also forbids creating threads, processes and sockets. See choosing sandbox profiles.
  • stop_on_failure: if the compile step fails, the test step is reported as skipped instead of running against a missing binary.

All tasks of the request share the workspace, so the test tasks find the main binary the compiler wrote. Every task starts in /faber unless it sets working_dir.

{
"steps": [
{
"stdout": "",
"stderr": "",
"exit_code": 0,
"cached": false,
"stats": { "outcome": "exited", "execution_time_ms": 211, "memory_peak_bytes": 41943040, "...": "..." }
},
[
{
"stdout": "3\n",
"stderr": "",
"exit_code": 0,
"cached": false,
"stats": { "outcome": "exited", "execution_time_ms": 3, "...": "..." }
},
{
"stdout": "42\n",
"stderr": "",
"exit_code": 0,
"cached": false,
"stats": { "outcome": "exited", "execution_time_ms": 2, "...": "..." }
}
]
],
"cache": { "enabled": false, "hits": 0, "stored": 0 }
}

Each step result has the shape of its step: an object for the compile task, an array in the same order for the parallel step. To judge a test, compare stdout with the expected output and check that exit_code is 0 and stats.outcome is exited.

Change a + b to a + c and send the request again. The compiler fails:

{
"steps": [
{
"stdout": "",
"stderr": "main.c: In function 'main':\nmain.c:6:24: error: 'c' undeclared ...",
"exit_code": 1,
"cached": false,
"stats": { "outcome": "exited", "...": "..." }
},
[
{
"error": "Skipped because an earlier step failed",
"cached": false,
"stats": { "outcome": "not_started", "...": "..." }
},
{
"error": "Skipped because an earlier step failed",
"cached": false,
"stats": { "outcome": "not_started", "...": "..." }
}
]
],
"cache": { "enabled": false, "hits": 0, "stored": 0 }
}

The HTTP status is still 200. A task that fails, times out, runs out of memory or makes a forbidden syscall is reported in its result; HTTP errors are reserved for requests Faber could not accept or could not run at all (see errors).

Skipped tasks have an error instead of stdout/stderr/exit_code. The execution model lists every outcome.

  • Compile once and reuse the binary across requests with the step cache.
  • Run many test cases within the limits of one request.
  • Use an SDK instead of hand-written JSON.