feat(mosstank): add VTank-style automation PoC
This commit is contained in:
parent
f6fe0f2a4f
commit
4e6e9bc9d9
212 changed files with 49462 additions and 416 deletions
194
tests/AcDream.Plugins.MossTank.Tests/ExpressionEngineTests.cs
Normal file
194
tests/AcDream.Plugins.MossTank.Tests/ExpressionEngineTests.cs
Normal file
|
|
@ -0,0 +1,194 @@
|
|||
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);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue