• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

tjcsl / tin / 17450891849

04 Sep 2025 01:49AM UTC coverage: 58.858% (+0.2%) from 58.654%
17450891849

Pull #92

github

web-flow
Merge 8c9f1d1d3 into 564991635
Pull Request #92: Allow choosing custom versions of python for graders

470 of 971 branches covered (48.4%)

Branch coverage included in aggregate %.

59 of 81 new or added lines in 12 files covered. (72.84%)

1 existing line in 1 file now uncovered.

1746 of 2794 relevant lines covered (62.49%)

0.62 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

13.08
/tin/apps/submissions/tasks.py
1
from __future__ import annotations
1✔
2

3
import contextlib
1✔
4
import logging
1✔
5
import os
1✔
6
import re
1✔
7
import select
1✔
8
import signal
1✔
9
import subprocess
1✔
10
import sys
1✔
11
import time
1✔
12
import traceback
1✔
13
from decimal import Decimal
1✔
14
from pathlib import Path
1✔
15

16
import psutil
1✔
17
from asgiref.sync import async_to_sync
1✔
18
from celery import shared_task
1✔
19
from channels.layers import get_channel_layer
1✔
20
from django.conf import settings
1✔
21
from django.utils import timezone
1✔
22

23
from ... import sandboxing
1✔
24
from ...sandboxing import get_assignment_sandbox_args
1✔
25
from .models import Submission
1✔
26

27
logger = logging.getLogger(__name__)
1✔
28

29

30
def truncate_output(text, field_name):
1✔
31
    max_len = Submission._meta.get_field(field_name).max_length
×
32
    return ("..." + text[-max_len + 5 :]) if len(text) > max_len else text
×
33

34

35
@shared_task
1✔
36
def run_submission(submission_id):
1✔
NEW
37
    submission = Submission.objects.select_related(
×
38
        "assignment", "assignment__language_details"
39
    ).get(id=submission_id)
40

41
    try:
×
42
        grader_path = os.path.join(settings.MEDIA_ROOT, submission.assignment.grader_file.name)
×
43
        grader_log_path = os.path.join(
×
44
            settings.MEDIA_ROOT, submission.assignment.grader_log_filename
45
        )
46
        submission_path = submission.file_path
×
47

48
        submission_wrapper_path = submission.wrapper_file_path
×
49

50
        args = get_assignment_sandbox_args(
×
51
            ["mkdir", "-p", "--", os.path.dirname(submission_wrapper_path)],
52
            network_access=False,
53
            whitelist=[os.path.dirname(os.path.dirname(submission_wrapper_path))],
54
        )
55

56
        try:
×
57
            subprocess.run(
×
58
                args,
59
                stdin=subprocess.DEVNULL,
60
                stdout=subprocess.DEVNULL,
61
                stderr=subprocess.PIPE,
62
                text=True,
63
                check=True,
64
            )
65
        except FileNotFoundError as e:
×
66
            logger.error("Cannot run processes: %s", e)
×
67
            raise FileNotFoundError from e
×
68

69
        if submission.assignment.venv_fully_created:
×
70
            python_exe = os.path.join(submission.assignment.venv.path, "bin", "python")
×
71
        elif settings.DEBUG:
×
72
            python_exe = sys.executable
×
73
        else:  # pragma: no cover
74
            python_exe = submission.assignment.language_details.info["python3"]
75

76
        if settings.IS_BUBBLEWRAP_PRESENT and settings.IS_SANDBOXING_MODULE_PRESENT:
×
77
            wrapper_text = (
×
78
                Path(settings.BASE_DIR)
79
                .joinpath(
80
                    "sandboxing",
81
                    "wrappers",
82
                    "sandboxed",
83
                    f"{submission.assignment.language}.txt",
84
                )
85
                .read_text("utf-8")
86
            )
87

88
        elif submission.assignment.language == "P":
×
89
            wrapper_text = settings.DEBUG_GRADER_WRAPPER_SCRIPT.read_text("utf-8")
×
90
        else:
91
            raise NotImplementedError(
×
92
                f"Unsupported language {submission.assignment.language} in DEBUG"
93
            )
94

95
        wrapper_text = wrapper_text.format(
×
96
            has_network_access=bool(submission.assignment.has_network_access),
97
            venv_path=(
98
                submission.assignment.venv.path
99
                if submission.assignment.venv_fully_created
100
                else None
101
            ),
102
            submission_path=submission_path,
103
            python=python_exe,
104
        )
105

106
        with open(submission_wrapper_path, "w", encoding="utf-8") as f_obj:
×
107
            f_obj.write(wrapper_text)
×
108

109
        os.chmod(submission_wrapper_path, 0o700)
×
110
    except OSError:
×
111
        submission.grader_output = (
×
112
            "An internal error occurred. Please try again.\n"
113
            "If the problem persists, contact your teacher."
114
        )
115
        submission.grader_errors = truncate_output(
×
116
            traceback.format_exc().replace("\0", ""), "grader_errors"
117
        )
118
        submission.complete = True
×
119
        submission.save()
×
120

121
        async_to_sync(get_channel_layer().group_send)(
×
122
            submission.channel_group_name, {"type": "submission.updated"}
123
        )
124
        return
×
125

126
    try:
×
127
        retcode = None
×
128
        killed = False
×
129

130
        output = ""
×
131
        errors = ""
×
132

133
        args = [
×
134
            python_exe,
135
            "-u",
136
            grader_path,
137
            submission_wrapper_path,
138
            submission_path,
139
            submission.student.username,
140
            grader_log_path,
141
        ]
142

143
        if settings.IS_FIREJAIL_PRESENT and settings.IS_SANDBOXING_MODULE_PRESENT:
×
144
            whitelist = [os.path.dirname(grader_path)]
×
145
            read_only = [grader_path, submission_path, os.path.dirname(submission_wrapper_path)]
×
146
            if submission.assignment.venv_fully_created:
×
147
                whitelist.append(submission.assignment.venv.path)
×
148
                read_only.append(submission.assignment.venv.path)
×
149

150
            args = sandboxing.get_assignment_sandbox_args(
×
151
                args,
152
                network_access=submission.assignment.grader_has_network_access,
153
                direct_network_access=False,
154
                whitelist=whitelist,
155
                read_only=read_only,
156
            )
157

158
        env = os.environ.copy()
×
159
        if submission.assignment.venv_fully_created:
×
160
            env.update(submission.assignment.venv.get_activation_env())
×
161

162
        with subprocess.Popen(  # pylint: disable=subprocess-popen-preexec-fn
×
163
            args,
164
            stdout=subprocess.PIPE,
165
            stderr=subprocess.PIPE,
166
            stdin=subprocess.DEVNULL,
167
            bufsize=0,
168
            cwd=os.path.dirname(grader_path),
169
            preexec_fn=os.setpgrp,  # noqa: PLW1509
170
            env=env,
171
        ) as proc:
172
            start_time = time.time()
×
173

174
            submission.grader_pid = proc.pid
×
175
            submission.grader_start_time = timezone.localtime().timestamp()
×
176
            submission.save()
×
177

178
            timed_out = False
×
179

180
            while proc.poll() is None:
×
181
                submission.refresh_from_db()
×
182
                submission.assignment.refresh_from_db()
×
183
                if submission.assignment.enable_grader_timeout:
×
184
                    time_elapsed = time.time() - start_time
×
185
                    timeout = submission.assignment.grader_timeout - time_elapsed
×
186
                    if timeout <= 0:
×
187
                        timed_out = True
×
188
                        break
×
189

190
                    timeout = min(timeout, 15)
×
191
                else:
192
                    timeout = 15
×
193

194
                if submission.kill_requested:
×
195
                    break
×
196

197
                files_ready = select.select([proc.stdout, proc.stderr], [], [], timeout)[0]
×
198
                if proc.stdout in files_ready:
×
199
                    output += proc.stdout.read(8192).decode()
×
200

201
                if proc.stderr in files_ready:
×
202
                    errors += proc.stderr.read(8192).decode()
×
203

204
                submission.grader_output = truncate_output(
×
205
                    output.replace("\0", ""), "grader_output"
206
                )
207
                submission.grader_errors = truncate_output(
×
208
                    errors.replace("\0", ""), "grader_errors"
209
                )
210
                submission.save(update_fields=["grader_output", "grader_errors"])
×
211

212
                async_to_sync(get_channel_layer().group_send)(
×
213
                    submission.channel_group_name, {"type": "submission.updated"}
214
                )
215

216
            if proc.poll() is None:
×
217
                killed = True
×
218

219
                children = psutil.Process(proc.pid).children(recursive=True)
×
220
                try:
×
221
                    os.killpg(proc.pid, signal.SIGKILL)
×
222
                except ProcessLookupError:
×
223
                    # Shouldn't happen, but just in case
224
                    proc.kill()
×
225
                for child in children:
×
226
                    try:
×
227
                        child.kill()
×
228
                    except psutil.NoSuchProcess:
×
229
                        pass
×
230

231
            output += proc.stdout.read().decode()
×
232
            errors += proc.stderr.read().decode()
×
233

234
            if killed:
×
235
                msg = "[Grader timed out]" if timed_out else "[Grader killed]"
×
236

237
                if output and not output.endswith("\n"):
×
238
                    output += "\n"
×
239
                output += msg
×
240

241
                if errors and not errors.endswith("\n"):
×
242
                    errors += "\n"
×
243
                errors += msg
×
244
            else:
245
                retcode = proc.poll()
×
246
                if retcode != 0:
×
247
                    if output and not output.endswith("\n"):
×
248
                        output += "\n"
×
249
                    output += "[Grader error]"
×
250

251
                    if errors and not errors.endswith("\n"):
×
252
                        errors += "\n"
×
253
                    errors += f"[Grader exited with status {retcode}]"
×
254

255
            submission.grader_output = truncate_output(output.replace("\0", ""), "grader_output")
×
256
            submission.grader_errors = truncate_output(errors.replace("\0", ""), "grader_errors")
×
257
            submission.save()
×
258
    except Exception:  # pylint: disable=broad-except  # noqa: BLE001
×
259
        submission.grader_output = "[Internal error]"
×
260
        submission.grader_errors = truncate_output(
×
261
            traceback.format_exc().replace("\0", ""), "grader_errors"
262
        )
263
        submission.save()
×
264
    else:
265
        if output and not killed and retcode == 0:
×
266
            last_line = output.splitlines()[-1]
×
267
            match = re.search(r"^Score: ([\d.]+%?)$", last_line)
×
268
            if match is not None:
×
269
                score = match.group(1)
×
270
                if score.endswith("%"):
×
271
                    score = submission.assignment.points_possible * Decimal(score[:-1]) / 100
×
272
                else:
273
                    score = Decimal(score)
×
274
                if abs(score) < 1000:
×
275
                    submission.points_received = score
×
276
                    submission.has_been_graded = True
×
277
    finally:
278
        submission.complete = True
×
279
        submission.grader_pid = None
×
280
        submission.save()
×
281

282
        async_to_sync(get_channel_layer().group_send)(
×
283
            submission.channel_group_name, {"type": "submission.updated"}
284
        )
285

286
        with contextlib.suppress(FileNotFoundError):
×
287
            os.remove(submission_wrapper_path)
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc