IJPL-224042 IJ-MR-181153 fix PluginDependencyGeneratorTest

GitOrigin-RevId: 9bf46caa09a523952c3c1a604e664ac38753d9f1
This commit is contained in:
Vladimir Krivosheev
2025-12-20 16:34:23 +00:00
committed by intellij-monorepo-bot
parent 3a05faea81
commit 6e8596f7e9
9 changed files with 73 additions and 33 deletions
@@ -10,6 +10,11 @@ interface ModuleOutputProvider {
fun findModule(name: String): JpsModule?
/**
* Returns the path to the module's .iml file.
*/
fun getModuleImlFile(module: JpsModule): Path
fun findRequiredModule(name: String): JpsModule
fun findLibraryRoots(libraryName: String, moduleLibraryModuleName: String? = null): List<Path>
@@ -228,7 +228,11 @@ data class GenerationResult(
@JvmField val errors: List<ValidationError>,
@JvmField val diffs: List<FileDiff>,
@JvmField val stats: GenerationStats,
)
) {
/** Combined list of all validation issues (errors + diffs) */
val allIssues: List<ValidationError>
get() = errors + diffs
}
/**
* Generates all module sets and products with validation.
@@ -132,14 +132,4 @@ internal data class ProductModuleIndex(
// endregion
// region Structured Output for Tests
/**
* Result of model generator validation.
* Contains all validation issues (diffs, missing dependencies, etc.) in a unified list.
*/
data class ModelValidationResult(
@JvmField val issues: List<ValidationError>,
)
// endregion
@@ -13,7 +13,6 @@ import org.jetbrains.jps.model.java.JpsJavaExtensionService
import org.jetbrains.jps.model.module.JpsLibraryDependency
import org.jetbrains.jps.model.module.JpsModuleDependency
import org.jetbrains.jps.model.module.JpsModuleReference
import org.jetbrains.jps.model.serialization.JpsModelSerializationDataService
import java.nio.file.Files
/**
@@ -130,10 +129,7 @@ private fun applyLibraryModuleFix(
strategy: FileUpdateStrategy,
) {
val module = outputProvider.findModule(moduleName) ?: return
val imlDir = requireNotNull(JpsModelSerializationDataService.getBaseDirectoryPath(module)) {
"Cannot find base directory for module $moduleName"
}
val imlFile = imlDir.resolve("${module.name}.iml")
val imlFile = outputProvider.getModuleImlFile(module)
val currentContent = Files.readString(imlFile)
val fixedContent = applyLibraryModuleFixes(currentContent, violations)
@@ -424,4 +424,8 @@ private class MockModuleOutputProvider : ModuleOutputProvider {
override fun getModuleOutputRoots(module: JpsModule, forTests: Boolean): List<Path> {
throw UnsupportedOperationException("Not available in mock")
}
override fun getModuleImlFile(module: JpsModule): Path {
throw UnsupportedOperationException("Not available in mock")
}
}
@@ -12,6 +12,7 @@ import org.jetbrains.jps.model.java.JpsJavaExtensionService
import org.jetbrains.jps.model.java.JpsJavaLibraryType
import org.jetbrains.jps.model.java.JpsJavaModuleType
import org.jetbrains.jps.model.module.JpsModule
import org.jetbrains.jps.model.serialization.JpsModelSerializationDataService
import org.jetbrains.jps.model.serialization.impl.JpsModuleSerializationDataExtensionImpl
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.io.TempDir
@@ -239,7 +240,18 @@ class PluginDependencyGeneratorTest {
@Test
fun `validateLibraryModuleDependencies detects direct library dependencies`(@TempDir tempDir: Path) {
// 1. Create test JPS project with modules
// 1. Create test .iml file with direct library dependency
val imlContent = """
|<?xml version="1.0" encoding="UTF-8"?>
|<module type="JAVA_MODULE" version="4">
| <component name="NewModuleRootManager">
| <orderEntry type="library" scope="TEST" name="JUnit4" level="project" />
| </component>
|</module>
""".trimMargin()
Files.writeString(tempDir.resolve("test.plugin.iml"), imlContent)
// 2. Create test JPS project with modules
val model = JpsElementFactory.getInstance().createModel()
val project = model.project
@@ -256,15 +268,19 @@ class PluginDependencyGeneratorTest {
// Create plugin module with DIRECT library dependency (the violation!)
val pluginModule = project.addModule("test.plugin", JpsJavaModuleType.INSTANCE)
pluginModule.container.setChild(
JpsModuleSerializationDataExtensionImpl.ROLE,
JpsModuleSerializationDataExtensionImpl(tempDir),
)
val pluginLibDep = pluginModule.dependenciesList.addLibraryDependency(junit4Library)
JpsJavaExtensionService.getInstance().getOrCreateDependencyExtension(pluginLibDep).apply {
scope = JpsJavaDependencyScope.TEST
}
// 2. Create test ModuleOutputProvider
// 3. Create test ModuleOutputProvider
val outputProvider = createTestModuleOutputProvider(project)
// 3. Call validateLibraryModuleDependencies with strategy
// 4. Call validateLibraryModuleDependencies with strategy
val strategy = DeferredFileUpdater(tempDir)
validateLibraryModuleDependencies(
modulesToCheck = setOf("intellij.libraries.junit4", "test.plugin"),
@@ -272,11 +288,10 @@ class PluginDependencyGeneratorTest {
strategy = strategy,
)
// 4. Verify no diff is returned because mock modules don't have real .iml files on disk
// In real usage, diffs contain the .iml changes needed for auto-fix
// 5. Verify diff is returned for the library dependency violation
assertThat(strategy.getDiffs())
.describedAs("No diffs returned for mock modules without .iml files")
.isEmpty()
.describedAs("Diff should be generated for library dependency violation")
.hasSize(1)
}
@Test
@@ -388,5 +403,12 @@ private fun createTestModuleOutputProvider(project: JpsProject): ModuleOutputPro
override suspend fun readFileContentFromModuleOutputAsync(module: JpsModule, relativePath: String, forTests: Boolean): ByteArray? {
throw UnsupportedOperationException("Not needed for this test")
}
override fun getModuleImlFile(module: JpsModule): Path {
val baseDir = requireNotNull(JpsModelSerializationDataService.getBaseDirectoryPath(module)) {
"Cannot find base directory for module ${module.name}"
}
return baseDir.resolve("${module.name}.iml")
}
}
}
@@ -9,6 +9,7 @@ import org.jetbrains.intellij.build.ModuleOutputProvider
import org.jetbrains.intellij.build.io.ZipEntryProcessorResult
import org.jetbrains.intellij.build.io.readZipFile
import org.jetbrains.jps.model.module.JpsModule
import org.jetbrains.jps.model.serialization.JpsModelSerializationDataService
import java.nio.file.Files
import java.nio.file.Path
import kotlin.io.path.isRegularFile
@@ -125,6 +126,13 @@ internal class BazelModuleOutputProvider(
)
}
override fun getModuleImlFile(module: JpsModule): Path {
val baseDir = requireNotNull(JpsModelSerializationDataService.getBaseDirectoryPath(module)) {
"Cannot find base directory for module ${module.name}"
}
return baseDir.resolve("${module.name}.iml")
}
override fun toString(): String = "BazelModuleOutputProvider(projectHome=$projectHome, bazelOutputRoot=$bazelOutputRoot)"
}
@@ -8,6 +8,7 @@ import org.jetbrains.jps.model.JpsProject
import org.jetbrains.jps.model.java.JpsJavaExtensionService
import org.jetbrains.jps.model.library.JpsOrderRootType
import org.jetbrains.jps.model.module.JpsModule
import org.jetbrains.jps.model.serialization.JpsModelSerializationDataService
import java.nio.file.Files
import java.nio.file.NoSuchFileException
import java.nio.file.Path
@@ -79,4 +80,11 @@ internal class JpsModuleOutputProvider(private val project: JpsProject) : Module
processedModules = processedModules,
)
}
override fun getModuleImlFile(module: JpsModule): Path {
val baseDir = requireNotNull(JpsModelSerializationDataService.getBaseDirectoryPath(module)) {
"Cannot find base directory for module ${module.name}"
}
return baseDir.resolve("${module.name}.iml")
}
}
@@ -29,8 +29,8 @@ import org.jetbrains.intellij.build.SoftwareBillOfMaterials
import org.jetbrains.intellij.build.impl.buildDistributions
import org.jetbrains.intellij.build.impl.createBuildContext
import org.jetbrains.intellij.build.impl.createCompilationContext
import org.jetbrains.intellij.build.productLayout.discovery.GenerationResult
import org.jetbrains.intellij.build.productLayout.validation.FileDiff
import org.jetbrains.intellij.build.productLayout.validation.ModelValidationResult
import org.jetbrains.intellij.build.productLayout.validation.XIncludeResolutionError
import org.jetbrains.intellij.build.productLayout.validation.formatValidationError
import org.jetbrains.intellij.build.productLayout.validation.getErrorId
@@ -60,7 +60,7 @@ private data class ContentReportList(
* The function takes the project home path and returns validation result
* containing file diffs and validation errors.
*/
typealias GeneratorValidator = suspend (projectHome: Path, outputProvider: ModuleOutputProvider) -> ModelValidationResult
typealias GeneratorValidator = suspend (projectHome: Path, outputProvider: ModuleOutputProvider) -> GenerationResult
@ApiStatus.Internal
fun createContentCheckTests(
@@ -91,9 +91,9 @@ fun createContentCheckTests(
}
// Start both tasks immediately in caller's scope (parallel after context is ready)
val validationDeferred: Deferred<ModelValidationResult> = scope.async {
val validationDeferred: Deferred<GenerationResult?> = scope.async {
val context = compilationContextDeferred.await()
modelValidator?.invoke(homePath, context.outputProvider) ?: ModelValidationResult(issues = emptyList())
modelValidator?.invoke(homePath, context.outputProvider)
}
val packagingDeferred: Deferred<PackageResult> = scope.async {
@@ -114,11 +114,12 @@ fun createContentCheckTests(
@Suppress("RunBlockingInSuspendFunction")
val validationResult = runBlocking { validationDeferred.await() }
val validationIssues = validationResult?.allIssues ?: emptyList()
if (validationResult.issues.isNotEmpty()) {
if (validationIssues.isNotEmpty()) {
// Check for xi-include errors first - they may cause cascading failures
val xiIncludeErrors = validationResult.issues.filterIsInstance<XIncludeResolutionError>()
for (issue in xiIncludeErrors.ifEmpty { validationResult.issues }) {
val xiIncludeErrors = validationIssues.filterIsInstance<XIncludeResolutionError>()
for (issue in xiIncludeErrors.ifEmpty { validationIssues }) {
val testId = if (issue is FileDiff) "file-out-of-sync:${homePath.relativize(issue.path)}" else "model-validation:${getErrorId(issue)}"
yield(DynamicTest.dynamicTest(testId) {
if (issue is FileDiff) {
@@ -148,22 +149,24 @@ fun createContentCheckTests(
}
private suspend fun SequenceScope<DynamicTest>.producePackagingTests(
validationResult: ModelValidationResult,
validationResult: GenerationResult?,
packagingDeferred: Deferred<PackageResult>,
contentYamlPath: String,
suggestedReviewer: String?,
checkPlugins: Boolean,
) {
val issues = validationResult?.allIssues ?: emptyList()
// Packaging - awaits inside test to capture timing
yield(DynamicTest.dynamicTest("packaging") {
if (validationResult.issues.isNotEmpty()) {
if (issues.isNotEmpty()) {
throw TestAbortedException("Skipped: model validation failed")
}
runBlocking { packagingDeferred.await() }
})
// Content check tests - use packaging result
if (validationResult.issues.isNotEmpty()) {
if (issues.isNotEmpty()) {
return
}