Skip to content

Commit 2256d0b

Browse files
committed
add tests for JSON typing/validation at fromJSON calls
1 parent 58e9469 commit 2256d0b

File tree

5 files changed

+180
-10
lines changed

5 files changed

+180
-10
lines changed

expr_sema_test.go

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -637,8 +637,8 @@ func TestExprSemanticsCheckOK(t *testing.T) {
637637
},
638638
{
639639
what: "non-special function",
640-
input: "fromJSON('{}')",
641-
expected: NewEmptyStrictObjectType(),
640+
input: "contains('hello, world', 'o, w')",
641+
expected: BoolType{},
642642
availSPFuncs: []string{"always"},
643643
},
644644
{
@@ -719,6 +719,15 @@ func TestExprSemanticsCheckOK(t *testing.T) {
719719
input: "!!('foo' || 10) && 20",
720720
expected: NumberType{},
721721
},
722+
{
723+
what: "fromJSON with JSON constant value",
724+
input: `fromJSON('{"foo":true,"bar":["foo", 12.3],"piyo":null}')`,
725+
expected: NewStrictObjectType(map[string]ExprType{
726+
"foo": BoolType{},
727+
"bar": &ArrayType{Elem: StringType{}}, // Element type was merged
728+
"piyo": NullType{},
729+
}),
730+
},
722731
}
723732

724733
allSPFuncs := []string{}
@@ -1231,6 +1240,13 @@ func TestExprSemanticsCheckError(t *testing.T) {
12311240
"must not start with the GITHUB_ prefix",
12321241
},
12331242
},
1243+
{
1244+
what: "broken JSON value at fromJSON argument",
1245+
input: `fromJSON('{"foo": true')`,
1246+
expected: []string{
1247+
"broken JSON string is passed to fromJSON() at offset 12",
1248+
},
1249+
},
12341250
}
12351251

12361252
allSP := []string{}

expr_type.go

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -428,15 +428,17 @@ func EqualTypes(l, r ExprType) bool {
428428
return l.Assignable(r) && r.Assignable(l)
429429
}
430430

431+
// typeOfJSONValue returns the type of the given JSON value. The JSON value is an any value decoded by json.Unmarshal.
432+
// https://pkg.go.dev/encoding/json#Unmarshal
433+
//
434+
// To unmarshal JSON into an interface value, Unmarshal stores one of these in the interface value:
435+
// - bool, for JSON booleans
436+
// - float64, for JSON numbers
437+
// - string, for JSON strings
438+
// - []interface{}, for JSON arrays
439+
// - map[string]interface{}, for JSON objects
440+
// - nil for JSON null
431441
func typeOfJSONValue(v any) ExprType {
432-
// https://pkg.go.dev/encoding/json#Unmarshal
433-
// To unmarshal JSON into an interface value, Unmarshal stores one of these in the interface value:
434-
// - bool, for JSON booleans
435-
// - float64, for JSON numbers
436-
// - string, for JSON strings
437-
// - []interface{}, for JSON arrays
438-
// - map[string]interface{}, for JSON objects
439-
// - nil for JSON null
440442
switch v := v.(type) {
441443
case bool:
442444
return BoolType{}

expr_type_test.go

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -929,3 +929,118 @@ func TestExprTypeDeepCopy(t *testing.T) {
929929
}
930930
}
931931
}
932+
933+
func TestExprTypeTypeOfJSONValue(t *testing.T) {
934+
tests := []struct {
935+
what string
936+
value any
937+
want ExprType
938+
}{
939+
{
940+
what: "null",
941+
value: nil,
942+
want: NullType{},
943+
},
944+
{
945+
what: "num",
946+
value: 1.0,
947+
want: NumberType{},
948+
},
949+
{
950+
what: "string",
951+
value: "hello",
952+
want: StringType{},
953+
},
954+
{
955+
what: "bool",
956+
value: true,
957+
want: BoolType{},
958+
},
959+
{
960+
what: "empty array",
961+
value: []any{},
962+
want: &ArrayType{Elem: AnyType{}},
963+
},
964+
{
965+
what: "array",
966+
value: []any{"hello", "world"},
967+
want: &ArrayType{Elem: StringType{}},
968+
},
969+
{
970+
what: "nested array",
971+
value: []any{[]any{[]any{[]any{"hi"}}}},
972+
want: &ArrayType{
973+
Elem: &ArrayType{
974+
Elem: &ArrayType{
975+
Elem: &ArrayType{
976+
Elem: StringType{},
977+
},
978+
},
979+
},
980+
},
981+
},
982+
{
983+
what: "merged array element",
984+
value: []any{"hello", 1.0, true},
985+
want: &ArrayType{Elem: StringType{}},
986+
},
987+
{
988+
what: "recursively merged array element",
989+
value: []any{[]any{"hello"}, []any{1.0}, []any{true}},
990+
want: &ArrayType{Elem: &ArrayType{Elem: StringType{}}},
991+
},
992+
{
993+
what: "array element fallback to any",
994+
value: []any{"hello", nil},
995+
want: &ArrayType{Elem: AnyType{}},
996+
},
997+
{
998+
what: "empty object",
999+
value: map[string]any{},
1000+
want: NewEmptyStrictObjectType(),
1001+
},
1002+
{
1003+
what: "object",
1004+
value: map[string]any{"hello": 1.0, "world": true},
1005+
want: NewStrictObjectType(map[string]ExprType{
1006+
"hello": NumberType{},
1007+
"world": BoolType{},
1008+
}),
1009+
},
1010+
{
1011+
what: "nested object",
1012+
value: map[string]any{
1013+
"hello": []any{1.0},
1014+
"world": map[string]any{
1015+
"foo": true,
1016+
"bar": "x",
1017+
},
1018+
},
1019+
want: NewStrictObjectType(map[string]ExprType{
1020+
"hello": &ArrayType{Elem: NumberType{}},
1021+
"world": NewStrictObjectType(map[string]ExprType{
1022+
"foo": BoolType{},
1023+
"bar": StringType{},
1024+
}),
1025+
}),
1026+
},
1027+
}
1028+
1029+
for _, tc := range tests {
1030+
t.Run(tc.what, func(t *testing.T) {
1031+
have := typeOfJSONValue(tc.value)
1032+
if !cmp.Equal(tc.want, have) {
1033+
t.Fatal(cmp.Diff(tc.want, have))
1034+
}
1035+
})
1036+
}
1037+
}
1038+
1039+
func TestExprTypePanicTypeOfJSONValue(t *testing.T) {
1040+
defer func() {
1041+
if r := recover(); r == nil {
1042+
t.Fatal("error didn't occur")
1043+
}
1044+
}()
1045+
typeOfJSONValue(struct{}{})
1046+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
test.yaml:12:37: broken JSON string is passed to fromJSON() at offset 4: unexpected end of JSON input [expression]
2+
test.yaml:13:37: broken JSON string is passed to fromJSON() at offset 6: unexpected end of JSON input [expression]
3+
test.yaml:14:37: broken JSON string is passed to fromJSON() at offset 0: unexpected end of JSON input [expression]
4+
test.yaml:24:19: object, array, and null values should not be evaluated in template with ${{ }} but evaluating the value of type null [expression]
5+
test.yaml:25:19: object, array, and null values should not be evaluated in template with ${{ }} but evaluating the value of type array<string> [expression]
6+
test.yaml:26:19: object, array, and null values should not be evaluated in template with ${{ }} but evaluating the value of type {array: array<bool>; bool: bool} [expression]
7+
test.yaml:27:19: object, array, and null values should not be evaluated in template with ${{ }} but evaluating the value of type array<bool> [expression]
8+
test.yaml:28:32: 1st argument of function call is not assignable. "{array: array<bool>; bool: bool}" cannot be assigned to "string". called function type is "contains(string, string) -> bool" [expression]
9+
test.yaml:28:32: 1st argument of function call is not assignable. "{array: array<bool>; bool: bool}" cannot be assigned to "array<any>". called function type is "contains(array<any>, any) -> bool" [expression]
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
on: push
2+
3+
jobs:
4+
foo:
5+
strategy:
6+
matrix:
7+
include:
8+
- string: ${{ fromJSON('"hello"') }}
9+
- null: ${{ fromJSON('null') }}
10+
- array: ${{ fromJSON('["foo", 1.2]') }}
11+
- object: ${{ fromJSON('{"bool":true,"array":[false]}') }}
12+
- invalid-1: ${{ fromJSON('"foo') }}
13+
- invalid-2: ${{ fromJSON('["foo"') }}
14+
- invalid-3: ${{ fromJSON('') }}
15+
runs-on: ubuntu-latest
16+
steps:
17+
# OK
18+
- run: echo ${{ matrix.string }}
19+
- run: echo ${{ matrix.array[0] }}
20+
- run: echo ${{ matrix.object.bool }}
21+
- run: echo ${{ contains(matrix.array, matrix.string) }}
22+
- run: echo ${{ matrix.invalid-1 }}
23+
# ERROR
24+
- run: echo ${{ matrix.null }}
25+
- run: echo ${{ matrix.array }}
26+
- run: echo ${{ matrix.object }}
27+
- run: echo ${{ matrix.object.array }}
28+
- run: echo ${{ contains(matrix.object, matrix.string) }}

0 commit comments

Comments
 (0)