diff --git a/Cargo.lock b/Cargo.lock index 92ef2d0..9a0539d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -170,6 +170,7 @@ dependencies = [ "error-stack", "glob-match", "ignore", + "indoc", "itertools", "path-clean", "predicates", @@ -387,6 +388,12 @@ dependencies = [ "hashbrown 0.15.1", ] +[[package]] +name = "indoc" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b248f5224d1d606005e02c97f5aa4e88eeb230488bcc03bc9ca4d7991399f2b5" + [[package]] name = "is_terminal_polyfill" version = "1.70.1" diff --git a/Cargo.toml b/Cargo.toml index 1f57b29..3260220 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,3 +28,4 @@ assert_cmd = "2.0.16" rusty-hook = "^0.11.2" predicates = "3.1.2" pretty_assertions = "1.4.1" # Shows a more readable diff when comparing objects +indoc = "2.0.5" diff --git a/src/common_test.rs b/src/common_test.rs index fa45906..ade1cc6 100644 --- a/src/common_test.rs +++ b/src/common_test.rs @@ -6,6 +6,8 @@ pub mod tests { path::PathBuf, }; + use indoc::indoc; + use tempfile::tempdir; use crate::{ownership::Ownership, project::Project}; @@ -21,18 +23,18 @@ pub mod tests { }}; } - const DEFAULT_CODE_OWNERSHIP_YML: &str = " ---- -owned_globs: - - \"{app,components,config,frontend,lib,packs,spec}/**/*.{rb,rake,js,jsx,ts,tsx,json,yml}\" -unowned_globs: - - config/code_ownership.yml -javascript_package_paths: - - javascript/packages/** -vendored_gems_path: gems -team_file_glob: - - config/teams/**/*.yml - "; + const DEFAULT_CODE_OWNERSHIP_YML: &str = indoc! {" + --- + owned_globs: + - \"{app,components,config,frontend,lib,packs,spec}/**/*.{rb,rake,js,jsx,ts,tsx,json,yml}\" + unowned_globs: + - config/code_ownership.yml + javascript_package_paths: + - javascript/packages/** + vendored_gems_path: gems + team_file_glob: + - config/teams/**/*.yml + "}; #[derive(Debug)] pub struct TestConfig { diff --git a/src/ownership.rs b/src/ownership.rs index c4d093a..b821d70 100644 --- a/src/ownership.rs +++ b/src/ownership.rs @@ -206,6 +206,7 @@ fn parse_for_team(team_name: String, codeowners_file: &str) -> Result Result<(), Box> { @@ -243,17 +244,17 @@ mod tests { #[test] fn test_parse_for_team_trims_header() -> Result<(), Box> { - let codeownership_file = r#" -# STOP! - DO NOT EDIT THIS FILE MANUALLY -# This file was automatically generated by "bin/codeownership validate". -# -# CODEOWNERS is used for GitHub to suggest code/file owners to various GitHub -# teams. This is useful when developers create Pull Requests since the -# code/file owner is notified. Reference GitHub docs for more details: -# https://help.github.com/en/articles/about-code-owners + let codeownership_file = indoc! {" + # STOP! - DO NOT EDIT THIS FILE MANUALLY + # This file was automatically generated by \"bin/codeownership validate\". + # + # CODEOWNERS is used for GitHub to suggest code/file owners to various GitHub + # teams. This is useful when developers create Pull Requests since the + # code/file owner is notified. Reference GitHub docs for more details: + # https://help.github.com/en/articles/about-code-owners -"#; + "}; let team_ownership = parse_for_team("@Bar".to_string(), codeownership_file)?; assert!(team_ownership.is_empty()); @@ -262,14 +263,14 @@ mod tests { #[test] fn test_parse_for_team_includes_owned_globs() -> Result<(), Box> { - let codeownership_file = r#" -# First Section -/path/to/owned @Foo -/path/to/not/owned @Bar + let codeownership_file = indoc! {" + # First Section + /path/to/owned @Foo + /path/to/not/owned @Bar -# Last Section -/another/owned/path @Foo -"#; + # Last Section + /another/owned/path @Foo + "}; let team_ownership = parse_for_team("@Foo".to_string(), codeownership_file)?; vecs_match( @@ -290,11 +291,11 @@ mod tests { #[test] fn test_parse_for_team_with_partial_team_match() -> Result<(), Box> { - let codeownership_file = r#" -# First Section -/path/to/owned @Foo -/path/to/not/owned @FooBar -"#; + let codeownership_file = indoc! {" + # First Section + /path/to/owned @Foo + /path/to/not/owned @FooBar + "}; let team_ownership = parse_for_team("@Foo".to_string(), codeownership_file)?; vecs_match( @@ -309,16 +310,16 @@ mod tests { #[test] fn test_parse_for_team_with_trailing_newlines() -> Result<(), Box> { - let codeownership_file = r#" -# First Section -/path/to/owned @Foo + let codeownership_file = indoc! {" + # First Section + /path/to/owned @Foo -# Last Section -/another/owned/path @Foo + # Last Section + /another/owned/path @Foo -"#; + "}; let team_ownership = parse_for_team("@Foo".to_string(), codeownership_file)?; vecs_match( @@ -339,9 +340,9 @@ mod tests { #[test] fn test_parse_for_team_without_trailing_newline() -> Result<(), Box> { - let codeownership_file = r#" -# First Section -/path/to/owned @Foo"#; + let codeownership_file = indoc! {" + # First Section + /path/to/owned @Foo"}; let team_ownership = parse_for_team("@Foo".to_string(), codeownership_file)?; vecs_match( @@ -356,12 +357,12 @@ mod tests { #[test] fn test_parse_for_team_with_missing_section_header() -> Result<(), Box> { - let codeownership_file = r#" -# First Section -/path/to/owned @Foo + let codeownership_file = indoc! {" + # First Section + /path/to/owned @Foo -/another/owned/path @Foo -"#; + /another/owned/path @Foo + "}; let team_ownership = parse_for_team("@Foo".to_string(), codeownership_file); assert!(team_ownership @@ -371,10 +372,10 @@ mod tests { #[test] fn test_parse_for_team_with_malformed_team_line() -> Result<(), Box> { - let codeownership_file = r#" -# First Section -@Foo -"#; + let codeownership_file = indoc! {" + # First Section + @Foo + "}; let team_ownership = parse_for_team("@Foo".to_string(), codeownership_file); assert!(team_ownership @@ -384,11 +385,11 @@ mod tests { #[test] fn test_parse_for_team_with_invalid_file() -> Result<(), Box> { - let codeownership_file = r#" -# First Section -# Second Section -path/to/owned @Foo -"#; + let codeownership_file = indoc! {" + # First Section + # Second Section + path/to/owned @Foo + "}; let team_ownership = parse_for_team("@Foo".to_string(), codeownership_file)?; vecs_match( &team_ownership, diff --git a/tests/invalid_project_test.rs b/tests/invalid_project_test.rs index 925ba7d..105eef6 100644 --- a/tests/invalid_project_test.rs +++ b/tests/invalid_project_test.rs @@ -1,4 +1,5 @@ use assert_cmd::prelude::*; +use indoc::indoc; use predicates::prelude::*; use std::{error::Error, process::Command}; @@ -10,31 +11,24 @@ fn test_validate() -> Result<(), Box> { .arg("validate") .assert() .failure() - .stdout(predicate::str::contains( - "CODEOWNERS out of date. Run `codeowners generate` to update the CODEOWNERS file", - )) - .stdout(predicate::str::contains( - "Some files are missing ownership\n- ruby/app/models/blockchain.rb\n- ruby/app/unowned.rb", - )) - .stdout(predicate::str::contains( - "Found invalid team annotations\n- ruby/app/models/blockchain.rb is referencing an invalid team - 'Web3'", - )) - .stdout(predicate::str::contains( - "Code ownership should only be defined for each file in one way. The following files have declared ownership in multiple ways", - )) - .stdout(predicate::str::contains( - "gems/payroll_calculator/calculator.rb (owner: Payments, source: Owner annotation at the top of the file)", - )) - .stdout(predicate::str::contains( - "gems/payroll_calculator/calculator.rb (owner: Payroll, source: Owner specified in Team YML's `owned_gems`)", - )) - .stdout(predicate::str::contains( - "ruby/app/services/multi_owned.rb (owner: Payments, source: Owner annotation at the top of the file)", - )) - .stdout(predicate::str::contains( - "ruby/app/services/multi_owned.rb (owner: Payroll, source: Owner specified in `ruby/app/services/.codeowner`", - )); + .stdout(predicate::eq(indoc! {" + CODEOWNERS out of date. Run `codeowners generate` to update the CODEOWNERS file + + Code ownership should only be defined for each file in one way. The following files have declared ownership in multiple ways + - gems/payroll_calculator/calculator.rb (owner: Payments, source: Owner annotation at the top of the file) + - gems/payroll_calculator/calculator.rb (owner: Payroll, source: Owner specified in Team YML's `owned_gems`) + - ruby/app/services/multi_owned.rb (owner: Payments, source: Owner annotation at the top of the file) + - ruby/app/services/multi_owned.rb (owner: Payroll, source: Owner specified in `ruby/app/services/.codeowner`) + + Found invalid team annotations + - ruby/app/models/blockchain.rb is referencing an invalid team - 'Web3' + + Some files are missing ownership + - ruby/app/models/blockchain.rb + - ruby/app/unowned.rb + + "})); Ok(()) } @@ -47,9 +41,10 @@ fn test_for_file() -> Result<(), Box> { .arg("ruby/app/models/blockchain.rb") .assert() .success() - .stdout(predicate::str::contains("Team: Unowned")) - .stdout(predicate::str::contains("Team YML: Unowned")); - + .stdout(predicate::eq(indoc! {" + Team: Unowned + Team YML: Unowned + Description: \n"})); // trailing whitespace Ok(()) } @@ -62,15 +57,17 @@ fn test_for_file_multiple_owners() -> Result<(), Box> { .arg("ruby/app/services/multi_owned.rb") .assert() .success() - .stdout(predicate::str::contains("Error: file is owned by multiple teams!")) - .stdout(predicate::str::contains("Team: Payments")) - .stdout(predicate::str::contains("Team YML: config/teams/payments.yml")) - .stdout(predicate::str::contains("Description: Owner annotation at the top of the file")) - .stdout(predicate::str::contains("Team: Payroll")) - .stdout(predicate::str::contains("Team YML: config/teams/payroll.yml")) - .stdout(predicate::str::contains( - "Description: Owner specified in `ruby/app/services/.codeowner`", - )); - + .stdout(predicate::str::starts_with("Error: file is owned by multiple teams!")) + // order not static + .stdout(predicate::str::contains(indoc! {" + Team: Payroll + Team YML: config/teams/payroll.yml + Description: Owner specified in `ruby/app/services/.codeowner` + "})) + .stdout(predicate::str::contains(indoc! {" + Team: Payments + Team YML: config/teams/payments.yml + Description: Owner annotation at the top of the file + "})); Ok(()) } diff --git a/tests/valid_project_test.rs b/tests/valid_project_test.rs index 61838e2..09c3b2e 100644 --- a/tests/valid_project_test.rs +++ b/tests/valid_project_test.rs @@ -1,4 +1,5 @@ use assert_cmd::prelude::*; +use indoc::indoc; use predicates::prelude::predicate; use std::{error::Error, path::Path, process::Command}; @@ -42,41 +43,16 @@ fn test_for_file() -> Result<(), Box> { .arg("ruby/app/models/payroll.rb") .assert() .success() - .stdout(predicate::str::contains("Team: Payroll")) - .stdout(predicate::str::contains("Team YML: config/teams/payroll.yml")); - + .stdout(predicate::eq(indoc! {" + Team: Payroll + Team YML: config/teams/payroll.yml + Description: Owner annotation at the top of the file + "})); Ok(()) } #[test] fn test_for_team() -> Result<(), Box> { - let expected_stdout = r#" -# Code Ownership Report for `Payroll` Team - -## Annotations at the top of file -/javascript/packages/PayrollFlow/index.tsx -/ruby/app/models/payroll.rb - -## Team-specific owned globs -This team owns nothing in this category. - -## Owner in .codeowner -/ruby/app/payroll/**/** - -## Owner metadata key in package.yml -/ruby/packages/payroll_flow/**/** - -## Owner metadata key in package.json -/javascript/packages/PayrollFlow/**/** - -## Team YML ownership -/config/teams/payroll.yml - -## Team owned gems -/gems/payroll_calculator/**/** -"# - .trim_start(); - Command::cargo_bin("codeowners")? .arg("--project-root") .arg("tests/fixtures/valid_project") @@ -84,8 +60,31 @@ This team owns nothing in this category. .arg("Payroll") .assert() .success() - .stdout(predicate::eq(expected_stdout)); + .stdout(predicate::eq(indoc! {" + # Code Ownership Report for `Payroll` Team + + ## Annotations at the top of file + /javascript/packages/PayrollFlow/index.tsx + /ruby/app/models/payroll.rb + + ## Team-specific owned globs + This team owns nothing in this category. + + ## Owner in .codeowner + /ruby/app/payroll/**/** + + ## Owner metadata key in package.yml + /ruby/packages/payroll_flow/**/** + + ## Owner metadata key in package.json + /javascript/packages/PayrollFlow/**/** + + ## Team YML ownership + /config/teams/payroll.yml + ## Team owned gems + /gems/payroll_calculator/**/** + "})); Ok(()) } @@ -98,7 +97,9 @@ fn test_for_missing_team() -> Result<(), Box> { .arg("Nope") .assert() .success() - .stdout(predicate::str::contains("Team not found")); + .stdout(predicate::eq(indoc! {" + Team not found + "})); Ok(()) }