#![forbid(unsafe_code)]
use core::fmt;
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct ContentHash(String);
impl ContentHash {
pub fn new(hex: impl Into<String>) -> Self {
Self(hex.into())
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for ContentHash {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct TargetTriple(String);
impl TargetTriple {
pub fn new(triple: impl Into<String>) -> Self {
Self(triple.into())
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for TargetTriple {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum Profile {
Dev,
Release,
}
impl Profile {
pub fn as_str(self) -> &'static str {
match self {
Profile::Dev => "dev",
Profile::Release => "release",
}
}
}
impl fmt::Display for Profile {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct BuildIdentity {
pub source_tree: ContentHash,
pub cargo_lock: ContentHash,
pub rust_toolchain: ContentHash,
pub tf_config: ContentHash,
pub target: TargetTriple,
pub profile: Profile,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct InputHash(String);
impl InputHash {
pub fn new(hex: impl Into<String>) -> Self {
Self(hex.into())
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for InputHash {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum FileState {
Green,
Red,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum TreeState {
Green,
Red,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum StateEvent {
FileVerdict { path: String, state: FileState },
BecameGreen { identity: BuildIdentity },
BecameRed,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BuildTrigger {
pub identity: BuildIdentity,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum BuildOutcome {
Deduplicated,
Compiled,
Failed { reason: String },
}
impl BuildOutcome {
pub fn is_servable(&self) -> bool {
matches!(self, BuildOutcome::Deduplicated | BuildOutcome::Compiled)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ArtifactMeta {
pub input_hash: InputHash,
pub identity: BuildIdentity,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BuildResult {
pub outcome: BuildOutcome,
pub artifact: Option<ArtifactMeta>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum Severity {
Error,
Warning,
Info,
Hint,
}
impl Severity {
pub fn as_str(self) -> &'static str {
match self {
Severity::Error => "error",
Severity::Warning => "warning",
Severity::Info => "info",
Severity::Hint => "hint",
}
}
}
impl fmt::Display for Severity {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Diagnostic {
pub file_path: std::path::PathBuf,
pub line: u32,
pub col: u32,
pub severity: Severity,
pub code: Option<String>,
pub message: String,
pub source: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CheckResult {
pub tree: TreeState,
pub diagnostics: Vec<Diagnostic>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct UnixSeconds(pub u64);
impl fmt::Display for UnixSeconds {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PublishedArtifact {
pub artifact: ArtifactMeta,
pub published_at: UnixSeconds,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PointerFormatError(pub String);
impl fmt::Display for PointerFormatError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "invalid latest-green pointer: {}", self.0)
}
}
impl std::error::Error for PointerFormatError {}
const POINTER_SCHEME: &str = "cargoless-latest-green/v1";
impl PublishedArtifact {
pub fn render(&self) -> String {
use core::fmt::Write as _;
let id = &self.artifact.identity;
let mut s = String::new();
s.push_str(POINTER_SCHEME);
s.push('\n');
let _ = writeln!(s, "input_hash={}", self.artifact.input_hash.as_str());
let _ = writeln!(s, "source_tree={}", id.source_tree.as_str());
let _ = writeln!(s, "cargo_lock={}", id.cargo_lock.as_str());
let _ = writeln!(s, "rust_toolchain={}", id.rust_toolchain.as_str());
let _ = writeln!(s, "tf_config={}", id.tf_config.as_str());
let _ = writeln!(s, "target={}", id.target.as_str());
let _ = writeln!(s, "profile={}", id.profile.as_str());
let _ = writeln!(s, "published_at={}", self.published_at.0);
s
}
pub fn parse(text: &str) -> Result<Self, PointerFormatError> {
let err = |m: &str| PointerFormatError(m.to_string());
let mut lines = text.lines();
match lines.next() {
Some(h) if h == POINTER_SCHEME => {}
_ => return Err(err("missing or unknown scheme header")),
}
let mut map: std::collections::BTreeMap<String, String> = std::collections::BTreeMap::new();
for line in lines {
if line.is_empty() {
continue;
}
let (k, v) = line
.split_once('=')
.ok_or_else(|| err("line is not key=value"))?;
map.insert(k.to_string(), v.to_string());
}
let get = |k: &str| -> Result<String, PointerFormatError> {
map.get(k)
.cloned()
.ok_or_else(|| err(&format!("missing key `{k}`")))
};
let profile = match get("profile")?.as_str() {
"dev" => Profile::Dev,
"release" => Profile::Release,
other => return Err(err(&format!("unknown profile `{other}`"))),
};
let published_at = get("published_at")?
.parse::<u64>()
.map_err(|_| err("published_at is not a u64"))?;
Ok(Self {
artifact: ArtifactMeta {
input_hash: InputHash::new(get("input_hash")?),
identity: BuildIdentity {
source_tree: ContentHash::new(get("source_tree")?),
cargo_lock: ContentHash::new(get("cargo_lock")?),
rust_toolchain: ContentHash::new(get("rust_toolchain")?),
tf_config: ContentHash::new(get("tf_config")?),
target: TargetTriple::new(get("target")?),
profile,
},
},
published_at: UnixSeconds(published_at),
})
}
}
#[cfg(test)]
mod tests {
use super::*;
fn sample_identity() -> BuildIdentity {
BuildIdentity {
source_tree: ContentHash::new("aaaa"),
cargo_lock: ContentHash::new("bbbb"),
rust_toolchain: ContentHash::new("cccc"),
tf_config: ContentHash::new("dddd"),
target: TargetTriple::new("wasm32-unknown-unknown"),
profile: Profile::Dev,
}
}
#[test]
fn input_hash_roundtrips_and_displays() {
let h = InputHash::new("deadbeef");
assert_eq!(h.as_str(), "deadbeef");
assert_eq!(h, InputHash::new("deadbeef".to_string()));
assert_eq!(h.to_string(), "deadbeef");
}
#[test]
fn identity_equality_is_componentwise() {
let a = sample_identity();
let b = sample_identity();
assert_eq!(
a, b,
"equal components ⇒ equal identity (the AC#5 invariant)"
);
let mut c = sample_identity();
c.profile = Profile::Release;
assert_ne!(a, c, "a release build must never alias a dev artifact");
let mut d = sample_identity();
d.source_tree = ContentHash::new("ffff");
assert_ne!(a, d, "a source change must invalidate the cache key");
}
#[test]
fn became_green_carries_identity_for_one_shot_build_trigger() {
let ev = StateEvent::BecameGreen {
identity: sample_identity(),
};
match ev {
StateEvent::BecameGreen { identity } => {
let trigger = BuildTrigger { identity };
assert_eq!(trigger.identity, sample_identity());
}
_ => unreachable!(),
}
}
#[test]
fn state_events_are_distinct() {
assert_ne!(
StateEvent::BecameRed,
StateEvent::BecameGreen {
identity: sample_identity()
}
);
let v = StateEvent::FileVerdict {
path: "src/lib.rs".into(),
state: FileState::Red,
};
assert_ne!(v, StateEvent::BecameRed);
}
#[test]
fn outcome_servability_drives_artifact_presence() {
assert!(BuildOutcome::Deduplicated.is_servable());
assert!(BuildOutcome::Compiled.is_servable());
assert!(
!BuildOutcome::Failed {
reason: "linker exploded".into()
}
.is_servable()
);
let ok = BuildResult {
outcome: BuildOutcome::Compiled,
artifact: Some(ArtifactMeta {
input_hash: InputHash::new("0123"),
identity: sample_identity(),
}),
};
assert!(ok.outcome.is_servable() && ok.artifact.is_some());
let bad = BuildResult {
outcome: BuildOutcome::Failed {
reason: "rustc ICE".into(),
},
artifact: None,
};
assert!(!bad.outcome.is_servable() && bad.artifact.is_none());
}
#[test]
fn profile_and_tree_state_render() {
assert_eq!(Profile::Dev.as_str(), "dev");
assert_eq!(Profile::Release.to_string(), "release");
assert_ne!(TreeState::Green, TreeState::Red);
}
fn sample_published() -> PublishedArtifact {
PublishedArtifact {
artifact: ArtifactMeta {
input_hash: InputHash::new("0123abcd"),
identity: sample_identity(),
},
published_at: UnixSeconds(1_747_000_000),
}
}
#[test]
fn published_artifact_round_trips_through_the_pointer_codec() {
let p = sample_published();
let text = p.render();
assert!(text.starts_with("cargoless-latest-green/v1\n"));
assert!(text.contains("input_hash=0123abcd\n"));
assert!(text.contains("profile=dev\n"));
assert!(text.contains("published_at=1747000000\n"));
assert_eq!(PublishedArtifact::parse(&text).unwrap(), p);
}
#[test]
fn pointer_parse_is_strict() {
assert!(PublishedArtifact::parse("").is_err());
assert!(PublishedArtifact::parse("not-a-pointer\ninput_hash=x\n").is_err());
assert!(PublishedArtifact::parse("cargoless-latest-green/v1\ninput_hash=x\n").is_err());
let mut bad = sample_published()
.render()
.replace("profile=dev", "profile=fast");
assert!(PublishedArtifact::parse(&bad).is_err());
bad = sample_published()
.render()
.replace("published_at=1747000000", "published_at=soon");
assert!(PublishedArtifact::parse(&bad).is_err());
}
#[test]
fn unix_seconds_is_a_distinct_newtype() {
assert_eq!(UnixSeconds(42).to_string(), "42");
assert!(UnixSeconds(1) < UnixSeconds(2));
assert_eq!(UnixSeconds(7), UnixSeconds(7));
}
#[test]
fn severity_renders_lowercase_and_is_exhaustive() {
assert_eq!(Severity::Error.as_str(), "error");
assert_eq!(Severity::Warning.as_str(), "warning");
assert_eq!(Severity::Info.as_str(), "info");
assert_eq!(Severity::Hint.as_str(), "hint");
assert_eq!(Severity::Error.to_string(), "error");
let s: std::collections::BTreeSet<_> = [
Severity::Error,
Severity::Warning,
Severity::Info,
Severity::Hint,
]
.into_iter()
.collect();
assert_eq!(s.len(), 4);
}
#[test]
fn diagnostic_carries_position_code_and_source() {
let d = Diagnostic {
file_path: std::path::PathBuf::from("/repo/src/lib.rs"),
line: 42,
col: 5,
severity: Severity::Error,
code: Some("E0277".to_string()),
message: "the trait bound `T: Foo` is not satisfied".to_string(),
source: Some("rustc".to_string()),
};
assert_eq!(d.line, 42);
assert_eq!(d.col, 5);
assert_eq!(d.code.as_deref(), Some("E0277"));
assert_eq!(d.source.as_deref(), Some("rustc"));
assert_eq!(d.severity, Severity::Error);
assert!(d.message.contains("trait bound"));
let d2 = d.clone();
assert_eq!(d, d2);
}
#[test]
fn check_result_pairs_tree_with_diagnostics() {
let green = CheckResult {
tree: TreeState::Green,
diagnostics: Vec::new(),
};
assert_eq!(green.tree, TreeState::Green);
assert!(green.diagnostics.is_empty());
let red = CheckResult {
tree: TreeState::Red,
diagnostics: vec![Diagnostic {
file_path: std::path::PathBuf::from("/r/src/lib.rs"),
line: 1,
col: 1,
severity: Severity::Error,
code: Some("E0599".to_string()),
message: "no method named `frob` found".to_string(),
source: Some("rustc".to_string()),
}],
};
assert_eq!(red.tree, TreeState::Red);
assert_eq!(red.diagnostics.len(), 1);
assert_eq!(red.diagnostics[0].code.as_deref(), Some("E0599"));
}
#[test]
fn diagnostic_path_is_relativisable_against_a_root() {
let d = Diagnostic {
file_path: std::path::PathBuf::from("/repo/src/lib.rs"),
line: 1,
col: 1,
severity: Severity::Warning,
code: None,
message: "x".to_string(),
source: None,
};
let rel = d
.file_path
.strip_prefix("/repo")
.expect("strips the root cleanly");
assert_eq!(rel, std::path::Path::new("src/lib.rs"));
}
}