1use std::{
11 collections::{BTreeMap, BTreeSet},
12 fs::{self, File, OpenOptions},
13 io::Read,
14 path::Path,
15};
16
17use serde::{Deserialize, Serialize};
18use supercov_contracts::{
19 RUST_LIBTEST_EVENT_HEADER_SIZE, RUST_LIBTEST_EVENT_MAGIC, RUST_LIBTEST_EVENT_MAX_NAME_BYTES,
20 RUST_LIBTEST_EVENT_PROTOCOL_VERSION, RUST_LIBTEST_EVENT_RECORD_HEADER_SIZE,
21 RUST_LIBTEST_EVENT_TOKEN_SIZE,
22};
23
24pub const RUST_LIBTEST_EVENTS_ENV: &str = "SUPERCOV_RUST_LIBTEST_EVENTS";
25pub const RUST_LIBTEST_TOKEN_ENV: &str = "SUPERCOV_RUST_LIBTEST_TOKEN";
26const RUST_LIBTEST_EVENT_RUNTIME: &str = include_str!("../runtime-assets/rust-libtest-events.rs");
27
28pub fn rust_libtest_event_runtime_source() -> &'static str {
29 RUST_LIBTEST_EVENT_RUNTIME
30}
31
32const ENDIAN_MARKER: u32 = 0x0102_0304;
33const HEADER_TOKEN_OFFSET: usize = 24;
34const HEADER_RESERVED_OFFSET: usize = 40;
35
36const RECORD_KIND_OFFSET: usize = 4;
37const RECORD_RESULT_OFFSET: usize = 5;
38const RECORD_FLAGS_OFFSET: usize = 6;
39const RECORD_SEQUENCE_OFFSET: usize = 8;
40const RECORD_COUNT_OFFSET: usize = 16;
41const RECORD_SEED_OFFSET: usize = 24;
42const RECORD_NAME_LENGTH_OFFSET: usize = 32;
43const RECORD_RESERVED_OFFSET: usize = 36;
44const RECORD_CHECKSUM_OFFSET: usize = 40;
45
46const KIND_FILTERED_OUT: u8 = 1;
47const KIND_FILTERED: u8 = 2;
48const KIND_STARTED: u8 = 3;
49const KIND_TIMEOUT: u8 = 4;
50const KIND_FINISHED: u8 = 5;
51const FLAG_SHUFFLE_SEED: u16 = 1;
52const NO_SEED: u64 = u64::MAX;
53
54const RESULT_NONE: u8 = 0;
55const RESULT_PASSED: u8 = 1;
56const RESULT_FAILED: u8 = 2;
57const RESULT_IGNORED: u8 = 3;
58const RESULT_BENCHMARKED: u8 = 4;
59
60#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
61#[serde(rename_all = "kebab-case")]
62pub enum RustLibtestTerminalResult {
63 Passed,
64 Failed,
65 Ignored,
66 Benchmarked,
67}
68
69#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
70#[serde(tag = "event", rename_all = "kebab-case", deny_unknown_fields)]
71pub enum RustLibtestEvent {
72 FilteredOut {
73 count: u64,
74 },
75 Filtered {
76 count: u64,
77 shuffle_seed: Option<u64>,
78 },
79 Started {
80 name: String,
81 },
82 Timeout {
83 name: String,
84 },
85 Finished {
86 name: String,
87 result: RustLibtestTerminalResult,
88 },
89}
90
91#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
92#[serde(rename_all = "camelCase", deny_unknown_fields)]
93pub struct RustLibtestAttemptEvent {
94 pub name: String,
95 pub result: RustLibtestTerminalResult,
96 pub timed_out: bool,
97}
98
99#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
100#[serde(rename_all = "camelCase", deny_unknown_fields)]
101pub struct RustLibtestRunEvents {
102 pub filtered_out: u64,
103 pub shuffle_seed: Option<u64>,
104 pub attempts: Vec<RustLibtestAttemptEvent>,
105 pub unstarted: Vec<String>,
106}
107
108#[derive(Debug, Clone, PartialEq, Eq)]
109pub enum RustLibtestEventError {
110 Io(String),
111 UnsafeFile(String),
112 InvalidHeader,
113 EmptyStream,
114 TruncatedRecord(u64),
115 InvalidRecord(u64),
116 InvalidSequence { expected: u64, actual: u64 },
117 NameTooLong(u64),
118 InvalidName(u64),
119 InvalidLifecycle(String),
120}
121
122impl std::fmt::Display for RustLibtestEventError {
123 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
124 match self {
125 Self::Io(error) => write!(formatter, "Rust libtest event I/O failed: {error}"),
126 Self::UnsafeFile(path) => {
127 write!(formatter, "unsafe Rust libtest event file: {path}")
128 }
129 Self::InvalidHeader => formatter.write_str("invalid Rust libtest event header"),
130 Self::EmptyStream => formatter.write_str("Rust libtest event stream is empty"),
131 Self::TruncatedRecord(sequence) => {
132 write!(
133 formatter,
134 "Rust libtest event record {sequence} is truncated"
135 )
136 }
137 Self::InvalidRecord(sequence) => {
138 write!(formatter, "Rust libtest event record {sequence} is invalid")
139 }
140 Self::InvalidSequence { expected, actual } => write!(
141 formatter,
142 "Rust libtest event sequence is not contiguous: expected {expected}, got {actual}"
143 ),
144 Self::NameTooLong(sequence) => {
145 write!(
146 formatter,
147 "Rust libtest event {sequence} has an oversized name"
148 )
149 }
150 Self::InvalidName(sequence) => {
151 write!(
152 formatter,
153 "Rust libtest event {sequence} has an invalid name"
154 )
155 }
156 Self::InvalidLifecycle(reason) => {
157 write!(formatter, "invalid Rust libtest event lifecycle: {reason}")
158 }
159 }
160 }
161}
162
163impl std::error::Error for RustLibtestEventError {}
164
165#[cfg(test)]
166fn put_u16(target: &mut [u8], offset: usize, value: u16) {
167 target[offset..offset + 2].copy_from_slice(&value.to_le_bytes());
168}
169
170fn put_u32(target: &mut [u8], offset: usize, value: u32) {
171 target[offset..offset + 4].copy_from_slice(&value.to_le_bytes());
172}
173
174#[cfg(test)]
175fn put_u64(target: &mut [u8], offset: usize, value: u64) {
176 target[offset..offset + 8].copy_from_slice(&value.to_le_bytes());
177}
178
179fn get_u16(source: &[u8], offset: usize) -> Option<u16> {
180 Some(u16::from_le_bytes(
181 source.get(offset..offset + 2)?.try_into().ok()?,
182 ))
183}
184
185fn get_u32(source: &[u8], offset: usize) -> Option<u32> {
186 Some(u32::from_le_bytes(
187 source.get(offset..offset + 4)?.try_into().ok()?,
188 ))
189}
190
191fn get_u64(source: &[u8], offset: usize) -> Option<u64> {
192 Some(u64::from_le_bytes(
193 source.get(offset..offset + 8)?.try_into().ok()?,
194 ))
195}
196
197fn checksum(token: &[u8; RUST_LIBTEST_EVENT_TOKEN_SIZE], prefix: &[u8], name: &[u8]) -> u64 {
198 let mut value = 0xcbf2_9ce4_8422_2325_u64;
199 for byte in token
200 .iter()
201 .copied()
202 .chain(prefix.iter().copied())
203 .chain(name.iter().copied())
204 {
205 value ^= u64::from(byte);
206 value = value.wrapping_mul(0x0000_0100_0000_01b3);
207 }
208 value
209}
210
211fn regular_file(path: &Path) -> Result<File, RustLibtestEventError> {
212 let metadata =
213 fs::symlink_metadata(path).map_err(|error| RustLibtestEventError::Io(error.to_string()))?;
214 if !metadata.file_type().is_file() {
215 return Err(RustLibtestEventError::UnsafeFile(
216 path.display().to_string(),
217 ));
218 }
219 let mut options = OpenOptions::new();
220 options.read(true);
221 #[cfg(any(target_os = "linux", target_os = "macos"))]
222 {
223 use std::os::unix::fs::OpenOptionsExt as _;
224 #[cfg(target_os = "linux")]
225 const O_NOFOLLOW: i32 = 0x2_0000;
226 #[cfg(target_os = "macos")]
227 const O_NOFOLLOW: i32 = 0x100;
228 options.custom_flags(O_NOFOLLOW);
229 }
230 let file = options
231 .open(path)
232 .map_err(|error| RustLibtestEventError::Io(error.to_string()))?;
233 if !file
234 .metadata()
235 .map_err(|error| RustLibtestEventError::Io(error.to_string()))?
236 .file_type()
237 .is_file()
238 {
239 return Err(RustLibtestEventError::UnsafeFile(
240 path.display().to_string(),
241 ));
242 }
243 Ok(file)
244}
245
246pub fn create_rust_libtest_event_file(
247 path: &Path,
248 token: [u8; RUST_LIBTEST_EVENT_TOKEN_SIZE],
249) -> Result<(), RustLibtestEventError> {
250 let mut options = OpenOptions::new();
251 options.write(true).create_new(true);
252 #[cfg(unix)]
253 {
254 use std::os::unix::fs::OpenOptionsExt as _;
255 options.mode(0o600);
256 }
257 let mut file = options
258 .open(path)
259 .map_err(|error| RustLibtestEventError::Io(error.to_string()))?;
260 let mut header = [0_u8; RUST_LIBTEST_EVENT_HEADER_SIZE];
261 header[..8].copy_from_slice(RUST_LIBTEST_EVENT_MAGIC.as_bytes());
262 put_u32(&mut header, 8, RUST_LIBTEST_EVENT_PROTOCOL_VERSION);
263 put_u32(&mut header, 12, RUST_LIBTEST_EVENT_HEADER_SIZE as u32);
264 put_u32(
265 &mut header,
266 16,
267 RUST_LIBTEST_EVENT_RECORD_HEADER_SIZE as u32,
268 );
269 put_u32(&mut header, 20, ENDIAN_MARKER);
270 header[HEADER_TOKEN_OFFSET..HEADER_TOKEN_OFFSET + RUST_LIBTEST_EVENT_TOKEN_SIZE]
271 .copy_from_slice(&token);
272 std::io::Write::write_all(&mut file, &header)
273 .and_then(|()| file.sync_data())
274 .map_err(|error| RustLibtestEventError::Io(error.to_string()))
275}
276
277pub fn read_rust_libtest_events(
278 path: &Path,
279 expected_token: &[u8; RUST_LIBTEST_EVENT_TOKEN_SIZE],
280) -> Result<Vec<RustLibtestEvent>, RustLibtestEventError> {
281 let mut file = regular_file(path)?;
282 let mut bytes = Vec::new();
283 file.read_to_end(&mut bytes)
284 .map_err(|error| RustLibtestEventError::Io(error.to_string()))?;
285 if bytes.len() < RUST_LIBTEST_EVENT_HEADER_SIZE
286 || bytes.get(..8) != Some(RUST_LIBTEST_EVENT_MAGIC.as_bytes())
287 || get_u32(&bytes, 8) != Some(RUST_LIBTEST_EVENT_PROTOCOL_VERSION)
288 || get_u32(&bytes, 12) != Some(RUST_LIBTEST_EVENT_HEADER_SIZE as u32)
289 || get_u32(&bytes, 16) != Some(RUST_LIBTEST_EVENT_RECORD_HEADER_SIZE as u32)
290 || get_u32(&bytes, 20) != Some(ENDIAN_MARKER)
291 || bytes.get(HEADER_TOKEN_OFFSET..HEADER_TOKEN_OFFSET + RUST_LIBTEST_EVENT_TOKEN_SIZE)
292 != Some(expected_token.as_slice())
293 || bytes
294 .get(HEADER_RESERVED_OFFSET..RUST_LIBTEST_EVENT_HEADER_SIZE)
295 .is_none_or(|reserved| reserved != [0; 24])
296 {
297 return Err(RustLibtestEventError::InvalidHeader);
298 }
299
300 let mut offset = RUST_LIBTEST_EVENT_HEADER_SIZE;
301 let mut expected_sequence = 0_u64;
302 let mut events = Vec::new();
303 while offset < bytes.len() {
304 let remaining = &bytes[offset..];
305 if remaining.len() < RUST_LIBTEST_EVENT_RECORD_HEADER_SIZE {
306 return Err(RustLibtestEventError::TruncatedRecord(expected_sequence));
307 }
308 let record_length = get_u32(remaining, 0)
309 .and_then(|value| usize::try_from(value).ok())
310 .filter(|length| *length >= RUST_LIBTEST_EVENT_RECORD_HEADER_SIZE)
311 .ok_or(RustLibtestEventError::InvalidRecord(expected_sequence))?;
312 if record_length > remaining.len() {
313 return Err(RustLibtestEventError::TruncatedRecord(expected_sequence));
314 }
315 let record = &remaining[..record_length];
316 let actual_sequence = get_u64(record, RECORD_SEQUENCE_OFFSET)
317 .ok_or(RustLibtestEventError::InvalidRecord(expected_sequence))?;
318 if actual_sequence != expected_sequence {
319 return Err(RustLibtestEventError::InvalidSequence {
320 expected: expected_sequence,
321 actual: actual_sequence,
322 });
323 }
324 let name_length = get_u32(record, RECORD_NAME_LENGTH_OFFSET)
325 .and_then(|value| usize::try_from(value).ok())
326 .ok_or(RustLibtestEventError::InvalidRecord(expected_sequence))?;
327 if name_length > RUST_LIBTEST_EVENT_MAX_NAME_BYTES {
328 return Err(RustLibtestEventError::NameTooLong(expected_sequence));
329 }
330 if record_length != RUST_LIBTEST_EVENT_RECORD_HEADER_SIZE + name_length
331 || get_u32(record, RECORD_RESERVED_OFFSET) != Some(0)
332 {
333 return Err(RustLibtestEventError::InvalidRecord(expected_sequence));
334 }
335 let name_bytes = &record[RUST_LIBTEST_EVENT_RECORD_HEADER_SIZE..];
336 if get_u64(record, RECORD_CHECKSUM_OFFSET)
337 != Some(checksum(
338 expected_token,
339 &record[..RECORD_CHECKSUM_OFFSET],
340 name_bytes,
341 ))
342 {
343 return Err(RustLibtestEventError::InvalidRecord(expected_sequence));
344 }
345 let kind = record[RECORD_KIND_OFFSET];
346 let result = record[RECORD_RESULT_OFFSET];
347 let flags = get_u16(record, RECORD_FLAGS_OFFSET)
348 .ok_or(RustLibtestEventError::InvalidRecord(expected_sequence))?;
349 let count = get_u64(record, RECORD_COUNT_OFFSET)
350 .ok_or(RustLibtestEventError::InvalidRecord(expected_sequence))?;
351 let seed = get_u64(record, RECORD_SEED_OFFSET)
352 .ok_or(RustLibtestEventError::InvalidRecord(expected_sequence))?;
353 let name = if name_bytes.is_empty() {
354 None
355 } else {
356 Some(
357 std::str::from_utf8(name_bytes)
358 .map_err(|_| RustLibtestEventError::InvalidName(expected_sequence))?
359 .to_owned(),
360 )
361 };
362 let event = match (kind, result, flags, count, seed, name) {
363 (KIND_FILTERED_OUT, RESULT_NONE, 0, count, NO_SEED, None) => {
364 RustLibtestEvent::FilteredOut { count }
365 }
366 (KIND_FILTERED, RESULT_NONE, 0, count, NO_SEED, None) => RustLibtestEvent::Filtered {
367 count,
368 shuffle_seed: None,
369 },
370 (KIND_FILTERED, RESULT_NONE, FLAG_SHUFFLE_SEED, count, seed, None)
371 if seed != NO_SEED =>
372 {
373 RustLibtestEvent::Filtered {
374 count,
375 shuffle_seed: Some(seed),
376 }
377 }
378 (KIND_STARTED, RESULT_NONE, 0, 0, NO_SEED, Some(name)) if !name.is_empty() => {
379 RustLibtestEvent::Started { name }
380 }
381 (KIND_TIMEOUT, RESULT_NONE, 0, 0, NO_SEED, Some(name)) if !name.is_empty() => {
382 RustLibtestEvent::Timeout { name }
383 }
384 (KIND_FINISHED, result, 0, 0, NO_SEED, Some(name)) if !name.is_empty() => {
385 let result = match result {
386 RESULT_PASSED => RustLibtestTerminalResult::Passed,
387 RESULT_FAILED => RustLibtestTerminalResult::Failed,
388 RESULT_IGNORED => RustLibtestTerminalResult::Ignored,
389 RESULT_BENCHMARKED => RustLibtestTerminalResult::Benchmarked,
390 _ => return Err(RustLibtestEventError::InvalidRecord(expected_sequence)),
391 };
392 RustLibtestEvent::Finished { name, result }
393 }
394 _ => return Err(RustLibtestEventError::InvalidRecord(expected_sequence)),
395 };
396 events.push(event);
397 offset += record_length;
398 expected_sequence = expected_sequence
399 .checked_add(1)
400 .ok_or(RustLibtestEventError::InvalidRecord(actual_sequence))?;
401 }
402 if events.is_empty() {
403 return Err(RustLibtestEventError::EmptyStream);
404 }
405 Ok(events)
406}
407
408pub fn validate_rust_libtest_run_events(
409 events: &[RustLibtestEvent],
410 selected_tests: impl IntoIterator<Item = String>,
411) -> Result<RustLibtestRunEvents, RustLibtestEventError> {
412 let mut selected = BTreeSet::new();
413 for test in selected_tests {
414 if test.is_empty() || test.contains('\0') || !selected.insert(test.clone()) {
415 return Err(RustLibtestEventError::InvalidLifecycle(format!(
416 "selected test identity is invalid or duplicated: {test:?}"
417 )));
418 }
419 }
420 let [
421 RustLibtestEvent::FilteredOut {
422 count: filtered_out,
423 },
424 RustLibtestEvent::Filtered {
425 count,
426 shuffle_seed,
427 },
428 remaining @ ..,
429 ] = events
430 else {
431 return Err(RustLibtestEventError::InvalidLifecycle(
432 "stream must begin with exactly one filtered-out and filtered event".into(),
433 ));
434 };
435 if *count != selected.len() as u64 {
436 return Err(RustLibtestEventError::InvalidLifecycle(format!(
437 "libtest selected {count} tests but the authenticated catalog contains {}",
438 selected.len()
439 )));
440 }
441
442 let mut started_order = Vec::new();
443 let mut attempts = BTreeMap::<String, (bool, Option<RustLibtestTerminalResult>)>::new();
444 for event in remaining {
445 match event {
446 RustLibtestEvent::Started { name } => {
447 if !selected.contains(name)
448 || attempts.insert(name.clone(), (false, None)).is_some()
449 {
450 return Err(RustLibtestEventError::InvalidLifecycle(format!(
451 "test started outside the catalog or more than once: {name}"
452 )));
453 }
454 started_order.push(name.clone());
455 }
456 RustLibtestEvent::Timeout { name } => {
457 let Some((timed_out, result)) = attempts.get_mut(name) else {
458 return Err(RustLibtestEventError::InvalidLifecycle(format!(
459 "timeout preceded start: {name}"
460 )));
461 };
462 if *timed_out || result.is_some() {
463 return Err(RustLibtestEventError::InvalidLifecycle(format!(
464 "timeout was duplicated or followed a terminal result: {name}"
465 )));
466 }
467 *timed_out = true;
468 }
469 RustLibtestEvent::Finished { name, result } => {
470 let Some((_, terminal)) = attempts.get_mut(name) else {
471 return Err(RustLibtestEventError::InvalidLifecycle(format!(
472 "terminal result preceded start: {name}"
473 )));
474 };
475 if terminal.replace(*result).is_some() {
476 return Err(RustLibtestEventError::InvalidLifecycle(format!(
477 "terminal result was duplicated: {name}"
478 )));
479 }
480 }
481 RustLibtestEvent::FilteredOut { .. } | RustLibtestEvent::Filtered { .. } => {
482 return Err(RustLibtestEventError::InvalidLifecycle(
483 "aggregate selection event appeared after execution started".into(),
484 ));
485 }
486 }
487 }
488
489 let mut joined = Vec::with_capacity(started_order.len());
490 for name in started_order {
491 let (timed_out, result) = attempts.remove(&name).expect("started attempt exists");
492 let result = result.ok_or_else(|| {
493 RustLibtestEventError::InvalidLifecycle(format!(
494 "started test has no terminal result: {name}"
495 ))
496 })?;
497 joined.push(RustLibtestAttemptEvent {
498 name,
499 result,
500 timed_out,
501 });
502 }
503 let started = joined
504 .iter()
505 .map(|attempt| attempt.name.as_str())
506 .collect::<BTreeSet<_>>();
507 let unstarted = selected
508 .into_iter()
509 .filter(|name| !started.contains(name.as_str()))
510 .collect();
511 Ok(RustLibtestRunEvents {
512 filtered_out: *filtered_out,
513 shuffle_seed: *shuffle_seed,
514 attempts: joined,
515 unstarted,
516 })
517}
518
519#[cfg(test)]
520fn encode_event(
521 token: &[u8; RUST_LIBTEST_EVENT_TOKEN_SIZE],
522 sequence: u64,
523 event: &RustLibtestEvent,
524) -> Vec<u8> {
525 let (kind, result, flags, count, seed, name) = match event {
526 RustLibtestEvent::FilteredOut { count } => {
527 (KIND_FILTERED_OUT, RESULT_NONE, 0, *count, NO_SEED, "")
528 }
529 RustLibtestEvent::Filtered {
530 count,
531 shuffle_seed,
532 } => (
533 KIND_FILTERED,
534 RESULT_NONE,
535 u16::from(shuffle_seed.is_some()),
536 *count,
537 shuffle_seed.unwrap_or(NO_SEED),
538 "",
539 ),
540 RustLibtestEvent::Started { name } => {
541 (KIND_STARTED, RESULT_NONE, 0, 0, NO_SEED, name.as_str())
542 }
543 RustLibtestEvent::Timeout { name } => {
544 (KIND_TIMEOUT, RESULT_NONE, 0, 0, NO_SEED, name.as_str())
545 }
546 RustLibtestEvent::Finished { name, result } => (
547 KIND_FINISHED,
548 match result {
549 RustLibtestTerminalResult::Passed => RESULT_PASSED,
550 RustLibtestTerminalResult::Failed => RESULT_FAILED,
551 RustLibtestTerminalResult::Ignored => RESULT_IGNORED,
552 RustLibtestTerminalResult::Benchmarked => RESULT_BENCHMARKED,
553 },
554 0,
555 0,
556 NO_SEED,
557 name.as_str(),
558 ),
559 };
560 let mut record = vec![0_u8; RUST_LIBTEST_EVENT_RECORD_HEADER_SIZE + name.len()];
561 let record_len = record.len() as u32;
562 put_u32(&mut record, 0, record_len);
563 record[RECORD_KIND_OFFSET] = kind;
564 record[RECORD_RESULT_OFFSET] = result;
565 put_u16(&mut record, RECORD_FLAGS_OFFSET, flags);
566 put_u64(&mut record, RECORD_SEQUENCE_OFFSET, sequence);
567 put_u64(&mut record, RECORD_COUNT_OFFSET, count);
568 put_u64(&mut record, RECORD_SEED_OFFSET, seed);
569 put_u32(&mut record, RECORD_NAME_LENGTH_OFFSET, name.len() as u32);
570 record[RUST_LIBTEST_EVENT_RECORD_HEADER_SIZE..].copy_from_slice(name.as_bytes());
571 let digest = checksum(token, &record[..RECORD_CHECKSUM_OFFSET], name.as_bytes());
572 put_u64(&mut record, RECORD_CHECKSUM_OFFSET, digest);
573 record
574}
575
576#[cfg(test)]
577mod tests {
578 use std::{
579 io::Write as _,
580 sync::atomic::{AtomicU64, Ordering},
581 time::{SystemTime, UNIX_EPOCH},
582 };
583
584 use super::*;
585
586 fn fixture() -> (std::path::PathBuf, [u8; RUST_LIBTEST_EVENT_TOKEN_SIZE]) {
587 static NEXT_FIXTURE: AtomicU64 = AtomicU64::new(0);
588 let nonce = SystemTime::now()
589 .duration_since(UNIX_EPOCH)
590 .unwrap()
591 .as_nanos();
592 let root = std::env::temp_dir().join(format!(
593 "supercov-libtest-events-{}-{nonce}-{}",
594 std::process::id(),
595 NEXT_FIXTURE.fetch_add(1, Ordering::Relaxed)
596 ));
597 fs::create_dir_all(&root).unwrap();
598 (
599 root.join("events.bin"),
600 [0x5a; RUST_LIBTEST_EVENT_TOKEN_SIZE],
601 )
602 }
603
604 #[test]
605 fn joins_concurrent_terminal_order_and_fail_fast_unstarted_tests() {
606 let events = vec![
607 RustLibtestEvent::FilteredOut { count: 3 },
608 RustLibtestEvent::Filtered {
609 count: 3,
610 shuffle_seed: Some(42),
611 },
612 RustLibtestEvent::Started { name: "b".into() },
613 RustLibtestEvent::Started { name: "a".into() },
614 RustLibtestEvent::Timeout { name: "b".into() },
615 RustLibtestEvent::Finished {
616 name: "a".into(),
617 result: RustLibtestTerminalResult::Passed,
618 },
619 RustLibtestEvent::Finished {
620 name: "b".into(),
621 result: RustLibtestTerminalResult::Failed,
622 },
623 ];
624 let run = validate_rust_libtest_run_events(
625 &events,
626 ["a".to_owned(), "b".to_owned(), "c".to_owned()],
627 )
628 .unwrap();
629 assert_eq!(run.filtered_out, 3);
630 assert_eq!(run.shuffle_seed, Some(42));
631 assert_eq!(
632 run.attempts,
633 vec![
634 RustLibtestAttemptEvent {
635 name: "b".into(),
636 result: RustLibtestTerminalResult::Failed,
637 timed_out: true,
638 },
639 RustLibtestAttemptEvent {
640 name: "a".into(),
641 result: RustLibtestTerminalResult::Passed,
642 timed_out: false,
643 },
644 ]
645 );
646 assert_eq!(run.unstarted, ["c"]);
647
648 let mut malformed = events;
649 malformed.push(RustLibtestEvent::Finished {
650 name: "b".into(),
651 result: RustLibtestTerminalResult::Passed,
652 });
653 assert!(matches!(
654 validate_rust_libtest_run_events(
655 &malformed,
656 ["a".to_owned(), "b".to_owned(), "c".to_owned()]
657 ),
658 Err(RustLibtestEventError::InvalidLifecycle(_))
659 ));
660 }
661
662 fn append(path: &Path, bytes: &[u8]) {
663 let mut file = OpenOptions::new().append(true).open(path).unwrap();
664 file.write_all(bytes).unwrap();
665 file.flush().unwrap();
666 }
667
668 #[test]
669 fn roundtrips_every_frozen_event_kind() {
670 let (path, token) = fixture();
671 create_rust_libtest_event_file(&path, token).unwrap();
672 let expected = vec![
673 RustLibtestEvent::FilteredOut { count: 2 },
674 RustLibtestEvent::Filtered {
675 count: 4,
676 shuffle_seed: Some(91),
677 },
678 RustLibtestEvent::Started {
679 name: "tests::works".into(),
680 },
681 RustLibtestEvent::Timeout {
682 name: "tests::works".into(),
683 },
684 RustLibtestEvent::Finished {
685 name: "tests::works".into(),
686 result: RustLibtestTerminalResult::Passed,
687 },
688 RustLibtestEvent::Finished {
689 name: "tests::ignored".into(),
690 result: RustLibtestTerminalResult::Ignored,
691 },
692 RustLibtestEvent::Finished {
693 name: "bench::one".into(),
694 result: RustLibtestTerminalResult::Benchmarked,
695 },
696 RustLibtestEvent::Finished {
697 name: "tests::fails".into(),
698 result: RustLibtestTerminalResult::Failed,
699 },
700 ];
701 for (sequence, event) in expected.iter().enumerate() {
702 append(&path, &encode_event(&token, sequence as u64, event));
703 }
704 assert_eq!(read_rust_libtest_events(&path, &token).unwrap(), expected);
705 fs::remove_dir_all(path.parent().unwrap()).unwrap();
706 }
707
708 #[test]
709 fn rejects_wrong_token_empty_partial_reordered_and_tampered_streams() {
710 let (path, token) = fixture();
711 create_rust_libtest_event_file(&path, token).unwrap();
712 assert_eq!(
713 read_rust_libtest_events(&path, &token),
714 Err(RustLibtestEventError::EmptyStream)
715 );
716 assert_eq!(
717 read_rust_libtest_events(&path, &[0; RUST_LIBTEST_EVENT_TOKEN_SIZE]),
718 Err(RustLibtestEventError::InvalidHeader)
719 );
720
721 let event = RustLibtestEvent::Started {
722 name: "tests::works".into(),
723 };
724 let encoded = encode_event(&token, 0, &event);
725 append(&path, &encoded[..encoded.len() - 1]);
726 assert_eq!(
727 read_rust_libtest_events(&path, &token),
728 Err(RustLibtestEventError::TruncatedRecord(0))
729 );
730 fs::remove_file(&path).unwrap();
731
732 create_rust_libtest_event_file(&path, token).unwrap();
733 append(&path, &encode_event(&token, 1, &event));
734 assert_eq!(
735 read_rust_libtest_events(&path, &token),
736 Err(RustLibtestEventError::InvalidSequence {
737 expected: 0,
738 actual: 1,
739 })
740 );
741 fs::remove_file(&path).unwrap();
742
743 create_rust_libtest_event_file(&path, token).unwrap();
744 let mut tampered = encode_event(&token, 0, &event);
745 *tampered.last_mut().unwrap() ^= 1;
746 append(&path, &tampered);
747 assert_eq!(
748 read_rust_libtest_events(&path, &token),
749 Err(RustLibtestEventError::InvalidRecord(0))
750 );
751 fs::remove_dir_all(path.parent().unwrap()).unwrap();
752 }
753
754 #[test]
755 fn rejects_unknown_kind_flags_result_and_non_utf8_name() {
756 for mutate in [
757 |record: &mut Vec<u8>| record[RECORD_KIND_OFFSET] = 99,
758 |record: &mut Vec<u8>| put_u16(record, RECORD_FLAGS_OFFSET, 2),
759 |record: &mut Vec<u8>| record[RECORD_RESULT_OFFSET] = RESULT_FAILED,
760 |record: &mut Vec<u8>| *record.last_mut().unwrap() = 0xff,
761 ] {
762 let (path, token) = fixture();
763 create_rust_libtest_event_file(&path, token).unwrap();
764 let mut record =
765 encode_event(&token, 0, &RustLibtestEvent::Started { name: "x".into() });
766 mutate(&mut record);
767 let name = &record[RUST_LIBTEST_EVENT_RECORD_HEADER_SIZE..];
768 let digest = checksum(&token, &record[..RECORD_CHECKSUM_OFFSET], name);
769 put_u64(&mut record, RECORD_CHECKSUM_OFFSET, digest);
770 append(&path, &record);
771 assert!(read_rust_libtest_events(&path, &token).is_err());
772 fs::remove_dir_all(path.parent().unwrap()).unwrap();
773 }
774 }
775
776 #[cfg(unix)]
777 #[test]
778 fn rejects_symlink_transport() {
779 use std::os::unix::fs::symlink;
780
781 let (path, token) = fixture();
782 let real = path.with_file_name("real.bin");
783 create_rust_libtest_event_file(&real, token).unwrap();
784 append(
785 &real,
786 &encode_event(&token, 0, &RustLibtestEvent::FilteredOut { count: 0 }),
787 );
788 symlink(&real, &path).unwrap();
789 assert_eq!(
790 read_rust_libtest_events(&path, &token),
791 Err(RustLibtestEventError::UnsafeFile(
792 path.display().to_string()
793 ))
794 );
795 fs::remove_dir_all(path.parent().unwrap()).unwrap();
796 }
797}