Skip to content

Commit 80c72de

Browse files
author
Nils Bars
committed
Update run_tests() to return results directly (fixes #17)
Changes run_tests() to accept parameters and return List[TaskTestResult] instead of writing results to a file and reading args from environment. This enables direct module invocation from task.py. - Rename _TestResult to TaskTestResult (public API) - Add result_will_be_submitted and only_run_these_tasks parameters - Update checks.py to use config module for paths - Add SUCCESS/FAILURE constants to utils.py
1 parent 721aa45 commit 80c72de

3 files changed

Lines changed: 61 additions & 49 deletions

File tree

ref_utils/checks.py

Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
"""Various checks you may want to run during submission tests"""
2-
from pathlib import Path
3-
from typing import List, Optional
4-
52
import os
3+
import subprocess
4+
from pathlib import Path
5+
from typing import List
66

7-
from .utils import print_ok, print_warn, print_err, SUCCESS, FAILURE
7+
from .config import get_config
88
from .process import run
9+
from .utils import FAILURE, SUCCESS, print_err, print_ok, print_warn
910

1011
_NO_LINT_ENV_VAR = "NO_LINT"
1112
_ENV_VAL_TRUE = "1"
@@ -16,10 +17,9 @@ def contains_flag(flag: str, python_script: Path, silent: bool = False) -> bool:
1617
Run submitted file and match whether it contains the flag value.
1718
"""
1819
cmd: List[str] = ["python3", python_script.as_posix()]
19-
output: Optional[str] = run(cmd, check_returncode=False, timeout=10)
20-
if output is None:
21-
return FAILURE
22-
if not flag in output:
20+
result = run(cmd, check_signal=False, timeout=10, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
21+
output = result.stdout.decode() if result.stdout else ""
22+
if flag not in output:
2323
if not silent:
2424
print_err("[!] Failed to find flag")
2525
return FAILURE
@@ -34,10 +34,10 @@ def run_pylint(python_files: List[Path]) -> bool:
3434
"""
3535
if not python_files or os.environ.get(_NO_LINT_ENV_VAR, '') == _ENV_VAL_TRUE:
3636
return SUCCESS
37-
lint_output = run(["pylint", "--exit-zero", "--rcfile", "/etc/pylintrc"] +
38-
[str(f.resolve()) for f in python_files])
39-
if lint_output is None:
40-
return FAILURE
37+
result = run(["pylint", "--exit-zero", "--rcfile", str(get_config().pylint_config_path)] +
38+
[str(f.resolve()) for f in python_files],
39+
stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
40+
lint_output = result.stdout.decode() if result.stdout else ""
4141
if lint_output != "":
4242
print_warn("[!] pylint's syntax and coding style checks failed:")
4343
print_warn(' ' + '\n '.join(lint_output.split('\n')))
@@ -52,9 +52,10 @@ def run_mypy(python_files: List[Path]) -> bool:
5252
"""
5353
if not python_files or os.environ.get(_NO_LINT_ENV_VAR, '') == _ENV_VAL_TRUE:
5454
return SUCCESS
55-
lint_output = run(["mypy", "--config-file", "/etc/mypyrc"] + [str(f.resolve()) for f in python_files])
56-
if lint_output is None:
57-
return FAILURE
55+
cmd = ["mypy", "--config-file", str(get_config().mypy_config_path)]
56+
cmd += [str(f.resolve()) for f in python_files]
57+
result = run(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
58+
lint_output = result.stdout.decode() if result.stdout else ""
5859
if lint_output != "":
5960
print_warn("[!] mypy's type checks failed:")
6061
print_warn(' ' + '\n '.join(lint_output.split('\n')))
@@ -68,7 +69,7 @@ def check_all_python_files() -> bool:
6869
Run checks only suited for Python files (mypy + pylint)
6970
"""
7071
tests_passed = True
71-
python_files = [f for f in Path("/home/user").glob("**/*.py") if not f.name.startswith(".")]
72+
python_files = [f for f in get_config().user_home_path.glob("**/*.py") if not f.name.startswith(".")]
7273
if not python_files or os.environ.get(_NO_LINT_ENV_VAR, '') == _ENV_VAL_TRUE:
7374
return tests_passed
7475
print_ok(f'[+] Testing {len(python_files)} Python source code files')

ref_utils/decorator.py

Lines changed: 34 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,14 @@
1-
from functools import wraps
2-
from collections import defaultdict
3-
from pathlib import Path
4-
from typing import Any, Callable, List
1+
import os
52
import typing as ty
3+
import warnings
4+
from dataclasses import dataclass
5+
from functools import wraps
6+
from typing import Any, Callable
67

78
from ref_utils.error import RefUtilsError
8-
from .utils import print_ok, print_err
9-
from dataclasses import dataclass, asdict
10-
import warnings
11-
import json
12-
import os
139

14-
TEST_RESULT_PATH = Path("/var/test_result")
10+
from .utils import print_err, print_ok
11+
1512
DEFAULT_TASK_NAME = 'default'
1613
__registered_tasks: ty.Dict[str, '_Task'] = {}
1714

@@ -29,9 +26,10 @@ class TestResult():
2926

3027

3128
@dataclass
32-
class _TestResult():
29+
class TaskTestResult():
3330
"""
34-
Class used to serialize data before sending it to the webserver.
31+
The result of a task's test, including the task name.
32+
Used for serialization when sending results to the webserver.
3533
"""
3634
task_name: str
3735
success: bool
@@ -111,30 +109,38 @@ def wrapper(*args: str, **kwargs: Any) -> Any:
111109

112110

113111

114-
def run_tests() -> None:
112+
def run_tests(
113+
*,
114+
result_will_be_submitted: bool = False,
115+
only_run_these_tasks: ty.Optional[ty.Sequence[str]] = None,
116+
) -> ty.List[TaskTestResult]:
115117
"""
116-
Must be called by the test script to execute all tests.
118+
Execute all registered tests.
119+
120+
Args:
121+
result_will_be_submitted: If True, indicates this is a final submission.
122+
only_run_these_tasks: Optional list of task names to run. If None, runs all tasks.
123+
124+
Returns:
125+
A list of TaskTestResult objects containing the results of each task.
117126
"""
127+
# Set env var so test_result_will_be_submitted() works within tests
128+
if result_will_be_submitted:
129+
os.environ["RESULT_WILL_BE_SUBMITTED"] = "1"
130+
118131
print_ok('[+] Running tests..')
119132
all_tests_passed = True
120133
has_multiple_tasks = len(__registered_tasks) > 1
121-
task_test_results: ty.List[_TestResult] = []
122-
123-
# This is set by task.py if the user only wants to run a subset of tests.
124-
only_run_these_tasks = os.environ.get("ONLY_RUN_THESE_TASKS")
125-
if only_run_these_tasks:
126-
only_run_these_tasks = only_run_these_tasks.split(":")
134+
task_test_results: ty.List[TaskTestResult] = []
127135

128136
# Run all sub-tasks one after another.
129137
for task_name, tests in __registered_tasks.items():
130138
task_passed = True
131139

132-
TEST_RESULT_PATH.unlink(missing_ok=True)
133-
134140
if has_multiple_tasks:
135141
print_ok(f'[+] *** Running tests for task \"{task_name}\" ***')
136142
if only_run_these_tasks and task_name not in only_run_these_tasks:
137-
print_ok(f"[+] User requested to exclude task, skipping...")
143+
print_ok("[+] User requested to exclude task, skipping...")
138144
continue
139145

140146
if tests.env_tests and not tests.submission_test and not tests.extended_submission_test:
@@ -150,7 +156,7 @@ def run_tests() -> None:
150156

151157
#Do not run submission tests if the environ is invalid
152158
if not task_passed:
153-
task_test_results.append(_TestResult(task_name, False, None))
159+
task_test_results.append(TaskTestResult(task_name, False, None))
154160
if has_multiple_tasks:
155161
# Only print this if we have multiple tasks. If we only have one,
156162
# the would just duplicate the error printed at the end.
@@ -170,9 +176,9 @@ def run_tests() -> None:
170176
ret = False
171177

172178
if isinstance(ret, bool):
173-
ret = _TestResult(task_name, ret, None)
179+
ret = TaskTestResult(task_name, ret, None)
174180
elif isinstance(ret, TestResult):
175-
ret = _TestResult(task_name, ret.success, ret.score)
181+
ret = TaskTestResult(task_name, ret.success, ret.score)
176182
else:
177183
raise RefUtilsError(f"Submission test returned unexpected type: {type(ret)}")
178184

@@ -181,7 +187,7 @@ def run_tests() -> None:
181187
all_tests_passed &= ret.success
182188
else:
183189
# If there is no test, we consider this to be an success.
184-
task_test_results.append(_TestResult(task_name, True, None))
190+
task_test_results.append(TaskTestResult(task_name, True, None))
185191
print_ok("[+] No test found")
186192

187193
if not task_passed and has_multiple_tasks:
@@ -197,5 +203,4 @@ def run_tests() -> None:
197203
else:
198204
print_ok('[+] All tests passed! Good job. Ready to submit!')
199205

200-
results = json.dumps([asdict(e) for e in task_test_results])
201-
TEST_RESULT_PATH.write_text(results)
206+
return task_test_results

ref_utils/utils.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,18 @@
11
""" Utility functions including colored printing or subprocess.run wrapper dropping privileges"""
2-
import sys
32
import os
3+
import sys
44
import typing as t
5+
from pathlib import Path
56
from typing import Any, AnyStr, List, Union
67

7-
from pathlib import Path
88
from colorama import Fore, Style
99

10+
from .config import get_config
11+
12+
# Constants for return values
13+
SUCCESS: bool = True
14+
FAILURE: bool = False
15+
1016

1117
def print_ok(*args: str, **kwargs: Any) -> None:
1218
"""Print green to signal correctness"""
@@ -46,7 +52,7 @@ def get_user_environment() -> t.Dict[str, Union[str, bytes]]:
4652
defined during submission.
4753
"""
4854
ret: t.Dict[str, Union[str, bytes]] = {}
49-
content = Path('/tmp/.user_environ').read_text()
55+
content = get_config().user_environ_path.read_text()
5056
lines = content.split('\x00')
5157
for line in lines:
5258
if line == '':
@@ -88,4 +94,4 @@ def map_path_as_posix(cmd: List[Union[Path, str, bytes]]) -> List[Union[str, byt
8894
ret.append(c.as_posix())
8995
else:
9096
ret.append(c)
91-
return ret
97+
return ret

0 commit comments

Comments
 (0)