Repository: puppetlabs/wash
Pull request #150 (2019-03-20 04:52:43)

Add plugin.CachedOp
This commit adds plugin.CachedOp. Plugin authors can use this method
when they need more fine-grained caching than is provided by the
existing CachedList, CachedOpen, and CachedMetadata methods.

This commit also does some small cleanup of the code, specifically:

* plugin.NewEntry now validates that the provided name is non-empty and
that it does not contain a "/". If this validation fails, then Wash will
panic.

* All of the plugin.Cached* methods will panic if entry.ID() is empty
(i.e. if the entry's ID was not set).

* As a consequence of ^, plugin.Registry's ID is now set to "/" instead
of the empty string. This makes more sense since plugin.Registry
represents Wash's root.

Resolves https://github.com/puppetlabs/wash/issues/123

Signed-off-by: Enis Inan <enis.inan@puppet.com>


Patch:
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
index 2dfbbd0..8267fdc 100644
--- a/.github/PULL_REQUEST_TEMPLATE.md
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -1,1 +1,1 @@
-Contributions to this project require sign-off consistent with the [Developers Certificate of Origin](https://developercertificate.org). This can be as simple as using `git commit -s` on each commit.
\ No newline at end of file
+Contributions to this project require sign-off consistent with the [Developers Certificate of Origin](https://developercertificate.org)
\ No newline at end of file
diff --git a/.travis.yml b/.travis.yml
index 6027443..7942fbe 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -19,10 +19,9 @@ jobs:
   - stage: deploy
     name: Deploy to GitHub Releases
     if: branch = master AND type = push
-    env: LINKFLAGS="-w -s -X github.com/puppetlabs/wash/cmd.version=${TRAVIS_TAG}" CGO_ENABLED=0
     script:
-    - GOOS=darwin GOARCH=amd64 go build -ldflags="$LINKFLAGS" -o ${PROJECT_NAME}-${TRAVIS_TAG}-x86_64-apple-darwin
-    - GOOS=linux GOARCH=amd64 go build -ldflags="$LINKFLAGS" -o ${PROJECT_NAME}-${TRAVIS_TAG}-x86_64-unknown-linux
+    - CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -a -installsuffix cgo -ldflags="-w -s" -o ${PROJECT_NAME}-${TRAVIS_TAG}-x86_64-apple-darwin
+    - CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -installsuffix cgo -ldflags="-w -s" -o ${PROJECT_NAME}-${TRAVIS_TAG}-x86_64-unknown-linux
     deploy:
       provider: releases
       api_key:
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index eda6c50..b5e5bd2 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -54,7 +54,7 @@ Tip: base new resources on the `EntryBase` struct to simplify creating them and
 ## Submitting Changes
 Fork the repo, make changes, file a Pull Request.
 
-Contributions to this project require sign-off consistent with the [Developers Certificate of Origin](https://developercertificate.org). This can be as simple as using `git commit -s` on each commit.
+Contributions to this project require sign-off consistent with the [Developers Certificate of Origin](https://developercertificate.org)
 
 ### Guidelines
 
diff --git a/README.md b/README.md
index 9964424..de738fb 100644
--- a/README.md
+++ b/README.md
@@ -101,7 +101,11 @@ When done, `cd` out of `mnt`, then run `umount mnt` or `Ctrl-C` the server proce
 
 ### On macOS
 
-If the `wash` daemon exits with a exit status of 255, that typically means that `wash` couldn't load the FUSE extensions. MacOS only allows for a certain (small) number of virtual devices on the system, and if all available slots are taken up by other programs then we won't be able to run. You can view loaded extensions with `kextstat`. More information in [this github issue for *FUSE for macOS*](https://github.com/osxfuse/osxfuse/issues/358).
+If the `wash` daemon exits with a exit status of 255, that typically means that `wash` couldn't load the FUSE extensions. MacOS only allows for a certain (small) number of virtual devices on the system, and if all available slots are taken up by other programs then we won't be able to run.
+
+The biggest culprit is VirtualBox, which creates many virtual devices and keeps them resident even if the program isn't running. Currently, we recommend uninstalling VirtualBox to make room for `wash`.
+
+More information in [this github issue for osxfuse](https://github.com/osxfuse/osxfuse/issues/358).
 
 ## Roadmap
 
diff --git a/TAXONOMY.md b/TAXONOMY.md
new file mode 100644
index 0000000..32be817
--- /dev/null
+++ b/TAXONOMY.md
@@ -0,0 +1,58 @@
+# Taxonomy of Resources
+
+This is an attempt to construct a taxonomy for cloud resources. We start by defining different protocols we expect resources may conform to, then define some common types of resources that support a combination of protocols.
+
+## Protocols
+- metadata: everything has metadata
+- group traversal (ls, find, tree): this object has Cloud Resources that can be enumerated
+- file traversal (ls, find, tree): this object has Filesystem Resources that can be enumerated
+- content (cat): this object has content we can view
+- stream (tail -f/streaming): this content has a stream of data we can follow
+- editable (edit): this object has content we can modify
+- exec (bolt): we can invoke commands on this object
+
+## Types
+
+### Cloud Resources
+
+#### Compute
+
+Systems that provide compute capacity.
+
+Protocols: metadata, stream (logs), exec. Could arguably have filesystem traversal, but that's probably not feasible.
+
+Examples: AWS EC2, AWS Lambda, K8s Pod, network device (with shell access)
+
+#### Volume
+
+A place to store hierarchical data.
+
+Protocols: metadata, stream (access logs, optional), file traversal
+
+Examples: AWS EBS, K8s Volume
+
+#### Service
+
+An appliance that provides a service.
+
+Protocols: metadata (configuration), stream (logs, optional)
+
+Examples: AWS ELB, K8s Ingress, network device (with no shell access)
+
+#### Group
+
+Organizational tools for grouping resources.
+
+Protocols: metadata, group traversal
+
+Examples: user, K8s namespace, cloud service (such as K8s cluster, aws), region, K8s deployment, AWS group or tag
+
+### Filesystem Resources
+
+#### File
+
+Protocols: attributes, content, editable
+
+#### Directory
+
+Protocols: attributes, file traversal
diff --git a/cmd/root.go b/cmd/root.go
index 97bbfc7..56ca029 100644
--- a/cmd/root.go
+++ b/cmd/root.go
@@ -1,7 +1,7 @@
 package cmd
 
 import (
-	cmdutil "github.com/puppetlabs/wash/cmd/util"
+	"github.com/puppetlabs/wash/cmd/util"
 	"github.com/spf13/cobra"
 )
 
@@ -45,7 +45,6 @@ func rootCommand() *cobra.Command {
 		SilenceErrors: true,
 	}
 
-	rootCmd.AddCommand(versionCommand())
 	rootCmd.AddCommand(serverCommand())
 	rootCmd.AddCommand(metaCommand())
 	rootCmd.AddCommand(lsCommand())
diff --git a/cmd/version.go b/cmd/version.go
deleted file mode 100644
index 3aa0543..0000000
--- a/cmd/version.go
+++ /dev/null
@@ -1,27 +0,0 @@
-package cmd
-
-import (
-	"fmt"
-
-	"github.com/spf13/cobra"
-)
-
-// Version is set with `go build -ldflags="-X github.com/puppetlabs/wash/cmd.version=${VERSION}"`
-// as part of tagged builds. A local build might use `cmd.version=$(git describe --always)` instead.
-var version = "unknown"
-
-func versionCommand() *cobra.Command {
-	versionCmd := &cobra.Command{
-		Use:   "version",
-		Short: "Print wash version",
-	}
-
-	versionCmd.RunE = toRunE(versionMain)
-
-	return versionCmd
-}
-
-func versionMain(cmd *cobra.Command, args []string) exitCode {
-	fmt.Println(version)
-	return exitCode{0}
-}
diff --git a/plugin/cache.go b/plugin/cache.go
index 7b502da..c345388 100644
--- a/plugin/cache.go
+++ b/plugin/cache.go
@@ -2,8 +2,10 @@ package plugin
 
 import (
 	"context"
+	"fmt"
 	"regexp"
 	"strings"
+	"time"
 
 	"github.com/puppetlabs/wash/datastore"
 )
@@ -77,34 +79,45 @@ func ClearCacheFor(path string) ([]string, error) {
 	return cache.Delete(rx), nil
 }
 
-func cachedOpHelper(op cacheableOp, entry Entry, generateValue func() (interface{}, error)) (interface{}, error) {
-	if cache == nil {
-		if notRunningTests() {
-			panic("The cache was not initialized. You can initialize the cache by invoking plugin.InitCache()")
-		} else {
-			panic("The test cache was not set. You can set it by invoking plugin.SetTestCache(<cache>)")
+type opFunc func() (interface{}, error)
+
+// CachedOp caches the given op's result for the duration specified by the
+// ttl. You should use it when you need more fine-grained caching than what
+// the existing CachedList, CachedOpen, and CachedMetadata methods provide.
+// For example, CachedOp could be useful to cache an API request whose response
+// lets you implement Attr() and Metadata() for the given entry.
+func CachedOp(opName string, entry Entry, ttl time.Duration, op opFunc) (interface{}, error) {
+	for _, actionOpName := range actionOpCodeToNameMap {
+		if opName == actionOpName {
+			panic(fmt.Sprintf("The opName %v conflicts with Cached%v", opName, actionOpName))
 		}
 	}
 
-	ttl := entry.getTTLOf(op)
 	if ttl < 0 {
-		return generateValue()
+		panic("plugin.CachedOp: received a negative TTL")
 	}
 
-	opName := cacheableOpToNameMap[op]
-	return cache.GetOrUpdate(opName+"::"+entry.ID(), ttl, false, generateValue)
+	return cachedOp(opName, entry, ttl, op)
 }
 
-// CachedList caches a Group object's List method
+// CachedList caches a Group object's List method. It also sets the
+// children's IDs to <parent_id> + "/" + <child_name>.
 func CachedList(ctx context.Context, g Group) ([]Entry, error) {
-	cachedEntries, err := cachedOpHelper(List, g, func() (interface{}, error) {
+	cachedEntries, err := cachedActionOp(List, g, func() (interface{}, error) {
 		entries, err := g.List(ctx)
 		if err != nil {
 			return nil, err
 		}
 
 		for _, entry := range entries {
-			entry.setID(g.ID() + "/" + strings.Trim(entry.Name(), "/"))
+			var id string
+			if g.ID() == "/" {
+				id = g.ID() + entry.Name()
+			} else {
+				id = g.ID() + "/" + entry.Name()
+			}
+
+			entry.setID(id)
 		}
 
 		return entries, nil
@@ -122,7 +135,7 @@ func CachedList(ctx context.Context, g Group) ([]Entry, error) {
 // such as ReadAt or wrap it in a SectionReader. Using Read operations on the cached
 // reader will change it and make subsequent uses of the cached reader invalid.
 func CachedOpen(ctx context.Context, r Readable) (SizedReader, error) {
-	cachedContent, err := cachedOpHelper(Open, r, func() (interface{}, error) {
+	cachedContent, err := cachedActionOp(Open, r, func() (interface{}, error) {
 		return r.Open(ctx)
 	})
 
@@ -135,7 +148,7 @@ func CachedOpen(ctx context.Context, r Readable) (SizedReader, error) {
 
 // CachedMetadata caches a Resource object's Metadata method
 func CachedMetadata(ctx context.Context, r Resource) (MetadataMap, error) {
-	cachedMetadata, err := cachedOpHelper(Metadata, r, func() (interface{}, error) {
+	cachedMetadata, err := cachedActionOp(Metadata, r, func() (interface{}, error) {
 		return r.Metadata(ctx)
 	})
 
@@ -145,3 +158,34 @@ func CachedMetadata(ctx context.Context, r Resource) (MetadataMap, error) {
 
 	return cachedMetadata.(MetadataMap), nil
 }
+
+// Common helper for CachedList, CachedOpen and CachedMetadata
+func cachedActionOp(opCode actionOpCode, entry Entry, op opFunc) (interface{}, error) {
+	opName := actionOpCodeToNameMap[opCode]
+	ttl := entry.getTTLOf(opCode)
+
+	return cachedOp(opName, entry, ttl, op)
+}
+
+// Common helper for CachedOp and cachedActionOp.
+func cachedOp(opName string, entry Entry, ttl time.Duration, op opFunc) (interface{}, error) {
+	if cache == nil {
+		if notRunningTests() {
+			panic("The cache was not initialized. You can initialize the cache by invoking plugin.InitCache()")
+		} else {
+			panic("The test cache was not set. You can set it by invoking plugin.SetTestCache(<cache>)")
+		}
+	}
+
+	if entry.ID() == "" {
+		// The Registry's ID is set to "/", while all other entries' IDs are
+		// set in CachedList. Thus, we should never hit this code-path.
+		panic("entry.ID() returned an empty ID")
+	}
+
+	if ttl < 0 {
+		return op()
+	}
+
+	return cache.GetOrUpdate(opName+"::"+entry.ID(), ttl, false, op)
+}
diff --git a/plugin/entryBase.go b/plugin/entryBase.go
index abda9b3..2028b80 100644
--- a/plugin/entryBase.go
+++ b/plugin/entryBase.go
@@ -2,21 +2,22 @@ package plugin
 
 import (
 	"flag"
+	"strings"
 	"time"
 )
 
-type cacheableOp int8
+type actionOpCode int8
 
 const (
 	// List represents Group#List
-	List cacheableOp = iota
+	List actionOpCode = iota
 	// Open represents Readable#Open
 	Open
 	// Metadata represents Resource#Metadata
 	Metadata
 )
 
-var cacheableOpToNameMap = [3]string{"List", "Open", "Metadata"}
+var actionOpCodeToNameMap = [3]string{"List", "Open", "Metadata"}
 
 // EntryBase implements Entry, making it easy to create new entries.
 // You should use plugin.NewEntry to create new EntryBase objects.
@@ -27,16 +28,30 @@ type EntryBase struct {
 	ttl [3]time.Duration
 }
 
-// NewEntry creates a new entry
-func NewEntry(name string) EntryBase {
+// newEntryBase is needed by NewEntry, NewRegistry,
+// and some of the cache tests
+func newEntryBase(name string) EntryBase {
 	e := EntryBase{name: name}
+
 	for op := range e.ttl {
-		e.SetTTLOf(cacheableOp(op), 15*time.Second)
+		e.SetTTLOf(actionOpCode(op), 15*time.Second)
 	}
 
 	return e
 }
 
+// NewEntry creates a new entry
+func NewEntry(name string) EntryBase {
+	if name == "" {
+		panic("plugin.NewEntry: received an empty name")
+	}
+	if strings.Contains(name, "/") {
+		panic("plugin.NewEntry: received a name containing a /")
+	}
+
+	return newEntryBase(name)
+}
+
 // Name returns the entry's name.
 func (e *EntryBase) Name() string {
 	return e.name
@@ -48,23 +63,23 @@ func (e *EntryBase) ID() string {
 }
 
 // SetTTLOf sets the specified op's TTL
-func (e *EntryBase) SetTTLOf(op cacheableOp, ttl time.Duration) {
+func (e *EntryBase) SetTTLOf(op actionOpCode, ttl time.Duration) {
 	e.ttl[op] = ttl
 }
 
 // TurnOffCachingFor turns off caching for the specified op
-func (e *EntryBase) TurnOffCachingFor(op cacheableOp) {
+func (e *EntryBase) TurnOffCachingFor(op actionOpCode) {
 	e.SetTTLOf(op, -1)
 }
 
 // TurnOffCaching turns off caching for all ops
 func (e *EntryBase) TurnOffCaching() {
 	for op := range e.ttl {
-		e.TurnOffCachingFor(cacheableOp(op))
+		e.TurnOffCachingFor(actionOpCode(op))
 	}
 }
 
-func (e *EntryBase) getTTLOf(op cacheableOp) time.Duration {
+func (e *EntryBase) getTTLOf(op actionOpCode) time.Duration {
 	return e.ttl[op]
 }
 
diff --git a/plugin/registry.go b/plugin/registry.go
index 51f0dd5..3af04bf 100644
--- a/plugin/registry.go
+++ b/plugin/registry.go
@@ -14,15 +14,10 @@ type Registry struct {
 // NewRegistry creates a new plugin registry object
 func NewRegistry() *Registry {
 	r := &Registry{
-		EntryBase: NewEntry("/"),
+		EntryBase: newEntryBase("/"),
 		plugins:   make(map[string]Root),
 	}
-
-	// Set the registry's ID to the empty string. This way,
-	// CachedList sets the cache IDs of the Plugin roots to
-	// "/<root_name>" (e.g. "/docker", "/kubernetes", "/aws"),
-	// and all other IDs are correctly set to <parent_id> + "/" + <name>.
-	r.setID("")
+	r.setID("/")
 	r.TurnOffCaching()
 
 	return r
diff --git a/plugin/types.go b/plugin/types.go
index e2884df..3ae1b36 100644
--- a/plugin/types.go
+++ b/plugin/types.go
@@ -15,10 +15,10 @@ import (
 type Entry interface {
 	Name() string
 	ID() string
-	SetTTLOf(op cacheableOp, ttl time.Duration)
-	TurnOffCachingFor(op cacheableOp)
+	SetTTLOf(op actionOpCode, ttl time.Duration)
+	TurnOffCachingFor(op actionOpCode)
 	TurnOffCaching()
-	getTTLOf(op cacheableOp) time.Duration
+	getTTLOf(op actionOpCode) time.Duration
 	setID(id string)
 }
 


Test patch:
diff --git a/plugin/cache_test.go b/plugin/cache_test.go
index fbb3efa..145c9fc 100644
--- a/plugin/cache_test.go
+++ b/plugin/cache_test.go
@@ -35,6 +35,19 @@ type CacheTestSuite struct {
 	cache *cacheTestsMockCache
 }
 
+type generateValueMatcherFunc = func(func() (interface{}, error)) bool
+
+func (suite *CacheTestSuite) makeGenerateValueMatcher(expectedValue interface{}) generateValueMatcherFunc {
+	return func(generateValue func() (interface{}, error)) bool {
+		res, err := generateValue()
+		if suite.NoError(err) {
+			return suite.Equal(expectedValue, res)
+		}
+
+		return false
+	}
+}
+
 func (suite *CacheTestSuite) SetupTest() {
 	suite.cache = &cacheTestsMockCache{}
 	SetTestCache(suite.cache)
@@ -100,7 +113,7 @@ type cacheTestsMockEntry struct {
 
 func newCacheTestsMockEntry(name string) *cacheTestsMockEntry {
 	return &cacheTestsMockEntry{
-		EntryBase: NewEntry(name),
+		EntryBase: newEntryBase(name),
 	}
 }
 
@@ -119,51 +132,83 @@ func (e *cacheTestsMockEntry) Metadata(ctx context.Context) (MetadataMap, error)
 	return args.Get(0).(MetadataMap), args.Error(1)
 }
 
-type cachedOpFunc func(ctx context.Context, e Entry) (interface{}, error)
+type cachedActionOpFunc func(ctx context.Context, e Entry) (interface{}, error)
+
+func (suite *CacheTestSuite) TestCachedOp() {
+	makePanicFunc := func(opName string, ttl time.Duration) func() {
+		return func() {
+			entry := newCacheTestsMockEntry("mock")
+			_, _ = CachedOp("List", entry, ttl, func() (interface{}, error) { return nil, nil })
+		}
+	}
 
-func (suite *CacheTestSuite) testCachedOp(op cacheableOp, opName string, mockValue interface{}, cachedOp cachedOpFunc) {
+	// Test that CachedOp panics if an opName == an action op's name
+	suite.Panics(makePanicFunc("List", 15), "The opName List conflicts with CachedList")
+
+	// Test that CachedOp panics if a negative TTL's passed-in
+	suite.Panics(makePanicFunc("Op", -15), "plugin.CachedOp: received a negative TTL")
+
+	// Test that CachedOp panics if the cache == nil
+	UnsetTestCache()
+	suite.Panics(makePanicFunc("Op", 15))
+	SetTestCache(suite.cache)
+
+	// Test that CachedOp panics if entry.ID() == ""
+	suite.Panics(makePanicFunc("Op", 15), "entry.ID() returned an empty ID")
+
+	// Test that CachedOp calls cache#GetOrUpdate with the right parameters
+	entry := newCacheTestsMockEntry("mock")
+	entry.SetTestID("id")
+	opName := "Op"
+	opTTL := 5 * time.Second
+	op := func() (interface{}, error) { return "result", nil }
+	opKey := "Op::id"
+	generateValueMatcher := suite.makeGenerateValueMatcher("result")
+	suite.cache.On("GetOrUpdate", opKey, opTTL, false, mock.MatchedBy(generateValueMatcher)).Return("result", nil).Once()
+	v, err := CachedOp(opName, entry, opTTL, op)
+	if suite.NoError(err) {
+		suite.Equal("result", v)
+	}
+	suite.cache.AssertCalled(suite.T(), "GetOrUpdate", opKey, opTTL, false, mock.MatchedBy(generateValueMatcher))
+}
+
+func (suite *CacheTestSuite) testCachedActionOp(op actionOpCode, opName string, mockValue interface{}, cachedActionOp cachedActionOpFunc) {
 	ctx := context.Background()
 
-	// Test that cachedOp panics if the cache == nil
+	// Test that cachedActionOp panics if the cache == nil
 	panicFunc := func() {
-		entry := newCacheTestsMockEntry("")
-		_, _ = cachedOp(ctx, entry)
+		entry := newCacheTestsMockEntry("mock")
+		_, _ = cachedActionOp(ctx, entry)
 	}
 	UnsetTestCache()
 	suite.Panics(panicFunc)
 	SetTestCache(suite.cache)
 
-	// Test that cachedOp does _not_ call cache#GetOrUpdate for an
+	// Test that cachedActionOp panics if entry.ID() == ""
+	suite.Panics(panicFunc, "entry.ID() returned an empty ID")
+
+	// Test that cachedActionOp does _not_ call cache#GetOrUpdate for an
 	// entry that's turned off caching
-	entry := newCacheTestsMockEntry("")
+	entry := newCacheTestsMockEntry("mock")
 	entry.SetTestID("id")
 	entry.On(opName, ctx).Return(mockValue, nil)
 	entry.TurnOffCachingFor(op)
-	v, err := cachedOp(ctx, entry)
+	v, err := cachedActionOp(ctx, entry)
 	if suite.NoError(err) {
 		suite.Equal(mockValue, v)
 	}
 	suite.cache.AssertNotCalled(suite.T(), "GetOrUpdate")
 
-	// Test that cachedOp does call cache#GetOrUpdate for an
+	// Test that cachedActionOp does call cache#GetOrUpdate for an
 	// entry that's enabled caching, and that it passes-in the
 	// right arguments.
 	opTTL := 5 * time.Second
 	entry.SetTTLOf(op, opTTL)
 	entry.On(opName, ctx).Return(mockValue, nil)
 	opKey := opName + "::" + "id"
-	generateValueMatcher := func(generateValue func() (interface{}, error)) bool {
-		// This matcher ensures that cachedOp is passing-in the right generator function to
-		// GetOrUpdate
-		res, err := generateValue()
-		if suite.NoError(err) {
-			return suite.Equal(mockValue, res)
-		}
-
-		return false
-	}
+	generateValueMatcher := suite.makeGenerateValueMatcher(mockValue)
 	suite.cache.On("GetOrUpdate", opKey, opTTL, false, mock.MatchedBy(generateValueMatcher)).Return(mockValue, nil).Once()
-	v, err = cachedOp(ctx, entry)
+	v, err = cachedActionOp(ctx, entry)
 	if suite.NoError(err) {
 		suite.Equal(mockValue, v)
 	}
@@ -171,42 +216,56 @@ func (suite *CacheTestSuite) testCachedOp(op cacheableOp, opName string, mockVal
 }
 
 func (suite *CacheTestSuite) TestCachedList() {
-	mockChildren := []Entry{newCacheTestsMockEntry("")}
-	suite.testCachedOp(List, "List", mockChildren, func(ctx context.Context, e Entry) (interface{}, error) {
+	mockChildren := []Entry{newCacheTestsMockEntry("mockChild")}
+	suite.testCachedActionOp(List, "List", mockChildren, func(ctx context.Context, e Entry) (interface{}, error) {
 		return CachedList(ctx, e.(Group))
 	})
 
-	// Set-up another mock entry for additional CachedList tests
-	ctx := context.Background()
-	entry := newCacheTestsMockEntry("parent")
-	entry.SetTestID("parentID")
-	entry.TurnOffCaching()
-
 	// Test that CachedList sets the children's cache IDs
 	// to <parent_cache_id>/<child_name>
+	ctx := context.Background()
 	mockChildren = []Entry{
-		newCacheTestsMockEntry("/child1/"),
+		newCacheTestsMockEntry("child1"),
 		newCacheTestsMockEntry("child2"),
 	}
+
+	// When the parent's the root
+	entry := newCacheTestsMockEntry("/")
+	entry.SetTestID("/")
+	entry.TurnOffCaching()
 	entry.On("List", ctx).Return(mockChildren, nil).Once()
 	children, err := CachedList(ctx, entry)
 	if suite.NoError(err) {
-		suite.Equal(mockChildren, children)
-		suite.Equal("parentID/child1", children[0].ID())
-		suite.Equal("parentID/child2", children[1].ID())
+		if suite.Equal(mockChildren, children) {
+			suite.Equal("/child1", children[0].ID())
+			suite.Equal("/child2", children[1].ID())
+		}
+	}
+
+	// When the parent's some other entry
+	entry = newCacheTestsMockEntry("parent")
+	entry.SetTestID("/parent")
+	entry.TurnOffCaching()
+	entry.On("List", ctx).Return(mockChildren, nil).Once()
+	children, err = CachedList(ctx, entry)
+	if suite.NoError(err) {
+		if suite.Equal(mockChildren, children) {
+			suite.Equal("/parent/child1", children[0].ID())
+			suite.Equal("/parent/child2", children[1].ID())
+		}
 	}
 }
 
 func (suite *CacheTestSuite) TestCachedOpen() {
 	mockReader := strings.NewReader("foo")
-	suite.testCachedOp(Open, "Open", mockReader, func(ctx context.Context, e Entry) (interface{}, error) {
+	suite.testCachedActionOp(Open, "Open", mockReader, func(ctx context.Context, e Entry) (interface{}, error) {
 		return CachedOpen(ctx, e.(Readable))
 	})
 }
 
 func (suite *CacheTestSuite) TestCachedMetadata() {
 	mockMetadataMap := MetadataMap{"foo": "bar"}
-	suite.testCachedOp(Metadata, "Metadata", mockMetadataMap, func(ctx context.Context, e Entry) (interface{}, error) {
+	suite.testCachedActionOp(Metadata, "Metadata", mockMetadataMap, func(ctx context.Context, e Entry) (interface{}, error) {
 		return CachedMetadata(ctx, e.(Resource))
 	})
 }
diff --git a/plugin/entryBase_test.go b/plugin/entryBase_test.go
index c6e19d9..d084f4e 100644
--- a/plugin/entryBase_test.go
+++ b/plugin/entryBase_test.go
@@ -12,8 +12,17 @@ type EntryBaseTestSuite struct {
 }
 
 func (suite *EntryBaseTestSuite) TestNewEntry() {
+	suite.Panics(
+		func() { NewEntry("") },
+		"plugin.NewEntry: received an empty name",
+	)
+	suite.Panics(
+		func() { NewEntry("/foo/") },
+		"plugin.NewEntry: received a name containing a /",
+	)
+
 	e := NewEntry("foo")
-	assertOpTTL := func(op cacheableOp, opName string, expectedTTL time.Duration) {
+	assertOpTTL := func(op actionOpCode, opName string, expectedTTL time.Duration) {
 		actualTTL := e.getTTLOf(op)
 		suite.Equal(
 			expectedTTL,
diff --git a/plugin/externalPluginEntry_test.go b/plugin/externalPluginEntry_test.go
index a463b4c..ad67935 100644
--- a/plugin/externalPluginEntry_test.go
+++ b/plugin/externalPluginEntry_test.go
@@ -116,7 +116,7 @@ func (suite *ExternalPluginEntryTestSuite) TestDecodeExternalPluginEntry() {
 	decodedEntry.CacheTTLs = decodedCacheTTLs{List: 1}
 	entryWithCacheConfig, err := decodedEntry.toExternalPluginEntry()
 	if suite.NoError(err) {
-		expectedTTLs := NewEntry("").ttl
+		expectedTTLs := NewEntry("mock").ttl
 		expectedTTLs[List] = decodedEntry.CacheTTLs.List * time.Second
 		suite.Equal(expectedTTLs, entryWithCacheConfig.EntryBase.ttl)
 	}
diff --git a/plugin/helpers_test.go b/plugin/helpers_test.go
index 568ddf9..41f217b 100644
--- a/plugin/helpers_test.go
+++ b/plugin/helpers_test.go
@@ -113,7 +113,7 @@ func newHelpersTestsMockEntry() *helpersTestsMockEntry {
 	e := &helpersTestsMockEntry{
 		EntryBase: NewEntry("mockEntry"),
 	}
-
+	e.SetTestID("id")
 	e.TurnOffCaching()
 
 	return e
@@ -133,7 +133,7 @@ func newHelpersTestsMockGroup() *helpersTestsMockGroup {
 	e := &helpersTestsMockGroup{
 		EntryBase: NewEntry("mockEntry"),
 	}
-
+	e.SetTestID("id")
 	e.TurnOffCaching()
 
 	return e