#![doc = include_str!("../README.md")]
use std::io::Write as _;
#[derive(Debug)]
pub enum Error {
Write(std::io::Error),
UnknownId(Id),
DoubleRetire(Id),
DanglingIds(Vec<Id>),
InvalidLabel(String),
}
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Write(error) => write!(f, "write failed: {error}"),
Self::UnknownId(id) => write!(f, "unknown instruction id: {id}"),
Self::DoubleRetire(id) => {
write!(f, "instruction {id} retired or flushed more than once")
}
Self::DanglingIds(ids) => {
write!(
f,
"trace finished with {} dangling instruction(s): {ids:?}",
ids.len()
)
}
Self::InvalidLabel(label) => write!(f, "incompatible label name: {label:?}"),
}
}
}
impl std::error::Error for Error {}
impl From<std::io::Error> for Error {
fn from(value: std::io::Error) -> Self {
Self::Write(value)
}
}
pub type Result<T = ()> = std::result::Result<T, Error>;
pub trait Config {
type Output: std::io::Write;
type Stage: std::fmt::Display;
const VALIDATE: bool = true;
const SANITIZE: bool = true;
}
#[derive(Debug, Default, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord)]
pub struct Id(u32);
impl std::fmt::Display for Id {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_fmt(format_args!("{}", self.0))
}
}
impl Id {
const fn new(value: u32) -> Self {
Self(value)
}
const fn get(self) -> u32 {
self.0
}
const fn bump(&mut self) -> Self {
let old = self.0;
self.0 = old + 1;
Self(old)
}
const fn index(self) -> usize {
self.0 as _
}
}
pub type Lane = u32;
#[derive(Clone, Copy)]
pub enum Level {
Pane,
Hover,
}
impl std::fmt::Display for Level {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Pane => f.write_str("0"),
Self::Hover => f.write_str("1"),
}
}
}
struct Validator<C: Config> {
live: fixedbitset::FixedBitSet,
phantom: std::marker::PhantomData<C>,
}
impl<C: Config> Default for Validator<C> {
fn default() -> Self {
Self {
live: fixedbitset::FixedBitSet::new(),
phantom: std::marker::PhantomData,
}
}
}
impl<C: Config> Validator<C> {
fn track(&mut self, id: Id) {
let was_present = self.live.contains(id.index());
self.live.grow_and_insert(id.index());
debug_assert!(!was_present, "validator received a duplicate id: {id}");
}
fn check(&self, id: Id) -> Result {
if id.index() < self.live.len() && self.live.contains(id.index()) {
Ok(())
} else {
Err(Error::UnknownId(id))
}
}
fn forget(&mut self, id: Id) -> Result {
if !self.live.contains(id.index()) {
return Err(Error::DoubleRetire(id));
}
self.live.remove(id.index());
Ok(())
}
fn orphaned(&self) -> Result {
let ids: Vec<Id> = self
.live
.ones()
.map(|index| {
#[allow(clippy::cast_possible_truncation)]
let id = index as u32;
Id::new(id)
})
.collect();
ids.is_empty().then_some(()).ok_or(Error::DanglingIds(ids))
}
}
struct Sanitizer;
impl Sanitizer {
fn sanitize<T>(text: &T) -> Result
where
T: std::fmt::Display,
{
let string = text.to_string();
#[allow(clippy::items_after_statements)]
const FORBIDDEN: &[&str] = &["\t", "\n", "\r"];
for forbidden in FORBIDDEN {
if string.contains(forbidden) {
return Err(Error::InvalidLabel(string));
}
}
Ok(())
}
}
pub struct Trace<C: Config> {
output: C::Output,
validator: Validator<C>,
instruction_id: Id,
retirement_id: Id,
}
impl<C: Config> Default for Trace<C>
where
C::Output: Default,
{
fn default() -> Self {
let cycle = 0;
let output = C::Output::default();
Self::new(cycle, output).expect("default parameters should not panic")
}
}
impl<C: Config> Trace<C> {
fn write(&mut self, args: std::fmt::Arguments<'_>) -> Result {
self.output.write_fmt(format_args!("{args}\n"))?;
Ok(())
}
}
impl<C: Config> Trace<C> {
pub fn new(cycle: u32, output: C::Output) -> Result<Self> {
let mut me = Self {
output,
validator: Validator::default(),
instruction_id: Id::new(0),
retirement_id: Id::new(0),
};
me.write(format_args!("Kanata\t0004"))?;
me.write(format_args!("C=\t{cycle}"))?;
Ok(me)
}
#[must_use = "`finish` surrenders the underlying sink and runs final \
validation; dropping the result discards both the produced \
bytes and any `DanglingIds` diagnostic"]
pub fn finish(self) -> Result<C::Output> {
if C::VALIDATE {
self.validator.orphaned()?;
}
Ok(self.output)
}
pub fn advance(&mut self, delta: u32) -> Result {
self.write(format_args!("C\t{delta}"))
}
#[must_use = "the returned `Id` is the only handle to the instruction \
just emitted; discarding it makes every later command for \
this instruction unreachable and guarantees `DanglingIds` \
on `finish`"]
pub fn start(&mut self, id: u32, thread: u32) -> Result<Id> {
let iid = self.instruction_id.bump();
if C::VALIDATE {
self.validator.track(iid);
}
self.write(format_args!("I\t{iid}\t{id}\t{thread}"))?;
Ok(iid)
}
pub fn end(&mut self, id: Id, lane: Lane, stage: &C::Stage) -> Result {
if C::VALIDATE {
self.validator.check(id)?;
Sanitizer::sanitize(&stage)?;
}
self.write(format_args!("E\t{id}\t{lane}\t{stage}"))
}
pub fn label<T>(&mut self, id: Id, text: T, level: Level) -> Result
where
T: std::fmt::Display,
{
if C::VALIDATE {
self.validator.check(id)?;
}
if C::SANITIZE {
Sanitizer::sanitize(&text)?;
}
self.write(format_args!("L\t{id}\t{level}\t{text}"))
}
pub fn stage(&mut self, id: Id, lane: Lane, stage: &C::Stage, dependency: bool) -> Result {
if C::VALIDATE {
self.validator.check(id)?;
Sanitizer::sanitize(&stage)?;
}
let marker = if dependency { "_X" } else { <&str>::default() };
self.write(format_args!("S\t{id}\t{lane}\t{stage}{marker}"))
}
pub fn retire(&mut self, id: Id) -> Result {
if C::VALIDATE {
self.validator.forget(id)?;
}
let rid = self.retirement_id.bump();
self.write(format_args!("R\t{id}\t{rid}\t0"))
}
pub fn flush(&mut self, id: Id) -> Result {
if C::VALIDATE {
self.validator.forget(id)?;
}
let rid = self.retirement_id.get();
self.write(format_args!("R\t{id}\t{rid}\t1"))
}
pub fn wake(&mut self, consumer: Id, producer: Id) -> Result {
if C::VALIDATE {
self.validator.check(consumer)?;
}
self.write(format_args!("W\t{consumer}\t{producer}\t0"))
}
}
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
struct Cfg;
impl Config for Cfg {
type Output = Vec<u8>;
type Stage = &'static str;
}
fn make(cycle: u32) -> Trace<Cfg> {
let output = Vec::new();
Trace::new(cycle, output).unwrap()
}
#[allow(clippy::unnecessary_wraps)]
fn success(trace: Trace<Cfg>, expected: &'static str) -> Result {
let bytes = trace.finish().unwrap();
let actual = std::str::from_utf8(&bytes).unwrap();
assert_eq!(actual, expected);
Ok(())
}
#[allow(clippy::unnecessary_wraps)]
fn failure(trace: Trace<Cfg>, message: &'static str) -> Result {
let error = trace.finish().unwrap_err().to_string();
assert_eq!(&error, message);
Ok(())
}
#[test]
fn header_carries_initial_cycle() -> Result {
let trace = make(42);
success(trace, concat!("Kanata\t0004\n", "C=\t42\n",))
}
#[test]
fn default_carries_cycle_zero() -> Result {
let trace = Trace::<Cfg>::default();
success(trace, concat!("Kanata\t0004\n", "C=\t0\n",))
}
#[test]
fn advance_emits_delta() -> Result {
let mut trace = make(0);
trace.advance(3)?;
trace.advance(1)?;
success(
trace,
concat!("Kanata\t0004\n", "C=\t0\n", "C\t3\n", "C\t1\n",),
)
}
#[test]
fn start_hands_out_monotonic_ids() -> Result {
let mut trace = make(0);
let a = trace.start(10, 0)?;
let b = trace.start(11, 1)?;
let c = trace.start(12, 0)?;
assert_eq!(a.get(), 0);
assert_eq!(b.get(), 1);
assert_eq!(c.get(), 2);
trace.retire(a)?;
trace.retire(b)?;
trace.retire(c)?;
success(
trace,
concat!(
"Kanata\t0004\n",
"C=\t0\n",
"I\t0\t10\t0\n",
"I\t1\t11\t1\n",
"I\t2\t12\t0\n",
"R\t0\t0\t0\n",
"R\t1\t1\t0\n",
"R\t2\t2\t0\n",
),
)
}
#[test]
fn end_emits_stage_boundary() -> Result {
let mut trace = make(0);
let id = trace.start(0, 0)?;
trace.stage(id, 0, &"fetch", false)?;
trace.end(id, 0, &"fetch")?;
trace.retire(id)?;
success(
trace,
concat!(
"Kanata\t0004\n",
"C=\t0\n",
"I\t0\t0\t0\n",
"S\t0\t0\tfetch\n",
"E\t0\t0\tfetch\n",
"R\t0\t0\t0\n",
),
)
}
#[test]
fn stages_chain_without_explicit_end() -> Result {
let mut trace = make(0);
let id = trace.start(0, 0)?;
trace.stage(id, 0, &"fetch", false)?;
trace.stage(id, 0, &"decode", false)?;
trace.retire(id)?;
success(
trace,
concat!(
"Kanata\t0004\n",
"C=\t0\n",
"I\t0\t0\t0\n",
"S\t0\t0\tfetch\n",
"S\t0\t0\tdecode\n",
"R\t0\t0\t0\n",
),
)
}
#[test]
fn stage_with_dependency_marker() -> Result {
let mut trace = make(0);
let id = trace.start(0, 0)?;
trace.stage(id, 0, &"exec", true)?;
trace.retire(id)?;
success(
trace,
concat!(
"Kanata\t0004\n",
"C=\t0\n",
"I\t0\t0\t0\n",
"S\t0\t0\texec_X\n",
"R\t0\t0\t0\n",
),
)
}
#[test]
fn lane_index_is_preserved() -> Result {
let mut trace = make(0);
let id = trace.start(0, 0)?;
trace.stage(id, 7, &"stall", false)?;
trace.end(id, 7, &"stall")?;
trace.retire(id)?;
success(
trace,
concat!(
"Kanata\t0004\n",
"C=\t0\n",
"I\t0\t0\t0\n",
"S\t0\t7\tstall\n",
"E\t0\t7\tstall\n",
"R\t0\t0\t0\n",
),
)
}
#[test]
fn label_pane_and_hover_use_distinct_types() -> Result {
let mut trace = make(0);
let id = trace.start(0, 0)?;
trace.label(id, "0x100: addi", Level::Pane)?;
trace.label(id, "rs1=5 rs2=7", Level::Hover)?;
trace.retire(id)?;
success(
trace,
concat!(
"Kanata\t0004\n",
"C=\t0\n",
"I\t0\t0\t0\n",
"L\t0\t0\t0x100: addi\n",
"L\t0\t1\trs1=5 rs2=7\n",
"R\t0\t0\t0\n",
),
)
}
#[test]
fn flush_reuses_retirement_id_then_retire_bumps() -> Result {
let mut trace = make(0);
let a = trace.start(0, 0)?;
let b = trace.start(1, 0)?;
let c = trace.start(2, 0)?;
trace.retire(a)?; trace.flush(b)?; trace.retire(c)?; success(
trace,
concat!(
"Kanata\t0004\n",
"C=\t0\n",
"I\t0\t0\t0\n",
"I\t1\t1\t0\n",
"I\t2\t2\t0\n",
"R\t0\t0\t0\n",
"R\t1\t1\t1\n",
"R\t2\t1\t0\n",
),
)
}
#[test]
fn wake_allows_retired_producer() -> Result {
let mut trace = make(0);
let producer = trace.start(0, 0)?;
let consumer = trace.start(1, 0)?;
trace.retire(producer)?;
trace.wake(consumer, producer)?;
trace.retire(consumer)?;
success(
trace,
concat!(
"Kanata\t0004\n",
"C=\t0\n",
"I\t0\t0\t0\n",
"I\t1\t1\t0\n",
"R\t0\t0\t0\n",
"W\t1\t0\t0\n",
"R\t1\t1\t0\n",
),
)
}
#[test]
fn stage_on_unknown_id_errors() {
let mut trace = make(0);
let bogus = Id::new(99);
let err = trace.stage(bogus, 0, &"fetch", false).unwrap_err();
assert!(matches!(err, Error::UnknownId(id) if id == bogus));
}
#[test]
fn end_on_unknown_id_errors() {
let mut trace = make(0);
let err = trace.end(Id::new(5), 0, &"fetch").unwrap_err();
assert!(matches!(err, Error::UnknownId(id) if id == Id::new(5)));
}
#[test]
fn label_on_unknown_id_errors() {
let mut trace = make(0);
let err = trace.label(Id::new(5), "x", Level::Pane).unwrap_err();
assert!(matches!(err, Error::UnknownId(id) if id == Id::new(5)));
}
#[test]
fn wake_on_unknown_consumer_errors() {
let mut trace = make(0);
let err = trace.wake(Id::new(5), Id::new(0)).unwrap_err();
assert!(matches!(err, Error::UnknownId(id) if id == Id::new(5)));
}
#[test]
fn stage_after_retire_errors() -> Result {
let mut trace = make(0);
let id = trace.start(0, 0)?;
trace.retire(id)?;
let err = trace.stage(id, 0, &"fetch", false).unwrap_err();
assert!(matches!(err, Error::UnknownId(x) if x == id));
Ok(())
}
#[test]
fn double_retire_errors() -> Result {
let mut trace = make(0);
let id = trace.start(0, 0)?;
trace.retire(id)?;
let err = trace.retire(id).unwrap_err();
assert!(matches!(err, Error::DoubleRetire(x) if x == id));
Ok(())
}
#[test]
fn flush_after_retire_errors() -> Result {
let mut trace = make(0);
let id = trace.start(0, 0)?;
trace.retire(id)?;
let err = trace.flush(id).unwrap_err();
assert!(matches!(err, Error::DoubleRetire(x) if x == id));
Ok(())
}
#[test]
fn dangling_ids_reported_on_finish() -> Result {
let mut trace = make(0);
let _ = trace.start(0, 0)?;
let _ = trace.start(1, 0)?;
let bytes = trace.finish().unwrap_err();
match bytes {
Error::DanglingIds(mut ids) => {
ids.sort();
assert_eq!(ids, vec![Id::new(0), Id::new(1)]);
}
other => panic!("expected DanglingIds, got {other:?}"),
}
Ok(())
}
#[test]
fn unfinished_trace_message() -> Result {
let mut trace = make(0);
let _ = trace.start(0, 0)?;
failure(
trace,
"trace finished with 1 dangling instruction(s): [Id(0)]",
)
}
#[test]
fn error_display_variants() {
let io = std::io::Error::other("disk full");
assert_eq!(format!("{}", Error::Write(io)), "write failed: disk full");
assert_eq!(
format!("{}", Error::UnknownId(Id::new(7))),
"unknown instruction id: 7"
);
assert_eq!(
format!("{}", Error::DoubleRetire(Id::new(3))),
"instruction 3 retired or flushed more than once",
);
}
#[test]
fn io_error_converts_into_write() {
let io = std::io::Error::other("nope");
let err: Error = io.into();
assert!(matches!(err, Error::Write(_)));
}
#[test]
fn id_display_matches_inner() {
assert_eq!(format!("{}", Id::new(0)), "0");
assert_eq!(format!("{}", Id::new(12345)), "12345");
}
#[test]
fn level_display_uses_numeric_encoding() {
assert_eq!(format!("{}", Level::Pane), "0");
assert_eq!(format!("{}", Level::Hover), "1");
}
#[test]
fn advance_zero_emits_command() -> Result {
let mut trace = make(0);
trace.advance(0)?;
success(trace, concat!("Kanata\t0004\n", "C=\t0\n", "C\t0\n",))
}
#[test]
fn advance_large_delta() -> Result {
let mut trace = make(0);
trace.advance(u32::MAX)?;
success(
trace,
concat!("Kanata\t0004\n", "C=\t0\n", "C\t4294967295\n",),
)
}
#[test]
fn initial_cycle_max_value() -> Result {
let trace = make(u32::MAX);
success(trace, concat!("Kanata\t0004\n", "C=\t4294967295\n",))
}
#[test]
fn multi_lane_overlay_same_instruction() -> Result {
let mut trace = make(0);
let id = trace.start(0, 0)?;
trace.stage(id, 0, &"fetch", false)?;
trace.stage(id, 1, &"stall", false)?;
trace.end(id, 1, &"stall")?;
trace.retire(id)?;
success(
trace,
concat!(
"Kanata\t0004\n",
"C=\t0\n",
"I\t0\t0\t0\n",
"S\t0\t0\tfetch\n",
"S\t0\t1\tstall\n",
"E\t0\t1\tstall\n",
"R\t0\t0\t0\n",
),
)
}
#[test]
fn ids_keep_growing_after_retire() -> Result {
let mut trace = make(0);
let a = trace.start(0, 0)?;
trace.retire(a)?;
let b = trace.start(1, 0)?;
trace.retire(b)?;
assert_eq!(a.get(), 0);
assert_eq!(b.get(), 1);
success(
trace,
concat!(
"Kanata\t0004\n",
"C=\t0\n",
"I\t0\t0\t0\n",
"R\t0\t0\t0\n",
"I\t1\t1\t0\n",
"R\t1\t1\t0\n",
),
)
}
#[test]
fn label_between_stages_is_allowed() -> Result {
let mut trace = make(0);
let id = trace.start(0, 0)?;
trace.stage(id, 0, &"fetch", false)?;
trace.label(id, "mid", Level::Hover)?;
trace.stage(id, 0, &"decode", false)?;
trace.retire(id)?;
success(
trace,
concat!(
"Kanata\t0004\n",
"C=\t0\n",
"I\t0\t0\t0\n",
"S\t0\t0\tfetch\n",
"L\t0\t1\tmid\n",
"S\t0\t0\tdecode\n",
"R\t0\t0\t0\n",
),
)
}
#[test]
fn multiple_wakes_for_one_consumer() -> Result {
let mut trace = make(0);
let p1 = trace.start(0, 0)?;
let p2 = trace.start(1, 0)?;
let consumer = trace.start(2, 0)?;
trace.retire(p1)?;
trace.wake(consumer, p1)?;
trace.wake(consumer, p2)?;
trace.retire(p2)?;
trace.retire(consumer)?;
success(
trace,
concat!(
"Kanata\t0004\n",
"C=\t0\n",
"I\t0\t0\t0\n",
"I\t1\t1\t0\n",
"I\t2\t2\t0\n",
"R\t0\t0\t0\n",
"W\t2\t0\t0\n",
"W\t2\t1\t0\n",
"R\t1\t1\t0\n",
"R\t2\t2\t0\n",
),
)
}
#[test]
fn wake_accepts_unissued_producer() -> Result {
let mut trace = make(0);
let consumer = trace.start(0, 0)?;
trace.wake(consumer, Id::new(999))?;
trace.retire(consumer)?;
success(
trace,
concat!(
"Kanata\t0004\n",
"C=\t0\n",
"I\t0\t0\t0\n",
"W\t0\t999\t0\n",
"R\t0\t0\t0\n",
),
)
}
#[test]
fn flush_alone_closes_lifecycle() -> Result {
let mut trace = make(0);
let id = trace.start(0, 0)?;
trace.flush(id)?;
success(
trace,
concat!("Kanata\t0004\n", "C=\t0\n", "I\t0\t0\t0\n", "R\t0\t0\t1\n",),
)
}
#[test]
fn advance_then_stage_uses_new_cycle_implicitly() -> Result {
let mut trace = make(0);
let id = trace.start(0, 0)?;
trace.stage(id, 0, &"fetch", false)?;
trace.advance(1)?;
trace.stage(id, 0, &"decode", false)?;
trace.advance(2)?;
trace.retire(id)?;
success(
trace,
concat!(
"Kanata\t0004\n",
"C=\t0\n",
"I\t0\t0\t0\n",
"S\t0\t0\tfetch\n",
"C\t1\n",
"S\t0\t0\tdecode\n",
"C\t2\n",
"R\t0\t0\t0\n",
),
)
}
#[test]
fn start_passes_through_sim_id_and_thread() -> Result {
let mut trace = make(0);
let id = trace.start(u32::MAX, 7)?;
trace.retire(id)?;
success(
trace,
concat!(
"Kanata\t0004\n",
"C=\t0\n",
"I\t0\t4294967295\t7\n",
"R\t0\t0\t0\n",
),
)
}
#[test]
fn label_display_uses_user_impl() -> Result {
struct Wrapped(u32);
impl std::fmt::Display for Wrapped {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "wrapped({})", self.0)
}
}
let mut trace = make(0);
let id = trace.start(0, 0)?;
trace.label(id, Wrapped(42), Level::Pane)?;
trace.retire(id)?;
success(
trace,
concat!(
"Kanata\t0004\n",
"C=\t0\n",
"I\t0\t0\t0\n",
"L\t0\t0\twrapped(42)\n",
"R\t0\t0\t0\n",
),
)
}
#[test]
fn finish_after_success_returns_full_bytes() -> Result {
let mut trace = make(0);
let id = trace.start(0, 0)?;
trace.retire(id)?;
let bytes = trace.finish()?;
assert_eq!(
std::str::from_utf8(&bytes).unwrap(),
"Kanata\t0004\nC=\t0\nI\t0\t0\t0\nR\t0\t0\t0\n",
);
Ok(())
}
struct CfgNoValidate;
impl Config for CfgNoValidate {
type Output = Vec<u8>;
type Stage = &'static str;
const VALIDATE: bool = false;
const SANITIZE: bool = true;
}
struct CfgNoSanitize;
impl Config for CfgNoSanitize {
type Output = Vec<u8>;
type Stage = &'static str;
const VALIDATE: bool = true;
const SANITIZE: bool = false;
}
#[test]
fn validate_disabled_bypasses_lifecycle_checks() -> Result {
let mut trace = Trace::<CfgNoValidate>::new(0, Vec::new())?;
let bogus = Id::new(99);
trace.stage(bogus, 0, &"fetch", false)?;
trace.end(bogus, 0, &"fetch")?;
trace.label(bogus, "x", Level::Pane)?;
trace.wake(bogus, Id::new(123))?;
trace.retire(bogus)?;
trace.flush(bogus)?;
let bytes = trace.finish()?;
let text = std::str::from_utf8(&bytes).unwrap();
assert!(text.starts_with("Kanata\t0004\nC=\t0\n"));
assert!(text.contains("S\t99\t0\tfetch\n"));
assert!(text.contains("R\t99\t0\t0\n"));
Ok(())
}
#[test]
fn validate_disabled_finish_ignores_dangling() -> Result {
let mut trace = Trace::<CfgNoValidate>::new(0, Vec::new())?;
let _ = trace.start(0, 0)?;
let _ = trace.start(1, 0)?;
let _ = trace.finish()?;
Ok(())
}
#[test]
fn sanitize_disabled_passes_forbidden_chars() -> Result {
let mut trace = Trace::<CfgNoSanitize>::new(0, Vec::new())?;
let id = trace.start(0, 0)?;
trace.label(id, "tab\there", Level::Pane)?;
trace.retire(id)?;
let bytes = trace.finish()?;
let text = std::str::from_utf8(&bytes).unwrap();
assert!(text.contains("L\t0\t0\ttab\there\n"));
Ok(())
}
#[test]
fn label_with_tab_is_rejected() -> Result {
let mut trace = make(0);
let id = trace.start(0, 0)?;
let err = trace.label(id, "bad\ttext", Level::Pane).unwrap_err();
assert!(matches!(err, Error::InvalidLabel(s) if s == "bad\ttext"));
trace.retire(id)?;
Ok(())
}
#[test]
fn label_with_newline_is_rejected() -> Result {
let mut trace = make(0);
let id = trace.start(0, 0)?;
let err = trace.label(id, "line\nbreak", Level::Pane).unwrap_err();
assert!(matches!(err, Error::InvalidLabel(s) if s == "line\nbreak"));
trace.retire(id)?;
Ok(())
}
#[test]
fn label_with_carriage_return_is_rejected() -> Result {
let mut trace = make(0);
let id = trace.start(0, 0)?;
let err = trace.label(id, "cr\rhere", Level::Pane).unwrap_err();
assert!(matches!(err, Error::InvalidLabel(s) if s == "cr\rhere"));
trace.retire(id)?;
Ok(())
}
#[test]
fn rejected_label_does_not_emit() -> Result {
let mut trace = make(0);
let id = trace.start(0, 0)?;
let _ = trace.label(id, "x\ty", Level::Pane);
trace.retire(id)?;
let bytes = trace.finish()?;
let text = std::str::from_utf8(&bytes).unwrap();
assert!(!text.contains("x\ty"));
assert!(!text.contains('L'));
Ok(())
}
#[test]
fn invalid_label_display() {
let err = Error::InvalidLabel("bad\tlabel".into());
assert_eq!(format!("{err}"), "incompatible label name: \"bad\\tlabel\"");
}
#[test]
fn wake_after_consumer_retired_errors() -> Result {
let mut trace = make(0);
let producer = trace.start(0, 0)?;
let consumer = trace.start(1, 0)?;
trace.retire(consumer)?;
let err = trace.wake(consumer, producer).unwrap_err();
assert!(matches!(err, Error::UnknownId(id) if id == consumer));
trace.retire(producer)?;
Ok(())
}
#[test]
fn label_after_retire_errors() -> Result {
let mut trace = make(0);
let id = trace.start(0, 0)?;
trace.retire(id)?;
let err = trace.label(id, "post", Level::Pane).unwrap_err();
assert!(matches!(err, Error::UnknownId(x) if x == id));
Ok(())
}
#[test]
fn end_after_retire_errors() -> Result {
let mut trace = make(0);
let id = trace.start(0, 0)?;
trace.retire(id)?;
let err = trace.end(id, 0, &"fetch").unwrap_err();
assert!(matches!(err, Error::UnknownId(x) if x == id));
Ok(())
}
#[test]
fn small_pipeline_three_instructions() -> Result {
let mut trace = make(0);
let a = trace.start(0, 0)?;
trace.stage(a, 0, &"F", false)?;
trace.advance(1)?;
let b = trace.start(1, 0)?;
trace.stage(a, 0, &"D", false)?;
trace.stage(b, 0, &"F", false)?;
trace.advance(1)?;
let c = trace.start(2, 0)?;
trace.stage(a, 0, &"X", true)?;
trace.stage(b, 0, &"D", false)?;
trace.stage(c, 0, &"F", false)?;
trace.advance(1)?;
trace.stage(c, 0, &"X", true)?;
trace.wake(c, a)?;
trace.retire(a)?;
trace.retire(b)?;
trace.retire(c)?;
success(
trace,
concat!(
"Kanata\t0004\n",
"C=\t0\n",
"I\t0\t0\t0\n",
"S\t0\t0\tF\n",
"C\t1\n",
"I\t1\t1\t0\n",
"S\t0\t0\tD\n",
"S\t1\t0\tF\n",
"C\t1\n",
"I\t2\t2\t0\n",
"S\t0\t0\tX_X\n",
"S\t1\t0\tD\n",
"S\t2\t0\tF\n",
"C\t1\n",
"S\t2\t0\tX_X\n",
"W\t2\t0\t0\n",
"R\t0\t0\t0\n",
"R\t1\t1\t0\n",
"R\t2\t2\t0\n",
),
)
}
}