-
-
Notifications
You must be signed in to change notification settings - Fork 8.7k
[grid] Fix DefaultSlotMatcher matching requests differentiated only by undeclared automationName #17898
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: trunk
Are you sure you want to change the base?
[grid] Fix DefaultSlotMatcher matching requests differentiated only by undeclared automationName #17898
Changes from all commits
8e4d708
e62d621
97ccbef
0d2c0c8
a5511ed
ae0e1bc
5d5202e
aa3fdcd
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,103 @@ | ||
| // Licensed to the Software Freedom Conservancy (SFC) under one | ||
| // or more contributor license agreements. See the NOTICE file | ||
| // distributed with this work for additional information | ||
| // regarding copyright ownership. The SFC licenses this file | ||
| // to you under the Apache License, Version 2.0 (the | ||
| // "License"); you may not use this file except in compliance | ||
| // with the License. You may obtain a copy of the License at | ||
| // | ||
| // http://www.apache.org/licenses/LICENSE-2.0 | ||
| // | ||
| // Unless required by applicable law or agreed to in writing, | ||
| // software distributed under the License is distributed on an | ||
| // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | ||
| // KIND, either express or implied. See the License for the | ||
| // specific language governing permissions and limitations | ||
| // under the License. | ||
|
|
||
| package org.openqa.selenium.grid.data; | ||
|
|
||
| import java.io.Serializable; | ||
| import java.util.Objects; | ||
| import org.openqa.selenium.Capabilities; | ||
|
|
||
| /** | ||
| * Opt-in matching implementation for Nodes that relay sessions to an Appium server. Unlike {@link | ||
| * DefaultSlotMatcher}, a stereotype that advertises Appium-awareness (an {@code appium:}-prefixed | ||
| * capability, or the non-W3C {@code platformVersion} signal) is treated as a wildcard for | ||
| * automationName, and a request carrying app-relay capabilities ({@link | ||
| * DefaultSlotMatcher#SPECIFIC_RELAY_CAPABILITIES_APP}) bypasses browserName/browserVersion | ||
| * matching. This lets a single relay slot serve varied automation frameworks and hybrid | ||
| * browser/native-app requests without the operator enumerating every client value in the | ||
| * stereotype. | ||
| * | ||
| * <p>Configure a Node to use this matcher instead of the default with: | ||
| * | ||
| * <pre> | ||
| * [distributor] | ||
| * slot-matcher = "org.openqa.selenium.grid.data.AppiumRelaySlotMatcher" | ||
| * </pre> | ||
| */ | ||
| public class AppiumRelaySlotMatcher implements SlotMatcher, Serializable { | ||
|
|
||
| private final DefaultSlotMatcher strict = new DefaultSlotMatcher(); | ||
|
|
||
| @Override | ||
| public boolean matches(Capabilities stereotype, Capabilities capabilities) { | ||
|
|
||
| if (capabilities.asMap().isEmpty()) { | ||
| return false; | ||
| } | ||
|
|
||
| if (!strict.initialMatch(stereotype, capabilities)) { | ||
| return false; | ||
| } | ||
|
|
||
| if (!strict.managedDownloadsEnabled(stereotype, capabilities)) { | ||
| return false; | ||
| } | ||
|
|
||
| if (!strict.extensionCapabilitiesMatch(stereotype, capabilities)) { | ||
| return false; | ||
|
Comment on lines
+60
to
+61
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 5. Relay wildcard check is preempted AppiumRelaySlotMatcher runs strict extension matching before its wildcard automationName check, so a stereotype advertising appium:automationName=UiAutomator2 still rejects a request for another framework. This contradicts the new matcher's documented promise that Appium-aware stereotypes act as automationName wildcards for varied frameworks. Agent Prompt
|
||
| } | ||
|
|
||
| if (!automationNameMatch(stereotype, capabilities)) { | ||
| return false; | ||
| } | ||
|
|
||
| if (!strict.platformVersionMatch(stereotype, capabilities)) { | ||
| return false; | ||
| } | ||
|
|
||
| boolean browserNameMatch = | ||
| (capabilities.getBrowserName() == null || capabilities.getBrowserName().isEmpty()) | ||
| || Objects.equals(stereotype.getBrowserName(), capabilities.getBrowserName()) | ||
| || DefaultSlotMatcher.matchConditionToRemoveCapability(capabilities); | ||
| boolean browserVersionMatch = | ||
| (capabilities.getBrowserVersion() == null | ||
| || capabilities.getBrowserVersion().isEmpty() | ||
| || Objects.equals(capabilities.getBrowserVersion(), "stable")) | ||
| || strict.browserVersionMatch( | ||
| stereotype.getBrowserVersion(), capabilities.getBrowserVersion()) | ||
| || DefaultSlotMatcher.matchConditionToRemoveCapability(capabilities); | ||
|
Comment on lines
+80
to
+82
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 2. browserversion bypass remains untested The new matcher bypasses browserVersion mismatches for app-relay requests, but its new test class contains no scenario with browser versions. A regression in this advertised behavior could therefore pass the test suite unnoticed. Agent Prompt
|
||
| boolean platformNameMatch = | ||
| capabilities.getPlatformName() == null | ||
| || Objects.equals(stereotype.getPlatformName(), capabilities.getPlatformName()) | ||
| || (stereotype.getPlatformName() != null | ||
| && stereotype.getPlatformName().is(capabilities.getPlatformName())); | ||
| return browserNameMatch && browserVersionMatch && platformNameMatch; | ||
| } | ||
|
|
||
| private boolean automationNameMatch(Capabilities stereotype, Capabilities capabilities) { | ||
| /* | ||
| A stereotype with no Appium-related capabilities at all has no relationship to a | ||
| requested automationName, so it should not match. Otherwise, an Appium-aware | ||
| stereotype is allowed to omit automationName and still match, since relay | ||
| stereotypes intentionally do this to serve varied automation sessions. | ||
| */ | ||
| boolean stereotypeIsAppiumAware = | ||
| stereotype.getCapabilityNames().stream() | ||
| .anyMatch(name -> name.contains("platformVersion") || name.startsWith("appium:")); | ||
| return stereotypeIsAppiumAware || DefaultSlotMatcher.automationNameValue(capabilities) == null; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -21,6 +21,7 @@ | |
|
|
||
| import java.io.Serializable; | ||
| import java.util.List; | ||
| import java.util.Map; | ||
| import java.util.Objects; | ||
| import org.openqa.selenium.Capabilities; | ||
|
|
||
|
|
@@ -56,6 +57,14 @@ public class DefaultSlotMatcher implements SlotMatcher, Serializable { | |
| public static final List<String> MANDATORY_CAPABILITIES = | ||
| List.of("platformName", "browserName", "browserVersion"); | ||
|
|
||
| /** | ||
| * Determines whether {@code stereotype} is an acceptable match for a new session request carrying | ||
| * {@code capabilities}, per the class-level matching rules described above. | ||
| * | ||
| * @param stereotype the capabilities declared by a candidate {@link Slot} | ||
| * @param capabilities the capabilities requested for a new session | ||
| * @return {@code true} if the stereotype may serve the request | ||
| */ | ||
| @Override | ||
| public boolean matches(Capabilities stereotype, Capabilities capabilities) { | ||
|
|
||
|
|
@@ -75,21 +84,24 @@ public boolean matches(Capabilities stereotype, Capabilities capabilities) { | |
| return false; | ||
| } | ||
|
|
||
| if (!automationNameMatch(stereotype, capabilities)) { | ||
| return false; | ||
| } | ||
|
|
||
|
qodo-code-review[bot] marked this conversation as resolved.
|
||
| if (!platformVersionMatch(stereotype, capabilities)) { | ||
| return false; | ||
| } | ||
|
|
||
| // At the end, a simple browser, browserVersion and platformName match | ||
| boolean browserNameMatch = | ||
| (capabilities.getBrowserName() == null || capabilities.getBrowserName().isEmpty()) | ||
| || Objects.equals(stereotype.getBrowserName(), capabilities.getBrowserName()) | ||
| || matchConditionToRemoveCapability(capabilities); | ||
| || Objects.equals(stereotype.getBrowserName(), capabilities.getBrowserName()); | ||
| boolean browserVersionMatch = | ||
| (capabilities.getBrowserVersion() == null | ||
| || capabilities.getBrowserVersion().isEmpty() | ||
| || Objects.equals(capabilities.getBrowserVersion(), "stable")) | ||
| || browserVersionMatch(stereotype.getBrowserVersion(), capabilities.getBrowserVersion()) | ||
| || matchConditionToRemoveCapability(capabilities); | ||
| || browserVersionMatch( | ||
| stereotype.getBrowserVersion(), capabilities.getBrowserVersion()); | ||
| boolean platformNameMatch = | ||
| capabilities.getPlatformName() == null | ||
| || Objects.equals(stereotype.getPlatformName(), capabilities.getPlatformName()) | ||
|
|
@@ -98,11 +110,11 @@ public boolean matches(Capabilities stereotype, Capabilities capabilities) { | |
| return browserNameMatch && browserVersionMatch && platformNameMatch; | ||
| } | ||
|
|
||
| private boolean browserVersionMatch(String stereotype, String capabilities) { | ||
| boolean browserVersionMatch(String stereotype, String capabilities) { | ||
| return new SemanticVersionComparator().compare(stereotype, capabilities) == 0; | ||
| } | ||
|
|
||
| private Boolean initialMatch(Capabilities stereotype, Capabilities capabilities) { | ||
| Boolean initialMatch(Capabilities stereotype, Capabilities capabilities) { | ||
| return stereotype.getCapabilityNames().stream() | ||
| // Matching of extension capabilities is implementation independent. Skip them | ||
| .filter(name -> !name.contains(":")) | ||
|
|
@@ -128,7 +140,7 @@ private Boolean initialMatch(Capabilities stereotype, Capabilities capabilities) | |
| .orElse(true); | ||
| } | ||
|
|
||
| private Boolean managedDownloadsEnabled(Capabilities stereotype, Capabilities capabilities) { | ||
| Boolean managedDownloadsEnabled(Capabilities stereotype, Capabilities capabilities) { | ||
| // First lets check if user wanted a Node with managed downloads enabled | ||
| Object raw = capabilities.getCapability(ENABLE_DOWNLOADS); | ||
| if (raw == null || !Boolean.parseBoolean(raw.toString())) { | ||
|
|
@@ -141,7 +153,7 @@ private Boolean managedDownloadsEnabled(Capabilities stereotype, Capabilities ca | |
| return raw != null && Boolean.parseBoolean(raw.toString()); | ||
| } | ||
|
|
||
| private Boolean platformVersionMatch(Capabilities stereotype, Capabilities capabilities) { | ||
| Boolean platformVersionMatch(Capabilities stereotype, Capabilities capabilities) { | ||
| /* | ||
| This platform version match is not W3C compliant but users can add Appium servers as | ||
| Nodes, so we avoid delaying the match until the Slot, which makes the whole matching | ||
|
|
@@ -158,7 +170,7 @@ private Boolean platformVersionMatch(Capabilities stereotype, Capabilities capab | |
| .orElse(true); | ||
| } | ||
|
|
||
| private Boolean extensionCapabilitiesMatch(Capabilities stereotype, Capabilities capabilities) { | ||
| Boolean extensionCapabilitiesMatch(Capabilities stereotype, Capabilities capabilities) { | ||
| /* | ||
| We match extension capabilities when they are not prefixed with any of the | ||
| EXTENSION_CAPABILITIES_PREFIXES items. Also, we match them only when the capabilities | ||
|
|
@@ -188,6 +200,39 @@ private Boolean extensionCapabilitiesMatch(Capabilities stereotype, Capabilities | |
| .orElse(true); | ||
| } | ||
|
|
||
| Boolean automationNameMatch(Capabilities stereotype, Capabilities capabilities) { | ||
| /* | ||
| If the request specifies automationName (directly or nested in an options map), the | ||
| stereotype must declare the same value -- including the case where the stereotype | ||
| doesn't declare it at all. See https://github.com/SeleniumHQ/selenium/issues/17845. | ||
| */ | ||
| Object requestedAutomationName = automationNameValue(capabilities); | ||
| if (requestedAutomationName == null) { | ||
| return true; | ||
| } | ||
| return Objects.equals(requestedAutomationName, automationNameValue(stereotype)); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 3. Automation name becomes case-sensitive DefaultSlotMatcher now compares extracted automationName values with Objects.equals, so values differing only in case are rejected even though extensionCapabilitiesMatch explicitly accepts all string extension values case-insensitively. A stereotype using XCUITest therefore no longer matches a request using xcuitest, despite matching before this change. Agent Prompt
|
||
| } | ||
|
|
||
| static Object automationNameValue(Capabilities capabilities) { | ||
| return capabilities.getCapabilityNames().stream() | ||
| .map(name -> automationNameValueFor(name, capabilities.getCapability(name))) | ||
| .filter(Objects::nonNull) | ||
| .findFirst() | ||
| .orElse(null); | ||
| } | ||
|
|
||
| private static Object automationNameValueFor(String name, Object value) { | ||
| if (name.equals("automationName") || name.endsWith(":automationName")) { | ||
| return value; | ||
| } | ||
| // automationName is sometimes nested inside an options map (e.g. appium:options) rather | ||
| // than sent as its own top-level capability. | ||
| if (name.toLowerCase().contains("options") && value instanceof Map) { | ||
| return ((Map<?, ?>) value).get("automationName"); | ||
|
Comment on lines
+230
to
+231
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 4. Unrelated options maps misclassified automationNameValueFor treats every map-valued capability whose name contains options as an automation-name container, including unrelated vendor capabilities such as vendor:customOptions. If such metadata contains an automationName field, an otherwise compatible plain browser stereotype is incorrectly rejected. Agent Prompt
|
||
| } | ||
| return null; | ||
| } | ||
|
qodo-code-review[bot] marked this conversation as resolved.
qodo-code-review[bot] marked this conversation as resolved.
|
||
|
|
||
| public static Boolean matchConditionToRemoveCapability(Capabilities capabilities) { | ||
| /* | ||
| This match is specific for the Relay capabilities that are related to the Appium server for native application. | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
1. matches lacks javadoc
π Rule violationβ§ QualityAgent Prompt
β Copy this prompt and use it to remediate the issue with your preferred AI generation tools