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

tjcsl / tin / 11766501715

10 Nov 2024 03:49PM UTC coverage: 54.923% (+0.1%) from 54.79%
11766501715

Pull #86

github

web-flow
Merge dc1dc32b2 into 6a43c847c
Pull Request #86: Improve instructions for dev env

403 of 895 branches covered (45.03%)

Branch coverage included in aggregate %.

26 of 43 new or added lines in 6 files covered. (60.47%)

13 existing lines in 2 files now uncovered.

1516 of 2599 relevant lines covered (58.33%)

0.58 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✔
37
    submission = Submission.objects.get(id=submission_id)
×
38

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

46
        submission_wrapper_path = submission.wrapper_file_path
×
47

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

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

NEW
67
        if submission.assignment.venv_fully_created:
×
NEW
68
            python_exe = os.path.join(submission.assignment.venv.path, "bin", "python")
×
NEW
69
        elif settings.DEBUG:
×
NEW
70
            python_exe = sys.executable
×
71
        else:  # pragma: no cover
72
            python_exe = "/usr/bin/python3.10"
73

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

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

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

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

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

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

124
    try:
×
125
        retcode = None
×
126
        killed = False
×
127

128
        output = ""
×
129
        errors = ""
×
130

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

NEW
141
        if settings.USE_SANDBOXING:
×
142
            whitelist = [os.path.dirname(grader_path)]
×
143
            read_only = [grader_path, submission_path, os.path.dirname(submission_wrapper_path)]
×
144
            if submission.assignment.venv_fully_created:
×
145
                whitelist.append(submission.assignment.venv.path)
×
146
                read_only.append(submission.assignment.venv.path)
×
147

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

NEW
156
        env = os.environ.copy()
×
157
        if submission.assignment.venv_fully_created:
×
158
            env.update(submission.assignment.venv.get_activation_env())
×
159

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

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

176
            timed_out = False
×
177

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

188
                    timeout = min(timeout, 15)
×
189
                else:
190
                    timeout = 15
×
191

192
                if submission.kill_requested:
×
193
                    break
×
194

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

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

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

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

214
            if proc.poll() is None:
×
215
                killed = True
×
216

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

229
            output += proc.stdout.read().decode()
×
230
            errors += proc.stderr.read().decode()
×
231

232
            if killed:
×
233
                msg = "[Grader timed out]" if timed_out else "[Grader killed]"
×
234

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

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

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

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

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

NEW
284
        with contextlib.suppress(FileNotFoundError):
×
285
            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