blob: 3bcd365a17ba700406a40889979074f2e35d5057 [file] [log] [blame]
import 'dart:io';
import 'dart:isolate';
import 'package:path/path.dart' as p;
import '../../dart_style.dart';
final _indentPattern = RegExp(r'\(indent (\d+)\)');
final _fixPattern = RegExp(r'\(fix ([a-x-]+)\)');
final _unicodeUnescapePattern = RegExp(r'×([0-9a-fA-F]{2,4})');
final _unicodeEscapePattern = RegExp('[\x0a\x0c\x0d]');
/// Get the absolute local file path to the package's "test" directory.
Future<String> findTestDirectory() async {
var libraryUri = await Isolate.resolvePackageUri(
Uri.parse('package:dart_style/src/testing/test_file.dart'));
return p
.normalize(p.join(p.dirname(libraryUri!.toFilePath()), '../../../test'));
}
/// A file containing a series of formatting tests.
class TestFile {
/// Finds all test files in the given directory relative to the package's
/// `test/` directory.
static Future<List<TestFile>> listDirectory(String name) async {
var testDir = await findTestDirectory();
var entries = Directory(p.join(testDir, name))
.listSync(recursive: true, followLinks: false);
entries.sort((a, b) => a.path.compareTo(b.path));
return [
for (var entry in entries)
if (entry is File &&
(entry.path.endsWith('.stmt') || entry.path.endsWith('.unit')))
TestFile._load(entry, p.relative(entry.path, from: testDir))
];
}
/// Reads the test file from [path], which is relative to the package's
/// `test/` directory.
static Future<TestFile> read(String path) async {
var testDir = await findTestDirectory();
var file = File(p.join(testDir, path));
return TestFile._load(file, p.relative(file.path, from: testDir));
}
/// Reads the test file from [file].
factory TestFile._load(File file, String relativePath) {
var lines = file.readAsLinesSync();
// The first line may have a "|" to indicate the page width.
var i = 0;
int? pageWidth;
if (lines[i].endsWith('|')) {
pageWidth = lines[i].indexOf('|');
i++;
}
var tests = <FormatTest>[];
while (i < lines.length) {
var line = i + 1;
var description = lines[i++].replaceAll('>>>', '');
var fixes = <StyleFix>[];
// Let the test specify a leading indentation. This is handy for
// regression tests which often come from a chunk of nested code.
var leadingIndent = 0;
description = description.replaceAllMapped(_indentPattern, (match) {
leadingIndent = int.parse(match[1]!);
return '';
});
// Let the test specify fixes to apply.
description = description.replaceAllMapped(_fixPattern, (match) {
fixes.add(StyleFix.all.firstWhere((fix) => fix.name == match[1]));
return '';
});
var inputBuffer = StringBuffer();
while (!lines[i].startsWith('<<<')) {
inputBuffer.writeln(lines[i++]);
}
var outputDescription = lines[i].replaceAll('<<<', '');
var outputBuffer = StringBuffer();
while (++i < lines.length && !lines[i].startsWith('>>>')) {
outputBuffer.writeln(lines[i]);
}
var isCompilationUnit = file.path.endsWith('.unit');
var input = _extractSelection(_unescapeUnicode(inputBuffer.toString()),
isCompilationUnit: isCompilationUnit);
var output = _extractSelection(_unescapeUnicode(outputBuffer.toString()),
isCompilationUnit: isCompilationUnit);
tests.add(FormatTest(input, output, description.trim(),
outputDescription.trim(), line, fixes, leadingIndent));
}
return TestFile._(relativePath, pageWidth, tests);
}
TestFile._(this.path, this.pageWidth, this.tests);
/// The path to the test file, relative to the `test/` directory.
final String path;
/// The page width for tests in this file or `null` if the default should be
/// used.
final int? pageWidth;
/// The tests in this file.
final List<FormatTest> tests;
bool get isCompilationUnit => path.endsWith('.unit');
}
/// A single formatting test inside a [TestFile].
class FormatTest {
/// The unformatted input.
final SourceCode input;
/// The expected output.
final SourceCode output;
/// The optional description of the test.
final String description;
/// If there is a remark on the "<<<" line, this is it.
final String outputDescription;
/// The 1-based index of the line where this test begins.
final int line;
/// The style fixes this test is applying.
final List<StyleFix> fixes;
/// The number of spaces of leading indentation that should be added to each
/// line.
final int leadingIndent;
FormatTest(this.input, this.output, this.description, this.outputDescription,
this.line, this.fixes, this.leadingIndent);
/// The line and description of the test.
String get label {
if (description.isEmpty) return 'line $line';
return 'line $line: $description';
}
}
/// Given a source string that contains ‹ and › to indicate a selection, returns
/// a [SourceCode] with the text (with the selection markers removed) and the
/// correct selection range.
SourceCode _extractSelection(String source, {bool isCompilationUnit = false}) {
var start = source.indexOf('‹');
source = source.replaceAll('‹', '');
var end = source.indexOf('›');
source = source.replaceAll('›', '');
return SourceCode(source,
isCompilationUnit: isCompilationUnit,
selectionStart: start == -1 ? null : start,
selectionLength: end == -1 ? null : end - start);
}
/// Turn the special Unicode escape marker syntax used in the tests into real
/// Unicode characters.
///
/// This does not use Dart's own string escape sequences so that we don't
/// accidentally modify the Dart code being formatted.
String _unescapeUnicode(String input) {
return input.replaceAllMapped(_unicodeUnescapePattern, (match) {
var codePoint = int.parse(match[1]!, radix: 16);
return String.fromCharCode(codePoint);
});
}
/// Turn the few Unicode characters used in tests back to their escape syntax.
String escapeUnicode(String input) {
return input.replaceAllMapped(_unicodeEscapePattern, (match) {
return '×${match[0]!.codeUnitAt(0).toRadixString(16)}';
});
}