services/application/applicationd: TidyNow support.

This change adde a TidyNow implementation to applicationd that removes
all but the configured number (5) most recent envelopes.

Change-Id: I44f68ac80eab2850d35e2a68e2fd5eb8f3aabccf
diff --git a/services/application/applicationd/impl_test.go b/services/application/applicationd/impl_test.go
index 68b07f9..b045bbf 100644
--- a/services/application/applicationd/impl_test.go
+++ b/services/application/applicationd/impl_test.go
@@ -5,6 +5,7 @@
 package main_test
 
 import (
+	"fmt"
 	"io/ioutil"
 	"os"
 	"reflect"
@@ -303,3 +304,238 @@
 		t.Fatalf("Incorrect output: expected %v, got %v", envelopeV1, output)
 	}
 }
+
+// TestTidyNow tests that TidyNow operates correctly.
+func TestTidyNow(t *testing.T) {
+	ctx, shutdown := test.V23Init()
+	defer shutdown()
+
+	dir, prefix := "", ""
+	store, err := ioutil.TempDir(dir, prefix)
+	if err != nil {
+		t.Fatalf("TempDir(%q, %q) failed: %v", dir, prefix, err)
+	}
+	defer os.RemoveAll(store)
+	dispatcher, err := appd.NewDispatcher(store)
+	if err != nil {
+		t.Fatalf("NewDispatcher() failed: %v", err)
+	}
+
+	server, err := xrpc.NewDispatchingServer(ctx, "", dispatcher)
+	if err != nil {
+		t.Fatalf("NewServer(%v) failed: %v", dispatcher, err)
+	}
+
+	defer func() {
+		if err := server.Stop(); err != nil {
+			t.Fatalf("Stop() failed: %v", err)
+		}
+	}()
+	endpoint := server.Status().Endpoints[0].String()
+
+	// Create client stubs for talking to the server.
+	stub := repository.ApplicationClient(naming.JoinAddressName(endpoint, "search"))
+	stubs := make([]repository.ApplicationClientStub, 0)
+	for _, vn := range []string{"v0", "v1", "v2", "v3"} {
+		s := repository.ApplicationClient(naming.JoinAddressName(endpoint, fmt.Sprintf("search/%s", vn)))
+		stubs = append(stubs, s)
+	}
+	blessings, sig := newPublisherSignature(t, ctx, []byte("binarycontents"))
+
+	// Create example envelopes.
+	envelopeV1 := application.Envelope{
+		Args: []string{"--help"},
+		Env:  []string{"DEBUG=1"},
+		Binary: application.SignedFile{
+			File:      "/v23/name/of/binary",
+			Signature: sig,
+		},
+		Publisher: blessings,
+	}
+	envelopeV2 := application.Envelope{
+		Args: []string{"--verbose"},
+		Env:  []string{"DEBUG=0"},
+		Binary: application.SignedFile{
+			File:      "/v23/name/of/binary",
+			Signature: sig,
+		},
+		Publisher: blessings,
+	}
+	envelopeV3 := application.Envelope{
+		Args: []string{"--verbose", "--spiffynewflag"},
+		Env:  []string{"DEBUG=0"},
+		Binary: application.SignedFile{
+			File:      "/v23/name/of/binary",
+			Signature: sig,
+		},
+		Publisher: blessings,
+	}
+
+	stuffEnvelopes(t, ctx, stubs, []profEnvTuple{
+		{
+			&envelopeV1,
+			[]string{"base", "media"},
+		},
+	})
+
+	// Verify that we have one
+	testGlob(t, ctx, endpoint, []string{
+		"",
+		"search",
+		"search/v0",
+	})
+
+	// Tidy when already tidy does not alter.
+	if err := stubs[0].TidyNow(ctx); err != nil {
+		t.Errorf("TidyNow failed: %v", err)
+	}
+	testGlob(t, ctx, endpoint, []string{
+		"",
+		"search",
+		"search/v0",
+	})
+
+	stuffEnvelopes(t, ctx, stubs, []profEnvTuple{
+		{
+			&envelopeV1,
+			[]string{"base", "media"},
+		},
+		{
+			&envelopeV2,
+			[]string{"media"},
+		},
+		{
+			&envelopeV3,
+			[]string{"base"},
+		},
+	})
+
+	// Now there are three envelopes which is one more than the
+	// numberOfVersionsToKeep set for the test. However
+	// we need both envelopes v0 and v2 to keep two versions for
+	// profile media and envelopes v0 and v3 to keep two versions
+	// for profile base so we continue to have three versions.
+	if err := stubs[0].TidyNow(ctx); err != nil {
+		t.Errorf("TidyNow failed: %v", err)
+	}
+	testGlob(t, ctx, endpoint, []string{
+		"",
+		"search",
+		"search/v0",
+		"search/v1",
+		"search/v2",
+	})
+
+	// And the newest version for each profile differs because
+	// not every version supports all profiles.
+	output1, err := stub.Match(ctx, []string{"media"})
+	if err != nil {
+		t.Fatalf("Match(%v) failed: %v", "media", err)
+	}
+	if !reflect.DeepEqual(envelopeV2, output1) {
+		t.Fatalf("Incorrect output: expected %v, got %v", envelopeV2, output1)
+	}
+
+	output2, err := stub.Match(ctx, []string{"base"})
+	if err != nil {
+		t.Fatalf("Match(%v) failed: %v", "base", err)
+	}
+	if !reflect.DeepEqual(envelopeV3, output2) {
+		t.Fatalf("Incorrect output: expected %v, got %v", envelopeV3, output2)
+	}
+
+	// Test that we can add an envelope for v3 with profile media and after calling
+	// TidyNow(), there will be all versions still in glob but v0 will only match profile
+	// base and not have an envelope for profile media.
+	if err := stubs[3].Put(ctx, []string{"media"}, envelopeV3); err != nil {
+		t.Fatalf("Put() failed: %v", err)
+	}
+
+	if err := stubs[0].TidyNow(ctx); err != nil {
+		t.Errorf("TidyNow failed: %v", err)
+	}
+	testGlob(t, ctx, endpoint, []string{
+		"",
+		"search",
+		"search/v0",
+		"search/v1",
+		"search/v2",
+		"search/v3",
+	})
+
+	output3, err := stubs[0].Match(ctx, []string{"base"})
+	if err != nil {
+		t.Fatalf("Match(%v) failed: %v", "base", err)
+	}
+	if !reflect.DeepEqual(envelopeV3, output2) {
+		t.Fatalf("Incorrect output: expected %v, got %v", envelopeV3, output3)
+	}
+
+	output4, err := stubs[0].Match(ctx, []string{"base"})
+	if err != nil {
+		t.Fatalf("Match(%v) failed: %v", "base", err)
+	}
+	if !reflect.DeepEqual(envelopeV3, output2) {
+		t.Fatalf("Incorrect output: expected %v, got %v", envelopeV3, output4)
+	}
+
+	_, err = stubs[0].Match(ctx, []string{"media"})
+	if verror.ErrorID(err) != verror.ErrNoExist.ID {
+		t.Fatalf("got error %v,  expected %v", err, verror.ErrNoExist)
+	}
+
+	stuffEnvelopes(t, ctx, stubs, []profEnvTuple{
+		{
+			&envelopeV1,
+			[]string{"base", "media"},
+		},
+		{
+			&envelopeV2,
+			[]string{"base", "media"},
+		},
+		{
+			&envelopeV3,
+			[]string{"base", "media"},
+		},
+		{
+			&envelopeV3,
+			[]string{"base", "media"},
+		},
+	})
+
+	// Now there are four versions for all profiles so tidying
+	// will remove the older versions.
+	if err := stubs[0].TidyNow(ctx); err != nil {
+		t.Errorf("TidyNow failed: %v", err)
+	}
+
+	testGlob(t, ctx, endpoint, []string{
+		"",
+		"search",
+		"search/v2",
+		"search/v3",
+	})
+}
+
+type profEnvTuple struct {
+	e *application.Envelope
+	p []string
+}
+
+func testGlob(t *testing.T, ctx *context.T, endpoint string, expected []string) {
+	matches, _, err := testutil.GlobName(ctx, naming.JoinAddressName(endpoint, ""), "...")
+	if err != nil {
+		t.Errorf("Unexpected Glob error: %v", err)
+	}
+	if !reflect.DeepEqual(matches, expected) {
+		t.Errorf("unexpected Glob results. Got %q, want %q", matches, expected)
+	}
+}
+
+func stuffEnvelopes(t *testing.T, ctx *context.T, stubs []repository.ApplicationClientStub, pets []profEnvTuple) {
+	for i, pet := range pets {
+		if err := stubs[i].Put(ctx, pet.p, *pet.e); err != nil {
+			t.Fatalf("%d: Put(%v) failed: %v", i, pet, err)
+		}
+	}
+}
diff --git a/services/application/applicationd/only_for_test.go b/services/application/applicationd/only_for_test.go
new file mode 100644
index 0000000..0b3724e
--- /dev/null
+++ b/services/application/applicationd/only_for_test.go
@@ -0,0 +1,9 @@
+// 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
+
+func init() {
+	numberOfVersionsToKeep = 2
+}
diff --git a/services/application/applicationd/service.go b/services/application/applicationd/service.go
index 2fc2a5f..e11f26d 100644
--- a/services/application/applicationd/service.go
+++ b/services/application/applicationd/service.go
@@ -5,7 +5,6 @@
 package main
 
 import (
-	"fmt"
 	"sort"
 	"strings"
 
@@ -72,7 +71,7 @@
 	defer i.store.Unlock()
 
 	if version == "" {
-		versions, err := i.allAppVersions(name)
+		versions, err := i.allAppVersionsForProfiles(name, profiles)
 		if err != nil {
 			return empty, err
 		}
@@ -187,15 +186,13 @@
 	return apps, nil
 }
 
-func (i *appRepoService) allAppVersions(appName string) ([]string, error) {
-	profiles, err := i.store.BindObject(naming.Join("/applications", appName)).Children()
-	if err != nil {
-		return nil, err
-	}
+func (i *appRepoService) allAppVersionsForProfiles(appName string, profiles []string) ([]string, error) {
 	uniqueVersions := make(map[string]struct{})
 	for _, profile := range profiles {
 		versions, err := i.store.BindObject(naming.Join("/applications", appName, profile)).Children()
-		if err != nil {
+		if verror.ErrorID(err) == verror.ErrNoExist.ID {
+			continue
+		} else if err != nil {
 			return nil, err
 		}
 		set.String.Union(uniqueVersions, set.String.FromSlice(versions))
@@ -203,6 +200,14 @@
 	return set.String.ToSlice(uniqueVersions), nil
 }
 
+func (i *appRepoService) allAppVersions(appName string) ([]string, error) {
+	profiles, err := i.store.BindObject(naming.Join("/applications", appName)).Children()
+	if err != nil {
+		return nil, err
+	}
+	return i.allAppVersionsForProfiles(appName, profiles)
+}
+
 func (i *appRepoService) GlobChildren__(ctx *context.T, _ rpc.ServerCall) (<-chan string, error) {
 	ctx.VI(0).Infof("%v.GlobChildren__()", i.suffix)
 	i.store.Lock()
@@ -334,6 +339,64 @@
 	return nil
 }
 
+func (i *appRepoService) tidyRemoveVersions(call rpc.ServerCall, tname, appName, profile string, versions []string) error {
+	for _, v := range versions {
+		path := naming.Join(tname, "/applications", appName, profile, v)
+		object := i.store.BindObject(path)
+		if err := object.Remove(call); err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+// numberOfVersionsToKeep can be set for tests.
+var numberOfVersionsToKeep = 5
+
 func (i *appRepoService) TidyNow(ctx *context.T, call rpc.ServerCall) error {
-	return fmt.Errorf("method not implemented")
+	ctx.VI(2).Infof("%v.TidyNow()", i.suffix)
+	i.store.Lock()
+	defer i.store.Unlock()
+
+	tname, err := i.store.BindTransactionRoot("").CreateTransaction(call)
+	if err != nil {
+		return err
+	}
+
+	apps, err := i.allApplications()
+	if err != nil {
+		return err
+	}
+
+	for _, app := range apps {
+		profiles, err := i.store.BindObject(naming.Join("/applications", app)).Children()
+		if err != nil {
+			return err
+		}
+
+		for _, profile := range profiles {
+			versions, err := i.store.BindObject(naming.Join("/applications", app, profile)).Children()
+			if err != nil {
+				return err
+			}
+
+			lv := len(versions)
+			if lv <= numberOfVersionsToKeep {
+				continue
+			}
+
+			// Per assumption in Match, version names should ascend.
+			sort.Strings(versions)
+			versionsToRemove := versions[0 : lv-numberOfVersionsToKeep]
+			if err := i.tidyRemoveVersions(call, tname, app, profile, versionsToRemove); err != nil {
+				return err
+			}
+		}
+	}
+
+	if err := i.store.BindTransaction(tname).Commit(call); err != nil {
+		return verror.New(ErrOperationFailed, ctx)
+	}
+	return nil
+
 }