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 <kezhenxu94@apache.org>
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 @@
}
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 @@
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 @@
}
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) 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 @@
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 @@
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 @@
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 @@
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 @@
}
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 @@
name = "range"
case NodeWith:
name = "with"
- case NodeAtLeastOnce:
- name = "atLeastOnce"
+ case NodeContains:
+ name = "contains"
default:
panic("unknown branch type")
}
@@ -859,8 +859,8 @@
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 @@
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 @@
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 @@
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: