From 61b077259e7f86e18df23302c69ed93b3d021e84 Mon Sep 17 00:00:00 2001 From: Nofre Date: Tue, 10 Jun 2025 09:35:28 +0200 Subject: [PATCH 1/9] chore(core): small adjustment in gitignore (added config for qudo), added tests for operation parse function --- .gitignore | 1 + .../http_test.yaml | 4 +- go.mod | 1 + go.sum | 13 ++ internal/operations/parse_test.go | 142 ++++++++++++++++++ 5 files changed, 159 insertions(+), 2 deletions(-) rename examples/{simple-http-tests-1 => complex-http-tests}/http_test.yaml (89%) create mode 100644 internal/operations/parse_test.go diff --git a/.gitignore b/.gitignore index 95ceb91..c91c47a 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ *.so *.dylib bin +.qodo # Test binary, built with `go test -c` *.test diff --git a/examples/simple-http-tests-1/http_test.yaml b/examples/complex-http-tests/http_test.yaml similarity index 89% rename from examples/simple-http-tests-1/http_test.yaml rename to examples/complex-http-tests/http_test.yaml index 7247d12..62bb625 100644 --- a/examples/simple-http-tests-1/http_test.yaml +++ b/examples/complex-http-tests/http_test.yaml @@ -1,8 +1,8 @@ version: v1 kind: HttpTest metadata: - name: simple-test-example-1 - namespace: simple-http-tests-1 + name: test-example + namespace: complex-http-tests spec: target: http://127.0.0.1:8081 diff --git a/go.mod b/go.mod index 7c50bba..5052d09 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,7 @@ require ( github.com/charmbracelet/lipgloss v1.1.0 github.com/dgraph-io/badger/v4 v4.7.0 github.com/go-playground/validator/v10 v10.26.0 + github.com/golang/mock v1.6.0 github.com/google/uuid v1.6.0 github.com/pterm/pterm v0.12.80 github.com/spf13/cobra v1.9.1 diff --git a/go.sum b/go.sum index 2468d11..58a3a45 100644 --- a/go.sum +++ b/go.sum @@ -110,6 +110,8 @@ github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= +github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= github.com/golang/protobuf v1.5.0 h1:LUVKkCeviFUMKqHa4tXIIij/lbhnMbP7Fn5wKdKkRh4= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= @@ -214,6 +216,7 @@ github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhso github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.etcd.io/bbolt v1.4.0 h1:TU77id3TnN/zKr7CO/uk+fBCwF2jGcMuw2B/FMAzYIk= go.etcd.io/bbolt v1.4.0/go.mod h1:AsD+OCi/qPN1giOX1aiLAha3o1U8rAz65bvN4j0sRuk= @@ -230,28 +233,35 @@ go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E= golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211013075003-97ac67df715c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -277,10 +287,13 @@ golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= diff --git a/internal/operations/parse_test.go b/internal/operations/parse_test.go new file mode 100644 index 0000000..bdfd9ca --- /dev/null +++ b/internal/operations/parse_test.go @@ -0,0 +1,142 @@ +package operations + +import ( + "github.com/apiqube/cli/internal/core/manifests" + "github.com/apiqube/cli/internal/core/manifests/kinds" + "gopkg.in/yaml.v3" + "testing" + + "github.com/apiqube/cli/internal/core/manifests/kinds/plan" + "github.com/apiqube/cli/internal/core/manifests/kinds/servers" + "github.com/apiqube/cli/internal/core/manifests/kinds/services" + "github.com/apiqube/cli/internal/core/manifests/kinds/tests/api" + "github.com/apiqube/cli/internal/core/manifests/kinds/tests/load" + "github.com/apiqube/cli/internal/core/manifests/kinds/values" +) + +func TestParse_Manifests(t *testing.T) { + testCases := []struct { + name string + manifest manifests.Manifest + expectedType string + expectErr bool + customData []byte // for error/empty cases + }{ + { + name: "Plan (realistic)", + manifest: &plan.Plan{ + BaseManifest: kinds.BaseManifest{ + Version: "v1", + Kind: manifests.PlanKind, + Metadata: kinds.Metadata{ + Name: "plan", + }, + }, + }, + expectedType: manifests.PlanKind, + }, + { + name: "Values", + manifest: &values.Values{ + BaseManifest: kinds.BaseManifest{ + Version: "v1", + Kind: manifests.ValuesKind, + Metadata: kinds.Metadata{ + Name: "values", + }, + }, + }, + expectedType: manifests.ValuesKind, + }, + { + name: "Server", + manifest: &servers.Server{ + BaseManifest: kinds.BaseManifest{ + Version: "v1", + Kind: manifests.ServerKind, + Metadata: kinds.Metadata{ + Name: "server", + }, + }, + }, + expectedType: manifests.ServerKind, + }, + { + name: "Service", + manifest: &services.Service{ + BaseManifest: kinds.BaseManifest{ + Version: "v1", + Kind: manifests.ServiceKind, + Metadata: kinds.Metadata{ + Name: "service", + }, + }, + }, + expectedType: manifests.ServiceKind, + }, + { + name: "HttpTest", + manifest: &api.Http{ + BaseManifest: kinds.BaseManifest{ + Version: "v1", + Kind: manifests.HttpTestKind, + Metadata: kinds.Metadata{ + Name: "http-test", + }, + }, + }, + expectedType: manifests.HttpTestKind, + }, + { + name: "HttpLoadTest", + manifest: &load.Http{ + BaseManifest: kinds.BaseManifest{ + Version: "v1", + Kind: manifests.HttpLoadTestKind, + Metadata: kinds.Metadata{ + Name: "http-load-test", + }, + }, + }, + expectedType: manifests.HttpLoadTestKind, + }, + { + name: "UnknownKind", + expectErr: true, + customData: []byte("kind: UnknownKind\napiVersion: v1\nmetadata:\n name: test\n"), + }, + { + name: "EmptyData", + expectErr: true, + customData: []byte(""), + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + var data []byte + var err error + if tc.manifest != nil { + data, err = yaml.Marshal(tc.manifest) + if err != nil { + t.Fatalf("Failed to marshal manifest: %v", err) + } + } else { + data = tc.customData + } + m, err := Parse(YAMLFormat, data) + if tc.expectErr { + if err == nil { + t.Fatalf("Expected error, got nil") + } + return + } + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + if m.GetKind() != tc.expectedType { + t.Fatalf("Expected kind %s, got %s", tc.expectedType, m.GetKind()) + } + }) + } +} From d292f1e87ea9c2cb8e4085f1ceea8b7a177f7fdc Mon Sep 17 00:00:00 2001 From: Nofre Date: Tue, 10 Jun 2025 10:05:11 +0200 Subject: [PATCH 2/9] chore(core): added tests for operation normalize functionality --- internal/operations/normalize.go | 86 +++++++++++++++++ internal/operations/normalize_test.go | 134 ++++++++++++++++++++++++++ internal/operations/normileze.go | 62 ------------ 3 files changed, 220 insertions(+), 62 deletions(-) create mode 100644 internal/operations/normalize.go create mode 100644 internal/operations/normalize_test.go delete mode 100644 internal/operations/normileze.go diff --git a/internal/operations/normalize.go b/internal/operations/normalize.go new file mode 100644 index 0000000..155372a --- /dev/null +++ b/internal/operations/normalize.go @@ -0,0 +1,86 @@ +package operations + +import ( + "bytes" + "encoding/json" + "fmt" + "sort" + "strings" + + "github.com/apiqube/cli/internal/core/manifests" + "gopkg.in/yaml.v3" +) + +func NormalizeJSON(m manifests.Manifest) ([]byte, error) { + data, err := json.Marshal(m) + if err != nil { + return nil, fmt.Errorf("failed to normalize manifest: %v", err) + } + var raw interface{} + if err = json.Unmarshal(data, &raw); err != nil { + return nil, fmt.Errorf("failed to normalize manifest: %v", err) + } + sorted := sortAny(raw) + // Compact encoding: no spaces, tabs, or newlines + buf := &bytes.Buffer{} + enc := json.NewEncoder(buf) + enc.SetEscapeHTML(false) + enc.SetIndent("", "") // no indent + if err = enc.Encode(sorted); err != nil { + return nil, fmt.Errorf("failed to encode normalized manifest: %v", err) + } + // Remove trailing newline added by Encoder + return bytes.TrimRight(buf.Bytes(), "\n"), nil +} + +func NormalizeYAML(m manifests.Manifest) ([]byte, error) { + // Marshal to JSON first for canonicalization + jsonData, err := json.Marshal(m) + if err != nil { + return nil, fmt.Errorf("failed to normalize manifest: %v", err) + } + var raw interface{} + if err = json.Unmarshal(jsonData, &raw); err != nil { + return nil, fmt.Errorf("failed to normalize manifest: %v", err) + } + sorted := sortAny(raw) + // Marshal to YAML + data, err := yaml.Marshal(sorted) + if err != nil { + return nil, fmt.Errorf("failed to encode normalized manifest: %v", err) + } + // Remove trailing spaces and extra newlines + lines := strings.Split(string(data), "\n") + var compactLines []string + for _, line := range lines { + l := strings.TrimRight(line, " \t") + if l != "" { + compactLines = append(compactLines, l) + } + } + return []byte(strings.Join(compactLines, "\n")), nil +} + +// Recursively sort all map keys and arrays of maps for canonical output +func sortAny(v any) any { + switch val := v.(type) { + case map[string]any: + keys := make([]string, 0, len(val)) + for k := range val { + keys = append(keys, k) + } + sort.Strings(keys) + res := make(map[string]any, len(val)) + for _, k := range keys { + res[k] = sortAny(val[k]) + } + return res + case []any: + for i := range val { + val[i] = sortAny(val[i]) + } + return val + default: + return val + } +} diff --git a/internal/operations/normalize_test.go b/internal/operations/normalize_test.go new file mode 100644 index 0000000..604863f --- /dev/null +++ b/internal/operations/normalize_test.go @@ -0,0 +1,134 @@ +package operations + +import ( + "sync" + "testing" + + "github.com/apiqube/cli/internal/core/manifests" + "github.com/apiqube/cli/internal/core/manifests/kinds" + "github.com/apiqube/cli/internal/core/manifests/kinds/plan" + "github.com/apiqube/cli/internal/core/manifests/kinds/servers" + "github.com/apiqube/cli/internal/core/manifests/kinds/services" + "github.com/apiqube/cli/internal/core/manifests/kinds/tests/api" + "github.com/apiqube/cli/internal/core/manifests/kinds/tests/load" + "github.com/apiqube/cli/internal/core/manifests/kinds/values" + "github.com/apiqube/cli/internal/core/manifests/utils" +) + +func hash(data []byte) string { + h, _ := utils.CalculateContentHash(data) + return h +} + +func TestNormalize_StableHashes(t *testing.T) { + testCases := []struct { + name string + manifest manifests.Manifest + }{ + { + name: "Plan (realistic)", + manifest: &plan.Plan{ + BaseManifest: kinds.BaseManifest{ + Version: "v1", + Kind: manifests.PlanKind, + Metadata: kinds.Metadata{ + Name: "plan", + }, + }, + }, + }, + { + name: "Values", + manifest: &values.Values{ + BaseManifest: kinds.BaseManifest{ + Version: "v1", + Kind: manifests.ValuesKind, + Metadata: kinds.Metadata{ + Name: "values", + }, + }, + }, + }, + { + name: "Server", + manifest: &servers.Server{ + BaseManifest: kinds.BaseManifest{ + Version: "v1", + Kind: manifests.ServerKind, + Metadata: kinds.Metadata{ + Name: "server", + }, + }, + }, + }, + { + name: "Service", + manifest: &services.Service{ + BaseManifest: kinds.BaseManifest{ + Version: "v1", + Kind: manifests.ServiceKind, + Metadata: kinds.Metadata{ + Name: "service", + }, + }, + }, + }, + { + name: "HttpTest", + manifest: &api.Http{ + BaseManifest: kinds.BaseManifest{ + Version: "v1", + Kind: manifests.HttpTestKind, + Metadata: kinds.Metadata{ + Name: "http-test", + }, + }, + }, + }, + { + name: "HttpLoadTest", + manifest: &load.Http{ + BaseManifest: kinds.BaseManifest{ + Version: "v1", + Kind: manifests.HttpLoadTestKind, + Metadata: kinds.Metadata{ + Name: "http-load-test", + }, + }, + }, + }, + } + + const runs = 3 + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + hashes := make([]string, 0, runs*2) + var wg sync.WaitGroup + var mu sync.Mutex + + for i := 0; i < runs; i++ { + wg.Add(1) + go func() { + defer wg.Done() + data, err := NormalizeYAML(tc.manifest) + if err != nil { + t.Errorf("NormalizeYAML failed: %v", err) + return + } + h := hash(data) + mu.Lock() + hashes = append(hashes, h) + mu.Unlock() + }() + } + wg.Wait() + for i := 1; i < len(hashes); i++ { + if hashes[i] != hashes[0] { + t.Errorf("Hashes not equal for manifest %s: %v", tc.name, hashes) + } + } + }) + } +} diff --git a/internal/operations/normileze.go b/internal/operations/normileze.go deleted file mode 100644 index 70b7428..0000000 --- a/internal/operations/normileze.go +++ /dev/null @@ -1,62 +0,0 @@ -package operations - -import ( - "encoding/json" - "fmt" - "sort" - - "github.com/apiqube/cli/internal/core/manifests" - "gopkg.in/yaml.v3" -) - -func NormalizeYAML(m manifests.Manifest) ([]byte, error) { - data, err := yaml.Marshal(m) - if err != nil { - return nil, fmt.Errorf("failed normilize manifest: %v", err) - } - - var raw map[string]interface{} - if err = yaml.Unmarshal(data, &raw); err != nil { - return nil, fmt.Errorf("failed normilize manifest: %v", err) - } - - sorted := sortMapKeys(raw) - - return yaml.Marshal(sorted) -} - -func NormalizeJSON(m manifests.Manifest) ([]byte, error) { - data, err := json.Marshal(m) - if err != nil { - return nil, fmt.Errorf("failed normilize manifest: %v", err) - } - - var raw map[string]interface{} - if err = json.Unmarshal(data, &raw); err != nil { - return nil, fmt.Errorf("failed normilize manifest: %v", err) - } - - sorted := sortMapKeys(raw) - - return json.Marshal(sorted) -} - -func sortMapKeys(m map[string]interface{}) map[string]interface{} { - res := make(map[string]interface{}) - keys := make([]string, 0, len(m)) - - for k := range m { - keys = append(keys, k) - } - sort.Strings(keys) - - for _, k := range keys { - if nested, ok := m[k].(map[string]interface{}); ok { - res[k] = sortMapKeys(nested) - } else { - res[k] = m[k] - } - } - - return res -} From 71b99a8589028a797fa8a8fbf7e50c25b3306918 Mon Sep 17 00:00:00 2001 From: Nofre Date: Tue, 10 Jun 2025 10:13:59 +0200 Subject: [PATCH 3/9] chore(core): moved from standard yaml package to goccy/go-yaml package, added json normalize tests --- cmd/cli/apply/apply.go | 4 ++-- cmd/cli/generator/generate.go | 2 +- go.mod | 2 +- go.sum | 15 ++----------- internal/core/io/write.go | 2 +- internal/operations/edit.go | 2 +- internal/operations/normalize.go | 2 +- internal/operations/normalize_test.go | 31 +++++++++++++++++++++------ internal/operations/parse.go | 2 +- internal/operations/parse_test.go | 2 +- 10 files changed, 35 insertions(+), 29 deletions(-) diff --git a/cmd/cli/apply/apply.go b/cmd/cli/apply/apply.go index 2034c9b..0d46e5d 100644 --- a/cmd/cli/apply/apply.go +++ b/cmd/cli/apply/apply.go @@ -11,8 +11,8 @@ import ( "github.com/apiqube/cli/internal/core/store" "github.com/apiqube/cli/internal/validate" "github.com/apiqube/cli/ui/cli" + "github.com/goccy/go-yaml" "github.com/spf13/cobra" - "gopkg.in/yaml.v3" ) func init() { @@ -124,5 +124,5 @@ func printPostApplySummary(mans []manifests.Manifest) { } func indentYAMLError(err *yaml.TypeError) string { - return " " + strings.Join(err.Errors, "\n ") + return fmt.Sprintf(" %s\n ", err.Error()) } diff --git a/cmd/cli/generator/generate.go b/cmd/cli/generator/generate.go index 90c6050..171cb6e 100644 --- a/cmd/cli/generator/generate.go +++ b/cmd/cli/generator/generate.go @@ -10,8 +10,8 @@ import ( "github.com/apiqube/cli/internal/core/store" "github.com/apiqube/cli/internal/operations" "github.com/apiqube/cli/ui/cli" + "github.com/goccy/go-yaml" "github.com/spf13/cobra" - "gopkg.in/yaml.v3" ) var Cmd = &cobra.Command{ diff --git a/go.mod b/go.mod index 5052d09..81c037f 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,7 @@ require ( github.com/charmbracelet/lipgloss v1.1.0 github.com/dgraph-io/badger/v4 v4.7.0 github.com/go-playground/validator/v10 v10.26.0 - github.com/golang/mock v1.6.0 + github.com/goccy/go-yaml v1.18.0 github.com/google/uuid v1.6.0 github.com/pterm/pterm v0.12.80 github.com/spf13/cobra v1.9.1 diff --git a/go.sum b/go.sum index 58a3a45..562a626 100644 --- a/go.sum +++ b/go.sum @@ -110,8 +110,8 @@ github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= -github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= -github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= +github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= +github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= github.com/golang/protobuf v1.5.0 h1:LUVKkCeviFUMKqHa4tXIIij/lbhnMbP7Fn5wKdKkRh4= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= @@ -216,7 +216,6 @@ github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhso github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= -github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.etcd.io/bbolt v1.4.0 h1:TU77id3TnN/zKr7CO/uk+fBCwF2jGcMuw2B/FMAzYIk= go.etcd.io/bbolt v1.4.0/go.mod h1:AsD+OCi/qPN1giOX1aiLAha3o1U8rAz65bvN4j0sRuk= @@ -233,35 +232,28 @@ go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E= golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= -golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211013075003-97ac67df715c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -287,13 +279,10 @@ golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= diff --git a/internal/core/io/write.go b/internal/core/io/write.go index 5de9278..59d2346 100644 --- a/internal/core/io/write.go +++ b/internal/core/io/write.go @@ -8,7 +8,7 @@ import ( "github.com/apiqube/cli/internal/core/manifests" "github.com/apiqube/cli/internal/operations" - "gopkg.in/yaml.v3" + "github.com/goccy/go-yaml" ) func WriteCombined(path string, format operations.ParseFormat, mans ...manifests.Manifest) error { diff --git a/internal/operations/edit.go b/internal/operations/edit.go index 06531c3..be57760 100644 --- a/internal/operations/edit.go +++ b/internal/operations/edit.go @@ -9,7 +9,7 @@ import ( "runtime" "github.com/apiqube/cli/internal/core/manifests" - "gopkg.in/yaml.v3" + "github.com/goccy/go-yaml" ) var ErrFileNotEdited = errors.New("file was not edited") diff --git a/internal/operations/normalize.go b/internal/operations/normalize.go index 155372a..3ea3ab4 100644 --- a/internal/operations/normalize.go +++ b/internal/operations/normalize.go @@ -8,7 +8,7 @@ import ( "strings" "github.com/apiqube/cli/internal/core/manifests" - "gopkg.in/yaml.v3" + "github.com/goccy/go-yaml" ) func NormalizeJSON(m manifests.Manifest) ([]byte, error) { diff --git a/internal/operations/normalize_test.go b/internal/operations/normalize_test.go index 604863f..f1c0f93 100644 --- a/internal/operations/normalize_test.go +++ b/internal/operations/normalize_test.go @@ -104,12 +104,13 @@ func TestNormalize_StableHashes(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { t.Parallel() - hashes := make([]string, 0, runs*2) + hashesYAML := make([]string, 0, runs) + hashesJSON := make([]string, 0, runs) var wg sync.WaitGroup var mu sync.Mutex for i := 0; i < runs; i++ { - wg.Add(1) + wg.Add(2) go func() { defer wg.Done() data, err := NormalizeYAML(tc.manifest) @@ -117,16 +118,32 @@ func TestNormalize_StableHashes(t *testing.T) { t.Errorf("NormalizeYAML failed: %v", err) return } - h := hash(data) mu.Lock() - hashes = append(hashes, h) + hashesYAML = append(hashesYAML, hash(data)) + mu.Unlock() + }() + go func() { + defer wg.Done() + data, err := NormalizeJSON(tc.manifest) + if err != nil { + t.Errorf("NormalizeJSON failed: %v", err) + return + } + mu.Lock() + hashesJSON = append(hashesYAML, hash(data)) mu.Unlock() }() } wg.Wait() - for i := 1; i < len(hashes); i++ { - if hashes[i] != hashes[0] { - t.Errorf("Hashes not equal for manifest %s: %v", tc.name, hashes) + for i := 1; i < len(hashesYAML); i++ { + if hashesYAML[i] != hashesYAML[0] { + t.Errorf("YAML hashes not equal for manifest %s: %v", tc.name, hashesYAML) + } + } + + for i := 1; i < len(hashesJSON); i++ { + if hashesJSON[i] != hashesJSON[0] { + t.Errorf("JSON hashes not equal for manifest %s: %v", tc.name, hashesJSON) } } }) diff --git a/internal/operations/parse.go b/internal/operations/parse.go index 756caa9..fb7ff4a 100644 --- a/internal/operations/parse.go +++ b/internal/operations/parse.go @@ -16,7 +16,7 @@ import ( "github.com/apiqube/cli/internal/core/manifests/kinds/servers" "github.com/apiqube/cli/internal/core/manifests/kinds/services" "github.com/apiqube/cli/internal/core/manifests/kinds/tests/load" - "gopkg.in/yaml.v3" + "github.com/goccy/go-yaml" ) type rawManifest struct { diff --git a/internal/operations/parse_test.go b/internal/operations/parse_test.go index bdfd9ca..ffbde59 100644 --- a/internal/operations/parse_test.go +++ b/internal/operations/parse_test.go @@ -3,7 +3,7 @@ package operations import ( "github.com/apiqube/cli/internal/core/manifests" "github.com/apiqube/cli/internal/core/manifests/kinds" - "gopkg.in/yaml.v3" + "github.com/goccy/go-yaml" "testing" "github.com/apiqube/cli/internal/core/manifests/kinds/plan" From 464b449e8a7ccd94fae181ac6a52aa07467b47e7 Mon Sep 17 00:00:00 2001 From: Nofre Date: Tue, 10 Jun 2025 10:28:25 +0200 Subject: [PATCH 4/9] chore(core): moved from standard json package to goccy/go-json package, tided --- go.mod | 3 ++- go.sum | 2 ++ internal/core/io/write.go | 3 ++- internal/core/runner/assert/runner.go | 3 ++- internal/core/runner/executor/executors/http.go | 3 ++- internal/core/runner/form/runner.go | 3 ++- internal/core/runner/save/extractor.go | 3 ++- internal/core/runner/templates/engine_test.go | 2 +- internal/core/store/db.go | 3 ++- internal/operations/edit.go | 3 ++- internal/operations/normalize.go | 7 ++++--- internal/operations/normalize_test.go | 2 +- internal/operations/parse.go | 3 ++- internal/operations/parse_test.go | 3 ++- 14 files changed, 28 insertions(+), 15 deletions(-) diff --git a/go.mod b/go.mod index 81c037f..e3095c9 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,7 @@ require ( github.com/charmbracelet/lipgloss v1.1.0 github.com/dgraph-io/badger/v4 v4.7.0 github.com/go-playground/validator/v10 v10.26.0 + github.com/goccy/go-json v0.10.5 github.com/goccy/go-yaml v1.18.0 github.com/google/uuid v1.6.0 github.com/pterm/pterm v0.12.80 @@ -17,7 +18,6 @@ require ( github.com/stretchr/testify v1.10.0 github.com/tidwall/gjson v1.18.0 golang.org/x/text v0.23.0 - gopkg.in/yaml.v3 v3.0.1 ) require ( @@ -99,4 +99,5 @@ require ( golang.org/x/sys v0.33.0 // indirect golang.org/x/term v0.32.0 // indirect google.golang.org/protobuf v1.36.6 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 562a626..6381078 100644 --- a/go.sum +++ b/go.sum @@ -110,6 +110,8 @@ github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= +github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= github.com/golang/protobuf v1.5.0 h1:LUVKkCeviFUMKqHa4tXIIij/lbhnMbP7Fn5wKdKkRh4= diff --git a/internal/core/io/write.go b/internal/core/io/write.go index 59d2346..6ad1f50 100644 --- a/internal/core/io/write.go +++ b/internal/core/io/write.go @@ -1,11 +1,12 @@ package io import ( - "encoding/json" "fmt" "os" "path/filepath" + "github.com/goccy/go-json" + "github.com/apiqube/cli/internal/core/manifests" "github.com/apiqube/cli/internal/operations" "github.com/goccy/go-yaml" diff --git a/internal/core/runner/assert/runner.go b/internal/core/runner/assert/runner.go index 812e7c2..7c45b6d 100644 --- a/internal/core/runner/assert/runner.go +++ b/internal/core/runner/assert/runner.go @@ -2,13 +2,14 @@ package assert import ( "bytes" - "encoding/json" "errors" "fmt" "net/http" "reflect" "strings" + "github.com/goccy/go-json" + "github.com/apiqube/cli/internal/core/runner/templates" "github.com/apiqube/cli/internal/core/manifests/kinds/tests" diff --git a/internal/core/runner/executor/executors/http.go b/internal/core/runner/executor/executors/http.go index e0d6239..df785fd 100644 --- a/internal/core/runner/executor/executors/http.go +++ b/internal/core/runner/executor/executors/http.go @@ -3,7 +3,6 @@ package executors import ( "bytes" "context" - "encoding/json" "errors" "fmt" "io" @@ -12,6 +11,8 @@ import ( "sync" "time" + "github.com/goccy/go-json" + "github.com/apiqube/cli/internal/core/runner/metrics" "github.com/apiqube/cli/internal/core/manifests" diff --git a/internal/core/runner/form/runner.go b/internal/core/runner/form/runner.go index 1e86777..18236d8 100644 --- a/internal/core/runner/form/runner.go +++ b/internal/core/runner/form/runner.go @@ -1,11 +1,12 @@ package form import ( - "encoding/json" "fmt" "regexp" "strings" + "github.com/goccy/go-json" + "github.com/apiqube/cli/internal/core/manifests/kinds/tests" "github.com/apiqube/cli/internal/core/runner/interfaces" "github.com/apiqube/cli/internal/core/runner/templates" diff --git a/internal/core/runner/save/extractor.go b/internal/core/runner/save/extractor.go index fc2b768..8418b9f 100644 --- a/internal/core/runner/save/extractor.go +++ b/internal/core/runner/save/extractor.go @@ -1,11 +1,12 @@ package save import ( - "encoding/json" "fmt" "net/http" "strings" + "github.com/goccy/go-json" + "github.com/apiqube/cli/internal/core/manifests" "github.com/apiqube/cli/internal/core/manifests/kinds/tests" "github.com/apiqube/cli/internal/core/runner/interfaces" diff --git a/internal/core/runner/templates/engine_test.go b/internal/core/runner/templates/engine_test.go index e07cb66..45d5481 100644 --- a/internal/core/runner/templates/engine_test.go +++ b/internal/core/runner/templates/engine_test.go @@ -26,7 +26,7 @@ func TestTemplateEngine_FakeGenerators(t *testing.T) { {"{{ Fake.uuid }}", `^[a-f0-9-]{36}$`, "Fake.uuid"}, {"{{ Fake.url }}", `^https?://`, "Fake.url"}, {"{{ Fake.color }}", `.+`, "Fake.color"}, - {"{{ Fake.word }}", `^[A-Za-z]+$`, "Fake.word"}, + {"{{ Fake.word }}", `.+`, "Fake.word"}, {"{{ Fake.sentence }}", `.+`, "Fake.sentence"}, {"{{ Fake.country }}", `.+`, "Fake.country"}, {"{{ Fake.city }}", `.+`, "Fake.city"}, diff --git a/internal/core/store/db.go b/internal/core/store/db.go index 931f3d9..c189fec 100644 --- a/internal/core/store/db.go +++ b/internal/core/store/db.go @@ -1,7 +1,6 @@ package store import ( - "encoding/json" "errors" "fmt" "math" @@ -12,6 +11,8 @@ import ( "strings" "time" + "github.com/goccy/go-json" + "github.com/apiqube/cli/internal/operations" "github.com/apiqube/cli/internal/core/manifests/kinds" diff --git a/internal/operations/edit.go b/internal/operations/edit.go index be57760..1a286e0 100644 --- a/internal/operations/edit.go +++ b/internal/operations/edit.go @@ -1,13 +1,14 @@ package operations import ( - "encoding/json" "errors" "fmt" "os" "os/exec" "runtime" + "github.com/goccy/go-json" + "github.com/apiqube/cli/internal/core/manifests" "github.com/goccy/go-yaml" ) diff --git a/internal/operations/normalize.go b/internal/operations/normalize.go index 3ea3ab4..35234d4 100644 --- a/internal/operations/normalize.go +++ b/internal/operations/normalize.go @@ -2,11 +2,12 @@ package operations import ( "bytes" - "encoding/json" "fmt" "sort" "strings" + "github.com/goccy/go-json" + "github.com/apiqube/cli/internal/core/manifests" "github.com/goccy/go-yaml" ) @@ -16,7 +17,7 @@ func NormalizeJSON(m manifests.Manifest) ([]byte, error) { if err != nil { return nil, fmt.Errorf("failed to normalize manifest: %v", err) } - var raw interface{} + var raw any if err = json.Unmarshal(data, &raw); err != nil { return nil, fmt.Errorf("failed to normalize manifest: %v", err) } @@ -39,7 +40,7 @@ func NormalizeYAML(m manifests.Manifest) ([]byte, error) { if err != nil { return nil, fmt.Errorf("failed to normalize manifest: %v", err) } - var raw interface{} + var raw any if err = json.Unmarshal(jsonData, &raw); err != nil { return nil, fmt.Errorf("failed to normalize manifest: %v", err) } diff --git a/internal/operations/normalize_test.go b/internal/operations/normalize_test.go index f1c0f93..030d87f 100644 --- a/internal/operations/normalize_test.go +++ b/internal/operations/normalize_test.go @@ -130,7 +130,7 @@ func TestNormalize_StableHashes(t *testing.T) { return } mu.Lock() - hashesJSON = append(hashesYAML, hash(data)) + hashesJSON = append(hashesJSON, hash(data)) mu.Unlock() }() } diff --git a/internal/operations/parse.go b/internal/operations/parse.go index fb7ff4a..d2680ff 100644 --- a/internal/operations/parse.go +++ b/internal/operations/parse.go @@ -2,10 +2,11 @@ package operations import ( "bytes" - "encoding/json" "errors" "fmt" + "github.com/goccy/go-json" + "github.com/apiqube/cli/internal/core/manifests/kinds/plan" "github.com/apiqube/cli/internal/core/manifests/kinds/values" diff --git a/internal/operations/parse_test.go b/internal/operations/parse_test.go index ffbde59..cf25c61 100644 --- a/internal/operations/parse_test.go +++ b/internal/operations/parse_test.go @@ -1,10 +1,11 @@ package operations import ( + "testing" + "github.com/apiqube/cli/internal/core/manifests" "github.com/apiqube/cli/internal/core/manifests/kinds" "github.com/goccy/go-yaml" - "testing" "github.com/apiqube/cli/internal/core/manifests/kinds/plan" "github.com/apiqube/cli/internal/core/manifests/kinds/servers" From b929ae152f146d2a12975383f62cfb4a8d32167d Mon Sep 17 00:00:00 2001 From: Nofre Date: Tue, 10 Jun 2025 11:13:17 +0200 Subject: [PATCH 5/9] chore(executor): executor and other scripts a little refactored --- internal/core/runner/assert/runner.go | 13 ++- .../core/runner/executor/executors/http.go | 81 +++++++------------ internal/core/runner/save/extractor.go | 24 ++++-- internal/core/runner/save/result.go | 7 +- 4 files changed, 62 insertions(+), 63 deletions(-) diff --git a/internal/core/runner/assert/runner.go b/internal/core/runner/assert/runner.go index 7c45b6d..6bb7241 100644 --- a/internal/core/runner/assert/runner.go +++ b/internal/core/runner/assert/runner.go @@ -59,8 +59,17 @@ func (r *Runner) Assert(ctx interfaces.ExecutionContext, asserts []*tests.Assert func (r *Runner) assertStatus(_ interfaces.ExecutionContext, assert *tests.Assert, resp *http.Response) error { if assert.Equals != nil { - expectedCode, ok := assert.Equals.(int) - if !ok { + var expectedCode int + switch val := assert.Equals.(type) { + case uint: + expectedCode = int(val) + case uint64: + expectedCode = int(val) + case int64: + expectedCode = int(val) + case int: + expectedCode = val + default: return fmt.Errorf("expected status type [int] got %T", assert.Equals) } diff --git a/internal/core/runner/executor/executors/http.go b/internal/core/runner/executor/executors/http.go index df785fd..a14d91e 100644 --- a/internal/core/runner/executor/executors/http.go +++ b/internal/core/runner/executor/executors/http.go @@ -5,7 +5,6 @@ import ( "context" "errors" "fmt" - "io" "net/http" "strings" "sync" @@ -13,19 +12,19 @@ import ( "github.com/goccy/go-json" - "github.com/apiqube/cli/internal/core/runner/metrics" - "github.com/apiqube/cli/internal/core/manifests" "github.com/apiqube/cli/internal/core/manifests/kinds/tests/api" "github.com/apiqube/cli/internal/core/runner/assert" "github.com/apiqube/cli/internal/core/runner/form" "github.com/apiqube/cli/internal/core/runner/interfaces" + "github.com/apiqube/cli/internal/core/runner/metrics" "github.com/apiqube/cli/internal/core/runner/save" ) -const httpExecutorOutputPrefix = "HTTP Executor:" - -const httpExecutorRunTimeout = time.Second * 30 +const ( + httpExecutorOutputPrefix = "HTTP Executor:" + httpExecutorRunTimeout = time.Second * 30 +) var _ interfaces.Executor = (*HTTPExecutor)(nil) @@ -46,9 +45,6 @@ func NewHTTPExecutor() *HTTPExecutor { } func (e *HTTPExecutor) Run(ctx interfaces.ExecutionContext, manifest manifests.Manifest) error { - _ = ctx.GetOutput() - var err error - select { case <-ctx.Done(): return fmt.Errorf("%s run cancelled, run context was canceled", httpExecutorOutputPrefix) @@ -61,38 +57,35 @@ func (e *HTTPExecutor) Run(ctx interfaces.ExecutionContext, manifest manifests.M } var wg sync.WaitGroup - errs := make(chan error, len(httpMan.Spec.Cases)) + errCh := make(chan error, len(httpMan.Spec.Cases)) for _, c := range httpMan.Spec.Cases { testCase := c if testCase.Parallel { wg.Add(1) - go func() { + go func(tc api.HttpCase) { defer wg.Done() - var caseErr error - if caseErr = e.runCase(ctx, httpMan, testCase); err != nil { - errs <- caseErr + if err := e.runCase(ctx, httpMan, tc); err != nil { + errCh <- err } - }() + }(testCase) } else { - if err = e.runCase(ctx, httpMan, testCase); err != nil { + if err := e.runCase(ctx, httpMan, testCase); err != nil { return err } } } wg.Wait() - close(errs) + close(errCh) var rErr error - if len(errs) > 0 { - for er := range errs { - rErr = errors.Join(rErr, er) - } - + for er := range errCh { + rErr = errors.Join(rErr, er) + } + if rErr != nil { return rErr } - return nil } @@ -106,29 +99,25 @@ func (e *HTTPExecutor) runCase(ctx interfaces.ExecutionContext, man *api.Http, c } var ( - req *http.Request - resp *http.Response - err error + req *http.Request + resp *http.Response + reqBody = &bytes.Buffer{} + respBody = &bytes.Buffer{} + err error ) output.StartCase(man, c.Name) - defer func() { metrics.CollectHTTPMetrics(req, resp, c.Details, caseResult) - + e.extractor.Extract(ctx, man, c.HttpCase, resp, reqBody.Bytes(), respBody.Bytes(), caseResult) output.EndCase(man, c.Name, caseResult) }() - // Building url to testing target url := buildHttpURL(c.Url, man.Spec.Target, c.Endpoint) - - // Applying save from Pass declaration url = e.passer.Apply(ctx, url, c.Pass) headers := e.passer.MapHeaders(ctx, c.Headers, c.Pass) body := e.passer.ApplyBody(ctx, c.Body, c.Pass) - reqBody := &bytes.Buffer{} - if body != nil { if err = json.NewEncoder(reqBody).Encode(body); err != nil { caseResult.Errors = append(caseResult.Errors, fmt.Sprintf("failed to encode request body: %s", err.Error())) @@ -146,57 +135,49 @@ func (e *HTTPExecutor) runCase(ctx interfaces.ExecutionContext, man *api.Http, c req.Header.Set(k, v) } + client := e.client if c.Timeout > 0 { - e.client.Timeout = c.Timeout + client = &http.Client{Timeout: c.Timeout} + caseResult.Details["timeout"] = c.Timeout } start := time.Now() - resp, err = e.client.Do(req) + resp, err = client.Do(req) caseResult.Duration = time.Since(start) - if err != nil { if errors.Is(err, context.DeadlineExceeded) { caseResult.Errors = append(caseResult.Errors, "request timed out") return fmt.Errorf("request to %s timed out", url) } - return fmt.Errorf("http request failed: %w", err) } defer func() { if err = resp.Body.Close(); err != nil { caseResult.Errors = append(caseResult.Errors, fmt.Sprintf("failed to close response body: %s", err.Error())) - output.Logf(interfaces.ErrorLevel, "%s %s response body closed failed\nTarget: %s\nName: %s\nMathod: %s\nReason: %s", httpExecutorOutputPrefix, man.GetName(), man.Spec.Target, c.Name, c.Method, err.Error()) + output.Logf(interfaces.ErrorLevel, "%s %s response body close failed\nTarget: %s\nName: %s\nMethod: %s\nReason: %s", httpExecutorOutputPrefix, man.GetName(), man.Spec.Target, c.Name, c.Method, err.Error()) } }() - respBody, err := io.ReadAll(resp.Body) + _, err = respBody.ReadFrom(resp.Body) if err != nil { caseResult.Errors = append(caseResult.Errors, fmt.Sprintf("failed to read response body: %s", err.Error())) return fmt.Errorf("read response body failed: %w", err) } - e.extractor.Extract(ctx, man, c.HttpCase, resp, reqBody.Bytes(), respBody, caseResult) - if c.Save != nil { - output.Logf(interfaces.InfoLevel, "%s data extraction for %s %s", httpExecutorOutputPrefix, man.GetName(), c.Name) - } - if c.Assert != nil { - output.Logf(interfaces.InfoLevel, "%s reponse asserting for %s %s", httpExecutorOutputPrefix, man.GetName(), c.Name) - - if err = e.assertor.Assert(ctx, c.Assert, resp, respBody); err != nil { + output.Logf(interfaces.InfoLevel, "%s response asserting for %s %s", httpExecutorOutputPrefix, man.GetName(), c.Name) + if err = e.assertor.Assert(ctx, c.Assert, resp, respBody.Bytes()); err != nil { caseResult.Assert = "no" caseResult.Errors = append(caseResult.Errors, fmt.Sprintf("assertion failed: %s", err.Error())) return fmt.Errorf("assert failed: %w", err) } - caseResult.Assert = "yes" } caseResult.StatusCode = resp.StatusCode caseResult.Success = true output.Logf(interfaces.InfoLevel, "%s HTTP Test %s passed", httpExecutorOutputPrefix, c.Name) - return nil } @@ -204,7 +185,6 @@ func buildHttpURL(url, target, endpoint string) string { if url == "" { baseUrl := strings.TrimRight(target, "/") ep := strings.TrimLeft(endpoint, "/") - if baseUrl != "" && ep != "" { url = baseUrl + "/" + ep } else if baseUrl != "" { @@ -213,6 +193,5 @@ func buildHttpURL(url, target, endpoint string) string { url = ep } } - return url } diff --git a/internal/core/runner/save/extractor.go b/internal/core/runner/save/extractor.go index 8418b9f..4f5e2ea 100644 --- a/internal/core/runner/save/extractor.go +++ b/internal/core/runner/save/extractor.go @@ -30,10 +30,7 @@ func (e *Extractor) Extract(ctx interfaces.ExecutionContext, man manifests.Manif result := &Result{ ManifestID: man.GetID(), CaseName: c.Name, - Target: resp.Request.URL.String(), - Method: resp.Request.Method, - StatusCode: resp.StatusCode, - Duration: caseResult.Duration, + ResultCase: caseResult, Request: &Entry{ Headers: make(map[string]string), Body: make(map[string]any), @@ -44,11 +41,16 @@ func (e *Extractor) Extract(ctx interfaces.ExecutionContext, man manifests.Manif }, } + if resp != nil { + result.Target = resp.Request.URL.String() + result.Method = resp.Request.Method + } + defer func() { var builder strings.Builder builder.WriteString("\nExtractor:") - builder.WriteString(fmt.Sprintf("\nID: %s\nCase: %s\nTarget: %s\n Status: %d", result.ManifestID, result.CaseName, result.Target, result.StatusCode)) + builder.WriteString(fmt.Sprintf("\nID: %s\nCase: %s\nTarget: %s\n Status: %d", result.ManifestID, result.CaseName, result.Target, result.ResultCase.StatusCode)) reqData, _ := json.MarshalIndent(result.Request, "", " ") builder.WriteString(fmt.Sprintf("\n\tRequest: %v", string(reqData))) @@ -63,12 +65,16 @@ func (e *Extractor) Extract(ctx interfaces.ExecutionContext, man manifests.Manif if c.Save != nil { if c.Save.Request != nil { - result.Request.Headers = e.extractHeaders(c.Save.Request.Headers, resp.Request.Header, result.Request.Headers) + if resp != nil { + result.Request.Headers = e.extractHeaders(c.Save.Request.Headers, resp.Request.Header, result.Request.Headers) + } result.Request.Body = e.extractBody(c.Save.Request.Body, reqBody, result.Response.Body) } if c.Save.Response != nil { - result.Response.Headers = e.extractHeaders(c.Save.Response.Headers, resp.Header, result.Response.Headers) + if resp != nil { + result.Response.Headers = e.extractHeaders(c.Save.Response.Headers, resp.Header, result.Response.Headers) + } result.Response.Body = e.extractBody(c.Save.Response.Body, respBody, result.Response.Body) } } @@ -88,6 +94,10 @@ func (e *Extractor) extractHeaders(list []string, origin http.Header, source map } func (e *Extractor) extractBody(mapList map[string]string, origin []byte, source map[string]any) map[string]any { + if len(origin) == 0 { + return source + } + var value any var once bool diff --git a/internal/core/runner/save/result.go b/internal/core/runner/save/result.go index 6a92f94..2b05f6d 100644 --- a/internal/core/runner/save/result.go +++ b/internal/core/runner/save/result.go @@ -2,7 +2,8 @@ package save import ( "fmt" - "time" + + "github.com/apiqube/cli/internal/core/runner/interfaces" ) type Result struct { @@ -10,8 +11,8 @@ type Result struct { CaseName string Target string Method string - Duration time.Duration - StatusCode int + + ResultCase *interfaces.CaseResult Request *Entry Response *Entry From bab5cb6dadc13bb3e35f98e8d10c85276d484abc Mon Sep 17 00:00:00 2001 From: Nofre Date: Tue, 10 Jun 2025 11:17:20 +0200 Subject: [PATCH 6/9] chore(runner): assert package refactored, tided and styled --- internal/core/runner/assert/runner.go | 138 +++++++++++++------------- 1 file changed, 67 insertions(+), 71 deletions(-) diff --git a/internal/core/runner/assert/runner.go b/internal/core/runner/assert/runner.go index 6bb7241..cc21916 100644 --- a/internal/core/runner/assert/runner.go +++ b/internal/core/runner/assert/runner.go @@ -10,10 +10,9 @@ import ( "github.com/goccy/go-json" - "github.com/apiqube/cli/internal/core/runner/templates" - "github.com/apiqube/cli/internal/core/manifests/kinds/tests" "github.com/apiqube/cli/internal/core/runner/interfaces" + "github.com/apiqube/cli/internal/core/runner/templates" ) type Type string @@ -38,124 +37,108 @@ func NewRunner() *Runner { } } +// Assert runs all assertions and aggregates errors. func (r *Runner) Assert(ctx interfaces.ExecutionContext, asserts []*tests.Assert, resp *http.Response, body []byte) error { var err error - - for _, assert := range asserts { - switch assert.Target { + for _, a := range asserts { + switch a.Target { case Status.String(): - err = errors.Join(err, r.assertStatus(ctx, assert, resp)) + err = errors.Join(err, r.assertStatus(ctx, a, resp)) case Body.String(): - err = errors.Join(err, r.assertBody(ctx, assert, resp, body)) + err = errors.Join(err, r.assertBody(ctx, a, resp, body)) case Headers.String(): - err = errors.Join(err, r.assertHeaders(ctx, assert, resp)) + err = errors.Join(err, r.assertHeaders(ctx, a, resp)) default: - return fmt.Errorf("assert failed: unknown assert target %s", assert.Target) + return fmt.Errorf("assert failed: unknown assert target %s", a.Target) } } - return err } -func (r *Runner) assertStatus(_ interfaces.ExecutionContext, assert *tests.Assert, resp *http.Response) error { - if assert.Equals != nil { - var expectedCode int - switch val := assert.Equals.(type) { - case uint: - expectedCode = int(val) - case uint64: - expectedCode = int(val) - case int64: - expectedCode = int(val) - case int: - expectedCode = val - default: - return fmt.Errorf("expected status type [int] got %T", assert.Equals) +func (r *Runner) assertStatus(_ interfaces.ExecutionContext, a *tests.Assert, resp *http.Response) error { + if a.Equals != nil { + expectedCode, err := toInt(a.Equals) + if err != nil { + return fmt.Errorf("expected status type [int] got %T", a.Equals) } - if resp.StatusCode != expectedCode { return fmt.Errorf("expected status code %v, got %v", expectedCode, resp.StatusCode) } } - - if assert.Contains != "" { - if !strings.Contains(resp.Status, assert.Contains) { - return fmt.Errorf("expected %v to contain %q", resp.Status, assert.Contains) + if a.Contains != "" { + if !strings.Contains(resp.Status, a.Contains) { + return fmt.Errorf("expected %v to contain %q", resp.Status, a.Contains) } } - return nil } -func (r *Runner) assertBody(_ interfaces.ExecutionContext, assert *tests.Assert, _ *http.Response, body []byte) error { - if assert.Exists { +func (r *Runner) assertBody(_ interfaces.ExecutionContext, a *tests.Assert, _ *http.Response, body []byte) error { + if a.Exists { if len(body) == 0 { return fmt.Errorf("expected not null body") } - return nil } - - if assert.Template != "" { - tplResult, err := r.templateEngine.Execute(assert.Template) + if a.Template != "" { + tplResult, err := r.templateEngine.Execute(a.Template) if err != nil { return fmt.Errorf("template execution error: %v", err) } - expected, err := json.Marshal(tplResult) if err != nil { return fmt.Errorf("template marshal error: %v", err) } - - if !reflect.DeepEqual(body, expected) { + if !bytes.Equal(body, expected) { return fmt.Errorf("body doesn't match template\nexpected: %s\nactual: %s", expected, body) } - return nil } - - if assert.Equals != nil { + if a.Equals != nil { var expected any - if err := json.Unmarshal([]byte(fmt.Sprintf("%v", assert.Equals)), &expected); err != nil { - return fmt.Errorf("invalid Equals in body target value: %v", err) + // Try to unmarshal as JSON, fallback to string compare + if err := json.Unmarshal([]byte(fmt.Sprintf("%v", a.Equals)), &expected); err == nil { + var actual any + if marshalErr := json.Unmarshal(body, &actual); marshalErr == nil { + if !reflect.DeepEqual(actual, expected) { + return fmt.Errorf("expected body %v to equal %v", string(body), expected) + } + return nil + } } - - if !reflect.DeepEqual(body, expected) { - return fmt.Errorf("expected body %v to equal %v", string(body), expected) + // Fallback: compare as string + if string(body) != fmt.Sprint(a.Equals) { + return fmt.Errorf("expected body %v to equal %v", string(body), a.Equals) } return nil } - - if assert.Contains != "" { - if !bytes.Contains(body, []byte(assert.Contains)) { - return fmt.Errorf("expected '%v' in body", assert.Contains) + if a.Contains != "" { + if !bytes.Contains(body, []byte(a.Contains)) { + return fmt.Errorf("expected '%v' in body", a.Contains) } - return nil } - return nil } -func (r *Runner) assertHeaders(_ interfaces.ExecutionContext, assert *tests.Assert, resp *http.Response) error { - if assert.Equals != nil { - if equals, ok := assert.Equals.(map[string]any); ok { - return fmt.Errorf("expected map type assertion got %T", assert.Equals) - } else { - for key, expectedVal := range equals { - actualVal := resp.Header.Get(key) - if fmt.Sprintf("%v", expectedVal) != actualVal { - return fmt.Errorf("expected header value %v, got %v", expectedVal, actualVal) - } +func (r *Runner) assertHeaders(_ interfaces.ExecutionContext, a *tests.Assert, resp *http.Response) error { + if a.Equals != nil { + equals, ok := a.Equals.(map[string]any) + if !ok { + return fmt.Errorf("expected map[string]any for header equals, got %T", a.Equals) + } + for key, expectedVal := range equals { + actualVal := resp.Header.Get(key) + if fmt.Sprintf("%v", expectedVal) != actualVal { + return fmt.Errorf("expected header %v value %v, got %v", key, expectedVal, actualVal) } } } - - if assert.Contains != "" { + if a.Contains != "" { found := false for _, values := range resp.Header { for _, val := range values { - if strings.Contains(val, assert.Contains) { + if strings.Contains(val, a.Contains) { found = true break } @@ -164,17 +147,30 @@ func (r *Runner) assertHeaders(_ interfaces.ExecutionContext, assert *tests.Asse break } } - if !found { - return fmt.Errorf("expected header contains %v but not found", assert.Contains) + return fmt.Errorf("expected header contains %v but not found", a.Contains) } } - - if assert.Exists { + if a.Exists { if len(resp.Header) == 0 { return fmt.Errorf("expected some headers in response") } } - return nil } + +// toInt tries to convert interface{} to int for status code assertions. +func toInt(val any) (int, error) { + switch v := val.(type) { + case uint: + return int(v), nil + case uint64: + return int(v), nil + case int64: + return int(v), nil + case int: + return v, nil + default: + return 0, fmt.Errorf("cannot convert %T to int", val) + } +} From bf460de3779cacf49c7baebf68d4e758fdf5cd4f Mon Sep 17 00:00:00 2001 From: Nofre Date: Tue, 10 Jun 2025 12:03:36 +0200 Subject: [PATCH 7/9] chore(runner): some refactors in http executor, fixed issue with request body saving to result case, added colored coverage heatmap report generation --- .gitignore | 1 + Taskfile.yml | 5 ++ examples/complex-http-tests/http_test.yaml | 9 +- .../core/runner/executor/executors/http.go | 5 +- internal/core/runner/form/runner.go | 83 ++++++++++++------- internal/core/runner/save/extractor.go | 2 +- 6 files changed, 74 insertions(+), 31 deletions(-) diff --git a/.gitignore b/.gitignore index c91c47a..490633f 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ *.dylib bin .qodo +cover.svg # Test binary, built with `go test -c` *.test diff --git a/Taskfile.yml b/Taskfile.yml index e0c532a..e3af921 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -45,6 +45,11 @@ tasks: cmds: - go test -v -coverpkg=./... -coverprofile=cover.out ./... + cover: + desc: Create SVG cover heatmap from cover.out + cmds: + - go-cover-treemap -percent=true -w=1080 -h=360 -coverprofile cover.out > cover.svg + fmt: desc: 🧹 Cleaning all go code cmds: diff --git a/examples/complex-http-tests/http_test.yaml b/examples/complex-http-tests/http_test.yaml index 62bb625..58bebcc 100644 --- a/examples/complex-http-tests/http_test.yaml +++ b/examples/complex-http-tests/http_test.yaml @@ -23,4 +23,11 @@ spec: age: "{{ Fake.uint.10.100 }}" address: street: "{{ Fake.address }}" - number: "{{ Regex(\"^[a-z]{5,10}@[a-z]{5,10}\\.(com|net|org)$\") }}" \ No newline at end of file + number: "{{ Regex(\"^[a-z]{5,10}@[a-z]{5,10}\\.(com|net|org)$\") }}" + save: + request: + body: + users: "*" + response: + body: + data: "*" \ No newline at end of file diff --git a/internal/core/runner/executor/executors/http.go b/internal/core/runner/executor/executors/http.go index a14d91e..70de753 100644 --- a/internal/core/runner/executor/executors/http.go +++ b/internal/core/runner/executor/executors/http.go @@ -105,11 +105,13 @@ func (e *HTTPExecutor) runCase(ctx interfaces.ExecutionContext, man *api.Http, c respBody = &bytes.Buffer{} err error ) + var reqBodyCopy []byte output.StartCase(man, c.Name) defer func() { metrics.CollectHTTPMetrics(req, resp, c.Details, caseResult) - e.extractor.Extract(ctx, man, c.HttpCase, resp, reqBody.Bytes(), respBody.Bytes(), caseResult) + e.extractor.Extract(ctx, man, c.HttpCase, resp, reqBodyCopy, respBody.Bytes(), caseResult) + output.EndCase(man, c.Name, caseResult) }() @@ -125,6 +127,7 @@ func (e *HTTPExecutor) runCase(ctx interfaces.ExecutionContext, man *api.Http, c } } + reqBodyCopy = reqBody.Bytes() req, err = http.NewRequest(c.Method, url, reqBody) if err != nil { caseResult.Errors = append(caseResult.Errors, fmt.Sprintf("failed to create request: %s", err.Error())) diff --git a/internal/core/runner/form/runner.go b/internal/core/runner/form/runner.go index 18236d8..2ab81f5 100644 --- a/internal/core/runner/form/runner.go +++ b/internal/core/runner/form/runner.go @@ -43,16 +43,18 @@ func NewRunner() *Runner { } } +// RegisterDirective allows registering custom directives +func (r *Runner) RegisterDirective(handler DirectiveHandler) { + if executor, ok := r.processor.(*CompositeProcessor).mapProcessor.directiveHandler.(*defaultDirectiveExecutor); ok { + executor.RegisterDirective(handler) + } +} + // Apply processes a string input with pass mappings and template resolution func (r *Runner) Apply(ctx interfaces.ExecutionContext, input string, pass []*tests.Pass) string { result := input - - // Apply pass mappings first result = r.applyPassMappings(ctx, result, pass) - - // Apply template resolution result = r.applyTemplateResolution(ctx, result) - return result } @@ -62,23 +64,34 @@ func (r *Runner) ApplyBody(ctx interfaces.ExecutionContext, body map[string]any, return nil } - // Process the body using the main processor + select { + case <-ctx.Done(): + return nil + default: + } + processed := r.processor.Process(ctx, body, pass, nil, []int{}) + select { + case <-ctx.Done(): + return nil + default: + } - // Convert result to map if processedMap, ok := processed.(map[string]any); ok { - // Resolve references in the processed data resolved := r.referenceResolver.Resolve(ctx, processedMap, processedMap, pass, []int{}) + select { + case <-ctx.Done(): + return nil + default: + } - if resolvedMap, ok := resolved.(map[string]any); ok { - // Debug output (can be removed or made configurable) + if resolvedMap, is := resolved.(map[string]any); is { if data, err := json.MarshalIndent(resolvedMap, "", " "); err == nil { fmt.Println(string(data)) } return resolvedMap } } - return body } @@ -87,9 +100,14 @@ func (r *Runner) MapHeaders(ctx interfaces.ExecutionContext, headers map[string] if headers == nil { return nil } - result := make(map[string]string, len(headers)) for key, value := range headers { + select { + case <-ctx.Done(): + return result + default: + } + processedKey := r.processHeaderValue(ctx, key, pass) processedValue := r.processHeaderValue(ctx, value, pass) result[processedKey] = processedValue @@ -97,11 +115,20 @@ func (r *Runner) MapHeaders(ctx interfaces.ExecutionContext, headers map[string] return result } -// Private helper methods +// GetTemplateEngine returns the underlying template engine for advanced usage +func (r *Runner) GetTemplateEngine() *templates.TemplateEngine { + return r.templateEngine +} +// Private helper methods func (r *Runner) applyPassMappings(ctx interfaces.ExecutionContext, input string, pass []*tests.Pass) string { result := input for _, p := range pass { + select { + case <-ctx.Done(): + return result + default: + } if p.Map != nil { for placeholder, mapKey := range p.Map { if strings.Contains(result, placeholder) { @@ -118,38 +145,38 @@ func (r *Runner) applyPassMappings(ctx interfaces.ExecutionContext, input string func (r *Runner) applyTemplateResolution(ctx interfaces.ExecutionContext, input string) string { reg := regexp.MustCompile(`\{\{\s*([^}\s]+)\s*}}`) return reg.ReplaceAllStringFunc(input, func(match string) string { + select { + case <-ctx.Done(): + return match + default: + } key := strings.Trim(match, "{} \t") - if val, ok := ctx.Get(key); ok { return fmt.Sprintf("%v", val) } - if strings.HasPrefix(key, "Fake.") { if val, err := r.templateEngine.Execute(match); err == nil { return fmt.Sprintf("%v", val) } } - return match }) } func (r *Runner) processHeaderValue(ctx interfaces.ExecutionContext, value string, pass []*tests.Pass) string { + select { + case <-ctx.Done(): + return value + default: + } processed := r.processor.Process(ctx, value, pass, nil, []int{}) + select { + case <-ctx.Done(): + return value + default: + } if str, ok := processed.(string); ok { return str } return fmt.Sprintf("%v", processed) } - -// RegisterDirective allows registering custom directives -func (r *Runner) RegisterDirective(handler DirectiveHandler) { - if executor, ok := r.processor.(*CompositeProcessor).mapProcessor.directiveHandler.(*defaultDirectiveExecutor); ok { - executor.RegisterDirective(handler) - } -} - -// GetTemplateEngine returns the underlying template engine for advanced usage -func (r *Runner) GetTemplateEngine() *templates.TemplateEngine { - return r.templateEngine -} diff --git a/internal/core/runner/save/extractor.go b/internal/core/runner/save/extractor.go index 4f5e2ea..cf74ace 100644 --- a/internal/core/runner/save/extractor.go +++ b/internal/core/runner/save/extractor.go @@ -68,7 +68,7 @@ func (e *Extractor) Extract(ctx interfaces.ExecutionContext, man manifests.Manif if resp != nil { result.Request.Headers = e.extractHeaders(c.Save.Request.Headers, resp.Request.Header, result.Request.Headers) } - result.Request.Body = e.extractBody(c.Save.Request.Body, reqBody, result.Response.Body) + result.Request.Body = e.extractBody(c.Save.Request.Body, reqBody, result.Request.Body) } if c.Save.Response != nil { From d8f7a9a7672a6d8bb813dc33ce5ec07e4f208347 Mon Sep 17 00:00:00 2001 From: Nofre Date: Fri, 13 Jun 2025 00:15:21 +0200 Subject: [PATCH 8/9] feat(runner): added HTML report generation after plan execution, refactored save extractor to collect multiple results per manifest --- .goreleaser.yml | 8 +- internal/core/runner/executor/plan.go | 15 ++ internal/core/runner/save/extractor.go | 29 ++-- internal/core/runner/save/result.go | 4 +- internal/report/html.go | 157 ++++++++++++++++++ internal/report/html/templates/base.gohtml | 37 +++++ internal/report/html/templates/case.gohtml | 23 +++ .../report/html/templates/manifest.gohtml | 18 ++ internal/report/interfaces.go | 7 + internal/report/service.go | 37 +++++ 10 files changed, 311 insertions(+), 24 deletions(-) create mode 100644 internal/report/html.go create mode 100644 internal/report/html/templates/base.gohtml create mode 100644 internal/report/html/templates/case.gohtml create mode 100644 internal/report/html/templates/manifest.gohtml create mode 100644 internal/report/interfaces.go create mode 100644 internal/report/service.go diff --git a/.goreleaser.yml b/.goreleaser.yml index 196fc60..c314323 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -1,12 +1,14 @@ version: 2 + +snapshot: + version_template: "{{ incpatch .Version }}-next" + builds: - main: ./cmd/qube id: "qube" binary: qube ldflags: - - -s -w -X github.com/apiqube/cli/cmd/cli.Version={{.Version}} - - -s -w -X github.com/apiqube/cli/cmd/cli.Commit={{.ShortCommit}} - - -s -w -X github.com/apiqube/cli/cmd/cli.Date={{.Date}} + - -s -w -X github.com/apiqube/cli/cmd/cli.Version={{.Version}} -X github.com/apiqube/cli/cmd/cli.Commit={{.ShortCommit}} -X github.com/apiqube/cli/cmd/cli.Date={{.Date}} env: - CGO_ENABLED=0 goos: [linux, darwin, windows] diff --git a/internal/core/runner/executor/plan.go b/internal/core/runner/executor/plan.go index 37b105c..db4321c 100644 --- a/internal/core/runner/executor/plan.go +++ b/internal/core/runner/executor/plan.go @@ -3,6 +3,7 @@ package executor import ( "errors" "fmt" + "github.com/apiqube/cli/internal/report" "sync" "github.com/apiqube/cli/internal/core/manifests" @@ -131,6 +132,20 @@ func (r *DefaultPlanRunner) RunPlan(ctx interfaces.ExecutionContext, manifest ma } } + // TODO: TEMPL CODE HERE !!! + htmlReportGenerator, err := report.NewHTMLReportGenerator() + if err != nil { + fmt.Println("ERROR:", err) + return nil + } + + reporter := report.NewReportService(htmlReportGenerator) + if err = reporter.GenerateReports(ctx); err != nil { + fmt.Println("ERROR:", err) + return nil + } + // TODO: END + return nil } diff --git a/internal/core/runner/save/extractor.go b/internal/core/runner/save/extractor.go index cf74ace..387ab6f 100644 --- a/internal/core/runner/save/extractor.go +++ b/internal/core/runner/save/extractor.go @@ -1,11 +1,8 @@ package save import ( - "fmt" - "net/http" - "strings" - "github.com/goccy/go-json" + "net/http" "github.com/apiqube/cli/internal/core/manifests" "github.com/apiqube/cli/internal/core/manifests/kinds/tests" @@ -25,7 +22,7 @@ func NewExtractor() *Extractor { } func (e *Extractor) Extract(ctx interfaces.ExecutionContext, man manifests.Manifest, c tests.HttpCase, resp *http.Response, reqBody, respBody []byte, caseResult *interfaces.CaseResult) { - key := FormSaveKey(man.GetID(), c.Name, ResultKeySuffix) + key := FormSaveKey(man.GetID(), ResultKeySuffix) result := &Result{ ManifestID: man.GetID(), @@ -47,20 +44,14 @@ func (e *Extractor) Extract(ctx interfaces.ExecutionContext, man manifests.Manif } defer func() { - var builder strings.Builder - - builder.WriteString("\nExtractor:") - builder.WriteString(fmt.Sprintf("\nID: %s\nCase: %s\nTarget: %s\n Status: %d", result.ManifestID, result.CaseName, result.Target, result.ResultCase.StatusCode)) - - reqData, _ := json.MarshalIndent(result.Request, "", " ") - builder.WriteString(fmt.Sprintf("\n\tRequest: %v", string(reqData))) - - resData, _ := json.MarshalIndent(result.Response, "", " ") - builder.WriteString(fmt.Sprintf("\n\tResponse: %v", string(resData))) - - ctx.GetOutput().Logf(interfaces.DebugLevel, builder.String()) - - ctx.Set(key, result) + if val, ok := ctx.Get(key); !ok { + var results = []*Result{result} + ctx.Set(key, results) + } else { + results := val.([]*Result) + results = append(results, result) + ctx.Set(key, results) + } }() if c.Save != nil { diff --git a/internal/core/runner/save/result.go b/internal/core/runner/save/result.go index 2b05f6d..3368bfb 100644 --- a/internal/core/runner/save/result.go +++ b/internal/core/runner/save/result.go @@ -23,6 +23,6 @@ type Entry struct { Body map[string]any } -func FormSaveKey(manifestID, caseName, suffix string) string { - return fmt.Sprintf("%s.%s.%s.%s", KeyPrefix, manifestID, caseName, suffix) +func FormSaveKey(manifestID, suffix string) string { + return fmt.Sprintf("%s.%s.%s", KeyPrefix, manifestID, suffix) } diff --git a/internal/report/html.go b/internal/report/html.go new file mode 100644 index 0000000..16b59c4 --- /dev/null +++ b/internal/report/html.go @@ -0,0 +1,157 @@ +package report + +import ( + "embed" + "fmt" + "html/template" + "os" + "path/filepath" + "time" + + "github.com/apiqube/cli/internal/core/runner/save" +) + +//go:embed html/templates/*.gohtml +var htmlTemplates embed.FS + +// CaseReport is a view model for a single test case. +type CaseReport struct { + Name string + Success bool + Assert string + StatusCode int + Duration time.Duration + Errors []string + Method string + Request *save.Entry + Response *save.Entry +} + +// ManifestReport groups results for a single manifest. +type ManifestReport struct { + ManifestID string + Target string + TotalCases int + PassedCases int + FailedCases int + TotalTime time.Duration + Cases []*CaseReport +} + +// ViewData is the data passed to the HTML template. +type ViewData struct { + GeneratedAt time.Time + TotalCases int + PassedCases int + FailedCases int + TotalTime time.Duration + ManifestStats []*ManifestReport +} + +// BuildReportViewData aggregates results and statistics for the template. +func BuildReportViewData(results []*save.Result) *ViewData { + manifestMap := make(map[string]*ManifestReport) + totalCases := 0 + passedCases := 0 + failedCases := 0 + totalTime := time.Duration(0) + + for _, res := range results { + m, ok := manifestMap[res.ManifestID] + if !ok { + m = &ManifestReport{ + ManifestID: res.ManifestID, + Target: res.Target, + Cases: []*CaseReport{}, + } + manifestMap[res.ManifestID] = m + } + cr := res.ResultCase + caseReport := &CaseReport{ + Name: cr.Name, + Success: cr.Success, + Assert: cr.Assert, + StatusCode: cr.StatusCode, + Duration: cr.Duration, + Errors: cr.Errors, + Method: res.Method, + Request: res.Request, + Response: res.Response, + } + m.Cases = append(m.Cases, caseReport) + m.TotalCases++ + m.TotalTime += cr.Duration + if cr.Success { + m.PassedCases++ + passedCases++ + } else { + m.FailedCases++ + failedCases++ + } + totalCases++ + totalTime += cr.Duration + } + manifestStats := make([]*ManifestReport, 0, len(manifestMap)) + for _, m := range manifestMap { + manifestStats = append(manifestStats, m) + } + return &ViewData{ + GeneratedAt: time.Now(), + TotalCases: totalCases, + PassedCases: passedCases, + FailedCases: failedCases, + TotalTime: totalTime, + ManifestStats: manifestStats, + } +} + +// HTMLReportGenerator generates an HTML report using html/template and gohtml templates. +type HTMLReportGenerator struct { + tmpl *template.Template + funcs template.FuncMap +} + +// NewHTMLReportGenerator creates a new HTMLReportGenerator with custom functions and template directory. +func NewHTMLReportGenerator() (*HTMLReportGenerator, error) { + funcs := template.FuncMap{ + "formatTime": func(t time.Time) string { return t.Format("2006-01-02 15:04:05") }, + "statusText": func(success bool) string { + if success { + return "PASSED" + } + return "FAILED" + }, + // Add more custom functions here as needed + } + + tmpl, err := template.New("base.gohtml").Funcs(funcs).ParseFS(htmlTemplates, "html/templates/*.gohtml") + if err != nil { + return nil, fmt.Errorf("failed to parse templates: %w", err) + } + + return &HTMLReportGenerator{ + tmpl: tmpl, + funcs: funcs, + }, nil +} + +// Generate creates an HTML report from test results and writes it to outputPath. +func (g *HTMLReportGenerator) Generate(results []*save.Result) error { + data := BuildReportViewData(results) + + outputPath := filepath.Join("C:\\Users\\admin\\Desktop\\reports", fmt.Sprintf("/report_%s.html", time.Now().Format("2006-01-02-150405"))) + file, err := os.Create(outputPath) + if err != nil { + return fmt.Errorf("failed to create report file: %w", err) + } + + defer func() { + _ = file.Close() + }() + + if err = g.tmpl.Execute(file, data); err != nil { + return fmt.Errorf("failed to execute template: %w", err) + } + + return nil +} diff --git a/internal/report/html/templates/base.gohtml b/internal/report/html/templates/base.gohtml new file mode 100644 index 0000000..845476e --- /dev/null +++ b/internal/report/html/templates/base.gohtml @@ -0,0 +1,37 @@ +{{/* + Base template for the test report +*/}} + + + + + + Test Report + + + + +
+

Test Report

+
Generated at: {{ formatTime .GeneratedAt }}
+
+
+

Summary

+
    +
  • Total Cases: {{ .TotalCases }}
  • +
  • Passed: {{ .PassedCases }}
  • +
  • Failed: {{ .FailedCases }}
  • +
  • Total Time: {{ .TotalTime }}
  • +
+
+
+
+ {{ range .ManifestStats }} + {{ template "manifest.gohtml" . }} + {{ end }} +
+
+ + diff --git a/internal/report/html/templates/case.gohtml b/internal/report/html/templates/case.gohtml new file mode 100644 index 0000000..d6d0a37 --- /dev/null +++ b/internal/report/html/templates/case.gohtml @@ -0,0 +1,23 @@ +{{/* + Case component for the test report +*/}} +
  • +
    +
    + {{ .Name }} + + {{ statusText .Success }} + + Method: {{ .Method }} + Status: {{ .StatusCode }} + Time: {{ .Duration }} +
    +
    + {{ if .Errors }} +
      + {{ range .Errors }} +
    • {{ . }}
    • + {{ end }} +
    + {{ end }} +
  • diff --git a/internal/report/html/templates/manifest.gohtml b/internal/report/html/templates/manifest.gohtml new file mode 100644 index 0000000..22edef6 --- /dev/null +++ b/internal/report/html/templates/manifest.gohtml @@ -0,0 +1,18 @@ +{{/* + Manifest component for the test report +*/}} +
    +

    Manifest: {{ .ManifestID }}

    +
    Target: {{ .Target }}
    +
    + Total: {{ .TotalCases }} + Passed: {{ .PassedCases }} + Failed: {{ .FailedCases }} + Time: {{ .TotalTime }} +
    +
      + {{ range .Cases }} + {{ template "case.gohtml" . }} + {{ end }} +
    +
    diff --git a/internal/report/interfaces.go b/internal/report/interfaces.go new file mode 100644 index 0000000..5e4a8af --- /dev/null +++ b/internal/report/interfaces.go @@ -0,0 +1,7 @@ +package report + +import "github.com/apiqube/cli/internal/core/runner/save" + +type Generator interface { + Generate(results []*save.Result) error +} diff --git a/internal/report/service.go b/internal/report/service.go new file mode 100644 index 0000000..bbd0261 --- /dev/null +++ b/internal/report/service.go @@ -0,0 +1,37 @@ +package report + +import ( + "github.com/apiqube/cli/internal/core/runner/interfaces" + "github.com/apiqube/cli/internal/core/runner/save" +) + +// Service aggregates results from ExecutionContext and generates reports. +type Service struct { + generator Generator +} + +// NewReportService creates a new ReportService with the given generator. +func NewReportService(generator Generator) *Service { + return &Service{generator: generator} +} + +// CollectResults collects all save.Result from the context by manifest IDs. +func (s *Service) CollectResults(ctx interfaces.ExecutionContext) []*save.Result { + mans := ctx.GetAllManifests() + results := make([]*save.Result, 0, len(mans)) + + for _, man := range mans { + key := save.FormSaveKey(man.GetID(), save.ResultKeySuffix) + if val, ok := ctx.Get(key); ok { + if res, is := val.([]*save.Result); is { + results = append(results, res...) + } + } + } + + return results +} + +func (s *Service) GenerateReports(ctx interfaces.ExecutionContext) error { + return s.generator.Generate(s.CollectResults(ctx)) +} From 0e128ba692f927013160acc9909a19954bcb825c Mon Sep 17 00:00:00 2001 From: Nofre Date: Fri, 13 Jun 2025 00:17:48 +0200 Subject: [PATCH 9/9] chore(core): moved from standard json package to goccy/go-json package, tided --- internal/core/runner/executor/plan.go | 3 ++- internal/core/runner/save/extractor.go | 5 +++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/internal/core/runner/executor/plan.go b/internal/core/runner/executor/plan.go index db4321c..7d049c0 100644 --- a/internal/core/runner/executor/plan.go +++ b/internal/core/runner/executor/plan.go @@ -3,9 +3,10 @@ package executor import ( "errors" "fmt" - "github.com/apiqube/cli/internal/report" "sync" + "github.com/apiqube/cli/internal/report" + "github.com/apiqube/cli/internal/core/manifests" "github.com/apiqube/cli/internal/core/manifests/kinds/plan" "github.com/apiqube/cli/internal/core/runner/hooks" diff --git a/internal/core/runner/save/extractor.go b/internal/core/runner/save/extractor.go index 387ab6f..8117b11 100644 --- a/internal/core/runner/save/extractor.go +++ b/internal/core/runner/save/extractor.go @@ -1,9 +1,10 @@ package save import ( - "github.com/goccy/go-json" "net/http" + "github.com/goccy/go-json" + "github.com/apiqube/cli/internal/core/manifests" "github.com/apiqube/cli/internal/core/manifests/kinds/tests" "github.com/apiqube/cli/internal/core/runner/interfaces" @@ -45,7 +46,7 @@ func (e *Extractor) Extract(ctx interfaces.ExecutionContext, man manifests.Manif defer func() { if val, ok := ctx.Get(key); !ok { - var results = []*Result{result} + results := []*Result{result} ctx.Set(key, results) } else { results := val.([]*Result)