use std::fs;
use std::path::PathBuf;
use candle_core::Device;
use serde::Deserialize;
use crate::error::CanddingError;
use crate::error::LengthKind;
use crate::tokenizer::PadToken;
use crate::tokenizer::PairTruncationPolicy;
use crate::tokenizer::TextTokenizer;
#[derive(Deserialize)]
struct Fixture {
cases: Vec<Case>,
}
#[derive(Deserialize)]
struct Case {
tokenizer: String,
policy: PairTruncationPolicy,
max_length: usize,
query: String,
document: String,
ids: Vec<u32>,
type_ids: Vec<u32>,
attention_mask: Vec<u32>,
}
const TOKENIZERS: [(&str, usize, usize, usize); 2] = [
("tiny_tokenizer.json", 3, 4, 12),
("tiny_tokenizer_double_separator.json", 4, 5, 16),
];
const POLICIES: [PairTruncationPolicy; 2] = [
PairTruncationPolicy::LongestFirst,
PairTruncationPolicy::PreserveQueryToThreeQuarters,
];
fn fixtures() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures")
}
fn cases() -> Vec<Case> {
let text = fs::read_to_string(fixtures().join("pair_truncation.json")).unwrap();
serde_json::from_str::<Fixture>(&text).unwrap().cases
}
#[test]
fn encode_pairs_matches_the_generators_pair_encoders_at_small_budgets() {
let cases = cases();
assert_eq!(cases.len(), 144);
for case in &cases {
let path = fixtures().join(&case.tokenizer);
let tokenizer =
TextTokenizer::from_file_for_pairs(&path, case.max_length, PadToken::Id(0)).unwrap();
let label = format!(
"{} {:?} max_length {} ({:?}, {:?})",
case.tokenizer, case.policy, case.max_length, case.query, case.document
);
let batch = tokenizer
.encode_pairs(
&[(case.query.as_str(), case.document.as_str())],
case.policy,
&Device::Cpu,
)
.unwrap_or_else(|err| panic!("{label}: {err}"));
assert_eq!(
batch.input_ids.to_vec2::<u32>().unwrap()[0],
case.ids,
"{label}: ids"
);
assert_eq!(
batch.token_type_ids.to_vec2::<u32>().unwrap()[0],
case.type_ids,
"{label}: type ids"
);
assert_eq!(
batch.attention_mask.to_vec2::<u32>().unwrap()[0],
case.attention_mask,
"{label}: attention mask"
);
}
}
#[test]
fn the_fixture_covers_each_tokenizers_minimum_and_edge() {
let cases = cases();
for (tokenizer, _, minimum, edge) in TOKENIZERS {
for policy in POLICIES {
for budget in [minimum, edge] {
assert!(
cases
.iter()
.any(|c| c.tokenizer == tokenizer && c.policy == policy && c.max_length == budget),
"{tokenizer} {policy:?} {budget}"
);
}
}
assert!(
cases.iter().any(|c| {
c.tokenizer == tokenizer
&& c.policy == PairTruncationPolicy::PreserveQueryToThreeQuarters
&& c.max_length == edge
&& !c.document.is_empty()
&& c.ids.len() == edge
}),
"{tokenizer}: the edge with a document cut to zero"
);
}
}
#[test]
fn a_budget_with_no_room_for_a_pair_is_refused_with_the_pair_minimum() {
for (tokenizer, added, minimum, _) in TOKENIZERS {
let path = fixtures().join(tokenizer);
for max_length in 0..minimum {
let refused = TextTokenizer::from_file_for_pairs(&path, max_length, PadToken::Id(0));
assert!(
matches!(refused, Err(CanddingError::LengthTooSmall { requested, minimum: m, kind: LengthKind::Pair { added: a } })
if requested == max_length && m == minimum && a == added),
"{tokenizer} {max_length}: {:?}",
refused.err()
);
let Ok(text_tokenizer) = TextTokenizer::from_file(&path, max_length, PadToken::Id(0)) else {
continue;
};
for policy in POLICIES {
let refused = text_tokenizer.encode_pairs(&[("a b", "c d")], policy, &Device::Cpu);
assert!(
matches!(refused, Err(CanddingError::LengthTooSmall { minimum: m, kind: LengthKind::Pair { .. }, .. }) if m == minimum),
"{tokenizer} {max_length} {policy:?}: {:?}",
refused.err()
);
}
}
assert!(TextTokenizer::from_file_for_pairs(&path, minimum, PadToken::Id(0)).is_ok());
}
}