use std::collections::BTreeMap;
use tpt_tokenizer_core::encoding::{Padding, Truncation};
use tpt_tokenizer_core::{
BpeTokenizer, EncodeConfig, LoadedTokenizer, Tokenizer, TokenizerExt, WordPieceTokenizer,
};
fn bpe_fixture() -> BpeTokenizer {
let mut vocab = BTreeMap::new();
vocab.insert("l".to_string(), 0u32);
vocab.insert("o".to_string(), 1u32);
vocab.insert("w".to_string(), 2u32);
vocab.insert("lo".to_string(), 3u32);
vocab.insert("low".to_string(), 4u32);
let merges = vec![
("l".to_string(), "o".to_string()),
("lo".to_string(), "w".to_string()),
];
BpeTokenizer::from_vocab_merges(vocab, merges)
}
#[test]
fn bpe_merges_to_single_token() {
let tok = bpe_fixture();
assert_eq!(tok.encode("low").unwrap(), vec![4]);
assert_eq!(tok.encode("lo").unwrap(), vec![3]);
}
#[test]
fn bpe_round_trip() {
let tok = bpe_fixture();
let text = "low lo l o w";
let ids = tok.encode(text).unwrap();
assert_eq!(ids, vec![4, 3, 0, 1, 2]);
assert_eq!(tok.decode(&ids).unwrap(), "lowlolow");
}
#[test]
fn bpe_unknown_without_unk_errors() {
let tok = bpe_fixture();
assert!(tok.encode("x").is_err());
}
fn wordpiece_fixture() -> WordPieceTokenizer {
let mut vocab = BTreeMap::new();
vocab.insert("[UNK]".to_string(), 0u32);
vocab.insert("un".to_string(), 1u32);
vocab.insert("##aff".to_string(), 2u32);
vocab.insert("##able".to_string(), 3u32);
vocab.insert("##a".to_string(), 4u32);
vocab.insert("##ble".to_string(), 5u32);
WordPieceTokenizer::from_vocab(vocab, "[UNK]").unwrap()
}
#[test]
fn wordpiece_longest_match() {
let tok = wordpiece_fixture();
assert_eq!(tok.encode("unaffable").unwrap(), vec![1, 2, 3]);
assert_eq!(tok.decode(&[1, 2, 3]).unwrap(), "unaffable");
}
#[test]
fn wordpiece_unknown_word_is_unk() {
let tok = wordpiece_fixture();
assert_eq!(tok.encode("zzz").unwrap(), vec![0]);
assert_eq!(tok.decode(&[0]).unwrap(), "[UNK]");
}
#[test]
fn wordpiece_whole_word_in_vocab() {
let tok = wordpiece_fixture();
assert_eq!(tok.encode("un").unwrap(), vec![1]);
}
fn byte_level_fixture() -> BpeTokenizer {
let mut vocab = BTreeMap::new();
for c in "helo wrdĠ".chars() {
let id = vocab.len() as u32;
vocab.insert(c.to_string(), id);
}
vocab.insert("he".to_string(), vocab.len() as u32);
vocab.insert("Ġw".to_string(), vocab.len() as u32);
let merges = vec![
("h".to_string(), "e".to_string()),
("Ġ".to_string(), "w".to_string()),
];
BpeTokenizer::from_vocab_merges(vocab, merges).with_byte_level()
}
#[test]
fn byte_level_round_trip_is_lossless() {
let tok = byte_level_fixture();
let text = "hello world";
let ids = tok.encode(text).unwrap();
assert_eq!(tok.decode(&ids).unwrap(), text);
}
#[test]
fn byte_level_never_fails_on_unicode() {
let mut vocab = BTreeMap::new();
vocab.insert("<unk>".to_string(), 0u32);
let tok = BpeTokenizer::from_vocab_merges(vocab, vec![]).with_byte_level();
assert!(tok.encode("é🎉").is_ok());
}
#[test]
fn special_tokens_are_atomic() {
let mut vocab = BTreeMap::new();
vocab.insert("a".to_string(), 0u32);
vocab.insert("b".to_string(), 1u32);
let mut specials = BTreeMap::new();
specials.insert("<|endoftext|>".to_string(), 100u32);
let tok = BpeTokenizer::from_vocab_merges(vocab, vec![]).with_special_tokens(specials);
let ids = tok.encode("a<|endoftext|>b").unwrap();
assert_eq!(ids, vec![0, 100, 1]);
assert_eq!(tok.decode(&[0, 100, 1]).unwrap(), "a<|endoftext|>b");
assert_eq!(tok.decode(&[100]).unwrap(), "<|endoftext|>");
}
#[test]
fn wordpiece_splits_punctuation() {
let mut vocab = BTreeMap::new();
vocab.insert("[UNK]".to_string(), 0u32);
vocab.insert("hi".to_string(), 1u32);
vocab.insert("!".to_string(), 2u32);
let tok = WordPieceTokenizer::from_vocab(vocab, "[UNK]").unwrap();
assert_eq!(tok.encode("hi!").unwrap(), vec![1, 2]);
}
#[test]
fn wordpiece_lowercase_option() {
let mut vocab = BTreeMap::new();
vocab.insert("[UNK]".to_string(), 0u32);
vocab.insert("hi".to_string(), 1u32);
let tok = WordPieceTokenizer::from_vocab(vocab, "[UNK]")
.unwrap()
.with_lowercase();
assert_eq!(tok.encode("HI").unwrap(), vec![1]);
}
#[test]
fn empty_input_encodes_to_nothing() {
let bpe = bpe_fixture();
assert_eq!(bpe.encode("").unwrap(), Vec::<u32>::new());
assert_eq!(bpe.encode(" ").unwrap(), Vec::<u32>::new());
let wp = wordpiece_fixture();
assert_eq!(wp.encode("").unwrap(), Vec::<u32>::new());
}
#[test]
fn bpe_from_files_loads_vocab_and_merges() {
let dir = std::env::temp_dir().join(format!("tpt_tok_test_{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let vocab_path = dir.join("vocab.txt");
let merges_path = dir.join("merges.txt");
std::fs::write(&vocab_path, "l\no\nw\nlo\nlow\n").unwrap();
std::fs::write(&merges_path, "#version: 0.2\nl o\nlo w\n").unwrap();
let tok = BpeTokenizer::from_files(vocab_path.to_str().unwrap(), merges_path.to_str().unwrap())
.unwrap();
assert_eq!(tok.encode("low").unwrap(), vec![4]);
assert_eq!(tok.vocab_size(), 5);
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn wordpiece_from_file_errors_on_missing_unk() {
let dir = std::env::temp_dir().join(format!("tpt_tok_test_wp_{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let vocab_path = dir.join("vocab.txt");
std::fs::write(&vocab_path, "hello\nworld\n").unwrap();
let result = WordPieceTokenizer::from_file(vocab_path.to_str().unwrap(), "[UNK]");
assert!(result.is_err());
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn bpe_heap_merges_long_word_correctly() {
let mut vocab = BTreeMap::new();
vocab.insert("a".to_string(), 0u32);
vocab.insert("b".to_string(), 1u32);
vocab.insert("ab".to_string(), 2u32);
vocab.insert("abab".to_string(), 3u32);
let merges = vec![
("a".to_string(), "b".to_string()),
("ab".to_string(), "ab".to_string()),
];
let tok = BpeTokenizer::from_vocab_merges(vocab, merges);
assert_eq!(tok.encode("abababab").unwrap(), vec![3, 3]);
}
#[test]
fn bpe_heap_respects_merge_rank_order() {
let mut vocab = BTreeMap::new();
for (i, t) in ["l", "o", "w", "lo", "low"].iter().enumerate() {
vocab.insert(t.to_string(), i as u32);
}
let merges = vec![
("l".to_string(), "o".to_string()),
("lo".to_string(), "w".to_string()),
];
let tok = BpeTokenizer::from_vocab_merges(vocab, merges);
assert_eq!(tok.encode("low").unwrap(), vec![4]);
}
#[test]
fn encode_with_adds_bos_eos() {
let tok = bpe_fixture();
let cfg = EncodeConfig::new().with_bos(90).with_eos(91);
let enc = tok.encode_with("low", &cfg).unwrap();
assert_eq!(enc.ids, vec![90, 4, 91]);
assert_eq!(enc.attention_mask, vec![1, 1, 1]);
}
#[test]
fn encode_with_fixed_padding() {
let tok = bpe_fixture();
let cfg = EncodeConfig::new().with_pad(0).padding(Padding::Fixed(4));
let enc = tok.encode_with("low", &cfg).unwrap();
assert_eq!(enc.ids, vec![4, 0, 0, 0]);
assert_eq!(enc.attention_mask, vec![1, 0, 0, 0]);
}
#[test]
fn encode_with_truncation_includes_specials() {
let tok = bpe_fixture();
let cfg = EncodeConfig::new()
.with_bos(90)
.with_eos(91)
.truncation(Truncation::Fixed(3));
let enc = tok.encode_with("low lo l o w", &cfg).unwrap();
assert_eq!(enc.ids, vec![90, 4, 91]);
}
#[test]
fn encode_batch_pads_to_longest() {
let tok = bpe_fixture();
let cfg = EncodeConfig::new().with_pad(0).padding(Padding::Longest);
let batch = tok.encode_batch(&["low", "low lo l"], &cfg).unwrap();
assert_eq!(batch[0].ids.len(), batch[1].ids.len());
assert_eq!(batch[0].ids, vec![4, 0, 0]);
assert_eq!(batch[0].attention_mask, vec![1, 0, 0]);
assert_eq!(batch[1].ids, vec![4, 3, 0]);
assert_eq!(batch[1].attention_mask, vec![1, 1, 1]);
}
#[test]
fn padding_without_pad_id_errors() {
let tok = bpe_fixture();
let cfg = EncodeConfig::new().padding(Padding::Fixed(4));
assert!(tok.encode_with("low", &cfg).is_err());
}
#[test]
fn load_bpe_tokenizer_json() {
let json = r#"{
"model": {
"type": "BPE",
"vocab": { "l": 0, "o": 1, "w": 2, "lo": 3, "low": 4 },
"merges": ["l o", ["lo", "w"]]
},
"added_tokens": [
{ "id": 100, "content": "<|endoftext|>", "special": true }
],
"pre_tokenizer": { "type": "Whitespace" }
}"#;
let loaded = tpt_tokenizer_core::from_tokenizer_json_str(json).unwrap();
let LoadedTokenizer::Bpe(tok) = loaded else {
panic!("expected BPE");
};
assert_eq!(tok.encode("low").unwrap(), vec![4]);
assert_eq!(tok.encode("low<|endoftext|>").unwrap(), vec![4, 100]);
}
#[test]
fn load_wordpiece_tokenizer_json_lowercase() {
let json = r#"{
"model": {
"type": "WordPiece",
"unk_token": "[UNK]",
"vocab": { "[UNK]": 0, "hi": 1 }
},
"normalizer": { "type": "BertNormalizer", "lowercase": true }
}"#;
let loaded = tpt_tokenizer_core::from_tokenizer_json_str(json).unwrap();
let LoadedTokenizer::WordPiece(tok) = loaded else {
panic!("expected WordPiece");
};
assert_eq!(tok.encode("HI").unwrap(), vec![1]);
}
#[test]
fn load_tokenizer_json_rejects_unknown_model() {
let json = r#"{ "model": { "type": "Unigram", "vocab": [] } }"#;
assert!(tpt_tokenizer_core::from_tokenizer_json_str(json).is_err());
}
#[test]
fn load_tokenizer_json_rejects_invalid_json() {
assert!(tpt_tokenizer_core::from_tokenizer_json_str("{ not json").is_err());
}
#[test]
fn json_parses_unicode_escapes() {
use tpt_tokenizer_core::json::{self, JsonValue};
let v = json::parse(r#"{ "k": "caf\u00e9 \ud83c\udf89" }"#).unwrap();
assert_eq!(v.get("k").and_then(JsonValue::as_str), Some("café 🎉"));
}
#[cfg(feature = "normalization")]
#[test]
fn nfc_normalization_before_encoding() {
use tpt_tokenizer_core::NormalizationForm;
let mut vocab = BTreeMap::new();
vocab.insert("é".to_string(), 0u32);
let tok =
BpeTokenizer::from_vocab_merges(vocab, vec![]).with_normalization(NormalizationForm::Nfc);
let decomposed = "e\u{0301}"; assert_eq!(tok.encode(decomposed).unwrap(), vec![0]);
}
#[cfg(feature = "normalization")]
#[test]
fn nfkc_normalization_folds_compatibility_chars() {
use tpt_tokenizer_core::{normalize, NormalizationForm};
assert_eq!(normalize("\u{FF21}", NormalizationForm::Nfkc), "A");
}