-
Notifications
You must be signed in to change notification settings - Fork 275
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
ngclient: top-level-roles update tests #1636
ngclient: top-level-roles update tests #1636
Conversation
eaeb091
to
a4655ce
Compare
Pull Request Test Coverage Report for Build 1399263464Warning: This coverage report may be inaccurate.This pull request's base commit is no longer the HEAD commit of its target branch. This means it includes changes from outside the original pull request, including, potentially, unrelated coverage changes.
Details
💛 - Coveralls |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I really like how much cleaner most of this is than either the legacy tests and even some of the comparable trustedmetadataset tests: e.g. test_new_snapshot_version_rollback()
is directly readable -- something I can't say of any rollback tests elsewhere.
The two main issues I have are:
- overall niggles about download count test
- most tests only check raised errors: file existence on disk is not checked, versions or content of the files is not checked. I'd like to see at least explanations of why these are not needed -- especially for the error cases I think we really should be checking that local files are what we expect
Both of these could be solved as followup issues: the tests are certainly already useful.
self.sim.root.expires = datetime.utcnow().replace( | ||
microsecond=0 | ||
) + timedelta(days=5) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I added RepositorySimulator.safe_expiry for this purpose -- it might be in the wrong place but maybe worth using... you could define a similar one for the expired case to avoid the ugly date math in multiple tests
metadata_files_after_refresh = os.listdir(self.metadata_dir) | ||
metadata_files_after_refresh.sort() | ||
self.assertListEqual( | ||
metadata_files_after_refresh, | ||
["root.json", "snapshot.json", "targets.json", "timestamp.json"], | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
these checks are currently done in this one test only but I wonder if it's worth making this file list check more ergonomic and a one-liner (see _assert_files()
in test_updater_ng.py, although yours is better in that it uses assertListEqual) and then do this check in more tests -- I'd really like to be sure our local metadata cache contains what we expect in all situations, especially the failure situations...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I added some convenience methods and a lot of additional checks with 5394c47.
args, kwargs = wrapped_download_metadata.call_args_list[1] | ||
self.assertIn("timestamp", args) | ||
|
||
def test_trusted_root_os_error(self): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
maybe
def test_trusted_root_os_error(self): | |
def test_trusted_root_missing(self): |
root_path = os.path.join(self.metadata_dir, "root.json") | ||
md_root = Metadata.from_file(root_path) | ||
md_root.signed.expires = datetime.utcnow().replace( | ||
microsecond=0 | ||
) - timedelta(days=5) | ||
for signer in self.sim.signers["root"]: | ||
md_root.sign(signer) | ||
md_root.to_file(root_path) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I dislike the pattern where local metadata is modified (unless that's the only way to test something, like test_trusted_root_unsigned()
). I think in this case you could instead
- use simulator to create an expired root,
- refresh (expecting failure)
- check that current local root is now the new expired root
- initialize a new updater: this should succeed since local root is allowed to be expired
with mock.patch.object( | ||
updater, "_download_metadata", wraps=updater._download_metadata | ||
) as wrapped_download_metadata: | ||
updater.refresh() | ||
|
||
self.assertEqual(wrapped_download_metadata.call_count, 2) | ||
for call in wrapped_download_metadata.call_args_list: | ||
args, kwargs = call | ||
self.assertNotIn("snapshot", args) | ||
self.assertNotIn("targets", args) | ||
|
||
args, kwargs = wrapped_download_metadata.call_args_list[0] | ||
self.assertIn("root", args) | ||
args, kwargs = wrapped_download_metadata.call_args_list[1] | ||
self.assertIn("timestamp", args) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
not a huge fan of hooking into updater internal methods... I think implementing download counters in RepositorySimulator would be more reliable and readable.
I'd also like to see this done for more than one single call to refresh() -- it seems like this only tests a single special case when metadata has not been updated in remote: why not verify that the first refresh downloads what you expect as well?
We could leave this test out of this PR, and file a new issue for download/file open tests. If you'd like to include this already, I'd at least ask you to have another look at the call_args_list
parsing: it feels vastly more complex than it should be -- I think you should be able to verify the exact argument for two calls instead of doing four separate assert*In()
calls.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I removed the entire commit. I'll open a follow up issue about testing whether cached metadata is loaded and verified as expected.
I did the weird parsing since the full call list was something like call("root", 123242, 2) but I agree it is ugly.
# intermediate files were downloaded. | ||
|
||
# Create some big number of root files in the repository | ||
highest_repo_root_version = UpdaterConfig.max_root_rotations + 10 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
haha I love this -- why not create 42 different root versions if it means you don't have to modify updater config from default!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oh well ... in order to test the default config :))
I updated the test with randomly chosen smaller values.
root_signers = self.sim.signers["root"] | ||
self.sim.signers["root"].clear() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this does not do what you expect: root_signers is also empty after this
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oh 🤦
with self.assertRaises(ExpiredMetadataError): | ||
self._run_refresh() | ||
|
||
def test_new_targets_hash_mismatch(self): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this test is the only one I don't quite understand... maybe we can catch up on this in chat
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
as we already discussed, I've assumed incorrectly what compute_metafile_hashes_length
does and in the same time its implementation is buggy. I opened #1651 and updated the tests.
def tearDown(self): | ||
self.temp_dir.cleanup() | ||
|
||
def _run_refresh(self) -> Updater: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you add doc to this function? That was a pylint warning I had to address for all functions besides the ones with names such as test_*
, setUp*
and tearDown*
.
For this function in test_updater_with_simulator.py
I had added """Creates a new updater and runs refresh."""
. You decide if you like it or not.
updater.refresh() | ||
return updater | ||
|
||
def _init_updater(self) -> Updater: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you add doc to this function? That was a pylint warning I had to address for all functions besides the ones with names such as test_*
, setUp*
and tearDown*
.
I think we agree on the possible checks but I'll just reiterate so there's no confusion:
There's no need to do all of these for all tests : 1 and 2 could be checked by just some tests, 3 might not need explicit checks but I think 4 is something we should check almost as extensively as 5 |
a4655ce
to
5394c47
Compare
After iterating over the tests again, I added much more checks related to expected contents on disk after refresh. |
|
||
def _assert_content_equals(self, role: str, version: Optional[int]=None) -> None: | ||
"""Assert that local file content is the expected""" | ||
expected_content = self.sim._fetch_metadata(role, version) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've abused using the internal _fetch_metadata
. Should we make it public? Much more convenient when you want to get the repository content in bytes than download_bytes(url, size ...).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed we should somehow make that ergonomic... Your proposal sounds ok to me.
The only alternative I can think of is adding a RepositorySimulator.assert_metadata(directory: str, roles: List[str])
which asserts that directory
contains metadata matching current repository state for roles
-- possibly there's a good default value for roles
as well.
You probably would not need _assert_files_exist() anymore as the new function could do that at the same time.
But I guess then you'd need to handle the failing cases somehow differently... Your call, could leave this as enhancement.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks quite nice to me. Left a few smaller comments.
I think the assert methods could still be improved... but maybe as a follow up? Just to document what it looks like to me, it seems that there are three separate assert types:
- _assert_files_exist
- _assert_content_equals
- _assert_version_equals (this function does not exist but same four lines are repeated a lot). This is mostly used in error cases where client version should not match repository version
The issue I have with this is that it's hard to easily see if a specific test is using the correct asserts or not... Anyway i can see it's not trivial to solve so we can leave as follow up task
updater = Updater( | ||
self.metadata_dir, | ||
"https://example.com/metadata/", | ||
"https://example.com/targets/", | ||
fetcher=self.sim, | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I should have made more noise about the recent constructor API change (sorry): please add the target dir argument to this call
# The expiration of the trusted root metadata file does not lead | ||
# to failure and Updater in successfully initialized. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
something wrong with sentence end here but maybe this could be shorter as well:
# The expiration of the trusted root metadata file does not lead | |
# to failure and Updater in successfully initialized. | |
# Local root metadata can be loaded even if expired |
in general I really appreciate it if we manage to keep comments in tests to single lines -- makes it easier to read
md_root = Metadata.from_file(root_path) | ||
initial_root_version = md_root.signed.version | ||
|
||
updater.refresh() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is there really no error here? I would expect an error. can you check legacy updater and possibly file an issue for ngclient?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Surprisingly (for me too), this is the correct behaviour. The spec treats this case the same way as if the next root version was not found. The legacy code does the same thing.
If this file is not available, or we have downloaded more than Y number of root metadata files (because the exact number is as yet unknown), then go to step 5.3.10.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
that seems like a very weird choice... thanks for checking
# Hash mismatch error | ||
with self.assertRaises(RepositoryError): | ||
self._run_refresh() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I assume the real error is something more specific?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For reasons that I cannot remember now, LengthOrHashMismatchError
does not inherit from RepositoryError
but the latter is raised from it:
except exceptions.LengthOrHashMismatchError as e: |
As a result LengthOrHashMismatchError
is not raised here. Should we reconsider this usage?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks. Hmm, maybe we should reconsider that, but this PR seems fine
# TODO: RepositorySimulator works always with consistent snapshot | ||
# enabled which forces the client to look for the snapshot version | ||
# written in timestamp (which leads to "Unknown snapshot version"). | ||
# This fails the test for a snapshot version mismatch. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
An option is to change the simulator so it does always return metadata, even if the requested version does not match the actual version. I don't think it would break anything... Feel free to just file an issue for this TODO though
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Did it in e9e5965. This means that consistent_snapshot with non-root metadata means nothing for the simulator, though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
well, it still supports consistent_snapshot... but only the latest/current consistent snapshot: trying to download using previous snapshot versions will fail in interesting ways (because the metadata will be incorrect). I think this is fine?
Add ngclient/updater tests following the top-level-roles metadata update from the specification (Detailed client workflow) using RepositorySimulator. Signed-off-by: Teodora Sechkova <[email protected]>
Extend the TestRefresh cases with additional checks for expected metadata files and their content written on the file system. Signed-off-by: Teodora Sechkova <[email protected]>
Fix formatting and some potential linter and typing errors. Signed-off-by: Teodora Sechkova <[email protected]>
Define _assert_version_equals for checking if the local metadata file's version is as expected. Signed-off-by: Teodora Sechkova <[email protected]>
4267abc
to
e9e5965
Compare
I did some improvement of the asserts as you suggested and opened #1669 to figure out a smarter way for asserting the local metadata state. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah looks nice: the assertion changes really improve readability, I can see what's being tested.
No need to block merging for this... but are you able to test how much runtime these tests add? It is a big chunk of tests that creates and verifies a lot of metadata... I think there's a lot of optimization we could do in RepositorySimulator and how we use it but I'm not interested in doing that unless it's a real bottleneck
def test_new_targets_version_mismatch(self): | ||
# Check against snapshot role’s targets version | ||
|
||
# Increase targets version without updating snapshot | ||
self.sim.targets.version += 1 | ||
with self.assertRaises(BadVersionNumberError): | ||
self._run_refresh() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
no _assert_files_exist() here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh yeah, I amended the last commit, thanks.
Except for 'root' role, RepositorySimulator does not keep previous metadata versions, it always serves the latest one. The metadata version check during fetch serves mostly for informative purposes and removing it allows generating test metadata with mismatching version. Signed-off-by: Teodora Sechkova <[email protected]>
e9e5965
to
d66c3ba
Compare
Well testing time is relative but RepositorySimulator seems to be performing good enough for our criteria.
In comparison with test_updater_ng.py and test_api.py which use the pre-generated data on disk:
and key rotation tests:
In total the whole test suite now takes ~25 seconds to complete in my environment. |
Addresses: #1606
Description of the changes being introduced by the pull request:
Tries to address the problem of sticking meaningful tests for ngclient/updater
Trying to answer some pending questions:
Updater._trusted_metadata_set["timestamp] != None
I don't find it fir for these tests, it seems like TrustedMetadataSet tests should accommodate it.Missing tests cases from the spec:
Please verify and check that the pull request fulfills the following
requirements: