Index: tools/testing/dart/lib/test_utils.dart |
diff --git a/tools/testing/dart/lib/test_utils.dart b/tools/testing/dart/lib/test_utils.dart |
new file mode 100644 |
index 0000000000000000000000000000000000000000..0a1108342b13f2a8df6d16edd4b48552dc53ef22 |
--- /dev/null |
+++ b/tools/testing/dart/lib/test_utils.dart |
@@ -0,0 +1,395 @@ |
+library test_utils; |
+ |
+import 'dart:async'; |
+import 'dart:io'; |
+ |
+import 'path.dart'; |
+ |
+class TestUtils { |
+ /** |
+ * Any script using TestUtils must set dartDirUri to a file:// URI |
+ * pointing to the root of the Dart checkout. |
+ */ |
+ static setDartDirUri(uri) { |
+ dartDirUri = uri; |
+ dartDir = new Path(uri.toFilePath()); |
+ } |
+ static Uri dartDirUri; |
+ static Path dartDir; |
+ static LastModifiedCache lastModifiedCache = new LastModifiedCache(); |
+ static ExistsCache existsCache = new ExistsCache(); |
+ static Path currentWorkingDirectory = |
+ new Path(Directory.current.path); |
+ |
+ /** |
+ * Creates a directory using a [relativePath] to an existing |
+ * [base] directory if that [relativePath] does not already exist. |
+ */ |
+ static Directory mkdirRecursive(Path base, Path relativePath) { |
+ if (relativePath.isAbsolute) { |
+ base = new Path('/'); |
+ } |
+ Directory dir = new Directory(base.toNativePath()); |
+ assert(dir.existsSync()); |
+ var segments = relativePath.segments(); |
+ for (String segment in segments) { |
+ base = base.append(segment); |
+ if (base.toString() == "/$segment" && |
+ segment.length == 2 && |
+ segment.endsWith(':')) { |
+ // Skip the directory creation for a path like "/E:". |
+ continue; |
+ } |
+ dir = new Directory(base.toNativePath()); |
+ if (!dir.existsSync()) { |
+ dir.createSync(); |
+ } |
+ assert(dir.existsSync()); |
+ } |
+ return dir; |
+ } |
+ |
+ /** |
+ * Copy a [source] file to a new place. |
+ * Assumes that the directory for [dest] already exists. |
+ */ |
+ static Future copyFile(Path source, Path dest) { |
+ return new File(source.toNativePath()).openRead() |
+ .pipe(new File(dest.toNativePath()).openWrite()); |
+ } |
+ |
+ static Future copyDirectory(String source, String dest) { |
+ source = new Path(source).toNativePath(); |
+ dest = new Path(dest).toNativePath(); |
+ |
+ var executable = 'cp'; |
+ var args = ['-Rp', source, dest]; |
+ if (Platform.operatingSystem == 'windows') { |
+ executable = 'xcopy'; |
+ args = [source, dest, '/e', '/i']; |
+ } |
+ return Process.run(executable, args).then((ProcessResult result) { |
+ if (result.exitCode != 0) { |
+ throw new Exception("Failed to execute '$executable " |
+ "${args.join(' ')}'."); |
+ } |
+ }); |
+ } |
+ |
+ static Future deleteDirectory(String path) { |
+ // We are seeing issues with long path names on windows when |
+ // deleting them. Use the system tools to delete our long paths. |
+ // See issue 16264. |
+ if (Platform.operatingSystem == 'windows') { |
+ var native_path = new Path(path).toNativePath(); |
+ // Running this in a shell sucks, but rmdir is not part of the standard |
+ // path. |
+ return Process.run('rmdir', ['/s', '/q', native_path], runInShell: true) |
+ .then((ProcessResult result) { |
+ if (result.exitCode != 0) { |
+ throw new Exception('Can\'t delete path $native_path. ' |
+ 'This path might be too long'); |
+ } |
+ }); |
+ } else { |
+ var dir = new Directory(path); |
+ return dir.delete(recursive: true); |
+ } |
+ } |
+ |
+ static Path debugLogfile() { |
+ return new Path(".debug.log"); |
+ } |
+ |
+ static String flakyFileName() { |
+ // If a flaky test did fail, infos about it (i.e. test name, stdin, stdout) |
+ // will be written to this file. This is useful for the debugging of |
+ // flaky tests. |
+ // When running on a built bot, the file can be made visible in the |
+ // waterfall UI. |
+ return ".flaky.log"; |
+ } |
+ |
+ static String testOutcomeFileName() { |
+ // If test.py was invoked with '--write-test-outcome-log it will write |
+ // test outcomes to this file. |
+ return ".test-outcome.log"; |
+ } |
+ |
+ static void ensureExists(String filename, Map configuration) { |
+ if (!configuration['list'] && !existsCache.doesFileExist(filename)) { |
+ throw "'$filename' does not exist"; |
+ } |
+ } |
+ |
+ static Path absolutePath(Path path) { |
+ if (!path.isAbsolute) { |
+ return currentWorkingDirectory.join(path); |
+ } |
+ return path; |
+ } |
+ |
+ static String outputDir(Map configuration) { |
+ var result = ''; |
+ var system = configuration['system']; |
+ if (system == 'linux') { |
+ result = 'out/'; |
+ } else if (system == 'macos') { |
+ result = 'xcodebuild/'; |
+ } else if (system == 'windows') { |
+ result = 'build/'; |
+ } |
+ return result; |
+ } |
+ |
+ static List<String> standardOptions(Map configuration) { |
+ List args = ["--ignore-unrecognized-flags"]; |
+ if (configuration["checked"]) { |
+ args.add('--enable_asserts'); |
+ args.add("--enable_type_checks"); |
+ } |
+ String compiler = configuration["compiler"]; |
+ if (compiler == "dart2js" || compiler == "dart2dart") { |
+ args = []; |
+ if (configuration["checked"]) { |
+ args.add('--enable-checked-mode'); |
+ } |
+ // args.add("--verbose"); |
+ if (!isBrowserRuntime(configuration['runtime'])) { |
+ args.add("--allow-mock-compilation"); |
+ args.add("--categories=all"); |
+ } |
+ } |
+ if ((compiler == "dart2js" || compiler == "dart2dart") && |
+ configuration["minified"]) { |
+ args.add("--minify"); |
+ } |
+ if (compiler == "dartanalyzer" || compiler == "dart2analyzer") { |
+ args.add("--show-package-warnings"); |
+ args.add("--enable-async"); |
+ } |
+ return args; |
+ } |
+ |
+ static bool isBrowserRuntime(String runtime) { |
+ const BROWSERS = const [ |
+ 'drt', |
+ 'dartium', |
+ 'ie9', |
+ 'ie10', |
+ 'ie11', |
+ 'safari', |
+ 'opera', |
+ 'chrome', |
+ 'ff', |
+ 'chromeOnAndroid', |
+ 'safarimobilesim', |
+ 'ContentShellOnAndroid', |
+ 'DartiumOnAndroid' |
+ ]; |
+ return BROWSERS.contains(runtime); |
+ } |
+ |
+ static bool isJsCommandLineRuntime(String runtime) => |
+ const ['d8', 'jsshell'].contains(runtime); |
+ |
+ static bool isCommandLineAnalyzer(String compiler) => |
+ compiler == 'dartanalyzer' || compiler == 'dart2analyzer'; |
+ |
+ static String buildDir(Map configuration) { |
+ // FIXME(kustermann,ricow): Our code assumes that the returned 'buildDir' |
+ // is relative to the current working directory. |
+ // Thus, if we pass in an absolute path (e.g. '--build-directory=/tmp/out') |
+ // we get into trouble. |
+ if (configuration['build_directory'] != '') { |
+ return configuration['build_directory']; |
+ } |
+ |
+ return "${outputDir(configuration)}${configurationDir(configuration)}"; |
+ } |
+ |
+ static getValidOutputDir(Map configuration, String mode, String arch) { |
+ // We allow our code to have been cross compiled, i.e., that there |
+ // is an X in front of the arch. We don't allow both a cross compiled |
+ // and a normal version to be present (except if you specifically pass |
+ // in the build_directory). |
+ var normal = '$mode$arch'; |
+ var cross = '${mode}X$arch'; |
+ var outDir = outputDir(configuration); |
+ var normalDir = new Directory(new Path('$outDir$normal').toNativePath()); |
+ var crossDir = new Directory(new Path('$outDir$cross').toNativePath()); |
+ if (normalDir.existsSync() && crossDir.existsSync()) { |
+ throw "You can't have both $normalDir and $crossDir, we don't know which" |
+ " binary to use"; |
+ } |
+ if (crossDir.existsSync()) { |
+ return cross; |
+ } |
+ return normal; |
+ } |
+ |
+ static String configurationDir(Map configuration) { |
+ // For regular dart checkouts, the configDir by default is mode+arch. |
+ // For Dartium, the configDir by default is mode (as defined by the Chrome |
+ // build setup). We can detect this because in the dartium checkout, the |
+ // "output" directory is a sibling of the dart directory instead of a child. |
+ var mode = (configuration['mode'] == 'debug') ? 'Debug' : 'Release'; |
+ var arch = configuration['arch'].toUpperCase(); |
+ if (currentWorkingDirectory != dartDir) { |
+ return getValidOutputDir(configuration, mode, arch); |
+ } else { |
+ return mode; |
+ } |
+ } |
+ |
+ /** |
+ * Returns the path to the dart binary checked into the repo, used for |
+ * bootstrapping test.dart. |
+ */ |
+ static Path get dartTestExecutable { |
+ var path = '$dartDir/tools/testing/bin/' |
+ '${Platform.operatingSystem}/dart'; |
+ if (Platform.operatingSystem == 'windows') { |
+ path = '$path.exe'; |
+ } |
+ return new Path(path); |
+ } |
+ |
+ /** |
+ * Gets extra options under [key] passed to the testing script. |
+ */ |
+ static List<String> getExtraOptions(Map configuration, String key) { |
+ if (configuration[key] == null) return <String>[]; |
+ return configuration[key] |
+ .split(" ") |
+ .map((s) => s.trim()) |
+ .where((s) => s.isNotEmpty) |
+ .toList(); |
+ } |
+ |
+ /** |
+ * Gets extra vm options passed to the testing script. |
+ */ |
+ static List<String> getExtraVmOptions(Map configuration) => |
+ getExtraOptions(configuration, 'vm_options'); |
+ |
+ static String getShortName(String path) { |
+ final PATH_REPLACEMENTS = const { |
+ "pkg_polymer_e2e_test_bad_import_test": "polymer_bi", |
+ "pkg_polymer_e2e_test_canonicalization_test": "polymer_c16n", |
+ "pkg_polymer_e2e_test_experimental_boot_test": "polymer_boot", |
+ "pkg_polymer_e2e_test_good_import_test": "polymer_gi", |
+ "tests_co19_src_Language_12_Expressions_14_Function_Invocation_": |
+ "co19_fn_invoke_", |
+ "tests_co19_src_LayoutTests_fast_css_getComputedStyle_getComputedStyle-": |
+ "co19_css_getComputedStyle_", |
+ "tests_co19_src_LayoutTests_fast_dom_Document_CaretRangeFromPoint_" |
+ "caretRangeFromPoint-": "co19_caretrangefrompoint_", |
+ "tests_co19_src_LayoutTests_fast_dom_Document_CaretRangeFromPoint_" |
+ "hittest-relative-to-viewport_": "co19_caretrange_hittest_", |
+ "tests_co19_src_LayoutTests_fast_dom_HTMLLinkElement_link-onerror-" |
+ "stylesheet-with-": "co19_dom_link-", |
+ "tests_co19_src_LayoutTests_fast_dom_": "co19_dom", |
+ "tests_co19_src_LayoutTests_fast_canvas_webgl": "co19_canvas_webgl", |
+ "tests_co19_src_LibTest_core_AbstractClassInstantiationError_" |
+ "AbstractClassInstantiationError_": "co19_abstract_class_", |
+ "tests_co19_src_LibTest_core_IntegerDivisionByZeroException_" |
+ "IntegerDivisionByZeroException_": "co19_division_by_zero", |
+ "tests_co19_src_WebPlatformTest_html_dom_documents_dom-tree-accessors_": |
+ "co19_dom_accessors_", |
+ "tests_co19_src_WebPlatformTest_html_semantics_embedded-content_" |
+ "media-elements_": "co19_media_elements", |
+ "tests_co19_src_WebPlatformTest_html_semantics_": "co19_semantics_", |
+ |
+ "tests_co19_src_WebPlatformTest_html-templates_additions-to-" |
+ "the-steps-to-clone-a-node_": "co19_htmltemplates_clone_", |
+ "tests_co19_src_WebPlatformTest_html-templates_definitions_" |
+ "template-contents-owner": "co19_htmltemplates_contents", |
+ "tests_co19_src_WebPlatformTest_html-templates_parsing-html-" |
+ "templates_additions-to-": "co19_htmltemplates_add_", |
+ "tests_co19_src_WebPlatformTest_html-templates_parsing-html-" |
+ "templates_appending-to-a-template_": "co19_htmltemplates_append_", |
+ "tests_co19_src_WebPlatformTest_html-templates_parsing-html-" |
+ "templates_clearing-the-stack-back-to-a-given-context_": |
+ "co19_htmltemplates_clearstack_", |
+ "tests_co19_src_WebPlatformTest_html-templates_parsing-html-" |
+ "templates_creating-an-element-for-the-token_": |
+ "co19_htmltemplates_create_", |
+ "tests_co19_src_WebPlatformTest_html-templates_template-element" |
+ "_template-": "co19_htmltemplates_element-", |
+ "tests_co19_src_WebPlatformTest_html-templates_": "co19_htmltemplate_", |
+ |
+ "tests_co19_src_WebPlatformTest_shadow-dom_shadow-trees_": |
+ "co19_shadow-trees_", |
+ "tests_co19_src_WebPlatformTest_shadow-dom_elements-and-dom-objects_": |
+ "co19_shadowdom_", |
+ "tests_co19_src_WebPlatformTest_shadow-dom_html-elements-in-" |
+ "shadow-trees_": "co19_shadow_html_", |
+ "tests_co19_src_WebPlatformTest_html_webappapis_system-state-and-" |
+ "capabilities_the-navigator-object": "co19_webappapis_navigator_", |
+ "tests_co19_src_WebPlatformTest_DOMEvents_approved_": "co19_dom_approved_" |
+ }; |
+ |
+ // Some tests are already in [build_dir]/generated_tests. |
+ String GEN_TESTS = 'generated_tests/'; |
+ if (path.contains(GEN_TESTS)) { |
+ int index = path.indexOf(GEN_TESTS) + GEN_TESTS.length; |
+ path = 'multitest/${path.substring(index)}'; |
+ } |
+ path = path.replaceAll('/', '_'); |
+ final int WINDOWS_SHORTEN_PATH_LIMIT = 58; |
+ if (Platform.operatingSystem == 'windows' && |
+ path.length > WINDOWS_SHORTEN_PATH_LIMIT) { |
+ for (var key in PATH_REPLACEMENTS.keys) { |
+ if (path.startsWith(key)) { |
+ path = path.replaceFirst(key, PATH_REPLACEMENTS[key]); |
+ break; |
+ } |
+ } |
+ } |
+ return path; |
+ } |
+} |
+ |
+ |
+class LastModifiedCache { |
+ final Map<String, DateTime> _cache = <String, DateTime>{}; |
+ |
+ /** |
+ * Returns the last modified date of the given [uri]. |
+ * |
+ * The return value will be cached for future queries. If [uri] is a local |
+ * file, it's last modified [Date] will be returned. If the file does not |
+ * exist, null will be returned instead. |
+ * In case [uri] is not a local file, this method will always return |
+ * the current date. |
+ */ |
+ DateTime getLastModified(Uri uri) { |
+ if (uri.scheme == "file") { |
+ if (_cache.containsKey(uri.path)) { |
+ return _cache[uri.path]; |
+ } |
+ var file = new File(new Path(uri.path).toNativePath()); |
+ _cache[uri.path] = file.existsSync() ? file.lastModifiedSync() : null; |
+ return _cache[uri.path]; |
+ } |
+ return new DateTime.now(); |
+ } |
+} |
+ |
+ |
+class ExistsCache { |
+ final Map<String, bool> _cache = <String, bool>{}; |
+ |
+ /** |
+ * Returns true if the file in [path] exists, false otherwise. |
+ * |
+ * The information will be cached. |
+ */ |
+ bool doesFileExist(String path) { |
+ if (!_cache.containsKey(path)) { |
+ _cache[path] = new File(path).existsSync(); |
+ } |
+ return _cache[path]; |
+ } |
+} |