This is an automated email from the ASF dual-hosted git repository.

kezhenxu94 pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/skywalking-infra-e2e.git


The following commit(s) were added to refs/heads/main by this push:
     new 5682560  Implement the `contains` action (#9)
5682560 is described below

commit 56825606333ed0b9b6931b9ec1efac4630f4bd96
Author: Hoshea Jiang <[email protected]>
AuthorDate: Mon Mar 1 22:50:39 2021 +0800

    Implement the `contains` action (#9)
    
    * implement the `contains` action
    
    * Add contains action
    
    * Polish code and add a unit test
    
    * Add header licenses
    
    Co-authored-by: kezhenxu94 <[email protected]>
---
 internal/components/verifier/verifier.go      |   1 +
 internal/components/verifier/verifier_test.go | 150 ++++++++++++++++++++++++++
 test/verify/2.actual.yaml                     |  27 +++++
 test/verify/2.expected.yaml                   |  26 +++++
 third-party/go/template/exec.go               |  57 ++++++----
 third-party/go/template/parse/lex.go          |  46 ++++----
 third-party/go/template/parse/node.go         |  62 +++++------
 third-party/go/template/parse/parse.go        |  12 +--
 8 files changed, 298 insertions(+), 83 deletions(-)

diff --git a/internal/components/verifier/verifier.go 
b/internal/components/verifier/verifier.go
index 84150c1..2f4675a 100644
--- a/internal/components/verifier/verifier.go
+++ b/internal/components/verifier/verifier.go
@@ -94,6 +94,7 @@ func verify(actualData, expectedTemplate string) error {
        }
 
        if !cmp.Equal(expected, actual) {
+               // TODO: use a custom Reporter (suggested by the comment of 
cmp.Diff)
                diff := cmp.Diff(expected, actual)
                fmt.Println(diff)
                return &MismatchError{}
diff --git a/internal/components/verifier/verifier_test.go 
b/internal/components/verifier/verifier_test.go
new file mode 100644
index 0000000..f7d073a
--- /dev/null
+++ b/internal/components/verifier/verifier_test.go
@@ -0,0 +1,150 @@
+// Licensed to Apache Software Foundation (ASF) under one or more contributor
+// license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright
+// ownership. Apache Software Foundation (ASF) licenses this file to you 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 verifier
+
+import "testing"
+
+func TestVerify(t *testing.T) {
+       type args struct {
+               actualData       string
+               expectedTemplate string
+       }
+       tests := []struct {
+               name    string
+               args    args
+               wantErr bool
+               err     error
+       }{
+               {
+                       name: "should contain two elements",
+                       args: args{
+                               actualData: `
+metrics:
+  - name: business-zone::projectA
+    id: YnVzaW5lc3Mtem9uZTo6cHJvamVjdEE=.1
+    value: 1
+  - name: system::load balancer1
+    id: c3lzdGVtOjpsb2FkIGJhbGFuY2VyMQ==.1
+    value: 0
+  - name: system::load balancer2
+    id: WW91cl9BcHBsaWNhdGlvbk5hbWU=.1
+    value: 2
+`,
+                               expectedTemplate: `
+metrics:
+{{- contains .metrics }}
+  - name: {{ notEmpty .name }}
+    id: {{ notEmpty .id }}
+    value: {{ gt .value 0 }}
+  - name: {{ notEmpty .name }}
+    id: {{ notEmpty .id }}
+    value: {{ gt .value 1 }}
+{{- end }}
+`,
+                       },
+                       wantErr: false,
+               },
+               {
+                       name: "fail to contain two elements",
+                       args: args{
+                               actualData: `
+metrics:
+  - name: business-zone::projectA
+    id: YnVzaW5lc3Mtem9uZTo6cHJvamVjdEE=.1
+    value: 1
+  - name: system::load balancer1
+    id: c3lzdGVtOjpsb2FkIGJhbGFuY2VyMQ==.1
+    value: 0
+  - name: system::load balancer2
+    id: WW91cl9BcHBsaWNhdGlvbk5hbWU=.1
+    value: 1
+`,
+                               expectedTemplate: `
+metrics:
+{{- contains .metrics }}
+  - name: {{ notEmpty .name }}
+    id: {{ notEmpty .id }}
+    value: {{ gt .value 0 }}
+  - name: {{ notEmpty .name }}
+    id: {{ notEmpty .id }}
+    value: {{ gt .value 1 }}
+{{- end }}
+`,
+                       },
+                       wantErr: true,
+               },
+               {
+                       name: "should contain one element",
+                       args: args{
+                               actualData: `
+metrics:
+  - name: business-zone::projectA
+    id: YnVzaW5lc3Mtem9uZTo6cHJvamVjdEE=.1
+    value: 1
+  - name: system::load balancer1
+    id: c3lzdGVtOjpsb2FkIGJhbGFuY2VyMQ==.1
+    value: 0
+  - name: system::load balancer2
+    id: WW91cl9BcHBsaWNhdGlvbk5hbWU=.1
+    value: 2
+`,
+                               expectedTemplate: `
+metrics:
+{{- contains .metrics }}
+  - name: {{ notEmpty .name }}
+    id: {{ notEmpty .id }}
+    value: {{ gt .value 1 }}
+{{- end }}
+`,
+                       },
+                       wantErr: false,
+               },
+               {
+                       name: "fail to contain one element",
+                       args: args{
+                               actualData: `
+metrics:
+  - name: business-zone::projectA
+    id: YnVzaW5lc3Mtem9uZTo6cHJvamVjdEE=.1
+    value: 1
+  - name: system::load balancer1
+    id: c3lzdGVtOjpsb2FkIGJhbGFuY2VyMQ==.1
+    value: 0
+  - name: system::load balancer2
+    id: WW91cl9BcHBsaWNhdGlvbk5hbWU=.1
+    value: 2
+`,
+                               expectedTemplate: `
+metrics:
+{{- contains .metrics }}
+  - name: {{ notEmpty .name }}
+    id: {{ notEmpty .id }}
+    value: {{ gt .value 3 }}
+{{- end }}
+`,
+                       },
+                       wantErr: true,
+               },
+       }
+       for _, tt := range tests {
+               t.Run(tt.name, func(t *testing.T) {
+                       if err := verify(tt.args.actualData, 
tt.args.expectedTemplate); (err != nil) != tt.wantErr {
+                               t.Errorf("verify() error = %v, wantErr %v", 
err, tt.wantErr)
+                       }
+               })
+       }
+}
diff --git a/test/verify/2.actual.yaml b/test/verify/2.actual.yaml
new file mode 100644
index 0000000..d0872b5
--- /dev/null
+++ b/test/verify/2.actual.yaml
@@ -0,0 +1,27 @@
+# Licensed to Apache Software Foundation (ASF) under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Apache Software Foundation (ASF) licenses this file to you 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.
+
+metrics:
+  - name: business-zone::projectA
+    id: YnVzaW5lc3Mtem9uZTo6cHJvamVjdEE=.1
+    value: 1
+  - name: system::load balancer1
+    id: c3lzdGVtOjpsb2FkIGJhbGFuY2VyMQ==.1
+    value: 0
+  - name: system::load balancer2
+    id: WW91cl9BcHBsaWNhdGlvbk5hbWU=.1
+    value: 2
diff --git a/test/verify/2.expected.yaml b/test/verify/2.expected.yaml
new file mode 100644
index 0000000..7a69757
--- /dev/null
+++ b/test/verify/2.expected.yaml
@@ -0,0 +1,26 @@
+# Licensed to Apache Software Foundation (ASF) under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Apache Software Foundation (ASF) licenses this file to you 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.
+
+metrics:
+{{- contains .metrics }}
+  - name: {{ notEmpty .name }}
+    id: {{ notEmpty .id }}
+    value: {{ gt .value 0 }}
+  - name: {{ notEmpty .name }}
+    id: {{ notEmpty .id }}
+    value: {{ gt .value 1 }}
+{{- end }}
diff --git a/third-party/go/template/exec.go b/third-party/go/template/exec.go
index b4af1ad..0d88f70 100644
--- a/third-party/go/template/exec.go
+++ b/third-party/go/template/exec.go
@@ -7,13 +7,16 @@ package template
 import (
        "bytes"
        "fmt"
-       "github.com/apache/skywalking-infra-e2e/third-party/go/internal/fmtsort"
-       "github.com/apache/skywalking-infra-e2e/third-party/go/template/parse"
-       "gopkg.in/yaml.v3"
        "io"
        "reflect"
        "runtime"
        "strings"
+
+       "github.com/apache/skywalking-infra-e2e/internal/logger"
+       "github.com/apache/skywalking-infra-e2e/third-party/go/internal/fmtsort"
+       "github.com/apache/skywalking-infra-e2e/third-party/go/template/parse"
+
+       "gopkg.in/yaml.v2"
 )
 
 // maxExecDepth specifies the maximum stack depth of templates within
@@ -274,8 +277,8 @@ func (s *state) walk(dot reflect.Value, node parse.Node) {
                }
        case *parse.WithNode:
                s.walkIfOrWith(parse.NodeWith, dot, node.Pipe, node.List, 
node.ElseList)
-       case *parse.AtLeastOnceNode:
-               s.walkAtLeastOnce(dot, node)
+       case *parse.ContainsNode:
+               s.walkContains(dot, node)
        default:
                s.errorf("unknown node: %s", node)
        }
@@ -398,13 +401,13 @@ func (s *state) walkRange(dot reflect.Value, r 
*parse.RangeNode) {
        }
 }
 
-func (s *state) walkAtLeastOnce(dot reflect.Value, r *parse.AtLeastOnceNode) {
+func (s *state) walkContains(dot reflect.Value, r *parse.ContainsNode) {
        s.at(r)
        defer s.pop(s.mark())
        val, _ := indirect(s.evalPipeline(dot, r.Pipe))
        // mark top of stack before any variables in the body are pushed.
        mark := s.mark()
-       oneIteration := func(index, elem reflect.Value) interface{} {
+       oneIteration := func(index, elem reflect.Value) []interface{} {
                var b bytes.Buffer
                ob := s.wr
                s.wr = &b
@@ -422,8 +425,11 @@ func (s *state) walkAtLeastOnce(dot reflect.Value, r 
*parse.AtLeastOnceNode) {
 
                s.wr = ob
 
-               var re interface{}
-               yaml.Unmarshal(b.Bytes(), &re)
+               // the contents inside `contains` must be an array
+               var re []interface{}
+               if err := yaml.Unmarshal(b.Bytes(), &re); err != nil {
+                       logger.Log.Errorf("failed to unmarshal index: %v", 
index)
+               }
                return re
        }
        switch val.Kind() {
@@ -431,23 +437,31 @@ func (s *state) walkAtLeastOnce(dot reflect.Value, r 
*parse.AtLeastOnceNode) {
                if val.Len() == 0 {
                        break
                }
-               match := false
-               sss := make([]interface{}, val.Len())
+               expectedSize := 0
+               // matched stores the matched pair of indices <expected index>: 
<actual index>
+               matched := make(map[int]int)
+               output := make([]interface{}, val.Len())
                for i := 0; i < val.Len(); i++ {
-                       actual := oneIteration(reflect.ValueOf(i), val.Index(i))
-                       sss[i] = actual
-                       value, _ := printableValue(val.Index(i))
-                       if fmt.Sprint(value) == fmt.Sprint(actual) {
-                               match = true
-                               break
+                       expectedArr := oneIteration(reflect.ValueOf(i), 
val.Index(i))
+                       // expectedSize is the number of elements that the 
actual array should contain.
+                       expectedSize = len(expectedArr)
+                       actual, _ := printableValue(val.Index(i))
+                       for j, expected := range expectedArr {
+                               if fmt.Sprint(actual) == fmt.Sprint(expected) {
+                                       matched[j] = i
+                                       output[i] = actual
+                               } else {
+                                       output[i] = expected
+                               }
                        }
                }
+
                var marshal []byte
-               if match {
+               if len(matched) == expectedSize {
                        value, _ := printableValue(val)
                        marshal, _ = yaml.Marshal(value)
                } else {
-                       marshal, _ = yaml.Marshal(sss)
+                       marshal, _ = yaml.Marshal(output)
                }
                s.wr.Write(append([]byte("\n"), marshal...))
                return
@@ -479,10 +493,7 @@ func (s *state) walkAtLeastOnce(dot reflect.Value, r 
*parse.AtLeastOnceNode) {
        case reflect.Invalid:
                break // An invalid value is likely a nil map, etc. and acts 
like an empty map.
        default:
-               s.errorf("range can't iterate over %v", val)
-       }
-       if r.ElseList != nil {
-               s.walk(dot, r.ElseList)
+               s.errorf("contains can't iterate over %v", val)
        }
 }
 
diff --git a/third-party/go/template/parse/lex.go 
b/third-party/go/template/parse/lex.go
index 8ebb789..19eed9a 100644
--- a/third-party/go/template/parse/lex.go
+++ b/third-party/go/template/parse/lex.go
@@ -59,32 +59,32 @@ const (
        itemText       // plain text
        itemVariable   // variable starting with '$', such as '$' or  '$1' or 
'$hello'
        // Keywords appear after all the rest.
-       itemKeyword     // used only to delimit the keywords
-       itemBlock       // block keyword
-       itemDot         // the cursor, spelled '.'
-       itemDefine      // define keyword
-       itemElse        // else keyword
-       itemEnd         // end keyword
-       itemIf          // if keyword
-       itemNil         // the untyped nil constant, easiest to treat as a 
keyword
-       itemRange       // range keyword
-       itemTemplate    // template keyword
-       itemWith        // with keyword
-       itemAtLeastOnce // atLeastOnce keyword
+       itemKeyword  // used only to delimit the keywords
+       itemBlock    // block keyword
+       itemDot      // the cursor, spelled '.'
+       itemDefine   // define keyword
+       itemElse     // else keyword
+       itemEnd      // end keyword
+       itemIf       // if keyword
+       itemNil      // the untyped nil constant, easiest to treat as a keyword
+       itemRange    // range keyword
+       itemTemplate // template keyword
+       itemWith     // with keyword
+       itemContains // contains keyword
 )
 
 var key = map[string]itemType{
-       ".":           itemDot,
-       "block":       itemBlock,
-       "define":      itemDefine,
-       "else":        itemElse,
-       "end":         itemEnd,
-       "if":          itemIf,
-       "range":       itemRange,
-       "nil":         itemNil,
-       "template":    itemTemplate,
-       "with":        itemWith,
-       "atLeastOnce": itemAtLeastOnce,
+       ".":        itemDot,
+       "block":    itemBlock,
+       "define":   itemDefine,
+       "else":     itemElse,
+       "end":      itemEnd,
+       "if":       itemIf,
+       "range":    itemRange,
+       "nil":      itemNil,
+       "template": itemTemplate,
+       "with":     itemWith,
+       "contains": itemContains,
 }
 
 const eof = -1
diff --git a/third-party/go/template/parse/node.go 
b/third-party/go/template/parse/node.go
index 57ca52c..938d5bf 100644
--- a/third-party/go/template/parse/node.go
+++ b/third-party/go/template/parse/node.go
@@ -50,27 +50,27 @@ func (t NodeType) Type() NodeType {
 }
 
 const (
-       NodeText        NodeType = iota // Plain text.
-       NodeAction                      // A non-control action such as a field 
evaluation.
-       NodeBool                        // A boolean constant.
-       NodeChain                       // A sequence of field accesses.
-       NodeCommand                     // An element of a pipeline.
-       NodeDot                         // The cursor, dot.
-       nodeElse                        // An else action. Not added to tree.
-       nodeEnd                         // An end action. Not added to tree.
-       NodeField                       // A field or method name.
-       NodeIdentifier                  // An identifier; always a function 
name.
-       NodeIf                          // An if action.
-       NodeList                        // A list of Nodes.
-       NodeNil                         // An untyped nil constant.
-       NodeNumber                      // A numerical constant.
-       NodePipe                        // A pipeline of commands.
-       NodeRange                       // A range action.
-       NodeString                      // A string constant.
-       NodeTemplate                    // A template invocation action.
-       NodeVariable                    // A $ variable.
-       NodeWith                        // A with action.
-       NodeAtLeastOnce                 // An atLeastOnce action.
+       NodeText       NodeType = iota // Plain text.
+       NodeAction                     // A non-control action such as a field 
evaluation.
+       NodeBool                       // A boolean constant.
+       NodeChain                      // A sequence of field accesses.
+       NodeCommand                    // An element of a pipeline.
+       NodeDot                        // The cursor, dot.
+       nodeElse                       // An else action. Not added to tree.
+       nodeEnd                        // An end action. Not added to tree.
+       NodeField                      // A field or method name.
+       NodeIdentifier                 // An identifier; always a function name.
+       NodeIf                         // An if action.
+       NodeList                       // A list of Nodes.
+       NodeNil                        // An untyped nil constant.
+       NodeNumber                     // A numerical constant.
+       NodePipe                       // A pipeline of commands.
+       NodeRange                      // A range action.
+       NodeString                     // A string constant.
+       NodeTemplate                   // A template invocation action.
+       NodeVariable                   // A $ variable.
+       NodeWith                       // A with action.
+       NodeContains                   // A contains action.
 )
 
 // Nodes.
@@ -829,8 +829,8 @@ func (b *BranchNode) writeTo(sb *strings.Builder) {
                name = "range"
        case NodeWith:
                name = "with"
-       case NodeAtLeastOnce:
-               name = "atLeastOnce"
+       case NodeContains:
+               name = "contains"
        default:
                panic("unknown branch type")
        }
@@ -859,8 +859,8 @@ func (b *BranchNode) Copy() Node {
                return b.tr.newRange(b.Pos, b.Line, b.Pipe, b.List, b.ElseList)
        case NodeWith:
                return b.tr.newWith(b.Pos, b.Line, b.Pipe, b.List, b.ElseList)
-       case NodeAtLeastOnce:
-               return b.tr.newAtLeastOnce(b.Pos, b.Line, b.Pipe, b.List, 
b.ElseList)
+       case NodeContains:
+               return b.tr.newContains(b.Pos, b.Line, b.Pipe, b.List, 
b.ElseList)
        default:
                panic("unknown branch type")
        }
@@ -905,17 +905,17 @@ func (w *WithNode) Copy() Node {
        return w.tr.newWith(w.Pos, w.Line, w.Pipe.CopyPipe(), 
w.List.CopyList(), w.ElseList.CopyList())
 }
 
-// AtLeastOnce represents a {{atLeastOnce}} action and its commands.
-type AtLeastOnceNode struct {
+// Contains represents a {{contains}} action and its commands.
+type ContainsNode struct {
        BranchNode
 }
 
-func (t *Tree) newAtLeastOnce(pos Pos, line int, pipe *PipeNode, list 
*ListNode, elseList *ListNode) *AtLeastOnceNode {
-       return &AtLeastOnceNode{BranchNode{tr: t, NodeType: NodeAtLeastOnce, 
Pos: pos, Line: line, Pipe: pipe, List: list}}
+func (t *Tree) newContains(pos Pos, line int, pipe *PipeNode, list *ListNode, 
elseList *ListNode) *ContainsNode {
+       return &ContainsNode{BranchNode{tr: t, NodeType: NodeContains, Pos: 
pos, Line: line, Pipe: pipe, List: list}}
 }
 
-func (w *AtLeastOnceNode) Copy() Node {
-       return w.tr.newAtLeastOnce(w.Pos, w.Line, w.Pipe.CopyPipe(), 
w.List.CopyList(), w.ElseList.CopyList())
+func (w *ContainsNode) Copy() Node {
+       return w.tr.newContains(w.Pos, w.Line, w.Pipe.CopyPipe(), 
w.List.CopyList(), w.ElseList.CopyList())
 }
 
 // TemplateNode represents a {{template}} action.
diff --git a/third-party/go/template/parse/parse.go 
b/third-party/go/template/parse/parse.go
index 6698aaf..ce46c8b 100644
--- a/third-party/go/template/parse/parse.go
+++ b/third-party/go/template/parse/parse.go
@@ -365,8 +365,8 @@ func (t *Tree) action() (n Node) {
                return t.templateControl()
        case itemWith:
                return t.withControl()
-       case itemAtLeastOnce:
-               return t.atLeastOnceControl()
+       case itemContains:
+               return t.containsControl()
        }
        t.backup()
        token := t.peek()
@@ -506,11 +506,11 @@ func (t *Tree) withControl() Node {
        return t.newWith(t.parseControl(false, "with"))
 }
 
-// AtLeastOnce:
-//     {{atLeastOnce number}} itemList {{end}}
+// Contains:
+//     {{contains number}} itemList {{end}}
 // If keyword is past.
-func (t *Tree) atLeastOnceControl() Node {
-       return t.newAtLeastOnce(t.parseControl(false, "atLeastOnce"))
+func (t *Tree) containsControl() Node {
+       return t.newContains(t.parseControl(false, "contains"))
 }
 
 // End:

Reply via email to