blob: 6da7935bd509e7b6d42bffd6151173f8eb13eff9 [file] [log] [blame]
#region License
/*
* 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.
*/
#endregion
using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Numerics;
using System.Text.Json;
using System.Text.RegularExpressions;
using Gherkin.Ast;
using Gremlin.Net.IntegrationTest.Gherkin.Attributes;
using Gremlin.Net.Process.Traversal;
using Gremlin.Net.Structure;
using Gremlin.Net.Structure.IO.GraphSON;
using Xunit;
using static Gremlin.Net.Process.Traversal.AnonymousTraversalSource;
namespace Gremlin.Net.IntegrationTest.Gherkin
{
internal class CommonSteps : StepDefinition
{
private GraphTraversalSource _g;
private string _graphName;
private readonly IDictionary<string, object> _parameters = new Dictionary<string, object>();
private ITraversal _traversal;
private object[] _result;
private Exception _error = null;
private static readonly JsonSerializerOptions JsonDeserializingOptions =
new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase };
public static ScenarioData ScenarioData { get; set; } = new ScenarioData(new GraphSON3MessageSerializer());
private static readonly IDictionary<Regex, Func<string, string, object>> Parsers =
new Dictionary<string, Func<string, string, object>>
{
{@"vp\[(.+)\]", ToVertexProperty},
{@"d\[(.*)\]\.([bsilfdmn])", ToNumber},
{@"D\[(.+)\]", ToDirection},
{@"M\[(.+)\]", ToMerge},
{@"v\[(.+)\]", ToVertex},
{@"v\[(.+)\]\.id", (x, graphName) => ToVertex(x, graphName).Id},
{@"v\[(.+)\]\.sid", (x, graphName) => ToVertex(x, graphName).Id.ToString()},
{@"e\[(.+)\]", ToEdge},
{@"e\[(.+)\].id", (x, graphName) => ToEdge(x, graphName).Id},
{@"e\[(.+)\].sid", (x, graphName) => ToEdge(x, graphName).Id.ToString()},
{@"p\[(.+)\]", ToPath},
{@"l\[(.*)\]", ToList},
{@"s\[(.*)\]", ToSet},
{@"m\[(.+)\]", ToMap},
{@"c\[(.+)\]", ToLambda},
{@"t\[(.+)\]", ToT},
{"null", (_, __) => null},
{"true", (_, __) => true},
{"false", (_, __) => false},
{@"d\[NaN\]", (_, __) => Double.NaN},
{@"d\[Infinity\]", (_, __) => Double.PositiveInfinity},
{@"d\[-Infinity\]", (_, __) => Double.NegativeInfinity}
}.ToDictionary(kv => new Regex("^" + kv.Key + "$", RegexOptions.Compiled), kv => kv.Value);
private static readonly IDictionary<char, Func<string, object>> NumericParsers =
new Dictionary<char, Func<string, object>>
{
{ 'b', s => Convert.ToByte(s) },
{ 's', s => Convert.ToInt16(s) },
{ 'i', s => Convert.ToInt32(s) },
{ 'l', s => Convert.ToInt64(s) },
{ 'f', s => Convert.ToSingle(s, CultureInfo.InvariantCulture) },
{ 'd', s => Convert.ToDouble(s, CultureInfo.InvariantCulture) },
{ 'm', s => Convert.ToDecimal(s, CultureInfo.InvariantCulture) },
{ 'n', s => BigInteger.Parse(s) }
};
[Given("the (\\w+) graph")]
public void ChooseModernGraph(string graphName)
{
if (graphName == "empty")
{
ScenarioData.CleanEmptyData();
}
var data = ScenarioData.GetByGraphName(graphName);
_graphName = graphName;
_g = Traversal().WithRemote(data.Connection);
}
[Given("using the parameter (\\w+) defined as \"(.*)\"")]
public void UsingParameter(string name, string value)
{
var parsedValue = ParseValue(value.Replace("\\\"", "\""), _graphName);
_parameters.Add(name, parsedValue);
}
[Given("using the parameter (\\w+) of P.(\\w+)\\(\"(.*)\"\\)")]
public void UsingParameterP(string name, string pval, string value)
{
var parsedValue = ParseValue(value.Replace("\\\"", "\""), _graphName);
_parameters.Add(name, new P(pval, parsedValue));
}
[Given("the traversal of")]
public void TranslateTraversal(string traversalText)
{
if (_g == null)
{
throw new InvalidOperationException("g should be a traversal source");
}
if (ScenarioData.CurrentFeature.Tags.Select(t => t.Name).ToList().Contains("@GraphComputerOnly"))
{
_g = _g.WithComputer();
}
_traversal =
Gremlin.UseTraversal(ScenarioData.CurrentScenario.Name, _g, _parameters);
}
[Given("the graph initializer of")]
public void InitTraversal(string traversalText)
{
var traversal =
Gremlin.UseTraversal(ScenarioData.CurrentScenario.Name, _g, _parameters);
traversal.Iterate();
// We may have modified the so-called `empty` graph
if (_graphName == "empty")
{
ScenarioData.ReloadEmptyData();
}
}
[Given("an unsupported test")]
public void UnsupportedTest()
{
}
[When("iterated to list")]
public void IterateToList()
{
if (_traversal == null)
{
throw new InvalidOperationException("Traversal should be set before iterating");
}
ITraversal t = _traversal;
var list = new List<object>();
try
{
while (t.MoveNext())
{
list.Add(t.Current);
}
_result = list.ToArray();
}
catch (Exception ex)
{
_error = ex;
}
}
[When("iterated next")]
public void IterateNext()
{
if (_traversal == null)
{
throw new InvalidOperationException("Traversal should be set before iterating");
}
try
{
_traversal.MoveNext();
var result = _traversal.Current;
switch (result)
{
case null:
_result = null;
return;
case object[] arrayResult:
_result = arrayResult;
return;
case IEnumerable enumerableResult:
_result = enumerableResult.Cast<object>().ToArray();
return;
}
}
catch (Exception ex)
{
_error = ex;
}
}
[Then("the traversal will raise an error")]
public void TraversalWillRaiseError()
{
Assert.NotNull(_error);
// consume the error now that it has been asserted
_error = null;
}
[Then("the traversal will raise an error with message (\\w+) text of \"(.*)\"")]
public void TraversalWillRaiseErrorWithMessage(string comparison, string expectedMessage)
{
Assert.NotNull(_error);
switch (comparison) {
case "containing":
Assert.Equal(true, _error.Message.Contains(expectedMessage));
break;
case "starting":
Assert.Equal(true, _error.Message.StartsWith(expectedMessage));
break;
case "ending":
Assert.Equal(true, _error.Message.EndsWith(expectedMessage));
break;
default:
throw new NotSupportedException(
"Unknown comparison of " + comparison + " - must be one of: containing, starting or ending");
}
// consume the error now that it has been asserted
_error = null;
}
[Then("the result should be (\\w+)")]
public void AssertResult(string characterizedAs, DataTable table = null)
{
assertThatNoErrorWasThrown();
var ordered = characterizedAs == "ordered";
switch (characterizedAs)
{
case "empty":
Assert.Empty(_result);
return;
case "ordered":
case "unordered":
case "of":
Assert.NotNull(table);
var rows = table.Rows.ToArray();
Assert.Equal("result", rows[0].Cells.First().Value);
var expected = rows.Skip(1).Select(x => ParseValue(x.Cells.First().Value, _graphName));
if (ordered)
{
Assert.Equal(expected, _result);
}
else
{
var expectedArray = expected.ToArray();
foreach (var resultItem in _result)
{
if (resultItem is Dictionary<object, object> resultItemDict)
{
var expectedArrayContainsResultDictionary = false;
foreach (var expectedItem in expectedArray)
{
if (expectedItem is not Dictionary<object, object> expectedItemDict) continue;
if (!expectedItemDict.DeepEqual(resultItemDict)) continue;
expectedArrayContainsResultDictionary = true;
break;
}
Assert.True(expectedArrayContainsResultDictionary);
}
else if (resultItem is double resultItemDouble &&
expectedArray.Select(e => e.GetType()).Any(t => t == typeof(decimal)))
{
// Java seems to use BigDecimal by default sometimes where .NET uses double, but we only
// care for the value not its type here. So we just convert these to decimal (equivalent
// to Java's BigDecimal) and then check whether the item is in the expected results.
Assert.Contains((decimal)resultItemDouble, expectedArray);
}
else
{
Assert.Contains(resultItem, expectedArray);
}
}
if (characterizedAs != "of")
{
Assert.Equal(expectedArray.Length, _result.Length);
}
}
break;
default:
throw new NotSupportedException($"Result as '{characterizedAs}' not supported");
}
}
[Then("the result should have a count of (\\d+)")]
public void AssertCount(int count)
{
assertThatNoErrorWasThrown();
Assert.Equal(count, _result.Length);
}
[Then("the graph should return (\\d+) for count of (.+)")]
public void AssertTraversalCount(int expectedCount, string traversalText)
{
assertThatNoErrorWasThrown();
if (traversalText.StartsWith("\""))
{
traversalText = traversalText.Substring(1, traversalText.Length - 2);
}
var traversal =
Gremlin.UseTraversal(ScenarioData.CurrentScenario.Name, _g, _parameters);
var count = 0;
while (traversal.MoveNext())
{
count++;
}
Assert.Equal(expectedCount, count);
}
[Then("nothing should happen because")]
public void AssertNothing(string reason)
{
}
private void assertThatNoErrorWasThrown()
{
if (_error != null) throw _error;
}
private static object ToMap(string stringMap, string graphName)
{
var jsonMap = JsonSerializer.Deserialize<JsonElement>(stringMap, JsonDeserializingOptions);
return ParseMapValue(jsonMap, graphName);
}
private static object ToLambda(string stringLambda, string graphName)
{
return Lambda.Groovy(stringLambda);
}
private static object ToT(string enumName, string graphName)
{
return T.GetByValue(enumName);
}
private static object ToDirection(string enumName, string graphName)
{
return Direction.GetByValue(enumName);
}
private static object ToMerge(string enumName, string graphName)
{
return Merge.GetByValue(enumName);
}
private static object ToNumber(string stringNumber, string graphName)
{
return NumericParsers[stringNumber[stringNumber.Length - 1]](
stringNumber.Substring(0, stringNumber.Length - 1));
}
private static object ParseMapValue(JsonElement value, string graphName)
{
switch (value.ValueKind)
{
case JsonValueKind.Object:
{
return value.EnumerateObject().ToDictionary(property => ParseValue(property.Name, graphName),
property => ParseMapValue(property.Value, graphName));
}
case JsonValueKind.Array:
return value.EnumerateArray().Select(v => ParseMapValue(v, graphName)).ToArray();
case JsonValueKind.Number:
{
// This can maybe be simplified when this issue is resolved:
// https://github.com/dotnet/runtime/issues/31274
if (value.TryGetInt32(out var integer))
{
return integer;
}
if (value.TryGetDouble(out var floating))
{
return floating;
}
throw new ArgumentOutOfRangeException(nameof(value), value, "Not a supported number type");
}
case JsonValueKind.String:
return ParseValue(value.GetString(), graphName);
case JsonValueKind.True:
return true;
case JsonValueKind.False:
return false;
case JsonValueKind.Null:
return null;
default:
throw new ArgumentOutOfRangeException(nameof(value.ValueKind), value.ValueKind,
"JSON type not supported");
}
}
private static ISet<object> ToSet(string stringSet, string graphName)
{
return new HashSet<object>(ToList(stringSet, graphName));
}
private static IList<object> ToList(string stringList, string graphName)
{
if (stringList == "")
{
return new List<object>(0);
}
return stringList.Split(',').Select(x => ParseValue(x, graphName)).ToList();
}
private static Vertex ToVertex(string name, string graphName)
{
if (ScenarioData.GetByGraphName(graphName).Vertices.ContainsKey(name))
{
return ScenarioData.GetByGraphName(graphName).Vertices[name];
}
else
{
return new Vertex(name);
}
}
private static Edge ToEdge(string name, string graphName)
{
return ScenarioData.GetByGraphName(graphName).Edges[name];
}
private static VertexProperty ToVertexProperty(string triplet, string graphName)
{
return ScenarioData.GetByGraphName(graphName).VertexProperties[triplet];
}
private static Path ToPath(string value, string graphName)
{
return new Path(new List<ISet<string>>(0), value.Split(',').Select(x => ParseValue(x, graphName)).ToList());
}
private static object ParseValue(string stringValue, string graphName)
{
Func<string, string, object> parser = null;
string extractedValue = null;
foreach (var kv in Parsers)
{
var match = kv.Key.Match(stringValue);
if (match.Success)
{
parser = kv.Value;
extractedValue = match.Groups[1].Value;
if (match.Groups.Count > 2)
{
extractedValue += match.Groups[2].Value;
}
break;
}
}
return parser != null ? parser(extractedValue, graphName) : stringValue;
}
}
}