flutter/dev/tools/gen_defaults/test/gen_defaults_test.dart
Pierre-Louis 66cda5917d
Improve defaults generation with logging, stats, and token validation (#128244)
## Description

This improves defaults generation with logging, stats, and token validation. 

This PR includes these changes:
* introduce `TokenLogger`, with a verbose mode
  * prints versions and tokens usage to the console
  * outputs `generated/used_tokens.csv`, a list of all used tokens, for use by Google
* find token files in `data` automatically
* hide tokens `Map`
  * tokens can be obtained using existing resolvers (e.g. `color`, `shape`), or directly through `getToken`.
  * tokens can be checked for existence with `tokenAvailable`
* remove version from template, since the tokens are aggregated and multiple versions are possible (as is the case currently), it does not make sense to attribute a single version
* improve documentation

## Related Issues
 - Fixes https://github.com/flutter/flutter/issues/122602

## Tests
 - Added tests for `TokenLogger`
 - Regenerated tokens, no-op except version removal

## Future work
A future PR should replace or remove the following invalid tokens usages

<img width="578" alt="image" src="https://github.com/flutter/flutter/assets/6655696/b6f9e5a7-523f-4f72-94f9-1b0bf4cc9f00">
2023-06-09 11:28:18 +00:00

372 lines
12 KiB
Dart

// 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:io';
import 'package:gen_defaults/template.dart';
import 'package:gen_defaults/token_logger.dart';
import 'package:path/path.dart' as path;
import 'package:test/test.dart';
void main() {
final TokenLogger logger = tokenLogger;
logger.init(allTokens: <String, dynamic>{}, versionMap: <String, List<String>>{});
test('Templates will append to the end of a file', () {
final Directory tempDir = Directory.systemTemp.createTempSync('gen_defaults');
try {
// Create a temporary file with some content.
final File tempFile = File(path.join(tempDir.path, 'test_template.txt'));
tempFile.createSync();
tempFile.writeAsStringSync('''
// This is a file with stuff in it.
// This part shouldn't be changed by
// the template.
''');
// Have a test template append new parameterized content to the end of
// the file.
final Map<String, dynamic> tokens = <String, dynamic>{'version': '0.0', 'foo': 'Foobar', 'bar': 'Barfoo'};
TestTemplate('Test', tempFile.path, tokens).updateFile();
expect(tempFile.readAsStringSync(), '''
// This is a file with stuff in it.
// This part shouldn't be changed by
// the template.
// BEGIN GENERATED TOKEN PROPERTIES - Test
// Do not edit by hand. The code between the "BEGIN GENERATED" and
// "END GENERATED" comments are generated from data in the Material
// Design token database by the script:
// dev/tools/gen_defaults/bin/gen_defaults.dart.
static final String tokenFoo = 'Foobar';
static final String tokenBar = 'Barfoo';
// END GENERATED TOKEN PROPERTIES - Test
''');
} finally {
tempDir.deleteSync(recursive: true);
}
});
test('Templates will update over previously generated code at the end of a file', () {
final Directory tempDir = Directory.systemTemp.createTempSync('gen_defaults');
try {
// Create a temporary file with some content.
final File tempFile = File(path.join(tempDir.path, 'test_template.txt'));
tempFile.createSync();
tempFile.writeAsStringSync('''
// This is a file with stuff in it.
// This part shouldn't be changed by
// the template.
// BEGIN GENERATED TOKEN PROPERTIES - Test
// Do not edit by hand. The code between the "BEGIN GENERATED" and
// "END GENERATED" comments are generated from data in the Material
// Design token database by the script:
// dev/tools/gen_defaults/bin/gen_defaults.dart.
static final String tokenFoo = 'Foobar';
static final String tokenBar = 'Barfoo';
// END GENERATED TOKEN PROPERTIES - Test
''');
// Have a test template append new parameterized content to the end of
// the file.
final Map<String, dynamic> tokens = <String, dynamic>{'version': '0.0', 'foo': 'foo', 'bar': 'bar'};
TestTemplate('Test', tempFile.path, tokens).updateFile();
expect(tempFile.readAsStringSync(), '''
// This is a file with stuff in it.
// This part shouldn't be changed by
// the template.
// BEGIN GENERATED TOKEN PROPERTIES - Test
// Do not edit by hand. The code between the "BEGIN GENERATED" and
// "END GENERATED" comments are generated from data in the Material
// Design token database by the script:
// dev/tools/gen_defaults/bin/gen_defaults.dart.
static final String tokenFoo = 'foo';
static final String tokenBar = 'bar';
// END GENERATED TOKEN PROPERTIES - Test
''');
} finally {
tempDir.deleteSync(recursive: true);
}
});
test('Multiple templates can modify different code blocks in the same file', () {
final Directory tempDir = Directory.systemTemp.createTempSync('gen_defaults');
try {
// Create a temporary file with some content.
final File tempFile = File(path.join(tempDir.path, 'test_template.txt'));
tempFile.createSync();
tempFile.writeAsStringSync('''
// This is a file with stuff in it.
// This part shouldn't be changed by
// the template.
''');
// Update file with a template for 'Block 1'
{
final Map<String, dynamic> tokens = <String, dynamic>{'version': '0.0', 'foo': 'foo', 'bar': 'bar'};
TestTemplate('Block 1', tempFile.path, tokens).updateFile();
}
expect(tempFile.readAsStringSync(), '''
// This is a file with stuff in it.
// This part shouldn't be changed by
// the template.
// BEGIN GENERATED TOKEN PROPERTIES - Block 1
// Do not edit by hand. The code between the "BEGIN GENERATED" and
// "END GENERATED" comments are generated from data in the Material
// Design token database by the script:
// dev/tools/gen_defaults/bin/gen_defaults.dart.
static final String tokenFoo = 'foo';
static final String tokenBar = 'bar';
// END GENERATED TOKEN PROPERTIES - Block 1
''');
// Update file with a template for 'Block 2', which should append but not
// disturb the code in 'Block 1'.
{
final Map<String, dynamic> tokens = <String, dynamic>{'version': '0.0', 'foo': 'bar', 'bar': 'foo'};
TestTemplate('Block 2', tempFile.path, tokens).updateFile();
}
expect(tempFile.readAsStringSync(), '''
// This is a file with stuff in it.
// This part shouldn't be changed by
// the template.
// BEGIN GENERATED TOKEN PROPERTIES - Block 1
// Do not edit by hand. The code between the "BEGIN GENERATED" and
// "END GENERATED" comments are generated from data in the Material
// Design token database by the script:
// dev/tools/gen_defaults/bin/gen_defaults.dart.
static final String tokenFoo = 'foo';
static final String tokenBar = 'bar';
// END GENERATED TOKEN PROPERTIES - Block 1
// BEGIN GENERATED TOKEN PROPERTIES - Block 2
// Do not edit by hand. The code between the "BEGIN GENERATED" and
// "END GENERATED" comments are generated from data in the Material
// Design token database by the script:
// dev/tools/gen_defaults/bin/gen_defaults.dart.
static final String tokenFoo = 'bar';
static final String tokenBar = 'foo';
// END GENERATED TOKEN PROPERTIES - Block 2
''');
// Update 'Block 1' again which should just update that block,
// leaving 'Block 2' undisturbed.
{
final Map<String, dynamic> tokens = <String, dynamic>{'version': '0.0', 'foo': 'FOO', 'bar': 'BAR'};
TestTemplate('Block 1', tempFile.path, tokens).updateFile();
}
expect(tempFile.readAsStringSync(), '''
// This is a file with stuff in it.
// This part shouldn't be changed by
// the template.
// BEGIN GENERATED TOKEN PROPERTIES - Block 1
// Do not edit by hand. The code between the "BEGIN GENERATED" and
// "END GENERATED" comments are generated from data in the Material
// Design token database by the script:
// dev/tools/gen_defaults/bin/gen_defaults.dart.
static final String tokenFoo = 'FOO';
static final String tokenBar = 'BAR';
// END GENERATED TOKEN PROPERTIES - Block 1
// BEGIN GENERATED TOKEN PROPERTIES - Block 2
// Do not edit by hand. The code between the "BEGIN GENERATED" and
// "END GENERATED" comments are generated from data in the Material
// Design token database by the script:
// dev/tools/gen_defaults/bin/gen_defaults.dart.
static final String tokenFoo = 'bar';
static final String tokenBar = 'foo';
// END GENERATED TOKEN PROPERTIES - Block 2
''');
} finally {
tempDir.deleteSync(recursive: true);
}
});
test('Templates can get proper shapes from given data', () {
const Map<String, dynamic> tokens = <String, dynamic>{
'foo.shape': 'shape.large',
'bar.shape': 'shape.full',
'shape.large': <String, dynamic>{
'family': 'SHAPE_FAMILY_ROUNDED_CORNERS',
'topLeft': 1.0,
'topRight': 2.0,
'bottomLeft': 3.0,
'bottomRight': 4.0,
},
'shape.full': <String, dynamic>{
'family': 'SHAPE_FAMILY_CIRCULAR',
},
};
final TestTemplate template = TestTemplate('Test', 'foobar.dart', tokens);
expect(template.shape('foo'), 'const RoundedRectangleBorder(borderRadius: BorderRadius.only(topLeft: Radius.circular(1.0), topRight: Radius.circular(2.0), bottomLeft: Radius.circular(3.0), bottomRight: Radius.circular(4.0)))');
expect(template.shape('bar'), 'const StadiumBorder()');
});
group('Tokens logger', () {
final List<String> printLog = List<String>.empty(growable: true);
final Map<String, List<String>> versionMap = <String, List<String>>{};
final Map<String, dynamic> allTokens = <String, dynamic>{};
// Add to printLog instead of printing to stdout
void Function() overridePrint(void Function() testFn) => () {
final ZoneSpecification spec = ZoneSpecification(
print: (_, __, ___, String msg) {
printLog.add(msg);
}
);
return Zone.current.fork(specification: spec).run<void>(testFn);
};
setUp(() {
logger.init(allTokens: allTokens, versionMap: versionMap);
});
tearDown(() {
logger.clear();
printLog.clear();
versionMap.clear();
allTokens.clear();
});
String errorColoredString(String str) => '\x1B[31m$str\x1B[0m';
const Map<String, List<String>> testVersions = <String, List<String>>{
'v1.0.0': <String>['file_1.json'],
'v2.0.0': <String>['file_2.json, file_3.json'],
};
test('can print empty usage', overridePrint(() {
logger.printVersionUsage(verbose: true);
expect(printLog, contains('Versions used: '));
logger.printTokensUsage(verbose: true);
expect(printLog, contains('Tokens used: 0/0'));
}));
test('can print version usage', overridePrint(() {
versionMap.addAll(testVersions);
logger.printVersionUsage(verbose: false);
expect(printLog, contains('Versions used: v1.0.0, v2.0.0'));
}));
test('can print version usage (verbose)', overridePrint(() {
versionMap.addAll(testVersions);
logger.printVersionUsage(verbose: true);
expect(printLog, contains('Versions used: v1.0.0, v2.0.0'));
expect(printLog, contains(' v1.0.0:'));
expect(printLog, contains(' file_1.json'));
expect(printLog, contains(' v2.0.0:'));
expect(printLog, contains(' file_2.json, file_3.json'));
}));
test('can log and print tokens usage', overridePrint(() {
allTokens['foo'] = 'value';
logger.log('foo');
logger.printTokensUsage(verbose: false);
expect(printLog, contains('Tokens used: 1/1'));
}));
test('can log and print tokens usage (verbose)', overridePrint(() {
allTokens['foo'] = 'value';
logger.log('foo');
logger.printTokensUsage(verbose: true);
expect(printLog, contains('✅ foo'));
expect(printLog, contains('Tokens used: 1/1'));
}));
test('detects invalid logs', overridePrint(() {
allTokens['foo'] = 'value';
logger.log('baz');
logger.log('foobar');
logger.printTokensUsage(verbose: true);
expect(printLog, contains(errorColoredString('Token unavailable: baz')));
expect(printLog, contains(errorColoredString('Token unavailable: foobar')));
expect(printLog, contains('❌ foo'));
expect(printLog, contains('Tokens used: 0/1'));
}));
test('can log and dump versions & tokens to a file', overridePrint(() {
versionMap.addAll(testVersions);
allTokens['foo'] = 'value';
allTokens['bar'] = 'value';
logger.log('foo');
logger.log('bar');
logger.dumpToFile('test.json');
final String fileContent = File('test.json').readAsStringSync();
expect(fileContent, contains('Versions used, v1.0.0, v2.0.0'));
expect(fileContent, contains('bar,'));
expect(fileContent, contains('foo'));
}));
test('integration test', overridePrint(() {
allTokens['foo'] = 'value';
allTokens['bar'] = 'value';
TestTemplate('block', 'filename', allTokens).generate();
logger.printTokensUsage(verbose: true);
expect(printLog, contains('✅ foo'));
expect(printLog, contains('✅ bar'));
expect(printLog, contains('Tokens used: 2/2'));
}));
});
}
class TestTemplate extends TokenTemplate {
TestTemplate(super.blockName, super.fileName, super.tokens);
@override
String generate() => '''
static final String tokenFoo = '${getToken('foo')}';
static final String tokenBar = '${getToken('bar')}';
''';
}