From 6478154d0c144f6465ada0c7abbf13443a54f23f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sergej=20Ko=C5=A1=C4=8Dejev?= Date: Mon, 25 Sep 2023 16:53:09 +0200 Subject: [PATCH] 1.19: Add MpsCheck task --- CHANGELOG.md | 7 + README.md | 99 ++++++++--- api/mps-gradle-plugin.api | 20 +++ build.gradle.kts | 6 +- gradle.lockfile | 1 + settings.gradle.kts | 4 + .../de/itemis/mps/gradle/common.gradle.kts | 8 +- .../de/itemis/mps/gradle/tasks/MpsCheck.kt | 160 ++++++++++++++++++ .../de/itemis/mps/gradle/tasks/PluginIds.kt | 121 +++++++++++++ .../de/itemis/mps/gradle/MpsCheckTaskTest.kt | 128 ++++++++++++++ .../de/itemis/mps/gradle/PluginIdsTest.kt | 73 ++++++++ 11 files changed, 606 insertions(+), 21 deletions(-) create mode 100644 src/main/kotlin/de/itemis/mps/gradle/tasks/MpsCheck.kt create mode 100644 src/main/kotlin/de/itemis/mps/gradle/tasks/PluginIds.kt create mode 100644 src/test/kotlin/test/de/itemis/mps/gradle/MpsCheckTaskTest.kt create mode 100644 src/test/kotlin/test/de/itemis/mps/gradle/PluginIdsTest.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index a15a7248..ededa044 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## 1.19 + +### Added + +- `MpsCheck` task for running the model checker. In contrast to the `modelcheck` plugin there can be multiple instances + of the task, and the task is written with the current Gradle best practices (lazy properties, caching). + ## 1.18 ### Changed diff --git a/README.md b/README.md index daae267f..ef2c13df 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,41 @@ Miscellaneous tasks that were found useful when building MPS-based projects with Gradle. +**Table of Contents** + + +* [mps-gradle-plugin](#mps-gradle-plugin) +* [Configuring the plugin repository](#configuring-the-plugin-repository) +* [Custom tasks](#custom-tasks) + * [RunAntScript](#runantscript) + * [Usage](#usage-) + * [Providing Global Defaults For Class Path And Arguments](#providing-global-defaults-for-class-path-and-arguments) + * [Providing Global Defaults For The Java Executable](#providing-global-defaults-for-the-java-executable) + * [Incremental Builds](#incremental-builds) + * [CreateDmg](#createdmg) + * [Usage](#usage) + * [Operation](#operation) + * [BundleMacosJdk](#bundlemacosjdk) + * [Usage](#usage-1) + * [Operation](#operation-1) + * [GenerateLibrariesXml](#generatelibrariesxml) + * [Usage](#usage-2) + * [Operation](#operation-2) + * [`generate`](#generate) + * [Usage](#usage-3) + * [`checkModels`](#checkmodels) + * [Usage](#usage-4) + * [Additional Plugins](#additional-plugins) + * [Additional Plugins](#additional-plugins-) + * [Run migrations](#run-migrations) + * [Usage](#usage-5) + * [Download JetBrains Runtime](#download-jetbrains-runtime) + * [Usage](#usage-6) + * [Parameters](#parameters) + * [Custom MPS Distribution](#custom-mps-distribution) + * [MPS vs IDEA environment](#mps-vs-idea-environment) + + # Configuring the plugin repository This plugin is not published to the Gradle plugin portal but to a public repository of itemis. To configure this @@ -211,7 +246,7 @@ Each property represents an entry in `destination` (a project library), where the property name is the library name and the property value is the path to the library. -## Generate +## `generate` Generate a specific or all models in a project without the need for a MPS model. @@ -279,12 +314,12 @@ Parameters: * `maxHeap` (since 1.15) - maximum heap size setting for the JVM that executes the generator. The value is a string understood by the JVM command line argument `-Xmx` e.g. `3G` or `512M`. -## Model Check +## `checkModels` Run the model check on a subset or all models in a project directly from gradle. -This functionality currently runs all model checks (typesystem, structure, constrains, etc.) from gralde. By default if -any of checks fails the complete build is failed. All messages (Info, Warning or Error) are reported through log4j to +This functionality currently runs all model checks (typesystem, structure, constrains, etc.) from Gradle. By default, if +any of checks fails, the complete build is failed. All messages (Info, Warning or Error) are reported through log4j to the command line. ### Usage @@ -358,7 +393,7 @@ Parameters: ### Additional Plugins -By default only the minimum required set of plugins are loaded. This includes base language and some utilities like the +By default, only the minimum required set of plugins is loaded. This includes base language and some utilities like the HTTP server from MPS. If your project requires additional plugins to be loaded this is done by setting plugin location to the place where your jar files are placed and adding your plugin id and folder name to the `plugins` list: @@ -378,27 +413,53 @@ modelcheck { Dependencies of the specified plugins are automatically loaded from the `pluginLocation` and the plugins directory of MPS. If they are not found the build will fail. -### Additional Plugins +## `MpsCheck` Task Type -By default only the minimum required set of plugins are loaded. This includes base language and some utilities like the -HTTP server from MPS. If your project requires additional plugins to be loaded this is done by setting plugin location -to the place where your jar files are placed and adding your plugin id and folder name to the `plugins` list: +This task improves over the `modelcheck` plugin described above and fixes some of its deficiencies. -``` -apply plugin: 'modelcheck' -... +The `modelcheck` extension provided by the eponymous plugin can only be configured once per Gradle project. Checking +multiple subprojects is not possible without resorting to tricks. In addition, the extension only has limited support +for lazy configuration and does not support Gradle build cache. -modelcheck { - pluginLocation.set(new File("path/to/my/plugins")) - plugins.set([new Plugin("com.mbeddr.core", "mbeddr.core")]) - projectLocation.set(new File("./mps-prj")) - mpsConfig.set(configurations.mps) +The `MpsCheck` task works similarly to the `checkmodels` task of `modelcheck` plugin but allows defining multiple +instances of itself, supports lazy configuration and caching. + +### Usage + +```groovy +import de.itemis.mps.gradle.tasks.MpsCheck + +plugins { + // Required in order to use the MpsCheck task + id("de.itemis.mps.gradle.common") } +tasks.register('checkProject', MpsCheck) { + mpsHome = file("...") // MPS home directory + projectLocation = projectDir +} ``` -Dependencies of the specified plugins are automatically loaded from the `pluginlocation` and the plugins directory of -MPS. If they are not found the the build will fail. +Parameters: + +* `projectLocation` - the location of the project to check. Default is the Gradle project directory. +* `models`, `modules`, `excludeModels`, `excludeModules` - regular expressions. Matching modules and models will be + included or excluded from checking. +* `additionalModelcheckBackendClasspath` - any extra libraries that should be on the classpath of the modelcheck + backend. +* `folderMacros` - path variables/macros that are necessary to open the project. Path macros are not considered part of + Gradle build cache key. +* `varMacros` - non-path variables/macros that are necessary to open the project. Variable macros *are* considered part + of Gradle build cache key. +* `junitFile` - the JUnit XML file to produce. Defaults to `$buildDir/TEST-${task.name}.xml` +* `junitFormat` - the format of the JUnit XML file. Defaults to `module-and-model`. +* `mpsHome` - the home directory of the MPS distribution (or RCP) to use for testing. +* `mpsVersion` - the MPS version, such as "2021.3". Autodetected by reading `$mpsHome/build.properties` by default. +* `pluginRoots` - directories containing additional plugins to load +* `warningAsError` - whether to treat warnings as errors. +* `ignoreFailures` (inherited from `VerificationTask`) - whether to fail the build if an error is found. + +Compatibility note: `MpsCheck` task currently extends `JavaExec` but this may change in the future. Do not rely on this. ## Run migrations diff --git a/api/mps-gradle-plugin.api b/api/mps-gradle-plugin.api index 3eb9a66d..dba80963 100644 --- a/api/mps-gradle-plugin.api +++ b/api/mps-gradle-plugin.api @@ -296,3 +296,23 @@ public final class de/itemis/mps/gradle/runmigrations/RunMigrationsMpsProjectPlu public final fun getMIN_VERSION_FOR_HALT_ON_PRECHECK_FAILURE ()Lnet/swiftzer/semver/SemVer; } +public abstract class de/itemis/mps/gradle/tasks/MpsCheck : org/gradle/api/tasks/JavaExec, org/gradle/api/tasks/VerificationTask { + public fun ()V + public fun exec ()V + public final fun getAdditionalModelcheckBackendClasspath ()Lorg/gradle/api/file/ConfigurableFileCollection; + public final fun getExcludeModels ()Lorg/gradle/api/provider/ListProperty; + public final fun getExcludeModules ()Lorg/gradle/api/provider/ListProperty; + public final fun getFolderMacros ()Lorg/gradle/api/provider/MapProperty; + public final fun getJunitFile ()Lorg/gradle/api/file/RegularFileProperty; + public final fun getJunitFormat ()Lorg/gradle/api/provider/Property; + public final fun getModels ()Lorg/gradle/api/provider/ListProperty; + public final fun getModules ()Lorg/gradle/api/provider/ListProperty; + public final fun getMpsHome ()Lorg/gradle/api/file/DirectoryProperty; + public final fun getMpsVersion ()Lorg/gradle/api/provider/Property; + public final fun getPluginRoots ()Lorg/gradle/api/provider/SetProperty; + public final fun getProjectLocation ()Lorg/gradle/api/file/DirectoryProperty; + protected final fun getSources ()Lorg/gradle/api/provider/Provider; + public final fun getVarMacros ()Lorg/gradle/api/provider/MapProperty; + public final fun getWarningAsError ()Lorg/gradle/api/provider/Property; +} + diff --git a/build.gradle.kts b/build.gradle.kts index 96d97e2c..c83162bf 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -25,7 +25,7 @@ plugins { } val versionMajor = 1 -val versionMinor = 18 +val versionMinor = 19 group = "de.itemis.mps" @@ -59,9 +59,13 @@ dependencies { api("de.itemis.mps.gradle:git-based-versioning") implementation(kotlin("stdlib", version = kotlinVersion)) implementation("net.swiftzer.semver:semver:1.1.2") + implementation("de.itemis.mps.build-backends:launcher:1.+") testImplementation("junit:junit:4.13.2") } +tasks.test { + useJUnit() +} gradlePlugin { plugins { diff --git a/gradle.lockfile b/gradle.lockfile index f4047139..85214ce0 100644 --- a/gradle.lockfile +++ b/gradle.lockfile @@ -1,6 +1,7 @@ # This is a Gradle generated file for dependency locking. # Manual edits can break the build and are not advised. # This file is expected to be part of source control. +de.itemis.mps.build-backends:launcher:1.0.0.55.b7ebac5=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath junit:junit:4.13.2=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath net.java.dev.jna:jna:5.6.0=kotlinCompilerClasspath,kotlinKlibCommonizerClasspath net.swiftzer.semver:semver:1.1.2=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath diff --git a/settings.gradle.kts b/settings.gradle.kts index ee68cc6f..656e6d8b 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -2,6 +2,10 @@ pluginManagement { includeBuild("git-based-versioning") } +plugins { + id("org.gradle.toolchains.foojay-resolver-convention") version ("0.7.0") +} + rootProject.name = "mps-gradle-plugin" includeBuild("git-based-versioning") diff --git a/src/main/kotlin/de/itemis/mps/gradle/common.gradle.kts b/src/main/kotlin/de/itemis/mps/gradle/common.gradle.kts index e33b9890..a43f291e 100644 --- a/src/main/kotlin/de/itemis/mps/gradle/common.gradle.kts +++ b/src/main/kotlin/de/itemis/mps/gradle/common.gradle.kts @@ -1,6 +1,12 @@ package de.itemis.mps.gradle /** - * Dummy empty plugin that lets us use `plugins` block rather than `buildscript` to put the task classes + * A side effect of this plugin is that it lets us use `plugins` block rather than `buildscript` to put the task classes * ([RunAntScript], [BuildLanguages], etc.) onto the classpath. */ + +val modelcheckBackend by configurations.creating + +modelcheckBackend.defaultDependencies { + add(dependencies.create("de.itemis.mps.build-backends:modelcheck:${MPS_BUILD_BACKENDS_VERSION}")) +} diff --git a/src/main/kotlin/de/itemis/mps/gradle/tasks/MpsCheck.kt b/src/main/kotlin/de/itemis/mps/gradle/tasks/MpsCheck.kt new file mode 100644 index 00000000..6f780544 --- /dev/null +++ b/src/main/kotlin/de/itemis/mps/gradle/tasks/MpsCheck.kt @@ -0,0 +1,160 @@ +package de.itemis.mps.gradle.tasks + +import de.itemis.mps.gradle.launcher.MpsBackendLauncher +import org.gradle.api.GradleException +import org.gradle.api.file.* +import org.gradle.api.logging.LogLevel +import org.gradle.api.provider.* +import org.gradle.api.tasks.* +import org.gradle.kotlin.dsl.* +import org.gradle.language.base.plugins.LifecycleBasePlugin +import org.gradle.process.CommandLineArgumentProvider +import java.io.File + +@CacheableTask +abstract class MpsCheck : JavaExec(), VerificationTask { + + // Having our own private launcher instance means we don't need to apply the launcher plugin. This works as long + // as the launcher remains stateless. + private val backendLauncher: MpsBackendLauncher = objectFactory.newInstance(MpsBackendLauncher::class) + + @get:InputDirectory + @get:PathSensitive(PathSensitivity.NONE) + val mpsHome: DirectoryProperty = objectFactory.directoryProperty() + + @get:Input + @get:Optional + val mpsVersion: Property = objectFactory.property() + .convention(backendLauncher.mpsVersionFromMpsHome(mpsHome.asFile)) + + @get:Internal("only modules and models matter, covered by #sources") + val projectLocation: DirectoryProperty = + objectFactory.directoryProperty().convention(project.layout.projectDirectory) + + @get:Classpath + val pluginRoots: SetProperty = objectFactory.setProperty() + + @get:Internal("Folder macros are ignored for the purposes of up-to-date checks and caching") + val folderMacros: MapProperty = objectFactory.mapProperty() + + @get:Input + val varMacros: MapProperty = objectFactory.mapProperty() + + @get:Input + val models: ListProperty = objectFactory.listProperty() + + @get:Input + val modules: ListProperty = objectFactory.listProperty() + + @get:Input + val excludeModels: ListProperty = objectFactory.listProperty() + + @get:Input + val excludeModules: ListProperty = objectFactory.listProperty() + + @get:Input + val warningAsError: Property = objectFactory.property().convention(false) + + @get:OutputFile + val junitFile: RegularFileProperty = objectFactory.fileProperty() + .convention(project.layout.buildDirectory.map { it.file("TEST-${this@MpsCheck.name}.xml") }) + + @get:Input + val junitFormat: Property = objectFactory.property().convention("module-and-model") + + @get:Internal("covered by classpath") + val additionalModelcheckBackendClasspath: ConfigurableFileCollection = + objectFactory.fileCollection().from(initialModelcheckBackendClasspath()) + + @Suppress("unused") + @InputFiles + @SkipWhenEmpty + @PathSensitive(PathSensitivity.NONE) + protected val sources: Provider = projectLocation.map { + it.asFileTree.matching { + exclude(project.layout.buildDirectory.get().asFile.relativeTo(projectLocation.get().asFile).path + "/**") + include("**/*.msd") + include("**/*.mpsr") + include("**/*.mps") + } + } + + init { + backendLauncher.configureJavaForMpsVersion(this, mpsHome.map { it.asFile }, mpsVersion) + argumentProviders.add(CommandLineArgumentProvider { + val result = mutableListOf() + + result.add("--project=${projectLocation.get().asFile}") + + pluginRoots.get().flatMap { findPluginsRecursively(it.asFile) } + .mapTo(result) { "--plugin=${it.id}::${it.path}" } + folderMacros.get().mapTo(result) { "--macro=${it.key}::${it.value.asFile}" } + varMacros.get().mapTo(result) { "--macro=${it.key}::${it.value}" } + + // Only a limited subset of checkers is registered in MPS environment, IDEA environment is necessary for + // proper checking. + result.add("--environment=IDEA") + + result.addAll(models.get().map { "--model=$it" }) + result.addAll(modules.get().map { "--module=$it" }) + result.addAll(excludeModels.get().map { "--exclude-model=$it" }) + result.addAll(excludeModules.get().map { "--exclude-module=$it" }) + + if (warningAsError.get()) { + result.add("--warning-as-error") + } + + if (ignoreFailures) { + result.add("--error-no-fail") + } + + if (junitFile.isPresent) { + result.add("--result-file=${junitFile.get().asFile}") + } + + if (junitFormat.isPresent) { + result.add("--result-format=${junitFormat.get()}") + } + + val effectiveLogLevel = logging.level ?: project.logging.level ?: project.gradle.startParameter.logLevel + if (effectiveLogLevel <= LogLevel.INFO) { + result.add("--log-level=info") + } + + result + }) + + group = LifecycleBasePlugin.VERIFICATION_GROUP + + classpath(project.configurations.named("modelcheckBackend")) + classpath(additionalModelcheckBackendClasspath) + + mainClass.set("de.itemis.mps.gradle.modelcheck.MainKt") + } + + override fun exec() { + val projectLocationAsFile = projectLocation.get().asFile + if (!projectLocationAsFile.resolve(".mps").isDirectory) { + throw GradleException(MpsCheckErrors.noMpsProjectIn(projectLocationAsFile)) + } + + super.exec() + } + + private fun initialModelcheckBackendClasspath() = mpsHome.asFileTree.matching { + include("lib/**/*.jar") + + // add only minimal number of plugins jars that are required by the modelcheck code + // (to avoid conflicts with plugin classloader if custom configured plugins are loaded) + // mps-httpsupport: we need it to print the node url to the console. + // mps-modelchecker: contains used UnresolvedReferencesChecker + // git4idea: has to be on classpath as bundled plugin to be loaded (since 2019.3) + include("plugins/mps-modelchecker/**/*.jar") + include("plugins/mps-httpsupport/**/*.jar") + include("plugins/git4idea/**/*.jar") + } +} + +internal object MpsCheckErrors { + fun noMpsProjectIn(dir: File): String = "Directory does not contain an MPS project: " + dir +} diff --git a/src/main/kotlin/de/itemis/mps/gradle/tasks/PluginIds.kt b/src/main/kotlin/de/itemis/mps/gradle/tasks/PluginIds.kt new file mode 100644 index 00000000..023f310a --- /dev/null +++ b/src/main/kotlin/de/itemis/mps/gradle/tasks/PluginIds.kt @@ -0,0 +1,121 @@ +package de.itemis.mps.gradle.tasks + +import de.itemis.mps.gradle.Plugin +import org.slf4j.LoggerFactory +import org.w3c.dom.Document +import java.io.File +import java.io.IOException +import java.io.InputStream +import java.nio.file.FileVisitResult +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.SimpleFileVisitor +import java.nio.file.attribute.BasicFileAttributes +import java.util.jar.JarFile +import javax.xml.parsers.DocumentBuilder +import javax.xml.parsers.DocumentBuilderFactory + +internal val logger = LoggerFactory.getLogger("de.itemis.mps.gradle.tasks.PluginIds")!! + +internal fun findPluginsRecursively(root: File): List = mutableListOf().apply { + Files.walkFileTree(root.toPath(), object : SimpleFileVisitor() { + override fun preVisitDirectory(dir: Path, attrs: BasicFileAttributes): FileVisitResult { + val dirAsFile = dir.toFile() + val id = readPluginId(dirAsFile) + if (id != null) { + this@apply.add(Plugin(id, dir.toString())) + return FileVisitResult.SKIP_SUBTREE + } + return FileVisitResult.CONTINUE + } + }) +}.toList() + +internal fun readPluginId(pluginDirectory: File): String? { + logger.debug("Reading plugin ID for {}", pluginDirectory) + val pluginXml = findPluginDescriptor(pluginDirectory) ?: return null + val ids = pluginXml.documentElement.getElementsByTagName("id") + if (ids.length != 1) { + logger.debug("Expected a single 'id' element, found {}", ids.length) + return null + } + + val result = ids.item(0).textContent + logger.debug("Found ID: {}", result) + return result.ifBlank { null } +} + +private fun findPluginDescriptor(pluginDirectory: File): Document? { + logger.debug("Looking for plugin descriptor in {}", pluginDirectory) + val libDir = pluginDirectory.resolve("lib") + + if (libDir.isDirectory) { + val jarsInLib = libDir.listFiles { file -> file.name.endsWith(".jar") && file.isFile } + + if (jarsInLib != null) { + for (jar in jarsInLib) { + val descriptor = readDescriptorFromJarFile(jar) + if (descriptor != null) { + logger.debug("Found plugin descriptor inside {}", jar) + return descriptor + } + } + } + } + + val pluginXmlFile = pluginDirectory.resolve("META-INF/plugin.xml") + if (pluginXmlFile.isFile) { + logger.debug("Found plugin descriptor in {}", pluginXmlFile) + return readXmlFile(pluginXmlFile) + } + + logger.debug("Plugin descriptor not found in {}", pluginDirectory) + return null +} + +private fun readDescriptorFromJarFile(file: File): Document? { + try { + JarFile(file).use { jarFile -> + val jarEntry = jarFile.getJarEntry("META-INF/plugin.xml") ?: return null + jarFile.getInputStream(jarEntry).use { + return readXmlFile(it, "${file}!${jarEntry.name}") + } + } + } catch (ex: IOException) { + logger.warn("Error reading JAR file $file", ex) + return null + } +} + +private fun readXmlFile(file: File): Document? { + return try { + newDocumentBuilder().parse(file) + } catch (e: Exception) { + logger.warn("Error reading $file", e) + null + } +} + +private fun readXmlFile(stream: InputStream, name: String): Document? { + return try { + newDocumentBuilder().parse(stream, name) + } catch (e: Exception) { + logger.warn("Error reading $name", e) + null + } +} + +private fun newDocumentBuilder(): DocumentBuilder { + val dbf = DocumentBuilderFactory.newInstance() + disableDTD(dbf) + return dbf.newDocumentBuilder() +} + +private fun disableDTD(dbf: DocumentBuilderFactory) { + dbf.isValidating = false; + dbf.isNamespaceAware = true; + dbf.setFeature("http://xml.org/sax/features/namespaces", false); + dbf.setFeature("http://xml.org/sax/features/validation", false); + dbf.setFeature("http://apache.org/xml/features/nonvalidating/load-dtd-grammar", false); + dbf.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false); +} diff --git a/src/test/kotlin/test/de/itemis/mps/gradle/MpsCheckTaskTest.kt b/src/test/kotlin/test/de/itemis/mps/gradle/MpsCheckTaskTest.kt new file mode 100644 index 00000000..cbbb64cc --- /dev/null +++ b/src/test/kotlin/test/de/itemis/mps/gradle/MpsCheckTaskTest.kt @@ -0,0 +1,128 @@ +package test.de.itemis.mps.gradle + +import de.itemis.mps.gradle.tasks.MpsCheckErrors +import org.gradle.api.GradleException +import org.gradle.api.invocation.Gradle +import org.gradle.testkit.runner.GradleRunner +import org.gradle.testkit.runner.TaskOutcome +import org.hamcrest.CoreMatchers.containsString +import org.hamcrest.MatcherAssert.assertThat +import org.junit.Assert +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TemporaryFolder +import java.io.File + +class MpsCheckTaskTest { + @Rule + @JvmField + val testProjectDir: TemporaryFolder = TemporaryFolder() + + private fun settingsScriptBoilerplate() = """ + plugins { + id("org.gradle.toolchains.foojay-resolver-convention") version ("0.7.0") + } + """.trimIndent() + + private fun buildScriptBoilerplate(mpsVersion: String) = """ + import de.itemis.mps.gradle.tasks.MpsCheck + + plugins { + id("de.itemis.mps.gradle.common") + } + + repositories { + mavenCentral() + maven("https://artifacts.itemis.cloud/repository/maven-mps") + } + + val mps = configurations.create("mps") + + dependencies { + mps("com.jetbrains:mps:$mpsVersion") + } + + val resolveMps by tasks.registering(Sync::class) { + from({ zipTree(mps.singleFile) }) + into(layout.buildDirectory.dir("mps")) + } + """.trimIndent() + "\n" + + @Test + fun noMpsProject() { + val settingsFile = testProjectDir.newFile("settings.gradle.kts") + val buildFile = testProjectDir.newFile("build.gradle.kts") + val mpsTestPrjLocation = testProjectDir.newFolder("mps-prj") + + extractTestProject("test-project", mpsTestPrjLocation) + + settingsFile.writeText(settingsScriptBoilerplate()) + buildFile.writeText(buildScriptBoilerplate("2021.3.3") + """ + val checkProject by tasks.registering(MpsCheck::class) { + mpsHome.set(layout.dir(resolveMps.map { it.destinationDir })) + junitFile.set(layout.buildDirectory.file("output.xml")) + } + """.trimIndent()) + + val result = gradleRunner().withArguments("checkProject").buildAndFail() + + Assert.assertEquals(TaskOutcome.FAILED, result.task(":checkProject")?.outcome) + assertThat(result.output, containsString(MpsCheckErrors.noMpsProjectIn(testProjectDir.root.canonicalFile))) + } + + @Test + fun simple() { + val settingsFile = testProjectDir.newFile("settings.gradle.kts") + val buildFile = testProjectDir.newFile("build.gradle.kts") + val mpsTestPrjLocation = testProjectDir.newFolder("mps-prj") + + extractTestProject("test-project", mpsTestPrjLocation) + + settingsFile.writeText(settingsScriptBoilerplate()) + buildFile.writeText(buildScriptBoilerplate("2021.3.3") + """ + val checkProject by tasks.registering(MpsCheck::class) { + mpsHome.set(layout.dir(resolveMps.map { it.destinationDir })) + projectLocation.set(file("mps-prj")) + junitFile.set(layout.buildDirectory.file("output.xml")) + } + """.trimIndent()) + + val result = gradleRunner().withArguments("checkProject").build() + + Assert.assertEquals(TaskOutcome.SUCCESS, result.task(":checkProject")?.outcome) + Assert.assertTrue("output file must exist", testProjectDir.root.resolve("build/output.xml").isFile) + } + + @Test + fun caching() { + val settingsFile = testProjectDir.newFile("settings.gradle.kts") + val buildFile = testProjectDir.newFile("build.gradle.kts") + val mpsTestPrjLocation = testProjectDir.newFolder("mps-prj") + val buildDir = testProjectDir.root.resolve("build") + + extractTestProject("test-project", mpsTestPrjLocation) + + settingsFile.writeText(settingsScriptBoilerplate()) + buildFile.writeText(buildScriptBoilerplate("2021.3.3") + """ + val checkProject by tasks.registering(MpsCheck::class) { + mpsHome.set(layout.dir(resolveMps.map { it.destinationDir })) + projectLocation.set(file("mps-prj")) + } + """.trimIndent()) + + val result = gradleRunner().withArguments("checkProject", "--build-cache").build() + + Assert.assertEquals(TaskOutcome.SUCCESS, result.task(":checkProject")?.outcome) + + buildDir.deleteRecursively() + + val secondResult = gradleRunner().withArguments("checkProject", "--build-cache").build() + + println(secondResult.output) + Assert.assertEquals(TaskOutcome.FROM_CACHE, secondResult.task(":checkProject")?.outcome) + } + + private fun gradleRunner(): GradleRunner = GradleRunner.create() + .withProjectDir(testProjectDir.root) + .withPluginClasspath() +} diff --git a/src/test/kotlin/test/de/itemis/mps/gradle/PluginIdsTest.kt b/src/test/kotlin/test/de/itemis/mps/gradle/PluginIdsTest.kt new file mode 100644 index 00000000..0c439416 --- /dev/null +++ b/src/test/kotlin/test/de/itemis/mps/gradle/PluginIdsTest.kt @@ -0,0 +1,73 @@ +package test.de.itemis.mps.gradle + +import de.itemis.mps.gradle.tasks.readPluginId +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TemporaryFolder +import java.io.File +import java.util.jar.JarEntry +import java.util.jar.JarOutputStream + +class PluginIdsTest { + + @JvmField @Rule + val folder = TemporaryFolder() + + @Test + fun idInLibJar() { + val pluginDir = folder.newFolder() + writeJarWithPluginXml(pluginDir.resolve("lib/myjar.jar"), "jetbrains.jetpad") + + assertEquals("jetbrains.jetpad", readPluginId(pluginDir)) + } + + + @Test + fun noDescriptor() { + val pluginDir = folder.newFolder() + assertNull(readPluginId(pluginDir)) + } + + @Test + fun idInMetaInf() { + val pluginDir = folder.newFolder() + writeTextFile(pluginDir.resolve("META-INF/plugin.xml"), "jetbrains.jetpad") + + assertEquals("jetbrains.jetpad", readPluginId(pluginDir)) + } + + @Test + fun idInLibJarTakesPrecedence() { + val pluginDir = folder.newFolder() + writeJarWithPluginXml(pluginDir.resolve("lib/foo.jar"), "foo") + writeTextFile(pluginDir.resolve("META-INF/plugin.xml"), "bar") + + assertEquals("foo", readPluginId(pluginDir)) + } + + @Test + fun invalidXml() { + val pluginDir = folder.newFolder() + writeJarWithPluginXml(pluginDir.resolve("lib/foo.jar"), "INVALID") + + assertNull(readPluginId(pluginDir)) + } + + private fun writeTextFile(metaInfPluginXml: File, contents: String) { + metaInfPluginXml.parentFile.mkdirs() + || throw RuntimeException("Could not create parent directory for $metaInfPluginXml") + + metaInfPluginXml.writeText(contents) + } + + private fun writeJarWithPluginXml(jarFile: File, contents: String) { + jarFile.parentFile.mkdirs() || throw RuntimeException("Could not create parent directory for $jarFile") + + JarOutputStream(jarFile.outputStream()).use { + it.putNextEntry(JarEntry("META-INF/plugin.xml")) + it.write(contents.toByteArray()) + } + } +}