Total Complexity | 40 |
Total Lines | 1010 |
Duplicated Lines | 3.37 % |
Changes | 0 |
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:
Complex classes like validation.tests.test_tasks often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
1 | #!/usr/bin/env python3 |
||
2 | # -*- coding: utf-8 -*- |
||
3 | """ |
||
4 | Created on Fri Oct 5 11:39:21 2018 |
||
5 | |||
6 | @author: Paolo Cozzi <[email protected]> |
||
7 | """ |
||
8 | |||
9 | import json |
||
10 | |||
11 | from collections import Counter |
||
12 | from unittest.mock import patch, Mock |
||
13 | from billiard.einfo import ExceptionInfo |
||
14 | from celery.exceptions import Retry |
||
15 | from pytest import raises |
||
16 | from image_validation.ValidationResult import ( |
||
17 | ValidationResultColumn, ValidationResultRecord) |
||
18 | |||
19 | from django.core import mail |
||
20 | from django.test import TestCase |
||
21 | |||
22 | from common.constants import LOADED, ERROR, READY, NEED_REVISION, COMPLETED |
||
23 | from common.tests import WebSocketMixin |
||
24 | from common.tests import PersonMixinTestCase |
||
25 | from image_app.models import Submission, Person, Name, Animal, Sample |
||
26 | |||
27 | from ..tasks import ValidateTask, ValidationError, ValidateSubmission |
||
28 | from ..helpers import OntologyCacheError, RulesetError |
||
29 | from ..models import ValidationSummary |
||
30 | from .common import PickableMock, MetaDataValidationTestMixin |
||
31 | |||
32 | |||
33 | class ValidateSubmissionMixin( |
||
34 | PersonMixinTestCase, MetaDataValidationTestMixin): |
||
35 | """A mixin to define common stuff for testing data validation""" |
||
36 | |||
37 | # an attribute for PersonMixinTestCase |
||
38 | person = Person |
||
39 | |||
40 | fixtures = [ |
||
41 | 'image_app/animal', |
||
42 | 'image_app/dictbreed', |
||
43 | 'image_app/dictcountry', |
||
44 | 'image_app/dictrole', |
||
45 | 'image_app/dictsex', |
||
46 | 'image_app/dictspecie', |
||
47 | 'image_app/dictstage', |
||
48 | 'image_app/dictuberon', |
||
49 | 'image_app/name', |
||
50 | 'image_app/ontology', |
||
51 | 'image_app/organization', |
||
52 | 'image_app/publication', |
||
53 | 'image_app/sample', |
||
54 | 'image_app/submission', |
||
55 | 'image_app/user', |
||
56 | "validation/validationresult", |
||
57 | 'validation/validationsummary' |
||
58 | ] |
||
59 | |||
60 | def setUp(self): |
||
61 | # calling base methods |
||
62 | super().setUp() |
||
63 | |||
64 | # get a submission object |
||
65 | self.submission = Submission.objects.get(pk=1) |
||
66 | |||
67 | # set a status which I can validate |
||
68 | self.submission.status = LOADED |
||
69 | self.submission.save() |
||
70 | |||
71 | # track submission ID |
||
72 | self.submission_id = self.submission.id |
||
73 | |||
74 | # track names |
||
75 | self.name_qs = Name.objects.exclude( |
||
76 | name__contains="unknown").order_by("animal", "id") |
||
77 | |||
78 | # track animal and samples |
||
79 | self.animal_qs = Animal.objects.filter( |
||
80 | name__submission=self.submission) |
||
81 | self.sample_qs = Sample.objects.filter( |
||
82 | name__submission=self.submission) |
||
83 | |||
84 | # track animal and samples count |
||
85 | self.n_animals = self.animal_qs.count() |
||
86 | self.n_samples = self.sample_qs.count() |
||
87 | |||
88 | # setting tasks |
||
89 | self.my_task = ValidateTask() |
||
90 | |||
91 | |||
92 | class CustomWebSocketMixin(WebSocketMixin): |
||
93 | """Override setUp to mock websocket objects""" |
||
94 | |||
95 | def check_message( |
||
96 | self, message, notification_message, validation_message=None, |
||
97 | pk=1): |
||
98 | |||
99 | """Check django channels async messages called""" |
||
100 | |||
101 | # defining default validation message. |
||
102 | if not validation_message: |
||
103 | animal_qs = Animal.objects.filter( |
||
104 | name__submission=self.submission) |
||
105 | sample_qs = Sample.objects.filter( |
||
106 | name__submission=self.submission) |
||
107 | |||
108 | validation_message = { |
||
109 | 'animals': self.n_animals, |
||
110 | 'samples': self.n_samples, |
||
111 | 'animal_unkn': animal_qs.filter( |
||
112 | name__validationresult__isnull=True).count(), |
||
113 | 'sample_unkn': sample_qs.filter( |
||
114 | name__validationresult__isnull=True).count(), |
||
115 | 'animal_issues': 0, |
||
116 | 'sample_issues': 0 |
||
117 | } |
||
118 | |||
119 | self.assertEqual(self.asyncio_mock.call_count, 1) |
||
120 | self.assertEqual(self.run_until.run_until_complete.call_count, 1) |
||
121 | self.assertEqual(self.send_msg_ws.call_count, 1) |
||
122 | self.send_msg_ws.assert_called_with( |
||
123 | {'message': message, |
||
124 | 'notification_message': notification_message, |
||
125 | 'validation_message': validation_message}, pk) |
||
126 | |||
127 | |||
128 | class ValidateSubmissionTest(ValidateSubmissionMixin, TestCase): |
||
129 | |||
130 | def setUp(self): |
||
131 | # calling base methods |
||
132 | super().setUp() |
||
133 | |||
134 | # get a submission data object (with no ruleset) |
||
135 | self.submission_data = ValidateSubmission( |
||
136 | self.submission, ruleset=None) |
||
137 | |||
138 | def test_check_valid_statuses(self): |
||
139 | """test validation supporting statuses""" |
||
140 | |||
141 | self.assertTrue(self.submission_data.check_valid_statuses()) |
||
142 | |||
143 | # set a fake status |
||
144 | self.submission_data.animals_statuses['foo'] = 1 |
||
145 | self.assertFalse(self.submission_data.check_valid_statuses()) |
||
146 | |||
147 | # reset and set sample status status |
||
148 | self.submission_data.animals_statuses = Counter() |
||
149 | self.submission_data.samples_statuses['foo'] = 1 |
||
150 | self.assertFalse(self.submission_data.check_valid_statuses()) |
||
151 | |||
152 | def test_has_keys(self): |
||
153 | """Test has error or warning in validation tests""" |
||
154 | |||
155 | self.assertFalse(self.submission_data.has_errors_in_rules()) |
||
156 | self.assertFalse(self.submission_data.has_warnings_in_rules()) |
||
157 | |||
158 | # set a fake status |
||
159 | self.submission_data.animals_statuses['Error'] = 1 |
||
160 | self.submission_data.samples_statuses['Warning'] = 1 |
||
161 | |||
162 | self.assertTrue(self.submission_data.has_errors_in_rules()) |
||
163 | self.assertTrue(self.submission_data.has_warnings_in_rules()) |
||
164 | |||
165 | def test_create_validation_summary(self): |
||
166 | """Create and set validation summary object""" |
||
167 | |||
168 | # get valdiationsummary objects |
||
169 | summary_qs = ValidationSummary.objects.filter( |
||
170 | submission=self.submission) |
||
171 | |||
172 | # wipe out validationsummary objects |
||
173 | summary_qs.delete() |
||
174 | |||
175 | # set up messages |
||
176 | self.submission_data.animals_statuses['Error'] = 1 |
||
177 | self.submission_data.samples_statuses['Warning'] = 1 |
||
178 | |||
179 | self.submission_data.animals_messages['test error'] = [1] |
||
180 | self.submission_data.samples_messages['test warning'] = [1] |
||
181 | |||
182 | self.submission_data.animals_offending_columns['test error'] = \ |
||
183 | 'test_error' |
||
184 | self.submission_data.samples_offending_columns['test warning'] = \ |
||
185 | 'test_warning' |
||
186 | |||
187 | # call function |
||
188 | self.submission_data.create_validation_summary() |
||
189 | |||
190 | # assert I have two objects |
||
191 | self.assertTrue(summary_qs.count(), 2) |
||
192 | |||
193 | # get animal vs |
||
194 | animal_summary = summary_qs.get(type="animal") |
||
195 | self.assertEqual(animal_summary.all_count, self.n_animals) |
||
196 | |||
197 | sample_summary = summary_qs.get(type="sample") |
||
198 | self.assertEqual(sample_summary.all_count, self.n_samples) |
||
199 | |||
200 | |||
201 | class ValidateTaskTest( |
||
202 | CustomWebSocketMixin, ValidateSubmissionMixin, TestCase): |
||
203 | |||
204 | def setUp(self): |
||
205 | # calling base methods |
||
206 | super().setUp() |
||
207 | |||
208 | # mocking task retry |
||
209 | self.validate_retry_patcher = patch( |
||
210 | "validation.tasks.ValidateTask.retry") |
||
211 | self.validate_retry = self.validate_retry_patcher.start() |
||
212 | |||
213 | def tearDown(self): |
||
214 | # stopping my mock objects |
||
215 | self.validate_retry_patcher.stop() |
||
216 | |||
217 | # calling base methods |
||
218 | super().tearDown() |
||
219 | |||
220 | View Code Duplication | def test_on_failure(self): |
|
|
|||
221 | """Testing on failure methods""" |
||
222 | |||
223 | exc = Exception("Test") |
||
224 | task_id = "test_task_id" |
||
225 | args = [self.submission_id] |
||
226 | kwargs = {} |
||
227 | einfo = ExceptionInfo |
||
228 | |||
229 | # call on_failure method |
||
230 | self.my_task.on_failure(exc, task_id, args, kwargs, einfo) |
||
231 | |||
232 | # check submission status and message |
||
233 | submission = Submission.objects.get(pk=self.submission_id) |
||
234 | |||
235 | # check submission.state changed |
||
236 | self.assertEqual(submission.status, ERROR) |
||
237 | self.assertEqual( |
||
238 | submission.message, |
||
239 | "Unknown error in validation - Test") |
||
240 | |||
241 | # test email sent |
||
242 | self.assertGreater(len(mail.outbox), 1) |
||
243 | |||
244 | # read email |
||
245 | email = mail.outbox[0] |
||
246 | |||
247 | self.assertEqual( |
||
248 | "Error in IMAGE Validation: Unknown error in validation - Test", |
||
249 | email.subject) |
||
250 | |||
251 | self.check_message( |
||
252 | 'Error', |
||
253 | 'Unknown error in validation - Test') |
||
254 | |||
255 | @patch("validation.tasks.ValidateSubmission.validate_model") |
||
256 | def test_validate_retry(self, my_validate): |
||
257 | """Test validation with retry""" |
||
258 | |||
259 | # Set a side effect on the patched methods |
||
260 | # so that they raise the errors we want. |
||
261 | self.validate_retry.side_effect = Retry() |
||
262 | |||
263 | my_validate.side_effect = Exception() |
||
264 | |||
265 | with raises(Retry): |
||
266 | self.my_task.run(submission_id=self.submission_id) |
||
267 | |||
268 | # asserting mock validate_model called |
||
269 | self.assertTrue(my_validate.called) |
||
270 | |||
271 | # asserting my mock objects |
||
272 | self.assertTrue(self.read_in_ruleset.called) |
||
273 | self.assertTrue(self.check_ruleset.called) |
||
274 | self.assertTrue(self.validate_retry.called) |
||
275 | |||
276 | # asserting django channels not called |
||
277 | self.check_message_not_called() |
||
278 | |||
279 | @patch("validation.tasks.ValidateSubmission.validate_model") |
||
280 | def test_issues_with_api(self, my_validate): |
||
281 | """Test errors with validation API""" |
||
282 | |||
283 | my_validate.side_effect = json.decoder.JSONDecodeError( |
||
284 | msg="test", doc="test", pos=1) |
||
285 | |||
286 | # call task. No retries with issues at EBI |
||
287 | res = self.my_task.run(submission_id=self.submission_id) |
||
288 | |||
289 | # assert a success with validation taks |
||
290 | self.assertEqual(res, "success") |
||
291 | |||
292 | # check submission status and message |
||
293 | self.submission.refresh_from_db() |
||
294 | |||
295 | # this is the message I want |
||
296 | message = "Errors in EBI API endpoints. Please try again later" |
||
297 | |||
298 | # check submission.status changed to NEED_REVISION |
||
299 | self.assertEqual(self.submission.status, LOADED) |
||
300 | self.assertIn( |
||
301 | message, |
||
302 | self.submission.message) |
||
303 | |||
304 | # test email sent |
||
305 | self.assertEqual(len(mail.outbox), 1) |
||
306 | |||
307 | # read email |
||
308 | email = mail.outbox[0] |
||
309 | |||
310 | self.assertEqual( |
||
311 | "Error in IMAGE Validation: %s" % message, |
||
312 | email.subject) |
||
313 | |||
314 | # asserting mock validate_model called |
||
315 | self.assertTrue(my_validate.called) |
||
316 | |||
317 | # asserting my mock objects |
||
318 | self.assertTrue(self.read_in_ruleset.called) |
||
319 | self.assertTrue(self.check_ruleset.called) |
||
320 | self.assertFalse(self.validate_retry.called) |
||
321 | |||
322 | self.check_message( |
||
323 | 'Loaded', |
||
324 | 'Errors in EBI API endpoints. Please try again later' |
||
325 | ) |
||
326 | |||
327 | def test_issues_with_ontologychache(self): |
||
328 | """Test errors with validation API when loading OntologyCache |
||
329 | objects""" |
||
330 | |||
331 | # return a custom exception with read_in_ruleset |
||
332 | self.read_in_ruleset.side_effect = OntologyCacheError("test exception") |
||
333 | |||
334 | # call task. No retries with issues at EBI |
||
335 | res = self.my_task.run(submission_id=self.submission_id) |
||
336 | |||
337 | # assert a success with validation taks |
||
338 | self.assertEqual(res, "success") |
||
339 | |||
340 | # check submission status and message |
||
341 | self.submission.refresh_from_db() |
||
342 | |||
343 | # this is the message I want |
||
344 | message = "Errors in EBI API endpoints. Please try again later" |
||
345 | |||
346 | # check submission.status changed to LOADED |
||
347 | self.assertEqual(self.submission.status, LOADED) |
||
348 | self.assertIn( |
||
349 | message, |
||
350 | self.submission.message) |
||
351 | |||
352 | # test email sent |
||
353 | self.assertEqual(len(mail.outbox), 1) |
||
354 | |||
355 | # read email |
||
356 | email = mail.outbox[0] |
||
357 | |||
358 | self.assertEqual( |
||
359 | "Error in IMAGE Validation: %s" % message, |
||
360 | email.subject) |
||
361 | |||
362 | # asserting my mock objects |
||
363 | self.assertTrue(self.read_in_ruleset.called) |
||
364 | self.assertFalse(self.check_ruleset.called) |
||
365 | self.assertFalse(self.validate_retry.called) |
||
366 | |||
367 | self.check_message( |
||
368 | 'Loaded', |
||
369 | 'Errors in EBI API endpoints. Please try again later') |
||
370 | |||
371 | def test_issues_with_ruleset(self): |
||
372 | """Test errors with ruleset""" |
||
373 | |||
374 | # return a custom exception with read_in_ruleset |
||
375 | self.read_in_ruleset.side_effect = RulesetError(["test exception"]) |
||
376 | |||
377 | # call task. No retries with issues at EBI |
||
378 | res = self.my_task.run(submission_id=self.submission_id) |
||
379 | |||
380 | # assert a success with validation taks |
||
381 | self.assertEqual(res, "success") |
||
382 | |||
383 | # check submission status and message |
||
384 | self.submission.refresh_from_db() |
||
385 | |||
386 | # this is the message I want |
||
387 | message = ( |
||
388 | "Error in IMAGE-metadata ruleset. Please inform InjectTool team") |
||
389 | |||
390 | # check submission.status changed to ERROR |
||
391 | self.assertEqual(self.submission.status, ERROR) |
||
392 | self.assertIn( |
||
393 | message, |
||
394 | self.submission.message) |
||
395 | |||
396 | # test email sent |
||
397 | self.assertGreater(len(mail.outbox), 1) |
||
398 | |||
399 | # read email |
||
400 | email = mail.outbox[0] |
||
401 | |||
402 | self.assertEqual( |
||
403 | "Error in IMAGE Validation: %s" % message, |
||
404 | email.subject) |
||
405 | |||
406 | # asserting my mock objects |
||
407 | self.assertTrue(self.read_in_ruleset.called) |
||
408 | self.assertFalse(self.check_ruleset.called) |
||
409 | self.assertFalse(self.validate_retry.called) |
||
410 | |||
411 | self.check_message( |
||
412 | 'Error', |
||
413 | 'Error in IMAGE-metadata ruleset. Please inform InjectTool team') |
||
414 | |||
415 | @patch("validation.tasks.MetaDataValidation.check_usi_structure") |
||
416 | @patch("validation.tasks.MetaDataValidation.validate") |
||
417 | def test_validate_submission( |
||
418 | self, my_validate, my_check): |
||
419 | """Test a valid submission. Simulate image_validation result and |
||
420 | status changes""" |
||
421 | |||
422 | # setting check_usi_structure result. now is a ValidateResultRecord |
||
423 | result = PickableMock() |
||
424 | result.get_overall_status.return_value = "Pass" |
||
425 | result.get_messages.return_value = [] |
||
426 | my_check.return_value = result |
||
427 | |||
428 | # setting a return value for check_with_ruleset |
||
429 | validation_result = Mock() |
||
430 | validation_result.get_overall_status.return_value = "Pass" |
||
431 | validation_result.get_messages.return_value = ["A message"] |
||
432 | result_set = Mock() |
||
433 | result_set.get_comparable_str.return_value = "A message" |
||
434 | validation_result.result_set = [result_set] |
||
435 | my_validate.return_value = validation_result |
||
436 | |||
437 | # NOTE that I'm calling the function directly, without delay |
||
438 | # (AsyncResult). I've patched the time consuming task |
||
439 | res = self.my_task.run(submission_id=self.submission_id) |
||
440 | |||
441 | # assert a success with validation taks |
||
442 | self.assertEqual(res, "success") |
||
443 | |||
444 | # check submission status and message |
||
445 | self.submission.refresh_from_db() |
||
446 | |||
447 | # check submission.state changed |
||
448 | self.assertEqual(self.submission.status, READY) |
||
449 | self.assertEqual( |
||
450 | self.submission.message, |
||
451 | "Submission validated with success") |
||
452 | |||
453 | # check Names (they are all ok) |
||
454 | for name in self.name_qs: |
||
455 | self.assertEqual(name.status, READY) |
||
456 | |||
457 | # test for model message (usi_results) |
||
458 | self.assertEqual( |
||
459 | name.validationresult.messages, ["A message"]) |
||
460 | self.assertEqual(name.validationresult.status, "Pass") |
||
461 | |||
462 | # assert validation functions called |
||
463 | self.assertTrue(my_check.called) |
||
464 | self.assertTrue(my_validate.called) |
||
465 | |||
466 | # asserting my mock objects |
||
467 | self.assertTrue(self.read_in_ruleset.called) |
||
468 | self.assertTrue(self.check_ruleset.called) |
||
469 | self.assertFalse(self.validate_retry.called) |
||
470 | |||
471 | # no unknown and sample with issues |
||
472 | validation_message = { |
||
473 | 'animals': self.n_animals, |
||
474 | 'samples': self.n_samples, |
||
475 | 'animal_unkn': 0, 'sample_unkn': 0, |
||
476 | 'animal_issues': 0, 'sample_issues': 0} |
||
477 | |||
478 | self.check_message( |
||
479 | 'Ready', |
||
480 | 'Submission validated with success', |
||
481 | validation_message=validation_message |
||
482 | ) |
||
483 | |||
484 | @patch("validation.tasks.MetaDataValidation.check_usi_structure") |
||
485 | @patch("validation.tasks.MetaDataValidation.validate") |
||
486 | def test_validate_submission_wrong_json( |
||
487 | self, my_validate, my_check): |
||
488 | """Test an error in JSON format""" |
||
489 | |||
490 | # setting check_usi_structure result. now is a ValidateResultRecord |
||
491 | messages = [ |
||
492 | ('Wrong JSON structure: no title field for record with ' |
||
493 | 'alias as animal_1'), |
||
494 | ('Wrong JSON structure: the values for attribute Person ' |
||
495 | 'role needs to be in an array for record animal_1') |
||
496 | ] |
||
497 | |||
498 | usi_result = ValidationResultRecord("animal_1") |
||
499 | usi_result.add_validation_result_column( |
||
500 | ValidationResultColumn( |
||
501 | "error", |
||
502 | messages[0], |
||
503 | "animal_1", |
||
504 | "") |
||
505 | ) |
||
506 | usi_result.add_validation_result_column( |
||
507 | ValidationResultColumn( |
||
508 | "error", |
||
509 | messages[1], |
||
510 | "animal_1", |
||
511 | "") |
||
512 | ) |
||
513 | |||
514 | my_check.return_value = usi_result |
||
515 | |||
516 | # setting a return value for check_with_ruleset |
||
517 | rule_result = Mock() |
||
518 | rule_result.get_overall_status.return_value = "Pass" |
||
519 | my_validate.return_value = rule_result |
||
520 | |||
521 | # call task |
||
522 | res = self.my_task.run(submission_id=self.submission_id) |
||
523 | |||
524 | # assert a success with validation taks |
||
525 | self.assertEqual(res, "success") |
||
526 | |||
527 | # check submission status and message |
||
528 | self.submission.refresh_from_db() |
||
529 | |||
530 | # check submission.state changed |
||
531 | self.assertEqual(self.submission.status, NEED_REVISION) |
||
532 | self.assertIn( |
||
533 | "Validation got errors", |
||
534 | self.submission.message) |
||
535 | |||
536 | # check Names (they require revisions) |
||
537 | for name in self.name_qs: |
||
538 | self.assertEqual(name.status, NEED_REVISION) |
||
539 | |||
540 | # test for model message (usi_results) |
||
541 | self.assertEqual( |
||
542 | name.validationresult.messages, usi_result.get_messages()) |
||
543 | self.assertEqual( |
||
544 | name.validationresult.status, usi_result.get_overall_status()) |
||
545 | |||
546 | # if JSON is not valid, I don't check for ruleset |
||
547 | self.assertTrue(my_check.called) |
||
548 | self.assertFalse(my_validate.called) |
||
549 | |||
550 | # asserting my mock objects |
||
551 | self.assertTrue(self.read_in_ruleset.called) |
||
552 | self.assertTrue(self.check_ruleset.called) |
||
553 | self.assertFalse(self.validate_retry.called) |
||
554 | |||
555 | # all sample and animals have issues |
||
556 | self.check_message( |
||
557 | 'Need Revision', |
||
558 | ('Validation got errors: Error in metadata. ' |
||
559 | 'Need revisions before submit'), |
||
560 | {'animals': self.n_animals, 'samples': self.n_samples, |
||
561 | 'animal_unkn': 0, 'sample_unkn': 0, |
||
562 | 'animal_issues': self.n_animals, |
||
563 | 'sample_issues': self.n_samples}, |
||
564 | 1) |
||
565 | |||
566 | @patch("validation.tasks.MetaDataValidation.check_usi_structure") |
||
567 | @patch("validation.tasks.MetaDataValidation.validate") |
||
568 | def test_unsupported_status( |
||
569 | self, my_validate, my_check): |
||
570 | """This test will ensure that image_validation ValidationResultRecord |
||
571 | still support the same statuses""" |
||
572 | |||
573 | # setting check_usi_structure result. now is a ValidateResultRecord |
||
574 | result = PickableMock() |
||
575 | result.get_overall_status.return_value = "Pass" |
||
576 | result.get_messages.return_value = [] |
||
577 | my_check.return_value = result |
||
578 | |||
579 | # setting a return value for check_with_ruleset |
||
580 | rule_result = PickableMock() |
||
581 | rule_result.get_overall_status.return_value = "A fake status" |
||
582 | rule_result.get_messages.return_value = ["A fake message", ] |
||
583 | |||
584 | result_set = Mock() |
||
585 | result_set.get_comparable_str.return_value = "A fake message" |
||
586 | rule_result.result_set = [result_set] |
||
587 | |||
588 | my_validate.return_value = rule_result |
||
589 | |||
590 | # call task |
||
591 | self.assertRaisesRegex( |
||
592 | ValidationError, |
||
593 | "Unsupported validation status for submission", |
||
594 | self.my_task.run, |
||
595 | submission_id=self.submission_id) |
||
596 | |||
597 | # check submission status and message |
||
598 | self.submission.refresh_from_db() |
||
599 | |||
600 | # check submission.state changed |
||
601 | self.assertEqual(self.submission.status, ERROR) |
||
602 | self.assertIn( |
||
603 | "Unsupported validation status for submission", |
||
604 | self.submission.message) |
||
605 | |||
606 | # if JSON is not valid, I don't check for ruleset |
||
607 | self.assertTrue(my_check.called) |
||
608 | self.assertTrue(my_validate.called) |
||
609 | |||
610 | # asserting my mock objects |
||
611 | self.assertTrue(self.read_in_ruleset.called) |
||
612 | self.assertTrue(self.check_ruleset.called) |
||
613 | self.assertFalse(self.validate_retry.called) |
||
614 | |||
615 | self.check_message( |
||
616 | message='Error', |
||
617 | notification_message=( |
||
618 | "Validation got errors: Unsupported validation status " |
||
619 | "for submission Cryoweb (United Kingdom, test)"), |
||
620 | validation_message={ |
||
621 | 'animals': self.n_animals, 'samples': self.n_samples, |
||
622 | 'animal_unkn': 0, 'sample_unkn': 0, |
||
623 | 'animal_issues': self.n_animals, |
||
624 | 'sample_issues': self.n_samples}, |
||
625 | pk=1) |
||
626 | |||
627 | @patch("validation.tasks.MetaDataValidation.check_usi_structure") |
||
628 | @patch("validation.tasks.MetaDataValidation.validate") |
||
629 | def test_validate_submission_warnings( |
||
630 | self, my_validate, my_check): |
||
631 | """A submission with warnings is a READY submission""" |
||
632 | |||
633 | # setting check_usi_structure result. now is a ValidateResultRecord |
||
634 | result = PickableMock() |
||
635 | result.get_overall_status.return_value = "Pass" |
||
636 | result.get_messages.return_value = [] |
||
637 | my_check.return_value = result |
||
638 | |||
639 | # setting a return value for check_with_ruleset |
||
640 | result1 = ValidationResultRecord("animal_1") |
||
641 | result1.add_validation_result_column( |
||
642 | ValidationResultColumn( |
||
643 | "warning", |
||
644 | "warn message", |
||
645 | "animal_1", |
||
646 | "warn column") |
||
647 | ) |
||
648 | |||
649 | result2 = ValidationResultRecord("animal_2") |
||
650 | result2.add_validation_result_column( |
||
651 | ValidationResultColumn( |
||
652 | "pass", |
||
653 | "a message", |
||
654 | "animal_2", |
||
655 | "") |
||
656 | ) |
||
657 | |||
658 | result3 = ValidationResultRecord("animal_3") |
||
659 | result3.add_validation_result_column( |
||
660 | ValidationResultColumn( |
||
661 | "pass", |
||
662 | "a message", |
||
663 | "animal_3", |
||
664 | "") |
||
665 | ) |
||
666 | |||
667 | result4 = ValidationResultRecord("sample_1") |
||
668 | result4.add_validation_result_column( |
||
669 | ValidationResultColumn( |
||
670 | "pass", |
||
671 | "a message", |
||
672 | "sample_1", |
||
673 | "") |
||
674 | ) |
||
675 | |||
676 | # add results to result set |
||
677 | responses = [result1, result2, result3, result4] |
||
678 | my_validate.side_effect = responses |
||
679 | |||
680 | # call task |
||
681 | res = self.my_task.run(submission_id=self.submission_id) |
||
682 | |||
683 | # assert a success with validation taks |
||
684 | self.assertEqual(res, "success") |
||
685 | |||
686 | # check submission status and message |
||
687 | self.submission.refresh_from_db() |
||
688 | |||
689 | # check submission.state changed |
||
690 | self.assertEqual(self.submission.status, READY) |
||
691 | self.assertIn( |
||
692 | "Submission validated with some warnings", |
||
693 | self.submission.message) |
||
694 | |||
695 | # check Names (they are all ok) |
||
696 | for i, name in enumerate(self.name_qs): |
||
697 | # get the appropriate ValidationResultRecord |
||
698 | result = responses[i] |
||
699 | |||
700 | # all objects are ready for submissions |
||
701 | self.assertEqual(name.status, READY) |
||
702 | |||
703 | self.assertEqual( |
||
704 | name.validationresult.messages, |
||
705 | result.get_messages()) |
||
706 | |||
707 | self.assertEqual( |
||
708 | name.validationresult.status, |
||
709 | result.get_overall_status()) |
||
710 | |||
711 | # test for my methods called |
||
712 | self.assertTrue(my_check.called) |
||
713 | self.assertTrue(my_validate.called) |
||
714 | |||
715 | # asserting my mock objects |
||
716 | self.assertTrue(self.read_in_ruleset.called) |
||
717 | self.assertTrue(self.check_ruleset.called) |
||
718 | self.assertFalse(self.validate_retry.called) |
||
719 | |||
720 | self.check_message( |
||
721 | message='Ready', |
||
722 | notification_message='Submission validated with some warnings', |
||
723 | validation_message={ |
||
724 | 'animals': self.n_animals, 'samples': self.n_samples, |
||
725 | 'animal_unkn': 0, 'sample_unkn': 0, |
||
726 | 'animal_issues': 0, 'sample_issues': 0}, |
||
727 | pk=1) |
||
728 | |||
729 | @patch("validation.tasks.MetaDataValidation.check_usi_structure") |
||
730 | @patch("validation.tasks.MetaDataValidation.validate") |
||
731 | def test_validate_submission_errors( |
||
732 | self, my_validate, my_check): |
||
733 | """A submission with errors is a NEED_REVISION submission""" |
||
734 | |||
735 | # setting check_usi_structure result. now is a ValidateResultRecord |
||
736 | result = PickableMock() |
||
737 | result.get_overall_status.return_value = "Pass" |
||
738 | result.get_messages.return_value = [] |
||
739 | my_check.return_value = result |
||
740 | |||
741 | # setting a return value for check_with_ruleset |
||
742 | result1 = ValidationResultRecord("animal_1") |
||
743 | result1.add_validation_result_column( |
||
744 | ValidationResultColumn( |
||
745 | "warning", |
||
746 | "warn message", |
||
747 | "animal_1", |
||
748 | "warn column") |
||
749 | ) |
||
750 | |||
751 | result2 = ValidationResultRecord("animal_2") |
||
752 | result2.add_validation_result_column( |
||
753 | ValidationResultColumn( |
||
754 | "pass", |
||
755 | "a message", |
||
756 | "animal_2", |
||
757 | "") |
||
758 | ) |
||
759 | |||
760 | result3 = ValidationResultRecord("animal_3") |
||
761 | result3.add_validation_result_column( |
||
762 | ValidationResultColumn( |
||
763 | "pass", |
||
764 | "a message", |
||
765 | "animal_3", |
||
766 | "") |
||
767 | ) |
||
768 | |||
769 | result4 = ValidationResultRecord("sample_1") |
||
770 | result4.add_validation_result_column( |
||
771 | ValidationResultColumn( |
||
772 | "error", |
||
773 | "error message", |
||
774 | "sample_1", |
||
775 | "error column") |
||
776 | ) |
||
777 | |||
778 | # add results to result set |
||
779 | responses = [result1, result2, result3, result4] |
||
780 | my_validate.side_effect = responses |
||
781 | |||
782 | # call task |
||
783 | res = self.my_task.run(submission_id=self.submission_id) |
||
784 | |||
785 | # assert a success with validation taks |
||
786 | self.assertEqual(res, "success") |
||
787 | |||
788 | # check submission status and message |
||
789 | self.submission.refresh_from_db() |
||
790 | |||
791 | # check submission.state changed |
||
792 | self.assertEqual(self.submission.status, NEED_REVISION) |
||
793 | self.assertIn( |
||
794 | "Error in metadata", |
||
795 | self.submission.message) |
||
796 | |||
797 | # check Names (they are all ok, except 1 - sample) |
||
798 | for i, name in enumerate(self.name_qs): |
||
799 | # get the appropriate ValidationResultRecord |
||
800 | result = responses[i] |
||
801 | |||
802 | if hasattr(name, "animal"): |
||
803 | self.assertEqual(name.status, READY) |
||
804 | |||
805 | else: |
||
806 | # sample has errors |
||
807 | self.assertEqual(name.status, NEED_REVISION) |
||
808 | |||
809 | self.assertEqual( |
||
810 | name.validationresult.messages, |
||
811 | result.get_messages()) |
||
812 | |||
813 | self.assertEqual( |
||
814 | name.validationresult.status, |
||
815 | result.get_overall_status()) |
||
816 | |||
817 | # test for my methods called |
||
818 | self.assertTrue(my_check.called) |
||
819 | self.assertTrue(my_validate.called) |
||
820 | |||
821 | # asserting my mock objects |
||
822 | self.assertTrue(self.read_in_ruleset.called) |
||
823 | self.assertTrue(self.check_ruleset.called) |
||
824 | self.assertFalse(self.validate_retry.called) |
||
825 | |||
826 | self.check_message( |
||
827 | message='Need Revision', |
||
828 | notification_message=( |
||
829 | 'Validation got errors: Error in ' |
||
830 | 'metadata. Need revisions before submit'), |
||
831 | validation_message={ |
||
832 | 'animals': self.n_animals, 'samples': self.n_samples, |
||
833 | 'animal_unkn': 0, 'sample_unkn': 0, |
||
834 | 'animal_issues': 0, 'sample_issues': 1}, |
||
835 | pk=1) |
||
836 | |||
837 | |||
838 | class ValidateSubmissionStatusTest(ValidateSubmissionMixin, TestCase): |
||
839 | """Check database statuses after calling validation""" |
||
840 | |||
841 | def setUp(self): |
||
842 | # calling base setup |
||
843 | super().setUp() |
||
844 | |||
845 | # get a submission data object (with no ruleset) |
||
846 | self.submission_data = ValidateSubmission( |
||
847 | self.submission, ruleset=None) |
||
848 | |||
849 | # track an animal for testing |
||
850 | self.animal = Animal.objects.get(pk=1) |
||
851 | self.animal_name = self.animal.name |
||
852 | |||
853 | def check_status(self, status, messages, name_status): |
||
854 | """Test if I can update status for a model that pass validation""" |
||
855 | |||
856 | result = PickableMock() |
||
857 | result.get_overall_status.return_value = status |
||
858 | result.get_messages.return_value = messages |
||
859 | result_set = Mock() |
||
860 | result_set.get_comparable_str.return_value = "A message" |
||
861 | result.result_set = [result_set] |
||
862 | |||
863 | submission_statuses = Counter( |
||
864 | {'Pass': 0, |
||
865 | 'Warning': 0, |
||
866 | 'Error': 0, |
||
867 | 'JSON': 0}) |
||
868 | |||
869 | # calling update statuses |
||
870 | self.submission_data.update_statuses( |
||
871 | submission_statuses, self.animal, result) |
||
872 | |||
873 | # Test for animal status |
||
874 | self.animal_name.refresh_from_db() |
||
875 | self.assertEqual(self.animal_name.status, name_status) |
||
876 | |||
877 | # get validation result object |
||
878 | validationresult = self.animal_name.validationresult |
||
879 | self.assertEqual(validationresult.status, status) |
||
880 | self.assertEqual(validationresult.messages, messages) |
||
881 | |||
882 | # test submission status |
||
883 | self.assertEqual(submission_statuses[status], 1) |
||
884 | |||
885 | def test_update_status_pass(self): |
||
886 | status = 'Pass' |
||
887 | messages = ['Passed all tests'] |
||
888 | name_status = READY |
||
889 | |||
890 | self.check_status(status, messages, name_status) |
||
891 | |||
892 | def test_update_status_warning(self): |
||
893 | status = 'Warning' |
||
894 | messages = ['issued a warning'] |
||
895 | name_status = READY |
||
896 | |||
897 | self.check_status(status, messages, name_status) |
||
898 | |||
899 | def test_update_status_error(self): |
||
900 | status = 'Error' |
||
901 | messages = ['issued an error'] |
||
902 | name_status = NEED_REVISION |
||
903 | |||
904 | self.check_status(status, messages, name_status) |
||
905 | |||
906 | |||
907 | class ValidateUpdatedSubmissionStatusTest(ValidateSubmissionMixin, TestCase): |
||
908 | """Check database statuses after calling validation for an updated |
||
909 | submission (a submission completed and submitted to biosample in which |
||
910 | I want to modify a thing)""" |
||
911 | |||
912 | def setUp(self): |
||
913 | # call base method |
||
914 | super().setUp() |
||
915 | |||
916 | # get a submission data object (with no ruleset) |
||
917 | self.submission_data = ValidateSubmission( |
||
918 | self.submission, ruleset=None) |
||
919 | |||
920 | # take all names and set them to completed, as after a successful |
||
921 | # submission: |
||
922 | self.name_qs.update(status=COMPLETED) |
||
923 | |||
924 | # take the animal name I want to update |
||
925 | self.animal_name = Name.objects.get(pk=3) |
||
926 | |||
927 | # update submission status. Simulate a completed submission in which |
||
928 | # I want to update something |
||
929 | self.submission.status = NEED_REVISION |
||
930 | self.submission.save() |
||
931 | |||
932 | # update name objects. In this case, animal was modified |
||
933 | self.animal_name.status = NEED_REVISION |
||
934 | self.animal_name.save() |
||
935 | |||
936 | def update_status(self, status, messages, name_status): |
||
937 | """Test if I can update status for a model that pass validation""" |
||
938 | |||
939 | # modelling validation same result for every object |
||
940 | result = PickableMock() |
||
941 | result.get_overall_status.return_value = status |
||
942 | result.get_messages.return_value = messages |
||
943 | |||
944 | result_set = Mock() |
||
945 | result_set.get_comparable_str.return_value = "A message" |
||
946 | result.result_set = [result_set] |
||
947 | |||
948 | submission_statuses = Counter( |
||
949 | {'Pass': 0, |
||
950 | 'Warning': 0, |
||
951 | 'Error': 0, |
||
952 | 'JSON': 0}) |
||
953 | |||
954 | # calling update statuses on name objects |
||
955 | for name in self.name_qs: |
||
956 | if hasattr(name, "animal"): |
||
957 | self.submission_data.update_statuses( |
||
958 | submission_statuses, |
||
959 | name.animal, |
||
960 | result) |
||
961 | else: |
||
962 | self.submission_data.update_statuses( |
||
963 | submission_statuses, |
||
964 | name.sample, |
||
965 | result) |
||
966 | |||
967 | # refreshing data from db |
||
968 | self.animal_name.refresh_from_db() |
||
969 | |||
970 | def test_update_status_pass(self): |
||
971 | status = 'Pass' |
||
972 | messages = ['Passed all tests'] |
||
973 | name_status = READY |
||
974 | |||
975 | self.update_status(status, messages, name_status) |
||
976 | |||
977 | # asserting status change for animal |
||
978 | self.assertEqual(self.animal_name.status, name_status) |
||
979 | |||
980 | # validationresult is tested outside this class |
||
981 | |||
982 | # other statuses are unchanged |
||
983 | for name in self.name_qs.exclude(pk=self.animal_name.pk): |
||
984 | self.assertEqual(name.status, COMPLETED) |
||
985 | |||
986 | def test_update_status_warning(self): |
||
987 | status = 'Warning' |
||
988 | messages = ['issued a warning'] |
||
989 | name_status = READY |
||
990 | |||
991 | self.update_status(status, messages, name_status) |
||
992 | |||
993 | # asserting status change for animal |
||
994 | self.assertEqual(self.animal_name.status, name_status) |
||
995 | |||
996 | # other statuses are unchanged |
||
997 | for name in self.name_qs.exclude(pk=self.animal_name.pk): |
||
998 | self.assertEqual(name.status, COMPLETED) |
||
999 | |||
1000 | def test_update_status_error(self): |
||
1001 | status = 'Error' |
||
1002 | messages = ['issued an error'] |
||
1003 | name_status = NEED_REVISION |
||
1004 | |||
1005 | self.update_status(status, messages, name_status) |
||
1006 | |||
1007 | # all statuses are changed (and need revisions) |
||
1008 | for name in self.name_qs: |
||
1009 | self.assertEqual(name.status, NEED_REVISION) |
||
1010 |