summaryrefslogtreecommitdiff
path: root/test
diff options
context:
space:
mode:
Diffstat (limited to 'test')
-rw-r--r--test/moe/anita/mai/buffer/MemBufTest.java51
-rw-r--r--test/moe/anita/mai/buffer/SrcBufTest.java72
-rw-r--r--test/moe/anita/mai/lexer/LexerNumberTest.java84
-rw-r--r--test/moe/anita/mai/lexer/LexerTest.java101
-rw-r--r--test/moe/anita/mai/test/Case.java26
-rw-r--r--test/moe/anita/mai/test/Runner.java21
-rw-r--r--test/moe/anita/mai/test/TestRunner.java209
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() {
+ }
+}