Skip to content

Commit 14734d5

Browse files
nshahanCommit Queue
authored andcommitted
[ddc] Add VM specific hot reload suite runner
Add a subclass to override common methods with VM specific implementations. DDC implementations will be added in future changes. Then these implementations will diverge more to support the VM rejecting edits at "runtime" vs DDC at compile time. Change-Id: I314b78e10d1f4d82da8af71e3fec3b3ef9f83aaf Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/393181 Reviewed-by: Mark Zhou <[email protected]> Commit-Queue: Nicholas Shahan <[email protected]>
1 parent e5739ab commit 14734d5

File tree

1 file changed

+170
-90
lines changed

1 file changed

+170
-90
lines changed

pkg/dev_compiler/test/hot_reload_suite.dart

Lines changed: 170 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,7 @@ Future<void> main(List<String> args) async {
4040
final runner = switch (options.runtime) {
4141
RuntimePlatforms.chrome => ChromeSuiteRunner(options),
4242
RuntimePlatforms.d8 => D8SuiteRunner(options),
43-
// TODO(nshahan): Create a suite runner specific to the VM.
44-
RuntimePlatforms.vm => HotReloadSuiteRunner(options),
43+
RuntimePlatforms.vm => VMSuiteRunner(options),
4544
};
4645
await runner.runSuite(options);
4746
}
@@ -229,11 +228,11 @@ class HotReloadSuiteRunner {
229228
Options options;
230229

231230
/// The root directory containing generated code for all tests.
232-
late final generatedCodeDir = Directory.systemTemp.createTempSync();
231+
late final Directory generatedCodeDir = Directory.systemTemp.createTempSync();
233232

234233
/// The directory containing files emitted from Frontend Server compiles and
235234
/// recompiles.
236-
late final frontendServerEmittedFilesDir =
235+
late final Directory frontendServerEmittedFilesDir =
237236
Directory.fromUri(generatedCodeDir.uri.resolve('.fes/'))..createSync();
238237

239238
/// The output location for .dill file created by the front end server.
@@ -246,16 +245,21 @@ class HotReloadSuiteRunner {
246245
frontendServerEmittedFilesDir.uri.resolve('output_incremental.dill');
247246

248247
/// All test results that are reported after running the entire test suite.
249-
final testOutcomes = <TestResultOutcome>[];
248+
final List<TestResultOutcome> testOutcomes = [];
250249

251250
/// The directory used as a temporary staging area to construct a compile-able
252251
/// test app across reload/restart generations.
253-
late final snapshotDir =
252+
late final Directory snapshotDir =
254253
Directory.fromUri(generatedCodeDir.uri.resolve('.snapshot/'))
255254
..createSync();
256255

257256
// TODO(markzipan): Support custom entrypoints.
258257
late final Uri snapshotEntrypointUri = snapshotDir.uri.resolve('main.dart');
258+
late final String snapshotEntrypointWithScheme = () {
259+
final snapshotEntrypointLibraryName = fe_shared.relativizeUri(
260+
snapshotDir.uri, snapshotEntrypointUri, fe_shared.isWindows);
261+
return '$filesystemScheme:///$snapshotEntrypointLibraryName';
262+
}();
259263

260264
HotReloadMemoryFilesystem? filesystem;
261265
final stopwatch = Stopwatch();
@@ -309,7 +313,15 @@ class HotReloadSuiteRunner {
309313
continue;
310314
}
311315
_print('Finished emitting assets.', label: test.name);
312-
await runTest(test, tempDirectory);
316+
final testOutputStreamController = StreamController<List<int>>();
317+
final testOutputBuffer = StringBuffer();
318+
testOutputStreamController.stream
319+
.transform(utf8.decoder)
320+
.listen(testOutputBuffer.write);
321+
final testPassed = await runTest(
322+
test, tempDirectory, IOSink(testOutputStreamController.sink));
323+
await reportTestOutcome(
324+
test.name, testOutputBuffer.toString(), testPassed);
313325
}
314326
await shutdown(controller);
315327
await reportAllResults();
@@ -638,7 +650,10 @@ class HotReloadSuiteRunner {
638650
}
639651

640652
/// Copy all files in [test] for the given [generation] into the snapshot
641-
/// directory.
653+
/// directory and returns uris of all the files copied.
654+
///
655+
/// The uris describe the copy destination in the form of the hot reload file
656+
/// system scheme.
642657
List<String> copyGenerationSources(HotReloadTest test, int generation) {
643658
_debugPrint('Entering generation $generation', label: test.name);
644659
final updatedFilesInCurrentGeneration = <String>[];
@@ -670,18 +685,14 @@ class HotReloadSuiteRunner {
670685
/// front end server [controller] and copy outputs to [outputDirectory].
671686
///
672687
/// Reports test failures on compile time errors.
688+
// TODO(nshahan): Move to a DDC specific suite runner.
673689
Future<bool> compileGeneration(
674690
HotReloadTest test,
675691
int generation,
676692
Directory outputDirectory,
677693
List<String> updatedFiles,
678694
HotReloadFrontendServerController controller) async {
679695
var hasCompileError = false;
680-
final filesystemScheme = 'hot-reload-test';
681-
final snapshotEntrypointLibraryName = fe_shared.relativizeUri(
682-
snapshotDir.uri, snapshotEntrypointUri, fe_shared.isWindows);
683-
final snapshotEntrypointWithScheme =
684-
'$filesystemScheme:///$snapshotEntrypointLibraryName';
685696
// The first generation calls `compile`, but subsequent ones call
686697
// `recompile`.
687698
// Likewise, use the incremental output directory for `recompile` calls.
@@ -737,44 +748,28 @@ class HotReloadSuiteRunner {
737748
'Frontend Server successfully compiled outputs to: '
738749
'$outputDillPath',
739750
label: test.name);
740-
if (options.runtime.emitsJS) {
741-
_debugPrint('Emitting JS code to ${outputDirectory.path}.',
742-
label: test.name);
743-
// Update the memory filesystem with the newly-created JS files.
744-
_print('Loading generation $generation files into the memory filesystem.',
745-
label: test.name);
746-
final codeFile = File('$outputDillPath.sources');
747-
final manifestFile = File('$outputDillPath.json');
748-
final sourcemapFile = File('$outputDillPath.map');
749-
filesystem!.update(codeFile, manifestFile, sourcemapFile,
750-
generation: '$generation');
751-
752-
// Write JS files and sourcemaps to their respective generation.
753-
_print('Writing generation $generation assets.', label: test.name);
754-
_debugPrint('Writing JS assets to ${outputDirectory.path}',
755-
label: test.name);
756-
filesystem!.writeToDisk(outputDirectory.uri, generation: '$generation');
757-
} else {
758-
final dillOutputDir = Directory.fromUri(
759-
outputDirectory.uri.resolve('generation$generation'));
760-
dillOutputDir.createSync();
761-
final dillOutputUri = dillOutputDir.uri.resolve('${test.name}.dill');
762-
File(outputDillPath).copySync(dillOutputUri.toFilePath());
763-
// Write dills their respective generation.
764-
_print('Writing generation $generation assets.', label: test.name);
765-
_debugPrint('Writing dill to ${dillOutputUri.toFilePath()}',
766-
label: test.name);
767-
}
751+
_debugPrint('Emitting JS code to ${outputDirectory.path}.',
752+
label: test.name);
753+
// Update the memory filesystem with the newly-created JS files.
754+
_print('Loading generation $generation files into the memory filesystem.',
755+
label: test.name);
756+
final codeFile = File('$outputDillPath.sources');
757+
final manifestFile = File('$outputDillPath.json');
758+
final sourcemapFile = File('$outputDillPath.map');
759+
filesystem!.update(codeFile, manifestFile, sourcemapFile,
760+
generation: '$generation');
761+
762+
// Write JS files and sourcemaps to their respective generation.
763+
_print('Writing generation $generation assets.', label: test.name);
764+
_debugPrint('Writing JS assets to ${outputDirectory.path}',
765+
label: test.name);
766+
filesystem!.writeToDisk(outputDirectory.uri, generation: '$generation');
768767
return true;
769768
}
770769

771770
// TODO(nshahan): Refactor into runtime specific implementations.
772-
Future<void> runTest(HotReloadTest test, Directory tempDirectory) async {
773-
final testOutputStreamController = StreamController<List<int>>();
774-
final testOutputBuffer = StringBuffer();
775-
testOutputStreamController.stream
776-
.transform(utf8.decoder)
777-
.listen(testOutputBuffer.write);
771+
Future<bool> runTest(
772+
HotReloadTest test, Directory tempDirectory, IOSink outputSink) async {
778773
var testPassed = false;
779774
switch (options.runtime) {
780775
case RuntimePlatforms.d8:
@@ -785,7 +780,7 @@ class HotReloadSuiteRunner {
785780
final d8Suite = (this as D8SuiteRunner)
786781
..bootstrapJsUri =
787782
tempDirectory.uri.resolve('generation0/bootstrap.js')
788-
..outputSink = IOSink(testOutputStreamController.sink);
783+
..outputSink = outputSink;
789784
await d8Suite.setupTest(
790785
testName: test.name,
791786
scriptDescriptors: filesystem!.scriptDescriptorForBootstrap,
@@ -805,7 +800,7 @@ class HotReloadSuiteRunner {
805800
tempDirectory.uri.resolve('generation0/bootstrap.js')
806801
..bootstrapHtmlUri =
807802
tempDirectory.uri.resolve('generation0/index.html')
808-
..outputSink = IOSink(testOutputStreamController.sink);
803+
..outputSink = outputSink;
809804
await suite.setupTest(
810805
testName: test.name,
811806
scriptDescriptors: filesystem!.scriptDescriptorForBootstrap,
@@ -814,48 +809,9 @@ class HotReloadSuiteRunner {
814809
final exitCode = await suite.runTestOld(testName: test.name);
815810
testPassed = exitCode == 0;
816811
case RuntimePlatforms.vm:
817-
final firstGenerationDillUri =
818-
tempDirectory.uri.resolve('generation0/${test.name}.dill');
819-
// Start the VM at generation 0.
820-
final vmArgs = [
821-
'--enable-vm-service=0', // 0 avoids port collisions.
822-
'--disable-service-auth-codes',
823-
'--disable-dart-dev',
824-
firstGenerationDillUri.toFilePath(),
825-
];
826-
final vm = await Process.start(Platform.executable, vmArgs);
827-
_debugPrint(
828-
'Starting VM with command: '
829-
'${Platform.executable} ${vmArgs.join(" ")}',
830-
label: test.name);
831-
vm.stdout
832-
.transform(utf8.decoder)
833-
.transform(LineSplitter())
834-
.listen((String line) {
835-
_debugPrint('VM stdout: $line', label: test.name);
836-
testOutputBuffer.writeln(line);
837-
});
838-
vm.stderr
839-
.transform(utf8.decoder)
840-
.transform(LineSplitter())
841-
.listen((String err) {
842-
_debugPrint('VM stderr: $err', label: test.name);
843-
testOutputBuffer.writeln(err);
844-
});
845-
_print('Executing VM test.', label: test.name);
846-
final testTimeoutSeconds = 10;
847-
final vmExitCode = await vm.exitCode
848-
.timeout(Duration(seconds: testTimeoutSeconds), onTimeout: () {
849-
final timeoutText =
850-
'Test timed out after $testTimeoutSeconds seconds.';
851-
_print(timeoutText, label: test.name);
852-
testOutputBuffer.writeln(timeoutText);
853-
vm.kill();
854-
return 1;
855-
});
856-
testPassed = vmExitCode == 0;
812+
throw UnsupportedError('Now implemented in VMSuiteRunner.');
857813
}
858-
await reportTestOutcome(test.name, testOutputBuffer.toString(), testPassed);
814+
return testPassed;
859815
}
860816

861817
/// Reports test results to standard out as well as the output .json file if
@@ -1211,3 +1167,127 @@ class ChromeSuiteRunner extends HotReloadSuiteRunner {
12111167
await super.runSuite(options);
12121168
}
12131169
}
1170+
1171+
/// Hot reload test suite runner for behavior specific to the VM.
1172+
class VMSuiteRunner extends HotReloadSuiteRunner {
1173+
VMSuiteRunner(super.options);
1174+
1175+
@override
1176+
Future<bool> compileGeneration(
1177+
HotReloadTest test,
1178+
int generation,
1179+
Directory outputDirectory,
1180+
List<String> updatedFiles,
1181+
HotReloadFrontendServerController controller) async {
1182+
// The first generation calls `compile`, but subsequent ones call
1183+
// `recompile`.
1184+
// Likewise, use the incremental output directory for `recompile` calls.
1185+
// TODO(nshahan): Sending compile/recompile instructions is likely
1186+
// the same across backends and should be shared code.
1187+
String outputDillPath;
1188+
_print('Compiling generation $generation with the Frontend Server.',
1189+
label: test.name);
1190+
CompilerOutput compilerOutput;
1191+
if (generation == 0) {
1192+
_debugPrint(
1193+
'Compiling snapshot entrypoint: $snapshotEntrypointWithScheme',
1194+
label: test.name);
1195+
outputDillPath = outputDillUri.toFilePath();
1196+
compilerOutput =
1197+
await controller.sendCompile(snapshotEntrypointWithScheme);
1198+
} else {
1199+
_debugPrint(
1200+
'Recompiling snapshot entrypoint: $snapshotEntrypointWithScheme',
1201+
label: test.name);
1202+
outputDillPath = outputIncrementalDillUri.toFilePath();
1203+
// TODO(markzipan): Add logic to reject bad compiles.
1204+
compilerOutput = await controller.sendRecompile(
1205+
snapshotEntrypointWithScheme,
1206+
invalidatedFiles: updatedFiles);
1207+
}
1208+
var hasCompileError = false;
1209+
// Frontend Server reported compile errors. Fail if they weren't
1210+
// expected, and do not run tests.
1211+
if (compilerOutput.errorCount > 0) {
1212+
hasCompileError = true;
1213+
await controller.sendReject();
1214+
// TODO(markzipan): Determine if 'contains' is good enough to determine
1215+
// compilation error correctness.
1216+
if (test.expectedError != null &&
1217+
compilerOutput.outputText.contains(test.expectedError!)) {
1218+
await reportTestOutcome(
1219+
test.name,
1220+
'Expected error found during compilation: '
1221+
'${test.expectedError}',
1222+
true);
1223+
} else {
1224+
await reportTestOutcome(
1225+
test.name,
1226+
'Test failed with compile error: ${compilerOutput.outputText}',
1227+
false);
1228+
}
1229+
} else {
1230+
controller.sendAccept();
1231+
}
1232+
// Stop processing further generations if compilation failed.
1233+
if (hasCompileError) return false;
1234+
_debugPrint(
1235+
'Frontend Server successfully compiled outputs to: '
1236+
'$outputDillPath',
1237+
label: test.name);
1238+
final dillOutputDir =
1239+
Directory.fromUri(outputDirectory.uri.resolve('generation$generation'));
1240+
dillOutputDir.createSync();
1241+
final dillOutputUri = dillOutputDir.uri.resolve('${test.name}.dill');
1242+
// Write dills their respective generation.
1243+
_print('Writing generation $generation assets.', label: test.name);
1244+
_debugPrint('Writing dill to ${dillOutputUri.toFilePath()}',
1245+
label: test.name);
1246+
File(outputDillPath).copySync(dillOutputUri.toFilePath());
1247+
return true;
1248+
}
1249+
1250+
@override
1251+
Future<bool> runTest(
1252+
HotReloadTest test, Directory tempDirectory, IOSink outputSink) async {
1253+
final firstGenerationDillUri =
1254+
tempDirectory.uri.resolve('generation0/${test.name}.dill');
1255+
// Start the VM at generation 0.
1256+
final vmArgs = [
1257+
'--enable-vm-service=0', // 0 avoids port collisions.
1258+
'--disable-service-auth-codes',
1259+
'--disable-dart-dev',
1260+
firstGenerationDillUri.toFilePath(),
1261+
];
1262+
_debugPrint(
1263+
'Starting VM with command: '
1264+
'${Platform.executable} ${vmArgs.join(" ")}',
1265+
label: test.name);
1266+
final vm = await Process.start(Platform.executable, vmArgs);
1267+
vm.stdout
1268+
.transform(utf8.decoder)
1269+
.transform(LineSplitter())
1270+
.listen((String line) {
1271+
_debugPrint('VM stdout: $line', label: test.name);
1272+
outputSink.writeln(line);
1273+
});
1274+
vm.stderr
1275+
.transform(utf8.decoder)
1276+
.transform(LineSplitter())
1277+
.listen((String err) {
1278+
_debugPrint('VM stderr: $err', label: test.name);
1279+
outputSink.writeln(err);
1280+
});
1281+
_print('Executing VM test.', label: test.name);
1282+
final testTimeoutSeconds = 10;
1283+
final vmExitCode = await vm.exitCode
1284+
.timeout(Duration(seconds: testTimeoutSeconds), onTimeout: () {
1285+
final timeoutText = 'Test timed out after $testTimeoutSeconds seconds.';
1286+
_print(timeoutText, label: test.name);
1287+
outputSink.writeln(timeoutText);
1288+
vm.kill();
1289+
return 1;
1290+
});
1291+
return vmExitCode == 0;
1292+
}
1293+
}

0 commit comments

Comments
 (0)