Add reconcile state planning core
This commit is contained in:
@@ -61,6 +61,38 @@ func TestParseValidStateWithLinks(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoveMissingOutputs(t *testing.T) {
|
||||
state, err := Parse([]byte(validStateJSON(t)))
|
||||
if err != nil {
|
||||
t.Fatalf("Parse() error = %v", err)
|
||||
}
|
||||
state.Outputs = append(state.Outputs, OutputFile{
|
||||
Path: "summary.txt",
|
||||
Kind: OutputKindSource,
|
||||
SourcePath: "summary.txt",
|
||||
SHA256: "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
Size: 10,
|
||||
CreatedAt: state.CreatedAt,
|
||||
UpdatedAt: state.UpdatedAt,
|
||||
})
|
||||
|
||||
next, changed := RemoveMissingOutputs(state, []string{"summary.txt"})
|
||||
if !changed {
|
||||
t.Fatal("RemoveMissingOutputs() changed = false, want true")
|
||||
}
|
||||
if got, want := ManagedOutputPaths(next), []string{"report.md"}; strings.Join(got, ",") != strings.Join(want, ",") {
|
||||
t.Fatalf("paths = %#v, want %#v", got, want)
|
||||
}
|
||||
|
||||
unchanged, changed := RemoveMissingOutputs(next, []string{"missing.txt"})
|
||||
if changed {
|
||||
t.Fatal("RemoveMissingOutputs() changed = true, want false")
|
||||
}
|
||||
if got, want := ManagedOutputPaths(unchanged), []string{"report.md"}; strings.Join(got, ",") != strings.Join(want, ",") {
|
||||
t.Fatalf("unchanged paths = %#v, want %#v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseNormalizesPublishedAtOffset(t *testing.T) {
|
||||
body := strings.Replace(validStateJSON(t), `"published_at": "2026-05-30T11:12:00Z"`, `"published_at": "2026-05-30T13:12:00+02:00"`, 1)
|
||||
state, err := Parse([]byte(body))
|
||||
|
||||
@@ -60,6 +60,24 @@ func ManagedOutputPaths(s DistributorState) []string {
|
||||
return paths
|
||||
}
|
||||
|
||||
func RemoveMissingOutputs(s DistributorState, missingPaths []string) (DistributorState, bool) {
|
||||
if len(missingPaths) == 0 {
|
||||
return s, false
|
||||
}
|
||||
missing := pathSet(missingPaths)
|
||||
next := s
|
||||
next.Outputs = make([]OutputFile, 0, len(s.Outputs))
|
||||
changed := false
|
||||
for _, output := range s.Outputs {
|
||||
if _, remove := missing[output.Path]; remove {
|
||||
changed = true
|
||||
continue
|
||||
}
|
||||
next.Outputs = append(next.Outputs, output)
|
||||
}
|
||||
return next, changed
|
||||
}
|
||||
|
||||
func ProjectOutputs(outputs []OutputProjection, existing []OutputFile, now time.Time) []OutputFile {
|
||||
now = now.UTC()
|
||||
files := make([]OutputFile, 0, len(outputs))
|
||||
@@ -122,6 +140,44 @@ func (s SharedRootState) AllManagedOutputPaths() []string {
|
||||
return paths
|
||||
}
|
||||
|
||||
func RemoveMissingSharedRootOwnerOutputs(s SharedRootState, scope OwnerScope, missingPaths []string) (SharedRootState, bool) {
|
||||
if len(missingPaths) == 0 {
|
||||
return s, false
|
||||
}
|
||||
missing := pathSet(missingPaths)
|
||||
next := s
|
||||
next.Outputs = make([]SharedRootOutputFile, 0, len(s.Outputs))
|
||||
changed := false
|
||||
for _, output := range s.Outputs {
|
||||
if output.Owner == scope {
|
||||
if _, remove := missing[output.Path]; remove {
|
||||
changed = true
|
||||
continue
|
||||
}
|
||||
}
|
||||
next.Outputs = append(next.Outputs, output)
|
||||
}
|
||||
return next, changed
|
||||
}
|
||||
|
||||
func RemoveMissingSharedRootOutputs(s SharedRootState, missingPaths []string) (SharedRootState, bool) {
|
||||
if len(missingPaths) == 0 {
|
||||
return s, false
|
||||
}
|
||||
missing := pathSet(missingPaths)
|
||||
next := s
|
||||
next.Outputs = make([]SharedRootOutputFile, 0, len(s.Outputs))
|
||||
changed := false
|
||||
for _, output := range s.Outputs {
|
||||
if _, remove := missing[output.Path]; remove {
|
||||
changed = true
|
||||
continue
|
||||
}
|
||||
next.Outputs = append(next.Outputs, output)
|
||||
}
|
||||
return next, changed
|
||||
}
|
||||
|
||||
func (s SharedRootState) OutputOwner(path string) (OwnerScope, bool) {
|
||||
for _, output := range s.Outputs {
|
||||
if output.Path == path {
|
||||
@@ -264,3 +320,11 @@ func upsertOwner(owners []OwnerRecord, owner OwnerRecord) []OwnerRecord {
|
||||
}
|
||||
return append(next, owner)
|
||||
}
|
||||
|
||||
func pathSet(paths []string) map[string]struct{} {
|
||||
set := make(map[string]struct{}, len(paths))
|
||||
for _, path := range paths {
|
||||
set[path] = struct{}{}
|
||||
}
|
||||
return set
|
||||
}
|
||||
|
||||
@@ -132,6 +132,32 @@ func TestSharedRootOutputHelpers(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoveMissingSharedRootOwnerOutputs(t *testing.T) {
|
||||
state := validSharedRootState(t)
|
||||
archive := CurrentOwnerScope("reports", "archive")
|
||||
next, changed := RemoveMissingSharedRootOwnerOutputs(state, archive, []string{"report.md", "report.html"})
|
||||
if !changed {
|
||||
t.Fatal("RemoveMissingSharedRootOwnerOutputs() changed = false, want true")
|
||||
}
|
||||
if got, want := next.AllManagedOutputPaths(), []string{"report.html"}; strings.Join(got, ",") != strings.Join(want, ",") {
|
||||
t.Fatalf("paths = %#v, want %#v", got, want)
|
||||
}
|
||||
if _, ok := next.OutputOwner("report.html"); !ok {
|
||||
t.Fatal("report.html owner missing, want unrelated owner preserved")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoveMissingSharedRootOutputs(t *testing.T) {
|
||||
state := validSharedRootState(t)
|
||||
next, changed := RemoveMissingSharedRootOutputs(state, []string{"report.md", "report.html"})
|
||||
if !changed {
|
||||
t.Fatal("RemoveMissingSharedRootOutputs() changed = false, want true")
|
||||
}
|
||||
if got := next.AllManagedOutputPaths(); len(got) != 0 {
|
||||
t.Fatalf("paths = %#v, want none", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSharedRootProjectAndMergeOwnerOutputs(t *testing.T) {
|
||||
state := validSharedRootState(t)
|
||||
archive := CurrentOwnerScope("reports", "archive")
|
||||
|
||||
Reference in New Issue
Block a user