flutter/packages/flutter_tools/test/commands.shard/hermetic/build_web_test.dart
Matan Lurey 78cfc1ae9b
Plugin.isDevDependency if exclusively in dev_dependencies (#157462)
Work towards https://github.com/flutter/flutter/issues/56591.

I explicitly want an LGTM from @andrewkolos @jmagman @jonahwilliams before merging.

---

After this PR, `<Plugin>.isDevDependency` is resolved based on the following logic, IFF:

- The plugin comes from a package _A_ listed in the app's package's `dev_dependencies: ...`
- The package _A_ is not a normal dependency of any transitive non-dev dependency of the app

See [`compute_dev_dependencies_test.dart`](51676093a3/packages/flutter_tools/test/general.shard/compute_dev_dependencies_test.dart) for probably the best specification of this behavior.

We (still) do not write the property to disk (i.e. it never makes it to `.flutter-plugins-dependencies`), so there is no impact to build artifacts at this time; that would come in a follow-up PR (and then follow-up follow-up PRs for the various build systems in both Gradle and Xcode to actually use that value to omit dependencies).

Some tests had to be updated; for the most part it was updating the default `ProcessManager` because a call to `dart pub deps --json` is now made in code that computes what plugins are available, but there should be no change in behavior.

_/cc @jonasfj @sigurdm for FYI only (we talked on an internal thread about this; see https://github.com/dart-lang/sdk/issues/56968)._

_/cc @camsim99 @cbracken @johnmccutchan for visibility on the change._
2024-11-07 18:09:22 +00:00

524 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.
import 'package:args/command_runner.dart';
import 'package:file/memory.dart';
import 'package:flutter_tools/src/artifacts.dart';
import 'package:flutter_tools/src/base/common.dart';
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/base/platform.dart';
import 'package:flutter_tools/src/base/process.dart';
import 'package:flutter_tools/src/build_info.dart';
import 'package:flutter_tools/src/build_system/build_system.dart';
import 'package:flutter_tools/src/build_system/targets/web.dart';
import 'package:flutter_tools/src/cache.dart';
import 'package:flutter_tools/src/commands/build.dart';
import 'package:flutter_tools/src/commands/build_web.dart';
import 'package:flutter_tools/src/features.dart';
import 'package:flutter_tools/src/runner/flutter_command.dart';
import 'package:flutter_tools/src/web/compile.dart';
import '../../src/common.dart';
import '../../src/context.dart';
import '../../src/fakes.dart';
import '../../src/test_build_system.dart';
import '../../src/test_flutter_command_runner.dart';
void main() {
late FileSystem fileSystem;
final Platform fakePlatform = FakePlatform(
environment: <String, String>{
'FLUTTER_ROOT': '/',
},
);
late ProcessUtils processUtils;
late BufferLogger logger;
late ProcessManager processManager;
late Artifacts artifacts;
setUpAll(() {
Cache.flutterRoot = '';
Cache.disableLocking();
});
setUp(() {
fileSystem = MemoryFileSystem.test();
fileSystem.file('pubspec.yaml')
..createSync()
..writeAsStringSync('name: foo\n');
fileSystem.directory('.dart_tool').childFile('package_config.json').createSync(recursive: true);
fileSystem.file(fileSystem.path.join('web', 'index.html')).createSync(recursive: true);
fileSystem.file(fileSystem.path.join('lib', 'main.dart')).createSync(recursive: true);
artifacts = Artifacts.test(fileSystem: fileSystem);
logger = BufferLogger.test();
processManager = FakeProcessManager.any();
processUtils = ProcessUtils(
logger: logger,
processManager: processManager,
);
});
testUsingContext('Refuses to build for web when missing index.html', () async {
fileSystem.file(fileSystem.path.join('web', 'index.html')).deleteSync();
final CommandRunner<void> runner = createTestCommandRunner(BuildCommand(
artifacts: artifacts,
androidSdk: FakeAndroidSdk(),
buildSystem: TestBuildSystem.all(BuildResult(success: true)),
fileSystem: fileSystem,
logger: logger,
processUtils: processUtils,
osUtils: FakeOperatingSystemUtils(),
));
expect(
() => runner.run(<String>['build', 'web', '--no-pub']),
throwsToolExit(message: 'Missing index.html.')
);
}, overrides: <Type, Generator>{
Platform: () => fakePlatform,
FileSystem: () => fileSystem,
FeatureFlags: () => TestFeatureFlags(isWebEnabled: true),
ProcessManager: () => processManager,
});
testUsingContext('Refuses to build for web when feature is disabled', () async {
final CommandRunner<void> runner = createTestCommandRunner(BuildCommand(
artifacts: artifacts,
androidSdk: FakeAndroidSdk(),
buildSystem: TestBuildSystem.all(BuildResult(success: true)),
fileSystem: MemoryFileSystem.test(),
logger: logger,
processUtils: processUtils,
osUtils: FakeOperatingSystemUtils(),
));
expect(
() => runner.run(<String>['build', 'web', '--no-pub']),
throwsToolExit(message: '"build web" is not currently supported. To enable, run "flutter config --enable-web".')
);
}, overrides: <Type, Generator>{
Platform: () => fakePlatform,
FileSystem: () => fileSystem,
FeatureFlags: () => TestFeatureFlags(),
ProcessManager: () => processManager,
});
testUsingContext('Setup for a web build with default output directory', () async {
final BuildCommand buildCommand = BuildCommand(
artifacts: artifacts,
androidSdk: FakeAndroidSdk(),
buildSystem: TestBuildSystem.all(BuildResult(success: true)),
fileSystem: fileSystem,
logger: logger,
processUtils: processUtils,
osUtils: FakeOperatingSystemUtils(),
);
final CommandRunner<void> runner = createTestCommandRunner(buildCommand);
setupFileSystemForEndToEndTest(fileSystem);
await runner.run(<String>['build', 'web', '--no-pub', '--no-web-resources-cdn', '--dart-define=foo=a', '--dart2js-optimization=O3']);
final Directory buildDir = fileSystem.directory(fileSystem.path.join('build', 'web'));
expect(buildDir.existsSync(), true);
expect(testLogger.statusText, contains('✓ Built ${buildDir.path}'));
}, overrides: <Type, Generator>{
Platform: () => fakePlatform,
FileSystem: () => fileSystem,
FeatureFlags: () => TestFeatureFlags(isWebEnabled: true),
ProcessManager: () => processManager,
BuildSystem: () => TestBuildSystem.all(BuildResult(success: true), (Target target, Environment environment) {
expect(environment.defines, <String, String>{
'TargetFile': 'lib/main.dart',
'HasWebPlugins': 'true',
'ServiceWorkerStrategy': 'offline-first',
'BuildMode': 'release',
'DartDefines': 'Zm9vPWE=',
'DartObfuscation': 'false',
'TrackWidgetCreation': 'false',
'TreeShakeIcons': 'true',
'UseLocalCanvasKit': 'true',
});
}),
});
testUsingContext('Does not allow -O0 optimization level', () async {
final BuildCommand buildCommand = BuildCommand(
artifacts: artifacts,
androidSdk: FakeAndroidSdk(),
buildSystem: TestBuildSystem.all(BuildResult(success: true)),
fileSystem: fileSystem,
logger: BufferLogger.test(),
osUtils: FakeOperatingSystemUtils(),
processUtils: processUtils,
);
final CommandRunner<void> runner = createTestCommandRunner(buildCommand);
setupFileSystemForEndToEndTest(fileSystem);
await expectLater(
() => runner.run(<String>[
'build',
'web',
'--no-pub', '--no-web-resources-cdn', '--dart-define=foo=a', '--dart2js-optimization=O0']),
throwsUsageException(message: '"O0" is not an allowed value for option "--dart2js-optimization"'),
);
final Directory buildDir = fileSystem.directory(fileSystem.path.join('build', 'web'));
expect(buildDir.existsSync(), isFalse);
}, overrides: <Type, Generator>{
Platform: () => fakePlatform,
FileSystem: () => fileSystem,
FeatureFlags: () => TestFeatureFlags(isWebEnabled: true),
ProcessManager: () => processManager,
BuildSystem: () => TestBuildSystem.all(BuildResult(success: true), (Target target, Environment environment) {
expect(environment.defines, <String, String>{
'TargetFile': 'lib/main.dart',
'HasWebPlugins': 'true',
'cspMode': 'false',
'SourceMaps': 'false',
'NativeNullAssertions': 'true',
'ServiceWorkerStrategy': 'offline-first',
'Dart2jsDumpInfo': 'false',
'Dart2jsNoFrequencyBasedMinification': 'false',
'Dart2jsOptimization': 'O3',
'BuildMode': 'release',
'DartDefines': 'Zm9vPWE=,RkxVVFRFUl9XRUJfQVVUT19ERVRFQ1Q9dHJ1ZQ==',
'DartObfuscation': 'false',
'TrackWidgetCreation': 'false',
'TreeShakeIcons': 'true',
});
}),
});
testUsingContext('Setup for a web build with a user specified output directory',
() async {
final BuildCommand buildCommand = BuildCommand(
artifacts: artifacts,
androidSdk: FakeAndroidSdk(),
buildSystem: TestBuildSystem.all(BuildResult(success: true)),
fileSystem: fileSystem,
logger: logger,
processUtils: processUtils,
osUtils: FakeOperatingSystemUtils(),
);
final CommandRunner<void> runner = createTestCommandRunner(buildCommand);
setupFileSystemForEndToEndTest(fileSystem);
const String newBuildDir = 'new_dir';
final Directory buildDir = fileSystem.directory(fileSystem.path.join(newBuildDir));
expect(buildDir.existsSync(), false);
await runner.run(<String>[
'build',
'web',
'--no-pub',
'--no-web-resources-cdn',
'--output=$newBuildDir'
]);
expect(buildDir.existsSync(), true);
expect(testLogger.statusText, contains('✓ Built $newBuildDir'));
}, overrides: <Type, Generator>{
Platform: () => fakePlatform,
FileSystem: () => fileSystem,
FeatureFlags: () => TestFeatureFlags(isWebEnabled: true),
ProcessManager: () => processManager,
BuildSystem: () => TestBuildSystem.all(BuildResult(success: true), (Target target, Environment environment) {
expect(environment.defines, <String, String>{
'TargetFile': 'lib/main.dart',
'HasWebPlugins': 'true',
'ServiceWorkerStrategy': 'offline-first',
'BuildMode': 'release',
'DartObfuscation': 'false',
'TrackWidgetCreation': 'false',
'TreeShakeIcons': 'true',
'UseLocalCanvasKit': 'true',
});
}),
});
testUsingContext('hidden if feature flag is not enabled', () async {
expect(BuildWebCommand(fileSystem: fileSystem, logger: BufferLogger.test(), verboseHelp: false).hidden, true);
}, overrides: <Type, Generator>{
Platform: () => fakePlatform,
FileSystem: () => fileSystem,
FeatureFlags: () => TestFeatureFlags(),
ProcessManager: () => processManager,
});
testUsingContext('not hidden if feature flag is enabled', () async {
expect(BuildWebCommand(fileSystem: fileSystem, logger: BufferLogger.test(), verboseHelp: false).hidden, false);
}, overrides: <Type, Generator>{
Platform: () => fakePlatform,
FileSystem: () => fileSystem,
FeatureFlags: () => TestFeatureFlags(isWebEnabled: true),
ProcessManager: () => processManager,
});
testUsingContext('Defaults to web renderer canvaskit mode when no option is specified', () async {
final TestWebBuildCommand buildCommand = TestWebBuildCommand(fileSystem: fileSystem);
final CommandRunner<void> runner = createTestCommandRunner(buildCommand);
setupFileSystemForEndToEndTest(fileSystem);
await runner.run(<String>['build', 'web', '--no-pub']);
}, overrides: <Type, Generator>{
Platform: () => fakePlatform,
FileSystem: () => fileSystem,
FeatureFlags: () => TestFeatureFlags(isWebEnabled: true),
ProcessManager: () => processManager,
BuildSystem: () => TestBuildSystem.all(BuildResult(success: true), (Target target, Environment environment) {
expect(target, isA<WebServiceWorker>());
final List<WebCompilerConfig> configs = (target as WebServiceWorker).compileConfigs;
expect(configs.length, 1);
expect(configs.first.renderer, WebRendererMode.canvaskit);
}),
});
testUsingContext('Defaults to web renderer skwasm mode for wasm when no option is specified', () async {
final TestWebBuildCommand buildCommand = TestWebBuildCommand(fileSystem: fileSystem);
final CommandRunner<void> runner = createTestCommandRunner(buildCommand);
setupFileSystemForEndToEndTest(fileSystem);
await runner.run(<String>['build', 'web', '--no-pub', '--wasm']);
}, overrides: <Type, Generator>{
Platform: () => fakePlatform,
FileSystem: () => fileSystem,
FeatureFlags: () => TestFeatureFlags(isWebEnabled: true),
ProcessManager: () => processManager,
BuildSystem: () => TestBuildSystem.all(BuildResult(success: true), (Target target, Environment environment) {
expect(target, isA<WebServiceWorker>());
final List<WebCompilerConfig> configs = (target as WebServiceWorker).compileConfigs;
expect(configs.length, 2);
expect(configs[0].renderer, WebRendererMode.skwasm);
expect(configs[0].compileTarget, CompileTarget.wasm);
expect(configs[1].renderer, WebRendererMode.canvaskit);
expect(configs[1].compileTarget, CompileTarget.js);
}),
});
testUsingContext('Web build supports build-name and build-number', () async {
final TestWebBuildCommand buildCommand = TestWebBuildCommand(fileSystem: fileSystem);
final CommandRunner<void> runner = createTestCommandRunner(buildCommand);
setupFileSystemForEndToEndTest(fileSystem);
await runner.run(<String>[
'build',
'web',
'--no-pub',
'--build-name=1.2.3',
'--build-number=42',
]);
final BuildInfo buildInfo = await buildCommand.webCommand
.getBuildInfo(forcedBuildMode: BuildMode.debug);
expect(buildInfo.buildNumber, '42');
expect(buildInfo.buildName, '1.2.3');
}, overrides: <Type, Generator>{
Platform: () => fakePlatform,
FileSystem: () => fileSystem,
FeatureFlags: () => TestFeatureFlags(isWebEnabled: true),
ProcessManager: () => processManager,
BuildSystem: () => TestBuildSystem.all(BuildResult(success: true)),
});
testUsingContext('Does not override custom CanvasKit URL', () async {
final TestWebBuildCommand buildCommand = TestWebBuildCommand(fileSystem: fileSystem);
final CommandRunner<void> runner = createTestCommandRunner(buildCommand);
setupFileSystemForEndToEndTest(fileSystem);
await runner.run(<String>['build', 'web', '--no-pub', '--web-resources-cdn', '--dart-define=FLUTTER_WEB_CANVASKIT_URL=abcdefg']);
final BuildInfo buildInfo =
await buildCommand.webCommand.getBuildInfo(forcedBuildMode: BuildMode.debug);
expect(buildInfo.dartDefines, contains('FLUTTER_WEB_CANVASKIT_URL=abcdefg'));
}, overrides: <Type, Generator>{
Platform: () => fakePlatform,
FileSystem: () => fileSystem,
FeatureFlags: () => TestFeatureFlags(isWebEnabled: true),
ProcessManager: () => processManager,
BuildSystem: () => TestBuildSystem.all(BuildResult(success: true)),
});
testUsingContext('Rejects --base-href value that does not start with /', () async {
final TestWebBuildCommand buildCommand = TestWebBuildCommand(fileSystem: fileSystem);
final CommandRunner<void> runner = createTestCommandRunner(buildCommand);
await expectLater(
runner.run(<String>[
'build',
'web',
'--no-pub',
'--base-href=i_dont_start_with_a_forward_slash',
]),
throwsToolExit(
message: 'Received a --base-href value of "i_dont_start_with_a_forward_slash"\n'
'--base-href should start and end with /',
),
);
}, overrides: <Type, Generator>{
Platform: () => fakePlatform,
FileSystem: () => fileSystem,
ProcessManager: () => processManager,
});
// Tests whether using a deprecated webRenderer toggles a warningText.
Future<void> testWebRendererDeprecationMessage(WebRendererMode webRenderer) async {
testUsingContext('Using --web-renderer=${webRenderer.name} triggers a warningText.', () async {
// Run the command so it parses --web-renderer, but ignore all errors.
// We only care about the logger.
try {
final TestWebBuildCommand buildCommand = TestWebBuildCommand(fileSystem: fileSystem);
await createTestCommandRunner(buildCommand).run(<String>[
'build',
'web',
'--no-pub',
'--web-renderer=${webRenderer.name}',
]);
} on ToolExit catch (error) {
expect(error, isA<ToolExit>());
}
expect(logger.warningText, contains(
'See: https://docs.flutter.dev/to/web-html-renderer-deprecation'
));
}, overrides: <Type, Generator>{
Platform: () => fakePlatform,
FileSystem: () => fileSystem,
ProcessManager: () => processManager,
Logger: () => logger,
});
}
/// Do test all the deprecated WebRendererModes
WebRendererMode.values
.where((WebRendererMode mode) => mode.isDeprecated)
.forEach(testWebRendererDeprecationMessage);
testUsingContext('flutter build web option visibility', () async {
final TestWebBuildCommand buildCommand = TestWebBuildCommand(fileSystem: fileSystem);
createTestCommandRunner(buildCommand);
final BuildWebCommand command = buildCommand.subcommands.values.single as BuildWebCommand;
void expectVisible(String option) {
expect(command.argParser.options.keys, contains(option));
expect(command.argParser.options[option]!.hide, isFalse);
expect(command.usage, contains(option));
}
void expectHidden(String option) {
expect(command.argParser.options.keys, contains(option));
expect(command.argParser.options[option]!.hide, isTrue);
expect(command.usage, isNot(contains(option)));
}
expectVisible('pwa-strategy');
expectVisible('web-resources-cdn');
expectVisible('optimization-level');
expectVisible('source-maps');
expectVisible('csp');
expectVisible('dart2js-optimization');
expectVisible('dump-info');
expectVisible('no-frequency-based-minification');
expectVisible('wasm');
expectVisible('strip-wasm');
expectVisible('base-href');
expectHidden('web-renderer');
}, overrides: <Type, Generator>{
Platform: () => fakePlatform,
FileSystem: () => fileSystem,
FeatureFlags: () => TestFeatureFlags(isWebEnabled: true),
ProcessManager: () => processManager,
});
}
void setupFileSystemForEndToEndTest(FileSystem fileSystem) {
final List<String> dependencies = <String>[
fileSystem.path.join('packages', 'flutter_tools', 'lib', 'src', 'build_system', 'targets', 'web.dart'),
fileSystem.path.join('bin', 'cache', 'flutter_web_sdk'),
fileSystem.path.join('bin', 'cache', 'dart-sdk', 'bin', 'dart'),
fileSystem.path.join('bin', 'cache', 'dart-sdk', 'bin', 'dartaotruntime'),
fileSystem.path.join('bin', 'cache', 'dart-sdk '),
];
for (final String dependency in dependencies) {
fileSystem.file(dependency).createSync(recursive: true);
}
// Project files.
fileSystem
.directory('.dart_tool')
.childFile('package_config.json')
..createSync(recursive: true)
..writeAsStringSync('''
{
"packages": [
{
"name": "foo",
"rootUri": "../",
"packageUri": "lib/",
"languageVersion": "3.2"
},
{
"name": "fizz",
"rootUri": "../bar",
"packageUri": "lib/",
"languageVersion": "3.2"
}
],
"configVersion": 2
}
''');
fileSystem.file('pubspec.yaml')
.writeAsStringSync('''
name: foo
dependencies:
flutter:
sdk: flutter
fizz:
path:
bar/
''');
fileSystem.file(fileSystem.path.join('bar', 'pubspec.yaml'))
..createSync(recursive: true)
..writeAsStringSync('''
name: bar
flutter:
plugin:
platforms:
web:
pluginClass: UrlLauncherPlugin
fileName: url_launcher_web.dart
''');
fileSystem.file(fileSystem.path.join('bar', 'lib', 'url_launcher_web.dart'))
..createSync(recursive: true)
..writeAsStringSync('''
class UrlLauncherPlugin {}
''');
fileSystem.file(fileSystem.path.join('lib', 'main.dart'))
.writeAsStringSync('void main() { }');
}
class TestWebBuildCommand extends FlutterCommand {
TestWebBuildCommand({ required FileSystem fileSystem, bool verboseHelp = false }) :
webCommand = BuildWebCommand(
fileSystem: fileSystem,
logger: BufferLogger.test(),
verboseHelp: verboseHelp) {
addSubcommand(webCommand);
}
final BuildWebCommand webCommand;
@override
final String name = 'build';
@override
final String description = 'Build a test executable app.';
@override
Future<FlutterCommandResult> runCommand() async => FlutterCommandResult.fail();
@override
bool get shouldRunPub => false;
}