mirror of
https://github.com/samuelncui/acp.git
synced 2026-11-08 04:21:16 +00:00
fix: emit report rows in a deterministic order
NewReportGetter rendered its rows by ranging over a map, so two runs over the same items could
write different documents even though every row was identical. The getter now emits the rows in the
order of the joined relative path that keys them, and a test renders the same set of rows eight
times so a map order cannot pass by chance.
The README states what the document actually contains while it is being corrected: an empty files or
errors array is omitted, so a run without rows and without pipeline problems writes `{}`.
This commit is contained in:
1 parent
e025d57d9d
commit
25500fffe5
4 files changed
+56
-13
No files matched your search
@@ -68,8 +68,9 @@ entry point is `NewStream` with `Submit`/`Close`/`Wait`; `Run` and `BatchSource`
|
||||
- A report row keeps the `af05f05c` shape: `Base` is the directory the source-relative `Path`
|
||||
segments are resolved against, so the JSON carries `path` as an array. `FullPath` and
|
||||
`SignatureCacheHit` are additive fields, `NewReportGetter` keys a row by the joined relative
|
||||
path, and the push engine owns no report row at all — the shell fills the row from its own
|
||||
compatibility item, which carries the base and the segments its walk knows.
|
||||
path and emits the rows in that key's order, and the push engine owns no report row at all — the
|
||||
shell fills the row from its own compatibility item, which carries the base and the segments its
|
||||
walk knows.
|
||||
- The shell publishes one terminal report row per item, so a failed item still has a row, with
|
||||
the item-level failure under the empty target key.
|
||||
- `cmd/acp` uses the shell and exits non-zero when any item, target or pipeline failure
|
||||
|
||||
@@ -466,14 +466,17 @@ acp -from-linear -to-linear example target/
|
||||
|
||||
## Report
|
||||
|
||||
`-report` writes one `files` row per item plus a top-level `errors` array. Per-item and
|
||||
per-target failures stay inside the row's `fail_target` map, keyed by the target path, or by
|
||||
the empty key when ACP could not process the item itself: an unreadable source, an item
|
||||
abandoned by a stop, or a targetless item whose content read failed. The empty key is the one
|
||||
slot no requested target can occupy, so every item-level failure has the same shape, and a
|
||||
failed item still gets a terminal row. The top-level `errors` array carries pipeline problems
|
||||
only, so a single failure is recorded once. A row names its source the `af05f05c` way: `base`
|
||||
plus the source-relative `path` array, with `full_path` as the additive whole path.
|
||||
`-report` writes one `files` row per item plus, when the run recorded a pipeline problem, a
|
||||
top-level `errors` array; an empty `files` or `errors` array is omitted, so a run with no items and
|
||||
no problems writes `{}`. Rows are ordered by the joined relative path that keys them, so the same
|
||||
items always write the same document. Per-item and per-target failures stay inside the row's
|
||||
`fail_target` map, keyed by the target path, or by the empty key when ACP could not process the item
|
||||
itself: an unreadable source, an item abandoned by a stop, or a targetless item whose content read
|
||||
failed. The empty key is the one slot no requested target can occupy, so every item-level failure
|
||||
has the same shape, and a failed item still gets a terminal row. The top-level `errors` array
|
||||
carries pipeline problems only, so a single failure is recorded once. A row names its source the
|
||||
`af05f05c` way: `base` plus the source-relative `path` array, with `full_path` as the additive whole
|
||||
path.
|
||||
|
||||
`-report-indent` writes the same document with a two-space indent (`cmd/acp-rewrite` indents its
|
||||
accumulated report with a tab). The report is plain JSON:
|
||||
|
||||
@@ -3,11 +3,16 @@ package acp
|
||||
import (
|
||||
"encoding/json"
|
||||
"path"
|
||||
"sort"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type ReportGetter func() *Report
|
||||
|
||||
// NewReportGetter returns the event handler that collects one run's report and the getter that
|
||||
// renders it. A row is keyed by the joined relative path of its item, and the getter emits the rows
|
||||
// in that key's order, so the same items always produce the same document; the pipeline errors keep
|
||||
// their arrival order.
|
||||
func NewReportGetter() (EventHandler, ReportGetter) {
|
||||
var lock sync.Mutex
|
||||
jobs := make(map[string]*Job, 8)
|
||||
@@ -32,9 +37,17 @@ func NewReportGetter() (EventHandler, ReportGetter) {
|
||||
lock.Lock()
|
||||
defer lock.Unlock()
|
||||
|
||||
jobsCopyed := make([]*Job, 0, len(jobs))
|
||||
for _, j := range jobs {
|
||||
jobsCopyed = append(jobsCopyed, j)
|
||||
// Emit the rows in the order of the key that identifies them instead of the map's
|
||||
// iteration order, so two runs over the same items write the same document.
|
||||
keys := make([]string, 0, len(jobs))
|
||||
for key := range jobs {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
jobsCopyed := make([]*Job, 0, len(keys))
|
||||
for _, key := range keys {
|
||||
jobsCopyed = append(jobsCopyed, jobs[key])
|
||||
}
|
||||
|
||||
errorsCopyed := make([]*Error, 0, len(errors))
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -229,6 +230,31 @@ func TestReportToJSONStringIndentUsesSpaces(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestReportGetterOrdersRowsByTheJoinedRelativePath pins the report document's row order: a row is
|
||||
// keyed by the joined relative path, and the getter emits the rows in that key's order, so the same
|
||||
// items always render the same document instead of following the map's iteration order.
|
||||
func TestReportGetterOrdersRowsByTheJoinedRelativePath(t *testing.T) {
|
||||
handler, getter := NewReportGetter()
|
||||
|
||||
for _, segments := range [][]string{{"z.txt"}, {"nested", "b.txt"}, {"a.txt"}, {"nested", "a.txt"}} {
|
||||
handler(&EventUpdateJob{Job: &Job{Base: "/source", Path: segments}})
|
||||
}
|
||||
|
||||
want := []string{"a.txt", "nested/a.txt", "nested/b.txt", "z.txt"}
|
||||
// A map has no order, so a single render could match by chance.
|
||||
for attempt := 0; attempt < 8; attempt++ {
|
||||
report := getter()
|
||||
if len(report.Jobs) != len(want) {
|
||||
t.Fatalf("rows = %d, want %d", len(report.Jobs), len(want))
|
||||
}
|
||||
for index, key := range want {
|
||||
if got := path.Join(report.Jobs[index].Path...); got != key {
|
||||
t.Fatalf("row %d = %q, want %q (attempt %d)", index, got, key, attempt)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestRunReportsAFailedItemAsATerminalRow pins the report row of an item ACP could not process:
|
||||
// consumers resolve an item by its row, and the failure travels under the empty target key.
|
||||
func TestRunReportsAFailedItemAsATerminalRow(t *testing.T) {
|
||||
|
||||
Reference in new issue
Block a user