1use serde::{Deserialize, Serialize};
12
13use crate::{
14 parse_contract_spec, parse_metadata, ContractEvent, ContractFunction, ContractSpec,
15 ContractType, WasmError,
16};
17
18#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
22pub struct SpecDiff {
23 pub old: WasmSummary,
25 pub new: WasmSummary,
27 pub added_functions: Vec<ContractFunction>,
29 pub removed_functions: Vec<ContractFunction>,
31 pub changed_functions: Vec<FunctionSignatureChange>,
33 pub added_events: Vec<String>,
35 pub removed_events: Vec<String>,
37 pub changed_events: Vec<EventSignatureChange>,
40 pub added_types: Vec<String>,
42 pub removed_types: Vec<String>,
44 pub changed_types: Vec<TypeDefinitionChange>,
46 pub size_delta_bytes: i64,
49 #[serde(default)]
55 pub size_delta_pct: Option<f64>,
56}
57
58#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
60pub struct WasmSummary {
61 pub hash: String,
63 pub size_bytes: usize,
65}
66
67#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
69pub struct FunctionSignatureChange {
70 pub name: String,
71 pub old: ContractFunction,
72 pub new: ContractFunction,
73}
74
75#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
77pub struct EventSignatureChange {
78 pub name: String,
79 pub old: ContractEvent,
80 pub new: ContractEvent,
81}
82
83#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
85pub struct TypeDefinitionChange {
86 pub name: String,
87 pub old: ContractType,
88 pub new: ContractType,
89}
90
91impl SpecDiff {
92 pub fn is_identical(&self) -> bool {
95 self.added_functions.is_empty()
96 && self.removed_functions.is_empty()
97 && self.changed_functions.is_empty()
98 && self.added_events.is_empty()
99 && self.removed_events.is_empty()
100 && self.changed_events.is_empty()
101 && self.added_types.is_empty()
102 && self.removed_types.is_empty()
103 && self.changed_types.is_empty()
104 }
105
106 pub fn total_changes(&self) -> usize {
109 self.added_functions.len()
110 + self.removed_functions.len()
111 + self.changed_functions.len()
112 + self.added_events.len()
113 + self.removed_events.len()
114 + self.changed_events.len()
115 + self.added_types.len()
116 + self.removed_types.len()
117 + self.changed_types.len()
118 }
119}
120
121fn summarize(raw: &[u8]) -> Result<WasmSummary, WasmError> {
123 let meta = parse_metadata(raw)?;
124 Ok(WasmSummary {
125 hash: meta.hash,
126 size_bytes: meta.size_bytes,
127 })
128}
129
130pub fn diff_wasm(old_raw: &[u8], new_raw: &[u8]) -> Result<SpecDiff, WasmError> {
137 let old_spec = parse_contract_spec(old_raw)?;
138 let new_spec = parse_contract_spec(new_raw)?;
139 diff_specs(
140 &old_spec,
141 &new_spec,
142 summarize(old_raw)?,
143 summarize(new_raw)?,
144 )
145}
146
147pub fn diff_specs(
152 old: &ContractSpec,
153 new: &ContractSpec,
154 old_summary: WasmSummary,
155 new_summary: WasmSummary,
156) -> Result<SpecDiff, WasmError> {
157 let mut diff = SpecDiff {
158 old: old_summary,
159 new: new_summary,
160 ..Default::default()
161 };
162 diff.size_delta_bytes = size_delta_bytes(diff.old.size_bytes, diff.new.size_bytes);
163 diff.size_delta_pct = size_delta_pct(diff.old.size_bytes, diff.new.size_bytes);
164
165 let old_fns: std::collections::BTreeMap<&str, &ContractFunction> =
167 old.functions.iter().map(|f| (f.name.as_str(), f)).collect();
168 let new_fns: std::collections::BTreeMap<&str, &ContractFunction> =
169 new.functions.iter().map(|f| (f.name.as_str(), f)).collect();
170
171 for (name, nf) in &new_fns {
172 match old_fns.get(name) {
173 None => diff.added_functions.push((**nf).clone()),
174 Some(of) => {
175 if of.parameters != nf.parameters || of.outputs != nf.outputs {
176 diff.changed_functions.push(FunctionSignatureChange {
177 name: (*name).to_string(),
178 old: (*of).clone(),
179 new: (**nf).clone(),
180 });
181 }
182 }
183 }
184 }
185 for (_name, of) in &old_fns {
186 if !new_fns.contains_key(_name) {
187 diff.removed_functions.push((**of).clone());
188 }
189 }
190
191 let old_events = by_name(&old.events, |e| e.name.as_str());
196 let new_events = by_name(&new.events, |e| e.name.as_str());
197 diff_set(
198 &old.events
199 .iter()
200 .map(|e| e.name.clone())
201 .collect::<Vec<_>>(),
202 &new.events
203 .iter()
204 .map(|e| e.name.clone())
205 .collect::<Vec<_>>(),
206 &mut diff.added_events,
207 &mut diff.removed_events,
208 );
209 for (name, ne) in &new_events {
210 if let Some(oe) = old_events.get(name) {
211 if !event_signature_eq(oe, ne) {
212 diff.changed_events.push(EventSignatureChange {
213 name: (*name).to_string(),
214 old: (*oe).clone(),
215 new: (**ne).clone(),
216 });
217 }
218 }
219 }
220
221 let old_types = by_name(&old.custom_types, |t| t.name.as_str());
222 let new_types = by_name(&new.custom_types, |t| t.name.as_str());
223 diff_set(
224 &old.custom_types
225 .iter()
226 .map(|t| t.name.clone())
227 .collect::<Vec<_>>(),
228 &new.custom_types
229 .iter()
230 .map(|t| t.name.clone())
231 .collect::<Vec<_>>(),
232 &mut diff.added_types,
233 &mut diff.removed_types,
234 );
235 for (name, nt) in &new_types {
236 if let Some(ot) = old_types.get(name) {
237 if !type_definition_eq(ot, nt) {
238 diff.changed_types.push(TypeDefinitionChange {
239 name: (*name).to_string(),
240 old: (*ot).clone(),
241 new: (**nt).clone(),
242 });
243 }
244 }
245 }
246
247 Ok(diff)
248}
249
250fn by_name<T, F>(items: &[T], name_of: F) -> std::collections::BTreeMap<&str, &T>
252where
253 F: Fn(&T) -> &str,
254{
255 items.iter().map(|i| (name_of(i), i)).collect()
256}
257
258fn type_ref_eq(a: &ContractType, b: &ContractType) -> bool {
266 if a.name != b.name || a.kind != b.kind {
267 return false;
268 }
269 if a.kind == "compound" {
274 return a.members.len() == b.members.len()
275 && a.members
276 .iter()
277 .zip(b.members.iter())
278 .all(|(x, y)| x.name == y.name && member_types_eq(&x.types, &y.types));
279 }
280 true
281}
282
283fn member_types_eq(a: &[ContractType], b: &[ContractType]) -> bool {
285 a.len() == b.len() && a.iter().zip(b.iter()).all(|(x, y)| type_ref_eq(x, y))
286}
287
288fn event_signature_eq(a: &ContractEvent, b: &ContractEvent) -> bool {
294 a.prefix_topics == b.prefix_topics
295 && a.data_format == b.data_format
296 && a.params.len() == b.params.len()
297 && a.params.iter().zip(b.params.iter()).all(|(x, y)| {
298 x.name == y.name && x.location == y.location && type_ref_eq(&x.type_, &y.type_)
299 })
300}
301
302fn type_definition_eq(a: &ContractType, b: &ContractType) -> bool {
306 a.kind == b.kind
307 && a.members.len() == b.members.len()
308 && a.members.iter().zip(b.members.iter()).all(|(x, y)| {
309 x.name == y.name && x.value == y.value && member_types_eq(&x.types, &y.types)
310 })
311}
312
313fn diff_set(old: &[String], new: &[String], added: &mut Vec<String>, removed: &mut Vec<String>) {
316 let old_set: std::collections::BTreeSet<&str> = old.iter().map(String::as_str).collect();
317 let new_set: std::collections::BTreeSet<&str> = new.iter().map(String::as_str).collect();
318
319 for n in &new_set {
320 if !old_set.contains(n) {
321 added.push((*n).to_string());
322 }
323 }
324 for n in &old_set {
325 if !new_set.contains(n) {
326 removed.push((*n).to_string());
327 }
328 }
329}
330
331#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
333#[serde(rename_all = "snake_case")]
334pub enum ChangeKind {
335 RemovedFunction,
336 ChangedSignature,
337 RemovedEvent,
338 RemovedType,
339 AddedFunction,
340 AddedEvent,
341 AddedType,
342 ChangedEvent,
343 ChangedTypeDefinition,
344}
345
346#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
348pub struct VerdictChange {
349 pub kind: ChangeKind,
350 pub name: String,
353 #[serde(default, skip_serializing_if = "String::is_empty")]
355 pub detail: String,
356}
357
358impl VerdictChange {
359 pub fn label(&self) -> String {
361 let kind = match self.kind {
362 ChangeKind::RemovedFunction => "Removed function",
363 ChangeKind::ChangedSignature => "Changed signature",
364 ChangeKind::RemovedEvent => "Removed event",
365 ChangeKind::RemovedType => "Removed type",
366 ChangeKind::AddedFunction => "Added function",
367 ChangeKind::AddedEvent => "Added event",
368 ChangeKind::AddedType => "Added type",
369 ChangeKind::ChangedEvent => "Changed event",
370 ChangeKind::ChangedTypeDefinition => "Changed type definition",
371 };
372 match self.kind {
373 ChangeKind::RemovedFunction
374 | ChangeKind::AddedFunction
375 | ChangeKind::ChangedSignature => {
376 format!("{}: {}()", kind, self.name)
377 }
378 _ => format!("{}: {}", kind, self.name),
379 }
380 }
381}
382
383#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
390pub struct UpgradeVerdict {
391 pub compatible: bool,
392 pub breaking_changes: Vec<VerdictChange>,
393 pub non_breaking_changes: Vec<VerdictChange>,
394}
395
396impl UpgradeVerdict {
397 pub fn from_diff(diff: &SpecDiff) -> Self {
402 let mut breaking = Vec::new();
403 let mut non_breaking = Vec::new();
404
405 for f in &diff.removed_functions {
406 breaking.push(VerdictChange {
407 kind: ChangeKind::RemovedFunction,
408 name: f.name.clone(),
409 detail: String::new(),
410 });
411 }
412 for c in &diff.changed_functions {
413 breaking.push(VerdictChange {
414 kind: ChangeKind::ChangedSignature,
415 name: c.name.clone(),
416 detail: format!("old: {}\n new: {}", sig_of(&c.old), sig_of(&c.new)),
417 });
418 }
419 for e in &diff.removed_events {
420 breaking.push(VerdictChange {
421 kind: ChangeKind::RemovedEvent,
422 name: e.clone(),
423 detail: String::new(),
424 });
425 }
426 for c in &diff.changed_events {
427 breaking.push(VerdictChange {
428 kind: ChangeKind::ChangedEvent,
429 name: c.name.clone(),
430 detail: format!(
431 "old: {}\n new: {}",
432 event_sig(&c.old),
433 event_sig(&c.new)
434 ),
435 });
436 }
437 for t in &diff.removed_types {
438 breaking.push(VerdictChange {
439 kind: ChangeKind::RemovedType,
440 name: t.clone(),
441 detail: String::new(),
442 });
443 }
444 for c in &diff.changed_types {
445 breaking.push(VerdictChange {
446 kind: ChangeKind::ChangedTypeDefinition,
447 name: c.name.clone(),
448 detail: format!("old: {}\n new: {}", type_sig(&c.old), type_sig(&c.new)),
449 });
450 }
451 for f in &diff.added_functions {
452 non_breaking.push(VerdictChange {
453 kind: ChangeKind::AddedFunction,
454 name: f.name.clone(),
455 detail: String::new(),
456 });
457 }
458 for e in &diff.added_events {
459 non_breaking.push(VerdictChange {
460 kind: ChangeKind::AddedEvent,
461 name: e.clone(),
462 detail: String::new(),
463 });
464 }
465 for t in &diff.added_types {
466 non_breaking.push(VerdictChange {
467 kind: ChangeKind::AddedType,
468 name: t.clone(),
469 detail: String::new(),
470 });
471 }
472
473 let compatible = breaking.is_empty();
474 Self {
475 compatible,
476 breaking_changes: breaking,
477 non_breaking_changes: non_breaking,
478 }
479 }
480}
481
482pub fn size_delta_bytes(old_bytes: usize, new_bytes: usize) -> i64 {
484 new_bytes as i64 - old_bytes as i64
485}
486
487pub fn size_delta_pct(old_bytes: usize, new_bytes: usize) -> Option<f64> {
495 if old_bytes == 0 {
496 return if new_bytes == 0 { Some(0.0) } else { None };
497 }
498 let pct = (new_bytes as f64 - old_bytes as f64) * 100.0 / old_bytes as f64;
499 Some((pct * 10.0).round() / 10.0)
500}
501
502fn sig_of(f: &ContractFunction) -> String {
504 let params = f
505 .parameters
506 .iter()
507 .map(|p| p.type_.name.clone())
508 .collect::<Vec<_>>()
509 .join(", ");
510 let outputs = f
511 .outputs
512 .iter()
513 .map(|o| o.name.clone())
514 .collect::<Vec<_>>()
515 .join(", ");
516 format!("{}({}) -> ({})", f.name, params, outputs)
517}
518
519pub fn event_sig(e: &ContractEvent) -> String {
523 let mut parts: Vec<String> = e
524 .prefix_topics
525 .iter()
526 .map(|t| format!("{}: symbol [prefix]", t))
527 .collect();
528 parts.extend(
529 e.params
530 .iter()
531 .map(|p| format!("{}: {} [{}]", p.name, p.type_.name, p.location)),
532 );
533 format!("{}({}) -> {}", e.name, parts.join(", "), e.data_format)
534}
535
536pub fn type_sig(t: &ContractType) -> String {
542 let members = t
543 .members
544 .iter()
545 .map(|m| {
546 let Some(value) = m.value else {
547 let types = m.types.iter().map(|ty| ty.name.clone()).collect::<Vec<_>>();
548 let shape = match types.len() {
549 0 => "void".to_string(),
550 1 => types.into_iter().next().unwrap_or_default(),
551 _ => format!("({})", types.join(", ")),
552 };
553 return format!("{}: {}", m.name, shape);
554 };
555 let types = m.types.iter().map(|ty| ty.name.clone()).collect::<Vec<_>>();
556 if types.is_empty() {
557 format!("{}={}", m.name, value)
558 } else {
559 format!("{}={}: ({})", m.name, value, types.join(", "))
560 }
561 })
562 .collect::<Vec<_>>();
563 if members.is_empty() {
564 format!("{} {}", t.kind, t.name)
565 } else {
566 format!("{} {} {{ {} }}", t.kind, t.name, members.join(", "))
567 }
568}
569
570pub fn upgrade_safety(old: &ContractSpec, new: &ContractSpec) -> UpgradeVerdict {
574 let diff =
575 diff_specs(old, new, WasmSummary::default(), WasmSummary::default()).unwrap_or_default();
576 UpgradeVerdict::from_diff(&diff)
577}
578
579pub fn upgrade_safety_wasm(old_raw: &[u8], new_raw: &[u8]) -> Result<UpgradeVerdict, WasmError> {
581 let diff = diff_wasm(old_raw, new_raw)?;
582 Ok(UpgradeVerdict::from_diff(&diff))
583}
584
585#[cfg(test)]
586mod tests {
587 use super::*;
588
589 #[test]
592 fn size_delta_bytes_is_signed_and_sums_correctly() {
593 assert_eq!(size_delta_bytes(467, 10543), 10076); assert_eq!(size_delta_bytes(10543, 467), -10076); assert_eq!(size_delta_bytes(530, 530), 0); assert_eq!(size_delta_bytes(0, 0), 0);
597 assert_eq!(size_delta_bytes(0, 4096), 4096);
598 }
599
600 #[test]
601 fn size_delta_pct_rounds_to_one_decimal() {
602 assert_eq!(size_delta_pct(467, 10543), Some(2157.6));
603 assert_eq!(size_delta_pct(198, 530), Some(167.7)); assert_eq!(size_delta_pct(467, 467), Some(0.0)); assert_eq!(size_delta_pct(1000, 900), Some(-10.0)); assert_eq!(size_delta_pct(10, 10), Some(0.0));
607 assert_eq!(size_delta_pct(1000, 1014), Some(1.4));
609 assert_eq!(size_delta_pct(1000, 1015), Some(1.5));
610 }
611
612 #[test]
613 fn size_delta_pct_zero_baseline_is_none_unless_both_zero() {
614 assert_eq!(size_delta_pct(0, 1), None);
616 assert_eq!(size_delta_pct(0, 4096), None);
617 assert_eq!(size_delta_pct(0, 0), Some(0.0)); }
619
620 #[test]
621 fn diff_specs_reports_size_fields() {
622 let old = ContractSpec {
623 env_meta: None,
624 functions: vec![],
625 custom_types: vec![],
626 events: vec![],
627 };
628 let new = old.clone();
629 let diff = diff_specs(
630 &old,
631 &new,
632 WasmSummary {
633 hash: "o".into(),
634 size_bytes: 467,
635 },
636 WasmSummary {
637 hash: "n".into(),
638 size_bytes: 530,
639 },
640 )
641 .unwrap();
642 assert_eq!(diff.size_delta_bytes, 63);
643 assert_eq!(diff.size_delta_pct, Some(13.5));
644 }
647
648 #[test]
649 fn diff_specs_zero_baseline_serializes_null_pct() {
650 let spec = ContractSpec {
651 env_meta: None,
652 functions: vec![],
653 custom_types: vec![],
654 events: vec![],
655 };
656 let diff = diff_specs(
657 &spec,
658 &spec,
659 WasmSummary::default(),
660 WasmSummary {
661 hash: "n".into(),
662 size_bytes: 100,
663 },
664 )
665 .unwrap();
666 assert_eq!(diff.size_delta_bytes, 100);
667 assert_eq!(diff.size_delta_pct, None);
668 }
669 use crate::spec::tests::func_entry;
670 use crate::spec::tests::spec_section;
671 use stellar_xdr::ScSpecTypeDef;
672
673 #[test]
676 fn identical_specs_diff_empty() {
677 let wasm = spec_section(&[func_entry(
678 "greet",
679 vec![("name".into(), ScSpecTypeDef::String)],
680 )]);
681 let d = diff_wasm(&wasm, &wasm).unwrap();
682 assert!(d.is_identical());
683 assert_eq!(d.total_changes(), 0);
684 }
685
686 #[test]
687 fn detects_added_function() {
688 let old = spec_section(&[func_entry("a", vec![])]);
689 let new = spec_section(&[
690 func_entry("a", vec![]),
691 func_entry("b", vec![("x".into(), ScSpecTypeDef::U32)]),
692 ]);
693 let d = diff_wasm(&old, &new).unwrap();
694 assert_eq!(d.added_functions.len(), 1);
695 assert_eq!(d.added_functions[0].name, "b");
696 assert!(d.removed_functions.is_empty());
697 assert!(d.changed_functions.is_empty());
698 }
699
700 #[test]
701 fn detects_removed_function() {
702 let old = spec_section(&[func_entry("a", vec![]), func_entry("b", vec![])]);
703 let new = spec_section(&[func_entry("a", vec![])]);
704 let d = diff_wasm(&old, &new).unwrap();
705 assert_eq!(d.removed_functions.len(), 1);
706 assert_eq!(d.removed_functions[0].name, "b");
707 }
708
709 #[test]
710 fn detects_changed_signature() {
711 let old = spec_section(&[func_entry("f", vec![("x".into(), ScSpecTypeDef::U32)])]);
712 let new = spec_section(&[func_entry("f", vec![("x".into(), ScSpecTypeDef::U64)])]);
713 let d = diff_wasm(&old, &new).unwrap();
714 assert_eq!(d.changed_functions.len(), 1);
715 assert_eq!(d.changed_functions[0].name, "f");
716 assert_eq!(d.changed_functions[0].old.parameters[0].type_.name, "u32");
717 assert_eq!(d.changed_functions[0].new.parameters[0].type_.name, "u64");
718 }
719
720 #[test]
721 fn detects_added_removed_events() {
722 use crate::spec::tests::event_entry;
723 let old = spec_section(&[event_entry("Transfer")]);
724 let new = spec_section(&[event_entry("Mint")]);
725 let d = diff_wasm(&old, &new).unwrap();
726 assert_eq!(d.added_events, vec!["Mint".to_string()]);
727 assert_eq!(d.removed_events, vec!["Transfer".to_string()]);
728 }
729
730 #[test]
731 fn detects_added_removed_types() {
732 use crate::spec::tests::udt_struct_entry;
733 let old = spec_section(&[udt_struct_entry("Point")]);
734 let new = spec_section(&[udt_struct_entry("Circle")]);
735 let d = diff_wasm(&old, &new).unwrap();
736 assert_eq!(d.added_types, vec!["Circle".to_string()]);
737 assert_eq!(d.removed_types, vec!["Point".to_string()]);
738 }
739
740 #[test]
741 fn detects_changed_event_params() {
742 use crate::spec::tests::event_entry_with_params;
743 let old = spec_section(&[event_entry_with_params(
745 "Transfer",
746 vec![
747 ("from".into(), ScSpecTypeDef::Address),
748 ("amount".into(), ScSpecTypeDef::I128),
749 ],
750 )]);
751 let new = spec_section(&[event_entry_with_params(
752 "Transfer",
753 vec![
754 ("from".into(), ScSpecTypeDef::Address),
755 ("amount".into(), ScSpecTypeDef::U64),
756 ("memo".into(), ScSpecTypeDef::String),
757 ],
758 )]);
759 let d = diff_wasm(&old, &new).unwrap();
760 assert_eq!(d.changed_events.len(), 1);
761 assert_eq!(d.changed_events[0].name, "Transfer");
762 assert_eq!(d.changed_events[0].old.params.len(), 2);
763 assert_eq!(d.changed_events[0].old.params[1].type_.name, "i128");
764 assert_eq!(d.changed_events[0].new.params.len(), 3);
765 assert_eq!(d.changed_events[0].new.params[1].type_.name, "u64");
766 assert!(d.added_events.is_empty());
768 assert!(d.removed_events.is_empty());
769 }
770
771 #[test]
772 fn identical_events_produce_no_change() {
773 use crate::spec::tests::event_entry_with_params;
774 let wasm = spec_section(&[event_entry_with_params(
775 "Transfer",
776 vec![("from".into(), ScSpecTypeDef::Address)],
777 )]);
778 let d = diff_wasm(&wasm, &wasm).unwrap();
779 assert!(d.changed_events.is_empty());
780 assert!(d.is_identical());
781 assert_eq!(d.total_changes(), 0);
782 }
783
784 #[test]
785 fn detects_changed_type_members() {
786 use crate::spec::tests::udt_struct_entry_with_fields;
787 let old = spec_section(&[udt_struct_entry_with_fields(
789 "Point",
790 vec![("x".into(), ScSpecTypeDef::I32)],
791 )]);
792 let new = spec_section(&[udt_struct_entry_with_fields(
793 "Point",
794 vec![
795 ("x".into(), ScSpecTypeDef::I64),
796 ("z".into(), ScSpecTypeDef::I32),
797 ],
798 )]);
799 let d = diff_wasm(&old, &new).unwrap();
800 assert_eq!(d.changed_types.len(), 1);
801 assert_eq!(d.changed_types[0].name, "Point");
802 assert_eq!(d.changed_types[0].old.members.len(), 1);
803 assert_eq!(d.changed_types[0].old.members[0].types[0].name, "i32");
804 assert_eq!(d.changed_types[0].new.members.len(), 2);
805 assert_eq!(d.changed_types[0].new.members[0].types[0].name, "i64");
806 assert!(d.added_types.is_empty());
808 assert!(d.removed_types.is_empty());
809 }
810
811 #[test]
812 fn detects_type_kind_change() {
813 use stellar_xdr::{
814 ScSpecEntry, ScSpecUdtEnumCaseV0, ScSpecUdtEnumV0, ScSpecUdtStructFieldV0,
815 ScSpecUdtStructV0,
816 };
817 let struct_entry = ScSpecEntry::UdtStructV0(ScSpecUdtStructV0 {
818 doc: "".try_into().unwrap(),
819 lib: "soroban_sdk".try_into().unwrap(),
820 name: "Status".try_into().unwrap(),
821 fields: vec![ScSpecUdtStructFieldV0 {
822 doc: "".try_into().unwrap(),
823 name: "active".try_into().unwrap(),
824 type_: ScSpecTypeDef::Bool,
825 }]
826 .try_into()
827 .unwrap(),
828 });
829 let enum_entry = ScSpecEntry::UdtEnumV0(ScSpecUdtEnumV0 {
830 doc: "".try_into().unwrap(),
831 lib: "soroban_sdk".try_into().unwrap(),
832 name: "Status".try_into().unwrap(),
833 cases: vec![ScSpecUdtEnumCaseV0 {
834 doc: "".try_into().unwrap(),
835 name: "Active".try_into().unwrap(),
836 value: 0,
837 }]
838 .try_into()
839 .unwrap(),
840 });
841 let d = diff_wasm(&spec_section(&[struct_entry]), &spec_section(&[enum_entry])).unwrap();
842 assert_eq!(d.changed_types.len(), 1);
843 assert_eq!(d.changed_types[0].name, "Status");
844 assert_eq!(d.changed_types[0].old.kind, "struct");
845 assert_eq!(d.changed_types[0].new.kind, "enum");
846 }
847
848 #[test]
849 fn identical_types_produce_no_change() {
850 use crate::spec::tests::udt_struct_entry_with_fields;
851 let wasm = spec_section(&[udt_struct_entry_with_fields(
852 "Point",
853 vec![("x".into(), ScSpecTypeDef::I32)],
854 )]);
855 let d = diff_wasm(&wasm, &wasm).unwrap();
856 assert!(d.changed_types.is_empty());
857 assert!(d.is_identical());
858 assert_eq!(d.total_changes(), 0);
859 }
860
861 #[test]
862 fn parse_error_propagates() {
863 let old = vec![0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00];
865 let new = spec_section(&[func_entry("a", vec![])]);
866 let err = diff_wasm(&old, &new);
867 assert!(matches!(err, Err(WasmError::NoContractSpec)));
868 }
869
870 #[test]
873 fn verdict_flags_removed_function_breaking() {
874 let old = spec_section(&[func_entry("a", vec![]), func_entry("b", vec![])]);
875 let new = spec_section(&[func_entry("a", vec![])]);
876 let v = upgrade_safety_wasm(&old, &new).unwrap();
877 assert!(!v.compatible);
878 assert_eq!(v.breaking_changes.len(), 1);
879 assert_eq!(v.breaking_changes[0].kind, ChangeKind::RemovedFunction);
880 assert_eq!(v.breaking_changes[0].name, "b");
881 }
882
883 #[test]
884 fn verdict_flags_changed_signature_breaking() {
885 let old = spec_section(&[func_entry("f", vec![("x".into(), ScSpecTypeDef::U32)])]);
886 let new = spec_section(&[func_entry("f", vec![("x".into(), ScSpecTypeDef::U64)])]);
887 let v = upgrade_safety_wasm(&old, &new).unwrap();
888 assert!(!v.compatible);
889 assert_eq!(v.breaking_changes.len(), 1);
890 assert_eq!(v.breaking_changes[0].kind, ChangeKind::ChangedSignature);
891 assert_eq!(v.breaking_changes[0].name, "f");
892 }
893
894 #[test]
895 fn verdict_flags_changed_event_breaking() {
896 use crate::spec::tests::event_entry_with_params;
897 let old = spec_section(&[event_entry_with_params(
898 "Transfer",
899 vec![("amount".into(), ScSpecTypeDef::I128)],
900 )]);
901 let new = spec_section(&[event_entry_with_params(
902 "Transfer",
903 vec![("amount".into(), ScSpecTypeDef::U64)],
904 )]);
905 let v = upgrade_safety_wasm(&old, &new).unwrap();
906 assert!(!v.compatible);
907 assert_eq!(v.breaking_changes.len(), 1);
908 assert_eq!(v.breaking_changes[0].kind, ChangeKind::ChangedEvent);
909 assert_eq!(v.breaking_changes[0].name, "Transfer");
910 let d = &v.breaking_changes[0].detail;
912 assert!(d.contains("i128"), "detail should show the old type: {}", d);
913 assert!(d.contains("u64"), "detail should show the new type: {}", d);
914 }
915
916 #[test]
917 fn detects_changed_enum_discriminant() {
918 use stellar_xdr::{ScSpecEntry, ScSpecUdtEnumCaseV0, ScSpecUdtEnumV0};
919 let enum_with = |value: u32| {
922 ScSpecEntry::UdtEnumV0(ScSpecUdtEnumV0 {
923 doc: "".try_into().unwrap(),
924 lib: "soroban_sdk".try_into().unwrap(),
925 name: "Status".try_into().unwrap(),
926 cases: vec![
927 ScSpecUdtEnumCaseV0 {
928 doc: "".try_into().unwrap(),
929 name: "Active".try_into().unwrap(),
930 value: 0,
931 },
932 ScSpecUdtEnumCaseV0 {
933 doc: "".try_into().unwrap(),
934 name: "Paused".try_into().unwrap(),
935 value,
936 },
937 ]
938 .try_into()
939 .unwrap(),
940 })
941 };
942 let d = diff_wasm(
943 &spec_section(&[enum_with(1)]),
944 &spec_section(&[enum_with(2)]),
945 )
946 .unwrap();
947 assert_eq!(d.changed_types.len(), 1);
948 assert_eq!(d.changed_types[0].name, "Status");
949 assert_eq!(
950 d.changed_types[0].old.members[1].value,
951 Some(1),
952 "old discriminant must be retained"
953 );
954 assert_eq!(d.changed_types[0].new.members[1].value, Some(2));
955 let v = upgrade_safety(
957 &parse_contract_spec(&spec_section(&[enum_with(1)])).unwrap(),
958 &parse_contract_spec(&spec_section(&[enum_with(2)])).unwrap(),
959 );
960 assert!(!v.compatible);
961 assert_eq!(
962 v.breaking_changes[0].kind,
963 ChangeKind::ChangedTypeDefinition
964 );
965 assert!(
966 v.breaking_changes[0].detail.contains("Paused=1"),
967 "{}",
968 v.breaking_changes[0].detail
969 );
970 assert!(
971 v.breaking_changes[0].detail.contains("Paused=2"),
972 "{}",
973 v.breaking_changes[0].detail
974 );
975 }
976
977 #[test]
978 fn identical_enum_discriminants_produce_no_change() {
979 use stellar_xdr::{ScSpecEntry, ScSpecUdtEnumCaseV0, ScSpecUdtEnumV0};
980 let entry = ScSpecEntry::UdtEnumV0(ScSpecUdtEnumV0 {
981 doc: "".try_into().unwrap(),
982 lib: "soroban_sdk".try_into().unwrap(),
983 name: "Status".try_into().unwrap(),
984 cases: vec![ScSpecUdtEnumCaseV0 {
985 doc: "".try_into().unwrap(),
986 name: "Active".try_into().unwrap(),
987 value: 7,
988 }]
989 .try_into()
990 .unwrap(),
991 });
992 let wasm = spec_section(&[entry]);
993 let d = diff_wasm(&wasm, &wasm).unwrap();
994 assert!(d.changed_types.is_empty());
995 assert!(d.is_identical());
996 }
997
998 #[test]
999 fn verdict_flags_changed_type_definition_breaking() {
1000 use crate::spec::tests::udt_struct_entry_with_fields;
1001 let old = spec_section(&[udt_struct_entry_with_fields(
1002 "Point",
1003 vec![("x".into(), ScSpecTypeDef::I32)],
1004 )]);
1005 let new = spec_section(&[udt_struct_entry_with_fields(
1006 "Point",
1007 vec![("x".into(), ScSpecTypeDef::I64)],
1008 )]);
1009 let v = upgrade_safety_wasm(&old, &new).unwrap();
1010 assert!(!v.compatible);
1011 assert_eq!(v.breaking_changes.len(), 1);
1012 assert_eq!(
1013 v.breaking_changes[0].kind,
1014 ChangeKind::ChangedTypeDefinition
1015 );
1016 assert_eq!(v.breaking_changes[0].name, "Point");
1017 let d = &v.breaking_changes[0].detail;
1018 assert!(d.contains("i32"), "detail should show the old type: {}", d);
1019 assert!(d.contains("i64"), "detail should show the new type: {}", d);
1020 }
1021
1022 #[test]
1023 fn verdict_flags_removed_event_breaking() {
1024 use crate::spec::tests::event_entry;
1025 let old = spec_section(&[event_entry("Transfer")]);
1026 let new = spec_section(&[]);
1027 let v = upgrade_safety_wasm(&old, &new).unwrap();
1028 assert!(!v.compatible);
1029 assert_eq!(v.breaking_changes.len(), 1);
1030 assert_eq!(v.breaking_changes[0].kind, ChangeKind::RemovedEvent);
1031 assert_eq!(v.breaking_changes[0].name, "Transfer");
1032 }
1033
1034 #[test]
1035 fn verdict_flags_removed_type_breaking() {
1036 use crate::spec::tests::udt_struct_entry;
1037 let old = spec_section(&[udt_struct_entry("Point")]);
1038 let new = spec_section(&[]);
1039 let v = upgrade_safety_wasm(&old, &new).unwrap();
1040 assert!(!v.compatible);
1041 assert_eq!(v.breaking_changes.len(), 1);
1042 assert_eq!(v.breaking_changes[0].kind, ChangeKind::RemovedType);
1043 assert_eq!(v.breaking_changes[0].name, "Point");
1044 }
1045
1046 #[test]
1047 fn verdict_only_additions_is_compatible() {
1048 use crate::spec::tests::event_entry;
1049 let old = spec_section(&[func_entry("a", vec![])]);
1050 let new = spec_section(&[
1051 func_entry("a", vec![]),
1052 func_entry("b", vec![("x".into(), ScSpecTypeDef::U32)]),
1053 ]);
1054 let old2 = spec_section(&[func_entry("a", vec![])]);
1055 let new2 = spec_section(&[func_entry("a", vec![]), event_entry("Mint")]);
1056 let v = upgrade_safety_wasm(&old, &new).unwrap();
1057 assert!(v.compatible);
1058 let v2 = upgrade_safety_wasm(&old2, &new2).unwrap();
1059 assert!(v2.compatible);
1060 assert!(v2.breaking_changes.is_empty());
1061 assert!(!v2.non_breaking_changes.is_empty());
1062 assert!(v2
1063 .non_breaking_changes
1064 .iter()
1065 .any(|c| c.kind == ChangeKind::AddedEvent && c.name == "Mint"));
1066 }
1067
1068 #[test]
1069 fn verdict_identical_is_compatible() {
1070 let wasm = spec_section(&[func_entry(
1071 "greet",
1072 vec![("name".into(), ScSpecTypeDef::String)],
1073 )]);
1074 let v = upgrade_safety_wasm(&wasm, &wasm).unwrap();
1075 assert!(v.compatible);
1076 assert!(v.breaking_changes.is_empty());
1077 assert!(v.non_breaking_changes.is_empty());
1078 }
1079}