1#![allow(deprecated)]
38mod header;
44mod package_json;
45pub mod serde_with;
46
47pub use header::{EnvelopeConfig, EnvelopeFormat, MAGIC_NUMBERS, ZstdConfig};
48pub use package_json::PackageEncodingError;
49
50use crate::{Hugr, HugrView};
51use crate::{
52 extension::{ExtensionRegistry, Version},
53 package::Package,
54};
55use header::EnvelopeHeader;
56use std::io::BufRead;
57use std::io::Write;
58use std::str::FromStr;
59use thiserror::Error;
60
61#[allow(unused_imports)]
62use itertools::Itertools as _;
63
64use crate::import::ImportError;
65use crate::{Extension, import::import_package};
66
67pub const GENERATOR_KEY: &str = "core.generator";
69pub const USED_EXTENSIONS_KEY: &str = "core.used_extensions";
71
72pub fn get_generator<H: HugrView>(modules: &[H]) -> Option<String> {
77 let generators: Vec<String> = modules
78 .iter()
79 .filter_map(|hugr| hugr.get_metadata(hugr.module_root(), GENERATOR_KEY))
80 .map(format_generator)
81 .collect();
82 if generators.is_empty() {
83 return None;
84 }
85
86 Some(generators.join(", "))
87}
88
89pub fn format_generator(json_val: &serde_json::Value) -> String {
91 match json_val {
92 serde_json::Value::String(s) => s.clone(),
93 serde_json::Value::Object(obj) => {
94 if let (Some(name), version) = (
95 obj.get("name").and_then(|v| v.as_str()),
96 obj.get("version").and_then(|v| v.as_str()),
97 ) {
98 if let Some(version) = version {
99 format!("{name}-v{version}")
101 } else {
102 name.to_string()
103 }
104 } else {
105 json_val.to_string()
107 }
108 }
109 _ => json_val.to_string(),
111 }
112}
113
114fn gen_str(generator: &Option<String>) -> String {
115 match generator {
116 Some(g) => format!("\ngenerated by {g}"),
117 None => String::new(),
118 }
119}
120
121#[derive(Error, Debug)]
123#[error("{inner}{}", gen_str(&self.generator))]
124pub struct WithGenerator<E: std::fmt::Display> {
125 inner: Box<E>,
126 generator: Option<String>,
128}
129
130impl<E: std::fmt::Display> WithGenerator<E> {
131 fn new(err: E, modules: &[impl HugrView]) -> Self {
132 Self {
133 inner: Box::new(err),
134 generator: get_generator(modules),
135 }
136 }
137}
138
139pub fn read_envelope(
148 mut reader: impl BufRead,
149 registry: &ExtensionRegistry,
150) -> Result<(EnvelopeConfig, Package), EnvelopeError> {
151 let header = EnvelopeHeader::read(&mut reader)?;
152
153 let package = match header.zstd {
154 #[cfg(feature = "zstd")]
155 true => read_impl(
156 std::io::BufReader::new(zstd::Decoder::new(reader)?),
157 header,
158 registry,
159 ),
160 #[cfg(not(feature = "zstd"))]
161 true => Err(EnvelopeError::ZstdUnsupported),
162 false => read_impl(reader, header, registry),
163 }?;
164 Ok((header.config(), package))
165}
166
167pub fn write_envelope(
172 writer: impl Write,
173 package: &Package,
174 config: EnvelopeConfig,
175) -> Result<(), EnvelopeError> {
176 write_envelope_impl(writer, &package.modules, &package.extensions, config)
177}
178
179pub(crate) fn write_envelope_impl<'h>(
184 mut writer: impl Write,
185 hugrs: impl IntoIterator<Item = &'h Hugr>,
186 extensions: &ExtensionRegistry,
187 config: EnvelopeConfig,
188) -> Result<(), EnvelopeError> {
189 let header = config.make_header();
190 header.write(&mut writer)?;
191
192 match config.zstd {
193 #[cfg(feature = "zstd")]
194 Some(zstd) => {
195 let writer = zstd::Encoder::new(writer, zstd.level())?.auto_finish();
196 write_impl(writer, hugrs, extensions, config)?;
197 }
198 #[cfg(not(feature = "zstd"))]
199 Some(_) => return Err(EnvelopeError::ZstdUnsupported),
200 None => write_impl(writer, hugrs, extensions, config)?,
201 }
202
203 Ok(())
204}
205
206#[derive(Debug, Error)]
208#[non_exhaustive]
209pub enum EnvelopeError {
210 #[error(
212 "Bad magic number. expected 0x{:X} found 0x{:X}",
213 u64::from_be_bytes(*expected),
214 u64::from_be_bytes(*found)
215 )]
216 MagicNumber {
217 expected: [u8; 8],
221 found: [u8; 8],
223 },
224 #[error("Format descriptor {descriptor} is invalid.")]
226 InvalidFormatDescriptor {
227 descriptor: usize,
229 },
230 #[error("Payload format {format} is not supported.{}",
232 match feature {
233 Some(f) => format!(" This requires the '{f}' feature for `hugr`."),
234 None => String::new()
235 },
236 )]
237 FormatUnsupported {
238 format: EnvelopeFormat,
240 feature: Option<&'static str>,
242 },
243 #[error("Envelope format {format} cannot be represented as ASCII.")]
247 NonASCIIFormat {
248 format: EnvelopeFormat,
250 },
251 #[error("Zstd compression is not supported. This requires the 'zstd' feature for `hugr`.")]
253 ZstdUnsupported,
254 #[error("Expected an envelope containing a single hugr, but it contained {}.", if *count == 0 {
256 "none".to_string()
257 } else {
258 count.to_string()
259 })]
260 ExpectedSingleHugr {
261 count: usize,
263 },
264 #[error(transparent)]
266 SerdeError {
267 #[from]
269 source: serde_json::Error,
270 },
271 #[error(transparent)]
273 IO {
274 #[from]
276 source: std::io::Error,
277 },
278 #[error(transparent)]
280 PackageEncoding {
281 #[from]
283 source: PackageEncodingError,
284 },
285 #[error(transparent)]
287 ModelImport {
288 #[from]
290 source: ImportError,
291 },
293 #[error(transparent)]
295 ModelRead {
296 #[from]
298 source: hugr_model::v0::binary::ReadError,
299 },
300 #[error(transparent)]
302 ModelWrite {
303 #[from]
305 source: hugr_model::v0::binary::WriteError,
306 },
307 #[error("Model text parsing error")]
309 ModelTextRead {
310 #[from]
312 source: hugr_model::v0::ast::ParseError,
313 },
314 #[error(transparent)]
316 ModelTextResolve {
317 #[from]
319 source: hugr_model::v0::ast::ResolveError,
320 },
321 #[error(transparent)]
323 ExtensionLoad {
324 #[from]
326 source: crate::extension::ExtensionRegistryLoadError,
327 },
328 #[error(
330 "The envelope configuration has unknown {}. Please update your HUGR version.",
331 if flag_ids.len() == 1 {format!("flag #{}", flag_ids[0])} else {format!("flags {}", flag_ids.iter().join(", "))}
332 )]
333 FlagUnsupported {
334 flag_ids: Vec<usize>,
336 },
337}
338
339fn read_impl(
341 payload: impl BufRead,
342 header: EnvelopeHeader,
343 registry: &ExtensionRegistry,
344) -> Result<Package, EnvelopeError> {
345 match header.format {
346 #[allow(deprecated)]
347 EnvelopeFormat::PackageJson => Ok(package_json::from_json_reader(payload, registry)?),
348 EnvelopeFormat::Model | EnvelopeFormat::ModelWithExtensions => {
349 decode_model(payload, registry, header.format)
350 }
351 EnvelopeFormat::ModelText | EnvelopeFormat::ModelTextWithExtensions => {
352 decode_model_ast(payload, registry, header.format)
353 }
354 }
355}
356
357fn decode_model(
365 mut stream: impl BufRead,
366 extension_registry: &ExtensionRegistry,
367 format: EnvelopeFormat,
368) -> Result<Package, EnvelopeError> {
369 use hugr_model::v0::bumpalo::Bump;
370
371 if format.model_version() != Some(0) {
372 return Err(EnvelopeError::FormatUnsupported {
373 format,
374 feature: None,
375 });
376 }
377
378 let bump = Bump::default();
379 let model_package = hugr_model::v0::binary::read_from_reader(&mut stream, &bump)?;
380
381 let mut extension_registry = extension_registry.clone();
382 if format == EnvelopeFormat::ModelWithExtensions {
383 let extra_extensions = ExtensionRegistry::load_json(stream, &extension_registry)?;
384 extension_registry.extend(extra_extensions);
385 }
386
387 Ok(import_package(&model_package, &extension_registry)?)
388}
389
390fn decode_model_ast(
398 mut stream: impl BufRead,
399 extension_registry: &ExtensionRegistry,
400 format: EnvelopeFormat,
401) -> Result<Package, EnvelopeError> {
402 use crate::import::import_package;
403 use hugr_model::v0::bumpalo::Bump;
404
405 if format.model_version() != Some(0) {
406 return Err(EnvelopeError::FormatUnsupported {
407 format,
408 feature: None,
409 });
410 }
411
412 let mut extension_registry = extension_registry.clone();
413 if format == EnvelopeFormat::ModelTextWithExtensions {
414 let deserializer = serde_json::Deserializer::from_reader(&mut stream);
415 let extra_extensions = deserializer
417 .into_iter::<Vec<Extension>>()
418 .next()
419 .unwrap_or(Ok(vec![]))?;
420 for ext in extra_extensions {
421 extension_registry.register_updated(ext);
422 }
423 }
424
425 let mut buffer = String::new();
429 stream.read_to_string(&mut buffer)?;
430 let ast_package = hugr_model::v0::ast::Package::from_str(&buffer)?;
431
432 let bump = Bump::default();
433 let model_package = ast_package.resolve(&bump)?;
434
435 Ok(import_package(&model_package, &extension_registry)?)
436}
437
438fn write_impl<'h>(
440 writer: impl Write,
441 hugrs: impl IntoIterator<Item = &'h Hugr>,
442 extensions: &ExtensionRegistry,
443 config: EnvelopeConfig,
444) -> Result<(), EnvelopeError> {
445 match config.format {
446 #[allow(deprecated)]
447 EnvelopeFormat::PackageJson => package_json::to_json_writer(hugrs, extensions, writer)?,
448 EnvelopeFormat::Model
449 | EnvelopeFormat::ModelWithExtensions
450 | EnvelopeFormat::ModelText
451 | EnvelopeFormat::ModelTextWithExtensions => {
452 encode_model(writer, hugrs, extensions, config.format)?;
453 }
454 }
455 Ok(())
456}
457
458fn encode_model<'h>(
459 mut writer: impl Write,
460 hugrs: impl IntoIterator<Item = &'h Hugr>,
461 extensions: &ExtensionRegistry,
462 format: EnvelopeFormat,
463) -> Result<(), EnvelopeError> {
464 use hugr_model::v0::{binary::write_to_writer, bumpalo::Bump};
465
466 use crate::export::export_package;
467
468 if format.model_version() != Some(0) {
469 return Err(EnvelopeError::FormatUnsupported {
470 format,
471 feature: None,
472 });
473 }
474
475 if format == EnvelopeFormat::ModelTextWithExtensions {
477 serde_json::to_writer(&mut writer, &extensions.iter().collect_vec())?;
478 }
479
480 let bump = Bump::default();
481 let model_package = export_package(hugrs, extensions, &bump);
482
483 match format {
484 EnvelopeFormat::Model | EnvelopeFormat::ModelWithExtensions => {
485 write_to_writer(&model_package, &mut writer)?;
486 }
487 EnvelopeFormat::ModelText | EnvelopeFormat::ModelTextWithExtensions => {
488 let model_package = model_package.as_ast().unwrap();
489 writeln!(writer, "{model_package}")?;
490 }
491 _ => unreachable!(),
492 }
493
494 if format == EnvelopeFormat::ModelWithExtensions {
496 serde_json::to_writer(writer, &extensions.iter().collect_vec())?;
497 }
498
499 Ok(())
500}
501
502#[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize)]
503struct UsedExtension {
504 name: String,
505 version: Version,
506}
507
508#[derive(Debug, Error)]
509#[error(
510 "Extension '{name}' version mismatch: registered version is {registered}, but used version is {used}"
511)]
512pub struct ExtensionVersionMismatch {
515 name: String,
516 registered: Version,
517 used: Version,
518}
519
520#[derive(Debug, Error)]
521#[non_exhaustive]
522pub enum ExtensionBreakingError {
524 #[error("{0}")]
526 ExtensionVersionMismatch(ExtensionVersionMismatch),
527
528 #[error("Failed to deserialize used extensions metadata")]
530 Deserialization(#[from] serde_json::Error),
531}
532fn check_breaking_extensions(
537 hugr: impl crate::HugrView,
538 registry: &ExtensionRegistry,
539) -> Result<(), ExtensionBreakingError> {
540 let Some(exts) = hugr.get_metadata(hugr.module_root(), USED_EXTENSIONS_KEY) else {
541 return Ok(()); };
543 let used_exts: Vec<UsedExtension> = serde_json::from_value(exts.clone())?; for ext in used_exts {
546 let Some(registered) = registry.get(ext.name.as_str()) else {
547 continue; };
549 if !compatible_versions(registered.version(), &ext.version) {
550 return Err(ExtensionBreakingError::ExtensionVersionMismatch(
553 ExtensionVersionMismatch {
554 name: ext.name,
555 registered: registered.version().clone(),
556 used: ext.version,
557 },
558 ));
559 }
560 }
561
562 Ok(())
563}
564
565fn compatible_versions(v1: &Version, v2: &Version) -> bool {
569 if v1.major != v2.major {
570 return false; }
572
573 if v1.major == 0 {
574 return v1.minor == v2.minor;
576 }
577
578 true
579}
580
581#[cfg(test)]
582pub(crate) mod test {
583 use super::*;
584 use cool_asserts::assert_matches;
585 use rstest::rstest;
586 use std::borrow::Cow;
587 use std::io::BufReader;
588
589 use crate::HugrView;
590 use crate::builder::test::{multi_module_package, simple_package};
591 use crate::extension::{Extension, ExtensionRegistry, Version};
592 use crate::extension::{ExtensionId, PRELUDE_REGISTRY};
593 use crate::hugr::HugrMut;
594 use crate::hugr::test::check_hugr_equality;
595 use crate::std_extensions::STD_REG;
596 use serde_json::json;
597 use std::sync::Arc;
598
599 fn join_extensions<'a>(
603 extensions: &'a ExtensionRegistry,
604 other: &ExtensionRegistry,
605 ) -> Cow<'a, ExtensionRegistry> {
606 if other.iter().all(|e| extensions.contains(e.name())) {
607 Cow::Borrowed(extensions)
608 } else {
609 let mut extensions = extensions.clone();
610 extensions.extend(other);
611 Cow::Owned(extensions)
612 }
613 }
614
615 pub(crate) fn check_hugr_roundtrip(hugr: &Hugr, config: EnvelopeConfig) -> Hugr {
624 let mut buffer = Vec::new();
625 hugr.store(&mut buffer, config).unwrap();
626
627 let extensions = join_extensions(&STD_REG, hugr.extensions());
628
629 let reader = BufReader::new(buffer.as_slice());
630 let extracted = Hugr::load(reader, Some(&extensions)).unwrap();
631
632 check_hugr_equality(&extracted, hugr);
633 extracted
634 }
635
636 #[rstest]
637 fn errors() {
638 let package = simple_package();
639 assert_matches!(
640 package.store_str(EnvelopeConfig::binary()),
641 Err(EnvelopeError::NonASCIIFormat { .. })
642 );
643 }
644
645 #[rstest]
646 #[case::empty(Package::default())]
647 #[case::simple(simple_package())]
648 #[case::multi(multi_module_package())]
649 fn text_roundtrip(#[case] package: Package) {
650 let envelope = package.store_str(EnvelopeConfig::text()).unwrap();
651 let new_package = Package::load_str(&envelope, None).unwrap();
652 assert_eq!(package, new_package);
653 }
654
655 #[rstest]
656 #[case::empty(Package::default())]
657 #[case::simple(simple_package())]
658 #[case::multi(multi_module_package())]
659 #[cfg_attr(all(miri, feature = "zstd"), ignore)] fn compressed_roundtrip(#[case] package: Package) {
661 let mut buffer = Vec::new();
662 let config = EnvelopeConfig {
663 format: EnvelopeFormat::PackageJson,
664 zstd: Some(ZstdConfig::default()),
665 };
666 let res = package.store(&mut buffer, config);
667
668 match cfg!(feature = "zstd") {
669 true => res.unwrap(),
670 false => {
671 assert_matches!(res, Err(EnvelopeError::ZstdUnsupported));
672 return;
673 }
674 }
675
676 let (decoded_config, new_package) =
677 read_envelope(BufReader::new(buffer.as_slice()), &PRELUDE_REGISTRY).unwrap();
678
679 assert_eq!(config.format, decoded_config.format);
680 assert_eq!(config.zstd.is_some(), decoded_config.zstd.is_some());
681 assert_eq!(package, new_package);
682 }
683
684 #[rstest]
685 #[case::empty_model(Package::default(), EnvelopeFormat::Model)]
687 #[case::empty_model_exts(Package::default(), EnvelopeFormat::ModelWithExtensions)]
688 #[case::empty_text(Package::default(), EnvelopeFormat::ModelText)]
689 #[case::empty_text_exts(Package::default(), EnvelopeFormat::ModelTextWithExtensions)]
690 #[case::simple_bin(simple_package(), EnvelopeFormat::Model)]
692 #[case::simple_bin_exts(simple_package(), EnvelopeFormat::ModelWithExtensions)]
693 #[case::simple_text(simple_package(), EnvelopeFormat::ModelText)]
694 #[case::simple_text_exts(simple_package(), EnvelopeFormat::ModelTextWithExtensions)]
695 #[case::multi_bin(multi_module_package(), EnvelopeFormat::Model)]
697 #[case::multi_bin_exts(multi_module_package(), EnvelopeFormat::ModelWithExtensions)]
698 #[case::multi_text(multi_module_package(), EnvelopeFormat::ModelText)]
699 #[case::multi_text_exts(multi_module_package(), EnvelopeFormat::ModelTextWithExtensions)]
700 fn model_roundtrip(#[case] package: Package, #[case] format: EnvelopeFormat) {
701 let mut buffer = Vec::new();
702 let config = EnvelopeConfig { format, zstd: None };
703 package.store(&mut buffer, config).unwrap();
704
705 let (decoded_config, new_package) =
706 read_envelope(BufReader::new(buffer.as_slice()), &PRELUDE_REGISTRY).unwrap();
707
708 assert_eq!(config.format, decoded_config.format);
709 assert_eq!(config.zstd.is_some(), decoded_config.zstd.is_some());
710
711 assert_eq!(package, new_package);
712 }
713
714 #[rstest]
715 #[case::simple(simple_package())]
716 fn test_check_breaking_extensions(#[case] mut package: Package) {
717 let test_ext_v0 =
719 Extension::new(ExtensionId::new_unchecked("test-v0"), Version::new(0, 2, 3));
720 let test_ext_v1 =
722 Extension::new(ExtensionId::new_unchecked("test-v1"), Version::new(1, 2, 3));
723
724 let registry =
726 ExtensionRegistry::new([Arc::new(test_ext_v0.clone()), Arc::new(test_ext_v1.clone())]);
727 let mut hugr = package.modules.remove(0);
728
729 assert_matches!(check_breaking_extensions(&hugr, ®istry), Ok(()));
731
732 let used_exts = json!([{ "name": "test-v0", "version": "0.2.3" }]);
734 hugr.set_metadata(hugr.module_root(), USED_EXTENSIONS_KEY, used_exts);
735 assert_matches!(check_breaking_extensions(&hugr, ®istry), Ok(()));
736
737 let used_exts = json!([{ "name": "test-v0", "version": "0.2.4" }]);
739 hugr.set_metadata(hugr.module_root(), USED_EXTENSIONS_KEY, used_exts);
740 assert_matches!(check_breaking_extensions(&hugr, ®istry), Ok(()));
741
742 let used_exts = json!([{ "name": "test-v0", "version": "0.3.3" }]);
744 hugr.set_metadata(hugr.module_root(), USED_EXTENSIONS_KEY, used_exts);
745 assert_matches!(
746 check_breaking_extensions(&hugr, ®istry),
747 Err(ExtensionBreakingError::ExtensionVersionMismatch(ExtensionVersionMismatch {
748 name,
749 registered,
750 used
751 })) if name == "test-v0" && registered == Version::new(0, 2, 3) && used == Version::new(0, 3, 3)
752 );
753
754 let used_exts = json!([{ "name": "test-v0", "version": "1.2.3" }]);
756 hugr.set_metadata(hugr.module_root(), USED_EXTENSIONS_KEY, used_exts);
757 assert_matches!(
758 check_breaking_extensions(&hugr, ®istry),
759 Err(ExtensionBreakingError::ExtensionVersionMismatch(ExtensionVersionMismatch {
760 name,
761 registered,
762 used
763 })) if name == "test-v0" && registered == Version::new(0, 2, 3) && used == Version::new(1, 2, 3)
764 );
765
766 let used_exts = json!([{ "name": "test-v1", "version": "1.2.3" }]);
768 hugr.set_metadata(hugr.module_root(), USED_EXTENSIONS_KEY, used_exts);
769 assert_matches!(check_breaking_extensions(&hugr, ®istry), Ok(()));
770
771 let used_exts = json!([{ "name": "test-v1", "version": "1.3.0" }]);
773 hugr.set_metadata(hugr.module_root(), USED_EXTENSIONS_KEY, used_exts);
774 assert_matches!(check_breaking_extensions(&hugr, ®istry), Ok(()));
775
776 let used_exts = json!([{ "name": "test-v1", "version": "1.2.4" }]);
778 hugr.set_metadata(hugr.module_root(), USED_EXTENSIONS_KEY, used_exts);
779 assert_matches!(check_breaking_extensions(&hugr, ®istry), Ok(()));
780
781 let used_exts = json!([{ "name": "test-v1", "version": "2.2.3" }]);
783 hugr.set_metadata(hugr.module_root(), USED_EXTENSIONS_KEY, used_exts);
784 assert_matches!(
785 check_breaking_extensions(&hugr, ®istry),
786 Err(ExtensionBreakingError::ExtensionVersionMismatch(ExtensionVersionMismatch {
787 name,
788 registered,
789 used
790 })) if name == "test-v1" && registered == Version::new(1, 2, 3) && used == Version::new(2, 2, 3)
791 );
792
793 let used_exts = json!([{ "name": "unknown", "version": "1.0.0" }]);
795 hugr.set_metadata(hugr.module_root(), USED_EXTENSIONS_KEY, used_exts);
796 assert_matches!(check_breaking_extensions(&hugr, ®istry), Ok(()));
797
798 let used_exts = json!([
800 { "name": "unknown", "version": "1.0.0" },
801 { "name": "test-v1", "version": "2.0.0" }
802 ]);
803 hugr.set_metadata(hugr.module_root(), USED_EXTENSIONS_KEY, used_exts);
804 assert_matches!(
805 check_breaking_extensions(&hugr, ®istry),
806 Err(ExtensionBreakingError::ExtensionVersionMismatch(ExtensionVersionMismatch {
807 name,
808 registered,
809 used
810 })) if name == "test-v1" && registered == Version::new(1, 2, 3) && used == Version::new(2, 0, 0)
811 );
812
813 hugr.set_metadata(
815 hugr.module_root(),
816 USED_EXTENSIONS_KEY,
817 json!("not an array"),
818 );
819 assert_matches!(
820 check_breaking_extensions(&hugr, ®istry),
821 Err(ExtensionBreakingError::Deserialization(_))
822 );
823
824 let used_exts = json!([
826 { "name": "test-v0", "version": "0.2.5" },
827 { "name": "test-v1", "version": "1.9.9" }
828 ]);
829 hugr.set_metadata(hugr.module_root(), USED_EXTENSIONS_KEY, used_exts);
830 assert_matches!(check_breaking_extensions(&hugr, ®istry), Ok(()));
831 }
832
833 #[test]
834 fn test_with_generator_error_message() {
835 let test_ext = Extension::new(ExtensionId::new_unchecked("test"), Version::new(1, 0, 0));
836 let registry = ExtensionRegistry::new([Arc::new(test_ext)]);
837
838 let mut hugr = simple_package().modules.remove(0);
839
840 let generator_name = json!({ "name": "TestGenerator", "version": "1.2.3" });
842 hugr.set_metadata(hugr.module_root(), GENERATOR_KEY, generator_name.clone());
843
844 let used_exts = json!([{ "name": "test", "version": "2.0.0" }]);
846 hugr.set_metadata(hugr.module_root(), USED_EXTENSIONS_KEY, used_exts);
847
848 let err = check_breaking_extensions(&hugr, ®istry).unwrap_err();
850 let with_gen = WithGenerator::new(err, &[&hugr]);
851
852 let err_msg = with_gen.to_string();
853 assert!(err_msg.contains("Extension 'test' version mismatch"));
854 assert!(err_msg.contains("TestGenerator-v1.2.3"));
855 }
856}