Skip to content

Commit ab6f63d

Browse files
committed
Using decorators for comprehensive testing
Jussi in his comment here: #1391 (comment) proposed using decorators when creating comprehensive testing for metadata serialization. The main problems he pointed out is that: 1) there is a lot of code needed to generate the data for each case 2) the test implementation scales badly when you want to add new cases for your tests, then you would have to add code as well 3) the dictionary format is not visible - we are loading external files and assuming they are not changed and valid In this change, I am using a decorator with an argument that complicates the implementation of the decorator and requires three nested functions, but the advantages are that we are resolving the above three problems: 1) we don't need new code when adding a new test case 2) a small amount of hardcoded data is required for each new test 3) the dictionaries are all in the test module without the need of creating new directories and copying data. Signed-off-by: Martin Vrachev <[email protected]>
1 parent 2779954 commit ab6f63d

File tree

3 files changed

+208
-261
lines changed

3 files changed

+208
-261
lines changed

tests/test_api.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -333,7 +333,7 @@ def test_key_class(self):
333333
test_key_dict = key_dict.copy()
334334
del test_key_dict[key]
335335
with self.assertRaises(KeyError):
336-
Key.from_dict(test_key_dict)
336+
Key.from_dict("id", test_key_dict)
337337

338338

339339
def test_role_class(self):
@@ -357,7 +357,7 @@ def test_role_class(self):
357357
test_role_dict = role_dict.copy()
358358
del test_role_dict[role_attr]
359359
with self.assertRaises(KeyError):
360-
Key.from_dict(test_role_dict)
360+
Role.from_dict(test_role_dict)
361361

362362

363363
def test_metadata_root(self):

tests/test_metadata_serialization.py

Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
# Copyright New York University and the TUF contributors
2+
# SPDX-License-Identifier: MIT OR Apache-2.0
3+
4+
""" Unit tests testing tuf/api/metadata.py classes
5+
serialization and deserialization.
6+
7+
"""
8+
9+
import json
10+
import sys
11+
import logging
12+
import unittest
13+
import copy
14+
15+
from typing import Any, Dict, Callable, Type
16+
17+
from tests import utils
18+
19+
from tuf.api.metadata import (
20+
Root,
21+
Snapshot,
22+
Timestamp,
23+
Targets,
24+
Key,
25+
Role,
26+
MetaFile,
27+
TargetFile,
28+
Delegations,
29+
DelegatedRole,
30+
)
31+
32+
logger = logging.getLogger(__name__)
33+
34+
# DataSet is only here so type hints can be used:
35+
# It is a dict of name to test dict
36+
DataSet = Dict[str, Dict[str, Any]]
37+
38+
# Test runner decorator: Runs the test as a set of N SubTests,
39+
# (where N is number of items in dataset), feeding the actual test
40+
# function one data item at a time
41+
def run_sub_tests_with_dataset(dataset: Type[DataSet]):
42+
def real_decorator(function: Callable[["TestSerialization", DataSet], None]):
43+
def wrapper(test_cls: "TestSerialization"):
44+
for case, data in dataset.items():
45+
with test_cls.subTest(case=case):
46+
function(test_cls, data)
47+
return wrapper
48+
return real_decorator
49+
50+
51+
class TestSerialization(unittest.TestCase):
52+
53+
KEY = '{"keytype": "rsa", "scheme": "rsassa-pss-sha256", \
54+
"keyval": {"public": "foo"}}'
55+
SIGNED_COMMON = '"spec_version": "1.0.0", "version": 1, \
56+
"expires": "2030-01-01T00:00:00Z"'
57+
58+
valid_keys: DataSet = {
59+
"all": '{"keytype": "rsa", "scheme": "rsassa-pss-sha256", \
60+
"keyval": {"public": "foo"}}',
61+
}
62+
63+
@run_sub_tests_with_dataset(valid_keys)
64+
def test_key_serialization(self, test_case_data: str):
65+
case_dict = json.loads(test_case_data)
66+
key = Key.from_dict("id", copy.copy(case_dict))
67+
self.assertDictEqual(case_dict, key.to_dict())
68+
69+
70+
valid_roles: DataSet = {
71+
"all": '{"keyids": ["keyid"], "threshold": 3}'
72+
}
73+
74+
@run_sub_tests_with_dataset(valid_roles)
75+
def test_role_serialization(self, test_case_data: str):
76+
case_dict = json.loads(test_case_data)
77+
role = Role.from_dict(copy.deepcopy(case_dict))
78+
self.assertDictEqual(case_dict, role.to_dict())
79+
80+
81+
valid_roots: DataSet = {
82+
"all": f'{{ "_type": "root", {SIGNED_COMMON}, \
83+
"consistent_snapshot": false, "keys": {{"keyid" : {KEY} }}, \
84+
"roles": {{ "targets": {{"keyids": ["keyid"], "threshold": 3}} }} \
85+
}}',
86+
"no consistent_snapshot": f'{{ "_type": "root", {SIGNED_COMMON}, \
87+
"keys": {{"keyid" : {KEY}}}, \
88+
"roles": {{ "targets": {{"keyids": ["keyid"], "threshold": 3}} }} \
89+
}}',
90+
}
91+
92+
@run_sub_tests_with_dataset(valid_roots)
93+
def test_root_serialization(self, test_case_data: str):
94+
case_dict = json.loads(test_case_data)
95+
root = Root.from_dict(copy.deepcopy(case_dict))
96+
self.assertDictEqual(case_dict, root.to_dict())
97+
98+
valid_metafiles: DataSet = {
99+
"all": '{"hashes": {"sha256" : "abc"}, "length": 12, "version": 1}',
100+
"no length": '{"hashes": {"sha256" : "abc"}, "version": 1 }',
101+
"no hashes": '{"length": 12, "version": 1}'
102+
}
103+
104+
@run_sub_tests_with_dataset(valid_metafiles)
105+
def test_metafile_serialization(self, test_case_data: str):
106+
case_dict = json.loads(test_case_data)
107+
metafile = MetaFile.from_dict(copy.copy(case_dict))
108+
self.assertDictEqual(case_dict, metafile.to_dict())
109+
110+
111+
valid_timestamps: DataSet = {
112+
"all": f'{{ "_type": "timestamp", {SIGNED_COMMON}, \
113+
"meta": {{ "snapshot.json": {{ "hashes": {{"sha256" : "abc"}}, "version": 1 }} }} \
114+
}}'
115+
}
116+
117+
@run_sub_tests_with_dataset(valid_timestamps)
118+
def test_timestamp_serialization(self, test_case_data: str):
119+
case_dict = json.loads(test_case_data)
120+
timestamp = Timestamp.from_dict(copy.deepcopy(case_dict))
121+
self.assertDictEqual(case_dict, timestamp.to_dict())
122+
123+
124+
valid_snapshots: DataSet = {
125+
"all": f'{{ "_type": "snapshot", {SIGNED_COMMON}, \
126+
"meta": {{ "file.txt": \
127+
{{ "hashes": {{"sha256" : "abc"}}, "version": 1 }} }} }}'
128+
}
129+
130+
@run_sub_tests_with_dataset(valid_snapshots)
131+
def test_snapshot_serialization(self, test_case_data: str):
132+
case_dict = json.loads(test_case_data)
133+
snapshot = Snapshot.from_dict(copy.deepcopy(case_dict))
134+
self.assertDictEqual(case_dict, snapshot.to_dict())
135+
136+
137+
valid_delegated_roles: DataSet = {
138+
"no hash prefix attribute":
139+
'{"keyids": ["keyid"], "name": "a", "paths": ["fn1", "fn2"], \
140+
"terminating": false, "threshold": 1}',
141+
"no path attribute":
142+
'{"keyids": ["keyid"], "name": "a", "terminating": false, \
143+
"path_hash_prefixes": ["h1", "h2"], "threshold": 99}',
144+
"no hash or path prefix":
145+
'{"keyids": ["keyid"], "name": "a", "terminating": true, "threshold": 3}',
146+
}
147+
148+
@run_sub_tests_with_dataset(valid_delegated_roles)
149+
def test_delegated_role_serialization(self, test_case_data: str):
150+
case_dict = json.loads(test_case_data)
151+
deserialized_role = DelegatedRole.from_dict(copy.copy(case_dict))
152+
self.assertDictEqual(case_dict, deserialized_role.to_dict())
153+
154+
155+
valid_delegations: DataSet = {
156+
"all": f'{{"keys": {{"keyid" : {KEY}}}, "roles": [ {{"keyids": ["keyid"], \
157+
"name": "a", "terminating": true, "threshold": 3}} ]}}'
158+
}
159+
160+
@run_sub_tests_with_dataset(valid_delegations)
161+
def test_delegation_serialization(self, test_case_data: str):
162+
case_dict = json.loads(test_case_data)
163+
delegation = Delegations.from_dict(copy.deepcopy(case_dict))
164+
self.assertDictEqual(case_dict, delegation.to_dict())
165+
166+
167+
valid_targetfiles: DataSet = {
168+
"all": '{"length": 12, "hashes": {"sha256" : "abc"}, \
169+
"custom" : {"foo": "bar"} }',
170+
"no custom": '{"length": 12, "hashes": {"sha256" : "abc"}}'
171+
}
172+
173+
@run_sub_tests_with_dataset(valid_targetfiles)
174+
def test_targetfile_serialization(self, test_case_data: str):
175+
case_dict = json.loads(test_case_data)
176+
target_file = TargetFile.from_dict(copy.copy(case_dict))
177+
self.assertDictEqual(case_dict, target_file.to_dict())
178+
179+
180+
valid_targets: DataSet = {
181+
"all attributes": f'{{"_type": "targets", {SIGNED_COMMON}, \
182+
"targets": {{ "file.txt": {{"length": 12, "hashes": {{"sha256" : "abc"}} }} }}, \
183+
"delegations": {{"keys": {{"keyid" : {KEY}}}, \
184+
"roles": [ {{"keyids": ["keyid"], "name": "a", "terminating": true, "threshold": 3}} ]}} \
185+
}}',
186+
"empty targets": f'{{"_type": "targets", {SIGNED_COMMON}, \
187+
"targets": {{}}, \
188+
"delegations": {{"keys": {{"keyid" : {KEY}}}, \
189+
"roles": [ {{"keyids": ["keyid"], "name": "a", "terminating": true, "threshold": 3}} ] \
190+
}} }}',
191+
"no delegations": f'{{"_type": "targets", {SIGNED_COMMON}, \
192+
"targets": {{ "file.txt": {{"length": 12, "hashes": {{"sha256" : "abc"}} }} }} \
193+
}}'
194+
}
195+
196+
@run_sub_tests_with_dataset(valid_targets)
197+
def test_targets_serialization(self, test_case_data):
198+
case_dict = json.loads(test_case_data)
199+
targets = Targets.from_dict(copy.deepcopy(case_dict))
200+
self.assertDictEqual(case_dict, targets.to_dict())
201+
202+
203+
# Run unit test.
204+
if __name__ == '__main__':
205+
utils.configure_test_logging(sys.argv)
206+
unittest.main()

0 commit comments

Comments
 (0)