Skip to content

Commit 089b49d

Browse files
author
Bruno Grande
authored
Merge pull request #19 from Sage-Bionetworks-Workflows/bgrande/tests-suites-cov
Close code coverage for `suites` and `tests` submodules
2 parents 6710930 + 437e766 commit 089b49d

File tree

6 files changed

+172
-56
lines changed

6 files changed

+172
-56
lines changed

src/dcqc/mixins.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ def to_dict(self) -> SerializedObject:
108108
return self.dict_factory(result)
109109

110110
# TODO: Use template method to handle `_serialized_properties`
111+
# as well as `deepcopy()`
111112
@classmethod
112113
@abstractmethod
113114
def from_dict(cls, dictionary: SerializedObject) -> SerializableMixin:

src/dcqc/suites/suite_abc.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,9 @@ def __init__(
5151
test_classes = self.list_test_classes()
5252
test_names = set(test.__name__ for test in test_classes)
5353

54-
required_tests = required_tests or self._default_required_tests()
54+
# To differentiate between None and []
55+
if required_tests is None:
56+
required_tests = self._default_required_tests()
5557
self.required_tests = set(required_tests).intersection(test_names)
5658

5759
skipped_tests = skipped_tests or list()
@@ -216,6 +218,10 @@ def get_subclass_by_file_type(
216218
return registry["*"]
217219
return registry[name]
218220

221+
@property
222+
def tests_by_name(self):
223+
return {test.type: test for test in self.tests}
224+
219225
def compute_tests(self) -> None:
220226
"""Compute the status for each initialized test."""
221227
self.target.stage()
@@ -225,8 +231,9 @@ def compute_tests(self) -> None:
225231
def compute_status(self) -> TestStatus:
226232
"""Compute the overall suite status."""
227233
self.compute_tests()
228-
if self._status is not TestStatus.NONE:
234+
if self._status != TestStatus.NONE:
229235
return self._status
236+
self._status = TestStatus.PASS
230237
for test in self.tests:
231238
test_name = test.type
232239
if test_name not in self.required_tests:

src/dcqc/tests/test_abc.py

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
import shlex
44
from abc import ABC, abstractmethod
55
from collections.abc import Sequence
6-
from dataclasses import dataclass
6+
from copy import deepcopy
7+
from dataclasses import InitVar, dataclass
78
from enum import Enum
89
from importlib import import_module
910
from pathlib import Path
@@ -62,9 +63,9 @@ def skip(self):
6263
"""Force the test to be skipped."""
6364
self._status = TestStatus.SKIP
6465

65-
def get_status(self) -> TestStatus:
66+
def get_status(self, compute_ok: bool = True) -> TestStatus:
6667
"""Compute (if applicable) and return the test status."""
67-
if self._status == TestStatus.NONE:
68+
if self._status == TestStatus.NONE and compute_ok:
6869
self._status = self.compute_status()
6970
return self._status
7071

@@ -141,18 +142,18 @@ def import_module(self, name: str) -> ModuleType:
141142
@dataclass
142143
class Process(SerializableMixin):
143144
container: str
144-
command_args: Sequence[str]
145+
command_args: InitVar[Sequence[str]]
145146
cpus: int = 1
146147
memory: int = 2 # In GB
147148

148-
def get_command(self) -> str:
149-
return shlex.join(self.command_args)
149+
_serialized_properties = ["command"]
150150

151-
def to_dict(self):
152-
dictionary = super(Process, self).to_dict()
153-
del dictionary["command_args"]
154-
dictionary["command"] = self.get_command()
155-
return dictionary
151+
def __post_init__(self, command_args: Sequence[str]):
152+
self._command_args = command_args
153+
154+
@property
155+
def command(self) -> str:
156+
return shlex.join(self._command_args)
156157

157158
@classmethod
158159
def from_dict(cls, dictionary: SerializedObject) -> Process:
@@ -164,6 +165,7 @@ def from_dict(cls, dictionary: SerializedObject) -> Process:
164165
Returns:
165166
The reconstructed process object.
166167
"""
168+
dictionary = deepcopy(dictionary)
167169
command = dictionary.pop("command")
168170
command_args = shlex.split(command)
169171
dictionary["command_args"] = command_args

tests/conftest.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
import pytest
1616

1717
from dcqc.file import File
18+
from dcqc.suites.suite_abc import SuiteABC
19+
from dcqc.target import Target
1820

1921
CNFPATH = Path(__file__).resolve()
2022
TESTDIR = CNFPATH.parent
@@ -52,6 +54,7 @@ def _get_data(filename: str) -> Path:
5254
@pytest.fixture
5355
def test_files(get_data):
5456
txt_path = get_data("test.txt")
57+
jsonld_path = get_data("example.jsonld")
5558
tiff_path = get_data("circuit.tif")
5659
syn_path = "syn://syn50555279"
5760
good_metadata = {
@@ -62,6 +65,10 @@ def test_files(get_data):
6265
"file_type": "tiff",
6366
"md5_checksum": "definitelynottherightmd5checksum",
6467
}
68+
jsonld_metadata = {
69+
"file_type": "JSON-LD",
70+
"md5_checksum": "56bb5f34da6d6df2ade3ac37e25586b7",
71+
}
6572
tiff_metadata = {
6673
"file_type": "tiff",
6774
"md5_checksum": "c7b08f6decb5e7572efbe6074926a843",
@@ -70,6 +77,7 @@ def test_files(get_data):
7077
"good": File(txt_path.as_posix(), good_metadata),
7178
"bad": File(txt_path.as_posix(), bad_metadata),
7279
"tiff": File(tiff_path.as_posix(), tiff_metadata),
80+
"jsonld": File(jsonld_path.as_posix(), jsonld_metadata),
7381
"synapse": File(syn_path, good_metadata),
7482
}
7583

@@ -81,6 +89,22 @@ def test_files(get_data):
8189
yield test_files
8290

8391

92+
@pytest.fixture
93+
def test_targets(test_files):
94+
test_targets = dict()
95+
for name, file in test_files.items():
96+
test_targets[name] = Target(file)
97+
yield test_targets
98+
99+
100+
@pytest.fixture
101+
def test_suites(test_targets):
102+
test_suites = dict()
103+
for name, target in test_targets.items():
104+
test_suites[name] = SuiteABC.from_target(target)
105+
yield test_suites
106+
107+
84108
@pytest.fixture
85109
def get_output():
86110
def _get_output(filename: str) -> Path:

tests/test_suites.py

Lines changed: 54 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,18 @@
33
from dcqc.file import FileType
44
from dcqc.suites.suite_abc import SuiteABC
55
from dcqc.suites.suites import FileSuite, OmeTiffSuite, TiffSuite
6-
from dcqc.target import Target
76
from dcqc.tests.test_abc import TestABC, TestStatus
8-
from dcqc.tests.tests import LibTiffInfoTest
7+
from dcqc.tests.tests import FileExtensionTest, LibTiffInfoTest
8+
9+
FileType("None", ())
10+
FileType("Unpaired", ())
911

1012

1113
class RedundantFileSuite(TiffSuite):
14+
file_type = FileType.get_file_type("None")
1215
del_tests = (LibTiffInfoTest,)
1316

1417

15-
FileType("Unpaired", ())
16-
17-
1818
class DummyTest(TestABC):
1919
def compute_status(self) -> TestStatus:
2020
return TestStatus.NONE
@@ -69,8 +69,52 @@ def test_that_the_generic_file_suite_is_retrieved_for_an_unpaired_file_type():
6969
assert actual is FileSuite
7070

7171

72-
def test_that_the_default_required_tests_are_only_tiers_1_and_2(test_files):
73-
tiff_file = test_files["tiff"]
74-
tiff_target = Target(tiff_file)
75-
tiff_suite = TiffSuite(tiff_target)
76-
assert all(test.tier <= 2 for test in tiff_suite.tests)
72+
def test_that_the_default_required_tests_are_only_tiers_1_and_2(test_suites):
73+
suite = test_suites["tiff"]
74+
assert all(test.tier <= 2 for test in suite.tests)
75+
76+
77+
def test_that_skipped_tests_are_skipped_when_building_suite_from_tests(test_suites):
78+
suite = test_suites["tiff"]
79+
tests = suite.tests
80+
new_suite = SuiteABC.from_tests(tests, skipped_tests=["LibTiffInfoTest"])
81+
skipped_test_before = suite.tests_by_name["LibTiffInfoTest"]
82+
skipped_test_after = new_suite.tests_by_name["LibTiffInfoTest"]
83+
assert skipped_test_before.get_status(compute_ok=False) != TestStatus.SKIP
84+
assert skipped_test_after.get_status(compute_ok=False) == TestStatus.SKIP
85+
86+
87+
def test_for_an_error_when_building_suite_from_tests_with_diff_targets(test_targets):
88+
target_1 = test_targets["good"]
89+
target_2 = test_targets["bad"]
90+
test_1 = FileExtensionTest(target_1)
91+
test_2 = FileExtensionTest(target_2)
92+
tests = [test_1, test_2]
93+
with pytest.raises(ValueError):
94+
SuiteABC.from_tests(tests)
95+
96+
97+
def test_that_a_suite_will_not_consider_unrequired_tests(test_targets):
98+
target = test_targets["bad"]
99+
required_tests = []
100+
skipped_tests = ["LibTiffInfoTest"]
101+
suite = SuiteABC.from_target(target, required_tests, skipped_tests)
102+
suite_status = suite.compute_status()
103+
assert suite_status == TestStatus.PASS
104+
105+
106+
def test_that_a_suite_will_consider_required_tests_when_failing(test_targets):
107+
target = test_targets["bad"]
108+
required_tests = ["FileExtensionTest"]
109+
skipped_tests = ["LibTiffInfoTest"]
110+
suite = SuiteABC.from_target(target, required_tests, skipped_tests)
111+
suite_status = suite.compute_status()
112+
assert suite_status == TestStatus.FAIL
113+
114+
115+
def test_that_a_suite_will_consider_required_tests_when_passing(test_targets):
116+
target = test_targets["good"]
117+
required_tests = ["Md5ChecksumTest"]
118+
suite = SuiteABC.from_target(target, required_tests)
119+
suite_status = suite.compute_status()
120+
assert suite_status == TestStatus.PASS

0 commit comments

Comments
 (0)