1 | // Copyright 2014 The Flutter Authors. All rights reserved. |
2 | // Use of this source code is governed by a BSD-style license that can be |
3 | // found in the LICENSE file. |
4 | |
5 | import 'package:file/memory.dart' ; |
6 | import 'package:flutter_tools/src/artifacts.dart'; |
7 | import 'package:flutter_tools/src/base/file_system.dart'; |
8 | import 'package:flutter_tools/src/base/logger.dart'; |
9 | import 'package:flutter_tools/src/build_info.dart'; |
10 | import 'package:flutter_tools/src/build_system/build_system.dart'; |
11 | import 'package:flutter_tools/src/build_system/targets/icon_tree_shaker.dart'; |
12 | import 'package:flutter_tools/src/devfs.dart'; |
13 | |
14 | import '../../../src/common.dart'; |
15 | import '../../../src/fake_process_manager.dart'; |
16 | import '../../../src/fakes.dart'; |
17 | |
18 | const List<int> _kTtfHeaderBytes = <int>[0, 1, 0, 0, 0, 15, 0, 128, 0, 3, 0, 112]; |
19 | |
20 | const String inputPath = '/input/fonts/MaterialIcons-Regular.otf' ; |
21 | const String outputPath = '/output/fonts/MaterialIcons-Regular.otf' ; |
22 | const String relativePath = 'fonts/MaterialIcons-Regular.otf' ; |
23 | |
24 | final RegExp whitespace = RegExp(r'\s+' ); |
25 | |
26 | void main() { |
27 | late BufferLogger logger; |
28 | late MemoryFileSystem fileSystem; |
29 | late FakeProcessManager processManager; |
30 | late Artifacts artifacts; |
31 | late DevFSStringContent fontManifestContent; |
32 | |
33 | late String dartPath; |
34 | late String constFinderPath; |
35 | late String fontSubsetPath; |
36 | late List<String> fontSubsetArgs; |
37 | |
38 | List<String> getConstFinderArgs(String appDillPath) => <String>[ |
39 | dartPath, |
40 | constFinderPath, |
41 | '--kernel-file' , |
42 | appDillPath, |
43 | '--class-library-uri' , |
44 | 'package:flutter/src/widgets/icon_data.dart' , |
45 | '--class-name' , |
46 | 'IconData' , |
47 | '--annotation-class-name' , |
48 | '_StaticIconProvider' , |
49 | '--annotation-class-library-uri' , |
50 | 'package:flutter/src/widgets/icon_data.dart' , |
51 | ]; |
52 | |
53 | void addConstFinderInvocation( |
54 | String appDillPath, { |
55 | int exitCode = 0, |
56 | String stdout = '' , |
57 | String stderr = '' , |
58 | }) { |
59 | processManager.addCommand( |
60 | FakeCommand( |
61 | command: getConstFinderArgs(appDillPath), |
62 | exitCode: exitCode, |
63 | stdout: stdout, |
64 | stderr: stderr, |
65 | ), |
66 | ); |
67 | } |
68 | |
69 | void resetFontSubsetInvocation({ |
70 | int exitCode = 0, |
71 | String stdout = '' , |
72 | String stderr = '' , |
73 | required CompleterIOSink stdinSink, |
74 | }) { |
75 | stdinSink.clear(); |
76 | processManager.addCommand( |
77 | FakeCommand( |
78 | command: fontSubsetArgs, |
79 | exitCode: exitCode, |
80 | stdout: stdout, |
81 | stderr: stderr, |
82 | stdin: stdinSink, |
83 | ), |
84 | ); |
85 | } |
86 | |
87 | setUp(() { |
88 | processManager = FakeProcessManager.empty(); |
89 | fontManifestContent = DevFSStringContent(validFontManifestJson); |
90 | artifacts = Artifacts.test(); |
91 | fileSystem = MemoryFileSystem.test(); |
92 | logger = BufferLogger.test(); |
93 | dartPath = artifacts.getArtifactPath(Artifact.engineDartBinary); |
94 | constFinderPath = artifacts.getArtifactPath(Artifact.constFinder); |
95 | fontSubsetPath = artifacts.getArtifactPath(Artifact.fontSubset); |
96 | |
97 | fontSubsetArgs = <String>[fontSubsetPath, outputPath, inputPath]; |
98 | |
99 | fileSystem.file(constFinderPath).createSync(recursive: true); |
100 | fileSystem.file(dartPath).createSync(recursive: true); |
101 | fileSystem.file(fontSubsetPath).createSync(recursive: true); |
102 | fileSystem.file(inputPath) |
103 | ..createSync(recursive: true) |
104 | ..writeAsBytesSync(_kTtfHeaderBytes); |
105 | }); |
106 | |
107 | Environment createEnvironment(Map<String, String> defines) { |
108 | return Environment.test( |
109 | fileSystem.directory('/icon_test' )..createSync(recursive: true), |
110 | defines: defines, |
111 | artifacts: artifacts, |
112 | processManager: FakeProcessManager.any(), |
113 | fileSystem: fileSystem, |
114 | logger: BufferLogger.test(), |
115 | ); |
116 | } |
117 | |
118 | testWithoutContext('Prints error in debug mode environment' , () async { |
119 | final Environment environment = createEnvironment(<String, String>{ |
120 | kIconTreeShakerFlag: 'true' , |
121 | kBuildMode: 'debug' , |
122 | }); |
123 | |
124 | final IconTreeShaker iconTreeShaker = IconTreeShaker( |
125 | environment, |
126 | fontManifestContent, |
127 | logger: logger, |
128 | processManager: processManager, |
129 | fileSystem: fileSystem, |
130 | artifacts: artifacts, |
131 | targetPlatform: TargetPlatform.android, |
132 | ); |
133 | |
134 | expect( |
135 | logger.errorText, |
136 | 'Font subsetting is not supported in debug mode. The --tree-shake-icons' |
137 | ' flag will be ignored.\n' , |
138 | ); |
139 | expect(iconTreeShaker.enabled, false); |
140 | |
141 | final bool subsets = await iconTreeShaker.subsetFont( |
142 | input: fileSystem.file(inputPath), |
143 | outputPath: outputPath, |
144 | relativePath: relativePath, |
145 | ); |
146 | expect(subsets, false); |
147 | expect(processManager, hasNoRemainingExpectations); |
148 | }); |
149 | |
150 | testWithoutContext('Does not get enabled without font manifest' , () { |
151 | final Environment environment = createEnvironment(<String, String>{ |
152 | kIconTreeShakerFlag: 'true' , |
153 | kBuildMode: 'release' , |
154 | }); |
155 | |
156 | final IconTreeShaker iconTreeShaker = IconTreeShaker( |
157 | environment, |
158 | null, |
159 | logger: logger, |
160 | processManager: processManager, |
161 | fileSystem: fileSystem, |
162 | artifacts: artifacts, |
163 | targetPlatform: TargetPlatform.android, |
164 | ); |
165 | |
166 | expect(logger.errorText, isEmpty); |
167 | expect(iconTreeShaker.enabled, false); |
168 | expect(processManager, hasNoRemainingExpectations); |
169 | }); |
170 | |
171 | testWithoutContext('Gets enabled' , () { |
172 | final Environment environment = createEnvironment(<String, String>{ |
173 | kIconTreeShakerFlag: 'true' , |
174 | kBuildMode: 'release' , |
175 | }); |
176 | |
177 | final IconTreeShaker iconTreeShaker = IconTreeShaker( |
178 | environment, |
179 | fontManifestContent, |
180 | logger: logger, |
181 | processManager: processManager, |
182 | fileSystem: fileSystem, |
183 | artifacts: artifacts, |
184 | targetPlatform: TargetPlatform.android, |
185 | ); |
186 | |
187 | expect(logger.errorText, isEmpty); |
188 | expect(iconTreeShaker.enabled, true); |
189 | expect(processManager, hasNoRemainingExpectations); |
190 | }); |
191 | |
192 | test('No app.dill throws exception' , () async { |
193 | final Environment environment = createEnvironment(<String, String>{ |
194 | kIconTreeShakerFlag: 'true' , |
195 | kBuildMode: 'release' , |
196 | }); |
197 | |
198 | final IconTreeShaker iconTreeShaker = IconTreeShaker( |
199 | environment, |
200 | fontManifestContent, |
201 | logger: logger, |
202 | processManager: processManager, |
203 | fileSystem: fileSystem, |
204 | artifacts: artifacts, |
205 | targetPlatform: TargetPlatform.android, |
206 | ); |
207 | |
208 | expect( |
209 | () async => iconTreeShaker.subsetFont( |
210 | input: fileSystem.file(inputPath), |
211 | outputPath: outputPath, |
212 | relativePath: relativePath, |
213 | ), |
214 | throwsA(isA<IconTreeShakerException>()), |
215 | ); |
216 | expect(processManager, hasNoRemainingExpectations); |
217 | }); |
218 | |
219 | testWithoutContext('Can subset a font' , () async { |
220 | final Environment environment = createEnvironment(<String, String>{ |
221 | kIconTreeShakerFlag: 'true' , |
222 | kBuildMode: 'release' , |
223 | }); |
224 | final File appDill = environment.buildDir.childFile('app.dill' )..createSync(recursive: true); |
225 | |
226 | final IconTreeShaker iconTreeShaker = IconTreeShaker( |
227 | environment, |
228 | fontManifestContent, |
229 | logger: logger, |
230 | processManager: processManager, |
231 | fileSystem: fileSystem, |
232 | artifacts: artifacts, |
233 | targetPlatform: TargetPlatform.android, |
234 | ); |
235 | final CompleterIOSink stdinSink = CompleterIOSink(); |
236 | addConstFinderInvocation(appDill.path, stdout: validConstFinderResult); |
237 | resetFontSubsetInvocation(stdinSink: stdinSink); |
238 | // Font starts out 2500 bytes long |
239 | final File inputFont = fileSystem.file(inputPath)..writeAsBytesSync(List<int>.filled(2500, 0)); |
240 | // after subsetting, font is 1200 bytes long |
241 | fileSystem.file(outputPath) |
242 | ..createSync(recursive: true) |
243 | ..writeAsBytesSync(List<int>.filled(1200, 0)); |
244 | bool subsetted = await iconTreeShaker.subsetFont( |
245 | input: inputFont, |
246 | outputPath: outputPath, |
247 | relativePath: relativePath, |
248 | ); |
249 | expect(stdinSink.getAndClear(), '59470\n' ); |
250 | resetFontSubsetInvocation(stdinSink: stdinSink); |
251 | |
252 | expect(subsetted, true); |
253 | subsetted = await iconTreeShaker.subsetFont( |
254 | input: fileSystem.file(inputPath), |
255 | outputPath: outputPath, |
256 | relativePath: relativePath, |
257 | ); |
258 | expect(subsetted, true); |
259 | expect(stdinSink.getAndClear(), '59470\n' ); |
260 | expect(processManager, hasNoRemainingExpectations); |
261 | expect( |
262 | logger.statusText, |
263 | contains( |
264 | 'Font asset "MaterialIcons-Regular.otf" was tree-shaken, reducing it from 2500 to 1200 bytes (52.0% reduction). Tree-shaking can be disabled by providing the --no-tree-shake-icons flag when building your app.' , |
265 | ), |
266 | ); |
267 | }); |
268 | |
269 | testWithoutContext('Does not subset a non-supported font' , () async { |
270 | final Environment environment = createEnvironment(<String, String>{ |
271 | kIconTreeShakerFlag: 'true' , |
272 | kBuildMode: 'release' , |
273 | }); |
274 | final File appDill = environment.buildDir.childFile('app.dill' )..createSync(recursive: true); |
275 | |
276 | final IconTreeShaker iconTreeShaker = IconTreeShaker( |
277 | environment, |
278 | fontManifestContent, |
279 | logger: logger, |
280 | processManager: processManager, |
281 | fileSystem: fileSystem, |
282 | artifacts: artifacts, |
283 | targetPlatform: TargetPlatform.android, |
284 | ); |
285 | |
286 | final CompleterIOSink stdinSink = CompleterIOSink(); |
287 | addConstFinderInvocation(appDill.path, stdout: validConstFinderResult); |
288 | resetFontSubsetInvocation(stdinSink: stdinSink); |
289 | |
290 | final File notAFont = |
291 | fileSystem.file('input/foo/bar.txt' ) |
292 | ..createSync(recursive: true) |
293 | ..writeAsStringSync('I could not think of a better string' ); |
294 | final bool subsetted = await iconTreeShaker.subsetFont( |
295 | input: notAFont, |
296 | outputPath: outputPath, |
297 | relativePath: relativePath, |
298 | ); |
299 | expect(subsetted, false); |
300 | }); |
301 | |
302 | testWithoutContext('Does not subset an invalid ttf font' , () async { |
303 | final Environment environment = createEnvironment(<String, String>{ |
304 | kIconTreeShakerFlag: 'true' , |
305 | kBuildMode: 'release' , |
306 | }); |
307 | final File appDill = environment.buildDir.childFile('app.dill' )..createSync(recursive: true); |
308 | |
309 | final IconTreeShaker iconTreeShaker = IconTreeShaker( |
310 | environment, |
311 | fontManifestContent, |
312 | logger: logger, |
313 | processManager: processManager, |
314 | fileSystem: fileSystem, |
315 | artifacts: artifacts, |
316 | targetPlatform: TargetPlatform.android, |
317 | ); |
318 | |
319 | final CompleterIOSink stdinSink = CompleterIOSink(); |
320 | addConstFinderInvocation(appDill.path, stdout: validConstFinderResult); |
321 | resetFontSubsetInvocation(stdinSink: stdinSink); |
322 | |
323 | final File notAFont = fileSystem.file(inputPath)..writeAsBytesSync(<int>[0, 1, 2]); |
324 | final bool subsetted = await iconTreeShaker.subsetFont( |
325 | input: notAFont, |
326 | outputPath: outputPath, |
327 | relativePath: relativePath, |
328 | ); |
329 | |
330 | expect(subsetted, false); |
331 | }); |
332 | |
333 | for (final TargetPlatform platform in <TargetPlatform>[ |
334 | TargetPlatform.android_arm, |
335 | TargetPlatform.web_javascript, |
336 | ]) { |
337 | testWithoutContext('Non-constant instances $platform' , () async { |
338 | final Environment environment = createEnvironment(<String, String>{ |
339 | kIconTreeShakerFlag: 'true' , |
340 | kBuildMode: 'release' , |
341 | }); |
342 | final File appDill = environment.buildDir.childFile('app.dill' )..createSync(recursive: true); |
343 | |
344 | final IconTreeShaker iconTreeShaker = IconTreeShaker( |
345 | environment, |
346 | fontManifestContent, |
347 | logger: logger, |
348 | processManager: processManager, |
349 | fileSystem: fileSystem, |
350 | artifacts: artifacts, |
351 | targetPlatform: platform, |
352 | ); |
353 | |
354 | addConstFinderInvocation(appDill.path, stdout: constFinderResultWithInvalid); |
355 | |
356 | await expectLater( |
357 | () => iconTreeShaker.subsetFont( |
358 | input: fileSystem.file(inputPath), |
359 | outputPath: outputPath, |
360 | relativePath: relativePath, |
361 | ), |
362 | throwsToolExit( |
363 | message: |
364 | 'Avoid non-constant invocations of IconData or try to build' |
365 | ' again with --no-tree-shake-icons.' , |
366 | ), |
367 | ); |
368 | expect(processManager, hasNoRemainingExpectations); |
369 | }); |
370 | } |
371 | |
372 | testWithoutContext('Does not add 0x32 for non-web builds' , () async { |
373 | final Environment environment = createEnvironment(<String, String>{ |
374 | kIconTreeShakerFlag: 'true' , |
375 | kBuildMode: 'release' , |
376 | }); |
377 | final File appDill = environment.buildDir.childFile('app.dill' )..createSync(recursive: true); |
378 | |
379 | final IconTreeShaker iconTreeShaker = IconTreeShaker( |
380 | environment, |
381 | fontManifestContent, |
382 | logger: logger, |
383 | processManager: processManager, |
384 | fileSystem: fileSystem, |
385 | artifacts: artifacts, |
386 | targetPlatform: TargetPlatform.android_arm64, |
387 | ); |
388 | |
389 | addConstFinderInvocation( |
390 | appDill.path, |
391 | // Does not contain space char |
392 | stdout: validConstFinderResult, |
393 | ); |
394 | final CompleterIOSink stdinSink = CompleterIOSink(); |
395 | resetFontSubsetInvocation(stdinSink: stdinSink); |
396 | expect(processManager.hasRemainingExpectations, isTrue); |
397 | final File inputFont = fileSystem.file(inputPath)..writeAsBytesSync(List<int>.filled(2500, 0)); |
398 | fileSystem.file(outputPath) |
399 | ..createSync(recursive: true) |
400 | ..writeAsBytesSync(List<int>.filled(1200, 0)); |
401 | |
402 | final bool result = await iconTreeShaker.subsetFont( |
403 | input: inputFont, |
404 | outputPath: outputPath, |
405 | relativePath: relativePath, |
406 | ); |
407 | |
408 | expect(result, isTrue); |
409 | final List<String> codePoints = stdinSink.getAndClear().trim().split(whitespace); |
410 | expect(codePoints, isNot(contains('optional:32' ))); |
411 | |
412 | expect(processManager, hasNoRemainingExpectations); |
413 | }); |
414 | |
415 | testWithoutContext('Ensures 0x32 is included for web builds' , () async { |
416 | final Environment environment = createEnvironment(<String, String>{ |
417 | kIconTreeShakerFlag: 'true' , |
418 | kBuildMode: 'release' , |
419 | }); |
420 | final File appDill = environment.buildDir.childFile('app.dill' )..createSync(recursive: true); |
421 | |
422 | final IconTreeShaker iconTreeShaker = IconTreeShaker( |
423 | environment, |
424 | fontManifestContent, |
425 | logger: logger, |
426 | processManager: processManager, |
427 | fileSystem: fileSystem, |
428 | artifacts: artifacts, |
429 | targetPlatform: TargetPlatform.web_javascript, |
430 | ); |
431 | |
432 | addConstFinderInvocation( |
433 | appDill.path, |
434 | // Does not contain space char |
435 | stdout: validConstFinderResult, |
436 | ); |
437 | final CompleterIOSink stdinSink = CompleterIOSink(); |
438 | resetFontSubsetInvocation(stdinSink: stdinSink); |
439 | expect(processManager.hasRemainingExpectations, isTrue); |
440 | final File inputFont = fileSystem.file(inputPath)..writeAsBytesSync(List<int>.filled(2500, 0)); |
441 | fileSystem.file(outputPath) |
442 | ..createSync(recursive: true) |
443 | ..writeAsBytesSync(List<int>.filled(1200, 0)); |
444 | |
445 | final bool result = await iconTreeShaker.subsetFont( |
446 | input: inputFont, |
447 | outputPath: outputPath, |
448 | relativePath: relativePath, |
449 | ); |
450 | |
451 | expect(result, isTrue); |
452 | final List<String> codePoints = stdinSink.getAndClear().trim().split(whitespace); |
453 | expect(codePoints, containsAllInOrder(const <String>['59470' , 'optional:32' ])); |
454 | |
455 | expect(processManager, hasNoRemainingExpectations); |
456 | }); |
457 | |
458 | testWithoutContext('Non-zero font-subset exit code' , () async { |
459 | final Environment environment = createEnvironment(<String, String>{ |
460 | kIconTreeShakerFlag: 'true' , |
461 | kBuildMode: 'release' , |
462 | }); |
463 | final File appDill = environment.buildDir.childFile('app.dill' )..createSync(recursive: true); |
464 | fileSystem.file(inputPath).createSync(recursive: true); |
465 | |
466 | final IconTreeShaker iconTreeShaker = IconTreeShaker( |
467 | environment, |
468 | fontManifestContent, |
469 | logger: logger, |
470 | processManager: processManager, |
471 | fileSystem: fileSystem, |
472 | artifacts: artifacts, |
473 | targetPlatform: TargetPlatform.android, |
474 | ); |
475 | |
476 | final CompleterIOSink stdinSink = CompleterIOSink(); |
477 | addConstFinderInvocation(appDill.path, stdout: validConstFinderResult); |
478 | resetFontSubsetInvocation(exitCode: -1, stdinSink: stdinSink); |
479 | |
480 | await expectLater( |
481 | () => iconTreeShaker.subsetFont( |
482 | input: fileSystem.file(inputPath), |
483 | outputPath: outputPath, |
484 | relativePath: relativePath, |
485 | ), |
486 | throwsA(isA<IconTreeShakerException>()), |
487 | ); |
488 | expect(processManager, hasNoRemainingExpectations); |
489 | }); |
490 | |
491 | testWithoutContext('font-subset throws on write to sdtin' , () async { |
492 | final Environment environment = createEnvironment(<String, String>{ |
493 | kIconTreeShakerFlag: 'true' , |
494 | kBuildMode: 'release' , |
495 | }); |
496 | final File appDill = environment.buildDir.childFile('app.dill' )..createSync(recursive: true); |
497 | |
498 | final IconTreeShaker iconTreeShaker = IconTreeShaker( |
499 | environment, |
500 | fontManifestContent, |
501 | logger: logger, |
502 | processManager: processManager, |
503 | fileSystem: fileSystem, |
504 | artifacts: artifacts, |
505 | targetPlatform: TargetPlatform.android, |
506 | ); |
507 | |
508 | final CompleterIOSink stdinSink = CompleterIOSink(throwOnAdd: true); |
509 | addConstFinderInvocation(appDill.path, stdout: validConstFinderResult); |
510 | resetFontSubsetInvocation(exitCode: -1, stdinSink: stdinSink); |
511 | |
512 | await expectLater( |
513 | () => iconTreeShaker.subsetFont( |
514 | input: fileSystem.file(inputPath), |
515 | outputPath: outputPath, |
516 | relativePath: relativePath, |
517 | ), |
518 | throwsA(isA<IconTreeShakerException>()), |
519 | ); |
520 | expect(processManager, hasNoRemainingExpectations); |
521 | }); |
522 | |
523 | testWithoutContext('Invalid font manifest' , () async { |
524 | final Environment environment = createEnvironment(<String, String>{ |
525 | kIconTreeShakerFlag: 'true' , |
526 | kBuildMode: 'release' , |
527 | }); |
528 | final File appDill = environment.buildDir.childFile('app.dill' )..createSync(recursive: true); |
529 | |
530 | fontManifestContent = DevFSStringContent(invalidFontManifestJson); |
531 | |
532 | final IconTreeShaker iconTreeShaker = IconTreeShaker( |
533 | environment, |
534 | fontManifestContent, |
535 | logger: logger, |
536 | processManager: processManager, |
537 | fileSystem: fileSystem, |
538 | artifacts: artifacts, |
539 | targetPlatform: TargetPlatform.android, |
540 | ); |
541 | |
542 | addConstFinderInvocation(appDill.path, stdout: validConstFinderResult); |
543 | |
544 | await expectLater( |
545 | () => iconTreeShaker.subsetFont( |
546 | input: fileSystem.file(inputPath), |
547 | outputPath: outputPath, |
548 | relativePath: relativePath, |
549 | ), |
550 | throwsA(isA<IconTreeShakerException>()), |
551 | ); |
552 | expect(processManager, hasNoRemainingExpectations); |
553 | }); |
554 | |
555 | testWithoutContext('Allow system font fallback when fontFamily is null' , () async { |
556 | final Environment environment = createEnvironment(<String, String>{ |
557 | kIconTreeShakerFlag: 'true' , |
558 | kBuildMode: 'release' , |
559 | }); |
560 | final File appDill = environment.buildDir.childFile('app.dill' )..createSync(recursive: true); |
561 | |
562 | // Valid manifest, just not using it. |
563 | fontManifestContent = DevFSStringContent(validFontManifestJson); |
564 | |
565 | final IconTreeShaker iconTreeShaker = IconTreeShaker( |
566 | environment, |
567 | fontManifestContent, |
568 | logger: logger, |
569 | processManager: processManager, |
570 | fileSystem: fileSystem, |
571 | artifacts: artifacts, |
572 | targetPlatform: TargetPlatform.android, |
573 | ); |
574 | |
575 | addConstFinderInvocation(appDill.path, stdout: emptyConstFinderResult); |
576 | // Does not throw |
577 | await iconTreeShaker.subsetFont( |
578 | input: fileSystem.file(inputPath), |
579 | outputPath: outputPath, |
580 | relativePath: relativePath, |
581 | ); |
582 | |
583 | expect( |
584 | logger.traceText, |
585 | contains( |
586 | 'Expected to find fontFamily for constant IconData with codepoint: ' |
587 | '59470, but found fontFamily: null. This usually means ' |
588 | 'you are relying on the system font. Alternatively, font families in ' |
589 | 'an IconData class can be provided in the assets section of your ' |
590 | 'pubspec.yaml, or you are missing "uses-material-design: true".\n' , |
591 | ), |
592 | ); |
593 | expect(processManager, hasNoRemainingExpectations); |
594 | }); |
595 | |
596 | testWithoutContext( |
597 | 'Allow system font fallback when fontFamily is null and manifest is empty' , |
598 | () async { |
599 | final Environment environment = createEnvironment(<String, String>{ |
600 | kIconTreeShakerFlag: 'true' , |
601 | kBuildMode: 'release' , |
602 | }); |
603 | final File appDill = environment.buildDir.childFile('app.dill' )..createSync(recursive: true); |
604 | |
605 | // Nothing in font manifest |
606 | fontManifestContent = DevFSStringContent(emptyFontManifestJson); |
607 | |
608 | final IconTreeShaker iconTreeShaker = IconTreeShaker( |
609 | environment, |
610 | fontManifestContent, |
611 | logger: logger, |
612 | processManager: processManager, |
613 | fileSystem: fileSystem, |
614 | artifacts: artifacts, |
615 | targetPlatform: TargetPlatform.android, |
616 | ); |
617 | |
618 | addConstFinderInvocation(appDill.path, stdout: emptyConstFinderResult); |
619 | // Does not throw |
620 | await iconTreeShaker.subsetFont( |
621 | input: fileSystem.file(inputPath), |
622 | outputPath: outputPath, |
623 | relativePath: relativePath, |
624 | ); |
625 | |
626 | expect( |
627 | logger.traceText, |
628 | contains( |
629 | 'Expected to find fontFamily for constant IconData with codepoint: ' |
630 | '59470, but found fontFamily: null. This usually means ' |
631 | 'you are relying on the system font. Alternatively, font families in ' |
632 | 'an IconData class can be provided in the assets section of your ' |
633 | 'pubspec.yaml, or you are missing "uses-material-design: true".\n' , |
634 | ), |
635 | ); |
636 | expect(processManager, hasNoRemainingExpectations); |
637 | }, |
638 | ); |
639 | |
640 | testWithoutContext('ConstFinder non-zero exit' , () async { |
641 | final Environment environment = createEnvironment(<String, String>{ |
642 | kIconTreeShakerFlag: 'true' , |
643 | kBuildMode: 'release' , |
644 | }); |
645 | final File appDill = environment.buildDir.childFile('app.dill' )..createSync(recursive: true); |
646 | |
647 | fontManifestContent = DevFSStringContent(invalidFontManifestJson); |
648 | |
649 | final IconTreeShaker iconTreeShaker = IconTreeShaker( |
650 | environment, |
651 | fontManifestContent, |
652 | logger: logger, |
653 | processManager: processManager, |
654 | fileSystem: fileSystem, |
655 | artifacts: artifacts, |
656 | targetPlatform: TargetPlatform.android, |
657 | ); |
658 | |
659 | addConstFinderInvocation(appDill.path, exitCode: -1); |
660 | |
661 | await expectLater( |
662 | () async => iconTreeShaker.subsetFont( |
663 | input: fileSystem.file(inputPath), |
664 | outputPath: outputPath, |
665 | relativePath: relativePath, |
666 | ), |
667 | throwsA(isA<IconTreeShakerException>()), |
668 | ); |
669 | expect(processManager, hasNoRemainingExpectations); |
670 | }); |
671 | } |
672 | |
673 | const String validConstFinderResult = ''' |
674 | { |
675 | "constantInstances": [ |
676 | { |
677 | "codePoint": 59470, |
678 | "fontFamily": "MaterialIcons", |
679 | "fontPackage": null, |
680 | "matchTextDirection": false |
681 | } |
682 | ], |
683 | "nonConstantLocations": [] |
684 | } |
685 | ''' ; |
686 | |
687 | const String emptyConstFinderResult = ''' |
688 | { |
689 | "constantInstances": [ |
690 | { |
691 | "codePoint": 59470, |
692 | "fontFamily": null, |
693 | "fontPackage": null, |
694 | "matchTextDirection": false |
695 | } |
696 | ], |
697 | "nonConstantLocations": [] |
698 | } |
699 | ''' ; |
700 | |
701 | const String constFinderResultWithInvalid = ''' |
702 | { |
703 | "constantInstances": [ |
704 | { |
705 | "codePoint": 59470, |
706 | "fontFamily": "MaterialIcons", |
707 | "fontPackage": null, |
708 | "matchTextDirection": false |
709 | } |
710 | ], |
711 | "nonConstantLocations": [ |
712 | { |
713 | "file": "file:///Path/to/hello_world/lib/file.dart", |
714 | "line": 19, |
715 | "column": 11 |
716 | } |
717 | ] |
718 | } |
719 | ''' ; |
720 | |
721 | const String validFontManifestJson = ''' |
722 | [ |
723 | { |
724 | "family": "MaterialIcons", |
725 | "fonts": [ |
726 | { |
727 | "asset": "fonts/MaterialIcons-Regular.otf" |
728 | } |
729 | ] |
730 | }, |
731 | { |
732 | "family": "GalleryIcons", |
733 | "fonts": [ |
734 | { |
735 | "asset": "packages/flutter_gallery_assets/fonts/private/gallery_icons/GalleryIcons.ttf" |
736 | } |
737 | ] |
738 | }, |
739 | { |
740 | "family": "packages/cupertino_icons/CupertinoIcons", |
741 | "fonts": [ |
742 | { |
743 | "asset": "packages/cupertino_icons/assets/CupertinoIcons.ttf" |
744 | } |
745 | ] |
746 | } |
747 | ] |
748 | ''' ; |
749 | |
750 | const String invalidFontManifestJson = ''' |
751 | { |
752 | "famly": "MaterialIcons", |
753 | "fonts": [ |
754 | { |
755 | "asset": "fonts/MaterialIcons-Regular.otf" |
756 | } |
757 | ] |
758 | } |
759 | ''' ; |
760 | |
761 | const String emptyFontManifestJson = '[]' ; |
762 | |