blob: 5cd18e912d474e2bbe82a3cbad79b6ee12adc29f [file] [edit]
// Copyright (c) 2023, 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:io';
import 'package:dart_style/dart_style.dart';
import 'package:dart_style/src/dart_version_history.dart';
import 'package:dart_style/src/testing/test_file.dart';
import 'package:path/path.dart' as p;
import 'package:pub_semver/pub_semver.dart';
/// Update the formatting test expectations based on the current formatter's
/// output.
///
/// The command line arguments should be the names of tests to be updated. A
/// name can be a directory to update all of the tests in that directory or a
/// file path to update the tests in that file.
///
/// All paths are relative to the package root directory.
///
/// Note: This script can't correctly update any tests that contain the special
/// "×XX" Unicode markers or selections.
// TODO(rnystrom): Support updating individual tests within a file.
void main(List<String> arguments) async {
if (arguments.isEmpty) {
print('Usage: update_tests.dart <tests...>');
exit(1);
}
for (var argument in arguments) {
var path = p.join(await findPackageDirectory(), argument);
if (Directory(path).existsSync()) {
await _updateDirectory(path);
} else if (File(path).existsSync()) {
await _updateFile(path);
}
}
if (_totalTests > 0) {
print('Changed $_changedTests out of $_totalTests updated tests');
} else {
print('No updatable tests found');
}
if (_skippedFiles > 0) {
print(
'Skipped $_skippedFiles files '
'which contain selections or Unicode escapes',
);
}
}
int _totalTests = 0;
int _changedTests = 0;
int _skippedFiles = 0;
Future<void> _updateDirectory(String path) async {
for (var testFile in await TestFile.listDirectory(path)) {
await _updateTestFile(testFile);
}
}
Future<void> _updateFile(String path) async {
await _updateTestFile(await TestFile.read(path));
}
Future<void> _updateTestFile(TestFile testFile) async {
// TODO(rnystrom): The test updater doesn't know how to handle selection
// markers or Unicode escapes in tests, so just skip any file that contains
// tests with those in it.
var testSource = File(p.join('test', testFile.path)).readAsStringSync();
if (testSource.contains('‹') || testSource.contains('×')) {
print('Skipped ${testFile.path}');
_skippedFiles++;
return;
}
var buffer = StringBuffer();
// Write the page width line if needed.
var pageWidth = testFile.pageWidth;
if (pageWidth != null) {
var columns = '$pageWidth columns';
buffer.write(columns);
buffer.write(' ' * (pageWidth - columns.length));
buffer.writeln('|');
}
// Write the file level options.
if (_optionStrings(testFile.options) case var options
when options.isNotEmpty) {
buffer.writeln(options.join(' '));
}
// Write the file-level comments.
_writeComments(buffer, testFile.comments);
_totalTests += testFile.tests.length;
// Write the tests.
for (var formatTest in testFile.tests) {
_updateTest(buffer, testFile, formatTest);
}
// Rewrite the file. Do this even if nothing changed so that we normalize the
// test markers.
var path = p.join(await findTestDirectory(), testFile.path);
File(path).writeAsStringSync(buffer.toString());
}
void _updateTest(
StringBuffer buffer,
TestFile testFile,
FormatTest formatTest,
) {
// Write the test input.
var description = [
..._optionStrings(formatTest.options),
formatTest.input.description,
].join(' ');
buffer.writeln('>>> $description'.trim());
_writeComments(buffer, formatTest.input.comments);
buffer.write(formatTest.input.code.text);
// Run the formatter for every supported version. Produce an output for every
// range of versions whose output is the same and label it with the beginning
// of that range.
var versionsToTest = _versionsToTest(testFile, formatTest);
String? previousVersionOutput;
var changed = false;
for (var version in versionsToTest) {
var formatter = testFile.formatterForTest(formatTest, version);
var output = formatter.formatSource(formatTest.input.code).text;
if (output != previousVersionOutput) {
changed |= _writeOutputSection(
buffer,
testFile,
formatTest,
version,
output,
);
previousVersionOutput = output;
}
}
if (formatTest.unsupportedVersion case var version?) {
buffer.writeln('<<< ${version.majorMinor} (unsupported)');
}
if (changed) {
print('Updated ${testFile.path} ${formatTest.label}');
_changedTests++;
}
}
/// Determine which range of language versions should be used for [formatTest].
List<Version> _versionsToTest(TestFile testFile, FormatTest formatTest) {
// The test must already have at least one output. We use the lowest output
// version as the beginning of the range because we assume the test author
// knows the tested syntax isn't supported on older versions.
var startVersion = formatTest.outputs.keys.first;
// If the test already has an unsupported marker, then we assume the test
// author knows the tested syntax isn't supported on later language versions
// so stop there.
var endVersion = switch (formatTest.unsupportedVersion) {
var unsupported? => DartVersionHistory.before(unsupported),
_ when testFile.isTall => DartVersionHistory.latest,
_ => DartVersionHistory.latestShortStyle,
};
return DartVersionHistory.all
.where((v) => v >= startVersion && v <= endVersion)
.toList();
}
/// Returns a list of strings for all of the options specified by [options].
List<String> _optionStrings(TestOptions options) => [
for (var experiment in options.experimentFlags) '(experiment $experiment)',
if (options.leadingIndent case var indent?) '(indent $indent)',
if (options.trailingCommas == TrailingCommas.preserve)
'(trailing_commas preserve)',
];
void _writeComments(StringBuffer buffer, List<String> comments) {
for (var comment in comments) {
buffer.writeln(comment);
}
}
/// Formats [formatTest] as [version] and writes the resulting output to
/// [buffer].
///
/// Returns `true` if the output changed from what was previously in the file.
bool _writeOutputSection(
StringBuffer buffer,
TestFile testFile,
FormatTest formatTest,
Version version,
String actualText,
) {
// Preserve the description and comments from the existing output section if
// there is one.
var originalEntry = formatTest.outputs[version];
var description = originalEntry?.description ?? '';
buffer.writeln('<<< ${version.majorMinor} $description'.trim());
if (originalEntry != null) _writeComments(buffer, originalEntry.comments);
buffer.write(actualText);
// When formatting a statement, the formatter correctly doesn't add a
// trailing newline, but we need one to separate this output from the
// next test.
if (!testFile.isCompilationUnit) buffer.writeln();
return originalEntry == null || actualText != originalEntry.code.text;
}