test: add library end-to-end coverage

This commit is contained in:
Samuel N Cui
2026-07-11 20:50:30 +08:00
parent 259f124032
commit f6dde5acb6
4 changed files with 158 additions and 7 deletions
+1 -1
View File
@@ -35,7 +35,7 @@ The current change set addresses these areas:
- select the longest matching mountpoint with directory-boundary checks;
- use `path/filepath` for host file-system paths;
- centralize platform-aware path ordering in `comparePath`;
- add semantic, race, cross-platform, and CLI end-to-end coverage.
- add semantic, race, cross-platform, CLI end-to-end, and library end-to-end coverage.
The working tree may already contain staged or unstaged changes. Preserve them and do not reset, rewrite, or discard unrelated work.
+24 -5
View File
@@ -28,9 +28,9 @@ Run the suite with the race detector:
go test -race ./...
```
## End-to-end test
## End-to-end tests
The end-to-end test builds the `acp` command, copies a directory tree through the CLI, and verifies:
The command end-to-end test builds the `acp` command, copies a directory tree through the CLI, and verifies:
- regular and empty file contents;
- nested destination paths;
@@ -38,13 +38,32 @@ The end-to-end test builds the `acp` command, copies a directory tree through th
- JSON report decoding;
- SHA256 generation for every copied file.
Run only this test with:
Run only the command test with:
```sh
go test -run '^TestACPCopyE2E$' -v .
go test -run '^TestACPCommandE2E$' -v .
```
The test is skipped when `go test` is run with `-short`.
The library end-to-end test uses the public Go API directly with two destinations and verifies:
- the complete index, prepare, copy, cleanup, and event pipeline;
- regular, empty, and nested file contents at both destinations;
- successful report status and destination sets;
- file size and exact SHA256 values.
Run only the library test with:
```sh
go test -run '^TestACPLibraryE2E$' -v .
```
Run both end-to-end tests with:
```sh
go test -run '^TestACP(Command|Library)E2E$' -v .
```
Both tests are skipped when `go test` is run with `-short`.
## Focused tests
+1 -1
View File
@@ -14,7 +14,7 @@ import (
"github.com/samuelncui/acp"
)
func TestACPCopyE2E(t *testing.T) {
func TestACPCommandE2E(t *testing.T) {
if testing.Short() {
t.Skip("skipping end-to-end test in short mode")
}
+132
View File
@@ -0,0 +1,132 @@
package acp_test
import (
"bytes"
"context"
"crypto/sha256"
"encoding/hex"
"os"
"path/filepath"
"testing"
"time"
"github.com/samuelncui/acp"
)
func TestACPLibraryE2E(t *testing.T) {
if testing.Short() {
t.Skip("skipping end-to-end test in short mode")
}
tempDir := t.TempDir()
source := filepath.Join(tempDir, "source")
targets := []string{
filepath.Join(tempDir, "target-a"),
filepath.Join(tempDir, "target-b"),
}
for _, target := range targets {
if err := os.MkdirAll(target, 0o755); err != nil {
t.Fatalf("create target directory %q: %v", target, err)
}
}
files := map[string][]byte{
"plain.txt": []byte("library copy\n"),
"empty.txt": nil,
filepath.Join("nested", "data"): {5, 4, 3, 2, 1},
}
for relativePath, data := range files {
path := filepath.Join(source, relativePath)
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
t.Fatalf("create source directory: %v", err)
}
if err := os.WriteFile(path, data, 0o644); err != nil {
t.Fatalf("write source file %q: %v", relativePath, err)
}
}
handler, getter := acp.NewReportGetter()
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
copyer, err := acp.New(
ctx,
acp.WildcardJob(acp.Source(source), acp.Target(targets...)),
acp.WithHash(true),
acp.Overwrite(true),
acp.WithEventHandler(handler),
)
if err != nil {
t.Fatalf("create copyer: %v", err)
}
done := make(chan struct{})
go func() {
copyer.Wait()
close(done)
}()
select {
case <-done:
case <-ctx.Done():
t.Fatalf("wait for copyer: %v", ctx.Err())
}
for _, target := range targets {
copyRoot := filepath.Join(target, filepath.Base(source))
for relativePath, want := range files {
got, err := os.ReadFile(filepath.Join(copyRoot, relativePath))
if err != nil {
t.Fatalf("read copied file %q from %q: %v", relativePath, target, err)
}
if !bytes.Equal(got, want) {
t.Fatalf("copied file %q from %q = %v, want %v", relativePath, target, got, want)
}
}
}
report := getter()
if len(report.Errors) != 0 {
t.Fatalf("report errors = %v", report.Errors)
}
if len(report.Jobs) != len(files) {
t.Fatalf("report jobs = %d, want %d", len(report.Jobs), len(files))
}
jobs := make(map[string]*acp.Job, len(report.Jobs))
for _, job := range report.Jobs {
jobs[job.FullPath] = job
}
for relativePath, data := range files {
sourcePath := filepath.Join(source, relativePath)
job, ok := jobs[sourcePath]
if !ok {
t.Fatalf("report job not found for %q", sourcePath)
}
if job.Status != acp.JobStatusFinished {
t.Fatalf("job %q status = %q", sourcePath, job.Status)
}
if len(job.FailTargets) != 0 {
t.Fatalf("job %q failures = %v", sourcePath, job.FailTargets)
}
if job.Size != int64(len(data)) {
t.Fatalf("job %q size = %d, want %d", sourcePath, job.Size, len(data))
}
wantTargets := make(map[string]struct{}, len(targets))
for _, target := range targets {
wantTargets[filepath.Join(target, filepath.Base(source), relativePath)] = struct{}{}
}
if len(job.SuccessTargets) != len(wantTargets) {
t.Fatalf("job %q success targets = %v", sourcePath, job.SuccessTargets)
}
for _, target := range job.SuccessTargets {
if _, ok := wantTargets[target]; !ok {
t.Fatalf("job %q unexpected success target %q", sourcePath, target)
}
}
sum := sha256.Sum256(data)
if want := hex.EncodeToString(sum[:]); job.SHA256 != want {
t.Fatalf("job %q SHA256 = %q, want %q", sourcePath, job.SHA256, want)
}
}
}