-
Notifications
You must be signed in to change notification settings - Fork 3
Add lightweight integration tests for Celery/Redis basics in CI #2369
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
berendt
wants to merge
3
commits into
main
Choose a base branch
from
implement/issue-2368-celery-redis-integration-tests
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| --- | ||
| - name: Run integration tests | ||
| hosts: all | ||
|
|
||
| vars: | ||
| python_venv_dir: /tmp/venv | ||
|
|
||
| tasks: | ||
| - name: Start Redis container | ||
| ansible.builtin.shell: | ||
| executable: /bin/bash | ||
| cmd: | | ||
| set -e | ||
| set -o pipefail | ||
| set -x | ||
|
|
||
| docker run -d --name redis -p 6379:6379 redis:7-alpine | ||
| changed_when: true | ||
|
|
||
| - name: Install dependencies | ||
| ansible.builtin.shell: | ||
| executable: /bin/bash | ||
| chdir: "{{ zuul.project.src_dir }}" | ||
| cmd: | | ||
| set -e | ||
| set -o pipefail | ||
| set -x | ||
|
|
||
| {{ python_venv_dir }}/bin/pipenv install --dev --deploy | ||
| {{ python_venv_dir }}/bin/pipenv run pip install . | ||
|
|
||
| - name: Run pytest | ||
| ansible.builtin.shell: | ||
| executable: /bin/bash | ||
| chdir: "{{ zuul.project.src_dir }}" | ||
| cmd: | | ||
| set -e | ||
| set -o pipefail | ||
| set -x | ||
|
|
||
| {{ python_venv_dir }}/bin/pipenv run pytest tests/integration | ||
| environment: | ||
| REDIS_HOST: localhost |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,117 @@ | ||
| # SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| """Fixtures and skip logic shared by the Celery/Redis integration tests. | ||
|
|
||
| These tests exercise the real task-processing core (broker, queue routing, | ||
| worker, result backend, Redis streams and distributed locks) against a live | ||
| Redis. They are skipped automatically when Redis is not reachable -- for | ||
| example during a local ``pytest`` run without the service -- so the suite stays | ||
| green outside the dedicated CI job. | ||
| """ | ||
|
|
||
| import os | ||
| import subprocess | ||
| import time | ||
|
|
||
| import pytest | ||
|
|
||
| from osism import settings | ||
|
|
||
| # Only the ``ansible`` Celery app is used as worker: it has no import-time | ||
| # dependency on NetBox, OpenStack or ansible-core, unlike the other task | ||
| # modules. ``osism.tasks.ansible.*`` is routed to the ``osism-ansible`` queue. | ||
| WORKER_APP = "osism.tasks.ansible" | ||
| WORKER_QUEUE = "osism-ansible" | ||
| # Celery treats a ``-n`` value without ``@`` as the host part (yielding | ||
| # ``celery@ci-worker``), so the node name must be given explicitly. | ||
| WORKER_NAME = "ci-worker@%h" | ||
| WORKER_BOOT_TIMEOUT = 60 | ||
|
|
||
|
|
||
| def _redis_reachable(): | ||
| """Return ``True`` when the configured Redis answers a ping.""" | ||
| try: | ||
| from redis import Redis | ||
|
|
||
| client = Redis( | ||
| host=settings.REDIS_HOST, | ||
| port=settings.REDIS_PORT, | ||
| db=settings.REDIS_DB, | ||
| socket_connect_timeout=1, | ||
| ) | ||
| try: | ||
| client.ping() | ||
| finally: | ||
| client.close() | ||
| return True | ||
| except Exception: | ||
| return False | ||
|
|
||
|
|
||
| def pytest_collection_modifyitems(config, items): | ||
| """Skip integration-marked tests when Redis is not reachable.""" | ||
| if _redis_reachable(): | ||
| return | ||
| skip = pytest.mark.skip( | ||
| reason=f"Redis is not reachable on {settings.REDIS_HOST}:{settings.REDIS_PORT}" | ||
| ) | ||
| for item in items: | ||
| if "integration" in item.keywords: | ||
| item.add_marker(skip) | ||
|
|
||
|
|
||
| @pytest.fixture(scope="session") | ||
| def celery_app(): | ||
| """The ``ansible`` Celery app, configured from the live broker URL.""" | ||
| from osism.tasks import ansible | ||
|
|
||
| return ansible.app | ||
|
|
||
|
|
||
| @pytest.fixture(scope="session") | ||
| def celery_worker(celery_app): | ||
| """Start a Celery worker for the ``osism-ansible`` queue for the session. | ||
|
|
||
| The worker runs from the same virtualenv as the tests. | ||
| ``GATHER_FACTS_SCHEDULE=0`` prevents registration of the periodic | ||
| ``gather_facts`` task, which would try to run ``/run.sh`` in containers that | ||
| do not exist in CI. | ||
| """ | ||
| proc = subprocess.Popen( | ||
| [ | ||
| "celery", | ||
| "-A", | ||
| WORKER_APP, | ||
| "worker", | ||
| "-n", | ||
| WORKER_NAME, | ||
| "-Q", | ||
| WORKER_QUEUE, | ||
| "-c", | ||
| "1", | ||
| ], | ||
| env={**os.environ, "GATHER_FACTS_SCHEDULE": "0"}, | ||
| ) | ||
|
|
||
| try: | ||
| deadline = time.time() + WORKER_BOOT_TIMEOUT | ||
| while time.time() < deadline: | ||
| if proc.poll() is not None: | ||
| raise RuntimeError( | ||
| f"Celery worker exited early with code {proc.returncode}" | ||
| ) | ||
| if celery_app.control.inspect().ping(): | ||
| break | ||
| time.sleep(1) | ||
| else: | ||
| raise RuntimeError( | ||
| f"Celery worker did not become ready within {WORKER_BOOT_TIMEOUT}s" | ||
| ) | ||
| yield proc | ||
| finally: | ||
| proc.terminate() | ||
| try: | ||
| proc.wait(timeout=30) | ||
| except subprocess.TimeoutExpired: | ||
| proc.kill() | ||
| proc.wait() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| # SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| """Celery round-trip and worker-visibility integration tests. | ||
|
|
||
| These validate the broker, queue routing (``osism-ansible``), worker and result | ||
| backend end-to-end against a live Redis and a worker started from the same | ||
| virtualenv. | ||
| """ | ||
|
|
||
| import pytest | ||
|
|
||
| pytestmark = pytest.mark.integration | ||
|
|
||
|
|
||
| def test_noop_round_trip(celery_worker): | ||
| """Dispatching ``noop`` returns ``True`` through the result backend. | ||
|
|
||
| A single round-trip exercises the broker, the ``osism-ansible`` queue | ||
| routing, the worker and the result backend. | ||
| """ | ||
| from osism.tasks import ansible | ||
|
|
||
| result = ansible.noop.delay() | ||
|
|
||
| assert result.get(timeout=60) is True | ||
|
|
||
|
|
||
| def test_worker_is_visible(celery_worker): | ||
| """``app.control.inspect().ping()`` sees the running worker. | ||
|
|
||
| This is the mechanism behind ``osism get status workers``: a fresh Celery | ||
| client configured from ``Config`` inspects the worker over the broker. The | ||
| ``celery_worker`` fixture starts it as ``ci-worker@<hostname>``. | ||
| """ | ||
| from celery import Celery | ||
|
|
||
| from osism.tasks import Config | ||
|
|
||
| app = Celery("status") | ||
| app.config_from_object(Config) | ||
|
|
||
| replies = app.control.inspect().ping() | ||
|
|
||
| assert replies | ||
| assert any(name.startswith("ci-worker@") for name in replies) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| # SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| """Distributed-locking integration tests against a live Redis. | ||
|
|
||
| Covers the Redlock helper used by ``run_ansible_in_environment`` for per-play | ||
| locking and the task-lock flag used by ``osism lock`` / ``osism unlock``. | ||
| """ | ||
|
|
||
| import uuid | ||
|
|
||
| import pytest | ||
|
|
||
| from osism import utils | ||
|
|
||
| pytestmark = pytest.mark.integration | ||
|
|
||
|
|
||
| def test_redlock_acquire_and_release(): | ||
| """A Redlock can be acquired and released against the live Redis.""" | ||
| lock = utils.create_redlock(key=f"itest-lock-{uuid.uuid4()}") | ||
|
|
||
| assert lock.acquire(timeout=10) | ||
| lock.release() | ||
|
|
||
|
|
||
| def test_task_lock_set_check_remove(): | ||
| """``set_task_lock`` / ``is_task_locked`` / ``remove_task_lock`` round-trip.""" | ||
| # Start from a known-unlocked state so the test is independent of prior runs. | ||
| utils.remove_task_lock() | ||
| assert utils.is_task_locked() is None | ||
|
|
||
| assert utils.set_task_lock(user="tester", reason="integration test") is True | ||
|
|
||
| lock_info = utils.is_task_locked() | ||
| assert lock_info is not None | ||
| assert lock_info["locked"] is True | ||
| assert lock_info["user"] == "tester" | ||
| assert lock_info["reason"] == "integration test" | ||
|
|
||
| assert utils.remove_task_lock() is True | ||
| assert utils.is_task_locked() is None |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| # SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| """Redis-streams task-output integration tests. | ||
|
|
||
| ``push_task_output`` / ``finish_task_output`` / ``fetch_task_output`` are the | ||
| mechanism ``osism apply`` uses to stream task logs; they round-trip through a | ||
| Redis stream keyed by the task id and are testable with Redis alone. | ||
| """ | ||
|
|
||
| import uuid | ||
|
|
||
| import pytest | ||
|
|
||
| from osism import utils | ||
|
|
||
| pytestmark = pytest.mark.integration | ||
|
|
||
|
|
||
| def test_task_output_round_trip(capsys): | ||
| """Output pushed to a task stream is read back with its return code.""" | ||
| task_id = f"itest-{uuid.uuid4()}" | ||
|
|
||
| utils.push_task_output(task_id, "first line\n") | ||
| utils.push_task_output(task_id, "second line\n") | ||
| utils.finish_task_output(task_id, rc=3) | ||
|
|
||
| rc = utils.fetch_task_output(task_id, timeout=10) | ||
|
|
||
| assert rc == 3 | ||
| assert capsys.readouterr().out == "first line\nsecond line\n" |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.