[recent files + diff] IJ-MR-209657 IJPL-242683 Add test for diff files in recent files list

(cherry picked from commit 48d6b1b7eb89233d099bd68eef1e41c354801b88)

GitOrigin-RevId: 0d0f2e8ae62cdace270414cfe8c0339ecebfa690
This commit is contained in:
Nikita Katkov
2026-06-28 13:03:18 +00:00
committed by intellij-monorepo-bot
parent a3e9476e35
commit 1620fd53ef
7 changed files with 294 additions and 0 deletions
+1
View File
@@ -1614,6 +1614,7 @@
<module fileurl="file://$PROJECT_DIR$/platform/recentFiles/backend/intellij.platform.recentFiles.backend.iml" filepath="$PROJECT_DIR$/platform/recentFiles/backend/intellij.platform.recentFiles.backend.iml" />
<module fileurl="file://$PROJECT_DIR$/platform/recentFiles/frontend/intellij.platform.recentFiles.frontend.iml" filepath="$PROJECT_DIR$/platform/recentFiles/frontend/intellij.platform.recentFiles.frontend.iml" />
<module fileurl="file://$PROJECT_DIR$/platform/recentFiles/plugin/intellij.platform.recentFiles.plugin.iml" filepath="$PROJECT_DIR$/platform/recentFiles/plugin/intellij.platform.recentFiles.plugin.iml" />
<module fileurl="file://$PROJECT_DIR$/platform/recentFiles/tests/intellij.platform.recentFiles.tests.iml" filepath="$PROJECT_DIR$/platform/recentFiles/tests/intellij.platform.recentFiles.tests.iml" />
<module fileurl="file://$PROJECT_DIR$/platform/refactoring/intellij.platform.refactoring.iml" filepath="$PROJECT_DIR$/platform/refactoring/intellij.platform.refactoring.iml" />
<module fileurl="file://$PROJECT_DIR$/platform/remote-core/intellij.platform.remote.core.iml" filepath="$PROJECT_DIR$/platform/remote-core/intellij.platform.remote.core.iml" />
<module fileurl="file://$PROJECT_DIR$/platform/remote-servers/api/intellij.platform.remoteServers.iml" filepath="$PROJECT_DIR$/platform/remote-servers/api/intellij.platform.remoteServers.iml" />
+2
View File
@@ -628,6 +628,8 @@ jvm_library(
"//xml/dom-tests:dom-tests_test_lib",
"//plugins/IntelliLang/tests",
"//plugins/IntelliLang/tests:tests_test_lib",
"//platform/recentFiles/tests",
"//platform/recentFiles/tests:tests_test_lib",
"//xml/tests",
"//xml/tests:tests_test_lib",
"//jps/model-serialization:model-serialization-tests",
+1
View File
@@ -819,6 +819,7 @@ platform/recentFiles/backend
platform/recentFiles/frontend
platform/recentFiles/plugin
platform/recentFiles/shared
platform/recentFiles/tests
platform/refactoring
platform/remote-core
platform/remote-driver/client
+1
View File
@@ -14,6 +14,7 @@
<orderEntry type="module" module-name="intellij.tasks.tests" scope="TEST" />
<orderEntry type="module" module-name="intellij.xml.dom.tests" scope="TEST" />
<orderEntry type="module" module-name="intellij.platform.langInjection.tests" scope="TEST" />
<orderEntry type="module" module-name="intellij.platform.recentFiles.tests" scope="TEST" />
<orderEntry type="module" module-name="intellij.xml.tests" scope="TEST" />
<orderEntry type="module" module-name="intellij.platform.jps.model.serialization.tests" scope="TEST" />
<orderEntry type="module" module-name="intellij.java.guiForms.jps" scope="TEST" />
+88
View File
@@ -0,0 +1,88 @@
load("@community//build:tests-options.bzl", "jps_test")
load("@rules_jvm//:jvm.bzl", "jvm_library")
### auto-generated section `build intellij.platform.recentFiles.tests` start
jvm_library(
name = "tests",
srcs = glob(
[],
allow_empty = True,
),
module_name = "intellij.platform.recentFiles.tests",
visibility = ["//visibility:public"],
runtime_deps = [
# do not sort,
"@lib//:kotlin-stdlib",
"//platform/recentFiles/plugin",
],
)
jvm_library(
name = "tests_test_lib",
testonly = True,
srcs = glob(
[
"testSrc/**/*.kt",
"testSrc/**/*.java",
"testSrc/**/*.form",
],
allow_empty = True,
),
associates = [
"//platform/recentFiles/backend",
"//platform/recentFiles/backend:backend_test_lib",
],
visibility = ["//visibility:public"],
runtime_deps = [
# do not sort,
":tests",
"//platform/recentFiles/plugin:plugin_test_lib",
"//platform/platform-impl:ide-impl_test_lib",
"//platform/platform-impl/rpc:rpc_test_lib",
],
deps = [
# do not sort,
"@lib//:kotlin-stdlib",
"//platform/recentFiles/frontend",
"//platform/recentFiles/frontend:frontend_test_lib",
"//platform/recentFiles/shared:recentFiles",
"//platform/recentFiles/shared:recentFiles_test_lib",
"//platform/testFramework/junit5",
"//platform/testFramework/junit5:junit5_test_lib",
"//platform/testFramework",
"//platform/testFramework:testFramework_test_lib",
"//libraries/junit5",
"//libraries/junit5:junit5_test_lib",
"//libraries/kotlinx/coroutines/core",
"//libraries/kotlinx/coroutines/core:core_test_lib",
"//platform/core-api:core",
"//platform/core-api:core_test_lib",
"//platform/util/concurrency",
"//platform/util/concurrency:concurrency_test_lib",
"//platform/platform-impl:ide-impl",
"//platform/platform-impl/rpc",
"//platform/kernel/rpc.backend",
"//platform/kernel/rpc.backend:rpc.backend_test_lib",
"//platform/util:util-ui",
"//platform/util:util-ui_test_lib",
],
)
### auto-generated section `build intellij.platform.recentFiles.tests` end
### auto-generated section `iml intellij.platform.recentFiles.tests` start
exports_files(
[
"intellij.platform.recentFiles.tests.iml",
],
visibility = ["//visibility:public"],
)
### auto-generated section `iml intellij.platform.recentFiles.tests` end
### auto-generated section `test intellij.platform.recentFiles.tests` start
jps_test(
name = "tests_test",
runtime_deps = [":tests_test_lib"],
)
### auto-generated section `test intellij.platform.recentFiles.tests` end
@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/testSrc" isTestSource="true" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" name="kotlin-stdlib" level="project" />
<orderEntry type="module" module-name="intellij.platform.recentFiles.backend" scope="TEST" />
<orderEntry type="module" module-name="intellij.platform.recentFiles.frontend" scope="TEST" />
<orderEntry type="module" module-name="intellij.platform.recentFiles" scope="TEST" />
<orderEntry type="module" module-name="intellij.platform.recentFiles.plugin" scope="RUNTIME" />
<orderEntry type="module" module-name="intellij.platform.testFramework.junit5" scope="TEST" />
<orderEntry type="module" module-name="intellij.platform.testFramework" scope="TEST" />
<orderEntry type="module" module-name="intellij.libraries.junit5" scope="TEST" />
<orderEntry type="module" module-name="intellij.libraries.kotlinx.coroutines.core" scope="TEST" />
<orderEntry type="module" module-name="intellij.platform.core" scope="TEST" />
<orderEntry type="module" module-name="intellij.platform.concurrency" scope="TEST" />
<orderEntry type="module" module-name="intellij.platform.ide.impl" scope="TEST" />
<orderEntry type="module" module-name="intellij.platform.ide.rpc" scope="TEST" />
<orderEntry type="module" module-name="intellij.platform.rpc.backend" scope="TEST" />
<orderEntry type="module" module-name="intellij.platform.util.ui" scope="TEST" />
</component>
<component name="TestModuleProperties" production-module="intellij.platform.recentFiles.backend" />
</module>
@@ -0,0 +1,174 @@
// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.platform.recentFiles.backend
import com.intellij.openapi.Disposable
import com.intellij.openapi.application.EDT
import com.intellij.openapi.fileEditor.impl.IdeDocumentHistoryImpl
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.platform.recentFiles.frontend.RecentFilesExcluder
import com.intellij.platform.recentFiles.frontend.model.FrontendRecentFilesModel
import com.intellij.platform.recentFiles.shared.FileChangeKind
import com.intellij.platform.recentFiles.shared.RecentFileKind
import com.intellij.testFramework.LightVirtualFile
import com.intellij.testFramework.junit5.TestApplication
import com.intellij.testFramework.junit5.TestDisposable
import com.intellij.testFramework.junit5.fixture.projectFixture
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.TimeoutCancellationException
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeout
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.fail
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds
@TestApplication
class RecentFilesDistributedModelTest {
private val projectFixture = projectFixture()
private val project by projectFixture
@TestDisposable
lateinit var disposable: Disposable
@BeforeEach
fun setUp() {
RecentFilesExcluder.EP_NAME.point.registerExtension(TestDiffRecentFilesExcluder(), disposable)
}
@Test
fun `regular file open and close are synchronized between frontend and backend models`() = runBlocking {
withModelSynchronization {
val file = LightVirtualFile("regular.txt", "content")
val anchorFile1 = LightVirtualFile("anchor1.txt", "content")
val anchorFile2 = LightVirtualFile("anchor2.txt", "content")
val openedFiles = listOf(file, anchorFile1, anchorFile2)
val frontendModel = FrontendRecentFilesModel.getInstance(project)
frontendModel.applyFrontendChanges(RecentFileKind.RECENTLY_OPENED, openedFiles, FileChangeKind.ADDED)
frontendModel.applyFrontendChanges(RecentFileKind.RECENTLY_OPENED_UNPINNED, openedFiles, FileChangeKind.ADDED)
waitUntilFileIsPresent(file, RecentFileKind.RECENTLY_OPENED)
waitUntilFileIsPresent(file, RecentFileKind.RECENTLY_OPENED_UNPINNED)
frontendModel.applyFrontendChanges(RecentFileKind.RECENTLY_OPENED_UNPINNED, listOf(file), FileChangeKind.REMOVED)
waitUntilFileIsAbsent(file, RecentFileKind.RECENTLY_OPENED_UNPINNED)
assertBothModelsContain(file, RecentFileKind.RECENTLY_OPENED)
}
}
@Test
fun `diff file open and close are synchronized between frontend and backend models`() = runBlocking {
withModelSynchronization {
val file = TestDiffVirtualFile("diff.txt")
val frontendModel = FrontendRecentFilesModel.getInstance(project)
frontendModel.applyFrontendChanges(RecentFileKind.RECENTLY_OPENED, listOf(file), FileChangeKind.ADDED)
frontendModel.applyFrontendChanges(RecentFileKind.RECENTLY_OPENED_UNPINNED, listOf(file), FileChangeKind.ADDED)
waitUntilFileIsPresent(file, RecentFileKind.RECENTLY_OPENED)
waitUntilFileIsPresent(file, RecentFileKind.RECENTLY_OPENED_UNPINNED)
file.isIncludedInHistory = false
frontendModel.applyFrontendChanges(RecentFileKind.RECENTLY_OPENED_UNPINNED, listOf(file), FileChangeKind.REMOVED)
frontendModel.applyFrontendChanges(RecentFileKind.RECENTLY_OPENED, listOf(file), FileChangeKind.REMOVED)
waitUntilFileIsAbsent(file, RecentFileKind.RECENTLY_OPENED)
waitUntilFileIsAbsent(file, RecentFileKind.RECENTLY_OPENED_UNPINNED)
}
}
private suspend fun withModelSynchronization(action: suspend CoroutineScope.() -> Unit) = coroutineScope {
val subscriptions = startModelSynchronization()
try {
action()
}
finally {
subscriptions.forEach(Job::cancel)
}
}
private fun CoroutineScope.startModelSynchronization(): List<Job> {
val backendModel = BackendRecentFilesModel.getInstance(project)
val frontendModel = FrontendRecentFilesModel.getInstance(project)
return RecentFileKind.entries.flatMap { kind ->
listOf(
launch { backendModel.subscribeToBackendRecentFilesUpdates(kind) },
launch { frontendModel.subscribeToBackendRecentFilesUpdates(kind) }
)
}
}
private suspend fun waitUntilFileIsPresent(file: VirtualFile, kind: RecentFileKind) {
waitUntil("Expected ${file.name} to appear in $kind") {
frontendFiles(kind).contains(file) && backendFiles(kind).contains(file)
}
}
private suspend fun waitUntilFileIsAbsent(file: VirtualFile, kind: RecentFileKind) {
var frontendFiles = emptyList<VirtualFile>()
var backendFiles = emptyList<VirtualFile>()
waitUntil({ "Expected ${file.name} to disappear from $kind, frontend=${frontendFiles.map { it.name }}, backend=${backendFiles.map { it.name }}" }) {
frontendFiles = frontendFiles(kind)
backendFiles = backendFiles(kind)
!frontendFiles.contains(file) && !backendFiles.contains(file)
}
}
private suspend fun assertBothModelsContain(file: VirtualFile, kind: RecentFileKind) {
val frontendFiles = frontendFiles(kind)
val backendFiles = backendFiles(kind)
assertTrue(frontendFiles.contains(file), "Expected frontend $kind to contain ${file.name}, got ${frontendFiles.map { it.name }}")
assertTrue(backendFiles.contains(file), "Expected backend $kind to contain ${file.name}, got ${backendFiles.map { it.name }}")
}
private suspend fun waitUntil(message: String, condition: suspend () -> Boolean) {
waitUntil({ message }, condition)
}
private suspend fun waitUntil(message: () -> String, condition: suspend () -> Boolean) {
try {
withTimeout(5.seconds) {
while (!condition()) {
delay(50.milliseconds)
}
}
}
catch (_: TimeoutCancellationException) {
fail(message())
}
}
private suspend fun frontendFiles(kind: RecentFileKind): List<VirtualFile> {
return withContext(Dispatchers.EDT) {
FrontendRecentFilesModel.getInstance(project).getRecentFiles(kind).mapNotNull { it.virtualFile }
}
}
private fun backendFiles(kind: RecentFileKind): List<VirtualFile> {
return BackendRecentFilesModel.getInstance(project).getFilesByKind(kind)
}
private class TestDiffVirtualFile(name: String) : LightVirtualFile(name, "diff"), IdeDocumentHistoryImpl.OptionallyIncluded {
var isIncludedInHistory: Boolean = true
override fun isIncludedInDocumentHistory(project: Project): Boolean {
return isIncludedInHistory
}
}
private class TestDiffRecentFilesExcluder : RecentFilesExcluder {
override fun isExcludedFromRecentlyOpened(project: Project, file: VirtualFile): Boolean {
return file is TestDiffVirtualFile && !file.isIncludedInDocumentHistory(project)
}
}
}