docker-evaluator 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- docker_evaluator-0.1.0/PKG-INFO +125 -0
- docker_evaluator-0.1.0/README.md +112 -0
- docker_evaluator-0.1.0/docker_evaluator/__init__.py +6 -0
- docker_evaluator-0.1.0/docker_evaluator/disk_helper.py +41 -0
- docker_evaluator-0.1.0/docker_evaluator/docker_helper.py +50 -0
- docker_evaluator-0.1.0/docker_evaluator/env_helper.py +8 -0
- docker_evaluator-0.1.0/docker_evaluator/evaluator.py +49 -0
- docker_evaluator-0.1.0/docker_evaluator/language_helpers/__init__.py +0 -0
- docker_evaluator-0.1.0/docker_evaluator/language_helpers/c_helper/Dockerfile +7 -0
- docker_evaluator-0.1.0/docker_evaluator/language_helpers/c_helper/__init__.py +0 -0
- docker_evaluator-0.1.0/docker_evaluator/language_helpers/c_helper/c_helper.py +8 -0
- docker_evaluator-0.1.0/docker_evaluator/language_helpers/c_helper/entrypoint.sh +56 -0
- docker_evaluator-0.1.0/docker_evaluator/language_helpers/cpp_helper/Dockerfile +7 -0
- docker_evaluator-0.1.0/docker_evaluator/language_helpers/cpp_helper/__init__.py +0 -0
- docker_evaluator-0.1.0/docker_evaluator/language_helpers/cpp_helper/cpp_helper.py +8 -0
- docker_evaluator-0.1.0/docker_evaluator/language_helpers/cpp_helper/entrypoint.sh +56 -0
- docker_evaluator-0.1.0/docker_evaluator/language_helpers/language_helper.py +83 -0
- docker_evaluator-0.1.0/docker_evaluator/language_helpers/py2_helper/Dockerfile +7 -0
- docker_evaluator-0.1.0/docker_evaluator/language_helpers/py2_helper/__init__.py +0 -0
- docker_evaluator-0.1.0/docker_evaluator/language_helpers/py2_helper/entrypoint.sh +39 -0
- docker_evaluator-0.1.0/docker_evaluator/language_helpers/py2_helper/py2_helper.py +8 -0
- docker_evaluator-0.1.0/docker_evaluator/language_helpers/py3_helper/Dockerfile +7 -0
- docker_evaluator-0.1.0/docker_evaluator/language_helpers/py3_helper/__init__.py +0 -0
- docker_evaluator-0.1.0/docker_evaluator/language_helpers/py3_helper/entrypoint.sh +39 -0
- docker_evaluator-0.1.0/docker_evaluator/language_helpers/py3_helper/py3_helper.py +8 -0
- docker_evaluator-0.1.0/docker_evaluator.egg-info/PKG-INFO +125 -0
- docker_evaluator-0.1.0/docker_evaluator.egg-info/SOURCES.txt +35 -0
- docker_evaluator-0.1.0/docker_evaluator.egg-info/dependency_links.txt +1 -0
- docker_evaluator-0.1.0/docker_evaluator.egg-info/requires.txt +7 -0
- docker_evaluator-0.1.0/docker_evaluator.egg-info/top_level.txt +1 -0
- docker_evaluator-0.1.0/pyproject.toml +49 -0
- docker_evaluator-0.1.0/setup.cfg +4 -0
- docker_evaluator-0.1.0/setup.py +3 -0
- docker_evaluator-0.1.0/tests/test_disk_helper.py +97 -0
- docker_evaluator-0.1.0/tests/test_docker_helper.py +115 -0
- docker_evaluator-0.1.0/tests/test_evaluator.py +125 -0
- docker_evaluator-0.1.0/tests/test_language_helper.py +183 -0
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: docker-evaluator
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Generic code evaluation in isolated Docker containers
|
|
5
|
+
Requires-Python: >=3.9
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: docker>=7.1.0
|
|
8
|
+
Requires-Dist: python-dotenv>=1.0.1
|
|
9
|
+
Provides-Extra: dev
|
|
10
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
11
|
+
Requires-Dist: pytest-mock>=3.0; extra == "dev"
|
|
12
|
+
Requires-Dist: ruff>=0.4.0; extra == "dev"
|
|
13
|
+
|
|
14
|
+
# docker-evaluator
|
|
15
|
+
|
|
16
|
+
A code evaluation backend for competitive programming judges. Runs untrusted submissions in isolated Docker containers, enforces time and memory limits, and checks output against expected results — similar to how Codeforces/CodeChef judge submissions.
|
|
17
|
+
|
|
18
|
+
Supports Python 2/3, C, and C++.
|
|
19
|
+
|
|
20
|
+
## Requirements
|
|
21
|
+
|
|
22
|
+
- Docker (running and accessible to the current user)
|
|
23
|
+
- Python 3.9+
|
|
24
|
+
|
|
25
|
+
## Installation
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
pip install docker-evaluator
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Usage
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
from docker_evaluator import DockerEvaluator
|
|
35
|
+
|
|
36
|
+
evaluator = DockerEvaluator()
|
|
37
|
+
|
|
38
|
+
result = evaluator.evaluate(
|
|
39
|
+
code='n = int(input()); print(n * 2)',
|
|
40
|
+
input="21",
|
|
41
|
+
expected_output="42",
|
|
42
|
+
language="py3",
|
|
43
|
+
time_limit=1,
|
|
44
|
+
memory_limit=256 * 1024, # 256 MB in KB
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
print(result)
|
|
48
|
+
# {'correct': True, 'details': 'OK (8ms)'}
|
|
49
|
+
|
|
50
|
+
evaluator.close()
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Parameters
|
|
54
|
+
|
|
55
|
+
| Parameter | Type | Default | Description |
|
|
56
|
+
|---|---|---|---|
|
|
57
|
+
| `code` | `str` | — | Submission source code |
|
|
58
|
+
| `input` | `str` | — | Problem input (stdin or file content) |
|
|
59
|
+
| `expected_output` | `str` | — | Correct output to compare against |
|
|
60
|
+
| `language` | `str` | — | `"py3"`, `"py2"`, `"c"`, or `"cpp"` |
|
|
61
|
+
| `time_limit` | `int` | — | Time limit in seconds |
|
|
62
|
+
| `input_type` | `str` | `"stdin"` | `"stdin"` or `"file"` |
|
|
63
|
+
| `file_io_name` | `str` | `""` | File name when using file I/O (e.g. `"input.txt"`) |
|
|
64
|
+
| `memory_limit` | `int` | `1024` | Memory limit in KB (minimum enforced: 256 MB) |
|
|
65
|
+
|
|
66
|
+
### Return value
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
{"correct": bool, "details": str}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
`details` is one of:
|
|
73
|
+
|
|
74
|
+
- `OK (Xms)` — accepted
|
|
75
|
+
- `Wrong Answer`
|
|
76
|
+
- `Time Limit Exceeded`
|
|
77
|
+
- `Memory Limit Exceeded`
|
|
78
|
+
- `Runtime Error (exit code N)`
|
|
79
|
+
- `Compilation Error`
|
|
80
|
+
|
|
81
|
+
## Supported languages
|
|
82
|
+
|
|
83
|
+
| Key | Language |
|
|
84
|
+
|---|---|
|
|
85
|
+
| `py3` | Python 3 |
|
|
86
|
+
| `py2` | Python 2 |
|
|
87
|
+
| `c` | C |
|
|
88
|
+
| `cpp` | C++ |
|
|
89
|
+
|
|
90
|
+
Docker images are built automatically on first use and cached for subsequent runs.
|
|
91
|
+
|
|
92
|
+
## File I/O
|
|
93
|
+
|
|
94
|
+
For problems that read/write files instead of stdin/stdout:
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
result = evaluator.evaluate(
|
|
98
|
+
code=open("solution.cpp").read(),
|
|
99
|
+
input="5\n1 2 3 4 5",
|
|
100
|
+
expected_output="15",
|
|
101
|
+
language="cpp",
|
|
102
|
+
time_limit=2,
|
|
103
|
+
input_type="file",
|
|
104
|
+
file_io_name="input.txt",
|
|
105
|
+
)
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Configuration
|
|
109
|
+
|
|
110
|
+
Create a `.env` file in your working directory:
|
|
111
|
+
|
|
112
|
+
| Variable | Default | Description |
|
|
113
|
+
|---|---|---|
|
|
114
|
+
| `KEEP_EVAL_CONTAINERS` | `0` | Set to `1` to keep containers after a run (useful for debugging) |
|
|
115
|
+
| `ENVIRONMENT` | — | If set, also loads `.env.<ENVIRONMENT>` |
|
|
116
|
+
|
|
117
|
+
## Compilation cache
|
|
118
|
+
|
|
119
|
+
C and C++ submissions are cached by source hash — repeated evaluations of the same code skip recompilation. To clear the cache:
|
|
120
|
+
|
|
121
|
+
```python
|
|
122
|
+
from docker_evaluator import clear_cache
|
|
123
|
+
|
|
124
|
+
clear_cache()
|
|
125
|
+
```
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# docker-evaluator
|
|
2
|
+
|
|
3
|
+
A code evaluation backend for competitive programming judges. Runs untrusted submissions in isolated Docker containers, enforces time and memory limits, and checks output against expected results — similar to how Codeforces/CodeChef judge submissions.
|
|
4
|
+
|
|
5
|
+
Supports Python 2/3, C, and C++.
|
|
6
|
+
|
|
7
|
+
## Requirements
|
|
8
|
+
|
|
9
|
+
- Docker (running and accessible to the current user)
|
|
10
|
+
- Python 3.9+
|
|
11
|
+
|
|
12
|
+
## Installation
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
pip install docker-evaluator
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Usage
|
|
19
|
+
|
|
20
|
+
```python
|
|
21
|
+
from docker_evaluator import DockerEvaluator
|
|
22
|
+
|
|
23
|
+
evaluator = DockerEvaluator()
|
|
24
|
+
|
|
25
|
+
result = evaluator.evaluate(
|
|
26
|
+
code='n = int(input()); print(n * 2)',
|
|
27
|
+
input="21",
|
|
28
|
+
expected_output="42",
|
|
29
|
+
language="py3",
|
|
30
|
+
time_limit=1,
|
|
31
|
+
memory_limit=256 * 1024, # 256 MB in KB
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
print(result)
|
|
35
|
+
# {'correct': True, 'details': 'OK (8ms)'}
|
|
36
|
+
|
|
37
|
+
evaluator.close()
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### Parameters
|
|
41
|
+
|
|
42
|
+
| Parameter | Type | Default | Description |
|
|
43
|
+
|---|---|---|---|
|
|
44
|
+
| `code` | `str` | — | Submission source code |
|
|
45
|
+
| `input` | `str` | — | Problem input (stdin or file content) |
|
|
46
|
+
| `expected_output` | `str` | — | Correct output to compare against |
|
|
47
|
+
| `language` | `str` | — | `"py3"`, `"py2"`, `"c"`, or `"cpp"` |
|
|
48
|
+
| `time_limit` | `int` | — | Time limit in seconds |
|
|
49
|
+
| `input_type` | `str` | `"stdin"` | `"stdin"` or `"file"` |
|
|
50
|
+
| `file_io_name` | `str` | `""` | File name when using file I/O (e.g. `"input.txt"`) |
|
|
51
|
+
| `memory_limit` | `int` | `1024` | Memory limit in KB (minimum enforced: 256 MB) |
|
|
52
|
+
|
|
53
|
+
### Return value
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
{"correct": bool, "details": str}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
`details` is one of:
|
|
60
|
+
|
|
61
|
+
- `OK (Xms)` — accepted
|
|
62
|
+
- `Wrong Answer`
|
|
63
|
+
- `Time Limit Exceeded`
|
|
64
|
+
- `Memory Limit Exceeded`
|
|
65
|
+
- `Runtime Error (exit code N)`
|
|
66
|
+
- `Compilation Error`
|
|
67
|
+
|
|
68
|
+
## Supported languages
|
|
69
|
+
|
|
70
|
+
| Key | Language |
|
|
71
|
+
|---|---|
|
|
72
|
+
| `py3` | Python 3 |
|
|
73
|
+
| `py2` | Python 2 |
|
|
74
|
+
| `c` | C |
|
|
75
|
+
| `cpp` | C++ |
|
|
76
|
+
|
|
77
|
+
Docker images are built automatically on first use and cached for subsequent runs.
|
|
78
|
+
|
|
79
|
+
## File I/O
|
|
80
|
+
|
|
81
|
+
For problems that read/write files instead of stdin/stdout:
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
result = evaluator.evaluate(
|
|
85
|
+
code=open("solution.cpp").read(),
|
|
86
|
+
input="5\n1 2 3 4 5",
|
|
87
|
+
expected_output="15",
|
|
88
|
+
language="cpp",
|
|
89
|
+
time_limit=2,
|
|
90
|
+
input_type="file",
|
|
91
|
+
file_io_name="input.txt",
|
|
92
|
+
)
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Configuration
|
|
96
|
+
|
|
97
|
+
Create a `.env` file in your working directory:
|
|
98
|
+
|
|
99
|
+
| Variable | Default | Description |
|
|
100
|
+
|---|---|---|
|
|
101
|
+
| `KEEP_EVAL_CONTAINERS` | `0` | Set to `1` to keep containers after a run (useful for debugging) |
|
|
102
|
+
| `ENVIRONMENT` | — | If set, also loads `.env.<ENVIRONMENT>` |
|
|
103
|
+
|
|
104
|
+
## Compilation cache
|
|
105
|
+
|
|
106
|
+
C and C++ submissions are cached by source hash — repeated evaluations of the same code skip recompilation. To clear the cache:
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
from docker_evaluator import clear_cache
|
|
110
|
+
|
|
111
|
+
clear_cache()
|
|
112
|
+
```
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
from docker_evaluator.disk_helper import clear_cache
|
|
2
|
+
from docker_evaluator.docker_helper import DockerHelper
|
|
3
|
+
from docker_evaluator.env_helper import load_env_variables
|
|
4
|
+
from docker_evaluator.evaluator import DockerEvaluator
|
|
5
|
+
|
|
6
|
+
__all__ = ["DockerEvaluator", "DockerHelper", "clear_cache", "load_env_variables"]
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
import os
|
|
3
|
+
import shutil
|
|
4
|
+
import tempfile
|
|
5
|
+
import threading
|
|
6
|
+
|
|
7
|
+
_compile_locks = {}
|
|
8
|
+
_compile_locks_mutex = threading.Lock()
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def get_compile_lock(cache_dir):
|
|
12
|
+
with _compile_locks_mutex:
|
|
13
|
+
if cache_dir not in _compile_locks:
|
|
14
|
+
_compile_locks[cache_dir] = threading.Lock()
|
|
15
|
+
return _compile_locks[cache_dir]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
_CACHE_BASE = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "compilation_cache"))
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def get_cache_dir(code, language):
|
|
22
|
+
code_hash = hashlib.sha256(code.encode()).hexdigest()
|
|
23
|
+
cache_dir = os.path.join(_CACHE_BASE, language, code_hash)
|
|
24
|
+
os.makedirs(cache_dir, exist_ok=True)
|
|
25
|
+
return cache_dir
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def clear_cache():
|
|
29
|
+
if os.path.exists(_CACHE_BASE):
|
|
30
|
+
shutil.rmtree(_CACHE_BASE)
|
|
31
|
+
print("Compilation cache cleared.")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def get_temp_dir(files):
|
|
35
|
+
temp_dir = tempfile.mkdtemp()
|
|
36
|
+
test_data_dir = os.path.join(temp_dir, "test_data")
|
|
37
|
+
os.makedirs(test_data_dir, exist_ok=True)
|
|
38
|
+
for file in files:
|
|
39
|
+
with open(os.path.join(test_data_dir, file["name"]), "w", encoding="utf-8", errors="replace") as file_writer:
|
|
40
|
+
file_writer.write(file["content"])
|
|
41
|
+
return test_data_dir
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
import docker
|
|
4
|
+
from docker.errors import ContainerError
|
|
5
|
+
from docker.types import LogConfig
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class DockerHelper:
|
|
9
|
+
def __init__(self):
|
|
10
|
+
self.client = docker.from_env()
|
|
11
|
+
|
|
12
|
+
def image_exists(self, image_tag):
|
|
13
|
+
images = self.client.images.list()
|
|
14
|
+
image_names = []
|
|
15
|
+
for image in images:
|
|
16
|
+
image_names.extend(image.tags)
|
|
17
|
+
return image_tag in image_names
|
|
18
|
+
|
|
19
|
+
def create_image(self, image_path, image_tag):
|
|
20
|
+
self.client.images.build(path=image_path, tag=image_tag)
|
|
21
|
+
|
|
22
|
+
def evaluate(self, image_name, volume, environment_variables, cpus=1, memory_limit_mb=None, cache_dir=None):
|
|
23
|
+
volumes = {volume: {"bind": "/test_data", "mode": "ro"}}
|
|
24
|
+
if cache_dir:
|
|
25
|
+
volumes[cache_dir] = {"bind": "/cache", "mode": "rw"}
|
|
26
|
+
# Read env at call time because env files may be loaded after helper init.
|
|
27
|
+
keep_eval_containers = os.getenv("KEEP_EVAL_CONTAINERS", "0") == "1"
|
|
28
|
+
try:
|
|
29
|
+
logs = self.client.containers.run(
|
|
30
|
+
image_name,
|
|
31
|
+
volumes=volumes,
|
|
32
|
+
detach=False,
|
|
33
|
+
environment=environment_variables,
|
|
34
|
+
remove=not keep_eval_containers,
|
|
35
|
+
log_config=LogConfig(type="json-file"),
|
|
36
|
+
nano_cpus=int(cpus * 1e9),
|
|
37
|
+
mem_limit=f"{memory_limit_mb}m" if memory_limit_mb else None,
|
|
38
|
+
memswap_limit=f"{memory_limit_mb}m" if memory_limit_mb else None,
|
|
39
|
+
network_disabled=True,
|
|
40
|
+
pids_limit=64,
|
|
41
|
+
)
|
|
42
|
+
except ContainerError as e:
|
|
43
|
+
if e.exit_status == 137:
|
|
44
|
+
return "Memory Limit Exceeded"
|
|
45
|
+
return f"Runtime Error (exit code {e.exit_status})"
|
|
46
|
+
output = logs.decode("utf-8", errors="replace").rstrip()
|
|
47
|
+
return output
|
|
48
|
+
|
|
49
|
+
def close(self):
|
|
50
|
+
self.client.close()
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
from docker_evaluator.docker_helper import DockerHelper
|
|
2
|
+
from docker_evaluator.env_helper import load_env_variables
|
|
3
|
+
from docker_evaluator.language_helpers.c_helper.c_helper import CHelper
|
|
4
|
+
from docker_evaluator.language_helpers.cpp_helper.cpp_helper import CppHelper
|
|
5
|
+
from docker_evaluator.language_helpers.py2_helper.py2_helper import Py2Helper
|
|
6
|
+
from docker_evaluator.language_helpers.py3_helper.py3_helper import Py3Helper
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class DockerEvaluator:
|
|
10
|
+
def __init__(self, docker_client=None):
|
|
11
|
+
load_env_variables()
|
|
12
|
+
self.docker_helper = docker_client
|
|
13
|
+
if docker_client is None:
|
|
14
|
+
self.docker_helper = DockerHelper()
|
|
15
|
+
|
|
16
|
+
self.language_helpers = [
|
|
17
|
+
CHelper(self.docker_helper),
|
|
18
|
+
CppHelper(self.docker_helper),
|
|
19
|
+
Py2Helper(self.docker_helper),
|
|
20
|
+
Py3Helper(self.docker_helper),
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
def evaluate(
|
|
24
|
+
self, code, input, expected_output, language, time_limit, input_type="stdin", file_io_name="", memory_limit=1024
|
|
25
|
+
):
|
|
26
|
+
# Enforce minimum memory limit of 256MB to avoid spurious segfaults on valid code.
|
|
27
|
+
# memory_limit is in KB, so 262144 KB = 256 MB.
|
|
28
|
+
MIN_MEMORY_KB = 256 * 1024
|
|
29
|
+
memory_limit = max(memory_limit, MIN_MEMORY_KB)
|
|
30
|
+
|
|
31
|
+
for language_helper in self.language_helpers:
|
|
32
|
+
if language_helper.language == language:
|
|
33
|
+
output = language_helper.evaluate(
|
|
34
|
+
code, input, time_limit, input_type, file_io_name, memory_limit=memory_limit
|
|
35
|
+
)
|
|
36
|
+
# Extract container-side timing appended by entrypoint as last line
|
|
37
|
+
time_str = None
|
|
38
|
+
lines = output.split("\n")
|
|
39
|
+
if lines and lines[-1].startswith("__TIME__:"):
|
|
40
|
+
time_str = lines[-1][len("__TIME__:") :]
|
|
41
|
+
output = "\n".join(lines[:-1]).rstrip()
|
|
42
|
+
if "Limit Exceeded" in output or "Compilation Error" in output or "Runtime Error" in output:
|
|
43
|
+
return {"correct": False, "details": output}
|
|
44
|
+
elif output.split() != expected_output.split():
|
|
45
|
+
return {"correct": False, "details": "Wrong Answer"}
|
|
46
|
+
return {"correct": True, "details": f"OK ({time_str})" if time_str else "OK (time unavailable)"}
|
|
47
|
+
|
|
48
|
+
def close(self):
|
|
49
|
+
self.docker_helper.close()
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
if [ -f /cache/main ]; then
|
|
3
|
+
cp /cache/main ./main
|
|
4
|
+
else
|
|
5
|
+
compile_output=$(gcc -std=c99 -O2 -o ./main /test_data/target.c 2>&1)
|
|
6
|
+
if [ $? -ne 0 ]; then
|
|
7
|
+
echo "Compilation Error: $compile_output"
|
|
8
|
+
exit 0
|
|
9
|
+
fi
|
|
10
|
+
cp ./main /cache/main 2>/dev/null || true
|
|
11
|
+
fi
|
|
12
|
+
|
|
13
|
+
cp /test_data/target.in ./target.in
|
|
14
|
+
|
|
15
|
+
start_ms=$(date +%s%3N)
|
|
16
|
+
if [ "$INPUT_TYPE" = "file" ]; then
|
|
17
|
+
cp ./target.in ./${FILE_IO_NAME}.in
|
|
18
|
+
if [ -n "$MEMORY_LIMIT_KB" ] && [ "$MEMORY_LIMIT_KB" -gt 0 ] 2>/dev/null; then
|
|
19
|
+
timeout -k 1 ${TIME_LIMIT} sh -c "ulimit -v \"$MEMORY_LIMIT_KB\" && exec ./main"
|
|
20
|
+
else
|
|
21
|
+
timeout -k 1 ${TIME_LIMIT} ./main
|
|
22
|
+
fi
|
|
23
|
+
exit_code=$?
|
|
24
|
+
else
|
|
25
|
+
if [ -n "$MEMORY_LIMIT_KB" ] && [ "$MEMORY_LIMIT_KB" -gt 0 ] 2>/dev/null; then
|
|
26
|
+
timeout -k 1 ${TIME_LIMIT} sh -c "ulimit -v \"$MEMORY_LIMIT_KB\" && exec ./main" < ./target.in > ./result.out
|
|
27
|
+
else
|
|
28
|
+
timeout -k 1 ${TIME_LIMIT} ./main < ./target.in > ./result.out
|
|
29
|
+
fi
|
|
30
|
+
exit_code=$?
|
|
31
|
+
fi
|
|
32
|
+
end_ms=$(date +%s%3N)
|
|
33
|
+
elapsed_ms=$((end_ms - start_ms))
|
|
34
|
+
|
|
35
|
+
if [ $exit_code -eq 124 ]; then
|
|
36
|
+
echo "Time Limit Exceeded"
|
|
37
|
+
exit 0
|
|
38
|
+
fi
|
|
39
|
+
|
|
40
|
+
if [ $exit_code -eq 137 ]; then
|
|
41
|
+
echo "Memory Limit Exceeded"
|
|
42
|
+
exit 0
|
|
43
|
+
fi
|
|
44
|
+
|
|
45
|
+
if [ $exit_code -ne 0 ]; then
|
|
46
|
+
echo "Runtime Error (exit code $exit_code)"
|
|
47
|
+
exit 0
|
|
48
|
+
fi
|
|
49
|
+
|
|
50
|
+
if [ "$INPUT_TYPE" = "file" ]; then
|
|
51
|
+
cat ./${FILE_IO_NAME}.out 2>/dev/null
|
|
52
|
+
else
|
|
53
|
+
cat ./result.out
|
|
54
|
+
fi
|
|
55
|
+
printf '\n__TIME__:%sms\n' "${elapsed_ms}"
|
|
56
|
+
exit 0
|
|
File without changes
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
from docker_evaluator.language_helpers.language_helper import LanguageHelper
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class CppHelper(LanguageHelper):
|
|
7
|
+
def __init__(self, docker_helper):
|
|
8
|
+
super().__init__(docker_helper, os.path.dirname(__file__), "cpp", "cpp", 1, cache_compilation=True)
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
if [ -f /cache/main ]; then
|
|
3
|
+
cp /cache/main ./main
|
|
4
|
+
else
|
|
5
|
+
compile_output=$(g++ -std=c++20 -O2 -o ./main /test_data/target.cpp 2>&1)
|
|
6
|
+
if [ $? -ne 0 ]; then
|
|
7
|
+
echo "Compilation Error: $compile_output"
|
|
8
|
+
exit 0
|
|
9
|
+
fi
|
|
10
|
+
cp ./main /cache/main 2>/dev/null || true
|
|
11
|
+
fi
|
|
12
|
+
|
|
13
|
+
cp /test_data/target.in ./target.in
|
|
14
|
+
|
|
15
|
+
start_ms=$(date +%s%3N)
|
|
16
|
+
if [ "$INPUT_TYPE" = "file" ]; then
|
|
17
|
+
cp ./target.in ./${FILE_IO_NAME}.in
|
|
18
|
+
if [ -n "$MEMORY_LIMIT_KB" ] && [ "$MEMORY_LIMIT_KB" -gt 0 ] 2>/dev/null; then
|
|
19
|
+
timeout -k 1 ${TIME_LIMIT} sh -c "ulimit -v \"$MEMORY_LIMIT_KB\" && exec ./main"
|
|
20
|
+
else
|
|
21
|
+
timeout -k 1 ${TIME_LIMIT} ./main
|
|
22
|
+
fi
|
|
23
|
+
exit_code=$?
|
|
24
|
+
else
|
|
25
|
+
if [ -n "$MEMORY_LIMIT_KB" ] && [ "$MEMORY_LIMIT_KB" -gt 0 ] 2>/dev/null; then
|
|
26
|
+
timeout -k 1 ${TIME_LIMIT} sh -c "ulimit -v \"$MEMORY_LIMIT_KB\" && exec ./main" < ./target.in > ./result.out
|
|
27
|
+
else
|
|
28
|
+
timeout -k 1 ${TIME_LIMIT} ./main < ./target.in > ./result.out
|
|
29
|
+
fi
|
|
30
|
+
exit_code=$?
|
|
31
|
+
fi
|
|
32
|
+
end_ms=$(date +%s%3N)
|
|
33
|
+
elapsed_ms=$((end_ms - start_ms))
|
|
34
|
+
|
|
35
|
+
if [ $exit_code -eq 124 ]; then
|
|
36
|
+
echo "Time Limit Exceeded"
|
|
37
|
+
exit 0
|
|
38
|
+
fi
|
|
39
|
+
|
|
40
|
+
if [ $exit_code -eq 137 ]; then
|
|
41
|
+
echo "Memory Limit Exceeded"
|
|
42
|
+
exit 0
|
|
43
|
+
fi
|
|
44
|
+
|
|
45
|
+
if [ $exit_code -ne 0 ]; then
|
|
46
|
+
echo "Runtime Error (exit code $exit_code)"
|
|
47
|
+
exit 0
|
|
48
|
+
fi
|
|
49
|
+
|
|
50
|
+
if [ "$INPUT_TYPE" = "file" ]; then
|
|
51
|
+
cat ./${FILE_IO_NAME}.out 2>/dev/null
|
|
52
|
+
else
|
|
53
|
+
cat ./result.out
|
|
54
|
+
fi
|
|
55
|
+
printf '\n__TIME__:%sms\n' "${elapsed_ms}"
|
|
56
|
+
exit 0
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
from docker_evaluator.disk_helper import get_cache_dir, get_compile_lock, get_temp_dir
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class LanguageHelper:
|
|
7
|
+
def __init__(
|
|
8
|
+
self,
|
|
9
|
+
docker_helper,
|
|
10
|
+
docker_context_path,
|
|
11
|
+
language,
|
|
12
|
+
file_extension,
|
|
13
|
+
language_time_limit_multiplier,
|
|
14
|
+
memory_overhead_mb=32,
|
|
15
|
+
cache_compilation=False,
|
|
16
|
+
):
|
|
17
|
+
docker_image_name = f"docker-evaluator-{language}"
|
|
18
|
+
self.docker_helper = docker_helper
|
|
19
|
+
self.docker_image_name = docker_image_name
|
|
20
|
+
self.language = language
|
|
21
|
+
self.language_time_limit_multiplier = language_time_limit_multiplier
|
|
22
|
+
self.memory_overhead_mb = memory_overhead_mb
|
|
23
|
+
self.cache_compilation = cache_compilation
|
|
24
|
+
self.file_extension = file_extension
|
|
25
|
+
self.initialize(docker_context_path, docker_image_name)
|
|
26
|
+
|
|
27
|
+
def initialize(self, docker_context_path, docker_image_name):
|
|
28
|
+
docker_image_tag = f"{docker_image_name}:latest"
|
|
29
|
+
if not self.docker_helper.image_exists(docker_image_tag):
|
|
30
|
+
print(f"Image {docker_image_tag} not found, building...")
|
|
31
|
+
self.docker_helper.create_image(docker_context_path, docker_image_tag)
|
|
32
|
+
print(f"Image {docker_image_tag} built successfully.")
|
|
33
|
+
|
|
34
|
+
def evaluate(self, code, code_input, time_limit, input_type="stdin", file_io_name="", memory_limit=1024):
|
|
35
|
+
files = [
|
|
36
|
+
{"name": f"target.{self.file_extension}", "content": code},
|
|
37
|
+
{"name": "target.in", "content": code_input},
|
|
38
|
+
]
|
|
39
|
+
# Add grace to compensate for Docker/WSL2 scheduling jitter on Windows.
|
|
40
|
+
# timeout measures wall clock, not CPU time, so the process may get less
|
|
41
|
+
# than a full CPU-second per wall-second under load.
|
|
42
|
+
GRACE_S = 0.2
|
|
43
|
+
effective_time_limit = time_limit * self.language_time_limit_multiplier + GRACE_S
|
|
44
|
+
environment_variables = {
|
|
45
|
+
"TIME_LIMIT": effective_time_limit,
|
|
46
|
+
"LANG": "C.UTF-8",
|
|
47
|
+
"LC_ALL": "C.UTF-8",
|
|
48
|
+
"INPUT_TYPE": input_type,
|
|
49
|
+
"FILE_IO_NAME": file_io_name or "",
|
|
50
|
+
"MEMORY_LIMIT_KB": str(memory_limit),
|
|
51
|
+
}
|
|
52
|
+
memory_limit_mb = memory_limit // 1024
|
|
53
|
+
total_memory_mb = memory_limit_mb + self.memory_overhead_mb
|
|
54
|
+
# C/C++ compilation can briefly use much more memory than runtime.
|
|
55
|
+
# Keep a minimum container budget for compile-heavy languages, while
|
|
56
|
+
# entrypoints enforce the requested runtime limit with ulimit.
|
|
57
|
+
compile_safe_memory_mb = max(total_memory_mb, 1536) if self.cache_compilation else total_memory_mb
|
|
58
|
+
temp_dir = get_temp_dir(files)
|
|
59
|
+
cache_dir = get_cache_dir(code, self.language) if self.cache_compilation else None
|
|
60
|
+
cache_file = os.path.join(cache_dir, "main") if cache_dir else None
|
|
61
|
+
cache_status = "disabled" if not cache_dir else ("hit" if os.path.exists(cache_file) else "miss")
|
|
62
|
+
print(
|
|
63
|
+
f"env: {environment_variables}, time: {time_limit}s x{self.language_time_limit_multiplier} +{GRACE_S}s grace = {effective_time_limit}s, memory: {memory_limit}KB ({memory_limit_mb}MB) + {self.memory_overhead_mb}MB overhead = {total_memory_mb}MB, container: {compile_safe_memory_mb}MB, cache: {cache_status}"
|
|
64
|
+
)
|
|
65
|
+
# On a cache miss, serialize via a per-hash lock so only one container
|
|
66
|
+
# compiles at a time. This prevents simultaneous writes to the same
|
|
67
|
+
# Windows volume path from hanging. Cache hits run without the lock.
|
|
68
|
+
if cache_dir and not os.path.exists(cache_file):
|
|
69
|
+
with get_compile_lock(cache_dir):
|
|
70
|
+
return self.docker_helper.evaluate(
|
|
71
|
+
self.docker_image_name,
|
|
72
|
+
temp_dir,
|
|
73
|
+
environment_variables=environment_variables,
|
|
74
|
+
memory_limit_mb=compile_safe_memory_mb,
|
|
75
|
+
cache_dir=cache_dir,
|
|
76
|
+
)
|
|
77
|
+
return self.docker_helper.evaluate(
|
|
78
|
+
self.docker_image_name,
|
|
79
|
+
temp_dir,
|
|
80
|
+
environment_variables=environment_variables,
|
|
81
|
+
memory_limit_mb=compile_safe_memory_mb,
|
|
82
|
+
cache_dir=cache_dir,
|
|
83
|
+
)
|
|
File without changes
|