Refactor how test metadata is managed in test.dart.

There's a bunch of information that gets pulled from the contents of a
test file itself: whether it's a multitest, VMOptions, etc. Previously,
that lived in some combination of TestInformation objects and a big
stringly-typed "optionsFromFile" map that got passed everywhere. Also,
the map got mutated in a couple of choice places.

This replaces all of that with a single TestFile class that represents
all of the metadata gleaned from a given test file. This cleans up the
code base and also should pave the way for supporting static error
tests using that same class.

There should be no behavioral changes in this patch.

I didn't remove the packageRoot stuff in this change since there's
another change in flight for that. I'll merge those two together once
one of them lands.

Change-Id: Ia6d4c926afb342b71cb0041db7219586a792ac80
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/106581
Commit-Queue: Bob Nystrom <rnystrom@google.com>
Reviewed-by: William Hesse <whesse@google.com>
diff --git a/pkg/test_runner/lib/src/compiler_configuration.dart b/pkg/test_runner/lib/src/compiler_configuration.dart
index b8ad52c..1c4499d 100644
--- a/pkg/test_runner/lib/src/compiler_configuration.dart
+++ b/pkg/test_runner/lib/src/compiler_configuration.dart
@@ -9,7 +9,7 @@
 import 'path.dart';
 import 'repository.dart';
 import 'runtime_configuration.dart';
-import 'test_suite.dart';
+import 'test_file.dart';
 import 'utils.dart';
 
 List<String> _replaceDartFiles(List<String> list, String replacement) {
@@ -134,17 +134,13 @@
       List<String> dart2jsOptions,
       List<String> ddcOptions,
       List<String> args) {
-    return sharedOptions.toList()
-      ..addAll(_configuration.sharedOptions)
-      ..addAll(args);
+    return [...sharedOptions, ..._configuration.sharedOptions, ...args];
   }
 
   List<String> computeRuntimeArguments(
       RuntimeConfiguration runtimeConfiguration,
-      TestInformation info,
+      TestFile testFile,
       List<String> vmOptions,
-      List<String> sharedOptions,
-      List<String> dartOptions,
       List<String> originalArguments,
       CommandArtifact artifact) {
     return [artifact.filename];
@@ -160,32 +156,26 @@
 
   List<String> computeRuntimeArguments(
       RuntimeConfiguration runtimeConfiguration,
-      TestInformation info,
+      TestFile testFile,
       List<String> vmOptions,
-      List<String> sharedOptions,
-      List<String> dartOptions,
       List<String> originalArguments,
       CommandArtifact artifact) {
-    var args = <String>[];
-    if (_isDebug) {
-      // Temporarily disable background compilation to avoid flaky crashes
-      // (see http://dartbug.com/30016 for details).
-      args.add('--no-background-compilation');
-    }
-    if (_useEnableAsserts) {
-      args.add('--enable_asserts');
-    }
-    if (_configuration.hotReload) {
-      args.add('--hot-reload-test-mode');
-    } else if (_configuration.hotReloadRollback) {
-      args.add('--hot-reload-rollback-test-mode');
-    }
-    return args
-      ..addAll(vmOptions)
-      ..addAll(sharedOptions)
-      ..addAll(_configuration.sharedOptions)
-      ..addAll(originalArguments)
-      ..addAll(dartOptions);
+    return [
+      if (_isDebug)
+        // Temporarily disable background compilation to avoid flaky crashes
+        // (see http://dartbug.com/30016 for details).
+        '--no-background-compilation',
+      if (_useEnableAsserts) '--enable_asserts',
+      if (_configuration.hotReload)
+        '--hot-reload-test-mode'
+      else if (_configuration.hotReloadRollback)
+        '--hot-reload-rollback-test-mode',
+      ...vmOptions,
+      ...testFile.sharedOptions,
+      ..._configuration.sharedOptions,
+      ...originalArguments,
+      ...testFile.dartOptions
+    ];
   }
 }
 
@@ -226,41 +216,39 @@
       List<String> dart2jsOptions,
       List<String> ddcOptions,
       List<String> args) {
-    return sharedOptions.toList()
-      ..addAll(_configuration.sharedOptions)
-      ..addAll(vmOptions)
-      ..addAll(args);
+    return [
+      ...sharedOptions,
+      ..._configuration.sharedOptions,
+      ...vmOptions,
+      ...args
+    ];
   }
 
   List<String> computeRuntimeArguments(
       RuntimeConfiguration runtimeConfiguration,
-      TestInformation info,
+      TestFile testFile,
       List<String> vmOptions,
-      List<String> sharedOptions,
-      List<String> dartOptions,
       List<String> originalArguments,
       CommandArtifact artifact) {
-    var args = <String>[];
-    if (_useEnableAsserts) {
-      args.add('--enable_asserts');
-    }
-    if (_configuration.hotReload) {
-      args.add('--hot-reload-test-mode');
-    } else if (_configuration.hotReloadRollback) {
-      args.add('--hot-reload-rollback-test-mode');
-    }
     var filename = artifact.filename;
     if (runtimeConfiguration is DartkAdbRuntimeConfiguration) {
       // On Android the Dill file will be pushed to a different directory on the
       // device. Use that one instead.
       filename = "${DartkAdbRuntimeConfiguration.DeviceTestDir}/out.dill";
     }
-    return args
-      ..addAll(vmOptions)
-      ..addAll(sharedOptions)
-      ..addAll(_configuration.sharedOptions)
-      ..addAll(_replaceDartFiles(originalArguments, filename))
-      ..addAll(dartOptions);
+
+    return [
+      if (_useEnableAsserts) '--enable_asserts',
+      if (_configuration.hotReload)
+        '--hot-reload-test-mode'
+      else if (_configuration.hotReloadRollback)
+        '--hot-reload-rollback-test-mode',
+      ...vmOptions,
+      ...testFile.sharedOptions,
+      ..._configuration.sharedOptions,
+      ..._replaceDartFiles(originalArguments, filename),
+      ...testFile.dartOptions
+    ];
   }
 }
 
@@ -351,31 +339,24 @@
       List<String> args) {
     // The result will be passed as an input to [extractArguments]
     // (i.e. the arguments to the [PipelineCommand]).
-    return <String>[]
-      ..addAll(vmOptions)
-      ..addAll(sharedOptions)
-      ..addAll(_configuration.sharedOptions)
-      ..addAll(args);
+    return [
+      ...vmOptions,
+      ...sharedOptions,
+      ..._configuration.sharedOptions,
+      ...args
+    ];
   }
 
   List<String> computeRuntimeArguments(
       RuntimeConfiguration runtimeConfiguration,
-      TestInformation info,
+      TestFile testFile,
       List<String> vmOptions,
-      List<String> sharedOptions,
-      List<String> dartOptions,
       List<String> originalArguments,
       CommandArtifact artifact) {
     CompilerConfiguration lastCompilerConfiguration =
         pipelineCommands.last.compilerConfiguration;
     return lastCompilerConfiguration.computeRuntimeArguments(
-        runtimeConfiguration,
-        info,
-        vmOptions,
-        sharedOptions,
-        dartOptions,
-        originalArguments,
-        artifact);
+        runtimeConfiguration, testFile, vmOptions, originalArguments, artifact);
   }
 }
 
@@ -453,19 +434,18 @@
       List<String> dart2jsOptions,
       List<String> ddcOptions,
       List<String> args) {
-    return <String>[]
-      ..addAll(sharedOptions)
-      ..addAll(_configuration.sharedOptions)
-      ..addAll(dart2jsOptions)
-      ..addAll(args);
+    return [
+      ...sharedOptions,
+      ..._configuration.sharedOptions,
+      ...dart2jsOptions,
+      ...args
+    ];
   }
 
   CommandArtifact computeCompilationArtifact(String tempDir,
       List<String> arguments, Map<String, String> environmentOverrides) {
-    var compilerArguments = arguments.toList()
-      ..addAll(_configuration.dart2jsOptions);
+    var compilerArguments = [...arguments, ..._configuration.dart2jsOptions];
 
-    var commands = <Command>[];
     // TODO(athom): input filename extraction is copied from DDC. Maybe this
     // should be passed to computeCompilationArtifact, instead?
     var inputFile = arguments.last;
@@ -476,22 +456,19 @@
     if (babel != null && babel.isNotEmpty) {
       out = out.replaceAll('.js', '.raw.js');
     }
-    commands.add(computeCompilationCommand(
-        out, compilerArguments, environmentOverrides));
-
-    if (babel != null && babel.isNotEmpty) {
-      commands.add(computeBabelCommand(out, babelOut, babel));
-    }
+    var commands = [
+      computeCompilationCommand(out, compilerArguments, environmentOverrides),
+      if (babel != null && babel.isNotEmpty)
+        computeBabelCommand(out, babelOut, babel)
+    ];
 
     return CommandArtifact(commands, babelOut, 'application/javascript');
   }
 
   List<String> computeRuntimeArguments(
       RuntimeConfiguration runtimeConfiguration,
-      TestInformation info,
+      TestFile testFile,
       List<String> vmOptions,
-      List<String> sharedOptions,
-      List<String> dartOptions,
       List<String> originalArguments,
       CommandArtifact artifact) {
     Uri sdk = _useSdk
@@ -538,13 +515,13 @@
       List<String> dart2jsOptions,
       List<String> ddcOptions,
       List<String> args) {
-    var result = sharedOptions.toList()
-      ..addAll(_configuration.sharedOptions)
-      ..addAll(ddcOptions);
-    // The file being compiled is the last argument.
-    result.add(args.last);
-
-    return result;
+    return [
+      ...sharedOptions,
+      ..._configuration.sharedOptions,
+      ...ddcOptions,
+      // The file being compiled is the last argument.
+      args.last
+    ];
   }
 
   Command _createCommand(String inputFile, String outputFile,
@@ -704,10 +681,10 @@
 
     if (Platform.isWindows) {
       exec = 'cmd.exe';
-      args = <String>['/c', 'del', tempKernelFile(tempDir)];
+      args = ['/c', 'del', tempKernelFile(tempDir)];
     } else {
       exec = 'rm';
-      args = <String>[tempKernelFile(tempDir)];
+      args = [tempKernelFile(tempDir)];
     }
 
     return Command.compilation('remove_kernel_file', tempDir,
@@ -718,7 +695,7 @@
   Command computeDartBootstrapCommand(String tempDir, List<String> arguments,
       Map<String, String> environmentOverrides) {
     var buildDir = _configuration.buildDirectory;
-    String exec = _configuration.genSnapshotPath;
+    var exec = _configuration.genSnapshotPath;
     if (exec == null) {
       if (_isAndroid) {
         if (_isArm) {
@@ -731,27 +708,21 @@
       }
     }
 
-    final args = <String>[];
-    if (_configuration.useBlobs) {
-      args.add("--snapshot-kind=app-aot-blobs");
-      args.add("--blobs_container_filename=$tempDir/out.aotsnapshot");
-    } else if (_configuration.useElf) {
-      args.add("--snapshot-kind=app-aot-elf");
-      args.add("--elf=$tempDir/out.aotsnapshot");
-    } else {
-      args.add("--snapshot-kind=app-aot-assembly");
-      args.add("--assembly=$tempDir/out.S");
-    }
-
-    if (_isAndroid && _isArm) {
-      args.add('--no-sim-use-hardfp');
-    }
-
-    if (_configuration.isMinified) {
-      args.add('--obfuscate');
-    }
-
-    args.addAll(_replaceDartFiles(arguments, tempKernelFile(tempDir)));
+    var args = [
+      if (_configuration.useBlobs) ...[
+        "--snapshot-kind=app-aot-blobs",
+        "--blobs_container_filename=$tempDir/out.aotsnapshot"
+      ] else if (_configuration.useElf) ...[
+        "--snapshot-kind=app-aot-elf",
+        "--elf=$tempDir/out.aotsnapshot"
+      ] else ...[
+        "--snapshot-kind=app-aot-assembly",
+        "--assembly=$tempDir/out.S"
+      ],
+      if (_isAndroid && _isArm) '--no-sim-use-hardfp',
+      if (_configuration.isMinified) '--obfuscate',
+      ..._replaceDartFiles(arguments, tempKernelFile(tempDir))
+    ];
 
     return Command.compilation('precompiler', tempDir, bootstrapDependencies(),
         exec, args, environmentOverrides,
@@ -805,18 +776,18 @@
         throw "Architecture not supported: ${_configuration.architecture.name}";
     }
 
-    var exec = cc;
-    var args = <String>[];
-    if (ccFlags != null) args.add(ccFlags);
-    if (ldFlags != null) args.add(ldFlags);
-    args.add(shared);
-    args.add('-nostdlib');
-    args.add('-o');
-    args.add('$tempDir/out.aotsnapshot');
-    args.add('$tempDir/out.S');
+    var args = [
+      if (ccFlags != null) ccFlags,
+      if (ldFlags != null) ldFlags,
+      shared,
+      '-nostdlib',
+      '-o',
+      '$tempDir/out.aotsnapshot',
+      '$tempDir/out.S'
+    ];
 
-    return Command.compilation('assemble', tempDir, bootstrapDependencies(),
-        exec, args, environmentOverrides,
+    return Command.compilation('assemble', tempDir, bootstrapDependencies(), cc,
+        args, environmentOverrides,
         alwaysCompile: !_useSdk);
   }
 
@@ -829,11 +800,8 @@
   /// almost identical configurations are tested simultaneously.
   Command computeRemoveAssemblyCommand(String tempDir, List arguments,
       Map<String, String> environmentOverrides) {
-    var exec = 'rm';
-    var args = ['$tempDir/out.S'];
-
     return Command.compilation('remove_assembly', tempDir,
-        bootstrapDependencies(), exec, args, environmentOverrides,
+        bootstrapDependencies(), 'rm', ['$tempDir/out.S'], environmentOverrides,
         alwaysCompile: !_useSdk);
   }
 
@@ -853,29 +821,21 @@
       List<String> dart2jsOptions,
       List<String> ddcOptions,
       List<String> originalArguments) {
-    List<String> args = [];
-    if (_useEnableAsserts) {
-      args.add('--enable_asserts');
-    }
-    return args
-      ..addAll(filterVmOptions(vmOptions))
-      ..addAll(sharedOptions)
-      ..addAll(_configuration.sharedOptions)
-      ..addAll(originalArguments);
+    return [
+      if (_useEnableAsserts) '--enable_asserts',
+      ...filterVmOptions(vmOptions),
+      ...sharedOptions,
+      ..._configuration.sharedOptions,
+      ...originalArguments
+    ];
   }
 
   List<String> computeRuntimeArguments(
       RuntimeConfiguration runtimeConfiguration,
-      TestInformation info,
+      TestFile testFile,
       List<String> vmOptions,
-      List<String> sharedOptions,
-      List<String> dartOptions,
       List<String> originalArguments,
       CommandArtifact artifact) {
-    var args = <String>[];
-    if (_useEnableAsserts) {
-      args.add('--enable_asserts');
-    }
     var dir = artifact.filename;
     if (runtimeConfiguration is DartPrecompiledAdbRuntimeConfiguration) {
       // On android the precompiled snapshot will be pushed to a different
@@ -885,12 +845,14 @@
     originalArguments =
         _replaceDartFiles(originalArguments, "$dir/out.aotsnapshot");
 
-    return args
-      ..addAll(vmOptions)
-      ..addAll(sharedOptions)
-      ..addAll(_configuration.sharedOptions)
-      ..addAll(originalArguments)
-      ..addAll(dartOptions);
+    return [
+      if (_useEnableAsserts) '--enable_asserts',
+      ...vmOptions,
+      ...testFile.sharedOptions,
+      ..._configuration.sharedOptions,
+      ...originalArguments,
+      ...testFile.dartOptions
+    ];
   }
 }
 
@@ -916,13 +878,14 @@
 
   Command computeCompilationCommand(String tempDir, List<String> arguments,
       Map<String, String> environmentOverrides) {
-    var exec = "${_configuration.buildDirectory}/dart";
     var snapshot = "$tempDir/out.jitsnapshot";
-    var args = ["--snapshot=$snapshot", "--snapshot-kind=app-jit"];
-    args.addAll(arguments);
-
-    return Command.compilation('app_jit', tempDir, bootstrapDependencies(),
-        exec, args, environmentOverrides,
+    return Command.compilation(
+        'app_jit',
+        tempDir,
+        bootstrapDependencies(),
+        "${_configuration.buildDirectory}/dart",
+        ["--snapshot=$snapshot", "--snapshot-kind=app-jit", ...arguments],
+        environmentOverrides,
         alwaysCompile: !_useSdk);
   }
 
@@ -933,36 +896,30 @@
       List<String> dart2jsOptions,
       List<String> ddcOptions,
       List<String> originalArguments) {
-    var args = <String>[];
-    if (_useEnableAsserts) {
-      args.add('--enable_asserts');
-    }
-    return args
-      ..addAll(vmOptions)
-      ..addAll(sharedOptions)
-      ..addAll(_configuration.sharedOptions)
-      ..addAll(originalArguments)
-      ..addAll(dartOptions);
+    return [
+      if (_useEnableAsserts) '--enable_asserts',
+      ...vmOptions,
+      ...sharedOptions,
+      ..._configuration.sharedOptions,
+      ...originalArguments,
+      ...dartOptions
+    ];
   }
 
   List<String> computeRuntimeArguments(
       RuntimeConfiguration runtimeConfiguration,
-      TestInformation info,
+      TestFile testFile,
       List<String> vmOptions,
-      List<String> sharedOptions,
-      List<String> dartOptions,
       List<String> originalArguments,
       CommandArtifact artifact) {
-    var args = <String>[];
-    if (_useEnableAsserts) {
-      args.add('--enable_asserts');
-    }
-    return args
-      ..addAll(vmOptions)
-      ..addAll(sharedOptions)
-      ..addAll(_configuration.sharedOptions)
-      ..addAll(_replaceDartFiles(originalArguments, artifact.filename))
-      ..addAll(dartOptions);
+    return [
+      if (_useEnableAsserts) '--enable_asserts',
+      ...vmOptions,
+      ...testFile.sharedOptions,
+      ..._configuration.sharedOptions,
+      ..._replaceDartFiles(originalArguments, artifact.filename),
+      ...testFile.dartOptions
+    ];
   }
 }
 
@@ -975,7 +932,7 @@
 
   String computeCompilerPath() {
     var prefix = 'sdk/bin';
-    String suffix = executableScriptSuffix;
+    var suffix = executableScriptSuffix;
     if (_isHostChecked) {
       if (_useSdk) {
         throw "--host-checked and --use-sdk cannot be used together";
@@ -993,32 +950,30 @@
 
   CommandArtifact computeCompilationArtifact(String tempDir,
       List<String> arguments, Map<String, String> environmentOverrides) {
-    arguments = arguments.toList();
     if (!previewDart2) {
       throw ArgumentError('--no-preview-dart-2 not supported');
     }
-    if (_configuration.useAnalyzerCfe) {
-      arguments.add('--use-cfe');
-    }
-    if (_configuration.useAnalyzerFastaParser) {
-      arguments.add('--use-fasta-parser');
-    }
+
+    var args = [
+      ...arguments,
+      if (_configuration.useAnalyzerCfe) '--use-cfe',
+      if (_configuration.useAnalyzerFastaParser) '--use-fasta-parser',
+    ];
 
     // Since this is not a real compilation, no artifacts are produced.
-    return CommandArtifact([
-      Command.analysis(computeCompilerPath(), arguments, environmentOverrides)
-    ], null, null);
+    return CommandArtifact(
+        [Command.analysis(computeCompilerPath(), args, environmentOverrides)],
+        null,
+        null);
   }
 
   List<String> computeRuntimeArguments(
       RuntimeConfiguration runtimeConfiguration,
-      TestInformation info,
+      TestFile testFile,
       List<String> vmOptions,
-      List<String> sharedOptions,
-      List<String> dartOptions,
       List<String> originalArguments,
       CommandArtifact artifact) {
-    return <String>[];
+    return [];
   }
 }
 
@@ -1039,7 +994,6 @@
 
   CommandArtifact computeCompilationArtifact(String tempDir,
       List<String> arguments, Map<String, String> environmentOverrides) {
-    arguments = arguments.toList();
     if (!previewDart2) {
       throw ArgumentError('--no-preview-dart-2 not supported');
     }
@@ -1047,19 +1001,17 @@
     // Since this is not a real compilation, no artifacts are produced.
     return CommandArtifact([
       Command.compareAnalyzerCfe(
-          computeCompilerPath(), arguments, environmentOverrides)
+          computeCompilerPath(), arguments.toList(), environmentOverrides)
     ], null, null);
   }
 
   List<String> computeRuntimeArguments(
       RuntimeConfiguration runtimeConfiguration,
-      TestInformation info,
+      TestFile testFile,
       List<String> vmOptions,
-      List<String> sharedOptions,
-      List<String> dartOptions,
       List<String> originalArguments,
       CommandArtifact artifact) {
-    return <String>[];
+    return [];
   }
 }
 
@@ -1082,13 +1034,11 @@
 
   List<String> computeRuntimeArguments(
       RuntimeConfiguration runtimeConfiguration,
-      TestInformation info,
+      TestFile testFile,
       List<String> vmOptions,
-      List<String> sharedOptions,
-      List<String> dartOptions,
       List<String> originalArguments,
       CommandArtifact artifact) {
-    return <String>[];
+    return [];
   }
 }
 
@@ -1128,43 +1078,35 @@
       kernelBinariesFolder += '/dart-sdk/lib/_internal';
     }
 
-    final vmPlatform = '$kernelBinariesFolder/vm_platform_strong.dill';
+    var vmPlatform = '$kernelBinariesFolder/vm_platform_strong.dill';
 
-    final dillFile = tempKernelFile(tempDir);
+    var dillFile = tempKernelFile(tempDir);
 
-    final args = [
+    var causalAsyncStacks = !arguments.any(noCausalAsyncStacksRegExp.hasMatch);
+
+    var args = [
       _isAot ? '--aot' : '--no-aot',
       '--platform=$vmPlatform',
       '-o',
       dillFile,
+      arguments.where((name) => name.endsWith('.dart')).single,
+      ...arguments.where((name) =>
+          name.startsWith('-D') ||
+          name.startsWith('--packages=') ||
+          name.startsWith('--enable-experiment=')),
+      '-Ddart.developer.causal_async_stacks=$causalAsyncStacks',
+      if (_useEnableAsserts ||
+          arguments.contains('--enable-asserts') ||
+          arguments.contains('--enable_asserts'))
+        '--enable-asserts',
+      if (_configuration.useKernelBytecode) ...[
+        '--gen-bytecode',
+        '--drop-ast',
+        '--bytecode-options=source-positions,local-var-info'
+      ]
     ];
 
-    final batchArgs = <String>[];
-    if (useAbiVersion != null) {
-      batchArgs.add(useAbiVersion);
-    }
-
-    args.add(arguments.where((name) => name.endsWith('.dart')).single);
-    args.addAll(arguments.where((name) =>
-        name.startsWith('-D') ||
-        name.startsWith('--packages=') ||
-        name.startsWith('--enable-experiment=')));
-
-    final bool causalAsyncStacks =
-        !arguments.any((String arg) => noCausalAsyncStacksRegExp.hasMatch(arg));
-    args.add('-Ddart.developer.causal_async_stacks=$causalAsyncStacks');
-
-    if (_useEnableAsserts ||
-        arguments.contains('--enable-asserts') ||
-        arguments.contains('--enable_asserts')) {
-      args.add('--enable-asserts');
-    }
-
-    if (_configuration.useKernelBytecode) {
-      args.add('--gen-bytecode');
-      args.add('--drop-ast');
-      args.add('--bytecode-options=source-positions,local-var-info');
-    }
+    var batchArgs = [if (useAbiVersion != null) useAbiVersion];
 
     return Command.vmKernelCompilation(dillFile, true, bootstrapDependencies(),
         genKernel, args, environmentOverrides, batchArgs);
@@ -1216,14 +1158,15 @@
         Uri.base.resolveUri(Uri.directory(tempDir)).resolve("out.dill");
     var outputFileName = output.toFilePath();
 
-    var compilerArguments = <String>['--verify'];
-    if (_isLegacy) {
-      compilerArguments.add("--legacy-mode");
-    }
-
-    compilerArguments.addAll(
-        ["-o", outputFileName, "--platform", _platformDill.toFilePath()]);
-    compilerArguments.addAll(arguments);
+    var compilerArguments = [
+      '--verify',
+      if (_isLegacy) "--legacy-mode",
+      "-o",
+      outputFileName,
+      "--platform",
+      _platformDill.toFilePath(),
+      ...arguments
+    ];
 
     return CommandArtifact([
       Command.fasta(
@@ -1245,9 +1188,8 @@
       List<String> dart2jsOptions,
       List<String> ddcOptions,
       List<String> args) {
-    List<String> arguments = List<String>.from(sharedOptions);
-    arguments.addAll(_configuration.sharedOptions);
-    for (String argument in args) {
+    var arguments = [...sharedOptions, ..._configuration.sharedOptions];
+    for (var argument in args) {
       if (argument == "--ignore-unrecognized-flags") continue;
       arguments.add(argument);
       if (!argument.startsWith("-")) {
@@ -1263,16 +1205,14 @@
   @override
   List<String> computeRuntimeArguments(
       RuntimeConfiguration runtimeConfiguration,
-      TestInformation info,
+      TestFile testFile,
       List<String> vmOptions,
-      List<String> sharedOptions,
-      List<String> dartOptions,
       List<String> originalArguments,
       CommandArtifact artifact) {
     if (runtimeConfiguration is! NoneRuntimeConfiguration) {
       throw "--compiler=fasta only supports --runtime=none";
     }
 
-    return <String>[];
+    return [];
   }
 }
diff --git a/pkg/test_runner/lib/src/multitest.dart b/pkg/test_runner/lib/src/multitest.dart
index 987af25..94567f8 100644
--- a/pkg/test_runner/lib/src/multitest.dart
+++ b/pkg/test_runner/lib/src/multitest.dart
@@ -73,7 +73,7 @@
 import "dart:io";
 
 import "path.dart";
-import "test_suite.dart";
+import "test_file.dart";
 import "utils.dart";
 
 /// Until legacy multitests are ported we need to support both /// and //#
@@ -90,7 +90,7 @@
   'checked mode compile-time error' // This is now a no-op
 ].toSet();
 
-void extractTestsFromMultitest(Path filePath, Map<String, String> tests,
+void _generateTestsFromMultitest(Path filePath, Map<String, String> tests,
     Map<String, Set<String>> outcomes) {
   var contents = File(filePath.toNativePath()).readAsStringSync();
 
@@ -162,31 +162,26 @@
   }
 }
 
-Future doMultitest(Path filePath, String outputDir, Path suiteDir,
-    CreateTest doTest, bool hotReload) {
-  void writeFile(String filepath, String content) {
-    var file = File(filepath);
-    if (file.existsSync()) {
-      var oldContent = file.readAsStringSync();
-      if (oldContent == content) {
-        // Don't write to the file if the content is the same
-        return;
-      }
-    }
-    file.writeAsStringSync(content);
-  }
-
-  // Each new test is a single String value in the Map tests.
+/// Split the given [multitest] into a series of separate tests for each
+/// section.
+///
+/// Writes the resulting tests to [outputDir] and returns a list of [TestFile]s
+/// for each of those generated tests.
+Future<List<TestFile>> splitMultitest(
+    TestFile multitest, String outputDir, Path suiteDir,
+    {bool hotReload}) async {
+  // Each key in the map tests is a multitest tag or "none", and the texts of
+  // the generated test it its value.
   var tests = <String, String>{};
   var outcomes = <String, Set<String>>{};
-  extractTestsFromMultitest(filePath, tests, outcomes);
+  _generateTestsFromMultitest(multitest.path, tests, outcomes);
 
-  var sourceDir = filePath.directoryPath;
+  var sourceDir = multitest.path.directoryPath;
   var targetDir = _createMultitestDirectory(outputDir, suiteDir, sourceDir);
   assert(targetDir != null);
 
   // Copy all the relative imports of the multitest.
-  var importsToCopy = _findAllRelativeImports(filePath);
+  var importsToCopy = _findAllRelativeImports(multitest.path);
   var futureCopies = <Future>[];
   for (var relativeImport in importsToCopy) {
     var importPath = Path(relativeImport);
@@ -204,35 +199,52 @@
   }
 
   // Wait until all imports are copied before scheduling test cases.
-  return Future.wait(futureCopies).then((_) {
-    var baseFilename = filePath.filenameWithoutExtension;
-    for (var key in tests.keys) {
-      var multitestFilename = targetDir.append('${baseFilename}_$key.dart');
-      writeFile(multitestFilename.toNativePath(), tests[key]);
+  await Future.wait(futureCopies);
 
-      var outcome = outcomes[key];
-      var hasStaticWarning = outcome.contains('static type warning');
-      var hasRuntimeError = outcome.contains('runtime error');
-      var hasSyntaxError = outcome.contains('syntax error');
-      var hasCompileError =
-          hasSyntaxError || outcome.contains('compile-time error');
+  var baseFilename = multitest.path.filenameWithoutExtension;
 
-      if (hotReload && hasCompileError) {
-        // Running a test that expects a compilation error with hot reloading
-        // is redundant with a regular run of the test.
-        continue;
-      }
+  var testFiles = <TestFile>[];
+  for (var section in tests.keys) {
+    var sectionFilePath = targetDir.append('${baseFilename}_$section.dart');
+    _writeFile(sectionFilePath.toNativePath(), tests[section]);
 
-      doTest(multitestFilename, filePath,
-          hasSyntaxError: hasSyntaxError,
-          hasCompileError: hasCompileError,
-          hasRuntimeError: hasRuntimeError,
-          hasStaticWarning: hasStaticWarning,
-          multitestKey: key);
+    var outcome = outcomes[section];
+    var hasStaticWarning = outcome.contains('static type warning');
+    var hasRuntimeError = outcome.contains('runtime error');
+    var hasSyntaxError = outcome.contains('syntax error');
+    var hasCompileError =
+        hasSyntaxError || outcome.contains('compile-time error');
+
+    if (hotReload && hasCompileError) {
+      // Running a test that expects a compilation error with hot reloading
+      // is redundant with a regular run of the test.
+      continue;
     }
 
-    return null;
-  });
+    // Create a [TestFile] for each split out section test.
+    testFiles.add(multitest.split(sectionFilePath, section,
+        hasSyntaxError: hasSyntaxError,
+        hasCompileError: hasCompileError,
+        hasRuntimeError: hasRuntimeError,
+        hasStaticWarning: hasStaticWarning));
+  }
+
+  return testFiles;
+}
+
+/// Writes [content] to [filePath] unless there is already a file at that path
+/// with the same content.
+void _writeFile(String filePath, String content) {
+  var file = File(filePath);
+
+  // Don't overwrite the file if the contents are the same. This way build
+  // systems don't think it has been modified.
+  if (file.existsSync()) {
+    var oldContent = file.readAsStringSync();
+    if (oldContent == content) return;
+  }
+
+  file.writeAsStringSync(content);
 }
 
 /// A multitest annotation in the special `//#` comment.
diff --git a/pkg/test_runner/lib/src/process_queue.dart b/pkg/test_runner/lib/src/process_queue.dart
index 0d5c77c..834053f 100644
--- a/pkg/test_runner/lib/src/process_queue.dart
+++ b/pkg/test_runner/lib/src/process_queue.dart
@@ -19,6 +19,7 @@
 import 'output_log.dart';
 import 'runtime_configuration.dart';
 import 'test_case.dart';
+import 'test_file.dart';
 import 'test_progress.dart';
 import 'test_suite.dart';
 import 'utils.dart';
@@ -236,7 +237,7 @@
     // Cache information about test cases per test suite. For multiple
     // configurations there is no need to repeatedly search the file
     // system, generate tests, and search test files for options.
-    var testCache = <String, List<TestInformation>>{};
+    var testCache = <String, List<TestFile>>{};
 
     var iterator = testSuites.iterator;
     void enqueueNextSuite() {
diff --git a/pkg/test_runner/lib/src/test_case.dart b/pkg/test_runner/lib/src/test_case.dart
index 6d1aa4e..af02960 100644
--- a/pkg/test_runner/lib/src/test_case.dart
+++ b/pkg/test_runner/lib/src/test_case.dart
@@ -15,7 +15,7 @@
 import 'output_log.dart';
 import 'process_queue.dart';
 import 'repository.dart';
-import 'test_suite.dart';
+import 'test_file.dart';
 import 'utils.dart';
 
 const _slowTimeoutMultiplier = 4;
@@ -71,27 +71,27 @@
 
   TestCase(this.displayName, this.commands, this.configuration,
       this.expectedOutcomes,
-      {TestInformation info}) {
+      {TestFile testFile}) {
     // A test case should do something.
     assert(commands.isNotEmpty);
 
-    if (info != null) {
-      _setExpectations(info);
-      hash = (info?.originTestPath?.relativeTo(Repository.dir)?.toString())
+    if (testFile != null) {
+      _setExpectations(testFile);
+      hash = (testFile.originPath?.relativeTo(Repository.dir)?.toString())
           .hashCode;
     }
   }
 
-  void _setExpectations(TestInformation info) {
+  void _setExpectations(TestFile testFile) {
     // We don't want to keep the entire (large) TestInformation structure,
     // so we copy the needed bools into flags set in a single integer.
-    if (info.hasRuntimeError) _expectations |= _hasRuntimeError;
-    if (info.hasSyntaxError) _expectations |= _hasSyntaxError;
-    if (info.hasCrash) _expectations |= _hasCrash;
-    if (info.hasCompileError || info.hasSyntaxError) {
+    if (testFile.hasRuntimeError) _expectations |= _hasRuntimeError;
+    if (testFile.hasSyntaxError) _expectations |= _hasSyntaxError;
+    if (testFile.hasCrash) _expectations |= _hasCrash;
+    if (testFile.hasCompileError || testFile.hasSyntaxError) {
       _expectations |= _hasCompileError;
     }
-    if (info.hasStaticWarning) _expectations |= _hasStaticWarning;
+    if (testFile.hasStaticWarning) _expectations |= _hasStaticWarning;
   }
 
   TestCase indexedCopy(int index) {
diff --git a/pkg/test_runner/lib/src/test_file.dart b/pkg/test_runner/lib/src/test_file.dart
new file mode 100644
index 0000000..4ab8063
--- /dev/null
+++ b/pkg/test_runner/lib/src/test_file.dart
@@ -0,0 +1,431 @@
+// Copyright (c) 2019, 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 'dart:io';
+
+import 'path.dart';
+
+final _multiHtmlTestGroupRegExp = RegExp(r"\s*[^/]\s*group\('[^,']*");
+final _multiHtmlTestRegExp = RegExp(r"useHtmlIndividualConfiguration\(\)");
+
+// TODO(rnystrom): Remove support for "///" once tests have been migrated.
+// https://dart-review.googlesource.com/c/sdk/+/106201
+// https://github.com/dart-lang/co19/issues/391
+/// Require at least one non-space character before '//[/#]'.
+final _multitestRegExp = RegExp(r"\S *//[#/] \w+:(.*)");
+
+final _vmOptionsRegExp = RegExp(r"// VMOptions=(.*)");
+final _environmentRegExp = RegExp(r"// Environment=(.*)");
+final _packageRootRegExp = RegExp(r"// PackageRoot=(.*)");
+final _packagesRegExp = RegExp(r"// Packages=(.*)");
+
+List<String> _splitWords(String s) =>
+    s.split(' ').where((e) => e != '').toList();
+
+List<String> _parseOption(String filePath, String contents, String name,
+    {bool allowMultiple = false}) {
+  var matches = RegExp('// $name=(.*)').allMatches(contents);
+  if (!allowMultiple && matches.length > 1) {
+    throw Exception('More than one "// $name=" line in test $filePath');
+  }
+
+  var options = <String>[];
+  for (var match in matches) {
+    options.addAll(_splitWords(match[1]));
+  }
+
+  return options;
+}
+
+abstract class _TestFileBase {
+  /// The test suite directory containing this test.
+  final Path _suiteDirectory;
+
+  /// The full path to the test file.
+  final Path path;
+
+  /// The path to the original multitest file this test was generated from.
+  ///
+  /// If this test was not generated from a multitest, just returns [path].
+  Path get originPath;
+
+  String get multitestKey;
+
+  _TestFileBase(this._suiteDirectory, this.path) {
+    assert(path.isAbsolute);
+  }
+
+  /// The logical name of the test.
+  ///
+  /// This is its path relative to the test suite directory containing it,
+  /// minus any file extension. If this test was split from a multitest,
+  /// it contains the multitest key.
+  String get name {
+    var testNamePath = originPath.relativeTo(_suiteDirectory);
+    var directory = testNamePath.directoryPath;
+    var filenameWithoutExt = testNamePath.filenameWithoutExtension;
+
+    String concat(String base, String part) {
+      if (base == "") return part;
+      if (part == "") return base;
+      return "$base/$part";
+    }
+
+    var result = "$directory";
+    result = concat(result, "$filenameWithoutExt");
+    result = concat(result, multitestKey);
+    return result;
+  }
+}
+
+/// Represents a single ".dart" file used as a test and the parsed metadata it
+/// contains.
+///
+/// Special options for individual tests are currently specified in various
+/// ways: with comments directly in test files, by using certain imports, or
+/// by creating additional files in the test directories.
+///
+/// Here is a list of options that are used by 'test.dart' today:
+///
+/// *   Flags can be passed to the VM process that runs the test by adding a
+///     comment to the test file:
+///
+///         // VMOptions=--flag1 --flag2
+///
+/// *   Flags can be passed to dart2js, vm or dartdevc by adding a comment to
+///     the test file:
+///
+///         // SharedOptions=--flag1 --flag2
+///
+/// *   Flags can be passed to dart2js by adding a comment to the test file:
+///
+///         // dart2jsOptions=--flag1 --flag2
+///
+/// *   Flags can be passed to the dart script that contains the test also
+///     using comments, as follows:
+///
+///         // DartOptions=--flag1 --flag2
+///
+/// *   Extra environment variables can be passed to the process that runs
+///     the test by adding comment(s) to the test file:
+///
+///         // Environment=ENV_VAR1=foo bar
+///         // Environment=ENV_VAR2=bazz
+///
+/// *   Most tests are not web tests, but can (and will be) wrapped within an
+///     HTML file and another script file to test them also on browser
+///     environments (e.g. language and corelib tests are run this way). We
+///     deduce that if a file with the same name as the test, but ending in
+///     ".html" instead of ".dart" exists, the test was intended to be a web
+///     test and no wrapping is necessary.
+///
+/// *   This test requires libfoobar.so, libfoobar.dylib or foobar.dll to be in
+///     the system linker path of the VM.
+///
+///         // SharedObjects=foobar
+///
+/// *   'test.dart' assumes tests fail if the process returns a non-zero exit
+///     code (in the case of web tests, we check for PASS/FAIL indications in
+///     the test output).
+class TestFile extends _TestFileBase {
+  /// Read the test file from the given [filePath].
+  factory TestFile.read(Path suiteDirectory, String filePath) {
+    if (filePath.endsWith('.dill')) {
+      return TestFile._(suiteDirectory, Path(filePath),
+          vmOptions: [[]],
+          sharedOptions: [],
+          dart2jsOptions: [],
+          dartOptions: [],
+          packageRoot: null,
+          packages: null,
+          hasSyntaxError: false,
+          hasCompileError: false,
+          hasRuntimeError: false,
+          hasStaticWarning: false,
+          isMultitest: false,
+          isMultiHtmlTest: false,
+          subtestNames: []);
+    }
+
+    var contents = File(filePath).readAsStringSync();
+
+    // VM options.
+    var vmOptions = <List<String>>[];
+    var matches = _vmOptionsRegExp.allMatches(contents);
+    for (var match in matches) {
+      vmOptions.add(_splitWords(match[1]));
+    }
+    if (vmOptions.isEmpty) vmOptions.add(<String>[]);
+
+    // Other options.
+    var dartOptions = _parseOption(filePath, contents, 'DartOptions');
+    var sharedOptions = _parseOption(filePath, contents, 'SharedOptions');
+    var dart2jsOptions = _parseOption(filePath, contents, 'dart2jsOptions');
+    var ddcOptions = _parseOption(filePath, contents, 'dartdevcOptions');
+    var otherResources =
+        _parseOption(filePath, contents, 'OtherResources', allowMultiple: true);
+    var sharedObjects =
+        _parseOption(filePath, contents, 'SharedObjects', allowMultiple: true);
+
+    // Environment.
+    Map<String, String> environment;
+    matches = _environmentRegExp.allMatches(contents);
+    for (var match in matches) {
+      var envDef = match[1];
+      var pos = envDef.indexOf('=');
+      var name = (pos < 0) ? envDef : envDef.substring(0, pos);
+      var value = (pos < 0) ? '' : envDef.substring(pos + 1);
+      environment ??= {};
+      environment[name] = value;
+    }
+
+    // Packages.
+    String packageRoot;
+    String packages;
+    matches = _packageRootRegExp.allMatches(contents);
+    for (var match in matches) {
+      if (packageRoot != null || packages != null) {
+        throw Exception('More than one "// Package... line in test $filePath');
+      }
+      packageRoot = match[1];
+      if (packageRoot != 'none') {
+        // PackageRoot=none means that no packages or package-root option
+        // should be given. Any other value overrides package-root and
+        // removes any packages option.  Don't use with // Packages=.
+        packageRoot = Uri.file(filePath)
+            .resolveUri(Uri.directory(packageRoot))
+            .toFilePath();
+      }
+    }
+
+    matches = _packagesRegExp.allMatches(contents);
+    for (var match in matches) {
+      if (packages != null || packageRoot != null) {
+        throw Exception('More than one "// Package..." line in test $filePath');
+      }
+      packages = match[1];
+      if (packages != 'none') {
+        // Packages=none means that no packages or package-root option
+        // should be given. Any other value overrides packages and removes
+        // any package-root option. Don't use with // PackageRoot=.
+        packages =
+            Uri.file(filePath).resolveUri(Uri.file(packages)).toFilePath();
+      }
+    }
+
+    var isMultitest = _multitestRegExp.hasMatch(contents);
+    var isMultiHtmlTest = _multiHtmlTestRegExp.hasMatch(contents);
+
+    var subtestNames = <String>[];
+    if (isMultiHtmlTest) {
+      for (var match in _multiHtmlTestGroupRegExp.allMatches(contents)) {
+        var fullMatch = match.group(0);
+        subtestNames.add(fullMatch.substring(fullMatch.indexOf("'") + 1));
+      }
+    }
+
+    // TODO(rnystrom): During the migration of the existing tests to Dart 2.0,
+    // we have a number of tests that used to both generate static type warnings
+    // and also validate some runtime behavior in an implementation that
+    // ignores those warnings. Those warnings are now errors. The test code
+    // validates the runtime behavior can and should be removed, but the code
+    // that causes the static warning should still be preserved since that is
+    // part of our coverage of the static type system.
+    //
+    // The test needs to indicate that it should have a static error. We could
+    // put that in the status file, but that makes it confusing because it
+    // would look like implementations that *don't* report the error are more
+    // correct. Eventually, we want to have a notation similar to what front_end
+    // is using for the inference tests where we can put a comment inside the
+    // test that says "This specific static error should be reported right by
+    // this token."
+    //
+    // That system isn't in place yet, so we do a crude approximation here in
+    // test.dart. If a test contains `/*@compile-error=`, which matches the
+    // beginning of the tag syntax that front_end uses, then we assume that
+    // this test must have a static error somewhere in it.
+    //
+    // Redo this code once we have a more precise test framework for detecting
+    // and locating these errors.
+    var hasSyntaxError = contents.contains("@syntax-error");
+    var hasCompileError = hasSyntaxError || contents.contains("@compile-error");
+
+    return TestFile._(suiteDirectory, Path(filePath),
+        packageRoot: packageRoot,
+        packages: packages,
+        environment: environment,
+        isMultitest: isMultitest,
+        isMultiHtmlTest: isMultiHtmlTest,
+        hasSyntaxError: hasSyntaxError,
+        hasCompileError: hasCompileError,
+        hasRuntimeError: contents.contains("@runtime-error"),
+        hasStaticWarning: contents.contains("@static-warning"),
+        hasCrash: false,
+        subtestNames: subtestNames,
+        sharedOptions: sharedOptions,
+        dartOptions: dartOptions,
+        dart2jsOptions: dart2jsOptions,
+        ddcOptions: ddcOptions,
+        vmOptions: vmOptions,
+        sharedObjects: sharedObjects,
+        otherResources: otherResources);
+  }
+
+  /// A special fake test file for representing a VM unit test written in C++.
+  TestFile.vmUnitTest(
+      {this.hasSyntaxError,
+      this.hasCompileError,
+      this.hasRuntimeError,
+      this.hasStaticWarning,
+      this.hasCrash})
+      : packageRoot = null,
+        packages = null,
+        environment = null,
+        isMultitest = false,
+        isMultiHtmlTest = false,
+        subtestNames = [],
+        sharedOptions = [],
+        dartOptions = [],
+        dart2jsOptions = [],
+        ddcOptions = [],
+        vmOptions = [],
+        sharedObjects = [],
+        otherResources = [],
+        super(null, null);
+
+  TestFile._(Path suiteDirectory, Path path,
+      {this.packageRoot,
+      this.packages,
+      this.environment,
+      this.isMultitest,
+      this.isMultiHtmlTest,
+      this.hasSyntaxError,
+      this.hasCompileError,
+      this.hasRuntimeError,
+      this.hasStaticWarning,
+      this.hasCrash,
+      this.subtestNames,
+      this.sharedOptions,
+      this.dartOptions,
+      this.dart2jsOptions,
+      this.ddcOptions,
+      this.vmOptions,
+      this.sharedObjects,
+      this.otherResources})
+      : super(suiteDirectory, path) {
+    assert(!isMultitest || dartOptions.isEmpty);
+  }
+
+  Path get originPath => path;
+
+  /// The name of the multitest section this file corresponds to if it was
+  /// generated from a multitest. Otherwise, returns an empty string.
+  String get multitestKey => "";
+
+  final String packageRoot;
+  final String packages;
+
+  final Map<String, String> environment;
+
+  final bool isMultitest;
+  final bool isMultiHtmlTest;
+  final bool hasSyntaxError;
+  final bool hasCompileError;
+  final bool hasRuntimeError;
+  final bool hasStaticWarning;
+  final bool hasCrash;
+
+  final List<String> subtestNames;
+  final List<String> sharedOptions;
+  final List<String> dartOptions;
+  final List<String> dart2jsOptions;
+  final List<String> ddcOptions;
+  final List<List<String>> vmOptions;
+  final List<String> sharedObjects;
+  final List<String> otherResources;
+
+  /// Derive a multitest test section file from this multitest file with the
+  /// given [multitestKey] and expectations.
+  TestFile split(Path path, String multitestKey,
+          {bool hasCompileError,
+          bool hasRuntimeError,
+          bool hasStaticWarning,
+          bool hasSyntaxError}) =>
+      _MultitestFile(this, path, multitestKey,
+          hasCompileError: hasCompileError,
+          hasRuntimeError: hasRuntimeError,
+          hasStaticWarning: hasStaticWarning,
+          hasSyntaxError: hasSyntaxError);
+
+  String toString() => """TestFile(
+  packageRoot: $packageRoot
+  packages: $packages
+  environment: $environment
+  isMultitest: $isMultitest
+  isMultiHtmlTest: $isMultiHtmlTest
+  hasSyntaxError: $hasSyntaxError
+  hasCompileError: $hasCompileError
+  hasRuntimeError: $hasRuntimeError
+  hasStaticWarning: $hasStaticWarning
+  hasCrash: $hasCrash
+  subtestNames: $subtestNames
+  sharedOptions: $sharedOptions
+  dartOptions: $dartOptions
+  dart2jsOptions: $dart2jsOptions
+  ddcOptions: $ddcOptions
+  vmOptions: $vmOptions
+  sharedObjects: $sharedObjects
+  otherResources: $otherResources
+)""";
+}
+
+/// A [TestFile] for a single section file derived from a multitest.
+///
+/// This inherits most properties from the original test file, but overrides
+/// the error flags based on the multitest section's expectation.
+class _MultitestFile extends _TestFileBase implements TestFile {
+  /// The authored test file that was split to generate this multitest.
+  final TestFile _origin;
+
+  final String multitestKey;
+
+  final bool hasCompileError;
+  final bool hasRuntimeError;
+  final bool hasStaticWarning;
+  final bool hasSyntaxError;
+  bool get hasCrash => _origin.hasCrash;
+
+  _MultitestFile(this._origin, Path path, this.multitestKey,
+      {this.hasCompileError,
+      this.hasRuntimeError,
+      this.hasStaticWarning,
+      this.hasSyntaxError})
+      : super(_origin._suiteDirectory, path);
+
+  Path get originPath => _origin.path;
+
+  String get packageRoot => _origin.packageRoot;
+  String get packages => _origin.packages;
+
+  List<String> get dart2jsOptions => _origin.dart2jsOptions;
+  List<String> get dartOptions => _origin.dartOptions;
+  List<String> get ddcOptions => _origin.ddcOptions;
+  Map<String, String> get environment => _origin.environment;
+
+  bool get isMultiHtmlTest => _origin.isMultiHtmlTest;
+  bool get isMultitest => _origin.isMultitest;
+
+  List<String> get otherResources => _origin.otherResources;
+  List<String> get sharedObjects => _origin.sharedObjects;
+  List<String> get sharedOptions => _origin.sharedOptions;
+  List<String> get subtestNames => _origin.subtestNames;
+  List<List<String>> get vmOptions => _origin.vmOptions;
+
+  TestFile split(Path path, String multitestKey,
+          {bool hasCompileError,
+          bool hasRuntimeError,
+          bool hasStaticWarning,
+          bool hasSyntaxError}) =>
+      throw UnsupportedError(
+          "Can't derive a test from one already derived from a multitest.");
+}
diff --git a/pkg/test_runner/lib/src/test_suite.dart b/pkg/test_runner/lib/src/test_suite.dart
index 69e09be..5883b16 100644
--- a/pkg/test_runner/lib/src/test_suite.dart
+++ b/pkg/test_runner/lib/src/test_suite.dart
@@ -26,15 +26,10 @@
 import 'summary_report.dart';
 import 'test_case.dart';
 import 'test_configurations.dart';
+import 'test_file.dart';
 import 'testing_servers.dart';
 import 'utils.dart';
 
-RegExp _multiHtmlTestGroupRegExp = RegExp(r"\s*[^/]\s*group\('[^,']*");
-RegExp _multiHtmlTestRegExp = RegExp(r"useHtmlIndividualConfiguration\(\)");
-
-/// Require at least one non-space character before '//[/#]'.
-RegExp _multiTestRegExp = RegExp(r"\S *//[#/] \w+:(.*)");
-
 typedef TestCaseEvent = void Function(TestCase testCase);
 
 /// A simple function that tests [arg] and returns `true` or `false`.
@@ -151,20 +146,20 @@
   /// cache information about the test suite, so that directories do not need
   /// to be listed each time.
   Future forEachTest(
-      TestCaseEvent onTest, Map<String, List<TestInformation>> testCache,
+      TestCaseEvent onTest, Map<String, List<TestFile>> testCache,
       [VoidFunction onDone]);
 
-  /// This function will be called for every TestCase of this test suite.
-  /// It will:
-  ///  - handle sharding
-  ///  - update SummaryReport
-  ///  - handle SKIP/SKIP_BY_DESIGN markers
-  ///  - test if the selector matches
-  /// and will enqueue the test (if necessary).
-  void enqueueNewTestCase(
-      String testName, List<Command> commands, Set<Expectation> expectations,
-      [TestInformation info]) {
-    var displayName = '$suiteName/$testName';
+  /// This function is called for every TestCase of this test suite. It:
+  ///
+  /// - Handles sharding.
+  /// - Updates [SummaryReport].
+  /// - Handle skip markers.
+  /// - Tests if the selector matches.
+  ///
+  /// and enqueue the test if necessary.
+  void enqueueNewTestCase(TestFile testFile, String fullName,
+      List<Command> commands, Set<Expectation> expectations) {
+    var displayName = '$suiteName/$fullName';
 
     // If the test is not going to be run at all, then a RuntimeError,
     // MissingRuntimeError or Timeout will never occur.
@@ -178,9 +173,9 @@
       if (expectations.isEmpty) expectations.add(Expectation.pass);
     }
 
-    var negative = info != null ? isNegative(info) : false;
+    var negative = testFile != null ? isNegative(testFile) : false;
     var testCase = TestCase(displayName, commands, configuration, expectations,
-        info: info);
+        testFile: testFile);
     if (negative &&
         configuration.runtimeConfiguration.shouldSkipNegativeTests) {
       return;
@@ -213,12 +208,12 @@
       }
     }
 
-    // Update Summary report
+    // Update Summary report.
     if (configuration.printReport) {
       summaryReport.add(testCase);
     }
 
-    // Handle skipped tests
+    // Handle skipped tests.
     if (expectations.contains(Expectation.skip) ||
         expectations.contains(Expectation.skipByDesign) ||
         expectations.contains(Expectation.skipSlow)) {
@@ -236,9 +231,9 @@
     doTest(testCase);
   }
 
-  bool isNegative(TestInformation info) =>
-      info.hasCompileError ||
-      info.hasRuntimeError && configuration.runtime != Runtime.none;
+  bool isNegative(TestFile testFile) =>
+      testFile.hasCompileError ||
+      testFile.hasRuntimeError && configuration.runtime != Runtime.none;
 
   String createGeneratedTestDirectoryHelper(
       String name, String dirname, Path testPath) {
@@ -258,24 +253,6 @@
         .replaceAll('\\', '/');
   }
 
-  String buildTestCaseDisplayName(Path suiteDir, Path originTestPath,
-      {String multitestName = ""}) {
-    Path testNamePath = originTestPath.relativeTo(suiteDir);
-    var directory = testNamePath.directoryPath;
-    var filenameWithoutExt = testNamePath.filenameWithoutExtension;
-
-    String concat(String base, String part) {
-      if (base == "") return part;
-      if (part == "") return base;
-      return "$base/$part";
-    }
-
-    var testName = "$directory";
-    testName = concat(testName, "$filenameWithoutExt");
-    testName = concat(testName, multitestName);
-    return testName;
-  }
-
   /// Create a directories for generated assets (tests, html files,
   /// pubspec checkouts ...).
   String createOutputDirectory(Path testPath) {
@@ -364,11 +341,11 @@
   }
 
   void _addTest(ExpectationSet testExpectations, VMUnitTest test) {
-    final fullName = 'cc/${test.name}';
+    var fullName = 'cc/${test.name}';
     var expectations = testExpectations.expectations(fullName);
 
     // Get the expectation from the cc/ test itself.
-    final Expectation testExpectation = Expectation.find(test.expectation);
+    var testExpectation = Expectation.find(test.expectation);
 
     // Update the legacy status-file based expectations to include
     // [testExpectation].
@@ -378,24 +355,19 @@
     }
 
     // Update the new workflow based expectations to include [testExpectation].
-    final Path filePath = null;
-    final Path originTestPath = null;
-    final hasSyntaxError = false;
-    final hasStaticWarning = false;
-    final hasCompileTimeError = testExpectation == Expectation.compileTimeError;
-    final hasRuntimeError = testExpectation == Expectation.runtimeError;
-    final hasCrash = testExpectation == Expectation.crash;
-    final optionsFromFile = const <String, dynamic>{};
-    final testInfo = TestInformation(filePath, originTestPath, optionsFromFile,
-        hasSyntaxError, hasCompileTimeError, hasRuntimeError, hasStaticWarning,
-        hasCrash: hasCrash);
+    var testFile = TestFile.vmUnitTest(
+        hasSyntaxError: false,
+        hasCompileError: testExpectation == Expectation.compileTimeError,
+        hasRuntimeError: testExpectation == Expectation.runtimeError,
+        hasStaticWarning: false,
+        hasCrash: testExpectation == Expectation.crash);
 
     var args = configuration.standardOptions.toList();
     if (configuration.compilerConfiguration.previewDart2) {
-      final filename = configuration.architecture == Architecture.x64
+      var filename = configuration.architecture == Architecture.x64
           ? '$buildDir/gen/kernel-service.dart.snapshot'
           : '$buildDir/gen/kernel_service.dill';
-      final dfePath = Path(filename).absolute.toNativePath();
+      var dfePath = Path(filename).absolute.toNativePath();
       // '--dfe' has to be the first argument for run_vm_test to pick it up.
       args.insert(0, '--dfe=$dfePath');
     }
@@ -405,9 +377,9 @@
 
     args.add(test.name);
 
-    final command = Command.process(
+    var command = Command.process(
         'run_vm_unittest', targetRunnerPath, args, environmentOverrides);
-    enqueueNewTestCase(fullName, [command], expectations, testInfo);
+    enqueueNewTestCase(testFile, fullName, [command], expectations);
   }
 
   Future<Iterable<VMUnitTest>> _listTests(String runnerPath) async {
@@ -435,37 +407,12 @@
   VMUnitTest(this.name, this.expectation);
 }
 
-class TestInformation {
-  Path filePath;
-  Path originTestPath;
-  Map<String, dynamic> optionsFromFile;
-  bool hasSyntaxError;
-  bool hasCompileError;
-  bool hasRuntimeError;
-  bool hasStaticWarning;
-  bool hasCrash;
-  String multitestKey;
-
-  TestInformation(
-      this.filePath,
-      this.originTestPath,
-      this.optionsFromFile,
-      this.hasSyntaxError,
-      this.hasCompileError,
-      this.hasRuntimeError,
-      this.hasStaticWarning,
-      {this.multitestKey = '',
-      this.hasCrash = false}) {
-    assert(filePath.isAbsolute);
-  }
-}
-
 /// A standard [TestSuite] implementation that searches for tests in a
 /// directory, and creates [TestCase]s that compile and/or run them.
 class StandardTestSuite extends TestSuite {
   final Path suiteDir;
   ExpectationSet testExpectations;
-  List<TestInformation> cachedTests;
+  List<TestFile> cachedTests;
   final Path dartDir;
   final bool listRecursively;
   final List<String> extraVmOptions;
@@ -581,19 +528,18 @@
 
   List<String> additionalOptions(Path filePath) => [];
 
-  Future forEachTest(
-      Function onTest, Map<String, List<TestInformation>> testCache,
+  Future forEachTest(Function onTest, Map<String, List<TestFile>> testCache,
       [VoidFunction onDone]) async {
     doTest = onTest;
     testExpectations = readExpectations();
 
     // Check if we have already found and generated the tests for this suite.
     if (!testCache.containsKey(suiteName)) {
-      cachedTests = testCache[suiteName] = <TestInformation>[];
+      cachedTests = testCache[suiteName] = <TestFile>[];
       await enqueueTests();
     } else {
-      for (var info in testCache[suiteName]) {
-        enqueueTestCaseFromTestInformation(info);
+      for (var testFile in testCache[suiteName]) {
+        enqueueTestCaseFromTestFile(testFile);
       }
     }
     testExpectations = null;
@@ -638,93 +584,73 @@
     group.add(lister);
   }
 
-  void enqueueFile(String filename, FutureGroup group) {
+  void enqueueFile(String filePath, FutureGroup group) {
     // This is an optimization to avoid scanning and generating extra tests.
     // The definitive check against configuration.testList is performed in
     // TestSuite.enqueueNewTestCase().
-    if (_testListPossibleFilenames?.contains(filename) == false) return;
+    if (_testListPossibleFilenames?.contains(filePath) == false) return;
+
     // Note: have to use Path instead of a filename for matching because
     // on Windows we need to convert backward slashes to forward slashes.
     // Our display test names (and filters) are given using forward slashes
     // while filenames on Windows use backwards slashes.
-    final Path filePath = Path(filename);
-    if (!_selectorFilenameRegExp.hasMatch(filePath.toString())) return;
+    if (!_selectorFilenameRegExp.hasMatch(Path(filePath).toString())) return;
 
-    if (!isTestFile(filename)) return;
+    if (!isTestFile(filePath)) return;
 
-    var optionsFromFile = readOptionsFromFile(Uri.file(filename));
-    CreateTest createTestCase = makeTestCaseCreator(optionsFromFile);
+    var testFile = TestFile.read(suiteDir, filePath);
 
-    if (optionsFromFile['isMultitest'] as bool) {
-      group.add(doMultitest(filePath, buildDir, suiteDir, createTestCase,
-          configuration.hotReload || configuration.hotReloadRollback));
+    if (testFile.isMultitest) {
+      group.add(splitMultitest(testFile, buildDir, suiteDir,
+              hotReload:
+                  configuration.hotReload || configuration.hotReloadRollback)
+          .then((splitTests) {
+        for (var test in splitTests) {
+          cachedTests.add(test);
+          enqueueTestCaseFromTestFile(test);
+        }
+      }));
     } else {
-      createTestCase(filePath, filePath,
-          hasSyntaxError: optionsFromFile['hasSyntaxError'] as bool,
-          hasCompileError: optionsFromFile['hasCompileError'] as bool,
-          hasRuntimeError: optionsFromFile['hasRuntimeError'] as bool,
-          hasStaticWarning: optionsFromFile['hasStaticWarning'] as bool);
+      cachedTests.add(testFile);
+      enqueueTestCaseFromTestFile(testFile);
     }
   }
 
-  void enqueueTestCaseFromTestInformation(TestInformation info) {
-    String testName = buildTestCaseDisplayName(suiteDir, info.originTestPath,
-        multitestName: info.optionsFromFile['isMultitest'] as bool
-            ? info.multitestKey
-            : "");
-    var optionsFromFile = info.optionsFromFile;
-
-    // If this test is inside a package, we will check if there is a
-    // pubspec.yaml file and if so, create a custom package root for it.
-    Path packageRoot;
-    Path packages;
-
-    if (optionsFromFile['packageRoot'] == null &&
-        optionsFromFile['packages'] == null) {
-      if (configuration.packageRoot != null) {
-        packageRoot = Path(configuration.packageRoot);
-        optionsFromFile['packageRoot'] = packageRoot.toNativePath();
-      }
-      if (configuration.packages != null) {
-        Path packages = Path(configuration.packages);
-        optionsFromFile['packages'] = packages.toNativePath();
-      }
-    }
+  void enqueueTestCaseFromTestFile(TestFile testFile) {
     if (configuration.compilerConfiguration.hasCompiler &&
-        info.hasCompileError) {
+        testFile.hasCompileError) {
       // If a compile-time error is expected, and we're testing a
       // compiler, we never need to attempt to run the program (in a
       // browser or otherwise).
-      enqueueStandardTest(info, testName);
+      enqueueStandardTest(testFile);
     } else if (configuration.runtime.isBrowser) {
       var expectationsMap = <String, Set<Expectation>>{};
 
-      if (info.optionsFromFile['isMultiHtmlTest'] as bool) {
+      if (testFile.isMultiHtmlTest) {
         // A browser multi-test has multiple expectations for one test file.
         // Find all the different sub-test expectations for one entire test
         // file.
-        var subtestNames = info.optionsFromFile['subtestNames'] as List<String>;
+        var subtestNames = testFile.subtestNames;
         expectationsMap = <String, Set<Expectation>>{};
         for (var subtest in subtestNames) {
           expectationsMap[subtest] =
-              testExpectations.expectations('$testName/$subtest');
+              testExpectations.expectations('${testFile.name}/$subtest');
         }
       } else {
-        expectationsMap[testName] = testExpectations.expectations(testName);
+        expectationsMap[testFile.name] =
+            testExpectations.expectations(testFile.name);
       }
 
-      _enqueueBrowserTest(
-          packageRoot, packages, info, testName, expectationsMap);
+      _enqueueBrowserTest(testFile, expectationsMap);
     } else {
-      enqueueStandardTest(info, testName);
+      enqueueStandardTest(testFile);
     }
   }
 
-  void enqueueStandardTest(TestInformation info, String testName) {
-    var commonArguments =
-        commonArgumentsFromFile(info.filePath, info.optionsFromFile);
+  void enqueueStandardTest(TestFile testFile) {
+    var commonArguments = _commonArgumentsFromFile(testFile);
 
-    var vmOptionsList = getVmOptions(info.optionsFromFile);
+    var vmOptionsList = getVmOptions(testFile);
     assert(!vmOptionsList.isEmpty);
 
     for (var vmOptionsVariant = 0;
@@ -736,53 +662,44 @@
         allVmOptions = vmOptions.toList()..addAll(extraVmOptions);
       }
 
-      var expectations = testExpectations.expectations(testName);
+      var expectations = testExpectations.expectations(testFile.name);
       var isCrashExpected = expectations.contains(Expectation.crash);
-      var commands = makeCommands(info, vmOptionsVariant, allVmOptions,
+      var commands = makeCommands(testFile, vmOptionsVariant, allVmOptions,
           commonArguments, isCrashExpected);
-      var variantTestName = testName;
+      var variantTestName = testFile.name;
       if (vmOptionsList.length > 1) {
-        variantTestName = "$testName/$vmOptionsVariant";
+        variantTestName = "${testFile.name}/$vmOptionsVariant";
       }
-      enqueueNewTestCase(variantTestName, commands, expectations, info);
+      enqueueNewTestCase(testFile, variantTestName, commands, expectations);
     }
   }
 
-  List<Command> makeCommands(TestInformation info, int vmOptionsVariant,
+  List<Command> makeCommands(TestFile testFile, int vmOptionsVariant,
       List<String> vmOptions, List<String> args, bool isCrashExpected) {
     var commands = <Command>[];
     var compilerConfiguration = configuration.compilerConfiguration;
-    var sharedOptions = info.optionsFromFile['sharedOptions'] as List<String>;
-    var dartOptions = info.optionsFromFile['dartOptions'] as List<String>;
-    var dart2jsOptions = info.optionsFromFile['dart2jsOptions'] as List<String>;
-    var ddcOptions = info.optionsFromFile['ddcOptions'] as List<String>;
-
-    var isMultitest = info.optionsFromFile["isMultitest"] as bool;
-    assert(!isMultitest || dartOptions.isEmpty);
 
     var compileTimeArguments = <String>[];
     String tempDir;
     if (compilerConfiguration.hasCompiler) {
       compileTimeArguments = compilerConfiguration.computeCompilerArguments(
           vmOptions,
-          sharedOptions,
-          dartOptions,
-          dart2jsOptions,
-          ddcOptions,
+          testFile.sharedOptions,
+          testFile.dartOptions,
+          testFile.dart2jsOptions,
+          testFile.ddcOptions,
           args);
       // Avoid doing this for analyzer.
-      var path = info.filePath;
+      var path = testFile.path;
       if (vmOptionsVariant != 0) {
         // Ensure a unique directory for each test case.
         path = path.join(Path(vmOptionsVariant.toString()));
       }
       tempDir = createCompilationOutputDirectory(path);
 
-      var otherResources =
-          info.optionsFromFile['otherResources'] as List<String>;
-      for (var name in otherResources) {
+      for (var name in testFile.otherResources) {
         var namePath = Path(name);
-        var fromPath = info.filePath.directoryPath.join(namePath);
+        var fromPath = testFile.path.directoryPath.join(namePath);
         File('$tempDir/$name').parent.createSync(recursive: true);
         File(fromPath.toNativePath()).copySync('$tempDir/$name');
       }
@@ -794,7 +711,7 @@
       commands.addAll(compilationArtifact.commands);
     }
 
-    if (info.hasCompileError &&
+    if (testFile.hasCompileError &&
         compilerConfiguration.hasCompiler &&
         !compilerConfiguration.runRuntimeDespiteMissingCompileTimeError) {
       // Do not attempt to run the compiled result. A compilation
@@ -809,44 +726,22 @@
 
     var runtimeArguments = compilerConfiguration.computeRuntimeArguments(
         configuration.runtimeConfiguration,
-        info,
+        testFile,
         vmOptions,
-        sharedOptions,
-        dartOptions,
         args,
         compilationArtifact);
 
-    var environment = environmentOverrides;
-    var extraEnv = info.optionsFromFile['environment'] as Map<String, String>;
-    if (extraEnv != null) {
-      environment = {...environment, ...extraEnv};
-    }
+    var environment = {...environmentOverrides, ...?testFile.environment};
 
     return commands
       ..addAll(configuration.runtimeConfiguration.computeRuntimeCommands(
           compilationArtifact,
           runtimeArguments,
           environment,
-          info.optionsFromFile["sharedObjects"] as List<String>,
+          testFile.sharedObjects,
           isCrashExpected));
   }
 
-  CreateTest makeTestCaseCreator(Map<String, dynamic> optionsFromFile) {
-    return (Path filePath, Path originTestPath,
-        {bool hasSyntaxError,
-        bool hasCompileError,
-        bool hasRuntimeError,
-        bool hasStaticWarning = false,
-        String multitestKey}) {
-      // Cache the test information for each test case.
-      var info = TestInformation(filePath, originTestPath, optionsFromFile,
-          hasSyntaxError, hasCompileError, hasRuntimeError, hasStaticWarning,
-          multitestKey: multitestKey);
-      cachedTests.add(info);
-      enqueueTestCaseFromTestInformation(info);
-    };
-  }
-
   /// Takes a [file], which is either located in the dart or in the build
   /// directory, and returns a String representing the relative path to either
   /// the dart or the build directory.
@@ -907,26 +802,20 @@
   ///
   /// In order to handle browser multitests, [expectations] is a map of subtest
   /// names to expectation sets. If the test is not a multitest, the map has
-  /// a single key, [testName].
+  /// a single key, [testFile.name].
   void _enqueueBrowserTest(
-      Path packageRoot,
-      Path packages,
-      TestInformation info,
-      String testName,
-      Map<String, Set<Expectation>> expectations) {
-    var tempDir = createOutputDirectory(info.filePath);
-    var fileName = info.filePath.toNativePath();
-    var optionsFromFile = info.optionsFromFile;
-    var compilationTempDir = createCompilationOutputDirectory(info.filePath);
-    var nameNoExt = info.filePath.filenameWithoutExtension;
+      TestFile testFile, Map<String, Set<Expectation>> expectations) {
+    var tempDir = createOutputDirectory(testFile.path);
+    var compilationTempDir = createCompilationOutputDirectory(testFile.path);
+    var nameNoExt = testFile.path.filenameWithoutExtension;
     var outputDir = compilationTempDir;
-    var commonArguments =
-        commonArgumentsFromFile(info.filePath, optionsFromFile);
+
+    var commonArguments = _commonArgumentsFromFile(testFile);
 
     // Use existing HTML document if available.
     String content;
     var customHtml = File(
-        info.filePath.directoryPath.append('$nameNoExt.html').toNativePath());
+        testFile.path.directoryPath.append('$nameNoExt.html').toNativePath());
     if (customHtml.existsSync()) {
       outputDir = tempDir;
       content = customHtml.readAsStringSync().replaceAll(
@@ -936,7 +825,7 @@
       if (configuration.compiler == Compiler.dart2js) {
         var scriptPath =
             _createUrlPathFromFile(Path('$compilationTempDir/$nameNoExt.js'));
-        content = dart2jsHtml(fileName, scriptPath);
+        content = dart2jsHtml(testFile.path.toNativePath(), scriptPath);
       } else {
         var jsDir =
             Path(compilationTempDir).relativeTo(Repository.dir).toString();
@@ -956,33 +845,40 @@
       Compiler.dartdevk
     };
     assert(supportedCompilers.contains(configuration.compiler));
-    var sharedOptions = optionsFromFile["sharedOptions"] as List<String>;
-    var dart2jsOptions = optionsFromFile["dart2jsOptions"] as List<String>;
-    var ddcOptions = optionsFromFile["ddcOptions"] as List<String>;
 
     var args = configuration.compilerConfiguration.computeCompilerArguments(
-        null, sharedOptions, null, dart2jsOptions, ddcOptions, commonArguments);
+        null,
+        testFile.sharedOptions,
+        null,
+        testFile.dart2jsOptions,
+        testFile.ddcOptions,
+        commonArguments);
     var compilation = configuration.compilerConfiguration
         .computeCompilationArtifact(outputDir, args, environmentOverrides);
     commands.addAll(compilation.commands);
 
-    if (info.optionsFromFile['isMultiHtmlTest'] as bool) {
+    if (testFile.isMultiHtmlTest) {
       // Variables for browser multi-tests.
-      var subtestNames = info.optionsFromFile['subtestNames'] as List<String>;
+      var subtestNames = testFile.subtestNames;
       for (var subtestName in subtestNames) {
-        _enqueueSingleBrowserTest(commands, info, '$testName/$subtestName',
-            subtestName, expectations[subtestName], htmlPath);
+        _enqueueSingleBrowserTest(
+            commands,
+            testFile,
+            '${testFile.name}/$subtestName',
+            subtestName,
+            expectations[subtestName],
+            htmlPath);
       }
     } else {
-      _enqueueSingleBrowserTest(
-          commands, info, testName, null, expectations[testName], htmlPath);
+      _enqueueSingleBrowserTest(commands, testFile, testFile.name, null,
+          expectations[testFile.name], htmlPath);
     }
   }
 
   /// Enqueues a single browser test, or a single subtest of an HTML multitest.
   void _enqueueSingleBrowserTest(
       List<Command> commands,
-      TestInformation info,
+      TestFile testFile,
       String testName,
       String subtestName,
       Set<Expectation> expectations,
@@ -994,293 +890,61 @@
     var fullHtmlPath = _uriForBrowserTest(htmlPathSubtest, subtestName);
 
     commands.add(Command.browserTest(fullHtmlPath, configuration,
-        retry: !isNegative(info)));
+        retry: !isNegative(testFile)));
 
     var fullName = testName;
     if (subtestName != null) fullName += "/$subtestName";
-    enqueueNewTestCase(fullName, commands, expectations, info);
+    enqueueNewTestCase(testFile, fullName, commands, expectations);
   }
 
-  List<String> commonArgumentsFromFile(
-      Path filePath, Map<String, dynamic> optionsFromFile) {
+  List<String> _commonArgumentsFromFile(TestFile testFile) {
     var args = configuration.standardOptions.toList();
 
-    var packages = packagesArgument(optionsFromFile['packageRoot'] as String,
-        optionsFromFile['packages'] as String);
+    var packages = packagesArgument(testFile.packageRoot, testFile.packages);
     if (packages != null) {
       args.add(packages);
     }
-    args.addAll(additionalOptions(filePath));
+    args.addAll(additionalOptions(testFile.path));
     if (configuration.compiler == Compiler.dart2analyzer) {
       args.add('--format=machine');
       args.add('--no-hints');
 
-      if (filePath.filename.contains("dart2js") ||
-          filePath.directoryPath.segments().last.contains('html_common')) {
+      if (testFile.path.filename.contains("dart2js") ||
+          testFile.path.directoryPath.segments().last.contains('html_common')) {
         args.add("--use-dart2js-libraries");
       }
     }
 
-    args.add(filePath.toNativePath());
+    args.add(testFile.path.toNativePath());
 
     return args;
   }
 
-  String packagesArgument(String packageRootFromFile, String packagesFromFile) {
-    if (packageRootFromFile == 'none' || packagesFromFile == 'none') {
+  String packagesArgument(String packageRoot, String packages) {
+    // If this test is inside a package, we will check if there is a
+    // pubspec.yaml file and if so, create a custom package root for it.
+    if (packageRoot == null && packages == null) {
+      if (configuration.packageRoot != null) {
+        packageRoot = Path(configuration.packageRoot).toNativePath();
+      }
+
+      if (configuration.packages != null) {
+        packages = Path(configuration.packages).toNativePath();
+      }
+    }
+
+    if (packageRoot == 'none' || packages == 'none') {
       return null;
-    } else if (packagesFromFile != null) {
-      return '--packages=$packagesFromFile';
-    } else if (packageRootFromFile != null) {
-      return '--package-root=$packageRootFromFile';
+    } else if (packages != null) {
+      return '--packages=$packages';
+    } else if (packageRoot != null) {
+      return '--package-root=$packageRoot';
     } else {
       return null;
     }
   }
 
-  /// Special options for individual tests are currently specified in various
-  /// ways: with comments directly in test files, by using certain imports, or
-  /// by creating additional files in the test directories.
-  ///
-  /// Here is a list of options that are used by 'test.dart' today:
-  ///   - Flags can be passed to the vm process that runs the test by adding a
-  ///   comment to the test file:
-  ///
-  ///     // VMOptions=--flag1 --flag2
-  ///
-  ///   - Flags can be passed to dart2js, vm or dartdevc by adding a comment to
-  ///   the test file:
-  ///
-  ///     // SharedOptions=--flag1 --flag2
-  ///
-  ///   - Flags can be passed to dart2js by adding a comment to the test file:
-  ///
-  ///     // dart2jsOptions=--flag1 --flag2
-  ///
-  ///   - Flags can be passed to the dart script that contains the test also
-  ///   using comments, as follows:
-  ///
-  ///     // DartOptions=--flag1 --flag2
-  ///
-  ///   - Extra environment variables can be passed to the process that runs
-  ///   the test by adding comment(s) to the test file:
-  ///
-  ///     // Environment=ENV_VAR1=foo bar
-  ///     // Environment=ENV_VAR2=bazz
-  ///
-  ///   - Most tests are not web tests, but can (and will be) wrapped within
-  ///   an HTML file and another script file to test them also on browser
-  ///   environments (e.g. language and corelib tests are run this way).
-  ///   We deduce that if a file with the same name as the test, but ending in
-  ///   .html instead of .dart exists, the test was intended to be a web test
-  ///   and no wrapping is necessary.
-  ///
-  ///     // SharedObjects=foobar
-  ///
-  ///   - This test requires libfoobar.so, libfoobar.dylib or foobar.dll to be
-  ///   in the system linker path of the VM.
-  ///
-  ///   - 'test.dart' assumes tests fail if
-  ///   the process returns a non-zero exit code (in the case of web tests, we
-  ///   check for PASS/FAIL indications in the test output).
-  ///
-  /// This method is static as the map is cached and shared amongst
-  /// configurations, so it may not use [configuration].
-  Map<String, dynamic> readOptionsFromFile(Uri uri) {
-    if (uri.path.endsWith('.dill')) {
-      return optionsFromKernelFile();
-    }
-    var testOptionsRegExp = RegExp(r"// VMOptions=(.*)");
-    var environmentRegExp = RegExp(r"// Environment=(.*)");
-    var otherResourcesRegExp = RegExp(r"// OtherResources=(.*)");
-    var sharedObjectsRegExp = RegExp(r"// SharedObjects=(.*)");
-    var packageRootRegExp = RegExp(r"// PackageRoot=(.*)");
-    var packagesRegExp = RegExp(r"// Packages=(.*)");
-    var isolateStubsRegExp = RegExp(r"// IsolateStubs=(.*)");
-    // TODO(gram) Clean these up once the old directives are not supported.
-    var domImportRegExp = RegExp(
-        r"^[#]?import.*dart:(html|web_audio|indexed_db|svg|web_sql)",
-        multiLine: true);
-
-    var bytes = File.fromUri(uri).readAsBytesSync();
-    var contents = decodeUtf8(bytes);
-    bytes = null;
-
-    // Find the options in the file.
-    var result = <List<String>>[];
-    List<String> dartOptions;
-    List<String> sharedOptions;
-    List<String> dart2jsOptions;
-    List<String> ddcOptions;
-    Map<String, String> environment;
-    String packageRoot;
-    String packages;
-
-    List<String> wordSplit(String s) =>
-        s.split(' ').where((e) => e != '').toList();
-
-    List<String> singleListOfOptions(String name) {
-      var matches = RegExp('// $name=(.*)').allMatches(contents);
-      List<String> options;
-      for (var match in matches) {
-        if (options != null) {
-          throw Exception(
-              'More than one "// $name=" line in test ${uri.toFilePath()}');
-        }
-        options = wordSplit(match[1]);
-      }
-      return options;
-    }
-
-    var matches = testOptionsRegExp.allMatches(contents);
-    for (var match in matches) {
-      result.add(wordSplit(match[1]));
-    }
-    if (result.isEmpty) result.add(<String>[]);
-
-    dartOptions = singleListOfOptions('DartOptions');
-    sharedOptions = singleListOfOptions('SharedOptions');
-    dart2jsOptions = singleListOfOptions('dart2jsOptions');
-    ddcOptions = singleListOfOptions('dartdevcOptions');
-
-    matches = environmentRegExp.allMatches(contents);
-    for (var match in matches) {
-      var envDef = match[1];
-      var pos = envDef.indexOf('=');
-      var name = (pos < 0) ? envDef : envDef.substring(0, pos);
-      var value = (pos < 0) ? '' : envDef.substring(pos + 1);
-      environment ??= <String, String>{};
-      environment[name] = value;
-    }
-
-    matches = packageRootRegExp.allMatches(contents);
-    for (var match in matches) {
-      if (packageRoot != null || packages != null) {
-        throw Exception(
-            'More than one "// Package... line in test ${uri.toFilePath()}');
-      }
-      packageRoot = match[1];
-      if (packageRoot != 'none') {
-        // PackageRoot=none means that no packages or package-root option
-        // should be given. Any other value overrides package-root and
-        // removes any packages option.  Don't use with // Packages=.
-        packageRoot = uri.resolveUri(Uri.directory(packageRoot)).toFilePath();
-      }
-    }
-
-    matches = packagesRegExp.allMatches(contents);
-    for (var match in matches) {
-      if (packages != null || packageRoot != null) {
-        throw Exception(
-            'More than one "// Package..." line in test ${uri.toFilePath()}');
-      }
-      packages = match[1];
-      if (packages != 'none') {
-        // Packages=none means that no packages or package-root option
-        // should be given. Any other value overrides packages and removes
-        // any package-root option. Don't use with // PackageRoot=.
-        packages = uri.resolveUri(Uri.file(packages)).toFilePath();
-      }
-    }
-
-    var otherResources = <String>[];
-    matches = otherResourcesRegExp.allMatches(contents);
-    for (var match in matches) {
-      otherResources.addAll(wordSplit(match[1]));
-    }
-
-    var sharedObjects = <String>[];
-    matches = sharedObjectsRegExp.allMatches(contents);
-    for (var match in matches) {
-      sharedObjects.addAll(wordSplit(match[1]));
-    }
-
-    var isMultitest = _multiTestRegExp.hasMatch(contents);
-    var isMultiHtmlTest = _multiHtmlTestRegExp.hasMatch(contents);
-    var isolateMatch = isolateStubsRegExp.firstMatch(contents);
-    var isolateStubs = isolateMatch != null ? isolateMatch[1] : '';
-    var containsDomImport = domImportRegExp.hasMatch(contents);
-
-    var subtestNames = <String>[];
-    var matchesIter = _multiHtmlTestGroupRegExp.allMatches(contents).iterator;
-    while (matchesIter.moveNext() && isMultiHtmlTest) {
-      var fullMatch = matchesIter.current.group(0);
-      subtestNames.add(fullMatch.substring(fullMatch.indexOf("'") + 1));
-    }
-
-    // TODO(rnystrom): During the migration of the existing tests to Dart 2.0,
-    // we have a number of tests that used to both generate static type warnings
-    // and also validate some runtime behavior in an implementation that
-    // ignores those warnings. Those warnings are now errors. The test code
-    // validates the runtime behavior can and should be removed, but the code
-    // that causes the static warning should still be preserved since that is
-    // part of our coverage of the static type system.
-    //
-    // The test needs to indicate that it should have a static error. We could
-    // put that in the status file, but that makes it confusing because it
-    // would look like implementations that *don't* report the error are more
-    // correct. Eventually, we want to have a notation similar to what front_end
-    // is using for the inference tests where we can put a comment inside the
-    // test that says "This specific static error should be reported right by
-    // this token."
-    //
-    // That system isn't in place yet, so we do a crude approximation here in
-    // test.dart. If a test contains `/*@compile-error=`, which matches the
-    // beginning of the tag syntax that front_end uses, then we assume that
-    // this test must have a static error somewhere in it.
-    //
-    // Redo this code once we have a more precise test framework for detecting
-    // and locating these errors.
-    final hasSyntaxError = contents.contains("@syntax-error");
-    final hasCompileError =
-        hasSyntaxError || contents.contains("@compile-error");
-    final hasRuntimeError = contents.contains("@runtime-error");
-    final hasStaticWarning = contents.contains("@static-warning");
-
-    return {
-      "vmOptions": result,
-      "sharedOptions": sharedOptions ?? <String>[],
-      "dart2jsOptions": dart2jsOptions ?? <String>[],
-      "ddcOptions": ddcOptions ?? <String>[],
-      "dartOptions": dartOptions ?? <String>[],
-      "environment": environment,
-      "packageRoot": packageRoot,
-      "packages": packages,
-      "hasSyntaxError": hasSyntaxError,
-      "hasCompileError": hasCompileError,
-      "hasRuntimeError": hasRuntimeError,
-      "hasStaticWarning": hasStaticWarning,
-      "otherResources": otherResources,
-      "sharedObjects": sharedObjects,
-      "isMultitest": isMultitest,
-      "isMultiHtmlTest": isMultiHtmlTest,
-      "subtestNames": subtestNames,
-      "isolateStubs": isolateStubs,
-      "containsDomImport": containsDomImport
-    };
-  }
-
-  Map<String, dynamic> optionsFromKernelFile() {
-    return const {
-      "vmOptions": [<String>[]],
-      "sharedOptions": <String>[],
-      "dart2jsOptions": <String>[],
-      "dartOptions": <String>[],
-      "packageRoot": null,
-      "packages": null,
-      "hasSyntaxError": false,
-      "hasCompileError": false,
-      "hasRuntimeError": false,
-      "hasStaticWarning": false,
-      "isMultitest": false,
-      "isMultiHtmlTest": false,
-      "subtestNames": [],
-      "isolateStubs": '',
-      "containsDomImport": false,
-    };
-  }
-
-  List<List<String>> getVmOptions(Map<String, dynamic> optionsFromFile) {
+  List<List<String>> getVmOptions(TestFile testFile) {
     const compilers = [
       Compiler.none,
       Compiler.dartk,
@@ -1294,7 +958,7 @@
     var needsVmOptions = compilers.contains(configuration.compiler) &&
         runtimes.contains(configuration.runtime);
     if (!needsVmOptions) return [[]];
-    return optionsFromFile['vmOptions'] as List<List<String>>;
+    return testFile.vmOptions;
   }
 }
 
@@ -1306,21 +970,20 @@
             ["$directoryPath/.status"],
             recursive: true);
 
-  void _enqueueBrowserTest(Path packageRoot, packages, TestInformation info,
-      String testName, Map<String, Set<Expectation>> expectations) {
-    var filePath = info.filePath;
-    var dir = filePath.directoryPath;
-    var nameNoExt = filePath.filenameWithoutExtension;
+  void _enqueueBrowserTest(
+      TestFile testFile, Map<String, Set<Expectation>> expectations) {
+    var dir = testFile.path.directoryPath;
+    var nameNoExt = testFile.path.filenameWithoutExtension;
     var customHtmlPath = dir.append('$nameNoExt.html');
     var customHtml = File(customHtmlPath.toNativePath());
     if (!customHtml.existsSync()) {
-      super._enqueueBrowserTest(
-          packageRoot, packages, info, testName, expectations);
+      super._enqueueBrowserTest(testFile, expectations);
     } else {
       var fullPath = _createUrlPathFromFile(customHtmlPath);
       var command = Command.browserTest(fullPath, configuration,
-          retry: !isNegative(info));
-      enqueueNewTestCase(testName, [command], expectations[testName], info);
+          retry: !isNegative(testFile));
+      enqueueNewTestCase(
+          testFile, testFile.name, [command], expectations[testFile.name]);
     }
   }
 }