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