From 2da602139aa72aaa4263c5fa846be21c07e2e795 Mon Sep 17 00:00:00 2001 From: Sviatoslav Vlasov Date: Sat, 30 Mar 2024 16:10:11 +0000 Subject: [PATCH] Support Lux for UI Test framework, prototype Fix IdeaUltimatePackagingTest Fix RemoteIdeaEduBuildTest Fix cwm plugin scrambling Fix layout issues, fix conflicts Fix compilation after rebase Fix layout issues Change jdk to inherited Transform intelij.cwm.guest.driver to plugin, move LuxHostRobotService to separate module Fix conflicts [lux] [ui-test-framework] Fix SDK for cwm.guest.driver [lux] [ui-test-framework] Fix build of performance testing plugin Fix conflicts Cleanup code A little renaming Move remoteDriverGuest to remote-dev Pass isRemoteIdeMode flag to DriverImpl Fix robot smoothness Small fixes, remove dead code, fixed review issues [ui-test-framework] Make remoteDriver possible to work in remdev mode [lux] [ui-test-framework] Implement LuxXpathDataModelExtension, which can gather ui hierarchy from backend [lux] [ui-test-framework] Implement LuxRobot, a backend-side service, which allows to propagate input events to frontend Co-authored-by: Yuriy Artamonov Merge-request: IJ-MR-126806 Merged-by: Sviatoslav Vlasov GitOrigin-RevId: c5cb13288d1b03cfdf343939b20c92c353ce1959 --- .../build/CommunityRepositoryModules.kt | 3 +- .../platform/impl/toolkit/ClientToolkit.kt | 4 +- .../platform/impl/toolkit/IdeMouseInfoPeer.kt | 12 +- .../platform/impl/toolkit/IdeToolkit.kt | 6 +- .../src/com/intellij/driver/client/Driver.kt | 32 +++-- .../src/com/intellij/driver/client/Remote.kt | 10 +- .../intellij/driver/client/impl/DriverImpl.kt | 122 ++++++++++-------- .../src/com/intellij/driver/impl/Invoker.java | 47 +++---- .../model/DriverIlligalStateException.kt | 7 + .../model/{RefDelegate.kt => RemdevUtils.kt} | 8 +- .../model/transport/NewInstanceCall.java | 7 +- .../intellij/driver/model/transport/Ref.java | 7 + .../driver/model/transport/RefCall.java | 11 +- .../driver/model/transport/RemoteCall.java | 13 +- .../driver/model/transport/ServiceCall.java | 13 +- .../driver/model/transport/UtilityCall.java | 4 +- .../intellij/driver/sdk/ProjectRootManager.kt | 5 +- .../src/com/intellij/driver/sdk/ui/Finder.kt | 17 ++- .../src/com/intellij/driver/sdk/ui/UiRobot.kt | 41 +++++- .../driver/sdk/ui/components/IdeaFrameUI.kt | 4 +- .../sdk/ui/components/UIComponentsList.kt | 10 +- .../driver/sdk/ui/components/UiComponent.kt | 31 +++-- .../sdk/ui/components/WelcomeScreenUI.kt | 6 +- .../driver/sdk/ui/remote/RobotService.kt | 12 +- .../driver/sdk/ui/remote/SearchService.kt | 13 ++ .../jmxDriver/InvokerService.kt | 30 ++++- .../jmxDriver/RemoteDevInvoker.kt | 53 ++++++-- ...tellij.performanceTesting.remoteDriver.iml | 6 +- ...tellij.performanceTesting.remoteDriver.xml | 7 +- .../remotedriver/RobotService.kt | 23 +--- .../remotedriver/SearchService.kt | 27 ++++ .../webservice/UiHierarchyWebService.kt | 37 +----- .../xpath/XpathDataModelCreator.kt | 62 ++++++--- ...Provider.kt => XpathDataModelExtension.kt} | 8 +- .../remotedriver/xpath/XpathSearcher.kt | 6 +- 35 files changed, 453 insertions(+), 251 deletions(-) create mode 100644 platform/remote-driver/model/src/com/intellij/driver/model/DriverIlligalStateException.kt rename platform/remote-driver/model/src/com/intellij/driver/model/{RefDelegate.kt => RemdevUtils.kt} (85%) create mode 100644 platform/remote-driver/test-sdk/src/com/intellij/driver/sdk/ui/remote/SearchService.kt create mode 100644 plugins/performanceTesting/remote-driver/src/com/jetbrains/performancePlugin/remotedriver/SearchService.kt rename plugins/performanceTesting/remote-driver/src/com/jetbrains/performancePlugin/remotedriver/xpath/{XpathDataModelSubTreeProvider.kt => XpathDataModelExtension.kt} (51%) diff --git a/platform/build-scripts/src/org/jetbrains/intellij/build/CommunityRepositoryModules.kt b/platform/build-scripts/src/org/jetbrains/intellij/build/CommunityRepositoryModules.kt index 2df1028cc90e..2a500fdb43fc 100644 --- a/platform/build-scripts/src/org/jetbrains/intellij/build/CommunityRepositoryModules.kt +++ b/platform/build-scripts/src/org/jetbrains/intellij/build/CommunityRepositoryModules.kt @@ -308,7 +308,8 @@ object CommunityRepositoryModules { ) { spec -> spec.withModule("intellij.turboComplete.languages.kotlin") }, - pluginAuto(listOf("intellij.performanceTesting", "intellij.performanceTesting.remoteDriver")) { spec -> + pluginAuto(listOf("intellij.performanceTesting", + "intellij.performanceTesting.remoteDriver")) { spec -> spec.withModule("intellij.driver.model") spec.withModule("intellij.driver.impl") spec.withModule("intellij.driver.client") diff --git a/platform/platform-impl/src/com/intellij/platform/impl/toolkit/ClientToolkit.kt b/platform/platform-impl/src/com/intellij/platform/impl/toolkit/ClientToolkit.kt index 1df38507d2a6..08e4f81a5635 100644 --- a/platform/platform-impl/src/com/intellij/platform/impl/toolkit/ClientToolkit.kt +++ b/platform/platform-impl/src/com/intellij/platform/impl/toolkit/ClientToolkit.kt @@ -1,11 +1,12 @@ +// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. @file:Suppress("JAVA_MODULE_DOES_NOT_EXPORT_PACKAGE") -// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. package com.intellij.platform.impl.toolkit import com.intellij.openapi.components.service import java.awt.* import java.awt.peer.DialogPeer import java.awt.peer.FramePeer +import java.awt.peer.RobotPeer import java.awt.peer.WindowPeer interface ClientToolkit { @@ -15,4 +16,5 @@ interface ClientToolkit { fun createWindow(target: Window): WindowPeer fun createDialog(target: Dialog): DialogPeer fun createFrame(target: Frame): FramePeer + fun createRobot(screen: GraphicsDevice?): RobotPeer } \ No newline at end of file diff --git a/platform/platform-impl/src/com/intellij/platform/impl/toolkit/IdeMouseInfoPeer.kt b/platform/platform-impl/src/com/intellij/platform/impl/toolkit/IdeMouseInfoPeer.kt index eaab10fc3bd1..1d540f923091 100644 --- a/platform/platform-impl/src/com/intellij/platform/impl/toolkit/IdeMouseInfoPeer.kt +++ b/platform/platform-impl/src/com/intellij/platform/impl/toolkit/IdeMouseInfoPeer.kt @@ -1,8 +1,9 @@ -// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. @file:Suppress("JAVA_MODULE_DOES_NOT_EXPORT_PACKAGE") package com.intellij.platform.impl.toolkit import java.awt.Component +import java.awt.GraphicsEnvironment import java.awt.Point import java.awt.Window import java.awt.event.MouseEvent @@ -10,16 +11,15 @@ import java.awt.peer.MouseInfoPeer import javax.swing.SwingUtilities object IdeMouseInfoPeer: MouseInfoPeer { - private const val PRIMARY_SCREEN_DEVICE_ID = 0 - private val lastMouseCoords = Point() private var lastWindowUnderMouse: Component? = null private var lastComponentUnderCursor: Component? = null override fun fillPointWithCoords(point: Point): Int { point.location = lastMouseCoords - // TODO: return correct screen id - return PRIMARY_SCREEN_DEVICE_ID + return GraphicsEnvironment.getLocalGraphicsEnvironment().getScreenDevices().indexOfFirst { + it.defaultConfiguration.bounds.contains(point) + } } override fun isWindowUnderMouse(w: Window): Boolean { @@ -29,7 +29,7 @@ object IdeMouseInfoPeer: MouseInfoPeer { fun getComponentUnderCursor(): Component? = lastComponentUnderCursor fun processMouseEvent(event: MouseEvent, window: Window) { - lastMouseCoords.setLocation(event.x, event.y) + lastMouseCoords.setLocation(event.xOnScreen, event.yOnScreen) lastWindowUnderMouse = window val component = SwingUtilities.getDeepestComponentAt(window, event.x, event.y) lastComponentUnderCursor = component diff --git a/platform/platform-impl/src/com/intellij/platform/impl/toolkit/IdeToolkit.kt b/platform/platform-impl/src/com/intellij/platform/impl/toolkit/IdeToolkit.kt index 00a59d9f3352..49d8a76142dc 100644 --- a/platform/platform-impl/src/com/intellij/platform/impl/toolkit/IdeToolkit.kt +++ b/platform/platform-impl/src/com/intellij/platform/impl/toolkit/IdeToolkit.kt @@ -1,4 +1,4 @@ -// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. @file:Suppress("JAVA_MODULE_DOES_NOT_EXPORT_PACKAGE") package com.intellij.platform.impl.toolkit @@ -47,6 +47,9 @@ class IdeToolkit : SunToolkit() { override fun createFrame(target: Frame): FramePeer = clientInstance().createFrame(target) override fun getSystemClipboard(): Clipboard = clipboard + override fun createRobot(screen: GraphicsDevice?): RobotPeer? = clientInstance().createRobot(screen) + override fun getMouseInfoPeer(): IdeMouseInfoPeer = IdeMouseInfoPeer + override fun getScreenResolution(): Int = 96 override fun prepareImage(img: Image, w: Int, h: Int, o: ImageObserver?): Boolean { //TODO: probably can be used for caching @@ -54,7 +57,6 @@ class IdeToolkit : SunToolkit() { } override fun createDesktopPeer(target: Desktop): DesktopPeer = IdeDesktopPeer() - override fun getMouseInfoPeer(): IdeMouseInfoPeer = IdeMouseInfoPeer override fun getKeyboardFocusManagerPeer(): IdeKeyboardFocusManagerPeer = IdeKeyboardFocusManagerPeer // Toolkit interface which shouldn't be used diff --git a/platform/remote-driver/client/src/com/intellij/driver/client/Driver.kt b/platform/remote-driver/client/src/com/intellij/driver/client/Driver.kt index 6e91915ca337..608715bb0546 100644 --- a/platform/remote-driver/client/src/com/intellij/driver/client/Driver.kt +++ b/platform/remote-driver/client/src/com/intellij/driver/client/Driver.kt @@ -67,6 +67,8 @@ interface Driver : AutoCloseable { */ val isConnected: Boolean + val isRemoteIdeMode: Boolean + /** * @return information about the product under test */ @@ -91,24 +93,28 @@ interface Driver : AutoCloseable { * @return new remote proxy for a [Remote] application service interface */ @Contract(pure = true) - fun service(clazz: KClass): T + fun service(clazz: KClass, forceRunOnBackend: Boolean = false): T /** * @return new remote proxy for a [Remote] project service interface */ @Contract(pure = true) - fun service(clazz: KClass, project: ProjectRef): T + fun service(clazz: KClass, project: ProjectRef?, forceRunOnBackend: Boolean = false): T /** * @return new remote proxy for a utility class or a class with static methods */ @Contract(pure = true) - fun utility(clazz: KClass): T + fun utility(clazz: KClass, forceRunOnBackend: Boolean = false): T /** * @return proxy reference for a newly created remote object */ - fun new(clazz: KClass, vararg args: Any?): T + fun new(clazz: KClass, vararg args: Any?): T { + return new(clazz, *args, forceRunOnBackend = false) + } + + fun new(clazz: KClass, vararg args: Any?, forceRunOnBackend: Boolean = false): T /** * Assumes that the remote reference corresponds to another type. Performs unsafe cast. @@ -140,8 +146,8 @@ interface Driver : AutoCloseable { */ @JvmStatic @Contract(pure = true) - fun create(host: JmxHost? = JmxHost(null, null, "localhost:7777")): Driver { - return DriverImpl(host) + fun create(host: JmxHost? = JmxHost(null, null, "localhost:7777"), isRemoteIdeMode: Boolean = false): Driver { + return DriverImpl(host, isRemoteIdeMode) } } } @@ -154,20 +160,20 @@ interface ProjectRef /** * @return new remote proxy for a [Remote] application service interface */ -inline fun Driver.service(): T { - return service(T::class) +inline fun Driver.service(forceRunOnBackend: Boolean = false): T { + return service(T::class, forceRunOnBackend) } /** * @return new remote proxy for a [Remote] application service interface */ -inline fun Driver.service(project: ProjectRef): T { - return service(T::class, project) +inline fun Driver.service(project: ProjectRef, forceRunOnBackend: Boolean = false): T { + return service(T::class, project, forceRunOnBackend) } /** * @return new remote proxy for a utility class or a class with static methods */ -inline fun Driver.utility(): T { - return utility(T::class) -} \ No newline at end of file +inline fun Driver.utility(forceRunOnBackend: Boolean = false): T { + return utility(T::class, forceRunOnBackend) +} diff --git a/platform/remote-driver/client/src/com/intellij/driver/client/Remote.kt b/platform/remote-driver/client/src/com/intellij/driver/client/Remote.kt index 868c30fc98cb..c95ee5853612 100644 --- a/platform/remote-driver/client/src/com/intellij/driver/client/Remote.kt +++ b/platform/remote-driver/client/src/com/intellij/driver/client/Remote.kt @@ -1,5 +1,6 @@ package com.intellij.driver.client +import com.intellij.driver.model.RdTarget import org.intellij.lang.annotations.Language /** @@ -25,5 +26,12 @@ annotation class Remote( * Identifier of a plugin where the class is located, e.g. `com.intellij.spring`. * If the class is declared in a module of a plugin (not main), use the following format: `some.plugin.id/some.plugin.id.submodule`. */ - val plugin: String = "" + val plugin: String = "", + + /** + * Determine the semantics for the remote call in case of Remote IDE. + */ + val rdTarget: RdTarget = RdTarget.FRONTEND_FIRST, + + val isControllerSession: Boolean = false ) \ No newline at end of file diff --git a/platform/remote-driver/client/src/com/intellij/driver/client/impl/DriverImpl.kt b/platform/remote-driver/client/src/com/intellij/driver/client/impl/DriverImpl.kt index 13e1d3307cb4..96eeaf71bd0e 100644 --- a/platform/remote-driver/client/src/com/intellij/driver/client/impl/DriverImpl.kt +++ b/platform/remote-driver/client/src/com/intellij/driver/client/impl/DriverImpl.kt @@ -7,7 +7,9 @@ import com.intellij.driver.client.Timed import com.intellij.driver.model.LockSemantics import com.intellij.driver.model.OnDispatcher import com.intellij.driver.model.ProductVersion +import com.intellij.driver.model.RdTarget import com.intellij.driver.model.transport.* +import java.lang.IllegalStateException import java.lang.reflect.Method import java.lang.reflect.ParameterizedType import java.lang.reflect.Proxy @@ -17,13 +19,13 @@ import javax.management.AttributeNotFoundException import javax.management.InstanceNotFoundException import kotlin.reflect.KClass -internal class DriverImpl(host: JmxHost?) : Driver { +class DriverImpl(host: JmxHost?, override val isRemoteIdeMode: Boolean) : Driver { private val invoker: Invoker = JmxCallHandler.jmx(Invoker::class.java, host) private val sessionHolder = ThreadLocal() - private val appServices: MutableMap, Any> = ConcurrentHashMap() + private val appServices: MutableMap = ConcurrentHashMap() private val projectServices: MutableMap = ConcurrentHashMap() - private val utils: MutableMap, Any> = ConcurrentHashMap() + private val utils: MutableMap = ConcurrentHashMap() override val isConnected: Boolean get() { @@ -42,6 +44,8 @@ internal class DriverImpl(host: JmxHost?) : Driver { } } + fun getInvoker() = invoker + override fun getProductVersion(): ProductVersion { return invoker.getProductVersion() } @@ -55,26 +59,27 @@ internal class DriverImpl(host: JmxHost?) : Driver { } @Suppress("UNCHECKED_CAST") - override fun service(clazz: KClass): T { - return appServices.computeIfAbsent(clazz.java, ::serviceBridge) as T + override fun service(clazz: KClass, isBackendService: Boolean): T { + return appServices.computeIfAbsent(AppServiceId(clazz.java, isBackendService)) { serviceBridge(clazz.java, null, isBackendService) } as T } @Suppress("UNCHECKED_CAST") - override fun service(clazz: KClass, project: ProjectRef): T { - val id = ProjectServiceId((project as RefWrapper).getRef().identityHashCode, clazz.java) - return projectServices.computeIfAbsent(id) { serviceBridge(clazz.java, project) } as T + override fun service(clazz: KClass, project: ProjectRef?, isBackendService: Boolean): T { + val id = ProjectServiceId((project as RefWrapper).getRef().identityHashCode, clazz.java, isBackendService) + return projectServices.computeIfAbsent(id) { serviceBridge(clazz.java, project, isBackendService) } as T } @Suppress("UNCHECKED_CAST") - override fun utility(clazz: KClass): T { - return utils.computeIfAbsent(clazz.java, ::utilityBridge) as T + override fun utility(clazz: KClass, isBackendUtility: Boolean): T { + return utils.computeIfAbsent(UtilityId(clazz.java, isBackendUtility)) { utilityBridge(clazz.java, isBackendUtility) } as T } @Suppress("UNCHECKED_CAST") - override fun new(clazz: KClass, vararg args: Any?): T { + override fun new(clazz: KClass, vararg args: Any?, forceRunOnBackend: Boolean): T { val remote = findRemoteMeta(clazz.java) ?: throw IllegalArgumentException("Class $clazz is not annotated with @Remote annotation") + val rdTarget = checkRdTargetConsistencyAndGetMostSuitable(clazz.java, if (forceRunOnBackend) RdTarget.BACKEND_ONLY else remote.rdTarget, *args) val (sessionId, dispatcher, semantics) = sessionHolder.get() ?: NO_SESSION val call = NewInstanceCall( sessionId, @@ -83,6 +88,7 @@ internal class DriverImpl(host: JmxHost?) : Driver { dispatcher, semantics, remote.value, + if (rdTarget == RdTarget.FRONTEND_FIRST) RdTarget.FRONTEND_ONLY else rdTarget, convertArgsToPass(args) ) val callResult = makeCall(call) @@ -156,48 +162,23 @@ internal class DriverImpl(host: JmxHost?) : Driver { return value // let's hope we will be able to cast it } - private fun serviceBridge(clazz: Class<*>): Any { - val remote = findRemoteMeta(clazz) ?: throw notAnnotatedError(clazz) - - return Proxy.newProxyInstance(getClassLoader(), arrayOf(clazz)) { proxy: Any?, method: Method, args: Array? -> - when (method.name) { - "equals" -> proxy === args?.firstOrNull() - "hashCode" -> clazz.hashCode() - "toString" -> "@Service " + remote.value - else -> { - val (sessionId, dispatcher, semantics) = sessionHolder.get() ?: NO_SESSION - val call = ServiceCall( - sessionId, - findTimedMeta(method)?.value, - getPluginId(remote), - dispatcher, - semantics, - remote.value, - method.name, - convertArgsToPass(args), - null, - remote.serviceInterface.takeIf { it.isNotBlank() } - ) - val callResult = makeCall(call) - convertResult(callResult, method, getPluginId(remote)) - } - } - } - } - private fun getPluginId(remote: Remote): String? { return remote.plugin.takeIf { it.isNotBlank() } } - private fun serviceBridge(clazz: Class<*>, project: ProjectRef): Any { + private fun serviceBridge(clazz: Class<*>, project: ProjectRef? = null, forceRunOnBackend: Boolean = false): Any { val remote = findRemoteMeta(clazz) ?: throw notAnnotatedError(clazz) - return Proxy.newProxyInstance(getClassLoader(), arrayOf(clazz)) { proxy: Any?, method: Method, args: Array? -> + val rdTarget = if (forceRunOnBackend) RdTarget.BACKEND_ONLY else remote.rdTarget + val isControllerSession = remote.isControllerSession + + return Proxy.newProxyInstance(clazz.classLoader, arrayOf(clazz)) { proxy: Any?, method: Method, args: Array? -> when (method.name) { "equals" -> proxy === args?.firstOrNull() "hashCode" -> clazz.hashCode() "toString" -> "@Service(APP) " + remote.value else -> { + val rdTarget = checkRdTargetConsistencyAndGetMostSuitable(clazz, rdTarget, args) val (sessionId, dispatcher, semantics) = sessionHolder.get() ?: NO_SESSION val call = ServiceCall( sessionId, @@ -208,8 +189,9 @@ internal class DriverImpl(host: JmxHost?) : Driver { remote.value, method.name, convertArgsToPass(args), - (project as RefWrapper).getRef(), - remote.serviceInterface.takeIf { it.isNotBlank() } + (project as? RefWrapper?)?.getRef(), + remote.serviceInterface.takeIf { it.isNotBlank() }, + rdTarget, isControllerSession ) val callResult = makeCall(call) convertResult(callResult, method, getPluginId(remote)) @@ -227,15 +209,17 @@ internal class DriverImpl(host: JmxHost?) : Driver { } } - private fun utilityBridge(clazz: Class<*>): Any { + private fun utilityBridge(clazz: Class<*>, forceRunOnBackend: Boolean): Any { val remote = findRemoteMeta(clazz) ?: throw notAnnotatedError(clazz) - return Proxy.newProxyInstance(getClassLoader(), arrayOf(clazz)) { proxy: Any?, method: Method, args: Array? -> + val rdTarget = if (forceRunOnBackend) RdTarget.BACKEND_ONLY else remote.rdTarget + return Proxy.newProxyInstance(clazz.classLoader, arrayOf(clazz)) { proxy: Any?, method: Method, args: Array? -> when (method.name) { "equals" -> proxy === args?.firstOrNull() "hashCode" -> clazz.hashCode() "toString" -> "Utility " + remote.value else -> { + val rdTarget = checkRdTargetConsistencyAndGetMostSuitable(clazz, rdTarget, args) val (sessionId, dispatcher, semantics) = sessionHolder.get() ?: NO_SESSION val call = UtilityCall( sessionId, @@ -245,7 +229,8 @@ internal class DriverImpl(host: JmxHost?) : Driver { semantics, remote.value, method.name, - convertArgsToPass(args) + rdTarget, + convertArgsToPass(args), ) val callResult = makeCall(call) convertResult(callResult, method, getPluginId(remote)) @@ -257,7 +242,7 @@ internal class DriverImpl(host: JmxHost?) : Driver { private fun refBridge(clazz: Class<*>, ref: Ref, pluginId: String?): Any { val remote = findRemoteMeta(clazz) ?: throw notAnnotatedError(clazz) - return Proxy.newProxyInstance(getClassLoader(), + return Proxy.newProxyInstance(clazz.classLoader, arrayOf(clazz, RefWrapper::class.java)) { proxy: Any?, method: Method, args: Array? -> when (method.name) { "equals" -> proxy === args?.firstOrNull() @@ -285,10 +270,6 @@ internal class DriverImpl(host: JmxHost?) : Driver { } } - private fun notAnnotatedError(clazz: Class<*>): IllegalArgumentException { - return IllegalArgumentException("Class $clazz is not annotated with @Remote annotation") - } - private fun getClassLoader(): ClassLoader? { return javaClass.classLoader } @@ -338,6 +319,10 @@ internal class DriverImpl(host: JmxHost?) : Driver { } } +private fun notAnnotatedError(clazz: Class<*>): IllegalArgumentException { + return IllegalArgumentException("Class $clazz is not annotated with @Remote annotation") +} + private fun findTimedMeta(method: Method): Timed? { return method.annotations .filterIsInstance() @@ -350,6 +335,29 @@ private fun findRemoteMeta(clazz: Class<*>): Remote? { .firstOrNull() } +private fun checkRdTargetConsistencyAndGetMostSuitable( + clazz: Class<*>, + rdTarget: RdTarget, + vararg args: Any? +): RdTarget { + val remote = findRemoteMeta(clazz) ?: throw notAnnotatedError(clazz) + val classRdTarget = remote.rdTarget + val argsRdTarget = + args.filterIsInstance() + .map { if (it.getRef().isBackendReference) RdTarget.BACKEND_ONLY else RdTarget.FRONTEND_ONLY } + .reduceOrNull { acc, b -> + if (acc != b) throw IllegalStateException("Inconsistent rdTarget") + acc + } ?: RdTarget.FRONTEND_FIRST + + return listOf(classRdTarget, argsRdTarget, rdTarget).reduce { acc, b -> + if (acc == RdTarget.FRONTEND_FIRST) b + else if (b == RdTarget.FRONTEND_FIRST) acc + else if (acc == b) acc + else throw IllegalStateException("Inconsistent rdTarget") + } +} + internal data class Session( val id: Int, val dispatcher: OnDispatcher, @@ -358,6 +366,12 @@ internal data class Session( private val NO_SESSION: Session = Session(0, OnDispatcher.DEFAULT, LockSemantics.NO_LOCK) +class DriverCallException(message: String, e: Throwable) : RuntimeException(message, e) + +private data class AppServiceId(val clazz: Class<*>, val isBackendService: Boolean) +private data class ProjectServiceId(val projectId: Int, val serviceClass: Class<*>, val isBackendService: Boolean) +private data class UtilityId(val clazz: Class<*>, val isBackendUtility: Boolean) + @JmxName("com.intellij.driver:type=Invoker") interface Invoker : AutoCloseable { fun getProductVersion(): ProductVersion @@ -376,7 +390,3 @@ interface Invoker : AutoCloseable { fun takeScreenshot(folder: String?): String? } - -class DriverCallException(message: String, e: Throwable) : RuntimeException(message, e) - -private data class ProjectServiceId(val projectId: Int, val serviceClass: Class<*>) \ No newline at end of file diff --git a/platform/remote-driver/core/src/com/intellij/driver/impl/Invoker.java b/platform/remote-driver/core/src/com/intellij/driver/impl/Invoker.java index 6ad39f363548..3ce91feaa2f4 100644 --- a/platform/remote-driver/core/src/com/intellij/driver/impl/Invoker.java +++ b/platform/remote-driver/core/src/com/intellij/driver/impl/Invoker.java @@ -1,9 +1,6 @@ package com.intellij.driver.impl; -import com.intellij.driver.model.LocalRefDelegate; -import com.intellij.driver.model.OnDispatcher; -import com.intellij.driver.model.ProductVersion; -import com.intellij.driver.model.RemoteRefDelegate; +import com.intellij.driver.model.*; import com.intellij.driver.model.transport.*; import com.intellij.ide.plugins.IdeaPluginDescriptor; import com.intellij.ide.plugins.IdeaPluginDescriptorImpl; @@ -12,6 +9,7 @@ import com.intellij.ide.plugins.PluginManagerCore; import com.intellij.openapi.application.*; import com.intellij.openapi.application.ex.ApplicationEx; import com.intellij.openapi.application.impl.ApplicationInfoImpl; +import com.intellij.openapi.client.ClientKind; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.extensions.PluginId; import com.intellij.openapi.project.Project; @@ -19,7 +17,6 @@ import com.intellij.openapi.util.BuildNumber; import com.intellij.openapi.util.ClearableLazyValue; import com.intellij.platform.diagnostic.telemetry.IJTracer; import com.intellij.util.ExceptionUtil; -import java.util.function.Function; import io.opentelemetry.api.trace.Span; import io.opentelemetry.api.trace.SpanBuilder; import io.opentelemetry.context.Context; @@ -30,14 +27,12 @@ import org.jetbrains.annotations.Nullable; import javax.swing.*; import java.lang.ref.WeakReference; -import java.lang.reflect.Constructor; -import java.lang.reflect.Method; -import java.lang.reflect.ParameterizedType; -import java.lang.reflect.Type; +import java.lang.reflect.*; import java.util.*; import java.util.concurrent.Callable; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Function; import java.util.function.Supplier; import java.util.stream.Collectors; @@ -128,7 +123,7 @@ public class Invoker implements InvokerMBean { catch (Exception e) { LOG.error("Unable to get instance for " + call); - throw new RuntimeException("Unable to get instance for " + call, e); + throw new DriverIlligalStateException("Unable to get instance for " + call, e); } if (call.getDispatcher() == OnDispatcher.EDT) { @@ -155,7 +150,7 @@ public class Invoker implements InvokerMBean { // first lookup in the current session Session session = sessions.get(sessionId); if (session == null) { - throw new IllegalStateException("No such session " + sessionId); + throw new DriverIlligalStateException("No such session " + sessionId); } Object value = session.findReference(id); @@ -167,9 +162,9 @@ public class Invoker implements InvokerMBean { if (value != null) return value; } - throw new IllegalStateException("No such reference with id " + id + ". " + - "It may happen if a weak reference to the variable expires. " + - "Please use `Driver.withContext { }` for hard variable references."); + throw new DriverIlligalStateException("No such reference with id " + id + ". " + + "It may happen if a weak reference to the variable expires. " + + "Please use `Driver.withContext { }` for hard variable references."); } private static @NotNull Object invokeConstructor(Constructor constructor, Object[] transformedArgs) throws Exception { @@ -237,6 +232,9 @@ public class Invoker implements InvokerMBean { try { return supplier.call(); } + catch (InvocationTargetException e) { + throw new DriverIlligalStateException(e); + } catch (Exception e) { ExceptionUtil.rethrow(e); throw new IllegalStateException(); @@ -364,7 +362,7 @@ public class Invoker implements InvokerMBean { clazz = getClassLoader(call).loadClass(call.getClassName()); } catch (ClassNotFoundException e) { - throw new IllegalStateException("No such class '" + call.getClassName() + "'", e); + throw new DriverIlligalStateException("No such class '" + call.getClassName() + "'", e); } return clazz; } @@ -378,7 +376,7 @@ public class Invoker implements InvokerMBean { String moduleId = StringsKt.substringAfter(pluginId, "/", pluginId); IdeaPluginDescriptor plugin = PluginManagerCore.getPlugin(PluginId.getId(mainId)); - if (plugin == null) throw new IllegalStateException("No such plugin " + mainId); + if (plugin == null) throw new DriverIlligalStateException("No such plugin " + mainId); List modules = ((IdeaPluginDescriptorImpl)plugin).content.modules; for (PluginContentDescriptor.ModuleItem module : modules) { @@ -387,11 +385,11 @@ public class Invoker implements InvokerMBean { } } - throw new IllegalStateException("No such plugin module " + pluginId); + throw new DriverIlligalStateException("No such plugin module " + pluginId); } IdeaPluginDescriptor plugin = PluginManagerCore.getPlugin(PluginId.getId(pluginId)); - if (plugin == null) throw new IllegalStateException("No such plugin " + pluginId); + if (plugin == null) throw new DriverIlligalStateException("No such plugin " + pluginId); return plugin.getClassLoader(); } @@ -409,16 +407,21 @@ public class Invoker implements InvokerMBean { if (serviceInterface != null) { serviceClass = findServiceInterface(clazz, serviceInterface); if (serviceClass == null) { - throw new IllegalStateException("Unable to find interface " + serviceInterface + " for service " + clazz); + throw new DriverIlligalStateException("Unable to find interface " + serviceInterface + " for service " + clazz); } } Object instance; + Boolean isControllerSession = ((ServiceCall)call).isControllerSession(); if (projectInstance instanceof Project) { - instance = ((Project)projectInstance).getService(serviceClass); + instance = isControllerSession + ? ((Project)projectInstance).getServices(serviceClass, ClientKind.CONTROLLER).get(0) + : ((Project)projectInstance).getService(serviceClass); } else { - instance = ApplicationManager.getApplication().getService(serviceClass); + instance = isControllerSession + ? ApplicationManager.getApplication().getServices(serviceClass, ClientKind.CONTROLLER).get(0) + : ApplicationManager.getApplication().getService(serviceClass); } return instance; } @@ -427,7 +430,7 @@ public class Invoker implements InvokerMBean { Ref ref = ((RefCall)call).getRef(); Object reference = getReference(call.getSessionId(), ref.id()); - if (reference == null) throw new IllegalStateException("No such ref exists " + ref); + if (reference == null) throw new DriverIlligalStateException("No such ref exists " + ref); return reference; } diff --git a/platform/remote-driver/model/src/com/intellij/driver/model/DriverIlligalStateException.kt b/platform/remote-driver/model/src/com/intellij/driver/model/DriverIlligalStateException.kt new file mode 100644 index 000000000000..425d144e24b3 --- /dev/null +++ b/platform/remote-driver/model/src/com/intellij/driver/model/DriverIlligalStateException.kt @@ -0,0 +1,7 @@ +package com.intellij.driver.model + +class DriverIlligalStateException : IllegalStateException { + constructor(message: String) : super(message) + constructor(cause: Throwable) : super(cause) + constructor(message: String, cause: Throwable) : super(message, cause) +} \ No newline at end of file diff --git a/platform/remote-driver/model/src/com/intellij/driver/model/RefDelegate.kt b/platform/remote-driver/model/src/com/intellij/driver/model/RemdevUtils.kt similarity index 85% rename from platform/remote-driver/model/src/com/intellij/driver/model/RefDelegate.kt rename to platform/remote-driver/model/src/com/intellij/driver/model/RemdevUtils.kt index 068c13ee0f8b..c7c40b5dbd25 100644 --- a/platform/remote-driver/model/src/com/intellij/driver/model/RefDelegate.kt +++ b/platform/remote-driver/model/src/com/intellij/driver/model/RemdevUtils.kt @@ -12,4 +12,10 @@ fun RefDelegate.unwrap(): T = when (this) { is RemoteRefDelegate -> throw UnsupportedOperationException("Cannot get remote value") } -fun List>.unwrap(): List = map { it.unwrap() } \ No newline at end of file +fun List>.unwrap(): List = map { it.unwrap() } + +enum class RdTarget { + FRONTEND_FIRST, + FRONTEND_ONLY, + BACKEND_ONLY, +} diff --git a/platform/remote-driver/model/src/com/intellij/driver/model/transport/NewInstanceCall.java b/platform/remote-driver/model/src/com/intellij/driver/model/transport/NewInstanceCall.java index 4724660642c9..296429e06546 100644 --- a/platform/remote-driver/model/src/com/intellij/driver/model/transport/NewInstanceCall.java +++ b/platform/remote-driver/model/src/com/intellij/driver/model/transport/NewInstanceCall.java @@ -2,6 +2,7 @@ package com.intellij.driver.model.transport; import com.intellij.driver.model.LockSemantics; import com.intellij.driver.model.OnDispatcher; +import com.intellij.driver.model.RdTarget; import java.io.Serial; @@ -15,8 +16,12 @@ public final class NewInstanceCall extends RemoteCall { OnDispatcher dispatcher, LockSemantics lockSemantics, String className, + RdTarget rdTarget, Object[] args) { - super(sessionId, timedSpan, pluginId, dispatcher, lockSemantics, className, "new", args); + super(sessionId, timedSpan, pluginId, dispatcher, lockSemantics, className, "new", rdTarget, args); + if (rdTarget != RdTarget.FRONTEND_ONLY && rdTarget != RdTarget.BACKEND_ONLY) { + throw new IllegalArgumentException("NewInstanceCall must have RdTarget set to either FRONTEND_ONLY or BACKEND_ONLY"); + } } @Override diff --git a/platform/remote-driver/model/src/com/intellij/driver/model/transport/Ref.java b/platform/remote-driver/model/src/com/intellij/driver/model/transport/Ref.java index 965b25d83486..385520827b71 100644 --- a/platform/remote-driver/model/src/com/intellij/driver/model/transport/Ref.java +++ b/platform/remote-driver/model/src/com/intellij/driver/model/transport/Ref.java @@ -11,4 +11,11 @@ public record Ref( ) implements Serializable { @Serial private static final long serialVersionUID = 1L; + + public static final String BACKEND_REFERENCE_PREFIX = "remdevHost_"; + public static final String FRONTEND_REFERENCE_PREFIX = "jbClient_"; + + public boolean isBackendReference() { + return id.startsWith(BACKEND_REFERENCE_PREFIX); + } } \ No newline at end of file diff --git a/platform/remote-driver/model/src/com/intellij/driver/model/transport/RefCall.java b/platform/remote-driver/model/src/com/intellij/driver/model/transport/RefCall.java index 70d2c82ace9f..4ba4f3ec1d42 100644 --- a/platform/remote-driver/model/src/com/intellij/driver/model/transport/RefCall.java +++ b/platform/remote-driver/model/src/com/intellij/driver/model/transport/RefCall.java @@ -2,6 +2,7 @@ package com.intellij.driver.model.transport; import com.intellij.driver.model.LockSemantics; import com.intellij.driver.model.OnDispatcher; +import com.intellij.driver.model.RdTarget; import java.io.Serial; @@ -20,7 +21,15 @@ public final class RefCall extends RemoteCall { String methodName, Object[] args, Ref ref) { - super(sessionId, timedSpan, pluginId, dispatcher, lockSemantics, className, methodName, args); + super(sessionId, + timedSpan, + pluginId, + dispatcher, + lockSemantics, + className, + methodName, + ref.isBackendReference() ? RdTarget.BACKEND_ONLY : RdTarget.FRONTEND_ONLY, + args); this.ref = ref; } diff --git a/platform/remote-driver/model/src/com/intellij/driver/model/transport/RemoteCall.java b/platform/remote-driver/model/src/com/intellij/driver/model/transport/RemoteCall.java index e4c193223e39..e2a7938fd19d 100644 --- a/platform/remote-driver/model/src/com/intellij/driver/model/transport/RemoteCall.java +++ b/platform/remote-driver/model/src/com/intellij/driver/model/transport/RemoteCall.java @@ -2,7 +2,9 @@ package com.intellij.driver.model.transport; import com.intellij.driver.model.LockSemantics; import com.intellij.driver.model.OnDispatcher; +import com.intellij.driver.model.RdTarget; import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; import java.awt.*; import java.io.Serial; @@ -15,6 +17,8 @@ public abstract class RemoteCall implements Serializable { @Serial private static final long serialVersionUID = 1L; + private final RdTarget rdTarget; + private final int sessionId; private final String pluginId; private final String timedSpan; @@ -23,8 +27,6 @@ public abstract class RemoteCall implements Serializable { private final LockSemantics lockSemantics; private final String className; private final String methodName; - private final Object[] args; - public RemoteCall(int sessionId, String timedSpan, String pluginId, @@ -32,6 +34,7 @@ public abstract class RemoteCall implements Serializable { LockSemantics lockSemantics, String className, String methodName, + RdTarget rdTarget, Object[] args) { this.sessionId = sessionId; this.pluginId = pluginId; @@ -40,8 +43,14 @@ public abstract class RemoteCall implements Serializable { this.lockSemantics = lockSemantics; this.className = className; this.methodName = methodName; + this.rdTarget = rdTarget; this.args = args; } + private final Object[] args; + + public @NotNull RdTarget getRdTarget() { + return rdTarget; + } public int getSessionId() { return sessionId; diff --git a/platform/remote-driver/model/src/com/intellij/driver/model/transport/ServiceCall.java b/platform/remote-driver/model/src/com/intellij/driver/model/transport/ServiceCall.java index e70dbef07b21..5e10a7d45363 100644 --- a/platform/remote-driver/model/src/com/intellij/driver/model/transport/ServiceCall.java +++ b/platform/remote-driver/model/src/com/intellij/driver/model/transport/ServiceCall.java @@ -2,6 +2,7 @@ package com.intellij.driver.model.transport; import com.intellij.driver.model.LockSemantics; import com.intellij.driver.model.OnDispatcher; +import com.intellij.driver.model.RdTarget; import org.jetbrains.annotations.Nullable; import java.io.Serial; @@ -12,6 +13,7 @@ public final class ServiceCall extends RemoteCall { private final Ref projectRef; private final String serviceInterface; + private final Boolean isControllerSession; public ServiceCall(int sessionId, String timedSpan, @@ -22,10 +24,13 @@ public final class ServiceCall extends RemoteCall { String methodName, Object[] args, @Nullable Ref projectRef, - @Nullable String serviceInterface) { - super(sessionId, timedSpan, pluginId, dispatcher, lockSemantics, className, methodName, args); + @Nullable String serviceInterface, + RdTarget rdTarget, + Boolean isControllerSession) { + super(sessionId, timedSpan, pluginId, dispatcher, lockSemantics, className, methodName, rdTarget, args); this.projectRef = projectRef; this.serviceInterface = serviceInterface; + this.isControllerSession = isControllerSession; } public Ref getProjectRef() { @@ -36,6 +41,10 @@ public final class ServiceCall extends RemoteCall { return serviceInterface; } + public Boolean isControllerSession() { + return isControllerSession; + } + @Override public String toString() { return "ServiceCall{" + diff --git a/platform/remote-driver/model/src/com/intellij/driver/model/transport/UtilityCall.java b/platform/remote-driver/model/src/com/intellij/driver/model/transport/UtilityCall.java index 4474b3b6f98a..f005470b7dfe 100644 --- a/platform/remote-driver/model/src/com/intellij/driver/model/transport/UtilityCall.java +++ b/platform/remote-driver/model/src/com/intellij/driver/model/transport/UtilityCall.java @@ -2,6 +2,7 @@ package com.intellij.driver.model.transport; import com.intellij.driver.model.LockSemantics; import com.intellij.driver.model.OnDispatcher; +import com.intellij.driver.model.RdTarget; import java.io.Serial; @@ -16,8 +17,9 @@ public final class UtilityCall extends RemoteCall { LockSemantics lockSemantics, String className, String methodName, + RdTarget rdTarget, Object[] args) { - super(sessionId, timedSpan, pluginId, dispatcher, lockSemantics, className, methodName, args); + super(sessionId, timedSpan, pluginId, dispatcher, lockSemantics, className, methodName, rdTarget, args); } @Override diff --git a/platform/remote-driver/test-sdk/src/com/intellij/driver/sdk/ProjectRootManager.kt b/platform/remote-driver/test-sdk/src/com/intellij/driver/sdk/ProjectRootManager.kt index b9f003fb20ba..54a6052aa47d 100644 --- a/platform/remote-driver/test-sdk/src/com/intellij/driver/sdk/ProjectRootManager.kt +++ b/platform/remote-driver/test-sdk/src/com/intellij/driver/sdk/ProjectRootManager.kt @@ -4,6 +4,7 @@ import com.intellij.driver.client.Driver import com.intellij.driver.client.Remote import com.intellij.driver.client.service import com.intellij.driver.client.utility +import com.intellij.driver.model.RdTarget @Remote("com.intellij.openapi.roots.ProjectRootManager") interface ProjectRootManager { @@ -30,7 +31,9 @@ interface Sdk { fun getHomePath(): String? } -@Remote(value = "com.jetbrains.performancePlugin.commands.SetupProjectSdkUtil", plugin = "com.jetbrains.performancePlugin") +@Remote(value = "com.jetbrains.performancePlugin.commands.SetupProjectSdkUtil", + plugin = "com.jetbrains.performancePlugin", + rdTarget = RdTarget.BACKEND_ONLY) interface SetupProjectSdkUtil { fun setupOrDetectSdk(project: Project, name: String, type: String, home: String) diff --git a/platform/remote-driver/test-sdk/src/com/intellij/driver/sdk/ui/Finder.kt b/platform/remote-driver/test-sdk/src/com/intellij/driver/sdk/ui/Finder.kt index b257c0d41313..ad541c6c2620 100644 --- a/platform/remote-driver/test-sdk/src/com/intellij/driver/sdk/ui/Finder.kt +++ b/platform/remote-driver/test-sdk/src/com/intellij/driver/sdk/ui/Finder.kt @@ -5,32 +5,37 @@ import com.intellij.driver.sdk.ui.components.ComponentData import com.intellij.driver.sdk.ui.components.UIComponentsList import com.intellij.driver.sdk.ui.components.UiComponent import com.intellij.driver.sdk.ui.remote.Component -import com.intellij.driver.sdk.ui.remote.RobotService +import com.intellij.driver.sdk.ui.remote.RobotServiceProvider +import com.intellij.driver.sdk.ui.remote.SearchService import org.intellij.lang.annotations.Language internal const val DEFAULT_FIND_TIMEOUT_SECONDS = 15 interface Finder { val driver: Driver - val robotService: RobotService + val searchService: SearchService + val robotServiceProvider: RobotServiceProvider val searchContext: SearchContext + val isRemoteIdeMode: Boolean + get() = driver.isRemoteIdeMode + fun x(@Language("xpath") xpath: String): UiComponent { - return UiComponent(ComponentData(xpath, driver, robotService, searchContext, null)) + return UiComponent(ComponentData(xpath, driver, searchService, robotServiceProvider, searchContext, null)) } fun x(@Language("xpath") xpath: String, type: Class): T { return type.getConstructor( ComponentData::class.java - ).newInstance(ComponentData(xpath, driver, robotService, searchContext, null)) + ).newInstance(ComponentData(xpath, driver, searchService, robotServiceProvider, searchContext, null)) } fun xx(@Language("xpath") xpath: String): UIComponentsList { - return UIComponentsList(xpath, UiComponent::class.java, driver, robotService, searchContext) + return UIComponentsList(xpath, UiComponent::class.java, driver, searchService, robotServiceProvider, searchContext) } fun xx(@Language("xpath") xpath: String, type: Class): UIComponentsList { - return UIComponentsList(xpath, type, driver, robotService, searchContext) + return UIComponentsList(xpath, type, driver, searchService, robotServiceProvider, searchContext) } } diff --git a/platform/remote-driver/test-sdk/src/com/intellij/driver/sdk/ui/UiRobot.kt b/platform/remote-driver/test-sdk/src/com/intellij/driver/sdk/ui/UiRobot.kt index b22284c22375..62989a958edd 100644 --- a/platform/remote-driver/test-sdk/src/com/intellij/driver/sdk/ui/UiRobot.kt +++ b/platform/remote-driver/test-sdk/src/com/intellij/driver/sdk/ui/UiRobot.kt @@ -2,18 +2,29 @@ package com.intellij.driver.sdk.ui import com.intellij.driver.client.Driver import com.intellij.driver.model.RemoteMouseButton +import com.intellij.driver.model.transport.Ref import com.intellij.driver.sdk.ui.keyboard.WithKeyboard import com.intellij.driver.sdk.ui.remote.Component import com.intellij.driver.sdk.ui.remote.RobotService +import com.intellij.driver.sdk.ui.remote.RobotServiceProvider +import com.intellij.driver.sdk.ui.remote.SearchService import java.awt.Point +import java.lang.reflect.Proxy /** * Provides access to UI actions with the IDE under test such as keyboard and mouse input. */ val Driver.ui - get(): UiRobot = UiRobot(this, service(RobotService::class)) + get(): UiRobot = UiRobot(this, service(SearchService::class), RemDevRobotServiceProvider(this)) -class UiRobot(override val driver: Driver, override val robotService: RobotService) : WithKeyboard, Finder { +class UiRobot( + override val driver: Driver, + override val searchService: SearchService, + override val robotServiceProvider: RobotServiceProvider +) : WithKeyboard, Finder { + + override val robotService: RobotService + get() = robotServiceProvider.getDefaultRobotService() fun moveMouse(point: Point) { robotService.robot.moveMouse(point) @@ -45,8 +56,32 @@ class UiRobot(override val driver: Driver, override val robotService: RobotServi override val context = "" override fun findAll(xpath: String): List { - return robotService.findAll(xpath) + return searchService.findAll(xpath) } } } +class RemDevRobotServiceProvider(private val driver: Driver) : RobotServiceProvider { + private val frontendRobotService: RobotService + get() = driver.service(RobotService::class) + private val backendRobotService: RobotService + get() = driver.service(RobotService::class, true) + + override fun getDefaultRobotService(): RobotService { + return frontendRobotService + } + + override fun getRobotServiceFor(obj: Any?): RobotService { + if (obj == null) { + return getDefaultRobotService() + } + val handler = Proxy.getInvocationHandler(obj) + val method = FakeRefInterface::class.java.getMethod("getRef") + val ref = handler.invoke(obj, method, arrayOf()) as Ref + return if (ref.isBackendReference) backendRobotService else frontendRobotService + } + + private interface FakeRefInterface { + fun getRef(): Ref + } +} \ No newline at end of file diff --git a/platform/remote-driver/test-sdk/src/com/intellij/driver/sdk/ui/components/IdeaFrameUI.kt b/platform/remote-driver/test-sdk/src/com/intellij/driver/sdk/ui/components/IdeaFrameUI.kt index 3854a8055ad2..54ff76efd538 100644 --- a/platform/remote-driver/test-sdk/src/com/intellij/driver/sdk/ui/components/IdeaFrameUI.kt +++ b/platform/remote-driver/test-sdk/src/com/intellij/driver/sdk/ui/components/IdeaFrameUI.kt @@ -17,8 +17,10 @@ fun Driver.ideFrame(action: IdeaFrameUI.() -> Unit) { } open class IdeaFrameUI(data: ComponentData) : UiComponent(data) { + private val projectViewTreeClass = if (isRemoteIdeMode) "ThinClientProjectViewTree" else "ProjectViewTree" + val projectViewTree = tree("//div[@class='${projectViewTreeClass}']") + private val ideaFrameComponent by lazy { driver.cast(component, IdeFrameImpl::class) } - val projectViewTree = tree("//div[@class='ProjectViewTree']") val project: Project? get() = driver.utility(ProjectFrameHelper::class).getFrameHelper(component).getProject() diff --git a/platform/remote-driver/test-sdk/src/com/intellij/driver/sdk/ui/components/UIComponentsList.kt b/platform/remote-driver/test-sdk/src/com/intellij/driver/sdk/ui/components/UIComponentsList.kt index eef6c7498560..49b249cf7b99 100644 --- a/platform/remote-driver/test-sdk/src/com/intellij/driver/sdk/ui/components/UIComponentsList.kt +++ b/platform/remote-driver/test-sdk/src/com/intellij/driver/sdk/ui/components/UIComponentsList.kt @@ -3,12 +3,14 @@ package com.intellij.driver.sdk.ui.components import com.intellij.driver.client.Driver import com.intellij.driver.sdk.ui.SearchContext import com.intellij.driver.sdk.ui.remote.Component -import com.intellij.driver.sdk.ui.remote.RobotService +import com.intellij.driver.sdk.ui.remote.RobotServiceProvider +import com.intellij.driver.sdk.ui.remote.SearchService class UIComponentsList(private val xpath: String, private val type: Class, val driver: Driver, - val robotService: RobotService, + val searchService: SearchService, + val robotServiceProvider: RobotServiceProvider, private val parentSearchContext: SearchContext) { fun list(): List { return parentSearchContext.findAll(xpath).mapIndexed { n, c -> @@ -17,12 +19,12 @@ class UIComponentsList(private val xpath: String, get() = parentSearchContext.context + xpath + "[$n]" override fun findAll(xpath: String): List { - return robotService.findAll(xpath, c) + return searchService.findAll(xpath, c) } } type.getConstructor( ComponentData::class.java - ).newInstance(ComponentData(xpath, driver, robotService, searchContext, c)) + ).newInstance(ComponentData(xpath, driver, searchService, robotServiceProvider, searchContext, c)) } } } \ No newline at end of file diff --git a/platform/remote-driver/test-sdk/src/com/intellij/driver/sdk/ui/components/UiComponent.kt b/platform/remote-driver/test-sdk/src/com/intellij/driver/sdk/ui/components/UiComponent.kt index 3af7ebcd1a57..0503df5d3a1a 100644 --- a/platform/remote-driver/test-sdk/src/com/intellij/driver/sdk/ui/components/UiComponent.kt +++ b/platform/remote-driver/test-sdk/src/com/intellij/driver/sdk/ui/components/UiComponent.kt @@ -10,6 +10,8 @@ import com.intellij.driver.sdk.ui.UiText import com.intellij.driver.sdk.ui.keyboard.WithKeyboard import com.intellij.driver.sdk.ui.remote.Component import com.intellij.driver.sdk.ui.remote.RobotService +import com.intellij.driver.sdk.ui.remote.RobotServiceProvider +import com.intellij.driver.sdk.ui.remote.SearchService import com.intellij.driver.sdk.waitFor import java.awt.Point import kotlin.time.Duration @@ -18,7 +20,8 @@ import kotlin.time.Duration.Companion.seconds data class ComponentData(val xpath: String, val driver: Driver, - val robotService: RobotService, + val searchService: SearchService, + val robotServiceProvider: RobotServiceProvider, val parentSearchContext: SearchContext, val foundComponent: Component?) @@ -32,21 +35,27 @@ open class UiComponent(private val data: ComponentData) : Finder, WithKeyboard { } private fun findThisComponent(): Component { + lateinit var result: List waitFor(DEFAULT_FIND_TIMEOUT_SECONDS.seconds, errorMessage = "Can't find component with '${data.xpath}' in ${data.parentSearchContext.context.takeIf { it.isNotEmpty() } ?: "whole hierarchy"}") { - data.parentSearchContext.findAll(data.xpath).size == 1 + result = data.parentSearchContext.findAll(data.xpath) + result.size == 1 } - return data.parentSearchContext.findAll(data.xpath).first() + return result.first() } override val driver: Driver = data.driver - override val robotService: RobotService = data.robotService + override val searchService: SearchService = data.searchService + override val robotServiceProvider: RobotServiceProvider = data.robotServiceProvider + override val robotService: RobotService by lazy { + data.robotServiceProvider.getRobotServiceFor(component) + } override val searchContext: SearchContext = object : SearchContext { override val context: String = data.parentSearchContext.context + data.xpath override fun findAll(xpath: String): List { - return robotService.findAll(xpath, component) + return searchService.findAll(xpath, component) } } @@ -92,19 +101,19 @@ open class UiComponent(private val data: ComponentData) : Finder, WithKeyboard { } fun findText(predicate: (TextData) -> Boolean): UiText { - return robotService.findAllText(component).single(predicate).let { UiText(this, it) } + return searchService.findAllText(component).single(predicate).let { UiText(this, it) } } fun present(): Boolean { - return robotService.findAll(data.xpath).isNotEmpty() + return searchService.findAll(data.xpath).isNotEmpty() } fun notPresent(): Boolean { - return robotService.findAll(data.xpath).isEmpty() + return searchService.findAll(data.xpath).isEmpty() } fun hasText(predicate: (TextData) -> Boolean): Boolean { - return robotService.findAllText(component).any(predicate) + return searchService.findAllText(component).any(predicate) } fun isVisible(): Boolean = component.isVisible() @@ -112,11 +121,11 @@ open class UiComponent(private val data: ComponentData) : Finder, WithKeyboard { fun isEnabled(): Boolean = component.isEnabled() fun findAllText(predicate: (TextData) -> Boolean): List { - return robotService.findAllText(component).filter(predicate).map { UiText(this, it) } + return searchService.findAllText(component).filter(predicate).map { UiText(this, it) } } fun findAllText(): List { - return robotService.findAllText(component).map { UiText(this, it) } + return searchService.findAllText(component).map { UiText(this, it) } } fun hasVisibleComponent(component: UiComponent): Boolean { diff --git a/platform/remote-driver/test-sdk/src/com/intellij/driver/sdk/ui/components/WelcomeScreenUI.kt b/platform/remote-driver/test-sdk/src/com/intellij/driver/sdk/ui/components/WelcomeScreenUI.kt index e25083202cc6..73b2ebfa3a55 100644 --- a/platform/remote-driver/test-sdk/src/com/intellij/driver/sdk/ui/components/WelcomeScreenUI.kt +++ b/platform/remote-driver/test-sdk/src/com/intellij/driver/sdk/ui/components/WelcomeScreenUI.kt @@ -3,8 +3,10 @@ package com.intellij.driver.sdk.ui.components import com.intellij.driver.client.Driver import com.intellij.driver.sdk.ui.* -fun Finder.welcomeScreen(action: WelcomeScreenUI.() -> Unit = {}) = - x("//div[@class='FlatWelcomeFrame']", WelcomeScreenUI::class.java).apply(action) +fun Finder.welcomeScreen(action: WelcomeScreenUI.() -> Unit = {}): WelcomeScreenUI { + val welcomeScreenClass = if (isRemoteIdeMode) "TabbedWelcomeScreen" else "FlatWelcomeFrame" + return x("//div[@class='${welcomeScreenClass}']", WelcomeScreenUI::class.java).apply(action) +} fun Driver.welcomeScreen(action: WelcomeScreenUI.() -> Unit = {}) = this.ui.welcomeScreen(action) diff --git a/platform/remote-driver/test-sdk/src/com/intellij/driver/sdk/ui/remote/RobotService.kt b/platform/remote-driver/test-sdk/src/com/intellij/driver/sdk/ui/remote/RobotService.kt index 59cb6264f842..c7740870c40e 100644 --- a/platform/remote-driver/test-sdk/src/com/intellij/driver/sdk/ui/remote/RobotService.kt +++ b/platform/remote-driver/test-sdk/src/com/intellij/driver/sdk/ui/remote/RobotService.kt @@ -1,8 +1,6 @@ package com.intellij.driver.sdk.ui.remote import com.intellij.driver.client.Remote -import com.intellij.driver.model.TextDataList -import org.intellij.lang.annotations.Language internal const val REMOTE_ROBOT_MODULE_ID = "com.jetbrains.performancePlugin/intellij.performanceTesting.remoteDriver" @@ -10,8 +8,10 @@ internal const val REMOTE_ROBOT_MODULE_ID = "com.jetbrains.performancePlugin/int plugin = REMOTE_ROBOT_MODULE_ID) interface RobotService { val robot: Robot - fun findAll(@Language("xpath") xpath: String): List - fun findAll(@Language("xpath") xpath: String, component: Component): List - fun findAllText(component: Component): TextDataList fun saveHierarchy(folderPath: String, fileName: String = "ui") -} \ No newline at end of file +} + +interface RobotServiceProvider { + fun getDefaultRobotService(): RobotService + fun getRobotServiceFor(obj: Any?): RobotService +} diff --git a/platform/remote-driver/test-sdk/src/com/intellij/driver/sdk/ui/remote/SearchService.kt b/platform/remote-driver/test-sdk/src/com/intellij/driver/sdk/ui/remote/SearchService.kt new file mode 100644 index 000000000000..3684b76e31f1 --- /dev/null +++ b/platform/remote-driver/test-sdk/src/com/intellij/driver/sdk/ui/remote/SearchService.kt @@ -0,0 +1,13 @@ +package com.intellij.driver.sdk.ui.remote + +import com.intellij.driver.client.Remote +import com.intellij.driver.model.TextDataList +import org.intellij.lang.annotations.Language + +@Remote("com.jetbrains.performancePlugin.remotedriver.SearchService", + plugin = REMOTE_ROBOT_MODULE_ID) +interface SearchService { + fun findAll(@Language("xpath") xpath: String): List + fun findAll(@Language("xpath") xpath: String, component: Component): List + fun findAllText(component: Component): TextDataList +} \ No newline at end of file diff --git a/plugins/performanceTesting/core/src/com/jetbrains/performancePlugin/jmxDriver/InvokerService.kt b/plugins/performanceTesting/core/src/com/jetbrains/performancePlugin/jmxDriver/InvokerService.kt index a5ec4652b8de..581f54d5f23c 100644 --- a/plugins/performanceTesting/core/src/com/jetbrains/performancePlugin/jmxDriver/InvokerService.kt +++ b/plugins/performanceTesting/core/src/com/jetbrains/performancePlugin/jmxDriver/InvokerService.kt @@ -2,12 +2,15 @@ package com.jetbrains.performancePlugin.jmxDriver import com.intellij.driver.impl.Invoker import com.intellij.driver.impl.InvokerMBean +import com.intellij.driver.model.transport.Ref +import com.intellij.idea.AppMode import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.components.Service import com.intellij.openapi.diagnostic.Logger import com.intellij.platform.diagnostic.telemetry.IJTracer import com.intellij.util.PlatformUtils import io.opentelemetry.context.Context +import java.awt.Component import java.lang.management.ManagementFactory import java.util.function.Consumer import java.util.function.Supplier @@ -26,10 +29,17 @@ class InvokerService { private val jmxBackendPort = System.getProperty(BACKEND_JMX_PORT_PROPERTY) private val log = Logger.getInstance(InvokerService::class.java) } - var invoker: InvokerMBean? = null - private set - fun isReady(): Boolean = invoker != null + private var myInvoker: InvokerMBean? = null + + val invoker: InvokerMBean + get() = myInvoker ?: throw IllegalStateException("Invoker is not registered") + + fun isReady(): Boolean = myInvoker != null + + fun putReference(c: Component): Ref { + return invoker.putAdhocReference(c) + } @Throws(JMException::class) fun register(tracerSupplier: Supplier, @@ -38,15 +48,21 @@ class InvokerService { val objectName = ObjectName("com.intellij.driver:type=Invoker") val server = ManagementFactory.getPlatformMBeanServer() - val localInvoker = Invoker("v", tracerSupplier, timedContextSupplier, screenshotAction) + val prefix = when { + PlatformUtils.isJetBrainsClient() -> Ref.FRONTEND_REFERENCE_PREFIX + AppMode.isRemoteDevHost() -> Ref.BACKEND_REFERENCE_PREFIX + else -> "" + } + + val localInvoker = Invoker(prefix, tracerSupplier, timedContextSupplier, screenshotAction) val remoteJmxAddress = jmxBackendPort?.let { "$JMX_BACKEND_IP:$it" } if (PlatformUtils.isJetBrainsClient() && remoteJmxAddress != null) { - log.info("Remote Dev Mode") - invoker = RemoteDevInvoker(localInvoker, remoteJmxAddress) + log.info("Remote Dev Mode, $remoteJmxAddress") + myInvoker = RemoteDevInvoker(localInvoker, remoteJmxAddress) } else { - invoker = localInvoker + myInvoker = localInvoker } server.registerMBean(invoker, objectName) } diff --git a/plugins/performanceTesting/core/src/com/jetbrains/performancePlugin/jmxDriver/RemoteDevInvoker.kt b/plugins/performanceTesting/core/src/com/jetbrains/performancePlugin/jmxDriver/RemoteDevInvoker.kt index 9b3994d0d0dc..4c57dcd6c4ff 100644 --- a/plugins/performanceTesting/core/src/com/jetbrains/performancePlugin/jmxDriver/RemoteDevInvoker.kt +++ b/plugins/performanceTesting/core/src/com/jetbrains/performancePlugin/jmxDriver/RemoteDevInvoker.kt @@ -1,20 +1,39 @@ package com.jetbrains.performancePlugin.jmxDriver +import com.intellij.driver.client.Driver +import com.intellij.driver.client.impl.DriverImpl import com.intellij.driver.client.impl.Invoker -import com.intellij.driver.client.impl.JmxCallHandler import com.intellij.driver.client.impl.JmxHost import com.intellij.driver.impl.InvokerMBean +import com.intellij.driver.model.DriverIlligalStateException import com.intellij.driver.model.ProductVersion +import com.intellij.driver.model.RdTarget import com.intellij.driver.model.transport.Ref import com.intellij.driver.model.transport.RemoteCall import com.intellij.driver.model.transport.RemoteCallResult -import kotlin.jvm.java -interface RemoteDevInvokerMBean : InvokerMBean +interface RemoteDevInvokerMBean : InvokerMBean { + val driver: Driver +} internal class RemoteDevInvoker(private val localInvoker: InvokerMBean, remoteJmxAddress: String) : RemoteDevInvokerMBean { - private val remoteInvoker = JmxCallHandler.jmx(Invoker::class.java, JmxHost(null, null, remoteJmxAddress)) + + override val driver: DriverImpl + + init { + val originalClassLoader = Thread.currentThread().getContextClassLoader() + try { + Thread.currentThread().setContextClassLoader(this::class.java.getClassLoader()) + driver = Driver.create(JmxHost(null, null, remoteJmxAddress)) as DriverImpl + } + finally { + Thread.currentThread().setContextClassLoader(originalClassLoader) + } + } + + private val remoteInvoker: Invoker + get() = driver.getInvoker() override fun getProductVersion(): ProductVersion { return localInvoker.productVersion @@ -29,14 +48,30 @@ internal class RemoteDevInvoker(private val localInvoker: InvokerMBean, remoteJm } override fun invoke(call: RemoteCall): RemoteCallResult { - - try { - return localInvoker.invoke(call) + when (call.rdTarget) { + RdTarget.FRONTEND_ONLY -> return localInvoker.invoke(call) + RdTarget.BACKEND_ONLY -> return invokeRemote(call) + RdTarget.FRONTEND_FIRST -> try { + return localInvoker.invoke(call) + } + catch (_: DriverIlligalStateException) { + return invokeRemote(call) + } + catch (t: Throwable) { + throw t + } } - // replace with ReferenceNotFoundException later - catch (e: Throwable) { + } + + private fun invokeRemote(call: RemoteCall): RemoteCallResult { + val originalClassLoader = Thread.currentThread().getContextClassLoader() + try { + Thread.currentThread().setContextClassLoader(this::class.java.getClassLoader()) return remoteInvoker.invoke(call) } + finally { + Thread.currentThread().setContextClassLoader(originalClassLoader) + } } override fun newSession(): Int { diff --git a/plugins/performanceTesting/remote-driver/intellij.performanceTesting.remoteDriver.iml b/plugins/performanceTesting/remote-driver/intellij.performanceTesting.remoteDriver.iml index a9962d784604..1addd79d4378 100644 --- a/plugins/performanceTesting/remote-driver/intellij.performanceTesting.remoteDriver.iml +++ b/plugins/performanceTesting/remote-driver/intellij.performanceTesting.remoteDriver.iml @@ -18,11 +18,11 @@ - + - - + + \ No newline at end of file diff --git a/plugins/performanceTesting/remote-driver/resources/intellij.performanceTesting.remoteDriver.xml b/plugins/performanceTesting/remote-driver/resources/intellij.performanceTesting.remoteDriver.xml index 1f5c24c01aa3..a228b9292fc1 100644 --- a/plugins/performanceTesting/remote-driver/resources/intellij.performanceTesting.remoteDriver.xml +++ b/plugins/performanceTesting/remote-driver/resources/intellij.performanceTesting.remoteDriver.xml @@ -1,10 +1,9 @@ - - + + > { - return xpathSearcher.findComponents(xpath, null) - } - - fun findAll(xpath: String, component: Component): List> { - return xpathSearcher.findComponents(xpath, component) - } - - fun findAllText(component: Component): TextDataList { - return TextParser.parseComponent(component, TextToKeyCache).let { TextDataList().apply { addAll(it) } } - } - fun saveHierarchy(folderPath: String, fileName: String = "ui.html") { - val html = xpathSearcher.modelCreator.create(null).convertToHtml() + val html = XpathDataModelCreator.create(null).convertToHtml() Paths.get(folderPath).resolve(fileName).toFile().writeText(html) staticFiles.forEach { staticFilePath -> diff --git a/plugins/performanceTesting/remote-driver/src/com/jetbrains/performancePlugin/remotedriver/SearchService.kt b/plugins/performanceTesting/remote-driver/src/com/jetbrains/performancePlugin/remotedriver/SearchService.kt new file mode 100644 index 000000000000..cd324ef79227 --- /dev/null +++ b/plugins/performanceTesting/remote-driver/src/com/jetbrains/performancePlugin/remotedriver/SearchService.kt @@ -0,0 +1,27 @@ +package com.jetbrains.performancePlugin.remotedriver + +import com.intellij.driver.model.RefDelegate +import com.intellij.driver.model.TextDataList +import com.intellij.openapi.components.Service +import com.jetbrains.performancePlugin.remotedriver.dataextractor.TextParser +import com.jetbrains.performancePlugin.remotedriver.dataextractor.TextToKeyCache +import com.jetbrains.performancePlugin.remotedriver.xpath.XpathSearcher +import java.awt.Component + +@Suppress("unused") +@Service(Service.Level.APP) +internal class SearchService { + private val xpathSearcher: XpathSearcher = XpathSearcher() + + fun findAll(xpath: String): List> { + return xpathSearcher.findComponents(xpath, null) + } + + fun findAll(xpath: String, component: Component): List> { + return xpathSearcher.findComponents(xpath, component) + } + + fun findAllText(component: Component): TextDataList { + return TextParser.parseComponent(component, TextToKeyCache).let { TextDataList().apply { addAll(it) } } + } +} \ No newline at end of file diff --git a/plugins/performanceTesting/remote-driver/src/com/jetbrains/performancePlugin/remotedriver/webservice/UiHierarchyWebService.kt b/plugins/performanceTesting/remote-driver/src/com/jetbrains/performancePlugin/remotedriver/webservice/UiHierarchyWebService.kt index a0c619373082..870d3da5e693 100644 --- a/plugins/performanceTesting/remote-driver/src/com/jetbrains/performancePlugin/remotedriver/webservice/UiHierarchyWebService.kt +++ b/plugins/performanceTesting/remote-driver/src/com/jetbrains/performancePlugin/remotedriver/webservice/UiHierarchyWebService.kt @@ -1,10 +1,7 @@ package com.jetbrains.performancePlugin.remotedriver.webservice import com.intellij.openapi.extensions.ExtensionNotApplicableException -import com.intellij.openapi.extensions.ExtensionPointName import com.intellij.openapi.util.registry.Registry -import com.jetbrains.performancePlugin.jmxDriver.InvokerService -import com.jetbrains.performancePlugin.remotedriver.dataextractor.TextToKeyCache import com.jetbrains.performancePlugin.remotedriver.webservice.routing.CantFindRouteException import com.jetbrains.performancePlugin.remotedriver.webservice.routing.Routing import com.jetbrains.performancePlugin.remotedriver.webservice.routing.StaticFile @@ -16,8 +13,6 @@ import io.netty.channel.ChannelHandlerContext import io.netty.handler.codec.http.* import org.jetbrains.ide.RestService import org.jetbrains.io.response -import org.jsoup.helper.W3CDom -import java.awt.Component internal class UiHierarchyWebService : RestService() { init { @@ -38,33 +33,11 @@ internal class UiHierarchyWebService : RestService() { get("/") { hierarchy() } - get("/raw") { - rawHierarchy() - } - get("/{id}") { - val id = pathParameters["id"] ?: throw IllegalStateException("id parameter is not found") - val component = UiHierarchyWebServiceExtension.EP_NAME.extensionList.firstNotNullOf { it.getComponentById(id) } - partialHierarchy(component) - } static("/static") } - private fun partialHierarchy(component: Component): String { - val doc = XpathDataModelCreator(TextToKeyCache) { c, e -> - val ref = InvokerService.getInstance().invoker!!.putAdhocReference(c) - e.setAttribute("remoteId", ref.id) - e.setAttribute("hashCode", ref.identityHashCode.toString()) - }.create(component) - return W3CDom().asString(doc) - } - - private fun rawHierarchy(): String { - val doc = XpathDataModelCreator(TextToKeyCache).create(null) - return W3CDom().asString(doc) - } - private fun hierarchy(): String { - val doc = XpathDataModelCreator(TextToKeyCache).create(null) + val doc = XpathDataModelCreator.create(null) return doc.convertToHtml() } @@ -84,11 +57,3 @@ internal class UiHierarchyWebService : RestService() { return null } } - -interface UiHierarchyWebServiceExtension { - companion object { - val EP_NAME = ExtensionPointName("com.jetbrains.performancePlugin.remoteDriver.uiHierarchyExtension") - } - - fun getComponentById(id: String): Component? -} diff --git a/plugins/performanceTesting/remote-driver/src/com/jetbrains/performancePlugin/remotedriver/xpath/XpathDataModelCreator.kt b/plugins/performanceTesting/remote-driver/src/com/jetbrains/performancePlugin/remotedriver/xpath/XpathDataModelCreator.kt index 58d54ff9b23c..85e8f6d232a3 100644 --- a/plugins/performanceTesting/remote-driver/src/com/jetbrains/performancePlugin/remotedriver/xpath/XpathDataModelCreator.kt +++ b/plugins/performanceTesting/remote-driver/src/com/jetbrains/performancePlugin/remotedriver/xpath/XpathDataModelCreator.kt @@ -2,6 +2,7 @@ package com.jetbrains.performancePlugin.remotedriver.xpath +import com.jetbrains.performancePlugin.jmxDriver.InvokerService import com.jetbrains.performancePlugin.remotedriver.dataextractor.TextParser import com.jetbrains.performancePlugin.remotedriver.dataextractor.TextToKeyCache import org.assertj.swing.edt.GuiActionRunner @@ -20,27 +21,39 @@ import javax.swing.JComponent import javax.xml.parsers.DocumentBuilderFactory import kotlin.math.absoluteValue -internal class XpathDataModelCreator(private val textToKeyCache: TextToKeyCache, - private val bindComponentToElement: (Component, Element) -> Unit = { c, e -> - e.setUserData("component", c, null) - }) { +object XpathDataModelCreator { + interface ComponentTranslator { + fun translate(c: Component, e: Element) + } + + private object DefaultComponentTranslator : ComponentTranslator { + override fun translate(c: Component, e: Element) { + e.setUserData("component", c, null) + } + } + + private object RemoteComponentTranslator : ComponentTranslator { + override fun translate(c: Component, e: Element) { + val ref = InvokerService.getInstance().putReference(c) + e.setAttribute("remoteId", ref.id) + e.setAttribute("hashCode", ref.identityHashCode.toString()) + } + } private fun addComponent( doc: Document, parentElement: Element, hierarchy: ComponentHierarchy, component: Component, + translator: ComponentTranslator, targetComponent: Component? = null ) { - val subtreeOverrider = XpathDataModelSubTreeProvider.EP_NAME.extensionList.find { provider -> - provider.shouldOverrideSubtree(component) - } - if (subtreeOverrider != null) { - subtreeOverrider.overrideSubtree(doc, parentElement, component) + XpathDataModelExtension.EP_NAME.extensionList.find { it.acceptComponent(component) }?.let { + it.processComponent(doc, parentElement, component) return } - val element = createElement(doc, component, targetComponent) + val element = createElement(doc, component, translator, targetComponent) parentElement.appendChild(element) val allChildren = hierarchy.childrenOf(component) @@ -58,17 +71,17 @@ internal class XpathDataModelCreator(private val textToKeyCache: TextToKeyCache, addAll(filteredChildren) addAll(exceptionChildren) }.sortedWith(ComponentOrderComparator).forEach { - addComponent(doc, element, hierarchy, it, targetComponent) + addComponent(doc, element, hierarchy, it, translator, targetComponent) } } - private fun createElement(doc: Document, component: Component, targetComponent: Component? = null): Element { + private fun createElement(doc: Document, component: Component, translator: ComponentTranslator, targetComponent: Component? = null): Element { val element = doc.createElement("div") component.fillElement(doc, element, targetComponent) - bindComponentToElement(component, element) + translator.translate(component, element) return element } @@ -123,7 +136,7 @@ internal class XpathDataModelCreator(private val textToKeyCache: TextToKeyCache, value?.removeInvalidXmlCharacters()?.apply { if (textFieldsFilter(attributeName, value)) { elementText.append("$attributeName: '$this'. ") - textToKeyCache.findKey(value)?.apply { + TextToKeyCache.findKey(value)?.apply { elementText.append("${attributeName}.key: '$this'. ") element.setAttribute(attributeName + ".key", this) } @@ -146,13 +159,13 @@ internal class XpathDataModelCreator(private val textToKeyCache: TextToKeyCache, } if (accessibleName != null) { element.setAttribute("accessiblename", accessibleName) - textToKeyCache.findKey(accessibleName)?.apply { element.setAttribute("accessiblename.key", this) } + TextToKeyCache.findKey(accessibleName)?.apply { element.setAttribute("accessiblename.key", this) } } val tooltipText = getTooltipText(this) if (tooltipText != null) { element.setAttribute("tooltiptext", tooltipText) - textToKeyCache.findKey(tooltipText)?.apply { element.setAttribute("tooltiptext.key", this) } + TextToKeyCache.findKey(tooltipText)?.apply { element.setAttribute("tooltiptext.key", this) } } if (isShowing) { @@ -166,7 +179,7 @@ internal class XpathDataModelCreator(private val textToKeyCache: TextToKeyCache, && bounds.width > 0 && bounds.height > 0 ) { try { - val foundText = TextParser.parseComponent(this, textToKeyCache) + val foundText = TextParser.parseComponent(this, TextToKeyCache) val text = foundText.joinToString(" || ") { it.text } element.setAttribute("visible_text", text) if (text.trim().isNotEmpty()) { @@ -291,8 +304,19 @@ internal class XpathDataModelCreator(private val textToKeyCache: TextToKeyCache, } + fun create(component: Component?, targetComponent: Component? = null): Document { + return create(component, DefaultComponentTranslator, targetComponent) + } - fun create(rootComponent: Component?, targetComponent: Component? = null): Document { + fun createForRemote(component: Component?, targetComponent: Component? = null): Document { + return create(component, RemoteComponentTranslator, targetComponent) + } + + private fun create( + rootComponent: Component?, + translator: ComponentTranslator, + targetComponent: Component? = null, + ): Document { val doc = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument() @@ -308,7 +332,7 @@ internal class XpathDataModelCreator(private val textToKeyCache: TextToKeyCache, hierarchy.roots() } containers.filter { it.isShowing || it.javaClass.name.endsWith("SharedOwnerFrame") }.forEach { - addComponent(doc, rootElement, hierarchy, it, targetComponent) + addComponent(doc, rootElement, hierarchy, it, translator, targetComponent) } } }) diff --git a/plugins/performanceTesting/remote-driver/src/com/jetbrains/performancePlugin/remotedriver/xpath/XpathDataModelSubTreeProvider.kt b/plugins/performanceTesting/remote-driver/src/com/jetbrains/performancePlugin/remotedriver/xpath/XpathDataModelExtension.kt similarity index 51% rename from plugins/performanceTesting/remote-driver/src/com/jetbrains/performancePlugin/remotedriver/xpath/XpathDataModelSubTreeProvider.kt rename to plugins/performanceTesting/remote-driver/src/com/jetbrains/performancePlugin/remotedriver/xpath/XpathDataModelExtension.kt index 71225349bc33..059905e82867 100644 --- a/plugins/performanceTesting/remote-driver/src/com/jetbrains/performancePlugin/remotedriver/xpath/XpathDataModelSubTreeProvider.kt +++ b/plugins/performanceTesting/remote-driver/src/com/jetbrains/performancePlugin/remotedriver/xpath/XpathDataModelExtension.kt @@ -6,11 +6,11 @@ import org.w3c.dom.Document import org.w3c.dom.Element import java.awt.Component -interface XpathDataModelSubTreeProvider { +interface XpathDataModelExtension { companion object { - val EP_NAME = ExtensionPointName("com.jetbrains.performancePlugin.remoteDriver.xpathDataModelExtension") + val EP_NAME = ExtensionPointName.create("com.jetbrains.performancePlugin.remotedriver.xpathDataModelExtension") } - fun shouldOverrideSubtree(component: Component): Boolean - fun overrideSubtree(doc: Document, parentElement: Element, component: Component) + fun acceptComponent(component: Component): Boolean + fun processComponent(doc: Document, parentElement: Element, component: Component) } \ No newline at end of file diff --git a/plugins/performanceTesting/remote-driver/src/com/jetbrains/performancePlugin/remotedriver/xpath/XpathSearcher.kt b/plugins/performanceTesting/remote-driver/src/com/jetbrains/performancePlugin/remotedriver/xpath/XpathSearcher.kt index 02e029361f3f..87d37c0bdf11 100644 --- a/plugins/performanceTesting/remote-driver/src/com/jetbrains/performancePlugin/remotedriver/xpath/XpathSearcher.kt +++ b/plugins/performanceTesting/remote-driver/src/com/jetbrains/performancePlugin/remotedriver/xpath/XpathSearcher.kt @@ -6,15 +6,13 @@ import com.intellij.driver.model.LocalRefDelegate import com.intellij.driver.model.RefDelegate import com.intellij.driver.model.RemoteRefDelegate import com.intellij.driver.model.transport.Ref -import com.jetbrains.performancePlugin.remotedriver.dataextractor.TextToKeyCache import org.w3c.dom.Element import org.w3c.dom.NodeList import java.awt.Component import javax.xml.xpath.XPathConstants import javax.xml.xpath.XPathFactory -internal class XpathSearcher(textToKeyCache: TextToKeyCache) { - val modelCreator = XpathDataModelCreator(textToKeyCache) +internal class XpathSearcher { private val xPath = XPathFactory.newInstance().newXPath() fun findComponent(xpathExpression: String, component: Component?): RefDelegate { @@ -29,7 +27,7 @@ internal class XpathSearcher(textToKeyCache: TextToKeyCache) { } fun findComponents(xpathExpression: String, component: Component?): List> { - val model = modelCreator.create(component) + val model = XpathDataModelCreator.create(component) val result = xPath.compile(xpathExpression).evaluate(model, XPathConstants.NODESET) as NodeList return (0 until result.length).mapNotNull { result.item(it) }.filterIsInstance().mapNotNull { if (it.hasAttribute("remoteId")) {