blob: 5c959bec47bfac5aacc8dbeab1af933010ff896d [file] [log] [blame]
// 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 'dart:async';
import 'dart:convert';
import 'dart:io';
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/terminal.dart';
import 'package:flutter_tools/src/build_system/build_system.dart';
import 'package:flutter_tools/src/build_system/targets/dart.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 'package:platform/platform.dart';
import '../../../src/common.dart';
import '../../../src/context.dart';
import '../../../src/mocks.dart' as mocks;
final Platform _kNoAnsiPlatform =
FakePlatform.fromPlatform(const LocalPlatform())
..stdoutSupportsAnsi = false;
void main() {
BufferLogger logger;
MemoryFileSystem fs;
MockProcessManager mockProcessManager;
MockProcess fontSubsetProcess;
MockArtifacts mockArtifacts;
DevFSStringContent fontManifestContent;
const String dartPath = '/flutter/dart';
const String constFinderPath = '/flutter/const_finder.snapshot.dart';
const String fontSubsetPath = '/flutter/font-subset';
const String inputPath = '/input/fonts/MaterialIcons-Regular.ttf';
const String outputPath = '/output/fonts/MaterialIcons-Regular.ttf';
const String relativePath = 'fonts/MaterialIcons-Regular.ttf';
List<String> getConstFinderArgs(String appDillPath) => <String>[
dartPath,
constFinderPath,
'--kernel-file', appDillPath,
'--class-library-uri', 'package:flutter/src/widgets/icon_data.dart',
'--class-name', 'IconData',
];
const List<String> fontSubsetArgs = <String>[
fontSubsetPath,
outputPath,
inputPath,
];
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 mocks.CompleterIOSink stdinSink,
}) {
assert(stdinSink != null);
stdinSink.writes.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();
mockArtifacts = MockArtifacts();
fs = MemoryFileSystem();
logger = BufferLogger(
terminal: AnsiTerminal(
stdio: mocks.MockStdio(),
platform: _kNoAnsiPlatform,
),
outputPreferences: OutputPreferences.test(showColor: false),
);
fs.file(constFinderPath).createSync(recursive: true);
fs.file(dartPath).createSync(recursive: true);
fs.file(fontSubsetPath).createSync(recursive: true);
when(mockArtifacts.getArtifactPath(Artifact.constFinder)).thenReturn(constFinderPath);
when(mockArtifacts.getArtifactPath(Artifact.fontSubset)).thenReturn(fontSubsetPath);
when(mockArtifacts.getArtifactPath(Artifact.engineDartBinary)).thenReturn(dartPath);
});
Environment _createEnvironment(Map<String, String> defines) {
return Environment.test(
fs.directory('/icon_test')..createSync(recursive: true),
defines: defines,
);
}
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: fs,
artifacts: mockArtifacts,
);
expect(
logger.errorText,
'Font subetting 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(
inputPath: 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: fs,
artifacts: mockArtifacts,
);
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: fs,
artifacts: mockArtifacts,
);
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: fs,
artifacts: mockArtifacts,
);
expect(
() => iconTreeShaker.subsetFont(
inputPath: inputPath,
outputPath: outputPath,
relativePath: relativePath,
),
throwsA(isA<IconTreeShakerException>()),
);
});
testWithoutContext('The happy path', () async {
final Environment environment = _createEnvironment(<String, String>{
kIconTreeShakerFlag: 'true',
kBuildMode: 'release',
});
final File appDill = environment.buildDir.childFile('app.dill')..createSync(recursive: true);
fs.file(inputPath).createSync(recursive: true);
final IconTreeShaker iconTreeShaker = IconTreeShaker(
environment,
fontManifestContent,
logger: logger,
processManager: mockProcessManager,
fileSystem: fs,
artifacts: mockArtifacts,
);
final mocks.CompleterIOSink stdinSink = mocks.CompleterIOSink();
_addConstFinderInvocation(appDill.path, stdout: validConstFinderResult);
_resetFontSubsetInvocation(stdinSink: stdinSink);
bool subsetted = await iconTreeShaker.subsetFont(
inputPath: inputPath,
outputPath: outputPath,
relativePath: relativePath,
);
expect(stdinSink.writes, <List<int>>[utf8.encode('59470\n')]);
_resetFontSubsetInvocation(stdinSink: stdinSink);
expect(subsetted, true);
subsetted = await iconTreeShaker.subsetFont(
inputPath: inputPath,
outputPath: outputPath,
relativePath: relativePath,
);
expect(subsetted, true);
expect(stdinSink.writes, <List<int>>[utf8.encode('59470\n')]);
verify(mockProcessManager.run(getConstFinderArgs(appDill.path))).called(1);
verify(mockProcessManager.start(fontSubsetArgs)).called(2);
});
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);
fs.file(inputPath).createSync(recursive: true);
final IconTreeShaker iconTreeShaker = IconTreeShaker(
environment,
fontManifestContent,
logger: logger,
processManager: mockProcessManager,
fileSystem: fs,
artifacts: mockArtifacts,
);
_addConstFinderInvocation(appDill.path, stdout: constFinderResultWithInvalid);
expect(
iconTreeShaker.subsetFont(
inputPath: 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);
fs.file(inputPath).createSync(recursive: true);
final IconTreeShaker iconTreeShaker = IconTreeShaker(
environment,
fontManifestContent,
logger: logger,
processManager: mockProcessManager,
fileSystem: fs,
artifacts: mockArtifacts,
);
final mocks.CompleterIOSink stdinSink = mocks.CompleterIOSink();
_addConstFinderInvocation(appDill.path, stdout: validConstFinderResult);
_resetFontSubsetInvocation(exitCode: -1, stdinSink: stdinSink);
expect(
iconTreeShaker.subsetFont(
inputPath: inputPath,
outputPath: outputPath,
relativePath: relativePath,
),
throwsA(isA<IconTreeShakerException>()),
);
verify(mockProcessManager.run(getConstFinderArgs(appDill.path))).called(1);
verifyNever(mockProcessManager.start(fontSubsetArgs));
});
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);
fs.file(inputPath).createSync(recursive: true);
final IconTreeShaker iconTreeShaker = IconTreeShaker(
environment,
fontManifestContent,
logger: logger,
processManager: mockProcessManager,
fileSystem: fs,
artifacts: mockArtifacts,
);
final mocks.CompleterIOSink stdinSink = mocks.CompleterIOSink(throwOnAdd: true);
_addConstFinderInvocation(appDill.path, stdout: validConstFinderResult);
_resetFontSubsetInvocation(exitCode: -1, stdinSink: stdinSink);
expect(
iconTreeShaker.subsetFont(
inputPath: inputPath,
outputPath: outputPath,
relativePath: relativePath,
),
throwsA(isA<IconTreeShakerException>()),
);
verify(mockProcessManager.run(getConstFinderArgs(appDill.path))).called(1);
verifyNever(mockProcessManager.start(fontSubsetArgs));
});
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);
fs.file(inputPath).createSync(recursive: true);
fontManifestContent = DevFSStringContent(invalidFontManifestJson);
final IconTreeShaker iconTreeShaker = IconTreeShaker(
environment,
fontManifestContent,
logger: logger,
processManager: mockProcessManager,
fileSystem: fs,
artifacts: mockArtifacts,
);
_addConstFinderInvocation(appDill.path, stdout: validConstFinderResult);
expect(
iconTreeShaker.subsetFont(
inputPath: 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);
fs.file(inputPath).createSync(recursive: true);
fontManifestContent = DevFSStringContent(invalidFontManifestJson);
final IconTreeShaker iconTreeShaker = IconTreeShaker(
environment,
fontManifestContent,
logger: logger,
processManager: mockProcessManager,
fileSystem: fs,
artifacts: mockArtifacts,
);
_addConstFinderInvocation(appDill.path, exitCode: -1);
expect(
iconTreeShaker.subsetFont(
inputPath: 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.ttf"
}
]
},
{
"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.ttf"
}
]
}
''';
class MockProcessManager extends Mock implements ProcessManager {}
class MockProcess extends Mock implements Process {}
class MockArtifacts extends Mock implements Artifacts {}