From 16c60c1656647fd093c297e2e2907edb30d1b83c Mon Sep 17 00:00:00 2001 From: Marcin Bilski Date: Tue, 26 Oct 2021 10:56:19 +0200 Subject: [PATCH] Extract & improve task list formatting (`oya tasks`). --- cmd/internal/printers/tasks.go | 132 ++++++++++++++++++++++++ cmd/internal/printers/tasks_test.go | 153 ++++++++++++++++++++++++++++ cmd/internal/tasks.go | 29 +----- features/tasks.feature | 37 ++++--- todo.org | 3 + 5 files changed, 315 insertions(+), 39 deletions(-) create mode 100644 cmd/internal/printers/tasks.go create mode 100644 cmd/internal/printers/tasks_test.go diff --git a/cmd/internal/printers/tasks.go b/cmd/internal/printers/tasks.go new file mode 100644 index 0000000..019e77b --- /dev/null +++ b/cmd/internal/printers/tasks.go @@ -0,0 +1,132 @@ +package printers + +import ( + "fmt" + "io" + "path" + "path/filepath" + "sort" + "strings" + + "github.com/tooploox/oya/pkg/task" + "github.com/tooploox/oya/pkg/types" +) + +type taskInfo struct { + taskName task.Name + alias types.Alias + bareTaskName string + meta task.Meta + relOyafilePath string +} + +type TaskList struct { + workDir string + tasks []taskInfo +} + +func NewTaskList(workDir string) *TaskList { + return &TaskList{ + workDir: workDir, + } +} + +func (p *TaskList) AddTask(taskName task.Name, meta task.Meta, oyafilePath string) error { + alias, bareTaskName := taskName.Split() + relPath, err := filepath.Rel(p.workDir, oyafilePath) + if err != nil { + return err + } + + p.tasks = append(p.tasks, taskInfo{taskName, alias, bareTaskName, meta, relPath}) + return nil +} + +func (p *TaskList) Print(w io.Writer) { + sortTasks(p.tasks) + + printTask := p.taskPrinter() + + lastRelPath := "" + first := true + for _, t := range p.tasks { + if t.relOyafilePath != lastRelPath { + if !first { + fmt.Fprintln(w) + } else { + first = false + } + + fmt.Fprintf(w, "# in ./%s\n", t.relOyafilePath) + } + printTask(w, t.taskName, t.meta) + lastRelPath = t.relOyafilePath + } +} + +func (p *TaskList) taskPrinter() func(io.Writer, task.Name, task.Meta) { + docOffset := maxTaskWidth(p.tasks) + return func(w io.Writer, taskName task.Name, meta task.Meta) { + fmt.Fprintf(w, "oya run %s", taskName) + if len(meta.Doc) > 0 { + padding := strings.Repeat(" ", docOffset-len(taskName)) + fmt.Fprintf(w, "%s # %s", padding, meta.Doc) + } + fmt.Fprintln(w) + } +} + +func maxTaskWidth(tasks []taskInfo) int { + w := 0 + for _, t := range tasks { + l := len(string(t.taskName)) + if l > w { + w = l + } + } + return w +} + +func isParentPath(p1, p2 string) bool { + relPath, _ := filepath.Rel(p2, p1) + return strings.Contains(relPath, "../") +} + +func sortTasks(tasks []taskInfo) { + sort.SliceStable(tasks, func(i, j int) bool { + lt := tasks[i] + rt := tasks[j] + + ldir := path.Dir(lt.relOyafilePath) + rdir := path.Dir(rt.relOyafilePath) + + // Top-level tasks go before tasks in subdirectories. + if isParentPath(ldir, rdir) { + return true + } + if isParentPath(rdir, ldir) { + return false + } + + if rdir == ldir { + if len(lt.alias) == 0 && len(rt.alias) != 0 { + return true + } + if len(lt.alias) != 0 && len(rt.alias) == 0 { + return false + } + // Tasks w/o alias before tasks with alias, + // sort aliases alphabetically. + if lt.alias < rt.alias { + return true + } + + // Sort tasks alphabetically. + if lt.bareTaskName < rt.bareTaskName { + return true + } + } + + return false + }) +} diff --git a/cmd/internal/printers/tasks_test.go b/cmd/internal/printers/tasks_test.go new file mode 100644 index 0000000..587bffc --- /dev/null +++ b/cmd/internal/printers/tasks_test.go @@ -0,0 +1,153 @@ +package printers_test + +import ( + "strings" + "testing" + + "github.com/tooploox/oya/cmd/internal/printers" + "github.com/tooploox/oya/pkg/task" + "github.com/tooploox/oya/pkg/types" + tu "github.com/tooploox/oya/testutil" +) + +type mockWriter struct { + bs []byte +} + +func (w *mockWriter) Write(p []byte) (n int, err error) { + w.bs = append(w.bs, p...) + return len(p), nil +} + +func (w *mockWriter) Lines() []string { + if len(w.bs) == 0 { + return []string{} + } + return strings.Split(string(w.bs), "\n") +} + +func TestTaskList(t *testing.T) { + type taskDef struct { + name task.Name + meta task.Meta + oyafilePath string + } + testCases := []struct { + desc string + workDir string + tasks []taskDef + expectedOutput []string + }{ + { + desc: "no tasks", + workDir: "/project/", + tasks: nil, + expectedOutput: []string{}, + }, + { + desc: "one global task", + workDir: "/project/", + tasks: []taskDef{ + {task.Name("task1"), task.Meta{}, "/project/Oyafile"}, + }, + expectedOutput: []string{ + "# in ./Oyafile", + "oya run task1", + "", + }, + }, + { + desc: "global tasks before imported tasks", + workDir: "/project/", + tasks: []taskDef{ + {task.Name("task1"), task.Meta{}, "/project/Oyafile"}, + {task.Name("othertask"), task.Meta{}, "/project/Oyafile"}, + {task.Name("task2").Aliased(types.Alias("pack")), task.Meta{}, "/project/Oyafile"}, + {task.Name("task3").Aliased(types.Alias("pack")), task.Meta{}, "/project/Oyafile"}, + }, + expectedOutput: []string{ + "# in ./Oyafile", + "oya run othertask", + "oya run task1", + "oya run pack.task2", + "oya run pack.task3", + "", + }, + }, + { + desc: "top-level tasks before tasks in subdirectories", + workDir: "/project/", + tasks: []taskDef{ + {task.Name("task1"), task.Meta{}, "/project/Oyafile"}, + {task.Name("otherTask"), task.Meta{}, "/project/Oyafile"}, + {task.Name("aTask"), task.Meta{}, "/project/subdir/Oyafile"}, + {task.Name("task3"), task.Meta{}, "/project/subdir/Oyafile"}, + }, + expectedOutput: []string{ + "# in ./Oyafile", + "oya run otherTask", + "oya run task1", + "", + "# in ./subdir/Oyafile", + "oya run aTask", + "oya run task3", + "", + }, + }, + { + desc: "sort aliases and task names alphabetically", + workDir: "/project/", + tasks: []taskDef{ + {task.Name("yyy"), task.Meta{}, "/project/Oyafile"}, + {task.Name("zzz"), task.Meta{}, "/project/Oyafile"}, + {task.Name("aaa").Aliased(types.Alias("BBB")), task.Meta{}, "/project/Oyafile"}, + {task.Name("ddd").Aliased(types.Alias("AAA")), task.Meta{}, "/project/Oyafile"}, + {task.Name("ccc").Aliased(types.Alias("AAA")), task.Meta{}, "/project/Oyafile"}, + }, + expectedOutput: []string{ + "# in ./Oyafile", + "oya run yyy", + "oya run zzz", + "oya run AAA.ccc", + "oya run AAA.ddd", + "oya run BBB.aaa", + "", + }, + }, + { + desc: "task descriptions are aligned", + workDir: "/project/", + tasks: []taskDef{ + {task.Name("y"), task.Meta{Doc: "a description"}, "/project/Oyafile"}, + {task.Name("zzz"), task.Meta{Doc: "another description"}, "/project/Oyafile"}, + {task.Name("aaa").Aliased(types.Alias("BBB")), task.Meta{Doc: "task aa"}, "/project/Oyafile"}, + {task.Name("ddddd").Aliased(types.Alias("AAA")), task.Meta{}, "/project/Oyafile"}, + {task.Name("ccc").Aliased(types.Alias("AAA")), task.Meta{}, "/project/Oyafile"}, + }, + expectedOutput: []string{ + "# in ./Oyafile", + "oya run y # a description", + "oya run zzz # another description", + "oya run AAA.ccc", + "oya run AAA.ddddd", + "oya run BBB.aaa # task aa", + "", + }, + }, + // NEXT: Exposed + } + + for _, tc := range testCases { + t.Run(tc.desc, func(t *testing.T) { + tl := printers.NewTaskList(tc.workDir) + for _, task := range tc.tasks { + tu.AssertNoErr(t, tl.AddTask(task.name, task.meta, task.oyafilePath), "Adding task failed") + + } + w := mockWriter{} + tl.Print(&w) + tu.AssertObjectsEqual(t, tc.expectedOutput, w.Lines()) + + }) + } +} diff --git a/cmd/internal/tasks.go b/cmd/internal/tasks.go index 4b34b54..9f9c39a 100644 --- a/cmd/internal/tasks.go +++ b/cmd/internal/tasks.go @@ -1,11 +1,10 @@ package internal import ( - "fmt" "io" - "path/filepath" "text/tabwriter" + "github.com/tooploox/oya/cmd/internal/printers" "github.com/tooploox/oya/pkg/project" "github.com/tooploox/oya/pkg/task" ) @@ -28,32 +27,12 @@ func Tasks(workDir string, recurse, changeset bool, stdout, stderr io.Writer) er return err } - first := true - + printer := printers.NewTaskList(workDir) err = p.ForEachTask(workDir, recurse, changeset, false, // No built-ins. func(i int, oyafilePath string, taskName task.Name, task task.Task, meta task.Meta) error { - if i == 0 { - relPath, err := filepath.Rel(workDir, oyafilePath) - if err != nil { - return err - } - if !first { - fmt.Fprintln(w) - } else { - first = false - } - - fmt.Fprintf(w, "# in ./%s\n", relPath) - } - - if len(meta.Doc) > 0 { - fmt.Fprintf(w, "oya run %s\t# %s\n", taskName, meta.Doc) - } else { - fmt.Fprintf(w, "oya run %s\t\n", taskName) - } - - return nil + return printer.AddTask(taskName, meta, oyafilePath) }) + printer.Print(w) w.Flush() return err } diff --git a/features/tasks.feature b/features/tasks.feature index 2f3bd1a..0ddd90e 100644 --- a/features/tasks.feature +++ b/features/tasks.feature @@ -15,7 +15,7 @@ Scenario: Single Oyafile And the command outputs text matching """ # in ./Oyafile - oya run build\s+ + oya run build """ @@ -32,7 +32,7 @@ Scenario: Show only user-defined And the command outputs text matching """ # in ./Oyafile - oya run build\s+ + oya run build """ @@ -53,10 +53,11 @@ Scenario: Subdirectories are not recursed by default And the command outputs text matching """ # in ./Oyafile - oya run build\s+ + oya run build """ +@current Scenario: Subdirectories can be recursed Given file ./Oyafile containing """ @@ -64,7 +65,12 @@ Scenario: Subdirectories can be recursed build: | echo "Done" """ - And file ./subdir1/Oyafile containing + And file ./subdir/Oyafile containing + """ + build: | + echo "Done" + """ + And file ./another_subdir/Oyafile containing """ build: | echo "Done" @@ -74,10 +80,13 @@ Scenario: Subdirectories can be recursed And the command outputs text matching """ # in ./Oyafile - oya run build\s+ + oya run build - # in ./subdir1/Oyafile - oya run build\s+ + # in ./subdir/Oyafile + oya run build + + # in ./another_subdir/Oyafile + oya run build """ @@ -96,7 +105,7 @@ Scenario: Docstring prints And the command outputs text matching """ # in ./Oyafile - oya run build # Build it.* + oya run build # Build it.* """ @@ -124,11 +133,11 @@ Scenario: Doc strings are properly aligned And the command outputs text matching """ # in ./Oyafile - oya run build # Build it.* - oya run testAll # Run all tests.* + oya run build # Build it.* + oya run testAll # Run all tests.* # in ./subdir1/Oyafile - oya run foo # Do foo + oya run foo # Do foo """ @@ -157,7 +166,7 @@ Scenario: Parent dir tasks are not listed And the command outputs text matching """ # in ./Oyafile - oya run foo # Do foo + oya run foo # Do foo """ @@ -185,7 +194,7 @@ Scenario: Imported packs tasks are listed And the command outputs text matching """ # in ./Oyafile - oya run foo.packTask\s+ - oya run test\s+ + oya run test + oya run foo.packTask """ diff --git a/todo.org b/todo.org index 346552b..6d53bfb 100644 --- a/todo.org +++ b/todo.org @@ -65,6 +65,9 @@ ** DONE Multiple packages? Not for now. ** TODO Better handling by oya tasks +*** TODO Show global tasks first +*** TODO Group aliased tasks +*** TODO Show global tasks pointing to exposed tasks ** TODO Transient exposure (package exposing another package) * TODO Vendoring is only partially implemented ** TODO Simplify oya get/vendor (based on Import statements) TBD