Skip to content
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

[ECO-5009][CHA-RL1] RoomLifecycle ATTACH tests #37

Open
wants to merge 21 commits into
base: feature/roomlifecycle-attach-with-retry
Choose a base branch
from

Conversation

sacOO7
Copy link

@sacOO7 sacOO7 commented Oct 25, 2024

  • Related to [CHA-RL1] Implement Room ATTACH operation #21
    Unit tests will be covered as per
  • CHA-RL1a) [Testable] If the room is already in the ATTACHED status, then this operation is no-op.
  • (CHA-RL1b) [Testable] If the room is in the RELEASING status, the operation shall be rejected with an ErrorInfo with the RoomIsReleasing error code from the chat-specific error codes.
  • (CHA-RL1c) [Testable] If the room is in the RELEASED status, the operation shall be rejected with an ErrorInfo with the RoomIsReleased error code from the chat-specific error codes.
  • (CHA-RL1d) [Testable] If a room lifecycle operation is already in progress, this operation shall wait until the current operation completes before continuing, subject to CHA-RL7.
  • (CHA-RL1e) [Testable] Notwithstanding the above points, when the attachment operation begins, the room shall be transitioned to the ATTACHING status.
  • (CHA-RL1f) [Testable] The underlying contributors will have their Realtime Channels attached in sequence – an attach call must complete successfully before the next is started.
  • (CHA-RL1g) When all contributors Realtime Channels successfully attach (the calls to attach() complete successfully), the operation is now complete and the room is considered attached.
  • (CHA-RL1g1) [Testable] The room status shall be transitioned to ATTACHED.
  • (CHA-RL1g2) [Testable] Any contributors to room that have a pending discontinuity event against them, must be notified of this fact, using the error that caused the original discontinuity.
  • (CHA-RL1g3) [Testable] Any transient disconnect timeouts shall be cleared.
  • (CHA-RL1h) If a one of the contributors Realtime Channels fails to attach (i.e. the call to attach() returns an error), then the operation has failed and must be rolled back. The procedure to roll back is described in subsequent points.
  • (CHA-RL1h1) [Testable] The attach call must throw an ErrorInfo. The code for this error is determined by the feature that failed (see here for more information on error codes).
  • (CHA-RL1h2) [Testable] If the underlying Realtime Channel entered the SUSPENDED state (i.e. the attach() operation fails and upon subsequently checking the channel state, it is SUSPENDED), then the room status will transition to SUSPENDED. The state transition ErrorInfo will use the Realtime Channel error as the cause. The status code shall be 500 and the error code whatever attachment error code corresponds to the feature that has failed, per the error code list.
  • (CHA-RL1h3) [Testable] When the room enters the SUSPENDED status as a result of CHA-RL1h2 the attach() operation shall fail using the error from CHA-RL1h2. The room lifecycle shall asynchronously (non-blocking to the ATTACH operation) enter the recovery loop (per CHA-RL5) and attempt to re-establish attachment.
  • (CHA-RL1h4) [Testable] If the underlying Realtime Channel entered the FAILED state, then the room status will transition to FAILED, using the error from the realtime channels attach() call as the cause field of the ErrorInfo. The state transition ErrorInfo will use the Realtime Channel error as the cause. The status code shall be 500 and the error code whatever attachment error code corresponds to the feature that has failed, per the error code list. The same error shall be thrown as the result of the ATTACH operation.
  • (CHA-RL1h5) [Testable] When the room enters the FAILED status as a result of CHA-RL1h4, asynchronously with respect to CHA-RL1h4, then the room will detach all channels that are not in the FAILED state.
  • (CHA-RL1h6) [Testable] If, when performing CHA-RL1h5, a channel fails to detach on command, then the detach operation will be repeated until such a time that all channels have detached successfully.
  • - Annotate code with related spec

Summary by CodeRabbit

Release Notes

  • New Features

    • Introduced featureName properties across various classes to enhance feature identification (e.g., messages, occupancy, presence, reactions, typing).
    • Added a new extension property errorMessage for the Channel class to improve error reporting.
  • Improvements

    • Enhanced error handling and clarity in the RoomLifecycleManager with updated attachment processes and error messages.
    • Updated the constructor of the DefaultStatus class to allow for optional logger initialization.
  • Tests

    • Added comprehensive unit tests for RoomLifecycleManager to validate channel attachment and state transitions.
    • Introduced utility functions in test helpers to facilitate testing of room lifecycle features.
  • Chores

    • Updated the mockk library version for improved testing capabilities.

Copy link

coderabbitai bot commented Oct 25, 2024

Walkthrough

The pull request introduces several modifications across multiple classes in the chat-android module. Key changes include the addition of a featureName property to various classes such as DefaultMessages, DefaultOccupancy, DefaultPresence, DefaultRoomReactions, and DefaultTyping, each assigned a specific string identifier. Additionally, the RoomLifecycleManager class and its interface were enhanced with new methods and improved error handling. The DefaultStatus class constructor was modified to allow a default logger parameter. New utility functions for testing were also added, alongside an update to the mockk library version.

Changes

File Path Change Summary
chat-android/src/main/java/com/ably/chat/Messages.kt Added property override val featureName: String in DefaultMessages.
chat-android/src/main/java/com/ably/chat/Occupancy.kt Added property val featureName: String in DefaultOccupancy.
chat-android/src/main/java/com/ably/chat/Presence.kt Added property override val featureName: String in DefaultPresence.
chat-android/src/main/java/com/ably/chat/RoomLifecycleManager.kt Added property val featureName: String in ContributesToRoomLifecycle interface; added methods internal suspend fun detach() and internal suspend fun release() in RoomLifecycleManager.
chat-android/src/main/java/com/ably/chat/RoomReactions.kt Added property override val featureName: String in DefaultRoomReactions; updated send method to handle params.headers.
chat-android/src/main/java/com/ably/chat/RoomStatus.kt Updated constructor to private constructor(logger: LogHandler? = null); added overloaded method fun setStatus(status: RoomLifecycle, error: ErrorInfo? = null).
chat-android/src/main/java/com/ably/chat/Typing.kt Added property override val featureName: String in DefaultTyping.
chat-android/src/main/java/com/ably/chat/Utils.kt Added extension property val Channel.errorMessage: String.
chat-android/src/test/java/com/ably/chat/RoomLifecycleManagerTest.kt Introduced unit tests for RoomLifecycleManager.
chat-android/src/test/java/com/ably/chat/TestUtils.kt Added functions fun Any.setPrivateField(name: String, value: Any?) and fun <T> Any.getPrivateField(name: String): T.
chat-android/src/test/java/com/ably/utils/RoomLifecycleManagerTestHelpers.kt Added functions for testing utilities related to RoomLifecycleManager.
gradle/libs.versions.toml Updated mockk library version from 1.13.12 to 1.13.13.

Assessment against linked issues

Objective Addressed Explanation
Implement Room ATTACH operation (ECO-5009)

Possibly related issues

  • [CHA-RL1] Implement Room ATTACH operation: The changes in the pull request enhance the room lifecycle management, which is directly related to the implementation of the room attach operation.
  • [CHA-RL7] Room lifecycle operation precedence: The modifications in RoomLifecycleManager may address aspects of room lifecycle operation precedence.
  • [CHA-RL1] Implement Room ATTACH operation: Similar objectives regarding room attachment are present in both the PR and the issue.

Possibly related PRs

Suggested reviewers

  • umair-ably
  • AndyTWF

Poem

🐰 In the chat room where bunnies play,
A feature name brightens the way.
From messages to presence, all in line,
With each new change, our code will shine!
So hop along, let's test and see,
A better chat for you and me! 🐇


Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

❤️ 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.
    • Generate unit testing code for this file.
    • 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. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • 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 src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai 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

Documentation and Community

  • 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.

@sacOO7 sacOO7 changed the title RoomLifecycle ATTACH tests [ECO-5009][CHA-RL1] RoomLifecycle ATTACH tests Oct 29, 2024
Copy link
Collaborator

@ttypic ttypic left a comment

Choose a reason for hiding this comment

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

LGTM

@sacOO7 sacOO7 marked this pull request as ready for review November 8, 2024 12:41
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

🧹 Outside diff range and nitpick comments (19)
chat-android/src/test/java/com/ably/utils/RoomLifecycleManagerTestHelpers.kt (2)

21-21: Add documentation explaining the purpose and testing context.

While accessing private fields via reflection is sometimes necessary in tests, it's a fragile approach that could break with implementation changes. Adding documentation would help explain why this access is needed and how it's used in tests.

+/**
+ * Test helper to access the private atomicCoroutineScope field of RoomLifecycleManager.
+ * This is needed for testing concurrent operations and verifying scope management.
+ * Note: This uses reflection and might break if the private field name changes.
+ */
 fun RoomLifecycleManager.atomicCoroutineScope(): AtomicCoroutineScope = getPrivateField("atomicCoroutineScope")

23-26: Add documentation and state validation.

The function modifies channel state directly without validation. Consider adding documentation and basic validation to prevent invalid states in tests.

+/**
+ * Test helper to set the state of an Ably Realtime Channel.
+ * Used for simulating different channel states in tests.
+ *
+ * @param state The desired channel state
+ * @param errorInfo Optional error information associated with the state
+ * @throws IllegalArgumentException if the state transition would be invalid
+ */
 fun AblyRealtimeChannel.setState(state: ChannelState, errorInfo: ErrorInfo? = null) {
+    // Validate state transition
+    if (state == ChannelState.attaching && this.state == ChannelState.failed) {
+        throw IllegalArgumentException("Cannot transition from FAILED to ATTACHING state")
+    }
     this.state = state
     this.reason = errorInfo
 }
chat-android/src/main/java/com/ably/chat/Occupancy.kt (1)

Line range hint 77-83: Note: Unimplemented methods present.

The subscribe() and get() methods are marked with TODO. While these implementations are outside the scope of the current PR (which focuses on Room ATTACH operation tests), they should be tracked for future implementation.

Would you like me to create a GitHub issue to track the implementation of these methods?

chat-android/src/main/java/com/ably/chat/Typing.kt (1)

Implement the unimplemented methods in DefaultTyping class to support room attachment

The verification shows that:

  1. DefaultTyping is used in production code and tests
  2. The class is a critical component for room attachment as shown in RoomLifecycleManagerTest.kt
  3. The unimplemented methods are blocking proper testing of room attachment functionality

Implementation points:

  • The subscribe, get, start, and stop methods in DefaultTyping class need to be implemented
  • These methods are required for proper room attachment testing as evidenced by test cases like CHA-RL1f which verifies sequential channel attachment including the typing indicators channel
  • The implementation is critical as typing indicator channel failures can cause the entire room to enter a failed state (see test CHA-RL1h1, CHA-RL1h4)
🔗 Analysis chain

Line range hint 101-119: Implement TODO methods to support room attachment testing.

The unimplemented methods (subscribe, get, start, stop) need to be implemented to properly test the Room ATTACH operation, as they are crucial for verifying the room's state transitions and error handling during attachment.

Let's verify if these methods are implemented in test files:

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Check for test implementations of DefaultTyping methods
# Expected: Find test files containing DefaultTyping tests

# Search for test files related to DefaultTyping
rg -l "DefaultTyping" --glob "**/*Test.kt"

# Search for specific method implementations in test files
ast-grep --pattern 'fun test$_(subscribe|get|start|stop)' 

Length of output: 102


Script:

#!/bin/bash
# Let's try a different approach to find test implementations

# First, let's find the exact location of the Typing interface and implementation
rg -l "interface Typing" --type kotlin

# Then find all test files that might contain typing-related tests
rg -l "Typing" --glob "**/*Test.kt" --glob "**/*Spec.kt"

# Look for any TODO implementations in Kotlin files
rg "TODO\(.*implemented.*\)" --type kotlin

# Check for any typing-related test classes
ast-grep --pattern 'class $_TypeTest {
  $$$
}'

Length of output: 1473


Script:

#!/bin/bash
# Let's examine the RoomLifecycleManagerTest.kt that references Typing
cat chat-android/src/test/java/com/ably/chat/RoomLifecycleManagerTest.kt

# Also check if there are any other files that might contain typing-related tests
# by searching for DefaultTyping class usage
rg "DefaultTyping" --type kotlin -C 3

# Look for any mock implementations of Typing interface in test files
rg "class.*implements.*Typing" --type kotlin
rg "class.*:.*Typing" --type kotlin

Length of output: 21854

chat-android/src/main/java/com/ably/chat/Utils.kt (1)

53-58: Add documentation to clarify usage intention.

The implementation is correct, but the comma prefix suggests this is meant to be concatenated with other messages. Consider:

  1. Adding KDoc to explain the intended usage
  2. Renaming to something more explicit like errorMessageSuffix to indicate it's meant for concatenation
+/**
+ * Returns a comma-prefixed error message from the channel's reason, 
+ * intended to be concatenated with other error messages.
+ * Returns empty string if no error reason is present.
+ */
 val Channel.errorMessage: String
     get() = if (reason == null) {
         ""
     } else {
         ", ${reason.message}"
     }
chat-android/src/main/java/com/ably/chat/RoomStatus.kt (2)

177-179: Remove unused property _logger

The private property _logger is declared but never used in the code. Consider removing it since the base logger property is sufficient.

class DefaultStatus(private val logger: LogHandler? = null) : InternalRoomStatus {
-    private val _logger = logger

177-177: LGTM with suggestions for improvement

The changes look good:

  1. Adding a default value for the logger parameter simplifies instantiation
  2. The new setStatus overload provides a more convenient way to set status with optional error

Consider these improvements:

  1. Use a data class instead of an anonymous object for better readability
  2. Add KDoc comments to document the new method
+    /**
+     * Sets the status of the room with an optional error.
+     *
+     * @param status The new status of the room
+     * @param error An optional error that provides a reason for the status change
+     */
     fun setStatus(status: RoomLifecycle, error: ErrorInfo? = null) {
-        val newStatus = object : NewRoomStatus {
-            override val status: RoomLifecycle = status
-            override val error: ErrorInfo? = error
-        }
+        val newStatus = SimpleRoomStatus(status, error)
         this.setStatus(newStatus)
     }
+
+    private data class SimpleRoomStatus(
+        override val status: RoomLifecycle,
+        override val error: ErrorInfo?
+    ) : NewRoomStatus

Also applies to: 224-231

chat-android/src/main/java/com/ably/chat/Messages.kt (1)

242-243: Verify Room Lifecycle integration.

The featureName property is part of a broader Room Lifecycle implementation. The class correctly implements:

  • Feature identification via featureName
  • Error handling for attachment/detachment
  • Channel state management

Consider documenting the relationship between this feature and the Room Lifecycle, particularly:

  • When this feature is attached/detached during room lifecycle
  • How it contributes to the overall room state

Also applies to: 244-246

chat-android/src/main/java/com/ably/chat/RoomLifecycleManager.kt (5)

114-114: Consider extracting error messages as constants

Multiple locations use similar error messages with the same error code. Consider extracting these into constants to improve maintainability and consistency.

Example implementation:

private companion object {
    const val UNKNOWN_ERROR_MESSAGE = "unknown error in %s"
    const val FEATURE_ATTACH_ERROR_MESSAGE = "failed to attach %s feature%s"
    const val UNEXPECTED_STATE_ERROR_MESSAGE = "unexpected channel state in %s %s%s"
}

Also applies to: 244-244, 279-279, 376-378, 390-392


Line range hint 296-351: Consider adding debug logging for room state transitions

The attach implementation handles different states correctly, but adding debug logging would help with troubleshooting issues in production.

Example addition:

 internal suspend fun attach() {
+    _logger?.d("RoomLifecycleManager", "Starting room attachment. Current state: ${_status.current}")
     val deferredAttach = atomicCoroutineScope.async(LifecycleOperationPrecedence.AttachOrDetach.priority) {
         when (_status.current) {
             RoomLifecycle.Attached -> {
+                _logger?.d("RoomLifecycleManager", "Room already attached, skipping")
                 return@async
             }

Line range hint 364-414: Consider parallel feature attachment for better performance

Currently, features are attached sequentially. Consider implementing parallel attachment while maintaining proper error handling for better performance with multiple features.

Example implementation:

private suspend fun doAttach(): RoomAttachmentResult = coroutineScope {
    val attachResult = DefaultRoomAttachmentResult()
    
    try {
        _contributors.map { feature ->
            async {
                feature.channel.attachCoroutine()
                _firstAttachesCompleted[feature] = true
            }
        }.awaitAll()
        
        // Success handling
        this@RoomLifecycleManager._status.setStatus(attachResult)
        this@RoomLifecycleManager._operationInProgress = false
        return@coroutineScope attachResult
    } catch (ex: Throwable) {
        // Error handling similar to current implementation
        // Need to identify which feature failed
        val failedFeature = _contributors.find { it.channel.state != ChannelState.attached }
        // ... rest of the error handling
    }
}

Line range hint 418-484: Consider breaking down complex wind-down logic

The channel wind-down implementation has high cognitive complexity. Consider extracting the channel detachment logic into a separate method for better readability and maintainability.

Example refactor:

private suspend fun detachChannel(contributor: ResolvedContributor, except: ResolvedContributor?): Result<Unit> = runCatching {
    if (shouldSkipDetachment(contributor, except)) {
        return@runCatching
    }
    
    contributor.channel.detachCoroutine()
}

private fun shouldSkipDetachment(contributor: ResolvedContributor, except: ResolvedContributor?): Boolean {
    return (contributor === except && _status.current !== RoomLifecycle.Failed) ||
        (isTerminalState() && contributor.channel.state === ChannelState.failed)
}

private fun isTerminalState(): Boolean {
    return _status.current === RoomLifecycle.Failed ||
        _status.current === RoomLifecycle.Releasing ||
        _status.current === RoomLifecycle.Released
}

487-507: TODO comments need implementation

The detach and release methods are currently stubbed with TODOs. These are critical parts of the room lifecycle that need implementation.

Would you like me to help with implementing these methods or create GitHub issues to track their implementation? I can provide a detailed implementation following the same patterns used in the attach operation.

chat-android/src/test/java/com/ably/chat/RoomLifecycleManagerTest.kt (6)

54-56: Remove unnecessary runBlocking within runTest blocks

Inside runTest, which is already a coroutine test scope, you can directly call suspend functions without wrapping them in runBlocking.

Apply this diff to simplify the code:

-val exception = Assert.assertThrows(AblyException::class.java) {
-    runBlocking {
-        roomLifecycle.attach()
-    }
-}
+val exception = Assert.assertThrows(AblyException::class.java) {
+    roomLifecycle.attach()
+}

Also applies to: 70-72


223-228: Correct the comment to match the condition in the test

In the condition if ("reactions" in channel.name), the comment refers to the "typing contributor," which is inconsistent.

Apply this diff to fix the comment:

 if ("reactions" in channel.name) {
-    // Throw error for typing contributor, likely to throw because it uses different channel
+    // Simulate failure by throwing error for reactions contributor
     channel.setState(ChannelState.suspended)
     throw AblyException.fromErrorInfo(ErrorInfo("error attaching channel ${channel.name}", 500))
 }

142-146: Ensure that static mocks are cleared after tests to prevent side effects

Using mockkStatic without unmocking can cause the static mocks to persist across tests, potentially leading to unexpected behavior.

Consider unmocking static methods in an @After method:

+@After
+fun tearDown() {
+    unmockkStatic(io.ably.lib.realtime.Channel::attachCoroutine)
+}

This will ensure that the static mocks are cleared after each test.

Also applies to: 171-173, 221-223, 284-285, 322-323


152-155: Avoid unnecessary duplicate calls to roomLifecycle.attach() in tests

In these tests, roomLifecycle.attach() is called twice, which may not be necessary and could lead to unintended side effects.

Consider removing the initial call to roomLifecycle.attach() if it's not needed:

-roomLifecycle.attach()
 val result = kotlin.runCatching { roomLifecycle.attach() }
 Assert.assertTrue(result.isSuccess)

Ensure that the test logic remains correct after this change.

Also applies to: 193-195


216-218: Avoid suppressing warnings for maximum line length

The @Suppress("MaximumLineLength") annotation is used to bypass the line length warning. It's better to adhere to coding standards for readability.

Consider formatting the test function name to fit within the line length limit:

-@Suppress("MaximumLineLength")
 @Test
-fun `(CHA-RL1h1, CHA-RL1h2) If a one of the contributors fails to attach (enters suspended state), attach throws related error and room enters suspended state`() = runTest {
+fun `Attach throws error and room enters suspended state when a contributor fails to attach`() = runTest {

This improves readability and maintains code quality.


80-81: Initialize status with an explicit initial state

Currently, the initial state of status.current might not be explicitly set, leading to potential ambiguity.

Consider initializing status with a defined initial state:

val status = spyk<DefaultStatus>().apply {
    setStatus(RoomLifecycle.Initializing)
}
Assert.assertEquals(RoomLifecycle.Initializing, status.current)

This makes the initial state explicit and improves test clarity.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between 290fb84 and 774cc3b.

📒 Files selected for processing (12)
  • chat-android/src/main/java/com/ably/chat/Messages.kt (1 hunks)
  • chat-android/src/main/java/com/ably/chat/Occupancy.kt (1 hunks)
  • chat-android/src/main/java/com/ably/chat/Presence.kt (1 hunks)
  • chat-android/src/main/java/com/ably/chat/RoomLifecycleManager.kt (13 hunks)
  • chat-android/src/main/java/com/ably/chat/RoomReactions.kt (1 hunks)
  • chat-android/src/main/java/com/ably/chat/RoomStatus.kt (1 hunks)
  • chat-android/src/main/java/com/ably/chat/Typing.kt (1 hunks)
  • chat-android/src/main/java/com/ably/chat/Utils.kt (1 hunks)
  • chat-android/src/test/java/com/ably/chat/RoomLifecycleManagerTest.kt (1 hunks)
  • chat-android/src/test/java/com/ably/chat/TestUtils.kt (1 hunks)
  • chat-android/src/test/java/com/ably/utils/RoomLifecycleManagerTestHelpers.kt (1 hunks)
  • gradle/libs.versions.toml (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • gradle/libs.versions.toml
🔇 Additional comments (13)
chat-android/src/test/java/com/ably/utils/RoomLifecycleManagerTestHelpers.kt (2)

1-20: LGTM! Well-organized imports and package structure.

The imports are properly organized and the Channel alias helps avoid naming conflicts.


28-38: 🛠️ Refactor suggestion

Improve function flexibility and documentation.

The function creates mocked room features but has some areas for improvement:

  1. Hard-coded client1 value should be parameterized
  2. Using recordPrivateCalls = true makes tests sensitive to implementation details
  3. Missing documentation about the purpose and usage of these mocks
+/**
+ * Creates a list of mocked room feature contributors for testing.
+ * Each contributor is partially mocked using spyk to allow verification of method calls.
+ *
+ * @param roomId The room identifier (defaults to "1234")
+ * @param clientId The client identifier (defaults to "client1")
+ * @return List of resolved contributors including messages, presence, occupancy, typing, and reactions
+ */
-fun createRoomFeatureMocks(roomId: String = "1234"): List<ResolvedContributor> {
+fun createRoomFeatureMocks(
+    roomId: String = "1234",
+    clientId: String = "client1"
+): List<ResolvedContributor> {
     val realtimeClient = spyk(AblyRealtime(ClientOptions("id:key").apply { autoConnect = false }))
     val chatApi = mockk<ChatApi>(relaxed = true)

     val messagesContributor = spyk(DefaultMessages(roomId, realtimeClient.channels, chatApi), recordPrivateCalls = true)
     val presenceContributor = spyk(DefaultPresence(messagesContributor), recordPrivateCalls = true)
     val occupancyContributor = spyk(DefaultOccupancy(messagesContributor), recordPrivateCalls = true)
     val typingContributor = spyk(DefaultTyping(roomId, realtimeClient), recordPrivateCalls = true)
-    val reactionsContributor = spyk(DefaultRoomReactions(roomId, "client1", realtimeClient.channels), recordPrivateCalls = true)
+    val reactionsContributor = spyk(DefaultRoomReactions(roomId, clientId, realtimeClient.channels), recordPrivateCalls = true)
     return listOf(messagesContributor, presenceContributor, occupancyContributor, typingContributor, reactionsContributor)
 }

Let's verify if these mocked contributors are used consistently across tests:

chat-android/src/main/java/com/ably/chat/Occupancy.kt (1)

66-67: LGTM! The featureName property is correctly implemented.

The addition of the featureName property with value "occupancy" aligns with the error reporting requirements for the Room ATTACH operation.

Let's verify the consistency of featureName across other classes:

✅ Verification successful

The featureName property is correctly implemented and consistent across all implementations

Based on the verification results, all classes implementing ContributesToRoomLifecycleImpl have unique and appropriate featureName values:

  • Messages: "messages"
  • Occupancy: "occupancy"
  • Presence: "presence"
  • Reactions: "reactions"
  • Typing: "typing"

Each implementation follows the same pattern and the values are used consistently for error reporting during room attachment, as seen in the doAttach() method of the room lifecycle management.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Verify featureName implementation across related classes
# Expected: All default implementations should have unique featureName values

# Search for featureName declarations in Default* classes
rg 'override val featureName: String = ".*"' --type kotlin

Length of output: 268


Script:

#!/bin/bash
# Let's verify if there are any other classes implementing ContributesToRoomLifecycleImpl
# to ensure all have consistent featureName implementations

# First, find the interface definition
ast-grep --pattern 'interface ContributesToRoomLifecycleImpl {
  $$$
}'

# Then find all implementations
ast-grep --pattern 'class $_ : $$$ContributesToRoomLifecycleImpl$$$'

Length of output: 144


Script:

#!/bin/bash
# Let's try a different approach to find implementations
# Search for the interface and its implementations using ripgrep

# Search for the interface definition
rg "interface ContributesToRoomLifecycleImpl" --type kotlin -A 5

# Search for classes implementing it
rg "class.*:.*ContributesToRoomLifecycleImpl" --type kotlin

Length of output: 128


Script:

#!/bin/bash
# Let's try one more approach to find all related implementations
# Search for any class that has featureName property and check their inheritance

rg "class.*Default.*:.*" --type kotlin -B 1 -A 5

# Also search for any interface that might define featureName
rg "interface.*{" --type kotlin -A 5

Length of output: 1493


Script:

#!/bin/bash
# Let's search for all files that might contain featureName implementations
# and check their content

# First, find all Kotlin files that might be relevant
fd -e kt -x cat {} \; | rg -A 10 -B 10 'featureName'

# Also search specifically for Presence implementation
rg "class.*Presence" --type kotlin -A 5

Length of output: 9406

chat-android/src/test/java/com/ably/chat/TestUtils.kt (1)

67-79: Consider alternative testing approaches before relying on reflection.

While these utilities can help test private state, they might indicate that the Room lifecycle implementation could benefit from a more testable design:

  1. Consider making certain fields protected or internal for testing
  2. Extract smaller, more testable components
  3. Use test-specific interfaces or test doubles instead of reflection

This would make tests more maintainable and less brittle.

Let's check if there are similar reflection patterns in other test files:

✅ Verification successful

Reflection usage is justified in this specific testing context

The reflection utilities are being used specifically for testing the RoomLifecycleManager's internal state and behavior, where:

  1. The reflection is limited to accessing internal state for verification purposes
  2. The usage is contained within test helpers and not scattered throughout the test suite
  3. The main codebase shows proper encapsulation with internal/default visibility modifiers

The reflection approach is appropriate here as it allows testing state transitions without compromising the production code's encapsulation. Alternative approaches like making fields protected would actually weaken the production code's design.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Search for reflection usage in tests
rg -t kotlin "getDeclaredField|isAccessible" "src/test"

Length of output: 104


Script:

#!/bin/bash
# Search for reflection usage in tests with correct path
rg -t kotlin "getDeclaredField|isAccessible" "chat-android/src/test"

# Also search for similar reflection patterns
rg -t kotlin "setAccessible|privateField" "chat-android/src/test"

# Look for test files that might be using these utilities
fd -t f -e kt . "chat-android/src/test" --exec grep -l "setPrivateField\|getPrivateField"

Length of output: 1054


Script:

#!/bin/bash
# Let's check how these utilities are actually being used in the test files
rg -A 5 "setPrivateField|getPrivateField" "chat-android/src/test/java/com/ably/chat/RoomLifecycleManagerTest.kt"
rg -A 5 "setPrivateField|getPrivateField" "chat-android/src/test/java/com/ably/utils/RoomLifecycleManagerTestHelpers.kt"

# Also check if there are any public/internal alternatives in the main source code
rg -t kotlin "class.*Room" "chat-android/src/main"

Length of output: 2304

chat-android/src/main/java/com/ably/chat/Typing.kt (1)

85-86: LGTM! The featureName property is correctly implemented.

The addition of featureName aligns with the broader changes across other components and provides consistent feature identification for error reporting during room attachment operations.

chat-android/src/main/java/com/ably/chat/Utils.kt (1)

53-58: Verify error handling integration with room attachment.

The new errorMessage property works with the existing coroutine functions, but let's verify its integration with room attachment error handling.

✅ Verification successful

Let me gather more context about the error handling integration by checking the RoomLifecycleManager implementation.


Error handling integration with room attachment is properly implemented

The errorMessage property is correctly integrated into the room attachment error handling system:

  • Used in RoomLifecycleManager to enrich error messages during feature attachment failures
  • Properly concatenated with feature-specific error information for detailed error reporting
  • Applied in both feature attachment failures and unexpected channel state scenarios
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Check how the errorMessage property is used in room attachment error handling

# Test 1: Look for direct usage of errorMessage in room lifecycle related files
echo "Checking direct usage of errorMessage in room lifecycle..."
rg "errorMessage" --type kotlin

# Test 2: Look for room attachment error handling patterns
echo "Checking room attachment error handling patterns..."
ast-grep --pattern 'catch ($_) {
  $$$
  reason$$$
  $$$
}'

Length of output: 784


Script:

#!/bin/bash
# Check the full context of error handling in RoomLifecycleManager
echo "Checking RoomLifecycleManager implementation..."
rg -A 5 "errorMessage" chat-android/src/main/java/com/ably/chat/RoomLifecycleManager.kt

# Look for any error handling in feature attachment
echo "Checking feature attachment error patterns..."
rg -B 3 -A 3 "failed to attach" chat-android/src/main/java/com/ably/chat/RoomLifecycleManager.kt

Length of output: 1773

chat-android/src/main/java/com/ably/chat/Presence.kt (2)

137-138: LGTM! The featureName property is consistent with other feature implementations.

The addition of the featureName property aligns with the standardization of feature identification across the chat module and supports improved error reporting in the room lifecycle management.


Line range hint 147-185: Verify implementation of presence methods for room attachment testing.

The presence methods are currently marked as TODO("Not yet implemented"). Since this PR focuses on implementing Room ATTACH operation tests, these unimplemented methods might affect the test coverage and functionality of room attachment operations, particularly when presence-related events occur during attachment.

Let's verify the test requirements for presence during room attachment:

Please clarify:

  1. Are presence methods required for the room attachment tests?
  2. Should these methods be implemented as part of this PR?
  3. If not part of this PR, is there a separate ticket tracking their implementation?
chat-android/src/main/java/com/ably/chat/RoomReactions.kt (1)

115-115: LGTM! The featureName property is well-integrated.

The addition of featureName = "reactions" follows a consistent naming pattern across features and properly integrates with the room lifecycle management system.

Let's verify the consistency of feature naming across other components:

✅ Verification successful

Feature naming pattern is consistent across all components

The verification confirms that the featureName property follows a consistent pattern across all components:

  • "typing" in Typing.kt
  • "presence" in Presence.kt
  • "reactions" in RoomReactions.kt

Each feature uses a clear, descriptive name that matches its functionality.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Verify feature naming consistency across components
# Expected: Each component should have a similar featureName property with a descriptive value

# Test: Search for featureName declarations
rg -A 1 'override\s+val\s+featureName\s*=' --type kotlin

Length of output: 516

chat-android/src/main/java/com/ably/chat/Messages.kt (2)

242-246: Ensure test coverage for attachment scenarios.

Given that this PR focuses on Room ATTACH operation tests, ensure test coverage for the following scenarios:

  1. Feature attachment during room lifecycle
  2. Error handling for MessagesAttachmentFailed
  3. Channel state transitions during attachment

Let's check for existing tests:

#!/bin/bash
# Description: Check for existing test coverage of Messages attachment

# Look for test files related to Messages
fd -e kt "Messages.*Test|Test.*Messages"

# Search for attachment-related test cases
rg -A 5 "test.*attach" --type kotlin

242-243: LGTM! Verify interface implementation.

The addition of the featureName property is consistent with the class's purpose and follows the pattern described in the AI summary.

Let's verify the interface requirement:

✅ Verification successful

Let me gather more information to verify the interface requirement.


Let me try a different approach to find the interface definition.


Property implementation verified and consistent across features

The featureName property is correctly implemented as it:

  • Fulfills the interface requirement from ContributesToRoomLifecycle
  • Is consistently implemented across all feature classes:
    • Messages: "messages"
    • Typing: "typing"
    • Reactions: "reactions"
    • Presence: "presence"
    • Occupancy: "occupancy"
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Verify that featureName is required by the interface
# and consistently implemented across other feature classes

# Check for the interface definition requiring featureName
rg -A 2 "interface ContributesToRoomLifecycle"

# Check for consistent implementation across other feature classes
rg "override val featureName.*=.*\".*\""

Length of output: 1453


Script:

#!/bin/bash
# Search for the interface definition and its members
ast-grep --pattern 'interface ContributesToRoomLifecycle {
  $$$
}'

Length of output: 69


Script:

#!/bin/bash
# Search for the interface definition using ripgrep
rg -A 10 "interface ContributesToRoomLifecycle"

# Also search for any files containing this interface name
fd -e kt | xargs grep -l "ContributesToRoomLifecycle"

Length of output: 1521

chat-android/src/main/java/com/ably/chat/RoomLifecycleManager.kt (2)

21-24: LGTM: Improved error reporting with feature names

The addition of the featureName property enhances error reporting by providing clear identification of features in error messages.


Line range hint 292-507: Verify test coverage for all specified scenarios

The implementation handles all the specified scenarios from ECO-5009, but we should ensure test coverage for:

  1. Room already in ATTACHED status
  2. Room in RELEASING status
  3. Room in RELEASED status
  4. Concurrent attachment operations
  5. Status transitions (ATTACHING → ATTACHED)
  6. Error handling for failed attachments
✅ Verification successful

Test coverage is comprehensive for all specified scenarios

After reviewing the test file, I can confirm that all the scenarios mentioned in the review comment are thoroughly covered:

  1. Room already in ATTACHED status: (CHA-RL1a) Attach success when channel in already in attached state
  2. Room in RELEASING status: (CHA-RL1b) Attach throws exception when channel in releasing state
  3. Room in RELEASED status: (CHA-RL1c) Attach throws exception when channel in released state
  4. Concurrent attachment operations: (CHA-RL1d) Attach op should wait for existing operation as per (CHA-RL7)
  5. Status transitions: (CHA-RL1e) Attach op should transition room into ATTACHING state
  6. Error handling for failed attachments: Multiple test cases covering various failure scenarios (CHA-RL1h1 through CHA-RL1h6)

The implementation is well-tested with both positive and negative test cases, including edge cases and error conditions.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Check for existing test files
echo "Checking for existing test files..."
fd -e kt -e java "Room.*Test|.*RoomTest" 

# Look for test cases covering specific scenarios
echo "Checking for test coverage of specific scenarios..."
rg -l "test.*AlreadyAttached|test.*Releasing|test.*Released|test.*Concurrent|test.*StatusTransition|test.*FailedAttachment"

Length of output: 570


Script:

#!/bin/bash
# Let's examine the content of RoomLifecycleManagerTest.kt to check for test coverage
echo "Checking RoomLifecycleManagerTest.kt content..."
cat "chat-android/src/test/java/com/ably/chat/RoomLifecycleManagerTest.kt"

Length of output: 18724

Comment on lines +68 to +72
fun Any.setPrivateField(name: String, value: Any?) {
val valueField = javaClass.getDeclaredField(name)
valueField.isAccessible = true
return valueField.set(this, value)
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add error handling and documentation for reflection utility.

While using reflection in tests is sometimes necessary, the current implementation could be more robust:

  1. Add error handling for potential exceptions (SecurityException, NoSuchFieldException)
  2. Document the intended usage and potential risks
  3. Consider adding a warning about using this sparingly to avoid masking design issues

Consider this implementation:

+/**
+ * Sets a private field value using reflection.
+ * NOTE: Use sparingly as excessive use might indicate design issues.
+ * @throws IllegalArgumentException if the field doesn't exist or is inaccessible
+ */
 fun Any.setPrivateField(name: String, value: Any?) {
-    val valueField = javaClass.getDeclaredField(name)
-    valueField.isAccessible = true
-    return valueField.set(this, value)
+    try {
+        val valueField = javaClass.getDeclaredField(name)
+        valueField.isAccessible = true
+        valueField.set(this, value)
+    } catch (e: Exception) {
+        throw IllegalArgumentException("Failed to set field $name", e)
+    }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
fun Any.setPrivateField(name: String, value: Any?) {
val valueField = javaClass.getDeclaredField(name)
valueField.isAccessible = true
return valueField.set(this, value)
}
/**
* Sets a private field value using reflection.
* NOTE: Use sparingly as excessive use might indicate design issues.
* @throws IllegalArgumentException if the field doesn't exist or is inaccessible
*/
fun Any.setPrivateField(name: String, value: Any?) {
try {
val valueField = javaClass.getDeclaredField(name)
valueField.isAccessible = true
valueField.set(this, value)
} catch (e: Exception) {
throw IllegalArgumentException("Failed to set field $name", e)
}
}

Comment on lines +74 to +79
fun <T>Any.getPrivateField(name: String): T {
val valueField = javaClass.getDeclaredField(name)
valueField.isAccessible = true
@Suppress("UNCHECKED_CAST")
return valueField.get(this) as T
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Improve type safety and error handling in reflection getter.

The current implementation has potential runtime safety issues:

  1. The unchecked cast could fail at runtime
  2. Missing error handling for reflection exceptions
  3. No documentation about type parameter requirements

Consider this safer implementation:

+/**
+ * Gets a private field value using reflection.
+ * @param T Expected type of the field
+ * @throws IllegalArgumentException if the field doesn't exist, is inaccessible, or of wrong type
+ */
 fun <T> Any.getPrivateField(name: String): T {
-    val valueField = javaClass.getDeclaredField(name)
-    valueField.isAccessible = true
-    @Suppress("UNCHECKED_CAST")
-    return valueField.get(this) as T
+    try {
+        val valueField = javaClass.getDeclaredField(name)
+        valueField.isAccessible = true
+        val value = valueField.get(this)
+        try {
+            @Suppress("UNCHECKED_CAST")
+            return value as T
+        } catch (e: ClassCastException) {
+            throw IllegalArgumentException("Field $name is not of type ${T::class.java.simpleName}", e)
+        }
+    } catch (e: Exception) {
+        throw IllegalArgumentException("Failed to get field $name", e)
+    }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
fun <T>Any.getPrivateField(name: String): T {
val valueField = javaClass.getDeclaredField(name)
valueField.isAccessible = true
@Suppress("UNCHECKED_CAST")
return valueField.get(this) as T
}
/**
* Gets a private field value using reflection.
* @param T Expected type of the field
* @throws IllegalArgumentException if the field doesn't exist, is inaccessible, or of wrong type
*/
fun <T>Any.getPrivateField(name: String): T {
try {
val valueField = javaClass.getDeclaredField(name)
valueField.isAccessible = true
val value = valueField.get(this)
try {
@Suppress("UNCHECKED_CAST")
return value as T
} catch (e: ClassCastException) {
throw IllegalArgumentException("Field $name is not of type ${T::class.java.simpleName}", e)
}
} catch (e: Exception) {
throw IllegalArgumentException("Failed to get field $name", e)
}
}

Comment on lines +129 to +130
roomStatusChanges.add(it)
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Handle potential concurrency issues with roomStatusChanges list

Accessing roomStatusChanges from multiple coroutines could lead to concurrency issues.

Synchronize access to the list or use a thread-safe collection:

+private val roomStatusChanges = Collections.synchronizedList(mutableListOf<RoomStatusChange>())

This ensures thread safety when multiple coroutines modify the list.

@sacOO7 sacOO7 force-pushed the tests/roomlifecycle-attach branch 2 times, most recently from 8c96226 to 774cc3b Compare November 11, 2024 17:12
replaced magical error codes with reference to ErrorCode enum constant
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: 2

🧹 Outside diff range and nitpick comments (2)
chat-android/src/test/java/com/ably/chat/RoomLifecycleManagerTest.kt (2)

219-282: Consider parameterizing similar test cases

The failure scenario tests (CHA-RL1h1, CHA-RL1h2) have similar structure with different conditions. This leads to code duplication.

Consider using a parameterized test:

@ParameterizedTest
@MethodSource("failureScenarios")
fun `test channel attachment failures`(
    channelName: String,
    expectedState: RoomLifecycle,
    expectedError: ErrorInfo
) = runTest {
    // Common test logic here
}

companion object {
    @JvmStatic
    fun failureScenarios() = Stream.of(
        Arguments.of("reactions", RoomLifecycle.Suspended, ErrorInfo(...)),
        Arguments.of("typing", RoomLifecycle.Failed, ErrorInfo(...))
    )
}

384-392: Improve test readability with custom assertions

The detach retry logic test uses a counter variable and mutable state, making it harder to understand the test's intention.

Consider extracting a custom assertion:

private fun verifyDetachRetries(
    channel: Channel,
    times: Int,
    detachedChannels: MutableList<Channel>
) = coEvery { channel.detachCoroutine() } coAnswers {
    delay(200)
    if (--times >= 0) {
        error("failed to detach channel")
    }
    detachedChannels.add(channel)
}
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between 452669c and f4883bc.

📒 Files selected for processing (1)
  • chat-android/src/test/java/com/ably/chat/RoomLifecycleManagerTest.kt (1 hunks)
🔇 Additional comments (1)
chat-android/src/test/java/com/ably/chat/RoomLifecycleManagerTest.kt (1)

131-134: Thread safety issue already identified

A previous review has already identified the thread safety concern with roomStatusChanges. The suggestion to use Collections.synchronizedList is still valid.

Comment on lines +32 to +34
private val roomScope = CoroutineScope(
Dispatchers.Default.limitedParallelism(1) + CoroutineName("roomId"),
)
Copy link

Choose a reason for hiding this comment

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

💡 Codebase verification

Add Test Lifecycle Management Across Test Files

Multiple test files define roomScope without explicit cleanup methods, which could lead to test flakiness. Consider implementing @Before and @After methods to manage coroutine scopes consistently.

🔗 Analysis chain

Consider adding test lifecycle management

While the roomScope correctly uses limited parallelism, there's no explicit cleanup of coroutines between tests. This could lead to test flakiness if coroutines from previous tests are still active.

Consider adding a @Before setup and @After cleanup:

private lateinit var roomScope: CoroutineScope

@Before
fun setup() {
    roomScope = CoroutineScope(
        Dispatchers.Default.limitedParallelism(1) + CoroutineName("roomId")
    )
}

@After
fun cleanup() {
    roomScope.cancel()
}
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Check for similar test lifecycle management patterns in other test files
rg -A 5 "roomScope.*=.*CoroutineScope" --type kotlin

Length of output: 1447

Comment on lines +82 to +126
fun `(CHA-RL1d) Attach op should wait for existing operation as per (CHA-RL7)`() = runTest {
val status = spyk<DefaultStatus>()
Assert.assertEquals(RoomLifecycle.Initializing, status.current)

val roomLifecycle = spyk(RoomLifecycleManager(roomScope, status, createRoomFeatureMocks()))

val roomReleased = Channel<Boolean>()
coEvery {
roomLifecycle.release()
} coAnswers {
roomLifecycle.atomicCoroutineScope().async {
status.setStatus(RoomLifecycle.Releasing)
roomReleased.receive()
status.setStatus(RoomLifecycle.Released)
}
}

// Release op started from separate coroutine
launch { roomLifecycle.release() }
assertWaiter { !roomLifecycle.atomicCoroutineScope().finishedProcessing }
Assert.assertEquals(0, roomLifecycle.atomicCoroutineScope().pendingJobCount) // no queued jobs, one job running
assertWaiter { status.current == RoomLifecycle.Releasing }

// Attach op started from separate coroutine
val roomAttachOpDeferred = async(SupervisorJob()) { roomLifecycle.attach() }
assertWaiter { roomLifecycle.atomicCoroutineScope().pendingJobCount == 1 } // attach op queued
Assert.assertEquals(RoomLifecycle.Releasing, status.current)

// Finish release op, so ATTACH op can start
roomReleased.send(true)
assertWaiter { status.current == RoomLifecycle.Released }

val result = kotlin.runCatching { roomAttachOpDeferred.await() }
Assert.assertTrue(roomLifecycle.atomicCoroutineScope().finishedProcessing)

Assert.assertTrue(result.isFailure)
val exception = result.exceptionOrNull() as AblyException

Assert.assertEquals("unable to attach room; room is released", exception.errorInfo.message)
Assert.assertEquals(ErrorCodes.RoomIsReleased.errorCode, exception.errorInfo.code)
Assert.assertEquals(HttpStatusCodes.InternalServerError, exception.errorInfo.statusCode)
assertWaiter { roomLifecycle.atomicCoroutineScope().finishedProcessing }

coVerify { roomLifecycle.release() }
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Improve test reliability by using TestCoroutineScheduler

The test relies on assertWaiter and channels for synchronization. This approach can be flaky and harder to maintain.

Consider using TestCoroutineScheduler for deterministic testing:

-fun `(CHA-RL1d) Attach op should wait for existing operation as per (CHA-RL7)`() = runTest {
+fun `(CHA-RL1d) Attach op should wait for existing operation as per (CHA-RL7)`() = runTest(UnconfinedTestDispatcher()) {
     // ... existing setup ...
-    launch { roomLifecycle.release() }
-    assertWaiter { !roomLifecycle.atomicCoroutineScope().finishedProcessing }
+    val releaseJob = launch { roomLifecycle.release() }
+    advanceUntilIdle()
+    assert(!roomLifecycle.atomicCoroutineScope().finishedProcessing)
     // ... rest of the test ...

Committable suggestion skipped: line range outside the PR's diff.

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.

2 participants