1mod rules;
15
16pub use rules::{
17 AttributeProtoValidityRule, DuplicateValueNameRule, FunctionProtoValidityRule,
18 GraphAcyclicRule, InitializerTypeMatchesDeclaredRule, InputOutputDeclaredRule,
19 IrVersionFeatureRule, IrVersionSupportedRule, MetadataKeysUniqueRule, MissingOpsetImportRule,
20 MultiDeviceConfigurationRule, NoUnconnectedNodesRule, ProtoTypeValidityRule,
21 SchemaNodeConformsRule, SparseTensorValidityRule, TensorPayloadValidityRule,
22 TypeConstraintSatisfiedRule,
23};
24
25use crate::model::Model;
26use crate::schema::SchemaRegistry;
27
28#[derive(Clone, Copy, Debug, PartialEq, Eq)]
30pub enum Severity {
31 Error,
33 Warning,
35 Info,
37}
38
39#[derive(Clone, Debug, PartialEq, Eq)]
41pub enum ViolationLocation {
42 Model,
44 Graph { graph_name: String },
46 Node {
48 graph_name: String,
49 node_name: String,
50 },
51 Value { value_name: String },
53}
54
55#[derive(Clone, Debug, PartialEq, Eq)]
57pub struct Violation {
58 pub rule_id: String,
60 pub severity: Severity,
62 pub message: String,
64 pub location: ViolationLocation,
66}
67
68#[derive(Clone, Debug)]
72pub struct ValidationContext {
73 schemas: std::sync::Arc<SchemaRegistry>,
74}
75
76impl ValidationContext {
77 pub fn new(schemas: SchemaRegistry) -> Self {
79 Self {
80 schemas: std::sync::Arc::new(schemas),
81 }
82 }
83
84 pub fn schemas(&self) -> &SchemaRegistry {
86 &self.schemas
87 }
88}
89
90impl Default for ValidationContext {
91 fn default() -> Self {
92 Self::new(SchemaRegistry::builtins())
93 }
94}
95
96pub trait ValidationRule: Send + Sync {
98 fn id(&self) -> &str;
100
101 fn severity(&self) -> Severity;
103
104 fn check(&self, model: &Model, ctx: &ValidationContext) -> Vec<Violation>;
106}
107
108#[derive(Clone, Debug, Default)]
110pub struct ValidationResult {
111 pub violations: Vec<Violation>,
113 pub errors: usize,
115 pub warnings: usize,
117}
118
119impl ValidationResult {
120 pub fn is_valid(&self) -> bool {
122 self.errors == 0
123 }
124}
125
126pub struct OnnxChecker {
133 rules: Vec<Box<dyn ValidationRule>>,
134 disabled: std::collections::HashSet<String>,
135 context: ValidationContext,
136}
137
138impl OnnxChecker {
139 pub fn empty() -> Self {
141 Self {
142 rules: Vec::new(),
143 disabled: std::collections::HashSet::new(),
144 context: ValidationContext::default(),
145 }
146 }
147
148 pub fn new() -> Self {
150 let mut checker = Self::empty();
151 checker.add_rule(MissingOpsetImportRule);
152 checker.add_rule(DuplicateValueNameRule);
153 checker.add_rule(GraphAcyclicRule);
154 checker.add_rule(SchemaNodeConformsRule);
155 checker.add_rule(InputOutputDeclaredRule);
156 checker.add_rule(NoUnconnectedNodesRule);
157 checker.add_rule(TypeConstraintSatisfiedRule);
158 checker.add_rule(InitializerTypeMatchesDeclaredRule);
159 checker.add_rule(IrVersionSupportedRule);
160 checker.add_rule(IrVersionFeatureRule);
161 checker.add_rule(FunctionProtoValidityRule);
162 checker.add_rule(MetadataKeysUniqueRule);
163 checker.add_rule(AttributeProtoValidityRule);
164 checker.add_rule(ProtoTypeValidityRule);
165 checker.add_rule(TensorPayloadValidityRule);
166 checker.add_rule(SparseTensorValidityRule);
167 checker.add_rule(MultiDeviceConfigurationRule);
168 checker
169 }
170
171 pub fn with_schema_registry(schemas: SchemaRegistry) -> Self {
173 let mut checker = Self::new();
174 checker.context = ValidationContext::new(schemas);
175 checker
176 }
177
178 pub fn add_rule<R: ValidationRule + 'static>(&mut self, rule: R) {
180 self.rules.push(Box::new(rule));
181 }
182
183 pub fn disable_rule(&mut self, rule_id: &str) {
185 self.disabled.insert(rule_id.to_string());
186 }
187
188 pub fn rule_ids(&self) -> Vec<&str> {
190 self.rules.iter().map(|r| r.id()).collect()
191 }
192
193 pub fn check(&self, model: &Model) -> ValidationResult {
195 let mut result = ValidationResult::default();
196 for rule in &self.rules {
197 if self.disabled.contains(rule.id()) {
198 continue;
199 }
200 for violation in rule.check(model, &self.context) {
201 match violation.severity {
202 Severity::Error => result.errors += 1,
203 Severity::Warning => result.warnings += 1,
204 Severity::Info => {}
205 }
206 result.violations.push(violation);
207 }
208 }
209 result
210 }
211}
212
213impl Default for OnnxChecker {
214 fn default() -> Self {
215 Self::new()
216 }
217}
218
219#[cfg(test)]
220mod tests {
221 use super::*;
222
223 struct AlwaysFails;
224 impl ValidationRule for AlwaysFails {
225 fn id(&self) -> &str {
226 "test.always_fails"
227 }
228 fn severity(&self) -> Severity {
229 Severity::Error
230 }
231 fn check(&self, _model: &Model, _ctx: &ValidationContext) -> Vec<Violation> {
232 vec![Violation {
233 rule_id: self.id().to_string(),
234 severity: Severity::Error,
235 message: "boom".to_string(),
236 location: ViolationLocation::Model,
237 }]
238 }
239 }
240
241 fn empty_model() -> Model {
242 let mut g = onnx_runtime_ir::Graph::new();
243 g.opset_imports.insert(String::new(), 21);
244 Model::new(g)
245 }
246
247 #[test]
248 fn empty_checker_reports_no_violations() {
249 let result = OnnxChecker::empty().check(&empty_model());
250 assert!(result.is_valid());
251 assert!(result.violations.is_empty());
252 }
253
254 #[test]
255 fn custom_rule_runs_and_counts_errors() {
256 let mut checker = OnnxChecker::empty();
257 checker.add_rule(AlwaysFails);
258 let result = checker.check(&empty_model());
259 assert_eq!(result.errors, 1);
260 assert!(!result.is_valid());
261 }
262
263 #[test]
264 fn disabled_rule_is_skipped() {
265 let mut checker = OnnxChecker::empty();
266 checker.add_rule(AlwaysFails);
267 checker.disable_rule("test.always_fails");
268 let result = checker.check(&empty_model());
269 assert!(result.is_valid());
270 }
271
272 #[test]
273 fn default_checker_has_builtin_rules() {
274 let checker = OnnxChecker::new();
275 let ids = checker.rule_ids();
276 assert!(ids.contains(&"ir.opset_import_present"));
277 assert!(ids.contains(&"structure.duplicate_value_name"));
278 assert!(ids.contains(&"structure.graph_acyclic"));
279 assert!(ids.contains(&"schema.node_conforms"));
280 assert!(ids.contains(&"structure.input_output_declared"));
281 assert!(ids.contains(&"structure.no_unconnected_nodes"));
282 assert!(ids.contains(&"schema.type_constraint_satisfied"));
283 assert!(ids.contains(&"type.initializer_matches_declared"));
284 assert!(ids.contains(&"ir.version_supported"));
285 assert!(ids.contains(&"ir.version_gated_features"));
286 assert!(ids.contains(&"proto.function_valid"));
287 assert!(ids.contains(&"proto.metadata_keys_unique"));
288 assert!(ids.contains(&"proto.attribute_valid"));
289 assert!(ids.contains(&"proto.type_valid"));
290 assert!(ids.contains(&"proto.tensor_payload_valid"));
291 assert!(ids.contains(&"proto.sparse_tensor_valid"));
292 assert!(ids.contains(&"multidevice.configuration_valid"));
293 }
294
295 #[test]
296 fn validation_context_exposes_custom_registry() {
297 let context = ValidationContext::new(SchemaRegistry::new());
298 assert_eq!(context.schemas().iter().count(), 0);
299 let checker = OnnxChecker::with_schema_registry(SchemaRegistry::new());
300 assert!(checker.rule_ids().contains(&"schema.node_conforms"));
301 }
302}