Skip to content

Add lightweight integration tests for Celery/Redis basics in CI - #2369

Merged
ideaship merged 4 commits into
mainfrom
implement/issue-2368-celery-redis-integration-tests
Jun 19, 2026
Merged

Add lightweight integration tests for Celery/Redis basics in CI#2369
ideaship merged 4 commits into
mainfrom
implement/issue-2368-celery-redis-integration-tests

Conversation

@berendt

@berendt berendt commented Jun 12, 2026

Copy link
Copy Markdown
Member

Closes #2368.

Adds a lightweight integration-test job that covers the Celery/Redis
task-processing core (broker, queue routing, worker, result backend, Redis
streams for task output, distributed locks) end-to-end in CI, plus the
prerequisite that makes the broker URL configurable.

Change set (in commit order)

1. Derive Celery broker URL from settings and environment

osism/tasks/__init__.py, osism/commands/service.py

  • Config.broker_url / Config.result_backend were hardcoded to
    redis://redis, with the same literals duplicated in the beat and
    flower service commands. They now derive from REDIS_HOST /
    REDIS_PORT / REDIS_DB (the same settings osism.utils._init_redis
    already honors), overridable via CELERY_BROKER_URL /
    CELERY_RESULT_BACKEND. The default of REDIS_HOST is redis, so this
    is backwards compatible.
  • service.py now references Config.broker_url (single source of truth)
    for beat/flower, removing the duplicated literals.
  • Fixed task_track_started, which was the tuple (True,) instead of the
    intended bool (it only worked because a non-empty tuple is truthy).

This is what lets the tests point Celery at a local Redis via
REDIS_HOST=localhost.

2. Add Celery/Redis integration tests

tests/integration/, setup.cfg, README.md

  • test_celery.pyround-trip: ansible.noop.delay()
    result.get(timeout=60) is True (broker + osism-ansible routing +
    worker + result backend); worker visibility: a fresh Celery
    client configured from Config sees the worker via
    inspect().ping() (the mechanism behind osism get status workers).
  • test_redis_streams.pypush_task_output / finish_task_output /
    fetch_task_output round-trip (the path osism apply uses to stream
    logs).
  • test_locking.pycreate_redlock acquire/release and
    set_task_lock / is_task_locked / remove_task_lock.
  • conftest.py starts the worker as a session-scoped fixture from the
    same venv, running only the ansible Celery app (no import-time
    dependency on NetBox/OpenStack/ansible-core) with
    GATHER_FACTS_SCHEDULE=0 so the periodic gather_facts task is not
    registered. It polls inspect().ping() until the worker is ready and
    terminates it on teardown.
  • Tests carry an integration marker (registered in setup.cfg for the
    --strict-markers run) and are skipped when Redis is not reachable,
    so a local pytest is unaffected.

3. Add Zuul integration-test job

.zuul.yaml, playbooks/test-integration.yml

  • New python-osism-integration-tests job added to the check and
    periodic-daily pipelines. It reuses playbooks/pre.yml (which already
    sets up Docker via ensure-docker), starts a redis:7-alpine
    container, installs the package with pipenv exactly like the unit-test
    job, and runs pytest tests/integration with REDIS_HOST=localhost.

Notes / deviations

  • The unit-test job is already restricted to tests/unit via
    testpaths = tests/unit in setup.cfg, so a bare pytest never
    collects tests/integration — the issue's first concern is already
    satisfied. The integration marker + Redis-reachability skip are still
    added (the marker is required by --strict-markers, and the skip keeps
    pytest tests/integration green locally without Redis).
  • No CHANGELOG.md entry: this repo maintains the changelog in batched,
    post-release commits that map merged PR numbers to release versions, so
    per-PR entries are out of convention here.
  • Follow-ups listed in the issue (compose-based variant, API/beat/revoke
    coverage) are intentionally out of scope.

Local verification

  • black --check — clean (6 files)
  • flake8 — clean
  • pytest tests/integration — not run locally (no Redis); exercised by
    the new CI job.

🤖 Generated with Claude Code

@berendt
berendt force-pushed the implement/issue-2368-celery-redis-integration-tests branch 3 times, most recently from 1b3d448 to cb9d08b Compare June 12, 2026 13:45
@berendt
berendt marked this pull request as ready for review June 12, 2026 13:48
@berendt
berendt requested a review from ideaship June 12, 2026 13:49

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 2 security issues, and 1 other issue

Security issues:

  • Detected subprocess function 'Popen' without a static string. If this data can be controlled by a malicious actor, it may be an instance of command injection. Audit the use of this call to ensure it is not controllable by an external resource. You may consider using 'shlex.escape()'. (link)
  • Detected subprocess function 'Popen' without a static string. If this data can be controlled by a malicious actor, it may be an instance of command injection. Audit the use of this call to ensure it is not controllable by an external resource. You may consider using 'shlex.escape()'. (link)
Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="osism/commands/service.py" line_range="63" />
<code_context>
         elif service == "flower":
             p = subprocess.Popen(
-                "celery --broker=redis://redis flower",
+                f"celery --broker={Config.broker_url} flower",
                 shell=True,
             )
</code_context>
<issue_to_address>
**issue (bug_risk):** Handle special characters in broker URL for the flower invocation.

Because this uses `shell=True`, any `&`, `?`, or other shell metacharacters in `Config.broker_url` can be split or reinterpreted by the shell. Please use a list-based `Popen` without `shell=True`, e.g. `Popen(["celery", "--broker", Config.broker_url, "flower"])`, or otherwise ensure the URL is safely quoted/escaped.
</issue_to_address>

### Comment 2
<location path="osism/commands/service.py" line_range="51-54" />
<code_context>
                subprocess.Popen(
                    f"celery -A {t} --broker={Config.broker_url} beat -s /tmp/celerybeat-schedule-{t}.db",
                    shell=True,
                )
</code_context>
<issue_to_address>
**security (python.lang.security.audit.dangerous-subprocess-use-audit):** Detected subprocess function 'Popen' without a static string. If this data can be controlled by a malicious actor, it may be an instance of command injection. Audit the use of this call to ensure it is not controllable by an external resource. You may consider using 'shlex.escape()'.

*Source: opengrep*
</issue_to_address>

### Comment 3
<location path="osism/commands/service.py" line_range="62-65" />
<code_context>
            p = subprocess.Popen(
                f"celery --broker={Config.broker_url} flower",
                shell=True,
            )
</code_context>
<issue_to_address>
**security (python.lang.security.audit.dangerous-subprocess-use-audit):** Detected subprocess function 'Popen' without a static string. If this data can be controlled by a malicious actor, it may be an instance of command injection. Audit the use of this call to ensure it is not controllable by an external resource. You may consider using 'shlex.escape()'.

*Source: opengrep*
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread osism/commands/service.py Outdated
Comment thread osism/commands/service.py
Comment thread osism/commands/service.py Outdated
berendt added 3 commits June 12, 2026 15:56
The Celery Config hardcoded broker_url and result_backend to
redis://redis, with the same literals duplicated in the beat and
flower service commands. Derive the URLs from REDIS_HOST / REDIS_PORT /
REDIS_DB -- the same settings osism.utils._init_redis already honors --
and allow an override via CELERY_BROKER_URL / CELERY_RESULT_BACKEND.
The default of REDIS_HOST is redis, so this stays backwards
compatible while letting the integration tests point Celery at a
local Redis with REDIS_HOST=localhost.

Also fix task_track_started, which was the tuple (True,) instead of
the intended bool; it only worked because a non-empty tuple is truthy.

Assisted-by: Claude:claude-opus-4-8
Signed-off-by: Christian Berendt <berendt@osism.tech>
Add tests/integration/ covering the task-processing core against a
live Redis and a Celery worker started from the same virtualenv:

- Celery round-trip: ansible.noop.delay() returns True, exercising
  broker, osism-ansible queue routing, worker and result backend.
- Worker visibility: app.control.inspect().ping() sees the worker,
  the mechanism behind `osism get status workers`.
- Redis streams: push/finish/fetch_task_output round-trip, the path
  `osism apply` uses to stream task logs.
- Locking: create_redlock acquire/release and set/is/remove_task_lock.

The worker runs only the ansible Celery app, which has no import-time
dependency on NetBox/OpenStack/ansible-core. GATHER_FACTS_SCHEDULE=0
keeps the periodic gather_facts task from registering.

The tests carry an `integration` marker (registered for the
--strict-markers run) and are skipped when Redis is not reachable, so
the default `pytest tests/unit` run is unaffected.

Assisted-by: Claude:claude-opus-4-8
Signed-off-by: Christian Berendt <berendt@osism.tech>
Add the python-osism-integration-tests job to the check and
periodic-daily pipelines. It reuses playbooks/pre.yml (which already
sets up Docker via ensure-docker), starts a redis:7-alpine container,
installs the package with pipenv like the unit-test job, and runs
`pytest tests/integration` with REDIS_HOST=localhost.

Assisted-by: Claude:claude-opus-4-8
Signed-off-by: Christian Berendt <berendt@osism.tech>
@berendt
berendt force-pushed the implement/issue-2368-celery-redis-integration-tests branch from cb9d08b to 81ec7d2 Compare June 12, 2026 13:57
set -o pipefail
set -x

{{ python_venv_dir }}/bin/pipenv run pytest tests/integration

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This job can pass green with zero tests executed. tests/integration/conftest.py skips all integration-marked tests when Redis is unreachable, and pytest exits 0 on all-skipped (reproduced locally as 5 skipped, exit 0). With no readiness gate after docker run -d, any Redis-startup failure here surfaces as a passing job instead of a red one, defeating the gate. Please add (a) a Redis-readiness wait before pytest, and (b) a required-Redis mode for CI, e.g. an OSISM_REQUIRE_REDIS=1 env lever that makes conftest.py fail the session instead of skipping when Redis is unreachable. Set it only in this job so local skip behavior is preserved.

Comment thread osism/tasks/__init__.py
enable_ironic = os.environ.get("ENABLE_IRONIC", "True")
broker_url = "redis://redis"
result_backend = "redis://redis"
broker_url = os.environ.get(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a production-path change with no unit test. It replaced two hardcoded constants with real precedence logic (CELERY_BROKER_URL override, and result_backend defaulting to broker_url), but there is no reference to either env var anywhere under tests/; the integration suite only exercises the REDIS_HOST-derived fallback. Please add a unit test asserting: (1) CELERY_BROKER_URL wins over the derived URL, (2) CELERY_RESULT_BACKEND overrides independently, (3) absent CELERY_RESULT_BACKEND, result_backend == broker_url. Note these are class attributes resolved at import time from import-frozen settings.REDIS_*, so the test must importlib.reload(settings) then importlib.reload(osism.tasks) after setting env; monkeypatch.setenv alone will assert the stale cached value and pass for the wrong reason.

Comment thread osism/tasks/__init__.py
task_create_missing_queues = True
task_default_queue = "default"
task_track_started = (True,)
task_track_started = True

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This (True,) to True fix is correct (the 1-tuple only worked by being truthy), but it is an unrelated bug fix folded into a commit about broker-URL derivation; it should have been its own commit so it is traceable independently. Since the commit's suite advertises end-to-end "task core" coverage, also consider asserting the corrected value: either a direct Config.task_track_started is True check, or a STARTED-state transition in the Celery round-trip test.

lock.release()


def test_task_lock_set_check_remove():

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test reads/writes/deletes the fixed global key osism:task_lock on db 0 (see osism/utils/__init__.py:500-554); it cannot use a per-run UUID like the redlock/stream tests do. Against a disposable CI Redis that is fine, but the README example points REDIS_HOST=localhost at a local box, where this would clobber a real operator lock. Please either run the integration suite against a dedicated REDIS_DB, or add an explicit note in the README that the suite mutates live state on db 0 and must target a disposable Redis.

Comment thread tests/integration/conftest.py Outdated
raise RuntimeError(
f"Celery worker exited early with code {proc.returncode}"
)
if celery_app.control.inspect().ping():

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Readiness breaks on any worker pinging the broker, not ci-worker specifically. On an isolated CI Redis this is fine, but on a shared local broker it can pass before this worker is up and hide a startup failure. Consider gating on the expected node, e.g. any(n.startswith("ci-worker@") for n in (celery_app.control.inspect().ping() or {})), mirroring what test_worker_is_visible already does.

@github-project-automation github-project-automation Bot moved this from Ready to In review in Human Board Jun 18, 2026
- conftest.py: add an OSISM_REQUIRE_REDIS lever so an unreachable Redis
  fails the session instead of skipping every test; otherwise an
  all-skipped run exits 0 and a broken Redis would pass the CI gate
  green. Gate worker readiness on the ci-worker@ node specifically
  rather than any worker answering on the broker.
- playbooks/test-integration.yml: wait for Redis to answer PING before
  running pytest, and set OSISM_REQUIRE_REDIS=1 only in this job so the
  local skip behaviour is preserved.
- tests/unit/tasks/test_config.py: cover the broker-URL precedence logic
  (CELERY_BROKER_URL override, independent CELERY_RESULT_BACKEND, and
  result_backend defaulting to broker_url) and assert task_track_started
  is True. settings and osism.tasks are reloaded after setting the
  environment because Config resolves these attributes at import time.
- README.md: warn that the suite mutates live state on the configured
  Redis (the global osism:task_lock key on the selected REDIS_DB) and
  must target a disposable Redis.

Assisted-by: Claude:claude-opus-4-8
Signed-off-by: Christian Berendt <berendt@osism.tech>
@berendt
berendt requested a review from ideaship June 18, 2026 20:13
@ideaship
ideaship merged commit f2aef5c into main Jun 19, 2026
3 checks passed
@ideaship
ideaship deleted the implement/issue-2368-celery-redis-integration-tests branch June 19, 2026 10:04
@github-project-automation github-project-automation Bot moved this from In review to Done in Human Board Jun 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Archived in project

Development

Successfully merging this pull request may close these issues.

Add lightweight integration tests for Celery/Redis basics in CI

3 participants