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}