diff --git a/vividus-plugin-mobitru/src/main/java/org/vividus/mobitru/client/MobitruClient.java b/vividus-plugin-mobitru/src/main/java/org/vividus/mobitru/client/MobitruClient.java index f24d815c3d..874494b030 100644 --- a/vividus-plugin-mobitru/src/main/java/org/vividus/mobitru/client/MobitruClient.java +++ b/vividus-plugin-mobitru/src/main/java/org/vividus/mobitru/client/MobitruClient.java @@ -57,6 +57,13 @@ public byte[] findDevices(String platform, Map parameters) throw MobitruDeviceSearchException::new); } + public byte[] takeDeviceBySerial(String udid) throws MobitruOperationException + { + URIBuilder uriBuilder = new URIBuilder().setPath(DEVICE_PATH).appendPath(udid); + return executeRequest(uriBuilder.toString(), HttpMethod.POST, UnaryOperator.identity(), + HttpStatus.SC_OK, MobitruDeviceTakeException::new); + } + public byte[] takeDevice(String requestedDevice) throws MobitruOperationException { return executePost(DEVICE_PATH, requestedDevice, HttpStatus.SC_OK); diff --git a/vividus-plugin-mobitru/src/main/java/org/vividus/mobitru/client/MobitruFacadeImpl.java b/vividus-plugin-mobitru/src/main/java/org/vividus/mobitru/client/MobitruFacadeImpl.java index 9b592844e1..8890400c79 100644 --- a/vividus-plugin-mobitru/src/main/java/org/vividus/mobitru/client/MobitruFacadeImpl.java +++ b/vividus-plugin-mobitru/src/main/java/org/vividus/mobitru/client/MobitruFacadeImpl.java @@ -23,6 +23,7 @@ import java.util.Map; import java.util.Objects; import java.util.Optional; +import java.util.function.Supplier; import java.util.stream.Stream; import com.fasterxml.jackson.databind.DeserializationFeature; @@ -30,6 +31,7 @@ import org.apache.commons.lang3.Validate; import org.apache.commons.lang3.function.FailableFunction; +import org.apache.commons.lang3.function.FailableSupplier; import org.openqa.selenium.remote.DesiredCapabilities; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -49,6 +51,7 @@ public class MobitruFacadeImpl implements MobitruFacade private static final Logger LOGGER = LoggerFactory.getLogger(MobitruFacadeImpl.class); private static final String UDID = "udid"; + private static final String APPIUM_UDID = "appium:udid"; private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper() .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); @@ -72,10 +75,25 @@ public String takeDevice(DesiredCapabilities desiredCapabilities) throws Mobitru List foundDevices = findDevices(deviceSearchParameters); return takeDevice(foundDevices); } + + Map capabilities = desiredCapabilities.asMap(); + Object deviceUdid = capabilities.getOrDefault(APPIUM_UDID, capabilities.get(UDID)); + if (deviceUdid != null) + { + //use different API in case if udid is provided in capabilities + //it's required in some cases like if the device is already taken + LOGGER.info("Trying to take device with udid {}", deviceUdid); + return takeDevice(() -> mobitruClient.takeDeviceBySerial(String.valueOf(deviceUdid)), + () -> "Unable to take device with udid " + deviceUdid, getDefaultDeviceWaiter()); + } Device device = new Device(); device.setDesiredCapabilities(desiredCapabilities.asMap()); - Waiter deviceWaiter = new DurationBasedWaiter(new WaitMode(waitForDeviceTimeout, RETRY_TIMES)); - return takeDevice(device, deviceWaiter); + return takeDevice(device, getDefaultDeviceWaiter()); + } + + private Waiter getDefaultDeviceWaiter() + { + return new DurationBasedWaiter(new WaitMode(waitForDeviceTimeout, RETRY_TIMES)); } @Override @@ -95,10 +113,17 @@ private String takeDevice(Device device, Waiter deviceWaiter) throws MobitruOper { LOGGER.info("Trying to take device with configuration {}", device); String capabilities = performMapperOperation(mapper -> mapper.writeValueAsString(device)); + return takeDevice(() -> mobitruClient.takeDevice(capabilities), + () -> "Unable to take device with configuration " + capabilities, deviceWaiter); + } + + private String takeDevice(FailableSupplier takeDeviceActions, + Supplier unableToTakeDeviceErrorMessage, Waiter deviceWaiter) throws MobitruOperationException + { byte[] receivedDevice = deviceWaiter.wait(() -> { try { - return mobitruClient.takeDevice(capabilities); + return takeDeviceActions.get(); } catch (MobitruDeviceTakeException e) { @@ -108,7 +133,7 @@ private String takeDevice(Device device, Waiter deviceWaiter) throws MobitruOper }, Objects::nonNull); if (null == receivedDevice) { - throw new MobitruDeviceTakeException(String.format("Unable to take device with configuration %s", device)); + throw new MobitruDeviceTakeException(unableToTakeDeviceErrorMessage.get()); } Device takenDevice = performMapperOperation(mapper -> mapper.readValue(receivedDevice, Device.class)); LOGGER.info("Device with configuration {} is taken", takenDevice); @@ -195,7 +220,7 @@ private boolean isSearchForDevice(DesiredCapabilities desiredCapabilities) .anyMatch(key -> key.startsWith("mobitru-device-search:")); if (containsSearchCapabilities) { - Optional conflictingCapability = Stream.of(UDID, "appium:udid", "deviceName", "appium:deviceName") + Optional conflictingCapability = Stream.of(UDID, APPIUM_UDID, "deviceName", "appium:deviceName") .filter(capabilities::containsKey).findFirst(); Validate.isTrue(conflictingCapability.isEmpty(), "Conflicting capabilities are found. `%s` capability can not be specified along with " diff --git a/vividus-plugin-mobitru/src/test/java/org/vividus/mobitru/client/MobitruClientTests.java b/vividus-plugin-mobitru/src/test/java/org/vividus/mobitru/client/MobitruClientTests.java index fe2972a57b..ef3bb9e2b4 100644 --- a/vividus-plugin-mobitru/src/test/java/org/vividus/mobitru/client/MobitruClientTests.java +++ b/vividus-plugin-mobitru/src/test/java/org/vividus/mobitru/client/MobitruClientTests.java @@ -54,6 +54,7 @@ class MobitruClientTests private static final String DEVICE_ENDPOINT = "/billing/unit/vividus/automation/api/device/deviceid"; private static final String DEVICE_ID = "deviceid"; private static final String TAKE_DEVICE_ENDPOINT = "/billing/unit/vividus/automation/api/device"; + private static final String UDID = "Z3CT103D2DZ"; @Mock private IHttpClient httpClient; @Mock private HttpResponse httpResponse; @@ -109,6 +110,25 @@ void shouldTakeDevice() throws IOException, MobitruOperationException } } + @Test + void shouldTakeDeviceBySerial() throws IOException, MobitruOperationException + { + var builder = mock(HttpRequestBuilder.class); + ClassicHttpRequest httpRequest = mock(); + try (MockedStatic builderMock = Mockito.mockStatic(HttpRequestBuilder.class)) + { + builderMock.when(HttpRequestBuilder::create).thenReturn(builder); + when(builder.withEndpoint(ENDPOINT)).thenReturn(builder); + when(builder.withHttpMethod(HttpMethod.POST)).thenReturn(builder); + when(builder.withRelativeUrl(TAKE_DEVICE_ENDPOINT.concat("/").concat(UDID))).thenReturn(builder); + when(builder.build()).thenReturn(httpRequest); + when(httpClient.execute(httpRequest)).thenReturn(httpResponse); + when(httpResponse.getResponseBody()).thenReturn(RESPONSE); + when(httpResponse.getStatusCode()).thenReturn(HttpStatus.SC_OK); + assertArrayEquals(RESPONSE, mobitruClient.takeDeviceBySerial(UDID)); + } + } + @Test void shouldThrowDeviceExceptionIfInvalidStatusCodeReturned() throws IOException { diff --git a/vividus-plugin-mobitru/src/test/java/org/vividus/mobitru/client/MobitruFacadeImplTests.java b/vividus-plugin-mobitru/src/test/java/org/vividus/mobitru/client/MobitruFacadeImplTests.java index 8f61fc101d..de16cb9f77 100644 --- a/vividus-plugin-mobitru/src/test/java/org/vividus/mobitru/client/MobitruFacadeImplTests.java +++ b/vividus-plugin-mobitru/src/test/java/org/vividus/mobitru/client/MobitruFacadeImplTests.java @@ -32,6 +32,9 @@ import java.util.List; import java.util.Map; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; import com.github.valfirst.slf4jtest.LoggingEvent; import com.github.valfirst.slf4jtest.TestLogger; import com.github.valfirst.slf4jtest.TestLoggerFactory; @@ -56,20 +59,30 @@ class MobitruFacadeImplTests { private static final String DEVICE_TAKEN_MESSAGE = "Device with configuration {} is taken"; private static final String TRYING_TO_TAKE_DEVICE_MESSAGE = "Trying to take device with configuration {}"; + private static final String TRYING_TO_TAKE_DEVICE_UDID_MESSAGE = "Trying to take device with udid {}"; private static final String RETRY_TO_TAKE_DEVICE_MESSAGE = "Unable to take device, retrying attempt."; - private static final String UNABLE_TO_TAKE_DEVICE_ERROR_FORMAT = "Unable to take device with configuration %s"; + private static final String UNABLE_TO_TAKE_DEVICE_WITH_CONFIGURATION_ERROR_FORMAT = + "Unable to take device with configuration %s"; + private static final String UNABLE_TO_TAKE_DEVICE_WITH_UDID_ERROR_FORMAT = "Unable to take device with udid %s"; private static final String UDID = "Z3CT103D2DZ"; private static final String DEVICE_TYPE_CAPABILITY_NAME = "mobitru-device-search:type"; private static final String PHONE = "phone"; private static final String PLATFORM_NAME = "platformName"; + private static final String UDID_CAP = "udid"; private static final String IOS = "ios"; private static final String ANDROID = "android"; private static final String NO_DEVICE = "no device"; private static final String CAPABILITIES_JSON_PREFIX = "{\"desiredCapabilities\":{\"platformName\":\"Android\","; - private static final String CAPABILITIES_JSON = CAPABILITIES_JSON_PREFIX + private static final String CAPABILITIES_IOS_PLATFORM = "{\"desiredCapabilities\":{\"platformName\":\"IOS\"}}"; + private static final String CAPABILITIES_WITHOUT_UDID_JSON = CAPABILITIES_JSON_PREFIX + + "\"platformVersion\":\"12\",\"deviceName\":\"SAMSUNG SM-G998B\"}}"; + private static final String CAPABILITIES_WITH_UDID_JSON = CAPABILITIES_JSON_PREFIX + "\"platformVersion\":\"12\",\"deviceName\":\"SAMSUNG SM-G998B\",\"udid\":\"Z3CT103D2DZ\"}}"; private static final Map DEVICE_SEARCH_PARAMETERS = Map.of("type", PHONE); + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper() + .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); + private static final TestLogger LOGGER = TestLoggerFactory.getTestLogger(MobitruFacadeImpl.class); @Mock private MobitruClient mobitruClient; @@ -77,7 +90,7 @@ class MobitruFacadeImplTests @InjectMocks private MobitruFacadeImpl mobitruFacadeImpl; @Test - void shouldFindDeviceThenTakeItAndProvideItsUdid() throws MobitruOperationException + void shouldFindDeviceThenTakeItAndProvideItsUdid() throws MobitruOperationException, JsonProcessingException { mobitruFacadeImpl.setWaitForDeviceTimeout(Duration.ofSeconds(20)); var deviceSearchException = new MobitruDeviceSearchException(NO_DEVICE); @@ -87,11 +100,11 @@ void shouldFindDeviceThenTakeItAndProvideItsUdid() throws MobitruOperationExcept var desiredCapabilities = new DesiredCapabilities( Map.of(PLATFORM_NAME, ANDROID, DEVICE_TYPE_CAPABILITY_NAME, PHONE)); when(mobitruClient.findDevices(ANDROID, DEVICE_SEARCH_PARAMETERS)).thenThrow(deviceSearchException) - .thenReturn(('[' + failedToTakeDeviceCapabilitiesJson + "," + CAPABILITIES_JSON + ']') + .thenReturn(('[' + failedToTakeDeviceCapabilitiesJson + "," + CAPABILITIES_WITH_UDID_JSON + ']') .getBytes(StandardCharsets.UTF_8)); when(mobitruClient.takeDevice(failedToTakeDeviceCapabilitiesJson)).thenThrow(deviceTakeException); - when(mobitruClient.takeDevice(CAPABILITIES_JSON)) - .thenReturn(CAPABILITIES_JSON.getBytes(StandardCharsets.UTF_8)); + when(mobitruClient.takeDevice(CAPABILITIES_WITH_UDID_JSON)) + .thenReturn(CAPABILITIES_WITH_UDID_JSON.getBytes(StandardCharsets.UTF_8)); assertEquals(UDID, mobitruFacadeImpl.takeDevice(desiredCapabilities)); var failedTakeDevice = createDevice("13", "GOOGLE PIXEL 6", "777"); var takenDevice = getTestDevice(); @@ -102,11 +115,29 @@ void shouldFindDeviceThenTakeItAndProvideItsUdid() throws MobitruOperationExcept + System.lineSeparator() + "Device 2: " + takenDevice), LoggingEvent.info(TRYING_TO_TAKE_DEVICE_MESSAGE, failedTakeDevice), LoggingEvent.warn(deviceTakeException, RETRY_TO_TAKE_DEVICE_MESSAGE), - LoggingEvent.warn(String.format(UNABLE_TO_TAKE_DEVICE_ERROR_FORMAT, failedTakeDevice)), + LoggingEvent.warn(UNABLE_TO_TAKE_DEVICE_WITH_CONFIGURATION_ERROR_FORMAT.formatted( + OBJECT_MAPPER.writeValueAsString(failedTakeDevice))), LoggingEvent.info(TRYING_TO_TAKE_DEVICE_MESSAGE, takenDevice), LoggingEvent.info(DEVICE_TAKEN_MESSAGE, takenDevice)), LOGGER.getLoggingEvents()); } + @ParameterizedTest + @ValueSource(strings = { "appium:udid", UDID_CAP}) + void shouldTakeDeviceByUdidAndProvideItsUdid(String capName) throws MobitruOperationException + { + mobitruFacadeImpl.setWaitForDeviceTimeout(Duration.ofSeconds(20)); + var deviceTakeException = new MobitruDeviceTakeException(NO_DEVICE); + var desiredCapabilities = new DesiredCapabilities(Map.of(capName, UDID)); + when(mobitruClient.takeDeviceBySerial(UDID)).thenThrow(deviceTakeException). + thenReturn(CAPABILITIES_WITH_UDID_JSON.getBytes(StandardCharsets.UTF_8)); + assertEquals(UDID, mobitruFacadeImpl.takeDevice(desiredCapabilities)); + var takenDevice = getTestDevice(); + assertEquals(List.of( + LoggingEvent.info(TRYING_TO_TAKE_DEVICE_UDID_MESSAGE, UDID), + LoggingEvent.warn(deviceTakeException, RETRY_TO_TAKE_DEVICE_MESSAGE), + LoggingEvent.info(DEVICE_TAKEN_MESSAGE, takenDevice)), LOGGER.getLoggingEvents()); + } + @Test void shouldTakeDeviceInUseAndProvideItsUdId() throws MobitruOperationException { @@ -117,7 +148,7 @@ void shouldTakeDeviceInUseAndProvideItsUdId() throws MobitruOperationException requestedDevice.setDesiredCapabilities(desiredCapabilities.asMap()); when(mobitruClient.takeDevice("{\"desiredCapabilities\":{\"platformName\":\"ANDROID\"}}")) .thenThrow(exception) - .thenReturn(CAPABILITIES_JSON.getBytes(StandardCharsets.UTF_8)); + .thenReturn(CAPABILITIES_WITH_UDID_JSON.getBytes(StandardCharsets.UTF_8)); assertEquals(UDID, mobitruFacadeImpl.takeDevice(desiredCapabilities)); assertEquals(List.of(LoggingEvent.info(TRYING_TO_TAKE_DEVICE_MESSAGE, requestedDevice), @@ -126,7 +157,7 @@ void shouldTakeDeviceInUseAndProvideItsUdId() throws MobitruOperationException } @ParameterizedTest - @ValueSource(strings = { "appium:udid", "udid", "deviceName", "appium:deviceName" }) + @ValueSource(strings = { "appium:udid", UDID_CAP, "deviceName", "appium:deviceName" }) void shouldThrowExceptionIfConflictingCapabilities(String capabilityName) { mobitruFacadeImpl.setWaitForDeviceTimeout(Duration.ofSeconds(20)); @@ -153,25 +184,37 @@ void shouldThrowExceptionIfFoundDeviceNotTaken() throws MobitruOperationExceptio { mobitruFacadeImpl.setWaitForDeviceTimeout(Duration.ofSeconds(1)); when(mobitruClient.findDevices(ANDROID, DEVICE_SEARCH_PARAMETERS)) - .thenReturn(('[' + CAPABILITIES_JSON + ']').getBytes(StandardCharsets.UTF_8)); - when(mobitruClient.takeDevice(CAPABILITIES_JSON)) + .thenReturn(('[' + CAPABILITIES_WITHOUT_UDID_JSON + ']').getBytes(StandardCharsets.UTF_8)); + when(mobitruClient.takeDevice(CAPABILITIES_WITHOUT_UDID_JSON)) .thenThrow(new MobitruDeviceTakeException(NO_DEVICE)); var capabilities = new DesiredCapabilities(Map.of(PLATFORM_NAME, ANDROID, DEVICE_TYPE_CAPABILITY_NAME, PHONE)); var exception = assertThrows(MobitruDeviceTakeException.class, () -> mobitruFacadeImpl.takeDevice(capabilities)); - assertEquals(String.format(UNABLE_TO_TAKE_DEVICE_ERROR_FORMAT, getTestDevice()), exception.getMessage()); + assertEquals(UNABLE_TO_TAKE_DEVICE_WITH_CONFIGURATION_ERROR_FORMAT.formatted(CAPABILITIES_WITHOUT_UDID_JSON), + exception.getMessage()); + } + + @Test + void shouldThrowExceptionIfNoDeviceWithUdid() throws MobitruOperationException + { + mobitruFacadeImpl.setWaitForDeviceTimeout(Duration.ofSeconds(1)); + var desiredCapabilities = new DesiredCapabilities(Map.of(UDID_CAP, UDID)); + when(mobitruClient.takeDeviceBySerial(UDID)).thenThrow(new MobitruDeviceTakeException(NO_DEVICE)); + var exception = assertThrows(MobitruDeviceTakeException.class, + () -> mobitruFacadeImpl.takeDevice(desiredCapabilities)); + assertEquals(UNABLE_TO_TAKE_DEVICE_WITH_UDID_ERROR_FORMAT.formatted(UDID), exception.getMessage()); } @Test void shouldThrowExceptionIfNoDeviceTaken() throws MobitruOperationException { mobitruFacadeImpl.setWaitForDeviceTimeout(Duration.ofSeconds(1)); - when(mobitruClient.takeDevice("{\"desiredCapabilities\":{\"platformName\":\"IOS\"}}")) + when(mobitruClient.takeDevice(CAPABILITIES_IOS_PLATFORM)) .thenThrow(new MobitruDeviceTakeException(NO_DEVICE)); var capabilities = new DesiredCapabilities(Map.of(PLATFORM_NAME, IOS)); var exception = assertThrows(MobitruDeviceTakeException.class, () -> mobitruFacadeImpl.takeDevice(capabilities)); - assertEquals("Unable to take device with configuration {desiredCapabilities={platformName=IOS}}", + assertEquals(UNABLE_TO_TAKE_DEVICE_WITH_CONFIGURATION_ERROR_FORMAT.formatted(CAPABILITIES_IOS_PLATFORM), exception.getMessage()); } @@ -227,7 +270,7 @@ private Device createDevice(String platformVersion, String deviceName, String ud desiredCapabilities.put(PLATFORM_NAME, "Android"); desiredCapabilities.put("platformVersion", platformVersion); desiredCapabilities.put("deviceName", deviceName); - desiredCapabilities.put("udid", udid); + desiredCapabilities.put(UDID_CAP, udid); Device device = new Device(); device.setDesiredCapabilities(desiredCapabilities); return device;