services/binary/tidy: add an external GC command

This CL adds a command that can delete binaryd objects not referenced
by envelopes in a specified applicationd instance.

Change-Id: I9919c6a496d574b75fdf5ca23231af0e6e2cc692
diff --git a/services/binary/tidy/appd/mock.go b/services/binary/tidy/appd/mock.go
new file mode 100644
index 0000000..3043304
--- /dev/null
+++ b/services/binary/tidy/appd/mock.go
@@ -0,0 +1,51 @@
+// 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 appd
+
+import (
+	"testing"
+
+	"v.io/v23/context"
+	"v.io/v23/rpc"
+	"v.io/v23/security"
+	"v.io/v23/services/application"
+
+	"v.io/x/ref/services/binary/tidy/binaryd"
+	"v.io/x/ref/services/internal/servicetest"
+)
+
+type mockAppdInvoker struct {
+	binaryd.MockBinarydInvoker
+}
+
+type MatchStimulus struct {
+	Name     string
+	Suffix   string
+	Profiles []string
+}
+
+type MatchResult struct {
+	Env application.Envelope
+	Err error
+}
+
+func (mdi *mockAppdInvoker) Match(ctx *context.T, _ rpc.ServerCall, profiles []string) (application.Envelope, error) {
+	ir := mdi.Tape.Record(MatchStimulus{"Match", mdi.Suffix, profiles})
+	r := ir.(MatchResult)
+	return r.Env, r.Err
+}
+
+type dispatcher struct {
+	tape *servicetest.Tape
+	t    *testing.T
+}
+
+func NewDispatcher(t *testing.T, tape *servicetest.Tape) rpc.Dispatcher {
+	return &dispatcher{tape: tape, t: t}
+}
+
+func (d *dispatcher) Lookup(p *context.T, suffix string) (interface{}, security.Authorizer, error) {
+	return &mockAppdInvoker{binaryd.NewMockBinarydInvoker(suffix, d.tape, d.t)}, nil, nil
+}
diff --git a/services/binary/tidy/binaryd/mock.go b/services/binary/tidy/binaryd/mock.go
new file mode 100644
index 0000000..5e9daf0
--- /dev/null
+++ b/services/binary/tidy/binaryd/mock.go
@@ -0,0 +1,99 @@
+// 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 binaryd
+
+import (
+	"log"
+	"testing"
+
+	"v.io/v23/context"
+	"v.io/v23/naming"
+	"v.io/v23/rpc"
+	"v.io/v23/security"
+	"v.io/v23/services/binary"
+	"v.io/v23/services/repository"
+
+	"v.io/x/ref/services/internal/servicetest"
+)
+
+type MockBinarydInvoker struct {
+	Suffix string
+	Tape   *servicetest.Tape
+	t      *testing.T
+}
+
+// simpleCore implements the core of all mock methods that take
+// arguments and return error.
+func (mdi *MockBinarydInvoker) SimpleCore(callRecord interface{}, name string) error {
+	ri := mdi.Tape.Record(callRecord)
+	switch r := ri.(type) {
+	case nil:
+		return nil
+	case error:
+		return r
+	}
+	log.Fatalf("%s (mock) response %v is of bad type", name, ri)
+	return nil
+}
+
+type DeleteStimulus struct {
+	Op     string
+	Suffix string
+}
+
+func (mdi *MockBinarydInvoker) Delete(ctx *context.T, _ rpc.ServerCall) error {
+	return mdi.SimpleCore(DeleteStimulus{"Delete", mdi.Suffix}, "Delete")
+}
+
+type StatStimulus struct {
+	Op     string
+	Suffix string
+}
+
+func (mdi *MockBinarydInvoker) Stat(ctx *context.T, _ rpc.ServerCall) ([]binary.PartInfo, repository.MediaInfo, error) {
+	// Only the presence or absence of the error is necessary.
+	if err := mdi.SimpleCore(StatStimulus{"Stat", mdi.Suffix}, "Stat"); err != nil {
+		return nil, repository.MediaInfo{}, err
+	}
+	return nil, repository.MediaInfo{}, nil
+}
+
+type GlobStimulus struct {
+	Pattern string
+}
+
+type GlobResponse struct {
+	Results []string
+	Err     error
+}
+
+func (mdi *MockBinarydInvoker) Glob__(p *context.T, _ rpc.ServerCall, pattern string) (<-chan naming.GlobReply, error) {
+	gs := GlobStimulus{pattern}
+	gr := mdi.Tape.Record(gs).(GlobResponse)
+	ch := make(chan naming.GlobReply, len(gr.Results))
+	defer close(ch)
+	for _, r := range gr.Results {
+		ch <- naming.GlobReplyEntry{naming.MountEntry{Name: r}}
+	}
+	return ch, gr.Err
+}
+
+type dispatcher struct {
+	tape *servicetest.Tape
+	t    *testing.T
+}
+
+func NewDispatcher(t *testing.T, tape *servicetest.Tape) rpc.Dispatcher {
+	return &dispatcher{tape: tape, t: t}
+}
+
+func NewMockBinarydInvoker(suffix string, tape *servicetest.Tape, t *testing.T) MockBinarydInvoker {
+	return MockBinarydInvoker{Suffix: suffix, Tape: tape, t: t}
+}
+
+func (d *dispatcher) Lookup(p *context.T, suffix string) (interface{}, security.Authorizer, error) {
+	v := NewMockBinarydInvoker(suffix, d.tape, d.t)
+	return &v, nil, nil
+}
diff --git a/services/binary/tidy/doc.go b/services/binary/tidy/doc.go
new file mode 100644
index 0000000..209fb4c
--- /dev/null
+++ b/services/binary/tidy/doc.go
@@ -0,0 +1,98 @@
+// 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.
+
+// This file was auto-generated via go generate.
+// DO NOT UPDATE MANUALLY
+
+/*
+Tidy tidies the Vanadium binary repository by removing unused binaries.
+
+Usage:
+   tidy <command>
+
+The tidy commands are:
+   binary      Binary sub-command tidies a specified binaryd
+   help        Display help for commands or topics
+
+The global flags are:
+ -alsologtostderr=true
+   log to standard error as well as files
+ -log_backtrace_at=:0
+   when logging hits line file:N, emit a stack trace
+ -log_dir=
+   if non-empty, write log files to this directory
+ -logtostderr=false
+   log to standard error instead of files
+ -max_stack_buf_size=4292608
+   max size in bytes of the buffer to use for logging stack traces
+ -stderrthreshold=2
+   logs at or above this threshold go to stderr
+ -v=0
+   log level for V logs
+ -v23.credentials=
+   directory to use for storing security credentials
+ -v23.i18n-catalogue=
+   18n catalogue files to load, comma separated
+ -v23.metadata=<just specify -v23.metadata to activate>
+   Displays metadata for the program and exits.
+ -v23.namespace.root=[/(dev.v.io/role/vprod/service/mounttabled)@ns.dev.v.io:8101]
+   local namespace root; can be repeated to provided multiple roots
+ -v23.proxy=
+   object name of proxy service to use to export services across network
+   boundaries
+ -v23.tcp.address=
+   address to listen on
+ -v23.tcp.protocol=wsh
+   protocol to listen with
+ -v23.vtrace.cache-size=1024
+   The number of vtrace traces to store in memory.
+ -v23.vtrace.collect-regexp=
+   Spans and annotations that match this regular expression will trigger trace
+   collection.
+ -v23.vtrace.dump-on-shutdown=true
+   If true, dump all stored traces on runtime shutdown.
+ -v23.vtrace.sample-rate=0
+   Rate (from 0.0 to 1.0) to sample vtrace traces.
+ -vmodule=
+   comma-separated list of pattern=N settings for filename-filtered logging
+ -vpath=
+   comma-separated list of pattern=N settings for file pathname-filtered logging
+
+Tidy binary - Binary sub-command tidies a specified binaryd
+
+Binary sub-command removes all binaries from a specified binaryd that are not
+referenced by an applicationd envelope stored in the specified applicationd.
+
+Usage:
+   tidy binary <applicationd> <binaryd>
+
+<applicationd> is the name or endpoint of the applicationd instance sourcing the
+envelopes. <binaryd> is the name or endpoint of a binaryd instance to clean up.
+
+Tidy help - Display help for commands or topics
+
+Help with no args displays the usage of the parent command.
+
+Help with args displays the usage of the specified sub-command or help topic.
+
+"help ..." recursively displays help for all commands and topics.
+
+Usage:
+   tidy help [flags] [command/topic ...]
+
+[command/topic ...] optionally identifies a specific sub-command or help topic.
+
+The tidy help flags are:
+ -style=compact
+   The formatting style for help output:
+      compact - Good for compact cmdline output.
+      full    - Good for cmdline output, shows all global flags.
+      godoc   - Good for godoc processing.
+   Override the default by setting the CMDLINE_STYLE environment variable.
+ -width=<terminal width>
+   Format output to this target width in runes, or unlimited if width < 0.
+   Defaults to the terminal width if available.  Override the default by setting
+   the CMDLINE_WIDTH environment variable.
+*/
+package main
diff --git a/services/binary/tidy/impl.go b/services/binary/tidy/impl.go
new file mode 100644
index 0000000..12bbe7d
--- /dev/null
+++ b/services/binary/tidy/impl.go
@@ -0,0 +1,200 @@
+// 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.
+
+// The following enables go generate to generate the doc.go file.
+//go:generate go run $V23_ROOT/release/go/src/v.io/x/lib/cmdline/testdata/gendoc.go .
+
+package main
+
+import (
+	"fmt"
+	"sort"
+	"time"
+
+	"v.io/v23"
+	"v.io/v23/context"
+	"v.io/v23/naming"
+	"v.io/x/lib/cmdline"
+	"v.io/x/lib/set"
+	"v.io/x/lib/vlog"
+	"v.io/x/ref/lib/v23cmd"
+	_ "v.io/x/ref/runtime/factories/generic"
+	"v.io/x/ref/services/internal/binarylib"
+	"v.io/x/ref/services/internal/profiles"
+	"v.io/x/ref/services/repository"
+)
+
+var cmdTidy = &cmdline.Command{
+	Runner: v23cmd.RunnerFunc(runTidyUp),
+	Name:   "binary",
+	Short:  "Binary sub-command tidies a specified binaryd",
+	Long: `
+Binary sub-command removes all binaries from a specified binaryd that
+are not referenced by an applicationd envelope stored in the specified
+applicationd.
+`,
+	ArgsName: "<applicationd> <binaryd>",
+	ArgsLong: `
+<applicationd> is the name or endpoint of the applicationd instance
+sourcing the envelopes.
+<binaryd> is the name or endpoint of a binaryd instance to clean up.
+`,
+}
+
+// simpleGlob globs the provided endpoint as the namespace cmd does.
+func mapGlob(ctx *context.T, pattern string, mapFunc func(string)) (error, []error) {
+	ctx, cancel := context.WithTimeout(ctx, time.Minute)
+	defer cancel()
+
+	ns := v23.GetNamespace(ctx)
+	c, err := ns.Glob(ctx, pattern)
+	if err != nil {
+		vlog.Infof("ns.Glob(%q) failed: %v", pattern, err)
+		return err, nil
+	}
+
+	errors := []*naming.GlobError{}
+	for res := range c {
+		switch v := res.(type) {
+		case *naming.GlobReplyEntry:
+			if v.Value.Name != "" {
+				mapFunc(v.Value.Name)
+			}
+		case *naming.GlobReplyError:
+			errors = append(errors, &v.Value)
+		}
+	}
+
+	globErrors := make([]error, 0, len(errors))
+	for _, err := range errors {
+		globErrors = append(globErrors, fmt.Errorf("Glob error: %s: %v\n", err.Name, err.Error))
+	}
+	return nil, globErrors
+}
+
+func logGlobErrors(env *cmdline.Env, errors []error) {
+	for _, err := range errors {
+		vlog.Errorf("Glob error: %v", err)
+	}
+}
+
+// getProfileNames uses glob to extract the list of profile names
+// available from the binary server specified by endpoint.
+func getProfileNames(ctx *context.T, env *cmdline.Env, endpoint string) ([]string, error) {
+	profiles, err := profiles.GetKnownProfiles()
+	if err != nil {
+		return nil, err
+	}
+
+	pnames := make([]string, 0, len(profiles))
+	for _, p := range profiles {
+		pnames = append(pnames, p.Label)
+	}
+	return pnames, nil
+}
+
+func getNames(ctx *context.T, env *cmdline.Env, endpoint string) ([]string, error) {
+	resultSet := make(map[string]struct{})
+	err, errors := mapGlob(ctx, endpoint, func(s string) {
+		resultSet[s] = struct{}{}
+	})
+
+	if err != nil {
+		return nil, err
+	}
+	logGlobErrors(env, errors)
+	s := set.String.ToSlice(resultSet)
+	sort.Strings(s)
+	return s, nil
+}
+
+func runTidyUp(ctx *context.T, env *cmdline.Env, args []string) error {
+	if expected, got := 2, len(args); expected != got {
+		return env.UsageErrorf("match: incorrect number of arguments, expected %d, got %d", expected, got)
+	}
+
+	appEndpoint, binEndpoint := args[0], args[1]
+
+	profileNames, err := getProfileNames(ctx, env, binEndpoint)
+	if err != nil {
+		return err
+	}
+
+	envelopeNames, err := getNames(ctx, env, naming.Join(appEndpoint, "..."))
+	if err != nil {
+		return err
+	}
+
+	// Add every path in use to a set. Deletion scope is limited to
+	// only the binEndpoint.
+	bpaths := make(map[string]struct{})
+	for _, en := range envelopeNames {
+		// convert an envelope name into an envelope.
+		ac := repository.ApplicationClient(en)
+
+		for _, p := range profileNames {
+			e, err := ac.Match(ctx, []string{p})
+			if err != nil {
+				// This error case is very noisy.
+				vlog.VI(2).Infof("applicationd.Match(%s, %s) failed: %v\n", en, p, err)
+				continue
+			}
+
+			root, relative := naming.SplitAddressName(e.Binary.File)
+			if root == binEndpoint || root == "" {
+				bpaths[relative] = struct{}{}
+			}
+			for _, sf := range e.Packages {
+				root, relative := naming.SplitAddressName(sf.File)
+				if root == binEndpoint || root == "" {
+					bpaths[relative] = struct{}{}
+				}
+			}
+		}
+	}
+
+	binaryNames, err := getNames(ctx, env, naming.Join(binEndpoint, "..."))
+	if err != nil {
+		return err
+	}
+
+	deletionCandidates := make([]int, 0, len(binaryNames)-len(envelopeNames))
+	for i, bn := range binaryNames {
+		_, relative := naming.SplitAddressName(bn)
+		if _, ok := bpaths[relative]; ok {
+			// relative is mentioned in an envelope.
+			continue
+		}
+
+		if _, err := binarylib.Stat(ctx, bn); err != nil {
+			// This name is not a binary.
+			continue
+		}
+		deletionCandidates = append(deletionCandidates, i)
+	}
+
+	for _, i := range deletionCandidates {
+		b := binaryNames[i]
+		if err := binarylib.Delete(ctx, b); err != nil {
+			vlog.Errorf("Couldn't delete binary %s: %v", b, err)
+		}
+	}
+
+	return nil
+}
+
+var cmdRoot = &cmdline.Command{
+	Name:  "tidy",
+	Short: "Tidy binary repositories",
+	Long: `
+Tidy tidies the Vanadium binary repository by removing unused
+binaries.
+`,
+	Children: []*cmdline.Command{cmdTidy},
+}
+
+func main() {
+	cmdline.HideGlobalFlagsExcept()
+	cmdline.Main(cmdRoot)
+}
diff --git a/services/binary/tidy/impl_test.go b/services/binary/tidy/impl_test.go
new file mode 100644
index 0000000..44e5d00
--- /dev/null
+++ b/services/binary/tidy/impl_test.go
@@ -0,0 +1,316 @@
+// 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 main
+
+import (
+	"bytes"
+	"fmt"
+	"reflect"
+	"strings"
+	"testing"
+
+	"v.io/v23/services/application"
+	"v.io/x/lib/cmdline"
+	"v.io/x/ref/lib/v23cmd"
+	"v.io/x/ref/lib/xrpc"
+	_ "v.io/x/ref/runtime/factories/generic"
+	"v.io/x/ref/services/binary/tidy/appd"
+	"v.io/x/ref/services/binary/tidy/binaryd"
+	"v.io/x/ref/services/internal/servicetest"
+	"v.io/x/ref/test"
+)
+
+//go:generate v23 test generate
+
+func TestBinaryClient(t *testing.T) {
+	ctx, shutdown := test.V23Init()
+	defer shutdown()
+
+	binarytape := servicetest.NewTape()
+	binserver, err := xrpc.NewDispatchingServer(ctx, "", binaryd.NewDispatcher(t, binarytape))
+	if err != nil {
+		t.Fatalf("binaryd NewDispatchingServer failed: %v", err)
+	}
+
+	apptape := servicetest.NewTape()
+	appserver, err := xrpc.NewDispatchingServer(ctx, "", appd.NewDispatcher(t, apptape))
+	if err != nil {
+		t.Fatalf("applicationd NewDispatchingServer failed: %v", err)
+	}
+
+	// Setup the command-line.
+	var stdout, stderr bytes.Buffer
+	env := &cmdline.Env{Stdout: &stdout, Stderr: &stderr}
+	binaryName := binserver.Status().Endpoints[0].Name()
+	applicationName := appserver.Status().Endpoints[0].Name()
+
+	binarytape.SetResponses(
+		// Glob for all binary names
+		binaryd.GlobResponse{[]string{
+			"binaries",
+			"binaries/applicationd",
+			"binaries/applicationd/darwin-amd64",
+			"binaries/applicationd/darwin-amd64/app-darwin-amd-1",
+			"binaries/applicationd/darwin-amd64/app-darwin-amd-2",
+			"binaries/applicationd/linux-amd64",
+			"binaries/applicationd/linux-amd64/app-linux-amd-1",
+			"binaries/applicationd/linux-amd64/app-linux-amd-2",
+			"binaries/binaryd",
+			"binaries/binaryd/linux-amd64",
+			"binaries/binaryd/linux-amd64/bind-linux-amd-1",
+			"binaries/binaryd/linux-amd64/bind-linux-amd-2",
+			"binaries/binaryd/linux-amd64/bind-linux-amd-3",
+			"binaries/libraries",
+			"binaries/libraries/linux-amd64",
+			"binaries/libraries/linux-amd64/extra-goo-1",
+		},
+			nil,
+		},
+
+		// Stat calls
+		fmt.Errorf("binaries"),
+		fmt.Errorf("binaries/applicationd"),
+		fmt.Errorf("binaries/applicationd/darwin-amd64"),
+		nil, // binaries/applicationd/darwin-amd64/app-darwin-amd-1
+		fmt.Errorf("binaries/applicationd/linux-amd64"),
+		nil, // binaries/applicationd/linux-amd64/app-linux-amd-1
+		fmt.Errorf("binaries/binaryd"),
+		fmt.Errorf("binaries/binaryd/linux-amd64"),
+		nil, // binaries/binaryd/linux-amd64/bind-linux-amd-1
+		nil, // binaries/binaryd/linux-amd64/bind-linux-amd-2
+		fmt.Errorf("binaries/libraries"),
+		fmt.Errorf("binaries/libraries/linux-amd64"),
+
+		// Deletion of five binaries.
+		nil,
+		nil,
+		nil,
+		nil,
+		nil,
+	)
+
+	apptape.SetResponses(
+		// Glob for all versions of all apps
+		binaryd.GlobResponse{[]string{
+			"applications",
+			"applications/applicationd",
+			"applications/applicationd/0",
+			"applications/binaryd",
+			"applications/binaryd/1",
+		},
+			nil,
+		},
+
+		// applications.Match(linux-amd64)
+		appd.MatchResult{
+			application.Envelope{},
+			fmt.Errorf("no applications.Match(linux-amd64)"),
+		},
+		// applications.Match(linux-386)
+		appd.MatchResult{
+			application.Envelope{},
+			fmt.Errorf("no applications.Match(linux-386)"),
+		},
+		// applications.Match(linux-arm)
+		appd.MatchResult{
+			application.Envelope{},
+			fmt.Errorf("no applications.Match(linux-arm)"),
+		},
+		// applications.Match(darwin-amd64)
+		appd.MatchResult{
+			application.Envelope{},
+			fmt.Errorf("no applications.Match(darwin-amd64)"),
+		},
+		// applications/applicationd.Match(linux-amd64)
+		appd.MatchResult{
+			application.Envelope{
+				Binary: application.SignedFile{
+					File: "binaries/applicationd/linux-amd64/app-linux-amd-2",
+				},
+			},
+			nil,
+		},
+		// applications/applicationd.Match(linux-386)
+		appd.MatchResult{
+			application.Envelope{},
+			fmt.Errorf("no applications/applicationd.Match(linux-386)"),
+		},
+		// applications/applicationd.Match(linux-arm)
+		appd.MatchResult{
+			application.Envelope{},
+			fmt.Errorf("no applications/applicationd.Match(linux-arm)"),
+		},
+		// applications/applicationd.Match(darwin-amd64)
+		appd.MatchResult{
+			application.Envelope{
+				Binary: application.SignedFile{
+					File: "binaries/applicationd/darwin-amd64/app-darwin-amd-2",
+				},
+			},
+			nil,
+		},
+		// applications/applicationd/0.Match(linux-amd64)
+		appd.MatchResult{
+			application.Envelope{
+				Binary: application.SignedFile{
+					File: "binaries/applicationd/linux-amd64/app-linux-amd-2",
+				},
+			},
+			nil,
+		},
+		// applications/applicationd/0.Match(linux-386)
+		appd.MatchResult{
+			application.Envelope{},
+			fmt.Errorf("no applications/applicationd/0.Match(linux-386)"),
+		},
+		// applications/applicationd/0.Match(linux-arm)
+		appd.MatchResult{
+			application.Envelope{},
+			fmt.Errorf("no applications/applicationd/0.Match(linux-arm)"),
+		},
+		// applications/applicationd/0.Match(darwin-amd64)
+		appd.MatchResult{
+			application.Envelope{
+				Binary: application.SignedFile{
+					File: "binaries/applicationd/darwin-amd64/app-darwin-amd-2",
+				},
+			},
+			nil,
+		},
+		// applications/binaryd.Match(linux-amd64)
+		appd.MatchResult{
+			application.Envelope{
+				Binary: application.SignedFile{
+					File: "binaries/binaryd/linux-amd64/bind-linux-amd-3",
+				},
+				Packages: application.Packages{
+					"somewhere": {
+						File: "binaries/libraries/linux-amd64/extra-goo-1",
+					},
+				},
+			},
+			nil,
+		},
+		// applications/binaryd.Match(linux-386)
+		appd.MatchResult{
+			application.Envelope{},
+			fmt.Errorf("no applications/binaryd.Match(linux-386)"),
+		},
+		// applications/binaryd.Match(linux-arm)
+		appd.MatchResult{
+			application.Envelope{},
+			fmt.Errorf("no applications/binaryd.Match(linux-arm)"),
+		},
+		// applications/binaryd.Match(darwin-amd64)
+		appd.MatchResult{
+			application.Envelope{
+				Binary: application.SignedFile{
+					// Deliberately doesn't exist to show that this case is correctly handled.
+					File: "binaries/binaryd/darwin-amd64/bind-darwin-amd-2",
+				},
+			},
+			nil,
+		},
+		// applications/binaryd/1.Match(linux-amd64)
+		appd.MatchResult{
+			application.Envelope{
+				Binary: application.SignedFile{
+					File: "binaries/binaryd/linux-amd64/bind-linux-amd-3",
+				},
+				Packages: application.Packages{
+					"somewhere": {
+						File: "binaries/libraries/linux-amd64/extra-goo-1",
+					},
+				},
+			},
+			nil,
+		},
+		// applications/binaryd/1.Match(linux-386)
+		appd.MatchResult{
+			application.Envelope{},
+			fmt.Errorf("no applications/binaryd/1.Match(linux-386)"),
+		},
+		// applications/binaryd/1.Match(linux-arm)
+		appd.MatchResult{
+			application.Envelope{},
+			fmt.Errorf("no applications/binaryd/1.Match(linux-arm)"),
+		},
+		// applications/binaryd/1.Match(darwin-amd64)
+		appd.MatchResult{
+			application.Envelope{
+				Binary: application.SignedFile{
+					// Deliberately doesn't exist to show that this case is correctly handled.
+					File: "binaries/binaryd/darwin-amd64/bind-darwin-amd-2",
+				},
+			},
+			nil,
+		},
+	)
+
+	if err := v23cmd.ParseAndRunForTest(cmdTidy, ctx, env, []string{applicationName, binaryName}); err != nil {
+		t.Fatalf("error: %v", err)
+	}
+
+	// Verify no output.
+	if expected, got := "", strings.TrimSpace(stdout.String()); got != expected {
+		t.Errorf("Unexpected output from bintidy. Got %q, expected %q", got, expected)
+	}
+	if expected, got := "", strings.TrimSpace(stderr.String()); got != expected {
+		t.Errorf("Unexpected error from bintidy. Got %q, expected %q", got, expected)
+	}
+
+	// Verify binaryd tape.
+	if got, expected := binarytape.Play(), []interface{}{
+		binaryd.GlobStimulus{Pattern: "..."},
+
+		binaryd.StatStimulus{Op: "Stat", Suffix: "binaries"},
+		binaryd.StatStimulus{Op: "Stat", Suffix: "binaries/applicationd"},
+		binaryd.StatStimulus{Op: "Stat", Suffix: "binaries/applicationd/darwin-amd64"},
+		binaryd.StatStimulus{Op: "Stat", Suffix: "binaries/applicationd/darwin-amd64/app-darwin-amd-1"},
+		binaryd.StatStimulus{Op: "Stat", Suffix: "binaries/applicationd/linux-amd64"},
+		binaryd.StatStimulus{Op: "Stat", Suffix: "binaries/applicationd/linux-amd64/app-linux-amd-1"},
+		binaryd.StatStimulus{Op: "Stat", Suffix: "binaries/binaryd"},
+		binaryd.StatStimulus{Op: "Stat", Suffix: "binaries/binaryd/linux-amd64"},
+		binaryd.StatStimulus{Op: "Stat", Suffix: "binaries/binaryd/linux-amd64/bind-linux-amd-1"},
+		binaryd.StatStimulus{Op: "Stat", Suffix: "binaries/binaryd/linux-amd64/bind-linux-amd-2"},
+		binaryd.StatStimulus{Op: "Stat", Suffix: "binaries/libraries"},
+		binaryd.StatStimulus{Op: "Stat", Suffix: "binaries/libraries/linux-amd64"},
+
+		binaryd.DeleteStimulus{Op: "Delete", Suffix: "binaries/applicationd/darwin-amd64/app-darwin-amd-1"},
+		binaryd.DeleteStimulus{Op: "Delete", Suffix: "binaries/applicationd/linux-amd64/app-linux-amd-1"},
+		binaryd.DeleteStimulus{Op: "Delete", Suffix: "binaries/binaryd/linux-amd64/bind-linux-amd-1"},
+		binaryd.DeleteStimulus{Op: "Delete", Suffix: "binaries/binaryd/linux-amd64/bind-linux-amd-2"},
+	}; !reflect.DeepEqual(expected, got) {
+		t.Errorf("binarytape invalid call sequence. Got %#v, want %#v", got, expected)
+	}
+
+	// Verify application tape.
+	if got, expected := apptape.Play(), []interface{}{
+		binaryd.GlobStimulus{"..."},
+		appd.MatchStimulus{Name: "Match", Suffix: "applications", Profiles: []string{"linux-amd64"}},
+		appd.MatchStimulus{Name: "Match", Suffix: "applications", Profiles: []string{"linux-386"}},
+		appd.MatchStimulus{Name: "Match", Suffix: "applications", Profiles: []string{"linux-arm"}},
+		appd.MatchStimulus{Name: "Match", Suffix: "applications", Profiles: []string{"darwin-amd64"}},
+		appd.MatchStimulus{Name: "Match", Suffix: "applications/applicationd", Profiles: []string{"linux-amd64"}},
+		appd.MatchStimulus{Name: "Match", Suffix: "applications/applicationd", Profiles: []string{"linux-386"}},
+		appd.MatchStimulus{Name: "Match", Suffix: "applications/applicationd", Profiles: []string{"linux-arm"}},
+		appd.MatchStimulus{Name: "Match", Suffix: "applications/applicationd", Profiles: []string{"darwin-amd64"}},
+		appd.MatchStimulus{Name: "Match", Suffix: "applications/applicationd/0", Profiles: []string{"linux-amd64"}},
+		appd.MatchStimulus{Name: "Match", Suffix: "applications/applicationd/0", Profiles: []string{"linux-386"}},
+		appd.MatchStimulus{Name: "Match", Suffix: "applications/applicationd/0", Profiles: []string{"linux-arm"}},
+		appd.MatchStimulus{Name: "Match", Suffix: "applications/applicationd/0", Profiles: []string{"darwin-amd64"}},
+		appd.MatchStimulus{Name: "Match", Suffix: "applications/binaryd", Profiles: []string{"linux-amd64"}},
+		appd.MatchStimulus{Name: "Match", Suffix: "applications/binaryd", Profiles: []string{"linux-386"}},
+		appd.MatchStimulus{Name: "Match", Suffix: "applications/binaryd", Profiles: []string{"linux-arm"}},
+		appd.MatchStimulus{Name: "Match", Suffix: "applications/binaryd", Profiles: []string{"darwin-amd64"}},
+		appd.MatchStimulus{Name: "Match", Suffix: "applications/binaryd/1", Profiles: []string{"linux-amd64"}},
+		appd.MatchStimulus{Name: "Match", Suffix: "applications/binaryd/1", Profiles: []string{"linux-386"}},
+		appd.MatchStimulus{Name: "Match", Suffix: "applications/binaryd/1", Profiles: []string{"linux-arm"}},
+		appd.MatchStimulus{Name: "Match", Suffix: "applications/binaryd/1", Profiles: []string{"darwin-amd64"}},
+	}; !reflect.DeepEqual(expected, got) {
+		t.Errorf("apptape invalid call sequence. Got %#v, want %#v", got, expected)
+	}
+
+}
diff --git a/services/binary/tidy/v23_internal_test.go b/services/binary/tidy/v23_internal_test.go
new file mode 100644
index 0000000..a80e0ec
--- /dev/null
+++ b/services/binary/tidy/v23_internal_test.go
@@ -0,0 +1,22 @@
+// 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.
+
+// This file was auto-generated via go generate.
+// DO NOT UPDATE MANUALLY
+
+package main
+
+import (
+	"os"
+	"testing"
+
+	"v.io/x/ref/test"
+	"v.io/x/ref/test/modules"
+)
+
+func TestMain(m *testing.M) {
+	test.Init()
+	modules.DispatchAndExitIfChild()
+	os.Exit(m.Run())
+}
diff --git a/services/device/deviced/internal/impl/profile.go b/services/device/deviced/internal/impl/profile.go
index 74f9ad1..9908666 100644
--- a/services/device/deviced/internal/impl/profile.go
+++ b/services/device/deviced/internal/impl/profile.go
@@ -13,6 +13,7 @@
 
 	"v.io/v23/services/build"
 	"v.io/v23/services/device"
+	"v.io/x/ref/services/internal/profiles"
 	"v.io/x/ref/services/profile"
 )
 
@@ -101,7 +102,7 @@
 // TODO(jsimsa): Avoid retrieving the list of known profiles from a
 // remote server if a recent cached copy exists.
 func getProfile(name string) (*profile.Specification, error) {
-	profiles, err := getKnownProfiles()
+	profiles, err := profiles.GetKnownProfiles()
 	if err != nil {
 		return nil, err
 	}
@@ -137,74 +138,6 @@
 	*/
 }
 
-// getKnownProfiles gets a list of description for all publicly known
-// profiles.
-//
-// TODO(jsimsa): Avoid retrieving the list of known profiles from a
-// remote server if a recent cached copy exists.
-func getKnownProfiles() ([]*profile.Specification, error) {
-	return []*profile.Specification{
-		{
-			Label:       "linux-amd64",
-			Description: "",
-			Arch:        build.ArchitectureAmd64,
-			Os:          build.OperatingSystemLinux,
-			Format:      build.FormatElf,
-		},
-		{
-			// Note that linux-386 is used instead of linux-x86 for the
-			// label to facilitate generation of a matching label string
-			// using the runtime.GOARCH value. In VDL, the 386 architecture
-			// is represented using the value X86 because the VDL grammar
-			// does not allow identifiers starting with a number.
-			Label:       "linux-386",
-			Description: "",
-			Arch:        build.ArchitectureX86,
-			Os:          build.OperatingSystemLinux,
-			Format:      build.FormatElf,
-		},
-		{
-			Label:       "linux-arm",
-			Description: "",
-			Arch:        build.ArchitectureArm,
-			Os:          build.OperatingSystemLinux,
-			Format:      build.FormatElf,
-		},
-		{
-			Label:       "darwin-amd64",
-			Description: "",
-			Arch:        build.ArchitectureAmd64,
-			Os:          build.OperatingSystemDarwin,
-			Format:      build.FormatMach,
-		},
-	}, nil
-
-	// TODO(jsimsa): This function assumes the existence of a profile
-	// server from which a list of known profiles can be retrieved. The
-	// profile server is a work in progress. When it exists, the
-	// commented out code below should work.
-
-	/*
-		knownProfiles := make([]profile.Specification, 0)
-				client, err := r.NewClient()
-				if err != nil {
-					return nil,  verror.New(ErrOperationFailed, nil, fmt.Sprintf("NewClient() failed: %v\n", err))
-				}
-				defer client.Close()
-			  server := // TODO
-				method := "List"
-				inputs := make([]interface{}, 0)
-				call, err := client.StartCall(server, method, inputs)
-				if err != nil {
-					return nil, verror.New(ErrOperationFailed, nil, fmt.Sprintf("StartCall(%s, %q, %v) failed: %v\n", server, method, inputs, err))
-				}
-				if err := call.Finish(&knownProfiles); err != nil {
-					return nil, verror.New(ErrOperationFailed, nil, fmt.Sprintf("Finish(&knownProfile) failed: %v\n", err))
-				}
-		return knownProfiles, nil
-	*/
-}
-
 // matchProfiles inputs a profile that describes the host device and a
 // set of publicly known profiles and outputs a device description that
 // identifies the publicly known profiles supported by the host device.
@@ -241,7 +174,7 @@
 	if err != nil {
 		return empty, err
 	}
-	knownProfiles, err := getKnownProfiles()
+	knownProfiles, err := profiles.GetKnownProfiles()
 	if err != nil {
 		return empty, err
 	}
diff --git a/services/internal/binarylib/client.go b/services/internal/binarylib/client.go
index 2219e34..2e0424e 100644
--- a/services/internal/binarylib/client.go
+++ b/services/internal/binarylib/client.go
@@ -105,6 +105,15 @@
 	return false
 }
 
+func Stat(ctx *context.T, name string) (repository.MediaInfo, error) {
+	client := repository.BinaryClient(name)
+	_, mediaInfo, err := client.Stat(ctx)
+	if err != nil {
+		return repository.MediaInfo{}, err
+	}
+	return mediaInfo, nil
+}
+
 func download(ctx *context.T, w io.WriteSeeker, von string) (repository.MediaInfo, error) {
 	client := repository.BinaryClient(von)
 	parts, mediaInfo, err := client.Stat(ctx)
diff --git a/services/internal/profiles/listprofiles.go b/services/internal/profiles/listprofiles.go
new file mode 100644
index 0000000..6542bbe
--- /dev/null
+++ b/services/internal/profiles/listprofiles.go
@@ -0,0 +1,85 @@
+// 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 profiles
+
+import (
+	//	"bytes"
+	//	"errors"
+	//	"os/exec"
+	//	"runtime"
+	//	"strings"
+
+	"v.io/v23/services/build"
+	//	"v.io/v23/services/device"
+	"v.io/x/ref/services/profile"
+)
+
+// GetKnownProfiles gets a list of description for all publicly known
+// profiles.
+//
+// TODO(jsimsa): Avoid retrieving the list of known profiles from a
+// remote server if a recent cached copy exists.
+func GetKnownProfiles() ([]*profile.Specification, error) {
+	return []*profile.Specification{
+		{
+			Label:       "linux-amd64",
+			Description: "",
+			Arch:        build.ArchitectureAmd64,
+			Os:          build.OperatingSystemLinux,
+			Format:      build.FormatElf,
+		},
+		{
+			// Note that linux-386 is used instead of linux-x86 for the
+			// label to facilitate generation of a matching label string
+			// using the runtime.GOARCH value. In VDL, the 386 architecture
+			// is represented using the value X86 because the VDL grammar
+			// does not allow identifiers starting with a number.
+			Label:       "linux-386",
+			Description: "",
+			Arch:        build.ArchitectureX86,
+			Os:          build.OperatingSystemLinux,
+			Format:      build.FormatElf,
+		},
+		{
+			Label:       "linux-arm",
+			Description: "",
+			Arch:        build.ArchitectureArm,
+			Os:          build.OperatingSystemLinux,
+			Format:      build.FormatElf,
+		},
+		{
+			Label:       "darwin-amd64",
+			Description: "",
+			Arch:        build.ArchitectureAmd64,
+			Os:          build.OperatingSystemDarwin,
+			Format:      build.FormatMach,
+		},
+	}, nil
+
+	// TODO(jsimsa): This function assumes the existence of a profile
+	// server from which a list of known profiles can be retrieved. The
+	// profile server is a work in progress. When it exists, the
+	// commented out code below should work.
+
+	/*
+		knownProfiles := make([]profile.Specification, 0)
+				client, err := r.NewClient()
+				if err != nil {
+					return nil,  verror.New(ErrOperationFailed, nil, fmt.Sprintf("NewClient() failed: %v\n", err))
+				}
+				defer client.Close()
+			  server := // TODO
+				method := "List"
+				inputs := make([]interface{}, 0)
+				call, err := client.StartCall(server, method, inputs)
+				if err != nil {
+					return nil, verror.New(ErrOperationFailed, nil, fmt.Sprintf("StartCall(%s, %q, %v) failed: %v\n", server, method, inputs, err))
+				}
+				if err := call.Finish(&knownProfiles); err != nil {
+					return nil, verror.New(ErrOperationFailed, nil, fmt.Sprintf("Finish(&knownProfile) failed: %v\n", err))
+				}
+		return knownProfiles, nil
+	*/
+}
diff --git a/services/internal/servicetest/mock.go b/services/internal/servicetest/mock.go
index 5419382..8699eae 100644
--- a/services/internal/servicetest/mock.go
+++ b/services/internal/servicetest/mock.go
@@ -29,8 +29,9 @@
 
 	if len(t.responses) < 1 {
 		// Returning an error at this point will likely cause the mock
-		// device manager to panic trying to cast the response to what
-		// it expects.  Panic'ing here at least makes the issue more
+		// using the tape to panic when it tries to cast the response
+		// to the desired type.
+		// Panic'ing here at least makes the issue more
 		// apparent.
 		// TODO(caprita): Don't panic.
 		panic(fmt.Errorf("Record(%#v) had no response", call))