KspCompilationTestRunner.kt

/*
 * Copyright 2020 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package androidx.room.compiler.processing.util.runner

import androidx.room.compiler.processing.ExperimentalProcessingApi
import androidx.room.compiler.processing.SyntheticKspProcessor
import androidx.room.compiler.processing.util.CompilationResult
import androidx.room.compiler.processing.util.KotlinCompilationUtil
import androidx.room.compiler.processing.util.KotlinCompileTestingCompilationResult
import androidx.room.compiler.processing.util.CompilationTestCapabilities
import androidx.room.compiler.processing.util.Source
import com.tschuchort.compiletesting.KotlinCompilation
import com.tschuchort.compiletesting.SourceFile
import com.tschuchort.compiletesting.kspSourcesDir
import com.tschuchort.compiletesting.symbolProcessors
import java.io.ByteArrayOutputStream
import java.io.File
import javax.tools.Diagnostic

@ExperimentalProcessingApi
internal object KspCompilationTestRunner : CompilationTestRunner {

    override val name: String = "ksp"

    override fun canRun(params: TestCompilationParameters): Boolean {
        return CompilationTestCapabilities.canTestWithKsp
    }

    override fun compile(params: TestCompilationParameters): CompilationResult {
        @Suppress("NAME_SHADOWING")
        val sources = if (params.sources.none { it is Source.KotlinSource }) {
            // looks like this requires a kotlin source file
            // see: https://github.com/tschuchortdev/kotlin-compile-testing/issues/57
            params.sources + Source.kotlin("placeholder.kt", "")
        } else {
            params.sources
        }
        val syntheticKspProcessor = SyntheticKspProcessor(params.handlers)

        val combinedOutputStream = ByteArrayOutputStream()
        val kspCompilation = KotlinCompilationUtil.prepareCompilation(
            sources = sources,
            outputStream = combinedOutputStream,
            classpaths = params.classpath
        )
        kspCompilation.symbolProcessors = listOf(syntheticKspProcessor)
        kspCompilation.compile()
        // ignore KSP result for now because KSP stops compilation, which might create false
        // negatives when java code accesses kotlin code.
        // TODO:  fix once https://github.com/tschuchortdev/kotlin-compile-testing/issues/72 is
        //  fixed

        // after ksp, compile without ksp with KSP's output as input
        val finalCompilation = KotlinCompilationUtil.prepareCompilation(
            sources = sources,
            outputStream = combinedOutputStream,
            classpaths = params.classpath,
        )
        // build source files from generated code
        finalCompilation.sources += kspCompilation.kspJavaSourceDir.collectSourceFiles() +
            kspCompilation.kspKotlinSourceDir.collectSourceFiles()
        val result = finalCompilation.compile()
        // workaround for: https://github.com/google/ksp/issues/122
        // KSP does not fail compilation for error diagnostics hence we do it here.
        val hasErrorDiagnostics = syntheticKspProcessor.messageWatcher
            .diagnostics()[Diagnostic.Kind.ERROR].orEmpty().isNotEmpty()
        return KotlinCompileTestingCompilationResult(
            testRunner = this,
            delegate = result,
            processor = syntheticKspProcessor,
            successfulCompilation = result.exitCode == KotlinCompilation.ExitCode.OK &&
                !hasErrorDiagnostics,
            outputSourceDirs = listOf(
                kspCompilation.kspJavaSourceDir,
                kspCompilation.kspKotlinSourceDir
            ),
            rawOutput = combinedOutputStream.toString(Charsets.UTF_8),
        )
    }

    // TODO get rid of these once kotlin compile testing supports two step compilation for KSP.
    //  https://github.com/tschuchortdev/kotlin-compile-testing/issues/72
    private val KotlinCompilation.kspJavaSourceDir: File
        get() = kspSourcesDir.resolve("java")

    private val KotlinCompilation.kspKotlinSourceDir: File
        get() = kspSourcesDir.resolve("kotlin")

    private fun File.collectSourceFiles(): List<SourceFile> {
        return walkTopDown().filter {
            it.isFile
        }.map { file ->
            SourceFile.fromPath(file)
        }.toList()
    }
}