Skip to content
This repository was archived by the owner on Feb 22, 2023. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion script/tool/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
## NEXT
## 0.8.2

- Adds a new `custom-test` command.
- Switches from deprecated `flutter packages` alias to `flutter pub`.

## 0.8.1
Expand Down
77 changes: 77 additions & 0 deletions script/tool/lib/src/custom_test_command.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
// Copyright 2013 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.

import 'package:file/file.dart';
import 'package:platform/platform.dart';

import 'common/package_looping_command.dart';
import 'common/process_runner.dart';
import 'common/repository_package.dart';

const String _scriptName = 'run_tests.dart';
const String _legacyScriptName = 'run_tests.sh';

/// A command to run custom, package-local tests on packages.
///
/// This is an escape hatch for adding tests that this tooling doesn't support.
/// It should be used sparingly; prefer instead to add functionality to this
/// tooling to eliminate the need for bespoke tests.
class CustomTestCommand extends PackageLoopingCommand {
/// Creates a custom test command instance.
CustomTestCommand(
Directory packagesDir, {
ProcessRunner processRunner = const ProcessRunner(),
Platform platform = const LocalPlatform(),
}) : super(packagesDir, processRunner: processRunner, platform: platform);

@override
final String name = 'custom-test';

@override
final String description = 'Runs package-specific custom tests defined in '
'a package\'s tool/$_scriptName file.\n\n'
'This command requires "dart" to be in your path.';

@override
Future<PackageResult> runForPackage(RepositoryPackage package) async {
final File script =
package.directory.childDirectory('tool').childFile(_scriptName);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pigeon has this in bin; I'll move it when I switch flutter/packages to this new command. Dart package layout docs say that bin is for package-client-facing commands, and scripts intended for package developers (the call out test scripts as an explicit example) belong in tool.

final File legacyScript = package.directory.childFile(_legacyScriptName);
String? customSkipReason;
bool ranTests = false;

// Run the custom Dart script if presest.
if (script.existsSync()) {
final int exitCode = await processRunner.runAndStream(
'dart', <String>['run', 'tool/$_scriptName'],
workingDir: package.directory);
if (exitCode != 0) {
return PackageResult.fail();
}
ranTests = true;
}

// Run the legacy script if present.
if (legacyScript.existsSync()) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will make it so we run the dart script and the sh script. That will cause certain tests to get executed twice since the shell script delegates to the dart script. We should execute one or the other.

We could juggle things around in Pigeon so that the correct way is to execute the dart script and it will delegate to a shell script test that haven't been migrated (if it isn't windows). We could even make run_tests.sh just execute the dart script and there is another bash script the dart script delegates to for unmigrated tests.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will make it so we run the dart script and the sh script.

That's already how it works. It had to work that way in order to support pigeon's tests, which were partially implemented in one and partially implemented in the other.

That will cause certain tests to get executed twice since the shell script delegates to the dart script.

This code was supposed to prevent that. If Pigeon's test structure changed after that in a way that causes it to double-run tests, that's a Pigeon test bug.

We could juggle things around in Pigeon so that the correct way is to execute the dart script and it will delegate to a shell script test that haven't been migrated (if it isn't windows).

I don't have any opinion on how Pigeon manages the Dart vs shell split in the interim period until all tests are migrated, but it's orthogonal to this PR since the behavior isn't changing.

Copy link
Member

@gaaclarke gaaclarke Mar 17, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's already how it works.

Fair enough.

that's a Pigeon test bug.

I'm just trying to think what the fix is though given this logic and making sure we have a path forward. The easiest thing for pigeon would be to rename the script 'run_tests.sh' to 'shell_tests.sh' and make run_tests.dart execute shell_tests.sh. If pigeon isn't using run_tests.sh then probably no one is so we can get rid of this.

Copy link
Contributor Author

@stuartmorgan-g stuartmorgan-g Mar 18, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm just trying to think what the fix is though given this logic and making sure we have a path forward.

I'm not clear on what the problem actually is. What is the flow where the code I linked to fails to avoid duplicate tests?

If pigeon isn't using run_tests.sh then probably no one is so we can get rid of this.

Four packages currently use run_test.sh. See the linked issue for details.

if (platform.isWindows) {
customSkipReason = '$_legacyScriptName is not supported on Windows. '
'Please migrate to $_scriptName.';
} else {
final int exitCode = await processRunner.runAndStream(
legacyScript.path, <String>[],
workingDir: package.directory);
if (exitCode != 0) {
return PackageResult.fail();
}
ranTests = true;
}
}

if (!ranTests) {
return PackageResult.skip(customSkipReason ?? 'No custom tests');
}

return PackageResult.success();
}
}
2 changes: 2 additions & 0 deletions script/tool/lib/src/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import 'analyze_command.dart';
import 'build_examples_command.dart';
import 'common/core.dart';
import 'create_all_plugins_app_command.dart';
import 'custom_test_command.dart';
import 'drive_examples_command.dart';
import 'federation_safety_check_command.dart';
import 'firebase_test_lab_command.dart';
Expand Down Expand Up @@ -50,6 +51,7 @@ void main(List<String> args) {
..addCommand(AnalyzeCommand(packagesDir))
..addCommand(BuildExamplesCommand(packagesDir))
..addCommand(CreateAllPluginsAppCommand(packagesDir))
..addCommand(CustomTestCommand(packagesDir))
..addCommand(DriveExamplesCommand(packagesDir))
..addCommand(FederationSafetyCheckCommand(packagesDir))
..addCommand(FirebaseTestLabCommand(packagesDir))
Expand Down
2 changes: 1 addition & 1 deletion script/tool/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name: flutter_plugin_tools
description: Productivity utils for flutter/plugins and flutter/packages
repository: https://github.com/flutter/plugins/tree/main/script/tool
version: 0.8.1
version: 0.8.2

dependencies:
args: ^2.1.0
Expand Down
281 changes: 281 additions & 0 deletions script/tool/test/custom_test_command_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,281 @@
// Copyright 2013 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.

import 'dart:io' as io;

import 'package:args/command_runner.dart';
import 'package:file/file.dart';
import 'package:file/memory.dart';
import 'package:flutter_plugin_tools/src/common/core.dart';
import 'package:flutter_plugin_tools/src/custom_test_command.dart';
import 'package:test/test.dart';

import 'mocks.dart';
import 'util.dart';

void main() {
late FileSystem fileSystem;
late MockPlatform mockPlatform;
late Directory packagesDir;
late RecordingProcessRunner processRunner;
late CommandRunner<void> runner;

group('posix', () {
setUp(() {
fileSystem = MemoryFileSystem();
mockPlatform = MockPlatform();
packagesDir = createPackagesDirectory(fileSystem: fileSystem);
processRunner = RecordingProcessRunner();
final CustomTestCommand analyzeCommand = CustomTestCommand(
packagesDir,
processRunner: processRunner,
platform: mockPlatform,
);

runner = CommandRunner<void>(
'custom_test_command', 'Test for custom_test_command');
runner.addCommand(analyzeCommand);
});

test('runs both new and legacy when both are present', () async {
final Directory package =
createFakePlugin('a_package', packagesDir, extraFiles: <String>[
'tool/run_tests.dart',
'run_tests.sh',
]);

final List<String> output =
await runCapturingPrint(runner, <String>['custom-test']);

expect(
processRunner.recordedCalls,
containsAll(<ProcessCall>[
ProcessCall(package.childFile('run_tests.sh').path,
const <String>[], package.path),
ProcessCall('dart', const <String>['run', 'tool/run_tests.dart'],
package.path),
]));

expect(
output,
containsAllInOrder(<Matcher>[
contains('Ran for 1 package(s)'),
]));
});

test('runs when only new is present', () async {
final Directory package = createFakePlugin('a_package', packagesDir,
extraFiles: <String>['tool/run_tests.dart']);

final List<String> output =
await runCapturingPrint(runner, <String>['custom-test']);

expect(
processRunner.recordedCalls,
containsAll(<ProcessCall>[
ProcessCall('dart', const <String>['run', 'tool/run_tests.dart'],
package.path),
]));

expect(
output,
containsAllInOrder(<Matcher>[
contains('Ran for 1 package(s)'),
]));
});

test('runs when only legacy is present', () async {
final Directory package = createFakePlugin('a_package', packagesDir,
extraFiles: <String>['run_tests.sh']);

final List<String> output =
await runCapturingPrint(runner, <String>['custom-test']);

expect(
processRunner.recordedCalls,
containsAll(<ProcessCall>[
ProcessCall(package.childFile('run_tests.sh').path,
const <String>[], package.path),
]));

expect(
output,
containsAllInOrder(<Matcher>[
contains('Ran for 1 package(s)'),
]));
});

test('skips when neither is present', () async {
createFakePlugin('a_package', packagesDir);

final List<String> output =
await runCapturingPrint(runner, <String>['custom-test']);

expect(processRunner.recordedCalls, isEmpty);

expect(
output,
containsAllInOrder(<Matcher>[
contains('Skipped 1 package(s)'),
]));
});

test('fails if new fails', () async {
createFakePlugin('a_package', packagesDir, extraFiles: <String>[
'tool/run_tests.dart',
'run_tests.sh',
]);

processRunner.mockProcessesForExecutable['dart'] = <io.Process>[
MockProcess(exitCode: 1),
];

Error? commandError;
final List<String> output = await runCapturingPrint(
runner, <String>['custom-test'], errorHandler: (Error e) {
commandError = e;
});

expect(commandError, isA<ToolExit>());
expect(
output,
containsAllInOrder(<Matcher>[
contains('The following packages had errors:'),
contains('a_package')
]));
});

test('fails if legacy fails', () async {
final Directory package =
createFakePlugin('a_package', packagesDir, extraFiles: <String>[
'tool/run_tests.dart',
'run_tests.sh',
]);

processRunner.mockProcessesForExecutable[
package.childFile('run_tests.sh').path] = <io.Process>[
MockProcess(exitCode: 1),
];

Error? commandError;
final List<String> output = await runCapturingPrint(
runner, <String>['custom-test'], errorHandler: (Error e) {
commandError = e;
});

expect(commandError, isA<ToolExit>());
expect(
output,
containsAllInOrder(<Matcher>[
contains('The following packages had errors:'),
contains('a_package')
]));
});
});

group('Windows', () {
setUp(() {
fileSystem = MemoryFileSystem(style: FileSystemStyle.windows);
mockPlatform = MockPlatform(isWindows: true);
packagesDir = createPackagesDirectory(fileSystem: fileSystem);
processRunner = RecordingProcessRunner();
final CustomTestCommand analyzeCommand = CustomTestCommand(
packagesDir,
processRunner: processRunner,
platform: mockPlatform,
);

runner = CommandRunner<void>(
'custom_test_command', 'Test for custom_test_command');
runner.addCommand(analyzeCommand);
});

test('runs new and skips old when both are present', () async {
final Directory package =
createFakePlugin('a_package', packagesDir, extraFiles: <String>[
'tool/run_tests.dart',
'run_tests.sh',
]);

final List<String> output =
await runCapturingPrint(runner, <String>['custom-test']);

expect(
processRunner.recordedCalls,
containsAll(<ProcessCall>[
ProcessCall('dart', const <String>['run', 'tool/run_tests.dart'],
package.path),
]));

expect(
output,
containsAllInOrder(<Matcher>[
contains('Ran for 1 package(s)'),
]));
});

test('runs when only new is present', () async {
final Directory package = createFakePlugin('a_package', packagesDir,
extraFiles: <String>['tool/run_tests.dart']);

final List<String> output =
await runCapturingPrint(runner, <String>['custom-test']);

expect(
processRunner.recordedCalls,
containsAll(<ProcessCall>[
ProcessCall('dart', const <String>['run', 'tool/run_tests.dart'],
package.path),
]));

expect(
output,
containsAllInOrder(<Matcher>[
contains('Ran for 1 package(s)'),
]));
});

test('skips package when only legacy is present', () async {
createFakePlugin('a_package', packagesDir,
extraFiles: <String>['run_tests.sh']);

final List<String> output =
await runCapturingPrint(runner, <String>['custom-test']);

expect(processRunner.recordedCalls, isEmpty);

expect(
output,
containsAllInOrder(<Matcher>[
contains('run_tests.sh is not supported on Windows'),
contains('Skipped 1 package(s)'),
]));
});

test('fails if new fails', () async {
createFakePlugin('a_package', packagesDir, extraFiles: <String>[
'tool/run_tests.dart',
'run_tests.sh',
]);

processRunner.mockProcessesForExecutable['dart'] = <io.Process>[
MockProcess(exitCode: 1),
];

Error? commandError;
final List<String> output = await runCapturingPrint(
runner, <String>['custom-test'], errorHandler: (Error e) {
commandError = e;
});

expect(commandError, isA<ToolExit>());
expect(
output,
containsAllInOrder(<Matcher>[
contains('The following packages had errors:'),
contains('a_package')
]));
});
});
}