Skip to content

Static Mocking removal from ClevertapAPI #830

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 7 commits into
base: feat/testing/SDK-4608
Choose a base branch
from

Conversation

CTLalit
Copy link
Collaborator

@CTLalit CTLalit commented Jun 17, 2025

https://wizrocket.atlassian.net/browse/SDK-4921

remove static mocking for CTExecutors
CoreState.java → convert to kotlin for configurability
CtExecutors → pass as a dependency so we can use MockCtExecutors in test
Configurable clock → Rather than current time in millis which makes it hard to test

Summary by CodeRabbit

  • Refactor
    • Improved internal structure for core state management and asynchronous task execution, leading to more maintainable and testable code.
    • Centralized time handling through a new clock abstraction.
  • Bug Fixes
    • Corrected property references in test setups for improved reliability.
  • Tests
    • Simplified and modernized test initialization and mocking, removing unnecessary complexity.
    • Removed obsolete and redundant test cases.

CTLalit added 6 commits June 11, 2025 18:04
- rather than nullable props with getters/setters
- ready for conversion to access prop syntax kotlin
- fixes test mock core state
…ests

- fixes legacy tests which required mocking for controllermanager
- does not reassign mock again
- adds final properties in the class
…tate to kotlin

- also does not allow access to integrator since it should not be allowed
- makes changes to convert class to kotlin and make it internal
- adds CTExectors and getCoreState passed as a dependency in the constructor
- uses internal constructor for test class so that we can pass mocked deps
- fixes CleverTapAPITest.kt class to make tests leaner and avoid static mocking
- cleans up CoreState.kt class to apply general kotlin idiomatic syntax. All of it is safe change suggested by IDE for access prop syntax and null un-wrapping suggestions, accidentally had defined val as var so fixes that as well
- Encapsulate time-related operations within a `Clock` interface and its `SYSTEM` implementation.
- Replace direct calls to `System.currentTimeMillis()` and `Utils.getNow()` with `Clock` methods.
- Update `CleverTapAPI` to accept a `Clock` instance for improved testability.
- Modify tests to utilize a `TestClock` for controlled time scenarios in unit tests.
- Remove `Utils.getNow()` as it's now handled by `Clock`.
Copy link
Contributor

coderabbitai bot commented Jun 17, 2025

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

🗂️ Base branches to auto review (1)
  • \b(master|main|develop)(_\w*)?

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Walkthrough

The changes refactor core SDK initialization and execution logic by introducing a Kotlin-based CoreState class, centralizing all component dependencies. Task execution is routed through instance-specific executors. Time handling is abstracted via a Clock interface, replacing direct system calls. Related tests and utility methods are updated to align with these architectural changes.

Changes

Files / Grouped Paths Change Summary
CleverTapAPI.java, CleverTapFactory.kt Refactored to use instance-specific executors from CoreState, introduced Clock abstraction for time, updated constructors and initialization logic.
CoreState.java Deleted Java version of CoreState class.
CoreState.kt Introduced new Kotlin-based CoreState class holding all SDK dependencies; added deprecated product config methods.
Utils.java Removed getNow() method for time retrieval.
utils/Clock.kt Enhanced Clock.SYSTEM with explicit overrides and @JvmField annotation.
AnalyticsManagerTest.kt, CleverTapFactoryTest.kt, EventQueueManagerTest.kt, MockCoreState.kt Updated property references to match new CoreState implementation; refactored mock setup and property names.
CleverTapAPITest.kt Refactored test setup to use direct instantiation and dependency injection; removed static mocking; simplified test helpers.
UtilsTest.kt Removed test for the deleted Utils.getNow() method.

Sequence Diagram(s)

sequenceDiagram
    participant App as Application
    participant Factory as CleverTapFactory
    participant Core as CoreState
    participant API as CleverTapAPI
    participant Clock as Clock

    App->>Factory: getCoreState(config)
    Factory->>Core: Construct CoreState with dependencies
    Factory-->>App: Return CoreState instance

    App->>API: Initialize CleverTapAPI(context, config, coreState, clock)
    API->>Core: Retrieve executors, managers, etc.
    API->>Clock: Get current time (for session, etc.)
    API->>API: asyncStartup() for async initialization
    API-->>App: CleverTapAPI instance ready
Loading

Suggested labels

Reviewed chained PRs

Suggested reviewers

  • piyush-kukadiya
  • Anush-Shand

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.
    • Explain this complex logic.
    • 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 explain this code block.
    • @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 explain its main purpose.
    • @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.

Support

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

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 generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this 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.

@francispereira
Copy link

francispereira commented Jun 17, 2025

🎉 Snyk checks have passed. No issues have been found so far.

security/snyk check is complete. No issues have been found. (View Details)

license/snyk check is complete. No issues have been found. (View Details)

code/snyk check is complete. No issues have been found. (View Details)

@CTLalit
Copy link
Collaborator Author

CTLalit commented Jun 17, 2025

@coderabbitai review

Copy link
Contributor

coderabbitai bot commented Jun 17, 2025

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Contributor

@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: 6

🔭 Outside diff range comments (2)
clevertap-core/src/main/java/com/clevertap/android/sdk/CleverTapFactory.kt (2)

128-146: Async encryption-migration may clash with first DB reads

migrateEncryption() is kicked off via executors.postAsyncSafelyTask() but getCoreState() returns immediately.
Any component that starts reading encrypted prefs/DB rows through cryptHandler or databaseManager before this migration finishes can hit inconsistent state (e.g., mixed keys, partially-migrated rows).

At minimum:

  1. Return a Future/Job from the task and await it in places that need consistent data, or
  2. Run the migration synchronously during first creation (it’s a one-time cost), or
  3. Gate downstream reads behind a latch/flag set by the task.

Failing to do so risks hard-to-reproduce data-corruption bugs.


196-220: Race condition & non-thread-safe mutation of StoreRegistry

storeRegistry.inAppStore and impressionStore are mutated from a background IO task after the mutable StoreRegistry instance has been published to the rest of the SDK (inside CoreState).
No synchronization/volatile barriers are present, so other threads may observe a half-initialised registry, leading to NullPointerExceptions when reading those fields.

Consider:

- val storeRegistry = StoreRegistry(inAppStore = null, …)
- executors.ioTask<Unit>().execute { storeRegistry.inAppStore = … }
+ // build fully initialised storeRegistry on background thread *before* publication
+ val storeRegistry = executors.ioTask<StoreRegistry>().submit {
+     StoreRegistry(
+         inAppStore = buildInAppStore(),
+         impressionStore = buildImpressionStore(),
+
+     )
+ }.get()      // or publish only after completion

or make the fields inside StoreRegistry immutable and provide fully constructed instances up-front.

♻️ Duplicate comments (1)
clevertap-core/src/main/java/com/clevertap/android/sdk/CleverTapAPI.java (1)

2109-2123: getTimeElapsed() still depends on static clock

Once the clock is made instance-scoped, remember to replace clevertapClock here as well.

🧹 Nitpick comments (12)
clevertap-core/src/main/java/com/clevertap/android/sdk/utils/Clock.kt (1)

50-56: Remove redundant overrides – rely on the interface defaults

currentTimeSeconds() and currentTimeSecondsInt() already have perfectly-good default implementations in the Clock interface.
Overriding them here just duplicates code and adds a maintenance surface without any behavioural gain.

-      override fun currentTimeSeconds(): Long {
-        return TimeUnit.MILLISECONDS.toSeconds(currentTimeMillis())
-      }
-
-      override fun currentTimeSecondsInt(): Int {
-        return (currentTimeMillis() / 1000).toInt()
-      }

Dropping the two overrides keeps SYSTEM lean and still exposes the exact same behaviour.

clevertap-core/src/test/java/com/clevertap/android/sdk/CleverTapFactoryTest.kt (1)

43-46: Consider harmonising the camel-casing of cTLockManager

The new property uses a capital “T” in the middle (cTLockManager) while most SDK fields follow lower-camel (ctLockManager).
Purely cosmetic, but worth aligning for searchability and consistency.

clevertap-core/src/test/java/com/clevertap/android/sdk/AnalyticsManagerTest.kt (1)

90-92: Same naming-style nit as above

cTLockManager breaks the usual lower-camel convention. If possible, rename to ctLockManager across prod & test code for uniformity.

clevertap-core/src/test/java/com/clevertap/android/sdk/EventQueueManagerTest.kt (1)

65-66: Camel-case inconsistency for cTLockManager

See earlier comments – consider renaming to ctLockManager to follow the prevailing style.

clevertap-core/src/test/java/com/clevertap/android/sdk/MockCoreState.kt (1)

7-38: Constructor call is getting unwieldy – consider named arguments or a builder

Passing 30-plus positional parameters makes the test double fragile to future signature tweaks and hard to scan.
Using named arguments (Kotlin) or a small builder/helper would greatly improve readability:

MockCoreStateKotlin(
    config = cleverTapInstanceConfig,
    coreMetaData = CoreMetaData(),
    deviceInfo = mockk(relaxed = true),
    //
)

Not urgent, but worth refactoring before the parameter list grows again.

clevertap-core/src/main/java/com/clevertap/android/sdk/CleverTapFactory.kt (1)

259-262: CTVariables initialisation fire-and-forget

ctVariables.init() may throw (reflection, IO) but the lambda result is ignored; failures silently disappear.
Recommend adding try/catch with explicit logging or returning a Result/Future to surface errors for troubleshooting.

clevertap-core/src/main/java/com/clevertap/android/sdk/CoreState.kt (2)

25-57: Make CoreState non-open unless subclassing is required

The class only serves as a dependency container; allowing inheritance widens the surface for improper extension and breaks immutability assumptions.
Mark it final (default in Kotlin) unless there is a concrete need for testing/mocking.


64-68: Deprecated accessor still performs work

getCtProductConfigController() delegates to initProductConfig() which may allocate heavy objects and mutate shared state even though the API is deprecated.
Either:

• Remove side-effects and simply return the cached controller, or
• Hard-deprecate by throwing UnsupportedOperationException to prevent new usage.

This helps phase-out the API cleanly.

clevertap-core/src/main/java/com/clevertap/android/sdk/CleverTapAPI.java (1)

1239-1280: asyncStartup() – missing exception handling inside tasks

If any of the lambdas throws, the task will silently swallow the exception (it’s executed inside an executor). Consider wrapping each callable with try/catch and logging, or use a helper that does this consistently so startup failures are visible.

 task.execute("CleverTapAPI#initializeDeviceInfo", () -> {
+    try {
         ...
-        return null;
+    } catch (Throwable t) {
+        getConfigLogger().error(getAccountId(), "Startup task failed", t);
+    }
+    return null;
 });
clevertap-core/src/test/java/com/clevertap/android/sdk/CleverTapAPITest.kt (3)

62-76: Use descriptive constants instead of magic numbers for clock manipulation

testClock.setCurrentTime(Int.MAX_VALUE.toLong()) relies on the reader knowing the time unit and the 5-second threshold logic in production code. Replacing the literal with a named constant (e.g., MORE_THAN_FIVE_SECONDS) or using TimeUnit.SECONDS.toMillis(10) would improve intent clarity and future-proof the assertion.


375-376: Property-access verification is brittle

verify(exactly = 2) { corestate.controllerManager.ctInboxController } counts the number of property reads. Any future refactor that introduces an extra read (e.g., logging) will break the test without changing behaviour.

Prefer verifying the interaction on the real collaborator:

verify { inboxController.deleteInboxMessagesForIDs(messageIDs) }

and drop the property-access count, or wrap the property in a local variable inside production code and verify downstream calls instead.


354-361: confirmVerified immediately after verify offers little value

The pattern

verify { corestate.controllerManager.ctInboxController }
confirmVerified(corestate.controllerManager)

adds minimal assertion power because only a single interaction is expected. It can be removed or replaced with a more explicit interaction assertion (e.g., verify(exactly = 1)), reducing noise and improving readability.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 36b1ea5 and 9155eaa.

📒 Files selected for processing (12)
  • clevertap-core/src/main/java/com/clevertap/android/sdk/CleverTapAPI.java (22 hunks)
  • clevertap-core/src/main/java/com/clevertap/android/sdk/CleverTapFactory.kt (8 hunks)
  • clevertap-core/src/main/java/com/clevertap/android/sdk/CoreState.java (0 hunks)
  • clevertap-core/src/main/java/com/clevertap/android/sdk/CoreState.kt (1 hunks)
  • clevertap-core/src/main/java/com/clevertap/android/sdk/Utils.java (0 hunks)
  • clevertap-core/src/main/java/com/clevertap/android/sdk/utils/Clock.kt (1 hunks)
  • clevertap-core/src/test/java/com/clevertap/android/sdk/AnalyticsManagerTest.kt (1 hunks)
  • clevertap-core/src/test/java/com/clevertap/android/sdk/CleverTapAPITest.kt (9 hunks)
  • clevertap-core/src/test/java/com/clevertap/android/sdk/CleverTapFactoryTest.kt (1 hunks)
  • clevertap-core/src/test/java/com/clevertap/android/sdk/EventQueueManagerTest.kt (1 hunks)
  • clevertap-core/src/test/java/com/clevertap/android/sdk/MockCoreState.kt (1 hunks)
  • clevertap-core/src/test/java/com/clevertap/android/sdk/UtilsTest.kt (0 hunks)
💤 Files with no reviewable changes (3)
  • clevertap-core/src/test/java/com/clevertap/android/sdk/UtilsTest.kt
  • clevertap-core/src/main/java/com/clevertap/android/sdk/CoreState.java
  • clevertap-core/src/main/java/com/clevertap/android/sdk/Utils.java
🧰 Additional context used
🧠 Learnings (3)
📓 Common learnings
Learnt from: CTLalit
PR: CleverTap/clevertap-android-sdk#798
File: clevertap-core/src/main/java/com/clevertap/android/sdk/network/NetworkManager.kt:152-152
Timestamp: 2025-05-30T08:21:16.962Z
Learning: CTLalit prefers to avoid modifying legacy code when working on specific features, keeping the scope focused to reduce risk and maintain clear boundaries for changes.
clevertap-core/src/main/java/com/clevertap/android/sdk/CleverTapFactory.kt (1)
Learnt from: vasct
PR: CleverTap/clevertap-android-sdk#797
File: clevertap-core/src/main/java/com/clevertap/android/sdk/inbox/CTInboxActivity.java:89-92
Timestamp: 2025-04-28T09:52:18.094Z
Learning: In the CleverTap Android SDK, if a CleverTapAPI instance is not null, then its getCoreState() method will never return null because CoreState is initialized in the constructor and set via setCoreState(). The CleverTapFactory.getCoreState() method throws a RuntimeException if it can't create a valid state.
clevertap-core/src/main/java/com/clevertap/android/sdk/CoreState.kt (1)
Learnt from: vasct
PR: CleverTap/clevertap-android-sdk#797
File: clevertap-core/src/main/java/com/clevertap/android/sdk/inbox/CTInboxActivity.java:89-92
Timestamp: 2025-04-28T09:52:18.094Z
Learning: In the CleverTap Android SDK, if a CleverTapAPI instance is not null, then its getCoreState() method will never return null because CoreState is initialized in the constructor and set via setCoreState(). The CleverTapFactory.getCoreState() method throws a RuntimeException if it can't create a valid state.
🧬 Code Graph Analysis (1)
clevertap-core/src/main/java/com/clevertap/android/sdk/CleverTapFactory.kt (1)
clevertap-core/src/main/java/com/clevertap/android/sdk/variables/Parser.java (1)
  • Parser (25-195)
🔇 Additional comments (4)
clevertap-core/src/test/java/com/clevertap/android/sdk/EventQueueManagerTest.kt (1)

63-63: Good change – explicit cast removed

Passing corestate.networkManager directly keeps the constructor call clean and avoids an unnecessary cast.

clevertap-core/src/main/java/com/clevertap/android/sdk/CleverTapFactory.kt (1)

460-491: CoreState advertised as immutable but contains mutable sub-objects

Although the new CoreState is built with val properties, several references (storeRegistry, controllerManager, etc.) are mutated later by background tasks.
Document this clearly or enforce immutability by:

  1. Initialising these collaborators completely before construction, or
  2. Wrapping mutable fields in thread-safe holders.

Without this, the “immutable container” guarantee is misleading to maintainers.

clevertap-core/src/main/java/com/clevertap/android/sdk/CoreState.kt (1)

77-98: Possible NPE when context is null

initProductConfig(null) passes a nullable context down to CTProductConfigFactory.getInstance(...).
If that factory does not explicitly handle null, an NPE will surface.
Guard early:

if (context == null) return

or enforce a non-null contract.

clevertap-core/src/main/java/com/clevertap/android/sdk/CleverTapAPI.java (1)

1009-1014: First-foreground timestamp can race

clevertapClock.currentTimeSecondsInt() is read without synchronisation while other threads may set CoreMetaData.setInitialAppEnteredForegroundTime(...).
If you move to an instance-scoped clock, also guard this block or migrate the field to AtomicInteger to avoid lost updates in multi-threaded apps.

Comment on lines +37 to +38
MockCTExecutors()
)
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

MockCTExecutors() is missing the required config argument – compilation will fail

All other usages instantiate the helper with the CleverTapInstanceConfig parameter:

MockCTExecutors(cleverTapInstanceConfig)

Apply the same here to satisfy the constructor.

-    MockCTExecutors()
+    MockCTExecutors(cleverTapInstanceConfig)
🤖 Prompt for AI Agents
In clevertap-core/src/test/java/com/clevertap/android/sdk/MockCoreState.kt at
lines 37 to 38, the instantiation of MockCTExecutors is missing the required
CleverTapInstanceConfig argument. Fix this by passing the
cleverTapInstanceConfig parameter to the MockCTExecutors constructor, matching
other usages to ensure successful compilation.

Comment on lines +223 to 240
val taskInitFCManager = executors.ioTask<Unit>()
taskInitFCManager.execute("initFCManager") {
if (coreState.deviceInfo != null && coreState.deviceInfo.deviceID != null && controllerManager.inAppFCManager == null) {
coreState.config.logger
val deviceId = deviceInfo.deviceID
if (deviceId != null && controllerManager.inAppFCManager == null) {
config.logger
.verbose(
config.accountId + ":async_deviceID",
"Initializing InAppFC with device Id = " + coreState.deviceInfo
.deviceID
"Initializing InAppFC with device Id = $deviceId"
)
controllerManager.inAppFCManager = InAppFCManager(
context,
config,
coreState.deviceInfo.deviceID,
deviceId,
storeRegistry,
impressionManager
)
}
null
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Async FC-manager initialisation can leave inAppFCManager null

initFCManager runs off-thread and populates controllerManager.inAppFCManager.
Callers that access this field immediately after obtaining CoreState can still find it null. Either:

• Make ControllerManager.inAppFCManager lazy & thread-safe, or
• Block until the task finishes, or
• Expose a CompletableFuture that callers can await.

This prevents sporadic NullPointerExceptions during early app-startup analytics.

🤖 Prompt for AI Agents
In clevertap-core/src/main/java/com/clevertap/android/sdk/CleverTapFactory.kt
around lines 223 to 240, the asynchronous initialization of inAppFCManager can
cause it to be null when accessed immediately after CoreState creation, leading
to NullPointerExceptions. To fix this, modify the initialization to either make
inAppFCManager lazy and thread-safe, or block the calling thread until the
initFCManager task completes, or alternatively expose a CompletableFuture that
callers can await to ensure initialization is complete before access.

Comment on lines +168 to +169
private static Clock clevertapClock = Clock.SYSTEM;

Copy link
Contributor

@coderabbitai coderabbitai bot Jun 17, 2025

Choose a reason for hiding this comment

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

⚠️ Potential issue

Static mutable clock breaks multi-instance safety

clevertapClock is static, yet it is mutated in every constructor.
With multiple SDK instances (or tests that inject a fake clock) the last-created instance overwrites the clock for all others, leading to time-skew bugs and data races.

- private static Clock clevertapClock = Clock.SYSTEM;
+ private final Clock clock;

and update all clevertapClock.* calls to this.clock.* (or expose it through coreState).
This keeps the dependency truly instance-scoped.

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

🤖 Prompt for AI Agents
In clevertap-core/src/main/java/com/clevertap/android/sdk/CleverTapAPI.java
around lines 168 to 169, the static mutable field clevertapClock is shared
across all instances, causing multi-instance safety issues. Change
clevertapClock from static to an instance field, initialize it per instance in
the constructor, and update all references from clevertapClock.* to this.clock.*
or access it via the instance's coreState to ensure the clock is instance-scoped
and avoid data races.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

its okay if it mutates; we always use system clock.

Copy link
Contributor

Choose a reason for hiding this comment

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

@CTLalit Understood! Since you always use the system clock across all instances, the shared static field won't cause practical issues in your use case. I'll note this design decision for future reference.


✏️ Learnings added
Learnt from: CTLalit
PR: CleverTap/clevertap-android-sdk#830
File: clevertap-core/src/main/java/com/clevertap/android/sdk/CleverTapAPI.java:168-169
Timestamp: 2025-06-17T13:54:08.118Z
Learning: In the CleverTap Android SDK, the static mutable clevertapClock field in CleverTapAPI.java is acceptable because all instances consistently use Clock.SYSTEM, so the mutation doesn't cause practical multi-instance issues in their use case.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: vasct
PR: CleverTap/clevertap-android-sdk#797
File: clevertap-core/src/main/java/com/clevertap/android/sdk/inbox/CTInboxActivity.java:89-92
Timestamp: 2025-04-28T09:52:18.094Z
Learning: In the CleverTap Android SDK, if a CleverTapAPI instance is not null, then its getCoreState() method will never return null because CoreState is initialized in the constructor and set via setCoreState(). The CleverTapFactory.getCoreState() method throws a RuntimeException if it can't create a valid state.

Learnt from: CTLalit
PR: CleverTap/clevertap-android-sdk#798
File: clevertap-core/src/main/java/com/clevertap/android/sdk/network/NetworkManager.kt:152-152
Timestamp: 2025-05-30T08:21:16.962Z
Learning: CTLalit prefers to avoid modifying legacy code when working on specific features, keeping the scope focused to reduce risk and maintain clear boundaries for changes.

Comment on lines 1214 to +1237
// Initialize
private CleverTapAPI(final Context context, final CleverTapInstanceConfig config, String cleverTapID) {
this.context = context;
private CleverTapAPI(
final Context context,
final CleverTapInstanceConfig config,
String cleverTapID
) {
this(context, config, CleverTapFactory.getCoreState(context, config, cleverTapID), Clock.SYSTEM);
}

CoreState coreState = CleverTapFactory.getCoreState(context, config, cleverTapID);
setCoreState(coreState);
// Internal constructor for testing
CleverTapAPI(
final Context context,
final CleverTapInstanceConfig config,
final CoreState coreState,
final Clock clock
) {
this.context = context;
this.coreState = coreState;
CleverTapAPI.clevertapClock = clock;
getConfigLogger().verbose(config.getAccountId() + ":async_deviceID", "CoreState is set");
asyncStartup();
Logger.i("CleverTap SDK initialized with accountId: " + config.getAccountId() + " accountToken: " + config
.getAccountToken() + " accountRegion: " + config.getAccountRegion());
}
Copy link
Contributor

@coderabbitai coderabbitai bot Jun 17, 2025

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Constructor ignores injected clock

You pass a Clock but immediately stash it into the static field, losing the benefit of injection and re-introducing the global mutable state.

- CleverTapAPI.clevertapClock = clock;
+ this.clock = clock;          // instance field

Do not mutate global state here; retain the injected dependency per instance.

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

🤖 Prompt for AI Agents
In clevertap-core/src/main/java/com/clevertap/android/sdk/CleverTapAPI.java
between lines 1214 and 1237, the constructor assigns the injected Clock
parameter to a static field, causing global mutable state and ignoring the
benefit of dependency injection. To fix this, remove the assignment to the
static field and instead store the Clock instance in a non-static instance
variable so each CleverTapAPI object retains its own injected Clock without
affecting global state.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

done for testing.

Copy link
Contributor

Choose a reason for hiding this comment

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

@CTLalit Thanks for addressing the dependency injection issue! Storing the Clock as an instance variable instead of assigning it to the static field is the right approach for proper testability and avoiding global mutable state.


🧠 Learnings used
Learnt from: CTLalit
PR: CleverTap/clevertap-android-sdk#798
File: clevertap-core/src/main/java/com/clevertap/android/sdk/network/NetworkManager.kt:152-152
Timestamp: 2025-05-30T08:21:16.962Z
Learning: CTLalit prefers to avoid modifying legacy code when working on specific features, keeping the scope focused to reduce risk and maintain clear boundaries for changes.

If you found this review helpful, would you consider giving us a shout-out on X?

Thank you for using CodeRabbit!

Comment on lines 78 to 93
@Test
fun testCleverTapAPI_constructor_when_InitialAppEnteredForegroundTime_greater_than_5_secs() {
executeBasicTest {
mockkStatic(Utils::class) {
// Arrange
every { Utils.getNow() } returns Int.MAX_VALUE
CoreMetaData.setInitialAppEnteredForegroundTime(0)

// Act
initializeCleverTapAPI()

// Assert
assertTrue(
"isCreatedPostAppLaunch must be true",
cleverTapInstanceConfig.isCreatedPostAppLaunch
)
verifyCommonConstructorBehavior()
}
}
@Ignore("This might be actual bug which happens due to long to int conversion, current ts does not overflow and cause this test to fail in real device")
fun testCleverTapAPI_constructor_when_InitialAppEnteredForegroundTime_greater_than_5_secs_long_int_conversion() {
testClock.setCurrentTime(Long.MAX_VALUE)
CoreMetaData.setInitialAppEnteredForegroundTime(0)

// Act
initializeCleverTapAPI()

// Assert
assertTrue(
"isCreatedPostAppLaunch must be true",
cleverTapInstanceConfig.isCreatedPostAppLaunch
)
verifyCommonConstructorBehavior()
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Ignored test hides a production bug – prefer investigation or Assume over @Ignore

Marking testCleverTapAPI_constructor_when_…_long_int_conversion with @Ignore permanently suppresses coverage of a potential overflow defect noted in the comment. This masks regressions.

If the overflow only affects certain ABIs/SDKs, gate the test with an Assume.assumeTrue(...) check or parameterised test instead of ignoring it outright, and file/track a ticket referencing the root cause.

🤖 Prompt for AI Agents
In clevertap-core/src/test/java/com/clevertap/android/sdk/CleverTapAPITest.kt
around lines 78 to 93, the test is currently ignored with @Ignore which hides a
potential overflow bug. Remove the @Ignore annotation and instead use an
Assume.assumeTrue(...) check to conditionally skip the test only on affected
ABIs or SDK versions. This way, the test runs in relevant environments and the
bug remains visible. Also, create and link a ticket to track the root cause of
the overflow issue.

Comment on lines 34 to 39
private fun initializeCleverTapAPI() {
cleverTapAPI = CleverTapAPI(application, cleverTapInstanceConfig, corestate, testClock)

private fun executeBasicTestWithAny(block: () -> Unit) {
executeMockExecutors {
executeMockFactoryWithAny {
block()
}
}
// we need to do this for static methods of CleverTapAPI tests to work correctly.
CleverTapAPI.setInstances(hashMapOf(Constant.ACC_ID to cleverTapAPI))
}
Copy link
Contributor

@coderabbitai coderabbitai bot Jun 17, 2025

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Static instance map manipulation may lead to subtle test coupling

initializeCleverTapAPI() pushes a new entry into the global CleverTapAPI.instances map on every invocation.
When a test method calls this helper more than once (or when future tests add calls without noticing), the previous entry remains unless explicitly cleared, causing shared state across tests and order-dependent failures.

Consider scoping the map population strictly to the JUnit lifecycle:

@@
-private fun initializeCleverTapAPI() {
-    cleverTapAPI = CleverTapAPI(application, cleverTapInstanceConfig, corestate, testClock)
-
-    // we need to do this for static methods of CleverTapAPI tests to work correctly.
-    CleverTapAPI.setInstances(hashMapOf(Constant.ACC_ID to cleverTapAPI))
-}
+private fun initializeCleverTapAPI() {
+    cleverTapAPI = CleverTapAPI(application, cleverTapInstanceConfig, corestate, testClock)
+    CleverTapAPI.setInstances(mutableMapOf(Constant.ACC_ID to cleverTapAPI))
+}
+
+@Before
+fun beforeEach() {
+    CleverTapAPI.setInstances(null)      // clear any leftovers
+    ...
+}
+
+@After
+fun afterEach() {
+    CleverTapAPI.setInstances(null)      // guarantee isolation
+}

This keeps tests isolated and avoids unintended leakage.

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

🤖 Prompt for AI Agents
In clevertap-core/src/test/java/com/clevertap/android/sdk/CleverTapAPITest.kt
around lines 34 to 39, the initializeCleverTapAPI method adds entries to the
global CleverTapAPI.instances map without clearing it, causing shared state
across tests. To fix this, clear or reset the instances map before or after each
test using JUnit lifecycle methods such as @Before or @After to ensure test
isolation and prevent order-dependent failures.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

fixed in commit d37a3c8

Copy link
Contributor

Choose a reason for hiding this comment

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

@CTLalit Thank you for addressing the test isolation concern! The fix ensures that tests won't interfere with each other through shared static state, which will improve test reliability and maintainability.


🧠 Learnings used
Learnt from: CTLalit
PR: CleverTap/clevertap-android-sdk#798
File: clevertap-core/src/main/java/com/clevertap/android/sdk/network/NetworkManager.kt:152-152
Timestamp: 2025-05-30T08:21:16.962Z
Learning: CTLalit prefers to avoid modifying legacy code when working on specific features, keeping the scope focused to reduce risk and maintain clear boundaries for changes.

If you found this review helpful, would you consider giving us a shout-out on X?

Thank you for using CodeRabbit!

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