diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..183416b --- /dev/null +++ b/.gitignore @@ -0,0 +1,19 @@ +.project +target +bin +.classpath +/test-output +/.settings +**/.settings +/com +/application.log +/sql.log +*.checkstyle +.idea +*.iml +test-output +*.log +/reports +/out +dependency-reduced-pom.xml +.DS_Store \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index c971fa0..cf0c928 100644 --- a/Dockerfile +++ b/Dockerfile @@ -64,6 +64,8 @@ ENV DEVICE_BUS=/dev/bus/usb/003/011 # Usbmuxd settings "host:port" ENV USBMUXD_SOCKET_ADDRESS= +ENV GRID_BROWSER_TIMEOUT 180 + #Setup libimobile device, usbmuxd and some tools RUN export DEBIAN_FRONTEND=noninteractive && apt-get update && apt-get -y install iputils-ping nano jq telnet netcat curl ffmpeg libimobiledevice-utils libimobiledevice6 usbmuxd socat @@ -82,6 +84,16 @@ COPY files/check-wda.sh /opt COPY files/zbr-config-gen.sh /opt COPY files/zbr-default-caps-gen.sh /opt +COPY target/mcloud-node-1.0.jar \ + /opt +COPY target/mcloud-node.jar \ + /opt + +COPY agent/target/mcloud-node-agent-1.0.jar \ + /opt +COPY agent/target/mcloud-node-agent.jar \ + /opt + ENV ENTRYPOINT_DIR=/opt/entrypoint RUN mkdir -p ${ENTRYPOINT_DIR} COPY entrypoint.sh ${ENTRYPOINT_DIR} diff --git a/agent/pom.xml b/agent/pom.xml new file mode 100644 index 0000000..5969ae8 --- /dev/null +++ b/agent/pom.xml @@ -0,0 +1,82 @@ + + + 4.0.0 + com.zebrunner + mcloud-node-agent + 1.0 + jar + Zebrunner Device Farm (Selenium Grid Node Agent) + + UTF-8 + 3.5.0 + 3.11.0 + + + + + net.bytebuddy + byte-buddy + 1.14.5 + + + + + + org.apache.maven.plugins + maven-shade-plugin + ${maven-shade-plugin.version} + + + package + + shade + + + false + + + + + MANIFEST.MF + + + + + META-INF/MANIFEST.MF + src/main/resources/META-INF/MANIFEST.MF + + + + + *:* + + META-INF/*.SF + META-INF/*.DSA + META-INF/*.RSA + + + + mcloud-node-agent + + + + + + org.apache.maven.plugins + maven-compiler-plugin + ${maven-compiler-plugin.version} + + 11 + 11 + + + + + diff --git a/agent/src/main/java/com/zebrunner/mcloud/grid/agent/NodeAgent.java b/agent/src/main/java/com/zebrunner/mcloud/grid/agent/NodeAgent.java new file mode 100644 index 0000000..86885bd --- /dev/null +++ b/agent/src/main/java/com/zebrunner/mcloud/grid/agent/NodeAgent.java @@ -0,0 +1,53 @@ +package com.zebrunner.mcloud.grid.agent; + +import net.bytebuddy.agent.builder.AgentBuilder; +import net.bytebuddy.description.method.MethodDescription; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.dynamic.DynamicType; +import net.bytebuddy.matcher.ElementMatcher; +import net.bytebuddy.matcher.NameMatcher; +import net.bytebuddy.pool.TypePool; + +import java.lang.instrument.Instrumentation; +import java.util.logging.Logger; + +import static net.bytebuddy.implementation.MethodDelegation.to; +import static net.bytebuddy.matcher.ElementMatchers.isPublic; +import static net.bytebuddy.matcher.ElementMatchers.isStatic; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.not; + +public class NodeAgent { + private static final Logger LOGGER = Logger.getLogger(NodeAgent.class.getName()); + private static final String RELAY_SESSION_FACTORY_CLASS = "org.openqa.selenium.grid.node.relay.RelaySessionFactory"; + private static final String TEST_METHOD_NAME = "test"; + + public static void premain(String args, Instrumentation instrumentation) { + try { + new AgentBuilder.Default() + .with(new AgentBuilder.InitializationStrategy.SelfInjection.Eager()) + .type(named(RELAY_SESSION_FACTORY_CLASS)) + .transform((builder, type, classloader, module, protectionDomain) -> addTestMethodInterceptor(builder)) + .installOn(instrumentation); + } catch (Exception e) { + LOGGER.warning(() -> "Could not init instrumentation."); + } + } + + private static DynamicType.Builder addTestMethodInterceptor(DynamicType.Builder builder) { + return builder.method(isTestMethod()) + .intercept(to(testMethodInterceptor())); + } + + public static ElementMatcher isTestMethod() { + return isPublic() + .and(not(isStatic())) + .and(new NameMatcher<>(TEST_METHOD_NAME::equals)); + } + + private static TypeDescription testMethodInterceptor() { + return TypePool.Default.ofSystemLoader() + .describe(RelaySessionFactoryInterceptor.class.getName()) + .resolve(); + } +} diff --git a/agent/src/main/java/com/zebrunner/mcloud/grid/agent/RelaySessionFactoryInterceptor.java b/agent/src/main/java/com/zebrunner/mcloud/grid/agent/RelaySessionFactoryInterceptor.java new file mode 100644 index 0000000..d91d809 --- /dev/null +++ b/agent/src/main/java/com/zebrunner/mcloud/grid/agent/RelaySessionFactoryInterceptor.java @@ -0,0 +1,16 @@ +package com.zebrunner.mcloud.grid.agent; + +import net.bytebuddy.implementation.bind.annotation.RuntimeType; +import net.bytebuddy.implementation.bind.annotation.SuperCall; +import net.bytebuddy.implementation.bind.annotation.This; + +import java.util.concurrent.Callable; + +public class RelaySessionFactoryInterceptor { + + @RuntimeType + public static Object onTestMethodInvocation(@This final Object factory, + @SuperCall final Callable proxy) throws Exception { + return true; + } +} diff --git a/agent/src/main/resources/META-INF/MANIFEST.MF b/agent/src/main/resources/META-INF/MANIFEST.MF new file mode 100644 index 0000000..2ecd4ea --- /dev/null +++ b/agent/src/main/resources/META-INF/MANIFEST.MF @@ -0,0 +1,4 @@ +Manifest-Version: 1.0 +Premain-Class: com.zebrunner.mcloud.grid.agent.NodeAgent +Can-Redefine-Classes: true +Can-Retransform-Classes: true diff --git a/entrypoint.sh b/entrypoint.sh index a3aa5aa..11b631e 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -1,6 +1,6 @@ #!/bin/bash -NODE_CONFIG_JSON="/root/nodeconfig.json" +NODE_CONFIG_JSON="/root/nodeconfig.toml" DEFAULT_CAPABILITIES_JSON="/root/defaultcapabilities.json" # show list of plugins including installed ones @@ -12,7 +12,7 @@ if [[ -n $APPIUM_PLUGINS ]]; then echo "plugins_cli: $plugins_cli" fi -CMD="xvfb-run appium --log-no-colors --log-timestamp -pa /wd/hub --port $APPIUM_PORT --log $TASK_LOG --log-level $LOG_LEVEL $APPIUM_CLI $plugins_cli" +CMD="xvfb-run appium --log-no-colors --log-timestamp -pa /wd/hub --port ${APPIUM_PORT} --log $TASK_LOG --log-level $LOG_LEVEL $APPIUM_CLI $plugins_cli" #--use-plugins=relaxed-caps share() { @@ -341,7 +341,7 @@ if [ "$CONNECT_TO_GRID" = true ]; then else /root/generate_config.sh $NODE_CONFIG_JSON fi - CMD+=" --nodeconfig $NODE_CONFIG_JSON" +# CMD+=" --nodeconfig $NODE_CONFIG_JSON" fi if [ "$DEFAULT_CAPABILITIES" = true ]; then @@ -365,8 +365,10 @@ rm -rf /tmp/.X99-lock touch ${TASK_LOG} echo $CMD -$CMD & +$CMD & +java ${JAVA_OPTS} -cp /opt/mcloud-node-1.0.jar:/opt/mcloud-node.jar -javaagent:/opt/mcloud-node-agent.jar org.openqa.selenium.grid.Bootstrap node \ + --config $NODE_CONFIG_JSON & trap 'finish' SIGTERM # start in background video artifacts capturing diff --git a/files/zbr-config-gen.sh b/files/zbr-config-gen.sh index 3a91e24..e362592 100755 --- a/files/zbr-config-gen.sh +++ b/files/zbr-config-gen.sh @@ -1,47 +1,55 @@ #!/bin/bash -#IMPORTANT!!! Don't do any echo otherwise you corrupt generated nodeconfig.json +#IMPORTANT!!! Don't do any echo otherwise you corrupt generated nodeconfig.toml # convert to lower case using Linux/Mac compatible syntax (bash v3.2) PLATFORM_NAME=`echo "$PLATFORM_NAME" | tr '[:upper:]' '[:lower:]'` cat << EndOfMessage -{ - "capabilities": - [ - { - "maxInstances": 1, - "deviceName": "${DEVICE_NAME}", - "deviceType": "${DEVICETYPE}", - "platformName":"${PLATFORM_NAME}", - "platformVersion":"${PLATFORM_VERSION}", - "udid": "${DEVICE_UDID}", - "adb_port": ${ADB_PORT}, - "proxy_port": ${PROXY_PORT}, - "automationName": "${AUTOMATION_NAME}" - } - ], - "configuration": - { - "proxy": "com.zebrunner.mcloud.grid.MobileRemoteProxy", - "url":"http://${STF_PROVIDER_HOST}:${APPIUM_PORT}/wd/hub", - "host": "${STF_PROVIDER_HOST}", - "port": ${APPIUM_PORT}, - "hubHost": "${SELENIUM_HOST}", - "hubPort": ${SELENIUM_PORT}, - "maxSession": 1, - "register": true, - "registerCycle": 300000, - "cleanUpCycle": 5000, - "timeout": 180, - "browserTimeout": 0, - "nodeStatusCheckTimeout": 5000, - "nodePolling": 5000, - "role": "node", - "unregisterIfStillDownAfter": ${UNREGISTER_IF_STILL_DOWN_AFTER}, - "downPollingLimit": 2, - "debug": false, - "servlets" : [], - "withoutServlets": [], - "custom": {} - } -} +[distributor] +slot-matcher = "com.zebrunner.mcloud.grid.MobileCapabilityMatcher" + +[node] +# Autodetect which drivers are available on the current system, and add them to the Node. +detect-drivers = false + +# Maximum number of concurrent sessions. Default value is the number of available processors. +max-sessions = 1 + +# Full classname of non-default Node implementation. This is used to manage a session’s lifecycle. +implementation = "com.zebrunner.mcloud.grid.MobileRemoteProxy" + +# The address of the Hub in a Hub-and-Node configuration. +hub = "http://${SELENIUM_HOST}:${SELENIUM_PORT}" + +# How often, in seconds, the Node will try to register itself for the first time to the Distributor. +register-cycle = 300 + +# How long, in seconds, will the Node try to register to the Distributor for the first time. +# After this period is completed, the Node will not attempt to register again. +register-period = 1000 + +# How often, in seconds, will the Node send heartbeat events to the Distributor to inform it that the Node is up. +heartbeat-period = 5 + +# Let X be the session-timeout in seconds. +# The Node will automatically kill a session that has not had any activity in the last X seconds. +# This will release the slot for other tests. +session-timeout = $GRID_BROWSER_TIMEOUT + +[relay] +# URL for connecting to the service that supports WebDriver commands like an Appium server or a cloud service. +url = "http://localhost:${APPIUM_PORT}/wd/hub" + +# Optional, endpoint to query the WebDriver service status, an HTTP 200 response is expected +status-endpoint = "/status" + +# Stereotypes supported by the service. The initial number is "max-sessions", and will allocate +# that many test slots to that particular configuration +configs = [ + "1", "{\"platformName\": \"${PLATFORM_NAME}\", \"appium:platformVersion\": \"${PLATFORM_VERSION}\", \"appium:deviceName\": \"${DEVICE_NAME}\", \"appium:automationName\": \"${AUTOMATION_NAME}\", \"zebrunner:deviceType\": \"${DEVICETYPE}\", \"appium:udid\": \"${DEVICE_UDID}\", \"zebrunner:adb_port\": \"${ADB_PORT}\", \"zebrunner:proxy_port\": \"${PROXY_PORT}\" }" +] + +[logging] +# Log level. Default logging level is INFO. Log levels are described here +# https://docs.oracle.com/javase/7/docs/api/java/util/logging/Level.html +log-level = "INFO" EndOfMessage diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..1906c95 --- /dev/null +++ b/pom.xml @@ -0,0 +1,154 @@ + + + 4.0.0 + com.zebrunner + mcloud-node + 1.0 + jar + Zebrunner Device Farm (Selenium Grid Node) + + UTF-8 + 4.17.0 + 1.2 + 3.14.0 + 2.16.0 + 5.2.2 + 1.6 + 1.19.4 + 1.18.30 + 3.5.0 + 3.6.1 + 3.11.0 + 3.3.0 + 3.6.2 + + + + org.seleniumhq.selenium + selenium-grid + ${selenium.version} + + + org.seleniumhq.selenium + selenium-api + ${selenium.version} + + + org.projectlombok + lombok + ${lombok.version} + provided + + + org.apache.commons + commons-lang3 + ${commons-lang3.version} + + + com.fasterxml.jackson.core + jackson-databind + ${jackson-databind.version} + + + org.apache.httpcomponents.client5 + httpclient5 + ${httpclient.version} + + + com.owlike + genson + ${genson.version} + + + com.sun.jersey + jersey-bundle + ${jersey-bundle.version} + + + jakarta.annotation + jakarta.annotation-api + 3.0.0-M1 + + + + + + org.apache.maven.plugins + maven-dependency-plugin + ${maven-dependency-plugin.version} + + + unpack-dependencies + package + + unpack-dependencies + + + system + META-INF/*.SF,META-INF/*.DSA,META-INF/*.RSA + junit,org.mockito,org.hamcrest + ${project.build.directory}/classes + + + + + + org.apache.maven.plugins + maven-shade-plugin + ${maven-shade-plugin.version} + + + package + + shade + + + false + + + *:* + + META-INF/*.SF + META-INF/*.DSA + META-INF/*.RSA + + + + + + + + mcloud-node + + + + org.apache.maven.plugins + maven-compiler-plugin + ${maven-compiler-plugin.version} + + 11 + + + + org.apache.maven.plugins + maven-source-plugin + ${maven-source-plugin.version} + + + attach-sources + + jar + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + ${maven-javadoc-plugin.version} + + + + diff --git a/src/main/java/com/zebrunner/mcloud/grid/LogsFilter.java b/src/main/java/com/zebrunner/mcloud/grid/LogsFilter.java new file mode 100644 index 0000000..7db41bc --- /dev/null +++ b/src/main/java/com/zebrunner/mcloud/grid/LogsFilter.java @@ -0,0 +1,14 @@ +package com.zebrunner.mcloud.grid; + +import org.apache.commons.lang3.StringUtils; + +import java.util.logging.Filter; +import java.util.logging.LogRecord; + +public class LogsFilter implements Filter { + + public boolean isLoggable(LogRecord record) { + // do not log this exception + return !StringUtils.containsIgnoreCase(record.getMessage(), "timed out waiting for a node to become available"); + } +} diff --git a/src/main/java/com/zebrunner/mcloud/grid/MobileCapabilityMatcher.java b/src/main/java/com/zebrunner/mcloud/grid/MobileCapabilityMatcher.java new file mode 100644 index 0000000..c0a2181 --- /dev/null +++ b/src/main/java/com/zebrunner/mcloud/grid/MobileCapabilityMatcher.java @@ -0,0 +1,82 @@ +/******************************************************************************* + * Copyright 2018-2021 Zebrunner (https://zebrunner.com/). + * + * Licensed 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 com.zebrunner.mcloud.grid; + +import com.zebrunner.mcloud.grid.validator.DeviceNameValidator; +import com.zebrunner.mcloud.grid.validator.DeviceTypeValidator; +import com.zebrunner.mcloud.grid.validator.MobilePlatformValidator; +import com.zebrunner.mcloud.grid.validator.PlatformVersionValidator; +import com.zebrunner.mcloud.grid.validator.UDIDValidator; +import com.zebrunner.mcloud.grid.validator.Validator; +import org.openqa.selenium.Capabilities; +import org.openqa.selenium.grid.data.DefaultSlotMatcher; +import org.openqa.selenium.remote.CapabilityType; + +import java.util.List; +import java.util.logging.Logger; + +import static com.zebrunner.mcloud.grid.utils.CapabilityUtils.getAppiumCapability; + +@SuppressWarnings("unused") +public final class MobileCapabilityMatcher extends DefaultSlotMatcher { + private static final Logger LOGGER = Logger.getLogger(MobileCapabilityMatcher.class.getName()); + private final List validators = List.of( + new MobilePlatformValidator(), + new PlatformVersionValidator(), + new DeviceNameValidator(), + new DeviceTypeValidator(), + new UDIDValidator()); + + @Override + public boolean matches(Capabilities stereotype, Capabilities capabilities) { + LOGGER.info(() -> "Requested capabilities: " + capabilities); + LOGGER.info(() -> "Stereotype capabilities: " + stereotype); + if (capabilities.getCapability(CapabilityType.PLATFORM_NAME) != null || + getAppiumCapability(capabilities, "platformVersion", Object.class) != null || + getAppiumCapability(capabilities, "deviceName", Object.class) != null || + getAppiumCapability(capabilities, "udid", Object.class) != null) { + // Mobile-based capabilities + LOGGER.info("Using extensionCapabilityCheck matcher."); + return extensionCapabilityCheck(stereotype, capabilities); + } else { + // Browser-based capabilities + LOGGER.info("Using default browser-based capabilities matcher."); + return super.matches(stereotype, capabilities); + } + } + + /** + * Verifies matching between requested and actual node capabilities. + * + * @param stereotype node capabilities + * @param capabilities capabilities requested by client + * @return match results + */ + private boolean extensionCapabilityCheck(Capabilities stereotype, Capabilities capabilities) { + if (stereotype == null) { + LOGGER.info("stereotype - NULL"); + } + if (capabilities == null) { + LOGGER.info("capabilities - NULL"); + } + boolean matches = stereotype != null && + capabilities != null && + validators.stream() + .allMatch(v -> v.apply(stereotype, capabilities)); + LOGGER.info(() -> "[MATCHES]" + matches); + return matches; + } +} diff --git a/src/main/java/com/zebrunner/mcloud/grid/MobileRemoteProxy.java b/src/main/java/com/zebrunner/mcloud/grid/MobileRemoteProxy.java new file mode 100644 index 0000000..0d98cec --- /dev/null +++ b/src/main/java/com/zebrunner/mcloud/grid/MobileRemoteProxy.java @@ -0,0 +1,174 @@ +/******************************************************************************* + * Copyright 2018-2021 Zebrunner (https://zebrunner.com/). + * + * Licensed 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 com.zebrunner.mcloud.grid; + +import java.io.IOException; +import java.net.URI; +import java.util.UUID; +import java.util.function.Supplier; + +import org.apache.commons.lang3.exception.ExceptionUtils; +import org.apache.commons.lang3.reflect.FieldUtils; +import org.openqa.selenium.Capabilities; +import org.openqa.selenium.NoSuchSessionException; +import org.openqa.selenium.WebDriverException; +import org.openqa.selenium.grid.config.Config; +import org.openqa.selenium.grid.data.CreateSessionRequest; +import org.openqa.selenium.grid.data.CreateSessionResponse; +import org.openqa.selenium.grid.data.NodeId; +import org.openqa.selenium.grid.data.NodeStatus; +import org.openqa.selenium.grid.data.Session; +import org.openqa.selenium.grid.log.LoggingOptions; +import org.openqa.selenium.grid.node.HealthCheck; +import org.openqa.selenium.grid.node.Node; +import org.openqa.selenium.grid.node.config.NodeOptions; +import org.openqa.selenium.grid.node.local.LocalNodeFactory; +import org.openqa.selenium.grid.security.Secret; +import org.openqa.selenium.grid.security.SecretOptions; +import org.openqa.selenium.grid.server.BaseServerOptions; +import org.openqa.selenium.internal.Either; +import org.openqa.selenium.io.TemporaryFilesystem; +import org.openqa.selenium.remote.DesiredCapabilities; +import org.openqa.selenium.remote.SessionId; +import org.openqa.selenium.remote.http.HttpRequest; +import org.openqa.selenium.remote.http.HttpResponse; +import org.openqa.selenium.remote.tracing.Tracer; + +public class MobileRemoteProxy extends Node { + + private Node node; + + protected MobileRemoteProxy(Tracer tracer, URI uri, Secret registrationSecret) { + super(tracer, new NodeId(UUID.randomUUID()), uri, registrationSecret); + } + + public static Node create(Config config) { + LoggingOptions loggingOptions = new LoggingOptions(config); + BaseServerOptions serverOptions = new BaseServerOptions(config); + URI uri = serverOptions.getExternalUri(); + SecretOptions secretOptions = new SecretOptions(config); + + // Refer to the foot notes for additional context on this line. + Node node = LocalNodeFactory.create(config); + + MobileRemoteProxy wrapper = new MobileRemoteProxy(loggingOptions.getTracer(), + uri, secretOptions.getRegistrationSecret()); + wrapper.node = node; + return wrapper; + } + + @Override + public Either newSession( + CreateSessionRequest sessionRequest) { + return perform(() -> node.newSession(sessionRequest), "newSession"); + } + + @Override + public HttpResponse executeWebDriverCommand(HttpRequest req) { + return perform(() -> node.executeWebDriverCommand(req), "executeWebDriverCommand"); + } + + @Override + public Session getSession(SessionId id) throws NoSuchSessionException { + return perform(() -> node.getSession(id), "getSession"); + } + + @Override + public HttpResponse uploadFile(HttpRequest req, SessionId id) { + return perform(() -> node.uploadFile(req, id), "uploadFile"); + } + + @Override + public HttpResponse downloadFile(HttpRequest req, SessionId id) { + return perform(() -> node.downloadFile(req, id), "downloadFile"); + } + + @Override + public TemporaryFilesystem getDownloadsFilesystem(UUID uuid) { + return perform(() -> { + try { + return node.getDownloadsFilesystem(uuid); + } catch (IOException e) { + throw new RuntimeException(e); + } + }, "downloadsFilesystem"); + } + + @Override + public TemporaryFilesystem getUploadsFilesystem(SessionId id) throws IOException { + return perform(() -> { + try { + return node.getUploadsFilesystem(id); + } catch (IOException e) { + throw new RuntimeException(e); + } + }, "uploadsFilesystem"); + + } + + @Override + public void stop(SessionId id) throws NoSuchSessionException { + perform(() -> node.stop(id), "stop"); + } + + @Override + public boolean isSessionOwner(SessionId id) { + return perform(() -> node.isSessionOwner(id), "isSessionOwner"); + } + + @Override + public boolean isSupporting(Capabilities capabilities) { + return perform(() -> node.isSupporting(capabilities), "isSupporting"); + } + + @Override + public NodeStatus getStatus() { + return perform(() -> node.getStatus(), "getStatus"); + } + + @Override + public HealthCheck getHealthCheck() { + return perform(() -> node.getHealthCheck(), "getHealthCheck"); + } + + @Override + public void drain() { + perform(() -> node.drain(), "drain"); + } + + @Override + public boolean isReady() { + return perform(() -> node.isReady(), "isReady"); + } + + private void perform(Runnable function, String operation) { + try { + System.err.printf("[COMMENTATOR] Before %s()%n", operation); + function.run(); + } finally { + System.err.printf("[COMMENTATOR] After %s()%n", operation); + } + } + + private T perform(Supplier function, String operation) { + try { + System.err.printf("[COMMENTATOR] Before %s()%n", operation); + return function.get(); + } finally { + System.err.printf("[COMMENTATOR] After %s()%n", operation); + } + } +} diff --git a/src/main/java/com/zebrunner/mcloud/grid/Platform.java b/src/main/java/com/zebrunner/mcloud/grid/Platform.java new file mode 100644 index 0000000..59e75cf --- /dev/null +++ b/src/main/java/com/zebrunner/mcloud/grid/Platform.java @@ -0,0 +1,49 @@ +/******************************************************************************* + * Copyright 2018-2021 Zebrunner (https://zebrunner.com/). + * + * Licensed 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 com.zebrunner.mcloud.grid; + +import org.openqa.selenium.Capabilities; + +/** + * Platforms available in Selenium Grid. + * + * @author Alex Khursevich (alex@qaprosoft.com) + */ +public enum Platform { + ANY, + ANDROID, + IOS, + WINDOWS, + MAC, + TVOS, + LINUX; + + /** + * Retrieves platform type from capabilities. + * + * @param cap - desired capabilities + * @return platform + */ + public static Platform fromCapabilities(Capabilities cap) { + Platform platform = Platform.ANY; + + if (cap != null && cap.getCapability("platformName") != null) { + platform = Platform.valueOf(cap.getCapability("platformName").toString().toUpperCase()); + } + + return platform; + } +} diff --git a/src/main/java/com/zebrunner/mcloud/grid/integration/client/Path.java b/src/main/java/com/zebrunner/mcloud/grid/integration/client/Path.java new file mode 100644 index 0000000..60ea490 --- /dev/null +++ b/src/main/java/com/zebrunner/mcloud/grid/integration/client/Path.java @@ -0,0 +1,49 @@ +/******************************************************************************* + * Copyright 2018-2021 Zebrunner (https://zebrunner.com/). + * + * Licensed 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 com.zebrunner.mcloud.grid.integration.client; + +public enum Path { + + EMPTY(""), + STF_USER_PATH("/api/v1/user"), + STF_DEVICES_PATH("/api/v1/devices"), + STF_DEVICES_ITEM_PATH("/api/v1/devices/%s"), + STF_USER_DEVICES_PATH("/api/v1/user/devices"), + STF_USER_DEVICES_BY_ID_PATH("/api/v1/user/devices/%s"), + STF_USER_DEVICES_REMOTE_CONNECT_PATH("/api/v1/user/devices/%s/remoteConnect"), + APPIUM_START_RECORDING_SCREEN_PATH("/session/%s/appium/start_recording_screen"), + APPIUM_STOP_RECORDING_SCREEN_PATH("/session/%s/appium/stop_recording_screen"), + APPIUM_GET_LOG_TYPES_PATH("/session/%s/log/types"), + APPIUM_GET_LOGS_PATH("/session/%s/log"), + APPIUM_STATUS("/status"), + APPIUM_STATUS_WDA("/status-wda"), + APPIUM_STATUS_ADB("/status-adb"); + + private final String relativePath; + + Path(String relativePath) { + this.relativePath = relativePath; + } + + public String getRelativePath() { + return relativePath; + } + + public String build(String serviceUrl, Object... parameters) { + return serviceUrl + String.format(relativePath, parameters); + } + +} diff --git a/src/main/java/com/zebrunner/mcloud/grid/integration/client/STFClient.java b/src/main/java/com/zebrunner/mcloud/grid/integration/client/STFClient.java new file mode 100644 index 0000000..9ae808b --- /dev/null +++ b/src/main/java/com/zebrunner/mcloud/grid/integration/client/STFClient.java @@ -0,0 +1,291 @@ +/******************************************************************************* + * Copyright 2018-2021 Zebrunner (https://zebrunner.com/). + * + * Licensed 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 com.zebrunner.mcloud.grid.integration.client; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; +import java.util.logging.Logger; + +import com.zebrunner.mcloud.grid.util.CapabilityUtils; +import org.apache.commons.lang3.StringUtils; + +import com.zebrunner.mcloud.grid.Platform; +import com.zebrunner.mcloud.grid.models.stf.Devices; +import com.zebrunner.mcloud.grid.models.stf.RemoteConnectUserDevice; +import com.zebrunner.mcloud.grid.models.stf.STFDevice; +import com.zebrunner.mcloud.grid.models.stf.User; +import com.zebrunner.mcloud.grid.util.HttpClient; +import org.openqa.selenium.Capabilities; + +@SuppressWarnings("rawtypes") +public final class STFClient { + private static final Logger LOGGER = Logger.getLogger(STFClient.class.getName()); + private static final Map STF_CLIENTS = new ConcurrentHashMap<>(); + private static final String STF_URL = System.getenv("STF_URL"); + private static final String DEFAULT_STF_TOKEN = System.getenv("STF_TOKEN"); + // Max time is seconds for reserving devices in STF + private static final String DEFAULT_STF_TIMEOUT = System.getenv("STF_TIMEOUT"); + + private Platform platform; + private String token; + private boolean isReservedManually = false; + private STFDevice device; + private String sessionUUID; + + private STFClient() { + //do nothing + } + + /** + * Get STF device + * + * @param udid udid of the device + * @return {@link STFDevice} if STF client registered, null otherwise + */ + public static STFDevice getSTFDevice(String udid) { + if (STF_CLIENTS.get(udid) != null) { + return STF_CLIENTS.get(udid).getDevice(); + } + return null; + } + + /** + * Reserve STF device + */ + public static boolean reserveSTFDevice(String deviceUDID, Capabilities requestedCapabilities, String sessionUUID) { + LOGGER.info(() -> String.format("[STF-%s] Reserve STF Device.", sessionUUID)); + if (!isSTFEnabled()) { + return true; + } + if (STF_CLIENTS.get(deviceUDID) != null) { + LOGGER.warning(() -> String.format("Device '%s' already busy (in the local pool). Info: %s", deviceUDID, STF_CLIENTS.get(deviceUDID))); + } + + STFClient stfClient = new STFClient(); + String stfToken = CapabilityUtils.getZebrunnerCapability(requestedCapabilities, "STF_TOKEN") + .map(String::valueOf) + .orElse(DEFAULT_STF_TOKEN); + Integer stfTimeout = CapabilityUtils.getZebrunnerCapability(requestedCapabilities, "STF_TIMEOUT") + .map(String::valueOf) + .map(Integer::parseInt) + .orElse(Integer.parseInt(DEFAULT_STF_TIMEOUT)); + + stfClient.setToken(stfToken); + + HttpClient.Response user = HttpClient.uri(Path.STF_USER_PATH, STF_URL) + .withAuthorization(buildAuthToken(stfToken)) + .get(User.class); + + if (user.getStatus() != 200) { + LOGGER.warning(() -> + String.format("[STF-%s] Not authenticated at STF successfully! URL: '%s'; Token: '%s';", sessionUUID, STF_URL, stfToken)); + return false; + } + + HttpClient.Response devices = HttpClient.uri(Path.STF_DEVICES_PATH, STF_URL) + .withAuthorization(buildAuthToken(stfToken)) + .get(Devices.class); + + if (devices.getStatus() != 200) { + LOGGER.warning(() -> String.format("[STF-%s] Unable to get devices status. HTTP status: %s", sessionUUID, devices.getStatus())); + return false; + } + + Optional optionalSTFDevice = devices.getObject().getDevices() + .stream().filter(device -> StringUtils.equals(device.getSerial(), deviceUDID)) + .findFirst(); + + if (optionalSTFDevice.isEmpty()) { + LOGGER.warning(() -> String.format("[STF-%s] Could not find STF device with udid: %s", sessionUUID, deviceUDID)); + return false; + } + + STFDevice stfDevice = optionalSTFDevice.get(); + LOGGER.info(() -> String.format("[STF-%s] STF Device info: %s", sessionUUID, stfDevice)); + + stfClient.setDevice(stfDevice); + + if (Platform.ANDROID.equals(Platform.fromCapabilities(requestedCapabilities)) && + CapabilityUtils.getZebrunnerCapability(requestedCapabilities, "enableAdb") + .map(String::valueOf) + .map(Boolean::parseBoolean) + .orElse(false)) { + if (StringUtils.isBlank((String) stfDevice.getRemoteConnectUrl())) { + LOGGER.warning(() -> String.format("[STF-%s] Detected 'true' enableAdb capability, but remoteURL is blank or empty.", sessionUUID)); + return false; + } else { + LOGGER.info(() -> String.format("[STF-%s] Detected 'true' enableAdb capability, and remoteURL is present.", sessionUUID)); + } + } + + if (stfDevice.getOwner() != null && StringUtils.equals(stfDevice.getOwner().getName(), user.getObject().getUser().getName()) && + stfDevice.getPresent() && + stfDevice.getReady()) { + LOGGER.info(() -> String.format("[STF-%s] Device [%s] already reserved manually by the same user: %s.", + sessionUUID, deviceUDID, stfDevice.getOwner().getName())); + stfClient.reservedManually(true); + } else if (stfDevice.getOwner() == null && stfDevice.getPresent() && stfDevice.getReady()) { + + Map entity = new HashMap<>(); + entity.put("serial", deviceUDID); + entity.put("timeout", TimeUnit.SECONDS.toMillis(stfTimeout)); + HttpClient.Response response = HttpClient.uri(Path.STF_USER_DEVICES_PATH, STF_URL) + .withAuthorization(buildAuthToken(stfToken)) + .post(Void.class, entity); + if (response.getStatus() != 200) { + LOGGER.warning(() -> String.format("[STF-%s] Could not reserve STF device with udid: %s. Status: %s. Response: %s", + sessionUUID, deviceUDID, response.getStatus(), response.getObject())); + return false; + } + + if (Platform.ANDROID.equals(Platform.fromCapabilities(requestedCapabilities))) { + LOGGER.info( + () -> String.format("[STF-%s] Additionally call 'remoteConnect'.", sessionUUID)); + + HttpClient.Response remoteConnectUserDevice = HttpClient.uri(Path.STF_USER_DEVICES_REMOTE_CONNECT_PATH, + STF_URL, deviceUDID) + .withAuthorization(buildAuthToken(stfToken)) + .post(RemoteConnectUserDevice.class, null); + + if (remoteConnectUserDevice.getStatus() != 200) { + LOGGER.warning( + () -> String.format("[STF-%s] Unsuccessful remoteConnect. Status: %s. Response: %s", + sessionUUID, remoteConnectUserDevice.getStatus(), remoteConnectUserDevice.getObject())); + + if (HttpClient.uri(Path.STF_USER_DEVICES_BY_ID_PATH, STF_URL, deviceUDID) + .withAuthorization(buildAuthToken(stfToken)) + .delete(Void.class).getStatus() != 200) { + LOGGER.warning(() -> String.format("[STF-%s] Could not return device to the STF after unsuccessful Android remoteConnect.", + sessionUUID)); + } + return false; + } + } + } else { + return false; + } + + stfClient.setPlatform(Platform.fromCapabilities(requestedCapabilities)); + stfClient.setSTFSessionUUID(sessionUUID); + LOGGER.info( + () -> String.format("[STF-%s] Device '%s' successfully reserved.", sessionUUID, stfDevice.getSerial())); + STF_CLIENTS.put(deviceUDID, stfClient); + return true; + } + + public static void disconnectSTFDevice(String udid) { + + STFClient client = STF_CLIENTS.get(udid); + if (client == null) { + return; + } + String sessionUUID = client.getSTFSessionUUID(); + try { + if (client.reservedManually()) { + LOGGER.info(() -> String.format("[STF-%s] Device '%s' will not be returned as it was reserved manually.", + sessionUUID, client.getDevice().getSerial())); + return; + } + LOGGER.info(() -> String.format("[STF-%s] Return STF Device.", sessionUUID)); + + // it seems like return and remote disconnect guarantee that device becomes free asap + if (Platform.ANDROID.equals(client.getPlatform())) { + LOGGER.info(() -> String.format("[STF-%s] Additionally disconnect 'remoteConnect'.", sessionUUID)); + HttpClient.Response response = HttpClient.uri(Path.STF_USER_DEVICES_REMOTE_CONNECT_PATH, STF_URL, udid) + .withAuthorization(buildAuthToken(client.getToken())) + .delete(Void.class); + if (response.getStatus() != 200) { + LOGGER.warning(() -> String.format("[STF-%s] Could not disconnect 'remoteConnect'.", sessionUUID)); + } + } + + HttpClient.Response response = HttpClient.uri(Path.STF_USER_DEVICES_BY_ID_PATH, STF_URL, udid) + .withAuthorization(buildAuthToken(client.getToken())) + .delete(Void.class); + if (response.getStatus() != 200) { + LOGGER.warning(() -> String.format("[STF-%s] Could not return device to the STF. Status: %s", sessionUUID, response.getStatus())); + } + LOGGER.warning(() -> String.format("[STF-%s] Device '%s' successfully returned to the STF.", + sessionUUID, client.getDevice().getSerial())); + } catch (Exception e) { + LOGGER.warning(() -> String.format("[STF-%s] Error when return device to the STF: %s", sessionUUID, e)); + } finally { + STF_CLIENTS.remove(udid); + } + + } + + private static String buildAuthToken(String authToken) { + return "Bearer " + authToken; + } + + public STFDevice getDevice() { + return device; + } + + public void setDevice(STFDevice device) { + this.device = device; + } + + public Platform getPlatform() { + return platform; + } + + public void setPlatform(Platform platform) { + this.platform = platform; + } + + public String getToken() { + return token; + } + + public String getSTFSessionUUID() { + return sessionUUID; + } + + public void setSTFSessionUUID(String uuid) { + this.sessionUUID = uuid; + } + + public void setToken(String token) { + this.token = token; + } + + public boolean reservedManually() { + return isReservedManually; + } + + public void reservedManually(boolean owned) { + isReservedManually = owned; + } + + private static boolean isSTFEnabled() { + return (!StringUtils.isEmpty(STF_URL) && !StringUtils.isEmpty(DEFAULT_STF_TOKEN)); + } + + @Override public String toString() { + return "STFClient{" + + "platform=" + platform + + ", token='" + token + '\'' + + ", isReservedManually=" + isReservedManually + + ", device=" + device + + ", sessionUUID='" + sessionUUID + '\'' + + '}'; + } +} diff --git a/src/main/java/com/zebrunner/mcloud/grid/models/stf/App.java b/src/main/java/com/zebrunner/mcloud/grid/models/stf/App.java new file mode 100644 index 0000000..bb6a1db --- /dev/null +++ b/src/main/java/com/zebrunner/mcloud/grid/models/stf/App.java @@ -0,0 +1,57 @@ +/******************************************************************************* + * Copyright 2018-2021 Zebrunner (https://zebrunner.com/). + * + * Licensed 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 com.zebrunner.mcloud.grid.models.stf; + +import com.fasterxml.jackson.annotation.JsonAnyGetter; +import com.fasterxml.jackson.annotation.JsonAnySetter; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.util.HashMap; +import java.util.Map; + +@Getter +@Setter +@NoArgsConstructor +@JsonInclude(JsonInclude.Include.NON_NULL) +public class App { + + private String developer; + private String id; + private String name; + private Boolean selected; + private Boolean system; + + private String type; + @JsonIgnore + private Map additionalProperties = new HashMap<>(); + + + @JsonAnyGetter + public Map getAdditionalProperties() { + return this.additionalProperties; + } + + @JsonAnySetter + public void setAdditionalProperty(String name, Object value) { + this.additionalProperties.put(name, value); + } + +} diff --git a/src/main/java/com/zebrunner/mcloud/grid/models/stf/Battery.java b/src/main/java/com/zebrunner/mcloud/grid/models/stf/Battery.java new file mode 100644 index 0000000..83ebd59 --- /dev/null +++ b/src/main/java/com/zebrunner/mcloud/grid/models/stf/Battery.java @@ -0,0 +1,57 @@ +/******************************************************************************* + * Copyright 2018-2021 Zebrunner (https://zebrunner.com/). + * + * Licensed 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 com.zebrunner.mcloud.grid.models.stf; + +import java.util.HashMap; +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonAnyGetter; +import com.fasterxml.jackson.annotation.JsonAnySetter; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@NoArgsConstructor +@JsonInclude(JsonInclude.Include.NON_NULL) +public class Battery { + + private String health; + private Double level; + private Double scale; + private String source; + private String status; + private String temp; + private Double voltage; + @JsonIgnore + private Map additionalProperties = new HashMap<>(); + + @JsonAnyGetter + public Map getAdditionalProperties() { + return this.additionalProperties; + } + + @JsonAnySetter + public void setAdditionalProperty(String name, Object value) { + this.additionalProperties.put(name, value); + } + +} diff --git a/src/main/java/com/zebrunner/mcloud/grid/models/stf/Browser.java b/src/main/java/com/zebrunner/mcloud/grid/models/stf/Browser.java new file mode 100644 index 0000000..66c9cf4 --- /dev/null +++ b/src/main/java/com/zebrunner/mcloud/grid/models/stf/Browser.java @@ -0,0 +1,54 @@ +/******************************************************************************* + * Copyright 2018-2021 Zebrunner (https://zebrunner.com/). + * + * Licensed 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 com.zebrunner.mcloud.grid.models.stf; + +import com.fasterxml.jackson.annotation.JsonAnyGetter; +import com.fasterxml.jackson.annotation.JsonAnySetter; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Getter +@Setter +@NoArgsConstructor +@JsonInclude(JsonInclude.Include.NON_NULL) +public class Browser { + + private List apps = new ArrayList(); + private Boolean selected; + + @JsonIgnore + private Map additionalProperties = new HashMap(); + + @JsonAnyGetter + public Map getAdditionalProperties() { + return this.additionalProperties; + } + + @JsonAnySetter + public void setAdditionalProperty(String name, Object value) { + this.additionalProperties.put(name, value); + } + +} diff --git a/src/main/java/com/zebrunner/mcloud/grid/models/stf/Device.java b/src/main/java/com/zebrunner/mcloud/grid/models/stf/Device.java new file mode 100644 index 0000000..98db35a --- /dev/null +++ b/src/main/java/com/zebrunner/mcloud/grid/models/stf/Device.java @@ -0,0 +1,52 @@ +/******************************************************************************* + * Copyright 2018-2021 Zebrunner (https://zebrunner.com/). + * + * Licensed 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 com.zebrunner.mcloud.grid.models.stf; + +import java.util.HashMap; +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonAnyGetter; +import com.fasterxml.jackson.annotation.JsonAnySetter; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@NoArgsConstructor +@JsonInclude(JsonInclude.Include.NON_NULL) +public class Device { + + private STFDevice device; + private Boolean success; + @JsonIgnore + private Map additionalProperties = new HashMap(); + + @JsonAnyGetter + public Map getAdditionalProperties() { + return this.additionalProperties; + } + + @JsonAnySetter + public void setAdditionalProperty(String name, Object value) { + this.additionalProperties.put(name, value); + } + +} diff --git a/src/main/java/com/zebrunner/mcloud/grid/models/stf/Devices.java b/src/main/java/com/zebrunner/mcloud/grid/models/stf/Devices.java new file mode 100644 index 0000000..49645f4 --- /dev/null +++ b/src/main/java/com/zebrunner/mcloud/grid/models/stf/Devices.java @@ -0,0 +1,53 @@ +/******************************************************************************* + * Copyright 2018-2021 Zebrunner (https://zebrunner.com/). + * + * Licensed 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 com.zebrunner.mcloud.grid.models.stf; + +import com.fasterxml.jackson.annotation.JsonAnyGetter; +import com.fasterxml.jackson.annotation.JsonAnySetter; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Getter +@Setter +@NoArgsConstructor +@JsonInclude(JsonInclude.Include.NON_NULL) +public class Devices { + + private List devices = new ArrayList(); + private Boolean success; + @JsonIgnore + private Map additionalProperties = new HashMap(); + + @JsonAnyGetter + public Map getAdditionalProperties() { + return this.additionalProperties; + } + + @JsonAnySetter + public void setAdditionalProperty(String name, Object value) { + this.additionalProperties.put(name, value); + } + +} diff --git a/src/main/java/com/zebrunner/mcloud/grid/models/stf/Display.java b/src/main/java/com/zebrunner/mcloud/grid/models/stf/Display.java new file mode 100644 index 0000000..2611f7f --- /dev/null +++ b/src/main/java/com/zebrunner/mcloud/grid/models/stf/Display.java @@ -0,0 +1,60 @@ +/******************************************************************************* + * Copyright 2018-2021 Zebrunner (https://zebrunner.com/). + * + * Licensed 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 com.zebrunner.mcloud.grid.models.stf; + +import com.fasterxml.jackson.annotation.JsonAnyGetter; +import com.fasterxml.jackson.annotation.JsonAnySetter; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.util.HashMap; +import java.util.Map; + +@Getter +@Setter +@NoArgsConstructor +@JsonInclude(JsonInclude.Include.NON_NULL) +public class Display { + + private Double density; + private Double fps; + private Double height; + private Double id; + private Double rotation; + private Boolean secure; + private Double size; + private String url; + private Double width; + private Double xdpi; + private Double ydpi; + @JsonIgnore + private Map additionalProperties = new HashMap(); + + @JsonAnyGetter + public Map getAdditionalProperties() { + return this.additionalProperties; + } + + @JsonAnySetter + public void setAdditionalProperty(String name, Object value) { + this.additionalProperties.put(name, value); + } + +} diff --git a/src/main/java/com/zebrunner/mcloud/grid/models/stf/Network.java b/src/main/java/com/zebrunner/mcloud/grid/models/stf/Network.java new file mode 100644 index 0000000..54a4509 --- /dev/null +++ b/src/main/java/com/zebrunner/mcloud/grid/models/stf/Network.java @@ -0,0 +1,56 @@ +/******************************************************************************* + * Copyright 2018-2021 Zebrunner (https://zebrunner.com/). + * + * Licensed 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 com.zebrunner.mcloud.grid.models.stf; + +import com.fasterxml.jackson.annotation.JsonAnyGetter; +import com.fasterxml.jackson.annotation.JsonAnySetter; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.util.HashMap; +import java.util.Map; + +@Getter +@Setter +@NoArgsConstructor +@JsonInclude(JsonInclude.Include.NON_NULL) +public class Network { + + private Boolean connected; + private Boolean failover; + private Boolean manual; + private String operator; + private Boolean roaming; + private String state; + private String subtype; + private String type; + @JsonIgnore + private Map additionalProperties = new HashMap<>(); + + @JsonAnyGetter + public Map getAdditionalProperties() { + return this.additionalProperties; + } + + @JsonAnySetter + public void setAdditionalProperty(String name, Object value) { + this.additionalProperties.put(name, value); + } + +} diff --git a/src/main/java/com/zebrunner/mcloud/grid/models/stf/Phone.java b/src/main/java/com/zebrunner/mcloud/grid/models/stf/Phone.java new file mode 100644 index 0000000..f9dfa4d --- /dev/null +++ b/src/main/java/com/zebrunner/mcloud/grid/models/stf/Phone.java @@ -0,0 +1,54 @@ +/******************************************************************************* + * Copyright 2018-2021 Zebrunner (https://zebrunner.com/). + * + * Licensed 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 com.zebrunner.mcloud.grid.models.stf; + +import com.fasterxml.jackson.annotation.JsonAnyGetter; +import com.fasterxml.jackson.annotation.JsonAnySetter; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.util.HashMap; +import java.util.Map; + +@Getter +@Setter +@NoArgsConstructor +@JsonInclude(JsonInclude.Include.NON_NULL) +public class Phone { + + private Object iccid; + private String imei; + private String network; + private Object phoneNumber; + @JsonIgnore + private Map additionalProperties = new HashMap<>(); + + + @JsonAnyGetter + public Map getAdditionalProperties() { + return this.additionalProperties; + } + + @JsonAnySetter + public void setAdditionalProperty(String name, Object value) { + this.additionalProperties.put(name, value); + } + +} diff --git a/src/main/java/com/zebrunner/mcloud/grid/models/stf/Provider.java b/src/main/java/com/zebrunner/mcloud/grid/models/stf/Provider.java new file mode 100644 index 0000000..161e1df --- /dev/null +++ b/src/main/java/com/zebrunner/mcloud/grid/models/stf/Provider.java @@ -0,0 +1,51 @@ +/******************************************************************************* + * Copyright 2018-2021 Zebrunner (https://zebrunner.com/). + * + * Licensed 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 com.zebrunner.mcloud.grid.models.stf; + +import com.fasterxml.jackson.annotation.JsonAnyGetter; +import com.fasterxml.jackson.annotation.JsonAnySetter; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.util.HashMap; +import java.util.Map; + +@Getter +@Setter +@NoArgsConstructor +@JsonInclude(JsonInclude.Include.NON_NULL) +public class Provider { + + private String channel; + private String name; + @JsonIgnore + private Map additionalProperties = new HashMap(); + + @JsonAnyGetter + public Map getAdditionalProperties() { + return this.additionalProperties; + } + + @JsonAnySetter + public void setAdditionalProperty(String name, Object value) { + this.additionalProperties.put(name, value); + } + +} diff --git a/src/main/java/com/zebrunner/mcloud/grid/models/stf/RemoteConnectUserDevice.java b/src/main/java/com/zebrunner/mcloud/grid/models/stf/RemoteConnectUserDevice.java new file mode 100644 index 0000000..d5fd173 --- /dev/null +++ b/src/main/java/com/zebrunner/mcloud/grid/models/stf/RemoteConnectUserDevice.java @@ -0,0 +1,30 @@ +/******************************************************************************* + * Copyright 2018-2021 Zebrunner (https://zebrunner.com/). + * + * Licensed 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 com.zebrunner.mcloud.grid.models.stf; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@NoArgsConstructor +public class RemoteConnectUserDevice { + + private String remoteConnectUrl; + private String serial; + +} diff --git a/src/main/java/com/zebrunner/mcloud/grid/models/stf/STFDevice.java b/src/main/java/com/zebrunner/mcloud/grid/models/stf/STFDevice.java new file mode 100644 index 0000000..1ade84d --- /dev/null +++ b/src/main/java/com/zebrunner/mcloud/grid/models/stf/STFDevice.java @@ -0,0 +1,114 @@ +/******************************************************************************* + * Copyright 2018-2021 Zebrunner (https://zebrunner.com/). + * + * Licensed 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 com.zebrunner.mcloud.grid.models.stf; + +import com.fasterxml.jackson.annotation.JsonAnyGetter; +import com.fasterxml.jackson.annotation.JsonAnySetter; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Getter +@Setter +@NoArgsConstructor +@JsonInclude(JsonInclude.Include.NON_NULL) +public class STFDevice { + + private String abi; + private Boolean airplaneMode; + private Battery battery; + private Browser browser; + private String channel; + private String createdAt; + private Display display; + private String manufacturer; + private String model; + private Network network; + private Object operator; + private STFUser owner; + private Phone phone; + private String platform; + private String presenceChangedAt; + private Boolean present; + private String product; + private Provider provider; + private Boolean ready; + private Object remoteConnectUrl; + private Boolean remoteConnect; + private List reverseForwards = new ArrayList<>(); + private String sdk; + private String serial; + private String statusChangedAt; + private Double status; + private Boolean using; + private String version; + private String deviceType = "Phone"; + @JsonIgnore + private Map additionalProperties = new HashMap<>(); + + @JsonAnyGetter + public Map getAdditionalProperties() { + return this.additionalProperties; + } + + @JsonAnySetter + public void setAdditionalProperty(String name, Object value) { + this.additionalProperties.put(name, value); + } + + @Override public String toString() { + return "STFDevice{" + + "abi='" + abi + '\'' + + ", airplaneMode=" + airplaneMode + + ", battery=" + battery + + ", browser=" + browser + + ", channel='" + channel + '\'' + + ", createdAt='" + createdAt + '\'' + + ", display=" + display + + ", manufacturer='" + manufacturer + '\'' + + ", model='" + model + '\'' + + ", network=" + network + + ", operator=" + operator + + ", owner=" + owner + + ", phone=" + phone + + ", platform='" + platform + '\'' + + ", presenceChangedAt='" + presenceChangedAt + '\'' + + ", present=" + present + + ", product='" + product + '\'' + + ", provider=" + provider + + ", ready=" + ready + + ", remoteConnectUrl=" + remoteConnectUrl + + ", remoteConnect=" + remoteConnect + + ", reverseForwards=" + reverseForwards + + ", sdk='" + sdk + '\'' + + ", serial='" + serial + '\'' + + ", statusChangedAt='" + statusChangedAt + '\'' + + ", status=" + status + + ", using=" + using + + ", version='" + version + '\'' + + ", deviceType='" + deviceType + '\'' + + ", additionalProperties=" + additionalProperties + + '}'; + } +} diff --git a/src/main/java/com/zebrunner/mcloud/grid/models/stf/STFUser.java b/src/main/java/com/zebrunner/mcloud/grid/models/stf/STFUser.java new file mode 100644 index 0000000..0812c41 --- /dev/null +++ b/src/main/java/com/zebrunner/mcloud/grid/models/stf/STFUser.java @@ -0,0 +1,54 @@ +/******************************************************************************* + * Copyright 2018-2021 Zebrunner (https://zebrunner.com/). + * + * Licensed 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 com.zebrunner.mcloud.grid.models.stf; + +import java.util.HashMap; +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonAnyGetter; +import com.fasterxml.jackson.annotation.JsonAnySetter; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@NoArgsConstructor +@JsonInclude(JsonInclude.Include.NON_NULL) +public class STFUser { + + private String email; + private String name; + private String privilege; + + @JsonIgnore + private Map additionalProperties = new HashMap<>(); + + @JsonAnyGetter + public Map getAdditionalProperties() { + return this.additionalProperties; + } + + @JsonAnySetter + public void setAdditionalProperty(String name, Object value) { + this.additionalProperties.put(name, value); + } + +} diff --git a/src/main/java/com/zebrunner/mcloud/grid/models/stf/User.java b/src/main/java/com/zebrunner/mcloud/grid/models/stf/User.java new file mode 100644 index 0000000..4ca1054 --- /dev/null +++ b/src/main/java/com/zebrunner/mcloud/grid/models/stf/User.java @@ -0,0 +1,52 @@ +/******************************************************************************* + * Copyright 2018-2021 Zebrunner (https://zebrunner.com/). + * + * Licensed 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 com.zebrunner.mcloud.grid.models.stf; + +import java.util.HashMap; +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonAnyGetter; +import com.fasterxml.jackson.annotation.JsonAnySetter; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@NoArgsConstructor +@JsonInclude(JsonInclude.Include.NON_NULL) +public class User { + + private STFUser user; + private Boolean success; + @JsonIgnore + private Map additionalProperties = new HashMap(); + + @JsonAnyGetter + public Map getAdditionalProperties() { + return this.additionalProperties; + } + + @JsonAnySetter + public void setAdditionalProperty(String name, Object value) { + this.additionalProperties.put(name, value); + } + +} diff --git a/src/main/java/com/zebrunner/mcloud/grid/util/CapabilityUtils.java b/src/main/java/com/zebrunner/mcloud/grid/util/CapabilityUtils.java new file mode 100644 index 0000000..632f9b6 --- /dev/null +++ b/src/main/java/com/zebrunner/mcloud/grid/util/CapabilityUtils.java @@ -0,0 +1,38 @@ +package com.zebrunner.mcloud.grid.util; + +import org.openqa.selenium.Capabilities; + +import java.util.Optional; + +public final class CapabilityUtils { + + private CapabilityUtils() { + //hide + } + + public static Optional getAppiumCapability(Capabilities capabilities, String capabilityName) { + Object value = capabilities.getCapability("appium:" + capabilityName); + if (value == null) { + // for backward compatibility + // todo investigate + value = capabilities.getCapability(capabilityName); + } + return Optional.ofNullable(value); + } + + public static Optional getZebrunnerCapability(Capabilities capabilities, String capabilityName) { + Object value = capabilities.getCapability("zebrunner:" + capabilityName); + if (value == null) { + // for backward compatibility + // todo investigate + value = capabilities.getCapability("appium:" + capabilityName); + } + if (value == null) { + // for backward compatibility + // todo investigate + value = capabilities.getCapability(capabilityName); + } + return Optional.ofNullable(value); + } + +} diff --git a/src/main/java/com/zebrunner/mcloud/grid/util/GensonProvider.java b/src/main/java/com/zebrunner/mcloud/grid/util/GensonProvider.java new file mode 100644 index 0000000..9f3de79 --- /dev/null +++ b/src/main/java/com/zebrunner/mcloud/grid/util/GensonProvider.java @@ -0,0 +1,39 @@ +/******************************************************************************* + * Copyright 2018-2021 Zebrunner (https://zebrunner.com/). + * + * Licensed 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 com.zebrunner.mcloud.grid.util; + +import com.owlike.genson.Genson; +import com.owlike.genson.GensonBuilder; + +import javax.ws.rs.ext.ContextResolver; +import javax.ws.rs.ext.Provider; + +/** + * GensonProvider - allows to deserialize timestamp to Date. + * + * @author akhursevich + */ +@Provider +public class GensonProvider implements ContextResolver { + + private final Genson genson = new GensonBuilder().create(); + + @Override + public Genson getContext(Class type) { + return genson; + } + +} \ No newline at end of file diff --git a/src/main/java/com/zebrunner/mcloud/grid/util/HttpClient.java b/src/main/java/com/zebrunner/mcloud/grid/util/HttpClient.java new file mode 100644 index 0000000..1bf2473 --- /dev/null +++ b/src/main/java/com/zebrunner/mcloud/grid/util/HttpClient.java @@ -0,0 +1,191 @@ +/******************************************************************************* + * Copyright 2018-2021 Zebrunner (https://zebrunner.com/). + * + * Licensed 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 com.zebrunner.mcloud.grid.util; + +import java.util.Map; +import java.util.function.Function; +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.MultivaluedMap; + +import org.apache.commons.lang3.StringUtils; + +import com.sun.jersey.api.client.Client; +import com.sun.jersey.api.client.ClientResponse; +import com.sun.jersey.api.client.WebResource; +import com.sun.jersey.api.client.config.DefaultClientConfig; +import com.sun.jersey.core.util.MultivaluedMapImpl; +import com.zebrunner.mcloud.grid.integration.client.Path; +import org.apache.commons.lang3.concurrent.ConcurrentException; +import org.apache.commons.lang3.concurrent.LazyInitializer; +import org.apache.commons.lang3.exception.ExceptionUtils; + +public final class HttpClient { + private static final Logger LOGGER = Logger.getLogger(HttpClient.class.getName()); + + private static final LazyInitializer CLIENT = new LazyInitializer<>() { + @Override + protected Client initialize() throws ConcurrentException { + Client client = Client.create(new DefaultClientConfig(GensonProvider.class)); + client.setConnectTimeout(3000); + client.setReadTimeout(3000); + return client; + } + }; + + private HttpClient() { + //hide + } + + public static Executor uri(Path path, String serviceUrl, Object... parameters) { + String url = path.build(serviceUrl, parameters); + return uri(url, null); + } + + public static Executor uri(Path path, Map queryParameters, String serviceUrl, Object... parameters) { + String url = path.build(serviceUrl, parameters); + return uri(url, queryParameters); + } + + private static Executor uri(String url, Map queryParameters) { + try { + WebResource webResource = CLIENT.get() + .resource(url); + if (queryParameters != null) { + MultivaluedMap requestParameters = new MultivaluedMapImpl(); + queryParameters.forEach(requestParameters::add); + webResource = webResource.queryParams(requestParameters); + } + return new Executor(webResource); + } catch (ConcurrentException e) { + return ExceptionUtils.rethrow(e); + } + } + + public static class Executor { + + private final WebResource.Builder builder; + private String errorMessage; + + public Executor(WebResource webResource) { + builder = webResource.type(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON); + } + + public Response get(Class responseClass) { + return execute(responseClass, builder -> builder.get(ClientResponse.class)); + } + + public Response post(Class responseClass, Object requestEntity) { + return execute(responseClass, builder -> builder.post(ClientResponse.class, requestEntity)); + } + + public Response put(Class responseClass, Object requestEntity) { + return execute(responseClass, builder -> builder.put(ClientResponse.class, requestEntity)); + } + + public Response delete(Class responseClass) { + return execute(responseClass, builder -> builder.delete(ClientResponse.class)); + } + + public Executor type(String mediaType) { + builder.type(mediaType); + return this; + } + + public Executor accept(String mediaType) { + builder.accept(mediaType); + return this; + } + + public Executor withAuthorization(String authToken) { + return withAuthorization(authToken, null); + } + + public Executor withAuthorization(String authToken, String project) { + initHeaders(builder, authToken, project); + return this; + } + + private static void initHeaders(WebResource.Builder builder, String authToken, String project) { + if (!StringUtils.isEmpty(authToken)) { + builder.header("Authorization", authToken); + } + if (!StringUtils.isEmpty(project)) { + builder.header("Project", project); + } + } + + private Response execute(Class responseClass, Function methodBuilder) { + Response rs = new Response<>(); + try { + ClientResponse response = methodBuilder.apply(builder); + int status = response.getStatus(); + rs.setStatus(status); + if (responseClass != null && !responseClass.isAssignableFrom(Void.class) && status == 200) { + rs.setObject(response.getEntity(responseClass)); + } + } catch (Exception e) { + String message = errorMessage == null ? e.getMessage() : e.getMessage() + ". " + errorMessage; + LOGGER.log(Level.SEVERE, message, e); + } + return rs; + } + + public Executor onFailure(String message) { + this.errorMessage = message; + return this; + } + + } + + public static class Response { + + private int status; + private T object; + + public Response() { + } + + Response(int status, T object) { + this.status = status; + this.object = object; + } + + public int getStatus() { + return status; + } + + public void setStatus(int status) { + this.status = status; + } + + public T getObject() { + return object; + } + + public void setObject(T object) { + this.object = object; + } + + @Override + public String toString() { + return "Response [status=" + status + ", object=" + object + "]"; + } + } +} diff --git a/src/main/java/com/zebrunner/mcloud/grid/util/HttpClientApache.java b/src/main/java/com/zebrunner/mcloud/grid/util/HttpClientApache.java new file mode 100644 index 0000000..387f2b8 --- /dev/null +++ b/src/main/java/com/zebrunner/mcloud/grid/util/HttpClientApache.java @@ -0,0 +1,142 @@ +/******************************************************************************* + * Copyright 2018-2021 Zebrunner (https://zebrunner.com/). + * + * Licensed 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 com.zebrunner.mcloud.grid.util; + +import java.io.IOException; +import java.net.URI; +import java.time.Duration; +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.apache.hc.client5.http.classic.methods.HttpDelete; +import org.apache.hc.client5.http.classic.methods.HttpGet; +import org.apache.hc.client5.http.classic.methods.HttpPost; +import org.apache.hc.client5.http.classic.methods.HttpPut; +import org.apache.hc.client5.http.classic.methods.HttpUriRequest; +import org.apache.hc.client5.http.classic.methods.HttpUriRequestBase; +import org.apache.hc.client5.http.config.RequestConfig; +import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; +import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse; +import org.apache.hc.client5.http.impl.classic.HttpClientBuilder; +import org.apache.hc.core5.http.HttpEntity; +import org.apache.hc.core5.http.ParseException; +import org.apache.hc.core5.http.io.entity.EntityUtils; +import org.apache.hc.core5.util.Timeout; + +import com.zebrunner.mcloud.grid.integration.client.Path; +import com.zebrunner.mcloud.grid.util.HttpClient.Response; + +public final class HttpClientApache { + + private static final Logger LOGGER = Logger.getLogger(HttpClientApache.class.getName()); + private static final RequestConfig DEFAULT_REQUEST_CFG = RequestConfig.custom() + .setConnectionRequestTimeout(Timeout.of(Duration.ofSeconds(3))) + .build(); + private RequestConfig requestConfig = DEFAULT_REQUEST_CFG; + private String url; + + private HttpClientApache() { + //hide + } + + public static HttpClientApache create() { + return new HttpClientApache(); + } + + public HttpClientApache withRequestConfig(RequestConfig requestConfig) { + this.requestConfig = requestConfig; + return this; + } + + public HttpClientApache withUri(Path path, String serviceUrl, Object... parameters) { + this.url = path.build(serviceUrl, parameters); + return this; + } + + public Response get() { + if (url == null) { + LOGGER.log(Level.WARNING, "url should be specified!"); + return null; + } + return execute(new HttpGet(url)); + } + + public static class HttpGetWithEntity extends HttpUriRequestBase { + public static final String METHOD_NAME = "GET"; + + public HttpGetWithEntity(final String uri) { + super(METHOD_NAME, URI.create(uri)); + } + + @Override + public String getMethod() { + return METHOD_NAME; + } + } + + public Response get(HttpEntity entity) { + if (url == null) { + LOGGER.log(Level.WARNING, "url should be specified!"); + return null; + } + HttpGetWithEntity get = new HttpGetWithEntity(url); + get.setEntity(entity); + return execute(get); + } + + public Response post(HttpEntity entity) { + if (url == null) { + LOGGER.log(Level.WARNING, "url should be specified!"); + return null; + } + HttpPost post = new HttpPost(url); + post.setEntity(entity); + return execute(post); + } + + public Response put(HttpEntity entity) { + if (url == null) { + LOGGER.log(Level.WARNING, "url should be specified!"); + return null; + } + HttpPut put = new HttpPut(url); + put.setEntity(entity); + return execute(put); + } + + public Response delete() { + if (url == null) { + LOGGER.log(Level.WARNING, "url should be specified!"); + return null; + } + HttpDelete delete = new HttpDelete(url); + return execute(delete); + } + + private Response execute(HttpUriRequest req) { + Response result = new Response<>(); + try (CloseableHttpClient httpClient = HttpClientBuilder.create() + .setDefaultRequestConfig(requestConfig) + .build(); + CloseableHttpResponse response = httpClient.execute(req)) { + result.setStatus(response.getCode()); + result.setObject(EntityUtils.toString(response.getEntity())); + } catch (IOException | ParseException e) { + LOGGER.log(Level.SEVERE, e.getMessage(), e); + } + return result; + } +} diff --git a/src/main/java/com/zebrunner/mcloud/grid/utils/CapabilityUtils.java b/src/main/java/com/zebrunner/mcloud/grid/utils/CapabilityUtils.java new file mode 100644 index 0000000..500282c --- /dev/null +++ b/src/main/java/com/zebrunner/mcloud/grid/utils/CapabilityUtils.java @@ -0,0 +1,60 @@ +package com.zebrunner.mcloud.grid.utils; + +import org.openqa.selenium.Capabilities; + +import java.util.ArrayList; +import java.util.List; + +public final class CapabilityUtils { + private static final String APPIUM_PREFIX = "appium:"; + private static final String ZEBRUNNER_PREFIX = "zebrunner:"; + + private CapabilityUtils() { + //hide + } + + public static T getAppiumCapability(Capabilities caps, String name, Class expectedType) { + List possibleNames = new ArrayList<>(); + possibleNames.add(name); + if (!name.startsWith(APPIUM_PREFIX)) { + possibleNames.add(APPIUM_PREFIX + name); + } + for (String capName : possibleNames) { + if (caps.getCapability(capName) == null) { + continue; + } + + if (expectedType == String.class) { + return expectedType.cast(String.valueOf(caps.getCapability(capName))); + } + if (expectedType.isAssignableFrom(caps.getCapability(capName).getClass())) { + return expectedType.cast(caps.getCapability(capName)); + } + } + return null; + } + + public static T getZebrunnerCapability(Capabilities caps, String name, Class expectedType) { + List possibleNames = new ArrayList<>(); + possibleNames.add(name); + if (!name.startsWith(APPIUM_PREFIX)) { + possibleNames.add(APPIUM_PREFIX + name); + } + if (!name.startsWith(ZEBRUNNER_PREFIX)) { + possibleNames.add(ZEBRUNNER_PREFIX + name); + } + for (String capName : possibleNames) { + if (caps.getCapability(capName) == null) { + continue; + } + + if (expectedType == String.class) { + return expectedType.cast(String.valueOf(caps.getCapability(capName))); + } + if (expectedType.isAssignableFrom(caps.getCapability(capName).getClass())) { + return expectedType.cast(caps.getCapability(capName)); + } + } + return null; + } +} diff --git a/src/main/java/com/zebrunner/mcloud/grid/validator/DeviceNameValidator.java b/src/main/java/com/zebrunner/mcloud/grid/validator/DeviceNameValidator.java new file mode 100644 index 0000000..216391b --- /dev/null +++ b/src/main/java/com/zebrunner/mcloud/grid/validator/DeviceNameValidator.java @@ -0,0 +1,36 @@ +package com.zebrunner.mcloud.grid.validator; + +import com.zebrunner.mcloud.grid.utils.CapabilityUtils; +import org.apache.commons.lang3.StringUtils; +import org.openqa.selenium.Capabilities; + +import java.lang.invoke.MethodHandles; +import java.util.Arrays; +import java.util.Optional; +import java.util.logging.Logger; + +public class DeviceNameValidator implements Validator { + private static final Logger LOGGER = Logger.getLogger(MethodHandles.lookup().lookupClass().getName()); + private static final String DEVICE_NAME_CAPABILITY = "deviceName"; + + @Override + public Boolean apply(Capabilities stereotype, Capabilities capabilities) { + String expectedValue = CapabilityUtils.getAppiumCapability(capabilities, DEVICE_NAME_CAPABILITY, String.class); + if (anything(expectedValue)) { + return true; + } + String actualValue = CapabilityUtils.getAppiumCapability(stereotype, DEVICE_NAME_CAPABILITY, String.class); + if (actualValue == null) { + LOGGER.warning("No 'deviceName' capability specified for node."); + return false; + } + boolean matches = Arrays.stream(Optional.ofNullable(StringUtils.split(expectedValue, ",")).orElse(new String[] {})) + .anyMatch(e -> StringUtils.equals(e, actualValue)); + if (matches) { + LOGGER.info(() -> String.format("[CAPABILITY-VALIDATOR] device name matches: %s - %s", expectedValue, actualValue)); + } else { + LOGGER.info(() -> String.format("[CAPABILITY-VALIDATOR] device name does not matches: %s - %s", expectedValue, actualValue)); + } + return matches; + } +} diff --git a/src/main/java/com/zebrunner/mcloud/grid/validator/DeviceTypeValidator.java b/src/main/java/com/zebrunner/mcloud/grid/validator/DeviceTypeValidator.java new file mode 100644 index 0000000..26a183d --- /dev/null +++ b/src/main/java/com/zebrunner/mcloud/grid/validator/DeviceTypeValidator.java @@ -0,0 +1,32 @@ +package com.zebrunner.mcloud.grid.validator; + +import com.zebrunner.mcloud.grid.utils.CapabilityUtils; +import org.apache.commons.lang3.StringUtils; +import org.openqa.selenium.Capabilities; + +import java.lang.invoke.MethodHandles; +import java.util.logging.Logger; + +public class DeviceTypeValidator implements Validator { + private static final Logger LOGGER = Logger.getLogger(MethodHandles.lookup().lookupClass().getName()); + private static final String ZEBRUNNER_DEVICE_TYPE_CAPABILITY = "deviceType"; + + @Override + public Boolean apply(Capabilities stereotype, Capabilities capabilities) { + String expectedValue = CapabilityUtils.getZebrunnerCapability(capabilities, ZEBRUNNER_DEVICE_TYPE_CAPABILITY, String.class); + if (anything(expectedValue)) { + return true; + } + String actualValue = CapabilityUtils.getZebrunnerCapability(stereotype, ZEBRUNNER_DEVICE_TYPE_CAPABILITY, String.class); + if (actualValue == null) { + return false; + } + boolean matches = StringUtils.equalsIgnoreCase(actualValue, expectedValue); + if (matches) { + LOGGER.info(() -> String.format("[CAPABILITY-VALIDATOR] device type matches: %s - %s", expectedValue, actualValue)); + } else { + LOGGER.info(() -> String.format("[CAPABILITY-VALIDATOR] device type does not matches: %s - %s", expectedValue, actualValue)); + } + return matches; + } +} diff --git a/src/main/java/com/zebrunner/mcloud/grid/validator/MobilePlatformValidator.java b/src/main/java/com/zebrunner/mcloud/grid/validator/MobilePlatformValidator.java new file mode 100644 index 0000000..2b70988 --- /dev/null +++ b/src/main/java/com/zebrunner/mcloud/grid/validator/MobilePlatformValidator.java @@ -0,0 +1,49 @@ +package com.zebrunner.mcloud.grid.validator; + +import org.openqa.selenium.Capabilities; +import org.openqa.selenium.Platform; +import org.openqa.selenium.WebDriverException; +import org.openqa.selenium.remote.CapabilityType; + +import java.lang.invoke.MethodHandles; +import java.util.logging.Logger; + +public class MobilePlatformValidator implements Validator { + private static final Logger LOGGER = Logger.getLogger(MethodHandles.lookup().lookupClass().getName()); + + @Override + public Boolean apply(Capabilities stereotype, Capabilities capabilities) { + Object requested = capabilities.getCapability(CapabilityType.PLATFORM_NAME); + if (anything(requested instanceof Platform ? ((Platform) requested).name() : String.valueOf(requested))) { + return true; + } + Object provided = stereotype.getCapability(CapabilityType.PLATFORM_NAME); + if (provided == null) { + LOGGER.warning("No 'platformName' capability specified for node."); + return false; + } + boolean matches = extractPlatform(provided).is(extractPlatform(requested)); + if (matches) { + LOGGER.info( + () -> String.format("[CAPABILITY-VALIDATOR] Platform matches: %s - %s", extractPlatform(requested), extractPlatform(provided))); + } else { + LOGGER.info(() -> String.format("[CAPABILITY-VALIDATOR] Platform does not matches: %s - %s", extractPlatform(requested), + extractPlatform(provided))); + } + return matches; + } + + private Platform extractPlatform(Object o) { + if (o == null) { + return null; + } + if (o instanceof Platform) { + return (Platform) o; + } + try { + return Platform.fromString(o.toString()); + } catch (WebDriverException ex) { + return null; + } + } +} diff --git a/src/main/java/com/zebrunner/mcloud/grid/validator/PlatformVersionValidator.java b/src/main/java/com/zebrunner/mcloud/grid/validator/PlatformVersionValidator.java new file mode 100644 index 0000000..7111e57 --- /dev/null +++ b/src/main/java/com/zebrunner/mcloud/grid/validator/PlatformVersionValidator.java @@ -0,0 +1,104 @@ +package com.zebrunner.mcloud.grid.validator; + +import com.zebrunner.mcloud.grid.utils.CapabilityUtils; +import org.openqa.selenium.Capabilities; + +import javax.annotation.Nonnull; +import java.lang.invoke.MethodHandles; +import java.util.logging.Logger; + +public class PlatformVersionValidator implements Validator { + private static final Logger LOGGER = Logger.getLogger(MethodHandles.lookup().lookupClass().getName()); + private static final String PLATFORM_VERSION_CAPABILITY = "platformVersion"; + + @Override + public Boolean apply(Capabilities nodeCapabilities, Capabilities requestedCapabilities) { + String expectedValue = CapabilityUtils.getAppiumCapability(requestedCapabilities, PLATFORM_VERSION_CAPABILITY, String.class); + if (anything(expectedValue)) { + return true; + } + + String actualValue = CapabilityUtils.getAppiumCapability(nodeCapabilities, PLATFORM_VERSION_CAPABILITY, String.class); + if (actualValue == null) { + return false; + } + + // Limited interval: 6.1.1-7.0 + if (expectedValue.matches("(\\d+\\.){0,}(\\d+)-(\\d+\\.){0,}(\\d+)$")) { + PlatformVersion actPV = new PlatformVersion(actualValue); + PlatformVersion minPV = new PlatformVersion(expectedValue.split("-")[0]); + PlatformVersion maxPV = new PlatformVersion(expectedValue.split("-")[1]); + + return !(actPV.compareTo(minPV) < 0 || actPV.compareTo(maxPV) > 0); + } + // Unlimited interval: 6.0+ + else if (expectedValue.matches("(\\d+\\.){0,}(\\d+)\\+$")) { + PlatformVersion actPV = new PlatformVersion(actualValue); + PlatformVersion minPV = new PlatformVersion(expectedValue.replace("+", "")); + + return actPV.compareTo(minPV) >= 0; + } + // Multiple versions: 6.1,7.0 + else if (expectedValue.matches("(\\d+\\.){0,}(\\d+,)+(\\d+\\.){0,}(\\d+)$")) { + boolean matches = false; + for (String version : expectedValue.split(",")) { + if (new PlatformVersion(version).compareTo(new PlatformVersion(actualValue)) == 0) { + matches = true; + break; + } + } + return matches; + } + // Exact version: 7.0 + else if (expectedValue.matches("(\\d+\\.){0,}(\\d+)$")) { + return new PlatformVersion(expectedValue).compareTo(new PlatformVersion(actualValue)) == 0; + } + LOGGER.warning("Cannot find suitable pattern for version: " + expectedValue); + return false; + } + + private class PlatformVersion implements Comparable { + private int[] version; + + public PlatformVersion(String v) { + if (v != null && v.matches("(\\d+\\.){0,}(\\d+)$")) { + String[] digits = v.split("\\."); + this.version = new int[digits.length]; + for (int i = 0; i < digits.length; i++) { + this.version[i] = Integer.valueOf(digits[i]); + } + } + } + + public int[] getVersion() { + return version; + } + + public void setVersion(int[] version) { + this.version = version; + } + + @Override + public int compareTo(@Nonnull PlatformVersion pv) { + int result = 0; + if (pv.getVersion() != null && this.version != null) { + int minL = Math.min(this.version.length, pv.getVersion().length); + int maxL = Math.max(this.version.length, pv.getVersion().length); + + for (int i = 0; i < minL; i++) { + result = this.version[i] - pv.getVersion()[i]; + if (result != 0) { + break; + } + } + + if (result == 0 && this.version.length == minL && minL != maxL) { + result = -1; + } else if (result == 0 && this.version.length == maxL && minL != maxL) { + result = 1; + } + } + return result; + } + } +} diff --git a/src/main/java/com/zebrunner/mcloud/grid/validator/UDIDValidator.java b/src/main/java/com/zebrunner/mcloud/grid/validator/UDIDValidator.java new file mode 100644 index 0000000..f6f6c63 --- /dev/null +++ b/src/main/java/com/zebrunner/mcloud/grid/validator/UDIDValidator.java @@ -0,0 +1,36 @@ +package com.zebrunner.mcloud.grid.validator; + +import com.zebrunner.mcloud.grid.utils.CapabilityUtils; +import org.apache.commons.lang3.StringUtils; +import org.openqa.selenium.Capabilities; + +import java.lang.invoke.MethodHandles; +import java.util.Arrays; +import java.util.Optional; +import java.util.logging.Logger; + +public class UDIDValidator implements Validator { + private static final Logger LOGGER = Logger.getLogger(MethodHandles.lookup().lookupClass().getName()); + private static final String APPIUM_UDID_CAPABILITY = "udid"; + + @Override + public Boolean apply(Capabilities nodeCapabilities, Capabilities requestedCapabilities) { + String expectedValue = CapabilityUtils.getAppiumCapability(requestedCapabilities, APPIUM_UDID_CAPABILITY, String.class); + if (anything(expectedValue)) { + return true; + } + String actualValue = CapabilityUtils.getAppiumCapability(nodeCapabilities, APPIUM_UDID_CAPABILITY, String.class); + if (actualValue == null) { + return false; + } + boolean matches = Arrays.stream(Optional.ofNullable(StringUtils.split(expectedValue, ",")).orElse(new String[] {})) + .anyMatch(e -> StringUtils.equals(e, actualValue)); + + if (matches) { + LOGGER.info(() -> String.format("[CAPABILITY-VALIDATOR] device udid matches: %s - %s", expectedValue, actualValue)); + } else { + LOGGER.info(() -> String.format("[CAPABILITY-VALIDATOR] device udid does not matches: %s - %s", expectedValue, actualValue)); + } + return matches; + } +} diff --git a/src/main/java/com/zebrunner/mcloud/grid/validator/Validator.java b/src/main/java/com/zebrunner/mcloud/grid/validator/Validator.java new file mode 100644 index 0000000..af7ed1c --- /dev/null +++ b/src/main/java/com/zebrunner/mcloud/grid/validator/Validator.java @@ -0,0 +1,13 @@ +package com.zebrunner.mcloud.grid.validator; + +import org.apache.commons.lang3.StringUtils; +import org.openqa.selenium.Capabilities; + +import java.util.function.BiFunction; + +public interface Validator extends BiFunction { + + default boolean anything(String requested) { + return StringUtils.equalsAnyIgnoreCase(requested, "ANY", "", "*", null); + } +}