use std::path::Path;
use serde::{Deserialize, Serialize};
use crate::error::Error;
use crate::util;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct CompatEnvelope {
pub schema_version: u32,
pub mode: String,
pub crate_name: String,
pub commit: String,
pub commands: Commands,
pub results: Results,
pub mismatch_examples: Vec<MismatchExample>,
pub errors: Vec<EnvelopeError>,
pub excluded_fixtures: Vec<ExcludedFixture>,
pub generated_paths: Vec<GeneratedPath>,
pub overlay: OverlayMetadata,
pub toolchain: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct Commands {
pub baseline: String,
pub lihaaf: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct Results {
pub baseline: BaselineCounts,
pub lihaaf: LihaafCounts,
pub mismatch_count: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct BaselineCounts {
pub pass: u32,
pub fail: u32,
pub unknown_count: u32,
pub exit_code: i32,
pub dur_ms: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct LihaafCounts {
pub pass: u32,
pub fail: u32,
pub exit_code: i32,
pub dur_ms: u64,
pub toolchain: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct MismatchExample {
pub fixture: String,
pub mismatch_type: String,
pub notes: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct EnvelopeError {
pub error_type: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub fixture: Option<String>,
pub file: String,
pub line: u32,
pub detail: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct ExcludedFixture {
pub fixture: String,
pub reason: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct GeneratedPath {
pub path: String,
pub class: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct OverlayMetadata {
pub generated: bool,
pub dropped_comments: Vec<String>,
pub upstream_already_has_dylib: bool,
}
pub fn generated_path_from_cleanup(
cleanup_entry: &crate::compat::cleanup::GeneratedPath,
compat_root: &Path,
) -> GeneratedPath {
let rel = util::relative_to(&cleanup_entry.path, compat_root);
let class = match cleanup_entry.class {
crate::compat::cleanup::GeneratedPathClass::Committed => "committed",
crate::compat::cleanup::GeneratedPathClass::Ignored => "ignored",
crate::compat::cleanup::GeneratedPathClass::Cleaned => "cleaned",
crate::compat::cleanup::GeneratedPathClass::Kept => "kept",
};
GeneratedPath {
path: rel,
class: class.to_string(),
}
}
pub fn canonicalize(envelope: &mut CompatEnvelope) {
envelope
.mismatch_examples
.sort_by(|a, b| a.fixture.cmp(&b.fixture));
envelope.errors.sort_by(|a, b| {
a.file
.cmp(&b.file)
.then_with(|| a.line.cmp(&b.line))
.then_with(|| a.error_type.cmp(&b.error_type))
});
envelope
.excluded_fixtures
.sort_by(|a, b| a.fixture.cmp(&b.fixture));
envelope.generated_paths.sort_by(|a, b| a.path.cmp(&b.path));
envelope.overlay.dropped_comments.sort();
}
pub fn write_envelope(envelope: &mut CompatEnvelope, path: &Path) -> Result<(), Error> {
canonicalize(envelope);
let mut text = serde_json::to_string_pretty(envelope).map_err(|e| Error::JsonParse {
context: "serializing compat envelope".into(),
message: e.to_string(),
})?;
text.push('\n');
util::write_file_atomic(path, text.as_bytes())
}
#[cfg(test)]
mod tests {
use super::*;
fn sample_envelope() -> CompatEnvelope {
CompatEnvelope {
schema_version: 1,
mode: "compat".into(),
crate_name: "demo".into(),
commit: String::new(),
commands: Commands {
baseline: "cargo test".into(),
lihaaf: "cargo lihaaf --compat --compat-root .".into(),
},
results: Results {
baseline: BaselineCounts {
pass: 0,
fail: 0,
unknown_count: 0,
exit_code: 0,
dur_ms: 0,
},
lihaaf: LihaafCounts {
pass: 0,
fail: 0,
exit_code: 0,
dur_ms: 0,
toolchain: "rustc 1.95.0".into(),
},
mismatch_count: 0,
},
mismatch_examples: Vec::new(),
errors: Vec::new(),
excluded_fixtures: Vec::new(),
generated_paths: Vec::new(),
overlay: OverlayMetadata {
generated: true,
dropped_comments: Vec::new(),
upstream_already_has_dylib: false,
},
toolchain: "rustc 1.95.0".into(),
}
}
#[test]
fn canonicalize_is_idempotent() {
let mut env = sample_envelope();
env.mismatch_examples = vec![
MismatchExample {
fixture: "tests/z.rs".into(),
mismatch_type: "verdict_mismatch".into(),
notes: String::new(),
},
MismatchExample {
fixture: "tests/a.rs".into(),
mismatch_type: "verdict_mismatch".into(),
notes: String::new(),
},
];
canonicalize(&mut env);
let after_first = env.clone();
canonicalize(&mut env);
assert_eq!(
after_first, env,
"canonicalize must be idempotent across repeated calls"
);
assert_eq!(env.mismatch_examples[0].fixture, "tests/a.rs");
assert_eq!(env.mismatch_examples[1].fixture, "tests/z.rs");
}
#[test]
fn errors_sort_by_file_then_line_then_type() {
let mut env = sample_envelope();
env.errors = vec![
EnvelopeError {
error_type: "z_type".into(),
fixture: None,
file: "tests/foo.rs".into(),
line: 10,
detail: String::new(),
},
EnvelopeError {
error_type: "a_type".into(),
fixture: None,
file: "tests/foo.rs".into(),
line: 10,
detail: String::new(),
},
EnvelopeError {
error_type: "m_type".into(),
fixture: None,
file: "tests/bar.rs".into(),
line: 100,
detail: String::new(),
},
];
canonicalize(&mut env);
assert_eq!(env.errors[0].file, "tests/bar.rs");
assert_eq!(env.errors[1].file, "tests/foo.rs");
assert_eq!(
env.errors[1].error_type, "a_type",
"errors at the same (file, line) must tiebreak by error_type"
);
assert_eq!(env.errors[2].error_type, "z_type");
}
}