taco_threshold_automaton/
path.rs

1//! Concrete Paths in a Threshold Automaton
2//!
3//! This module defines the types for concrete paths in a threshold automaton.
4//! Such a path can, for example, be used to represent a counterexample to a
5//! specification.
6//!
7//! A full path is represented by a [`Path`] object that contains a sequence of
8//! [`Configuration`]s and [`Transition`]s.
9//!
10//! To construct a [`Path`], a [`PathBuilder`] should be used, as it will ensure
11//! that the constructed path is valid in the threshold automaton.
12
13use std::{
14    collections::HashMap,
15    error,
16    fmt::{self},
17    hash,
18};
19
20use taco_display_utils::join_iterator;
21
22use crate::{
23    ActionDefinition, RuleDefinition, ThresholdAutomaton,
24    expressions::{
25        Atomic, BooleanExpression, Location, Parameter, Variable, properties::EvaluationError,
26    },
27    general_threshold_automaton::{GeneralThresholdAutomaton, Rule, UpdateExpression},
28};
29
30/// Global configuration of in a threshold automaton
31///
32/// A configuration specifies how many processes are in each location of the
33/// threshold automaton and the valuation of the shared variables
34#[derive(Debug, Clone, PartialEq)]
35pub struct Configuration {
36    /// Valuation of the shared variables
37    variable_assignment: HashMap<Variable, u32>,
38    /// Number of processes per location
39    location_assignment: HashMap<Location, u32>,
40}
41impl Configuration {
42    /// Create a new configuration
43    ///
44    /// This function creates a new configuration with the given variable and
45    /// location assignments.
46    ///
47    /// The variable and location assignments are represented as [`HashMap`]s,
48    /// where the keys are the variables and locations, and the values are the
49    /// number of processes in the respective location or the value of the
50    /// variable.
51    pub fn new<T, S>(variable_assignment: T, location_assignment: S) -> Self
52    where
53        T: Into<HashMap<Variable, u32>>,
54        S: Into<HashMap<Location, u32>>,
55    {
56        Configuration {
57            variable_assignment: variable_assignment.into(),
58            location_assignment: location_assignment.into(),
59        }
60    }
61
62    /// Number of processes in location `loc`
63    ///
64    /// This function returns the number of processes in the given location
65    /// `loc`. If the location is not known in the configuration, the function
66    /// returns `0`.
67    pub fn get_processes_in_location(&self, loc: &Location) -> u32 {
68        *self.location_assignment.get(loc).unwrap_or(&0)
69    }
70
71    /// Current value of variable
72    ///
73    /// This function returns the value of the variable `var` in the configuration.
74    /// If the variable is not known in the configuration, the function returns `0`.
75    pub fn get_variable_value(&self, var: &Variable) -> u32 {
76        *self.variable_assignment.get(var).unwrap_or(&0)
77    }
78
79    /// Iterator over the variable assignment
80    ///
81    /// This function returns an iterator over the variable assignment,
82    /// where each item is a tuple of the variable and its value.
83    pub fn variable_assignment(&self) -> impl Iterator<Item = (&Variable, &u32)> {
84        self.variable_assignment.iter()
85    }
86
87    /// Iterator over the location assignment
88    ///
89    /// This function returns an iterator over the location assignment,
90    /// where each item is a tuple of the location and the number of processes
91    /// in it.
92    pub fn location_assignment(&self) -> impl Iterator<Item = (&Location, &u32)> {
93        self.location_assignment.iter()
94    }
95
96    /// Create the successor configuration after applying a transition
97    ///
98    /// Returns `None` if the transition cannot be applied to the configuration.
99    /// This can for example happen if the source location of the applied rule
100    /// has no processes or not enough processes to apply the rule.
101    pub fn apply_rule(&self, tr: &Transition) -> Option<Self> {
102        // Check whether the source location has at least one process (necessary
103        // for self-loops)
104        if *self.location_assignment.get(tr.rule_used.source())? == 0 {
105            return None;
106        }
107
108        let mut new_loc_assignment = self.location_assignment.clone();
109        let mut new_var_assignment = self.variable_assignment.clone();
110
111        new_loc_assignment
112            .entry(tr.rule_used.target().clone())
113            .and_modify(|e| *e += tr.number_applied);
114
115        if let Some(e) = new_loc_assignment.get_mut(tr.rule_used.source()) {
116            if let Some(sub) = e.checked_sub(tr.number_applied) {
117                *e = sub;
118            } else {
119                return None;
120            }
121        }
122
123        for act in tr.rule_used.actions() {
124            let var = act.variable();
125
126            match act.update() {
127                UpdateExpression::Inc(i) => {
128                    *new_var_assignment.entry(var.clone()).or_insert(0) += tr.number_applied * i;
129                }
130                UpdateExpression::Dec(d) => {
131                    *new_var_assignment.entry(var.clone()).or_insert(0) -= tr.number_applied * d;
132                }
133                UpdateExpression::Reset => _ = new_var_assignment.insert(var.clone(), 0),
134                UpdateExpression::Unchanged => continue,
135            }
136        }
137
138        Some(Self {
139            variable_assignment: new_var_assignment,
140            location_assignment: new_loc_assignment,
141        })
142    }
143
144    /// Returns a compact representation of the configuration skipping locations
145    /// and variables which are assigned 0
146    pub fn display_compact(&self) -> String {
147        let mut locs = self
148            .location_assignment
149            .iter()
150            .filter(|(_, n)| **n > 0)
151            .map(|(l, _)| l)
152            .collect::<Vec<_>>();
153        locs.sort_by_key(|loc| loc.name());
154        let locs = join_iterator(
155            locs.into_iter()
156                .map(|loc| format!("{} : {}", loc.name(), self.location_assignment[loc])),
157            ", ",
158        );
159
160        let mut vars = self
161            .variable_assignment
162            .iter()
163            .filter(|(_, n)| **n > 0)
164            .map(|(v, _)| v)
165            .collect::<Vec<_>>();
166        vars.sort_by_key(|var| var.name());
167        let vars = join_iterator(
168            vars.into_iter()
169                .map(|var| format!("{} : {}", var.name(), self.variable_assignment[var])),
170            ", ",
171        );
172
173        format!("locations: ({locs}), variables: ({vars})")
174    }
175}
176
177/// Transition that applying a rule a certain number of times
178///
179/// This struct represents a transition in a threshold automaton that applies a
180/// rule a fixed number of times. It contains the rule that is applied and the
181/// number of times the rule is applied.
182/// In the literature such transitions are often called accelerated.
183#[derive(Debug, Clone, PartialEq)]
184pub struct Transition {
185    /// Rule that is applied in this transition
186    rule_used: Rule,
187    /// Number of times the rule is applied
188    number_applied: u32,
189}
190
191impl Transition {
192    /// Creates a new transition
193    ///
194    /// Creates a new transition that applies the given [`Rule`] `n` times.
195    pub fn new(rule_used: Rule, n: u32) -> Self {
196        Transition {
197            rule_used,
198            number_applied: n,
199        }
200    }
201
202    /// Get the rule of the transition
203    pub fn rule(&self) -> &Rule {
204        &self.rule_used
205    }
206
207    /// Check whether the transition does not change the current configuration
208    ///
209    /// Returns true if this transition does neither update the overall location
210    /// state nor the variable state
211    pub fn is_noop(&self) -> bool {
212        self.number_applied == 0
213            || (self.rule_used.source() == self.rule_used.target()
214                && self.rule_used.actions().all(|act| act.is_unchanged()))
215    }
216}
217
218/// Concrete path in a [`GeneralThresholdAutomaton`]
219///
220/// This struct represents a concrete path in a [`GeneralThresholdAutomaton`],
221/// which consists of a sequence of configurations and transitions between them.
222#[derive(Debug, Clone, PartialEq)]
223pub struct Path {
224    /// The threshold automaton this path belongs to
225    ta: GeneralThresholdAutomaton,
226    /// Parameter assignment for the path
227    parameter_assignment: HashMap<Parameter, u32>,
228    /// Configurations of the path
229    configurations: Vec<Configuration>,
230    /// Transitions of the path
231    transitions: Vec<Transition>,
232}
233impl Path {
234    /// Get the value of parameter `param` in the path
235    pub fn get_parameter_value(&self, param: &Parameter) -> Option<u32> {
236        self.parameter_assignment.get(param).cloned()
237    }
238
239    /// Returns an iterator over the parameters and their values in the path
240    pub fn parameter_values(&self) -> impl Iterator<Item = (&Parameter, &u32)> {
241        self.parameter_assignment.iter()
242    }
243
244    /// Iterator over the configurations of the path
245    pub fn configurations(&self) -> impl Iterator<Item = &Configuration> {
246        self.configurations.iter()
247    }
248
249    /// Iterator over the transitions of the path
250    pub fn transitions(&self) -> impl Iterator<Item = &Transition> {
251        self.transitions.iter()
252    }
253
254    /// Get a string representation of the path
255    pub fn display_compact(&self) -> String {
256        debug_assert!(self.configurations.len() == self.transitions.len() + 1);
257
258        let mut res = format!("Path of threshold automaton {}: \n", self.ta.name());
259
260        let param = display_assignment(self.parameter_assignment.clone());
261        res += &format!("parameters: {param}\n");
262
263        let mut it_transitions = self.transitions.iter();
264        for (n_config, config) in self.configurations.iter().enumerate() {
265            res += &format!("Configuration {n_config}: {}\n", config.display_compact());
266            if let Some(transition) = it_transitions.next() {
267                res += "        |\n";
268                res += &format!("        |{transition}\n");
269                res += "        V\n";
270            }
271        }
272
273        res
274    }
275}
276
277/// Builder for creating a path of a [`GeneralThresholdAutomaton`]
278///
279/// This builder allows to create a path of a [`GeneralThresholdAutomaton`] by
280/// adding configurations and transitions between them. It then verifies that
281/// the configurations and transitions are a valid path in the threshold
282/// automaton.
283///
284/// First, this builder needs to be initialized with a
285/// [`GeneralThresholdAutomaton`] and an assignment of parameters. Then it can
286/// be transformed into an [`InitializedPathBuilder`] using
287/// [`PathBuilder::add_parameter_assignment`], which then configurations and
288/// transitions can be added.
289#[derive(Debug, Clone, PartialEq)]
290pub struct PathBuilder {
291    ta: GeneralThresholdAutomaton,
292    parameter_assignment: HashMap<Parameter, u32>,
293}
294
295impl PathBuilder {
296    /// Create a new path builder for the given [`GeneralThresholdAutomaton`]
297    pub fn new(ta: GeneralThresholdAutomaton) -> Self {
298        Self {
299            ta,
300            parameter_assignment: HashMap::new(),
301        }
302    }
303
304    /// Add a parameter assignment to the path and validate it against the
305    /// resilience conditions of the threshold automaton
306    pub fn add_parameter_assignment(
307        mut self,
308        param_evaluation: impl Into<HashMap<Parameter, u32>>,
309    ) -> Result<InitializedPathBuilder, PathBuilderError> {
310        let param_evaluation = param_evaluation.into();
311
312        self.ta.resilience_conditions().try_for_each(|cond| {
313            if !cond
314                .check_satisfied(&HashMap::new(), &param_evaluation)
315                .map_err(PathBuilderError::from)?
316            {
317                return Err(PathBuilderError::ResilienceConditionNotSatisfied(
318                    Box::new(cond.clone()),
319                    Box::new(param_evaluation.clone()),
320                ));
321            }
322
323            Ok(())
324        })?;
325
326        self.parameter_assignment = param_evaluation;
327
328        Ok(InitializedPathBuilder {
329            ta: self.ta,
330            n_processes: 0,
331            parameter_assignment: self.parameter_assignment,
332            configurations: Vec::new(),
333            transitions: Vec::new(),
334        })
335    }
336}
337
338/// Builder for creating a validated [`Path`] of a [`GeneralThresholdAutomaton`]
339/// that has been initialized with a threshold automaton and a parameter
340/// assignment.
341///
342/// This type should always be derived from a [`PathBuilder`].
343#[derive(Debug, Clone, PartialEq)]
344pub struct InitializedPathBuilder {
345    /// Threshold automaton this path belongs to
346    ta: GeneralThresholdAutomaton,
347    /// Number of processes in the path, will be initialized with the first configuration
348    n_processes: u32,
349    /// Parameter assignment for the path
350    parameter_assignment: HashMap<Parameter, u32>,
351    /// Configurations of the path
352    configurations: Vec<Configuration>,
353    /// Transitions of the path
354    transitions: Vec<Transition>,
355}
356
357impl InitializedPathBuilder {
358    /// Add a configuration to the path
359    pub fn add_configuration(mut self, config: Configuration) -> Result<Self, PathBuilderError> {
360        self.validate_configuration(&config)?;
361        self.configurations.push(config);
362        Ok(self)
363    }
364
365    /// Add multiple configurations to the path
366    pub fn add_configurations(
367        self,
368        configs: impl IntoIterator<Item = Configuration>,
369    ) -> Result<Self, PathBuilderError> {
370        let mut builder = self;
371
372        for config in configs {
373            builder = builder.add_configuration(config)?;
374        }
375
376        Ok(builder)
377    }
378
379    /// Add a transition to the path
380    pub fn add_transition(mut self, transition: Transition) -> Result<Self, PathBuilderError> {
381        // Check that the rule used in the transition are valid
382        if !self.ta.rules().any(|r| *r == transition.rule_used) {
383            return Err(PathBuilderError::UnknownRule(transition.rule_used.clone()));
384        }
385
386        self.transitions.push(transition);
387
388        Ok(self)
389    }
390
391    /// Add multiple transitions to the path
392    pub fn add_transitions(
393        mut self,
394        transitions: impl IntoIterator<Item = Transition>,
395    ) -> Result<Self, PathBuilderError> {
396        for transition in transitions {
397            self = self.add_transition(transition)?;
398        }
399
400        Ok(self)
401    }
402
403    /// Returns the last configuration that was added to the path builder
404    pub fn last_configuration(&self) -> Option<&Configuration> {
405        self.configurations.last()
406    }
407
408    /// Initial validation function of an inserted configuration
409    fn validate_configuration(&mut self, config: &Configuration) -> Result<(), PathBuilderError> {
410        // Check that all locations are present in the location assignment
411        for loc in self.ta.locations() {
412            if !config.location_assignment.contains_key(loc) {
413                return Err(PathBuilderError::MissingLocation(loc.clone()));
414            }
415        }
416
417        // Check that all variables are present in the variable assignment
418        for var in self.ta.variables() {
419            if !config.variable_assignment.contains_key(var) {
420                return Err(PathBuilderError::MissingVariable(var.clone()));
421            }
422        }
423
424        // If this is the first configuration, validate the initial configuration
425        if self.configurations.is_empty() {
426            Self::validate_initial_configuration(self, config)?;
427            self.n_processes = config.location_assignment.values().sum();
428            return Ok(());
429        }
430
431        // Check that the number of processes is consistent between configurations
432        if self.n_processes != config.location_assignment.values().sum() {
433            return Err(PathBuilderError::InconsistentNumberOfProcesses {
434                initial_n: self.n_processes,
435                config_n: config.location_assignment.values().sum(),
436            });
437        }
438
439        Ok(())
440    }
441
442    /// Validate the initial configuration of the path
443    fn validate_initial_configuration(
444        &self,
445        config: &Configuration,
446    ) -> Result<(), PathBuilderError> {
447        // Check that the initial location constraint is satisfied
448        for loc_constr in self.ta.initial_location_conditions() {
449            if !loc_constr
450                .check_satisfied(&config.location_assignment, &self.parameter_assignment)
451                .map_err(PathBuilderError::from)?
452            {
453                return Err(PathBuilderError::InitialLocationConstraintNotSatisfied(
454                    Box::new(loc_constr.clone()),
455                    Box::new(config.location_assignment.clone()),
456                ));
457            }
458        }
459
460        // Check that the initial variable constraint is satisfied
461        for var_constr in self.ta.initial_variable_conditions() {
462            if !var_constr
463                .check_satisfied(&config.variable_assignment, &self.parameter_assignment)
464                .map_err(PathBuilderError::from)?
465            {
466                return Err(PathBuilderError::InitialVariableConstraintNotSatisfied(
467                    Box::new(var_constr.clone()),
468                    Box::new(config.variable_assignment.clone()),
469                ));
470            }
471        }
472
473        Ok(())
474    }
475
476    /// Validate the transition and rule sequence and build the path
477    pub fn build(mut self) -> Result<Path, PathBuilderError> {
478        // empty path, no validation needed
479        if self.configurations.is_empty() {
480            return Ok(Path {
481                ta: self.ta,
482                parameter_assignment: self.parameter_assignment,
483                configurations: self.configurations,
484                transitions: self.transitions,
485            });
486        }
487        // We shorten the error path here remove noop transitions
488        self.transitions.retain(|tr| !tr.is_noop());
489        // remove configurations without updates
490        self.configurations.dedup();
491
492        self.validate_path()?;
493
494        self.shorten_path();
495
496        Ok(Path {
497            ta: self.ta,
498            parameter_assignment: self.parameter_assignment,
499            configurations: self.configurations,
500            transitions: self.transitions,
501        })
502    }
503
504    /// This function shortens an error path by combining successive
505    /// applications of the same rule
506    ///
507    /// It does not reorder rules
508    fn shorten_path(&mut self) {
509        if self.configurations.len() <= 2 {
510            return;
511        }
512
513        let mut new_config = vec![
514            self.configurations[0].clone(),
515            self.configurations[1].clone(),
516        ];
517        let mut new_tr = vec![self.transitions[0].clone()];
518
519        for (tr, config) in self
520            .transitions
521            .iter()
522            .skip(1)
523            .zip(self.configurations.iter().skip(2))
524        {
525            let mut tr = tr.clone();
526
527            if new_tr.last().unwrap().rule() == tr.rule() {
528                tr.number_applied += new_tr.last().unwrap().number_applied;
529
530                new_tr.pop();
531                new_config.pop();
532            }
533
534            new_tr.push(tr);
535            new_config.push(config.clone());
536        }
537
538        self.configurations = new_config;
539        self.transitions = new_tr;
540
541        debug_assert!(self.validate_path().is_ok());
542    }
543
544    /// Validate the path and check that it is consistent
545    fn validate_path(&self) -> Result<(), PathBuilderError> {
546        // Check that the number of configurations and transitions is consistent
547        if self.transitions.len() + 1 != self.configurations.len() {
548            return Err(PathBuilderError::InconsistentNumberOfConfigurations);
549        }
550
551        let mut it_configs = self.configurations.iter();
552        let mut curr_config = it_configs.next().unwrap();
553
554        for transition in self.transitions.iter() {
555            // Check that the transition guard is enabled
556            if !transition
557                .rule_used
558                .guard()
559                .check_satisfied(&curr_config.variable_assignment, &self.parameter_assignment)
560                .unwrap()
561            {
562                return Err(PathBuilderError::GuardNotSatisfied(
563                    Box::new(curr_config.clone()),
564                    Box::new(transition.clone()),
565                ));
566            }
567
568            // number_applied > 1 => guard has to be satisfied throughout executing the transition,
569            // i.e., the last time before next config
570            if transition.number_applied > 1 {
571                let mut transition_with_reduced_counter = transition.clone();
572                transition_with_reduced_counter.number_applied -= 1;
573                let last_config = curr_config.apply_rule(&transition_with_reduced_counter);
574                if last_config.is_none() {
575                    return Err(PathBuilderError::NotEnoughProcesses(
576                        Box::new(curr_config.clone()),
577                        Box::new(transition.clone()),
578                    ));
579                }
580                let last_config = last_config.unwrap();
581                if !transition
582                    .rule_used
583                    .guard()
584                    .check_satisfied(&last_config.variable_assignment, &self.parameter_assignment)
585                    .unwrap()
586                {
587                    return Err(PathBuilderError::GuardNotSatisfied(
588                        Box::new(curr_config.clone()),
589                        Box::new(transition.clone()),
590                    ));
591                }
592            }
593
594            let next_config = it_configs.next().unwrap();
595
596            let calculated_config = curr_config.apply_rule(transition);
597            if calculated_config.is_none() {
598                return Err(PathBuilderError::NotEnoughProcesses(
599                    Box::new(curr_config.clone()),
600                    Box::new(transition.clone()),
601                ));
602            }
603            let calculated_config = calculated_config.unwrap();
604
605            if calculated_config != *next_config {
606                return Err(PathBuilderError::InconsistentTransition {
607                    config: Box::new(curr_config.clone()),
608                    tr: Box::new(transition.clone()),
609                    expected_config: Box::new(next_config.clone()),
610                    next_config: Box::new(calculated_config),
611                });
612            }
613
614            curr_config = next_config;
615        }
616
617        Ok(())
618    }
619}
620
621/// Error that can occur when building a path using a [`PathBuilder`]
622#[derive(Debug, Clone, PartialEq)]
623pub enum PathBuilderError {
624    /// The parameter assignment does not satisfy the resilience conditions
625    ResilienceConditionNotSatisfied(
626        Box<BooleanExpression<Parameter>>,
627        Box<HashMap<Parameter, u32>>,
628    ),
629    /// A parameter is missing in the parameter assignment
630    MissingParameter(Parameter),
631    /// The location assignment does not satisfy the resilience conditions
632    InitialLocationConstraintNotSatisfied(
633        Box<BooleanExpression<Location>>,
634        Box<HashMap<Location, u32>>,
635    ),
636    /// A location is missing in the location assignment
637    MissingLocation(Location),
638    /// The variable assignment does not satisfy the variable conditions
639    InitialVariableConstraintNotSatisfied(
640        Box<BooleanExpression<Variable>>,
641        Box<HashMap<Variable, u32>>,
642    ),
643    /// A variable is missing in the variable assignment
644    MissingVariable(Variable),
645    /// The number of processes is inconsistent between configurations
646    InconsistentNumberOfProcesses {
647        /// Number of processes in the initial configuration
648        initial_n: u32,
649        /// Number of processes in the current configuration
650        config_n: u32,
651    },
652    /// An unknown rule was used in a transition
653    UnknownRule(Rule),
654    /// Inconsistent number of configurations and rules
655    InconsistentNumberOfConfigurations,
656    /// Inconsistent transition
657    InconsistentTransition {
658        /// Configuration involved in the transition
659        config: Box<Configuration>,
660        /// Transition that was applied
661        tr: Box<Transition>,
662        /// Expected configuration that was supplied to the builder
663        expected_config: Box<Configuration>,
664        /// Next configuration that was computed by applying the transition
665        next_config: Box<Configuration>,
666    },
667    /// Guard of rule not satisfied
668    GuardNotSatisfied(Box<Configuration>, Box<Transition>),
669    /// Not enough processes to apply transition
670    NotEnoughProcesses(Box<Configuration>, Box<Transition>),
671}
672
673impl error::Error for PathBuilderError {}
674
675impl fmt::Display for PathBuilderError {
676    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
677        write!(f, "Error building path: ")?;
678        match self {
679            PathBuilderError::ResilienceConditionNotSatisfied(rc, assignment) => {
680                write!(
681                    f,
682                    "Resilience condition {} not satisfied with assignment {}",
683                    rc,
684                    display_assignment(*assignment.clone())
685                )
686            }
687            PathBuilderError::MissingParameter(p) => {
688                write!(f, "Parameter assignment is missing parameter {p}")
689            }
690            PathBuilderError::InitialLocationConstraintNotSatisfied(constr, assignment) => {
691                write!(
692                    f,
693                    "Initial location constraint {} not satisfied with assignment {}",
694                    constr,
695                    display_assignment(*assignment.clone())
696                )
697            }
698            PathBuilderError::MissingLocation(loc) => {
699                write!(f, "Location assignment is missing location {loc}")
700            }
701            PathBuilderError::InitialVariableConstraintNotSatisfied(constr, assignment) => {
702                write!(
703                    f,
704                    "Initial variable constraint {} not satisfied with assignment {}",
705                    constr,
706                    display_assignment(*assignment.clone())
707                )
708            }
709            PathBuilderError::MissingVariable(variable) => {
710                write!(f, "Variable assignment is missing variable {variable}")
711            }
712            PathBuilderError::InconsistentNumberOfProcesses {
713                initial_n,
714                config_n,
715            } => {
716                write!(
717                    f,
718                    "Inconsistent number of processes: expected {initial_n} but got {config_n}"
719                )
720            }
721            PathBuilderError::UnknownRule(rule) => {
722                write!(f, "Unknown rule appears in the transition: {rule}")
723            }
724            PathBuilderError::InconsistentNumberOfConfigurations => {
725                write!(
726                    f,
727                    "Inconsistent number of configurations and rules were supplied to the path builder"
728                )
729            }
730            PathBuilderError::InconsistentTransition {
731                config,
732                tr,
733                expected_config,
734                next_config,
735            } => {
736                write!(
737                    f,
738                    "Inconsistent transition: configuration {config} with transition {tr} does not lead to the expected configuration {expected_config} but to {next_config}"
739                )
740            }
741            PathBuilderError::GuardNotSatisfied(config, tr) => {
742                write!(
743                    f,
744                    "Tried to apply rule {} to configuration {}, but the guard is not satisfied",
745                    tr.rule_used, config
746                )
747            }
748            PathBuilderError::NotEnoughProcesses(config, tr) => {
749                write!(
750                    f,
751                    "Tried to apply rule {} to configuration {}, but there are not enough processes in the source  location",
752                    tr.rule_used.id(),
753                    config
754                )
755            }
756        }
757    }
758}
759
760/// Display a an assignment for example of parameters, locations or variables
761fn display_assignment<K: fmt::Display + hash::Hash + Eq, V: fmt::Display>(
762    map: HashMap<K, V>,
763) -> String {
764    let mut keys = map.keys().collect::<Vec<_>>();
765    keys.sort_by_key(|k| k.to_string());
766    join_iterator(
767        keys.into_iter().map(|k| format!("{} : {}", k, map[k])),
768        ", ",
769    )
770}
771
772impl From<EvaluationError<Parameter>> for PathBuilderError {
773    fn from(err: EvaluationError<Parameter>) -> Self {
774        match err {
775            EvaluationError::AtomicNotFound(p) | EvaluationError::ParameterNotFound(p) => {
776                PathBuilderError::MissingParameter(p)
777            }
778        }
779    }
780}
781
782impl From<EvaluationError<Location>> for PathBuilderError {
783    fn from(err: EvaluationError<Location>) -> Self {
784        match err {
785            EvaluationError::AtomicNotFound(l) => PathBuilderError::MissingLocation(l),
786            EvaluationError::ParameterNotFound(p) => PathBuilderError::MissingParameter(p),
787        }
788    }
789}
790
791impl From<EvaluationError<Variable>> for PathBuilderError {
792    fn from(err: EvaluationError<Variable>) -> Self {
793        match err {
794            EvaluationError::AtomicNotFound(v) => PathBuilderError::MissingVariable(v),
795            EvaluationError::ParameterNotFound(p) => PathBuilderError::MissingParameter(p),
796        }
797    }
798}
799
800impl fmt::Display for Configuration {
801    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
802        let mut locs = self.location_assignment.keys().collect::<Vec<_>>();
803        locs.sort_by_key(|loc| loc.name());
804        let locs = join_iterator(
805            locs.into_iter()
806                .map(|loc| format!("{} : {}", loc.name(), self.location_assignment[loc])),
807            ", ",
808        );
809
810        let mut vars = self.variable_assignment.keys().collect::<Vec<_>>();
811        vars.sort_by_key(|var| var.name());
812        let vars = join_iterator(
813            vars.into_iter()
814                .map(|var| format!("{} : {}", var.name(), self.variable_assignment[var])),
815            ", ",
816        );
817
818        write!(f, "locations: ({locs}), variables: ({vars})")
819    }
820}
821
822impl fmt::Display for Transition {
823    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
824        write!(
825            f,
826            "rule: {} applied {} times",
827            self.rule_used.id(),
828            self.number_applied
829        )
830    }
831}
832
833impl fmt::Display for Path {
834    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
835        debug_assert!(self.configurations.len() == self.transitions.len() + 1);
836
837        writeln!(f, "Path of threshold automaton {}: ", self.ta.name())?;
838
839        let param = display_assignment(self.parameter_assignment.clone());
840        writeln!(f, "parameters: {param}")?;
841
842        let mut it_transitions = self.transitions.iter();
843        for (n_config, config) in self.configurations.iter().enumerate() {
844            writeln!(f, "Configuration {n_config}: {config}")?;
845            if let Some(transition) = it_transitions.next() {
846                writeln!(f, "        |",)?;
847                writeln!(f, "        |{transition}")?;
848                writeln!(f, "        V")?;
849            }
850        }
851
852        Ok(())
853    }
854}
855
856#[cfg(test)]
857mod test {
858    use crate::{
859        BooleanVarConstraint, LocationConstraint, ParameterConstraint,
860        expressions::{BooleanExpression, ComparisonOp, IntegerExpression, IntegerOp},
861        general_threshold_automaton::{
862            Action,
863            builder::{GeneralThresholdAutomatonBuilder, RuleBuilder},
864        },
865    };
866
867    use super::*;
868
869    lazy_static::lazy_static! {
870        static ref TEST_TA: GeneralThresholdAutomaton = {
871            GeneralThresholdAutomatonBuilder::new("test_ta1")
872            .with_parameters(vec![
873                Parameter::new("n"),
874                Parameter::new("t"),
875                Parameter::new("f"),
876            ])
877            .unwrap()
878            .with_variables(vec![
879                Variable::new("var1"),
880                Variable::new("var2"),
881                Variable::new("var3"),
882            ])
883            .unwrap()
884            .with_locations(vec![
885                Location::new("loc1"),
886                Location::new("loc2"),
887                Location::new("loc3"),
888            ])
889            .unwrap()
890            .initialize()
891            .with_initial_variable_constraints(vec![BooleanVarConstraint::ComparisonExpression(
892                Box::new(IntegerExpression::Atom(Variable::new("var1"))),
893                ComparisonOp::Eq,
894                Box::new(IntegerExpression::Const(1)),
895            )])
896            .unwrap()
897            .with_initial_location_constraints(vec![
898                LocationConstraint::ComparisonExpression(
899                    Box::new(IntegerExpression::Atom(Location::new("loc1"))),
900                    ComparisonOp::Eq,
901                    Box::new(
902                        IntegerExpression::Param(Parameter::new("n"))
903                            - IntegerExpression::Param(Parameter::new("f")),
904                    ),
905                ) & LocationConstraint::ComparisonExpression(
906                    Box::new(IntegerExpression::Atom(Location::new("loc2"))),
907                    ComparisonOp::Eq,
908                    Box::new(IntegerExpression::Const(0)),
909                ),
910            ])
911            .unwrap()
912            .with_resilience_conditions(vec![ParameterConstraint::ComparisonExpression(
913                Box::new(IntegerExpression::Atom(Parameter::new("n"))),
914                ComparisonOp::Gt,
915                Box::new(IntegerExpression::BinaryExpr(
916                    Box::new(IntegerExpression::Const(3)),
917                    IntegerOp::Mul,
918                    Box::new(IntegerExpression::Atom(Parameter::new("f"))),
919                )),
920            )])
921            .unwrap()
922            .with_rules(vec![
923                RuleBuilder::new(0, Location::new("loc1"), Location::new("loc2"))
924                    .with_actions(vec![Action::new(
925                        Variable::new("var1"),
926                        IntegerExpression::Atom(Variable::new("var1")),
927                    )
928                    .unwrap()])
929                    .build(),
930                RuleBuilder::new(1, Location::new("loc2"), Location::new("loc3"))
931                    .with_guard(
932                        BooleanVarConstraint::ComparisonExpression(
933                            Box::new(IntegerExpression::Atom(Variable::new("var1"))),
934                            ComparisonOp::Eq,
935                            Box::new(IntegerExpression::Const(1)),
936                        ) & BooleanVarConstraint::ComparisonExpression(
937                            Box::new(IntegerExpression::Atom(Variable::new("var2"))),
938                            ComparisonOp::Eq,
939                            Box::new(IntegerExpression::Param(Parameter::new("n")) - IntegerExpression::Const(2)),
940                        ),
941                    )
942                    .with_actions(vec![
943                        Action::new(Variable::new("var3"), IntegerExpression::Const(0)).unwrap(),
944                        Action::new(
945                            Variable::new("var1"),
946                            IntegerExpression::BinaryExpr(
947                                Box::new(IntegerExpression::Atom(Variable::new("var1"))),
948                                IntegerOp::Add,
949                                Box::new(IntegerExpression::Const(1)),
950                            ),
951                        )
952                        .unwrap(),
953                    ])
954                    .build(),
955                    RuleBuilder::new(2, Location::new("loc3"), Location::new("loc3"))
956                    .with_guard(
957                        BooleanVarConstraint::True,
958                    )
959                    .with_actions(vec![
960                        Action::new(
961                            Variable::new("var1"),
962                            IntegerExpression::BinaryExpr(
963                                Box::new(IntegerExpression::Atom(Variable::new("var1"))),
964                                IntegerOp::Add,
965                                Box::new(-IntegerExpression::Const(1)),
966                            ),
967                        )
968                        .unwrap(),
969                    ])
970                    .build(),
971            ])
972            .unwrap()
973            .build()
974        };
975    }
976
977    #[test]
978    fn test_configuration_getters() {
979        let config = Configuration {
980            variable_assignment: HashMap::from([
981                (Variable::new("var1"), 1),
982                (Variable::new("var2"), 2),
983            ]),
984
985            location_assignment: HashMap::from([
986                (Location::new("loc1"), 1),
987                (Location::new("loc2"), 2),
988            ]),
989        };
990
991        let got_var_assignment: HashMap<_, _> = config
992            .variable_assignment()
993            .map(|(x, y)| (x.clone(), *y))
994            .collect();
995        assert_eq!(
996            got_var_assignment,
997            HashMap::from([(Variable::new("var1"), 1), (Variable::new("var2"), 2),])
998        );
999
1000        let got_loc_assignment: HashMap<_, _> = config
1001            .location_assignment()
1002            .map(|(x, y)| (x.clone(), *y))
1003            .collect();
1004        assert_eq!(
1005            got_loc_assignment,
1006            HashMap::from([(Location::new("loc1"), 1), (Location::new("loc2"), 2),])
1007        );
1008    }
1009
1010    #[test]
1011    fn test_display_configuration() {
1012        let config = Configuration {
1013            variable_assignment: HashMap::from([
1014                (Variable::new("var1"), 1),
1015                (Variable::new("var2"), 2),
1016            ]),
1017
1018            location_assignment: HashMap::from([
1019                (Location::new("loc1"), 1),
1020                (Location::new("loc2"), 2),
1021            ]),
1022        };
1023
1024        assert_eq!(
1025            format!("{config}"),
1026            "locations: (loc1 : 1, loc2 : 2), variables: (var1 : 1, var2 : 2)"
1027        );
1028    }
1029
1030    #[test]
1031    fn test_display_compact_configuration() {
1032        let config = Configuration {
1033            variable_assignment: HashMap::from([
1034                (Variable::new("var1"), 1),
1035                (Variable::new("var2"), 2),
1036                (Variable::new("var3"), 0),
1037            ]),
1038
1039            location_assignment: HashMap::from([
1040                (Location::new("loc1"), 1),
1041                (Location::new("loc2"), 2),
1042                (Location::new("loc3"), 0),
1043            ]),
1044        };
1045
1046        assert_eq!(
1047            config.display_compact(),
1048            "locations: (loc1 : 1, loc2 : 2), variables: (var1 : 1, var2 : 2)"
1049        );
1050    }
1051
1052    #[test]
1053    fn test_display_rule() {
1054        let rule1 = RuleBuilder::new(1, Location::new("loc1"), Location::new("loc2"))
1055            .with_guard(BooleanExpression::True)
1056            .build();
1057
1058        let transition = Transition {
1059            rule_used: rule1.clone(),
1060            number_applied: 1,
1061        };
1062
1063        assert_eq!(format!("{transition}"), "rule: 1 applied 1 times");
1064    }
1065
1066    #[test]
1067    fn test_display_path() {
1068        let ta = TEST_TA.clone();
1069
1070        let path = Path {
1071            ta: ta.clone(),
1072            parameter_assignment: [(Parameter::new("n"), 4), (Parameter::new("t"), 2)]
1073                .iter()
1074                .cloned()
1075                .collect(),
1076            configurations: vec![
1077                Configuration {
1078                    variable_assignment: [(Variable::new("var1"), 1), (Variable::new("var2"), 2)]
1079                        .into(),
1080                    location_assignment: [(Location::new("loc1"), 3), (Location::new("loc2"), 0)]
1081                        .into(),
1082                },
1083                Configuration {
1084                    variable_assignment: [(Variable::new("var1"), 1), (Variable::new("var2"), 2)]
1085                        .into(),
1086                    location_assignment: [(Location::new("loc1"), 2), (Location::new("loc2"), 1)]
1087                        .into(),
1088                },
1089                Configuration {
1090                    variable_assignment: [(Variable::new("var1"), 2), (Variable::new("var2"), 2)]
1091                        .into(),
1092                    location_assignment: [(Location::new("loc1"), 2), (Location::new("loc3"), 1)]
1093                        .into(),
1094                },
1095            ],
1096            transitions: vec![
1097                Transition {
1098                    rule_used: ta.rules().find(|r| r.id() == 0).unwrap().clone(),
1099                    number_applied: 1,
1100                },
1101                Transition {
1102                    rule_used: ta.rules().find(|r| r.id() == 1).unwrap().clone(),
1103                    number_applied: 1,
1104                },
1105            ],
1106        };
1107
1108        assert_eq!(
1109            format!("{path}"),
1110            "Path of threshold automaton test_ta1: \nparameters: n : 4, t : 2\nConfiguration 0: locations: (loc1 : 3, loc2 : 0), variables: (var1 : 1, var2 : 2)\n        |\n        |rule: 0 applied 1 times\n        V\nConfiguration 1: locations: (loc1 : 2, loc2 : 1), variables: (var1 : 1, var2 : 2)\n        |\n        |rule: 1 applied 1 times\n        V\nConfiguration 2: locations: (loc1 : 2, loc3 : 1), variables: (var1 : 2, var2 : 2)\n"
1111        );
1112    }
1113
1114    #[test]
1115    fn test_display_compact_path() {
1116        let ta = TEST_TA.clone();
1117
1118        let path = Path {
1119            ta: ta.clone(),
1120            parameter_assignment: [(Parameter::new("n"), 5), (Parameter::new("t"), 2)].into(),
1121            configurations: vec![
1122                Configuration {
1123                    variable_assignment: [
1124                        (Variable::new("var0"), 0),
1125                        (Variable::new("var1"), 1),
1126                        (Variable::new("var2"), 2),
1127                    ]
1128                    .into(),
1129                    location_assignment: [
1130                        (Location::new("loc1"), 3),
1131                        (Location::new("loc2"), 1),
1132                        (Location::new("loc3"), 0),
1133                    ]
1134                    .into(),
1135                },
1136                Configuration {
1137                    variable_assignment: [
1138                        (Variable::new("var0"), 0),
1139                        (Variable::new("var1"), 1),
1140                        (Variable::new("var2"), 2),
1141                    ]
1142                    .into(),
1143                    location_assignment: [
1144                        (Location::new("loc1"), 1),
1145                        (Location::new("loc2"), 3),
1146                        (Location::new("loc3"), 0),
1147                    ]
1148                    .into(),
1149                },
1150                Configuration {
1151                    variable_assignment: [
1152                        (Variable::new("var0"), 0),
1153                        (Variable::new("var1"), 2),
1154                        (Variable::new("var2"), 2),
1155                    ]
1156                    .into(),
1157                    location_assignment: [
1158                        (Location::new("loc1"), 2),
1159                        (Location::new("loc2"), 2),
1160                        (Location::new("loc3"), 0),
1161                    ]
1162                    .into(),
1163                },
1164            ],
1165            transitions: vec![
1166                Transition {
1167                    rule_used: ta.rules().find(|r| r.id() == 0).unwrap().clone(),
1168                    number_applied: 1,
1169                },
1170                Transition {
1171                    rule_used: ta.rules().find(|r| r.id() == 1).unwrap().clone(),
1172                    number_applied: 1,
1173                },
1174            ],
1175        };
1176
1177        assert_eq!(
1178            path.display_compact(),
1179            "Path of threshold automaton test_ta1: \nparameters: n : 5, t : 2\nConfiguration 0: locations: (loc1 : 3, loc2 : 1), variables: (var1 : 1, var2 : 2)\n        |\n        |rule: 0 applied 1 times\n        V\nConfiguration 1: locations: (loc1 : 1, loc2 : 3), variables: (var1 : 1, var2 : 2)\n        |\n        |rule: 1 applied 1 times\n        V\nConfiguration 2: locations: (loc1 : 2, loc2 : 2), variables: (var1 : 2, var2 : 2)\n"
1180        );
1181    }
1182
1183    #[test]
1184    fn test_display_transition() {
1185        let rule = RuleBuilder::new(1, Location::new("loc1"), Location::new("loc2"))
1186            .with_guard(BooleanExpression::True)
1187            .build();
1188
1189        let transition = Transition {
1190            rule_used: rule.clone(),
1191            number_applied: 1,
1192        };
1193
1194        assert_eq!(format!("{transition}"), "rule: 1 applied 1 times");
1195    }
1196
1197    #[test]
1198    fn test_path_builder_valid() {
1199        let ta = TEST_TA.clone();
1200
1201        let path = PathBuilder::new(ta.clone())
1202            .add_parameter_assignment([
1203                (Parameter::new("n"), 4),
1204                (Parameter::new("t"), 2),
1205                (Parameter::new("f"), 1),
1206            ])
1207            .unwrap()
1208            .add_configuration(Configuration {
1209                variable_assignment: HashMap::from([
1210                    (Variable::new("var1"), 1),
1211                    (Variable::new("var2"), 2),
1212                    (Variable::new("var3"), 0),
1213                ]),
1214                location_assignment: HashMap::from([
1215                    (Location::new("loc1"), 3),
1216                    (Location::new("loc2"), 0),
1217                    (Location::new("loc3"), 0),
1218                ]),
1219            })
1220            .unwrap()
1221            .add_transition(Transition {
1222                rule_used: ta.rules().find(|r| r.id() == 0).unwrap().clone(),
1223                number_applied: 1,
1224            })
1225            .unwrap()
1226            .add_configuration(Configuration {
1227                variable_assignment: HashMap::from([
1228                    (Variable::new("var1"), 1),
1229                    (Variable::new("var2"), 2),
1230                    (Variable::new("var3"), 0),
1231                ]),
1232                location_assignment: HashMap::from([
1233                    (Location::new("loc1"), 2),
1234                    (Location::new("loc2"), 1),
1235                    (Location::new("loc3"), 0),
1236                ]),
1237            })
1238            .unwrap()
1239            .add_transition(Transition {
1240                rule_used: ta.rules().find(|r| r.id() == 1).unwrap().clone(),
1241                number_applied: 1,
1242            })
1243            .unwrap()
1244            .add_configuration(Configuration {
1245                variable_assignment: HashMap::from([
1246                    (Variable::new("var1"), 2),
1247                    (Variable::new("var2"), 2),
1248                    (Variable::new("var3"), 0),
1249                ]),
1250                location_assignment: HashMap::from([
1251                    (Location::new("loc1"), 2),
1252                    (Location::new("loc2"), 0),
1253                    (Location::new("loc3"), 1),
1254                ]),
1255            })
1256            .unwrap()
1257            .add_transition(Transition {
1258                rule_used: ta.rules().find(|r| r.id() == 2).unwrap().clone(),
1259                number_applied: 1,
1260            })
1261            .unwrap()
1262            .add_configuration(Configuration {
1263                variable_assignment: HashMap::from([
1264                    (Variable::new("var1"), 1),
1265                    (Variable::new("var2"), 2),
1266                    (Variable::new("var3"), 0),
1267                ]),
1268                location_assignment: HashMap::from([
1269                    (Location::new("loc1"), 2),
1270                    (Location::new("loc2"), 0),
1271                    (Location::new("loc3"), 1),
1272                ]),
1273            })
1274            .unwrap()
1275            .build()
1276            .unwrap();
1277
1278        assert_eq!(path.configurations.len(), 4);
1279        assert_eq!(path.transitions.len(), 3);
1280
1281        assert_eq!(
1282            path.configurations[0].variable_assignment,
1283            HashMap::from([
1284                (Variable::new("var1"), 1),
1285                (Variable::new("var2"), 2),
1286                (Variable::new("var3"), 0),
1287            ])
1288        );
1289        assert_eq!(
1290            path.configurations[0].location_assignment,
1291            HashMap::from([
1292                (Location::new("loc1"), 3),
1293                (Location::new("loc2"), 0),
1294                (Location::new("loc3"), 0),
1295            ])
1296        );
1297
1298        assert_eq!(
1299            path.configurations[1].variable_assignment,
1300            HashMap::from([
1301                (Variable::new("var1"), 1),
1302                (Variable::new("var2"), 2),
1303                (Variable::new("var3"), 0),
1304            ])
1305        );
1306
1307        assert_eq!(
1308            path.configurations[1].location_assignment,
1309            HashMap::from([
1310                (Location::new("loc1"), 2),
1311                (Location::new("loc2"), 1),
1312                (Location::new("loc3"), 0),
1313            ])
1314        );
1315
1316        assert_eq!(
1317            path.configurations[2].variable_assignment,
1318            HashMap::from([
1319                (Variable::new("var1"), 2),
1320                (Variable::new("var2"), 2),
1321                (Variable::new("var3"), 0),
1322            ])
1323        );
1324        assert_eq!(
1325            path.configurations[2].location_assignment,
1326            HashMap::from([
1327                (Location::new("loc1"), 2),
1328                (Location::new("loc2"), 0),
1329                (Location::new("loc3"), 1),
1330            ])
1331        );
1332
1333        assert_eq!(
1334            path.configurations[3].variable_assignment,
1335            HashMap::from([
1336                (Variable::new("var1"), 1),
1337                (Variable::new("var2"), 2),
1338                (Variable::new("var3"), 0),
1339            ])
1340        );
1341        assert_eq!(
1342            path.configurations[3].location_assignment,
1343            HashMap::from([
1344                (Location::new("loc1"), 2),
1345                (Location::new("loc2"), 0),
1346                (Location::new("loc3"), 1),
1347            ])
1348        );
1349    }
1350
1351    #[test]
1352    fn test_empty_path_builder() {
1353        let ta = TEST_TA.clone();
1354
1355        let path = PathBuilder::new(ta.clone())
1356            .add_parameter_assignment([
1357                (Parameter::new("n"), 4),
1358                (Parameter::new("t"), 2),
1359                (Parameter::new("f"), 1),
1360            ])
1361            .unwrap()
1362            .build()
1363            .unwrap();
1364
1365        assert_eq!(path.configurations.len(), 0);
1366        assert_eq!(path.transitions.len(), 0);
1367    }
1368
1369    #[test]
1370    fn test_path_builder_err_inconsistent_rc() {
1371        let ta = TEST_TA.clone();
1372
1373        let path = PathBuilder::new(ta.clone()).add_parameter_assignment(HashMap::from([
1374            (Parameter::new("n"), 3),
1375            (Parameter::new("t"), 2),
1376            (Parameter::new("f"), 1),
1377        ]));
1378
1379        assert!(path.is_err());
1380        let err = path.unwrap_err();
1381        assert!(
1382            matches!(err, PathBuilderError::ResilienceConditionNotSatisfied(_, _)),
1383            "got error instead {err}"
1384        );
1385        assert!(err.to_string().contains("Resilience condition"));
1386        assert!(
1387            err.to_string().contains(
1388                &ParameterConstraint::ComparisonExpression(
1389                    Box::new(IntegerExpression::Atom(Parameter::new("n"))),
1390                    ComparisonOp::Gt,
1391                    Box::new(IntegerExpression::BinaryExpr(
1392                        Box::new(IntegerExpression::Const(3)),
1393                        IntegerOp::Mul,
1394                        Box::new(IntegerExpression::Atom(Parameter::new("f"))),
1395                    )),
1396                )
1397                .to_string()
1398            )
1399        );
1400    }
1401
1402    #[test]
1403    fn test_path_builder_err_missing_param() {
1404        let ta = TEST_TA.clone();
1405
1406        // parameter missing from assignment
1407        let path = PathBuilder::new(ta.clone()).add_parameter_assignment(HashMap::from([
1408            (Parameter::new("n"), 4),
1409            (Parameter::new("t"), 2),
1410        ]));
1411
1412        assert!(path.is_err());
1413        let err = path.unwrap_err();
1414        assert!(
1415            matches!(err, PathBuilderError::MissingParameter(_)),
1416            "got error instead {err}"
1417        );
1418        assert!(err.to_string().contains("missing parameter"));
1419        assert!(err.to_string().contains(&Parameter::new("f").to_string()));
1420    }
1421
1422    #[test]
1423    fn test_path_builder_err_init_loc_constr() {
1424        let ta = TEST_TA.clone();
1425
1426        // initial location constraint not satisfied
1427        let path = PathBuilder::new(ta.clone())
1428            .add_parameter_assignment(HashMap::from([
1429                (Parameter::new("n"), 4),
1430                (Parameter::new("t"), 2),
1431                (Parameter::new("f"), 1),
1432            ]))
1433            .unwrap()
1434            .add_configuration(Configuration {
1435                variable_assignment: HashMap::from([
1436                    (Variable::new("var1"), 1),
1437                    (Variable::new("var2"), 2),
1438                    (Variable::new("var3"), 0),
1439                ]),
1440                location_assignment: HashMap::from([
1441                    (Location::new("loc1"), 1),
1442                    (Location::new("loc2"), 0),
1443                    (Location::new("loc3"), 0),
1444                ]),
1445            });
1446
1447        assert!(path.is_err());
1448        let err = path.unwrap_err();
1449        assert!(
1450            matches!(
1451                err,
1452                PathBuilderError::InitialLocationConstraintNotSatisfied(_, _)
1453            ),
1454            "got error instead {err}"
1455        );
1456        assert!(err.to_string().contains("Initial location constraint"));
1457        assert!(
1458            err.to_string().contains(
1459                &LocationConstraint::ComparisonExpression(
1460                    Box::new(IntegerExpression::Atom(Location::new("loc1"))),
1461                    ComparisonOp::Eq,
1462                    Box::new(
1463                        IntegerExpression::Param(Parameter::new("n"))
1464                            - IntegerExpression::Param(Parameter::new("f")),
1465                    ),
1466                )
1467                .to_string()
1468            )
1469        );
1470    }
1471
1472    #[test]
1473    fn test_path_builder_err_loc_missing() {
1474        let ta = TEST_TA.clone();
1475
1476        // location missing from assignment
1477        let path = PathBuilder::new(ta.clone())
1478            .add_parameter_assignment(HashMap::from([
1479                (Parameter::new("n"), 4),
1480                (Parameter::new("t"), 2),
1481                (Parameter::new("f"), 1),
1482            ]))
1483            .unwrap()
1484            .add_configuration(Configuration {
1485                variable_assignment: HashMap::from([
1486                    (Variable::new("var1"), 1),
1487                    (Variable::new("var2"), 2),
1488                    (Variable::new("var3"), 0),
1489                ]),
1490                location_assignment: HashMap::from([
1491                    (Location::new("loc1"), 2),
1492                    (Location::new("loc3"), 0),
1493                ]),
1494            });
1495
1496        assert!(path.is_err());
1497        let err = path.unwrap_err();
1498        assert!(
1499            matches!(err, PathBuilderError::MissingLocation(_)),
1500            "got error instead {err}"
1501        );
1502        assert!(err.to_string().contains("missing location"));
1503        assert!(err.to_string().contains(&Location::new("loc2").to_string()));
1504    }
1505
1506    #[test]
1507    fn test_path_builder_err_init_var_constr() {
1508        let ta = TEST_TA.clone();
1509        // initial variable constraint not satisfied
1510        let path = PathBuilder::new(ta.clone())
1511            .add_parameter_assignment(HashMap::from([
1512                (Parameter::new("n"), 4),
1513                (Parameter::new("t"), 2),
1514                (Parameter::new("f"), 1),
1515            ]))
1516            .unwrap()
1517            .add_configuration(Configuration {
1518                variable_assignment: HashMap::from([
1519                    (Variable::new("var1"), 2),
1520                    (Variable::new("var2"), 2),
1521                    (Variable::new("var3"), 0),
1522                ]),
1523                location_assignment: HashMap::from([
1524                    (Location::new("loc1"), 3),
1525                    (Location::new("loc2"), 0),
1526                    (Location::new("loc3"), 1),
1527                ]),
1528            });
1529
1530        assert!(path.is_err());
1531        let err = path.unwrap_err();
1532        assert!(
1533            matches!(
1534                err,
1535                PathBuilderError::InitialVariableConstraintNotSatisfied(_, _)
1536            ),
1537            "got error instead {err}"
1538        );
1539        assert!(err.to_string().contains("Initial variable constraint"));
1540    }
1541
1542    #[test]
1543    fn test_path_builder_err_var_missing() {
1544        let ta = TEST_TA.clone();
1545        // variable missing from assignment
1546        let path = PathBuilder::new(ta.clone())
1547            .add_parameter_assignment(HashMap::from([
1548                (Parameter::new("n"), 4),
1549                (Parameter::new("t"), 2),
1550                (Parameter::new("f"), 1),
1551            ]))
1552            .unwrap()
1553            .add_configuration(Configuration {
1554                variable_assignment: HashMap::from([
1555                    (Variable::new("var1"), 1),
1556                    (Variable::new("var2"), 2),
1557                ]),
1558                location_assignment: HashMap::from([
1559                    (Location::new("loc1"), 2),
1560                    (Location::new("loc2"), 0),
1561                    (Location::new("loc3"), 1),
1562                ]),
1563            });
1564
1565        assert!(path.is_err());
1566        let err = path.unwrap_err();
1567        assert!(
1568            matches!(err, PathBuilderError::MissingVariable(_)),
1569            "got error instead {err}"
1570        );
1571        assert!(err.to_string().contains("missing variable"));
1572        assert!(err.to_string().contains(&Variable::new("var3").to_string()));
1573    }
1574
1575    #[test]
1576    fn test_path_builder_err_inconsistent_n_proc() {
1577        let ta = TEST_TA.clone();
1578        // inconsistent number of processes
1579        let path = PathBuilder::new(ta.clone())
1580            .add_parameter_assignment(HashMap::from([
1581                (Parameter::new("n"), 4),
1582                (Parameter::new("t"), 2),
1583                (Parameter::new("f"), 1),
1584            ]))
1585            .unwrap()
1586            .add_configuration(Configuration {
1587                variable_assignment: HashMap::from([
1588                    (Variable::new("var1"), 1),
1589                    (Variable::new("var2"), 2),
1590                    (Variable::new("var3"), 0),
1591                ]),
1592                location_assignment: HashMap::from([
1593                    (Location::new("loc1"), 3),
1594                    (Location::new("loc2"), 0),
1595                    (Location::new("loc3"), 0),
1596                ]),
1597            })
1598            .unwrap()
1599            .add_transition(Transition {
1600                rule_used: ta.rules().find(|r| r.id() == 0).unwrap().clone(),
1601                number_applied: 1,
1602            })
1603            .unwrap()
1604            .add_configuration(Configuration {
1605                variable_assignment: HashMap::from([
1606                    (Variable::new("var1"), 1),
1607                    (Variable::new("var2"), 2),
1608                    (Variable::new("var3"), 0),
1609                ]),
1610                location_assignment: HashMap::from([
1611                    (Location::new("loc1"), 2),
1612                    (Location::new("loc2"), 5),
1613                    (Location::new("loc3"), 0),
1614                ]),
1615            });
1616
1617        assert!(path.is_err());
1618        let err = path.unwrap_err();
1619        assert!(
1620            matches!(err, PathBuilderError::InconsistentNumberOfProcesses { .. }),
1621            "got error instead {err}"
1622        );
1623        assert!(err.to_string().contains("Inconsistent number of processes"));
1624        assert!(err.to_string().contains("expected 3 but got 7"));
1625    }
1626
1627    #[test]
1628    fn test_path_builder_err_not_enough_proc() {
1629        let ta = TEST_TA.clone();
1630        // inconsistent number of processes
1631        let path = PathBuilder::new(ta.clone())
1632            .add_parameter_assignment(HashMap::from([
1633                (Parameter::new("n"), 4),
1634                (Parameter::new("t"), 2),
1635                (Parameter::new("f"), 1),
1636            ]))
1637            .unwrap()
1638            .add_configuration(Configuration {
1639                variable_assignment: HashMap::from([
1640                    (Variable::new("var1"), 1),
1641                    (Variable::new("var2"), 2),
1642                    (Variable::new("var3"), 0),
1643                ]),
1644                location_assignment: HashMap::from([
1645                    (Location::new("loc1"), 3),
1646                    (Location::new("loc2"), 0),
1647                    (Location::new("loc3"), 0),
1648                ]),
1649            })
1650            .unwrap()
1651            .add_transition(Transition {
1652                rule_used: ta.rules().find(|r| r.id() == 0).unwrap().clone(),
1653                number_applied: 4,
1654            })
1655            .unwrap()
1656            .add_configuration(Configuration {
1657                variable_assignment: HashMap::from([
1658                    (Variable::new("var1"), 0),
1659                    (Variable::new("var2"), 4),
1660                    (Variable::new("var3"), 0),
1661                ]),
1662                location_assignment: HashMap::from([
1663                    (Location::new("loc1"), 0),
1664                    (Location::new("loc2"), 3),
1665                    (Location::new("loc3"), 0),
1666                ]),
1667            })
1668            .unwrap()
1669            .build();
1670
1671        assert!(path.is_err());
1672        let err = path.unwrap_err();
1673        assert!(
1674            matches!(err, PathBuilderError::NotEnoughProcesses(_, _)),
1675            "got error instead {err}"
1676        );
1677    }
1678
1679    #[test]
1680    fn test_path_guard_not_sat() {
1681        let ta = TEST_TA.clone();
1682        // inconsistent number of processes
1683        let path = PathBuilder::new(ta.clone())
1684            .add_parameter_assignment(HashMap::from([
1685                (Parameter::new("n"), 4),
1686                (Parameter::new("t"), 2),
1687                (Parameter::new("f"), 1),
1688            ]))
1689            .unwrap()
1690            .add_configuration(Configuration {
1691                variable_assignment: HashMap::from([
1692                    (Variable::new("var1"), 1),
1693                    (Variable::new("var2"), 2),
1694                    (Variable::new("var3"), 0),
1695                ]),
1696                location_assignment: HashMap::from([
1697                    (Location::new("loc1"), 3),
1698                    (Location::new("loc2"), 0),
1699                    (Location::new("loc3"), 0),
1700                ]),
1701            })
1702            .unwrap()
1703            .add_transition(Transition {
1704                rule_used: ta.rules().find(|r| r.id() == 0).unwrap().clone(),
1705                number_applied: 2,
1706            })
1707            .unwrap()
1708            .add_configuration(Configuration {
1709                variable_assignment: HashMap::from([
1710                    (Variable::new("var1"), 1),
1711                    (Variable::new("var2"), 2),
1712                    (Variable::new("var3"), 0),
1713                ]),
1714                location_assignment: HashMap::from([
1715                    (Location::new("loc1"), 1),
1716                    (Location::new("loc2"), 2),
1717                    (Location::new("loc3"), 0),
1718                ]),
1719            })
1720            .unwrap()
1721            .add_transition(Transition {
1722                rule_used: ta.rules().find(|r| r.id() == 1).unwrap().clone(),
1723                number_applied: 2,
1724            })
1725            .unwrap()
1726            .add_configuration(Configuration {
1727                variable_assignment: HashMap::from([
1728                    (Variable::new("var1"), 3),
1729                    (Variable::new("var2"), 2),
1730                    (Variable::new("var3"), 0),
1731                ]),
1732                location_assignment: HashMap::from([
1733                    (Location::new("loc1"), 1),
1734                    (Location::new("loc2"), 1),
1735                    (Location::new("loc3"), 1),
1736                ]),
1737            })
1738            .unwrap()
1739            .build();
1740
1741        assert!(path.is_err());
1742        let err = path.unwrap_err();
1743        assert!(
1744            matches!(err, PathBuilderError::GuardNotSatisfied(_, _)),
1745            "got error instead {err}"
1746        );
1747    }
1748
1749    #[test]
1750    fn test_builder_err_unknown_rule() {
1751        let ta = TEST_TA.clone();
1752        // unknown rule
1753        let path = PathBuilder::new(ta.clone())
1754            .add_parameter_assignment(HashMap::from([
1755                (Parameter::new("n"), 4),
1756                (Parameter::new("t"), 2),
1757                (Parameter::new("f"), 1),
1758            ]))
1759            .unwrap()
1760            .add_configuration(Configuration {
1761                variable_assignment: HashMap::from([
1762                    (Variable::new("var1"), 1),
1763                    (Variable::new("var2"), 2),
1764                    (Variable::new("var3"), 0),
1765                ]),
1766                location_assignment: HashMap::from([
1767                    (Location::new("loc1"), 3),
1768                    (Location::new("loc2"), 0),
1769                    (Location::new("loc3"), 0),
1770                ]),
1771            })
1772            .unwrap()
1773            .add_transition(Transition {
1774                rule_used: RuleBuilder::new(42, Location::new("loc1"), Location::new("loc2"))
1775                    .build(),
1776                number_applied: 1,
1777            });
1778
1779        assert!(path.is_err());
1780        let err = path.unwrap_err();
1781        assert!(
1782            matches!(err, PathBuilderError::UnknownRule(_)),
1783            "got error instead {err}"
1784        );
1785        assert!(err.to_string().contains("Unknown rule"));
1786        assert!(
1787            err.to_string().contains(
1788                &RuleBuilder::new(42, Location::new("loc1"), Location::new("loc2"))
1789                    .build()
1790                    .to_string()
1791            )
1792        );
1793    }
1794
1795    #[test]
1796    fn test_builder_err_inconsistent_number_cfgs() {
1797        let ta = TEST_TA.clone();
1798
1799        let path = PathBuilder::new(ta.clone())
1800            .add_parameter_assignment(HashMap::from([
1801                (Parameter::new("n"), 4),
1802                (Parameter::new("t"), 2),
1803                (Parameter::new("f"), 1),
1804            ]))
1805            .unwrap()
1806            .add_configuration(Configuration {
1807                variable_assignment: HashMap::from([
1808                    (Variable::new("var1"), 1),
1809                    (Variable::new("var2"), 2),
1810                    (Variable::new("var3"), 0),
1811                ]),
1812                location_assignment: HashMap::from([
1813                    (Location::new("loc1"), 3),
1814                    (Location::new("loc2"), 0),
1815                    (Location::new("loc3"), 0),
1816                ]),
1817            })
1818            .unwrap()
1819            .add_configuration(Configuration {
1820                variable_assignment: HashMap::from([
1821                    (Variable::new("var1"), 1),
1822                    (Variable::new("var2"), 2),
1823                    (Variable::new("var3"), 0),
1824                ]),
1825                location_assignment: HashMap::from([
1826                    (Location::new("loc1"), 1),
1827                    (Location::new("loc2"), 2),
1828                    (Location::new("loc3"), 0),
1829                ]),
1830            })
1831            .unwrap()
1832            .build();
1833
1834        assert!(path.is_err());
1835        let err = path.unwrap_err();
1836        assert!(
1837            matches!(err, PathBuilderError::InconsistentNumberOfConfigurations),
1838            "got error instead {err}"
1839        );
1840        assert!(
1841            err.to_string()
1842                .contains("Inconsistent number of configurations")
1843        );
1844    }
1845
1846    #[test]
1847    fn test_builder_err_inconsistent_transition() {
1848        let ta = TEST_TA.clone();
1849
1850        let path = PathBuilder::new(ta.clone())
1851            .add_parameter_assignment(HashMap::from([
1852                (Parameter::new("n"), 4),
1853                (Parameter::new("t"), 2),
1854                (Parameter::new("f"), 1),
1855            ]))
1856            .unwrap()
1857            .add_configuration(Configuration {
1858                variable_assignment: HashMap::from([
1859                    (Variable::new("var1"), 1),
1860                    (Variable::new("var2"), 2),
1861                    (Variable::new("var3"), 0),
1862                ]),
1863                location_assignment: HashMap::from([
1864                    (Location::new("loc1"), 3),
1865                    (Location::new("loc2"), 0),
1866                    (Location::new("loc3"), 0),
1867                ]),
1868            })
1869            .unwrap()
1870            .add_transition(Transition {
1871                rule_used: ta.rules().find(|r| r.id() == 0).unwrap().clone(),
1872                number_applied: 1,
1873            })
1874            .unwrap()
1875            .add_configuration(Configuration {
1876                variable_assignment: HashMap::from([
1877                    (Variable::new("var1"), 1),
1878                    (Variable::new("var2"), 2),
1879                    (Variable::new("var3"), 0),
1880                ]),
1881                location_assignment: HashMap::from([
1882                    (Location::new("loc1"), 2),
1883                    (Location::new("loc2"), 0),
1884                    (Location::new("loc3"), 1),
1885                ]),
1886            })
1887            .unwrap()
1888            .build();
1889
1890        assert!(
1891            path.is_err(),
1892            "expected error because of inconsistent transition"
1893        );
1894        let err = path.unwrap_err();
1895        assert!(
1896            matches!(err, PathBuilderError::InconsistentTransition { .. }),
1897            "got error instead {err}"
1898        );
1899        assert!(err.to_string().contains("Inconsistent transition"));
1900
1901        assert!(
1902            err.to_string().contains(
1903                &Configuration {
1904                    variable_assignment: HashMap::from([
1905                        (Variable::new("var1"), 1),
1906                        (Variable::new("var2"), 2),
1907                        (Variable::new("var3"), 0),
1908                    ]),
1909                    location_assignment: HashMap::from([
1910                        (Location::new("loc1"), 2),
1911                        (Location::new("loc2"), 0),
1912                        (Location::new("loc3"), 1),
1913                    ]),
1914                }
1915                .to_string()
1916            )
1917        );
1918
1919        assert!(
1920            err.to_string().contains(
1921                &Transition {
1922                    rule_used: ta.rules().find(|r| r.id() == 0).unwrap().clone(),
1923                    number_applied: 1,
1924                }
1925                .to_string()
1926            )
1927        );
1928
1929        assert!(
1930            err.to_string().contains(
1931                &Configuration {
1932                    variable_assignment: HashMap::from([
1933                        (Variable::new("var1"), 1),
1934                        (Variable::new("var2"), 2),
1935                        (Variable::new("var3"), 0),
1936                    ]),
1937                    location_assignment: HashMap::from([
1938                        (Location::new("loc1"), 3),
1939                        (Location::new("loc2"), 0),
1940                        (Location::new("loc3"), 0),
1941                    ]),
1942                }
1943                .to_string()
1944            )
1945        );
1946    }
1947
1948    #[test]
1949    fn test_builder_err_not_enough_processes() {
1950        let ta = TEST_TA.clone();
1951
1952        let path = PathBuilder::new(ta.clone())
1953            .add_parameter_assignment(HashMap::from([
1954                (Parameter::new("n"), 4),
1955                (Parameter::new("t"), 2),
1956                (Parameter::new("f"), 1),
1957            ]))
1958            .unwrap()
1959            .add_configuration(Configuration {
1960                variable_assignment: HashMap::from([
1961                    (Variable::new("var1"), 1),
1962                    (Variable::new("var2"), 2),
1963                    (Variable::new("var3"), 0),
1964                ]),
1965                location_assignment: HashMap::from([
1966                    (Location::new("loc1"), 3),
1967                    (Location::new("loc2"), 0),
1968                    (Location::new("loc3"), 0),
1969                ]),
1970            })
1971            .unwrap()
1972            .add_transition(Transition {
1973                rule_used: ta.rules().find(|r| r.id() == 1).unwrap().clone(),
1974                number_applied: 1,
1975            })
1976            .unwrap()
1977            .add_configuration(Configuration {
1978                variable_assignment: HashMap::from([
1979                    (Variable::new("var1"), 1),
1980                    (Variable::new("var2"), 2),
1981                    (Variable::new("var3"), 0),
1982                ]),
1983                location_assignment: HashMap::from([
1984                    (Location::new("loc1"), 2),
1985                    (Location::new("loc2"), 0),
1986                    (Location::new("loc3"), 1),
1987                ]),
1988            })
1989            .unwrap()
1990            .build();
1991
1992        assert!(
1993            path.is_err(),
1994            "expected error because not enough processes to apply rule"
1995        );
1996        let err = path.unwrap_err();
1997        assert!(
1998            matches!(err, PathBuilderError::NotEnoughProcesses(_, _)),
1999            "got error instead {err}"
2000        );
2001        assert!(err.to_string().contains("not enough processes"));
2002    }
2003
2004    #[test]
2005    fn test_builder_err_not_enough_processes_multi_apply() {
2006        let ta = TEST_TA.clone();
2007
2008        let path = PathBuilder::new(ta.clone())
2009            .add_parameter_assignment(HashMap::from([
2010                (Parameter::new("n"), 5),
2011                (Parameter::new("t"), 2),
2012                (Parameter::new("f"), 1),
2013            ]))
2014            .unwrap()
2015            .add_configuration(Configuration {
2016                variable_assignment: HashMap::from([
2017                    (Variable::new("var1"), 1),
2018                    (Variable::new("var2"), 2),
2019                    (Variable::new("var3"), 0),
2020                ]),
2021                location_assignment: HashMap::from([
2022                    (Location::new("loc1"), 4),
2023                    (Location::new("loc2"), 0),
2024                    (Location::new("loc3"), 0),
2025                ]),
2026            })
2027            .unwrap()
2028            .add_transition(Transition {
2029                rule_used: ta.rules().find(|r| r.id() == 0).unwrap().clone(),
2030                number_applied: 5,
2031            })
2032            .unwrap()
2033            .add_configuration(Configuration {
2034                variable_assignment: HashMap::from([
2035                    (Variable::new("var1"), 1),
2036                    (Variable::new("var2"), 2),
2037                    (Variable::new("var3"), 0),
2038                ]),
2039                location_assignment: HashMap::from([
2040                    (Location::new("loc1"), 0),
2041                    (Location::new("loc2"), 4),
2042                    (Location::new("loc3"), 0),
2043                ]),
2044            })
2045            .unwrap()
2046            .build();
2047
2048        assert!(
2049            path.is_err(),
2050            "expected error because not enough processes to apply rule"
2051        );
2052        let err = path.unwrap_err();
2053        assert!(
2054            matches!(err, PathBuilderError::NotEnoughProcesses(_, _)),
2055            "got error instead {err}"
2056        );
2057        assert!(err.to_string().contains("not enough processes"));
2058    }
2059
2060    #[test]
2061    fn test_builder_error_guard_not_satisfied() {
2062        let ta = TEST_TA.clone();
2063
2064        let path = PathBuilder::new(ta.clone())
2065            .add_parameter_assignment([
2066                (Parameter::new("n"), 4),
2067                (Parameter::new("t"), 2),
2068                (Parameter::new("f"), 1),
2069            ])
2070            .unwrap()
2071            .add_configurations(vec![
2072                Configuration {
2073                    variable_assignment: HashMap::from([
2074                        (Variable::new("var1"), 1),
2075                        (Variable::new("var2"), 5),
2076                        (Variable::new("var3"), 0),
2077                    ]),
2078                    location_assignment: HashMap::from([
2079                        (Location::new("loc1"), 3),
2080                        (Location::new("loc2"), 0),
2081                        (Location::new("loc3"), 0),
2082                    ]),
2083                },
2084                Configuration {
2085                    variable_assignment: HashMap::from([
2086                        (Variable::new("var1"), 1),
2087                        (Variable::new("var2"), 5),
2088                        (Variable::new("var3"), 0),
2089                    ]),
2090                    location_assignment: HashMap::from([
2091                        (Location::new("loc1"), 2),
2092                        (Location::new("loc2"), 1),
2093                        (Location::new("loc3"), 0),
2094                    ]),
2095                },
2096                Configuration {
2097                    variable_assignment: HashMap::from([
2098                        (Variable::new("var1"), 2),
2099                        (Variable::new("var2"), 5),
2100                        (Variable::new("var3"), 0),
2101                    ]),
2102                    location_assignment: HashMap::from([
2103                        (Location::new("loc1"), 2),
2104                        (Location::new("loc2"), 0),
2105                        (Location::new("loc3"), 1),
2106                    ]),
2107                },
2108            ])
2109            .unwrap()
2110            .add_transitions(vec![
2111                Transition {
2112                    rule_used: ta.rules().find(|r| r.id() == 0).unwrap().clone(),
2113                    number_applied: 1,
2114                },
2115                Transition {
2116                    rule_used: ta.rules().find(|r| r.id() == 1).unwrap().clone(),
2117                    number_applied: 1,
2118                },
2119            ])
2120            .unwrap()
2121            .build();
2122
2123        assert!(
2124            path.is_err(),
2125            "expected error because guard of rule not satisfied"
2126        );
2127        let err = path.unwrap_err();
2128        assert!(
2129            matches!(err, PathBuilderError::GuardNotSatisfied(_, _)),
2130            "got error instead {err}"
2131        );
2132        assert!(err.to_string().contains("guard is not satisfied"));
2133        assert!(
2134            err.to_string().contains(
2135                &ta.rules()
2136                    .find(|r| r.id() == 1)
2137                    .unwrap()
2138                    .clone()
2139                    .to_string()
2140            )
2141        );
2142    }
2143
2144    #[test]
2145    fn test_from_evaluation_error_location() {
2146        let err = EvaluationError::AtomicNotFound(Location::new("loc1"));
2147        let path_err: PathBuilderError = err.into();
2148        assert!(matches!(path_err, PathBuilderError::MissingLocation(_)));
2149
2150        let err: EvaluationError<Location> =
2151            EvaluationError::ParameterNotFound(Parameter::new("param1"));
2152        let path_err: PathBuilderError = err.into();
2153        assert!(matches!(path_err, PathBuilderError::MissingParameter(_)));
2154    }
2155
2156    #[test]
2157    fn test_from_evaluation_error_variable() {
2158        let err = EvaluationError::AtomicNotFound(Variable::new("var1"));
2159        let path_err: PathBuilderError = err.into();
2160        assert!(matches!(path_err, PathBuilderError::MissingVariable(_)));
2161
2162        let err: EvaluationError<Variable> =
2163            EvaluationError::ParameterNotFound(Parameter::new("param1"));
2164        let path_err: PathBuilderError = err.into();
2165        assert!(matches!(path_err, PathBuilderError::MissingParameter(_)));
2166    }
2167
2168    #[test]
2169    fn test_apply_rule() {
2170        let cfg = Configuration {
2171            variable_assignment: HashMap::from([]),
2172            location_assignment: HashMap::from([
2173                (Location::new("loc1"), 2),
2174                (Location::new("loc2"), 0),
2175                (Location::new("loc3"), 1),
2176            ]),
2177        };
2178
2179        let rule = RuleBuilder::new(1, Location::new("loc1"), Location::new("loc2"))
2180            .with_guard(BooleanExpression::True)
2181            .build();
2182
2183        let trans = Transition {
2184            rule_used: rule,
2185            number_applied: 1,
2186        };
2187
2188        let expected_cfg = Configuration {
2189            variable_assignment: HashMap::from([]),
2190            location_assignment: HashMap::from([
2191                (Location::new("loc1"), 1),
2192                (Location::new("loc2"), 1),
2193                (Location::new("loc3"), 1),
2194            ]),
2195        };
2196
2197        assert_eq!(cfg.apply_rule(&trans), Some(expected_cfg));
2198    }
2199
2200    #[test]
2201    fn test_apply_rule_self_loop_multi() {
2202        let cfg = Configuration {
2203            variable_assignment: HashMap::from([]),
2204            location_assignment: HashMap::from([
2205                (Location::new("loc1"), 2),
2206                (Location::new("loc2"), 0),
2207                (Location::new("loc3"), 1),
2208            ]),
2209        };
2210
2211        let rule = RuleBuilder::new(1, Location::new("loc1"), Location::new("loc1"))
2212            .with_guard(BooleanExpression::True)
2213            .build();
2214
2215        let trans = Transition {
2216            rule_used: rule,
2217            number_applied: 5,
2218        };
2219
2220        let expected_cfg = Configuration {
2221            variable_assignment: HashMap::from([]),
2222            location_assignment: HashMap::from([
2223                (Location::new("loc1"), 2),
2224                (Location::new("loc2"), 0),
2225                (Location::new("loc3"), 1),
2226            ]),
2227        };
2228
2229        assert_eq!(cfg.apply_rule(&trans), Some(expected_cfg));
2230    }
2231
2232    #[test]
2233    fn test_apply_rule_self_loop_no_proc() {
2234        let cfg = Configuration {
2235            variable_assignment: HashMap::from([]),
2236            location_assignment: HashMap::from([
2237                (Location::new("loc1"), 2),
2238                (Location::new("loc2"), 0),
2239                (Location::new("loc3"), 1),
2240            ]),
2241        };
2242
2243        let rule = RuleBuilder::new(1, Location::new("loc2"), Location::new("loc2"))
2244            .with_guard(BooleanExpression::True)
2245            .build();
2246
2247        let trans = Transition {
2248            rule_used: rule,
2249            number_applied: 1,
2250        };
2251
2252        assert_eq!(cfg.apply_rule(&trans), None);
2253    }
2254
2255    #[test]
2256    fn test_apply_rule_not_enough_processes() {
2257        let cfg = Configuration {
2258            variable_assignment: HashMap::from([]),
2259            location_assignment: HashMap::from([
2260                (Location::new("loc1"), 2),
2261                (Location::new("loc2"), 0),
2262                (Location::new("loc3"), 1),
2263            ]),
2264        };
2265
2266        let rule = RuleBuilder::new(1, Location::new("loc1"), Location::new("loc2"))
2267            .with_guard(BooleanExpression::True)
2268            .build();
2269
2270        let trans = Transition {
2271            rule_used: rule,
2272            number_applied: 3,
2273        };
2274
2275        assert_eq!(cfg.apply_rule(&trans), None);
2276    }
2277
2278    #[test]
2279    fn test_shorten_path() {
2280        let ta = TEST_TA.clone();
2281
2282        let path = PathBuilder::new(ta.clone())
2283            .add_parameter_assignment([
2284                (Parameter::new("n"), 4),
2285                (Parameter::new("t"), 2),
2286                (Parameter::new("f"), 1),
2287            ])
2288            .unwrap()
2289            .add_configurations(vec![
2290                Configuration {
2291                    variable_assignment: HashMap::from([
2292                        (Variable::new("var1"), 1),
2293                        (Variable::new("var2"), 0),
2294                        (Variable::new("var3"), 0),
2295                    ]),
2296                    location_assignment: HashMap::from([
2297                        (Location::new("loc1"), 3),
2298                        (Location::new("loc2"), 0),
2299                        (Location::new("loc3"), 0),
2300                    ]),
2301                },
2302                Configuration {
2303                    variable_assignment: HashMap::from([
2304                        (Variable::new("var1"), 1),
2305                        (Variable::new("var2"), 0),
2306                        (Variable::new("var3"), 0),
2307                    ]),
2308                    location_assignment: HashMap::from([
2309                        (Location::new("loc1"), 2),
2310                        (Location::new("loc2"), 1),
2311                        (Location::new("loc3"), 0),
2312                    ]),
2313                },
2314                Configuration {
2315                    variable_assignment: HashMap::from([
2316                        (Variable::new("var1"), 1),
2317                        (Variable::new("var2"), 0),
2318                        (Variable::new("var3"), 0),
2319                    ]),
2320                    location_assignment: HashMap::from([
2321                        (Location::new("loc1"), 1),
2322                        (Location::new("loc2"), 2),
2323                        (Location::new("loc3"), 0),
2324                    ]),
2325                },
2326            ])
2327            .unwrap()
2328            .add_transitions(vec![
2329                Transition {
2330                    rule_used: ta.rules().find(|r| r.id() == 0).unwrap().clone(),
2331                    number_applied: 1,
2332                },
2333                Transition {
2334                    rule_used: ta.rules().find(|r| r.id() == 0).unwrap().clone(),
2335                    number_applied: 1,
2336                },
2337            ])
2338            .unwrap()
2339            .build();
2340
2341        assert!(
2342            path.is_ok(),
2343            "Expected path to be valid, got error {}",
2344            path.unwrap_err()
2345        );
2346
2347        let got_path = path.unwrap();
2348
2349        let expected_path = Path {
2350            ta: ta.clone(),
2351            parameter_assignment: HashMap::from([
2352                (Parameter::new("n"), 4),
2353                (Parameter::new("t"), 2),
2354                (Parameter::new("f"), 1),
2355            ]),
2356            configurations: vec![
2357                Configuration {
2358                    variable_assignment: HashMap::from([
2359                        (Variable::new("var1"), 1),
2360                        (Variable::new("var2"), 0),
2361                        (Variable::new("var3"), 0),
2362                    ]),
2363                    location_assignment: HashMap::from([
2364                        (Location::new("loc1"), 3),
2365                        (Location::new("loc2"), 0),
2366                        (Location::new("loc3"), 0),
2367                    ]),
2368                },
2369                Configuration {
2370                    variable_assignment: HashMap::from([
2371                        (Variable::new("var1"), 1),
2372                        (Variable::new("var2"), 0),
2373                        (Variable::new("var3"), 0),
2374                    ]),
2375                    location_assignment: HashMap::from([
2376                        (Location::new("loc1"), 1),
2377                        (Location::new("loc2"), 2),
2378                        (Location::new("loc3"), 0),
2379                    ]),
2380                },
2381            ],
2382            transitions: vec![Transition {
2383                rule_used: ta.rules().find(|r| r.id() == 0).unwrap().clone(),
2384                number_applied: 2,
2385            }],
2386        };
2387
2388        assert_eq!(
2389            got_path, expected_path,
2390            "Got: {}\nExpected: {}",
2391            got_path, expected_path
2392        )
2393    }
2394}