// 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:collection/collection.dart'; import 'package:dart_style/dart_style.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 arguments) async { if (arguments.isEmpty) { print('Usage: update_tests.dart '); 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 _updateDirectory(String path) async { for (var testFile in await TestFile.listDirectory(path)) { await _updateTestFile(testFile); } } Future _updateFile(String path) async { await _updateTestFile(await TestFile.read(path)); } Future _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) { // 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); // Write the test outputs. var changed = false; switch (formatTest) { case UnversionedFormatTest(:var output): changed = _writeOutput(buffer, testFile, formatTest, output); case VersionedFormatTest(:var outputs): // Order the outputs by version. var versions = outputs.keys.toList()..sort(); // The outputs were reordered, the test was changed. if (!const DeepCollectionEquality().equals( versions, outputs.keys.toList(), )) { print('Re-ordered outputs for ${testFile.path} ${formatTest.label}'); } // Write the outputs at their versions. for (var version in versions) { changed |= _writeOutput( buffer, testFile, formatTest, outputs[version]!, version: version, ); } } if (changed) { print('Updated ${testFile.path} ${formatTest.label}'); _changedTests++; } } // 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()); } /// Returns a list of strings for all of the options specified by [options]. List _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 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 _writeOutput( StringBuffer buffer, TestFile testFile, FormatTest formatTest, TestEntry output, { Version? version, }) { var outputDescription = [ // Include the version in the description if the output is versioned. if (version != null) '${version.major}.${version.minor}', output.description, ].join(' '); buffer.writeln('<<< $outputDescription'.trim()); _writeComments(buffer, output.comments); var defaultVersion = testFile.isTall ? DartFormatter.latestLanguageVersion : DartFormatter.latestShortStyleLanguageVersion; var formatter = testFile.formatterForTest( formatTest, version ?? defaultVersion, ); var actual = formatter.formatSource(formatTest.input.code); buffer.write(actual.text); // 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 actual.text != output.code.text; }