From 56fc70634c06bec86ba88cb26d04fa090895fd45 Mon Sep 17 00:00:00 2001 From: Seyon Sivarajah Date: Mon, 15 Jan 2024 11:29:31 +0000 Subject: [PATCH] test: add schema validation to roundtrips requires minor fixes to schema --- Cargo.toml | 1 + devenv.nix | 2 + specification/schema/hugr_schema_v0.json | 240 ++++++++--------------- src/hugr/serialize.rs | 43 +++- 4 files changed, 128 insertions(+), 158 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 6bc0549f9c..e324d7a7d2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -61,6 +61,7 @@ webbrowser = "0.8.10" urlencoding = "2.1.2" cool_asserts = "2.0.3" insta = { version = "1.34.0", features = ["yaml"] } +jsonschema = "0.17.1" [[bench]] name = "bench_main" diff --git a/devenv.nix b/devenv.nix index bc9fdba1cf..adc035dbcf 100644 --- a/devenv.nix +++ b/devenv.nix @@ -16,6 +16,8 @@ in (with pkgs.darwin.apple_sdk; [ frameworks.CoreServices frameworks.CoreFoundation + # added for json schema validation tess + frameworks.SystemConfiguration ]); # https://devenv.sh/scripts/ diff --git a/specification/schema/hugr_schema_v0.json b/specification/schema/hugr_schema_v0.json index 6cf15c6b91..f69ea62818 100644 --- a/specification/schema/hugr_schema_v0.json +++ b/specification/schema/hugr_schema_v0.json @@ -74,11 +74,7 @@ "type": "integer" }, "input_extensions": { - "items": { - "type": "string" - }, - "title": "Input Extensions", - "type": "array" + "$ref": "#/$defs/InputExtensions" }, "op": { "const": "CFG", @@ -90,7 +86,8 @@ } }, "required": [ - "parent" + "parent", + "input_extensions" ], "title": "CFG", "type": "object" @@ -103,11 +100,7 @@ "type": "integer" }, "input_extensions": { - "items": { - "type": "string" - }, - "title": "Input Extensions", - "type": "array" + "$ref": "#/$defs/InputExtensions" }, "op": { "const": "Call", @@ -119,7 +112,8 @@ } }, "required": [ - "parent" + "parent", + "input_extensions" ], "title": "Call", "type": "object" @@ -132,11 +126,7 @@ "type": "integer" }, "input_extensions": { - "items": { - "type": "string" - }, - "title": "Input Extensions", - "type": "array" + "$ref": "#/$defs/InputExtensions" }, "op": { "const": "CallIndirect", @@ -148,7 +138,8 @@ } }, "required": [ - "parent" + "parent", + "input_extensions" ], "title": "CallIndirect", "type": "object" @@ -161,11 +152,7 @@ "type": "integer" }, "input_extensions": { - "items": { - "type": "string" - }, - "title": "Input Extensions", - "type": "array" + "$ref": "#/$defs/InputExtensions" }, "op": { "const": "Case", @@ -177,7 +164,8 @@ } }, "required": [ - "parent" + "parent", + "input_extensions" ], "title": "Case", "type": "object" @@ -190,11 +178,7 @@ "type": "integer" }, "input_extensions": { - "items": { - "type": "string" - }, - "title": "Input Extensions", - "type": "array" + "$ref": "#/$defs/InputExtensions" }, "op": { "const": "Conditional", @@ -234,7 +218,8 @@ } }, "required": [ - "parent" + "parent", + "input_extensions" ], "title": "Conditional", "type": "object" @@ -247,11 +232,7 @@ "type": "integer" }, "input_extensions": { - "items": { - "type": "string" - }, - "title": "Input Extensions", - "type": "array" + "$ref": "#/$defs/InputExtensions" }, "op": { "const": "Const", @@ -281,11 +262,7 @@ "type": "integer" }, "input_extensions": { - "items": { - "type": "string" - }, - "title": "Input Extensions", - "type": "array" + "$ref": "#/$defs/InputExtensions" }, "op": { "const": "LeafOp", @@ -355,11 +332,7 @@ "type": "integer" }, "input_extensions": { - "items": { - "type": "string" - }, - "title": "Input Extensions", - "type": "array" + "$ref": "#/$defs/InputExtensions" }, "op": { "const": "DFG", @@ -371,7 +344,8 @@ } }, "required": [ - "parent" + "parent", + "input_extensions" ], "title": "DFG", "type": "object" @@ -384,11 +358,7 @@ "type": "integer" }, "input_extensions": { - "items": { - "type": "string" - }, - "title": "Input Extensions", - "type": "array" + "$ref": "#/$defs/InputExtensions" }, "op": { "const": "DataflowBlock", @@ -428,7 +398,8 @@ } }, "required": [ - "parent" + "parent", + "input_extensions" ], "title": "DataflowBlock", "type": "object" @@ -441,11 +412,7 @@ "type": "integer" }, "input_extensions": { - "items": { - "type": "string" - }, - "title": "Input Extensions", - "type": "array" + "$ref": "#/$defs/InputExtensions" }, "op": { "const": "ExitBlock", @@ -520,11 +487,7 @@ "type": "integer" }, "input_extensions": { - "items": { - "type": "string" - }, - "title": "Input Extensions", - "type": "array" + "$ref": "#/$defs/InputExtensions" }, "op": { "const": "FuncDecl", @@ -554,11 +517,7 @@ "type": "integer" }, "input_extensions": { - "items": { - "type": "string" - }, - "title": "Input Extensions", - "type": "array" + "$ref": "#/$defs/InputExtensions" }, "op": { "const": "FuncDefn", @@ -665,11 +624,7 @@ "type": "integer" }, "input_extensions": { - "items": { - "type": "string" - }, - "title": "Input Extensions", - "type": "array" + "$ref": "#/$defs/InputExtensions" }, "op": { "const": "Input", @@ -685,11 +640,26 @@ } }, "required": [ - "parent" + "parent", + "input_extensions" ], "title": "Input", "type": "object" }, + "InputExtensions": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null + }, "USize": { "description": "Unsigned integer size type.", "properties": { @@ -760,11 +730,7 @@ "type": "integer" }, "input_extensions": { - "items": { - "type": "string" - }, - "title": "Input Extensions", - "type": "array" + "$ref": "#/$defs/InputExtensions" }, "op": { "const": "LoadConstant", @@ -790,11 +756,7 @@ "type": "integer" }, "input_extensions": { - "items": { - "type": "string" - }, - "title": "Input Extensions", - "type": "array" + "$ref": "#/$defs/InputExtensions" }, "op": { "const": "LeafOp", @@ -815,7 +777,8 @@ } }, "required": [ - "parent" + "parent", + "input_extensions" ], "title": "MakeTuple", "type": "object" @@ -828,11 +791,7 @@ "type": "integer" }, "input_extensions": { - "items": { - "type": "string" - }, - "title": "Input Extensions", - "type": "array" + "$ref": "#/$defs/InputExtensions" }, "op": { "const": "Module", @@ -841,7 +800,8 @@ } }, "required": [ - "parent" + "parent", + "input_extensions" ], "title": "Module", "type": "object" @@ -854,11 +814,7 @@ "type": "integer" }, "input_extensions": { - "items": { - "type": "string" - }, - "title": "Input Extensions", - "type": "array" + "$ref": "#/$defs/InputExtensions" }, "op": { "const": "LeafOp", @@ -1040,11 +996,7 @@ "type": "integer" }, "input_extensions": { - "items": { - "type": "string" - }, - "title": "Input Extensions", - "type": "array" + "$ref": "#/$defs/InputExtensions" }, "op": { "const": "Output", @@ -1060,7 +1012,8 @@ } }, "required": [ - "parent" + "parent", + "input_extensions" ], "title": "Output", "type": "object" @@ -1155,11 +1108,7 @@ "type": "integer" }, "input_extensions": { - "items": { - "type": "string" - }, - "title": "Input Extensions", - "type": "array" + "$ref": "#/$defs/InputExtensions" }, "op": { "const": "LeafOp", @@ -1199,11 +1148,7 @@ "type": "integer" }, "input_extensions": { - "items": { - "type": "string" - }, - "title": "Input Extensions", - "type": "array" + "$ref": "#/$defs/InputExtensions" }, "op": { "const": "TailLoop", @@ -1233,7 +1178,8 @@ } }, "required": [ - "parent" + "parent", + "input_extensions" ], "title": "TailLoop", "type": "object" @@ -1303,31 +1249,32 @@ "title": "TupleType", "type": "object" }, + "SumType": { + "discriminator": { + "mapping": { + "General": "#/$defs/GeneralSum", + "Unit": "#/$defs/UnitSum" + }, + "propertyName": "s" + }, + "oneOf": [ + { + "$ref": "#/$defs/UnitSum" + }, + { + "$ref": "#/$defs/GeneralSum" + } + ] + }, "Type": { "discriminator": { "mapping": { "Array": "#/$defs/Array", "G": "#/$defs/PolyFuncType", - "I": "#/ $defs/USize", + "I": "#/$defs/USize", "Opaque": "#/$defs/Opaque", "Q": "#/$defs/Qubit", - "Sum": { - "discriminator": { - "mapping": { - "General": "#/$defs/GeneralSum", - "Unit": "#/$defs/UnitSum" - }, - "propertyName": "s" - }, - "oneOf": [ - { - "$ref": "#/$defs/UnitSum" - }, - { - "$ref": "#/$defs/GeneralSum" - } - ] - }, + "Sum": "#/$defs/SumType", "Tuple": "#/$defs/TupleType", "V": "#/$defs/Variable" }, @@ -1341,7 +1288,7 @@ "$ref": "#/$defs/Variable" }, { - "$ref": "#/ $defs/USize" + "$ref": "#/$defs/USize" }, { "$ref": "#/$defs/PolyFuncType" @@ -1353,21 +1300,7 @@ "$ref": "#/$defs/TupleType" }, { - "discriminator": { - "mapping": { - "General": "#/$defs/GeneralSum", - "Unit": "#/$defs/UnitSum" - }, - "propertyName": "s" - }, - "oneOf": [ - { - "$ref": "#/$defs/UnitSum" - }, - { - "$ref": "#/$defs/GeneralSum" - } - ] + "$ref": "#/$defs/SumType" }, { "$ref": "#/$defs/Opaque" @@ -1407,11 +1340,7 @@ "type": "integer" }, "input_extensions": { - "items": { - "type": "string" - }, - "title": "Input Extensions", - "type": "array" + "$ref": "#/$defs/InputExtensions" }, "op": { "const": "LeafOp", @@ -1567,11 +1496,7 @@ "type": "integer" }, "input_extensions": { - "items": { - "type": "string" - }, - "title": "Input Extensions", - "type": "array" + "$ref": "#/$defs/InputExtensions" }, "op": { "const": "LeafOp", @@ -1592,7 +1517,8 @@ } }, "required": [ - "parent" + "parent", + "input_extensions" ], "title": "UnpackTuple", "type": "object" diff --git a/src/hugr/serialize.rs b/src/hugr/serialize.rs index 796b36aa4a..5155e89db4 100644 --- a/src/hugr/serialize.rs +++ b/src/hugr/serialize.rs @@ -268,14 +268,33 @@ pub mod test { use crate::types::{FunctionType, Type}; use crate::OutgoingPort; use itertools::Itertools; + use jsonschema::{Draft, JSONSchema}; + use lazy_static::lazy_static; use portgraph::LinkView; use portgraph::{ multiportgraph::MultiPortGraph, Hierarchy, LinkMut, PortMut, PortView, UnmanagedDenseMap, }; + use std::fs; const NAT: Type = crate::extension::prelude::USIZE_T; const QB: Type = crate::extension::prelude::QB_T; + lazy_static! { + static ref SCHEMA: JSONSchema = { + let path = format!( + "{}/specification/schema/hugr_schema_v0.json", + env!("CARGO_MANIFEST_DIR") + ); + let data = fs::read_to_string(path).unwrap(); + let schema_val: serde_json::Value = serde_json::from_str(&data).unwrap(); + + JSONSchema::options() + .with_draft(Draft::Draft7) + .compile(&schema_val) + .expect("A valid schema") + }; + } + #[test] fn empty_hugr_serialize() { let hg = Hugr::default(); @@ -284,7 +303,29 @@ pub mod test { /// Serialize and deserialize a value. pub fn ser_roundtrip(g: &T) -> T { + ser_roundtrip_validate(g, None) + } + + /// Serialize and deserialize a value, optionally validating against a schema. + pub fn ser_roundtrip_validate( + g: &T, + schema: Option<&JSONSchema>, + ) -> T { let s = serde_json::to_string(g).unwrap(); + let val: serde_json::Value = serde_json::from_str(&s).unwrap(); + + if let Some(schema) = schema { + let validate = schema.validate(&val); + + if let Err(errors) = validate { + // errors don't necessarily implement Debug + for error in errors { + println!("Validation error: {}", error); + println!("Instance path: {}", error.instance_path); + } + panic!("Serialization test failed."); + } + } serde_json::from_str(&s).unwrap() } @@ -292,7 +333,7 @@ pub mod test { /// /// Returns the deserialized HUGR. pub fn check_hugr_roundtrip(hugr: &Hugr) -> Hugr { - let new_hugr: Hugr = ser_roundtrip(hugr); + let new_hugr: Hugr = ser_roundtrip_validate(hugr, Some(&SCHEMA)); // Original HUGR, with canonicalized node indices //