All Downloads are FREE. Search and download functionalities are using the official Maven repository.

stioner.plugin.2024.11.0.source-code.QuestionerPlugin.kt Maven / Gradle / Ivy

package edu.illinois.cs.cs125.questioner.plugin

import com.fasterxml.jackson.annotation.JsonIgnoreProperties
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory
import com.fasterxml.jackson.module.kotlin.readValue
import com.fasterxml.jackson.module.kotlin.registerKotlinModule
import com.github.sherter.googlejavaformatgradleplugin.GoogleJavaFormatExtension
import com.github.sherter.googlejavaformatgradleplugin.GoogleJavaFormatPlugin
import edu.illinois.cs.cs125.questioner.lib.VERSION
import io.github.cdimascio.dotenv.Dotenv
import io.gitlab.arturbosch.detekt.DetektPlugin
import io.gitlab.arturbosch.detekt.extensions.DetektExtension
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.logging.LogLevel
import org.gradle.api.plugins.JavaPluginExtension
import org.gradle.api.tasks.Delete
import org.gradle.api.tasks.SourceTask
import org.gradle.api.tasks.testing.Test
import org.gradle.jvm.toolchain.JavaLanguageVersion
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
import org.jmailen.gradle.kotlinter.KotlinterPlugin
import java.net.URI
import java.util.Locale

@JsonIgnoreProperties(ignoreUnknown = true)
data class QuestionerConfig(val endpoints: List = listOf()) {
    data class EndPoint(val name: String, val token: String, val url: String, val label: String? = null)
}

val dotenv: Dotenv = Dotenv.configure().ignoreIfMissing().load()

private val testFiles = listOf("TestAllQuestions.kt", "TestUnvalidatedQuestions.kt", "TestFocusedQuestions.kt")

@Suppress("unused")
class QuestionerPlugin : Plugin {
    private fun Project.configurePlugins() {
        pluginManager.apply("java")
        extensions.getByType(JavaPluginExtension::class.java).apply {
            toolchain.apply {
                languageVersion.set(JavaLanguageVersion.of(21))
            }
            sourceSets.getByName("test").java.srcDirs(layout.buildDirectory.dir("questioner").get().asFile)
        }

        buildscript.dependencies.add("classpath", "org.jetbrains.kotlin:kotlin-gradle-plugin:2.0.0")
        pluginManager.apply("org.jetbrains.kotlin.jvm")

        pluginManager.apply("checkstyle")

        pluginManager.apply(GoogleJavaFormatPlugin::class.java)
        extensions.getByType(GoogleJavaFormatExtension::class.java).apply {
            toolVersion = "1.22.0"
        }

        pluginManager.apply(DetektPlugin::class.java)
        extensions.getByType(DetektExtension::class.java).apply {
            buildUponDefaultConfig = true
        }

        pluginManager.apply(KotlinterPlugin::class.java)
    }

    private fun Project.finalizeConfiguration() {
        project.configurations.getByName("implementation").dependencies.find { dependency ->
            (dependency.group == "org.cs124" && dependency.name == "questioner") || (dependency.group == "org.cs124.questioner")
        }?.let {
            error("Found explicit questioner library dependency. Please remove it, since it is automatically added by the plugin.")
        }
        project.dependencies.add("implementation", project.dependencies.create("org.cs124.questioner:lib:$VERSION"))

        tasks.withType(SourceTask::class.java) { sourceTask ->
            sourceTask.exclude("**/.question.json")
            sourceTask.exclude("**/report.html")
            sourceTask.exclude("questions.json", "packageMap.json", *testFiles.toTypedArray())
        }
        tasks.withType(Test::class.java).forEach { testTask ->
            testTask.dependsOn("generateQuestionTests")
        }

        tasks.withType(KotlinCompile::class.java) { kompileTask ->
            kompileTask.compilerOptions.apply {
                jvmTarget.set(JvmTarget.JVM_21)
            }
        }

        tasks.getByName("check")
            .dependsOn("detekt", "checkstyleMain", "googleJavaFormat", "lintKotlinMain", "formatKotlinMain")
        tasks.getByName("checkstyleMain").mustRunAfter("googleJavaFormat")
        tasks.getByName("saveQuestions")
            .mustRunAfter("detekt", "checkstyleMain", "googleJavaFormat", "lintKotlinMain", "formatKotlinMain")

        project.tasks.getByName("compileJava").mustRunAfter("reconfigureForTesting")
        project.tasks.getByName("compileKotlin").mustRunAfter("reconfigureForTesting")
        project.tasks.getByName("jar").mustRunAfter("reconfigureForTesting")

        val agentJarPath = configurations.getByName("runtimeClasspath")
            .resolvedConfiguration.resolvedArtifacts
            .find { artifact ->
                artifact.moduleVersion.id.group == "com.beyondgrader.resource-agent" &&
                    artifact.moduleVersion.id.name == "agent"
            }!!.file.absolutePath
        tasks.withType(Test::class.java) { testTask ->
            testTask.useJUnitPlatform()
            testTask.enableAssertions = true
            testTask.environment["JEED_USE_CACHE"] = true
            @Suppress("SpellCheckingInspection")
            testTask.jvmArgs(
                "-ea", "--enable-preview", "-Dfile.encoding=UTF-8", "-Djava.security.manager=allow",
                "-XX:+UseZGC", "-XX:ZCollectionInterval=8", "-XX:-OmitStackTraceInFastThrow",
                "-Xmx4G",
                "--add-opens", "java.base/java.lang=ALL-UNNAMED",
                "--add-opens", "java.base/java.util=ALL-UNNAMED",
                "--add-exports", "jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED",
                "--add-exports", "jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED",
                "--add-exports", "jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED",
                "--add-exports", "jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED",
                "--add-exports", "jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED",
                "--add-exports", "java.management/sun.management=ALL-UNNAMED",
                "-Dslf4j.internal.verbosity=WARN",
                "-javaagent:$agentJarPath",
            )
            testTask.outputs.upToDateWhen { false }
            testTask.dependsOn("reconfigureForTesting")
            testTask.finalizedBy("recollectQuestions")
            testTask.logging.captureStandardError(LogLevel.DEBUG)
        }

        configurations.getByName("checkstyle").apply {
            resolutionStrategy.capabilitiesResolution.withCapability("com.google.collections:google-collections") {
                it.select("com.google.guava:guava:0")
            }
        }
    }

    override fun apply(project: Project) {
        project.configurePlugins()

        val config = project.extensions.create("questioner", QuestionerConfigExtension::class.java)

        project.repositories.add(project.repositories.mavenCentral())
        project.repositories.add(project.repositories.mavenLocal())
        project.repositories.add(
            project.repositories.maven { mavenRepository ->
                mavenRepository.url = URI("https://maven.codeawakening.com")
            },
        )

        project.tasks.register("checkQuestionerVersion", CheckQuestionerVersion::class.java) { checkQuestionerVersion ->
            checkQuestionerVersion.outputs.upToDateWhen { false }
        }

        project.tasks.register("buildPackageMap", BuildPackageMap::class.java) { buildPackageMap ->
            buildPackageMap.dependsOn("checkQuestionerVersion")
        }

        project.tasks.register("cleanQuestions", Delete::class.java) { cleanQuestions ->
            cleanQuestions.delete(
                project.extensions.getByType(JavaPluginExtension::class.java)
                    .sourceSets.getByName("main").allSource
                    .filter { file -> file.name == ".validation.json" || file.name == "report.html" || file.name == ".question.json" },
            )
        }
        project.tasks.getByName("clean").dependsOn("cleanQuestions")

        project.tasks.register("saveQuestions", SaveQuestions::class.java) { saveQuestions ->
            saveQuestions.dependsOn("buildPackageMap")
            saveQuestions.mustRunAfter("cleanQuestions")
        }

        project.tasks.register("reconfigureForTesting") {
            project.tasks.getByName("compileJava").enabled = false
            project.tasks.getByName("compileKotlin").enabled = false
            project.tasks.getByName("jar").enabled = false
        }

        project.tasks.register("recollectQuestions", CollectQuestions::class.java) { recollectQuestions ->
            recollectQuestions.dependsOn("saveQuestions")
        }

        listOf("testAllQuestions", "testUnvalidatedQuestions", "testFocusedQuestions").map { testName ->
            project.tasks.create(testName, Test::class.java) { testTask ->
                testTask.setTestNameIncludePatterns(listOf(testName.capitalized()))
            }
        }
        project.tasks.getByName("test") { testTask ->
            testTask as Test
            testTask.setTestNameIncludePatterns(listOf("TestUnvalidatedQuestions"))
        }

        project.tasks.register("collectQuestions", CollectQuestions::class.java) { collectQuestions ->
            collectQuestions.dependsOn("saveQuestions")
            collectQuestions.outputs.upToDateWhen { false }
        }

        project.tasks.register("generateQuestionTests", GenerateQuestionTests::class.java) { generateQuestionTests ->
            generateQuestionTests.dependsOn("collectQuestions")
            project.tasks.getByName("compileTestKotlin").dependsOn(generateQuestionTests)
        }

        val uploadConfiguration = project.file(".questioner.yaml").let { questionerConfigFile ->
            if (questionerConfigFile.exists()) {
                try {
                    ObjectMapper(YAMLFactory()).apply { registerKotlinModule() }.readValue(questionerConfigFile)
                } catch (e: Exception) {
                    project.logger.warn("Invalid questioner.yaml file.")
                    QuestionerConfig()
                }
            } else {
                QuestionerConfig()
            }
        }

        val publishingTasks = uploadConfiguration.endpoints.map { endpoint ->
            project.tasks.register(
                "publishQuestionsTo${endpoint.name}",
                PublishQuestions::class.java,
            ) { publishQuestions ->
                publishQuestions.endpoint = endpoint
                publishQuestions.dependsOn("collectQuestions", "recollectQuestions")
                publishQuestions.outputs.upToDateWhen { false }
                publishQuestions.description = "Publish questions to ${endpoint.name} (${endpoint.url})"
            }.get()
        }

        if (uploadConfiguration.endpoints.isNotEmpty()) {
            project.tasks.register("publishQuestions") { publishAll ->
                if (uploadConfiguration.endpoints.isNotEmpty()) {
                    publishAll.dependsOn(publishingTasks)
                } else {
                    publishAll.doLast {
                        error("No publishing configuration found. Please add a .questioner.yaml file to the root of your repository.")
                    }
                }
                publishAll.outputs.upToDateWhen { false }
            }
        }

        val dumpTasks = uploadConfiguration.endpoints.map { endpoint ->
            project.tasks.register("dumpQuestionsTo${endpoint.name}", DumpQuestions::class.java) { dumpQuestions ->
                dumpQuestions.endpoint = endpoint
                dumpQuestions.dependsOn("collectQuestions")
                dumpQuestions.outputs.upToDateWhen { false }
                dumpQuestions.description =
                    "Dump questions that would be published to ${endpoint.name} (${endpoint.url})"
            }.get()
        }

        val printSlowQuestions =
            project.tasks.register("printSlowQuestions", PrintSlowQuestions::class.java) { printSlowQuestions ->
                printSlowQuestions.dependsOn("collectQuestions")
                printSlowQuestions.outputs.upToDateWhen { false }
            }.get()

        val showUpdatedSeeds =
            project.tasks.register("showUpdatedSeeds", ShowUpdatedSeeds::class.java) { showUpdatedSeeds ->
                showUpdatedSeeds.dependsOn("collectQuestions")
                showUpdatedSeeds.outputs.upToDateWhen { false }
            }.get()

        project.afterEvaluate {
            project.finalizeConfiguration()

            project.tasks.withType(GenerateQuestionTests::class.java) { generateQuestionTests ->
                generateQuestionTests.maxMutationCount = config.maxMutationCount
                generateQuestionTests.retries = config.retries
                generateQuestionTests.verbose = config.verbose
                generateQuestionTests.shuffleTests = config.shuffleTests
            }

            publishingTasks.forEach { task ->
                task.publishIncludes = config.publishIncludes
                task.ignorePackages = config.ignorePackages
            }
            dumpTasks.forEach { task ->
                task.publishIncludes = config.publishIncludes
                task.ignorePackages = config.ignorePackages
            }

            printSlowQuestions.ignorePackages = config.ignorePackages
            showUpdatedSeeds.ignorePackages = config.ignorePackages
        }
    }
}

private fun String.capitalized() = replaceFirstChar { firstChar ->
    when {
        firstChar.isLowerCase() -> firstChar.titlecase(Locale.getDefault())
        else -> firstChar.toString()
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy