blob: 7ee06e001ca4212c5195ec66a58f3e580248b4bf [file]
// Copyright 2026 The IREE Authors
//
// Licensed under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
#include "iree/tokenizer/normalizer/lowercase.h"
#include <string>
#include "iree/base/api.h"
#include "iree/testing/gtest.h"
#include "iree/testing/status_matchers.h"
#include "iree/tokenizer/normalizer/normalizer_test_util.h"
namespace iree::tokenizer {
namespace {
using testing::ProcessAndFinalize;
using testing::ScopedNormalizer;
using testing::ScopedNormalizerState;
using testing::TestLimitedOutputCapacity;
using testing::TestWithAllChunkSizes;
//===----------------------------------------------------------------------===//
// Test fixture for lowercase normalizer tests.
//===----------------------------------------------------------------------===//
class LowercaseNormalizerTest : public ::testing::Test {
protected:
void SetUp() override {
iree_tokenizer_normalizer_t* raw_normalizer = nullptr;
IREE_ASSERT_OK(iree_tokenizer_normalizer_lowercase_allocate(
iree_allocator_system(), &raw_normalizer));
normalizer_ = ScopedNormalizer(raw_normalizer);
}
iree_tokenizer_normalizer_t* normalizer() { return normalizer_.get(); }
private:
ScopedNormalizer normalizer_;
};
//===----------------------------------------------------------------------===//
// Lifecycle
//===----------------------------------------------------------------------===//
TEST_F(LowercaseNormalizerTest, CreateAndDestroy) {
EXPECT_NE(normalizer(), nullptr);
}
TEST_F(LowercaseNormalizerTest, StateSizeIsReasonable) {
iree_host_size_t state_size =
iree_tokenizer_normalizer_state_size(normalizer());
EXPECT_GT(state_size, 0u);
// State should be small (pending UTF-8 + pending output).
EXPECT_LE(state_size, 64u);
}
//===----------------------------------------------------------------------===//
// No-ops
//===----------------------------------------------------------------------===//
TEST_F(LowercaseNormalizerTest, EmptyInput) {
std::string result = ProcessAndFinalize(
normalizer(), "", /*expect_pending_after_process=*/false);
EXPECT_TRUE(result.empty());
}
TEST_F(LowercaseNormalizerTest, AlreadyLowercase) {
TestWithAllChunkSizes(normalizer(), "hello world", "hello world",
/*expect_pending_after_process=*/false);
}
//===----------------------------------------------------------------------===//
// ASCII Lowercasing (Fast Path)
//===----------------------------------------------------------------------===//
TEST_F(LowercaseNormalizerTest, AsciiUppercase) {
TestWithAllChunkSizes(normalizer(), "HELLO", "hello",
/*expect_pending_after_process=*/false);
}
TEST_F(LowercaseNormalizerTest, AsciiMixedCase) {
TestWithAllChunkSizes(normalizer(), "HeLLo WoRLD", "hello world",
/*expect_pending_after_process=*/false);
}
TEST_F(LowercaseNormalizerTest, AsciiWithNumbersAndSymbols) {
TestWithAllChunkSizes(normalizer(), "ABC123!@#XYZ", "abc123!@#xyz",
/*expect_pending_after_process=*/false);
}
TEST_F(LowercaseNormalizerTest, LongAsciiRun) {
// Test that ASCII fast path handles long runs efficiently.
std::string input(1000, 'A');
std::string expected(1000, 'a');
TestWithAllChunkSizes(normalizer(), input, expected,
/*expect_pending_after_process=*/false);
}
//===----------------------------------------------------------------------===//
// UTF-8 Lowercasing (Non-ASCII)
//===----------------------------------------------------------------------===//
TEST_F(LowercaseNormalizerTest, Utf8LatinAccented) {
// CAFÉ → café (É is U+00C9, é is U+00E9).
TestWithAllChunkSizes(normalizer(), "CAF\xC3\x89", "caf\xC3\xA9",
/*expect_pending_after_process=*/false);
}
TEST_F(LowercaseNormalizerTest, Utf8Greek) {
// ΩMEGA → ωmega (Ω is U+03A9, ω is U+03C9).
TestWithAllChunkSizes(normalizer(), "\xCE\xA9MEGA", "\xCF\x89mega",
/*expect_pending_after_process=*/false);
}
TEST_F(LowercaseNormalizerTest, Utf8Cyrillic) {
// ПРИВЕТ → привет.
TestWithAllChunkSizes(
normalizer(),
"\xD0\x9F\xD0\xA0\xD0\x98\xD0\x92\xD0\x95\xD0\xA2", // ПРИВЕТ
"\xD0\xBF\xD1\x80\xD0\xB8\xD0\xB2\xD0\xB5\xD1\x82", // привет
/*expect_pending_after_process=*/false);
}
TEST_F(LowercaseNormalizerTest, MixedAsciiAndUtf8) {
// "HELLO WÖRLD" → "hello wörld" (tests ASCII fast path → UTF-8 transition).
TestWithAllChunkSizes(normalizer(), "HELLO W\xC3\x96RLD",
"hello w\xC3\xB6rld",
/*expect_pending_after_process=*/false);
}
//===----------------------------------------------------------------------===//
// İ Expansion (U+0130 → U+0069 + U+0307)
//===----------------------------------------------------------------------===//
TEST_F(LowercaseNormalizerTest, TurkishDottedI) {
// İ (U+0130) → i + combining dot above (U+0069 + U+0307).
// İ in UTF-8 is 0xC4 0xB0.
// i is 0x69, combining dot above is 0xCC 0x87.
TestWithAllChunkSizes(normalizer(), "\xC4\xB0", "i\xCC\x87",
/*expect_pending_after_process=*/false);
}
TEST_F(LowercaseNormalizerTest, TurkishDottedIInWord) {
// İSTANBUL → i̇stanbul.
TestWithAllChunkSizes(normalizer(), "\xC4\xB0STANBUL", "i\xCC\x87stanbul",
/*expect_pending_after_process=*/false);
}
TEST_F(LowercaseNormalizerTest, MultipleTurkishDottedI) {
// İİİ → i̇i̇i̇ (3 expansions).
TestWithAllChunkSizes(normalizer(), "\xC4\xB0\xC4\xB0\xC4\xB0",
"i\xCC\x87i\xCC\x87i\xCC\x87",
/*expect_pending_after_process=*/false);
}
//===----------------------------------------------------------------------===//
// Characters That Don't Change
//===----------------------------------------------------------------------===//
TEST_F(LowercaseNormalizerTest, NumbersUnchanged) {
TestWithAllChunkSizes(normalizer(), "12345", "12345",
/*expect_pending_after_process=*/false);
}
TEST_F(LowercaseNormalizerTest, SymbolsUnchanged) {
TestWithAllChunkSizes(normalizer(), "!@#$%^&*()", "!@#$%^&*()",
/*expect_pending_after_process=*/false);
}
TEST_F(LowercaseNormalizerTest, EmojiUnchanged) {
// Emoji have no case. 🎉 is U+1F389 (4-byte UTF-8).
TestWithAllChunkSizes(normalizer(), "\xF0\x9F\x8E\x89", "\xF0\x9F\x8E\x89",
/*expect_pending_after_process=*/false);
}
TEST_F(LowercaseNormalizerTest, CjkUnchanged) {
// CJK has no case. 你好 (U+4F60 U+597D).
TestWithAllChunkSizes(normalizer(), "\xE4\xBD\xA0\xE5\xA5\xBD",
"\xE4\xBD\xA0\xE5\xA5\xBD",
/*expect_pending_after_process=*/false);
}
//===----------------------------------------------------------------------===//
// Streaming Behavior
//===----------------------------------------------------------------------===//
// Tests for partial UTF-8 handling were removed because the normalizer
// interface contract guarantees callers provide input on codepoint boundaries.
TEST_F(LowercaseNormalizerTest, LimitedOutputCapacity) {
TestLimitedOutputCapacity(normalizer(), "HELLO WORLD", "hello world");
}
TEST_F(LowercaseNormalizerTest, Capacity1WithExpansion) {
// Test that İ expansion works correctly with output capacity=1.
ScopedNormalizerState state(normalizer());
// İ expands to 3 bytes output (i + combining dot = 0x69 + 0xCC 0x87).
std::string result;
char output_char;
const char* input = "\xC4\xB0"; // İ
size_t position = 0;
while (position < 2 ||
iree_tokenizer_normalizer_state_has_pending(state.get())) {
iree_host_size_t consumed = 0;
iree_host_size_t written = 0;
iree_string_view_t remaining = {input + position, 2 - position};
IREE_ASSERT_OK(iree_tokenizer_normalizer_state_process(
state.get(), remaining, iree_make_mutable_string_view(&output_char, 1),
IREE_TOKENIZER_NORMALIZER_FLAG_NONE, &consumed, &written));
if (written > 0) {
result += output_char;
}
ASSERT_TRUE(consumed > 0 || written > 0) << "No progress";
position += consumed;
}
// Finalize.
while (true) {
iree_host_size_t finalize_written = 0;
IREE_ASSERT_OK(iree_tokenizer_normalizer_state_finalize(
state.get(), iree_make_mutable_string_view(&output_char, 1),
&finalize_written));
if (finalize_written > 0) {
result += output_char;
} else {
break;
}
}
EXPECT_EQ(result, "i\xCC\x87");
}
} // namespace
} // namespace iree::tokenizer