22#[ cfg( test) ]
33mod tests {
44 use rustpython_sre_engine:: { Request , State , StrDrive } ;
5+ use rustpython_wtf8:: Wtf8Buf ;
56
67 struct Pattern {
78 #[ expect( unused, reason = "Needed for automated script" ) ]
@@ -44,7 +45,7 @@ mod tests {
4445 #[ rustfmt:: skip] let big_b = Pattern { pattern : "\\ B" , code : & [ 14 , 4 , 0 , 0 , 0 , 6 , 11 , 1 ] } ;
4546 // END GENERATED
4647 let ( req, mut state) = big_b. state ( "" ) ;
47- assert ! ( ! state. search( req) ) ;
48+ assert ! ( state. search( req) ) ;
4849 }
4950
5051 #[ test]
@@ -169,6 +170,49 @@ mod tests {
169170 assert ! ( !state. py_match( & req) ) ;
170171 }
171172
173+ #[ test]
174+ fn possessive_repeat_keeps_last_capture ( ) {
175+ use optional:: Optioned ;
176+
177+ let single_code = & [ 17 , 0 , 24 , 6 , 0 , 1 , 16 , 101 , 1 , 17 , 1 , 1 ] ;
178+ let req = Request :: new ( "eeea" , 3 , usize:: MAX , single_code, false ) ;
179+ let mut single_state = State :: default ( ) ;
180+ assert ! ( single_state. py_match( & req) ) ;
181+ assert_eq ! (
182+ single_state. marks. get( 0 ) ,
183+ ( Optioned :: some( 3 ) , Optioned :: some( 3 ) )
184+ ) ;
185+
186+ // (e?){2,4}+a: the fourth successful iteration is empty, so group 1
187+ // must retain its final empty span rather than the previous "e".
188+ #[ rustfmt:: skip] let optional = Pattern {
189+ pattern : "(e?){2,4}+a" ,
190+ code : & [ 14 , 4 , 0 , 1 , 5 , 28 , 14 , 2 , 4 , 17 , 0 , 24 , 6 , 0 , 1 , 16 , 101 , 1 , 17 , 1 , 1 , 16 , 97 , 1 ] ,
191+ } ;
192+ let ( req, mut state) = optional. state ( "eeea" ) ;
193+ assert ! ( state. py_match( & req) ) ;
194+ assert_eq ! (
195+ state. marks. get( 0 ) ,
196+ ( Optioned :: some( 3 ) , Optioned :: some( 3 ) )
197+ ) ;
198+
199+ // ((x)|y|z){3}+: group 1 is the final "z"; group 2 retains "x".
200+ #[ rustfmt:: skip] let alternation = Pattern {
201+ pattern : "((x)|y|z){3}+" ,
202+ code : & [ 14 , 4 , 0 , 3 , 3 , 28 , 28 , 3 , 3 , 17 , 0 , 7 , 9 , 17 , 2 , 16 , 120 , 17 , 3 , 15 , 12 , 5 , 16 , 121 , 15 , 7 , 5 , 16 , 122 , 15 , 2 , 0 , 17 , 1 , 1 , 1 ] ,
203+ } ;
204+ let ( req, mut state) = alternation. state ( "xyz" ) ;
205+ assert ! ( state. py_match( & req) ) ;
206+ assert_eq ! (
207+ state. marks. get( 0 ) ,
208+ ( Optioned :: some( 2 ) , Optioned :: some( 3 ) )
209+ ) ;
210+ assert_eq ! (
211+ state. marks. get( 1 ) ,
212+ ( Optioned :: some( 0 ) , Optioned :: some( 1 ) )
213+ ) ;
214+ }
215+
172216 #[ test]
173217 fn bug_20998 ( ) {
174218 // pattern p = re.compile('[a-c]+', re.I)
@@ -181,6 +225,23 @@ mod tests {
181225 assert_eq ! ( state. cursor. position, 3 ) ;
182226 }
183227
228+ #[ test]
229+ fn ascii_ignore_keeps_nonascii_range_literal ( ) {
230+ // pattern p = re.compile(r'[\u0430-\u045f]', re.I | re.A)
231+ //
232+ // ASCII-only case folding must not discard an exact non-ASCII range:
233+ // U+0450 lies in the compiled U+0430..U+045F interval.
234+ #[ rustfmt:: skip] let p = Pattern {
235+ pattern : "[\\ u0430-\\ u045f]" ,
236+ code : & [ 14 , 8 , 4 , 1 , 1 , 22 , 1072 , 1119 , 0 , 13 , 5 , 22 , 1072 , 1119 , 0 , 1 ] ,
237+ } ;
238+ let ( req, mut state) = p. state ( "\u{0450} " ) ;
239+ assert ! ( state. py_match( & req) ) ;
240+ let subject = Wtf8Buf :: from ( "\u{0450} " ) ;
241+ let ( req, mut state) = p. state ( subject. as_ref ( ) ) ;
242+ assert ! ( state. py_match( & req) ) ;
243+ }
244+
184245 #[ test]
185246 fn bigcharset ( ) {
186247 // pattern p = re.compile('[a-z]*', re.I)
0 commit comments