Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: date generator functions #3327

Merged
merged 7 commits into from
Nov 3, 2023
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
28 changes: 17 additions & 11 deletions docs/docs/cli/creating-tests.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -169,17 +169,23 @@ Generator functions can be invoked as part of expressions. Therefore, you only n
Available functions:

| Function | Description |
| :-------------------- | ------------------------------------------------------------------------------------- |
| `uuid()` | Generates a random v4 uuid. |
| `firstName()` | Generates a random English first name. |
| `lastName()` | Generates a random English last name. |
| `fullName()` | Generates a random English first and last name. |
| `email()` | Generates a random email address. |
| `phone()` | Generates a random phone number. |
| `creditCard()` | Generates a random credit card number (from 12 to 19 digits). |
| `creditCardCvv()` | Generates a random credit card cvv (3 digits). |
| `creditCardExpDate()` | Generates a random credit card expiration date (mm/yy). |
| `randomInt(min, max)` | Generates a random integer contained in the closed interval defined by [`min`, `max`]. |
| :-------------------- | --------------------------------------------------------------------------------------------- |
| `uuid()` | Generates a random v4 uuid. |
| `firstName()` | Generates a random English first name. |
| `lastName()` | Generates a random English last name. |
| `fullName()` | Generates a random English first and last name. |
| `email()` | Generates a random email address. |
| `phone()` | Generates a random phone number. |
| `creditCard()` | Generates a random credit card number (from 12 to 19 digits). |
| `creditCardCvv()` | Generates a random credit card cvv (3 digits). |
| `creditCardExpDate()` | Generates a random credit card expiration date (mm/yy). |
| `randomInt(min, max)` | Generates a random integer contained in the closed interval defined by [`min`, `max`]. |
| `date(format?)` | Get the current date and formats it. Default is `YYYY-MM-DD` but you can specify other formats|
mathnogueira marked this conversation as resolved.
Show resolved Hide resolved
| `dateTime(format?)` | Get the current datetime and formats it. Default is RFC3339 but you can specify other formats |
mathnogueira marked this conversation as resolved.
Show resolved Hide resolved

:::tip
[Continue reading about date and datetime formats, here.](https://www.w3.org/TR/NOTE-datetime)
mathnogueira marked this conversation as resolved.
Show resolved Hide resolved
:::

:::tip
[Continue reading about Test Specs, here.](/cli/creating-test-specifications)
mathnogueira marked this conversation as resolved.
Show resolved Hide resolved
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,7 @@ require (
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
gitlab.com/metakeule/fmtdate v1.2.2 // indirect
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This library uses the time.Format function to format dates but it allows us to use the W3C format instead of go's magic numbers:

DD/MM/YYYY instead of 02/01/2006

go.opentelemetry.io/collector v0.80.0 // indirect
go.opentelemetry.io/collector/config/configauth v0.80.0 // indirect
go.opentelemetry.io/collector/config/confignet v0.80.0 // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -1848,6 +1848,8 @@ github.com/yvasiyarov/go-metrics v0.0.0-20140926110328-57bccd1ccd43/go.mod h1:aX
github.com/yvasiyarov/gorelic v0.0.0-20141212073537-a9bba5b9ab50/go.mod h1:NUSPSUX/bi6SeDMUh6brw0nXpxHnc96TguQh0+r/ssA=
github.com/yvasiyarov/newrelic_platform_go v0.0.0-20140908184405-b21fdbd4370f/go.mod h1:GlGEuHIJweS1mbCqG+7vt2nvWLzLLnRHbXz5JKd/Qbg=
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
gitlab.com/metakeule/fmtdate v1.2.2 h1:ce0Qnwo6PAONi6xwPr4YxdxAFIKqNfoMbHG4c49vIjk=
gitlab.com/metakeule/fmtdate v1.2.2/go.mod h1:uZUf21xepWGLp6PgJGBbHeBVWO+/gsKi3Gdh0Fu4lGg=
gitlab.com/nyarla/go-crypt v0.0.0-20160106005555-d9a5dc2b789b/go.mod h1:T3BPAOm2cqquPa0MKWeNkmOM5RQsRhkrwMWonFMN7fE=
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
Expand Down
21 changes: 21 additions & 0 deletions server/expression/executor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"fmt"
"strings"
"testing"
"time"

"github.com/kubeshop/tracetest/server/expression"
"github.com/kubeshop/tracetest/server/traces"
Expand Down Expand Up @@ -244,6 +245,26 @@ func TestFunctionExecution(t *testing.T) {
Query: `randomInt(10,20) < 10`,
ShouldPass: false,
},
{
Name: "should_generate_date_string",
Query: fmt.Sprintf(`date() = "%s"`, time.Now().Format(time.DateOnly)),
ShouldPass: true,
},
{
Name: "should_generate_date_string",
Query: fmt.Sprintf(`date("DD/MM/YYYY") = "%s"`, time.Now().Format("02/01/2006")),
ShouldPass: true,
},
{
Name: "should_generate_date_string",
Query: fmt.Sprintf(`dateTime() = "%s"`, time.Now().Format(time.RFC3339)),
ShouldPass: true,
},
{
Name: "should_generate_date_string",
Query: fmt.Sprintf(`dateTime("DD/MM/YYYY hh:mm") = "%s"`, time.Now().Format("02/01/2006 15:04")),
ShouldPass: true,
},
}

executeTestCases(t, testCases)
Expand Down
82 changes: 63 additions & 19 deletions server/expression/functions/function.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,55 @@ import (
type Invoker func(args ...types.TypedValue) string

type Function struct {
name string
invoker Invoker
argTypes []types.Type
name string
invoker Invoker
parameters []Parameter
}

func (f Function) NumRequiredParams() int {
count := 0
for _, param := range f.parameters {
if !param.optional {
count++
}
}

return count
}

func (f Function) Validate() error {
foundOptionalParameterIndex := -1
for i, param := range f.parameters {
if param.optional {
foundOptionalParameterIndex = i
}

if foundOptionalParameterIndex > -1 && !param.optional {
// there's a required parameter after a optional parameter
// this is invalid in most programming languages and makes it
// extremely hard to resolve the function, so fail it.
return fmt.Errorf("argument at index %d is required, but it's after optional argument at index %d", i, foundOptionalParameterIndex)
}
}

return nil
}

type Parameter struct {
pType types.Type
optional bool
}

func (f Function) Invoke(args ...types.TypedValue) (types.TypedValue, error) {
if len(args) != len(f.argTypes) {
return types.TypedValue{}, fmt.Errorf("invalid number of arguments. Expected %d, got %d", len(f.argTypes), len(args))
numberRequiredParams := f.NumRequiredParams()
if len(args) < numberRequiredParams {
return types.TypedValue{}, fmt.Errorf("missing required parameters. Expected at least %d params, got %d", numberRequiredParams, len(args))
}

for i, arg := range args {
expectedArgType := f.argTypes[i]
if arg.Type != expectedArgType {
return types.TypedValue{}, fmt.Errorf("invalid argument type on index %d: expected %s, got %s", i, arg.Type.String(), expectedArgType.String())
param := f.parameters[i]
if arg.Type != param.pType {
return types.TypedValue{}, fmt.Errorf("invalid argument type on index %d: expected %s, got %s", i, arg.Type.String(), param.pType.String())
}
}

Expand All @@ -31,19 +66,28 @@ func (f Function) Invoke(args ...types.TypedValue) (types.TypedValue, error) {
}

func DefaultRegistry() Registry {
emptyArgsConfig := []types.Type{}
registry := newRegistry()

registry.Add("uuid", generateUUID, emptyArgsConfig)
registry.Add("firstName", generateFirstName, emptyArgsConfig)
registry.Add("lastName", generateLastName, emptyArgsConfig)
registry.Add("fullName", generateFullName, emptyArgsConfig)
registry.Add("email", generateEmail, emptyArgsConfig)
registry.Add("phone", generatePhoneNumber, emptyArgsConfig)
registry.Add("creditCard", generateCreditCard, emptyArgsConfig)
registry.Add("creditCardCvv", generateCreditCardCVV, emptyArgsConfig)
registry.Add("creditCardExpDate", generateCreditCardExpiration, emptyArgsConfig)
registry.Add("randomInt", generateRandomInt, []types.Type{types.TypeNumber, types.TypeNumber})
registry.Add("uuid", generateUUID)
registry.Add("firstName", generateFirstName)
registry.Add("lastName", generateLastName)
registry.Add("fullName", generateFullName)
registry.Add("email", generateEmail)
registry.Add("phone", generatePhoneNumber)
registry.Add("date", generateDate, OptionalParam(types.TypeString))
registry.Add("dateTime", generateDateTime, OptionalParam(types.TypeString))
registry.Add("creditCard", generateCreditCard)
registry.Add("creditCardCvv", generateCreditCardCVV)
registry.Add("creditCardExpDate", generateCreditCardExpiration)
registry.Add("randomInt", generateRandomInt, Param(types.TypeNumber), Param(types.TypeNumber))

return registry
}

func Param(pType types.Type) Parameter {
return Parameter{pType: pType, optional: false}
}

func OptionalParam(pType types.Type) Parameter {
return Parameter{pType: pType, optional: true}
}
21 changes: 13 additions & 8 deletions server/expression/functions/function_registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,10 @@ package functions
import (
"fmt"
"sync"

"github.com/kubeshop/tracetest/server/expression/types"
)

type Registry interface {
Add(string, Invoker, []types.Type)
Add(string, Invoker, ...Parameter)
Get(string) (Function, error)
}

Expand All @@ -23,15 +21,22 @@ type registry struct {
mutex sync.Mutex
}

func (r *registry) Add(name string, function Invoker, argsConfig []types.Type) {
func (r *registry) Add(name string, function Invoker, argsConfig ...Parameter) {
r.mutex.Lock()
defer r.mutex.Unlock()

r.functions[name] = Function{
name: name,
invoker: function,
argTypes: argsConfig,
f := Function{
name: name,
invoker: function,
parameters: argsConfig,
}

if err := f.Validate(); err != nil {
// this is a development error. Fail it as fast as possible
panic(err)
}

r.functions[name] = f
}

func (r *registry) Get(name string) (Function, error) {
Expand Down
16 changes: 15 additions & 1 deletion server/expression/functions/function_registry_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,20 @@ import (
"github.com/stretchr/testify/require"
)

func TestFunctionValidation(t *testing.T) {
registry := functions.DefaultRegistry()

emptyStringFn := func(args ...types.TypedValue) string { return "" }

assert.Panics(t, func() {
registry.Add("faulty", emptyStringFn,
functions.Param(types.TypeString),
functions.OptionalParam(types.TypeString),
functions.Param(types.TypeNumber),
)
})
}

func TestFunctionWithoutArgs(t *testing.T) {
registry := functions.DefaultRegistry()

Expand Down Expand Up @@ -55,7 +69,7 @@ func TestFunctionWithWrongArgNumber(t *testing.T) {

_, err = function.Invoke()
assert.Error(t, err)
assert.Contains(t, err.Error(), "invalid number of arguments")
assert.Contains(t, err.Error(), "missing required parameters")
}

func TestFunctionWithWrongArgType(t *testing.T) {
Expand Down
19 changes: 19 additions & 0 deletions server/expression/functions/functions.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ package functions
import (
"fmt"
"strconv"
"time"

"github.com/brianvoe/gofakeit/v6"
"github.com/kubeshop/tracetest/server/expression/types"
"gitlab.com/metakeule/fmtdate"
)

func generateUUID(args ...types.TypedValue) string {
Expand Down Expand Up @@ -50,3 +52,20 @@ func generateRandomInt(args ...types.TypedValue) string {
max, _ := strconv.Atoi(args[1].Value)
return fmt.Sprintf("%d", gofakeit.Number(min, max))
}

func generateDate(args ...types.TypedValue) string {
format := time.DateOnly
if len(args) > 0 {
format = args[0].Value
}

return fmtdate.Format(format, time.Now())
}

func generateDateTime(args ...types.TypedValue) string {
format := time.RFC3339
if len(args) > 0 {
format = args[0].Value
}
return fmtdate.Format(format, time.Now())
}