Files
RobustToolbox/Robust.UnitTesting/Shared/Toolshed/ToolshedTests.cs
T

511 lines
26 KiB
C#

using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using NUnit.Framework;
using Robust.Shared.GameObjects;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Toolshed;
using Robust.Shared.Toolshed.Commands.Generic;
using Robust.Shared.Toolshed.Syntax;
using Robust.Shared.Toolshed.TypeParsers;
using Robust.Shared.Toolshed.TypeParsers.Math;
namespace Robust.UnitTesting.Shared.Toolshed;
/// <summary>
/// Collection of miscellaneous toolshed command tests.
/// Several of these were just added ad hoc as bugs arose.
/// </summary>
public sealed class ToolshedTests : ToolshedTest
{
// TODO Robust.UnitTesting
// split these into separate [TestCase]s when we have pooling.
// Or some other way to avoid starting a server per command.
[Test]
public async Task TestMiscCommands()
{
await Server.WaitAssertion(() =>
{
AssertResult("i 5 iota reduce { max $value }", 5);
AssertResult("i 5 iota sum", 15);
AssertResult("i 5 iota map { + 1 }", new[] {2, 3, 4, 5, 6});
AssertResult("f 5 iota map { iota sum emplace { f 2 pow $value } }", new[] {2.0f, 8.0f, 64.0f, 1024.0f, 32768.0f});
AssertResult("f 5 iota map {iota sum emplace {f 2 pow $value}}", new[] {2.0f, 8.0f, 64.0f, 1024.0f, 32768.0f});
AssertResult("i 0 map { + 1 }", new [] { 1 });
AssertResult("i 1 to 1", new [] { 1 });
AssertResult("f 1 to 1", new [] { 1 });
AssertResult("i -2 to 2 map { + 1 }", new [] { -1, 0, 1, 2, 3 });
AssertResult("f -2 to 2 map { + 1 }", new [] { -1, 0, 1, 2, 3 });
AssertResult("f 1 to 1", new [] { 1 });
AssertResult("i 2 + 2", 4);
AssertResult("i 2 + { i 2 }", 4);
AssertResult("i 2 + 2 * 2", 8);
AssertResult("i 2 + { i 2 * 2 }", 6);
AssertResult("i 3 iota max { i 3 iota }", new[] {1, 2, 3 });
AssertResult("i 1 iota iterate { take 1 } 3", new[] { new[]{1}, [1], [1] });
AssertResult("i 3 iota iterate { take 2 } 3", new[] { new[]{1, 2}, [1, 2], [1, 2] });
ParseError<OutOfInputError>("");
ParseError<OutOfInputError>(" ");
ParseError<NotValidCommandError>("{");
ParseError<NotValidCommandError>("}");
ParseError<NotValidCommandError>("{}");
ParseError<NotValidCommandError>(";");
ParseError<EmptyCommandRun>("i 2 + { }");
ParseError<MustBeVarOrBlock>("i 3 iota max 3");
ParseError<WrongCommandReturn>("i 1 iota iterate { average } 2");
// Meta-test: check that ParseError() fails if the parse actually succeeds without generating an error
Assert.Throws<AssertionException>(() => ParseError<OutOfInputError>("i 2"));
Assert.That(ExpectedErrors.Count, Is.EqualTo(1));
ExpectedErrors.Clear();
});
}
[Test]
public async Task TestTypeArgs()
{
await Server.WaitAssertion(() =>
{
AssertResult("testtypearg int", "Int32");
AssertResult("testmultitypearg float string -1", "Single, String, -1");
ParseError<ExpectedTypeArgumentError>("testtypearg");
ParseError<ExpectedTypeArgumentError>("testtypearg ");
ParseError<UnknownType>("testtypearg invalidType");
ParseError<ExpectedTypeArgumentError>("testmultitypearg int");
ParseError<ExpectedTypeArgumentError>("testmultitypearg int ");
ParseError<UnknownType>("testmultitypearg int invalidType");
ParseError<ExpectedArgumentError>("testmultitypearg int float");
ParseError<ExpectedArgumentError>("testmultitypearg int float ");
});
}
[Test]
public async Task TestCommandTerminator()
{
await Server.WaitAssertion(() =>
{
// Terminators can be used to chain together commands that output void
AssertResult("testvoid", null);
ParseError<EndOfCommandError>("testvoid testvoid");
AssertResult("testvoid;testvoid", null);
AssertResult("testvoid; testvoid", null);
AssertResult("testvoid ; testvoid", null);
AssertResult("testvoid ;; ; testvoid", null);
// Terminators allow commands that output data to be chained with commands that take no inputs
AssertResult("testint", 1);
AssertResult("testint; testint", 1);
AssertResult("testvoid; testint", 1);
AssertResult("testint; testvoid", null);
// Terminators can interrupt argument parsing
AssertResult("testintstrarg 1 \"A\"", 1);
AssertResult("testintstrarg 1 \"A\"; testvoid", null);
ParseError<ExpectedArgumentError>("testintstrarg 1;");
ParseError<ExpectedArgumentError>("testintstrarg 1; \"A\"");
ParseError<ExpectedArgumentError>("testintstrarg 1; testvoid");
ParseError<ExpectedArgumentError>("testintstrarg 1; \"A\"; testvoid");
// Terminators can interrupt type-argument parsing
AssertResult("testmultitypearg float string -1", "Single, String, -1");
AssertResult("testmultitypearg float string -1; testvoid", null);
ParseError<ExpectedTypeArgumentError>("testmultitypearg float;");
ParseError<ExpectedTypeArgumentError>("testmultitypearg float; string -1");
ParseError<ExpectedTypeArgumentError>("testmultitypearg float; testvoid");
ParseError<ExpectedArgumentError>("testmultitypearg float string; testvoid");
// Terminators don't actually discard the final output type if it is the end of the command.;
AssertResult("testint;", 1);
AssertResult("testint; testint;", 1);
ParseError<WrongCommandReturn>("i 2 + { i 2; }");
ParseError<WrongCommandReturn>("i 2 + { i 2; ; } ;; ;");
});
}
[Test]
public async Task TestVariables()
{
await Server.WaitAssertion(() =>
{
AssertResult("i 2 => $x", 2);
AssertResult("val int $x", 2);
AssertResult("var $x", 2);
AssertResult("i 2 + $x", 4);
AssertResult("i 2 + $x + 2 ", 6);
AssertResult("i 2 + { i 2 * $x }", 6);
AssertResult("i 5 iota emplace { val int $value }", new[] {1, 2, 3, 4, 5});
AssertResult("i 5 iota emplace { var $value }", new[] {1, 2, 3, 4, 5});
AssertResult("i 5 iota emplace { i 1 + $value }", new[] {2, 3, 4, 5, 6});
AssertResult("i 5 iota reduce { + $value }", 15);
});
}
[Test, TestOf(typeof(EmplaceCommand))]
public async Task TestEmplace()
{
await Server.WaitAssertion(() =>
{
var ent = Server.EntMan.Spawn();
Server.System<MetaDataSystem>().SetEntityName(ent, "Foo");
AssertResult($"ent e{ent.Id} emplace {{ val EntityUid $value }}", ent);
AssertResult($"ent e{ent.Id} emplace {{ var $value }}", ent);
AssertResult($"ent e{ent.Id} emplace {{ f 2 + $wx }}", 2.0f);
AssertResult($"ent e{ent.Id} emplace {{ var $name }}", "Foo");
AssertResult($"player:list emplace {{ var $value }} ", Array.Empty<ICommonSession>());
AssertResult($"player:list emplace {{ var $ent }} ", Array.Empty<EntityUid>());
AssertCompletionSingle($"ent e{ent.Id} emplace ", "{");
AssertCompletionContains($"ent e{ent.Id} emplace {{ ", "val", "var", "f");
AssertCompletionContains($"ent e{ent.Id} emplace {{ val EntityUi", "EntityUi");
AssertCompletionContains($"ent e{ent.Id} emplace {{ val EntityUid", "EntityUid");
AssertCompletionContains($"ent e{ent.Id} emplace {{ val EntityUid $", "$value");
AssertCompletionContains($"ent e{ent.Id} emplace {{ val EntityUid $val", "$value");
AssertCompletionContains($"ent e{ent.Id} emplace {{ var ", "$value", "$wx", "$paused");
AssertCompletionContains($"ent e{ent.Id} emplace {{ var $", "$value", "$wx", "$paused");
AssertCompletionContains($"ent e{ent.Id} emplace {{ var $val", "$value");
AssertCompletionSingle($"ent e{ent.Id} emplace {{ var $value ", "}");
// Intentionally misspelled variable name
AssertCompletionEmpty($"ent e{ent.Id} emplace {{ var $valuie ");
AssertResult($"ent e{ent.Id} emplace {{ delete $value; i 5 }}", 5 );
Assert.That(Server.EntMan.Deleted(ent));
AssertCompletionContains("i 2 emplace { var ", "$value");
AssertCompletionInvalid("i 2 emplace { var ", "wx");
AssertCompletionContains("player:list emplace { var ", "$value", "$ent", "$paused");
AssertCompletionInvalid("player:list emplace { var ", "wx");
AssertCompletionContains("i 1 emplace { var $value empla", "emplace");
AssertCompletionSingle("i 1 emplace { var $value emplace ", "{");
AssertCompletionContains("player:list emplace { var $ent emplace { var $", "$value", "$wx", "$paused");
ParseError<NotValidCommandError>("i 1 emplace {{");
});
}
[Test]
public async Task TestTerminators()
{
await Server.WaitAssertion(() =>
{
// Baseline check that these commands work:
AssertResult("i 1", 1);
AssertResult("i 1 + 1", 2);
AssertResult("i { i 1 }", 1);
// Trailing terminators have no clear effect.
AssertResult("i 1;", 1);
AssertResult("i { i 1 };", 1);
// Simple explicit piping works
AssertResult("i 1 | + 1", 2);
// Explicit pipes imply a command is expected. Ending a command or a block after a pipe should error.
ParseError<OutOfInputError>("i 1 |");
ParseError<UnexpectedCloseBrace>("i { i 1 | }");
// A terminator inside a block or command run doesn't pipe anything;
ParseError<NoImplementationError>("i 1 ; + 1");
ParseError<WrongCommandReturn>("i { i 1 ; }");
// Check double terminators
// A starting terminators/pipes will try to be parsed as a command.
ParseError<NotValidCommandError>("|");
ParseError<NotValidCommandError>(";");
ParseError<NotValidCommandError>(";;");
ParseError<NotValidCommandError>("||");
ParseError<NotValidCommandError>("|;");
ParseError<NotValidCommandError>(";|");
AssertResult("i 1 ;;", 1);
// Consecutive pipes will try to parse the second one as the command, which will not succeed.
ParseError<NotValidCommandError>("i 1 ||");
ParseError<NotValidCommandError>("i 1 |;");
ParseError<NotValidCommandError>("i 1 ;|");
AssertResult("i 1 ;; i 1", 1);
ParseError<NotValidCommandError>("i 1 || i 1");
ParseError<NotValidCommandError>("i 1 |; i 1");
ParseError<NotValidCommandError>("i 1 ;| i 1");
ParseError<NoImplementationError>("i 1 ;; + 1");
ParseError<NotValidCommandError>("i 1 || + 1");
ParseError<NotValidCommandError>("i 1 |; + 1");
ParseError<NotValidCommandError>("i 1 ;| + 1");
});
}
[Test]
public async Task TestOptionalArgs()
{
await Server.WaitAssertion(() =>
{
// Check that straightforward optional args work.
ParseError<ExpectedArgumentError>("testoptionalargs ");
AssertResult("testoptionalargs 1", new[] {1, 0, 1});
AssertResult("testoptionalargs 1 2", new[] {1, 2, 1});
AssertResult("testoptionalargs 1 2 3", new[] {1, 2, 3});
AssertResult("testoptionalargs 1 2 3 append 4", new[] {1, 2, 3, 4});
ParseError<UnknownCommandError>("testoptionalargs 1 2 3 4");
ParseError<InvalidNumber<int>>("testoptionalargs 1 append 4");
ParseError<InvalidNumber<int>>("testoptionalargs 1 2 append 4");
// Check that semicolon terminators interrupt optional args
ParseError<ExpectedArgumentError>("testoptionalargs ;");
AssertResult("testoptionalargs 1;", new[] {1, 0, 1});
AssertResult("testoptionalargs 1 2;", new[] {1, 2, 1});
AssertResult("testoptionalargs 1 2 3;", new[] {1, 2, 3});
ParseError<UnknownCommandError>("testoptionalargs 1 2 3; 4");
AssertResult("testoptionalargs 1 2; i 3", 3);
AssertResult("testoptionalargs 1 2 3; i 4", 4);
// Check that explicit pipes interrupt optional args
ParseError<ExpectedArgumentError>("testoptionalargs |");
ParseError<OutOfInputError>("testoptionalargs 1 |");
AssertResult("testoptionalargs 1 | append 4", new[] {1, 0, 1, 4});
AssertResult("testoptionalargs 1 2 | append 4", new[] {1, 2, 1, 4});
AssertResult("testoptionalargs 1 2 3 | append 4", new[] {1, 2, 3, 4});
// Check that variables and blocks can be used to specify optional args;
AssertResult("i -1 => $i", -1);
AssertResult("testoptionalargs 1 $i", new[] {1, -1, 1});
AssertResult("testoptionalargs 1 $i 2", new[] {1, -1, 2});
AssertResult("testoptionalargs 1 { i -1 }", new[] {1, -1, 1});
AssertResult("testoptionalargs 1 { i -1 } 2", new[] {1, -1, 2});
// Repeat the above groups of tests, but within a command block.
// I.e., wrap the commands in "i 1 join { <old command> }" to prepend "1" to the results.
// This first block also effectively checks that closing braces can interrupt optional args
ParseError<ExpectedArgumentError>("i 1 join { testoptionalargs } ");
AssertResult("i 1 join { testoptionalargs 1 } ", new[] {1, 1, 0, 1});
AssertResult("i 1 join { testoptionalargs 1 2 }", new[] {1, 1, 2, 1});
AssertResult("i 1 join { testoptionalargs 1 2 3 }", new[] {1, 1, 2, 3});
AssertResult("i 1 join { testoptionalargs 1 2 3 append 4 }", new[] {1, 1, 2, 3, 4});
ParseError<UnknownCommandError>("testoptionalargs 1 2 3 4 }");
ParseError<InvalidNumber<int>>("testoptionalargs 1 2 i 3 }");
ParseError<NoImplementationError>("testoptionalargs 1 2 3 i 4 }");
ParseError<ExpectedArgumentError>("i 1 join { testoptionalargs | }");
ParseError<UnexpectedCloseBrace>("i 1 join { testoptionalargs 1 | }");
AssertResult("i 1 join { testoptionalargs 1 | append 4 }", new[] {1, 1, 0, 1, 4});
AssertResult("i 1 join { testoptionalargs 1 2 | append 4 }", new[] {1, 1, 2, 1, 4});
AssertResult("i 1 join { testoptionalargs 1 2 3 | append 4 }", new[] {1, 1, 2, 3, 4});
AssertResult("i 1 join { testoptionalargs 1 $i }", new[] {1, 1, -1, 1});
AssertResult("i 1 join { testoptionalargs 1 $i 2 }", new[] {1, 1, -1, 2});
AssertResult("i 1 join { testoptionalargs 1 { i -1 } }", new[] {1, 1, -1, 1});
AssertResult("i 1 join { testoptionalargs 1 { i -1 } 2 }", new[] {1, 1, -1, 2});
});
}
[Test]
public async Task TestParamsCollections()
{
await Server.WaitAssertion(() =>
{
// Check that straightforward optional args work.
ParseError<ExpectedArgumentError>("testparamscollection");
AssertResult("testparamsonly", new int[] {});
AssertResult("testparamscollection 1", new[] {1, 0});
AssertResult("testparamscollection 1 2", new[] {1, 2});
AssertResult("testparamscollection 1 2 3", new[] {1, 2, 3});
AssertResult("testparamscollection 1 2 3 4", new[] {1, 2, 3, 4});
ParseError<InvalidNumber<int>>("testparamscollection 1 2 append 4");
ParseError<InvalidNumber<int>>("testparamscollection 1 2 3 append 4");
ParseError<InvalidNumber<int>>("testparamscollection 1 2 3 4 append 4");
// Check that semicolon terminators interrupt optional args
ParseError<ExpectedArgumentError>("testparamscollection ;");
AssertResult("testparamsonly;", new int[] { });
AssertResult("testparamscollection 1;", new[] {1, 0});
AssertResult("testparamscollection 1 2;", new[] {1, 2});
AssertResult("testparamscollection 1 2 3;", new[] {1, 2, 3});
AssertResult("testparamscollection 1 2 3 4;", new[] {1, 2, 3, 4});
AssertResult("testparamscollection 1 2; i 4", 4);
AssertResult("testparamscollection 1 2 3; i 4", 4);
AssertResult("testparamscollection 1 2 3 4; i 4", 4);
// Check that explicit pipes interrupt optional args
ParseError<ExpectedArgumentError>("testparamscollection |");
ParseError<OutOfInputError>("testparamsonly |");
ParseError<OutOfInputError>("testparamscollection 1 |");
ParseError<OutOfInputError>("testparamscollection 1 2 |");
ParseError<OutOfInputError>("testparamscollection 1 2 3 |");
ParseError<OutOfInputError>("testparamscollection 1 2 3 4 |");
AssertResult("testparamsonly | append 1", new[] {1});
AssertResult("testparamscollection 1 | append 1", new[] {1, 0, 1});
AssertResult("testparamscollection 1 2 | append 1", new[] {1, 2, 1});
AssertResult("testparamscollection 1 2 3 | append 1", new[] {1, 2, 3, 1});
AssertResult("testparamscollection 1 2 3 4 | append 1", new[] {1, 2, 3, 4, 1});
// Check that variables and blocks can be used to specify args inside params arrays;
AssertResult("i -1 => $i", -1);
AssertResult("testparamscollection 1 2 3 $i 5", new[] {1, 2, 3, -1, 5});
AssertResult("testparamscollection 1 2 3 { i -1 } 5", new[] {1, 2, 3, -1, 5});
// Check that closing braces interrupt optional args
AssertResult("i 1 join { testparamsonly }", new[] {1});
AssertResult("i 1 join { testparamscollection 1 }", new[] {1, 1, 0});
AssertResult("i 1 join { testparamscollection 1 2 }", new[] {1, 1, 2});
AssertResult("i 1 join { testparamscollection 1 2 3 }", new[] {1, 1, 2, 3});
AssertResult("i 1 join { testparamscollection 1 2 3 4 }", new[] {1, 1, 2, 3, 4});
});
}
/// <summary>
/// Check that the type of generic parameters can be correctly inferred from the piped-in value. I.e., when check
/// that if we pipe a <see cref="List{T}"/> into a command that takes an <see cref="IEnumerable{T}"/>, the value of
/// the generic parameter can be properly inferred.
/// </summary>
[Test]
[TestOf(typeof(TakesPipedTypeAsGenericAttribute))]
public async Task TestGenericPipeInference()
{
await Server.WaitAssertion(() =>
{
// Pipe T[] -> T[]
AssertResult("testarray testarrayinfer 1", typeof(int));
// Pipe List<T> -> List<T>
AssertResult("testlist testlistinfer 1", typeof(int));
// Pipe T[] -> IEnumerable<T>
AssertResult("testarray testenumerableinfer 1", typeof(int));
// Pipe List<T> -> IEnumerable<T>
AssertResult("testlist testenumerableinfer 1", typeof(int));
// Pipe IEnumerable<T> -> IEnumerable<T>
AssertResult("testenumerable testenumerableinfer 1", typeof(int));
// Repeat but with nested types. i.e. extracting T when piping ProtoId<T> -> IEnumerable<ProtoId<T>>
AssertResult("testnestedarray testnestedarrayinfer", typeof(EntityCategoryPrototype));
AssertResult("testnestedlist testnestedlistinfer", typeof(EntityCategoryPrototype));
AssertResult("testnestedarray testnestedenumerableinfer", typeof(EntityCategoryPrototype));
AssertResult("testnestedlist testnestedenumerableinfer", typeof(EntityCategoryPrototype));
AssertResult("testnestedenumerable testnestedenumerableinfer", typeof(EntityCategoryPrototype));
// The map command used to work when the piped type was passed as an IEnumerable<T> directly, but would fail
// when given a List<T> or something else that implemented the interface.
// In particular, this would become relevant when using command variables (which store enumerables as a List<T>).
AssertResult("i 1 to 4 map { * 2 }", new[] {2, 4, 6, 8});
InvokeCommand("i 1 to 4 => $x", out _);
AssertResult("var $x map { * 2 }", new[] {2, 4, 6, 8});
});
}
[Test]
public async Task TestCompletions()
{
await Server.WaitAssertion(() =>
{
InvokeCommand($"i 1 => $x", out _);
// Valid/complete commands ending in a whitespace don't generate completions.
AssertCompletionEmpty($"i 1 ");
AssertCompletionEmpty($"i 1 => $x ");
AssertCompletionEmpty($"testvoid ");
// Without a whitespace, they will still suggest the hint for the command that is currently being typed.
AssertCompletionHint("i 1", "<value (Int32)>");
AssertCompletionSingle($"i 1 => $x", "$x");
AssertCompletionContains($"testvoid", "testvoid");
// If an error occurs while parsing something, but tha error is not at the end of the command, we should
// not generate completions. I.e., we don't want to mislead people into thinking a command is valid and is
// expecting additional arguments.
AssertCompletionHint("i a", "<value (Int32)>");
AssertCompletionEmpty("i a ");
AssertCompletionEmpty("i a 1");
AssertCompletionSingle("i $", "$x");
AssertCompletionEmpty("i $a ");
AssertCompletionSingle("var $", "$x");
AssertCompletionEmpty("var $a ");
// Test variable completion
AssertCompletionSingle($"i 1 + $", "$x");
AssertCompletionSingle($"i 1 + $x", "$x");
AssertCompletionEmpty($"i 1 + $x ");
// Completion suggestions are restricted based on the piped type.
AssertCompletionContains("", "i");
AssertCompletionInvalid("", "testpipedint");
AssertCompletionEmpty("i 5 ");
AssertCompletionContains("i 5 t", "testpipedint");
AssertCompletionInvalid("i 5 ", "i");
AssertCompletionInvalid("i 5; t", "testpipedint");
AssertCompletionEmpty("i 5; ");
AssertCompletionContains("i 5; i", "i");
// Check completions when typing out; var $x
AssertCompletionContains($"va", "val", "var", "vars");
AssertCompletionContains($"var", "var", "vars");
AssertCompletionSingle($"var ", "$x");
AssertCompletionSingle($"var $", "$x");
AssertCompletionSingle($"var $x", "$x");
AssertCompletionEmpty($"var $x ");
// Check completions when typing out: testintstrarg 1 "a"
AssertCompletionContains("testintstrarg", "testintstrarg");
AssertCompletionHint("testintstrarg ", "<i (Int32)>");
AssertCompletionHint("testintstrarg 1", "<i (Int32)>");
AssertCompletionSingle("testintstrarg 1 ", "\"");
AssertCompletionHint("testintstrarg 1 \"", "<str (String)>");
AssertCompletionHint("testintstrarg 1 \"a\"", "<str (String)>");
AssertCompletionEmpty("testintstrarg 1 \"a\" ");
AssertCompletionHint("testintstrarg 1 \"a\" + ", "<y (Int32)>");
AssertCompletionContains("i 5 iota reduce { ma", "max");
AssertCompletionContains("i 5 iota reduce { max $", "$x", "$value");
AssertCompletionEmpty("notarealcommand ");
AssertCompletionEmpty("i 2 + { notarealcommand ");
});
}
[Test]
public async Task TestCustomParser()
{
await Server.WaitAssertion(() =>
{
InvokeCommand("i -1 => $x", out _);
// custom parsers still result in argument expectation erroprs
ParseError<ExpectedArgumentError>("testcustomparser");
ParseError<ExpectedArgumentError>("testcustomparser ");
// parser overrides integer parsing. Completion parser falls back to normal integer parsing
AssertResult("testcustomvarrefparser 20", 1);
AssertResult("testcustomparser 20", 1);
// Custom parser is used to generate completion options
AssertCompletionContains("testcustomvarrefparser", "testcustomparser");
AssertCompletion("testcustomvarrefparser ", new([new("A")], "B"));
AssertCompletion("testcustomvarrefparser 2", new([new("A")], "B"));
AssertCompletionEmpty("testcustomvarrefparser 2 ");
AssertCompletionContains("testcustomparser", "testcustomparser");
AssertCompletion("testcustomparser ", new([new("A")], "B"));
AssertCompletion("testcustomparser 2", new([new("A")], "B"));
AssertCompletionEmpty("testcustomparser 2 ");
// custom Parsers still support variables and blocks, unless explicitly forbidden
AssertResult("testcustomvarrefparser { i 20 }", 20);
AssertResult("testcustomvarrefparser $x", -1);
ParseError<ExpectedNumericError>("testcustomparser { i 20 }");
ParseError<ExpectedNumericError>("testcustomparser $x");
// Variable and block completions still work
AssertCompletionSingle("testcustomvarrefparser $", "$x");
AssertCompletionSingle("testcustomvarrefparser $x", "$x");
AssertCompletionSingle("testcustomvarrefparser { i 2 ", "}");
AssertCompletionSingle("testcustomvarrefparser { i 2 ", "}");
});
}
}