Skip to content

Commit

Permalink
Expression command for testing (#81)
Browse files Browse the repository at this point in the history
Add ability to test and compile expressions without running it on a file
  • Loading branch information
zix99 committed Sep 15, 2022
1 parent f7b4e4e commit 7f4de82
Show file tree
Hide file tree
Showing 10 changed files with 308 additions and 3 deletions.
1 change: 1 addition & 0 deletions cmd/commands.go
Expand Up @@ -10,6 +10,7 @@ var commands []*cli.Command = []*cli.Command{
analyzeCommand(),
tabulateCommand(),
docsCommand(),
expressionCommand(),
}

func GetSupportedCommands() []*cli.Command {
Expand Down
125 changes: 125 additions & 0 deletions cmd/expressions.go
@@ -0,0 +1,125 @@
package cmd

import (
"errors"
"fmt"
"rare/pkg/expressions"
"rare/pkg/expressions/exprofiler"
"rare/pkg/expressions/stdlib"
"rare/pkg/humanize"
"strings"
"time"

"github.com/urfave/cli/v2"
)

func expressionFunction(c *cli.Context) error {
var (
expString = c.Args().First()
noOptimize = c.Bool("no-optimize")
data = c.StringSlice("data")
keys = c.StringSlice("key")
benchmark = c.Bool("benchmark")
stats = c.Bool("stats")
)

if c.NArg() != 1 {
return errors.New("expected exactly 1 expression argument")
}

if expString == "" {
return errors.New("empty expression")
}

builder := stdlib.NewStdKeyBuilderEx(!noOptimize)
compiled, err := builder.Compile(expString)
expCtx := expressions.KeyBuilderContextArray{
Elements: data,
Keys: parseKeyValuesIntoMap(keys...),
}

if err != nil {
return err
}

fmt.Printf("Expression: %s\n", expString)
if len(data) > 0 {
result := compiled.BuildKey(&expCtx)
fmt.Printf("Result: %s\n", result)
}

if stats {
stats := exprofiler.GetMetrics(compiled, &expCtx)

fmt.Println()
fmt.Println("Stats")
fmt.Printf(" Stages: %d\n", compiled.StageCount())
fmt.Printf(" Match Lookups: %d\n", stats.MatchLookups)
fmt.Printf(" Key Lookups: %d\n", stats.KeyLookups)
}

if benchmark {
fmt.Println()
duration, iterations := exprofiler.Benchmark(compiled, &expCtx)
perf := (duration / time.Duration(iterations)).String()
fmt.Printf("Benchmark: %s (%s iterations in %s)\n", perf, humanize.Hi(iterations), duration.String())
}

return nil
}

// Parse multiple kv's into a map
func parseKeyValuesIntoMap(kvs ...string) map[string]string {
ret := make(map[string]string)
for _, item := range kvs {
k, v := parseKeyValue(item)
ret[k] = v
}
return ret
}

// parse keys and values separated by '='
func parseKeyValue(s string) (string, string) {
idx := strings.IndexByte(s, '=')
if idx < 0 {
return s, s
}
return s[:idx], s[idx+1:]
}

func expressionCommand() *cli.Command {
return &cli.Command{
Name: "expression",
Usage: "Test and benchmark expressions",
Description: "Given an expression, and optionally some data, test the output and performance of an expression",
ArgsUsage: "<expression>",
Aliases: []string{"exp"},
Action: expressionFunction,
Flags: []cli.Flag{
&cli.BoolFlag{
Name: "benchmark",
Aliases: []string{"b"},
Usage: "Benchmark the expression (slow)",
},
&cli.BoolFlag{
Name: "stats",
Aliases: []string{"s"},
Usage: "Display stats about the expression",
},
&cli.StringSliceFlag{
Name: "data",
Aliases: []string{"d"},
Usage: "Specify positional data in the expression",
},
&cli.StringSliceFlag{
Name: "key",
Aliases: []string{"k"},
Usage: "Specify a named argument, a=b",
},
&cli.BoolFlag{
Name: "no-optimize",
Usage: "Disable expression static analysis optimization",
},
},
}
}
49 changes: 49 additions & 0 deletions cmd/expressions_test.go
@@ -0,0 +1,49 @@
package cmd

import (
"testing"

"github.com/stretchr/testify/assert"
)

func TestExpressionCmd(t *testing.T) {
testCommandSet(t, expressionCommand(),
`--help`,
`"a b c"`,
`-b -s -d test --key a=b "abc {0} {a}"`,
)
}

func TestExpressionResults(t *testing.T) {
o, e, err := testCommandCapture(expressionCommand(), `-s -d bob "abc {0}"`)
assert.NoError(t, err)
assert.Empty(t, e)

assert.Equal(t,
`Expression: abc {0}
Result: abc bob
Stats
Stages: 2
Match Lookups: 1
Key Lookups: 0
`, o)
}

func TestKeyParser(t *testing.T) {
k, v := parseKeyValue("")
assert.Empty(t, k)
assert.Empty(t, v)

k, v = parseKeyValue("a")
assert.Equal(t, "a", k)
assert.Equal(t, "a", v)

k, v = parseKeyValue("a=b")
assert.Equal(t, "a", k)
assert.Equal(t, "b", v)

k, v = parseKeyValue("a=b=c")
assert.Equal(t, "a", k)
assert.Equal(t, "b=c", v)
}
12 changes: 11 additions & 1 deletion docs/usage/expressions.md
Expand Up @@ -22,7 +22,7 @@ The basic syntax structure is as follows:
* Quotes in an argument create a single argument eg. `{coalesce {4} {3} "not found"}`
* Truthiness is the presence of a value. False is an empty value (or only whitespace)

## Special Keys
### Special Keys

The following are special Keys:

Expand All @@ -32,6 +32,16 @@ The following are special Keys:
* `{#}` Returns all matched numbered values as JSON
* `{.#}` Returned numbered and named matches as JSON

### Testing

You can test and benchmark expressions with the `rare expression` command. For example

```sh
$ rare expression -d 15 -d 20 -k key=30 "The sum is {sumi {0} {1} {key}}"
Expression: The sum is {sumi {0} {1} {key}}
Result: The sum is 65
```

## Examples

### Parsing an nginx access.log file
Expand Down
4 changes: 2 additions & 2 deletions pkg/expressions/contextArray.go
Expand Up @@ -3,6 +3,7 @@ package expressions
// KeyBuilderContextArray is a simple implementation of context with an array of elements
type KeyBuilderContextArray struct {
Elements []string
Keys map[string]string
}

// GetMatch implements `KeyBuilderContext`
Expand All @@ -15,6 +16,5 @@ func (s *KeyBuilderContextArray) GetMatch(idx int) string {

// GetKey gets a key for a given element
func (s *KeyBuilderContextArray) GetKey(key string) string {
// Unimplemented
return ""
return s.Keys[key]
}
21 changes: 21 additions & 0 deletions pkg/expressions/contextArray_test.go
@@ -0,0 +1,21 @@
package expressions

import (
"testing"

"github.com/stretchr/testify/assert"
)

func TestContextArray(t *testing.T) {
ctx := KeyBuilderContextArray{
Elements: []string{"a", "b"},
Keys: map[string]string{
"a": "b",
},
}
assert.Equal(t, "a", ctx.GetMatch(0))
assert.Equal(t, "b", ctx.GetMatch(1))
assert.Equal(t, "", ctx.GetMatch(2))
assert.Equal(t, "", ctx.GetKey("bla"))
assert.Equal(t, "b", ctx.GetKey("a"))
}
26 changes: 26 additions & 0 deletions pkg/expressions/exprofiler/benchmark.go
@@ -0,0 +1,26 @@
package exprofiler

import (
"rare/pkg/expressions"
"time"
)

func Benchmark(kb *expressions.CompiledKeyBuilder, ctx expressions.KeyBuilderContext) (duration time.Duration, iterations int) {
const minTime = 500 * time.Millisecond
iterations = 100_000

for {
start := time.Now()
for i := 0; i < iterations; i++ {
kb.BuildKey(ctx)
}
stop := time.Now()

duration = stop.Sub(start)
if duration >= minTime {
return duration, iterations
}

iterations *= 4
}
}
20 changes: 20 additions & 0 deletions pkg/expressions/exprofiler/benchmark_test.go
@@ -0,0 +1,20 @@
package exprofiler

import (
"rare/pkg/expressions"
"rare/pkg/expressions/stdlib"
"testing"

"github.com/stretchr/testify/assert"
)

func TestBenchmarking(t *testing.T) {
kb := stdlib.NewStdKeyBuilder()
ckb, _ := kb.Compile("hello {0}")
ctx := expressions.KeyBuilderContextArray{}

duration, iterations := Benchmark(ckb, &ctx)

assert.NotZero(t, duration.Milliseconds())
assert.NotZero(t, iterations)
}
34 changes: 34 additions & 0 deletions pkg/expressions/exprofiler/stats.go
@@ -0,0 +1,34 @@
package exprofiler

import "rare/pkg/expressions"

type ExpressionMetrics struct {
MatchLookups, KeyLookups int
}

type trackingExpressionContext struct {
Nested expressions.KeyBuilderContext
MatchLookups int
KeyLookups int
}

var _ expressions.KeyBuilderContext = &trackingExpressionContext{}

func (s *trackingExpressionContext) GetMatch(idx int) string {
s.MatchLookups++
return s.Nested.GetMatch(idx)
}

func (s *trackingExpressionContext) GetKey(key string) string {
s.KeyLookups++
return s.Nested.GetKey(key)
}

func GetMetrics(kb *expressions.CompiledKeyBuilder, ctx expressions.KeyBuilderContext) ExpressionMetrics {
trackingContext := trackingExpressionContext{ctx, 0, 0}
kb.BuildKey(&trackingContext)
return ExpressionMetrics{
MatchLookups: trackingContext.MatchLookups,
KeyLookups: trackingContext.KeyLookups,
}
}
19 changes: 19 additions & 0 deletions pkg/expressions/exprofiler/stats_test.go
@@ -0,0 +1,19 @@
package exprofiler

import (
"rare/pkg/expressions"
"rare/pkg/expressions/stdlib"
"testing"

"github.com/stretchr/testify/assert"
)

func TestExpressionStats(t *testing.T) {
kb := stdlib.NewStdKeyBuilder()
ckb, _ := kb.Compile("this is {0} a {test}")
ctx := &expressions.KeyBuilderContextArray{}
stats := GetMetrics(ckb, ctx)

assert.Equal(t, 1, stats.MatchLookups)
assert.Equal(t, 1, stats.MatchLookups)
}

0 comments on commit 7f4de82

Please sign in to comment.