blob: 617675748c1fd161c3c34f0b3ed6cd20335df806 [file] [log] [blame]
// Copyright 2022 The Fuchsia 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 'package:fuchsia_component_test/realm_builder.dart';
import 'package:fidl/fidl.dart' as fidl;
import 'package:fidl_fidl_examples_routing_echo/fidl_async.dart' as fecho;
import 'package:fidl_fuchsia_component/fidl_async.dart' as fcomponent;
import 'package:fidl_fuchsia_component_test/fidl_async.dart' as fctest;
import 'package:fidl_fuchsia_component_decl/fidl_async.dart' as fdecl;
import 'package:fidl_fuchsia_io/fidl_async.dart' as fio;
import 'package:fidl_fuchsia_logger/fidl_async.dart' as flogger;
import 'package:fuchsia_logger/logger.dart';
import 'package:fuchsia_services/services.dart' as services;
import 'package:test/test.dart';
import 'dart:convert' show utf8;
import 'dart:typed_data';
const String v1EchoClientUrl =
'fuchsia-pkg://fuchsia.com/dart_realm_builder_unittests#meta/echo_client.cmx';
const String v2EchoClientUrl = '#meta/echo_client.cm';
const String v1EchoServerUrl =
'fuchsia-pkg://fuchsia.com/dart_realm_builder_unittests#meta/echo_server.cmx';
const String v2EchoServerUrl = '#meta/echo_server.cm';
const String v2EchoClientWithBinderUrl = '#meta/echo_client_with_binder.cm';
const String v2EchoClientWithBinderArg = 'Hello Fuchsia!';
const String v2EchoClientStructuredConfigUrl = '#meta/echo_client_sc.cm';
void checkCommonExceptions(Exception err, StackTrace stacktrace) {
if (err is fidl.MethodException<fcomponent.Error>) {
late final String errorName;
for (final name in fcomponent.Error.$valuesMap.keys) {
if (err.value == fcomponent.Error.$valuesMap[name]) {
errorName = name;
break;
}
}
log.warning('fidl.$err: fuchsia.component.Error.$errorName');
} else if (err is fidl.MethodException<fctest.RealmBuilderError>) {
late final String errorName;
for (final name in fctest.RealmBuilderError.$valuesMap.keys) {
if (err.value == fctest.RealmBuilderError.$valuesMap[name]) {
errorName = name;
break;
}
}
log.warning('fidl.$err: fuchsia.component.test.Error.$errorName');
} else if (err is fidl.MethodException) {
log.warning('fidl.MethodException<${err.value.runtimeType}>($err)');
} else if (err is fidl.FidlError) {
log.warning('fidl.${err.runtimeType}($err), FidlErrorCode: ${err.code}');
} else {
log.warning('caught exception: ${err.runtimeType}($err)');
}
log.warning('stacktrace (if available)...\n${stacktrace.toString()}');
}
void main() {
setupLogger(name: 'fuchsia-component-test-dart-tests');
group('realm builder tests', () {
group('RealmBuilder with CFv2 child', () {
test('RealmBuilder.create()', () async {
final builder = await RealmBuilder.create();
expect(
builder.addChild(
'v2EchoServer',
v2EchoServerUrl,
ChildOptions()..eager(),
),
completes,
);
});
test('RealmBuilder with legacy (CFv1) child', () async {
final builder = await RealmBuilder.create();
expect(
builder.addLegacyChild(
'v1EchoServer',
v1EchoServerUrl,
ChildOptions(),
),
completes,
);
});
test('RealmBuilder from Component decl', () async {
final builder = await RealmBuilder.create();
final decl = fdecl.Component();
expect(
builder.addChildFromDecl(
'componentFromDecl',
decl,
ChildOptions(),
),
completes,
);
});
});
group('basic RealmBuilder tests', () {
test('protocol and directory capabilities', () async {
RealmInstance? realmInstance;
try {
final builder = await RealmBuilder.create();
final v2EchoServer = await builder.addChild(
'v2EchoServer',
v2EchoServerUrl,
);
await builder.addRoute(Route()
..capability(DirectoryCapability('hub')..rights = fio.rStarDir)
..from(Ref.framework())
..to(Ref.parent()));
await builder.addRoute(Route()
..capability(ProtocolCapability(flogger.LogSink.$serviceName))
..from(Ref.parent())
..to(Ref.child(v2EchoServer)));
await builder.addRoute(Route()
..capability(ProtocolCapability(fecho.Echo.$serviceName))
..from(Ref.child(v2EchoServer))
..to(Ref.parent()));
realmInstance = await builder.build();
final echo = realmInstance.root
.connectToProtocolAtExposedDir(fecho.EchoProxy());
const testString = 'ECHO...Echo...echo...(echo)...';
final reply = await echo.echoString(testString);
expect(testString, reply);
} on Exception catch (err, stacktrace) {
checkCommonExceptions(err, stacktrace);
rethrow;
} finally {
if (realmInstance != null) {
realmInstance.root.close();
}
}
});
test('connectToNamedProtocol', () async {
RealmInstance? realmInstance;
try {
final builder = await RealmBuilder.create();
final v2EchoServer = await builder.addChild(
'v2EchoServer',
v2EchoServerUrl,
);
await builder.addRoute(Route()
..capability(ProtocolCapability(flogger.LogSink.$serviceName))
..from(Ref.parent())
..to(Ref.child(v2EchoServer)));
await builder.addRoute(Route()
..capability(ProtocolCapability(fecho.Echo.$serviceName))
..from(Ref.child(v2EchoServer))
..to(Ref.parent()));
realmInstance = await builder.build();
final echo = fecho.EchoProxy();
realmInstance.root.connectToNamedProtocolAtExposedDir(
fecho.Echo.$serviceName,
echo.ctrl.request().passChannel()!,
);
const testString = 'ECHO...Echo...echo...(echo)...';
final reply = await echo.echoString(testString);
expect(testString, reply);
} finally {
if (realmInstance != null) {
realmInstance.root.close();
}
}
});
test('connectToProtocol using legacy component', () async {
RealmInstance? realmInstance;
try {
final builder = await RealmBuilder.create();
final v1EchoServer = await builder.addLegacyChild(
'v1EchoServer',
v1EchoServerUrl,
);
await builder.addRoute(Route()
..capability(ProtocolCapability(flogger.LogSink.$serviceName))
..from(Ref.parent())
..to(Ref.child(v1EchoServer)));
await builder.addRoute(Route()
..capability(ProtocolCapability(fecho.Echo.$serviceName))
..from(Ref.child(v1EchoServer))
..to(Ref.parent()));
realmInstance = await builder.build();
final echo = realmInstance.root
.connectToProtocolAtExposedDir(fecho.EchoProxy());
const testString = 'ECHO...Echo...echo...(echo)...';
final reply = await echo.echoString(testString);
expect(testString, reply);
} finally {
if (realmInstance != null) {
realmInstance.root.close();
}
}
});
});
test('connect to child in subrealm', () async {
RealmInstance? realmInstance;
try {
const subRealmName = 'sub_realm';
final realmBuilder = await RealmBuilder.create();
final subRealmBuilder = await realmBuilder.addChildRealm(subRealmName);
final v2EchoServer = await subRealmBuilder.addChild(
'v2EchoServer',
v2EchoServerUrl,
);
// Route LogSink from RealmBuilder to the subRealm.
await realmBuilder.addRoute(Route()
..capability(ProtocolCapability(flogger.LogSink.$serviceName))
..from(Ref.parent())
..to(Ref.childFromSubRealm(subRealmBuilder)));
// Route LogSink from the subRealm to the Echo component.
await subRealmBuilder.addRoute(Route()
..capability(ProtocolCapability(flogger.LogSink.$serviceName))
..from(Ref.parent())
..to(Ref.child(v2EchoServer)));
// Route the Echo service from the Echo child component to its parent
// (the subRealm).
await subRealmBuilder.addRoute(Route()
..capability(ProtocolCapability(fecho.Echo.$serviceName))
..from(Ref.child(v2EchoServer))
..to(Ref.parent()));
// Route the Echo service from the subRealm child to its parent
// (the RealmBuilder).
await realmBuilder.addRoute(Route()
..capability(ProtocolCapability(fecho.Echo.$serviceName))
..from(Ref.childFromSubRealm(subRealmBuilder))
..to(Ref.parent()));
realmInstance = await realmBuilder.build();
final echo = fecho.EchoProxy();
realmInstance.root.connectToNamedProtocolAtExposedDir(
fecho.Echo.$serviceName,
echo.ctrl.request().passChannel()!,
);
const testString = 'ECHO...Echo...echo...(echo)...';
final reply = await echo.echoString(testString);
expect(testString, reply);
} finally {
if (realmInstance != null) {
realmInstance.root.close();
}
}
});
test('storage', () async {
RealmInstance? realmInstance;
try {
final builder = await RealmBuilder.create();
const localStorageClientName = 'localStorageUser';
final localChildCompleter = Completer();
final localStorageClient = await builder.addLocalChild(
localStorageClientName,
options: ChildOptions()..eager(),
onRun: (handles, onStop) async {
var dataDir = handles.cloneFromNamespace('data');
final exampleFile = fio.FileProxy();
await dataDir.open(
fio.OpenFlags.rightReadable |
fio.OpenFlags.rightWritable |
fio.OpenFlags.create |
fio.OpenFlags.describe,
fio.modeTypeFile,
'example_file',
fidl.InterfaceRequest<fio.Node>(
exampleFile.ctrl.request().passChannel()));
const exampleData = 'example data';
final encodedData = utf8.encode(exampleData);
await exampleFile.write(utf8.encode(exampleData) as Uint8List);
await exampleFile.seek(fio.SeekOrigin.start, 0);
final fileContents =
utf8.decode(await exampleFile.read(encodedData.length));
if (exampleData != fileContents) {
localChildCompleter.completeError(
Exception('Wrote "$exampleData" but read back "$fileContents"'),
);
return;
}
localChildCompleter.complete(); // success!
},
);
await builder.addRoute(Route()
..capability(StorageCapability('data')..path = '/data')
..from(Ref.parent())
..to(Ref.child(localStorageClient)));
realmInstance = await builder.build();
await localChildCompleter.future;
} finally {
if (realmInstance != null) {
realmInstance.root.close();
}
}
});
group('API checks', () {
test('default ChildOptions', () {
final foptions = ChildOptions().toFidlType();
expect(foptions.startup, fdecl.StartupMode.lazy);
expect(foptions.environment, isNull);
expect(foptions.onTerminate, fdecl.OnTerminate.none);
});
test('set ChildOptions', () {
const envName = 'someEnv';
final childOptions = ChildOptions()
..eager()
..rebootOnTerminate()
..environment = envName;
final foptions = childOptions.toFidlType();
expect(foptions.startup, fdecl.StartupMode.eager);
expect(foptions.environment, envName);
expect(foptions.onTerminate, fdecl.OnTerminate.reboot);
});
test('named ScopedInstance', () async {
const collectionName = 'someCollection';
final fac = ScopedInstanceFactory(collectionName);
expect(fac.collectionName, collectionName);
});
test('ScopedInstance collectionName not found', () async {
final fac = ScopedInstanceFactory('badCollectionName');
ScopedInstance? scopedInstance;
try {
scopedInstance = await fac.newNamedInstance('someChild', 'badUrl');
} on fidl.MethodException<fcomponent.Error> catch (err) {
expect(err.value, fcomponent.Error.invalidArguments);
} finally {
expect(scopedInstance, isNull);
}
var caught = false;
try {
scopedInstance =
await fac.newNamedInstance('someChild', '#meta/someComponent.cm');
} on fidl.MethodException<fcomponent.Error> catch (err) {
expect(err.value, fcomponent.Error.collectionNotFound);
caught = true;
} finally {
expect(caught, true);
}
expect(scopedInstance, isNull);
});
});
group('ScopedInstance checks', () {
test('defaults', () async {
final builder = await RealmBuilder.create();
final realmInstance = await builder.build();
final scopedInstance = realmInstance.root;
expect(scopedInstance.collectionName, defaultCollectionName);
expect(scopedInstance.childName, startsWith('auto-'));
final nodeInfo = await scopedInstance.exposedDir.describe();
expect(nodeInfo.$tag, fio.NodeInfoTag.directory);
scopedInstance.close();
});
});
test('replace realm decl', () async {
try {
final builder = await RealmBuilder.create();
var rootDecl = await builder.getRealmDecl();
expect(rootDecl, fdecl.Component());
final children = (rootDecl.children != null
? rootDecl.children!.toList()
: <fdecl.Child>[])
..add(
fdecl.Child(
name: 'example-child',
url: 'example://url',
startup: fdecl.StartupMode.eager,
),
);
rootDecl = rootDecl.$cloneWith(children: fidl.Some(children));
await builder.replaceRealmDecl(rootDecl);
expect(rootDecl, await builder.getRealmDecl());
} on Exception catch (err, stacktrace) {
checkCommonExceptions(err, stacktrace);
rethrow;
}
});
test('replace component decl', () async {
RealmInstance? realmInstance;
try {
final builder = await RealmBuilder.create();
const echoServerName = 'v2EchoServer';
final v2EchoServer = await builder.addChild(
echoServerName,
v2EchoServerUrl,
);
var decl = await builder.getComponentDecl(v2EchoServer);
final exposes =
(decl.exposes != null ? decl.exposes!.toList() : <fdecl.Expose>[])
..add(
fdecl.Expose.withProtocol(
fdecl.ExposeProtocol(
source: fdecl.Ref.withSelf(fdecl.SelfRef()),
sourceName: fecho.Echo.$serviceName,
target: fdecl.Ref.withParent(fdecl.ParentRef()),
targetName: 'renamedEchoService',
),
),
);
decl = decl.$cloneWith(exposes: fidl.Some(exposes));
await builder.replaceComponentDecl(v2EchoServer, decl);
// Route logging to child
await builder.addRoute(Route()
..capability(ProtocolCapability(flogger.LogSink.$serviceName))
..from(Ref.parent())
..to(Ref.child(v2EchoServer)));
await builder.addRoute(Route()
..capability(ProtocolCapability('renamedEchoService'))
..from(Ref.child(v2EchoServer))
..to(Ref.parent()));
// Start the realmInstance. The EchoServer is not "eager", so it should
// not start automatically.
realmInstance = await builder.build();
final echo = realmInstance.root.connectToProtocolAtPath(
fecho.EchoProxy(),
'renamedEchoService',
);
const testString = 'ECHO...Echo...echo...(echo)...';
final reply = await echo.echoString(testString);
expect(testString, reply);
} on Exception catch (err, stacktrace) {
checkCommonExceptions(err, stacktrace);
rethrow;
} finally {
if (realmInstance != null) {
realmInstance.root.close();
}
}
});
test('start by binding', () async {
RealmInstance? realmInstance;
try {
final builder = await RealmBuilder.create();
const echoClientName = 'v2Client';
const echoClientBinder = 'echoClientBinder';
const echoServerName = 'localEchoServer';
// This test leverages the `echo_client` binary, with an augmented
// component manifest that exposes `fuchsia.component.Binder`.
//
// The client is *NOT* started eagerly!
//
// The `echo_server` will automatically start if a client connects to
// its `Echo` service. This test validates that connecting to the
// _`echo_client_`'s `Binder` service will start the client, which will
// subsequently invoke the `echo_server`. Since the `echo_server` is a
// local client, the test can confirm that the echo request was made.
final v2Client = await builder.addChild(
echoClientName,
v2EchoClientWithBinderUrl,
);
final echoRequestReceived = Completer<String?>();
final localEchoServer = await builder.addLocalChild(
echoServerName,
onRun: (handles, onStop) async {
LocalEcho(handles, echoRequestReceived: echoRequestReceived);
// Keep the component alive until the test is complete and the
// realm is closed.
await onStop.future;
},
);
// Route logging to child
await builder.addRoute(Route()
..capability(ProtocolCapability(flogger.LogSink.$serviceName))
..from(Ref.parent())
..to(Ref.child(v2Client))
..to(Ref.child(localEchoServer)));
// Route the echo service
await builder.addRoute(Route()
..capability(ProtocolCapability(fecho.Echo.$serviceName))
..from(Ref.child(localEchoServer))
..to(Ref.child(v2Client)));
// Route the child's Binder service to parent, so the test can connect
// to it to start the child.
await builder.addRoute(Route()
..capability(ProtocolCapability(fcomponent.Binder.$serviceName,
as: echoClientBinder))
..from(Ref.child(v2Client))
..to(Ref.parent()));
// Start the realmInstance. The child component (the client) is not
// "eager", so it should not start automatically.
realmInstance = await builder.build();
final scopedInstance = realmInstance.root;
// Start the client by binding.
/*proxy=*/ scopedInstance.connectToProtocolAtPath(
fcomponent.BinderProxy(),
echoClientBinder,
);
final requestedEchoString = await echoRequestReceived.future;
expect(requestedEchoString, v2EchoClientWithBinderArg);
} on Exception catch (err, stacktrace) {
checkCommonExceptions(err, stacktrace);
rethrow;
} finally {
if (realmInstance != null) {
realmInstance.root.close();
}
}
});
test('replace config', () async {
RealmInstance? realmInstance;
try {
final builder = await RealmBuilder.create();
const echoServerName = 'localEchoServer';
const echoClientName = 'v2EchoClient';
final v2EchoClientStructuredConfig = await builder.addChild(
echoClientName,
v2EchoClientStructuredConfigUrl,
ChildOptions()..eager(),
);
// NOTE: Important! This test updates the default configuration values
// in the successful calls to `replaceConfigValue...()` below (after the
// try/catch blocks).
//
// If this test was changed to run the EchoClient without first
// replacing some configurations, the EchoClient's default configuration
// values (as presently implemented) cause EchoClient to generate a
// string that exceeds the `echoString()` FIDL API's [MAX_STRING_LENGTH]
// (presently only 32, as declared in `echo.fidl`). This would cause the
// client to crash with a FIDL error, and the request would never reach
// the server.
final echoRequestReceived = Completer<String?>();
final localEchoServer = await builder.addLocalChild(
echoServerName,
onRun: (handles, onStop) async {
LocalEcho(handles, echoRequestReceived: echoRequestReceived);
// Keep the component alive until the test is complete
await onStop.future;
},
);
// fail to replace a config field in a component that doesn't have a config schema
var caught = false;
try {
await builder.replaceConfigValueBool(
localEchoServer, 'echo_bool', false);
} on fidl.MethodException<fctest.RealmBuilderError> catch (err) {
expect(err.value, fctest.RealmBuilderError.noConfigSchema);
caught = true;
} finally {
expect(caught, true);
}
// fail to replace a field that doesn't exist
caught = false;
try {
await builder.replaceConfigValueString(
v2EchoClientStructuredConfig, 'doesnt_exist', 'test');
} on fidl.MethodException<fctest.RealmBuilderError> catch (err) {
expect(err.value, fctest.RealmBuilderError.noSuchConfigField);
caught = true;
} finally {
expect(caught, true);
}
// fail to replace a field with the wrong type
caught = false;
try {
await builder.replaceConfigValueString(
v2EchoClientStructuredConfig, 'echo_bool', 'test');
} on fidl.MethodException<fctest.RealmBuilderError> catch (err) {
expect(err.value, fctest.RealmBuilderError.configValueInvalid);
caught = true;
} finally {
expect(caught, true);
}
// fail to replace a string that violates max_len
final longString = 'F' * 20;
caught = false;
try {
await builder.replaceConfigValueString(
v2EchoClientStructuredConfig, 'echo_string', longString);
} on fidl.MethodException<fctest.RealmBuilderError> catch (err) {
expect(err.value, fctest.RealmBuilderError.configValueInvalid);
caught = true;
} finally {
expect(caught, true);
}
// fail to replace a vector whose string element violates max_len
caught = false;
try {
await builder.replaceConfigValueStringVector(
v2EchoClientStructuredConfig, 'echo_string_vector', [longString]);
} on fidl.MethodException<fctest.RealmBuilderError> catch (err) {
expect(err.value, fctest.RealmBuilderError.configValueInvalid);
caught = true;
} finally {
expect(caught, true);
}
// fail to replace a vector that violates max_count
caught = false;
try {
await builder.replaceConfigValueStringVector(
v2EchoClientStructuredConfig,
'echo_string_vector',
['a', 'b', 'c', 'd'],
);
} on fidl.MethodException<fctest.RealmBuilderError> catch (err) {
expect(err.value, fctest.RealmBuilderError.configValueInvalid);
caught = true;
} finally {
expect(caught, true);
}
// succeed at replacing all fields with proper constraints
await builder.replaceConfigValueString(
v2EchoClientStructuredConfig, 'echo_string', 'Foobar!');
await builder.replaceConfigValueStringVector(
v2EchoClientStructuredConfig,
'echo_string_vector',
['Hey', 'Folks']);
await builder.replaceConfigValueBool(
v2EchoClientStructuredConfig, 'echo_bool', true);
await builder.replaceConfigValueUint64(
v2EchoClientStructuredConfig, 'echo_num', 42);
// Route logging to children
await builder.addRoute(Route()
..capability(ProtocolCapability(flogger.LogSink.$serviceName))
..from(Ref.parent())
..to(Ref.child(localEchoServer))
..to(Ref.child(v2EchoClientStructuredConfig)));
// Route the echo service from server to client
await builder.addRoute(Route()
..capability(ProtocolCapability(fecho.Echo.$serviceName))
..from(Ref.child(localEchoServer))
..to(Ref.child(v2EchoClientStructuredConfig)));
// The EchoClient at the referenced URL should be using this string:
const echoClientStructuredConfigRequest =
'Foobar!, Hey, Folks, true, 42';
// Start the realm instance.
realmInstance = await builder.build();
final requestedEchoString = await echoRequestReceived.future;
expect(requestedEchoString, echoClientStructuredConfigRequest);
} on Exception catch (err, stacktrace) {
checkCommonExceptions(err, stacktrace);
rethrow;
} finally {
if (realmInstance != null) {
realmInstance.root.close();
}
}
});
group('local child tests', () {
test('local server', () async {
RealmInstance? realmInstance;
try {
final builder = await RealmBuilder.create();
const echoServerName = 'localEchoServer';
final localEchoServer = await builder.addLocalChild(
echoServerName,
onRun: (handles, onStop) async {
LocalEcho(handles);
// Keep the component alive until the test is complete
await onStop.future;
},
);
await builder.addRoute(Route()
..capability(ProtocolCapability(fecho.Echo.$serviceName))
..from(Ref.child(localEchoServer))
..to(Ref.parent()));
realmInstance = await builder.build();
final echo = realmInstance.root
.connectToProtocolAtExposedDir(fecho.EchoProxy());
const testString = 'ECHO...Echo...echo...(echo)...';
final reply = await echo.echoString(testString);
expect(testString, reply);
} on Exception catch (err, stacktrace) {
checkCommonExceptions(err, stacktrace);
rethrow;
} finally {
if (realmInstance != null) {
realmInstance.root.close();
}
}
});
});
});
}
class LocalEcho extends fecho.Echo {
final LocalComponentHandles handles;
final Completer<String?>? echoRequestReceived;
final echoBinding = fecho.EchoBinding();
LocalEcho(this.handles, {this.echoRequestReceived}) {
services.Outgoing()
..serve(
fidl.InterfaceRequest<fio.Node>(handles.outgoingDir.passChannel()!))
..addPublicService(
(fidl.InterfaceRequest<fecho.Echo> connector) {
echoBinding.bind(this, connector);
},
fecho.Echo.$serviceName,
);
}
@override
Future<String?> echoString(String? str) async {
if (echoRequestReceived != null) {
echoRequestReceived!.complete(str);
}
return str;
}
}