Skip to content
103 changes: 103 additions & 0 deletions java/src/org/openqa/selenium/grid/data/AppiumRelaySlotMatcher.java
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) {
Comment on lines +45 to +46

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

1. matches lacks javadoc πŸ“˜ Rule violation ✧ Quality

The new public AppiumRelaySlotMatcher.matches method has no immediately preceding Javadoc
documenting its purpose, parameters, and return value. This leaves the newly exposed matching
behavior incompletely documented.
Agent Prompt
## Issue description
Add complete Javadoc immediately above the public `matches` method.

## Issue Context
The documentation should include a purpose sentence, `@param` tags for `stereotype` and `capabilities`, and a non-empty `@return` description.

## Fix Focus Areas
- java/src/org/openqa/selenium/grid/data/AppiumRelaySlotMatcher.java[45-46]

β“˜ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

5. Relay wildcard check is preempted 🐞 Bug ≑ Correctness

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
## Issue description
The relay matcher delegates `appium:automationName` to strict extension matching before applying its wildcard policy. Ensure automationName is excluded from that strict comparison for this matcher, or narrow the documented contract if explicit values are intentionally constraints.

## Issue Context
`DefaultSlotMatcher.extensionCapabilitiesMatch` compares matching Appium extension values, so the later permissive `automationNameMatch` cannot override a mismatch.

## Fix Focus Areas
- java/src/org/openqa/selenium/grid/data/AppiumRelaySlotMatcher.java[24-32]
- java/src/org/openqa/selenium/grid/data/AppiumRelaySlotMatcher.java[60-66]
- java/src/org/openqa/selenium/grid/data/DefaultSlotMatcher.java[173-200]
- java/test/org/openqa/selenium/grid/data/AppiumRelaySlotMatcherTest.java[74-117]

β“˜ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

}

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

2. browserversion bypass remains untested πŸ“˜ Rule violation β–£ Testability

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
## Issue description
Add a regression test for the new app-relay `browserVersion` bypass.

## Issue Context
Construct an Appium-aware stereotype and request with different non-empty browser versions plus an app-relay capability, then assert that `AppiumRelaySlotMatcher.matches` returns `true`. The assertion should fail if the bypass at lines 80-82 is removed.

## Fix Focus Areas
- java/test/org/openqa/selenium/grid/data/AppiumRelaySlotMatcherTest.java[119-142]
- java/src/org/openqa/selenium/grid/data/AppiumRelaySlotMatcher.java[76-82]

β“˜ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

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;
}
}
63 changes: 54 additions & 9 deletions java/src/org/openqa/selenium/grid/data/DefaultSlotMatcher.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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) {

Expand All @@ -75,21 +84,24 @@ public boolean matches(Capabilities stereotype, Capabilities capabilities) {
return false;
}

if (!automationNameMatch(stereotype, capabilities)) {
return false;
}

Comment thread
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())
Expand All @@ -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(":"))
Expand All @@ -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())) {
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

3. Automation name becomes case-sensitive 🐞 Bug ≑ Correctness

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
## Issue description
`automationNameMatch` uses `Objects.equals`, making string automation names case-sensitive even though existing extension-capability matching uses case-insensitive string comparison. Preserve the established behavior while still handling non-string values safely.

## Issue Context
This new check runs after extension matching and can reverse a successful case-insensitive extension match.

## Fix Focus Areas
- java/src/org/openqa/selenium/grid/data/DefaultSlotMatcher.java[203-214]
- java/test/org/openqa/selenium/grid/data/DefaultSlotMatcherTest.java[728-753]

β“˜ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

}

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

4. Unrelated options maps misclassified 🐞 Bug ≑ Correctness

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
## Issue description
Nested automationName extraction currently examines every capability whose name contains `options`. Restrict this behavior to capability names that actually represent Appium options and add coverage proving unrelated options maps do not affect matching.

## Issue Context
Capability keys are general extension names; containing the substring `options` does not establish Appium semantics.

## Fix Focus Areas
- java/src/org/openqa/selenium/grid/data/DefaultSlotMatcher.java[224-233]
- java/test/org/openqa/selenium/grid/data/DefaultSlotMatcherTest.java[799-818]

β“˜ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

}
return null;
}
Comment thread
qodo-code-review[bot] marked this conversation as resolved.
Comment thread
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.
Expand Down
Loading