use std::fs;
use std::path::Path;
use std::path::PathBuf;
use candle_core::Device;
use tempfile::TempDir;
use tempfile::tempdir;
use crate::error::CanddingError;
use crate::error::LengthKind;
use crate::tokenizer::PadToken;
use crate::tokenizer::PromptTemplate;
use crate::tokenizer::PromptTokenizer;
pub(super) const PAD: u32 = 151_643;
pub(super) const DEFAULT: &str =
"Given a web search query, retrieve relevant passages that answer the query";
const CUSTOM: &str =
"Given a question about geography, judge whether the passage names the correct city";
const QUERY: &str = "What is the capital of China?";
const DOCUMENT: &str = "The capital of China is Beijing.";
const P0_IDS: [u32; 85] = [
151644, 8948, 198, 60256, 3425, 279, 11789, 20027, 279, 8502, 3118, 389, 279, 11361, 323, 279,
758, 1235, 3897, 13, 7036, 429, 279, 4226, 646, 1172, 387, 330, 9693, 1, 476, 330, 2152, 3263,
151645, 198, 151644, 872, 198, 27, 641, 1235, 26818, 16246, 264, 3482, 2711, 3239, 11, 17179,
9760, 46769, 429, 4226, 279, 3239, 198, 27, 2859, 26818, 3555, 374, 279, 6722, 315, 5616, 5267,
75692, 26818, 576, 6722, 315, 5616, 374, 26549, 13, 151645, 198, 151644, 77091, 198, 151667, 271,
151668, 271,
];
pub(super) fn subset_path() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("tests/fixtures/tiny_tokenizer_qwen3_reranker.json")
}
pub(super) fn qwen3(path: &Path, max_length: usize) -> PromptTokenizer {
PromptTokenizer::from_file(
path,
max_length,
PadToken::Id(PAD),
PromptTemplate::Qwen3Reranker,
)
.unwrap()
}
fn edited(edit: impl FnOnce(&mut serde_json::Value)) -> (TempDir, PathBuf) {
let mut json: serde_json::Value =
serde_json::from_str(&fs::read_to_string(subset_path()).unwrap()).unwrap();
edit(&mut json);
let dir = tempdir().unwrap();
let path = dir.path().join("tokenizer.json");
fs::write(&path, serde_json::to_string(&json).unwrap()).unwrap();
(dir, path)
}
#[test]
fn the_template_renders_its_prefix_content_and_suffix_verbatim() {
let template = PromptTemplate::Qwen3Reranker;
assert_eq!(
template.render("I", "Q", "D"),
format!(
"{}<Instruct>: I\n<Query>: Q\n<Document>: D{}",
template.prefix(),
template.suffix()
)
);
assert!(template.suffix().ends_with("</think>\n\n"));
assert!(!template.suffix().ends_with("\n\n\n"));
}
#[test]
fn a_known_triple_encodes_to_the_references_ids() {
let tokenizer = qwen3(&subset_path(), 8192);
assert_eq!(tokenizer.prefix_len(), 39);
assert_eq!(tokenizer.suffix_ids(), &P0_IDS[76..]);
assert_eq!(
tokenizer.prompt_ids(DEFAULT, QUERY, DOCUMENT).unwrap(),
P0_IDS
);
}
#[test]
fn the_instruction_reaches_its_slot() {
let tokenizer = qwen3(&subset_path(), 8192);
let slot = |instruction: &str| {
let ids = tokenizer.prompt_ids(instruction, QUERY, DOCUMENT).unwrap();
let head = &P0_IDS[..43];
let tail = &P0_IDS[56..];
assert!(ids.starts_with(head), "{instruction:?}");
assert!(ids.ends_with(tail), "{instruction:?}");
ids[head.len()..ids.len() - tail.len()].to_vec()
};
assert_eq!(slot(DEFAULT), &P0_IDS[43..56]);
let custom = slot(CUSTOM);
assert_ne!(custom, slot(DEFAULT));
assert_eq!(custom.len(), 14);
}
#[test]
fn an_empty_instruction_renders_empty() {
let template = PromptTemplate::Qwen3Reranker;
assert!(
template
.content("", QUERY, DOCUMENT)
.starts_with("<Instruct>: \n<Query>: ")
);
let tokenizer = qwen3(&subset_path(), 8192);
let empty = tokenizer.prompt_ids("", QUERY, DOCUMENT).unwrap();
assert_eq!(empty.len(), P0_IDS.len() - 13);
assert_eq!(&empty[..43], &P0_IDS[..43]);
assert_eq!(&empty[44..], &P0_IDS[57..]);
}
#[test]
fn a_max_length_below_the_templates_minimum_is_refused_by_name() {
for max_length in [0, 1, 9, 39, 48] {
let refused = PromptTokenizer::from_file(
&subset_path(),
max_length,
PadToken::Id(PAD),
PromptTemplate::Qwen3Reranker,
);
match refused {
Err(
err @ CanddingError::LengthTooSmall {
requested,
minimum: 49,
kind:
LengthKind::Prompt {
template: PromptTemplate::Qwen3Reranker,
prefix: 39,
suffix: 9,
},
},
) => {
assert_eq!(requested, max_length);
let message = err.to_string();
assert!(message.contains("minimum 49"), "{message}");
assert!(message.contains("qwen3_reranker"), "{message}");
}
Err(other) => panic!("{max_length}: unexpected error: {other}"),
Ok(_) => panic!("{max_length} leaves no content token"),
}
}
assert_eq!(qwen3(&subset_path(), 49).max_length(), 49);
}
#[test]
fn the_suffix_check_refuses_a_rendering_that_disagrees_with_the_template() {
let tokenizer = qwen3(&subset_path(), 8192);
let template = PromptTemplate::Qwen3Reranker;
let rendered = format!("{}\n", template.render(DEFAULT, QUERY, DOCUMENT));
match tokenizer.rendered_ids(&rendered) {
Err(CanddingError::PromptSuffixMismatch {
expected, found, ..
}) => {
assert_eq!(expected, &P0_IDS[76..]);
assert_ne!(found, expected);
}
other => panic!("expected a suffix mismatch, got {other:?}"),
}
}
#[test]
fn the_suffix_check_refuses_a_tokenizer_that_merges_the_suffix_into_the_document() {
let (_dir, path) = edited(|json| json["added_tokens"] = serde_json::json!([]));
let tokenizer = qwen3(&path, 8192);
let minimum = tokenizer.prefix_len() + tokenizer.suffix_ids().len() + 1;
assert!(minimum > 49, "{minimum}");
let refused =
PromptTokenizer::from_file(&path, 49, PadToken::Id(PAD), PromptTemplate::Qwen3Reranker);
assert!(
matches!(refused, Err(CanddingError::LengthTooSmall { minimum: m, kind: LengthKind::Prompt { .. }, .. }) if m == minimum),
"{:?}",
refused.err()
);
match tokenizer.prompt_ids(DEFAULT, QUERY, DOCUMENT) {
Err(CanddingError::PromptSuffixMismatch {
expected, found, ..
}) => assert_ne!(found, expected),
other => panic!("expected a suffix mismatch, got {other:?}"),
}
}
#[test]
fn a_prompt_gets_no_special_tokens_from_the_post_processor() {
let (_dir, path) = edited(|json| {
json["post_processor"] = serde_json::json!({
"type": "TemplateProcessing",
"single": [{"SpecialToken": {"id": "<|endoftext|>", "type_id": 0}}, {"Sequence": {"id": "A", "type_id": 0}}],
"pair": [{"Sequence": {"id": "A", "type_id": 0}}, {"Sequence": {"id": "B", "type_id": 1}}],
"special_tokens": {"<|endoftext|>": {"id": "<|endoftext|>", "ids": [PAD], "tokens": ["<|endoftext|>"]}}
})
});
let tokenizer = qwen3(&path, 8192);
assert_eq!(
tokenizer.prompt_ids(DEFAULT, QUERY, DOCUMENT).unwrap(),
P0_IDS
);
}
#[test]
fn a_batch_is_right_padded_with_real_lengths_and_zero_token_types() {
let tokenizer = qwen3(&subset_path(), 8192);
let pairs = [(QUERY, DOCUMENT), ("", "")];
let batch = tokenizer.encode(DEFAULT, &pairs, &Device::Cpu).unwrap();
let short = tokenizer.prompt_ids(DEFAULT, "", "").unwrap();
assert_eq!(batch.lengths, vec![85, short.len()]);
assert_eq!(tokenizer.count(DEFAULT, &pairs).unwrap(), batch.lengths);
let ids = batch.input_ids.to_vec2::<u32>().unwrap();
assert_eq!(ids[0], P0_IDS);
assert_eq!(&ids[1][..short.len()], short.as_slice());
assert!(ids[1][short.len()..].iter().all(|&id| id == PAD));
let mask = batch.attention_mask.to_vec2::<u32>().unwrap();
assert_eq!(mask[1].iter().filter(|&&m| m == 1).count(), short.len());
assert!(mask[0].iter().all(|&m| m == 1));
let types = batch.token_type_ids.to_vec2::<u32>().unwrap();
assert!(types.iter().flatten().all(|&t| t == 0));
}