Skip to main content

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