diff --git a/icann-rdap-cli/src/bin/rdap-test/error.rs b/icann-rdap-cli/src/bin/rdap-test/error.rs index c039ade..9b56095 100644 --- a/icann-rdap-cli/src/bin/rdap-test/error.rs +++ b/icann-rdap-cli/src/bin/rdap-test/error.rs @@ -1,6 +1,6 @@ use std::process::{ExitCode, Termination}; -use icann_rdap_cli::rt::exec::TestError; +use icann_rdap_cli::rt::exec::TestExecutionError; use icann_rdap_client::iana_request::IanaResponseError; use icann_rdap_client::RdapClientError; use thiserror::Error; @@ -9,18 +9,22 @@ use thiserror::Error; pub enum RdapTestError { #[error("No errors encountered")] Success, + #[error("Tests completed with execution errors.")] + TestsCompletedExecutionErrors, + #[error("Tests completed, warning checks found.")] + TestsCompletedWarningsFound, + #[error("Tests completed, error checks found.")] + TestsCompletedErrorsFound, #[error(transparent)] RdapClient(#[from] RdapClientError), #[error(transparent)] - TestError(#[from] TestError), + TestExecutionError(#[from] TestExecutionError), #[error(transparent)] Termimad(#[from] termimad::Error), #[error(transparent)] IoError(#[from] std::io::Error), #[error("Unknown output type")] UnknownOutputType, - #[error("RDAP response failed checks.")] - ErrorOnChecks, #[error(transparent)] Json(#[from] serde_json::Error), #[error(transparent)] @@ -40,13 +44,16 @@ impl Termination for RdapTestError { let exit_code: u8 = match self { // Success RdapTestError::Success => 0, + RdapTestError::TestsCompletedExecutionErrors => 1, + RdapTestError::TestsCompletedWarningsFound => 2, + RdapTestError::TestsCompletedErrorsFound => 3, // Internal Errors RdapTestError::Termimad(_) => 10, // I/O Errors RdapTestError::IoError(_) => 40, - RdapTestError::TestError(_) => 40, + RdapTestError::TestExecutionError(_) => 40, // RDAP Errors RdapTestError::Json(_) => 100, @@ -58,7 +65,6 @@ impl Termination for RdapTestError { // User Errors RdapTestError::UnknownOutputType => 200, - RdapTestError::ErrorOnChecks => 201, // RDAP Client Errrors RdapTestError::RdapClient(e) => match e { diff --git a/icann-rdap-cli/src/bin/rdap-test/main.rs b/icann-rdap-cli/src/bin/rdap-test/main.rs index 00b9980..6e7b774 100644 --- a/icann-rdap-cli/src/bin/rdap-test/main.rs +++ b/icann-rdap-cli/src/bin/rdap-test/main.rs @@ -9,9 +9,12 @@ use icann_rdap_cli::dirs::fcbs::FileCacheBootstrapStore; use icann_rdap_cli::rt::exec::execute_tests; use icann_rdap_cli::rt::exec::ExtensionGroup; use icann_rdap_cli::rt::exec::TestOptions; +use icann_rdap_cli::rt::results::RunOutcome; +use icann_rdap_cli::rt::results::TestResults; use icann_rdap_client::client::ClientConfig; use icann_rdap_client::md::MdOptions; use icann_rdap_client::QueryType; +use icann_rdap_common::check::traverse_checks; use icann_rdap_common::check::CheckClass; use termimad::crossterm::style::Color::*; use termimad::Alignment; @@ -412,9 +415,73 @@ pub async fn wrapped_main() -> Result<(), RdapTestError> { } } + // if some tests could not execute + // + let execution_errors = test_results + .test_runs + .iter() + .filter(|r| !matches!(r.outcome, RunOutcome::Tested | RunOutcome::Skipped)) + .count(); + if execution_errors != 0 { + return Err(RdapTestError::TestsCompletedExecutionErrors); + } + + // if tests had check errors + // + // get the error classes but only if they were specified. + let error_classes = check_classes + .iter() + .filter(|c| { + matches!( + c, + CheckClass::StdError | CheckClass::Cidr0Error | CheckClass::IcannError + ) + }) + .copied() + .collect::>(); + // return proper exit code if errors found + if are_there_checks(error_classes, &test_results) { + return Err(RdapTestError::TestsCompletedErrorsFound); + } + + // if tests had check warnings + // + // get the warning classes but only if they were specified. + let warning_classes = check_classes + .iter() + .filter(|c| matches!(c, CheckClass::StdWarning)) + .copied() + .collect::>(); + // return proper exit code if errors found + if are_there_checks(warning_classes, &test_results) { + return Err(RdapTestError::TestsCompletedWarningsFound); + } + Ok(()) } +fn are_there_checks(classes: Vec, test_results: &TestResults) -> bool { + // see if there are any checks in the test runs + let run_count = test_results + .test_runs + .iter() + .filter(|r| { + if let Some(checks) = &r.checks { + traverse_checks(checks, &classes, None, &mut |_, _| {}) + } else { + false + } + }) + .count(); + // see if there are any classes in the service checks + let service_count = test_results + .service_checks + .iter() + .filter(|c| classes.contains(&c.check_class)) + .count(); + run_count + service_count != 0 +} + #[cfg(test)] mod tests { use crate::Cli; diff --git a/icann-rdap-cli/src/rt/exec.rs b/icann-rdap-cli/src/rt/exec.rs index 32db922..099d92d 100644 --- a/icann-rdap-cli/src/rt/exec.rs +++ b/icann-rdap-cli/src/rt/exec.rs @@ -43,7 +43,7 @@ pub enum ExtensionGroup { } #[derive(Debug, Error)] -pub enum TestError { +pub enum TestExecutionError { #[error(transparent)] RdapClient(#[from] RdapClientError), #[error(transparent)] @@ -73,7 +73,7 @@ pub async fn execute_tests<'a, BS: BootstrapStore>( value: &QueryType, options: &TestOptions, client_config: &ClientConfig, -) -> Result { +) -> Result { let bs_client = create_client(client_config)?; // normalize extensions @@ -87,7 +87,7 @@ pub async fn execute_tests<'a, BS: BootstrapStore>( // get the query url let mut query_url = match value { - QueryType::Help => return Err(TestError::UnsupportedQueryType), + QueryType::Help => return Err(TestExecutionError::UnsupportedQueryType), QueryType::Url(url) => url.to_owned(), _ => { let base_url = qtype_to_bootstrap_url(&bs_client, bs, value, |reg| { @@ -103,7 +103,7 @@ pub async fn execute_tests<'a, BS: BootstrapStore>( let response_data = rdap_url_request(&query_url, &client).await?; query_url = get_related_links(&response_data.rdap) .first() - .ok_or(TestError::NoReferralToChase)? + .ok_or(TestExecutionError::NoReferralToChase)? .to_string(); debug!("Chasing referral {query_url}"); } @@ -116,7 +116,9 @@ pub async fn execute_tests<'a, BS: BootstrapStore>( 80 } }); - let host = parsed_url.host_str().ok_or(TestError::NoHostToResolve)?; + let host = parsed_url + .host_str() + .ok_or(TestExecutionError::NoHostToResolve)?; info!("Testing {query_url}"); let dns_data = get_dns_records(host).await?; @@ -172,7 +174,7 @@ pub async fn execute_tests<'a, BS: BootstrapStore>( Ok(test_results) } -async fn get_dns_records(host: &str) -> Result { +async fn get_dns_records(host: &str) -> Result { let conn = UdpClientConnection::new("8.8.8.8:53".parse()?) .unwrap() .new_stream(None); @@ -194,10 +196,10 @@ async fn get_dns_records(host: &str) -> Result { RecordType::CNAME => { let cname = answer .data() - .ok_or(TestError::NoRdata)? + .ok_or(TestExecutionError::NoRdata)? .clone() .into_cname() - .map_err(|_e| TestError::BadRdata)? + .map_err(|_e| TestExecutionError::BadRdata)? .0 .to_string(); debug!("Found cname {cname}"); @@ -206,10 +208,10 @@ async fn get_dns_records(host: &str) -> Result { RecordType::A => { let addr = answer .data() - .ok_or(TestError::NoRdata)? + .ok_or(TestExecutionError::NoRdata)? .clone() .into_a() - .map_err(|_e| TestError::BadRdata)? + .map_err(|_e| TestExecutionError::BadRdata)? .0; debug!("Found IPv4 {addr}"); dns_data.v4_addrs.push(addr); @@ -235,10 +237,10 @@ async fn get_dns_records(host: &str) -> Result { RecordType::CNAME => { let cname = answer .data() - .ok_or(TestError::NoRdata)? + .ok_or(TestExecutionError::NoRdata)? .clone() .into_cname() - .map_err(|_e| TestError::BadRdata)? + .map_err(|_e| TestExecutionError::BadRdata)? .0 .to_string(); debug!("Found cname {cname}"); @@ -247,10 +249,10 @@ async fn get_dns_records(host: &str) -> Result { RecordType::AAAA => { let addr = answer .data() - .ok_or(TestError::NoRdata)? + .ok_or(TestExecutionError::NoRdata)? .clone() .into_aaaa() - .map_err(|_e| TestError::BadRdata)? + .map_err(|_e| TestExecutionError::BadRdata)? .0; debug!("Found IPv6 {addr}"); dns_data.v6_addrs.push(addr); @@ -264,14 +266,14 @@ async fn get_dns_records(host: &str) -> Result { Ok(dns_data) } -fn normalize_extension_ids(options: &TestOptions) -> Result, TestError> { +fn normalize_extension_ids(options: &TestOptions) -> Result, TestExecutionError> { let mut retval = options.expect_extensions.clone(); // check for unregistered extensions if !options.allow_unregistered_extensions { for ext in &retval { if ExtensionId::from_str(ext).is_err() { - return Err(TestError::UnregisteredExtension); + return Err(TestExecutionError::UnregisteredExtension); } } }