1
1
#!/usr/bin/env python3
2
-
2
+ """Meant to be run from inside python-test-runner container,
3
+ where this track repo is mounted at /python
4
+ """
5
+ import argparse
6
+ from functools import wraps
7
+ from itertools import zip_longest
8
+ import json
9
+ from pathlib import Path
3
10
import shutil
4
11
import subprocess
5
12
import sys
6
13
import tempfile
7
- from pathlib import Path
8
-
9
- from data import Config , ExerciseInfo
14
+ from data import Config , ExerciseConfig , ExerciseInfo
10
15
11
16
# Allow high-performance tests to be skipped
12
17
ALLOW_SKIP = ['alphametics' , 'largest-series-product' ]
13
18
19
+ TEST_RUNNER_DIR = Path ('/opt/test-runner' )
20
+
21
+ RUNNERS = {}
22
+
23
+
24
+ def runner (name ):
25
+ def _decorator (runner_func ):
26
+ RUNNERS [name ] = runner_func
27
+ @wraps (runner_func )
28
+ def _wrapper (exercise : ExerciseInfo , workdir : Path , quiet : bool = False ):
29
+ return runner_func (exercise , workdir , quiet = quiet )
30
+ return _wrapper
31
+ return _decorator
32
+
14
33
15
- def check_assignment (exercise : ExerciseInfo , quiet = False ) -> int :
16
- # Returns the exit code of the tests
17
- workdir = Path (tempfile .mkdtemp (exercise .slug ))
18
- solution_file = exercise .solution_stub .name
19
- try :
20
- test_file_out = workdir / exercise .test_file .name
21
- if exercise .slug in ALLOW_SKIP :
22
- shutil .copyfile (exercise .test_file , test_file_out )
34
+ def copy_file (src : Path , dst : Path , strip_skips = False ):
35
+ if strip_skips :
36
+ with src .open ('r' ) as src_file :
37
+ lines = [line for line in src_file .readlines ()
38
+ if not line .strip ().startswith ('@unittest.skip' )]
39
+ with dst .open ('w' ) as dst_file :
40
+ dst_file .writelines (lines )
41
+ else :
42
+ shutil .copy2 (src , dst )
43
+
44
+ def copy_solution_files (exercise : ExerciseInfo , workdir : Path , exercise_config : ExerciseConfig = None ):
45
+ if exercise_config is not None :
46
+ solution_files = exercise_config .files .solution
47
+ exemplar_files = exercise_config .files .exemplar
48
+ else :
49
+ solution_files = []
50
+ exemplar_files = []
51
+ if not solution_files :
52
+ solution_files .append (exercise .solution_stub .name )
53
+ solution_files = [exercise .path / s for s in solution_files ]
54
+ if not exemplar_files :
55
+ exemplar_files .append (exercise .exemplar_file .relative_to (exercise .path ))
56
+ exemplar_files = [exercise .path / e for e in exemplar_files ]
57
+ for solution_file , exemplar_file in zip_longest (solution_files , exemplar_files ):
58
+ if solution_file is None :
59
+ copy_file (exemplar_file , workdir / exemplar_file .name )
60
+ elif exemplar_file is None :
61
+ copy_file (solution_file , workdir / solution_file .name )
23
62
else :
24
- with exercise .test_file .open ('r' ) as src_file :
25
- lines = [line for line in src_file .readlines ()
26
- if not line .strip ().startswith ('@unittest.skip' )]
27
- with test_file_out .open ('w' ) as dst_file :
28
- dst_file .writelines (lines )
29
- shutil .copyfile (exercise .exemplar_file , workdir / solution_file )
30
- kwargs = {}
31
- if quiet :
32
- kwargs ['stdout' ] = subprocess .DEVNULL
33
- kwargs ['stderr' ] = subprocess .DEVNULL
34
- return subprocess .run ([sys .executable , '-m' , 'pytest' , test_file_out ], ** kwargs ).returncode
35
- finally :
36
- shutil .rmtree (workdir )
63
+ dst = workdir / solution_file .relative_to (exercise .path )
64
+ copy_file (exemplar_file , dst )
65
+
66
+
67
+ def copy_test_files (exercise : ExerciseInfo , workdir : Path , exercise_config = None ):
68
+ if exercise_config is not None :
69
+ test_files = exercise_config .files .test
70
+ else :
71
+ test_files = []
72
+ if not test_files :
73
+ test_files .append (exercise .test_file .name )
74
+ for test_file_name in test_files :
75
+ test_file = exercise .path / test_file_name
76
+ test_file_out = workdir / test_file_name
77
+ copy_file (test_file , test_file_out , strip_skips = (exercise .slug not in ALLOW_SKIP ))
78
+
79
+
80
+ def copy_exercise_files (exercise : ExerciseInfo , workdir : Path ):
81
+ exercise_config = None
82
+ if exercise .config_file .is_file ():
83
+ workdir_meta = workdir / '.meta'
84
+ workdir_meta .mkdir (exist_ok = True )
85
+ copy_file (exercise .config_file , workdir_meta / exercise .config_file .name )
86
+ exercise_config = exercise .load_config ()
87
+ copy_solution_files (exercise , workdir , exercise_config )
88
+ copy_test_files (exercise , workdir , exercise_config )
89
+
90
+
91
+ @runner ('pytest' )
92
+ def run_with_pytest (_exercise , workdir , quiet : bool = False ) -> int :
93
+ kwargs = {'cwd' : str (workdir )}
94
+ if quiet :
95
+ kwargs ['stdout' ] = subprocess .DEVNULL
96
+ kwargs ['stderr' ] = subprocess .DEVNULL
97
+ return subprocess .run ([sys .executable , '-m' , 'pytest' ], ** kwargs ).returncode
98
+
99
+
100
+ @runner ('test-runner' )
101
+ def run_with_test_runner (exercise , workdir , quiet : bool = False ) -> int :
102
+ kwargs = {}
103
+ if quiet :
104
+ kwargs ['stdout' ] = subprocess .DEVNULL
105
+ kwargs ['stderr' ] = subprocess .DEVNULL
106
+ if TEST_RUNNER_DIR .is_dir ():
107
+ kwargs ['cwd' ] = str (TEST_RUNNER_DIR )
108
+ args = ['./bin/run.sh' , exercise .slug , workdir , workdir ]
109
+ else :
110
+ args = [
111
+ 'docker-compose' ,
112
+ 'run' ,
113
+ '-w' , str (TEST_RUNNER_DIR ),
114
+ '--entrypoint' , './bin/run.sh' ,
115
+ '-v' , f'{ workdir } :/{ exercise .slug } ' ,
116
+ 'test-runner' ,
117
+ exercise .slug ,
118
+ f'/{ exercise .slug } ' ,
119
+ f'/{ exercise .slug } ' ,
120
+ ]
121
+ subprocess .run (args , ** kwargs )
122
+ results_file = workdir / 'results.json'
123
+ if results_file .is_file ():
124
+ with results_file .open () as f :
125
+ results = json .load (f )
126
+ if results ['status' ] == 'pass' :
127
+ return 0
128
+ return 1
129
+
130
+
131
+ def check_assignment (exercise : ExerciseInfo , runner : str = 'pytest' , quiet : bool = False ) -> int :
132
+ ret = 1
133
+ with tempfile .TemporaryDirectory (exercise .slug ) as workdir :
134
+ workdir = Path (workdir )
135
+ copy_exercise_files (exercise , workdir )
136
+ ret = RUNNERS [runner ](exercise , workdir , quiet = quiet )
137
+ return ret
138
+
139
+
140
+ def get_cli () -> argparse .ArgumentParser :
141
+ parser = argparse .ArgumentParser ()
142
+ runners = list (RUNNERS .keys ())
143
+ if not runners :
144
+ print ('No runners registered!' )
145
+ raise SystemExit (1 )
146
+ parser .add_argument ('-q' , '--quiet' , action = 'store_true' )
147
+ parser .add_argument ('-r' , '--runner' , choices = runners , default = runners [0 ])
148
+ parser .add_argument ('exercises' , nargs = '*' )
149
+ return parser
37
150
38
151
39
152
def main ():
153
+ opts = get_cli ().parse_args ()
40
154
config = Config .load ()
41
155
exercises = config .exercises .all ()
42
- if len ( sys . argv ) >= 2 :
156
+ if opts . exercises :
43
157
# test specific exercises
44
158
exercises = [
45
- e for e in exercises if e .slug in sys . argv [ 1 :]
159
+ e for e in exercises if e .slug in opts . exercises
46
160
]
161
+ not_found = [
162
+ slug for slug in opts .exercises
163
+ if not any (e .slug == slug for e in exercises )
164
+ ]
165
+ if not_found :
166
+ for slug in not_found :
167
+ if slug not in exercises :
168
+ print (f"unknown exercise '{ slug } '" )
169
+ raise SystemExit (1 )
170
+
171
+ print (f'TestEnvironment: { sys .executable .capitalize ()} ' )
172
+ print (f'Runner: { opts .runner } \n \n ' )
47
173
48
174
failures = []
49
175
for exercise in exercises :
@@ -52,18 +178,16 @@ def main():
52
178
print ('FAIL: File with test cases not found' )
53
179
failures .append ('{} (FileNotFound)' .format (exercise .slug ))
54
180
else :
55
- if check_assignment (exercise ):
181
+ if check_assignment (exercise , runner = opts . runner , quiet = opts . quiet ):
56
182
failures .append ('{} (TestFailed)' .format (exercise .slug ))
57
183
print ('' )
58
184
59
- print ('TestEnvironment:' , sys .executable .capitalize (), '\n \n ' )
60
-
61
185
if failures :
62
186
print ('FAILURES: ' , ', ' .join (failures ))
63
187
raise SystemExit (1 )
64
188
else :
65
189
print ('SUCCESS!' )
66
190
67
191
68
- if __name__ == ' __main__' :
192
+ if __name__ == " __main__" :
69
193
main ()
0 commit comments