-
Notifications
You must be signed in to change notification settings - Fork 2
Update replicator tests for new API #27
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
base: RN-v1
Are you sure you want to change the base?
Conversation
Summary of ChangesHello @jayant-dhingra-cb, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request significantly expands the end-to-end test coverage for the Replicator API by introducing a new test suite specifically designed for the Highlights
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here. You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension. Footnotes
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Code Review
This pull request introduces a comprehensive test suite for the new replicator API, which is a great addition. The tests cover a wide range of scenarios, from basic configuration to complex conflict and filtering cases. My review focuses on improving the robustness of a test helper function, increasing type safety, and removing some dead code. Overall, this is a solid set of tests.
| let listenerToken: string; | ||
| const completionPromise = new Promise<void>((resolve, reject) => { | ||
| replicator | ||
| .addChangeListener((change) => { | ||
| const status = change.status; | ||
| const activityLevel = status.getActivityLevel(); | ||
|
|
||
| if ( | ||
| config.getContinuous() && | ||
| activityLevel == ReplicatorActivityLevel.IDLE | ||
| ) { | ||
| if ( | ||
| status.getProgress().getCompleted() == | ||
| status.getProgress().getTotal() | ||
| ) | ||
| replicator.stop(); | ||
| } | ||
|
|
||
| if (activityLevel === ReplicatorActivityLevel.STOPPED) { | ||
| const error = status.getError(); | ||
| if (error) { | ||
| reject(new Error(`Replication ${JSON.stringify(error)}`)); | ||
| } else { | ||
| resolve(); | ||
| } | ||
| } | ||
| }) | ||
| .then((token) => { | ||
| listenerToken = token; | ||
| }); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There are a couple of issues here that could lead to hanging tests or crashes. First, listenerToken is not initialized and could be undefined if addChangeListener fails. It should be typed as string | undefined for safety. Second, if the promise from addChangeListener rejects, it's an unhandled rejection, and completionPromise will hang, causing the test to time out. You should add a .catch(reject) to the promise chain to propagate the error.
let listenerToken: string | undefined;
const completionPromise = new Promise<void>((resolve, reject) => {
replicator
.addChangeListener((change) => {
const status = change.status;
const activityLevel = status.getActivityLevel();
if (
config.getContinuous() &&
activityLevel == ReplicatorActivityLevel.IDLE
) {
if (
status.getProgress().getCompleted() ==
status.getProgress().getTotal()
)
replicator.stop();
}
if (activityLevel === ReplicatorActivityLevel.STOPPED) {
const error = status.getError();
if (error) {
reject(new Error(`Replication ${JSON.stringify(error)}`));
} else {
resolve();
}
}
})
.then((token) => {
listenerToken = token;
})
.catch(reject);
});There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't know enough about Promises to know if either this would be an actual issue only inside the test or if this is standard practice and we should accommodate such use ?better?. In theory we don't expect users to remove a listener right after they create it as the expectation is that they are looking to receive at least an update before they would dispose it and so the above is redundant.
| await replicator.removeChangeListener(listenerToken); | ||
| replicator.stop(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since listenerToken can be undefined (as it's set asynchronously and the operation might fail), you should add a guard to prevent calling removeChangeListener with an invalid token. This will prevent a crash in the finally block.
if (listenerToken) {
await replicator.removeChangeListener(listenerToken);
}
replicator.stop();| if (!(otherDb instanceof Database)) { | ||
| return { | ||
| testName: 'testDocumentReplicationEventWithPullConflict', | ||
| success: false, | ||
| message: "otherDb isn't a database instance", | ||
| data: undefined, | ||
| }; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This if (!(otherDb instanceof Database)) check is unreachable. The new Database(...) constructor on line 669 will either return a Database instance or throw an error. In the success case, otherDb is guaranteed to be an instance of Database, so this condition will never be true. This block can be safely removed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm curious about this check? as pointed out by the bot, I expect that new Database... call above to throw at a low level iOS/LiteCore in case the database cannot be opened. Including creating a new one if it doesn't already exists in "open". Is that not the case for RN?
We should never end up into this if basically.
| const replicatedDocuments: any = []; | ||
|
|
||
| const token = await replicator.addDocumentChangeListener((change: any) => { | ||
| change.documents.forEach((document: any) => | ||
| replicatedDocuments.push(document) | ||
| ); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using any for replicatedDocuments, change, and document reduces type safety. You should use the specific types from cblite-js like ReplicatedDocument and DocumentReplication. You'll need to import these types to use them in the suggestion.
const replicatedDocuments: ReplicatedDocument[] = [];
const token = await replicator.addDocumentChangeListener((change: DocumentReplication) => {
change.documents.forEach((document) =>
replicatedDocuments.push(document)
);
});| if (!this.otherDatabase) { | ||
| const databaseResult = await this.getDatabase( | ||
| this.otherDatabaseName, | ||
| this.otherDatabaseName, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
extra space
| } catch (e) { | ||
| console.error(e); | ||
| } finally { | ||
| await replicator.removeChangeListener(listenerToken); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ideally we would want to have listenerToken.remove() here as discussed in zoom.
| } catch (e) { | ||
| console.error(e); | ||
| } finally { | ||
| await replicator.removeChangeListener(listenerToken); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ideally we would want to have listenerToken.remove() here as discussed in zoom.
| await this.sleep(500); | ||
|
|
||
| // Clean up | ||
| await replicator.removeChangeListener(token); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
same here
| await this.sleep(500); | ||
|
|
||
| // Clean up | ||
| await replicator.removeChangeListener(token); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
same here
|
|
||
| await replicator.start(false); | ||
| await replicatorCompletionPromise; | ||
| await replicator.removeChangeListener(listenerToken); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
same here etc.
| let listenerToken: string; | ||
| const completionPromise = new Promise<void>((resolve, reject) => { | ||
| replicator | ||
| .addChangeListener((change) => { | ||
| const status = change.status; | ||
| const activityLevel = status.getActivityLevel(); | ||
|
|
||
| if ( | ||
| config.getContinuous() && | ||
| activityLevel == ReplicatorActivityLevel.IDLE | ||
| ) { | ||
| if ( | ||
| status.getProgress().getCompleted() == | ||
| status.getProgress().getTotal() | ||
| ) | ||
| replicator.stop(); | ||
| } | ||
|
|
||
| if (activityLevel === ReplicatorActivityLevel.STOPPED) { | ||
| const error = status.getError(); | ||
| if (error) { | ||
| reject(new Error(`Replication ${JSON.stringify(error)}`)); | ||
| } else { | ||
| resolve(); | ||
| } | ||
| } | ||
| }) | ||
| .then((token) => { | ||
| listenerToken = token; | ||
| }); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't know enough about Promises to know if either this would be an actual issue only inside the test or if this is standard practice and we should accommodate such use ?better?. In theory we don't expect users to remove a listener right after they create it as the expectation is that they are looking to receive at least an update before they would dispose it and so the above is redundant.
| if (!(otherDb instanceof Database)) { | ||
| return { | ||
| testName: 'testDocumentReplicationEventWithPullConflict', | ||
| success: false, | ||
| message: "otherDb isn't a database instance", | ||
| data: undefined, | ||
| }; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm curious about this check? as pointed out by the bot, I expect that new Database... call above to throw at a low level iOS/LiteCore in case the database cannot be opened. Including creating a new one if it doesn't already exists in "open". Is that not the case for RN?
We should never end up into this if basically.
|
If time permits, we should list/inventory this tests somewhere in a readme or similar. See https://github.com/couchbaselabs/couchbase-lite-api/tree/master/spec/tests for examples |
No description provided.