1use super::types::{
8 PinnedDataset, DatasetMetadata, DatasetVersion, ValidationResult,
9 InconsistentQuery, LoadingError, BenchmarkConfig, GoldenQuery
10};
11use anyhow::{anyhow, Result};
12use serde_json;
13use std::collections::HashMap;
14use std::path::{Path, PathBuf};
15use std::sync::Arc;
16use tokio::fs;
17use tokio::sync::RwLock;
18use tracing::{info, warn, error, debug};
19use chrono::{DateTime, Utc};
20
21pub struct PinnedDatasetLoader {
23 config: BenchmarkConfig,
24 dataset_cache: Arc<RwLock<HashMap<String, Arc<PinnedDataset>>>>,
25 available_versions: Arc<RwLock<Vec<DatasetVersion>>>,
26}
27
28impl PinnedDatasetLoader {
29 pub async fn new() -> Result<Self> {
31 let config = BenchmarkConfig::default();
32 Self::with_config(config).await
33 }
34
35 pub async fn with_config(config: BenchmarkConfig) -> Result<Self> {
37 info!("๐ง Initializing pinned dataset loader");
38 debug!("Dataset path: {}", config.dataset_path);
39
40 let loader = Self {
41 config,
42 dataset_cache: Arc::new(RwLock::new(HashMap::new())),
43 available_versions: Arc::new(RwLock::new(Vec::new())),
44 };
45
46 if loader.config.auto_discover_datasets {
48 if let Err(e) = loader.discover_available_versions().await {
49 warn!("Failed to discover datasets: {}", e);
50 }
52 }
53
54 info!("โ
Pinned dataset loader initialized");
55 Ok(loader)
56 }
57
58 pub async fn load_current_pinned_dataset(&self) -> Result<PinnedDataset> {
60 debug!("๐ Loading current pinned dataset");
61
62 let current_path = PathBuf::from(&self.config.dataset_path)
64 .join("golden-pinned-current.json");
65
66 if current_path.exists() {
67 info!("๐ Loading current dataset from: {}", current_path.display());
68 return self.load_dataset_from_file(¤t_path).await;
69 }
70
71 if let Some(ref default_version) = self.config.default_version {
73 info!("๐ Loading default version: {}", default_version);
74 return self.load_pinned_dataset_version(default_version).await;
75 }
76
77 let versions = self.list_available_versions().await?;
79 if !versions.is_empty() {
80 let latest = &versions[0]; info!("๐ Loading latest available version: {}", latest.version);
82 return self.load_dataset_from_file(&latest.file_path).await;
83 }
84
85 warn!("โ ๏ธ No pinned datasets found, creating mock dataset for development");
87 Ok(self.create_mock_dataset())
88 }
89
90 pub async fn load_pinned_dataset_version(&self, version: &str) -> Result<PinnedDataset> {
92 debug!("๐ Loading pinned dataset version: {}", version);
93
94 if self.config.enable_caching {
96 let cache = self.dataset_cache.read().await;
97 if let Some(cached) = cache.get(version) {
98 info!("๐พ Loaded dataset from cache: {}", version);
99 return Ok((**cached).clone());
100 }
101 }
102
103 let filename = format!("golden-pinned-{}.json", version);
105 let file_path = PathBuf::from(&self.config.dataset_path).join(&filename);
106
107 if !file_path.exists() {
108 return Err(anyhow!(LoadingError::VersionNotFound {
109 version: version.to_string()
110 }));
111 }
112
113 info!("๐ Loading dataset from: {}", file_path.display());
114 let dataset = self.load_dataset_from_file(&file_path).await?;
115
116 if self.config.enable_caching {
118 self.cache_dataset(version, &dataset).await;
119 }
120
121 Ok(dataset)
122 }
123
124 async fn load_dataset_from_file(&self, file_path: &Path) -> Result<PinnedDataset> {
126 let start_time = std::time::Instant::now();
127
128 let content = fs::read_to_string(file_path).await
129 .map_err(|e| anyhow!(LoadingError::IoError { source: e }))?;
130
131 if let Ok(dataset) = serde_json::from_str::<PinnedDataset>(&content) {
133 let duration = start_time.elapsed();
134 info!("โ
Loaded pinned dataset: {} queries in {:?}",
135 dataset.queries.len(), duration);
136 return Ok(dataset);
137 }
138
139 match serde_json::from_str::<Vec<GoldenQuery>>(&content) {
141 Ok(queries) => {
142 info!("๐ Converting legacy dataset format to PinnedDataset");
143 let dataset = self.convert_legacy_to_pinned(queries, file_path)?;
144
145 let duration = start_time.elapsed();
146 info!("โ
Converted and loaded dataset: {} queries in {:?}",
147 dataset.queries.len(), duration);
148 Ok(dataset)
149 }
150 Err(e) => {
151 error!("โ Failed to parse dataset from {}: {}", file_path.display(), e);
152 Err(anyhow!(LoadingError::JsonParseError { source: e }))
153 }
154 }
155 }
156
157 fn convert_legacy_to_pinned(&self, queries: Vec<GoldenQuery>, file_path: &Path) -> Result<PinnedDataset> {
159 let version = file_path.file_stem()
160 .and_then(|s| s.to_str())
161 .unwrap_or("unknown")
162 .replace("golden-pinned-", "");
163
164 let mut query_distribution = HashMap::new();
165 let mut languages = std::collections::HashSet::new();
166
167 for query in &queries {
168 *query_distribution.entry(query.query_type.clone()).or_insert(0) += 1;
169 if let Some(ref lang) = query.language {
170 languages.insert(lang.clone());
171 }
172 }
173
174 let metadata = DatasetMetadata {
175 version: version.clone(),
176 name: "storyviz-pinned".to_string(),
177 created_at: Utc::now(),
178 total_queries: queries.len(),
179 query_distribution,
180 languages: languages.into_iter().collect(),
181 git_sha: None,
182 additional_metadata: HashMap::new(),
183 };
184
185 let mut slices = HashMap::new();
187 let all_indices: Vec<usize> = (0..queries.len()).collect();
188 slices.insert("ALL".to_string(), all_indices.clone());
189
190 let smoke_size = std::cmp::min(40, queries.len());
192 slices.insert("SMOKE_DEFAULT".to_string(), (0..smoke_size).collect());
193
194 Ok(PinnedDataset {
195 metadata,
196 queries,
197 slices,
198 corpus_info: None,
199 })
200 }
201
202 pub async fn validate_dataset_consistency(&self, dataset: &PinnedDataset) -> Result<ValidationResult> {
204 info!("๐ Validating corpus consistency for {} queries", dataset.queries.len());
205
206 let mut valid_queries = 0;
207 let mut inconsistent_queries = Vec::new();
208
209 let corpus_paths = [
211 "indexed-content",
212 "benchmark-corpus",
213 "src",
214 "rust-core/src"
215 ];
216
217 let corpus_path = corpus_paths.iter()
218 .find(|path| std::path::Path::new(path).exists())
219 .map(|&s| s.to_string());
220
221 if corpus_path.is_none() {
222 warn!("โ ๏ธ No corpus directory found for validation");
223 return Ok(ValidationResult {
224 is_consistent: false,
225 valid_queries: 0,
226 total_queries: dataset.queries.len(),
227 inconsistent_queries,
228 validated_at: Utc::now(),
229 });
230 }
231
232 let corpus_path = corpus_path.unwrap();
233 debug!("๐ Using corpus path: {}", corpus_path);
234
235 for query in &dataset.queries {
237 let mut query_valid = true;
238 let mut missing_files = Vec::new();
239
240 for expected_file in &query.expected_files {
242 let file_paths_to_check = vec![
244 PathBuf::from(&corpus_path).join(expected_file),
245 PathBuf::from(expected_file),
246 PathBuf::from(&corpus_path).join(
247 expected_file.strip_prefix("indexed-content/").unwrap_or(expected_file)
248 ),
249 ];
250
251 let file_exists = file_paths_to_check.iter().any(|path| path.exists());
252
253 if !file_exists {
254 query_valid = false;
255 missing_files.push(expected_file.clone());
256 }
257 }
258
259 if query_valid {
260 valid_queries += 1;
261 } else {
262 inconsistent_queries.push(InconsistentQuery {
263 query: query.clone(),
264 failure_reason: "Expected files not found in corpus".to_string(),
265 missing_files,
266 });
267 }
268 }
269
270 let is_consistent = valid_queries == dataset.queries.len();
271 let consistency_rate = valid_queries as f64 / dataset.queries.len() as f64 * 100.0;
272
273 if is_consistent {
274 info!("โ
Perfect corpus consistency: {}/{} queries (100%)",
275 valid_queries, dataset.queries.len());
276 } else {
277 warn!("โ ๏ธ Partial corpus consistency: {}/{} queries ({:.1}%)",
278 valid_queries, dataset.queries.len(), consistency_rate);
279 }
280
281 Ok(ValidationResult {
282 is_consistent,
283 valid_queries,
284 total_queries: dataset.queries.len(),
285 inconsistent_queries,
286 validated_at: Utc::now(),
287 })
288 }
289
290 async fn discover_available_versions(&self) -> Result<()> {
292 debug!("๐ Discovering available dataset versions");
293
294 let dataset_dir = Path::new(&self.config.dataset_path);
295 if !dataset_dir.exists() {
296 warn!("Dataset directory does not exist: {}", dataset_dir.display());
297 return Ok(());
298 }
299
300 let mut entries = fs::read_dir(dataset_dir).await?;
301 let mut versions = Vec::new();
302
303 while let Some(entry) = entries.next_entry().await? {
304 let path = entry.path();
305 let filename = path.file_name()
306 .and_then(|n| n.to_str())
307 .unwrap_or("");
308
309 if filename.starts_with("golden-pinned-") && filename.ends_with(".json") {
311 let version = filename
312 .strip_prefix("golden-pinned-")
313 .and_then(|s| s.strip_suffix(".json"))
314 .unwrap_or("unknown")
315 .to_string();
316
317 if version != "current" { let metadata = entry.metadata().await?;
319 let created_at = metadata.created().ok()
320 .and_then(|t| DateTime::from_timestamp(
321 t.duration_since(std::time::UNIX_EPOCH).ok()?.as_secs() as i64, 0
322 ))
323 .unwrap_or_else(|| Utc::now());
324
325 let query_count = match self.load_dataset_from_file(&path).await {
327 Ok(dataset) => dataset.queries.len(),
328 Err(_) => 0,
329 };
330
331 versions.push(DatasetVersion {
332 version: version.clone(),
333 created_at,
334 query_count,
335 file_path: path,
336 is_current: version == self.config.default_version.as_deref().unwrap_or(""),
337 });
338 }
339 }
340 }
341
342 versions.sort_by(|a, b| b.created_at.cmp(&a.created_at));
344
345 let count = versions.len();
346 *self.available_versions.write().await = versions;
347
348 info!("๐ Discovered {} dataset versions", count);
349 Ok(())
350 }
351
352 pub async fn list_available_versions(&self) -> Result<Vec<DatasetVersion>> {
354 Ok(self.available_versions.read().await.clone())
355 }
356
357 async fn cache_dataset(&self, version: &str, dataset: &PinnedDataset) {
359 if !self.config.enable_caching {
360 return;
361 }
362
363 let mut cache = self.dataset_cache.write().await;
364
365 if cache.len() >= self.config.max_cache_size {
367 if let Some(oldest_key) = cache.keys().next().cloned() {
369 cache.remove(&oldest_key);
370 }
371 }
372
373 cache.insert(version.to_string(), Arc::new(dataset.clone()));
374 debug!("๐พ Cached dataset version: {}", version);
375 }
376
377 fn create_mock_dataset(&self) -> PinnedDataset {
379 let mock_queries = vec![
380 GoldenQuery {
381 query: "function".to_string(),
382 expected_files: vec!["src/main.rs".to_string()],
383 query_type: super::types::QueryType::Identifier,
384 metadata: HashMap::new(),
385 language: Some("rust".to_string()),
386 confidence: Some(0.8),
387 },
388 GoldenQuery {
389 query: "struct SearchEngine".to_string(),
390 expected_files: vec!["src/search.rs".to_string()],
391 query_type: super::types::QueryType::Identifier,
392 metadata: HashMap::new(),
393 language: Some("rust".to_string()),
394 confidence: Some(0.9),
395 },
396 ];
397
398 let mut query_distribution = HashMap::new();
399 query_distribution.insert(super::types::QueryType::Identifier, mock_queries.len());
400
401 let metadata = DatasetMetadata {
402 version: "mock-dev".to_string(),
403 name: "mock-development-dataset".to_string(),
404 created_at: Utc::now(),
405 total_queries: mock_queries.len(),
406 query_distribution,
407 languages: vec!["rust".to_string()],
408 git_sha: None,
409 additional_metadata: HashMap::new(),
410 };
411
412 let mut slices = HashMap::new();
413 slices.insert("ALL".to_string(), vec![0, 1]);
414 slices.insert("SMOKE_DEFAULT".to_string(), vec![0, 1]);
415
416 PinnedDataset {
417 metadata,
418 queries: mock_queries,
419 slices,
420 corpus_info: None,
421 }
422 }
423
424 pub fn get_dataset_slice(&self, dataset: &PinnedDataset, slice_name: &str) -> Option<Vec<GoldenQuery>> {
426 if let Some(indices) = dataset.slices.get(slice_name) {
427 let slice_queries = indices.iter()
428 .filter_map(|&idx| dataset.queries.get(idx).cloned())
429 .collect();
430 Some(slice_queries)
431 } else {
432 None
433 }
434 }
435
436 pub fn get_smoke_dataset(&self, dataset: &PinnedDataset) -> Vec<GoldenQuery> {
438 self.get_dataset_slice(dataset, "SMOKE_DEFAULT")
439 .unwrap_or_else(|| {
440 let smoke_size = std::cmp::min(40, dataset.queries.len());
442 dataset.queries.iter().take(smoke_size).cloned().collect()
443 })
444 }
445
446 pub async fn clear_cache(&self) {
448 let mut cache = self.dataset_cache.write().await;
449 cache.clear();
450 info!("๐งน Cleared dataset cache");
451 }
452}
453
454#[cfg(test)]
455mod tests {
456 use super::*;
457 use tempfile::TempDir;
458 use tokio::fs::File;
459 use tokio::io::AsyncWriteExt;
460
461 async fn create_test_dataset_file(dir: &Path, filename: &str, queries: Vec<GoldenQuery>) -> Result<PathBuf> {
462 let file_path = dir.join(filename);
463 let mut file = File::create(&file_path).await?;
464
465 let version = filename
467 .strip_prefix("golden-pinned-")
468 .and_then(|s| s.strip_suffix(".json"))
469 .unwrap_or("test");
470
471 let dataset = PinnedDataset {
472 metadata: DatasetMetadata {
473 version: version.to_string(),
474 name: "test-dataset".to_string(),
475 created_at: Utc::now(),
476 total_queries: queries.len(),
477 query_distribution: HashMap::new(),
478 languages: vec!["rust".to_string()],
479 git_sha: None,
480 additional_metadata: HashMap::new(),
481 },
482 queries,
483 slices: HashMap::new(),
484 corpus_info: None,
485 };
486
487 let json = serde_json::to_string_pretty(&dataset)?;
488 file.write_all(json.as_bytes()).await?;
489
490 Ok(file_path)
491 }
492
493 #[tokio::test]
494 async fn test_pinned_loader_creation() {
495 let temp_dir = TempDir::new().unwrap();
496 let config = BenchmarkConfig {
497 dataset_path: temp_dir.path().to_string_lossy().to_string(),
498 auto_discover_datasets: false,
499 ..Default::default()
500 };
501
502 let loader = PinnedDatasetLoader::with_config(config).await.unwrap();
503 assert!(!loader.config.dataset_path.is_empty());
504 }
505
506 #[tokio::test]
507 async fn test_mock_dataset_creation() {
508 let temp_dir = TempDir::new().unwrap();
509 let config = BenchmarkConfig {
510 dataset_path: temp_dir.path().to_string_lossy().to_string(),
511 auto_discover_datasets: false,
512 default_version: None, ..Default::default()
514 };
515
516 let loader = PinnedDatasetLoader::with_config(config).await.unwrap();
517 let dataset = loader.load_current_pinned_dataset().await.unwrap();
518
519 assert!(!dataset.queries.is_empty());
520 assert_eq!(dataset.metadata.version, "mock-dev");
521 }
522
523 #[tokio::test]
524 async fn test_dataset_loading_and_caching() {
525 let temp_dir = TempDir::new().unwrap();
526 let config = BenchmarkConfig {
527 dataset_path: temp_dir.path().to_string_lossy().to_string(),
528 enable_caching: true,
529 auto_discover_datasets: false,
530 default_version: None, ..Default::default()
532 };
533
534 let test_queries = vec![
536 GoldenQuery {
537 query: "test query".to_string(),
538 expected_files: vec!["test.rs".to_string()],
539 query_type: super::super::types::QueryType::Identifier,
540 metadata: HashMap::new(),
541 language: None,
542 confidence: None,
543 }
544 ];
545
546 let _file_path = create_test_dataset_file(
547 temp_dir.path(),
548 "golden-pinned-test-version.json",
549 test_queries
550 ).await.unwrap();
551
552 let loader = PinnedDatasetLoader::with_config(config).await.unwrap();
553
554 let dataset1 = loader.load_pinned_dataset_version("test-version").await.unwrap();
556 let dataset2 = loader.load_pinned_dataset_version("test-version").await.unwrap();
557
558 assert_eq!(dataset1.queries.len(), dataset2.queries.len());
559 assert_eq!(dataset1.metadata.version, "test-version");
560 }
561}