@@ -18,6 +18,7 @@ use eq_matcher::EqMatcher;
18
18
use googletest:: matcher:: { Matcher , MatcherResult } ;
19
19
#[ cfg( not( google3) ) ]
20
20
use googletest:: matchers:: eq_matcher;
21
+ use std:: borrow:: Cow ;
21
22
use std:: fmt:: Debug ;
22
23
use std:: ops:: Deref ;
23
24
@@ -88,7 +89,7 @@ pub fn ends_with<T>(expected: T) -> StrMatcher<T> {
88
89
/// Matchers which match against string values and, through configuration,
89
90
/// specialise to [StrMatcher] implement this trait. Currently that only
90
91
/// includes [EqMatcher] and [StrMatcher].
91
- pub trait StrMatcherConfigurator < T > {
92
+ pub trait StrMatcherConfigurator < ExpectedT > {
92
93
/// Configures the matcher to ignore any leading whitespace in either the
93
94
/// actual or the expected value.
94
95
///
@@ -104,7 +105,7 @@ pub trait StrMatcherConfigurator<T> {
104
105
/// actual value.
105
106
///
106
107
/// [`str::trim_start`]: https://doc.rust-lang.org/std/primitive.str.html#method.trim_start
107
- fn ignoring_leading_whitespace ( self ) -> StrMatcher < T > ;
108
+ fn ignoring_leading_whitespace ( self ) -> StrMatcher < ExpectedT > ;
108
109
109
110
/// Configures the matcher to ignore any trailing whitespace in either the
110
111
/// actual or the expected value.
@@ -121,7 +122,7 @@ pub trait StrMatcherConfigurator<T> {
121
122
/// actual value.
122
123
///
123
124
/// [`str::trim_end`]: https://doc.rust-lang.org/std/primitive.str.html#method.trim_end
124
- fn ignoring_trailing_whitespace ( self ) -> StrMatcher < T > ;
125
+ fn ignoring_trailing_whitespace ( self ) -> StrMatcher < ExpectedT > ;
125
126
126
127
/// Configures the matcher to ignore both leading and trailing whitespace in
127
128
/// either the actual or the expected value.
@@ -142,7 +143,7 @@ pub trait StrMatcherConfigurator<T> {
142
143
/// value.
143
144
///
144
145
/// [`str::trim`]: https://doc.rust-lang.org/std/primitive.str.html#method.trim
145
- fn ignoring_outer_whitespace ( self ) -> StrMatcher < T > ;
146
+ fn ignoring_outer_whitespace ( self ) -> StrMatcher < ExpectedT > ;
146
147
147
148
/// Configures the matcher to ignore ASCII case when comparing values.
148
149
///
@@ -157,7 +158,28 @@ pub trait StrMatcherConfigurator<T> {
157
158
/// case characters outside of the codepoints 0-127 covered by ASCII.
158
159
///
159
160
/// [`str::eq_ignore_ascii_case`]: https://doc.rust-lang.org/std/primitive.str.html#method.eq_ignore_ascii_case
160
- fn ignoring_ascii_case ( self ) -> StrMatcher < T > ;
161
+ fn ignoring_ascii_case ( self ) -> StrMatcher < ExpectedT > ;
162
+
163
+ /// Configures the matcher to match only strings which otherwise satisfy the
164
+ /// conditions a number times matched by the matcher `times`.
165
+ ///
166
+ /// ```
167
+ /// verify_that!("Some value\nSome value", contains_substring("value").times(eq(2)))?; // Passes
168
+ /// verify_that!("Some value", contains_substring("value").times(eq(2)))?; // Fails
169
+ /// ```
170
+ ///
171
+ /// The matched substrings must be disjoint from one another to be counted.
172
+ /// For example:
173
+ ///
174
+ /// ```
175
+ /// // Fails: substrings distinct but not disjoint!
176
+ /// verify_that!("ababab", contains_substring("abab").times(eq(2)))?;
177
+ /// ```
178
+ ///
179
+ /// This is only meaningful when the matcher was constructed with
180
+ /// [`contains_substring`]. This method will panic when it is used with any
181
+ /// other matcher construction.
182
+ fn times ( self , times : impl Matcher < usize > + ' static ) -> StrMatcher < ExpectedT > ;
161
183
}
162
184
163
185
/// A matcher which matches equality or containment of a string-like value in a
@@ -169,8 +191,8 @@ pub trait StrMatcherConfigurator<T> {
169
191
/// * [`contains_substring`],
170
192
/// * [`starts_with`],
171
193
/// * [`ends_with`].
172
- pub struct StrMatcher < T > {
173
- expected : T ,
194
+ pub struct StrMatcher < ExpectedT > {
195
+ expected : ExpectedT ,
174
196
configuration : Configuration ,
175
197
}
176
198
@@ -192,32 +214,42 @@ where
192
214
}
193
215
}
194
216
195
- impl < T , MatcherT : Into < StrMatcher < T > > > StrMatcherConfigurator < T > for MatcherT {
196
- fn ignoring_leading_whitespace ( self ) -> StrMatcher < T > {
217
+ impl < ExpectedT , MatcherT : Into < StrMatcher < ExpectedT > > > StrMatcherConfigurator < ExpectedT >
218
+ for MatcherT
219
+ {
220
+ fn ignoring_leading_whitespace ( self ) -> StrMatcher < ExpectedT > {
197
221
let existing = self . into ( ) ;
198
222
StrMatcher {
199
223
configuration : existing. configuration . ignoring_leading_whitespace ( ) ,
200
224
..existing
201
225
}
202
226
}
203
227
204
- fn ignoring_trailing_whitespace ( self ) -> StrMatcher < T > {
228
+ fn ignoring_trailing_whitespace ( self ) -> StrMatcher < ExpectedT > {
205
229
let existing = self . into ( ) ;
206
230
StrMatcher {
207
231
configuration : existing. configuration . ignoring_trailing_whitespace ( ) ,
208
232
..existing
209
233
}
210
234
}
211
235
212
- fn ignoring_outer_whitespace ( self ) -> StrMatcher < T > {
236
+ fn ignoring_outer_whitespace ( self ) -> StrMatcher < ExpectedT > {
213
237
let existing = self . into ( ) ;
214
238
StrMatcher { configuration : existing. configuration . ignoring_outer_whitespace ( ) , ..existing }
215
239
}
216
240
217
- fn ignoring_ascii_case ( self ) -> StrMatcher < T > {
241
+ fn ignoring_ascii_case ( self ) -> StrMatcher < ExpectedT > {
218
242
let existing = self . into ( ) ;
219
243
StrMatcher { configuration : existing. configuration . ignoring_ascii_case ( ) , ..existing }
220
244
}
245
+
246
+ fn times ( self , times : impl Matcher < usize > + ' static ) -> StrMatcher < ExpectedT > {
247
+ let existing = self . into ( ) ;
248
+ if !matches ! ( existing. configuration. mode, MatchMode :: Contains ) {
249
+ panic ! ( "The times() configurator is only meaningful with contains_substring()." ) ;
250
+ }
251
+ StrMatcher { configuration : existing. configuration . times ( times) , ..existing }
252
+ }
221
253
}
222
254
223
255
impl < T : Deref < Target = str > > From < EqMatcher < T > > for StrMatcher < T > {
@@ -242,12 +274,13 @@ impl<T> StrMatcher<T> {
242
274
// parameterised, saving compilation time and binary size on monomorphisation.
243
275
//
244
276
// The default value represents exact equality of the strings.
245
- #[ derive( Default , Clone ) ]
277
+ #[ derive( Default ) ]
246
278
struct Configuration {
247
279
mode : MatchMode ,
248
280
ignore_leading_whitespace : bool ,
249
281
ignore_trailing_whitespace : bool ,
250
282
case_policy : CasePolicy ,
283
+ times : Option < Box < dyn Matcher < usize > > > ,
251
284
}
252
285
253
286
#[ derive( Default , Clone ) ]
@@ -283,10 +316,11 @@ impl Configuration {
283
316
CasePolicy :: IgnoreAscii => expected. eq_ignore_ascii_case ( actual) ,
284
317
} ,
285
318
MatchMode :: Contains => match self . case_policy {
286
- CasePolicy :: Respect => actual. contains ( expected) ,
287
- CasePolicy :: IgnoreAscii => {
288
- actual. to_ascii_lowercase ( ) . contains ( & expected. to_ascii_lowercase ( ) )
289
- }
319
+ CasePolicy :: Respect => self . does_containment_match ( actual, expected) ,
320
+ CasePolicy :: IgnoreAscii => self . does_containment_match (
321
+ actual. to_ascii_lowercase ( ) . as_str ( ) ,
322
+ expected. to_ascii_lowercase ( ) . as_str ( ) ,
323
+ ) ,
290
324
} ,
291
325
MatchMode :: StartsWith => match self . case_policy {
292
326
CasePolicy :: Respect => actual. starts_with ( expected) ,
@@ -304,18 +338,35 @@ impl Configuration {
304
338
} ,
305
339
}
306
340
}
341
+
342
+ // Returns whether actual contains expected a number of times matched by the
343
+ // matcher self.times. Does not take other configuration into account.
344
+ fn does_containment_match ( & self , actual : & str , expected : & str ) -> bool {
345
+ if let Some ( times) = self . times . as_ref ( ) {
346
+ // Split returns an iterator over the "boundaries" left and right of the
347
+ // substring to be matched, of which there is one more than the number of
348
+ // substrings.
349
+ matches ! ( times. matches( & ( actual. split( expected) . count( ) - 1 ) ) , MatcherResult :: Matches )
350
+ } else {
351
+ actual. contains ( expected)
352
+ }
353
+ }
354
+
307
355
// StrMatcher::describe redirects immediately to this function.
308
356
fn describe ( & self , matcher_result : MatcherResult , expected : & str ) -> String {
309
- let mut addenda = Vec :: with_capacity ( 3 ) ;
357
+ let mut addenda: Vec < Cow < ' static , str > > = Vec :: with_capacity ( 3 ) ;
310
358
match ( self . ignore_leading_whitespace , self . ignore_trailing_whitespace ) {
311
- ( true , true ) => addenda. push ( "ignoring leading and trailing whitespace" ) ,
312
- ( true , false ) => addenda. push ( "ignoring leading whitespace" ) ,
313
- ( false , true ) => addenda. push ( "ignoring trailing whitespace" ) ,
359
+ ( true , true ) => addenda. push ( "ignoring leading and trailing whitespace" . into ( ) ) ,
360
+ ( true , false ) => addenda. push ( "ignoring leading whitespace" . into ( ) ) ,
361
+ ( false , true ) => addenda. push ( "ignoring trailing whitespace" . into ( ) ) ,
314
362
( false , false ) => { }
315
363
}
316
364
match self . case_policy {
317
365
CasePolicy :: Respect => { }
318
- CasePolicy :: IgnoreAscii => addenda. push ( "ignoring ASCII case" ) ,
366
+ CasePolicy :: IgnoreAscii => addenda. push ( "ignoring ASCII case" . into ( ) ) ,
367
+ }
368
+ if let Some ( times) = self . times . as_ref ( ) {
369
+ addenda. push ( format ! ( "count {}" , times. describe( matcher_result) ) . into ( ) ) ;
319
370
}
320
371
let extra =
321
372
if !addenda. is_empty ( ) { format ! ( " ({})" , addenda. join( ", " ) ) } else { "" . into ( ) } ;
@@ -355,6 +406,10 @@ impl Configuration {
355
406
fn ignoring_ascii_case ( self ) -> Self {
356
407
Self { case_policy : CasePolicy :: IgnoreAscii , ..self }
357
408
}
409
+
410
+ fn times ( self , times : impl Matcher < usize > + ' static ) -> Self {
411
+ Self { times : Some ( Box :: new ( times) ) , ..self }
412
+ }
358
413
}
359
414
360
415
#[ cfg( test) ]
@@ -365,7 +420,7 @@ mod tests {
365
420
#[ cfg( not( google3) ) ]
366
421
use googletest:: matchers;
367
422
use googletest:: { google_test, verify_that, Result } ;
368
- use matchers:: { eq, not} ;
423
+ use matchers:: { eq, gt , not} ;
369
424
370
425
#[ google_test]
371
426
fn matches_string_reference_with_equal_string_reference ( ) -> Result < ( ) > {
@@ -496,6 +551,21 @@ mod tests {
496
551
verify_that ! ( "Some string" , contains_substring( "STR" ) . ignoring_ascii_case( ) )
497
552
}
498
553
554
+ #[ google_test]
555
+ fn contains_substring_matches_correct_number_of_substrings ( ) -> Result < ( ) > {
556
+ verify_that ! ( "Some string" , contains_substring( "str" ) . times( eq( 1 ) ) )
557
+ }
558
+
559
+ #[ google_test]
560
+ fn contains_substring_does_not_match_incorrect_number_of_substrings ( ) -> Result < ( ) > {
561
+ verify_that ! ( "Some string\n Some string" , not( contains_substring( "string" ) . times( eq( 1 ) ) ) )
562
+ }
563
+
564
+ #[ google_test]
565
+ fn contains_substring_does_not_match_when_substrings_overlap ( ) -> Result < ( ) > {
566
+ verify_that ! ( "ababab" , not( contains_substring( "abab" ) . times( eq( 2 ) ) ) )
567
+ }
568
+
499
569
#[ google_test]
500
570
fn starts_with_matches_string_reference_with_prefix ( ) -> Result < ( ) > {
501
571
verify_that ! ( "Some value" , starts_with( "Some" ) )
@@ -650,6 +720,15 @@ mod tests {
650
720
)
651
721
}
652
722
723
+ #[ google_test]
724
+ fn describes_itself_with_count_number ( ) -> Result < ( ) > {
725
+ let matcher = contains_substring ( "A string" ) . times ( gt ( 2 ) ) ;
726
+ verify_that ! (
727
+ Matcher :: <& str >:: describe( & matcher, MatcherResult :: Matches ) ,
728
+ eq( "contains a substring \" A string\" (count is greater than 2)" )
729
+ )
730
+ }
731
+
653
732
#[ google_test]
654
733
fn describes_itself_for_matching_result_in_starts_with_mode ( ) -> Result < ( ) > {
655
734
let matcher = starts_with ( "A string" ) ;
0 commit comments