Repository: grafana/tanka
Pull request #195 (2020-02-06 11:25:42)

Add tests for jsonnet functions and fix parseYAML.
- funcs.go was missing tests for the jsonnet native functions, this PR
adds some basic tests exercising some generic classses of arguments

- `parseYAML` seems to have been doing one extra step of marshalling the
YAML documents into JSON, which it probably shouldn't do and this PR
removes the functionality.

W.r.t. second point, the current implementation looks like
manifestJsonFrom Yaml or similar -- is there any use case for that that
would be worth adding it?

mplzik: This should https://github.com/grafana/tanka/issues/194 .

Patch:
diff --git a/pkg/jsonnet/native/funcs.go b/pkg/jsonnet/native/funcs.go
index dbdaa876..291f80df 100644
--- a/pkg/jsonnet/native/funcs.go
+++ b/pkg/jsonnet/native/funcs.go
@@ -9,7 +9,8 @@ import (
 
 	jsonnet "github.com/google/go-jsonnet"
 	"github.com/google/go-jsonnet/ast"
-	yaml "gopkg.in/yaml.v2"
+	"github.com/pkg/errors"
+	yaml "gopkg.in/yaml.v3"
 )
 
 // Funcs returns a slice of native Go functions that shall be available
@@ -52,19 +53,26 @@ var parseYAML = &jsonnet.NativeFunction{
 
 		d := yaml.NewDecoder(bytes.NewReader(data))
 		for {
-			var doc interface{}
+			var doc, jsonDoc interface{}
 			if err := d.Decode(&doc); err != nil {
 				if err == io.EOF {
 					break
 				}
-				return nil, err
+				return nil, errors.Wrap(err, "parsing yaml")
 			}
-			jsonDoc, err := json.Marshal(doc)
+
+			jsonRaw, err := json.Marshal(doc)
 			if err != nil {
-				return nil, err
+				return nil, errors.Wrap(err, "converting yaml to json")
+			}
+
+			if err := json.Unmarshal(jsonRaw, &jsonDoc); err != nil {
+				return nil, errors.Wrap(err, "converting yaml to json")
 			}
+
 			ret = append(ret, jsonDoc)
 		}
+
 		return ret, nil
 	},
 }
diff --git a/pkg/tanka/parse.go b/pkg/tanka/parse.go
index 118b6e44..c1d48c99 100644
--- a/pkg/tanka/parse.go
+++ b/pkg/tanka/parse.go
@@ -122,6 +122,9 @@ func evalJsonnet(baseDir string, env *v1alpha1.Config, extCode map[string]string
 		filepath.Join(baseDir, "main.jsonnet"),
 		ext...,
 	)
+	if err != nil {
+		return nil, err
+	}
 
 	var dict map[string]interface{}
 	if err := json.Unmarshal([]byte(raw), &dict); err != nil {


Test patch:
diff --git a/pkg/jsonnet/native/funcs_test.go b/pkg/jsonnet/native/funcs_test.go
new file mode 100644
index 00000000..d6cefbdf
--- /dev/null
+++ b/pkg/jsonnet/native/funcs_test.go
@@ -0,0 +1,190 @@
+package native
+
+import (
+	"encoding/json"
+	"fmt"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+// callNative calls a native function used by jsonnet VM.
+func callNative(name string, data []interface{}) (res interface{}, err error, callerr error) {
+	for _, fun := range Funcs() {
+		if fun.Name == name {
+			// Call the function
+			ret, err := fun.Func(data)
+			return ret, err, nil
+		}
+	}
+
+	return nil, nil, fmt.Errorf("could not find native function %s", name)
+}
+
+func TestParseJSONEmptyDict(t *testing.T) {
+	ret, err, callerr := callNative("parseJson", []interface{}{"{}"})
+
+	assert.Empty(t, callerr)
+	assert.Equal(t, map[string]interface{}{}, ret)
+	assert.Empty(t, err)
+}
+
+func TestParseJSONkeyValuet(t *testing.T) {
+	ret, err, callerr := callNative("parseJson", []interface{}{"{\"a\": 47}"})
+
+	assert.Empty(t, callerr)
+	assert.Equal(t, map[string]interface{}{"a": 47.0}, ret)
+	assert.Empty(t, err)
+}
+
+func TestParseJSONInvalid(t *testing.T) {
+	ret, err, callerr := callNative("parseJson", []interface{}{""})
+
+	assert.Empty(t, callerr)
+	assert.Empty(t, ret)
+	assert.IsType(t, &json.SyntaxError{}, err)
+}
+
+func TestParseYAMLEmpty(t *testing.T) {
+	ret, err, callerr := callNative("parseYaml", []interface{}{""})
+
+	assert.Empty(t, callerr)
+	assert.Equal(t, []interface{}{}, ret)
+	assert.Empty(t, err)
+}
+
+func TestParseYAMLKeyValue(t *testing.T) {
+	ret, err, callerr := callNative("parseYaml", []interface{}{"a: 47"})
+
+	assert.Empty(t, callerr)
+	assert.Equal(t, []interface{}{map[string]interface{}{"a": 47.0}}, ret)
+	assert.Empty(t, err)
+}
+
+func TestParseYAMLInvalid(t *testing.T) {
+	ret, err, callerr := callNative("parseYaml", []interface{}{"'"})
+
+	assert.Empty(t, callerr)
+	assert.Empty(t, ret)
+	assert.NotEmpty(t, err)
+}
+
+func TestManifestJSONFromJSON(t *testing.T) {
+	ret, err, callerr := callNative("manifestJsonFromJson", []interface{}{"{}", float64(4)})
+
+	assert.Empty(t, callerr)
+	assert.Equal(t, "{}\n", ret)
+	assert.Empty(t, err)
+}
+
+func TestManifestJSONFromJSONReindent(t *testing.T) {
+	ret, err, callerr := callNative("manifestJsonFromJson", []interface{}{"{ \"a\": 47}", float64(4)})
+
+	assert.Empty(t, callerr)
+	assert.Equal(t, "{\n    \"a\": 47\n}\n", ret)
+	assert.Empty(t, err)
+}
+
+func TestManifestJSONFromJSONInvalid(t *testing.T) {
+	ret, err, callerr := callNative("manifestJsonFromJson", []interface{}{"", float64(4)})
+
+	assert.Empty(t, callerr)
+	assert.Empty(t, ret)
+	assert.NotEmpty(t, err)
+}
+
+func TestManifestYAMLFromJSONEmpty(t *testing.T) {
+	ret, err, callerr := callNative("manifestYamlFromJson", []interface{}{"{}"})
+
+	assert.Empty(t, callerr)
+	assert.Equal(t, "{}\n", ret)
+	assert.Empty(t, err)
+}
+
+func TestManifestYAMLFromJSONKeyValue(t *testing.T) {
+	ret, err, callerr := callNative("manifestYamlFromJson", []interface{}{"{ \"a\": 47}"})
+
+	assert.Empty(t, callerr)
+	assert.Equal(t, "a: 47\n", ret)
+	assert.Empty(t, err)
+}
+
+func TestManifestYAMLFromJSONInvalid(t *testing.T) {
+	ret, err, callerr := callNative("manifestYamlFromJson", []interface{}{""})
+
+	assert.Empty(t, callerr)
+	assert.Empty(t, ret)
+	assert.NotEmpty(t, err)
+}
+
+func TestEscapeStringRegex(t *testing.T) {
+	ret, err, callerr := callNative("escapeStringRegex", []interface{}{""})
+
+	assert.Empty(t, callerr)
+	assert.Equal(t, "", ret)
+	assert.Empty(t, err)
+}
+
+func TestEscapeStringRegexValue(t *testing.T) {
+	ret, err, callerr := callNative("escapeStringRegex", []interface{}{"([0-9]+).*\\s"})
+
+	assert.Empty(t, callerr)
+	assert.Equal(t, "\\(\\[0-9\\]\\+\\)\\.\\*\\\\s", ret)
+	assert.Empty(t, err)
+}
+
+func TestEscapeStringRegexInvalid(t *testing.T) {
+	ret, err, callerr := callNative("escapeStringRegex", []interface{}{"([0-9]+"})
+
+	assert.Empty(t, callerr)
+	assert.Equal(t, "\\(\\[0-9\\]\\+", ret)
+	assert.Empty(t, err)
+}
+
+func TestRegexMatch(t *testing.T) {
+	ret, err, callerr := callNative("regexMatch", []interface{}{"", "a"})
+
+	assert.Empty(t, callerr)
+	assert.Equal(t, true, ret)
+	assert.Empty(t, err)
+}
+
+func TestRegexMatchNoMatch(t *testing.T) {
+	ret, err, callerr := callNative("regexMatch", []interface{}{"a", "b"})
+
+	assert.Empty(t, callerr)
+	assert.Equal(t, false, ret)
+	assert.Empty(t, err)
+}
+
+func TestRegexMatchInvalidRegex(t *testing.T) {
+	ret, err, callerr := callNative("regexMatch", []interface{}{"[0-", "b"})
+
+	assert.Empty(t, callerr)
+	assert.Empty(t, ret)
+	assert.NotEmpty(t, err)
+}
+
+func TestRegexSubstNoChange(t *testing.T) {
+	ret, err, callerr := callNative("regexSubst", []interface{}{"a", "b", "c"})
+
+	assert.Empty(t, callerr)
+	assert.Equal(t, "b", ret)
+	assert.Empty(t, err)
+}
+
+func TestRegexSubstValid(t *testing.T) {
+	ret, err, callerr := callNative("regexSubst", []interface{}{"p[^m]*", "pm", "poe"})
+
+	assert.Empty(t, callerr)
+	assert.Equal(t, "poem", ret)
+	assert.Empty(t, err)
+}
+
+func TestRegexSubstInvalid(t *testing.T) {
+	ret, err, callerr := callNative("regexSubst", []interface{}{"p[^m*", "pm", "poe"})
+
+	assert.Empty(t, callerr)
+	assert.Empty(t, ret)
+	assert.NotEmpty(t, err)
+}