| // Copyright (c) 2015, 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:async'; | |
| import 'dart:io'; | |
| import 'package:_fe_analyzer_shared/src/util/filenames.dart'; | |
| import 'package:expect/async_helper.dart'; | |
| import 'package:expect/expect.dart'; | |
| import 'package:compiler/src/commandline_options.dart'; | |
| import 'package:compiler/src/io/source_information.dart'; | |
| import 'package:compiler/src/js/js_debug.dart'; | |
| import 'package:js_ast/js_ast.dart'; | |
| import '../helpers/sourcemap_helper.dart'; | |
| typedef CodePointWhiteListFunction WhiteListFunction( | |
| String configuration, | |
| String file, | |
| ); | |
| typedef bool CodePointWhiteListFunction(CodePoint codePoint); | |
| CodePointWhiteListFunction emptyWhiteListFunction(String config, String file) { | |
| return emptyWhiteList; | |
| } | |
| bool emptyWhiteList(CodePoint codePoint) => false; | |
| main(List<String> arguments) { | |
| test(arguments); | |
| } | |
| void test( | |
| List<String> arguments, { | |
| WhiteListFunction whiteListFunction = emptyWhiteListFunction, | |
| }) { | |
| Set<String> configurations = Set<String>(); | |
| Map<String, Uri> tests = <String, Uri>{}; | |
| if (!parseArguments(arguments, configurations, tests)) { | |
| return; | |
| } | |
| asyncTest(() async { | |
| bool errorsFound = false; | |
| for (String file in tests.keys) { | |
| print('==$file========================================================='); | |
| for (String config in configurations) { | |
| List<String> options = TEST_CONFIGURATIONS[config]!; | |
| print('---$config----------------------------------------------------'); | |
| Uri uri = tests[file]!; | |
| TestResult result = await runTests(config, file, uri, options); | |
| if (result.missingCodePointsMap.isNotEmpty) { | |
| errorsFound = result.printMissingCodePoints( | |
| whiteListFunction(config, file), | |
| ); | |
| } | |
| if (result.multipleNodesMap.isNotEmpty) { | |
| result.printMultipleNodes(); | |
| errorsFound = true; | |
| } | |
| if (result.multipleOffsetsMap.isNotEmpty) { | |
| result.printMultipleOffsets(); | |
| errorsFound = true; | |
| } | |
| } | |
| } | |
| Expect.isFalse( | |
| errorsFound, | |
| "Errors found. " | |
| "Run the test with a URI option, " | |
| "`source_mapping_test_viewer [--out=<uri>] [configs] [tests]`, to " | |
| "create a html visualization of the missing code points.", | |
| ); | |
| }); | |
| } | |
| bool parseArguments( | |
| List<String> arguments, | |
| Set<String> configurations, | |
| Map<String, Uri> tests, { | |
| bool measure = false, | |
| }) { | |
| Set<String>? extra = arguments.contains('--file') ? Set<String>() : null; | |
| for (String argument in arguments) { | |
| if (!parseArgument(argument, configurations, tests, extra)) { | |
| return false; | |
| } | |
| } | |
| if (configurations.isEmpty) { | |
| configurations.addAll(TEST_CONFIGURATIONS.keys); | |
| if (!measure) { | |
| configurations.remove('old'); | |
| } | |
| } | |
| if (extra != null) { | |
| for (String file in extra) { | |
| Uri uri = Uri.base.resolve(nativeToUriPath(file)); | |
| tests[uri.pathSegments.last] = uri; | |
| } | |
| } | |
| if (tests.isEmpty) { | |
| tests.addAll(TEST_FILES); | |
| } | |
| if (arguments.contains('--exclude')) { | |
| List<String> filesToRemove = List<String>.of(tests.keys); | |
| tests.clear(); | |
| tests.addAll(TEST_FILES); | |
| filesToRemove.forEach(tests.remove); | |
| } | |
| return true; | |
| } | |
| /// Parse [argument] for a valid configuration or test-file option. | |
| /// | |
| /// On success, the configuration name is added to [configurations] or the | |
| /// test-file name is added to [testFiles], and `true` is returned. | |
| /// On failure, a message is printed and `false` is returned. | |
| /// | |
| /// Unmatching arguments are added to [files] is provided. | |
| bool parseArgument( | |
| String argument, | |
| Set<String> configurations, | |
| Map<String, Uri> tests, | |
| Set<String>? extra, | |
| ) { | |
| if (argument.startsWith('-')) { | |
| // Skip options. | |
| return true; | |
| } else if (TEST_CONFIGURATIONS.containsKey(argument)) { | |
| configurations.add(argument); | |
| } else if (TEST_FILES.containsKey(argument)) { | |
| tests[argument] = TEST_FILES[argument]!; | |
| } else if (extra != null) { | |
| extra.add(argument); | |
| } else { | |
| print( | |
| "Unknown configuration or test file '$argument'. " | |
| "Must be one of '${TEST_CONFIGURATIONS.keys.join("', '")}' or " | |
| "'${TEST_FILES.keys.join("', '")}'.", | |
| ); | |
| return false; | |
| } | |
| return true; | |
| } | |
| const Map<String, List<String>> TEST_CONFIGURATIONS = const { | |
| 'kernel': const [], | |
| }; | |
| final Map<String, Uri> TEST_FILES = _computeTestFiles(); | |
| Map<String, Uri> _computeTestFiles() { | |
| Map<String, Uri> map = <String, Uri>{}; | |
| Directory dataDir = Directory.fromUri( | |
| Uri.base.resolve('pkg/compiler/test/sourcemaps/data/'), | |
| ); | |
| for (FileSystemEntity file in dataDir.listSync()) { | |
| Uri uri = file.uri; | |
| map[uri.pathSegments.last] = uri; | |
| } | |
| return map; | |
| } | |
| Future<TestResult> runTests( | |
| String config, | |
| String filename, | |
| Uri uri, | |
| List<String> options, { | |
| bool verbose = true, | |
| }) async { | |
| SourceMapProcessor processor = SourceMapProcessor(uri); | |
| SourceMaps sourceMaps = await processor.process([ | |
| '--csp', | |
| Flags.disableInlining, | |
| ...options, | |
| ], verbose: verbose); | |
| TestResult result = TestResult(config, filename, processor); | |
| for (SourceMapInfo info in sourceMaps.elementSourceMapInfos.values) { | |
| if (info.element!.library.canonicalUri.isScheme('dart')) continue; | |
| result.userInfoList.add(info); | |
| Iterable<CodePoint> missingCodePoints = info.codePoints.where( | |
| (c) => c.isMissing, | |
| ); | |
| if (missingCodePoints.isNotEmpty) { | |
| result.missingCodePointsMap[info] = missingCodePoints; | |
| } | |
| Map<int, Set<SourceLocation>> offsetToLocationsMap = | |
| <int, Set<SourceLocation>>{}; | |
| for (Node node in info.nodeMap.nodes) { | |
| info.nodeMap[node]!.forEach(( | |
| int targetOffset, | |
| List<SourceLocation> sourceLocations, | |
| ) { | |
| if (sourceLocations.length > 1) { | |
| Map<Node, List<SourceLocation>> multipleMap = result.multipleNodesMap | |
| .putIfAbsent(info, () => <Node, List<SourceLocation>>{}); | |
| multipleMap[node] = sourceLocations; | |
| } else { | |
| offsetToLocationsMap | |
| .putIfAbsent(targetOffset, () => Set<SourceLocation>()) | |
| .addAll(sourceLocations); | |
| } | |
| }); | |
| } | |
| offsetToLocationsMap.forEach(( | |
| int targetOffset, | |
| Set<SourceLocation> sourceLocations, | |
| ) { | |
| if (sourceLocations.length > 1) { | |
| Map<int, Set<SourceLocation>> multipleMap = result.multipleOffsetsMap | |
| .putIfAbsent(info, () => <int, Set<SourceLocation>>{}); | |
| multipleMap[targetOffset] = sourceLocations; | |
| } | |
| }); | |
| } | |
| return result; | |
| } | |
| class TestResult { | |
| final String config; | |
| final String file; | |
| final SourceMapProcessor processor; | |
| List<SourceMapInfo> userInfoList = <SourceMapInfo>[]; | |
| Map<SourceMapInfo, Iterable<CodePoint>> missingCodePointsMap = | |
| <SourceMapInfo, Iterable<CodePoint>>{}; | |
| /// For each [SourceMapInfo] a map from JS node to multiple source locations | |
| /// associated with the node. | |
| Map<SourceMapInfo, Map<Node, List<SourceLocation>>> multipleNodesMap = | |
| <SourceMapInfo, Map<Node, List<SourceLocation>>>{}; | |
| /// For each [SourceMapInfo] a map from JS offset to multiple source locations | |
| /// associated with the offset. | |
| Map<SourceMapInfo, Map<int, Set<SourceLocation>>> multipleOffsetsMap = | |
| <SourceMapInfo, Map<int, Set<SourceLocation>>>{}; | |
| TestResult(this.config, this.file, this.processor); | |
| bool printMissingCodePoints([ | |
| CodePointWhiteListFunction codePointWhiteList = emptyWhiteList, | |
| ]) { | |
| bool allWhiteListed = true; | |
| missingCodePointsMap.forEach((info, missingCodePoints) { | |
| print( | |
| "Missing code points for ${info.element} in '$file' " | |
| "in config '$config':", | |
| ); | |
| for (CodePoint codePoint in missingCodePoints) { | |
| if (codePointWhiteList(codePoint)) { | |
| print(" $codePoint (white-listed)"); | |
| } else { | |
| print(" $codePoint"); | |
| allWhiteListed = false; | |
| } | |
| } | |
| }); | |
| return !allWhiteListed; | |
| } | |
| void printMultipleNodes() { | |
| multipleNodesMap.forEach((info, multipleMap) { | |
| multipleMap.forEach((node, sourceLocations) { | |
| print( | |
| 'Multiple source locations:\n ${sourceLocations.join('\n ')}\n' | |
| 'for `${nodeToString(node)}` in ${info.element} in ' | |
| '$file.', | |
| ); | |
| }); | |
| }); | |
| } | |
| void printMultipleOffsets() { | |
| multipleOffsetsMap.forEach((info, multipleMap) { | |
| multipleMap.forEach((targetOffset, sourceLocations) { | |
| print( | |
| 'Multiple source locations:\n ${sourceLocations.join('\n ')}\n' | |
| 'for offset $targetOffset in ${info.element} in $file.', | |
| ); | |
| }); | |
| }); | |
| } | |
| } |