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