mojo/discovery: Attachment support for Dart client
library and adding dart apptests.

Change-Id: I618585332d239c1ea0105202330e7e6871b7d472
diff --git a/.gitignore b/.gitignore
index 34b760f..9460c52 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,6 +3,8 @@
 /lib/gen/dart-pkg
 /lib/gen/mojom
 /packages
+/example/packages
+/test/packages
 /.packages
 /.pub
 /pubspec.lock
diff --git a/Makefile b/Makefile
index 5d924fe..043c70c 100644
--- a/Makefile
+++ b/Makefile
@@ -98,7 +98,7 @@
 	$(call MOGO_TEST,-v vanadium/discovery/internal/...)
 
 .PHONY: apptest
-apptest: mojoapptests $(DISCOVERY_BUILD_DIR)/discovery_apptests.mojo | mojo-env-check
+apptest: build mojoapptests | mojo-env-check
 	$(call MOJO_APPTEST,"mojoapptests")
 
 $(DISCOVERY_BUILD_DIR)/discovery_apptests.mojo: $(V23_GO_FILES) | mojo-env-check
diff --git a/go/src/mojom/vanadium/discovery/discovery.mojom.go b/go/src/mojom/vanadium/discovery/discovery.mojom.go
index f4da926..39cdb12 100644
--- a/go/src/mojom/vanadium/discovery/discovery.mojom.go
+++ b/go/src/mojom/vanadium/discovery/discovery.mojom.go
@@ -50,6 +50,7 @@
 	return discovery_Name
 }
 
+// TODO(rudominer) This should only be defined for top-level interfaces.
 func (f *Discovery_ServiceFactory) ServiceDescription() service_describer.ServiceDescription {
 	return &Discovery_ServiceDescription{}
 }
@@ -691,6 +692,7 @@
 	Create(request Closer_Request)
 }
 
+// TODO(rudominer) This should only be defined for top-level interfaces.
 func (f *Closer_ServiceFactory) ServiceDescription() service_describer.ServiceDescription {
 	return &Closer_ServiceDescription{}
 }
@@ -928,6 +930,7 @@
 	Create(request ScanHandler_Request)
 }
 
+// TODO(rudominer) This should only be defined for top-level interfaces.
 func (f *ScanHandler_ServiceFactory) ServiceDescription() service_describer.ServiceDescription {
 	return &ScanHandler_ServiceDescription{}
 }
@@ -1119,6 +1122,7 @@
 	Create(request Update_Request)
 }
 
+// TODO(rudominer) This should only be defined for top-level interfaces.
 func (f *Update_ServiceFactory) ServiceDescription() service_describer.ServiceDescription {
 	return &Update_ServiceDescription{}
 }
diff --git a/lib/client_impl.dart b/lib/client_impl.dart
index 8c5a0ff..242a78e 100644
--- a/lib/client_impl.dart
+++ b/lib/client_impl.dart
@@ -3,74 +3,111 @@
 // license that can be found in the LICENSE file.
 part of discovery;
 
-typedef Future _StopFunction();
-
 class _Client implements Client {
-  final DiscoveryProxy _discoveryProxy = new DiscoveryProxy.unbound();
+  final mojom.DiscoveryProxy _discoveryProxy =
+      new mojom.DiscoveryProxy.unbound();
 
   _Client(ConnectToServiceFunction cts, String url) {
     cts(url, _discoveryProxy);
   }
 
   Future<Scanner> scan(String query) async {
-    StreamController<ScanUpdate> onUpdate = new StreamController<ScanUpdate>();
-    ScanHandlerStub handlerStub = new ScanHandlerStub.unbound();
+    StreamController<Update> onUpdate = new StreamController<Update>();
+
+    mojom.ScanHandlerStub handlerStub = new mojom.ScanHandlerStub.unbound();
     handlerStub.impl = new _ScanHandler(onUpdate);
 
-    DiscoveryStartScanResponseParams scanResponse =
-        await _discoveryProxy.ptr.startScan(query, handlerStub);
+    mojom.DiscoveryScanResponseParams scanResponse =
+        await _discoveryProxy.ptr.scan(query, handlerStub);
+
     if (scanResponse.err != null) {
       throw scanResponse.err;
     }
 
-    Future stop() {
-      return _discoveryProxy.ptr.stopScan(scanResponse.scanId);
-    }
-    return new _Scanner(stop, onUpdate.stream);
+    return new _Scanner(scanResponse.closer, onUpdate.stream);
   }
 
-  Future<Advertiser> advertise(Service service,
+  Future<Advertiser> advertise(Advertisement advertisement,
       {List<String> visibility: null}) async {
-    DiscoveryStartAdvertisingResponseParams advertiseResponse =
-        await _discoveryProxy.ptr.startAdvertising(service, visibility);
+    mojom.Advertisement mAdvertisement = new mojom.Advertisement()
+      ..id = advertisement.id
+      ..interfaceName = advertisement.interfaceName
+      ..attributes = advertisement.attributes
+      ..attachments = advertisement.attachments
+      ..addresses = advertisement.addresses;
+
+    mojom.DiscoveryAdvertiseResponseParams advertiseResponse =
+        await _discoveryProxy.ptr.advertise(mAdvertisement, visibility);
 
     if (advertiseResponse.err != null) {
       throw advertiseResponse.err;
     }
 
-    Future stop() {
-      return _discoveryProxy.ptr.stopAdvertising(advertiseResponse.instanceId);
-    }
-    return new _Advertiser(stop);
+    return new _Advertiser(advertiseResponse.closer);
   }
 }
 
 class _Scanner implements Scanner {
-  final Stream<ScanUpdate> onUpdate;
+  final Stream<Update> onUpdate;
 
-  final _StopFunction _stop;
-  _Scanner(this._stop, this.onUpdate) {}
+  final mojom.CloserProxy _closer;
+  _Scanner(this._closer, this.onUpdate) {}
 
   Future stop() {
-    return _stop();
+    return _closer.close();
   }
 }
 
 class _Advertiser implements Advertiser {
-  final _StopFunction _stop;
-  _Advertiser(this._stop) {}
+  final mojom.CloserProxy _closer;
+  _Advertiser(this._closer) {}
 
   Future stop() {
-    return _stop();
+    return _closer.close();
   }
 }
 
-class _ScanHandler extends ScanHandler {
-  StreamController<ScanUpdate> _onUpdate;
+class _ScanHandler extends mojom.ScanHandler {
+  StreamController<Update> _onUpdate;
 
   _ScanHandler(this._onUpdate);
 
-  update(ScanUpdate update) {
+  onUpdate(mojom.UpdateProxy mUpdate) async {
+    mojom.UpdateIsLostResponseParams isLostParams = await mUpdate.ptr.isLost();
+    bool isLost = isLostParams.lost;
+
+    mojom.UpdateGetAdvertisementResponseParams advertisementParams =
+        await mUpdate.ptr.getAdvertisement();
+
+    Future<List<int>> attachmentFetcher(String key) async {
+      var attachmentResponse = mUpdate.ptr.getAttachment(key);
+
+      if (!(attachmentResponse is mojom.UpdateGetAttachmentResponseParams)) {
+        throw new ArgumentError.value(key, 'key', 'Attachment does not exist');
+      }
+
+      ByteData data =
+          await mojo_core.DataPipeDrainer.drainHandle(attachmentResponse.data);
+      return data.buffer.asUint8List().toList();
+    }
+
+    mojom.Advertisement mAdvertisement = advertisementParams.ad;
+
+    Update update = new Update._internal(
+        isLost ? UpdateTypes.lost : UpdateTypes.found,
+        attachmentFetcher,
+        mAdvertisement.id,
+        mAdvertisement.interfaceName);
+
+    if (mAdvertisement.attributes != null) {
+      update.attributes = mAdvertisement.attributes;
+    }
+    if (mAdvertisement.addresses != null) {
+      update.addresses = mAdvertisement.addresses;
+    }
+    if (mAdvertisement.attachments != null) {
+      update._attachments = mAdvertisement.attachments;
+    }
     _onUpdate.add(update);
   }
 }
diff --git a/lib/discovery.dart b/lib/discovery.dart
index fefb827..d70ba95 100644
--- a/lib/discovery.dart
+++ b/lib/discovery.dart
@@ -4,12 +4,12 @@
 library discovery;
 
 import 'dart:async';
+import 'dart:typed_data';
 
 import 'package:mojo/bindings.dart' as bindings;
-import 'gen/dart-gen/mojom/lib/discovery/discovery.mojom.dart';
+import 'package:mojo/core.dart' as mojo_core;
 
-export 'gen/dart-gen/mojom/lib/discovery/discovery.mojom.dart'
-    show Service, ScanUpdate, UpdateType;
+import './gen/dart-gen/mojom/lib/discovery/discovery.mojom.dart' as mojom;
 
 part 'client_impl.dart';
 
@@ -20,50 +20,49 @@
     return new _Client(cts, url);
   }
 
-  /// Scan scans services that match the query and returns a scanner handle that
-  /// includes streams of found and lost services.
+  /// Scan scans advertisements that match the query and returns a scanner handle that
+  /// includes streams of found and lost advertisements.
   /// Scanning will continue until [stop] is called on the [Scanner] handle.
   ///
-  /// For example, the following code waits until finding the first service that matches the
+  /// For example, the following code waits until finding the first advertisement that matches the
   /// query and then stops scanning.
   ///
   ///    Scanner scanner = client.scan('v.InterfaceName = "v.io/i" AND v.Attrs["a"] = "v"');
-  ///    Service firstFoundService = await scanner.onUpdate.firstWhere((update) => update.updateType == UpdateTypes.found).service;
+  ///    Update firstFound = await scanner.onUpdate.firstWhere((update) => update.updateType == UpdateTypes.found);
   ///    scanner.stop();
   ///
-  /// The query is a WHERE expression of a syncQL query against advertised services, where
-  /// keys are InstanceIds and values are Services.
+  /// The query is a WHERE expression of a syncQL query against advertisements, where
+  /// keys are Ids and values are Advertisement.
   ///
   /// SyncQL tutorial at:
   ///    https://github.com/vanadium/docs/blob/master/tutorials/syncql-tutorial.md
   Future<Scanner> scan(String query);
 
-  /// Advertise advertises the [Service] to be discovered by [scan].
+  /// Advertise the [Advertisement] to be discovered by [scan].
   /// [visibility] is used to limit the principals that can see the advertisement.
   /// An empty or null [visibility] means that there are no restrictions on visibility.
   /// Advertising will continue until [stop] is called on the [Advertiser] handle.
   ///
-  /// If service.InstanceId is not specified, a random unique identifier will be
-  /// assigned to it. Any change to service will not be applied after advertising starts.
+  /// If advertisement.id is not specified, a random unique identifier will be
+  /// assigned to it. Any change to advertisement will not be applied after advertising starts.
   ///
   /// It is an error to have simultaneously active advertisements for two identical
-  /// instances (service.InstanceId).
+  /// instances (advertisement.id).
   ///
-  /// For example, the following code advertises a service for 10 seconds.
+  /// For example, the following code advertises an advertisement for 10 seconds.
   ///
-  ///   Service service = new Service()
-  ///     ..interfaceName = 'v.io/i'
-  ///     ..attrs = {'a', 'v'};
-  ///   Advertiser advertiser = client.advertise(service);
+  ///   Advertisement ad = new Advertisement('v.io/interfaceName', ['v.io/address']);
+  ///   ad.attributes['a'] = 'v';
+  ///   Advertiser advertiser = client.advertise(ad);
   ///   new Timer(const Duration(seconds: 10), () => advertiser.stop());
-  Future<Advertiser> advertise(Service service,
+  Future<Advertiser> advertise(Advertisement advertisement,
       {List<String> visibility: null});
 }
 
 /// Handle to a scan call.
 abstract class Scanner {
-  /// A stream of [Update] objects as services are found or lost by the scanner.
-  Stream<ScanUpdate> get onUpdate;
+  /// A stream of [Update] objects as advertisements are found or lost by the scanner.
+  Stream<Update> get onUpdate;
 
   /// Stops scanning.
   Future stop();
@@ -74,3 +73,85 @@
   /// Stops the advertisement.
   Future stop();
 }
+
+/// Advertisement represents a feed into advertiser to broadcast its contents
+/// to scanners.
+///
+/// A large advertisement may require additional RPC calls causing delay in
+/// discovery. We limit the maximum size of an advertisement to 512 bytes
+/// excluding id and attachments.
+class Advertisement {
+  /// Universal unique identifier of the advertisement.
+  /// If this is not specified, a random unique identifier will be assigned.
+  List<int> id = null;
+
+  /// Interface name that the advertised service implements.
+  /// E.g., 'v.io/v23/services/vtrace.Store'.
+  String interfaceName = null;
+
+  /// Addresses (vanadium object names) that the advertised service is served on.
+  /// E.g., '/host:port/a/b/c', '/ns.dev.v.io:8101/blah/blah'.
+  List<String> addresses = null;
+
+  /// Attributes as a key/value pair.
+  /// E.g., {'resolution': '1024x768'}.
+  ///
+  /// The key must be US-ASCII printable characters, excluding the '=' character
+  /// and should not start with '_' character.
+  Map<String, String> attributes = new Map<String, String>();
+
+  /// Attachments as a key/value pair.
+  /// E.g., {'thumbnail': binary_data }.
+  ///
+  /// Unlike attributes, attachments are for binary data and they are not queryable.
+  /// We limit the maximum size of a single attachment to 4K bytes.
+  ///
+  /// The key must be US-ASCII printable characters, excluding the '=' character
+  /// and should not start with '_' character.
+  Map<String, List<int>> attachments = new Map<String, List<int>>();
+
+  Advertisement(this.interfaceName, this.addresses);
+}
+
+/// Enum for different types of updates.
+enum UpdateTypes { found, lost }
+
+/// Update represents a discovery update.
+class Update {
+  // The update type.
+  UpdateTypes updateType;
+
+  // The universal unique identifier of the advertisement.
+  List<int> id = null;
+
+  // The interface name that the service implements.
+  String interfaceName = null;
+
+  // The addresses (vanadium object names) that the service
+  // is served on.
+  List<String> addresses = new List<String>();
+
+  // Returns the named attribute. An empty string is returned if
+  // not found.
+  Map<String, String> attributes = new Map<String, String>();
+
+  Map<String, List<int>> _attachments = new Map<String, List<int>>();
+  Function _attachmentFetcher;
+
+  /// Fetches an attachment on-demand from its source.
+  /// ArgumentError is thrown if not found.
+  ///
+  /// This may do RPC calls if the attachment is not fetched yet.
+  ///
+  /// Attachments may not be available when this update is for lost advertisement.
+  Future<List<int>> fetchAttachment(String key) async {
+    if (_attachments.containsKey(key)) {
+      return _attachments[key];
+    }
+
+    return _attachmentFetcher(key);
+  }
+
+  Update._internal(
+      this.updateType, this._attachmentFetcher, this.id, this.interfaceName);
+}
diff --git a/lib/gen/dart-gen/mojom/lib/discovery/discovery.mojom.dart b/lib/gen/dart-gen/mojom/lib/discovery/discovery.mojom.dart
index 20eb267..128c2b8 100644
--- a/lib/gen/dart-gen/mojom/lib/discovery/discovery.mojom.dart
+++ b/lib/gen/dart-gen/mojom/lib/discovery/discovery.mojom.dart
@@ -3,12 +3,9 @@
 // found in the LICENSE file.
 
 library discovery_mojom;
-
 import 'dart:async';
-
 import 'package:mojo/bindings.dart' as bindings;
 import 'package:mojo/core.dart' as core;
-import 'package:mojo/mojo/bindings/types/mojom_types.mojom.dart' as mojom_types;
 import 'package:mojo/mojo/bindings/types/service_describer.mojom.dart' as service_describer;
 
 
@@ -158,69 +155,87 @@
 
   void encode(bindings.Encoder encoder) {
     var encoder0 = encoder.getStructEncoderAtOffset(kVersions.last);
-    
-    encoder0.encodeUint8Array(id, 8, bindings.kArrayNullable, 16);
-    
-    encoder0.encodeString(interfaceName, 16, false);
-    
-    if (addresses == null) {
-      encoder0.encodeNullPointer(24, false);
-    } else {
-      var encoder1 = encoder0.encodePointerArray(addresses.length, 24, bindings.kUnspecifiedArrayLength);
-      for (int i0 = 0; i0 < addresses.length; ++i0) {
+    try {
+      encoder0.encodeUint8Array(id, 8, bindings.kArrayNullable, 16);
+    } on bindings.MojoCodecError catch(e) {
+      e.message = "Error encountered while encoding field "
+          "id of struct Advertisement: $e";
+      rethrow;
+    }
+    try {
+      encoder0.encodeString(interfaceName, 16, false);
+    } on bindings.MojoCodecError catch(e) {
+      e.message = "Error encountered while encoding field "
+          "interfaceName of struct Advertisement: $e";
+      rethrow;
+    }
+    try {
+      if (addresses == null) {
+        encoder0.encodeNullPointer(24, false);
+      } else {
+        var encoder1 = encoder0.encodePointerArray(addresses.length, 24, bindings.kUnspecifiedArrayLength);
+        for (int i0 = 0; i0 < addresses.length; ++i0) {
+          encoder1.encodeString(addresses[i0], bindings.ArrayDataHeader.kHeaderSize + bindings.kPointerSize * i0, false);
+        }
+      }
+    } on bindings.MojoCodecError catch(e) {
+      e.message = "Error encountered while encoding field "
+          "addresses of struct Advertisement: $e";
+      rethrow;
+    }
+    try {
+      if (attributes == null) {
+        encoder0.encodeNullPointer(32, true);
+      } else {
+        var encoder1 = encoder0.encoderForMap(32);
+        var keys0 = attributes.keys.toList();
+        var values0 = attributes.values.toList();
         
-        encoder1.encodeString(addresses[i0], bindings.ArrayDataHeader.kHeaderSize + bindings.kPointerSize * i0, false);
+        {
+          var encoder2 = encoder1.encodePointerArray(keys0.length, bindings.ArrayDataHeader.kHeaderSize, bindings.kUnspecifiedArrayLength);
+          for (int i1 = 0; i1 < keys0.length; ++i1) {
+            encoder2.encodeString(keys0[i1], bindings.ArrayDataHeader.kHeaderSize + bindings.kPointerSize * i1, false);
+          }
+        }
+        
+        {
+          var encoder2 = encoder1.encodePointerArray(values0.length, bindings.ArrayDataHeader.kHeaderSize + bindings.kPointerSize, bindings.kUnspecifiedArrayLength);
+          for (int i1 = 0; i1 < values0.length; ++i1) {
+            encoder2.encodeString(values0[i1], bindings.ArrayDataHeader.kHeaderSize + bindings.kPointerSize * i1, false);
+          }
+        }
       }
+    } on bindings.MojoCodecError catch(e) {
+      e.message = "Error encountered while encoding field "
+          "attributes of struct Advertisement: $e";
+      rethrow;
     }
-    
-    if (attributes == null) {
-      encoder0.encodeNullPointer(32, true);
-    } else {
-      var encoder1 = encoder0.encoderForMap(32);
-      int size0 = attributes.length;
-      var keys0 = attributes.keys.toList();
-      var values0 = attributes.values.toList();
-      
-      {
-        var encoder2 = encoder1.encodePointerArray(keys0.length, bindings.ArrayDataHeader.kHeaderSize, bindings.kUnspecifiedArrayLength);
-        for (int i1 = 0; i1 < keys0.length; ++i1) {
-          
-          encoder2.encodeString(keys0[i1], bindings.ArrayDataHeader.kHeaderSize + bindings.kPointerSize * i1, false);
+    try {
+      if (attachments == null) {
+        encoder0.encodeNullPointer(40, true);
+      } else {
+        var encoder1 = encoder0.encoderForMap(40);
+        var keys0 = attachments.keys.toList();
+        var values0 = attachments.values.toList();
+        
+        {
+          var encoder2 = encoder1.encodePointerArray(keys0.length, bindings.ArrayDataHeader.kHeaderSize, bindings.kUnspecifiedArrayLength);
+          for (int i1 = 0; i1 < keys0.length; ++i1) {
+            encoder2.encodeString(keys0[i1], bindings.ArrayDataHeader.kHeaderSize + bindings.kPointerSize * i1, false);
+          }
+        }
+        
+        {
+          var encoder2 = encoder1.encodePointerArray(values0.length, bindings.ArrayDataHeader.kHeaderSize + bindings.kPointerSize, bindings.kUnspecifiedArrayLength);
+          for (int i1 = 0; i1 < values0.length; ++i1) {
+            encoder2.encodeUint8Array(values0[i1], bindings.ArrayDataHeader.kHeaderSize + bindings.kPointerSize * i1, bindings.kNothingNullable, bindings.kUnspecifiedArrayLength);
+          }
         }
       }
-      
-      {
-        var encoder2 = encoder1.encodePointerArray(values0.length, bindings.ArrayDataHeader.kHeaderSize + bindings.kPointerSize, bindings.kUnspecifiedArrayLength);
-        for (int i1 = 0; i1 < values0.length; ++i1) {
-          
-          encoder2.encodeString(values0[i1], bindings.ArrayDataHeader.kHeaderSize + bindings.kPointerSize * i1, false);
-        }
-      }
-    }
-    
-    if (attachments == null) {
-      encoder0.encodeNullPointer(40, true);
-    } else {
-      var encoder1 = encoder0.encoderForMap(40);
-      int size0 = attachments.length;
-      var keys0 = attachments.keys.toList();
-      var values0 = attachments.values.toList();
-      
-      {
-        var encoder2 = encoder1.encodePointerArray(keys0.length, bindings.ArrayDataHeader.kHeaderSize, bindings.kUnspecifiedArrayLength);
-        for (int i1 = 0; i1 < keys0.length; ++i1) {
-          
-          encoder2.encodeString(keys0[i1], bindings.ArrayDataHeader.kHeaderSize + bindings.kPointerSize * i1, false);
-        }
-      }
-      
-      {
-        var encoder2 = encoder1.encodePointerArray(values0.length, bindings.ArrayDataHeader.kHeaderSize + bindings.kPointerSize, bindings.kUnspecifiedArrayLength);
-        for (int i1 = 0; i1 < values0.length; ++i1) {
-          
-          encoder2.encodeUint8Array(values0[i1], bindings.ArrayDataHeader.kHeaderSize + bindings.kPointerSize * i1, bindings.kNothingNullable, bindings.kUnspecifiedArrayLength);
-        }
-      }
+    } on bindings.MojoCodecError catch(e) {
+      e.message = "Error encountered while encoding field "
+          "attachments of struct Advertisement: $e";
+      rethrow;
     }
   }
 
@@ -307,12 +322,27 @@
 
   void encode(bindings.Encoder encoder) {
     var encoder0 = encoder.getStructEncoderAtOffset(kVersions.last);
-    
-    encoder0.encodeString(id, 8, false);
-    
-    encoder0.encodeUint32(actionCode, 16);
-    
-    encoder0.encodeString(msg, 24, false);
+    try {
+      encoder0.encodeString(id, 8, false);
+    } on bindings.MojoCodecError catch(e) {
+      e.message = "Error encountered while encoding field "
+          "id of struct Error: $e";
+      rethrow;
+    }
+    try {
+      encoder0.encodeUint32(actionCode, 16);
+    } on bindings.MojoCodecError catch(e) {
+      e.message = "Error encountered while encoding field "
+          "actionCode of struct Error: $e";
+      rethrow;
+    }
+    try {
+      encoder0.encodeString(msg, 24, false);
+    } on bindings.MojoCodecError catch(e) {
+      e.message = "Error encountered while encoding field "
+          "msg of struct Error: $e";
+      rethrow;
+    }
   }
 
   String toString() {
@@ -400,17 +430,26 @@
 
   void encode(bindings.Encoder encoder) {
     var encoder0 = encoder.getStructEncoderAtOffset(kVersions.last);
-    
-    encoder0.encodeStruct(ad, 8, false);
-    
-    if (visibility == null) {
-      encoder0.encodeNullPointer(16, true);
-    } else {
-      var encoder1 = encoder0.encodePointerArray(visibility.length, 16, bindings.kUnspecifiedArrayLength);
-      for (int i0 = 0; i0 < visibility.length; ++i0) {
-        
-        encoder1.encodeString(visibility[i0], bindings.ArrayDataHeader.kHeaderSize + bindings.kPointerSize * i0, false);
+    try {
+      encoder0.encodeStruct(ad, 8, false);
+    } on bindings.MojoCodecError catch(e) {
+      e.message = "Error encountered while encoding field "
+          "ad of struct _DiscoveryAdvertiseParams: $e";
+      rethrow;
+    }
+    try {
+      if (visibility == null) {
+        encoder0.encodeNullPointer(16, true);
+      } else {
+        var encoder1 = encoder0.encodePointerArray(visibility.length, 16, bindings.kUnspecifiedArrayLength);
+        for (int i0 = 0; i0 < visibility.length; ++i0) {
+          encoder1.encodeString(visibility[i0], bindings.ArrayDataHeader.kHeaderSize + bindings.kPointerSize * i0, false);
+        }
       }
+    } on bindings.MojoCodecError catch(e) {
+      e.message = "Error encountered while encoding field "
+          "visibility of struct _DiscoveryAdvertiseParams: $e";
+      rethrow;
     }
   }
 
@@ -492,12 +531,27 @@
 
   void encode(bindings.Encoder encoder) {
     var encoder0 = encoder.getStructEncoderAtOffset(kVersions.last);
-    
-    encoder0.encodeUint8Array(instanceId, 8, bindings.kArrayNullable, 16);
-    
-    encoder0.encodeInterface(closer, 16, true);
-    
-    encoder0.encodeStruct(err, 24, true);
+    try {
+      encoder0.encodeUint8Array(instanceId, 8, bindings.kArrayNullable, 16);
+    } on bindings.MojoCodecError catch(e) {
+      e.message = "Error encountered while encoding field "
+          "instanceId of struct DiscoveryAdvertiseResponseParams: $e";
+      rethrow;
+    }
+    try {
+      encoder0.encodeInterface(closer, 16, true);
+    } on bindings.MojoCodecError catch(e) {
+      e.message = "Error encountered while encoding field "
+          "closer of struct DiscoveryAdvertiseResponseParams: $e";
+      rethrow;
+    }
+    try {
+      encoder0.encodeStruct(err, 24, true);
+    } on bindings.MojoCodecError catch(e) {
+      e.message = "Error encountered while encoding field "
+          "err of struct DiscoveryAdvertiseResponseParams: $e";
+      rethrow;
+    }
   }
 
   String toString() {
@@ -571,10 +625,20 @@
 
   void encode(bindings.Encoder encoder) {
     var encoder0 = encoder.getStructEncoderAtOffset(kVersions.last);
-    
-    encoder0.encodeString(query, 8, false);
-    
-    encoder0.encodeInterface(handler, 16, false);
+    try {
+      encoder0.encodeString(query, 8, false);
+    } on bindings.MojoCodecError catch(e) {
+      e.message = "Error encountered while encoding field "
+          "query of struct _DiscoveryScanParams: $e";
+      rethrow;
+    }
+    try {
+      encoder0.encodeInterface(handler, 16, false);
+    } on bindings.MojoCodecError catch(e) {
+      e.message = "Error encountered while encoding field "
+          "handler of struct _DiscoveryScanParams: $e";
+      rethrow;
+    }
   }
 
   String toString() {
@@ -648,10 +712,20 @@
 
   void encode(bindings.Encoder encoder) {
     var encoder0 = encoder.getStructEncoderAtOffset(kVersions.last);
-    
-    encoder0.encodeInterface(closer, 8, true);
-    
-    encoder0.encodeStruct(err, 16, true);
+    try {
+      encoder0.encodeInterface(closer, 8, true);
+    } on bindings.MojoCodecError catch(e) {
+      e.message = "Error encountered while encoding field "
+          "closer of struct DiscoveryScanResponseParams: $e";
+      rethrow;
+    }
+    try {
+      encoder0.encodeStruct(err, 16, true);
+    } on bindings.MojoCodecError catch(e) {
+      e.message = "Error encountered while encoding field "
+          "err of struct DiscoveryScanResponseParams: $e";
+      rethrow;
+    }
   }
 
   String toString() {
@@ -839,8 +913,13 @@
 
   void encode(bindings.Encoder encoder) {
     var encoder0 = encoder.getStructEncoderAtOffset(kVersions.last);
-    
-    encoder0.encodeInterface(update, 8, false);
+    try {
+      encoder0.encodeInterface(update, 8, false);
+    } on bindings.MojoCodecError catch(e) {
+      e.message = "Error encountered while encoding field "
+          "update of struct _ScanHandlerOnUpdateParams: $e";
+      rethrow;
+    }
   }
 
   String toString() {
@@ -967,8 +1046,13 @@
 
   void encode(bindings.Encoder encoder) {
     var encoder0 = encoder.getStructEncoderAtOffset(kVersions.last);
-    
-    encoder0.encodeBool(lost, 8, 0);
+    try {
+      encoder0.encodeBool(lost, 8, 0);
+    } on bindings.MojoCodecError catch(e) {
+      e.message = "Error encountered while encoding field "
+          "lost of struct UpdateIsLostResponseParams: $e";
+      rethrow;
+    }
   }
 
   String toString() {
@@ -1096,8 +1180,13 @@
 
   void encode(bindings.Encoder encoder) {
     var encoder0 = encoder.getStructEncoderAtOffset(kVersions.last);
-    
-    encoder0.encodeUint8Array(id, 8, bindings.kNothingNullable, 16);
+    try {
+      encoder0.encodeUint8Array(id, 8, bindings.kNothingNullable, 16);
+    } on bindings.MojoCodecError catch(e) {
+      e.message = "Error encountered while encoding field "
+          "id of struct UpdateGetIdResponseParams: $e";
+      rethrow;
+    }
   }
 
   String toString() {
@@ -1225,8 +1314,13 @@
 
   void encode(bindings.Encoder encoder) {
     var encoder0 = encoder.getStructEncoderAtOffset(kVersions.last);
-    
-    encoder0.encodeString(interfaceName, 8, false);
+    try {
+      encoder0.encodeString(interfaceName, 8, false);
+    } on bindings.MojoCodecError catch(e) {
+      e.message = "Error encountered while encoding field "
+          "interfaceName of struct UpdateGetInterfaceNameResponseParams: $e";
+      rethrow;
+    }
   }
 
   String toString() {
@@ -1362,15 +1456,19 @@
 
   void encode(bindings.Encoder encoder) {
     var encoder0 = encoder.getStructEncoderAtOffset(kVersions.last);
-    
-    if (addresses == null) {
-      encoder0.encodeNullPointer(8, false);
-    } else {
-      var encoder1 = encoder0.encodePointerArray(addresses.length, 8, bindings.kUnspecifiedArrayLength);
-      for (int i0 = 0; i0 < addresses.length; ++i0) {
-        
-        encoder1.encodeString(addresses[i0], bindings.ArrayDataHeader.kHeaderSize + bindings.kPointerSize * i0, false);
+    try {
+      if (addresses == null) {
+        encoder0.encodeNullPointer(8, false);
+      } else {
+        var encoder1 = encoder0.encodePointerArray(addresses.length, 8, bindings.kUnspecifiedArrayLength);
+        for (int i0 = 0; i0 < addresses.length; ++i0) {
+          encoder1.encodeString(addresses[i0], bindings.ArrayDataHeader.kHeaderSize + bindings.kPointerSize * i0, false);
+        }
       }
+    } on bindings.MojoCodecError catch(e) {
+      e.message = "Error encountered while encoding field "
+          "addresses of struct UpdateGetAddressesResponseParams: $e";
+      rethrow;
     }
   }
 
@@ -1439,8 +1537,13 @@
 
   void encode(bindings.Encoder encoder) {
     var encoder0 = encoder.getStructEncoderAtOffset(kVersions.last);
-    
-    encoder0.encodeString(name, 8, false);
+    try {
+      encoder0.encodeString(name, 8, false);
+    } on bindings.MojoCodecError catch(e) {
+      e.message = "Error encountered while encoding field "
+          "name of struct _UpdateGetAttributeParams: $e";
+      rethrow;
+    }
   }
 
   String toString() {
@@ -1508,8 +1611,13 @@
 
   void encode(bindings.Encoder encoder) {
     var encoder0 = encoder.getStructEncoderAtOffset(kVersions.last);
-    
-    encoder0.encodeString(attribute, 8, false);
+    try {
+      encoder0.encodeString(attribute, 8, false);
+    } on bindings.MojoCodecError catch(e) {
+      e.message = "Error encountered while encoding field "
+          "attribute of struct UpdateGetAttributeResponseParams: $e";
+      rethrow;
+    }
   }
 
   String toString() {
@@ -1577,8 +1685,13 @@
 
   void encode(bindings.Encoder encoder) {
     var encoder0 = encoder.getStructEncoderAtOffset(kVersions.last);
-    
-    encoder0.encodeString(name, 8, false);
+    try {
+      encoder0.encodeString(name, 8, false);
+    } on bindings.MojoCodecError catch(e) {
+      e.message = "Error encountered while encoding field "
+          "name of struct _UpdateGetAttachmentParams: $e";
+      rethrow;
+    }
   }
 
   String toString() {
@@ -1646,8 +1759,13 @@
 
   void encode(bindings.Encoder encoder) {
     var encoder0 = encoder.getStructEncoderAtOffset(kVersions.last);
-    
-    encoder0.encodeConsumerHandle(data, 8, false);
+    try {
+      encoder0.encodeConsumerHandle(data, 8, false);
+    } on bindings.MojoCodecError catch(e) {
+      e.message = "Error encountered while encoding field "
+          "data of struct UpdateGetAttachmentResponseParams: $e";
+      rethrow;
+    }
   }
 
   String toString() {
@@ -1775,8 +1893,13 @@
 
   void encode(bindings.Encoder encoder) {
     var encoder0 = encoder.getStructEncoderAtOffset(kVersions.last);
-    
-    encoder0.encodeStruct(ad, 8, false);
+    try {
+      encoder0.encodeStruct(ad, 8, false);
+    } on bindings.MojoCodecError catch(e) {
+      e.message = "Error encountered while encoding field "
+          "ad of struct UpdateGetAdvertisementResponseParams: $e";
+      rethrow;
+    }
   }
 
   String toString() {
@@ -1800,11 +1923,14 @@
 
 
 class _DiscoveryServiceDescription implements service_describer.ServiceDescription {
-  dynamic getTopLevelInterface([Function responseFactory]) => null;
+  dynamic getTopLevelInterface([Function responseFactory]) =>
+      responseFactory(null);
 
-  dynamic getTypeDefinition(String typeKey, [Function responseFactory]) => null;
+  dynamic getTypeDefinition(String typeKey, [Function responseFactory]) =>
+      responseFactory(null);
 
-  dynamic getAllTypeDefinitions([Function responseFactory]) => null;
+  dynamic getAllTypeDefinitions([Function responseFactory]) =>
+      responseFactory(null);
 }
 
 abstract class Discovery {
@@ -2079,8 +2205,13 @@
 
   int get version => 0;
 
-  service_describer.ServiceDescription get serviceDescription =>
-    new _DiscoveryServiceDescription();
+  static service_describer.ServiceDescription _cachedServiceDescription;
+  static service_describer.ServiceDescription get serviceDescription {
+    if (_cachedServiceDescription == null) {
+      _cachedServiceDescription = new _DiscoveryServiceDescription();
+    }
+    return _cachedServiceDescription;
+  }
 }
 
 const int _Closer_closeName = 0;
@@ -2088,11 +2219,14 @@
 
 
 class _CloserServiceDescription implements service_describer.ServiceDescription {
-  dynamic getTopLevelInterface([Function responseFactory]) => null;
+  dynamic getTopLevelInterface([Function responseFactory]) =>
+      responseFactory(null);
 
-  dynamic getTypeDefinition(String typeKey, [Function responseFactory]) => null;
+  dynamic getTypeDefinition(String typeKey, [Function responseFactory]) =>
+      responseFactory(null);
 
-  dynamic getAllTypeDefinitions([Function responseFactory]) => null;
+  dynamic getAllTypeDefinitions([Function responseFactory]) =>
+      responseFactory(null);
 }
 
 abstract class Closer {
@@ -2262,8 +2396,6 @@
     assert(_impl != null);
     switch (message.header.type) {
       case _Closer_closeName:
-        var params = _CloserCloseParams.deserialize(
-            message.payload);
         var response = _impl.close(_CloserCloseResponseParamsFactory);
         if (response is Future) {
           return response.then((response) {
@@ -2303,8 +2435,13 @@
 
   int get version => 0;
 
-  service_describer.ServiceDescription get serviceDescription =>
-    new _CloserServiceDescription();
+  static service_describer.ServiceDescription _cachedServiceDescription;
+  static service_describer.ServiceDescription get serviceDescription {
+    if (_cachedServiceDescription == null) {
+      _cachedServiceDescription = new _CloserServiceDescription();
+    }
+    return _cachedServiceDescription;
+  }
 }
 
 const int _ScanHandler_onUpdateName = 0;
@@ -2312,11 +2449,14 @@
 
 
 class _ScanHandlerServiceDescription implements service_describer.ServiceDescription {
-  dynamic getTopLevelInterface([Function responseFactory]) => null;
+  dynamic getTopLevelInterface([Function responseFactory]) =>
+      responseFactory(null);
 
-  dynamic getTypeDefinition(String typeKey, [Function responseFactory]) => null;
+  dynamic getTypeDefinition(String typeKey, [Function responseFactory]) =>
+      responseFactory(null);
 
-  dynamic getAllTypeDefinitions([Function responseFactory]) => null;
+  dynamic getAllTypeDefinitions([Function responseFactory]) =>
+      responseFactory(null);
 }
 
 abstract class ScanHandler {
@@ -2487,8 +2627,13 @@
 
   int get version => 0;
 
-  service_describer.ServiceDescription get serviceDescription =>
-    new _ScanHandlerServiceDescription();
+  static service_describer.ServiceDescription _cachedServiceDescription;
+  static service_describer.ServiceDescription get serviceDescription {
+    if (_cachedServiceDescription == null) {
+      _cachedServiceDescription = new _ScanHandlerServiceDescription();
+    }
+    return _cachedServiceDescription;
+  }
 }
 
 const int _Update_isLostName = 0;
@@ -2502,11 +2647,14 @@
 
 
 class _UpdateServiceDescription implements service_describer.ServiceDescription {
-  dynamic getTopLevelInterface([Function responseFactory]) => null;
+  dynamic getTopLevelInterface([Function responseFactory]) =>
+      responseFactory(null);
 
-  dynamic getTypeDefinition(String typeKey, [Function responseFactory]) => null;
+  dynamic getTypeDefinition(String typeKey, [Function responseFactory]) =>
+      responseFactory(null);
 
-  dynamic getAllTypeDefinitions([Function responseFactory]) => null;
+  dynamic getAllTypeDefinitions([Function responseFactory]) =>
+      responseFactory(null);
 }
 
 abstract class Update {
@@ -2883,8 +3031,6 @@
     assert(_impl != null);
     switch (message.header.type) {
       case _Update_isLostName:
-        var params = _UpdateIsLostParams.deserialize(
-            message.payload);
         var response = _impl.isLost(_UpdateIsLostResponseParamsFactory);
         if (response is Future) {
           return response.then((response) {
@@ -2905,8 +3051,6 @@
         }
         break;
       case _Update_getIdName:
-        var params = _UpdateGetIdParams.deserialize(
-            message.payload);
         var response = _impl.getId(_UpdateGetIdResponseParamsFactory);
         if (response is Future) {
           return response.then((response) {
@@ -2927,8 +3071,6 @@
         }
         break;
       case _Update_getInterfaceNameName:
-        var params = _UpdateGetInterfaceNameParams.deserialize(
-            message.payload);
         var response = _impl.getInterfaceName(_UpdateGetInterfaceNameResponseParamsFactory);
         if (response is Future) {
           return response.then((response) {
@@ -2949,8 +3091,6 @@
         }
         break;
       case _Update_getAddressesName:
-        var params = _UpdateGetAddressesParams.deserialize(
-            message.payload);
         var response = _impl.getAddresses(_UpdateGetAddressesResponseParamsFactory);
         if (response is Future) {
           return response.then((response) {
@@ -3015,8 +3155,6 @@
         }
         break;
       case _Update_getAdvertisementName:
-        var params = _UpdateGetAdvertisementParams.deserialize(
-            message.payload);
         var response = _impl.getAdvertisement(_UpdateGetAdvertisementResponseParamsFactory);
         if (response is Future) {
           return response.then((response) {
@@ -3056,8 +3194,13 @@
 
   int get version => 0;
 
-  service_describer.ServiceDescription get serviceDescription =>
-    new _UpdateServiceDescription();
+  static service_describer.ServiceDescription _cachedServiceDescription;
+  static service_describer.ServiceDescription get serviceDescription {
+    if (_cachedServiceDescription == null) {
+      _cachedServiceDescription = new _UpdateServiceDescription();
+    }
+    return _cachedServiceDescription;
+  }
 }
 
 
diff --git a/mojoapptests b/mojoapptests
index fe6124e..bb0da47 100644
--- a/mojoapptests
+++ b/mojoapptests
@@ -7,4 +7,9 @@
     "test-args": [],
     "shell-args": [],
   },
+  {
+    "test": "https://dart.test.mojo.v.io/discovery_apptests.dart",
+    "type": "dart",
+    "dart_strict_mode": True
+  }
 ]
diff --git a/mojoconfig b/mojoconfig
index 1aeabf8..1240d0c 100644
--- a/mojoconfig
+++ b/mojoconfig
@@ -11,5 +11,14 @@
         ]),
       ],
     },
+    {
+      'host': 'https://dart.test.mojo.v.io/',
+      'port': 32001,
+      'mappings': [
+        ('', [
+          '@{DISCOVERY_DIR}/test',
+        ]),
+      ],
+    },
   ],
 }
diff --git a/pubspec.yaml b/pubspec.yaml
index a01a5cc..ff2bddf 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -2,11 +2,11 @@
 description: Discovery is a discovery system for developers that makes it easy to advertise apps and scan for them. It works over MDNS and BLE.
 homepage: https://github.com/vanadium/mojo.discovery
 name: v23discovery
-version: 0.0.14
+version: 0.0.16
 dependencies:
-  mojo_sdk: 0.2.15
+  mojo_sdk: 0.2.17
 dev_dependencies:
   dart_style: any
-  test: any
+  mojo_apptest: any
 environment:
   sdk: '>=1.12.0 <2.0.0'
diff --git a/test/discovery_apptests.dart b/test/discovery_apptests.dart
new file mode 100644
index 0000000..58038ec
--- /dev/null
+++ b/test/discovery_apptests.dart
@@ -0,0 +1,50 @@
+#!mojo mojo:dart_content_handler?strict=true
+// Copyright 2015 The Vanadium 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:convert';
+
+import 'package:mojo_apptest/apptest.dart';
+import 'package:mojo/application.dart';
+
+import 'package:v23discovery/discovery.dart';
+
+main(List args, Object handleToken) {
+  runAppTests(handleToken, [discoveryApptests]);
+}
+
+discoveryApptests(Application application, String url) {
+  test('Advertise and Scan', () async {
+    const String mojoUrl = 'https://mojo.v.io/discovery.mojo';
+    const String interfaceName = 'v.io/myInterface';
+
+    // Advertise
+    Client client1 = new Client(application.connectToService, mojoUrl);
+    Advertisement input = new Advertisement(
+        interfaceName, ['v.io/myAddress1', 'v.io/myAddress2']);
+    input.attributes['myAttr1'] = 'myAttrValue1';
+    input.attributes['myAttr2'] = 'myAttrValue2';
+    String attachmentContent = new List.generate(500, (_) => 'X').join();
+    input.attachments['myAttachment'] = UTF8.encode(attachmentContent);
+    Advertiser advertiser = await client1.advertise(input);
+
+    // Scan
+    Client client2 = new Client(application.connectToService, mojoUrl);
+    Scanner scanner = await client2.scan('v.InterfaceName = "$interfaceName"');
+    Update update = await scanner.onUpdate.first;
+
+    expect(update.updateType, equals(UpdateTypes.found));
+    expect(update.id, isNotEmpty);
+    expect(update.interfaceName, equals(interfaceName));
+    expect(update.addresses, equals(input.addresses));
+    expect(update.attributes, equals(input.attributes));
+    expect(UTF8.decode(await update.fetchAttachment('myAttachment')),
+        equals(attachmentContent));
+    expect(update.fetchAttachment('badAttachmentKey'), throwsArgumentError);
+
+    // Clean up
+    await advertiser.stop();
+    await scanner.stop();
+  });
+}