#![forbid(unsafe_code)]
use std::fmt;
use crate::metrics_registry::{BuiltinCounter, METRICS};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum SchemaKind {
Evidence,
RenderTrace,
EventTrace,
GoldenTrace,
Telemetry,
MigrationIr,
}
impl SchemaKind {
pub const ALL: [Self; 6] = [
Self::Evidence,
Self::RenderTrace,
Self::EventTrace,
Self::GoldenTrace,
Self::Telemetry,
Self::MigrationIr,
];
pub const fn current_version(self) -> &'static str {
match self {
Self::Evidence => "ftui-evidence-v2",
Self::RenderTrace => "render-trace-v1",
Self::EventTrace => "event-trace-v1",
Self::GoldenTrace => "golden-trace-v1",
Self::Telemetry => "1.0.0",
Self::MigrationIr => "migration-ir-v1",
}
}
const fn version_prefix(self) -> &'static str {
match self {
Self::Evidence => "ftui-evidence-v",
Self::RenderTrace => "render-trace-v",
Self::EventTrace => "event-trace-v",
Self::GoldenTrace => "golden-trace-v",
Self::Telemetry => "", Self::MigrationIr => "migration-ir-v",
}
}
pub const fn as_str(self) -> &'static str {
match self {
Self::Evidence => "evidence",
Self::RenderTrace => "render_trace",
Self::EventTrace => "event_trace",
Self::GoldenTrace => "golden_trace",
Self::Telemetry => "telemetry",
Self::MigrationIr => "migration_ir",
}
}
}
impl fmt::Display for SchemaKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Compatibility {
Exact,
Forward {
reader_version: u32,
writer_version: u32,
},
Backward {
reader_version: u32,
writer_version: u32,
},
Unknown { writer_version: String },
}
impl Compatibility {
pub fn is_compatible(&self) -> bool {
matches!(self, Self::Exact | Self::Forward { .. })
}
}
impl fmt::Display for Compatibility {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Exact => write!(f, "exact match"),
Self::Forward {
reader_version,
writer_version,
} => write!(
f,
"forward compatible (reader=v{reader_version}, writer=v{writer_version})"
),
Self::Backward {
reader_version,
writer_version,
} => write!(
f,
"incompatible: writer newer (reader=v{reader_version}, writer=v{writer_version})"
),
Self::Unknown { writer_version } => {
write!(f, "unknown version format: {writer_version}")
}
}
}
}
#[derive(Debug, Clone)]
pub struct CompatCheckResult {
pub kind: SchemaKind,
pub reader_version: &'static str,
pub writer_version: String,
pub compatibility: Compatibility,
}
impl CompatCheckResult {
pub fn is_compatible(&self) -> bool {
self.compatibility.is_compatible()
}
}
impl fmt::Display for CompatCheckResult {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{}: {} (reader={}, writer={})",
self.kind, self.compatibility, self.reader_version, self.writer_version,
)
}
}
fn parse_prefixed_version(version: &str, prefix: &str) -> Option<u32> {
version.strip_prefix(prefix)?.parse().ok()
}
fn parse_semver_major(version: &str) -> Option<u32> {
version.split('.').next()?.parse().ok()
}
fn parse_version_number(kind: SchemaKind, version: &str) -> Option<u32> {
if kind == SchemaKind::Telemetry {
parse_semver_major(version)
} else {
parse_prefixed_version(version, kind.version_prefix())
}
}
pub fn check_schema_compat(kind: SchemaKind, writer_version: &str) -> CompatCheckResult {
let reader_version = kind.current_version();
let compatibility = if writer_version == reader_version {
Compatibility::Exact
} else {
match (
parse_version_number(kind, reader_version),
parse_version_number(kind, writer_version),
) {
(Some(rv), Some(wv)) if rv > wv => Compatibility::Forward {
reader_version: rv,
writer_version: wv,
},
(Some(rv), Some(wv)) if rv == wv => Compatibility::Exact,
(Some(rv), Some(wv)) => Compatibility::Backward {
reader_version: rv,
writer_version: wv,
},
_ => Compatibility::Unknown {
writer_version: writer_version.to_string(),
},
}
};
let compatible = compatibility.is_compatible();
#[cfg(feature = "tracing")]
{
use tracing::{error, info_span};
let span = info_span!(
"trace.compat_check",
schema_version = kind.current_version(),
reader_version = reader_version,
writer_version = writer_version,
compatible = compatible,
);
let _guard = span.enter();
if !compatible {
error!(
schema_kind = kind.as_str(),
reader_version = reader_version,
writer_version = writer_version,
"trace schema version incompatible"
);
}
}
if !compatible {
METRICS
.counter(BuiltinCounter::TraceCompatFailuresTotal)
.inc();
}
CompatCheckResult {
kind,
reader_version,
writer_version: writer_version.to_string(),
compatibility,
}
}
pub fn check_evidence_compat(writer_version: &str) -> CompatCheckResult {
check_schema_compat(SchemaKind::Evidence, writer_version)
}
pub fn check_render_trace_compat(writer_version: &str) -> CompatCheckResult {
check_schema_compat(SchemaKind::RenderTrace, writer_version)
}
pub fn check_event_trace_compat(writer_version: &str) -> CompatCheckResult {
check_schema_compat(SchemaKind::EventTrace, writer_version)
}
pub fn check_golden_trace_compat(writer_version: &str) -> CompatCheckResult {
check_schema_compat(SchemaKind::GoldenTrace, writer_version)
}
#[derive(Debug, Clone)]
pub struct MatrixEntry {
pub kind: SchemaKind,
pub writer_version: String,
pub expected_compatible: bool,
}
pub fn run_compatibility_matrix(entries: &[MatrixEntry]) -> Vec<(MatrixEntry, CompatCheckResult)> {
entries
.iter()
.map(|entry| {
let result = check_schema_compat(entry.kind, &entry.writer_version);
(entry.clone(), result)
})
.collect()
}
pub fn default_compatibility_matrix() -> Vec<MatrixEntry> {
let mut entries = Vec::new();
for kind in SchemaKind::ALL {
let current = kind.current_version();
entries.push(MatrixEntry {
kind,
writer_version: current.to_string(),
expected_compatible: true,
});
entries.push(MatrixEntry {
kind,
writer_version: "not-a-version".to_string(),
expected_compatible: false,
});
if kind == SchemaKind::Telemetry {
entries.push(MatrixEntry {
kind,
writer_version: "0.9.0".to_string(),
expected_compatible: true,
});
entries.push(MatrixEntry {
kind,
writer_version: "2.0.0".to_string(),
expected_compatible: false,
});
} else {
let prefix = kind.version_prefix();
if let Some(current_num) = parse_version_number(kind, current) {
if current_num > 0 {
entries.push(MatrixEntry {
kind,
writer_version: format!("{prefix}{}", current_num - 1),
expected_compatible: true,
});
}
entries.push(MatrixEntry {
kind,
writer_version: format!("{prefix}{}", current_num + 1),
expected_compatible: false,
});
}
}
}
entries
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn exact_match_all_kinds() {
for kind in SchemaKind::ALL {
let result = check_schema_compat(kind, kind.current_version());
assert_eq!(result.compatibility, Compatibility::Exact, "{kind}");
assert!(result.is_compatible(), "{kind}");
}
}
#[test]
fn forward_compat_evidence() {
let result = check_schema_compat(SchemaKind::Evidence, "ftui-evidence-v1");
assert!(
matches!(
result.compatibility,
Compatibility::Forward {
reader_version: 2,
writer_version: 1
}
),
"got {:?}",
result.compatibility
);
assert!(result.is_compatible());
}
#[test]
fn backward_incompat_evidence() {
let result = check_schema_compat(SchemaKind::Evidence, "ftui-evidence-v3");
assert!(
matches!(
result.compatibility,
Compatibility::Backward {
reader_version: 2,
writer_version: 3
}
),
"got {:?}",
result.compatibility
);
assert!(!result.is_compatible());
}
#[test]
fn unknown_version_format() {
let result = check_schema_compat(SchemaKind::Evidence, "garbage-string");
assert!(
matches!(result.compatibility, Compatibility::Unknown { .. }),
"got {:?}",
result.compatibility
);
assert!(!result.is_compatible());
}
#[test]
fn forward_compat_telemetry_semver() {
let result = check_schema_compat(SchemaKind::Telemetry, "0.9.0");
assert!(
matches!(
result.compatibility,
Compatibility::Forward {
reader_version: 1,
writer_version: 0
}
),
"got {:?}",
result.compatibility
);
assert!(result.is_compatible());
}
#[test]
fn backward_incompat_telemetry_semver() {
let result = check_schema_compat(SchemaKind::Telemetry, "2.0.0");
assert!(
matches!(
result.compatibility,
Compatibility::Backward {
reader_version: 1,
writer_version: 2
}
),
"got {:?}",
result.compatibility
);
assert!(!result.is_compatible());
}
#[test]
fn all_kinds_have_current_version() {
for kind in SchemaKind::ALL {
let v = kind.current_version();
assert!(!v.is_empty(), "{kind} has empty version");
}
}
#[test]
fn all_kinds_have_unique_versions() {
let mut versions = std::collections::HashSet::new();
for kind in SchemaKind::ALL {
assert!(
versions.insert(kind.current_version()),
"duplicate version: {}",
kind.current_version()
);
}
}
#[test]
fn default_matrix_covers_all_kinds() {
let matrix = default_compatibility_matrix();
for kind in SchemaKind::ALL {
let count = matrix.iter().filter(|e| e.kind == kind).count();
assert!(
count >= 3,
"{kind} has only {count} matrix entries, expected >=3"
);
}
}
#[test]
fn default_matrix_all_pass() {
let matrix = default_compatibility_matrix();
let results = run_compatibility_matrix(&matrix);
for (entry, result) in &results {
assert_eq!(
result.is_compatible(),
entry.expected_compatible,
"{}: writer={}, expected_compatible={}, got {:?}",
entry.kind,
entry.writer_version,
entry.expected_compatible,
result.compatibility,
);
}
}
#[test]
fn compat_failures_counter_increments() {
let before = METRICS
.counter(BuiltinCounter::TraceCompatFailuresTotal)
.get();
let _ = check_schema_compat(SchemaKind::Evidence, "ftui-evidence-v99");
let after = METRICS
.counter(BuiltinCounter::TraceCompatFailuresTotal)
.get();
assert!(
after > before,
"counter should increment on incompatibility"
);
}
#[test]
fn exact_match_does_not_increment_counter() {
let before = METRICS
.counter(BuiltinCounter::TraceCompatFailuresTotal)
.get();
let _ = check_schema_compat(SchemaKind::Evidence, "ftui-evidence-v2");
let after = METRICS
.counter(BuiltinCounter::TraceCompatFailuresTotal)
.get();
assert_eq!(after, before, "counter should not increment on exact match");
}
#[test]
fn display_impls() {
let result = check_schema_compat(SchemaKind::Evidence, "ftui-evidence-v1");
let s = result.to_string();
assert!(s.contains("evidence"), "{s}");
assert!(s.contains("forward compatible"), "{s}");
let result2 = check_schema_compat(SchemaKind::RenderTrace, "render-trace-v99");
let s2 = result2.to_string();
assert!(s2.contains("incompatible"), "{s2}");
}
#[test]
fn schema_kind_display() {
assert_eq!(SchemaKind::Evidence.to_string(), "evidence");
assert_eq!(SchemaKind::RenderTrace.to_string(), "render_trace");
assert_eq!(SchemaKind::Telemetry.to_string(), "telemetry");
}
#[test]
fn render_trace_forward_compat() {
let result = check_schema_compat(SchemaKind::RenderTrace, "render-trace-v0");
assert!(result.is_compatible());
assert!(matches!(
result.compatibility,
Compatibility::Forward { .. }
));
}
#[test]
fn event_trace_exact() {
let result = check_schema_compat(SchemaKind::EventTrace, "event-trace-v1");
assert_eq!(result.compatibility, Compatibility::Exact);
}
#[test]
fn golden_trace_backward_incompat() {
let result = check_schema_compat(SchemaKind::GoldenTrace, "golden-trace-v2");
assert!(!result.is_compatible());
}
#[test]
fn migration_ir_exact() {
let result = check_schema_compat(SchemaKind::MigrationIr, "migration-ir-v1");
assert_eq!(result.compatibility, Compatibility::Exact);
}
#[test]
fn evidence_v0_forward() {
let result = check_schema_compat(SchemaKind::Evidence, "ftui-evidence-v0");
assert!(result.is_compatible());
assert!(matches!(
result.compatibility,
Compatibility::Forward {
reader_version: 2,
writer_version: 0
}
));
}
}