aboutsummaryrefslogtreecommitdiffstats
path: root/resources/unittest_template.py
blob: 755f7ccb506beabb4a06d7d487eeca1254cfaa7d (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
# flake8: noqa
"""This template is used inside snekbox to evaluate and test user code."""
import ast
import io
import os
import sys
import traceback
import unittest
from itertools import chain
from types import ModuleType, SimpleNamespace
from typing import NoReturn
from unittest import mock

### USER CODE


class RunnerTestCase(unittest.TestCase):
### UNIT CODE


def _exit_sandbox(code: int) -> NoReturn:
    """
    Exit the sandbox by printing the result to the actual stdout and exit with the provided code.

    Codes:
    - 0: Executed with success
    - 5: Syntax error while parsing user code
    - 99: Internal error
    """
    print(RESULT.getvalue(), file=ORIGINAL_STDOUT, end="")
    sys.exit(code)


def _load_user_module() -> ModuleType:
    """Load the user code into a new module and return it."""
    try:
        ast.parse(USER_CODE, "<input>")
    except SyntaxError:
        RESULT.write("".join(traceback.format_exception(*sys.exc_info(), limit=0)))
        _exit_sandbox(5)

    _module = ModuleType("module")
    exec(USER_CODE, _module.__dict__)

    return _module


def _main() -> None:
    suite = unittest.defaultTestLoader.loadTestsFromTestCase(RunnerTestCase)
    result = suite.run(unittest.TestResult())

    RESULT.write(str(int(result.wasSuccessful())))

    if not result.wasSuccessful():
        RESULT.write(
            ";".join(chain(
                (error[0]._testMethodName.lstrip("test_") for error in result.errors),
                (failure[0]._testMethodName.lstrip("test_") for failure in result.failures)
            ))
        )

    _exit_sandbox(0)


try:
    # Fake file object not writing anything
    DEVNULL = SimpleNamespace(write=lambda *_: None, flush=lambda *_: None)

    RESULT = io.StringIO()
    ORIGINAL_STDOUT = sys.stdout

    # stdout/err is patched in order to control what is outputted by the runner
    sys.stdout = DEVNULL
    sys.stderr = DEVNULL
    
    # Load the user code as a global module variable
    module = _load_user_module()
    _main()
except Exception:
    print("Uncaught exception inside runner.", file=RESULT)
    _exit_sandbox(99)