Files
prog8/compiler/test/TestOutputFileName.kt
T

280 lines
12 KiB
Kotlin

package prog8tests.compiler
import io.kotest.assertions.withClue
import io.kotest.core.spec.style.FunSpec
import io.kotest.engine.spec.tempdir
import io.kotest.matchers.shouldBe
import io.kotest.matchers.shouldNotBe
import prog8.code.core.*
import prog8.code.target.C64Target
import prog8.code.target.Cx16Target
import prog8.code.target.VMTarget
import prog8.compiler.CompilationResult
import prog8.compiler.CompilerArguments
import prog8.compiler.compileProgram
import prog8tests.helpers.ErrorReporterForTests
import prog8tests.helpers.assumeReadableFile
import prog8tests.helpers.fixturesDir
import java.nio.file.Files
import java.nio.file.Path
import kotlin.io.path.*
private val minimalProgram = """
%zeropage basicsafe
%option no_sysinit
main {
sub start() {
cx16.r0++
}
}
"""
private fun writeSourceTo(dir: Path, source: String): Path {
val filepath = dir.resolve("testoutput.p8")
filepath.toFile().writeText(source)
return filepath
}
// local copy of the compileTheThing pattern: the shared compileText/compileFile helpers cannot pass an outputFile
private fun compileWithOutputFile(
filepath: Path,
target: ICompilationTarget,
outputDir: Path,
outputFile: Path,
assemble: Boolean = false
): CompilationResult? {
val args = CompilerArguments(
filepath,
optimize = false,
writeAssembly = true,
warnSymbolShadowing = false,
warnImplicitTypeCasts = false,
quietAll = true,
quietAssembler = true,
showTimings = false,
asmListfile = false,
includeSourcelines = false,
newCodegen = false,
dumpVariables = false,
dumpSymbols = false,
varsHighBank = null,
varsGolden = false,
varsAddress = null,
compilationTarget = target.name,
breakpointCpuInstruction = null,
printAst1 = false,
printAst2 = false,
ignoreFootguns = false,
profilingInstrumentation = false,
symbolDefs = emptyMap(),
outputDir = outputDir,
outputFile = outputFile,
errors = ErrorReporterForTests(),
assemble = assemble
)
return compileProgram(args)
}
class TestOutputFileName : FunSpec({
test("outputStemOf strips the last extension") {
outputStemOf(Path.of("game.pgz")) shouldBe "game"
outputStemOf(Path.of("my.game")) shouldBe "my"
}
test("outputStemOf keeps names without an extension unchanged") {
outputStemOf(Path.of("game")) shouldBe "game"
}
test("outputStemOf treats a leading dot as not an extension") {
outputStemOf(Path.of(".pgz")) shouldBe ".pgz"
}
test("programFile maps each OutputType to its legacy extension") {
val outputDir = tempdir().toPath()
fun options(output: OutputType) = CompilationOptions.builder(C64Target())
.output(output)
.outputDir(outputDir)
.build()
options(OutputType.PRG).programFile("prog") shouldBe outputDir.resolve("prog.prg")
options(OutputType.XEX).programFile("prog") shouldBe outputDir.resolve("prog.xex")
options(OutputType.RAW).programFile("prog") shouldBe outputDir.resolve("prog.bin")
options(OutputType.LIBRARY).programFile("prog") shouldBe outputDir.resolve("prog.bin")
options(OutputType.ELF).programFile("prog") shouldBe outputDir.resolve("prog.elf")
options(OutputType.AMIGAHUNK).programFile("prog") shouldBe outputDir.resolve("prog")
}
test("programFile special-cases VMTarget to the p8ir artifact") {
val outputDir = tempdir().toPath()
val options = CompilationOptions.builder(VMTarget())
.output(OutputType.ELF)
.outputDir(outputDir)
.build()
options.programFile("prog") shouldBe outputDir.resolve("prog.p8ir")
}
test("programFile enforces the .p8ir extension for -o on the virtual target") {
val tmp = tempdir().toPath()
val builder = { ir: Path ->
CompilationOptions.builder(VMTarget()).outputDir(tmp).outputFile(ir).build()
}
// the VM identifies the IR by its extension, so an extension-less -o must get one appended;
// only the exact lowercase ".p8ir" is recognized as already being an IR file
builder(tmp.resolve("game")).programFile("prog") shouldBe tmp.resolve("game.p8ir")
builder(tmp.resolve("game.p8ir")).programFile("prog") shouldBe tmp.resolve("game.p8ir")
builder(tmp.resolve("game.P8IR")).programFile("prog") shouldBe tmp.resolve("game.P8IR.p8ir")
builder(tmp.resolve("sub/game")).programFile("prog") shouldBe tmp.resolve("sub/game.p8ir")
// ... and the stem must follow the resolved name, so the aux files stay consistent with the artifact
OutputFiles.of(builder(tmp.resolve("game")), "prog").ir() shouldBe tmp.resolve("game.p8ir")
OutputFiles.of(builder(tmp.resolve("game")), "prog").stem shouldBe "game"
}
test("OutputFiles.of with -o resolves the artifact verbatim and all aux files from its stem") {
val tmp = tempdir().toPath()
val outDir = tmp.resolve("out")
val options = CompilationOptions.builder(Cx16Target())
.outputDir(tmp)
.outputFile(outDir.resolve("custom.pgz"))
.build()
val outs = OutputFiles.of(options, "prog")
outs.programFile shouldBe outDir.resolve("custom.pgz")
outs.stem shouldBe "custom"
outs.dir shouldBe outDir
outs.asm() shouldBe outDir.resolve("custom.asm")
outs.list() shouldBe outDir.resolve("custom.list")
outs.viceMonList() shouldBe outDir.resolve("custom.vice-mon-list")
outs.binFile() shouldBe outDir.resolve("custom.bin")
outs.ir() shouldBe outDir.resolve("custom.p8ir")
outs.bankedCalls() shouldBe outDir.resolve("custom.bankedcalls")
outs.obj() shouldBe outDir.resolve("custom.o")
outs.linkScript() shouldBe outDir.resolve("custom.link.ld")
}
test("viceMonList is derived from the stem, never from the program file name") {
val tmp = tempdir().toPath()
val outDir = tmp.resolve("out")
val options = CompilationOptions.builder(Cx16Target())
.outputDir(tmp)
.outputFile(outDir.resolve("custom.pgz"))
.build()
val outs = OutputFiles.of(options, "prog")
withClue("vice-mon-list must not keep the program file extension") {
outs.viceMonList() shouldNotBe outDir.resolve("custom.pgz.vice-mon-list")
}
}
test("OutputFiles.of with a bare -o filename resolves aux files relative to outputDir") {
val tmp = tempdir().toPath()
val options = CompilationOptions.builder(Cx16Target())
.outputDir(tmp)
.outputFile(Path.of("game.pgz"))
.build()
val outs = OutputFiles.of(options, "prog")
outs.programFile shouldBe Path.of("game.pgz")
outs.dir shouldBe tmp
outs.asm() shouldBe tmp.resolve("game.asm")
outs.ir() shouldBe tmp.resolve("game.p8ir")
}
test("legacy naming keeps the full dotted program stem when -o is not used") {
val tmp = tempdir().toPath()
val options = CompilationOptions.builder(Cx16Target()).outputDir(tmp).build()
val outs = OutputFiles.of(options, "my.game")
withClue("legacy stem must be used verbatim, not re-derived by outputStemOf") {
outs.stem shouldBe "my.game"
outs.asm() shouldBe tmp.resolve("my.game.asm")
outs.list() shouldBe tmp.resolve("my.game.list")
outs.viceMonList() shouldBe tmp.resolve("my.game.vice-mon-list")
outs.ir() shouldBe tmp.resolve("my.game.p8ir")
outs.bankedCalls() shouldBe tmp.resolve("my.game.bankedcalls")
outs.obj() shouldBe tmp.resolve("my.game.o")
outs.linkScript() shouldBe tmp.resolve("my.game.link.ld")
}
outs.asm() shouldNotBe tmp.resolve("my.asm")
}
test("legacy program file path is unchanged without -o") {
val tmp = tempdir().toPath()
val options = CompilationOptions.builder(Cx16Target()).outputDir(tmp).build()
OutputFiles.of(options, "prog").programFile shouldBe tmp.resolve("prog.prg")
}
test("compiling with -o writes the aux files next to the artifact") {
val outputDir = tempdir().toPath()
val outDir = outputDir.resolve("out")
outDir.createDirectories()
val outputFile = outDir.resolve("custom.pgz")
val filepath = writeSourceTo(outputDir, minimalProgram)
val result = compileWithOutputFile(filepath, Cx16Target(), outputDir, outputFile)
result shouldNotBe null
withClue("no assembler ran, so the artifact must not exist yet") {
outputFile.exists() shouldBe false
}
withClue("the aux .asm must be named after the -o stem") {
outDir.resolve("custom.asm").exists() shouldBe true
}
withClue("no source-named .asm may appear in the output directory") {
outDir.resolve("testoutput.asm").exists() shouldBe false
val topLevelAsms = Files.list(outputDir).use { stream ->
stream.filter { it.name.endsWith(".asm") }.toList()
}
topLevelAsms shouldBe emptyList()
}
result!!.compilationOptions.outputFile shouldBe outputFile
}
test("compiling with -o and assemble=true writes the final artifact to the -o path") {
val outputDir = tempdir().toPath()
val outDir = outputDir.resolve("out")
outDir.createDirectories()
val outputFile = outDir.resolve("custom.prg")
val filepath = writeSourceTo(outputDir, minimalProgram)
val result = compileWithOutputFile(filepath, Cx16Target(), outputDir, outputFile, assemble = true)
result shouldNotBe null
withClue("the assembled artifact must exist at the -o path") {
outputFile.exists() shouldBe true
outputFile.isRegularFile() shouldBe true
}
withClue("the aux .asm must be named after the -o stem") {
outDir.resolve("custom.asm").exists() shouldBe true
}
}
test("%asmbinary under -o keeps the .binary path relative to the generated .asm") {
val outputDir = tempdir().toPath()
val outputFile = outputDir.resolve("game.prg")
val p8Path = assumeReadableFile(fixturesDir, "asmBinaryFromSameFolder.p8")
val result = compileWithOutputFile(p8Path, Cx16Target(), outputDir, outputFile)
result shouldNotBe null
val asmPath = OutputFiles.of(result!!.compilationOptions, result.compilerAst.name).asm()
asmPath.exists() shouldBe true
val asmLines = asmPath.readLines().map { it.trim() }
val binaryLine = asmLines.first { it.contains(".binary") }
val binPath = binaryLine.substringAfter(".binary \"").substringBefore('"')
withClue("the .binary path must be relative to the .asm file, not absolute: $binaryLine") {
Path.of(binPath).isAbsolute shouldBe false
}
}
test("virtual target -o writes the .p8ir artifact") {
val outputDir = tempdir().toPath()
val outputFile = outputDir.resolve("ir.p8ir")
val filepath = writeSourceTo(outputDir, minimalProgram)
val result = compileWithOutputFile(filepath, VMTarget(), outputDir, outputFile, assemble = true)
result shouldNotBe null
outputFile.exists() shouldBe true
outputFile.isRegularFile() shouldBe true
}
test("virtual target -o without extension still writes a .p8ir artifact") {
val outputDir = tempdir().toPath()
val filepath = writeSourceTo(outputDir, minimalProgram)
val result = compileWithOutputFile(filepath, VMTarget(), outputDir, outputDir.resolve("ir"), assemble = true)
result shouldNotBe null
outputDir.resolve("ir.p8ir").exists() shouldBe true
outputDir.resolve("ir").exists() shouldBe false
}
})