From 354ce1aa5aa6f3221760e486cd08f3564ad1b94d Mon Sep 17 00:00:00 2001 From: Jonah Williams Date: Mon, 10 Jun 2019 11:27:25 -0700 Subject: [PATCH] More verification on flutter build web, add tests and cleanup (#34090) --- .../lib/src/application_package.dart | 3 + packages/flutter_tools/lib/src/project.dart | 7 +- .../lib/src/resident_web_runner.dart | 12 ++- .../flutter_tools/lib/src/web/compile.dart | 3 + .../test/commands/build_linux_test.dart | 37 ++++--- .../test/commands/build_macos_test.dart | 40 +++++--- .../test/commands/build_web_test.dart | 96 +++++++++++++++++++ .../test/commands/build_windows_test.dart | 45 +++++---- packages/flutter_tools/test/src/testbed.dart | 42 +++++++- 9 files changed, 234 insertions(+), 51 deletions(-) create mode 100644 packages/flutter_tools/test/commands/build_web_test.dart diff --git a/packages/flutter_tools/lib/src/application_package.dart b/packages/flutter_tools/lib/src/application_package.dart index 60f0276439c..0cc9f2183ea 100644 --- a/packages/flutter_tools/lib/src/application_package.dart +++ b/packages/flutter_tools/lib/src/application_package.dart @@ -57,6 +57,9 @@ class ApplicationPackageFactory { ? MacOSApp.fromMacOSProject(FlutterProject.current().macos) : MacOSApp.fromPrebuiltApp(applicationBinary); case TargetPlatform.web_javascript: + if (!FlutterProject.current().web.existsSync()) { + return null; + } return WebApplicationPackage(FlutterProject.current()); case TargetPlatform.linux_x64: return applicationBinary == null diff --git a/packages/flutter_tools/lib/src/project.dart b/packages/flutter_tools/lib/src/project.dart index 2ea276e2b22..ef4a38d39e5 100644 --- a/packages/flutter_tools/lib/src/project.dart +++ b/packages/flutter_tools/lib/src/project.dart @@ -580,11 +580,14 @@ class WebProject { /// Whether this flutter project has a web sub-project. bool existsSync() { - return parent.directory.childDirectory('web').existsSync(); + return parent.directory.childDirectory('web').existsSync() + && indexFile.existsSync(); } /// The html file used to host the flutter web application. - File get indexFile => parent.directory.childDirectory('web').childFile('index.html'); + File get indexFile => parent.directory + .childDirectory('web') + .childFile('index.html'); Future ensureReadyForPlatformSpecificTooling() async {} } diff --git a/packages/flutter_tools/lib/src/resident_web_runner.dart b/packages/flutter_tools/lib/src/resident_web_runner.dart index dffef97df12..5b2cdee31a5 100644 --- a/packages/flutter_tools/lib/src/resident_web_runner.dart +++ b/packages/flutter_tools/lib/src/resident_web_runner.dart @@ -7,6 +7,7 @@ import 'dart:async'; import 'package:meta/meta.dart'; import 'package:webkit_inspection_protocol/webkit_inspection_protocol.dart'; +import 'application_package.dart'; import 'asset.dart'; import 'base/common.dart'; import 'base/file_system.dart'; @@ -112,7 +113,14 @@ class ResidentWebRunner extends ResidentRunner { String route, bool shouldBuild = true, }) async { - final FlutterProject currentProject = FlutterProject.current(); + final ApplicationPackage package = await ApplicationPackageFactory.instance.getPackageForPlatform( + TargetPlatform.web_javascript, + applicationBinary: null, + ); + if (package == null) { + printError('No application found for TargetPlatform.web_javascript'); + return 1; + } if (!fs.isFileSync(mainPath)) { String message = 'Tried to run $mainPath, but that file does not exist.'; if (target == null) { @@ -124,7 +132,7 @@ class ResidentWebRunner extends ResidentRunner { } // Start the web compiler and build the assets. await webCompilationProxy.initialize( - projectDirectory: currentProject.directory, + projectDirectory: FlutterProject.current().directory, targets: [target], ); _lastCompiled = DateTime.now(); diff --git a/packages/flutter_tools/lib/src/web/compile.dart b/packages/flutter_tools/lib/src/web/compile.dart index 3816377dace..42d6ff28739 100644 --- a/packages/flutter_tools/lib/src/web/compile.dart +++ b/packages/flutter_tools/lib/src/web/compile.dart @@ -19,6 +19,9 @@ import '../usage.dart'; WebCompilationProxy get webCompilationProxy => context.get(); Future buildWeb(FlutterProject flutterProject, String target, BuildInfo buildInfo) async { + if (!flutterProject.web.existsSync()) { + throwToolExit('Missing index.html.'); + } final Status status = logger.startProgress('Compiling $target for the Web...', timeout: null); final Stopwatch sw = Stopwatch()..start(); final Directory outputDir = fs.directory(getWebBuildDirectory()) diff --git a/packages/flutter_tools/test/commands/build_linux_test.dart b/packages/flutter_tools/test/commands/build_linux_test.dart index 3e59b242d99..da9d918e26f 100644 --- a/packages/flutter_tools/test/commands/build_linux_test.dart +++ b/packages/flutter_tools/test/commands/build_linux_test.dart @@ -19,23 +19,32 @@ import '../src/context.dart'; import '../src/mocks.dart'; void main() { - Cache.disableLocking(); - final MockProcessManager mockProcessManager = MockProcessManager(); - final MockProcess mockProcess = MockProcess(); - final MockPlatform linuxPlatform = MockPlatform(); - final MockPlatform notLinuxPlatform = MockPlatform(); + MockProcessManager mockProcessManager; + MockProcess mockProcess; + MockPlatform linuxPlatform; + MockPlatform notLinuxPlatform; - when(mockProcess.exitCode).thenAnswer((Invocation invocation) async { - return 0; + setUpAll(() { + Cache.disableLocking(); }); - when(mockProcess.stderr).thenAnswer((Invocation invocation) { - return const Stream>.empty(); + + setUp(() { + mockProcessManager = MockProcessManager(); + mockProcess = MockProcess(); + linuxPlatform = MockPlatform(); + notLinuxPlatform = MockPlatform(); + when(mockProcess.exitCode).thenAnswer((Invocation invocation) async { + return 0; + }); + when(mockProcess.stderr).thenAnswer((Invocation invocation) { + return const Stream>.empty(); + }); + when(mockProcess.stdout).thenAnswer((Invocation invocation) { + return const Stream>.empty(); + }); + when(linuxPlatform.isLinux).thenReturn(true); + when(notLinuxPlatform.isLinux).thenReturn(false); }); - when(mockProcess.stdout).thenAnswer((Invocation invocation) { - return const Stream>.empty(); - }); - when(linuxPlatform.isLinux).thenReturn(true); - when(notLinuxPlatform.isLinux).thenReturn(false); testUsingContext('Linux build fails when there is no linux project', () async { final BuildCommand command = BuildCommand(); diff --git a/packages/flutter_tools/test/commands/build_macos_test.dart b/packages/flutter_tools/test/commands/build_macos_test.dart index 946cb41400d..20d802766fd 100644 --- a/packages/flutter_tools/test/commands/build_macos_test.dart +++ b/packages/flutter_tools/test/commands/build_macos_test.dart @@ -19,24 +19,34 @@ import '../src/context.dart'; import '../src/mocks.dart'; void main() { - Cache.disableLocking(); - final MockProcessManager mockProcessManager = MockProcessManager(); - final MemoryFileSystem memoryFilesystem = MemoryFileSystem(); - final MockProcess mockProcess = MockProcess(); - final MockPlatform macosPlatform = MockPlatform(); - final MockPlatform notMacosPlatform = MockPlatform(); + MockProcessManager mockProcessManager; + MemoryFileSystem memoryFilesystem; + MockProcess mockProcess; + MockPlatform macosPlatform; + MockPlatform notMacosPlatform; - when(mockProcess.exitCode).thenAnswer((Invocation invocation) async { + setUpAll(() { + Cache.disableLocking(); + }); + + setUp(() { + mockProcessManager = MockProcessManager(); + memoryFilesystem = MemoryFileSystem(); + mockProcess = MockProcess(); + macosPlatform = MockPlatform(); + notMacosPlatform = MockPlatform(); + when(mockProcess.exitCode).thenAnswer((Invocation invocation) async { return 0; + }); + when(mockProcess.stderr).thenAnswer((Invocation invocation) { + return const Stream>.empty(); + }); + when(mockProcess.stdout).thenAnswer((Invocation invocation) { + return const Stream>.empty(); + }); + when(macosPlatform.isMacOS).thenReturn(true); + when(notMacosPlatform.isMacOS).thenReturn(false); }); - when(mockProcess.stderr).thenAnswer((Invocation invocation) { - return const Stream>.empty(); - }); - when(mockProcess.stdout).thenAnswer((Invocation invocation) { - return const Stream>.empty(); - }); - when(macosPlatform.isMacOS).thenReturn(true); - when(notMacosPlatform.isMacOS).thenReturn(false); testUsingContext('macOS build fails when there is no macos project', () async { final BuildCommand command = BuildCommand(); diff --git a/packages/flutter_tools/test/commands/build_web_test.dart b/packages/flutter_tools/test/commands/build_web_test.dart new file mode 100644 index 00000000000..f32461943b7 --- /dev/null +++ b/packages/flutter_tools/test/commands/build_web_test.dart @@ -0,0 +1,96 @@ +// Copyright 2019 The Chromium 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:flutter_tools/src/base/common.dart'; +import 'package:flutter_tools/src/base/file_system.dart'; +import 'package:flutter_tools/src/base/platform.dart'; +import 'package:flutter_tools/src/build_info.dart'; +import 'package:flutter_tools/src/cache.dart'; +import 'package:flutter_tools/src/project.dart'; +import 'package:flutter_tools/src/resident_runner.dart'; +import 'package:flutter_tools/src/resident_web_runner.dart'; +import 'package:flutter_tools/src/version.dart'; +import 'package:flutter_tools/src/web/compile.dart'; +import 'package:mockito/mockito.dart'; + +import '../src/common.dart'; +import '../src/testbed.dart'; + +void main() { + MockWebCompilationProxy mockWebCompilationProxy; + Testbed testbed; + MockPlatform mockPlatform; + + setUpAll(() { + Cache.disableLocking(); + }); + + setUp(() { + mockWebCompilationProxy = MockWebCompilationProxy(); + testbed = Testbed(setup: () { + fs.file('pubspec.yaml') + ..createSync() + ..writeAsStringSync('name: foo\n'); + fs.file('.packages').createSync(); + fs.file(fs.path.join('web', 'index.html')).createSync(recursive: true); + fs.file(fs.path.join('lib', 'main.dart')).createSync(recursive: true); + when(mockWebCompilationProxy.initialize( + projectDirectory: anyNamed('projectDirectory'), + targets: anyNamed('targets'), + release: anyNamed('release') + )).thenAnswer((Invocation invocation) { + final String path = fs.path.join('.dart_tool', 'build', 'flutter_web', 'foo', 'lib', 'main_web_entrypoint.dart.js'); + fs.file(path).createSync(recursive: true); + fs.file('$path.map').createSync(); + return Future.value(true); + }); + }, overrides: { + WebCompilationProxy: () => mockWebCompilationProxy, + Platform: () => mockPlatform, + FlutterVersion: () => MockFlutterVersion(), + }); + }); + + test('Refuses to build for web when missing index.html', () => testbed.run(() async { + fs.file(fs.path.join('web', 'index.html')).deleteSync(); + + expect(buildWeb( + FlutterProject.current(), + fs.path.join('lib', 'main.dart'), + BuildInfo.debug, + ), throwsA(isInstanceOf())); + })); + + test('Refuses to build using runner when missing index.html', () => testbed.run(() async { + fs.file(fs.path.join('web', 'index.html')).deleteSync(); + + final ResidentWebRunner runner = ResidentWebRunner( + [], + flutterProject: FlutterProject.current(), + ipv6: false, + ); + expect(await runner.run(), 1); + })); + + test('Can build for web', () => testbed.run(() async { + + await buildWeb( + FlutterProject.current(), + fs.path.join('lib', 'main.dart'), + BuildInfo.debug, + ); + })); +} + +class MockWebCompilationProxy extends Mock implements WebCompilationProxy {} +class MockPlatform extends Mock implements Platform { + @override + Map environment = { + 'FLUTTER_ROOT': '/', + }; +} +class MockFlutterVersion extends Mock implements FlutterVersion { + @override + bool get isStable => false; +} diff --git a/packages/flutter_tools/test/commands/build_windows_test.dart b/packages/flutter_tools/test/commands/build_windows_test.dart index cc86816b7d8..4c7b9e38ca6 100644 --- a/packages/flutter_tools/test/commands/build_windows_test.dart +++ b/packages/flutter_tools/test/commands/build_windows_test.dart @@ -19,29 +19,40 @@ import '../src/context.dart'; import '../src/mocks.dart'; void main() { - Cache.disableLocking(); - final MockProcessManager mockProcessManager = MockProcessManager(); - final MemoryFileSystem memoryFilesystem = MemoryFileSystem(style: FileSystemStyle.windows); - final MockProcess mockProcess = MockProcess(); - final MockPlatform windowsPlatform = MockPlatform() - ..environment['PROGRAMFILES(X86)'] = r'C:\Program Files (x86)\'; - final MockPlatform notWindowsPlatform = MockPlatform(); - final MockVisualStudio mockVisualStudio = MockVisualStudio(); + MockProcessManager mockProcessManager; + MemoryFileSystem memoryFilesystem; + MockProcess mockProcess; + MockPlatform windowsPlatform; + MockPlatform notWindowsPlatform; + MockVisualStudio mockVisualStudio; const String solutionPath = r'C:\windows\Runner.sln'; const String visualStudioPath = r'C:\Program Files (x86)\Microsoft Visual Studio\2017\Community'; const String vcvarsPath = visualStudioPath + r'\VC\Auxiliary\Build\vcvars64.bat'; - when(mockProcess.exitCode).thenAnswer((Invocation invocation) async { - return 0; + setUpAll(() { + Cache.disableLocking(); }); - when(mockProcess.stderr).thenAnswer((Invocation invocation) { - return const Stream>.empty(); + + setUp(() { + mockProcessManager = MockProcessManager(); + memoryFilesystem = MemoryFileSystem(style: FileSystemStyle.windows); + mockProcess = MockProcess(); + windowsPlatform = MockPlatform() + ..environment['PROGRAMFILES(X86)'] = r'C:\Program Files (x86)\'; + notWindowsPlatform = MockPlatform(); + mockVisualStudio = MockVisualStudio(); + when(mockProcess.exitCode).thenAnswer((Invocation invocation) async { + return 0; + }); + when(mockProcess.stderr).thenAnswer((Invocation invocation) { + return const Stream>.empty(); + }); + when(mockProcess.stdout).thenAnswer((Invocation invocation) { + return const Stream>.empty(); + }); + when(windowsPlatform.isWindows).thenReturn(true); + when(notWindowsPlatform.isWindows).thenReturn(false); }); - when(mockProcess.stdout).thenAnswer((Invocation invocation) { - return const Stream>.empty(); - }); - when(windowsPlatform.isWindows).thenReturn(true); - when(notWindowsPlatform.isWindows).thenReturn(false); testUsingContext('Windows build fails when there is no vcvars64.bat', () async { final BuildCommand command = BuildCommand(); diff --git a/packages/flutter_tools/test/src/testbed.dart b/packages/flutter_tools/test/src/testbed.dart index 53ecb8820ac..77208e4011b 100644 --- a/packages/flutter_tools/test/src/testbed.dart +++ b/packages/flutter_tools/test/src/testbed.dart @@ -11,6 +11,7 @@ import 'package:flutter_tools/src/base/logger.dart'; import 'package:flutter_tools/src/base/terminal.dart'; import 'package:flutter_tools/src/cache.dart'; import 'package:flutter_tools/src/context_runner.dart'; +import 'package:flutter_tools/src/usage.dart'; import 'context.dart'; @@ -24,6 +25,7 @@ final Map _testbedDefaults = { FileSystem: () => MemoryFileSystem(), // Keeps tests fast by avoid actual file system. Logger: () => BufferLogger(), // Allows reading logs and prevents stdout. OutputPreferences: () => OutputPreferences(showColor: false), // configures BufferLogger to avoid color codes. + Usage: () => NoOpUsage(), // prevent addition of analytics from burdening test mocks }; /// Manages interaction with the tool injection and runner system. @@ -60,7 +62,7 @@ class Testbed { /// `overrides` provides more overrides in addition to the test defaults. /// `setup` may be provided to apply mocks within the tool managed zone, /// including any specified overrides. - Testbed({Future Function() setup, Map overrides}) + Testbed({FutureOr Function() setup, Map overrides}) : _setup = setup, _overrides = overrides; @@ -101,3 +103,41 @@ class Testbed { }); } } + +/// A no-op implementation of [Usage] for testing. +class NoOpUsage implements Usage { + @override + bool enabled = false; + + @override + bool suppressAnalytics = true; + + @override + String get clientId => 'test'; + + @override + Future ensureAnalyticsSent() { + return null; + } + + @override + bool get isFirstRun => false; + + @override + Stream> get onSend => const Stream.empty(); + + @override + void printWelcome() {} + + @override + void sendCommand(String command, {Map parameters}) {} + + @override + void sendEvent(String category, String parameter, {Map parameters}) {} + + @override + void sendException(dynamic exception, StackTrace trace) {} + + @override + void sendTiming(String category, String variableName, Duration duration, {String label}) {} +} \ No newline at end of file