Replace false coverage exclusions with unreachable!() and tests
Two cov-excl markers hid invariant violations rather than genuinely
untestable code: the else branch of an all_maps loop and the Seq/Map
arm of to_kdl_value (only called on primitives). Both now panic via
unreachable!() and grcov excludes the unreachable! pattern automatically.
The Null branch of to_kdl_value was wrongly excluded — it fires when
serializing Vec<Option<T>> with None elements (Null is primitive, so
it reaches to_kdl_value through the all_primitive sequence path). Added
tests for both this path and the mixed-sequence Null path through
value_to_nodes.
Assisted-by: Claude Opus 4.6 via pi
diff --git a/justfile b/justfile
index cfcff33..de8afe8 100644
--- a/justfile
+++ b/justfile
@@ -23,7 +23,7 @@ coverage:
-t covdir \
--ignore-not-existing \
--keep-only 'src/**' \
- --excl-line 'cov-excl-line' \
+ --excl-line 'cov-excl-line|unreachable!' \
--excl-start 'cov-excl-start' \
--excl-stop 'cov-excl-stop')
echo "$REPORT" | jq -r '
diff --git a/src/ser.rs b/src/ser.rs
index 0e61b12..b69159e 100644
--- a/src/ser.rs
+++ b/src/ser.rs
@@ -63,12 +63,12 @@ impl Value {
fn to_kdl_value(&self) -> Option<KdlValue> {
match self {
- Value::Null => Some(KdlValue::Null), // cov-excl-line — Null is skipped by SerializeStruct/SerializeMap
+ Value::Null => Some(KdlValue::Null),
Value::Bool(b) => Some(KdlValue::Bool(*b)),
Value::Integer(i) => Some(KdlValue::Integer(*i)),
Value::Float(f) => Some(KdlValue::Float(*f)),
Value::String(s) => Some(KdlValue::String(s.clone())),
- _ => None, // cov-excl-line — Seq/Map elements never reach to_kdl_value
+ _ => unreachable!("to_kdl_value is only called on primitive values"),
}
}
}
@@ -96,15 +96,13 @@ fn value_to_doc(value: Value) -> Result<KdlDocument> {
/// Convert a key-value pair to one or more KDL nodes.
fn value_to_nodes(name: &str, value: Value) -> Result<Vec<KdlNode>> {
match value {
- // cov-excl-start — Null values are filtered out by SerializeStruct
- // and SerializeMap (representing Option::None), so they never reach
- // value_to_nodes as a standalone field.
+ // Null reaches value_to_nodes through the mixed-sequence `-`
+ // children path (e.g., Vec<Option<Vec<T>>> with a None element).
Value::Null => {
let mut node = KdlNode::new(name);
node.push(KdlEntry::new(KdlValue::Null));
Ok(vec![node])
}
- // cov-excl-stop
Value::Bool(b) => {
let mut node = KdlNode::new(name);
node.push(KdlEntry::new(KdlValue::Bool(b)));
@@ -158,7 +156,9 @@ fn value_to_nodes(name: &str, value: Value) -> Result<Vec<KdlNode>> {
let children_doc = map_entries_to_doc(entries)?;
node.set_children(children_doc);
nodes.push(node);
- } // cov-excl-line — else is unreachable: all_maps was true
+ } else {
+ unreachable!("all_maps was true but element is not Map");
+ }
}
Ok(nodes)
} else {
diff --git a/tests/integration.rs b/tests/integration.rs
index 4a94365..8effdc6 100644
--- a/tests/integration.rs
+++ b/tests/integration.rs
@@ -2642,6 +2642,38 @@ fn serialize_vec_bools() {
assert_eq!(val, roundtrip);
}
+#[test]
+fn serialize_vec_option_with_nulls() {
+ // Exercises to_kdl_value Null branch: None serializes as Value::Null,
+ // is_primitive() is true for Null, so it reaches to_kdl_value.
+ #[derive(Serialize, Debug, PartialEq)]
+ struct S {
+ vals: Vec<Option<i32>>,
+ }
+ let val = S {
+ vals: vec![Some(1), None, Some(3)],
+ };
+ let output = serde_kdl::to_string(&val).unwrap();
+ assert!(output.contains("vals"));
+ assert!(output.contains("#null"));
+}
+
+#[test]
+fn serialize_mixed_seq_with_null() {
+ // Exercises value_to_nodes Null branch: a mixed sequence containing
+ // None alongside nested Vecs hits the `-` children path, which calls
+ // value_to_nodes("-", Value::Null).
+ #[derive(Serialize, Debug)]
+ struct S {
+ items: Vec<Option<Vec<i32>>>,
+ }
+ let val = S {
+ items: vec![Some(vec![1, 2]), None, Some(vec![3])],
+ };
+ let output = serde_kdl::to_string(&val).unwrap();
+ assert!(output.contains("#null"));
+}
+
// ════════════════════════════════════════════════════════════════════════
// Remaining coverage: ValueDeserializer::deserialize_any Integer/Float
// ════════════════════════════════════════════════════════════════════════