candding 0.1.0

Pure candle embeddings for Rust with no ONNX: dense, sparse, multi-vector and rerank models today, image models on the roadmap
Documentation
use super::*;

#[test]
fn missing_weights_name_every_candidate() {
  let dir = tempdir().unwrap();
  write(dir.path(), "config.json", r#"{"model_type":"bert"}"#);
  write(dir.path(), "tokenizer.json", "{}");
  let message = discover(dir.path(), Pipeline::Dense)
    .unwrap_err()
    .to_string();
  for candidate in [
    "model.safetensors",
    "model.safetensors.index.json",
    "pytorch_model.bin",
  ] {
    assert!(message.contains(candidate), "{message}");
  }
}

/// An unregistered pickle-only repository is fetched without its weights, so the message names a Rust download.
#[test]
fn missing_weights_names_the_remedy_a_refetch_cannot_supply() {
  let dir = tempdir().unwrap();
  write(dir.path(), "config.json", r#"{"model_type":"xlm-roberta"}"#);
  write(dir.path(), "tokenizer.json", "{}");
  let message = discover(dir.path(), Pipeline::Dense)
    .unwrap_err()
    .to_string();
  assert!(message.contains("fetch the model again"), "{message}");
  assert!(
    message.contains("https://huggingface.co/<repo>/resolve/<revision>/pytorch_model.bin"),
    "{message}"
  );
  assert!(
    message.contains(&format!("into `{}`", dir.path().display())),
    "the remedy must name this directory: {message}"
  );
  assert!(message.contains("local directory"), "{message}");
  assert!(!message.contains("hf download"), "{message}");
}

#[test]
fn missing_config_is_reported() {
  let dir = tempdir().unwrap();
  write(dir.path(), "tokenizer.json", "{}");
  let err = discover(dir.path(), Pipeline::Dense).unwrap_err();
  assert!(matches!(err, CanddingError::MissingFile(p) if p.ends_with("config.json")));
}

/// A mean in place of a pooling mode candding lacks would answer with a vector the repository does not describe.
#[test]
fn a_pooling_mode_candding_does_not_implement_is_refused() {
  let dir = tempdir().unwrap();
  let path = dir.path().join("config.json");
  for mode in [
    "pooling_mode_max_tokens",
    "pooling_mode_weightedmean_tokens",
    "pooling_mode_mean_sqrt_len_tokens",
  ] {
    write(
      dir.path(),
      "config.json",
      &format!(r#"{{"word_embedding_dimension":384,"{mode}":true}}"#),
    );
    let err = read_pooling(&path).unwrap_err();
    assert!(
      matches!(err, CanddingError::UnsupportedPooling(_)),
      "{mode}: {err}"
    );
    assert!(err.to_string().contains(mode), "{err}");
  }
  write(
    dir.path(),
    "config.json",
    r#"{"pooling_mode_cls_token":true,"pooling_mode_max_tokens":false}"#,
  );
  assert_eq!(
    read_pooling(&path).unwrap(),
    Pooling::Cls,
    "a mode written false is not a request"
  );
  write(dir.path(), "config.json", "{}");
  assert_eq!(
    read_pooling(&path).unwrap(),
    Pooling::Mean,
    "an empty config still means mean"
  );
}

/// A module candding does not run between the transformer and the pooling changes the vectors.
#[test]
fn a_modules_json_entry_candding_does_not_implement_is_refused() {
  let dir = tempdir().unwrap();
  write(dir.path(), "config.json", r#"{"model_type":"bert"}"#);
  write(dir.path(), "tokenizer.json", "{}");
  write(dir.path(), "model.safetensors", "");
  write(
    dir.path(),
    "modules.json",
    r#"[{"idx":0,"name":"0","path":"","type":"sentence_transformers.models.Transformer"},{"idx":1,"name":"1","path":"1_LSTM","type":"sentence_transformers.models.LSTM"},{"idx":2,"name":"2","path":"2_Pooling","type":"sentence_transformers.models.Pooling"}]"#,
  );
  let err = discover(dir.path(), Pipeline::Dense).unwrap_err();
  assert!(matches!(err, CanddingError::UnsupportedModule(_)), "{err}");
  assert!(
    err
      .to_string()
      .contains("sentence_transformers.models.LSTM"),
    "{err}"
  );
}