| // Copyright 2023 The Flutter Authors | |
| // Use of this source code is governed by a BSD-style license that can be | |
| // found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd. | |
| import 'dart:async'; | |
| import 'dart:io'; | |
| import 'package:args/command_runner.dart'; | |
| import 'package:io/io.dart'; | |
| import 'package:path/path.dart' as path; | |
| import '../utils.dart'; | |
| const _runIdArg = 'run-id'; | |
| /// A command for downloading and applying golden fixes, when they are broken. | |
| class FixGoldensCommand extends Command { | |
| FixGoldensCommand() { | |
| argParser.addOption( | |
| _runIdArg, | |
| help: | |
| 'The ID of the workflow run where the goldens are failing. ' | |
| 'e.g.https://github.com/flutter/devtools/actions/runs/<run-id>/job/16691428186', | |
| valueHelp: '12345', | |
| mandatory: true, | |
| ); | |
| } | |
| @override | |
| String get description => | |
| 'A command for downloading and applying golden fixes when they are broken on the CI.'; | |
| @override | |
| String get name => 'fix-goldens'; | |
| final processManager = ProcessManager(); | |
| @override | |
| FutureOr? run() async { | |
| // Change the CWD to the repo root | |
| Directory.current = pathFromRepoRoot(""); | |
| final runId = argResults![_runIdArg] as String; | |
| final tmpDownloadDir = await Directory.systemTemp.createTemp(); | |
| try { | |
| print('Note: ensure you have gh installed and authenticated.'); | |
| print('Downloading the artifacts to ${tmpDownloadDir.path}'); | |
| await processManager.runProcess( | |
| CliCommand('gh', [ | |
| 'run', | |
| 'download', | |
| runId, | |
| '-p', | |
| '*golden_image_failures*', | |
| '-R', | |
| 'github.com/flutter/devtools', | |
| '-D', | |
| tmpDownloadDir.path, | |
| ]), | |
| ); | |
| final downloadedGoldens = tmpDownloadDir | |
| .listSync(recursive: true) | |
| .where((e) => e.path.endsWith('testImage.png')); | |
| final allLocalGoldenPngs = Directory( | |
| pathFromRepoRoot("packages/devtools_app/test/"), | |
| ).listSync(recursive: true).where((e) => e.path.endsWith('.png')); | |
| for (final downloadedGolden in downloadedGoldens) { | |
| final downloadedGoldenBaseName = path.basename(downloadedGolden.path); | |
| final expectedGoldenFileName = | |
| '${RegExp(r'(^.*)_testImage.png').firstMatch(downloadedGoldenBaseName)?.group(1)}.png'; | |
| final fileMatches = allLocalGoldenPngs.where( | |
| (e) => e.path.endsWith(expectedGoldenFileName), | |
| ); | |
| final String destinationPath; | |
| if (fileMatches.isEmpty) { | |
| throw 'Could not find a golden Image for $downloadedGoldenBaseName using $expectedGoldenFileName as ' | |
| 'the name of the search.'; | |
| } else if (fileMatches.length == 1) { | |
| destinationPath = fileMatches.first.path; | |
| } else { | |
| print("Multiple goldens found for ${downloadedGolden.path}"); | |
| print("Select which golden should be overridden:"); | |
| for (int i = 0; i < fileMatches.length; i++) { | |
| final fileMatch = fileMatches.elementAt(i); | |
| print('${i + 1}) ${fileMatch.path}'); | |
| } | |
| final userSelection = int.parse(stdin.readLineSync()!); | |
| destinationPath = fileMatches.elementAt(userSelection - 1).path; | |
| } | |
| await downloadedGolden.rename(destinationPath); | |
| print("Fixed: $destinationPath"); | |
| } | |
| print('Done updating ${downloadedGoldens.length} goldens'); | |
| } finally { | |
| tmpDownloadDir.deleteSync(recursive: true); | |
| } | |
| } | |
| } |