use std::io::Cursor;
use face_core::input::sniff::SNIFF_PROBE_BYTES;
use face_core::input::{SniffOutcome, sniff_format};
use face_core::{FaceError, InputFormat};
fn sniff(bytes: &[u8]) -> Result<SniffOutcome, FaceError> {
let mut reader = Cursor::new(bytes);
sniff_format(&mut reader)
}
mod json {
use super::*;
#[test]
fn array_of_numbers() {
let outcome = sniff(b"[1, 2, 3]").expect("array sniffs as json");
assert_eq!(outcome.format, InputFormat::Json);
assert_eq!(outcome.bom_bytes, 0);
}
#[test]
fn single_object() {
let outcome = sniff(b"{\"a\": 1}").expect("object sniffs as json");
assert_eq!(outcome.format, InputFormat::Json);
assert_eq!(outcome.bom_bytes, 0);
}
#[test]
fn nested_object_with_array_field() {
let outcome = sniff(b"{\"items\": [1, 2]}").expect("nested-array object sniffs as json");
assert_eq!(outcome.format, InputFormat::Json);
}
#[test]
fn pretty_printed_with_leading_whitespace() {
let outcome =
sniff(b" \n [1, 2, 3]\n").expect("leading whitespace then array sniffs as json");
assert_eq!(outcome.format, InputFormat::Json);
}
#[test]
fn large_pretty_array_past_probe() {
let bytes = format!(
r#"[{{"kind":"bug","text":"{}"}},{{"kind":"feat","text":"done"}}]"#,
"x".repeat(SNIFF_PROBE_BYTES * 2)
);
let outcome = sniff(bytes.as_bytes()).expect("large array prefix sniffs as json");
assert_eq!(
outcome.format,
InputFormat::Json,
"a valid JSON value larger than the sniff probe should be parsed by the full JSON parser",
);
}
}
mod jsonl {
use super::*;
#[test]
fn simple_two_records() {
let outcome = sniff(b"{\"a\": 1}\n{\"a\": 2}\n")
.expect("two newline-separated objects sniff as jsonl");
assert_eq!(outcome.format, InputFormat::Jsonl);
assert_eq!(outcome.bom_bytes, 0);
}
#[test]
fn trailing_no_newline() {
let outcome = sniff(b"{\"a\": 1}\n{\"a\": 2}")
.expect("two records without trailing newline sniff as jsonl");
assert_eq!(outcome.format, InputFormat::Jsonl);
}
#[test]
fn singleline_object_followed_by_array() {
let outcome = sniff(b"{\"a\": 1}\n[1, 2, 3]").expect("object then array sniffs as jsonl");
assert_eq!(outcome.format, InputFormat::Jsonl);
}
}
mod face_envelope {
use super::*;
const ENVELOPE_RESULT_FIRST: &[u8] = br#"{
"result": {"input_total": 0, "skipped": 0, "axes": []},
"clusters": []
}"#;
const ENVELOPE_CLUSTERS_FIRST: &[u8] = br#"{
"clusters": [],
"result": {"input_total": 0, "skipped": 0, "axes": []}
}"#;
const RESULT_ONLY: &[u8] = br#"{
"result": {"input_total": 0}
}"#;
#[test]
fn minimal_envelope_signature() {
let outcome = sniff(ENVELOPE_RESULT_FIRST).expect("envelope signature sniffs");
assert_eq!(outcome.format, InputFormat::FaceEnvelope);
assert_eq!(outcome.bom_bytes, 0);
}
#[test]
fn clusters_first_then_result() {
let outcome =
sniff(ENVELOPE_CLUSTERS_FIRST).expect("envelope sniffs regardless of key order");
assert_eq!(outcome.format, InputFormat::FaceEnvelope);
}
#[test]
fn result_only_no_clusters() {
let outcome = sniff(RESULT_ONLY).expect("partial signature sniffs as json");
assert_eq!(
outcome.format,
InputFormat::Json,
"object with `result` but no `clusters` is plain JSON, not a face envelope",
);
}
#[test]
fn detects_large_envelope() {
let padding = "x".repeat(8192);
let mut bytes = String::new();
bytes.push_str(r#"{"result":{"input_total":0,"skipped":0,"axes":[]},"#);
bytes.push_str(r#""clusters":[],"#);
bytes.push_str(&format!(r#""meta":{{"pad":"{padding}"}}}}"#));
let outcome = sniff(bytes.as_bytes()).expect("large envelope sniffs");
assert_eq!(
outcome.format,
InputFormat::FaceEnvelope,
"envelope larger than the sniff prefix must still classify as face-envelope",
);
}
}
mod bom {
use super::*;
const BOM: &[u8] = b"\xEF\xBB\xBF";
#[test]
fn utf8_bom_then_json_array() {
let mut bytes = Vec::from(BOM);
bytes.extend_from_slice(b"[1, 2, 3]");
let outcome = sniff(&bytes).expect("BOM + array sniffs as json");
assert_eq!(outcome.format, InputFormat::Json);
assert_eq!(
outcome.bom_bytes, 3,
"UTF-8 BOM is 3 bytes; sniff_format must report the consumed prefix length",
);
}
#[test]
fn utf8_bom_then_jsonl() {
let mut bytes = Vec::from(BOM);
bytes.extend_from_slice(b"{\"a\": 1}\n{\"a\": 2}\n");
let outcome = sniff(&bytes).expect("BOM + jsonl sniffs as jsonl");
assert_eq!(outcome.format, InputFormat::Jsonl);
assert_eq!(outcome.bom_bytes, 3);
}
#[test]
fn no_bom_returns_zero() {
let outcome = sniff(b"[1, 2, 3]").expect("plain array");
assert_eq!(
outcome.bom_bytes, 0,
"no BOM in input → bom_bytes must be 0"
);
}
}
mod delimited {
use super::*;
#[test]
fn csv_input_sniffs() {
let outcome = sniff(b"a,b,c\n1,2,3\n4,5,6").expect("CSV sniffs");
assert_eq!(outcome.format, InputFormat::Csv);
}
#[test]
fn tsv_input_sniffs() {
let outcome = sniff(b"a\tb\tc\n1\t2\t3\n4\t5\t6").expect("TSV sniffs");
assert_eq!(outcome.format, InputFormat::Tsv);
}
#[test]
fn pipe_delimited_input_sniffs_as_csv() {
let outcome = sniff(b"a|b|c\n1|2|3\n4|5|6").expect("pipe-delimited input sniffs");
assert_eq!(outcome.format, InputFormat::Csv);
}
}
mod errors {
use super::*;
#[test]
fn unclassifiable_input_errors() {
let result = sniff(b"not structured enough");
assert!(
result.is_err(),
"unclassifiable input must error, got {result:?}"
);
}
#[test]
fn empty_input_errors() {
let result = sniff(b"");
assert!(result.is_err(), "empty input must error, got {result:?}");
}
}