Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Experimental task exposure #87

Open
wants to merge 9 commits into
base: bilus/requirements
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 22 additions & 4 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,31 @@

- Ensure non-zero exit code from a command in Oya tasks, including sub-commands,
propagates to the shell invoking `oya run`, even without `set -e`.

### Added

- Fix `Replace` directive having no effect when used in imported packs.

### Added

- REPL, helping build scripts interactively with access to values in .oya files
and auto-completion, started using `oya repl`, an example session:

oya run repl
$ echo ${Oya[someValue]}
foobar


- Added `Expose` statement, pulling imported tasks into global scope, for example:

Project: someproject

Import:
bar: github.com/foo/bar

Expose: bar

With this command the imported tasks are available both under the `bar` alias
(e.g. `oya run bar.doSomething`) as well as without it (e.g. `oya run
doSomething`).

The `oya import` command now has a flag, to expose the import, for example:

oya import github.com/foo/bar --expose
7 changes: 6 additions & 1 deletion cmd/import.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,16 @@ var importCmd = &cobra.Command{
if err != nil {
return err
}
return internal.Import(cwd, args[0], alias, cmd.OutOrStdout(), cmd.OutOrStderr())
expose, err := cmd.Flags().GetBool("expose")
if err != nil {
return err
}
return internal.Import(cwd, args[0], alias, expose, cmd.OutOrStdout(), cmd.OutOrStderr())
},
}

func init() {
rootCmd.AddCommand(importCmd)
importCmd.Flags().StringP("alias", "a", "", "Import pack under alias name")
importCmd.Flags().BoolP("expose", "e", false, "Expose imported tasks")
}
12 changes: 11 additions & 1 deletion cmd/internal/import.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import (
"github.com/tooploox/oya/pkg/raw"
)

func Import(workDir, uri, alias string, stdout, stderr io.Writer) error {
func Import(workDir, uri, alias string, exposeTasks bool, stdout, stderr io.Writer) error {
if alias == "" {
uriArr := strings.Split(uri, "/")
alias = strcase.ToLowerCamel(uriArr[len(uriArr)-1])
Expand All @@ -37,5 +37,15 @@ func Import(workDir, uri, alias string, stdout, stderr io.Writer) error {
return err
}

if exposeTasks {
if err := raw.Expose(alias); err != nil {
return err
}
}

if err := raw.ApplyChanges(); err != nil {
return err
}

return proj.InstallPacks()
}
140 changes: 140 additions & 0 deletions cmd/internal/printers/tasks.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
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)
exposed := meta.IsTaskExposed(taskName)
if len(meta.Doc) > 0 || exposed {
padding := strings.Repeat(" ", docOffset-len(taskName))
fmt.Fprintf(w, "%s #", padding)
if len(meta.Doc) > 0 {
fmt.Fprintf(w, " %s", meta.Doc)
}

if exposed {
fmt.Fprintf(w, " (%s)", string(meta.OriginalTaskName))
}
}
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
})
}
166 changes: 166 additions & 0 deletions cmd/internal/printers/tasks_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
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",
"",
},
},
{
desc: "exposed tasks",
workDir: "/project/",
tasks: []taskDef{
{task.Name("y"), task.Meta{Doc: "a description", OriginalTaskName: task.Name("y").Aliased("somepack")}, "/project/Oyafile"},
{task.Name("z"), task.Meta{OriginalTaskName: task.Name("z").Aliased("somepack")}, "/project/Oyafile"},
},
expectedOutput: []string{
"# in ./Oyafile",
"oya run y # a description (somepack.y)",
"oya run z # (somepack.z)",
"",
},
},
}

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())

})
}
}
Loading