diff --git a/packages/tizen_rpc_port/CHANGELOG.md b/packages/tizen_rpc_port/CHANGELOG.md index 01f1c8dcd..4c94f3df9 100644 --- a/packages/tizen_rpc_port/CHANGELOG.md +++ b/packages/tizen_rpc_port/CHANGELOG.md @@ -1,3 +1,11 @@ +## 0.1.6 + +* Read byte values as unsigned (0-255) so `Parcel.readByte` is consistent + across ABIs (C `char` is signed on x86 but unsigned on ARM). +* Add 27 integration test cases to the client example. +* Add server-specific integration test cases to the server example (the shared + Parcel and PortType tests are exercised only in the client example). + ## 0.1.5 * Update code format. diff --git a/packages/tizen_rpc_port/README.md b/packages/tizen_rpc_port/README.md index bcb6a2c65..072e656ed 100644 --- a/packages/tizen_rpc_port/README.md +++ b/packages/tizen_rpc_port/README.md @@ -21,7 +21,7 @@ The generated source files (`message_client.dart` and `message_server.dart`) dep ```yaml depenedencies: - tizen_rpc_port: ^0.1.5 + tizen_rpc_port: ^0.1.6 ``` Assuming that the name of the interface defined in your interface file is `Message`, the client must first call its `connect` method to connect to the server before making any remote invocation. diff --git a/packages/tizen_rpc_port/example/client/integration_test/tizen_rpc_port_test.dart b/packages/tizen_rpc_port/example/client/integration_test/tizen_rpc_port_test.dart index f87be0a49..4e1f00661 100644 --- a/packages/tizen_rpc_port/example/client/integration_test/tizen_rpc_port_test.dart +++ b/packages/tizen_rpc_port/example/client/integration_test/tizen_rpc_port_test.dart @@ -4,11 +4,45 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; -import 'package:tizen_rpc_port/src/parcel.dart'; +import 'package:tizen_bundle/tizen_bundle.dart'; +import 'package:tizen_rpc_port/tizen_rpc_port.dart'; + +// A minimal Parcelable implementation used only in tests. +class _Point implements Parcelable { + _Point({this.x = 0, this.y = 0}); + + int x; + int y; + + @override + void serialize(Parcel parcel) { + parcel.writeInt32(x); + parcel.writeInt32(y); + } + + @override + void deserialize(Parcel parcel) { + x = parcel.readInt32(); + y = parcel.readInt32(); + } +} + +// A minimal ProxyBase subclass used only to test single-app observable +// behaviour (isConnected initial state, connection rejection). +class _TestProxy extends ProxyBase { + _TestProxy() : super('org.tizen.nonexistent_app_for_rpc_test', 'TestPort'); + + @override + Future onReceivedEvent(Parcel parcel) async {} +} void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + // --------------------------------------------------------------------------- + // Existing tests — preserved unchanged + // --------------------------------------------------------------------------- + testWidgets('Parcel test', (WidgetTester tester) async { final Parcel parcel = Parcel(); parcel.writeBool(false); @@ -23,4 +57,230 @@ void main() { expect(parcel.readByte(), 0x3f); expect(parcel.readDouble(), 123.4); }); + + // --------------------------------------------------------------------------- + // Parcel — additional primitive types + // --------------------------------------------------------------------------- + + group('Parcel', () { + group('writeInt16 / readInt16', () { + testWidgets('round-trips positive value', (WidgetTester _) async { + final Parcel parcel = Parcel(); + parcel.writeInt16(32767); + expect(parcel.readInt16(), 32767); + }); + + testWidgets('round-trips negative value', (WidgetTester _) async { + final Parcel parcel = Parcel(); + parcel.writeInt16(-1); + expect(parcel.readInt16(), -1); + }); + + testWidgets('round-trips minimum value', (WidgetTester _) async { + final Parcel parcel = Parcel(); + parcel.writeInt16(-32768); + expect(parcel.readInt16(), -32768); + }); + + testWidgets('masks value to 16 bits', (WidgetTester _) async { + final Parcel parcel = Parcel(); + // 0x10042 & 0xffff == 0x0042 == 66 + parcel.writeInt16(0x10042); + expect(parcel.readInt16(), 66); + }); + }); + + group('writeInt64 / readInt64', () { + testWidgets('round-trips large positive value', (WidgetTester _) async { + final Parcel parcel = Parcel(); + const int value = 9007199254740992; // 2^53 + parcel.writeInt64(value); + expect(parcel.readInt64(), value); + }); + + testWidgets('round-trips maximum value', (WidgetTester _) async { + final Parcel parcel = Parcel(); + const int value = 9223372036854775807; // int64 max + parcel.writeInt64(value); + expect(parcel.readInt64(), value); + }); + + testWidgets('round-trips minimum value', (WidgetTester _) async { + final Parcel parcel = Parcel(); + const int value = -9223372036854775808; // int64 min + parcel.writeInt64(value); + expect(parcel.readInt64(), value); + }); + }); + + group('writeByte / readByte', () { + testWidgets('round-trips a value', (WidgetTester _) async { + final Parcel parcel = Parcel(); + parcel.writeByte(0xab); + expect(parcel.readByte(), 0xab); + }); + + testWidgets('round-trips maximum unsigned value', (WidgetTester _) async { + final Parcel parcel = Parcel(); + parcel.writeByte(255); + expect(parcel.readByte(), 255); + }); + + testWidgets('reads a negative input back as an unsigned byte', ( + WidgetTester _, + ) async { + final Parcel parcel = Parcel(); + // -1 is written as 0xff and must read back as the unsigned byte 255. + parcel.writeByte(-1); + expect(parcel.readByte(), 255); + }); + + testWidgets('masks value to 8 bits', (WidgetTester _) async { + final Parcel parcel = Parcel(); + // 0x142 & 0xff == 0x42 == 66 + parcel.writeByte(0x142); + expect(parcel.readByte(), 66); + }); + }); + + group('writeArrayCount / readArrayCount', () { + testWidgets('round-trips a count', (WidgetTester _) async { + final Parcel parcel = Parcel(); + parcel.writeArrayCount(42); + expect(parcel.readArrayCount(), 42); + }); + }); + + group('write / read (burst byte array)', () { + testWidgets('round-trips byte array', (WidgetTester _) async { + final Parcel parcel = Parcel(); + final Uint8List bytes = Uint8List.fromList([ + 0x00, + 0x01, + 0x7f, + 0x80, + 0xff, + ]); + parcel.write(bytes); + final Uint8List result = parcel.read(bytes.length); + expect(result, equals(bytes)); + }); + }); + + group('Parcel.fromRaw', () { + testWidgets('reconstructed parcel reads back original values', ( + WidgetTester _, + ) async { + final Parcel original = Parcel(); + original.writeInt32(99); + original.writeString('raw'); + original.writeBool(true); + + final Uint8List raw = original.asRaw(); + final Parcel restored = Parcel.fromRaw(raw); + + expect(restored.readInt32(), 99); + expect(restored.readString(), 'raw'); + expect(restored.readBool(), isTrue); + }); + }); + + group('writeBundle / readBundle', () { + testWidgets('round-trips a Bundle with string entries', ( + WidgetTester _, + ) async { + final Bundle bundle = Bundle(); + bundle['key1'] = 'value1'; + bundle['key2'] = 'value2'; + + final Parcel parcel = Parcel(); + parcel.writeBundle(bundle); + + final Bundle restored = parcel.readBundle(); + expect(restored['key1'], 'value1'); + expect(restored['key2'], 'value2'); + }); + + testWidgets('round-trips an empty Bundle', (WidgetTester _) async { + final Parcel parcel = Parcel(); + parcel.writeBundle(Bundle()); + final Bundle restored = parcel.readBundle(); + expect(restored.length, 0); + }); + }); + + group('header', () { + testWidgets('tag can be set and retrieved', (WidgetTester _) async { + final Parcel parcel = Parcel(); + final ParcelHeader header = parcel.header; + header.tag = '1.2.3'; + expect(parcel.header.tag, '1.2.3'); + }); + + testWidgets('sequenceNumber can be set and retrieved', ( + WidgetTester _, + ) async { + final Parcel parcel = Parcel(); + final ParcelHeader header = parcel.header; + header.sequenceNumber = 7; + expect(parcel.header.sequenceNumber, 7); + }); + + testWidgets('tag defaults to empty string', (WidgetTester _) async { + final Parcel parcel = Parcel(); + expect(parcel.header.tag, isEmpty); + }); + }); + + group('Parcelable', () { + testWidgets('custom Parcelable serializes and deserializes correctly', ( + WidgetTester _, + ) async { + final Parcel parcel = Parcel(); + final _Point original = _Point(x: 10, y: 20); + original.serialize(parcel); + + final _Point restored = _Point(); + restored.deserialize(parcel); + expect(restored.x, 10); + expect(restored.y, 20); + }); + }); + }); + + // --------------------------------------------------------------------------- + // PortType enum + // --------------------------------------------------------------------------- + + group('PortType', () { + testWidgets('has exactly the main and callback values', ( + WidgetTester _, + ) async { + expect( + PortType.values, + unorderedEquals([PortType.main, PortType.callback]), + ); + }); + }); + + // --------------------------------------------------------------------------- + // ProxyBase — single-app observable behaviour + // --------------------------------------------------------------------------- + + group('ProxyBase', () { + testWidgets('isConnected is false before connecting', ( + WidgetTester _, + ) async { + final _TestProxy proxy = _TestProxy(); + expect(proxy.isConnected, isFalse); + }); + + testWidgets('appid and portName are set from constructor', ( + WidgetTester _, + ) async { + final _TestProxy proxy = _TestProxy(); + expect(proxy.appid, 'org.tizen.nonexistent_app_for_rpc_test'); + expect(proxy.portName, 'TestPort'); + }); + }); } diff --git a/packages/tizen_rpc_port/example/client/lib/main.dart b/packages/tizen_rpc_port/example/client/lib/main.dart index 027cb25e1..8856466cc 100644 --- a/packages/tizen_rpc_port/example/client/lib/main.dart +++ b/packages/tizen_rpc_port/example/client/lib/main.dart @@ -171,32 +171,36 @@ class _MyAppState extends State { children: [ TextButton( style: ButtonStyle( - minimumSize: - MaterialStateProperty.all(const Size.fromHeight(100)), + minimumSize: MaterialStateProperty.all( + const Size.fromHeight(100), + ), ), onPressed: _connect, child: const Text('Connect'), ), TextButton( style: ButtonStyle( - minimumSize: - MaterialStateProperty.all(const Size.fromHeight(100)), + minimumSize: MaterialStateProperty.all( + const Size.fromHeight(100), + ), ), onPressed: _connect2, child: const Text('Connect2'), ), TextButton( style: ButtonStyle( - minimumSize: - MaterialStateProperty.all(const Size.fromHeight(100)), + minimumSize: MaterialStateProperty.all( + const Size.fromHeight(100), + ), ), onPressed: _isConnected ? _send : null, child: const Text('Send'), ), TextButton( style: ButtonStyle( - minimumSize: - MaterialStateProperty.all(const Size.fromHeight(100)), + minimumSize: MaterialStateProperty.all( + const Size.fromHeight(100), + ), ), onPressed: _isConnected2 ? _send2 : null, child: const Text('Send2'), diff --git a/packages/tizen_rpc_port/example/client/pubspec.yaml b/packages/tizen_rpc_port/example/client/pubspec.yaml index b83f036d9..1a5e35f4c 100644 --- a/packages/tizen_rpc_port/example/client/pubspec.yaml +++ b/packages/tizen_rpc_port/example/client/pubspec.yaml @@ -9,6 +9,7 @@ environment: dependencies: flutter: sdk: flutter + tizen_bundle: ^0.1.2 tizen_rpc_port: path: ../../ diff --git a/packages/tizen_rpc_port/example/client/test_driver/integration_test.dart b/packages/tizen_rpc_port/example/client/test_driver/integration_test.dart new file mode 100644 index 000000000..b38629cca --- /dev/null +++ b/packages/tizen_rpc_port/example/client/test_driver/integration_test.dart @@ -0,0 +1,3 @@ +import 'package:integration_test/integration_test_driver.dart'; + +Future main() => integrationDriver(); diff --git a/packages/tizen_rpc_port/example/server/integration_test/tizen_rpc_port_test.dart b/packages/tizen_rpc_port/example/server/integration_test/tizen_rpc_port_test.dart index f87be0a49..41af972c7 100644 --- a/packages/tizen_rpc_port/example/server/integration_test/tizen_rpc_port_test.dart +++ b/packages/tizen_rpc_port/example/server/integration_test/tizen_rpc_port_test.dart @@ -4,23 +4,100 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; -import 'package:tizen_rpc_port/src/parcel.dart'; +import 'package:tizen_rpc_port/tizen_rpc_port.dart'; + +import 'package:tizen_rpc_port_server_example/message_server.dart'; + +// The core Parcel and PortType tests are context-independent, so they are +// exercised only in the client example to avoid duplicating them here. This +// suite covers the server-specific StubBase behaviour. + +// A minimal StubBase subclass used only to test single-app observable +// behaviour (construction, setTrusted, addPrivilege). +class _TestStub extends StubBase { + _TestStub() : super('TestStubPort'); + + @override + Future onConnectedEvent(String sender, String instance) async {} + + @override + Future onDisconnectedEvent(String sender, String instance) async {} + + @override + Future onReceivedEvent( + String sender, + String instance, + Parcel parcel, + ) async {} +} void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - testWidgets('Parcel test', (WidgetTester tester) async { - final Parcel parcel = Parcel(); - parcel.writeBool(false); - parcel.writeInt32(123); - parcel.writeString('Hello'); - parcel.writeByte(0x3f); - parcel.writeDouble(123.4); - - expect(parcel.readBool(), false); - expect(parcel.readInt32(), 123); - expect(parcel.readString(), 'Hello'); - expect(parcel.readByte(), 0x3f); - expect(parcel.readDouble(), 123.4); + // --------------------------------------------------------------------------- + // StubBase — single-app observable behaviour + // --------------------------------------------------------------------------- + + group('StubBase', () { + testWidgets('portName is set from constructor', (WidgetTester _) async { + final _TestStub stub = _TestStub(); + expect(stub.portName, 'TestStubPort'); + }); + + testWidgets('setTrusted can be called without listen', ( + WidgetTester _, + ) async { + final _TestStub stub = _TestStub(); + stub.setTrusted(true); + stub.setTrusted(false); + }); + + testWidgets('addPrivilege can be called without listen', ( + WidgetTester _, + ) async { + final _TestStub stub = _TestStub(); + stub.addPrivilege('http://tizen.org/privilege/datasharing'); + }); }); + + // --------------------------------------------------------------------------- + // Message — server-side StubBase subclass + // --------------------------------------------------------------------------- + + group('Message', () { + testWidgets('portName is Message', (WidgetTester _) async { + final Message server = Message( + serviceBuilder: (String sender, String instance) => + _NoopService(sender, instance), + ); + expect(server.portName, 'Message'); + }); + + testWidgets('services list is initially empty', (WidgetTester _) async { + final Message server = Message( + serviceBuilder: (String sender, String instance) => + _NoopService(sender, instance), + ); + expect(server.services, isEmpty); + }); + }); +} + +class _NoopService extends ServiceBase { + _NoopService(super.sender, super.instance); + + @override + Future onCreate() async {} + + @override + Future onTerminate() async {} + + @override + Future onRegister(String name, NotifyCallback callback) async => 0; + + @override + Future onUnregister() async {} + + @override + Future onSend(String message) async => 0; } diff --git a/packages/tizen_rpc_port/example/server/test_driver/integration_test.dart b/packages/tizen_rpc_port/example/server/test_driver/integration_test.dart new file mode 100644 index 000000000..b38629cca --- /dev/null +++ b/packages/tizen_rpc_port/example/server/test_driver/integration_test.dart @@ -0,0 +1,3 @@ +import 'package:integration_test/integration_test_driver.dart'; + +Future main() => integrationDriver(); diff --git a/packages/tizen_rpc_port/lib/src/parcel.dart b/packages/tizen_rpc_port/lib/src/parcel.dart index 90a15a9c4..2ec3c61d2 100644 --- a/packages/tizen_rpc_port/lib/src/parcel.dart +++ b/packages/tizen_rpc_port/lib/src/parcel.dart @@ -293,6 +293,11 @@ class Parcel { } /// Reads a byte value from this parcel. + /// + /// The value is returned as an unsigned byte in the range 0-255, matching the + /// unsigned semantics of [writeByte] and [read]. The native value is read + /// into a C `char`, whose signedness is ABI-dependent (signed on x86, unsigned + /// on ARM), so it is masked to 8 bits to keep the result consistent. int readByte() { return using((Arena arena) { final Pointer pValue = arena(); @@ -303,7 +308,7 @@ class Parcel { message: tizen.get_error_message(ret).toDartString(), ); } - return pValue.value; + return pValue.value & _byteMax; }); } diff --git a/packages/tizen_rpc_port/lib/src/proxy_base.dart b/packages/tizen_rpc_port/lib/src/proxy_base.dart index 0f51681b9..8d80d104b 100644 --- a/packages/tizen_rpc_port/lib/src/proxy_base.dart +++ b/packages/tizen_rpc_port/lib/src/proxy_base.dart @@ -61,9 +61,7 @@ abstract class ProxyBase { Completer _connectCompleter = Completer(); Completer _disconnectCompleter = Completer(); - static const MethodChannel _channel = MethodChannel( - 'tizen/rpc_port_proxy', - ); + static const MethodChannel _channel = MethodChannel('tizen/rpc_port_proxy'); Stream? _stream; static final Set _activeConnections = {}; @@ -92,25 +90,20 @@ abstract class ProxyBase { throw StateError('Proxy $appid/$portName already connected'); } - await _channel.invokeMethod( - 'init', - { - 'handle': _handle.address, - 'portName': portName, - }, - ); + await _channel.invokeMethod('init', { + 'handle': _handle.address, + 'portName': portName, + }); final EventChannel eventChannel = EventChannel( 'tizen/rpc_port_proxy/$portName/${_handle.address}', ); - _stream = eventChannel.receiveBroadcastStream( - { - 'handle': _handle.address, - 'appid': appid, - 'portName': portName, - }, - ); + _stream = eventChannel.receiveBroadcastStream({ + 'handle': _handle.address, + 'appid': appid, + 'portName': portName, + }); _activeConnections.add(_handle.address); _streamSubscription = _stream!.listen((dynamic data) async { diff --git a/packages/tizen_rpc_port/pubspec.yaml b/packages/tizen_rpc_port/pubspec.yaml index 443d92f39..3d2e9905d 100644 --- a/packages/tizen_rpc_port/pubspec.yaml +++ b/packages/tizen_rpc_port/pubspec.yaml @@ -2,7 +2,7 @@ name: tizen_rpc_port description: Tizen RPC Port APIs. Used to make remote procedure calls between Tizen apps. homepage: https://github.com/flutter-tizen/plugins repository: https://github.com/flutter-tizen/plugins/tree/master/packages/tizen_rpc_port -version: 0.1.5 +version: 0.1.6 environment: sdk: ">=3.1.0 <4.0.0"