Skip to content

Commit 1037e8a

Browse files
feat: Detox simulator test runner (#590)
Co-authored-by: stevensJourney <[email protected]>
1 parent 438bc8a commit 1037e8a

File tree

71 files changed

+8088
-702
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

71 files changed

+8088
-702
lines changed

.changeset/khaki-bottles-sniff.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@powersync/op-sqlite': patch
3+
---
4+
5+
Rejecting pending read/write operations when the database is closed.

.changeset/light-oranges-compete.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@powersync/op-sqlite': minor
3+
---
4+
5+
`close()` is now async, which allows clients to use it with `await`.
Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
# Ensures certain packages work on simulators
2+
name: Test Simulators/Emulators
3+
on:
4+
pull_request: # triggered for any PR updates (including new pushes to PR branch)
5+
6+
jobs:
7+
check-changes:
8+
name: Check for relevant changes
9+
runs-on: ubuntu-latest
10+
outputs:
11+
should_run: ${{ steps.check.outputs.should_run }}
12+
steps:
13+
- uses: actions/checkout@v4
14+
with:
15+
fetch-depth: 0
16+
- name: Check for changes
17+
id: check
18+
run: |
19+
git fetch origin ${{ github.base_ref }}
20+
if git diff --quiet origin/${{ github.base_ref }} -- packages/common packages/powersync-op-sqlite; then
21+
echo "should_run=false" >> $GITHUB_OUTPUT
22+
else
23+
echo "should_run=true" >> $GITHUB_OUTPUT
24+
fi
25+
26+
test-android:
27+
name: Test Android
28+
needs: check-changes
29+
if: ${{ needs.check-changes.outputs.should_run == 'true' }}
30+
runs-on: ubuntu-xl
31+
env:
32+
AVD_NAME: ubuntu-avd-x86_64-31
33+
steps:
34+
- uses: actions/checkout@v4
35+
with:
36+
persist-credentials: false
37+
38+
- name: Enable KVM group perms
39+
run: |
40+
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
41+
sudo udevadm control --reload-rules
42+
sudo udevadm trigger --name-match=kvm
43+
44+
- name: Setup Gradle
45+
uses: gradle/actions/setup-gradle@v4
46+
47+
- name: AVD Cache
48+
uses: actions/cache@v3
49+
id: avd-cache
50+
with:
51+
path: |
52+
~/.android/avd/*
53+
~/.android/adb*
54+
key: avd-31
55+
56+
- name: Set up JDK 17
57+
uses: actions/setup-java@v3
58+
with:
59+
java-version: 17
60+
distribution: 'adopt'
61+
cache: 'gradle'
62+
63+
- name: Setup NodeJS
64+
uses: actions/setup-node@v4
65+
with:
66+
node-version-file: '.nvmrc'
67+
68+
- uses: pnpm/action-setup@v2
69+
name: Install pnpm
70+
with:
71+
version: 9
72+
run_install: false
73+
74+
- name: Get pnpm store directory
75+
shell: bash
76+
run: |
77+
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
78+
79+
- uses: actions/cache@v3
80+
name: Setup pnpm cache
81+
with:
82+
path: ${{ env.STORE_PATH }}
83+
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('pnpm-lock.yaml') }}
84+
restore-keys: |
85+
${{ runner.os }}-pnpm-store-
86+
87+
- name: Install dependencies
88+
run: pnpm install
89+
90+
- name: Build
91+
run: pnpm build:packages
92+
93+
- name: Setup Detox build framework cache
94+
working-directory: ./tools/powersynctests
95+
run: |
96+
pnpx detox clean-framework-cache && pnpx detox build-framework-cache
97+
98+
- name: Initialize Android Folder
99+
run: mkdir -p ~/.android/avd
100+
101+
- name: create AVD and generate snapshot for caching
102+
if: steps.avd-cache.outputs.cache-hit != 'true'
103+
uses: reactivecircus/[email protected]
104+
with:
105+
api-level: 31
106+
force-avd-creation: false
107+
target: google_apis
108+
arch: x86_64
109+
disable-animations: false
110+
avd-name: $AVD_NAME
111+
emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
112+
script: echo "Generated AVD snapshot for caching."
113+
114+
- name: Android Emulator Build
115+
working-directory: ./tools/powersynctests
116+
run: pnpx detox build --configuration android.emu.release
117+
118+
- name: Run connected Android tests
119+
uses: ReactiveCircus/[email protected]
120+
with:
121+
api-level: 31
122+
target: google_apis
123+
arch: x86_64
124+
avd-name: $AVD_NAME
125+
script: cd tools/powersynctests && pnpx detox test --configuration android.emu.release --headless
126+
force-avd-creation: false
127+
emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
128+
disable-animations: true
129+
130+
test-ios:
131+
name: Test iOS
132+
needs: check-changes
133+
if: ${{ needs.check-changes.outputs.should_run == 'true' }}
134+
runs-on: macOS-15
135+
136+
steps:
137+
- uses: actions/checkout@v4
138+
with:
139+
persist-credentials: false
140+
141+
- name: CocoaPods Cache
142+
uses: actions/cache@v3
143+
id: cocoapods-cache
144+
with:
145+
path: |
146+
tools/powersynctests/ios/Pods/*
147+
key: ${{ runner.os }}-${{ hashFiles('tools/powersynctests/ios/Podfile.lock') }}
148+
149+
- name: Cache Xcode Derived Data
150+
uses: actions/cache@v3
151+
with:
152+
path: |
153+
tools/powersynctests/ios/build/*
154+
key: xcode-derived-${{ runner.os }}-${{ hashFiles('tools/powersynctests/ios/Podfile.lock') }}
155+
restore-keys: |
156+
xcode-derived-${{ runner.os }}-
157+
158+
- name: Setup NodeJS
159+
uses: actions/setup-node@v4
160+
with:
161+
node-version-file: '.nvmrc'
162+
163+
- uses: pnpm/action-setup@v2
164+
name: Install pnpm
165+
with:
166+
version: 9
167+
run_install: false
168+
169+
- name: Get pnpm store directory
170+
shell: bash
171+
run: |
172+
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
173+
174+
- uses: actions/cache@v3
175+
name: Setup pnpm cache
176+
with:
177+
path: ${{ env.STORE_PATH }}
178+
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('pnpm-lock.yaml') }}
179+
restore-keys: |
180+
${{ runner.os }}-pnpm-store-
181+
182+
- name: Install dependencies
183+
run: pnpm install
184+
185+
- name: Build
186+
run: pnpm build:packages
187+
188+
- name: Install Detox dependencies
189+
run: |
190+
brew tap wix/brew
191+
brew install applesimutils
192+
npm install -g detox-cli
193+
detox clean-framework-cache && detox build-framework-cache
194+
195+
- name: Install CocoaPods dependencies
196+
working-directory: tools/powersynctests/ios
197+
run: pod install
198+
199+
- name: iOS Simulator Build
200+
working-directory: ./tools/powersynctests
201+
run: pnpx detox build --configuration ios.sim.release
202+
203+
- name: iOS Simulator Test
204+
working-directory: ./tools/powersynctests
205+
run: pnpx detox test --configuration ios.sim.release --cleanup

packages/powersync-op-sqlite/src/db/OPSqliteAdapter.ts

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
import { BaseObserver, DBAdapter, DBAdapterListener, DBLockOptions, QueryResult, Transaction } from '@powersync/common';
21
import { ANDROID_DATABASE_PATH, getDylibPath, IOS_LIBRARY_PATH, open, type DB } from '@op-engineering/op-sqlite';
2+
import { BaseObserver, DBAdapter, DBAdapterListener, DBLockOptions, QueryResult, Transaction } from '@powersync/common';
33
import Lock from 'async-lock';
4-
import { OPSQLiteConnection } from './OPSQLiteConnection';
54
import { Platform } from 'react-native';
5+
import { OPSQLiteConnection } from './OPSQLiteConnection';
66
import { SqliteOptions } from './SqliteOptions';
77

88
/**
@@ -32,6 +32,7 @@ export class OPSQLiteDBAdapter extends BaseObserver<DBAdapterListener> implement
3232
protected writeConnection: OPSQLiteConnection | null;
3333

3434
private readQueue: Array<() => void> = [];
35+
private abortController: AbortController;
3536

3637
constructor(protected options: OPSQLiteAdapterOptions) {
3738
super();
@@ -40,6 +41,7 @@ export class OPSQLiteDBAdapter extends BaseObserver<DBAdapterListener> implement
4041
this.locks = new Lock();
4142
this.readConnections = null;
4243
this.writeConnection = null;
44+
this.abortController = new AbortController();
4345
this.initialized = this.init();
4446
}
4547

@@ -153,11 +155,14 @@ export class OPSQLiteDBAdapter extends BaseObserver<DBAdapterListener> implement
153155
}
154156
}
155157

156-
close() {
157-
this.initialized.then(() => {
158-
this.writeConnection!.close();
159-
this.readConnections!.forEach((c) => c.connection.close());
160-
});
158+
async close() {
159+
await this.initialized;
160+
// Abort any pending operations
161+
this.abortController.abort();
162+
this.readQueue = [];
163+
164+
this.writeConnection!.close();
165+
this.readConnections!.forEach((c) => c.connection.close());
161166
}
162167

163168
async readLock<T>(fn: (tx: OPSQLiteConnection) => Promise<T>, options?: DBLockOptions): Promise<T> {
@@ -203,17 +208,30 @@ export class OPSQLiteDBAdapter extends BaseObserver<DBAdapterListener> implement
203208

204209
return new Promise(async (resolve, reject) => {
205210
try {
211+
// Set up abort signal listener
212+
const abortListener = () => {
213+
reject(new Error('Database connection was closed'));
214+
};
215+
this.abortController.signal.addEventListener('abort', abortListener);
216+
206217
await this.locks
207218
.acquire(
208219
LockType.WRITE,
209220
async () => {
221+
// Check if operation was aborted before executing
222+
if (this.abortController.signal.aborted) {
223+
reject(new Error('Database connection was closed'));
224+
}
210225
resolve(await fn(this.writeConnection!));
211226
},
212227
{ timeout: options?.timeoutMs }
213228
)
214229
.then(() => {
215230
// flush updates once a write lock has been released
216231
this.writeConnection!.flushUpdates();
232+
})
233+
.finally(() => {
234+
this.abortController.signal.removeEventListener('abort', abortListener);
217235
});
218236
} catch (ex) {
219237
reject(ex);

0 commit comments

Comments
 (0)