Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 18 additions & 3 deletions cmd/clean.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package cmd

import (
"encoding/json"
"errors"
"fmt"
"io"
"io/fs"
Expand Down Expand Up @@ -71,18 +72,21 @@ clean never touches your config file, installed extensions, crash reports
RunE: func(cmd *cobra.Command, _ []string) error {
force, _ := cmd.Flags().GetBool("force")
asJSON, _ := cmd.Flags().GetBool("json")
return runClean(cmd.OutOrStdout(), cleanTargets(dataDir()), force, asJSON)
check, _ := cmd.Flags().GetBool("check")
return runClean(cmd.OutOrStdout(), cleanTargets(dataDir()), force, asJSON, check)
},
}
cmd.Flags().Bool("force", false, "Delete the transient data (without this flag, clean only previews)")
cmd.Flags().Bool("json", false, "Print the clean report as JSON")
cmd.Flags().Bool("check", false, "Exit with an error when transient data is present")
return cmd
}

// runClean scans each target, then either previews it or removes it. A
// missing target is reported and skipped rather than treated as an error, so
// running clean on a fresh machine is a no-op.
func runClean(out io.Writer, targets []cleanTarget, force, asJSON bool) error {
func runClean(out io.Writer, targets []cleanTarget, force, asJSON, check bool) error {
force = force && !check
report := cleanReport{
Forced: force,
Targets: make([]cleanTargetReport, 0, len(targets)),
Expand Down Expand Up @@ -127,7 +131,13 @@ func runClean(out io.Writer, targets []cleanTarget, force, asJSON bool) error {
if asJSON {
enc := json.NewEncoder(out)
enc.SetIndent("", " ")
return enc.Encode(report)
if err := enc.Encode(report); err != nil {
return err
}
if check && report.PresentCount > 0 {
return errCleanTargetsPresent
}
return nil
}

fmt.Fprintln(out)
Expand All @@ -140,9 +150,14 @@ func runClean(out io.Writer, targets []cleanTarget, force, asJSON bool) error {
return nil
}
fmt.Fprintf(out, "%s across %d location(s). Run with --force to delete.\n", humanizeBytes(report.TotalBytes), report.PresentCount)
if check {
return errCleanTargetsPresent
}
return nil
}

var errCleanTargetsPresent = errors.New("transient data is present")

// dirSize returns the total size of all files under path. The second return
// value reports whether path exists; a missing path yields (0, false, nil).
func dirSize(path string) (int64, bool, error) {
Expand Down
95 changes: 92 additions & 3 deletions cmd/clean_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,14 +38,22 @@ func writeFile(t *testing.T, path, content string) {
}

func execClean(t *testing.T, dataDir string, args ...string) string {
t.Helper()
out, err := execCleanErr(t, dataDir, args...)
require.NoError(t, err)
return out
}

func execCleanErr(t *testing.T, dataDir string, args ...string) (string, error) {
t.Helper()
cmd := newCleanCmdWithDeps(func() string { return dataDir })
var out bytes.Buffer
var errOut bytes.Buffer
cmd.SetOut(&out)
cmd.SetErr(&out)
cmd.SetErr(&errOut)
cmd.SetArgs(args)
require.NoError(t, cmd.Execute())
return out.String()
err := cmd.Execute()
return out.String(), err
}

func TestCleanPreviewListsTargetsWithoutDeleting(t *testing.T) {
Expand Down Expand Up @@ -101,6 +109,40 @@ func TestCleanMissingDirsForce(t *testing.T) {
assert.Contains(t, out, "Reclaimed 0 B.")
}

func TestCleanCheckMissingDirsSucceeds(t *testing.T) {
dataDir := t.TempDir()

out := execClean(t, dataDir, "--check")

assert.Contains(t, out, "not present")
assert.Contains(t, out, "Nothing to clean.")
}

func TestCleanCheckReturnsErrorWhenTargetsPresent(t *testing.T) {
dataDir := seedCleanData(t)

out, err := execCleanErr(t, dataDir, "--check")

require.ErrorIs(t, err, errCleanTargetsPresent)
assert.Contains(t, out, "sessions")
assert.Contains(t, out, "diagnostics")
assert.Contains(t, out, "Run with --force to delete.")
assert.DirExists(t, filepath.Join(dataDir, "sessions"))
assert.DirExists(t, filepath.Join(dataDir, "diagnostics"))
}

func TestCleanCheckForceDoesNotDelete(t *testing.T) {
dataDir := seedCleanData(t)

out, err := execCleanErr(t, dataDir, "--check", "--force")

require.ErrorIs(t, err, errCleanTargetsPresent)
assert.NotContains(t, out, "removed")
assert.Contains(t, out, "Run with --force to delete.")
assert.DirExists(t, filepath.Join(dataDir, "sessions"))
assert.DirExists(t, filepath.Join(dataDir, "diagnostics"))
}

func TestCleanJSONPreviewReportsTargetsWithoutDeleting(t *testing.T) {
dataDir := seedCleanData(t)

Expand Down Expand Up @@ -152,6 +194,53 @@ func TestCleanJSONMissingDirs(t *testing.T) {
}
}

func TestCleanJSONCheckSucceedsWhenNothingPresent(t *testing.T) {
dataDir := t.TempDir()

out := execClean(t, dataDir, "--check", "--json")

var report cleanReport
require.NoError(t, json.Unmarshal([]byte(out), &report))
assert.False(t, report.Forced)
assert.Equal(t, 0, report.PresentCount)
}

func TestCleanJSONCheckReturnsErrorWhenTargetsPresent(t *testing.T) {
dataDir := seedCleanData(t)

out, err := execCleanErr(t, dataDir, "--check", "--json")

require.ErrorIs(t, err, errCleanTargetsPresent)
var report cleanReport
require.NoError(t, json.Unmarshal([]byte(out), &report))
assert.False(t, report.Forced)
assert.Equal(t, int64(len("session")+len("diag")), report.TotalBytes)
assert.Equal(t, 2, report.PresentCount)
for _, target := range report.Targets {
assert.True(t, target.Exists)
assert.False(t, target.Removed)
}
assert.DirExists(t, filepath.Join(dataDir, "sessions"))
assert.DirExists(t, filepath.Join(dataDir, "diagnostics"))
}

func TestCleanJSONCheckForceDoesNotDelete(t *testing.T) {
dataDir := seedCleanData(t)

out, err := execCleanErr(t, dataDir, "--check", "--force", "--json")

require.ErrorIs(t, err, errCleanTargetsPresent)
var report cleanReport
require.NoError(t, json.Unmarshal([]byte(out), &report))
assert.False(t, report.Forced)
for _, target := range report.Targets {
assert.True(t, target.Exists)
assert.False(t, target.Removed)
}
assert.DirExists(t, filepath.Join(dataDir, "sessions"))
assert.DirExists(t, filepath.Join(dataDir, "diagnostics"))
}

func TestCleanRejectsArgs(t *testing.T) {
cmd := newCleanCmdWithDeps(func() string { return t.TempDir() })
cmd.SetOut(&bytes.Buffer{})
Expand Down
3 changes: 3 additions & 0 deletions web/src/pages/docs/cli.astro
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,9 @@ grut keys --json</code></pre>
<pre><code># Preview what would be removed
grut clean

# Fail if transient data is present without deleting it
grut clean --check

# Delete the transient data
grut clean --force</code></pre>

Expand Down
Loading