max / makenotwork
- Co-Authored-By
- Claude Opus 5 (1M context) <noreply@anthropic.com>
- Claude-Session
- https://claude.ai/code/session_01DwpiantpUgohzML4xr6KeQ
4 files changed,
+189 insertions,
-2 deletions
| @@ -13,8 +13,16 @@ | |||
| 13 | 13 | # equivalent at a glance and were not. So: try to kill it first, and add an | |
| 14 | 14 | # entry only with the reason written out. | |
| 15 | 15 | # | |
| 16 | - | # The patterns are regexes, so `+`, `*` and `|` need escaping. A bare `||` is an | |
| 17 | - | # empty alternation and silently excludes every mutant in the crate. | |
| 16 | + | # The patterns are regexes matched against the WHOLE listing line, `file:line:col` | |
| 17 | + | # included, so `+`, `*` and `|` need escaping. A bare `||` is an empty | |
| 18 | + | # alternation and silently excludes every mutant in the crate. | |
| 19 | + | # | |
| 20 | + | # An entry carries a line number when the description alone would also match a | |
| 21 | + | # mutant that is being CAUGHT -- cargo-mutants names a mutant by its operator and | |
| 22 | + | # its enclosing function and nothing else, so one function with two similar | |
| 23 | + | # operators needs the position to tell them apart. A stale anchor stops matching | |
| 24 | + | # and the mutant returns as a visible survivor, which is the safe direction. | |
| 25 | + | # Re-derive by diffing `cargo mutants --list` with and without this file. | |
| 18 | 26 | ||
| 19 | 27 | exclude_re = [ | |
| 20 | 28 | # The two oracle wrappers in `mod oracle`. An oracle asserts what must hold | |
| @@ -28,4 +36,11 @@ | |||
| 28 | 36 | # were caught, and they were caught AT THESE ASSERTIONS. | |
| 29 | 37 | "replace oracle::check_plan with \\(\\)", | |
| 30 | 38 | "replace oracle::check_auto with \\(\\)", | |
| 39 | + | ||
| 40 | + | # `while bytes_read < part_size` -> `<=` in the part-filling loop. At | |
| 41 | + | # equality the extra iteration reads into `&mut buf[part_size..]`, an empty | |
| 42 | + | # slice, and a read into an empty buffer returns `Ok(0)` without touching | |
| 43 | + | # the file -- which is the loop's own break arm. One wasted call to `read` | |
| 44 | + | # and an identical buffer, so no observation distinguishes it. | |
| 45 | + | "src/lib.rs:1072:30: replace < with <= in S3Client::run_multipart_upload", | |
| 31 | 46 | ] |
| @@ -1621,6 +1621,57 @@ | |||
| 1621 | 1621 | ); | |
| 1622 | 1622 | } | |
| 1623 | 1623 | ||
| 1624 | + | /// A page may name only ONE of the two markers, and that is still a page to | |
| 1625 | + | /// follow. The guard reads "neither marker", so it is an `&&`; an `||` there | |
| 1626 | + | /// stops on the first page whose key marker happens to be the only one set, | |
| 1627 | + | /// silently returning a short list to the orphan reaper -- which then leaves | |
| 1628 | + | /// the parts it did not see billing forever. | |
| 1629 | + | /// | |
| 1630 | + | /// `ListMultipartUploads` returns `NextUploadIdMarker` only when the page | |
| 1631 | + | /// splits a key's uploads, so a page ending on a key boundary carries the | |
| 1632 | + | /// key marker alone. That is the ordinary case, not a corner. | |
| 1633 | + | #[tokio::test] | |
| 1634 | + | async fn a_page_naming_only_the_key_marker_is_still_followed() { | |
| 1635 | + | let (client, replay) = replay_client(vec![ | |
| 1636 | + | xml_ok( | |
| 1637 | + | r#"<?xml version="1.0" encoding="UTF-8"?> | |
| 1638 | + | <ListMultipartUploadsResult xmlns="http://s3.amazonaws.com/doc/2006-03-01/"> | |
| 1639 | + | <Bucket>test-bucket</Bucket> | |
| 1640 | + | <IsTruncated>true</IsTruncated> | |
| 1641 | + | <NextKeyMarker>staging/abc</NextKeyMarker> | |
| 1642 | + | <Upload> | |
| 1643 | + | <Key>staging/abc</Key> | |
| 1644 | + | <UploadId>upload-one</UploadId> | |
| 1645 | + | <Initiated>2026-08-31T00:00:00.000Z</Initiated> | |
| 1646 | + | </Upload> | |
| 1647 | + | </ListMultipartUploadsResult>"#, | |
| 1648 | + | ), | |
| 1649 | + | xml_ok( | |
| 1650 | + | r#"<?xml version="1.0" encoding="UTF-8"?> | |
| 1651 | + | <ListMultipartUploadsResult xmlns="http://s3.amazonaws.com/doc/2006-03-01/"> | |
| 1652 | + | <Bucket>test-bucket</Bucket> | |
| 1653 | + | <IsTruncated>false</IsTruncated> | |
| 1654 | + | <Upload> | |
| 1655 | + | <Key>staging/abc</Key> | |
| 1656 | + | <UploadId>upload-two</UploadId> | |
| 1657 | + | <Initiated>2026-08-31T00:00:00.000Z</Initiated> | |
| 1658 | + | </Upload> | |
| 1659 | + | </ListMultipartUploadsResult>"#, | |
| 1660 | + | ), | |
| 1661 | + | ]); | |
| 1662 | + | ||
| 1663 | + | let ids = client | |
| 1664 | + | .list_multipart_uploads_for_key("staging/abc") | |
| 1665 | + | .await | |
| 1666 | + | .expect("one marker is enough to continue"); | |
| 1667 | + | ||
| 1668 | + | assert_eq!( | |
| 1669 | + | ids, | |
| 1670 | + | vec!["upload-one".to_string(), "upload-two".to_string()] | |
| 1671 | + | ); | |
| 1672 | + | assert_eq!(replay.actual_requests().count(), 2); | |
| 1673 | + | } | |
| 1674 | + | ||
| 1624 | 1675 | /// infra `a536db81`. `configure_cors` returns `()` and the crate exposed no | |
| 1625 | 1676 | /// readback, so replacing its whole body with `()` was invisible to every | |
| 1626 | 1677 | /// test that could be written against it. The request it sends is the |
| @@ -2098,4 +2098,43 @@ | |||
| 2098 | 2098 | "abc should fuzzy-match cab.x via segment 'cab' (distance 2); got {results:?}" | |
| 2099 | 2099 | ); | |
| 2100 | 2100 | } | |
| 2101 | + | ||
| 2102 | + | /// The two-stage length check reads bytes first because byte length is an | |
| 2103 | + | /// exact upper bound on character count, so the cheap test gates the O(n) | |
| 2104 | + | /// one. That makes the CHARACTER comparison the one that decides, and | |
| 2105 | + | /// mutation found it could become `>=` unnoticed: every existing test uses | |
| 2106 | + | /// ASCII, where the byte gate is already false at the boundary and masks it. | |
| 2107 | + | /// | |
| 2108 | + | /// A multi-byte tag is what separates them. Ten Greek letters are twenty | |
| 2109 | + | /// bytes and ten characters, so the byte gate opens and the character gate | |
| 2110 | + | /// decides. `>=` there rejects a tag of exactly `max_length` characters as | |
| 2111 | + | /// too long; the crate rejects it for its charset instead, which is the | |
| 2112 | + | /// error a caller has to act on. | |
| 2113 | + | #[test] | |
| 2114 | + | fn a_tag_of_exactly_max_length_characters_is_not_too_long() { | |
| 2115 | + | const CFG: TagConfig = TagConfig { | |
| 2116 | + | max_depth: 5, | |
| 2117 | + | max_length: 10, | |
| 2118 | + | semantic_depth: 0, | |
| 2119 | + | }; | |
| 2120 | + | ||
| 2121 | + | assert_eq!( | |
| 2122 | + | validate_with("abcdefghij", &CFG), | |
| 2123 | + | Ok(()), | |
| 2124 | + | "10 ASCII, 10 bytes" | |
| 2125 | + | ); | |
| 2126 | + | assert_eq!( | |
| 2127 | + | validate_with("abcdefghijk", &CFG), | |
| 2128 | + | Err(TagError::TooLong { max: 10 }), | |
| 2129 | + | "11 characters is over" | |
| 2130 | + | ); | |
| 2131 | + | ||
| 2132 | + | // 10 characters, 20 bytes: over the byte bound, exactly on the | |
| 2133 | + | // character bound. The charset is what rejects it, not the length. | |
| 2134 | + | assert_eq!( | |
| 2135 | + | validate_with("αβγδεζηθικ", &CFG), | |
| 2136 | + | Err(TagError::InvalidChar('α')), | |
| 2137 | + | "the character count is the bound, and 10 is not over 10" | |
| 2138 | + | ); | |
| 2139 | + | } | |
| 2101 | 2140 | } |
| @@ -1,0 +1,82 @@ | |||
| 1 | + | # Mutants that NO test can kill, with the reason each one is unreachable by any | |
| 2 | + | # test that could be written. | |
| 3 | + | # | |
| 4 | + | # THE BAR IS IMPOSSIBILITY, NOT COST. A mutant that a test could kill, where | |
| 5 | + | # nobody has written that test, does not belong here however unappealing the | |
| 6 | + | # test looks. It stays a visible survivor with a GoingsOn task against it. The | |
| 7 | + | # two classes are easy to conflate and the difference is the whole value of the | |
| 8 | + | # number: an exclusion list that also absorbs "not worth it" reports zero while | |
| 9 | + | # real coverage gaps sit underneath it. | |
| 10 | + | # | |
| 11 | + | # The patterns are regexes matched against the WHOLE listing line, `file:line:col` | |
| 12 | + | # included, so `+`, `*`, `|`, `(` and `.` need escaping. A bare `||` is an empty | |
| 13 | + | # alternation and silently excludes every mutant in the crate. | |
| 14 | + | # | |
| 15 | + | # WHY MOST OF THESE CARRY A LINE NUMBER. cargo-mutants names a mutant by its | |
| 16 | + | # operator and its enclosing function, and nothing else: three different `>` in | |
| 17 | + | # `validate_with` all render as "replace > with >= in validate_with", and one of | |
| 18 | + | # the three is a real coverage gap that `a_tag_of_exactly_max_length_characters_ | |
| 19 | + | # is_not_too_long` kills. A pattern written on the description alone excluded | |
| 20 | + | # that one too, which is precisely the failure this file's own bar warns about. | |
| 21 | + | # Measured the same way for `edit_distance` and `suggest`, where the loose | |
| 22 | + | # patterns swallowed four mutants that were being CAUGHT. | |
| 23 | + | # | |
| 24 | + | # Line numbers go stale when the file moves, and that is the safe direction: a | |
| 25 | + | # stale anchor stops matching, and the mutant comes back as a visible survivor | |
| 26 | + | # rather than silently staying excluded. Re-derive them by diffing | |
| 27 | + | # `cargo mutants --list` with and without this file. | |
| 28 | + | ||
| 29 | + | exclude_re = [ | |
| 30 | + | # --- The segment index is an optimisation and nothing observes it ----- | |
| 31 | + | # | |
| 32 | + | # `TagIndex::segments` has exactly one reader, `any_segment_starts_with`, | |
| 33 | + | # and that has exactly one caller: the guard on tier 2 of `suggest`. Every | |
| 34 | + | # condition in that guard is re-checked inside the loop it guards -- the | |
| 35 | + | # limit at the top of each iteration, and `seg.starts_with(input)` per | |
| 36 | + | # segment -- so the guard decides how much work runs and never what comes | |
| 37 | + | # back. That makes six mutants unobservable through the public API, and no | |
| 38 | + | # test can observe "the same answer, reached the slow way". | |
| 39 | + | # | |
| 40 | + | # The two `rebuild` guards are the same fact one step removed. `TagIndex:: | |
| 41 | + | # remove` deliberately does not prune orphaned segments, so a rebuild after | |
| 42 | + | # a no-op edit genuinely changes `segments` -- and changes nothing anyone | |
| 43 | + | # can read. | |
| 44 | + | "replace TagIndex::any_segment_starts_with -> bool with true", | |
| 45 | + | "src/lib.rs:891:26: replace < with <= in TagIndex::suggest", | |
| 46 | + | "src/lib.rs:892:13: replace && with \\|\\| in TagIndex::suggest", | |
| 47 | + | "src/lib.rs:893:13: replace && with \\|\\| in TagIndex::suggest", | |
| 48 | + | "src/lib.rs:616:14: replace > with >= in rename_prefix_bulk", | |
| 49 | + | "src/lib.rs:645:16: replace > with >= in remove_subtree", | |
| 50 | + | ||
| 51 | + | # --- Bounds that cannot be reached ------------------------------------ | |
| 52 | + | # | |
| 53 | + | # `tag.len() > max_length && tag.chars().count() > max_length`. The byte | |
| 54 | + | # test is a gate on the O(n) character test because byte length is an exact | |
| 55 | + | # upper bound on character count. `>=` on the BYTE side differs only when | |
| 56 | + | # `len == max && chars > max`, and `chars <= len` makes that impossible. | |
| 57 | + | # (The character side is a different matter and is killed by | |
| 58 | + | # `a_tag_of_exactly_max_length_characters_is_not_too_long`.) | |
| 59 | + | "src/lib.rs:166:18: replace > with >= in validate_with", | |
| 60 | + | ||
| 61 | + | # `config.semantic_depth > 0` guarding the minimum-depth check. `>= 0` is | |
| 62 | + | # always true for a usize, so the mutant runs the check with | |
| 63 | + | # `semantic_depth == 0` and asks `d < 1`. `validate_with` rejects the empty | |
| 64 | + | # tag on its first line, and `depth` returns 0 only for the empty string, so | |
| 65 | + | # `d >= 1` always holds by the time this line runs and the extra check | |
| 66 | + | # cannot fire. | |
| 67 | + | "src/lib.rs:189:30: replace > with >= in validate_with", | |
| 68 | + | ||
| 69 | + | # `tag.starts_with(prefix) && tag.len() > prefix.len() && ...` in `subtree`. | |
| 70 | + | # `>=` differs only at equal lengths, and a tag that starts with the prefix | |
| 71 | + | # and has its length IS the prefix -- which the preceding `*tag == prefix` | |
| 72 | + | # arm has already returned true for, so the third arm is never evaluated | |
| 73 | + | # there. (Without that short-circuit the mutant would index one past the | |
| 74 | + | # end, which is a different bug.) | |
| 75 | + | "src/lib.rs:490:34: replace > with >= in subtree", | |
| 76 | + | ||
| 77 | + | # The operand swap in `edit_distance`, which puts the shorter string first | |
| 78 | + | # so the DP row is the smaller of the two. `>=` swaps at equal lengths as | |
| 79 | + | # well, and Levenshtein distance is symmetric: same row length, same | |
| 80 | + | # `b.len() - a.len()` of zero for the short-circuit, same distance. | |
| 81 | + | "src/lib.rs:561:29: replace > with >= in edit_distance", | |
| 82 | + | ] |