diff options
| author | Antia Anderson <anita@ansan.io> | 2026-08-25 02:54:50 -0400 |
|---|---|---|
| committer | Antia Anderson <anita@ansan.io> | 2026-08-25 02:54:50 -0400 |
| commit | a532a21b792f45359aab9e6259b2e9412f31b81a (patch) | |
| tree | 693b08091a3de67d1721bcc55a9f6014760c9f65 /test/moe | |
| parent | 14b0ec4928ec1876d3844c9b0fb23ba3ca583ff8 (diff) | |
| download | mai-a532a21b792f45359aab9e6259b2e9412f31b81a.tar.gz mai-a532a21b792f45359aab9e6259b2e9412f31b81a.zip | |
Start work on the lexer and added tests
Signed-off-by: Antia Anderson <anita@ansan.io>
Diffstat (limited to 'test/moe')
| -rw-r--r-- | test/moe/anita/mai/buffer/MemBufTest.java | 51 | ||||
| -rw-r--r-- | test/moe/anita/mai/buffer/SrcBufTest.java | 72 | ||||
| -rw-r--r-- | test/moe/anita/mai/lexer/LexerNumberTest.java | 84 | ||||
| -rw-r--r-- | test/moe/anita/mai/lexer/LexerTest.java | 101 | ||||
| -rw-r--r-- | test/moe/anita/mai/test/Case.java | 26 | ||||
| -rw-r--r-- | test/moe/anita/mai/test/Runner.java | 21 | ||||
| -rw-r--r-- | test/moe/anita/mai/test/TestRunner.java | 209 |
7 files changed, 564 insertions, 0 deletions
diff --git a/test/moe/anita/mai/buffer/MemBufTest.java b/test/moe/anita/mai/buffer/MemBufTest.java new file mode 100644 index 0000000..450ac31 --- /dev/null +++ b/test/moe/anita/mai/buffer/MemBufTest.java @@ -0,0 +1,51 @@ +package moe.anita.mai.buffer; + +import moe.anita.mai.test.Case; +import moe.anita.mai.test.Runner; + +@Runner(name = "MemBuf") +public final class MemBufTest { + + @Case(name = "length") + static void length() { + var buffer = new MemBuf("mai"); + + assert buffer.length() == 3; + } + + @Case(name = "read") + static void read() { + var buffer = new MemBuf("mai"); + + assert buffer.get(0) == 'm'; + assert buffer.get(1) == 'a'; + assert buffer.get(2) == 'i'; + } + + @Case(name = "bounds") + static void bounds() { + var buffer = new MemBuf("mai"); + + assert !buffer.has(-1); + assert buffer.has(0); + assert buffer.has(2); + assert !buffer.has(3); + } + + @Case(name = "empty") + static void empty() { + assert new MemBuf("").isEmpty(); + assert !new MemBuf("mai").isEmpty(); + } + + @Case(name = "slice") + static void slice() { + var buffer = new MemBuf("hello mai"); + + assert buffer.slice(0, 5).toString().equals("hello"); + assert buffer.slice(6, 9).toString().equals("mai"); + } + + private MemBufTest() { + } +} diff --git a/test/moe/anita/mai/buffer/SrcBufTest.java b/test/moe/anita/mai/buffer/SrcBufTest.java new file mode 100644 index 0000000..ced5d1c --- /dev/null +++ b/test/moe/anita/mai/buffer/SrcBufTest.java @@ -0,0 +1,72 @@ +package moe.anita.mai.buffer; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +import moe.anita.mai.buffer.SrcBuf; + +import moe.anita.mai.test.Case; +import moe.anita.mai.test.Runner; + +@Runner(name = "SrcBuf") +public final class SrcBufTest { + + @Case(name = "read test") + static void read() throws IOException { + Path path = Files.createTempFile("mai-src-", ".mai"); + + try { + Files.writeString(path, "mai"); + + var buffer = new SrcBuf(path); + + assert buffer.length() == 3; + + assert buffer.get(0) == 'm'; + assert buffer.get(1) == 'a'; + assert buffer.get(2) == 'i'; + } finally { + Files.deleteIfExists(path); + } + } + + @Case(name = "slices test") + static void slice() throws IOException { + Path path = Files.createTempFile("mai-src-",".mai"); + + try { + Files.writeString(path, "hello mai"); + + var buffer = new SrcBuf(path); + + assert buffer + .slice(0, 5) + .toString() + .equals("hello"); + + assert buffer + .slice(6, 9) + .toString() + .equals("mai"); + } finally { + Files.deleteIfExists(path); + } + } + + @Case(name = "retains source path") + static void path() throws IOException { + Path path = Files.createTempFile("mai-src-", ".mai"); + + try { + var buffer = new SrcBuf(path); + + assert buffer.path().equals(path); + } finally { + Files.deleteIfExists(path); + } + } + + private SrcBufTest() { + } +} diff --git a/test/moe/anita/mai/lexer/LexerNumberTest.java b/test/moe/anita/mai/lexer/LexerNumberTest.java new file mode 100644 index 0000000..1f4ab1f --- /dev/null +++ b/test/moe/anita/mai/lexer/LexerNumberTest.java @@ -0,0 +1,84 @@ +package moe.anita.mai.lexer; + +import moe.anita.mai.buffer.MemBuf; +import moe.anita.mai.test.Case; +import moe.anita.mai.test.Runner; +import moe.anita.mai.token.TokenKind; + +@Runner(name = "Lexer Numbers") +public final class LexerNumberTest { + + @Case(name = "integer literal") + static void integer() { + var lexer = lexer("12345"); + + assert lexer.next().kind() == TokenKind.INT; + assert lexer.next().kind() == TokenKind.EOF; + } + + @Case(name = "integer underscores") + static void integerUnderscores() { + var lexer = lexer("1_000_000"); + + assert lexer.next().kind() == TokenKind.INT; + assert lexer.next().kind() == TokenKind.EOF; + } + + @Case(name = "hexadecimal integer") + static void hexadecimal() { + var lexer = lexer("0xDEAD_BEEF"); + + assert lexer.next().kind() == TokenKind.INT; + assert lexer.next().kind() == TokenKind.EOF; + } + + @Case(name = "binary integer") + static void binary() { + var lexer = lexer("0b1010_1100"); + + assert lexer.next().kind() == TokenKind.INT; + assert lexer.next().kind() == TokenKind.EOF; + } + + @Case(name = "octal integer") + static void octal() { + var lexer = lexer("0o755"); + + assert lexer.next().kind() == TokenKind.INT; + assert lexer.next().kind() == TokenKind.EOF; + } + + @Case(name = "float literal") + static void floatingPoint() { + var lexer = lexer("123.456"); + + assert lexer.next().kind() == TokenKind.FLOAT; + assert lexer.next().kind() == TokenKind.EOF; + } + + @Case(name = "float underscores") + static void floatingPointUnderscores() { + var lexer = lexer("1_000.25_50"); + + assert lexer.next().kind() == TokenKind.FLOAT; + assert lexer.next().kind() == TokenKind.EOF; + } + + @Case(name = "range is not a float") + static void range() { + var lexer = lexer("10..20"); + + assert lexer.next().kind() == TokenKind.INT; + assert lexer.next().kind() == TokenKind.DOT; + assert lexer.next().kind() == TokenKind.DOT; + assert lexer.next().kind() == TokenKind.INT; + assert lexer.next().kind() == TokenKind.EOF; + } + + private static Lexer lexer(String source) { + return new Lexer(new MemBuf(source)); + } + + private LexerNumberTest() { + } +} diff --git a/test/moe/anita/mai/lexer/LexerTest.java b/test/moe/anita/mai/lexer/LexerTest.java new file mode 100644 index 0000000..9ce8bef --- /dev/null +++ b/test/moe/anita/mai/lexer/LexerTest.java @@ -0,0 +1,101 @@ +package moe.anita.mai.lexer; + +import moe.anita.mai.buffer.MemBuf; +import moe.anita.mai.test.Case; +import moe.anita.mai.test.Runner; +import moe.anita.mai.token.TokenKind; + +@Runner(name = "Lexer") +public final class LexerTest { + + @Case(name = "empty source") + static void empty() { + var lexer = lexer(""); + + assert lexer.next().kind() == TokenKind.EOF; + } + + @Case(name = "identifier") + static void identifier() { + var lexer = lexer("hello"); + + assert lexer.next().kind() == TokenKind.IDENT; + assert lexer.next().kind() == TokenKind.EOF; + } + + @Case(name = "Unicode identifier") + static void unicodeIdentifier() { + var lexer = lexer("こんにちは"); + + assert lexer.next().kind() == TokenKind.IDENT; + assert lexer.next().kind() == TokenKind.EOF; + } + + @Case(name = "newline") + static void newline() { + var lexer = lexer("\n"); + + assert lexer.next().kind() == TokenKind.EOL; + assert lexer.next().kind() == TokenKind.EOF; + } + + @Case(name = "operators") + static void operators() { + var lexer = lexer( + "+ ++ - -- * / % == = := < <= <> > >= && & || | ^ ~ !" + ); + + assert lexer.next().kind() == TokenKind.ADD; + assert lexer.next().kind() == TokenKind.INC; + + assert lexer.next().kind() == TokenKind.SUB; + assert lexer.next().kind() == TokenKind.DEC; + + assert lexer.next().kind() == TokenKind.MUL; + assert lexer.next().kind() == TokenKind.DIV; + assert lexer.next().kind() == TokenKind.MOD; + + assert lexer.next().kind() == TokenKind.EQ; + assert lexer.next().kind() == TokenKind.ASSIGN; + assert lexer.next().kind() == TokenKind.DECLARE; + + assert lexer.next().kind() == TokenKind.LT; + assert lexer.next().kind() == TokenKind.LE; + assert lexer.next().kind() == TokenKind.NE; + + assert lexer.next().kind() == TokenKind.GT; + assert lexer.next().kind() == TokenKind.GE; + + assert lexer.next().kind() == TokenKind.AND; + assert lexer.next().kind() == TokenKind.BIT_AND; + + assert lexer.next().kind() == TokenKind.OR; + assert lexer.next().kind() == TokenKind.BIT_OR; + + assert lexer.next().kind() == TokenKind.BIT_XOR; + assert lexer.next().kind() == TokenKind.BIT_NOT; + assert lexer.next().kind() == TokenKind.NOT; + + assert lexer.next().kind() == TokenKind.EOF; + } + + @Case(name = "shift operators remain separate") + static void shiftsRemainSeparate() { + var lexer = lexer("<< >>"); + + assert lexer.next().kind() == TokenKind.LT; + assert lexer.next().kind() == TokenKind.LT; + + assert lexer.next().kind() == TokenKind.GT; + assert lexer.next().kind() == TokenKind.GT; + + assert lexer.next().kind() == TokenKind.EOF; + } + + private static Lexer lexer(String source) { + return new Lexer(new MemBuf(source)); + } + + private LexerTest() { + } +} diff --git a/test/moe/anita/mai/test/Case.java b/test/moe/anita/mai/test/Case.java new file mode 100644 index 0000000..1288a70 --- /dev/null +++ b/test/moe/anita/mai/test/Case.java @@ -0,0 +1,26 @@ +package moe.anita.mai.test; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Marks a method as a test case. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface Case { + + /** + * Display name of the test case. + * + * If empty, the method name is used. + */ + String name() default ""; + + /** + * Controls whether this test case is executed. + */ + boolean enabled() default true; +} diff --git a/test/moe/anita/mai/test/Runner.java b/test/moe/anita/mai/test/Runner.java new file mode 100644 index 0000000..c7c18c0 --- /dev/null +++ b/test/moe/anita/mai/test/Runner.java @@ -0,0 +1,21 @@ +package moe.anita.mai.test; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Marks a class as a test runner. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface Runner { + + /** + * Display name of the runner. + * + * If empty, the class name is used. + */ + String name() default ""; +} diff --git a/test/moe/anita/mai/test/TestRunner.java b/test/moe/anita/mai/test/TestRunner.java new file mode 100644 index 0000000..7c74bf3 --- /dev/null +++ b/test/moe/anita/mai/test/TestRunner.java @@ -0,0 +1,209 @@ +package moe.anita.mai.test; + +import java.io.File; +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.nio.file.Files; +import java.nio.file.Path; + +public final class TestRunner { + + public static void main(String[] args) throws IOException { + Path root = Path.of( + System.getProperty( + "mai.test.classes", + "build/test-classes" + ) + ); + + Result result = new Result(); + + try (var paths = Files.walk(root)) { + paths + .filter(Files::isRegularFile) + .filter(TestRunner::isClassFile) + .filter(TestRunner::isTopLevelClass) + .map(path -> className(root, path)) + .sorted() + .forEach(name -> runClass(name, result)); + } + + printSummary(result); + + if (result.failed != 0) { + System.exit(1); + } + } + + private static boolean isClassFile(Path path) { + return path.toString().endsWith(".class"); + } + + private static boolean isTopLevelClass(Path path) { + return !path.getFileName() + .toString() + .contains("$"); + } + + private static String className(Path root, Path classFile) { + String relative = root + .relativize(classFile) + .toString(); + + String name = relative.substring(0, relative.length() - ".class".length() + ); + + return name.replace(File.separatorChar, '.'); + } + + private static void runClass(String className, Result result) { + try { + Class<?> testClass = Class.forName(className); + + Runner runner = + testClass.getAnnotation(Runner.class); + + if (runner == null) { + return; + } + + runClass(testClass, runner, result); + + } catch (ClassNotFoundException e) { + System.err.printf( + "[FAIL] unable to load %s: %s%n", + className, + e + ); + + result.failed++; + } + } + + private static void runClass( + Class<?> testClass, + Runner runner, + Result result + ) { + String name = runner.name(); + + if (name.isEmpty()) { + name = testClass.getSimpleName(); + } + + System.out.println(); + System.out.println(name); + + for (Method method : testClass.getDeclaredMethods()) { + Case testCase = method.getAnnotation(Case.class); + + if (testCase == null) { + continue; + } + + runCase(testClass, method, testCase, result ); + } + } + + private static void runCase(Class<?> testClass, Method method, Case testCase, Result result) { + String name = testCase.name(); + + if (name.isEmpty()) { + name = method.getName(); + } + + if (!testCase.enabled()) { + System.out.printf( + " [SKIP] %s%n", + name + ); + + result.skipped++; + return; + } + + if (!validCase(method)) { + System.err.printf( + " [FAIL] %s: invalid test case%n", + name + ); + + result.failed++; + return; + } + + try { + Object instance = null; + + if (!Modifier.isStatic(method.getModifiers())) { + instance = createInstance(testClass); + } + + method.setAccessible(true); + method.invoke(instance); + + System.out.printf( + " [PASS] %s%n", + name + ); + + result.passed++; + + } catch (InvocationTargetException e) { + Throwable cause = e.getCause(); + + System.err.printf( + " [FAIL] %s: %s%n", + name, + cause + ); + + result.failed++; + + } catch (ReflectiveOperationException e) { + System.err.printf( + " [FAIL] %s: %s%n", + name, + e + ); + + result.failed++; + } + } + + private static boolean validCase(Method method) { + return method.getParameterCount() == 0 + && method.getReturnType() == void.class; + } + + private static Object createInstance(Class<?> testClass) throws ReflectiveOperationException { + var constructor = + testClass.getDeclaredConstructor(); + + constructor.setAccessible(true); + + return constructor.newInstance(); + } + + private static void printSummary(Result result) { + System.out.println(); + + System.out.printf( + "Tests: %d passed, %d failed, %d skipped%n", + result.passed, + result.failed, + result.skipped + ); + } + + private static final class Result { + int passed; + int failed; + int skipped; + } + + private TestRunner() { + } +} |
