mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00

Rolls forward https://github.com/flutter/flutter/pull/166717. Does not copy the `README` or `DEPS` files, and instead uses synthetic scratch files. These files can change, so we can't possibly know how to hash them consistently.
319 lines
11 KiB
Dart
319 lines
11 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() {
|
|
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;
|
|
}
|
|
|
|
setUp(() async {
|
|
tmpDir = localFs.systemTempDirectory.createTempSync('content_aware_hash.');
|
|
testRoot = _FlutterRootUnderTest.fromPath(tmpDir.childDirectory('flutter').path);
|
|
|
|
environment = <String, String>{};
|
|
|
|
if (const LocalPlatform().isWindows) {
|
|
// 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';
|
|
args = <String>[testRoot.contentAwareHashPs1.path];
|
|
} else {
|
|
executable = testRoot.contentAwareHashSh.path;
|
|
args = <String>[];
|
|
}
|
|
return run(executable, args);
|
|
}
|
|
|
|
/// Initializes a blank git repo in [testRoot.root].
|
|
void initGitRepoWithBlankInitialCommit({String? workingPath}) {
|
|
run('git', <String>['init', '--initial-branch', 'master'], 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);
|
|
}
|
|
|
|
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']);
|
|
}
|
|
|
|
test('generates a hash', () async {
|
|
initGitRepoWithBlankInitialCommit();
|
|
expect(runContentAwareHash(), processStdout('3bbeb6a394378478683ece4f8e8663c42f8dc814'));
|
|
});
|
|
|
|
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'));
|
|
});
|
|
}
|
|
|
|
/// 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');
|
|
}
|
|
}
|