Coverage Report

Created: 2025-09-05 15:26

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/home/noah/src/ruchy/src/testing/properties.rs
Line
Count
Source
1
//! Property-based tests for verifying compiler invariants
2
3
#![allow(clippy::unnecessary_wraps)] // Property tests often need Result for the test framework
4
5
use crate::backend::Transpiler;
6
use crate::frontend::ast::{BinaryOp, Expr, ExprKind, Literal, StringPart, UnaryOp};
7
use crate::frontend::{Parser, RecoveryParser};
8
#[allow(unused_imports)]
9
use crate::testing::generators::{arb_expr, arb_well_typed_expr};
10
use proptest::prelude::*;
11
use proptest::test_runner::TestCaseError;
12
13
/// Property: Parser should never panic on any input
14
///
15
/// # Errors
16
///
17
/// Returns an error if the property test fails
18
33
pub fn prop_parser_never_panics(input: &str) -> Result<(), TestCaseError> {
19
33
    let mut parser = Parser::new(input);
20
    // Parser should either succeed or return an error, never panic
21
33
    let _ = parser.parse();
22
33
    Ok(())
23
33
}
24
25
/// Property: Recovery parser should always produce some AST
26
///
27
/// # Errors
28
///
29
/// Returns an error if the property test fails
30
33
pub fn prop_recovery_parser_always_produces_ast(input: &str) -> Result<(), TestCaseError> {
31
33
    let mut parser = RecoveryParser::new(input);
32
33
    let result = parser.parse_with_recovery();
33
34
    // For non-empty input, we should get some AST or errors
35
33
    if !input.trim().is_empty() {
36
33
        prop_assert!(
37
33
            result.ast.is_some() || 
!result.errors.is_empty()0
,
38
0
            "Recovery parser should produce AST or errors for non-empty input"
39
        );
40
0
    }
41
33
    Ok(())
42
33
}
43
44
/// Property: Transpilation preserves expression structure
45
///
46
/// # Errors
47
///
48
/// Returns an error if the property test fails
49
33
pub fn prop_transpilation_preserves_structure(expr: &Expr) -> Result<(), TestCaseError> {
50
33
    let transpiler = Transpiler::new();
51
52
    // Transpilation should either succeed or fail cleanly
53
33
    if let Ok(rust_code) = transpiler.transpile(expr) {
54
        // The generated Rust code should not be empty
55
33
        let code_str = rust_code.to_string();
56
33
        prop_assert!(!code_str.is_empty(), 
"Transpiled code should not be empty"0
);
57
0
    } else {
58
0
        // Transpilation errors are acceptable for some ASTs
59
0
    }
60
33
    Ok(())
61
33
}
62
63
/// Property: String interpolation transpiles correctly
64
///
65
/// # Errors
66
///
67
/// Returns an error if the property test fails
68
0
pub fn prop_string_interpolation_transpiles(parts: &[StringPart]) -> Result<(), TestCaseError> {
69
0
    let transpiler = Transpiler::new();
70
0
    let result = transpiler.transpile_string_interpolation(parts);
71
72
    // Should either succeed or fail cleanly, never panic
73
0
    if let Ok(tokens) = result {
74
0
        let code = tokens.to_string();
75
        // Should either be a format! call or a simple string literal
76
0
        prop_assert!(
77
0
            code.contains("format!") || code.starts_with('"') || code == "()",
78
0
            "String interpolation should produce format! call or string literal"
79
        );
80
0
    }
81
    // Transpilation errors are acceptable for malformed parts
82
0
    Ok(())
83
0
}
84
85
/// Property: Parse-print roundtrip
86
///
87
/// # Errors
88
///
89
/// Returns an error if the property test fails
90
33
pub fn prop_parse_print_roundtrip(expr: &Expr) -> Result<(), TestCaseError> {
91
    // This would require a pretty-printer, which we'll implement later
92
    // For now, just check that we can transpile and the result is valid
93
33
    let transpiler = Transpiler::new();
94
33
    if let Ok(rust_code) = transpiler.transpile(expr) {
95
        // Check that the Rust code contains expected elements based on expr type
96
33
        let code_str = rust_code.to_string();
97
98
8
        match &expr.kind {
99
1
            ExprKind::Literal(Literal::Integer(n)) => {
100
                // Integer literals are transpiled with type suffixes (e.g., "42 i32")
101
1
                prop_assert!(
102
1
                    code_str.contains(&n.to_string()),
103
0
                    "Integer literal {n} not found in transpiled code"
104
                );
105
            }
106
1
            ExprKind::Literal(Literal::Bool(b)) => {
107
1
                prop_assert!(
108
1
                    code_str.contains(&b.to_string()),
109
0
                    "Bool literal {b} not found in transpiled code"
110
                );
111
            }
112
            ExprKind::Binary {
113
                op: BinaryOp::Add, ..
114
            } => {
115
11
                prop_assert!(
116
11
                    code_str.contains('+'),
117
0
                    "Addition operator not found in transpiled code"
118
                );
119
            }
120
20
            _ => {
121
20
                // Other cases are more complex to verify
122
20
            }
123
        }
124
0
    }
125
33
    Ok(())
126
33
}
127
128
/// Property: Well-typed expressions should always transpile successfully
129
///
130
/// # Errors
131
///
132
/// Returns an error if the property test fails
133
33
pub fn prop_well_typed_always_transpiles(expr: &Expr) -> Result<(), TestCaseError> {
134
33
    let transpiler = Transpiler::new();
135
136
    // Check if this is a simple, well-typed expression
137
33
    if is_well_typed(expr) {
138
33
        match transpiler.transpile(expr) {
139
33
            Ok(_) => Ok(()),
140
0
            Err(e) => {
141
0
                prop_assert!(
142
0
                    false,
143
0
                    "Well-typed expression failed to transpile: {:?}\nError: {}",
144
                    expr,
145
                    e
146
                );
147
0
                Ok(())
148
            }
149
        }
150
    } else {
151
        // Complex expressions may fail, which is acceptable
152
0
        Ok(())
153
    }
154
33
}
155
156
/// Property: Error recovery should handle truncated input gracefully
157
///
158
/// # Errors
159
///
160
/// Returns an error if the property test fails
161
33
pub fn prop_recovery_handles_truncation(input: &str) -> Result<(), TestCaseError> {
162
33
    if input.is_empty() {
163
0
        return Ok(());
164
33
    }
165
166
    // Try parsing truncated versions of the input
167
538
    for i in 0..
input33
.
len33
() {
168
538
        let truncated = &input[..i];
169
538
        let mut parser = RecoveryParser::new(truncated);
170
538
        let result = parser.parse_with_recovery();
171
172
        // Should not panic, and should produce something
173
538
        if !truncated.trim().is_empty() {
174
501
            prop_assert!(
175
501
                result.ast.is_some() || 
!result.errors.is_empty()0
,
176
0
                "Recovery parser should handle truncated input at position {i}"
177
            );
178
37
        }
179
    }
180
33
    Ok(())
181
33
}
182
183
/// Helper to check if an expression is well-typed (simplified)
184
33
fn is_well_typed(expr: &Expr) -> bool {
185
33
    match &expr.kind {
186
20
        ExprKind::Literal(_) | ExprKind::Identifier(_) => true,
187
13
        ExprKind::Binary { left, right, op } => {
188
13
            match op {
189
                BinaryOp::Add | BinaryOp::Subtract | BinaryOp::Multiply | BinaryOp::Divide => {
190
13
                    is_numeric(left) && is_numeric(right)
191
                }
192
0
                BinaryOp::And | BinaryOp::Or => is_boolean(left) && is_boolean(right),
193
                BinaryOp::Equal | BinaryOp::NotEqual => {
194
                    // Equality can work on many types
195
0
                    is_well_typed(left) && is_well_typed(right)
196
                }
197
0
                _ => is_well_typed(left) && is_well_typed(right),
198
            }
199
        }
200
0
        ExprKind::Unary { operand, op } => match op {
201
0
            UnaryOp::Not => is_boolean(operand),
202
0
            UnaryOp::Negate | UnaryOp::BitwiseNot => is_numeric(operand),
203
0
            UnaryOp::Reference => true, // Reference can be applied to any type
204
        },
205
        ExprKind::If {
206
0
            condition,
207
0
            then_branch,
208
0
            else_branch,
209
        } => {
210
0
            is_boolean(condition)
211
0
                && is_well_typed(then_branch)
212
0
                && else_branch.as_ref().is_none_or(|e| is_well_typed(e))
213
        }
214
0
        _ => false, // Conservative for complex expressions
215
    }
216
33
}
217
218
26
fn is_numeric(expr: &Expr) -> bool {
219
0
    matches!(
220
26
        &expr.kind,
221
        ExprKind::Literal(Literal::Integer(_) | Literal::Float(_))
222
    )
223
26
}
224
225
0
fn is_boolean(expr: &Expr) -> bool {
226
0
    matches!(&expr.kind, ExprKind::Literal(Literal::Bool(_)))
227
0
}
228
229
#[cfg(test)]
230
#[allow(clippy::unwrap_used, clippy::panic)]
231
mod tests {
232
    use super::*;
233
234
    proptest! {
235
        #[test]
236
        fn test_parser_never_panics(input in ".*") {
237
            prop_parser_never_panics(&input)?;
238
        }
239
240
        #[test]
241
        fn test_recovery_parser_always_produces_ast(input in ".*") {
242
            prop_recovery_parser_always_produces_ast(&input)?;
243
        }
244
245
        #[test]
246
        fn test_transpilation_preserves_structure(expr in arb_expr()) {
247
            prop_transpilation_preserves_structure(&expr)?;
248
        }
249
250
        #[test]
251
        fn test_well_typed_always_transpiles(expr in arb_well_typed_expr()) {
252
            prop_well_typed_always_transpiles(&expr)?;
253
        }
254
255
        #[test]
256
        fn test_recovery_handles_truncation(input in "[a-zA-Z0-9 +\\-*/()]+") {
257
            prop_recovery_handles_truncation(&input)?;
258
        }
259
260
        #[test]
261
        fn test_parse_print_roundtrip(expr in arb_well_typed_expr()) {
262
            prop_parse_print_roundtrip(&expr)?;
263
        }
264
    }
265
266
    #[test]
267
    #[ignore = "This test hangs due to infinite loop in recovery parser"]
268
0
    fn test_specific_recovery_cases() {
269
        // Test specific error recovery scenarios
270
0
        let cases = vec![
271
0
            ("let x =", "Missing value in let binding"),
272
0
            ("if x >", "Missing right operand"),
273
0
            ("fun foo(", "Missing closing paren"),
274
0
            ("[1, 2,", "Missing closing bracket"),
275
0
            ("1 + + 2", "Double operator"),
276
        ];
277
278
0
        for (input, _description) in cases {
279
0
            let mut parser = RecoveryParser::new(input);
280
0
            let result = parser.parse_with_recovery();
281
282
0
            assert!(
283
0
                result.ast.is_some() || !result.errors.is_empty(),
284
0
                "Failed to handle: {input}"
285
            );
286
        }
287
0
    }
288
}