mirror of
https://github.com/kubernetes-sigs/kustomize.git
synced 2026-05-17 18:25:26 +00:00
Make filesys walk work.
This commit is contained in:
@@ -12,14 +12,14 @@ var _ os.FileInfo = fileInfo{}
|
||||
|
||||
// fileInfo implements os.FileInfo for a fileInMemory instance.
|
||||
type fileInfo struct {
|
||||
*fileInMemory
|
||||
node *fsNode
|
||||
}
|
||||
|
||||
// Name returns the name of the file
|
||||
func (fi fileInfo) Name() string { return fi.name }
|
||||
func (fi fileInfo) Name() string { return fi.node.Name() }
|
||||
|
||||
// Size returns the size of the file
|
||||
func (fi fileInfo) Size() int64 { return int64(len(fi.content)) }
|
||||
func (fi fileInfo) Size() int64 { return fi.node.Size() }
|
||||
|
||||
// Mode returns the file mode
|
||||
func (fi fileInfo) Mode() os.FileMode { return 0777 }
|
||||
@@ -28,7 +28,7 @@ func (fi fileInfo) Mode() os.FileMode { return 0777 }
|
||||
func (fi fileInfo) ModTime() time.Time { return time.Time{} }
|
||||
|
||||
// IsDir returns true if it is a directory
|
||||
func (fi fileInfo) IsDir() bool { return fi.dir }
|
||||
func (fi fileInfo) IsDir() bool { return fi.node.isNodeADir() }
|
||||
|
||||
// Sys should return underlying data source, but it now returns nil
|
||||
func (fi fileInfo) Sys() interface{} { return nil }
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
// Copyright 2019 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package filesys
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
)
|
||||
|
||||
var _ File = &fileInMemory{}
|
||||
|
||||
// fileInMemory implements File in-memory for tests.
|
||||
type fileInMemory struct {
|
||||
name string
|
||||
content []byte
|
||||
dir bool
|
||||
open bool
|
||||
}
|
||||
|
||||
// makeDir makes a fake directory.
|
||||
func makeDir(name string) *fileInMemory {
|
||||
return &fileInMemory{name: name, dir: true}
|
||||
}
|
||||
|
||||
// Close marks the fake file closed.
|
||||
func (f *fileInMemory) Close() error {
|
||||
f.open = false
|
||||
return nil
|
||||
}
|
||||
|
||||
// Read never fails, and doesn't mutate p.
|
||||
func (f *fileInMemory) Read(p []byte) (n int, err error) {
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
// Write saves the contents of the argument to memory.
|
||||
func (f *fileInMemory) Write(p []byte) (n int, err error) {
|
||||
f.content = p
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
// ContentMatches returns true if v matches fake file's content.
|
||||
func (f *fileInMemory) ContentMatches(v []byte) bool {
|
||||
return bytes.Equal(v, f.content)
|
||||
}
|
||||
|
||||
// GetContent the content of a fake file.
|
||||
func (f *fileInMemory) GetContent() []byte {
|
||||
return f.content
|
||||
}
|
||||
|
||||
// Stat returns nil.
|
||||
func (f *fileInMemory) Stat() (os.FileInfo, error) {
|
||||
return nil, nil
|
||||
}
|
||||
@@ -10,7 +10,6 @@ import (
|
||||
|
||||
const (
|
||||
Separator = string(filepath.Separator)
|
||||
doubleSep = Separator + Separator
|
||||
SelfDir = "."
|
||||
ParentDir = ".."
|
||||
)
|
||||
@@ -38,7 +37,8 @@ type FileSystem interface {
|
||||
CleanedAbs(path string) (ConfirmedDir, string, error)
|
||||
// Exists is true if the path exists in the file system.
|
||||
Exists(path string) bool
|
||||
// Glob returns the list of matching files
|
||||
// Glob returns the list of matching files,
|
||||
// emulating https://golang.org/pkg/path/filepath/#Glob
|
||||
Glob(pattern string) ([]string, error)
|
||||
// ReadFile returns the contents of the file at the given path.
|
||||
ReadFile(path string) ([]byte, error)
|
||||
|
||||
@@ -1,218 +0,0 @@
|
||||
// Copyright 2019 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package filesys
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var _ FileSystem = &fsInMemory{}
|
||||
|
||||
// fsInMemory implements FileSystem using a in-memory filesystem
|
||||
// primarily for use in tests.
|
||||
type fsInMemory struct {
|
||||
m map[string]*fileInMemory
|
||||
}
|
||||
|
||||
// MakeFsInMemory returns an instance of fsInMemory with no files in it.
|
||||
func MakeFsInMemory() FileSystem {
|
||||
result := &fsInMemory{m: map[string]*fileInMemory{}}
|
||||
result.Mkdir(Separator)
|
||||
return result
|
||||
}
|
||||
|
||||
// Create assures a fake file appears in the in-memory file system.
|
||||
func (fs *fsInMemory) Create(name string) (File, error) {
|
||||
f := &fileInMemory{}
|
||||
f.open = true
|
||||
fs.m[name] = f
|
||||
return fs.m[name], nil
|
||||
}
|
||||
|
||||
// Mkdir assures a fake directory appears in the in-memory file system.
|
||||
func (fs *fsInMemory) Mkdir(name string) error {
|
||||
fs.m[name] = makeDir(name)
|
||||
return nil
|
||||
}
|
||||
|
||||
// MkdirAll delegates to Mkdir
|
||||
func (fs *fsInMemory) MkdirAll(name string) error {
|
||||
return fs.Mkdir(name)
|
||||
}
|
||||
|
||||
// RemoveAll presumably does rm -r on a path.
|
||||
// There's no error.
|
||||
func (fs *fsInMemory) RemoveAll(name string) error {
|
||||
var toRemove []string
|
||||
for k := range fs.m {
|
||||
if strings.HasPrefix(k, name) {
|
||||
toRemove = append(toRemove, k)
|
||||
}
|
||||
}
|
||||
for _, k := range toRemove {
|
||||
delete(fs.m, k)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Open returns a fake file in the open state.
|
||||
func (fs *fsInMemory) Open(name string) (File, error) {
|
||||
if _, found := fs.m[name]; !found {
|
||||
return nil, fmt.Errorf("file %q cannot be opened", name)
|
||||
}
|
||||
return fs.m[name], nil
|
||||
}
|
||||
|
||||
// CleanedAbs cannot fail.
|
||||
func (fs *fsInMemory) CleanedAbs(path string) (ConfirmedDir, string, error) {
|
||||
if fs.IsDir(path) {
|
||||
return ConfirmedDir(path), "", nil
|
||||
}
|
||||
d := filepath.Dir(path)
|
||||
if d == path {
|
||||
return ConfirmedDir(d), "", nil
|
||||
}
|
||||
return ConfirmedDir(d), filepath.Base(path), nil
|
||||
}
|
||||
|
||||
// Exists returns true if file is known.
|
||||
func (fs *fsInMemory) Exists(name string) bool {
|
||||
_, found := fs.m[name]
|
||||
return found
|
||||
}
|
||||
|
||||
// Glob returns the list of matching files
|
||||
func (fs *fsInMemory) Glob(pattern string) ([]string, error) {
|
||||
var result []string
|
||||
for p := range fs.m {
|
||||
if fs.pathMatch(p, pattern) {
|
||||
result = append(result, p)
|
||||
}
|
||||
}
|
||||
sort.Strings(result)
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// IsDir returns true if the file exists and is a directory.
|
||||
func (fs *fsInMemory) IsDir(name string) bool {
|
||||
f, found := fs.m[name]
|
||||
if found && f.dir {
|
||||
return true
|
||||
}
|
||||
if !strings.HasSuffix(name, Separator) {
|
||||
name = name + Separator
|
||||
}
|
||||
for k := range fs.m {
|
||||
if strings.HasPrefix(k, name) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// ReadFile always returns an empty bytes and error depending on content of m.
|
||||
func (fs *fsInMemory) ReadFile(name string) ([]byte, error) {
|
||||
if ff, found := fs.m[name]; found {
|
||||
return ff.content, nil
|
||||
}
|
||||
return nil, fmt.Errorf("'%s' doesn't exist", name)
|
||||
}
|
||||
|
||||
// WriteFile always succeeds and does nothing.
|
||||
func (fs *fsInMemory) WriteFile(name string, c []byte) error {
|
||||
ff := &fileInMemory{}
|
||||
ff.Write(c)
|
||||
fs.m[name] = ff
|
||||
return nil
|
||||
}
|
||||
|
||||
// Walk implements filepath.Walk using the fake filesystem.
|
||||
func (fs *fsInMemory) Walk(path string, walkFn filepath.WalkFunc) error {
|
||||
info, err := fs.lstat(path)
|
||||
if err != nil {
|
||||
err = walkFn(path, info, err)
|
||||
} else {
|
||||
err = fs.walk(path, info, walkFn)
|
||||
}
|
||||
if err == filepath.SkipDir {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (fs *fsInMemory) pathMatch(path, pattern string) bool {
|
||||
match, _ := filepath.Match(pattern, path)
|
||||
return match
|
||||
}
|
||||
|
||||
func (fs *fsInMemory) lstat(path string) (*fileInfo, error) {
|
||||
f, found := fs.m[path]
|
||||
if !found {
|
||||
return nil, os.ErrNotExist
|
||||
}
|
||||
return &fileInfo{f}, nil
|
||||
}
|
||||
|
||||
func (fs *fsInMemory) join(elem ...string) string {
|
||||
for i, e := range elem {
|
||||
if e != "" {
|
||||
return strings.Replace(
|
||||
strings.Join(elem[i:], Separator), doubleSep, Separator, -1)
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (fs *fsInMemory) readDirNames(path string) []string {
|
||||
var names []string
|
||||
if !strings.HasSuffix(path, Separator) {
|
||||
path += Separator
|
||||
}
|
||||
pathSegments := strings.Count(path, Separator)
|
||||
for name := range fs.m {
|
||||
if name == path {
|
||||
continue
|
||||
}
|
||||
if strings.Count(name, Separator) > pathSegments {
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(name, path) {
|
||||
names = append(names, filepath.Base(name))
|
||||
}
|
||||
}
|
||||
sort.Strings(names)
|
||||
return names
|
||||
}
|
||||
|
||||
func (fs *fsInMemory) walk(path string, info os.FileInfo, walkFn filepath.WalkFunc) error {
|
||||
if !info.IsDir() {
|
||||
return walkFn(path, info, nil)
|
||||
}
|
||||
|
||||
names := fs.readDirNames(path)
|
||||
if err := walkFn(path, info, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, name := range names {
|
||||
filename := fs.join(path, name)
|
||||
fileInfo, err := fs.lstat(filename)
|
||||
if err != nil {
|
||||
if err := walkFn(filename, fileInfo, os.ErrNotExist); err != nil && err != filepath.SkipDir {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
err = fs.walk(filename, fileInfo, walkFn)
|
||||
if err != nil {
|
||||
if !fileInfo.IsDir() || err != filepath.SkipDir {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,149 +0,0 @@
|
||||
// Copyright 2019 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package filesys_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
. "sigs.k8s.io/kustomize/api/filesys"
|
||||
)
|
||||
|
||||
func TestOldExists(t *testing.T) {
|
||||
fSys := MakeFsInMemory()
|
||||
if fSys.Exists("foo") {
|
||||
t.Fatalf("expected no foo")
|
||||
}
|
||||
fSys.Mkdir("/")
|
||||
if !fSys.IsDir("/") {
|
||||
t.Fatalf("expected dir at /")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsDir(t *testing.T) {
|
||||
fSys := MakeFsInMemory()
|
||||
expectedName := "my-dir"
|
||||
err := fSys.Mkdir(expectedName)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
shouldExist(t, fSys, expectedName)
|
||||
if !fSys.IsDir(expectedName) {
|
||||
t.Fatalf(expectedName + " should be a dir")
|
||||
}
|
||||
}
|
||||
|
||||
func shouldExist(t *testing.T, fSys FileSystem, name string) {
|
||||
if !fSys.Exists(name) {
|
||||
t.Fatalf(name + " should exist")
|
||||
}
|
||||
}
|
||||
|
||||
func shouldNotExist(t *testing.T, fSys FileSystem, name string) {
|
||||
if fSys.Exists(name) {
|
||||
t.Fatalf(name + " should not exist")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoveAll(t *testing.T) {
|
||||
fSys := MakeFsInMemory()
|
||||
fSys.WriteFile("/foo/project/file.yaml", []byte("Unused"))
|
||||
fSys.WriteFile("/foo/project/subdir/file.yaml", []byte("Unused"))
|
||||
fSys.WriteFile("/foo/apple/subdir/file.yaml", []byte("Unused"))
|
||||
shouldExist(t, fSys, "/foo/project/file.yaml")
|
||||
shouldExist(t, fSys, "/foo/project/subdir/file.yaml")
|
||||
shouldExist(t, fSys, "/foo/apple/subdir/file.yaml")
|
||||
err := fSys.RemoveAll("/foo/project")
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %v", err)
|
||||
}
|
||||
shouldNotExist(t, fSys, "/foo/project/file.yaml")
|
||||
shouldNotExist(t, fSys, "/foo/project/subdir/file.yaml")
|
||||
shouldExist(t, fSys, "/foo/apple/subdir/file.yaml")
|
||||
}
|
||||
|
||||
func TestIsDirDeeper(t *testing.T) {
|
||||
fSys := MakeFsInMemory()
|
||||
fSys.WriteFile("/foo/project/file.yaml", []byte("Unused"))
|
||||
fSys.WriteFile("/foo/project/subdir/file.yaml", []byte("Unused"))
|
||||
if !fSys.IsDir("/") {
|
||||
t.Fatalf("/ should be a dir")
|
||||
}
|
||||
if !fSys.IsDir("/foo") {
|
||||
t.Fatalf("/foo should be a dir")
|
||||
}
|
||||
if !fSys.IsDir("/foo/project") {
|
||||
t.Fatalf("/foo/project should be a dir")
|
||||
}
|
||||
if fSys.IsDir("/fo") {
|
||||
t.Fatalf("/fo should not be a dir")
|
||||
}
|
||||
if fSys.IsDir("/x") {
|
||||
t.Fatalf("/x should not be a dir")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreate(t *testing.T) {
|
||||
fSys := MakeFsInMemory()
|
||||
f, err := fSys.Create("foo")
|
||||
if f == nil {
|
||||
t.Fatalf("expected file")
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error")
|
||||
}
|
||||
shouldExist(t, fSys, "foo")
|
||||
}
|
||||
|
||||
func TestReadFile(t *testing.T) {
|
||||
fSys := MakeFsInMemory()
|
||||
f, err := fSys.Create("foo")
|
||||
if f == nil {
|
||||
t.Fatalf("expected file")
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error")
|
||||
}
|
||||
content, err := fSys.ReadFile("foo")
|
||||
if len(content) != 0 {
|
||||
t.Fatalf("expected no content")
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteFile(t *testing.T) {
|
||||
fSys := MakeFsInMemory()
|
||||
c := []byte("heybuddy")
|
||||
err := fSys.WriteFile("foo", c)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error")
|
||||
}
|
||||
content, err := fSys.ReadFile("foo")
|
||||
if err != nil {
|
||||
t.Fatalf("expected read to work: %v", err)
|
||||
}
|
||||
if !bytes.Equal(c, content) {
|
||||
t.Fatalf("incorrect content: %v", content)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOldGlob(t *testing.T) {
|
||||
fSys := MakeFsInMemory()
|
||||
fSys.Create("dir/foo")
|
||||
fSys.Create("dir/bar")
|
||||
files, err := fSys.Glob("dir/*")
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error")
|
||||
}
|
||||
expected := []string{
|
||||
"dir/bar",
|
||||
"dir/foo",
|
||||
}
|
||||
if !reflect.DeepEqual(files, expected) {
|
||||
t.Fatalf("incorrect files found by glob: %v", files)
|
||||
}
|
||||
}
|
||||
557
api/filesys/fsnode.go
Normal file
557
api/filesys/fsnode.go
Normal file
@@ -0,0 +1,557 @@
|
||||
// Copyright 2019 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package filesys
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
var _ File = &fsNode{}
|
||||
var _ FileSystem = &fsNode{}
|
||||
|
||||
// fsNode is either a file or a directory.
|
||||
type fsNode struct {
|
||||
// What node owns me?
|
||||
parent *fsNode
|
||||
|
||||
// Value to return as the Name() when the
|
||||
// parent is nil.
|
||||
nilParentName string
|
||||
|
||||
// A directory mapping names to nodes.
|
||||
// If dir is nil, then self node is a file.
|
||||
// If dir is non-nil, then self node is a directory,
|
||||
// albeit possibly an empty directory.
|
||||
dir map[string]*fsNode
|
||||
|
||||
// if this node is a file, this is the content.
|
||||
content []byte
|
||||
|
||||
// if this node is a file, this tracks whether or
|
||||
// not it is "open".
|
||||
open bool
|
||||
}
|
||||
|
||||
// MakeEmptyDirInMemory returns an empty directory.
|
||||
// The paths of nodes in this object will never
|
||||
// report a leading Separator, meaning they
|
||||
// aren't "absolute" in the sense defined by
|
||||
// https://golang.org/pkg/path/filepath/#IsAbs.
|
||||
func MakeEmptyDirInMemory() *fsNode {
|
||||
return &fsNode{
|
||||
dir: make(map[string]*fsNode),
|
||||
}
|
||||
}
|
||||
|
||||
// MakeFsInMemory returns an empty 'file system'.
|
||||
// The paths of nodes in this object will always
|
||||
// report a leading Separator, meaning they
|
||||
// are "absolute" in the sense defined by
|
||||
// https://golang.org/pkg/path/filepath/#IsAbs.
|
||||
// This is a relevant difference when using Walk,
|
||||
// Glob, Match, etc.
|
||||
func MakeFsInMemory() FileSystem {
|
||||
return &fsNode{
|
||||
nilParentName: Separator,
|
||||
dir: make(map[string]*fsNode),
|
||||
}
|
||||
}
|
||||
|
||||
// Name returns the name of the node.
|
||||
func (n *fsNode) Name() string {
|
||||
if n.parent == nil {
|
||||
// Unable to lookup name in parent.
|
||||
return n.nilParentName
|
||||
}
|
||||
if !n.parent.isNodeADir() {
|
||||
log.Fatal("parent not a dir")
|
||||
}
|
||||
for key, value := range n.parent.dir {
|
||||
if value == n {
|
||||
return key
|
||||
}
|
||||
}
|
||||
log.Fatal("unable to find fsNode name")
|
||||
return ""
|
||||
}
|
||||
|
||||
// Path returns the full path to the node.
|
||||
func (n *fsNode) Path() string {
|
||||
if n.parent == nil {
|
||||
return n.nilParentName
|
||||
}
|
||||
if !n.parent.isNodeADir() {
|
||||
log.Fatal("parent not a dir, structural error")
|
||||
}
|
||||
return filepath.Join(n.parent.Path(), n.Name())
|
||||
}
|
||||
|
||||
// mySplit trims trailing separators from the directory
|
||||
// result of filepath.Split.
|
||||
func mySplit(s string) (string, string) {
|
||||
dName, fName := filepath.Split(s)
|
||||
return StripTrailingSeps(dName), fName
|
||||
}
|
||||
|
||||
func (n *fsNode) addFile(name string, c []byte) (result *fsNode, err error) {
|
||||
parent := n
|
||||
dName, fileName := mySplit(name)
|
||||
if dName != "" {
|
||||
parent, err = parent.addDir(dName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if !isLegalFileNameForCreation(fileName) {
|
||||
return nil, fmt.Errorf(
|
||||
"illegal name '%s' in file creation", fileName)
|
||||
}
|
||||
result, ok := parent.dir[fileName]
|
||||
if ok {
|
||||
// File already exists; overwrite it.
|
||||
result.content = c
|
||||
return result, nil
|
||||
}
|
||||
result = &fsNode{
|
||||
content: c,
|
||||
parent: parent,
|
||||
}
|
||||
parent.dir[fileName] = result
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Create implements FileSystem.
|
||||
// Create makes an empty file.
|
||||
func (n *fsNode) Create(path string) (result File, err error) {
|
||||
return n.AddFile(path, []byte{})
|
||||
}
|
||||
|
||||
// WriteFile implements FileSystem.
|
||||
func (n *fsNode) WriteFile(path string, d []byte) error {
|
||||
_, err := n.AddFile(path, d)
|
||||
return err
|
||||
}
|
||||
|
||||
// AddFile adds a file and any necessary containing
|
||||
// directories to the node.
|
||||
func (n *fsNode) AddFile(
|
||||
name string, c []byte) (result *fsNode, err error) {
|
||||
if n.dir == nil {
|
||||
return nil, fmt.Errorf(
|
||||
"cannot add a file to a non-directory '%s'", n.Name())
|
||||
}
|
||||
return n.addFile(cleanQueryPath(name), c)
|
||||
}
|
||||
|
||||
func (n *fsNode) addDir(path string) (result *fsNode, err error) {
|
||||
parent := n
|
||||
dName, subDirName := mySplit(path)
|
||||
if dName != "" {
|
||||
parent, err = n.addDir(dName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
switch subDirName {
|
||||
case "", SelfDir:
|
||||
return n, nil
|
||||
case ParentDir:
|
||||
if n.parent == nil {
|
||||
return nil, fmt.Errorf(
|
||||
"cannot add a directory above '%s'", n.Path())
|
||||
}
|
||||
return n.parent, nil
|
||||
default:
|
||||
if !isLegalFileNameForCreation(subDirName) {
|
||||
return nil, fmt.Errorf(
|
||||
"illegal name '%s' in directory creation", subDirName)
|
||||
}
|
||||
result, ok := parent.dir[subDirName]
|
||||
if ok {
|
||||
if result.isNodeADir() {
|
||||
// it's already there.
|
||||
return result, nil
|
||||
}
|
||||
return nil, fmt.Errorf(
|
||||
"cannot make dir '%s'; a file of that name already exists in '%s'",
|
||||
subDirName, parent.Name())
|
||||
}
|
||||
result = &fsNode{
|
||||
dir: make(map[string]*fsNode),
|
||||
parent: parent,
|
||||
}
|
||||
parent.dir[subDirName] = result
|
||||
return result, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Mkdir implements FileSystem.
|
||||
// Mkdir creates a directory.
|
||||
func (n *fsNode) Mkdir(path string) error {
|
||||
_, err := n.AddDir(path)
|
||||
return err
|
||||
}
|
||||
|
||||
// MkdirAll implements FileSystem.
|
||||
// MkdirAll creates a directory.
|
||||
func (n *fsNode) MkdirAll(path string) error {
|
||||
_, err := n.AddDir(path)
|
||||
return err
|
||||
}
|
||||
|
||||
// AddDir adds a directory to the node, not complaining
|
||||
// if it is already there.
|
||||
func (n *fsNode) AddDir(path string) (result *fsNode, err error) {
|
||||
if n.dir == nil {
|
||||
return nil, fmt.Errorf(
|
||||
"cannot add a directory to file node '%s'", n.Name())
|
||||
}
|
||||
return n.addDir(cleanQueryPath(path))
|
||||
}
|
||||
|
||||
// CleanedAbs implements FileSystem.
|
||||
func (n *fsNode) CleanedAbs(path string) (ConfirmedDir, string, error) {
|
||||
node, err := n.Find(path)
|
||||
if err != nil {
|
||||
return "", "", errors.Wrap(err, "unable to clean")
|
||||
}
|
||||
if node == nil {
|
||||
return "", "", fmt.Errorf("'%s' doesn't exist", path)
|
||||
}
|
||||
if node.isNodeADir() {
|
||||
return ConfirmedDir(node.Path()), "", nil
|
||||
}
|
||||
return ConfirmedDir(node.parent.Path()), node.Name(), nil
|
||||
}
|
||||
|
||||
// Exists implements FileSystem.
|
||||
// Exists returns true if the path exists.
|
||||
func (n *fsNode) Exists(path string) bool {
|
||||
if !n.isNodeADir() {
|
||||
return n.Name() == path
|
||||
}
|
||||
result, err := n.Find(path)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return result != nil
|
||||
}
|
||||
|
||||
func cleanQueryPath(path string) string {
|
||||
// Always ignore leading separator?
|
||||
// Remember that filepath.Clean returns "." if
|
||||
// given an empty string argument.
|
||||
return filepath.Clean(StripLeadingSeps(path))
|
||||
}
|
||||
|
||||
// Find finds the given node, else nil if not found.
|
||||
// Return error on structural/argument errors.
|
||||
func (n *fsNode) Find(path string) (*fsNode, error) {
|
||||
if !n.isNodeADir() {
|
||||
return nil, fmt.Errorf("can only find inside a dir")
|
||||
}
|
||||
if path == "" {
|
||||
// Special case; check *before* cleaning and *before*
|
||||
// comparison to nilParentName.
|
||||
return nil, nil
|
||||
}
|
||||
if (n.parent == nil && path == n.nilParentName) || path == SelfDir {
|
||||
// Special case
|
||||
return n, nil
|
||||
}
|
||||
return n.findIt(cleanQueryPath(path))
|
||||
}
|
||||
|
||||
func (n *fsNode) findIt(path string) (result *fsNode, err error) {
|
||||
parent := n
|
||||
dName, item := mySplit(path)
|
||||
if dName != "" {
|
||||
parent, err = n.findIt(dName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if parent == nil {
|
||||
// all done, target doesn't exist.
|
||||
return nil, nil
|
||||
}
|
||||
}
|
||||
if !parent.isNodeADir() {
|
||||
return nil, fmt.Errorf("'%s' is not a directory", parent.Path())
|
||||
}
|
||||
return parent.dir[item], nil
|
||||
}
|
||||
|
||||
// RemoveAll implements FileSystem.
|
||||
// RemoveAll removes an item and everything it contains.
|
||||
func (n *fsNode) RemoveAll(path string) error {
|
||||
result, err := n.Find(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if result == nil {
|
||||
return fmt.Errorf("cannot find '%s' to remove it", path)
|
||||
}
|
||||
return result.Remove()
|
||||
}
|
||||
|
||||
// Remove drop the node, and everything it contains, from its parent.
|
||||
func (n *fsNode) Remove() error {
|
||||
if n.parent == nil {
|
||||
return fmt.Errorf("cannot remove a root node")
|
||||
}
|
||||
if !n.parent.isNodeADir() {
|
||||
log.Fatal("parent not a dir")
|
||||
}
|
||||
for key, value := range n.parent.dir {
|
||||
if value == n {
|
||||
delete(n.parent.dir, key)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
log.Fatal("unable to find self in parent")
|
||||
return nil
|
||||
}
|
||||
|
||||
// isNodeADir returns true if the node is a directory.
|
||||
// Cannot collide with the poorly named "IsDir".
|
||||
func (n *fsNode) isNodeADir() bool {
|
||||
return n.dir != nil
|
||||
}
|
||||
|
||||
// IsDir implements FileSystem.
|
||||
// IsDir returns true if the argument resolves
|
||||
// to a directory rooted at the node.
|
||||
func (n *fsNode) IsDir(path string) bool {
|
||||
result, err := n.Find(path)
|
||||
if err != nil || result == nil {
|
||||
return false
|
||||
}
|
||||
return result.isNodeADir()
|
||||
}
|
||||
|
||||
// Size returns the size of the node.
|
||||
func (n *fsNode) Size() int64 {
|
||||
if n.isNodeADir() {
|
||||
return int64(len(n.dir))
|
||||
}
|
||||
return int64(len(n.content))
|
||||
}
|
||||
|
||||
// Open implements FileSystem.
|
||||
// Open opens the node for reading (just marks it).
|
||||
func (n *fsNode) Open(path string) (File, error) {
|
||||
result, err := n.Find(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if result == nil {
|
||||
return nil, fmt.Errorf("cannot find '%s' to open it", path)
|
||||
}
|
||||
result.open = true
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Close marks the node closed.
|
||||
func (n *fsNode) Close() error {
|
||||
n.open = false
|
||||
return nil
|
||||
}
|
||||
|
||||
// ReadFile implements FileSystem.
|
||||
func (n *fsNode) ReadFile(path string) (c []byte, err error) {
|
||||
result, err := n.Find(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if result == nil {
|
||||
return nil, fmt.Errorf("cannot find '%s' to read it", path)
|
||||
}
|
||||
c = make([]byte, len(result.content))
|
||||
_, err = result.Read(c)
|
||||
return c, err
|
||||
}
|
||||
|
||||
// Read returns the content of the file node.
|
||||
func (n *fsNode) Read(d []byte) (c int, err error) {
|
||||
if n.isNodeADir() {
|
||||
return 0, fmt.Errorf(
|
||||
"cannot read content from non-file '%s'", n.Path())
|
||||
}
|
||||
return copy(d, n.content), nil
|
||||
}
|
||||
|
||||
// Write saves the contents of the argument to the file node.
|
||||
func (n *fsNode) Write(p []byte) (c int, err error) {
|
||||
if n.isNodeADir() {
|
||||
return 0, fmt.Errorf(
|
||||
"cannot write content to non-file '%s'", n.Path())
|
||||
}
|
||||
n.content = make([]byte, len(p))
|
||||
return copy(n.content, p), nil
|
||||
}
|
||||
|
||||
// ContentMatches returns true if v matches fake file's content.
|
||||
func (n *fsNode) ContentMatches(v []byte) bool {
|
||||
return bytes.Equal(v, n.content)
|
||||
}
|
||||
|
||||
// GetContent the content of a fake file.
|
||||
func (n *fsNode) GetContent() []byte {
|
||||
return n.content
|
||||
}
|
||||
|
||||
// Stat returns an instance of FileInfo.
|
||||
func (n *fsNode) Stat() (os.FileInfo, error) {
|
||||
return fileInfo{node: n}, nil
|
||||
}
|
||||
|
||||
// Walk implements FileSystem.
|
||||
func (n *fsNode) Walk(path string, walkFn filepath.WalkFunc) error {
|
||||
result, err := n.Find(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if result == nil {
|
||||
return fmt.Errorf("cannot find '%s' to walk it", path)
|
||||
}
|
||||
return result.WalkMe(walkFn)
|
||||
}
|
||||
|
||||
// Walk runs the given walkFn on each node.
|
||||
func (n *fsNode) WalkMe(walkFn filepath.WalkFunc) error {
|
||||
fi, err := n.Stat()
|
||||
// always visit self first
|
||||
err = walkFn(n.Path(), fi, err)
|
||||
if !n.isNodeADir() {
|
||||
// it's a file, so nothing more to do
|
||||
return err
|
||||
}
|
||||
// process self as a directory
|
||||
if err == filepath.SkipDir {
|
||||
return nil
|
||||
}
|
||||
// Walk is supposed to visit in lexical order.
|
||||
for _, k := range n.sortedDirEntries() {
|
||||
if err := n.dir[k].WalkMe(walkFn); err != nil {
|
||||
if err == filepath.SkipDir {
|
||||
// stop processing this directory
|
||||
break
|
||||
}
|
||||
// bail out completely
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *fsNode) sortedDirEntries() []string {
|
||||
keys := make([]string, len(n.dir))
|
||||
i := 0
|
||||
for k := range n.dir {
|
||||
keys[i] = k
|
||||
i++
|
||||
}
|
||||
sort.Strings(keys)
|
||||
return keys
|
||||
}
|
||||
|
||||
// FileCount returns a count of files.
|
||||
// Directories, empty or otherwise, not counted.
|
||||
func (n *fsNode) FileCount() int {
|
||||
count := 0
|
||||
n.WalkMe(func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !info.IsDir() {
|
||||
count++
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return count
|
||||
}
|
||||
|
||||
func (n *fsNode) DebugPrint() {
|
||||
n.WalkMe(func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
fmt.Printf("err '%v' at path %q\n", err, path)
|
||||
return nil
|
||||
}
|
||||
if info.IsDir() {
|
||||
if info.Size() == 0 {
|
||||
fmt.Println("empty dir: " + path)
|
||||
}
|
||||
} else {
|
||||
fmt.Println(" file: " + path)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
var legalFileNamePattern = regexp.MustCompile("^[a-zA-Z0-9-_.]+$")
|
||||
|
||||
// This rules enforced here should be simpler and tighter
|
||||
// than what's allowed on a real OS.
|
||||
// Should be fine for testing or in-memory purposes.
|
||||
func isLegalFileNameForCreation(n string) bool {
|
||||
if n == "" || n == SelfDir || !legalFileNamePattern.MatchString(n) {
|
||||
return false
|
||||
}
|
||||
return !strings.Contains(n, ParentDir)
|
||||
}
|
||||
|
||||
// RegExpGlob returns a list of file paths matching the regexp.
|
||||
// Excludes directories.
|
||||
func (n *fsNode) RegExpGlob(pattern string) ([]string, error) {
|
||||
var result []string
|
||||
var expression = regexp.MustCompile(pattern)
|
||||
n.WalkMe(func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !info.IsDir() {
|
||||
if expression.MatchString(path) {
|
||||
result = append(result, path)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
sort.Strings(result)
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Glob implements FileSystem.
|
||||
// Glob returns the list of file paths matching
|
||||
// per filepath.Match semantics, i.e. unlike RegExpGlob,
|
||||
// Match("foo/a*") will not match sub-sub directories of foo.
|
||||
// This is how /bin/ls behaves.
|
||||
func (n *fsNode) Glob(pattern string) ([]string, error) {
|
||||
var result []string
|
||||
n.WalkMe(func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !info.IsDir() {
|
||||
match, err := filepath.Match(pattern, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if match {
|
||||
result = append(result, path)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
sort.Strings(result)
|
||||
return result, nil
|
||||
}
|
||||
788
api/filesys/fsnode_test.go
Normal file
788
api/filesys/fsnode_test.go
Normal file
@@ -0,0 +1,788 @@
|
||||
// Copyright 2019 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package filesys
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
const content = `
|
||||
Lorem ipsum dolor sit amet,
|
||||
consectetur adipiscing elit,
|
||||
sed do eiusmod tempor incididunt
|
||||
ut labore et dolore magna aliqua.
|
||||
`
|
||||
const shortContent = "hi"
|
||||
|
||||
var topCases = []pathCase{
|
||||
{
|
||||
what: "dotdot",
|
||||
arg: ParentDir,
|
||||
errStr: "illegal name '..' in file creation",
|
||||
},
|
||||
{
|
||||
what: "empty",
|
||||
arg: "",
|
||||
name: "",
|
||||
errStr: "illegal name '.' in file creation",
|
||||
},
|
||||
{
|
||||
what: "simple",
|
||||
arg: "bob",
|
||||
name: "bob",
|
||||
path: "bob",
|
||||
},
|
||||
{
|
||||
what: "longer",
|
||||
arg: filepath.Join("longer", "bob"),
|
||||
name: "bob",
|
||||
path: filepath.Join("longer", "bob"),
|
||||
},
|
||||
{
|
||||
what: "longer yet",
|
||||
arg: filepath.Join("longer", "foo", "bar", "beans", "bob"),
|
||||
name: "bob",
|
||||
path: filepath.Join("longer", "foo", "bar", "beans", "bob"),
|
||||
},
|
||||
{
|
||||
what: "tricky",
|
||||
arg: filepath.Join("bob", ParentDir, "sally"),
|
||||
name: "sally",
|
||||
path: "sally",
|
||||
},
|
||||
{
|
||||
what: "trickier",
|
||||
arg: filepath.Join("bob", "sally", ParentDir, ParentDir, "jean"),
|
||||
name: "jean",
|
||||
path: "jean",
|
||||
},
|
||||
}
|
||||
|
||||
func TestMakeEmptyDirInMemory(t *testing.T) {
|
||||
n := MakeEmptyDirInMemory()
|
||||
if !n.isNodeADir() {
|
||||
t.Fatalf("not a directory")
|
||||
}
|
||||
if n.Size() != 0 {
|
||||
t.Fatalf("unexpected size %d", n.Size())
|
||||
}
|
||||
if n.Name() != "" {
|
||||
t.Fatalf("unexpected name '%s'", n.Name())
|
||||
}
|
||||
if n.Path() != "" {
|
||||
t.Fatalf("unexpected path '%s'", n.Path())
|
||||
}
|
||||
runBasicOperations(
|
||||
t, "MakeEmptyDirInMemory", false, topCases, n)
|
||||
}
|
||||
|
||||
func TestMakeFsInMemory(t *testing.T) {
|
||||
runBasicOperations(
|
||||
t, "MakeFsInMemory", true, topCases, MakeFsInMemory())
|
||||
}
|
||||
|
||||
//nolint:gocyclo
|
||||
func runBasicOperations(
|
||||
t *testing.T, tName string, isFSysRooted bool,
|
||||
cases []pathCase, fSys FileSystem) {
|
||||
buff := make([]byte, 500)
|
||||
for _, c := range cases {
|
||||
err := fSys.WriteFile(c.arg, []byte(content))
|
||||
if c.errStr != "" {
|
||||
if err == nil {
|
||||
t.Fatalf("%s; expected error writing to '%s'!", c.what, c.arg)
|
||||
}
|
||||
if !strings.Contains(err.Error(), c.errStr) {
|
||||
t.Fatalf("%s; expected err containing '%s', got '%v'",
|
||||
c.what, c.errStr, err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("%s; unexpected error: %v", c.what, err)
|
||||
}
|
||||
if !fSys.Exists(c.path) {
|
||||
t.Fatalf("%s; expect existence of '%s'", c.what, c.path)
|
||||
}
|
||||
stuff, err := fSys.ReadFile(c.path)
|
||||
if err != nil {
|
||||
t.Fatalf("%s; unexpected error: %v", c.what, err)
|
||||
}
|
||||
if string(stuff) != content {
|
||||
t.Fatalf("%s; unexpected content '%s'", c.what, stuff)
|
||||
}
|
||||
f, err := fSys.Open(c.arg)
|
||||
if err != nil {
|
||||
t.Fatalf("%s; unexpected error: %v", c.what, err)
|
||||
}
|
||||
fi, err := f.Stat()
|
||||
if err != nil {
|
||||
t.Fatalf("%s; unexpected error: %v", c.what, err)
|
||||
}
|
||||
if fi.Name() != c.name {
|
||||
t.Fatalf("%s; expected name '%s', got '%s'", c.what, c.name, fi.Name())
|
||||
}
|
||||
count, err := f.Read(buff)
|
||||
if err != nil {
|
||||
t.Fatalf("%s; unexpected error: %v", c.what, err)
|
||||
}
|
||||
if string(buff[:count]) != content {
|
||||
t.Fatalf("%s; unexpected buff '%s'", c.what, buff)
|
||||
}
|
||||
count, err = f.Write([]byte(shortContent))
|
||||
if err != nil {
|
||||
t.Fatalf("%s; unexpected error: %v", c.what, err)
|
||||
}
|
||||
if count != len(shortContent) {
|
||||
t.Fatalf("%s; unexpected count: %d", c.what, len(shortContent))
|
||||
}
|
||||
stuff, err = fSys.ReadFile(c.path)
|
||||
if err != nil {
|
||||
t.Fatalf("%s; unexpected error: %v", c.what, err)
|
||||
}
|
||||
if string(stuff) != shortContent {
|
||||
t.Fatalf("%s; unexpected content '%s'", c.what, stuff)
|
||||
}
|
||||
}
|
||||
|
||||
var actualPaths []string
|
||||
var err error
|
||||
prefix := ""
|
||||
{
|
||||
root := SelfDir
|
||||
if isFSysRooted {
|
||||
root = Separator
|
||||
prefix = Separator
|
||||
}
|
||||
err = fSys.Walk(root, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
fmt.Printf("err '%v' at path %q\n", err, path)
|
||||
return nil
|
||||
}
|
||||
if !info.IsDir() {
|
||||
actualPaths = append(actualPaths, path)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error %v", err)
|
||||
}
|
||||
var expectedPaths []string
|
||||
for _, c := range cases {
|
||||
if c.errStr == "" {
|
||||
expectedPaths = append(expectedPaths, prefix+c.path)
|
||||
}
|
||||
}
|
||||
sort.Strings(expectedPaths)
|
||||
assertEqualStringSlices(t, expectedPaths, actualPaths, tName)
|
||||
}
|
||||
|
||||
type pathCase struct {
|
||||
what string
|
||||
arg string
|
||||
name string
|
||||
path string
|
||||
errStr string
|
||||
}
|
||||
|
||||
func TestAddDir(t *testing.T) {
|
||||
cases := []pathCase{
|
||||
{
|
||||
what: "dotdot",
|
||||
arg: ParentDir,
|
||||
errStr: "cannot add a directory above ''",
|
||||
},
|
||||
{
|
||||
what: "empty",
|
||||
arg: "",
|
||||
name: "",
|
||||
path: "",
|
||||
},
|
||||
{
|
||||
what: "simple",
|
||||
arg: "bob",
|
||||
name: "bob",
|
||||
path: "bob",
|
||||
},
|
||||
{
|
||||
what: "longer",
|
||||
arg: filepath.Join("longer", "bob"),
|
||||
name: "bob",
|
||||
path: filepath.Join("longer", "bob"),
|
||||
},
|
||||
{
|
||||
what: "longer yet",
|
||||
arg: filepath.Join("longer", "foo", "bar", "beans", "bob"),
|
||||
name: "bob",
|
||||
path: filepath.Join("longer", "foo", "bar", "beans", "bob"),
|
||||
},
|
||||
{
|
||||
what: "tricky",
|
||||
arg: filepath.Join("bob", ParentDir, "sally"),
|
||||
name: "sally",
|
||||
path: "sally",
|
||||
},
|
||||
{
|
||||
what: "trickier",
|
||||
arg: filepath.Join("bob", "sally", ParentDir, ParentDir, "jean"),
|
||||
name: "jean",
|
||||
path: "jean",
|
||||
},
|
||||
}
|
||||
for _, c := range cases {
|
||||
n := MakeEmptyDirInMemory()
|
||||
f, err := n.AddDir(c.arg)
|
||||
if c.errStr != "" {
|
||||
if err == nil {
|
||||
t.Fatalf("%s; expected error!", c.what)
|
||||
}
|
||||
if !strings.Contains(err.Error(), c.errStr) {
|
||||
t.Fatalf(
|
||||
"%s; expected error with '%s', got '%v'",
|
||||
c.what, c.errStr, err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("%s; unexpected error: %v", c.what, err)
|
||||
}
|
||||
checkNode(t, c.what, f, c.name, 0, true, c.path)
|
||||
checkOsStat(t, c.what, f, f.Name(), 0, true)
|
||||
}
|
||||
}
|
||||
|
||||
var bagOfCases = []pathCase{
|
||||
{
|
||||
what: "empty",
|
||||
arg: "",
|
||||
errStr: "illegal name '.' in file creation",
|
||||
},
|
||||
{
|
||||
what: "simple",
|
||||
arg: "bob",
|
||||
name: "bob",
|
||||
path: "bob",
|
||||
},
|
||||
{
|
||||
what: "longer",
|
||||
arg: filepath.Join("longer", "bob"),
|
||||
name: "bob",
|
||||
path: filepath.Join("longer", "bob"),
|
||||
},
|
||||
{
|
||||
what: "longer",
|
||||
arg: filepath.Join("longer", "sally"),
|
||||
name: "sally",
|
||||
path: filepath.Join("longer", "sally"),
|
||||
},
|
||||
{
|
||||
what: "even longer",
|
||||
arg: filepath.Join("longer", "than", "the", "other", "bob"),
|
||||
name: "bob",
|
||||
path: filepath.Join("longer", "than", "the", "other", "bob"),
|
||||
},
|
||||
{
|
||||
what: "even longer",
|
||||
arg: filepath.Join("even", "much", "longer", "than", "the", "other", "bob"),
|
||||
name: "bob",
|
||||
path: filepath.Join("even", "much", "longer", "than", "the", "other", "bob"),
|
||||
},
|
||||
}
|
||||
|
||||
func TestAddFile(t *testing.T) {
|
||||
n := MakeEmptyDirInMemory()
|
||||
if n.FileCount() != 0 {
|
||||
t.Fatalf("expected no files, got %d", n.FileCount())
|
||||
}
|
||||
expectedFileCount := 0
|
||||
for _, c := range bagOfCases {
|
||||
f, err := n.AddFile(c.arg, []byte(content))
|
||||
if c.errStr != "" {
|
||||
if err == nil {
|
||||
t.Fatalf("%s; expected error!", c.what)
|
||||
}
|
||||
if !strings.Contains(err.Error(), c.errStr) {
|
||||
t.Fatalf("%s; expected err containing '%s', got '%v'",
|
||||
c.what, c.errStr, err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("%s; unexpected error %v", c.what, err)
|
||||
}
|
||||
checkNode(t, c.what, f, c.name, len(content), false, c.path)
|
||||
checkOsStat(t, c.what, f, f.Name(), len(content), false)
|
||||
|
||||
result, err := n.Find(c.arg)
|
||||
if err != nil {
|
||||
t.Fatalf("%s; unexpected find error %v", c.what, err)
|
||||
}
|
||||
if result != f {
|
||||
t.Fatalf("%s; unexpected find result %v", c.what, result)
|
||||
}
|
||||
|
||||
result, err = n.Find(filepath.Join("longer", "bogus"))
|
||||
if err != nil {
|
||||
t.Fatalf("%s; unexpected find error %v", c.what, err)
|
||||
}
|
||||
if result != nil {
|
||||
t.Fatalf("%s; unexpected find result %v", c.what, result)
|
||||
}
|
||||
|
||||
expectedFileCount++
|
||||
fc := n.FileCount()
|
||||
if fc != expectedFileCount {
|
||||
t.Fatalf("expected file count %d, got %d",
|
||||
expectedFileCount, fc)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func checkNode(
|
||||
t *testing.T, what string, f *fsNode, name string,
|
||||
size int, isDir bool, path string) {
|
||||
if f.isNodeADir() != isDir {
|
||||
t.Fatalf("%s; unexpected isNodeADir = %v", what, f.isNodeADir())
|
||||
}
|
||||
if f.Size() != int64(size) {
|
||||
t.Fatalf("%s; unexpected size %d", what, f.Size())
|
||||
}
|
||||
if name != f.Name() {
|
||||
t.Fatalf("%s; expected name '%s', got '%s'", what, name, f.Name())
|
||||
}
|
||||
if path != f.Path() {
|
||||
t.Fatalf("%s; expected path '%s', got '%s'", what, path, f.Path())
|
||||
}
|
||||
}
|
||||
|
||||
func checkOsStat(
|
||||
t *testing.T, what string, f File, name string,
|
||||
size int, isDir bool) {
|
||||
info, err := f.Stat()
|
||||
if err != nil {
|
||||
t.Fatalf("%s; unexpected stat error %v", what, err)
|
||||
}
|
||||
if info.IsDir() != isDir {
|
||||
t.Fatalf("%s; unexpected info.isNodeADir = %v", what, info.IsDir())
|
||||
}
|
||||
if info.Size() != int64(size) {
|
||||
t.Fatalf("%s; unexpected info.size %d", what, info.Size())
|
||||
}
|
||||
if info.Name() != name {
|
||||
t.Fatalf("%s; expected name '%s', got info.Name '%s'", what, name, info.Name())
|
||||
}
|
||||
}
|
||||
|
||||
var bunchOfFiles = []struct {
|
||||
path string
|
||||
addAsDir bool
|
||||
}{
|
||||
{
|
||||
path: filepath.Join("b", "e", "a", "c", "g"),
|
||||
},
|
||||
{
|
||||
path: filepath.Join("z", "r", "a", "b", "g"),
|
||||
},
|
||||
{
|
||||
path: filepath.Join("b", "q", "a", "c", "g"),
|
||||
},
|
||||
{
|
||||
path: filepath.Join("b", "a", "a", "m", "g"),
|
||||
addAsDir: true,
|
||||
},
|
||||
{
|
||||
path: filepath.Join("b", "w"),
|
||||
},
|
||||
{
|
||||
path: filepath.Join("b", "d", "a", "c", "m"),
|
||||
},
|
||||
{
|
||||
path: filepath.Join("b", "d", "z"),
|
||||
},
|
||||
{
|
||||
path: filepath.Join("b", "d", "y"),
|
||||
},
|
||||
{
|
||||
path: filepath.Join("b", "d", "ignore", "c", "n"),
|
||||
},
|
||||
{
|
||||
path: filepath.Join("b", "d", "x"),
|
||||
},
|
||||
{
|
||||
path: filepath.Join("b", "d", "ignore", "c", "o"),
|
||||
},
|
||||
{
|
||||
path: filepath.Join("b", "d", "ignore", "c", "m"),
|
||||
},
|
||||
{
|
||||
path: filepath.Join("b", "d", "a", "c", "i"),
|
||||
addAsDir: true,
|
||||
},
|
||||
{
|
||||
path: filepath.Join("x"),
|
||||
},
|
||||
{
|
||||
path: filepath.Join("y"),
|
||||
},
|
||||
{
|
||||
path: filepath.Join("b", "d", "a", "c", "i", "beans"),
|
||||
},
|
||||
{
|
||||
path: filepath.Join("b", "d", "a", "c", "r", "w"),
|
||||
addAsDir: true,
|
||||
},
|
||||
{
|
||||
path: filepath.Join("b", "d", "a", "c", "u"),
|
||||
},
|
||||
}
|
||||
|
||||
func makeLoadedFileTree(t *testing.T) *fsNode {
|
||||
n := MakeEmptyDirInMemory()
|
||||
var err error
|
||||
expectedFileCount := 0
|
||||
for _, item := range bunchOfFiles {
|
||||
if item.addAsDir {
|
||||
_, err = n.AddDir(item.path)
|
||||
} else {
|
||||
_, err = n.AddFile(item.path, []byte(content))
|
||||
expectedFileCount++
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error %v", err)
|
||||
}
|
||||
}
|
||||
fc := n.FileCount()
|
||||
if fc != expectedFileCount {
|
||||
t.Fatalf("expected file count %d, got %d",
|
||||
expectedFileCount, fc)
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
func TestWalkMe(t *testing.T) {
|
||||
n := makeLoadedFileTree(t)
|
||||
var actualPaths []string
|
||||
err := n.WalkMe(func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
fmt.Printf("err '%v' at path %q\n", err, path)
|
||||
return nil
|
||||
}
|
||||
if info.IsDir() {
|
||||
if info.Name() == "ignore" {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
} else {
|
||||
actualPaths = append(actualPaths, path)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error %v", err)
|
||||
}
|
||||
var expectedPaths []string
|
||||
for _, c := range bunchOfFiles {
|
||||
if !c.addAsDir && !strings.Contains(c.path, "ignore") {
|
||||
expectedPaths = append(expectedPaths, c.path)
|
||||
}
|
||||
}
|
||||
sort.Strings(expectedPaths)
|
||||
assertEqualStringSlices(t, expectedPaths, actualPaths, "testWalkMe")
|
||||
}
|
||||
|
||||
func TestRemove(t *testing.T) {
|
||||
n := makeLoadedFileTree(t)
|
||||
orgCount := n.FileCount()
|
||||
|
||||
// Remove the "ignore" directory and everything below it.
|
||||
path := filepath.Join("b", "d", "ignore")
|
||||
result, err := n.Find(path)
|
||||
if err != nil {
|
||||
t.Fatalf("%s; unexpected error %v", path, err)
|
||||
}
|
||||
if result == nil {
|
||||
t.Fatalf("%s; expected to find '%s'", path, path)
|
||||
}
|
||||
if !result.isNodeADir() {
|
||||
t.Fatalf("%s; expected to find a directory", path)
|
||||
}
|
||||
err = result.Remove()
|
||||
if err != nil {
|
||||
t.Fatalf("%s; unable to remove: %v", path, err)
|
||||
}
|
||||
result, err = n.Find(path)
|
||||
if err != nil {
|
||||
// Just because it's gone doesn't mean error.
|
||||
t.Fatalf("%s; unexpected error %v", path, err)
|
||||
}
|
||||
if result != nil {
|
||||
t.Fatalf("%s; should not have been able to find '%s'", path, path)
|
||||
}
|
||||
|
||||
// There were three files below "ignore".
|
||||
orgCount -= 3
|
||||
|
||||
// Now drop one more for a total of four dropped.
|
||||
result, _ = n.Find(filepath.Join("y"))
|
||||
err = result.Remove()
|
||||
if err != nil {
|
||||
t.Fatalf("%s; unable to remove: %v", path, err)
|
||||
}
|
||||
orgCount -= 1
|
||||
|
||||
fc := n.FileCount()
|
||||
if fc != orgCount {
|
||||
t.Fatalf("expected file count %d, got %d",
|
||||
orgCount, fc)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExists(t *testing.T) {
|
||||
n := makeLoadedFileTree(t)
|
||||
path := filepath.Join("b", "d", "a")
|
||||
if !n.Exists(path) {
|
||||
t.Fatalf("expected existence at %s", path)
|
||||
}
|
||||
if !n.IsDir(path) {
|
||||
t.Fatalf("expected directory at %s", path)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegExpGlob(t *testing.T) {
|
||||
n := makeLoadedFileTree(t)
|
||||
expected := []string{
|
||||
filepath.Join("b", "d", "a", "c", "i", "beans"),
|
||||
filepath.Join("b", "d", "a", "c", "m"),
|
||||
filepath.Join("b", "d", "a", "c", "u"),
|
||||
filepath.Join("b", "d", "ignore", "c", "m"),
|
||||
filepath.Join("b", "d", "ignore", "c", "n"),
|
||||
filepath.Join("b", "d", "ignore", "c", "o"),
|
||||
filepath.Join("b", "d", "x"),
|
||||
filepath.Join("b", "d", "y"),
|
||||
filepath.Join("b", "d", "z"),
|
||||
}
|
||||
paths, err := n.RegExpGlob("b/d/*")
|
||||
if err != nil {
|
||||
t.Fatalf("glob error: %v", err)
|
||||
}
|
||||
assertEqualStringSlices(t, expected, paths, "glob test")
|
||||
}
|
||||
|
||||
func TestGlob(t *testing.T) {
|
||||
n := makeLoadedFileTree(t)
|
||||
expected := []string{
|
||||
filepath.Join("b", "d", "x"),
|
||||
filepath.Join("b", "d", "y"),
|
||||
filepath.Join("b", "d", "z"),
|
||||
}
|
||||
paths, err := n.Glob("b/d/*")
|
||||
if err != nil {
|
||||
t.Fatalf("glob error: %v", err)
|
||||
}
|
||||
assertEqualStringSlices(t, expected, paths, "glob test")
|
||||
}
|
||||
|
||||
func assertEqualStringSlices(t *testing.T, expected, actual []string, message string) {
|
||||
if len(expected) != len(actual) {
|
||||
t.Fatalf(
|
||||
"%s; unequal sizes; len(expected)=%d, len(actual)=%d\n%+v\n%+v\n",
|
||||
message, len(expected), len(actual), expected, actual)
|
||||
}
|
||||
for i := range expected {
|
||||
if expected[i] != actual[i] {
|
||||
t.Fatalf(
|
||||
"%s; unequal entries; expected=%s, actual=%s",
|
||||
message, expected[i], actual[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFind(t *testing.T) {
|
||||
cases := []struct {
|
||||
what string
|
||||
arg string
|
||||
expectDir bool
|
||||
expectFile bool
|
||||
errStr string
|
||||
}{
|
||||
{
|
||||
what: "garbage",
|
||||
arg: "///1(*&SA",
|
||||
},
|
||||
{
|
||||
what: "simple",
|
||||
arg: "bob",
|
||||
},
|
||||
{
|
||||
what: "no directory",
|
||||
arg: filepath.Join("b", "rrrrrr"),
|
||||
},
|
||||
{
|
||||
what: "is a directory",
|
||||
arg: filepath.Join("b", "d", "ignore"),
|
||||
expectDir: true,
|
||||
},
|
||||
{
|
||||
what: "longer, ending in file",
|
||||
arg: filepath.Join("b", "d", "x"),
|
||||
expectFile: true,
|
||||
},
|
||||
{
|
||||
what: "moar longer, ending in file",
|
||||
arg: filepath.Join("b", "d", "a", "c", "u"),
|
||||
expectFile: true,
|
||||
},
|
||||
{
|
||||
what: "directory",
|
||||
arg: filepath.Join("b"),
|
||||
expectDir: true,
|
||||
},
|
||||
{
|
||||
// Querying for the empty string could
|
||||
// 1) be an error,
|
||||
// 2) return no result (and no error) as with
|
||||
// any illegal and therefore non-existent
|
||||
// file name,
|
||||
// 3) return the node itself, like running
|
||||
// 'ls' with no argument.
|
||||
// Going with option 2 (no result, no error),
|
||||
// since at this low level it makes more sense
|
||||
// if the results for the empty string query
|
||||
// differ from the results for the "." query.
|
||||
what: "empty name",
|
||||
arg: "",
|
||||
},
|
||||
{
|
||||
what: "self dir",
|
||||
arg: SelfDir,
|
||||
expectDir: true,
|
||||
},
|
||||
{
|
||||
what: "parent dir - doesn't exist",
|
||||
arg: ParentDir,
|
||||
},
|
||||
{
|
||||
what: "many parents - doesn't exist",
|
||||
arg: filepath.Join(ParentDir, ParentDir, ParentDir),
|
||||
},
|
||||
}
|
||||
|
||||
n := makeLoadedFileTree(t)
|
||||
for _, item := range cases {
|
||||
result, err := n.Find(item.arg)
|
||||
if item.errStr != "" {
|
||||
if err == nil {
|
||||
t.Fatalf("%s; expected error", item.what)
|
||||
}
|
||||
if !strings.Contains(err.Error(), item.errStr) {
|
||||
t.Fatalf("%s; expected err containing '%s', got '%v'",
|
||||
item.what, item.errStr, err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("%s; unexpected error: %v", item.what, err)
|
||||
}
|
||||
if result == nil {
|
||||
if item.expectDir {
|
||||
t.Fatalf(
|
||||
"%s; expected to find directory '%s'", item.what, item.arg)
|
||||
}
|
||||
if item.expectFile {
|
||||
t.Fatalf(
|
||||
"%s; expected to find file '%s'", item.what, item.arg)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if item.expectDir {
|
||||
if !result.isNodeADir() {
|
||||
t.Fatalf(
|
||||
"%s; expected '%s' to be a directory", item.what, item.arg)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if item.expectFile {
|
||||
if result.isNodeADir() {
|
||||
t.Fatalf("%s; expected '%s' to be a file", item.what, item.arg)
|
||||
}
|
||||
continue
|
||||
}
|
||||
t.Fatalf(
|
||||
"%s; expected nothing for '%s', but got '%s'",
|
||||
item.what, item.arg, result.Path())
|
||||
}
|
||||
}
|
||||
|
||||
func TestCleanedAbs(t *testing.T) {
|
||||
cases := []struct {
|
||||
what string
|
||||
full string
|
||||
cDir string
|
||||
name string
|
||||
errStr string
|
||||
}{
|
||||
{
|
||||
what: "empty",
|
||||
full: "",
|
||||
errStr: "doesn't exist",
|
||||
},
|
||||
{
|
||||
what: "simple",
|
||||
full: "bob",
|
||||
errStr: "'bob' doesn't exist",
|
||||
},
|
||||
{
|
||||
what: "no directory",
|
||||
full: filepath.Join("b", "rrrrrr"),
|
||||
errStr: "'b/rrrrrr' doesn't exist",
|
||||
},
|
||||
{
|
||||
what: "longer, ending in file",
|
||||
full: filepath.Join("b", "d", "x"),
|
||||
cDir: filepath.Join("b", "d"),
|
||||
name: "x",
|
||||
},
|
||||
{
|
||||
what: "moar longer, ending in file",
|
||||
full: filepath.Join("b", "d", "a", "c", "u"),
|
||||
cDir: filepath.Join("b", "d", "a", "c"),
|
||||
name: "u",
|
||||
},
|
||||
{
|
||||
what: "directory",
|
||||
full: filepath.Join("b", "d"),
|
||||
cDir: filepath.Join("b", "d"),
|
||||
name: "",
|
||||
},
|
||||
}
|
||||
|
||||
n := makeLoadedFileTree(t)
|
||||
for _, item := range cases {
|
||||
cDir, name, err := n.CleanedAbs(item.full)
|
||||
if item.errStr != "" {
|
||||
if err == nil {
|
||||
t.Fatalf("%s; expected error", item.what)
|
||||
}
|
||||
if !strings.Contains(err.Error(), item.errStr) {
|
||||
t.Fatalf("%s; expected err containing '%s', got '%v'",
|
||||
item.what, item.errStr, err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("%s; unexpected error: %v", item.what, err)
|
||||
}
|
||||
if cDir != ConfirmedDir(item.cDir) {
|
||||
t.Fatalf("%s; expected cDir=%s, got '%s'", item.what, item.cDir, cDir)
|
||||
}
|
||||
if name != item.name {
|
||||
t.Fatalf("%s; expected name=%s, got '%s'", item.what, item.name, name)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,14 +11,19 @@ import (
|
||||
"sigs.k8s.io/kustomize/api/krusty"
|
||||
)
|
||||
|
||||
func TestSomething1(t *testing.T) {
|
||||
// TODO: move most of the tests in api/internal/target
|
||||
// to this package, as they are all high level tests and
|
||||
// examples appropriate to this level and package.
|
||||
// The following test isn't much more than a usage example;
|
||||
// everything is actually tested down in api/internal/target.
|
||||
func TestSomething(t *testing.T) {
|
||||
fSys := filesys.MakeFsInMemory()
|
||||
b := krusty.MakeKustomizer(fSys, krusty.MakeDefaultOptions())
|
||||
_, err := b.Run("hey")
|
||||
_, err := b.Run("noSuchThing")
|
||||
if err == nil {
|
||||
t.Fatalf("expected error")
|
||||
}
|
||||
if err.Error() != "got file 'hey', but 'hey' must be a directory to be a root" {
|
||||
if err.Error() != "'noSuchThing' doesn't exist" {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -148,8 +148,7 @@ func demandDirectoryRoot(
|
||||
}
|
||||
d, f, err := fSys.CleanedAbs(path)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf(
|
||||
"absolute path error in '%s' : %v", path, err)
|
||||
return "", err
|
||||
}
|
||||
if f != "" {
|
||||
return "", fmt.Errorf(
|
||||
|
||||
@@ -45,7 +45,7 @@ var testCases = []testData{
|
||||
func MakeFakeFs(td []testData) filesys.FileSystem {
|
||||
fSys := filesys.MakeFsInMemory()
|
||||
for _, x := range td {
|
||||
fSys.WriteFile("/"+x.path, []byte(x.expectedContent))
|
||||
fSys.WriteFile(x.path, []byte(x.expectedContent))
|
||||
}
|
||||
return fSys
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ func TestCreateNoArgs(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestCreateWithResources(t *testing.T) {
|
||||
fSys := filesys.MakeFsInMemory()
|
||||
fSys := filesys.MakeEmptyDirInMemory()
|
||||
fSys.WriteFile("foo.yaml", []byte(""))
|
||||
fSys.WriteFile("bar.yaml", []byte(""))
|
||||
opts := createFlags{resources: "foo.yaml,bar.yaml"}
|
||||
|
||||
@@ -20,7 +20,7 @@ sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
|
||||
)
|
||||
|
||||
func TestAddPatchHappyPath(t *testing.T) {
|
||||
fSys := filesys.MakeFsInMemory()
|
||||
fSys := filesys.MakeEmptyDirInMemory()
|
||||
fSys.WriteFile(patchFileName, []byte(patchFileContent))
|
||||
fSys.WriteFile(patchFileName+"another", []byte(patchFileContent))
|
||||
testutils_test.WriteTestKustomization(fSys)
|
||||
@@ -44,7 +44,7 @@ func TestAddPatchHappyPath(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestAddPatchAlreadyThere(t *testing.T) {
|
||||
fSys := filesys.MakeFsInMemory()
|
||||
fSys := filesys.MakeEmptyDirInMemory()
|
||||
fSys.WriteFile(patchFileName, []byte(patchFileContent))
|
||||
testutils_test.WriteTestKustomization(fSys)
|
||||
|
||||
@@ -63,7 +63,7 @@ func TestAddPatchAlreadyThere(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestAddPatchNoArgs(t *testing.T) {
|
||||
fSys := filesys.MakeFsInMemory()
|
||||
fSys := filesys.MakeEmptyDirInMemory()
|
||||
|
||||
cmd := newCmdAddPatch(fSys)
|
||||
err := cmd.Execute()
|
||||
|
||||
@@ -20,7 +20,7 @@ sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
|
||||
)
|
||||
|
||||
func TestAddResourceHappyPath(t *testing.T) {
|
||||
fSys := filesys.MakeFsInMemory()
|
||||
fSys := filesys.MakeEmptyDirInMemory()
|
||||
fSys.WriteFile(resourceFileName, []byte(resourceFileContent))
|
||||
fSys.WriteFile(resourceFileName+"another", []byte(resourceFileContent))
|
||||
testutils_test.WriteTestKustomization(fSys)
|
||||
|
||||
@@ -73,7 +73,7 @@ func TestDataConfigValidation_Flags(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestExpandFileSource(t *testing.T) {
|
||||
fSys := filesys.MakeFsInMemory()
|
||||
fSys := filesys.MakeEmptyDirInMemory()
|
||||
fSys.Create("dir/fa1")
|
||||
fSys.Create("dir/fa2")
|
||||
fSys.Create("dir/readme")
|
||||
@@ -91,7 +91,7 @@ func TestExpandFileSource(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestExpandFileSourceWithKey(t *testing.T) {
|
||||
fSys := filesys.MakeFsInMemory()
|
||||
fSys := filesys.MakeEmptyDirInMemory()
|
||||
fSys.Create("dir/faaaaaaaaaabbbbbbbbbccccccccccccccccc")
|
||||
fSys.Create("dir/foobar")
|
||||
fSys.Create("dir/simplebar")
|
||||
|
||||
@@ -21,7 +21,7 @@ sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
|
||||
)
|
||||
|
||||
func makeKustomizationPatchFS() filesys.FileSystem {
|
||||
fSys := filesys.MakeFsInMemory()
|
||||
fSys := filesys.MakeEmptyDirInMemory()
|
||||
patches := []string{"patch1.yaml", "patch2.yaml"}
|
||||
|
||||
testutils_test.WriteTestKustomizationWith(fSys, []byte(
|
||||
|
||||
Reference in New Issue
Block a user