determinator/
rules.rs

1// Copyright (c) The cargo-guppy Contributors
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4//! Custom rules for the target determinator.
5//!
6//! By default, the target determinator follows a simple set of rules:
7//! * Every changed path is matched to its nearest package, and that package is marked changed.
8//! * Cargo builds are simulated against the old and new package graphs, and any packages with
9//!   different results are marked changed.
10//! * The affected set is found through observing simulated Cargo builds and doing a reverse map.
11//!
12//! However, there is often a need to customize these rules, for example to:
13//! * ignore certain files
14//! * build everything if certain files or packages have changed
15//! * add *virtual dependencies* that Cargo may not know about: if a package changes, also consider
16//!   certain other packages as changed.
17//!
18//! These custom behaviors can be specified through *determinator rules*.
19//!
20//! There are two sorts of determinator rules:
21//! * **Path rules** match on changed paths, and are applied **in order**, before regular matches.
22//! * **Package rules** match based on changed packages, and are applied as required until
23//!   exhausted (i.e. a fixpoint is reached).
24//!
25//! Determinator rules are a configuration file format and can be read from a TOML file.
26//!
27//! # Default path rules
28//!
29//! The determinator ships with a set of default path rules for common files such as `.gitignore`
30//! and `Cargo.lock`. These rules are applied *after* custom rules, so custom rules matching the
31//! same paths can override them.
32//!
33//! The default rules can be [viewed here](DeterminatorRules::DEFAULT_RULES_TOML).
34//!
35//! To disable default rules entirely, set at the top level:
36//!
37//! ```toml
38//! use-default-rules = false
39//! ```
40//!
41//! # Examples for path rules
42//!
43//! To ignore all files named `README.md` and `README.tpl`, and skip all further processing:
44//!
45//! ```toml
46//! [[path-rule]]
47//! # Globs are implemented using globset: https://docs.rs/globset/0.4
48//! globs = ["**/README.md", "**/README.tpl"]
49//! mark-changed = []
50//! # "skip" is the default for post-rule, so it can be omitted.
51//! post-rule = "skip"
52//! ```
53//!
54//! To mark a package changed if a file in a different directory changes, but also continue to
55//! use the standard algorithm to match paths to their nearest package:
56//!
57//! ```toml
58//! [[path-rule]]
59//! # Note that globs are relative to the root of the workspace.
60//! globs = ["cargo-guppy/src/lib.rs"]
61//! # Workspace packages are specified through their names.
62//! mark-changed = ["cargo-compare"]
63//! # "skip-rules" means that cargo-guppy/src/lib.rs will also match cargo-guppy.
64//! post-rule = "skip-rules"
65//! ```
66//!
67//! To build everything if a special file changes:
68//!
69//! ```toml
70//! [[path-rule]]
71//! name = "rust-toolchain"
72//! mark-changed = "all"
73//! ```
74//!
75//! To apply multiple rules to a file, say `CODE_OF_CONDUCT.md`:
76//!
77//! ```toml
78//! [[path-rule]]
79//! globs = ["CODE_OF_CONDUCT.md", "CONTRIBUTING.md"]
80//! mark-changed = ["cargo-guppy"]
81//! # "fallthrough" means further rules are applied as well.
82//! post-rule = "fallthrough"
83//!
84//! [[path-rule]]
85//! globs = ["CODE_OF_CONDUCT.md"]
86//! mark-changed = ["guppy"]
87//! ```
88//!
89//! # Examples for package rules
90//!
91//! To add a "virtual dependency" that Cargo may not know about:
92//!
93//! ```toml
94//! [[package-rule]]
95//! on-affected = ["fixtures"]
96//! mark-changed = ["guppy-cmdlib"]
97//! ```
98//!
99//! To build everything if a package changes.
100//!
101//! ```toml
102//! [[package-rule]]
103//! on-affected = ["guppy-benchmarks"]
104//! mark-changed = "all"
105//! ```
106
107use crate::errors::RulesError;
108use globset::{Glob, GlobSet, GlobSetBuilder};
109use guppy::graph::{PackageGraph, PackageMetadata, PackageSet, Workspace};
110use once_cell::sync::Lazy;
111use serde::{Deserialize, Serialize};
112use std::fmt;
113
114/// Rules for the target determinator.
115///
116/// This forms a configuration file format that can be read from a TOML file.
117///
118/// For more about determinator rules, see [the module-level documentation](index.html).
119#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
120#[serde(deny_unknown_fields)]
121pub struct DeterminatorRules {
122    /// Whether to use the default rules, as specified by `DEFAULT_RULES_TOML` and `default_rules`.
123    ///
124    /// This is true by default.
125    #[serde(default = "default_true", rename = "use-default-rules")]
126    use_default_rules: bool,
127
128    /// A list of rules that each changed file path is matched against.
129    #[serde(default, rename = "path-rule")]
130    pub path_rules: Vec<PathRule>,
131
132    /// A list of rules that each affected package is matched against.
133    ///
134    /// Sometimes, dependencies between workspace packages aren't expressed in Cargo.tomls. The
135    /// packages here act as "virtual dependencies" for the determinator.
136    #[serde(default, rename = "package-rule")]
137    pub package_rules: Vec<PackageRule>,
138}
139
140/// The `Default` impl is the set of custom rules used by the determinator if
141/// [`set_rules`](crate::Determinator::set_rules) isn't called. It is an empty set of determinator
142/// rules, with `use_default_rules` set to true. This means that if `set_rules` isn't
143/// called, the only rules in effect are the default ones.
144impl Default for DeterminatorRules {
145    fn default() -> Self {
146        Self {
147            use_default_rules: true,
148            path_rules: vec![],
149            package_rules: vec![],
150        }
151    }
152}
153
154#[inline]
155fn default_true() -> bool {
156    true
157}
158
159/// A hack that lets the contents of default-rules.toml be included.
160macro_rules! doc_comment {
161    ($doc:expr, $($t:tt)*) => (
162        #[doc = $doc]
163        $($t)*
164    );
165}
166
167impl DeterminatorRules {
168    /// Deserializes determinator rules from the given TOML string.
169    pub fn parse(s: &str) -> Result<Self, toml::de::Error> {
170        toml::from_str(s)
171    }
172
173    doc_comment! {
174        concat!("\
175Contains the default rules in a TOML file format.
176
177The default rules included with this copy of the determinator are:
178
179```toml
180", include_str!("../default-rules.toml"), "\
181```
182
183The latest version of the default rules is available
184[on GitHub](https://github.com/guppy-rs/guppy/blob/main/tools/determinator/default-rules.toml).
185"),
186        pub const DEFAULT_RULES_TOML: &'static str = include_str!("../default-rules.toml");
187    }
188
189    /// Returns the default rules.
190    ///
191    /// These rules are applied *after* any custom rules, so they can be overridden by custom rules.
192    pub fn default_rules() -> &'static DeterminatorRules {
193        static DEFAULT_RULES: Lazy<DeterminatorRules> = Lazy::new(|| {
194            DeterminatorRules::parse(DeterminatorRules::DEFAULT_RULES_TOML)
195                .expect("default rules should parse")
196        });
197
198        &DEFAULT_RULES
199    }
200}
201
202/// Path-based rules for the determinator.
203///
204/// These rules customize the behavior of the determinator based on changed paths.
205///
206/// # Examples
207///
208/// ```toml
209/// [[path-rule]]
210/// globs = ["**/README.md", "**/README.tpl"]
211/// mark-changed = ["guppy"]
212/// ```
213///
214/// For more examples, see [the module-level documentation](index.html).
215#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
216#[serde(deny_unknown_fields, rename_all = "kebab-case")]
217pub struct PathRule {
218    /// The globs to match against.
219    ///
220    /// A changed path matches a rule if it matches any of the globs on this list.
221    ///
222    /// # Examples
223    ///
224    /// In TOML format, this is specified as [`globset`](https://docs.rs/globset/0.4) globs:
225    ///
226    /// ```toml
227    /// globs = ["foo", "**/bar/*.rs"]
228    /// ```
229    pub globs: Vec<String>,
230
231    /// The set of packages to mark as changed.
232    ///
233    /// # Examples
234    ///
235    /// In TOML format, this may be the string `"all"` to cause all packages to be marked changed:
236    ///
237    /// ```toml
238    /// mark-changed = "all"
239    /// ```
240    ///
241    /// Alternatively, `mark-changed` may be an array of workspace package names:
242    ///
243    /// ```toml
244    /// mark-changed = ["guppy", "determinator"]
245    /// ```
246    #[serde(with = "mark_changed_impl")]
247    pub mark_changed: DeterminatorMarkChanged,
248
249    /// The operation to perform after applying the rule. Set to "skip" by default.
250    #[serde(default)]
251    pub post_rule: DeterminatorPostRule,
252}
253
254/// The operation to perform after applying the rule.
255#[derive(Copy, Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
256#[serde(rename_all = "kebab-case")]
257#[non_exhaustive]
258#[derive(Default)]
259pub enum DeterminatorPostRule {
260    /// Skip all further processing of this path.
261    ///
262    /// This is the default.
263    ///
264    /// # Examples
265    ///
266    /// In TOML format, specified as the string `"skip"`:
267    ///
268    /// ```toml
269    /// post-rule = "skip"
270    /// ```
271    #[default]
272    Skip,
273
274    /// Skip rule processing but continue attempting to match the changed path to the nearest
275    /// package name.
276    ///
277    /// # Examples
278    ///
279    /// In TOML format, specified as the string `"skip-rules"`:
280    ///
281    /// ```toml
282    /// post-rule = "skip-rules"
283    /// ```
284    SkipRules,
285
286    /// Continue to apply further rules.
287    ///
288    /// # Examples
289    ///
290    /// In TOML format, specified as the string `"fallthrough"`:
291    ///
292    /// ```toml
293    /// post-rule = "fallthrough"
294    /// ```
295    Fallthrough,
296}
297
298/// Package-based rules for the determinator.
299///
300/// These rules customize the behavior of the determinator based on affected packages, and can be
301/// used to insert "virtual dependencies" that Cargo may not be aware of.
302///
303/// # Examples
304///
305/// ```toml
306/// [[package-rules]]
307/// on-affected = ["determinator"]
308/// mark-changed = ["guppy"]
309/// ```
310///
311/// For more examples, see [the module-level documentation](index.html).
312#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
313#[serde(deny_unknown_fields, rename_all = "kebab-case")]
314pub struct PackageRule {
315    /// The package names to match against.
316    ///
317    /// If any of the packages in this list is affected, the given packages will be marked changed.
318    ///
319    /// # Examples
320    ///
321    /// In TOML format, specified as an array of workspace package names:
322    ///
323    /// ```toml
324    /// on-affected = ["target-spec", "guppy"]
325    /// ```
326    pub on_affected: Vec<String>,
327
328    /// The set of packages to mark as changed.
329    ///
330    /// # Examples
331    ///
332    /// In TOML format, this may be the string `"all"`:
333    ///
334    /// ```toml
335    /// mark-changed = "all"
336    /// ```
337    ///
338    /// or an array of workspace package names:
339    ///
340    /// ```toml
341    /// mark-changed = ["guppy", "determinator"]
342    /// ```
343    #[serde(with = "mark_changed_impl")]
344    pub mark_changed: DeterminatorMarkChanged,
345}
346
347/// The set of packages to mark as changed.
348///
349/// # Examples
350///
351/// In TOML format, this may be the string `"all"` to cause all packages to be marked changed:
352///
353/// ```toml
354/// mark-changed = "all"
355/// ```
356///
357/// Alternatively, `mark-changed` may be an array of workspace package names:
358///
359/// ```toml
360/// mark-changed = ["guppy", "determinator"]
361/// ```
362///
363/// For more examples, see [the module-level documentation](index.html).
364#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
365#[serde(rename_all = "kebab-case", untagged)]
366pub enum DeterminatorMarkChanged {
367    /// Mark the workspace packages with the given names as changed.
368    ///
369    /// This may be empty:
370    ///
371    /// ```toml
372    /// mark-changed = []
373    /// ```
374    Packages(Vec<String>),
375
376    /// Mark the entire tree as changed. Skip over all further processing and return the entire
377    /// workspace as affected.
378    ///
379    /// This is most useful for global files that affect the environment.
380    All,
381}
382
383/// The result of matching a file path against a determinator.
384///
385/// Returned by `Determinator::match_path`.
386#[derive(Copy, Clone, Debug, Eq, PartialEq)]
387pub enum PathMatch {
388    /// The path matched a rule, causing everything to be rebuilt.
389    RuleMatchedAll,
390    /// The path matched a rule and ancestor-based matching was not followed.
391    ///
392    /// This will not be returned if the matched rule caused ancestor-based matching to happen.
393    RuleMatched(RuleIndex),
394    /// The path was matched to a package through inspecting the parent directories of each path.
395    AncestorMatched,
396    /// The path wasn't matched to a rule or a nearby package, causing everything to be rebuilt.
397    NoMatches,
398}
399
400/// The index of a rule.
401///
402/// Used in `PathMatch` and while returning errors.
403#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
404pub enum RuleIndex {
405    /// The custom path rule at this index.
406    CustomPath(usize),
407    /// The default path rule at this index.
408    DefaultPath(usize),
409    /// The package rule at this index.
410    ///
411    /// All package rules are custom: there are no default package rules.
412    Package(usize),
413}
414
415impl fmt::Display for RuleIndex {
416    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
417        match self {
418            RuleIndex::CustomPath(index) => write!(f, "custom path rule {}", index),
419            RuleIndex::DefaultPath(index) => write!(f, "default path rule {}", index),
420            RuleIndex::Package(index) => write!(f, "package rule {}", index),
421        }
422    }
423}
424
425// ---
426// Private types
427// ---
428
429/// Internal version of determinator rules.
430#[derive(Clone, Debug)]
431pub(crate) struct RulesImpl<'g> {
432    pub(crate) path_rules: Vec<PathRuleImpl<'g>>,
433    pub(crate) package_rules: Vec<PackageRuleImpl<'g>>,
434}
435
436impl<'g> RulesImpl<'g> {
437    pub(crate) fn new(
438        graph: &'g PackageGraph,
439        options: &DeterminatorRules,
440    ) -> Result<Self, RulesError> {
441        let workspace = graph.workspace();
442
443        let custom_path_rules = options
444            .path_rules
445            .iter()
446            .enumerate()
447            .map(|(idx, rule)| (RuleIndex::CustomPath(idx), rule));
448
449        let default_path_rules = if options.use_default_rules {
450            let default_rules = DeterminatorRules::default_rules();
451            default_rules.path_rules.as_slice()
452        } else {
453            &[]
454        };
455
456        let default_path_rules = default_path_rules
457            .iter()
458            .enumerate()
459            .map(|(idx, rule)| (RuleIndex::DefaultPath(idx), rule));
460
461        // Default rules come after custom ones.
462        let path_rules = custom_path_rules
463            .chain(default_path_rules)
464            .map(
465                |(
466                    rule_index,
467                    PathRule {
468                        globs,
469                        mark_changed,
470                        post_rule,
471                    },
472                )| {
473                    // Convert the globs to a globset.
474                    let mut builder = GlobSetBuilder::new();
475                    for glob in globs {
476                        let glob = Glob::new(glob)
477                            .map_err(|err| RulesError::glob_parse(rule_index, err))?;
478                        builder.add(glob);
479                    }
480
481                    let glob_set = builder
482                        .build()
483                        .map_err(|err| RulesError::glob_parse(rule_index, err))?;
484
485                    // Convert workspace paths to packages.
486                    let mark_changed = MarkChangedImpl::new(&workspace, mark_changed)
487                        .map_err(|err| RulesError::resolve_ref(rule_index, err))?;
488
489                    Ok(PathRuleImpl {
490                        rule_index,
491                        glob_set,
492                        mark_changed,
493                        post_rule: *post_rule,
494                    })
495                },
496            )
497            .collect::<Result<Vec<_>, _>>()?;
498
499        let package_rules = options
500            .package_rules
501            .iter()
502            .enumerate()
503            .map(
504                |(
505                    rule_index,
506                    PackageRule {
507                        on_affected,
508                        mark_changed,
509                    },
510                )| {
511                    let rule_index = RuleIndex::Package(rule_index);
512                    let on_affected = graph
513                        .resolve_workspace_names(on_affected)
514                        .map_err(|err| RulesError::resolve_ref(rule_index, err))?;
515                    let mark_changed = MarkChangedImpl::new(&workspace, mark_changed)
516                        .map_err(|err| RulesError::resolve_ref(rule_index, err))?;
517                    Ok(PackageRuleImpl {
518                        on_affected,
519                        mark_changed,
520                    })
521                },
522            )
523            .collect::<Result<Vec<_>, _>>()?;
524
525        Ok(Self {
526            path_rules,
527            package_rules,
528        })
529    }
530}
531
532#[derive(Clone, Debug)]
533pub(crate) struct PathRuleImpl<'g> {
534    pub(crate) rule_index: RuleIndex,
535    pub(crate) glob_set: GlobSet,
536    pub(crate) mark_changed: MarkChangedImpl<'g>,
537    pub(crate) post_rule: DeterminatorPostRule,
538}
539
540#[derive(Clone, Debug)]
541pub(crate) struct PackageRuleImpl<'g> {
542    pub(crate) on_affected: PackageSet<'g>,
543    pub(crate) mark_changed: MarkChangedImpl<'g>,
544}
545
546#[derive(Clone, Debug)]
547pub(crate) enum MarkChangedImpl<'g> {
548    All,
549    Packages(Vec<PackageMetadata<'g>>),
550}
551
552impl<'g> MarkChangedImpl<'g> {
553    fn new(
554        workspace: &Workspace<'g>,
555        mark_changed: &DeterminatorMarkChanged,
556    ) -> Result<Self, guppy::Error> {
557        match mark_changed {
558            DeterminatorMarkChanged::Packages(names) => Ok(MarkChangedImpl::Packages(
559                workspace.members_by_names(names)?,
560            )),
561            DeterminatorMarkChanged::All => Ok(MarkChangedImpl::All),
562        }
563    }
564}
565
566mod mark_changed_impl {
567    use super::*;
568    use serde::{Deserializer, Serializer, de::Error};
569
570    pub fn serialize<S>(
571        mark_changed: &DeterminatorMarkChanged,
572        serializer: S,
573    ) -> Result<S::Ok, S::Error>
574    where
575        S: Serializer,
576    {
577        match mark_changed {
578            DeterminatorMarkChanged::Packages(names) => names.serialize(serializer),
579            DeterminatorMarkChanged::All => "all".serialize(serializer),
580        }
581    }
582
583    pub fn deserialize<'de, D>(deserializer: D) -> Result<DeterminatorMarkChanged, D::Error>
584    where
585        D: Deserializer<'de>,
586    {
587        let d = MarkChangedDeserialized::deserialize(deserializer)?;
588        match d {
589            MarkChangedDeserialized::String(s) => match s.as_str() {
590                "all" => Ok(DeterminatorMarkChanged::All),
591                other => Err(D::Error::custom(format!(
592                    "unknown string for mark-changed: {}",
593                    other,
594                ))),
595            },
596            MarkChangedDeserialized::VecString(strings) => {
597                Ok(DeterminatorMarkChanged::Packages(strings))
598            }
599        }
600    }
601
602    #[derive(Deserialize)]
603    #[serde(untagged)]
604    enum MarkChangedDeserialized {
605        String(String),
606        VecString(Vec<String>),
607    }
608}
609
610#[cfg(test)]
611mod tests {
612    use super::*;
613
614    #[test]
615    fn parse() {
616        let s = r#"[[path-rule]]
617        globs = ["all/*"]
618        mark-changed = "all"
619        post-rule = "fallthrough"
620
621        [[path-rule]]
622        globs = ["all/1/2/*"]
623        mark-changed = ["c"]
624        post-rule = "skip-rules"
625
626        [[path-rule]]
627        globs = ["none/**/test", "foo/bar"]
628        mark-changed = []
629
630        [[package-rule]]
631        on-affected = ["foo"]
632        mark-changed = ["wat"]
633
634        [[package-rule]]
635        on-affected = ["test1"]
636        mark-changed = "all"
637        "#;
638
639        let expected = DeterminatorRules {
640            use_default_rules: true,
641            path_rules: vec![
642                PathRule {
643                    globs: vec!["all/*".to_owned()],
644                    mark_changed: DeterminatorMarkChanged::All,
645                    post_rule: DeterminatorPostRule::Fallthrough,
646                },
647                PathRule {
648                    globs: vec!["all/1/2/*".to_owned()],
649                    mark_changed: DeterminatorMarkChanged::Packages(vec!["c".to_owned()]),
650                    post_rule: DeterminatorPostRule::SkipRules,
651                },
652                PathRule {
653                    globs: vec!["none/**/test".to_owned(), "foo/bar".to_owned()],
654                    mark_changed: DeterminatorMarkChanged::Packages(vec![]),
655                    post_rule: DeterminatorPostRule::Skip,
656                },
657            ],
658            package_rules: vec![
659                PackageRule {
660                    on_affected: vec!["foo".to_string()],
661                    mark_changed: DeterminatorMarkChanged::Packages(vec!["wat".to_string()]),
662                },
663                PackageRule {
664                    on_affected: vec!["test1".to_string()],
665                    mark_changed: DeterminatorMarkChanged::All,
666                },
667            ],
668        };
669
670        assert_eq!(
671            DeterminatorRules::parse(s),
672            Ok(expected),
673            "parse() result matches"
674        );
675    }
676
677    #[test]
678    fn parse_empty() {
679        let expected = DeterminatorRules::default();
680
681        assert_eq!(
682            DeterminatorRules::parse(""),
683            Ok(expected),
684            "parse_empty() returns default"
685        );
686    }
687
688    #[test]
689    fn parse_bad() {
690        let bads = &[
691            // **************
692            // General errors
693            // **************
694
695            // unrecognized section
696            r#"[[foo]]
697            bar = "baz"
698            "#,
699            // unrecognized section
700            r#"[foo]
701            bar = "baz"
702            "#,
703            //
704            // **********
705            // Path rules
706            // **********
707            //
708            // unrecognized key
709            r#"[[path-rule]]
710            globs = ["a/b"]
711            mark-changed = []
712            foo = "bar"
713            "#,
714            // globs is not a list
715            r#"[[path-rule]]
716            globs = "x"
717            mark-changed = []
718            "#,
719            // glob list doesn't have a string
720            r#"[[path-rule]]
721            globs = [123, "a/b"]
722            mark-changed = []
723            "#,
724            // rule totally missing
725            r#"[[path-rule]]
726            "#,
727            // globs missing
728            r#"[[path-rule]]
729            mark-changed = "all"
730            "#,
731            // mark-changed missing
732            r#"[[path-rule]]
733            globs = ["a/b"]
734            "#,
735            // mark-changed is an invalid string
736            r#"[[path-rule]]
737            globs = ["a/b"]
738            mark-changed = "foo"
739            "#,
740            // mark-changed is not a string or list
741            r#"[[path-rule]]
742            globs = ["a/b"]
743            mark-changed = 123
744            "#,
745            // mark-changed is not a list of strings
746            r#"[[path-rule]]
747            globs = ["a/b"]
748            mark-changed = [123, "abc"]
749            "#,
750            // post-rule is invalid
751            r#"[[path-rule]]
752            globs = ["a/b"]
753            mark-changed = []
754            post-rule = "abc"
755            "#,
756            // post-rule is not a string
757            r#"[[path-rule]]
758            globs = ["a/b"]
759            mark-changed = "all"
760            post-rule = []
761            "#,
762            //
763            // *************
764            // Package rules
765            // *************
766            //
767            // unrecognized key
768            r#"[[package-rule]]
769            on-affected = ["foo"]
770            mark-changed = []
771            foo = "bar"
772            "#,
773            // on-affected is not a list
774            r#"[[package-rule]]
775            on-affected = "foo"
776            mark-changed = []
777            "#,
778            // on-affected doesn't contain strings
779            r#"[[package-rule]]
780            on-affected = ["foo", 123]
781            mark-changed = []
782            "#,
783            // mark-changed is not a string or list
784            r#"[[package-rule]]
785            on-affected = ["foo"]
786            mark-changed = 123
787            "#,
788            // mark-changed is not a list of strings
789            r#"[[package-rule]]
790            on-affected = ["foo", 123]
791            mark-changed = ["bar", 456]
792            "#,
793            // mark-changed is an invalid string
794            r#"[[package-rule]]
795            on-affected = ["foo"]
796            mark-changed = "bar"
797            "#,
798            // on-affected is missing
799            r#"[[package-rule]]
800            mark-changed = "all"
801            "#,
802            // mark-changed is missing
803            r#"[[package-rule]]
804            on-affected = ["foo"]
805            "#,
806        ];
807
808        for &bad in bads {
809            let res = DeterminatorRules::parse(bad);
810            if res.is_ok() {
811                panic!(
812                    "parsing should have failed but succeeded:\n\
813                     input = {}\n\
814                     output: {:?}\n",
815                    bad, res
816                );
817            }
818        }
819    }
820}