diff --git a/app/vmctl/backoff/backoff_test.go b/app/vmctl/backoff/backoff_test.go index 3506c4a18..2205b0e34 100644 --- a/app/vmctl/backoff/backoff_test.go +++ b/app/vmctl/backoff/backoff_test.go @@ -7,103 +7,106 @@ import ( "time" ) -func TestRetry_Do(t *testing.T) { - counter := 0 - tests := []struct { - name string - backoffRetries int - backoffFactor float64 - backoffMinDuration time.Duration - retryableFunc retryableFunc - cancelTimeout time.Duration - want uint64 - wantErr bool - }{ - { - name: "return bad request", - retryableFunc: func() error { - return ErrBadRequest - }, - want: 0, - wantErr: true, - }, - { - name: "empty retries values", - retryableFunc: func() error { - time.Sleep(time.Millisecond * 100) - return nil - }, - want: 0, - wantErr: true, - }, - { - name: "only one retry test", - backoffRetries: 5, - backoffFactor: 1.7, - backoffMinDuration: time.Millisecond * 10, - retryableFunc: func() error { - t := time.NewTicker(time.Millisecond * 5) - defer t.Stop() - for range t.C { - counter++ - if counter%2 == 0 { - return fmt.Errorf("got some error") - } - if counter%3 == 0 { - return nil - } - } - return nil - }, - want: 1, - wantErr: false, - }, - { - name: "all retries failed test", - backoffRetries: 5, - backoffFactor: 0.1, - backoffMinDuration: time.Millisecond * 10, - retryableFunc: func() error { - t := time.NewTicker(time.Millisecond * 5) - defer t.Stop() - for range t.C { - return fmt.Errorf("got some error") - } - return nil - }, - want: 5, - wantErr: true, - }, - { - name: "cancel context", - backoffRetries: 5, - backoffFactor: 1.7, - backoffMinDuration: time.Millisecond * 10, - retryableFunc: func() error { - return fmt.Errorf("got some error") - }, - cancelTimeout: time.Millisecond * 40, - want: 3, - wantErr: true, - }, +func TestBackoffRetry_Failure(t *testing.T) { + f := func(backoffFactor float64, backoffRetries int, cancelTimeout time.Duration, retryFunc func() error, resultExpected int) { + t.Helper() + + r := &Backoff{ + retries: backoffRetries, + factor: backoffFactor, + minDuration: time.Millisecond * 10, + } + ctx := context.Background() + if cancelTimeout != 0 { + newCtx, cancelFn := context.WithTimeout(context.Background(), cancelTimeout) + ctx = newCtx + defer cancelFn() + } + + result, err := r.Retry(ctx, retryFunc) + if err == nil { + t.Fatalf("expecting non-nil error") + } + if result != uint64(resultExpected) { + t.Fatalf("unexpected result: got %d; want %d", result, resultExpected) + } } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - r := &Backoff{retries: tt.backoffRetries, factor: tt.backoffFactor, minDuration: tt.backoffMinDuration} - ctx := context.Background() - if tt.cancelTimeout != 0 { - newCtx, cancelFn := context.WithTimeout(context.Background(), tt.cancelTimeout) - ctx = newCtx - defer cancelFn() - } - got, err := r.Retry(ctx, tt.retryableFunc) - if (err != nil) != tt.wantErr { - t.Errorf("Retry() error = %v, wantErr %v", err, tt.wantErr) - return - } - if got != tt.want { - t.Errorf("Retry() got = %v, want %v", got, tt.want) - } - }) + + // return bad request + retryFunc := func() error { + return ErrBadRequest } + f(0, 0, 0, retryFunc, 0) + + // empty retries values + retryFunc = func() error { + time.Sleep(time.Millisecond * 100) + return nil + } + f(0, 0, 0, retryFunc, 0) + + // all retries failed test + backoffFactor := 0.1 + backoffRetries := 5 + cancelTimeout := time.Second * 0 + retryFunc = func() error { + t := time.NewTicker(time.Millisecond * 5) + defer t.Stop() + for range t.C { + return fmt.Errorf("got some error") + } + return nil + } + resultExpected := 5 + f(backoffFactor, backoffRetries, cancelTimeout, retryFunc, resultExpected) + + // cancel context + backoffFactor = 1.7 + backoffRetries = 5 + cancelTimeout = time.Millisecond * 40 + retryFunc = func() error { + return fmt.Errorf("got some error") + } + resultExpected = 3 + f(backoffFactor, backoffRetries, cancelTimeout, retryFunc, resultExpected) +} + +func TestBackoffRetry_Success(t *testing.T) { + f := func(retryFunc func() error, resultExpected int) { + t.Helper() + + r := &Backoff{ + retries: 5, + factor: 1.7, + minDuration: time.Millisecond * 10, + } + ctx := context.Background() + + result, err := r.Retry(ctx, retryFunc) + if err != nil { + t.Fatalf("Retry() error: %s", err) + } + if result != uint64(resultExpected) { + t.Fatalf("unexpected result: got %d; want %d", result, resultExpected) + } + } + + // only one retry test + counter := 0 + retryFunc := func() error { + t := time.NewTicker(time.Millisecond * 5) + defer t.Stop() + for range t.C { + counter++ + if counter%2 == 0 { + return fmt.Errorf("got some error") + } + if counter%3 == 0 { + return nil + } + } + return nil + } + resultExpected := 1 + f(retryFunc, resultExpected) } diff --git a/app/vmctl/influx/influx_test.go b/app/vmctl/influx/influx_test.go index 94a90bac7..d36fdb642 100644 --- a/app/vmctl/influx/influx_test.go +++ b/app/vmctl/influx/influx_test.go @@ -3,125 +3,97 @@ package influx import "testing" func TestFetchQuery(t *testing.T) { - testCases := []struct { - s Series - timeFilter string - expected string - }{ - { - s: Series{ - Measurement: "cpu", - Field: "value", - LabelPairs: []LabelPair{ - { - Name: "foo", - Value: "bar", - }, - }, - }, - expected: `select "value" from "cpu" where "foo"::tag='bar'`, - }, - { - s: Series{ - Measurement: "cpu", - Field: "value", - LabelPairs: []LabelPair{ - { - Name: "foo", - Value: "bar", - }, - { - Name: "baz", - Value: "qux", - }, - }, - }, - expected: `select "value" from "cpu" where "foo"::tag='bar' and "baz"::tag='qux'`, - }, - { - s: Series{ - Measurement: "cpu", - Field: "value", - LabelPairs: []LabelPair{ - { - Name: "foo", - Value: "b'ar", - }, - }, - }, - timeFilter: "time >= now()", - expected: `select "value" from "cpu" where "foo"::tag='b\'ar' and time >= now()`, - }, - { - s: Series{ - Measurement: "cpu", - Field: "value", - LabelPairs: []LabelPair{ - { - Name: "name", - Value: `dev-mapper-centos\x2dswap.swap`, - }, - { - Name: "state", - Value: "dev-mapp'er-c'en'tos", - }, - }, - }, - timeFilter: "time >= now()", - expected: `select "value" from "cpu" where "name"::tag='dev-mapper-centos\\x2dswap.swap' and "state"::tag='dev-mapp\'er-c\'en\'tos' and time >= now()`, - }, - { - s: Series{ - Measurement: "cpu", - Field: "value", - }, - timeFilter: "time >= now()", - expected: `select "value" from "cpu" where time >= now()`, - }, - { - s: Series{ - Measurement: "cpu", - Field: "value", - }, - expected: `select "value" from "cpu"`, - }, - } + f := func(s *Series, timeFilter, resultExpected string) { + t.Helper() - for _, tc := range testCases { - query := tc.s.fetchQuery(tc.timeFilter) - if query != tc.expected { - t.Fatalf("got: \n%s;\nexpected: \n%s", query, tc.expected) + result := s.fetchQuery(timeFilter) + if result != resultExpected { + t.Fatalf("unexpected result\ngot\n%s\nwant\n%s", result, resultExpected) } } + + f(&Series{ + Measurement: "cpu", + Field: "value", + LabelPairs: []LabelPair{ + { + Name: "foo", + Value: "bar", + }, + }, + }, "", `select "value" from "cpu" where "foo"::tag='bar'`) + + f(&Series{ + Measurement: "cpu", + Field: "value", + LabelPairs: []LabelPair{ + { + Name: "foo", + Value: "bar", + }, + { + Name: "baz", + Value: "qux", + }, + }, + }, "", `select "value" from "cpu" where "foo"::tag='bar' and "baz"::tag='qux'`) + + f(&Series{ + Measurement: "cpu", + Field: "value", + LabelPairs: []LabelPair{ + { + Name: "foo", + Value: "b'ar", + }, + }, + }, "time >= now()", `select "value" from "cpu" where "foo"::tag='b\'ar' and time >= now()`) + + f(&Series{ + Measurement: "cpu", + Field: "value", + LabelPairs: []LabelPair{ + { + Name: "name", + Value: `dev-mapper-centos\x2dswap.swap`, + }, + { + Name: "state", + Value: "dev-mapp'er-c'en'tos", + }, + }, + }, "time >= now()", `select "value" from "cpu" where "name"::tag='dev-mapper-centos\\x2dswap.swap' and "state"::tag='dev-mapp\'er-c\'en\'tos' and time >= now()`) + + f(&Series{ + Measurement: "cpu", + Field: "value", + }, "time >= now()", `select "value" from "cpu" where time >= now()`) + + f(&Series{ + Measurement: "cpu", + Field: "value", + }, "", `select "value" from "cpu"`) } func TestTimeFilter(t *testing.T) { - testCases := []struct { - start string - end string - expected string - }{ - { - start: "2020-01-01T20:07:00Z", - end: "2020-01-01T21:07:00Z", - expected: "time >= '2020-01-01T20:07:00Z' and time <= '2020-01-01T21:07:00Z'", - }, - { - expected: "", - }, - { - start: "2020-01-01T20:07:00Z", - expected: "time >= '2020-01-01T20:07:00Z'", - }, - { - end: "2020-01-01T21:07:00Z", - expected: "time <= '2020-01-01T21:07:00Z'", - }, - } - for _, tc := range testCases { - f := timeFilter(tc.start, tc.end) - if f != tc.expected { - t.Fatalf("got: \n%q;\nexpected: \n%q", f, tc.expected) + f := func(start, end, resultExpected string) { + t.Helper() + + result := timeFilter(start, end) + if result != resultExpected { + t.Fatalf("unexpected result\ngot\n%v\nwant\n%s", result, resultExpected) } } + + // no start and end filters + f("", "", "") + + // missing end filter + f("2020-01-01T20:07:00Z", "", "time >= '2020-01-01T20:07:00Z'") + + // missing start filter + f("", "2020-01-01T21:07:00Z", "time <= '2020-01-01T21:07:00Z'") + + // both start and end filters + f("2020-01-01T20:07:00Z", "2020-01-01T21:07:00Z", "time >= '2020-01-01T20:07:00Z' and time <= '2020-01-01T21:07:00Z'") } diff --git a/app/vmctl/influx/parser_test.go b/app/vmctl/influx/parser_test.go index 861f379cf..cc89eeef9 100644 --- a/app/vmctl/influx/parser_test.go +++ b/app/vmctl/influx/parser_test.go @@ -6,71 +6,71 @@ import ( "testing" ) -func TestSeries_Unmarshal(t *testing.T) { +func TestSeriesUnmarshal(t *testing.T) { + f := func(s string, resultExpected *Series) { + t.Helper() + + result := &Series{} + if err := result.unmarshal(s); err != nil { + t.Fatalf("cannot unmarshal series from %q: %s", s, err) + } + if !reflect.DeepEqual(result, resultExpected) { + t.Fatalf("unexpected result\ngot\n%v\nwant\n%v", result, resultExpected) + } + } + tag := func(name, value string) LabelPair { return LabelPair{ Name: name, Value: value, } } - series := func(measurement string, lp ...LabelPair) Series { - return Series{ + series := func(measurement string, lp ...LabelPair) *Series { + return &Series{ Measurement: measurement, LabelPairs: lp, } } - testCases := []struct { - got string - want Series - }{ - { - got: "cpu", - want: series("cpu"), - }, - { - got: "cpu,host=localhost", - want: series("cpu", tag("host", "localhost")), - }, - { - got: "cpu,host=localhost,instance=instance", - want: series("cpu", tag("host", "localhost"), tag("instance", "instance")), - }, - { - got: `fo\,bar\=baz,x\=\b=\\a\,\=\q\ `, - want: series("fo,bar=baz", tag(`x=\b`, `\a,=\q `)), - }, - { - got: "cpu,host=192.168.0.1,instance=fe80::fdc8:5e36:c2c6:baac%utun1", - want: series("cpu", tag("host", "192.168.0.1"), tag("instance", "fe80::fdc8:5e36:c2c6:baac%utun1")), - }, - { - got: `cpu,db=db1,host=localhost,server=host\=localhost\ user\=user\ `, - want: series("cpu", tag("db", "db1"), - tag("host", "localhost"), tag("server", "host=localhost user=user ")), - }, - } - for _, tc := range testCases { - s := Series{} - if err := s.unmarshal(tc.got); err != nil { - t.Fatalf("%q: unmarshal err: %s", tc.got, err) - } - if !reflect.DeepEqual(s, tc.want) { - t.Fatalf("%q: expected\n%#v\nto be equal\n%#v", tc.got, s, tc.want) - } - } + + f("cpu", series("cpu")) + + f("cpu,host=localhost", series("cpu", tag("host", "localhost"))) + + f("cpu,host=localhost,instance=instance", series("cpu", tag("host", "localhost"), tag("instance", "instance"))) + + f(`fo\,bar\=baz,x\=\b=\\a\,\=\q\ `, series("fo,bar=baz", tag(`x=\b`, `\a,=\q `))) + + f("cpu,host=192.168.0.1,instance=fe80::fdc8:5e36:c2c6:baac%utun1", series("cpu", tag("host", "192.168.0.1"), tag("instance", "fe80::fdc8:5e36:c2c6:baac%utun1"))) + + f(`cpu,db=db1,host=localhost,server=host\=localhost\ user\=user\ `, series("cpu", tag("db", "db1"), tag("host", "localhost"), tag("server", "host=localhost user=user "))) } -func TestToFloat64(t *testing.T) { - f := func(in any, want float64) { +func TestToFloat64_Failure(t *testing.T) { + f := func(in any) { t.Helper() - got, err := toFloat64(in) - if err != nil { - t.Fatalf("unexpected err: %s", err) - } - if got != want { - t.Errorf("got %v; want %v", got, want) + + _, err := toFloat64(in) + if err == nil { + t.Fatalf("expecting non-nil error") } } + + f("text") +} + +func TestToFloat64_Success(t *testing.T) { + f := func(in any, resultExpected float64) { + t.Helper() + + result, err := toFloat64(in) + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + if result != resultExpected { + t.Fatalf("unexpected result: got %v; want %v", result, resultExpected) + } + } + f("123.4", 123.4) f(float64(123.4), 123.4) f(float32(12), 12) @@ -78,9 +78,4 @@ func TestToFloat64(t *testing.T) { f(true, 1) f(false, 0) f(json.Number("123456.789"), 123456.789) - - _, err := toFloat64("text") - if err == nil { - t.Fatalf("expected to get err; got nil instead") - } } diff --git a/app/vmctl/prometheus/prometheus_test.go b/app/vmctl/prometheus/prometheus_test.go index 6fd738f4e..43ca81d2a 100644 --- a/app/vmctl/prometheus/prometheus_test.go +++ b/app/vmctl/prometheus/prometheus_test.go @@ -5,30 +5,27 @@ import ( ) func TestInRange(t *testing.T) { - testCases := []struct { - filterMin, filterMax int64 - blockMin, blockMax int64 - expected bool - }{ - {0, 0, 1, 2, true}, - {0, 3, 1, 2, true}, - {0, 3, 4, 5, false}, - {3, 0, 1, 2, false}, - {3, 0, 2, 4, true}, - {3, 10, 1, 2, false}, - {3, 10, 1, 4, true}, - {3, 10, 5, 9, true}, - {3, 10, 9, 12, true}, - {3, 10, 12, 15, false}, - } - for _, tc := range testCases { + f := func(filterMin, filterMax, blockMin, blockMax int64, resultExpected bool) { + t.Helper() + f := filter{ - min: tc.filterMin, - max: tc.filterMax, + min: filterMin, + max: filterMax, } - got := f.inRange(tc.blockMin, tc.blockMax) - if got != tc.expected { - t.Fatalf("got %v; expected %v: %v", got, tc.expected, tc) + result := f.inRange(blockMin, blockMax) + if result != resultExpected { + t.Fatalf("unexpected result; got %v; want %v", result, resultExpected) } } + + f(0, 0, 1, 2, true) + f(0, 3, 1, 2, true) + f(0, 3, 4, 5, false) + f(3, 0, 1, 2, false) + f(3, 0, 2, 4, true) + f(3, 10, 1, 2, false) + f(3, 10, 1, 4, true) + f(3, 10, 5, 9, true) + f(3, 10, 9, 12, true) + f(3, 10, 12, 15, false) } diff --git a/app/vmctl/prometheus_test.go b/app/vmctl/prometheus_test.go index 13281d025..d14b0e08e 100644 --- a/app/vmctl/prometheus_test.go +++ b/app/vmctl/prometheus_test.go @@ -139,11 +139,11 @@ func Test_prometheusProcessor_run(t *testing.T) { _, err = w.Write(input) if err != nil { - t.Error(err) + t.Fatalf("cannot send 'Y' to importer: %s", err) } err = w.Close() if err != nil { - t.Error(err) + t.Fatalf("cannot close writer: %s", err) } stdin := os.Stdin @@ -151,7 +151,6 @@ func Test_prometheusProcessor_run(t *testing.T) { defer func() { os.Stdin = stdin _ = r.Close() - _ = w.Close() }() os.Stdin = r } @@ -162,7 +161,7 @@ func Test_prometheusProcessor_run(t *testing.T) { } if err := pp.run(); (err != nil) != tt.wantErr { - t.Errorf("run() error = %v, wantErr %v", err, tt.wantErr) + t.Fatalf("run() error = %v, wantErr %v", err, tt.wantErr) } }) } diff --git a/app/vmctl/stepper/split_test.go b/app/vmctl/stepper/split_test.go index a5f10cc8a..3586bdf82 100644 --- a/app/vmctl/stepper/split_test.go +++ b/app/vmctl/stepper/split_test.go @@ -16,535 +16,358 @@ func mustParseDatetime(t string) time.Time { return result } -func Test_splitDateRange(t *testing.T) { - type args struct { - start string - end string - granularity string - } - tests := []struct { - name string - args args - want []testTimeRange - wantErr bool - }{ - { - name: "validates start is before end", - args: args{ - start: "2022-02-01T00:00:00Z", - end: "2022-01-01T00:00:00Z", - granularity: StepMonth, - }, - want: nil, - wantErr: true, - }, - { - name: "validates granularity value", - args: args{ - start: "2022-01-01T00:00:00Z", - end: "2022-02-01T00:00:00Z", - granularity: "non-existent-format", - }, - want: nil, - wantErr: true, - }, - { - name: "month chunking", - args: args{ - start: "2022-01-03T11:11:11Z", - end: "2022-03-03T12:12:12Z", - granularity: StepMonth, - }, - want: []testTimeRange{ - { - "2022-01-03T11:11:11Z", - "2022-01-31T23:59:59.999999999Z", - }, - { - "2022-02-01T00:00:00Z", - "2022-02-28T23:59:59.999999999Z", - }, - { - "2022-03-01T00:00:00Z", - "2022-03-03T12:12:12Z", - }, - }, - wantErr: false, - }, - { - name: "daily chunking", - args: args{ - start: "2022-01-03T11:11:11Z", - end: "2022-01-05T12:12:12Z", - granularity: StepDay, - }, - want: []testTimeRange{ - { - "2022-01-03T11:11:11Z", - "2022-01-04T11:11:11Z", - }, - { - "2022-01-04T11:11:11Z", - "2022-01-05T11:11:11Z", - }, - { - "2022-01-05T11:11:11Z", - "2022-01-05T12:12:12Z", - }, - }, - wantErr: false, - }, - { - name: "hourly chunking", - args: args{ - start: "2022-01-03T11:11:11Z", - end: "2022-01-03T14:14:14Z", - granularity: StepHour, - }, - want: []testTimeRange{ - { - "2022-01-03T11:11:11Z", - "2022-01-03T12:11:11Z", - }, - { - "2022-01-03T12:11:11Z", - "2022-01-03T13:11:11Z", - }, - { - "2022-01-03T13:11:11Z", - "2022-01-03T14:11:11Z", - }, - { - "2022-01-03T14:11:11Z", - "2022-01-03T14:14:14Z", - }, - }, - wantErr: false, - }, - { - name: "month chunking with one day time range", - args: args{ - start: "2022-01-03T11:11:11Z", - end: "2022-01-04T12:12:12Z", - granularity: StepMonth, - }, - want: []testTimeRange{ - { - "2022-01-03T11:11:11Z", - "2022-01-04T12:12:12Z", - }, - }, - wantErr: false, - }, - { - name: "month chunking with same day time range", - args: args{ - start: "2022-01-03T11:11:11Z", - end: "2022-01-03T12:12:12Z", - granularity: StepMonth, - }, - want: []testTimeRange{ - { - "2022-01-03T11:11:11Z", - "2022-01-03T12:12:12Z", - }, - }, - wantErr: false, - }, - { - name: "month chunking with one month and two days range", - args: args{ - start: "2022-01-03T11:11:11Z", - end: "2022-02-03T00:00:00Z", - granularity: StepMonth, - }, - want: []testTimeRange{ - { - "2022-01-03T11:11:11Z", - "2022-01-31T23:59:59.999999999Z", - }, - { - "2022-02-01T00:00:00Z", - "2022-02-03T00:00:00Z", - }, - }, - wantErr: false, - }, - { - name: "week chunking with not full week", - args: args{ - start: "2023-07-30T00:00:00Z", - end: "2023-08-05T23:59:59.999999999Z", - granularity: StepWeek, - }, - want: []testTimeRange{ - { - "2023-07-30T00:00:00Z", - "2023-08-05T23:59:59.999999999Z", - }, - }, - }, - { - name: "week chunking with start of the week and end of the week", - args: args{ - start: "2023-07-30T00:00:00Z", - end: "2023-08-06T00:00:00Z", - granularity: StepWeek, - }, - want: []testTimeRange{ - { - "2023-07-30T00:00:00Z", - "2023-08-06T00:00:00Z", - }, - }, - }, - { - name: "week chunking with next one day week", - args: args{ - start: "2023-07-30T00:00:00Z", - end: "2023-08-07T01:12:00Z", - granularity: StepWeek, - }, - want: []testTimeRange{ - { - "2023-07-30T00:00:00Z", - "2023-08-06T00:00:00Z", - }, - { - "2023-08-06T00:00:00Z", - "2023-08-07T01:12:00Z", - }, - }, - }, - { - name: "week chunking with month and not full week representation", - args: args{ - start: "2023-07-30T00:00:00Z", - end: "2023-09-01T01:12:00Z", - granularity: StepWeek, - }, - want: []testTimeRange{ - { - "2023-07-30T00:00:00Z", - "2023-08-06T00:00:00Z", - }, - { - "2023-08-06T00:00:00Z", - "2023-08-13T00:00:00Z", - }, - { - "2023-08-13T00:00:00Z", - "2023-08-20T00:00:00Z", - }, - { - "2023-08-20T00:00:00Z", - "2023-08-27T00:00:00Z", - }, - { - "2023-08-27T00:00:00Z", - "2023-09-01T01:12:00Z", - }, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - start := mustParseDatetime(tt.args.start) - end := mustParseDatetime(tt.args.end) +func TestSplitDateRange_Failure(t *testing.T) { + f := func(startStr, endStr, granularity string) { + t.Helper() - got, err := SplitDateRange(start, end, tt.args.granularity, false) - if (err != nil) != tt.wantErr { - t.Errorf("splitDateRange() error = %v, wantErr %v", err, tt.wantErr) - return - } + start := mustParseDatetime(startStr) + end := mustParseDatetime(endStr) - var testExpectedResults [][]time.Time - if tt.want != nil { - testExpectedResults = make([][]time.Time, 0) - for _, dr := range tt.want { - testExpectedResults = append(testExpectedResults, []time.Time{ - mustParseDatetime(dr[0]), - mustParseDatetime(dr[1]), - }) - } - } - - if !reflect.DeepEqual(got, testExpectedResults) { - t.Errorf("splitDateRange() got = %v, want %v", got, testExpectedResults) - } - }) + _, err := SplitDateRange(start, end, granularity, false) + if err == nil { + t.Fatalf("expecting non-nil result") + } } + + // validates start is before end + f("2022-02-01T00:00:00Z", "2022-01-01T00:00:00Z", StepMonth) + + // validates granularity value + f("2022-01-01T00:00:00Z", "2022-02-01T00:00:00Z", "non-existent-format") } -func Test_splitDateRange_reverse(t *testing.T) { - type args struct { - start string - end string - granularity string - timeReverse bool - } - tests := []struct { - name string - args args - want []testTimeRange - wantErr bool - }{ - { - name: "validates start is before end", - args: args{ - start: "2022-02-01T00:00:00Z", - end: "2022-01-01T00:00:00Z", - granularity: StepMonth, - timeReverse: true, - }, - want: nil, - wantErr: true, - }, - { - name: "validates granularity value", - args: args{ - start: "2022-01-01T00:00:00Z", - end: "2022-02-01T00:00:00Z", - granularity: "non-existent-format", - timeReverse: true, - }, - want: nil, - wantErr: true, - }, - { - name: "month chunking", - args: args{ - start: "2022-01-03T11:11:11Z", - end: "2022-03-03T12:12:12Z", - granularity: StepMonth, - timeReverse: true, - }, - want: []testTimeRange{ - { - "2022-03-01T00:00:00Z", - "2022-03-03T12:12:12Z", - }, - { - "2022-02-01T00:00:00Z", - "2022-02-28T23:59:59.999999999Z", - }, - { - "2022-01-03T11:11:11Z", - "2022-01-31T23:59:59.999999999Z", - }, - }, - wantErr: false, - }, - { - name: "daily chunking", - args: args{ - start: "2022-01-03T11:11:11Z", - end: "2022-01-05T12:12:12Z", - granularity: StepDay, - timeReverse: true, - }, - want: []testTimeRange{ - { - "2022-01-05T11:11:11Z", - "2022-01-05T12:12:12Z", - }, - { - "2022-01-04T11:11:11Z", - "2022-01-05T11:11:11Z", - }, - { - "2022-01-03T11:11:11Z", - "2022-01-04T11:11:11Z", - }, - }, - wantErr: false, - }, - { - name: "hourly chunking", - args: args{ - start: "2022-01-03T11:11:11Z", - end: "2022-01-03T14:14:14Z", - granularity: StepHour, - timeReverse: true, - }, - want: []testTimeRange{ - { - "2022-01-03T14:11:11Z", - "2022-01-03T14:14:14Z", - }, - { - "2022-01-03T13:11:11Z", - "2022-01-03T14:11:11Z", - }, - { - "2022-01-03T12:11:11Z", - "2022-01-03T13:11:11Z", - }, - { - "2022-01-03T11:11:11Z", - "2022-01-03T12:11:11Z", - }, - }, - wantErr: false, - }, - { - name: "month chunking with one day time range", - args: args{ - start: "2022-01-03T11:11:11Z", - end: "2022-01-04T12:12:12Z", - granularity: StepMonth, - timeReverse: true, - }, - want: []testTimeRange{ - { - "2022-01-03T11:11:11Z", - "2022-01-04T12:12:12Z", - }, - }, - wantErr: false, - }, - { - name: "month chunking with same day time range", - args: args{ - start: "2022-01-03T11:11:11Z", - end: "2022-01-03T12:12:12Z", - granularity: StepMonth, - timeReverse: true, - }, - want: []testTimeRange{ - { - "2022-01-03T11:11:11Z", - "2022-01-03T12:12:12Z", - }, - }, - wantErr: false, - }, - { - name: "month chunking with one month and two days range", - args: args{ - start: "2022-01-03T11:11:11Z", - end: "2022-02-03T00:00:00Z", - granularity: StepMonth, - timeReverse: true, - }, - want: []testTimeRange{ - { - "2022-02-01T00:00:00Z", - "2022-02-03T00:00:00Z", - }, - { - "2022-01-03T11:11:11Z", - "2022-01-31T23:59:59.999999999Z", - }, - }, - wantErr: false, - }, - { - name: "week chunking with not full week", - args: args{ - start: "2023-07-30T00:00:00Z", - end: "2023-08-05T23:59:59.999999999Z", - granularity: StepWeek, - timeReverse: true, - }, - want: []testTimeRange{ - { - "2023-07-30T00:00:00Z", - "2023-08-05T23:59:59.999999999Z", - }, - }, - }, - { - name: "week chunking with start of the week and end of the week", - args: args{ - start: "2023-07-30T00:00:00Z", - end: "2023-08-06T00:00:00Z", - granularity: StepWeek, - timeReverse: true, - }, - want: []testTimeRange{ - { - "2023-07-30T00:00:00Z", - "2023-08-06T00:00:00Z", - }, - }, - }, - { - name: "week chunking with next one day week", - args: args{ - start: "2023-07-30T00:00:00Z", - end: "2023-08-07T01:12:00Z", - granularity: StepWeek, - timeReverse: true, - }, - want: []testTimeRange{ - { - "2023-08-06T00:00:00Z", - "2023-08-07T01:12:00Z", - }, - { - "2023-07-30T00:00:00Z", - "2023-08-06T00:00:00Z", - }, - }, - }, - { - name: "week chunking with month and not full week representation", - args: args{ - start: "2023-07-30T00:00:00Z", - end: "2023-09-01T01:12:00Z", - granularity: StepWeek, - timeReverse: true, - }, - want: []testTimeRange{ - { - "2023-08-27T00:00:00Z", - "2023-09-01T01:12:00Z", - }, - { - "2023-08-20T00:00:00Z", - "2023-08-27T00:00:00Z", - }, - { - "2023-08-13T00:00:00Z", - "2023-08-20T00:00:00Z", - }, - { - "2023-08-06T00:00:00Z", - "2023-08-13T00:00:00Z", - }, - { - "2023-07-30T00:00:00Z", - "2023-08-06T00:00:00Z", - }, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - start := mustParseDatetime(tt.args.start) - end := mustParseDatetime(tt.args.end) +func TestSplitDateRange_Success(t *testing.T) { + f := func(startStr, endStr, granularity string, resultExpected []testTimeRange) { + t.Helper() - got, err := SplitDateRange(start, end, tt.args.granularity, tt.args.timeReverse) - if (err != nil) != tt.wantErr { - t.Errorf("splitDateRange() error = %v, wantErr %v", err, tt.wantErr) - return - } + start := mustParseDatetime(startStr) + end := mustParseDatetime(endStr) - var testExpectedResults [][]time.Time - if tt.want != nil { - testExpectedResults = make([][]time.Time, 0) - for _, dr := range tt.want { - testExpectedResults = append(testExpectedResults, []time.Time{ - mustParseDatetime(dr[0]), - mustParseDatetime(dr[1]), - }) - } - } + result, err := SplitDateRange(start, end, granularity, false) + if err != nil { + t.Fatalf("SplitDateRange() error: %s", err) + } - if !reflect.DeepEqual(got, testExpectedResults) { - t.Errorf("splitDateRange() got = %v, want %v", got, testExpectedResults) - } - }) + var testExpectedResults [][]time.Time + for _, dr := range resultExpected { + testExpectedResults = append(testExpectedResults, []time.Time{ + mustParseDatetime(dr[0]), + mustParseDatetime(dr[1]), + }) + } + + if !reflect.DeepEqual(result, testExpectedResults) { + t.Fatalf("unexpected result\ngot\n%v\nwant\n%v", result, testExpectedResults) + } } + + // month chunking + f("2022-01-03T11:11:11Z", "2022-03-03T12:12:12Z", StepMonth, []testTimeRange{ + { + "2022-01-03T11:11:11Z", + "2022-01-31T23:59:59.999999999Z", + }, + { + "2022-02-01T00:00:00Z", + "2022-02-28T23:59:59.999999999Z", + }, + { + "2022-03-01T00:00:00Z", + "2022-03-03T12:12:12Z", + }, + }) + + // daily chunking + f("2022-01-03T11:11:11Z", "2022-01-05T12:12:12Z", StepDay, []testTimeRange{ + { + "2022-01-03T11:11:11Z", + "2022-01-04T11:11:11Z", + }, + { + "2022-01-04T11:11:11Z", + "2022-01-05T11:11:11Z", + }, + { + "2022-01-05T11:11:11Z", + "2022-01-05T12:12:12Z", + }, + }) + + // hourly chunking + f("2022-01-03T11:11:11Z", "2022-01-03T14:14:14Z", StepHour, []testTimeRange{ + { + "2022-01-03T11:11:11Z", + "2022-01-03T12:11:11Z", + }, + { + "2022-01-03T12:11:11Z", + "2022-01-03T13:11:11Z", + }, + { + "2022-01-03T13:11:11Z", + "2022-01-03T14:11:11Z", + }, + { + "2022-01-03T14:11:11Z", + "2022-01-03T14:14:14Z", + }, + }) + + // month chunking with one day time range + f("2022-01-03T11:11:11Z", "2022-01-04T12:12:12Z", StepMonth, []testTimeRange{ + { + "2022-01-03T11:11:11Z", + "2022-01-04T12:12:12Z", + }, + }) + + // month chunking with same day time range + f("2022-01-03T11:11:11Z", "2022-01-03T12:12:12Z", StepMonth, []testTimeRange{ + { + "2022-01-03T11:11:11Z", + "2022-01-03T12:12:12Z", + }, + }) + + // month chunking with one month and two days range + f("2022-01-03T11:11:11Z", "2022-02-03T00:00:00Z", StepMonth, []testTimeRange{ + { + "2022-01-03T11:11:11Z", + "2022-01-31T23:59:59.999999999Z", + }, + { + "2022-02-01T00:00:00Z", + "2022-02-03T00:00:00Z", + }, + }) + + // week chunking with not full week + f("2023-07-30T00:00:00Z", "2023-08-05T23:59:59.999999999Z", StepWeek, []testTimeRange{ + { + "2023-07-30T00:00:00Z", + "2023-08-05T23:59:59.999999999Z", + }, + }) + + // week chunking with start of the week and end of the week + f("2023-07-30T00:00:00Z", "2023-08-06T00:00:00Z", StepWeek, []testTimeRange{ + { + "2023-07-30T00:00:00Z", + "2023-08-06T00:00:00Z", + }, + }) + + // week chunking with next one day week + f("2023-07-30T00:00:00Z", "2023-08-07T01:12:00Z", StepWeek, []testTimeRange{ + { + "2023-07-30T00:00:00Z", + "2023-08-06T00:00:00Z", + }, + { + "2023-08-06T00:00:00Z", + "2023-08-07T01:12:00Z", + }, + }) + + // week chunking with month and not full week representation + f("2023-07-30T00:00:00Z", "2023-09-01T01:12:00Z", StepWeek, []testTimeRange{ + { + "2023-07-30T00:00:00Z", + "2023-08-06T00:00:00Z", + }, + { + "2023-08-06T00:00:00Z", + "2023-08-13T00:00:00Z", + }, + { + "2023-08-13T00:00:00Z", + "2023-08-20T00:00:00Z", + }, + { + "2023-08-20T00:00:00Z", + "2023-08-27T00:00:00Z", + }, + { + "2023-08-27T00:00:00Z", + "2023-09-01T01:12:00Z", + }, + }) +} + +func TestSplitDateRange_Reverse_Failure(t *testing.T) { + f := func(startStr, endStr, granularity string) { + t.Helper() + + start := mustParseDatetime(startStr) + end := mustParseDatetime(endStr) + + _, err := SplitDateRange(start, end, granularity, true) + if err == nil { + t.Fatalf("expecting non-nil error") + } + } + + // validates start is before end + f("2022-02-01T00:00:00Z", "2022-01-01T00:00:00Z", StepMonth) + + // validates granularity value + f("2022-01-01T00:00:00Z", "2022-02-01T00:00:00Z", "non-existent-format") +} + +func TestSplitDateRange_Reverse_Success(t *testing.T) { + f := func(startStr, endStr, granularity string, resultExpected []testTimeRange) { + t.Helper() + + start := mustParseDatetime(startStr) + end := mustParseDatetime(endStr) + + result, err := SplitDateRange(start, end, granularity, true) + if err != nil { + t.Fatalf("SplitDateRange() error: %s", err) + } + + var testExpectedResults [][]time.Time + for _, dr := range resultExpected { + testExpectedResults = append(testExpectedResults, []time.Time{ + mustParseDatetime(dr[0]), + mustParseDatetime(dr[1]), + }) + } + + if !reflect.DeepEqual(result, testExpectedResults) { + t.Fatalf("unexpected result\ngot\n%v\nwant\n%v", result, testExpectedResults) + } + } + + // month chunking + f("2022-01-03T11:11:11Z", "2022-03-03T12:12:12Z", StepMonth, []testTimeRange{ + { + "2022-03-01T00:00:00Z", + "2022-03-03T12:12:12Z", + }, + { + "2022-02-01T00:00:00Z", + "2022-02-28T23:59:59.999999999Z", + }, + { + "2022-01-03T11:11:11Z", + "2022-01-31T23:59:59.999999999Z", + }, + }) + + // daily chunking + f("2022-01-03T11:11:11Z", "2022-01-05T12:12:12Z", StepDay, []testTimeRange{ + { + "2022-01-05T11:11:11Z", + "2022-01-05T12:12:12Z", + }, + { + "2022-01-04T11:11:11Z", + "2022-01-05T11:11:11Z", + }, + { + "2022-01-03T11:11:11Z", + "2022-01-04T11:11:11Z", + }, + }) + + // hourly chunking + f("2022-01-03T11:11:11Z", "2022-01-03T14:14:14Z", StepHour, []testTimeRange{ + { + "2022-01-03T14:11:11Z", + "2022-01-03T14:14:14Z", + }, + { + "2022-01-03T13:11:11Z", + "2022-01-03T14:11:11Z", + }, + { + "2022-01-03T12:11:11Z", + "2022-01-03T13:11:11Z", + }, + { + "2022-01-03T11:11:11Z", + "2022-01-03T12:11:11Z", + }, + }) + + // month chunking with one day time range + f("2022-01-03T11:11:11Z", "2022-01-04T12:12:12Z", StepMonth, []testTimeRange{ + { + "2022-01-03T11:11:11Z", + "2022-01-04T12:12:12Z", + }, + }) + + // month chunking with same day time range + f("2022-01-03T11:11:11Z", "2022-01-03T12:12:12Z", StepMonth, []testTimeRange{ + { + "2022-01-03T11:11:11Z", + "2022-01-03T12:12:12Z", + }, + }) + + // month chunking with one month and two days range + f("2022-01-03T11:11:11Z", "2022-02-03T00:00:00Z", StepMonth, []testTimeRange{ + { + "2022-02-01T00:00:00Z", + "2022-02-03T00:00:00Z", + }, + { + "2022-01-03T11:11:11Z", + "2022-01-31T23:59:59.999999999Z", + }, + }) + + // week chunking with not full week + f("2023-07-30T00:00:00Z", "2023-08-05T23:59:59.999999999Z", StepWeek, []testTimeRange{ + { + "2023-07-30T00:00:00Z", + "2023-08-05T23:59:59.999999999Z", + }, + }) + + // week chunking with start of the week and end of the week + f("2023-07-30T00:00:00Z", "2023-08-06T00:00:00Z", StepWeek, []testTimeRange{ + { + "2023-07-30T00:00:00Z", + "2023-08-06T00:00:00Z", + }, + }) + + // week chunking with next one day week + f("2023-07-30T00:00:00Z", "2023-08-07T01:12:00Z", StepWeek, []testTimeRange{ + { + "2023-08-06T00:00:00Z", + "2023-08-07T01:12:00Z", + }, + { + "2023-07-30T00:00:00Z", + "2023-08-06T00:00:00Z", + }, + }) + + // week chunking with month and not full week representation + f("2023-07-30T00:00:00Z", "2023-09-01T01:12:00Z", StepWeek, []testTimeRange{ + { + "2023-08-27T00:00:00Z", + "2023-09-01T01:12:00Z", + }, + { + "2023-08-20T00:00:00Z", + "2023-08-27T00:00:00Z", + }, + { + "2023-08-13T00:00:00Z", + "2023-08-20T00:00:00Z", + }, + { + "2023-08-06T00:00:00Z", + "2023-08-13T00:00:00Z", + }, + { + "2023-07-30T00:00:00Z", + "2023-08-06T00:00:00Z", + }, + }) } diff --git a/app/vmctl/utils/time_test.go b/app/vmctl/utils/time_test.go index 95f5fbdf4..b469a5899 100644 --- a/app/vmctl/utils/time_test.go +++ b/app/vmctl/utils/time_test.go @@ -5,175 +5,87 @@ import ( "time" ) -func TestGetTime(t *testing.T) { - tests := []struct { - name string - s string - want func() time.Time - wantErr bool - }{ - { - name: "empty string", - s: "", - want: func() time.Time { return time.Time{} }, - wantErr: true, - }, - { - name: "only year", - s: "2019", - want: func() time.Time { - t := time.Date(2019, 1, 1, 0, 0, 0, 0, time.UTC) - return t - }, - }, - { - name: "year and month", - s: "2019-01", - want: func() time.Time { - t := time.Date(2019, 1, 1, 0, 0, 0, 0, time.UTC) - return t - }, - }, - { - name: "year and not first month", - s: "2019-02", - want: func() time.Time { - t := time.Date(2019, 2, 1, 0, 0, 0, 0, time.UTC) - return t - }, - }, - { - name: "year, month and day", - s: "2019-02-01", - want: func() time.Time { - t := time.Date(2019, 2, 1, 0, 0, 0, 0, time.UTC) - return t - }, - }, - { - name: "year, month and not first day", - s: "2019-02-10", - want: func() time.Time { - t := time.Date(2019, 2, 10, 0, 0, 0, 0, time.UTC) - return t - }, - }, - { - name: "year, month, day and time", - s: "2019-02-02T00", - want: func() time.Time { - t := time.Date(2019, 2, 2, 0, 0, 0, 0, time.UTC) - return t - }, - }, - { - name: "year, month, day and one hour time", - s: "2019-02-02T01", - want: func() time.Time { - t := time.Date(2019, 2, 2, 1, 0, 0, 0, time.UTC) - return t - }, - }, - { - name: "time with zero minutes", - s: "2019-02-02T01:00", - want: func() time.Time { - t := time.Date(2019, 2, 2, 1, 0, 0, 0, time.UTC) - return t - }, - }, - { - name: "time with one minute", - s: "2019-02-02T01:01", - want: func() time.Time { - t := time.Date(2019, 2, 2, 1, 1, 0, 0, time.UTC) - return t - }, - }, - { - name: "time with zero seconds", - s: "2019-02-02T01:01:00", - want: func() time.Time { - t := time.Date(2019, 2, 2, 1, 1, 0, 0, time.UTC) - return t - }, - }, - { - name: "timezone with one second", - s: "2019-02-02T01:01:01", - want: func() time.Time { - t := time.Date(2019, 2, 2, 1, 1, 1, 0, time.UTC) - return t - }, - }, - { - name: "time with two second and timezone", - s: "2019-07-07T20:01:02Z", - want: func() time.Time { - t := time.Date(2019, 7, 7, 20, 1, 02, 0, time.UTC) - return t - }, - }, - { - name: "time with seconds and timezone", - s: "2019-07-07T20:47:40+03:00", - want: func() time.Time { - l, _ := time.LoadLocation("Europe/Kiev") - t := time.Date(2019, 7, 7, 20, 47, 40, 0, l) - return t - }, - }, - { - name: "negative time", - s: "-292273086-05-16T16:47:06Z", - want: func() time.Time { return time.Time{} }, - wantErr: true, - }, - { - name: "float timestamp representation", - s: "1562529662.324", - want: func() time.Time { - t := time.Date(2019, 7, 7, 20, 01, 02, 324e6, time.UTC) - return t - }, - }, - { - name: "negative timestamp", - s: "-9223372036.855", - want: func() time.Time { - return time.Date(1970, 01, 01, 00, 00, 00, 00, time.UTC) - }, - wantErr: false, - }, - { - name: "big timestamp", - s: "1223372036855", - want: func() time.Time { - t := time.Date(2008, 10, 7, 9, 33, 56, 855e6, time.UTC) - return t - }, - wantErr: false, - }, - { - name: "duration time", - s: "1h5m", - want: func() time.Time { - t := time.Now().Add(-1 * time.Hour).Add(-5 * time.Minute) - return t - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := ParseTime(tt.s) - if (err != nil) != tt.wantErr { - t.Errorf("ParseTime() error = %v, wantErr %v", err, tt.wantErr) - return - } - w := tt.want() - if got.Unix() != w.Unix() { - t.Errorf("ParseTime() got = %v, want %v", got, w) - } - }) +func TestGetTime_Failure(t *testing.T) { + f := func(s string) { + t.Helper() + + _, err := ParseTime(s) + if err == nil { + t.Fatalf("expecting non-nil error") + } } + + // empty string + f("") + + // negative time + f("-292273086-05-16T16:47:06Z") +} + +func TestGetTime_Success(t *testing.T) { + f := func(s string, resultExpected time.Time) { + t.Helper() + + result, err := ParseTime(s) + if err != nil { + t.Fatalf("ParseTime() error: %s", err) + } + if result.Unix() != resultExpected.Unix() { + t.Fatalf("unexpected result; got %s; want %s", result, resultExpected) + } + } + + // only year + f("2019", time.Date(2019, 1, 1, 0, 0, 0, 0, time.UTC)) + + // year and month + f("2019-01", time.Date(2019, 1, 1, 0, 0, 0, 0, time.UTC)) + + // year and not first month + f("2019-02", time.Date(2019, 2, 1, 0, 0, 0, 0, time.UTC)) + + // year, month and day + f("2019-02-01", time.Date(2019, 2, 1, 0, 0, 0, 0, time.UTC)) + + // year, month and not first day + f("2019-02-10", time.Date(2019, 2, 10, 0, 0, 0, 0, time.UTC)) + + // year, month, day and time + f("2019-02-02T00", time.Date(2019, 2, 2, 0, 0, 0, 0, time.UTC)) + + // year, month, day and one hour time + f("2019-02-02T01", time.Date(2019, 2, 2, 1, 0, 0, 0, time.UTC)) + + // time with zero minutes + f("2019-02-02T01:00", time.Date(2019, 2, 2, 1, 0, 0, 0, time.UTC)) + + // time with one minute + f("2019-02-02T01:01", time.Date(2019, 2, 2, 1, 1, 0, 0, time.UTC)) + + // time with zero seconds + f("2019-02-02T01:01:00", time.Date(2019, 2, 2, 1, 1, 0, 0, time.UTC)) + + // timezone with one second + f("2019-02-02T01:01:01", time.Date(2019, 2, 2, 1, 1, 1, 0, time.UTC)) + + // time with two second and timezone + f("2019-07-07T20:01:02Z", time.Date(2019, 7, 7, 20, 1, 02, 0, time.UTC)) + + // time with seconds and timezone + f("2019-07-07T20:47:40+03:00", func() time.Time { + l, _ := time.LoadLocation("Europe/Kiev") + return time.Date(2019, 7, 7, 20, 47, 40, 0, l) + }()) + + // float timestamp representation", + f("1562529662.324", time.Date(2019, 7, 7, 20, 01, 02, 324e6, time.UTC)) + + // negative timestamp + f("-9223372036.855", time.Date(1970, 01, 01, 00, 00, 00, 00, time.UTC)) + + // big timestamp + f("1223372036855", time.Date(2008, 10, 7, 9, 33, 56, 855e6, time.UTC)) + + // duration time + f("1h5m", time.Now().Add(-1*time.Hour).Add(-5*time.Minute)) } diff --git a/app/vmctl/vm/timeseries_test.go b/app/vmctl/vm/timeseries_test.go index f020df96f..e983f76cc 100644 --- a/app/vmctl/vm/timeseries_test.go +++ b/app/vmctl/vm/timeseries_test.go @@ -7,83 +7,68 @@ import ( "testing" ) -func TestTimeSeries_Write(t *testing.T) { - var testCases = []struct { - name string - ts *TimeSeries - exp string - }{ - { - name: "one datapoint", - ts: &TimeSeries{ - Name: "foo", - LabelPairs: []LabelPair{ - { - Name: "key", - Value: "val", - }, - }, - Timestamps: []int64{1577877162200}, - Values: []float64{1}, - }, - exp: `{"metric":{"__name__":"foo","key":"val"},"timestamps":[1577877162200],"values":[1]}`, - }, - { - name: "multiple samples", - ts: &TimeSeries{ - Name: "foo", - LabelPairs: []LabelPair{ - { - Name: "key", - Value: "val", - }, - }, - Timestamps: []int64{1577877162200, 15778771622400, 15778771622600}, - Values: []float64{1, 1.6263, 32.123}, - }, - exp: `{"metric":{"__name__":"foo","key":"val"},"timestamps":[1577877162200,15778771622400,15778771622600],"values":[1,1.6263,32.123]}`, - }, - { - name: "no samples", - ts: &TimeSeries{ - Name: "foo", - LabelPairs: []LabelPair{ - { - Name: "key", - Value: "val", - }, - }, - }, - exp: ``, - }, - { - name: "inf values", - ts: &TimeSeries{ - Name: "foo", - LabelPairs: []LabelPair{ - { - Name: "key", - Value: "val", - }, - }, - Timestamps: []int64{1577877162200, 1577877162200, 1577877162200}, - Values: []float64{0, math.Inf(-1), math.Inf(1)}, - }, - exp: `{"metric":{"__name__":"foo","key":"val"},"timestamps":[1577877162200,1577877162200,1577877162200],"values":[0,-Inf,+Inf]}`, - }, +func TestTimeSeriesWrite(t *testing.T) { + f := func(ts *TimeSeries, resultExpected string) { + t.Helper() + + var b bytes.Buffer + _, err := ts.write(&b) + if err != nil { + t.Fatalf("error in TimeSeries.write: %s", err) + } + result := strings.TrimSpace(b.String()) + if result != resultExpected { + t.Fatalf("unexpected result\ngot\n%v\nwant\n%v", result, resultExpected) + } } - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - b := &bytes.Buffer{} - _, err := tc.ts.write(b) - if err != nil { - t.Error(err) - } - got := strings.TrimSpace(b.String()) - if got != tc.exp { - t.Fatalf("\ngot: %q\nwant: %q", got, tc.exp) - } - }) - } + // one datapoint + f(&TimeSeries{ + Name: "foo", + LabelPairs: []LabelPair{ + { + Name: "key", + Value: "val", + }, + }, + Timestamps: []int64{1577877162200}, + Values: []float64{1}, + }, `{"metric":{"__name__":"foo","key":"val"},"timestamps":[1577877162200],"values":[1]}`) + + // multiple samples + f(&TimeSeries{ + Name: "foo", + LabelPairs: []LabelPair{ + { + Name: "key", + Value: "val", + }, + }, + Timestamps: []int64{1577877162200, 15778771622400, 15778771622600}, + Values: []float64{1, 1.6263, 32.123}, + }, `{"metric":{"__name__":"foo","key":"val"},"timestamps":[1577877162200,15778771622400,15778771622600],"values":[1,1.6263,32.123]}`) + + // no samples + f(&TimeSeries{ + Name: "foo", + LabelPairs: []LabelPair{ + { + Name: "key", + Value: "val", + }, + }, + }, ``) + + // inf values + f(&TimeSeries{ + Name: "foo", + LabelPairs: []LabelPair{ + { + Name: "key", + Value: "val", + }, + }, + Timestamps: []int64{1577877162200, 1577877162200, 1577877162200}, + Values: []float64{0, math.Inf(-1), math.Inf(1)}, + }, `{"metric":{"__name__":"foo","key":"val"},"timestamps":[1577877162200,1577877162200,1577877162200],"values":[0,-Inf,+Inf]}`) } diff --git a/app/vmctl/vm/vm_test.go b/app/vmctl/vm/vm_test.go index 1d9d42523..3e9a1b85b 100644 --- a/app/vmctl/vm/vm_test.go +++ b/app/vmctl/vm/vm_test.go @@ -2,68 +2,42 @@ package vm import "testing" -func TestAddExtraLabelsToImportPath(t *testing.T) { - type args struct { - path string - extraLabels []string - } - tests := []struct { - name string - args args - want string - wantErr bool - }{ - { - name: "ok w/o extra labels", - args: args{ - path: "/api/v1/import", - }, - want: "/api/v1/import", - }, - { - name: "ok one extra label", - args: args{ - path: "/api/v1/import", - extraLabels: []string{"instance=host-1"}, - }, - want: "/api/v1/import?extra_label=instance=host-1", - }, - { - name: "ok two extra labels", - args: args{ - path: "/api/v1/import", - extraLabels: []string{"instance=host-2", "job=vmagent"}, - }, - want: "/api/v1/import?extra_label=instance=host-2&extra_label=job=vmagent", - }, - { - name: "ok two extra with exist param", - args: args{ - path: "/api/v1/import?timeout=50", - extraLabels: []string{"instance=host-2", "job=vmagent"}, - }, - want: "/api/v1/import?timeout=50&extra_label=instance=host-2&extra_label=job=vmagent", - }, - { - name: "bad incorrect format for extra label", - args: args{ - path: "/api/v1/import", - extraLabels: []string{"label=value", "bad_label_wo_value"}, - }, - want: "/api/v1/import", - wantErr: true, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := AddExtraLabelsToImportPath(tt.args.path, tt.args.extraLabels) - if (err != nil) != tt.wantErr { - t.Errorf("AddExtraLabelsToImportPath() error = %v, wantErr %v", err, tt.wantErr) - return - } - if got != tt.want { - t.Errorf("AddExtraLabelsToImportPath() got = %v, want %v", got, tt.want) - } - }) +func TestAddExtraLabelsToImportPath_Failure(t *testing.T) { + f := func(path string, extraLabels []string) { + t.Helper() + + _, err := AddExtraLabelsToImportPath(path, extraLabels) + if err == nil { + t.Fatalf("expecting non-nil error") + } } + + // bad incorrect format for extra label + f("/api/v1/import", []string{"label=value", "bad_label_wo_value"}) +} + +func TestAddExtraLabelsToImportPath_Success(t *testing.T) { + f := func(path string, extraLabels []string, resultExpected string) { + t.Helper() + + result, err := AddExtraLabelsToImportPath(path, extraLabels) + if err != nil { + t.Fatalf("AddExtraLabelsToImportPath() error: %s", err) + } + if result != resultExpected { + t.Fatalf("unexpected result; got %q; want %q", result, resultExpected) + } + } + + // ok w/o extra labels + f("/api/v1/import", nil, "/api/v1/import") + + // ok one extra label + f("/api/v1/import", []string{"instance=host-1"}, "/api/v1/import?extra_label=instance=host-1") + + // ok two extra labels + f("/api/v1/import", []string{"instance=host-2", "job=vmagent"}, "/api/v1/import?extra_label=instance=host-2&extra_label=job=vmagent") + + // ok two extra with exist param + f("/api/v1/import?timeout=50", []string{"instance=host-2", "job=vmagent"}, "/api/v1/import?timeout=50&extra_label=instance=host-2&extra_label=job=vmagent") } diff --git a/app/vmctl/vm_native_test.go b/app/vmctl/vm_native_test.go index 1f24dad56..6060d79c4 100644 --- a/app/vmctl/vm_native_test.go +++ b/app/vmctl/vm_native_test.go @@ -13,7 +13,6 @@ import ( "github.com/VictoriaMetrics/VictoriaMetrics/app/vmctl/backoff" "github.com/VictoriaMetrics/VictoriaMetrics/app/vmctl/native" - "github.com/VictoriaMetrics/VictoriaMetrics/app/vmctl/stepper" remote_read_integration "github.com/VictoriaMetrics/VictoriaMetrics/app/vmctl/testdata/servers_integration_test" "github.com/VictoriaMetrics/VictoriaMetrics/app/vmctl/vm" "github.com/VictoriaMetrics/VictoriaMetrics/app/vmselect/promql" @@ -27,7 +26,82 @@ const ( retentionPeriod = "100y" ) -func Test_vmNativeProcessor_run(t *testing.T) { +func TestVMNativeProcessorRun(t *testing.T) { + f := func(startStr, endStr string, numOfSeries, numOfSamples int, resultExpected []vm.TimeSeries) { + t.Helper() + + src := remote_read_integration.NewRemoteWriteServer(t) + dst := remote_read_integration.NewRemoteWriteServer(t) + + defer func() { + src.Close() + dst.Close() + }() + + start, err := time.Parse(time.RFC3339, startStr) + if err != nil { + t.Fatalf("cannot parse start time: %s", err) + } + + end, err := time.Parse(time.RFC3339, endStr) + if err != nil { + t.Fatalf("cannot parse end time: %s", err) + } + + matchName := "__name__" + matchValue := ".*" + filter := native.Filter{ + Match: fmt.Sprintf("{%s=~%q}", matchName, matchValue), + TimeStart: startStr, + TimeEnd: endStr, + } + + rws := remote_read_integration.GenerateVNSeries(start.Unix(), end.Unix(), int64(numOfSeries), int64(numOfSamples)) + + src.Series(rws) + dst.ExpectedSeries(resultExpected) + + if err := fillStorage(rws); err != nil { + t.Fatalf("cannot add series to storage: %s", err) + } + + srcClient := &native.Client{ + AuthCfg: nil, + Addr: src.URL(), + ExtraLabels: []string{}, + HTTPClient: &http.Client{Transport: &http.Transport{DisableKeepAlives: false}}, + } + dstClient := &native.Client{ + AuthCfg: nil, + Addr: dst.URL(), + ExtraLabels: []string{}, + HTTPClient: &http.Client{Transport: &http.Transport{DisableKeepAlives: false}}, + } + + isSilent = true + defer func() { isSilent = false }() + + p := &vmNativeProcessor{ + filter: filter, + dst: dstClient, + src: srcClient, + backoff: backoff.New(), + cc: 1, + isNative: true, + } + + ctx := context.Background() + if err := p.run(ctx); err != nil { + t.Fatalf("run() error: %s", err) + } + deleted, err := deleteSeries(matchName, matchValue) + if err != nil { + t.Fatalf("cannot delete series: %s", err) + } + if deleted != numOfSeries { + t.Fatalf("unexpected number of deleted series; got %d; want %d", deleted, numOfSeries) + } + } processFlags() vmstorage.Init(promql.ResetRollupResultCacheIfNeeded) @@ -42,214 +116,78 @@ func Test_vmNativeProcessor_run(t *testing.T) { defer func() { barpool.Disable(false) }() - defer func() { isSilent = false }() - type fields struct { - filter native.Filter - dst *native.Client - src *native.Client - backoff *backoff.Backoff - s *stats - rateLimit int64 - interCluster bool - cc int - matchName string - matchValue string - } - type args struct { - ctx context.Context - silent bool - } - - tests := []struct { - name string - fields fields - args args - vmSeries func(start, end, numOfSeries, numOfSamples int64) []vm.TimeSeries - expectedSeries []vm.TimeSeries - start string - end string - numOfSamples int64 - numOfSeries int64 - chunk string - wantErr bool - }{ + // step minute on minute time range + start := "2022-11-25T11:23:05+02:00" + end := "2022-11-27T11:24:05+02:00" + numOfSeries := 3 + numOfSamples := 2 + resultExpected := []vm.TimeSeries{ { - name: "step minute on minute time range", - start: "2022-11-25T11:23:05+02:00", - end: "2022-11-27T11:24:05+02:00", - numOfSamples: 2, - numOfSeries: 3, - chunk: stepper.StepMinute, - fields: fields{ - filter: native.Filter{}, - backoff: backoff.New(), - rateLimit: 0, - interCluster: false, - cc: 1, - matchName: "__name__", - matchValue: ".*", - }, - args: args{ - ctx: context.Background(), - silent: true, - }, - vmSeries: remote_read_integration.GenerateVNSeries, - expectedSeries: []vm.TimeSeries{ - { - Name: "vm_metric_1", - LabelPairs: []vm.LabelPair{{Name: "job", Value: "0"}}, - Timestamps: []int64{1669368185000, 1669454615000}, - Values: []float64{0, 0}, - }, - { - Name: "vm_metric_1", - LabelPairs: []vm.LabelPair{{Name: "job", Value: "1"}}, - Timestamps: []int64{1669368185000, 1669454615000}, - Values: []float64{100, 100}, - }, - { - Name: "vm_metric_1", - LabelPairs: []vm.LabelPair{{Name: "job", Value: "2"}}, - Timestamps: []int64{1669368185000, 1669454615000}, - Values: []float64{200, 200}, - }, - }, - wantErr: false, + Name: "vm_metric_1", + LabelPairs: []vm.LabelPair{{Name: "job", Value: "0"}}, + Timestamps: []int64{1669368185000, 1669454615000}, + Values: []float64{0, 0}, }, { - name: "step month on month time range", - start: "2022-09-26T11:23:05+02:00", - end: "2022-11-26T11:24:05+02:00", - numOfSamples: 2, - numOfSeries: 3, - chunk: stepper.StepMonth, - fields: fields{ - filter: native.Filter{}, - backoff: backoff.New(), - rateLimit: 0, - interCluster: false, - cc: 1, - matchName: "__name__", - matchValue: ".*", - }, - args: args{ - ctx: context.Background(), - silent: true, - }, - vmSeries: remote_read_integration.GenerateVNSeries, - expectedSeries: []vm.TimeSeries{ - { - Name: "vm_metric_1", - LabelPairs: []vm.LabelPair{{Name: "job", Value: "0"}}, - Timestamps: []int64{1664184185000}, - Values: []float64{0}, - }, - { - Name: "vm_metric_1", - LabelPairs: []vm.LabelPair{{Name: "job", Value: "0"}}, - Timestamps: []int64{1666819415000}, - Values: []float64{0}, - }, - { - Name: "vm_metric_1", - LabelPairs: []vm.LabelPair{{Name: "job", Value: "1"}}, - Timestamps: []int64{1664184185000}, - Values: []float64{100}, - }, - { - Name: "vm_metric_1", - LabelPairs: []vm.LabelPair{{Name: "job", Value: "1"}}, - Timestamps: []int64{1666819415000}, - Values: []float64{100}, - }, - { - Name: "vm_metric_1", - LabelPairs: []vm.LabelPair{{Name: "job", Value: "2"}}, - Timestamps: []int64{1664184185000}, - Values: []float64{200}, - }, - { - Name: "vm_metric_1", - LabelPairs: []vm.LabelPair{{Name: "job", Value: "2"}}, - Timestamps: []int64{1666819415000}, - Values: []float64{200}, - }, - }, - wantErr: false, + Name: "vm_metric_1", + LabelPairs: []vm.LabelPair{{Name: "job", Value: "1"}}, + Timestamps: []int64{1669368185000, 1669454615000}, + Values: []float64{100, 100}, + }, + { + Name: "vm_metric_1", + LabelPairs: []vm.LabelPair{{Name: "job", Value: "2"}}, + Timestamps: []int64{1669368185000, 1669454615000}, + Values: []float64{200, 200}, }, } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - src := remote_read_integration.NewRemoteWriteServer(t) - dst := remote_read_integration.NewRemoteWriteServer(t) + f(start, end, numOfSeries, numOfSamples, resultExpected) - defer func() { - src.Close() - dst.Close() - }() - - start, err := time.Parse(time.RFC3339, tt.start) - if err != nil { - t.Fatalf("Error parse start time: %s", err) - } - - end, err := time.Parse(time.RFC3339, tt.end) - if err != nil { - t.Fatalf("Error parse end time: %s", err) - } - - tt.fields.filter.Match = fmt.Sprintf("{%s=~%q}", tt.fields.matchName, tt.fields.matchValue) - tt.fields.filter.TimeStart = tt.start - tt.fields.filter.TimeEnd = tt.end - - rws := tt.vmSeries(start.Unix(), end.Unix(), tt.numOfSeries, tt.numOfSamples) - - src.Series(rws) - dst.ExpectedSeries(tt.expectedSeries) - - if err := fillStorage(rws); err != nil { - t.Fatalf("error add series to storage: %s", err) - } - - tt.fields.src = &native.Client{ - AuthCfg: nil, - Addr: src.URL(), - ExtraLabels: []string{}, - HTTPClient: &http.Client{Transport: &http.Transport{DisableKeepAlives: false}}, - } - tt.fields.dst = &native.Client{ - AuthCfg: nil, - Addr: dst.URL(), - ExtraLabels: []string{}, - HTTPClient: &http.Client{Transport: &http.Transport{DisableKeepAlives: false}}, - } - - isSilent = tt.args.silent - p := &vmNativeProcessor{ - filter: tt.fields.filter, - dst: tt.fields.dst, - src: tt.fields.src, - backoff: tt.fields.backoff, - s: tt.fields.s, - rateLimit: tt.fields.rateLimit, - interCluster: tt.fields.interCluster, - cc: tt.fields.cc, - isNative: true, - } - - if err := p.run(tt.args.ctx); (err != nil) != tt.wantErr { - t.Errorf("run() error = %v, wantErr %v", err, tt.wantErr) - } - deleted, err := deleteSeries(tt.fields.matchName, tt.fields.matchValue) - if err != nil { - t.Fatalf("error delete series: %s", err) - } - if int64(deleted) != tt.numOfSeries { - t.Fatalf("expected deleted series %d; got deleted series %d", tt.numOfSeries, deleted) - } - }) + // step month on month time range + start = "2022-09-26T11:23:05+02:00" + end = "2022-11-26T11:24:05+02:00" + numOfSeries = 3 + numOfSamples = 2 + resultExpected = []vm.TimeSeries{ + { + Name: "vm_metric_1", + LabelPairs: []vm.LabelPair{{Name: "job", Value: "0"}}, + Timestamps: []int64{1664184185000}, + Values: []float64{0}, + }, + { + Name: "vm_metric_1", + LabelPairs: []vm.LabelPair{{Name: "job", Value: "0"}}, + Timestamps: []int64{1666819415000}, + Values: []float64{0}, + }, + { + Name: "vm_metric_1", + LabelPairs: []vm.LabelPair{{Name: "job", Value: "1"}}, + Timestamps: []int64{1664184185000}, + Values: []float64{100}, + }, + { + Name: "vm_metric_1", + LabelPairs: []vm.LabelPair{{Name: "job", Value: "1"}}, + Timestamps: []int64{1666819415000}, + Values: []float64{100}, + }, + { + Name: "vm_metric_1", + LabelPairs: []vm.LabelPair{{Name: "job", Value: "2"}}, + Timestamps: []int64{1664184185000}, + Values: []float64{200}, + }, + { + Name: "vm_metric_1", + LabelPairs: []vm.LabelPair{{Name: "job", Value: "2"}}, + Timestamps: []int64{1666819415000}, + Values: []float64{200}, + }, } + f(start, end, numOfSeries, numOfSamples, resultExpected) } func processFlags() { @@ -311,95 +249,57 @@ func deleteSeries(name, value string) (int, error) { return vmstorage.DeleteSeries(nil, []*storage.TagFilters{tfs}) } -func Test_buildMatchWithFilter(t *testing.T) { - tests := []struct { - name string - filter string - metricName string - want string - wantErr bool - }{ - { - name: "parsed metric with label", - filter: `{__name__="http_request_count_total",cluster="kube1"}`, - metricName: "http_request_count_total", - want: `{cluster="kube1",__name__="http_request_count_total"}`, - wantErr: false, - }, - { - name: "metric name with label", - filter: `http_request_count_total{cluster="kube1"}`, - metricName: "http_request_count_total", - want: `{cluster="kube1",__name__="http_request_count_total"}`, - wantErr: false, - }, - { - name: "parsed metric with regexp value", - filter: `{__name__="http_request_count_total",cluster=~"kube.*"}`, - metricName: "http_request_count_total", - want: `{cluster=~"kube.*",__name__="http_request_count_total"}`, - wantErr: false, - }, - { - name: "only label with regexp", - filter: `{cluster=~".*"}`, - metricName: "http_request_count_total", - want: `{cluster=~".*",__name__="http_request_count_total"}`, - wantErr: false, - }, - { - name: "many labels in filter with regexp", - filter: `{cluster=~".*",job!=""}`, - metricName: "http_request_count_total", - want: `{cluster=~".*",job!="",__name__="http_request_count_total"}`, - wantErr: false, - }, - { - name: "match with error", - filter: `{cluster~=".*"}`, - metricName: "http_request_count_total", - want: ``, - wantErr: true, - }, - { - name: "all names", - filter: `{__name__!=""}`, - metricName: "http_request_count_total", - want: `{__name__="http_request_count_total"}`, - wantErr: false, - }, - { - name: "with many underscores labels", - filter: `{__name__!="", __meta__!=""}`, - metricName: "http_request_count_total", - want: `{__meta__!="",__name__="http_request_count_total"}`, - wantErr: false, - }, - { - name: "metric name has regexp", - filter: `{__name__=~".*"}`, - metricName: "http_request_count_total", - want: `{__name__="http_request_count_total"}`, - wantErr: false, - }, - { - name: "metric name has negative regexp", - filter: `{__name__!~".*"}`, - metricName: "http_request_count_total", - want: `{__name__="http_request_count_total"}`, - wantErr: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := buildMatchWithFilter(tt.filter, tt.metricName) - if (err != nil) != tt.wantErr { - t.Errorf("buildMatchWithFilter() error = %v, wantErr %v", err, tt.wantErr) - return - } - if got != tt.want { - t.Errorf("buildMatchWithFilter() got = %v, want %v", got, tt.want) - } - }) +func TestBuildMatchWithFilter_Failure(t *testing.T) { + f := func(filter, metricName string) { + t.Helper() + + _, err := buildMatchWithFilter(filter, metricName) + if err == nil { + t.Fatalf("expecting non-nil error") + } } + + // match with error + f(`{cluster~=".*"}`, "http_request_count_total") +} + +func TestBuildMatchWithFilter_Success(t *testing.T) { + f := func(filter, metricName, resultExpected string) { + t.Helper() + + result, err := buildMatchWithFilter(filter, metricName) + if err != nil { + t.Fatalf("buildMatchWithFilter() error: %s", err) + } + if result != resultExpected { + t.Fatalf("unexpected result\ngot\n%s\nwant\n%s", result, resultExpected) + } + } + + // parsed metric with label + f(`{__name__="http_request_count_total",cluster="kube1"}`, "http_request_count_total", `{cluster="kube1",__name__="http_request_count_total"}`) + + // metric name with label + f(`http_request_count_total{cluster="kube1"}`, "http_request_count_total", `{cluster="kube1",__name__="http_request_count_total"}`) + + // parsed metric with regexp value + f(`{__name__="http_request_count_total",cluster=~"kube.*"}`, "http_request_count_total", `{cluster=~"kube.*",__name__="http_request_count_total"}`) + + // only label with regexp + f(`{cluster=~".*"}`, "http_request_count_total", `{cluster=~".*",__name__="http_request_count_total"}`) + + // many labels in filter with regexp + f(`{cluster=~".*",job!=""}`, "http_request_count_total", `{cluster=~".*",job!="",__name__="http_request_count_total"}`) + + // all names + f(`{__name__!=""}`, "http_request_count_total", `{__name__="http_request_count_total"}`) + + // with many underscores labels + f(`{__name__!="", __meta__!=""}`, "http_request_count_total", `{__meta__!="",__name__="http_request_count_total"}`) + + // metric name has regexp + f(`{__name__=~".*"}`, "http_request_count_total", `{__name__="http_request_count_total"}`) + + // metric name has negative regexp + f(`{__name__!~".*"}`, "http_request_count_total", `{__name__="http_request_count_total"}`) }