| // 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 'package:shell_arg_splitter/shell_arg_splitter.dart'; | |
| import 'feature.dart'; | |
| import 'path.dart'; | |
| import 'static_error.dart'; | |
| import 'utils.dart'; | |
| final _multitestRegExp = RegExp(r"//# \w+:"); | |
| final _vmOptionsRegExp = RegExp(r"^[ \t]*// VMOptions=(.*)", multiLine: true); | |
| final _environmentRegExp = RegExp( | |
| r"^[ \t]*// Environment=(.*)", | |
| multiLine: true, | |
| ); | |
| final _packagesRegExp = RegExp(r"^[ \t]*// Packages=(.*)", multiLine: true); | |
| final _experimentRegExp = RegExp(r"^--enable-experiment=([a-z0-9,-]+)$"); | |
| final _localFileRegExp = RegExp( | |
| r"""^[ \t]*(?:import|part)\s*""" | |
| r"""['"](?!package:|dart:)(.*?)['"]\s*""" | |
| r"""(?:(?:deferred\s+)?as\s+\w+\s*)?""" | |
| r"""(?:(?:show|hide)\s+\w+\s*(?:,\s*\w+\s*))*;""", | |
| multiLine: true, | |
| ); | |
| List<T> _parseOption<T>( | |
| String filePath, | |
| String contents, | |
| String name, | |
| T Function(String) convert, { | |
| bool allowMultiple = false, | |
| }) { | |
| var matches = RegExp( | |
| '^[ \t]*// $name=(.*)', | |
| multiLine: true, | |
| ).allMatches(contents).toList(); | |
| if (!allowMultiple && matches.length > 1) { | |
| throw FormatException('More than one "// $name=" line in test $filePath'); | |
| } | |
| var options = <T>[]; | |
| for (var match in matches) { | |
| for (var option in splitLine(match[1]!)) { | |
| options.add(convert(option)); | |
| } | |
| } | |
| return options; | |
| } | |
| List<String> _parseStringOption( | |
| String filePath, | |
| String contents, | |
| String name, { | |
| bool allowMultiple = false, | |
| }) => _parseOption<String>( | |
| filePath, | |
| contents, | |
| name, | |
| (string) => string, | |
| allowMultiple: allowMultiple, | |
| ); | |
| 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; | |
| /// The parsed error expectation markers in this test, if it is a static | |
| /// error test. | |
| /// | |
| /// If empty, the test is not a static error test. | |
| final List<StaticError> expectedErrors; | |
| /// 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; | |
| /// If the test contains static error expectations, it's a "static error | |
| /// test". | |
| /// | |
| /// These tests exist to validate that a front end reports the right static | |
| /// errors. Unless the expected errors are all warnings, a static error test | |
| /// is skipped on configurations that are not purely front end. | |
| bool get isStaticErrorTest => expectedErrors.isNotEmpty; | |
| /// If the test contains any web-specific (`[web]`) static error expectations, | |
| /// then it's a "web static error test". | |
| /// | |
| /// These tests exist to validate that a Dart web compiler reports the right | |
| /// expected errors. | |
| bool get isWebStaticErrorTest => | |
| expectedErrors.any((error) => error.source == ErrorSource.web); | |
| /// If the tests has no static error expectations, or all of the expectations | |
| /// are warnings, then the test tests runtime semantics. | |
| /// | |
| /// Note that this is *not* the negation of [isStaticErrorTest]. A test that | |
| /// contains only warning expectations is both a static "error" test and a | |
| /// runtime test. The test runner will validate that the front ends produce | |
| /// the expected warnings *and* that a runtime also correctly executes the | |
| /// test. | |
| bool get isRuntimeTest => expectedErrors.every((error) => error.isWarning); | |
| /// A hash code used to spread tests across shards. | |
| int get shardHash { | |
| // The VM C++ unit tests have a special fake TestFile with no suite | |
| // directory or path. Don't crash in that case. | |
| if (_suiteDirectory == null) { | |
| return path.toString().hashCode; | |
| } | |
| return originPath.relativeTo(_suiteDirectory).toString().hashCode; | |
| } | |
| _TestFileBase(this._suiteDirectory, this.path, this.expectedErrors); | |
| /// 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 join(String base, String part) { | |
| if (base.isEmpty) return part; | |
| if (part.isEmpty) return base; | |
| return "$base/$part"; | |
| } | |
| var result = "$directory"; | |
| result = join(result, filenameWithoutExt); | |
| result = join(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 dart2wasm by adding a comment to the test file: | |
| /// | |
| /// // dart2wasmOptions=--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), | |
| [], | |
| requirements: [], | |
| vmOptions: [[]], | |
| sharedOptions: [], | |
| dart2jsOptions: [], | |
| dart2wasmOptions: [], | |
| ddcOptions: [], | |
| dartOptions: [], | |
| packages: null, | |
| isMultitest: false, | |
| sharedObjects: [], | |
| otherResources: [], | |
| environment: {}, | |
| experiments: [], | |
| ); | |
| } | |
| final contents = File(filePath).readAsStringSync(); | |
| // Required features. | |
| var requirements = _parseOption<Feature>( | |
| filePath, | |
| contents, | |
| 'Requirements', | |
| (name) { | |
| for (var feature in Feature.all) { | |
| if (feature.name == name) return feature; | |
| } | |
| throw FormatException('Unknown feature "$name" in test $filePath'); | |
| }, | |
| ); | |
| final isVmIntermediateLanguageTest = filePath.endsWith('_il_test.dart'); | |
| // VM options. | |
| var vmOptions = <List<String>>[]; | |
| var matches = _vmOptionsRegExp.allMatches(contents); | |
| for (var match in matches) { | |
| vmOptions.add(splitLine(match[1]!)); | |
| } | |
| if (vmOptions.isEmpty) vmOptions.add(<String>[]); | |
| // Other options. | |
| var dartOptions = _parseStringOption(filePath, contents, 'DartOptions'); | |
| var sharedOptions = _parseStringOption(filePath, contents, 'SharedOptions'); | |
| var dart2jsOptions = _parseStringOption( | |
| filePath, | |
| contents, | |
| 'dart2jsOptions', | |
| ); | |
| var dart2wasmOptions = _parseStringOption( | |
| filePath, | |
| contents, | |
| 'dart2wasmOptions', | |
| ); | |
| var ddcOptions = _parseStringOption(filePath, contents, 'ddcOptions'); | |
| var otherResources = _parseStringOption( | |
| filePath, | |
| contents, | |
| 'OtherResources', | |
| allowMultiple: true, | |
| ); | |
| var sharedObjects = _parseStringOption( | |
| filePath, | |
| contents, | |
| 'SharedObjects', | |
| allowMultiple: true, | |
| ); | |
| // Extract the experiments from the shared options. | |
| // TODO(rnystrom): Either tests should stop specifying experiment flags | |
| // entirely and use "// Requirements=", or we should come up with a better | |
| // syntax. Parsing from "// SharedOptions=" for now since that's where they | |
| // are currently specified. | |
| var experiments = <String>[]; | |
| for (var i = 0; i < sharedOptions.length; i++) { | |
| var sharedOption = sharedOptions[i]; | |
| if (sharedOption.contains("--enable-experiment")) { | |
| var match = _experimentRegExp.firstMatch(sharedOption); | |
| if (match == null) { | |
| throw Exception( | |
| "SharedOptions marker cannot mix experiment flags with other " | |
| "flags. Was:\n$sharedOption", | |
| ); | |
| } | |
| experiments.addAll(match[1]!.split(",")); | |
| sharedOptions.removeAt(i); | |
| i--; | |
| } | |
| } | |
| // Environment. | |
| var environment = <String, String>{}; | |
| matches = _environmentRegExp.allMatches(contents); | |
| for (var match in matches) { | |
| var envDef = match[1]!; | |
| var name = envDef; | |
| var value = ''; | |
| var pos = envDef.indexOf('='); | |
| if (pos >= 0) { | |
| name = envDef.substring(0, pos); | |
| value = envDef.substring(pos + 1); | |
| } | |
| environment[name] = value; | |
| } | |
| // Packages. | |
| String? packages; | |
| matches = _packagesRegExp.allMatches(contents); | |
| for (var match in matches) { | |
| if (packages != null) { | |
| throw FormatException( | |
| 'More than one "// Package..." line in test $filePath', | |
| ); | |
| } | |
| packages = match[1]!; | |
| if (packages != 'none') { | |
| // Packages=none means that no packages option should be given. Any | |
| // other value overrides packages. | |
| packages = Uri.file( | |
| filePath, | |
| ).resolveUri(Uri.file(packages)).toFilePath(); | |
| } | |
| } | |
| var isMultitest = _multitestRegExp.hasMatch(contents); | |
| if (isMultitest) { | |
| DebugLogger.warning( | |
| "${Path(filePath).toNativePath()} is a legacy multi-test file.", | |
| ); | |
| } | |
| var errorExpectations = <StaticError>[]; | |
| // The analyzer package also uses a similar syntax for expectations, but | |
| // we don't want the test_runner to inadvertently think files containing | |
| // those are static error tests. | |
| if (!filePath.replaceAll('\\', '/').contains('pkg/analyzer/')) { | |
| try { | |
| errorExpectations.addAll(_parseExpectations(filePath)); | |
| } on FormatException catch (error) { | |
| throw FormatException( | |
| "Invalid error expectation syntax in $filePath:\n$error", | |
| ); | |
| } | |
| } | |
| return TestFile._( | |
| suiteDirectory, | |
| Path(filePath), | |
| errorExpectations, | |
| packages: packages, | |
| environment: environment, | |
| isMultitest: isMultitest, | |
| requirements: requirements, | |
| sharedOptions: sharedOptions, | |
| dartOptions: dartOptions, | |
| dart2jsOptions: dart2jsOptions, | |
| dart2wasmOptions: dart2wasmOptions, | |
| ddcOptions: ddcOptions, | |
| vmOptions: vmOptions, | |
| sharedObjects: sharedObjects, | |
| otherResources: otherResources, | |
| experiments: experiments, | |
| isVmIntermediateLanguageTest: isVmIntermediateLanguageTest, | |
| ); | |
| } | |
| /// Parse expectations from the file with [path]. | |
| /// | |
| /// Recurses to follow local (not `dart:` or `package:`) imports. | |
| static List<StaticError> _parseExpectations(String path) { | |
| var parsed = <ParsedStaticErrorExpectations>[]; | |
| _parseExpectationFiles(path, parsed: parsed); | |
| return StaticError.attachContextExpectations(parsed, rootPath: path); | |
| } | |
| /// Recursively parses expectation files starting from [path]. | |
| /// | |
| /// This follows local imports and explicit context paths in errors to find | |
| /// all files that might contain expectations for the test. | |
| /// | |
| /// [parsed] accumulates the results. [alreadyParsed] is used to prevent | |
| /// infinite recursion in case of circular references. | |
| static void _parseExpectationFiles( | |
| String path, { | |
| required List<ParsedStaticErrorExpectations> parsed, | |
| Set<String>? alreadyParsed, | |
| }) { | |
| alreadyParsed ??= {}; | |
| var file = File(path); | |
| var pathUri = Uri.parse(path); | |
| // Missing files set no expectations. | |
| if (!file.existsSync()) return; | |
| // Catch import loops. | |
| if (!alreadyParsed.add(StaticError.normalizePath(path))) return; | |
| // Parse one file. | |
| var contents = File(path).readAsStringSync(); | |
| var result = StaticError.parseExpectationsUnattached( | |
| path: path, | |
| source: contents, | |
| ); | |
| parsed.add(result); | |
| // Parse imports and recurse. | |
| var matches = _localFileRegExp.allMatches(contents); | |
| for (var match in matches) { | |
| var localPath = Uri.tryParse(match[1]!); | |
| // Broken import paths set no expectations. | |
| if (localPath == null) continue; | |
| var uriString = pathUri.resolve(localPath.path).toString(); | |
| _parseExpectationFiles( | |
| uriString, | |
| parsed: parsed, | |
| alreadyParsed: alreadyParsed, | |
| ); | |
| } | |
| for (var error in result.errors) { | |
| for (var contextPath in error.contextPaths) { | |
| _parseExpectationFiles( | |
| contextPath, | |
| parsed: parsed, | |
| alreadyParsed: alreadyParsed, | |
| ); | |
| } | |
| } | |
| } | |
| /// A special fake test file for representing a VM unit test written in C++. | |
| TestFile.vmUnitTest( | |
| String name, { | |
| required this.hasCompileError, | |
| required this.hasRuntimeError, | |
| required this.hasCrash, | |
| }) : packages = null, | |
| environment = {}, | |
| isMultitest = false, | |
| hasSyntaxError = false, | |
| hasStaticWarning = false, | |
| requirements = [], | |
| sharedOptions = [], | |
| dartOptions = [], | |
| dart2jsOptions = [], | |
| dart2wasmOptions = [], | |
| ddcOptions = [], | |
| vmOptions = [], | |
| sharedObjects = [], | |
| otherResources = [], | |
| experiments = [], | |
| isVmIntermediateLanguageTest = false, | |
| super(null, Path("/fake/vm/cc/$name"), []); | |
| TestFile._( | |
| Path super.suiteDirectory, | |
| super.path, | |
| super.expectedErrors, { | |
| this.packages, | |
| required this.environment, | |
| required this.isMultitest, | |
| required this.requirements, | |
| required this.sharedOptions, | |
| required this.dartOptions, | |
| required this.dart2jsOptions, | |
| required this.dart2wasmOptions, | |
| required this.ddcOptions, | |
| required this.vmOptions, | |
| required this.sharedObjects, | |
| required this.otherResources, | |
| required this.experiments, | |
| this.isVmIntermediateLanguageTest = false, | |
| }) : hasSyntaxError = false, | |
| hasCompileError = false, | |
| hasRuntimeError = false, | |
| hasStaticWarning = false, | |
| hasCrash = false { | |
| assert(!isMultitest || dartOptions.isEmpty); | |
| } | |
| @override | |
| Path get originPath => path; | |
| @override | |
| String get multitestKey => ""; | |
| final String? packages; | |
| final Map<String, String> environment; | |
| final bool isMultitest; | |
| final bool hasSyntaxError; | |
| final bool hasCompileError; | |
| final bool hasRuntimeError; | |
| final bool hasStaticWarning; | |
| final bool hasCrash; | |
| final bool isVmIntermediateLanguageTest; | |
| /// The features that a test configuration must support in order to run this | |
| /// test. | |
| /// | |
| /// If the current configuration does not support one or more of these | |
| /// requirements, the test is implicitly skipped. | |
| final List<Feature> requirements; | |
| final List<String> sharedOptions; | |
| final List<String> dartOptions; | |
| final List<String> dart2jsOptions; | |
| final List<String> dart2wasmOptions; | |
| final List<String> ddcOptions; | |
| final List<List<String>> vmOptions; | |
| final List<String> sharedObjects; | |
| final List<String> otherResources; | |
| /// The experiments this test enables. | |
| /// | |
| /// Parsed from a shared options line like: | |
| /// | |
| /// // SharedOptions=--enable-experiment=flubber,gloop | |
| final List<String> experiments; | |
| /// Derive a multitest test section file from this multitest file with the | |
| /// given [multitestKey] and expectations. | |
| TestFile split( | |
| Path path, | |
| String multitestKey, | |
| String contents, { | |
| bool hasCompileError = false, | |
| bool hasRuntimeError = false, | |
| bool hasStaticWarning = false, | |
| bool hasSyntaxError = false, | |
| }) => _MultitestFile( | |
| this, | |
| path, | |
| multitestKey, | |
| StaticError.parseExpectations(path: path.toString(), source: contents), | |
| hasCompileError: hasCompileError, | |
| hasRuntimeError: hasRuntimeError, | |
| hasStaticWarning: hasStaticWarning, | |
| hasSyntaxError: hasSyntaxError, | |
| ); | |
| @override | |
| String toString() => | |
| """TestFile( | |
| packages: $packages | |
| environment: $environment | |
| isMultitest: $isMultitest | |
| hasSyntaxError: $hasSyntaxError | |
| hasCompileError: $hasCompileError | |
| hasRuntimeError: $hasRuntimeError | |
| hasStaticWarning: $hasStaticWarning | |
| hasCrash: $hasCrash | |
| requirements: $requirements | |
| sharedOptions: $sharedOptions | |
| dartOptions: $dartOptions | |
| dart2jsOptions: $dart2jsOptions | |
| dart2wasmOptions: $dart2wasmOptions | |
| ddcOptions: $ddcOptions | |
| vmOptions: $vmOptions | |
| sharedObjects: $sharedObjects | |
| otherResources: $otherResources | |
| experiments: $experiments | |
| )"""; | |
| } | |
| /// 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; | |
| @override | |
| final String multitestKey; | |
| @override | |
| final bool hasCompileError; | |
| @override | |
| final bool hasRuntimeError; | |
| @override | |
| final bool hasStaticWarning; | |
| @override | |
| final bool hasSyntaxError; | |
| @override | |
| bool get hasCrash => _origin.hasCrash; | |
| @override | |
| bool get isVmIntermediateLanguageTest => _origin.isVmIntermediateLanguageTest; | |
| _MultitestFile( | |
| this._origin, | |
| Path path, | |
| this.multitestKey, | |
| List<StaticError> expectedErrors, { | |
| required this.hasCompileError, | |
| required this.hasRuntimeError, | |
| required this.hasStaticWarning, | |
| required this.hasSyntaxError, | |
| }) : super(_origin._suiteDirectory, path, expectedErrors); | |
| @override | |
| Path get originPath => _origin.path; | |
| @override | |
| String? get packages => _origin.packages; | |
| @override | |
| List<Feature> get requirements => _origin.requirements; | |
| @override | |
| List<String> get dart2jsOptions => _origin.dart2jsOptions; | |
| @override | |
| List<String> get dart2wasmOptions => _origin.dart2wasmOptions; | |
| @override | |
| List<String> get dartOptions => _origin.dartOptions; | |
| @override | |
| List<String> get ddcOptions => _origin.ddcOptions; | |
| @override | |
| Map<String, String> get environment => _origin.environment; | |
| @override | |
| bool get isMultitest => _origin.isMultitest; | |
| @override | |
| List<String> get otherResources => _origin.otherResources; | |
| @override | |
| List<String> get sharedObjects => _origin.sharedObjects; | |
| @override | |
| List<String> get experiments => _origin.experiments; | |
| @override | |
| List<String> get sharedOptions => _origin.sharedOptions; | |
| @override | |
| List<List<String>> get vmOptions => _origin.vmOptions; | |
| @override | |
| TestFile split( | |
| Path path, | |
| String multitestKey, | |
| String contents, { | |
| bool hasCompileError = false, | |
| bool hasRuntimeError = false, | |
| bool hasStaticWarning = false, | |
| bool hasSyntaxError = false, | |
| }) => throw UnsupportedError( | |
| "Can't derive a test from one already derived from a multitest.", | |
| ); | |
| } |