194 lines
7.4 KiB
C#
194 lines
7.4 KiB
C#
using AcDream.Plugins.MossTank.Expressions;
|
|
|
|
namespace AcDream.Plugins.MossTank.Tests;
|
|
|
|
public sealed class ExpressionEngineTests
|
|
{
|
|
[Theory]
|
|
[InlineData("1+2*3", 7d)]
|
|
[InlineData("(1+2)*3", 9d)]
|
|
[InlineData("2^3^2", 512d)]
|
|
[InlineData("0x10+1", 17d)]
|
|
[InlineData("~1", -2d)]
|
|
[InlineData("16>>2", 4d)]
|
|
public void ArithmeticUsesUtilityBeltPrecedence(string source, double expected)
|
|
{
|
|
Assert.Equal(expected, Evaluate(source).AsNumber(), 10);
|
|
}
|
|
|
|
[Fact]
|
|
public void StatementsAndAllVariableScopesReturnLastValue()
|
|
{
|
|
var state = new ExpressionState();
|
|
|
|
ExpressionValue result = Evaluate(
|
|
"$session=2;@persistent=3;&global=4;"
|
|
+ "setvar[dynamic,5];$session+@persistent+&global+$dynamic",
|
|
state);
|
|
|
|
Assert.Equal(14d, result.AsNumber());
|
|
Assert.Equal(2d, state.Get(ExpressionVariableScope.Session, "session").AsNumber());
|
|
Assert.Equal(3d, state.Get(ExpressionVariableScope.Persistent, "persistent").AsNumber());
|
|
Assert.Equal(4d, state.Get(ExpressionVariableScope.Global, "global").AsNumber());
|
|
}
|
|
|
|
[Fact]
|
|
public void NumericAndComputedVariableNamesAreSupported()
|
|
{
|
|
var state = new ExpressionState();
|
|
|
|
ExpressionValue result = Evaluate(
|
|
"$1=7;$name=`chosen`;$getvar[name]=9;$1+$chosen",
|
|
state);
|
|
|
|
Assert.Equal(16d, result.AsNumber());
|
|
}
|
|
|
|
[Fact]
|
|
public void BooleanOperatorsShortCircuitAndReturnUtilityBeltValues()
|
|
{
|
|
ExpressionFunctionRegistry functions = CoreExpressionFunctions.CreateDefault();
|
|
int calls = 0;
|
|
functions.Register("boom", 0, 0, (_, _) =>
|
|
{
|
|
calls++;
|
|
throw new InvalidOperationException("must not run");
|
|
});
|
|
var context = new ExpressionEvaluationContext(new ExpressionState(), functions);
|
|
|
|
Assert.Equal(0d, ExpressionProgram.Compile("0&&boom[]").Evaluate(context).AsNumber());
|
|
Assert.Equal(3d, ExpressionProgram.Compile("3||boom[]").Evaluate(context).AsNumber());
|
|
Assert.Equal(0, calls);
|
|
}
|
|
|
|
[Fact]
|
|
public void StringsAreCaseInsensitiveAndPreserveDashedBareText()
|
|
{
|
|
Assert.True(Evaluate("`Olthoi`==`olthoi`").IsTruthy);
|
|
Assert.Equal("Olthoi-Noble", Evaluate("Olthoi-Noble").AsString());
|
|
Assert.Equal("Olthoi Noble", Evaluate("`Olthoi `+Noble").AsString());
|
|
}
|
|
|
|
[Fact]
|
|
public void RegexOperatorPublishesCaptureGroups()
|
|
{
|
|
var state = new ExpressionState();
|
|
|
|
ExpressionValue result = Evaluate(
|
|
"`Olthoi 275`#`(?<level>[0-9]+)`;$capturegroup_level",
|
|
state);
|
|
|
|
Assert.Equal("275", result.AsString());
|
|
}
|
|
|
|
[Fact]
|
|
public void ListsSupportMutationIndexSlicesAndHigherOrderFunctions()
|
|
{
|
|
var state = new ExpressionState();
|
|
Evaluate("$0=old0;$1=old1;$2=old2", state);
|
|
|
|
Assert.Equal(4d, Evaluate(
|
|
"$items=listcreate[1,2,3];listadd[$items,4];listcount[$items]",
|
|
state).AsNumber());
|
|
Assert.Equal(4d, Evaluate("$items{-1}", state).AsNumber());
|
|
Assert.Equal("[2,3]", Evaluate("$items{1:3}", state).ToDisplayString());
|
|
Assert.Equal("[2,4,6,8]", Evaluate(
|
|
"listmap[$items,`$1*2`]", state).ToDisplayString());
|
|
Assert.Equal("[1,3]", Evaluate(
|
|
"listfilter[$items,`$1%2==1`]", state).ToDisplayString());
|
|
Assert.Equal(10d, Evaluate(
|
|
"listreduce[$items,`$2+$1`]", state).AsNumber());
|
|
Assert.Equal("[4,3,2,1]", Evaluate(
|
|
"listsort[$items,`$2-$1`]", state).ToDisplayString());
|
|
Assert.Equal("old0", state.Get(ExpressionVariableScope.Session, "0").AsString());
|
|
Assert.Equal("old1", state.Get(ExpressionVariableScope.Session, "1").AsString());
|
|
Assert.Equal("old2", state.Get(ExpressionVariableScope.Session, "2").AsString());
|
|
}
|
|
|
|
[Fact]
|
|
public void ListRangeSupportsBothDirections()
|
|
{
|
|
Assert.Equal("[1,2,3]", Evaluate("listfromrange[1,3]").ToDisplayString());
|
|
Assert.Equal("[3,2,1]", Evaluate("listfromrange[3,1]").ToDisplayString());
|
|
}
|
|
|
|
[Fact]
|
|
public void DictionariesSupportMutationAndShallowCopy()
|
|
{
|
|
var state = new ExpressionState();
|
|
|
|
Assert.Equal(2d, Evaluate(
|
|
"$dict=dictcreate[a,1,b,2];$dict{b}", state).AsNumber());
|
|
Assert.False(Evaluate("dictadditem[$dict,c,3]", state).IsTruthy);
|
|
Assert.True(Evaluate("dictadditem[$dict,c,4]", state).IsTruthy);
|
|
Assert.Equal(3d, Evaluate("dictsize[$dict]", state).AsNumber());
|
|
Assert.Equal("[a,b,c]", Evaluate("dictkeys[$dict]", state).ToDisplayString());
|
|
Assert.Equal(4d, Evaluate("dictgetitem[dictcopy[$dict],c]", state).AsNumber());
|
|
Assert.True(Evaluate("dictremovekey[$dict,b]", state).IsTruthy);
|
|
}
|
|
|
|
[Fact]
|
|
public void CollectionsRejectDirectAndIndirectCycles()
|
|
{
|
|
var state = new ExpressionState();
|
|
Evaluate("$first=listcreate[];$second=listcreate[$first]", state);
|
|
|
|
ExpressionEvaluationException direct = Assert.Throws<ExpressionEvaluationException>(
|
|
() => Evaluate("listadd[$first,$first]", state));
|
|
ExpressionEvaluationException indirect = Assert.Throws<ExpressionEvaluationException>(
|
|
() => Evaluate("listadd[$first,$second]", state));
|
|
|
|
Assert.Contains("cyclic", direct.Message, StringComparison.OrdinalIgnoreCase);
|
|
Assert.Contains("cyclic", indirect.Message, StringComparison.OrdinalIgnoreCase);
|
|
}
|
|
|
|
[Fact]
|
|
public void CoordinatesUseAcCompassScaleAndMeters()
|
|
{
|
|
Assert.Equal(-12.5d, Evaluate(
|
|
"coordinategetns[coordinateparse[`12.5S, 3.0E`]]").AsNumber());
|
|
Assert.Equal(24d, Evaluate(
|
|
"coordinatedistanceflat[coordinateparse[`0N, 0E`],"
|
|
+ "coordinateparse[`0.1N, 0E`]]").AsNumber(), 8);
|
|
Assert.Equal("12.5S, 3.0E", Evaluate(
|
|
"coordinatetostring[coordinateparse[`12.5S, 3.0E`]]").AsString());
|
|
}
|
|
|
|
[Fact]
|
|
public void InstructionBudgetAndCancellationBoundNestedEvaluation()
|
|
{
|
|
var functions = CoreExpressionFunctions.CreateDefault();
|
|
var budgeted = new ExpressionEvaluationContext(
|
|
new ExpressionState(), functions, instructionBudget: 25);
|
|
Assert.Throws<ExpressionEvaluationException>(() =>
|
|
ExpressionProgram.Compile(
|
|
"listmap[listfromrange[1,100],`$1*2`]").Evaluate(budgeted));
|
|
|
|
using var cancellation = new CancellationTokenSource();
|
|
cancellation.Cancel();
|
|
var cancelled = new ExpressionEvaluationContext(
|
|
new ExpressionState(), functions, cancellationToken: cancellation.Token);
|
|
Assert.Throws<OperationCanceledException>(() =>
|
|
ExpressionProgram.Compile("1+1").Evaluate(cancelled));
|
|
}
|
|
|
|
[Fact]
|
|
public void FunctionErrorsNameTheSignatureAndOffset()
|
|
{
|
|
ExpressionEvaluationException error = Assert.Throws<ExpressionEvaluationException>(
|
|
() => Evaluate("sqrt[1,2]"));
|
|
|
|
Assert.Contains("sqrt[number]", error.Message, StringComparison.Ordinal);
|
|
Assert.Contains("offset 0", error.Message, StringComparison.Ordinal);
|
|
}
|
|
|
|
private static ExpressionValue Evaluate(
|
|
string source,
|
|
ExpressionState? state = null)
|
|
{
|
|
var context = new ExpressionEvaluationContext(
|
|
state ?? new ExpressionState(),
|
|
CoreExpressionFunctions.CreateDefault(new Random(1234)));
|
|
return ExpressionProgram.Compile(source).Evaluate(context);
|
|
}
|
|
}
|