Skip to main content

supercov_engine/
rust_libtest_events.rs

1//! Authenticated, crash-detecting outcomes emitted by Supercov's exact-version
2//! libtest companion.
3//!
4//! This transport deliberately does not infer outcomes from libtest's human
5//! output. The selected toolchain's own libtest remains the scheduling,
6//! capture and presentation authority and appends one strict binary event for
7//! each callback it handles. A partial or semantically invalid stream is a
8//! fatal attribution error.
9
10use 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}