perl_test_generators/
variable.rs1use proptest::prelude::*;
7
8static IDENT_START: &[u8] = b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_";
10
11static IDENT_CONT: &[u8] = b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_0123456789";
13
14fn identifier() -> impl Strategy<Value = String> {
16 prop_oneof![
17 Just("self".to_string()),
19 Just("class".to_string()),
20 (
22 prop::sample::select(IDENT_START),
23 prop::collection::vec(prop::sample::select(IDENT_CONT), 0..=11_usize),
24 )
25 .prop_map(|(first, rest)| {
26 let mut s = String::new();
27 s.push(first as char);
28 for b in rest {
29 s.push(b as char);
30 }
31 s
32 }),
33 ]
34}
35
36fn package_path() -> impl Strategy<Value = String> {
38 prop::collection::vec(identifier(), 1..=4_usize).prop_map(|segments| segments.join("::"))
39}
40
41pub fn variable() -> impl Strategy<Value = String> {
47 prop_oneof![
48 Just("$_".to_string()),
50 Just("@_".to_string()),
51 Just("%ENV".to_string()),
52 Just("@ARGV".to_string()),
53 Just("$0".to_string()),
54 (1u32..=9).prop_map(|n| format!("${}", n)),
55 (prop_oneof![Just('$'), Just('@'), Just('%')], identifier())
57 .prop_map(|(sigil, name)| format!("{}{}", sigil, name)),
58 (prop_oneof![Just('$'), Just('@'), Just('%')], package_path(), identifier())
60 .prop_map(|(sigil, pkg, name)| format!("{}{}::{}", sigil, pkg, name)),
61 ]
62}
63
64#[cfg(test)]
65mod tests {
66 use super::*;
67
68 proptest! {
69 #[test]
70 fn variable_starts_with_sigil(v in variable()) {
71 prop_assert!(v.starts_with('$') || v.starts_with('@') || v.starts_with('%'));
72 }
73
74 #[test]
75 fn variable_body_is_valid(v in variable()) {
76 let body = &v[1..];
77 prop_assert!(!body.is_empty(), "variable body must not be empty: {}", v);
78 }
79
80 #[test]
81 fn identifier_is_ascii(id in identifier()) {
82 prop_assert!(id.is_ascii(), "identifier must be ASCII: {}", id);
83 }
84
85 #[test]
86 fn package_path_has_no_empty_segments(pkg in package_path()) {
87 for segment in pkg.split("::") {
88 prop_assert!(!segment.is_empty(), "empty package segment in {pkg}");
89 }
90 }
91
92 #[test]
93 fn variable_is_ascii(v in variable()) {
94 prop_assert!(v.is_ascii(), "variable must be ASCII: {v}");
95 }
96
97 #[test]
98 fn variable_len_is_at_least_two(v in variable()) {
99 prop_assert!(v.len() >= 2, "variable too short: {v}");
101 }
102
103 #[test]
104 fn identifier_starts_with_letter_or_underscore(id in identifier()) {
105 let first = id.chars().next();
106 match first {
107 Some(ch) => prop_assert!(
108 ch.is_ascii_alphabetic() || ch == '_',
109 "identifier must start with letter or '_': {id}"
110 ),
111 None => prop_assert!(false, "identifier must not be empty"),
112 }
113 }
114
115 #[test]
116 fn identifier_is_non_empty(id in identifier()) {
117 prop_assert!(!id.is_empty(), "identifier must be non-empty");
118 }
119
120 #[test]
121 fn identifier_body_chars_are_word_chars(id in identifier()) {
122 let mut chars = id.chars();
123 let _ = chars.next();
125 for ch in chars {
126 prop_assert!(
127 ch.is_ascii_alphanumeric() || ch == '_',
128 "invalid identifier body char '{ch}' in {id}"
129 );
130 }
131 }
132
133 #[test]
134 fn package_path_segment_count_bounded(pkg in package_path()) {
135 let count = pkg.split("::").count();
136 prop_assert!(count >= 1, "package path must have at least one segment: {pkg}");
137 prop_assert!(count <= 4, "package path must have at most 4 segments: {pkg}");
138 }
139
140 #[test]
141 fn package_qualified_variable_contains_double_colon(v in variable()) {
142 if let Some(last_sep) = v.rfind("::") {
145 let sigil_and_pkg = &v[..last_sep];
146 let pkg = &sigil_and_pkg[1..];
148 prop_assert!(!pkg.is_empty(), "package prefix must be non-empty in {v}");
149 }
150 }
151
152 #[test]
153 fn numeric_special_variables_in_range(v in variable()) {
154 if let Some(body) = v.strip_prefix('$') {
157 if let Ok(n) = body.parse::<u32>() {
158 prop_assert!(n <= 9, "numeric capture variable out of range: {v}");
159 }
160 }
161 }
162
163 #[test]
164 fn special_at_underscore_sigil_is_at(v in variable()) {
165 if v == "@_" {
167 prop_assert!(v.starts_with('@'));
168 }
169 }
170
171 #[test]
172 fn package_path_is_ascii(pkg in package_path()) {
173 prop_assert!(pkg.is_ascii(), "package path must be ASCII: {pkg}");
174 }
175 }
176}