Skip to content

Commit

Permalink
refactor: Successor getting with separation of concerns
Browse files Browse the repository at this point in the history
Signed-off-by: Terry Howe <terrylhowe@gmail.com>
  • Loading branch information
TerryHowe committed Aug 3, 2024
1 parent 82949a2 commit 12c4d07
Show file tree
Hide file tree
Showing 11 changed files with 351 additions and 168 deletions.
20 changes: 16 additions & 4 deletions cmd/oras/internal/display/status/text.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,13 @@ package status

import (
"context"
"oras.land/oras/internal/graph"
"sync"

"oras.land/oras/cmd/oras/internal/output"

ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"oras.land/oras-go/v2"
"oras.land/oras-go/v2/content"
"oras.land/oras/cmd/oras/internal/output"
)

// TextPushHandler handles text status output for push events.
Expand Down Expand Up @@ -65,9 +65,15 @@ func (ph *TextPushHandler) UpdateCopyOptions(opts *oras.CopyGraphOptions, fetche
}
opts.PostCopy = func(ctx context.Context, desc ocispec.Descriptor) error {
committed.Store(desc.Digest.String(), desc.Annotations[ocispec.AnnotationTitle])
if err := output.PrintSuccessorStatus(ctx, desc, fetcher, committed, ph.printer.StatusPrinter(PushPromptSkipped)); err != nil {
successors, err := graph.FilteredSuccessors(ctx, desc, fetcher, DeduplicatedFilter(committed))
if err != nil {
return err
}
for _, successor := range successors {
if err = ph.printer.PrintStatus(successor, PushPromptSkipped); err != nil {
return err

Check warning on line 74 in cmd/oras/internal/display/status/text.go

View check run for this annotation

Codecov / codecov/patch

cmd/oras/internal/display/status/text.go#L74

Added line #L74 was not covered by tests
}
}
return ph.printer.PrintStatus(desc, PushPromptUploaded)
}
}
Expand Down Expand Up @@ -149,9 +155,15 @@ func (ch *TextCopyHandler) PreCopy(_ context.Context, desc ocispec.Descriptor) e
// PostCopy implements PostCopy of CopyHandler.
func (ch *TextCopyHandler) PostCopy(ctx context.Context, desc ocispec.Descriptor) error {
ch.committed.Store(desc.Digest.String(), desc.Annotations[ocispec.AnnotationTitle])
if err := output.PrintSuccessorStatus(ctx, desc, ch.fetcher, ch.committed, ch.printer.StatusPrinter(copyPromptSkipped)); err != nil {
successors, err := graph.FilteredSuccessors(ctx, desc, ch.fetcher, DeduplicatedFilter(ch.committed))
if err != nil {
return err
}
for _, successor := range successors {
if err = ch.printer.PrintStatus(successor, copyPromptSkipped); err != nil {
return err

Check warning on line 164 in cmd/oras/internal/display/status/text.go

View check run for this annotation

Codecov / codecov/patch

cmd/oras/internal/display/status/text.go#L164

Added line #L164 was not covered by tests
}
}
return ch.printer.PrintStatus(desc, copyPromptCopied)
}

Expand Down
131 changes: 131 additions & 0 deletions cmd/oras/internal/display/status/text_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
/*
Copyright The ORAS Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package status

import (
"bytes"
"context"
"encoding/json"
"fmt"
"github.com/opencontainers/go-digest"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"oras.land/oras-go/v2/content/memory"
"oras.land/oras/cmd/oras/internal/output"
"oras.land/oras/internal/testutils"
"os"
"strings"
"testing"
)

var (
ctx context.Context
builder *strings.Builder
printer *output.Printer
bogus ocispec.Descriptor
memStore *memory.Store
memDesc ocispec.Descriptor
manifestDesc ocispec.Descriptor
)

func TestMain(m *testing.M) {
// memory store for testing
memStore = memory.New()
content := []byte("test")
r := bytes.NewReader(content)
memDesc = ocispec.Descriptor{
MediaType: "application/octet-stream",
Digest: digest.FromBytes(content),
Size: int64(len(content)),
}
if err := memStore.Push(context.Background(), memDesc, r); err != nil {
fmt.Println("Setup failed:", err)
os.Exit(1)
}
if err := memStore.Tag(context.Background(), memDesc, memDesc.Digest.String()); err != nil {
fmt.Println("Setup failed:", err)
os.Exit(1)
}

layer1Desc := memDesc
layer1Desc.Annotations = map[string]string{ocispec.AnnotationTitle: "layer1"}
layer2Desc := memDesc
layer2Desc.Annotations = map[string]string{ocispec.AnnotationTitle: "layer2"}
manifest := ocispec.Manifest{
MediaType: ocispec.MediaTypeImageManifest,
Layers: []ocispec.Descriptor{layer1Desc, layer2Desc},
Config: memDesc,
}
manifestContent, err := json.Marshal(&manifest)
if err != nil {
fmt.Println("Setup failed:", err)
os.Exit(1)
}
manifestDesc = ocispec.Descriptor{
MediaType: manifest.MediaType,
Size: int64(len(manifestContent)),
Digest: digest.FromBytes(manifestContent),
}
if err := memStore.Push(context.Background(), manifestDesc, strings.NewReader(string(manifestContent))); err != nil {
fmt.Println("Setup failed:", err)
os.Exit(1)
}
if err := memStore.Tag(context.Background(), memDesc, memDesc.Digest.String()); err != nil {
fmt.Println("Setup failed:", err)
os.Exit(1)
}

ctx = context.Background()
builder = &strings.Builder{}
printer = output.NewPrinter(builder, os.Stderr, false)
bogus = ocispec.Descriptor{MediaType: ocispec.MediaTypeImageManifest}
m.Run()
}

func TestTextCopyHandler_OnMounted(t *testing.T) {
fetcher := testutils.NewMockFetcher(t)
ch := NewTextCopyHandler(printer, fetcher.Fetcher)
if ch.OnMounted(ctx, fetcher.OciImage) != nil {
t.Error("OnMounted() should not return an error")
}

}

func TestTextCopyHandler_OnCopySkipped(t *testing.T) {
fetcher := testutils.NewMockFetcher(t)
ch := NewTextCopyHandler(printer, fetcher.Fetcher)
if ch.OnCopySkipped(ctx, fetcher.OciImage) != nil {
t.Error("OnCopySkipped() should not return an error")
}
}

func TestTextCopyHandler_PostCopy(t *testing.T) {
fetcher := testutils.NewMockFetcher(t)
ch := NewTextCopyHandler(printer, fetcher.Fetcher)
if ch.PostCopy(ctx, fetcher.OciImage) != nil {
t.Error("PostCopy() should not return an error")
}
if ch.PostCopy(ctx, bogus) == nil {
t.Error("PostCopy() should return an error")
}
}

func TestTextCopyHandler_PreCopy(t *testing.T) {
fetcher := testutils.NewMockFetcher(t)
ch := NewTextCopyHandler(printer, fetcher.Fetcher)
if ch.PreCopy(ctx, fetcher.OciImage) != nil {
t.Error("PreCopy() should not return an error")
}
}
17 changes: 12 additions & 5 deletions cmd/oras/internal/display/status/tty.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,10 @@ package status

import (
"context"
"oras.land/oras/internal/graph"
"os"
"sync"

"oras.land/oras/cmd/oras/internal/output"

ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"oras.land/oras-go/v2"
"oras.land/oras-go/v2/content"
Expand Down Expand Up @@ -70,9 +69,17 @@ func (ph *TTYPushHandler) UpdateCopyOptions(opts *oras.CopyGraphOptions, fetcher
}
opts.PostCopy = func(ctx context.Context, desc ocispec.Descriptor) error {
committed.Store(desc.Digest.String(), desc.Annotations[ocispec.AnnotationTitle])
return output.PrintSuccessorStatus(ctx, desc, fetcher, committed, func(d ocispec.Descriptor) error {
return ph.tracked.Prompt(d, PushPromptSkipped)
})
successors, err := graph.FilteredSuccessors(ctx, desc, fetcher, DeduplicatedFilter(committed))
if err != nil {
return err

Check warning on line 74 in cmd/oras/internal/display/status/tty.go

View check run for this annotation

Codecov / codecov/patch

cmd/oras/internal/display/status/tty.go#L74

Added line #L74 was not covered by tests
}
for _, successor := range successors {
err = ph.tracked.Prompt(successor, PushPromptSkipped)
if err != nil {
return err

Check warning on line 79 in cmd/oras/internal/display/status/tty.go

View check run for this annotation

Codecov / codecov/patch

cmd/oras/internal/display/status/tty.go#L79

Added line #L79 was not covered by tests
}
}
return nil
}
}

Expand Down
62 changes: 0 additions & 62 deletions cmd/oras/internal/display/status/tty_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,74 +16,12 @@ limitations under the License.
package status

import (
"bytes"
"context"
"encoding/json"
"fmt"
"os"
"strings"
"testing"

"github.com/opencontainers/go-digest"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"oras.land/oras-go/v2/content/memory"
)

var (
memStore *memory.Store
memDesc ocispec.Descriptor
manifestDesc ocispec.Descriptor
)

func TestMain(m *testing.M) {
// memory store for testing
memStore = memory.New()
content := []byte("test")
r := bytes.NewReader(content)
memDesc = ocispec.Descriptor{
MediaType: "application/octet-stream",
Digest: digest.FromBytes(content),
Size: int64(len(content)),
}
if err := memStore.Push(context.Background(), memDesc, r); err != nil {
fmt.Println("Setup failed:", err)
os.Exit(1)
}
if err := memStore.Tag(context.Background(), memDesc, memDesc.Digest.String()); err != nil {
fmt.Println("Setup failed:", err)
os.Exit(1)
}

layer1Desc := memDesc
layer1Desc.Annotations = map[string]string{ocispec.AnnotationTitle: "layer1"}
layer2Desc := memDesc
layer2Desc.Annotations = map[string]string{ocispec.AnnotationTitle: "layer2"}
manifest := ocispec.Manifest{
MediaType: ocispec.MediaTypeImageManifest,
Layers: []ocispec.Descriptor{layer1Desc, layer2Desc},
Config: memDesc,
}
manifestContent, err := json.Marshal(&manifest)
if err != nil {
fmt.Println("Setup failed:", err)
os.Exit(1)
}
manifestDesc = ocispec.Descriptor{
MediaType: manifest.MediaType,
Size: int64(len(manifestContent)),
Digest: digest.FromBytes(manifestContent),
}
if err := memStore.Push(context.Background(), manifestDesc, strings.NewReader(string(manifestContent))); err != nil {
fmt.Println("Setup failed:", err)
os.Exit(1)
}
if err := memStore.Tag(context.Background(), memDesc, memDesc.Digest.String()); err != nil {
fmt.Println("Setup failed:", err)
os.Exit(1)
}
m.Run()
}

func TestTTYPushHandler_OnFileLoading(t *testing.T) {
ph := NewTTYPushHandler(os.Stdout)
if ph.OnFileLoading("test") != nil {
Expand Down
16 changes: 16 additions & 0 deletions cmd/oras/internal/display/status/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ limitations under the License.

package status

import (
"sync"

ocispec "github.com/opencontainers/image-spec/specs-go/v1"
)

// Prompts for pull events.
const (
PullPromptDownloading = "Downloading"
Expand All @@ -41,3 +47,13 @@ const (
copyPromptSkipped = "Skipped"
copyPromptMounted = "Mounted"
)

// DeduplicatedFilter filters out deduplicated descriptors.
func DeduplicatedFilter(committed *sync.Map) func(desc ocispec.Descriptor) bool {
return func(desc ocispec.Descriptor) bool {
name := desc.Annotations[ocispec.AnnotationTitle]
v, ok := committed.Load(desc.Digest.String())
// committed but not printed == deduplicated
return ok && v != name
}
}
30 changes: 0 additions & 30 deletions cmd/oras/internal/output/print.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,20 +16,15 @@ limitations under the License.
package output

import (
"context"
"fmt"
"io"
"sync"

"oras.land/oras/internal/descriptor"

ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"oras.land/oras-go/v2/content"
)

// PrintFunc is the function type returned by StatusPrinter.
type PrintFunc func(ocispec.Descriptor) error

// Printer prints for status handlers.
type Printer struct {
out io.Writer
Expand Down Expand Up @@ -92,28 +87,3 @@ func (p *Printer) PrintStatus(desc ocispec.Descriptor, status string) error {
}
return p.Println(status, descriptor.ShortDigest(desc), name)
}

// StatusPrinter returns a tracking function for transfer status.
func (p *Printer) StatusPrinter(status string) PrintFunc {
return func(desc ocispec.Descriptor) error {
return p.PrintStatus(desc, status)
}
}

// PrintSuccessorStatus prints transfer status of successors.
func PrintSuccessorStatus(ctx context.Context, desc ocispec.Descriptor, fetcher content.Fetcher, committed *sync.Map, print PrintFunc) error {
successors, err := content.Successors(ctx, fetcher, desc)
if err != nil {
return err
}
for _, s := range successors {
name := s.Annotations[ocispec.AnnotationTitle]
if v, ok := committed.Load(s.Digest.String()); ok && v != name {
// Reprint status for deduplicated content
if err := print(s); err != nil {
return err
}
}
}
return nil
}
14 changes: 10 additions & 4 deletions cmd/oras/root/cp.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ import (
"oras.land/oras/cmd/oras/internal/display/status/track"
oerrors "oras.land/oras/cmd/oras/internal/errors"
"oras.land/oras/cmd/oras/internal/option"
"oras.land/oras/cmd/oras/internal/output"
"oras.land/oras/internal/docker"
"oras.land/oras/internal/graph"
"oras.land/oras/internal/listener"
Expand Down Expand Up @@ -198,9 +197,16 @@ func doCopy(ctx context.Context, copyHandler status.CopyHandler, src oras.ReadOn
}
extendedCopyOptions.PostCopy = func(ctx context.Context, desc ocispec.Descriptor) error {
committed.Store(desc.Digest.String(), desc.Annotations[ocispec.AnnotationTitle])
return output.PrintSuccessorStatus(ctx, desc, tracked, committed, func(desc ocispec.Descriptor) error {
return tracked.Prompt(desc, promptSkipped)
})
successors, err := graph.FilteredSuccessors(ctx, desc, tracked, status.DeduplicatedFilter(committed))
if err != nil {
return err

Check warning on line 202 in cmd/oras/root/cp.go

View check run for this annotation

Codecov / codecov/patch

cmd/oras/root/cp.go#L202

Added line #L202 was not covered by tests
}
for _, successor := range successors {
if err = tracked.Prompt(successor, promptSkipped); err != nil {
return err

Check warning on line 206 in cmd/oras/root/cp.go

View check run for this annotation

Codecov / codecov/patch

cmd/oras/root/cp.go#L205-L206

Added lines #L205 - L206 were not covered by tests
}
}
return nil
}
extendedCopyOptions.OnMounted = func(ctx context.Context, desc ocispec.Descriptor) error {
committed.Store(desc.Digest.String(), desc.Annotations[ocispec.AnnotationTitle])
Expand Down
Loading

0 comments on commit 12c4d07

Please sign in to comment.