blob: 7cb66df599e551913accc0bd14dedc59f8a84a64 [file] [log] [blame]
/*
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
*/
package gremlingo
import (
"context"
"encoding/json"
"errors"
"fmt"
"github.com/apache/tinkerpop/gremlin-go/v3/driver"
"github.com/cucumber/godog"
"reflect"
"regexp"
"strconv"
"strings"
"sync"
"testing"
)
type tinkerPopGraph struct {
*CucumberWorld
sync.Mutex
}
var parsers map[*regexp.Regexp]func(string, string) interface{}
var toListLock sync.Mutex
func init() {
parsers = map[*regexp.Regexp]func(string, string) interface{}{
regexp.MustCompile(`^d\[(.*)]\.[lfdm]$`): toNumeric,
regexp.MustCompile(`^d\[(.*)]\.[i]$`): toInt32,
regexp.MustCompile(`^v\[(.+)]$`): toVertex,
regexp.MustCompile(`^v\[(.+)]\.id$`): toVertexId,
regexp.MustCompile(`^e\[(.+)]$`): toEdge,
regexp.MustCompile(`^v\[(.+)]\.sid$`): toVertexIdString,
regexp.MustCompile(`^e\[(.+)]\.id$`): toEdgeId,
regexp.MustCompile(`^e\[(.+)]\.sid$`): toEdgeIdString,
regexp.MustCompile(`^p\[(.+)]$`): toPath,
regexp.MustCompile(`^l\[(.*)]$`): toList,
regexp.MustCompile(`^s\[(.*)]$`): toSet,
regexp.MustCompile(`^m\[(.+)]$`): toMap,
regexp.MustCompile(`^c\[(.+)]$`): toLambda,
regexp.MustCompile(`^t\[(.+)]$`): toT,
regexp.MustCompile(`^D\[(.+)]$`): toDirection,
}
}
func parseValue(value string, graphName string) interface{} {
if regexp.MustCompile(`^null$`).MatchString(value) {
return nil
}
var extractedValue string
var parser func(string, string) interface{}
for key, element := range parsers {
var match = key.FindAllStringSubmatch(value, -1)
if len(match) > 0 {
parser = element
extractedValue = match[0][1]
break
}
}
if parser == nil {
return value
} else {
return parser(extractedValue, graphName)
}
}
// Parse numeric.
func toNumeric(stringVal, graphName string) interface{} {
if strings.Contains(stringVal, ".") {
val, err := strconv.ParseFloat(stringVal, 64)
if err != nil {
return nil
}
return val
}
val, err := strconv.ParseInt(stringVal, 10, 64)
if err != nil {
return nil
}
return val
}
// Parse int32.
func toInt32(stringVal, graphName string) interface{} {
val, err := strconv.ParseInt(stringVal, 10, 32)
if err != nil {
return nil
}
return int32(val)
}
// Parse vertex.
func toVertex(name, graphName string) interface{} {
return tg.getDataGraphFromMap(graphName).vertices[name]
}
// Parse vertex id.
func toVertexId(name, graphName string) interface{} {
if tg.getDataGraphFromMap(graphName).vertices[name] == nil {
return nil
}
return tg.getDataGraphFromMap(graphName).vertices[name].Id
}
// Parse vertex id as string.
func toVertexIdString(name, graphName string) interface{} {
if tg.getDataGraphFromMap(graphName).vertices[name] == nil {
return nil
}
return fmt.Sprint(tg.getDataGraphFromMap(graphName).vertices[name].Id)
}
// Parse edge.
func toEdge(name, graphName string) interface{} {
return tg.getDataGraphFromMap(graphName).edges[name]
}
// Parse edge id.
func toEdgeId(name, graphName string) interface{} {
if tg.getDataGraphFromMap(graphName).edges[name] == nil {
return nil
}
return tg.getDataGraphFromMap(graphName).edges[name].Id
}
// Parse edge id as string.
func toEdgeIdString(name, graphName string) interface{} {
if tg.getDataGraphFromMap(graphName).edges[name] == nil {
return nil
}
return fmt.Sprint(tg.getDataGraphFromMap(graphName).edges[name].Id)
}
// Parse path.
func toPath(stringObjects, graphName string) interface{} {
objects := make([]interface{}, 0)
for _, str := range strings.Split(stringObjects, ",") {
objects = append(objects, parseValue(str, graphName))
}
return &gremlingo.Path{
Labels: []gremlingo.Set{},
Objects: objects,
}
}
// Parse list.
func toList(stringList, graphName string) interface{} {
listVal := make([]interface{}, 0)
if len(stringList) == 0 {
return listVal
}
for _, str := range strings.Split(stringList, ",") {
listVal = append(listVal, parseValue(str, graphName))
}
return listVal
}
// Parse set to simple set.
func toSet(stringSet, graphName string) interface{} {
setVal := gremlingo.NewSimpleSet()
if len(stringSet) == 0 {
return setVal
}
for _, str := range strings.Split(stringSet, ",") {
setVal.Add(parseValue(str, graphName))
}
return setVal
}
// Parse json as a map.
func toMap(name, graphName string) interface{} {
var jsonMap interface{}
err := json.Unmarshal([]byte(name), &jsonMap)
if err != nil {
return nil
}
return parseMapValue(jsonMap, graphName)
}
func parseMapValue(mapVal interface{}, graphName string) interface{} {
if mapVal == nil {
return nil
}
switch reflect.TypeOf(mapVal).Kind() {
case reflect.String:
return parseValue(mapVal.(string), graphName)
case reflect.Float64, reflect.Int64:
return mapVal
case reflect.Array, reflect.Slice:
var valSlice []interface{}
oriSlice := reflect.ValueOf(mapVal)
for i := 0; i < oriSlice.Len(); i++ {
valSlice = append(valSlice, parseMapValue(oriSlice.Index(i).Interface(), graphName))
}
return valSlice
case reflect.Map:
valMap := make(map[interface{}]interface{})
v := reflect.ValueOf(mapVal)
keys := v.MapKeys()
for _, k := range keys {
convKey := k.Convert(v.Type().Key())
val := v.MapIndex(convKey)
keyVal := parseMapValue(k.Interface(), graphName)
if reflect.ValueOf(keyVal).Kind() == reflect.Slice {
// Turning map keys of slice type into string type for comparison purposes
// string slices should also be converted into slices more easily
valMap[fmt.Sprint(keyVal)] = parseMapValue(val.Interface(), graphName)
} else {
valMap[keyVal] = parseMapValue(val.Interface(), graphName)
}
}
return valMap
default:
// Not supported types.
return nil
}
}
// Parse lambda.
func toLambda(name, graphName string) interface{} {
return &gremlingo.Lambda{Script: name}
}
func toT(name, graphName string) interface{} {
// Return as is, since t values are just strings.
return name
}
func toDirection(name, graphName string) interface{} {
// Return as is, since direction values are just strings.
return name
}
func (tg *tinkerPopGraph) anUnsupportedTest() error {
return nil
}
func (tg *tinkerPopGraph) iteratedNext() error {
if tg.traversal == nil {
// Return pending because this is not currently implemented.
return godog.ErrPending
}
result, err := tg.traversal.Next()
if err != nil {
return err
}
var nextResults []interface{}
switch result.GetType().Kind() {
case reflect.Array, reflect.Slice:
resSlice := reflect.ValueOf(result.GetInterface())
for i := 0; i < resSlice.Len(); i++ {
nextResults = append(nextResults, resSlice.Index(i).Interface())
}
default:
simpleSet, ok := result.GetInterface().(*gremlingo.SimpleSet)
if ok {
nextResults = simpleSet.ToSlice()
} else {
nextResults = append(nextResults, result)
}
}
tg.result = nextResults
return nil
}
func (tg *tinkerPopGraph) iteratedToList() error {
if tg.traversal == nil {
// Return pending because this is not currently implemented.
return godog.ErrPending
}
results, err := tg.traversal.ToList()
if err != nil {
return err
}
var listResults []interface{}
for _, res := range results {
listResults = append(listResults, res)
}
tg.result = listResults
return nil
}
func (tg *tinkerPopGraph) nothingShouldHappenBecause(arg1 *godog.DocString) error {
return nil
}
// Choose the graph.
func (tg *tinkerPopGraph) chooseGraph(graphName string) error {
tg.graphName = graphName
data := tg.graphDataMap[graphName]
tg.g = gremlingo.Traversal_().WithRemote(data.connection)
if graphName == "empty" {
err := tg.cleanEmptyDataGraph(tg.g)
if err != nil {
return err
}
}
return nil
}
func (tg *tinkerPopGraph) theGraphInitializerOf(arg1 *godog.DocString) error {
traversal, err := GetTraversal(tg.scenario.Name, tg.g, tg.parameters)
if err != nil {
return err
}
future := traversal.Iterate()
return <-future
}
func (tg *tinkerPopGraph) theResultShouldHaveACountOf(expectedCount int) error {
actualCount := len(tg.result)
if actualCount != expectedCount {
if actualCount == 1 {
switch reflect.TypeOf(tg.result).Kind() {
case reflect.Slice, reflect.Array:
result := tg.result[0].(*gremlingo.Result).GetInterface()
switch reflect.TypeOf(result).Kind() {
case reflect.Map:
actualCount = len(result.(map[interface{}]interface{}))
}
}
if actualCount != expectedCount {
return fmt.Errorf("result should return %d for count, but returned %d", expectedCount, actualCount)
}
} else {
return fmt.Errorf("result should return %d for count, but returned %d", expectedCount, actualCount)
}
}
return nil
}
func (tg *tinkerPopGraph) theGraphShouldReturnForCountOf(expectedCount int, traversalText string) error {
traversal, err := GetTraversal(tg.scenario.Name, tg.g, tg.parameters)
if err != nil {
return err
}
results, err := traversal.ToList()
if err != nil {
return err
}
if len(results) != expectedCount {
return fmt.Errorf("graph returned count of %d when %d was expected", len(results), expectedCount)
}
return nil
}
func (tg *tinkerPopGraph) theResultShouldBeEmpty() error {
if len(tg.result) != 0 {
return errors.New("actual result is not empty as expected")
}
return nil
}
func (tg *tinkerPopGraph) theResultShouldBe(characterizedAs string, table *godog.Table) error {
ordered := characterizedAs == "ordered"
// For comparing ordered gremlingo.SimpleSet case.
var expectSet bool
var expectPath bool
switch characterizedAs {
case "ordered", "unordered", "of":
var expectedResult []interface{}
for idx, row := range table.Rows {
if idx == 0 {
// Skip the header line.
continue
}
val := parseValue(row.Cells[0].Value, tg.graphName)
v, ok := val.(*gremlingo.Path)
expectPath = ok
if ok {
// Clear the labels since we don't define them in feature files.
v.Labels = []gremlingo.Set{}
val = v
}
_, expectSet = val.(*gremlingo.SimpleSet)
expectedResult = append(expectedResult, val)
}
var actualResult []interface{}
if len(tg.result) == 1 {
switch r := tg.result[0].(type) {
case *gremlingo.Result:
val, ok := r.GetInterface().(*gremlingo.Path)
if !expectPath && ok {
actualResult = val.Objects
} else {
actualResult = append(actualResult, r.GetInterface())
}
default:
actualResult = append(actualResult, r)
}
} else {
for _, res := range tg.result {
switch r := res.(type) {
case *gremlingo.Result:
actualResult = append(actualResult, r.GetInterface())
default:
actualResult = append(actualResult, r)
}
}
}
if characterizedAs != "of" && (len(actualResult) != len(expectedResult)) {
err := fmt.Sprintf("actual result length does not equal expected (%d!=%d).", len(actualResult), len(expectedResult))
return errors.New(err)
}
if ordered {
if expectSet {
for i, a := range actualResult {
if fmt.Sprint(a.(*gremlingo.SimpleSet).ToSlice()) != fmt.Sprint(expectedResult[i].(*gremlingo.SimpleSet).ToSlice()) {
return fmt.Errorf("actual result does not match expected (order expected)\nActual: %v\nExpected: %v", actualResult, expectedResult)
}
}
} else if len(actualResult) == 1 && len(expectedResult) == 1 && reflect.TypeOf(actualResult[0]).Kind() == reflect.Map &&
reflect.TypeOf(expectedResult[0]).Kind() == reflect.Map {
if !compareMapEquals(actualResult[0].(map[interface{}]interface{}), expectedResult[0].(map[interface{}]interface{})) {
return fmt.Errorf("actual result does not match expected (order expected)\nActual: %v\nExpected: %v", actualResult, expectedResult)
}
} else if fmt.Sprint(actualResult) != fmt.Sprint(expectedResult) {
return fmt.Errorf("actual result does not match expected (order expected)\nActual: %v\nExpected: %v", actualResult, expectedResult)
}
} else {
if characterizedAs == "of" {
if !compareListEqualsWithOf(expectedResult, actualResult) {
return fmt.Errorf("actual result does not match expected (order not expected)\nActual: %v\nExpected: %v", actualResult, expectedResult)
}
} else {
if !compareListEqualsWithoutOrder(expectedResult, actualResult) {
return fmt.Errorf("actual result does not match expected (order not expected)\nActual: %v\nExpected: %v", actualResult, expectedResult)
}
}
}
return nil
default:
return errors.New("scenario not supported")
}
}
func compareMapEquals(expected map[interface{}]interface{}, actual map[interface{}]interface{}) bool {
for k, a := range actual {
var e interface{}
containsKey := false
for ke, ee := range expected {
if fmt.Sprint(k) == fmt.Sprint(ke) {
containsKey = true
e = ee
break
} else {
if reflect.ValueOf(k).Kind() == reflect.Ptr &&
reflect.ValueOf(ke).Kind() == reflect.Ptr {
switch k.(type) {
case *gremlingo.Vertex:
switch ke.(type) {
case *gremlingo.Vertex:
if fmt.Sprint(*k.(*gremlingo.Vertex)) == fmt.Sprint(*ke.(*gremlingo.Vertex)) {
containsKey = true
}
default:
// Not equal.
}
default:
// If we are here we probably need to implement an additional type like the Vertex above.
if fmt.Sprint(*k.(*interface{})) == fmt.Sprint(*ke.(*interface{})) {
fmt.Println("WARNING: Encountered unknown pointer type as map key.")
containsKey = true
}
}
if containsKey {
e = ee
break
}
}
}
}
if !containsKey {
fmt.Printf("Map comparison error: Failed to find key %s in %v\n", k, expected)
return false
}
if a == nil && e == nil {
continue
} else if a == nil || e == nil {
// One value is nil, other is not. They are not equal.
fmt.Printf("Map comparison error: One map has a nil key, other does not.\n")
return false
} else {
switch reflect.TypeOf(a).Kind() {
case reflect.Array, reflect.Slice:
switch reflect.TypeOf(e).Kind() {
case reflect.Array, reflect.Slice:
// Compare arrays
if !compareListEqualsWithoutOrder(e.([]interface{}), a.([]interface{})) {
return false
}
default:
fmt.Printf("Map comparison error: Expected type is Array/Slice, actual is %s.\n", reflect.TypeOf(a).Kind())
return false
}
case reflect.Map:
switch reflect.TypeOf(a).Kind() {
case reflect.Map:
// Compare maps
if !compareMapEquals(e.(map[interface{}]interface{}), a.(map[interface{}]interface{})) {
return false
}
default:
fmt.Printf("Map comparison error: Expected type is Map, actual is %s.\n", reflect.TypeOf(a).Kind())
return false
}
default:
if fmt.Sprint(a) != fmt.Sprint(e) {
fmt.Printf("Map comparison error: Expected != Actual (%s!=%s)\n", fmt.Sprint(a), fmt.Sprint(e))
return false
}
}
}
}
return true
}
func compareListEqualsWithoutOrder(expected []interface{}, actual []interface{}) bool {
// This is a little weird, but there isn't a good solution to either of these problems:
// 1. Comparison of types in Go. No deep equals which actually works properly. Needs to be done manually.
// 2. In place deletion in a loop.
// So to do in place deletion in a loop we can do the following:
// 1. Loop from back to wrong (don't need to worry about deleted indices that way.
// 2. Create a new slice with the index removed when we fix the item we want to delete.
// To do an orderless copy, a copy of the expected result is created. Results are removed as they are found. This stops
// the following from returning equal [1 2 2 2] and [1 1 1 2]
expectedCopy := make([]interface{}, len(expected))
copy(expectedCopy, expected)
for _, a := range actual {
found := false
if a == nil {
for i := len(expectedCopy) - 1; i >= 0; i-- {
if expectedCopy[i] == nil {
expectedCopy = append(expectedCopy[:i], expectedCopy[i+1:]...)
found = true
break
}
}
} else {
switch reflect.TypeOf(a).Kind() {
case reflect.Array, reflect.Slice:
for i := len(expectedCopy) - 1; i >= 0; i-- {
if expectedCopy[i] != nil {
switch reflect.TypeOf(expectedCopy[i]).Kind() {
case reflect.Array, reflect.Slice:
if compareListEqualsWithoutOrder(expectedCopy[i].([]interface{}), a.([]interface{})) {
expectedCopy = append(expectedCopy[:i], expectedCopy[i+1:]...)
found = true
}
}
if found {
break
}
}
}
case reflect.Map:
for i := len(expectedCopy) - 1; i >= 0; i-- {
if expectedCopy[i] != nil {
switch reflect.TypeOf(expectedCopy[i]).Kind() {
case reflect.Map:
if compareMapEquals(expectedCopy[i].(map[interface{}]interface{}), a.(map[interface{}]interface{})) {
expectedCopy = append(expectedCopy[:i], expectedCopy[i+1:]...)
found = true
}
}
if found {
break
}
}
}
default:
for i := len(expectedCopy) - 1; i >= 0; i-- {
if fmt.Sprint(a) == fmt.Sprint(expectedCopy[i]) {
expectedCopy = append(expectedCopy[:i], expectedCopy[i+1:]...)
found = true
break
}
}
}
}
if !found {
fmt.Printf("Failed to find %v in %v\n", a, expected)
return false
}
}
return true
}
func compareListEqualsWithOf(expected []interface{}, actual []interface{}) bool {
// When comparing with "of", we expect cases like [1 2] (expected) and [1 1 1 2] (actual) , or
// [1 1 1 2] (expected) and [1 2] (actual) to return equal.
for _, a := range actual {
found := false
if a == nil {
for i := len(expected) - 1; i >= 0; i-- {
if expected[i] == nil {
found = true
break
}
}
} else {
switch reflect.TypeOf(a).Kind() {
case reflect.Array, reflect.Slice:
for i := len(expected) - 1; i >= 0; i-- {
if expected[i] != nil {
switch reflect.TypeOf(expected[i]).Kind() {
case reflect.Array, reflect.Slice:
if compareListEqualsWithoutOrder(expected[i].([]interface{}), a.([]interface{})) {
found = true
}
}
if found {
break
}
}
}
case reflect.Map:
for i := len(expected) - 1; i >= 0; i-- {
if expected[i] != nil {
switch reflect.TypeOf(expected[i]).Kind() {
case reflect.Map:
if compareMapEquals(expected[i].(map[interface{}]interface{}), a.(map[interface{}]interface{})) {
found = true
}
}
if found {
break
}
}
}
default:
for i := len(expected) - 1; i >= 0; i-- {
if fmt.Sprint(a) == fmt.Sprint(expected[i]) {
found = true
break
}
}
}
}
if !found {
fmt.Printf("Failed to find %v in %v\n", a, expected)
return false
}
}
return true
}
func (tg *tinkerPopGraph) theTraversalOf(arg1 *godog.DocString) error {
traversal, err := GetTraversal(tg.scenario.Name, tg.g, tg.parameters)
if err != nil {
return err
}
tg.traversal = traversal
return nil
}
func (tg *tinkerPopGraph) usingTheParameterDefined(name string, params string) error {
if tg.graphName == "empty" {
tg.reloadEmptyData()
}
tg.parameters[name] = parseValue(strings.Replace(params, "\\\"", "\"", -1), tg.graphName)
return nil
}
func (tg *tinkerPopGraph) usingTheParameterOfP(paramName, pVal, stringVal string) error {
predicate := reflect.ValueOf(gremlingo.P).MethodByName(strings.Title(pVal)).Interface().(func(...interface{}) gremlingo.Predicate)
values := parseValue(stringVal, tg.graphName)
switch reflect.TypeOf(values).Kind() {
case reflect.Array, reflect.Slice:
tg.parameters[paramName] = predicate(values.([]interface{})...)
default:
tg.parameters[paramName] = predicate(values)
}
return nil
}
var tg = &tinkerPopGraph{
NewCucumberWorld(),
sync.Mutex{},
}
func InitializeTestSuite(ctx *godog.TestSuiteContext) {
ctx.BeforeSuite(func() {
tg.loadAllDataGraph()
})
ctx.AfterSuite(func() {
err := tg.closeAllDataGraphConnection()
if err != nil {
return
}
})
}
func InitializeScenario(ctx *godog.ScenarioContext) {
ctx.Before(func(ctx context.Context, sc *godog.Scenario) (context.Context, error) {
tg.scenario = sc
// Add tg.recreateAllDataGraphConnection() here and tg.closeAllDataGraphConnection() in an After scenario
// hook if necessary to isolate failing tests that closes the shared connection.
tg.Lock()
return ctx, nil
})
ctx.After(func(ctx context.Context, sc *godog.Scenario, err error) (context.Context, error) {
tg.Unlock()
return ctx, nil
})
ctx.Step(`^an unsupported test$`, tg.anUnsupportedTest)
ctx.Step(`^iterated next$`, tg.iteratedNext)
ctx.Step(`^iterated to list$`, tg.iteratedToList)
ctx.Step(`^nothing should happen because$`, tg.nothingShouldHappenBecause)
ctx.Step(`^the (.+) graph$`, tg.chooseGraph)
ctx.Step(`^the graph initializer of$`, tg.theGraphInitializerOf)
ctx.Step(`^the graph should return (\d+) for count of "(.+)"$`, tg.theGraphShouldReturnForCountOf)
ctx.Step(`^the result should be empty$`, tg.theResultShouldBeEmpty)
ctx.Step(`^the result should be (o\w+)$`, tg.theResultShouldBe)
ctx.Step(`^the result should be (u\w+)$`, tg.theResultShouldBe)
ctx.Step(`^the result should have a count of (\d+)$`, tg.theResultShouldHaveACountOf)
ctx.Step(`^the traversal of$`, tg.theTraversalOf)
ctx.Step(`^using the parameter (.+) defined as "(.+)"$`, tg.usingTheParameterDefined)
ctx.Step(`^using the parameter (.+) of P\.(.+)\("(.+)"\)$`, tg.usingTheParameterOfP)
}
func skipTestsIfNotEnabled(t *testing.T, testSuiteName string, testSuiteEnabled bool) {
if !testSuiteEnabled {
t.Skip(fmt.Sprintf("Skipping %s because %s tests are not enabled.", t.Name(), testSuiteName))
}
}
func getEnvOrDefaultBool(key string, defaultValue bool) bool {
value := getEnvOrDefaultString(key, "")
if len(value) != 0 {
boolValue, err := strconv.ParseBool(value)
if err == nil {
return boolValue
}
}
return defaultValue
}
func TestCucumberFeatures(t *testing.T) {
skipTestsIfNotEnabled(t, "cucumber godog tests",
getEnvOrDefaultBool("RUN_INTEGRATION_WITH_ALIAS_TESTS", true))
suite := godog.TestSuite{
TestSuiteInitializer: InitializeTestSuite,
ScenarioInitializer: InitializeScenario,
Options: &godog.Options{
Format: "pretty",
Paths: []string{getEnvOrDefaultString("CUCUMBER_FEATURE_FOLDER", "../../../gremlin-test/features")},
TestingT: t, // Testing instance that will run subtests.
},
}
if suite.Run() != 0 {
t.Fatal("non-zero status returned, failed to run feature tests")
}
}