mirror of
https://github.com/koloideal/Argenta.git
synced 2026-06-10 10:05:28 +03:00
docs
This commit is contained in:
@@ -0,0 +1,32 @@
|
||||
import sys
|
||||
from unittest.mock import patch
|
||||
import pytest
|
||||
from pytest import CaptureFixture
|
||||
|
||||
from argenta import App, Orchestrator, Router, Command, Response
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def patched_argv():
|
||||
with patch.object(sys, 'argv', ['program.py']):
|
||||
yield
|
||||
|
||||
def test_input_incorrect_command(capsys: CaptureFixture[str]):
|
||||
router = Router()
|
||||
orchestrator = Orchestrator()
|
||||
|
||||
@router.command(Command('test'))
|
||||
def test(response: Response) -> None:
|
||||
print('test command')
|
||||
|
||||
app = App(override_system_messages=True, print_func=print)
|
||||
app.include_router(router)
|
||||
app.set_unknown_command_handler(
|
||||
lambda command: print(f'Unknown command: {command.trigger}')
|
||||
)
|
||||
|
||||
with patch("builtins.input", side_effect=["help", "q"]):
|
||||
orchestrator.start_polling(app)
|
||||
|
||||
output = capsys.readouterr().out
|
||||
assert "\nUnknown command: help\n" in output
|
||||
@@ -1,25 +1,21 @@
|
||||
import io
|
||||
import unittest
|
||||
from contextlib import redirect_stdout
|
||||
|
||||
from argenta import App, Router, Command, Response
|
||||
from argenta.command import InputCommand
|
||||
|
||||
|
||||
class TestAppIntegration(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
self.app = App(override_system_messages=True, repeat_command_groups=False)
|
||||
self.router = Router(title="App")
|
||||
def test_simple_app() -> None:
|
||||
app = App(override_system_messages=True, repeat_command_groups_printing=False)
|
||||
router = Router(title="App")
|
||||
|
||||
@self.router.command(Command("HELP", description="Show help"))
|
||||
def help_cmd(response: Response):
|
||||
print("Available commands: HELP")
|
||||
@router.command(Command("HELP", description="Show help"))
|
||||
def help_cmd(response: Response):
|
||||
print("Available commands: HELP")
|
||||
|
||||
_ = help_cmd # appease linter: function is registered via decorator
|
||||
app.include_router(router)
|
||||
|
||||
self.app.include_router(self.router)
|
||||
|
||||
def test_help_command(self):
|
||||
with redirect_stdout(io.StringIO()) as stdout:
|
||||
self.router.finds_appropriate_handler(InputCommand.parse("HELP"))
|
||||
self.assertIn("Available commands:", stdout.getvalue())
|
||||
with redirect_stdout(io.StringIO()) as stdout:
|
||||
router.finds_appropriate_handler(InputCommand.parse("HELP"))
|
||||
|
||||
assert "Available commands:" in stdout.getvalue()
|
||||
|
||||
@@ -1,59 +1,42 @@
|
||||
import io
|
||||
import unittest
|
||||
from contextlib import redirect_stdout
|
||||
from unittest.mock import Mock
|
||||
|
||||
from dishka import Provider, provide, make_container, Scope # pyright: ignore[reportUnknownVariableType]
|
||||
from argenta.command import InputCommand
|
||||
from dishka import Provider, make_container, Scope
|
||||
|
||||
from argenta import Router, Command
|
||||
from argenta.di.integration import inject, setup_dishka, FromDishka
|
||||
from argenta.response import Response
|
||||
from argenta.response.status import ResponseStatus
|
||||
from argenta import Router, Response
|
||||
from argenta.di.integration import setup_dishka, FromDishka
|
||||
|
||||
|
||||
class Service:
|
||||
def hello(self) -> str:
|
||||
return "world"
|
||||
|
||||
|
||||
def get_service() -> Service:
|
||||
return Service()
|
||||
|
||||
|
||||
router = Router(title="DI")
|
||||
|
||||
|
||||
@router.command(Command("HELLO"))
|
||||
@inject # Auto-inject dependencies from the Response container
|
||||
@router.command("HELLO")
|
||||
def hello(response: Response, service: FromDishka[Service]) -> None:
|
||||
print(f"hello {service.hello()}")
|
||||
|
||||
|
||||
class TestDIHandler(unittest.TestCase):
|
||||
def test_hello_uses_service(self):
|
||||
# Prepare DI container with a stub
|
||||
fake = Mock(spec=Service)
|
||||
fake.hello.return_value = "mocked"
|
||||
|
||||
class TestProvider(Provider):
|
||||
scope = Scope.APP
|
||||
|
||||
@provide(scope=Scope.APP)
|
||||
def service(self) -> Service:
|
||||
return fake
|
||||
|
||||
container = make_container(TestProvider())
|
||||
|
||||
# Bind container to Response via integration
|
||||
setup_dishka(app=_FakeApp(), container=container, auto_inject=False) # type: ignore[arg-type]
|
||||
|
||||
# Create Response bound to the container
|
||||
r = Response(ResponseStatus.ALL_FLAGS_VALID)
|
||||
r._dishka_container = container # direct assignment is acceptable in tests # pyright: ignore[reportPrivateUsage]
|
||||
|
||||
# Call handler directly
|
||||
with redirect_stdout(io.StringIO()) as stdout:
|
||||
hello(r)
|
||||
|
||||
self.assertIn("hello mocked", stdout.getvalue())
|
||||
|
||||
|
||||
|
||||
|
||||
class _FakeApp:
|
||||
# Minimal stub for setup_dishka; app object is not used in unit tests
|
||||
registered_routers = []
|
||||
registered_routers = [router]
|
||||
|
||||
|
||||
def test_hello_uses_service():
|
||||
provider = Provider(scope=Scope.APP)
|
||||
provider.provide(get_service)
|
||||
|
||||
container = make_container(provider)
|
||||
setup_dishka(app=_FakeApp(), container=container, auto_inject=True)
|
||||
|
||||
# Call handler
|
||||
with redirect_stdout(io.StringIO()) as stdout:
|
||||
router.finds_appropriate_handler(InputCommand.parse('HELLO'))
|
||||
|
||||
assert "hello world" in stdout.getvalue()
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import io
|
||||
import unittest
|
||||
from contextlib import redirect_stdout
|
||||
|
||||
from argenta import Router, Command, Response
|
||||
@@ -8,15 +7,13 @@ from argenta.command import InputCommand
|
||||
|
||||
router = Router(title="Demo")
|
||||
|
||||
|
||||
@router.command(Command("PING", description="Ping command"))
|
||||
def ping(response: Response):
|
||||
print("PONG")
|
||||
|
||||
|
||||
class TestSimpleHandler(unittest.TestCase):
|
||||
def test_ping_prints_pong(self):
|
||||
# Имитация запуска хендлера через роутер
|
||||
with redirect_stdout(io.StringIO()) as stdout:
|
||||
router.finds_appropriate_handler(InputCommand.parse("PING"))
|
||||
self.assertIn("PONG", stdout.getvalue())
|
||||
def test_ping_prints_pong():
|
||||
# Call handler
|
||||
with redirect_stdout(io.StringIO()) as stdout:
|
||||
router.finds_appropriate_handler(InputCommand.parse("PING"))
|
||||
assert "PONG" in stdout.getvalue()
|
||||
|
||||
+1
-5
@@ -12,7 +12,7 @@ default:
|
||||
@{{sphinxbuild}} -M help "{{sourcedir}}" "{{builddir}}" {{sphinxopts}}
|
||||
|
||||
# Build all language versions
|
||||
build-all:
|
||||
build:
|
||||
{{sphinxbuild}} -b html -D language=ru {{sourcedir}} {{builddir}}/html/ru
|
||||
{{sphinxbuild}} -b html -D language =en {{sourcedir}} {{builddir}}/html/en
|
||||
|
||||
@@ -28,7 +28,3 @@ live-en:
|
||||
update-langs:
|
||||
{{sphinxbuild}} -b gettext . _build/gettext
|
||||
sphinx-intl update -p _build/gettext -l en
|
||||
|
||||
# Generic build target (html, latex, etc.)
|
||||
build target:
|
||||
{{sphinxbuild}} -M {{target}} "{{sourcedir}}" "{{builddir}}" {{sphinxopts}}
|
||||
|
||||
@@ -75,7 +75,7 @@ msgstr ""
|
||||
|
||||
#: ../../root/api/app/index.rst:47
|
||||
msgid ""
|
||||
"``repeat_command_groups``: Если ``True``, список доступных команд "
|
||||
"``repeat_command_groups_printing``: Если ``True``, список доступных команд "
|
||||
"выводится перед каждым вводом."
|
||||
msgstr ""
|
||||
|
||||
@@ -296,7 +296,7 @@ msgstr ""
|
||||
#~ msgstr ""
|
||||
|
||||
#~ msgid ""
|
||||
#~ "``repeat_command_groups``: Если **True** (по "
|
||||
#~ "``repeat_command_groups_printing``: Если **True** (по "
|
||||
#~ "умолчанию), описание доступных команд будет"
|
||||
#~ " выводиться перед каждым вводом."
|
||||
#~ msgstr ""
|
||||
|
||||
@@ -30,7 +30,7 @@ App
|
||||
system_router_title: str | None = "System points:",
|
||||
ignore_command_register: bool = True,
|
||||
dividing_line: AVAILABLE_DIVIDING_LINES = DEFAULT_DIVIDING_LINE,
|
||||
repeat_command_groups: bool = True,
|
||||
repeat_command_groups_printing: bool = True,
|
||||
override_system_messages: bool = False,
|
||||
autocompleter: AutoCompleter = DEFAULT_AUTOCOMPLETER,
|
||||
print_func: Printer = DEFAULT_PRINT_FUNC) -> None
|
||||
@@ -40,14 +40,14 @@ App
|
||||
* ``prompt``: Приглашение к вводу, отображаемое перед каждой командой.
|
||||
* ``initial_message``: Сообщение, выводимое при запуске приложения.
|
||||
* ``farewell_message``: Сообщение, выводимое при выходе из приложения.
|
||||
* ``exit_command``: Команда, используемая для выхода из приложения.
|
||||
* ``exit_command``: Команда, которая маркируется как триггер для выхода из приложения.
|
||||
* ``system_router_title``: Заголовок для системного роутера (содержит команду выхода).
|
||||
* ``ignore_command_register``: Если ``True``, регистр команд игнорируется при поиске обработчика.
|
||||
* ``dividing_line``: Стиль разделительной линии (``StaticDividingLine`` или ``DynamicDividingLine``).
|
||||
* ``repeat_command_groups``: Если ``True``, список доступных команд выводится перед каждым вводом.
|
||||
* ``ignore_command_register``: Если ``True``, регистр вводимых команд игнорируется при поиске обработчика.
|
||||
* ``dividing_line``: Тип разделительной линии (``StaticDividingLine`` или ``DynamicDividingLine``).
|
||||
* ``repeat_command_groups_printing``: Если ``True``, список доступных команд выводится перед каждым вводом.
|
||||
* ``override_system_messages``: Если ``True``, стандартное форматирование (цвета, ASCII-арт) отключается.
|
||||
* ``autocompleter``: Объект, отвечающий за автодополнение команд.
|
||||
* ``print_func``: Функция для вывода всех системных сообщений (по умолчанию ``rich.print``).
|
||||
* ``autocompleter``: Экземпляр класса :ref:`AutoCompleter <root_api_app_autocompleter>`, отвечающий за автодополнение команд.
|
||||
* ``print_func``: Функция для вывода всех системных сообщений (по умолчанию ``rich.Console().print``).
|
||||
|
||||
-----
|
||||
|
||||
@@ -68,7 +68,7 @@ App
|
||||
|
||||
- .. py:method:: add_message_on_startup(self, message: str) -> None
|
||||
|
||||
Добавляет текстовое сообщение, которое выводится при запуске приложения после `initial_message`.
|
||||
Добавляет текстовое сообщение, которое выводится при запуске приложения после ``initial_message``.
|
||||
|
||||
:param message: Строка с сообщением.
|
||||
|
||||
@@ -89,7 +89,7 @@ App
|
||||
|
||||
.. py:method:: set_description_message_pattern(self, handler: Callable[[str, str], str]) -> None
|
||||
|
||||
Устанавливает шаблон для форматирования строки описания команды.
|
||||
Устанавливает шаблон для форматирования описания команды.
|
||||
|
||||
Обработчик принимает триггер команды (``str``) и её описание (``str``), а возвращает отформатированную строку.
|
||||
|
||||
|
||||
+12
-14
@@ -27,14 +27,12 @@
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from argenta import (
|
||||
App, Orchestrator, Router, Command, Response
|
||||
)
|
||||
from argenta import App, Orchestrator, Router, Command, Response
|
||||
|
||||
* :ref:`App <root_api_app_index>` — Основной класс приложения.
|
||||
* :ref:`Orchestrator <root_api_orchestrator_index>` — Класс для управления жизненным циклом.
|
||||
* :ref:`App <root_api_app_index>` — Объект приложения, который отвечает за логику роутинга, настройки, валидации и т.д.
|
||||
* :ref:`Orchestrator <root_api_orchestrator_index>` — Класс для конфигурирования и запуска всего приложения.
|
||||
* :ref:`Router <root_api_router>` — Класс для группировки и регистрации команд.
|
||||
* :ref:`Command <root_api_command_index>` — Класс для создания команд.
|
||||
* :ref:`Command <root_api_command_index>` — Класс для создания команд при инициализации хэндлеров.
|
||||
* :ref:`Response <root_api_response>` — Объект ответа, передаваемый в обработчики.
|
||||
|
||||
.. rubric:: Команды и флаги
|
||||
@@ -55,9 +53,9 @@
|
||||
* :ref:`Flags <root_api_command_flags>` — Коллекция для регистрации флагов.
|
||||
* :ref:`InputFlag <root_api_command_input_flag>` — Класс для введённого пользователем флага.
|
||||
* :ref:`InputFlags <root_api_command_input_flags>` — Коллекция введённых флагов.
|
||||
* :ref:`PossibleValues <root_api_command_possible_values>` — Правила валидации значений флагов.
|
||||
* :ref:`PossibleValues <root_api_command_possible_values>` — Правила валидации значений флага.
|
||||
* :ref:`ValidationStatus <root_api_command_validation_status>` — Статусы валидации флагов.
|
||||
* ``PredefinedFlags`` — Готовые наборы флагов (например, ``--help``).
|
||||
* :ref:`PredefinedFlags <root_api_command_flag_predefined_flags>` — Коллекция предопределённых флагов.
|
||||
|
||||
.. rubric:: Настройка приложения
|
||||
|
||||
@@ -70,10 +68,10 @@
|
||||
PredefinedMessages
|
||||
)
|
||||
|
||||
* :ref:`AutoCompleter <root_api_app_autocompleter>` — Базовый класс для автодополнения.
|
||||
* :ref:`StaticDividingLine <root_api_app_dividing_lines>` — Статическая разделительная линия.
|
||||
* :ref:`DynamicDividingLine <root_api_app_dividing_lines>` — Динамическая разделительная линия.
|
||||
* ``PredefinedMessages`` — Готовые системные сообщения.
|
||||
* :ref:`AutoCompleter <root_api_app_autocompleter>` - Класс для настройки автодополнения.
|
||||
* :ref:`StaticDividingLine <root_api_app_dividing_lines>` — Статическая разделительная линия для оформления вывода.
|
||||
* :ref:`DynamicDividingLine <root_api_app_dividing_lines>` — Динамическая разделительная линия для оформления вывода.
|
||||
* :ref:`PredefinedMessages <root_api_predefined_messages>` — Готовые сообщения для вывода при старте приложения.
|
||||
|
||||
.. rubric:: Внедрение зависимостей
|
||||
|
||||
@@ -84,8 +82,8 @@
|
||||
inject
|
||||
)
|
||||
|
||||
* :ref:`FromDishka <root_dependency_injection>` — Маркер для внедрения зависимостей.
|
||||
* :ref:`inject <root_dependency_injection>` — Декоратор для асинхронного внедрения.
|
||||
* :ref:`FromDishka <root_dependency_injection>` — Маркер аргумента функции как зависимости, которая должна быть инжектирована.
|
||||
* :ref:`inject <root_dependency_injection>` — Декоратор для инжектирования зависимостей, указанных в сигнатуре.
|
||||
|
||||
|
||||
.. toctree::
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
Наше обязательство
|
||||
------------------
|
||||
|
||||
В целях создания открытой и гостеприимной атмосферы мы, как участники и мейнтейнеры, обязуемся сделать участие в нашем проекте и сообществе свободным от преследований для всех, независимо от возраста, телосложения, инвалидности, этнической принадлежности, гендерной идентичности и самовыражения, уровня опыта, образования, социально-экономического статуса, национальности, внешности, расы, религии или сексуальной идентичности и ориентации.
|
||||
В целях создания открытой и гостеприимной атмосферы мы, как участники и мейнтейнеры, обязуемся сделать участие в нашем проекте и сообществе свободным от преследований для всех, независимо от возраста, телосложения, инвалидности, этнической принадлежности, уровня опыта, образования, социально-экономического статуса, национальности, внешности, расы или религии.
|
||||
|
||||
-----
|
||||
|
||||
|
||||
+16
-13
@@ -55,7 +55,7 @@
|
||||
|
||||
Поищите ответ в существующих `Issues <https://github.com/koloideal/Argenta/issues>`_. Если вы нашли похожий вопрос, но всё ещё нуждаетесь в разъяснениях, можете написать в нём. Также рекомендуем поискать ответ в интернете.
|
||||
|
||||
Если ответа не нашлось, создайте новый `Issue <https://github.com/koloideal/Argenta/issues/new>`_ и предоставьте как можно больше контекста, включая версии проекта и платформы (CPython, pip и т.д.).
|
||||
Если ответа не нашлось, создайте новый `Issue <https://github.com/koloideal/Argenta/issues/new>`_ и предоставьте как можно больше контекста, включая версии проекта и платформы.
|
||||
|
||||
Мы займемся вашей задачей как можно скорее.
|
||||
|
||||
@@ -87,10 +87,10 @@
|
||||
* Также поищите в интернете (включая `Stack Overflow`), чтобы узнать, обсуждалась ли проблема за пределами `GitHub`.
|
||||
* Соберите информацию об ошибке:
|
||||
* Трассировка стека.
|
||||
* ОС, платформа и версия (Windows, Linux, macOS, x86, ARM).
|
||||
* Версия интерпретатора, компилятора, SDK, среды выполнения, менеджера пакетов и т.д.
|
||||
* Входные данные и полученный результат.
|
||||
* Можете ли вы надёжно воспроизвести проблему? Воспроизводится ли она на старых версиях?
|
||||
* ОС, платформа и версия (Windows, Linux, macOS, x86, ARM).
|
||||
* Версия интерпретатора, компилятора, SDK, среды выполнения, менеджера пакетов и т.д.
|
||||
* Входные данные и полученный результат.
|
||||
* Можете ли вы надёжно воспроизвести проблему? Воспроизводится ли она на старых версиях?
|
||||
|
||||
.. rubric:: Как мне отправить хороший отчет об ошибке?
|
||||
|
||||
@@ -187,7 +187,7 @@
|
||||
|
||||
python -m pytest tests
|
||||
|
||||
#. Сделайте коммит, следуя нашему руководству по стилю, и отправьте изменения в ваш форк.
|
||||
#. Сделайте коммит, следуя :ref:`нашему руководству по стилю <styleguide>`, и отправьте изменения в ваш форк.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
@@ -209,6 +209,10 @@
|
||||
.. note::
|
||||
|
||||
Мы поддерживаем документацию на двух языках: русском и английском.
|
||||
|
||||
.. important::
|
||||
|
||||
Для инкапсуляции различных команд, необходимых для настройки и запуска проекта мы используем ``just``, он же фигурирует в различных примерах в документации, поэтому рекомендуем вам `установить его <https://github.com/casey/just#installation>`_
|
||||
|
||||
Для улучшения документации вы можете следовать процессу, похожему на внесение вклада в код:
|
||||
|
||||
@@ -220,33 +224,32 @@
|
||||
cd docs
|
||||
|
||||
#. Внесите изменения в **русскую** версию документации (`docs/index.rst` и/или `docs/root/*`).
|
||||
#. Чтобы собрать документацию локально и увидеть изменения, выполните:
|
||||
#. Чтобы собрать документацию локально в режиме автоматического ребилда и увидеть изменения, выполните:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
make live-ru
|
||||
just live-ru
|
||||
|
||||
#. Откройте `127.0.0.1:8000` в браузере, чтобы просмотреть сгенерированную документацию.
|
||||
#. После завершения работы над русской версией необходимо создать английский перевод:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
make update-langs
|
||||
just update-langs
|
||||
|
||||
#. После обновления шаблона обновите файлы перевода, расположенные в `docs/locales/en/LC_MESSAGES/`.
|
||||
#. Когда изменения будут готовы, сделайте коммит и откройте `Pull Request`. Используйте префикс `docs:` в сообщении коммита.
|
||||
|
||||
-----
|
||||
|
||||
.. _Руководства по стилю:
|
||||
.. _styleguide:
|
||||
|
||||
Руководства по стилю
|
||||
--------------------
|
||||
|
||||
.. _Сообщения коммитов:
|
||||
.. _commits_messages:
|
||||
|
||||
Сообщения коммитов
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
**Сообщения коммитов**
|
||||
|
||||
Мы следуем спецификации `Conventional Commits <https://www.conventionalcommits.org/en/v1.0.0/>`_. Это делает историю проекта более читаемой и позволяет автоматически генерировать журнал изменений.
|
||||
|
||||
|
||||
+19
-4
@@ -1,18 +1,20 @@
|
||||
Тестирование
|
||||
============
|
||||
|
||||
В этом разделе описаны практики тестирования приложений на основе ``Argenta``. Примеры основаны на фактическом публичном API: ``App``, ``Router``, ``Command``, ``Orchestrator``, DI через ``dishka`` и интеграцию в ``argenta.di.integration``.
|
||||
В этом разделе описаны практики тестирования приложений на основе ``Argenta``. Примеры основаны на фактическом публичном API.
|
||||
|
||||
Модульное тестирование хендлеров
|
||||
--------------------------------
|
||||
|
||||
Обработчики в Argenta — обычные функции. Их удобно тестировать как чистые функции, не поднимая весь цикл приложения. Рекомендуются ``unittest`` или ``pytest``.
|
||||
|
||||
Пример с ``unittest`` для простого хендлера без DI:
|
||||
Пример для простого хендлера без DI:
|
||||
|
||||
.. literalinclude:: ../code_snippets/testing/simple_handler_unittest.py
|
||||
:language: python
|
||||
:linenos:
|
||||
|
||||
-----
|
||||
|
||||
Тестирование с внедрением зависимостей (DI)
|
||||
-------------------------------------------
|
||||
@@ -22,6 +24,8 @@
|
||||
.. literalinclude:: ../code_snippets/testing/di_handler_unittest.py
|
||||
:language: python
|
||||
:linenos:
|
||||
|
||||
-----
|
||||
|
||||
Интеграционное тестирование приложения
|
||||
--------------------------------------
|
||||
@@ -31,11 +35,22 @@
|
||||
.. literalinclude:: ../code_snippets/testing/app_integration_unittest.py
|
||||
:language: python
|
||||
:linenos:
|
||||
|
||||
-----
|
||||
|
||||
E2E-тестирование цикла (опционально)
|
||||
E2E-тестирование цикла
|
||||
------------------------------------
|
||||
|
||||
Полный запуск цикла ``start_polling`` можно покрывать через подпроцесс с передачей строк во ``stdin``. Это тяжелее и обычно не требуется. Если всё же необходимо — вынесите конфигурацию в функцию ``main()`` и запускайте модуль в подпроцессе с подготовленным вводом/выводом.
|
||||
Полный запуск цикла ``start_polling`` можно покрывать через подпроцесс с передачей строк в ``stdin``. Это тяжелее и обычно не требуется. Если всё же необходимо — пример ниже.
|
||||
|
||||
.. danger ::
|
||||
Обязательно передавайте строковый триггер команды выхода последним элементом в списке, который передаёте в контекстном менеджере при патче ``input`` как аргумент ``side_effects``, иначе тестируемое приложение будет ожидать ввода следующей команды и не сможет корректно завершиться.
|
||||
|
||||
.. literalinclude:: ../code_snippets/testing/app_e2e_test.py
|
||||
:language: python
|
||||
:linenos:
|
||||
|
||||
-----
|
||||
|
||||
Советы по тестированию
|
||||
----------------------
|
||||
|
||||
Reference in New Issue
Block a user