Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[TRUNK-12978] v1 validate command #129

Merged
merged 9 commits into from
Oct 17, 2024

Conversation

max-trunk
Copy link
Contributor

@max-trunk max-trunk commented Oct 15, 2024

TRUNK-12978

Adds a first iteration of a validate command, intended to allow users onboarding to flaky tests to verify they're producing valid JUnit xml files that we can accurately process, before they configure their CI jobs to start uploading to us.

Users pass a list of --junit-paths to validate. For each file found, validate will parse and validate using the parser and validator in this repo's context crate.

If any fatal parsing errors are encountered, validate will exit with a non-zero exit code.

If a file contains INVALID-level validation errors, validate considers this file to be 'invalid.' If a file contains no INVALID-level validation errors, and zero or more SUBOPTIMAL-level validation errors, validate considers this file to be 'valid.' If any 'invalid' files are encountered, validate exits with a non-zero exit code. If all files found are 'valid,' validate exits successfully and prints (eventually) a link to return to the onboarding flow of flaky tests.

Example invocation where fatal parsing error is encountered:

Screenshot 2024-10-17 at 10 30 15 AM

Example invocation where some files are invalid:

Screenshot 2024-10-17 at 10 29 23 AM

Example invocation where all files are valid, some with SUBOPTIMAL validation errors:

Screenshot 2024-10-17 at 10 29 47 AM

Example invocation where all files are valid, all with no SUBOPTIMAL validation errors:

Screenshot 2024-10-17 at 10 29 10 AM

Follow-ups:

  • show line numbers of where validation errors occur
  • add some more context to validation errors to better describe impact on flaky tests processing to users
  • use actual URL for flaky tests onboarding when that is known

Copy link

trunk-staging-io bot commented Oct 16, 2024

406 tests were run on cb493aaa. ✅ 406 Passed. View Full Report ↗︎

settings

cli/src/main.rs Outdated
Comment on lines 33 to 35

use colored::{ColoredString, Colorize};

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: can we group this with the rest of the imports for organization?

cli/src/main.rs Outdated
Comment on lines 546 to 551
log::info!(
"Starting trunk-analytics-cli {} (git={}) rustc={}",
env!("CARGO_PKG_VERSION"),
env!("VERGEN_GIT_SHA"),
env!("VERGEN_RUSTC_SEMVER")
);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we make a function that does this and reuse it across commands?

cli/src/main.rs Outdated
Comment on lines 570 to 576
" File set ({:?}): {}",
file_set.file_set_type,
file_set.glob
);
for file in &file_set.files {
log::info!(" {}", file.original_path_rel);
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit, optional: may want to use \t tab character for spacing

Comment on lines 72 to 81
let original_path = path
let original_path_abs = path
.to_str()
.expect("failed to convert path to string")
.to_string();
let original_path_rel = path
.strip_prefix(repo_root)
.unwrap_or(&path)
.to_str()
.expect("failed to convert path to string")
.to_string();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know you're following the already existing pattern here, but I think we need to update this so it doesn't panic here. Instead of using expect we can use map_err + ? (try sigil)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! went with ok_or_else since I was working with an Option rather than a Result

cli/src/types.rs Outdated
Comment on lines 128 to 132
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct WithFilePath<T> {
pub file_path: String,
pub wrapped: T,
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you probably don't need a named struct, but rather just a simple tuple:
https://doc.rust-lang.org/rust-by-example/primitives/tuples.html

cli/src/main.rs Outdated
Comment on lines 579 to 606
let mut reports: Vec<WithFilePath<Report>> = Vec::new();
let mut parse_errors: Vec<WithFilePath<JunitParseError>> = Vec::new();
file_sets.iter().try_for_each(|file_set| {
file_set.files.iter().try_for_each(|bundled_file| {
let path = std::path::Path::new(&bundled_file.original_path);
let file = std::fs::File::open(path)?;
let file_buf_reader = BufReader::new(file);
let mut junit_parser = JunitParser::new();
junit_parser.parse(file_buf_reader).context(format!(
"Encountered unrecoverable error while parsing file: {}",
bundled_file.original_path_rel
))?;
parse_errors.extend(junit_parser.errors().iter().map(|e| WithFilePath::<
JunitParseError,
> {
file_path: bundled_file.original_path_rel.clone(),
wrapped: *e,
}));
reports.extend(junit_parser.into_reports().iter().map(
|report| WithFilePath::<Report> {
file_path: bundled_file.original_path_rel.clone(),
wrapped: report.clone(),
},
));
Ok::<(), anyhow::Error>(())
})?;
Ok::<(), anyhow::Error>(())
})?;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: this is more idiomatically a fold operation. for_each is typically for side-effects

cli/src/main.rs Outdated
@@ -511,10 +537,222 @@ async fn run_test(test_args: TestArgs) -> anyhow::Result<i32> {
Ok(exit_code)
}

async fn run_validate(validate_args: ValidateArgs) -> anyhow::Result<i32> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file is getting unwieldy in length. Can we split out a file for this command and we'll split out the other commands into files later?

Also, I'd love for you to split out functions for each logical part of the command that you run so that it's easy to understand the flow

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Totally

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alrighty, lmk if what I've got now is a bit easier on the eyes

Copy link
Collaborator

@TylerJang27 TylerJang27 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

) -> (usize, usize) {
log::info!("");
let mut num_invalid_reports: usize = 0;
let mut num_optionally_invalid_reports: usize = 0;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: rename to suboptimal for consistency with the underlying types/enums
(and rename elsewhere)


println!("{assert}");
}

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: would also be nice to have a test for suboptimal junits

cli/src/main.rs Outdated
async fn run(cli: Cli) -> anyhow::Result<i32> {
match cli.command {
Commands::Upload(upload_args) => run_upload(upload_args, None, None, None, None).await,
Commands::Test(test_args) => run_test(test_args).await,
Commands::Validate(validate_args) => run_validate(validate_args).await,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Totally optional, but we can get rid of the run_validate indirection here:

Suggested change
Commands::Validate(validate_args) => run_validate(validate_args).await,
Commands::Validate(validate_args) => {
let ValidateArgs {
junit_paths,
show_warnings,
} = validate_args;
print_cli_start_info();
validate(junit_paths, show_warnings).await
},

IMO, this gets us closer to splitting concerns in a way that prevents repeated code and inconsistencies

Comment on lines 57 to 58
Vec::<(Report, String)>::new(), // Vec<(Report, file path)>
Vec::<(JunitParseError, String)>::new(), // Vec<(JunitParseError, file path)>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Accidental comments?


fn parse_file_sets(
file_sets: Vec<FileSet>,
) -> anyhow::Result<(Vec<(Report, String)>, Vec<(JunitParseError, String)>)> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can create a type alias for these more complex types and compose them together nicely for readability
https://doc.rust-lang.org/reference/items/type-aliases.html

For example:

type ParsedReportAndFilePaths = Vec<(Report, String)>;
type JunitParseErrorsAndFilePaths = Vec<(JunitParseError, String)>;

which would update your return type to:

anyhow::Result<(ParsedReportAndFilePaths, JunitParseErrorsAndFilePaths)>

Btw, it occurs to me that we could use a BTreeMap here though, no? Something like

type JunitFileToReportAndErrors = BTreeMap<String, (Vec<Report>, Vec<JunitParseError>)>;

Comment on lines 94 to 96
Ok::<(Vec<(Report, String)>, Vec<(JunitParseError, String)>), anyhow::Error>(
file_sets_parse_results,
)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you set a return type of the closure you won't have to annotate the type of Result here:

e.g.

        |mut file_sets_parse_results, file_set| -> anyhow::Result<(Vec<(Report, String)>, Vec<(JunitParseError, String)>)> {
            // ...
            Ok(file_sets_parse_results)
        }

Comment on lines 101 to 133
fn print_matched_files(file_sets: &[FileSet], file_counter: FileSetCounter) {
log::info!("");
log::info!(
"Validating the following {} files:",
file_counter.get_count()
);
for file_set in file_sets {
log::info!(" File set matching {}:", file_set.glob);
for file in &file_set.files {
log::info!("\t{}", file.original_path_rel);
}
}
}

fn print_parse_errors(parse_errors: Vec<(JunitParseError, String)>) {
log::info!("");
log::warn!(
"Encountered the following {} non-fatal errors while parsing files:",
parse_errors.len().to_string().yellow()
);

let mut current_file_original_path = parse_errors[0].1.clone();
log::warn!(" File: {}", current_file_original_path);

for error in parse_errors {
if error.1 != current_file_original_path {
current_file_original_path = error.1;
log::warn!(" File: {}", current_file_original_path);
}

log::warn!("\t{}", error.0);
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is so much nicer! Thank you 🙏

Comment on lines +102 to +103
log::info!("");
log::info!(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider using \n for making newlines

@max-trunk max-trunk merged commit 2fea36d into main Oct 17, 2024
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.

3 participants