blob: ebeca065f7fba936c7c8061fb2a39136c0ed6bbd [file]
// Copyright (c) 2026, 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.
@TestOn('vm')
library;
import 'dart:convert';
import 'package:test/test.dart';
import '../tool/expected_output.dart';
import '../tool/stats_lib.dart';
void main() {
test('parseSpecExamples extracts examples and skips disabled ones', () {
const spec = '''
# Tabs <!-- comment -->
```````````````````````````````` example
→foo
.
<pre><code>foo
</code></pre>
````````````````````````````````
# Strikethrough (extension)
```````````````````````````````` example strikethrough
~~Hi~~
.
<p><del>Hi</del></p>
````````````````````````````````
```````````````````````````````` example disabled
ignored
.
<p>ignored</p>
````````````````````````````````
''';
final examples = parseSpecExamples(spec);
expect(examples, hasLength(2));
expect(examples[0].example, 1);
expect(examples[0].section, 'Tabs');
expect(examples[0].markdown, '\tfoo\n');
expect(examples[0].html, '<pre><code>foo\n</code></pre>\n');
expect(examples[0].extensions, isEmpty);
expect(examples[0].toJson().containsKey('extensions'), isFalse);
expect(examples[1].example, 2);
expect(examples[1].section, 'Strikethrough (extension)');
expect(examples[1].markdown, '~~Hi~~\n');
expect(examples[1].html, '<p><del>Hi</del></p>\n');
expect(examples[1].extensions, {'strikethrough'});
expect(examples[1].toJson()['extensions'], ['strikethrough']);
});
test('parseSpecExamples throws FormatException on malformed examples', () {
expect(
() => parseSpecExamples(
'```````````````````````````````` example\nfoo\n'
'````````````````````````````````\n',
),
throwsFormatException,
);
expect(
() => parseSpecExamples(
'```````````````````````````````` example\nfoo\n.\n<p>foo</p>\n',
),
throwsFormatException,
);
});
for (final prefix in ['common_mark', 'gfm']) {
test('$prefix unit files and stats are in sync with spec tests', () {
final sections = loadCommonMarkSections(prefix);
final statsJson =
jsonDecode(getStatsFile(prefix).readAsStringSync())
as Map<String, dynamic>;
final specExamplesByHeader = <String, CommonMarkTestCase>{};
final statsLevelByHeader = <String, String?>{};
for (final entry in sections.entries) {
final sectionStats = statsJson[entry.key] as Map<String, dynamic>?;
expect(
sectionStats,
isNotNull,
reason:
'Missing section "${entry.key}" in tool/${prefix}_stats.json. '
'Run `dart run tool/stats.dart --update-files`.',
);
for (final testCase in entry.value) {
final header = testCase.toString();
specExamplesByHeader[header] = testCase;
statsLevelByHeader[header] =
sectionStats![testCase.example.toString()] as String?;
}
}
final unitCases = dataCasesUnder(testDirectory: prefix).toList();
expect(
unitCases.length,
specExamplesByHeader.length,
reason:
'test/$prefix/*.unit has ${unitCases.length} cases, but '
'tool/${prefix}_tests.json has ${specExamplesByHeader.length}. '
'Do not hand-edit generated files in test/$prefix/; add custom '
'tests to test/extensions/ or test/original/ and run '
'`dart run tool/stats.dart --update-files`.',
);
for (final dataCase in unitCases) {
final header = _unitHeader(dataCase);
final specCase = specExamplesByHeader[header];
expect(
specCase,
isNotNull,
reason:
'Unexpected test case "$header" in '
'test/${dataCase.directory}/${dataCase.file}.unit. '
'Run `dart run tool/stats.dart --update-files`.',
);
expect(
dataCase.input,
specCase!.markdown,
reason:
'Input mismatch for "$header" in '
'test/${dataCase.directory}/${dataCase.file}.unit. '
'Run `dart run tool/stats.dart --update-files`.',
);
final isStrict = dataCase.expectedOutput == specCase.html;
final recordedLevel = statsLevelByHeader[header];
expect(
recordedLevel == 'strict',
isStrict,
reason:
'Stats mismatch for "$header" in tool/${prefix}_stats.json '
'(recorded "$recordedLevel", strict match: $isStrict). '
'Run `dart run tool/stats.dart --update-files`.',
);
}
});
test('$prefix divergences are documented and current', () {
final specCases = {
for (final cases in loadCommonMarkSections(prefix).values)
for (final testCase in cases) testCase.example: testCase,
};
final unitOutputs = {
for (final dataCase in dataCasesUnder(testDirectory: prefix))
_unitHeader(dataCase): dataCase.expectedOutput,
};
final statsJson =
jsonDecode(getStatsFile(prefix).readAsStringSync())
as Map<String, dynamic>;
final divergences = loadDivergences(prefix);
for (final MapEntry(key: example, value: divergence)
in divergences.entries) {
final where = 'tool/divergences.json $prefix/$example';
final specCase = specCases[example];
expect(specCase, isNotNull, reason: '$where: no such spec example.');
expect(divergence.reason, isNotEmpty, reason: '$where: needs reason.');
expect(
Uri.tryParse(divergence.link)?.isScheme('https'),
isTrue,
reason: '$where: link must be an https URL.',
);
expect(
divergence.html,
isNot(specCase!.html),
reason: '$where: output now matches the spec; remove the entry.',
);
expect(
unitOutputs[specCase.toString()],
divergence.html,
reason:
'$where: actual output no longer matches the recorded "html".',
);
}
final recordedDiverged = [
for (final section in statsJson.values.cast<Map<String, dynamic>>())
for (final MapEntry(:key, :value) in section.entries)
if (value == 'diverged') int.parse(key),
];
expect(
recordedDiverged,
unorderedEquals(divergences.keys),
reason:
'tool/${prefix}_stats.json and tool/divergences.json disagree. '
'Run `dart run tool/stats.dart --update-files`.',
);
});
}
}
/// The spec test header (e.g. `Tabs - 1`) of a generated `.unit` [dataCase].
String _unitHeader(DataCase dataCase) {
final colonIndex = dataCase.description.indexOf(': ');
return colonIndex == -1
? dataCase.description
: dataCase.description.substring(colonIndex + 2);
}