1use super::{
11 CompletedRunStats, RecordedRunStatus, RunRecorder, RunStore, ShortestRunIdPrefix, StoreSizes,
12 StressCompletedRunStats, records_cache_dir,
13 retention::{PruneResult, RecordRetentionPolicy},
14};
15use crate::{
16 errors::{RecordPruneError, RecordSetupError, RunStoreError},
17 record::{Styles, format::RerunInfo},
18 reporter::{
19 RunFinishedInfo,
20 events::{FinalRunStats, RunFinishedStats, StressFinalRunStats},
21 },
22};
23use bytesize::ByteSize;
24use camino::{Utf8Path, Utf8PathBuf};
25use chrono::{DateTime, FixedOffset};
26use owo_colors::OwoColorize;
27use quick_junit::ReportUuid;
28use semver::Version;
29use std::{collections::BTreeMap, fmt};
30
31#[derive(Clone, Debug)]
33pub struct RecordSessionConfig<'a> {
34 pub workspace_root: &'a Utf8Path,
36 pub run_id: ReportUuid,
38 pub nextest_version: Version,
40 pub started_at: DateTime<FixedOffset>,
42 pub cli_args: Vec<String>,
44 pub build_scope_args: Vec<String>,
49 pub env_vars: BTreeMap<String, String>,
51 pub max_output_size: ByteSize,
53 pub rerun_info: Option<RerunInfo>,
57}
58
59#[derive(Debug)]
61pub struct RecordSessionSetup {
62 pub session: RecordSession,
64 pub recorder: RunRecorder,
66 pub run_id_unique_prefix: ShortestRunIdPrefix,
71}
72
73#[derive(Debug)]
77pub struct RecordSession {
78 cache_dir: Utf8PathBuf,
79 run_id: ReportUuid,
80}
81
82impl RecordSession {
83 pub fn setup(config: RecordSessionConfig<'_>) -> Result<RecordSessionSetup, RecordSetupError> {
92 let cache_dir =
93 records_cache_dir(config.workspace_root).map_err(RecordSetupError::CacheDirNotFound)?;
94
95 let store = RunStore::new(&cache_dir).map_err(RecordSetupError::StoreCreate)?;
96
97 let locked_store = store
98 .lock_exclusive()
99 .map_err(RecordSetupError::StoreLock)?;
100
101 let (mut recorder, run_id_unique_prefix) = locked_store
102 .create_run_recorder(
103 config.run_id,
104 config.nextest_version,
105 config.started_at,
106 config.cli_args,
107 config.build_scope_args,
108 config.env_vars,
109 config.max_output_size,
110 config.rerun_info.as_ref().map(|info| info.parent_run_id),
111 )
112 .map_err(RecordSetupError::RecorderCreate)?;
113
114 if let Some(rerun_info) = config.rerun_info {
116 recorder
117 .write_rerun_info(&rerun_info)
118 .map_err(RecordSetupError::RecorderCreate)?;
119 }
120
121 let session = RecordSession {
122 cache_dir,
123 run_id: config.run_id,
124 };
125
126 Ok(RecordSessionSetup {
127 session,
128 recorder,
129 run_id_unique_prefix,
130 })
131 }
132
133 pub fn run_id(&self) -> ReportUuid {
135 self.run_id
136 }
137
138 pub fn cache_dir(&self) -> &Utf8Path {
140 &self.cache_dir
141 }
142
143 pub fn finalize(
155 self,
156 recording_sizes: Option<StoreSizes>,
157 run_finished: Option<RunFinishedInfo>,
158 exit_code: i32,
159 policy: &RecordRetentionPolicy,
160 ) -> RecordFinalizeResult {
161 let mut result = RecordFinalizeResult::default();
162
163 let Some(sizes) = recording_sizes else {
165 return result;
166 };
167
168 let (status, duration_secs) = match run_finished {
170 Some(info) => (
171 convert_run_stats_to_status(info.stats, exit_code),
172 Some(info.elapsed.as_secs_f64()),
173 ),
174 None => (RecordedRunStatus::Incomplete, None),
176 };
177
178 let store = match RunStore::new(&self.cache_dir) {
180 Ok(store) => store,
181 Err(err) => {
182 result
183 .warnings
184 .push(RecordFinalizeWarning::StoreOpenFailed(err));
185 return result;
186 }
187 };
188
189 let mut locked_store = match store.lock_exclusive() {
190 Ok(locked) => locked,
191 Err(err) => {
192 result
193 .warnings
194 .push(RecordFinalizeWarning::StoreLockFailed(err));
195 return result;
196 }
197 };
198
199 match locked_store.complete_run(self.run_id, sizes, status, duration_secs) {
201 Ok(true) => {}
202 Ok(false) => {
203 result
205 .warnings
206 .push(RecordFinalizeWarning::RunNotFoundDuringComplete(
207 self.run_id,
208 ));
209 }
210 Err(err) => {
211 result
212 .warnings
213 .push(RecordFinalizeWarning::MetadataPersistFailed(err));
214 }
215 }
216 match locked_store.prune_if_needed(policy) {
220 Ok(Some(mut prune_result)) => {
221 for error in prune_result.errors.drain(..) {
223 result
224 .warnings
225 .push(RecordFinalizeWarning::PruneError(error));
226 }
227 result.prune_result = Some(prune_result);
228 }
229 Ok(None) => {
230 }
232 Err(err) => {
233 result
234 .warnings
235 .push(RecordFinalizeWarning::PruneFailed(err));
236 }
237 }
238
239 result
240 }
241}
242
243fn convert_run_stats_to_status(stats: RunFinishedStats, exit_code: i32) -> RecordedRunStatus {
245 match stats {
246 RunFinishedStats::Single(run_stats) => {
247 let completed_stats = CompletedRunStats {
248 initial_run_count: run_stats.initial_run_count,
249 passed: run_stats.passed,
250 failed: run_stats.failed_count(),
251 exit_code,
252 };
253
254 match run_stats.summarize_final() {
256 FinalRunStats::Success
257 | FinalRunStats::NoTestsRun
258 | FinalRunStats::Failed { .. } => RecordedRunStatus::Completed(completed_stats),
259 FinalRunStats::Cancelled { .. } => RecordedRunStatus::Cancelled(completed_stats),
260 }
261 }
262 RunFinishedStats::Stress(stress_stats) => {
263 let stress_completed_stats = StressCompletedRunStats {
264 initial_iteration_count: stress_stats.completed.total,
265 success_count: stress_stats.success_count,
266 failed_count: stress_stats.failed_count,
267 exit_code,
268 };
269
270 match stress_stats.summarize_final() {
272 StressFinalRunStats::Success
273 | StressFinalRunStats::NoTestsRun
274 | StressFinalRunStats::Failed => {
275 RecordedRunStatus::StressCompleted(stress_completed_stats)
276 }
277 StressFinalRunStats::Cancelled => {
278 RecordedRunStatus::StressCancelled(stress_completed_stats)
279 }
280 }
281 }
282 }
283}
284
285#[derive(Debug, Default)]
287pub struct RecordFinalizeResult {
288 pub warnings: Vec<RecordFinalizeWarning>,
290 pub prune_result: Option<PruneResult>,
292}
293
294impl RecordFinalizeResult {
295 pub fn log(&self, styles: &Styles) {
297 for warning in &self.warnings {
298 tracing::warn!("{warning}");
299 }
300
301 if let Some(prune_result) = &self.prune_result
302 && (prune_result.deleted_count > 0 || prune_result.orphans_deleted > 0)
303 {
304 tracing::info!(
305 "{}(hint: {} to replay runs)",
306 prune_result.display(styles),
307 "cargo nextest replay".style(styles.count),
308 );
309 }
310 }
311}
312
313#[derive(Debug)]
315pub enum RecordFinalizeWarning {
316 StoreOpenFailed(RunStoreError),
318 StoreLockFailed(RunStoreError),
320 MetadataPersistFailed(RunStoreError),
322 RunNotFoundDuringComplete(ReportUuid),
327 PruneFailed(RunStoreError),
329 PruneError(RecordPruneError),
331}
332
333impl fmt::Display for RecordFinalizeWarning {
334 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
335 match self {
336 Self::StoreOpenFailed(err) => {
337 write!(f, "recording completed but failed to open run store: {err}")
338 }
339 Self::StoreLockFailed(err) => {
340 write!(f, "recording completed but failed to lock run store: {err}")
341 }
342 Self::MetadataPersistFailed(err) => {
343 write!(
344 f,
345 "recording completed but failed to persist run metadata: {err}"
346 )
347 }
348 Self::RunNotFoundDuringComplete(run_id) => {
349 write!(
350 f,
351 "recording completed but run {run_id} was not found in store \
352 (may have been pruned during execution)"
353 )
354 }
355 Self::PruneFailed(err) => write!(f, "error during prune: {err}"),
356 Self::PruneError(msg) => write!(f, "error during prune: {msg}"),
357 }
358 }
359}