| // 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. | 
 |  | 
 | /// This library defines how `modular_test` converts the contents of a folder | 
 | /// into a modular test. At this time, the logic in this library assumes this is | 
 | /// only used within the Dart SDK repo. | 
 | /// | 
 | /// A modular test folder contains: | 
 | ///   * individual .dart files, each file is considered a module. A | 
 | ///   `main.dart` file is required as the entry point of the test. | 
 | ///   * subfolders: each considered a module with multiple files | 
 | ///   * a modules.yaml file: a specification of dependencies between modules. | 
 | ///     The format is described in `test_specification_parser.dart`. | 
 | library; | 
 |  | 
 | import 'dart:io'; | 
 | import 'suite.dart'; | 
 | import 'test_specification_parser.dart'; | 
 | import 'find_sdk_root.dart'; | 
 |  | 
 | /// Returns the [ModularTest] associated with a folder under [uri]. | 
 | /// | 
 | /// After scanning the contents of the folder, this method creates a | 
 | /// [ModularTest] that contains only modules that are reachable from the main | 
 | /// module.  This method runs several validations including that modules don't | 
 | /// have conflicting names, that the default packages are always visible, and | 
 | /// that modules do not contain cycles. | 
 | Future<ModularTest> loadTest(Uri uri) async { | 
 |   var folder = Directory.fromUri(uri); | 
 |   var testUri = folder.uri; // normalized in case the trailing '/' was missing. | 
 |   Uri root = await findRoot(); | 
 |   final defaultTestSpecification = parseTestSpecification(_defaultPackagesSpec); | 
 |   Set<String> defaultPackages = defaultTestSpecification.packages.keys.toSet(); | 
 |   Module sdkModule = await _createSdkModule(root); | 
 |   Map<String, Module> modules = {'sdk': sdkModule}; | 
 |   String? specString; | 
 |   Module? mainModule; | 
 |   var entries = folder.listSync(recursive: false).toList() | 
 |     // Sort to avoid dependency on file system order. | 
 |     ..sort(_compareFileSystemEntity); | 
 |   for (var entry in entries) { | 
 |     var entryUri = entry.uri; | 
 |     if (entry is File) { | 
 |       var fileName = entryUri.path.substring(testUri.path.length); | 
 |       if (fileName.endsWith('.dart')) { | 
 |         var moduleName = fileName.substring(0, fileName.indexOf('.dart')); | 
 |         if (moduleName == 'sdk') { | 
 |           return _invalidTest("The file '$fileName' defines a module called " | 
 |               "'$moduleName' which conflicts with the sdk module " | 
 |               "that is provided by default."); | 
 |         } | 
 |         if (defaultPackages.contains(moduleName)) { | 
 |           return _invalidTest("The file '$fileName' defines a module called " | 
 |               "'$moduleName' which conflicts with a package by the same name " | 
 |               "that is provided by default."); | 
 |         } | 
 |         if (modules.containsKey(moduleName)) { | 
 |           return _moduleConflict(fileName, modules[moduleName]!, testUri); | 
 |         } | 
 |         var relativeUri = Uri.parse(fileName); | 
 |         var isMain = moduleName == 'main'; | 
 |         var module = Module(moduleName, [], testUri, [relativeUri], | 
 |             mainSource: isMain ? relativeUri : null, | 
 |             isMain: isMain, | 
 |             packageBase: Uri.parse('.')); | 
 |         if (isMain) mainModule = module; | 
 |         modules[moduleName] = module; | 
 |       } else if (fileName == 'modules.yaml') { | 
 |         specString = await entry.readAsString(); | 
 |       } | 
 |     } else { | 
 |       assert(entry is Directory); | 
 |       var path = entryUri.path; | 
 |       var moduleName = path.substring(testUri.path.length, path.length - 1); | 
 |       if (moduleName == 'sdk') { | 
 |         return _invalidTest("The folder '$moduleName' defines a module " | 
 |             "which conflicts with the sdk module " | 
 |             "that is provided by default."); | 
 |       } | 
 |       if (defaultPackages.contains(moduleName)) { | 
 |         return _invalidTest("The folder '$moduleName' defines a module " | 
 |             "which conflicts with a package by the same name " | 
 |             "that is provided by default."); | 
 |       } | 
 |       if (modules.containsKey(moduleName)) { | 
 |         return _moduleConflict(moduleName, modules[moduleName]!, testUri); | 
 |       } | 
 |       var sources = await _listModuleSources(entryUri); | 
 |       modules[moduleName] = Module(moduleName, [], testUri, sources, | 
 |           packageBase: Uri.parse('$moduleName/')); | 
 |     } | 
 |   } | 
 |   if (specString == null) { | 
 |     return _invalidTest("modules.yaml file is missing"); | 
 |   } | 
 |   if (mainModule == null) { | 
 |     return _invalidTest("main module is missing"); | 
 |   } | 
 |  | 
 |   TestSpecification spec = parseTestSpecification(specString); | 
 |   for (final name in defaultPackages) { | 
 |     if (spec.packages.containsKey(name)) { | 
 |       _invalidTest( | 
 |           ".packages file defines a conflicting entry for package '$name'."); | 
 |     } | 
 |   } | 
 |   await _addModulePerPackage(defaultTestSpecification.packages, root, modules); | 
 |   await _addModulePerPackage(spec.packages, testUri, modules); | 
 |   _attachDependencies(spec.dependencies, modules); | 
 |   _attachDependencies(defaultTestSpecification.dependencies, modules); | 
 |   _addSdkDependencies(modules, sdkModule); | 
 |   _detectCyclesAndRemoveUnreachable(modules, mainModule); | 
 |   var sortedModules = modules.values.toList() | 
 |     ..sort((a, b) => a.name.compareTo(b.name)); | 
 |   var sortedFlags = spec.flags.toList()..sort(); | 
 |   return ModularTest(sortedModules, mainModule, sortedFlags); | 
 | } | 
 |  | 
 | /// Returns all source files recursively found in a folder as relative URIs. | 
 | Future<List<Uri>> _listModuleSources(Uri root) async { | 
 |   List<Uri> sources = []; | 
 |   Directory folder = Directory.fromUri(root); | 
 |   int baseUriPrefixLength = folder.parent.uri.path.length; | 
 |   await for (var file in folder.list(recursive: true)) { | 
 |     var path = file.uri.path; | 
 |     if (path.endsWith('.dart')) { | 
 |       sources.add(Uri.parse(path.substring(baseUriPrefixLength))); | 
 |     } | 
 |   } | 
 |   return sources..sort((a, b) => a.path.compareTo(b.path)); | 
 | } | 
 |  | 
 | /// Add links between modules based on the provided dependency map. | 
 | void _attachDependencies( | 
 |     Map<String, List<String>> dependencies, Map<String, Module> modules) { | 
 |   dependencies.forEach((name, moduleDependencies) { | 
 |     final module = modules[name]; | 
 |     if (module == null) { | 
 |       _invalidTest( | 
 |           "declared dependencies for a nonexistent module named '$name'"); | 
 |     } | 
 |     if (module.dependencies.isNotEmpty) { | 
 |       _invalidTest("Module dependencies have already been declared on $name."); | 
 |     } | 
 |     for (var dependencyName in moduleDependencies) { | 
 |       final moduleDependency = modules[dependencyName]; | 
 |       if (moduleDependency == null) { | 
 |         _invalidTest("'$name' declares a dependency on a nonexistent module " | 
 |             "named '$dependencyName'"); | 
 |       } | 
 |       module.dependencies.add(moduleDependency); | 
 |     } | 
 |   }); | 
 | } | 
 |  | 
 | /// Make every module depend on the sdk module. | 
 | void _addSdkDependencies(Map<String, Module> modules, Module sdkModule) { | 
 |   for (var module in modules.values) { | 
 |     if (module != sdkModule) { | 
 |       module.dependencies.add(sdkModule); | 
 |     } | 
 |   } | 
 | } | 
 |  | 
 | /// Create a module for each package dependency. | 
 | Future<void> _addModulePerPackage(Map<String, String> packages, Uri configRoot, | 
 |     Map<String, Module> modules) async { | 
 |   for (var packageName in packages.keys) { | 
 |     var module = modules[packageName]; | 
 |     if (module != null) { | 
 |       module.isPackage = true; | 
 |     } else { | 
 |       var packageLibUri = configRoot.resolve(packages[packageName]!); | 
 |       var packageRootUri = Directory.fromUri(packageLibUri).parent.uri; | 
 |       var sources = await _listModuleSources(packageLibUri); | 
 |       // TODO(sigmund): validate that we don't use a different alias for a | 
 |       // module that is part of the test (package name and module name should | 
 |       // match). | 
 |       modules[packageName] = Module(packageName, [], packageRootUri, sources, | 
 |           isPackage: true, packageBase: Uri.parse('lib/'), isShared: true); | 
 |     } | 
 |   } | 
 | } | 
 |  | 
 | Future<Module> _createSdkModule(Uri root) async { | 
 |   List<Uri> sources = [ | 
 |     Uri.parse('sdk/lib/libraries.json'), | 
 |   ]; | 
 |  | 
 |   // Include all dart2js, ddc, vm library sources and patch files. | 
 |   // Note: we don't extract the list of files from the libraries.json because | 
 |   // it doesn't list files that are transitively imported. | 
 |   var sdkLibrariesAndPatchesRoots = [ | 
 |     'sdk/lib/', | 
 |     'runtime/lib/', | 
 |     'runtime/bin/', | 
 |   ]; | 
 |   for (var path in sdkLibrariesAndPatchesRoots) { | 
 |     var dir = Directory.fromUri(root.resolve(path)); | 
 |     await for (var file in dir.list(recursive: true)) { | 
 |       if (file is File && file.path.endsWith(".dart")) { | 
 |         sources.add(Uri.parse(file.uri.path.substring(root.path.length))); | 
 |       } | 
 |     } | 
 |   } | 
 |   sources.sort((a, b) => a.path.compareTo(b.path)); | 
 |   return Module('sdk', [], root, sources, isSdk: true, isShared: true); | 
 | } | 
 |  | 
 | /// Trim the set of modules, and detect cycles while we are at it. | 
 | _detectCyclesAndRemoveUnreachable(Map<String, Module> modules, Module main) { | 
 |   Set<Module> visiting = {}; | 
 |   Set<Module> visited = {}; | 
 |  | 
 |   helper(Module current) { | 
 |     if (!visiting.add(current)) { | 
 |       _invalidTest("module '${current.name}' has a dependency cycle."); | 
 |     } | 
 |     if (visited.add(current)) { | 
 |       current.dependencies.forEach(helper); | 
 |     } | 
 |     visiting.remove(current); | 
 |   } | 
 |  | 
 |   helper(main); | 
 |   Set<String> toKeep = visited.map((m) => m.name).toSet(); | 
 |   List<String> toRemove = | 
 |       modules.keys.where((name) => !toKeep.contains(name)).toList(); | 
 |   toRemove.forEach(modules.remove); | 
 | } | 
 |  | 
 | /// Specifies the dependencies of all packages in [_defaultPackagesInput]. This | 
 | /// string needs to be updated if dependencies between those packages changes | 
 | /// (which is rare). | 
 | // TODO(sigmund): consider either computing this from the pubspec files or the | 
 | // import graph, or adding tests that validate this is always up to date. | 
 | String _defaultPackagesSpec = ''' | 
 | dependencies: | 
 |   expect: [meta, smith] | 
 |   smith: [] | 
 |   meta: [] | 
 |   collection: [] | 
 | packages: | 
 |   expect: pkg/expect/lib | 
 |   smith: pkg/smith/lib | 
 |   meta: pkg/meta/lib | 
 |   collection: third_party/pkg/core/pkgs/collection/lib | 
 | '''; | 
 |  | 
 | /// Report an conflict error. | 
 | _moduleConflict(String name, Module existing, Uri root) { | 
 |   var isFile = name.endsWith('.dart'); | 
 |   var entryType = isFile ? 'file' : 'folder'; | 
 |  | 
 |   var existingIsFile = | 
 |       existing.packageBase!.path == './' && existing.sources.length == 1; | 
 |   var existingEntryType = existingIsFile ? 'file' : 'folder'; | 
 |  | 
 |   var existingName = existingIsFile | 
 |       ? existing.sources.single.pathSegments.last | 
 |       : existing.name; | 
 |  | 
 |   return _invalidTest("The $entryType '$name' defines a module " | 
 |       "which conflicts with the module defined by the $existingEntryType " | 
 |       "'$existingName'."); | 
 | } | 
 |  | 
 | Never _invalidTest(String message) { | 
 |   throw InvalidTestError(message); | 
 | } | 
 |  | 
 | class InvalidTestError extends Error { | 
 |   final String message; | 
 |   InvalidTestError(this.message); | 
 |   @override | 
 |   String toString() => "Invalid test: $message"; | 
 | } | 
 |  | 
 | /// Comparator to sort directories before files. | 
 | int _compareFileSystemEntity(FileSystemEntity a, FileSystemEntity b) { | 
 |   if (a is Directory) { | 
 |     if (b is Directory) { | 
 |       return a.path.compareTo(b.path); | 
 |     } else { | 
 |       return -1; | 
 |     } | 
 |   } else { | 
 |     if (b is Directory) { | 
 |       return 1; | 
 |     } else { | 
 |       return a.path.compareTo(b.path); | 
 |     } | 
 |   } | 
 | } |