test: mutation testing, block boundary tests, exact-fit decode tests (0.3.2)
Added cargo-mutants with 88 mutants tested, 59 caught, 0 missed. Added targeted tests for 254-byte block boundary encoding (interop-verified) and exact-fit/too-small decode buffer sizing for the batch zero path. 111 tests total (was 106). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
5 files changed
+86
-20
No files matched your search
+7
-17
@@ -5,29 +5,19 @@ test_tool = "nextest"
|
|||||||
# are sufficient to catch mutants. Full proptests run separately via just test-unit.
|
# are sufficient to catch mutants. Full proptests run separately via just test-unit.
|
||||||
additional_cargo_test_args = ["-E", "not test(proptests)"]
|
additional_cargo_test_args = ["-E", "not test(proptests)"]
|
||||||
|
|
||||||
# Excluded mutants (false positives):
|
# Excluded mutants (false positives only):
|
||||||
#
|
#
|
||||||
# 1. legacy-msrv const_copy (lines 172-178): dead code under default features,
|
# - legacy-msrv const_copy: dead code under default features, only compiled
|
||||||
# only compiled with --features legacy-msrv. Tested separately in just test-unit.
|
# with --features legacy-msrv. Tested separately.
|
||||||
#
|
#
|
||||||
# 2. usize comparisons `> 0` mutated to `>= 0` or `< 0`: no-ops because usize
|
# - usize `> 0` → `>= 0` or `< 0`: tautological on usize (always >= 0).
|
||||||
# is always >= 0. These are tautological mutations, not real test gaps.
|
|
||||||
#
|
#
|
||||||
# 3. `< 254` → `<= 254` in max_run: changes the block boundary from 254 to 255,
|
# - `< 254` → `<= 254` in max_run: both branches yield 254 when remaining
|
||||||
# which is a 1-instruction difference caught by the COBS spec (code byte 0xFF
|
# is exactly 254, so the mutation is a semantic no-op.
|
||||||
# means exactly 254). Existing tests cover 254/255/256 boundaries but the
|
|
||||||
# mutant still passes because both values produce valid (different) encodings
|
|
||||||
# that round-trip correctly.
|
|
||||||
#
|
|
||||||
# 4. Decode batch bounds check `> dest.len()` → `>= dest.len()`: overly strict
|
|
||||||
# (rejects exact-fit), not unsound. Caught by proptest but not by unit tests
|
|
||||||
# in the fast subset.
|
|
||||||
exclude_re = [
|
exclude_re = [
|
||||||
"replace const_copy with \\(\\)",
|
|
||||||
"in const_copy",
|
"in const_copy",
|
||||||
|
"replace const_copy with \\(\\)",
|
||||||
"replace > with < in encode",
|
"replace > with < in encode",
|
||||||
"replace > with >= in encode",
|
"replace > with >= in encode",
|
||||||
"replace < with <= in encode",
|
"replace < with <= in encode",
|
||||||
"replace > with == in decode",
|
|
||||||
"replace > with >= in decode",
|
|
||||||
]
|
]
|
||||||
@@ -1,5 +1,15 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 0.3.2 — 2026-03-31
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
|
||||||
|
- **Mutation testing passes clean** — 88 mutants tested, 59 caught, 0 missed.
|
||||||
|
Added targeted tests for 254-byte block boundary encoding and exact-fit
|
||||||
|
decode buffer sizing that close gaps found by `cargo-mutants`.
|
||||||
|
- **111 tests** (was 106): added block-boundary interop, exact-fit decode zeros,
|
||||||
|
and too-small decode zeros tests.
|
||||||
|
|
||||||
## 0.3.1 — 2026-03-31
|
## 0.3.1 — 2026-03-31
|
||||||
|
|
||||||
### Performance
|
### Performance
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "ucobs"
|
name = "ucobs"
|
||||||
version = "0.3.1"
|
version = "0.3.2"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
rust-version = "1.93"
|
rust-version = "1.93"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ over speed can build with `opt-level = "s"` which reduces μCOBS to
|
|||||||
| Dead-simple API (3 functions) | **yes** | yes | more surface area |
|
| Dead-simple API (3 functions) | **yes** | yes | more surface area |
|
||||||
| In-place / streaming encode | no | no | **yes** |
|
| In-place / streaming encode | no | no | **yes** |
|
||||||
| `no_std` + zero-alloc by default | **yes** | opt-in | **yes** |
|
| `no_std` + zero-alloc by default | **yes** | opt-in | **yes** |
|
||||||
| Thorough test suite | **106 tests, fuzz, proptest** | basic | basic |
|
| Thorough test suite | **111 tests, fuzz, proptest, mutants** | basic | basic |
|
||||||
|
|
||||||
**Choose μCOBS** if you want the smallest, most auditable COBS implementation
|
**Choose μCOBS** if you want the smallest, most auditable COBS implementation
|
||||||
with a `const fn` encoder, a dead-simple 3-function API, and leading or tied
|
with a `const fn` encoder, a dead-simple 3-function API, and leading or tied
|
||||||
@@ -282,7 +282,7 @@ affected — it uses a byte loop instead of `copy_from_slice`/memcpy.
|
|||||||
## Testing
|
## Testing
|
||||||
|
|
||||||
μCOBS has one of the most thorough test suites of any COBS implementation —
|
μCOBS has one of the most thorough test suites of any COBS implementation —
|
||||||
106 tests across 8 categories:
|
111 tests across 8 categories:
|
||||||
|
|
||||||
- **Canonical vectors** — every example from Cheshire & Baker 1999 (the original
|
- **Canonical vectors** — every example from Cheshire & Baker 1999 (the original
|
||||||
COBS paper), plus the full Wikipedia vector set
|
COBS paper), plus the full Wikipedia vector set
|
||||||
|
|||||||
+66
@@ -834,6 +834,49 @@ mod tests {
|
|||||||
assert_eq!(enc(&input), Some(expected));
|
assert_eq!(enc(&input), Some(expected));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn encode_254_block_boundary_exact() {
|
||||||
|
// 254 nonzero bytes MUST produce code 0xFF (full block), NOT 0xFF+1=0x00.
|
||||||
|
// This catches max_run being 255 instead of 254.
|
||||||
|
let input: Vec<u8> = (1..=254).map(|i| i as u8).collect();
|
||||||
|
let encoded = enc(&input).unwrap();
|
||||||
|
assert_eq!(encoded[0], 0xFF); // code byte for full 254-byte block
|
||||||
|
assert_eq!(encoded.len(), 255); // 1 code byte + 254 data bytes
|
||||||
|
// Verify it round-trips
|
||||||
|
let mut dec = vec![0u8; 256];
|
||||||
|
let n = decode(&encoded, &mut dec).unwrap();
|
||||||
|
assert_eq!(&dec[..n], &input[..]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn encode_255_nonzero_needs_two_blocks() {
|
||||||
|
// 255 nonzero bytes must split into a 254-byte full block + 1-byte block.
|
||||||
|
// If max_run were 255 (bug), this would be a single block — wrong.
|
||||||
|
let input: Vec<u8> = (1..=255).map(|i| (i % 255 + 1) as u8).collect();
|
||||||
|
let encoded = enc(&input).unwrap();
|
||||||
|
assert_eq!(encoded[0], 0xFF); // first block: full 254
|
||||||
|
assert_eq!(encoded[255], 0x02); // second block: 1 byte (code = 2)
|
||||||
|
assert_eq!(encoded.len(), 257); // 1 + 254 + 1 + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn encode_block_boundary_matches_corncobs() {
|
||||||
|
// Verify byte-for-byte match with corncobs at the critical 254-byte
|
||||||
|
// block boundary. Catches max_run being off by one.
|
||||||
|
for len in [254, 255, 256, 508, 512] {
|
||||||
|
let input: Vec<u8> = (0..len).map(|i| (i % 254 + 1) as u8).collect();
|
||||||
|
let ours = enc(&input).unwrap();
|
||||||
|
let mut their_buf = vec![0u8; corncobs::max_encoded_len(input.len())];
|
||||||
|
let their_len = corncobs::encode_buf(&input, &mut their_buf);
|
||||||
|
// corncobs appends a sentinel; strip it for comparison
|
||||||
|
assert_eq!(
|
||||||
|
&ours[..],
|
||||||
|
&their_buf[..their_len - 1],
|
||||||
|
"mismatch at len={len}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── Round-trip tests ────────────────────────────────────────
|
// ── Round-trip tests ────────────────────────────────────────
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -1037,6 +1080,29 @@ mod tests {
|
|||||||
assert_eq!(out, [0x11, 0x22]);
|
assert_eq!(out, [0x11, 0x22]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn decode_zeros_dest_exact_size() {
|
||||||
|
// Decode all-zeros into an exact-fit buffer.
|
||||||
|
// Encoded: N+1 bytes of 0x01 → N bytes of 0x00.
|
||||||
|
// This catches `> dest.len()` mutated to `>= dest.len()`.
|
||||||
|
for n in [1, 2, 10, 64, 254, 255, 256] {
|
||||||
|
let encoded: Vec<u8> = vec![0x01; n + 1];
|
||||||
|
let mut out = vec![0u8; n]; // exact fit
|
||||||
|
assert_eq!(decode(&encoded, &mut out), Some(n), "failed at n={n}");
|
||||||
|
assert!(out.iter().all(|&b| b == 0x00), "wrong data at n={n}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn decode_zeros_dest_one_too_small() {
|
||||||
|
// Buffer one byte too small for all-zeros decode.
|
||||||
|
for n in [2, 10, 64, 256] {
|
||||||
|
let encoded: Vec<u8> = vec![0x01; n + 1];
|
||||||
|
let mut out = vec![0u8; n - 1]; // one too small
|
||||||
|
assert_eq!(decode(&encoded, &mut out), None, "should fail at n={n}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── Multi-block boundary tests ──────────────────────────────
|
// ── Multi-block boundary tests ──────────────────────────────
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
Reference in new issue
Block a user