use candle_core::DType;
use super::CanddingError;
use super::LengthKind;
use crate::registry::Backend;
use crate::registry::Catalog;
use crate::tokenizer::PromptTemplate;
fn on_device(backend: Backend, dtype: DType) -> String {
CanddingError::UnsupportedOnDevice {
model: "m".to_string(),
backend,
dtype,
}
.to_string()
}
#[test]
fn a_dtype_refusal_names_the_dtype_the_backend_and_the_reason() {
let unsupported = CanddingError::UnsupportedDtype {
model: "m".to_string(),
dtype: DType::F16,
};
assert_eq!(
unsupported.to_string(),
"model `m` cannot run in f16: the catalog marks that dtype unsupported for this model"
);
assert_eq!(
on_device(Backend::Cpu, DType::BF16),
"model `m` cannot run on cpu: bf16 matmul is not supported"
);
assert_eq!(
on_device(Backend::Cpu, DType::F16),
"model `m` cannot run on cpu: f16 matmul for this checkpoint (it is accelerator-only there) is not supported"
);
assert_eq!(
on_device(Backend::Metal, DType::F32),
"model `m` cannot run on metal: f32 for this checkpoint (its registry cell reads unsupported there) is not supported"
);
assert_eq!(
on_device(Backend::Cuda, DType::BF16),
"model `m` cannot run on cuda: bf16 for this checkpoint (its registry cell reads unsupported there) is not supported"
);
}
#[test]
fn a_catalog_refusal_names_the_builder_or_the_missing_descriptor() {
let non_dense = |catalog| {
CanddingError::NonDenseModel {
model: "org/m".to_string(),
catalog,
}
.to_string()
};
assert_eq!(
non_dense(Catalog::Sparse),
"model `org/m` is registered in the sparse catalog, not the dense one: load it with `SparseTextEmbedding::builder` (`candding embed --sparse`)"
);
assert_eq!(
non_dense(Catalog::Bm25),
"model `org/m` is registered in the sparse catalog, not the dense one: load it with `Bm25TextEmbedding::builder`"
);
assert_eq!(
non_dense(Catalog::MultiVector),
"model `org/m` is registered in the multi-vector catalog, not the dense one: load it with `MultiVectorTextEmbedding::builder` (`candding embed --multi-vector`)"
);
assert_eq!(
non_dense(Catalog::Rerank),
"model `org/m` is registered in the rerank catalog, not the dense one: load it with `TextCrossEncoder::builder` (`candding rerank`)"
);
let unregistered = |catalog| {
CanddingError::UnregisteredModel {
id: "org/m".to_string(),
catalog,
}
.to_string()
};
assert_eq!(
unregistered(Catalog::Sparse),
"model `org/m` is not in the sparse registry, and no descriptor was supplied: an unregistered sparse model has no snapshot convention to infer its head or vocabulary from"
);
assert_eq!(
unregistered(Catalog::MultiVector),
"model `org/m` is not in the multi-vector registry, and no descriptor was supplied: an unregistered multi-vector model has no snapshot convention to infer its head or projection shape from"
);
assert_eq!(
unregistered(Catalog::Rerank),
"model `org/m` is not in the rerank registry, and no descriptor was supplied: an unregistered rerank model has no snapshot convention to infer its head or score activation from"
);
}
#[test]
fn a_device_error_names_the_backend_and_its_feature_or_candles_reason() {
let not_compiled = CanddingError::BackendNotCompiled {
backend: Backend::Metal,
};
assert_eq!(
not_compiled.to_string(),
"device error: the metal backend is not compiled in; build candding with its `metal` feature"
);
let failed = CanddingError::Device {
backend: Backend::Cuda,
reason: "no device 3".to_string(),
};
assert_eq!(
failed.to_string(),
"device error: the cuda device could not be created: no device 3"
);
}
#[test]
fn a_load_that_cannot_fit_names_both_sizes_in_gigabytes_and_the_ways_out() {
let refused = CanddingError::InsufficientMemory {
model: "Qwen/Qwen3-Embedding-8B".to_string(),
backend: Backend::Metal,
needed_bytes: 30_200_000_762,
available_bytes: 30_150_672_384,
};
assert_eq!(
refused.to_string(),
"model `Qwen/Qwen3-Embedding-8B` needs 30.20 GB for its weights, but the metal device has 30.15 GB available: load it in a smaller dtype, on another device, or choose a smaller model"
);
}
#[test]
fn a_length_refusal_names_its_minimum_and_what_the_fixed_tokens_take() {
let too_small = |requested, minimum, kind| {
CanddingError::LengthTooSmall {
requested,
minimum,
kind,
}
.to_string()
};
assert_eq!(
too_small(2, 3, LengthKind::Text { added: 2 }),
"max_length 2 is below the minimum 3 for this tokenizer: its post-processor adds 2 special tokens to every text, and one real token has to fit beside them"
);
assert_eq!(
too_small(3, 4, LengthKind::Pair { added: 3 }),
"max_length 3 is below the minimum 4 for a (query, document) pair with this tokenizer: its post-processor adds 3 special tokens to every pair, and one real token has to fit beside them"
);
let prompt = LengthKind::Prompt {
template: PromptTemplate::Qwen3Reranker,
prefix: 39,
suffix: 9,
};
assert_eq!(
too_small(48, 49, prompt),
"max_length 48 is below the minimum 49 for the qwen3_reranker prompt template with this tokenizer: its fixed prefix is 39 tokens and its suffix 9, and one content token has to fit between them"
);
let windowed = too_small(11, 12, LengthKind::Windowed { query_cap: 6 });
assert!(
windowed
.starts_with("max_length 11 is below the minimum 12 for a windowed input with query cap 6:"),
"{windowed}"
);
let query_cap = too_small(2, 3, LengthKind::QueryCap { added: 2 });
assert!(
query_cap.starts_with("query cap 2 is below the minimum 3 for a windowed input"),
"{query_cap}"
);
assert!(
query_cap.contains("adds 2 special tokens to a query"),
"{query_cap}"
);
}