Рубрики
Без рубрики

Приключения в Ci Land

Первоначально опубликовано в моем блоге. Сегодня на работе я написал сценарий CI, чтобы проверить приложение React, и … Tagged с CI, Python, JavaScript.

Первоначально опубликовано на мой блог .

Сегодня на работе я написал сценарий CI, чтобы проверить приложение React, и он оказался немного сложнее, чем ожидалось.

Давайте попробуем воспроизвести интересные проблемы, которые у меня были, и как я их решил.

Вот что вам понадобится, если вы хотите попытаться воспроизвести то, что я сделал.

  • Node.js, пряжа
  • Python3 и пипенв
  • Chromedriver бинарный.

Начнем с создания простого приложения React:

$ yarn global add create-react-app
$ create-react-app hello
$ cd hello
$ yarn

Теперь у нас есть красивое приложение React, работающее в нашем любимом браузере.

Давайте отредактируем App.js Файл для отображения Привет, мир вместо:

import React, { Component } from 'react';

class App extends Component {
  render() {
    return (
      

Hello, world!

); } } export default App;

Давайте использовать Pipenv Чтобы создать VirtualEnv с тем, что нам нужно:

$ pipenv install pytest
$ pipenv install selenium
$ pipenv shell

Теперь давайте добавим немного сквозного, используя Selenium и Pytest 1 Анкет

# in test_hello.py
import selenium.webdriver

def test_home():
    driver = selenium.webdriver.Chrome()
    driver.get("http://127.0.0.1:3000")
    assert "Hello, world!" in driver.page_source

Теперь мы можем запустить тесты с Pytest, как обычно:

$ pytest
collected 1 item

test_hello.py . [100%]
1 passed in 4.77 seconds

Хорошо, это работает!

Теперь давайте представим, что у вас есть команда людей, работающих над приложением, и вы хотели бы, чтобы эти тесты работали в любое время, когда кто -то создает запрос на объединение в этом репо.

Это известно как Непрерывная интеграция (CI для краткости) И, поверьте мне в этом, это работает намного лучше, чем сказать вашим товарищам по команде, чтобы они не могли запустить тесты, прежде чем отправлять изменения для обзора!

Мы используем Gitlab на работе и являются большим поклонником его функций CI.

Если вы вообще не знаете Gitlab CI, вот как это работает:

  • Вы устанавливаете и настраиваете Gitlab-Runner Программа на некоторых машинах (называемые бегуны )
  • Тогда вы пишете .gitlab-ci.yml Файл, который содержит описание работы.

На моей работе мы предпочитаем сохранить .gitlab-ci.yml просто, и держите код сценариев CI отдельно, как это:

(Обратите внимание, как мы используем python3 -m Pipenv вместо просто пипенв . Это чтобы убедиться, что Pipenv работает с ожидаемой версией Python)

# in .gitlab-ci.yml

stages:
 - check

check:
  stage: check
  script:
    - python3 -m pipenv install
    - python3 -m pipenv run python ci.py
# in ci.py

def main():
    # Call yarn here

if __name__ == " __main__":
    main()

Мы делаем это, потому что это позволяет легко воспроизводить сбои сборки, найденные во время CI на местном уровне. Любой разработчик в команде может работать Python ci/ci.py На их машине напрямую вместо того, чтобы пытаться скопировать/вставить код из файла YAML.

Прямо сейчас в тестах селена используется полноценная хромированная хромированность для запуска тестов. Это приятно для разработчиков, но не очень приятно на бегуне Gitlab.

Вместо этого было бы намного лучше, чтобы те, кто бежал в безголовом хром, то есть без какого -либо графического интерфейса.

Давайте исправим это, добавив -Без головой вариант:

# in conftest.py

import pytest

def pytest_addoption(parser):
    parser.addoption("--headless", action="store_true")

@pytest.fixture
def headless(request):
    return request.config.getoption("--headless")
# in test_hello.py

from selenium.webdriver.chrome.options import Options as ChromeOptions

def test_home(headless):
    options = ChromeOptions()
    options.headless = headless
    driver = selenium.webdriver.Chrome(chrome_options=options)
    ...

Теперь, если мы запустим pytest с -Без головой вариант, без головы параметр test_home Функция будет установлена на Истинный от Pytest. Вот как pytest светильники Работа.

В любом случае, теперь мы можем проверить, что это работает, работая:

$ pytest --headless

Итак, теперь мы сталкиваемся с новой задачей: нам нужно запустить пряжа старт до бег pytest и убейте сценарий React, когда тесты селена завершены.

Хороший способ сделать это в Python – использовать с заявление, так что давайте сделаем это:

class BackgroundProcess:
    """ Run `yarn start` in the background. Ensure the yarn process
    is killed when exiting the `with` block

    """
    def __init__ (self):
        self.process = None

    def __enter__ (self):
        self.process = subprocess.Popen(["yarn", "start"])

    def __exit__ (self, type, value, tb):
        self.process.terminate()

def main():
    with BackgroundProcess("yarn", "start"):
        subprocess.run(["pytest", "--headless"], check=True)

if __name__ == " __main__":
    main()

__enter__ Метод будет вызван прямо перед содержимым с Блок, так что раньше pytest начинается. Тогда __exit__ Метод будет вызван после pytest сделано, Даже если произошло исключение , передавая данные об исключении в качестве аргументов для __exit__ () метод Поскольку мы не хотим делать с чем-то, кроме повторного, если это произойдет, мы просто игнорируем их.

В любом случае, это гораздо более читаемо, чем использование Попробуйте/кроме/наконец ты не думаешь?

Нам все еще нужно крошечное исправление: по умолчанию, пряжа старт Откроет новую вкладку в нашем браузере. Это было здорово, когда мы работали над кодом JavaScript, но здесь мы работаем над сценарием CI, поэтому мы бы предпочли отключить такое поведение.

К счастью, все, что нам нужно сделать, это установить Браузер переменная среды к Нет :

class BackgroundProcess:
    ...

    def __enter__ (self):
        env = os.environ.copy()
        env["BROWSER"] = "NONE"
        self.process = subprocess.Popen(self.cmd, env=env)

Примечание: вы можете задаться вопросом, почему мы не просто установили Браузер переменная среды непосредственно в .gitlab-ci.yml файл. Это сработало бы, но здесь мы создаем специальное копия текущих переменных среды, и мы устанавливаем Браузер переменная среды Просто для пряжа процесс . Почему?

Что ж, если вы думаете о переменных окружающей среды как о неприятных глобальных переменных (и вы должны: среда процесса – это просто большое измененное общее состояние), имеет смысл ограничить их объем таким образом.

В любом случае, вернемся к основной теме:

Примечание. Остальная часть статьи предполагает, что вы используете Linux. Вещи могут работать немного по -другому (или вообще не) в других операционных системах.

Посмотрим, работает ли сценарий CI.

$ python ci.py
yarn run v1.7.0
$ react-scripts start
Starting the development server...
...
1 passed in 4.77 seconds

Давайте запустим его во второй раз, чтобы проверить, что пряжа процесс был действительно убит:

$ python ci.py
? Something is already running on port 3000. Probably:
  hello (pid 16508)

Would you like to run the app on another port instead? (Y/n)

Э-э-э.

Давайте запустим pgrep Чтобы проверить, что пряжа процесс мертв:

$ pgrep yarn
[err 1]

Процесс пряжи это мертвых. Что дает ?

Если мы посмотрим на .Terminate () Реализация, вот что мы находим:

# in /usr/lib/python3.6/subprocess.py

class Popen:

      def send_signal(self, sig):
          """Send a signal to the process."""
          # Skip signalling a process that we know has already died.
          if self.returncode is None:
              os.kill(self.pid, sig)

      def terminate(self):
          """Terminate the process with SIGTERM
          """
          self.send_signal(signal.SIGTERM)

Итак, Terminate () Просто отправляет Sigterm Сигнал с использованием идентификатора процесса ( pid ). Ошибки там нет.

Правда в том, что мы только что создали сироту (мы монстры!)

Когда мы бежали пряжа старт , пряжа Процесс посмотрел раздел под названием старт в Package.json и нашел что -то вроде этого:

{
...
  "scripts": {
    "start": "react-scripts start",
    ...
  }
}

Затем он создал Ребенок процесс, а именно React-Scripts Start , с другой пид Анкет

Поэтому, когда мы убили родительский процесс, Узел Процесс стал сиротой, так как ее родитель был мертв (плохой небольшой процесс).

По крайней мере, на Linux все процессы сирот автоматически переоцениваются в первый в истории процесс, который был создан с момента загрузки машины. ( Systemd на моей машине). Этот процесс всегда имеет PID, равный 1 и часто называют init Анкет

Мы можем проверить это, запустив пиро :

$ pstree
systemd─┬ <- PID 1
...
        ├─node── <- our poor orphan
...
        ├─plasmashell─┬
                      ├─konsole─┬─zsh─ <- our shell

Итак, как мы убедитесь, что Узел Детский процесс тоже убивает?

Есть несколько причудливых способов решить такие проблемы (например, мы могли бы использовать cgroups , но мы можем сделать это только со Stdlib Python.

Оказывается, мы можем использовать start_new_session аргумент в подпроцесс. Popen () вызов. Это создаст сессия и прикрепить пряжа процесс (и всех его детей) к нему.

Тогда мы можем отправить Sigterm сигнал на пид родителя, и все процессы в сеансе получат его:

import os
import signal

def __enter__ (self):
  ...
  self.process = subprocess.Popen(self.cmd, start_new_session=True)

def __exit__ (self):
    os.killpg(self.process.pid, signal.SIGTERM)

Теперь, если мы перезапустим наш сценарий, мы увидим, что ни один пряжа или Узел оставаться живым, когда сценарий CI завершается:

$ python ci.py
$ pgrep yarn
[err 1]
$ pgrep node
[err 1]

Это все на сегодня. Ваше здоровье!

Спасибо, что прочитали это далеко:)

Я хотел бы услышать, что вы скажете, поэтому, пожалуйста, оставьте комментарий ниже или прочитайте Страница обратной связи Для большего количества способов связаться со мной.

  1. Это не первый раз, когда я использовал эти инструменты для написания сквозных тестов для веб-приложения. Смотрите Портирование в pytest например. ↩

Оригинал: “https://dev.to/dmerejkowsky/adventures-in-ci-land-44oo”