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
//! Property-Based Testing — 对应 sz-orm 项目成熟度评估报告 §3.7 验证体系
//!
//! 使用 proptest 对核心数据类型(Value)进行属性测试,
//! 自动生成大量随机输入验证不变量(invariant),弥补单元测试的边界盲区。
//!
//! # 测试的属性
//!
//! - **整数往返**:`Value::from(v).as_i64()` 保持原值
//! - **浮点往返**:`Value::from(v).as_f64()` 保持原值
//! - **布尔往返**:`Value::Bool(b).as_bool()` 保持原值
//! - **字符串往返**:`Value::String(s).as_str()` 保持原值
//! - **SQL 注入防御**:`to_param()` 结果中 `'` 成对出现(无法突破字面量)
//! - **U64 边界**:超过 `i64::MAX` 的 U64 转换为 i64 时返回 None(不静默截断)
//!
//! # 与单元测试的区别
//!
//! 单元测试是"已知输入 → 期望输出",属性测试是"任意满足前置条件的输入 → 不变量必成立"。
//! proptest 在属性不成立时会自动 shrinking,找到最小反例。
use proptest::prelude::*;
use sz_orm_core::Value;
proptest! {
/// 属性:I8 → as_i64 往返保持原值
#[test]
fn prop_i8_to_i64_roundtrip(v: i8) {
prop_assert_eq!(Value::I8(v).as_i64(), Some(v as i64));
}
/// 属性:I16 → as_i64 往返保持原值
#[test]
fn prop_i16_to_i64_roundtrip(v: i16) {
prop_assert_eq!(Value::I16(v).as_i64(), Some(v as i64));
}
/// 属性:I32 → as_i64 往返保持原值
#[test]
fn prop_i32_to_i64_roundtrip(v: i32) {
prop_assert_eq!(Value::I32(v).as_i64(), Some(v as i64));
}
/// 属性:I64 → as_i64 往返保持原值(完整 i64 范围)
#[test]
fn prop_i64_to_i64_roundtrip(v: i64) {
prop_assert_eq!(Value::I64(v).as_i64(), Some(v));
}
/// 属性:U8 → as_i64 往返保持原值
#[test]
fn prop_u8_to_i64_roundtrip(v: u8) {
prop_assert_eq!(Value::U8(v).as_i64(), Some(v as i64));
}
/// 属性:U16 → as_i64 往返保持原值
#[test]
fn prop_u16_to_i64_roundtrip(v: u16) {
prop_assert_eq!(Value::U16(v).as_i64(), Some(v as i64));
}
/// 属性:U32 → as_i64 往返保持原值
#[test]
fn prop_u32_to_i64_roundtrip(v: u32) {
prop_assert_eq!(Value::U32(v).as_i64(), Some(v as i64));
}
/// 属性:U64 → as_i64 边界正确
/// - v <= i64::MAX 时,as_i64() == Some(v as i64)
/// - v > i64::MAX 时,as_i64() == None(不静默截断为负数)
#[test]
fn prop_u64_to_i64_boundary(v: u64) {
let expected = i64::try_from(v).ok();
prop_assert_eq!(Value::U64(v).as_i64(), expected);
if v > i64::MAX as u64 {
prop_assert_eq!(Value::U64(v).as_i64(), None,
"U64 超过 i64::MAX 时必须返回 None,不静默截断");
}
}
/// 属性:F32 → as_f64 往返保持原值(f32 → f64 是无损扩展)
#[test]
fn prop_f32_to_f64_roundtrip(v: f32) {
// 排除 NaN/Inf,因为 to_param/to_string 不支持
prop_assume!(v.is_finite());
prop_assert_eq!(Value::F32(v).as_f64(), Some(v as f64));
}
/// 属性:F64 → as_f64 往返保持原值
#[test]
fn prop_f64_to_f64_roundtrip(v: f64) {
prop_assume!(v.is_finite());
prop_assert_eq!(Value::F64(v).as_f64(), Some(v));
}
/// 属性:Bool → as_bool 往返保持原值
#[test]
fn prop_bool_roundtrip(b: bool) {
prop_assert_eq!(Value::Bool(b).as_bool(), Some(b));
}
/// 属性:String → as_str 往返保持原值
#[test]
fn prop_string_as_str_roundtrip(s: String) {
let v = Value::String(s.clone());
prop_assert_eq!(v.as_str(), Some(s.as_str()));
}
/// 属性:String → to_param 的 SQL 注入防御
///
/// `to_param()` 返回 `'...'` 格式的 SQL 字符串字面量,
/// 其中所有 `'` 都被转义为 `''`。因此:
///
/// 1. 结果必须以 `'` 开头
/// 2. 结果必须以 `'` 结尾
/// 3. 去掉首尾 `'` 后,剩余部分中 `'` 的数量必须是偶数
/// (每个原始 `'` 变成 `''`,所以数量翻倍)
///
/// 这保证了攻击者无法通过包含 `'` 的输入突破字符串字面量。
#[test]
fn prop_to_param_sql_injection_defense(s: String) {
// to_param() 返回 Cow<str>,用 into_owned() 避免 temporary value 生命周期问题
let param = Value::String(s).to_param().into_owned();
// 属性 1:以 ' 开头
prop_assert!(
param.starts_with('\''),
"to_param 必须以 ' 开头,实际: {:?}",
param
);
// 属性 2:以 ' 结尾
prop_assert!(
param.ends_with('\''),
"to_param 必须以 ' 结尾,实际: {:?}",
param
);
// 属性 3:去掉首尾 ' 后,' 数量为偶数
let inner = ¶m[1..param.len() - 1];
let quote_count = inner.chars().filter(|&c| c == '\'').count();
prop_assert_eq!(
quote_count % 2,
0,
"to_param 内部 ' 数量必须为偶数(每个 ' 被转义为 ''),实际 {} 个",
quote_count
);
}
/// 属性:整数 → as_f64 往返保持原值(i64 → f64 在 2^53 内无损)
///
/// 限制 v 在 |v| < 2^53 范围内(f64 尾数 52 位 + 符号位),
/// 此范围内 i64 → f64 转换无损。超出此范围会有精度损失(非 bug,是 IEEE 754 限制)。
#[test]
fn prop_i64_to_f64_roundtrip_within_safe_range(
v in -(1i64 << 53)..(1i64 << 53)
) {
prop_assert_eq!(Value::I64(v).as_f64(), Some(v as f64));
}
/// 属性:Value::from 往返 — 任意 i64 通过 from 创建后 as_i64 还原
#[test]
fn prop_value_from_i64_roundtrip(v: i64) {
let value: Value = v.into();
prop_assert_eq!(value.as_i64(), Some(v));
}
/// 属性:Value::from(String) 往返
#[test]
fn prop_value_from_string_roundtrip(s: String) {
let value: Value = s.clone().into();
prop_assert_eq!(value.as_str(), Some(s.as_str()));
}
}