mirror of https://github.com/databricks/cli.git
Add command line autocomplete to the fs commands (#1622)
## Changes This PR adds autocomplete for cat, cp, ls, mkdir and rm. The new completer can do completion for any `Filer`. The command completion for the `sync` command can be moved to use this general completer as a follow-up. ## Tests - Tested manually against a workspace - Unit tests
This commit is contained in:
parent
d3d828d175
commit
65f4aad87c
|
@ -30,5 +30,8 @@ func newCatCommand() *cobra.Command {
|
||||||
return cmdio.Render(ctx, r)
|
return cmdio.Render(ctx, r)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
v := newValidArgs()
|
||||||
|
cmd.ValidArgsFunction = v.Validate
|
||||||
|
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
|
@ -200,5 +200,10 @@ func newCpCommand() *cobra.Command {
|
||||||
return c.cpFileToFile(sourcePath, targetPath)
|
return c.cpFileToFile(sourcePath, targetPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
v := newValidArgs()
|
||||||
|
// The copy command has two paths that can be completed (SOURCE_PATH & TARGET_PATH)
|
||||||
|
v.pathArgCount = 2
|
||||||
|
cmd.ValidArgsFunction = v.Validate
|
||||||
|
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,8 @@ import (
|
||||||
|
|
||||||
"github.com/databricks/cli/cmd/root"
|
"github.com/databricks/cli/cmd/root"
|
||||||
"github.com/databricks/cli/libs/filer"
|
"github.com/databricks/cli/libs/filer"
|
||||||
|
"github.com/databricks/cli/libs/filer/completer"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
func filerForPath(ctx context.Context, fullPath string) (filer.Filer, string, error) {
|
func filerForPath(ctx context.Context, fullPath string) (filer.Filer, string, error) {
|
||||||
|
@ -46,6 +48,58 @@ func filerForPath(ctx context.Context, fullPath string) (filer.Filer, string, er
|
||||||
return f, path, err
|
return f, path, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const dbfsPrefix string = "dbfs:"
|
||||||
|
|
||||||
func isDbfsPath(path string) bool {
|
func isDbfsPath(path string) bool {
|
||||||
return strings.HasPrefix(path, "dbfs:/")
|
return strings.HasPrefix(path, dbfsPrefix)
|
||||||
|
}
|
||||||
|
|
||||||
|
type validArgs struct {
|
||||||
|
mustWorkspaceClientFunc func(cmd *cobra.Command, args []string) error
|
||||||
|
filerForPathFunc func(ctx context.Context, fullPath string) (filer.Filer, string, error)
|
||||||
|
pathArgCount int
|
||||||
|
onlyDirs bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func newValidArgs() *validArgs {
|
||||||
|
return &validArgs{
|
||||||
|
mustWorkspaceClientFunc: root.MustWorkspaceClient,
|
||||||
|
filerForPathFunc: filerForPath,
|
||||||
|
pathArgCount: 1,
|
||||||
|
onlyDirs: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *validArgs) Validate(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||||
|
cmd.SetContext(root.SkipPrompt(cmd.Context()))
|
||||||
|
|
||||||
|
if len(args) >= v.pathArgCount {
|
||||||
|
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||||
|
}
|
||||||
|
|
||||||
|
err := v.mustWorkspaceClientFunc(cmd, args)
|
||||||
|
if err != nil {
|
||||||
|
return nil, cobra.ShellCompDirectiveError
|
||||||
|
}
|
||||||
|
|
||||||
|
filer, toCompletePath, err := v.filerForPathFunc(cmd.Context(), toComplete)
|
||||||
|
if err != nil {
|
||||||
|
return nil, cobra.ShellCompDirectiveError
|
||||||
|
}
|
||||||
|
|
||||||
|
completer := completer.New(cmd.Context(), filer, v.onlyDirs)
|
||||||
|
|
||||||
|
// Dbfs should have a prefix and always use the "/" separator
|
||||||
|
isDbfsPath := isDbfsPath(toComplete)
|
||||||
|
if isDbfsPath {
|
||||||
|
completer.SetPrefix(dbfsPrefix)
|
||||||
|
completer.SetIsLocalPath(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
completions, directive, err := completer.CompletePath(toCompletePath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, cobra.ShellCompDirectiveError
|
||||||
|
}
|
||||||
|
|
||||||
|
return completions, directive
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,9 +3,13 @@ package fs
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/databricks/cli/cmd/root"
|
||||||
"github.com/databricks/cli/libs/filer"
|
"github.com/databricks/cli/libs/filer"
|
||||||
|
"github.com/databricks/databricks-sdk-go/experimental/mocks"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
@ -60,3 +64,88 @@ func TestFilerForWindowsLocalPaths(t *testing.T) {
|
||||||
testWindowsFilerForPath(t, ctx, `d:\abc`)
|
testWindowsFilerForPath(t, ctx, `d:\abc`)
|
||||||
testWindowsFilerForPath(t, ctx, `f:\abc\ef`)
|
testWindowsFilerForPath(t, ctx, `f:\abc\ef`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func mockMustWorkspaceClientFunc(cmd *cobra.Command, args []string) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func setupCommand(t *testing.T) (*cobra.Command, *mocks.MockWorkspaceClient) {
|
||||||
|
m := mocks.NewMockWorkspaceClient(t)
|
||||||
|
ctx := context.Background()
|
||||||
|
ctx = root.SetWorkspaceClient(ctx, m.WorkspaceClient)
|
||||||
|
|
||||||
|
cmd := &cobra.Command{}
|
||||||
|
cmd.SetContext(ctx)
|
||||||
|
|
||||||
|
return cmd, m
|
||||||
|
}
|
||||||
|
|
||||||
|
func setupTest(t *testing.T) (*validArgs, *cobra.Command, *mocks.MockWorkspaceClient) {
|
||||||
|
cmd, m := setupCommand(t)
|
||||||
|
|
||||||
|
fakeFilerForPath := func(ctx context.Context, fullPath string) (filer.Filer, string, error) {
|
||||||
|
fakeFiler := filer.NewFakeFiler(map[string]filer.FakeFileInfo{
|
||||||
|
"dir": {FakeName: "root", FakeDir: true},
|
||||||
|
"dir/dirA": {FakeDir: true},
|
||||||
|
"dir/dirB": {FakeDir: true},
|
||||||
|
"dir/fileA": {},
|
||||||
|
})
|
||||||
|
return fakeFiler, strings.TrimPrefix(fullPath, "dbfs:/"), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
v := newValidArgs()
|
||||||
|
v.filerForPathFunc = fakeFilerForPath
|
||||||
|
v.mustWorkspaceClientFunc = mockMustWorkspaceClientFunc
|
||||||
|
|
||||||
|
return v, cmd, m
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetValidArgsFunctionDbfsCompletion(t *testing.T) {
|
||||||
|
v, cmd, _ := setupTest(t)
|
||||||
|
completions, directive := v.Validate(cmd, []string{}, "dbfs:/dir/")
|
||||||
|
assert.Equal(t, []string{"dbfs:/dir/dirA/", "dbfs:/dir/dirB/", "dbfs:/dir/fileA"}, completions)
|
||||||
|
assert.Equal(t, cobra.ShellCompDirectiveNoSpace, directive)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetValidArgsFunctionLocalCompletion(t *testing.T) {
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
t.Skip()
|
||||||
|
}
|
||||||
|
|
||||||
|
v, cmd, _ := setupTest(t)
|
||||||
|
completions, directive := v.Validate(cmd, []string{}, "dir/")
|
||||||
|
assert.Equal(t, []string{"dir/dirA/", "dir/dirB/", "dir/fileA", "dbfs:/"}, completions)
|
||||||
|
assert.Equal(t, cobra.ShellCompDirectiveNoSpace, directive)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetValidArgsFunctionLocalCompletionWindows(t *testing.T) {
|
||||||
|
if runtime.GOOS != "windows" {
|
||||||
|
t.Skip()
|
||||||
|
}
|
||||||
|
|
||||||
|
v, cmd, _ := setupTest(t)
|
||||||
|
completions, directive := v.Validate(cmd, []string{}, "dir/")
|
||||||
|
assert.Equal(t, []string{"dir\\dirA\\", "dir\\dirB\\", "dir\\fileA", "dbfs:/"}, completions)
|
||||||
|
assert.Equal(t, cobra.ShellCompDirectiveNoSpace, directive)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetValidArgsFunctionCompletionOnlyDirs(t *testing.T) {
|
||||||
|
v, cmd, _ := setupTest(t)
|
||||||
|
v.onlyDirs = true
|
||||||
|
completions, directive := v.Validate(cmd, []string{}, "dbfs:/dir/")
|
||||||
|
assert.Equal(t, []string{"dbfs:/dir/dirA/", "dbfs:/dir/dirB/"}, completions)
|
||||||
|
assert.Equal(t, cobra.ShellCompDirectiveNoSpace, directive)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetValidArgsFunctionNotCompletedArgument(t *testing.T) {
|
||||||
|
cmd, _ := setupCommand(t)
|
||||||
|
|
||||||
|
v := newValidArgs()
|
||||||
|
v.pathArgCount = 0
|
||||||
|
v.mustWorkspaceClientFunc = mockMustWorkspaceClientFunc
|
||||||
|
|
||||||
|
completions, directive := v.Validate(cmd, []string{}, "dbfs:/")
|
||||||
|
|
||||||
|
assert.Nil(t, completions)
|
||||||
|
assert.Equal(t, cobra.ShellCompDirectiveNoFileComp, directive)
|
||||||
|
}
|
||||||
|
|
|
@ -89,5 +89,9 @@ func newLsCommand() *cobra.Command {
|
||||||
`))
|
`))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
v := newValidArgs()
|
||||||
|
v.onlyDirs = true
|
||||||
|
cmd.ValidArgsFunction = v.Validate
|
||||||
|
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,5 +28,9 @@ func newMkdirCommand() *cobra.Command {
|
||||||
return f.Mkdir(ctx, path)
|
return f.Mkdir(ctx, path)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
v := newValidArgs()
|
||||||
|
v.onlyDirs = true
|
||||||
|
cmd.ValidArgsFunction = v.Validate
|
||||||
|
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,5 +32,8 @@ func newRmCommand() *cobra.Command {
|
||||||
return f.Delete(ctx, path)
|
return f.Delete(ctx, path)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
v := newValidArgs()
|
||||||
|
cmd.ValidArgsFunction = v.Validate
|
||||||
|
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
package internal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
_ "github.com/databricks/cli/cmd/fs"
|
||||||
|
"github.com/databricks/cli/libs/filer"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func setupCompletionFile(t *testing.T, f filer.Filer) {
|
||||||
|
err := f.Write(context.Background(), "dir1/file1.txt", strings.NewReader("abc"), filer.CreateParentDirectories)
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAccFsCompletion(t *testing.T) {
|
||||||
|
f, tmpDir := setupDbfsFiler(t)
|
||||||
|
setupCompletionFile(t, f)
|
||||||
|
|
||||||
|
stdout, _ := RequireSuccessfulRun(t, "__complete", "fs", "ls", tmpDir)
|
||||||
|
expectedOutput := fmt.Sprintf("%s/dir1/\n:2\n", tmpDir)
|
||||||
|
assert.Equal(t, expectedOutput, stdout.String())
|
||||||
|
}
|
|
@ -0,0 +1,95 @@
|
||||||
|
package completer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"path"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/databricks/cli/libs/filer"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
type completer struct {
|
||||||
|
ctx context.Context
|
||||||
|
|
||||||
|
// The filer to use for completing remote or local paths.
|
||||||
|
filer filer.Filer
|
||||||
|
|
||||||
|
// CompletePath will only return directories when onlyDirs is true.
|
||||||
|
onlyDirs bool
|
||||||
|
|
||||||
|
// Prefix to prepend to completions.
|
||||||
|
prefix string
|
||||||
|
|
||||||
|
// Whether the path is local or remote. If the path is local we use the `filepath`
|
||||||
|
// package for path manipulation. Otherwise we use the `path` package.
|
||||||
|
isLocalPath bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// General completer that takes a filer to complete remote paths when TAB-ing through a path.
|
||||||
|
func New(ctx context.Context, filer filer.Filer, onlyDirs bool) *completer {
|
||||||
|
return &completer{ctx: ctx, filer: filer, onlyDirs: onlyDirs, prefix: "", isLocalPath: true}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *completer) SetPrefix(p string) {
|
||||||
|
c.prefix = p
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *completer) SetIsLocalPath(i bool) {
|
||||||
|
c.isLocalPath = i
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *completer) CompletePath(p string) ([]string, cobra.ShellCompDirective, error) {
|
||||||
|
trailingSeparator := "/"
|
||||||
|
joinFunc := path.Join
|
||||||
|
|
||||||
|
// Use filepath functions if we are in a local path.
|
||||||
|
if c.isLocalPath {
|
||||||
|
joinFunc = filepath.Join
|
||||||
|
trailingSeparator = string(filepath.Separator)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the user is TAB-ing their way through a path and the
|
||||||
|
// path ends in a trailing slash, we should list nested directories.
|
||||||
|
// If the path is incomplete, however, then we should list adjacent
|
||||||
|
// directories.
|
||||||
|
dirPath := p
|
||||||
|
if !strings.HasSuffix(p, trailingSeparator) {
|
||||||
|
dirPath = path.Dir(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
entries, err := c.filer.ReadDir(c.ctx, dirPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, cobra.ShellCompDirectiveError, err
|
||||||
|
}
|
||||||
|
|
||||||
|
completions := []string{}
|
||||||
|
for _, entry := range entries {
|
||||||
|
if c.onlyDirs && !entry.IsDir() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Join directory path and entry name
|
||||||
|
completion := joinFunc(dirPath, entry.Name())
|
||||||
|
|
||||||
|
// Prepend prefix if it has been set
|
||||||
|
if c.prefix != "" {
|
||||||
|
completion = joinFunc(c.prefix, completion)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add trailing separator for directories.
|
||||||
|
if entry.IsDir() {
|
||||||
|
completion += trailingSeparator
|
||||||
|
}
|
||||||
|
|
||||||
|
completions = append(completions, completion)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the path is local, we add the dbfs:/ prefix suggestion as an option
|
||||||
|
if c.isLocalPath {
|
||||||
|
completions = append(completions, "dbfs:/")
|
||||||
|
}
|
||||||
|
|
||||||
|
return completions, cobra.ShellCompDirectiveNoSpace, err
|
||||||
|
}
|
|
@ -0,0 +1,104 @@
|
||||||
|
package completer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"runtime"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/databricks/cli/cmd/root"
|
||||||
|
"github.com/databricks/cli/libs/filer"
|
||||||
|
"github.com/databricks/databricks-sdk-go/experimental/mocks"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func setupCompleter(t *testing.T, onlyDirs bool) *completer {
|
||||||
|
ctx := context.Background()
|
||||||
|
// Needed to make type context.valueCtx for mockFilerForPath
|
||||||
|
ctx = root.SetWorkspaceClient(ctx, mocks.NewMockWorkspaceClient(t).WorkspaceClient)
|
||||||
|
|
||||||
|
fakeFiler := filer.NewFakeFiler(map[string]filer.FakeFileInfo{
|
||||||
|
"dir": {FakeName: "root", FakeDir: true},
|
||||||
|
"dir/dirA": {FakeDir: true},
|
||||||
|
"dir/dirB": {FakeDir: true},
|
||||||
|
"dir/fileA": {},
|
||||||
|
})
|
||||||
|
|
||||||
|
completer := New(ctx, fakeFiler, onlyDirs)
|
||||||
|
completer.SetIsLocalPath(false)
|
||||||
|
return completer
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFilerCompleterSetsPrefix(t *testing.T) {
|
||||||
|
completer := setupCompleter(t, true)
|
||||||
|
completer.SetPrefix("dbfs:")
|
||||||
|
completions, directive, err := completer.CompletePath("dir/")
|
||||||
|
|
||||||
|
assert.Equal(t, []string{"dbfs:/dir/dirA/", "dbfs:/dir/dirB/"}, completions)
|
||||||
|
assert.Equal(t, cobra.ShellCompDirectiveNoSpace, directive)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFilerCompleterReturnsNestedDirs(t *testing.T) {
|
||||||
|
completer := setupCompleter(t, true)
|
||||||
|
completions, directive, err := completer.CompletePath("dir/")
|
||||||
|
|
||||||
|
assert.Equal(t, []string{"dir/dirA/", "dir/dirB/"}, completions)
|
||||||
|
assert.Equal(t, cobra.ShellCompDirectiveNoSpace, directive)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFilerCompleterReturnsAdjacentDirs(t *testing.T) {
|
||||||
|
completer := setupCompleter(t, true)
|
||||||
|
completions, directive, err := completer.CompletePath("dir/wrong_path")
|
||||||
|
|
||||||
|
assert.Equal(t, []string{"dir/dirA/", "dir/dirB/"}, completions)
|
||||||
|
assert.Equal(t, cobra.ShellCompDirectiveNoSpace, directive)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFilerCompleterReturnsNestedDirsAndFiles(t *testing.T) {
|
||||||
|
completer := setupCompleter(t, false)
|
||||||
|
completions, directive, err := completer.CompletePath("dir/")
|
||||||
|
|
||||||
|
assert.Equal(t, []string{"dir/dirA/", "dir/dirB/", "dir/fileA"}, completions)
|
||||||
|
assert.Equal(t, cobra.ShellCompDirectiveNoSpace, directive)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFilerCompleterAddsDbfsPath(t *testing.T) {
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
t.Skip()
|
||||||
|
}
|
||||||
|
|
||||||
|
completer := setupCompleter(t, true)
|
||||||
|
completer.SetIsLocalPath(true)
|
||||||
|
completions, directive, err := completer.CompletePath("dir/")
|
||||||
|
|
||||||
|
assert.Equal(t, []string{"dir/dirA/", "dir/dirB/", "dbfs:/"}, completions)
|
||||||
|
assert.Equal(t, cobra.ShellCompDirectiveNoSpace, directive)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFilerCompleterWindowsSeparator(t *testing.T) {
|
||||||
|
if runtime.GOOS != "windows" {
|
||||||
|
t.Skip()
|
||||||
|
}
|
||||||
|
|
||||||
|
completer := setupCompleter(t, true)
|
||||||
|
completer.SetIsLocalPath(true)
|
||||||
|
completions, directive, err := completer.CompletePath("dir/")
|
||||||
|
|
||||||
|
assert.Equal(t, []string{"dir\\dirA\\", "dir\\dirB\\", "dbfs:/"}, completions)
|
||||||
|
assert.Equal(t, cobra.ShellCompDirectiveNoSpace, directive)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFilerCompleterNoCompletions(t *testing.T) {
|
||||||
|
completer := setupCompleter(t, true)
|
||||||
|
completions, directive, err := completer.CompletePath("wrong_dir/wrong_dir")
|
||||||
|
|
||||||
|
assert.Nil(t, completions)
|
||||||
|
assert.Equal(t, cobra.ShellCompDirectiveError, directive)
|
||||||
|
assert.Error(t, err)
|
||||||
|
}
|
|
@ -0,0 +1,134 @@
|
||||||
|
package filer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/fs"
|
||||||
|
"path"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type FakeDirEntry struct {
|
||||||
|
FakeFileInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
func (entry FakeDirEntry) Type() fs.FileMode {
|
||||||
|
typ := fs.ModePerm
|
||||||
|
if entry.FakeDir {
|
||||||
|
typ |= fs.ModeDir
|
||||||
|
}
|
||||||
|
return typ
|
||||||
|
}
|
||||||
|
|
||||||
|
func (entry FakeDirEntry) Info() (fs.FileInfo, error) {
|
||||||
|
return entry.FakeFileInfo, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type FakeFileInfo struct {
|
||||||
|
FakeName string
|
||||||
|
FakeSize int64
|
||||||
|
FakeDir bool
|
||||||
|
FakeMode fs.FileMode
|
||||||
|
}
|
||||||
|
|
||||||
|
func (info FakeFileInfo) Name() string {
|
||||||
|
return info.FakeName
|
||||||
|
}
|
||||||
|
|
||||||
|
func (info FakeFileInfo) Size() int64 {
|
||||||
|
return info.FakeSize
|
||||||
|
}
|
||||||
|
|
||||||
|
func (info FakeFileInfo) Mode() fs.FileMode {
|
||||||
|
return info.FakeMode
|
||||||
|
}
|
||||||
|
|
||||||
|
func (info FakeFileInfo) ModTime() time.Time {
|
||||||
|
return time.Now()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (info FakeFileInfo) IsDir() bool {
|
||||||
|
return info.FakeDir
|
||||||
|
}
|
||||||
|
|
||||||
|
func (info FakeFileInfo) Sys() any {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type FakeFiler struct {
|
||||||
|
entries map[string]FakeFileInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FakeFiler) Write(ctx context.Context, p string, reader io.Reader, mode ...WriteMode) error {
|
||||||
|
return fmt.Errorf("not implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FakeFiler) Read(ctx context.Context, p string) (io.ReadCloser, error) {
|
||||||
|
_, ok := f.entries[p]
|
||||||
|
if !ok {
|
||||||
|
return nil, fs.ErrNotExist
|
||||||
|
}
|
||||||
|
|
||||||
|
return io.NopCloser(strings.NewReader("foo")), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FakeFiler) Delete(ctx context.Context, p string, mode ...DeleteMode) error {
|
||||||
|
return fmt.Errorf("not implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FakeFiler) ReadDir(ctx context.Context, p string) ([]fs.DirEntry, error) {
|
||||||
|
p = strings.TrimSuffix(p, "/")
|
||||||
|
entry, ok := f.entries[p]
|
||||||
|
if !ok {
|
||||||
|
return nil, NoSuchDirectoryError{p}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !entry.FakeDir {
|
||||||
|
return nil, fs.ErrInvalid
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find all entries contained in the specified directory `p`.
|
||||||
|
var out []fs.DirEntry
|
||||||
|
for k, v := range f.entries {
|
||||||
|
if k == p || path.Dir(k) != p {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
out = append(out, FakeDirEntry{v})
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Slice(out, func(i, j int) bool { return out[i].Name() < out[j].Name() })
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FakeFiler) Mkdir(ctx context.Context, path string) error {
|
||||||
|
return fmt.Errorf("not implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FakeFiler) Stat(ctx context.Context, path string) (fs.FileInfo, error) {
|
||||||
|
entry, ok := f.entries[path]
|
||||||
|
if !ok {
|
||||||
|
return nil, fs.ErrNotExist
|
||||||
|
}
|
||||||
|
|
||||||
|
return entry, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewFakeFiler(entries map[string]FakeFileInfo) *FakeFiler {
|
||||||
|
fakeFiler := &FakeFiler{
|
||||||
|
entries: entries,
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, v := range fakeFiler.entries {
|
||||||
|
if v.FakeName != "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
v.FakeName = path.Base(k)
|
||||||
|
fakeFiler.entries[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
return fakeFiler
|
||||||
|
}
|
|
@ -2,124 +2,14 @@ package filer
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
|
||||||
"io"
|
"io"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"path"
|
|
||||||
"sort"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
type fakeDirEntry struct {
|
|
||||||
fakeFileInfo
|
|
||||||
}
|
|
||||||
|
|
||||||
func (entry fakeDirEntry) Type() fs.FileMode {
|
|
||||||
typ := fs.ModePerm
|
|
||||||
if entry.dir {
|
|
||||||
typ |= fs.ModeDir
|
|
||||||
}
|
|
||||||
return typ
|
|
||||||
}
|
|
||||||
|
|
||||||
func (entry fakeDirEntry) Info() (fs.FileInfo, error) {
|
|
||||||
return entry.fakeFileInfo, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type fakeFileInfo struct {
|
|
||||||
name string
|
|
||||||
size int64
|
|
||||||
dir bool
|
|
||||||
mode fs.FileMode
|
|
||||||
}
|
|
||||||
|
|
||||||
func (info fakeFileInfo) Name() string {
|
|
||||||
return info.name
|
|
||||||
}
|
|
||||||
|
|
||||||
func (info fakeFileInfo) Size() int64 {
|
|
||||||
return info.size
|
|
||||||
}
|
|
||||||
|
|
||||||
func (info fakeFileInfo) Mode() fs.FileMode {
|
|
||||||
return info.mode
|
|
||||||
}
|
|
||||||
|
|
||||||
func (info fakeFileInfo) ModTime() time.Time {
|
|
||||||
return time.Now()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (info fakeFileInfo) IsDir() bool {
|
|
||||||
return info.dir
|
|
||||||
}
|
|
||||||
|
|
||||||
func (info fakeFileInfo) Sys() any {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type fakeFiler struct {
|
|
||||||
entries map[string]fakeFileInfo
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *fakeFiler) Write(ctx context.Context, p string, reader io.Reader, mode ...WriteMode) error {
|
|
||||||
return fmt.Errorf("not implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *fakeFiler) Read(ctx context.Context, p string) (io.ReadCloser, error) {
|
|
||||||
_, ok := f.entries[p]
|
|
||||||
if !ok {
|
|
||||||
return nil, fs.ErrNotExist
|
|
||||||
}
|
|
||||||
|
|
||||||
return io.NopCloser(strings.NewReader("foo")), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *fakeFiler) Delete(ctx context.Context, p string, mode ...DeleteMode) error {
|
|
||||||
return fmt.Errorf("not implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *fakeFiler) ReadDir(ctx context.Context, p string) ([]fs.DirEntry, error) {
|
|
||||||
entry, ok := f.entries[p]
|
|
||||||
if !ok {
|
|
||||||
return nil, fs.ErrNotExist
|
|
||||||
}
|
|
||||||
|
|
||||||
if !entry.dir {
|
|
||||||
return nil, fs.ErrInvalid
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find all entries contained in the specified directory `p`.
|
|
||||||
var out []fs.DirEntry
|
|
||||||
for k, v := range f.entries {
|
|
||||||
if k == p || path.Dir(k) != p {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
out = append(out, fakeDirEntry{v})
|
|
||||||
}
|
|
||||||
|
|
||||||
sort.Slice(out, func(i, j int) bool { return out[i].Name() < out[j].Name() })
|
|
||||||
return out, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *fakeFiler) Mkdir(ctx context.Context, path string) error {
|
|
||||||
return fmt.Errorf("not implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *fakeFiler) Stat(ctx context.Context, path string) (fs.FileInfo, error) {
|
|
||||||
entry, ok := f.entries[path]
|
|
||||||
if !ok {
|
|
||||||
return nil, fs.ErrNotExist
|
|
||||||
}
|
|
||||||
|
|
||||||
return entry, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFsImplementsFS(t *testing.T) {
|
func TestFsImplementsFS(t *testing.T) {
|
||||||
var _ fs.FS = &filerFS{}
|
var _ fs.FS = &filerFS{}
|
||||||
}
|
}
|
||||||
|
@ -145,22 +35,12 @@ func TestFsDirImplementsFsReadDirFile(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func fakeFS() fs.FS {
|
func fakeFS() fs.FS {
|
||||||
fakeFiler := &fakeFiler{
|
fakeFiler := NewFakeFiler(map[string]FakeFileInfo{
|
||||||
entries: map[string]fakeFileInfo{
|
".": {FakeName: "root", FakeDir: true},
|
||||||
".": {name: "root", dir: true},
|
"dirA": {FakeDir: true},
|
||||||
"dirA": {dir: true},
|
"dirB": {FakeDir: true},
|
||||||
"dirB": {dir: true},
|
"fileA": {FakeSize: 3},
|
||||||
"fileA": {size: 3},
|
})
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for k, v := range fakeFiler.entries {
|
|
||||||
if v.name != "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
v.name = path.Base(k)
|
|
||||||
fakeFiler.entries[k] = v
|
|
||||||
}
|
|
||||||
|
|
||||||
return NewFS(context.Background(), fakeFiler)
|
return NewFS(context.Background(), fakeFiler)
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,7 +20,7 @@ func collectRelativePaths(files []File) []string {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGlobFileset(t *testing.T) {
|
func TestGlobFileset(t *testing.T) {
|
||||||
root := vfs.MustNew("../filer")
|
root := vfs.MustNew("./")
|
||||||
entries, err := root.ReadDir(".")
|
entries, err := root.ReadDir(".")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
@ -32,6 +32,7 @@ func TestGlobFileset(t *testing.T) {
|
||||||
files, err := g.All()
|
files, err := g.All()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// +1 as there's one folder in ../filer
|
||||||
require.Equal(t, len(files), len(entries))
|
require.Equal(t, len(files), len(entries))
|
||||||
for _, f := range files {
|
for _, f := range files {
|
||||||
exists := slices.ContainsFunc(entries, func(de fs.DirEntry) bool {
|
exists := slices.ContainsFunc(entries, func(de fs.DirEntry) bool {
|
||||||
|
@ -51,7 +52,7 @@ func TestGlobFileset(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGlobFilesetWithRelativeRoot(t *testing.T) {
|
func TestGlobFilesetWithRelativeRoot(t *testing.T) {
|
||||||
root := vfs.MustNew("../filer")
|
root := vfs.MustNew("../set")
|
||||||
entries, err := root.ReadDir(".")
|
entries, err := root.ReadDir(".")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue