|
1
|
|
|
''' |
|
2
|
|
|
The official executor API for validation test and full test scripts. |
|
3
|
|
|
''' |
|
4
|
|
|
|
|
5
|
|
|
import os |
|
6
|
|
|
import sys |
|
7
|
|
|
import importlib |
|
8
|
|
|
import shutil |
|
9
|
|
|
import os.path |
|
10
|
|
|
import glob |
|
11
|
|
|
import json |
|
12
|
|
|
import re |
|
13
|
|
|
|
|
14
|
|
|
from .compiler import compiler_cmdline, GCC |
|
15
|
|
|
from .config import read_config |
|
16
|
|
|
from .running import kill_longrunning, RunningProgram |
|
17
|
|
|
from .exceptions import * |
|
18
|
|
|
from .filesystem import has_file, create_working_dir, prepare_working_directory |
|
19
|
|
|
from .hostinfo import all_host_infos, ipaddress |
|
20
|
|
|
|
|
21
|
|
|
from urllib.request import urlopen, urlretrieve |
|
22
|
|
|
from urllib.error import HTTPError, URLError |
|
23
|
|
|
from urllib.parse import urlencode |
|
24
|
|
|
|
|
25
|
|
|
import logging |
|
26
|
|
|
logger = logging.getLogger('opensubmitexec') |
|
27
|
|
|
|
|
28
|
|
|
UNSPECIFIC_ERROR = -9999 |
|
29
|
|
|
|
|
30
|
|
|
|
|
31
|
|
|
class Job(): |
|
32
|
|
|
''' |
|
33
|
|
|
A OpenSubmit job to be run by the test machine. |
|
34
|
|
|
''' |
|
35
|
|
|
|
|
36
|
|
|
# The current executor configuration. |
|
37
|
|
|
_config = None |
|
38
|
|
|
# Talk to the configured OpenSubmit server? |
|
39
|
|
|
_online = None |
|
40
|
|
|
|
|
41
|
|
|
# Download source for the student sub |
|
42
|
|
|
submission_url = None |
|
43
|
|
|
# Download source for the validator |
|
44
|
|
|
validator_url = None |
|
45
|
|
|
# The working directory for this job |
|
46
|
|
|
working_dir = None |
|
47
|
|
|
# The timeout for execution, as demanded by the server |
|
48
|
|
|
timeout = None |
|
49
|
|
|
# The OpenSubmit submission ID |
|
50
|
|
|
submission_id = None |
|
51
|
|
|
# The OpenSubmit submission file ID |
|
52
|
|
|
file_id = None |
|
53
|
|
|
# Did the validator script sent a result to the server? |
|
54
|
|
|
result_sent = False |
|
55
|
|
|
# Action requested by the server (legacy) |
|
56
|
|
|
action = None |
|
57
|
|
|
# Name of the submitting student |
|
58
|
|
|
submitter_name = None |
|
59
|
|
|
# Student ID of the submitting student |
|
60
|
|
|
submitter_student_id = None |
|
61
|
|
|
# Names of the submission authors |
|
62
|
|
|
author_names = None |
|
63
|
|
|
# Name of the study program of the submitter |
|
64
|
|
|
submitter_studyprogram = None |
|
65
|
|
|
# Name of the course where this submission was done |
|
66
|
|
|
course = None |
|
67
|
|
|
# Name of the assignment where this job was done |
|
68
|
|
|
assignment = None |
|
69
|
|
|
|
|
70
|
|
|
# The base name of the validation / full test script |
|
71
|
|
|
# on disk, for importing. |
|
72
|
|
|
_validator_import_name = 'validator' |
|
73
|
|
|
|
|
74
|
|
|
@property |
|
75
|
|
|
# The file name of the validation / full test script |
|
76
|
|
|
# on disk, after unpacking / renaming. |
|
77
|
|
|
def validator_script_name(self): |
|
78
|
|
|
return self.working_dir + self._validator_import_name + '.py' |
|
79
|
|
|
|
|
80
|
|
|
def __init__(self, config=None, online=True): |
|
81
|
|
|
if config: |
|
82
|
|
|
self._config = config |
|
83
|
|
|
else: |
|
84
|
|
|
self._config = read_config() |
|
85
|
|
|
self._online = online |
|
86
|
|
|
|
|
87
|
|
|
def __str__(self): |
|
88
|
|
|
''' |
|
89
|
|
|
Nicer logging of job objects. |
|
90
|
|
|
''' |
|
91
|
|
|
return str(vars(self)) |
|
92
|
|
|
|
|
93
|
|
|
def _run_validate(self): |
|
94
|
|
|
''' |
|
95
|
|
|
Execute the validate() method in the test script belonging to this job. |
|
96
|
|
|
''' |
|
97
|
|
|
assert(os.path.exists(self.validator_script_name)) |
|
98
|
|
|
old_path = sys.path |
|
99
|
|
|
sys.path = [self.working_dir] + old_path |
|
100
|
|
|
# logger.debug('Python search path is now {0}.'.format(sys.path)) |
|
101
|
|
|
|
|
102
|
|
|
try: |
|
103
|
|
|
module = importlib.import_module(self._validator_import_name) |
|
104
|
|
|
except Exception as e: |
|
105
|
|
|
text_student = "Internal validation problem, please contact your course responsible." |
|
106
|
|
|
text_tutor = "Exception while loading the validator: " + str(e) |
|
107
|
|
|
self._send_result(text_student, text_tutor, UNSPECIFIC_ERROR) |
|
108
|
|
|
return |
|
109
|
|
|
|
|
110
|
|
|
# Looped validator loading in the test suite demands this |
|
111
|
|
|
importlib.reload(module) |
|
112
|
|
|
|
|
113
|
|
|
# make the call |
|
114
|
|
|
try: |
|
115
|
|
|
module.validate(self) |
|
116
|
|
|
except Exception as e: |
|
117
|
|
|
# get more info |
|
118
|
|
|
text_student = None |
|
119
|
|
|
text_tutor = None |
|
120
|
|
|
if type(e) is TerminationException: |
|
121
|
|
|
text_student = "The execution of '{0}' terminated unexpectely.".format( |
|
122
|
|
|
e.instance.name) |
|
123
|
|
|
text_tutor = "The execution of '{0}' terminated unexpectely.".format( |
|
124
|
|
|
e.instance.name) |
|
125
|
|
|
text_student += "\n\nOutput so far:\n" + e.output |
|
126
|
|
|
text_tutor += "\n\nOutput so far:\n" + e.output |
|
127
|
|
|
elif type(e) is TimeoutException: |
|
128
|
|
|
text_student = "The execution of '{0}' was cancelled, since it took too long.".format( |
|
129
|
|
|
e.instance.name) |
|
130
|
|
|
text_tutor = "The execution of '{0}' was cancelled due to timeout.".format( |
|
131
|
|
|
e.instance.name) |
|
132
|
|
|
text_student += "\n\nOutput so far:\n" + e.output |
|
133
|
|
|
text_tutor += "\n\nOutput so far:\n" + e.output |
|
134
|
|
|
elif type(e) is NestedException: |
|
135
|
|
|
text_student = "Unexpected problem during the execution of '{0}'. {1}".format( |
|
136
|
|
|
e.instance.name, |
|
137
|
|
|
str(e.real_exception)) |
|
138
|
|
|
text_tutor = "Unkown exception during the execution of '{0}'. {1}".format( |
|
139
|
|
|
e.instance.name, |
|
140
|
|
|
str(e.real_exception)) |
|
141
|
|
|
text_student += "\n\nOutput so far:\n" + e.output |
|
142
|
|
|
text_tutor += "\n\nOutput so far:\n" + e.output |
|
143
|
|
|
elif type(e) is WrongExitStatusException: |
|
144
|
|
|
text_student = "The execution of '{0}' resulted in the unexpected exit status {1}.".format( |
|
145
|
|
|
e.instance.name, |
|
146
|
|
|
e.got) |
|
147
|
|
|
text_tutor = "The execution of '{0}' resulted in the unexpected exit status {1}.".format( |
|
148
|
|
|
e.instance.name, |
|
149
|
|
|
e.got) |
|
150
|
|
|
text_student += "\n\nOutput so far:\n" + e.output |
|
151
|
|
|
text_tutor += "\n\nOutput so far:\n" + e.output |
|
152
|
|
|
elif type(e) is JobException: |
|
153
|
|
|
# Some problem with our own code |
|
154
|
|
|
text_student = e.info_student |
|
155
|
|
|
text_tutor = e.info_tutor |
|
156
|
|
|
elif type(e) is FileNotFoundError: |
|
157
|
|
|
text_student = "A file is missing: {0}".format( |
|
158
|
|
|
str(e)) |
|
159
|
|
|
text_tutor = "Missing file: {0}".format( |
|
160
|
|
|
str(e)) |
|
161
|
|
|
elif type(e) is AssertionError: |
|
162
|
|
|
# Need this harsh approach to kill the |
|
163
|
|
|
# test suite execution at this point |
|
164
|
|
|
# Otherwise, the problem gets lost in |
|
165
|
|
|
# the log storm |
|
166
|
|
|
logger.error( |
|
167
|
|
|
"Failed assertion in validation script. Should not happen in production.") |
|
168
|
|
|
exit(-1) |
|
169
|
|
|
else: |
|
170
|
|
|
# Something really unexpected |
|
171
|
|
|
text_student = "Internal problem while validating your submission. Please contact the course responsible." |
|
172
|
|
|
text_tutor = "Unknown exception while running the validator: {0}".format( |
|
173
|
|
|
str(e)) |
|
174
|
|
|
# We got the text. Report the problem. |
|
175
|
|
|
self._send_result(text_student, text_tutor, UNSPECIFIC_ERROR) |
|
176
|
|
|
return |
|
177
|
|
|
# no unhandled exception during the execution of the validator |
|
178
|
|
|
if not self.result_sent: |
|
179
|
|
|
logger.debug("Validation script forgot result sending.") |
|
180
|
|
|
self.send_pass_result() |
|
181
|
|
|
# roll back |
|
182
|
|
|
sys.path = old_path |
|
183
|
|
|
|
|
184
|
|
|
def _send_result(self, info_student, info_tutor, error_code): |
|
185
|
|
|
post_data = [("SubmissionFileId", self.file_id), |
|
186
|
|
|
("Message", info_student), |
|
187
|
|
|
("Action", self.action), |
|
188
|
|
|
("MessageTutor", info_tutor), |
|
189
|
|
|
("ExecutorDir", self.working_dir), |
|
190
|
|
|
("ErrorCode", error_code), |
|
191
|
|
|
("Secret", self._config.get("Server", "secret")), |
|
192
|
|
|
("UUID", self._config.get("Server", "uuid")) |
|
193
|
|
|
] |
|
194
|
|
|
logger.info( |
|
195
|
|
|
'Sending result to OpenSubmit Server: ' + str(post_data)) |
|
196
|
|
|
if self._online: |
|
197
|
|
|
send_post(self._config, "/jobs/", post_data) |
|
198
|
|
|
self.result_sent = True |
|
199
|
|
|
|
|
200
|
|
|
def send_fail_result(self, info_student, info_tutor): |
|
201
|
|
|
self._send_result(info_student, info_tutor, UNSPECIFIC_ERROR) |
|
202
|
|
|
|
|
203
|
|
|
def send_pass_result(self, |
|
204
|
|
|
info_student="All tests passed. Awesome!", |
|
205
|
|
|
info_tutor="All tests passed."): |
|
206
|
|
|
self._send_result(info_student, info_tutor, 0) |
|
207
|
|
|
|
|
208
|
|
|
def delete_binaries(self): |
|
209
|
|
|
''' |
|
210
|
|
|
Scans the submission files in the self.working_dir for |
|
211
|
|
|
binaries and deletes them. |
|
212
|
|
|
Returns the list of deleted files. |
|
213
|
|
|
''' |
|
214
|
|
|
raise NotImplementedError |
|
215
|
|
|
|
|
216
|
|
|
def run_configure(self, mandatory=True): |
|
217
|
|
|
''' |
|
218
|
|
|
Runs the configure tool configured for the machine in self.working_dir. |
|
219
|
|
|
''' |
|
220
|
|
|
if not has_file(self.working_dir, 'configure'): |
|
221
|
|
|
if mandatory: |
|
222
|
|
|
raise FileNotFoundError( |
|
223
|
|
|
"Could not find a configure script for execution.") |
|
224
|
|
|
else: |
|
225
|
|
|
return |
|
226
|
|
|
try: |
|
227
|
|
|
prog = RunningProgram(self, 'configure') |
|
228
|
|
|
prog.expect_exit_status(0) |
|
229
|
|
|
except Exception: |
|
230
|
|
|
if mandatory: |
|
231
|
|
|
raise |
|
232
|
|
|
|
|
233
|
|
|
def run_make(self, mandatory=True): |
|
234
|
|
|
''' |
|
235
|
|
|
Runs the make tool configured for the machine in self.working_dir. |
|
236
|
|
|
''' |
|
237
|
|
|
if not has_file(self.working_dir, 'Makefile'): |
|
238
|
|
|
if mandatory: |
|
239
|
|
|
raise FileNotFoundError("Could not find a Makefile.") |
|
240
|
|
|
else: |
|
241
|
|
|
return |
|
242
|
|
|
try: |
|
243
|
|
|
prog = RunningProgram(self, 'make') |
|
244
|
|
|
prog.expect_exit_status(0) |
|
245
|
|
|
except Exception: |
|
246
|
|
|
if mandatory: |
|
247
|
|
|
raise |
|
248
|
|
|
|
|
249
|
|
|
def run_compiler(self, compiler=GCC, inputs=None, output=None): |
|
250
|
|
|
''' |
|
251
|
|
|
Runs the compiler in self.working_dir. |
|
252
|
|
|
''' |
|
253
|
|
|
# Let exceptions travel through |
|
254
|
|
|
prog = RunningProgram(self, *compiler_cmdline(compiler=compiler, |
|
255
|
|
|
inputs=inputs, |
|
256
|
|
|
output=output)) |
|
257
|
|
|
prog.expect_exit_status(0) |
|
258
|
|
|
|
|
259
|
|
|
def run_build(self, compiler=GCC, inputs=None, output=None): |
|
260
|
|
|
logger.info("Running build steps ...") |
|
261
|
|
|
self.run_configure(mandatory=False) |
|
262
|
|
|
self.run_make(mandatory=False) |
|
263
|
|
|
self.run_compiler(compiler=compiler, |
|
264
|
|
|
inputs=inputs, |
|
265
|
|
|
output=output) |
|
266
|
|
|
|
|
267
|
|
|
def spawn_program(self, name, arguments=[], timeout=30, exclusive=False): |
|
268
|
|
|
''' |
|
269
|
|
|
Spawns a program in the working directory and allows |
|
270
|
|
|
interaction with it. Returns a RunningProgram object. |
|
271
|
|
|
|
|
272
|
|
|
The caller can demand exclusive execution on this machine. |
|
273
|
|
|
''' |
|
274
|
|
|
logger.debug("Spawning program for interaction ...") |
|
275
|
|
|
if exclusive: |
|
276
|
|
|
kill_longrunning(self.config) |
|
277
|
|
|
|
|
278
|
|
|
return RunningProgram(self, name, arguments, timeout) |
|
279
|
|
|
|
|
280
|
|
|
def run_program(self, name, arguments=[], timeout=30, exclusive=False): |
|
281
|
|
|
''' |
|
282
|
|
|
Runs a program in the working directory. |
|
283
|
|
|
The result is a tuple of exit code and output. |
|
284
|
|
|
|
|
285
|
|
|
The caller can demand exclusive execution on this machine. |
|
286
|
|
|
''' |
|
287
|
|
|
logger.debug("Running program ...") |
|
288
|
|
|
if exclusive: |
|
289
|
|
|
kill_longrunning(self.config) |
|
290
|
|
|
|
|
291
|
|
|
prog = RunningProgram(self, name, arguments, timeout) |
|
292
|
|
|
return prog.expect_end() |
|
293
|
|
|
|
|
294
|
|
|
def grep(self, regex): |
|
295
|
|
|
''' |
|
296
|
|
|
Searches the student files in self.working_dir for files |
|
297
|
|
|
containing a specific regular expression. |
|
298
|
|
|
|
|
299
|
|
|
Returns the names of the matching files as list. |
|
300
|
|
|
''' |
|
301
|
|
|
matches = [] |
|
302
|
|
|
logger.debug("Searching student files for '{0}'".format(regex)) |
|
303
|
|
|
for fname in self.student_files: |
|
304
|
|
|
if os.path.isfile(self.working_dir + fname): |
|
305
|
|
|
for line in open(self.working_dir + fname, 'br'): |
|
306
|
|
|
if re.search(regex.encode(), line): |
|
307
|
|
|
logger.debug("{0} contains '{1}'".format(fname, regex)) |
|
308
|
|
|
matches.append(fname) |
|
309
|
|
|
return matches |
|
310
|
|
|
|
|
311
|
|
|
def ensure_files(self, filenames): |
|
312
|
|
|
''' |
|
313
|
|
|
Searches the student submission for specific files. |
|
314
|
|
|
Expects a list of filenames. Returns a boolean indicator. |
|
315
|
|
|
''' |
|
316
|
|
|
logger.debug("Testing {0} for the following files: {1}".format( |
|
317
|
|
|
self.working_dir, filenames)) |
|
318
|
|
|
dircontent = os.listdir(self.working_dir) |
|
319
|
|
|
for fname in filenames: |
|
320
|
|
|
if fname not in dircontent: |
|
321
|
|
|
return False |
|
322
|
|
|
return True |
|
323
|
|
|
|
|
324
|
|
|
|
|
325
|
|
|
def fetch(url, fullpath): |
|
326
|
|
|
''' |
|
327
|
|
|
Fetch data from an URL and save it under the given target name. |
|
328
|
|
|
''' |
|
329
|
|
|
logger.debug("Fetching %s from %s" % (fullpath, url)) |
|
330
|
|
|
|
|
331
|
|
|
tmpfile, headers = urlretrieve(url) |
|
332
|
|
|
if os.path.exists(fullpath): |
|
333
|
|
|
os.remove(fullpath) |
|
334
|
|
|
shutil.move(tmpfile, fullpath) |
|
335
|
|
|
|
|
336
|
|
|
|
|
337
|
|
|
def send_post(config, urlpath, post_data): |
|
338
|
|
|
''' |
|
339
|
|
|
Send POST data to an OpenSubmit server url path, |
|
340
|
|
|
according to the configuration. |
|
341
|
|
|
''' |
|
342
|
|
|
server = config.get("Server", "url") |
|
343
|
|
|
post_data = urlencode(post_data) |
|
344
|
|
|
post_data = post_data.encode("utf-8", errors="ignore") |
|
345
|
|
|
url = server + urlpath |
|
346
|
|
|
try: |
|
347
|
|
|
urlopen(url, post_data) |
|
348
|
|
|
except Exception as e: |
|
349
|
|
|
logger.error('Error while sending data to server: ' + str(e)) |
|
350
|
|
|
|
|
351
|
|
|
|
|
352
|
|
|
def send_hostinfo(config): |
|
353
|
|
|
''' |
|
354
|
|
|
Register this host on OpenSubmit test machine. |
|
355
|
|
|
''' |
|
356
|
|
|
info = all_host_infos() |
|
357
|
|
|
logger.debug("Sending host information: " + str(info)) |
|
358
|
|
|
post_data = [("Config", json.dumps(info)), |
|
359
|
|
|
("Action", "get_config"), |
|
360
|
|
|
("UUID", config.get("Server", "uuid")), |
|
361
|
|
|
("Address", ipaddress()), |
|
362
|
|
|
("Secret", config.get("Server", "secret")) |
|
363
|
|
|
] |
|
364
|
|
|
|
|
365
|
|
|
send_post(config, "/machines/", post_data) |
|
366
|
|
|
|
|
367
|
|
|
|
|
368
|
|
|
def compatible_api_version(server_version): |
|
369
|
|
|
''' |
|
370
|
|
|
Check if this server API version is compatible to us. |
|
371
|
|
|
''' |
|
372
|
|
|
try: |
|
373
|
|
|
semver = server_version.split('.') |
|
374
|
|
|
if semver[0] != '1': |
|
375
|
|
|
logger.error( |
|
376
|
|
|
'Server API version (%s) is too new for us. Please update the executor installation.' % server_version) |
|
377
|
|
|
return False |
|
378
|
|
|
else: |
|
379
|
|
|
return True |
|
380
|
|
|
except Exception: |
|
381
|
|
|
logger.error( |
|
382
|
|
|
'Cannot understand the server API version (%s). Please update the executor installation.' % server_version) |
|
383
|
|
|
return False |
|
384
|
|
|
|
|
385
|
|
|
|
|
386
|
|
|
def fetch_job(config): |
|
387
|
|
|
''' |
|
388
|
|
|
Fetch any available work from the OpenSubmit server and |
|
389
|
|
|
return an according job object. |
|
390
|
|
|
|
|
391
|
|
|
Returns None if no work is available. |
|
392
|
|
|
|
|
393
|
|
|
Errors are reported by this function directly. |
|
394
|
|
|
''' |
|
395
|
|
|
url = "%s/jobs/?Secret=%s&UUID=%s" % (config.get("Server", "url"), |
|
396
|
|
|
config.get("Server", "secret"), |
|
397
|
|
|
config.get("Server", "uuid")) |
|
398
|
|
|
|
|
399
|
|
|
try: |
|
400
|
|
|
# Fetch information from server |
|
401
|
|
|
result = urlopen(url) |
|
402
|
|
|
headers = result.info() |
|
403
|
|
|
if not compatible_api_version(headers["APIVersion"]): |
|
404
|
|
|
# No proper reporting possible, so only logging. |
|
405
|
|
|
logger.error("Incompatible API version. Please update OpenSubmit.") |
|
406
|
|
|
return None |
|
407
|
|
|
|
|
408
|
|
|
if headers["Action"] == "get_config": |
|
409
|
|
|
# The server does not know us, |
|
410
|
|
|
# so it demands registration before hand. |
|
411
|
|
|
logger.info("Machine unknown on server, sending registration ...") |
|
412
|
|
|
send_hostinfo(config) |
|
413
|
|
|
return None |
|
414
|
|
|
|
|
415
|
|
|
# Create job object with information we got |
|
416
|
|
|
job = Job(config) |
|
417
|
|
|
|
|
418
|
|
|
job.submitter_name = headers['SubmitterName'] |
|
419
|
|
|
job.author_names = headers['AuthorNames'] |
|
420
|
|
|
job.submitter_studyprogram = headers['SubmitterStudyProgram'] |
|
421
|
|
|
job.course = headers['Course'] |
|
422
|
|
|
job.assignment = headers['Assignment'] |
|
423
|
|
|
job.action = headers["Action"] |
|
424
|
|
|
job.file_id = headers["SubmissionFileId"] |
|
425
|
|
|
job.sub_id = headers["SubmissionId"] |
|
426
|
|
|
job.file_name = headers["SubmissionOriginalFilename"] |
|
427
|
|
|
job.submitter_student_id = headers["SubmitterStudentId"] |
|
428
|
|
|
if "Timeout" in headers: |
|
429
|
|
|
job.timeout = int(headers["Timeout"]) |
|
430
|
|
|
if "PostRunValidation" in headers: |
|
431
|
|
|
job.validator_url = headers["PostRunValidation"] |
|
432
|
|
|
job.working_dir = create_working_dir(config, job.sub_id) |
|
433
|
|
|
|
|
434
|
|
|
# Store submission in working directory |
|
435
|
|
|
submission_fname = job.working_dir + job.file_name |
|
436
|
|
|
with open(submission_fname, 'wb') as target: |
|
437
|
|
|
target.write(result.read()) |
|
438
|
|
|
assert(os.path.exists(submission_fname)) |
|
439
|
|
|
|
|
440
|
|
|
# Store validator package in working directory |
|
441
|
|
|
validator_fname = job.working_dir + 'download.validator' |
|
442
|
|
|
fetch(job.validator_url, validator_fname) |
|
443
|
|
|
|
|
444
|
|
|
try: |
|
445
|
|
|
prepare_working_directory(job, submission_fname, validator_fname) |
|
446
|
|
|
except JobException as e: |
|
447
|
|
|
job.send_fail_result(e.info_student, e.info_tutor) |
|
448
|
|
|
return None |
|
449
|
|
|
logger.debug("Got job: " + str(job)) |
|
450
|
|
|
return job |
|
451
|
|
|
except HTTPError as e: |
|
452
|
|
|
if e.code == 404: |
|
453
|
|
|
logger.debug("Nothing to do.") |
|
454
|
|
|
return None |
|
455
|
|
|
except URLError as e: |
|
456
|
|
|
logger.error("Error while contacting {0}: {1}".format(url, str(e))) |
|
457
|
|
|
return None |
|
458
|
|
|
|
|
459
|
|
|
|
|
460
|
|
|
def fake_fetch_job(config, src_dir): |
|
461
|
|
|
''' |
|
462
|
|
|
Act like fetch_job, but take the validator file and the student |
|
463
|
|
|
submission files directly from a directory. |
|
464
|
|
|
|
|
465
|
|
|
Intended for testing purposes when developing test scripts. |
|
466
|
|
|
|
|
467
|
|
|
Check also cmdline.py. |
|
468
|
|
|
''' |
|
469
|
|
|
logger.debug("Creating fake job from " + src_dir) |
|
470
|
|
|
job = Job(config, online=False) |
|
471
|
|
|
job.working_dir = create_working_dir(config, '42') |
|
472
|
|
|
for fname in glob.glob(src_dir + os.sep + '*'): |
|
473
|
|
|
logger.debug("Copying {0} to {1} ...".format(fname, job.working_dir)) |
|
474
|
|
|
shutil.copy(fname, job.working_dir) |
|
475
|
|
|
case_files = glob.glob(job.working_dir + os.sep + '*') |
|
476
|
|
|
assert(len(case_files) == 2) |
|
477
|
|
|
if os.path.basename(case_files[0]) in ['validator.py', 'validator.zip']: |
|
478
|
|
|
validator = case_files[0] |
|
479
|
|
|
submission = case_files[1] |
|
480
|
|
|
else: |
|
481
|
|
|
validator = case_files[1] |
|
482
|
|
|
submission = case_files[0] |
|
483
|
|
|
logger.debug('{0} is the validator.'.format(validator)) |
|
484
|
|
|
logger.debug('{0} the submission.'.format(submission)) |
|
485
|
|
|
try: |
|
486
|
|
|
prepare_working_directory(job, |
|
487
|
|
|
submission_path=submission, |
|
488
|
|
|
validator_path=validator) |
|
489
|
|
|
except JobException as e: |
|
490
|
|
|
job.send_fail_result(e.info_student, e.info_tutor) |
|
491
|
|
|
return None |
|
492
|
|
|
logger.debug("Got fake job: " + str(job)) |
|
493
|
|
|
return job |
|
494
|
|
|
|