blob: 53fb0b95376cfae1da475c4ce98a3140631870f4 [file] [log] [blame]
// 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.
import 'dart:convert';
import 'dart:io';
import 'dart:mirrors';
import 'package:html/dom.dart' show Element;
import 'package:html/parser.dart' show parseFragment;
import 'package:markdown/markdown.dart' show markdownToHtml, ExtensionSet;
import 'package:path/path.dart' as p;
import '../test/util.dart';
// Locate the "tool" directory. Use mirrors so that this works with the test
// package, which loads this suite into an isolate.
String get toolDir =>
p.dirname((reflect(loadCommonMarkSections) as ClosureMirror)
.function
.location
.sourceUri
.path);
File getStatsFile(String prefix) =>
File(p.join(toolDir, '${prefix}_stats.json'));
Map<String, List<CommonMarkTestCase>> loadCommonMarkSections(
String testPrefix) {
var testFile = File(p.join(toolDir, '${testPrefix}_tests.json'));
var testsJson = testFile.readAsStringSync();
var testArray = jsonDecode(testsJson) as List;
var sections = <String, List<CommonMarkTestCase>>{};
for (var exampleMap in testArray) {
var exampleTest =
CommonMarkTestCase.fromJson(exampleMap as Map<String, dynamic>);
var sectionList =
sections.putIfAbsent(exampleTest.section, () => <CommonMarkTestCase>[]);
sectionList.add(exampleTest);
}
return sections;
}
class Config {
static final Config commonMarkConfig =
Config._('common_mark', 'http://spec.commonmark.org/0.28/', null);
static final Config gfmConfig = Config._(
'gfm', 'https://github.github.com/gfm/', ExtensionSet.gitHubFlavored);
final String prefix;
final String baseUrl;
final ExtensionSet extensionSet;
Config._(this.prefix, this.baseUrl, this.extensionSet);
}
class CommonMarkTestCase {
final String markdown;
final String section;
final int example;
final String html;
final int startLine;
final int endLine;
CommonMarkTestCase(this.example, this.section, this.startLine, this.endLine,
this.markdown, this.html);
factory CommonMarkTestCase.fromJson(Map<String, dynamic> json) {
return CommonMarkTestCase(
json['example'] as int,
json['section'] as String,
json['start_line'] as int,
json['end_line'] as int,
json['markdown'] as String,
json['html'] as String);
}
@override
String toString() => '$section - $example';
}
enum CompareLevel { strict, loose, fail, error }
class CompareResult {
final CompareLevel compareLevel;
final CommonMarkTestCase testCase;
final String result;
CompareResult(this.testCase, this.result, this.compareLevel);
}
CompareResult compareResult(Config config, CommonMarkTestCase testCase,
{bool throwOnError = false,
bool verboseFail = false,
bool verboseLooseMatch = false}) {
String output;
try {
output =
markdownToHtml(testCase.markdown, extensionSet: config.extensionSet);
} catch (err, stackTrace) {
if (throwOnError) {
rethrow;
}
if (verboseFail) {
_printVerboseFailure(
config.baseUrl, 'ERROR', testCase, 'Thrown: $err\n$stackTrace');
}
return CompareResult(testCase, null, CompareLevel.error);
}
if (testCase.html == output) {
return CompareResult(testCase, output, CompareLevel.strict);
}
var expectedParsed = parseFragment(testCase.html);
var actual = parseFragment(output);
var looseMatch = _compareHtml(expectedParsed.children, actual.children);
if (!looseMatch && verboseFail) {
_printVerboseFailure(config.baseUrl, 'FAIL', testCase, output);
}
if (looseMatch && verboseLooseMatch) {
_printVerboseFailure(config.baseUrl, 'LOOSE', testCase, output);
}
return CompareResult(
testCase, output, looseMatch ? CompareLevel.loose : CompareLevel.fail);
}
String _indent(String s) =>
s.splitMapJoin('\n', onNonMatch: (n) => ' ${whitespaceColor(n)}');
void _printVerboseFailure(String baseUrl, String message,
CommonMarkTestCase testCase, String actual) {
print('$message: $baseUrl#example-${testCase.example} '
'@ ${testCase.section}');
print('input:');
print(_indent(testCase.markdown));
print('expected:');
print(_indent(testCase.html));
print('actual:');
print(_indent(actual));
print('-----------------------');
}
/// Compare two DOM trees for equality.
bool _compareHtml(
List<Element> expectedElements, List<Element> actualElements) {
if (expectedElements.length != actualElements.length) {
return false;
}
for (var childNum = 0; childNum < expectedElements.length; childNum++) {
var expected = expectedElements[childNum];
var actual = actualElements[childNum];
if (expected.runtimeType != actual.runtimeType) {
return false;
}
if (expected.localName != actual.localName) {
return false;
}
if (expected.attributes.length != actual.attributes.length) {
return false;
}
var expectedAttrKeys = expected.attributes.keys.toList();
expectedAttrKeys.sort();
var actualAttrKeys = actual.attributes.keys.toList();
actualAttrKeys.sort();
for (var attrNum = 0; attrNum < actualAttrKeys.length; attrNum++) {
var expectedAttrKey = expectedAttrKeys[attrNum];
var actualAttrKey = actualAttrKeys[attrNum];
if (expectedAttrKey != actualAttrKey) {
return false;
}
if (expected.attributes[expectedAttrKey] !=
actual.attributes[actualAttrKey]) {
return false;
}
}
var childrenEqual = _compareHtml(expected.children, actual.children);
if (!childrenEqual) {
return false;
}
}
return true;
}