Files
prog8/compiler/test/TestModuleImporter.kt
T
2026-10-03 02:08:57 +02:00

316 lines
16 KiB
Kotlin

package prog8tests.compiler
import com.github.michaelbull.result.getErrorOrElse
import com.github.michaelbull.result.getOrElse
import io.kotest.assertions.throwables.shouldThrow
import io.kotest.assertions.withClue
import io.kotest.core.spec.style.FunSpec
import io.kotest.engine.spec.tempdir
import io.kotest.matchers.collections.shouldBeIn
import io.kotest.matchers.shouldBe
import io.kotest.matchers.string.shouldContain
import io.kotest.matchers.string.shouldStartWith
import prog8.ast.Program
import prog8.ast.SyntaxError
import prog8.code.INTERNED_STRINGS_MODULENAME
import prog8.code.PROG8_CONTAINER_MODULES
import prog8.code.core.IErrorReporter
import prog8.code.source.ImportFileSystem
import prog8.code.source.SourceCode
import prog8.code.target.VMTarget
import prog8.compiler.ModuleImporter
import prog8.parser.MultipleParseErrors
import prog8.parser.Prog8Parser
import prog8tests.helpers.*
import kotlin.io.path.*
class TestModuleImporter: FunSpec({
val count = listOf("1st", "2nd", "3rd", "4th", "5th")
lateinit var program: Program
beforeTest {
program = Program("foo", DummyFunctions, VMTarget())
}
fun makeImporter(errors: IErrorReporter? = null, searchIn: Iterable<String>) =
ModuleImporter(program, VMTarget(), errors ?: ErrorReporterForTests(false), searchIn.toList(), emptyList(), Path(".").absolute(), false)
fun makeImporter(errors: IErrorReporter?, vararg searchIn: String): ModuleImporter {
return makeImporter(errors, searchIn.asList())
}
context("ImportModule") {
context("WithInvalidPath") {
test("testNonexisting") {
val dirRel = assumeDirectory(".", workingDir.relativize(fixturesDir))
val importer = makeImporter(null, dirRel.invariantSeparatorsPathString)
val srcPathRel = assumeNotExists(dirRel, "i_do_not_exist")
val srcPathAbs = srcPathRel.absolute()
val error1 = importer.importMainModule(srcPathRel).getErrorOrElse { error("should have import error") }
withClue(".file should be normalized") {
"${error1.file}" shouldBe "${error1.file.normalize()}"
}
withClue(".file should point to specified path") {
error1.file.absolutePath shouldBe "${srcPathAbs.normalize()}"
}
program.modules.size shouldBe PROG8_CONTAINER_MODULES.size
val error2 = importer.importMainModule(srcPathAbs).getErrorOrElse { error("should have import error") }
withClue(".file should be normalized") {
"${error2.file}" shouldBe "${error2.file.normalize()}"
}
withClue(".file should point to specified path") {
error2.file.absolutePath shouldBe "${srcPathAbs.normalize()}"
}
program.modules.size shouldBe PROG8_CONTAINER_MODULES.size
}
test("testDirectory") {
val srcPathRel = assumeDirectory(workingDir.relativize(fixturesDir))
val srcPathAbs = srcPathRel.absolute()
val searchIn = Path(".", "$srcPathRel").invariantSeparatorsPathString
val importer = makeImporter(null, searchIn)
shouldThrow<FileSystemException> { importer.importMainModule(srcPathRel) }
.let {
withClue(".file should be normalized") {
"${it.file}" shouldBe "${it.file.normalize()}"
}
withClue(".file should point to specified path") {
it.file.absolutePath shouldBe "${srcPathAbs.normalize()}"
}
}
program.modules.size shouldBe PROG8_CONTAINER_MODULES.size
shouldThrow<FileSystemException> { importer.importMainModule(srcPathAbs) }
.let {
withClue(".file should be normalized") {
"${it.file}" shouldBe "${it.file.normalize()}"
}
withClue(".file should point to specified path") {
it.file.absolutePath shouldBe "${srcPathAbs.normalize()}"
}
}
program.modules.size shouldBe PROG8_CONTAINER_MODULES.size
}
}
context("WithValidPath") {
test("testAbsolute") {
val searchIn = listOf(
workingDir.relativize(fixturesDir), // we do want a dot "." in front
).map { it.invariantSeparatorsPathString }
val importer = makeImporter(null, searchIn)
val fileName = "ast_simple_main.p8"
val path = assumeReadableFile(searchIn[0], fileName)
val module = importer.importMainModule(path.absolute()).getOrElse { throw it }
program.modules.size shouldBe PROG8_CONTAINER_MODULES.size+1
module shouldBeIn program.modules
module.program shouldBe program
}
test("testRelativeToWorkingDir") {
val searchIn = listOf(
workingDir.relativize(fixturesDir), // we do want a dot "." in front
).map { it.invariantSeparatorsPathString }
val importer = makeImporter(null, searchIn)
val fileName = "ast_simple_main.p8"
val path = assumeReadableFile(searchIn[0], fileName)
withClue("sanity check: path should NOT be absolute") {
path.isAbsolute shouldBe false
}
val module = importer.importMainModule(path).getOrElse { throw it }
program.modules.size shouldBe PROG8_CONTAINER_MODULES.size+1
module shouldBeIn program.modules
module.program shouldBe program
}
test("testRelativeTo1stDirInSearchList") {
val searchIn = (workingDir.relativize(fixturesDir)).invariantSeparatorsPathString
val importer = makeImporter(null, searchIn)
val fileName = "ast_simple_main.p8"
val path = Path(".", fileName)
assumeReadableFile(searchIn, path)
val module = importer.importMainModule(path).getOrElse { throw it }
program.modules.size shouldBe PROG8_CONTAINER_MODULES.size+1
module shouldBeIn program.modules
module.program shouldBe program
}
context("WithBadFile") {
test("testWithSyntaxError") {
val searchIn = assumeDirectory("./", workingDir.relativize(fixturesDir))
val importer = makeImporter(null, searchIn.invariantSeparatorsPathString)
val srcPath = assumeReadableFile(fixturesDir, "ast_file_with_syntax_error.p8")
val act = { importer.importMainModule(srcPath) }
repeat(2) { n -> withClue(count[n] + " call") {
shouldThrow<MultipleParseErrors> { act() }.let { e ->
e.errors.first().position.file shouldBe SourceCode.relative(srcPath).toString()
withClue("line; should be 1-based") { e.errors.first().position.line shouldBe 2 }
withClue("startCol; should be 1-based") { e.errors.first().position.startCol shouldBe 5 }
withClue("endCol; should be 1-based") { e.errors.first().position.endCol shouldBe 7 }
}
}
program.modules.size shouldBe PROG8_CONTAINER_MODULES.size
}
}
fun doTestImportingFileWithSyntaxError(repetitions: Int) {
val searchIn = assumeDirectory("./", workingDir.relativize(fixturesDir))
val importer = makeImporter(null, searchIn.invariantSeparatorsPathString)
val importing = assumeReadableFile(fixturesDir, "import_file_with_syntax_error.p8")
val imported = assumeReadableFile(fixturesDir, "file_with_syntax_error.p8")
val act = { importer.importMainModule(importing) }
repeat(repetitions) { n -> withClue(count[n] + " call") {
shouldThrow<MultipleParseErrors> { act() }.let { e ->
e.errors.first().position.file shouldBe SourceCode.relative(imported).toString()
withClue("line; should be 1-based") { e.errors.first().position.line shouldBe 2 }
withClue("startCol; should be 1-based") { e.errors.first().position.startCol shouldBe 5 }
withClue("endCol; should be 1-based") { e.errors.first().position.endCol shouldBe 7 }
}
}
withClue("imported module with error in it should not be present") { program.modules.size shouldBe PROG8_CONTAINER_MODULES.size }
program.modules.size shouldBe PROG8_CONTAINER_MODULES.size
program.modules[0].name shouldBe INTERNED_STRINGS_MODULENAME
}
}
test("testImportingFileWithSyntaxError_once") {
doTestImportingFileWithSyntaxError(1)
}
test("testImportingFileWithSyntaxError_twice") {
doTestImportingFileWithSyntaxError(2)
}
}
context("WithBadImportArgument") {
test("import directive with expression argument gives SyntaxError instead of crashing") {
val dir = tempdir().toPath()
val srcPath = dir.resolve("badimport.p8")
srcPath.toFile().writeText("""
%import "textio" as t
main {
sub start() {
}
}
""".trimIndent())
val importer = makeImporter(null, dir.invariantSeparatorsPathString)
shouldThrow<SyntaxError> { importer.importMainModule(srcPath) }
.message shouldContain "invalid import directive"
program.modules.size shouldBe PROG8_CONTAINER_MODULES.size
}
}
}
}
context("ImportFromResourceModule") {
test("neighbor directory search is skipped for modules loaded from internal library resources") {
val importer = makeImporter(null)
val resourceModule = Prog8Parser.parseModule(ImportFileSystem.getResource("/prog8lib/cx16/textio.p8"), VMTarget())
resourceModule.position.file shouldStartWith "library:"
val (result, searched) = importer.getModuleFromFile("syslib", resourceModule)
withClue("syslib only exists as an internal library resource, not on the filesystem") {
result.getErrorOrElse { error("expected a filesystem search failure") }.file.name shouldBe "syslib"
}
withClue("no filesystem search location may be derived from the library: origin: $searched") {
searched.none { it.toString().contains("library:") } shouldBe true
}
}
}
context("ImportLibraryModule") {
context("WithInvalidName") {
test("testWithNonExistingName") {
val searchIn = assumeDirectory("./", workingDir.relativize(fixturesDir))
val errors = ErrorReporterForTests(false)
val importer = makeImporter(errors, searchIn.invariantSeparatorsPathString)
val filenameNoExt = assumeNotExists(fixturesDir, "i_do_not_exist").name
val filenameWithExt = assumeNotExists(fixturesDir, "i_do_not_exist.p8").name
repeat(2) { n ->
val result = importer.importImplicitLibraryModule(filenameNoExt)
withClue(count[n] + " call / NO .p8 extension") { result shouldBe null }
withClue(count[n] + " call / NO .p8 extension") { errors.noErrors() shouldBe false }
errors.errors.single() shouldContain "0:0: no module found with name i_do_not_exist"
errors.report()
program.modules.size shouldBe PROG8_CONTAINER_MODULES.size
val result2 = importer.importImplicitLibraryModule(filenameWithExt)
withClue(count[n] + " call / with .p8 extension") { result2 shouldBe null }
withClue(count[n] + " call / with .p8 extension") { importer.errors.noErrors() shouldBe false }
errors.errors.single() shouldContain "0:0: no module found with name i_do_not_exist.p8"
errors.report()
program.modules.size shouldBe PROG8_CONTAINER_MODULES.size
}
}
}
context("WithValidName") {
context("WithBadFile") {
test("testWithSyntaxError") {
val searchIn = assumeDirectory("./", workingDir.relativize(fixturesDir))
val importer = makeImporter(null, searchIn.invariantSeparatorsPathString)
val srcPath = assumeReadableFile(fixturesDir, "ast_file_with_syntax_error.p8")
repeat(2) { n -> withClue(count[n] + " call") {
shouldThrow<MultipleParseErrors> {
importer.importImplicitLibraryModule(srcPath.nameWithoutExtension)
}.let { e ->
e.errors.first().position.file shouldBe SourceCode.relative(srcPath).toString()
withClue("line; should be 1-based") { e.errors.first().position.line shouldBe 2 }
withClue("startCol; should be 1-based") { e.errors.first().position.startCol shouldBe 5 }
withClue("endCol; should be 1-based") { e.errors.first().position.endCol shouldBe 7 }
}
}
program.modules.size shouldBe PROG8_CONTAINER_MODULES.size
}
}
fun doTestImportingFileWithSyntaxError(repetitions: Int) {
val searchIn = assumeDirectory("./", workingDir.relativize(fixturesDir))
val importer = makeImporter(null, searchIn.invariantSeparatorsPathString)
val importing = assumeReadableFile(fixturesDir, "import_file_with_syntax_error.p8")
val imported = assumeReadableFile(fixturesDir, "file_with_syntax_error.p8")
val act = { importer.importImplicitLibraryModule(importing.nameWithoutExtension) }
repeat(repetitions) { n -> withClue(count[n] + " call") {
shouldThrow<MultipleParseErrors> {
act() }.let { e ->
e.errors.first().position.file shouldBe SourceCode.relative(imported).toString()
withClue("line; should be 1-based") { e.errors.first().position.line shouldBe 2 }
withClue("startCol; should be 1-based") { e.errors.first().position.startCol shouldBe 5 }
withClue("endCol; should be 1-based") { e.errors.first().position.endCol shouldBe 7 }
}
}
withClue("imported module with error in it should not be present") { program.modules.size shouldBe PROG8_CONTAINER_MODULES.size }
program.modules[0].name shouldBe INTERNED_STRINGS_MODULENAME
importer.errors.report()
}
}
test("testImportingFileWithSyntaxError_once") {
doTestImportingFileWithSyntaxError(1)
}
test("testImportingFileWithSyntaxError_twice") {
doTestImportingFileWithSyntaxError(2)
}
}
}
}
})