From b6035956cee33a9decee414420469b34cfb06fa5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sergej=20Ko=C5=A1=C4=8Dejev?= Date: Wed, 11 Nov 2026 19:31:30 +0100 Subject: [PATCH 1/2] fix(daemon): give solutions of generated presets the default output path A solution created with the text, java, java-tests or java-mps-plugin preset had no output root, so MPS saved generatorOutputPath="" and could not generate its models. Set the output root to ${module}/source_gen, as for languages; MPS then omits the attribute. Fixes #199 --- .../cli/CreatedSolutionIntegrationTest.kt | 59 +++++++++++++++++++ .../mops/daemon/ModuleCreator.kt | 12 +++- .../daemon/ModuleCreationSemanticsTest.kt | 14 +++++ 3 files changed, 82 insertions(+), 3 deletions(-) create mode 100644 cli/src/integrationTest/kotlin/com/specificlanguages/mops/cli/CreatedSolutionIntegrationTest.kt diff --git a/cli/src/integrationTest/kotlin/com/specificlanguages/mops/cli/CreatedSolutionIntegrationTest.kt b/cli/src/integrationTest/kotlin/com/specificlanguages/mops/cli/CreatedSolutionIntegrationTest.kt new file mode 100644 index 00000000..4d3d8a9e --- /dev/null +++ b/cli/src/integrationTest/kotlin/com/specificlanguages/mops/cli/CreatedSolutionIntegrationTest.kt @@ -0,0 +1,59 @@ +package com.specificlanguages.mops.cli + +import com.specificlanguages.mops.protocol.MakeOutcome +import com.specificlanguages.mops.protocol.MakeResponse +import com.specificlanguages.mops.protocol.ProtocolJson +import org.junit.jupiter.api.io.CleanupMode +import org.junit.jupiter.api.io.TempDir +import org.junit.jupiter.api.parallel.ResourceLock +import java.nio.file.Path +import kotlin.io.path.createDirectories +import kotlin.io.path.pathString +import kotlin.io.path.writeText +import kotlin.test.Test +import kotlin.test.assertEquals + +/** Creates a solution with `mops create solution`, adds a test to it, and makes and tests it in a worker process. */ +@ResourceLock("system-streams") +class CreatedSolutionIntegrationTest { + @TempDir(cleanup = CleanupMode.ON_SUCCESS) + lateinit var tempDir: Path + + @Test + fun `a solution created with the java-tests preset makes and runs its tests`() { + val project = copyTestProject("base-language-sandbox", tempDir.resolve("project")) + val home = tempDir.resolve("daemon-home").createDirectories() + fun cli(vararg args: String) = runCommandLine(project, "--daemon-home", home.pathString, *javaAndMpsHomeArgs(), *args) + try { + assertEquals(0, cli("create", "solution", "created.tests", "--usage-preset", "java-tests").exitCode) + assertEquals(0, cli("create", "model", "created.tests.main", "--module", "created.tests").exitCode) + val script = tempDir.resolve("content.groovy") + script.writeText(""" + project.command { + def module = mops.lookup.requireModule('created.tests') + def descriptor = module.moduleDescriptor + ['49808fad-9d41-4b96-83fa-9231640f6b2b(JUnit)', '6354ebe7-c22a-4a0f-ac54-50b52ab9b065(JDK)'].each { + descriptor.dependencies.add(new jetbrains.mps.project.structure.modules.Dependency( + org.jetbrains.mps.openapi.persistence.PersistenceFacade.instance.createModuleReference(it), false)) + } + module.setModuleDescriptor(descriptor) + module.save() + mops.parsing.java.addJavaClassesFromString(mops.lookup.requireModel('created.tests.main'), + 'public class CreatedTest extends junit.framework.TestCase { public void testSum() { assertEquals(2, 1 + 1); } }').unresolved + } + """.trimIndent()) + val content = cli("code", "run", script.pathString) + assertEquals(0, content.exitCode, content.output) + assertEquals("[]", content.stdout.trim(), content.output) + + val make = cli("make", "module", "created.tests", "--json") + assertEquals(0, make.exitCode, make.output) + assertEquals(MakeOutcome.SUCCESS, (ProtocolJson.decodeResponse(make.stdout) as MakeResponse).outcome, make.output) + val test = cli("test", "created.tests", "--no-build", "--json") + assertEquals(0, test.exitCode, test.output) + val report = ProtocolJson.testReport.decode(test.stdout) + assertEquals("SUCCESS", report.outcome, test.output) + assertEquals(listOf("PASSED"), report.results.filter { it.kind == "TEST" }.map { it.status }, test.output) + } finally { stopDaemons(project, home) } + } +} diff --git a/daemon/src/main/kotlin/com/specificlanguages/mops/daemon/ModuleCreator.kt b/daemon/src/main/kotlin/com/specificlanguages/mops/daemon/ModuleCreator.kt index 1b343657..a3ddffcd 100644 --- a/daemon/src/main/kotlin/com/specificlanguages/mops/daemon/ModuleCreator.kt +++ b/daemon/src/main/kotlin/com/specificlanguages/mops/daemon/ModuleCreator.kt @@ -140,7 +140,7 @@ class ModuleCreator(private val project: Project) { moduleFacetDescriptors.add(JavaModuleFacetImpl.forJavaCodeModule( JavaModuleFacet.Compile.MPS, JavaModuleFacet.LoadClasses.ManagedByMPS, JavaModuleFacet.LoadExtensions.Plugin, )) - outputRoot = "\${module}/source_gen" + outputRoot = SOURCE_GEN } return (ModuleRepositoryFacade(mpsProject).instantiate(value, descriptorFile) as Language) .also { mpsProject.addModule(it); it.setChanged() } @@ -156,7 +156,7 @@ class ModuleCreator(private val project: Project) { SolutionUsagePreset.NOT_GENERATED -> Unit SolutionUsagePreset.TEXT -> moduleFacetDescriptors.add(ModuleFacetDescriptor("plaintext", MementoImpl().apply { put("folders", "true") - put("root", "\${module}/source_gen") + put("root", SOURCE_GEN) })) SolutionUsagePreset.JAVA -> moduleFacetDescriptors.add(JavaModuleFacetImpl.forNewJavaCodeModule()) SolutionUsagePreset.JAVA_TESTS -> { @@ -167,6 +167,7 @@ class ModuleCreator(private val project: Project) { JavaModuleFacet.Compile.MPS, JavaModuleFacet.LoadClasses.ManagedByMPS, JavaModuleFacet.LoadExtensions.Plugin, )) } + if (preset != SolutionUsagePreset.NOT_GENERATED) outputRoot = SOURCE_GEN } return (instantiate(value, file(descriptor)) as Solution).also { mpsProject.addModule(it); it.setChanged() } } @@ -263,7 +264,7 @@ class ModuleCreator(private val project: Project) { private fun plannedFacets(preset: SolutionUsagePreset): List = when (preset) { SolutionUsagePreset.NOT_GENERATED -> emptyList() - SolutionUsagePreset.TEXT -> listOf(FacetMementoJson("plaintext", mapOf("folders" to "true", "root" to "\${module}/source_gen"))) + SolutionUsagePreset.TEXT -> listOf(FacetMementoJson("plaintext", mapOf("folders" to "true", "root" to SOURCE_GEN))) SolutionUsagePreset.JAVA -> listOf(FacetMementoJson("java", mapOf("compile" to "mps", "classes" to "mps", "ext" to "no"))) SolutionUsagePreset.JAVA_TESTS -> listOf( FacetMementoJson("java", mapOf("compile" to "mps", "classes" to "mps", "ext" to "no")), @@ -279,4 +280,9 @@ class ModuleCreator(private val project: Project) { private fun file(path: Path) = mpsProject.fileSystem.getFile(path.toString()) private fun fail(code: MpsErrorCode, message: String): Nothing = throw MpsRequestException(code, message) + + private companion object { + /** The generator output root that MPS uses when a module descriptor does not name one. */ + const val SOURCE_GEN = "\${module}/source_gen" + } } diff --git a/daemon/src/test/kotlin/com/specificlanguages/mops/daemon/ModuleCreationSemanticsTest.kt b/daemon/src/test/kotlin/com/specificlanguages/mops/daemon/ModuleCreationSemanticsTest.kt index 6de91aae..6b21d565 100644 --- a/daemon/src/test/kotlin/com/specificlanguages/mops/daemon/ModuleCreationSemanticsTest.kt +++ b/daemon/src/test/kotlin/com/specificlanguages/mops/daemon/ModuleCreationSemanticsTest.kt @@ -8,6 +8,8 @@ import com.specificlanguages.mops.protocol.GeneratorPersistence import com.specificlanguages.mops.protocol.ModuleCreationResponse import com.specificlanguages.mops.protocol.ModuleKind import com.specificlanguages.mops.protocol.SolutionUsagePreset +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.EnumSource import kotlin.io.path.exists import kotlin.io.path.readText import kotlin.test.assertIs @@ -35,6 +37,18 @@ class ModuleCreationSemanticsTest { } } + @ParameterizedTest + @EnumSource(SolutionUsagePreset::class, names = ["TEXT", "JAVA", "JAVA_TESTS", "JAVA_MPS_PLUGIN"]) + fun `solution of a generated preset keeps the default generator output path`(preset: SolutionUsagePreset) { + SharedMpsEnvironment.withOpenProjectCopy { project, projectPath -> + DomainRequestHandler(projectPath, SharedMpsEnvironment.mpsAccess(project)) + .handleDomainRequest(CreateSolutionRequest("", "example.generated", usagePreset = preset)) + + val descriptor = projectPath.resolve("solutions/example.generated/example.generated.msd").readText() + assertFalse("generatorOutputPath" in descriptor, descriptor) + } + } + @Test fun `solution report names the usage preset and the facets it produced`() { SharedMpsEnvironment.withOpenProjectCopy { project, _ -> From 37843aad4946006ed47741e2beeb5c242ace6f8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sergej=20Ko=C5=A1=C4=8Dejev?= Date: Wed, 11 Nov 2026 19:47:45 +0100 Subject: [PATCH 2/2] refactor(daemon): name the default output root constant after its role --- .../mops/cli/CreatedSolutionIntegrationTest.kt | 3 ++- .../com/specificlanguages/mops/daemon/ModuleCreator.kt | 10 +++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/cli/src/integrationTest/kotlin/com/specificlanguages/mops/cli/CreatedSolutionIntegrationTest.kt b/cli/src/integrationTest/kotlin/com/specificlanguages/mops/cli/CreatedSolutionIntegrationTest.kt index 4d3d8a9e..906ffe4f 100644 --- a/cli/src/integrationTest/kotlin/com/specificlanguages/mops/cli/CreatedSolutionIntegrationTest.kt +++ b/cli/src/integrationTest/kotlin/com/specificlanguages/mops/cli/CreatedSolutionIntegrationTest.kt @@ -44,7 +44,8 @@ class CreatedSolutionIntegrationTest { """.trimIndent()) val content = cli("code", "run", script.pathString) assertEquals(0, content.exitCode, content.output) - assertEquals("[]", content.stdout.trim(), content.output) + val unresolvedReferences = content.stdout.trim() + assertEquals("[]", unresolvedReferences, content.output) val make = cli("make", "module", "created.tests", "--json") assertEquals(0, make.exitCode, make.output) diff --git a/daemon/src/main/kotlin/com/specificlanguages/mops/daemon/ModuleCreator.kt b/daemon/src/main/kotlin/com/specificlanguages/mops/daemon/ModuleCreator.kt index a3ddffcd..e4eb8e0a 100644 --- a/daemon/src/main/kotlin/com/specificlanguages/mops/daemon/ModuleCreator.kt +++ b/daemon/src/main/kotlin/com/specificlanguages/mops/daemon/ModuleCreator.kt @@ -140,7 +140,7 @@ class ModuleCreator(private val project: Project) { moduleFacetDescriptors.add(JavaModuleFacetImpl.forJavaCodeModule( JavaModuleFacet.Compile.MPS, JavaModuleFacet.LoadClasses.ManagedByMPS, JavaModuleFacet.LoadExtensions.Plugin, )) - outputRoot = SOURCE_GEN + outputRoot = DEFAULT_OUTPUT_ROOT } return (ModuleRepositoryFacade(mpsProject).instantiate(value, descriptorFile) as Language) .also { mpsProject.addModule(it); it.setChanged() } @@ -156,7 +156,7 @@ class ModuleCreator(private val project: Project) { SolutionUsagePreset.NOT_GENERATED -> Unit SolutionUsagePreset.TEXT -> moduleFacetDescriptors.add(ModuleFacetDescriptor("plaintext", MementoImpl().apply { put("folders", "true") - put("root", SOURCE_GEN) + put("root", "\${module}/source_gen") })) SolutionUsagePreset.JAVA -> moduleFacetDescriptors.add(JavaModuleFacetImpl.forNewJavaCodeModule()) SolutionUsagePreset.JAVA_TESTS -> { @@ -167,7 +167,7 @@ class ModuleCreator(private val project: Project) { JavaModuleFacet.Compile.MPS, JavaModuleFacet.LoadClasses.ManagedByMPS, JavaModuleFacet.LoadExtensions.Plugin, )) } - if (preset != SolutionUsagePreset.NOT_GENERATED) outputRoot = SOURCE_GEN + if (preset != SolutionUsagePreset.NOT_GENERATED) outputRoot = DEFAULT_OUTPUT_ROOT } return (instantiate(value, file(descriptor)) as Solution).also { mpsProject.addModule(it); it.setChanged() } } @@ -264,7 +264,7 @@ class ModuleCreator(private val project: Project) { private fun plannedFacets(preset: SolutionUsagePreset): List = when (preset) { SolutionUsagePreset.NOT_GENERATED -> emptyList() - SolutionUsagePreset.TEXT -> listOf(FacetMementoJson("plaintext", mapOf("folders" to "true", "root" to SOURCE_GEN))) + SolutionUsagePreset.TEXT -> listOf(FacetMementoJson("plaintext", mapOf("folders" to "true", "root" to "\${module}/source_gen"))) SolutionUsagePreset.JAVA -> listOf(FacetMementoJson("java", mapOf("compile" to "mps", "classes" to "mps", "ext" to "no"))) SolutionUsagePreset.JAVA_TESTS -> listOf( FacetMementoJson("java", mapOf("compile" to "mps", "classes" to "mps", "ext" to "no")), @@ -283,6 +283,6 @@ class ModuleCreator(private val project: Project) { private companion object { /** The generator output root that MPS uses when a module descriptor does not name one. */ - const val SOURCE_GEN = "\${module}/source_gen" + const val DEFAULT_OUTPUT_ROOT = "\${module}/source_gen" } }