Библиотека сайта rus-linux.net
Проект Selenium WebDriver
Автор:Simon Stewart
Перевод: Н.Ромоданов
16.7. Драйвер IE
Internet Explorer является интересным браузером. Он сконструирован из ряда интерфейсов COM, работающих как единый оркестр. Это происходит посюду в движке Javascript, где знакомые нам переменные языка Javascript на самом деле ссылаются к лежащим в их основе экземплярам COM. Переменная window в Javascript является IHTMLWindow. Переменная document является экземпляром COM-интерфейса IHTMLDocument. Фирма Microsoft, в процессе разработки своих браузеров, проделала отличную работу по сохранении существующего поведения интерфейсов COM. Это означает, что если приложение работает с COM-классами, предоставляемыми IE6, то оно будет также продолжать работать с IE9.
Драйвер Internet Explorer имеет архитектуру, которая развивалась на протяжении длительного времени. Одним из основных направлений затрат усилий на ее разработку было соблюдение требования не использовать инсталлятор. Это немного необычное требование, поэтому, возможно, требуется некоторое пояснение. Первая причина не использовать инсталлятор объясняется тем, что он осложняет драйверу WebDriver проходит «5 минутный тест», когда разработчик загружает пакет и испытывает его в течение непродолжительного периода времени. Более того, у пользователей WebDriver относительно типичная ситуация, когда на их машинах нельзя устанавливать программное обеспечение. Это также означает, что когда нужно начинать тестирование проекта с использованием IE, никто не должен помнить, как входить на постоянно работающий сервер с тем, чтобы запустить инсталлятор. Наконец, запускаемые инсталляторы просто не соответствуют культуре использования некоторых языков. Обычная идиома языка Java состоит просто в указании файлов JAR в переменной CLASSPATH, и, по моему опыту, те библиотеки, для которых требуются инсталляторы, как правило, не столь любимы и не столь широко применимы.
Итак, инсталлятора нет. И это решение ведет к следующему следствию.
Типичным языком, используемым для программирования на Windows, может быть любой, которые работает на платформе .Net, например, C#. Драйвер IE интегрируется с IE с помощью интерфейсов автоматизации IE COM Automation, которые поставляются с каждой версией Windows. В частности, мы используем интерфейсы COM из нативных DLL-библиотек MSHTML и ShDocVw, которые являются частью IE. До C# 4, взаимодействие CLR/COM достигалось за счет использования отдельных сборокPrimary Interop Assemblies (сборок PIA). Сборка PIA является, в сущности, сгенерированным мостиком между управляемым миром CLR и миром COM.
К несчастью, использование C# 4 будет означать использование очень современной среды исполнения .Net, а многие компании избегают жить на переднем крае разработок, предпочитая стабильность и известные проблемы более старых версий. При использовании C# 4 мы автоматически исключаем определенный процент нашей базы пользователей. Есть и другие недостатки в использовании PIA. Рассмотрим лицензионные ограничения. После консультаций с Microsoft, стало ясно, что проект Selenium не будет иметь прав на распространение сборок PIA ни в виде библиотек MSHTML, ни в виде библиотек ShDocVw. Даже если эти права были бы предоставлены, каждая установка Windows и IE имеет уникальное сочетание этих библиотек, а это означает, что нужно было бы поставлять огромное количество лишних библиотек. Создание сборок PIA по требованию на клиентской машине также не для начинающих разработчиков, поскольку для этого требуются инструментальные средства, которых может не быть на компьютере обычного пользователя.
Поэтому, хотя могло бы быть заманчиво использовать язык C# для создания большей части кода, он не подходил. Нам нужно было использовать что-то естественное, по крайней мере, для взаимодействия с IE. Следующим естественным выбором для этого стал язык C++, и это был тот язык, который мы, в конце концов, выбрали. При использовании языка C++ у нас преимущество в том, что не нужно использовать сборки PIA, но это означает, что в случае, если мы не выполняем с библиотекой Visual Studio C + + Runtime DLL статическую компоновку, мы должны ее распространять. Поскольку для того, чтобы библиотека DLL была доступна, нам требуется запускать инсталлятор, мы статически компонуем нашу библиотеку для взаимодействия с IE.
Т.е. цена, которую нужно заплатить за требование не использовать инсталлятор, слишком высокая. Но, если вернуться к теме, где явно видна вся сложность, то в ней инвестиции стоящие, поскольку они делают жизнь наших пользователей значительно проще. Это решение, которое мы пересмотрели, поскольку преимущества для пользователя является компромиссом с тем, что, как оказывается, количество тех, кто способен внести свой вклад в современный проект на языке C++ с открытым исходным кодом значительно меньше, чем тех, кто может внести свой вклад в эквивалентный проект на языке C#.
На рис.16.5 показана исходная схема драйвера IE
Рис.16.5: Исходная схема драйвера IE
Если начать с самой правой части этой цепочки, то видно, что мы используем интерфейсы автоматизации IE - COM Automation. Для того, чтобы с ними на концептуальном уровне было легче иметь дело, мы поместили эти интерфейсы внутрь набора классов C++, которые непосредственно отображаются в WebDriver API. Чтобы позволить классам языка Java взаимодействовать с C++, мы воспользовались технологией JNI, причем при реализации методов JNI использовались абстракции C++ интерфейсов COM.
Пока язык Java был единственным языковым клиентом, этот подход работал достаточно хорошо, но в случае, если для каждого языка, который нам требовалось поддерживать, нужно было заменять базовую библиотеку, он мог бы быть источником боли и сложности. Итак, хотя технология JNI работала, она не обеспечивала необходимый уровень абстракции.
Что было правильным уровнем абстракции? В каждом языке, который нам требовалось поддерживать, был механизм непосредственного вызова кода на языке C. В языке C#, он имеет вид PInvoke. В языке Ruby есть FFI, а в языке Python есть ctypes. В мире Java, есть отличная библиотека, которая называется JNA (Java Native Architecture — нативная архитектура языка Java). Нам нужно было предоставить наш интерфейс API с помощью этого наименьшего общего деноминатора. Это было сделано путем преобразования нашей объектной модели в более плоскую модель, использующую двух или трехбуквенный префиксы, которые указывают на «домашний интерфейс» метода: wd - для WebDriver и wde - для элемента WebDriver Element. Таким образом WebDriver.get стал wdGet, а WebElement.getText стал wdeGetText. Каждый метод возвращает целое число, представляющее собой код состояния, и параметры out, позволяющие функциям возвращать более конкретные данные. Таким образом, мы пришли к сигнатурами методов, например:
int wdeGetAttribute(WebDriver*, WebElement*, const wchar_t*, StringWrapper**)
При вызове кода, типы сигнатур WebDriver, WebElement и StringWrapper не показываются: для того, чтобы было понятно, какое значение должно использоваться в качестве конкретного параметра, мы отразили различие в интерфейсе AP, хотя проще было бы использовать описатель «void *». Также вам должно быть видно, что для текста мы использовали символы расширенного формата, т. к. нам нужно было работать с текстовыми свойствами на различных языках.
На стороне языка Java мы предоставили доступ к функциям этой библиотеки через интерфейс, который затем мы адаптировали так, что он стал выглядеть как обычный объектно-ориентированный интерфейс, представляемый пакетом WebDriver. Например, определение на языке Java метода getAttribute выглядит следующим образом:
public String getAttribute(String name) { PointerByReference wrapper = new PointerByReference(); int result = lib.wdeGetAttribute( parent.getDriverPointer(), element, new WString(name), wrapper); errors.verifyErrorCode(result, "get attribute of"); return wrapper.getValue() == null ? null : new StringWrapper(lib, wrapper).toString(); }
Это привело к использованию схемы, показанной на рис.16.6.
Рис.16.6: Измененная схема драйвера IE
Пока все тесты исполнялись на локальном компьютере, все это работало хорошо, но как только мы начали использовать драйвер IE в для WebDriver дистанционно, у нас стали в случайном порядке возникать блокировки. Мы проследили эту проблему и обнаружили ограничение в интерфейсах автоматизации IE COM Automation. Эти интерфейсы предназначены для использования в «однопотоковой» модели. В сущности все это сводится к требованию, чтобы интерфейс каждый раз вызывался из того же самого потока. Когда запуск осуществляется на локальной машине, то выполняется по умолчанию. Однако сервера приложений Java для того, что распределить возможную нагрузку, и запускают несколько потоков. Что в итоге? У нас не было способа, чтобы всегда быть уверенным, что для доступа к драйверу IE будет использоваться один и тот же поток.
Одним из способов решения этой проблемы мог бы быть запуск драйвера IE с помощью модуля, осуществляющего его запуск в одном и том же потоке, и сериализация всего доступа через Futures сервера приложений , и на некоторое время мы выбрали эту схему. Но, как оказалось, нагружать всей этой сложностью вызывающий код является неправильным решение, причем все равно слишком легко представить себе ситуации, когда драйвер IE будут случайно использовать из нескольких потоков. Мы решили упрятать эту сложность поглубже в самом драйвере. Мы сделали это, поместив экземпляр IE в отдельный поток и использовав PostThreadMessage из интерфеса Win32 API для передачи сообщений через границу потока. Таким образом, на момент написания статьи, схема драйвера IE выглядит так, как это показано на рис.16.7.
Рис.16.7: Схема драйвера IE версии Selenium 2.0 alpha 7
Это не та схема, которую я бы выбрал добровольно, но она дает возможность работать и выдерживать те ужасы, которые наши пользователи могут в нее добавить.
Один из недостатков этой схемы заключается в том, что трудно определить, намертво ли заблокировал сам себя экземпляр IE. Это может произойти в случае, если во время взаимодействия с DOM открывается модальное диалоговое окно, либо это может произойти в случае, если катастрофическая ситуация возникнет с другой стороны границы потока. Поэтому мы имеем таймаут, связанный с каждый сообщением в потоке, которое мы передаем, и он устанавливается равным относительно щедрым 2 минутам. По обратной реакции пользователей, получаемой из списка рассылки, это допущение, хотя в целом и верно, но не всегда правильное, и в более поздних версиях драйвера IE, вполне возможно, таймаут можно будет задавать самостоятельно.
Еще один недостаток состоит в том, что отладка внутренних механизмов кода может стать крайне проблематичной, требующей сочетания скорости (в конце концов, у вас есть две минуты, чтобы оттрассировать настолько, насколько это будет возможным), разумного использования точек останова и понимание предполагаемого пути, где будет происходить пересечение границы потока. Излишне говорить, что в проекте с открытым кодом и со многими другими интересными проблемами, которые необходимо решать, с шероховатостями такого рода возиться совсем неинтересно. В системе это существенно сокращает фактор автобуса и меня, как менеджера проекта, это беспокоит.
Чтобы с этим справиться, драйвер IE точно также как и драйвер Firefox и Selenium Core, все больше и больше перемещается туда, где находятся атомы автоматизации Automation Atoms. Мы делаем это с помощью компилирования каждого из атомов, который мы планируем использовать, и подготовки его в виде заголовочного файла C++, где каждая функция представлена в виде константы. Мы подготавливаем Javascript с тем, чтобы во время выполнения он мог выполнять эти константы. Такой подход означает, что мы можем разработать и протестировать значительное количество кода в драйвере IE без необходимости привлечения компилятора C, а это, в свою очередь, позволяет гораздо большему числу разработчиков внести свой собственный вклад в поиск и исправление ошибок. Целью, в конце концов, является оставить в нативном виде только интерфейсы API, используемые при взаимодействии, и как можно больше работы возложить на атомы.
Еще один подход, которые мы исследуем, состоящий в переписывании драйвера IE для того, чтобы добавить легкий сервер HTTP, позволяющий нам использовать его как WebDriver, работающий дистанционно. Если это произойдет, то мы сможем избавиться от многих сложностей, связанных с границами потоков, сократить общее количество необходимого кода и сделать процесс управления значительно проще.
Продолжение статьи - Selenium RC.