use std::io::Write;
use std::path::PathBuf;
use futures::StreamExt;
use tempfile::{NamedTempFile, TempDir};
use crate::app::cache::{CacheConfig, CacheManager, ReservationStatus};
use crate::app::hash::Md5Hash;
use crate::app::manifest::{ManifestConfig, ManifestStreamer};
use crate::app::models::{DatasetFileInfo, FileInfo, QualityControlVersion};
#[tokio::test]
#[ignore = "Integration test - run explicitly to verify cache structure"]
async fn test_synthetic_manifest_full_integration() {
let temp_dir = TempDir::new().unwrap();
let cache_config = CacheConfig::with_cache_root(temp_dir.path().to_path_buf());
let cache = CacheManager::new(cache_config).await.unwrap();
let manifest_content = generate_synthetic_manifest();
let mut manifest_file = NamedTempFile::new().unwrap();
manifest_file
.write_all(manifest_content.as_bytes())
.unwrap();
manifest_file.flush().unwrap();
let manifest_config = ManifestConfig {
destination_root: temp_dir.path().to_path_buf(),
..Default::default()
};
let manifest_version =
ManifestStreamer::extract_manifest_version_from_path(manifest_file.path());
let mut streamer = ManifestStreamer::with_config_and_version(manifest_config, manifest_version);
let mut stream = streamer.stream(manifest_file.path()).await.unwrap();
let mut processed_files = Vec::new();
let mut cache_operations = 0;
while let Some(result) = stream.next().await {
match result {
Ok(file_info) => {
println!(
"Processing: {} -> {}",
file_info.file_name,
cache.get_file_path(&file_info).display()
);
match cache.check_and_reserve(&file_info).await.unwrap() {
ReservationStatus::Reserved => {
let synthetic_content = generate_file_content_for_hash(&file_info.hash);
match cache.save_file_atomic(&synthetic_content, &file_info).await {
Ok(_) => {
cache_operations += 1;
println!(" ✓ Cached successfully");
}
Err(e) => {
println!(" ⚠ Cache failed (expected for hash mismatch): {}", e);
}
}
}
ReservationStatus::AlreadyExists => {
println!(" ✓ Already exists in cache");
}
ReservationStatus::ReservedByOther { worker_id } => {
println!(" ⚠ Reserved by worker {}", worker_id);
}
}
processed_files.push(file_info);
}
Err(e) => {
eprintln!("Error processing manifest line: {}", e);
}
}
}
println!("\n📊 Processing Summary:");
println!(" Total files processed: {}", processed_files.len());
println!(" Cache operations: {}", cache_operations);
verify_cache_structure(temp_dir.path(), &processed_files).await;
println!("\n🔍 Running cache verification...");
let verification_report = cache
.verify_cache_integrity(processed_files.clone())
.await
.unwrap();
println!(" Files checked: {}", verification_report.files_checked);
println!(" Files verified: {}", verification_report.files_verified);
println!(" Files missing: {}", verification_report.files_missing);
println!(" Files failed: {}", verification_report.files_failed);
println!(" Success rate: {:.1}%", verification_report.success_rate());
println!(
" Verification time: {:.2}s",
verification_report.verification_time.as_secs_f64()
);
if !verification_report.failed_files.is_empty() {
println!("\n Failed files (first 5):");
for (i, failure) in verification_report.failed_files.iter().take(5).enumerate() {
println!(
" {}. {} - {}",
i + 1,
failure.file_info.file_name,
failure.reason
);
}
}
assert!(
processed_files.len() >= 50,
"Should process at least 50 files"
);
assert!(
cache_operations > 0,
"Should have performed some cache operations"
);
assert_eq!(verification_report.files_checked, processed_files.len());
println!(
" Cache operations: {} / {} files",
cache_operations,
processed_files.len()
);
println!("\n✅ Integration test completed successfully!");
println!(
"📁 Cache structure created at: {}",
temp_dir.path().display()
);
println!(" You can inspect the directory structure manually if needed.");
}
#[tokio::test]
async fn test_cache_manager_with_different_file_types() {
let temp_dir = TempDir::new().unwrap();
let cache_config = CacheConfig::with_cache_root(temp_dir.path().to_path_buf());
let cache = CacheManager::new(cache_config).await.unwrap();
let data_file = create_test_file_info_with_type("data", "test-data.csv");
test_file_caching(&cache, data_file).await;
let capability_file = create_test_file_info_with_type("capability", "capability.csv");
test_file_caching(&cache, capability_file).await;
let metadata_file = create_test_file_info_with_type("station-metadata", "station-metadata.csv");
test_file_caching(&cache, metadata_file).await;
let stats = cache.get_cache_stats().await;
assert!(stats.cached_files_count >= 3);
assert!(stats.total_cache_size > 0);
}
#[tokio::test]
async fn test_concurrent_cache_access() {
let temp_dir = TempDir::new().unwrap();
let cache_config = CacheConfig::with_cache_root(temp_dir.path().to_path_buf());
let cache = std::sync::Arc::new(CacheManager::new(cache_config).await.unwrap());
let mut handles = Vec::new();
for i in 0..10 {
let cache = cache.clone();
let handle = tokio::spawn(async move {
let mut file_info = create_test_file_info_with_type("data", &format!("test-{}.csv", i));
let content = format!("test content for file {}", i);
let hash = md5::compute(content.as_bytes());
file_info.hash = Md5Hash::from_bytes(hash.0);
match cache.check_and_reserve(&file_info).await.unwrap() {
ReservationStatus::Reserved => {
cache
.save_file_atomic(content.as_bytes(), &file_info)
.await
.unwrap();
true
}
_ => false,
}
});
handles.push(handle);
}
let results: Vec<bool> = futures::future::join_all(handles)
.await
.into_iter()
.map(|r| r.unwrap())
.collect();
assert_eq!(results.iter().filter(|&&r| r).count(), 10);
let stats = cache.get_cache_stats().await;
assert_eq!(stats.cached_files_count, 10);
}
#[tokio::test]
async fn test_cache_cleanup_operations() {
let temp_dir = TempDir::new().unwrap();
let cache_config = CacheConfig::with_cache_root(temp_dir.path().to_path_buf())
.with_reservation_timeout(std::time::Duration::from_millis(50));
let cache = CacheManager::new(cache_config).await.unwrap();
for i in 0..5 {
let mut file_info = create_test_file_info_with_type("data", &format!("test-{}.csv", i));
let content = format!("unique content {}", i);
let hash = md5::compute(content.as_bytes());
file_info.hash = Md5Hash::from_bytes(hash.0);
cache.check_and_reserve(&file_info).await.unwrap();
}
let reservations = cache.get_all_reservations().await;
assert_eq!(reservations.len(), 5);
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
let cleaned = cache.cleanup_stale_reservations().await.unwrap();
assert_eq!(cleaned, 5);
let reservations = cache.get_all_reservations().await;
assert_eq!(reservations.len(), 0);
}
async fn test_file_caching(cache: &CacheManager, mut file_info: FileInfo) {
let content = format!("test content for {}", file_info.file_name);
let hash = md5::compute(content.as_bytes());
file_info.hash = Md5Hash::from_bytes(hash.0);
let status = cache.check_and_reserve(&file_info).await.unwrap();
assert_eq!(status, ReservationStatus::Reserved);
cache
.save_file_atomic(content.as_bytes(), &file_info)
.await
.unwrap();
let file_path = cache.get_file_path(&file_info);
assert!(file_path.exists());
let status = cache.check_and_reserve(&file_info).await.unwrap();
assert_eq!(status, ReservationStatus::AlreadyExists);
}
fn create_test_file_info_with_type(file_type: &str, file_name: &str) -> FileInfo {
let hash = Md5Hash::from_hex("50c9d1c465f3cbff652be1509c2e2a4e").unwrap();
let dataset_info = DatasetFileInfo {
dataset_name: "test-dataset".to_string(),
version: "202407".to_string(),
county: Some("devon".to_string()),
station_id: Some("01381".to_string()),
station_name: Some("twist".to_string()),
quality_version: Some(QualityControlVersion::V1),
year: Some("1980".to_string()),
file_type: Some(file_type.to_string()),
};
FileInfo {
hash,
relative_path: format!("./data/{}", file_name),
file_name: file_name.to_string(),
dataset_info,
manifest_version: Some(202507),
retry_count: 0,
last_attempt: None,
estimated_size: None,
destination_path: PathBuf::from(format!("/tmp/{}", file_name)),
}
}
fn generate_synthetic_manifest() -> String {
let mut manifest = String::new();
let datasets = [
("uk-daily-temperature-obs", "202407"),
("uk-daily-rainfall-obs", "202407"),
("uk-daily-weather-obs", "202407"),
];
let locations = [
(
"devon",
[
("01381", "twist"),
("01382", "exeter"),
("01383", "plymouth"),
],
),
(
"cornwall",
[
("01234", "newquay"),
("01235", "truro"),
("01236", "falmouth"),
],
),
(
"london",
[("00123", "heathrow"), ("00124", "kew"), ("00125", "city")],
),
(
"yorkshire",
[
("02345", "leeds"),
("02346", "york"),
("02347", "sheffield"),
],
),
];
let quality_versions = ["qc-version-0", "qc-version-1"];
let years = ["1980", "1981", "1985", "2020", "2021", "2023"];
let mut hash_counter = 0u32;
for (dataset, version) in &datasets {
for (county, stations) in &locations {
for (station_id, station_name) in stations {
let capability_path = format!(
"./data/{}/dataset-version-{}/{}/{}_{}/midas-open_{}_dv-{}_{}_{}_{}_capability.csv",
dataset,
version,
county,
station_id,
station_name,
dataset,
version,
county,
station_id,
station_name
);
let capability_content =
generate_synthetic_file_content(hash_counter, "capability");
let capability_hash = compute_content_hash(&capability_content);
manifest.push_str(&format!("{} {}\n", capability_hash, capability_path));
hash_counter += 1;
let metadata_path = format!(
"./data/{}/dataset-version-{}/{}/{}_{}/midas-open_{}_dv-{}_{}_{}_{}_metadata.csv",
dataset,
version,
county,
station_id,
station_name,
dataset,
version,
county,
station_id,
station_name
);
let metadata_content = generate_synthetic_file_content(hash_counter, "metadata");
let metadata_hash = compute_content_hash(&metadata_content);
manifest.push_str(&format!("{} {}\n", metadata_hash, metadata_path));
hash_counter += 1;
for quality_version in &quality_versions {
let qv_short = if quality_version.contains("0") {
"qcv-0"
} else {
"qcv-1"
};
for year in &years {
let data_path = format!(
"./data/{}/dataset-version-{}/{}/{}_{}/{}/midas-open_{}_dv-{}_{}_{}_{}_{}_{}.csv",
dataset,
version,
county,
station_id,
station_name,
quality_version,
dataset,
version,
county,
station_id,
station_name,
qv_short,
year
);
let data_content = generate_synthetic_file_content(hash_counter, "data");
let data_hash = compute_content_hash(&data_content);
manifest.push_str(&format!("{} {}\n", data_hash, data_path));
hash_counter += 1;
}
}
}
}
}
let no_year_path = "./data/uk-daily-temperature-obs/dataset-version-202407/devon/01999_special/special_file.csv";
let no_year_content = generate_synthetic_file_content(hash_counter, "special");
let no_year_hash = compute_content_hash(&no_year_content);
manifest.push_str(&format!("{} {}\n", no_year_hash, no_year_path));
hash_counter += 1;
let no_county_path =
"./data/uk-daily-temperature-obs/dataset-version-202407/unknown_station.csv";
let no_county_content = generate_synthetic_file_content(hash_counter, "unknown");
let no_county_hash = compute_content_hash(&no_county_content);
manifest.push_str(&format!("{} {}\n", no_county_hash, no_county_path));
println!(
"Generated synthetic manifest with {} entries",
manifest.lines().count()
);
manifest
}
fn generate_synthetic_file_content(counter: u32, file_type: &str) -> Vec<u8> {
let content = match file_type {
"capability" => format!(
"# MIDAS Open Dataset Capability File\n# Generated for testing - ID: {}\n\nparameter,units,frequency,start_date,end_date\ntemperature,celsius,daily,1980-01-01,2023-12-31\nrainfall,mm,daily,1980-01-01,2023-12-31\nwind_speed,m/s,daily,1985-01-01,2023-12-31\n",
counter
),
"metadata" => format!(
"# MIDAS Open Dataset Metadata File\n# Generated for testing - ID: {}\n\nstation_id,name,latitude,longitude,elevation,county,start_date,end_date\n12345,Test Station,50.7236,-3.5275,120,Devon,1980-01-01,2023-12-31\n",
counter
),
"data" => format!(
"# MIDAS Open Dataset Data File\n# Generated for testing - ID: {}\n\nstation_id,date,temperature_max,temperature_min,rainfall\n12345,2023-01-01,15.5,8.2,2.1\n12345,2023-01-02,16.2,9.1,0.0\n12345,2023-01-03,14.8,7.5,5.3\n",
counter
),
_ => format!(
"# MIDAS Open Dataset File ({})\n# Generated for testing - ID: {}\n\ndata_column,value\ntest_data,{}\n",
file_type, counter, counter
),
};
content.into_bytes()
}
fn compute_content_hash(content: &[u8]) -> String {
let digest = md5::compute(content);
format!("{:x}", digest)
}
fn generate_file_content_for_hash(hash: &Md5Hash) -> Vec<u8> {
let hash_str = hash.to_hex();
for file_type in &["capability", "metadata", "data", "special", "unknown"] {
for counter in 0u32..1000 {
let test_content = generate_synthetic_file_content(counter, file_type);
let test_hash = compute_content_hash(&test_content);
if test_hash == hash_str {
return test_content;
}
}
}
let fallback_content = format!(
"# Synthetic MIDAS data file\n# Hash: {}\n# Generated for testing purposes\n\nstation_id,date,value\n12345,2023-01-01,15.5\n",
hash_str
);
fallback_content.into_bytes()
}
async fn verify_cache_structure(cache_root: &std::path::Path, files: &[FileInfo]) {
println!("\n🗂️ Cache Directory Structure:");
print_directory_tree(cache_root, 0);
let mut dataset_counts = std::collections::HashMap::new();
for file in files {
*dataset_counts
.entry(&file.dataset_info.dataset_name)
.or_insert(0) += 1;
}
println!("\n📈 Files by Dataset:");
for (dataset, count) in dataset_counts {
println!(" {}: {} files", dataset, count);
}
let expected_datasets = [
"uk-daily-temperature-obs",
"uk-daily-rainfall-obs",
"uk-daily-weather-obs",
];
for dataset in &expected_datasets {
let dataset_path = cache_root.join(dataset);
assert!(
dataset_path.exists(),
"Dataset directory should exist: {}",
dataset
);
let year_dirs = if let Ok(entries) = std::fs::read_dir(&dataset_path) {
entries
.filter_map(|entry| entry.ok())
.filter(|entry| entry.file_type().unwrap().is_dir())
.count()
} else {
0
};
println!(" {}: {} year directories", dataset, year_dirs);
assert!(
year_dirs > 0,
"Should have at least one year directory for {}",
dataset
);
}
let devon_temp_1980_qcv1 = cache_root
.join("uk-daily-temperature-obs")
.join("1980")
.join("qcv-1")
.join("devon");
if devon_temp_1980_qcv1.exists() {
let station_count = if let Ok(entries) = std::fs::read_dir(&devon_temp_1980_qcv1) {
entries
.filter_map(|entry| entry.ok())
.filter(|entry| entry.file_type().unwrap().is_dir())
.count()
} else {
0
};
println!(" devon/1980/qcv-1 stations: {}", station_count);
assert!(station_count > 0, "Should have station directories");
}
}
fn print_directory_tree(dir: &std::path::Path, indent: usize) {
let prefix = " ".repeat(indent);
if let Ok(entries) = std::fs::read_dir(dir) {
let mut entries: Vec<_> = entries.filter_map(|entry| entry.ok()).collect();
entries.sort_by_key(|entry| entry.file_name());
for entry in entries.iter().take(10) {
let file_name = entry.file_name();
let file_name_str = file_name.to_string_lossy();
if entry.file_type().unwrap().is_dir() {
println!("{}📁 {}/", prefix, file_name_str);
if indent < 4 {
print_directory_tree(&entry.path(), indent + 1);
}
} else {
println!("{}📄 {}", prefix, file_name_str);
}
}
if entries.len() > 10 {
println!("{}... ({} more items)", prefix, entries.len() - 10);
}
}
}