| // Copyright 2018 The CUE Authors |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| package cue |
| |
| import ( |
| "fmt" |
| "log" |
| "strings" |
| "testing" |
| |
| "cuelang.org/go/cue/format" |
| ) |
| |
| func TestExport(t *testing.T) { |
| testCases := []struct { |
| raw bool // skip evaluation the root, fully raw |
| eval bool // evaluate the full export |
| noOpt bool |
| in, out string |
| }{{ |
| in: `"hello"`, |
| out: `"hello"`, |
| }, { |
| in: `'hello'`, |
| out: `'hello'`, |
| }, { |
| in: `'hello\nworld'`, |
| out: "'''" + |
| multiSep + "hello" + |
| multiSep + "world" + |
| multiSep + "'''", |
| }, { |
| in: `"hello\nworld"`, |
| out: `"""` + |
| multiSep + "hello" + |
| multiSep + "world" + |
| multiSep + `"""`, |
| }, { |
| in: `{ |
| $type: 3 |
| "_": int |
| "_foo": int |
| _bar: int |
| }`, |
| out: unindent(` |
| { |
| $type: 3 |
| "_": int |
| "_foo": int |
| _bar: int |
| }`), |
| }, { |
| in: "{ a: 1, b: a + 2, c: null, d: true, e: _, f: string }", |
| out: unindent(` |
| { |
| a: 1 |
| b: 3 |
| c: null |
| d: true |
| e: _ |
| f: string |
| }`), |
| }, { |
| // Here the failed lookups are not considered permanent |
| // failures, as the structs are open. |
| in: `{ a: { b: 2.0, s: "abc" }, b: a.b, c: a.c, d: a["d"], e: a.t[2:3] }`, |
| out: unindent(` |
| { |
| a: { |
| b: 2.0 |
| s: "abc" |
| } |
| b: 2.0 |
| c: a.c |
| d: a["d"] |
| e: a.t[2:3] |
| }`), |
| }, { |
| // Here the failed lookups are permanent failures as the structs are |
| // closed. |
| in: `{ a :: { b: 2.0, s: "abc" }, b: a.b, c: a.c, d: a["d"], e: a.t[2:3] }`, |
| out: unindent(` |
| { |
| a :: { |
| b: 2.0 |
| s: "abc" |
| } |
| b: 2.0 |
| c: _|_ // undefined field "c" |
| d: _|_ // undefined field "d" |
| e: _|_ // undefined field "t" |
| }`), |
| }, { |
| // Insert comma between error and inserted message. |
| in: `{ a: [ 3&4] }`, |
| out: unindent(` |
| { |
| a: [_|_, // conflicting values 3 and 4 |
| ] |
| }`), |
| }, { |
| in: `{ |
| a: 5*[int] |
| a: [1, 2, ...] |
| b: <=5*[int] |
| b: [1, 2, ...] |
| c: (>=3 & <=5)*[int] |
| c: [1, 2, ...] |
| d: >=2*[int] |
| d: [1, 2, ...] |
| e: [...int] |
| e: [1, 2, ...] |
| f: [1, 2, ...] |
| }`, |
| out: unindent(` |
| { |
| a: [1, 2, int, int, int] |
| b: <=5*[int] & [1, 2, ...] |
| c: (>=3 & <=5)*[int] & [1, 2, ...] |
| d: >=2*[int] & [1, 2, ...] |
| e: [1, 2] |
| f: [1, 2] |
| }`), |
| }, { |
| raw: true, |
| in: `{ |
| a: 5*[int] |
| a: [1, 2, ...] |
| b: <=5*[int] |
| b: [1, 2, ...] |
| c: (>=3 & <=5)*[int] |
| c: [1, 2, ...] |
| d: >=2*[int] |
| d: [1, 2, ...] |
| e: [...int] |
| e: [1, 2, ...] |
| f: [1, 2, ...] |
| }`, |
| out: unindent(` |
| { |
| a: 5*[int] & [1, 2, ...] |
| b: <=5*[int] & [1, 2, ...] |
| c: (>=3 & <=5)*[int] & [1, 2, ...] |
| d: >=2*[int] & [1, 2, ...] |
| e: [...int] & [1, 2, ...] |
| f: [1, 2, ...] |
| }`), |
| }, { |
| raw: true, |
| in: `{ a: { b: [] }, c: a.b, d: a["b"] }`, |
| out: unindent(` |
| { |
| a: b: [] |
| c: a.b |
| d: a["b"] |
| }`), |
| }, { |
| raw: true, |
| in: `{ a: *"foo" | *"bar" | *string | int, b: a[2:3] }`, |
| out: unindent(` |
| { |
| a: *"foo" | *"bar" | *string | int |
| b: a[2:3] |
| }`), |
| }, { |
| in: `{ |
| a: >=0 & <=10 & !=1 |
| }`, |
| out: unindent(` |
| { |
| a: >=0 & <=10 & !=1 |
| }`), |
| }, { |
| raw: true, |
| in: `{ |
| a: >=0 & <=10 & !=1 |
| }`, |
| out: unindent(` |
| { |
| a: >=0 & <=10 & !=1 |
| }`), |
| }, { |
| raw: true, |
| eval: true, |
| in: `{ |
| a: (*1 | 2) & (1 | *2) |
| b: [(*1 | 2) & (1 | *2)] |
| }`, |
| out: unindent(` |
| { |
| a: 1 | 2 | *_|_ |
| b: [1 | 2 | *_|_] |
| }`), |
| }, { |
| raw: true, |
| eval: true, |
| in: `{ |
| u16: int & >=0 & <=65535 |
| u32: uint32 |
| u64: uint64 |
| u128: uint128 |
| u8: uint8 |
| ua: uint16 & >0 |
| us: >=0 & <10_000 & int |
| i16: >=-32768 & int & <=32767 |
| i32: int32 & > 0 |
| i64: int64 |
| i128: int128 |
| f64: float64 |
| fi: float64 & int |
| }`, |
| out: unindent(` |
| { |
| u16: uint16 |
| u32: uint32 |
| u64: uint64 |
| u128: uint128 |
| u8: uint8 |
| ua: uint16 & >0 |
| us: uint & <10000 |
| i16: int16 |
| i32: int32 & >0 |
| i64: int64 |
| i128: int128 |
| f64: float64 |
| fi: int & float64 |
| }`), |
| }, { |
| raw: true, |
| in: `{ a: [1, 2], b: { for k, v in a if v > 1 { "\(k)": v } } }`, |
| out: unindent(` |
| { |
| a: [1, 2] |
| b: { |
| for k, v in a if v > 1 { |
| "\(k)": v |
| } |
| } |
| }`), |
| }, { |
| raw: true, |
| in: `{ a: [1, 2], b: [ v for k, v in a ] }`, |
| out: unindent(` |
| { |
| a: [1, 2] |
| b: [ v for k, v in a ] |
| }`), |
| }, { |
| raw: true, |
| in: `{ a: >=0 & <=10, b: "Count: \(a) times" }`, |
| out: unindent(` |
| { |
| a: >=0 & <=10 |
| b: "Count: \(a) times" |
| }`), |
| }, { |
| raw: true, |
| in: `{ a: "", b: len(a) }`, |
| out: unindent(` |
| { |
| a: "" |
| b: len(a) |
| }`), |
| }, { |
| raw: true, |
| eval: true, |
| in: `{ |
| b: { |
| idx: a[str] |
| str: string |
| } |
| b a b: 4 |
| a b: 3 |
| }`, |
| // reference to a must be redirected to outer a through alias |
| out: unindent(` |
| { |
| A = a |
| b: { |
| idx: A[str] |
| a: b: 4 |
| str: string |
| } |
| a: b: 3 |
| }`), |
| }, { |
| raw: true, |
| eval: true, |
| noOpt: true, |
| in: `{ |
| job <Name>: { |
| name: Name |
| replicas: uint | *1 @protobuf(10) |
| command: string |
| } |
| |
| job list command: "ls" |
| |
| job nginx: { |
| command: "nginx" |
| replicas: 2 |
| } |
| }`, |
| out: unindent(` |
| { |
| job: { |
| list: { |
| name: "list" |
| replicas: 1 @protobuf(10) |
| command: "ls" |
| } |
| nginx: { |
| name: "nginx" |
| replicas: 2 @protobuf(10) |
| command: "nginx" |
| } |
| } |
| }`), |
| }, { |
| // TODO: positions of embedded structs is not preserved. Use some kind |
| // of topological sort to preserve order. |
| raw: true, |
| in: `{ |
| emb :: { |
| a: 1 |
| |
| sub: { |
| f: 3 |
| } |
| } |
| def :: { |
| emb |
| |
| b: 2 |
| } |
| f :: { a: 10 } |
| e :: { |
| f |
| |
| b: int |
| [_]: <100 |
| } |
| }`, |
| out: unindent(` |
| { |
| emb :: { |
| a: 1 |
| sub: f: 3 |
| } |
| f :: { |
| a: 10 |
| } |
| def :: { |
| b: 2 |
| emb |
| } |
| e :: { |
| [string]: <100 |
| b: int |
| f |
| } |
| }`), |
| }, { |
| raw: true, |
| eval: true, |
| noOpt: true, |
| in: `{ |
| reg: { foo: 1, bar: { baz: 3 } } |
| def :: { |
| a: 1 |
| |
| sub: reg |
| } |
| val: def |
| def2 :: { |
| a: { b: int } |
| } |
| val2: def2 |
| }`, |
| out: unindent(` |
| { |
| reg: { |
| foo: 1 |
| bar: baz: 3 |
| } |
| def :: { |
| a: 1 |
| sub: { |
| foo: 1 |
| bar: baz: 3 |
| } |
| } |
| val: { |
| a: 1 |
| sub: { |
| foo: 1 |
| bar: baz: 3 |
| } |
| } |
| def2 :: { |
| a: b: int |
| } |
| val2: a: b: int |
| }`), |
| }, { |
| raw: true, |
| eval: true, |
| in: `{ |
| b: [{ |
| [X=_]: int |
| if a > 4 { |
| f: 4 |
| } |
| }][a] |
| a: int |
| c: *1 | 2 |
| }`, |
| // reference to a must be redirected to outer a through alias |
| out: unindent(` |
| { |
| b: [{ |
| [X=string]: int |
| if a > 4 { |
| f: 4 |
| } |
| }][a] |
| a: int |
| c: 1 |
| }`), |
| }, { |
| raw: true, |
| in: `{ |
| if false { |
| { a: 1 } | { b: 1 } |
| } |
| }`, |
| // reference to a must be redirected to outer a through alias |
| out: unindent(` |
| { |
| if false { |
| { |
| a: 1 |
| } | { |
| b: 1 |
| } |
| } |
| }`), |
| }, { |
| raw: true, |
| eval: true, |
| in: `{ |
| Foo :: { |
| Bar :: Foo | string |
| } |
| }`, |
| out: unindent(` |
| { |
| Foo :: { |
| Bar :: Foo | string |
| } |
| }`), |
| }, { |
| raw: true, |
| eval: true, |
| in: `{ |
| FindInMap :: { |
| "Fn::FindInMap" :: [string | FindInMap] |
| } |
| a: [...string] |
| }`, |
| out: unindent(` |
| { |
| FindInMap :: { |
| "Fn::FindInMap" :: [string | FindInMap] |
| } |
| a: [] |
| }`)}, { |
| raw: true, |
| eval: true, |
| noOpt: true, |
| in: `{ |
| And :: { |
| "Fn::And": [...(3 | And)] |
| } |
| Ands: And & { |
| "Fn::And" : [_] |
| } |
| }`, |
| out: unindent(` |
| { |
| And :: { |
| "Fn::And": [] |
| } |
| Ands: "Fn::And": [3 | And] |
| }`), |
| }, { |
| raw: true, |
| eval: true, |
| noOpt: true, |
| in: `{ |
| Foo :: { |
| sgl: Bar |
| ref: null | Foo |
| ext: Bar | null |
| ref: null | Foo |
| ref2: null | Foo.sgl |
| ... |
| } |
| Foo :: { |
| Foo: 2 |
| ... |
| } |
| Bar :: string |
| }`, |
| out: unindent(` |
| { |
| FOO = Foo |
| FOO658221 = Foo |
| Foo :: { |
| Foo: 2 |
| sgl: string |
| ref: null | { |
| Foo: 2 |
| sgl: Bar |
| ref: (null | FOO) & (null | FOO) |
| ext: Bar | null |
| ref2: null | FOO.sgl |
| } |
| ext: Bar | null |
| ref2: null | FOO658221.sgl |
| } |
| Bar :: string |
| }`), |
| }, { |
| raw: true, |
| eval: true, |
| in: `{ |
| A: [uint] |
| B: A & ([10] | [192]) |
| }`, |
| out: unindent(` |
| { |
| A: [>=0] |
| B: [10] | [192] |
| }`), |
| }} |
| for _, tc := range testCases { |
| t.Run("", func(t *testing.T) { |
| body := fmt.Sprintf("Test: %s", tc.in) |
| ctx, obj := compileFile(t, body) |
| ctx.trace = *traceOn |
| var root value = obj |
| if !tc.raw { |
| root = testResolve(ctx, obj, evalFull) |
| } |
| t.Log(debugStr(ctx, root)) |
| |
| n := root.(*structLit).arcs[0].v |
| v := newValueRoot(ctx, n) |
| |
| opts := options{raw: !tc.eval, omitOptional: tc.noOpt} |
| node, _ := export(ctx, v.eval(ctx), opts) |
| b, err := format.Node(node, format.Simplify()) |
| if err != nil { |
| log.Fatal(err) |
| } |
| if got := string(b); got != tc.out { |
| t.Errorf("\ngot %v;\nwant %v", got, tc.out) |
| } |
| }) |
| } |
| } |
| |
| func TestExportFile(t *testing.T) { |
| testCases := []struct { |
| eval bool // evaluate the full export |
| in, out string |
| opts []Option |
| }{{ |
| in: ` |
| import "strings" |
| |
| a: strings.ContainsAny("c") |
| `, |
| out: unindent(` |
| import "strings" |
| |
| a: strings.ContainsAny("c")`), |
| }, { |
| in: ` |
| import "time" |
| |
| a: time.Time |
| `, |
| out: unindent(` |
| import "time" |
| |
| a: time.Time`), |
| }, { |
| in: ` |
| import "time" |
| |
| { |
| a: time.Time |
| } & { |
| time: int |
| } `, |
| out: unindent(` |
| import timex "time" |
| |
| time: int |
| a: timex.Time`), |
| }, { |
| in: ` |
| import time2 "time" |
| |
| a: time2.Time`, |
| out: unindent(` |
| import "time" |
| |
| a: time.Time`), |
| }, { |
| in: ` |
| import time2 "time" |
| |
| time: int |
| a: time2.Time`, |
| out: unindent(` |
| import time2 "time" |
| |
| time: int |
| a: time2.Time`), |
| }, { |
| in: ` |
| import "strings" |
| |
| a: strings.TrimSpace(" c ") |
| `, |
| out: unindent(` |
| import "strings" |
| |
| a: strings.TrimSpace(" c ")`), |
| }, { |
| in: ` |
| import "strings" |
| |
| stringsx = strings |
| |
| a: { |
| strings: stringsx.ContainsAny("c") |
| } |
| `, |
| out: unindent(` |
| import "strings" |
| |
| STRINGS = strings |
| a: strings: STRINGS.ContainsAny("c")`), |
| }, { |
| in: ` |
| a: b - 100 |
| b: a + 100 |
| `, |
| out: unindent(` |
| { |
| a: b - 100 |
| b: a + 100 |
| }`), |
| }, { |
| in: `A: { |
| [_]: B |
| } @protobuf(1,"test") |
| |
| B: {} |
| B: {a: int} | {b: int} |
| `, |
| out: unindent(` |
| { |
| A: { |
| [string]: B |
| } @protobuf(1,"test") |
| B: { |
| } & ({ |
| a: int |
| } | { |
| b: int |
| }) |
| }`), |
| }, { |
| in: ` |
| import "time" |
| |
| a: { b: time.Duration } | { c: time.Duration } |
| `, |
| out: unindent(` |
| import "time" |
| |
| a: { |
| b: time.Duration |
| } | { |
| c: time.Duration |
| }`), |
| }, { |
| // a closed struct unified with a struct with a template restrictions is |
| // exported as a conjunction of two structs. |
| eval: true, |
| in: ` |
| A :: { b: int } |
| a: A & { [string]: <10 } |
| B :: a |
| `, |
| out: unindent(` |
| { |
| A :: { |
| b: int |
| } |
| a: close({ |
| b: <10 |
| }) |
| B :: { |
| b: <10 |
| } |
| }`), |
| }, { |
| eval: true, |
| in: `{ |
| reg: { foo: 1, bar: { baz: 3 } } |
| def :: { |
| a: 1 |
| |
| sub: reg |
| } |
| val: def |
| }`, |
| out: unindent(` |
| { |
| reg: { |
| foo: 1 |
| bar: baz: 3 |
| } |
| def :: { |
| a: 1 |
| sub: { |
| foo: 1 |
| bar: { |
| baz: 3 |
| ... |
| } |
| ... |
| } |
| } |
| val: close({ |
| a: 1 |
| sub: { |
| foo: 1 |
| bar: baz: 3 |
| } |
| }) |
| }`), |
| }, { |
| eval: true, |
| in: ` |
| T :: { |
| [_]: int64 |
| } |
| X :: { |
| x: int |
| } & T |
| x: X |
| `, |
| out: unindent(` |
| { |
| T :: { |
| [string]: int64 |
| } |
| x: { |
| [string]: int64 |
| x: int64 |
| } |
| X :: { |
| [string]: int64 |
| x: int64 |
| } |
| }`), |
| }, { |
| eval: true, |
| opts: []Option{Optional(false)}, |
| in: ` |
| T :: { |
| [_]: int64 |
| } |
| X :: { |
| x: int |
| } & T |
| x: X |
| `, |
| out: unindent(` |
| { |
| T :: { |
| } |
| x: x: int64 |
| X :: { |
| x: int64 |
| } |
| }`), |
| }, { |
| eval: true, |
| in: `{ |
| reg: { foo: 1, bar: { baz: 3 } } |
| def :: { |
| a: 1 |
| |
| sub: reg |
| } |
| val: def |
| def2 :: { |
| a: { b: int } |
| } |
| val2: def2 |
| }`, |
| out: unindent(` |
| { |
| reg: { |
| foo: 1 |
| bar: baz: 3 |
| } |
| def :: { |
| a: 1 |
| sub: { |
| foo: 1 |
| bar: { |
| baz: 3 |
| ... |
| } |
| ... |
| } |
| } |
| val: close({ |
| a: 1 |
| sub: { |
| foo: 1 |
| bar: baz: 3 |
| } |
| }) |
| def2 :: { |
| a: b: int |
| } |
| val2: close({ |
| a: close({ |
| b: int |
| }) |
| }) |
| }`), |
| }, { |
| eval: true, |
| in: ` |
| a?: 1 |
| b?: 2 |
| b?: 2 |
| c?: 3 |
| c: 3`, |
| out: unindent(` |
| { |
| a?: 1 |
| b?: 2 |
| c: 3 |
| }`), |
| }, { |
| eval: true, |
| in: ` |
| A :: { |
| [=~"^[a-s]*$"]: int |
| } |
| B :: { |
| [=~"^[m-z]+"]: int |
| } |
| C: {A & B} |
| D :: {A & B} |
| `, |
| // TODO: the outer close of C could be optimized away. |
| out: unindent(` |
| { |
| A :: { |
| [=~"^[a-s]*$"]: int |
| } |
| B :: { |
| [=~"^[m-z]+"]: int |
| } |
| C: close({ |
| close({ |
| [=~"^[a-s]*$"]: int |
| }) & close({ |
| [=~"^[m-z]+"]: int |
| }) |
| }) |
| D :: { |
| close({ |
| [=~"^[a-s]*$"]: int |
| }) & close({ |
| [=~"^[m-z]+"]: int |
| }) |
| } |
| }`), |
| }, { |
| eval: true, |
| in: ` |
| x: [string]: int |
| a: [P=string]: { |
| b: x[P] |
| c: P |
| e: len(P) |
| } |
| `, |
| out: unindent(` |
| { |
| x: [string]: int |
| a: [P=string]: { |
| b: x[P] |
| c: string |
| e: len(P) |
| } |
| }`), |
| }} |
| for _, tc := range testCases { |
| t.Run("", func(t *testing.T) { |
| var r Runtime |
| inst, err := r.Compile("test", tc.in) |
| if err != nil { |
| t.Fatal(err) |
| } |
| opts := tc.opts |
| if !tc.eval { |
| opts = []Option{Raw()} |
| } |
| b, err := format.Node(inst.Value().Syntax(opts...), format.Simplify()) |
| if err != nil { |
| log.Fatal(err) |
| } |
| if got := strings.TrimSpace(string(b)); got != tc.out { |
| t.Errorf("\ngot:\n%v\nwant:\n%v", got, tc.out) |
| } |
| }) |
| } |
| } |
| |
| func unindent(s string) string { |
| lines := strings.Split(s, "\n")[1:] |
| ws := lines[0][:len(lines[0])-len(strings.TrimLeft(lines[0], " \t"))] |
| for i, s := range lines { |
| if s == "" { |
| continue |
| } |
| if !strings.HasPrefix(s, ws) { |
| panic("invalid indentation") |
| } |
| lines[i] = lines[i][len(ws):] |
| } |
| return strings.Join(lines, "\n") |
| } |