Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Expression command for testing (#81)
Add ability to test and compile expressions without running it on a file
- Loading branch information
Showing
10 changed files
with
308 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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", | ||
}, | ||
}, | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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")) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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, | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) | ||
} |