Commit d75b16a
authored
🧪 test(coverage): drive the error arms no test reached (#2178)
* 🧪 test(coverage): walk every arm in one binary
llvm-cov scores a function by its best instantiation, not by the union
across them, so an arm exercised only in a different compilation of the
same crate still reports as uncovered. Two functions were split that way
and neither showed up in lcov, because the merged file view lcov renders
does count those arms as covered.
TransportError::terminal_reason had four variants with no case in
transport_error_contract, leaving peryx-ha's own test compilation short
of the arms another compilation happened to walk. logging_layer had the
mirror image: the sink loop drove Journald and Syslog while Stdout and
File were reached only from elsewhere, so no single compilation covered
the whole match.
Both fixes extend an existing parameterised test rather than adding a
new one. A new test with a new type would add another partial view and
make the reported number worse.
* 🧪 test(coverage): drive the report error arms
A sort comparator never runs on a collection of fewer than two elements,
and every cache-inspection fixture had exactly one index and one driver.
So neither ordering the report documents had ever executed: not the
longest-first attribution that decides which index a key belongs to, nor
the by-name ecosystem order that keeps two runs diffable. Both now have
a fixture with something to reorder.
The ecosystem case needs four ecosystems rather than two. The registry
is a hash map, so with a single other ecosystem an unsorted report comes
out in the right order half the time and the test would pass against a
missing sort.
The OCI metadata check reported success on two failures it could not
survive: a manifest scan that cannot read the store, and a finding that
cannot be written out. Neither arm had a test because neither redb nor a
Vec writer fails on its own, so both go through a seam - the storage
backend fault for the read, a writer that refuses for the report.
* 🧪 test(coverage): pin the blob scan failure arm
A descriptor check reads the blob store to decide whether a layer is
present, and that read can fail for reasons that are not absence. The
arm that tells those apart had no test, because a filesystem blob store
does not fail on its own once its root exists.
Rooting the store at a regular file makes every path under it fail as
not a directory, which is a plain io error rather than a missing file.
The test asserts nothing was written as well as that the call failed:
the finding the check must not produce here is "missing blob", and an
empty report is what distinguishes a surfaced failure from a guess.
* 🧪 test(coverage): pin the tag scan failure arm
The tag scan is the second read the metadata check makes, so injecting a
storage failure at the first opportunity never reaches it - the manifest
scan consumes the injection. Sweeping the injection point finds the run
that fails during the tags instead.
Both reads fail with the same text, so the error cannot say which one
failed and the report has to. An empty report means the manifest scan
failed before writing anything; a report carrying the manifest findings
means those were written and the tag scan then failed.
A store handle does not survive its own injected failure, so each sweep
step reopens the retained pages rather than reusing one handle. Reusing
it leaves every step after the first failing in the manifest scan, which
looks exactly like "no injection point reaches the tags".
* 🧪 test(coverage): reject a partial summary count
The row count is assembled from two prefix scans, and neither scan's
failure arm had ever run, so nothing established what a failure partway
through returns. A count that comes back short is the dangerous shape
here: it reads as a real answer, and an operator cannot tell it from a
store that genuinely holds fewer rows.
Sweeping the injection point across both scans asserts the property
rather than one arm - every point either returns the whole count or an
error, and none returns a smaller one. The sweep also reaches both
failure arms, which one fixed injection point cannot.
pypi gains the fault-injection dev-dependencies the OCI crate already
carries. Resolver 3 keeps dev-dependency features out of the normal
build, so this stays test-only.
* 🧪 test(coverage): reject partial audit and repair results
The audit assembles its defect list from four scans and none of their
failure arms had run, so nothing established what a failure partway
through returns. A short list of defects is a false all-clear in the
same way a short count is a wrong answer: an operator reads two defects
as the whole truth and stops looking.
The repair has the matching shape one level down. Its write either lands
or it does not, and a store left with some rows rebuilt and some not
reports as a smaller set of defects on the next audit, which reads as
progress rather than as a repair that failed. The sweep asserts the
store settles at either its original defects or none, never between.
Both drive the property rather than the arm, which is also what reaches
every scan: a single fixed injection point can only fail one of them.
* 🧪 test(coverage): reject a short problem count
The metadata check sums problems across nine scans and the failure arm
of five of them had never run, so nothing established what a failure
partway through reports. A short problem count is the same false
all-clear as a short defect list: an operator reads "three problems" as
the whole truth and fixes three.
The sweep asserts the count and the report together, so a scan that
fails after writing part of its findings cannot pass by arriving at the
right number with the wrong lines. Sweeping is also what reaches all
five arms, since one injection point can only fail one scan.
* 🧪 test(coverage): drive the cache blob scan failure
Both cache reports end by scanning the blob store, and neither scan's
failure arm had run. The existing failure cases all come from the driver
seam, which returns before either report reaches its blob scan.
Rooting the store at a regular file does not work here, because a scan
skips a digest directory that does not exist and reports no blobs - the
seam would be silently ignored and the request would answer 200 with an
empty blob list. A regular file where the digest directory belongs is a
root that exists and cannot be read, which is what makes the scan fail.
The cases extend the existing parameterised failure test rather than
adding two of their own, so the failure seam stays in one place.
* 🧪 test(coverage): order the repair report by ecosystem
The repair report sorts its ecosystems because the driver registry is a
hash map, but every fixture registered one repairable ecosystem, so the
comparator never ran and the ordering it promises had never been checked.
Four ecosystems rather than two. With one other ecosystem an unsorted
report comes out in the right order half the time, so the test would
agree with a missing sort as often as not; four takes that to one in
twenty-four. The assertion is the whole report rather than a property of
it, so a wrong order fails on the line that is wrong.
The driver double gains the repair capability it lacked, which keeps the
test beside the driver set it exercises rather than rebuilding a plugin
registry to reach the same two calls.
* 🧪 test(coverage): reject a short journal page
The journal read propagates a store failure but nothing had ever driven
that arm, so nothing established what a failure partway through a page
returns. A short page is the damaging shape: a reader treats what it
gets as everything after its cursor, so entries dropped by a failed read
are never asked for again and the reader believes it is caught up.
The sweep asserts the whole snapshot rather than the entry count, since
a page that ends early still reports the serial it reached and a
count-only check would accept a page with the wrong cursor.
* 🧪 test(coverage): reject a short prefix scan
Both driver prefix scans propagate a store failure and neither arm had
run, so nothing established what a failure partway through a scan hands
back. The rows already gathered are the damaging answer: a caller that
picked a generation from this snapshot reads the short list as the whole
of it and works from a prefix it never finished reading.
The bounded and unbounded scans get one sweep each rather than one
covering both, since a handle does not survive its own injected failure
and the second call would run against a store the first already broke.
* 🧪 test(coverage): reject a short expired-upload scan
The reaper deletes whatever the expired-upload scan returns, so a scan
that fails partway and hands back the rows it had gathered leaves the
sessions it skipped holding their reserved bytes, with nothing arranged
to look at them again. The sweep asserts the scan returns the whole list
or an error.
A stored manifest checkpoint that cannot be decoded gets the matching
treatment one layer up. The decode is read to answer whether a request
already committed, so treating a corrupt record as absent would let that
request run a second time. The round trip is asserted beside it as the
control, since a decode that rejected everything would pass without it.
* 🧪 test(coverage): pin two blob store failure arms
A usage figure assembled from a walk that stopped early is not a usage
figure. Reporting the bytes it reached would understate what is on disk,
and a caller sizing a sweep from that number frees less than it believed
it would. The sweep already had this test for an unreadable store; the
usage walk beside it did not.
The multipart size check runs before the first request, so an object
past the protocol limit is refused against a bucket nobody contacted.
That refusal has to name the backend and the operation, or an operator
reads a bare size complaint with nothing to attach it to. The assertion
is the whole rendered message rather than the error kind, since the kind
alone is identical whether the context survived or was dropped.
* 🧪 test(coverage): match the source a routed 304 names
A routed upstream names which configured source answered, and only a
generation that same source published may be reused on a 304. Without
the match a source swapped in behind one index would have its "not
modified" applied to files a different source had published.
Every existing sync test drives a plain client, which leaves the
answering source absent and short-circuits the comparison before it
runs. Naming a source at all takes a router, so the test builds one.
The page refresh path has a sibling test for a 304 from a source that
never answered. This is the generation path, which reaches the same
question through different code.
* 🧪 test(pypi): serve a cached page through a rate limit
A rate-limited refresh is about the refresh, not about the copy peryx
already holds, so a page still inside its stale bound is served rather
than the failure reaching the caller. Every other rate-limit test runs
with no cached page at all, which settles the question before the bound
is ever read.
This does not close the branch that weighs the bound against a 429
directly. Measurement puts that line still uncovered: the client retries
a 429 and surfaces it as an upstream failure, so the arm matching an
Ok response with that status is reached another way, if at all.
* 🧪 test(pypi): weigh a cached page against a rate limit
A rate-limited refresh is about the refresh, not about the copy peryx
already holds. A page still inside its stale bound is served; once it
ages past the bound there is nothing to fall back on and the rate limit
reaches the caller as a rate limit, which says when to come back, rather
than as a gateway failure, which says nothing.
Three details decide which arm answers, and getting any of them wrong
lands on a neighbour that returns the same shape of failure. The
fixture is built here rather than through the stale-page helper, which
mounts a 503 on this same path and would be matched instead. The
retry-after has to exceed what the client will honour, or the response
is retried rather than handed back. And the request asks for HTML,
since the JSON path tries the stream first and logs its failure before
falling through.
* 🧪 test(pypi): keep a purge from preserving too little
A purge decides what to keep from a scan of the project's file rows, so
a scan that fails partway must not hand back a smaller preserved set.
Preserving less means removing more, and the rows it would take are the
ones a project populated by a catalog sync needs for a cold download.
The sweep runs as a dry run, so the store is identical at every
injection point and any difference in the report comes from the scan
rather than from what an earlier step deleted.
The stale-bound refusal beside it now names the status it expects. It
asserted only that the answer was not OK, which several arms satisfy,
and that looseness already sent one reading of this file wrong.
* 🧪 test(oci): commit no manifest without its checkpoint
The checkpoint is what a retry reads to decide the work is already
done, so a manifest committed without one is published again on the
next attempt as though it had never landed. Nothing had driven the
failure arm of that write, and every existing publish test passes no
operation at all, which skips the checkpoint entirely.
Each sweep step gets its own backend, because a publish mutates and the
next injection point would otherwise run against whatever the previous
one left behind. The fault is armed after the store opens so the count
applies to the publish rather than to creating the tables.
* 🧪 test(oci): refuse an oversized referrers document
A referrers document past the manifest bound is refused by size rather
than read in, so an upstream cannot spend peryx's memory on a body it
would reject anyway. The bound had no test on this path.
The assertion names the reason as well as the status. A failing proxy
answers with the same status from the arm beside this one, so a status
on its own would not tell a refused size from a refused upstream.
* 🧪 test(oci): leave no torn tag when a delete fails
A tag delete that cannot finish must leave the mapping either as it was
or gone, never pointing somewhere neither the write nor the delete put
it. Nothing had driven the failure arm of the trash write.
The first shape of this test asserted that a failed delete keeps the
tag, and that is not the guarantee: the delete is a soft one, so a
failure part way through can legitimately have trashed the tag already
and still answer 502. The assertion that holds is the narrower one
about what a pull could resolve afterwards.
* 🧪 test(oci): leave no torn tag when a restore fails
The mirror of the delete guarantee. A restore that cannot finish must
leave the tag either still trashed or pointing back at exactly what it
pointed at before, never at a digest neither the original write nor the
restore put there.
The delete that sets the tag up runs unfaulted, so the injection lands
on the restore rather than on the trashing that precedes it.
* 🧪 test(coverage): drive the arms these tests left behind
The gate measures test files too, and three of the harnesses added here
carried arms nothing ran. A coverage push that adds uncovered test code
moves the number the wrong way while looking like progress.
The repair report is now asserted for a rebuild as well as a preview,
which reaches the double's repair path and checks the ordering holds on
both. The report writer is a zero-capacity cursor rather than a type of
our own, since a hand-rolled one carries a flush this path never calls.
The publish sweep needed more than its dead branch removing. Every step
was failing because nothing had claimed the operation, so the injected
fault decided nothing and the success arm could not run. Claiming it
first makes the sweep exercise what it describes, and a control publish
proves the range contains a success at all. The invariant it asserts is
now the one the name claims: the manifest and its checkpoint are both
there or neither is, which holds at every injection point.
* 🧪 test(coverage): pin three worker and request failures
An export whose driver comes apart takes the worker with it, so nothing
sends the summary the request waits on. That wait has to end as an
interruption rather than hanging on a sender that will never write. A
driver returning without a snapshot does not reach it: the export
validates that itself and forwards a store error through the channel.
A driver that opens a second snapshot has already sent the first, so the
request is answered and the violation reaches the caller through the
body already streaming. Both doubles existed and were only ever driven
through the collect path, never through the export.
A request that cannot be built never reached a server, so it carries no
status; reporting one would attribute the failure to a response nobody
received. A scratch directory that cannot be created names the path it
failed on, since the profile write that follows depends on it.
* 🧪 test(peryx): name the backup config a restore cannot read
A restore reads the backup config before it verifies anything, so a
config that is not text is the first thing an operator hears about a
damaged backup. The failure has to name the file it was reading rather
than surfacing a bare encoding error with nothing attached to it.
The verify path reaches the same read, but only for a config whose hash
already matched, which bytes that fail to decode cannot do. The restore
path has no such check in front of it.
* 🧪 test(driver): tell the worker its request is gone
A client that abandons an export leaves nothing waiting for the summary,
so the send that opens the snapshot has nowhere to go. The worker has to
learn the request went rather than carry on producing a body no one will
read.
The order is forced rather than raced. The driver parks inside the plan
and says so, the test drops the request and waits for that drop to
finish, and only then is the driver released to send. Nothing here
depends on which thread wins.1 parent 0df149b commit d75b16a
22 files changed
Lines changed: 1179 additions & 27 deletions
File tree
- crates
- peryx-bench/tests/unit
- peryx-driver/tests
- unit/retention
- peryx-ecosystem-oci/tests/unit
- quota
- registry
- tests
- peryx-ecosystem-pypi
- tests/unit
- admin
- cache/fetch
- store
- journal
- summary
- tests/http
- peryx-ha/tests/unit
- peryx-http/tests/unit/tests
- peryx-identity/tests/unit/tests
- peryx-storage/tests/unit
- blob
- s3
- meta/index
- peryx/tests/unit
- tests/operator
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
923 | 923 | | |
924 | 924 | | |
925 | 925 | | |
| 926 | + | |
| 927 | + | |
| 928 | + | |
| 929 | + | |
| 930 | + | |
| 931 | + | |
| 932 | + | |
| 933 | + | |
| 934 | + | |
| 935 | + | |
| 936 | + | |
| 937 | + | |
| 938 | + | |
| 939 | + | |
| 940 | + | |
| 941 | + | |
| 942 | + | |
| 943 | + | |
| 944 | + | |
| 945 | + | |
| 946 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
6 | 6 | | |
7 | 7 | | |
8 | 8 | | |
9 | | - | |
10 | | - | |
| 9 | + | |
| 10 | + | |
11 | 11 | | |
12 | 12 | | |
13 | 13 | | |
| |||
72 | 72 | | |
73 | 73 | | |
74 | 74 | | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
75 | 97 | | |
76 | 98 | | |
77 | 99 | | |
| |||
461 | 483 | | |
462 | 484 | | |
463 | 485 | | |
| 486 | + | |
| 487 | + | |
| 488 | + | |
| 489 | + | |
| 490 | + | |
| 491 | + | |
| 492 | + | |
| 493 | + | |
| 494 | + | |
| 495 | + | |
| 496 | + | |
| 497 | + | |
| 498 | + | |
| 499 | + | |
| 500 | + | |
| 501 | + | |
| 502 | + | |
| 503 | + | |
| 504 | + | |
| 505 | + | |
| 506 | + | |
| 507 | + | |
| 508 | + | |
| 509 | + | |
| 510 | + | |
| 511 | + | |
| 512 | + | |
| 513 | + | |
| 514 | + | |
| 515 | + | |
| 516 | + | |
| 517 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
655 | 655 | | |
656 | 656 | | |
657 | 657 | | |
| 658 | + | |
| 659 | + | |
| 660 | + | |
| 661 | + | |
| 662 | + | |
| 663 | + | |
| 664 | + | |
| 665 | + | |
| 666 | + | |
| 667 | + | |
| 668 | + | |
| 669 | + | |
| 670 | + | |
| 671 | + | |
| 672 | + | |
| 673 | + | |
| 674 | + | |
| 675 | + | |
| 676 | + | |
| 677 | + | |
| 678 | + | |
| 679 | + | |
| 680 | + | |
| 681 | + | |
| 682 | + | |
| 683 | + | |
| 684 | + | |
| 685 | + | |
| 686 | + | |
| 687 | + | |
| 688 | + | |
| 689 | + | |
| 690 | + | |
| 691 | + | |
| 692 | + | |
| 693 | + | |
| 694 | + | |
| 695 | + | |
| 696 | + | |
| 697 | + | |
| 698 | + | |
| 699 | + | |
| 700 | + | |
| 701 | + | |
| 702 | + | |
| 703 | + | |
| 704 | + | |
| 705 | + | |
| 706 | + | |
| 707 | + | |
| 708 | + | |
| 709 | + | |
| 710 | + | |
| 711 | + | |
| 712 | + | |
| 713 | + | |
| 714 | + | |
| 715 | + | |
| 716 | + | |
| 717 | + | |
| 718 | + | |
| 719 | + | |
| 720 | + | |
| 721 | + | |
| 722 | + | |
| 723 | + | |
| 724 | + | |
| 725 | + | |
| 726 | + | |
| 727 | + | |
| 728 | + | |
| 729 | + | |
| 730 | + | |
| 731 | + | |
| 732 | + | |
| 733 | + | |
| 734 | + | |
| 735 | + | |
| 736 | + | |
| 737 | + | |
| 738 | + | |
| 739 | + | |
| 740 | + | |
| 741 | + | |
| 742 | + | |
| 743 | + | |
| 744 | + | |
| 745 | + | |
| 746 | + | |
| 747 | + | |
| 748 | + | |
| 749 | + | |
| 750 | + | |
| 751 | + | |
| 752 | + | |
| 753 | + | |
| 754 | + | |
| 755 | + | |
| 756 | + | |
| 757 | + | |
| 758 | + | |
| 759 | + | |
| 760 | + | |
| 761 | + | |
| 762 | + | |
| 763 | + | |
| 764 | + | |
| 765 | + | |
| 766 | + | |
| 767 | + | |
| 768 | + | |
| 769 | + | |
| 770 | + | |
| 771 | + | |
| 772 | + | |
| 773 | + | |
| 774 | + | |
| 775 | + | |
| 776 | + | |
| 777 | + | |
| 778 | + | |
| 779 | + | |
| 780 | + | |
| 781 | + | |
| 782 | + | |
| 783 | + | |
| 784 | + | |
| 785 | + | |
| 786 | + | |
| 787 | + | |
| 788 | + | |
| 789 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | 2 | | |
3 | 3 | | |
4 | | - | |
5 | | - | |
| 4 | + | |
| 5 | + | |
6 | 6 | | |
7 | 7 | | |
8 | 8 | | |
| |||
246 | 246 | | |
247 | 247 | | |
248 | 248 | | |
| 249 | + | |
| 250 | + | |
| 251 | + | |
| 252 | + | |
| 253 | + | |
| 254 | + | |
| 255 | + | |
| 256 | + | |
| 257 | + | |
| 258 | + | |
| 259 | + | |
| 260 | + | |
| 261 | + | |
| 262 | + | |
| 263 | + | |
| 264 | + | |
| 265 | + | |
| 266 | + | |
| 267 | + | |
| 268 | + | |
| 269 | + | |
| 270 | + | |
| 271 | + | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
| 275 | + | |
| 276 | + | |
| 277 | + | |
| 278 | + | |
| 279 | + | |
| 280 | + | |
| 281 | + | |
| 282 | + | |
| 283 | + | |
| 284 | + | |
| 285 | + | |
| 286 | + | |
| 287 | + | |
| 288 | + | |
| 289 | + | |
| 290 | + | |
| 291 | + | |
| 292 | + | |
| 293 | + | |
| 294 | + | |
| 295 | + | |
| 296 | + | |
| 297 | + | |
| 298 | + | |
| 299 | + | |
| 300 | + | |
| 301 | + | |
| 302 | + | |
| 303 | + | |
| 304 | + | |
| 305 | + | |
| 306 | + | |
| 307 | + | |
| 308 | + | |
| 309 | + | |
| 310 | + | |
| 311 | + | |
| 312 | + | |
| 313 | + | |
| 314 | + | |
| 315 | + | |
| 316 | + | |
| 317 | + | |
| 318 | + | |
| 319 | + | |
| 320 | + | |
| 321 | + | |
| 322 | + | |
| 323 | + | |
| 324 | + | |
| 325 | + | |
| 326 | + | |
| 327 | + | |
| 328 | + | |
| 329 | + | |
| 330 | + | |
| 331 | + | |
| 332 | + | |
| 333 | + | |
| 334 | + | |
| 335 | + | |
| 336 | + | |
| 337 | + | |
| 338 | + | |
| 339 | + | |
| 340 | + | |
| 341 | + | |
| 342 | + | |
| 343 | + | |
| 344 | + | |
| 345 | + | |
0 commit comments