Skip to content

Commit e4bf986

Browse files
add jq-type test step
1 parent 9ee0d2b commit e4bf986

File tree

3 files changed

+257
-4
lines changed

3 files changed

+257
-4
lines changed

checks/checks.go

Lines changed: 213 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"os/exec"
1212
"regexp"
1313
"runtime"
14+
"slices"
1415
"strings"
1516
"time"
1617

@@ -47,6 +48,107 @@ func runCLICommand(command api.CLIStepCLICommand, variables map[string]string) (
4748
return result
4849
}
4950

51+
func runJqQuery(step api.CLIStepJqQuery, variables map[string]string) api.JqQueryResult {
52+
filePath := InterpolateVariables(step.FilePath, variables)
53+
queryText := InterpolateVariables(step.Query, variables)
54+
result := api.JqQueryResult{
55+
FilePath: filePath,
56+
Query: queryText,
57+
}
58+
59+
input, err := readJqInput(filePath, step.InputMode)
60+
if err != nil {
61+
result.ExitCode = 1
62+
result.Stdout = err.Error()
63+
return result
64+
}
65+
66+
query, err := gojq.Parse(queryText)
67+
if err != nil {
68+
result.ExitCode = 1
69+
result.Stdout = err.Error()
70+
return result
71+
}
72+
73+
iter := query.Run(input)
74+
results := make([]string, 0)
75+
for {
76+
value, ok := iter.Next()
77+
if !ok {
78+
break
79+
}
80+
if err, ok := value.(error); ok {
81+
result.ExitCode = 1
82+
result.Stdout = err.Error()
83+
result.Results = results
84+
return result
85+
}
86+
stringified, err := stringifyJqResult(value)
87+
if err != nil {
88+
result.ExitCode = 1
89+
result.Stdout = err.Error()
90+
result.Results = results
91+
return result
92+
}
93+
results = append(results, stringified)
94+
}
95+
96+
result.ExitCode = 0
97+
result.Results = results
98+
result.Stdout = strings.Join(results, "\n")
99+
return result
100+
}
101+
102+
func readJqInput(filePath string, inputMode string) (any, error) {
103+
contents, err := os.ReadFile(filePath)
104+
if err != nil {
105+
return nil, err
106+
}
107+
108+
mode := strings.ToLower(strings.TrimSpace(inputMode))
109+
if mode == "" {
110+
mode = "json"
111+
}
112+
113+
decoder := json.NewDecoder(bytes.NewReader(contents))
114+
if mode == "jsonl" {
115+
values := make([]any, 0)
116+
for {
117+
var value any
118+
err := decoder.Decode(&value)
119+
if errors.Is(err, io.EOF) {
120+
break
121+
}
122+
if err != nil {
123+
return nil, err
124+
}
125+
values = append(values, value)
126+
}
127+
return values, nil
128+
}
129+
130+
var value any
131+
if err := decoder.Decode(&value); err != nil {
132+
return nil, err
133+
}
134+
if err := decoder.Decode(&struct{}{}); err != io.EOF {
135+
if err == nil {
136+
return nil, errors.New("expected a single JSON value")
137+
}
138+
return nil, err
139+
}
140+
141+
return value, nil
142+
}
143+
144+
func stringifyJqResult(value any) (string, error) {
145+
data, err := json.Marshal(value)
146+
if err != nil {
147+
return "", err
148+
}
149+
return string(data), nil
150+
}
151+
50152
func runHTTPRequest(
51153
client *http.Client,
52154
baseURL string,
@@ -157,6 +259,10 @@ func CLIChecks(cliData api.CLIData, overrideBaseURL string, ch chan tea.Msg) (re
157259
Method: step.HTTPRequest.Request.Method,
158260
ResponseVariables: step.HTTPRequest.ResponseVariables,
159261
}
262+
} else if step.JqQuery != nil {
263+
filePath := InterpolateVariables(step.JqQuery.FilePath, variables)
264+
queryText := InterpolateVariables(step.JqQuery.Query, variables)
265+
ch <- messages.StartStepMsg{CMD: fmt.Sprintf("jq '%s' %s", queryText, filePath)}
160266
}
161267

162268
switch {
@@ -173,6 +279,12 @@ func CLIChecks(cliData api.CLIData, overrideBaseURL string, ch chan tea.Msg) (re
173279
sendHTTPRequestResults(ch, *step.HTTPRequest, result, i)
174280
handleSleep(step.HTTPRequest, ch)
175281

282+
case step.JqQuery != nil:
283+
result := runJqQuery(*step.JqQuery, variables)
284+
results[i].JqQueryResult = &result
285+
sendJqQueryResults(ch, *step.JqQuery, result, i)
286+
handleSleep(step.JqQuery, ch)
287+
176288
default:
177289
cobra.CheckErr("unable to run lesson: missing step")
178290
}
@@ -220,6 +332,31 @@ func sendHTTPRequestResults(ch chan tea.Msg, req api.CLIStepHTTPRequest, result
220332
}
221333
}
222334

335+
func sendJqQueryResults(ch chan tea.Msg, step api.CLIStepJqQuery, result api.JqQueryResult, index int) {
336+
for _, test := range step.Tests {
337+
ch <- messages.StartTestMsg{Text: prettyPrintJqTest(test)}
338+
}
339+
340+
allPassed := true
341+
for j, test := range step.Tests {
342+
passed := evaluateJqTest(test, result)
343+
allPassed = allPassed && passed
344+
ch <- messages.ResolveTestMsg{
345+
StepIndex: index,
346+
TestIndex: j,
347+
Passed: &passed,
348+
}
349+
}
350+
351+
ch <- messages.ResolveStepMsg{
352+
Index: index,
353+
Passed: &allPassed,
354+
Result: &api.CLIStepResult{
355+
JqQueryResult: &result,
356+
},
357+
}
358+
}
359+
223360
func ApplySubmissionResults(cliData api.CLIData, failure *api.VerificationResultStructuredErrCLI, ch chan tea.Msg) {
224361
for i, step := range cliData.Steps {
225362
pass := true
@@ -250,6 +387,15 @@ func ApplySubmissionResults(cliData api.CLIData, failure *api.VerificationResult
250387
}
251388
}
252389
}
390+
if step.JqQuery != nil {
391+
for j := range step.JqQuery.Tests {
392+
ch <- messages.ResolveTestMsg{
393+
StepIndex: i,
394+
TestIndex: j,
395+
Passed: &pass,
396+
}
397+
}
398+
}
253399

254400
if !pass {
255401
break
@@ -258,6 +404,69 @@ func ApplySubmissionResults(cliData api.CLIData, failure *api.VerificationResult
258404
}
259405
}
260406

407+
func evaluateJqTest(test api.JqQueryTest, result api.JqQueryResult) bool {
408+
switch {
409+
case test.ExitCode != nil:
410+
return result.ExitCode == *test.ExitCode
411+
case test.ExpectedBool != nil:
412+
if len(result.Results) == 0 {
413+
return false
414+
}
415+
expected := fmt.Sprintf("%t", *test.ExpectedBool)
416+
return result.Results[0] == expected
417+
case test.ResultsContainAll != nil:
418+
return resultsContainAll(result.Results, test.ResultsContainAll)
419+
case test.ResultsContainNone != nil:
420+
return resultsContainNone(result.Results, test.ResultsContainNone)
421+
default:
422+
return false
423+
}
424+
}
425+
426+
func resultsContainAll(results []string, expected []string) bool {
427+
for _, item := range expected {
428+
if !slices.Contains(results, item) {
429+
return false
430+
}
431+
}
432+
return true
433+
}
434+
435+
func resultsContainNone(results []string, forbidden []string) bool {
436+
for _, item := range forbidden {
437+
if slices.Contains(results, item) {
438+
return false
439+
}
440+
}
441+
return true
442+
}
443+
444+
func prettyPrintJqTest(test api.JqQueryTest) string {
445+
if test.ExitCode != nil {
446+
return fmt.Sprintf("Expect exit code %d", *test.ExitCode)
447+
}
448+
if test.ExpectedBool != nil {
449+
return fmt.Sprintf("Expect first result to be %t", *test.ExpectedBool)
450+
}
451+
if test.ResultsContainAll != nil {
452+
var str strings.Builder
453+
str.WriteString("Expect results to contain all of:")
454+
for _, contains := range test.ResultsContainAll {
455+
fmt.Fprintf(&str, "\n - '%s'", contains)
456+
}
457+
return str.String()
458+
}
459+
if test.ResultsContainNone != nil {
460+
var str strings.Builder
461+
str.WriteString("Expect results to contain none of:")
462+
for _, containsNone := range test.ResultsContainNone {
463+
fmt.Fprintf(&str, "\n - '%s'", containsNone)
464+
}
465+
return str.String()
466+
}
467+
return ""
468+
}
469+
261470
func prettyPrintCLICommand(test api.CLICommandTest, variables map[string]string) string {
262471
if test.ExitCode != nil {
263472
return fmt.Sprintf("Expect exit code %d", *test.ExitCode)
@@ -351,7 +560,7 @@ func truncateAndStringifyBody(body []byte) string {
351560

352561
func parseVariables(body []byte, vardefs []api.HTTPRequestResponseVariable, variables map[string]string) error {
353562
for _, vardef := range vardefs {
354-
val, err := valFromJQPath(vardef.Path, string(body))
563+
val, err := valFromJqPath(vardef.Path, string(body))
355564
if err != nil {
356565
return err
357566
}
@@ -360,8 +569,8 @@ func parseVariables(body []byte, vardefs []api.HTTPRequestResponseVariable, vari
360569
return nil
361570
}
362571

363-
func valFromJQPath(path string, jsn string) (any, error) {
364-
vals, err := valsFromJQPath(path, jsn)
572+
func valFromJqPath(path string, jsn string) (any, error) {
573+
vals, err := valsFromJqPath(path, jsn)
365574
if err != nil {
366575
return nil, err
367576
}
@@ -375,7 +584,7 @@ func valFromJQPath(path string, jsn string) (any, error) {
375584
return val, nil
376585
}
377586

378-
func valsFromJQPath(path string, jsn string) ([]any, error) {
587+
func valsFromJqPath(path string, jsn string) ([]any, error) {
379588
var parseable any
380589
err := json.Unmarshal([]byte(jsn), &parseable)
381590
if err != nil {

client/lessons.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ type CLIData struct {
3030
type CLIStep struct {
3131
CLICommand *CLIStepCLICommand
3232
HTTPRequest *CLIStepHTTPRequest
33+
JqQuery *CLIStepJqQuery
3334
}
3435

3536
type CLIStepCLICommand struct {
@@ -45,6 +46,21 @@ type CLICommandTest struct {
4546
StdoutLinesGt *int
4647
}
4748

49+
type CLIStepJqQuery struct {
50+
FilePath string
51+
InputMode string
52+
Query string
53+
Tests []JqQueryTest
54+
SleepAfterMs *int
55+
}
56+
57+
type JqQueryTest struct {
58+
ExitCode *int
59+
ExpectedBool *bool
60+
ResultsContainAll []string
61+
ResultsContainNone []string
62+
}
63+
4864
type CLIStepHTTPRequest struct {
4965
ResponseVariables []HTTPRequestResponseVariable
5066
Tests []HTTPRequestTest
@@ -64,6 +80,10 @@ func (h *CLIStepHTTPRequest) GetSleepAfterMs() *int {
6480
return h.SleepAfterMs
6581
}
6682

83+
func (j *CLIStepJqQuery) GetSleepAfterMs() *int {
84+
return j.SleepAfterMs
85+
}
86+
6787
const BaseURLPlaceholder = "${baseURL}"
6888

6989
type HTTPRequest struct {
@@ -134,6 +154,7 @@ func FetchLesson(uuid string) (*Lesson, error) {
134154
type CLIStepResult struct {
135155
CLICommandResult *CLICommandResult
136156
HTTPRequestResult *HTTPRequestResult
157+
JqQueryResult *JqQueryResult
137158
}
138159

139160
type CLICommandResult struct {
@@ -143,6 +164,14 @@ type CLICommandResult struct {
143164
Variables map[string]string
144165
}
145166

167+
type JqQueryResult struct {
168+
ExitCode int
169+
Query string `json:"-"`
170+
FilePath string `json:"-"`
171+
Results []string
172+
Stdout string
173+
}
174+
146175
type HTTPRequestResult struct {
147176
Err string `json:"-"`
148177
StatusCode int

render/render.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,21 @@ func (m rootModel) View() string {
236236
}
237237
}
238238

239+
if step.result.JqQueryResult != nil {
240+
for _, test := range step.tests {
241+
if strings.Contains(test.text, "exit code") {
242+
fmt.Fprintf(&str, "\n > jq exit code: %d\n", step.result.JqQueryResult.ExitCode)
243+
break
244+
}
245+
}
246+
str.WriteString(" > jq output:\n\n")
247+
sliced := strings.SplitSeq(step.result.JqQueryResult.Stdout, "\n")
248+
for s := range sliced {
249+
str.WriteString(gray.Render(s))
250+
str.WriteByte('\n')
251+
}
252+
}
253+
239254
if step.result.HTTPRequestResult != nil {
240255
str.WriteString(printHTTPRequestResult(*step.result.HTTPRequestResult))
241256
}

0 commit comments

Comments
 (0)