|  | // Copyright (c) 2023, 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:convert'; | 
|  |  | 
|  | import 'package:analyzer/dart/analysis/analysis_context_collection.dart'; | 
|  | import 'package:analyzer/dart/analysis/results.dart'; | 
|  | import 'package:analyzer/dart/analysis/session.dart'; | 
|  | import 'package:analyzer/dart/ast/ast.dart'; | 
|  | import 'package:analyzer/dart/ast/visitor.dart'; | 
|  | import 'package:analyzer/file_system/file_system.dart'; | 
|  | import 'package:analyzer/file_system/physical_file_system.dart'; | 
|  | import 'package:analyzer_utilities/package_root.dart'; | 
|  | import 'package:http/http.dart' as http; | 
|  | import 'package:test/test.dart'; | 
|  | import 'package:test_reflective_loader/test_reflective_loader.dart'; | 
|  |  | 
|  | Future<void> main() async { | 
|  | var tests = <AnnotatedTest>[]; | 
|  |  | 
|  | // Note: Unauthenticated GitHub API calls are limited to 60 per hour | 
|  | // so this script cannot be run often and will fail if there are over | 
|  | // 60 unique issues listed. | 
|  |  | 
|  | await findFailingTestAnnotations(tests, packagePath: 'analysis_server'); | 
|  | await findFailingTestAnnotations(tests, packagePath: 'analyzer'); | 
|  | await findFailingTestAnnotations(tests, packagePath: 'analyzer_cli'); | 
|  | await findFailingTestAnnotations(tests, packagePath: 'analyzer_plugin'); | 
|  |  | 
|  | var issueUris = tests.map((test) => test.issueUri).toSet(); | 
|  | print('Found ${tests.length} with ${issueUris.length} unique issues.'); | 
|  | print('Fetching test statuses from GitHub...'); | 
|  |  | 
|  | var closedIssues = <Uri>{}; | 
|  | for (var issueUri in issueUris) { | 
|  | var response = await http.get(issueUri); | 
|  | if (response.statusCode != 200) { | 
|  | throw 'Failed to call GitHub API: ${response.statusCode} - ${response.reasonPhrase}\n${response.body}'; | 
|  | } | 
|  | var issueData = jsonDecode(response.body) as Map<String, Object?>; | 
|  | if (issueData['state'] != 'open') { | 
|  | closedIssues.add(issueUri); | 
|  | } | 
|  | } | 
|  |  | 
|  | print('Found ${closedIssues.length} closed issues:'); | 
|  | for (var tests | 
|  | in tests.where((test) => closedIssues.contains(test.issueUri))) { | 
|  | var relativePath = pathContext.relative(tests.file.path, from: packageRoot); | 
|  | print('$relativePath ${tests.testName} ${_formatUri(tests.issueUri)}'); | 
|  | } | 
|  | } | 
|  |  | 
|  | final pathContext = provider.pathContext; | 
|  |  | 
|  | final provider = PhysicalResourceProvider.INSTANCE; | 
|  |  | 
|  | Future<void> findFailingTestAnnotations(List<AnnotatedTest> tests, | 
|  | {required String packagePath}) async { | 
|  | var pkgRootPath = pathContext.normalize(packageRoot); | 
|  | var testsPath = pathContext.join(pkgRootPath, packagePath, 'test'); | 
|  |  | 
|  | var collection = AnalysisContextCollection( | 
|  | includedPaths: <String>[testsPath], | 
|  | resourceProvider: provider, | 
|  | ); | 
|  | var contexts = collection.contexts; | 
|  | if (contexts.length != 1) { | 
|  | fail('The directory $testsPath contains multiple analysis contexts.'); | 
|  | } | 
|  |  | 
|  | var session = contexts[0].currentSession; | 
|  | var directory = provider.getFolder(testsPath); | 
|  |  | 
|  | print('Searching for FailedTest/SkippedTest annotations in $packagePath...'); | 
|  | await _findFailingTestAnnotationsIn(session, testsPath, directory, tests); | 
|  | } | 
|  |  | 
|  | Future<void> _findFailingTestAnnotationsIn(AnalysisSession session, | 
|  | String testDirPath, Folder directory, List<AnnotatedTest> tests) async { | 
|  | var children = directory.getChildren(); | 
|  | children.sort((first, second) => first.shortName.compareTo(second.shortName)); | 
|  | for (var child in children) { | 
|  | if (child is Folder) { | 
|  | await _findFailingTestAnnotationsIn(session, testDirPath, child, tests); | 
|  | } else if (child is File && child.shortName.endsWith('_test.dart')) { | 
|  | var path = child.path; | 
|  |  | 
|  | var result = session.getParsedUnit(path); | 
|  | if (result is! ParsedUnitResult) { | 
|  | fail('Could not parse $path'); | 
|  | } | 
|  | var unit = result.unit; | 
|  | var errors = result.errors; | 
|  | if (errors.isNotEmpty) { | 
|  | fail('Errors found when parsing $path'); | 
|  | } | 
|  | var tracker = FailingTestAnnotationTracker(child); | 
|  | unit.accept(tracker); | 
|  | tests.addAll(tracker.annotatedTests); | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | String _formatUri(Uri uri) => | 
|  | uri.path.replaceAll('/repos/', '').replaceAll('/issues/', '#'); | 
|  |  | 
|  | class AnnotatedTest { | 
|  | final File file; | 
|  | final String testName; | 
|  | final Uri issueUri; | 
|  |  | 
|  | AnnotatedTest(this.file, this.testName, this.issueUri); | 
|  | } | 
|  |  | 
|  | /// A [RecursiveAstVisitor] that tracks nodes annotated with [FailingTest] or | 
|  | /// [SkippedTest]. | 
|  | class FailingTestAnnotationTracker extends RecursiveAstVisitor<void> { | 
|  | final annotatedTests = <AnnotatedTest>[]; | 
|  | final File file; | 
|  |  | 
|  | FailingTestAnnotationTracker(this.file); | 
|  |  | 
|  | @override | 
|  | void visitAnnotation(Annotation node) { | 
|  | if (node.name.name == 'FailingTest' || node.name.name == 'SkippedTest') { | 
|  | var issue = node.arguments?.arguments | 
|  | .whereType<NamedExpression>() | 
|  | .where((arg) => arg.name.label.name == 'issue') | 
|  | .firstOrNull; | 
|  | var issueUrl = (issue?.expression as SimpleStringLiteral?)?.value; | 
|  | if (issueUrl != null && issueUrl.startsWith('https://github.com/')) { | 
|  | var issueUri = Uri.parse(issueUrl); | 
|  | var apiUri = issueUri.replace( | 
|  | host: 'api.github.com', | 
|  | pathSegments: ['repos', ...issueUri.pathSegments]); | 
|  | var method = node.parent as MethodDeclaration; | 
|  | annotatedTests.add(AnnotatedTest(file, method.name.lexeme, apiUri)); | 
|  | } | 
|  | } | 
|  | super.visitAnnotation(node); | 
|  | } | 
|  | } |