mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00

Support for FFI calls with `@Native external` functions through Native assets on MacOS and iOS. This enables bundling native code without any build-system boilerplate code. For more info see: * https://github.com/flutter/flutter/issues/129757 ### Implementation details for MacOS and iOS. Dylibs are bundled by (1) making them fat binaries if multiple architectures are targeted, (2) code signing these, and (3) copying them to the frameworks folder. These steps are done manual rather than via CocoaPods. CocoaPods would have done the same steps, but (a) needs the dylibs to be there before the `xcodebuild` invocation (we could trick it, by having a minimal dylib in the place and replace it during the build process, that works), and (b) can't deal with having no dylibs to be bundled (we'd have to bundle a dummy dylib or include some dummy C code in the build file). The dylibs are build as a new target inside flutter assemble, as that is the moment we know what build-mode and architecture to target. The mapping from asset id to dylib-path is passed in to every kernel compilation path. The interesting case is hot-restart where the initial kernel file is compiled by the "inner" flutter assemble, while after hot restart the "outer" flutter run compiled kernel file is pushed to the device. Both kernel files need to contain the mapping. The "inner" flutter assemble gets its mapping from the NativeAssets target which builds the native assets. The "outer" flutter run get its mapping from a dry-run invocation. Since this hot restart can be used for multiple target devices (`flutter run -d all`) it contains the mapping for all known targets. ### Example vs template The PR includes a new template that uses the new native assets in a package and has an app importing that. Separate discussion in: https://github.com/flutter/flutter/issues/131209. ### Tests This PR adds new tests to cover the various use cases. * dev/devicelab/bin/tasks/native_assets_ios.dart * Runs an example app with native assets in all build modes, doing hot reload and hot restart in debug mode. * dev/devicelab/bin/tasks/native_assets_ios_simulator.dart * Runs an example app with native assets, doing hot reload and hot restart. * packages/flutter_tools/test/integration.shard/native_assets_test.dart * Runs (incl hot reload/hot restart), builds, builds frameworks for iOS, MacOS and flutter-tester. * packages/flutter_tools/test/general.shard/build_system/targets/native_assets_test.dart * Unit tests the new Target in the backend. * packages/flutter_tools/test/general.shard/ios/native_assets_test.dart * packages/flutter_tools/test/general.shard/macos/native_assets_test.dart * Unit tests the native assets being packaged on a iOS/MacOS build. It also extends various existing tests: * dev/devicelab/bin/tasks/module_test_ios.dart * Exercises the add2app scenario. * packages/flutter_tools/test/general.shard/features_test.dart * Unit test the new feature flag.
832 lines
30 KiB
Dart
832 lines
30 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:convert';
|
|
import 'dart:io' as io show Process, ProcessSignal;
|
|
|
|
import 'package:file/file.dart';
|
|
import 'package:file/memory.dart';
|
|
import 'package:file_testing/file_testing.dart';
|
|
import 'package:flutter_tools/src/artifacts.dart';
|
|
import 'package:flutter_tools/src/asset.dart';
|
|
import 'package:flutter_tools/src/base/file_system.dart';
|
|
import 'package:flutter_tools/src/base/io.dart';
|
|
import 'package:flutter_tools/src/base/logger.dart';
|
|
import 'package:flutter_tools/src/base/os.dart';
|
|
import 'package:flutter_tools/src/base/platform.dart';
|
|
import 'package:flutter_tools/src/build_info.dart';
|
|
import 'package:flutter_tools/src/build_system/targets/shader_compiler.dart';
|
|
import 'package:flutter_tools/src/compile.dart';
|
|
import 'package:flutter_tools/src/devfs.dart';
|
|
import 'package:flutter_tools/src/device.dart';
|
|
import 'package:flutter_tools/src/vmservice.dart';
|
|
import 'package:package_config/package_config.dart';
|
|
import 'package:test/fake.dart';
|
|
|
|
import '../src/common.dart';
|
|
import '../src/context.dart';
|
|
import '../src/fake_http_client.dart';
|
|
import '../src/fake_vm_services.dart';
|
|
import '../src/fakes.dart';
|
|
import '../src/logging_logger.dart';
|
|
|
|
final FakeVmServiceRequest createDevFSRequest = FakeVmServiceRequest(
|
|
method: '_createDevFS',
|
|
args: <String, Object>{
|
|
'fsName': 'test',
|
|
},
|
|
jsonResponse: <String, Object>{
|
|
'uri': Uri.parse('test').toString(),
|
|
}
|
|
);
|
|
|
|
const FakeVmServiceRequest failingCreateDevFSRequest = FakeVmServiceRequest(
|
|
method: '_createDevFS',
|
|
args: <String, Object>{
|
|
'fsName': 'test',
|
|
},
|
|
errorCode: RPCErrorCodes.kServiceDisappeared,
|
|
);
|
|
|
|
const FakeVmServiceRequest failingDeleteDevFSRequest = FakeVmServiceRequest(
|
|
method: '_deleteDevFS',
|
|
args: <String, dynamic>{'fsName': 'test'},
|
|
errorCode: RPCErrorCodes.kServiceDisappeared,
|
|
);
|
|
|
|
void main() {
|
|
testWithoutContext('DevFSByteContent', () {
|
|
final DevFSByteContent content = DevFSByteContent(<int>[4, 5, 6]);
|
|
|
|
expect(content.bytes, orderedEquals(<int>[4, 5, 6]));
|
|
expect(content.isModified, isTrue);
|
|
expect(content.isModified, isFalse);
|
|
content.bytes = <int>[7, 8, 9, 2];
|
|
expect(content.bytes, orderedEquals(<int>[7, 8, 9, 2]));
|
|
expect(content.isModified, isTrue);
|
|
expect(content.isModified, isFalse);
|
|
});
|
|
|
|
testWithoutContext('DevFSStringContent', () {
|
|
final DevFSStringContent content = DevFSStringContent('some string');
|
|
|
|
expect(content.string, 'some string');
|
|
expect(content.bytes, orderedEquals(utf8.encode('some string')));
|
|
expect(content.isModified, isTrue);
|
|
expect(content.isModified, isFalse);
|
|
content.string = 'another string';
|
|
expect(content.string, 'another string');
|
|
expect(content.bytes, orderedEquals(utf8.encode('another string')));
|
|
expect(content.isModified, isTrue);
|
|
expect(content.isModified, isFalse);
|
|
content.bytes = utf8.encode('foo bar');
|
|
expect(content.string, 'foo bar');
|
|
expect(content.bytes, orderedEquals(utf8.encode('foo bar')));
|
|
expect(content.isModified, isTrue);
|
|
expect(content.isModified, isFalse);
|
|
});
|
|
|
|
testWithoutContext('DevFSFileContent', () async {
|
|
final FileSystem fileSystem = MemoryFileSystem.test();
|
|
final File file = fileSystem.file('foo.txt');
|
|
final DevFSFileContent content = DevFSFileContent(file);
|
|
expect(content.isModified, isFalse);
|
|
expect(content.isModified, isFalse);
|
|
|
|
file.parent.createSync(recursive: true);
|
|
file.writeAsBytesSync(<int>[1, 2, 3], flush: true);
|
|
|
|
final DateTime fiveSecondsAgo = file.statSync().modified.subtract(const Duration(seconds: 5));
|
|
expect(content.isModifiedAfter(fiveSecondsAgo), isTrue);
|
|
expect(content.isModifiedAfter(fiveSecondsAgo), isTrue);
|
|
|
|
file.writeAsBytesSync(<int>[2, 3, 4], flush: true);
|
|
|
|
expect(content.isModified, isTrue);
|
|
expect(content.isModified, isFalse);
|
|
expect(await content.contentsAsBytes(), <int>[2, 3, 4]);
|
|
|
|
expect(content.isModified, isFalse);
|
|
expect(content.isModified, isFalse);
|
|
|
|
file.deleteSync();
|
|
expect(content.isModified, isTrue);
|
|
expect(content.isModified, isFalse);
|
|
expect(content.isModified, isFalse);
|
|
});
|
|
|
|
testWithoutContext('DevFSStringCompressingBytesContent', () {
|
|
final DevFSStringCompressingBytesContent content =
|
|
DevFSStringCompressingBytesContent('uncompressed string');
|
|
|
|
expect(content.equals('uncompressed string'), isTrue);
|
|
expect(content.bytes, isNotNull);
|
|
expect(content.isModified, isTrue);
|
|
expect(content.isModified, isFalse);
|
|
});
|
|
|
|
testWithoutContext('DevFS create throws a DevFSException when vmservice disconnects unexpectedly', () async {
|
|
final FileSystem fileSystem = MemoryFileSystem.test();
|
|
final OperatingSystemUtils osUtils = FakeOperatingSystemUtils();
|
|
final FakeVmServiceHost fakeVmServiceHost = FakeVmServiceHost(
|
|
requests: <VmServiceExpectation>[failingCreateDevFSRequest],
|
|
httpAddress: Uri.parse('http://localhost'),
|
|
);
|
|
|
|
final DevFS devFS = DevFS(
|
|
fakeVmServiceHost.vmService,
|
|
'test',
|
|
fileSystem.currentDirectory,
|
|
osUtils: osUtils,
|
|
fileSystem: fileSystem,
|
|
logger: BufferLogger.test(),
|
|
httpClient: FakeHttpClient.any(),
|
|
);
|
|
expect(() async => devFS.create(), throwsA(isA<DevFSException>()));
|
|
});
|
|
|
|
testWithoutContext('DevFS destroy is resilient to vmservice disconnection', () async {
|
|
final FileSystem fileSystem = MemoryFileSystem.test();
|
|
final OperatingSystemUtils osUtils = FakeOperatingSystemUtils();
|
|
final FakeVmServiceHost fakeVmServiceHost = FakeVmServiceHost(
|
|
requests: <VmServiceExpectation>[
|
|
createDevFSRequest,
|
|
failingDeleteDevFSRequest,
|
|
],
|
|
httpAddress: Uri.parse('http://localhost'),
|
|
);
|
|
|
|
final DevFS devFS = DevFS(
|
|
fakeVmServiceHost.vmService,
|
|
'test',
|
|
fileSystem.currentDirectory,
|
|
osUtils: osUtils,
|
|
fileSystem: fileSystem,
|
|
logger: BufferLogger.test(),
|
|
httpClient: FakeHttpClient.any(),
|
|
);
|
|
|
|
expect(await devFS.create(), isNotNull);
|
|
await devFS.destroy(); // Testing that this does not throw.
|
|
});
|
|
|
|
testWithoutContext('DevFS retries uploads when connection reset by peer', () async {
|
|
final FileSystem fileSystem = MemoryFileSystem.test();
|
|
final OperatingSystemUtils osUtils = OperatingSystemUtils(
|
|
fileSystem: fileSystem,
|
|
platform: FakePlatform(),
|
|
logger: BufferLogger.test(),
|
|
processManager: FakeProcessManager.any(),
|
|
);
|
|
final FakeResidentCompiler residentCompiler = FakeResidentCompiler();
|
|
final FakeVmServiceHost fakeVmServiceHost = FakeVmServiceHost(
|
|
requests: <VmServiceExpectation>[createDevFSRequest],
|
|
httpAddress: Uri.parse('http://localhost'),
|
|
);
|
|
residentCompiler.onRecompile = (Uri mainUri, List<Uri>? invalidatedFiles) async {
|
|
fileSystem.file('lib/foo.dill')
|
|
..createSync(recursive: true)
|
|
..writeAsBytesSync(<int>[1, 2, 3, 4, 5]);
|
|
return const CompilerOutput('lib/foo.dill', 0, <Uri>[]);
|
|
};
|
|
|
|
/// This output can change based on the host platform.
|
|
final List<List<int>> expectedEncoded = await osUtils.gzipLevel1Stream(
|
|
Stream<List<int>>.value(<int>[1, 2, 3, 4, 5]),
|
|
).toList();
|
|
|
|
final DevFS devFS = DevFS(
|
|
fakeVmServiceHost.vmService,
|
|
'test',
|
|
fileSystem.currentDirectory,
|
|
osUtils: osUtils,
|
|
fileSystem: fileSystem,
|
|
logger: BufferLogger.test(),
|
|
httpClient: FakeHttpClient.list(<FakeRequest>[
|
|
FakeRequest(Uri.parse('http://localhost'), method: HttpMethod.put, responseError: const OSError('Connection Reset by peer')),
|
|
FakeRequest(Uri.parse('http://localhost'), method: HttpMethod.put, responseError: const OSError('Connection Reset by peer')),
|
|
FakeRequest(Uri.parse('http://localhost'), method: HttpMethod.put, responseError: const OSError('Connection Reset by peer')),
|
|
FakeRequest(Uri.parse('http://localhost'), method: HttpMethod.put, responseError: const OSError('Connection Reset by peer')),
|
|
FakeRequest(Uri.parse('http://localhost'), method: HttpMethod.put, responseError: const OSError('Connection Reset by peer')),
|
|
// This is the value of `<int>[1, 2, 3, 4, 5]` run through `osUtils.gzipLevel1Stream`.
|
|
FakeRequest(Uri.parse('http://localhost'), method: HttpMethod.put, body: <int>[for (final List<int> chunk in expectedEncoded) ...chunk]),
|
|
]),
|
|
uploadRetryThrottle: Duration.zero,
|
|
);
|
|
await devFS.create();
|
|
|
|
final UpdateFSReport report = await devFS.update(
|
|
mainUri: Uri.parse('lib/foo.txt'),
|
|
dillOutputPath: 'lib/foo.dill',
|
|
generator: residentCompiler,
|
|
pathToReload: 'lib/foo.txt.dill',
|
|
trackWidgetCreation: false,
|
|
invalidatedFiles: <Uri>[],
|
|
packageConfig: PackageConfig.empty,
|
|
shaderCompiler: const FakeShaderCompiler(),
|
|
);
|
|
|
|
expect(report.syncedBytes, 5);
|
|
expect(report.success, isTrue);
|
|
});
|
|
|
|
testWithoutContext('DevFS reports unsuccessful compile when errors are returned', () async {
|
|
final FileSystem fileSystem = MemoryFileSystem.test();
|
|
final FakeVmServiceHost fakeVmServiceHost = FakeVmServiceHost(
|
|
requests: <VmServiceExpectation>[createDevFSRequest],
|
|
httpAddress: Uri.parse('http://localhost'),
|
|
);
|
|
|
|
final DevFS devFS = DevFS(
|
|
fakeVmServiceHost.vmService,
|
|
'test',
|
|
fileSystem.currentDirectory,
|
|
fileSystem: fileSystem,
|
|
logger: BufferLogger.test(),
|
|
osUtils: FakeOperatingSystemUtils(),
|
|
httpClient: FakeHttpClient.any(),
|
|
);
|
|
|
|
await devFS.create();
|
|
final DateTime? previousCompile = devFS.lastCompiled;
|
|
|
|
final FakeResidentCompiler residentCompiler = FakeResidentCompiler();
|
|
residentCompiler.onRecompile = (Uri mainUri, List<Uri>? invalidatedFiles) async {
|
|
return const CompilerOutput('lib/foo.dill', 2, <Uri>[]);
|
|
};
|
|
|
|
final UpdateFSReport report = await devFS.update(
|
|
mainUri: Uri.parse('lib/foo.txt'),
|
|
generator: residentCompiler,
|
|
dillOutputPath: 'lib/foo.dill',
|
|
pathToReload: 'lib/foo.txt.dill',
|
|
trackWidgetCreation: false,
|
|
invalidatedFiles: <Uri>[],
|
|
packageConfig: PackageConfig.empty,
|
|
shaderCompiler: const FakeShaderCompiler(),
|
|
);
|
|
|
|
expect(report.success, false);
|
|
expect(devFS.lastCompiled, previousCompile);
|
|
});
|
|
|
|
testWithoutContext('DevFS correctly updates last compiled time when compilation does not fail', () async {
|
|
final FileSystem fileSystem = MemoryFileSystem.test();
|
|
final FakeVmServiceHost fakeVmServiceHost = FakeVmServiceHost(
|
|
requests: <VmServiceExpectation>[createDevFSRequest],
|
|
httpAddress: Uri.parse('http://localhost'),
|
|
);
|
|
|
|
final DevFS devFS = DevFS(
|
|
fakeVmServiceHost.vmService,
|
|
'test',
|
|
fileSystem.currentDirectory,
|
|
fileSystem: fileSystem,
|
|
logger: BufferLogger.test(),
|
|
osUtils: FakeOperatingSystemUtils(),
|
|
httpClient: FakeHttpClient.any(),
|
|
);
|
|
|
|
await devFS.create();
|
|
final DateTime? previousCompile = devFS.lastCompiled;
|
|
|
|
final FakeResidentCompiler residentCompiler = FakeResidentCompiler();
|
|
residentCompiler.onRecompile = (Uri mainUri, List<Uri>? invalidatedFiles) async {
|
|
fileSystem.file('lib/foo.txt.dill').createSync(recursive: true);
|
|
return const CompilerOutput('lib/foo.txt.dill', 0, <Uri>[]);
|
|
};
|
|
|
|
final UpdateFSReport report = await devFS.update(
|
|
mainUri: Uri.parse('lib/main.dart'),
|
|
generator: residentCompiler,
|
|
dillOutputPath: 'lib/foo.dill',
|
|
pathToReload: 'lib/foo.txt.dill',
|
|
trackWidgetCreation: false,
|
|
invalidatedFiles: <Uri>[],
|
|
packageConfig: PackageConfig.empty,
|
|
shaderCompiler: const FakeShaderCompiler(),
|
|
);
|
|
|
|
expect(report.success, true);
|
|
expect(devFS.lastCompiled, isNot(previousCompile));
|
|
});
|
|
|
|
testWithoutContext('DevFS can reset compilation time', () async {
|
|
final FileSystem fileSystem = MemoryFileSystem.test();
|
|
final FakeVmServiceHost fakeVmServiceHost = FakeVmServiceHost(
|
|
requests: <VmServiceExpectation>[createDevFSRequest],
|
|
);
|
|
final LocalDevFSWriter localDevFSWriter = LocalDevFSWriter(fileSystem: fileSystem);
|
|
fileSystem.directory('test').createSync();
|
|
|
|
final DevFS devFS = DevFS(
|
|
fakeVmServiceHost.vmService,
|
|
'test',
|
|
fileSystem.currentDirectory,
|
|
fileSystem: fileSystem,
|
|
logger: BufferLogger.test(),
|
|
osUtils: FakeOperatingSystemUtils(),
|
|
httpClient: HttpClient(),
|
|
);
|
|
|
|
await devFS.create();
|
|
final DateTime? previousCompile = devFS.lastCompiled;
|
|
|
|
final FakeResidentCompiler residentCompiler = FakeResidentCompiler();
|
|
residentCompiler.onRecompile = (Uri mainUri, List<Uri>? invalidatedFiles) async {
|
|
fileSystem.file('lib/foo.txt.dill').createSync(recursive: true);
|
|
return const CompilerOutput('lib/foo.txt.dill', 0, <Uri>[]);
|
|
};
|
|
|
|
final UpdateFSReport report = await devFS.update(
|
|
mainUri: Uri.parse('lib/main.dart'),
|
|
generator: residentCompiler,
|
|
dillOutputPath: 'lib/foo.dill',
|
|
pathToReload: 'lib/foo.txt.dill',
|
|
trackWidgetCreation: false,
|
|
invalidatedFiles: <Uri>[],
|
|
packageConfig: PackageConfig.empty,
|
|
devFSWriter: localDevFSWriter,
|
|
shaderCompiler: const FakeShaderCompiler(),
|
|
);
|
|
|
|
expect(report.success, true);
|
|
expect(devFS.lastCompiled, isNot(previousCompile));
|
|
|
|
devFS.resetLastCompiled();
|
|
expect(devFS.lastCompiled, previousCompile);
|
|
|
|
// Does not reset to report compile time.
|
|
devFS.resetLastCompiled();
|
|
expect(devFS.lastCompiled, previousCompile);
|
|
});
|
|
|
|
testWithoutContext('DevFS uses provided DevFSWriter instead of default HTTP writer', () async {
|
|
final FileSystem fileSystem = MemoryFileSystem.test();
|
|
final FakeDevFSWriter writer = FakeDevFSWriter();
|
|
final FakeVmServiceHost fakeVmServiceHost = FakeVmServiceHost(
|
|
requests: <VmServiceExpectation>[createDevFSRequest],
|
|
);
|
|
|
|
final DevFS devFS = DevFS(
|
|
fakeVmServiceHost.vmService,
|
|
'test',
|
|
fileSystem.currentDirectory,
|
|
fileSystem: fileSystem,
|
|
logger: BufferLogger.test(),
|
|
osUtils: FakeOperatingSystemUtils(),
|
|
httpClient: FakeHttpClient.any(),
|
|
);
|
|
|
|
await devFS.create();
|
|
|
|
final FakeResidentCompiler residentCompiler = FakeResidentCompiler();
|
|
residentCompiler.onRecompile = (Uri mainUri, List<Uri>? invalidatedFiles) async {
|
|
fileSystem.file('example').createSync();
|
|
return const CompilerOutput('lib/foo.txt.dill', 0, <Uri>[]);
|
|
};
|
|
|
|
expect(writer.written, false);
|
|
|
|
final UpdateFSReport report = await devFS.update(
|
|
mainUri: Uri.parse('lib/main.dart'),
|
|
generator: residentCompiler,
|
|
dillOutputPath: 'lib/foo.dill',
|
|
pathToReload: 'lib/foo.txt.dill',
|
|
trackWidgetCreation: false,
|
|
invalidatedFiles: <Uri>[],
|
|
packageConfig: PackageConfig.empty,
|
|
devFSWriter: writer,
|
|
shaderCompiler: const FakeShaderCompiler(),
|
|
);
|
|
|
|
expect(report.success, true);
|
|
expect(writer.written, true);
|
|
});
|
|
|
|
testWithoutContext('Local DevFSWriter can copy and write files', () async {
|
|
final FileSystem fileSystem = MemoryFileSystem.test();
|
|
final File file = fileSystem.file('foo_bar')
|
|
..writeAsStringSync('goodbye');
|
|
final LocalDevFSWriter writer = LocalDevFSWriter(fileSystem: fileSystem);
|
|
|
|
await writer.write(<Uri, DevFSContent>{
|
|
Uri.parse('hello'): DevFSStringContent('hello'),
|
|
Uri.parse('goodbye'): DevFSFileContent(file),
|
|
}, Uri.parse('/foo/bar/devfs/'));
|
|
|
|
expect(fileSystem.file('/foo/bar/devfs/hello'), exists);
|
|
expect(fileSystem.file('/foo/bar/devfs/hello').readAsStringSync(), 'hello');
|
|
expect(fileSystem.file('/foo/bar/devfs/goodbye'), exists);
|
|
expect(fileSystem.file('/foo/bar/devfs/goodbye').readAsStringSync(), 'goodbye');
|
|
});
|
|
|
|
testWithoutContext('Local DevFSWriter turns FileSystemException into DevFSException', () async {
|
|
final FileExceptionHandler handler = FileExceptionHandler();
|
|
final FileSystem fileSystem = MemoryFileSystem.test(opHandle: handler.opHandle);
|
|
final LocalDevFSWriter writer = LocalDevFSWriter(fileSystem: fileSystem);
|
|
final File file = fileSystem.file('foo');
|
|
handler.addError(file, FileSystemOp.read, const FileSystemException('foo'));
|
|
|
|
await expectLater(() async => writer.write(<Uri, DevFSContent>{
|
|
Uri.parse('goodbye'): DevFSFileContent(file),
|
|
}, Uri.parse('/foo/bar/devfs/')), throwsA(isA<DevFSException>()));
|
|
});
|
|
|
|
testWithoutContext('DevFS correctly records the elapsed time', () async {
|
|
final FileSystem fileSystem = MemoryFileSystem.test();
|
|
// final FakeDevFSWriter writer = FakeDevFSWriter();
|
|
final FakeVmServiceHost fakeVmServiceHost = FakeVmServiceHost(
|
|
requests: <VmServiceExpectation>[createDevFSRequest],
|
|
httpAddress: Uri.parse('http://localhost'),
|
|
);
|
|
|
|
final DevFS devFS = DevFS(
|
|
fakeVmServiceHost.vmService,
|
|
'test',
|
|
fileSystem.currentDirectory,
|
|
fileSystem: fileSystem,
|
|
logger: BufferLogger.test(),
|
|
osUtils: FakeOperatingSystemUtils(),
|
|
httpClient: FakeHttpClient.any(),
|
|
stopwatchFactory: FakeStopwatchFactory(stopwatches: <String, Stopwatch>{
|
|
'compile': FakeStopwatch()..elapsed = const Duration(seconds: 3),
|
|
'transfer': FakeStopwatch()..elapsed = const Duration(seconds: 5),
|
|
}),
|
|
);
|
|
|
|
await devFS.create();
|
|
|
|
final FakeResidentCompiler residentCompiler = FakeResidentCompiler();
|
|
residentCompiler.onRecompile = (Uri mainUri, List<Uri>? invalidatedFiles) async {
|
|
fileSystem.file('lib/foo.txt.dill').createSync(recursive: true);
|
|
return const CompilerOutput('lib/foo.txt.dill', 0, <Uri>[]);
|
|
};
|
|
|
|
final UpdateFSReport report = await devFS.update(
|
|
mainUri: Uri.parse('lib/main.dart'),
|
|
generator: residentCompiler,
|
|
dillOutputPath: 'lib/foo.dill',
|
|
pathToReload: 'lib/foo.txt.dill',
|
|
trackWidgetCreation: false,
|
|
invalidatedFiles: <Uri>[],
|
|
packageConfig: PackageConfig.empty,
|
|
shaderCompiler: const FakeShaderCompiler(),
|
|
);
|
|
|
|
expect(report.success, true);
|
|
expect(report.compileDuration, const Duration(seconds: 3));
|
|
expect(report.transferDuration, const Duration(seconds: 5));
|
|
});
|
|
|
|
|
|
testUsingContext('DevFS actually starts compile before processing bundle', () async {
|
|
final FileSystem fileSystem = MemoryFileSystem.test();
|
|
final FakeVmServiceHost fakeVmServiceHost = FakeVmServiceHost(
|
|
requests: <VmServiceExpectation>[createDevFSRequest],
|
|
httpAddress: Uri.parse('http://localhost'),
|
|
);
|
|
|
|
final LoggingLogger logger = LoggingLogger();
|
|
|
|
final DevFS devFS = DevFS(
|
|
fakeVmServiceHost.vmService,
|
|
'test',
|
|
fileSystem.currentDirectory,
|
|
fileSystem: fileSystem,
|
|
logger: logger,
|
|
osUtils: FakeOperatingSystemUtils(),
|
|
httpClient: FakeHttpClient.any(),
|
|
);
|
|
|
|
await devFS.create();
|
|
|
|
final MemoryIOSink frontendServerStdIn = MemoryIOSink();
|
|
Stream<List<int>> frontendServerStdOut() async* {
|
|
int processed = 0;
|
|
while (true) {
|
|
while (frontendServerStdIn.writes.length == processed) {
|
|
await Future<dynamic>.delayed(const Duration(milliseconds: 5));
|
|
}
|
|
|
|
String? boundaryKey;
|
|
while (processed < frontendServerStdIn.writes.length) {
|
|
final List<int> data = frontendServerStdIn.writes[processed];
|
|
final String stringData = utf8.decode(data);
|
|
if (stringData.startsWith('compile ')) {
|
|
yield utf8.encode('result abc1\nline1\nline2\nabc1\nabc1 lib/foo.txt.dill 0\n');
|
|
} else if (stringData.startsWith('recompile ')) {
|
|
final String line = stringData.split('\n').first;
|
|
final int spaceDelim = line.lastIndexOf(' ');
|
|
boundaryKey = line.substring(spaceDelim + 1);
|
|
} else if (boundaryKey != null && stringData.startsWith(boundaryKey)) {
|
|
yield utf8.encode('result abc2\nline1\nline2\nabc2\nabc2 lib/foo.txt.dill 0\n');
|
|
} else {
|
|
throw Exception('Saw $data ($stringData)');
|
|
}
|
|
processed++;
|
|
}
|
|
}
|
|
}
|
|
Stream<List<int>> frontendServerStdErr() async* {
|
|
// Output nothing on stderr.
|
|
}
|
|
|
|
final AnsweringFakeProcessManager fakeProcessManager = AnsweringFakeProcessManager(frontendServerStdOut(), frontendServerStdErr(), frontendServerStdIn);
|
|
final StdoutHandler generatorStdoutHandler = StdoutHandler(logger: testLogger, fileSystem: fileSystem);
|
|
|
|
final DefaultResidentCompiler residentCompiler = DefaultResidentCompiler(
|
|
'sdkroot',
|
|
buildMode: BuildMode.debug,
|
|
logger: logger,
|
|
processManager: fakeProcessManager,
|
|
artifacts: Artifacts.test(),
|
|
platform: FakePlatform(),
|
|
fileSystem: fileSystem,
|
|
stdoutHandler: generatorStdoutHandler,
|
|
);
|
|
|
|
fileSystem.file('lib/foo.txt.dill').createSync(recursive: true);
|
|
|
|
final UpdateFSReport report1 = await devFS.update(
|
|
mainUri: Uri.parse('lib/main.dart'),
|
|
generator: residentCompiler,
|
|
dillOutputPath: 'lib/foo.dill',
|
|
pathToReload: 'lib/foo.txt.dill',
|
|
trackWidgetCreation: false,
|
|
invalidatedFiles: <Uri>[],
|
|
packageConfig: PackageConfig.empty,
|
|
bundle: FakeBundle(),
|
|
shaderCompiler: const FakeShaderCompiler(),
|
|
);
|
|
expect(report1.success, true);
|
|
logger.messages.clear();
|
|
|
|
final UpdateFSReport report2 = await devFS.update(
|
|
mainUri: Uri.parse('lib/main.dart'),
|
|
generator: residentCompiler,
|
|
dillOutputPath: 'lib/foo.dill',
|
|
pathToReload: 'lib/foo.txt.dill',
|
|
trackWidgetCreation: false,
|
|
invalidatedFiles: <Uri>[],
|
|
packageConfig: PackageConfig.empty,
|
|
bundle: FakeBundle(),
|
|
shaderCompiler: const FakeShaderCompiler(),
|
|
);
|
|
expect(report2.success, true);
|
|
|
|
final int processingBundleIndex = logger.messages.indexOf('Processing bundle.');
|
|
final int bundleProcessingDoneIndex = logger.messages.indexOf('Bundle processing done.');
|
|
final int compileLibMainIndex = logger.messages.indexWhere((String element) => element.startsWith('<- recompile lib/main.dart '));
|
|
expect(processingBundleIndex, greaterThanOrEqualTo(0));
|
|
expect(bundleProcessingDoneIndex, greaterThanOrEqualTo(0));
|
|
expect(compileLibMainIndex, greaterThanOrEqualTo(0));
|
|
expect(bundleProcessingDoneIndex, greaterThan(compileLibMainIndex));
|
|
});
|
|
|
|
group('Shader compilation', () {
|
|
late FileSystem fileSystem;
|
|
late ProcessManager processManager;
|
|
|
|
setUp(() {
|
|
fileSystem = MemoryFileSystem.test();
|
|
processManager = FakeProcessManager.any();
|
|
});
|
|
|
|
testUsingContext('DevFS recompiles shaders', () async {
|
|
final FakeVmServiceHost fakeVmServiceHost = FakeVmServiceHost(
|
|
requests: <VmServiceExpectation>[createDevFSRequest],
|
|
httpAddress: Uri.parse('http://localhost'),
|
|
);
|
|
final BufferLogger logger = BufferLogger.test();
|
|
final DevFS devFS = DevFS(
|
|
fakeVmServiceHost.vmService,
|
|
'test',
|
|
fileSystem.currentDirectory,
|
|
fileSystem: fileSystem,
|
|
logger: logger,
|
|
osUtils: FakeOperatingSystemUtils(),
|
|
httpClient: FakeHttpClient.any(),
|
|
);
|
|
|
|
await devFS.create();
|
|
|
|
final FakeResidentCompiler residentCompiler = FakeResidentCompiler()
|
|
..onRecompile = (Uri mainUri, List<Uri>? invalidatedFiles) async {
|
|
fileSystem.file('lib/foo.dill')
|
|
..createSync(recursive: true)
|
|
..writeAsBytesSync(<int>[1, 2, 3, 4, 5]);
|
|
return const CompilerOutput('lib/foo.dill', 0, <Uri>[]);
|
|
};
|
|
final FakeBundle bundle = FakeBundle()
|
|
..entries['foo.frag'] = DevFSByteContent(<int>[1, 2, 3, 4])
|
|
..entries['not.frag'] = DevFSByteContent(<int>[1, 2, 3, 4])
|
|
..entryKinds['foo.frag'] = AssetKind.shader;
|
|
|
|
final UpdateFSReport report = await devFS.update(
|
|
mainUri: Uri.parse('lib/main.dart'),
|
|
generator: residentCompiler,
|
|
dillOutputPath: 'lib/foo.dill',
|
|
pathToReload: 'lib/foo.txt.dill',
|
|
trackWidgetCreation: false,
|
|
invalidatedFiles: <Uri>[],
|
|
packageConfig: PackageConfig.empty,
|
|
shaderCompiler: const FakeShaderCompiler(),
|
|
bundle: bundle,
|
|
);
|
|
|
|
expect(report.success, true);
|
|
expect(devFS.shaderPathsToEvict, <String>{'foo.frag'});
|
|
expect(devFS.assetPathsToEvict, <String>{'not.frag'});
|
|
}, overrides: <Type, Generator>{
|
|
FileSystem: () => fileSystem,
|
|
ProcessManager: () => processManager,
|
|
});
|
|
|
|
testUsingContext('DevFS tracks when FontManifest is updated', () async {
|
|
final FakeVmServiceHost fakeVmServiceHost = FakeVmServiceHost(
|
|
requests: <VmServiceExpectation>[createDevFSRequest],
|
|
httpAddress: Uri.parse('http://localhost'),
|
|
);
|
|
final BufferLogger logger = BufferLogger.test();
|
|
final DevFS devFS = DevFS(
|
|
fakeVmServiceHost.vmService,
|
|
'test',
|
|
fileSystem.currentDirectory,
|
|
fileSystem: fileSystem,
|
|
logger: logger,
|
|
osUtils: FakeOperatingSystemUtils(),
|
|
httpClient: FakeHttpClient.any(),
|
|
);
|
|
|
|
await devFS.create();
|
|
|
|
expect(devFS.didUpdateFontManifest, false);
|
|
|
|
final FakeResidentCompiler residentCompiler = FakeResidentCompiler()
|
|
..onRecompile = (Uri mainUri, List<Uri>? invalidatedFiles) async {
|
|
fileSystem.file('lib/foo.dill')
|
|
..createSync(recursive: true)
|
|
..writeAsBytesSync(<int>[1, 2, 3, 4, 5]);
|
|
return const CompilerOutput('lib/foo.dill', 0, <Uri>[]);
|
|
};
|
|
final FakeBundle bundle = FakeBundle()
|
|
..entries['FontManifest.json'] = DevFSByteContent(<int>[1, 2, 3, 4]);
|
|
|
|
final UpdateFSReport report = await devFS.update(
|
|
mainUri: Uri.parse('lib/main.dart'),
|
|
generator: residentCompiler,
|
|
dillOutputPath: 'lib/foo.dill',
|
|
pathToReload: 'lib/foo.txt.dill',
|
|
trackWidgetCreation: false,
|
|
invalidatedFiles: <Uri>[],
|
|
packageConfig: PackageConfig.empty,
|
|
shaderCompiler: const FakeShaderCompiler(),
|
|
bundle: bundle,
|
|
);
|
|
|
|
expect(report.success, true);
|
|
expect(devFS.shaderPathsToEvict, <String>{});
|
|
expect(devFS.assetPathsToEvict, <String>{'FontManifest.json'});
|
|
expect(devFS.didUpdateFontManifest, true);
|
|
}, overrides: <Type, Generator>{
|
|
FileSystem: () => fileSystem,
|
|
ProcessManager: () => processManager,
|
|
});
|
|
});
|
|
}
|
|
|
|
class FakeResidentCompiler extends Fake implements ResidentCompiler {
|
|
Future<CompilerOutput> Function(Uri mainUri, List<Uri>? invalidatedFiles)? onRecompile;
|
|
|
|
@override
|
|
Future<CompilerOutput> recompile(
|
|
Uri mainUri,
|
|
List<Uri>? invalidatedFiles, {
|
|
String? outputPath,
|
|
PackageConfig? packageConfig,
|
|
String? projectRootPath,
|
|
FileSystem? fs,
|
|
bool suppressErrors = false,
|
|
bool checkDartPluginRegistry = false,
|
|
File? dartPluginRegistrant,
|
|
Uri? nativeAssetsYaml,
|
|
}) {
|
|
return onRecompile?.call(mainUri, invalidatedFiles)
|
|
?? Future<CompilerOutput>.value(const CompilerOutput('', 1, <Uri>[]));
|
|
}
|
|
}
|
|
|
|
class FakeDevFSWriter implements DevFSWriter {
|
|
bool written = false;
|
|
|
|
@override
|
|
Future<void> write(Map<Uri, DevFSContent> entries, Uri baseUri, DevFSWriter parent) async {
|
|
written = true;
|
|
}
|
|
}
|
|
|
|
class FakeBundle extends AssetBundle {
|
|
@override
|
|
List<File> get additionalDependencies => <File>[];
|
|
|
|
@override
|
|
Future<int> build({String manifestPath = defaultManifestPath, String? assetDirPath, String? packagesPath, bool deferredComponentsEnabled = false, TargetPlatform? targetPlatform}) async {
|
|
return 0;
|
|
}
|
|
|
|
@override
|
|
Map<String, Map<String, DevFSContent>> get deferredComponentsEntries => <String, Map<String, DevFSContent>>{};
|
|
|
|
@override
|
|
final Map<String, DevFSContent> entries = <String, DevFSContent>{};
|
|
|
|
@override
|
|
final Map<String, AssetKind> entryKinds = <String, AssetKind>{};
|
|
|
|
@override
|
|
List<File> get inputFiles => <File>[];
|
|
|
|
@override
|
|
bool needsBuild({String manifestPath = defaultManifestPath}) {
|
|
return true;
|
|
}
|
|
|
|
@override
|
|
bool wasBuiltOnce() {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
class AnsweringFakeProcessManager implements ProcessManager {
|
|
AnsweringFakeProcessManager(this.stdout, this.stderr, this.stdin);
|
|
|
|
final Stream<List<int>> stdout;
|
|
final Stream<List<int>> stderr;
|
|
final IOSink stdin;
|
|
|
|
@override
|
|
bool canRun(dynamic executable, {String? workingDirectory}) {
|
|
return true;
|
|
}
|
|
|
|
@override
|
|
bool killPid(int pid, [io.ProcessSignal signal = io.ProcessSignal.sigterm]) {
|
|
return true;
|
|
}
|
|
|
|
@override
|
|
Future<ProcessResult> run(List<Object> command, {String? workingDirectory, Map<String, String>? environment, bool includeParentEnvironment = true, bool runInShell = false, Encoding? stdoutEncoding = systemEncoding, Encoding? stderrEncoding = systemEncoding}) async {
|
|
throw UnimplementedError();
|
|
}
|
|
|
|
@override
|
|
ProcessResult runSync(List<Object> command, {String? workingDirectory, Map<String, String>? environment, bool includeParentEnvironment = true, bool runInShell = false, Encoding? stdoutEncoding = systemEncoding, Encoding? stderrEncoding = systemEncoding}) {
|
|
throw UnimplementedError();
|
|
}
|
|
|
|
@override
|
|
Future<Process> start(List<Object> command, {String? workingDirectory, Map<String, String>? environment, bool includeParentEnvironment = true, bool runInShell = false, ProcessStartMode mode = ProcessStartMode.normal}) async {
|
|
return AnsweringFakeProcess(stdout, stderr, stdin);
|
|
}
|
|
}
|
|
|
|
class AnsweringFakeProcess implements io.Process {
|
|
AnsweringFakeProcess(this.stdout,this.stderr, this.stdin);
|
|
|
|
@override
|
|
final Stream<List<int>> stdout;
|
|
@override
|
|
final Stream<List<int>> stderr;
|
|
@override
|
|
final IOSink stdin;
|
|
|
|
@override
|
|
Future<int> get exitCode async => 0;
|
|
|
|
@override
|
|
bool kill([io.ProcessSignal signal = io.ProcessSignal.sigterm]) {
|
|
return true;
|
|
}
|
|
|
|
@override
|
|
int get pid => 42;
|
|
}
|
|
|
|
class FakeShaderCompiler implements DevelopmentShaderCompiler {
|
|
const FakeShaderCompiler();
|
|
|
|
@override
|
|
void configureCompiler(
|
|
TargetPlatform? platform, {
|
|
required ImpellerStatus impellerStatus,
|
|
}) { }
|
|
|
|
@override
|
|
Future<DevFSContent> recompileShader(DevFSContent inputShader) async {
|
|
return DevFSByteContent(await inputShader.contentsAsBytes());
|
|
}
|
|
}
|