Виртуальные устройства ввода в тестировании
В нашей ОС появилась новая фича, позволяющая разработчикам и тестировщикам производить анализ в рамках методик безопасной разработки. Новый механизм позволяет реализовывать как разнообразные сценарии тестирования UI, так и выполнять emergency-сценарии управления интерфейсом без вмешательства в кодовую базу программно-аппаратных систем. Как всё это работает вы узнаете из этой статьи.
Предыстория
Достаточно давно, когда мы ещё только начинали внедрять методики безопасной разработки, одним из критически важных квестов, появившихся перед отделом тестирования, стал вопрос тестирования UI. Если тестируется веб-интерфейс, набор инструментов достаточно понятен и браузер-ориентирован. Если задача в контроле качества GUI приложения — полагаемся на системные механизмы. Но что делать, если требуется исследовать сами эти механизмы, доверие к которым априори отсутствует?
Вопрос доверия к инструментам и методам
Вопрос доверия в тестировании является ключевым. Допустим, что анализируется некий аппаратный сигнал с помощью осциллографа. Исследователь обычно исходит из того, что прибор показывает достаточно точные данные. Однако, если устройство не внушает доверия, его отдают на поверку или заменяют средства измерения.
Эта же логика применима и к методам тестирования, которые не могут быть эндосистемными. В противном случае их обоснованно можно считать частью самого объекта тестирования, а это уже вызывает серьёзные вопросы к состоятельности такого тестирования. Ведь при тестировании объект тестирования — сущность, в характеристиках которой мы сомневаемся (а иначе зачем мы тестируем?). На практике сопричастность такого рода обычно легко доказуема.
За счет того, что ЗОСРВ «Нейтрино» является микроядерной ОС, процесс декомпозиции и анализа задачи оказался не очень сложным. Поскольку все системные сервисы имеют независимые адресные пространства (aka процессы), подсистема ввода строго изолирована от графической и оконной подсистем и, в конечном счете, от приложений. В этом случае любые негуманные эксперименты над первой не могут оказать существенного влияния на остальные компоненты системы (или модули в рамках модульного тестирования). Справедливо и обратное, при тестировании UI оконных приложений, модулем, к которому есть первичное доверие, может выступать подсистема ввода. Синтезированное решение появилось в виде разработки простейшего логического драйвера, позволяющего скриптовать манипуляции пользователя с устройствами ввода.
В окружении монолитного ядра это было бы выполнить концептуально сложнее ввиду тесного взаимодействия графики и ввода, что может косвенно выливаться в вопрос доверия к рендерингу UI приложениями.
Новый инструмент разрабатывался для достижения следующих целей:
Тестирование HMI (Human-Machine Interface) как в графических, так и консольных режимах. Путём эмуляции действий оператора с устройствами ввода драйвер позволяет воспроизводить различные операции ввода и анализировать адекватность реакции системы на тест-кейсы.
Автоматизация тестирования — для драйвера был разработан собственный командный интерфейс, позволяющий скриптовать операции и выполнять сложные сценарии.
Emergency сценарии использования. В ряде систем предусматривается автоматическая реакция на нештатные состояния. Данный драйвер позволит реализовывать подобные действия прямо на уровне подсистемы ввода, блокируя весь ввод (исключая человеческий фактор). Пример: обнаружили критически важное событие → терминируем драйвер ввода → запускаем драйвер виртуального ввода → вместо пользователя автоматом посылаем нужную hotkey-комбинацию. Если "влезать" в прикладной код нежелательно — вполне себе альтернатива.
Способы реализации
Общая подсистемы ввода имеет следующую структуру:
Изначально рассматривались три варианта реализации, причем, не только драйверные:
HID-драйвер (devh-*) — компонент ОС, поддерживающий различные конкретные устройства ввода через интерфейсы менеджера io-hid. io-hid обеспечивает ввод информации от физических устройств и оперирует такими понятиями как адреса на шине, номера прерываний и т. п. На данном уровне необходимо писать отдельные драйвера под каждое виртуальное устройство ввода, которые потом отдельно придется поднимать при запуске io-hid с опциями -d. Получается слишком инвазивно и, при необходимости изменения конфигурации, потребует перезапуска всех смежных драйверов, что будет деструктивным фактором для системы в некоторых сценариях.
Драйвер ввода (devi-*) - standalone драйвер ввода, который может, как выполнять функции провайдера HID-устройств, так и напрямую взаимодействовать с произвольной периферией. Основной задачей этих компонентов является передача данных в вышележащие подсистемы и поэтому не требует обязательной коммуникации с физическими интерфейсами. В данном случае потребуется описать модули различных поддерживаемых драйвером устройств, а также реализовать менеджер ресурсов для захвата данных от внешнего кода. На этом уровне можно изолированно от других драйверов описать множественную логику виртуализируемых устройств (например, можно получать данные по сети или через промышленные протоколы).
Реализация в виде приложений для конкретных оконных окружений (которых у нас несколько: legacy и собственное). Основными недостатками являются отсутствие доверия к такому подходу и необходимость частных решений для каждого окружения.
По понятным причинам остановились на втором варианте. Вариант с использованием devh-* интерфейсов был отброшен как избыточно сложный и слишком низкоуровневый.
Детали реализации
Последовательность ввода данных для типичного devi-драйвера:
Основной функцией, которая используется для ввода с устройств, является devi_enqueue_packet(). Эта функция используется модулями уровня фильтра для отправки пакета данных клиенту, в качестве которого может выступать Photon или менеджер ресурсов, предоставляющий POSIX-интерфейс всем желающим.
Дескриптор модуля виртуального устройства ввода имеет следующий вид:
struct _input_module
{
/* Module parameters */
input_module_t *up; /* Up module in the bus line */
input_module_t *down; /* Down module in the bus line */
struct Line *line; /* Bus line */
int flags; /* flags */
int type; /* type of module */
char name[12]; /* Module name */
char date[12]; /* Date of compilation */
const char *args; /* Module arguments */
void *data; /* Private module data */
/* Callbacks */
int (*init)( input_module_t *module );
int (*reset)( input_module_t *module );
int (*input)( input_module_t *module, int num, void *data );
int (*output)( input_module_t *module, void *data, int num );
int (*pulse)( message_context_t *ctx, int code, unsigned flags, void *ptr );
int (*parm)( input_module_t *module, int code, char *optarg );
int (*devctrl)( input_module_t *module, int event, void *data );
int (*shutdown)( input_module_t *module, int delay );
}
Последовательность инициализации:
Более подробно нюансы структуры и подходы к разработке описаны в корневом README публичного репозитория с примерами таких драйверов: https://git.kpda.ru/drivers/input.
Результат и примеры использования
В итоге появился devi-virtual (войдёт в следующий релиз ЗОСРВ «Нейтрино»). Изначально предполагалось, что будут реализованы два виртуальных устройства: клавиатура (/dev/virtual-kbd) и мышь (/dev/virtual-mouse). Но от мыши пока пришлось отказаться, так как она подразумевает перемещения в относительных координатах, что крайне неудобно для скриптования. Вместо неё сделали виртуальный тачскрин с абсолютными координатами (/dev/virtual-touch). Для удобства разрешение сопоставили с экранными координатами, чтобы не требовалось выполнять предварительную калибровку драйвера.
В качестве исключения интерфейс пришлось сделать текстовым, чтобы можно было из скриптов отправлять команды через echo.
Текстовые интерфейсы в драйверах
В наших драйверах такие подходы не являются best practice, так как текстовые парсеры не самая эффективная штука. Протокол получается изобилующим человеческим фаткором, на пустом месте капитально расширяется набор тест-кейсов и снижается общая надежность системного компонента.
Для работы клавиатуры добавлено два режима: "string" - для ввода строк и "key" - для ввода отдельных, в основном командных, клавиш. У тачпада задается нажимаемая кнопка мыши (да, действительно бывают аппаратные тачскрины с "правой клавишей", силой нажатия и т. п.) и координаты нажатия.
Драйвер запускается и терминируется совершенно автономно. С помощью опций -d и -n задаются задержка между инжектируемыми командами в миллисекундах и имя устройства. Пример запуска драйвера с обоими устройствами:
devi-virtual vkbd -d 500 vtouch -d2500
Ввод строки символов:
echo "string|ABCDE" > /dev/virtual-kbd
...
Эмуляция нажатия правой кнопки мыши в нужных координатах:
echo "RIGHT_BUTTON|800|80" > /dev/virtual-touch
...
Для демонстрации работы драйвера виртуальных устройств ввода был написан скрипт, с помощью которого можно зайти на сайт www.habr.com.
devi-virtual vkbd -d 500 vtouch -d 2500
waitfor /dev/virtual-touch 2
waitfor /dev/virtual-kbd 2
echo "LEFT_BUTTON|1200|80" > /dev/virtual-touch
sleep 2 # Нужно дождаться запуска браузера
echo "LEFT_BUTTON|30|30" > /dev/virtual-touch
echo "LEFT_BUTTON|30|50" > /dev/virtual-touch
echo "LEFT_BUTTON|270|80" > /dev/virtual-touch
echo "string|www.habr.com" > /dev/virtual-kbd
echo "key|ENTER" > /dev/virtual-kbd
С помощью первой команды выполняется запуск браузера через панель быстрого доступа. А дальше по классике:
Открываем меню.
Создаём новую вкладку через выпадающее меню,
меняем фокус ввода,
вводим адрес
и загружаем страницу.
Вместо заключения
Драйвер долгое время обкатывался во внутренних тепличных условиях, но со следующего релиза ЗОСРВ «Нейтрино» будет доступен для использования всем пользователям. Если интерес к этой разработке возникнет раньше релиза, опубликуем материалы в публичном репозитории компании.
Бонус: как определить координаты точки в оконной системе?
Для определения конкретных координат можно перезапустить devi-hid с добавлением опции -vvvvvvvv. В этом режиме драйвер сообщает обо всех передвижениях мыши в stdout (на картинке подчеркнуты красным):