Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
1 change: 1 addition & 0 deletions commands/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ var AddCommands AddCommandsFN = func(root *cobra.Command) {
AddKoolPreset(root)
AddKoolRestart(root)
AddKoolRun(root)
AddKoolScripts(root)
AddKoolSelfUpdate(root)
AddKoolShare(root)
AddKoolStart(root)
Expand Down
1 change: 1 addition & 0 deletions commands/root_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,7 @@ func TestAddCommands(t *testing.T) {
"preset": false,
"restart": false,
"run": false,
"scripts": false,
"self-update": false,
"share": false,
"start": false,
Expand Down
152 changes: 152 additions & 0 deletions commands/scripts.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
package commands

import (
"encoding/json"
"errors"
"kool-dev/kool/core/environment"
"kool-dev/kool/core/parser"
"path"

"github.com/spf13/cobra"
)

// KoolScriptsFlags holds the flags for the scripts command
type KoolScriptsFlags struct {
JSON bool
}

// KoolScripts holds handlers and functions to implement the scripts command logic
type KoolScripts struct {
DefaultKoolService
Flags *KoolScriptsFlags
parser parser.Parser
env environment.EnvStorage
}

func AddKoolScripts(root *cobra.Command) {
var (
scripts = NewKoolScripts()
scriptsCmd = NewScriptsCommand(scripts)
)

root.AddCommand(scriptsCmd)
}

// NewKoolScripts creates a new handler for scripts logic
func NewKoolScripts() *KoolScripts {
return &KoolScripts{
*newDefaultKoolService(),
&KoolScriptsFlags{},
parser.NewParser(),
environment.NewEnvStorage(),
}
}

// Execute runs the scripts logic with incoming arguments.
func (s *KoolScripts) Execute(args []string) (err error) {
var filter string
if len(args) > 0 {
filter = args[0]
}

cwdErr := s.parser.AddLookupPath(s.env.Get("PWD"))
homeErr := s.parser.AddLookupPath(path.Join(s.env.Get("HOME"), "kool"))

if isKoolYmlNotFound(cwdErr) && isKoolYmlNotFound(homeErr) {
if s.Flags.JSON {
return s.printJSON([]parser.ScriptDetail{})
}
s.Shell().Warning("No kool.yml found in current directory or ~/kool.")
return nil
}

if err = firstLookupError(cwdErr, homeErr); err != nil {
return
}

if s.Flags.JSON {
var details []parser.ScriptDetail
if details, err = s.parser.ParseAvailableScriptsDetails(filter); err != nil {
return
}
return s.printJSON(details)
}

var scripts []string
if scripts, err = s.parser.ParseAvailableScripts(filter); err != nil {
return
}

if len(scripts) == 0 {
if filter == "" {
s.Shell().Warning("No scripts found.")
} else {
s.Shell().Warning("No scripts found with prefix:", filter)
}
return nil
}

s.Shell().Info("Available scripts:")
for _, script := range scripts {
s.Shell().Println(" " + script)
}

return
}

// NewScriptsCommand initializes new kool scripts command
func NewScriptsCommand(scripts *KoolScripts) *cobra.Command {
cmd := &cobra.Command{
Use: "scripts [FILTER]",
Short: "List scripts defined in kool.yml",
Long: `List the scripts defined in kool.yml or kool.yaml in the current
working directory and in ~/kool. Use the optional FILTER to show only scripts
that start with a given prefix.`,
Args: cobra.MaximumNArgs(1),
RunE: DefaultCommandRunFunction(scripts),
DisableFlagsInUseLine: true,
}

cmd.Flags().BoolVar(&scripts.Flags.JSON, "json", false, "Output scripts as JSON")

return cmd
}

func isKoolYmlNotFound(err error) bool {
return errors.Is(err, parser.ErrKoolYmlNotFound)
}

func firstLookupError(cwdErr, homeErr error) error {
if cwdErr != nil && !isKoolYmlNotFound(cwdErr) {
return cwdErr
}

if homeErr != nil && !isKoolYmlNotFound(homeErr) {
return homeErr
}

return nil
}

func (s *KoolScripts) printJSON(details []parser.ScriptDetail) (err error) {
if details == nil {
details = []parser.ScriptDetail{}
}

for i := range details {
if details[i].Comments == nil {
details[i].Comments = []string{}
}
if details[i].Commands == nil {
details[i].Commands = []string{}
}
}

var payload []byte
if payload, err = json.Marshal(details); err != nil {
return
}

s.Shell().Println(string(payload))
return nil
}
154 changes: 154 additions & 0 deletions commands/scripts_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
package commands

import (
"encoding/json"
"errors"
"fmt"
"kool-dev/kool/core/environment"
"kool-dev/kool/core/parser"
"kool-dev/kool/core/shell"
"strings"
"testing"
)

func newFakeKoolScripts(mockScripts []string, mockParseErr error) *KoolScripts {
var details []parser.ScriptDetail
for _, script := range mockScripts {
details = append(details, parser.ScriptDetail{Name: script, Comments: []string{}, Commands: []string{}})
}
return &KoolScripts{
*(newDefaultKoolService().Fake()),
&KoolScriptsFlags{},
&parser.FakeParser{
MockScripts: mockScripts,
MockScriptDetails: details,
MockParseAvailableScriptsError: mockParseErr,
},
environment.NewFakeEnvStorage(),
}
}

func TestScriptsCommandListsScripts(t *testing.T) {
f := newFakeKoolScripts([]string{"setup", "lint"}, nil)
cmd := NewScriptsCommand(f)
cmd.SetArgs([]string{})

if err := cmd.Execute(); err != nil {
t.Errorf("unexpected error executing scripts command; error: %v", err)
}

if !f.parser.(*parser.FakeParser).CalledParseAvailableScripts {
t.Errorf("did not call ParseAvailableScripts")
}

fakeShell := f.shell.(*shell.FakeShell)

if !containsLine(fakeShell.OutLines, "setup") || !containsLine(fakeShell.OutLines, "lint") {
t.Errorf("missing scripts on output: %v", fakeShell.OutLines)
}
}

func TestScriptsCommandFiltersScripts(t *testing.T) {
f := newFakeKoolScripts([]string{"setup", "lint"}, nil)
cmd := NewScriptsCommand(f)

cmd.SetArgs([]string{"se"})

if err := cmd.Execute(); err != nil {
t.Errorf("unexpected error executing scripts command; error: %v", err)
}

fakeShell := f.shell.(*shell.FakeShell)

if containsLine(fakeShell.OutLines, "lint") {
t.Errorf("unexpected script on output: %v", fakeShell.OutLines)
}

if !containsLine(fakeShell.OutLines, "setup") {
t.Errorf("missing filtered script on output: %v", fakeShell.OutLines)
}
}

func TestScriptsCommandNoScripts(t *testing.T) {
f := newFakeKoolScripts([]string{}, nil)
cmd := NewScriptsCommand(f)
cmd.SetArgs([]string{})

if err := cmd.Execute(); err != nil {
t.Errorf("unexpected error executing scripts command; error: %v", err)
}

fakeShell := f.shell.(*shell.FakeShell)

if !fakeShell.CalledWarning {
t.Errorf("did not warn about missing scripts")
}

if !strings.Contains(fmt.Sprint(fakeShell.WarningOutput...), "No scripts found") {
t.Errorf("unexpected warning output: %v", fakeShell.WarningOutput)
}
}

func TestScriptsCommandParseError(t *testing.T) {
f := newFakeKoolScripts([]string{"setup"}, errors.New("parse error"))
cmd := NewScriptsCommand(f)
cmd.SetArgs([]string{})

assertExecGotError(t, cmd, "parse error")
}

func TestScriptsCommandJsonOutput(t *testing.T) {
parserMock := &parser.FakeParser{
MockScriptDetails: []parser.ScriptDetail{
{
Name: "setup",
Comments: []string{"Sets up dependencies"},
Commands: []string{"kool run composer install"},
},
{
Name: "lint",
Comments: []string{},
Commands: []string{"kool run go:linux fmt ./..."},
},
},
}

f := newFakeKoolScripts([]string{}, nil)
f.parser = parserMock
cmd := NewScriptsCommand(f)
cmd.SetArgs([]string{"--json"})

if err := cmd.Execute(); err != nil {
t.Errorf("unexpected error executing scripts command; error: %v", err)
}

fakeShell := f.shell.(*shell.FakeShell)

if len(fakeShell.OutLines) == 0 {
t.Errorf("expected JSON output")
return
}

var output []parser.ScriptDetail
if err := json.Unmarshal([]byte(fakeShell.OutLines[0]), &output); err != nil {
t.Fatalf("failed to parse json output: %v", err)
}

if len(output) != 2 {
t.Fatalf("expected 2 script entries, got %d", len(output))
}

if output[0].Name != "lint" || output[1].Name != "setup" {
t.Errorf("unexpected scripts order or names: %v", output)
}
}

func containsLine(lines []string, match string) bool {
for _, line := range lines {
if strings.Contains(line, match) {
return true
}
}

return false
}
28 changes: 28 additions & 0 deletions core/parser/fake_parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package parser

import (
"kool-dev/kool/core/builder"
"sort"
"strings"
)

Expand All @@ -11,10 +12,13 @@ type FakeParser struct {
TargetFiles []string
CalledParse bool
CalledParseAvailableScripts bool
CalledParseAvailableDetails bool
MockParsedCommands map[string][]builder.Command
MockParseError map[string]error
MockScripts []string
MockScriptDetails []ScriptDetail
MockParseAvailableScriptsError error
MockParseAvailableDetailsError error
}

// AddLookupPath implements fake AddLookupPath behavior
Expand Down Expand Up @@ -49,3 +53,27 @@ func (f *FakeParser) ParseAvailableScripts(filter string) (scripts []string, err
err = f.MockParseAvailableScriptsError
return
}

// ParseAvailableScriptsDetails implements fake ParseAvailableScriptsDetails behavior
func (f *FakeParser) ParseAvailableScriptsDetails(filter string) (details []ScriptDetail, err error) {
f.CalledParseAvailableDetails = true

if filter == "" {
details = append(details, f.MockScriptDetails...)
} else {
for _, detail := range f.MockScriptDetails {
if strings.HasPrefix(detail.Name, filter) {
details = append(details, detail)
}
}
}

if len(details) > 1 {
sort.Slice(details, func(i, j int) bool {
return details[i].Name < details[j].Name
})
}

err = f.MockParseAvailableDetailsError
return
}
Loading
Loading