Как всегда, оригинал в блоге на буржуйском.
В одной из прошлогодних тем был затронут вопрос автоматизации поставки драйверов в проект.
Тогда я активно отговаривал господина @dzhariy от этой, как мне казалось, безнадежной затеи.
Прошел год, и вот в один из теплых летних вечеров, просматривая очередные интересные исходники - docker-selenium, мне стало очень интересно, как же ребята поставляют драйвера контейнеру. Не сильно углубляясь в детали, я сразу же наткнулся на весьма интересный скрипт, а именно строку:
wget --no-verbose http://selenium-release.storage.googleapis.com/2.47/selenium-server-standalone-2.47.1.jar -O /opt/selenium/selenium-server-standalone.jar
Т.е. нужный артефакт выгребается обычным GET запросом. Достаточно лишь знать прямой URL. Это подогрело мой интерес и я заглянул в рут ресурса. И что же я там нашел? С виду - какая-то бесполезная для рядовых юзеров XML. Но только не для нас - QAA-инженеров, которые бы сразу заприметили целый кладезь полезной информации. И что же тут полезного? - спросите вы. А вот что:
<Contents>
<Key>2.47/IEDriverServer_Win32_2.47.0.zip</Key>
</Contents>
<Contents>
<Key>2.47/IEDriverServer_x64_2.47.0.zip</Key>
</Contents>
<Contents>
<Key>2.47/selenium-server-standalone-2.47.1.jar</Key>
</Contents>
Наша XML содержит недостающую часть мозаики - Key, который путем конкатенации с рутом дает нам прямой URL для закачки нужного компонента, будь то драйвер, или грид. Я тут же потопал в рут хром сторэджа, где увидел точно такую же картину.
И тут у меня в голове созрела идея: а почему бы нам не парсить эту самую XML для выгребания самой свежей версии драйверов и грида? Получая нужный URL, мы бы смогли без особого труда наладить постоянную поставку артефактов.
Итак, цель ясна, желание есть, значит пишем Selenium content supplier.
Начнем, пожалуй, с модели. Содержимое XML то нужно куда-то записывать, ведь так? Благо, у нас под рукой есть наша любимая IntelliJ IDEA, которой подавай лишь XML, а взамен она нам совершенно бескорыстно сгенерит и XSD
схему, а там - и сами entities
по новоиспеченной схеме.
Итого, всего в несколько кликов мы имеем готовую модель, которая с радостью примет наш респонс на GET запрос к руту → http://selenium-release.storage.googleapis.com.
Начало неплохое, не так ли? Теперь было бы неплохо подготовить и клиентскую часть:
private Optional<String> getLatestPath(final Content contentType) {
try {
return Optional.ofNullable(client.target(contentType.getUrl())
.request(MediaType.APPLICATION_XML)
.get()
.readEntity(ListBucketResultType.class))
.map(contentContainer -> getLatestPath(contentContainer, contentType))
.orElse(Optional.<String>empty());
} catch (Exception e) {
CLIENT_LOGGER.severe("Unable to get resource content: " + e.getMessage());
return Optional.empty();
}
}
private Optional<String> getLatestPath(final ListBucketResultType contentContainer, final Content contentType) {
return contentContainer.getContents().stream()
.map(ContentsType::getKey)
.filter(key -> key.contains(contentType.getName()))
.reduce((key1, key2) -> key2);
}
На выходе у нас получилось 2 метода, задача которых - получить содержимое XML, отфильтровать его по ключу и вернуть имя самого свежего артефакта.
Вам наверное интересно, что же за тип такой - Content? По сути, это обычный интерфейс, разработанный для предоставления доступа к оберткам над основными Selenium
ресурсами - ChromeDriver, IEDriver и SeleniumServer. Ввиду обилия доступных версий и платформ без дополнительных оберток мы потеряли бы всю универсальность.
public interface Content {
enum SeleniumContent {
IE_DRIVER_X32(new IEDriver(IE_DRIVER, OS.WIN32)),
IE_DRIVER_X64(new IEDriver(IE_DRIVER, OS.WIN64)),
CHROME_DRIVER_WIN32(new ChromeDriver(CHROME_DRIVER, OS.WIN32)),
CHROME_DRIVER_MAC32(new ChromeDriver(CHROME_DRIVER, OS.MAC32)),
CHROME_DRIVER_LINUX32(new ChromeDriver(CHROME_DRIVER, OS.LINUX32)),
CHROME_DRIVER_LINUX64(new ChromeDriver(CHROME_DRIVER, OS.LINUX64)),
SELENIUM_SERVER(new SeleniumServer(SELENIUM_SERVER_STANDALONE));
private Content content;
SeleniumContent(Content content) {
this.content = content;
}
public Content getContent() {
return content;
}
public String toString() {
return content.getName();
}
}
enum OS {
WIN32("Win32"),
WIN64("x64"),
LINUX32("linux32"),
LINUX64("linux64"),
MAC32("mac32");
private String name;
OS(final String name) {
this.name = name;
}
public String getName() {
return name;
}
}
String IE_DRIVER = "IEDriverServer";
String CHROME_DRIVER = "chromedriver";
String SELENIUM_SERVER_STANDALONE = "selenium-server-standalone";
default String getUrl() {
return "http://selenium-release.storage.googleapis.com";
}
String getName();
}
В учебных целях были использованы лишь некоторые доступные ресурсы, но при желании конечно же сей список можно и расширить.
Содержимое оберток весьма примитивно, посему листинг приводить не стану. Сорсы будут доступны по ссылке ниже.
Итак, теперь самое время познакомиться с клиентской частью загрузчика:
public String downloadFile(final SeleniumContent contentType, final String localFolder) {
final String[] latestPath = getLatestPath(contentType.getContent())
.map(path -> path.split("/"))
.orElse(new String[0]);
if (latestPath.length < 2) {
throw new IllegalArgumentException("Unable to parse provided end-point path: " + Arrays.asList(latestPath));
}
final String version = latestPath[0];
final String fileName = latestPath[1];
String prettifiedFileName = separatorsToSystem(Optional.ofNullable(localFolder)
.orElse(System.getProperty("user.home")) + File.separator + fileName);
CLIENT_LOGGER.info("Detected '" + fileName + "' -> Trying to download " + version + " version...");
try (final InputStream inputStream = client.target(contentType.getContent().getUrl())
.path(version)
.path(fileName)
.request()
.get(InputStream.class);
final FileOutputStream fileOutputStream = new FileOutputStream(new File(prettifiedFileName))) {
copy(inputStream, fileOutputStream);
fileOutputStream.flush();
CLIENT_LOGGER.info("'" + prettifiedFileName + "' has been successfully saved!");
} catch (Exception e) {
CLIENT_LOGGER.severe("Unable to save a file: " + e.getMessage());
prettifiedFileName = "";
}
return prettifiedFileName;
}
Алгоритм весьма прост и уже обсуждался выше:
- Получаем значение ключа по заданной маске (содержимое наших оберток).
- Парсим ключ, получая версию и собственно само имя файла.
- Формируем путь к сохраняемому файлу.
- Отправляем GET запрос на получение файла по переданному ключу.
- Результат читаем в
InputStream
, который в последствии копируем вFileOutputStream
при помощи апачевского метода copy. - Возвращаем имя файла для последующей обработки.
Возникает логичный вопрос - а какой может быть последующая обработка? Файл мы скачали. Чего еще надо для счастья? А надо бы добавить фичу распаковки zip архива, ибо драйвера то у нас поставляются в запакованном виде. Не долго думая, можно применить уже существующую библиотеку zip4j для реализации сего коварного плана. Можно конечно обойтись и стандартными API, но хотелось все же минимизировать кол-во писанины.
public static Status unZipItems(final String filePath) {
return unZipItems(filePath, getFullPath(filePath) + getBaseName(filePath));
}
public static Status unZipItems(final String filePath, final String outputFolder) {
Status response;
if (getExtension(filePath).equals("zip")) {
try {
new ZipFile(filePath).extractAll(outputFolder);
IO_LOGGER.info("All files have been successfully extracted!");
response = Status.OK;
} catch (ZipException e) {
IO_LOGGER.severe("Unable extract files from " + filePath + ": " + e.getMessage());
response = Status.INTERNAL_SERVER_ERROR;
}
} else {
response = Status.NOT_FOUND;
}
return response;
}
API достаточно примитивный и в особом представлении, я думаю, не нуждается.
Супер, теперь у нас есть полноценный загрузчик последних версий Selenium ресурсов. Но чего-то все равно не хватает… Вспоминая о гриде, мы наверняка задумались бы о целом батальоне подключенных к хабу нодов, которые тоже было бы неплохо обновлять новыми артефактами. Не вопрос. Пишем небольшой file upload
сервис → profit:
@POST
@Path("/upload")
@Consumes(MediaType.MULTIPART_FORM_DATA)
@Produces(MediaType.APPLICATION_JSON)
public Response uploadFile(final FormDataMultiPart form, @QueryParam("saveTo") final String saveToPath,
@QueryParam("unZip") final boolean unZip) {
return Response.status(form.getFields("file")
.parallelStream()
.flatMap(filePart -> saveFile(filePart.getValueAs(InputStream.class),
formatPath(filePart.getContentDisposition().getFileName(), saveToPath), unZip).stream())
.filter(response -> response.equals(Response.Status.INTERNAL_SERVER_ERROR))
.findAny()
.orElse(Response.Status.OK))
.build();
}
Сразу же расширяем возможности сервиса поддержкой multi-part данных (для множественного аплоада), разархивации, ну и конечно же было бы неплохо все это дело оптимизировать, распараллелив процесс сохранения файлов при помощи Java 8 parallelStream()
фичи.
Теперь посмотрим на клиентскую часть:
public void sendFileToRemoteAndUnZip(final List<String> filesPath, final String saveToPath) {
Response response = null;
final MultiPart multiPart = new MultiPart(MediaType.MULTIPART_FORM_DATA_TYPE);
filesPath.stream().forEach(path -> multiPart.bodyPart(new FileDataBodyPart("file",
new File(separatorsToSystem(path)), MediaType.APPLICATION_OCTET_STREAM_TYPE)));
try {
response = client.target("http://" + ip + ":" + port + "/selenium")
.path("content")
.path("upload")
.queryParam("saveTo", separatorsToSystem(saveToPath))
.queryParam("unZip", true)
.request(MediaType.APPLICATION_JSON_TYPE)
.post(Entity.entity(multiPart, multiPart.getMediaType()));
final int status = response.getStatus();
if (status == Response.Status.OK.getStatusCode()) {
CLIENT_LOGGER.info("File(-s) " + filesPath + " has been saved to " + separatorsToSystem(saveToPath) + " on " + ip);
} else {
CLIENT_LOGGER.severe("Unable to save or unZip file(-s) " + filesPath + " to " + separatorsToSystem(saveToPath) + " on " + ip + "; status = " + status);
}
} catch (Exception e) {
CLIENT_LOGGER.info("An error occurred while file(-s) uploading: " + e);
response = null;
} finally {
if (response != null) {
response.close();
}
}
}
Ничего военного, нечто подобное уже было когда-то предложено в SikuliX
.
Вот собственно и все. Т.к. сей библиотеки нет в официальном maven репозитории, вам придется самостоятельно собрать проект при помощи команды:
mvn clean install
На выходе получите сразу и рабочий сервер, и готовую клиентскую dependency в локальном репозитории.
Ну и конечно же результат:
@Test
public void downloadAndUnZipFilesLocally() {
Stream.of(SeleniumContent.values())
.parallel()
.forEach(content -> localSupplier.downloadAndUnZipFile(content, OUTPUT_PATH));
}
@Test
public void downloadFilesLocallyAndSendToRemote() {
Arrays.asList(SeleniumContent.SELENIUM_SERVER, SeleniumContent.CHROME_DRIVER_WIN32, SeleniumContent.IE_DRIVER_X32)
.parallelStream()
.forEach(content -> remoteSupplier.sendFileToRemoteAndUnZip(Collections.singletonList(
remoteSupplier.downloadAndUnZipFile(content, OUTPUT_PATH)), OUTPUT_PATH));
}
При этом, первый тест зальет все доступные артефакты последних версий, и тут же их распакует.
Второй - скачает 3 артефакта локально (аля поставка хабу), а затем отправит их прямиком удаленному сервису (аля ноду), и также распакует. Для ремоутной отправки естественно на ноде должен быть поднят selenium-supplier-server
, ждущий дальнейших указаний.
В целом, данную библиотеку можно скомбинировать с EnvironmentWatcher service для реализации следующего сценария: прибиваем все процессы (хабы / ноды / драйвера), обновляем ресурсы последними версиями, запускаем процессы уже с новыми артефактами.
Код selenium-supplier, и отдельно парочка примеров на GitHub.
Если данная библиотека окажется вам полезной, не забываем ставить лайки.
Ну и собственно мини-опрос о целесообразности развития данной библиотеки:
- Текущей реализации вполне достаточно.
- Не хватает определенных фич, я поделюсь идеями с автором.
- Я хочу принять участие в ее дальнейшем развитии.
- Хотелось бы вывести библиотеку на уровень CI плагина (включая любой из предыдущих пунктов).
- Это очень сложно, мне проще выкачивать все ручками.
0 участников