pub mod human;
pub mod json;
use crate::client::GradleBuildDetails;
use crate::models::{
GradleAttributes, GradleBuildCachePerformance, GradleDependencies, GradleDeprecationEntry,
GradleFailures, GradleNetworkActivity, GradleTests,
};
use chrono::{TimeZone, Utc};
use serde::Serialize;
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct BuildOutput {
pub build_id: String,
pub build_scan_url: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub result: Option<ResultOutput>,
#[serde(skip_serializing_if = "Option::is_none")]
pub deprecations: Option<Vec<DeprecationOutput>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub failures: Option<FailuresOutput>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tests: Option<TestsOutput>,
#[serde(skip_serializing_if = "Option::is_none")]
pub task_execution: Option<TaskExecutionOutput>,
#[serde(skip_serializing_if = "Option::is_none")]
pub network_activity: Option<NetworkActivityOutput>,
#[serde(skip_serializing_if = "Option::is_none")]
pub dependencies: Option<DependenciesOutput>,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ResultOutput {
pub outcome: String,
pub has_failed: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub has_verification_failure: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub has_non_verification_failure: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub project_name: Option<String>,
pub gradle_version: String,
pub build_duration_ms: i64,
pub build_start_time: String,
pub requested_tasks: Vec<String>,
pub tags: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub username: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub hostname: Option<String>,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct DeprecationOutput {
pub summary: String,
pub removal_details: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub advice: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub documentation_url: Option<String>,
pub usage_count: usize,
pub usages: Vec<DeprecationUsageOutput>,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct DeprecationUsageOutput {
#[serde(rename = "type")]
pub owner_type: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub location: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub contextual_advice: Option<String>,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct FailuresOutput {
pub build_failures: Vec<BuildFailureOutput>,
#[serde(skip_serializing_if = "Option::is_none")]
pub test_failures: Option<Vec<TestFailureOutput>>,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct BuildFailureOutput {
pub header: String,
pub message: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub task_path: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub location: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub stacktrace: Option<String>,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct TestFailureOutput {
pub class_name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub test_name: Option<String>,
pub message: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub stacktrace: Option<String>,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct TestsOutput {
pub summary: TestSummaryOutput,
pub tests: Vec<TestExecutionOutput>,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct TestSummaryOutput {
pub total: i32,
pub passed: i32,
pub failed: i32,
pub skipped: i32,
pub flaky: i32,
pub not_selected: i32,
pub duration_ms: i64,
pub pass_rate: f64,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct TestExecutionOutput {
pub work_unit: String,
pub class_name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub test_name: Option<String>,
pub outcome: String,
pub duration_ms: i64,
pub execution_count: usize,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct TaskExecutionOutput {
pub summary: TaskExecutionSummaryOutput,
pub avoidance_savings: AvoidanceSavingsOutput,
pub tasks: Vec<TaskEntryOutput>,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct TaskExecutionSummaryOutput {
pub build_time_ms: i64,
pub effective_task_execution_time_ms: i64,
pub serial_task_execution_time_ms: i64,
pub serialization_factor: f64,
pub total_tasks: usize,
pub avoided_tasks: usize,
pub executed_tasks: usize,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct AvoidanceSavingsOutput {
pub total_ms: i64,
pub ratio: f64,
pub up_to_date_ms: i64,
pub local_build_cache_ms: i64,
pub remote_build_cache_ms: i64,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct TaskEntryOutput {
pub task_path: String,
pub outcome: String,
pub duration_ms: i64,
pub has_failed: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub task_type: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub cache_key: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub cache_artifact_size: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub non_cacheability_reason: Option<String>,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct NetworkActivityOutput {
pub network_request_count: i64,
pub serial_network_request_time_ms: i64,
pub wall_clock_network_request_time_ms: i64,
pub file_download_count: i64,
pub file_download_size_bytes: i64,
pub methods: Vec<NetworkActivityEntryOutput>,
pub repositories: Vec<NetworkActivityEntryOutput>,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct NetworkActivityEntryOutput {
pub name: String,
pub network_request_count: i64,
pub serial_network_request_time_ms: i64,
pub wall_clock_network_request_time_ms: i64,
pub file_download_count: i64,
pub file_download_size_bytes: i64,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct DependenciesOutput {
pub total: usize,
pub dependencies: Vec<DependencyOutput>,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct DependencyOutput {
#[serde(skip_serializing_if = "Option::is_none")]
pub dependency_type: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub namespace: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub version: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub purl: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub repository_url: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub resolution_source: Option<String>,
}
impl BuildOutput {
pub fn from_details(
details: GradleBuildDetails,
build_scan_url: String,
verbose: bool,
) -> Self {
let result = details.attributes.map(ResultOutput::from);
let deprecations = details.deprecations.and_then(|deps| {
deps.deprecations
.map(|entries| entries.into_iter().map(DeprecationOutput::from).collect())
});
let failures = details
.failures
.map(|f| FailuresOutput::from_failures(f, verbose));
let tests = details.tests.map(|t| TestsOutput::from_tests(t, verbose));
let task_execution = details
.build_cache_performance
.map(TaskExecutionOutput::from_performance);
let network_activity = details
.network_activity
.map(NetworkActivityOutput::from_activity);
let dependencies = details
.dependencies
.map(DependenciesOutput::from_dependencies);
Self {
build_id: details.build.id,
build_scan_url,
result,
deprecations,
failures,
tests,
task_execution,
network_activity,
dependencies,
}
}
}
impl From<GradleAttributes> for ResultOutput {
fn from(attrs: GradleAttributes) -> Self {
let build_start_time = Utc
.timestamp_millis_opt(attrs.build_start_time)
.single()
.map(|dt| dt.to_rfc3339())
.unwrap_or_else(|| attrs.build_start_time.to_string());
let hostname = attrs
.environment
.public_hostname
.or(attrs.environment.local_hostname);
Self {
outcome: if attrs.has_failed {
"failed".to_string()
} else {
"success".to_string()
},
has_failed: attrs.has_failed,
has_verification_failure: attrs.has_verification_failure,
has_non_verification_failure: attrs.has_non_verification_failure,
project_name: attrs.root_project_name,
gradle_version: attrs.gradle_version,
build_duration_ms: attrs.build_duration,
build_start_time,
requested_tasks: attrs.requested_tasks,
tags: attrs.tags,
username: attrs.environment.username,
hostname,
}
}
}
impl From<GradleDeprecationEntry> for DeprecationOutput {
fn from(entry: GradleDeprecationEntry) -> Self {
let usage_count = entry.usages.len();
let usages = entry
.usages
.into_iter()
.map(|u| DeprecationUsageOutput {
owner_type: u.owner.owner_type.to_string(),
location: u.owner.location,
contextual_advice: u.contextual_advice,
})
.collect();
Self {
summary: entry.summary,
removal_details: entry.removal_details,
advice: entry.advice,
documentation_url: entry.documentation_url,
usage_count,
usages,
}
}
}
impl FailuresOutput {
fn from_failures(failures: GradleFailures, verbose: bool) -> Self {
let build_failures = failures
.build_failures
.into_iter()
.map(|f| BuildFailureOutput {
header: f.header,
message: f.message,
task_path: f.task_path,
location: f.location,
stacktrace: if verbose { f.stacktrace } else { None },
})
.collect();
let test_failures = failures.test_failures.map(|tests| {
tests
.into_iter()
.map(|t| TestFailureOutput {
class_name: t.id.suite_name,
test_name: t.id.test_name,
message: t.message,
stacktrace: if verbose { t.stacktrace } else { None },
})
.collect()
});
Self {
build_failures,
test_failures,
}
}
}
impl TestsOutput {
fn from_tests(tests: GradleTests, _verbose: bool) -> Self {
let dist = &tests.summary.test_cases_outcome_distribution;
let total = dist.total;
let passed = dist.passed;
let summary = TestSummaryOutput {
total: dist.total,
passed: dist.passed,
failed: dist.failed,
skipped: dist.skipped,
flaky: dist.flaky,
not_selected: dist.not_selected,
duration_ms: tests.summary.duration.total,
pass_rate: if total > 0 {
(passed as f64 / total as f64) * 100.0
} else {
0.0
},
};
let test_executions = tests
.flatten_test_cases()
.into_iter()
.map(|tc| TestExecutionOutput {
work_unit: tc.work_unit,
class_name: tc.container_name,
test_name: Some(tc.test_name),
outcome: tc.outcome.to_string(),
duration_ms: tc.duration_ms,
execution_count: tc.execution_count,
})
.collect();
Self {
summary,
tests: test_executions,
}
}
}
impl TaskExecutionOutput {
fn from_performance(perf: GradleBuildCachePerformance) -> Self {
let total_tasks = perf.task_execution.len();
let avoided_tasks = perf
.task_execution
.iter()
.filter(|t| {
matches!(
t.avoidance_outcome,
crate::models::AvoidanceOutcome::AvoidedUpToDate
| crate::models::AvoidanceOutcome::AvoidedFromLocalCache
| crate::models::AvoidanceOutcome::AvoidedFromRemoteCache
| crate::models::AvoidanceOutcome::AvoidedUnknownReason
)
})
.count();
let executed_tasks = perf
.task_execution
.iter()
.filter(|t| {
matches!(
t.avoidance_outcome,
crate::models::AvoidanceOutcome::ExecutedCacheable
| crate::models::AvoidanceOutcome::ExecutedNotCacheable
| crate::models::AvoidanceOutcome::ExecutedUnknownCacheability
)
})
.count();
let summary = TaskExecutionSummaryOutput {
build_time_ms: perf.build_time,
effective_task_execution_time_ms: perf.effective_task_execution_time,
serial_task_execution_time_ms: perf.serial_task_execution_time,
serialization_factor: perf.serialization_factor,
total_tasks,
avoided_tasks,
executed_tasks,
};
let savings = &perf.task_avoidance_savings_summary;
let avoidance_savings = AvoidanceSavingsOutput {
total_ms: savings.total,
ratio: savings.ratio,
up_to_date_ms: savings.up_to_date,
local_build_cache_ms: savings.local_build_cache,
remote_build_cache_ms: savings.remote_build_cache,
};
let tasks = perf
.task_execution
.into_iter()
.map(|entry| {
let non_cacheability_reason = entry.non_cacheability_category.as_ref().map(|cat| {
match &entry.non_cacheability_reason {
Some(reason) => format!("{}: {}", cat, reason),
None => cat.to_string(),
}
});
TaskEntryOutput {
task_path: entry.task_path,
outcome: entry.avoidance_outcome.to_string(),
duration_ms: entry.duration,
has_failed: entry.has_failed,
task_type: Some(entry.task_type),
cache_key: entry.cache_key,
cache_artifact_size: entry.cache_artifact_size,
non_cacheability_reason,
}
})
.collect();
Self {
summary,
avoidance_savings,
tasks,
}
}
}
impl NetworkActivityOutput {
fn from_activity(activity: GradleNetworkActivity) -> Self {
let mut methods: Vec<NetworkActivityEntryOutput> = activity
.methods
.into_iter()
.map(|(name, m)| NetworkActivityEntryOutput {
name,
network_request_count: m.network_request_count,
serial_network_request_time_ms: m.serial_network_request_time,
wall_clock_network_request_time_ms: m.wall_clock_network_request_time,
file_download_count: m.file_download_count,
file_download_size_bytes: m.file_download_size,
})
.collect();
methods.sort_by(|a, b| b.network_request_count.cmp(&a.network_request_count));
let mut repositories: Vec<NetworkActivityEntryOutput> = activity
.repositories
.into_iter()
.map(|(name, m)| NetworkActivityEntryOutput {
name,
network_request_count: m.network_request_count,
serial_network_request_time_ms: m.serial_network_request_time,
wall_clock_network_request_time_ms: m.wall_clock_network_request_time,
file_download_count: m.file_download_count,
file_download_size_bytes: m.file_download_size,
})
.collect();
repositories.sort_by(|a, b| b.network_request_count.cmp(&a.network_request_count));
Self {
network_request_count: activity.network_request_count,
serial_network_request_time_ms: activity.serial_network_request_time,
wall_clock_network_request_time_ms: activity.wall_clock_network_request_time,
file_download_count: activity.file_download_count,
file_download_size_bytes: activity.file_download_size,
methods,
repositories,
}
}
}
impl DependenciesOutput {
fn from_dependencies(deps: GradleDependencies) -> Self {
let mut dependencies: Vec<DependencyOutput> = deps
.dependencies
.into_iter()
.map(|d| DependencyOutput {
dependency_type: d.dependency_type,
namespace: d.namespace,
name: d.name,
version: d.version,
purl: d.purl,
repository_url: d.repository.as_ref().and_then(|r| r.url.clone()),
resolution_source: d.repository.and_then(|r| r.resolution_source),
})
.collect();
dependencies.sort_by(|a, b| {
a.dependency_type
.cmp(&b.dependency_type)
.then(a.namespace.cmp(&b.namespace))
.then(a.name.cmp(&b.name))
});
let total = dependencies.len();
Self { total, dependencies }
}
}
pub fn format_duration(millis: i64) -> String {
let total_secs = millis / 1000;
let hours = total_secs / 3600;
let minutes = (total_secs % 3600) / 60;
let seconds = total_secs % 60;
if hours > 0 {
format!("{}h {}m {}s", hours, minutes, seconds)
} else if minutes > 0 {
format!("{}m {}s", minutes, seconds)
} else if seconds > 0 {
format!("{}s", seconds)
} else {
format!("{}ms", millis)
}
}
pub fn format_timestamp(millis: i64) -> String {
Utc.timestamp_millis_opt(millis)
.single()
.map(|dt| dt.format("%Y-%m-%d %H:%M:%S UTC").to_string())
.unwrap_or_else(|| millis.to_string())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::models::{
BuildTestOrContainer, BuildTestOrContainerDuration, BuildTestOrContainerExecution,
BuildTestOrContainerOutcome, BuildTestWorkUnit, BuildTestsDuration, BuildTestsResponse,
BuildTestsSummary, TestOutcome, TestOutcomeDistribution, TestWorkUnitOutcome,
};
fn make_summary(
total: i32,
passed: i32,
failed: i32,
skipped: i32,
flaky: i32,
not_selected: i32,
duration_ms: i64,
) -> BuildTestsSummary {
BuildTestsSummary {
duration: BuildTestsDuration {
total: duration_ms,
serial: Some(duration_ms),
},
test_cases_outcome_distribution: TestOutcomeDistribution {
total,
passed,
failed,
skipped,
flaky,
not_selected,
},
test_containers_outcome_distribution: TestOutcomeDistribution {
total: 1,
passed: 1,
failed: 0,
skipped: 0,
flaky: 0,
not_selected: 0,
},
}
}
fn make_leaf_test(name: &str, outcome: TestOutcome, duration_ms: i64) -> BuildTestOrContainer {
BuildTestOrContainer {
name: name.to_string(),
duration: BuildTestOrContainerDuration {
total: Some(duration_ms),
own: None,
serial: None,
},
outcome: BuildTestOrContainerOutcome {
overall: outcome.clone(),
own: None,
children: None,
},
executions: vec![BuildTestOrContainerExecution {
duration: BuildTestOrContainerDuration {
total: Some(duration_ms),
own: None,
serial: None,
},
outcome: BuildTestOrContainerOutcome {
overall: outcome,
own: None,
children: None,
},
}],
children: vec![],
}
}
fn make_container(name: &str, children: Vec<BuildTestOrContainer>) -> BuildTestOrContainer {
BuildTestOrContainer {
name: name.to_string(),
duration: BuildTestOrContainerDuration {
total: Some(100),
own: Some(5),
serial: Some(100),
},
outcome: BuildTestOrContainerOutcome {
overall: TestOutcome::Passed,
own: Some(TestOutcome::Passed),
children: Some(TestOutcome::Passed),
},
executions: vec![],
children,
}
}
fn make_work_unit(name: &str, tests: Vec<BuildTestOrContainer>) -> BuildTestWorkUnit {
BuildTestWorkUnit {
name: name.to_string(),
duration: BuildTestOrContainerDuration {
total: Some(200),
own: Some(10),
serial: Some(200),
},
outcome: TestWorkUnitOutcome::Passed,
tests,
}
}
fn make_gradle_tests(
summary: BuildTestsSummary,
work_units: Vec<BuildTestWorkUnit>,
) -> GradleTests {
BuildTestsResponse {
summary,
work_units,
}
}
#[test]
fn test_tests_output_from_tests_uses_api_summary() {
let summary = make_summary(10, 8, 1, 1, 0, 0, 5000);
let gradle_tests = make_gradle_tests(summary, vec![]);
let output = TestsOutput::from_tests(gradle_tests, false);
assert_eq!(output.summary.total, 10);
assert_eq!(output.summary.passed, 8);
assert_eq!(output.summary.failed, 1);
assert_eq!(output.summary.skipped, 1);
assert_eq!(output.summary.flaky, 0);
assert_eq!(output.summary.not_selected, 0);
assert_eq!(output.summary.duration_ms, 5000);
assert!((output.summary.pass_rate - 80.0).abs() < 0.01);
}
#[test]
fn test_tests_output_pass_rate_calculation_zero_total() {
let summary = make_summary(0, 0, 0, 0, 0, 0, 0);
let gradle_tests = make_gradle_tests(summary, vec![]);
let output = TestsOutput::from_tests(gradle_tests, false);
assert_eq!(output.summary.pass_rate, 0.0);
}
#[test]
fn test_tests_output_pass_rate_100_percent() {
let summary = make_summary(2, 2, 0, 0, 0, 0, 200);
let gradle_tests = make_gradle_tests(summary, vec![]);
let output = TestsOutput::from_tests(gradle_tests, false);
assert!((output.summary.pass_rate - 100.0).abs() < 0.01);
}
#[test]
fn test_tests_output_pass_rate_0_percent() {
let summary = make_summary(2, 0, 2, 0, 0, 0, 200);
let gradle_tests = make_gradle_tests(summary, vec![]);
let output = TestsOutput::from_tests(gradle_tests, false);
assert_eq!(output.summary.pass_rate, 0.0);
}
#[test]
fn test_tests_output_includes_flaky_and_not_selected() {
let summary = make_summary(10, 5, 1, 1, 2, 1, 1000);
let gradle_tests = make_gradle_tests(summary, vec![]);
let output = TestsOutput::from_tests(gradle_tests, false);
assert_eq!(output.summary.flaky, 2);
assert_eq!(output.summary.not_selected, 1);
}
#[test]
fn test_execution_output_from_flattened_tests() {
let container = make_container(
"com.example.TestSuite",
vec![
make_leaf_test("testA", TestOutcome::Passed, 100),
make_leaf_test("testB", TestOutcome::Failed, 200),
],
);
let wu = make_work_unit(":app:test", vec![container]);
let summary = make_summary(2, 1, 1, 0, 0, 0, 300);
let gradle_tests = make_gradle_tests(summary, vec![wu]);
let output = TestsOutput::from_tests(gradle_tests, false);
assert_eq!(output.tests.len(), 2);
assert_eq!(output.tests[0].work_unit, ":app:test");
assert_eq!(output.tests[0].class_name, "com.example.TestSuite");
assert_eq!(output.tests[0].test_name, Some("testA".to_string()));
assert_eq!(output.tests[0].outcome, "passed");
assert_eq!(output.tests[0].duration_ms, 100);
assert_eq!(output.tests[0].execution_count, 1);
assert_eq!(output.tests[1].work_unit, ":app:test");
assert_eq!(output.tests[1].test_name, Some("testB".to_string()));
assert_eq!(output.tests[1].outcome, "failed");
assert_eq!(output.tests[1].duration_ms, 200);
}
#[test]
fn test_execution_output_outcome_strings() {
for (outcome, expected) in [
(TestOutcome::Passed, "passed"),
(TestOutcome::Failed, "failed"),
(TestOutcome::Skipped, "skipped"),
(TestOutcome::Flaky, "flaky"),
(TestOutcome::NotSelected, "notSelected"),
] {
let container = make_container("Suite", vec![make_leaf_test("test", outcome, 0)]);
let wu = make_work_unit(":test", vec![container]);
let summary = make_summary(1, 0, 0, 0, 0, 0, 0);
let gradle_tests = make_gradle_tests(summary, vec![wu]);
let output = TestsOutput::from_tests(gradle_tests, false);
assert_eq!(output.tests[0].outcome, expected);
}
}
#[test]
fn test_execution_output_multiple_work_units() {
let container1 = make_container(
"AppTest",
vec![make_leaf_test("test1", TestOutcome::Passed, 100)],
);
let container2 = make_container(
"LibTest",
vec![make_leaf_test("test2", TestOutcome::Passed, 200)],
);
let wu1 = make_work_unit(":app:test", vec![container1]);
let wu2 = make_work_unit(":lib:test", vec![container2]);
let summary = make_summary(2, 2, 0, 0, 0, 0, 300);
let gradle_tests = make_gradle_tests(summary, vec![wu1, wu2]);
let output = TestsOutput::from_tests(gradle_tests, false);
assert_eq!(output.tests.len(), 2);
assert_eq!(output.tests[0].work_unit, ":app:test");
assert_eq!(output.tests[1].work_unit, ":lib:test");
}
#[test]
fn test_execution_output_empty_work_units() {
let summary = make_summary(0, 0, 0, 0, 0, 0, 0);
let gradle_tests = make_gradle_tests(summary, vec![]);
let output = TestsOutput::from_tests(gradle_tests, false);
assert!(output.tests.is_empty());
assert_eq!(output.summary.total, 0);
}
#[test]
fn test_execution_output_retries_counted() {
let test = BuildTestOrContainer {
name: "flakyTest".to_string(),
duration: BuildTestOrContainerDuration {
total: Some(40),
own: None,
serial: None,
},
outcome: BuildTestOrContainerOutcome {
overall: TestOutcome::Flaky,
own: None,
children: None,
},
executions: vec![
BuildTestOrContainerExecution {
duration: BuildTestOrContainerDuration {
total: Some(20),
own: None,
serial: None,
},
outcome: BuildTestOrContainerOutcome {
overall: TestOutcome::Failed,
own: None,
children: None,
},
},
BuildTestOrContainerExecution {
duration: BuildTestOrContainerDuration {
total: Some(20),
own: None,
serial: None,
},
outcome: BuildTestOrContainerOutcome {
overall: TestOutcome::Passed,
own: None,
children: None,
},
},
],
children: vec![],
};
let container = make_container("Suite", vec![test]);
let wu = make_work_unit(":test", vec![container]);
let summary = make_summary(1, 0, 0, 0, 1, 0, 40);
let gradle_tests = make_gradle_tests(summary, vec![wu]);
let output = TestsOutput::from_tests(gradle_tests, false);
assert_eq!(output.tests.len(), 1);
assert_eq!(output.tests[0].outcome, "flaky");
assert_eq!(output.tests[0].execution_count, 2);
}
#[test]
fn test_summary_output_json_serialization() {
let summary = make_summary(100, 95, 3, 2, 0, 0, 60000);
let gradle_tests = make_gradle_tests(summary, vec![]);
let output = TestsOutput::from_tests(gradle_tests, false);
let json = serde_json::to_string(&output.summary).unwrap();
assert!(json.contains("\"total\":100"));
assert!(json.contains("\"passed\":95"));
assert!(json.contains("\"failed\":3"));
assert!(json.contains("\"skipped\":2"));
assert!(json.contains("\"flaky\":0"));
assert!(json.contains("\"notSelected\":0"));
assert!(json.contains("\"durationMs\":60000"));
assert!(json.contains("\"passRate\":95"));
}
#[test]
fn test_format_duration_milliseconds() {
assert_eq!(format_duration(500), "500ms");
assert_eq!(format_duration(0), "0ms");
assert_eq!(format_duration(999), "999ms");
}
#[test]
fn test_format_duration_seconds() {
assert_eq!(format_duration(1000), "1s");
assert_eq!(format_duration(5000), "5s");
assert_eq!(format_duration(59000), "59s");
}
#[test]
fn test_format_duration_minutes() {
assert_eq!(format_duration(60000), "1m 0s");
assert_eq!(format_duration(90000), "1m 30s");
assert_eq!(format_duration(3599000), "59m 59s");
}
#[test]
fn test_format_duration_hours() {
assert_eq!(format_duration(3600000), "1h 0m 0s");
assert_eq!(format_duration(7200000), "2h 0m 0s");
assert_eq!(format_duration(3661000), "1h 1m 1s");
}
}