use std::fs;
use std::path::PathBuf;
use tempfile::tempdir;
use super::*;
use crate::error::LengthKind;
mod budgets;
mod markers;
mod pairs;
mod prompt;
mod prompt_truncation;
mod windows;
fn tiny(max_length: usize) -> TextTokenizer {
let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/tiny_tokenizer.json");
TextTokenizer::from_file(&path, max_length, PadToken::Id(0)).unwrap()
}
#[test]
fn pads_on_the_right_and_reports_lengths() {
let batch = tiny(64)
.encode(&["hello world", "the cat sat on the mat"], &Device::Cpu)
.unwrap();
assert_eq!(batch.input_ids.dims(), &[2, 8]);
let ids = batch.input_ids.to_vec2::<u32>().unwrap();
assert_eq!(ids[0], vec![2, 4, 5, 3, 0, 0, 0, 0]);
assert_eq!(ids[1], vec![2, 6, 7, 8, 9, 6, 10, 3]);
let mask = batch.attention_mask.to_vec2::<u32>().unwrap();
assert_eq!(mask[0], vec![1, 1, 1, 1, 0, 0, 0, 0]);
assert_eq!(
batch.token_type_ids.to_vec2::<u32>().unwrap()[1],
vec![0; 8]
);
assert_eq!(batch.lengths, vec![4, 8]);
}
#[test]
fn truncates_to_max_length_including_special_tokens() {
let tokenizer = tiny(5);
let batch = tokenizer
.encode(&["hello world", "the cat sat on the mat"], &Device::Cpu)
.unwrap();
assert_eq!(batch.input_ids.dims(), &[2, 5]);
assert_eq!(
batch.input_ids.to_vec2::<u32>().unwrap()[1],
vec![2, 6, 7, 8, 3]
);
assert_eq!(batch.lengths, vec![4, 5]);
assert_eq!(
tokenizer.count(&["the cat sat on the mat", ""]).unwrap(),
vec![5, 2]
);
}
#[test]
fn counting_encodes_without_padding() {
let tokenizer = tiny(64);
assert!(
tokenizer.inner.get_padding().is_none(),
"the tokenizer that counts must carry no padding"
);
let texts = ["hello", "the cat sat on the mat", "hello world"];
assert_eq!(tokenizer.count(&texts).unwrap(), vec![3, 8, 4]);
let padded = tokenizer.encode(&texts, &Device::Cpu).unwrap();
assert_eq!(padded.input_ids.dims(), &[3, 8], "encode still pads");
assert_eq!(padded.lengths, vec![3, 8, 4]);
}
#[test]
fn a_max_length_that_leaves_no_room_for_text_is_refused() {
let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/tiny_tokenizer.json");
for max_length in [0, 1, 2] {
let err = match TextTokenizer::from_file(&path, max_length, PadToken::Id(0)) {
Ok(_) => panic!("max_length {max_length} must be refused"),
Err(err) => err,
};
assert!(
matches!(err, CanddingError::LengthTooSmall { requested, minimum: 3, kind: LengthKind::Text { added: 2 } }
if requested == max_length),
"{max_length}: {err}"
);
assert!(err.to_string().contains('3'), "{err}");
}
assert_eq!(tiny(3).count(&["hello world"]).unwrap(), vec![3]);
}
#[test]
fn splits_subwords() {
let ids = tiny(16)
.encode(&["cats"], &Device::Cpu)
.unwrap()
.input_ids
.to_vec2::<u32>()
.unwrap();
assert_eq!(ids[0], vec![2, 7, 11, 3]);
}
#[test]
fn named_pad_token_is_looked_up_in_the_vocabulary() {
let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/tiny_tokenizer.json");
let tokenizer =
TextTokenizer::from_file(&path, 64, PadToken::Named("[SEP]".to_string())).unwrap();
let ids = tokenizer
.encode(&["hello", "the cat sat"], &Device::Cpu)
.unwrap()
.input_ids
.to_vec2::<u32>()
.unwrap();
assert_eq!(ids[0], vec![2, 4, 3, 3, 3]);
}
#[test]
fn pad_token_prefers_config_then_tokenizer_config_then_unknown() {
let dir = tempdir().unwrap();
let plain = dir.path().join("plain.json");
fs::write(&plain, r#"{"pad_token": "<|endoftext|>"}"#).unwrap();
let added = dir.path().join("added.json");
fs::write(
&added,
r#"{"pad_token": {"content": "<pad>", "lstrip": false}}"#,
)
.unwrap();
assert!(matches!(
read_pad_token(r#"{"pad_token_id": 1}"#, Some(plain.as_path())).unwrap(),
PadToken::Id(1)
));
assert!(matches!(
read_pad_token(r#"{"pad_token_id": null}"#, Some(plain.as_path())).unwrap(),
PadToken::Named(name) if name == "<|endoftext|>"
));
assert!(matches!(
read_pad_token("{}", Some(added.as_path())).unwrap(),
PadToken::Named(name) if name == "<pad>"
));
assert!(matches!(
read_pad_token("{}", None).unwrap(),
PadToken::Unknown
));
}
#[test]
fn a_negative_pad_token_id_is_absent_rather_than_fatal() {
let dir = tempdir().unwrap();
let named = dir.path().join("tokenizer_config.json");
fs::write(&named, r#"{"pad_token": "<pad>"}"#).unwrap();
assert!(matches!(
read_pad_token(r#"{"pad_token_id": -1}"#, Some(named.as_path())).unwrap(),
PadToken::Named(name) if name == "<pad>"
));
assert!(matches!(
read_pad_token(r#"{"pad_token_id": -1}"#, None).unwrap(),
PadToken::Unknown
));
assert!(matches!(
read_pad_token(r#"{"pad_token_id": 0}"#, Some(named.as_path())).unwrap(),
PadToken::Id(0),
));
}
#[test]
fn read_mask_token_prefers_the_published_name_and_falls_back_to_mask_bracket() {
let dir = tempdir().unwrap();
let named = dir.path().join("tokenizer_config.json");
fs::write(&named, r#"{"mask_token": "<mask>"}"#).unwrap();
assert_eq!(read_mask_token(Some(named.as_path())).unwrap(), "<mask>");
let added = dir.path().join("added.json");
fs::write(
&added,
r#"{"mask_token": {"content": "<mask>", "lstrip": true}}"#,
)
.unwrap();
assert_eq!(read_mask_token(Some(added.as_path())).unwrap(), "<mask>");
let no_mask_key = dir.path().join("no_mask_key.json");
fs::write(&no_mask_key, r#"{"pad_token": "[PAD]"}"#).unwrap();
assert_eq!(
read_mask_token(Some(no_mask_key.as_path())).unwrap(),
"[MASK]"
);
assert_eq!(read_mask_token(None).unwrap(), "[MASK]");
}
#[test]
fn a_padding_section_in_the_published_file_is_not_inherited() {
let source = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/tiny_tokenizer.json");
let mut json: serde_json::Value =
serde_json::from_str(&fs::read_to_string(source).unwrap()).unwrap();
json["padding"] = serde_json::json!({
"strategy": {"Fixed": 8},
"direction": "Right",
"pad_to_multiple_of": null,
"pad_id": 0,
"pad_type_id": 0,
"pad_token": "[PAD]"
});
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("tokenizer.json");
fs::write(&path, json.to_string()).unwrap();
let tokenizer = TextTokenizer::from_file(&path, 64, PadToken::Id(0)).unwrap();
assert!(
tokenizer.inner.get_padding().is_none(),
"the file's padding must be cleared"
);
assert_eq!(tokenizer.count(&["", "hello"]).unwrap(), vec![2, 3]);
let batch = tokenizer.encode(&["hello"], &Device::Cpu).unwrap();
assert_eq!(
batch.input_ids.dims(),
&[1, 3],
"a batch pads to its longest row, not to the file's fixed length"
);
}