The POSIX shell installers staged shared Skill links and published them with mv after an existence check; a file or symlink another process created at the destination between the check and the move was silently replaced, and the inode confirmation could not detect the loss. Publish by creating each link directly at its destination instead — symlink(2) refuses an occupied path with EEXIST, so the creation itself is the atomic no-replace check. A directory that appears at the destination turns ln -s into a container; the nested link is removed after an identity check and the transaction rolls back, leaving the foreign directory untouched. Applied to install.sh, install-skills.sh, install-event.sh, and install-devapp.sh. Also covers the remaining retraction branches of the Go shell-removal fallback so changed-code coverage is complete. Regression tests inject a concurrent occupant at the publish instant for regular-file and directory cases and assert the foreign object and its contents stay completely unchanged.
619 lines
21 KiB
Go
619 lines
21 KiB
Go
package upgrade
|
|
|
|
import (
|
|
"errors"
|
|
"io"
|
|
"io/fs"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/testseam"
|
|
)
|
|
|
|
func TestCrossPlatformCoverageSkillPathMoveSystemErrors(t *testing.T) {
|
|
t.Run("destination probe", func(t *testing.T) {
|
|
src, dst := makeSkillMoveFixture(t)
|
|
original := skillPathLstat
|
|
testseam.Swap(t, &skillPathLstat, func(path string) (os.FileInfo, error) {
|
|
if path == dst {
|
|
return nil, os.ErrPermission
|
|
}
|
|
return original(path)
|
|
})
|
|
if err := moveSkillPathRecoverably(src, dst); !errors.Is(err, os.ErrPermission) {
|
|
t.Fatalf("error = %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("destination parent", func(t *testing.T) {
|
|
src, dst := makeSkillMoveFixture(t)
|
|
testseam.Swap(t, &skillPathMkdirAll, func(string, os.FileMode) error { return os.ErrPermission })
|
|
if err := moveSkillPathRecoverably(src, dst); !errors.Is(err, os.ErrPermission) {
|
|
t.Fatalf("error = %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("staging creation", func(t *testing.T) {
|
|
src, dst := makeSkillMoveFixture(t)
|
|
forceCrossDeviceRename(t, src, dst)
|
|
testseam.Swap(t, &skillPathMkdirTemp, func(string, string) (string, error) { return "", os.ErrPermission })
|
|
if err := moveSkillPathRecoverably(src, dst); !errors.Is(err, os.ErrPermission) {
|
|
t.Fatalf("error = %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("failed staging cleanup", func(t *testing.T) {
|
|
src, dst := makeSkillMoveFixture(t)
|
|
forceCrossDeviceRename(t, src, dst)
|
|
testseam.Swap(t, &skillPathCopy, func(string, string) error { return errors.New("copy failed") })
|
|
testseam.Swap(t, &skillPathRemoveAll, func(string) error { return errors.New("cleanup failed") })
|
|
err := moveSkillPathRecoverably(src, dst)
|
|
if err == nil || !strings.Contains(err.Error(), "copy failed") || !strings.Contains(err.Error(), "cleanup failed") {
|
|
t.Fatalf("error = %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("missing copied staging", func(t *testing.T) {
|
|
src, dst := makeSkillMoveFixture(t)
|
|
forceCrossDeviceRename(t, src, dst)
|
|
testseam.Swap(t, &skillPathCopy, func(string, string) error { return nil })
|
|
testseam.Swap(t, &skillPathVerify, func(string, string) error { return nil })
|
|
if err := moveSkillPathRecoverably(src, dst); err == nil || !strings.Contains(err.Error(), "检查跨设备") {
|
|
t.Fatalf("error = %v", err)
|
|
}
|
|
})
|
|
|
|
for _, tc := range []struct {
|
|
name string
|
|
path func(src, dst string) string
|
|
want string
|
|
}{
|
|
{name: "prepare staging mode", path: func(_, dst string) string { return filepath.Join(filepath.Dir(dst), "payload") }, want: "准备跨设备"},
|
|
{name: "restore published mode", path: func(_, dst string) string { return dst }, want: "恢复已发布"},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
src, dst := makeSkillMoveFixture(t)
|
|
forceCrossDeviceRename(t, src, dst)
|
|
original := skillPathChmod
|
|
testseam.Swap(t, &skillPathChmod, func(path string, mode os.FileMode) error {
|
|
if tc.name == "prepare staging mode" && strings.HasSuffix(path, string(filepath.Separator)+"payload") {
|
|
return errors.New("chmod failed")
|
|
}
|
|
if tc.name == "restore published mode" && path == dst {
|
|
return errors.New("chmod failed")
|
|
}
|
|
return original(path, mode)
|
|
})
|
|
if err := moveSkillPathRecoverably(src, dst); err == nil || !strings.Contains(err.Error(), tc.want) {
|
|
t.Fatalf("error = %v", err)
|
|
}
|
|
})
|
|
}
|
|
|
|
t.Run("published staging cleanup", func(t *testing.T) {
|
|
src, dst := makeSkillMoveFixture(t)
|
|
forceCrossDeviceRename(t, src, dst)
|
|
original := skillPathRemoveAll
|
|
testseam.Swap(t, &skillPathRemoveAll, func(path string) error {
|
|
if strings.Contains(filepath.Base(path), ".cross-device-") {
|
|
return errors.New("cleanup failed")
|
|
}
|
|
return original(path)
|
|
})
|
|
if err := moveSkillPathRecoverably(src, dst); err == nil || !strings.Contains(err.Error(), "清理已发布") {
|
|
t.Fatalf("error = %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("source deletion cannot be confirmed", func(t *testing.T) {
|
|
src, dst := makeSkillMoveFixture(t)
|
|
forceCrossDeviceRename(t, src, dst)
|
|
removalAttempted := false
|
|
originalLstat := skillPathLstat
|
|
originalRemove := skillPathRemoveAll
|
|
testseam.Swap(t, &skillPathRemoveAll, func(path string) error {
|
|
if path == src {
|
|
removalAttempted = true
|
|
return nil
|
|
}
|
|
return originalRemove(path)
|
|
})
|
|
testseam.Swap(t, &skillPathLstat, func(path string) (os.FileInfo, error) {
|
|
if path == src && removalAttempted {
|
|
return nil, os.ErrPermission
|
|
}
|
|
return originalLstat(path)
|
|
})
|
|
if err := moveSkillPathRecoverably(src, dst); !errors.Is(err, os.ErrPermission) {
|
|
t.Fatalf("error = %v", err)
|
|
}
|
|
})
|
|
}
|
|
|
|
// TestCrossPlatformCoverageSkillMoveShellCleanup covers the emptied-source
|
|
// shell handling in moveSkillPathRecoverably: a child-move publication (the
|
|
// degraded-filesystem slow path) leaves the source behind as an empty shell,
|
|
// and move semantics require that shell to be gone once the move reports
|
|
// success — including the failure branches around its removal.
|
|
func TestCrossPlatformCoverageSkillMoveShellCleanup(t *testing.T) {
|
|
forceSlowClaimMove := func(t *testing.T, src, dst string) {
|
|
t.Helper()
|
|
testseam.Swap(t, &skillPathRenameNoReplaceAtomic, func(string, string) error {
|
|
return errNoReplaceRenameUnsupported
|
|
})
|
|
forceFastClaimRenameFailure(t, src, dst)
|
|
}
|
|
|
|
t.Run("slow path shell is removed after publication", func(t *testing.T) {
|
|
src, dst := makeSkillMoveFixture(t)
|
|
forceSlowClaimMove(t, src, dst)
|
|
if err := moveSkillPathRecoverably(src, dst); err != nil {
|
|
t.Fatalf("slow-path move must succeed, got %v", err)
|
|
}
|
|
assertUpgradeSkillContent(t, dst, "old")
|
|
if _, statErr := os.Lstat(src); !os.IsNotExist(statErr) {
|
|
t.Fatalf("emptied source shell must be removed, stat err=%v", statErr)
|
|
}
|
|
})
|
|
|
|
t.Run("shell vanished concurrently after publication completes the move", func(t *testing.T) {
|
|
src, dst := makeSkillMoveFixture(t)
|
|
forceSlowClaimMove(t, src, dst)
|
|
originalRemove := skillPathRemove
|
|
testseam.Swap(t, &skillPathRemove, func(path string) error {
|
|
if path == src {
|
|
return &os.PathError{Op: "remove", Path: path, Err: os.ErrNotExist}
|
|
}
|
|
return originalRemove(path)
|
|
})
|
|
if err := moveSkillPathRecoverably(src, dst); err != nil {
|
|
t.Fatalf("concurrent shell removal must complete the move, got %v", err)
|
|
}
|
|
assertUpgradeSkillContent(t, dst, "old")
|
|
})
|
|
|
|
t.Run("shell removal failure retracts the publication and restores the source", func(t *testing.T) {
|
|
src, dst := makeSkillMoveFixture(t)
|
|
forceSlowClaimMove(t, src, dst)
|
|
originalRemove := skillPathRemove
|
|
testseam.Swap(t, &skillPathRemove, func(path string) error {
|
|
if path == src {
|
|
return os.ErrPermission
|
|
}
|
|
return originalRemove(path)
|
|
})
|
|
err := moveSkillPathRecoverably(src, dst)
|
|
if err == nil || !strings.Contains(err.Error(), "原路径已恢复") {
|
|
t.Fatalf("shell removal failure must surface with the source restored, got %v", err)
|
|
}
|
|
assertUpgradeSkillContent(t, src, "old")
|
|
if _, statErr := os.Lstat(dst); !os.IsNotExist(statErr) {
|
|
t.Fatalf("retracted destination must be gone, stat err=%v", statErr)
|
|
}
|
|
})
|
|
|
|
t.Run("shell stat failure after publication surfaces", func(t *testing.T) {
|
|
src, dst := makeSkillMoveFixture(t)
|
|
forceSlowClaimMove(t, src, dst)
|
|
published := false
|
|
originalNoReplace := skillPathRenameNoReplace
|
|
testseam.Swap(t, &skillPathRenameNoReplace, func(from, to string) error {
|
|
err := originalNoReplace(from, to)
|
|
if err == nil && from == src && to == dst {
|
|
published = true
|
|
}
|
|
return err
|
|
})
|
|
originalLstat := skillPathLstat
|
|
testseam.Swap(t, &skillPathLstat, func(path string) (os.FileInfo, error) {
|
|
if path == src && published {
|
|
return nil, os.ErrPermission
|
|
}
|
|
return originalLstat(path)
|
|
})
|
|
err := moveSkillPathRecoverably(src, dst)
|
|
if err == nil || !strings.Contains(err.Error(), "检查已移动 Skill 源路径失败") {
|
|
t.Fatalf("shell stat failure must surface, got %v", err)
|
|
}
|
|
assertUpgradeSkillContent(t, dst, "old")
|
|
})
|
|
}
|
|
|
|
func TestCrossPlatformCoverageSkillPathPermissionRestorationErrors(t *testing.T) {
|
|
t.Run("source preparation error", func(t *testing.T) {
|
|
testseam.Swap(t, &skillPathWalkDir, func(string, fs.WalkDirFunc) error {
|
|
return errors.New("prepare failed")
|
|
})
|
|
if err := removePublishedSkillSource("ignored"); err == nil || !strings.Contains(err.Error(), "prepare failed") {
|
|
t.Fatalf("error = %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("walk error", func(t *testing.T) {
|
|
testseam.Swap(t, &skillPathWalkDir, func(root string, fn fs.WalkDirFunc) error {
|
|
return fn(root, nil, errors.New("walk failed"))
|
|
})
|
|
if _, err := prepareSkillPathTreeRemoval("ignored"); err == nil || !strings.Contains(err.Error(), "walk failed") {
|
|
t.Fatalf("error = %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("writable mode preparation", func(t *testing.T) {
|
|
entry := skillPathModeDirEntry{mode: os.ModeDir | 0o500}
|
|
testseam.Swap(t, &skillPathWalkDir, func(root string, fn fs.WalkDirFunc) error {
|
|
return fn(root, entry, nil)
|
|
})
|
|
var chmodMode os.FileMode
|
|
testseam.Swap(t, &skillPathChmod, func(_ string, mode os.FileMode) error {
|
|
chmodMode = mode
|
|
return nil
|
|
})
|
|
modes, err := prepareSkillPathTreeRemoval("source")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if chmodMode != 0o700 || len(modes) != 1 || modes[0].path != "source" || modes[0].mode != 0o500 {
|
|
t.Fatalf("prepared mode = %v, records = %#v", chmodMode, modes)
|
|
}
|
|
})
|
|
|
|
t.Run("writable mode preparation chmod error", func(t *testing.T) {
|
|
entry := skillPathModeDirEntry{mode: os.ModeDir | 0o500}
|
|
testseam.Swap(t, &skillPathWalkDir, func(root string, fn fs.WalkDirFunc) error {
|
|
return fn(root, entry, nil)
|
|
})
|
|
testseam.Swap(t, &skillPathChmod, func(string, os.FileMode) error {
|
|
return errors.New("chmod failed")
|
|
})
|
|
if _, err := prepareSkillPathTreeRemoval("source"); err == nil || !strings.Contains(err.Error(), "chmod failed") {
|
|
t.Fatalf("error = %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("entry info error", func(t *testing.T) {
|
|
testseam.Swap(t, &skillPathWalkDir, func(root string, fn fs.WalkDirFunc) error {
|
|
return fn(root, skillPathErrorDirEntry{}, nil)
|
|
})
|
|
if _, err := prepareSkillPathTreeRemoval("ignored"); err == nil || !strings.Contains(err.Error(), "info failed") {
|
|
t.Fatalf("error = %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("missing restored directory", func(t *testing.T) {
|
|
if err := restoreSkillPathDirModes([]skillPathDirMode{{path: filepath.Join(t.TempDir(), "missing"), mode: 0o555}}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
})
|
|
|
|
t.Run("restore probe error", func(t *testing.T) {
|
|
testseam.Swap(t, &skillPathLstat, func(string) (os.FileInfo, error) { return nil, os.ErrPermission })
|
|
if err := restoreSkillPathDirModes([]skillPathDirMode{{path: "blocked", mode: 0o555}}); !errors.Is(err, os.ErrPermission) {
|
|
t.Fatalf("error = %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("restore chmod error", func(t *testing.T) {
|
|
path := t.TempDir()
|
|
testseam.Swap(t, &skillPathChmod, func(string, os.FileMode) error { return os.ErrPermission })
|
|
if err := restoreSkillPathDirModes([]skillPathDirMode{{path: path, mode: 0o555}}); !errors.Is(err, os.ErrPermission) {
|
|
t.Fatalf("error = %v", err)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestCrossPlatformCoverageSkillPathLexicalCopyErrors(t *testing.T) {
|
|
t.Run("source lstat", func(t *testing.T) {
|
|
if err := copySkillPathLexically(filepath.Join(t.TempDir(), "missing"), "unused"); !os.IsNotExist(err) {
|
|
t.Fatalf("error = %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("destination mkdir", func(t *testing.T) {
|
|
root := t.TempDir()
|
|
src, dst := filepath.Join(root, "src"), filepath.Join(root, "dst")
|
|
if err := os.Mkdir(src, 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(dst, []byte("occupied"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := copySkillPathLexically(src, dst); err == nil {
|
|
t.Fatal("expected mkdir failure")
|
|
}
|
|
})
|
|
|
|
t.Run("source readdir", func(t *testing.T) {
|
|
root := t.TempDir()
|
|
src := filepath.Join(root, "src")
|
|
if err := os.Mkdir(src, 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
testseam.Swap(t, &skillPathReadDir, func(string) ([]os.DirEntry, error) { return nil, os.ErrPermission })
|
|
if err := copySkillPathLexically(src, filepath.Join(root, "dst")); !errors.Is(err, os.ErrPermission) {
|
|
t.Fatalf("error = %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("nested copy", func(t *testing.T) {
|
|
root := t.TempDir()
|
|
src, dst := filepath.Join(root, "src"), filepath.Join(root, "dst")
|
|
if err := os.Mkdir(src, 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(src, "child"), []byte("x"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
original := skillPathLstat
|
|
testseam.Swap(t, &skillPathLstat, func(path string) (os.FileInfo, error) {
|
|
if filepath.Base(path) == "child" {
|
|
return nil, os.ErrPermission
|
|
}
|
|
return original(path)
|
|
})
|
|
if err := copySkillPathLexically(src, dst); !errors.Is(err, os.ErrPermission) {
|
|
t.Fatalf("error = %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("readlink", func(t *testing.T) {
|
|
root := t.TempDir()
|
|
src := filepath.Join(root, "link")
|
|
if err := os.Symlink("target", src); err != nil {
|
|
t.Skipf("symlink unavailable: %v", err)
|
|
}
|
|
testseam.Swap(t, &skillPathReadlink, func(string) (string, error) { return "", os.ErrPermission })
|
|
if err := copySkillPathLexically(src, filepath.Join(root, "dst")); !errors.Is(err, os.ErrPermission) {
|
|
t.Fatalf("error = %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("special path", func(t *testing.T) {
|
|
testseam.Swap(t, &skillPathLstat, func(string) (os.FileInfo, error) {
|
|
return skillPathFakeInfo{mode: os.ModeNamedPipe}, nil
|
|
})
|
|
if err := copySkillPathLexically("special", "dst"); err == nil || !strings.Contains(err.Error(), "特殊") {
|
|
t.Fatalf("error = %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("regular open", func(t *testing.T) {
|
|
if err := copyRegularSkillFile(filepath.Join(t.TempDir(), "missing"), "dst", 0o600); !os.IsNotExist(err) {
|
|
t.Fatalf("error = %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("regular destination open", func(t *testing.T) {
|
|
root := t.TempDir()
|
|
src, dst := filepath.Join(root, "src"), filepath.Join(root, "dst")
|
|
if err := os.WriteFile(src, []byte("x"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(dst, []byte("occupied"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := copyRegularSkillFile(src, dst, 0o600); err == nil {
|
|
t.Fatal("expected exclusive create failure")
|
|
}
|
|
})
|
|
|
|
for _, tc := range []struct {
|
|
name string
|
|
swap func(*testing.T)
|
|
}{
|
|
{name: "copy", swap: func(t *testing.T) {
|
|
testseam.Swap(t, &skillPathCopyBytes, func(io.Writer, io.Reader) (int64, error) { return 0, errors.New("copy failed") })
|
|
}},
|
|
{name: "sync", swap: func(t *testing.T) {
|
|
testseam.Swap(t, &skillPathSync, func(*os.File) error { return errors.New("sync failed") })
|
|
}},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
root := t.TempDir()
|
|
src, dst := filepath.Join(root, "src"), filepath.Join(root, "dst")
|
|
if err := os.WriteFile(src, []byte("x"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
tc.swap(t)
|
|
if err := copyRegularSkillFile(src, dst, 0o600); err == nil || !strings.Contains(err.Error(), tc.name) {
|
|
t.Fatalf("error = %v", err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCrossPlatformCoverageSkillPathVerificationErrors(t *testing.T) {
|
|
root := t.TempDir()
|
|
|
|
t.Run("source and destination lstat", func(t *testing.T) {
|
|
if err := verifySkillPathCopy(filepath.Join(root, "missing-src"), "dst"); !os.IsNotExist(err) {
|
|
t.Fatalf("source error = %v", err)
|
|
}
|
|
src := filepath.Join(root, "source")
|
|
if err := os.WriteFile(src, []byte("x"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := verifySkillPathCopy(src, filepath.Join(root, "missing-dst")); !os.IsNotExist(err) {
|
|
t.Fatalf("destination error = %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("directory and file modes", func(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
name string
|
|
src os.FileMode
|
|
dst os.FileMode
|
|
}{
|
|
{name: "directory", src: os.ModeDir | 0o700, dst: os.ModeDir | 0o755},
|
|
{name: "file", src: 0o600, dst: 0o644},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
src, dst := "src", "dst"
|
|
testseam.Swap(t, &skillPathLstat, func(path string) (os.FileInfo, error) {
|
|
if path == src {
|
|
return skillPathFakeInfo{mode: tc.src}, nil
|
|
}
|
|
return skillPathFakeInfo{mode: tc.dst}, nil
|
|
})
|
|
if err := verifySkillPathCopy(src, dst); err == nil || !strings.Contains(err.Error(), "权限") {
|
|
t.Fatalf("error = %v", err)
|
|
}
|
|
})
|
|
}
|
|
})
|
|
|
|
t.Run("directory read errors", func(t *testing.T) {
|
|
for _, failDest := range []bool{false, true} {
|
|
t.Run(map[bool]string{false: "source", true: "destination"}[failDest], func(t *testing.T) {
|
|
base := t.TempDir()
|
|
src, dst := filepath.Join(base, "src"), filepath.Join(base, "dst")
|
|
if err := os.Mkdir(src, 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.Mkdir(dst, 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
original := skillPathReadDir
|
|
testseam.Swap(t, &skillPathReadDir, func(path string) ([]os.DirEntry, error) {
|
|
if (path == dst) == failDest {
|
|
return nil, os.ErrPermission
|
|
}
|
|
return original(path)
|
|
})
|
|
if err := verifySkillPathCopy(src, dst); !errors.Is(err, os.ErrPermission) {
|
|
t.Fatalf("error = %v", err)
|
|
}
|
|
})
|
|
}
|
|
})
|
|
|
|
t.Run("nested mismatch", func(t *testing.T) {
|
|
base := t.TempDir()
|
|
src, dst := filepath.Join(base, "src"), filepath.Join(base, "dst")
|
|
if err := os.Mkdir(src, 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.Mkdir(dst, 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(src, "child"), []byte("x"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.Mkdir(filepath.Join(dst, "child"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := verifySkillPathCopy(src, dst); err == nil || !strings.Contains(err.Error(), "类型") {
|
|
t.Fatalf("error = %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("symlink reads", func(t *testing.T) {
|
|
base := t.TempDir()
|
|
src, dst := filepath.Join(base, "src"), filepath.Join(base, "dst")
|
|
if err := os.Symlink("one", src); err != nil {
|
|
t.Skipf("symlink unavailable: %v", err)
|
|
}
|
|
if err := os.Symlink("one", dst); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, failDest := range []bool{false, true} {
|
|
t.Run(map[bool]string{false: "source", true: "destination"}[failDest], func(t *testing.T) {
|
|
original := skillPathReadlink
|
|
testseam.Swap(t, &skillPathReadlink, func(path string) (string, error) {
|
|
if (path == dst) == failDest {
|
|
return "", os.ErrPermission
|
|
}
|
|
return original(path)
|
|
})
|
|
if err := verifySkillPathCopy(src, dst); !errors.Is(err, os.ErrPermission) {
|
|
t.Fatalf("error = %v", err)
|
|
}
|
|
})
|
|
}
|
|
})
|
|
|
|
t.Run("file digest opens", func(t *testing.T) {
|
|
base := t.TempDir()
|
|
src, dst := filepath.Join(base, "src"), filepath.Join(base, "dst")
|
|
if err := os.WriteFile(src, []byte("x"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(dst, []byte("x"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, failDest := range []bool{false, true} {
|
|
t.Run(map[bool]string{false: "source", true: "destination"}[failDest], func(t *testing.T) {
|
|
original := skillPathOpen
|
|
testseam.Swap(t, &skillPathOpen, func(path string) (*os.File, error) {
|
|
if (path == dst) == failDest {
|
|
return nil, os.ErrPermission
|
|
}
|
|
return original(path)
|
|
})
|
|
if err := verifySkillPathCopy(src, dst); !errors.Is(err, os.ErrPermission) {
|
|
t.Fatalf("error = %v", err)
|
|
}
|
|
})
|
|
}
|
|
})
|
|
|
|
t.Run("special path", func(t *testing.T) {
|
|
fake := skillPathFakeInfo{mode: os.ModeNamedPipe}
|
|
testseam.Swap(t, &skillPathLstat, func(string) (os.FileInfo, error) { return fake, nil })
|
|
if err := verifySkillPathCopy("src", "dst"); err == nil || !strings.Contains(err.Error(), "特殊") {
|
|
t.Fatalf("error = %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("entry names", func(t *testing.T) {
|
|
if _, err := skillPathEntryNames(filepath.Join(root, "missing-dir")); !os.IsNotExist(err) {
|
|
t.Fatalf("error = %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("digest open and read", func(t *testing.T) {
|
|
if _, err := skillPathFileDigest(filepath.Join(root, "missing-file")); !os.IsNotExist(err) {
|
|
t.Fatalf("open error = %v", err)
|
|
}
|
|
file := filepath.Join(root, "digest-file")
|
|
if err := os.WriteFile(file, []byte("x"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
testseam.Swap(t, &skillPathCopyBytes, func(io.Writer, io.Reader) (int64, error) { return 0, errors.New("read failed") })
|
|
if _, err := skillPathFileDigest(file); err == nil || !strings.Contains(err.Error(), "read failed") {
|
|
t.Fatalf("read error = %v", err)
|
|
}
|
|
})
|
|
}
|
|
|
|
type skillPathFakeInfo struct {
|
|
mode os.FileMode
|
|
}
|
|
|
|
func (skillPathFakeInfo) Name() string { return "fake" }
|
|
func (skillPathFakeInfo) Size() int64 { return 0 }
|
|
func (info skillPathFakeInfo) Mode() os.FileMode { return info.mode }
|
|
func (skillPathFakeInfo) ModTime() time.Time { return time.Time{} }
|
|
func (info skillPathFakeInfo) IsDir() bool { return info.mode.IsDir() }
|
|
func (skillPathFakeInfo) Sys() any { return nil }
|
|
|
|
type skillPathErrorDirEntry struct{}
|
|
|
|
func (skillPathErrorDirEntry) Name() string { return "broken" }
|
|
func (skillPathErrorDirEntry) IsDir() bool { return true }
|
|
func (skillPathErrorDirEntry) Type() os.FileMode { return os.ModeDir }
|
|
func (skillPathErrorDirEntry) Info() (os.FileInfo, error) { return nil, errors.New("info failed") }
|
|
|
|
type skillPathModeDirEntry struct {
|
|
mode os.FileMode
|
|
}
|
|
|
|
func (skillPathModeDirEntry) Name() string { return "directory" }
|
|
func (skillPathModeDirEntry) IsDir() bool { return true }
|
|
func (entry skillPathModeDirEntry) Type() os.FileMode { return entry.mode.Type() }
|
|
func (entry skillPathModeDirEntry) Info() (os.FileInfo, error) {
|
|
return skillPathFakeInfo{mode: entry.mode}, nil
|
|
}
|