diff --git a/.github/workflows/binary-compatibility.yaml b/.github/workflows/build.yaml similarity index 54% rename from .github/workflows/binary-compatibility.yaml rename to .github/workflows/build.yaml index eab7e8f7..d8b9fdf9 100644 --- a/.github/workflows/binary-compatibility.yaml +++ b/.github/workflows/build.yaml @@ -1,4 +1,4 @@ -name: Binary Compatibility +name: Build on: push: @@ -9,20 +9,30 @@ on: workflow_dispatch: jobs: - checkBinaryCompatibility: - + build-mps-components: runs-on: ubuntu-latest - + timeout-minutes: 60 steps: - uses: actions/checkout@v4 - name: Set up JDK uses: actions/setup-java@v4 with: distribution: 'temurin' - java-version: '17' + java-version: '21' - name: Set up Gradle uses: gradle/gradle-build-action@v3 - name: Build env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: ./check-binary-compatibility.sh + run: >- + ./gradlew --build-cache + build + -PciBuild=true + - name: Archive test report + uses: actions/upload-artifact@v4 + if: always() + with: + name: test-report + path: | + */build/test-results + */build/reports diff --git a/.github/workflows/mps-compatibility.yaml b/.github/workflows/mps-compatibility.yaml deleted file mode 100644 index f016b5c7..00000000 --- a/.github/workflows/mps-compatibility.yaml +++ /dev/null @@ -1,54 +0,0 @@ -name: MPS compatibility - -on: - push: - branches: - - 'main' - pull_request: {} - # allow manual execution just in case - workflow_dispatch: - -jobs: - build-mps-components: - - runs-on: ubuntu-latest - timeout-minutes: 30 - strategy: - fail-fast: false - matrix: - version: -# Testing with the first and last version of the supported range should be enough. Breaking changes usually stay there -# after being introduced. It's not expected that an incompatibility only exists in some intermediate version and then -# becomes compatible again. -# - "2020.3.6" VersionFixer was replaced by ModuleDependencyVersions in 2021.1 (used by model-sync-plugin) - - "2021.1" - - "2021.2" - - "2021.3" - - "2022.2" - - "2022.3" - - "2023.2" - - steps: - - uses: actions/checkout@v4 - - name: Set up JDK 17 - uses: actions/setup-java@v4 - with: - distribution: 'temurin' - java-version: '17' - - name: Set up Gradle - uses: gradle/gradle-build-action@v3 - - name: Build with ${{ matrix.version }} - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: >- - ./gradlew --build-cache - build - -Pmps.version.major=${{ matrix.version }} - - name: Archive test report - uses: actions/upload-artifact@v4 - if: always() - with: - name: test-report-${{ matrix.version }} - path: | - */build/test-results - */build/reports diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index b18eb00b..6fdfd89c 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -23,7 +23,7 @@ jobs: uses: actions/setup-java@v4 with: distribution: 'temurin' - java-version: '17' + java-version: '21' - name: Set up Gradle uses: gradle/gradle-build-action@v3 - name: Use tag as version @@ -40,4 +40,3 @@ jobs: -Pgpr.user=${{ github.actor }} -Pgpr.key=${{ secrets.GITHUB_TOKEN }} -Pgpr.universalkey=${{ secrets.GHP_UNIVERSAL_PUBLISH_TOKEN }} - -Pmps.version=2023.2 diff --git a/build.gradle.kts b/build.gradle.kts index d4cc8e64..e8a7aefe 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -98,10 +98,10 @@ subprojects { allprojects { repositories { - mavenLocal() maven { url = uri("https://artifacts.itemis.cloud/repository/maven-mps/") } mavenCentral() maven { url = uri("https://maven.pkg.jetbrains.space/public/p/kotlinx-html/maven") } + mavenLocal() } publishing { diff --git a/buildSrc/src/main/kotlin/org/modelix/CopyMps.kt b/buildSrc/src/main/kotlin/org/modelix/CopyMps.kt index 24065666..7ab591a8 100644 --- a/buildSrc/src/main/kotlin/org/modelix/CopyMps.kt +++ b/buildSrc/src/main/kotlin/org/modelix/CopyMps.kt @@ -58,8 +58,6 @@ val Project.mpsPlatformVersion: Int get() { return mpsVersion.replace(Regex("""20(\d\d)\.(\d+).*"""), "$1$2").toInt() } -val Project.mpsJavaVersion: Int get() = if (mpsPlatformVersion >= 223) 17 else 11 - val Project.mpsHomeDir: Provider get() { if (project != rootProject) return rootProject.mpsHomeDir return project.layout.buildDirectory.dir("mps-$mpsVersion") diff --git a/check-binary-compatibility.sh b/check-binary-compatibility.sh deleted file mode 100755 index 5e5995af..00000000 --- a/check-binary-compatibility.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/bin/sh - -set -e -set -x - -mkdir -p build/binary-compatibility/mps-legacy-sync-plugin -./gradlew :mps-legacy-sync-plugin:clean :mps-legacy-sync-plugin:jar -Pmps.version=2021.1.4 -VERSION=$(cat version.txt) -cp "mps-legacy-sync-plugin/build/libs/mps-legacy-sync-plugin-$VERSION.jar" "build/binary-compatibility/mps-legacy-sync-plugin/a.jar" -./gradlew :mps-legacy-sync-plugin:clean :mps-legacy-sync-plugin:jar -Pmps.version=2023.2 -cp "mps-legacy-sync-plugin/build/libs/mps-legacy-sync-plugin-$VERSION.jar" "build/binary-compatibility/mps-legacy-sync-plugin/b.jar" -./gradlew :mps-legacy-sync-plugin:checkBinaryCompatibility - -#mkdir -p build/binary-compatibility/mps-diff-plugin -#./gradlew :mps-diff-plugin:clean :mps-diff-plugin:jar -Pmps.version=2021.1.4 -#cp "mps-diff-plugin/build/libs/mps-diff-plugin-$VERSION.jar" "build/binary-compatibility/mps-diff-plugin/a.jar" -#./gradlew :mps-diff-plugin:clean :mps-diff-plugin:jar -Pmps.version=2023.2 -#cp "mps-diff-plugin/build/libs/mps-diff-plugin-$VERSION.jar" "build/binary-compatibility/mps-diff-plugin/b.jar" -#./gradlew :mps-diff-plugin:checkBinaryCompatibility diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index fec91957..32c5a14e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -20,6 +20,7 @@ modelix-modelql-untyped = { group = "org.modelix", name = "modelql-untyped", ver modelix-modelql-html = { group = "org.modelix", name = "modelql-html", version.ref = "modelixCore" } modelix-model-datastructure = { group = "org.modelix", name = "model-datastructure", version.ref = "modelixCore" } modelix-mps-model-adapters = { group = "org.modelix.mps", name = "model-adapters", version.ref = "modelixCore" } +modelix-mpsApi = { group = "org.modelix.mps", name = "stable-api", version = "1.1.1" } kotlin-logging = { group = "io.github.oshai", name = "kotlin-logging", version = "7.0.3" } kotlin-logging-microutils = { group = "io.github.microutils", name = "kotlin-logging", version = "3.0.5" } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 7f93135c..d64cd491 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index a80b22ce..e18bc253 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.12.1-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew.bat b/gradlew.bat index 93e3f59f..25da30db 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -43,11 +43,11 @@ set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if %ERRORLEVEL% equ 0 goto execute -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail @@ -57,11 +57,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto execute -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail diff --git a/mps-diff-plugin-test/build.gradle.kts b/mps-diff-plugin-test/build.gradle.kts new file mode 100644 index 00000000..0c4e47c1 --- /dev/null +++ b/mps-diff-plugin-test/build.gradle.kts @@ -0,0 +1,27 @@ +import org.zeroturnaround.zip.ZipUtil + +buildscript { + dependencies { + classpath("org.modelix.mps:build-tools-lib:1.8.1") + } +} + +plugins { + id("org.jetbrains.kotlin.jvm") +} + +dependencies { + implementation(coreLibs.kotlin.coroutines.test) + implementation(coreLibs.ktor.client.cio) + implementation(coreLibs.testcontainers) +} + +tasks { + val unzipTestProject by registering { + ZipUtil.unpack(layout.projectDirectory.file("diff-test-project.zip").asFile, layout.buildDirectory.dir("diff-test-project").get().asFile) + } + test { + dependsOn(":mps-diff-plugin:prepareSandbox") + dependsOn(unzipTestProject) + } +} diff --git a/mps-diff-plugin/diff-test-project.zip b/mps-diff-plugin-test/diff-test-project.zip similarity index 100% rename from mps-diff-plugin/diff-test-project.zip rename to mps-diff-plugin-test/diff-test-project.zip diff --git a/mps-diff-plugin-test/src/test/kotlin/DiffPluginTest.kt b/mps-diff-plugin-test/src/test/kotlin/DiffPluginTest.kt new file mode 100644 index 00000000..fb9fbf76 --- /dev/null +++ b/mps-diff-plugin-test/src/test/kotlin/DiffPluginTest.kt @@ -0,0 +1,96 @@ +package org.modelix.mps.generator.web + +import io.ktor.client.HttpClient +import io.ktor.client.engine.cio.CIO +import io.ktor.client.request.get +import io.ktor.client.statement.bodyAsBytes +import io.ktor.client.statement.bodyAsText +import junit.framework.TestCase.assertEquals +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.Parameterized +import org.testcontainers.containers.GenericContainer +import org.testcontainers.containers.wait.strategy.Wait +import org.testcontainers.utility.MountableFile +import java.io.File +import kotlin.time.Duration.Companion.minutes +import kotlin.time.toJavaDuration + +@RunWith(Parameterized::class) +class DiffPluginTest(val mpsVersion: String) { + + companion object { + val timeout = 5.minutes + + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun mpsVersions() = listOf( + "2024.3", + "2024.1", + "2023.3", + "2023.2", +// "2022.3", +// "2022.2", +// "2021.3", +// "2021.2", +// "2021.1", +// "2020.3", + ) + } + + private fun runWithMPS(body: suspend (port: Int) -> Unit) = runTest(timeout = timeout) { + println("MPS version: $mpsVersion") + + val mps: GenericContainer<*> = GenericContainer("docker.io/modelix/mps-vnc-baseimage:0.9.4-mps$mpsVersion") + .withExposedPorts(33334) + .withCopy( + "../mps-diff-plugin/build/idea-sandbox/plugins/mps-diff-plugin", + "/mps/plugins/mps-diff-plugin", + ) + .withCopy( + "build/diff-test-project", + "/mps-projects/diff-test-project", + ) +// .withCreateContainerCmdModifier { +// it.withPlatform("linux/amd64") +// } + .waitingFor(Wait.forListeningPort().withStartupTimeout(timeout.toJavaDuration())) + .withLogConsumer { + println(it.utf8StringWithoutLineEnding) + } + + mps.start() + try { + body(mps.firstMappedPort) + } finally { + mps.stop() + } + } + + @Test + fun `diff images page`() = runWithMPS { port -> + val client = HttpClient(CIO) + val baseUrl = "http://localhost:$port" + val diffUrl = "$baseUrl/54ff8ce1442ac24e065d0ba99ae96b752a4427cf/38fd1c4668b8bb600a4193def54ae4281045c790/" + val diffPageHtml = client.get(diffUrl).bodyAsText() + println(diffPageHtml) + val folder = File("build/diff-images/$mpsVersion/") + folder.mkdirs() + folder.resolve("index.html").writeText(diffPageHtml) + + val imageNames = Regex("""src="([^\"]+\.png)"""").findAll(diffPageHtml).map { it.groupValues[1] }.toList() + println(imageNames) + assertEquals(3, imageNames.size) + + for (imageName in imageNames) { + val data = client.get("$diffUrl/$imageName").bodyAsBytes() + folder.resolve(imageName).writeBytes(data) + } + } +} + +private fun > T.withCopy(from: String, to: String): T { + require(File(from).exists()) { "Doesn't exist: $from" } + return withCopyFileToContainer(MountableFile.forHostPath(from), to) +} diff --git a/mps-diff-plugin/build.gradle.kts b/mps-diff-plugin/build.gradle.kts index d4c31344..06f6284c 100644 --- a/mps-diff-plugin/build.gradle.kts +++ b/mps-diff-plugin/build.gradle.kts @@ -1,4 +1,3 @@ -import org.jetbrains.kotlin.gradle.dsl.JvmTarget import org.jetbrains.kotlin.gradle.dsl.KotlinVersion import org.modelix.excludeMPSLibraries import org.modelix.mpsHomeDir @@ -17,30 +16,10 @@ plugins { group = "org.modelix.mps" -java { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 - toolchain { - languageVersion.set(JavaLanguageVersion.of(11)) - } - withSourcesJar() -} - -tasks.compileJava { - options.release = 11 -} - -tasks.compileKotlin { - kotlinOptions { - jvmTarget = "11" - freeCompilerArgs += listOf("-Xjvm-default=all-compatibility") - apiVersion = "1.6" - } -} - kotlin { + jvmToolchain(11) compilerOptions { - jvmTarget.set(JvmTarget.JVM_11) + freeCompilerArgs.addAll("-Xjvm-default=all-compatibility") } sourceSets { main { @@ -61,12 +40,7 @@ dependencies { implementationWithoutBundled(coreLibs.ktor.server.cors) implementationWithoutBundled(coreLibs.ktor.server.status.pages) implementationWithoutBundled(coreLibs.kotlin.logging) - implementationWithoutBundled(libs.kotlin.coroutines.swing) - - testImplementation(coreLibs.kotlin.coroutines.test) - testImplementation(coreLibs.ktor.server.test.host) - testImplementation(coreLibs.ktor.client.cio) - testImplementation(libs.zt.zip) + implementationWithoutBundled(libs.modelix.mpsApi) } // Configure Gradle IntelliJ Plugin @@ -82,21 +56,8 @@ intellij { tasks { patchPluginXml { - sinceBuild.set("211") // 203 not supported, because VersionFixer was replaced by ModuleDependencyVersions in 211 - untilBuild.set("232.10072.781") - } - - test { - // tests currently fail for these versions -// enabled = !setOf( -// 211, // jetbrains.mps.vcs plugin cannot be loaded -// 212, // timeout because of some deadlock -// 213, // timeout because of some deadlock -// 222, // timeout because of some deadlock -// ).contains(mpsPlatformVersion) - - // incompatibility of ktor 3 with the bundled coroutines version - enabled = false + sinceBuild.set("232") + untilBuild.set("243.*") } buildSearchableOptions { @@ -110,9 +71,9 @@ tasks { val mpsPluginDir = project.findProperty("mps$mpsPlatformVersion.plugins.dir")?.toString()?.let { file(it) } if (mpsPluginDir != null && mpsPluginDir.isDirectory) { - create("installMpsPlugin") { + register("installMpsPlugin") { dependsOn(prepareSandbox) - from(buildDir.resolve("idea-sandbox/plugins/mps-diff-plugin")) + from(layout.buildDirectory.dir("idea-sandbox/plugins/mps-diff-plugin")) into(mpsPluginDir.resolve("mps-diff-plugin")) } } @@ -129,7 +90,3 @@ publishing { } } } - -fun Provider.dir(name: String): Provider = map { it.dir(name) } -fun Provider.file(name: String): Provider = map { it.file(name) } -fun Provider.dir(name: Provider): Provider = flatMap { it.dir(name) } diff --git a/mps-diff-plugin/src/main/kotlin/org/modelix/ui/diff/DiffHandler.kt b/mps-diff-plugin/src/main/kotlin/org/modelix/ui/diff/DiffHandler.kt index 281d28d6..ff641cf7 100644 --- a/mps-diff-plugin/src/main/kotlin/org/modelix/ui/diff/DiffHandler.kt +++ b/mps-diff-plugin/src/main/kotlin/org/modelix/ui/diff/DiffHandler.kt @@ -18,7 +18,6 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Deferred import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async -import kotlinx.coroutines.swing.Swing import kotlinx.coroutines.withContext import kotlinx.html.HTML import kotlinx.html.body @@ -78,10 +77,7 @@ class DiffHandlerImpl() { .diffRevisions(diffRequest.leftRevision, diffRequest.rightRevision).flatMap { // The computation of the diff is not allowed to happen on the EDT, // but the rendering of the diff dialog has to happen on the EDT. - // - // Using Dispatchers.Swing instead of Dispatchers.Main, because it's not - // initialized in older MPS versions. - withContext(Dispatchers.Swing) { + withContext(Dispatchers.Main) { it() } } diff --git a/mps-diff-plugin/src/main/kotlin/org/modelix/ui/diff/DiffImages.kt b/mps-diff-plugin/src/main/kotlin/org/modelix/ui/diff/DiffImages.kt index a64c33fd..39f1e581 100644 --- a/mps-diff-plugin/src/main/kotlin/org/modelix/ui/diff/DiffImages.kt +++ b/mps-diff-plugin/src/main/kotlin/org/modelix/ui/diff/DiffImages.kt @@ -30,6 +30,7 @@ import git4idea.repo.GitRepositoryImpl import jetbrains.mps.baseLanguage.closures.runtime.Wrappers._T import jetbrains.mps.baseLanguage.closures.runtime._FunctionTypes._return_P0_E0 import jetbrains.mps.ide.ThreadUtils +import jetbrains.mps.ide.project.ProjectHelper import jetbrains.mps.internal.collections.runtime.CollectionSequence import jetbrains.mps.internal.collections.runtime.ILeftCombinator import jetbrains.mps.internal.collections.runtime.ISelector @@ -41,7 +42,6 @@ import jetbrains.mps.internal.collections.runtime.SetSequence import jetbrains.mps.nodeEditor.EditorComponent import jetbrains.mps.nodeEditor.NodeHighlightManager import jetbrains.mps.project.AbstractModule -import jetbrains.mps.smodel.MPSModuleRepository import jetbrains.mps.vcs.diff.ui.RootDifferencePaneBase import jetbrains.mps.vcs.diff.ui.common.DiffModelTree import jetbrains.mps.vcs.diff.ui.common.DiffModelTree.RootTreeNode @@ -61,6 +61,8 @@ import javax.swing.JTree import javax.swing.tree.TreePath import kotlin.math.max import kotlin.math.min +import kotlin.reflect.KClass +import kotlin.reflect.full.primaryConstructor class DiffImages( private val project: Project = (ProjectManager.getInstance().openProjects + ProjectManager.getInstance().defaultProject).first(), @@ -96,7 +98,7 @@ class DiffImages( repoRoots.addAll(additionalGitRepos.mapNotNull { LocalFileSystem.getInstance().findFileByIoFile(it)?.also { ensureRepoLoaded(project, it) } }) if (repoRoots.isEmpty()) { - val moduleRepo = MPSModuleRepository.getInstance() + val moduleRepo = ProjectHelper.getProjectRepository(project)!! lateinit var moduleFiles: List moduleRepo.modelAccess.runReadAction { moduleFiles = moduleRepo.modules.filterIsInstance().mapNotNull { it.moduleSourceDir } @@ -235,7 +237,12 @@ class DiffImages( val modelDiffViewer = ModelDiffViewer((context)!!, (diffRequest as ContentDiffRequest?)!!) try { val viewer = modelDiffViewer.component - val frame = FrameWrapper(project, null, false, "Modelix Diff Viewer", viewer) + // reflection because of binary incompatibility from MPS 2023.2 to 2023.3 + val frame = FrameWrapper::class.instantiate( + "project" to project, + "title" to "Modelix Diff Viewer", + "component" to viewer, + ) try { frame.show() } catch (ex: Exception) { @@ -550,3 +557,16 @@ class DiffImages( } } } + +fun KClass.instantiate(vararg parameters: Pair): T { + return instantiate(mapOf(*parameters)) +} + +fun KClass.instantiate(parameters: Map): T { + return primaryConstructor!!.callBy( + primaryConstructor!! + .parameters + .filter { parameters.containsKey(it.name) } + .associateWith { parameters[it.name] }, + ) +} diff --git a/mps-diff-plugin/src/main/kotlin/org/modelix/ui/diff/DiffServer.kt b/mps-diff-plugin/src/main/kotlin/org/modelix/ui/diff/DiffServer.kt index d9bc7bde..488e18cd 100644 --- a/mps-diff-plugin/src/main/kotlin/org/modelix/ui/diff/DiffServer.kt +++ b/mps-diff-plugin/src/main/kotlin/org/modelix/ui/diff/DiffServer.kt @@ -127,8 +127,9 @@ class DiffServer : Disposable { } } -class DiffServerStartupActivity : StartupActivity.Background { +class DiffServerStartupActivity : StartupActivity { override fun runActivity(project: Project) { + println(this::class.java.name + " called") project.service() // just ensure it's initialized } } diff --git a/mps-diff-plugin/src/test/kotlin/DiffPluginTest.kt b/mps-diff-plugin/src/test/kotlin/DiffPluginTest.kt deleted file mode 100644 index c2a2f57c..00000000 --- a/mps-diff-plugin/src/test/kotlin/DiffPluginTest.kt +++ /dev/null @@ -1,24 +0,0 @@ -import com.intellij.ide.plugins.PluginManager -import io.ktor.client.request.get -import io.ktor.client.statement.bodyAsText - -class DiffPluginTest : DiffPluginTestBase() { - - fun testMpsVcsPluginLoaded() { - val pluginId = "jetbrains.mps.vcs" - val loadedPluginIds = PluginManager.getPlugins().map { it.pluginId.idString }.toSet() - val message = "Plugin $pluginId not found. Found only:\n" + loadedPluginIds.joinToString("\n") { " $it" } + "\n" - assertTrue(message, loadedPluginIds.contains(pluginId)) - } - - fun testDiffImages() = runTestWithDiffServer { - PluginManager.getPlugins().map { it.name }.forEach(::println) - - val baseUrl = "http://localhost" - val diffPageHtml = client.get("$baseUrl/54ff8ce1442ac24e065d0ba99ae96b752a4427cf/38fd1c4668b8bb600a4193def54ae4281045c790/").bodyAsText() - println(diffPageHtml) - val imageNames = Regex("""src="([^\"]+)\.png"""").findAll(diffPageHtml).map { it.groupValues.first() }.toList() - println(imageNames) - assertEquals(3, imageNames.size) - } -} diff --git a/mps-diff-plugin/src/test/kotlin/DiffPluginTestBase.kt b/mps-diff-plugin/src/test/kotlin/DiffPluginTestBase.kt deleted file mode 100644 index 94ef02bd..00000000 --- a/mps-diff-plugin/src/test/kotlin/DiffPluginTestBase.kt +++ /dev/null @@ -1,54 +0,0 @@ -import com.intellij.openapi.application.ApplicationManager -import com.intellij.serviceContainer.AlreadyDisposedException -import com.intellij.testFramework.EdtTestUtil -import com.intellij.testFramework.HeavyPlatformTestCase -import io.ktor.client.HttpClient -import io.ktor.server.testing.ApplicationTestBuilder -import io.ktor.server.testing.testApplication -import org.modelix.ui.diff.DiffServer -import org.zeroturnaround.zip.ZipUtil -import java.io.File -import java.nio.file.Path - -@Suppress("removal") -abstract class DiffPluginTestBase() : HeavyPlatformTestCase() { - protected lateinit var projectDir: Path - protected lateinit var httpClient: HttpClient - - override fun tearDown() { - EdtTestUtil.runInEdtAndGet<_, Throwable> { - try { - super.tearDown() - } catch (ex: AlreadyDisposedException) { - Exception("Ignoring exception", ex).printStackTrace() - } - } - } - - override fun setUp() { - EdtTestUtil.runInEdtAndGet<_, Throwable> { - super.setUp() - } - } - - protected fun runTestWithDiffServer(block: suspend ApplicationTestBuilder.() -> Unit) = testApplication { - val diffServer = ApplicationManager.getApplication().getService(DiffServer::class.java) - diffServer.registerProject(project) - application { - diffServer.initKtorServer(this) - } - block() - } - - override fun runInDispatchThread(): Boolean = false - - override fun isCreateDirectoryBasedProject(): Boolean = true - - override fun getProjectDirOrFile(isDirectoryBasedProject: Boolean): Path { - val projectDir = super.getProjectDirOrFile(isDirectoryBasedProject) - println("projectDir: " + projectDir) - ZipUtil.unpack(File("diff-test-project.zip"), projectDir.toFile()) - this.projectDir = projectDir - return projectDir - } -} diff --git a/mps-generator-execution-plugin-test/build.gradle.kts b/mps-generator-execution-plugin-test/build.gradle.kts new file mode 100644 index 00000000..51612c74 --- /dev/null +++ b/mps-generator-execution-plugin-test/build.gradle.kts @@ -0,0 +1,21 @@ +buildscript { + dependencies { + classpath("org.modelix.mps:build-tools-lib:1.8.1") + } +} + +plugins { + id("org.jetbrains.kotlin.jvm") +} + +dependencies { + implementation(coreLibs.kotlin.coroutines.test) + implementation(coreLibs.ktor.client.cio) + implementation(coreLibs.testcontainers) +} + +tasks { + test { + dependsOn(":mps-generator-execution-plugin:prepareSandbox") + } +} diff --git a/mps-generator-execution-plugin-test/src/test/kotlin/GeneratorPluginTest.kt b/mps-generator-execution-plugin-test/src/test/kotlin/GeneratorPluginTest.kt new file mode 100644 index 00000000..c773ec53 --- /dev/null +++ b/mps-generator-execution-plugin-test/src/test/kotlin/GeneratorPluginTest.kt @@ -0,0 +1,87 @@ +import io.ktor.client.HttpClient +import io.ktor.client.engine.cio.CIO +import io.ktor.client.request.get +import io.ktor.client.statement.bodyAsText +import junit.framework.TestCase.assertEquals +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.Parameterized +import org.testcontainers.containers.GenericContainer +import org.testcontainers.containers.wait.strategy.Wait +import org.testcontainers.utility.MountableFile +import java.io.File +import kotlin.time.Duration.Companion.minutes +import kotlin.time.toJavaDuration + +@RunWith(Parameterized::class) +class GeneratorPluginTest(val mpsVersion: String) { + + companion object { + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun mpsVersions() = listOf( + "2024.3", + "2024.1", + "2023.3", + "2023.2", + "2022.3", + "2022.2", + "2021.3", + "2021.2", + "2021.1", + "2020.3", + ) + } + + private fun runWithMPS(body: suspend (port: Int) -> Unit) = runTest(timeout = 3.minutes) { + println("MPS version: $mpsVersion") + + val mps: GenericContainer<*> = GenericContainer("docker.io/modelix/mps-vnc-baseimage:0.9.4-mps$mpsVersion") + .withExposedPorts(33335) + .withCopy( + "../mps-generator-execution-plugin/build/idea-sandbox/plugins/mps-generator-execution-plugin", + "/mps/plugins/mps-generator-execution-plugin", + ) + .withCopy( + "testdata/SimpleProject", + "/mps-projects/SimpleProject", + ) + .waitingFor(Wait.forListeningPort().withStartupTimeout(3.minutes.toJavaDuration())) + .withLogConsumer { + println(it.utf8StringWithoutLineEnding) + } + + mps.start() + try { + body(mps.firstMappedPort) + } finally { + mps.stop() + } + } + + @Test + fun `single raw file`() = runWithMPS { port -> + val client = HttpClient(CIO) + val urlString = "http://localhost:$port/modules/6517ba0d-f632-49c5-a166-401587c2c3ca/models/r%3A630db018-71e0-498d-8b21-6ec252b3533a/generatorOutput/files/Class1.java/view" + val actual = client.get(urlString).bodyAsText() + val expected = """ + package Solution1.model1; + + /*Generated by MPS */ + + + public class Class1 { + public void method1() { + } + } + + """.trimIndent() + assertEquals(expected, actual) + } +} + +private fun > T.withCopy(from: String, to: String): T { + require(File(from).exists()) { "Doesn't exist: $from" } + return withCopyFileToContainer(MountableFile.forHostPath(from), to) +} diff --git a/mps-generator-execution-plugin-test/testdata/SimpleProject/.mps/migration.xml b/mps-generator-execution-plugin-test/testdata/SimpleProject/.mps/migration.xml new file mode 100644 index 00000000..d512ce45 --- /dev/null +++ b/mps-generator-execution-plugin-test/testdata/SimpleProject/.mps/migration.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/mps-generator-execution-plugin-test/testdata/SimpleProject/.mps/modules.xml b/mps-generator-execution-plugin-test/testdata/SimpleProject/.mps/modules.xml new file mode 100644 index 00000000..d24dfe06 --- /dev/null +++ b/mps-generator-execution-plugin-test/testdata/SimpleProject/.mps/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/mps-generator-execution-plugin-test/testdata/SimpleProject/solutions/Solution1/Solution1.msd b/mps-generator-execution-plugin-test/testdata/SimpleProject/solutions/Solution1/Solution1.msd new file mode 100644 index 00000000..f015948f --- /dev/null +++ b/mps-generator-execution-plugin-test/testdata/SimpleProject/solutions/Solution1/Solution1.msd @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/mps-generator-execution-plugin-test/testdata/SimpleProject/solutions/Solution1/models/Solution1.model1.mps b/mps-generator-execution-plugin-test/testdata/SimpleProject/solutions/Solution1/models/Solution1.model1.mps new file mode 100644 index 00000000..64b0724b --- /dev/null +++ b/mps-generator-execution-plugin-test/testdata/SimpleProject/solutions/Solution1/models/Solution1.model1.mps @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/mps-generator-execution-plugin/build.gradle.kts b/mps-generator-execution-plugin/build.gradle.kts index 095a2d97..3f6a2591 100644 --- a/mps-generator-execution-plugin/build.gradle.kts +++ b/mps-generator-execution-plugin/build.gradle.kts @@ -1,4 +1,3 @@ -import org.jetbrains.kotlin.gradle.dsl.JvmTarget import org.jetbrains.kotlin.gradle.dsl.KotlinVersion import org.modelix.excludeMPSLibraries import org.modelix.mpsHomeDir @@ -17,30 +16,10 @@ plugins { group = "org.modelix.mps" -java { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 - toolchain { - languageVersion.set(JavaLanguageVersion.of(11)) - } - withSourcesJar() -} - -tasks.compileJava { - options.release = 11 -} - -tasks.compileKotlin { - kotlinOptions { - jvmTarget = "11" - freeCompilerArgs += listOf("-Xjvm-default=all-compatibility") - apiVersion = "1.6" - } -} - kotlin { + jvmToolchain(11) compilerOptions { - jvmTarget.set(JvmTarget.JVM_11) + freeCompilerArgs.addAll("-Xjvm-default=all-compatibility") } sourceSets { main { @@ -56,18 +35,14 @@ dependencies { implementation(dependencyNotation, excludeMPSLibraries) } - implementationWithoutBundled(coreLibs.ktor.server.html.builder) - implementationWithoutBundled(coreLibs.ktor.server.netty) - implementationWithoutBundled(coreLibs.ktor.server.cors) - implementationWithoutBundled(coreLibs.ktor.server.status.pages) - implementationWithoutBundled(coreLibs.kotlin.logging) + implementation(coreLibs.ktor.server.html.builder) + implementation(coreLibs.ktor.server.netty) + implementation(coreLibs.ktor.server.cors) + implementation(coreLibs.ktor.server.status.pages) + implementation(coreLibs.kotlin.logging) + implementation(libs.modelix.mpsApi) compileOnly(mpsHomeDir.map { it.files("languages/languageDesign/jetbrains.mps.lang.core.jar") }) - - testImplementation(coreLibs.kotlin.coroutines.test, excludeMPSLibraries) - testImplementation(coreLibs.ktor.server.test.host, excludeMPSLibraries) - testImplementation(coreLibs.ktor.client.cio, excludeMPSLibraries) - testImplementation(libs.zt.zip, excludeMPSLibraries) } // Configure Gradle IntelliJ Plugin @@ -75,16 +50,12 @@ dependencies { intellij { localPath = mpsHomeDir.map { it.asFile.absolutePath } instrumentCode = false - plugins = listOf( - "Git4Idea", - "jetbrains.mps.vcs", - ) } tasks { patchPluginXml { - sinceBuild.set("211") // 203 not supported, because VersionFixer was replaced by ModuleDependencyVersions in 211 - untilBuild.set("232.10072.781") + sinceBuild.set("203") + untilBuild.set("243.*") } test { @@ -97,7 +68,16 @@ tasks { // ).contains(mpsPlatformVersion) // incompatibility of ktor 3 with the bundled coroutines version - enabled = false + enabled = true + + jvmArgs("-Dintellij.platform.load.app.info.from.resources=true") + val arch = System.getProperty("os.arch") + val jnaDir = mpsHomeDir.get().asFile.resolve("lib/jna/$arch") + if (jnaDir.exists()) { + jvmArgs("-Djna.boot.library.path=${jnaDir.absolutePath}") + jvmArgs("-Djna.noclasspath=true") + jvmArgs("-Djna.nosys=true") + } } buildSearchableOptions { @@ -111,9 +91,9 @@ tasks { val mpsPluginDir = project.findProperty("mps$mpsPlatformVersion.plugins.dir")?.toString()?.let { file(it) } if (mpsPluginDir != null && mpsPluginDir.isDirectory) { - create("installMpsPlugin") { + register("installMpsPlugin") { dependsOn(prepareSandbox) - from(buildDir.resolve("idea-sandbox/plugins/mps-generator-execution-plugin")) + from(layout.buildDirectory.dir("idea-sandbox/plugins/mps-generator-execution-plugin")) into(mpsPluginDir.resolve("mps-generator-execution-plugin")) } } @@ -130,7 +110,3 @@ publishing { } } } - -fun Provider.dir(name: String): Provider = map { it.dir(name) } -fun Provider.file(name: String): Provider = map { it.file(name) } -fun Provider.dir(name: Provider): Provider = flatMap { it.dir(name) } diff --git a/mps-generator-execution-plugin/src/main/kotlin/org/modelix/mps/generator/web/AsyncGenerator.kt b/mps-generator-execution-plugin/src/main/kotlin/org/modelix/mps/generator/web/AsyncGenerator.kt index 87632782..e8ffe0c7 100644 --- a/mps-generator-execution-plugin/src/main/kotlin/org/modelix/mps/generator/web/AsyncGenerator.kt +++ b/mps-generator-execution-plugin/src/main/kotlin/org/modelix/mps/generator/web/AsyncGenerator.kt @@ -24,8 +24,6 @@ import jetbrains.mps.messages.IMessageHandler import jetbrains.mps.messages.Message import jetbrains.mps.messages.MessageKind import jetbrains.mps.project.Project -import jetbrains.mps.project.ProjectManager -import jetbrains.mps.smodel.MPSModuleRepository import jetbrains.mps.smodel.resources.ModelsToResources import jetbrains.mps.text.TextGenResult import jetbrains.mps.text.TextUnit @@ -37,6 +35,8 @@ import org.jetbrains.mps.openapi.model.SModel import org.jetbrains.mps.openapi.model.SModelReference import org.jetbrains.mps.openapi.module.ModelAccess import org.jetbrains.mps.openapi.util.ProgressMonitor +import org.modelix.mps.api.ModelixMpsApi +import java.lang.reflect.Constructor import java.util.Collections import java.util.concurrent.Future @@ -70,8 +70,7 @@ class AsyncGenerator { // most of the following code is similar to the one in // https://github.com/JetBrains/MPS/blob/master/plugins/mps-make/source_gen/jetbrains/mps/ide/make/actions/TextPreviewModel_Action.java - val projects = ProjectManager.getInstance().openedProjects - val project: Project = projects.first() + val project = ModelixMpsApi.getMPSProject() as Project val textGenName = resolveFacetName("jetbrains.mps.make.facets.TextGen", "jetbrains.mps.lang.core.TextGen") val scr: IScript = @@ -124,7 +123,10 @@ class AsyncGenerator { // Some facets where moved to a different package in MPS 2022.3 // See https://github.com/JetBrains/MPS/commit/e15a11d4b5e84ff3372365cdab832c56b68b7050 // To make the plugin compatible with version before and after the renaming we have to look up the correct name. - val facetRegistry = ProjectManager.getInstance().openedProjects.mapNotNull { it.getComponent(FacetRegistry::class.java) }.first() + val facetRegistry = ModelixMpsApi.getMPSProjects() + .filterIsInstance() + .mapNotNull { it.getComponent(FacetRegistry::class.java) } + .first() val resolvedFacet = alternativeNames.asSequence().mapNotNull { facetRegistry.lookup(IFacet.Name(it)) }.firstOrNull() return requireNotNull(resolvedFacet) { "None of the facet names found: $alternativeNames" @@ -132,7 +134,7 @@ class AsyncGenerator { } private fun computeModelHash(model: SModel): String? { - return MPSModuleRepository.getInstance().getModelAccess().computeRead { + return ModelixMpsApi.getRepository().modelAccess.computeRead { (model as? GeneratableSModel)?.modelHash } } @@ -153,7 +155,7 @@ class AsyncGenerator { } val makeSeq: MakeSequence = MakeSequence(resources, script, session) val ctl: IScriptController = this.completeController(controller, session) - val task: CoreMakeTask = CoreMakeTask(scrName, makeSeq, ctl, session.getMessageHandler()) + val task: CoreMakeTask = createCoreMakeTask(makeSeq, ctl, session.getMessageHandler()) task.run(monitor) return FutureValue(task.getResult()) } @@ -207,3 +209,23 @@ fun ModelAccess.computeRead(body: () -> R): R { } return result as R } + +/** + * Using reflection because of a breaking change in MPS. + */ +private fun createCoreMakeTask(makeSeq: MakeSequence, ctl: IScriptController, mh: IMessageHandler): CoreMakeTask { + val taskConstructor = CoreMakeTask::class.java.firstConstructor() + val parameterValues = taskConstructor.parameterTypes.map { + when (it) { + String::class.java -> "" + MakeSequence::class.java -> makeSeq + IScriptController::class.java -> ctl + IMessageHandler::class.java -> mh + else -> error("Unexpected parameter: $it") + } + }.toTypedArray() + return taskConstructor.newInstance(*parameterValues) +} + +@Suppress("UNCHECKED_CAST") +private fun Class.firstConstructor() = CoreMakeTask::class.java.constructors.first() as Constructor diff --git a/mps-generator-execution-plugin/src/main/kotlin/org/modelix/mps/generator/web/GeneratorExecutionServer.kt b/mps-generator-execution-plugin/src/main/kotlin/org/modelix/mps/generator/web/GeneratorExecutionServer.kt index d34f10dd..a67afbf3 100644 --- a/mps-generator-execution-plugin/src/main/kotlin/org/modelix/mps/generator/web/GeneratorExecutionServer.kt +++ b/mps-generator-execution-plugin/src/main/kotlin/org/modelix/mps/generator/web/GeneratorExecutionServer.kt @@ -72,7 +72,7 @@ class GeneratorServer : Disposable { fun ensureStarted() { if (server != null) return - println("starting diff server") + println("starting generator server") server = embeddedServer(Netty, port = 33335) { initKtorServer(this) @@ -123,8 +123,9 @@ class GeneratorServer : Disposable { } } -class GeneratorServerStartupActivity : StartupActivity.Background { +class GeneratorServerStartupActivity : StartupActivity { override fun runActivity(project: Project) { + println(this::class.java.name + " called") project.service() // just ensure it's initialized } } diff --git a/mps-generator-execution-plugin/src/main/resources/META-INF/plugin.xml b/mps-generator-execution-plugin/src/main/resources/META-INF/plugin.xml index 965667c0..18d73d84 100644 --- a/mps-generator-execution-plugin/src/main/resources/META-INF/plugin.xml +++ b/mps-generator-execution-plugin/src/main/resources/META-INF/plugin.xml @@ -24,8 +24,6 @@ - - - + diff --git a/mps-generator-execution-plugin/src/test/kotlin/GeneratorPluginTest.kt b/mps-generator-execution-plugin/src/test/kotlin/GeneratorPluginTest.kt deleted file mode 100644 index 1a5c2ca8..00000000 --- a/mps-generator-execution-plugin/src/test/kotlin/GeneratorPluginTest.kt +++ /dev/null @@ -1,24 +0,0 @@ -import jetbrains.mps.ide.project.ProjectHelper -import org.modelix.mps.generator.web.AsyncGenerator -import org.modelix.mps.generator.web.computeRead -import kotlin.time.ExperimentalTime - -class GeneratorPluginTest : GeneratorPluginTestBase() { - - @OptIn(ExperimentalTime::class) - fun testAsyncGenerator() = kotlinx.coroutines.test.runTest { - val generator = AsyncGenerator() - val mpsProject = checkNotNull(ProjectHelper.fromIdeaProject(project)) { "MPSProject is null" } - val repository = mpsProject.repository - val model = repository.modelAccess.computeRead { - // just some random model - val module = repository.modules.first { it.moduleName == "jetbrains.mps.baseLanguage.tuples.runtime" } - module.models.first { it.name.longName == "jetbrains.mps.baseLanguage.tuples.runtime" } - } - val expected = repository.modelAccess.computeRead { model.rootNodes.count() } - val output = generator.getTextGenOutput(model) - val files = output.outputFiles.await() - assertTrue(files.isNotEmpty()) - assertEquals(expected, files.size) - } -} diff --git a/mps-generator-execution-plugin/src/test/kotlin/GeneratorPluginTestBase.kt b/mps-generator-execution-plugin/src/test/kotlin/GeneratorPluginTestBase.kt deleted file mode 100644 index 6abcc378..00000000 --- a/mps-generator-execution-plugin/src/test/kotlin/GeneratorPluginTestBase.kt +++ /dev/null @@ -1,52 +0,0 @@ -import com.intellij.openapi.application.ApplicationManager -import com.intellij.serviceContainer.AlreadyDisposedException -import com.intellij.testFramework.EdtTestUtil -import com.intellij.testFramework.HeavyPlatformTestCase -import io.ktor.client.HttpClient -import io.ktor.server.testing.ApplicationTestBuilder -import io.ktor.server.testing.testApplication -import org.modelix.mps.generator.web.GeneratorServer -import java.nio.file.Path - -abstract class GeneratorPluginTestBase() : HeavyPlatformTestCase() { - protected lateinit var projectDir: Path - protected lateinit var httpClient: HttpClient - protected lateinit var generatorServer: GeneratorServer - - override fun tearDown() { - EdtTestUtil.runInEdtAndGet<_, Throwable> { - try { - super.tearDown() - } catch (ex: AlreadyDisposedException) { - Exception("Ignoring exception", ex).printStackTrace() - } - } - } - - override fun setUp() { - EdtTestUtil.runInEdtAndGet<_, Throwable> { - super.setUp() - } - } - - protected fun runTestWithGeneratorServer(block: suspend ApplicationTestBuilder.() -> Unit) = testApplication { - val generatorServer = ApplicationManager.getApplication().getService(GeneratorServer::class.java) - this@GeneratorPluginTestBase.generatorServer = generatorServer - generatorServer.registerProject(project) - application { - generatorServer.initKtorServer(this) - } - block() - } - - override fun runInDispatchThread(): Boolean = false - - override fun isCreateDirectoryBasedProject(): Boolean = true - - override fun getProjectDirOrFile(isDirectoryBasedProject: Boolean): Path { - val projectDir = super.getProjectDirOrFile(isDirectoryBasedProject) - println("projectDir: " + projectDir) - this.projectDir = projectDir - return projectDir - } -} diff --git a/mps-legacy-sync-plugin/build.gradle.kts b/mps-legacy-sync-plugin/build.gradle.kts index f717f7a7..e29a72b3 100644 --- a/mps-legacy-sync-plugin/build.gradle.kts +++ b/mps-legacy-sync-plugin/build.gradle.kts @@ -6,7 +6,6 @@ import org.modelix.buildtools.ModuleIdAndName import org.modelix.buildtools.StubsSolutionGenerator import org.modelix.mpsHomeDir import org.modelix.mpsPlatformVersion -import java.util.zip.ZipInputStream buildscript { dependencies { @@ -189,31 +188,6 @@ tasks { intoChild(pluginName.map { "$it/languages" }) .from(project.layout.projectDirectory.dir("repositoryconcepts")) } - - val checkBinaryCompatibility by registering { - group = "verification" - doLast { - val ignoredFiles = setOf( - "META-INF/MANIFEST.MF", - "org/modelix/model/mpsplugin/AllowedBinaryIncompatibilityKt.class", - ) - fun loadEntries(fileName: String) = rootProject.layout.buildDirectory - .dir("binary-compatibility") - .dir(project.name) - .file(fileName) - .get().asFile.inputStream().use { - val zip = ZipInputStream(it) - val entries = generateSequence { zip.nextEntry } - entries.associate { it.name to "size:${it.size},crc:${it.crc}" } - } - ignoredFiles - val entriesA = loadEntries("a.jar") - val entriesB = loadEntries("b.jar") - val mismatches = (entriesA.keys + entriesB.keys).map { it to (entriesA[it] to entriesB[it]) }.filter { it.second.first != it.second.second } - check(mismatches.isEmpty()) { - "The following files have a different content:\n" + mismatches.joinToString("\n") { " ${it.first}: ${it.second.first} != ${it.second.second}" } - } - } - } } publishing { diff --git a/settings.gradle.kts b/settings.gradle.kts index 8abc8d9a..bced805c 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -28,6 +28,8 @@ rootProject.name = "modelix.mps-plugins" include("mps-legacy-sync-plugin") include("mps-diff-plugin") +include("mps-diff-plugin-test") include("mps-generator-execution-plugin") +include("mps-generator-execution-plugin-test") include("mps-sync-plugin") include("mps-sync-plugin-lib")