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