github.com/tilt-dev/tilt@v0.36.0/internal/engine/dockerprune/docker_pruner_test.go (about)

     1  package dockerprune
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"fmt"
     7  	"testing"
     8  	"time"
     9  
    10  	"github.com/distribution/reference"
    11  	"github.com/docker/docker/api/types"
    12  	"github.com/docker/docker/api/types/filters"
    13  	typesimage "github.com/docker/docker/api/types/image"
    14  	"github.com/docker/go-units"
    15  	"github.com/stretchr/testify/assert"
    16  	"github.com/stretchr/testify/require"
    17  
    18  	"github.com/tilt-dev/tilt/internal/container"
    19  	"github.com/tilt-dev/tilt/internal/docker"
    20  	"github.com/tilt-dev/tilt/internal/store"
    21  	"github.com/tilt-dev/tilt/internal/testutils"
    22  	"github.com/tilt-dev/tilt/pkg/apis/core/v1alpha1"
    23  	"github.com/tilt-dev/tilt/pkg/model"
    24  )
    25  
    26  var (
    27  	cachesPruned     = []string{"cacheA", "cacheB", "cacheC"}
    28  	containersPruned = []string{"containerA", "containerB", "containerC"}
    29  	numImages        = 3
    30  	maxAge           = 11 * time.Hour
    31  	refSel           = container.MustParseSelector("some-ref")
    32  	keep0            = 0
    33  )
    34  
    35  var buildHistory = []model.BuildRecord{
    36  	model.BuildRecord{StartTime: time.Now().Add(-24 * time.Hour)},
    37  }
    38  
    39  func twoHrsAgo() time.Time {
    40  	return time.Now().Add(-2 * time.Hour)
    41  }
    42  
    43  func TestPruneFilters(t *testing.T) {
    44  	f, imgSelectors := newFixture(t).withPruneOutput(cachesPruned, containersPruned, numImages)
    45  	err := f.dp.prune(f.ctx, maxAge, keep0, imgSelectors)
    46  	require.NoError(t, err)
    47  
    48  	expectedFilters := filters.NewArgs(
    49  		filters.Arg("label", gcEnabledSelector),
    50  		filters.Arg("until", maxAge.String()),
    51  	)
    52  	expectedImageFilters := filters.NewArgs(
    53  		filters.Arg("label", gcEnabledSelector),
    54  	)
    55  
    56  	assert.Equal(t, expectedFilters, f.dCli.BuildCachePruneOpts.Filters, "build cache prune filters")
    57  	assert.Equal(t, expectedFilters, f.dCli.ContainersPruneFilters, "container prune filters")
    58  	if assert.Len(t, f.dCli.ImageListOpts, 1, "expect exactly one call to ImageList") {
    59  		assert.Equal(t, expectedImageFilters, f.dCli.ImageListOpts[0].Filters, "image list filters")
    60  	}
    61  }
    62  
    63  func TestPruneOutput(t *testing.T) {
    64  	f, imgSelectors := newFixture(t).withPruneOutput(cachesPruned, containersPruned, numImages)
    65  	err := f.dp.prune(f.ctx, maxAge, keep0, imgSelectors)
    66  	require.NoError(t, err)
    67  
    68  	logs := f.logs.String()
    69  	assert.Contains(t, logs, "[Docker Prune] removed 3 containers, reclaimed 3MB")
    70  	assert.Contains(t, logs, "- containerC")
    71  	assert.Contains(t, logs, "[Docker Prune] removed 3 images, reclaimed 6MB")
    72  	assert.Contains(t, logs, "- deleted: build-id-2")
    73  	assert.Contains(t, logs, "[Docker Prune] removed 3 caches, reclaimed 3MB")
    74  	assert.Contains(t, logs, "- cacheC")
    75  }
    76  
    77  func TestPruneVersionTooLow(t *testing.T) {
    78  	f, imgSelectors := newFixture(t).withPruneOutput(cachesPruned, containersPruned, numImages)
    79  	f.dCli.ThrowNewVersionError = true
    80  	err := f.dp.prune(f.ctx, maxAge, keep0, imgSelectors)
    81  	require.NoError(t, err) // should log failure but not throw error
    82  
    83  	logs := f.logs.String()
    84  	assert.Contains(t, logs, "skipping Docker prune")
    85  
    86  	// Should NOT have called any of the prune funcs
    87  	assert.Empty(t, f.dCli.BuildCachePruneOpts)
    88  	assert.Empty(t, f.dCli.ContainersPruneFilters)
    89  	assert.Empty(t, f.dCli.ImageListOpts)
    90  	assert.Empty(t, f.dCli.RemovedImageIDs)
    91  }
    92  
    93  func TestPruneSkipCachePruneIfVersionTooLow(t *testing.T) {
    94  	f, imgSelectors := newFixture(t).withPruneOutput(cachesPruned, containersPruned, numImages)
    95  	f.dCli.BuildCachePruneErr = f.dCli.VersionError("1.2.3", "build prune")
    96  	err := f.dp.prune(f.ctx, maxAge, keep0, imgSelectors)
    97  	require.NoError(t, err) // should log failure but not throw error
    98  
    99  	logs := f.logs.String()
   100  	assert.Contains(t, logs, "skipping build cache prune")
   101  
   102  	// Should have called previous prune funcs as normal
   103  	assert.NotEmpty(t, f.dCli.ContainersPruneFilters)
   104  	assert.NotEmpty(t, f.dCli.ImageListOpts)
   105  	assert.NotEmpty(t, f.dCli.RemovedImageIDs)
   106  }
   107  
   108  func TestPruneReturnsCachePruneError(t *testing.T) {
   109  	f, imgSelectors := newFixture(t).withPruneOutput(cachesPruned, containersPruned, numImages)
   110  	f.dCli.BuildCachePruneErr = fmt.Errorf("this is a real error, NOT an API version error")
   111  	err := f.dp.prune(f.ctx, maxAge, keep0, imgSelectors)
   112  	require.NotNil(t, err) // For all errors besides API version error, expect them to return
   113  	assert.Contains(t, err.Error(), "this is a real error")
   114  
   115  	logs := f.logs.String()
   116  	assert.NotContains(t, logs, "skipping build cache prune")
   117  
   118  	// Should have called previous prune funcs as normal
   119  	assert.NotEmpty(t, f.dCli.ContainersPruneFilters)
   120  	assert.NotEmpty(t, f.dCli.ImageListOpts)
   121  	assert.NotEmpty(t, f.dCli.RemovedImageIDs)
   122  }
   123  
   124  func TestDeleteOldImages(t *testing.T) {
   125  	f := newFixture(t)
   126  	maxAge := 3 * time.Hour
   127  	_, _ = f.withImageInspect(0, 25, time.Hour)       // young enough, won't be pruned
   128  	id, ref := f.withImageInspect(1, 50, 4*time.Hour) // older than max age, will be pruned
   129  	_, _ = f.withImageInspect(2, 75, 6*time.Hour)     // older than max age but doesn't match passed ref selectors
   130  	report, err := f.dp.deleteOldImages(f.ctx, maxAge, keep0, []container.RefSelector{container.NameSelector(ref)})
   131  	require.NoError(t, err)
   132  
   133  	assert.Len(t, report.ImagesDeleted, 1, "expected exactly one deleted image")
   134  	assert.Equal(t, 50, int(report.SpaceReclaimed), "expected space reclaimed")
   135  
   136  	expectedDeleted := []string{id}
   137  	assert.Equal(t, expectedDeleted, f.dCli.RemovedImageIDs)
   138  
   139  	expectedFilters := filters.NewArgs(filters.Arg("label", gcEnabledSelector))
   140  	if assert.Len(t, f.dCli.ImageListOpts, 1, "expected exactly one call to ImageList") {
   141  		assert.Equal(t, expectedFilters, f.dCli.ImageListOpts[0].Filters,
   142  			"expected ImageList to called with label=builtby:tilt filter")
   143  	}
   144  }
   145  
   146  func TestKeepRecentImages(t *testing.T) {
   147  	f := newFixture(t)
   148  	maxAge := time.Minute
   149  	_, ref1 := f.withImageInspect(0, 10, time.Hour)
   150  	idOldest, ref2 := f.withImageInspect(0, 100, 4*time.Hour)
   151  	_, ref3 := f.withImageInspect(0, 1000, 3*time.Hour)
   152  	selectors := []container.RefSelector{
   153  		container.NameSelector(ref1),
   154  		container.NameSelector(ref2),
   155  		container.NameSelector(ref3),
   156  	}
   157  
   158  	keep4 := 4
   159  	report, err := f.dp.deleteOldImages(f.ctx, maxAge, keep4, selectors)
   160  	require.NoError(t, err)
   161  	assert.Len(t, report.ImagesDeleted, 0)
   162  
   163  	keep2 := 2
   164  	report, err = f.dp.deleteOldImages(f.ctx, maxAge, keep2, selectors)
   165  	require.NoError(t, err)
   166  	assert.Len(t, report.ImagesDeleted, 1)
   167  
   168  	// deletes the oldest image
   169  	expectedDeleted := []string{idOldest}
   170  	assert.Equal(t, expectedDeleted, f.dCli.RemovedImageIDs)
   171  }
   172  
   173  func TestKeepRecentImagesMultipleTags(t *testing.T) {
   174  	f := newFixture(t)
   175  	maxAge := time.Minute
   176  	_, refA1 := f.withImageInspect(0, 10, time.Hour)
   177  	idA2, refA2 := f.withImageInspect(0, 100, 2*time.Hour)
   178  	idA3, refA3 := f.withImageInspect(0, 1000, 3*time.Hour)
   179  	idB2, refB2 := f.withImageInspect(1, 100, 5*time.Hour)
   180  	_, refB1 := f.withImageInspect(1, 10, 4*time.Hour)
   181  	selectors := []container.RefSelector{
   182  		container.NameSelector(refA1),
   183  		container.NameSelector(refA2),
   184  		container.NameSelector(refA3),
   185  		container.NameSelector(refB1),
   186  		container.NameSelector(refB2),
   187  	}
   188  
   189  	keep4 := 4
   190  	report, err := f.dp.deleteOldImages(f.ctx, maxAge, keep4, selectors)
   191  	require.NoError(t, err)
   192  	assert.Len(t, report.ImagesDeleted, 0)
   193  
   194  	keep1 := 1
   195  	report, err = f.dp.deleteOldImages(f.ctx, maxAge, keep1, selectors)
   196  	require.NoError(t, err)
   197  	assert.Len(t, report.ImagesDeleted, 3)
   198  
   199  	// deletes the oldest images from each tag
   200  	expectedDeleted := []string{idA2, idA3, idB2}
   201  	assert.Equal(t, expectedDeleted, f.dCli.RemovedImageIDs)
   202  }
   203  
   204  func TestDeleteOldImagesDontRemoveImageWithMultipleTags(t *testing.T) {
   205  	f := newFixture(t)
   206  	maxAge := 3 * time.Hour
   207  	id, ref := f.withImageInspect(0, 50, 4*time.Hour)
   208  	inspect := f.dCli.Images[id]
   209  	inspect.RepoTags = append(f.dCli.Images[id].RepoTags, "some-additional-tag")
   210  	f.dCli.Images[id] = inspect
   211  
   212  	report, err := f.dp.deleteOldImages(f.ctx, maxAge, keep0, []container.RefSelector{container.NameSelector(ref)})
   213  	require.NoError(t, err) // error is silent
   214  
   215  	assert.Len(t, report.ImagesDeleted, 0, "expected no deleted images")
   216  	assert.Equal(t, 0, int(report.SpaceReclaimed), "expected space reclaimed")
   217  
   218  	assert.Contains(t, f.logs.String(), "`docker image remove --force` required to remove an image with multiple tags")
   219  }
   220  
   221  func TestDockerPrunerSinceNBuilds(t *testing.T) {
   222  	f := newFixture(t)
   223  	f.withDockerManifestAlreadyBuilt()
   224  	f.withBuildCount(11)
   225  	f.withDockerPruneSettings(true, 0, 5, 0)
   226  	f.dp.lastPruneBuildCount = 5
   227  	f.dp.lastPruneTime = twoHrsAgo()
   228  
   229  	_ = f.dp.OnChange(f.ctx, f.st, store.LegacyChangeSummary())
   230  
   231  	f.assertPrune()
   232  }
   233  
   234  func TestDockerPrunerNotEnoughBuilds(t *testing.T) {
   235  	f := newFixture(t)
   236  	f.withDockerManifestAlreadyBuilt()
   237  	f.withBuildCount(11)
   238  	f.withDockerPruneSettings(true, 0, 10, 0)
   239  	f.dp.lastPruneBuildCount = 5
   240  	f.dp.lastPruneTime = twoHrsAgo()
   241  
   242  	_ = f.dp.OnChange(f.ctx, f.st, store.LegacyChangeSummary())
   243  
   244  	f.assertNoPrune()
   245  }
   246  
   247  func TestDockerPrunerSinceInterval(t *testing.T) {
   248  	f := newFixture(t)
   249  	f.withDockerManifestAlreadyBuilt()
   250  	f.withDockerPruneSettings(true, 0, 0, 30*time.Minute)
   251  	f.dp.lastPruneTime = twoHrsAgo()
   252  
   253  	_ = f.dp.OnChange(f.ctx, f.st, store.LegacyChangeSummary())
   254  
   255  	f.assertPrune()
   256  }
   257  
   258  func TestDockerPrunerSinceDefaultInterval(t *testing.T) {
   259  	f := newFixture(t)
   260  	f.withDockerManifestAlreadyBuilt()
   261  	f.withDockerPruneSettings(true, 0, 0, 0)
   262  	f.dp.lastPruneTime = time.Now().Add(-1 * (model.DockerPruneDefaultInterval + time.Minute))
   263  
   264  	_ = f.dp.OnChange(f.ctx, f.st, store.LegacyChangeSummary())
   265  
   266  	f.assertPrune()
   267  }
   268  
   269  func TestDockerPrunerNotEnoughTimeElapsed(t *testing.T) {
   270  	f := newFixture(t)
   271  	f.withDockerManifestAlreadyBuilt()
   272  	f.withDockerPruneSettings(true, 0, 0, 3*time.Hour)
   273  	f.dp.lastPruneTime = twoHrsAgo()
   274  
   275  	_ = f.dp.OnChange(f.ctx, f.st, store.LegacyChangeSummary())
   276  
   277  	f.assertNoPrune()
   278  }
   279  
   280  func TestDockerPrunerSinceDefaultIntervalNotEnoughTime(t *testing.T) {
   281  	f := newFixture(t)
   282  	f.withDockerManifestAlreadyBuilt()
   283  	f.withDockerPruneSettings(true, 0, 0, 0)
   284  	f.dp.lastPruneTime = time.Now().Add(-1 * model.DockerPruneDefaultInterval).Add(20 * time.Minute)
   285  
   286  	_ = f.dp.OnChange(f.ctx, f.st, store.LegacyChangeSummary())
   287  
   288  	f.assertNoPrune()
   289  }
   290  
   291  func TestDockerPrunerFirstRun(t *testing.T) {
   292  	f := newFixture(t)
   293  	f.withDockerManifestAlreadyBuilt()
   294  	f.withBuildCount(5)
   295  	f.withDockerPruneSettings(true, 0, 10, 0)
   296  
   297  	_ = f.dp.OnChange(f.ctx, f.st, store.LegacyChangeSummary())
   298  
   299  	f.assertPrune()
   300  }
   301  
   302  func TestDockerPrunerFirstRunButNoCompletedBuilds(t *testing.T) {
   303  	f := newFixture(t)
   304  	f.withDockerManifestAlreadyBuilt()
   305  	f.withBuildCount(0)
   306  	f.withDockerPruneSettings(true, 0, 10, 0)
   307  
   308  	_ = f.dp.OnChange(f.ctx, f.st, store.LegacyChangeSummary())
   309  
   310  	f.assertNoPrune()
   311  }
   312  
   313  func TestDockerPrunerNoBuildManifests(t *testing.T) {
   314  	f := newFixture(t)
   315  	f.withK8sOnlyManifest()
   316  	f.withBuildCount(11)
   317  	f.withDockerPruneSettings(true, 0, 5, 0)
   318  
   319  	_ = f.dp.OnChange(f.ctx, f.st, store.LegacyChangeSummary())
   320  
   321  	f.assertNoPrune()
   322  }
   323  
   324  func TestDockerPrunerOnlyCustomBuildManifests(t *testing.T) {
   325  	f := newFixture(t)
   326  	f.withBuildCount(11)
   327  	f.withDockerPruneSettings(true, 0, 5, 0)
   328  
   329  	iTarget := model.MustNewImageTarget(refSel).WithBuildDetails(model.CustomBuild{})
   330  	m := model.Manifest{Name: "custom-build-manifest"}.WithImageTarget(iTarget)
   331  	f.withManifestTarget(store.NewManifestTarget(m), true)
   332  
   333  	_ = f.dp.OnChange(f.ctx, f.st, store.LegacyChangeSummary())
   334  
   335  	f.assertPrune()
   336  }
   337  
   338  func TestDockerPrunerDisabled(t *testing.T) {
   339  	f := newFixture(t)
   340  	f.withDockerManifestAlreadyBuilt()
   341  	f.withDockerPruneSettings(false, 0, 0, 0)
   342  
   343  	_ = f.dp.OnChange(f.ctx, f.st, store.LegacyChangeSummary())
   344  
   345  	f.assertNoPrune()
   346  }
   347  
   348  func TestDockerPrunerCurrentlyBuilding(t *testing.T) {
   349  	f := newFixture(t)
   350  	f.withDockerManifestAlreadyBuilt()
   351  	f.withCurrentlyBuilding("idk something")
   352  	f.withDockerPruneSettings(true, 0, 0, time.Hour)
   353  	f.dp.lastPruneTime = twoHrsAgo()
   354  
   355  	_ = f.dp.OnChange(f.ctx, f.st, store.LegacyChangeSummary())
   356  
   357  	f.assertNoPrune()
   358  }
   359  
   360  func TestDockerPrunerPendingBuild(t *testing.T) {
   361  	f := newFixture(t)
   362  	f.withDockerManifestUnbuilt() // manifest not yet built will be pending, so we should not prune
   363  	f.withDockerPruneSettings(true, 0, 0, time.Hour)
   364  	f.dp.lastPruneTime = twoHrsAgo()
   365  
   366  	_ = f.dp.OnChange(f.ctx, f.st, store.LegacyChangeSummary())
   367  
   368  	f.assertNoPrune()
   369  }
   370  
   371  func TestDockerPrunerMaxAgeFromSettings(t *testing.T) {
   372  	f := newFixture(t)
   373  	f.withDockerManifestAlreadyBuilt()
   374  	f.withBuildCount(5)
   375  	maxAge := time.Hour
   376  	f.withDockerPruneSettings(true, maxAge, 10, 0)
   377  
   378  	_ = f.dp.OnChange(f.ctx, f.st, store.LegacyChangeSummary())
   379  
   380  	f.assertPrune()
   381  	untilVals := f.dCli.ContainersPruneFilters.Get("until")
   382  	require.Len(t, untilVals, 1, "unexpected number of filters for \"until\"")
   383  	assert.Equal(t, untilVals[0], maxAge.String())
   384  }
   385  
   386  type dockerPruneFixture struct {
   387  	t    *testing.T
   388  	ctx  context.Context
   389  	logs *bytes.Buffer
   390  	st   *store.TestingStore
   391  
   392  	dCli *docker.FakeClient
   393  	dp   *DockerPruner
   394  }
   395  
   396  func newFixture(t *testing.T) *dockerPruneFixture {
   397  	logs := new(bytes.Buffer)
   398  	ctx, _, _ := testutils.ForkedCtxAndAnalyticsForTest(logs)
   399  	st := store.NewTestingStore()
   400  
   401  	dCli := docker.NewFakeClient()
   402  	dp := NewDockerPruner(dCli)
   403  
   404  	return &dockerPruneFixture{
   405  		t:    t,
   406  		ctx:  ctx,
   407  		logs: logs,
   408  		st:   st,
   409  		dCli: dCli,
   410  		dp:   dp,
   411  	}
   412  }
   413  
   414  func (dpf *dockerPruneFixture) withPruneOutput(caches, containers []string, numImages int) (*dockerPruneFixture, []container.RefSelector) {
   415  	dpf.dCli.BuildCachesPruned = caches
   416  	dpf.dCli.ContainersPruned = containers
   417  
   418  	selectors := make([]container.RefSelector, numImages)
   419  	for i := 0; i < numImages; i++ {
   420  		_, ref := dpf.withImageInspect(i, units.MB*(i+1), 48*time.Hour) // make each image 2 days old (def older than maxAge)
   421  		selectors[i] = container.NameSelector(ref)
   422  	}
   423  	return dpf, selectors
   424  }
   425  
   426  func (dpf *dockerPruneFixture) withImageInspect(i, size int, timeSinceLastTag time.Duration) (id string, ref reference.Named) {
   427  	tag := fmt.Sprintf("tag-%d", i)
   428  	id = fmt.Sprintf("build-id-%d", dpf.dCli.ImageListCount)
   429  	dpf.dCli.Images[id] = types.ImageInspect{
   430  		ID:       id,
   431  		RepoTags: []string{tag},
   432  		Size:     int64(size),
   433  		Metadata: typesimage.Metadata{
   434  			LastTagTime: time.Now().Add(-1 * timeSinceLastTag),
   435  		},
   436  	}
   437  	dpf.dCli.ImageListCount += 1
   438  	return id, container.MustParseNamed(tag)
   439  }
   440  
   441  func (dpf *dockerPruneFixture) withDockerManifestAlreadyBuilt() {
   442  	dpf.withDockerManifest(true)
   443  }
   444  
   445  func (dpf *dockerPruneFixture) withDockerManifestUnbuilt() {
   446  	dpf.withDockerManifest(false)
   447  }
   448  
   449  func (dpf *dockerPruneFixture) withDockerManifest(alreadyBuilt bool) {
   450  	iTarget := model.MustNewImageTarget(refSel).
   451  		WithBuildDetails(model.DockerBuild{})
   452  
   453  	m := model.Manifest{Name: "some-docker-manifest"}.
   454  		WithImageTarget(iTarget)
   455  
   456  	dpf.withManifestTarget(store.NewManifestTarget(m), alreadyBuilt)
   457  }
   458  
   459  func (dpf *dockerPruneFixture) withK8sOnlyManifest() {
   460  	m := model.Manifest{Name: "i'm-k8s-only"}.WithDeployTarget(model.K8sTarget{})
   461  	dpf.withManifestTarget(store.NewManifestTarget(m), true)
   462  }
   463  
   464  func (dpf *dockerPruneFixture) withManifestTarget(mt *store.ManifestTarget, alreadyBuilt bool) {
   465  	mt.State.DisableState = v1alpha1.DisableStateEnabled
   466  	if alreadyBuilt {
   467  		// spoof build history so we think this manifest has already been built (i.e. isn't pending)
   468  		mt.State.BuildHistory = buildHistory
   469  	}
   470  
   471  	store := dpf.st.LockMutableStateForTesting()
   472  	store.UpsertManifestTarget(mt)
   473  	dpf.st.UnlockMutableState()
   474  }
   475  
   476  func (dpf *dockerPruneFixture) withBuildCount(count int) {
   477  	store := dpf.st.LockMutableStateForTesting()
   478  	store.CompletedBuildCount = count
   479  	dpf.st.UnlockMutableState()
   480  }
   481  
   482  func (dpf *dockerPruneFixture) withCurrentlyBuilding(mn model.ManifestName) {
   483  	store := dpf.st.LockMutableStateForTesting()
   484  	store.CurrentBuildSet[mn] = true
   485  	dpf.st.UnlockMutableState()
   486  }
   487  
   488  func (dpf *dockerPruneFixture) withDockerPruneSettings(enabled bool, maxAge time.Duration, numBuilds int, interval time.Duration) {
   489  	settings := model.DockerPruneSettings{
   490  		Enabled:   enabled,
   491  		MaxAge:    maxAge,
   492  		NumBuilds: numBuilds,
   493  		Interval:  interval,
   494  	}
   495  	store := dpf.st.LockMutableStateForTesting()
   496  	store.DockerPruneSettings = settings
   497  	dpf.st.UnlockMutableState()
   498  }
   499  
   500  func (dpf *dockerPruneFixture) pruneCalled() bool {
   501  	// ContainerPrune was called -- we use this as a proxy for dp.Prune having been called.
   502  	return dpf.dCli.ContainersPruneFilters.Len() > 0
   503  }
   504  
   505  func (dpf *dockerPruneFixture) assertPrune() {
   506  	if !dpf.pruneCalled() {
   507  		dpf.t.Errorf("expected Prune() to be called, but it was not")
   508  		dpf.t.FailNow()
   509  	}
   510  	if time.Since(dpf.dp.lastPruneTime) > time.Second {
   511  		dpf.t.Errorf("Prune() was called, but dp.lastPruneTime was not updated/" +
   512  			"not updated recently")
   513  		dpf.t.FailNow()
   514  	}
   515  }
   516  
   517  func (dpf *dockerPruneFixture) assertNoPrune() {
   518  	if dpf.pruneCalled() {
   519  		dpf.t.Errorf("Prune() was called, when no calls expected")
   520  		dpf.t.FailNow()
   521  	}
   522  }