use std::collections::BTreeSet;
use std::path::{Path, PathBuf};
use crate::persist::local::models::catalogued_test::{CataloguedTest, TestLanguage};
use crate::persist::local::repos::catalogue_repo::CatalogueRepo;
use crate::catalogue::{scan_tree, ProjectType, TestInfo};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RepoScan {
pub repo: String,
pub found: usize,
pub numbered: usize,
}
pub fn repositories(root: &Path) -> Vec<PathBuf> {
let mut out = vec![root.to_path_buf()];
let mut queue = vec![root.to_path_buf()];
while let Some(current) = queue.pop() {
let Ok(modules) = crate::fleet::config::GitModules::parse(¤t) else {
continue;
};
for submodule in modules.submodules {
let path = current.join(&submodule.path);
if !path.is_dir() {
continue;
}
out.push(path.clone());
queue.push(path);
}
}
out.sort();
out.dedup();
out
}
fn nested_within(parent: &Path, all: &[PathBuf]) -> BTreeSet<PathBuf> {
all.iter()
.filter(|path| path.as_path() != parent)
.filter_map(|path| path.strip_prefix(parent).ok())
.map(Path::to_path_buf)
.collect()
}
fn language_for(info: &TestInfo, language: ProjectType) -> TestLanguage {
match language {
ProjectType::Rust => TestLanguage::Rust,
ProjectType::Go => TestLanguage::Go,
ProjectType::Python => TestLanguage::Python,
ProjectType::Js => TestLanguage::Js,
ProjectType::Shell => TestLanguage::Shell,
ProjectType::Swift => {
if info.file_path.extension().is_some_and(|e| e == "m") {
TestLanguage::Objc
} else {
TestLanguage::Swift
}
}
}
}
fn rows_for(repo_root: &Path, repo: &str, found: Vec<TestInfo>) -> Result<Vec<CataloguedTest>, String> {
let mut out = Vec::with_capacity(found.len());
for info in found {
let relative = info.file_path.strip_prefix(repo_root).map_err(|_| {
format!(
"{} is outside the repository {} it was found in",
info.file_path.display(),
repo_root.display()
)
})?;
let language = crate::catalogue::language_of_path(&info.file_path).ok_or_else(
|| {
format!(
"{} produced a test but names no language",
info.file_path.display()
)
},
)?;
let number = match &info.number {
Some(text) => Some(text.parse::<i64>().map_err(|e| {
format!("{} claims a number that is not one: {text} ({e})", info.function_name)
})?),
None => None,
};
out.push(CataloguedTest {
id: uuid::Uuid::new_v4().to_string(),
repo: repo.to_string(),
language: language_for(&info, language),
number,
function_name: info.function_name,
description: info.description,
file_path: relative.display().to_string(),
line_number: info.line_number as i64,
feature_id: None,
});
}
Ok(out)
}
pub fn scan_workspace(root: &Path, repo: &CatalogueRepo) -> Result<Vec<RepoScan>, String> {
let all = repositories(root);
let mut report = Vec::new();
for repo_root in &all {
let relative = repo_root
.strip_prefix(root)
.map(|p| p.display().to_string())
.unwrap_or_default();
let nested = nested_within(repo_root, &all);
let mut found = scan_tree(repo_root)?;
found.retain(|test| {
let Ok(rel) = test.file_path.strip_prefix(repo_root) else {
return true;
};
!nested.iter().any(|sub| rel.starts_with(sub))
});
let numbered = found.iter().filter(|t| t.number.is_some()).count();
let total = found.len();
let rows = rows_for(repo_root, &relative, found)?;
repo.replace_repo(&relative, &rows)
.map_err(|e| format!("could not record {}'s tests: {e}", display_repo(&relative)))?;
report.push(RepoScan {
repo: relative,
found: total,
numbered,
});
}
let scanned: Vec<String> = report.iter().map(|scan| scan.repo.clone()).collect();
repo.retain_repos(&scanned)
.map_err(|e| format!("could not forget repositories that have moved: {e}"))?;
Ok(report)
}
pub fn display_repo(repo: &str) -> &str {
if repo.is_empty() {
"(superproject)"
} else {
repo
}
}