mirror of
https://github.com/samuelncui/acp.git
synced 2026-11-08 04:21:16 +00:00
Simplify indexed copy facts and unify staged output completion. Fix cleanup, rewrite persistence, native paths, and terminal reporting. Maintain compatibility, upstream notices, and automated release checks.
210 lines
7.2 KiB
Go
210 lines
7.2 KiB
Go
package acp
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// walkTestEntries runs the wildcard enumeration the shell uses, with a stream in place because
|
|
// the walk reports the sources it cannot read through it.
|
|
func walkTestEntries(t *testing.T, sources, targets []string) []*compatItem {
|
|
t.Helper()
|
|
|
|
option := WildcardJob(Source(sources...), Target(targets...))(newOption())
|
|
if err := option.check(); err != nil {
|
|
t.Fatalf("check options: %v", err)
|
|
}
|
|
|
|
copyer := &Copyer{option: option, stream: newTestStream(t)}
|
|
entries, err := copyer.walkWildcard(option.wildcardJobs[0])
|
|
if err != nil {
|
|
t.Fatalf("walk wildcard: %v", err)
|
|
}
|
|
|
|
return entries
|
|
}
|
|
|
|
func TestWalkWildcardMapsSourcesOntoTargets(t *testing.T) {
|
|
// Enumerate a tree with regular, empty and nested files plus an empty directory.
|
|
root := t.TempDir()
|
|
source := filepath.Join(root, "source")
|
|
writeSourceFile(t, source, "plain.txt", []byte("plain"))
|
|
writeSourceFile(t, source, "empty.txt", nil)
|
|
writeSourceFile(t, source, filepath.Join("nested", "data"), []byte("nested"))
|
|
if err := os.MkdirAll(filepath.Join(source, "emptydir"), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
targets := []string{filepath.Join(root, "target-a"), filepath.Join(root, "target-b")}
|
|
for _, target := range targets {
|
|
if err := os.MkdirAll(target, 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
// Directories are not items; paths stay in platform order and map onto every target.
|
|
entries := walkTestEntries(t, []string{source}, targets)
|
|
want := []string{
|
|
"empty.txt",
|
|
filepath.Join("nested", "data"),
|
|
"plain.txt",
|
|
}
|
|
if len(entries) != len(want) {
|
|
t.Fatalf("entries = %d, want %d: %+v", len(entries), len(want), entries)
|
|
}
|
|
for index, entry := range entries {
|
|
relative := filepath.Join("source", want[index])
|
|
if got := strings.Join(entry.path, "/"); got != filepath.ToSlash(relative) {
|
|
t.Fatalf("entry %d path = %q, want %q", index, got, relative)
|
|
}
|
|
if base, _ := filepath.Split(source); entry.base != base {
|
|
t.Fatalf("entry %d base = %q, want %q", index, entry.base, base)
|
|
}
|
|
if entry.source != filepath.Join(source, want[index]) {
|
|
t.Fatalf("entry %d source = %q", index, entry.source)
|
|
}
|
|
if len(entry.targets) != len(targets) {
|
|
t.Fatalf("entry %d targets = %v, want one per target directory", index, entry.targets)
|
|
}
|
|
for targetIndex, target := range targets {
|
|
if got, wantTarget := entry.targets[targetIndex], filepath.Join(target, relative); got != wantTarget {
|
|
t.Fatalf("entry %d target %d = %q, want %q", index, targetIndex, got, wantTarget)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestWalkWildcardMapsASingleFileOntoTargets(t *testing.T) {
|
|
root := t.TempDir()
|
|
source := writeSourceFile(t, root, "plain.txt", []byte("plain"))
|
|
target := filepath.Join(root, "target")
|
|
if err := os.MkdirAll(target, 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
entries := walkTestEntries(t, []string{source}, []string{target})
|
|
if len(entries) != 1 {
|
|
t.Fatalf("entries = %+v, want one", entries)
|
|
}
|
|
entry := entries[0]
|
|
base, _ := filepath.Split(source)
|
|
if entry.source != source || len(entry.path) != 1 || entry.path[0] != "plain.txt" || entry.base != base {
|
|
t.Fatalf("entry = %+v", entry)
|
|
}
|
|
if len(entry.targets) != 1 || entry.targets[0] != filepath.Join(target, "plain.txt") {
|
|
t.Fatalf("targets = %v", entry.targets)
|
|
}
|
|
}
|
|
|
|
func TestWildcardPreservesWhitespaceInFileAndDirectoryNames(t *testing.T) {
|
|
// Spaces are filename content in both Source and Target options.
|
|
root := t.TempDir()
|
|
source := writeSourceFile(t, root, " name ", []byte("content"))
|
|
target := filepath.Join(root, " target ")
|
|
if err := os.Mkdir(target, 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
handler, report := NewReportGetter()
|
|
copyer, err := New(context.Background(), WildcardJob(Source(source), Target(target)), WithEventHandler(handler))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := copyer.WaitErr(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
want := filepath.Join(target, " name ")
|
|
data, err := os.ReadFile(want)
|
|
if err != nil || string(data) != "content" {
|
|
t.Fatalf("literal name copy=%q / %v", data, err)
|
|
}
|
|
rows := report().Jobs
|
|
if len(rows) != 1 || len(rows[0].SuccessTargets) != 1 || rows[0].SuccessTargets[0] != want {
|
|
t.Fatalf("unexpected report: %+v", rows)
|
|
}
|
|
}
|
|
|
|
func TestShellRejectsRepeatedRelativePaths(t *testing.T) {
|
|
// Two source trees with the same name would write the same target twice, so the run now
|
|
// fails before it copies anything instead of keeping one entry per relative path.
|
|
root := t.TempDir()
|
|
first := filepath.Join(root, "first", "tree")
|
|
second := filepath.Join(root, "second", "tree")
|
|
writeSourceFile(t, first, "data.txt", []byte("first"))
|
|
writeSourceFile(t, second, "data.txt", []byte("second"))
|
|
target := filepath.Join(root, "target")
|
|
if err := os.MkdirAll(target, 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
copyer, err := New(context.Background(), WildcardJob(Source(second, first), Target(target)))
|
|
if err != nil {
|
|
t.Fatalf("new: %v", err)
|
|
}
|
|
err = copyer.WaitErr()
|
|
if err == nil || !strings.Contains(err.Error(), "same relative path") {
|
|
t.Fatalf("WaitErr() error = %v, want a repeated relative path", err)
|
|
}
|
|
|
|
// The ambiguous target file must not be written.
|
|
ambiguous := filepath.Join(target, "tree", "data.txt")
|
|
if _, err := os.Stat(ambiguous); !errors.Is(err, os.ErrNotExist) {
|
|
t.Fatalf("ambiguous target stat error = %v, want %v", err, os.ErrNotExist)
|
|
}
|
|
}
|
|
|
|
func TestNewRejectsInvalidInput(t *testing.T) {
|
|
root := t.TempDir()
|
|
target := filepath.Join(root, "target")
|
|
if err := os.MkdirAll(target, 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
file := writeSourceFile(t, root, "plain.txt", []byte("plain"))
|
|
|
|
// An empty WildcardJob or Source() carries no source at all and is a no-op option, so the
|
|
// old "no source" case has no equivalent here.
|
|
tests := []struct {
|
|
name string
|
|
sources []string
|
|
targets []string
|
|
}{
|
|
{name: "missing source", sources: []string{filepath.Join(root, "missing")}, targets: []string{target}},
|
|
{name: "missing target", sources: []string{file}, targets: []string{filepath.Join(root, "missing-target")}},
|
|
{name: "target is a file", sources: []string{file}, targets: []string{file}},
|
|
}
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
option := WildcardJob(Source(test.sources...), Target(test.targets...))
|
|
if _, err := New(context.Background(), option); err == nil {
|
|
t.Fatal("New() error = nil")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestRelativeSourceReportCoordinatesAgree(t *testing.T) {
|
|
// Resolve a relative tree from a temporary working directory, preserving target-relative paths.
|
|
root := t.TempDir()
|
|
writeSourceFile(t, root, filepath.Join("tree", "nested", "data"), []byte("relative"))
|
|
target := filepath.Join(root, "target")
|
|
if err := os.Mkdir(target, 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Chdir(root)
|
|
entries := walkTestEntries(t, []string{"tree"}, []string{target})
|
|
if len(entries) != 1 {
|
|
t.Fatalf("entries=%v", entries)
|
|
}
|
|
entry := entries[0]
|
|
resolved := filepath.Join(append([]string{entry.base}, entry.path...)...)
|
|
if !filepath.IsAbs(entry.base) || resolved != entry.source || entry.source != filepath.Join(root, "tree", "nested", "data") {
|
|
t.Fatalf("coordinates: base=%q path=%v full=%q", entry.base, entry.path, entry.source)
|
|
}
|
|
if entry.targets[0] != filepath.Join(target, "tree", "nested", "data") {
|
|
t.Fatal(entry.targets)
|
|
}
|
|
}
|