render/dsl: port of experimental/dsl

This commit is contained in:
Peter Bourgon
2015-08-28 18:04:04 +02:00
parent 9e7e4cc040
commit fbc5ef2c8e
6 changed files with 1036 additions and 0 deletions

398
render/dsl/expression.go Normal file
View File

@@ -0,0 +1,398 @@
package dsl
import (
"fmt"
"log"
"math/rand"
"net"
"regexp"
"strings"
"github.com/weaveworks/scope/probe/endpoint"
"github.com/weaveworks/scope/probe/host"
"github.com/weaveworks/scope/render"
"github.com/weaveworks/scope/report"
)
// Evaluator describes a monadic transformer of a topology. It is expected
// that the topology contains heterogeneous nodes.
type Evaluator interface {
Eval(report.Topology) report.Topology
}
// Expression is a single evaluator.
type Expression struct {
selector
transformer
}
// Eval implements Evaluator.
func (e Expression) Eval(tpy report.Topology) report.Topology {
return e.transformer(tpy, e.selector(tpy))
}
// Expressions is an ordered collection of expressions.
type Expressions []Expression
// Eval implements Evaluator.
func (e Expressions) Eval(tpy report.Topology) report.Topology {
for _, expr := range e {
tpy = expr.Eval(tpy)
}
return tpy
}
type selector func(report.Topology) []string
type transformer func(report.Topology, []string) report.Topology
func selectAll(tpy report.Topology) []string {
out := make([]string, 0, len(tpy.NodeMetadatas))
for id := range tpy.NodeMetadatas {
out = append(out, id)
}
log.Printf("select ALL: %d", len(out))
return out
}
func selectConnected(tpy report.Topology) []string {
degree := map[string]int{}
for src, dsts := range tpy.Adjacency {
a, ok := report.ParseAdjacencyID(src)
if !ok {
panic(src)
}
degree[a] += len(dsts)
for _, dst := range dsts {
degree[dst]++
}
}
out := []string{}
for id := range tpy.NodeMetadatas {
if degree[id] > 0 {
out = append(out, id)
}
}
log.Printf("select CONNECTED: %d", len(out))
return out
}
func selectNonlocal(tpy report.Topology) []string {
local := report.Networks{}
for _, md := range tpy.NodeMetadatas {
for k, v := range md.Metadata {
if k == host.LocalNetworks {
local = append(local, render.ParseNetworks(v)...)
}
}
}
out := []string{}
for id, md := range tpy.NodeMetadatas {
if addr, ok := md.Metadata[endpoint.Addr]; ok {
if ip := net.ParseIP(addr); ip != nil && !local.Contains(ip) {
out = append(out, id) // valid addr metadata key, nonlocal
continue
}
}
if _, addr, ok := report.ParseAddressNodeID(id); ok {
if ip := net.ParseIP(addr); ip != nil && !local.Contains(ip) {
out = append(out, id) // valid address node ID, nonlocal
continue
}
}
if _, addr, _, ok := report.ParseEndpointNodeID(id); ok {
if ip := net.ParseIP(addr); ip != nil && !local.Contains(ip) {
out = append(out, id) // valid endpoint node ID, nonlocal
continue
}
}
}
log.Printf("select NONLOCAL: %d", len(out))
return out
}
func selectLike(s string) selector {
re, err := regexp.Compile(s)
if err != nil {
log.Printf("select LIKE %q: %v", s, err)
re = regexp.MustCompile("")
}
return func(tpy report.Topology) []string {
out := []string{}
for id := range tpy.NodeMetadatas {
if re.MatchString(id) {
out = append(out, id)
}
}
log.Printf("select LIKE %q: %d", s, len(out))
return out
}
}
func selectWith(s string) selector {
var k, v string
if fields := strings.SplitN(s, "=", 2); len(fields) == 1 {
k = strings.TrimSpace(fields[0])
} else if len(fields) == 2 {
k, v = strings.TrimSpace(fields[0]), strings.TrimSpace(fields[1])
}
return func(tpy report.Topology) []string {
out := []string{}
for id, md := range tpy.NodeMetadatas {
if vv, ok := md.Metadata[k]; ok {
if v == "" || (v != "" && v == vv) {
out = append(out, id)
}
}
}
log.Printf("select WITH %q: %d", s, len(out))
return out
}
}
func selectNot(s selector) selector {
return func(tpy report.Topology) []string {
set := map[string]struct{}{}
for _, id := range s(tpy) {
set[id] = struct{}{}
}
out := []string{}
for id := range tpy.NodeMetadatas {
if _, ok := set[id]; ok {
continue // selected by that one -> not by this one
}
out = append(out, id)
}
log.Printf("select NOT: %d", len(out))
return out
}
}
const highlightKey = "_highlight"
func transformHighlight(tpy report.Topology, ids []string) report.Topology {
for _, id := range ids {
tpy.NodeMetadatas[id] = tpy.NodeMetadatas[id].Merge(report.MakeNodeMetadataWith(map[string]string{highlightKey: "true"}))
}
log.Printf("transform HIGHLIGHT %d: OK", len(ids))
return tpy
}
func transformRemove(tpy report.Topology, ids []string) report.Topology {
toRemove := map[string]struct{}{}
for _, id := range ids {
toRemove[id] = struct{}{}
}
out := report.MakeTopology()
for id := range tpy.NodeMetadatas {
if _, ok := toRemove[id]; ok {
continue
}
cp(out, tpy, id)
}
clean(out, toRemove)
log.Printf("transform REMOVE %d: in %d, out %d", len(ids), len(tpy.NodeMetadatas), len(out.NodeMetadatas))
return out
}
func transformShowOnly(tpy report.Topology, ids []string) report.Topology {
out := report.MakeTopology()
for _, id := range ids {
if _, ok := tpy.NodeMetadatas[id]; !ok {
continue
}
cp(out, tpy, id)
}
log.Printf("transform SHOWONLY %d: in %d, out %d", len(ids), len(tpy.NodeMetadatas), len(out.NodeMetadatas))
return out
}
func transformMerge(tpy report.Topology, ids []string) report.Topology {
name := fmt.Sprintf("%x", rand.Int31())
mapped := map[string]string{}
for _, id := range ids {
mapped[id] = name
}
out := report.MakeTopology()
for id := range tpy.NodeMetadatas {
if dstID, ok := mapped[id]; ok {
merge(out, dstID, tpy, id, mapped)
} else {
cp(out, tpy, id)
}
}
log.Printf("transform MERGE %d: in %d, out %d", len(ids), len(tpy.NodeMetadatas), len(out.NodeMetadatas))
return out
}
func transformGroupBy(s string) transformer {
keys := []string{}
for _, key := range strings.Split(s, ",") {
keys = append(keys, strings.TrimSpace(key))
}
return func(tpy report.Topology, ids []string) report.Topology {
set := map[string]struct{}{}
for _, id := range ids {
set[id] = struct{}{}
}
// Identify all nodes that should be grouped.
mapped := map[string]string{} // src ID: dst ID
for id, md := range tpy.NodeMetadatas {
if _, ok := set[id]; !ok {
continue // not selected
}
parts := []string{}
for _, key := range keys {
if val, ok := md.Metadata[key]; ok {
parts = append(parts, fmt.Sprintf("%s-%s", key, val))
}
}
if len(parts) < len(keys) {
continue // didn't match all required keys
}
dstID := strings.Join(parts, "-")
mapped[id] = dstID
}
// Walk nodes again, merging those that should be grouped.
out := report.MakeTopology()
for id := range tpy.NodeMetadatas {
if dstID, ok := mapped[id]; ok {
merge(out, dstID, tpy, id, mapped)
} else {
cp(out, tpy, id)
}
}
log.Printf("transform GROUPBY %v %d: in %d, out %d", keys, len(ids), len(tpy.NodeMetadatas), len(out.NodeMetadatas))
return out
}
}
func transformJoin(key string) transformer {
return func(tpy report.Topology, ids []string) report.Topology {
// key is e.g. host_node_id.
// Collect the set of represented values.
values := map[string]report.NodeMetadata{}
for _, md := range tpy.NodeMetadatas {
for k, v := range md.Metadata {
if k == key {
values[v] = report.MakeNodeMetadata() // gather later
}
}
}
// Next, gather the metadata from nodes in the set.
for id, md := range tpy.NodeMetadatas {
if found, ok := values[id]; ok {
values[id] = found.Merge(md) // gather
}
}
// Finally, join that metadata to referential nodes.
// And delete the referenced nodes.
out := report.MakeTopology()
for id, md := range tpy.NodeMetadatas {
if _, ok := values[id]; ok {
continue // delete
}
cp(out, tpy, id) // copy node
for k, v := range md.Metadata {
if k == key {
md = md.Merge(values[v]) // join metadata
}
}
out.NodeMetadatas[id] = md // write
}
log.Printf("transform JOIN %v %d: in %d, out %d", key, len(ids), len(tpy.NodeMetadatas), len(out.NodeMetadatas))
return out
}
}
func cp(dst report.Topology, src report.Topology, id string) {
adjacencyID := report.MakeAdjacencyID(id)
dst.Adjacency[adjacencyID] = src.Adjacency[adjacencyID]
for _, otherID := range dst.Adjacency[id] {
edgeID := report.MakeEdgeID(id, otherID)
dst.EdgeMetadatas[edgeID] = src.EdgeMetadatas[edgeID]
}
dst.NodeMetadatas[id] = src.NodeMetadatas[id]
}
func clean(dst report.Topology, toRemove map[string]struct{}) {
for srcAdjacencyID, dstIDs := range dst.Adjacency {
newIDs := report.IDList{}
for _, dstID := range dstIDs {
if _, ok := toRemove[dstID]; ok {
continue // can't be a dst anymore
}
newIDs = append(newIDs, dstID)
}
if len(newIDs) <= 0 {
delete(dst.Adjacency, srcAdjacencyID) // all dsts are gone, so rm src
continue
}
dst.Adjacency[srcAdjacencyID] = newIDs // overwrite
}
for id := range toRemove {
delete(dst.NodeMetadatas, id)
}
for edgeID := range dst.EdgeMetadatas {
srcNodeID, dstNodeID, ok := report.ParseEdgeID(edgeID)
if !ok {
continue // panic
}
if _, ok := toRemove[srcNodeID]; ok {
delete(dst.EdgeMetadatas, edgeID)
}
if _, ok := toRemove[dstNodeID]; ok {
delete(dst.EdgeMetadatas, edgeID)
}
}
}
func merge(dst report.Topology, dstID string, src report.Topology, srcID string, mapped map[string]string) {
// We gonna take srcID from src and merge it into dstID in dst. That's
// like renaming the node, so we gotta update the adjacency lists. Both
// outgoing *and* incoming links!
dstAdjacencyID := report.MakeAdjacencyID(dstID)
srcAdjacencyID := report.MakeAdjacencyID(srcID)
// Merge the src's adjacency list into the dst topology.
dst.Adjacency[dstAdjacencyID] = dst.Adjacency[dstAdjacencyID].Merge(src.Adjacency[srcAdjacencyID])
// Update any dst adjacencies from the old ID to the new ID.
for existingSrcAdjacencyID, existingDstIDs := range dst.Adjacency {
for i, existingDstID := range existingDstIDs {
if newDstID, ok := mapped[existingDstID]; ok {
existingDstIDs[i] = newDstID
}
}
dst.Adjacency[existingSrcAdjacencyID] = existingDstIDs
}
// Update the EdgeMetadatas to have the new IDs.
for _, otherID := range src.Adjacency[srcAdjacencyID] {
oldEdgeID := report.MakeEdgeID(srcID, otherID)
newEdgeID := report.MakeEdgeID(dstID, otherID)
dst.EdgeMetadatas[newEdgeID] = dst.EdgeMetadatas[newEdgeID].Merge(src.EdgeMetadatas[oldEdgeID])
}
// Merge the src node metadata into the dst node metadata.
md, ok := dst.NodeMetadatas[dstID]
if !ok {
md = report.MakeNodeMetadata()
}
md = md.Merge(src.NodeMetadatas[srcID])
dst.NodeMetadatas[dstID] = md
}

View File

@@ -0,0 +1,64 @@
package dsl_test
import (
"reflect"
"testing"
"github.com/weaveworks/scope/render/dsl"
"github.com/weaveworks/scope/report"
"github.com/weaveworks/scope/test"
)
var fixture = report.Topology{
Adjacency: map[string]report.IDList{
report.MakeAdjacencyID("a"): report.MakeIDList("b", "c"), // a -> b, a-> c
report.MakeAdjacencyID("b"): report.MakeIDList("c"), // b -> c
report.MakeAdjacencyID("c"): report.MakeIDList("c"), // c -> c
},
NodeMetadatas: map[string]report.NodeMetadata{
"a": report.MakeNodeMetadataWith(map[string]string{"is-a-or-b-or-c": "true", "is-a-or-b": "true", "is-a": "true"}),
"b": report.MakeNodeMetadataWith(map[string]string{"is-a-or-b-or-c": "true", "is-a-or-b": "true"}),
"c": report.MakeNodeMetadataWith(map[string]string{"is-a-or-b-or-c": "true"}),
},
EdgeMetadatas: map[string]report.EdgeMetadata{
report.MakeEdgeID("a", "c"): report.EdgeMetadata{EgressPacketCount: newu64(1)},
report.MakeEdgeID("b", "c"): report.EdgeMetadata{EgressPacketCount: newu64(2)},
},
}
func TestSingleExpressions(t *testing.T) {
for _, testcase := range []struct {
input string
want report.Topology
}{
{
"ALL REMOVE",
report.MakeTopology(),
},
{
"NOT WITH {{is-a-or-b}} REMOVE",
report.Topology{
Adjacency: map[string]report.IDList{
report.MakeAdjacencyID("a"): report.MakeIDList("b"),
},
NodeMetadatas: map[string]report.NodeMetadata{
"a": report.MakeNodeMetadataWith(map[string]string{"is-a-or-b-or-c": "true", "is-a-or-b": "true", "is-a": "true"}),
"b": report.MakeNodeMetadataWith(map[string]string{"is-a-or-b-or-c": "true", "is-a-or-b": "true"}),
},
EdgeMetadatas: map[string]report.EdgeMetadata{},
},
},
} {
expr, err := dsl.ParseExpression(testcase.input)
if err != nil {
t.Errorf("%q: %v", testcase.input, err)
continue
}
if want, have := testcase.want, expr.Eval(fixture); !reflect.DeepEqual(want, have) {
t.Errorf("%s: %s", testcase.input, test.Diff(want, have))
continue
}
}
}
func newu64(value uint64) *uint64 { return &value }

326
render/dsl/lexer.go Normal file
View File

@@ -0,0 +1,326 @@
package dsl
import (
"fmt"
"strings"
"unicode/utf8"
)
// Expression = [NOT] Selector [Transformer]
// Selector = ALL / CONNECTED / NONLOCAL / LIKE {{ <regex> }} / WITH {{ <key> [= <value>] }}
// Transformer = HIGHLIGHT / REMOVE / SHOWONLY / MERGE / GROUPBY {{ <key>, ... }} // JOIN {{ key }}
type lexer struct {
input string // string being scanned
start int // start position of this item
pos int // current position within the input
width int // width of last rune read
items chan item
}
func lex(input string) (*lexer, <-chan item) {
l := &lexer{
input: input,
items: make(chan item),
}
go l.run()
return l, l.items
}
const (
keywordNot = "NOT"
keywordAll = "ALL"
keywordConnected = "CONNECTED"
keywordNonlocal = "NONLOCAL"
keywordLike = "LIKE"
keywordWith = "WITH"
keywordHighlight = "HIGHLIGHT"
keywordRemove = "REMOVE"
keywordShowOnly = "SHOWONLY"
keywordMerge = "MERGE"
keywordGroupBy = "GROUPBY"
keywordJoin = "JOIN"
)
type itemType int
const (
itemError itemType = iota
itemNot
itemAll
itemConnected
itemNonlocal
itemLike
itemWith
itemHighlight
itemRemove
itemShowOnly
itemMerge
itemGroupBy
itemJoin
itemRegex
itemKeyValue
itemKeyList
itemKey
)
func (t itemType) String() string {
switch t {
case itemError:
return "ERROR"
case itemNot:
return keywordNot
case itemAll:
return keywordAll
case itemConnected:
return keywordConnected
case itemNonlocal:
return keywordNonlocal
case itemLike:
return keywordLike
case itemWith:
return keywordWith
case itemHighlight:
return keywordHighlight
case itemRemove:
return keywordRemove
case itemShowOnly:
return keywordShowOnly
case itemMerge:
return keywordMerge
case itemGroupBy:
return keywordGroupBy
case itemJoin:
return keywordJoin
case itemRegex:
return "<regex>"
case itemKeyValue:
return "<key=value>"
case itemKeyList:
return "<key list>"
case itemKey:
return "<key>"
default:
return "unknown"
}
}
type stateFn func(*lexer) stateFn
func (l *lexer) run() {
for state := lexExpression; state != nil; {
state = state(l)
}
close(l.items)
}
func (l *lexer) emit(t itemType) {
l.items <- item{t, l.input[l.start:l.pos]}
l.start = l.pos
}
const eof rune = -1
func (l *lexer) next() (r rune) {
if l.pos >= len(l.input) {
l.width = 0
return eof
}
r, l.width = utf8.DecodeRuneInString(l.input[l.pos:])
l.pos += l.width
return r
}
func (l *lexer) backup() { l.pos -= l.width }
// acceptRun consumes a run of runes from the valid set.
func (l *lexer) acceptRun(validSet string) {
for strings.IndexRune(validSet, l.next()) >= 0 {
// consume
}
l.backup()
}
func (l *lexer) eatWhitespace() {
l.acceptRun(" \t\r\n")
}
// errorf terminates lexing with an error.
func (l *lexer) errorf(format string, args ...interface{}) stateFn {
l.items <- item{itemError, fmt.Sprintf(format, args...)}
return nil
}
type item struct {
itemType itemType
literal string
}
func (i item) String() string {
return fmt.Sprintf("%s %q", i.itemType, i.literal)
}
func lexExpression(l *lexer) stateFn {
l.eatWhitespace()
if strings.HasPrefix(l.input[l.pos:], keywordNot) {
return lexNot
}
return lexSelector
}
func lexNot(l *lexer) stateFn {
l.pos += len(keywordNot)
l.emit(itemNot)
return lexSelector
}
func lexSelector(l *lexer) stateFn {
l.eatWhitespace()
switch {
case strings.HasPrefix(l.input[l.pos:], keywordAll):
return lexAll
case strings.HasPrefix(l.input[l.pos:], keywordConnected):
return lexConnected
case strings.HasPrefix(l.input[l.pos:], keywordNonlocal):
return lexNonlocal
case strings.HasPrefix(l.input[l.pos:], keywordLike):
return lexLike
case strings.HasPrefix(l.input[l.pos:], keywordWith):
return lexWith
default:
return l.errorf("bad selector")
}
}
func lexAll(l *lexer) stateFn {
l.pos += len(keywordAll)
l.emit(itemAll)
return lexTransformer
}
func lexConnected(l *lexer) stateFn {
l.pos += len(keywordConnected)
l.emit(itemConnected)
return lexTransformer
}
func lexNonlocal(l *lexer) stateFn {
l.pos += len(keywordNonlocal)
l.emit(itemNonlocal)
return lexTransformer
}
func lexLike(l *lexer) stateFn {
l.pos += len(keywordLike)
l.emit(itemLike)
return lexRegex
}
func lexWith(l *lexer) stateFn {
l.pos += len(keywordWith)
l.emit(itemWith)
return lexKeyValue
}
func lexRegex(l *lexer) stateFn {
return lexMeta("regex", itemRegex, lexTransformer)
}
func lexKeyValue(l *lexer) stateFn {
return lexMeta("key=value", itemKeyValue, lexTransformer)
}
func lexTransformer(l *lexer) stateFn {
l.eatWhitespace()
switch {
case l.pos == len(l.input):
return nil // done
case strings.HasPrefix(l.input[l.pos:], keywordHighlight):
return lexHighlight
case strings.HasPrefix(l.input[l.pos:], keywordRemove):
return lexRemove
case strings.HasPrefix(l.input[l.pos:], keywordShowOnly):
return lexShowOnly
case strings.HasPrefix(l.input[l.pos:], keywordMerge):
return lexMerge
case strings.HasPrefix(l.input[l.pos:], keywordGroupBy):
return lexGroupBy
case strings.HasPrefix(l.input[l.pos:], keywordJoin):
return lexJoin
default:
return l.errorf("bad transformer at position %d: %s", l.pos, l.input[l.pos:])
}
}
func lexHighlight(l *lexer) stateFn {
l.pos += len(keywordHighlight)
l.emit(itemHighlight)
return nil
}
func lexRemove(l *lexer) stateFn {
l.pos += len(keywordRemove)
l.emit(itemRemove)
return nil
}
func lexShowOnly(l *lexer) stateFn {
l.pos += len(keywordShowOnly)
l.emit(itemShowOnly)
return nil
}
func lexMerge(l *lexer) stateFn {
l.pos += len(keywordMerge)
l.emit(itemMerge)
return nil
}
func lexGroupBy(l *lexer) stateFn {
l.pos += len(keywordGroupBy)
l.emit(itemGroupBy)
return lexKeyList
}
func lexJoin(l *lexer) stateFn {
l.pos += len(keywordJoin)
l.emit(itemJoin)
return lexKey
}
func lexKeyList(l *lexer) stateFn {
return lexMeta("key list", itemKeyList, nil)
}
func lexKey(l *lexer) stateFn {
return lexMeta("key", itemKey, nil)
}
const (
leftMeta = "{{"
rightMeta = "}}"
)
func lexMeta(what string, t itemType, next stateFn) stateFn {
return func(l *lexer) stateFn {
l.eatWhitespace()
if !strings.HasPrefix(l.input[l.pos:], leftMeta) {
return l.errorf("%s must begin with %s", what, leftMeta)
}
l.pos += len(leftMeta)
l.start = l.pos
for {
if l.pos > len(l.input) {
return l.errorf("%s must end with %s", what, rightMeta)
}
if strings.HasPrefix(l.input[l.pos:], rightMeta) {
break
}
l.pos++
}
l.emit(t)
l.pos += len(rightMeta)
l.start = l.pos
return next
}
}

View File

@@ -0,0 +1,76 @@
package dsl
import (
"errors"
"testing"
)
func TestLexer(t *testing.T) {
for _, test := range []struct {
input string
want []item
err error
}{
{
"",
[]item{},
errors.New("bad selector"),
},
{
"foo",
[]item{},
errors.New("bad selector"),
},
{
"ALL",
[]item{{itemAll, keywordAll}},
errors.New("bad transformer"),
},
{
"NOT ALL",
[]item{{itemNot, keywordNot}, {itemAll, keywordAll}},
errors.New("bad transformer"),
},
{
"ALL HIGHLIGHT",
[]item{{itemAll, keywordAll}, {itemHighlight, keywordHighlight}},
nil,
},
{
"WITH {{pid}} REMOVE",
[]item{{itemWith, keywordWith}, {itemKeyValue, "pid"}, {itemRemove, keywordRemove}},
nil,
},
} {
_, c := lex(test.input)
for item := range c {
if item.itemType == itemError {
if test.err == nil {
t.Errorf("%q: unexpected error: %v", test.input, item.literal)
break
}
if want, have := test.err.Error(), item.literal; want != have {
t.Errorf("%q: want error %q, have %q", test.input, want, have)
break
}
t.Logf("%q: got expected error %v", test.input, test.err)
break
}
if len(test.want) <= 0 {
t.Errorf("%q: got too many items", test.input)
break
}
want := test.want[0]
test.want = test.want[1:]
if want, have := want.itemType, item.itemType; want != have {
t.Errorf("%q: unexpected item: want %v, have %v", test.input, want, have)
break
}
t.Logf("%s: lex %s (%q) OK", test.input, item.itemType, item.literal)
}
}
}

104
render/dsl/parser.go Normal file
View File

@@ -0,0 +1,104 @@
package dsl
import (
"errors"
"fmt"
"log"
)
// ParseExpression parses a single expression string.
func ParseExpression(str string) (Expression, error) {
var (
expr Expression
not bool
)
_, c := lex(str)
for item := range c {
switch item.itemType {
case itemNot:
not = !not
case itemAll:
expr.selector = selectAll
case itemConnected:
expr.selector = selectConnected
case itemNonlocal:
expr.selector = selectNonlocal
case itemLike:
item = <-c
switch item.itemType {
case itemRegex:
expr.selector = selectLike(item.literal)
default:
return Expression{}, fmt.Errorf("bad LIKE: want %s, got %s", itemRegex, item.itemType)
}
case itemWith:
item = <-c
switch item.itemType {
case itemKeyValue:
expr.selector = selectWith(item.literal)
default:
return Expression{}, fmt.Errorf("bad WITH: want %s, got %s", itemKeyValue, item.itemType)
}
case itemHighlight:
expr.transformer = transformHighlight
case itemRemove:
expr.transformer = transformRemove
case itemShowOnly:
expr.transformer = transformShowOnly
case itemMerge:
expr.transformer = transformMerge
case itemGroupBy:
item = <-c
switch item.itemType {
case itemKeyList:
expr.transformer = transformGroupBy(item.literal)
default:
return Expression{}, fmt.Errorf("bad GROUPBY: want %s, got %s", itemKeyList, item.itemType)
}
case itemJoin:
item = <-c
switch item.itemType {
case itemKey:
expr.transformer = transformJoin(item.literal)
default:
return Expression{}, fmt.Errorf("bad JOIN: want %s, got %s", itemKey, item.itemType)
}
default:
return Expression{}, errors.New(item.literal)
}
}
if not {
expr.selector = selectNot(expr.selector)
}
if expr.transformer == nil {
expr.transformer = transformHighlight
}
return expr, nil
}
// ParseExpressions parses multiple expression strings.
func ParseExpressions(strs ...string) Expressions {
var exprs Expressions
for _, str := range strs {
expr, err := ParseExpression(str)
if err != nil {
log.Printf("%s: %v", str, err)
continue
}
log.Printf("%s: OK", str)
exprs = append(exprs, expr)
}
return exprs
}

View File

@@ -0,0 +1,68 @@
package dsl
import (
"errors"
"reflect"
"runtime"
"strings"
"testing"
)
func TestParseExpression(t *testing.T) {
for _, test := range []struct {
input string
want Expression
err error
}{
{
"",
Expression{},
errors.New("bad selector"),
},
{
"ALL",
Expression{selectAll, transformHighlight},
nil,
},
{
"HIGHLIGHT",
Expression{},
errors.New("bad selector"),
},
{
"CONNECTED REMOVE",
Expression{selectConnected, transformRemove},
nil,
},
{
"NOT CONNECTED MERGE",
Expression{selectNot(selectConnected), transformMerge},
nil,
},
} {
have, err := ParseExpression(test.input)
if err == nil && test.err != nil {
t.Errorf("%q: want error %q, have no error", test.input, test.err.Error())
continue
} else if err != nil && test.err == nil {
t.Errorf("%q: want no error, have error %q", test.input, err.Error())
continue
} else if err != nil && test.err != nil && test.err.Error() != err.Error() {
t.Errorf("%q: want error %q, have %q", test.input, test.err.Error(), err.Error())
continue
}
if want, have := nameof(test.want.selector), nameof(have.selector); want != have {
t.Errorf("%q: selector: want %v, have %v", test.input, want, have)
}
if want, have := nameof(test.want.transformer), nameof(have.transformer); want != have {
t.Errorf("%q: transformer: want %v, have %v", test.input, want, have)
}
}
}
func nameof(i interface{}) string {
full := runtime.FuncForPC(reflect.ValueOf(i).Pointer()).Name() // github.com/weaveworks/scope/render/dsl.selectAll
fields := strings.Split(full, ".") // [github com/weaveworks/scope/render/dsl selectAll]
last := fields[len(fields)-1] // selectAll
return last
}