discovery(mojo): add apptest.

MultiPart: 2/2
Change-Id: Ib88656ca6b6e49fe295f4984c8a7d5e0d5965602
diff --git a/Makefile b/Makefile
index 5175e8a..5d924fe 100644
--- a/Makefile
+++ b/Makefile
@@ -33,15 +33,24 @@
 packages:
 	pub upgrade
 
+# Build mojo app.
 .PHONY: build
 build: packages gen-mojom $(DISCOVERY_BUILD_DIR)/discovery.mojo
 
-.PHONY: test
-test: discovery-test
-
 .PHONY: gen-mojom
 gen-mojom: go/src/mojom/vanadium/discovery/discovery.mojom.go lib/gen/dart-gen/mojom/lib/mojo/discovery.mojom.dart java/generated-src/io/v/mojo/discovery/Advertiser.java
 
+go/src/mojom/vanadium/discovery/discovery.mojom.go: mojom/vanadium/discovery.mojom | mojo-env-check
+	$(call MOJOM_GEN,$<,.,.,go)
+	gofmt -w $@
+
+lib/gen/dart-gen/mojom/lib/mojo/discovery.mojom.dart: mojom/vanadium/discovery.mojom | mojo-env-check
+	$(call MOJOM_GEN,$<,.,lib/gen,dart)
+	# TODO(nlacasse): mojom_bindings_generator creates bad symlinks on dart
+	# files, so we delete them.  Stop doing this once the generator is fixed.
+	# See https://github.com/domokit/mojo/issues/386
+	rm -f lib/gen/mojom/$(notdir $@)
+
 # Note: These Java files are checked in.
 java/generated-src/io/v/mojo/discovery/Advertiser.java: java/generated-src/mojom/vanadium/discovery.mojom.srcjar
 	cd java/generated-src/ && jar -xf mojom/vanadium/discovery.mojom.srcjar
@@ -54,11 +63,6 @@
 	mkdir -p java/generated-src/mojom/vanadium
 	$(call MOJOM_GEN,$<,.,java/generated-src,java)
 
-go/src/mojom/vanadium/discovery/discovery.mojom.go: mojom/vanadium/discovery.mojom | mojo-env-check
-	$(call MOJOM_GEN,$<,.,.,go)
-	gofmt -w $@
-
-
 ifdef ANDROID
 gradle-build:
 	cd java && ./gradlew build
@@ -81,28 +85,26 @@
 	echo "#!mojo mojo:java_handler" > $@
 	cat build/discovery.zip >> $@
 else
-
 $(DISCOVERY_BUILD_DIR)/discovery.mojo: $(V23_GO_FILES) $(MOJO_SHARED_LIB) | mojo-env-check
 	$(call MOGO_BUILD,vanadium/discovery,$@)
 endif
 
+# Tests
+.PHONY: test
+test: unittest apptest
 
-lib/gen/dart-gen/mojom/lib/mojo/discovery.mojom.dart: mojom/vanadium/discovery.mojom | mojo-env-check
-	$(call MOJOM_GEN,$<,.,lib/gen,dart)
-	# TODO(nlacasse): mojom_bindings_generator creates bad symlinks on dart
-	# files, so we delete them.  Stop doing this once the generator is fixed.
-	# See https://github.com/domokit/mojo/issues/386
-	rm -f lib/gen/mojom/$(notdir $@)
-
-discovery-test: $(V23_GO_FILES) go/src/mojom/vanadium/discovery/discovery.mojom.go | mojo-env-check
+.PHONY: unittest
+unittest: $(V23_GO_FILES) go/src/mojom/vanadium/discovery/discovery.mojom.go | mojo-env-check
 	$(call MOGO_TEST,-v vanadium/discovery/internal/...)
 
-clean:
-	rm -rf gen
-	rm -rf lib/gen/dart-pkg
-	rm -rf lib/gen/mojom
-	rm -rf $(PACKAGE_MOJO_BIN_DIR)
+.PHONY: apptest
+apptest: mojoapptests $(DISCOVERY_BUILD_DIR)/discovery_apptests.mojo | mojo-env-check
+	$(call MOJO_APPTEST,"mojoapptests")
 
+$(DISCOVERY_BUILD_DIR)/discovery_apptests.mojo: $(V23_GO_FILES) | mojo-env-check
+	$(call MOGO_BUILD,vanadium/discovery/internal/apptest/main,$@)
+
+# Publish
 .PHONY: publish
 # NOTE(aghassemi): This must be inside lib in order to be accessible.
 PACKAGE_MOJO_BIN_DIR := lib/mojo_services
@@ -129,7 +131,14 @@
 	mkdir -p $(PACKAGE_MOJO_BIN_DIR)
 	cp -r gen/mojo/* $(PACKAGE_MOJO_BIN_DIR)
 
-# Examples.
+# Cleanup
+clean:
+	rm -rf gen
+	rm -rf lib/gen/dart-pkg
+	rm -rf lib/gen/mojom
+	rm -rf $(PACKAGE_MOJO_BIN_DIR)
+
+# Examples
 run-advertiser: $(DISCOVERY_BUILD_DIR)/advertiser.mojo $(DISCOVERY_BUILD_DIR)/discovery.mojo
 	$(call MOJO_RUN,"https://mojo.v.io/advertiser.mojo")
 
diff --git a/go/src/vanadium/discovery/discovery.go b/go/src/vanadium/discovery/discovery.go
index 229dd68..e81e0b1 100644
--- a/go/src/vanadium/discovery/discovery.go
+++ b/go/src/vanadium/discovery/discovery.go
@@ -2,6 +2,8 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
+// +build mojo
+
 package main
 
 import (
diff --git a/go/src/vanadium/discovery/internal/apptest/apptest.go b/go/src/vanadium/discovery/internal/apptest/apptest.go
new file mode 100644
index 0000000..6670a7d
--- /dev/null
+++ b/go/src/vanadium/discovery/internal/apptest/apptest.go
@@ -0,0 +1,34 @@
+// Copyright 2016 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.
+
+// +build mojo
+
+package apptest
+
+import (
+	"reflect"
+	"regexp"
+	"runtime"
+	"strings"
+	"testing"
+
+	"mojo/public/go/application"
+)
+
+func RunAppTests(mctx application.Context) int {
+	apptests := []func(*testing.T, application.Context){
+		AppTestBasic,
+	}
+
+	var tests []testing.InternalTest
+	for _, apptest := range apptests {
+		qname := runtime.FuncForPC(reflect.ValueOf(apptest).Pointer()).Name()
+		name := qname[strings.LastIndex(qname, ".")+1:]
+		tests = append(tests, testing.InternalTest{name, func(t *testing.T) { apptest(t, mctx) }})
+	}
+
+	// MainStart is not supposed to be called directly, but there is no other way
+	// to run tests programatically at run time.
+	return testing.MainStart(regexp.MatchString, tests, nil, nil).Run()
+}
diff --git a/go/src/vanadium/discovery/internal/apptest/discovery_apptest.go b/go/src/vanadium/discovery/internal/apptest/discovery_apptest.go
new file mode 100644
index 0000000..3c88b23
--- /dev/null
+++ b/go/src/vanadium/discovery/internal/apptest/discovery_apptest.go
@@ -0,0 +1,286 @@
+// Copyright 2016 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.
+
+// +build mojo
+
+package apptest
+
+import (
+	"bytes"
+	"errors"
+	"fmt"
+	"log"
+	"reflect"
+	"testing"
+	"time"
+
+	"mojo/public/go/application"
+	"mojo/public/go/bindings"
+	"mojo/public/go/system"
+
+	"mojom/vanadium/discovery"
+
+	idiscovery "v.io/x/ref/lib/discovery"
+	dfactory "v.io/x/ref/lib/discovery/factory"
+	"v.io/x/ref/lib/discovery/plugins/mock"
+	_ "v.io/x/ref/runtime/factories/generic"
+	"v.io/x/ref/test"
+
+	"vanadium/discovery/internal"
+)
+
+func AppTestBasic(t *testing.T, mctx application.Context) {
+	ctx, shutdown := test.V23Init(mctx)
+	defer shutdown()
+
+	df, _ := idiscovery.NewFactory(ctx, mock.New())
+	dfactory.InjectFactory(df)
+
+	ads := []discovery.Advertisement{
+		{
+			Id:            &[internal.AdIdLen]uint8{1, 2, 3},
+			InterfaceName: "v.io/v23/a",
+			Addresses:     []string{"/h1:123/x"},
+			Attributes:    &map[string]string{"a1": "v1"},
+		},
+		{
+			InterfaceName: "v.io/v23/b",
+			Addresses:     []string{"/h1:123/y"},
+			Attributes:    &map[string]string{"b1": "v1"},
+		},
+	}
+
+	d1 := internal.NewDiscovery(ctx)
+	defer d1.Close()
+
+	var stops []func()
+	for i, ad := range ads {
+		id, closer, e1, e2 := d1.Advertise(ad, nil)
+		if e1 != nil || e2 != nil {
+			t.Fatalf("ad[%d]: failed to advertise: %v, %v", i, e1, e2)
+		}
+		if id == nil {
+			t.Errorf("ad[%d]: got nil id", i)
+			continue
+		}
+		if ad.Id == nil {
+			ads[i].Id = id
+		} else if *id != *ad.Id {
+			t.Errorf("ad[%d]: got ad id %v, but wanted %v", i, *id, *ad.Id)
+		}
+
+		stop := func() {
+			p := discovery.NewCloserProxy(*closer, bindings.GetAsyncWaiter())
+			p.Close()
+			p.Close_Proxy()
+		}
+		stops = append(stops, stop)
+	}
+
+	// Make sure none of advertisements are discoverable by the same discovery instance.
+	if err := scanAndMatch(d1, ""); err != nil {
+		t.Error(err)
+	}
+
+	// Create a new discovery instance. All advertisements should be discovered with that.
+	d2 := internal.NewDiscovery(ctx)
+	defer d2.Close()
+
+	if err := scanAndMatch(d2, `v.InterfaceName="v.io/v23/a"`, ads[0]); err != nil {
+		t.Error(err)
+	}
+
+	if err := scanAndMatch(d2, `v.InterfaceName="v.io/v23/b"`, ads[1]); err != nil {
+		t.Error(err)
+	}
+	if err := scanAndMatch(d2, ``, ads...); err != nil {
+		t.Error(err)
+	}
+
+	// Open a new scan channel and consume expected advertisements first.
+	scanCh, stop, err := scan(d2, `v.InterfaceName="v.io/v23/a"`)
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer stop()
+	update := <-scanCh
+	if !matchFound([]*discovery.Update_Proxy{update}, ads[0]) {
+		t.Errorf("unexpected scan: %v", update)
+	}
+
+	// Make sure scan returns the lost advertisement when advertising is stopped.
+	stops[0]()
+
+	update = <-scanCh
+	if !matchLost([]*discovery.Update_Proxy{update}, ads[0]) {
+		t.Errorf("unexpected scan: %v", update)
+	}
+
+	// Also it shouldn't affect the other.
+	if err := scanAndMatch(d2, `v.InterfaceName="v.io/v23/b"`, ads[1]); err != nil {
+		t.Error(err)
+	}
+
+	// Stop advertising the remaining one; Shouldn't discover any advertisements.
+	stops[1]()
+	if err := scanAndMatch(d2, ""); err != nil {
+		t.Error(err)
+	}
+}
+
+type mockScanHandler struct {
+	ch chan *discovery.Update_Proxy
+}
+
+func (h *mockScanHandler) OnUpdate(ptr discovery.Update_Pointer) error {
+	h.ch <- discovery.NewUpdateProxy(ptr, bindings.GetAsyncWaiter())
+	return nil
+}
+
+func scan(d discovery.Discovery, query string) (<-chan *discovery.Update_Proxy, func(), error) {
+	ch := make(chan *discovery.Update_Proxy)
+	handler := &mockScanHandler{ch}
+	req, ptr := discovery.CreateMessagePipeForScanHandler()
+	stub := discovery.NewScanHandlerStub(req, handler, bindings.GetAsyncWaiter())
+
+	closer, e1, e2 := d.Scan(query, ptr)
+	if e1 != nil {
+		close(ch)
+		return nil, nil, errors.New(e1.Msg)
+	}
+	if e2 != nil {
+		close(ch)
+		return nil, nil, e2
+	}
+
+	go func() {
+		for {
+			if err := stub.ServeRequest(); err != nil {
+				connErr, ok := err.(*bindings.ConnectionError)
+				if !ok || !connErr.Closed() {
+					log.Println(err)
+				}
+				break
+			}
+		}
+	}()
+
+	stop := func() {
+		p := discovery.NewCloserProxy(*closer, bindings.GetAsyncWaiter())
+		p.Close()
+		p.Close_Proxy()
+		close(ch)
+	}
+	return ch, stop, nil
+}
+
+func scanAndMatch(d discovery.Discovery, query string, wants ...discovery.Advertisement) error {
+	const timeout = 3 * time.Second
+
+	var updates []*discovery.Update_Proxy
+	for now := time.Now(); time.Since(now) < timeout; {
+		var err error
+		updates, err = doScan(d, query, len(wants))
+		if err != nil {
+			return err
+		}
+		if matchFound(updates, wants...) {
+			return nil
+		}
+	}
+	return fmt.Errorf("Match failed; got %v, but wanted %v", updates, wants)
+}
+
+func doScan(d discovery.Discovery, query string, expectedUpdates int) ([]*discovery.Update_Proxy, error) {
+	scanCh, stop, err := scan(d, query)
+	if err != nil {
+		return nil, err
+	}
+	defer stop()
+
+	updates := make([]*discovery.Update_Proxy, 0, expectedUpdates)
+	for {
+		timeout := 5 * time.Millisecond
+		if len(updates) < expectedUpdates {
+			// Increase the timeout if we do not receive enough updates
+			// to avoid flakiness in unit tests.
+			timeout = 1 * time.Second
+		}
+
+		select {
+		case update := <-scanCh:
+			updates = append(updates, update)
+		case <-time.After(timeout):
+			return updates, nil
+		}
+	}
+}
+
+func matchFound(updates []*discovery.Update_Proxy, wants ...discovery.Advertisement) bool {
+	return match(updates, false, wants...)
+}
+
+func matchLost(updates []*discovery.Update_Proxy, wants ...discovery.Advertisement) bool {
+	return match(updates, true, wants...)
+}
+
+func match(updates []*discovery.Update_Proxy, lost bool, wants ...discovery.Advertisement) bool {
+	updateMap := make(map[[internal.AdIdLen]uint8]discovery.Update)
+	for _, update := range updates {
+		defer update.Close_Proxy()
+		id, _ := update.GetId()
+		updateMap[id] = update
+	}
+
+	for _, want := range wants {
+		update := updateMap[*want.Id]
+		if update == nil {
+			return false
+		}
+		if !updateEqual(update, want) {
+			return false
+		}
+		delete(updateMap, *want.Id)
+	}
+	return len(updateMap) == 0
+}
+
+func updateEqual(update discovery.Update, ad discovery.Advertisement) bool {
+	if got, _ := update.GetId(); got != *ad.Id {
+		return false
+	}
+	if got, _ := update.GetInterfaceName(); got != ad.InterfaceName {
+		return false
+	}
+	if got, _ := update.GetAddresses(); !reflect.DeepEqual(got, ad.Addresses) {
+		return false
+	}
+	if ad.Attributes != nil {
+		for k, v := range *ad.Attributes {
+			if got, _ := update.GetAttribute(k); got != v {
+				return false
+			}
+		}
+	}
+	if ad.Attachments != nil {
+		for k, v := range *ad.Attachments {
+			h, err := update.GetAttachment(k)
+			if err != nil {
+				return false
+			}
+			defer h.Close()
+			r, got := h.ReadData(system.MOJO_READ_DATA_FLAG_NONE)
+			if r != system.MOJO_RESULT_OK {
+				return false
+			}
+			if !bytes.Equal(got, v) {
+				return false
+			}
+		}
+	}
+	if got, _ := update.GetAdvertisement(); !reflect.DeepEqual(got, ad) {
+		return false
+	}
+	return true
+}
diff --git a/go/src/vanadium/discovery/internal/apptest/main/main.go b/go/src/vanadium/discovery/internal/apptest/main/main.go
new file mode 100644
index 0000000..005c1bc
--- /dev/null
+++ b/go/src/vanadium/discovery/internal/apptest/main/main.go
@@ -0,0 +1,57 @@
+// Copyright 2016 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.
+
+// +build mojo
+
+package main
+
+import (
+	"flag"
+	"fmt"
+	"os"
+
+	"mojo/public/go/application"
+	"mojo/public/go/system"
+
+	"vanadium/discovery/internal/apptest"
+)
+
+//#include "mojo/public/c/system/types.h"
+import "C"
+
+func init() {
+	// Add flag placeholders to suppress warnings on unhandled mojo flags.
+	flag.String("child-connection-id", "", "")
+	flag.String("platform-channel-handle-info", "", "")
+}
+
+type delegate struct{}
+
+func (*delegate) Initialize(mctx application.Context) {
+	os.Args = mctx.Args()
+	if len(os.Args) == 0 {
+		// TODO(jhahn): mojo_run doesn't pass the service url when there is
+		// no flags. See https://github.com/domokit/mojo/issues/586.
+		os.Args = []string{mctx.URL()}
+	}
+	// mojo_test checks the output for the test results since mojo_shell
+	// always exits with 0.
+	if apptest.RunAppTests(mctx) == 0 {
+		fmt.Println("[  PASSED  ]")
+	} else {
+		fmt.Println("[  FAILED  ]")
+	}
+	mctx.Close()
+}
+
+func (*delegate) AcceptConnection(connection *application.Connection) { connection.Close() }
+func (*delegate) Quit()                                               {}
+
+//export MojoMain
+func MojoMain(handle C.MojoHandle) C.MojoResult {
+	application.Run(&delegate{}, system.MojoHandle(handle))
+	return C.MOJO_RESULT_OK
+}
+
+func main() {}
diff --git a/go/src/vanadium/discovery/internal/discovery.go b/go/src/vanadium/discovery/internal/discovery.go
index a068b3b..7ef3cac 100644
--- a/go/src/vanadium/discovery/internal/discovery.go
+++ b/go/src/vanadium/discovery/internal/discovery.go
@@ -16,7 +16,8 @@
 	"v.io/v23/discovery"
 )
 
-// TODO(jhahn): Mojom 'const' is ignored in mojom.go. Remove this once it is fixed.
+// TODO(jhahn): Mojom 'const' is ignored in mojom.go.
+// See https://github.com/domokit/mojo/issues/685.
 const AdIdLen = 16
 
 // closer implements the mojom.Closer.
@@ -46,93 +47,60 @@
 }
 
 func (d *mdiscovery) Advertise(ad mojom.Advertisement, visibility *[]string) (*[AdIdLen]uint8, *mojom.Closer_Pointer, *mojom.Error, error) {
-	// There is no way to mock _Pointer or _Request types. So we put Advertise()
-	// logic into a separate function doAdvertise() for unit testing.
-	closer, err := d.doAdvertise(&ad, visibility)
-	if err != nil {
-		return nil, nil, v2mError(err), nil
-	}
-
-	req, ptr := mojom.CreateMessagePipeForCloser()
-	stub := mojom.NewCloserStub(req, closer, bindings.GetAsyncWaiter())
-	d.serveStub(stub, closer.cancel)
-	return ad.Id, &ptr, nil, nil
-}
-
-func (d *mdiscovery) doAdvertise(ad *mojom.Advertisement, visibility *[]string) (*closer, error) {
-	vAd := m2vAd(ad)
-	vVisibility := m2vVisibility(visibility)
-
 	ctx, cancel := context.WithCancel(d.ctx)
+
+	vAd := m2vAd(&ad)
+	vVisibility := m2vVisibility(visibility)
 	done, err := d.d.Advertise(ctx, &vAd, vVisibility)
 	if err != nil {
 		cancel()
-		return nil, err
+		return nil, nil, v2mError(err), nil
 	}
-	if ad.Id == nil {
-		ad.Id = new([AdIdLen]uint8)
-	}
-	*ad.Id = vAd.Id
+
 	stop := func() {
 		cancel()
 		<-done
 	}
-	return &closer{stop}, nil
-}
+	req, ptr := mojom.CreateMessagePipeForCloser()
+	stub := mojom.NewCloserStub(req, &closer{stop}, bindings.GetAsyncWaiter())
+	d.serveStub(stub, stop)
 
-type scanHandlerProxy interface {
-	passUpdate(update mojom.Update) error
-	Close_Proxy()
-}
-
-type scanHandlerProxyImpl struct {
-	*mojom.ScanHandler_Proxy
-
-	d *mdiscovery
-}
-
-func (p *scanHandlerProxyImpl) passUpdate(update mojom.Update) error {
-	req, ptr := mojom.CreateMessagePipeForUpdate()
-	stub := mojom.NewUpdateStub(req, update, bindings.GetAsyncWaiter())
-	p.d.serveStub(stub, nil)
-	return p.OnUpdate(ptr)
+	var id [AdIdLen]uint8
+	id = vAd.Id
+	return &id, &ptr, nil, nil
 }
 
 func (d *mdiscovery) Scan(query string, handlerPtr mojom.ScanHandler_Pointer) (*mojom.Closer_Pointer, *mojom.Error, error) {
-	// There is no way to mock _Pointer or _Request types. So we put Scan()
-	// logic into a separate function doScan() for unit testing.
-	proxy := mojom.NewScanHandlerProxy(handlerPtr, bindings.GetAsyncWaiter())
-	closer, err := d.doScan(query, &scanHandlerProxyImpl{proxy, d})
-	if err != nil {
-		return nil, v2mError(err), nil
-	}
-
-	req, ptr := mojom.CreateMessagePipeForCloser()
-	stub := mojom.NewCloserStub(req, closer, bindings.GetAsyncWaiter())
-	d.serveStub(stub, closer.cancel)
-	return &ptr, nil, nil
-}
-
-func (d *mdiscovery) doScan(query string, proxy scanHandlerProxy) (*closer, error) {
 	ctx, cancel := context.WithCancel(d.ctx)
+
 	scanCh, err := d.d.Scan(ctx, query)
 	if err != nil {
 		cancel()
-		proxy.Close_Proxy()
-		return nil, err
+		return nil, v2mError(err), nil
 	}
 
+	handler := mojom.NewScanHandlerProxy(handlerPtr, bindings.GetAsyncWaiter())
 	go func() {
-		defer proxy.Close_Proxy()
+		defer handler.Close_Proxy()
 
 		for update := range scanCh {
-			mUpdate := newMojoUpdate(ctx, update)
-			if err := proxy.passUpdate(mUpdate); err != nil {
+			mUpdate := newMojoUpdate(d.ctx, update)
+
+			req, ptr := mojom.CreateMessagePipeForUpdate()
+			stub := mojom.NewUpdateStub(req, mUpdate, bindings.GetAsyncWaiter())
+			if err := handler.OnUpdate(ptr); err != nil {
+				stub.Close()
+				cancel()
 				return
 			}
+			d.serveStub(stub, nil)
 		}
 	}()
-	return &closer{cancel}, nil
+
+	req, ptr := mojom.CreateMessagePipeForCloser()
+	stub := mojom.NewCloserStub(req, &closer{cancel}, bindings.GetAsyncWaiter())
+	d.serveStub(stub, cancel)
+	return &ptr, nil, nil
 }
 
 func (d *mdiscovery) serveStub(stub *bindings.Stub, cleanup func()) {
diff --git a/go/src/vanadium/discovery/internal/discovery_test.go b/go/src/vanadium/discovery/internal/discovery_test.go
deleted file mode 100644
index 93e809b..0000000
--- a/go/src/vanadium/discovery/internal/discovery_test.go
+++ /dev/null
@@ -1,199 +0,0 @@
-// 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.
-
-package internal
-
-import (
-	"fmt"
-	"reflect"
-	"testing"
-	"time"
-
-	mojom "mojom/vanadium/discovery"
-
-	idiscovery "v.io/x/ref/lib/discovery"
-	dfactory "v.io/x/ref/lib/discovery/factory"
-	"v.io/x/ref/lib/discovery/plugins/mock"
-	_ "v.io/x/ref/runtime/factories/generic"
-	"v.io/x/ref/test"
-)
-
-func TestBasic(t *testing.T) {
-	ctx, shutdown := test.V23Init()
-	defer shutdown()
-
-	df, _ := idiscovery.NewFactory(ctx, mock.New())
-	dfactory.InjectFactory(df)
-
-	ads := []mojom.Advertisement{
-		{
-			Id:            &[AdIdLen]uint8{1, 2, 3},
-			InterfaceName: "v.io/v23/a",
-			Addresses:     []string{"/h1:123/x"},
-			Attributes:    &map[string]string{"a1": "v1"},
-		},
-		{
-			InterfaceName: "v.io/v23/b",
-			Addresses:     []string{"/h1:123/y"},
-			Attributes:    &map[string]string{"b1": "v1"},
-		},
-	}
-
-	d1 := NewDiscovery(ctx)
-	defer d1.Close()
-
-	var adClosers []mojom.Closer
-	for i, _ := range ads {
-		closer, err := d1.(*mdiscovery).doAdvertise(&ads[i], nil)
-		if err != nil {
-			t.Fatalf("ad[%d]: failed to advertise: %v", i, err)
-		}
-		if ads[i].Id == nil {
-			t.Errorf("ad[%d]: got nil id", i)
-		}
-		adClosers = append(adClosers, closer)
-	}
-
-	// Make sure none of advertisements are discoverable by the same discovery instance.
-	if err := scanAndMatch(d1, ""); err != nil {
-		t.Error(err)
-	}
-
-	// Create a new discovery instance. All advertisements should be discovered with that.
-	d2 := NewDiscovery(ctx)
-	defer d2.Close()
-
-	if err := scanAndMatch(d2, `v.InterfaceName="v.io/v23/a"`, ads[0]); err != nil {
-		t.Error(err)
-	}
-
-	if err := scanAndMatch(d2, `v.InterfaceName="v.io/v23/b"`, ads[1]); err != nil {
-		t.Error(err)
-	}
-	if err := scanAndMatch(d2, ``, ads...); err != nil {
-		t.Error(err)
-	}
-
-	// Open a new scan channel and consume expected advertisements first.
-	scanCh, scanCloser, err := scan(d2, `v.InterfaceName="v.io/v23/a"`)
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer scanCloser.Close()
-	update := <-scanCh
-	if !matchFound([]mojom.Update{update}, ads[0]) {
-		t.Errorf("unexpected scan: %v", update)
-	}
-
-	// Make sure scan returns the lost advertisement when advertising is stopped.
-	adClosers[0].Close()
-
-	update = <-scanCh
-	if !matchLost([]mojom.Update{update}, ads[0]) {
-		t.Errorf("unexpected scan: %v", update)
-	}
-
-	// Also it shouldn't affect the other.
-	if err := scanAndMatch(d2, `v.InterfaceName="v.io/v23/b"`, ads[1]); err != nil {
-		t.Error(err)
-	}
-
-	// Stop advertising the remaining one; Shouldn't discover any advertisements.
-	adClosers[1].Close()
-	if err := scanAndMatch(d2, ""); err != nil {
-		t.Error(err)
-	}
-}
-
-type mockScanHandler struct {
-	ch chan mojom.Update
-}
-
-func (m *mockScanHandler) Close_Proxy() { close(m.ch) }
-func (m *mockScanHandler) passUpdate(u mojom.Update) error {
-	m.ch <- u
-	return nil
-}
-
-func scan(d mojom.Discovery, query string) (<-chan mojom.Update, mojom.Closer, error) {
-	ch := make(chan mojom.Update)
-	closer, err := d.(*mdiscovery).doScan(query, &mockScanHandler{ch})
-	if err != nil {
-		return nil, nil, err
-	}
-	return ch, closer, nil
-}
-
-func scanAndMatch(d mojom.Discovery, query string, wants ...mojom.Advertisement) error {
-	const timeout = 10 * time.Second
-
-	var updates []mojom.Update
-	for now := time.Now(); time.Since(now) < timeout; {
-		var err error
-		updates, err = doScan(d, query, len(wants))
-		if err != nil {
-			return err
-		}
-		if matchFound(updates, wants...) {
-			return nil
-		}
-	}
-	return fmt.Errorf("Match failed; got %v, but wanted %v", updates, wants)
-}
-
-func doScan(d mojom.Discovery, query string, expectedUpdates int) ([]mojom.Update, error) {
-	scanCh, closer, err := scan(d, query)
-	if err != nil {
-		return nil, err
-	}
-	defer closer.Close()
-
-	updates := make([]mojom.Update, 0, expectedUpdates)
-	for {
-		timeout := 5 * time.Millisecond
-		if len(updates) < expectedUpdates {
-			// Increase the timeout if we do not receive enough updates
-			// to avoid flakiness in unit tests.
-			timeout = 5 * time.Second
-		}
-
-		select {
-		case update := <-scanCh:
-			updates = append(updates, update)
-		case <-time.After(timeout):
-			return updates, nil
-		}
-	}
-}
-
-func matchFound(updates []mojom.Update, wants ...mojom.Advertisement) bool {
-	return match(updates, false, wants...)
-}
-
-func matchLost(updates []mojom.Update, wants ...mojom.Advertisement) bool {
-	return match(updates, true, wants...)
-}
-
-func match(updates []mojom.Update, lost bool, wants ...mojom.Advertisement) bool {
-	updateMap := make(map[[AdIdLen]uint8]mojom.Update)
-	for _, update := range updates {
-		id, _ := update.GetId()
-		updateMap[id] = update
-	}
-
-	for _, want := range wants {
-		update := updateMap[*want.Id]
-		if update == nil {
-			return false
-		}
-		if got, _ := update.IsLost(); got != lost {
-			return false
-		}
-		if got, _ := update.GetAdvertisement(); !reflect.DeepEqual(got, want) {
-			return false
-		}
-		delete(updateMap, *want.Id)
-	}
-	return len(updateMap) == 0
-}
diff --git a/go/src/vanadium/discovery/internal/update_test.go b/go/src/vanadium/discovery/internal/update_test.go
index 05a2d8f..0f961a8 100644
--- a/go/src/vanadium/discovery/internal/update_test.go
+++ b/go/src/vanadium/discovery/internal/update_test.go
@@ -58,6 +58,9 @@
 			}
 		}
 
+		// Note that we cannot test attachments in this unit test since it is
+		// using mojo data handle. This test is covered by apptest.
+
 		mAd := v2mAd(&ad)
 		if got, _ := mUpdate.GetAdvertisement(); !reflect.DeepEqual(got, mAd) {
 			t.Errorf("Advertisement: got %v, but want %v", got, mAd)
diff --git a/mojoapptests b/mojoapptests
new file mode 100644
index 0000000..fe6124e
--- /dev/null
+++ b/mojoapptests
@@ -0,0 +1,10 @@
+# This file contains a list of Mojo apptests.
+# For description of the file format, see 'mojo_test'.
+
+tests = [
+  {
+    "test": "https://mojo.v.io/discovery_apptests.mojo",
+    "test-args": [],
+    "shell-args": [],
+  },
+]