Skip to content

Commit

Permalink
Merge pull request #31 from mbeddr/modelcheck
Browse files Browse the repository at this point in the history
model checking support
  • Loading branch information
abstraktor authored Jun 11, 2019
2 parents 1282cb6 + 822864e commit 2237a6e
Show file tree
Hide file tree
Showing 25 changed files with 861 additions and 44 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,4 @@ mps-prj/solutions/my.build/build.xml
test/generate-build-solution/mps-prj/solutions/my.build/build.xml
.DS_Store
workspace.xml
.gradle
73 changes: 72 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ configurations {
mps
}
ext.mpsVersion = '2017.3.5'
ext.mpsVersion = '2018.2.5'
generate {
projectLocation = new File("./mps-prj")
Expand All @@ -226,3 +226,74 @@ Parameters:
* `debug` - optionally allows to start the JVM that is used to generated with a debugger. Setting it to `true` will cause
the started JVM to suspend until a debugger is attached. Useful for debugging classloading problems or exceptions during
the build.

## Model Check

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
the command line.

### Usage

A minimal build script to check all models in a MPS project with no external plugins would look like this:

```
apply plugin: 'modelcheck'
configurations {
mps
}
dependencies {
mps "com.jetbrains:mps:$mpsVersion"
}
ext.mpsVersion = '2018.2.5'
modelcheck {
projectLocation = new File("./mps-prj")
mpsConfig = configurations.mps
}
```

Parameters:
* `mpsConfig` - the configuration used to resolve MPS. Currently only vanilla MPS is supported and no custom RCPs.
Custom plugins are supported via the `pluginLocation` parameter.
* `mpsLocation` - optional location where to place the MPS files.
* `plugins` - optional list of plugins to load before generation is attempted.
The notation is `new Plugin("someID", "somePath")`. Where the first parameter is the plugin id and the second the `short (folder) name`.
* `pluginLocation` - location where to load the plugins from. Structure needs to be a flat folder structure similar to the
`plugins` directory inside of the MPS installation.
* `models` - optional list of models to generate. If omitted all models in the project will be generated. Only full name
matched are supported and no RegEx or partial name matching.
* `macros` - optional list of path macros. The notation is `new Macro("name", "value")`.
* `projectLocation` - location of the MPS project to generate.
* `errorNoFail` - report errors but do not fail the build.
* `warningAsError` - handles warnings as errors and will fail the build if any is found when `errorNoFail` is not set.
* `debug` - optionally allows to start the JVM that is used to generated with a debugger. Setting it to `true` will cause
the started JVM to suspend until a debugger is attached. Useful for debugging classloading problems or exceptions during
the build.

### Additional Plugins

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:

```
apply plugin: 'modelcheck'
...
modelcheck {
pluginLocation = new File("path/to/my/plugins")
plugins = [new Plugin("com.mbeddr.core", "mbeddr.core")]
projectLocation = new File("./mps-prj")
mpsConfig = configurations.mps
}
```

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.
6 changes: 5 additions & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ val nexusUsername: String? by project
val nexusPassword: String? by project

val kotlinArgParserVersion by extra { "2.0.7" }
val mpsVersion by extra { "2018.2.4" }
val mpsVersion by extra { "2018.2.5" }


version = if (!project.hasProperty("useSnapshot") &&
Expand Down Expand Up @@ -57,6 +57,10 @@ gradlePlugin {
id = "generate-models"
implementationClass = "de.itemis.mps.gradle.generate.GenerateMpsProjectPlugin"
}
register("modelcheck") {
id = "modelcheck"
implementationClass = "de.itemis.mps.gradle.modelcheck.ModelcheckMpsProjectPlugin"
}
}
}

Expand Down
2 changes: 1 addition & 1 deletion execute-generators/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ dependencies {
implementation(kotlin("stdlib-jdk8", version = kotlinVersion))
implementation("com.xenomachina:kotlin-argparser:$kotlinArgParserVersion")
mpsConfiguration("com.jetbrains:mps:$mpsVersion")
compileOnly(mpsConfiguration.resolve().map { zipTree(it) }.first().matching { include("lib/*.jar")})
compileOnly(mpsConfiguration.resolve().map { zipTree(it) }.first().matching { include("lib/*.jar") })
implementation(project(":project-loader"))
}

Expand Down
47 changes: 47 additions & 0 deletions modelcheck/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

import java.net.URI

group = "de.itemis.mps"

plugins {
kotlin("jvm")
`maven-publish`
`java-gradle-plugin`
}

repositories {
mavenCentral()
maven {
url = URI("https://projects.itemis.de/nexus/content/repositories/mbeddr")
}
}

val nexusUsername: String? by project
val nexusPassword: String? by project

val kotlinArgParserVersion: String by project
val mpsVersion: String by project

val pluginVersion = "2"

version = if (project.hasProperty("forceCI") || project.hasProperty("teamcity")) {
de.itemis.mps.gradle.GitBasedVersioning.getVersion(mpsVersion, pluginVersion)
} else {
"$mpsVersion.$pluginVersion-SNAPSHOT"
}


val mpsConfiguration = configurations.create("mps")

dependencies {
implementation(kotlin("stdlib-jdk8"))
implementation("com.xenomachina:kotlin-argparser:$kotlinArgParserVersion")
mpsConfiguration("com.jetbrains:mps:$mpsVersion")
compileOnly(mpsConfiguration.resolve().map { zipTree(it) }.first().matching { include("lib/*.jar", "plugins/modelchecker/**/*.jar", "plugins/http-support/**/*.jar")})
implementation(project(":project-loader"))
}

tasks.withType<KotlinCompile> {
kotlinOptions.jvmTarget = "1.8"
}
140 changes: 140 additions & 0 deletions modelcheck/src/main/kotlin/Main.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
package de.itemis.mps.gradle.modelcheck

import com.xenomachina.argparser.ArgParser
import com.xenomachina.argparser.mainBody
import de.itemis.mps.gradle.project.loader.Args
import de.itemis.mps.gradle.project.loader.executeWithProject
import jetbrains.mps.checkers.*
import jetbrains.mps.errors.MessageStatus
import jetbrains.mps.errors.item.IssueKindReportItem
import jetbrains.mps.ide.httpsupport.runtime.base.HttpSupportUtil
import jetbrains.mps.ide.modelchecker.platform.actions.ModelCheckerIssueFinder
import jetbrains.mps.ide.modelchecker.platform.actions.ModelCheckerSettings
import jetbrains.mps.ide.modelchecker.platform.actions.UnresolvedReferencesChecker
import jetbrains.mps.progress.EmptyProgressMonitor
import jetbrains.mps.project.Project
import jetbrains.mps.project.validation.StructureChecker
import jetbrains.mps.typesystemEngine.checker.TypesystemChecker
import jetbrains.mps.util.CollectConsumer
import org.apache.log4j.Logger

private val logger = Logger.getLogger("de.itemis.mps.gradle.generate")

class ModelCheckArgs(parser: ArgParser) : Args(parser) {
val models by parser.adding("--model",
help = "list of models to generate")
val warningAsError by parser.flagging("--warning-as-error", help = "treat model checker warning as errors")
val dontFailOnError by parser.flagging("--error-no-fail", help = "report errors but don't fail the build")
}

fun printInfo(msg: String) {
logger.info(msg)
}

fun printWarn(msg: String) {
logger.warn(msg)
}

fun printError(msg: String) {
logger.error(msg)
}

fun printResult(item: IssueKindReportItem, project: Project, args: ModelCheckArgs) {
val path = IssueKindReportItem.PATH_OBJECT.get(item)

val info = ::printInfo
val warn = if (args.warningAsError) {
::printError
} else {
::printWarn
}

val err = ::printError

val print = fun (severity: MessageStatus, msg: String) {
when(severity) {
MessageStatus.OK -> info(msg)
MessageStatus.WARNING -> warn(msg)
MessageStatus.ERROR -> err(msg)
}
}

when (path) {
is IssueKindReportItem.PathObject.ModulePathObject -> {
val module = path.resolve(project.repository)
print(item.severity, "${item.message}[${module.moduleName}]")
}
is IssueKindReportItem.PathObject.ModelPathObject -> {
val model = path.resolve(project.repository)
print(item.severity, "${item.message} [${model.name.longName}]")
}
is IssueKindReportItem.PathObject.NodePathObject -> {
val node = path.resolve(project.repository)
val url = HttpSupportUtil.getURL(node)
print(item.severity, "${item.message} [$url]")
}
else -> print(item.severity, item.message)
}
}

fun modelCheckProject(args: ModelCheckArgs, project: Project) : Boolean {

// see ModelCheckerSettings.getSpecificCheckers for details
// we do not call into that class because we don't want to load the settings from the user
val checkers = listOf(TypesystemChecker(),
ConstraintsChecker(),
RefScopeChecker(),
TargetConceptChecker(),
UsedLanguagesChecker(),
StructureChecker(),
ModelPropertiesChecker(),
UnresolvedReferencesChecker(project),
ModuleChecker())


// We don't use ModelCheckerIssueFinder because it has strange dependency on the ModelCheckerSettings which we
// want to avoid when running in headless mode
val errorCollector = CollectConsumer<IssueKindReportItem>()
val checker = ModelCheckerBuilder(false).createChecker(checkers)

val itemsToCheck = ModelCheckerBuilder.ItemsToCheck()

project.modelAccess.runReadAction {
if(args.models.isNotEmpty()) {
itemsToCheck.models.addAll(project.projectModulesWithGenerators.flatMap { it.models }.filter { args.models.contains(it.name.longName) })
} else {
itemsToCheck.models.addAll(project.projectModulesWithGenerators.flatMap { it.models })
}
checker.check(itemsToCheck, project.repository, errorCollector, EmptyProgressMonitor())

// We need read access here to resolve the node pointers in the report items
errorCollector.result.map{ printResult(it, project, args) }
}

val hasErrors = if (args.warningAsError) {
errorCollector.result.any { it.severity == MessageStatus.WARNING }
} else {
errorCollector.result.any { it.severity == MessageStatus.ERROR }
}

return hasErrors
}

fun main(args: Array<String>) = mainBody {

val parsed = ArgParser(args).parseInto(::ModelCheckArgs)
var hasErrors = true
try {
hasErrors = executeWithProject(parsed) { project -> modelCheckProject(parsed, project) }
} catch (ex: java.lang.Exception) {
logger.fatal("error model checking", ex)
} catch (t: Throwable) {
logger.fatal("error model checking", t)
}

if (hasErrors && !parsed.dontFailOnError) {
System.exit(-1)
}

System.exit(0)
}
4 changes: 2 additions & 2 deletions settings.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
rootProject.name = "mps-gradle-plugin"
include("execute-generators")
include("project-loader")

enableFeaturePreview("STABLE_PUBLISHING")
include("modelcheck")
enableFeaturePreview("STABLE_PUBLISHING")
43 changes: 43 additions & 0 deletions src/main/kotlin/de/itemis/mps/gradle/Common.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package de.itemis.mps.gradle

import org.gradle.api.GradleException
import org.gradle.api.Project
import org.gradle.api.artifacts.Configuration
import java.io.File

data class Plugin(
var id: String,
var path: String
)

data class Macro(
var name: String,
var value: String
)

open class BasePluginExtensions {
lateinit var mpsConfig: Configuration
var mpsLocation: File? = null
var plugins: List<Plugin> = emptyList()
var pluginLocation: File? = null
var macros: List<Macro> = emptyList()
var projectLocation: File? = null
var debug = false
}

fun argsFromBaseExtension(extensions: BasePluginExtensions): MutableList<String> {
val pluginLocation = if (extensions.pluginLocation != null) {
sequenceOf("--plugin-location=${extensions.pluginLocation!!.absolutePath}")
} else {
emptySequence()
}


val projectLocation = extensions.projectLocation ?: throw GradleException("No project path set")
val prj = sequenceOf("--project=${projectLocation.absolutePath}")

return sequenceOf(pluginLocation,
extensions.plugins.map { "--plugin=${it.id}:${it.path}" }.asSequence(),
extensions.macros.map { "--macro=${it.name}:${it.value}" }.asSequence(),
prj).flatten().toMutableList()
}
Loading

0 comments on commit 2237a6e

Please sign in to comment.