| // 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. |
| |
| // Removes all status file expectations that are not relevant in the |
| // new workflow, but preserves entries with comments. |
| // |
| // For example, using the script on this status file |
| // a: Crash |
| // b: RuntimeError |
| // c: RuntimeError # Comment |
| // d: Pass, RuntimeError |
| // e: Pass, Slow, RuntimeError |
| // f: Pass, Slow, RuntimeError # Another comment |
| // will produce the output |
| // c: RuntimeError # Comment |
| // e: Slow |
| // f: Pass, Slow, RuntimeError # Another comment |
| // |
| // When using the option to keep crashes, there will be an additional line |
| // a: Crash |
| // |
| // The option -r can be used to also process expectations in lines with |
| // comments. In this mode, deleted comments are collected and either printed |
| // out or written to a separate file (with -w). |
| // |
| // The option -i (with -r) tries to resolve the status of issues mentioned in |
| // comments and adds it to the collected comments. This requires an issue.log |
| // file as described in [parseIssueFile]. |
| |
| import 'dart:io'; |
| |
| import 'package:args/args.dart'; |
| import 'package:status_file/canonical_status_file.dart'; |
| import 'package:status_file/expectation.dart'; |
| import 'package:status_file/src/expression.dart'; |
| |
| StatusEntry? filterExpectations( |
| StatusEntry entry, List<Expectation> expectationsToKeep) { |
| List<Expectation> remaining = entry.expectations |
| .where( |
| (Expectation expectation) => expectationsToKeep.contains(expectation)) |
| .toList(); |
| return remaining.isEmpty |
| ? null |
| : StatusEntry(entry.path, entry.lineNumber, remaining, entry.comment); |
| } |
| |
| late Map<String, Map<int, String>> issues; |
| |
| String getIssueState(String project, int issue) { |
| var projectIssues = issues[project]; |
| if (projectIssues == null) { |
| throw "Cannot find project $project, not one of {${issues.keys.join(",")}}"; |
| } |
| var state = projectIssues[issue] ?? ""; |
| return "\t$state"; |
| } |
| |
| // This method assumes the following data format: |
| // <project>, <state>, <issue number>, <update timestamp> |
| // sorted by issue number then timestamp ascending. |
| // |
| // The first line is expected to contain the field names and is skipped. |
| Future<void> parseIssueFile() async { |
| issues = {}; |
| String issuesLog = await File("issues.log").readAsString(); |
| List<String> lines = issuesLog.split("\n"); |
| for (String line in lines.skip(1).where((line) => line.isNotEmpty)) { |
| List<String> fields = line.split(","); |
| if (fields.length != 4) { |
| throw "invalid issue state line $line"; |
| } |
| String project = fields[0]; |
| String state = fields[1]; |
| int issueNumber = int.parse(fields[2]); |
| issues.putIfAbsent(project, () => {})[issueNumber] = state; |
| } |
| } |
| |
| List<RegExp> co19IssuePatterns = [ |
| RegExp(r"https://github.com/dart-lang/co19/issues/(\d+)"), |
| RegExp(r"co19 issue (\d+)"), |
| ]; |
| |
| List<RegExp> sdkIssuePatterns = [ |
| RegExp(r"[Ii]ssue (\d+)"), |
| RegExp(r"#(\d+)"), |
| RegExp(r"^(\d+)$"), |
| RegExp(r"http://dartbug.com/(\d+)"), |
| RegExp(r"https://github.com/dart-lang/sdk/issues/(\d+)"), |
| ]; |
| |
| String getIssueText(String comment, bool resolveState) { |
| int? issue; |
| late String prefix; |
| late String project; |
| for (var pattern in co19IssuePatterns) { |
| var match = pattern.firstMatch(comment); |
| if (match != null) { |
| issue = int.tryParse(match[1]!); |
| if (issue != null) { |
| prefix = "https://github.com/dart-lang/co19/issues/"; |
| project = "dart-lang/co19"; |
| break; |
| } |
| } |
| } |
| if (issue == null) { |
| for (var pattern in sdkIssuePatterns) { |
| var match = pattern.firstMatch(comment); |
| if (match != null) { |
| issue = int.tryParse(match[1]!); |
| if (issue != null) { |
| prefix = "https://dartbug.com/"; |
| project = "dart-lang/sdk"; |
| break; |
| } |
| } |
| } |
| } |
| if (issue != null) { |
| var state = resolveState ? getIssueState(project, issue) : ""; |
| return "$prefix$issue$state"; |
| } else { |
| return ""; |
| } |
| } |
| |
| Future<StatusFile> removeNonEssentialEntries( |
| StatusFile statusFile, |
| List<Expectation> expectationsToKeep, |
| bool removeComments, |
| List<String> comments, |
| bool resolveIssueState) async { |
| List<StatusSection> sections = <StatusSection>[]; |
| for (StatusSection section in statusFile.sections) { |
| bool hasStatusEntries = false; |
| List<Entry> entries = <Entry>[]; |
| for (Entry entry in section.entries) { |
| if (entry is EmptyEntry) { |
| entries.add(entry); |
| } else if (entry is CommentEntry) { |
| entries.add(entry); |
| hasStatusEntries = true; |
| } else if (entry is StatusEntry) { |
| StatusEntry? newEntry = entry; |
| if (entry.comment == null) { |
| newEntry = filterExpectations(entry, expectationsToKeep); |
| } else if (removeComments) { |
| newEntry = filterExpectations(entry, expectationsToKeep); |
| // Store comment if entry will be removed. |
| if (newEntry == null) { |
| String comment = entry.comment.toString().substring(1).trim(); |
| String testName = entry.path; |
| String expectations = entry.expectations.toString(); |
| // Remove '[' and ']'. |
| expectations = expectations.substring(1, expectations.length - 1); |
| String conditionPrefix = section.condition != Expression.always |
| ? "${section.condition}" |
| : ""; |
| String issueText = getIssueText(comment, resolveIssueState); |
| String statusLine = "$conditionPrefix\t$testName\t$expectations" |
| "\t$comment\t$issueText"; |
| comments.add(statusLine); |
| } |
| } |
| if (newEntry != null) { |
| entries.add(newEntry); |
| hasStatusEntries = true; |
| } |
| } else { |
| throw "Unknown entry type ${entry.runtimeType}"; |
| } |
| } |
| |
| var isDefaultSection = section.condition == Expression.always; |
| if (hasStatusEntries || |
| (isDefaultSection && section.sectionHeaderComments.isNotEmpty)) { |
| var newSection = |
| StatusSection(section.condition, -1, section.sectionHeaderComments); |
| newSection.entries.addAll(entries); |
| sections.add(newSection); |
| } |
| } |
| |
| var newStatusFile = StatusFile(statusFile.path); |
| newStatusFile.sections.addAll(sections); |
| return newStatusFile; |
| } |
| |
| ArgParser buildParser() { |
| var parser = ArgParser(); |
| parser.addFlag("overwrite", |
| abbr: 'w', |
| negatable: false, |
| defaultsTo: false, |
| help: "Overwrite input file with output."); |
| parser.addFlag("keep-crashes", |
| abbr: 'c', negatable: false, defaultsTo: false); |
| parser.addFlag("remove-comments", |
| abbr: 'r', negatable: false, defaultsTo: false); |
| parser.addFlag("resolve-issue-states", |
| abbr: 'i', negatable: false, defaultsTo: false); |
| parser.addFlag("help", |
| abbr: "h", |
| negatable: false, |
| defaultsTo: false, |
| help: "Show help and commands for this tool."); |
| return parser; |
| } |
| |
| void printHelp(ArgParser parser) { |
| print("Usage: dart pkg/status_file/bin/remove_non_essential_entries.dart" |
| " <path>"); |
| print(parser.usage); |
| } |
| |
| String formatComments(List<String> comments) { |
| StringBuffer sb = StringBuffer(); |
| for (String statusLine in comments) { |
| sb.writeln(statusLine); |
| } |
| return sb.toString(); |
| } |
| |
| main(List<String> arguments) async { |
| var parser = buildParser(); |
| var results = parser.parse(arguments); |
| if (results["help"] || results.rest.isEmpty) { |
| printHelp(parser); |
| return; |
| } |
| |
| final List<Expectation> expectationsToKeep = <Expectation>[ |
| Expectation.skip, |
| Expectation.skipByDesign, |
| Expectation.skipSlow, |
| Expectation.slow, |
| Expectation.extraSlow |
| ]; |
| |
| if (results["keep-crashes"]) { |
| expectationsToKeep.add(Expectation.crash); |
| } |
| |
| bool removeComments = results["remove-comments"]; |
| |
| for (String path in results.rest) { |
| List<String> comments = []; |
| |
| bool writeFile = results["overwrite"]; |
| bool resolveGithubIssueState = results["resolve-issue-states"]; |
| var statusFile = StatusFile.read(path); |
| if (resolveGithubIssueState) { |
| await parseIssueFile(); |
| } |
| statusFile = await removeNonEssentialEntries(statusFile, expectationsToKeep, |
| removeComments, comments, resolveGithubIssueState); |
| if (writeFile) { |
| await File(path).writeAsString(statusFile.toString()); |
| print("Wrote $path."); |
| if (removeComments) { |
| await File("$path.csv").writeAsString(formatComments(comments)); |
| print("Wrote $path.csv."); |
| } |
| } else { |
| print(statusFile); |
| if (removeComments) { |
| print(""); |
| print(formatComments(comments)); |
| } |
| } |
| } |
| } |