Skip to content

Commit 25322c8

Browse files
Googlercopybara-github
Googler
authored andcommitted
Adds ignoring_unicode_case() to StrMatcher.
This adds configuration to support Unicode case-insensitive matching with `StrMatcher`. Moreso than its ASCII counterpart, this allocates with `str::to_lowercase` when doing case-insensitive matching. PiperOrigin-RevId: 707921470
1 parent 48ffd20 commit 25322c8

File tree

1 file changed

+101
-3
lines changed

1 file changed

+101
-3
lines changed

googletest/src/matchers/str_matcher.rs

Lines changed: 101 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -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.
6162
pub 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 {
394419
enum CasePolicy {
395420
Respect,
396421
IgnoreAscii,
422+
IgnoreUnicode,
397423
}
398424

399425
impl 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\tA 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

Comments
 (0)