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