1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
// SPDX-License-Identifier: MIT OR Apache-2.0
// Copyright (c) 2026 Noyalib. All rights reserved.
//! Panic-free property tests.
//!
//! Formalises the contract from
//! [`POLICIES.md` §8 — Panic policy](../../../doc/POLICIES.md#8-panic-policy)
//! that the public `from_*` family **never panics on input**,
//! regardless of how malformed. Every entry point handles
//! malformed input by returning `Err(Error)`, never by
//! unwinding.
//!
//! These property tests use `proptest` to throw arbitrary byte
//! sequences (UTF-8 and non-UTF-8) at every public parse entry
//! and assert that whatever the parser does, it does not panic.
#![allow(missing_docs)]
use noyalib::{from_slice, from_str, Value};
use proptest::prelude::*;
// Default to 256 random cases per property; CI may override
// via `PROPTEST_CASES`.
proptest! {
#![proptest_config(ProptestConfig::with_cases(
std::env::var("PROPTEST_CASES")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(256)
))]
/// `from_str::<Value>` on any valid UTF-8 string never panics.
/// It returns either `Ok(Value)` or `Err(Error)` — both fine.
#[test]
fn from_str_value_never_panics_on_arbitrary_utf8(s in ".{0,1024}") {
std::panic::catch_unwind(|| {
let _: Result<Value, _> = from_str(&s);
}).expect("from_str must not panic on arbitrary UTF-8");
}
/// `from_slice::<Value>` on any byte sequence never panics
/// — even non-UTF-8 input. Non-UTF-8 should error cleanly.
#[test]
fn from_slice_value_never_panics_on_arbitrary_bytes(bytes in prop::collection::vec(any::<u8>(), 0..1024)) {
std::panic::catch_unwind(|| {
let _: Result<Value, _> = from_slice(&bytes);
}).expect("from_slice must not panic on arbitrary bytes");
}
/// `from_str::<i64>` on any UTF-8 string never panics.
#[test]
fn from_str_typed_never_panics(s in ".{0,256}") {
std::panic::catch_unwind(|| {
let _: Result<i64, _> = from_str(&s);
}).expect("typed from_str must not panic");
}
/// `from_str::<String>` on arbitrary input never panics.
#[test]
fn from_str_string_target_never_panics(s in ".{0,256}") {
std::panic::catch_unwind(|| {
let _: Result<String, _> = from_str(&s);
}).expect("String-target from_str must not panic");
}
/// `load_all` on arbitrary input never panics.
#[test]
fn load_all_never_panics(s in ".{0,512}") {
std::panic::catch_unwind(|| {
let _: Result<Vec<Value>, _> = noyalib::load_all_as(&s);
}).expect("load_all_as must not panic");
}
/// `cst::parse_document` never panics on arbitrary UTF-8.
#[test]
fn cst_parse_document_never_panics(s in ".{0,512}") {
std::panic::catch_unwind(|| {
let _ = noyalib::cst::parse_document(&s);
}).expect("cst::parse_document must not panic");
}
}
// ── Targeted regression-style panic-free tests ───────────────────
/// Inputs known to have caused panics in earlier YAML parsers.
const HISTORICAL_PANIC_INPUTS: &[&[u8]] = &[
// Empty
b"",
// BOM only
b"\xef\xbb\xbf",
// Just a directive
b"%YAML 1.2",
// Unclosed flow
b"[",
b"{",
b"[[[[",
b"{{{{",
// Empty alias
b"*",
// Empty anchor
b"&",
// Empty tag
b"!",
// Just a colon
b":",
// Just a dash
b"-",
// Mixed
b"---\n...\n---\n...\n",
// Quote bombs
b"'\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\",
b"\"\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\",
// Deep nesting
b"[[[[[[[[[[[[[[[[[[[[",
// Bad UTF-8
b"\xff\xff\xff",
b"\x80\x81\x82",
// CRLF mix
b"a:\r\n b:\r\n c",
// Tabs in indent (should error)
b"\ta:\n\t\tb: 1",
];
#[test]
fn historical_panic_inputs_do_not_panic() {
for (i, bytes) in HISTORICAL_PANIC_INPUTS.iter().enumerate() {
let res = std::panic::catch_unwind(|| {
let _: Result<Value, _> = from_slice(bytes);
});
assert!(
res.is_ok(),
"panic on historical input #{i}: {:?}",
String::from_utf8_lossy(bytes)
);
}
}
#[test]
fn historical_panic_inputs_via_str_when_utf8() {
for (i, bytes) in HISTORICAL_PANIC_INPUTS.iter().enumerate() {
if let Ok(s) = std::str::from_utf8(bytes) {
let res = std::panic::catch_unwind(|| {
let _: Result<Value, _> = from_str(s);
});
assert!(res.is_ok(), "panic on str-cast historical #{i}: {s:?}");
}
}
}