diff --git a/README.md b/README.md
index 1ad706b1..f63a0f40 100644
--- a/README.md
+++ b/README.md
@@ -12,6 +12,7 @@ Spread
[Blacklisting and whitelisting](#blacklisting)
[Preparing and restoring](#preparing)
[Functions](#functions)
+[Skipping](#skipping)
[Rebooting](#rebooting)
[Timeouts](#timeouts)
[Fast iterations with reuse](#reuse)
@@ -417,6 +418,36 @@ A few helper functions are available for scripts to use:
* _FATAL_ - Similar to ERROR, but prevents retries. Specific to [adhoc backend](#adhoc).
* _ADDRESS_ - Set allocated system address. Specific to [adhoc backend](#adhoc).
+
+
+## Skipping
+
+A skip condition determines whether a task or suite should be skipped before it begins execution.
+
+Skip conditions can only be defined at the task or suite level. They are evaluated before the preparation phase. If a condition evaluates to true, then:
+
+ * Task: the task is completely skipped — including the prepare, execute, and restore phases.
+
+ * Suite: the suite is not prepared or restored, and all tasks inside it are skipped as well.
+
+Each skip condition must include:
+
+ * if: a shell expression (or list of expressions) evaluated to decide whether the task/suite should be skipped.
+
+ * reason: a human-readable explanation of why the task or suite was skipped.
+
+Below is an example showing how to define skip conditions for a task:
+
+```
+summary: skip condition example
+skip:
+ - reason: This is the first skip reason
+ if: [ -d /path/to/dir ]
+ - reason: This is the second skip reason
+ if: [ -f /path/to/file ]
+execute: |
+ echo "This is an example"
+```
diff --git a/spread/project.go b/spread/project.go
index 2c9d01b1..03d9993d 100644
--- a/spread/project.go
+++ b/spread/project.go
@@ -316,6 +316,11 @@ func (e *Environment) Replace(oldkey, newkey, value string) {
e.vals[newkey] = value
}
+type Skip struct {
+ Reason string `yaml:"reason"`
+ If string `yaml:"if"`
+}
+
type Suite struct {
Summary string
Systems []string
@@ -340,6 +345,7 @@ type Suite struct {
Priority OptionalInt
Manual bool
+ Skip []Skip
}
func (s *Suite) String() string { return "suite " + s.Name }
@@ -371,6 +377,7 @@ type Task struct {
Priority OptionalInt
Manual bool
+ Skip []Skip
}
func (t *Task) String() string { return t.Name }
@@ -387,7 +394,8 @@ type Job struct {
Environment *Environment
Sample int
- Priority int64
+ Priority int64
+ SkipReason string
}
func (job *Job) String() string {
@@ -625,6 +633,13 @@ func Load(path string) (*Project, error) {
suite.PrepareEach = strings.TrimSpace(suite.PrepareEach)
suite.RestoreEach = strings.TrimSpace(suite.RestoreEach)
suite.DebugEach = strings.TrimSpace(suite.DebugEach)
+ for i := range suite.Skip {
+ suite.Skip[i].Reason = strings.TrimSpace(suite.Skip[i].Reason)
+ suite.Skip[i].If = strings.TrimSpace(suite.Skip[i].If)
+ if suite.Skip[i].If == "" || suite.Skip[i].Reason == "" {
+ return nil, fmt.Errorf("%s is missing either the if or reason for the skip", suite)
+ }
+ }
project.Suites[suite.Name] = suite
@@ -677,6 +692,13 @@ func Load(path string) (*Project, error) {
task.Prepare = strings.TrimSpace(task.Prepare)
task.Restore = strings.TrimSpace(task.Restore)
task.Debug = strings.TrimSpace(task.Debug)
+ for _, skip := range task.Skip {
+ skip.Reason = strings.TrimSpace(skip.Reason)
+ skip.If = strings.TrimSpace(skip.If)
+ if skip.If == "" || skip.Reason == "" {
+ return nil, fmt.Errorf("%s is missing either the if or reason for the skip", task)
+ }
+ }
if !validTask.MatchString(task.Name) {
return nil, fmt.Errorf("invalid task name: %q", task.Name)
}
diff --git a/spread/runner.go b/spread/runner.go
index 36417447..bf4ea130 100644
--- a/spread/runner.go
+++ b/spread/runner.go
@@ -13,9 +13,10 @@ import (
"sync"
"time"
- "gopkg.in/tomb.v2"
"math"
"math/rand"
+
+ "gopkg.in/tomb.v2"
)
type Options struct {
@@ -433,6 +434,8 @@ const (
preparing = "preparing"
executing = "executing"
restoring = "restoring"
+ checking = "checking"
+ skipping = "skipping"
)
func (r *Runner) run(client *Client, job *Job, verb string, context interface{}, script, debug string, abend *bool) bool {
@@ -476,6 +479,9 @@ func (r *Runner) run(client *Client, job *Job, verb string, context interface{},
client.SetKillTimeout(job.KillTimeoutFor(context))
_, err := client.Trace(script, dir, job.Environment)
printft(start, endTime, "")
+ if verb == checking {
+ return err == nil
+ }
if err != nil {
// Use a different time so it has a different id on Travis, but keep
// the original start time so the error message shows the task time.
@@ -542,6 +548,7 @@ func (r *Runner) worker(backend *Backend, system *System, order []int) {
var abend bool
var badProject bool
var badSuite = make(map[*Suite]bool)
+ var skippedSuite = make(map[*Suite]string)
var insideProject bool
var insideBackend bool
@@ -549,6 +556,7 @@ func (r *Runner) worker(backend *Backend, system *System, order []int) {
var job, last *Job
+outer:
for {
r.mu.Lock()
if job != nil {
@@ -573,6 +581,11 @@ func (r *Runner) worker(backend *Backend, system *System, order []int) {
r.add(&stats.TaskAbort, job)
continue
}
+ if skippedSuite[job.Suite] != "" {
+ job.SkipReason = skippedSuite[job.Suite]
+ r.add(&stats.TaskSkip, job)
+ continue
+ }
if insideSuite != nil && insideSuite != job.Suite {
if false {
@@ -608,6 +621,19 @@ func (r *Runner) worker(backend *Backend, system *System, order []int) {
if insideSuite != job.Suite {
insideSuite = job.Suite
+
+ // Check if the suite should be skipped
+ for _, skip := range job.Suite.Skip {
+ if r.run(client, job, checking, job.Suite, skip.If, job.Suite.Debug, &abend) {
+ job.SkipReason = skip.Reason
+ r.add(&stats.SuiteSkip, job)
+ r.add(&stats.TaskSkip, job)
+ skippedSuite[job.Suite] = skip.Reason
+ printft(time.Now(), startTime|endTime, "%s %s (%s)...", strings.Title(skipping), job, client.server.Label())
+ continue outer
+ }
+ }
+
if !r.options.Restore && !r.run(client, job, preparing, job.Suite, job.Suite.Prepare, job.Suite.Debug, &abend) {
r.add(&stats.SuitePrepareError, job)
r.add(&stats.TaskAbort, job)
@@ -617,32 +643,46 @@ func (r *Runner) worker(backend *Backend, system *System, order []int) {
}
debug := job.Debug()
- for repeat := r.options.Repeat; repeat >= 0; repeat-- {
- if r.options.Restore {
- // Do not prepare or execute, and don't repeat.
- repeat = -1
- } else if !r.options.Restore && !r.run(client, job, preparing, job, job.Prepare(), debug, &abend) {
- r.add(&stats.TaskPrepareError, job)
- r.add(&stats.TaskAbort, job)
- debug = ""
- repeat = -1
- } else if !r.options.Restore && r.run(client, job, executing, job, job.Task.Execute, debug, &abend) {
- r.add(&stats.TaskDone, job)
- } else if !r.options.Restore {
- r.add(&stats.TaskError, job)
- debug = ""
- repeat = -1
+
+ // Check if the task should be skipped
+ skipRun := false
+ for _, skip := range job.Task.Skip {
+ if r.run(client, job, checking, job, skip.If, debug, &abend) {
+ skipRun = true
+ job.SkipReason = skip.Reason
+ r.add(&stats.TaskSkip, job)
+ printft(time.Now(), startTime|endTime, "%s %s (%s)...", strings.Title(skipping), job, client.server.Label())
+ break
}
- if !abend && !r.options.Restore && repeat <= 0 {
- if err := r.fetchArtifacts(client, job); err != nil {
- printf("Cannot fetch artifacts of %s: %v", job, err)
- r.tomb.Killf("cannot fetch artifacts of %s: %v", job, err)
+ }
+ if !skipRun {
+ for repeat := r.options.Repeat; repeat >= 0; repeat-- {
+ if r.options.Restore {
+ // Do not prepare or execute, and don't repeat.
+ repeat = -1
+ } else if !r.options.Restore && !r.run(client, job, preparing, job, job.Prepare(), debug, &abend) {
+ r.add(&stats.TaskPrepareError, job)
+ r.add(&stats.TaskAbort, job)
+ debug = ""
+ repeat = -1
+ } else if !r.options.Restore && r.run(client, job, executing, job, job.Task.Execute, debug, &abend) {
+ r.add(&stats.TaskDone, job)
+ } else if !r.options.Restore {
+ r.add(&stats.TaskError, job)
+ debug = ""
+ repeat = -1
+ }
+ if !abend && !r.options.Restore && repeat <= 0 {
+ if err := r.fetchArtifacts(client, job); err != nil {
+ printf("Cannot fetch artifacts of %s: %v", job, err)
+ r.tomb.Killf("cannot fetch artifacts of %s: %v", job, err)
+ }
+ }
+ if !abend && !r.run(client, job, restoring, job, job.Restore(), debug, &abend) {
+ r.add(&stats.TaskRestoreError, job)
+ badProject = true
+ repeat = -1
}
- }
- if !abend && !r.run(client, job, restoring, job, job.Restore(), debug, &abend) {
- r.add(&stats.TaskRestoreError, job)
- badProject = true
- repeat = -1
}
}
}
@@ -1030,8 +1070,10 @@ type stats struct {
TaskDone []*Job
TaskError []*Job
TaskAbort []*Job
+ TaskSkip []*Job
TaskPrepareError []*Job
TaskRestoreError []*Job
+ SuiteSkip []*Job
SuitePrepareError []*Job
SuiteRestoreError []*Job
BackendPrepareError []*Job
@@ -1063,9 +1105,11 @@ func (s *stats) log() {
printf("Successful tasks: %d", len(s.TaskDone))
printf("Aborted tasks: %d", len(s.TaskAbort))
+ logNames(printf, "Skipped tasks", s.TaskSkip, taskSkipReason)
logNames(printf, "Failed tasks", s.TaskError, taskName)
logNames(printf, "Failed task prepare", s.TaskPrepareError, taskName)
logNames(printf, "Failed task restore", s.TaskRestoreError, taskName)
+ logNames(printf, "Skipped suites", s.SuiteSkip, suiteSkipReason)
logNames(printf, "Failed suite prepare", s.SuitePrepareError, suiteName)
logNames(printf, "Failed suite restore", s.SuiteRestoreError, suiteName)
logNames(printf, "Failed backend prepare", s.BackendPrepareError, backendName)
@@ -1085,6 +1129,14 @@ func taskName(job *Job) string {
return job.Task.Name + ":" + job.Variant
}
+func taskSkipReason(job *Job) string {
+ return taskName(job) + " - " + job.SkipReason
+}
+
+func suiteSkipReason(job *Job) string {
+ return job.Suite.Name + " - " + job.SkipReason
+}
+
func logNames(f func(format string, args ...interface{}), prefix string, jobs []*Job, name func(job *Job) string) {
names := make([]string, 0, len(jobs))
for _, job := range jobs {
diff --git a/tests/skip/checks/multi-skip/task.yaml b/tests/skip/checks/multi-skip/task.yaml
new file mode 100644
index 00000000..e5485b83
--- /dev/null
+++ b/tests/skip/checks/multi-skip/task.yaml
@@ -0,0 +1,20 @@
+summary: Ensure skip condition works.
+
+environment:
+ skip_1/T1: true
+ skip_2/T1: true
+ skip_1/T2: false
+ skip_2/T2: true
+ skip_1/T3: false
+ skip_2/T3: false
+
+skip:
+ - reason: This is the first skip reason
+ if: |
+ [ "$skip_1" = "true" ]
+ - reason: This is the second skip reason
+ if: |
+ [ "$skip_2" = "true" ]
+
+execute: |
+ exit 0
diff --git a/tests/skip/checks/single-skip/task.yaml b/tests/skip/checks/single-skip/task.yaml
new file mode 100644
index 00000000..a15af910
--- /dev/null
+++ b/tests/skip/checks/single-skip/task.yaml
@@ -0,0 +1,19 @@
+summary: Ensure skip condition works.
+
+environment:
+ skip/T1: true
+ exec/T1: true
+ skip/T2: false
+ exec/T2: true
+ skip/T3: false
+ exec/T3: false
+ skip/T4: true
+ exec/T4: false
+
+skip:
+ - reason: This is the skip reason
+ if: |
+ [ "$skip" = "true" ]
+
+execute: |
+ [ "$exec" = "true" ]
diff --git a/tests/skip/spread.yaml b/tests/skip/spread.yaml
new file mode 100644
index 00000000..2a7f3881
--- /dev/null
+++ b/tests/skip/spread.yaml
@@ -0,0 +1,18 @@
+project: spread
+
+backends:
+ lxd:
+ systems:
+ - ubuntu-22.04
+
+path: /home/test
+
+suites:
+ checks/:
+ summary: Verification tasks.
+ environment:
+ MY_TEST_VAR: '$(HOST: echo "${MY_TEST_VAR:-}")'
+ skip:
+ - reason: Skip all tests for checks suite
+ if: |
+ [ "$MY_TEST_VAR" = 1 ]
diff --git a/tests/skip/task.yaml b/tests/skip/task.yaml
new file mode 100644
index 00000000..6e585f2e
--- /dev/null
+++ b/tests/skip/task.yaml
@@ -0,0 +1,44 @@
+summary: Test the skip condition feature
+
+prepare: |
+ if [ ! -f .spread-reuse.yaml ]; then
+ touch /run/spread-reuse.yaml
+ ln -s /run/spread-reuse.yaml .spread-reuse.yaml
+ fi
+
+execute: |
+ # Check when the skip condition matches with a single skip condition
+ spread -reuse -resend lxd:ubuntu-22.04:checks/single-skip &> task1.out || true
+ grep 'Successful tasks: 1' task1.out
+ grep 'Failed tasks: 1' task1.out
+ grep 'Skipped tasks: 2' task1.out
+ test "$(grep -c 'Skipping ' task1.out)" = 2
+ grep 'single-skip:T1 - This is the skip reason' task1.out
+ grep 'single-skip:T4 - This is the skip reason' task1.out
+
+ # Check when the skip condition matches with multiple skip conditions
+ spread -reuse -resend lxd:ubuntu-22.04:checks/multi-skip &> task2.out || true
+ grep 'Successful tasks: 1' task2.out
+ grep 'Skipped tasks: 2' task2.out
+ test "$(grep -c 'Skipping ' task2.out)" = 2
+ grep 'multi-skip:T1 - This is the first skip reason' task2.out
+ grep 'multi-skip:T2 - This is the second skip reason' task2.out
+
+ # Check when the skip condition is set at suite level
+ MY_TEST_VAR=1 spread -reuse -resend lxd:ubuntu-22.04:checks/single-skip &> task3.out || true
+ grep 'Successful tasks: 0' task3.out
+ grep 'Skipped tasks: 4' task3.out
+ grep 'Skipped suites: 1' task3.out
+ test "$(grep -c 'Skipping ' task3.out)" = 1
+ grep 'checks/ - Skip all tests for checks suite' task3.out
+ grep 'single-skip:T1 - Skip all tests for checks suite' task3.out
+ grep 'single-skip:T2 - Skip all tests for checks suite' task3.out
+ grep 'single-skip:T3 - Skip all tests for checks suite' task3.out
+ grep 'single-skip:T4 - Skip all tests for checks suite' task3.out
+
+debug: |
+ cat task1.out || true
+ echo
+ cat task2.out || true
+ echo
+ cat task3.out || true