| // Copyright 2025 The Go Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| package unify |
| |
| import ( |
| "bytes" |
| "fmt" |
| "iter" |
| "log" |
| "strings" |
| "testing" |
| "testing/fstest" |
| |
| "gopkg.in/yaml.v3" |
| ) |
| |
| func mustParse(expr string) Closure { |
| var c Closure |
| if err := yaml.Unmarshal([]byte(expr), &c); err != nil { |
| panic(err) |
| } |
| return c |
| } |
| |
| func oneValue(t *testing.T, c Closure) *Value { |
| t.Helper() |
| var v *Value |
| var i int |
| for v = range c.All() { |
| i++ |
| } |
| if i != 1 { |
| t.Fatalf("expected 1 value, got %d", i) |
| } |
| return v |
| } |
| |
| func printYaml(val any) { |
| fmt.Println(prettyYaml(val)) |
| } |
| |
| func prettyYaml(val any) string { |
| b, err := yaml.Marshal(val) |
| if err != nil { |
| panic(err) |
| } |
| var node yaml.Node |
| if err := yaml.Unmarshal(b, &node); err != nil { |
| panic(err) |
| } |
| |
| // Map lines to start offsets. We'll use this to figure out when nodes are |
| // "small" and should use inline style. |
| lines := []int{-1, 0} |
| for pos := 0; pos < len(b); { |
| next := bytes.IndexByte(b[pos:], '\n') |
| if next == -1 { |
| break |
| } |
| pos += next + 1 |
| lines = append(lines, pos) |
| } |
| lines = append(lines, len(b)) |
| |
| // Strip comments and switch small nodes to inline style |
| cleanYaml(&node, lines, len(b)) |
| |
| b, err = yaml.Marshal(&node) |
| if err != nil { |
| panic(err) |
| } |
| return string(b) |
| } |
| |
| func cleanYaml(node *yaml.Node, lines []int, endPos int) { |
| node.HeadComment = "" |
| node.FootComment = "" |
| node.LineComment = "" |
| |
| for i, n2 := range node.Content { |
| end2 := endPos |
| if i < len(node.Content)-1 { |
| end2 = lines[node.Content[i+1].Line] |
| } |
| cleanYaml(n2, lines, end2) |
| } |
| |
| // Use inline style? |
| switch node.Kind { |
| case yaml.MappingNode, yaml.SequenceNode: |
| if endPos-lines[node.Line] < 40 { |
| node.Style = yaml.FlowStyle |
| } |
| } |
| } |
| |
| func allYamlNodes(n *yaml.Node) iter.Seq[*yaml.Node] { |
| return func(yield func(*yaml.Node) bool) { |
| if !yield(n) { |
| return |
| } |
| for _, n2 := range n.Content { |
| for n3 := range allYamlNodes(n2) { |
| if !yield(n3) { |
| return |
| } |
| } |
| } |
| } |
| } |
| |
| func TestRoundTripString(t *testing.T) { |
| // Check that we can round-trip a string with regexp meta-characters in it. |
| const y = `!string test*` |
| t.Logf("input:\n%s", y) |
| |
| v1 := oneValue(t, mustParse(y)) |
| var buf1 strings.Builder |
| enc := yaml.NewEncoder(&buf1) |
| if err := enc.Encode(v1); err != nil { |
| log.Fatal(err) |
| } |
| enc.Close() |
| t.Logf("after parse 1:\n%s", buf1.String()) |
| |
| v2 := oneValue(t, mustParse(buf1.String())) |
| var buf2 strings.Builder |
| enc = yaml.NewEncoder(&buf2) |
| if err := enc.Encode(v2); err != nil { |
| log.Fatal(err) |
| } |
| enc.Close() |
| t.Logf("after parse 2:\n%s", buf2.String()) |
| |
| if buf1.String() != buf2.String() { |
| t.Fatal("parse 1 and parse 2 differ") |
| } |
| } |
| |
| func TestEmptyString(t *testing.T) { |
| // Regression test. Make sure an empty string is parsed as an exact string, |
| // not a regexp. |
| const y = `""` |
| t.Logf("input:\n%s", y) |
| |
| v1 := oneValue(t, mustParse(y)) |
| if !v1.Exact() { |
| t.Fatal("expected exact string") |
| } |
| } |
| |
| func TestImport(t *testing.T) { |
| // Test a basic import |
| main := strings.NewReader("!import x/y.yaml") |
| fs := fstest.MapFS{ |
| // Test a glob import with a relative path |
| "x/y.yaml": {Data: []byte("!import y/*.yaml")}, |
| "x/y/z.yaml": {Data: []byte("42")}, |
| } |
| cl, err := Read(main, "x.yaml", ReadOpts{FS: fs}) |
| if err != nil { |
| t.Fatal(err) |
| } |
| x := 42 |
| checkDecode(t, oneValue(t, cl), &x) |
| } |
| |
| func TestImportEscape(t *testing.T) { |
| // Make sure an import can't escape its subdirectory. |
| main := strings.NewReader("!import x/y.yaml") |
| fs := fstest.MapFS{ |
| "x/y.yaml": {Data: []byte("!import ../y/*.yaml")}, |
| "y/z.yaml": {Data: []byte("42")}, |
| } |
| _, err := Read(main, "x.yaml", ReadOpts{FS: fs}) |
| if err == nil { |
| t.Fatal("relative !import should have failed") |
| } |
| if !strings.Contains(err.Error(), "must not contain") { |
| t.Fatalf("unexpected error %v", err) |
| } |
| } |
| |
| func TestImportScope(t *testing.T) { |
| // Test that imports have different variable scopes. |
| main := strings.NewReader("[!import y.yaml, !import y.yaml]") |
| fs := fstest.MapFS{ |
| "y.yaml": {Data: []byte("$v")}, |
| } |
| cl1, err := Read(main, "x.yaml", ReadOpts{FS: fs}) |
| if err != nil { |
| t.Fatal(err) |
| } |
| cl2 := mustParse("[1, 2]") |
| res, err := Unify(cl1, cl2) |
| if err != nil { |
| t.Fatal(err) |
| } |
| checkDecode(t, oneValue(t, res), []int{1, 2}) |
| } |