From dcb439dccec198b6b5b0bfc52377afb5a003fb8b Mon Sep 17 00:00:00 2001 From: Ian Hickson Date: Wed, 18 Nov 2020 15:22:12 -0800 Subject: [PATCH] Move test support logic out of the default Dart output. This will allow us to move the mock logic out of the core framework. --- packages/pigeon/README.md | 4 +- packages/pigeon/bin/pigeon.dart | 7 +- packages/pigeon/lib/ast.dart | 2 +- packages/pigeon/lib/dart_generator.dart | 248 ++++++++++++------ packages/pigeon/lib/generator_tools.dart | 58 ++-- packages/pigeon/lib/pigeon_lib.dart | 41 ++- .../mock_handler_tester/test/message.dart | 171 ++++++------ .../pigeon/mock_handler_tester/test/test.dart | 54 ++++ .../mock_handler_tester/test/widget_test.dart | 2 + packages/pigeon/run_tests.sh | 47 ++-- packages/pigeon/test/dart_generator_test.dart | 53 ++-- 11 files changed, 446 insertions(+), 241 deletions(-) create mode 100644 packages/pigeon/mock_handler_tester/test/test.dart diff --git a/packages/pigeon/README.md b/packages/pigeon/README.md index e91ad4b6918..f5977670a6f 100644 --- a/packages/pigeon/README.md +++ b/packages/pigeon/README.md @@ -23,7 +23,8 @@ doesn't need to worry about conflicting versions of Pigeon. 1) Add Pigeon as a dev_dependency. 1) Make a ".dart" file outside of your "lib" directory for defining the communication interface. -1) Run pigeon on your ".dart" file to generate the required Dart and Objective-C code. +1) Run pigeon on your ".dart" file to generate the required Dart and Objective-C code: + `flutter pub get` then `flutter pub run pigeon` with suitable arguments. 1) Add the generated Dart code to `lib` for compilation. 1) Add the generated Objective-C code to your Xcode project for compilation (e.g. `ios/Runner.xcworkspace` or `.podspec`). @@ -36,6 +37,7 @@ doesn't need to worry about conflicting versions of Pigeon. 1) Add Pigeon as a dev_dependency. 1) Make a ".dart" file outside of your "lib" directory for defining the communication interface. 1) Run pigeon on your ".dart" file to generate the required Dart and Java code. + `flutter pub get` then `flutter pub run pigeon` with suitable arguments. 1) Add the generated Dart code to `./lib` for compilation. 1) Add the generated Java code to your `./android/app/src/main/java` directory for compilation. 1) Implement the generated Java interface for handling the calls on Android, set it up diff --git a/packages/pigeon/bin/pigeon.dart b/packages/pigeon/bin/pigeon.dart index 25aba1f9207..1a424e092f0 100644 --- a/packages/pigeon/bin/pigeon.dart +++ b/packages/pigeon/bin/pigeon.dart @@ -1,7 +1,9 @@ // Copyright 2020 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. + // @dart = 2.2 + import 'dart:async'; import 'dart:io'; import 'dart:isolate'; @@ -26,7 +28,9 @@ String _posixRelative(String input, {String from}) { Future main(List args) async { final PigeonOptions opts = Pigeon.parseArgs(args); - final Directory tempDir = Directory.systemTemp.createTempSync(); + final Directory tempDir = Directory.systemTemp.createTempSync( + 'flutter_pigeon.', + ); String importLine = ''; if (opts.input != null) { @@ -43,7 +47,6 @@ void main(List args, SendPort sendPort) async { sendPort.send(await Pigeon.run(args)); } """; - final File tempFile = File(path.join(tempDir.path, '_pigeon_temp_.dart')); await tempFile.writeAsString(code); final ReceivePort receivePort = ReceivePort(); diff --git a/packages/pigeon/lib/ast.dart b/packages/pigeon/lib/ast.dart index e2534613378..e3c3a969257 100644 --- a/packages/pigeon/lib/ast.dart +++ b/packages/pigeon/lib/ast.dart @@ -32,7 +32,7 @@ class Method extends Node { bool isAsynchronous; } -/// Represents a collection of [Method]s that are hosted ona given [location]. +/// Represents a collection of [Method]s that are hosted on a given [location]. class Api extends Node { /// Parametric constructor for [Api]. Api({this.name, this.location, this.methods, this.dartHostTestHandler}); diff --git a/packages/pigeon/lib/dart_generator.dart b/packages/pigeon/lib/dart_generator.dart index 121056803fb..80f7cb5e616 100644 --- a/packages/pigeon/lib/dart_generator.dart +++ b/packages/pigeon/lib/dart_generator.dart @@ -11,64 +11,81 @@ class DartOptions { bool isNullSafe = false; } +String _escapeForDartSingleQuotedString(String raw) { + return raw + .replaceAll(r'\', r'\\') + .replaceAll(r'$', r'\$') + .replaceAll(r"'", r"\'"); +} + void _writeHostApi(DartOptions opt, Indent indent, Api api) { assert(api.location == ApiLocation.host); final String nullTag = opt.isNullSafe ? '?' : ''; + final String unwrapOperator = opt.isNullSafe ? '!' : ''; + bool first = true; indent.write('class ${api.name} '); indent.scoped('{', '}', () { for (Method func in api.methods) { + if (!first) { + indent.writeln(''); + } else { + first = false; + } String argSignature = ''; String sendArgument = 'null'; - String requestMapDeclaration; + String encodedDeclaration; if (func.argType != 'void') { argSignature = '${func.argType} arg'; - sendArgument = 'requestMap'; - requestMapDeclaration = - 'final Map requestMap = arg._toMap();'; + sendArgument = 'encoded'; + encodedDeclaration = 'final Object encoded = arg.encode();'; } indent.write( - 'Future<${func.returnType}> ${func.name}($argSignature) async '); + 'Future<${func.returnType}> ${func.name}($argSignature) async ', + ); indent.scoped('{', '}', () { - if (requestMapDeclaration != null) { - indent.writeln(requestMapDeclaration); + if (encodedDeclaration != null) { + indent.writeln(encodedDeclaration); } final String channelName = makeChannelName(api, func); - indent.writeln('const BasicMessageChannel channel ='); - indent.inc(); - indent.inc(); - indent.writeln( - 'BasicMessageChannel(\'$channelName\', StandardMessageCodec());'); - indent.dec(); - indent.dec(); - indent.writeln(''); + indent.writeln('const BasicMessageChannel channel ='); + indent.nest(2, () { + indent.writeln( + 'BasicMessageChannel(\'$channelName\', StandardMessageCodec());', + ); + }); final String returnStatement = func.returnType == 'void' ? '// noop' - : 'return ${func.returnType}._fromMap(replyMap[\'${Keys.result}\']);'; + : 'return ${func.returnType}.decode(replyMap[\'${Keys.result}\']$unwrapOperator);'; indent.format( - '''final Map$nullTag replyMap = await channel.send($sendArgument); + '''final Map$nullTag replyMap = await channel.send($sendArgument) as Map$nullTag; if (replyMap == null) { \tthrow PlatformException( \t\tcode: 'channel-error', \t\tmessage: 'Unable to establish connection on channel.', -\t\tdetails: null); +\t\tdetails: null, +\t); } else if (replyMap['error'] != null) { -\tfinal Map error = replyMap['${Keys.error}']; +\tfinal Map error = replyMap['${Keys.error}'] as Map; \tthrow PlatformException( -\t\t\tcode: error['${Keys.errorCode}'], -\t\t\tmessage: error['${Keys.errorMessage}'], -\t\t\tdetails: error['${Keys.errorDetails}']); +\t\tcode: error['${Keys.errorCode}'] as String, +\t\tmessage: error['${Keys.errorMessage}'] as String$nullTag, +\t\tdetails: error['${Keys.errorDetails}'], +\t); } else { \t$returnStatement -} -'''); +}'''); }); } }); - indent.writeln(''); } -void _writeFlutterApi(DartOptions opt, Indent indent, Api api, - {String Function(Method) channelNameFunc, bool isMockHandler = false}) { +void _writeFlutterApi( + DartOptions opt, + Indent indent, + Api api, { + String Function(Method) channelNameFunc, + bool isMockHandler = false, +}) { assert(api.location == ApiLocation.flutter); final String nullTag = opt.isNullSafe ? '?' : ''; indent.write('abstract class ${api.name} '); @@ -86,53 +103,62 @@ void _writeFlutterApi(DartOptions opt, Indent indent, Api api, for (Method func in api.methods) { indent.write(''); indent.scoped('{', '}', () { - indent.writeln('const BasicMessageChannel channel ='); - indent.inc(); - indent.inc(); - final String channelName = channelNameFunc == null - ? makeChannelName(api, func) - : channelNameFunc(func); indent.writeln( - 'BasicMessageChannel(\'$channelName\', StandardMessageCodec());'); - indent.dec(); - indent.dec(); + 'const BasicMessageChannel channel =', + ); + indent.nest(2, () { + final String channelName = channelNameFunc == null + ? makeChannelName(api, func) + : channelNameFunc(func); + indent.writeln( + 'BasicMessageChannel(\'$channelName\', StandardMessageCodec());', + ); + }); final String messageHandlerSetter = isMockHandler ? 'setMockMessageHandler' : 'setMessageHandler'; indent.write('if (api == null) '); - indent.scoped('{', '} else {', () { + indent.scoped('{', '}', () { indent.writeln('channel.$messageHandlerSetter(null);'); - }); - indent.scoped('', '}', () { + }, addTrailingNewline: false); + indent.add(' else '); + indent.scoped('{', '}', () { indent.write( - 'channel.$messageHandlerSetter((dynamic message) async '); + 'channel.$messageHandlerSetter((Object$nullTag message) async ', + ); indent.scoped('{', '});', () { final String argType = func.argType; final String returnType = func.returnType; final bool isAsync = func.isAsynchronous; + final String emptyReturnStatement = isMockHandler + ? 'return {};' + : func.returnType == 'void' + ? 'return;' + : 'return null;'; + indent.write('if (message == null) '); + indent.scoped('{', '}', () { + indent.writeln(emptyReturnStatement); + }); String call; if (argType == 'void') { call = 'api.${func.name}()'; } else { indent.writeln( - 'final Map mapMessage = message as Map;'); - indent.writeln( - 'final $argType input = $argType._fromMap(mapMessage);'); + 'final $argType input = $argType.decode(message);', + ); call = 'api.${func.name}(input)'; } if (returnType == 'void') { indent.writeln('$call;'); - if (isMockHandler) { - indent.writeln('return {};'); - } + indent.writeln(emptyReturnStatement); } else { if (isAsync) { indent.writeln('final $returnType output = await $call;'); } else { indent.writeln('final $returnType output = $call;'); } - const String returnExpresion = 'output._toMap()'; + const String returnExpresion = 'output.encode()'; final String returnStatement = isMockHandler - ? 'return {\'${Keys.result}\': $returnExpresion};' + ? 'return {\'${Keys.result}\': $returnExpresion};' : 'return $returnExpresion;'; indent.writeln(returnStatement); } @@ -142,84 +168,148 @@ void _writeFlutterApi(DartOptions opt, Indent indent, Api api, } }); }); - indent.addln(''); +} + +String _addGenericTypes(String dataType, String nullTag) { + switch (dataType) { + case 'List': + return 'List'; + case 'Map': + return 'Map'; + default: + return dataType; + } } /// Generates Dart source code for the given AST represented by [root], /// outputting the code to [sink]. void generateDart(DartOptions opt, Root root, StringSink sink) { + final String nullTag = opt.isNullSafe ? '?' : ''; + final String unwrapOperator = opt.isNullSafe ? '!' : ''; final List customClassNames = root.classes.map((Class x) => x.name).toList(); final Indent indent = Indent(sink); indent.writeln('// $generatedCodeWarning'); indent.writeln('// $seeAlsoWarning'); indent.writeln( - '// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import'); + '// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import', + ); indent.writeln('// @dart = ${opt.isNullSafe ? '2.10' : '2.8'}'); indent.writeln('import \'dart:async\';'); - indent.writeln('import \'package:flutter/services.dart\';'); indent.writeln( - 'import \'dart:typed_data\' show Uint8List, Int32List, Int64List, Float64List;'); + 'import \'dart:typed_data\' show Uint8List, Int32List, Int64List, Float64List;', + ); indent.writeln(''); - - final String nullBang = opt.isNullSafe ? '!' : ''; + indent.writeln('import \'package:flutter/services.dart\';'); for (Class klass in root.classes) { + indent.writeln(''); sink.write('class ${klass.name} '); indent.scoped('{', '}', () { for (Field field in klass.fields) { final String datatype = - opt.isNullSafe ? '${field.dataType}?' : field.dataType; + '${_addGenericTypes(field.dataType, nullTag)}$nullTag'; indent.writeln('$datatype ${field.name};'); } + if (klass.fields.isNotEmpty) { + indent.writeln(''); + } indent.writeln('// ignore: unused_element'); - indent.write('Map _toMap() '); + indent.write('Object encode() '); indent.scoped('{', '}', () { indent.writeln( - 'final Map pigeonMap = {};'); + 'final Map pigeonMap = {};', + ); for (Field field in klass.fields) { indent.write('pigeonMap[\'${field.name}\'] = '); if (customClassNames.contains(field.dataType)) { indent.addln( - '${field.name} == null ? null : ${field.name}$nullBang._toMap();'); + '${field.name} == null ? null : ${field.name}$unwrapOperator.encode();', + ); } else { indent.addln('${field.name};'); } } indent.writeln('return pigeonMap;'); }); + indent.writeln(''); indent.writeln('// ignore: unused_element'); indent.write( - 'static ${klass.name} _fromMap(Map pigeonMap) '); + 'static ${klass.name} decode(Object message) ', + ); indent.scoped('{', '}', () { - indent.writeln('final ${klass.name} result = ${klass.name}();'); - for (Field field in klass.fields) { - indent.write('result.${field.name} = '); - if (customClassNames.contains(field.dataType)) { - indent.addln( - 'pigeonMap[\'${field.name}\'] != null ? ${field.dataType}._fromMap(pigeonMap[\'${field.name}\']) : null;'); - } else { - indent.addln('pigeonMap[\'${field.name}\'];'); + indent.writeln( + 'final Map pigeonMap = message as Map;', + ); + indent.writeln('return ${klass.name}()'); + indent.nest(1, () { + for (int index = 0; index < klass.fields.length; index += 1) { + final Field field = klass.fields[index]; + indent.write('..${field.name} = '); + if (customClassNames.contains(field.dataType)) { + indent.add( + 'pigeonMap[\'${field.name}\'] != null ? ${field.dataType}.decode(pigeonMap[\'${field.name}\']$unwrapOperator) : null', + ); + } else { + indent.add( + 'pigeonMap[\'${field.name}\'] as ${_addGenericTypes(field.dataType, nullTag)}', + ); + } + indent.addln(index == klass.fields.length - 1 ? ';' : ''); } - } - indent.writeln('return result;'); + }); }); }); - indent.writeln(''); } for (Api api in root.apis) { + indent.writeln(''); if (api.location == ApiLocation.host) { _writeHostApi(opt, indent, api); - if (api.dartHostTestHandler != null) { - final Api mockApi = Api( - name: api.dartHostTestHandler, - methods: api.methods, - location: ApiLocation.flutter); - _writeFlutterApi(opt, indent, mockApi, - channelNameFunc: (Method func) => makeChannelName(api, func), - isMockHandler: true); - } } else if (api.location == ApiLocation.flutter) { _writeFlutterApi(opt, indent, api); } } } + +/// Generates Dart source code for test support libraries based on the +/// given AST represented by [root], outputting the code to [sink]. +void generateTestDart( + DartOptions opt, + Root root, + StringSink sink, + String mainDartFile, +) { + final Indent indent = Indent(sink); + indent.writeln('// $generatedCodeWarning'); + indent.writeln('// $seeAlsoWarning'); + indent.writeln( + '// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import', + ); + indent.writeln('// @dart = ${opt.isNullSafe ? '2.10' : '2.8'}'); + indent.writeln('import \'dart:async\';'); + indent.writeln( + 'import \'dart:typed_data\' show Uint8List, Int32List, Int64List, Float64List;', + ); + indent.writeln('import \'package:flutter/services.dart\';'); + indent.writeln('import \'package:flutter_test/flutter_test.dart\';'); + indent.writeln(''); + indent.writeln( + 'import \'${_escapeForDartSingleQuotedString(mainDartFile)}\';', + ); + for (Api api in root.apis) { + if (api.location == ApiLocation.host && api.dartHostTestHandler != null) { + final Api mockApi = Api( + name: api.dartHostTestHandler, + methods: api.methods, + location: ApiLocation.flutter, + ); + indent.writeln(''); + _writeFlutterApi( + opt, + indent, + mockApi, + channelNameFunc: (Method func) => makeChannelName(api, func), + isMockHandler: true, + ); + } + } +} diff --git a/packages/pigeon/lib/generator_tools.dart b/packages/pigeon/lib/generator_tools.dart index 447c37075c3..f79b16a5589 100644 --- a/packages/pigeon/lib/generator_tools.dart +++ b/packages/pigeon/lib/generator_tools.dart @@ -36,16 +36,16 @@ class Indent { final String tab = ' '; /// Increase the indentation level. - void inc() { - _count += 1; + void inc([int level = 1]) { + _count += level; } /// Decrement the indentation level. - void dec() { - _count -= 1; + void dec([int level = 1]) { + _count -= level; } - /// Returns the String represneting the current indentation. + /// Returns the String representing the current indentation. String str() { String result = ''; for (int i = 0; i < _count; i++) { @@ -63,7 +63,12 @@ class Indent { /// Scoped increase of the ident level. For the execution of [func] the /// indentation will be incremented. - void scoped(String begin, String end, Function func) { + void scoped( + String begin, + String end, + Function func, { + bool addTrailingNewline = true, + }) { if (begin != null) { _sink.write(begin + newline); } @@ -71,28 +76,43 @@ class Indent { func(); dec(); if (end != null) { - _sink.write(str() + end + newline); + _sink.write(str() + end); + if (addTrailingNewline) { + _sink.write(newline); + } } } - /// Add [str] with indentation and a newline. - void writeln(String str) { - _sink.write(this.str() + str + newline); + /// Scoped increase of the ident level. For the execution of [func] the + /// indentation will be incremented by the given amount. + void nest(int count, Function func) { + inc(count); + func(); + dec(count); + } + + /// Add [text] with indentation and a newline. + void writeln(String text) { + if (text.isEmpty) { + _sink.write(newline); + } else { + _sink.write(str() + text + newline); + } } - /// Add [str] with indentation. - void write(String str) { - _sink.write(this.str() + str); + /// Add [text] with indentation. + void write(String text) { + _sink.write(str() + text); } - /// Add [str] with a newline. - void addln(String str) { - _sink.write(str + newline); + /// Add [text] with a newline. + void addln(String text) { + _sink.write(text + newline); } - /// Just adds [str]. - void add(String str) { - _sink.write(str); + /// Just adds [text]. + void add(String text) { + _sink.write(text); } } diff --git a/packages/pigeon/lib/pigeon_lib.dart b/packages/pigeon/lib/pigeon_lib.dart index 79de4994f3c..22d3871d6b2 100644 --- a/packages/pigeon/lib/pigeon_lib.dart +++ b/packages/pigeon/lib/pigeon_lib.dart @@ -6,6 +6,7 @@ import 'dart:io'; import 'dart:mirrors'; import 'package:args/args.dart'; +import 'package:path/path.dart' as path; import 'package:path/path.dart'; import 'package:pigeon/java_generator.dart'; @@ -44,10 +45,13 @@ class HostApi { /// Parametric constructor for [HostApi]. const HostApi({this.dartHostTestHandler}); - /// The name of an interface generated next to the [HostApi] class. Implement - /// this interface and invoke `[name of this handler].setup` to receive calls - /// from your real [HostApi] class in Dart instead of the host platform code, - /// as is typical. + /// The name of an interface generated for tests. Implement this + /// interface and invoke `[name of this handler].setup` to receive + /// calls from your real [HostApi] class in Dart instead of the host + /// platform code, as is typical. + /// + /// When using this, you must specify the `--out_test_dart` argument + /// to specify where to generate the test file. /// /// Prefer to use a mock of the real [HostApi] with a mocking library for unit /// tests. Generating this Dart handler is sometimes useful in integration @@ -119,6 +123,9 @@ class PigeonOptions { /// Path to the dart file that will be generated. String dartOut; + /// Path to the dart file that will be generated for test support classes. + String dartTestOut; + /// Path to the ".h" Objective-C file will be generated. String objcHeaderOut; @@ -279,7 +286,10 @@ options: static final ArgParser _argParser = ArgParser() ..addOption('input', help: 'REQUIRED: Path to pigeon file.') ..addOption('dart_out', - help: 'REQUIRED: Path to generated dart source file (.dart).') + help: 'REQUIRED: Path to generated Dart source file (.dart).') + ..addOption('dart_test_out', + help: 'Path to generated library for Dart tests, when using ' + '@HostApi(dartHostTestHandler:).') ..addOption('objc_source_out', help: 'Path to generated Objective-C source file (.m).') ..addOption('java_out', help: 'Path to generated Java file (.java).') @@ -299,6 +309,7 @@ options: final PigeonOptions opts = PigeonOptions(); opts.input = results['input']; opts.dartOut = results['dart_out']; + opts.dartTestOut = results['dart_test_out']; opts.objcHeaderOut = results['objc_header_out']; opts.objcSourceOut = results['objc_source_out']; opts.objcOptions.prefix = results['objc_prefix']; @@ -372,6 +383,11 @@ options: } } + static String _posixify(String input) { + final path.Context context = path.Context(style: path.Style.posix); + return context.fromUri(path.toUri(path.absolute(input))); + } + /// The 'main' entrypoint used by the command-line tool. [args] are the /// command-line arguments. static Future run(List args) async { @@ -413,6 +429,21 @@ options: (StringSink sink) => generateDart(options.dartOptions, parseResults.root, sink)); } + if (options.dartTestOut != null) { + final String mainPath = context.relative( + _posixify(options.dartOut), + from: _posixify(path.dirname(options.dartTestOut)), + ); + await _runGenerator( + options.dartTestOut, + (StringSink sink) => generateTestDart( + options.dartOptions, + parseResults.root, + sink, + mainPath, + ), + ); + } if (options.objcHeaderOut != null) { await _runGenerator( options.objcHeaderOut, diff --git a/packages/pigeon/mock_handler_tester/test/message.dart b/packages/pigeon/mock_handler_tester/test/message.dart index 1aa33c6494b..2b051a3e97e 100644 --- a/packages/pigeon/mock_handler_tester/test/message.dart +++ b/packages/pigeon/mock_handler_tester/test/message.dart @@ -1,29 +1,29 @@ -// Autogenerated from Pigeon (v0.1.2), do not edit directly. +// Autogenerated from Pigeon (v0.1.15), do not edit directly. // See also: https://pub.dev/packages/pigeon // ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import +// @dart = 2.8 import 'dart:async'; +import 'dart:typed_data' show Uint8List, Int32List, Int64List, Float64List; import 'package:flutter/services.dart'; class SearchReply { String result; String error; + // ignore: unused_element - Map _toMap() { - final Map pigeonMap = {}; + Object encode() { + final Map pigeonMap = {}; pigeonMap['result'] = result; pigeonMap['error'] = error; return pigeonMap; } // ignore: unused_element - static SearchReply _fromMap(Map pigeonMap) { - if (pigeonMap == null) { - return null; - } - final SearchReply result = SearchReply(); - result.result = pigeonMap['result']; - result.error = pigeonMap['error']; - return result; + static SearchReply decode(Object message) { + final Map pigeonMap = message as Map; + return SearchReply() + ..result = pigeonMap['result'] as String + ..error = pigeonMap['error'] as String; } } @@ -31,9 +31,10 @@ class SearchRequest { String query; int anInt; bool aBool; + // ignore: unused_element - Map _toMap() { - final Map pigeonMap = {}; + Object encode() { + final Map pigeonMap = {}; pigeonMap['query'] = query; pigeonMap['anInt'] = anInt; pigeonMap['aBool'] = aBool; @@ -41,35 +42,32 @@ class SearchRequest { } // ignore: unused_element - static SearchRequest _fromMap(Map pigeonMap) { - if (pigeonMap == null) { - return null; - } - final SearchRequest result = SearchRequest(); - result.query = pigeonMap['query']; - result.anInt = pigeonMap['anInt']; - result.aBool = pigeonMap['aBool']; - return result; + static SearchRequest decode(Object message) { + final Map pigeonMap = message as Map; + return SearchRequest() + ..query = pigeonMap['query'] as String + ..anInt = pigeonMap['anInt'] as int + ..aBool = pigeonMap['aBool'] as bool; } } class Nested { SearchRequest request; + // ignore: unused_element - Map _toMap() { - final Map pigeonMap = {}; - pigeonMap['request'] = request == null ? null : request._toMap(); + Object encode() { + final Map pigeonMap = {}; + pigeonMap['request'] = request == null ? null : request.encode(); return pigeonMap; } // ignore: unused_element - static Nested _fromMap(Map pigeonMap) { - if (pigeonMap == null) { - return null; - } - final Nested result = Nested(); - result.request = SearchRequest._fromMap(pigeonMap['request']); - return result; + static Nested decode(Object message) { + final Map pigeonMap = message as Map; + return Nested() + ..request = pigeonMap['request'] != null + ? SearchRequest.decode(pigeonMap['request']) + : null; } } @@ -77,97 +75,74 @@ abstract class FlutterSearchApi { SearchReply search(SearchRequest arg); static void setup(FlutterSearchApi api) { { - const BasicMessageChannel channel = BasicMessageChannel( + const BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.FlutterSearchApi.search', StandardMessageCodec()); - channel.setMessageHandler((dynamic message) async { - final Map mapMessage = - message as Map; - final SearchRequest input = SearchRequest._fromMap(mapMessage); - final SearchReply output = api.search(input); - return output._toMap(); - }); + if (api == null) { + channel.setMessageHandler(null); + } else { + channel.setMessageHandler((Object message) async { + if (message == null) { + return null; + } + final SearchRequest input = SearchRequest.decode(message); + final SearchReply output = api.search(input); + return output.encode(); + }); + } } } } class NestedApi { Future search(Nested arg) async { - final Map requestMap = arg._toMap(); - const BasicMessageChannel channel = BasicMessageChannel( + final Object encoded = arg.encode(); + const BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.NestedApi.search', StandardMessageCodec()); - - final Map replyMap = await channel.send(requestMap); + final Map replyMap = + await channel.send(encoded) as Map; if (replyMap == null) { throw PlatformException( - code: 'channel-error', - message: 'Unable to establish connection on channel.', - details: null); + code: 'channel-error', + message: 'Unable to establish connection on channel.', + details: null, + ); } else if (replyMap['error'] != null) { - final Map error = replyMap['error']; + final Map error = + replyMap['error'] as Map; throw PlatformException( - code: error['code'], - message: error['message'], - details: error['details']); + code: error['code'] as String, + message: error['message'] as String, + details: error['details'], + ); } else { - return SearchReply._fromMap(replyMap['result']); - } - } -} - -abstract class TestNestedApi { - SearchReply search(Nested arg); - static void setup(TestNestedApi api) { - { - const BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.NestedApi.search', StandardMessageCodec()); - channel.setMockMessageHandler((dynamic message) async { - final Map mapMessage = - message as Map; - final Nested input = Nested._fromMap(mapMessage); - final SearchReply output = api.search(input); - return {'result': output._toMap()}; - }); + return SearchReply.decode(replyMap['result']); } } } class Api { Future search(SearchRequest arg) async { - final Map requestMap = arg._toMap(); - const BasicMessageChannel channel = BasicMessageChannel( + final Object encoded = arg.encode(); + const BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.Api.search', StandardMessageCodec()); - - final Map replyMap = await channel.send(requestMap); + final Map replyMap = + await channel.send(encoded) as Map; if (replyMap == null) { throw PlatformException( - code: 'channel-error', - message: 'Unable to establish connection on channel.', - details: null); + code: 'channel-error', + message: 'Unable to establish connection on channel.', + details: null, + ); } else if (replyMap['error'] != null) { - final Map error = replyMap['error']; + final Map error = + replyMap['error'] as Map; throw PlatformException( - code: error['code'], - message: error['message'], - details: error['details']); + code: error['code'] as String, + message: error['message'] as String, + details: error['details'], + ); } else { - return SearchReply._fromMap(replyMap['result']); - } - } -} - -abstract class TestHostApi { - SearchReply search(SearchRequest arg); - static void setup(TestHostApi api) { - { - const BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.Api.search', StandardMessageCodec()); - channel.setMockMessageHandler((dynamic message) async { - final Map mapMessage = - message as Map; - final SearchRequest input = SearchRequest._fromMap(mapMessage); - final SearchReply output = api.search(input); - return {'result': output._toMap()}; - }); + return SearchReply.decode(replyMap['result']); } } } diff --git a/packages/pigeon/mock_handler_tester/test/test.dart b/packages/pigeon/mock_handler_tester/test/test.dart new file mode 100644 index 00000000000..c2dc28e7988 --- /dev/null +++ b/packages/pigeon/mock_handler_tester/test/test.dart @@ -0,0 +1,54 @@ +// Autogenerated from Pigeon (v0.1.15), do not edit directly. +// See also: https://pub.dev/packages/pigeon +// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import +// @dart = 2.8 +import 'dart:async'; +import 'dart:typed_data' show Uint8List, Int32List, Int64List, Float64List; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'message.dart'; + +abstract class TestNestedApi { + SearchReply search(Nested arg); + static void setup(TestNestedApi api) { + { + const BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.NestedApi.search', StandardMessageCodec()); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object message) async { + if (message == null) { + return {}; + } + final Nested input = Nested.decode(message); + final SearchReply output = api.search(input); + return {'result': output.encode()}; + }); + } + } + } +} + +abstract class TestHostApi { + SearchReply search(SearchRequest arg); + static void setup(TestHostApi api) { + { + const BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.Api.search', StandardMessageCodec()); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object message) async { + if (message == null) { + return {}; + } + final SearchRequest input = SearchRequest.decode(message); + final SearchReply output = api.search(input); + return {'result': output.encode()}; + }); + } + } + } +} diff --git a/packages/pigeon/mock_handler_tester/test/widget_test.dart b/packages/pigeon/mock_handler_tester/test/widget_test.dart index 41322b3e9e1..7fba290235a 100644 --- a/packages/pigeon/mock_handler_tester/test/widget_test.dart +++ b/packages/pigeon/mock_handler_tester/test/widget_test.dart @@ -3,7 +3,9 @@ // found in the LICENSE file. import 'package:flutter_test/flutter_test.dart'; + import 'message.dart'; +import 'test.dart'; class Mock implements TestHostApi { bool didCall = false; diff --git a/packages/pigeon/run_tests.sh b/packages/pigeon/run_tests.sh index 0ebfb4a3f5b..b740f5f346b 100755 --- a/packages/pigeon/run_tests.sh +++ b/packages/pigeon/run_tests.sh @@ -19,12 +19,19 @@ framework_path="$flutter_bin/cache/artifacts/engine/ios/" # Functions ############################################################################### +# Create a temporary directory in a way that works on both Linux and macOS. +# +# The mktemp commands have slighly semantics on the BSD systems vs GNU systems. +mktmpdir() { + mktemp -d flutter_pigeon.XXXXXX 2>/dev/null || mktemp -d -t flutter_pigeon. +} + # test_pigeon_ios() # # Compiles the pigeon file to a temp directory and attempts to compile the code # and runs the dart analyzer on the generated dart code. test_pigeon_ios() { - temp_dir=$(mktemp -d -t pigeon) + temp_dir=$(mktmpdir) pub run pigeon \ --input $1 \ @@ -41,8 +48,6 @@ test_pigeon_ios() { -c $temp_dir/pigeon.m \ -o $temp_dir/pigeon.o - dartfmt -w $temp_dir/pigeon.dart - dartanalyzer $temp_dir/pigeon.dart --fatal-infos --fatal-warnings --packages ./e2e_tests/test_objc/.packages rm -rf $temp_dir } @@ -50,7 +55,7 @@ test_pigeon_ios() { # # Compiles the pigeon file to a temp directory and attempts to compile the code. test_pigeon_android() { - temp_dir=$(mktemp -d -t pigeon) + temp_dir=$(mktmpdir) pub run pigeon \ --input $1 \ @@ -64,6 +69,9 @@ test_pigeon_android() { exit 1 fi + dartfmt -w $temp_dir/pigeon.dart + dartanalyzer $temp_dir/pigeon.dart --fatal-infos --fatal-warnings --packages ./e2e_tests/test_objc/.packages + rm -rf $temp_dir } @@ -72,7 +80,7 @@ test_pigeon_android() { # Compiles the pigeon file to a temp directory and attempts to run the dart # analyzer on it with null safety turned on. test_null_safe_dart() { - temp_dir=$(mktemp -d -t pigeon) + temp_dir=$(mktmpdir) pub run pigeon \ --input $1 \ @@ -84,9 +92,10 @@ test_null_safe_dart() { } ############################################################################### -# Dart unit tests +# Dart analysis and unit tests ############################################################################### pub get +dartanalyzer bin lib pub run test test/ ############################################################################### @@ -94,6 +103,20 @@ pub run test test/ ############################################################################### pub run pigeon 1> /dev/null +############################################################################### +# Mock handler flutter tests. +############################################################################### +pushd $PWD +pub run pigeon \ + --input pigeons/message.dart \ + --dart_out mock_handler_tester/test/message.dart \ + --dart_test_out mock_handler_tester/test/test.dart +dartfmt -w mock_handler_tester/test/message.dart +dartfmt -w mock_handler_tester/test/test.dart +cd mock_handler_tester +flutter test +popd + ############################################################################### # Compilation tests (Code is generated and compiled) ############################################################################### @@ -125,18 +148,6 @@ test_pigeon_ios ./pigeons/all_datatypes.dart # Not implemented yet. # test_pigeon_ios ./pigeons/async_handlers.dart -############################################################################### -# Mock handler flutter tests. -############################################################################### -pushd $PWD -pub run pigeon \ - --input pigeons/message.dart \ - --dart_out mock_handler_tester/test/message.dart -dartfmt -w mock_handler_tester/test/message.dart -cd mock_handler_tester -flutter test -popd - ############################################################################### # iOS unit tests on generated code. ############################################################################### diff --git a/packages/pigeon/test/dart_generator_test.dart b/packages/pigeon/test/dart_generator_test.dart index b90e60f6698..075e575bc68 100644 --- a/packages/pigeon/test/dart_generator_test.dart +++ b/packages/pigeon/test/dart_generator_test.dart @@ -54,23 +54,29 @@ void main() { test('nested class', () { final Root root = Root(apis: [], classes: [ Class( - name: 'Input', - fields: [Field(name: 'input', dataType: 'String')]), + name: 'Input', + fields: [Field(name: 'input', dataType: 'String')], + ), Class( - name: 'Nested', - fields: [Field(name: 'nested', dataType: 'Input')]) + name: 'Nested', + fields: [Field(name: 'nested', dataType: 'Input')], + ) ]); final StringBuffer sink = StringBuffer(); generateDart(DartOptions(), root, sink); final String code = sink.toString(); expect( - code, - contains( - 'pigeonMap[\'nested\'] = nested == null ? null : nested._toMap()')); + code, + contains( + 'pigeonMap[\'nested\'] = nested == null ? null : nested.encode()', + ), + ); expect( - code, - contains( - 'result.nested = pigeonMap[\'nested\'] != null ? Input._fromMap(pigeonMap[\'nested\']) : null;')); + code, + contains( + '..nested = pigeonMap[\'nested\'] != null ? Input.decode(pigeonMap[\'nested\']) : null;', + ), + ); }); test('flutterapi', () { @@ -140,7 +146,7 @@ void main() { final String code = sink.toString(); expect(code, isNot(matches('=.*doSomething'))); expect(code, contains('doSomething(')); - expect(code, isNot(contains('._toMap()'))); + expect(code, isNot(contains('.encode()'))); }); test('flutter void argument', () { @@ -214,13 +220,24 @@ void main() { name: 'Output', fields: [Field(name: 'output', dataType: 'String')]) ]); - final StringBuffer sink = StringBuffer(); - generateDart(DartOptions(), root, sink); - final String code = sink.toString(); - expect(code, matches('abstract class ApiMock')); - expect(code, isNot(matches('\.ApiMock\.doSomething'))); - expect(code, matches('\'${Keys.result}\': output._toMap()')); - expect(code, contains('return {};')); + final StringBuffer mainCodeSink = StringBuffer(); + final StringBuffer testCodeSink = StringBuffer(); + generateDart(DartOptions(), root, mainCodeSink); + final String mainCode = mainCodeSink.toString(); + expect(mainCode, isNot(contains('import \'fo\\\'o.dart\';'))); + expect(mainCode, contains('class Api {')); + expect(mainCode, isNot(contains('abstract class ApiMock'))); + expect(mainCode, isNot(contains('\.ApiMock\.doSomething'))); + expect(mainCode, isNot(contains('\'${Keys.result}\': output.encode()'))); + expect(mainCode, isNot(contains('return {};'))); + generateTestDart(DartOptions(), root, testCodeSink, "fo'o.dart"); + final String testCode = testCodeSink.toString(); + expect(testCode, contains('import \'fo\\\'o.dart\';')); + expect(testCode, isNot(contains('class Api {'))); + expect(testCode, contains('abstract class ApiMock')); + expect(testCode, isNot(contains('\.ApiMock\.doSomething'))); + expect(testCode, contains('\'${Keys.result}\': output.encode()')); + expect(testCode, contains('return {};')); }); test('opt out of nndb', () {