2018-07-26 20:27:35 +00:00
package options_test
import (
"fmt"
2018-08-16 15:37:26 +00:00
"math"
2019-03-19 16:53:54 +00:00
"strings"
2018-07-26 20:27:35 +00:00
"testing"
"time"
"github.com/google/go-cmp/cmp"
2020-04-03 17:39:20 +00:00
"github.com/influxdata/influxdb/v2/pkg/pointer"
_ "github.com/influxdata/influxdb/v2/query/builtin"
"github.com/influxdata/influxdb/v2/task/options"
2018-07-26 20:27:35 +00:00
)
func scriptGenerator ( opt options . Options , body string ) string {
taskData := ""
if opt . Name != "" {
taskData = fmt . Sprintf ( "%s name: %q,\n" , taskData , opt . Name )
}
if opt . Cron != "" {
taskData = fmt . Sprintf ( "%s cron: %q,\n" , taskData , opt . Cron )
}
2019-03-27 20:24:53 +00:00
if ! opt . Every . IsZero ( ) {
2018-07-26 20:27:35 +00:00
taskData = fmt . Sprintf ( "%s every: %s,\n" , taskData , opt . Every . String ( ) )
}
2019-03-27 20:24:53 +00:00
if opt . Offset != nil && ! ( * opt . Offset ) . IsZero ( ) {
2018-12-06 15:57:25 +00:00
taskData = fmt . Sprintf ( "%s offset: %s,\n" , taskData , opt . Offset . String ( ) )
2018-07-26 20:27:35 +00:00
}
2019-03-01 20:09:31 +00:00
if opt . Concurrency != nil && * opt . Concurrency != 0 {
taskData = fmt . Sprintf ( "%s concurrency: %d,\n" , taskData , * opt . Concurrency )
2018-07-26 20:27:35 +00:00
}
2019-03-01 20:09:31 +00:00
if opt . Retry != nil && * opt . Retry != 0 {
taskData = fmt . Sprintf ( "%s retry: %d,\n" , taskData , * opt . Retry )
2018-07-26 20:27:35 +00:00
}
if body == "" {
2018-08-22 15:53:11 +00:00
body = ` from ( bucket : "test" )
2018-07-26 20:27:35 +00:00
| > range ( start : - 1 h ) `
}
return fmt . Sprintf ( ` option task = {
% s
}
% s ` , taskData , body )
}
2019-03-27 20:24:53 +00:00
func TestNegDurations ( t * testing . T ) {
dur := options . MustParseDuration ( "-1m" )
d , err := dur . DurationFrom ( time . Now ( ) )
if err != nil {
t . Fatal ( err )
}
if d != - time . Minute {
t . Fatalf ( "expected duration to be -1m but was %s" , d )
}
}
2018-07-26 20:27:35 +00:00
func TestFromScript ( t * testing . T ) {
for _ , c := range [ ] struct {
script string
exp options . Options
shouldErr bool
} {
2019-03-27 20:24:53 +00:00
{ script : scriptGenerator ( options . Options { Name : "name0" , Cron : "* * * * *" , Concurrency : pointer . Int64 ( 2 ) , Retry : pointer . Int64 ( 3 ) , Offset : options . MustParseDuration ( "-1m" ) } , "" ) ,
exp : options . Options { Name : "name0" ,
Cron : "* * * * *" ,
Concurrency : pointer . Int64 ( 2 ) ,
Retry : pointer . Int64 ( 3 ) ,
Offset : options . MustParseDuration ( "-1m" ) } } ,
{ script : scriptGenerator ( options . Options { Name : "name1" , Every : * ( options . MustParseDuration ( "5s" ) ) } , "" ) , exp : options . Options { Name : "name1" , Every : * ( options . MustParseDuration ( "5s" ) ) , Concurrency : pointer . Int64 ( 1 ) , Retry : pointer . Int64 ( 1 ) } } ,
2019-03-01 20:09:31 +00:00
{ script : scriptGenerator ( options . Options { Name : "name2" , Cron : "* * * * *" } , "" ) , exp : options . Options { Name : "name2" , Cron : "* * * * *" , Concurrency : pointer . Int64 ( 1 ) , Retry : pointer . Int64 ( 1 ) } } ,
2019-03-27 20:24:53 +00:00
{ script : scriptGenerator ( options . Options { Name : "name3" , Every : * ( options . MustParseDuration ( "1h" ) ) , Cron : "* * * * *" } , "" ) , shouldErr : true } ,
{ script : scriptGenerator ( options . Options { Name : "name4" , Concurrency : pointer . Int64 ( 1000 ) , Every : * ( options . MustParseDuration ( "1h" ) ) } , "" ) , shouldErr : true } ,
2019-03-01 20:09:31 +00:00
{ script : "option task = {\n name: \"name5\",\n concurrency: 0,\n every: 1m0s,\n\n}\n\nfrom(bucket: \"test\")\n |> range(start:-1h)" , shouldErr : true } ,
{ script : "option task = {\n name: \"name6\",\n concurrency: 1,\n every: 1,\n\n}\n\nfrom(bucket: \"test\")\n |> range(start:-1h)" , shouldErr : true } ,
2019-03-27 20:24:53 +00:00
{ script : scriptGenerator ( options . Options { Name : "name7" , Retry : pointer . Int64 ( 20 ) , Every : * ( options . MustParseDuration ( "1h" ) ) } , "" ) , shouldErr : true } ,
2019-03-01 20:09:31 +00:00
{ script : "option task = {\n name: \"name8\",\n retry: 0,\n every: 1m0s,\n\n}\n\nfrom(bucket: \"test\")\n |> range(start:-1h)" , shouldErr : true } ,
{ script : scriptGenerator ( options . Options { Name : "name9" } , "" ) , shouldErr : true } ,
2018-07-26 20:27:35 +00:00
{ script : scriptGenerator ( options . Options { } , "" ) , shouldErr : true } ,
2019-09-17 21:44:00 +00:00
{ script : ` option task = {
2020-01-06 21:19:55 +00:00
name : "name10" ,
2019-09-17 21:44:00 +00:00
every : 1 d ,
2020-01-06 21:19:55 +00:00
offset : 1 m ,
2019-09-17 21:44:00 +00:00
}
from ( bucket : "metrics" )
| > range ( start : now ( ) , stop : 8 w )
2020-01-06 21:19:55 +00:00
` ,
exp : options . Options { Name : "name10" , Every : * ( options . MustParseDuration ( "1d" ) ) , Concurrency : pointer . Int64 ( 1 ) , Retry : pointer . Int64 ( 1 ) , Offset : options . MustParseDuration ( "1m" ) } ,
} ,
2019-09-17 21:44:00 +00:00
{ script : ` option task = {
2020-01-06 21:19:55 +00:00
name : "name11" ,
2019-09-17 21:44:00 +00:00
every : 1 m ,
2020-01-06 21:19:55 +00:00
offset : 1 d ,
2019-09-17 21:44:00 +00:00
}
from ( bucket : "metrics" )
| > range ( start : now ( ) , stop : 8 w )
2020-01-06 21:19:55 +00:00
` ,
exp : options . Options { Name : "name11" , Every : * ( options . MustParseDuration ( "1m" ) ) , Concurrency : pointer . Int64 ( 1 ) , Retry : pointer . Int64 ( 1 ) , Offset : options . MustParseDuration ( "1d" ) } ,
} ,
2019-09-17 21:44:00 +00:00
{ script : "option task = {name:\"test_task_smoke_name\", every:30s} from(bucket:\"test_tasks_smoke_bucket_source\") |> range(start: -1h) |> map(fn: (r) => ({r with _time: r._time, _value:r._value, t : \"quality_rocks\"}))|> to(bucket:\"test_tasks_smoke_bucket_dest\", orgID:\"3e73e749495d37d5\")" ,
exp : options . Options { Name : "test_task_smoke_name" , Every : * ( options . MustParseDuration ( "30s" ) ) , Retry : pointer . Int64 ( 1 ) , Concurrency : pointer . Int64 ( 1 ) } , shouldErr : false } , // TODO(docmerlin): remove this once tasks fully supports all flux duration units.
2018-07-26 20:27:35 +00:00
} {
o , err := options . FromScript ( c . script )
if c . shouldErr && err == nil {
t . Fatalf ( "script %q should have errored but didn't" , c . script )
} else if ! c . shouldErr && err != nil {
t . Fatalf ( "script %q should not have errored, but got %v" , c . script , err )
}
if err != nil {
continue
}
if ! cmp . Equal ( o , c . exp ) {
t . Fatalf ( "script %q got unexpected result -got/+exp\n%s" , c . script , cmp . Diff ( o , c . exp ) )
}
}
}
2018-08-15 20:51:25 +00:00
2019-07-09 17:46:13 +00:00
func BenchmarkFromScriptFunc ( b * testing . B ) {
for n := 0 ; n < b . N ; n ++ {
_ , err := options . FromScript ( ` option task = { every: 20s, name: "foo"} from(bucket:"x") |> range(start:-1h) ` )
if err != nil {
fmt . Printf ( "error: %v" , err )
}
}
}
2019-03-19 16:53:54 +00:00
func TestFromScriptWithUnknownOptions ( t * testing . T ) {
const optPrefix = ` option task = { name: "x", every: 1m `
const bodySuffix = ` } from(bucket:"b") |> range(start:-1m) `
// Script without unknown option should be good.
if _ , err := options . FromScript ( optPrefix + bodySuffix ) ; err != nil {
t . Fatal ( err )
}
_ , err := options . FromScript ( optPrefix + ` , Offset: 2s, foo: "bar" ` + bodySuffix )
if err == nil {
t . Fatal ( "expected error from unknown option but got nil" )
}
msg := err . Error ( )
if ! strings . Contains ( msg , "Offset" ) || ! strings . Contains ( msg , "foo" ) {
t . Errorf ( "expected error to mention unrecognized options, but it said: %v" , err )
}
validOpts := [ ] string { "name" , "cron" , "every" , "offset" , "concurrency" , "retry" }
for _ , o := range validOpts {
if ! strings . Contains ( msg , o ) {
t . Errorf ( "expected error to mention valid option %q but it said: %v" , o , err )
}
}
}
2018-08-16 15:37:26 +00:00
func TestValidate ( t * testing . T ) {
2019-03-01 20:09:31 +00:00
good := options . Options { Name : "x" , Cron : "* * * * *" , Concurrency : pointer . Int64 ( 1 ) , Retry : pointer . Int64 ( 1 ) }
2018-08-16 15:37:26 +00:00
if err := good . Validate ( ) ; err != nil {
t . Fatal ( err )
}
bad := new ( options . Options )
* bad = good
bad . Name = ""
if err := bad . Validate ( ) ; err == nil {
t . Error ( "expected error for options without name" )
}
* bad = good
bad . Cron = ""
if err := bad . Validate ( ) ; err == nil {
t . Error ( "expected error for options without cron or every" )
}
* bad = good
2019-03-27 20:24:53 +00:00
bad . Every = * options . MustParseDuration ( "1m" )
2018-08-16 15:37:26 +00:00
if err := bad . Validate ( ) ; err == nil {
t . Error ( "expected error for options with both cron and every" )
}
* bad = good
bad . Cron = "not a cron string"
if err := bad . Validate ( ) ; err == nil {
t . Error ( "expected error for options with invalid cron" )
}
* bad = good
bad . Cron = ""
2019-03-27 20:24:53 +00:00
bad . Every = * options . MustParseDuration ( "-1m" )
2018-08-16 15:37:26 +00:00
if err := bad . Validate ( ) ; err == nil {
t . Error ( "expected error for negative every" )
}
* bad = good
2019-03-27 20:24:53 +00:00
bad . Offset = options . MustParseDuration ( "1500ms" )
2018-08-16 15:37:26 +00:00
if err := bad . Validate ( ) ; err == nil {
t . Error ( "expected error for sub-second delay resolution" )
}
* bad = good
2019-03-01 20:09:31 +00:00
bad . Concurrency = pointer . Int64 ( 0 )
2018-08-16 15:37:26 +00:00
if err := bad . Validate ( ) ; err == nil {
t . Error ( "expected error for 0 concurrency" )
}
* bad = good
2019-03-01 20:09:31 +00:00
bad . Concurrency = pointer . Int64 ( math . MaxInt64 )
2018-08-16 15:37:26 +00:00
if err := bad . Validate ( ) ; err == nil {
t . Error ( "expected error for concurrency too large" )
}
* bad = good
2019-03-01 20:09:31 +00:00
bad . Retry = pointer . Int64 ( 0 )
2018-08-16 15:37:26 +00:00
if err := bad . Validate ( ) ; err == nil {
t . Error ( "expected error for 0 retry" )
}
* bad = good
2019-03-01 20:09:31 +00:00
bad . Retry = pointer . Int64 ( math . MaxInt64 )
2018-08-16 15:37:26 +00:00
if err := bad . Validate ( ) ; err == nil {
t . Error ( "expected error for retry too large" )
}
2020-01-06 21:19:55 +00:00
notbad := new ( options . Options )
* notbad = good
notbad . Cron = ""
notbad . Every = * options . MustParseDuration ( "22d" )
if err := notbad . Validate ( ) ; err != nil {
t . Error ( "expected no error for days every" )
}
2018-08-16 15:37:26 +00:00
}
2018-08-15 20:51:25 +00:00
func TestEffectiveCronString ( t * testing . T ) {
for _ , c := range [ ] struct {
c string
2019-03-27 20:24:53 +00:00
e options . Duration
2018-08-15 20:51:25 +00:00
exp string
} {
{ c : "10 * * * *" , exp : "10 * * * *" } ,
2019-03-27 20:24:53 +00:00
{ e : * ( options . MustParseDuration ( "10s" ) ) , exp : "@every 10s" } ,
2018-08-15 20:51:25 +00:00
{ exp : "" } ,
2020-01-06 21:19:55 +00:00
{ e : * ( options . MustParseDuration ( "10d" ) ) , exp : "@every 10d" } ,
2018-08-15 20:51:25 +00:00
} {
o := options . Options { Cron : c . c , Every : c . e }
got := o . EffectiveCronString ( )
if got != c . exp {
t . Fatalf ( "exp cron string %q, got %q for %v" , c . exp , got , o )
}
}
}
2019-03-27 20:24:53 +00:00
func TestDurationMarshaling ( t * testing . T ) {
t . Run ( "unmarshaling" , func ( t * testing . T ) {
2020-01-06 21:19:55 +00:00
now := time . Now ( ) . UTC ( ) /* to guarantee 24 hour days*/
2019-03-27 20:24:53 +00:00
dur1 := options . Duration { }
2020-01-06 21:19:55 +00:00
if err := dur1 . UnmarshalText ( [ ] byte ( "1d1h10m3s" ) ) ; err != nil {
2019-03-27 20:24:53 +00:00
t . Fatal ( err )
}
d1 , err1 := dur1 . DurationFrom ( now )
if err1 != nil {
t . Fatal ( err1 )
}
dur2 := options . Duration { }
2020-01-06 21:19:55 +00:00
if err := dur2 . Parse ( "1d1h10m3s" ) ; err != nil {
2019-03-27 20:24:53 +00:00
t . Fatal ( err )
}
d2 , err2 := dur2 . DurationFrom ( now )
if err2 != nil {
t . Fatal ( err2 )
}
2020-01-06 21:19:55 +00:00
if d1 != d2 || d1 != 25 * time . Hour + 10 * time . Minute + 3 * time . Second /* we know that this day is 24 hours long because its UTC and go ignores leap seconds*/ {
2019-03-27 20:24:53 +00:00
t . Fatal ( "Parse and Marshaling do not give us the same result" )
}
} )
t . Run ( "marshaling" , func ( t * testing . T ) {
dur := options . Duration { }
if err := dur . UnmarshalText ( [ ] byte ( "1h10m3s" ) ) ; err != nil {
t . Fatal ( err )
}
if dur . String ( ) != "1h10m3s" {
t . Fatalf ( "duration string should be \"1h10m3s\" but was %s" , dur . String ( ) )
}
text , err := dur . MarshalText ( )
if err != nil {
t . Fatal ( err )
}
if string ( text ) != "1h10m3s" {
t . Fatalf ( "duration text should be \"1h10m3s\" but was %s" , text )
}
} )
t . Run ( "parse zero" , func ( t * testing . T ) {
dur := options . Duration { }
if err := dur . UnmarshalText ( [ ] byte ( "0h0s" ) ) ; err != nil {
t . Fatal ( err )
}
if ! dur . IsZero ( ) {
t . Fatalf ( "expected duration \"0s\" to be zero but was %s" , dur . String ( ) )
}
} )
}
func TestDurationMath ( t * testing . T ) {
dur := options . MustParseDuration ( "10s" )
d , err := dur . DurationFrom ( time . Now ( ) )
if err != nil {
t . Fatal ( err )
}
if d != 10 * time . Second {
t . Fatalf ( "expected duration to be 10s but it was %s" , d )
}
}