Communication   A
last analyzed

Complexity

Total Complexity 27

Size/Duplication

Total Lines 278
Duplicated Lines 42.45 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 118
loc 278
rs 10
wmc 27

17 Methods

Rating   Name   Duplication   Size   Complexity  
A _register_executor() 0 3 1
A test_assignment_specific_test_machine() 0 23 1
A _run_executor() 0 2 1
A setUp() 0 6 1
A test_full_test() 0 13 1
B test_inconsistent_state_email() 0 24 1
A test_single_file_validator_test() 0 19 1
A test_output_logging() 17 17 1
A test_register_executor_explicit() 0 4 2
A test_invalid_secret() 0 3 1
B test_parallel_executors_test() 0 26 4
B test_too_long_full_test() 26 26 2
B test_broken_validator_feedback() 25 25 2
B test_too_long_validation() 27 27 2
A test_ungraded_validation_email() 23 23 3
A _register_test_machine() 0 12 1
A test_everything_already_tested() 0 4 2

How to fix   Duplicated Code   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

1
'''
2
    Test cases for the executor code.
3
4
    Note the neccessary python search path manipulation below.
5
6
Missing tests:
7
8
  - Makefile in validator package should override student Makefile
9
10
'''
11
12
import os
13
import os.path
14
import sys
15
import logging
16
17
from django.core import mail
18
from django.conf import settings
19
from django.test import TestCase
20
from django.test.utils import override_settings
21
from opensubmit.tests.cases import SubmitStudentScenarioTestCase
22
from django.core.urlresolvers import reverse
23
24
from opensubmit.models import TestMachine, SubmissionTestResult, Submission
25
from opensubmit.tests import utils
26
27
from .helpers.submission import create_validatable_submission
28
from .helpers.submission import create_validated_submission
29
from .helpers.assignment import create_validated_assignment
30
from .helpers.assignment import create_pass_fail_grading
31
from .helpers.assignment import create_validated_assignment_with_file
32
from .helpers.djangofiles import create_submission_file
33
from .helpers.user import create_user, get_student_dict
34
from . import uccrap, rootdir
35
36
sys.path.insert(0, os.path.dirname(__file__) + '/../../../executor/')
37
from opensubmitexec import config, cmdline, server, locking, compiler, exceptions  # NOQA
38
39
logger = logging.getLogger('opensubmitexec')
40
41
42
class CmdLine(TestCase):
43
    '''
44
    Test cases for the executor command-line script.
45
    '''
46
    def setUp(self):
47
        self.config = config.read_config(
48
            os.path.dirname(__file__) + "/executor.cfg")
49
50
    def test_help_call_without_config(self):
51
        # simulate 'help' call
52
        sys.argv = ['opensubmit-exec', 'help']
53
        cmdline.console_script()
54
55
    def test_unlock_call_without_lock(self):
56
        # simulate 'unlock' call without existing lock
57
        sys.argv = ['opensubmit-exec', 'unlock']
58
        cmdline.console_script()
59
60
    def test_unlock_call_with_lock(self):
61
        # simulate 'unlock' call with existing lock
62
        sys.argv = ['opensubmit-exec', 'unlock']
63
        with locking.ScriptLock(self.config):
64
            cmdline.console_script()
65
66
67
class Library(SubmitStudentScenarioTestCase):
68
    '''
69
    Tests for the executor library functions used by the validator script.
70
    '''
71
    def setUp(self):
72
        settings.MAIN_URL = self.live_server_url
73
        super(Library, self).setUp()
74
        self.config = config.read_config(
75
            os.path.dirname(__file__) + "/executor.cfg",
76
            override_url=self.live_server_url)
77
78
    def _register_executor(self):
79
        server.send_hostinfo(self.config)
80
        return TestMachine.objects.order_by('-last_contact')[0]
81
82
    def _run_executor(self):
83
        return cmdline.download_and_run(self.config)
84
85
    def test_send_result(self):
86
        sf = create_submission_file()
87
        sub = create_validatable_submission(
88
            self.user, self.validated_assignment, sf)
89
        test_machine = self._register_executor()
90
        sub.assignment.test_machines.add(test_machine)
91
92
        job = server.fetch_job(self.config)
93
        self.assertNotEquals(None, job)
94
95
        msg = uccrap + "Message from validation script."
96
97
        job.send_pass_result(info_student=msg)
98
99
        db_entries = SubmissionTestResult.objects.filter(
100
            kind=SubmissionTestResult.VALIDITY_TEST)
101
102
        self.assertEqual(db_entries[0].result, msg)
103
104
    def test_fetch_job_renaming(self):
105
        test_machine = self._register_executor()
106
        self.validated_assignment.test_machines.add(test_machine)
107
        with open(rootdir + "submfiles/validation/1000fff/helloworld.c", 'rb') as f:
108
            self.c.post('/assignments/%s/new/' % self.validated_assignment.pk, {
109
                'notes': 'This is a test submission.',
110
                'authors': str(self.user.pk),
111
                'attachment': f
112
            })
113
        job = server.fetch_job(self.config)
114
        self.assertIn('helloworld.c', os.listdir(job.working_dir))
115
116
    def test_grep(self):
117
        sf = create_submission_file()
118
        sub = create_validatable_submission(
119
            self.user, self.validated_assignment, sf)
120
        test_machine = self._register_executor()
121
        sub.assignment.test_machines.add(test_machine)
122
        job = server.fetch_job(self.config)
123
        self.assertNotEquals(None, job)
124
        self.assertListEqual(job.grep("World"), ['helloworld.c'])
125
        self.assertListEqual(job.grep("foobar"), [])
126
127
    def test_ensure_files(self):
128
        sf = create_submission_file()
129
        sub = create_validatable_submission(
130
            self.user, self.validated_assignment, sf)
131
        test_machine = self._register_executor()
132
        sub.assignment.test_machines.add(test_machine)
133
        job = server.fetch_job(self.config)
134
        self.assertNotEquals(None, job)
135
        self.assertTrue(job.ensure_files(['helloworld.c']))
136
137
    def test_job_attributes(self):
138
        sf = create_submission_file()
139
        sub = create_validatable_submission(
140
            self.user, self.validated_assignment, sf)
141
        test_machine = self._register_executor()
142
        sub.assignment.test_machines.add(test_machine)
143
        job = server.fetch_job(self.config)
144
        self.assertNotEquals(None, job)
145
        self.assertEquals(job.timeout, self.validated_assignment.attachment_test_timeout)
146
        self.assertEquals(job.sub_id, str(sub.pk))
147
        self.assertEquals(job.file_id, str(sf.pk))
148
        self.assertEquals(job.submitter_name, sub.submitter.get_full_name())
149
        self.assertEquals(job.submitter_student_id, str(sub.submitter.profile.student_id))
150
        self.assertEquals(job.submitter_studyprogram, str(sub.submitter.profile.study_program))
151
        self.assertEquals(job.course, str(self.course))
152
        self.assertEquals(job.assignment, str(self.validated_assignment))
153
154
    def test_wrong_compile_call(self):
155
        sf = create_submission_file()
156
        sub = create_validatable_submission(
157
            self.user, self.validated_assignment, sf)
158
        test_machine = self._register_executor()
159
        sub.assignment.test_machines.add(test_machine)
160
        job = server.fetch_job(self.config)
161
        # Missing output
162
        self.assertRaises(exceptions.ValidatorBrokenException, job.run_compiler, inputs=['input.c'])
163
        # Missing input
164
        self.assertRaises(exceptions.ValidatorBrokenException, job.run_compiler, output='output')
165
        # Wrong file, determined by compiler
166
        self.assertRaises(exceptions.WrongExitStatusException, job.run_compiler, inputs=['foo.c'], output='output')
167
168
    def test_cleanup(self):
169
        '''
170
        If configured, then the executor should remove all temporary
171
        files after finishing.
172
        '''
173
        for do_cleanup in [False, True]:
174
            sf = create_submission_file()
175
            sub = create_validatable_submission(
176
                self.user, self.validated_assignment, sf)
177
            test_machine = self._register_executor()
178
            sub.assignment.test_machines.add(test_machine)
179
            job = server.fetch_job(self.config)
180
            self.assertNotEquals(None, job)
181
            working_dir = job.working_dir
182
            job._config.set("Execution", "cleanup", str(do_cleanup))
183
            job._run_validate()
184
            if do_cleanup:
185
                with self.assertRaises(FileNotFoundError):
186
                    os.listdir(working_dir)
187
            else:
188
                self.assertNotEqual(os.listdir(working_dir), [])
189
190
191
192
class Validation(TestCase):
193
    '''
194
    Tests for the execution of validation scripts by the executor.
195
    '''
196
197
    def setUp(self):
198
        super(Validation, self).setUp()
199
        self.config = config.read_config(
200
            os.path.dirname(__file__) + "/executor.cfg")
201
202
    def _test_validation_case(self, directory):
203
        '''
204
        Each of the validator.py files uses the Python assert()
205
        statement to check by itself if the result is the expected
206
        one for its case.
207
        '''
208
        base_dir = os.path.dirname(__file__) + '/submfiles/validation/'
209
        cmdline.copy_and_run(self.config, base_dir + directory)
210
211
    def test_0100fff(self):
212
        self._test_validation_case('0100fff')
213
214
    def test_0100tff(self):
215
        self._test_validation_case('0100tff')
216
217
    def test_0100ttf(self):
218
        self._test_validation_case('0100ttf')
219
220
    def test_1000fff(self):
221
        self._test_validation_case('1000fff')
222
223
    def test_1000fft(self):
224
        self._test_validation_case('1000fft')
225
226
    def test_1000tff(self):
227
        self._test_validation_case('1000tff')
228
229
    def test_1000tft(self):
230
        self._test_validation_case('1000tft')
231
232
    def test_1000ttf(self):
233
        self._test_validation_case('1000ttf')
234
235
    def test_1000ttt(self):
236
        self._test_validation_case('1000ttt')
237
238
    def test_1010tff(self):
239
        self._test_validation_case('1010tff')
240
241
    def test_1010ttf(self):
242
        self._test_validation_case('1010ttf')
243
244
    def test_1100tff(self):
245
        self._test_validation_case('1100tff')
246
247
    def test_1100ttf(self):
248
        self._test_validation_case('1100ttf')
249
250
    def test_3000tff(self):
251
        self._test_validation_case('3000tff')
252
253
    def test_3000ttf(self):
254
        self._test_validation_case('3000ttf')
255
256
    def test_3010tff(self):
257
        self._test_validation_case('3010tff')
258
259
    def test_3010ttf(self):
260
        self._test_validation_case('3010ttf')
261
262
    def test_b000tff(self):
263
        self._test_validation_case('b000tff')
264
265
    def test_b010tff(self):
266
        self._test_validation_case('b010tff')
267
268
    def test_1000tfm(self):
269
        self._test_validation_case('1000tfm')
270
271
    def test_regressions(self):
272
        self._test_validation_case('regression_001')
273
274
275
class Communication(SubmitStudentScenarioTestCase):
276
    '''
277
    Tests for the communication of the executor with the OpenSubmit server.
278
    '''
279
280
    def setUp(self):
281
        settings.MAIN_URL = self.live_server_url
282
        super(Communication, self).setUp()
283
        self.config = config.read_config(
284
            os.path.dirname(__file__) + "/executor.cfg",
285
            override_url=self.live_server_url)
286
287
    def _register_executor(self):
288
        server.send_hostinfo(self.config)
289
        return TestMachine.objects.order_by('-last_contact')[0]
290
291
    def _run_executor(self):
292
        return cmdline.download_and_run(self.config)
293
294
    def _register_test_machine(self):
295
        '''
296
        Utility step for a common test case preparation:
297
        - Create validatable submission
298
        - Register a test machine for it
299
        '''
300
        sf = create_submission_file()
301
        sub = create_validatable_submission(
302
            self.user, self.validated_assignment, sf)
303
        test_machine = self._register_executor()
304
        sub.assignment.test_machines.add(test_machine)
305
        return sub
306
307
    def test_register_executor_explicit(self):
308
        machine_count = TestMachine.objects.all().count()
309
        assert(self._register_executor().pk)
310
        self.assertEqual(machine_count + 1, TestMachine.objects.all().count())
311
312
    @override_settings(JOB_EXECUTOR_SECRET='foo')
313
    def test_invalid_secret(self):
314
        self.assertNotEqual(True, self._run_executor())
315
316
    def test_everything_already_tested(self):
317
        create_validated_submission(self.user, self.validated_assignment)
318
        assert(self._register_executor().pk)
319
        self.assertEqual(False, self._run_executor())
320
321
    def test_parallel_executors_test(self):
322
        NUM_PARALLEL = 3
323
        self.validated_assignment.test_machines.add(self._register_executor())
324
        subs = []
325
        for i in range(1, NUM_PARALLEL + 1):
326
            stud = create_user(get_student_dict(i))
327
            self.course.participants.add(stud.profile)
328
            self.course.save()
329
            sf = create_submission_file()
330
            subs.append(create_validatable_submission(
331
                stud, self.validated_assignment, sf))
332
333
        # Span a number of threads, each triggering the executor
334
        # This only creates a real test case if executor serialization
335
        # is off (see tests/executor.cfg)
336
        return_codes = utils.run_parallel(len(subs), self._run_executor)
337
        self.assertEqual(
338
            len(list(filter((lambda x: x is True), return_codes))),
339
            len(subs))
340
341
        for sub in subs:
342
            results = SubmissionTestResult.objects.filter(
343
                kind=SubmissionTestResult.VALIDITY_TEST
344
            )
345
            self.assertEqual(NUM_PARALLEL, len(results))
346
            self.assertNotEqual(0, len(results[0].result))
347
348 View Code Duplication
    def test_too_long_validation(self):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
349
        from django.core import mail
350
351
        grading = create_pass_fail_grading()
352
        assignment = create_validated_assignment(
353
            self.course, grading, "/submfiles/validation/d000fff/", "validator_run.py")
354
        assignment.attachment_test_timeout = 1
355
        assignment.save()
356
        sf = create_submission_file("/submfiles/validation/d000fff/helloworld.c")
357
        sub = create_validatable_submission(
358
            self.user, assignment, sf)
359
        test_machine = self._register_executor()
360
        sub.assignment.test_machines.add(test_machine)
361
362
        # Fire up the executor, should mark the submission as timed out
363
        self.assertEqual(True, self._run_executor())
364
        # Check if timeout marking took place
365
        sub.refresh_from_db()
366
        self.assertEqual(sub.state, Submission.TEST_VALIDITY_FAILED)
367
        text = sub.get_validation_result().result
368
        self.assertIn("took too long", text)
369
        # Check mail outbox for student information
370
        self.assertEqual(1, len(mail.outbox))
371
        for email in mail.outbox:
372
            self.assertIn("Validation failed", email.subject)
373
            self.assertIn("failed", email.body)
374
            self.assertIn("localhost", email.body)
375
376 View Code Duplication
    def test_broken_validator_feedback(self):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
377
        from django.core import mail
378
379
        grading = create_pass_fail_grading()
380
        assignment = create_validated_assignment(
381
            self.course, grading, "/submfiles/validation/1000tfm/", "validator.zip")
382
        assignment.save()
383
        sf = create_submission_file("/submfiles/validation/1000tfm/packed.zip")
384
        sub = create_validatable_submission(
385
            self.user, assignment, sf)
386
        test_machine = self._register_executor()
387
        sub.assignment.test_machines.add(test_machine)
388
389
        # Fire up the executor
390
        self.assertEqual(False, self._run_executor())
391
        sub.refresh_from_db()
392
        self.assertEqual(sub.state, Submission.TEST_VALIDITY_FAILED)
393
        text = sub.get_validation_result().result
394
        self.assertIn("Internal error", text)
395
        # Check mail outbox for student information
396
        self.assertEqual(1, len(mail.outbox))
397
        for email in mail.outbox:
398
            self.assertIn("Validation failed", email.subject)
399
            self.assertIn("failed", email.body)
400
            self.assertIn("localhost", email.body)
401
402 View Code Duplication
    def test_output_logging(self):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
403
        grading = create_pass_fail_grading()
404
        assignment = create_validated_assignment(
405
            self.course, grading, "/submfiles/validation/1000fff/", "validator.py")
406
        assignment.save()
407
        sf = create_submission_file("/submfiles/validation/1000fff/helloworld.c")
408
        sub = create_validatable_submission(
409
            self.user, assignment, sf)
410
        test_machine = self._register_executor()
411
        sub.assignment.test_machines.add(test_machine)
412
        # Fire up the executor
413
        self.assertEqual(True, self._run_executor())
414
        sub.refresh_from_db()
415
        self.assertEqual(sub.state, Submission.SUBMITTED_TESTED)
416
        text = sub.get_validation_result().result
417
        self.assertIn("quick brown fox", text)
418
        self.assertIn("provide your input", text)
419
420 View Code Duplication
    def test_too_long_full_test(self):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
421
        grading = create_pass_fail_grading()
422
        assignment = create_validated_assignment(
423
            self.course, 
424
            grading, 
425
            "/submfiles/validation/d000fff/",
426
            "validator_build.py",
427
            "validator_run.py")
428
        assignment.attachment_test_timeout = 1
429
        assignment.save()
430
        sf = create_submission_file("/submfiles/validation/d000fff/helloworld.c")
431
        sub = create_validatable_submission(
432
            self.user, assignment, sf)
433
        test_machine = self._register_executor()
434
        sub.assignment.test_machines.add(test_machine)
435
436
        # Fire up the executor for validation
437
        self.assertEqual(True, self._run_executor())
438
        # Fire up the executor for full test
439
        self.assertEqual(True, self._run_executor())
440
        # Check if timeout marking took place
441
        sub.refresh_from_db()
442
        self.assertEqual(sub.state, Submission.TEST_FULL_FAILED)
443
        assert("timeout" in sub.get_fulltest_result().result_tutor)
444
        # Failed full tests shall not be reported
445
        self.assertEqual(0, len(mail.outbox))
446
447
    def test_single_file_validator_test(self):
448
        sub = self._register_test_machine()
449
450
        self.assertEqual(True, self._run_executor())
451
        results = SubmissionTestResult.objects.filter(
452
            submission_file=sub.file_upload,
453
            kind=SubmissionTestResult.VALIDITY_TEST
454
        )
455
        self.assertEqual(1, len(results))
456
457
        self.assertEqual(True, self._run_executor())
458
        results = SubmissionTestResult.objects.filter(
459
            submission_file=sub.file_upload,
460
            kind=SubmissionTestResult.VALIDITY_TEST
461
        )
462
        self.assertEqual(1, len(results))
463
        self.assertNotEqual(0, len(results[0].result))
464
        # Graded assignment, so no mail at this stage
465
        self.assertEqual(0, len(mail.outbox))
466
467 View Code Duplication
    def test_ungraded_validation_email(self):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
468
        from django.core import mail
469
470
        sf = create_submission_file()
471
        assign = create_validated_assignment_with_file(self.course, None)
472
        sub = create_validatable_submission(self.user, assign, sf)
473
        test_machine = self._register_executor()
474
        assign.test_machines.add(test_machine)
475
        # has no grading scheme, so it is not graded
476
        assert(not sub.assignment.is_graded())
477
478
        # Fire up the executor for validation
479
        self.assertEqual(True, self._run_executor())
480
481
        # Fire up the executor for full test
482
        self.assertEqual(True, self._run_executor())
483
484
        # Check mail outbox for student information
485
        self.assertEqual(1, len(mail.outbox))
486
        for email in mail.outbox:
487
            self.assertIn("Validation successful", email.subject)
488
            self.assertIn("passed", email.body)
489
            self.assertIn("localhost", email.body)
490
491
    def test_full_test(self):
492
        sub = self._register_test_machine()
493
        # validation test
494
        self.assertEqual(True, self._run_executor())
495
        # full test
496
        self.assertEqual(True, self._run_executor())
497
        results = SubmissionTestResult.objects.filter(
498
            submission_file=sub.file_upload,
499
            kind=SubmissionTestResult.FULL_TEST
500
        )
501
        self.assertEqual(1, len(results))
502
        self.assertNotEqual(0, len(results[0].result_tutor))
503
        self.assertEqual(None, results[0].result)
504
505
    def test_inconsistent_state_email(self):
506
        '''
507
        Test operator email on inconsistent state.
508
        Since executor execution and submission sending is one step,
509
        we need to mock the incoming invalid executor request.
510
        '''
511
        sf = create_submission_file()
512
        self.sub = create_validatable_submission(
513
            self.user, self.validated_assignment, sf)
514
        test_machine = self._register_executor()
515
        self.sub.assignment.test_machines.add(test_machine)
516
        self.sub.state = Submission.TEST_FULL_PENDING
517
        self.sub.save()
518
        post_data = {'Secret': settings.JOB_EXECUTOR_SECRET,
519
                     'UUID': test_machine.host,
520
                     'Action': 'test_compile',
521
                     'SubmissionFileId': self.sub.file_upload.pk,
522
                     'PerfData': '',
523
                     'ErrorCode': 0,
524
                     'Message': 'In A Bottle'}
525
        self.c.post(reverse('jobs'), post_data)
526
        self.assertNotEqual(len(mail.outbox), 0)
527
        self.assertIn('Action reported by the executor: test_compile',
528
                      mail.outbox[0].message().as_string())
529
530
    def test_assignment_specific_test_machine(self):
531
        # Register two test machines T1 and T2
532
        real_machine = self._register_executor()
533
        fake_machine = TestMachine(host="127.0.0.2")
534
        fake_machine.save()
535
        # Assign each of them to a different assignment
536
        self.open_assignment.test_machines.add(real_machine)
537
        self.validated_assignment.test_machines.add(fake_machine)
538
        # Produce submission for the assignment linked to fake_machine
539
        sub1 = Submission(
540
            assignment=self.validated_assignment,
541
            submitter=self.user,
542
            state=Submission.TEST_VALIDITY_PENDING,
543
            file_upload=create_submission_file()
544
        )
545
        sub1.save()
546
        # Run real_machine executor, should not react on this submission
547
        old_sub1_state = sub1.state
548
        self.assertEqual(False, self._run_executor())
549
        # Make sure that submission object was not touched,
550
        # whatever the executor says
551
        sub1 = Submission.objects.get(pk=sub1.pk)
552
        self.assertEqual(old_sub1_state, sub1.state)
553