package influxdb // This file is run within the "influxdb" package and allows for internal unit tests. import ( "reflect" "testing" "time" "github.com/influxdb/influxdb/influxql" ) // Ensure a measurement can return a set of unique tag values specified by an expression. func TestMeasurement_uniqueTagValues(t *testing.T) { // Create a measurement to run against. m := NewMeasurement("cpu") m.createFieldIfNotExists("value", influxql.Number) for i, tt := range []struct { expr string values map[string][]string }{ {expr: `1`, values: map[string][]string{}}, {expr: `foo = 'bar'`, values: map[string][]string{"foo": {"bar"}}}, {expr: `(region = 'us-west' AND value > 10) OR ('us-east' = region AND value > 20) OR (host = 'serverA' AND value > 30)`, values: map[string][]string{"region": {"us-east", "us-west"}, "host": {"serverA"}}}, } { // Extract unique tag values from the expression. values := m.uniqueTagValues(MustParseExpr(tt.expr)) if !reflect.DeepEqual(tt.values, values) { t.Errorf("%d. %s: mismatch: exp=%+v, got=%+v", i, tt.expr, tt.values, values) } } } // Ensure a measurement can expand an expression for all possible tag values used. func TestMeasurement_expandExpr(t *testing.T) { m := NewMeasurement("cpu") m.createFieldIfNotExists("value", influxql.Number) type tagSetExprString struct { tagExpr []tagExpr expr string } for i, tt := range []struct { expr string exprs []tagSetExprString }{ // Single tag key, single value. { expr: `region = 'us-east' AND value > 10`, exprs: []tagSetExprString{ {tagExpr: []tagExpr{{"region", []string{"us-east"}, influxql.EQ}}, expr: `value > 10.000`}, }, }, // Single tag key, multiple values. { expr: `(region = 'us-east' AND value > 10) OR (region = 'us-west' AND value > 20)`, exprs: []tagSetExprString{ {tagExpr: []tagExpr{{"region", []string{"us-east"}, influxql.EQ}}, expr: `value > 10.000`}, {tagExpr: []tagExpr{{"region", []string{"us-west"}, influxql.EQ}}, expr: `value > 20.000`}, }, }, // Multiple tag keys, multiple values. { expr: `(region = 'us-east' AND value > 10) OR ((host = 'serverA' OR host = 'serverB') AND value > 20)`, exprs: []tagSetExprString{ {tagExpr: []tagExpr{{key: "host", values: []string{"serverA"}, op: influxql.EQ}, {key: "region", values: []string{"us-east"}, op: influxql.EQ}}, expr: "(value > 10.000) OR (value > 20.000)"}, {tagExpr: []tagExpr{{key: "host", values: []string{"serverA"}, op: influxql.EQ}, {key: "region", values: []string{"us-east"}, op: influxql.NEQ}}, expr: "value > 20.000"}, {tagExpr: []tagExpr{{key: "host", values: []string{"serverB"}, op: influxql.EQ}, {key: "region", values: []string{"us-east"}, op: influxql.EQ}}, expr: "(value > 10.000) OR (value > 20.000)"}, {tagExpr: []tagExpr{{key: "host", values: []string{"serverB"}, op: influxql.EQ}, {key: "region", values: []string{"us-east"}, op: influxql.NEQ}}, expr: "value > 20.000"}, {tagExpr: []tagExpr{{key: "host", values: []string{"serverA", "serverB"}, op: influxql.NEQ}, {key: "region", values: []string{"us-east"}, op: influxql.EQ}}, expr: "value > 10.000"}, }, }, } { // Expand out an expression to all possible expressions based on tag values. tagExprs := m.expandExpr(MustParseExpr(tt.expr)) // Convert to intermediate representation. var a []tagSetExprString for _, tagExpr := range tagExprs { a = append(a, tagSetExprString{tagExpr: tagExpr.values, expr: tagExpr.expr.String()}) } // Validate that the expanded expressions are what we expect. if !reflect.DeepEqual(tt.exprs, a) { t.Errorf("%d. %s: mismatch:\n\nexp=%#v\n\ngot=%#v\n\ns", i, tt.expr, tt.exprs, a) } } } // Ensure the createMeasurementsIfNotExistsCommand operates correctly. func TestCreateMeasurementsCommand(t *testing.T) { var err error var n int c := newCreateMeasurementsIfNotExistsCommand("foo") if c == nil { t.Fatal("createMeasurementsIfNotExistsCommand is nil") } // Add Measurement twice, to make sure nothing blows up. c.addMeasurementIfNotExists("bar") c.addMeasurementIfNotExists("bar") n = len(c.Measurements) if n != 1 { t.Fatalf("wrong number of measurements, expected 1, got %d", n) } // Add Series, no tags. c.addSeriesIfNotExists("bar", nil) // Add Series, some tags. tags := map[string]string{"host": "server01"} c.addSeriesIfNotExists("bar", tags) // Add Series, same tags again. c.addSeriesIfNotExists("bar", tags) for _, m := range c.Measurements { if m.Name == "bar" { if len(m.Tags) != 2 { t.Fatalf("measurement has wrong number of tags, expected 2, got %d", n) } } } // Add a field. err = c.addFieldIfNotExists("bar", "value", influxql.Number) if err != nil { t.Fatal("error adding field \"value\"") } // Add same field again. err = c.addFieldIfNotExists("bar", "value", influxql.Number) if err != nil { t.Fatal("error re-adding field \"value\"") } // Add another field. err = c.addFieldIfNotExists("bar", "value2", influxql.String) if err != nil { t.Fatal("error re-adding field \"value2\"") } for _, m := range c.Measurements { if m.Name == "bar" { if len(m.Fields) != 2 { t.Fatalf("measurement has wrong number of fields, expected 2, got %d", n) } } } } // Ensure the createMeasurementsIfNotExistsCommand returns expected errors. func TestCreateMeasurementsCommand_Errors(t *testing.T) { var err error c := newCreateMeasurementsIfNotExistsCommand("foo") if c == nil { t.Fatal("createMeasurementsIfNotExistsCommand is nil") } // Ensure fields can be added to non-existent Measurements. The // Measurements should be created automatically. c.addSeriesIfNotExists("bar", nil) err = c.addFieldIfNotExists("bar", "value", influxql.Number) if err != nil { t.Fatalf("unexpected error got %s", err.Error()) } // Add Measurement. Adding it now should be OK. c.addMeasurementIfNotExists("bar") // Test type conflicts err = c.addFieldIfNotExists("bar", "value", influxql.Number) if err != nil { t.Fatal("error adding field \"value\"") } err = c.addFieldIfNotExists("bar", "value", influxql.String) if err != ErrFieldTypeConflict { t.Fatalf("expected ErrFieldTypeConflict got %s", err.Error()) } } // Test comparing seriesIDs for equality. func Test_seriesIDs_equals(t *testing.T) { ids1 := seriesIDs{1, 2, 3} ids2 := seriesIDs{1, 2, 3} ids3 := seriesIDs{4, 5, 6} if !ids1.equals(ids2) { t.Fatal("expected ids1 == ids2") } else if ids1.equals(ids3) { t.Fatal("expected ids1 != ids3") } } // Test intersecting sets of seriesIDs. func Test_seriesIDs_intersect(t *testing.T) { // Test swaping l & r, all branches of if-else, and exit loop when 'j < len(r)' ids1 := seriesIDs{1, 3, 4, 5, 6} ids2 := seriesIDs{1, 2, 3, 7} exp := seriesIDs{1, 3} got := ids1.intersect(ids2) if !exp.equals(got) { t.Fatalf("exp=%v, got=%v", exp, got) } // Test exit for loop when 'i < len(l)' ids1 = seriesIDs{1} ids2 = seriesIDs{1, 2} exp = seriesIDs{1} got = ids1.intersect(ids2) if !exp.equals(got) { t.Fatalf("exp=%v, got=%v", exp, got) } } // Test union sets of seriesIDs. func Test_seriesIDs_union(t *testing.T) { // Test all branches of if-else, exit loop because of 'j < len(r)', and append remainder from left. ids1 := seriesIDs{1, 2, 3, 7} ids2 := seriesIDs{1, 3, 4, 5, 6} exp := seriesIDs{1, 2, 3, 4, 5, 6, 7} got := ids1.union(ids2) if !exp.equals(got) { t.Fatalf("exp=%v, got=%v", exp, got) } // Test exit because of 'i < len(l)' and append remainder from right. ids1 = seriesIDs{1} ids2 = seriesIDs{1, 2} exp = seriesIDs{1, 2} got = ids1.union(ids2) if !exp.equals(got) { t.Fatalf("exp=%v, got=%v", exp, got) } } // Test removing one set of seriesIDs from another. func Test_seriesIDs_reject(t *testing.T) { // Test all branches of if-else, exit loop because of 'j < len(r)', and append remainder from left. ids1 := seriesIDs{1, 2, 3, 7} ids2 := seriesIDs{1, 3, 4, 5, 6} exp := seriesIDs{2, 7} got := ids1.reject(ids2) if !exp.equals(got) { t.Fatalf("exp=%v, got=%v", exp, got) } // Test exit because of 'i < len(l)'. ids1 = seriesIDs{1} ids2 = seriesIDs{1, 2} exp = seriesIDs{} got = ids1.reject(ids2) if !exp.equals(got) { t.Fatalf("exp=%v, got=%v", exp, got) } } // Test shard group selection. func TestShardGroup_Contains(t *testing.T) { // Make a shard group 1 hour in duration g := newShardGroup() g.StartTime, _ = time.Parse(time.RFC3339, "2000-01-01T00:00:00Z") g.EndTime = g.StartTime.Add(time.Hour) if !g.Contains(g.StartTime.Add(-time.Minute), g.EndTime) { t.Fatal("shard group not selected when min before start time") } if !g.Contains(g.StartTime, g.EndTime.Add(time.Minute)) { t.Fatal("shard group not selected when max after after end time") } if !g.Contains(g.StartTime.Add(-time.Minute), g.EndTime.Add(time.Minute)) { t.Fatal("shard group not selected when min before start time and when max after end time") } if !g.Contains(g.StartTime.Add(time.Minute), g.EndTime.Add(-time.Minute)) { t.Fatal("shard group not selected when min after start time and when max before end time") } if !g.Contains(g.StartTime, g.EndTime) { t.Fatal("shard group not selected when min at start time and when max at end time") } if !g.Contains(g.StartTime, g.StartTime) { t.Fatal("shard group not selected when min and max set to start time") } if !g.Contains(g.EndTime, g.EndTime) { t.Fatal("shard group not selected when min and max set to end time") } if g.Contains(g.StartTime.Add(-10*time.Hour), g.EndTime.Add(-9*time.Hour)) { t.Fatal("shard group selected when both min and max before shard times") } if g.Contains(g.StartTime.Add(24*time.Hour), g.EndTime.Add(25*time.Hour)) { t.Fatal("shard group selected when both min and max after shard times") } } // MustParseExpr parses an expression string and returns its AST representation. func MustParseExpr(s string) influxql.Expr { expr, err := influxql.ParseExpr(s) if err != nil { panic(err.Error()) } return expr } func strref(s string) *string { return &s }