| // 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. |
| |
| // @dart = 2.8 |
| |
| import 'dart:convert'; |
| import 'dart:io' hide Directory, File; |
| |
| import 'package:file/memory.dart'; |
| import 'package:flutter_tools/src/artifacts.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/terminal.dart'; |
| import 'package:flutter_tools/src/build_system/build_system.dart'; |
| import 'package:flutter_tools/src/build_system/targets/common.dart'; |
| import 'package:flutter_tools/src/build_system/targets/icon_tree_shaker.dart'; |
| import 'package:flutter_tools/src/devfs.dart'; |
| import 'package:meta/meta.dart'; |
| import 'package:mockito/mockito.dart'; |
| |
| import '../../../src/common.dart'; |
| import '../../../src/context.dart'; |
| import '../../../src/fakes.dart'; |
| |
| final Platform kNoAnsiPlatform = FakePlatform(stdoutSupportsAnsi: false); |
| const List<int> _kTtfHeaderBytes = <int>[0, 1, 0, 0, 0, 15, 0, 128, 0, 3, 0, 112]; |
| |
| const String inputPath = '/input/fonts/MaterialIcons-Regular.otf'; |
| const String outputPath = '/output/fonts/MaterialIcons-Regular.otf'; |
| const String relativePath = 'fonts/MaterialIcons-Regular.otf'; |
| |
| void main() { |
| BufferLogger logger; |
| MemoryFileSystem fileSystem; |
| MockProcessManager mockProcessManager; |
| MockProcess fontSubsetProcess; |
| Artifacts artifacts; |
| DevFSStringContent fontManifestContent; |
| |
| String dartPath; |
| String constFinderPath; |
| String fontSubsetPath; |
| List<String> fontSubsetArgs; |
| |
| List<String> _getConstFinderArgs(String appDillPath) => <String>[ |
| dartPath, |
| '--disable-dart-dev', |
| constFinderPath, |
| '--kernel-file', appDillPath, |
| '--class-library-uri', 'package:flutter/src/widgets/icon_data.dart', |
| '--class-name', 'IconData', |
| ]; |
| |
| void _addConstFinderInvocation( |
| String appDillPath, { |
| int exitCode = 0, |
| String stdout = '', |
| String stderr = '', |
| }) { |
| when(mockProcessManager.run(_getConstFinderArgs(appDillPath))).thenAnswer((_) async { |
| return ProcessResult(0, exitCode, stdout, stderr); |
| }); |
| } |
| |
| void _resetFontSubsetInvocation({ |
| int exitCode = 0, |
| String stdout = '', |
| String stderr = '', |
| @required CompleterIOSink stdinSink, |
| }) { |
| assert(stdinSink != null); |
| stdinSink.clear(); |
| when(fontSubsetProcess.exitCode).thenAnswer((_) async => exitCode); |
| when(fontSubsetProcess.stdout).thenAnswer((_) => Stream<List<int>>.fromIterable(<List<int>>[utf8.encode(stdout)])); |
| when(fontSubsetProcess.stderr).thenAnswer((_) => Stream<List<int>>.fromIterable(<List<int>>[utf8.encode(stderr)])); |
| when(fontSubsetProcess.stdin).thenReturn(stdinSink); |
| when(mockProcessManager.start(fontSubsetArgs)).thenAnswer((_) async { |
| return fontSubsetProcess; |
| }); |
| } |
| |
| setUp(() { |
| fontManifestContent = DevFSStringContent(validFontManifestJson); |
| |
| mockProcessManager = MockProcessManager(); |
| fontSubsetProcess = MockProcess(); |
| artifacts = Artifacts.test(); |
| fileSystem = MemoryFileSystem.test(); |
| logger = BufferLogger( |
| terminal: AnsiTerminal( |
| stdio: FakeStdio(), |
| platform: kNoAnsiPlatform, |
| ), |
| outputPreferences: OutputPreferences.test(showColor: false), |
| ); |
| |
| dartPath = artifacts.getArtifactPath(Artifact.engineDartBinary); |
| constFinderPath = artifacts.getArtifactPath(Artifact.constFinder); |
| fontSubsetPath = artifacts.getArtifactPath(Artifact.fontSubset); |
| |
| fontSubsetArgs = <String>[ |
| fontSubsetPath, |
| outputPath, |
| inputPath, |
| ]; |
| |
| fileSystem.file(constFinderPath).createSync(recursive: true); |
| fileSystem.file(dartPath).createSync(recursive: true); |
| fileSystem.file(fontSubsetPath).createSync(recursive: true); |
| fileSystem.file(inputPath) |
| ..createSync(recursive: true) |
| ..writeAsBytesSync(_kTtfHeaderBytes); |
| }); |
| |
| Environment _createEnvironment(Map<String, String> defines) { |
| return Environment.test( |
| fileSystem.directory('/icon_test')..createSync(recursive: true), |
| defines: defines, |
| artifacts: artifacts, |
| processManager: FakeProcessManager.any(), |
| fileSystem: fileSystem, |
| logger: BufferLogger.test(), |
| ); |
| } |
| |
| testWithoutContext('Prints error in debug mode environment', () async { |
| final Environment environment = _createEnvironment(<String, String>{ |
| kIconTreeShakerFlag: 'true', |
| kBuildMode: 'debug', |
| }); |
| |
| final IconTreeShaker iconTreeShaker = IconTreeShaker( |
| environment, |
| fontManifestContent, |
| logger: logger, |
| processManager: mockProcessManager, |
| fileSystem: fileSystem, |
| artifacts: artifacts, |
| ); |
| |
| expect( |
| logger.errorText, |
| 'Font subsetting is not supported in debug mode. The --tree-shake-icons' |
| ' flag will be ignored.\n', |
| ); |
| expect(iconTreeShaker.enabled, false); |
| |
| final bool subsets = await iconTreeShaker.subsetFont( |
| input: fileSystem.file(inputPath), |
| outputPath: outputPath, |
| relativePath: relativePath, |
| ); |
| expect(subsets, false); |
| |
| verifyNever(mockProcessManager.run(any)); |
| verifyNever(mockProcessManager.start(any)); |
| }); |
| |
| testWithoutContext('Does not get enabled without font manifest', () { |
| final Environment environment = _createEnvironment(<String, String>{ |
| kIconTreeShakerFlag: 'true', |
| kBuildMode: 'release', |
| }); |
| |
| final IconTreeShaker iconTreeShaker = IconTreeShaker( |
| environment, |
| null, |
| logger: logger, |
| processManager: mockProcessManager, |
| fileSystem: fileSystem, |
| artifacts: artifacts, |
| ); |
| |
| expect( |
| logger.errorText, |
| isEmpty, |
| ); |
| expect(iconTreeShaker.enabled, false); |
| verifyNever(mockProcessManager.run(any)); |
| verifyNever(mockProcessManager.start(any)); |
| }); |
| |
| testWithoutContext('Gets enabled', () { |
| final Environment environment = _createEnvironment(<String, String>{ |
| kIconTreeShakerFlag: 'true', |
| kBuildMode: 'release', |
| }); |
| |
| final IconTreeShaker iconTreeShaker = IconTreeShaker( |
| environment, |
| fontManifestContent, |
| logger: logger, |
| processManager: mockProcessManager, |
| fileSystem: fileSystem, |
| artifacts: artifacts, |
| ); |
| |
| expect( |
| logger.errorText, |
| isEmpty, |
| ); |
| expect(iconTreeShaker.enabled, true); |
| verifyNever(mockProcessManager.run(any)); |
| verifyNever(mockProcessManager.start(any)); |
| }); |
| |
| test('No app.dill throws exception', () async { |
| final Environment environment = _createEnvironment(<String, String>{ |
| kIconTreeShakerFlag: 'true', |
| kBuildMode: 'release', |
| }); |
| |
| final IconTreeShaker iconTreeShaker = IconTreeShaker( |
| environment, |
| fontManifestContent, |
| logger: logger, |
| processManager: mockProcessManager, |
| fileSystem: fileSystem, |
| artifacts: artifacts, |
| ); |
| |
| expect( |
| () async => iconTreeShaker.subsetFont( |
| input: fileSystem.file(inputPath), |
| outputPath: outputPath, |
| relativePath: relativePath, |
| ), |
| throwsA(isA<IconTreeShakerException>()), |
| ); |
| }); |
| |
| testWithoutContext('Can subset a font', () async { |
| final Environment environment = _createEnvironment(<String, String>{ |
| kIconTreeShakerFlag: 'true', |
| kBuildMode: 'release', |
| }); |
| final File appDill = environment.buildDir.childFile('app.dill') |
| ..createSync(recursive: true); |
| |
| final IconTreeShaker iconTreeShaker = IconTreeShaker( |
| environment, |
| fontManifestContent, |
| logger: logger, |
| processManager: mockProcessManager, |
| fileSystem: fileSystem, |
| artifacts: artifacts, |
| ); |
| |
| final CompleterIOSink stdinSink = CompleterIOSink(); |
| _addConstFinderInvocation(appDill.path, stdout: validConstFinderResult); |
| _resetFontSubsetInvocation(stdinSink: stdinSink); |
| |
| bool subsetted = await iconTreeShaker.subsetFont( |
| input: fileSystem.file(inputPath), |
| outputPath: outputPath, |
| relativePath: relativePath, |
| ); |
| expect(stdinSink.getAndClear(), '59470\n'); |
| _resetFontSubsetInvocation(stdinSink: stdinSink); |
| |
| expect(subsetted, true); |
| subsetted = await iconTreeShaker.subsetFont( |
| input: fileSystem.file(inputPath), |
| outputPath: outputPath, |
| relativePath: relativePath, |
| ); |
| expect(subsetted, true); |
| expect(stdinSink.getAndClear(), '59470\n'); |
| |
| verify(mockProcessManager.run(_getConstFinderArgs(appDill.path))).called(1); |
| verify(mockProcessManager.start(fontSubsetArgs)).called(2); |
| }); |
| |
| testWithoutContext('Does not subset a non-supported font', () async { |
| final Environment environment = _createEnvironment(<String, String>{ |
| kIconTreeShakerFlag: 'true', |
| kBuildMode: 'release', |
| }); |
| final File appDill = environment.buildDir.childFile('app.dill') |
| ..createSync(recursive: true); |
| |
| final IconTreeShaker iconTreeShaker = IconTreeShaker( |
| environment, |
| fontManifestContent, |
| logger: logger, |
| processManager: mockProcessManager, |
| fileSystem: fileSystem, |
| artifacts: artifacts, |
| ); |
| |
| final CompleterIOSink stdinSink = CompleterIOSink(); |
| _addConstFinderInvocation(appDill.path, stdout: validConstFinderResult); |
| _resetFontSubsetInvocation(stdinSink: stdinSink); |
| |
| final File notAFont = fileSystem.file('input/foo/bar.txt') |
| ..createSync(recursive: true) |
| ..writeAsStringSync('I could not think of a better string'); |
| final bool subsetted = await iconTreeShaker.subsetFont( |
| input: notAFont, |
| outputPath: outputPath, |
| relativePath: relativePath, |
| ); |
| expect(subsetted, false); |
| |
| verifyNever(mockProcessManager.run(_getConstFinderArgs(appDill.path))); |
| verifyNever(mockProcessManager.start(fontSubsetArgs)); |
| }); |
| |
| testWithoutContext('Does not subset an invalid ttf font', () async { |
| final Environment environment = _createEnvironment(<String, String>{ |
| kIconTreeShakerFlag: 'true', |
| kBuildMode: 'release', |
| }); |
| final File appDill = environment.buildDir.childFile('app.dill') |
| ..createSync(recursive: true); |
| |
| final IconTreeShaker iconTreeShaker = IconTreeShaker( |
| environment, |
| fontManifestContent, |
| logger: logger, |
| processManager: mockProcessManager, |
| fileSystem: fileSystem, |
| artifacts: artifacts, |
| ); |
| |
| final CompleterIOSink stdinSink = CompleterIOSink(); |
| _addConstFinderInvocation(appDill.path, stdout: validConstFinderResult); |
| _resetFontSubsetInvocation(stdinSink: stdinSink); |
| |
| final File notAFont = fileSystem.file(inputPath) |
| ..writeAsBytesSync(<int>[0, 1, 2]); |
| final bool subsetted = await iconTreeShaker.subsetFont( |
| input: notAFont, |
| outputPath: outputPath, |
| relativePath: relativePath, |
| ); |
| |
| expect(subsetted, false); |
| verifyNever(mockProcessManager.run(_getConstFinderArgs(appDill.path))); |
| verifyNever(mockProcessManager.start(fontSubsetArgs)); |
| }); |
| |
| testWithoutContext('Non-constant instances', () async { |
| final Environment environment = _createEnvironment(<String, String>{ |
| kIconTreeShakerFlag: 'true', |
| kBuildMode: 'release', |
| }); |
| final File appDill = environment.buildDir.childFile('app.dill') |
| ..createSync(recursive: true); |
| |
| final IconTreeShaker iconTreeShaker = IconTreeShaker( |
| environment, |
| fontManifestContent, |
| logger: logger, |
| processManager: mockProcessManager, |
| fileSystem: fileSystem, |
| artifacts: artifacts, |
| ); |
| |
| _addConstFinderInvocation(appDill.path, stdout: constFinderResultWithInvalid); |
| |
| await expectLater( |
| () async => iconTreeShaker.subsetFont( |
| input: fileSystem.file(inputPath), |
| outputPath: outputPath, |
| relativePath: relativePath, |
| ), |
| throwsToolExit( |
| message: |
| 'Avoid non-constant invocations of IconData or try to build' |
| ' again with --no-tree-shake-icons.', |
| ), |
| ); |
| |
| verify(mockProcessManager.run(_getConstFinderArgs(appDill.path))).called(1); |
| verifyNever(mockProcessManager.start(fontSubsetArgs)); |
| }); |
| |
| testWithoutContext('Non-zero font-subset exit code', () async { |
| final Environment environment = _createEnvironment(<String, String>{ |
| kIconTreeShakerFlag: 'true', |
| kBuildMode: 'release', |
| }); |
| final File appDill = environment.buildDir.childFile('app.dill') |
| ..createSync(recursive: true); |
| fileSystem.file(inputPath).createSync(recursive: true); |
| |
| final IconTreeShaker iconTreeShaker = IconTreeShaker( |
| environment, |
| fontManifestContent, |
| logger: logger, |
| processManager: mockProcessManager, |
| fileSystem: fileSystem, |
| artifacts: artifacts, |
| ); |
| |
| final CompleterIOSink stdinSink = CompleterIOSink(); |
| _addConstFinderInvocation(appDill.path, stdout: validConstFinderResult); |
| _resetFontSubsetInvocation(exitCode: -1, stdinSink: stdinSink); |
| |
| await expectLater( |
| () async => iconTreeShaker.subsetFont( |
| input: fileSystem.file(inputPath), |
| outputPath: outputPath, |
| relativePath: relativePath, |
| ), |
| throwsA(isA<IconTreeShakerException>()), |
| ); |
| |
| verify(mockProcessManager.run(_getConstFinderArgs(appDill.path))).called(1); |
| verify(mockProcessManager.start(fontSubsetArgs)).called(1); |
| }); |
| |
| testWithoutContext('font-subset throws on write to sdtin', () async { |
| final Environment environment = _createEnvironment(<String, String>{ |
| kIconTreeShakerFlag: 'true', |
| kBuildMode: 'release', |
| }); |
| final File appDill = environment.buildDir.childFile('app.dill') |
| ..createSync(recursive: true); |
| |
| final IconTreeShaker iconTreeShaker = IconTreeShaker( |
| environment, |
| fontManifestContent, |
| logger: logger, |
| processManager: mockProcessManager, |
| fileSystem: fileSystem, |
| artifacts: artifacts, |
| ); |
| |
| final CompleterIOSink stdinSink = CompleterIOSink(throwOnAdd: true); |
| _addConstFinderInvocation(appDill.path, stdout: validConstFinderResult); |
| _resetFontSubsetInvocation(exitCode: -1, stdinSink: stdinSink); |
| |
| await expectLater( |
| () async => iconTreeShaker.subsetFont( |
| input: fileSystem.file(inputPath), |
| outputPath: outputPath, |
| relativePath: relativePath, |
| ), |
| throwsA(isA<IconTreeShakerException>()), |
| ); |
| |
| verify(mockProcessManager.run(_getConstFinderArgs(appDill.path))).called(1); |
| verify(mockProcessManager.start(fontSubsetArgs)).called(1); |
| }); |
| |
| testWithoutContext('Invalid font manifest', () async { |
| final Environment environment = _createEnvironment(<String, String>{ |
| kIconTreeShakerFlag: 'true', |
| kBuildMode: 'release', |
| }); |
| final File appDill = environment.buildDir.childFile('app.dill') |
| ..createSync(recursive: true); |
| |
| fontManifestContent = DevFSStringContent(invalidFontManifestJson); |
| |
| final IconTreeShaker iconTreeShaker = IconTreeShaker( |
| environment, |
| fontManifestContent, |
| logger: logger, |
| processManager: mockProcessManager, |
| fileSystem: fileSystem, |
| artifacts: artifacts, |
| ); |
| |
| _addConstFinderInvocation(appDill.path, stdout: validConstFinderResult); |
| |
| await expectLater( |
| () async => iconTreeShaker.subsetFont( |
| input: fileSystem.file(inputPath), |
| outputPath: outputPath, |
| relativePath: relativePath, |
| ), |
| throwsA(isA<IconTreeShakerException>()), |
| ); |
| |
| verify(mockProcessManager.run(_getConstFinderArgs(appDill.path))).called(1); |
| verifyNever(mockProcessManager.start(fontSubsetArgs)); |
| }); |
| |
| testWithoutContext('ConstFinder non-zero exit', () async { |
| final Environment environment = _createEnvironment(<String, String>{ |
| kIconTreeShakerFlag: 'true', |
| kBuildMode: 'release', |
| }); |
| final File appDill = environment.buildDir.childFile('app.dill') |
| ..createSync(recursive: true); |
| |
| fontManifestContent = DevFSStringContent(invalidFontManifestJson); |
| |
| final IconTreeShaker iconTreeShaker = IconTreeShaker( |
| environment, |
| fontManifestContent, |
| logger: logger, |
| processManager: mockProcessManager, |
| fileSystem: fileSystem, |
| artifacts: artifacts, |
| ); |
| |
| _addConstFinderInvocation(appDill.path, exitCode: -1); |
| |
| await expectLater( |
| () async => iconTreeShaker.subsetFont( |
| input: fileSystem.file(inputPath), |
| outputPath: outputPath, |
| relativePath: relativePath, |
| ), |
| throwsA(isA<IconTreeShakerException>()), |
| ); |
| |
| verify(mockProcessManager.run(_getConstFinderArgs(appDill.path))).called(1); |
| verifyNever(mockProcessManager.start(fontSubsetArgs)); |
| }); |
| } |
| |
| const String validConstFinderResult = ''' |
| { |
| "constantInstances": [ |
| { |
| "codePoint": 59470, |
| "fontFamily": "MaterialIcons", |
| "fontPackage": null, |
| "matchTextDirection": false |
| } |
| ], |
| "nonConstantLocations": [] |
| } |
| '''; |
| |
| const String constFinderResultWithInvalid = ''' |
| { |
| "constantInstances": [ |
| { |
| "codePoint": 59470, |
| "fontFamily": "MaterialIcons", |
| "fontPackage": null, |
| "matchTextDirection": false |
| } |
| ], |
| "nonConstantLocations": [ |
| { |
| "file": "file:///Path/to/hello_world/lib/file.dart", |
| "line": 19, |
| "column": 11 |
| } |
| ] |
| } |
| '''; |
| |
| const String validFontManifestJson = ''' |
| [ |
| { |
| "family": "MaterialIcons", |
| "fonts": [ |
| { |
| "asset": "fonts/MaterialIcons-Regular.otf" |
| } |
| ] |
| }, |
| { |
| "family": "GalleryIcons", |
| "fonts": [ |
| { |
| "asset": "packages/flutter_gallery_assets/fonts/private/gallery_icons/GalleryIcons.ttf" |
| } |
| ] |
| }, |
| { |
| "family": "packages/cupertino_icons/CupertinoIcons", |
| "fonts": [ |
| { |
| "asset": "packages/cupertino_icons/assets/CupertinoIcons.ttf" |
| } |
| ] |
| } |
| ] |
| '''; |
| |
| const String invalidFontManifestJson = ''' |
| { |
| "famly": "MaterialIcons", |
| "fonts": [ |
| { |
| "asset": "fonts/MaterialIcons-Regular.otf" |
| } |
| ] |
| } |
| '''; |
| |
| class MockProcessManager extends Mock implements ProcessManager {} |
| class MockProcess extends Mock implements Process {} |