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/android.sh b/files/android.sh index 7906afe..a2abfad 100755 --- a/files/android.sh +++ b/files/android.sh @@ -30,12 +30,7 @@ else export DEVICETYPE='Phone' fi -if [[ ${PLATFORM_VERSION} == 4* ]] || [[ ${PLATFORM_VERSION} == 5* ]] || [[ ${PLATFORM_VERSION} == 6* ]] -then - export AUTOMATION_NAME='Appium' -else export AUTOMATION_NAME='uiautomator2' -fi # there is no sense to clean something for scalable one-time redroid container if [ "$ANDROID_DEVICE" != "device:5555" ]; then diff --git a/files/zbr-config-gen.sh b/files/zbr-config-gen.sh index 3a91e24..720174e 100755 --- a/files/zbr-config-gen.sh +++ b/files/zbr-config-gen.sh @@ -1,47 +1,52 @@ #!/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": {} - } -} +[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/MobileRemoteProxy.java b/src/main/java/com/zebrunner/mcloud/grid/MobileRemoteProxy.java new file mode 100644 index 0000000..7af4dd0 --- /dev/null +++ b/src/main/java/com/zebrunner/mcloud/grid/MobileRemoteProxy.java @@ -0,0 +1,269 @@ +/******************************************************************************* + * 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.integration.client.Path; +import com.zebrunner.mcloud.grid.models.stf.Devices; +import com.zebrunner.mcloud.grid.models.stf.STFDevice; +import com.zebrunner.mcloud.grid.models.stf.User; +import com.zebrunner.mcloud.grid.util.CapabilityUtils; +import com.zebrunner.mcloud.grid.util.HttpClient; +import org.apache.commons.lang3.StringUtils; +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.RetrySessionRequestException; +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.local.LocalNode; +import org.openqa.selenium.grid.node.local.LocalNodeFactory; +import org.openqa.selenium.grid.node.local.SessionSlot; +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.SessionId; +import org.openqa.selenium.remote.http.HttpMethod; +import org.openqa.selenium.remote.http.HttpRequest; +import org.openqa.selenium.remote.http.HttpResponse; +import org.openqa.selenium.remote.tracing.Tracer; + +import java.io.IOException; +import java.net.URI; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.TimeUnit; +import java.util.logging.Logger; + +public class MobileRemoteProxy extends Node { + private static final Logger LOGGER = Logger.getLogger(MobileRemoteProxy.class.getName()); + 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 = Optional.ofNullable(System.getenv("STF_TIMEOUT")) + .filter(StringUtils::isNotBlank) + .orElse("3600"); + + private LocalNode node; + private Capabilities stereotype; + private String udid; + + 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 = (LocalNode) node; + try { + wrapper.stereotype = ((List) FieldUtils.readField(node, "factories", true)).get(0) + .getStereotype(); + } catch (IllegalAccessException e) { + return ExceptionUtils.rethrow(e); + } + wrapper.udid = String.valueOf(CapabilityUtils.getAppiumCapability(wrapper.stereotype, "udid").orElseThrow()); + return wrapper; + } + + @Override + public Either newSession(CreateSessionRequest sessionRequest) { + String NOT_AUTHENTICATED_ERROR = "[STF] Not authenticated at STF successfully! URL: '%s'; Token: '%s'"; + String UNABLE_GET_DEVICES_STATUS_ERROR = "[STF] Unable to get devices status. HTTP status: %s"; + String COULD_NOT_FIND_DEVICE_ERROR = "[STF] Could not find STF device with udid: %s"; + if (node.getCurrentSessionCount() >= node.getMaxSessionCount()) { + return Either.left(new RetrySessionRequestException("Max session count reached.")); + } + if (node.isDraining()) { + return Either.left(new RetrySessionRequestException("The node is draining. Cannot accept new sessions.")); + } + String stfToken = CapabilityUtils.getZebrunnerCapability(sessionRequest.getDesiredCapabilities(), "STF_TOKEN") + .map(String::valueOf) + .orElse(DEFAULT_STF_TOKEN); + if (isSTFEnabled()) { + 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(NOT_AUTHENTICATED_ERROR, STF_URL, stfToken)); + return Either.left( + new RetrySessionRequestException(String.format(NOT_AUTHENTICATED_ERROR, STF_URL, stfToken))); + } + 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(UNABLE_GET_DEVICES_STATUS_ERROR, devices.getStatus())); + return Either.left( + new RetrySessionRequestException(String.format(UNABLE_GET_DEVICES_STATUS_ERROR, devices.getStatus()))); + } + + Optional optionalSTFDevice = devices.getObject() + .getDevices() + .stream() + .filter(device -> StringUtils.equals(device.getSerial(), udid)) + .findFirst(); + if (optionalSTFDevice.isEmpty()) { + LOGGER.warning(() -> String.format(COULD_NOT_FIND_DEVICE_ERROR, udid)); + return Either.left( + new RetrySessionRequestException(String.format(COULD_NOT_FIND_DEVICE_ERROR, udid))); + } + + STFDevice device = optionalSTFDevice.get(); + LOGGER.info(() -> String.format("STF device info: %s", device)); + + if (device.getOwner() != null) { + if (!(StringUtils.equals(device.getOwner().getName(), user.getObject().getUser().getName()) && + device.getPresent() && + device.getReady())) { + return Either.left(new RetrySessionRequestException( + String.format("STF device busy by %s or not present/ready.", device.getOwner().getName()))); + } + } + } + + Either response = node.newSession(sessionRequest); + if (response.isRight() && isSTFEnabled()) { + Map entity = new HashMap<>(); + entity.put("serial", udid); + entity.put("timeout", + TimeUnit.SECONDS.toMillis(CapabilityUtils.getZebrunnerCapability(sessionRequest.getDesiredCapabilities(), "STF_TIMEOUT") + .map(String::valueOf) + .map(Integer::parseInt) + .orElse(Integer.parseInt(DEFAULT_STF_TIMEOUT)))); + if (HttpClient.uri(Path.STF_USER_DEVICES_PATH, STF_URL) + .withAuthorization(buildAuthToken(stfToken)) + .post(Void.class, entity).getStatus() != 200) { + LOGGER.warning(() -> String.format("[STF] Could not reserve STF device with udid: %s.", udid)); + } + } + return response; + } + + @Override + public HttpResponse executeWebDriverCommand(HttpRequest req) { + if (HttpMethod.DELETE.equals(req.getMethod())) { + if (isSTFEnabled()) { + LOGGER.info(() -> "[STF] Return STF Device."); + if (HttpClient.uri(Path.STF_USER_DEVICES_BY_ID_PATH, STF_URL, udid) + .withAuthorization(buildAuthToken(DEFAULT_STF_TOKEN)) + .delete(Void.class).getStatus() != 200) { + LOGGER.warning(() -> "[STF] Could not return device to the STF."); + } + } + } + return node.executeWebDriverCommand(req); + } + + @Override + public Session getSession(SessionId id) throws NoSuchSessionException { + return node.getSession(id); + } + + @Override + public HttpResponse uploadFile(HttpRequest req, SessionId id) { + return node.uploadFile(req, id); + } + + @Override + public HttpResponse downloadFile(HttpRequest req, SessionId id) { + return node.downloadFile(req, id); + } + + @Override + public TemporaryFilesystem getDownloadsFilesystem(UUID uuid) throws IOException { + return node.getDownloadsFilesystem(uuid); + } + + @Override + public TemporaryFilesystem getUploadsFilesystem(SessionId id) throws IOException { + return node.getUploadsFilesystem(id); + } + + @Override + public void stop(SessionId id) throws NoSuchSessionException { + if (isSTFEnabled()) { + LOGGER.info(() -> "[STF] Return STF Device."); + if (HttpClient.uri(Path.STF_USER_DEVICES_BY_ID_PATH, STF_URL, udid) + .withAuthorization(buildAuthToken(DEFAULT_STF_TOKEN)) + .delete(Void.class).getStatus() != 200) { + LOGGER.warning(() -> "[STF] Could not return device to the STF."); + } + } + node.stop(id); + } + + @Override + public boolean isSessionOwner(SessionId id) { + return node.isSessionOwner(id); + } + + @Override + public boolean isSupporting(Capabilities capabilities) { + return node.isSupporting(capabilities); + } + + @Override + public NodeStatus getStatus() { + return node.getStatus(); + } + + @Override + public HealthCheck getHealthCheck() { + return node.getHealthCheck(); + } + + @Override + public void drain() { + node.drain(); + } + + @Override + public boolean isReady() { + return node.isReady(); + } + + private static boolean isSTFEnabled() { + return (!StringUtils.isEmpty(STF_URL) && !StringUtils.isEmpty(DEFAULT_STF_TOKEN)); + } + + private static String buildAuthToken(String authToken) { + return "Bearer " + authToken; + } + +} 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/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; + } +}