diff --git a/compiler/test/Helpers.kt b/compiler/test/Helpers.kt new file mode 100644 index 000000000..0ea41bf05 --- /dev/null +++ b/compiler/test/Helpers.kt @@ -0,0 +1,156 @@ +package prog8tests.helpers + +import kotlin.test.* +import kotlin.io.path.* +import java.nio.file.Path + +import prog8.ast.IBuiltinFunctions +import prog8.ast.IMemSizer +import prog8.ast.IStringEncoding +import prog8.ast.base.DataType +import prog8.ast.base.Position +import prog8.ast.expressions.Expression +import prog8.ast.expressions.InferredTypes +import prog8.ast.expressions.NumericLiteralValue +import prog8.compiler.CompilationResult +import prog8.compiler.compileProgram +import prog8.compiler.target.ICompilationTarget + +// TODO: find a way to share with compilerAst/test/Helpers.kt, while still being able to amend it (-> compileFile(..)) + +internal fun CompilationResult.assertSuccess(description: String = ""): CompilationResult { + assertTrue(success, "expected successful compilation but failed $description") + return this +} + +internal fun CompilationResult.assertFailure(description: String = ""): CompilationResult { + assertFalse(success, "expected failure to compile but succeeded $description") + return this +} + +/** + * @see CompilationResult.assertSuccess + * @see CompilationResult.assertFailure + */ +internal fun compileFile( + platform: ICompilationTarget, + optimize: Boolean, + fileDir: Path, + fileName: String, + outputDir: Path = prog8tests.helpers.outputDir +) : CompilationResult { + val filepath = fileDir.resolve(fileName) + assumeReadableFile(filepath) + return compileProgram( + filepath, + optimize, + writeAssembly = true, + slowCodegenWarnings = false, + platform.name, + libdirs = listOf(), + outputDir + ) +} + +/** + * Takes a [sourceText] as a String, writes it to a temporary + * file and then runs the compiler on that. + * @see compileFile + */ +internal fun compileText( + platform: ICompilationTarget, + optimize: Boolean, + sourceText: String +) : CompilationResult { + val filePath = outputDir.resolve("on_the_fly_test_" + sourceText.hashCode().toUInt().toString(16) + ".p8") + // we don't assumeNotExists(filePath) - should be ok to just overwrite it + filePath.toFile().writeText(sourceText) + return compileFile(platform, optimize, filePath.parent, filePath.name) +} + + + +val workingDir : Path = Path("").absolute() // Note: Path(".") does NOT work..! +val fixturesDir : Path = workingDir.resolve("test/fixtures") +val resourcesDir : Path = workingDir.resolve("res") +val outputDir : Path = workingDir.resolve("build/tmp/test") + +fun assumeReadable(path: Path) { + assertTrue(path.isReadable(), "sanity check: should be readable: ${path.absolute()}") +} + +fun assumeReadableFile(path: Path) { + assumeReadable(path) + assertTrue(path.isRegularFile(), "sanity check: should be normal file: ${path.absolute()}") +} + +fun assumeDirectory(path: Path) { + assertTrue(path.isDirectory(), "sanity check; should be directory: $path") +} + +fun assumeNotExists(path: Path) { + assertFalse(path.exists(), "sanity check: should not exist: ${path.absolute()}") +} + +fun sanityCheckDirectories(workingDirName: String? = null) { + if (workingDirName != null) + assertEquals(workingDirName, workingDir.fileName.toString(), "name of current working dir") + assumeDirectory(workingDir) + assumeDirectory(fixturesDir) + assumeDirectory(resourcesDir) + assumeDirectory(outputDir) +} + + +fun mapCombinations(dim1: Iterable, dim2: Iterable, combine2: (A, B) -> R) = + sequence { + for (a in dim1) + for (b in dim2) + yield(combine2(a, b)) + }.toList() + +fun mapCombinations(dim1: Iterable, dim2: Iterable, dim3: Iterable, combine3: (A, B, C) -> R) = + sequence { + for (a in dim1) + for (b in dim2) + for (c in dim3) + yield(combine3(a, b, c)) + }.toList() + +fun mapCombinations(dim1: Iterable, dim2: Iterable, dim3: Iterable, dim4: Iterable, combine4: (A, B, C, D) -> R) = + sequence { + for (a in dim1) + for (b in dim2) + for (c in dim3) + for (d in dim4) + yield(combine4(a, b, c, d)) + }.toList() + + +val DummyEncoding = object : IStringEncoding { + override fun encodeString(str: String, altEncoding: Boolean): List { + throw Exception("just a dummy - should not be called") + } + + override fun decodeString(bytes: List, altEncoding: Boolean): String { + throw Exception("just a dummy - should not be called") + } +} + +val DummyFunctions = object : IBuiltinFunctions { + override val names: Set = emptySet() + override val purefunctionNames: Set = emptySet() + override fun constValue( + name: String, + args: List, + position: Position, + memsizer: IMemSizer + ): NumericLiteralValue? = null + + override fun returnType(name: String, args: MutableList) = InferredTypes.InferredType.unknown() +} + +val DummyMemsizer = object : IMemSizer { + override fun memorySize(dt: DataType): Int = 0 +} + diff --git a/compiler/test/TestCompilerOnExamples.kt b/compiler/test/TestCompilerOnExamples.kt index effd4c23d..361d5f174 100644 --- a/compiler/test/TestCompilerOnExamples.kt +++ b/compiler/test/TestCompilerOnExamples.kt @@ -1,80 +1,127 @@ package prog8tests -import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestInstance +import org.junit.jupiter.api.TestFactory +import org.junit.jupiter.api.Disabled +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.DynamicTest +import org.junit.jupiter.api.DynamicTest.dynamicTest +import prog8tests.helpers.* +import kotlin.io.path.* + import prog8.compiler.compileProgram +import prog8.compiler.target.C64Target import prog8.compiler.target.Cx16Target import prog8.compiler.target.ICompilationTarget -import kotlin.io.path.Path -import kotlin.io.path.absolute -import kotlin.io.path.isDirectory -import kotlin.test.assertEquals -import kotlin.test.assertTrue /** * ATTENTION: this is just kludge! * They are not really unit tests, but rather tests of the whole process, * from source file loading all the way through to running 64tass. - * What's more: in case of failure (to compile and assemble) - which is when tests should help you - - * these tests will actually be ignored (ie. NOT fail), - * because in the code there are calls to Process.exit, making it essentially untestable. */ +//@Disabled("to save some time") @TestInstance(TestInstance.Lifecycle.PER_CLASS) class TestCompilerOnExamples { - val workingDir = Path("").absolute() // Note: Path(".") does NOT work..! - val examplesDir = workingDir.resolve("../examples") - val outputDir = workingDir.resolve("build/tmp/test") + private val examplesDir = workingDir.resolve("../examples") - @Test - fun testDirectoriesSanityCheck() { - assertEquals("compiler", workingDir.fileName.toString()) - assertTrue(examplesDir.isDirectory(), "sanity check; should be directory: $examplesDir") - assertTrue(outputDir.isDirectory(), "sanity check; should be directory: $outputDir") + @BeforeAll + fun setUp() { + sanityCheckDirectories("compiler") + assumeDirectory(examplesDir) } // TODO: make assembly stage testable - in case of failure (eg of 64tass) it Process.exit s - fun testExample(nameWithoutExt: String, platform: ICompilationTarget, optimize: Boolean) { - val filepath = examplesDir.resolve("$nameWithoutExt.p8") - val result = compileProgram( - filepath, - optimize, - writeAssembly = true, - slowCodegenWarnings = false, - compilationTarget = platform.name, - libdirs = listOf(), - outputDir - ) - assertTrue(result.success, "${platform.name}, optimize=$optimize: \"$filepath\"") + private fun makeDynamicCompilerTest(name: String, platform: ICompilationTarget, optimize: Boolean) : DynamicTest { + val searchIn = mutableListOf(examplesDir) + if (platform == Cx16Target) { + searchIn.add(0, examplesDir.resolve("cx16")) + } + val filepath = searchIn.map { it.resolve("$name.p8") }.first { it.exists() } + val displayName = "${examplesDir.relativize(filepath)}: ${platform.name}, optimize=$optimize" + return dynamicTest(displayName) { + compileProgram( + filepath, + optimize, + writeAssembly = true, + slowCodegenWarnings = false, + compilationTarget = platform.name, + libdirs = listOf(), + outputDir + ).assertSuccess("; $displayName") + } } + @TestFactory + @Disabled + fun bothCx16AndC64() = mapCombinations( + dim1 = listOf( + "animals", + "balls", + "cube3d", + "cube3d-float", + "cube3d-gfx", + "cxlogo", + "dirlist", + "fibonacci", + "line-circle-gfx", + "line-circle-txt", + "mandelbrot", + "mandelbrot-gfx", + "numbergame", + "primes", + "rasterbars", + "screencodes", + "sorting", + "swirl", + "swirl-float", + "tehtriz", + "test", + "textelite", + ), + dim2 = listOf(Cx16Target, C64Target), + dim3 = listOf(false, true), + combine3 = ::makeDynamicCompilerTest + ) - @Test - fun test_cxLogo_noopt() { - testExample("cxlogo", Cx16Target, false) - } - @Test - fun test_cxLogo_opt() { - testExample("cxlogo", Cx16Target, true) - } - - @Test - fun test_swirl_noopt() { - testExample("swirl", Cx16Target, false) - } - @Test - fun test_swirl_opt() { - testExample("swirl", Cx16Target, true) - } - - @Test - fun test_animals_noopt() { - testExample("animals", Cx16Target, false) - } - @Test - fun test_animals_opt() { - testExample("animals", Cx16Target, true) - } + @TestFactory +// @Disabled + fun onlyC64() = mapCombinations( + dim1 = listOf( + "balloonflight", + "bdmusic", + "bdmusic-irq", + "charset", + "cube3d-sprites", + "plasma", + "sprites", + "turtle-gfx", + "wizzine", + ), + dim2 = listOf(C64Target), + dim3 = listOf(false, true), + combine3 = ::makeDynamicCompilerTest + ) + @TestFactory +// @Disabled + fun onlyCx16() = mapCombinations( + dim1 = listOf( + "vtui/testvtui", + "amiga", + "bobs", + "cobramk3-gfx", + "colorbars", + "datetime", + "highresbitmap", + "kefrenbars", + "mandelbrot-gfx-colors", + "multipalette", + "testgfx2", + ), + dim2 = listOf(Cx16Target), + dim3 = listOf(false, true), + combine3 = ::makeDynamicCompilerTest + ) } diff --git a/compiler/test/TestCompilerOnImportsAndIncludes.kt b/compiler/test/TestCompilerOnImportsAndIncludes.kt new file mode 100644 index 000000000..e67600783 --- /dev/null +++ b/compiler/test/TestCompilerOnImportsAndIncludes.kt @@ -0,0 +1,135 @@ +package prog8tests + +import org.junit.jupiter.api.TestInstance +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.TestFactory +import org.junit.jupiter.api.DynamicTest +import org.junit.jupiter.api.DynamicTest.dynamicTest +import kotlin.test.* +import kotlin.io.path.* +import prog8tests.helpers.* + +import prog8.ast.expressions.AddressOf +import prog8.ast.expressions.IdentifierReference +import prog8.ast.expressions.StringLiteralValue +import prog8.ast.statements.FunctionCallStatement +import prog8.ast.statements.Label +import prog8.compiler.target.Cx16Target + + +/** + * ATTENTION: this is just kludge! + * They are not really unit tests, but rather tests of the whole process, + * from source file loading all the way through to running 64tass. + */ +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class TestCompilerOnImportsAndIncludes { + + @BeforeAll + fun setUp() { + sanityCheckDirectories("compiler") + } + + @Test + fun testImportFromSameFolder() { + val filepath = fixturesDir.resolve("importFromSameFolder.p8") + val imported = fixturesDir.resolve("foo_bar.p8") + assumeReadableFile(filepath) + assumeReadableFile(imported) + + val platform = Cx16Target + val result = compileFile(platform, false, fixturesDir, filepath.name) + .assertSuccess() + + val program = result.programAst + val startSub = program.entrypoint() + val strLits = startSub.statements + .filterIsInstance() + .map { it.args[0] as IdentifierReference } + .map { it.targetVarDecl(program)!!.value as StringLiteralValue } + + assertEquals("main.bar", strLits[0].value) + assertEquals("foo.bar", strLits[1].value) + assertEquals("main", strLits[0].definingScope().name) + assertEquals("foo", strLits[1].definingScope().name) + } + + @Test + fun testAsmIncludeFromSameFolder() { + val filepath = fixturesDir.resolve("asmIncludeFromSameFolder.p8") + val included = fixturesDir.resolve("foo_bar.asm") + assumeReadableFile(filepath) + assumeReadableFile(included) + + val platform = Cx16Target + val result = compileFile(platform, false, fixturesDir, filepath.name) + .assertSuccess() + + val program = result.programAst + val startSub = program.entrypoint() + val args = startSub.statements + .filterIsInstance() + .map { it.args[0] } + + val str0 = (args[0] as IdentifierReference).targetVarDecl(program)!!.value as StringLiteralValue + assertEquals("main.bar", str0.value) + assertEquals("main", str0.definingScope().name) + + val id1 = (args[1] as AddressOf).identifier + val lbl1 = id1.targetStatement(program) as Label + assertEquals("foo_bar", lbl1.name) + assertEquals("start", lbl1.definingScope().name) + } + + @Test + fun testAsmbinaryDirectiveWithNonExistingFile() { + val p8Path = fixturesDir.resolve("asmbinaryNonExisting.p8") + val binPath = fixturesDir.resolve("i_do_not_exist.bin") + assumeReadableFile(p8Path) + assumeNotExists(binPath) + + compileFile(Cx16Target, false, p8Path.parent, p8Path.name, outputDir) + .assertFailure() + } + + @Test + fun testAsmbinaryDirectiveWithNonReadableFile() { + val p8Path = fixturesDir.resolve("asmbinaryNonReadable.p8") + val binPath = fixturesDir.resolve("subFolder") + assumeReadableFile(p8Path) + assumeDirectory(binPath) + + compileFile(Cx16Target, false, p8Path.parent, p8Path.name, outputDir) + .assertFailure() + } + + @TestFactory + fun asmbinaryDirectiveWithExistingBinFile(): Iterable = + listOf( + Triple("same ", "asmBinaryFromSameFolder.p8", "do_nothing1.bin"), + Triple("sub", "asmBinaryFromSubFolder.p8", "subFolder/do_nothing2.bin"), + ).map { + val (where, p8Str, binStr) = it + val p8Path = fixturesDir.resolve(p8Str) + val binPath = fixturesDir.resolve(binStr) + val displayName = "%asmbinary from ${where}folder" + dynamicTest(displayName) { + assumeReadableFile(p8Path) + assumeReadableFile(binPath) + assertNotEquals( // the bug we're testing for (#??) was hidden if outputDir == workinDir + workingDir.normalize().toAbsolutePath(), + outputDir.normalize().toAbsolutePath(), + "sanity check: workingDir and outputDir should not be the same folder") + + compileFile(Cx16Target, false, p8Path.parent, p8Path.name, outputDir) + .assertSuccess( + "argument to assembler directive .binary " + + "should be relative to the generated .asm file (in output dir), " + + "NOT relative to .p8 neither current working dir" + ) + } + } + + } + diff --git a/compiler/test/fixtures/asmBinaryFromSameFolder.p8 b/compiler/test/fixtures/asmBinaryFromSameFolder.p8 new file mode 100644 index 000000000..d48a8c6b0 --- /dev/null +++ b/compiler/test/fixtures/asmBinaryFromSameFolder.p8 @@ -0,0 +1,10 @@ +main { + sub start() { + stuff.do_nothing() + } +} + +stuff $1000 { + romsub $1000 = do_nothing() + %asmbinary "do_nothing1.bin", 0 +} \ No newline at end of file diff --git a/compiler/test/fixtures/asmBinaryFromSubFolder.p8 b/compiler/test/fixtures/asmBinaryFromSubFolder.p8 new file mode 100644 index 000000000..5b48b7892 --- /dev/null +++ b/compiler/test/fixtures/asmBinaryFromSubFolder.p8 @@ -0,0 +1,10 @@ +main { + sub start() { + stuff.do_nothing() + } +} + +stuff $1000 { + romsub $1000 = do_nothing() + %asmbinary "subFolder/do_nothing2.bin", 0 +} \ No newline at end of file diff --git a/compiler/test/fixtures/asmBinaryNonExisting.p8 b/compiler/test/fixtures/asmBinaryNonExisting.p8 new file mode 100644 index 000000000..2ff4e2f46 --- /dev/null +++ b/compiler/test/fixtures/asmBinaryNonExisting.p8 @@ -0,0 +1,10 @@ +main { + sub start() { + stuff.do_nothing() + } +} + +stuff $1000 { + romsub $1000 = do_nothing() + %asmbinary "i_do_not_exist.bin", 0 +} \ No newline at end of file diff --git a/compiler/test/fixtures/asmBinaryNonReadable.p8 b/compiler/test/fixtures/asmBinaryNonReadable.p8 new file mode 100644 index 000000000..3b7f63bfc --- /dev/null +++ b/compiler/test/fixtures/asmBinaryNonReadable.p8 @@ -0,0 +1,10 @@ +main { + sub start() { + stuff.do_nothing() + } +} + +stuff $1000 { + romsub $1000 = do_nothing() + %asmbinary "subFolder", 0 +} \ No newline at end of file diff --git a/compiler/test/fixtures/asmIncludeFromSameFolder.p8 b/compiler/test/fixtures/asmIncludeFromSameFolder.p8 new file mode 100644 index 000000000..df25f6e78 --- /dev/null +++ b/compiler/test/fixtures/asmIncludeFromSameFolder.p8 @@ -0,0 +1,13 @@ +%import textio +main { + str myBar = "main.bar" +;foo_bar: +; %asminclude "foo_bar.asm" ; FIXME: should be accessible from inside start() but give assembler error + sub start() { + txt.print(myBar) + txt.print(&foo_bar) + return +foo_bar: + %asminclude "foo_bar.asm" + } +} diff --git a/compiler/test/fixtures/do_nothing.asm b/compiler/test/fixtures/do_nothing.asm new file mode 100644 index 000000000..6bfed22b6 --- /dev/null +++ b/compiler/test/fixtures/do_nothing.asm @@ -0,0 +1,7 @@ +; this is the source code for do_nothing1.bin and subFolder/do_nothing2.bin +; command lines: +; 64tass --ascii --nostart do_nothing.asm --output do_nothing1.bin +; 64tass --ascii --nostart do_nothing.asm --output subFolder/do_nothing2.bin +*=0 + rts + diff --git a/compiler/test/fixtures/do_nothing1.bin b/compiler/test/fixtures/do_nothing1.bin new file mode 100644 index 000000000..64845fb76 --- /dev/null +++ b/compiler/test/fixtures/do_nothing1.bin @@ -0,0 +1 @@ +` \ No newline at end of file diff --git a/compiler/test/fixtures/foo_bar.asm b/compiler/test/fixtures/foo_bar.asm new file mode 100644 index 000000000..d415b6923 --- /dev/null +++ b/compiler/test/fixtures/foo_bar.asm @@ -0,0 +1,2 @@ +bar .text "foo.bar",0 + diff --git a/compiler/test/fixtures/foo_bar.p8 b/compiler/test/fixtures/foo_bar.p8 new file mode 100644 index 000000000..e60fa8199 --- /dev/null +++ b/compiler/test/fixtures/foo_bar.p8 @@ -0,0 +1,3 @@ +foo { + str bar = "foo.bar" +} diff --git a/compiler/test/fixtures/importFromSameFolder.p8 b/compiler/test/fixtures/importFromSameFolder.p8 new file mode 100644 index 000000000..3f233b10d --- /dev/null +++ b/compiler/test/fixtures/importFromSameFolder.p8 @@ -0,0 +1,9 @@ +%import textio +%import foo_bar +main { + str myBar = "main.bar" + sub start() { + txt.print(myBar) + txt.print(foo.bar) + } +} diff --git a/compiler/test/fixtures/subFolder/do_nothing2.bin b/compiler/test/fixtures/subFolder/do_nothing2.bin new file mode 100644 index 000000000..64845fb76 --- /dev/null +++ b/compiler/test/fixtures/subFolder/do_nothing2.bin @@ -0,0 +1 @@ +` \ No newline at end of file