Skip to content
Open
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
5 changes: 5 additions & 0 deletions pkgs/test/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@
* Require a function definition named `main` directly in a test suite and
provide a more direct error message than a failing compiler output.
* Suppress skip reason messages in the compact and failures-only reporters.
* Add `vm-asan`, `vm-msan`, and `vm-tsan` runtimes to run tests on the standalone
Dart VM under Address Sanitizer, Memory Sanitizer or Thread Sanitizer. This is
useful for finding issues when using foreign libraries through dart:ffi, such
as use-after-free, use of initialized memory and data races, or for detecting
data races in Dart code using shared fields.

## 1.28.0

Expand Down
2 changes: 1 addition & 1 deletion pkgs/test/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ dependencies:
stream_channel: ^2.1.0

# Use an exact version until the test_api and test_core package are stable.
test_api: 0.7.8
test_api: 0.7.9-wip
test_core: 0.6.15-wip

typed_data: ^1.3.0
Expand Down
3 changes: 3 additions & 0 deletions pkgs/test/test/io.dart
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ final Future<String> packageDir = Isolate.resolvePackageUri(
return dir;
});

/// The root directory of the Dart SDK.
final String sdkDir = p.dirname(p.dirname(Platform.resolvedExecutable));

/// The platform-specific message emitted when a nonexistent file is loaded.
final String noSuchFileMessage =
Platform.isWindows
Expand Down
14 changes: 13 additions & 1 deletion pkgs/test/test/runner/compiler_runtime_matrix_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,10 @@ void main() {
for (var compiler in runtime.supportedCompilers) {
// Ignore the platforms we can't run on this OS.
if ((runtime == Runtime.edge && !Platform.isWindows) ||
(runtime == Runtime.safari && !Platform.isMacOS)) {
(runtime == Runtime.safari && !Platform.isMacOS) ||
(runtime == Runtime.vmAsan && !Platform.isLinux) ||
(runtime == Runtime.vmMsan && !Platform.isLinux) ||
(runtime == Runtime.vmTsan && !Platform.isLinux)) {
continue;
}
String? skipReason;
Expand All @@ -36,6 +39,15 @@ void main() {
skipReason = 'https://github.com/dart-lang/test/issues/1942';
} else if (runtime == Runtime.firefox && Platform.isMacOS) {
skipReason = 'https://github.com/dart-lang/test/pull/2276';
} else if (runtime == Runtime.vmAsan &&
!File('$sdkDir/bin/dartaotruntime_asan').existsSync()) {
skipReason = 'SDK too old';
} else if (runtime == Runtime.vmMsan &&
!File('$sdkDir/bin/dartaotruntime_msan').existsSync()) {
skipReason = 'SDK too old';
} else if (runtime == Runtime.vmTsan &&
!File('$sdkDir/bin/dartaotruntime_tsan').existsSync()) {
skipReason = 'SDK too old';
}
group(
'--runtime ${runtime.identifier} --compiler ${compiler.identifier}',
Expand Down
5 changes: 4 additions & 1 deletion pkgs/test/test/runner/runner_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -129,12 +129,15 @@ Output:
''';

final _runtimes =
'[vm (default), chrome, firefox'
'[vm (default), vm-asan, vm-msan, vm-tsan, chrome, firefox'
'${Platform.isMacOS ? ', safari' : ''}'
', edge, node]';

final _runtimeCompilers = [
'[vm]: kernel (default), source, exe',
'[vm-asan]: exe (default)',
'[vm-msan]: exe (default)',
'[vm-tsan]: exe (default)',
'[chrome]: dart2js (default), dart2wasm',
'[firefox]: dart2js (default), dart2wasm',
if (Platform.isMacOS) '[safari]: dart2js (default)',
Expand Down
239 changes: 239 additions & 0 deletions pkgs/test/test/runner/sanitizer_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,239 @@
// Copyright (c) 2025, 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.

@TestOn('vm && linux')
library;

import 'dart:io';
import 'package:test/test.dart';
import 'package:test_descriptor/test_descriptor.dart' as d;

import '../io.dart';

void main() {
setUpAll(precompileTestExecutable);

String? skipReason;
if (!File('$sdkDir/bin/dartaotruntime_asan').existsSync()) {
skipReason = 'SDK too old';
}

test('asan success', () async {
final testSource = '''
@TestOn('vm-asan')
library asan_environment_test;

import 'package:test/test.dart';

void main() {
test('const', () {
// I.e., correct during kernel compilation.
expect(const bool.fromEnvironment("dart.vm.asan"), equals(true));

expect(const bool.fromEnvironment("dart.vm.msan"), equals(false));
expect(const bool.fromEnvironment("dart.vm.tsan"), equals(false));
});

test('new', () {
// I.e., correct during VM lookup.
expect(new bool.fromEnvironment("dart.vm.asan"), equals(true));

expect(new bool.fromEnvironment("dart.vm.msan"), equals(false));
expect(new bool.fromEnvironment("dart.vm.tsan"), equals(false));
});
}
''';

await d.file('test.dart', testSource).create();
var test = await runTest(['test.dart', '-p', 'vm-asan']);

expect(test.stdout, emitsThrough(contains('+2: All tests passed!')));
await test.shouldExit(0);
}, skip: skipReason);

test('asan failure', () async {
final testSource = '''
@TestOn('vm-asan')
library asan_test;

import 'package:test/test.dart';
import 'dart:ffi';

@Native<Pointer Function(IntPtr)>(symbol: 'malloc')
external Pointer malloc(int size);
@Native<Void Function(Pointer)>(symbol: 'free')
external void free(Pointer ptr);
@Native<Void Function(Pointer, Int, Size)>(symbol: 'memset')
external void memset(Pointer ptr, int char, int size);

void main() {
test('use-after-free', () {
var p = malloc(sizeOf<Long>()).cast<Long>();
free(p);
memset(p, 42, sizeOf<Long>()); // ASAN: heap-use-after-free
});
}
''';

await d.file('test.dart', testSource).create();
var test = await runTest(['test.dart', '-p', 'vm-asan']);

expect(
test.stderr,
emitsThrough(contains('AddressSanitizer: heap-use-after-free')),
);
await test.shouldExit(6);
}, skip: skipReason);

test('msan success', () async {
final testSource = '''
@TestOn('vm-msan')
library msan_environment_test;

import 'package:test/test.dart';

void main() {
test('const', () {
// I.e., correct during kernel compilation.
expect(const bool.fromEnvironment("dart.vm.msan"), equals(true));

expect(const bool.fromEnvironment("dart.vm.asan"), equals(false));
expect(const bool.fromEnvironment("dart.vm.tsan"), equals(false));
});

test('new', () {
// I.e., correct during VM lookup.
expect(new bool.fromEnvironment("dart.vm.msan"), equals(true));

expect(new bool.fromEnvironment("dart.vm.asan"), equals(false));
expect(new bool.fromEnvironment("dart.vm.tsan"), equals(false));
});
}
''';

await d.file('test.dart', testSource).create();
var test = await runTest(['test.dart', '-p', 'vm-msan']);

expect(test.stdout, emitsThrough(contains('+2: All tests passed!')));
await test.shouldExit(0);
}, skip: skipReason);

test('msan failure', () async {
final testSource = '''
@TestOn('vm-msan')
library msan_test;

import 'dart:ffi';
import 'package:test/test.dart';

@Native<Pointer Function(IntPtr)>(symbol: 'malloc')
external Pointer malloc(int size);
@Native<Void Function(Pointer)>(symbol: 'free')
external void free(Pointer ptr);
@Native<Void Function(Pointer, Pointer, Size)>(symbol: 'memcmp')
external void memcmp(Pointer a, Pointer b, int size);

void main() {
test('uninitialized', () {
var a = malloc(8);
var b = malloc(8);
memcmp(a, b, 8); // MSAN: use-of-uninitialized-value
free(b);
free(a);
});
}
''';

await d.file('test.dart', testSource).create();
var test = await runTest(['test.dart', '-p', 'vm-msan']);

expect(
test.stderr,
emitsThrough(contains('MemorySanitizer: use-of-uninitialized-value')),
);
await test.shouldExit(6);
}, skip: skipReason);

test('tsan success', () async {
final testSource = '''
@TestOn('vm-tsan')
library tsan_environment_test;

import 'package:test/test.dart';

void main() {
test('const', () {
// I.e., correct during kernel compilation.
expect(const bool.fromEnvironment("dart.vm.tsan"), equals(true));

expect(const bool.fromEnvironment("dart.vm.asan"), equals(false));
expect(const bool.fromEnvironment("dart.vm.msan"), equals(false));
});

test('new', () {
// I.e., correct during VM lookup.
expect(new bool.fromEnvironment("dart.vm.tsan"), equals(true));

expect(new bool.fromEnvironment("dart.vm.asan"), equals(false));
expect(new bool.fromEnvironment("dart.vm.msan"), equals(false));
});
}
''';

await d.file('test.dart', testSource).create();
var test = await runTest(['test.dart', '-p', 'vm-tsan']);

expect(test.stdout, emitsThrough(contains('+2: All tests passed!')));
await test.shouldExit(0);
}, skip: skipReason);

test('tsan failure', () async {
final testSource = '''
@TestOn('vm-tsan')
library tsan_test;

import 'dart:ffi';
import 'dart:isolate';
import 'package:test/test.dart';

@Native<Pointer Function(IntPtr)>(symbol: 'malloc')
external Pointer malloc(int size);
@Native<Void Function(Pointer)>(symbol: 'free')
external void free(Pointer ptr);
@Native<Void Function(Pointer, Int, Size)>(symbol: 'memset', isLeaf: true)
external void memset_leaf(Pointer ptr, int char, int size);
@Native<Void Function(IntPtr)>(symbol: 'usleep', isLeaf: true)
external void usleep_leaf(int useconds);

child(addr) {
var p = Pointer<IntPtr>.fromAddress(addr);
for (var i = 0; i < 50000; i++) {
memset_leaf(p, 42, sizeOf<IntPtr>()); // TSAN: data race
usleep_leaf(100);
}
}

void main() {
test('data race', () async {
var p = malloc(sizeOf<IntPtr>()).cast<IntPtr>();
var f = Isolate.run(() => child(p.address));

for (var i = 0; i < 50000; i++) {
p[0] = p[0] + 1; // TSAN: data race
usleep_leaf(100);
}

await f;
free(p);
});
}
''';

await d.file('test.dart', testSource).create();
var test = await runTest(['test.dart', '-p', 'vm-tsan']);

expect(test.stderr, emitsThrough(contains('ThreadSanitizer: data race')));
await test.shouldExit(6);
}, skip: skipReason);
}
4 changes: 4 additions & 0 deletions pkgs/test_api/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 0.7.9-wip

* Add `vmAsan`, `vmMsan` and `vmTsan` runtimes.

## 0.7.8

* Add a zone function available from the test suite `main` that allows creating
Expand Down
24 changes: 24 additions & 0 deletions pkgs/test_api/lib/src/backend/runtime.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,27 @@ final class Runtime {
Compiler.source,
Compiler.exe,
], isDartVM: true);
static const Runtime vmAsan = Runtime(
'VM with Address Sanitizer',
'vm-asan',
Compiler.exe,
[Compiler.exe],
isDartVM: true,
);
static const Runtime vmMsan = Runtime(
'VM with Memory Sanitizer',
'vm-msan',
Compiler.exe,
[Compiler.exe],
isDartVM: true,
);
static const Runtime vmTsan = Runtime(
'VM with Thread Sanitizer',
'vm-tsan',
Compiler.exe,
[Compiler.exe],
isDartVM: true,
);

/// Google Chrome.
static const Runtime chrome = Runtime(
Expand Down Expand Up @@ -69,6 +90,9 @@ final class Runtime {
/// The platforms that are supported by the test runner by default.
static const List<Runtime> builtIn = [
Runtime.vm,
Runtime.vmAsan,
Runtime.vmMsan,
Runtime.vmTsan,
Runtime.chrome,
Runtime.firefox,
Runtime.safari,
Expand Down
2 changes: 1 addition & 1 deletion pkgs/test_api/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
name: test_api
version: 0.7.8
version: 0.7.9-wip
description: >-
The user facing API for structuring Dart tests and checking expectations.
repository: https://github.com/dart-lang/test/tree/master/pkgs/test_api
Expand Down
5 changes: 5 additions & 0 deletions pkgs/test_core/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@
* Fix default coverage filter when running in a workspace package. Default
filter now includes all the workspace's package.
* Add support for reading test package version within pub workspaces.
* Add `vm-asan`, `vm-msan`, and `vm-tsan` runtimes to run tests on the standalone
Dart VM under Address Sanitizer, Memory Sanitizer or Thread Sanitizer. This is
useful for finding issues when using foreign libraries through dart:ffi, such
as use-after-free, use of initialized memory and data races, or for detecting
data races in Dart code using shared fields.

## 0.6.14

Expand Down
7 changes: 6 additions & 1 deletion pkgs/test_core/lib/src/runner/loader.dart
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,12 @@ class Loader {
/// Creates a new loader that loads tests on platforms defined in
/// [Configuration.current].
Loader() {
_registerPlatformPlugin([Runtime.vm], VMPlatform.new);
_registerPlatformPlugin([
Runtime.vm,
if (File('$sdkDir/bin/dartaotruntime_asan').existsSync()) Runtime.vmAsan,
if (File('$sdkDir/bin/dartaotruntime_msan').existsSync()) Runtime.vmMsan,
if (File('$sdkDir/bin/dartaotruntime_tsan').existsSync()) Runtime.vmTsan,
], VMPlatform.new);

platformCallbacks.forEach((runtime, plugin) {
_registerPlatformPlugin([runtime], plugin);
Expand Down
Loading