mod common;
use std::io;
use std::time::{Duration, SystemTime};
use common::{golden_digits, one_million_path, two_million_path, TempYcd};
use ycd_reader::{YcdFileUtil, YcdIndex};
#[test]
fn build_empty_list_returns_error() {
let empty: [&str; 0] = [];
assert_eq!(
YcdIndex::build(&empty).unwrap_err().kind(),
io::ErrorKind::InvalidInput
);
}
#[test]
fn build_single_file_succeeds() {
let idx = YcdIndex::build(&[one_million_path(0)]).unwrap();
assert_eq!(idx.len(), 1);
assert!(!idx.is_empty());
}
#[test]
fn build_three_files_succeeds() {
let files = [
one_million_path(0),
one_million_path(1),
one_million_path(2),
];
let idx = YcdIndex::build(&files).unwrap();
assert_eq!(idx.len(), 3);
}
#[test]
fn build_noncontiguous_files_returns_error() {
let files = [one_million_path(0), one_million_path(2)];
assert_eq!(
YcdIndex::build(&files).unwrap_err().kind(),
io::ErrorKind::InvalidInput
);
}
#[test]
fn build_stores_correct_metadata() {
let files = [one_million_path(0), one_million_path(1)];
let idx = YcdIndex::build(&files).unwrap();
let entries = idx.entries();
assert_eq!(entries[0].file_start, 1);
assert_eq!(entries[0].file_length, 1_000_000);
assert_eq!(entries[1].file_start, 1_000_001);
assert_eq!(entries[1].file_length, 1_000_000);
}
#[test]
fn build_stores_absolute_path_when_given_relative() {
let file = TempYcd::valid("1415926535897932384", 19, 0, "\n", "").unwrap();
let abs_path = file.path.canonicalize().unwrap();
let cwd = std::env::current_dir().unwrap();
let rel_path = abs_path
.strip_prefix(&cwd)
.unwrap_or(abs_path.as_path())
.to_path_buf();
let idx = YcdIndex::build(&[&rel_path]).unwrap();
assert!(
idx.entries()[0].path.is_absolute(),
"expected an absolute path, got: {}",
idx.entries()[0].path.display()
);
}
#[test]
fn rebuild_replaces_index() {
let files1 = [one_million_path(0)];
let mut idx = YcdIndex::build(&files1).unwrap();
assert_eq!(idx.len(), 1);
let files3 = [
one_million_path(0),
one_million_path(1),
one_million_path(2),
];
idx.rebuild(&files3).unwrap();
assert_eq!(idx.len(), 3);
}
#[test]
fn rebuild_leaves_index_unchanged_on_error() {
let files1 = [one_million_path(0)];
let mut idx = YcdIndex::build(&files1).unwrap();
let bad = [one_million_path(0), one_million_path(2)];
assert!(idx.rebuild(&bad).is_err());
assert_eq!(idx.len(), 1);
}
#[test]
fn read_digits_matches_ycd_file_util() {
let golden = golden_digits();
let files = [
one_million_path(0),
one_million_path(1),
one_million_path(2),
];
let idx = YcdIndex::build(&files).unwrap();
let test_cases: &[(usize, usize)] = &[
(1, 1),
(1, 19),
(1, 20),
(1, 100),
(19, 1),
(20, 1),
(19, 2),
(500, 1),
(12_345, 50),
(500_000, 100),
(999_990, 20),
(1_000_000, 1),
(1_000_001, 1),
(1_000_000, 2),
(1_999_990, 20),
(2_000_000, 1),
(2_000_001, 1),
(3_000_000, 1),
(2_999_996, 5),
];
for &(start, len) in test_cases {
let from_util = YcdFileUtil::read_digits(&files, start, len).unwrap();
let from_idx = idx.read_digits(start, len).unwrap();
assert_eq!(from_util, from_idx, "mismatch at start={start} len={len}");
assert_eq!(
from_idx,
&golden[start - 1..start - 1 + len],
"golden mismatch at start={start} len={len}"
);
}
}
#[test]
fn read_digits_two_million_layout() {
let golden = golden_digits();
let files = [two_million_path(0), one_million_path(2)];
let idx = YcdIndex::build(&files).unwrap();
let test_cases: &[(usize, usize)] = &[
(1, 100),
(1_999_990, 20),
(2_000_000, 1),
(2_000_001, 1),
(2_000_000, 2),
(2_999_996, 5),
];
for &(start, len) in test_cases {
let from_util = YcdFileUtil::read_digits(&files, start, len).unwrap();
let from_idx = idx.read_digits(start, len).unwrap();
assert_eq!(from_util, from_idx, "mismatch at start={start} len={len}");
assert_eq!(
from_idx,
&golden[start - 1..start - 1 + len],
"golden mismatch at start={start} len={len}"
);
}
}
#[test]
fn read_digits_non_zero_blockid_first_file() {
let golden = golden_digits();
let files = [one_million_path(1), one_million_path(2)];
let idx = YcdIndex::build(&files).unwrap();
let result = idx.read_digits(1_000_001, 10).unwrap();
assert_eq!(result, &golden[1_000_000..1_000_010]);
}
#[test]
fn read_digits_error_position_zero() {
let idx = YcdIndex::build(&[one_million_path(0)]).unwrap();
assert_eq!(
idx.read_digits(0, 1).unwrap_err().kind(),
io::ErrorKind::InvalidInput
);
}
#[test]
fn read_digits_error_length_zero() {
let idx = YcdIndex::build(&[one_million_path(0)]).unwrap();
assert_eq!(
idx.read_digits(1, 0).unwrap_err().kind(),
io::ErrorKind::InvalidInput
);
}
#[test]
fn read_digits_error_out_of_range() {
let idx = YcdIndex::build(&[one_million_path(1)]).unwrap();
assert_eq!(
idx.read_digits(1_000_000, 1).unwrap_err().kind(),
io::ErrorKind::InvalidInput
);
assert_eq!(
idx.read_digits(2_000_001, 1).unwrap_err().kind(),
io::ErrorKind::InvalidInput
);
assert_eq!(
idx.read_digits(2_000_000, 2).unwrap_err().kind(),
io::ErrorKind::InvalidInput
);
}
#[test]
fn read_digits_error_overflow() {
let idx = YcdIndex::build(&[one_million_path(0)]).unwrap();
assert_eq!(
idx.read_digits(1, usize::MAX).unwrap_err().kind(),
io::ErrorKind::InvalidData
);
}
#[test]
fn stale_index_file_size_change_detected() {
let file = TempYcd::valid("1415926535897932384", 19, 0, "\n", "").unwrap();
let mut idx = YcdIndex::build(&[&file.path]).unwrap();
let mut contents = std::fs::read(&file.path).unwrap();
contents.push(0xff);
std::fs::write(&file.path, &contents).unwrap();
let err = idx.read_digits(1, 1).unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::InvalidData);
assert!(
err.to_string().contains("stale"),
"expected 'stale' in error message, got: {err}"
);
idx.rebuild(&[&file.path]).unwrap();
assert!(idx.read_digits(1, 1).is_ok());
}
#[test]
fn stale_index_mtime_change_detected() {
let file = TempYcd::valid("1415926535897932384", 19, 0, "\n", "").unwrap();
let idx = YcdIndex::build(&[&file.path]).unwrap();
let new_mtime = SystemTime::UNIX_EPOCH
.checked_add(Duration::from_secs(1))
.unwrap();
let new_mtime_ft = filetime::FileTime::from_system_time(new_mtime);
filetime::set_file_mtime(&file.path, new_mtime_ft).unwrap();
let err = idx.read_digits(1, 1).unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::InvalidData);
assert!(
err.to_string().contains("stale"),
"expected 'stale' in error message, got: {err}"
);
}
#[test]
fn index_does_not_open_unneeded_files() {
let src0 = std::fs::read(one_million_path(0)).unwrap();
let src1 = std::fs::read(one_million_path(1)).unwrap();
let tmp0 = TempYcd::from_bytes(&src0).unwrap();
let tmp1 = TempYcd::from_bytes(&src1).unwrap();
let files = [&tmp0.path, &tmp1.path];
let idx = YcdIndex::build(&files).unwrap();
let mut contents1 = std::fs::read(&tmp1.path).unwrap();
contents1.push(0xff);
std::fs::write(&tmp1.path, &contents1).unwrap();
let result = idx.read_digits(1, 100);
assert!(
result.is_ok(),
"read within first file should not check second file, but got: {:?}",
result.unwrap_err()
);
let err = idx.read_digits(1_000_000, 2).unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::InvalidData);
assert!(err.to_string().contains("stale"));
}