mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
The `content_aware_hash.sh` script determines which version of the
engine to use. For local development, it uses the merge-base with the
remote tracking branch to avoid unnecessary rebuilds.
However, when using `jj`, the underlying git repository is in a detached
HEAD state. The script was incorrectly interpreting this as a CI
environment and was not calculating the hash based on the merge-base,
leading to incorrect engine versions and failed Dart SDK downloads.
This change modifies the script to differentiate between a local
detached HEAD state (like with `jj`) and a CI environment by checking
for the `LUCI_CI` environment variable. This ensures the correct engine
hash is generated for both local `jj` users and CI builds.
Here is an example of failing to download the Dart SDK before:
```
Downloading Darwin arm64 Dart SDK from Flutter engine f6ea244d7b75547c2c1a4613299b24dcebe3ce5c...
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 258 100 258 0 0 858 0 --:--:-- --:--:-- --:--:-- 857
[/Users/het/Projects/flutter/bin/cache/dart-sdk-darwin-arm64.zip]
End-of-central-directory signature not found. Either this file is not
a zipfile, or it constitutes one disk of a multi-part archive. In the
latter case the central directory and zipfile comment will be found on
the last disk(s) of this archive.
unzip: cannot find zipfile directory in one of /Users/het/Projects/flutter/bin/cache/dart-sdk-darwin-arm64.zip or
/Users/het/Projects/flutter/bin/cache/dart-sdk-darwin-arm64.zip.zip, and cannot find /Users/het/Projects/flutter/bin/cache/dart-sdk-darwin-arm64.zip.ZIP, period.
It appears that the downloaded file is corrupt; please try again.
If this problem persists, please report the problem at:
https://github.com/flutter/flutter/issues/new?template=01_activation.yml
```
## Pre-launch Checklist
- [x] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [x] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [x] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [x] I signed the [CLA].
- [ ] I listed at least one issue that this PR fixes in the description
above.
- [x] I updated/added relevant documentation (doc comments with `///`).
- [x] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [x] I followed the [breaking change policy] and added [Data Driven
Fixes] where supported.
- [x] All existing and new tests are passing.
If you need help, consider asking for advice on the #hackers-new channel
on [Discord].
**Note**: The Flutter team is currently trialing the use of [Gemini Code
Assist for
GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code).
Comments from the `gemini-code-assist` bot should not be taken as
authoritative feedback from the Flutter team. If you find its comments
useful you can update your code accordingly, but if you are unsure or
disagree with the feedback, please feel free to wait for a Flutter team
member's review for guidance on which automated comments should be
addressed.
<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview
[Tree Hygiene]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md
[test-exempt]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests
[Flutter Style Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md
[Features we expect every widget to implement]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement
[CLA]: https://cla.developers.google.com/
[flutter/tests]: https://github.com/flutter/tests
[breaking change policy]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes
[Discord]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md
[Data Driven Fixes]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
545 lines
19 KiB
Dart
545 lines
19 KiB
Dart
// 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 (content_aware_hash.ps1) file, but running
|
|
// a macOS or Linux machine? You can install powershell and then opt-in to
|
|
// running `pwsh bin/internal/content_aware_hash.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 = io.Platform.environment['FORCE_POWERSHELL'] == 'true';
|
|
|
|
print('env: ${io.Platform.environment}');
|
|
|
|
const FileSystem localFs = LocalFileSystem();
|
|
final _FlutterRootUnderTest flutterRoot = _FlutterRootUnderTest.findWithin();
|
|
|
|
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, {String? workingPath}) {
|
|
print('Running "$executable ${args.join(" ")}"${workingPath != null ? ' $workingPath' : ''}');
|
|
final io.ProcessResult result = io.Process.runSync(
|
|
executable,
|
|
args,
|
|
environment: environment,
|
|
workingDirectory: workingPath ?? 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}) on POSIX for local debugging and testing');
|
|
}
|
|
});
|
|
|
|
setUp(() async {
|
|
tmpDir = localFs.systemTempDirectory.createTempSync('content_aware_hash.');
|
|
testRoot = _FlutterRootUnderTest.fromPath(tmpDir.childDirectory('flutter').path);
|
|
|
|
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 List<String> powerShellVariables = <String>['SystemRoot', 'PATH', 'PATHEXT'];
|
|
for (final String key in powerShellVariables) {
|
|
final String? value = io.Platform.environment[key];
|
|
if (value != null) {
|
|
environment[key] = value;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Make a slim copy of the flutterRoot.
|
|
flutterRoot.copyTo(testRoot);
|
|
|
|
// Generate blank files for what otherwise would exist in the engine.
|
|
testRoot
|
|
..engineReadMe.createSync(recursive: true)
|
|
..flutterReadMe.createSync(recursive: true)
|
|
..deps.createSync(recursive: true);
|
|
});
|
|
|
|
tearDown(() {
|
|
// Git adds a lot of files, we don't want to test for them.
|
|
final Directory gitDir = testRoot.root.childDirectory('.git');
|
|
if (gitDir.existsSync()) {
|
|
gitDir.deleteSync(recursive: true);
|
|
}
|
|
|
|
// Now do cleanup so even if the next step fails, we still deleted tmp.
|
|
tmpDir.deleteSync(recursive: true);
|
|
});
|
|
|
|
/// Runs `bin/internal/content_aware_hash.{sh|ps1}` and returns the process result.
|
|
///
|
|
/// If the exit code is 0, it is considered a success, and files should exist as a side-effect.
|
|
/// - On Windows, `powershell` is used (to run `update_engine_version.ps1`);
|
|
/// - Otherwise, `update_engine_version.sh` is used.
|
|
io.ProcessResult runContentAwareHash() {
|
|
final String executable;
|
|
final List<String> args;
|
|
if (const LocalPlatform().isWindows) {
|
|
executable = 'powershell';
|
|
// "ExecutionPolicy Bypass" is required to execute scripts from temp
|
|
// folders on Windows 11 machines.
|
|
args = <String>['-ExecutionPolicy', 'Bypass', '-File', testRoot.contentAwareHashPs1.path];
|
|
} else if (usePowershellOnPosix) {
|
|
executable = 'pwsh';
|
|
args = <String>[testRoot.contentAwareHashPs1.path];
|
|
} else {
|
|
executable = testRoot.contentAwareHashSh.path;
|
|
args = <String>[];
|
|
}
|
|
return run(executable, args);
|
|
}
|
|
|
|
/// Sets up and fetches a [remote] (such as `upstream` or `origin`) for [testRoot.root].
|
|
///
|
|
/// The remote points at itself (`testRoot.root.path`) for ease of testing.
|
|
void setupRemote({required String remote, String? rootPath}) {
|
|
run('git', <String>[
|
|
'remote',
|
|
'add',
|
|
remote,
|
|
rootPath ?? testRoot.root.path,
|
|
], workingPath: rootPath);
|
|
run('git', <String>['fetch', remote], workingPath: rootPath);
|
|
}
|
|
|
|
/// Initializes a blank git repo in [testRoot.root].
|
|
void initGitRepoWithBlankInitialCommit({
|
|
String? workingPath,
|
|
String branch = 'master',
|
|
String remote = 'upstream',
|
|
}) {
|
|
run('git', <String>['init', '--initial-branch', branch], workingPath: workingPath);
|
|
// autocrlf is very important for tests to work on windows.
|
|
run('git', 'config --local core.autocrlf true'.split(' '), workingPath: workingPath);
|
|
run('git', <String>[
|
|
'config',
|
|
'--local',
|
|
'user.email',
|
|
'test@example.com',
|
|
], workingPath: workingPath);
|
|
run('git', <String>['config', '--local', 'user.name', 'Test User'], workingPath: workingPath);
|
|
run('git', <String>['add', '.'], workingPath: workingPath);
|
|
run('git', <String>[
|
|
'commit',
|
|
'--allow-empty',
|
|
'-m',
|
|
'Initial commit',
|
|
], workingPath: workingPath);
|
|
|
|
setupRemote(remote: remote, rootPath: workingPath);
|
|
}
|
|
|
|
String gitShaFor(String ref, {String? workingPath}) {
|
|
return (run('git', <String>['rev-parse', ref], workingPath: workingPath).stdout as String)
|
|
.trim();
|
|
}
|
|
|
|
void writeFileAndCommit(File file, String contents) {
|
|
file.writeAsStringSync(contents);
|
|
run('git', <String>['add', '--all']);
|
|
run('git', <String>['commit', '--all', '-m', 'changed ${file.basename} to $contents']);
|
|
}
|
|
|
|
void gitSwitchBranch(String branch, {bool create = true}) {
|
|
run('git', <String>['switch', if (create) '-c', branch]);
|
|
}
|
|
|
|
// Downstream flutter user tests: (origin|upstream)/(main|master), stable, and
|
|
// beta should work.
|
|
|
|
test('generates a hash or upstream/master', () async {
|
|
initGitRepoWithBlankInitialCommit();
|
|
expect(runContentAwareHash(), processStdout('3bbeb6a394378478683ece4f8e8663c42f8dc814'));
|
|
});
|
|
|
|
test('generates a hash for origin/master', () {
|
|
initGitRepoWithBlankInitialCommit(remote: 'origin');
|
|
expect(runContentAwareHash(), processStdout('3bbeb6a394378478683ece4f8e8663c42f8dc814'));
|
|
});
|
|
|
|
test('generates a hash for origin/main', () {
|
|
initGitRepoWithBlankInitialCommit(remote: 'origin', branch: 'main');
|
|
expect(runContentAwareHash(), processStdout('3bbeb6a394378478683ece4f8e8663c42f8dc814'));
|
|
});
|
|
|
|
test('generates a hash for upstream/main', () {
|
|
initGitRepoWithBlankInitialCommit(branch: 'main');
|
|
expect(runContentAwareHash(), processStdout('3bbeb6a394378478683ece4f8e8663c42f8dc814'));
|
|
});
|
|
|
|
test('generates a hash for CI/CD from HEAD', () {
|
|
// This test validates the workflow with LUCI recipes in which the git sha
|
|
// is checked out, not the branch.
|
|
initGitRepoWithBlankInitialCommit(branch: 'main');
|
|
writeFileAndCommit(testRoot.deps, 'deps changed');
|
|
|
|
final String headSha = gitShaFor('HEAD');
|
|
run('git', <String>['checkout', '-f', headSha]);
|
|
run('git', <String>['--no-pager', 'log', '--decorate=short', '--pretty=oneline']);
|
|
expect(
|
|
(run('git', <String>['rev-parse', '--abbrev-ref', 'HEAD']).stdout as String).trim(),
|
|
equals('HEAD'),
|
|
);
|
|
|
|
// Simulate being in a LUCI environment.
|
|
environment['LUCI_CI'] = 'true';
|
|
expect(runContentAwareHash(), processStdout('f049fdcd4300c8c0d5041b5e35b3d11c2d289bdf'));
|
|
});
|
|
|
|
test('generates a hash based on merge-base in local detached HEAD', () {
|
|
// This test validates the workflow with a detached HEAD, which is common
|
|
// when working with jj.
|
|
initGitRepoWithBlankInitialCommit(branch: 'main');
|
|
writeFileAndCommit(testRoot.deps, 'deps changed');
|
|
|
|
final String headSha = gitShaFor('HEAD');
|
|
run('git', <String>['checkout', '-f', headSha]);
|
|
run('git', <String>['--no-pager', 'log', '--decorate=short', '--pretty=oneline']);
|
|
expect(
|
|
(run('git', <String>['rev-parse', '--abbrev-ref', 'HEAD']).stdout as String).trim(),
|
|
equals('HEAD'),
|
|
);
|
|
|
|
expect(runContentAwareHash(), processStdout('3bbeb6a394378478683ece4f8e8663c42f8dc814'));
|
|
});
|
|
|
|
group('stable branches calculate hash locally', () {
|
|
test('with no changes', () {
|
|
initGitRepoWithBlankInitialCommit(branch: 'main');
|
|
gitSwitchBranch('stable');
|
|
expect(runContentAwareHash(), processStdout('3bbeb6a394378478683ece4f8e8663c42f8dc814'));
|
|
});
|
|
|
|
test('with engine changes', () {
|
|
initGitRepoWithBlankInitialCommit(branch: 'main');
|
|
gitSwitchBranch('stable');
|
|
writeFileAndCommit(testRoot.deps, 'deps changed');
|
|
|
|
expect(runContentAwareHash(), processStdout('f049fdcd4300c8c0d5041b5e35b3d11c2d289bdf'));
|
|
});
|
|
});
|
|
|
|
group('beta branches calculate hash locally', () {
|
|
test('with no changes', () {
|
|
initGitRepoWithBlankInitialCommit(branch: 'main');
|
|
gitSwitchBranch('beta');
|
|
expect(runContentAwareHash(), processStdout('3bbeb6a394378478683ece4f8e8663c42f8dc814'));
|
|
});
|
|
|
|
test('with engine changes', () {
|
|
initGitRepoWithBlankInitialCommit(branch: 'main');
|
|
gitSwitchBranch('beta');
|
|
writeFileAndCommit(testRoot.deps, 'deps changed');
|
|
|
|
expect(runContentAwareHash(), processStdout('f049fdcd4300c8c0d5041b5e35b3d11c2d289bdf'));
|
|
});
|
|
});
|
|
|
|
group('release branches calculate hash locally', () {
|
|
test('with no changes', () {
|
|
initGitRepoWithBlankInitialCommit(branch: 'main');
|
|
gitSwitchBranch('flutter-4.35-candidate.2');
|
|
expect(runContentAwareHash(), processStdout('3bbeb6a394378478683ece4f8e8663c42f8dc814'));
|
|
});
|
|
|
|
test('with engine changes', () {
|
|
initGitRepoWithBlankInitialCommit(branch: 'main');
|
|
gitSwitchBranch('flutter-4.35-candidate.2');
|
|
writeFileAndCommit(testRoot.deps, 'deps changed');
|
|
|
|
expect(runContentAwareHash(), processStdout('f049fdcd4300c8c0d5041b5e35b3d11c2d289bdf'));
|
|
});
|
|
});
|
|
|
|
test('github special merge group branches calculate hash locally', () {
|
|
initGitRepoWithBlankInitialCommit(
|
|
remote: 'origin',
|
|
branch: 'gh-readonly-queue/master/pr-1234-abcd',
|
|
);
|
|
writeFileAndCommit(testRoot.deps, 'deps changed');
|
|
|
|
expect(runContentAwareHash(), processStdout('f049fdcd4300c8c0d5041b5e35b3d11c2d289bdf'));
|
|
});
|
|
|
|
test('generates a hash for shallow clones', () {
|
|
initGitRepoWithBlankInitialCommit(remote: 'origin', branch: 'blip');
|
|
final String headSha = gitShaFor('HEAD');
|
|
testRoot.root
|
|
.childFile(localFs.path.joinAll('.git/shallow'.split('/')))
|
|
.writeAsStringSync(headSha);
|
|
writeFileAndCommit(testRoot.deps, 'deps changed');
|
|
expect(runContentAwareHash(), processStdout('f049fdcd4300c8c0d5041b5e35b3d11c2d289bdf'));
|
|
});
|
|
|
|
group('ignores local engine for', () {
|
|
test('upstream', () {
|
|
initGitRepoWithBlankInitialCommit();
|
|
gitSwitchBranch('engineTest');
|
|
testRoot.deps.writeAsStringSync('deps changed');
|
|
expect(
|
|
runContentAwareHash(),
|
|
processStdout('3bbeb6a394378478683ece4f8e8663c42f8dc814'),
|
|
reason: 'content hash from master for non-committed file',
|
|
);
|
|
|
|
writeFileAndCommit(testRoot.deps, 'deps changed');
|
|
expect(
|
|
runContentAwareHash(),
|
|
processStdout('3bbeb6a394378478683ece4f8e8663c42f8dc814'),
|
|
reason: 'content hash from master for committed file',
|
|
);
|
|
});
|
|
|
|
test('origin', () {
|
|
initGitRepoWithBlankInitialCommit(remote: 'origin');
|
|
gitSwitchBranch('engineTest');
|
|
testRoot.deps.writeAsStringSync('deps changed');
|
|
expect(
|
|
runContentAwareHash(),
|
|
processStdout('3bbeb6a394378478683ece4f8e8663c42f8dc814'),
|
|
reason: 'content hash from master for non-committed file',
|
|
);
|
|
|
|
writeFileAndCommit(testRoot.deps, 'deps changed');
|
|
expect(
|
|
runContentAwareHash(),
|
|
processStdout('3bbeb6a394378478683ece4f8e8663c42f8dc814'),
|
|
reason: 'content hash from master for committed file',
|
|
);
|
|
});
|
|
});
|
|
|
|
group('generates a different hash when', () {
|
|
setUp(() {
|
|
initGitRepoWithBlankInitialCommit();
|
|
});
|
|
|
|
test('DEPS is changed', () async {
|
|
writeFileAndCommit(testRoot.deps, 'deps changed');
|
|
expect(runContentAwareHash(), processStdout('f049fdcd4300c8c0d5041b5e35b3d11c2d289bdf'));
|
|
});
|
|
|
|
test('an engine file changes', () async {
|
|
writeFileAndCommit(testRoot.engineReadMe, 'engine file changed');
|
|
expect(runContentAwareHash(), processStdout('49e58f425cb039e745614d7ea10c369387c43681'));
|
|
});
|
|
|
|
test('a new engine file is added', () async {
|
|
final List<String> gibberish = ('_abcdefghijklmnopqrstuvqxyz0123456789' * 20).split('')
|
|
..shuffle();
|
|
final String newFileName = gibberish.take(20).join();
|
|
|
|
writeFileAndCommit(
|
|
testRoot.engineReadMe.parent.childFile(newFileName),
|
|
'$newFileName file added',
|
|
);
|
|
|
|
expect(
|
|
runContentAwareHash(),
|
|
isNot(processStdout('e9d1f7dc1718dac8e8189791a8073e38abdae1cf')),
|
|
);
|
|
});
|
|
|
|
test('bin/internal/release-candidate-branch.version is present', () {
|
|
writeFileAndCommit(
|
|
testRoot.contentAwareHashPs1.parent.childFile('release-candidate-branch.version'),
|
|
'sup',
|
|
);
|
|
expect(runContentAwareHash(), processStdout('3b81cd2164f26a8db3271d46c7022c159193417d'));
|
|
});
|
|
});
|
|
|
|
test('does not hash non-engine files', () async {
|
|
initGitRepoWithBlankInitialCommit();
|
|
testRoot.flutterReadMe.writeAsStringSync('codefu was here');
|
|
expect(runContentAwareHash(), processStdout('3bbeb6a394378478683ece4f8e8663c42f8dc814'));
|
|
});
|
|
|
|
test('missing merge-base defaults to HEAD', () {
|
|
initGitRepoWithBlankInitialCommit();
|
|
|
|
run('git', <String>['branch', '-m', 'no-merge-base'], workingPath: testRoot.root.path);
|
|
run('git', <String>['remote', 'remove', 'upstream'], workingPath: testRoot.root.path);
|
|
|
|
writeFileAndCommit(testRoot.deps, 'deps changed');
|
|
expect(
|
|
runContentAwareHash(),
|
|
processStdout('f049fdcd4300c8c0d5041b5e35b3d11c2d289bdf'),
|
|
reason: 'content hash from HEAD when no merge-base',
|
|
);
|
|
});
|
|
}
|
|
|
|
/// 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 a minimal set of files.
|
|
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(),
|
|
}) {
|
|
final Directory root = fileSystem.directory(path);
|
|
return _FlutterRootUnderTest._(
|
|
root,
|
|
contentAwareHashPs1: root.childFile(
|
|
fileSystem.path.joinAll('bin/internal/content_aware_hash.ps1'.split('/')),
|
|
),
|
|
contentAwareHashSh: root.childFile(
|
|
fileSystem.path.joinAll('bin/internal/content_aware_hash.sh'.split('/')),
|
|
),
|
|
engineReadMe: root.childFile(fileSystem.path.joinAll('engine/README.md'.split('/'))),
|
|
deps: root.childFile(fileSystem.path.join('DEPS')),
|
|
flutterReadMe: root.childFile(
|
|
fileSystem.path.joinAll('packages/flutter/README.md'.split('/')),
|
|
),
|
|
);
|
|
}
|
|
|
|
factory _FlutterRootUnderTest.findWithin({
|
|
String? path,
|
|
FileSystem fileSystem = const LocalFileSystem(),
|
|
}) {
|
|
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);
|
|
}
|
|
|
|
const _FlutterRootUnderTest._(
|
|
this.root, {
|
|
required this.deps,
|
|
required this.contentAwareHashPs1,
|
|
required this.contentAwareHashSh,
|
|
required this.engineReadMe,
|
|
required this.flutterReadMe,
|
|
});
|
|
|
|
final Directory root;
|
|
|
|
final File deps;
|
|
final File contentAwareHashPs1;
|
|
final File contentAwareHashSh;
|
|
final File engineReadMe;
|
|
final File flutterReadMe;
|
|
|
|
/// Copies files under test to the [testRoot].
|
|
void copyTo(_FlutterRootUnderTest testRoot) {
|
|
contentAwareHashPs1.copySyncRecursive(testRoot.contentAwareHashPs1.path);
|
|
contentAwareHashSh.copySyncRecursive(testRoot.contentAwareHashSh.path);
|
|
}
|
|
}
|
|
|
|
extension on File {
|
|
void copySyncRecursive(String newPath) {
|
|
fileSystem.directory(fileSystem.path.dirname(newPath)).createSync(recursive: true);
|
|
copySync(newPath);
|
|
}
|
|
}
|
|
|
|
/// Returns a matcher, that, given [stdout]:
|
|
///
|
|
/// 1. Process exists with code 0
|
|
/// 2. Stdout is a String
|
|
/// 3. Stdout contents, after applying [collapseWhitespace], is the same as
|
|
/// [stdout], after applying [collapseWhitespace].
|
|
Matcher processStdout(String stdout) {
|
|
return _ProcessSucceedsAndOutputs(stdout);
|
|
}
|
|
|
|
final class _ProcessSucceedsAndOutputs extends Matcher {
|
|
_ProcessSucceedsAndOutputs(String stdout) : _expected = collapseWhitespace(stdout);
|
|
|
|
final String _expected;
|
|
|
|
@override
|
|
bool matches(Object? item, _) {
|
|
if (item is! io.ProcessResult || item.exitCode != 0 || item.stdout is! String) {
|
|
return false;
|
|
}
|
|
final String actual = item.stdout as String;
|
|
return collapseWhitespace(actual) == collapseWhitespace(_expected);
|
|
}
|
|
|
|
@override
|
|
Description describe(Description description) {
|
|
return description.add(
|
|
'The process exists normally and stdout (ignoring whitespace): $_expected',
|
|
);
|
|
}
|
|
|
|
@override
|
|
Description describeMismatch(Object? item, Description mismatch, _, _) {
|
|
if (item is! io.ProcessResult) {
|
|
return mismatch.add('is not a process result (${item.runtimeType})');
|
|
}
|
|
if (item.exitCode != 0) {
|
|
return mismatch.add('exit code is not zero (${item.exitCode})');
|
|
}
|
|
if (item.stdout is! String) {
|
|
return mismatch.add('stdout is not String (${item.stdout.runtimeType})');
|
|
}
|
|
return mismatch
|
|
.add('is ')
|
|
.addDescriptionOf(collapseWhitespace(item.stdout as String))
|
|
.add(' with whitespace compressed');
|
|
}
|
|
}
|