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 pub fn inner(&self) -> &E {
141 &self.inner
142 }
143
144 pub fn generator(&self) -> Option<&String> {
146 self.generator.as_ref()
147 }
148}
149
150pub fn read_envelope(
159 mut reader: impl BufRead,
160 registry: &ExtensionRegistry,
161) -> Result<(EnvelopeConfig, Package), EnvelopeError> {
162 let header = EnvelopeHeader::read(&mut reader)?;
163
164 let package = match header.zstd {
165 #[cfg(feature = "zstd")]
166 true => read_impl(
167 std::io::BufReader::new(zstd::Decoder::new(reader)?),
168 header,
169 registry,
170 ),
171 #[cfg(not(feature = "zstd"))]
172 true => Err(EnvelopeError::ZstdUnsupported),
173 false => read_impl(reader, header, registry),
174 }?;
175 Ok((header.config(), package))
176}
177
178pub fn write_envelope(
183 writer: impl Write,
184 package: &Package,
185 config: EnvelopeConfig,
186) -> Result<(), EnvelopeError> {
187 write_envelope_impl(writer, &package.modules, &package.extensions, config)
188}
189
190pub(crate) fn write_envelope_impl<'h>(
195 mut writer: impl Write,
196 hugrs: impl IntoIterator<Item = &'h Hugr>,
197 extensions: &ExtensionRegistry,
198 config: EnvelopeConfig,
199) -> Result<(), EnvelopeError> {
200 let header = config.make_header();
201 header.write(&mut writer)?;
202
203 match config.zstd {
204 #[cfg(feature = "zstd")]
205 Some(zstd) => {
206 let writer = zstd::Encoder::new(writer, zstd.level())?.auto_finish();
207 write_impl(writer, hugrs, extensions, config)?;
208 }
209 #[cfg(not(feature = "zstd"))]
210 Some(_) => return Err(EnvelopeError::ZstdUnsupported),
211 None => write_impl(writer, hugrs, extensions, config)?,
212 }
213
214 Ok(())
215}
216
217#[derive(Debug, Error)]
219#[non_exhaustive]
220pub enum EnvelopeError {
221 #[error(
223 "Bad magic number. expected 0x{:X} found 0x{:X}",
224 u64::from_be_bytes(*expected),
225 u64::from_be_bytes(*found)
226 )]
227 MagicNumber {
228 expected: [u8; 8],
232 found: [u8; 8],
234 },
235 #[error("Format descriptor {descriptor} is invalid.")]
237 InvalidFormatDescriptor {
238 descriptor: usize,
240 },
241 #[error("Payload format {format} is not supported.{}",
243 match feature {
244 Some(f) => format!(" This requires the '{f}' feature for `hugr`."),
245 None => String::new()
246 },
247 )]
248 FormatUnsupported {
249 format: EnvelopeFormat,
251 feature: Option<&'static str>,
253 },
254 #[error("Envelope format {format} cannot be represented as ASCII.")]
258 NonASCIIFormat {
259 format: EnvelopeFormat,
261 },
262 #[error("Zstd compression is not supported. This requires the 'zstd' feature for `hugr`.")]
264 ZstdUnsupported,
265 #[error("Expected an envelope containing a single hugr, but it contained {}.", if *count == 0 {
267 "none".to_string()
268 } else {
269 count.to_string()
270 })]
271 ExpectedSingleHugr {
272 count: usize,
274 },
275 #[error(transparent)]
277 SerdeError {
278 #[from]
280 source: serde_json::Error,
281 },
282 #[error(transparent)]
284 IO {
285 #[from]
287 source: std::io::Error,
288 },
289 #[error(transparent)]
291 PackageEncoding {
292 #[from]
294 source: PackageEncodingError,
295 },
296 #[error(transparent)]
298 ModelImport {
299 #[from]
301 source: ImportError,
302 },
304 #[error(transparent)]
306 ModelRead {
307 #[from]
309 source: hugr_model::v0::binary::ReadError,
310 },
311 #[error(transparent)]
313 ModelWrite {
314 #[from]
316 source: hugr_model::v0::binary::WriteError,
317 },
318 #[error("Model text parsing error")]
320 ModelTextRead {
321 #[from]
323 source: hugr_model::v0::ast::ParseError,
324 },
325 #[error(transparent)]
327 ModelTextResolve {
328 #[from]
330 source: hugr_model::v0::ast::ResolveError,
331 },
332 #[error(transparent)]
334 ExtensionLoad {
335 #[from]
337 source: crate::extension::ExtensionRegistryLoadError,
338 },
339 #[error(
341 "The envelope configuration has unknown {}. Please update your HUGR version.",
342 if flag_ids.len() == 1 {format!("flag #{}", flag_ids[0])} else {format!("flags {}", flag_ids.iter().join(", "))}
343 )]
344 FlagUnsupported {
345 flag_ids: Vec<usize>,
347 },
348}
349
350fn read_impl(
352 payload: impl BufRead,
353 header: EnvelopeHeader,
354 registry: &ExtensionRegistry,
355) -> Result<Package, EnvelopeError> {
356 match header.format {
357 #[allow(deprecated)]
358 EnvelopeFormat::PackageJson => Ok(package_json::from_json_reader(payload, registry)?),
359 EnvelopeFormat::Model | EnvelopeFormat::ModelWithExtensions => {
360 decode_model(payload, registry, header.format)
361 }
362 EnvelopeFormat::ModelText | EnvelopeFormat::ModelTextWithExtensions => {
363 decode_model_ast(payload, registry, header.format)
364 }
365 }
366}
367
368fn decode_model(
376 mut stream: impl BufRead,
377 extension_registry: &ExtensionRegistry,
378 format: EnvelopeFormat,
379) -> Result<Package, EnvelopeError> {
380 use hugr_model::v0::bumpalo::Bump;
381
382 if format.model_version() != Some(0) {
383 return Err(EnvelopeError::FormatUnsupported {
384 format,
385 feature: None,
386 });
387 }
388
389 let bump = Bump::default();
390 let model_package = hugr_model::v0::binary::read_from_reader(&mut stream, &bump)?;
391
392 let mut extension_registry = extension_registry.clone();
393 if format == EnvelopeFormat::ModelWithExtensions {
394 let extra_extensions = ExtensionRegistry::load_json(stream, &extension_registry)?;
395 extension_registry.extend(extra_extensions);
396 }
397
398 Ok(import_package(&model_package, &extension_registry)?)
399}
400
401fn decode_model_ast(
409 mut stream: impl BufRead,
410 extension_registry: &ExtensionRegistry,
411 format: EnvelopeFormat,
412) -> Result<Package, EnvelopeError> {
413 use crate::import::import_package;
414 use hugr_model::v0::bumpalo::Bump;
415
416 if format.model_version() != Some(0) {
417 return Err(EnvelopeError::FormatUnsupported {
418 format,
419 feature: None,
420 });
421 }
422
423 let mut extension_registry = extension_registry.clone();
424 if format == EnvelopeFormat::ModelTextWithExtensions {
425 let deserializer = serde_json::Deserializer::from_reader(&mut stream);
426 let extra_extensions = deserializer
428 .into_iter::<Vec<Extension>>()
429 .next()
430 .unwrap_or(Ok(vec![]))?;
431 for ext in extra_extensions {
432 extension_registry.register_updated(ext);
433 }
434 }
435
436 let mut buffer = String::new();
440 stream.read_to_string(&mut buffer)?;
441 let ast_package = hugr_model::v0::ast::Package::from_str(&buffer)?;
442
443 let bump = Bump::default();
444 let model_package = ast_package.resolve(&bump)?;
445
446 let package = import_package(&model_package, &extension_registry)?;
447 for module in &package.modules {
448 check_breaking_extensions(module, &extension_registry).map_err(|err| {
449 PackageEncodingError::ExtensionVersion(WithGenerator::new(err, &package.modules))
450 })?;
451 }
452 Ok(package)
453}
454
455fn write_impl<'h>(
457 writer: impl Write,
458 hugrs: impl IntoIterator<Item = &'h Hugr>,
459 extensions: &ExtensionRegistry,
460 config: EnvelopeConfig,
461) -> Result<(), EnvelopeError> {
462 match config.format {
463 #[allow(deprecated)]
464 EnvelopeFormat::PackageJson => package_json::to_json_writer(hugrs, extensions, writer)?,
465 EnvelopeFormat::Model
466 | EnvelopeFormat::ModelWithExtensions
467 | EnvelopeFormat::ModelText
468 | EnvelopeFormat::ModelTextWithExtensions => {
469 encode_model(writer, hugrs, extensions, config.format)?;
470 }
471 }
472 Ok(())
473}
474
475fn encode_model<'h>(
476 mut writer: impl Write,
477 hugrs: impl IntoIterator<Item = &'h Hugr>,
478 extensions: &ExtensionRegistry,
479 format: EnvelopeFormat,
480) -> Result<(), EnvelopeError> {
481 use hugr_model::v0::{binary::write_to_writer, bumpalo::Bump};
482
483 use crate::export::export_package;
484
485 if format.model_version() != Some(0) {
486 return Err(EnvelopeError::FormatUnsupported {
487 format,
488 feature: None,
489 });
490 }
491
492 if format == EnvelopeFormat::ModelTextWithExtensions {
494 serde_json::to_writer(&mut writer, &extensions.iter().collect_vec())?;
495 }
496
497 let bump = Bump::default();
498 let model_package = export_package(hugrs, extensions, &bump);
499
500 match format {
501 EnvelopeFormat::Model | EnvelopeFormat::ModelWithExtensions => {
502 write_to_writer(&model_package, &mut writer)?;
503 }
504 EnvelopeFormat::ModelText | EnvelopeFormat::ModelTextWithExtensions => {
505 let model_package = model_package.as_ast().unwrap();
506 writeln!(writer, "{model_package}")?;
507 }
508 _ => unreachable!(),
509 }
510
511 if format == EnvelopeFormat::ModelWithExtensions {
513 serde_json::to_writer(writer, &extensions.iter().collect_vec())?;
514 }
515
516 Ok(())
517}
518
519#[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize)]
520struct UsedExtension {
521 name: String,
522 version: Version,
523}
524
525#[derive(Debug, Error)]
526#[error(
527 "Extension '{name}' version mismatch: registered version is {registered}, but used version is {used}"
528)]
529pub struct ExtensionVersionMismatch {
532 pub name: String,
534 pub registered: Version,
536 pub used: Version,
538}
539
540#[derive(Debug, Error)]
541#[non_exhaustive]
542pub enum ExtensionBreakingError {
544 #[error("{0}")]
546 ExtensionVersionMismatch(ExtensionVersionMismatch),
547
548 #[error("Failed to deserialize used extensions metadata")]
550 Deserialization(#[from] serde_json::Error),
551}
552fn check_breaking_extensions(
557 hugr: impl crate::HugrView,
558 registry: &ExtensionRegistry,
559) -> Result<(), ExtensionBreakingError> {
560 let Some(exts) = hugr.get_metadata(hugr.module_root(), USED_EXTENSIONS_KEY) else {
561 return Ok(()); };
563 let used_exts: Vec<UsedExtension> = serde_json::from_value(exts.clone())?; for ext in used_exts {
566 let Some(registered) = registry.get(ext.name.as_str()) else {
567 continue; };
569 if !compatible_versions(registered.version(), &ext.version) {
570 return Err(ExtensionBreakingError::ExtensionVersionMismatch(
573 ExtensionVersionMismatch {
574 name: ext.name,
575 registered: registered.version().clone(),
576 used: ext.version,
577 },
578 ));
579 }
580 }
581
582 Ok(())
583}
584
585fn compatible_versions(v1: &Version, v2: &Version) -> bool {
589 if v1.major != v2.major {
590 return false; }
592
593 if v1.major == 0 {
594 return v1.minor == v2.minor;
596 }
597
598 true
599}
600
601#[cfg(test)]
602pub(crate) mod test {
603 use super::*;
604 use cool_asserts::assert_matches;
605 use rstest::rstest;
606 use std::borrow::Cow;
607 use std::io::BufReader;
608
609 use crate::HugrView;
610 use crate::builder::test::{multi_module_package, simple_package};
611 use crate::extension::{Extension, ExtensionRegistry, Version};
612 use crate::extension::{ExtensionId, PRELUDE_REGISTRY};
613 use crate::hugr::HugrMut;
614 use crate::hugr::test::check_hugr_equality;
615 use crate::std_extensions::STD_REG;
616 use serde_json::json;
617 use std::sync::Arc;
618
619 fn join_extensions<'a>(
623 extensions: &'a ExtensionRegistry,
624 other: &ExtensionRegistry,
625 ) -> Cow<'a, ExtensionRegistry> {
626 if other.iter().all(|e| extensions.contains(e.name())) {
627 Cow::Borrowed(extensions)
628 } else {
629 let mut extensions = extensions.clone();
630 extensions.extend(other);
631 Cow::Owned(extensions)
632 }
633 }
634
635 pub(crate) fn check_hugr_roundtrip(hugr: &Hugr, config: EnvelopeConfig) -> Hugr {
644 let mut buffer = Vec::new();
645 hugr.store(&mut buffer, config).unwrap();
646
647 let extensions = join_extensions(&STD_REG, hugr.extensions());
648
649 let reader = BufReader::new(buffer.as_slice());
650 let extracted = Hugr::load(reader, Some(&extensions)).unwrap();
651
652 check_hugr_equality(&extracted, hugr);
653 extracted
654 }
655
656 #[rstest]
657 fn errors() {
658 let package = simple_package();
659 assert_matches!(
660 package.store_str(EnvelopeConfig::binary()),
661 Err(EnvelopeError::NonASCIIFormat { .. })
662 );
663 }
664
665 #[rstest]
666 #[case::empty(Package::default())]
667 #[case::simple(simple_package())]
668 #[case::multi(multi_module_package())]
669 fn text_roundtrip(#[case] package: Package) {
670 let envelope = package.store_str(EnvelopeConfig::text()).unwrap();
671 let new_package = Package::load_str(&envelope, None).unwrap();
672 assert_eq!(package, new_package);
673 }
674
675 #[rstest]
676 #[case::empty(Package::default())]
677 #[case::simple(simple_package())]
678 #[case::multi(multi_module_package())]
679 #[cfg_attr(all(miri, feature = "zstd"), ignore)] fn compressed_roundtrip(#[case] package: Package) {
681 let mut buffer = Vec::new();
682 let config = EnvelopeConfig {
683 format: EnvelopeFormat::PackageJson,
684 zstd: Some(ZstdConfig::default()),
685 };
686 let res = package.store(&mut buffer, config);
687
688 match cfg!(feature = "zstd") {
689 true => res.unwrap(),
690 false => {
691 assert_matches!(res, Err(EnvelopeError::ZstdUnsupported));
692 return;
693 }
694 }
695
696 let (decoded_config, new_package) =
697 read_envelope(BufReader::new(buffer.as_slice()), &PRELUDE_REGISTRY).unwrap();
698
699 assert_eq!(config.format, decoded_config.format);
700 assert_eq!(config.zstd.is_some(), decoded_config.zstd.is_some());
701 assert_eq!(package, new_package);
702 }
703
704 #[rstest]
705 #[case::empty_model(Package::default(), EnvelopeFormat::Model)]
707 #[case::empty_model_exts(Package::default(), EnvelopeFormat::ModelWithExtensions)]
708 #[case::empty_text(Package::default(), EnvelopeFormat::ModelText)]
709 #[case::empty_text_exts(Package::default(), EnvelopeFormat::ModelTextWithExtensions)]
710 #[case::simple_bin(simple_package(), EnvelopeFormat::Model)]
712 #[case::simple_bin_exts(simple_package(), EnvelopeFormat::ModelWithExtensions)]
713 #[case::simple_text(simple_package(), EnvelopeFormat::ModelText)]
714 #[case::simple_text_exts(simple_package(), EnvelopeFormat::ModelTextWithExtensions)]
715 #[case::multi_bin(multi_module_package(), EnvelopeFormat::Model)]
717 #[case::multi_bin_exts(multi_module_package(), EnvelopeFormat::ModelWithExtensions)]
718 #[case::multi_text(multi_module_package(), EnvelopeFormat::ModelText)]
719 #[case::multi_text_exts(multi_module_package(), EnvelopeFormat::ModelTextWithExtensions)]
720 fn model_roundtrip(#[case] package: Package, #[case] format: EnvelopeFormat) {
721 let mut buffer = Vec::new();
722 let config = EnvelopeConfig { format, zstd: None };
723 package.store(&mut buffer, config).unwrap();
724
725 let (decoded_config, new_package) =
726 read_envelope(BufReader::new(buffer.as_slice()), &PRELUDE_REGISTRY).unwrap();
727
728 assert_eq!(config.format, decoded_config.format);
729 assert_eq!(config.zstd.is_some(), decoded_config.zstd.is_some());
730
731 assert_eq!(package, new_package);
732 }
733
734 #[rstest]
735 #[case::simple(simple_package())]
736 fn test_check_breaking_extensions(#[case] mut package: Package) {
737 let test_ext_v0 =
739 Extension::new(ExtensionId::new_unchecked("test-v0"), Version::new(0, 2, 3));
740 let test_ext_v1 =
742 Extension::new(ExtensionId::new_unchecked("test-v1"), Version::new(1, 2, 3));
743
744 let registry =
746 ExtensionRegistry::new([Arc::new(test_ext_v0.clone()), Arc::new(test_ext_v1.clone())]);
747 let mut hugr = package.modules.remove(0);
748
749 assert_matches!(check_breaking_extensions(&hugr, ®istry), Ok(()));
751
752 let used_exts = json!([{ "name": "test-v0", "version": "0.2.3" }]);
754 hugr.set_metadata(hugr.module_root(), USED_EXTENSIONS_KEY, used_exts);
755 assert_matches!(check_breaking_extensions(&hugr, ®istry), Ok(()));
756
757 let used_exts = json!([{ "name": "test-v0", "version": "0.2.4" }]);
759 hugr.set_metadata(hugr.module_root(), USED_EXTENSIONS_KEY, used_exts);
760 assert_matches!(check_breaking_extensions(&hugr, ®istry), Ok(()));
761
762 let used_exts = json!([{ "name": "test-v0", "version": "0.3.3" }]);
764 hugr.set_metadata(hugr.module_root(), USED_EXTENSIONS_KEY, used_exts);
765 assert_matches!(
766 check_breaking_extensions(&hugr, ®istry),
767 Err(ExtensionBreakingError::ExtensionVersionMismatch(ExtensionVersionMismatch {
768 name,
769 registered,
770 used
771 })) if name == "test-v0" && registered == Version::new(0, 2, 3) && used == Version::new(0, 3, 3)
772 );
773
774 let used_exts = json!([{ "name": "test-v0", "version": "1.2.3" }]);
776 hugr.set_metadata(hugr.module_root(), USED_EXTENSIONS_KEY, used_exts);
777 assert_matches!(
778 check_breaking_extensions(&hugr, ®istry),
779 Err(ExtensionBreakingError::ExtensionVersionMismatch(ExtensionVersionMismatch {
780 name,
781 registered,
782 used
783 })) if name == "test-v0" && registered == Version::new(0, 2, 3) && used == Version::new(1, 2, 3)
784 );
785
786 let used_exts = json!([{ "name": "test-v1", "version": "1.2.3" }]);
788 hugr.set_metadata(hugr.module_root(), USED_EXTENSIONS_KEY, used_exts);
789 assert_matches!(check_breaking_extensions(&hugr, ®istry), Ok(()));
790
791 let used_exts = json!([{ "name": "test-v1", "version": "1.3.0" }]);
793 hugr.set_metadata(hugr.module_root(), USED_EXTENSIONS_KEY, used_exts);
794 assert_matches!(check_breaking_extensions(&hugr, ®istry), Ok(()));
795
796 let used_exts = json!([{ "name": "test-v1", "version": "1.2.4" }]);
798 hugr.set_metadata(hugr.module_root(), USED_EXTENSIONS_KEY, used_exts);
799 assert_matches!(check_breaking_extensions(&hugr, ®istry), Ok(()));
800
801 let used_exts = json!([{ "name": "test-v1", "version": "2.2.3" }]);
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, 2, 3)
811 );
812
813 let used_exts = json!([{ "name": "unknown", "version": "1.0.0" }]);
815 hugr.set_metadata(hugr.module_root(), USED_EXTENSIONS_KEY, used_exts);
816 assert_matches!(check_breaking_extensions(&hugr, ®istry), Ok(()));
817
818 let used_exts = json!([
820 { "name": "unknown", "version": "1.0.0" },
821 { "name": "test-v1", "version": "2.0.0" }
822 ]);
823 hugr.set_metadata(hugr.module_root(), USED_EXTENSIONS_KEY, used_exts);
824 assert_matches!(
825 check_breaking_extensions(&hugr, ®istry),
826 Err(ExtensionBreakingError::ExtensionVersionMismatch(ExtensionVersionMismatch {
827 name,
828 registered,
829 used
830 })) if name == "test-v1" && registered == Version::new(1, 2, 3) && used == Version::new(2, 0, 0)
831 );
832
833 hugr.set_metadata(
835 hugr.module_root(),
836 USED_EXTENSIONS_KEY,
837 json!("not an array"),
838 );
839 assert_matches!(
840 check_breaking_extensions(&hugr, ®istry),
841 Err(ExtensionBreakingError::Deserialization(_))
842 );
843
844 let used_exts = json!([
846 { "name": "test-v0", "version": "0.2.5" },
847 { "name": "test-v1", "version": "1.9.9" }
848 ]);
849 hugr.set_metadata(hugr.module_root(), USED_EXTENSIONS_KEY, used_exts);
850 assert_matches!(check_breaking_extensions(&hugr, ®istry), Ok(()));
851 }
852
853 #[test]
854 fn test_with_generator_error_message() {
855 let test_ext = Extension::new(ExtensionId::new_unchecked("test"), Version::new(1, 0, 0));
856 let registry = ExtensionRegistry::new([Arc::new(test_ext)]);
857
858 let mut hugr = simple_package().modules.remove(0);
859
860 let generator_name = json!({ "name": "TestGenerator", "version": "1.2.3" });
862 hugr.set_metadata(hugr.module_root(), GENERATOR_KEY, generator_name.clone());
863
864 let used_exts = json!([{ "name": "test", "version": "2.0.0" }]);
866 hugr.set_metadata(hugr.module_root(), USED_EXTENSIONS_KEY, used_exts);
867
868 let err = check_breaking_extensions(&hugr, ®istry).unwrap_err();
870 let with_gen = WithGenerator::new(err, &[&hugr]);
871
872 let err_msg = with_gen.to_string();
873 assert!(err_msg.contains("Extension 'test' version mismatch"));
874 assert!(err_msg.contains("TestGenerator-v1.2.3"));
875 }
876}