huskies: merge 978
This commit is contained in:
@@ -85,10 +85,14 @@ pub trait LanguageAdapter {
|
||||
/// Reads the existing map, updates only the entries for the provided files, and
|
||||
/// writes back. Entries for files not in `passing_files` are preserved unchanged.
|
||||
/// Running twice with the same input produces identical file content (idempotent).
|
||||
///
|
||||
/// When `root` is `Some`, keys are written as paths relative to `root` so the
|
||||
/// map stays portable across machines and worktree locations.
|
||||
fn update_source_map(
|
||||
&self,
|
||||
passing_files: &[&Path],
|
||||
source_map_path: &Path,
|
||||
root: Option<&Path>,
|
||||
) -> Result<(), String>;
|
||||
}
|
||||
|
||||
@@ -228,7 +232,14 @@ pub fn check_files(files: &[&Path]) -> CheckResult {
|
||||
///
|
||||
/// Dispatches each file to the appropriate [`LanguageAdapter`] based on extension.
|
||||
/// Files with unsupported extensions are silently skipped.
|
||||
pub fn update_source_map(passing_files: &[&Path], source_map_path: &Path) -> Result<(), String> {
|
||||
///
|
||||
/// When `root` is `Some`, keys in the map are written relative to `root` so the
|
||||
/// map stays portable across machines and worktree locations.
|
||||
pub fn update_source_map(
|
||||
passing_files: &[&Path],
|
||||
source_map_path: &Path,
|
||||
root: Option<&Path>,
|
||||
) -> Result<(), String> {
|
||||
let mut by_ext: HashMap<String, Vec<&Path>> = HashMap::new();
|
||||
for &file in passing_files {
|
||||
if let Some(ext) = file.extension().and_then(|e| e.to_str()) {
|
||||
@@ -237,7 +248,7 @@ pub fn update_source_map(passing_files: &[&Path], source_map_path: &Path) -> Res
|
||||
}
|
||||
for (ext, ext_files) in &by_ext {
|
||||
if let Some(adapter) = adapter_for_ext(ext) {
|
||||
adapter.update_source_map(ext_files, source_map_path)?;
|
||||
adapter.update_source_map(ext_files, source_map_path, root)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
@@ -295,7 +306,20 @@ pub fn update_for_worktree(
|
||||
std::fs::create_dir_all(parent).map_err(|e| format!("create_dir_all: {e}"))?;
|
||||
}
|
||||
|
||||
update_source_map(&passing, source_map_path)
|
||||
update_source_map(&passing, source_map_path, Some(worktree_path))
|
||||
}
|
||||
|
||||
/// Compute the map key for a file, stripping `root` when present.
|
||||
///
|
||||
/// Returns a root-relative path string when `root` is `Some` and the file is
|
||||
/// under that root; falls back to the file's own path string otherwise.
|
||||
pub(crate) fn relative_key(file: &Path, root: Option<&Path>) -> String {
|
||||
if let Some(r) = root
|
||||
&& let Ok(rel) = file.strip_prefix(r)
|
||||
{
|
||||
return rel.to_string_lossy().to_string();
|
||||
}
|
||||
file.to_string_lossy().to_string()
|
||||
}
|
||||
|
||||
/// Read the existing source map from `path` as a JSON object.
|
||||
@@ -444,10 +468,10 @@ mod tests {
|
||||
let map_path = tmp.path().join("source-map.json");
|
||||
let files: &[&Path] = &[&rs_path];
|
||||
|
||||
update_source_map(files, &map_path).unwrap();
|
||||
update_source_map(files, &map_path, None).unwrap();
|
||||
let first = std::fs::read_to_string(&map_path).unwrap();
|
||||
|
||||
update_source_map(files, &map_path).unwrap();
|
||||
update_source_map(files, &map_path, None).unwrap();
|
||||
let second = std::fs::read_to_string(&map_path).unwrap();
|
||||
|
||||
assert_eq!(first, second, "update_source_map must be idempotent");
|
||||
@@ -468,7 +492,7 @@ mod tests {
|
||||
"new.rs",
|
||||
"//! Module doc.\n\n/// A function.\npub fn bar() {}\n",
|
||||
);
|
||||
update_source_map(&[&rs_path], &map_path).unwrap();
|
||||
update_source_map(&[&rs_path], &map_path, None).unwrap();
|
||||
|
||||
let content = std::fs::read_to_string(&map_path).unwrap();
|
||||
assert!(
|
||||
@@ -736,4 +760,72 @@ mod tests {
|
||||
"map must list the documented function"
|
||||
);
|
||||
}
|
||||
|
||||
/// AC2/AC3: keys written by `update_for_worktree` are project-root-relative,
|
||||
/// not absolute paths into the worktree directory.
|
||||
#[test]
|
||||
fn update_for_worktree_writes_relative_keys() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
init_git_repo(tmp.path());
|
||||
|
||||
write_rs(
|
||||
tmp.path(),
|
||||
"lib.rs",
|
||||
"//! Module doc.\n\n/// A function.\npub fn greet() {}\n",
|
||||
);
|
||||
Command::new("git")
|
||||
.args(["add", "lib.rs"])
|
||||
.current_dir(tmp.path())
|
||||
.output()
|
||||
.expect("git add");
|
||||
Command::new("git")
|
||||
.args(["commit", "-m", "add lib.rs"])
|
||||
.current_dir(tmp.path())
|
||||
.output()
|
||||
.expect("git commit");
|
||||
|
||||
let huskies_dir = tmp.path().join(".huskies");
|
||||
std::fs::create_dir_all(&huskies_dir).unwrap();
|
||||
let map_path = huskies_dir.join("source-map.json");
|
||||
|
||||
update_for_worktree(tmp.path(), "HEAD~1", &map_path).unwrap();
|
||||
|
||||
let content = std::fs::read_to_string(&map_path).unwrap();
|
||||
let map: serde_json::Value = serde_json::from_str(&content).unwrap();
|
||||
let obj = map.as_object().unwrap();
|
||||
|
||||
// Every key must be relative — no absolute path prefix.
|
||||
for key in obj.keys() {
|
||||
assert!(
|
||||
!key.starts_with('/'),
|
||||
"key must be relative, got absolute path: {key}"
|
||||
);
|
||||
assert!(
|
||||
!key.contains("/.huskies/worktrees/"),
|
||||
"key must not contain worktree path infix: {key}"
|
||||
);
|
||||
}
|
||||
|
||||
// The key for lib.rs must be exactly "lib.rs".
|
||||
assert!(
|
||||
obj.contains_key("lib.rs"),
|
||||
"expected key 'lib.rs', got keys: {:?}",
|
||||
obj.keys().collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
|
||||
/// `relative_key` strips the root prefix from an absolute path.
|
||||
#[test]
|
||||
fn relative_key_strips_root_prefix() {
|
||||
let root = Path::new("/workspace/.huskies/worktrees/978");
|
||||
let file = Path::new("/workspace/.huskies/worktrees/978/server/src/foo.rs");
|
||||
assert_eq!(relative_key(file, Some(root)), "server/src/foo.rs");
|
||||
}
|
||||
|
||||
/// `relative_key` falls back to the full path when root is `None`.
|
||||
#[test]
|
||||
fn relative_key_none_root_returns_full_path() {
|
||||
let file = Path::new("/absolute/path/foo.rs");
|
||||
assert_eq!(relative_key(file, None), "/absolute/path/foo.rs");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
|
||||
use crate::{CheckFailure, CheckResult, LanguageAdapter};
|
||||
use crate::{CheckFailure, CheckResult, LanguageAdapter, relative_key};
|
||||
|
||||
/// Rust documentation coverage adapter.
|
||||
pub struct RustAdapter;
|
||||
@@ -79,10 +79,11 @@ impl LanguageAdapter for RustAdapter {
|
||||
&self,
|
||||
passing_files: &[&Path],
|
||||
source_map_path: &Path,
|
||||
root: Option<&Path>,
|
||||
) -> Result<(), String> {
|
||||
let mut map = crate::read_map(source_map_path)?;
|
||||
for &file in passing_files {
|
||||
let key = file.to_string_lossy().to_string();
|
||||
let key = relative_key(file, root);
|
||||
let items: Vec<serde_json::Value> = Self::extract_items(file)
|
||||
.into_iter()
|
||||
.map(serde_json::Value::String)
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
|
||||
use crate::{CheckFailure, CheckResult, LanguageAdapter};
|
||||
use crate::{CheckFailure, CheckResult, LanguageAdapter, relative_key};
|
||||
|
||||
/// TypeScript documentation coverage adapter.
|
||||
pub struct TypeScriptAdapter;
|
||||
@@ -80,10 +80,11 @@ impl LanguageAdapter for TypeScriptAdapter {
|
||||
&self,
|
||||
passing_files: &[&Path],
|
||||
source_map_path: &Path,
|
||||
root: Option<&Path>,
|
||||
) -> Result<(), String> {
|
||||
let mut map = crate::read_map(source_map_path)?;
|
||||
for &file in passing_files {
|
||||
let key = file.to_string_lossy().to_string();
|
||||
let key = relative_key(file, root);
|
||||
let items: Vec<serde_json::Value> = Self::extract_items(file)
|
||||
.into_iter()
|
||||
.map(serde_json::Value::String)
|
||||
|
||||
Reference in New Issue
Block a user