| // Copyright (c) 2020, 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:collection'; | |
| import 'dart:convert'; | |
| import 'dart:io'; | |
| import 'package:args/args.dart'; | |
| import 'package:collection/collection.dart'; | |
| import 'package:path/path.dart' as p; | |
| import '../tool/expected_output.dart'; | |
| import 'stats_lib.dart'; | |
| final _configs = List<Config>.unmodifiable([ | |
| Config.commonMarkConfig, | |
| Config.gfmConfig, | |
| ]); | |
| Future<void> main(List<String> args) async { | |
| final parser = ArgParser() | |
| ..addOption( | |
| 'section', | |
| help: 'Restrict tests to one section, provided after the option.', | |
| ) | |
| ..addFlag('raw', help: 'raw JSON format', negatable: false) | |
| ..addFlag( | |
| 'update-files', | |
| help: 'Update stats files in $toolDir', | |
| negatable: false, | |
| ) | |
| ..addFlag( | |
| 'verbose', | |
| help: 'Print details for failures and errors.', | |
| negatable: false, | |
| ) | |
| ..addFlag( | |
| 'verbose-loose', | |
| help: 'Print details for "loose" matches.', | |
| negatable: false, | |
| ) | |
| ..addOption('flavor', allowed: _configs.map((c) => c.prefix)) | |
| ..addFlag('help', negatable: false); | |
| ArgResults options; | |
| try { | |
| options = parser.parse(args); | |
| } on FormatException catch (e) { | |
| stderr.writeln(e); | |
| print(parser.usage); | |
| exitCode = 64; // unix standard improper usage | |
| return; | |
| } | |
| if (options['help'] as bool) { | |
| print(parser.usage); | |
| return; | |
| } | |
| final specifiedSection = options['section'] as String?; | |
| final raw = options['raw'] as bool; | |
| final verbose = options['verbose'] as bool; | |
| final verboseLooseMatch = options['verbose-loose'] as bool; | |
| final updateFiles = options['update-files'] as bool; | |
| if (updateFiles && (raw || verbose || (specifiedSection != null))) { | |
| stderr.writeln('The `update-files` flag must be used by itself'); | |
| print(parser.usage); | |
| exitCode = 64; // unix standard improper usage | |
| return; | |
| } | |
| var testPrefix = options['flavor'] as String?; | |
| if (!updateFiles) { | |
| testPrefix = _configs.first.prefix; | |
| } | |
| final testPrefixes = testPrefix == null | |
| ? _configs.map((c) => c.prefix) | |
| : <String>[testPrefix]; | |
| for (final testPrefix in testPrefixes) { | |
| await _processConfig( | |
| testPrefix, | |
| raw, | |
| updateFiles, | |
| verbose, | |
| specifiedSection, | |
| verboseLooseMatch, | |
| ); | |
| } | |
| } | |
| final _sectionNameReplace = RegExp(r'[ \)\(]+'); | |
| String _unitOutput(Iterable<DataCase> cases) => cases | |
| .map( | |
| (dataCase) => | |
| ''' | |
| >>> ${dataCase.front_matter} | |
| ${dataCase.input}<<< | |
| ${dataCase.expectedOutput}''', | |
| ) | |
| .join(); | |
| /// Set this to `true` and rerun `--update-files` to ease finding easy strict | |
| /// fixes. | |
| const _improveStrict = false; | |
| Future<void> _processConfig( | |
| String testPrefix, | |
| bool raw, | |
| bool updateFiles, | |
| bool verbose, | |
| String? specifiedSection, | |
| bool verboseLooseMatch, | |
| ) async { | |
| final config = _configs.singleWhere((c) => c.prefix == testPrefix); | |
| final sections = loadCommonMarkSections(testPrefix); | |
| final scores = SplayTreeMap<String, SplayTreeMap<int, CompareLevel>>( | |
| compareAsciiLowerCaseNatural, | |
| ); | |
| for (final entry in sections.entries) { | |
| if (specifiedSection != null && entry.key != specifiedSection) { | |
| continue; | |
| } | |
| final units = <DataCase>[]; | |
| for (final e in entry.value) { | |
| final result = compareResult( | |
| config, | |
| e, | |
| verboseFail: verbose, | |
| verboseLooseMatch: verboseLooseMatch, | |
| extensions: e.extensions, | |
| ); | |
| units.add( | |
| DataCase( | |
| front_matter: result.testCase.toString(), | |
| input: result.testCase.markdown, | |
| expectedOutput: | |
| (_improveStrict && result.compareLevel == CompareLevel.loose) | |
| ? result.testCase.html | |
| : result.result!, | |
| ), | |
| ); | |
| final nestedMap = scores.putIfAbsent( | |
| entry.key, | |
| SplayTreeMap<int, CompareLevel>.new, | |
| ); | |
| nestedMap[e.example] = result.compareLevel; | |
| } | |
| if (updateFiles && units.isNotEmpty) { | |
| var fileName = entry.key.toLowerCase().replaceAll( | |
| _sectionNameReplace, | |
| '_', | |
| ); | |
| while (fileName.endsWith('_')) { | |
| fileName = fileName.substring(0, fileName.length - 1); | |
| } | |
| fileName = '$fileName.unit'; | |
| File(p.join('test', testPrefix, fileName)) | |
| ..createSync(recursive: true) | |
| ..writeAsStringSync(_unitOutput(units)); | |
| } | |
| } | |
| if (raw || updateFiles) { | |
| await _printRaw(testPrefix, scores, updateFiles); | |
| } | |
| if (!raw || updateFiles) { | |
| await _printFriendly(testPrefix, scores, updateFiles); | |
| } | |
| } | |
| Object? _convert(Object? obj) { | |
| if (obj is CompareLevel) { | |
| return obj.name; | |
| } | |
| if (obj is Map) { | |
| return obj.map<String, Object?>( | |
| (key, value) => MapEntry(key.toString(), value), | |
| ); | |
| } | |
| return obj; | |
| } | |
| Future<void> _printRaw( | |
| String testPrefix, | |
| Map<String, Map<int, CompareLevel>> scores, | |
| bool updateFiles, | |
| ) async { | |
| IOSink sink; | |
| if (updateFiles) { | |
| final file = getStatsFile(testPrefix); | |
| print('Updating ${file.path}'); | |
| sink = file.openWrite(); | |
| } else { | |
| sink = stdout; | |
| } | |
| const encoder = JsonEncoder.withIndent(' ', _convert); | |
| try { | |
| sink.writeln(encoder.convert(scores)); | |
| // ignore: avoid_catching_errors | |
| } on JsonUnsupportedObjectError catch (e) { | |
| stderr.writeln(e.cause); | |
| stderr.writeln(e.unsupportedObject.runtimeType); | |
| rethrow; | |
| } | |
| await sink.flush(); | |
| await sink.close(); | |
| } | |
| String _pct(int value, int total, String section) => | |
| '${value.toString().padLeft(4)} ' | |
| 'of ${total.toString().padLeft(4)} ' | |
| '– ${(100 * value / total).toStringAsFixed(1).padLeft(5)}% $section'; | |
| Future<void> _printFriendly( | |
| String testPrefix, | |
| SplayTreeMap<String, SplayTreeMap<int, CompareLevel>> scores, | |
| bool updateFiles, | |
| ) async { | |
| var totalValid = 0; | |
| var totalStrict = 0; | |
| var totalExamples = 0; | |
| IOSink sink; | |
| if (updateFiles) { | |
| final path = p.join(toolDir, '${testPrefix}_stats.txt'); | |
| print('Updating $path'); | |
| final file = File(path); | |
| sink = file.openWrite(); | |
| } else { | |
| sink = stdout; | |
| } | |
| scores.forEach((section, Map<int, CompareLevel> map) { | |
| final total = map.values.length; | |
| totalExamples += total; | |
| final sectionStrictCount = map.values | |
| .where((val) => val == CompareLevel.strict) | |
| .length; | |
| final sectionLooseCount = map.values | |
| .where((val) => val == CompareLevel.loose) | |
| .length; | |
| final sectionValidCount = sectionStrictCount + sectionLooseCount; | |
| totalStrict += sectionStrictCount; | |
| totalValid += sectionValidCount; | |
| sink.writeln(_pct(sectionValidCount, total, section)); | |
| }); | |
| sink.writeln(_pct(totalValid, totalExamples, 'TOTAL')); | |
| sink.writeln(_pct(totalStrict, totalValid, 'TOTAL Strict')); | |
| await sink.flush(); | |
| await sink.close(); | |
| } |