From 67563c515e4727a77ee75bf4512e992b4cc10fe4 Mon Sep 17 00:00:00 2001 From: Joe Davidson Date: Thu, 13 Apr 2023 10:38:38 +0100 Subject: [PATCH] feat: implemented run once attribute for required tasks (#83) * feat: implemented run once attribute for required tasks * feat: update already ran message * chore: build documentation for run attribute --- doc/content/task-syntax/index.md | 3 +- .../{dependencies.md => requires.md} | 8 ++- doc/content/task-syntax/run.md | 48 ++++++++++++++++++ models/models.go | 49 ++++++++++++++++--- parser/parser.go | 11 +++++ parser/parser_test.go | 30 ++++++++++++ run/run.go | 11 ++++- run/run_test.go | 46 +++++++++++++++++ 8 files changed, 193 insertions(+), 13 deletions(-) rename doc/content/task-syntax/{dependencies.md => requires.md} (90%) create mode 100644 doc/content/task-syntax/run.md diff --git a/doc/content/task-syntax/index.md b/doc/content/task-syntax/index.md index 5ae2d15..0b08f9e 100644 --- a/doc/content/task-syntax/index.md +++ b/doc/content/task-syntax/index.md @@ -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/) diff --git a/doc/content/task-syntax/dependencies.md b/doc/content/task-syntax/requires.md similarity index 90% rename from doc/content/task-syntax/dependencies.md rename to doc/content/task-syntax/requires.md index b1a79f8..e5bfcf3 100644 --- a/doc/content/task-syntax/dependencies.md +++ b/doc/content/task-syntax/requires.md @@ -1,7 +1,7 @@ --- -title: "Dependencies" +title: "Requires" description: -linkTitle: "Dependencies" +linkTitle: "Requires" menu: { main: { parent: 'task-syntax', weight: 10 } } --- @@ -72,3 +72,7 @@ TASK 3 ``` Running in the order of `Task1` -> `Task2` -> `Task` + +## Modifying required task behaviour + +See [Run](/task-syntax/run/) diff --git a/doc/content/task-syntax/run.md b/doc/content/task-syntax/run.md new file mode 100644 index 0000000..b178e25 --- /dev/null +++ b/doc/content/task-syntax/run.md @@ -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" +``` +```` diff --git a/models/models.go b/models/models.go index d67e5de..2c639f5 100644 --- a/models/models.go +++ b/models/models.go @@ -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. @@ -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) @@ -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 + } +} diff --git a/parser/parser.go b/parser/parser.go index e6abca1..9a35220 100644 --- a/parser/parser.go +++ b/parser/parser.go @@ -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{ @@ -136,6 +139,7 @@ var attMap = map[string]AttributeType{ "dir": AttributeTypeDir, "directory": AttributeTypeDir, "inputs": AttributeTypeInp, + "run": AttributeTypeRun, } func (p *parser) parseAttribute() (bool, error) { @@ -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 diff --git a/parser/parser_test.go b/parser/parser_test.go index 757febd..ec13158 100644 --- a/parser/parser_test.go +++ b/parser/parser_test.go @@ -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) } @@ -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 @@ -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", @@ -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_*`", @@ -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) + } }) } } diff --git a/run/run.go b/run/run.go index d8a950d..f08b231 100644 --- a/run/run.go +++ b/run/run.go @@ -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. @@ -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{}) @@ -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) diff --git a/run/run_test.go b/run/run_test.go index a6815a6..255d080 100644 --- a/run/run_test.go +++ b/run/run_test.go @@ -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