Skip to content
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
2 changes: 2 additions & 0 deletions checks/runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ func CLIChecks(cliData api.CLIData, overrideBaseURL string, ch chan tea.Msg) (re
// This is the magic of the initial message sent before executing the test
if step.CLICommand != nil {
ch <- messages.StartStepMsg{
Description: step.Description,
CMD: step.CLICommand.Command,
TmdlQuery: step.CLICommand.StdoutFilterTmdl,
NoPenaltyOnFail: step.NoPenaltyOnFail,
Expand All @@ -42,6 +43,7 @@ func CLIChecks(cliData api.CLIData, overrideBaseURL string, ch chan tea.Msg) (re
interpolatedURL := InterpolateVariables(fullURL, variables)

ch <- messages.StartStepMsg{
Description: step.Description,
URL: interpolatedURL,
Method: step.HTTPRequest.Request.Method,
NoPenaltyOnFail: step.NoPenaltyOnFail,
Expand Down
1 change: 1 addition & 0 deletions client/lessons.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ type CLIData struct {
}

type CLIStep struct {
Description string `yaml:"description"`
CLICommand *CLIStepCLICommand `yaml:"cliCommand"`
HTTPRequest *CLIStepHTTPRequest `yaml:"httpRequest"`
NoPenaltyOnFail bool `yaml:"noPenaltyOnFail"`
Expand Down
3 changes: 2 additions & 1 deletion cmd/localtest.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (

func init() {
rootCmd.AddCommand(localTestCmd)
localTestCmd.Flags().BoolVarP(&verboseOutput, "verbose", "v", false, "show detailed output for every step")
}

var localTestCmd = &cobra.Command{
Expand Down Expand Up @@ -46,7 +47,7 @@ func localTestHandler(cmd *cobra.Command, args []string) error {
}

ch := make(chan tea.Msg, 1)
finalise := render.StartRenderer(data, true, ch)
finalise := render.StartRenderer(data, true, verboseOutput, ch)

cliResults := checks.CLIChecks(data, overrideBaseURL, ch)
submissionEvent := checks.LocalSubmissionEvent(data, cliResults)
Expand Down
6 changes: 5 additions & 1 deletion cmd/localtest_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ func TestReadLocalCLIDataAcceptsLessonDirectory(t *testing.T) {
- darwin
baseURLDefault: http://localhost:3000
steps:
- cliCommand:
- description: Prints a greeting
cliCommand:
command: echo hello
tests:
- exitCode: 0
Expand All @@ -36,6 +37,9 @@ steps:
if len(data.Steps) != 1 || data.Steps[0].CLICommand == nil {
t.Fatalf("expected one CLI command step, got %#v", data.Steps)
}
if data.Steps[0].Description != "Prints a greeting" {
t.Fatalf("Description = %q, want manifest description", data.Steps[0].Description)
}
if len(data.Steps[0].CLICommand.Tests[1].StdoutContainsAll) != 1 {
t.Fatalf("expected stdoutContainsAll test to load")
}
Expand Down
1 change: 1 addition & 0 deletions cmd/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ func init() {
rootCmd.AddCommand(runCmd)
runCmd.Flags().BoolVarP(&forceSubmit, "submit", "s", false, "shortcut flag to submit after running")
runCmd.Flags().BoolVar(&debugSubmission, "debug", false, "log submission request/response debug output")
runCmd.Flags().BoolVarP(&verboseOutput, "verbose", "v", false, "show detailed output for every step")
}

// runCmd represents the run command
Expand Down
4 changes: 3 additions & 1 deletion cmd/submit.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,13 @@ import (
var (
forceSubmit bool
debugSubmission bool
verboseOutput bool
)

func init() {
rootCmd.AddCommand(submitCmd)
submitCmd.Flags().BoolVar(&debugSubmission, "debug", false, "log submission request/response debug output")
submitCmd.Flags().BoolVarP(&verboseOutput, "verbose", "v", false, "show detailed output for every step")
}

// submitCmd represents the submit command
Expand Down Expand Up @@ -73,7 +75,7 @@ func submissionHandler(cmd *cobra.Command, args []string) error {

ch := make(chan tea.Msg, 1)
// StartRenderer and returns immediately, finalise function blocks the execution until the renderer is closed.
finalise := render.StartRenderer(data, isSubmit, ch)
finalise := render.StartRenderer(data, isSubmit, verboseOutput, ch)

cliResults := checks.CLIChecks(data, overrideBaseURL, ch)

Expand Down
1 change: 1 addition & 0 deletions messages/messages.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package messages
import api "github.com/bootdotdev/bootdev/client"

type StartStepMsg struct {
Description string
CMD string
URL string
Method string
Expand Down
8 changes: 3 additions & 5 deletions render/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,18 +56,16 @@ func printHTTPRequestResult(result api.HTTPRequestResult) string {
bytes := []byte(result.BodyString)
contentType := http.DetectContentType(bytes)
if contentType == "application/json" || strings.HasPrefix(contentType, "text/") {
body := result.BodyString
var unmarshalled any
err := json.Unmarshal([]byte(result.BodyString), &unmarshalled)
if err == nil {
pretty, err := json.MarshalIndent(unmarshalled, "", " ")
if err == nil {
str.Write(pretty)
} else {
str.WriteString(result.BodyString)
body = string(pretty)
}
} else {
str.WriteString(result.BodyString)
}
str.WriteString(truncateVisualOutput(body))
} else {
fmt.Fprintf(
&str,
Expand Down
7 changes: 5 additions & 2 deletions render/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ type testModel struct {
}

type stepModel struct {
step string
description string
detail string
passed *bool
result *api.CLIStepResult
finished bool
Expand All @@ -29,16 +30,18 @@ type rootModel struct {
xpReward int
xpBreakdown []api.XPBreakdownItem
isSubmit bool
verbose bool
finalized bool
clear bool
}

func initModel(isSubmit bool) rootModel {
func initModel(isSubmit bool, verbose bool) rootModel {
s := spinner.New()
s.Spinner = spinner.Dot
return rootModel{
spinner: s,
isSubmit: isSubmit,
verbose: verbose,
steps: []stepModel{},
}
}
19 changes: 13 additions & 6 deletions render/render.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package render
import (
"fmt"
"os"
"strings"
"sync"

api "github.com/bootdotdev/bootdev/client"
Expand Down Expand Up @@ -42,15 +43,21 @@ func (m rootModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, tea.Quit

case messages.StartStepMsg:
step := fmt.Sprintf("Running: %s", msg.CMD)
description := strings.TrimSpace(msg.Description)
detail := fmt.Sprintf("Command: %s", msg.CMD)
if msg.TmdlQuery != nil {
step += fmt.Sprintf(" (TMDL query: '%s')", *msg.TmdlQuery)
detail += fmt.Sprintf(" (TMDL query: '%s')", *msg.TmdlQuery)
}
if msg.CMD == "" {
step = fmt.Sprintf("%s %s", msg.Method, msg.URL)
detail = fmt.Sprintf("Request: %s %s", msg.Method, msg.URL)
}
if description == "" {
description = strings.TrimPrefix(detail, "Command: ")
description = strings.TrimPrefix(description, "Request: ")
}
m.steps = append(m.steps, stepModel{
step: step,
description: description,
detail: detail,
tests: []testModel{},
noPenaltyOnFail: msg.NoPenaltyOnFail,
})
Expand Down Expand Up @@ -97,9 +104,9 @@ func (m rootModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}
}

func StartRenderer(data api.CLIData, isSubmit bool, ch chan tea.Msg) func(api.LessonSubmissionEvent) {
func StartRenderer(data api.CLIData, isSubmit bool, verbose bool, ch chan tea.Msg) func(api.LessonSubmissionEvent) {
var wg sync.WaitGroup
p := tea.NewProgram(initModel(isSubmit), tea.WithoutSignalHandler())
p := tea.NewProgram(initModel(isSubmit, verbose), tea.WithoutSignalHandler())

wg.Add(1)
go func() {
Expand Down
139 changes: 98 additions & 41 deletions render/view.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,53 +101,40 @@ func (m rootModel) View() string {
}
s := m.spinner.View()
var str strings.Builder
for _, step := range m.steps {
str.WriteString(renderTestHeader(step.step, m.spinner, step.finished, m.isSubmit, step.passed, step.noPenaltyOnFail))
str.WriteString(renderTests(step.tests, s))
failedStepIndex := -1
if m.failure != nil && m.failure.FailedStepIndex >= 0 && m.failure.FailedStepIndex < len(m.steps) {
failedStepIndex = m.failure.FailedStepIndex
}
for i, step := range m.steps {
if m.finalized && !m.verbose && failedStepIndex >= 0 && i > failedStepIndex {
break
}

showAllDetails := m.verbose || (!m.isSubmit && m.finalized)
Comment thread
theodore-s-beers marked this conversation as resolved.
failed := step.passed != nil && !*step.passed
if showAllDetails {
str.WriteString(renderTestHeader(step.description, m.spinner, step.finished, m.isSubmit, step.passed, step.noPenaltyOnFail))
fmt.Fprintf(&str, " > %s\n", step.detail)
str.WriteString(renderTests(step.tests, s))
} else {
str.WriteString(renderCompactStep(step, s, m.isSubmit))
if failed && m.finalized {
fmt.Fprintf(&str, "\n > %s\n", step.detail)
str.WriteString(renderTests(step.tests, s))
}
}

if step.sleepAfter != "" && step.finished {
if step.sleepAfter != "" && step.finished && showAllDetails {
sleepBox := borderBox.Render(fmt.Sprintf(" %s ", step.sleepAfter))
str.WriteString(sleepBox)
str.WriteByte('\n')
}

if step.result == nil || !m.finalized {
if step.result == nil || !m.finalized || (!showAllDetails && !failed) {
continue
}

if step.result.CLICommandResult != nil {
for _, test := range step.tests {
if strings.Contains(test.text, "exit code") {
fmt.Fprintf(&str, "\n > Command exit code: %d\n", step.result.CLICommandResult.ExitCode)
break
}
}
str.WriteString(" > Command stdout:\n\n")
sliced := strings.SplitSeq(step.result.CLICommandResult.Stdout, "\n")
i := 0
runeCount := 0
const maxLines, maxRunes = 32, 5120
for s := range sliced {
if i >= maxLines || runeCount >= maxRunes {
str.WriteString(gray.Render("... output visually truncated, full output captured"))
str.WriteByte('\n')
break
}
runeCount += utf8.RuneCountInString(s)
str.WriteString(gray.Render(s))
str.WriteByte('\n')
i++
}
str.WriteString(renderJqOutputs(step.result.CLICommandResult.JqOutputs))
availableVariables, expectsVariables := availableVariablesForCLIResult(*step.result.CLICommandResult)
if expectsVariables {
str.WriteString(renderVariableSection("Variables Available", availableVariables))
}
}

if step.result.HTTPRequestResult != nil {
str.WriteString(printHTTPRequestResult(*step.result.HTTPRequestResult))
}
str.WriteString(renderStepResult(step))
}

if m.result == api.VerificationResultSlugSuccess && m.isSubmit {
Expand Down Expand Up @@ -196,9 +183,6 @@ func (m rootModel) View() string {
str.WriteByte('\n')
str.WriteString(red.Render("Tests failed! ❌"))
if m.failure != nil {
if m.failure.FailedStepIndex >= 0 && m.failure.FailedStepIndex < len(m.steps) {
str.WriteString(red.Render(fmt.Sprintf("\n\nFailed Command: %s", m.steps[m.failure.FailedStepIndex].step)))
}
str.WriteString(red.Render(fmt.Sprintf("\n\nFailed Step: %v", m.failure.FailedStepIndex+1)))
str.WriteString(red.Render(fmt.Sprintf("\nError: %s", m.failure.ErrorMessage)))
} else {
Expand All @@ -215,3 +199,76 @@ func (m rootModel) View() string {

return str.String()
}

func renderCompactStep(step stepModel, spinner string, isSubmit bool) string {
line := renderTest(step.description, spinner, step.finished, &isSubmit, step.passed)
if step.noPenaltyOnFail {
line = fmt.Sprintf("%s %s", line, white.Render(safeStepIcon))
}
return line + "\n"
}

func renderStepResult(step stepModel) string {
var str strings.Builder
if step.result.CLICommandResult != nil {
for _, test := range step.tests {
if strings.Contains(strings.ToLower(test.text), "exit code") {
fmt.Fprintf(&str, "\n > Command exit code: %d\n", step.result.CLICommandResult.ExitCode)
break
}
}
str.WriteString(" > Command stdout:\n\n")
str.WriteString(gray.Render(truncateVisualOutput(step.result.CLICommandResult.Stdout)))
str.WriteByte('\n')
str.WriteString(renderJqOutputs(step.result.CLICommandResult.JqOutputs))
availableVariables, expectsVariables := availableVariablesForCLIResult(*step.result.CLICommandResult)
if expectsVariables {
str.WriteString(renderVariableSection("Variables Available", availableVariables))
}
}

if step.result.HTTPRequestResult != nil {
str.WriteString(printHTTPRequestResult(*step.result.HTTPRequestResult))
}
return str.String()
}

func truncateVisualOutput(output string) string {
const maxLines, maxRunes = 32, 5120
var str strings.Builder
str.Grow(min(len(output), maxRunes*utf8.UTFMax))
lineCount := 1
runeCount := 0
offset := 0
endsWithNewline := false

for offset < len(output) {
r, size := utf8.DecodeRuneInString(output[offset:])
if r == '\n' {
if lineCount >= maxLines {
break
}
str.WriteByte('\n')
offset += size
lineCount++
endsWithNewline = true
continue
}
if runeCount >= maxRunes {
break
}

str.WriteString(output[offset : offset+size])
offset += size
runeCount++
endsWithNewline = false
}

if offset < len(output) {
if str.Len() > 0 && !endsWithNewline {
str.WriteByte('\n')
}
str.WriteString("... output visually truncated")
}
return str.String()
}
Loading