[reload_test] Adding support for negative tests in reload suite.
* Extends the frontend server controller to validate/reject compile errors in compiles/recompiles.
* Extends the config to permit an 'expectedError' entry.
* Adds a handful of tests to the suite (adapted from the VM's hot reload tests).
Change-Id: I47d814e375c4c72d0406ebf5bdfee3f1975c64f0
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/363800
Reviewed-by: Nicholas Shahan <nshahan@google.com>
Commit-Queue: Mark Zhou <markzipan@google.com>
diff --git a/pkg/dev_compiler/test/hot_reload_suite.dart b/pkg/dev_compiler/test/hot_reload_suite.dart
index 1055356..9846d40 100644
--- a/pkg/dev_compiler/test/hot_reload_suite.dart
+++ b/pkg/dev_compiler/test/hot_reload_suite.dart
@@ -155,8 +155,8 @@
// Ignore Dart source files, which may be imported as helpers
continue;
}
- throw Exception(
- 'Non-directory or file entity found in ${allTestsUri.toFilePath()}: $testDir');
+ throw Exception('Non-directory or file entity found in '
+ '${allTestsUri.toFilePath()}: $testDir');
}
final testDirParts = testDir.uri.pathSegments;
final testName = testDirParts[testDirParts.length - 2];
@@ -167,6 +167,20 @@
);
var stopwatch = Stopwatch()..start();
+ // Report results for this test.
+ 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);
+ }
+ }
+
final tempUri = generatedCodeUri.resolve('$testName/');
Directory.fromUri(tempUri).createSync();
@@ -221,6 +235,7 @@
_print('Generating code over ${maxGenerations + 1} generations.',
label: testName);
+ var hasCompileError = false;
// Generate hot reload/restart generations as subdirectories in a loop.
var currentGeneration = 0;
while (currentGeneration <= maxGenerations) {
@@ -231,7 +246,8 @@
// names restored (e.g., path/to/main' from 'path/to/main.0.dart).
// TODO(markzipan): support subdirectories.
_debugPrint(
- 'Copying Dart files to snapshot directory: ${snapshotUri.toFilePath()}',
+ 'Copying Dart files to snapshot directory: '
+ '${snapshotUri.toFilePath()}',
label: testName);
for (var file in testDir.listSync()) {
// Convert a name like `/path/foo.bar.25.dart` to `/path/foo.bar.dart`.
@@ -267,21 +283,49 @@
_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();
- await controller.sendCompileAndAccept(snapshotEntrypointWithScheme);
+ compilerOutput =
+ await controller.sendCompile(snapshotEntrypointWithScheme);
} else {
_debugPrint(
'Recompiling snapshot entrypoint: $snapshotEntrypointWithScheme',
label: testName);
outputDirectoryPath = outputIncrementalDillUri.toFilePath();
// TODO(markzipan): Add logic to reject bad compiles.
- await controller.sendRecompileAndAccept(snapshotEntrypointWithScheme,
+ 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 (testConfig.expectedError != null &&
+ compilerOutput.outputText.contains(testConfig.expectedError!)) {
+ await reportTestOutcome(
+ 'Expected error found during compilation: '
+ '${testConfig.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;
+
_debugPrint(
'Frontend Server successfully compiled outputs to: '
'$outputDirectoryPath',
@@ -324,6 +368,14 @@
currentGeneration++;
}
+ // 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;
+ }
+
_print('Finished emitting assets.', label: testName);
final testOutputStreamController = StreamController<List<int>>();
@@ -369,7 +421,8 @@
];
final vm = await Process.start(Platform.executable, vmArgs);
_debugPrint(
- 'Starting VM with command: ${Platform.executable} ${vmArgs.join(" ")}',
+ 'Starting VM with command: '
+ '${Platform.executable} ${vmArgs.join(" ")}',
label: testName);
vm.stdout
.transform(utf8.decoder)
@@ -398,19 +451,7 @@
testPassed = vmExitCode == 0;
}
- stopwatch.stop();
- final testOutput = testOutputBuffer.toString();
- outcome.elapsedTime = stopwatch.elapsed;
- outcome.matchedExpectations = testPassed;
- outcome.testOutput = testOutput;
- testOutcomes.add(outcome);
- if (testPassed) {
- _print('PASSED with:\n$testOutput', label: testName);
- } else {
- _print('FAILED with:\n$testOutput', label: testName);
- await shutdown();
- exit(1);
- }
+ await reportTestOutcome(testOutputBuffer.toString(), testPassed);
}
await shutdown();
@@ -444,6 +485,17 @@
_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(1);
+ }
}
/// Runs the [command] with [args] in [environment].
diff --git a/pkg/reload_test/lib/frontend_server_controller.dart b/pkg/reload_test/lib/frontend_server_controller.dart
index 49981aa6..bf4e19e 100644
--- a/pkg/reload_test/lib/frontend_server_controller.dart
+++ b/pkg/reload_test/lib/frontend_server_controller.dart
@@ -13,6 +13,38 @@
final debug = false;
+/// Represents the output of a FrontendServer's 'compile' or 'recompile'.
+class CompilerOutput {
+ CompilerOutput({
+ required this.outputDillPath,
+ required this.errorCount,
+ this.sources = const [],
+ this.outputText = '',
+ });
+
+ /// Output for a 'reject' response.
+ factory CompilerOutput.rejectOutput() {
+ return CompilerOutput(
+ outputDillPath: '',
+ errorCount: 0,
+ );
+ }
+
+ final String outputDillPath;
+ final int errorCount;
+ final List<Uri> sources;
+ final String outputText;
+}
+
+enum FrontendServerState {
+ awaitingResult,
+ awaitingKey,
+ collectingResultSources,
+ awaitingReject,
+ awaitingRejectKey,
+ finished,
+}
+
/// Controls and synchronizes the Frontend Server during hot reloaad tests.
///
/// The Frontend Server accepts the following instructions:
@@ -27,11 +59,12 @@
/// > accept
///
/// > quit
-
-// 'compile' and 'recompile' instructions output the following on completion:
-// result <boundary-key>
-// <compiler output>
-// <boundary-key> [<output.dill>]
+///
+/// 'compile' and 'recompile' instructions output the following on completion:
+/// result <boundary-key>
+/// <boundary-key>
+/// [<error text or modified files prefixed by '-' or '+'>]
+/// <boundary-key> [<output.dill>] <error-count>
class HotReloadFrontendServerController {
final List<String> frontendServerArgs;
@@ -43,23 +76,36 @@
/// Contains one event per completed Frontend Server 'compile' or 'recompile'
/// command.
- final StreamController<String> compileCommandOutputChannel;
+ final StreamController<CompilerOutput> compileCommandOutputChannel;
/// An iterator over `compileCommandOutputChannel`.
/// Should be awaited after every 'compile' or 'recompile' command.
- final StreamIterator<String> synchronizer;
+ final StreamIterator<CompilerOutput> synchronizer;
+ /// Whether or not this controller has already been started.
bool started = false;
- String? _boundaryKey;
+
+ /// Initialize to an invalid string prior to the first result.
+ String _boundaryKey = 'INVALID';
+
late Future<int> frontendServerExitCode;
+ /// Source file URIs reported by the Frontend Server.
+ List<Uri> sources = [];
+
+ List<String> accumulatedOutput = [];
+
+ int totalErrors = 0;
+
+ FrontendServerState _state = FrontendServerState.awaitingResult;
+
HotReloadFrontendServerController._(this.frontendServerArgs, this.input,
this.output, this.compileCommandOutputChannel, this.synchronizer);
factory HotReloadFrontendServerController(List<String> frontendServerArgs) {
var input = StreamController<List<int>>();
var output = StreamController<List<int>>();
- var compileCommandOutputChannel = StreamController<String>();
+ var compileCommandOutputChannel = StreamController<CompilerOutput>();
var synchronizer = StreamIterator(compileCommandOutputChannel.stream);
return HotReloadFrontendServerController._(frontendServerArgs, input,
output, compileCommandOutputChannel, synchronizer);
@@ -78,15 +124,67 @@
.transform(const LineSplitter())
.listen((String s) {
if (debug) print('Frontend Server Response: $s');
- if (_boundaryKey == null) {
- if (s.startsWith(frontEndResponsePrefix)) {
+ switch (_state) {
+ case FrontendServerState.awaitingReject:
+ if (!s.startsWith(frontEndResponsePrefix)) {
+ throw Exception('Unexpected Frontend Server response: $s');
+ }
_boundaryKey = s.substring(frontEndResponsePrefix.length);
- }
- } else {
- if (s.startsWith(_boundaryKey!)) {
- compileCommandOutputChannel.add(_boundaryKey!);
- _boundaryKey = null;
- }
+ _state = FrontendServerState.awaitingRejectKey;
+ break;
+ case FrontendServerState.awaitingRejectKey:
+ if (s != _boundaryKey) {
+ throw Exception('Unexpected Frontend Server response for reject '
+ '(expected just a key): $s');
+ }
+ _state = FrontendServerState.finished;
+ compileCommandOutputChannel.add(CompilerOutput.rejectOutput());
+ _clearState();
+ break;
+ case FrontendServerState.awaitingResult:
+ if (!s.startsWith(frontEndResponsePrefix)) {
+ throw Exception('Unexpected Frontend Server response: $s');
+ }
+ _boundaryKey = s.substring(frontEndResponsePrefix.length);
+ _state = FrontendServerState.awaitingKey;
+ break;
+ case FrontendServerState.awaitingKey:
+ // Advance to the next state when we encounter a lone boundary key.
+ if (s == _boundaryKey) {
+ _state = FrontendServerState.collectingResultSources;
+ } else {
+ accumulatedOutput.add(s);
+ }
+ case FrontendServerState.collectingResultSources:
+ // Stop and record the result when we encounter a boundary key.
+ if (s.startsWith(_boundaryKey)) {
+ final compilationReportOutput = s.split(' ');
+ final outputDillPath = compilationReportOutput[1];
+ final errorCount = int.parse(compilationReportOutput[2]);
+ // The FrontendServer accumulates all errors seen so far, so we
+ // need to correct for errors from previous compilations.
+ final actualErrorCount = errorCount - totalErrors;
+ final compilerOutput = CompilerOutput(
+ outputDillPath: outputDillPath,
+ errorCount: actualErrorCount,
+ sources: sources,
+ outputText: accumulatedOutput.join('\n'),
+ );
+ totalErrors = errorCount;
+ _state = FrontendServerState.finished;
+ compileCommandOutputChannel.add(compilerOutput);
+ _clearState();
+ } else if (s.startsWith('+')) {
+ sources.add(Uri.parse(s.substring(1)));
+ } else if (s.startsWith('-')) {
+ sources.remove(Uri.parse(s.substring(1)));
+ } else {
+ throw Exception("Unexpected Frontend Server response "
+ "(expected '+' or '-')'): $s");
+ }
+ break;
+ case FrontendServerState.finished:
+ throw StateError('Frontend Server reached an unexpected state: $s');
}
});
@@ -99,12 +197,23 @@
started = true;
}
- Future<void> sendCompile(String dartSourcePath) async {
+ /// Clears the controller's state between commands.
+ ///
+ /// Note: this does not reset the Frontend Server's state.
+ void _clearState() {
+ sources.clear();
+ accumulatedOutput.clear();
+ _boundaryKey = 'INVALID';
+ }
+
+ Future<CompilerOutput> sendCompile(String dartSourcePath) async {
if (!started) throw Exception('Frontend Server has not been started yet.');
+ _state = FrontendServerState.awaitingResult;
final command = 'compile $dartSourcePath\n';
if (debug) print('Sending instruction to Frontend Server:\n$command');
input.add(command.codeUnits);
await synchronizer.moveNext();
+ return synchronizer.current;
}
Future<void> sendCompileAndAccept(String dartSourcePath) async {
@@ -112,15 +221,17 @@
sendAccept();
}
- Future<void> sendRecompile(String entrypointPath,
+ Future<CompilerOutput> sendRecompile(String entrypointPath,
{List<String> invalidatedFiles = const [],
String boundaryKey = fakeBoundaryKey}) async {
if (!started) throw Exception('Frontend Server has not been started yet.');
+ _state = FrontendServerState.awaitingResult;
final command = 'recompile $entrypointPath $boundaryKey\n'
'${invalidatedFiles.join('\n')}\n$boundaryKey\n';
if (debug) print('Sending instruction to Frontend Server:\n$command');
input.add(command.codeUnits);
await synchronizer.moveNext();
+ return synchronizer.current;
}
Future<void> sendRecompileAndAccept(String entrypointPath,
@@ -134,12 +245,19 @@
void sendAccept() {
if (!started) throw Exception('Frontend Server has not been started yet.');
final command = 'accept\n';
- // TODO(markzipan): We should reject certain invalid compiles (e.g., those
- // with unimplemented or invalid nodes).
if (debug) print('Sending instruction to Frontend Server:\n$command');
input.add(command.codeUnits);
}
+ Future<void> sendReject() async {
+ if (!started) throw Exception('Frontend Server has not been started yet.');
+ _state = FrontendServerState.awaitingReject;
+ final command = 'reject\n';
+ if (debug) print('Sending instruction to Frontend Server:\n$command');
+ input.add(command.codeUnits);
+ await synchronizer.moveNext();
+ }
+
void _sendQuit() {
if (!started) throw Exception('Frontend Server has not been started yet.');
final command = 'quit\n';
diff --git a/pkg/reload_test/lib/test_helpers.dart b/pkg/reload_test/lib/test_helpers.dart
index 3db6bca..7b984a0 100644
--- a/pkg/reload_test/lib/test_helpers.dart
+++ b/pkg/reload_test/lib/test_helpers.dart
@@ -69,11 +69,13 @@
class ReloadTestConfiguration {
final Map<String, dynamic> _values;
final Set<RuntimePlatforms> excludedPlaforms;
+ final String? expectedError;
- ReloadTestConfiguration._(this._values, this.excludedPlaforms);
+ ReloadTestConfiguration._(
+ this._values, this.excludedPlaforms, this.expectedError);
factory ReloadTestConfiguration() => ReloadTestConfiguration._(
- const <String, dynamic>{}, <RuntimePlatforms>{});
+ const <String, dynamic>{}, <RuntimePlatforms>{}, null);
factory ReloadTestConfiguration.fromJsonFile(Uri file) {
final Map<String, dynamic> jsonData =
@@ -86,7 +88,8 @@
excludedPlaforms.add(runtimePlatform);
}
}
- return ReloadTestConfiguration._(jsonData, excludedPlaforms);
+ return ReloadTestConfiguration._(
+ jsonData, excludedPlaforms, jsonData['expectedError']);
}
String toJson() {
diff --git a/tests/hot_reload/bad_class/config.json b/tests/hot_reload/bad_class/config.json
new file mode 100644
index 0000000..736ed4d
--- /dev/null
+++ b/tests/hot_reload/bad_class/config.json
@@ -0,0 +1,7 @@
+{
+ "exclude": [
+ "d8",
+ "chrome"
+ ],
+ "expectedError": "Error: Expected ';' after this"
+}
\ No newline at end of file
diff --git a/tests/hot_reload/bad_class/main.0.dart b/tests/hot_reload/bad_class/main.0.dart
new file mode 100644
index 0000000..f464c78
--- /dev/null
+++ b/tests/hot_reload/bad_class/main.0.dart
@@ -0,0 +1,21 @@
+// 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';
+
+// Adapted from:
+// https://github.com/dart-lang/sdk/blob/36c0788137d55c6c77f4b9a8be12e557bc764b1c/runtime/vm/isolate_reload_test.cc#L364
+
+class Foo {
+ final a;
+ Foo(this.a);
+}
+
+Future<void> main() async {
+ var foo = Foo(5);
+ Expect.equals(5, foo.a);
+ await hotReload();
+ throw Exception('This should never run.');
+}
diff --git a/tests/hot_reload/bad_class/main.1.dart b/tests/hot_reload/bad_class/main.1.dart
new file mode 100644
index 0000000..653efa7
--- /dev/null
+++ b/tests/hot_reload/bad_class/main.1.dart
@@ -0,0 +1,21 @@
+// 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';
+
+// Adapted from:
+// https://github.com/dart-lang/sdk/blob/36c0788137d55c6c77f4b9a8be12e557bc764b1c/runtime/vm/isolate_reload_test.cc#L364
+
+class Foo {
+ final a kjsdf ksjdf;
+ Foo(this.a);
+}
+
+Future<void> main() async {
+ var foo = Foo(5);
+ Expect.equals(5, foo.a);
+ await hotReload();
+ throw Exception('This should never run.');
+}
diff --git a/tests/hot_reload/compile_base_class/config.json b/tests/hot_reload/compile_base_class/config.json
new file mode 100644
index 0000000..908c53e
--- /dev/null
+++ b/tests/hot_reload/compile_base_class/config.json
@@ -0,0 +1,3 @@
+{
+ "exclude": ["d8", "chrome"]
+}
diff --git a/tests/hot_reload/compile_base_class/lib.0.dart b/tests/hot_reload/compile_base_class/lib.0.dart
new file mode 100644
index 0000000..05c5039
--- /dev/null
+++ b/tests/hot_reload/compile_base_class/lib.0.dart
@@ -0,0 +1,12 @@
+// 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.
+
+class State<T, U> {
+ T? t;
+ U? u;
+ State(List l) {
+ t = l[0] is T ? l[0] : null;
+ u = l[1] is U ? l[1] : null;
+ }
+}
diff --git a/tests/hot_reload/compile_base_class/lib.1.dart b/tests/hot_reload/compile_base_class/lib.1.dart
new file mode 100644
index 0000000..eec8eeb
--- /dev/null
+++ b/tests/hot_reload/compile_base_class/lib.1.dart
@@ -0,0 +1,12 @@
+// 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.
+
+class State<U, T> {
+ T? t;
+ U? u;
+ State(List l) {
+ t = l[0] is T ? l[0] : null;
+ u = l[1] is U ? l[1] : null;
+ }
+}
diff --git a/tests/hot_reload/compile_base_class/main.0.dart b/tests/hot_reload/compile_base_class/main.0.dart
new file mode 100644
index 0000000..767d5f1
--- /dev/null
+++ b/tests/hot_reload/compile_base_class/main.0.dart
@@ -0,0 +1,20 @@
+// 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';
+
+import 'util.dart';
+
+// Adapted from:
+// https://github.com/dart-lang/sdk/blob/36c0788137d55c6c77f4b9a8be12e557bc764b1c/runtime/vm/isolate_reload_test.cc#L278
+
+Future<void> main() async {
+ var v = doWork();
+ Expect.equals(42, v);
+ await hotReload();
+
+ v = doWork();
+ Expect.equals(null, v);
+}
diff --git a/tests/hot_reload/compile_base_class/util.0.dart b/tests/hot_reload/compile_base_class/util.0.dart
new file mode 100644
index 0000000..1a3ce7f
--- /dev/null
+++ b/tests/hot_reload/compile_base_class/util.0.dart
@@ -0,0 +1,12 @@
+// 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 'lib.dart';
+
+class MyAccountState extends State<int, String> {
+ MyAccountState(List l) : super(l) {}
+ first() => t;
+}
+
+doWork() => new MyAccountState(<dynamic>[42, 'abc']).first();
diff --git a/tests/hot_reload/compile_generics/config.json b/tests/hot_reload/compile_generics/config.json
new file mode 100644
index 0000000..908c53e
--- /dev/null
+++ b/tests/hot_reload/compile_generics/config.json
@@ -0,0 +1,3 @@
+{
+ "exclude": ["d8", "chrome"]
+}
diff --git a/tests/hot_reload/compile_generics/lib.0.dart b/tests/hot_reload/compile_generics/lib.0.dart
new file mode 100644
index 0000000..cf1ba7ced
--- /dev/null
+++ b/tests/hot_reload/compile_generics/lib.0.dart
@@ -0,0 +1,9 @@
+// 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.
+
+class State<T> {
+ T t;
+ State(this.t);
+ T howAreTheThings() => t;
+}
diff --git a/tests/hot_reload/compile_generics/main.0.dart b/tests/hot_reload/compile_generics/main.0.dart
new file mode 100644
index 0000000..030ef99
--- /dev/null
+++ b/tests/hot_reload/compile_generics/main.0.dart
@@ -0,0 +1,28 @@
+// 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';
+
+import 'lib.dart';
+
+// Adapted from:
+// https://github.com/dart-lang/sdk/blob/36c0788137d55c6c77f4b9a8be12e557bc764b1c/runtime/vm/isolate_reload_test.cc#L204
+
+class Account {
+ int balance() => 42;
+}
+
+class MyAccountState extends State<Account> {
+ MyAccountState(Account a) : super(a) {}
+}
+
+Future<void> main() async {
+ var balance = (MyAccountState(Account())).howAreTheThings().balance();
+ Expect.equals(42, balance);
+ await hotReload();
+
+ balance = (MyAccountState(Account())).howAreTheThings().balance();
+ Expect.equals(24, balance);
+}
diff --git a/tests/hot_reload/compile_generics/main.1.dart b/tests/hot_reload/compile_generics/main.1.dart
new file mode 100644
index 0000000..706f9d5
--- /dev/null
+++ b/tests/hot_reload/compile_generics/main.1.dart
@@ -0,0 +1,28 @@
+// 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';
+
+import 'lib.dart';
+
+// Adapted from:
+// https://github.com/dart-lang/sdk/blob/36c0788137d55c6c77f4b9a8be12e557bc764b1c/runtime/vm/isolate_reload_test.cc#L204
+
+class Account {
+ int balance() => 24;
+}
+
+class MyAccountState extends State<Account> {
+ MyAccountState(Account a) : super(a) {}
+}
+
+Future<void> main() async {
+ var balance = (MyAccountState(Account())).howAreTheThings().balance();
+ Expect.equals(42, balance);
+ await hotReload();
+
+ balance = (MyAccountState(Account())).howAreTheThings().balance();
+ Expect.equals(24, balance);
+}