Skip to content
Merged
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
46 changes: 40 additions & 6 deletions commands/run.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package commands

import (
"encoding/json"
"errors"
"kool-dev/kool/core/builder"
"kool-dev/kool/core/environment"
Expand All @@ -15,6 +16,7 @@ import (
// KoolRunFlags holds the flags for the run command
type KoolRunFlags struct {
EnvVariables []string
JSON bool
}

// KoolRun holds handlers and functions to implement the run command logic
Expand Down Expand Up @@ -48,7 +50,7 @@ func AddKoolRun(root *cobra.Command) {
func NewKoolRun() *KoolRun {
return &KoolRun{
*newDefaultKoolService(),
&KoolRunFlags{[]string{}},
&KoolRunFlags{[]string{}, false},
parser.NewParser(),
environment.NewEnvStorage(),
shell.NewPromptSelect(),
Expand All @@ -58,7 +60,15 @@ func NewKoolRun() *KoolRun {

// Execute runs the run logic with incoming arguments.
func (r *KoolRun) Execute(originalArgs []string) (err error) {
// look for kool.yml on current working directory
_ = r.parser.AddLookupPath(r.env.Get("PWD"))
// look for kool.yml on kool folder within user home directory
_ = r.parser.AddLookupPath(path.Join(r.env.Get("HOME"), "kool"))

if len(originalArgs) == 0 {
if r.Flags.JSON {
return r.printScriptsJSON("")
}
r.shell.Info("\nAvailable scripts:\n")
scripts := compListScripts("", r)
for _, cmd := range scripts {
Expand All @@ -74,11 +84,6 @@ func (r *KoolRun) Execute(originalArgs []string) (err error) {
args []string = originalArgs[1:]
)

// look for kool.yml on current working directory
_ = r.parser.AddLookupPath(r.env.Get("PWD"))
// look for kool.yml on kool folder within user home directory
_ = r.parser.AddLookupPath(path.Join(r.env.Get("HOME"), "kool"))

if err = r.parseScript(script); err != nil {
return
}
Expand Down Expand Up @@ -125,6 +130,7 @@ A single-line SCRIPT can be run with optional arguments.`,
}

runCmd.Flags().StringArrayVarP(&run.Flags.EnvVariables, "env", "e", []string{}, "Environment variables.")
runCmd.Flags().BoolVar(&run.Flags.JSON, "json", false, "Output available scripts as JSON (use without script argument)")

// after a non-flag arg, stop parsing flags
runCmd.Flags().SetInterspersed(false)
Expand Down Expand Up @@ -246,3 +252,31 @@ func compListScripts(toComplete string, run *KoolRun) (scripts []string) {

return
}

func (r *KoolRun) printScriptsJSON(filter string) (err error) {
var details []parser.ScriptDetail
if details, err = r.parser.ParseAvailableScriptsDetails(filter); err != nil {
return
}

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
}

r.Shell().Println(string(payload))
return nil
}
107 changes: 106 additions & 1 deletion commands/run_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package commands

import (
"encoding/json"
"errors"
"fmt"
"io"
Expand All @@ -18,7 +19,7 @@ import (
func newFakeKoolRun(mockParsedCommands map[string][]builder.Command, mockParseError map[string]error) *KoolRun {
return &KoolRun{
*(newDefaultKoolService().Fake()),
&KoolRunFlags{[]string{}},
&KoolRunFlags{[]string{}, false},
&parser.FakeParser{MockParsedCommands: mockParsedCommands, MockParseError: mockParseError},
environment.NewFakeEnvStorage(),
&shell.FakePromptSelect{},
Expand Down Expand Up @@ -745,3 +746,107 @@ func TestNewRunCommandWithTypoErrorCancelled(t *testing.T) {
t.Errorf("expecting warning '%s', got '%s'", expected, output)
}
}

func TestNewRunCommandJsonOutput(t *testing.T) {
f := newFakeKoolRun(nil, nil)
f.parser.(*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 ./..."},
},
}

cmd := NewRunCommand(f)
cmd.SetArgs([]string{"--json"})

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

if !f.parser.(*parser.FakeParser).CalledParseAvailableDetails {
t.Error("did not call ParseAvailableScriptsDetails")
}

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

if len(fakeShell.OutLines) == 0 {
t.Error("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 TestNewRunCommandJsonOutputEmpty(t *testing.T) {
f := newFakeKoolRun(nil, nil)
f.parser.(*parser.FakeParser).MockScriptDetails = []parser.ScriptDetail{}

cmd := NewRunCommand(f)
cmd.SetArgs([]string{"--json"})

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

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

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

// Should output empty array, not null
if fakeShell.OutLines[0] != "[]" {
t.Errorf("expected empty JSON array '[]', got '%s'", fakeShell.OutLines[0])
}
}

func TestNewRunCommandJsonOutputNullSafety(t *testing.T) {
f := newFakeKoolRun(nil, nil)
f.parser.(*parser.FakeParser).MockScriptDetails = []parser.ScriptDetail{
{
Name: "test",
Comments: nil, // nil comments
Commands: nil, // nil commands
},
}

cmd := NewRunCommand(f)
cmd.SetArgs([]string{"--json"})

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

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

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

// Verify nil values are converted to empty arrays
if output[0].Comments == nil {
t.Error("Comments should not be nil in JSON output")
}
if output[0].Commands == nil {
t.Error("Commands should not be nil in JSON output")
}
}
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
}
48 changes: 48 additions & 0 deletions core/parser/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ type Parser interface {
AddLookupPath(string) error
Parse(string) ([]builder.Command, error)
ParseAvailableScripts(string) ([]string, error)
ParseAvailableScriptsDetails(string) ([]ScriptDetail, error)
}

// DefaultParser implements all default behavior for using kool.yml files.
Expand Down Expand Up @@ -142,3 +143,50 @@ func (p *DefaultParser) ParseAvailableScripts(filter string) (scripts []string,

return
}

// ParseAvailableScriptsDetails parses all available scripts with details
func (p *DefaultParser) ParseAvailableScriptsDetails(filter string) (details []ScriptDetail, err error) {
var (
koolFile string
parsedFile *KoolYaml
found map[string]ScriptDetail
keys []string
)

if len(p.targetFiles) == 0 {
err = errors.New("kool.yml not found")
return
}

found = make(map[string]ScriptDetail)

for _, koolFile = range p.targetFiles {
if parsedFile, err = ParseKoolYamlWithDetails(koolFile); err != nil {
return
}

for name, detail := range parsedFile.ScriptDetails {
if _, exists := found[name]; exists {
continue
}
if filter != "" && !strings.HasPrefix(name, filter) {
continue
}
found[name] = detail
}
}

for name := range found {
keys = append(keys, name)
}
if len(keys) == 0 {
return
}

sort.Strings(keys)
for _, name := range keys {
details = append(details, found[name])
}

return
}
31 changes: 31 additions & 0 deletions core/parser/parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -150,3 +150,34 @@ func TestParserParseAvailableScriptsFilter(t *testing.T) {
t.Error("failed to get filtered scripts from kool.yml")
}
}

func TestParserParseAvailableScriptsDetails(t *testing.T) {
var (
p Parser = NewParser()
details []ScriptDetail
err error
)

if _, err = p.ParseAvailableScriptsDetails(""); err == nil {
t.Error("expecting 'kool.yml not found' error, got none")
}

if err != nil && err.Error() != "kool.yml not found" {
t.Errorf("expecting error 'kool.yml not found', got '%s'", err.Error())
}

workDir, _ := os.Getwd()
_ = p.AddLookupPath(path.Join(workDir, "testing_files"))

if details, err = p.ParseAvailableScriptsDetails(""); err != nil {
t.Errorf("unexpected error; error: %s", err)
}

if len(details) != 1 || details[0].Name != "testing" {
t.Error("failed to get script details from kool.yml")
}

if len(details[0].Commands) != 1 || details[0].Commands[0] != "echo testing" {
t.Errorf("unexpected command details: %v", details[0].Commands)
}
}
Loading
Loading