Skip to content

Fix test infrastructure, pamqp 4 compatibility, and runtime bugs#152

Merged
gmr merged 4 commits into
mainfrom
fix/test-infrastructure-and-pamqp4-compat
Apr 1, 2026
Merged

Fix test infrastructure, pamqp 4 compatibility, and runtime bugs#152
gmr merged 4 commits into
mainfrom
fix/test-infrastructure-and-pamqp4-compat

Conversation

@gmr

@gmr gmr commented Apr 1, 2026

Copy link
Copy Markdown
Owner

Summary

Completes test infrastructure that was silently broken, fixes four pamqp 4 runtime incompatibilities that prevented the library from working end-to-end, and expands test coverage from 62% to 84%.

Problem

After the library modernization and module restoration (PR #151), two categories of problems existed:

Tests not running: The restored test files (base_tests.py, channel_test.py, integration_tests.py, utils_tests.py) used the old naming convention and didn't match unittest's default test*.py discovery pattern. 130+ tests were silently not executing — including all integration tests. The library had never been validated end-to-end after the modernization.

pamqp 4 incompatibilities: Once tests ran against a real broker, four runtime failures surfaced:

  1. pamqp.commands.Frame removed — _validate_frame_type() fell through silently, causing _wait_on_frame() to loop forever on any channel operation
  2. Channel.Close / Connection.Close with default class_id=NoneTypeError marshaling to wire format
  3. ContentHeader.properties.to_dict() removed → AttributeError on every received message
  4. _coerce_properties() encoding str properties as bytesTypeError marshaling outbound messages (pamqp 4 requires str)

Solution

Test infrastructure:

  • Rename base_tests.pytest_base.py, channel_test.pytest_channel.py, integration_tests.pytest_integration.py, utils_tests.pytest_utils.py
  • Add dotenv.load_dotenv() to test_integration.py so RABBITMQ_URL is available without depending on test execution order
  • Add three new unit test files: test_channel0.py (95% coverage), test_connection.py (90% coverage), test_heartbeat.py (100% coverage)
  • Add integration tests: multiple channels, nack-without-requeue, queue purge, large messages, channel context manager, transactions
  • Restore auto-connect in Connection.__init__() to match the original API contract

Bug fixes:

  • base.py: commands.Framepamqp.base.Frame; class_id=0, method_id=0 in _build_close_frame()
  • channel.py: replace properties.to_dict() with attributes()-based dict comprehension
  • channel0.py: add _raise_if_server_close() so ACCESS_REFUSED raises AMQPAccessRefused not a generic ConnectionException
  • message.py: fix _coerce_properties() to produce str not bytes for shortstr properties

Impact

  • 368 tests pass (up from 296), including 39 integration tests against a live broker
  • Coverage: 62% → 84%; channel.py 22% → 62%, base.py 35% → 76%, simple.py 24% → 95%
  • All four bug fixes are breaking runtime failures — the library could not publish or consume messages without them
  • No API changes visible to users

gmr and others added 2 commits April 1, 2026 13:53
Adds 97 new tests (296 total, up from 199) targeting previously untested
critical modules:

tests/test_channel0.py (new):
- Full happy-path AMQP handshake: ProtocolHeader → Start/StartOk →
  Tune/TuneOk → Open/OpenOk; verifies every frame is sent and stored
- Heartbeat negotiation edge cases: server/client zero, both zero,
  server smaller value
- Negotiation error paths: wrong frame type at each step, pre-injected
  exception, EXCEPTION_RAISED event set on failure
- Runtime frames: Connection.Blocked/Unblocked events, server-initiated
  Connection.Close with AMQP reply-code mapping
- Channel0.send_heartbeat and close() behaviour

tests/test_connection.py (new):
- Connection.__init__ and URL parsing
- connect() happy path, socket timeout, socket exception, negotiation
  timeout, negotiation exception propagation, heartbeat start/skip
- channel() method: return type, first/second ID, channels dict, IO
  registration, raises when closed
- _get_next_channel_id(): first id=1, increment, reclaim closed id,
  remove closed from dict, TooManyChannelsError (#121)
- capabilities/server_properties properties, blocked flag

tests/test_heartbeat.py (new):
- Start/stop lifecycle, zero-interval disabled path, double-stop safety
- _maybe_send(): sends when no data written, skips when data written,
  skips when stopped, updates last_written, restarts timer

tests/integration_tests.py (additions):
- MultipleChannelsTest: two channels have different IDs; closed channel
  ID is reused; independent queues on separate channels
- NackWithoutRequeueTest: nacked message is discarded not requeued
- QueuePurgeTest: purge empties a 5-message queue
- LargeMessageTest: 300 kB body round-trips correctly (multi-frame)
- ChannelContextManagerTest: channel closes on exit, new channel opens
- TransactionTest: committed message visible, rolled-back message absent

Coverage improvements:
- channel0.py:   65% → 94%
- connection.py: 57% → 88%
- heartbeat.py:  61% → 100%
- Overall:       62% → 68%

Co-Authored-By: Gavin M. Roy <gavinmroy@gmail.com>
This commit fixes a series of runtime failures discovered when the
integration test suite was properly enabled, all caused by API differences
between pamqp 3 (which the restored code was written for) and pamqp 4:

rabbitpy/base.py:
- Replace commands.Frame (removed in pamqp 4) with pamqp.base.Frame in
  _validate_frame_type() — was silently falling through, causing
  _wait_on_frame() to loop forever instead of matching the expected frame
- Pass explicit class_id=0, method_id=0 to Channel.Close in
  _build_close_frame() — pamqp 4 encodes None fields as short_uint and
  raises TypeError on None

rabbitpy/channel.py:
- Replace properties.to_dict() (removed in pamqp 4) with a dict
  comprehension over properties.attributes() in _create_message()

rabbitpy/channel0.py:
- Add _raise_if_server_close() helper called at each _wait_for_frame()
  step in _negotiate(); ensures auth failures (ACCESS_REFUSED/403) raise
  AMQPAccessRefused rather than a generic ConnectionException

rabbitpy/connection.py:
- Restore auto-connect in __init__() to match the original rabbitpy API
  (Connection(url) connects on construction); guard __enter__() to skip
  connect() if already open

rabbitpy/message.py:
- Fix _coerce_properties(): pamqp 4 requires str for shortstr fields;
  replace maybe_utf8_encode() (which produced bytes) with UTF-8 decode
  for incoming bytes values

Test infrastructure:
- Rename base_tests.py → test_base.py, channel_test.py → test_channel.py,
  integration_tests.py → test_integration.py, utils_tests.py → test_utils.py
  so unittest discover (test*.py pattern) picks them up — these 130+ tests
  were silently not running before
- Add dotenv loading to test_integration.py so RABBITMQ_URL is available
- Fix test_base: test_name_bytes now expects ValueError (bytes rejected)
- Fix test_channel: test_invoking_consume_raises uses register_consumer
- Fix test_message: test_coerce_property_int_to_str expects str not bytes
- Fix test_integration: remove .decode('utf-8') calls on message_id
  (properties are now str, not bytes)
- Update test_connection._make_conn() to patch connect() so unit tests
  do not attempt a real network connection on instantiation

Co-Authored-By: Gavin M. Roy <gavinmroy@gmail.com>
@coderabbitai

coderabbitai Bot commented Apr 1, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@gmr has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 2 minutes and 41 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 2 minutes and 41 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: cb04fbfb-b407-4061-a9e9-d328068676cf

📥 Commits

Reviewing files that changed from the base of the PR and between 4152a88 and b513029.

📒 Files selected for processing (14)
  • pyproject.toml
  • rabbitpy/base.py
  • rabbitpy/channel.py
  • rabbitpy/channel0.py
  • rabbitpy/connection.py
  • rabbitpy/message.py
  • tests/test_base.py
  • tests/test_channel.py
  • tests/test_channel0.py
  • tests/test_connection.py
  • tests/test_heartbeat.py
  • tests/test_integration.py
  • tests/test_message.py
  • tests/test_utils.py
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/test-infrastructure-and-pamqp4-compat

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

gmr and others added 2 commits April 1, 2026 15:14
Seven categories of issues were identified from CI logs and resolved:

- ruff format: apply canonical formatting to 6 source/test files
- ruff RUF059: prefix 7 unused unpacked variables with _ in
  test_channel0.py (ch0, ev, exc pattern from _make())
- mypy var-annotated: annotate exc as queue.Queue[Exception] in _make()
- basedpyright ConnectionArgs: type _make_args return as ConnectionArgs
  and supply a properly typed SslOptions for ssl_options; add
  SslOptions import to test_channel0.py
- mypy method-assign: replace direct method monkey-patch in
  TestConnectionChannelMethod.setUp/tearDown with mock.patch.object so
  mypy no longer rejects the assignment
- basedpyright Optional member access: use pyright: ignore comments on
  two lines in test_connection.py that access mock attributes through
  Optional-typed fields (_io and _channel0)
- mypy attr-defined / basedpyright: add tests/test_integration.py to
  the basedpyright exclude list and the mypy ignore_errors override;
  update stale legacy module names in the mypy overrides block

All 207 unit tests pass; pre-commit suite (ruff format, ruff lint,
mypy, basedpyright) is clean.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Without publisher confirms, all 5 Basic.Publish frames were sent to
the broker in rapid succession with no acknowledgment, then len()
immediately issued a passive Queue.Declare — which returned whatever
count RabbitMQ had processed so far (typically 2 of 5), causing the
assertion to fail intermittently on all Python versions.

Adding channel.enable_publisher_confirms() before publishing makes
each publish() call block until RabbitMQ sends Basic.Ack, guaranteeing
all 5 messages are queued before the test assertion runs.

Co-Authored-By: Gavin M. Roy <gavinmroy@gmail.com>
@gmr gmr merged commit daef08c into main Apr 1, 2026
5 checks passed
@gmr gmr deleted the fix/test-infrastructure-and-pamqp4-compat branch April 1, 2026 19:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant