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)
|
||||
}
|
||||
|
||||
v := newValidArgs()
|
||||
cmd.ValidArgsFunction = v.Validate
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
|
|
@ -200,5 +200,10 @@ func newCpCommand() *cobra.Command {
|
|||
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
|
||||
}
|
||||
|
|
|
@ -8,6 +8,8 @@ import (
|
|||
|
||||
"github.com/databricks/cli/cmd/root"
|
||||
"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) {
|
||||
|
@ -46,6 +48,58 @@ func filerForPath(ctx context.Context, fullPath string) (filer.Filer, string, er
|
|||
return f, path, err
|
||||
}
|
||||
|
||||
const dbfsPrefix string = "dbfs:"
|
||||
|
||||
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 (
|
||||
"context"
|
||||
"runtime"
|
||||
"strings"
|
||||
"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"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
@ -60,3 +64,88 @@ func TestFilerForWindowsLocalPaths(t *testing.T) {
|
|||
testWindowsFilerForPath(t, ctx, `d:\abc`)
|
||||
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
|
||||
}
|
||||
|
|
|
@ -28,5 +28,9 @@ func newMkdirCommand() *cobra.Command {
|
|||
return f.Mkdir(ctx, path)
|
||||
}
|
||||
|
||||
v := newValidArgs()
|
||||
v.onlyDirs = true
|
||||
cmd.ValidArgsFunction = v.Validate
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
|
|
@ -32,5 +32,8 @@ func newRmCommand() *cobra.Command {
|
|||
return f.Delete(ctx, path)
|
||||
}
|
||||
|
||||
v := newValidArgs()
|
||||
cmd.ValidArgsFunction = v.Validate
|
||||
|
||||
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 (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"path"
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"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) {
|
||||
var _ fs.FS = &filerFS{}
|
||||
}
|
||||
|
@ -145,22 +35,12 @@ func TestFsDirImplementsFsReadDirFile(t *testing.T) {
|
|||
}
|
||||
|
||||
func fakeFS() fs.FS {
|
||||
fakeFiler := &fakeFiler{
|
||||
entries: map[string]fakeFileInfo{
|
||||
".": {name: "root", dir: true},
|
||||
"dirA": {dir: true},
|
||||
"dirB": {dir: true},
|
||||
"fileA": {size: 3},
|
||||
},
|
||||
}
|
||||
|
||||
for k, v := range fakeFiler.entries {
|
||||
if v.name != "" {
|
||||
continue
|
||||
}
|
||||
v.name = path.Base(k)
|
||||
fakeFiler.entries[k] = v
|
||||
}
|
||||
fakeFiler := NewFakeFiler(map[string]FakeFileInfo{
|
||||
".": {FakeName: "root", FakeDir: true},
|
||||
"dirA": {FakeDir: true},
|
||||
"dirB": {FakeDir: true},
|
||||
"fileA": {FakeSize: 3},
|
||||
})
|
||||
|
||||
return NewFS(context.Background(), fakeFiler)
|
||||
}
|
||||
|
|
|
@ -20,7 +20,7 @@ func collectRelativePaths(files []File) []string {
|
|||
}
|
||||
|
||||
func TestGlobFileset(t *testing.T) {
|
||||
root := vfs.MustNew("../filer")
|
||||
root := vfs.MustNew("./")
|
||||
entries, err := root.ReadDir(".")
|
||||
require.NoError(t, err)
|
||||
|
||||
|
@ -32,6 +32,7 @@ func TestGlobFileset(t *testing.T) {
|
|||
files, err := g.All()
|
||||
require.NoError(t, err)
|
||||
|
||||
// +1 as there's one folder in ../filer
|
||||
require.Equal(t, len(files), len(entries))
|
||||
for _, f := range files {
|
||||
exists := slices.ContainsFunc(entries, func(de fs.DirEntry) bool {
|
||||
|
@ -51,7 +52,7 @@ func TestGlobFileset(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestGlobFilesetWithRelativeRoot(t *testing.T) {
|
||||
root := vfs.MustNew("../filer")
|
||||
root := vfs.MustNew("../set")
|
||||
entries, err := root.ReadDir(".")
|
||||
require.NoError(t, err)
|
||||
|
||||
|
|
Loading…
Reference in New Issue