Version 3.7.0-167.0.dev
Merge 14734d5767f5e2af59f138a74668bd0ccac5fa9b into dev
diff --git a/pkg/analyzer/lib/src/test_utilities/mock_sdk_elements.dart b/pkg/analyzer/lib/src/test_utilities/mock_sdk_elements.dart
index 33c9878..17bc32d 100644
--- a/pkg/analyzer/lib/src/test_utilities/mock_sdk_elements.dart
+++ b/pkg/analyzer/lib/src/test_utilities/mock_sdk_elements.dart
@@ -26,6 +26,8 @@
var builder = _MockSdkElementsBuilder(analysisContext, analysisSession);
var coreLibrary = builder._buildCore();
var asyncLibrary = builder._buildAsync();
+ builder._populateCore();
+ builder._populateAsync();
return MockSdkElements._(coreLibrary, asyncLibrary);
}
@@ -70,6 +72,10 @@
InterfaceType? _stringType;
InterfaceType? _typeType;
+ late CompilationUnitElementImpl _asyncUnit;
+
+ late CompilationUnitElementImpl _coreUnit;
+
_MockSdkElementsBuilder(
this.analysisContext,
this.analysisSession,
@@ -79,7 +85,7 @@
var boolElement = _boolElement;
if (boolElement != null) return boolElement;
- _boolElement = boolElement = _class(name: 'bool');
+ _boolElement = boolElement = _class(name: 'bool', unit: _coreUnit);
boolElement.supertype = objectType;
boolElement.constructors = [
@@ -111,6 +117,7 @@
name: 'Comparable',
isAbstract: true,
typeParameters: [tElement],
+ unit: _coreUnit,
);
comparableElement.supertype = objectType;
@@ -127,6 +134,7 @@
name: 'Completer',
isAbstract: true,
typeParameters: [tElement],
+ unit: _asyncUnit,
);
completerElement.supertype = objectType;
@@ -138,7 +146,8 @@
var deprecatedElement = _deprecatedElement;
if (deprecatedElement != null) return deprecatedElement;
- _deprecatedElement = deprecatedElement = _class(name: 'Deprecated');
+ _deprecatedElement =
+ deprecatedElement = _class(name: 'Deprecated', unit: _coreUnit);
deprecatedElement.supertype = objectType;
deprecatedElement.fields = [
@@ -168,6 +177,7 @@
_doubleElement = doubleElement = _class(
name: 'double',
isAbstract: true,
+ unit: _coreUnit,
);
doubleElement.supertype = numType;
@@ -236,6 +246,7 @@
_functionElement = functionElement = _class(
name: 'Function',
isAbstract: true,
+ unit: _coreUnit,
);
functionElement.supertype = objectType;
@@ -258,6 +269,7 @@
name: 'Future',
isAbstract: true,
typeParameters: [tElement],
+ unit: _asyncUnit,
);
futureElement.supertype = objectType;
@@ -305,6 +317,7 @@
_futureOrElement = futureOrElement = _class(
name: 'FutureOr',
typeParameters: [tElement],
+ unit: _asyncUnit,
);
futureOrElement.supertype = objectType;
@@ -316,7 +329,8 @@
var intElement = _intElement;
if (intElement != null) return intElement;
- _intElement = intElement = _class(name: 'int', isAbstract: true);
+ _intElement =
+ intElement = _class(name: 'int', isAbstract: true, unit: _coreUnit);
intElement.supertype = numType;
intElement.constructors = [
@@ -376,6 +390,7 @@
name: 'Iterable',
isAbstract: true,
typeParameters: [eElement],
+ unit: _coreUnit,
);
iterableElement.supertype = objectType;
@@ -403,6 +418,7 @@
name: 'Iterator',
isAbstract: true,
typeParameters: [eElement],
+ unit: _coreUnit,
);
iteratorElement.supertype = objectType;
@@ -425,6 +441,7 @@
name: 'List',
isAbstract: true,
typeParameters: [eElement],
+ unit: _coreUnit,
);
listElement.supertype = objectType;
listElement.interfaces = [
@@ -471,6 +488,7 @@
name: 'Map',
isAbstract: true,
typeParameters: [kElement, vElement],
+ unit: _coreUnit,
);
mapElement.supertype = objectType;
@@ -496,7 +514,7 @@
var nullElement = _nullElement;
if (nullElement != null) return nullElement;
- _nullElement = nullElement = _class(name: 'Null');
+ _nullElement = nullElement = _class(name: 'Null', unit: _coreUnit);
nullElement.supertype = objectType;
nullElement.constructors = [
@@ -514,7 +532,8 @@
var numElement = _numElement;
if (numElement != null) return numElement;
- _numElement = numElement = _class(name: 'num', isAbstract: true);
+ _numElement =
+ numElement = _class(name: 'num', isAbstract: true, unit: _coreUnit);
numElement.supertype = objectType;
numElement.interfaces = [
_interfaceType(
@@ -600,6 +619,7 @@
if (objectElement != null) return objectElement;
_objectElement = objectElement = ElementFactory.object;
+ _coreUnit.encloseElement(objectElement);
objectElement.interfaces = const <InterfaceType>[];
objectElement.mixins = const <InterfaceType>[];
objectElement.typeParameters = const <TypeParameterElement>[];
@@ -634,7 +654,8 @@
var overrideElement = _overrideElement;
if (overrideElement != null) return overrideElement;
- _overrideElement = overrideElement = _class(name: '_Override');
+ _overrideElement =
+ overrideElement = _class(name: '_Override', unit: _coreUnit);
overrideElement.supertype = objectType;
overrideElement.constructors = [
@@ -652,6 +673,7 @@
_recordElement = recordElement = _class(
name: 'Record',
isAbstract: true,
+ unit: _coreUnit,
);
recordElement.supertype = objectType;
@@ -674,6 +696,7 @@
name: 'Set',
isAbstract: true,
typeParameters: [eElement],
+ unit: _coreUnit,
);
setElement.supertype = objectType;
setElement.interfaces = [
@@ -691,6 +714,7 @@
_stackTraceElement = stackTraceElement = _class(
name: 'StackTrace',
isAbstract: true,
+ unit: _coreUnit,
);
stackTraceElement.supertype = objectType;
@@ -709,6 +733,7 @@
name: 'Stream',
isAbstract: true,
typeParameters: [tElement],
+ unit: _asyncUnit,
);
streamElement.isAbstract = true;
streamElement.supertype = objectType;
@@ -752,6 +777,7 @@
name: 'StreamSubscription',
isAbstract: true,
typeParameters: [tElement],
+ unit: _asyncUnit,
);
streamSubscriptionElement.supertype = objectType;
@@ -766,6 +792,7 @@
_stringElement = stringElement = _class(
name: 'String',
isAbstract: true,
+ unit: _coreUnit,
);
stringElement.supertype = objectType;
@@ -810,6 +837,7 @@
_symbolElement = symbolElement = _class(
name: 'Symbol',
isAbstract: true,
+ unit: _coreUnit,
);
symbolElement.supertype = objectType;
@@ -834,6 +862,7 @@
_typeElement = typeElement = _class(
name: 'Type',
isAbstract: true,
+ unit: _coreUnit,
);
typeElement.supertype = objectType;
@@ -900,21 +929,13 @@
FeatureSet.latestLanguageVersion(),
);
- var asyncUnit = CompilationUnitElementImpl(
+ _asyncUnit = CompilationUnitElementImpl(
library: asyncLibrary,
source: asyncSource,
lineInfo: LineInfo([0]),
);
- asyncUnit.classes = <ClassElementImpl>[
- completerElement,
- futureElement,
- futureOrElement,
- streamElement,
- streamSubscriptionElement
- ];
-
- asyncLibrary.definingCompilationUnit = asyncUnit;
+ asyncLibrary.definingCompilationUnit = _asyncUnit;
return asyncLibrary;
}
@@ -939,65 +960,13 @@
FeatureSet.latestLanguageVersion(),
);
- var coreUnit = CompilationUnitElementImpl(
+ _coreUnit = CompilationUnitElementImpl(
library: coreLibrary,
source: coreSource,
lineInfo: LineInfo([0]),
);
- coreUnit.classes = <ClassElementImpl>[
- boolElement,
- comparableElement,
- deprecatedElement,
- doubleElement,
- functionElement,
- intElement,
- iterableElement,
- iteratorElement,
- listElement,
- mapElement,
- nullElement,
- numElement,
- objectElement,
- overrideElement,
- recordElement,
- setElement,
- stackTraceElement,
- stringElement,
- symbolElement,
- typeElement,
- ];
-
- coreUnit.functions = <FunctionElementImpl>[
- _function('identical', boolType, parameters: [
- _requiredParameter('a', objectType),
- _requiredParameter('b', objectType),
- ]),
- _function('print', voidType, parameters: [
- _requiredParameter('object', objectType),
- ]),
- ];
-
- var deprecatedVariable = _topLevelVariableConst(
- 'deprecated',
- _interfaceType(deprecatedElement),
- );
-
- var overrideVariable = _topLevelVariableConst(
- 'override',
- _interfaceType(overrideElement),
- );
-
- coreUnit.accessors = <PropertyAccessorElementImpl>[
- deprecatedVariable.getter!,
- overrideVariable.getter!,
- ];
- coreUnit.topLevelVariables = <TopLevelVariableElementImpl>[
- deprecatedVariable,
- overrideVariable,
- ];
-
- coreLibrary.definingCompilationUnit = coreUnit;
+ coreLibrary.definingCompilationUnit = _coreUnit;
return coreLibrary;
}
@@ -1005,12 +974,14 @@
required String name,
bool isAbstract = false,
List<TypeParameterElement> typeParameters = const [],
+ required CompilationUnitElementImpl unit,
}) {
var element = ClassElementImpl(name, 0);
element.typeParameters = typeParameters;
element.constructors = <ConstructorElementImpl>[
_constructor(),
];
+ unit.encloseElement(element);
return element;
}
@@ -1118,6 +1089,70 @@
return parameter;
}
+ void _populateAsync() {
+ _asyncUnit.classes = <ClassElementImpl>[
+ completerElement,
+ futureElement,
+ futureOrElement,
+ streamElement,
+ streamSubscriptionElement
+ ];
+ }
+
+ void _populateCore() {
+ _coreUnit.classes = <ClassElementImpl>[
+ boolElement,
+ comparableElement,
+ deprecatedElement,
+ doubleElement,
+ functionElement,
+ intElement,
+ iterableElement,
+ iteratorElement,
+ listElement,
+ mapElement,
+ nullElement,
+ numElement,
+ objectElement,
+ overrideElement,
+ recordElement,
+ setElement,
+ stackTraceElement,
+ stringElement,
+ symbolElement,
+ typeElement,
+ ];
+
+ _coreUnit.functions = <FunctionElementImpl>[
+ _function('identical', boolType, parameters: [
+ _requiredParameter('a', objectType),
+ _requiredParameter('b', objectType),
+ ]),
+ _function('print', voidType, parameters: [
+ _requiredParameter('object', objectType),
+ ]),
+ ];
+
+ var deprecatedVariable = _topLevelVariableConst(
+ 'deprecated',
+ _interfaceType(deprecatedElement),
+ );
+
+ var overrideVariable = _topLevelVariableConst(
+ 'override',
+ _interfaceType(overrideElement),
+ );
+
+ _coreUnit.accessors = <PropertyAccessorElementImpl>[
+ deprecatedVariable.getter!,
+ overrideVariable.getter!,
+ ];
+ _coreUnit.topLevelVariables = <TopLevelVariableElementImpl>[
+ deprecatedVariable,
+ overrideVariable,
+ ];
+ }
+
ParameterElement _positionalParameter(String name, DartType type) {
var parameter = ParameterElementImpl(
name: name,
diff --git a/pkg/analyzer/tool/diagnostics/diagnostics.md b/pkg/analyzer/tool/diagnostics/diagnostics.md
index 7dcbda9..1c6589f 100644
--- a/pkg/analyzer/tool/diagnostics/diagnostics.md
+++ b/pkg/analyzer/tool/diagnostics/diagnostics.md
@@ -29271,7 +29271,7 @@
by a `StatefulWidget` after an asynchronous gap without first checking the
`mounted` property.
-Storing a `BuildContext` for later use can lead to difficult to diagnose
+Storing a `BuildContext` for later use can lead to difficult-to-diagnose
crashes. Asynchronous gaps implicitly store a `BuildContext`, making them
easy to overlook for diagnosis.
diff --git a/pkg/dev_compiler/lib/src/js_ast/builder.dart b/pkg/dev_compiler/lib/src/js_ast/builder.dart
index 4e50674..4655804 100644
--- a/pkg/dev_compiler/lib/src/js_ast/builder.dart
+++ b/pkg/dev_compiler/lib/src/js_ast/builder.dart
@@ -1708,8 +1708,8 @@
if (isGetter || isSetter) {
var token = lastToken;
getToken();
- if (lastCategory == COLON) {
- // That wasn't a accessor but the 'get' or 'set' property: retropedal.
+ if (lastCategory == COLON || lastCategory == LPAREN) {
+ // That wasn't a accessor but the 'get' or 'set' property/function.
isGetter = isSetter = false;
name = LiteralString('"$token"');
}
diff --git a/pkg/dev_compiler/lib/src/kernel/compiler_new.dart b/pkg/dev_compiler/lib/src/kernel/compiler_new.dart
index 126d6a04..f66f431 100644
--- a/pkg/dev_compiler/lib/src/kernel/compiler_new.dart
+++ b/pkg/dev_compiler/lib/src/kernel/compiler_new.dart
@@ -2747,6 +2747,130 @@
/// sentinel value if [field] is final to detect multiple initializations.
js_ast.Fun _emitLazyInitializingFunction(js_ast.Expression valueCache,
js_ast.Expression initializer, Field field) {
+ // We avoid emitting casts for top level fields in the legacy SDK since
+ // some are used for legacy type checks and must be initialized to avoid
+ // infinite loops.
+ var initialFieldValueExpression =
+ !_options.soundNullSafety && _isSdkInternalRuntime(_currentLibrary!)
+ ? valueCache
+ : _emitCast(valueCache, field.type);
+
+ // Lazy static fields require an additional type check around their value
+ // cache if their type is updated after hot reload. To avoid a type check
+ // on every access, the generated getter overrides itself with a direct
+ // access on its underlying value cache on first access.
+ // TODO(markzipan): The performance ramifications of a lookup vs
+ // self-rewriting "smart" getter are unknown. We should revisit this if
+ // property accesses become a bottleneck.
+ if (field.isStatic) {
+ var getterName = memberNames[field]!;
+ // Final fields are generated with additional logic to detect
+ // initialization cycles via a special sentinel.
+ if (field.isFinal) {
+ var finalLateInitDetectorSentinel = _getSymbol(
+ _emitPrivateNameSymbol(field.enclosingLibrary, '_#initializing'));
+ // Emits code like:
+ //
+ // if ([valueCache] === _#initializing)
+ // dart.throwLateInitializationError(field);
+ // if ([valueCache] === void 0) {
+ // [valueCache] = _#initializing;
+ // try {
+ // [valueCache] = initializer;
+ // } catch (e) {
+ // // Reset the sentinel on error so it can be reinitialized.
+ // if ([valueCache] === _#initializing) {
+ // [valueCache] = void 0;
+ // }
+ // throw e;
+ // }
+ // }
+ // _typeCheck([valueCache]);
+ // Object.defineProperty(this, field, {
+ // get() {
+ // return [valueCache];
+ // }
+ // });
+ // return this.field;
+ return js.fun(r'''
+ function() {
+ if (# === #) #;
+ if (# === void 0) {
+ # = #;
+ try {
+ # = #;
+ } catch (e) {
+ if (# === #) {
+ # = void 0;
+ }
+ throw e;
+ }
+ }
+ #;
+ Object.defineProperty(this, #, {
+ get() {
+ return #;
+ }
+ });
+ return this.#;
+ }
+ ''', [
+ valueCache,
+ finalLateInitDetectorSentinel,
+ _runtimeCall(
+ 'throwLateInitializationError(#)',
+ [js.string(field.name.text)],
+ ),
+ valueCache,
+ valueCache,
+ finalLateInitDetectorSentinel,
+ valueCache,
+ initializer,
+ valueCache,
+ finalLateInitDetectorSentinel,
+ valueCache,
+ initialFieldValueExpression,
+ js.string(getterName),
+ valueCache,
+ getterName,
+ ]);
+ } else {
+ // Emits code like:
+ //
+ // if ([valueCache] === void 0) {
+ // [valueCache] = initializer;
+ // }
+ // _typeCheck([valueCache]);
+ // Object.defineProperty(this, field, {
+ // get() {
+ // return [valueCache];
+ // }
+ // });
+ // return this.field;
+ return js.fun(r'''
+ function() {
+ if (# === void 0) {
+ # = #;
+ }
+ #;
+ Object.defineProperty(this, #, {
+ get() {
+ return #;
+ }
+ });
+ return this.#;
+ }
+ ''', [
+ valueCache,
+ valueCache,
+ initializer,
+ initialFieldValueExpression,
+ js.string(getterName),
+ valueCache,
+ getterName,
+ ]);
+ }
+ }
// Final fields are generated with additional logic to detect
// initialization cycles via a special sentinel.
if (field.isFinal) {
@@ -2800,7 +2924,7 @@
valueCache,
finalLateInitDetectorSentinel,
valueCache,
- valueCache,
+ initialFieldValueExpression,
]);
} else {
return js.fun(r'''
@@ -2814,7 +2938,7 @@
valueCache,
valueCache,
initializer,
- valueCache,
+ initialFieldValueExpression,
]);
}
}
diff --git a/pkg/dev_compiler/test/hot_reload_suite.dart b/pkg/dev_compiler/test/hot_reload_suite.dart
index c4d54c9..8f0beca 100644
--- a/pkg/dev_compiler/test/hot_reload_suite.dart
+++ b/pkg/dev_compiler/test/hot_reload_suite.dart
@@ -20,16 +20,31 @@
import 'package:reload_test/hot_reload_memory_filesystem.dart';
import 'package:reload_test/test_helpers.dart';
-// Set an arbitrary cap on generations.
-final globalMaxGenerations = 100;
+final buildRootUri = fe.computePlatformBinariesLocation(forceBuildDir: true);
+final sdkRoot = Platform.script.resolve('../../../');
-final testTimeoutSeconds = 10;
+/// SDK test directory containing hot reload tests.
+final allTestsUri = sdkRoot.resolve('tests/hot_reload/');
-// The separator between a test file and its inlined diff.
-//
-// All contents after this separator are considered are diff comments.
+/// The separator between a test file and its inlined diff.
+///
+/// All contents after this separator are considered are diff comments.
final testDiffSeparator = '/** DIFF **/';
+Future<void> main(List<String> args) async {
+ final options = Options.parse(args);
+ if (options.help) {
+ print(options.usage);
+ return;
+ }
+ final runner = switch (options.runtime) {
+ RuntimePlatforms.chrome => ChromeSuiteRunner(options),
+ RuntimePlatforms.d8 => D8SuiteRunner(options),
+ RuntimePlatforms.vm => VMSuiteRunner(options),
+ };
+ await runner.runSuite(options);
+}
+
/// Command line options for the hot reload test suite.
class Options {
final bool help;
@@ -129,7 +144,7 @@
/// Tests in this suite also define a config.json file with further information
/// describing how the test runs.
class HotReloadTest {
- /// Root [Directory] containing the files for this test.
+ /// Root [Directory] containing the source files for this test.
final Directory directory;
/// Test name used in results.
@@ -207,98 +222,157 @@
TestFileEdit(this.generation, this.fileUri);
}
-late final bool verbose;
-late final bool debug;
+// TODO(nshahan): Make this abstract again when there are subclasses for all
+// runtimes.
+class HotReloadSuiteRunner {
+ Options options;
-Future<void> main(List<String> args) async {
- final options = Options.parse(args);
- if (options.help) {
- print(options.usage);
- return;
- }
- verbose = options.verbose;
- debug = options.debug;
+ /// The root directory containing generated code for all tests.
+ late final Directory generatedCodeDir = Directory.systemTemp.createTempSync();
- // Used to communicate individual test failures to our test bots.
- final emitTestResultsJson = options.testResultsOutputDir != null;
- final buildRootUri = fe.computePlatformBinariesLocation(forceBuildDir: true);
- // We can use the outline instead of the full SDK dill here.
- final ddcPlatformDillUri = buildRootUri.resolve('ddc_outline.dill');
- final vmPlatformDillUri = buildRootUri.resolve('vm_platform_strong.dill');
+ /// The directory containing files emitted from Frontend Server compiles and
+ /// recompiles.
+ late final Directory frontendServerEmittedFilesDir =
+ Directory.fromUri(generatedCodeDir.uri.resolve('.fes/'))..createSync();
- final sdkRoot = Platform.script.resolve('../../../');
- final packageConfigUri = sdkRoot.resolve('.dart_tool/package_config.json');
- final allTestsUri = sdkRoot.resolve('tests/hot_reload/');
- final soundStableDartSdkJsUri =
- buildRootUri.resolve('gen/utils/ddc/canary/sdk/ddc/dart_sdk.js');
- final ddcModuleLoaderJsUri =
- sdkRoot.resolve('pkg/dev_compiler/lib/js/ddc/ddc_module_loader.js');
+ /// The output location for .dill file created by the front end server.
+ late final Uri outputDillUri =
+ frontendServerEmittedFilesDir.uri.resolve('output.dill');
- // Contains generated code for all tests.
- final generatedCodeDir = Directory.systemTemp.createTempSync();
- final generatedCodeUri = generatedCodeDir.uri;
- _debugPrint('See generated hot reload framework code in $generatedCodeUri');
+ /// The output location for the incremental .dill file created by the front
+ /// end server.
+ late final Uri outputIncrementalDillUri =
+ frontendServerEmittedFilesDir.uri.resolve('output_incremental.dill');
- // The snapshot directory is a staging area the test framework uses to
- // construct a compile-able test app across reload/restart generations.
- final snapshotDir = Directory.fromUri(generatedCodeUri.resolve('.snapshot/'));
- snapshotDir.createSync();
- final snapshotUri = snapshotDir.uri;
+ /// All test results that are reported after running the entire test suite.
+ final List<TestResultOutcome> testOutcomes = [];
- // Contains files emitted from Frontend Server compiles and recompiles.
- final frontendServerEmittedFilesDirUri = generatedCodeUri.resolve('.fes/');
- Directory.fromUri(frontendServerEmittedFilesDirUri).createSync();
- final outputDillUri = frontendServerEmittedFilesDirUri.resolve('output.dill');
- final outputIncrementalDillUri =
- frontendServerEmittedFilesDirUri.resolve('output_incremental.dill');
+ /// The directory used as a temporary staging area to construct a compile-able
+ /// test app across reload/restart generations.
+ late final Directory snapshotDir =
+ Directory.fromUri(generatedCodeDir.uri.resolve('.snapshot/'))
+ ..createSync();
// TODO(markzipan): Support custom entrypoints.
- final snapshotEntrypointUri = snapshotUri.resolve('main.dart');
- final filesystemRootUri = snapshotUri;
+ late final Uri snapshotEntrypointUri = snapshotDir.uri.resolve('main.dart');
+ late final String snapshotEntrypointWithScheme = () {
+ final snapshotEntrypointLibraryName = fe_shared.relativizeUri(
+ snapshotDir.uri, snapshotEntrypointUri, fe_shared.isWindows);
+ return '$filesystemScheme:///$snapshotEntrypointLibraryName';
+ }();
+
+ HotReloadMemoryFilesystem? filesystem;
+ final stopwatch = Stopwatch();
+
final filesystemScheme = 'hot-reload-test';
- final snapshotEntrypointLibraryName = fe_shared.relativizeUri(
- filesystemRootUri, snapshotEntrypointUri, fe_shared.isWindows);
- final snapshotEntrypointWithScheme =
- '$filesystemScheme:///$snapshotEntrypointLibraryName';
- _print('Initializing the Frontend Server.');
- HotReloadFrontendServerController controller;
- final commonArgs = [
- '--incremental',
- '--filesystem-root=${snapshotUri.toFilePath()}',
- '--filesystem-scheme=$filesystemScheme',
- '--output-dill=${outputDillUri.toFilePath()}',
- '--output-incremental-dill=${outputIncrementalDillUri.toFilePath()}',
- '--packages=${packageConfigUri.toFilePath()}',
- '--sdk-root=${sdkRoot.toFilePath()}',
- '--verbosity=${options.verbose ? 'all' : 'info'}',
- ];
- switch (options.runtime) {
- case RuntimePlatforms.d8:
- case RuntimePlatforms.chrome:
- final ddcPlatformDillFromSdkRoot = fe_shared.relativizeUri(
- sdkRoot, ddcPlatformDillUri, fe_shared.isWindows);
- final fesArgs = [
- ...commonArgs,
- '--dartdevc-module-format=ddc',
- '--dartdevc-canary',
- '--platform=$ddcPlatformDillFromSdkRoot',
- '--target=dartdevc',
- ];
- controller = HotReloadFrontendServerController(fesArgs);
- case RuntimePlatforms.vm:
- final vmPlatformDillFromSdkRoot = fe_shared.relativizeUri(
- sdkRoot, vmPlatformDillUri, fe_shared.isWindows);
- final fesArgs = [
- ...commonArgs,
- '--platform=$vmPlatformDillFromSdkRoot',
- '--target=vm',
- ];
- controller = HotReloadFrontendServerController(fesArgs);
+ HotReloadSuiteRunner(this.options);
+
+ Future<void> runSuite(Options options) async {
+ // TODO(nshahan): report time for collecting and validating test sources.
+ final testSuite = collectTestSources(options);
+ _debugPrint(
+ 'See generated hot reload framework code in ${generatedCodeDir.uri}');
+ final controller = createFrontEndServer();
+ for (final test in testSuite) {
+ stopwatch
+ ..start()
+ ..reset();
+ diffCheck(test);
+ final tempDirectory =
+ Directory.fromUri(generatedCodeDir.uri.resolve('${test.name}/'))
+ ..createSync();
+ if (options.runtime == RuntimePlatforms.d8 ||
+ options.runtime == RuntimePlatforms.chrome) {
+ filesystem = HotReloadMemoryFilesystem(tempDirectory.uri);
+ }
+ var compileSuccess = false;
+ _print('Generating test assets.', label: test.name);
+ // TODO(markzipan): replace this with a test-configurable main entrypoint.
+ final mainDartFilePath =
+ test.directory.uri.resolve('main.dart').toFilePath();
+ _debugPrint('Test entrypoint: $mainDartFilePath', label: test.name);
+ _print('Generating code over ${test.generationCount} generations.',
+ label: test.name);
+ stopwatch
+ ..start()
+ ..reset();
+ for (var generation = 0;
+ generation < test.generationCount;
+ generation++) {
+ final updatedFiles = copyGenerationSources(test, generation);
+ compileSuccess = await compileGeneration(
+ test, generation, tempDirectory, updatedFiles, controller);
+ if (!compileSuccess) break;
+ }
+ if (!compileSuccess) {
+ _print('Did not emit all assets due to compilation error.',
+ label: test.name);
+ // Skip to the next test and avoid execution if there is an unexpected
+ // compilation error.
+ continue;
+ }
+ _print('Finished emitting assets.', label: test.name);
+ final testOutputStreamController = StreamController<List<int>>();
+ final testOutputBuffer = StringBuffer();
+ testOutputStreamController.stream
+ .transform(utf8.decoder)
+ .listen(testOutputBuffer.write);
+ final testPassed = await runTest(
+ test, tempDirectory, IOSink(testOutputStreamController.sink));
+ await reportTestOutcome(
+ test.name, testOutputBuffer.toString(), testPassed);
+ }
+ await shutdown(controller);
+ await reportAllResults();
}
- controller.start();
- Future<void> shutdown() async {
+ /// Returns a controller for a freshly started front end server instance to
+ /// handle compile and recompile requests for a hot reload test.
+ // TODO(nshahan): Breakout into specialized versions for each suite runner.
+ HotReloadFrontendServerController createFrontEndServer() {
+ _print('Initializing the Frontend Server.');
+ HotReloadFrontendServerController controller;
+ final packageConfigUri = sdkRoot.resolve('.dart_tool/package_config.json');
+ final commonArgs = [
+ '--incremental',
+ '--filesystem-root=${snapshotDir.uri.toFilePath()}',
+ '--filesystem-scheme=$filesystemScheme',
+ '--output-dill=${outputDillUri.toFilePath()}',
+ '--output-incremental-dill=${outputIncrementalDillUri.toFilePath()}',
+ '--packages=${packageConfigUri.toFilePath()}',
+ '--sdk-root=${sdkRoot.toFilePath()}',
+ '--verbosity=${options.verbose ? 'all' : 'info'}',
+ ];
+ switch (options.runtime) {
+ case RuntimePlatforms.d8:
+ case RuntimePlatforms.chrome:
+ final ddcPlatformDillFromSdkRoot = fe_shared.relativizeUri(sdkRoot,
+ buildRootUri.resolve('ddc_outline.dill'), fe_shared.isWindows);
+ final fesArgs = [
+ ...commonArgs,
+ '--dartdevc-module-format=ddc',
+ '--dartdevc-canary',
+ '--platform=$ddcPlatformDillFromSdkRoot',
+ '--target=dartdevc',
+ ];
+ controller = HotReloadFrontendServerController(fesArgs);
+ case RuntimePlatforms.vm:
+ final vmPlatformDillFromSdkRoot = fe_shared.relativizeUri(
+ sdkRoot,
+ buildRootUri.resolve('vm_platform_strong.dill'),
+ fe_shared.isWindows);
+ final fesArgs = [
+ ...commonArgs,
+ '--platform=$vmPlatformDillFromSdkRoot',
+ '--target=vm',
+ ];
+ controller = HotReloadFrontendServerController(fesArgs);
+ }
+ return controller..start();
+ }
+
+ Future<void> shutdown(HotReloadFrontendServerController controller) async {
// Persist the temp directory for debugging.
await controller.stop();
_print('Frontend Server has shut down.');
@@ -307,25 +381,6 @@
}
}
- // Only allow Chrome when debugging a single test.
- // TODO(markzipan): Add support for full Chrome testing.
- if (options.runtime == RuntimePlatforms.chrome) {
- var matchingTests =
- Directory.fromUri(allTestsUri).listSync().where((testDir) {
- if (testDir is! Directory) return false;
- final testDirParts = testDir.uri.pathSegments;
- final testName = testDirParts[testDirParts.length - 2];
- return options.testNameFilter.hasMatch(testName);
- });
-
- if (matchingTests.length > 1) {
- throw Exception('Chrome is only supported when debugging a single test.'
- "Please filter on a single test with '-f'.");
- }
- }
-
- final testOutcomes = <TestResultOutcome>[];
-
/// Returns a suite of hot reload tests discovered in the directory
/// [allTestsUri].
///
@@ -387,6 +442,12 @@
}
}
}
+ if (testConfig.excludedPlatforms.contains(options.runtime)) {
+ // Skip this test directory if this platform is excluded.
+ _print('Skipping test on platform: ${options.runtime.text}',
+ label: testName);
+ continue;
+ }
if (maxGenerations > globalMaxGenerations) {
throw Exception('Too many generations specified in test '
'(requested: $maxGenerations, max: $globalMaxGenerations).');
@@ -405,67 +466,59 @@
return testSuite;
}
- for (final test in collectTestSources(options)) {
- final testName = test.name;
- var outcome = TestResultOutcome(
+ /// Report results for this test's execution.
+ Future<void> reportTestOutcome(
+ String testName, String testOutput, bool testPassed) async {
+ stopwatch.stop();
+ final outcome = TestResultOutcome(
configuration: options.namedConfiguration,
testName: testName,
+ testOutput: testOutput,
);
- var stopwatch = Stopwatch()..start();
-
- // Report results for this test's execution.
- Future<void> reportTestOutcome(String testOutput, bool testPassed) async {
- stopwatch.stop();
- outcome.elapsedTime = stopwatch.elapsed;
- outcome.testOutput = testOutput;
- outcome.matchedExpectations = testPassed;
- testOutcomes.add(outcome);
- if (testPassed) {
- _print('PASSED with:\n $testOutput', label: testName);
- } else {
- _print('FAILED with:\n $testOutput', label: testName);
- }
+ outcome.elapsedTime = stopwatch.elapsed;
+ outcome.matchedExpectations = testPassed;
+ testOutcomes.add(outcome);
+ if (testPassed) {
+ _print('PASSED with:\n $testOutput', label: testName);
+ } else {
+ _print('FAILED with:\n $testOutput', label: testName);
}
+ }
- // Report results for this test's sources' diff validations.
- void reportDiffOutcome(Uri fileUri, String testOutput, bool testPassed) {
- final filePath = fileUri.path;
- final relativeFilePath = p.relative(filePath, from: allTestsUri.path);
- var outcome = TestResultOutcome(
- configuration: options.namedConfiguration,
- testName: '$relativeFilePath-diff',
- testOutput: testOutput,
- );
- outcome.elapsedTime = stopwatch.elapsed;
- outcome.matchedExpectations = testPassed;
- testOutcomes.add(outcome);
- if (testPassed) {
- _debugPrint('PASSED (diff on $filePath) with:\n $testOutput',
- label: testName);
- } else {
- _debugPrint('FAILED (diff on $filePath) with:\n $testOutput',
- label: testName);
- }
+ /// Report results for this test's sources' diff validations.
+ void reportDiffOutcome(
+ String testName, Uri fileUri, String testOutput, bool testPassed) {
+ stopwatch.stop();
+ final filePath = fileUri.path;
+ final relativeFilePath = p.relative(filePath, from: allTestsUri.path);
+ final outcome = TestResultOutcome(
+ configuration: options.namedConfiguration,
+ testName: '$relativeFilePath-diff',
+ testOutput: testOutput,
+ );
+ outcome.elapsedTime = stopwatch.elapsed;
+ outcome.matchedExpectations = testPassed;
+ testOutcomes.add(outcome);
+ if (testPassed) {
+ _debugPrint('PASSED (diff on $filePath) with:\n $testOutput',
+ label: testName);
+ } else {
+ _debugPrint('FAILED (diff on $filePath) with:\n $testOutput',
+ label: testName);
}
+ }
- final tempUri = generatedCodeUri.resolve('$testName/');
- Directory.fromUri(tempUri).createSync();
-
- _print('Generating test assets.', label: testName);
- _debugPrint('Emitting JS code to ${tempUri.toFilePath()}.',
- label: testName);
-
- var filesystem = HotReloadMemoryFilesystem(tempUri);
-
+ /// Performs the desired diff checks for [test] and reports the results.
+ void diffCheck(HotReloadTest test) {
var diffMode = options.diffMode;
if (fe_shared.isWindows && diffMode != DiffMode.ignore) {
_print("Diffing isn't supported on Windows. Defaulting to 'ignore'.",
- label: testName);
+ label: test.name);
diffMode = DiffMode.ignore;
}
switch (diffMode) {
case DiffMode.check:
- _print('Checking source file diffs.', label: testName);
+ _print('Checking source file diffs.', label: test.name);
for (final file in test.files) {
_debugPrint('Checking source file diffs for $file.',
label: test.name);
@@ -479,10 +532,10 @@
_splitTestByDiff(currentEdit.fileUri);
var diffCount = testDiffSeparator.allMatches(currentDiff).length;
if (diffCount == 0) {
- reportDiffOutcome(currentEdit.fileUri,
+ reportDiffOutcome(test.name, currentEdit.fileUri,
'First generation does not have a diff', true);
} else {
- reportDiffOutcome(currentEdit.fileUri,
+ reportDiffOutcome(test.name, currentEdit.fileUri,
'First generation should not have any diffs', false);
}
while (edits.moveNext()) {
@@ -493,11 +546,12 @@
// Check that exactly one diff exists.
diffCount = testDiffSeparator.allMatches(currentDiff).length;
if (diffCount == 0) {
- reportDiffOutcome(currentEdit.fileUri,
+ reportDiffOutcome(test.name, currentEdit.fileUri,
'No diff found for ${currentEdit.fileUri}', false);
continue;
} else if (diffCount > 1) {
reportDiffOutcome(
+ test.name,
currentEdit.fileUri,
'Too many diffs found for ${currentEdit.fileUri} '
'(expected 1)',
@@ -527,13 +581,14 @@
_filterLineDeltas(diffOutput, currentDiff);
if (filteredDiffOutput != filteredCurrentDiff) {
reportDiffOutcome(
+ test.name,
currentEdit.fileUri,
'Unexpected diff found for ${currentEdit.fileUri}:\n'
'-- Expected --\n$diffOutput\n'
'-- Actual --\n$currentDiff',
false);
} else {
- reportDiffOutcome(currentEdit.fileUri,
+ reportDiffOutcome(test.name, currentEdit.fileUri,
'Correct diff found for ${currentEdit.fileUri}', true);
}
}
@@ -577,7 +632,7 @@
_print('Writing updated diff to $currentEdit.fileUri',
label: test.name);
_debugPrint('Updated diff:\n$diffOutput', label: test.name);
- reportDiffOutcome(currentEdit.fileUri,
+ reportDiffOutcome(test.name, currentEdit.fileUri,
'diff updated for $currentEdit.fileUri', true);
}
}
@@ -588,462 +643,342 @@
var uri = edit.fileUri;
_debugPrint('Ignoring source file diffs for $uri.',
label: test.name);
- reportDiffOutcome(uri, 'Ignoring diff for $uri', true);
+ reportDiffOutcome(test.name, uri, 'Ignoring diff for $uri', true);
}
}
}
+ }
- // Skip this test directory if this platform is excluded.
- if (test.excludedPlatforms.contains(options.runtime)) {
- _print('Skipping test on platform: ${options.runtime.text}',
- label: testName);
- continue;
+ /// Copy all files in [test] for the given [generation] into the snapshot
+ /// directory and returns uris of all the files copied.
+ ///
+ /// The uris describe the copy destination in the form of the hot reload file
+ /// system scheme.
+ List<String> copyGenerationSources(HotReloadTest test, int generation) {
+ _debugPrint('Entering generation $generation', label: test.name);
+ final updatedFilesInCurrentGeneration = <String>[];
+ // Copy all files in this generation to the snapshot directory with their
+ // names restored (e.g., path/to/main' from 'path/to/main.0.dart).
+ // TODO(markzipan): support subdirectories.
+ _debugPrint(
+ 'Copying Dart files to snapshot directory: '
+ '${snapshotDir.uri.toFilePath()}',
+ label: test.name);
+ for (final file in test.filesEditedInGeneration(generation)) {
+ final fileSnapshotUri = snapshotDir.uri.resolve(file.baseName);
+ final editUri = file.editForGeneration(generation).fileUri;
+ File.fromUri(editUri).copySync(fileSnapshotUri.toFilePath());
+ final relativeSnapshotPath = fe_shared.relativizeUri(
+ snapshotDir.uri, fileSnapshotUri, fe_shared.isWindows);
+ final snapshotPathWithScheme =
+ '$filesystemScheme:///$relativeSnapshotPath';
+ updatedFilesInCurrentGeneration.add(snapshotPathWithScheme);
}
+ _print(
+ 'Updated files in generation $generation: '
+ '$updatedFilesInCurrentGeneration',
+ label: test.name);
+ return updatedFilesInCurrentGeneration;
+ }
- // TODO(markzipan): replace this with a test-configurable main entrypoint.
- final mainDartFilePath =
- test.directory.uri.resolve('main.dart').toFilePath();
- _debugPrint('Test entrypoint: $mainDartFilePath', label: testName);
- _print('Generating code over ${test.generationCount} generations.',
- label: testName);
-
+ /// Compile all [updatedFiles] in [test] for the given [generation] with the
+ /// front end server [controller] and copy outputs to [outputDirectory].
+ ///
+ /// Reports test failures on compile time errors.
+ // TODO(nshahan): Move to a DDC specific suite runner.
+ Future<bool> compileGeneration(
+ HotReloadTest test,
+ int generation,
+ Directory outputDirectory,
+ List<String> updatedFiles,
+ HotReloadFrontendServerController controller) async {
var hasCompileError = false;
- // Generate hot reload/restart generations as subdirectories in a loop.
- var currentGeneration = 0;
- while (currentGeneration < test.generationCount) {
- _debugPrint('Entering generation $currentGeneration', label: testName);
- var updatedFilesInCurrentGeneration = <String>[];
-
- // Copy all files in this generation to the snapshot directory with their
- // names restored (e.g., path/to/main' from 'path/to/main.0.dart).
- // TODO(markzipan): support subdirectories.
+ // The first generation calls `compile`, but subsequent ones call
+ // `recompile`.
+ // Likewise, use the incremental output directory for `recompile` calls.
+ String outputDillPath;
+ _print('Compiling generation $generation with the Frontend Server.',
+ label: test.name);
+ CompilerOutput compilerOutput;
+ if (generation == 0) {
_debugPrint(
- 'Copying Dart files to snapshot directory: '
- '${snapshotUri.toFilePath()}',
- label: testName);
- for (final file in test.filesEditedInGeneration(currentGeneration)) {
- final fileSnapshotUri = snapshotDir.uri.resolve(file.baseName);
- final editUri = file.editForGeneration(currentGeneration).fileUri;
- File.fromUri(editUri).copySync(fileSnapshotUri.toFilePath());
- final relativeSnapshotPath = fe_shared.relativizeUri(
- snapshotDir.uri, fileSnapshotUri, fe_shared.isWindows);
- final snapshotPathWithScheme =
- '$filesystemScheme:///$relativeSnapshotPath';
- updatedFilesInCurrentGeneration.add(snapshotPathWithScheme);
- }
- _print(
- 'Updated files in generation $currentGeneration: '
- '$updatedFilesInCurrentGeneration',
+ 'Compiling snapshot entrypoint: $snapshotEntrypointWithScheme',
label: test.name);
-
- // The first generation calls `compile`, but subsequent ones call
- // `recompile`.
- // Likewise, use the incremental output directory for `recompile` calls.
- String outputDirectoryPath;
- _print(
- 'Compiling generation $currentGeneration with the Frontend Server.',
- label: testName);
- CompilerOutput compilerOutput;
- if (currentGeneration == 0) {
- _debugPrint(
- 'Compiling snapshot entrypoint: $snapshotEntrypointWithScheme',
- label: testName);
- outputDirectoryPath = outputDillUri.toFilePath();
- compilerOutput =
- await controller.sendCompile(snapshotEntrypointWithScheme);
- } else {
- _debugPrint(
- 'Recompiling snapshot entrypoint: $snapshotEntrypointWithScheme',
- label: testName);
- outputDirectoryPath = outputIncrementalDillUri.toFilePath();
- // TODO(markzipan): Add logic to reject bad compiles.
- compilerOutput = await controller.sendRecompile(
- snapshotEntrypointWithScheme,
- invalidatedFiles: updatedFilesInCurrentGeneration);
- }
- // Frontend Server reported compile errors. Fail if they weren't
- // expected, and do not run tests.
- if (compilerOutput.errorCount > 0) {
- hasCompileError = true;
- await controller.sendReject();
- // TODO(markzipan): Determine if 'contains' is good enough to determine
- // compilation error correctness.
- if (test.expectedError != null &&
- compilerOutput.outputText.contains(test.expectedError!)) {
- await reportTestOutcome(
- 'Expected error found during compilation: '
- '${test.expectedError}',
- true);
- } else {
- await reportTestOutcome(
- 'Test failed with compile error: ${compilerOutput.outputText}',
- false);
- }
- } else {
- controller.sendAccept();
- }
-
- // Stop processing further generations if compilation failed.
- if (hasCompileError) break;
-
+ outputDillPath = outputDillUri.toFilePath();
+ compilerOutput =
+ await controller.sendCompile(snapshotEntrypointWithScheme);
+ } else {
_debugPrint(
- 'Frontend Server successfully compiled outputs to: '
- '$outputDirectoryPath',
- label: testName);
-
- if (options.runtime.emitsJS) {
- // Update the memory filesystem with the newly-created JS files
- _print(
- 'Loading generation $currentGeneration files '
- 'into the memory filesystem.',
- label: testName);
- final codeFile = File('$outputDirectoryPath.sources');
- final manifestFile = File('$outputDirectoryPath.json');
- final sourcemapFile = File('$outputDirectoryPath.map');
- filesystem.update(
- codeFile,
- manifestFile,
- sourcemapFile,
- generation: '$currentGeneration',
- );
-
- // Write JS files and sourcemaps to their respective generation.
- _print('Writing generation $currentGeneration assets.',
- label: testName);
- _debugPrint('Writing JS assets to ${tempUri.toFilePath()}',
- label: testName);
- filesystem.writeToDisk(tempUri, generation: '$currentGeneration');
+ 'Recompiling snapshot entrypoint: $snapshotEntrypointWithScheme',
+ label: test.name);
+ outputDillPath = outputIncrementalDillUri.toFilePath();
+ // TODO(markzipan): Add logic to reject bad compiles.
+ compilerOutput = await controller.sendRecompile(
+ snapshotEntrypointWithScheme,
+ invalidatedFiles: updatedFiles);
+ }
+ // Frontend Server reported compile errors. Fail if they weren't
+ // expected, and do not run tests.
+ if (compilerOutput.errorCount > 0) {
+ hasCompileError = true;
+ await controller.sendReject();
+ // TODO(markzipan): Determine if 'contains' is good enough to determine
+ // compilation error correctness.
+ if (test.expectedError != null &&
+ compilerOutput.outputText.contains(test.expectedError!)) {
+ await reportTestOutcome(
+ test.name,
+ 'Expected error found during compilation: '
+ '${test.expectedError}',
+ true);
} else {
- final dillOutputDir =
- Directory.fromUri(tempUri.resolve('generation$currentGeneration'));
- dillOutputDir.createSync();
- final dillOutputUri = dillOutputDir.uri.resolve('$testName.dill');
- File(outputDirectoryPath).copySync(dillOutputUri.toFilePath());
- // Write dills their respective generation.
- _print('Writing generation $currentGeneration assets.',
- label: testName);
- _debugPrint('Writing dill to ${dillOutputUri.toFilePath()}',
- label: testName);
+ await reportTestOutcome(
+ test.name,
+ 'Test failed with compile error: ${compilerOutput.outputText}',
+ false);
}
- currentGeneration++;
+ } else {
+ controller.sendAccept();
}
- // Skip to the next test and avoid execution if we encountered a
- // compilation error.
- if (hasCompileError) {
- _print('Did not emit all assets due to compilation error.',
- label: testName);
- continue;
- }
+ // Stop processing further generations if compilation failed.
+ if (hasCompileError) return false;
- _print('Finished emitting assets.', label: testName);
+ _debugPrint(
+ 'Frontend Server successfully compiled outputs to: '
+ '$outputDillPath',
+ label: test.name);
+ _debugPrint('Emitting JS code to ${outputDirectory.path}.',
+ label: test.name);
+ // Update the memory filesystem with the newly-created JS files.
+ _print('Loading generation $generation files into the memory filesystem.',
+ label: test.name);
+ final codeFile = File('$outputDillPath.sources');
+ final manifestFile = File('$outputDillPath.json');
+ final sourcemapFile = File('$outputDillPath.map');
+ filesystem!.update(codeFile, manifestFile, sourcemapFile,
+ generation: '$generation');
- final testOutputStreamController = StreamController<List<int>>();
- final testOutputBuffer = StringBuffer();
- testOutputStreamController.stream
- .transform(utf8.decoder)
- .listen(testOutputBuffer.write);
+ // Write JS files and sourcemaps to their respective generation.
+ _print('Writing generation $generation assets.', label: test.name);
+ _debugPrint('Writing JS assets to ${outputDirectory.path}',
+ label: test.name);
+ filesystem!.writeToDisk(outputDirectory.uri, generation: '$generation');
+ return true;
+ }
+
+ // TODO(nshahan): Refactor into runtime specific implementations.
+ Future<bool> runTest(
+ HotReloadTest test, Directory tempDirectory, IOSink outputSink) async {
var testPassed = false;
switch (options.runtime) {
case RuntimePlatforms.d8:
// Run the compiled JS generations with D8.
- _print('Creating D8 hot reload test suite.', label: testName);
- final d8Config = ddc_helpers.D8Configuration(sdkRoot);
- final d8Suite = D8SuiteRunner(
- config: d8Config,
- bootstrapJsUri: tempUri.resolve('generation0/bootstrap.js'),
- entrypointLibraryExportName:
- ddc_names.libraryUriToJsIdentifier(snapshotEntrypointUri),
- dartSdkJsUri: soundStableDartSdkJsUri,
- ddcModuleLoaderJsUri: ddcModuleLoaderJsUri,
- outputSink: IOSink(testOutputStreamController.sink),
- );
+ _print('Creating D8 hot reload test suite.', label: test.name);
+ // TODO(nshahan): Clean this up! The cast here just serves as a way to
+ // allow for smaller refactor changes.
+ final d8Suite = (this as D8SuiteRunner)
+ ..bootstrapJsUri =
+ tempDirectory.uri.resolve('generation0/bootstrap.js')
+ ..outputSink = outputSink;
await d8Suite.setupTest(
- testName: testName,
- scriptDescriptors: filesystem.scriptDescriptorForBootstrap,
- generationToModifiedFiles: filesystem.generationsToModifiedFilePaths,
+ testName: test.name,
+ scriptDescriptors: filesystem!.scriptDescriptorForBootstrap,
+ generationToModifiedFiles: filesystem!.generationsToModifiedFilePaths,
);
- final d8ExitCode = await d8Suite.runTest(testName: testName);
+ final d8ExitCode = await d8Suite.runTestOld(testName: test.name);
testPassed = d8ExitCode == 0;
- await d8Suite.teardownTest(testName: testName);
case RuntimePlatforms.chrome:
// Run the compiled JS generations with Chrome.
- _print('Creating Chrome hot reload test suite.', label: testName);
- final chromeConfig = ddc_helpers.ChromeConfiguration(sdkRoot);
- final suite = ChromeSuiteRunner(
- config: chromeConfig,
- mainEntrypointJsUri:
- tempUri.resolve('generation0/main_module.bootstrap.js'),
- bootstrapJsUri: tempUri.resolve('generation0/bootstrap.js'),
- bootstrapHtmlUri: tempUri.resolve('generation0/index.html'),
- entrypointLibraryExportName:
- ddc_names.libraryUriToJsIdentifier(snapshotEntrypointUri),
- dartSdkJsUri: soundStableDartSdkJsUri,
- ddcModuleLoaderJsUri: ddcModuleLoaderJsUri,
- outputSink: IOSink(testOutputStreamController.sink),
- );
+ _print('Creating Chrome hot reload test suite.', label: test.name);
+ // TODO(nshahan): Clean this up! The cast here just serves as a way to
+ // allow for smaller refactor changes.
+ final suite = (this as ChromeSuiteRunner)
+ ..mainEntrypointJsUri =
+ tempDirectory.uri.resolve('generation0/main_module.bootstrap.js')
+ ..bootstrapJsUri =
+ tempDirectory.uri.resolve('generation0/bootstrap.js')
+ ..bootstrapHtmlUri =
+ tempDirectory.uri.resolve('generation0/index.html')
+ ..outputSink = outputSink;
await suite.setupTest(
- testName: testName,
- scriptDescriptors: filesystem.scriptDescriptorForBootstrap,
- generationToModifiedFiles: filesystem.generationsToModifiedFilePaths,
+ testName: test.name,
+ scriptDescriptors: filesystem!.scriptDescriptorForBootstrap,
+ generationToModifiedFiles: filesystem!.generationsToModifiedFilePaths,
);
- final exitCode = await suite.runTest(testName: testName);
+ final exitCode = await suite.runTestOld(testName: test.name);
testPassed = exitCode == 0;
- await suite.teardownTest(testName: testName);
case RuntimePlatforms.vm:
- final firstGenerationDillUri =
- tempUri.resolve('generation0/$testName.dill');
- // Start the VM at generation 0.
- final vmArgs = [
- '--enable-vm-service=0', // 0 avoids port collisions.
- '--disable-service-auth-codes',
- '--disable-dart-dev',
- firstGenerationDillUri.toFilePath(),
- ];
- final vm = await Process.start(Platform.executable, vmArgs);
- _debugPrint(
- 'Starting VM with command: '
- '${Platform.executable} ${vmArgs.join(" ")}',
- label: testName);
- vm.stdout
- .transform(utf8.decoder)
- .transform(LineSplitter())
- .listen((String line) {
- _debugPrint('VM stdout: $line', label: testName);
- testOutputBuffer.writeln(line);
- });
- vm.stderr
- .transform(utf8.decoder)
- .transform(LineSplitter())
- .listen((String err) {
- _debugPrint('VM stderr: $err', label: testName);
- testOutputBuffer.writeln(err);
- });
- _print('Executing VM test.', label: testName);
- final vmExitCode = await vm.exitCode
- .timeout(Duration(seconds: testTimeoutSeconds), onTimeout: () {
- final timeoutText =
- 'Test timed out after $testTimeoutSeconds seconds.';
- _print(timeoutText, label: testName);
- testOutputBuffer.writeln(timeoutText);
- vm.kill();
- return 1;
- });
- testPassed = vmExitCode == 0;
+ throw UnsupportedError('Now implemented in VMSuiteRunner.');
}
- await reportTestOutcome(testOutputBuffer.toString(), testPassed);
+ return testPassed;
}
- await shutdown();
- _print('Testing complete.');
+ /// Reports test results to standard out as well as the output .json file if
+ /// requested.
+ Future<void> reportAllResults() async {
+ if (options.testResultsOutputDir != null) {
+ // Used to communicate individual test failures to our test bots.
+ final testOutcomeResults = testOutcomes.map((o) => o.toRecordJson());
+ final testOutcomeLogs = testOutcomes.map((o) => o.toLogJson());
+ final testResultsOutputDir = options.testResultsOutputDir!;
+ _print('Saving test results to ${testResultsOutputDir.toFilePath()}.');
- if (emitTestResultsJson) {
- final testOutcomeResults = testOutcomes.map((o) => o.toRecordJson());
- final testOutcomeLogs = testOutcomes.map((o) => o.toLogJson());
- final testResultsOutputDir = options.testResultsOutputDir!;
- _print('Saving test results to ${testResultsOutputDir.toFilePath()}.');
+ // Test outputs must have one JSON blob per line and be
+ // newline-terminated.
+ final testResultsUri = testResultsOutputDir.resolve('results.json');
+ final testResultsSink = File.fromUri(testResultsUri).openWrite();
+ testOutcomeResults.forEach(testResultsSink.writeln);
+ await testResultsSink.flush();
+ await testResultsSink.close();
- // Test outputs must have one JSON blob per line and be newline-terminated.
- final testResultsUri = testResultsOutputDir.resolve('results.json');
- final testResultsSink = File.fromUri(testResultsUri).openWrite();
- testOutcomeResults.forEach(testResultsSink.writeln);
- await testResultsSink.flush();
- await testResultsSink.close();
-
- final testLogsUri = testResultsOutputDir.resolve('logs.json');
- if (Platform.isWindows) {
- // TODO(55297): Logs are disabled on windows until this but is fixed.
- _print('Logs are not written on Windows. '
- 'See: https://github.com/dart-lang/sdk/issues/55297');
- } else {
- final testLogsSink = File.fromUri(testLogsUri).openWrite();
- testOutcomeLogs.forEach(testLogsSink.writeln);
- await testLogsSink.flush();
- await testLogsSink.close();
+ final testLogsUri = testResultsOutputDir.resolve('logs.json');
+ if (Platform.isWindows) {
+ // TODO(55297): Logs are disabled on windows until this but is fixed.
+ _print('Logs are not written on Windows. '
+ 'See: https://github.com/dart-lang/sdk/issues/55297');
+ } else {
+ final testLogsSink = File.fromUri(testLogsUri).openWrite();
+ testOutcomeLogs.forEach(testLogsSink.writeln);
+ await testLogsSink.flush();
+ await testLogsSink.close();
+ }
+ _print('Emitted logs to ${testResultsUri.toFilePath()} '
+ 'and ${testLogsUri.toFilePath()}.');
}
- _print('Emitted logs to ${testResultsUri.toFilePath()} '
- 'and ${testLogsUri.toFilePath()}.');
- }
-
- // Report failed tests.
- var failedTests =
- testOutcomes.where((outcome) => !outcome.matchedExpectations);
- if (failedTests.isNotEmpty) {
- print('Some tests failed:');
- failedTests.forEach((outcome) {
- print('${outcome.testName} failed with:\n ${outcome.testOutput}');
- });
- // Exit cleanly after writing test results.
- exit(0);
- }
-}
-
-/// Runs the [command] with [args] in [environment].
-///
-/// Will echo the commands to the console before running them when running in
-/// `verbose` mode.
-Future<Process> startProcess(String name, String command, List<String> args,
- {Map<String, String> environment = const {},
- ProcessStartMode mode = ProcessStartMode.normal}) {
- if (verbose) {
- print('Running $name:\n$command ${args.join(' ')}\n');
- if (environment.isNotEmpty) {
- var environmentVariables =
- environment.entries.map((e) => '${e.key}: ${e.value}').join('\n');
- print('With environment:\n$environmentVariables\n');
+ if (testOutcomes.isEmpty) {
+ print('No tests ran: no sub-directories in ${allTestsUri.toFilePath()} '
+ 'match the provided filter:\n'
+ '${options.testNameFilter}');
+ exit(0);
+ }
+ // Report failed tests.
+ var failedTests =
+ testOutcomes.where((outcome) => !outcome.matchedExpectations);
+ if (failedTests.isNotEmpty) {
+ print('Some tests failed:');
+ failedTests.forEach((outcome) {
+ print('${outcome.testName} failed with:\n ${outcome.testOutput}');
+ });
+ // Exit cleanly after writing test results.
+ exit(0);
}
}
- return Process.start(command, args, mode: mode, environment: environment);
-}
-/// Prints messages if 'verbose' mode is enabled.
-void _print(String message, {String? label}) {
- if (verbose) {
- final labelText = label == null ? '' : '($label)';
- print('hot_reload_test$labelText: $message');
- }
-}
-
-/// Prints messages if 'debug' mode is enabled.
-void _debugPrint(String message, {String? label}) {
- if (debug) {
- final labelText = label == null ? '' : '($label)';
- print('DEBUG$labelText: $message');
- }
-}
-
-/// Returns the diff'd output between two files.
-///
-/// These diffs are appended at the end of updated file generations for better
-/// test readability.
-///
-/// If [commented] is set, the output will be wrapped in multiline comments
-/// and the diff separator.
-///
-/// If [trimHeaders] is set, the leading '+++' and '---' file headers will be
-/// removed.
-String _diffWithFileUris(Uri file1, Uri file2,
- {String label = '', bool commented = true, bool trimHeaders = true}) {
- final file1Path = file1.toFilePath();
- final file2Path = file2.toFilePath();
- final diffArgs = ['-u', '--width=120', '--expand-tabs', file1Path, file2Path];
- _debugPrint("Running diff with 'diff ${diffArgs.join(' ')}'.", label: label);
- final diffProcess = Process.runSync('diff', diffArgs);
- final errOutput = diffProcess.stderr as String;
- if (errOutput.isNotEmpty) {
- throw Exception('diff failed with:\n$errOutput');
- }
- var output = diffProcess.stdout as String;
- if (trimHeaders) {
- // Skip the first two lines.
- // TODO(markzipan): Add support for Windows-style line endings.
- output = output.split('\n').skip(2).join('\n');
- }
- return commented ? '$testDiffSeparator\n/*\n$output*/' : output;
-}
-
-/// Removes diff lines that show added or removed newlines.
-///
-/// 'diff' can be unstable across platforms around newline offsets.
-(String, String) _filterLineDeltas(String diff1, String diff2) {
- bool isBlankLineOrDelta(String s) {
- return s.trim().isEmpty ||
- (s.startsWith('+') || s.startsWith('-')) && s.trim().length == 1;
- }
-
- var diff1Lines = LineSplitter().convert(diff1)
- ..removeWhere(isBlankLineOrDelta);
- var diff2Lines = LineSplitter().convert(diff2)
- ..removeWhere(isBlankLineOrDelta);
- return (diff1Lines.join('\n'), diff2Lines.join('\n'));
-}
-
-/// Returns the code and diff portions of [file].
-(String, String) _splitTestByDiff(Uri file) {
- final text = File.fromUri(file).readAsStringSync();
- final diffIndex = text.indexOf(testDiffSeparator);
- final diffSplitIndex = diffIndex == -1 ? text.length - 1 : diffIndex;
- final codeText = text.substring(0, diffSplitIndex);
- final diffText = text.substring(diffSplitIndex, text.length - 1);
- // Avoid 'No newline at end of file' messages in the output by appending a
- // newline if one is not already trailing.
- return ('$codeText${codeText.endsWith('\n') ? '' : '\n'}', diffText);
-}
-
-abstract class HotReloadSuiteRunner {
- final String entrypointModuleName;
- final String entrypointLibraryExportName;
- final Uri dartSdkJsUri;
- final Uri ddcModuleLoaderJsUri;
- final StreamSink<List<int>> outputSink;
-
- HotReloadSuiteRunner({
- required this.entrypointModuleName,
- required this.entrypointLibraryExportName,
- required this.dartSdkJsUri,
- required this.ddcModuleLoaderJsUri,
- required this.outputSink,
- });
-
- /// Logic that needs to be run before every test begins.
+ /// Runs the [command] with [args] in [environment].
///
- /// [scriptDescriptors] and [generationToModifiedFiles] are only used for
- /// DDC-based execution environments.
- Future<void> setupTest(
- {String? testName,
- List<Map<String, String?>>? scriptDescriptors,
- ddc_helpers.FileDataPerGeneration? generationToModifiedFiles});
+ /// Will echo the commands to the console before running them when running in
+ /// `verbose` mode.
+ Future<Process> startProcess(String name, String command, List<String> args,
+ {Map<String, String> environment = const {},
+ ProcessStartMode mode = ProcessStartMode.normal}) {
+ if (options.verbose) {
+ print('Running $name:\n$command ${args.join(' ')}\n');
+ if (environment.isNotEmpty) {
+ var environmentVariables =
+ environment.entries.map((e) => '${e.key}: ${e.value}').join('\n');
+ print('With environment:\n$environmentVariables\n');
+ }
+ }
+ return Process.start(command, args, mode: mode, environment: environment);
+ }
- /// Executes a test.
- Future<int> runTest({String? testName});
+ /// Prints messages if 'verbose' mode is enabled.
+ void _print(String message, {String? label}) {
+ if (options.verbose) {
+ final labelText = label == null ? '' : '($label)';
+ print('hot_reload_test$labelText: $message');
+ }
+ }
- /// Logic that needs to be run after every test completes.
- Future<void> teardownTest({String? testName});
+ /// Prints messages if 'debug' mode is enabled.
+ void _debugPrint(String message, {String? label}) {
+ if (options.debug) {
+ final labelText = label == null ? '' : '($label)';
+ print('DEBUG$labelText: $message');
+ }
+ }
+
+ /// Returns the diff'd output between two files.
+ ///
+ /// These diffs are appended at the end of updated file generations for better
+ /// test readability.
+ ///
+ /// If [commented] is set, the output will be wrapped in multiline comments
+ /// and the diff separator.
+ ///
+ /// If [trimHeaders] is set, the leading '+++' and '---' file headers will be
+ /// removed.
+ String _diffWithFileUris(Uri file1, Uri file2,
+ {String label = '', bool commented = true, bool trimHeaders = true}) {
+ final file1Path = file1.toFilePath();
+ final file2Path = file2.toFilePath();
+ final diffArgs = [
+ '-u',
+ '--width=120',
+ '--expand-tabs',
+ file1Path,
+ file2Path
+ ];
+ _debugPrint("Running diff with 'diff ${diffArgs.join(' ')}'.",
+ label: label);
+ final diffProcess = Process.runSync('diff', diffArgs);
+ final errOutput = diffProcess.stderr as String;
+ if (errOutput.isNotEmpty) {
+ throw Exception('diff failed with:\n$errOutput');
+ }
+ var output = diffProcess.stdout as String;
+ if (trimHeaders) {
+ // Skip the first two lines.
+ // TODO(markzipan): Add support for Windows-style line endings.
+ output = output.split('\n').skip(2).join('\n');
+ }
+ return commented ? '$testDiffSeparator\n/*\n$output*/' : output;
+ }
+
+ /// Removes diff lines that show added or removed newlines.
+ ///
+ /// 'diff' can be unstable across platforms around newline offsets.
+ (String, String) _filterLineDeltas(String diff1, String diff2) {
+ bool isBlankLineOrDelta(String s) {
+ return s.trim().isEmpty ||
+ (s.startsWith('+') || s.startsWith('-')) && s.trim().length == 1;
+ }
+
+ var diff1Lines = LineSplitter().convert(diff1)
+ ..removeWhere(isBlankLineOrDelta);
+ var diff2Lines = LineSplitter().convert(diff2)
+ ..removeWhere(isBlankLineOrDelta);
+ return (diff1Lines.join('\n'), diff2Lines.join('\n'));
+ }
+
+ /// Returns the code and diff portions of [file].
+ (String, String) _splitTestByDiff(Uri file) {
+ final text = File.fromUri(file).readAsStringSync();
+ final diffIndex = text.indexOf(testDiffSeparator);
+ final diffSplitIndex = diffIndex == -1 ? text.length - 1 : diffIndex;
+ final codeText = text.substring(0, diffSplitIndex);
+ final diffText = text.substring(diffSplitIndex, text.length - 1);
+ // Avoid 'No newline at end of file' messages in the output by appending a
+ // newline if one is not already trailing.
+ return ('$codeText${codeText.endsWith('\n') ? '' : '\n'}', diffText);
+ }
}
-class D8SuiteRunner implements HotReloadSuiteRunner {
- final ddc_helpers.D8Configuration config;
- final Uri bootstrapJsUri;
- @override
- final String entrypointModuleName;
- @override
- final String entrypointLibraryExportName;
- @override
- final Uri dartSdkJsUri;
- @override
- final Uri ddcModuleLoaderJsUri;
- @override
- final StreamSink<List<int>> outputSink;
+class D8SuiteRunner extends HotReloadSuiteRunner {
+ final ddc_helpers.D8Configuration config =
+ ddc_helpers.D8Configuration(sdkRoot);
+ late Uri bootstrapJsUri;
+ final String entrypointModuleName = 'hot-reload-test:///main.dart';
+ late final String entrypointLibraryExportName =
+ ddc_names.libraryUriToJsIdentifier(snapshotEntrypointUri);
+ final Uri dartSdkJsUri =
+ buildRootUri.resolve('gen/utils/ddc/canary/sdk/ddc/dart_sdk.js');
+ final Uri ddcModuleLoaderJsUri =
+ sdkRoot.resolve('pkg/dev_compiler/lib/js/ddc/ddc_module_loader.js');
+ late StreamSink<List<int>> outputSink;
- D8SuiteRunner._({
- required this.config,
- required this.bootstrapJsUri,
- required this.entrypointModuleName,
- required this.entrypointLibraryExportName,
- required this.dartSdkJsUri,
- required this.ddcModuleLoaderJsUri,
- required this.outputSink,
- });
-
- factory D8SuiteRunner({
- required ddc_helpers.D8Configuration config,
- required Uri bootstrapJsUri,
- String entrypointModuleName = 'hot-reload-test:///main.dart',
- String entrypointLibraryExportName = 'main',
- required Uri dartSdkJsUri,
- required Uri ddcModuleLoaderJsUri,
- StreamSink<List<int>>? outputSink,
- }) {
- return D8SuiteRunner._(
- config: config,
- entrypointModuleName: entrypointModuleName,
- entrypointLibraryExportName: entrypointLibraryExportName,
- bootstrapJsUri: bootstrapJsUri,
- dartSdkJsUri: dartSdkJsUri,
- ddcModuleLoaderJsUri: ddcModuleLoaderJsUri,
- outputSink: outputSink ?? stdout,
- );
- }
+ D8SuiteRunner(super.options);
String _generateBootstrapper({
required List<Map<String, String?>> scriptDescriptors,
@@ -1059,7 +994,6 @@
);
}
- @override
Future<void> setupTest({
String? testName,
List<Map<String, String?>>? scriptDescriptors,
@@ -1077,8 +1011,7 @@
_debugPrint('Writing D8 bootstrapper: $bootstrapJsUri', label: testName);
}
- @override
- Future<int> runTest({String? testName}) async {
+ Future<int> runTestOld({String? testName}) async {
final process = await startProcess('D8', config.binary.toFilePath(), [
config.sealNativeObjectScript.toFilePath(),
config.preamblesScript.toFilePath(),
@@ -1087,62 +1020,24 @@
unawaited(process.stdout.pipe(outputSink));
return process.exitCode;
}
-
- @override
- Future<void> teardownTest({String? testName}) async {}
}
-class ChromeSuiteRunner implements HotReloadSuiteRunner {
- final ddc_helpers.ChromeConfiguration config;
- final Uri bootstrapJsUri;
- final Uri mainEntrypointJsUri;
- final Uri bootstrapHtmlUri;
- @override
- final String entrypointModuleName;
- @override
- final String entrypointLibraryExportName;
- @override
- final Uri dartSdkJsUri;
- @override
- final Uri ddcModuleLoaderJsUri;
- @override
- final StreamSink<List<int>> outputSink;
+class ChromeSuiteRunner extends HotReloadSuiteRunner {
+ final ddc_helpers.ChromeConfiguration config =
+ ddc_helpers.ChromeConfiguration(sdkRoot);
+ late Uri bootstrapJsUri;
+ late Uri mainEntrypointJsUri;
+ late Uri bootstrapHtmlUri;
+ final String entrypointModuleName = 'hot-reload-test:///main.dart';
+ late final String entrypointLibraryExportName =
+ ddc_names.libraryUriToJsIdentifier(snapshotEntrypointUri);
+ final Uri dartSdkJsUri =
+ buildRootUri.resolve('gen/utils/ddc/canary/sdk/ddc/dart_sdk.js');
+ final Uri ddcModuleLoaderJsUri =
+ sdkRoot.resolve('pkg/dev_compiler/lib/js/ddc/ddc_module_loader.js');
+ late StreamSink<List<int>> outputSink;
- ChromeSuiteRunner._({
- required this.config,
- required this.mainEntrypointJsUri,
- required this.bootstrapJsUri,
- required this.bootstrapHtmlUri,
- required this.entrypointModuleName,
- required this.entrypointLibraryExportName,
- required this.dartSdkJsUri,
- required this.ddcModuleLoaderJsUri,
- required this.outputSink,
- });
-
- factory ChromeSuiteRunner({
- required ddc_helpers.ChromeConfiguration config,
- required Uri mainEntrypointJsUri,
- required Uri bootstrapJsUri,
- required Uri bootstrapHtmlUri,
- String entrypointModuleName = 'hot-reload-test:///main.dart',
- String entrypointLibraryExportName = 'main',
- required Uri dartSdkJsUri,
- required Uri ddcModuleLoaderJsUri,
- StreamSink<List<int>>? outputSink,
- }) {
- return ChromeSuiteRunner._(
- config: config,
- entrypointModuleName: entrypointModuleName,
- entrypointLibraryExportName: entrypointLibraryExportName,
- mainEntrypointJsUri: mainEntrypointJsUri,
- bootstrapJsUri: bootstrapJsUri,
- bootstrapHtmlUri: bootstrapHtmlUri,
- dartSdkJsUri: dartSdkJsUri,
- ddcModuleLoaderJsUri: ddcModuleLoaderJsUri,
- outputSink: outputSink ?? stdout,
- );
- }
+ ChromeSuiteRunner(super.options);
/// Generates all files required for bootstrapping a DDC project in Chrome.
void _generateBootstrapper({
@@ -1168,8 +1063,8 @@
mainModuleEntrypointJsPath:
escapedString(mainEntrypointJsUri.toFilePath()),
entrypointLibraryExportName: escapedString(entrypointLibraryExportName),
- scriptDescriptors: scriptDescriptors,
- modifiedFilesPerGeneration: generationToModifiedFiles,
+ scriptDescriptors: filesystem!.scriptDescriptorForBootstrap,
+ modifiedFilesPerGeneration: filesystem!.generationsToModifiedFilePaths,
);
File.fromUri(mainEntrypointJsUri).writeAsStringSync(chromeMainEntrypointJS);
@@ -1177,7 +1072,6 @@
File.fromUri(bootstrapHtmlUri).writeAsStringSync(bootstrapHtml);
}
- @override
Future<void> setupTest({
String? testName,
List<Map<String, String?>>? scriptDescriptors,
@@ -1195,8 +1089,7 @@
label: testName);
}
- @override
- Future<int> runTest({String? testName}) async {
+ Future<int> runTestOld({String? testName}) async {
// TODO(markzipan): Chrome tests are currently only configured for
// debugging a single test instance. This is due to:
// 1) Our tests not capturing test success/failure signals. These must be
@@ -1254,5 +1147,147 @@
}
@override
- Future<void> teardownTest({String? testName}) async {}
+ Future<void> runSuite(Options options) async {
+ // Only allow Chrome when debugging a single test.
+ // TODO(markzipan): Add support for full Chrome testing.
+ if (options.runtime == RuntimePlatforms.chrome) {
+ var matchingTests =
+ Directory.fromUri(allTestsUri).listSync().where((testDir) {
+ if (testDir is! Directory) return false;
+ final testDirParts = testDir.uri.pathSegments;
+ final testName = testDirParts[testDirParts.length - 2];
+ return options.testNameFilter.hasMatch(testName);
+ });
+
+ if (matchingTests.length > 1) {
+ throw Exception('Chrome is only supported when debugging a single test.'
+ "Please filter on a single test with '-f'.");
+ }
+ }
+ await super.runSuite(options);
+ }
+}
+
+/// Hot reload test suite runner for behavior specific to the VM.
+class VMSuiteRunner extends HotReloadSuiteRunner {
+ VMSuiteRunner(super.options);
+
+ @override
+ Future<bool> compileGeneration(
+ HotReloadTest test,
+ int generation,
+ Directory outputDirectory,
+ List<String> updatedFiles,
+ HotReloadFrontendServerController controller) async {
+ // The first generation calls `compile`, but subsequent ones call
+ // `recompile`.
+ // Likewise, use the incremental output directory for `recompile` calls.
+ // TODO(nshahan): Sending compile/recompile instructions is likely
+ // the same across backends and should be shared code.
+ String outputDillPath;
+ _print('Compiling generation $generation with the Frontend Server.',
+ label: test.name);
+ CompilerOutput compilerOutput;
+ if (generation == 0) {
+ _debugPrint(
+ 'Compiling snapshot entrypoint: $snapshotEntrypointWithScheme',
+ label: test.name);
+ outputDillPath = outputDillUri.toFilePath();
+ compilerOutput =
+ await controller.sendCompile(snapshotEntrypointWithScheme);
+ } else {
+ _debugPrint(
+ 'Recompiling snapshot entrypoint: $snapshotEntrypointWithScheme',
+ label: test.name);
+ outputDillPath = outputIncrementalDillUri.toFilePath();
+ // TODO(markzipan): Add logic to reject bad compiles.
+ compilerOutput = await controller.sendRecompile(
+ snapshotEntrypointWithScheme,
+ invalidatedFiles: updatedFiles);
+ }
+ var hasCompileError = false;
+ // Frontend Server reported compile errors. Fail if they weren't
+ // expected, and do not run tests.
+ if (compilerOutput.errorCount > 0) {
+ hasCompileError = true;
+ await controller.sendReject();
+ // TODO(markzipan): Determine if 'contains' is good enough to determine
+ // compilation error correctness.
+ if (test.expectedError != null &&
+ compilerOutput.outputText.contains(test.expectedError!)) {
+ await reportTestOutcome(
+ test.name,
+ 'Expected error found during compilation: '
+ '${test.expectedError}',
+ true);
+ } else {
+ await reportTestOutcome(
+ test.name,
+ 'Test failed with compile error: ${compilerOutput.outputText}',
+ false);
+ }
+ } else {
+ controller.sendAccept();
+ }
+ // Stop processing further generations if compilation failed.
+ if (hasCompileError) return false;
+ _debugPrint(
+ 'Frontend Server successfully compiled outputs to: '
+ '$outputDillPath',
+ label: test.name);
+ final dillOutputDir =
+ Directory.fromUri(outputDirectory.uri.resolve('generation$generation'));
+ dillOutputDir.createSync();
+ final dillOutputUri = dillOutputDir.uri.resolve('${test.name}.dill');
+ // Write dills their respective generation.
+ _print('Writing generation $generation assets.', label: test.name);
+ _debugPrint('Writing dill to ${dillOutputUri.toFilePath()}',
+ label: test.name);
+ File(outputDillPath).copySync(dillOutputUri.toFilePath());
+ return true;
+ }
+
+ @override
+ Future<bool> runTest(
+ HotReloadTest test, Directory tempDirectory, IOSink outputSink) async {
+ final firstGenerationDillUri =
+ tempDirectory.uri.resolve('generation0/${test.name}.dill');
+ // Start the VM at generation 0.
+ final vmArgs = [
+ '--enable-vm-service=0', // 0 avoids port collisions.
+ '--disable-service-auth-codes',
+ '--disable-dart-dev',
+ firstGenerationDillUri.toFilePath(),
+ ];
+ _debugPrint(
+ 'Starting VM with command: '
+ '${Platform.executable} ${vmArgs.join(" ")}',
+ label: test.name);
+ final vm = await Process.start(Platform.executable, vmArgs);
+ vm.stdout
+ .transform(utf8.decoder)
+ .transform(LineSplitter())
+ .listen((String line) {
+ _debugPrint('VM stdout: $line', label: test.name);
+ outputSink.writeln(line);
+ });
+ vm.stderr
+ .transform(utf8.decoder)
+ .transform(LineSplitter())
+ .listen((String err) {
+ _debugPrint('VM stderr: $err', label: test.name);
+ outputSink.writeln(err);
+ });
+ _print('Executing VM test.', label: test.name);
+ final testTimeoutSeconds = 10;
+ final vmExitCode = await vm.exitCode
+ .timeout(Duration(seconds: testTimeoutSeconds), onTimeout: () {
+ final timeoutText = 'Test timed out after $testTimeoutSeconds seconds.';
+ _print(timeoutText, label: test.name);
+ outputSink.writeln(timeoutText);
+ vm.kill();
+ return 1;
+ });
+ return vmExitCode == 0;
+ }
}
diff --git a/pkg/linter/messages.yaml b/pkg/linter/messages.yaml
index 7e7b5b8..f398131 100644
--- a/pkg/linter/messages.yaml
+++ b/pkg/linter/messages.yaml
@@ -13161,7 +13161,7 @@
by a `StatefulWidget` after an asynchronous gap without first checking the
`mounted` property.
- Storing a `BuildContext` for later use can lead to difficult to diagnose
+ Storing a `BuildContext` for later use can lead to difficult-to-diagnose
crashes. Asynchronous gaps implicitly store a `BuildContext`, making them
easy to overlook for diagnosis.
@@ -13213,7 +13213,7 @@
deprecatedDetails: |-
**DON'T** use `BuildContext` across asynchronous gaps.
- Storing `BuildContext` for later usage can easily lead to difficult to diagnose
+ Storing `BuildContext` for later usage can easily lead to difficult-to-diagnose
crashes. Asynchronous gaps are implicitly storing `BuildContext` and are some of
the easiest to overlook when writing code.
diff --git a/runtime/docs/gc.md b/runtime/docs/gc.md
index fa233c4..479d9b8 100644
--- a/runtime/docs/gc.md
+++ b/runtime/docs/gc.md
@@ -71,7 +71,7 @@
### Barrier
-With the mutator and marker running concurrently, the mutator could write a pointer to an object that has not been marked (TARGET) into an object that has already been marked and visited (SOURCE), leading to incorrect collection of TARGET. To prevent this, the write barrier checks if a store creates a pointer from an old-space object to an old-space object that is not marked, and marks the target object for such stores. We ignore pointers from new-space objects because we treat new-space objects as roots and will revisit them to finalize marking. We ignore the marking state of the source object to avoid expensive memory barriers required to ensure reordering of accesses to the header and slots can't lead skipped marking, and on the assumption that objects accessed during marking are likely to remain live when marking finishes.
+With the mutator and marker running concurrently, the mutator could write a pointer to an object that has not been marked (TARGET) into an object that has already been marked and visited (SOURCE), leading to incorrect collection of TARGET. To prevent this, the write barrier checks if a store creates a pointer to an object that is not marked, and marks the target object. We ignore the marking state of the source object to avoid expensive memory barriers required to ensure reordering of accesses to the header and slots can't lead skipped marking, and on the assumption that objects accessed during marking are likely to remain live when marking finishes.
The barrier is equivalent to
@@ -82,7 +82,7 @@
if (source->IsOldObject() && !source->IsRemembered() && target->IsNewObject()) {
source->SetRemembered();
AddToRememberedSet(source);
- } else if (source->IsOldObject() && target->IsOldObject() && !target->IsMarked() && Thread::Current()->IsMarking()) {
+ } else if (!target->IsMarked() && Thread::Current()->IsMarking()) {
if (target->TryAcquireMarkBit()) {
AddToMarkList(target);
}
@@ -150,7 +150,9 @@
For old-space objects created after marking started, the marker may see uninitialized values because operations on slots are not synchronized. To prevent this, during marking we allocate old-space objects [black (marked)](https://en.wikipedia.org/wiki/Tracing_garbage_collection#TRI-COLOR) so the marker will not visit them.
-New-space objects and roots are only visited during a safepoint, and safepoints establish synchronization.
+New-space objects inside an active TLAB and roots are only visited during a safepoint, and safepoints establish synchronization.
+
+New-space objects outside an active TLAB are synchronized by the store-release used to switch to the next TLAB.
When the mutator's mark block becomes full, it transferred to the marker by an acquire-release operation, so the marker will see the stores into the block.
@@ -167,8 +169,7 @@
The incremental marking write barrier, needed by the marker, checks if
-* `container` is old, and
-* `value` is old and not marked, and
+* `value` is not marked, and
* marking is in progress
When this occurs, we must insert `value` into the marking worklist.
@@ -178,7 +179,10 @@
* `value` is a constant. Constants are always old, and they will be marked via the constant pools even if we fail to mark them via `container`.
* `value` has the static type bool. All possible values of the bool type (null, false, true) are constants.
* `value` is known to be a Smi. Smis are not heap objects.
-* `container` is known to be a new object or known to be an old object that is in the remembered set and is marked if marking is in progress.
+* `value` is `container`. Self-references cannot cross generations or marking states.
+* `container` is known to be
+ * a new object or or an old object that is in the remembered set, AND
+ * a new object in the active TLAB or queued for re-scanning
We can know that `container` meets the last property if `container` is the result of an allocation (instead of a heap load), and there is no instruction that can trigger a GC between the allocation and the store. This is because the allocation stubs ensure the result of AllocateObject is either a new-space object (common case, bump pointer allocation succeeds), or has been preemptively added to the remembered set and marking worklist (uncommon case, entered runtime to allocate object, possibly triggering GC).
diff --git a/sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/records.dart b/sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/records.dart
index eb9227f..eb52f1e 100644
--- a/sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/records.dart
+++ b/sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/records.dart
@@ -10,7 +10,11 @@
final int positionals;
/// The names of the named elements in the record in alphabetical order.
- final List<String>? named;
+ ///
+ /// This is a JS Array of JS String literals. We avoid typing this explicitly
+ /// so it can remain unboxed, as it's only used for our internal
+ /// representation.
+ final List<dynamic /* String */ >? named;
Shape(this.positionals, this.named);
@@ -149,7 +153,7 @@
Shape registerShape(
@notNull String shapeKey,
@notNull int positionals,
- List<String>? named,
+ List<dynamic /* String */ >? named,
) {
var cached = JS<Shape?>('', '#.get(#)', shapes, shapeKey);
if (cached != null) {
@@ -169,7 +173,7 @@
Object registerRecord(
@notNull String shapeKey,
@notNull int positionals,
- List<String>? named,
+ List<dynamic /* String */ >? named,
) {
var cached = JS('', '#.get(#)', _records, shapeKey);
if (cached != null) {
@@ -245,7 +249,7 @@
Object recordLiteral(
@notNull String shapeKey,
@notNull int positionals,
- List<String>? named,
+ List<dynamic /* String */ >? named,
@notNull List values,
) {
var shape = registerShape(shapeKey, positionals, named);
diff --git a/tests/hot_reload/existing_field_changes_type_no_initializer/config.json b/tests/hot_reload/existing_field_changes_type_no_initializer/config.json
new file mode 100644
index 0000000..b72fb4b
--- /dev/null
+++ b/tests/hot_reload/existing_field_changes_type_no_initializer/config.json
@@ -0,0 +1,3 @@
+{
+ "exclude": []
+}
\ No newline at end of file
diff --git a/tests/hot_reload/existing_field_changes_type_no_initializer/main.0.dart b/tests/hot_reload/existing_field_changes_type_no_initializer/main.0.dart
new file mode 100644
index 0000000..2d8e991
--- /dev/null
+++ b/tests/hot_reload/existing_field_changes_type_no_initializer/main.0.dart
@@ -0,0 +1,27 @@
+// Copyright (c) 2024, 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.
+
+import 'package:expect/expect.dart';
+import 'package:reload_test/reload_test_utils.dart';
+
+class Foo {
+ int x;
+ Foo(this.x);
+}
+
+late Foo foo;
+
+helper() {
+ foo = Foo(42);
+}
+
+Future<void> main() async {
+ helper();
+ Expect.type<int>(foo.x);
+ Expect.equals(42, foo.x);
+
+ await hotReload();
+
+ Expect.throws<TypeError>(() => foo.x);
+}
diff --git a/tests/hot_reload/existing_field_changes_type_no_initializer/main.1.dart b/tests/hot_reload/existing_field_changes_type_no_initializer/main.1.dart
new file mode 100644
index 0000000..a83d2cd
--- /dev/null
+++ b/tests/hot_reload/existing_field_changes_type_no_initializer/main.1.dart
@@ -0,0 +1,46 @@
+// Copyright (c) 2024, 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.
+
+import 'package:expect/expect.dart';
+import 'package:reload_test/reload_test_utils.dart';
+
+class Foo {
+ String x;
+ Foo(this.x);
+}
+
+late Foo foo;
+
+helper() {}
+
+Future<void> main() async {
+ helper();
+ Expect.type<int>(foo.x);
+ Expect.equals(42, foo.x);
+
+ await hotReload();
+
+ Expect.throws<TypeError>(() => foo.x);
+}
+/** DIFF **/
+/*
+@@ -6,15 +6,13 @@
+ import 'package:reload_test/reload_test_utils.dart';
+
+ class Foo {
+- int x;
++ String x;
+ Foo(this.x);
+ }
+
+ late Foo foo;
+
+-helper() {
+- foo = Foo(42);
+-}
++helper() {}
+
+ Future<void> main() async {
+ helper();
+*/
diff --git a/tools/VERSION b/tools/VERSION
index 972a4fb..78c7634 100644
--- a/tools/VERSION
+++ b/tools/VERSION
@@ -27,5 +27,5 @@
MAJOR 3
MINOR 7
PATCH 0
-PRERELEASE 166
+PRERELEASE 167
PRERELEASE_PATCH 0