Skip to content

Commit f7cedb4

Browse files
authored
feat: include generator metatada in model import and cli validate errors (#2452)
Closes #2433 Artificially induced error from python export: <img width="656" height="321" alt="image" src="https://github.com/user-attachments/assets/2a41d489-3f34-4a5c-99b0-ab31d759bf90" />
1 parent 5e36770 commit f7cedb4

File tree

11 files changed

+259
-111
lines changed

11 files changed

+259
-111
lines changed

hugr-cli/src/lib.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,4 +126,13 @@ pub enum CliError {
126126
"Invalid format: '{_0}'. Valid formats are: json, model, model-exts, model-text, model-text-exts"
127127
)]
128128
InvalidFormat(String),
129+
#[error("Error validating HUGR generated by {generator}")]
130+
/// Errors produced by the `validate` subcommand, with a known generator of the HUGR.
131+
ValidateKnownGenerator {
132+
#[source]
133+
/// The inner validation error.
134+
inner: PackageValidationError,
135+
/// The generator of the HUGR.
136+
generator: Box<String>,
137+
},
129138
}

hugr-cli/src/validate.rs

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,16 +29,32 @@ impl ValArgs {
2929
pub fn run(&mut self) -> Result<()> {
3030
if self.input_args.hugr_json {
3131
let hugr = self.input_args.get_hugr()?;
32+
let generator = hugr::envelope::get_generator(&[&hugr]);
33+
3234
hugr.validate()
3335
.map_err(PackageValidationError::Validation)
34-
.map_err(CliError::Validate)?;
36+
.map_err(|val_err| wrap_generator(generator, val_err))?;
3537
} else {
3638
let package = self.input_args.get_package()?;
37-
package.validate().map_err(CliError::Validate)?;
39+
let generator = hugr::envelope::get_generator(&package.modules);
40+
package
41+
.validate()
42+
.map_err(|val_err| wrap_generator(generator, val_err))?;
3843
};
3944

4045
info!("{VALID_PRINT}");
4146

4247
Ok(())
4348
}
4449
}
50+
51+
fn wrap_generator(generator: Option<String>, val_err: PackageValidationError) -> CliError {
52+
if let Some(g) = generator {
53+
CliError::ValidateKnownGenerator {
54+
inner: val_err,
55+
generator: Box::new(g.to_string()),
56+
}
57+
} else {
58+
CliError::Validate(val_err)
59+
}
60+
}

hugr-cli/tests/validate.rs

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,15 @@ use hugr::types::Type;
1313
use hugr::{
1414
builder::{Container, Dataflow},
1515
extension::prelude::{bool_t, qb_t},
16+
hugr::HugrView,
17+
hugr::hugrmut::HugrMut,
1618
std_extensions::arithmetic::float_types::float64_type,
1719
types::Signature,
1820
};
1921
use hugr_cli::validate::VALID_PRINT;
2022
use predicates::{prelude::*, str::contains};
2123
use rstest::{fixture, rstest};
24+
use serde_json::json;
2225

2326
#[fixture]
2427
fn cmd() -> Command {
@@ -123,6 +126,7 @@ fn test_mermaid_invalid(bad_hugr_string: String, mut cmd: Command) {
123126
cmd.write_stdin(bad_hugr_string);
124127
cmd.assert()
125128
.failure()
129+
.stderr(contains("unconnected port"))
126130
.stderr(contains("Error validating HUGR"));
127131
}
128132

@@ -134,6 +138,7 @@ fn test_bad_hugr(bad_hugr_string: String, mut val_cmd: Command) {
134138
val_cmd
135139
.assert()
136140
.failure()
141+
.stderr(contains("unconnected port"))
137142
.stderr(contains("Error validating HUGR"));
138143
}
139144

@@ -146,7 +151,8 @@ fn test_bad_json(mut val_cmd: Command) {
146151
val_cmd
147152
.assert()
148153
.failure()
149-
.stderr(contains("Error decoding HUGR envelope"));
154+
.stderr(contains("Error decoding HUGR envelope"))
155+
.stderr(contains("missing field"));
150156
}
151157

152158
#[rstest]
@@ -199,3 +205,38 @@ fn test_package_validation(package_string: String, mut val_cmd: Command) {
199205

200206
val_cmd.assert().success().stderr(contains(VALID_PRINT));
201207
}
208+
209+
/// Create a deliberately invalid HUGR with a known generator
210+
#[fixture]
211+
fn invalid_hugr_with_generator() -> Vec<u8> {
212+
// Create an invalid HUGR (missing outputs in a dataflow)
213+
let df = DFGBuilder::new(Signature::new_endo(vec![qb_t()])).unwrap();
214+
let mut bad_hugr = df.hugr().clone(); // Missing outputs makes this invalid
215+
bad_hugr.set_metadata(
216+
bad_hugr.module_root(),
217+
hugr::envelope::GENERATOR_KEY,
218+
json!({"name": "test-generator", "version": "1.0.1"}),
219+
);
220+
// Create envelope with a specific generator
221+
let envelope_config = EnvelopeConfig::binary();
222+
223+
let mut buff = Vec::new();
224+
// Serialize to string
225+
bad_hugr.store(&mut buff, envelope_config).unwrap();
226+
buff
227+
}
228+
229+
#[rstest]
230+
fn test_validate_known_generator(invalid_hugr_with_generator: Vec<u8>, mut val_cmd: Command) {
231+
// Write the invalid HUGR to stdin
232+
val_cmd.write_stdin(invalid_hugr_with_generator);
233+
val_cmd.arg("-");
234+
235+
// Expect a failure with the generator name in the error message
236+
val_cmd
237+
.assert()
238+
.failure()
239+
.stderr(contains("Error validating HUGR"))
240+
.stderr(contains("unconnected port"))
241+
.stderr(contains("generated by test-generator-v1.0.1"));
242+
}

hugr-core/src/envelope.rs

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -73,11 +73,11 @@ pub const USED_EXTENSIONS_KEY: &str = "core.used_extensions";
7373
/// If multiple modules have different generators, a comma-separated list is returned in
7474
/// module order.
7575
/// If no generator is found, `None` is returned.
76-
fn get_generator<H: HugrView>(modules: &[H]) -> Option<String> {
76+
pub fn get_generator<H: HugrView>(modules: &[H]) -> Option<String> {
7777
let generators: Vec<String> = modules
7878
.iter()
7979
.filter_map(|hugr| hugr.get_metadata(hugr.module_root(), GENERATOR_KEY))
80-
.map(|v| v.to_string())
80+
.map(format_generator)
8181
.collect();
8282
if generators.is_empty() {
8383
return None;
@@ -86,6 +86,31 @@ fn get_generator<H: HugrView>(modules: &[H]) -> Option<String> {
8686
Some(generators.join(", "))
8787
}
8888

89+
/// Format a generator value from the metadata.
90+
pub 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+
// Expected format: {"name": "generator", "version": "1.0.0"}
100+
format!("{name}-v{version}")
101+
} else {
102+
name.to_string()
103+
}
104+
} else {
105+
// just print the whole object as a string
106+
json_val.to_string()
107+
}
108+
}
109+
// Raw JSON string fallback
110+
_ => json_val.to_string(),
111+
}
112+
}
113+
89114
fn gen_str(generator: &Option<String>) -> String {
90115
match generator {
91116
Some(g) => format!("\ngenerated by {g}"),
@@ -280,7 +305,7 @@ pub enum EnvelopeError {
280305
source: hugr_model::v0::binary::WriteError,
281306
},
282307
/// Error reading a HUGR model payload.
283-
#[error(transparent)]
308+
#[error("Model text parsing error")]
284309
ModelTextRead {
285310
/// The source error.
286311
#[from]
@@ -826,6 +851,6 @@ pub(crate) mod test {
826851

827852
let err_msg = with_gen.to_string();
828853
assert!(err_msg.contains("Extension 'test' version mismatch"));
829-
assert!(err_msg.contains(generator_name.to_string().as_str()));
854+
assert!(err_msg.contains("TestGenerator-v1.2.3"));
830855
}
831856
}

hugr-core/src/envelope/package_json.rs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -77,15 +77,16 @@ pub(super) fn to_json_writer<'h>(
7777
/// Error raised while loading a package.
7878
#[derive(Debug, Display, Error, From)]
7979
#[non_exhaustive]
80+
#[display("Error reading or writing a package in JSON format.")]
8081
pub enum PackageEncodingError {
8182
/// Error raised while parsing the package json.
82-
JsonEncoding(serde_json::Error),
83+
JsonEncoding(#[from] serde_json::Error),
8384
/// Error raised while reading from a file.
84-
IOError(io::Error),
85+
IOError(#[from] io::Error),
8586
/// Could not resolve the extension needed to encode the hugr.
86-
ExtensionResolution(WithGenerator<ExtensionResolutionError>),
87+
ExtensionResolution(#[from] WithGenerator<ExtensionResolutionError>),
8788
/// Error raised while checking for breaking extension version mismatch.
88-
ExtensionVersion(WithGenerator<ExtensionBreakingError>),
89+
ExtensionVersion(#[from] WithGenerator<ExtensionBreakingError>),
8990
}
9091

9192
/// A private package structure implementing the serde traits.

hugr-core/src/extension.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -706,6 +706,7 @@ pub enum ExtensionRegistryError {
706706
/// An error that can occur while loading an extension registry.
707707
#[derive(Debug, Error)]
708708
#[non_exhaustive]
709+
#[error("Extension registry load error")]
709710
pub enum ExtensionRegistryLoadError {
710711
/// Deserialization error.
711712
#[error(transparent)]

0 commit comments

Comments
 (0)