Skip to content

Commit f50cb19

Browse files
committed
Test devtools extension
1 parent cde734e commit f50cb19

8 files changed

Lines changed: 173 additions & 7 deletions

File tree

packages/powersync/CHANGELOG.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
1-
## 2.1.0
1+
## 2.1.0 (unreleased)
2+
3+
- Add a DevTools extension to inspect running PowerSync databases in your app.
4+
5+
## 2.0.2
26

37
- Update PowerSync SQLite core extension to version 0.4.13.
48
- Fix changes in active Sync Stream subscriptions causing a reconnect delay.
5-
- Add a DevTools extension to inspect running PowerSync databases in your app.
69

710
## 2.0.1
811

packages/powersync/lib/src/devtools/devtools.dart

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ import '../database/powersync_database.dart';
99
import 'extension.dart';
1010

1111
// We want to avoid including this code for release-mode builds, since it's only
12-
// relevant for development tooling.
12+
// relevant for development tooling. This matches the definition of Flutter's
13+
// kReleaseMode: https://api.flutter.dev/flutter/foundation/kReleaseMode-constant.html
1314
const _releaseMode = bool.fromEnvironment('dart.vm.product');
1415
const enable = !_releaseMode;
1516

packages/powersync/pubspec.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ dev_dependencies:
4848
test_descriptor: ^2.0.2
4949
mockito: ^5.5.0
5050
yaml: ^3.1.3
51+
vm_service: ^15.2.0
5152

5253
platforms:
5354
android:
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import 'dart:io';
2+
3+
import 'package:powersync/powersync.dart';
4+
import 'package:path/path.dart' as p;
5+
6+
/// An app using a PowerSync database.
7+
///
8+
/// We use this to test the VM service extension making this database available
9+
/// through an IPC protocol.
10+
void main(List<String> args) async {
11+
String databasePath;
12+
if (args.isEmpty) {
13+
final dir = await Directory.systemTemp.createTemp('ps-dart-extension-test');
14+
databasePath = p.join(dir.path, 'test.db');
15+
} else {
16+
databasePath = args[0];
17+
}
18+
19+
const schema = Schema([
20+
Table('users', [Column.text('name')])
21+
]);
22+
final database = PowerSyncDatabase(schema: schema, path: databasePath);
23+
await database.initialize();
24+
print('database is running at $databasePath!');
25+
}
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import 'dart:async';
2+
import 'dart:convert';
3+
import 'dart:io';
4+
5+
import 'package:path/path.dart' as p;
6+
import 'package:powersync/src/version.dart';
7+
import 'package:test/test.dart';
8+
import 'package:vm_service/vm_service.dart';
9+
import 'package:vm_service/vm_service_io.dart';
10+
11+
void main() {
12+
late Process child;
13+
late VmService vm;
14+
late String isolateId;
15+
late Directory tmpDir;
16+
17+
setUpAll(() async {
18+
tmpDir = await Directory.systemTemp.createTemp('ps-dart-extension-test');
19+
20+
// Get a random unused port.
21+
final socket = await ServerSocket.bind(InternetAddress.loopbackIPv4, 0);
22+
final port = socket.port;
23+
await socket.close();
24+
25+
String sdk = p.dirname(p.dirname(Platform.resolvedExecutable));
26+
child = await Process.start(p.join(sdk, 'bin', 'dart'), [
27+
// Don't use dart run here to avoid https://github.com/dart-lang/native/issues/2921.
28+
// Build hooks would have run for the parent process anyway.
29+
//'run',
30+
'--enable-vm-service=$port',
31+
'--disable-service-auth-codes',
32+
'--enable-asserts',
33+
'test/devtools/app.dart',
34+
p.join(tmpDir.path, 'test.db'),
35+
]);
36+
37+
final vmServiceListening = Completer<void>();
38+
final databaseOpened = Completer<void>();
39+
40+
child.stdout.map(utf8.decode).transform(const LineSplitter()).listen((
41+
line,
42+
) {
43+
print('[child]: $line');
44+
45+
if (line.startsWith('The Dart VM service is listening')) {
46+
vmServiceListening.complete();
47+
} else if (line.contains('database is running')) {
48+
databaseOpened.complete();
49+
}
50+
});
51+
52+
await vmServiceListening.future;
53+
54+
vm = await vmServiceConnectUri('ws://localhost:$port/ws');
55+
await databaseOpened.future;
56+
57+
final state = await vm.getVM();
58+
isolateId = state.isolates!.firstWhere((i) => i.name == 'main').id!;
59+
});
60+
61+
tearDownAll(() async {
62+
child.kill();
63+
await child.exitCode;
64+
await tmpDir.delete(recursive: true);
65+
});
66+
67+
test('can get version', () async {
68+
final response = await vm.callServiceExtension('ext.powersync.version',
69+
isolateId: isolateId);
70+
71+
expect(response.json, {'version': libraryVersion});
72+
});
73+
74+
test('can run queries', () async {
75+
final response = await vm.callServiceExtension(
76+
'ext.powersync.database',
77+
args: {
78+
'command': 'select',
79+
'db': '0',
80+
'sql': 'SELECT ?',
81+
'params': '[123]',
82+
},
83+
isolateId: isolateId,
84+
);
85+
expect(response.json, {
86+
'ok': {
87+
'columnNames': ['?'],
88+
'rows': [
89+
[123]
90+
]
91+
}
92+
});
93+
});
94+
95+
test('can get schema', () async {
96+
final response = await vm.callServiceExtension(
97+
'ext.powersync.database',
98+
args: {'command': 'schema', 'db': '0'},
99+
isolateId: isolateId,
100+
);
101+
expect(response.json, {
102+
'ok': {
103+
'raw_tables': <Object?>[],
104+
'tables': [containsPair('name', 'users')]
105+
}
106+
});
107+
});
108+
109+
test('can get sync status', () async {
110+
final response = await vm.callServiceExtension(
111+
'ext.powersync.database',
112+
args: {'command': 'status-listen', 'db': '0'},
113+
isolateId: isolateId,
114+
);
115+
116+
expect(response.json,
117+
{'ok': containsPair('current', containsPair('connected', false))});
118+
});
119+
}

packages/powersync_devtools_extension/lib/state/databases.dart

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,24 @@ import 'remote_database.dart';
99
import 'service.dart';
1010

1111
final class DatabaseReference {
12+
/// The id of the database object generated by the `powersync` package.
1213
final int id;
14+
15+
/// A short name identifying the database.
16+
///
17+
/// This is usually the last component of [path].
1318
final String name;
19+
20+
/// The path of the database.
1421
final String path;
22+
23+
/// The last credentials a connector on this database has returned.
24+
///
25+
/// We use this to show credentials in the DevTools extension and to deeplink
26+
/// into the diagnostics app.
1527
final PowerSyncCredentials? lastCredentials;
1628

29+
/// The isolate on which the database object has been opened.
1730
final IsolateRef isolate;
1831

1932
DatabaseReference({
@@ -25,6 +38,7 @@ final class DatabaseReference {
2538
});
2639
}
2740

41+
/// Rebuilds whenever the app opens or closes a PowerSync database.
2842
final _databaseListChanged = StreamProvider.autoDispose<void>((ref) {
2943
return Stream.fromFuture(ref.watch(serviceProvider.future)).asyncExpand(
3044
(serviceProvider) => serviceProvider.onExtensionEvent.where((event) {
@@ -33,6 +47,7 @@ final _databaseListChanged = StreamProvider.autoDispose<void>((ref) {
3347
);
3448
});
3549

50+
/// An auto-updating list of currently opened PowerSync databases in the app.
3651
final databaseList = FutureProvider.autoDispose<List<DatabaseReference>>((
3752
ref,
3853
) async {
@@ -84,6 +99,8 @@ final selectedDatabase =
8499
controller.state = null;
85100
} else if (controller.state == null &&
86101
databases.every((e) => e.id != controller.state?.ref.id)) {
102+
// Auto-select the first available database once one becomes
103+
// available.
87104
controller.state = RemoteDatabase(databases.first, service);
88105
}
89106
}, fireImmediately: true);

packages/powersync_devtools_extension/lib/ui/overview.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,7 @@ final class _SyncIssues extends ConsumerWidget {
221221
} else if (waitingForCheckpoint.value == true) {
222222
trackIssue(
223223
Text(
224-
'Waiting for a write checkpoint containing previous uploads. If this status persist, new data would not be synced.',
224+
'Waiting for a write checkpoint containing previous uploads. If this status persists, new data would not be synced.',
225225
),
226226
);
227227
}
@@ -233,7 +233,7 @@ final class _SyncIssues extends ConsumerWidget {
233233
crossAxisAlignment: .stretch,
234234
children: [
235235
Text(
236-
'The issues might affect PowerSync in your app',
236+
'These issues might affect PowerSync in your app',
237237
style: TextTheme.of(context).bodyLarge,
238238
),
239239
Padding(

pubspec.lock

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1761,10 +1761,10 @@ packages:
17611761
dependency: transitive
17621762
description:
17631763
name: vm_service
1764-
sha256: "046d3928e16fa4dc46e8350415661755ab759d9fc97fc21b5ab295f71e4f0499"
1764+
sha256: "0016aef94fc66495ac78af5859181e3f3bf2026bd8eecc72b9565601e19ab360"
17651765
url: "https://pub.dev"
17661766
source: hosted
1767-
version: "15.1.0"
1767+
version: "15.2.0"
17681768
watcher:
17691769
dependency: transitive
17701770
description:

0 commit comments

Comments
 (0)