1
//! Regression test: parsing strings with many escapes must stay linear.
2
//!
3
//! `Parser::escaped_byte_buf` used to call `find('"')` once per escape. That call
4
//! scans to the closing quote, while the cursor only advances past one escape per
5
//! iteration, so a string with N escapes rescanned the tail N times — quadratic.
6
//!
7
//! The assertion is on *scaling*, not on absolute time, so it stays meaningful on
8
//! slow/noisy CI machines: doubling the input should roughly double the work
9
//! (ratio ~2). Before the fix the ratio is ~4. The threshold sits far from both.
10

            
11
use std::time::Instant;
12

            
13
12
fn string_of_escapes(n: usize) -> String {
14
12
    let mut s = String::from("[\"");
15
620000
    for _ in 0..n {
16
620000
        s.push_str("\\n");
17
620000
    }
18
12
    s.push_str("\"]");
19
12
    s
20
12
}
21

            
22
12
fn parse_millis(n: usize) -> f64 {
23
12
    let src = string_of_escapes(n);
24
12
    let start = Instant::now();
25
    // No `black_box` (it needs Rust 1.66, MSRV is 1.64): the unwrapped
26
    // cross-crate `from_str` allocates, so the parse isn't optimized away.
27
12
    let _value: ron::Value = ron::from_str(&src).unwrap();
28
12
    let elapsed = start.elapsed().as_secs_f64() * 1000.0;
29
12
    elapsed
30
12
}
31

            
32
#[test]
33
4
fn parsing_many_escapes_scales_linearly() {
34
    const N: usize = 50_000;
35

            
36
    // Warm up so allocator/caches don't skew the first sample.
37
4
    parse_millis(N / 10);
38

            
39
4
    let base = parse_millis(N);
40
4
    let double = parse_millis(N * 2);
41

            
42
    // Guard against a uselessly small denominator on a very fast machine.
43
4
    if base < 1.0 {
44
        return;
45
4
    }
46

            
47
4
    let ratio = double / base;
48
4
    assert!(
49
4
        ratio < 3.0,
50
        "parsing a string with {} escapes took {:.1}ms but {} escapes took {:.1}ms \
51
         (ratio {:.2}); expected ~2 (linear), ~4 means the quadratic rescan is back",
52
        N,
53
        base,
54
        N * 2,
55
        double,
56
        ratio
57
    );
58
4
}
59

            
60
/// The escaped path is shared with byte strings, and it is the only path that
61
/// allocates — keep a plain correctness check next to the scaling one.
62
#[test]
63
4
fn escapes_still_parse_correctly() {
64
4
    assert_eq!(ron::from_str::<String>(r#""a\nb""#).unwrap(), "a\nb");
65
4
    assert_eq!(ron::from_str::<String>(r#""a\"b""#).unwrap(), "a\"b");
66
4
    assert_eq!(ron::from_str::<String>(r#""a\\b""#).unwrap(), "a\\b");
67
4
    assert_eq!(ron::from_str::<String>(r#""\u{41}\u{42}""#).unwrap(), "AB");
68
4
    assert_eq!(ron::from_str::<String>(r#""\n\n\n""#).unwrap(), "\n\n\n");
69
    // escape immediately before the closing quote, and an empty tail after it
70
4
    assert_eq!(ron::from_str::<String>(r#""ab\n""#).unwrap(), "ab\n");
71
    // no escapes at all still takes the borrowed fast path
72
4
    assert_eq!(ron::from_str::<String>(r#""abc""#).unwrap(), "abc");
73
4
}