Skip to content

Commit 35e099c

Browse files
authored
Merge pull request #955 from meshery/copilot/utils-coverage-tests
test: drive utils coverage to 100%
2 parents 00e5c41 + fd8582e commit 35e099c

11 files changed

Lines changed: 2312 additions & 48 deletions

File tree

utils/csv/csv_test.go

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
package csv
2+
3+
import (
4+
"os"
5+
"path/filepath"
6+
"testing"
7+
8+
meshkiterrors "github.com/meshery/meshkit/errors"
9+
"github.com/meshery/meshkit/utils"
10+
)
11+
12+
type mappedRow struct {
13+
Name string `json:"name"`
14+
Age string `json:"person_age"`
15+
}
16+
17+
type numericAgeRow struct {
18+
Name string `json:"name"`
19+
Age int `json:"age"`
20+
}
21+
22+
func TestNewCSVParserReturnsWrappedReadErrorForMissingFiles(t *testing.T) {
23+
_, err := NewCSVParser[mappedRow](filepath.Join(t.TempDir(), "missing.csv"), 0, nil, nil)
24+
if err == nil {
25+
t.Fatal("expected NewCSVParser to return an error for a missing file")
26+
}
27+
if got := meshkiterrors.GetCode(err); got != utils.ErrReadFileCode {
28+
t.Fatalf("expected error code %q, got %q", utils.ErrReadFileCode, got)
29+
}
30+
}
31+
32+
func TestExtractCols(t *testing.T) {
33+
t.Run("reads the configured header row", func(t *testing.T) {
34+
path := writeCSVFixture(t, "Name,Age,Include\nAlice,30,yes\n")
35+
parser, err := NewCSVParser[mappedRow](path, 0, nil, nil)
36+
if err != nil {
37+
t.Fatalf("NewCSVParser() returned error: %v", err)
38+
}
39+
40+
cols, err := parser.ExtractCols(0)
41+
if err != nil {
42+
t.Fatalf("ExtractCols() returned error: %v", err)
43+
}
44+
45+
if len(cols) != 3 {
46+
t.Fatalf("expected 3 columns, got %d", len(cols))
47+
}
48+
if cols[0] != "Name" || cols[1] != "Age" || cols[2] != "Include" {
49+
t.Fatalf("unexpected columns: %#v", cols)
50+
}
51+
})
52+
53+
t.Run("wraps read errors when the header row is missing", func(t *testing.T) {
54+
path := writeCSVFixture(t, "Name,Age\n")
55+
parser, err := NewCSVParser[mappedRow](path, 0, nil, nil)
56+
if err != nil {
57+
t.Fatalf("NewCSVParser() returned error: %v", err)
58+
}
59+
60+
_, err = parser.ExtractCols(1)
61+
if err == nil {
62+
t.Fatal("expected ExtractCols to return an error when the requested header row is missing")
63+
}
64+
if got := meshkiterrors.GetCode(err); got != utils.ErrReadFileCode {
65+
t.Fatalf("expected error code %q, got %q", utils.ErrReadFileCode, got)
66+
}
67+
})
68+
}
69+
70+
func TestParseAppliesPredicateAndColumnMapping(t *testing.T) {
71+
path := writeCSVFixture(t, "Name,Age,Include\nAlice,30,yes\nBob,40,no\nCharlie,50,yes\n")
72+
parser, err := NewCSVParser[mappedRow](
73+
path,
74+
0,
75+
map[string]string{"Age": "person_age"},
76+
func(columns []string, currentRow []string) bool {
77+
return currentRow[2] == "yes"
78+
},
79+
)
80+
if err != nil {
81+
t.Fatalf("NewCSVParser() returned error: %v", err)
82+
}
83+
84+
rows := make(chan mappedRow, 3)
85+
errs := make(chan error, 1)
86+
if err := parser.Parse(rows, errs); err != nil {
87+
t.Fatalf("Parse() returned error: %v", err)
88+
}
89+
90+
select {
91+
case err := <-errs:
92+
t.Fatalf("did not expect parse errors, got %v", err)
93+
default:
94+
}
95+
96+
if len(rows) != 2 {
97+
t.Fatalf("expected 2 parsed rows, got %d", len(rows))
98+
}
99+
100+
first := <-rows
101+
second := <-rows
102+
if first.Name != "Alice" || first.Age != "30" {
103+
t.Fatalf("unexpected first row: %#v", first)
104+
}
105+
if second.Name != "Charlie" || second.Age != "50" {
106+
t.Fatalf("unexpected second row: %#v", second)
107+
}
108+
109+
select {
110+
case <-parser.Context.Done():
111+
default:
112+
t.Fatal("expected parser context to be canceled after Parse returns")
113+
}
114+
}
115+
116+
func TestParseSendsMarshalErrorsToErrorChannel(t *testing.T) {
117+
path := writeCSVFixture(t, "Name,Age\nAlice,not-a-number\n")
118+
parser, err := NewCSVParser[numericAgeRow](
119+
path,
120+
0,
121+
nil,
122+
func(columns []string, currentRow []string) bool {
123+
return true
124+
},
125+
)
126+
if err != nil {
127+
t.Fatalf("NewCSVParser() returned error: %v", err)
128+
}
129+
130+
rows := make(chan numericAgeRow, 1)
131+
errs := make(chan error, 1)
132+
if err := parser.Parse(rows, errs); err != nil {
133+
t.Fatalf("Parse() returned error: %v", err)
134+
}
135+
136+
if len(rows) != 0 {
137+
t.Fatalf("expected no successfully parsed rows, got %d", len(rows))
138+
}
139+
140+
select {
141+
case err := <-errs:
142+
if err == nil {
143+
t.Fatal("expected a non-nil marshal error")
144+
}
145+
default:
146+
t.Fatal("expected Parse to send a marshal error to the error channel")
147+
}
148+
}
149+
150+
func TestParseWrapsCSVReadErrors(t *testing.T) {
151+
path := writeCSVFixture(t, "Name,Age\n\"Alice,30\n")
152+
parser, err := NewCSVParser[mappedRow](
153+
path,
154+
0,
155+
nil,
156+
func(columns []string, currentRow []string) bool {
157+
return true
158+
},
159+
)
160+
if err != nil {
161+
t.Fatalf("NewCSVParser() returned error: %v", err)
162+
}
163+
164+
err = parser.Parse(make(chan mappedRow, 1), make(chan error, 1))
165+
if err == nil {
166+
t.Fatal("expected Parse to return a read error for malformed CSV data")
167+
}
168+
if got := meshkiterrors.GetCode(err); got != utils.ErrReadFileCode {
169+
t.Fatalf("expected error code %q, got %q", utils.ErrReadFileCode, got)
170+
}
171+
}
172+
173+
func writeCSVFixture(t *testing.T, content string) string {
174+
t.Helper()
175+
176+
path := filepath.Join(t.TempDir(), "test.csv")
177+
if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
178+
t.Fatalf("failed to write CSV fixture: %v", err)
179+
}
180+
181+
return path
182+
}

utils/cue.go

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,17 @@ import (
1313
"cuelang.org/go/encoding/yaml"
1414
)
1515

16+
var (
17+
formatCueNode = format.Node
18+
compileCue = func(ctx *cue.Context, src string) cue.Value {
19+
return ctx.CompileString(src)
20+
}
21+
lookupCuePath = func(root cue.Value, path string) (cue.Value, error, bool) {
22+
res := root.LookupPath(cue.ParsePath(path))
23+
return res, res.Err(), res.Exists()
24+
}
25+
)
26+
1627
func Validate(schema cue.Value, value cue.Value) (bool, []errors.Error) {
1728
var errs []errors.Error
1829
uval := value.Unify(schema)
@@ -94,23 +105,23 @@ func JsonSchemaToCue(value string) (cue.Value, error) {
94105
if err != nil {
95106
return out, ErrJsonSchemaToCue(err)
96107
}
97-
src, err := format.Node(extractedSchema)
108+
src, err := formatCueNode(extractedSchema)
98109
if err != nil {
99110
return out, ErrJsonSchemaToCue(err)
100111
}
101-
out = cueCtx.CompileString(string(src))
112+
out = compileCue(cueCtx, string(src))
102113
if out.Err() != nil {
103114
return out, ErrJsonSchemaToCue(out.Err())
104115
}
105116
return out, nil
106117
}
107118

108119
func Lookup(rootVal cue.Value, path string) (cue.Value, error) {
109-
res := rootVal.LookupPath(cue.ParsePath(path))
110-
if res.Err() != nil {
111-
return res, ErrCueLookup(res.Err())
120+
res, err, exists := lookupCuePath(rootVal, path)
121+
if err != nil {
122+
return res, ErrCueLookup(err)
112123
}
113-
if !res.Exists() {
124+
if !exists {
114125
return res, ErrCueLookup(fmt.Errorf("Could not find the value at the path: %s", path))
115126
}
116127

utils/error.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,11 @@ func ErrUnmarshal(err error) error {
118118
}
119119

120120
func ErrUnmarshalInvalid(err error, typ reflect.Type) error {
121-
return errors.New(ErrUnmarshalInvalidCode, errors.Alert, []string{"Unmarshal invalid error for type: ", typ.String()}, []string{err.Error()}, []string{"Invalid object format"}, []string{"Make sure to input a valid JSON object"})
121+
typeName := "<nil>"
122+
if typ != nil {
123+
typeName = typ.String()
124+
}
125+
return errors.New(ErrUnmarshalInvalidCode, errors.Alert, []string{"Unmarshal invalid error for type: ", typeName}, []string{err.Error()}, []string{"Invalid object format"}, []string{"Make sure to input a valid JSON object"})
122126
}
123127

124128
func ErrUnmarshalSyntax(err error, offset int64) error {

utils/error_constructors_test.go

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
package utils
2+
3+
import (
4+
stderrors "errors"
5+
"math"
6+
"reflect"
7+
"testing"
8+
9+
meshkiterrors "github.com/meshery/meshkit/errors"
10+
)
11+
12+
func TestErrorConstructorsExposeExpectedCodes(t *testing.T) {
13+
testErr := stderrors.New("boom")
14+
15+
testCases := []struct {
16+
name string
17+
err error
18+
code string
19+
}{
20+
{"invalid construct schema version", ErrInvalidConstructSchemaVersion("design", "v0", "v1"), ErrInvalidSchemaVersionCode},
21+
{"cue lookup", ErrCueLookup(testErr), ErrCueLookupCode},
22+
{"json schema to cue", ErrJsonSchemaToCue(testErr), ErrJsonSchemaToCueCode},
23+
{"yaml to cue", ErrYamlToCue(testErr), ErrYamlToCueCode},
24+
{"json to cue", ErrJsonToCue(testErr), ErrJsonToCueCode},
25+
{"expected type mismatch", ErrExpectedTypeMismatch(testErr, "string"), ErrExpectedTypeMismatchCode},
26+
{"missing field", ErrMissingField(testErr, "name"), ErrMissingFieldCode},
27+
{"unmarshal", ErrUnmarshal(testErr), ErrUnmarshalCode},
28+
{"unmarshal invalid", ErrUnmarshalInvalid(testErr, reflect.TypeOf("")), ErrUnmarshalInvalidCode},
29+
{"unmarshal syntax", ErrUnmarshalSyntax(testErr, 42), ErrUnmarshalSyntaxCode},
30+
{"unmarshal type", ErrUnmarshalType(testErr, "field"), ErrUnmarshalTypeCode},
31+
{"unmarshal unsupported type", ErrUnmarshalUnsupportedType(testErr, reflect.TypeOf(func() {})), ErrUnmarshalUnsupportedTypeCode},
32+
{"unmarshal unsupported value", ErrUnmarshalUnsupportedValue(testErr, reflect.ValueOf(math.Inf(1))), ErrUnmarshalUnsupportedValueCode},
33+
{"marshal", ErrMarshal(testErr), ErrMarshalCode},
34+
{"get bool", ErrGetBool("enabled", testErr), ErrGetBoolCode},
35+
{"remote file not found", ErrRemoteFileNotFound("https://example.com/file"), ErrRemoteFileNotFoundCode},
36+
{"reading remote file", ErrReadingRemoteFile(testErr), ErrReadingRemoteFileCode},
37+
{"reading local file", ErrReadingLocalFile(testErr), ErrReadingLocalFileCode},
38+
{"read file", ErrReadFile(testErr, "/tmp/file"), ErrReadFileCode},
39+
{"write file", ErrWriteFile(testErr, "/tmp/file"), ErrWriteFileCode},
40+
{"create file", ErrCreateFile(testErr, "/tmp/file"), ErrCreateFileCode},
41+
{"create dir", ErrCreateDir(testErr, "/tmp/dir"), ErrCreateDirCode},
42+
{"convert to byte", ErrConvertToByte(testErr), ErrConvertToByteCode},
43+
{"getting latest release tag", ErrGettingLatestReleaseTag(testErr), ErrGettingLatestReleaseTagCode},
44+
{"type cast", ErrTypeCast(testErr), ErrTypeCastCode},
45+
{"decode yaml", ErrDecodeYaml(testErr), ErrDecodeYamlCode},
46+
{"compress to tar gz", ErrCompressToTarGZ(testErr, "/tmp/file"), ErrCompressToTarGZCode},
47+
{"extract tar xz", ErrExtractTarXZ(testErr, "/tmp/archive"), ErrExtractTarXZCode},
48+
{"extract zip", ErrExtractZip(testErr, "/tmp/archive"), ErrExtractZipCode},
49+
{"read dir", ErrReadDir(testErr, "/tmp/dir"), ErrReadDirCode},
50+
{"file walk dir", ErrFileWalkDir(testErr, "/tmp/dir"), ErrFileWalkDirCode},
51+
{"relative path", ErrRelPath(testErr, "/tmp/dir"), ErrRelPathCode},
52+
{"copy file", ErrCopyFile(testErr), ErrCopyFileCode},
53+
{"close file", ErrCloseFile(testErr), ErrCloseFileCode},
54+
{"open file", ErrOpenFile("/tmp/file"), ErrOpenFileCode},
55+
{"google jwt invalid", ErrGoogleJwtInvalid(testErr), ErrGoogleJwtInvalidCode},
56+
{"google sheet service", ErrGoogleSheetSRV(testErr), ErrGoogleSheetSRVCode},
57+
{"writing into file", ErrWritingIntoFile(testErr, "artifact"), ErrWritingIntoFileCode},
58+
{"invalid protocol", ErrInvalidProtocol, ErrInvalidProtocolCode},
59+
{"extract type", ErrExtractType, ErrUnmarshalTypeCode},
60+
{"invalid schema version", ErrInvalidSchemaVersion, ErrInvalidSchemaVersionCode},
61+
}
62+
63+
for _, tc := range testCases {
64+
t.Run(tc.name, func(t *testing.T) {
65+
if got := meshkiterrors.GetCode(tc.err); got != tc.code {
66+
t.Fatalf("expected error code %q, got %q", tc.code, got)
67+
}
68+
})
69+
}
70+
}

utils/git.go

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,19 +19,21 @@ func Git() (version, commitHead string) {
1919
reader := bytes.NewReader(b)
2020
r := csv.NewReader(reader)
2121
rows, _ := r.ReadAll()
22-
for idx, row := range rows {
23-
if len(row) < 1 {
22+
fieldIndex := 0
23+
for _, row := range rows {
24+
if len(row) < 1 || (len(row) == 1 && strings.TrimSpace(row[0]) == "") {
2425
continue
2526
}
26-
switch idx {
27+
switch fieldIndex {
2728
case 0:
2829
{
2930
commitHead = strings.TrimSpace(row[0])
31+
fieldIndex++
3032
}
3133
case 1:
3234
{
3335
version = strings.TrimSpace(row[0])
34-
break
36+
return
3537
}
3638
}
3739
}

utils/google.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111

1212
var (
1313
GoogleSpreadSheetURL = "https://docs.google.com/spreadsheets/d/"
14+
newSheetsService = sheets.NewService
1415
)
1516

1617
func NewSheetSRV(cred string) (*sheets.Service, error) {
@@ -24,7 +25,7 @@ func NewSheetSRV(cred string) (*sheets.Service, error) {
2425
// create client with config and context
2526
client := config.Client(ctx)
2627
// create new service using client
27-
srv, err := sheets.NewService(ctx, option.WithHTTPClient(client))
28+
srv, err := newSheetsService(ctx, option.WithHTTPClient(client))
2829
if err != nil {
2930
return nil, ErrGoogleSheetSRV(err)
3031
}

utils/svg_utils.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,15 @@ import (
1010

1111
const XMLTAG = "<?xml version=\"1.0\" encoding=\"UTF-8\"?><!DOCTYPE svg>"
1212

13+
type xmlTokenEncoder interface {
14+
EncodeToken(xml.Token) error
15+
Flush() error
16+
}
17+
18+
var newXMLTokenEncoder = func(w io.Writer) xmlTokenEncoder {
19+
return xml.NewEncoder(w)
20+
}
21+
1322
// UpdateSVGString updates the width and height attributes of an SVG file and returns the modified SVG as a string.
1423
func UpdateSVGString(svgStr string, width, height int, skipHeader bool) (string, error) {
1524
// Create a reader for the SVG string.
@@ -22,7 +31,7 @@ func UpdateSVGString(svgStr string, width, height int, skipHeader bool) (string,
2231
var b bytes.Buffer
2332

2433
// Create an encoder for the buffer.
25-
e := xml.NewEncoder(&b)
34+
e := newXMLTokenEncoder(&b)
2635

2736
// Iterate through the tokens in the SVG string.
2837
for {

0 commit comments

Comments
 (0)