mirror of
https://github.com/Anuken/Mindustry.git
synced 2026-09-22 13:01:12 +03:00
Logic parser improvements for quote/escape handling + unit tests
This commit is contained in:
@@ -38,7 +38,6 @@ import static org.junit.jupiter.api.DynamicTest.*;
|
||||
public class ApplicationTests{
|
||||
static Map testMap;
|
||||
static boolean initialized;
|
||||
//core/assets
|
||||
static final Fi testDataFolder = new Fi("../../tests/build/test_data");
|
||||
|
||||
@BeforeAll
|
||||
|
||||
243
tests/src/test/java/LogicTests.java
Normal file
243
tests/src/test/java/LogicTests.java
Normal file
@@ -0,0 +1,243 @@
|
||||
import mindustry.logic.*;
|
||||
import mindustry.logic.LExecutor.*;
|
||||
import org.junit.jupiter.api.*;
|
||||
import org.junit.jupiter.params.*;
|
||||
import org.junit.jupiter.params.provider.*;
|
||||
|
||||
import java.util.stream.*;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
public class LogicTests{
|
||||
|
||||
@BeforeAll
|
||||
static void init(){
|
||||
ApplicationTests.launchApplication();
|
||||
}
|
||||
|
||||
/** Assembles and loads a small mlog program, mirroring the LogicBlock usage pattern. */
|
||||
static LExecutor load(String code){
|
||||
LExecutor exec = new LExecutor();
|
||||
exec.privileged = true;
|
||||
exec.load(LAssembler.assemble(code, true));
|
||||
return exec;
|
||||
}
|
||||
|
||||
/** Runs a single `set result <value>` line and returns the decoded value assigned to `from`. */
|
||||
static Object setFromValue(String code){
|
||||
LExecutor exec = load(code);
|
||||
assertTrue(exec.instructions.length > 0, "expected at least one instruction to be parsed from: " + code);
|
||||
assertTrue(exec.instructions[0] instanceof SetI, "expected a set instruction from: " + code);
|
||||
return ((SetI)exec.instructions[0]).from.objval;
|
||||
}
|
||||
|
||||
@ParameterizedTest(name = "{0}")
|
||||
@MethodSource("stringEscapeCases")
|
||||
void parsesStringEscapes(String name, String code, String expected){
|
||||
assertEquals(expected, setFromValue(code));
|
||||
}
|
||||
|
||||
static Stream<Arguments> stringEscapeCases(){
|
||||
return Stream.of(
|
||||
Arguments.of(
|
||||
"plain string has no escapes",
|
||||
"set result \"asdf\"",
|
||||
"asdf"
|
||||
),
|
||||
Arguments.of(
|
||||
"\\n decodes to a real newline character",
|
||||
"set result \"line1\\nline2\"",
|
||||
"line1\nline2"
|
||||
),
|
||||
Arguments.of(
|
||||
"\\\" decodes to a literal quote without ending the string",
|
||||
"set result \"the entity said \\\"hi\\\"\"",
|
||||
"the entity said \"hi\""
|
||||
),
|
||||
Arguments.of(
|
||||
"\\\\ decodes to a single literal backslash",
|
||||
"set result \"a\\\\b\"",
|
||||
"a\\b"
|
||||
),
|
||||
Arguments.of(
|
||||
"an escaped backslash immediately followed by a literal n must NOT collapse into a newline",
|
||||
"set result \"a\\\\nb\"",
|
||||
"a\\nb"
|
||||
),
|
||||
Arguments.of(
|
||||
"unrecognized escape sequences pass through as a literal backslash + character",
|
||||
"set result \"tab\\ttab\"",
|
||||
"tab\\ttab"
|
||||
),
|
||||
Arguments.of(
|
||||
"\\n, \\\" and \\\\ combined in a single string",
|
||||
"set result \"start\\nmid\\\"quoted\\\"\\\\end\"",
|
||||
"start\nmid\"quoted\"\\end"
|
||||
),
|
||||
Arguments.of(
|
||||
"empty quoted string",
|
||||
"set result \"\"",
|
||||
""
|
||||
),
|
||||
Arguments.of(
|
||||
"escaped backslash sitting right before the closing quote",
|
||||
"set result \"end\\\\\"",
|
||||
"end\\"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void plainNumberIsNotTreatedAsAString(){
|
||||
LExecutor exec = load("set result 5");
|
||||
SetI set = (SetI)exec.instructions[0];
|
||||
assertFalse(set.from.isobj, "a bare number should not be stored as an object value");
|
||||
assertEquals(5.0, set.from.numval);
|
||||
}
|
||||
|
||||
@ParameterizedTest(name = "{0}")
|
||||
@MethodSource("sanitizeCases")
|
||||
void sanitizesInput(String name, String input, String expected){
|
||||
assertEquals(expected, LStatement.sanitize(input));
|
||||
}
|
||||
|
||||
static Stream<Arguments> sanitizeCases(){
|
||||
return Stream.of(
|
||||
Arguments.of("a bare single quote character is invalid", "\"", "invalid"),
|
||||
Arguments.of("a bare single semicolon is invalid", ";", "invalid"),
|
||||
Arguments.of("a bare single space is invalid", " ", "invalid"),
|
||||
Arguments.of("a single ordinary character passes through unchanged", "a", "a"),
|
||||
Arguments.of("empty input stays empty", "", ""),
|
||||
Arguments.of("a plain already-quoted value is untouched", "\"hello\"", "\"hello\""),
|
||||
Arguments.of("a unescaped quote at the end gets doubled", "\"hello\\\"", "\"hello\\\\\""),
|
||||
Arguments.of(
|
||||
"a bare, unescaped quote embedded inside a quoted value gets escaped",
|
||||
"\"a\"b\"",
|
||||
"\"a\\\"b\""
|
||||
),
|
||||
Arguments.of(
|
||||
"a literal backslash inside a quoted value gets escaped so it can't form an accidental escape",
|
||||
"\"C:\\Users\"",
|
||||
"\"C:\\\\Users\""
|
||||
),
|
||||
Arguments.of(
|
||||
"a real embedded newline inside a quoted value is turned into the \\n escape sequence",
|
||||
"\"line1\nline2\"",
|
||||
"\"line1\\nline2\""
|
||||
),
|
||||
Arguments.of(
|
||||
"bare quote + literal backslash + real newline combined inside one quoted value",
|
||||
"\"a\\b\"c\nd\"",
|
||||
"\"a\\\\b\\\"c\\nd\""
|
||||
),
|
||||
Arguments.of(
|
||||
"unquoted values get semicolons/spaces/quotes replaced instead of escaped",
|
||||
"hello world;test\"quote",
|
||||
"hello_worldstest'quote"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@ParameterizedTest(name = "{0}")
|
||||
@MethodSource("sanitizeRoundTripCases")
|
||||
void sanitizedQuotedValuesRoundTripThroughTheParser(String name, String userInput, String expectedDecoded){
|
||||
String sanitized = LStatement.sanitize(userInput);
|
||||
//sanity check: sanitize should have kept this as a quoted string value
|
||||
assertTrue(sanitized.length() >= 2 && sanitized.charAt(0) == '"' && sanitized.charAt(sanitized.length() - 1) == '"', "expected a quoted value, got: " + sanitized);
|
||||
|
||||
Object decoded = setFromValue("set result " + sanitized);
|
||||
assertEquals(expectedDecoded, decoded);
|
||||
}
|
||||
|
||||
static Stream<Arguments> sanitizeRoundTripCases(){
|
||||
return Stream.of(
|
||||
Arguments.of("plain quoted value", "\"hello\"", "hello"),
|
||||
Arguments.of("bare embedded quote round-trips to a literal quote", "\"a\"b\"", "a\"b"),
|
||||
Arguments.of("literal backslash round-trips to a single backslash", "\"C:\\Users\"", "C:\\Users"),
|
||||
Arguments.of("real embedded newline round-trips to a real newline", "\"line1\nline2\"", "line1\nline2"),
|
||||
Arguments.of(
|
||||
"bare quote + backslash + newline combined round-trip correctly together",
|
||||
"\"a\\b\"c\nd\"",
|
||||
"a\\b\"c\nd"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
//unterminated / malformed string literals: these must fail loudly, never silently misparse
|
||||
|
||||
@ParameterizedTest(name = "{0}")
|
||||
@MethodSource("unterminatedStringCases")
|
||||
void unterminatedStringsThrow(String name, String code){
|
||||
assertThrows(RuntimeException.class, () -> load(code), "expected a parse error from: " + code);
|
||||
}
|
||||
|
||||
static Stream<Arguments> unterminatedStringCases(){
|
||||
return Stream.of(
|
||||
Arguments.of(
|
||||
"a trailing escaped quote is content, not a terminator, so the string is left unterminated",
|
||||
"set result \"asd\\\""
|
||||
),
|
||||
Arguments.of(
|
||||
"a dangling backslash at the absolute end of input has nothing to escape",
|
||||
"set result \"asd\\"
|
||||
),
|
||||
Arguments.of(
|
||||
"an escaped-quote-then-semicolon does not let the semicolon act as a statement separator inside the string",
|
||||
"set result \"asd\\\"; set bar 5"
|
||||
),
|
||||
Arguments.of(
|
||||
"a string missing its closing quote entirely",
|
||||
"set result \"asd"
|
||||
),
|
||||
Arguments.of(
|
||||
"a string that never closes before the line ends, with trailing content after it",
|
||||
"set result \"asd\nset bar 5"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void varWithProperlyQuotedEmptyString(){
|
||||
LAssembler asm = new LAssembler();
|
||||
LVar v = asm.var("\"\"");
|
||||
assertTrue(v.isobj);
|
||||
assertEquals("", v.objval);
|
||||
}
|
||||
|
||||
@Test
|
||||
void quoteInVariableNameThrows(){
|
||||
//quotes mid-variable should not be allowed
|
||||
assertThrows(RuntimeException.class, () -> load("set result abc\"def"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void quoteAtEndOfVariableNameThrows(){
|
||||
//ditto
|
||||
assertThrows(RuntimeException.class, () -> load("set result abcdef\""));
|
||||
}
|
||||
|
||||
@Test
|
||||
void crlfAfterUnquotedTokenDoesNotCorruptTheToken(){
|
||||
LExecutor exec = load("set a bar\r\nset b bar\n");
|
||||
SetI first = (SetI)exec.instructions[0];
|
||||
SetI second = (SetI)exec.instructions[1];
|
||||
assertSame(first.from, second.from, "a CRLF-terminated reference to 'bar' must resolve to the same variable as an LF-terminated one");
|
||||
}
|
||||
|
||||
@Test
|
||||
void crlfAfterQuotedStringParsesCleanly(){
|
||||
assertEquals("bar", setFromValue("set result \"bar\"\r\n"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void loneCarriageReturnActsAsALineEnding(){
|
||||
//old-style Mac ('\r'-only) line endings are normalized the same way as CRLF.
|
||||
LExecutor exec = load("set result 1\rset result2 2\r");
|
||||
assertEquals(2, exec.instructions.length, "expected two separate statements, split on the lone '\\r'");
|
||||
}
|
||||
|
||||
@Test
|
||||
void crlfLabelsResolveToTheSameJumpLocationAsLfLabels(){
|
||||
assertDoesNotThrow(() -> load("loop:\r\njump loop always\r\n"));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user