blob: 67b1d394d6b342a638e846420e8f482f81f1f1a8 [file] [log] [blame]
// 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();
}