blob: 90d961d1b3630d53cd450315b34d279bf997d9f2 [file] [log] [blame]
// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
@Timeout(Duration(minutes: 2))
import 'dart:async';
import 'dart:convert';
import 'package:dwds/data/devtools_request.dart';
import 'package:dwds/data/extension_request.dart';
import 'package:dwds/data/serializers.dart';
import 'package:dwds/src/debugging/execution_context.dart';
import 'package:dwds/src/servers/extension_debugger.dart';
import 'package:test/test.dart';
import 'package:test_common/logging.dart';
import 'package:webkit_inspection_protocol/webkit_inspection_protocol.dart';
import 'fixtures/fakes.dart';
void main() async {
final bool debug = false;
group('ExecutionContext', () {
setUpAll(() {
setCurrentLogWriter(debug: debug);
});
TestDebuggerConnection? debugger;
TestDebuggerConnection getDebugger() => debugger!;
setUp(() async {
setCurrentLogWriter(debug: debug);
debugger = TestDebuggerConnection();
});
tearDown(() async {
await debugger?.close();
});
test('is created on devtools request', () async {
final debugger = getDebugger();
await debugger.createDebuggerExecutionContext(TestContextId.dartDefault);
// Expect the context ID to be set.
expect(await debugger.defaultContextId(), TestContextId.dartDefault);
});
test('clears context ID', () async {
final debugger = getDebugger();
await debugger.createDebuggerExecutionContext(TestContextId.dartDefault);
debugger.sendContextsClearedEvent();
// Expect non-dart context.
expect(await debugger.defaultContextId(), TestContextId.none);
});
test('finds dart context ID', () async {
final debugger = getDebugger();
await debugger.createDebuggerExecutionContext(TestContextId.none);
debugger.sendContextCreatedEvent(TestContextId.dartNormal);
// Expect dart context.
expect(await debugger.dartContextId(), TestContextId.dartNormal);
});
test('does not find dart context ID if not available', () async {
final debugger = getDebugger();
await debugger.createDebuggerExecutionContext(TestContextId.none);
// No context IDs received yet.
expect(await debugger.defaultContextId(), TestContextId.none);
debugger.sendContextCreatedEvent(TestContextId.dartLate);
// Expect no dart context.
// This mocks injected client still loading.
expect(await debugger.noContextId(), TestContextId.none);
// Expect dart context.
// This mocks injected client loading later for previously
// received context ID.
expect(await debugger.dartContextId(), TestContextId.dartLate);
});
test('works with stale contexts', () async {
final debugger = getDebugger();
await debugger.createDebuggerExecutionContext(TestContextId.none);
debugger.sendContextCreatedEvent(TestContextId.stale);
// Expect no dart context.
expect(await debugger.noContextId(), TestContextId.none);
debugger.sendContextsClearedEvent();
debugger.sendContextCreatedEvent(TestContextId.dartNormal);
// Expect dart context.
expect(await debugger.dartContextId(), TestContextId.dartNormal);
});
});
}
enum TestContextId {
none,
dartDefault,
dartNormal,
dartLate,
nonDart,
stale;
factory TestContextId.from(int? value) {
return switch (value) {
null => none,
0 => dartDefault,
1 => dartNormal,
2 => dartLate,
3 => nonDart,
4 => stale,
_ => throw StateError('$value is not a TestContextId'),
};
}
int? get id {
return switch (this) {
none => null,
dartDefault => 0,
dartNormal => 1,
dartLate => 2,
nonDart => 3,
stale => 4,
};
}
}
class TestExtensionDebugger extends ExtensionDebugger {
TestExtensionDebugger(FakeSseConnection super.sseConnection);
@override
Future<WipResponse> sendCommand(
String command, {
Map<String, dynamic>? params,
}) {
final id = params?['contextId'];
final response = super.sendCommand(command, params: params);
/// Mock stale contexts that cause the evaluation to throw.
if (command == 'Runtime.evaluate' &&
TestContextId.from(id) == TestContextId.stale) {
throw Exception('Stale execution context');
}
return response;
}
}
class TestDebuggerConnection {
late final TestExtensionDebugger extensionDebugger;
late final FakeSseConnection connection;
int _evaluateRequestId = 0;
TestDebuggerConnection() {
connection = FakeSseConnection();
extensionDebugger = TestExtensionDebugger(connection);
}
/// Create a new execution context in the debugger.
Future<void> createDebuggerExecutionContext(TestContextId contextId) {
_sendDevToolsRequest(contextId: contextId.id);
return _executionContext();
}
/// Flush the streams and close debugger connection.
Future<void> close() async {
unawaited(connection.controllerOutgoing.stream.any((e) => false));
unawaited(extensionDebugger.devToolsRequestStream.any((e) => false));
await connection.controllerIncoming.sink.close();
await connection.controllerOutgoing.sink.close();
await extensionDebugger.close();
}
/// Return the initial context ID from the DevToolsRequest.
Future<TestContextId> defaultContextId() async {
// Give the previous events time to propagate.
await Future.delayed(Duration(milliseconds: 100));
return TestContextId.from(await extensionDebugger.executionContext!.id);
}
/// Mock receiving dart context ID in the execution context.
///
/// Note: dart context is detected by evaluation of
/// `window.$dartAppInstanceId` in that context returning
/// a non-null value.
Future<TestContextId> dartContextId() async {
// Try getting execution id.
final executionContextId = extensionDebugger.executionContext!.id;
// Give it time to send the evaluate request.
await Future.delayed(Duration(milliseconds: 100));
// Respond to the evaluate request.
_sendEvaluationResponse({
'result': {'value': 'dart'},
});
return TestContextId.from(await executionContextId);
}
/// Mock receiving non-dart context ID in the execution context.
///
/// Note: dart context is detected by evaluation of
/// `window.$dartAppInstanceId` in that context returning
/// a null value.
Future<TestContextId> noContextId() async {
// Try getting execution id.
final executionContextId = extensionDebugger.executionContext!.id;
// Give it time to send the evaluate request.
await Future.delayed(Duration(milliseconds: 100));
// Respond to the evaluate request.
_sendEvaluationResponse({
'result': {'value': null},
});
return TestContextId.from(await executionContextId);
}
/// Send `Runtime.executionContextsCleared` event to the execution
/// context in the extension debugger.
void sendContextsClearedEvent() {
final extensionEvent = ExtensionEvent(
(b) => b
..method = jsonEncode('Runtime.executionContextsCleared')
..params = jsonEncode({}),
);
connection.controllerIncoming.sink
.add(jsonEncode(serializers.serialize(extensionEvent)));
}
/// Send `Runtime.executionContextCreated` event to the execution
/// context in the extension debugger.
void sendContextCreatedEvent(TestContextId contextId) {
final extensionEvent = ExtensionEvent(
(b) => b
..method = jsonEncode('Runtime.executionContextCreated')
..params = jsonEncode({
'context': {'id': '${contextId.id}'},
}),
);
connection.controllerIncoming.sink
.add(jsonEncode(serializers.serialize(extensionEvent)));
}
void _sendEvaluationResponse(Map<String, dynamic> response) {
// Respond to the evaluate request.
final extensionResponse = ExtensionResponse(
(b) => b
..result = jsonEncode(response)
..id = _evaluateRequestId++
..success = true,
);
connection.controllerIncoming.sink
.add(jsonEncode(serializers.serialize(extensionResponse)));
}
void _sendDevToolsRequest({int? contextId}) {
final devToolsRequest = DevToolsRequest(
(b) => b
..contextId = contextId
..appId = 'app'
..instanceId = '0',
);
connection.controllerIncoming.sink
.add(jsonEncode(serializers.serialize(devToolsRequest)));
}
Future<void> _executionContext() async {
final executionContext = await _waitForExecutionContext().timeout(
const Duration(milliseconds: 100),
onTimeout: () {
expect(fail, 'Timeout getting execution context');
return null;
},
);
expect(executionContext, isNotNull);
}
Future<ExecutionContext?> _waitForExecutionContext() async {
while (extensionDebugger.executionContext == null) {
await Future.delayed(Duration(milliseconds: 20));
}
return extensionDebugger.executionContext;
}
}