| // Copyright 2014 The Flutter Authors. 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:io' as io; | |
| import 'package:file/file.dart'; | |
| import 'package:file/local.dart'; | |
| import 'package:platform/platform.dart'; | |
| import 'package:test/test.dart'; | |
| ////////////////////////////////////////////////////////////////////// | |
| // // | |
| // ✨ THINKING OF MOVING/REFACTORING THIS FILE? READ ME FIRST! ✨ // | |
| // // | |
| // There is a link to this file in //docs/tool/Engine-artfiacts.md // | |
| // and it would be very kind of you to update the link, if needed. // | |
| // // | |
| ////////////////////////////////////////////////////////////////////// | |
| void main() { | |
| // Want to test the powershell (update_engine_version.ps1) file, but running | |
| // a macOS or Linux machine? You can install powershell and then opt-in to | |
| // running `pwsh bin/internal/update_engine_version.ps1`. | |
| // | |
| // macOS: https://learn.microsoft.com/en-us/powershell/scripting/install/installing-powershell-on-macos | |
| // linux: https://learn.microsoft.com/en-us/powershell/scripting/install/installing-powershell-on-linux | |
| // | |
| // Then, set this variable to true: | |
| final bool usePowershellOnPosix = () { | |
| // Intentionally not a const so that linting doesn't go wild across the test. | |
| return false; | |
| }(); | |
| const FileSystem localFs = LocalFileSystem(); | |
| final flutterRoot = _FlutterRootUnderTest.findWithin(forcePowershell: usePowershellOnPosix); | |
| late Directory tmpDir; | |
| late _FlutterRootUnderTest testRoot; | |
| late Map<String, String> environment; | |
| void printIfNotEmpty(String prefix, String string) { | |
| if (string.isNotEmpty) { | |
| string.split(io.Platform.lineTerminator).forEach((String s) { | |
| print('$prefix:>$s<'); | |
| }); | |
| } | |
| } | |
| io.ProcessResult run(String executable, List<String> args) { | |
| final io.ProcessResult result = io.Process.runSync( | |
| executable, | |
| args, | |
| environment: environment, | |
| workingDirectory: testRoot.root.absolute.path, | |
| includeParentEnvironment: false, | |
| ); | |
| if (result.exitCode != 0) { | |
| fail( | |
| 'Failed running "$executable $args" (exit code = ${result.exitCode}),' | |
| '\nstdout: ${result.stdout}' | |
| '\nstderr: ${result.stderr}', | |
| ); | |
| } | |
| printIfNotEmpty('stdout', (result.stdout as String).trim()); | |
| printIfNotEmpty('stderr', (result.stderr as String).trim()); | |
| return result; | |
| } | |
| setUpAll(() async { | |
| if (usePowershellOnPosix) { | |
| final io.ProcessResult result = io.Process.runSync('pwsh', <String>['--version']); | |
| print( | |
| 'Using Powershell (${(result.stdout as String).trim()}) on POSIX for local debugging and testing', | |
| ); | |
| } | |
| }); | |
| /// Initializes a blank git repo in [testRoot.root]. | |
| void initGitRepoWithBlankInitialCommit() { | |
| run('git', <String>['init', '--initial-branch', 'master']); | |
| run('git', <String>['config', '--local', 'user.email', 'test@example.com']); | |
| run('git', <String>['config', '--local', 'user.name', 'Test User']); | |
| run('git', <String>['add', '.']); | |
| run('git', <String>['commit', '--allow-empty', '-m', 'Initial commit']); | |
| } | |
| late int commitCount; | |
| setUp(() async { | |
| commitCount = 0; | |
| tmpDir = localFs.systemTempDirectory.createTempSync('last_engine_commit_test.'); | |
| testRoot = _FlutterRootUnderTest.fromPath( | |
| tmpDir.childDirectory('flutter').path, | |
| forcePowershell: usePowershellOnPosix, | |
| ); | |
| environment = <String, String>{}; | |
| if (const LocalPlatform().isWindows || usePowershellOnPosix) { | |
| // Copy a minimal set of environment variables needed to run the update_engine_version script in PowerShell. | |
| const powerShellVariables = <String>['SYSTEMROOT', 'PATH', 'PATHEXT']; | |
| for (final key in powerShellVariables) { | |
| final String? value = io.Platform.environment[key]; | |
| if (value != null) { | |
| environment[key] = value; | |
| } | |
| } | |
| } | |
| // Copy the update_engine_version script and create a rough directory structure. | |
| flutterRoot.binInternalLastEngineCommit.copySyncRecursive( | |
| testRoot.binInternalLastEngineCommit.path, | |
| ); | |
| initGitRepoWithBlankInitialCommit(); | |
| }); | |
| tearDown(() { | |
| tmpDir.deleteSync(recursive: true); | |
| }); | |
| /// Runs `bin/internal/last_engine_commit.{sh|ps1}` and returns the stdout. | |
| /// | |
| /// - On Windows, `powershell` is used (to run `last_engine_commit.ps1`); | |
| /// - On POSIX, if [usePowershellOnPosix] is set, `pwsh` is used (to run `last_engine_commit.ps1`); | |
| /// - Otherwise, `last_engine_commit.sh` is used. | |
| String getLastEngineCommit() { | |
| final String executable; | |
| final List<String> args; | |
| if (const LocalPlatform().isWindows) { | |
| executable = 'powershell'; | |
| args = <String>[testRoot.binInternalLastEngineCommit.path]; | |
| } else if (usePowershellOnPosix) { | |
| executable = 'pwsh'; | |
| args = <String>[testRoot.binInternalLastEngineCommit.path]; | |
| } else { | |
| executable = testRoot.binInternalLastEngineCommit.path; | |
| args = <String>[]; | |
| } | |
| return (run(executable, args).stdout as String).trim(); | |
| } | |
| /// Gets the latest commit on the current branch. | |
| String getLastCommit() { | |
| return (run('git', <String>['rev-parse', 'HEAD']).stdout as String).trim(); | |
| } | |
| void writeCommit(Iterable<String> files) { | |
| commitCount++; | |
| for (final relativePath in files) { | |
| localFs.file(localFs.path.join(testRoot.root.path, relativePath)) | |
| ..createSync(recursive: true) | |
| ..writeAsStringSync('$commitCount'); | |
| } | |
| run('git', <String>['add', '.']); | |
| run('git', <String>['commit', '-m', 'Wrote ${files.length} files']); | |
| } | |
| test('returns the last engine commit', () { | |
| writeCommit(<String>['bin/internal/release-candidate-branch.version']); | |
| writeCommit(<String>['DEPS', 'engine/README.md']); | |
| final String lastEngine = getLastEngineCommit(); | |
| expect(lastEngine, isNotEmpty); | |
| writeCommit(<String>['CHANGELOG.md', 'dev/folder/called/engine/README.md']); | |
| expect(getLastEngineCommit(), lastEngine); | |
| }); | |
| test('considers DEPS an engine change', () { | |
| writeCommit(<String>['bin/internal/release-candidate-branch.version']); | |
| writeCommit(<String>['DEPS', 'engine/README.md']); | |
| final String lastEngineA = getLastEngineCommit(); | |
| expect(lastEngineA, isNotEmpty); | |
| writeCommit(<String>['DEPS']); | |
| final String lastEngineB = getLastEngineCommit(); | |
| expect(lastEngineB, allOf(isNotEmpty, isNot(equals(lastEngineA)))); | |
| }); | |
| test('if there have been no engine changes, uses the first commit since the branch point', () { | |
| final String initialStartingCommit = getLastCommit(); | |
| // Make an engine change *before* the branch. | |
| writeCommit(<String>['engine/README.md']); | |
| final String engineCommitPreBranch = getLastCommit(); | |
| // Write the branch file. | |
| writeCommit(<String>['bin/internal/release-candidate-branch.version']); | |
| final String initialBranchCommit = getLastCommit(); | |
| // Write another commit to make sure we don't always use the latest. | |
| writeCommit(<String>['CHANGELOG.md']); | |
| final String latestCommitIgnore = getLastCommit(); | |
| // Get the engine commit, which should fallback to HEAD~2 (in this case). | |
| final String lastCommitToEngine = getLastEngineCommit(); | |
| expect( | |
| lastCommitToEngine, | |
| initialBranchCommit, | |
| reason: | |
| 'The git history for this simulation looks like this:\n' | |
| 'master | $initialStartingCommit\n' | |
| 'master | $engineCommitPreBranch\n' | |
| 'release | $initialBranchCommit\n' | |
| 'release | $latestCommitIgnore\n' | |
| '\n' | |
| 'We expected our script to select HEAD~2, $initialBranchCommit, but ' | |
| 'instead it selected $lastCommitToEngine, which is incorrect. See ' | |
| 'the table above to help debug.', | |
| ); | |
| }); | |
| } | |
| extension on File { | |
| void copySyncRecursive(String newPath) { | |
| fileSystem.directory(fileSystem.path.dirname(newPath)).createSync(recursive: true); | |
| copySync(newPath); | |
| } | |
| } | |
| /// A FrUT, or "Flutter Root"-Under Test (parallel to a SUT, System Under Test). | |
| /// | |
| /// For the intent of this test case, the "Flutter Root" is a directory | |
| /// structure with the following elements: | |
| /// | |
| /// ```txt | |
| /// ├── DEPS | |
| /// ├── engine/ | |
| /// ├── bin/ | |
| /// │ ├── internal/ | |
| /// │ │ └── last_engine_commit.{sh|ps1} | |
| /// ``` | |
| final class _FlutterRootUnderTest { | |
| /// Creates a root-under test using [path] as the root directory. | |
| /// | |
| /// It is assumed the files already exist or will be created if needed. | |
| factory _FlutterRootUnderTest.fromPath( | |
| String path, { | |
| FileSystem fileSystem = const LocalFileSystem(), | |
| Platform platform = const LocalPlatform(), | |
| bool forcePowershell = false, | |
| }) { | |
| final Directory root = fileSystem.directory(path); | |
| return _FlutterRootUnderTest._( | |
| root, | |
| depsFile: root.childFile('DEPS'), | |
| engineDirectory: root.childDirectory('engine'), | |
| binInternalLastEngineCommit: root.childFile( | |
| fileSystem.path.join( | |
| 'bin', | |
| 'internal', | |
| 'last_engine_commit.${platform.isWindows || forcePowershell ? 'ps1' : 'sh'}', | |
| ), | |
| ), | |
| ); | |
| } | |
| factory _FlutterRootUnderTest.findWithin({ | |
| String? path, | |
| FileSystem fileSystem = const LocalFileSystem(), | |
| bool forcePowershell = false, | |
| }) { | |
| path ??= fileSystem.currentDirectory.path; | |
| Directory current = fileSystem.directory(path); | |
| while (!current.childFile('DEPS').existsSync()) { | |
| if (current.path == current.parent.path) { | |
| throw ArgumentError.value(path, 'path', 'Could not resolve flutter root'); | |
| } | |
| current = current.parent; | |
| } | |
| return _FlutterRootUnderTest.fromPath(current.path, forcePowershell: forcePowershell); | |
| } | |
| const _FlutterRootUnderTest._( | |
| this.root, { | |
| required this.binInternalLastEngineCommit, | |
| required this.depsFile, | |
| required this.engineDirectory, | |
| }); | |
| final Directory root; | |
| /// `DEPS`. | |
| final File depsFile; | |
| /// The `engine/` directory. | |
| final Directory engineDirectory; | |
| /// `bin/internal/last_engine_commit.{sh|ps1}`. | |
| final File binInternalLastEngineCommit; | |
| } |