From 07514214e14b5f05faf2e87a640df3b0f80b24d6 Mon Sep 17 00:00:00 2001 From: James Phillips Date: Fri, 26 Feb 2016 20:03:15 -0800 Subject: [PATCH] Adds tests for the low-level template functions. --- consul/prepared_query/template.go | 18 +- consul/prepared_query/template_test.go | 258 +++++++++++++++++++++++-- 2 files changed, 251 insertions(+), 25 deletions(-) diff --git a/consul/prepared_query/template.go b/consul/prepared_query/template.go index 0854576a18..70986fc2dc 100644 --- a/consul/prepared_query/template.go +++ b/consul/prepared_query/template.go @@ -60,7 +60,7 @@ func Compile(query *structs.PreparedQuery) (*CompiledTemplate, error) { parse := func(path string, v reflect.Value) error { tree, err := hil.Parse(v.String()) if err != nil { - return fmt.Errorf("Bad Service%s field with contents '%s': %s", path, v.String(), err) + return fmt.Errorf("Bad format '%s' in Service%s: %s", v.String(), path, err) } ct.trees[path] = tree @@ -79,6 +79,15 @@ func Compile(query *structs.PreparedQuery) (*CompiledTemplate, error) { } } + // Finally do a test render with the supplied name prefix. This will + // help catch errors before run time, and this is the most minimal + // prefix it will be expected to run with. The results might not make + // sense and create a valid service to lookup, but it should render + // without any errors. + if _, err = ct.Render(ct.query.Name); err != nil { + return nil, err + } + return ct, nil } @@ -107,7 +116,8 @@ func (ct *CompiledTemplate) Render(name string) (*structs.PreparedQuery, error) // from multiple goroutines. var matches []string if ct.re != nil { - matches = ct.re.Copy().FindStringSubmatch(name) + re := ct.re.Copy() + matches = re.FindStringSubmatch(name) } // Create a safe match function that can't fail at run time. It will @@ -159,10 +169,10 @@ func (ct *CompiledTemplate) Render(name string) (*structs.PreparedQuery, error) hv, ht, err := hil.Eval(tree, config) if err != nil { - return err + return fmt.Errorf("Bad evaluation for '%s' in Service%s: %s", v.String(), path, err) } if ht != ast.TypeString { - return fmt.Errorf("Expected Service%s filed to be a string, got %s", path, ht) + return fmt.Errorf("Expected Service%s field to be a string, got %s", path, ht) } v.SetString(hv.(string)) diff --git a/consul/prepared_query/template_test.go b/consul/prepared_query/template_test.go index 7060f7c736..aa3bc8d10d 100644 --- a/consul/prepared_query/template_test.go +++ b/consul/prepared_query/template_test.go @@ -1,14 +1,18 @@ package prepared_query import ( - "fmt" + "reflect" + "strings" "testing" "github.com/hashicorp/consul/consul/structs" + "github.com/mitchellh/copystructure" ) var ( - bench = &structs.PreparedQuery{ + // bigBench is a test query that uses all the features of templates, not + // in a realistic way, but in a complete way. + bigBench = &structs.PreparedQuery{ Name: "hello", Template: structs.QueryTemplateOptions{ Type: structs.QueryTemplateTypeNamePrefixMatch, @@ -16,22 +20,58 @@ var ( }, Service: structs.ServiceQuery{ Service: "${name.full}", - Tags: []string{"${name.prefix}", "${name.suffix}", "${match(0)}", "${match(1)}", "${match(2)}"}, + Failover: structs.QueryDatacenterOptions{ + Datacenters: []string{ + "${name.full}", + "${name.prefix}", + "${name.suffix}", + "${match(0)}", + "${match(1)}", + "${match(2)}", + }, + }, + Tags: []string{ + "${name.full}", + "${name.prefix}", + "${name.suffix}", + "${match(0)}", + "${match(1)}", + "${match(2)}", + }, + }, + } + + // smallBench is a small prepared query just for doing geo failover. This + // is a minimal, useful configuration. + smallBench = &structs.PreparedQuery{ + Name: "", + Template: structs.QueryTemplateOptions{ + Type: structs.QueryTemplateTypeNamePrefixMatch, + }, + Service: structs.ServiceQuery{ + Service: "${name.full}", + Failover: structs.QueryDatacenterOptions{ + Datacenters: []string{ + "dc1", + "dc2", + "dc3", + }, + }, }, } ) -func BenchmarkTemplate_Compile(b *testing.B) { +func compileBench(b *testing.B, query *structs.PreparedQuery) { for i := 0; i < b.N; i++ { - _, err := Compile(bench) + _, err := Compile(query) if err != nil { b.Fatalf("err: %v", err) } } } -func BenchmarkTemplate_Render(b *testing.B) { - compiled, err := Compile(bench) +func renderBench(b *testing.B, query *structs.PreparedQuery) { + compiled, err := Compile(query) if err != nil { b.Fatalf("err: %v", err) } @@ -44,29 +84,205 @@ func BenchmarkTemplate_Render(b *testing.B) { } } +func BenchmarkTemplate_CompileSmall(b *testing.B) { + compileBench(b, smallBench) +} + +func BenchmarkTemplate_CompileBig(b *testing.B) { + compileBench(b, bigBench) +} + +func BenchmarkTemplate_RenderSmall(b *testing.B) { + renderBench(b, smallBench) +} + +func BenchmarkTemplate_RenderBig(b *testing.B) { + renderBench(b, bigBench) +} + func TestTemplate_Compile(t *testing.T) { - query := &structs.PreparedQuery{ - Name: "hello", + // Start with an empty query that's not even a template. + query := &structs.PreparedQuery{} + _, err := Compile(query) + if err == nil || !strings.Contains(err.Error(), "Bad Template") { + t.Fatalf("bad: %v", err) + } + if IsTemplate(query) { + t.Fatalf("should not be a template") + } + + // Make it a basic template, keeping a copy before we compile. + query.Template.Type = structs.QueryTemplateTypeNamePrefixMatch + query.Template.Regexp = "^(hello)there$" + query.Service.Service = "${name.full}" + query.Service.Tags = []string{"${match(1)}"} + backup, err := copystructure.Copy(query) + if err != nil { + t.Fatalf("err: %v", err) + } + ct, err := Compile(query) + if err != nil { + t.Fatalf("err: %v", err) + } + if !IsTemplate(query) { + t.Fatalf("should be a template") + } + + // Do a sanity check render on it. + actual, err := ct.Render("hellothere") + if err != nil { + t.Fatalf("err: %v", err) + } + + // See if it rendered correctly. + expected := &structs.PreparedQuery{ Template: structs.QueryTemplateOptions{ Type: structs.QueryTemplateTypeNamePrefixMatch, - Regexp: "^hello-(.*)$", + Regexp: "^(hello)there$", }, Service: structs.ServiceQuery{ - Service: "${name.full}", - Tags: []string{"${name.prefix}", "${name.suffix}", "${match(0)}", "${match(1)}", "${match(2)}"}, + Service: "hellothere", + Tags: []string{ + "hello", + }, }, } - - compiled, err := Compile(query) - if err != nil { - t.Fatalf("err: %v", err) + if !reflect.DeepEqual(actual, expected) { + t.Fatalf("bad: %#v", actual) } - rendered, err := compiled.Render("hello-everyone") - if err != nil { - t.Fatalf("err: %v", err) + // Prove that it didn't alter the definition we compiled. + if !reflect.DeepEqual(query, backup.(*structs.PreparedQuery)) { + t.Fatalf("bad: %#v", query) } - fmt.Printf("%#v\n", *query) - fmt.Printf("%#v\n", *rendered) + // Try a bad HIL interpolation (syntax error). + query.Service.Service = "${name.full" + _, err = Compile(query) + if err == nil || !strings.Contains(err.Error(), "Bad format") { + t.Fatalf("bad: %v", err) + } + + // Try a bad HIL interpolation (syntax ok but unknown variable). + query.Service.Service = "${name.nope}" + _, err = Compile(query) + if err == nil || !strings.Contains(err.Error(), "unknown variable") { + t.Fatalf("bad: %v", err) + } + + // Try a bad regexp. + query.Template.Regexp = "^(nope$" + query.Service.Service = "${name.full}" + _, err = Compile(query) + if err == nil || !strings.Contains(err.Error(), "Bad Regexp") { + t.Fatalf("bad: %v", err) + } +} + +func TestTemplate_Render(t *testing.T) { + // Try a noop template that is all static. + { + query := &structs.PreparedQuery{ + Template: structs.QueryTemplateOptions{ + Type: structs.QueryTemplateTypeNamePrefixMatch, + }, + Service: structs.ServiceQuery{ + Service: "hellothere", + }, + } + ct, err := Compile(query) + if err != nil { + t.Fatalf("err: %v", err) + } + + actual, err := ct.Render("unused") + if err != nil { + t.Fatalf("err: %v", err) + } + if !reflect.DeepEqual(actual, query) { + t.Fatalf("bad: %#v", actual) + } + } + + // Try all the variables and functions. + query := &structs.PreparedQuery{ + Name: "hello-", + Template: structs.QueryTemplateOptions{ + Type: structs.QueryTemplateTypeNamePrefixMatch, + Regexp: "^(.*?)-(.*?)-(.*)$", + }, + Service: structs.ServiceQuery{ + Service: "${name.prefix} xxx ${name.full} xxx ${name.suffix}", + Tags: []string{ + "${match(0)}", + "${match(1)}", + "${match(2)}", + "${match(3)}", + "${match(4)}", + "${40 + 2}", + }, + }, + } + ct, err := Compile(query) + if err != nil { + t.Fatalf("err: %v", err) + } + + // Run a case that matches the regexp. + { + actual, err := ct.Render("hello-foo-bar-none") + if err != nil { + t.Fatalf("err: %v", err) + } + expected := &structs.PreparedQuery{ + Name: "hello-", + Template: structs.QueryTemplateOptions{ + Type: structs.QueryTemplateTypeNamePrefixMatch, + Regexp: "^(.*?)-(.*?)-(.*)$", + }, + Service: structs.ServiceQuery{ + Service: "hello- xxx hello-foo-bar-none xxx foo-bar-none", + Tags: []string{ + "hello-foo-bar-none", + "hello", + "foo", + "bar-none", + "", + "42", + }, + }, + } + if !reflect.DeepEqual(actual, expected) { + t.Fatalf("bad: %#v", actual) + } + } + + // Run a case that doesn't match the regexp + { + actual, err := ct.Render("hello-nope") + if err != nil { + t.Fatalf("err: %v", err) + } + expected := &structs.PreparedQuery{ + Name: "hello-", + Template: structs.QueryTemplateOptions{ + Type: structs.QueryTemplateTypeNamePrefixMatch, + Regexp: "^(.*?)-(.*?)-(.*)$", + }, + Service: structs.ServiceQuery{ + Service: "hello- xxx hello-nope xxx nope", + Tags: []string{ + "", + "", + "", + "", + "", + "42", + }, + }, + } + if !reflect.DeepEqual(actual, expected) { + t.Fatalf("bad: %#v", actual) + } + } }