From 1620fd53ef63c45809176bbdef593c603868db1f Mon Sep 17 00:00:00 2001 From: Nikita Katkov Date: Mon, 22 Jun 2026 23:41:18 +0200 Subject: [PATCH] [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 --- .idea/modules.xml | 1 + BUILD.bazel | 2 + build/bazel-generated-file-list.txt | 1 + intellij.idea.community.main.tests.iml | 1 + platform/recentFiles/tests/BUILD.bazel | 88 +++++++++ .../intellij.platform.recentFiles.tests.iml | 27 +++ .../RecentFilesDistributedModelTest.kt | 174 ++++++++++++++++++ 7 files changed, 294 insertions(+) create mode 100644 platform/recentFiles/tests/BUILD.bazel create mode 100644 platform/recentFiles/tests/intellij.platform.recentFiles.tests.iml create mode 100644 platform/recentFiles/tests/testSrc/com/intellij/platform/recentFiles/backend/RecentFilesDistributedModelTest.kt diff --git a/.idea/modules.xml b/.idea/modules.xml index 8f018c48fbd9..4066899c77f4 100644 --- a/.idea/modules.xml +++ b/.idea/modules.xml @@ -1614,6 +1614,7 @@ + diff --git a/BUILD.bazel b/BUILD.bazel index f542074caf0d..47ce414952e5 100644 --- a/BUILD.bazel +++ b/BUILD.bazel @@ -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", diff --git a/build/bazel-generated-file-list.txt b/build/bazel-generated-file-list.txt index 106f23b8dcf5..44fb8e4c795b 100644 --- a/build/bazel-generated-file-list.txt +++ b/build/bazel-generated-file-list.txt @@ -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 diff --git a/intellij.idea.community.main.tests.iml b/intellij.idea.community.main.tests.iml index 4a05cabe74a6..dfd86a0cbbf3 100644 --- a/intellij.idea.community.main.tests.iml +++ b/intellij.idea.community.main.tests.iml @@ -14,6 +14,7 @@ + diff --git a/platform/recentFiles/tests/BUILD.bazel b/platform/recentFiles/tests/BUILD.bazel new file mode 100644 index 000000000000..4e863f6044e7 --- /dev/null +++ b/platform/recentFiles/tests/BUILD.bazel @@ -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 diff --git a/platform/recentFiles/tests/intellij.platform.recentFiles.tests.iml b/platform/recentFiles/tests/intellij.platform.recentFiles.tests.iml new file mode 100644 index 000000000000..4ad9565d0363 --- /dev/null +++ b/platform/recentFiles/tests/intellij.platform.recentFiles.tests.iml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/platform/recentFiles/tests/testSrc/com/intellij/platform/recentFiles/backend/RecentFilesDistributedModelTest.kt b/platform/recentFiles/tests/testSrc/com/intellij/platform/recentFiles/backend/RecentFilesDistributedModelTest.kt new file mode 100644 index 000000000000..05388e01f0f8 --- /dev/null +++ b/platform/recentFiles/tests/testSrc/com/intellij/platform/recentFiles/backend/RecentFilesDistributedModelTest.kt @@ -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 { + 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() + var backendFiles = emptyList() + 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 { + return withContext(Dispatchers.EDT) { + FrontendRecentFilesModel.getInstance(project).getRecentFiles(kind).mapNotNull { it.virtualFile } + } + } + + private fun backendFiles(kind: RecentFileKind): List { + 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) + } + } +}