Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions .github/workflows/build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,31 @@ jobs:
PLATFORM: vm
run: ./tool/ci/bots.sh

webdriver_test:
name: ${{ matrix.os }} Webdriver test
needs: flutter-prep
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
bot:
- test_webdriver
os: [ubuntu-latest, windows-latest]
steps:
- name: git clone
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8
- name: Load Cached Flutter SDK
uses: actions/cache@d4323d4df104b026a6aa633fdb11d772146be0bf
with:
path: |
./tool/flutter-sdk
key: flutter-sdk-${{ runner.os }}-${{ needs.flutter-prep.outputs.latest_flutter_candidate }}
- name: tool/ci/bots.sh
env:
BOT: ${{ matrix.bot }}
PLATFORM: vm
run: ./tool/ci/bots.sh

macos-test:
needs: flutter-prep
name: macos goldens ${{ matrix.bot }}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -272,7 +272,7 @@ Future<int> _findAvailablePort({required int startingAt}) async {

Future<bool> _isPortAvailable(int port) async {
try {
final RawSocket socket = await RawSocket.connect('localhost', port);
final socket = await RawSocket.connect('localhost', port);
socket.shutdown(SocketDirection.both);
await socket.close();
return false;
Expand Down
123 changes: 80 additions & 43 deletions packages/devtools_app/integration_test/test_infra/run/run_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import 'dart:io';

import 'package:args/args.dart';
import 'package:devtools_shared/devtools_test_utils.dart';
import 'package:path/path.dart' as p;

import '_in_file_args.dart';
import '_test_app_driver.dart';
Expand Down Expand Up @@ -35,50 +36,10 @@ Future<void> runFlutterIntegrationTest(
// TODO(https://github.com/flutter/devtools/issues/9196): support starting
// DTD and passing the URI to DevTools server. Workspace roots should be set
// on the DTD instance based on the connected test app.

// Start the DevTools server. This will use the DevTools server that is
// shipped with the Dart SDK.
// TODO(https://github.com/flutter/devtools/issues/9197): launch the
// DevTools server from source so that end to end changes (server + app) can
// be tested.
devToolsServerProcess = await Process.start('dart', [
'devtools',
// Do not launch DevTools app in the browser. This DevTools server
// instance will be used to connect to the DevTools app that is run from
// Flutter driver from the integration test runner.
'--no-launch-browser',
// Disable CORS restrictions so that we can connect to the server from
// DevTools app that is served on a different origin.
'--disable-cors',
]);

final addressCompleter = Completer<void>();
final sub = devToolsServerProcess.stdout.transform(utf8.decoder).listen((
line,
) {
if (line.startsWith(_devToolsServerAddressLine)) {
// This will pull the server address from a String like:
// "Serving DevTools at http://127.0.0.1:9104.".
final regexp = RegExp(
r'http:\/\/\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}:\d+',
);
final match = regexp.firstMatch(line);
if (match != null) {
devToolsServerAddress = match.group(0);
addressCompleter.complete();
}
}
});

await addressCompleter.future.timeout(
const Duration(seconds: 10),
onTimeout: () async {
await sub.cancel();
devToolsServerProcess?.kill();
throw Exception('Timed out waiting for DevTools server to start.');
},
devToolsServerProcess = await startDevToolsServer();
devToolsServerAddress = await listenForDevToolsAddress(
devToolsServerProcess,
);
await sub.cancel();
}

if (!offline) {
Expand Down Expand Up @@ -195,3 +156,79 @@ class DevToolsAppTestRunnerArgs extends IntegrationTestRunnerArgs {
);
}
}

Future<Process> startDevToolsServer({bool useLocalServer = false}) async {
// Start the DevTools server from source.
if (useLocalServer) {
return _startLocalDevToolsServer();
}

// Start the DevTools server. This will use the DevTools server that is
// shipped with the Dart SDK.
final devToolsServerProcess = await Process.start('dart', [
'devtools',
// Do not launch DevTools app in the browser. This DevTools server
// instance will be used to connect to the DevTools app that is run from
// Flutter driver from the integration test runner.
'--no-launch-browser',
// Disable CORS restrictions so that we can connect to the server from
// DevTools app that is served on a different origin.
'--disable-cors',
]);
return devToolsServerProcess;
}

Future<String> listenForDevToolsAddress(
Process devToolsServerProcess, {
Duration timeout = const Duration(minutes: 3),
}) async {
final devToolsAddressCompleter = Completer<String>();

final sub = devToolsServerProcess.stdout.transform(utf8.decoder).listen((
line,
) {
print('[Server - ${DateTime.now()}] $line');
if (line.contains(_devToolsServerAddressLine)) {
// This will pull the server address from a String like:
// "Serving DevTools at http://127.0.0.1:9104.".
final regexp = RegExp(r'http:\/\/\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}:\d+');
final match = regexp.firstMatch(line);
if (match != null) {
final devToolsServerAddress = match.group(0);
devToolsAddressCompleter.complete(devToolsServerAddress);
}
}
});

await devToolsAddressCompleter.future.timeout(
timeout,
onTimeout: () async {
await sub.cancel();
devToolsServerProcess.kill();
throw Exception('Timed out waiting for DevTools server to start.');
},
);
await sub.cancel();

return devToolsAddressCompleter.future;
}

Future<Process> _startLocalDevToolsServer() async {
final devtoolsProcess = await Process.start('dart', [
'run',
'bin/dt.dart',
'serve',
'--no-launch-browser',
], workingDirectory: _toolPath());
return devtoolsProcess;
}

String _toolPath() {
final dir = Directory.current;
final pathParts = p.split(dir.path);
if (!pathParts.contains('packages')) {
throw StateError('Expected to be in a package, instead in ${dir.path}');
}
final root = pathParts.sublist(0, pathParts.indexOf('packages'));
return p.joinAll([...root, 'tool']);
}
Original file line number Diff line number Diff line change
Expand Up @@ -744,12 +744,12 @@ class CpuProfileData with Serializable {

List<CpuStackFrame>? _bottomUpRoots;

late final Iterable<String> userTags = {
late final userTags = {
for (final cpuSample in cpuSamples)
if (cpuSample.userTag case final userTag?) userTag,
};

late final Iterable<String> vmTags = {
late final vmTags = {
for (final cpuSample in cpuSamples)
if (cpuSample.vmTag case final vmTag?) vmTag,
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ class DartIOHttpInstantEvent {
TimeRange get timeRange => _timeRangeBuilder.build();

// This is modified from within HttpRequestData.
final TimeRangeBuilder _timeRangeBuilder = TimeRangeBuilder();
final _timeRangeBuilder = TimeRangeBuilder();
}

/// An abstraction of an HTTP request made through dart:io.
Expand Down
1 change: 1 addition & 0 deletions packages/devtools_app/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ dev_dependencies:
stream_channel: ^2.1.1
test: ^1.21.0
web_benchmarks: ^4.0.0
webdriver: ^3.1.0
webkit_inspection_protocol: ">=0.5.0 <2.0.0"

dependency_overrides:
Expand Down
93 changes: 93 additions & 0 deletions packages/devtools_app/webdriver_test/web_compiler_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
// Copyright 2025 The Flutter Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd.

import 'dart:async';
import 'dart:io';

import 'package:devtools_shared/devtools_test_utils.dart';
import 'package:devtools_test/helpers.dart';
import 'package:devtools_test/integration_test.dart';
import 'package:test/test.dart';
import 'package:webdriver/async_io.dart';

import '../integration_test/test_infra/run/run_test.dart';

void main() {
late Process devtoolsProcess;
late WebDriver driver;
late String devToolsServerAddress;

const serverStartupTimeout = Duration(minutes: 3);

setUp(() async {
// Start ChromeDriver.
await ChromeDriver().start(debugLogging: true);

// Start the DevTools server.
devtoolsProcess = await startDevToolsServer(useLocalServer: true);
devToolsServerAddress = await listenForDevToolsAddress(
devtoolsProcess,
timeout: serverStartupTimeout,
);

// Create a WebDriver instance.
driver = await createDriver(
uri: Uri.parse('http://127.0.0.1:${ChromeDriver.port}'),
desired: {
...Capabilities.chrome,
Capabilities.chromeOptions: {
'args': ['--headless'],
},
},
);
});

tearDown(() async {
await driver.quit();
devtoolsProcess.kill();
});

Future<String?> getRendererAttribute() => retryUntilNotNull(() async {
final body = await driver.findElement(const By.tagName('body'));
return body.attributes['flt-renderer'];
});

group('compilation', () {
test(
'compiler query param determines skwasm/canvaskit renderer',
timeout: longTimeout,
() async {
// Open the DevTools URL with ?compiler=wasm.
await driver.get(
_addQueryParam(
devToolsServerAddress,
param: 'compiler',
value: 'wasm',
),
);
// Verify we are using the skwasm renderer.
expect(await getRendererAttribute(), equals('skwasm'));

// Open the DevTools URL with ?compiler=js.
await driver.get(
_addQueryParam(devToolsServerAddress, param: 'compiler', value: 'js'),
);
// Verify we are using the canvaskit renderer.
expect(await getRendererAttribute(), equals('canvaskit'));
},
retry: 1,
);
});
}

String _addQueryParam(
String url, {
required String param,
required String value,
}) {
final uri = Uri.parse(url);
final newQueryParameters = Map<String, dynamic>.of(uri.queryParameters);
newQueryParameters[param] = value;
return uri.replace(queryParameters: newQueryParameters).toString();
}
4 changes: 3 additions & 1 deletion packages/devtools_shared/lib/src/test/chrome_driver.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import 'dart:io';
import 'io_utils.dart';

class ChromeDriver with IOMixin {
static const port = 4444;

Process? _process;

// TODO(kenz): add error messaging if the chromedriver executable is not
Expand All @@ -17,7 +19,7 @@ class ChromeDriver with IOMixin {
Future<void> start({bool debugLogging = false}) async {
try {
const chromedriverExe = 'chromedriver';
const chromedriverArgs = ['--port=4444'];
const chromedriverArgs = ['--port=$port'];
if (debugLogging) {
print('${DateTime.now()}: starting the chromedriver process');
print('${DateTime.now()}: > $chromedriverExe '
Expand Down
18 changes: 18 additions & 0 deletions packages/devtools_test/lib/src/helpers/utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ const includeForCustomerTestsTag = 'include-for-flutter-customer-tests';
/// since the library containing the test case is already excluded.
const skipForCustomerTestsTag = 'skip-for-flutter-customer-tests';

const extraShortPumpDuration = Duration(milliseconds: 250);
const shortPumpDuration = Duration(seconds: 1);
const safePumpDuration = Duration(seconds: 3);
const longPumpDuration = Duration(seconds: 6);
Expand Down Expand Up @@ -240,6 +241,23 @@ Future<Finder> retryUntilFound(
return retryUntilFound(finder, tester: tester, retries: retries - 1);
}


/// Retries the [callback] until the result is not null.
///
/// This will retry the [callback] up to [retries] times, with an
/// [extraShortPumpDuration] delay between each attempt.
Future<T?> retryUntilNotNull<T>(
Future<T?> Function() callback, {
int retries = 3,
}) async {
final result = await callback();
if (retries == 0 || result != null) return result;

await Future.delayed(extraShortPumpDuration);
return retryUntilNotNull(callback, retries: retries - 1);
}


void logStatus(String message) {
// ignore: avoid_print, intentional print for test output
print('${DateTime.now()}: TEST STATUS: $message');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -156,16 +156,16 @@ Future<void> verifyScreenshot(
///
/// Adjust as needed; this is used to override the 10-minute or infinite timeout
/// in [testWidgets].
const Timeout shortTimeout = Timeout(Duration(minutes: 2));
const shortTimeout = Timeout(Duration(minutes: 2));

/// A timeout for a "medium" integration test.
///
/// Adjust as needed; this is used to override the 10-minute or infinite timeout
/// in [testWidgets].
const Timeout mediumTimeout = Timeout(Duration(minutes: 3));
const mediumTimeout = Timeout(Duration(minutes: 3));

/// A timeout for a "long" integration test.
///
/// Adjust as needed; this is used to override the 10-minute or infinite timeout
/// in [testWidgets].
const Timeout longTimeout = Timeout(Duration(minutes: 4));
const longTimeout = Timeout(Duration(minutes: 4));
3 changes: 3 additions & 0 deletions tool/ci/bots.sh
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@ elif [[ "$BOT" == "test_ddc" || "$BOT" == "test_dart2js" ]]; then
exit 1
fi

elif [ "$BOT" = "test_webdriver" ]; then
flutter test webdriver_test

# TODO(https://github.com/flutter/devtools/issues/1987): consider running integration tests
# for a DDC build of DevTools
# elif [ "$BOT" = "integration_ddc" ]; then
Expand Down
Loading
Loading