@@ -56,8 +56,9 @@ use std::ops::Deref;
5656/// > Note on memory use: In most cases, this matcher does not allocate memory
5757/// > when matching strings. However, it must allocate copies of both the actual
5858/// > and expected values when matching strings while
59- /// > [`ignoring_ascii_case`][StrMatcherConfigurator::ignoring_ascii_case] is
60- /// > set.
59+ /// > [`ignoring_ascii_case`][StrMatcherConfigurator::ignoring_ascii_case] or
60+ /// > [`ignoring_unicode_case`][StrMatcherConfigurator::ignoring_unicode_case]
61+ /// > are set.
6162pub fn contains_substring < T > ( expected : T ) -> StrMatcher < T > {
6263 StrMatcher {
6364 configuration : Configuration { mode : MatchMode :: Contains , ..Default :: default ( ) } ,
@@ -235,6 +236,25 @@ pub trait StrMatcherConfigurator<ExpectedT> {
235236 /// case characters outside of the codepoints 0-127 covered by ASCII.
236237 fn ignoring_ascii_case ( self ) -> StrMatcher < ExpectedT > ;
237238
239+ /// Configures the matcher to ignore Unicode case when comparing values.
240+ ///
241+ /// This uses the same rules for case as [`str::to_lowercase`].
242+ ///
243+ /// ```
244+ /// # use googletest::prelude::*;
245+ /// # fn should_pass() -> Result<()> {
246+ /// verify_that!("ὈΔΥΣΣΕΎΣ", eq("ὀδυσσεύς").ignoring_unicode_case())?; // Passes
247+ /// # Ok(())
248+ /// # }
249+ /// # fn should_fail() -> Result<()> {
250+ /// verify_that!("secret", eq("비밀").ignoring_unicode_case())?; // Fails
251+ /// # Ok(())
252+ /// # }
253+ /// # should_pass().unwrap();
254+ /// # should_fail().unwrap_err();
255+ /// ```
256+ fn ignoring_unicode_case ( self ) -> StrMatcher < ExpectedT > ;
257+
238258 /// Configures the matcher to match only strings which otherwise satisfy the
239259 /// conditions a number times matched by the matcher `times`.
240260 ///
@@ -333,6 +353,11 @@ impl<ExpectedT, MatcherT: Into<StrMatcher<ExpectedT>>> StrMatcherConfigurator<Ex
333353 StrMatcher { configuration : existing. configuration . ignoring_ascii_case ( ) , ..existing }
334354 }
335355
356+ fn ignoring_unicode_case ( self ) -> StrMatcher < ExpectedT > {
357+ let existing = self . into ( ) ;
358+ StrMatcher { configuration : existing. configuration . ignoring_unicode_case ( ) , ..existing }
359+ }
360+
336361 fn times ( self , times : impl Matcher < usize > + ' static ) -> StrMatcher < ExpectedT > {
337362 let existing = self . into ( ) ;
338363 if !matches ! ( existing. configuration. mode, MatchMode :: Contains ) {
@@ -394,6 +419,7 @@ impl MatchMode {
394419enum CasePolicy {
395420 Respect ,
396421 IgnoreAscii ,
422+ IgnoreUnicode ,
397423}
398424
399425impl Configuration {
@@ -411,27 +437,38 @@ impl Configuration {
411437 MatchMode :: Equals => match self . case_policy {
412438 CasePolicy :: Respect => expected == actual,
413439 CasePolicy :: IgnoreAscii => expected. eq_ignore_ascii_case ( actual) ,
440+ CasePolicy :: IgnoreUnicode => expected. to_lowercase ( ) == actual. to_lowercase ( ) ,
414441 } ,
415442 MatchMode :: Contains => match self . case_policy {
416443 CasePolicy :: Respect => self . does_containment_match ( actual, expected) ,
417444 CasePolicy :: IgnoreAscii => self . does_containment_match (
418445 actual. to_ascii_lowercase ( ) . as_str ( ) ,
419446 expected. to_ascii_lowercase ( ) . as_str ( ) ,
420447 ) ,
448+ CasePolicy :: IgnoreUnicode => self . does_containment_match (
449+ actual. to_lowercase ( ) . as_str ( ) ,
450+ expected. to_lowercase ( ) . as_str ( ) ,
451+ ) ,
421452 } ,
422453 MatchMode :: StartsWith => match self . case_policy {
423454 CasePolicy :: Respect => actual. starts_with ( expected) ,
424455 CasePolicy :: IgnoreAscii => {
425456 actual. len ( ) >= expected. len ( )
426457 && actual[ ..expected. len ( ) ] . eq_ignore_ascii_case ( expected)
427458 }
459+ CasePolicy :: IgnoreUnicode => {
460+ actual. to_lowercase ( ) . starts_with ( & expected. to_lowercase ( ) )
461+ }
428462 } ,
429463 MatchMode :: EndsWith => match self . case_policy {
430464 CasePolicy :: Respect => actual. ends_with ( expected) ,
431465 CasePolicy :: IgnoreAscii => {
432466 actual. len ( ) >= expected. len ( )
433467 && actual[ actual. len ( ) - expected. len ( ) ..] . eq_ignore_ascii_case ( expected)
434468 }
469+ CasePolicy :: IgnoreUnicode => {
470+ actual. to_lowercase ( ) . ends_with ( & expected. to_lowercase ( ) )
471+ }
435472 } ,
436473 }
437474 }
@@ -461,6 +498,7 @@ impl Configuration {
461498 match self . case_policy {
462499 CasePolicy :: Respect => { }
463500 CasePolicy :: IgnoreAscii => addenda. push ( "ignoring ASCII case" . into ( ) ) ,
501+ CasePolicy :: IgnoreUnicode => addenda. push ( "ignoring Unicode case" . into ( ) ) ,
464502 }
465503 if let Some ( times) = self . times . as_ref ( ) {
466504 addenda. push ( format ! ( "count {}" , times. describe( matcher_result) ) . into ( ) ) ;
@@ -516,6 +554,10 @@ impl Configuration {
516554 // TODO - b/283448414 : Support StrMatcher with ignore ascii case policy.
517555 return default_explanation;
518556 }
557+ if matches ! ( self . case_policy, CasePolicy :: IgnoreUnicode ) {
558+ // TODO - b/283448414 : Support StrMatcher with ignore unicode case policy.
559+ return default_explanation;
560+ }
519561 if self . do_strings_match ( expected, actual) {
520562 // TODO - b/283448414 : Consider supporting debug difference if the
521563 // strings match. This can be useful when a small contains is found
@@ -556,6 +598,10 @@ impl Configuration {
556598 Self { case_policy : CasePolicy :: IgnoreAscii , ..self }
557599 }
558600
601+ fn ignoring_unicode_case ( self ) -> Self {
602+ Self { case_policy : CasePolicy :: IgnoreUnicode , ..self }
603+ }
604+
559605 fn times ( self , times : impl Matcher < usize > + ' static ) -> Self {
560606 Self { times : Some ( Box :: new ( times) ) , ..self }
561607 }
@@ -677,6 +723,12 @@ mod tests {
677723 verify_that ! ( "A STRING" , matcher. ignoring_ascii_case( ) )
678724 }
679725
726+ #[ test]
727+ fn ignores_unicode_case_when_requested ( ) -> Result < ( ) > {
728+ let matcher = StrMatcher :: with_default_config ( "ὈΔΥΣΣΕΎΣ" ) ;
729+ verify_that ! ( "ὀδυσσεύς" , matcher. ignoring_unicode_case( ) )
730+ }
731+
680732 #[ test]
681733 fn allows_ignoring_leading_whitespace_from_eq ( ) -> Result < ( ) > {
682734 verify_that ! ( "A string" , eq( " \n \t A string" ) . ignoring_leading_whitespace( ) )
@@ -697,6 +749,16 @@ mod tests {
697749 verify_that ! ( "A string" , eq( "A STRING" ) . ignoring_ascii_case( ) )
698750 }
699751
752+ #[ test]
753+ fn allows_ignoring_unicode_case_from_eq ( ) -> Result < ( ) > {
754+ verify_that ! ( "ὈΔΥΣΣΕΎΣ" , eq( "ὀδυσσεύς" ) . ignoring_unicode_case( ) )
755+ }
756+
757+ #[ test]
758+ fn unicode_case_sensitive_from_eq ( ) -> Result < ( ) > {
759+ verify_that ! ( "ὈΔΥΣΣΕΎΣ" , not( eq( "ὀδυσσεύς" ) ) )
760+ }
761+
700762 #[ test]
701763 fn matches_string_containing_expected_value_in_contains_mode ( ) -> Result < ( ) > {
702764 verify_that ! ( "Some string" , contains_substring( "str" ) )
@@ -708,6 +770,12 @@ mod tests {
708770 verify_that ! ( "Some string" , contains_substring( "STR" ) . ignoring_ascii_case( ) )
709771 }
710772
773+ #[ test]
774+ fn matches_string_containing_expected_value_in_contains_mode_while_ignoring_unicode_case (
775+ ) -> Result < ( ) > {
776+ verify_that ! ( "Some σpsilon" , contains_substring( "Σps" ) . ignoring_unicode_case( ) )
777+ }
778+
711779 #[ test]
712780 fn contains_substring_matches_correct_number_of_substrings ( ) -> Result < ( ) > {
713781 verify_that ! ( "Some string" , contains_substring( "str" ) . times( eq( 1 ) ) )
@@ -739,10 +807,25 @@ mod tests {
739807 }
740808
741809 #[ test]
742- fn ends_with_does_not_match_short_string_ignoring_ascii_case ( ) -> Result < ( ) > {
810+ fn starts_with_does_not_match_short_string_ignoring_ascii_case ( ) -> Result < ( ) > {
743811 verify_that ! ( "Some" , not( starts_with( "OTHER" ) . ignoring_ascii_case( ) ) )
744812 }
745813
814+ #[ test]
815+ fn starts_with_matches_string_reference_with_prefix_ignoring_unicode_case ( ) -> Result < ( ) > {
816+ verify_that ! ( "비밀 santa" , starts_with( "비밀" ) . ignoring_unicode_case( ) )
817+ }
818+
819+ #[ test]
820+ fn starts_with_does_not_match_wrong_prefix_ignoring_unicode_case ( ) -> Result < ( ) > {
821+ verify_that ! ( "secret santa" , not( starts_with( "비밀" ) . ignoring_unicode_case( ) ) )
822+ }
823+
824+ #[ test]
825+ fn starts_with_does_not_match_short_string_ignoring_unicode_case ( ) -> Result < ( ) > {
826+ verify_that ! ( "비밀" , not( starts_with( "秘密" ) . ignoring_unicode_case( ) ) )
827+ }
828+
746829 #[ test]
747830 fn starts_with_does_not_match_string_without_prefix ( ) -> Result < ( ) > {
748831 verify_that ! ( "Some value" , not( starts_with( "Another" ) ) )
@@ -783,6 +866,21 @@ mod tests {
783866 verify_that ! ( "Some value" , not( ends_with( "Some" ) ) )
784867 }
785868
869+ #[ test]
870+ fn ends_with_matches_string_reference_with_suffix_ignoring_unicode_case ( ) -> Result < ( ) > {
871+ verify_that ! ( "santa 비밀" , ends_with( "비밀" ) . ignoring_unicode_case( ) )
872+ }
873+
874+ #[ test]
875+ fn ends_with_does_not_match_wrong_suffix_ignoring_unicode_case ( ) -> Result < ( ) > {
876+ verify_that ! ( "secret santa" , not( ends_with( "비밀" ) . ignoring_unicode_case( ) ) )
877+ }
878+
879+ #[ test]
880+ fn ends_with_does_not_match_short_string_ignoring_unicode_case ( ) -> Result < ( ) > {
881+ verify_that ! ( "비밀" , not( ends_with( "秘密" ) . ignoring_unicode_case( ) ) )
882+ }
883+
786884 #[ test]
787885 fn describes_itself_for_matching_result ( ) -> Result < ( ) > {
788886 let matcher = StrMatcher :: with_default_config ( "A string" ) ;
0 commit comments