Skip to content

Conversation

@ttypic
Copy link
Contributor

@ttypic ttypic commented Aug 27, 2025

Summary by CodeRabbit

  • Tests

    • Reworked publish flow tests to use waiter-based polling instead of fixed delays, reducing flakiness and improving reliability.
    • Added async and sync waiter utilities to support consistent, timeout-bound condition checking in tests.
  • Chores

    • Updated internal async-to-sync conversion mapping to keep naming consistent across generated code.

@coderabbitai
Copy link

coderabbitai bot commented Aug 27, 2025

Walkthrough

Adds waiter utilities for async and sync polling in tests, updates REST channel publish tests to use waiter-based checks instead of sleeps, and extends unasync token replacement to map assert_waiter to assert_waiter_sync.

Changes

Cohort / File(s) Summary
Unasync token mapping
ably/scripts/unasync.py
Added token replacement: map assert_waiter to assert_waiter_sync in unasync run() to support sync conversion.
Test utilities: waiter helpers
test/ably/utils.py
Introduced assert_waiter (async), _poll_until_success (async helper), and assert_waiter_sync (sync) for polling conditions with timeout. Added asyncio/time imports.
REST publish tests updated to waiter pattern
test/ably/rest/restchannelpublish_test.py
Replaced sleeps with waiter-based polling. Added assert_waiter import. Refactored validations into async check functions for publish result and history; assert HTTP 201 responses. Removed asyncio import.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant T as Test (pytest)
  participant C as REST Channel
  participant W as Waiter (assert_waiter)
  participant A as Ably REST API

  T->>C: publish(data or messages)
  C-->>T: HTTP response (201)

  T->>W: await assert_waiter(check_data)
  loop poll until condition true or timeout
    W->>A: GET latest message / history
    A-->>W: Message payload
    W-->>W: verify encoding/data/type
  end
  W-->>T: success or raise TimeoutError
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20–30 minutes

Poem

A rabbit taps in patient loops,
No more sleeps or drowsy swoops—
Waiters watch the messages hop,
Polling gently, stop by stop.
Sync or async, whiskers twitch—
Tests now pass without a hitch. 🐇✨

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/try-to-fix-broken-tests

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
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore or @coderabbit ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@github-actions github-actions bot temporarily deployed to staging/pull/616/features August 27, 2025 12:15 Inactive
@ttypic ttypic requested review from VeskeR and sacOO7 September 2, 2025 11:03
Copy link
Collaborator

@sacOO7 sacOO7 left a comment

Choose a reason for hiding this comment

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

So, tests were failing because of this issue? Since sleep statements often make tests flaky, I asked cursor for kotlin assertWaiter equivalent here

import asyncio
from typing import Callable, Awaitable

async def assert_waiter(block: Callable[[], Awaitable[bool]], timeout_ms: int = 10_000) -> None:
    timeout_seconds = timeout_ms / 1000.0
    
    try:
        await asyncio.wait_for(_poll_until_success(block), timeout=timeout_seconds)
    except asyncio.TimeoutError:
        raise asyncio.TimeoutError(f"Condition not met within {timeout_ms}ms")

async def _poll_until_success(block: Callable[[], Awaitable[bool]]) -> None:
    while True:
        success = await block()
        if success:
            break
        await asyncio.sleep(0.1)

Usage for the same =>

async def test_something():
    async def condition_checker():
        return some_condition_is_met()
    
    await assert_waiter(condition_checker)
    await assert_waiter(condition_checker, timeout_ms=5000)

I guess this will be similar to waiting library -> https://pypi.org/project/waiting/

@ttypic ttypic force-pushed the fix/try-to-fix-broken-tests branch from a05d4f6 to f8850ae Compare September 3, 2025 09:38
@ttypic ttypic marked this pull request as ready for review September 3, 2025 09:38
@github-actions github-actions bot temporarily deployed to staging/pull/616/features September 3, 2025 09:39 Inactive
@ttypic ttypic force-pushed the fix/try-to-fix-broken-tests branch from f8850ae to ac541c0 Compare September 3, 2025 09:46
@github-actions github-actions bot temporarily deployed to staging/pull/616/features September 3, 2025 09:47 Inactive
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (1)
test/ably/utils.py (1)

209-229: Use monotonic clock for timeouts in sync waiter

time.time() is wall-clock and can go backwards/forwards. Use time.monotonic() for elapsed timing.

-    start_time = time.time()
+    start_time = time.monotonic()
@@
-        if time.time() - start_time >= timeout:
+        if time.monotonic() - start_time >= timeout:
             raise TimeoutError(f"Condition not met within {timeout}s")
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between f8850ae and ac541c0.

📒 Files selected for processing (3)
  • ably/scripts/unasync.py (1 hunks)
  • test/ably/rest/restchannelpublish_test.py (2 hunks)
  • test/ably/utils.py (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • ably/scripts/unasync.py
🧰 Additional context used
🧬 Code graph analysis (1)
test/ably/rest/restchannelpublish_test.py (3)
ably/rest/channel.py (3)
  • ably (143-144)
  • publish (106-132)
  • history (32-39)
test/ably/utils.py (1)
  • assert_waiter (186-198)
ably/http/paginatedresult.py (2)
  • status_code (116-117)
  • items (56-57)
🪛 Ruff (0.12.2)
test/ably/utils.py

198-198: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


198-198: Avoid specifying long messages outside the exception class

(TRY003)


226-226: Avoid specifying long messages outside the exception class

(TRY003)

test/ably/rest/restchannelpublish_test.py

405-405: Use of assert detected

(S101)


411-411: Function definition does not bind loop variable encoding

(B023)


412-412: Function definition does not bind loop variable encoding

(B023)


413-413: Function definition does not bind loop variable msg_data

(B023)


415-415: Function definition does not bind loop variable msg_data

(B023)


421-421: Use of assert detected

(S101)


426-426: Function definition does not bind loop variable expected_value

(B023)


426-426: Use is and is not for type comparisons, or isinstance() for isinstance checks

(E721)


426-426: Function definition does not bind loop variable expected_type

(B023)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (7)
  • GitHub Check: check (3.11)
  • GitHub Check: check (3.9)
  • GitHub Check: check (3.7)
  • GitHub Check: check (3.8)
  • GitHub Check: check (3.10)
  • GitHub Check: check (3.13)
  • GitHub Check: check (3.12)
🔇 Additional comments (1)
test/ably/rest/restchannelpublish_test.py (1)

22-22: assert_waiter mapping confirmed
The unasync token map in ably/scripts/unasync.py includes assert_waiter → assert_waiter_sync, so sync-converted tests will use assert_waiter_sync instead of the async variant.

@ttypic ttypic force-pushed the fix/try-to-fix-broken-tests branch from ac541c0 to ab9306d Compare September 3, 2025 12:31
@github-actions github-actions bot temporarily deployed to staging/pull/616/features September 3, 2025 12:32 Inactive
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
test/ably/utils.py (1)

213-236: Fix CI: avoid bare except in sync waiter

Same E722 issue in the sync path; keep retries while not masking system-exiting exceptions.

     while True:
         try:
             success = block()
             if success:
-                break
-        except:
-            pass
+                break
+        except Exception:
+            pass
         if time.time() - start_time >= timeout:
             raise TimeoutError(f"Condition not met within {timeout}s")
         time.sleep(0.1)
♻️ Duplicate comments (3)
test/ably/utils.py (1)

186-199: Match docstring: raise built-in TimeoutError and suppress chaining

Raise the built-in TimeoutError (not asyncio.TimeoutError) and use from None to satisfy B904 and align with the docstring.

     try:
-        await asyncio.wait_for(_poll_until_success(block), timeout=timeout)
-    except asyncio.TimeoutError:
-        raise asyncio.TimeoutError(f"Condition not met within {timeout}s")
+        await asyncio.wait_for(_poll_until_success(block), timeout=timeout)
+    except asyncio.TimeoutError:
+        raise TimeoutError(f"Condition not met within {timeout}s") from None
test/ably/rest/restchannelpublish_test.py (2)

404-417: Harden polling: guard empty list, bind loop vars, add client timeout

Prevents IndexError during eventual consistency, avoids late-binding issues (B023), and keeps polls snappy.

-                async def check_data():
-                    async with httpx.AsyncClient(http2=True) as client:
-                        r = await client.get(url, auth=auth)
-                        item = r.json()[0]
-                        encoding_is_correct = item.get('encoding') == encoding
-                        if encoding == 'json':
-                            return encoding_is_correct and json.loads(item['data']) == json.loads(msg_data)
-                        else:
-                            return encoding_is_correct and item['data'] == msg_data
+                async def check_data(encoding=encoding, msg_data=msg_data):
+                    async with httpx.AsyncClient(http2=True, timeout=2.0) as client:
+                        r = await client.get(url, auth=auth)
+                        items = r.json()
+                        if not items:
+                            return False
+                        item = items[0]
+                        encoding_is_correct = item.get('encoding') == encoding
+                        if encoding == 'json':
+                            return encoding_is_correct and json.loads(item['data']) == json.loads(msg_data)
+                        else:
+                            return encoding_is_correct and item['data'] == msg_data

420-428: Harden history check: guard empty, bind vars, and use isinstance

Avoids empty history access, captures expected values, and replaces direct type equality per E721.

-                async def check_history():
-                    history = await channel.history()
-                    message = history.items[0]
-                    return message.data == expected_value and type(message.data) == type_mapping[expected_type]
+                async def check_history(expected_value=expected_value, expected_type=expected_type):
+                    history = await channel.history()
+                    items = history.items
+                    if not items:
+                        return False
+                    message = items[0]
+                    return message.data == expected_value and isinstance(message.data, type_mapping[expected_type])
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between ac541c0 and ab9306d.

📒 Files selected for processing (3)
  • ably/scripts/unasync.py (1 hunks)
  • test/ably/rest/restchannelpublish_test.py (2 hunks)
  • test/ably/utils.py (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • ably/scripts/unasync.py
🧰 Additional context used
🧬 Code graph analysis (1)
test/ably/rest/restchannelpublish_test.py (2)
ably/rest/channel.py (3)
  • ably (143-144)
  • publish (106-132)
  • history (32-39)
test/ably/utils.py (1)
  • assert_waiter (186-198)
🪛 Ruff (0.12.2)
test/ably/rest/restchannelpublish_test.py

405-405: Use of assert detected

(S101)


411-411: Function definition does not bind loop variable encoding

(B023)


412-412: Function definition does not bind loop variable encoding

(B023)


413-413: Function definition does not bind loop variable msg_data

(B023)


415-415: Function definition does not bind loop variable msg_data

(B023)


421-421: Use of assert detected

(S101)


426-426: Function definition does not bind loop variable expected_value

(B023)


426-426: Use is and is not for type comparisons, or isinstance() for isinstance checks

(E721)


426-426: Function definition does not bind loop variable expected_type

(B023)

test/ably/utils.py

198-198: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


198-198: Avoid specifying long messages outside the exception class

(TRY003)


207-207: Do not use bare except

(E722)


207-208: try-except-pass detected, consider logging the exception

(S110)


229-229: Do not use bare except

(E722)


229-230: try-except-pass detected, consider logging the exception

(S110)


233-233: Avoid specifying long messages outside the exception class

(TRY003)

🪛 GitHub Actions: Linting check
test/ably/utils.py

[error] 207-207: flake8: E722 do not use bare 'except' during 'poetry run flake8' step.


[error] 229-229: flake8: E722 do not use bare 'except' during 'poetry run flake8' step.

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (7)
  • GitHub Check: check (3.12)
  • GitHub Check: check (3.13)
  • GitHub Check: check (3.11)
  • GitHub Check: check (3.10)
  • GitHub Check: check (3.8)
  • GitHub Check: check (3.7)
  • GitHub Check: check (3.9)
🔇 Additional comments (1)
test/ably/rest/restchannelpublish_test.py (1)

22-22: Verified mapping for assert_waiter
assert_waiterassert_waiter_sync is defined in _TOKEN_REPLACE within scripts/unasync.py.

Copy link
Collaborator

@sacOO7 sacOO7 left a comment

Choose a reason for hiding this comment

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

Seems we have other flaky tests, current test_interoperability working just fine.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (4)
test/ably/utils.py (2)

186-199: Raise built‑in TimeoutError and suppress chaining to match docstring

Unify with sync variant; avoid surfacing asyncio-specific type.

-    try:
-        await asyncio.wait_for(_poll_until_success(block), timeout=timeout)
-    except asyncio.TimeoutError:
-        raise asyncio.TimeoutError(f"Condition not met within {timeout}s")
+    try:
+        await asyncio.wait_for(_poll_until_success(block), timeout=timeout)
+    except asyncio.TimeoutError as err:
+        raise TimeoutError(f"Condition not met within {timeout}s") from None

201-211: Don’t swallow cancellations; re‑raise CancelledError

Swallowing CancelledError breaks task cancellation semantics and can hang shutdowns.

 async def _poll_until_success(block: Callable[[], Awaitable[bool]]) -> None:
     while True:
         try:
             success = await block()
             if success:
                 break
-        except Exception:
-            pass
+        except asyncio.CancelledError:
+            raise
+        except Exception:
+            pass
         await asyncio.sleep(0.1)
test/ably/rest/restchannelpublish_test.py (2)

404-417: Avoid flakiness: guard empty list, bind loop vars, add per‑request timeout

Prevents IndexError before persistence; avoids late-binding issues; keeps polls snappy.

-                async def check_data():
-                    async with httpx.AsyncClient(http2=True) as client:
-                        r = await client.get(url, auth=auth)
-                        item = r.json()[0]
-                        encoding_is_correct = item.get('encoding') == encoding
-                        if encoding == 'json':
-                            return encoding_is_correct and json.loads(item['data']) == json.loads(msg_data)
-                        else:
-                            return encoding_is_correct and item['data'] == msg_data
+                async def check_data(encoding=encoding, msg_data=msg_data):
+                    async with httpx.AsyncClient(http2=True, timeout=2.0) as client:
+                        r = await client.get(url, auth=auth)
+                        items = r.json()
+                        if not items:
+                            return False
+                        item = items[0]
+                        encoding_is_correct = item.get('encoding') == encoding
+                        if encoding == 'json':
+                            return encoding_is_correct and json.loads(item['data']) == json.loads(msg_data)
+                        else:
+                            return encoding_is_correct and item['data'] == msg_data

420-428: History polling: guard empties, bind vars, and prefer isinstance

Prevents IndexError and E721; robust to subclasses.

-                async def check_history():
-                    history = await channel.history()
-                    message = history.items[0]
-                    return message.data == expected_value and type(message.data) == type_mapping[expected_type]
+                async def check_history(expected_value=expected_value, expected_type=expected_type):
+                    history = await channel.history()
+                    items = history.items
+                    if not items:
+                        return False
+                    message = items[0]
+                    return message.data == expected_value and isinstance(message.data, type_mapping[expected_type])
🧹 Nitpick comments (2)
test/ably/utils.py (1)

213-236: Use monotonic clock for timeout accounting

Avoids skew from system clock changes.

-    start_time = time.time()
+    start_time = time.monotonic()
@@
-        if time.time() - start_time >= timeout:
+        if time.monotonic() - start_time >= timeout:
             raise TimeoutError(f"Condition not met within {timeout}s")
test/ably/rest/restchannelpublish_test.py (1)

404-417: Optional: reuse one AsyncClient across polls to cut connection churn

Create one client and close it in finally; bind into closures.

Example:

client = httpx.AsyncClient(http2=True, timeout=2.0)
try:
    async def check_data(client=client, encoding=encoding, msg_data=msg_data):
        r = await client.get(url, auth=auth)
        ...
    async def check_history(client=client, expected_value=expected_value, expected_type=expected_type):
        history = await channel.history()
        ...
    await assert_waiter(check_data)
    await assert_waiter(check_history)
finally:
    await client.aclose()

Also applies to: 420-428

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between ab9306d and 4f08dc8.

📒 Files selected for processing (3)
  • ably/scripts/unasync.py (1 hunks)
  • test/ably/rest/restchannelpublish_test.py (2 hunks)
  • test/ably/utils.py (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • ably/scripts/unasync.py
🧰 Additional context used
🧬 Code graph analysis (1)
test/ably/rest/restchannelpublish_test.py (3)
ably/rest/channel.py (3)
  • ably (143-144)
  • publish (106-132)
  • history (32-39)
test/ably/utils.py (1)
  • assert_waiter (186-198)
ably/types/message.py (1)
  • Message (24-226)
🪛 Ruff (0.12.2)
test/ably/rest/restchannelpublish_test.py

405-405: Use of assert detected

(S101)


411-411: Function definition does not bind loop variable encoding

(B023)


412-412: Function definition does not bind loop variable encoding

(B023)


413-413: Function definition does not bind loop variable msg_data

(B023)


415-415: Function definition does not bind loop variable msg_data

(B023)


421-421: Use of assert detected

(S101)


426-426: Function definition does not bind loop variable expected_value

(B023)


426-426: Use is and is not for type comparisons, or isinstance() for isinstance checks

(E721)


426-426: Function definition does not bind loop variable expected_type

(B023)

test/ably/utils.py

198-198: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


198-198: Avoid specifying long messages outside the exception class

(TRY003)


207-208: try-except-pass detected, consider logging the exception

(S110)


207-207: Do not catch blind exception: Exception

(BLE001)


229-230: try-except-pass detected, consider logging the exception

(S110)


229-229: Do not catch blind exception: Exception

(BLE001)


233-233: Avoid specifying long messages outside the exception class

(TRY003)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (7)
  • GitHub Check: check (3.11)
  • GitHub Check: check (3.12)
  • GitHub Check: check (3.9)
  • GitHub Check: check (3.13)
  • GitHub Check: check (3.10)
  • GitHub Check: check (3.8)
  • GitHub Check: check (3.7)
🔇 Additional comments (1)
test/ably/utils.py (1)

1-1: LGTM: imports added for waiter utilities

No issues spotted.

Also applies to: 7-9

Copy link
Collaborator

@sacOO7 sacOO7 left a comment

Choose a reason for hiding this comment

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

You can double check if some other tests are flaky and create a new issue to fix them
Otherwise LGTM

@ttypic ttypic merged commit b5a563d into main Sep 4, 2025
16 of 17 checks passed
@ttypic ttypic deleted the fix/try-to-fix-broken-tests branch September 4, 2025 13:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

3 participants