Привет, меня зовут Андрей Николаев и я занимаюсь автоматизацией тестирования в hh. Более 2/3 наших десктопных пользователей прямо сейчас используют последнюю версию Google Chrome, поэтому мы хотим, чтобы и в наших E2E-автотестах (Java+Selenium) версия браузера была максимально приближена к пользовательской. Но не всегда апгрейд версии в тестах проходит гладко (то работа с куками поменяется, то remote DevTools по умолчанию оказываются недоступны, то просто наши хитровыдуманные клики начинают кликать не туда, и т.д. и т.п.). Поэтому нельзя просто так взять и поднять версию Chrome в автотестах — нужна предварительная проверка, которая при ручном выполнении требовала множества телодвижений, поэтому в какой-то момент мы решили, что раз работа серверов стоит дешевле работы человека, то пусть они и проверяют.

Сразу оговорюсь, что это не универсальное решение (а я не мастер спорта по python), мы просто хотели подсветить наличие такого подхода, ведь, как говорил один мудрый человек, живший еще во времена с приставкой «до н.э.», порой суета сует не дает поднять головы, чтобы оценить ситуацию в целом и придумать новые способы автоматизации рутины.
Предусловия
Наши тесты — это свой фреймворк поверх Selenium на Java, запускаем их через Jenkins, браузеры берем в selenoid-гриде (именно docker-образы Chrome от aerokube мы и проверяем на совместимость), а браузеры ходят на тестовые стенды через прокси.
Итак, приступим
Для реализации задачи мы создали план в Bamboo, который запускает python-скрипт (лежащий в одном из репозиториев, которые предварительно клонируется при старте плана) каждое утро, пока большинство ресурсов свободно (т.к. никто еще не проснулся и не пользуется ими).
Первым делом проверяем, есть ли новый образ Chrome: если нет, то выходим, иначе вычленяем из тега образа в docker registry номер версии браузера (отфильтровывая тег latest, т.к. для нас это слово бесполезно)
код
def get_latest_tag_for_last_day(): try: image_tags_url = 'https://registry.hub.docker.com/v2/repositories/selenoid/chrome/tags' response = requests.get(image_tags_url) except Exception as e: logger.error('Failed to retrieve docker image tags: %s', e) sys.exit(1) if response.status_code != 200: logger.error('Failed to retrieve docker image tags: %s', response.content) sys.exit(1) try: datetime_pattern = '%Y-%m-%dT%H:%M:%S.%fZ' results = [result for result in response.json()['results'] if result['name'] != 'latest'] latest_tag = max(results, key=lambda result: datetime.datetime.strptime(result['last_updated'], datetime_pattern)) last_updated = datetime.datetime.strptime(latest_tag['last_updated'], datetime_pattern) if datetime.datetime.utcnow() - last_updated > datetime.timedelta(days=1): logger.info('No new chrome images for last 24 hours.') sys.exit(0) latest_version = latest_tag['name'].split('.')[0] except Exception as e: logger.error('Failed to parse docker image tags: %s', e) sys.exit(1) logger.info('The latest version of chrome image is %s', latest_version) return latest_version
Также в процессе предварительной подготовки мы получаем из строки запуска:
-
адрес selenoid-ноды, куда установим свежую версию Chrome. Выбрана она на глаз так, чтобы ее лимиты были чуть выше лимита тредов в запускаемых автотестах. Мы не стали делать оверинжиниринг с перебором конфигов нод и поиском в них минимального значения для лимита браузеров. Получение параметра сделано через стандартный argparse, никакого рокетсаенса.
-
опционально: версию Chrome для тестирования (на случай, если что-то пойдет не так и нужно будет перетестировать)
-
номер запуска плана в Bamboo, чтобы потом сослаться на него в оповещении и создать одноименные ветки в репозиториях
-
из окружения: данные для авторизации в Jenkins (задаем их в Bamboo, чтобы лишний раз не светились в строке запуска) — тоже ничего особенного:
os.environ.get('JENKINS_USER')
Далее мы резервируем тестовый стенд (как начинали создаваться наши стенды в текущем виде, можно почитать здесь), на котором будем запускать автотесты, чтобы никто другой им не пользовался и не влиял на результаты тестирования (это стандартная у нас практика). Делается это через ручку нашего самописного CI, так что полный код тут будет бесполезен (используйте API вашей любимой CI/CD-системы), но выглядит это довольно стандартно:result = requests.post(f'{CI_URL}/assign_stand', json=...)
Если что-то к этому моменту пошло не так, то скрипт просто падает, план в Bamboo краснеет, заинтересованные получают уведомление. Все остальные шаги мы оборачиваем в try-except-finally, чтобы в конце откатить внесенные в инфраструктуру изменения.
Деплой фермы браузеров у нас осуществляется через ansible-плейбуки, внутри которых ничего особенного: установка пакетов, копирование файлов и запуск нужных образов через ansible-модуль docker_container. Для наших целей нужно запустить два плейбука.
Выключаем ноду-жертву из балансировки: балансировку осуществляет аерокубовский же ggr — Go Grid Router — из его конфига мы и удаляем эту ноду целиком вот таким «элегантным» движением руки:
брюки превращаются…
def run_ggr_playbook(host, revert=False): cmd = 'ansible-playbook ggr.yml -i inventory -t "chrome_image_update" ' \ + (f'-e "excluded_host={host}"' if not revert else '') logger.info('Starting ansible playbook: %s', cmd) subprocess.run(cmd, shell=True, check=True, cwd=os.path.join(os.pardir, 'somedir/playbooks'))
сам шаблон конфига ggr: quota.xml выглядит так
... <browser name="chrome" defaultVersion="{{ selenoid_default_browser_version }}"> <version number="{{ selenoid_default_browser_version }}"> <region name="grid farm"> {% for selenoidhost in groups['selenoid-user'] %} {% if selenoidhost != excluded_host|default('') %} <host name="{{ selenoidhost }}" port="4242" count="{{ hostvars[selenoidhost]['selenoid_browser_limit'] }}"/> {% endif %} {% endfor %} </region> </version> </browser> ...
Примерно таким же образом меняем версию образа Chrome на нашей ноде (переменная selenoid_default_browser_version рендерится в конфиг selenoid и в аргумент модуля docker_image в плейбуке)
превращаются брюки…
def run_selenoid_playbook(host, version=None, revert=False): cmd = f'ansible-playbook selenoid.yml -i inventory -l "{host}" -t "chrome_image_update" ' \ + (f'-e "selenoid_default_browser_version={version}"' if not revert else '') logger.info('Starting ansible playbook: %s', cmd) subprocess.run(cmd, shell=True, check=True, cwd=os.path.join(os.pardir, 'somedir/playbooks'))
vars-файл для ansible
selenoid_chrome_image_version: "{{ selenoid_default_browser_version }}.0"
шаблон конфига selenoid — browsers.json
… "chrome": { "default": "{{ selenoid_default_browser_version }}", "versions": { "{{ selenoid_default_browser_version }}": { "image": "selenoid/chrome:{{ selenoid_chrome_image_version }}"
часть плейбука selenoid
- name: pull chrome image docker_image: name: selenoid/chrome:{{ selenoid_chrome_image_version }} source: pull tags: - chromedriver_update
Теперь всё готово к запуску тестов в Jenkins, делаем это при помощи пакета jenkinsapi. В случае упавших тестов (как говорится, какой же русский не любит быстрых прогонов с флаки-тестами) даем стенду время «подостыть» и перезапускаем упавшие тесты.
Версию Chrome в автотестах мы получаем из Java system properties и передаем её в browser capabilities/chrome options тестов, поэтому при запуске мы просто оверрайдим её:
код
def run_test_job(user, token, stand, retry=False, params=None, job_pattern='__TESTS'): stand_name = stand.upper()... jenkins = Jenkins("https://jenkins_url.org/", username=user, password=token) job_name = stand_name + (job_pattern if not retry else '__RETRY_1') job = jenkins[job_name] logger.info('Starting jenkins job %s', job_name) queue_item = job.invoke(block=True, build_params=params if params else {}) build = queue_item.get_build() build_result = build.get_status() logger.info('Job result is: %s', build_result) return build_result, build.get_build_url() ... jenkins_job_params = { ..., 'OPTIONAL_PARAMS': f'-Dselenoid.host=http://{grid_host}:4242 -Dchrome.image.version={chrome_image_version}' } jenkins_run_result, jenkins_run_url = run_test_job(jenkins_user, jenkins_token, test_stand, params=jenkins_job_params) if jenkins_run_result == 'UNSTABLE': time.sleep(30) # retry jobs will disappear, so we keep link to original run run_test_job(jenkins_user, jenkins_token, test_stand, retry=True)
Если полученную ранее версию Chrome удалось и раскатить, и запустить с ней тесты, то, кажется, она валидная, и мы можем для облегчения себе ручной части работы сделать соответствующие коммиты с поднятием версии. Конечно, для более сложных правок можно использовать xml.etree.ElementTree, yamlpath, ruamel.yaml и другие пакеты, но в данном случае это показалось излишним.
код
def set_new_version_in_configs_and_commit(repo_name, branch, file_type, version): local_repo_path = os.path.join(..., repo_name) repo = Repo(local_repo_path) try: new_branch = repo.create_head(branch) new_branch.checkout() if file_type == 'yaml': local_file_path = 'path/to/ansible/var' key_to_modify = 'selenoid_default_browser_version' replacement_string = f'{key_to_modify}: {version}' elif file_type == 'properties': local_file_path = 'path/to/java/system.properties' key_to_modify = 'chrome.image.version' replacement_string = f'{key_to_modify}={version}' else: raise ValueError(f'invalid replacement file type {file_type}') file_path = os.path.join(local_repo_path, local_file_path) logger.info('Overriding file: %s', file_path) with open(file_path, 'r', encoding='utf-8') as file: content = file.read() content = re.sub(f'{key_to_modify}.*', replacement_string, content) with open(file_path, 'w', encoding='utf-8') as file: file.write(content) logger.info('Committing file: %s', file_path) repo.git.add(local_file_path) repo.git.commit('-m', f'Update Chrome image for autotests to {version}') except Exception as exception: logger.error('Error while creating new configs: %s', exception) raise exception finally: repo.git.checkout('master') ... set_new_version_in_configs_and_commit('auto-tests-repo', options.branch, 'properties', chrome_image_version) set_new_version_in_configs_and_commit('deploy-repo', options.branch, 'yaml', chrome_image_version)
Пушим ветки мы через соответствующие шаги в плане Bamboo ввиду некоторых ограничений в правах, но у себя в коде вы можете сделать repo.git.push(‘origin’, branch)
Когда всё готово, и даже если нет, в finally-блоке мы прокатываем плейбуки (с параметром revert=True), освобождаем стенд (также POST-запросом на ручку нашего CI) и отправляем в командный чат сообщение с результатами тестирования, ссылками на созданные ветки в репозиториях и запуск плана в Bamboo.

Заключение
Описанный подход помог сэкономить время, которые мы потратили на более полезные вещи (например, написание этой статьи). Надеюсь, наш опыт будет вам полезен и вдохновит на свежие идеи по автоматизации рутины, буду рад ответить на вопросы в комментариях.
ссылка на оригинал статьи https://habr.com/ru/articles/730474/
Добавить комментарий