1use std::path::PathBuf;
17
18#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
19pub struct EffectSignature {
20 pub check_kind: String,
21 pub outcome: String,
22 pub severity: String,
23 pub message_class: String,
24 pub line_offset_from_edit: Option<i32>,
25}
26
27#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
28pub struct LocatedEffect {
29 pub file: Option<PathBuf>,
30 pub line: Option<u32>,
31 pub col: Option<u32>,
32 pub message: String,
33 pub sig: EffectSignature,
34}
35
36pub fn effect_signature_hash(signature: &EffectSignature) -> String {
37 let payload = serde_json::to_vec(signature).unwrap_or_default();
38 blake3::hash(&payload).to_hex().to_string()
39}
40
41#[cfg(test)]
42mod wave1_tests {
43 use super::*;
44
45 fn sample_signature() -> EffectSignature {
46 EffectSignature {
47 check_kind: "clippy".into(),
48 outcome: "fail".into(),
49 severity: "warning".into(),
50 message_class: "unused_variable".into(),
51 line_offset_from_edit: Some(2),
52 }
53 }
54
55 #[test]
56 fn effect_signature_hash_is_deterministic_and_sensitive_to_fields() {
57 let baseline = sample_signature();
58 let same_hash = effect_signature_hash(&baseline);
59 assert_eq!(same_hash, effect_signature_hash(&baseline));
60
61 let mut changed = baseline.clone();
62 changed.message_class = "different".into();
63 assert_ne!(same_hash, effect_signature_hash(&changed));
64 }
65
66 #[test]
67 fn located_effect_round_trips_through_json() {
68 let located = LocatedEffect {
69 file: Some(PathBuf::from("src/lib.rs")),
70 line: Some(12),
71 col: Some(4),
72 message: "unused variable".into(),
73 sig: sample_signature(),
74 };
75
76 let encoded = serde_json::to_string(&located).unwrap();
77 let decoded: LocatedEffect = serde_json::from_str(&encoded).unwrap();
78 assert_eq!(decoded, located);
79 }
80}
81
82#[cfg(test)]
83mod tests {
84 use super::*;
85
86 fn sample_signature() -> EffectSignature {
87 EffectSignature {
88 check_kind: "clippy".into(),
89 outcome: "warning".into(),
90 severity: "medium".into(),
91 message_class: "lint".into(),
92 line_offset_from_edit: Some(2),
93 }
94 }
95
96 #[test]
97 fn located_effect_round_trips_through_json() {
98 let effect = LocatedEffect {
99 file: Some("src/lib.rs".into()),
100 line: Some(12),
101 col: Some(4),
102 message: "unused variable".into(),
103 sig: sample_signature(),
104 };
105
106 let encoded = serde_json::to_string(&effect).unwrap();
107 let decoded: LocatedEffect = serde_json::from_str(&encoded).unwrap();
108
109 assert_eq!(decoded, effect);
110 }
111
112 #[test]
113 fn hash_is_stable_for_identical_signatures() {
114 let left = sample_signature();
115 let right = sample_signature();
116
117 assert_eq!(effect_signature_hash(&left), effect_signature_hash(&right));
118 }
119
120 #[test]
121 fn hash_changes_when_signature_changes() {
122 let baseline = sample_signature();
123 let mut changed = sample_signature();
124 changed.message_class = "type_mismatch".into();
125
126 assert_ne!(
127 effect_signature_hash(&baseline),
128 effect_signature_hash(&changed)
129 );
130 }
131}