use rabs_protocol::result_identity::TypedDigest;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LinkOutput {
pub logical_name: String,
pub digest: TypedDigest,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct StockLinkOutcome {
pub outputs: Vec<LinkOutput>,
pub stdout: Vec<u8>,
pub stderr: Vec<u8>,
pub exit_code: i32,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum BundleRefusal {
LinkFailed {
exit_code: i32,
},
NoOutputs,
DuplicateLogicalName(String),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LinkResultBundle {
outputs: Vec<LinkOutput>,
stdout: Vec<u8>,
stderr: Vec<u8>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ReplayedLink {
pub outputs: Vec<LinkOutput>,
pub stdout: Vec<u8>,
pub stderr: Vec<u8>,
pub exit_code: i32,
}
impl LinkResultBundle {
pub fn bundle(outcome: &StockLinkOutcome) -> Result<Self, BundleRefusal> {
if outcome.exit_code != 0 {
return Err(BundleRefusal::LinkFailed {
exit_code: outcome.exit_code,
});
}
if outcome.outputs.is_empty() {
return Err(BundleRefusal::NoOutputs);
}
for (i, output) in outcome.outputs.iter().enumerate() {
if outcome.outputs[..i]
.iter()
.any(|prior| prior.logical_name == output.logical_name)
{
return Err(BundleRefusal::DuplicateLogicalName(
output.logical_name.clone(),
));
}
}
Ok(Self {
outputs: outcome.outputs.clone(),
stdout: outcome.stdout.clone(),
stderr: outcome.stderr.clone(),
})
}
#[must_use]
pub fn replay(&self) -> ReplayedLink {
ReplayedLink {
outputs: self.outputs.clone(),
stdout: self.stdout.clone(),
stderr: self.stderr.clone(),
exit_code: 0, }
}
}
#[must_use]
pub fn equivalent_to_stock(replayed: &ReplayedLink, stock: &StockLinkOutcome) -> bool {
replayed.outputs == stock.outputs
&& replayed.stdout == stock.stdout
&& replayed.stderr == stock.stderr
&& replayed.exit_code == stock.exit_code
}
#[cfg(test)]
mod tests {
use super::*;
use rabs_protocol::result_identity::DigestAlgorithm;
fn d(tag: u8) -> TypedDigest {
TypedDigest {
algorithm: DigestAlgorithm::Sha256V1,
domain: "rabs.object.v1",
bytes: [tag; 32],
}
}
fn stock_link() -> StockLinkOutcome {
StockLinkOutcome {
outputs: vec![
LinkOutput {
logical_name: "bin/rch".into(),
digest: d(1),
},
LinkOutput {
logical_name: "bin/rch.dwp".into(),
digest: d(2),
},
],
stdout: b"".to_vec(),
stderr: b"warning: linking against system libunwind\n".to_vec(),
exit_code: 0,
}
}
#[test]
fn link_hit_equivalence_vs_stock_link() {
let stock = stock_link();
let bundle = LinkResultBundle::bundle(&stock).expect("clean link bundles");
let replayed = bundle.replay();
assert!(equivalent_to_stock(&replayed, &stock));
assert_eq!(replayed.stderr, stock.stderr, "warnings byte-identical");
assert_eq!(replayed.stdout, stock.stdout);
assert_eq!(replayed.exit_code, 0);
assert_eq!(replayed.outputs.len(), 2);
let mut wrong = replayed.clone();
wrong.stderr = b"".to_vec(); assert!(!equivalent_to_stock(&wrong, &stock));
let mut wrong = bundle.replay();
wrong.outputs.pop();
assert!(!equivalent_to_stock(&wrong, &stock));
}
#[test]
fn a_failed_link_never_bundles() {
let mut failed = stock_link();
failed.exit_code = 1;
failed.stderr = b"error: undefined symbol: _main\n".to_vec();
assert_eq!(
LinkResultBundle::bundle(&failed),
Err(BundleRefusal::LinkFailed { exit_code: 1 })
);
}
#[test]
fn output_set_validation_at_bundling() {
let mut empty = stock_link();
empty.outputs.clear();
assert_eq!(
LinkResultBundle::bundle(&empty),
Err(BundleRefusal::NoOutputs)
);
let mut dup = stock_link();
dup.outputs.push(LinkOutput {
logical_name: "bin/rch".into(), digest: d(9),
});
assert_eq!(
LinkResultBundle::bundle(&dup),
Err(BundleRefusal::DuplicateLogicalName("bin/rch".into()))
);
}
#[test]
fn the_bundle_is_atomic_by_construction() {
let bundle = LinkResultBundle::bundle(&stock_link()).expect("bundles");
let ReplayedLink {
outputs,
stdout,
stderr,
exit_code,
} = bundle.replay(); assert!(!outputs.is_empty());
assert!(stdout.is_empty() && !stderr.is_empty());
assert_eq!(exit_code, 0);
}
}