Identify triggers that need changed paths #18
@@ -1,238 +1,245 @@
|
||||
use std::fmt;
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum EventKind {
|
||||
Push,
|
||||
PullRequest,
|
||||
Manual,
|
||||
Schedule,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub enum GitReference {
|
||||
Branch(String),
|
||||
Tag(String),
|
||||
}
|
||||
|
||||
/// What happened, as much of it as deciding needs: which kind, on which Git
|
||||
/// reference, and which paths it touched.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct Event {
|
||||
kind: EventKind,
|
||||
reference: GitReference,
|
||||
changed_paths: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq)]
|
||||
pub struct Filter {
|
||||
include: Vec<Pattern>,
|
||||
exclude: Vec<Pattern>,
|
||||
}
|
||||
|
||||
/// A subset of the glob syntax the dialect allows: literals, `*` within one
|
||||
/// segment, and `**` across segments. Anything else is refused rather than
|
||||
/// matched by accident.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct Pattern(String);
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct Trigger {
|
||||
kind: EventKind,
|
||||
branches: Filter,
|
||||
tags: Filter,
|
||||
paths: Filter,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq)]
|
||||
pub struct Triggers(Vec<Trigger>);
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum TriggerError {
|
||||
#[error("pattern {0:?} uses glob syntax this compiler does not support")]
|
||||
UnsupportedPattern(String),
|
||||
}
|
||||
|
||||
impl Event {
|
||||
#[must_use]
|
||||
pub fn new(kind: EventKind, reference: GitReference, changed_paths: Vec<String>) -> Self {
|
||||
Self {
|
||||
kind,
|
||||
reference,
|
||||
changed_paths,
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn kind(&self) -> EventKind {
|
||||
self.kind
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn reference(&self) -> &GitReference {
|
||||
&self.reference
|
||||
}
|
||||
}
|
||||
|
||||
impl GitReference {
|
||||
#[must_use]
|
||||
pub fn name(&self) -> &str {
|
||||
match self {
|
||||
Self::Branch(name) | Self::Tag(name) => name,
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn full_name(&self) -> String {
|
||||
match self {
|
||||
Self::Branch(name) => format!("refs/heads/{name}"),
|
||||
Self::Tag(name) => format!("refs/tags/{name}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Pattern {
|
||||
pub fn parse(value: &str) -> Result<Self, TriggerError> {
|
||||
if value.contains(['?', '[', ']', '+', '!']) {
|
||||
return Err(TriggerError::UnsupportedPattern(value.to_owned()));
|
||||
}
|
||||
Ok(Self(value.to_owned()))
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn matches(&self, candidate: &str) -> bool {
|
||||
matches_from(&self.0, candidate)
|
||||
}
|
||||
}
|
||||
|
||||
fn matches_from(pattern: &str, candidate: &str) -> bool {
|
||||
match pattern.find('*') {
|
||||
None => pattern == candidate,
|
||||
Some(position) => {
|
||||
let (literal, rest) = pattern.split_at(position);
|
||||
if !candidate.starts_with(literal) {
|
||||
return false;
|
||||
}
|
||||
let candidate = &candidate[literal.len()..];
|
||||
if let Some(rest) = rest.strip_prefix("**") {
|
||||
(0..=candidate.len()).any(|skip| matches_from(rest, &candidate[skip..]))
|
||||
} else {
|
||||
let rest = &rest[1..];
|
||||
candidate
|
||||
.char_indices()
|
||||
.take_while(|(_, character)| *character != '/')
|
||||
.map(|(index, character)| index + character.len_utf8())
|
||||
.chain(std::iter::once(0))
|
||||
.any(|skip| matches_from(rest, &candidate[skip..]))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Filter {
|
||||
#[must_use]
|
||||
pub const fn new(include: Vec<Pattern>, exclude: Vec<Pattern>) -> Self {
|
||||
Self { include, exclude }
|
||||
}
|
||||
|
||||
/// Nothing stated means everything passes. An exclusion always wins, which
|
||||
/// is what makes `paths-ignore` mean what it says.
|
||||
#[must_use]
|
||||
pub fn admits(&self, candidate: &str) -> bool {
|
||||
if self
|
||||
.exclude
|
||||
.iter()
|
||||
.any(|pattern| pattern.matches(candidate))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
self.include.is_empty()
|
||||
|| self
|
||||
.include
|
||||
.iter()
|
||||
.any(|pattern| pattern.matches(candidate))
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.include.is_empty() && self.exclude.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
impl Trigger {
|
||||
#[must_use]
|
||||
pub const fn new(kind: EventKind, branches: Filter, tags: Filter, paths: Filter) -> Self {
|
||||
Self {
|
||||
kind,
|
||||
branches,
|
||||
tags,
|
||||
paths,
|
||||
}
|
||||
}
|
||||
|
||||
fn matches_kind_and_reference(&self, event: &Event) -> bool {
|
||||
if self.kind != event.kind {
|
||||
return false;
|
||||
}
|
||||
match event.reference() {
|
||||
GitReference::Branch(branch) => {
|
||||
(self.tags.is_empty() || !self.branches.is_empty()) && self.branches.admits(branch)
|
||||
}
|
||||
GitReference::Tag(tag) => {
|
||||
(self.branches.is_empty() || !self.tags.is_empty()) && self.tags.admits(tag)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn fires_on(&self, event: &Event) -> bool {
|
||||
if !self.matches_kind_and_reference(event) {
|
||||
return false;
|
||||
}
|
||||
// A path filter on an event that touched nothing has nothing to admit,
|
||||
// so it does not fire.
|
||||
self.paths.is_empty()
|
||||
|| event
|
||||
.changed_paths
|
||||
.iter()
|
||||
.any(|path| self.paths.admits(path))
|
||||
}
|
||||
}
|
||||
|
||||
impl Triggers {
|
||||
#[must_use]
|
||||
pub const fn new(triggers: Vec<Trigger>) -> Self {
|
||||
Self(triggers)
|
||||
}
|
||||
|
||||
/// A workflow with no `on:` is never triggered by an event. Saying nothing
|
||||
/// is not saying everything.
|
||||
#[must_use]
|
||||
pub fn fire_on(&self, event: &Event) -> bool {
|
||||
self.0.iter().any(|trigger| trigger.fires_on(event))
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn may_fire_on(&self, event: &Event) -> bool {
|
||||
self.0
|
||||
.iter()
|
||||
.any(|trigger| trigger.matches_kind_and_reference(event))
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.0.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for EventKind {
|
||||
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::Push => formatter.write_str("push"),
|
||||
Self::PullRequest => formatter.write_str("pull_request"),
|
||||
Self::Manual => formatter.write_str("workflow_dispatch"),
|
||||
Self::Schedule => formatter.write_str("schedule"),
|
||||
}
|
||||
}
|
||||
}
|
||||
use std::fmt;
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum EventKind {
|
||||
Push,
|
||||
PullRequest,
|
||||
Manual,
|
||||
Schedule,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub enum GitReference {
|
||||
Branch(String),
|
||||
Tag(String),
|
||||
}
|
||||
|
||||
/// What happened, as much of it as deciding needs: which kind, on which Git
|
||||
/// reference, and which paths it touched.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct Event {
|
||||
kind: EventKind,
|
||||
reference: GitReference,
|
||||
changed_paths: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq)]
|
||||
pub struct Filter {
|
||||
include: Vec<Pattern>,
|
||||
exclude: Vec<Pattern>,
|
||||
}
|
||||
|
||||
/// A subset of the glob syntax the dialect allows: literals, `*` within one
|
||||
/// segment, and `**` across segments. Anything else is refused rather than
|
||||
/// matched by accident.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct Pattern(String);
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct Trigger {
|
||||
kind: EventKind,
|
||||
branches: Filter,
|
||||
tags: Filter,
|
||||
paths: Filter,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq)]
|
||||
pub struct Triggers(Vec<Trigger>);
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum TriggerError {
|
||||
#[error("pattern {0:?} uses glob syntax this compiler does not support")]
|
||||
UnsupportedPattern(String),
|
||||
}
|
||||
|
||||
impl Event {
|
||||
#[must_use]
|
||||
pub fn new(kind: EventKind, reference: GitReference, changed_paths: Vec<String>) -> Self {
|
||||
Self {
|
||||
kind,
|
||||
reference,
|
||||
changed_paths,
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn kind(&self) -> EventKind {
|
||||
self.kind
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn reference(&self) -> &GitReference {
|
||||
&self.reference
|
||||
}
|
||||
}
|
||||
|
||||
impl GitReference {
|
||||
#[must_use]
|
||||
pub fn name(&self) -> &str {
|
||||
match self {
|
||||
Self::Branch(name) | Self::Tag(name) => name,
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn full_name(&self) -> String {
|
||||
match self {
|
||||
Self::Branch(name) => format!("refs/heads/{name}"),
|
||||
Self::Tag(name) => format!("refs/tags/{name}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Pattern {
|
||||
pub fn parse(value: &str) -> Result<Self, TriggerError> {
|
||||
if value.contains(['?', '[', ']', '+', '!']) {
|
||||
return Err(TriggerError::UnsupportedPattern(value.to_owned()));
|
||||
}
|
||||
Ok(Self(value.to_owned()))
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn matches(&self, candidate: &str) -> bool {
|
||||
matches_from(&self.0, candidate)
|
||||
}
|
||||
}
|
||||
|
||||
fn matches_from(pattern: &str, candidate: &str) -> bool {
|
||||
match pattern.find('*') {
|
||||
None => pattern == candidate,
|
||||
Some(position) => {
|
||||
let (literal, rest) = pattern.split_at(position);
|
||||
if !candidate.starts_with(literal) {
|
||||
return false;
|
||||
}
|
||||
let candidate = &candidate[literal.len()..];
|
||||
if let Some(rest) = rest.strip_prefix("**") {
|
||||
(0..=candidate.len()).any(|skip| matches_from(rest, &candidate[skip..]))
|
||||
} else {
|
||||
let rest = &rest[1..];
|
||||
candidate
|
||||
.char_indices()
|
||||
.take_while(|(_, character)| *character != '/')
|
||||
.map(|(index, character)| index + character.len_utf8())
|
||||
.chain(std::iter::once(0))
|
||||
.any(|skip| matches_from(rest, &candidate[skip..]))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Filter {
|
||||
#[must_use]
|
||||
pub const fn new(include: Vec<Pattern>, exclude: Vec<Pattern>) -> Self {
|
||||
Self { include, exclude }
|
||||
}
|
||||
|
||||
/// Nothing stated means everything passes. An exclusion always wins, which
|
||||
/// is what makes `paths-ignore` mean what it says.
|
||||
#[must_use]
|
||||
pub fn admits(&self, candidate: &str) -> bool {
|
||||
if self
|
||||
.exclude
|
||||
.iter()
|
||||
.any(|pattern| pattern.matches(candidate))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
self.include.is_empty()
|
||||
|| self
|
||||
.include
|
||||
.iter()
|
||||
.any(|pattern| pattern.matches(candidate))
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.include.is_empty() && self.exclude.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
impl Trigger {
|
||||
#[must_use]
|
||||
pub const fn new(kind: EventKind, branches: Filter, tags: Filter, paths: Filter) -> Self {
|
||||
Self {
|
||||
kind,
|
||||
branches,
|
||||
tags,
|
||||
paths,
|
||||
}
|
||||
}
|
||||
|
||||
fn matches_kind_and_reference(&self, event: &Event) -> bool {
|
||||
if self.kind != event.kind {
|
||||
return false;
|
||||
}
|
||||
match event.reference() {
|
||||
GitReference::Branch(branch) => {
|
||||
(self.tags.is_empty() || !self.branches.is_empty()) && self.branches.admits(branch)
|
||||
}
|
||||
GitReference::Tag(tag) => {
|
||||
(self.branches.is_empty() || !self.tags.is_empty()) && self.tags.admits(tag)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn fires_on(&self, event: &Event) -> bool {
|
||||
if !self.matches_kind_and_reference(event) {
|
||||
return false;
|
||||
}
|
||||
// A path filter on an event that touched nothing has nothing to admit,
|
||||
// so it does not fire.
|
||||
self.paths.is_empty()
|
||||
|| event
|
||||
.changed_paths
|
||||
.iter()
|
||||
.any(|path| self.paths.admits(path))
|
||||
}
|
||||
}
|
||||
|
||||
impl Triggers {
|
||||
#[must_use]
|
||||
pub const fn new(triggers: Vec<Trigger>) -> Self {
|
||||
Self(triggers)
|
||||
}
|
||||
|
||||
/// A workflow with no `on:` is never triggered by an event. Saying nothing
|
||||
/// is not saying everything.
|
||||
#[must_use]
|
||||
pub fn fire_on(&self, event: &Event) -> bool {
|
||||
self.0.iter().any(|trigger| trigger.fires_on(event))
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn may_fire_on(&self, event: &Event) -> bool {
|
||||
self.0
|
||||
.iter()
|
||||
.any(|trigger| trigger.matches_kind_and_reference(event))
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn needs_changed_paths(&self, event: &Event) -> bool {
|
||||
self.0
|
||||
.iter()
|
||||
.any(|trigger| trigger.matches_kind_and_reference(event) && !trigger.paths.is_empty())
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.0.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for EventKind {
|
||||
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::Push => formatter.write_str("push"),
|
||||
Self::PullRequest => formatter.write_str("pull_request"),
|
||||
Self::Manual => formatter.write_str("workflow_dispatch"),
|
||||
Self::Schedule => formatter.write_str("schedule"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,189 +1,205 @@
|
||||
#![allow(clippy::expect_used, clippy::panic, clippy::unwrap_used)]
|
||||
|
||||
use syncode_workflow::{Event, EventKind, Filter, GitReference, Pattern, Trigger, Triggers};
|
||||
|
||||
fn pattern(value: &str) -> Pattern {
|
||||
Pattern::parse(value).unwrap_or_else(|error| panic!("pattern {value}: {error}"))
|
||||
}
|
||||
|
||||
fn patterns(values: &[&str]) -> Vec<Pattern> {
|
||||
values.iter().copied().map(pattern).collect()
|
||||
}
|
||||
|
||||
fn push(branch: &str, paths: &[&str]) -> Event {
|
||||
Event::new(
|
||||
EventKind::Push,
|
||||
GitReference::Branch(branch.to_owned()),
|
||||
paths.iter().map(|path| (*path).to_owned()).collect(),
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_workflow_that_says_nothing_is_never_triggered() {
|
||||
assert!(!Triggers::default().fire_on(&push("main", &["src/main.rs"])));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_branch_filter_decides_which_pushes_count() {
|
||||
let triggers = Triggers::new(vec![Trigger::new(
|
||||
EventKind::Push,
|
||||
Filter::new(patterns(&["main", "release/*"]), Vec::new()),
|
||||
Filter::default(),
|
||||
Filter::default(),
|
||||
)]);
|
||||
|
||||
assert!(triggers.fire_on(&push("main", &[])));
|
||||
assert!(triggers.fire_on(&push("release/0.3", &[])));
|
||||
assert!(!triggers.fire_on(&push("feature/x", &[])));
|
||||
assert!(
|
||||
!triggers.fire_on(&push("release/0.3/hotfix", &[])),
|
||||
"a single star does not cross a slash"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_double_star_crosses_slashes() {
|
||||
let triggers = Triggers::new(vec![Trigger::new(
|
||||
EventKind::Push,
|
||||
Filter::new(patterns(&["release/**"]), Vec::new()),
|
||||
Filter::default(),
|
||||
Filter::default(),
|
||||
)]);
|
||||
|
||||
assert!(triggers.fire_on(&push("release/0.3", &[])));
|
||||
assert!(triggers.fire_on(&push("release/0.3/hotfix", &[])));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_exclusion_wins_over_an_inclusion() {
|
||||
let triggers = Triggers::new(vec![Trigger::new(
|
||||
EventKind::Push,
|
||||
Filter::new(patterns(&["**"]), patterns(&["wip/**"])),
|
||||
Filter::default(),
|
||||
Filter::default(),
|
||||
)]);
|
||||
|
||||
assert!(triggers.fire_on(&push("main", &[])));
|
||||
assert!(!triggers.fire_on(&push("wip/experiment", &[])));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_path_filter_needs_a_touched_path_to_admit() {
|
||||
let triggers = Triggers::new(vec![Trigger::new(
|
||||
EventKind::Push,
|
||||
Filter::default(),
|
||||
Filter::default(),
|
||||
Filter::new(patterns(&["crates/**"]), Vec::new()),
|
||||
)]);
|
||||
|
||||
assert!(triggers.fire_on(&push("main", &["crates/workflow/src/lib.rs"])));
|
||||
assert!(!triggers.fire_on(&push("main", &["README.md"])));
|
||||
assert!(
|
||||
!triggers.fire_on(&push("main", &[])),
|
||||
"an event that touched nothing has nothing for a path filter to admit"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn kind_and_branch_can_reject_an_event_before_paths_are_loaded() {
|
||||
let triggers = Triggers::new(vec![Trigger::new(
|
||||
EventKind::Push,
|
||||
Filter::new(patterns(&["main"]), Vec::new()),
|
||||
Filter::default(),
|
||||
Filter::new(patterns(&["crates/**"]), Vec::new()),
|
||||
)]);
|
||||
|
||||
assert!(!triggers.may_fire_on(&push("feature/x", &[])));
|
||||
assert!(triggers.may_fire_on(&push("main", &[])));
|
||||
assert!(!triggers.fire_on(&push("main", &[])));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ignored_paths_do_not_trigger_on_their_own() {
|
||||
let triggers = Triggers::new(vec![Trigger::new(
|
||||
EventKind::Push,
|
||||
Filter::default(),
|
||||
Filter::default(),
|
||||
Filter::new(Vec::new(), patterns(&["**.md"])),
|
||||
)]);
|
||||
|
||||
assert!(!triggers.fire_on(&push("main", &["README.md"])));
|
||||
assert!(
|
||||
triggers.fire_on(&push("main", &["README.md", "src/main.rs"])),
|
||||
"one path outside the ignore list is enough"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_event_of_another_kind_does_not_fire_the_trigger() {
|
||||
let triggers = Triggers::new(vec![Trigger::new(
|
||||
EventKind::PullRequest,
|
||||
Filter::default(),
|
||||
Filter::default(),
|
||||
Filter::default(),
|
||||
)]);
|
||||
|
||||
assert!(!triggers.fire_on(&push("main", &["src/main.rs"])));
|
||||
assert!(triggers.fire_on(&Event::new(
|
||||
EventKind::PullRequest,
|
||||
GitReference::Branch("main".to_owned()),
|
||||
vec!["src/main.rs".to_owned()],
|
||||
)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn branch_and_tag_filters_are_distinct() {
|
||||
let triggers = Triggers::new(vec![Trigger::new(
|
||||
EventKind::Push,
|
||||
Filter::new(patterns(&["main"]), Vec::new()),
|
||||
Filter::new(patterns(&["v*"]), Vec::new()),
|
||||
Filter::default(),
|
||||
)]);
|
||||
|
||||
assert!(triggers.fire_on(&push("main", &[])));
|
||||
assert!(!triggers.fire_on(&push("feature/x", &[])));
|
||||
assert!(triggers.fire_on(&Event::new(
|
||||
EventKind::Push,
|
||||
GitReference::Tag("v0.4.0".to_owned()),
|
||||
Vec::new(),
|
||||
)));
|
||||
assert!(!triggers.fire_on(&Event::new(
|
||||
EventKind::Push,
|
||||
GitReference::Tag("release-candidate".to_owned()),
|
||||
Vec::new(),
|
||||
)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_filter_for_one_reference_kind_excludes_the_other_kind() {
|
||||
let branches = Triggers::new(vec![Trigger::new(
|
||||
EventKind::Push,
|
||||
Filter::new(patterns(&["main"]), Vec::new()),
|
||||
Filter::default(),
|
||||
Filter::default(),
|
||||
)]);
|
||||
let tags = Triggers::new(vec![Trigger::new(
|
||||
EventKind::Push,
|
||||
Filter::default(),
|
||||
Filter::new(patterns(&["v*"]), Vec::new()),
|
||||
Filter::default(),
|
||||
)]);
|
||||
|
||||
let tag = Event::new(
|
||||
EventKind::Push,
|
||||
GitReference::Tag("v0.4.0".to_owned()),
|
||||
Vec::new(),
|
||||
);
|
||||
assert!(!branches.fire_on(&tag));
|
||||
assert!(!tags.fire_on(&push("main", &[])));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn glob_syntax_this_compiler_does_not_support_is_refused() {
|
||||
let error = Pattern::parse("release/[0-9]*").expect_err("character classes are not supported");
|
||||
|
||||
assert!(
|
||||
error.to_string().contains("does not support"),
|
||||
"unexpected error: {error}"
|
||||
);
|
||||
}
|
||||
#![allow(clippy::expect_used, clippy::panic, clippy::unwrap_used)]
|
||||
|
||||
use syncode_workflow::{Event, EventKind, Filter, GitReference, Pattern, Trigger, Triggers};
|
||||
|
||||
fn pattern(value: &str) -> Pattern {
|
||||
Pattern::parse(value).unwrap_or_else(|error| panic!("pattern {value}: {error}"))
|
||||
}
|
||||
|
||||
fn patterns(values: &[&str]) -> Vec<Pattern> {
|
||||
values.iter().copied().map(pattern).collect()
|
||||
}
|
||||
|
||||
fn push(branch: &str, paths: &[&str]) -> Event {
|
||||
Event::new(
|
||||
EventKind::Push,
|
||||
GitReference::Branch(branch.to_owned()),
|
||||
paths.iter().map(|path| (*path).to_owned()).collect(),
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_workflow_that_says_nothing_is_never_triggered() {
|
||||
assert!(!Triggers::default().fire_on(&push("main", &["src/main.rs"])));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_branch_filter_decides_which_pushes_count() {
|
||||
let triggers = Triggers::new(vec![Trigger::new(
|
||||
EventKind::Push,
|
||||
Filter::new(patterns(&["main", "release/*"]), Vec::new()),
|
||||
Filter::default(),
|
||||
Filter::default(),
|
||||
)]);
|
||||
|
||||
assert!(triggers.fire_on(&push("main", &[])));
|
||||
assert!(triggers.fire_on(&push("release/0.3", &[])));
|
||||
assert!(!triggers.fire_on(&push("feature/x", &[])));
|
||||
assert!(
|
||||
!triggers.fire_on(&push("release/0.3/hotfix", &[])),
|
||||
"a single star does not cross a slash"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_double_star_crosses_slashes() {
|
||||
let triggers = Triggers::new(vec![Trigger::new(
|
||||
EventKind::Push,
|
||||
Filter::new(patterns(&["release/**"]), Vec::new()),
|
||||
Filter::default(),
|
||||
Filter::default(),
|
||||
)]);
|
||||
|
||||
assert!(triggers.fire_on(&push("release/0.3", &[])));
|
||||
assert!(triggers.fire_on(&push("release/0.3/hotfix", &[])));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_exclusion_wins_over_an_inclusion() {
|
||||
let triggers = Triggers::new(vec![Trigger::new(
|
||||
EventKind::Push,
|
||||
Filter::new(patterns(&["**"]), patterns(&["wip/**"])),
|
||||
Filter::default(),
|
||||
Filter::default(),
|
||||
)]);
|
||||
|
||||
assert!(triggers.fire_on(&push("main", &[])));
|
||||
assert!(!triggers.fire_on(&push("wip/experiment", &[])));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_path_filter_needs_a_touched_path_to_admit() {
|
||||
let triggers = Triggers::new(vec![Trigger::new(
|
||||
EventKind::Push,
|
||||
Filter::default(),
|
||||
Filter::default(),
|
||||
Filter::new(patterns(&["crates/**"]), Vec::new()),
|
||||
)]);
|
||||
|
||||
assert!(triggers.fire_on(&push("main", &["crates/workflow/src/lib.rs"])));
|
||||
assert!(!triggers.fire_on(&push("main", &["README.md"])));
|
||||
assert!(
|
||||
!triggers.fire_on(&push("main", &[])),
|
||||
"an event that touched nothing has nothing for a path filter to admit"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn kind_and_branch_can_reject_an_event_before_paths_are_loaded() {
|
||||
let triggers = Triggers::new(vec![Trigger::new(
|
||||
EventKind::Push,
|
||||
Filter::new(patterns(&["main"]), Vec::new()),
|
||||
Filter::default(),
|
||||
Filter::new(patterns(&["crates/**"]), Vec::new()),
|
||||
)]);
|
||||
|
||||
assert!(!triggers.may_fire_on(&push("feature/x", &[])));
|
||||
assert!(triggers.may_fire_on(&push("main", &[])));
|
||||
assert!(!triggers.needs_changed_paths(&push("feature/x", &[])));
|
||||
assert!(triggers.needs_changed_paths(&push("main", &[])));
|
||||
assert!(!triggers.fire_on(&push("main", &[])));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_pathless_trigger_does_not_need_changed_paths() {
|
||||
let triggers = Triggers::new(vec![Trigger::new(
|
||||
EventKind::Push,
|
||||
Filter::new(patterns(&["main"]), Vec::new()),
|
||||
Filter::default(),
|
||||
Filter::default(),
|
||||
)]);
|
||||
|
||||
assert!(triggers.may_fire_on(&push("main", &[])));
|
||||
assert!(!triggers.needs_changed_paths(&push("main", &[])));
|
||||
assert!(triggers.fire_on(&push("main", &[])));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ignored_paths_do_not_trigger_on_their_own() {
|
||||
let triggers = Triggers::new(vec![Trigger::new(
|
||||
EventKind::Push,
|
||||
Filter::default(),
|
||||
Filter::default(),
|
||||
Filter::new(Vec::new(), patterns(&["**.md"])),
|
||||
)]);
|
||||
|
||||
assert!(!triggers.fire_on(&push("main", &["README.md"])));
|
||||
assert!(
|
||||
triggers.fire_on(&push("main", &["README.md", "src/main.rs"])),
|
||||
"one path outside the ignore list is enough"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_event_of_another_kind_does_not_fire_the_trigger() {
|
||||
let triggers = Triggers::new(vec![Trigger::new(
|
||||
EventKind::PullRequest,
|
||||
Filter::default(),
|
||||
Filter::default(),
|
||||
Filter::default(),
|
||||
)]);
|
||||
|
||||
assert!(!triggers.fire_on(&push("main", &["src/main.rs"])));
|
||||
assert!(triggers.fire_on(&Event::new(
|
||||
EventKind::PullRequest,
|
||||
GitReference::Branch("main".to_owned()),
|
||||
vec!["src/main.rs".to_owned()],
|
||||
)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn branch_and_tag_filters_are_distinct() {
|
||||
let triggers = Triggers::new(vec![Trigger::new(
|
||||
EventKind::Push,
|
||||
Filter::new(patterns(&["main"]), Vec::new()),
|
||||
Filter::new(patterns(&["v*"]), Vec::new()),
|
||||
Filter::default(),
|
||||
)]);
|
||||
|
||||
assert!(triggers.fire_on(&push("main", &[])));
|
||||
assert!(!triggers.fire_on(&push("feature/x", &[])));
|
||||
assert!(triggers.fire_on(&Event::new(
|
||||
EventKind::Push,
|
||||
GitReference::Tag("v0.4.0".to_owned()),
|
||||
Vec::new(),
|
||||
)));
|
||||
assert!(!triggers.fire_on(&Event::new(
|
||||
EventKind::Push,
|
||||
GitReference::Tag("release-candidate".to_owned()),
|
||||
Vec::new(),
|
||||
)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_filter_for_one_reference_kind_excludes_the_other_kind() {
|
||||
let branches = Triggers::new(vec![Trigger::new(
|
||||
EventKind::Push,
|
||||
Filter::new(patterns(&["main"]), Vec::new()),
|
||||
Filter::default(),
|
||||
Filter::default(),
|
||||
)]);
|
||||
let tags = Triggers::new(vec![Trigger::new(
|
||||
EventKind::Push,
|
||||
Filter::default(),
|
||||
Filter::new(patterns(&["v*"]), Vec::new()),
|
||||
Filter::default(),
|
||||
)]);
|
||||
|
||||
let tag = Event::new(
|
||||
EventKind::Push,
|
||||
GitReference::Tag("v0.4.0".to_owned()),
|
||||
Vec::new(),
|
||||
);
|
||||
assert!(!branches.fire_on(&tag));
|
||||
assert!(!tags.fire_on(&push("main", &[])));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn glob_syntax_this_compiler_does_not_support_is_refused() {
|
||||
let error = Pattern::parse("release/[0-9]*").expect_err("character classes are not supported");
|
||||
|
||||
assert!(
|
||||
error.to_string().contains("does not support"),
|
||||
"unexpected error: {error}"
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user