diff --git a/cmd/internal-tools/campaign/campaign_test.go b/cmd/internal-tools/campaign/campaign_test.go index bf64b31..dedf64e 100644 --- a/cmd/internal-tools/campaign/campaign_test.go +++ b/cmd/internal-tools/campaign/campaign_test.go @@ -138,14 +138,19 @@ func TestRunCampaign_RecordsDispatchedActionsNotSteps(t *testing.T) { } // A run that never produced a readable trace still has to carry the field, so -// analysis can tell a zero-action run from a file written before the count. -func TestRunRecord_AlwaysCarriesTheActionCount(t *testing.T) { +// analysis can tell a zero-action run from a file written before the count. The +// same holds for the unattributed count: a run whose every action named its +// producer says so with a zero, and a file that says nothing is one recorded +// before actions named one at all. +func TestRunRecord_AlwaysCarriesTheActionCounts(t *testing.T) { body, err := json.Marshal(runRecord{Seed: 7, TraceError: "no run directory with meta.json"}) if err != nil { t.Fatal(err) } - if !strings.Contains(string(body), `"actions":0`) { - t.Errorf("record %s omits the action count", body) + for _, field := range []string{`"actions":0`, `"unattributed_actions":0`} { + if !strings.Contains(string(body), field) { + t.Errorf("record %s omits %s", body, field) + } } } diff --git a/cmd/internal-tools/campaign/summary.go b/cmd/internal-tools/campaign/summary.go index cbdf5aa..cd842a4 100644 --- a/cmd/internal-tools/campaign/summary.go +++ b/cmd/internal-tools/campaign/summary.go @@ -31,8 +31,11 @@ type traceSummary struct { // UnattributedActions counts the dispatched steps whose action names no // producer, which only a trace recorded before actions carried one can do. // Such a run's Actions is the count it was already reported with rather than - // a setup-excluding one, and this is what says so. - UnattributedActions int `json:"unattributed_actions,omitempty"` + // a setup-excluding one, and this is what says so. It is written even when + // it is zero, because a record that omits it is one the analysis has to read + // as unattributable and a run where every action named a producer is the + // opposite of that. + UnattributedActions int `json:"unattributed_actions"` FirstViolationOriginStep *int `json:"first_violation_origin_step"` FirstViolationDetectedStep *int `json:"first_violation_detected_step"` FirstViolationProperties []string `json:"first_violation_properties,omitempty"`