1extern crate alloc;
118
119use alloc::{borrow::Cow, boxed::Box, collections::BTreeMap, string::String, vec::Vec};
120use core::fmt;
121
122use aranya_crypto::BaseId;
123use aranya_policy_vm::{
124 ActionContext, CommandContext, CommandDef, ExitReason, KVPair, Machine, MachineIO,
125 MachineStack, OpenContext, PolicyContext, RunState, Stack as _, Struct, Value,
126 ast::{Identifier, Persistence},
127};
128use buggy::{BugExt as _, bug};
129use tracing::{error, info, instrument};
130
131use crate::{
132 ActionPlacement, Address, CommandPlacement, FactPerspective, MergeIds, Perspective, Prior,
133 Priority,
134 command::{CmdId, Command},
135 policy::{NullSink, Policy, PolicyError, Sink},
136};
137
138mod error;
139mod io;
140mod protocol;
141pub mod testing;
142
143pub use error::*;
144pub use io::*;
145pub use protocol::*;
146
147#[macro_export]
159macro_rules! vm_action {
160 ($name:ident($($arg:expr),* $(,)?)) => {
161 $crate::VmAction {
162 name: ::aranya_policy_vm::ident!(stringify!($name)),
163 args: [$(::aranya_policy_vm::Value::from($arg)),*].as_slice().into(),
164 }
165 };
166}
167
168#[macro_export]
182macro_rules! vm_effect {
183 ($name:ident { $($field:ident : $val:expr),* $(,)? }) => {
184 $crate::VmEffectData {
185 name: ::aranya_policy_vm::ident!(stringify!($name)),
186 fields: vec![$(
187 ::aranya_policy_vm::KVPair::new(::aranya_policy_vm::ident!(stringify!($field)), $val.into())
188 ),*],
189 }
190 };
191}
192
193pub struct VmPolicy<CE> {
195 machine: Machine,
196 engine: CE,
197 ffis: Vec<Box<dyn FfiCallable<CE> + Send + 'static>>,
198 priority_map: BTreeMap<Identifier, VmPriority>,
199}
200
201impl<CE> VmPolicy<CE> {
202 pub fn new(
204 machine: Machine,
205 engine: CE,
206 ffis: Vec<Box<dyn FfiCallable<CE> + Send + 'static>>,
207 ) -> Result<Self, VmPolicyError> {
208 let priority_map = get_command_priorities(&machine)?;
209 Ok(Self {
210 machine,
211 engine,
212 ffis,
213 priority_map,
214 })
215 }
216
217 fn source_location<M>(&self, rs: &RunState<'_, M>) -> String
218 where
219 M: MachineIO<MachineStack>,
220 {
221 rs.source_location()
222 .unwrap_or_else(|| String::from("(unknown location)"))
223 }
224}
225
226fn get_command_priorities(
228 machine: &Machine,
229) -> Result<BTreeMap<Identifier, VmPriority>, AttributeError> {
230 let mut priority_map = BTreeMap::new();
231 for def in machine.command_defs.iter() {
232 let name = &def.name.name;
233 let attrs = PriorityAttrs::load(name.as_str(), def)?;
234 match def.persistence {
235 Persistence::Persistent => {
236 priority_map.insert(name.clone(), get_command_priority(name, &attrs)?);
237 }
238 Persistence::Ephemeral { .. } => {
239 if attrs != PriorityAttrs::default() {
240 return Err(AttributeError(
241 "ephemeral command must not have priority".into(),
242 ));
243 }
244 }
245 }
246 }
247 Ok(priority_map)
248}
249
250#[derive(Default, PartialEq)]
251struct PriorityAttrs {
252 init: bool,
253 finalize: bool,
254 priority: Option<u32>,
255}
256
257impl PriorityAttrs {
258 fn load(name: &str, def: &CommandDef) -> Result<Self, AttributeError> {
259 let attrs = &def.attributes;
260 let init = attrs
261 .get("init")
262 .map(|attr| match attr.value {
263 Value::Bool(b) => Ok(b),
264 _ => Err(AttributeError::type_mismatch(
265 name,
266 "finalize",
267 "Bool",
268 &attr.value.type_name(),
269 )),
270 })
271 .transpose()?
272 == Some(true);
273 let finalize = attrs
274 .get("finalize")
275 .map(|attr| match attr.value {
276 Value::Bool(b) => Ok(b),
277 _ => Err(AttributeError::type_mismatch(
278 name,
279 "finalize",
280 "Bool",
281 &attr.value.type_name(),
282 )),
283 })
284 .transpose()?
285 == Some(true);
286 let priority: Option<u32> = attrs
287 .get("priority")
288 .map(|attr| match attr.value {
289 Value::Int(b) => b.try_into().map_err(|_| {
290 AttributeError::int_range(name, "priority", u32::MIN.into(), u32::MAX.into())
291 }),
292 _ => Err(AttributeError::type_mismatch(
293 name,
294 "priority",
295 "Int",
296 &attr.value.type_name(),
297 )),
298 })
299 .transpose()?;
300 Ok(Self {
301 init,
302 finalize,
303 priority,
304 })
305 }
306}
307
308fn get_command_priority(
309 name: &Identifier,
310 attrs: &PriorityAttrs,
311) -> Result<VmPriority, AttributeError> {
312 match (attrs.init, attrs.finalize, attrs.priority) {
313 (true, true, _) => Err(AttributeError::exclusive(name.as_str(), "init", "finalize")),
314 (true, false, Some(_)) => Err(AttributeError::exclusive(name.as_str(), "init", "priority")),
315 (true, false, None) => Ok(VmPriority::Init),
316
317 (false, true, Some(_)) => Err(AttributeError::exclusive(
318 name.as_str(),
319 "finalize",
320 "priority",
321 )),
322 (false, true, None) => Ok(VmPriority::Finalize),
323
324 (false, false, Some(n)) => Ok(VmPriority::Basic(n)),
325
326 (false, false, None) => Err(AttributeError::missing(
327 name.as_str(),
328 "init | finalize | priority",
329 )),
330 }
331}
332
333impl<CE: aranya_crypto::Engine> VmPolicy<CE> {
334 #[allow(clippy::too_many_arguments)]
335 #[instrument(skip_all, fields(name = name.as_str()))]
336 fn evaluate_rule<'a, P>(
337 &self,
338 name: Identifier,
339 fields: &[KVPair],
340 envelope: Envelope<'_>,
341 facts: &'a mut P,
342 sink: &'a mut impl Sink<VmEffect>,
343 ctx: CommandContext,
344 placement: CommandPlacement,
345 ) -> Result<(), PolicyError>
346 where
347 P: FactPerspective,
348 {
349 let mut io = VmPolicyIO::new(facts, sink, &self.engine, &self.ffis);
350 let mut rs = self.machine.create_run_state(&mut io, ctx);
351 let this_data = Struct::new(name, fields);
352 match rs.call_command_policy(this_data.clone(), envelope.clone().into()) {
353 Ok(reason) => match reason {
354 ExitReason::Normal => Ok(()),
355 ExitReason::Yield => bug!("unexpected yield"),
356 ExitReason::Check => {
357 info!("Check {}", self.source_location(&rs));
358
359 match placement {
360 CommandPlacement::OnGraphAtOrigin | CommandPlacement::OffGraph => {
361 return Err(PolicyError::Check);
363 }
364 CommandPlacement::OnGraphInBraid => {
365 }
367 }
368
369 let CommandContext::Policy(policy_ctx) = rs.get_context() else {
371 error!(
372 "Non-policy context while evaluating rule: {:?}",
373 rs.get_context()
374 );
375 return Err(PolicyError::InternalError);
376 };
377 let recall_ctx = CommandContext::Recall(policy_ctx.clone());
378 rs.set_context(recall_ctx);
379 self.recall_internal(&mut rs, this_data, envelope)
380 }
381 ExitReason::Panic => {
382 info!("Panicked {}", self.source_location(&rs));
383 Err(PolicyError::Panic)
384 }
385 },
386 Err(e) => {
387 error!("\n{e}");
388 Err(PolicyError::InternalError)
389 }
390 }
391 }
392
393 fn recall_internal<M>(
394 &self,
395 rs: &mut RunState<'_, M>,
396 this_data: Struct,
397 envelope: Envelope<'_>,
398 ) -> Result<(), PolicyError>
399 where
400 M: MachineIO<MachineStack>,
401 {
402 match rs.call_command_recall(this_data, envelope.into()) {
403 Ok(ExitReason::Normal) => Err(PolicyError::Check),
404 Ok(ExitReason::Yield) => bug!("unexpected yield"),
405 Ok(ExitReason::Check) => {
406 info!("Recall failed: {}", self.source_location(rs));
407 Err(PolicyError::Check)
408 }
409 Ok(ExitReason::Panic) | Err(_) => {
410 info!("Recall panicked: {}", self.source_location(rs));
411 Err(PolicyError::Panic)
412 }
413 }
414 }
415
416 #[instrument(skip_all, fields(name = name.as_str()))]
417 fn open_command<P>(
418 &self,
419 name: Identifier,
420 envelope: Envelope<'_>,
421 facts: &mut P,
422 ) -> Result<Struct, PolicyError>
423 where
424 P: FactPerspective,
425 {
426 let mut sink = NullSink;
427 let mut io = VmPolicyIO::new(facts, &mut sink, &self.engine, &self.ffis);
428 let ctx = CommandContext::Open(OpenContext { name: name.clone() });
429 let mut rs = self.machine.create_run_state(&mut io, ctx);
430 let status = rs.call_open(name, envelope.into());
431 match status {
432 Ok(reason) => match reason {
433 ExitReason::Normal => {
434 let v = rs.consume_return().map_err(|e| {
435 error!("Could not pull envelope from stack: {e}");
436 PolicyError::InternalError
437 })?;
438 Ok(v.try_into().map_err(|e| {
439 error!("Envelope is not a struct: {e}");
440 PolicyError::InternalError
441 })?)
442 }
443 ExitReason::Yield => bug!("unexpected yield"),
444 ExitReason::Check => {
445 info!("Check {}", self.source_location(&rs));
446 Err(PolicyError::Check)
447 }
448 ExitReason::Panic => {
449 info!("Panicked {}", self.source_location(&rs));
450 Err(PolicyError::Check)
451 }
452 },
453 Err(e) => {
454 error!("\n{e}");
455 Err(PolicyError::InternalError)
456 }
457 }
458 }
459}
460
461#[derive(Clone, Debug, PartialEq, Eq)]
463pub struct VmAction<'a> {
464 pub name: Identifier,
466 pub args: Cow<'a, [Value]>,
468}
469
470#[derive(Debug)]
474pub struct VmEffectData {
475 pub name: Identifier,
477 pub fields: Vec<KVPair>,
479}
480
481impl PartialEq<VmEffect> for VmEffectData {
482 fn eq(&self, other: &VmEffect) -> bool {
483 self.name == other.name && self.fields == other.fields
484 }
485}
486
487impl PartialEq<VmEffectData> for VmEffect {
488 fn eq(&self, other: &VmEffectData) -> bool {
489 self.name == other.name && self.fields == other.fields
490 }
491}
492
493#[derive(Clone, Debug, PartialEq, Eq)]
495pub struct VmEffect {
496 pub name: Identifier,
498 pub fields: Vec<KVPair>,
500 pub command: CmdId,
502 pub recalled: bool,
504}
505
506#[derive(Copy, Clone, Debug, PartialEq)]
507enum VmPriority {
508 Init,
509 Basic(u32),
510 Finalize,
511}
512
513impl Default for VmPriority {
514 fn default() -> Self {
515 Self::Basic(0)
516 }
517}
518
519impl From<VmPriority> for Priority {
520 fn from(value: VmPriority) -> Self {
521 match value {
522 VmPriority::Init => Self::Init,
523 VmPriority::Basic(p) => Self::Basic(p),
524 VmPriority::Finalize => Self::Finalize,
525 }
526 }
527}
528
529impl<CE> VmPolicy<CE> {
530 fn get_command_priority(&self, name: &Identifier) -> VmPriority {
531 debug_assert!(self.machine.command_defs.contains(name));
532 self.priority_map.get(name).copied().unwrap_or_default()
533 }
534}
535
536impl<CE: aranya_crypto::Engine> Policy for VmPolicy<CE> {
537 type Action<'a> = VmAction<'a>;
538 type Effect = VmEffect;
539 type Command<'a> = VmProtocol<'a>;
540
541 fn serial(&self) -> u32 {
542 0u32
544 }
545
546 #[instrument(skip_all)]
547 fn call_rule(
548 &self,
549 command: &impl Command,
550 facts: &mut impl FactPerspective,
551 sink: &mut impl Sink<Self::Effect>,
552 placement: CommandPlacement,
553 ) -> Result<(), PolicyError> {
554 let parent_id = match command.parent() {
555 Prior::None => CmdId::default(),
556 Prior::Single(parent) => parent.id,
557 Prior::Merge(_, _) => bug!("merge commands are not evaluated"),
558 };
559
560 let VmProtocolData {
561 author_id,
562 kind,
563 serialized_fields,
564 signature,
565 } = postcard::from_bytes(command.bytes()).map_err(|e| {
566 error!("Could not deserialize: {e:?}");
567 PolicyError::Read
568 })?;
569
570 let expected_priority = self.get_command_priority(&kind).into();
571 if command.priority() != expected_priority {
572 error!(
573 "Expected priority {:?}, got {:?}",
574 expected_priority,
575 command.priority()
576 );
577 bug!("Command has invalid priority");
578 }
579
580 let def = self.machine.command_defs.get(&kind).ok_or_else(|| {
581 error!("unknown command {kind}");
582 PolicyError::InternalError
583 })?;
584
585 let envelope = Envelope {
586 parent_id,
587 author_id,
588 command_id: command.id(),
589 payload: Cow::Borrowed(serialized_fields),
590 signature: Cow::Borrowed(signature),
591 };
592
593 match (placement, &def.persistence) {
594 (CommandPlacement::OnGraphAtOrigin, Persistence::Persistent) => {}
595 (CommandPlacement::OnGraphInBraid, Persistence::Persistent) => {}
596 (CommandPlacement::OffGraph, Persistence::Ephemeral(_)) => {}
597 (CommandPlacement::OnGraphAtOrigin, Persistence::Ephemeral(_)) => {
598 error!("cannot evaluate ephemeral command on-graph");
599 return Err(PolicyError::InternalError);
600 }
601 (CommandPlacement::OnGraphInBraid, Persistence::Ephemeral(_)) => {
602 error!("cannot evaluate ephemeral command in braid");
603 return Err(PolicyError::InternalError);
604 }
605 (CommandPlacement::OffGraph, Persistence::Persistent) => {
606 error!("cannot evaluate persistent command off-graph");
607 return Err(PolicyError::InternalError);
608 }
609 }
610
611 let command_struct = self.open_command(kind.clone(), envelope.clone(), facts)?;
612 let fields: Vec<KVPair> = command_struct
613 .fields
614 .into_iter()
615 .map(|(k, v)| KVPair::new(k, v))
616 .collect();
617 let ctx = CommandContext::Policy(PolicyContext {
618 name: kind.clone(),
619 id: command.id(),
620 author: author_id,
621 version: BaseId::default(),
622 });
623 self.evaluate_rule(
624 kind,
625 fields.as_slice(),
626 envelope,
627 facts,
628 sink,
629 ctx,
630 placement,
631 )?;
632
633 Ok(())
634 }
635
636 #[instrument(skip_all, fields(name = action.name.as_str()))]
637 fn call_action(
638 &self,
639 action: Self::Action<'_>,
640 facts: &mut impl Perspective,
641 sink: &mut impl Sink<Self::Effect>,
642 action_placement: ActionPlacement,
643 ) -> Result<(), PolicyError> {
644 let VmAction { name, args } = action;
645
646 let def = self.machine.action_defs.get(&name).ok_or_else(|| {
647 error!("action not found");
648 PolicyError::InternalError
649 })?;
650
651 match (action_placement, &def.persistence) {
652 (ActionPlacement::OnGraph, Persistence::Persistent) => {}
653 (ActionPlacement::OffGraph, Persistence::Ephemeral(_)) => {}
654 (ActionPlacement::OnGraph, Persistence::Ephemeral(_)) => {
655 error!("cannot call ephemeral action on-graph");
656 return Err(PolicyError::InternalError);
657 }
658 (ActionPlacement::OffGraph, Persistence::Persistent) => {
659 error!("cannot call persistent action off-graph");
660 return Err(PolicyError::InternalError);
661 }
662 }
663
664 let parent = match facts.head_address()? {
665 Prior::None => None,
666 Prior::Single(id) => Some(id),
667 Prior::Merge(_, _) => bug!("cannot have a merge parent in call_action"),
668 };
669 let ctx_parent = parent.map(|a| a.id).unwrap_or_default();
672 let mut io = VmPolicyIO::new(facts, sink, &self.engine, &self.ffis);
673 let ctx = CommandContext::Action(ActionContext {
674 name: name.clone(),
675 head_id: ctx_parent,
676 });
677 let command_placement = match action_placement {
678 ActionPlacement::OnGraph => CommandPlacement::OnGraphAtOrigin,
679 ActionPlacement::OffGraph => CommandPlacement::OffGraph,
680 };
681 {
682 let mut rs = self.machine.create_run_state(&mut io, ctx);
683 let mut exit_reason = match args {
684 Cow::Borrowed(args) => rs.call_action(name, args.iter().cloned()),
685 Cow::Owned(args) => rs.call_action(name, args),
686 }
687 .map_err(|e| {
688 error!("\n{e}");
689 PolicyError::InternalError
690 })?;
691 loop {
692 match exit_reason {
693 ExitReason::Normal => {
694 break;
696 }
697 ExitReason::Yield => {
698 let command_struct: Struct = rs.stack.pop().map_err(|e| {
700 error!("should have command struct: {e}");
701 PolicyError::InternalError
702 })?;
703 let command_name = command_struct.name.clone();
704
705 let seal_ctx = rs.get_context().seal_from_action(command_name.clone())?;
706 let mut rs_seal = self.machine.create_run_state(rs.io, seal_ctx);
707 match rs_seal.call_seal(command_struct).map_err(|e| {
708 error!("Cannot seal command: {}", e);
709 PolicyError::Panic
710 })? {
711 ExitReason::Normal => (),
712 r @ (ExitReason::Yield | ExitReason::Check | ExitReason::Panic) => {
713 error!("Could not seal command: {}", r);
714 return Err(PolicyError::Panic);
715 }
716 }
717
718 let envelope_struct: Struct = rs_seal.stack.pop().map_err(|e| {
720 error!("Expected a sealed envelope {e}");
721 PolicyError::InternalError
722 })?;
723 let envelope = Envelope::try_from(envelope_struct).map_err(|e| {
724 error!("Malformed envelope: {e}");
725 PolicyError::InternalError
726 })?;
727
728 let parent = rs.io.facts.head_address()?;
731 let priority = self.get_command_priority(&command_name).into();
732
733 let policy;
734 match parent {
735 Prior::None => {
736 policy = Some(0u64.to_le_bytes());
738 if !matches!(priority, Priority::Init) {
739 error!(
740 "Command {command_name} has invalid priority {priority:?}"
741 );
742 return Err(PolicyError::InternalError);
743 }
744 }
745 Prior::Single(_) => {
746 policy = None;
747 if !matches!(priority, Priority::Basic(_) | Priority::Finalize) {
748 error!(
749 "Command {command_name} has invalid priority {priority:?}"
750 );
751 return Err(PolicyError::InternalError);
752 }
753 }
754 Prior::Merge(_, _) => bug!("cannot have a merge parent in call_action"),
755 }
756
757 let data = VmProtocolData {
758 author_id: envelope.author_id,
759 kind: command_name.clone(),
760 serialized_fields: &envelope.payload,
761 signature: &envelope.signature,
762 };
763
764 let wrapped = postcard::to_allocvec(&data)
765 .assume("can serialize vm protocol data")?;
766
767 let new_command = VmProtocol {
768 id: envelope.command_id,
769 priority,
770 parent,
771 policy,
772 data: &wrapped,
773 };
774
775 self.call_rule(&new_command, rs.io.facts, rs.io.sink, command_placement)?;
776 rs.io.facts.add_command(&new_command).map_err(|e| {
777 error!("{e}");
778 PolicyError::Write
779 })?;
780
781 rs.update_context_with_new_head(new_command.id())?;
783
784 exit_reason = rs.run().map_err(|e| {
786 error!("{e}");
787 PolicyError::InternalError
788 })?;
789 }
790 ExitReason::Check => {
791 info!("Check {}", self.source_location(&rs));
792 return Err(PolicyError::Check);
793 }
794 ExitReason::Panic => {
795 info!("Panicked {}", self.source_location(&rs));
796 return Err(PolicyError::Panic);
797 }
798 }
799 }
800 }
801
802 Ok(())
803 }
804
805 fn merge<'a>(
806 &self,
807 _target: &'a mut [u8],
808 ids: MergeIds,
809 ) -> Result<Self::Command<'a>, PolicyError> {
810 let (left, right): (Address, Address) = ids.into();
811 let id = aranya_crypto::merge_cmd_id::<CE::CS>(left.id, right.id);
812 Ok(VmProtocol {
813 id,
814 priority: Priority::Merge,
815 parent: Prior::Merge(left, right),
816 policy: None,
817 data: &[],
818 })
819 }
820}
821
822impl fmt::Display for VmAction<'_> {
823 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
824 let mut d = f.debug_tuple(self.name.as_str());
825 for arg in self.args.as_ref() {
826 d.field(&DebugViaDisplay(arg));
827 }
828 d.finish()
829 }
830}
831
832impl fmt::Display for VmEffect {
833 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
834 let mut d = f.debug_struct(self.name.as_str());
835 for field in &self.fields {
836 d.field(field.key().as_str(), &DebugViaDisplay(field.value()));
837 }
838 d.finish()
839 }
840}
841
842struct DebugViaDisplay<T>(T);
844
845impl<T: fmt::Display> fmt::Debug for DebugViaDisplay<T> {
846 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
847 write!(f, "{}", self.0)
848 }
849}
850
851#[cfg(test)]
852mod test {
853 use alloc::format;
854
855 use aranya_policy_compiler::Compiler;
856 use aranya_policy_lang::lang::parse_policy_str;
857 use aranya_policy_vm::ast::Version;
858
859 use super::*;
860
861 #[test]
862 fn test_require_command_priority() {
863 let cases = [
864 r#"command Test {
865 fields {}
866 seal { return todo() }
867 open { return todo() }
868 policy {}
869 }"#,
870 r#"command Test {
871 attributes {}
872 fields {}
873 seal { return todo() }
874 open { return todo() }
875 policy {}
876 }"#,
877 r#"command Test {
878 attributes {
879 init: false,
880 finalize: false,
881 }
882 fields {}
883 seal { return todo() }
884 open { return todo() }
885 policy {}
886 }"#,
887 ];
888
889 for case in cases {
890 let ast = parse_policy_str(case, Version::V2).unwrap_or_else(|e| panic!("{e}"));
891 let module = Compiler::new(&ast)
892 .compile()
893 .unwrap_or_else(|e| panic!("{e}"));
894 let machine = Machine::from_module(module).expect("can create machine");
895 let err = get_command_priorities(&machine).expect_err("should fail");
896 assert_eq!(
897 err,
898 AttributeError::missing("Test", "init | finalize | priority")
899 );
900 }
901 }
902
903 #[test]
904 fn test_get_command_priorities() {
905 fn process(attrs: &str) -> Result<VmPriority, AttributeError> {
906 let policy = format!(
907 r#"
908 command Test {{
909 attributes {{
910 {attrs}
911 }}
912 fields {{ }}
913 seal {{ return todo() }}
914 open {{ return todo() }}
915 policy {{ }}
916 }}
917 "#
918 );
919 let ast = parse_policy_str(&policy, Version::V2).unwrap_or_else(|e| panic!("{e}"));
920 let module = Compiler::new(&ast)
921 .compile()
922 .unwrap_or_else(|e| panic!("{e}"));
923 let machine = Machine::from_module(module).expect("can create machine");
924 let priorities = get_command_priorities(&machine)?;
925 Ok(*priorities.get("Test").expect("priorities are mandatory"))
926 }
927
928 assert_eq!(process("priority: 42"), Ok(VmPriority::Basic(42)));
929 assert_eq!(
930 process("finalize: false, priority: 42"),
931 Ok(VmPriority::Basic(42))
932 );
933 assert_eq!(
934 process("init: false, priority: 42, finalize: false"),
935 Ok(VmPriority::Basic(42))
936 );
937
938 assert_eq!(process("init: true"), Ok(VmPriority::Init));
939 assert_eq!(process("finalize: true"), Ok(VmPriority::Finalize));
940
941 assert_eq!(
942 process("finalize: 42"),
943 Err(AttributeError::type_mismatch(
944 "Test", "finalize", "Bool", "Int"
945 ))
946 );
947 assert_eq!(
948 process("priority: false"),
949 Err(AttributeError::type_mismatch(
950 "Test", "priority", "Int", "Bool"
951 ))
952 );
953 assert_eq!(
954 process("priority: -1"),
955 Err(AttributeError::int_range(
956 "Test",
957 "priority",
958 u32::MIN.into(),
959 u32::MAX.into(),
960 ))
961 );
962 assert_eq!(
963 process(&format!("priority: {}", i64::MAX)),
964 Err(AttributeError::int_range(
965 "Test",
966 "priority",
967 u32::MIN.into(),
968 u32::MAX.into(),
969 ))
970 );
971
972 assert_eq!(
973 process("finalize: true, priority: 42"),
974 Err(AttributeError::exclusive("Test", "finalize", "priority"))
975 );
976 assert_eq!(
977 process("init: true, priority: 42"),
978 Err(AttributeError::exclusive("Test", "init", "priority"))
979 );
980 assert_eq!(
981 process("init: true, finalize: true"),
982 Err(AttributeError::exclusive("Test", "init", "finalize"))
983 );
984 }
985}