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
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
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
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
//! Property-based tests for error handling
//!
//! **Feature: serializer-production-hardening, Property 2: Error Messages Contain Location Context**
//! **Validates: Requirements 7.1, 7.2, 7.3**
//!
//! For any invalid input that causes a parse error, the resulting DxError SHALL contain:
//! - Line number (≥ 1)
//! - Column number (≥ 1)
//! - Byte offset
//! - Non-empty snippet of problematic input
#[cfg(test)]
mod property_tests {
use crate::error::{
DX_MAGIC, DX_VERSION, DxError, MAX_SNIPPET_LENGTH, SourceLocation, extract_snippet,
};
use proptest::prelude::*;
/// Generate random input with newlines for location testing
fn arb_multiline_input() -> impl Strategy<Value = Vec<u8>> {
proptest::collection::vec(
prop_oneof![
Just(b'\n'),
(0x20u8..0x7Eu8), // Printable ASCII
],
10..100,
)
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(100))]
/// Feature: serializer-production-hardening, Property 2: Error Messages Contain Location Context
///
/// For any invalid input that causes a parse error, the returned DxError SHALL contain:
/// (a) line number >= 1, (b) column number >= 1, (c) byte offset >= 0, and
/// (d) a non-empty snippet of the problematic input (up to 50 characters).
///
/// **Validates: Requirements 7.1, 7.2, 7.3**
#[test]
fn prop_error_diagnostics_quality(
input in arb_multiline_input(),
offset in 0usize..100
) {
// Clamp offset to valid range
let offset = offset.min(input.len().saturating_sub(1));
// Create a parse error with the input
let err = DxError::parse_error(&input, offset, "test error message");
// (a) Line number should be >= 1
let line = err.line();
prop_assert!(
line.is_some() && line.unwrap() >= 1,
"Parse error should have line number >= 1, got {:?}", line
);
// (b) Column number should be >= 1
let column = err.column();
prop_assert!(
column.is_some() && column.unwrap() >= 1,
"Parse error should have column number >= 1, got {:?}", column
);
// (c) Byte offset should be >= 0 (always true for usize, but verify it's present)
let byte_offset = err.offset();
prop_assert!(
byte_offset.is_some(),
"Parse error should have byte offset"
);
prop_assert_eq!(
byte_offset.unwrap(), offset,
"Byte offset should match the input offset"
);
// (d) Snippet should be non-empty (if input is non-empty) and <= 50 chars
let snippet = err.snippet();
prop_assert!(
snippet.is_some(),
"Parse error should have a snippet"
);
let snippet_str = snippet.unwrap();
if !input.is_empty() {
prop_assert!(
!snippet_str.is_empty(),
"Snippet should be non-empty for non-empty input"
);
}
prop_assert!(
snippet_str.len() <= MAX_SNIPPET_LENGTH,
"Snippet should be at most {} characters, got {}",
MAX_SNIPPET_LENGTH, snippet_str.len()
);
}
/// Property 2b: Extract snippet should produce valid output
///
/// **Validates: Requirements 7.3**
#[test]
fn prop_extract_snippet_valid(
input in arb_multiline_input(),
offset in 0usize..200
) {
let snippet = extract_snippet(&input, offset);
// Snippet should never exceed MAX_SNIPPET_LENGTH
prop_assert!(
snippet.len() <= MAX_SNIPPET_LENGTH,
"Snippet length {} exceeds max {}", snippet.len(), MAX_SNIPPET_LENGTH
);
// Snippet should be valid UTF-8 (extract_snippet uses from_utf8_lossy)
// This is implicitly true since we return a String
// Snippet should not contain control characters (except space/tab)
for c in snippet.chars() {
prop_assert!(
!c.is_control() || c == ' ' || c == '\t',
"Snippet should not contain control characters, found {:?}", c
);
}
}
/// Property 2c: Type mismatch errors should include both expected and actual types
///
/// **Validates: Requirements 7.2**
#[test]
fn prop_type_mismatch_has_both_types(
expected in "[a-z]{3,10}",
actual in "[a-z]{3,10}"
) {
let err = DxError::type_mismatch(&expected, &actual);
let msg = err.to_string();
prop_assert!(
msg.contains(&expected),
"Type mismatch error should contain expected type '{}': {}", expected, msg
);
prop_assert!(
msg.contains(&actual),
"Type mismatch error should contain actual type '{}': {}", actual, msg
);
}
/// Property 12a: Error location line numbers SHALL be 1-indexed
///
/// **Property 12: Error Messages with Location**
/// **Validates: Requirements 6.1**
#[test]
fn prop_error_location_line_is_1_indexed(input in arb_multiline_input()) {
let loc = SourceLocation::from_offset(&input, 0);
prop_assert!(loc.line >= 1, "Line number should be >= 1, got {}", loc.line);
}
/// Property 12b: Error location column numbers SHALL be 1-indexed
///
/// **Property 12: Error Messages with Location**
/// **Validates: Requirements 6.1**
#[test]
fn prop_error_location_column_is_1_indexed(input in arb_multiline_input()) {
let loc = SourceLocation::from_offset(&input, 0);
prop_assert!(loc.column >= 1, "Column number should be >= 1, got {}", loc.column);
}
/// Property 12c: Line number SHALL increment after each newline
///
/// **Property 12: Error Messages with Location**
/// **Validates: Requirements 6.1**
#[test]
fn prop_line_increments_after_newline(
prefix in "[a-z]{1,10}",
suffix in "[a-z]{1,10}"
) {
let input = format!("{}\n{}", prefix, suffix);
let bytes = input.as_bytes();
// Location before newline
let loc_before = SourceLocation::from_offset(bytes, prefix.len());
// Location after newline (first char of second line)
let loc_after = SourceLocation::from_offset(bytes, prefix.len() + 1);
prop_assert_eq!(loc_before.line, 1, "Before newline should be line 1");
prop_assert_eq!(loc_after.line, 2, "After newline should be line 2");
prop_assert_eq!(loc_after.column, 1, "First char of new line should be column 1");
}
/// Property 12d: Column SHALL reset to 1 after newline
///
/// **Property 12: Error Messages with Location**
/// **Validates: Requirements 6.1**
#[test]
fn prop_column_resets_after_newline(
line1 in "[a-z]{5,15}",
line2 in "[a-z]{3,10}"
) {
let input = format!("{}\n{}", line1, line2);
let bytes = input.as_bytes();
// Check column at start of second line
let offset = line1.len() + 1; // After newline
let loc = SourceLocation::from_offset(bytes, offset);
prop_assert_eq!(loc.column, 1, "Column should reset to 1 after newline");
}
/// Property 12e: Offset SHALL be preserved in SourceLocation
///
/// **Property 12: Error Messages with Location**
/// **Validates: Requirements 6.1**
#[test]
fn prop_offset_preserved(input in arb_multiline_input()) {
if input.is_empty() {
return Ok(());
}
let offset = input.len() / 2;
let loc = SourceLocation::from_offset(&input, offset);
prop_assert_eq!(loc.offset, offset, "Offset should be preserved");
}
/// Property 13: Invalid magic bytes SHALL produce InvalidMagic error
///
/// **Property 13: Binary Header Validation**
/// **Validates: Requirements 6.2**
#[test]
fn prop_invalid_magic_error_contains_bytes(byte0 in 0u8..255, byte1 in 0u8..255) {
// Skip valid magic bytes
if byte0 == DX_MAGIC[0] && byte1 == DX_MAGIC[1] {
return Ok(());
}
let err = DxError::invalid_magic(byte0, byte1);
let msg = err.to_string();
prop_assert!(
msg.contains(&format!("{:#04X}", byte0)) || msg.contains(&format!("{:#02X}", byte0)),
"Error message should contain first byte: {}", msg
);
}
/// Property 14: UTF-8 errors SHALL include byte offset
///
/// **Property 14: Invalid UTF-8 Handling**
/// **Validates: Requirements 6.6**
#[test]
fn prop_utf8_error_has_offset(offset in 0usize..10000) {
let err = DxError::utf8_error(offset);
prop_assert_eq!(err.offset(), Some(offset), "UTF-8 error should have offset");
}
/// Property 15: Buffer too small errors SHALL include required and available sizes
///
/// **Property 15: Buffer Size Error**
/// **Validates: Requirements 6.7**
#[test]
fn prop_buffer_error_has_sizes(required in 1usize..10000, available in 0usize..10000) {
let err = DxError::buffer_too_small(required, available);
let msg = err.to_string();
prop_assert!(
msg.contains(&required.to_string()),
"Error should contain required size {}: {}", required, msg
);
prop_assert!(
msg.contains(&available.to_string()),
"Error should contain available size {}: {}", available, msg
);
}
/// Property: Unsupported version errors SHALL include found and expected versions
///
/// **Validates: Requirements 6.2**
#[test]
fn prop_version_error_has_versions(found in 0u8..255) {
if found == DX_VERSION {
return Ok(());
}
let err = DxError::unsupported_version(found);
let msg = err.to_string();
prop_assert!(
msg.contains(&found.to_string()),
"Error should contain found version {}: {}", found, msg
);
prop_assert!(
msg.contains(&DX_VERSION.to_string()),
"Error should contain expected version {}: {}", DX_VERSION, msg
);
}
/// Property: Base62 errors SHALL include character and position
///
/// **Validates: Requirements 6.8**
#[test]
fn prop_base62_error_has_details(
char in proptest::char::any(),
position in 0usize..1000
) {
let err = DxError::base62_error(char, position, "test error");
let msg = err.to_string();
prop_assert!(
msg.contains(&position.to_string()),
"Error should contain position {}: {}", position, msg
);
}
}
#[test]
fn test_source_location_multiline() {
let input = b"line1\nline2\nline3\nline4";
// Test various positions
let cases = vec![
(0, 1, 1), // Start of line 1
(3, 1, 4), // Middle of line 1
(5, 1, 6), // End of line 1 (before newline)
(6, 2, 1), // Start of line 2
(11, 2, 6), // End of line 2
(12, 3, 1), // Start of line 3
(18, 4, 1), // Start of line 4
];
for (offset, expected_line, expected_col) in cases {
let loc = SourceLocation::from_offset(input, offset);
assert_eq!(
loc.line, expected_line,
"Offset {} should be line {}, got {}",
offset, expected_line, loc.line
);
assert_eq!(
loc.column, expected_col,
"Offset {} should be column {}, got {}",
offset, expected_col, loc.column
);
}
}
#[test]
fn test_error_offset_extraction() {
// Errors with offsets
assert_eq!(DxError::UnexpectedEof(42).offset(), Some(42));
assert_eq!(DxError::utf8_error(100).offset(), Some(100));
assert_eq!(DxError::DittoNoPrevious(50).offset(), Some(50));
let parse_err = DxError::parse_error(b"test\ninput", 7, "test");
assert_eq!(parse_err.offset(), Some(7));
// Errors without offsets
assert_eq!(DxError::SchemaError("test".into()).offset(), None);
assert_eq!(DxError::IntegerOverflow.offset(), None);
assert_eq!(DxError::Io("test".into()).offset(), None);
}
#[test]
fn test_error_is_recoverable() {
// Recoverable errors
assert!(DxError::UnknownAlias("test".into()).is_recoverable());
assert!(DxError::UnknownAnchor("test".into()).is_recoverable());
assert!(
DxError::TypeMismatch {
expected: "int".into(),
actual: "string".into()
}
.is_recoverable()
);
// Non-recoverable errors
assert!(!DxError::UnexpectedEof(0).is_recoverable());
assert!(!DxError::InvalidMagic(0, 0).is_recoverable());
assert!(!DxError::IntegerOverflow.is_recoverable());
}
#[test]
fn test_error_from_io() {
let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
let dx_err: DxError = io_err.into();
match dx_err {
DxError::Io(msg) => assert!(msg.contains("not found")),
_ => panic!("Expected Io error"),
}
}
#[test]
fn test_error_from_utf8() {
// Create invalid UTF-8
let invalid = vec![0xFF, 0xFE];
let result = std::str::from_utf8(&invalid);
if let Err(utf8_err) = result {
let dx_err: DxError = utf8_err.into();
match dx_err {
DxError::Utf8Error { offset } => assert_eq!(offset, 0),
_ => panic!("Expected Utf8Error"),
}
}
}
}