Skip to content

Commit 95c1f1e

Browse files
authored
add command lesson type (#11)
* its time * add cli command lesson type
1 parent aef4fed commit 95c1f1e

File tree

8 files changed

+501
-88
lines changed

8 files changed

+501
-88
lines changed

checks/command.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package checks
2+
3+
import (
4+
"os/exec"
5+
6+
api "github.com/bootdotdev/bootdev/client"
7+
)
8+
9+
func CLICommand(
10+
assignment api.Assignment,
11+
) []api.CLICommandResult {
12+
data := assignment.Assignment.AssignmentDataCLICommand.CLICommandData
13+
responses := make([]api.CLICommandResult, len(data.Commands))
14+
for i, command := range data.Commands {
15+
cmd := exec.Command("sh", "-c", command.Command)
16+
b, err := cmd.Output()
17+
if ee, ok := err.(*exec.ExitError); ok {
18+
responses[i].ExitCode = ee.ExitCode()
19+
} else if err != nil {
20+
responses[i].ExitCode = -2
21+
}
22+
responses[i].Stdout = string(b)
23+
}
24+
return responses
25+
}

client/assignments.go

Lines changed: 79 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -38,35 +38,55 @@ type HTTPTestHeader struct {
3838
Value string
3939
}
4040

41-
type Assignment struct {
42-
Assignment struct {
43-
Type string
44-
AssignmentDataHTTPTests *struct {
45-
HttpTests struct {
46-
BaseURL *string
47-
ContainsCompleteDir bool
48-
Requests []struct {
49-
ResponseVariables []ResponseVariable
50-
Tests []HTTPTest
51-
Request struct {
52-
BasicAuth *struct {
53-
Username string
54-
Password string
55-
}
56-
Headers map[string]string
57-
BodyJSON map[string]interface{}
58-
Method string
59-
Path string
60-
Actions struct {
61-
DelayRequestByMs *int32
62-
}
63-
}
41+
type AssignmentDataHTTPTests struct {
42+
HttpTests struct {
43+
BaseURL *string
44+
ContainsCompleteDir bool
45+
Requests []struct {
46+
ResponseVariables []ResponseVariable
47+
Tests []HTTPTest
48+
Request struct {
49+
BasicAuth *struct {
50+
Username string
51+
Password string
52+
}
53+
Headers map[string]string
54+
BodyJSON map[string]interface{}
55+
Method string
56+
Path string
57+
Actions struct {
58+
DelayRequestByMs *int32
6459
}
6560
}
6661
}
6762
}
6863
}
6964

65+
type CLICommandTestCase struct {
66+
ExitCode *int
67+
StdoutContainsAll []string
68+
StdoutContainsNone []string
69+
StdoutMatches *string
70+
StdoutLinesGt *int
71+
}
72+
73+
type AssignmentDataCLICommand struct {
74+
CLICommandData struct {
75+
Commands []struct {
76+
Command string
77+
Tests []CLICommandTestCase
78+
}
79+
}
80+
}
81+
82+
type Assignment struct {
83+
Assignment struct {
84+
Type string
85+
AssignmentDataHTTPTests *AssignmentDataHTTPTests
86+
AssignmentDataCLICommand *AssignmentDataCLICommand
87+
}
88+
}
89+
7090
func FetchAssignment(uuid string) (*Assignment, error) {
7191
resp, err := fetchWithAuth("GET", "/v1/assignments/"+uuid)
7292
if err != nil {
@@ -105,3 +125,39 @@ func SubmitHTTPTestAssignment(uuid string, results any) error {
105125
}
106126
return nil
107127
}
128+
129+
type submitCLICommandRequest struct {
130+
CLICommandResults []CLICommandResult `json:"cliCommandResults"`
131+
}
132+
133+
type StructuredErrCLICommand struct {
134+
ErrorMessage string `json:"Error"`
135+
FailedCommandIndex int `json:"FailedCommandIndex"`
136+
FailedTestIndex int `json:"FailedTestIndex"`
137+
}
138+
139+
type CLICommandResult struct {
140+
ExitCode int
141+
Stdout string
142+
}
143+
144+
func SubmitCLICommandAssignment(uuid string, results []CLICommandResult) (*StructuredErrCLICommand, error) {
145+
bytes, err := json.Marshal(submitCLICommandRequest{CLICommandResults: results})
146+
if err != nil {
147+
return nil, err
148+
}
149+
resp, code, err := fetchWithAuthAndPayload("POST", "/v1/assignments/"+uuid+"/cli_command", bytes)
150+
if err != nil {
151+
return nil, err
152+
}
153+
if code != 200 {
154+
return nil, fmt.Errorf("failed to submit CLI command tests. code: %v: %s", code, string(resp))
155+
}
156+
var failure StructuredErrCLICommand
157+
err = json.Unmarshal(resp, &failure)
158+
if err != nil || failure.ErrorMessage == "" {
159+
// this is ok - it means we had success
160+
return nil, nil
161+
}
162+
return &failure, nil
163+
}

cmd/run.go

Lines changed: 2 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,12 @@
11
package cmd
22

33
import (
4-
"github.com/bootdotdev/bootdev/checks"
5-
api "github.com/bootdotdev/bootdev/client"
64
"github.com/spf13/cobra"
75
)
86

9-
var runBaseURL string
10-
117
func init() {
128
rootCmd.AddCommand(runCmd)
13-
runCmd.Flags().StringVarP(&runBaseURL, "baseurl", "b", "", "set the base URL for HTTP tests, overriding any default")
9+
runCmd.Flags().StringVarP(&submitBaseURL, "baseurl", "b", "", "set the base URL for HTTP tests, overriding any default")
1410
}
1511

1612
// runCmd represents the run command
@@ -19,20 +15,5 @@ var runCmd = &cobra.Command{
1915
Args: cobra.ExactArgs(1),
2016
Short: "Run an assignment without submitting",
2117
PreRun: compose(requireUpdated, requireAuth),
22-
RunE: func(cmd *cobra.Command, args []string) error {
23-
cmd.SilenceUsage = true
24-
assignmentUUID := args[0]
25-
assignment, err := api.FetchAssignment(assignmentUUID)
26-
if err != nil {
27-
return err
28-
}
29-
if assignment.Assignment.Type == "type_http_tests" {
30-
results, finalBaseURL := checks.HttpTest(*assignment, &runBaseURL)
31-
printResults(results, assignment, finalBaseURL)
32-
cobra.CheckErr(err)
33-
} else {
34-
cobra.CheckErr("unsupported assignment type")
35-
}
36-
return nil
37-
},
18+
RunE: submissionHandler,
3819
}

cmd/submit.go

Lines changed: 34 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,20 @@
11
package cmd
22

33
import (
4-
"encoding/json"
4+
"errors"
55
"fmt"
66

77
"github.com/bootdotdev/bootdev/checks"
88
api "github.com/bootdotdev/bootdev/client"
9+
"github.com/bootdotdev/bootdev/render"
910
"github.com/spf13/cobra"
1011
)
1112

12-
var sbumitBaseURL string
13+
var submitBaseURL string
1314

1415
func init() {
1516
rootCmd.AddCommand(submitCmd)
16-
submitCmd.Flags().StringVarP(&sbumitBaseURL, "baseurl", "b", "", "set the base URL for HTTP tests, overriding any default")
17+
submitCmd.Flags().StringVarP(&submitBaseURL, "baseurl", "b", "", "set the base URL for HTTP tests, overriding any default")
1718
}
1819

1920
// submitCmd represents the submit command
@@ -22,53 +23,42 @@ var submitCmd = &cobra.Command{
2223
Args: cobra.MatchAll(cobra.ExactArgs(1)),
2324
Short: "Submit an assignment",
2425
PreRun: compose(requireUpdated, requireAuth),
25-
Run: func(cmd *cobra.Command, args []string) {
26-
assignmentUUID := args[0]
27-
assignment, err := api.FetchAssignment(assignmentUUID)
28-
cobra.CheckErr(err)
29-
if assignment.Assignment.Type == "type_http_tests" {
30-
results, finalBaseURL := checks.HttpTest(*assignment, &sbumitBaseURL)
31-
printResults(results, assignment, finalBaseURL)
32-
cobra.CheckErr(err)
33-
err := api.SubmitHTTPTestAssignment(assignmentUUID, results)
34-
cobra.CheckErr(err)
35-
fmt.Println("\nSubmitted! Check the lesson on Boot.dev for results")
36-
} else {
37-
cobra.CheckErr("unsupported assignment type")
38-
}
39-
},
26+
RunE: submissionHandler,
4027
}
4128

42-
func printResults(results []checks.HttpTestResult, assignment *api.Assignment, finalBaseURL string) {
43-
fmt.Println("=====================================")
44-
defer fmt.Println("=====================================")
45-
fmt.Printf("Running requests against: %s\n", finalBaseURL)
46-
for i, result := range results {
47-
printResult(result, i, assignment)
29+
func submissionHandler(cmd *cobra.Command, args []string) error {
30+
cmd.SilenceUsage = true
31+
isSubmit := cmd.Name() == "submit"
32+
assignmentUUID := args[0]
33+
assignment, err := api.FetchAssignment(assignmentUUID)
34+
if err != nil {
35+
return err
4836
}
49-
}
50-
51-
func printResult(result checks.HttpTestResult, i int, assignment *api.Assignment) {
52-
req := assignment.Assignment.AssignmentDataHTTPTests.HttpTests.Requests[i]
53-
fmt.Printf("%v. %v %v\n", i+1, req.Request.Method, req.Request.Path)
54-
if result.Err != "" {
55-
fmt.Printf(" Err: %v\n", result.Err)
56-
} else {
57-
fmt.Printf(" Response Status Code: %v\n", result.StatusCode)
58-
fmt.Println(" Response Headers:")
59-
for k, v := range req.Request.Headers {
60-
fmt.Printf(" - %v: %v\n", k, v)
37+
switch assignment.Assignment.Type {
38+
case "type_http_tests":
39+
results, finalBaseURL := checks.HttpTest(*assignment, &submitBaseURL)
40+
render.PrintHTTPResults(results, assignment, finalBaseURL)
41+
if isSubmit {
42+
err := api.SubmitHTTPTestAssignment(assignmentUUID, results)
43+
if err != nil {
44+
return err
45+
}
46+
fmt.Println("\nSubmitted! Check the lesson on Boot.dev for results")
6147
}
62-
fmt.Println(" Response Body:")
63-
unmarshalled := map[string]interface{}{}
64-
err := json.Unmarshal([]byte(result.BodyString), &unmarshalled)
65-
if err == nil {
66-
pretty, err := json.MarshalIndent(unmarshalled, "", " ")
67-
if err == nil {
68-
fmt.Println(string(pretty))
48+
case "type_cli_command":
49+
results := checks.CLICommand(*assignment)
50+
data := *assignment.Assignment.AssignmentDataCLICommand
51+
if isSubmit {
52+
failure, err := api.SubmitCLICommandAssignment(assignmentUUID, results)
53+
if err != nil {
54+
return err
6955
}
56+
render.CommandSubmission(data, results, failure)
7057
} else {
71-
fmt.Println(result.BodyString)
58+
render.CommandRun(data, results)
7259
}
60+
default:
61+
return errors.New("unsupported assignment type")
7362
}
63+
return nil
7464
}

go.mod

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,22 @@ require (
1111

1212
require (
1313
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
14+
github.com/charmbracelet/bubbles v0.18.0 // indirect
15+
github.com/charmbracelet/bubbletea v0.26.1 // indirect
1416
github.com/charmbracelet/lipgloss v0.10.0 // indirect
17+
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
1518
github.com/fsnotify/fsnotify v1.7.0 // indirect
1619
github.com/hashicorp/hcl v1.0.0 // indirect
1720
github.com/inconshreveable/mousetrap v1.1.0 // indirect
1821
github.com/itchyny/timefmt-go v0.1.5 // indirect
1922
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
2023
github.com/magiconair/properties v1.8.7 // indirect
2124
github.com/mattn/go-isatty v0.0.20 // indirect
25+
github.com/mattn/go-localereader v0.0.1 // indirect
2226
github.com/mattn/go-runewidth v0.0.15 // indirect
2327
github.com/mitchellh/mapstructure v1.5.0 // indirect
28+
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
29+
github.com/muesli/cancelreader v0.2.2 // indirect
2430
github.com/muesli/reflow v0.3.0 // indirect
2531
github.com/muesli/termenv v0.15.2 // indirect
2632
github.com/pelletier/go-toml/v2 v2.1.0 // indirect
@@ -35,6 +41,7 @@ require (
3541
go.uber.org/atomic v1.9.0 // indirect
3642
go.uber.org/multierr v1.9.0 // indirect
3743
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
44+
golang.org/x/sync v0.7.0 // indirect
3845
golang.org/x/sys v0.19.0 // indirect
3946
golang.org/x/term v0.19.0 // indirect
4047
golang.org/x/text v0.14.0 // indirect

go.sum

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,18 @@
11
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
22
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
3+
github.com/charmbracelet/bubbles v0.18.0 h1:PYv1A036luoBGroX6VWjQIE9Syf2Wby2oOl/39KLfy0=
4+
github.com/charmbracelet/bubbles v0.18.0/go.mod h1:08qhZhtIwzgrtBjAcJnij1t1H0ZRjwHyGsy6AL11PSw=
5+
github.com/charmbracelet/bubbletea v0.26.1 h1:xujcQeF73rh4jwu3+zhfQsvV18x+7zIjlw7/CYbzGJ0=
6+
github.com/charmbracelet/bubbletea v0.26.1/go.mod h1:FzKr7sKoO8iFVcdIBM9J0sJOcQv5nDQaYwsee3kpbgo=
37
github.com/charmbracelet/lipgloss v0.10.0 h1:KWeXFSexGcfahHX+54URiZGkBFazf70JNMtwg/AFW3s=
48
github.com/charmbracelet/lipgloss v0.10.0/go.mod h1:Wig9DSfvANsxqkRsqj6x87irdy123SR4dOXlKa91ciE=
59
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
610
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
711
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
812
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
913
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
14+
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
15+
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
1016
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
1117
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
1218
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
@@ -31,11 +37,17 @@ github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0V
3137
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
3238
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
3339
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
40+
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
41+
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
3442
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
3543
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
3644
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
3745
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
3846
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
47+
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
48+
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
49+
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
50+
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
3951
github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
4052
github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
4153
github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo=
@@ -86,6 +98,9 @@ golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjs
8698
golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
8799
golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
88100
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
101+
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
102+
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
103+
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
89104
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
90105
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
91106
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=

0 commit comments

Comments
 (0)