[flutter_tools] reland: integrate l10n tool into hot reload/restart/build (#57510)

Reland: #56167
This commit is contained in:
Jonah Williams 2020-05-18 12:47:18 -07:00 committed by GitHub
parent 27a6705aa4
commit 70b889a9a3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 632 additions and 54 deletions

View file

@ -88,6 +88,12 @@ class CommandHelp {
'Detach (terminate "flutter run" but leave application running).',
);
CommandHelpOption _g;
CommandHelpOption get g => _g ??= _makeOption(
'g',
'Run source code generators.'
);
CommandHelpOption _h;
CommandHelpOption get h => _h ??= _makeOption(
'h',

View file

@ -487,6 +487,7 @@ class _ResidentWebRunner extends ResidentWebRunner {
);
if (debuggingOptions.buildInfo.isDebug) {
await runSourceGenerators();
// Full restart is always false for web, since the extra recompile is wasteful.
final UpdateFSReport report = await _updateDevFS(fullRestart: false);
if (report.success) {

View file

@ -134,6 +134,12 @@ abstract class Target {
/// A list of zero or more depfiles, located directly under {BUILD_DIR}.
List<String> get depfiles => const <String>[];
/// Whether this target can be executed with the given [environment].
///
/// Returning `true` will cause [build] to be skipped. This is equivalent
/// to a build that produces no outputs.
bool canSkip(Environment environment) => false;
/// The action which performs this build step.
Future<void> build(Environment environment);
@ -773,18 +779,26 @@ class _BuildInstance {
updateGraph();
return succeeded;
}
logger.printTrace('${node.target.name}: Starting due to ${node.invalidatedReasons}');
await node.target.build(environment);
logger.printTrace('${node.target.name}: Complete');
// Clear old inputs. These will be replaced with new inputs/outputs
// after the target is run. In the case of a runtime skip, each list
// must be empty to ensure the previous outputs are purged.
node.inputs.clear();
node.outputs.clear();
node.inputs
..clear()
..addAll(node.target.resolveInputs(environment).sources);
node.outputs
..clear()
..addAll(node.target.resolveOutputs(environment).sources);
// Check if we can skip via runtime dependencies.
final bool runtimeSkip = node.target.canSkip(environment);
if (runtimeSkip) {
logger.printTrace('Skipping target: ${node.target.name}');
skipped = true;
} else {
logger.printTrace('${node.target.name}: Starting due to ${node.invalidatedReasons}');
await node.target.build(environment);
logger.printTrace('${node.target.name}: Complete');
node.inputs.addAll(node.target.resolveInputs(environment).sources);
node.outputs.addAll(node.target.resolveOutputs(environment).sources);
}
// If we were missing the depfile, resolve input files after executing the
// If we were missing the depfile, resolve input files after executing the
// target so that all file hashes are up to date on the next run.
if (node.missingDepfile) {
await fileCache.diffFileList(node.inputs);

View file

@ -17,6 +17,7 @@ import '../depfile.dart';
import '../exceptions.dart';
import 'assets.dart';
import 'icon_tree_shaker.dart';
import 'localizations.dart';
/// The define to pass a [BuildMode].
const String kBuildMode = 'BuildMode';
@ -183,7 +184,9 @@ class KernelSnapshot extends Target {
];
@override
List<Target> get dependencies => <Target>[];
List<Target> get dependencies => const <Target>[
GenerateLocalizationsTarget(),
];
@override
Future<void> build(Environment environment) async {

View file

@ -0,0 +1,272 @@
// 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 'package:meta/meta.dart';
import 'package:process/process.dart';
import 'package:yaml/yaml.dart';
import '../../artifacts.dart';
import '../../base/file_system.dart';
import '../../base/io.dart';
import '../../base/logger.dart';
import '../../convert.dart';
import '../../globals.dart' as globals;
import '../build_system.dart';
import '../depfile.dart';
const String _kDependenciesFileName = 'gen_l10n_inputs_and_outputs.json';
/// Run the localizations generation script with the configuration [options].
Future<void> generateLocalizations({
@required LocalizationOptions options,
@required String flutterRoot,
@required FileSystem fileSystem,
@required ProcessManager processManager,
@required Logger logger,
@required Directory projectDir,
@required String dartBinaryPath,
@required Directory dependenciesDir,
}) async {
final String genL10nPath = fileSystem.path.join(
flutterRoot,
'dev',
'tools',
'localization',
'bin',
'gen_l10n.dart',
);
final ProcessResult result = await processManager.run(<String>[
dartBinaryPath,
genL10nPath,
'--gen-inputs-and-outputs-list=${dependenciesDir.path}',
if (options.arbDirectory != null)
'--arb-dir=${options.arbDirectory.toFilePath()}',
if (options.templateArbFile != null)
'--template-arb-file=${options.templateArbFile.toFilePath()}',
if (options.outputLocalizationsFile != null)
'--output-localization-file=${options.outputLocalizationsFile.toFilePath()}',
if (options.untranslatedMessagesFile != null)
'--untranslated-messages-file=${options.untranslatedMessagesFile.toFilePath()}',
if (options.outputClass != null)
'--output-class=${options.outputClass}',
if (options.headerFile != null)
'--header-file=${options.headerFile.toFilePath()}',
if (options.header != null)
'--header=${options.header}',
if (options.deferredLoading != null)
'--use-deferred-loading',
if (options.preferredSupportedLocales != null)
'--preferred-supported-locales=${options.preferredSupportedLocales}',
]);
if (result.exitCode != 0) {
logger.printError(result.stdout + result.stderr as String);
throw Exception();
}
}
/// A build step that runs the generate localizations script from
/// dev/tool/localizations.
class GenerateLocalizationsTarget extends Target {
const GenerateLocalizationsTarget();
@override
List<Target> get dependencies => <Target>[];
@override
List<Source> get inputs => <Source>[
// This is added as a convenience for developing the tool.
const Source.pattern('{FLUTTER_ROOT}/packages/flutter_tools/lib/src/build_system/targets/localizations.dart'),
// TODO(jonahwilliams): once https://github.com/flutter/flutter/issues/56321 is
// complete, we should add the artifact as a dependency here. Since the tool runs
// this code from source, looking up each dependency will be cumbersome.
];
@override
String get name => 'gen_localizations';
@override
List<Source> get outputs => <Source>[];
@override
List<String> get depfiles => <String>['gen_localizations.d'];
@override
bool canSkip(Environment environment) {
final File configFile = environment.projectDir.childFile('l10n.yaml');
return !configFile.existsSync();
}
@override
Future<void> build(Environment environment) async {
final File configFile = environment.projectDir.childFile('l10n.yaml');
assert(configFile.existsSync());
final LocalizationOptions options = parseLocalizationsOptions(
file: configFile,
logger: globals.logger,
);
final DepfileService depfileService = DepfileService(
logger: environment.logger,
fileSystem: environment.fileSystem,
);
await generateLocalizations(
fileSystem: environment.fileSystem,
flutterRoot: environment.flutterRootDir.path,
logger: environment.logger,
processManager: environment.processManager,
options: options,
projectDir: environment.projectDir,
dartBinaryPath: environment.artifacts
.getArtifactPath(Artifact.engineDartBinary),
dependenciesDir: environment.buildDir,
);
final Map<String, Object> dependencies = json
.decode(environment.buildDir.childFile(_kDependenciesFileName).readAsStringSync()) as Map<String, Object>;
final Depfile depfile = Depfile(
<File>[
configFile,
for (dynamic inputFile in dependencies['inputs'] as List<dynamic>)
environment.fileSystem.file(inputFile)
],
<File>[
for (dynamic outputFile in dependencies['outputs'] as List<dynamic>)
environment.fileSystem.file(outputFile)
]
);
depfileService.writeToFile(
depfile,
environment.buildDir.childFile('gen_localizations.d'),
);
}
}
/// Typed configuration from the localizations config file.
class LocalizationOptions {
const LocalizationOptions({
this.arbDirectory,
this.templateArbFile,
this.outputLocalizationsFile,
this.untranslatedMessagesFile,
this.header,
this.outputClass,
this.preferredSupportedLocales,
this.headerFile,
this.deferredLoading,
});
/// The `--arb-dir` argument.
///
/// The directory where all localization files should reside.
final Uri arbDirectory;
/// The `--template-arb-file` argument.
///
/// This URI is relative to [arbDirectory].
final Uri templateArbFile;
/// The `--output-localization-file` argument.
///
/// This URI is relative to [arbDirectory].
final Uri outputLocalizationsFile;
/// The `--untranslated-messages-file` argument.
///
/// This URI is relative to [arbDirectory].
final Uri untranslatedMessagesFile;
/// The `--header` argument.
///
/// The header to prepend to the generated Dart localizations
final String header;
/// The `--output-class` argument.
final String outputClass;
/// The `--preferred-supported-locales` argument.
final String preferredSupportedLocales;
/// The `--header-file` argument.
///
/// A file containing the header to preprend to the generated
/// Dart localizations.
final Uri headerFile;
/// The `--use-deferred-loading` argument.
///
/// Whether to generate the Dart localization file with locales imported
/// as deferred.
final bool deferredLoading;
}
/// Parse the localizations configuration options from [file].
///
/// Throws [Exception] if any of the contents are invalid. Returns a
/// [LocalizationOptions] with all fields as `null` if the config file exists
/// but is empty.
LocalizationOptions parseLocalizationsOptions({
@required File file,
@required Logger logger,
}) {
final String contents = file.readAsStringSync();
if (contents.trim().isEmpty) {
return const LocalizationOptions();
}
final YamlNode yamlNode = loadYamlNode(file.readAsStringSync());
if (yamlNode is! YamlMap) {
logger.printError('Expected ${file.path} to contain a map, instead was $yamlNode');
throw Exception();
}
final YamlMap yamlMap = yamlNode as YamlMap;
return LocalizationOptions(
arbDirectory: _tryReadUri(yamlMap, 'arb-dir', logger),
templateArbFile: _tryReadUri(yamlMap, 'template-arb-file', logger),
outputLocalizationsFile: _tryReadUri(yamlMap, 'output-localization-file', logger),
untranslatedMessagesFile: _tryReadUri(yamlMap, 'untranslated-messages-file', logger),
header: _tryReadString(yamlMap, 'header', logger),
outputClass: _tryReadString(yamlMap, 'output-class', logger),
preferredSupportedLocales: _tryReadString(yamlMap, 'preferred-supported-locales', logger),
headerFile: _tryReadUri(yamlMap, 'header-file', logger),
deferredLoading: _tryReadBool(yamlMap, 'use-deferred-loading', logger),
);
}
// Try to read a `bool` value or null from `yamlMap`, otherwise throw.
bool _tryReadBool(YamlMap yamlMap, String key, Logger logger) {
final Object value = yamlMap[key];
if (value == null) {
return null;
}
if (value is! bool) {
logger.printError('Expected "$key" to have a bool value, instead was "$value"');
throw Exception();
}
return value as bool;
}
// Try to read a `String` value or null from `yamlMap`, otherwise throw.
String _tryReadString(YamlMap yamlMap, String key, Logger logger) {
final Object value = yamlMap[key];
if (value == null) {
return null;
}
if (value is! String) {
logger.printError('Expected "$key" to have a String value, instead was "$value"');
throw Exception();
}
return value as String;
}
// Try to read a valid `Uri` or null from `yamlMap`, otherwise throw.
Uri _tryReadUri(YamlMap yamlMap, String key, Logger logger) {
final String value = _tryReadString(yamlMap, key, logger);
if (value == null) {
return null;
}
final Uri uri = Uri.tryParse(value);
if (uri == null) {
logger.printError('"$value" must be a relative file URI');
}
return uri;
}

View file

@ -15,6 +15,7 @@ import '../build_system.dart';
import '../depfile.dart';
import 'assets.dart';
import 'dart.dart';
import 'localizations.dart';
/// Whether web builds should call the platform initialization logic.
const String kInitializePlatform = 'InitializePlatform';
@ -132,7 +133,8 @@ class Dart2JSTarget extends Target {
@override
List<Target> get dependencies => const <Target>[
WebEntrypointTarget()
WebEntrypointTarget(),
GenerateLocalizationsTarget(),
];
@override

View file

@ -4,10 +4,10 @@
import 'dart:async';
import 'package:vm_service/vm_service.dart' as vm_service;
import 'package:devtools_server/devtools_server.dart' as devtools_server;
import 'package:meta/meta.dart';
import 'package:package_config/package_config.dart';
import 'package:vm_service/vm_service.dart' as vm_service;
import 'application_package.dart';
import 'artifacts.dart';
@ -21,7 +21,10 @@ import 'base/signals.dart';
import 'base/terminal.dart';
import 'base/utils.dart';
import 'build_info.dart';
import 'build_system/build_system.dart';
import 'build_system/targets/localizations.dart';
import 'bundle.dart';
import 'cache.dart';
import 'codegen.dart';
import 'compile.dart';
import 'convert.dart';
@ -800,6 +803,40 @@ abstract class ResidentRunner {
throw '${fullRestart ? 'Restart' : 'Reload'} is not supported in $mode mode';
}
BuildResult _lastBuild;
Environment _environment;
Future<void> runSourceGenerators() async {
_environment ??= Environment(
artifacts: globals.artifacts,
logger: globals.logger,
cacheDir: globals.cache.getRoot(),
engineVersion: globals.flutterVersion.engineRevision,
fileSystem: globals.fs,
flutterRootDir: globals.fs.directory(Cache.flutterRoot),
outputDir: globals.fs.directory(getBuildDirectory()),
processManager: globals.processManager,
projectDir: globals.fs.currentDirectory,
);
globals.logger.printTrace('Starting incremental build...');
_lastBuild = await globals.buildSystem.buildIncremental(
const GenerateLocalizationsTarget(),
_environment,
_lastBuild,
);
if (!_lastBuild.success) {
for (final ExceptionMeasurement exceptionMeasurement in _lastBuild.exceptions.values) {
globals.logger.printError(
exceptionMeasurement.exception.toString(),
stackTrace: globals.logger.isVerbose
? exceptionMeasurement.stackTrace
: null,
);
}
}
globals.logger.printTrace('complete');
}
/// Toggle whether canvaskit is being used for rendering, returning the new
/// state.
///
@ -1200,6 +1237,7 @@ abstract class ResidentRunner {
commandHelp.p.print();
commandHelp.o.print();
commandHelp.z.print();
commandHelp.g.print();
} else {
commandHelp.S.print();
commandHelp.U.print();
@ -1337,6 +1375,9 @@ class TerminalHandler {
case 'D':
await residentRunner.detach();
return true;
case 'g':
await residentRunner.runSourceGenerators();
return true;
case 'h':
case 'H':
case '?':

View file

@ -7,8 +7,8 @@ import 'package:package_config/package_config.dart';
import 'package:vm_service/vm_service.dart' as vm_service;
import 'package:meta/meta.dart';
import 'package:pool/pool.dart';
import 'base/async_guard.dart';
import 'base/async_guard.dart';
import 'base/context.dart';
import 'base/file_system.dart';
import 'base/logger.dart';
@ -365,6 +365,7 @@ class HotRunner extends ResidentRunner {
// build, reducing overall initialization time. This is safe because the first
// invocation of the frontend server produces a full dill file that the
// subsequent invocation in devfs will not overwrite.
await runSourceGenerators();
if (device.generator != null) {
startupTasks.add(
device.generator.recompile(
@ -674,6 +675,10 @@ class HotRunner extends ResidentRunner {
emulator = false;
}
final Stopwatch timer = Stopwatch()..start();
// Run source generation if needed.
await runSourceGenerators();
if (fullRestart) {
final OperationResult result = await _fullRestartHelper(
targetPlatform: targetPlatform,
@ -1190,7 +1195,7 @@ class ProjectFileInvalidator {
static const String _pubCachePathWindows = 'Pub/Cache';
// As of writing, Dart supports up to 32 asynchronous I/O threads per
// isolate. We also want to avoid hitting platform limits on open file
// isolate. We also want to avoid hitting platform limits on open file
// handles/descriptors.
//
// This value was chosen based on empirical tests scanning a set of
@ -1223,7 +1228,6 @@ class ProjectFileInvalidator {
if (_isNotInPubCache(uri)) uri,
];
final List<Uri> invalidatedFiles = <Uri>[];
if (asyncScanning) {
final Pool pool = Pool(_kMaxPendingStats);
final List<Future<void>> waitList = <Future<void>>[];

View file

@ -57,6 +57,7 @@ void _testMessageLength({
expect(commandHelp.U.toString().length, lessThanOrEqualTo(expectedWidth));
expect(commandHelp.a.toString().length, lessThanOrEqualTo(expectedWidth));
expect(commandHelp.d.toString().length, lessThanOrEqualTo(expectedWidth));
expect(commandHelp.g.toString().length, lessThanOrEqualTo(expectedWidth));
expect(commandHelp.h.toString().length, lessThanOrEqualTo(expectedWidth));
expect(commandHelp.i.toString().length, lessThanOrEqualTo(expectedWidth));
expect(commandHelp.k.toString().length, lessThanOrEqualTo(expectedWidth));
@ -88,6 +89,7 @@ void main() {
expect(commandHelp.U.toString(), startsWith('\x1B[1mU\x1B[22m'));
expect(commandHelp.a.toString(), startsWith('\x1B[1ma\x1B[22m'));
expect(commandHelp.d.toString(), startsWith('\x1B[1md\x1B[22m'));
expect(commandHelp.g.toString(), startsWith('\x1B[1mg\x1B[22m'));
expect(commandHelp.h.toString(), startsWith('\x1B[1mh\x1B[22m'));
expect(commandHelp.i.toString(), startsWith('\x1B[1mi\x1B[22m'));
expect(commandHelp.o.toString(), startsWith('\x1B[1mo\x1B[22m'));
@ -164,6 +166,7 @@ void main() {
expect(commandHelp.U.toString(), equals('\x1B[1mU\x1B[22m Dump accessibility tree in inverse hit test order. \x1B[1;30m(debugDumpSemantics)\x1B[39m'));
expect(commandHelp.a.toString(), equals('\x1B[1ma\x1B[22m Toggle timeline events for all widget build methods. \x1B[1;30m(debugProfileWidgetBuilds)\x1B[39m'));
expect(commandHelp.d.toString(), equals('\x1B[1md\x1B[22m Detach (terminate "flutter run" but leave application running).'));
expect(commandHelp.g.toString(), equals('\x1B[1mg\x1B[22m Run source code generators.'));
expect(commandHelp.h.toString(), equals('\x1B[1mh\x1B[22m Repeat this help message.'));
expect(commandHelp.i.toString(), equals('\x1B[1mi\x1B[22m Toggle widget inspector. \x1B[1;30m(WidgetsApp.showWidgetInspectorOverride)\x1B[39m'));
expect(commandHelp.o.toString(), equals('\x1B[1mo\x1B[22m Simulate different operating systems. \x1B[1;30m(defaultTargetPlatform)\x1B[39m'));
@ -190,6 +193,7 @@ void main() {
expect(commandHelp.U.toString(), equals('U Dump accessibility tree in inverse hit test order. (debugDumpSemantics)'));
expect(commandHelp.a.toString(), equals('a Toggle timeline events for all widget build methods. (debugProfileWidgetBuilds)'));
expect(commandHelp.d.toString(), equals('d Detach (terminate "flutter run" but leave application running).'));
expect(commandHelp.g.toString(), equals('g Run source code generators.'));
expect(commandHelp.h.toString(), equals('h Repeat this help message.'));
expect(commandHelp.i.toString(), equals('i Toggle widget inspector. (WidgetsApp.showWidgetInspectorOverride)'));
expect(commandHelp.o.toString(), equals('o Simulate different operating systems. (defaultTargetPlatform)'));

View file

@ -581,6 +581,50 @@ void main() {
expect(fileSystem.file('output/debug'), isNot(exists));
expect(fileSystem.file('output/release'), exists);
});
testWithoutContext('A target using canSkip can create a conditional output', () async {
final BuildSystem buildSystem = setUpBuildSystem(fileSystem);
final File bar = environment.buildDir.childFile('bar');
final File foo = environment.buildDir.childFile('foo');
// The target will write a file `foo`, but only if `bar` already exists.
final TestTarget target = TestTarget(
(Environment environment) async {
foo.writeAsStringSync(bar.readAsStringSync());
environment.buildDir
.childFile('example.d')
.writeAsStringSync('${foo.path}: ${bar.path}');
},
(Environment environment) {
return !environment.buildDir.childFile('bar').existsSync();
}
)
..depfiles = const <String>['example.d'];
// bar does not exist, there should be no inputs/outputs.
final BuildResult firstResult = await buildSystem.build(target, environment);
expect(foo, isNot(exists));
expect(firstResult.inputFiles, isEmpty);
expect(firstResult.outputFiles, isEmpty);
// bar is created, the target should be able to run.
bar.writeAsStringSync('content-1');
final BuildResult secondResult = await buildSystem.build(target, environment);
expect(foo, exists);
expect(secondResult.inputFiles.map((File file) => file.path), <String>[bar.path]);
expect(secondResult.outputFiles.map((File file) => file.path), <String>[foo.path]);
// bar is destroyed, foo is also deleted.
bar.deleteSync();
final BuildResult thirdResult = await buildSystem.build(target, environment);
expect(foo, isNot(exists));
expect(thirdResult.inputFiles, isEmpty);
expect(thirdResult.outputFiles, isEmpty);
});
}
BuildSystem setUpBuildSystem(FileSystem fileSystem) {
@ -592,10 +636,20 @@ BuildSystem setUpBuildSystem(FileSystem fileSystem) {
}
class TestTarget extends Target {
TestTarget([this._build]);
TestTarget([this._build, this._canSkip]);
final Future<void> Function(Environment environment) _build;
final bool Function(Environment environment) _canSkip;
@override
bool canSkip(Environment environment) {
if (_canSkip != null) {
return _canSkip(environment);
}
return super.canSkip(environment);
}
@override
Future<void> build(Environment environment) => _build(environment);

View file

@ -0,0 +1,131 @@
// 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 'package:file/memory.dart';
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/build_system/build_system.dart';
import 'package:flutter_tools/src/build_system/targets/localizations.dart';
import '../../../src/common.dart';
import '../../../src/context.dart';
void main() {
// Verifies that values are correctly passed through the localizations
// target, but does not validate them beyond the serialized data type.
testWithoutContext('generateLocalizations forwards arguments correctly', () async {
final FileSystem fileSystem = MemoryFileSystem.test();
final Logger logger = BufferLogger.test();
final FakeProcessManager processManager = FakeProcessManager.list(<FakeCommand>[
const FakeCommand(
command: <String>[
'dart',
'dev/tools/localization/bin/gen_l10n.dart',
'--gen-inputs-and-outputs-list=/',
'--arb-dir=arb',
'--template-arb-file=example.arb',
'--output-localization-file=bar',
'--untranslated-messages-file=untranslated',
'--output-class=Foo',
'--header-file=header',
'--header=HEADER',
'--use-deferred-loading',
'--preferred-supported-locales=en_US'
],
),
]);
final Directory arbDirectory = fileSystem.directory('arb')
..createSync();
arbDirectory.childFile('foo.arb').createSync();
arbDirectory.childFile('bar.arb').createSync();
final LocalizationOptions options = LocalizationOptions(
header: 'HEADER',
headerFile: Uri.file('header'),
arbDirectory: Uri.file('arb'),
deferredLoading: true,
outputClass: 'Foo',
outputLocalizationsFile: Uri.file('bar'),
preferredSupportedLocales: 'en_US',
templateArbFile: Uri.file('example.arb'),
untranslatedMessagesFile: Uri.file('untranslated'),
);
await generateLocalizations(
options: options,
logger: logger,
fileSystem: fileSystem,
processManager: processManager,
projectDir: fileSystem.currentDirectory,
dartBinaryPath: 'dart',
flutterRoot: '',
dependenciesDir: fileSystem.currentDirectory,
);
expect(processManager.hasRemainingExpectations, false);
});
testWithoutContext('generateLocalizations is skipped if l10n.yaml does not exist.', () async {
final FileSystem fileSystem = MemoryFileSystem.test();
final Environment environment = Environment.test(
fileSystem.currentDirectory,
artifacts: null,
fileSystem: fileSystem,
logger: BufferLogger.test(),
processManager: FakeProcessManager.any(),
);
expect(const GenerateLocalizationsTarget().canSkip(environment), true);
environment.projectDir.childFile('l10n.yaml').createSync();
expect(const GenerateLocalizationsTarget().canSkip(environment), false);
});
testWithoutContext('parseLocalizationsOptions handles valid yaml configuration', () async {
final FileSystem fileSystem = MemoryFileSystem.test();
final File configFile = fileSystem.file('l10n.yaml')
..writeAsStringSync('''
arb-dir: arb
template-arb-file: example.arb
output-localization-file: bar
untranslated-messages-file: untranslated
output-class: Foo
header-file: header
header: HEADER
use-deferred-loading: true
preferred-supported-locales: en_US
''');
final LocalizationOptions options = parseLocalizationsOptions(
file: configFile,
logger: BufferLogger.test(),
);
expect(options.arbDirectory, Uri.parse('arb'));
expect(options.templateArbFile, Uri.parse('example.arb'));
expect(options.outputLocalizationsFile, Uri.parse('bar'));
expect(options.untranslatedMessagesFile, Uri.parse('untranslated'));
expect(options.outputClass, 'Foo');
expect(options.headerFile, Uri.parse('header'));
expect(options.header, 'HEADER');
expect(options.deferredLoading, true);
expect(options.preferredSupportedLocales, 'en_US');
});
testWithoutContext('parseLocalizationsOptions throws exception on invalid yaml configuration', () async {
final FileSystem fileSystem = MemoryFileSystem.test();
final File configFile = fileSystem.file('l10n.yaml')
..writeAsStringSync('''
use-deferred-loading: string
''');
expect(
() => parseLocalizationsOptions(
file: configFile,
logger: BufferLogger.test(),
),
throwsA(isA<Exception>()),
);
});
}

View file

@ -4,6 +4,7 @@
import 'dart:async';
import 'package:flutter_tools/src/cache.dart';
import 'package:vm_service/vm_service.dart' as vm_service;
import 'package:file/memory.dart';
import 'package:file_testing/file_testing.dart';
@ -537,6 +538,61 @@ void main() {
expect(cacheDill, isNot(exists));
}));
test('ResidentRunner can run source generation', () => testbed.run(() async {
final FakeProcessManager processManager = globals.processManager as FakeProcessManager;
final Directory dependencies = globals.fs.directory(
globals.fs.path.join('build', '6ec2559087977927717927ede0a147f1'));
processManager.addCommand(FakeCommand(
command: <String>[
globals.artifacts.getArtifactPath(Artifact.engineDartBinary),
globals.fs.path.join(Cache.flutterRoot, 'dev', 'tools', 'localization', 'bin', 'gen_l10n.dart'),
'--gen-inputs-and-outputs-list=${dependencies.absolute.path}',
],
onRun: () {
dependencies
.childFile('gen_l10n_inputs_and_outputs.json')
..createSync()
..writeAsStringSync('{"inputs":[],"outputs":[]}');
}
));
globals.fs.file(globals.fs.path.join('lib', 'l10n', 'foo.arb'))
.createSync(recursive: true);
globals.fs.file('l10n.yaml').createSync();
await residentRunner.runSourceGenerators();
expect(testLogger.errorText, isEmpty);
}, overrides: <Type, Generator>{
ProcessManager: () => FakeProcessManager.list(<FakeCommand>[]),
}));
test('ResidentRunner can run source generation - generation fails', () => testbed.run(() async {
final FakeProcessManager processManager = globals.processManager as FakeProcessManager;
final Directory dependencies = globals.fs.directory(
globals.fs.path.join('build', '6ec2559087977927717927ede0a147f1'));
processManager.addCommand(FakeCommand(
command: <String>[
globals.artifacts.getArtifactPath(Artifact.engineDartBinary),
globals.fs.path.join(Cache.flutterRoot, 'dev', 'tools', 'localization', 'bin', 'gen_l10n.dart'),
'--gen-inputs-and-outputs-list=${dependencies.absolute.path}',
],
exitCode: 1,
stderr: 'stderr'
));
globals.fs.file(globals.fs.path.join('lib', 'l10n', 'foo.arb'))
.createSync(recursive: true);
globals.fs.file('l10n.yaml').createSync();
await residentRunner.runSourceGenerators();
expect(testLogger.errorText, allOf(
contains('stderr'), // Message from gen_l10n.dart
contains('Exception') // Message from build_system
));
}, overrides: <Type, Generator>{
ProcessManager: () => FakeProcessManager.list(<FakeCommand>[]),
}));
test('ResidentRunner printHelpDetails', () => testbed.run(() {
fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[]);
when(mockDevice.supportsHotRestart).thenReturn(true);
@ -573,6 +629,7 @@ void main() {
commandHelp.p,
commandHelp.o,
commandHelp.z,
commandHelp.g,
commandHelp.M,
commandHelp.v,
commandHelp.P,

View file

@ -6,15 +6,14 @@ import 'dart:async';
import 'package:file/file.dart';
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/io.dart';
import 'package:flutter_tools/src/globals.dart' as globals;
import 'package:process/process.dart';
import '../src/common.dart';
import 'test_data/gen_l10n_project.dart';
import 'test_driver.dart';
import 'test_utils.dart';
final GenL10nProject project = GenL10nProject();
// Verify that the code generated by gen_l10n executes correctly.
// It can fail if gen_l10n produces a lib/l10n/app_localizations.dart that:
// - Does not analyze cleanly.
@ -23,50 +22,23 @@ import 'test_utils.dart';
// loaded workstation, so the test could time out on a heavily loaded bot.
void main() {
Directory tempDir;
final GenL10nProject _project = GenL10nProject();
FlutterRunTestDriver _flutter;
FlutterRunTestDriver flutter;
setUp(() async {
tempDir = createResolvedTempDirectorySync('gen_l10n_test.');
await _project.setUpIn(tempDir);
_flutter = FlutterRunTestDriver(tempDir);
});
tearDown(() async {
await _flutter.stop();
await flutter.stop();
tryToDelete(tempDir);
});
void runCommand(List<String> command) {
final ProcessResult result = const LocalProcessManager().runSync(
command,
workingDirectory: tempDir.path,
environment: <String, String>{ 'FLUTTER_ROOT': getFlutterRoot() },
);
if (result.exitCode != 0) {
throw Exception('FAILED [${result.exitCode}]: ${command.join(' ')}\n${result.stderr}\n${result.stdout}');
}
}
void setUpAndRunGenL10n({List<String> args}) {
// Get the intl packages before running gen_l10n.
final String flutterBin = globals.platform.isWindows ? 'flutter.bat' : 'flutter';
final String flutterPath = globals.fs.path.join(getFlutterRoot(), 'bin', flutterBin);
runCommand(<String>[flutterPath, 'pub', 'get']);
// Generate lib/l10n/app_localizations.dart
final String genL10nPath = globals.fs.path.join(getFlutterRoot(), 'dev', 'tools', 'localization', 'bin', 'gen_l10n.dart');
final String dartBin = globals.platform.isWindows ? 'dart.exe' : 'dart';
final String dartPath = globals.fs.path.join(getFlutterRoot(), 'bin', 'cache', 'dart-sdk', 'bin', dartBin);
runCommand(<String>[dartPath, genL10nPath, args?.join(' ')]);
}
Future<StringBuffer> runApp() async {
// Run the app defined in GenL10nProject.main and wait for it to
// send '#l10n END' to its stdout.
final Completer<void> l10nEnd = Completer<void>();
final StringBuffer stdout = StringBuffer();
final StreamSubscription<String> subscription = _flutter.stdout.listen((String line) {
final StreamSubscription<String> subscription = flutter.stdout.listen((String line) {
if (line.contains('#l10n')) {
stdout.writeln(line.substring(line.indexOf('#l10n')));
}
@ -74,7 +46,7 @@ void main() {
l10nEnd.complete();
}
});
await _flutter.run();
await flutter.run();
await l10nEnd.future;
await subscription.cancel();
return stdout;
@ -180,13 +152,15 @@ void main() {
}
test('generated l10n classes produce expected localized strings', () async {
setUpAndRunGenL10n();
await project.setUpIn(tempDir);
flutter = FlutterRunTestDriver(tempDir);
final StringBuffer stdout = await runApp();
expectOutput(stdout);
});
test('generated l10n classes produce expected localized strings with deferred loading', () async {
setUpAndRunGenL10n(args: <String>['--use-deferred-loading']);
await project.setUpIn(tempDir, useDeferredLoading: true);
flutter = FlutterRunTestDriver(tempDir);
final StringBuffer stdout = await runApp();
expectOutput(stdout);
});

View file

@ -7,13 +7,16 @@ import 'dart:async';
import 'package:file/file.dart';
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/globals.dart' as globals;
import 'package:meta/meta.dart';
import '../test_utils.dart';
import 'project.dart';
class GenL10nProject extends Project {
@override
Future<void> setUpIn(Directory dir) {
Future<void> setUpIn(Directory dir, {
bool useDeferredLoading = false,
}) {
this.dir = dir;
writeFile(globals.fs.path.join(dir.path, 'lib', 'l10n', 'app_en.arb'), appEn);
writeFile(globals.fs.path.join(dir.path, 'lib', 'l10n', 'app_en_CA.arb'), appEnCa);
@ -24,6 +27,7 @@ class GenL10nProject extends Project {
writeFile(globals.fs.path.join(dir.path, 'lib', 'l10n', 'app_zh_Hant.arb'), appZhHant);
writeFile(globals.fs.path.join(dir.path, 'lib', 'l10n', 'app_zh_Hans.arb'), appZhHans);
writeFile(globals.fs.path.join(dir.path, 'lib', 'l10n', 'app_zh_Hant_TW.arb'), appZhHantTw);
writeFile(globals.fs.path.join(dir.path, 'l10n.yaml'), l10nYaml(useDeferredLoading: useDeferredLoading));
return super.setUpIn(dir);
}
@ -561,4 +565,15 @@ void main() {
"helloWorld": "台灣繁體你好世界"
}
''';
String l10nYaml({
@required bool useDeferredLoading,
}) {
if (useDeferredLoading) {
return r'''
use-deferred-loading: false
''';
}
return '';
}
}