|
18 | 18 | //! that, a test cannot tell "checked correctly" from "mishandled and spuriously reported", |
19 | 19 | //! which is exactly how an earlier draft of this file passed against unfixed code. |
20 | 20 | //! |
21 | | -//! One test covers the `**`-glob direction, since the symptom there is the opposite. |
| 21 | +//! One test covers the `**`-glob direction, since the symptom there is the opposite. Two |
| 22 | +//! more assert an *owned* file still passes under each odd spelling — without those, every |
| 23 | +//! assertion here would also hold if normalization mangled one path into some other unowned |
| 24 | +//! path. |
22 | 25 | //! |
23 | 26 | //! Path forms covered, against both states the project root can be in (resolved or not, |
24 | 27 | //! since `cli.rs` canonicalizes it but a library caller need not): |
|
27 | 30 | //! - absolute, root and path agreeing about symlinks |
28 | 31 | //! - absolute, root resolved and path not |
29 | 32 | //! - absolute, path resolved and root not (library callers only) |
| 33 | +//! - absolute, naming a symlinked *file* -- must check the symlink, not its target |
30 | 34 | //! - a deleted path, and a path outside the project -- both skipped, deliberately |
31 | 35 |
|
32 | 36 | use assert_cmd::prelude::*; |
@@ -55,6 +59,27 @@ fn fixture_with_an_unowned_file() -> TempDir { |
55 | 59 | temp_dir |
56 | 60 | } |
57 | 61 |
|
| 62 | +/// Assert `validate <spelling>` succeeds, i.e. the path reached the check *and* resolved to |
| 63 | +/// a file that really is owned. |
| 64 | +/// |
| 65 | +/// The counterpart to `assert_normalizes`: those tests would still pass if normalization |
| 66 | +/// mangled a path into some *other* unowned path, and these would not. |
| 67 | +fn assert_owned_file_passes(spelling: &str) -> Result<(), Box<dyn Error>> { |
| 68 | + let temp_dir = fixture_with_an_unowned_file(); |
| 69 | + |
| 70 | + Command::cargo_bin("codeowners")? |
| 71 | + .arg("--project-root") |
| 72 | + .arg(temp_dir.path()) |
| 73 | + .arg("--no-cache") |
| 74 | + .arg("validate") |
| 75 | + .arg(spelling) |
| 76 | + .assert() |
| 77 | + .success() |
| 78 | + .stdout(predicate::eq("")); |
| 79 | + |
| 80 | + Ok(()) |
| 81 | +} |
| 82 | + |
58 | 83 | /// Assert `validate <spelling>` reached the ownership check and reported the file under its |
59 | 84 | /// normalized name. |
60 | 85 | fn assert_normalizes(spelling_from: impl Fn(&std::path::Path) -> String) -> Result<(), Box<dyn Error>> { |
@@ -159,6 +184,64 @@ fn test_absolute_path_when_only_the_path_is_resolved() { |
159 | 184 | ); |
160 | 185 | } |
161 | 186 |
|
| 187 | +#[test] |
| 188 | +fn test_owned_file_passes_with_a_dot_slash_prefix() -> Result<(), Box<dyn Error>> { |
| 189 | + // A positive guard. Every assertion above is that an *unowned* file gets reported, which |
| 190 | + // would also hold if normalization mangled the path into some other unowned path. This |
| 191 | + // pins that a well-owned file still resolves to itself and passes. |
| 192 | + assert_owned_file_passes("./ruby/app/models/payroll.rb") |
| 193 | +} |
| 194 | + |
| 195 | +#[test] |
| 196 | +fn test_owned_file_passes_with_an_interior_parent_dir() -> Result<(), Box<dyn Error>> { |
| 197 | + assert_owned_file_passes("ruby/app/payments/../models/payroll.rb") |
| 198 | +} |
| 199 | + |
| 200 | +#[test] |
| 201 | +fn test_absolute_path_to_a_symlink_names_the_symlink_not_its_target() { |
| 202 | + // Regression guard. The retry used to canonicalize the whole supplied path, which |
| 203 | + // follows a symlinked *file*, so an absolute path naming a symlink was checked as its |
| 204 | + // target -- a different file than the caller asked about. The retry now resolves only |
| 205 | + // the parent and re-attaches the file name, fixing the ancestor `/var` -> |
| 206 | + // `/private/var` discrepancy without following the leaf. |
| 207 | + // |
| 208 | + // Both the symlink and its target are unowned here, and the assertion is on *which path |
| 209 | + // the report names* rather than on pass/fail. An earlier version pointed the symlink at |
| 210 | + // an owned file and asserted failure, which was fixture-coupled and wrong: reading |
| 211 | + // through a symlink sees the target's contents, so once ownership is resolved through |
| 212 | + // the mappers rather than by reading CODEOWNERS back, the symlink genuinely inherits the |
| 213 | + // target's `@team` annotation and is owned. Naming the path sidesteps that entirely. |
| 214 | + // |
| 215 | + // The symlink is created after `setup_fixture_repo` because that helper copies with |
| 216 | + // `fs::copy`, which would follow it and write a regular file instead. |
| 217 | + let temp_dir = fixture_with_an_unowned_file(); |
| 218 | + let project_root = temp_dir.path(); |
| 219 | + |
| 220 | + let link = project_root.join("ruby/app/link_to_unowned.rb"); |
| 221 | + std::os::unix::fs::symlink("unowned.rb", &link).expect("failed to create symlink"); |
| 222 | + git_add_all_files(project_root); |
| 223 | + |
| 224 | + // Deliberately NOT canonicalized, so the retry fires. |
| 225 | + let absolute = link.to_string_lossy().to_string(); |
| 226 | + |
| 227 | + let output = Command::cargo_bin("codeowners") |
| 228 | + .expect("binary") |
| 229 | + .arg("--project-root") |
| 230 | + .arg(project_root) |
| 231 | + .arg("--no-cache") |
| 232 | + .arg("validate") |
| 233 | + .arg(&absolute) |
| 234 | + .output() |
| 235 | + .expect("run"); |
| 236 | + let stdout = String::from_utf8_lossy(&output.stdout); |
| 237 | + |
| 238 | + assert!( |
| 239 | + stdout.contains("link_to_unowned.rb"), |
| 240 | + "the report names the symlink's target instead of the symlink the caller asked \ |
| 241 | + about, so the retry followed the leaf.\nstdout={stdout}" |
| 242 | + ); |
| 243 | +} |
| 244 | + |
162 | 245 | #[test] |
163 | 246 | fn test_owned_file_is_not_spuriously_reported_under_star_star_globs() -> Result<(), Box<dyn Error>> { |
164 | 247 | // The other failure mode. `invalid_project`'s `owned_globs` are `**`-leading, so a |
|
0 commit comments