Total Complexity | 50 |
Total Lines | 419 |
Duplicated Lines | 2.39 % |
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 tests.test_submissions 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 Thu Jul 12 14:23:09 2018 |
||
5 | |||
6 | @author: Paolo Cozzi <[email protected]> |
||
7 | """ |
||
8 | |||
9 | import os |
||
10 | import json |
||
11 | import types |
||
12 | |||
13 | from collections import defaultdict |
||
14 | from unittest.mock import patch, Mock |
||
15 | from unittest import TestCase |
||
16 | |||
17 | from pyUSIrest.auth import Auth |
||
18 | from pyUSIrest.client import Document |
||
19 | from pyUSIrest.exceptions import NotReadyError, USIDataError |
||
20 | from pyUSIrest.usi import Submission, Sample |
||
21 | |||
22 | from .common import DATA_PATH |
||
23 | from .test_auth import generate_token |
||
24 | |||
25 | |||
26 | class SubmissionTest(TestCase): |
||
27 | @classmethod |
||
28 | def setup_class(cls): |
||
29 | cls.mock_get_patcher = patch('requests.Session.get') |
||
30 | cls.mock_get = cls.mock_get_patcher.start() |
||
31 | |||
32 | cls.mock_post_patcher = patch('requests.Session.post') |
||
33 | cls.mock_post = cls.mock_post_patcher.start() |
||
34 | |||
35 | cls.mock_put_patcher = patch('requests.Session.put') |
||
36 | cls.mock_put = cls.mock_put_patcher.start() |
||
37 | |||
38 | cls.mock_patch_patcher = patch('requests.Session.patch') |
||
39 | cls.mock_patch = cls.mock_patch_patcher.start() |
||
40 | |||
41 | cls.mock_delete_patcher = patch('requests.Session.delete') |
||
42 | cls.mock_delete = cls.mock_delete_patcher.start() |
||
43 | |||
44 | @classmethod |
||
45 | def teardown_class(cls): |
||
46 | cls.mock_get_patcher.stop() |
||
47 | cls.mock_post_patcher.stop() |
||
48 | cls.mock_put_patcher.stop() |
||
49 | cls.mock_patch_patcher.stop() |
||
50 | cls.mock_delete_patcher.stop() |
||
51 | |||
52 | def setUp(self): |
||
53 | self.auth = Auth(token=generate_token()) |
||
54 | |||
55 | with open(os.path.join(DATA_PATH, "newSubmission.json")) as handle: |
||
56 | data = json.load(handle) |
||
57 | |||
58 | self.submission = Submission(self.auth, data=data) |
||
59 | |||
60 | with open(os.path.join(DATA_PATH, "contents.json")) as handle: |
||
61 | self.content = json.load(handle) |
||
62 | |||
63 | # defining samples |
||
64 | self.sample1 = { |
||
65 | 'alias': 'animal_1', |
||
66 | 'title': 'animal_title', |
||
67 | 'releaseDate': '2018-07-13', |
||
68 | 'taxonId': 9940, |
||
69 | 'taxon': 'Ovis aries', |
||
70 | 'attributes': { |
||
71 | 'material': [ |
||
72 | {'value': 'organism', |
||
73 | 'terms': [ |
||
74 | {'url': 'http://purl.obolibrary.org/obo/OBI_0100026'} |
||
75 | ]} |
||
76 | ], |
||
77 | 'project': [{'value': 'test'}] |
||
78 | }, |
||
79 | 'sampleRelationships': []} |
||
80 | |||
81 | self.sample2 = { |
||
82 | 'alias': 'sample_1', |
||
83 | 'title': 'sample_title', |
||
84 | 'taxonId': 9940, |
||
85 | 'taxon': 'Ovis aries', |
||
86 | 'description': 'a description', |
||
87 | 'attributes': { |
||
88 | 'material': [ |
||
89 | {'value': 'specimen from organism', |
||
90 | 'terms': [ |
||
91 | {'url': 'http://purl.obolibrary.org/obo/OBI_0001479'} |
||
92 | ]} |
||
93 | ], |
||
94 | 'project': [{'value': 'test'}] |
||
95 | }, |
||
96 | 'sampleRelationships': [{ |
||
97 | 'alias': 'animal_1', |
||
98 | 'relationshipNature': 'derived from'}] |
||
99 | } |
||
100 | |||
101 | def test_str(self): |
||
102 | with open(os.path.join(DATA_PATH, "submissionStatus1.json")) as handle: |
||
103 | data = json.load(handle) |
||
104 | |||
105 | self.mock_get.return_value = Mock() |
||
106 | self.mock_get.return_value.json.return_value = data |
||
107 | self.mock_get.return_value.status_code = 200 |
||
108 | |||
109 | test = self.submission.__str__() |
||
110 | self.assertIsInstance(test, str) |
||
111 | |||
112 | def create_sample(self, sample): |
||
113 | self.mock_get.return_value = Mock() |
||
114 | self.mock_get.return_value.json.return_value = self.content |
||
115 | self.mock_get.return_value.status_code = 200 |
||
116 | |||
117 | with open(os.path.join(DATA_PATH, "%s.json" % (sample))) as handle: |
||
118 | data = json.load(handle) |
||
119 | |||
120 | self.mock_post.return_value = Mock() |
||
121 | self.mock_post.return_value.json.return_value = data |
||
122 | self.mock_post.return_value.status_code = 201 |
||
123 | |||
124 | return self.submission.create_sample(getattr(self, sample)) |
||
125 | |||
126 | def test_create_sample(self): |
||
127 | sample1 = self.create_sample("sample1") |
||
128 | self.assertIsInstance(sample1, Sample) |
||
129 | |||
130 | sample2 = self.create_sample("sample2") |
||
131 | self.assertIsInstance(sample2, Sample) |
||
132 | |||
133 | def mocked_get_samples(*args, **kwargs): |
||
134 | class MockResponse: |
||
135 | def __init__(self, json_data, status_code): |
||
136 | self.json_data = json_data |
||
137 | self.status_code = status_code |
||
138 | self.text = "MockResponse not implemented: %s" % (args[0]) |
||
139 | |||
140 | def json(self): |
||
141 | return self.json_data |
||
142 | |||
143 | get_samples_link = ( |
||
144 | "https://submission-test.ebi.ac.uk/api/submissions/" |
||
145 | "c8c86558-8d3a-4ac5-8638-7aa354291d61/contents/samples") |
||
146 | |||
147 | with open(os.path.join(DATA_PATH, "samples.json")) as handle: |
||
148 | samples = json.load(handle) |
||
149 | |||
150 | with open(os.path.join(DATA_PATH, "validation1.json")) as handle: |
||
151 | validation1 = json.load(handle) |
||
152 | |||
153 | with open(os.path.join(DATA_PATH, "validation2.json")) as handle: |
||
154 | validation2 = json.load(handle) |
||
155 | |||
156 | # followin content -> samples |
||
157 | if args[0] == get_samples_link: |
||
158 | return MockResponse(samples, 200) |
||
159 | |||
160 | # sample1 validation result |
||
161 | elif args[0] == ( |
||
162 | 'https://submission-test.ebi.ac.uk/api/samples/90c8f449-' |
||
163 | 'b3c2-4238-a22b-fd03bc02a5d2/validationResult'): |
||
164 | return MockResponse(validation1, 200) |
||
165 | |||
166 | # sample1 validtation result |
||
167 | elif args[0] == ( |
||
168 | 'https://submission-test.ebi.ac.uk/api/samples/58cb010a-' |
||
169 | '3a89-42b7-8ccd-67b6f8b6dd4c/validationResult'): |
||
170 | return MockResponse(validation2, 200) |
||
171 | |||
172 | return MockResponse(None, 404) |
||
173 | |||
174 | # We patch 'requests.Session.get' with our own method. The mock object is |
||
175 | # passed in to our test case method. |
||
176 | @patch('requests.Session.get', side_effect=mocked_get_samples) |
||
177 | def test_get_samples(self, mock_get): |
||
178 | samples = self.submission.get_samples(status='Complete') |
||
179 | |||
180 | # samples is now a generator |
||
181 | self.assertIsInstance(samples, types.GeneratorType) |
||
182 | |||
183 | # convert it into a list |
||
184 | samples = list(samples) |
||
185 | self.assertEqual(len(samples), 2) |
||
186 | |||
187 | def mocked_get_empty_samples(*args, **kwargs): |
||
188 | """Simulate a submission with no samples at all""" |
||
189 | |||
190 | class MockResponse: |
||
191 | def __init__(self, json_data, status_code): |
||
192 | self.json_data = json_data |
||
193 | self.status_code = status_code |
||
194 | self.text = "MockResponse not implemented: %s" % (args[0]) |
||
195 | |||
196 | def json(self): |
||
197 | return self.json_data |
||
198 | |||
199 | get_samples_link = ( |
||
200 | "https://submission-test.ebi.ac.uk/api/submissions/" |
||
201 | "c8c86558-8d3a-4ac5-8638-7aa354291d61/contents/samples") |
||
202 | |||
203 | with open(os.path.join(DATA_PATH, "empty_samples.json")) as handle: |
||
204 | samples = json.load(handle) |
||
205 | |||
206 | # followin content -> samples |
||
207 | if args[0] == get_samples_link: |
||
208 | return MockResponse(samples, 200) |
||
209 | |||
210 | # default response |
||
211 | return MockResponse(None, 404) |
||
212 | |||
213 | # patch a request.get to return 0 samples for a submission |
||
214 | @patch('requests.Session.get', side_effect=mocked_get_empty_samples) |
||
215 | def test_get_empty_samples(self, mock_get): |
||
216 | samples = self.submission.get_samples(status='Complete') |
||
217 | |||
218 | self.assertRaises(StopIteration, next, samples) |
||
219 | |||
220 | def test_get_status(self): |
||
221 | with open(os.path.join(DATA_PATH, "validationResults.json")) as handle: |
||
222 | data = json.load(handle) |
||
223 | |||
224 | self.mock_get.return_value = Mock() |
||
225 | self.mock_get.return_value.json.return_value = data |
||
226 | self.mock_get.return_value.status_code = 200 |
||
227 | |||
228 | statuses = self.submission.get_status() |
||
229 | self.assertEqual(statuses['Complete'], 2) |
||
230 | |||
231 | def test_check_ready(self): |
||
232 | with open(os.path.join( |
||
233 | DATA_PATH, "availableSubmissionStatuses.json")) as handle: |
||
234 | data = json.load(handle) |
||
235 | |||
236 | self.mock_get.return_value = Mock() |
||
237 | self.mock_get.return_value.json.return_value = data |
||
238 | self.mock_get.return_value.status_code = 200 |
||
239 | |||
240 | check = self.submission.check_ready() |
||
241 | self.assertTrue(check) |
||
242 | |||
243 | def mocked_finalize(*args, **kwargs): |
||
244 | class MockResponse: |
||
245 | def __init__(self, json_data, status_code): |
||
246 | self.json_data = json_data |
||
247 | self.status_code = status_code |
||
248 | self.text = "MockResponse not implemented: %s" % (args[0]) |
||
249 | |||
250 | def json(self): |
||
251 | return self.json_data |
||
252 | |||
253 | check_ready_link = ( |
||
254 | "https://submission-test.ebi.ac.uk/api/submissions/c8c86558-" |
||
255 | "8d3a-4ac5-8638-7aa354291d61/availableSubmissionStatuses") |
||
256 | |||
257 | with open(os.path.join( |
||
258 | DATA_PATH, "availableSubmissionStatuses.json")) as handle: |
||
259 | check_ready_data = json.load(handle) |
||
260 | |||
261 | validation_link = ( |
||
262 | "https://submission-test.ebi.ac.uk/api/validationResults/search/" |
||
263 | "by-submission?submissionId=c8c86558-8d3a-4ac5-8638-7aa354291d61") |
||
264 | |||
265 | with open(os.path.join(DATA_PATH, "validationResults.json")) as handle: |
||
266 | validation_data = json.load(handle) |
||
267 | |||
268 | self_link = ( |
||
269 | "https://submission-test.ebi.ac.uk/api/submissions/" |
||
270 | "c8c86558-8d3a-4ac5-8638-7aa354291d61") |
||
271 | |||
272 | with open(os.path.join(DATA_PATH, "newSubmission.json")) as handle: |
||
273 | self_data = json.load(handle) |
||
274 | |||
275 | status_link = ( |
||
276 | "https://submission-test.ebi.ac.uk/api/submissions/c8c86558-" |
||
277 | "8d3a-4ac5-8638-7aa354291d61/submissionStatus") |
||
278 | |||
279 | with open(os.path.join(DATA_PATH, "submissionStatus2.json")) as handle: |
||
280 | status_data = json.load(handle) |
||
281 | |||
282 | if args[0] == check_ready_link: |
||
283 | return MockResponse(check_ready_data, 200) |
||
284 | |||
285 | elif args[0] == validation_link: |
||
286 | return MockResponse(validation_data, 200) |
||
287 | |||
288 | elif args[0] == self_link: |
||
289 | return MockResponse(self_data, 200) |
||
290 | |||
291 | elif args[0] == status_link: |
||
292 | return MockResponse(status_data, 200) |
||
293 | |||
294 | return MockResponse(None, 404) |
||
295 | |||
296 | @patch('requests.Session.get', side_effect=mocked_finalize) |
||
297 | def test_finalize(self, mock_get): |
||
298 | self.mock_put.return_value = Mock() |
||
299 | self.mock_put.return_value.json.return_value = {} |
||
300 | self.mock_put.return_value.status_code = 200 |
||
301 | |||
302 | document = self.submission.finalize() |
||
303 | self.assertIsInstance(document, Document) |
||
304 | |||
305 | def test_finalize_not_ready(self): |
||
306 | with open(os.path.join( |
||
307 | DATA_PATH, "availableSubmissionStatuses.json")) as handle: |
||
308 | data = json.load(handle) |
||
309 | |||
310 | # remove a key from data |
||
311 | del data['_embedded'] |
||
312 | |||
313 | self.mock_get.return_value = Mock() |
||
314 | self.mock_get.return_value.json.return_value = data |
||
315 | self.mock_get.return_value.status_code = 200 |
||
316 | |||
317 | self.assertRaises( |
||
318 | NotReadyError, |
||
319 | self.submission.finalize) |
||
320 | |||
321 | def mocked_finalize_errors(*args, **kwargs): |
||
322 | class MockResponse: |
||
323 | def __init__(self, json_data, status_code): |
||
324 | self.json_data = json_data |
||
325 | self.status_code = status_code |
||
326 | self.text = "MockResponse not implemented: %s" % (args[0]) |
||
327 | |||
328 | def json(self): |
||
329 | return self.json_data |
||
330 | |||
331 | # this variable will collect all replies |
||
332 | replies = defaultdict(lambda: MockResponse(None, 404)) |
||
333 | |||
334 | # a custom function to set up replies for link |
||
335 | View Code Duplication | def set_reply(url, filename, status=200): |
|
|
|||
336 | # referring to the upper replies variable |
||
337 | nonlocal replies |
||
338 | |||
339 | # open data file |
||
340 | with open(os.path.join(DATA_PATH, filename)) as handle: |
||
341 | data = json.load(handle) |
||
342 | |||
343 | # track reply to URL |
||
344 | replies[url] = MockResponse(data, status) |
||
345 | |||
346 | set_reply("https://submission-test.ebi.ac.uk/api/submissions/" |
||
347 | "c8c86558-8d3a-4ac5-8638-7aa354291d61/" |
||
348 | "availableSubmissionStatuses", |
||
349 | "availableSubmissionStatuses.json") |
||
350 | |||
351 | set_reply("https://submission-test.ebi.ac.uk/api/" |
||
352 | "validationResults/search/by-submission?" |
||
353 | "submissionId=c8c86558-8d3a-4ac5-8638-7aa354291d61", |
||
354 | "validationResultsError.json") |
||
355 | |||
356 | return replies[args[0]] |
||
357 | |||
358 | @patch('requests.Session.get', side_effect=mocked_finalize_errors) |
||
359 | def test_finalize_has_errors(self, my_get): |
||
360 | self.assertRaises( |
||
361 | USIDataError, |
||
362 | self.submission.finalize) |
||
363 | |||
364 | def test_delete(self): |
||
365 | self.mock_delete.return_value = Mock() |
||
366 | self.mock_delete.return_value.last_response = '' |
||
367 | self.mock_delete.return_value.status_code = 204 |
||
368 | |||
369 | self.submission.delete() |
||
370 | |||
371 | |||
372 | class SampleTest(TestCase): |
||
373 | @classmethod |
||
374 | def setup_class(cls): |
||
375 | cls.mock_get_patcher = patch('requests.Session.get') |
||
376 | cls.mock_get = cls.mock_get_patcher.start() |
||
377 | |||
378 | cls.mock_patch_patcher = patch('requests.Session.patch') |
||
379 | cls.mock_patch = cls.mock_patch_patcher.start() |
||
380 | |||
381 | cls.mock_delete_patcher = patch('requests.Session.delete') |
||
382 | cls.mock_delete = cls.mock_delete_patcher.start() |
||
383 | |||
384 | @classmethod |
||
385 | def teardown_class(cls): |
||
386 | cls.mock_get_patcher.stop() |
||
387 | cls.mock_patch_patcher.stop() |
||
388 | cls.mock_delete_patcher.stop() |
||
389 | |||
390 | def setUp(self): |
||
391 | self.auth = Auth(token=generate_token()) |
||
392 | |||
393 | with open(os.path.join(DATA_PATH, "sample2.json")) as handle: |
||
394 | self.data = json.load(handle) |
||
395 | |||
396 | self.sample = Sample(self.auth, data=self.data) |
||
397 | |||
398 | def test_str(self): |
||
399 | test = self.sample.__str__() |
||
400 | self.assertIsInstance(test, str) |
||
401 | |||
402 | def test_patch(self): |
||
403 | self.mock_patch.return_value = Mock() |
||
404 | self.mock_patch.return_value.json.return_value = self.data |
||
405 | self.mock_patch.return_value.status_code = 200 |
||
406 | |||
407 | self.mock_get.return_value = Mock() |
||
408 | self.mock_get.return_value.json.return_value = self.data |
||
409 | self.mock_get.return_value.status_code = 200 |
||
410 | |||
411 | self.sample.patch(sample_data={'title': 'new title'}) |
||
412 | |||
413 | def test_delete(self): |
||
414 | self.mock_delete.return_value = Mock() |
||
415 | self.mock_delete.return_value.last_response = '' |
||
416 | self.mock_delete.return_value.status_code = 204 |
||
417 | |||
418 | self.sample.delete() |
||
419 |