Skip to content

Commit

Permalink
feat: implemented run once attribute for required tasks (#83)
Browse files Browse the repository at this point in the history
* feat: implemented run once attribute for required tasks

* feat: update already ran message

* chore: build documentation for run attribute
  • Loading branch information
joerdav authored Apr 13, 2023
1 parent 97f60c4 commit 67563c5
Show file tree
Hide file tree
Showing 8 changed files with 193 additions and 13 deletions.
3 changes: 2 additions & 1 deletion doc/content/task-syntax/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ linkTitle: "Task Syntax"
- [Task List](/task-syntax/task-list/)
- [Name](/task-syntax/task-name/)
- [Scripts](/task-syntax/scripts/)
- [Dependencies](/task-syntax/dependencies/)
- [Requires](/task-syntax/requires/)
- [Run](/task-syntax/run/)
- [Directory](/task-syntax/directory/)
- [Environment Variables](/task-syntax/environment-variables/)
- [Inputs](/task-syntax/inputs/)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
title: "Dependencies"
title: "Requires"
description:
linkTitle: "Dependencies"
linkTitle: "Requires"
menu: { main: { parent: 'task-syntax', weight: 10 } }
---

Expand Down Expand Up @@ -72,3 +72,7 @@ TASK 3
```

Running in the order of `Task1` -> `Task2` -> `Task`

## Modifying required task behaviour

See [Run](/task-syntax/run/)
48 changes: 48 additions & 0 deletions doc/content/task-syntax/run.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
---
title: "Run"
description:
linkTitle: "Run"
menu: { main: { parent: 'task-syntax', weight: 10 } }
---

## Run attribute

By default, a task can run as many times as it appears in the requires tree.
Consider a scenario where a task is required to run only one time per `xc` invocation,
regardless of how many times it is required.

The solution would be to set the `run` attribute to `once` (defaults to `always`).

````markdown
### setup

run: once

```
echo "TASK 3"
```
````

This will result in the task only running the first time it is invoked.

The default is `always`, which can be omitted or specified.

````markdown
### setup

```
echo "TASK 3"
```
````

is the same as

````markdown
### setup

run: always

```
echo "TASK 3"
```
````
49 changes: 41 additions & 8 deletions models/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,15 @@ import (

// Task represents a parsed Task.
type Task struct {
Name string
Description []string
Script string
Dir string
Env []string
DependsOn []string
Inputs []string
ParsingError string
Name string
Description []string
Script string
Dir string
Env []string
DependsOn []string
Inputs []string
ParsingError string
RequiredBehaviour RequiredBehaviour
}

// Display writes a Task as Markdown.
Expand All @@ -41,6 +42,8 @@ func (t Task) Display(w io.Writer) {
fmt.Fprintln(w, "Inputs:", strings.Join(t.Inputs, ", "))
fmt.Fprintln(w)
}
fmt.Fprintln(w, "Run:", t.RequiredBehaviour)
fmt.Fprintln(w)
if len(t.Script) > 0 {
fmt.Fprintln(w, "```")
fmt.Fprintln(w, t.Script)
Expand All @@ -62,3 +65,33 @@ func (ts Tasks) Get(tsname string) (task Task, ok bool) {
}
return
}

// RequiredBehaviour represents a tasks behaviour when
// required by another task.
// The default is RequiredBehaviourAlways
type RequiredBehaviour int

const (
// RequiredBehaviourAlways should be used if the task is to be run every time it is required.
RequiredBehaviourAlways RequiredBehaviour = iota
// RequiredBehaviourOnce should be used if a task should be run once, even if required multiple times.
RequiredBehaviourOnce
)

func (b RequiredBehaviour) String() string {
if b == RequiredBehaviourOnce {
return "once"
}
return "always"
}

func ParseRequiredBehaviour(s string) (RequiredBehaviour, bool) {
switch strings.ToLower(s) {
case "once":
return RequiredBehaviourOnce, true
case "always":
return RequiredBehaviourAlways, true
default:
return 0, false
}
}
11 changes: 11 additions & 0 deletions parser/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,9 @@ const (
// AttributeTypeInp sets the required inputs for a Task, inputs can be provided
// as commandline arguments or environment variables.
AttributeTypeInp
// AttrubuteTypeRun sets the tasks requiredBehaviour, can be always or once.
// Default is always
AttributeTypeRun
)

var attMap = map[string]AttributeType{
Expand All @@ -136,6 +139,7 @@ var attMap = map[string]AttributeType{
"dir": AttributeTypeDir,
"directory": AttributeTypeDir,
"inputs": AttributeTypeInp,
"run": AttributeTypeRun,
}

func (p *parser) parseAttribute() (bool, error) {
Expand Down Expand Up @@ -169,6 +173,13 @@ func (p *parser) parseAttribute() (bool, error) {
}
s := strings.Trim(rest, trimValues)
p.currTask.Dir = s
case AttributeTypeRun:
s := strings.Trim(rest, trimValues)
r, ok := models.ParseRequiredBehaviour(s)
if !ok {
return false, fmt.Errorf("run contains invalid behaviour %q should be (always, once): %s", s, p.currTask.Name)
}
p.currTask.RequiredBehaviour = r
}
p.scan()
return true, nil
Expand Down
30 changes: 30 additions & 0 deletions parser/parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ func assertTask(t *testing.T, expected, actual models.Task) {
if expected.Dir != actual.Dir {
t.Fatalf("dir want=%q got=%q", expected.Dir, actual.Dir)
}
if expected.RequiredBehaviour != actual.RequiredBehaviour {
t.Fatalf("Run want=%q got=%q", expected.RequiredBehaviour, actual.RequiredBehaviour)
}
if strings.Join(expected.DependsOn, ",") != strings.Join(actual.DependsOn, ",") {
t.Fatalf("requires want=%v got=%v", expected.DependsOn, actual.DependsOn)
}
Expand Down Expand Up @@ -96,6 +99,14 @@ func TestMultipleDirs(t *testing.T) {
}
}

func TestInvalidRun(t *testing.T) {
p, _ := NewParser(strings.NewReader("run: never"), "tasks")
_, err := p.parseAttribute()
if err == nil {
t.Fatal("expected error got nil")
}
}

func TestCommandlessTask(t *testing.T) {
p, _ := NewParser(strings.NewReader(`
# Tasks
Expand Down Expand Up @@ -175,6 +186,7 @@ func TestParseAttribute(t *testing.T) {
expectDir string
expectDependsOn string
expectInputs string
expectBehaviour models.RequiredBehaviour
}{
{
name: "given a basic Env, should parse",
Expand Down Expand Up @@ -256,6 +268,21 @@ func TestParseAttribute(t *testing.T) {
in: "dir: _*`my:attribute_*`",
expectDir: "my:attribute",
},
{
name: "given run always, should parse",
in: "run: always",
expectBehaviour: models.RequiredBehaviourAlways,
},
{
name: "given run once, should parse",
in: "run: once",
expectBehaviour: models.RequiredBehaviourOnce,
},
{
name: "given run once with formatting, should parse",
in: "run: _*`once`*_",
expectBehaviour: models.RequiredBehaviourOnce,
},
{
name: "given env with no colon, should not parse",
in: "env _*`my:attribute_*`",
Expand Down Expand Up @@ -295,6 +322,9 @@ func TestParseAttribute(t *testing.T) {
if tt.expectDir != "" && p.currTask.Dir != tt.expectDir {
t.Fatalf("Dir=%s, want=%s", p.currTask.Dir, tt.expectDir)
}
if p.currTask.RequiredBehaviour != tt.expectBehaviour {
t.Fatalf("got=%q, want=%q", p.currTask.RequiredBehaviour, tt.expectBehaviour)
}
})
}
}
Expand Down
11 changes: 9 additions & 2 deletions run/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ type Runner struct {
scriptRunner ScriptRunner
tasks models.Tasks
dir string
alreadyRan map[string]bool
}

// NewRunner takes Tasks and returns a Runner.
Expand All @@ -42,8 +43,9 @@ func NewRunner(ts models.Tasks, dir string) (runner Runner, err error) {
}
return nil
},
tasks: ts,
dir: dir,
tasks: ts,
dir: dir,
alreadyRan: map[string]bool{},
}
for _, t := range ts {
err = runner.ValidateDependencies(t.Name, []string{})
Expand Down Expand Up @@ -106,6 +108,11 @@ func (r *Runner) Run(ctx context.Context, name string, inputs []string) error {
if !ok {
return fmt.Errorf("task %s not found", name)
}
if task.RequiredBehaviour == models.RequiredBehaviourOnce && r.alreadyRan[task.Name] {
fmt.Printf("task %q ran already: skipping\n", task.Name)
return nil
}
r.alreadyRan[task.Name] = true
env := os.Environ()
env = append(env, task.Env...)
inp, err := getInputs(task, inputs, env)
Expand Down
46 changes: 46 additions & 0 deletions run/run_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,52 @@ func TestRun(t *testing.T) {
taskName: "mytask2",
expectedTasksRun: 2,
},
{
name: "given a valid command with run always set, should only run always",
tasks: []models.Task{
{
Name: "setup",
Script: "somecmd",
RequiredBehaviour: models.RequiredBehaviourAlways,
},
{
Name: "mytask",
Script: "somecmd",
DependsOn: []string{"setup"},
},
{
Name: "mytask2",
Script: "somecmd2",
Dir: ".",
DependsOn: []string{"mytask", "setup"},
},
},
taskName: "mytask2",
expectedTasksRun: 4,
},
{
name: "given a valid command with run once set, should only run once",
tasks: []models.Task{
{
Name: "setup",
Script: "somecmd",
RequiredBehaviour: models.RequiredBehaviourOnce,
},
{
Name: "mytask",
Script: "somecmd",
DependsOn: []string{"setup"},
},
{
Name: "mytask2",
Script: "somecmd2",
Dir: ".",
DependsOn: []string{"mytask", "setup"},
},
},
taskName: "mytask2",
expectedTasksRun: 3,
},
}
for _, tt := range tests {
tt := tt
Expand Down

0 comments on commit 67563c5

Please sign in to comment.