From 54f841f5f26bea092a876f99f28e4c3b5ba33a9c Mon Sep 17 00:00:00 2001 From: Terry Howe Date: Fri, 26 Jul 2024 13:23:08 -0600 Subject: [PATCH] refactor: Successor getting with separation of concerns Signed-off-by: Terry Howe --- .gitignore | 1 + Makefile | 3 +- .../display/status/progress/manager_test.go | 2 +- .../display/status/progress/status_test.go | 2 +- cmd/oras/internal/display/status/text.go | 20 ++- cmd/oras/internal/display/status/text_test.go | 131 ++++++++++++++++++ .../display/status/track/target_test.go | 2 +- cmd/oras/internal/display/status/tty.go | 17 ++- .../display/status/tty_console_test.go | 2 +- cmd/oras/internal/display/status/tty_test.go | 62 --------- cmd/oras/internal/display/status/utils.go | 16 +++ cmd/oras/internal/option/common_unix_test.go | 2 +- cmd/oras/internal/output/print.go | 30 ---- cmd/oras/root/blob/fetch_test.go | 2 +- cmd/oras/root/blob/push_test.go | 2 +- cmd/oras/root/cp.go | 14 +- cmd/oras/root/cp_test.go | 2 +- internal/descriptor/descriptor.go | 1 - internal/graph/graph.go | 16 +++ internal/graph/graph_test.go | 111 +++++++-------- .../testutils/console.go | 0 internal/testutils/fetcher.go | 101 ++++++++++++++ 22 files changed, 362 insertions(+), 177 deletions(-) create mode 100644 cmd/oras/internal/display/status/text_test.go rename cmd/oras/internal/display/status/console/testutils/testutils.go => internal/testutils/console.go (100%) create mode 100644 internal/testutils/fetcher.go diff --git a/.gitignore b/.gitignore index 93f2f5cbf..3962ba8c1 100644 --- a/.gitignore +++ b/.gitignore @@ -36,6 +36,7 @@ debug # Custom coverage.txt +coverage-all.txt test/e2e/coverage.txt **/covcounters.* **/covmeta.* diff --git a/Makefile b/Makefile index ca72c5b4b..7ff99ec3d 100644 --- a/Makefile +++ b/Makefile @@ -33,7 +33,8 @@ LDFLAGS += -X $(PROJECT_PKG)/internal/version.GitTreeState=${GIT_DIRTY} .PHONY: test test: tidy vendor check-encoding ## tidy and run tests - $(GO_EXE) test -race -v -coverprofile=coverage.txt -covermode=atomic -coverpkg=./... ./... + $(GO_EXE) test -race -v -coverprofile=coverage-all.txt -covermode=atomic -coverpkg=./... ./... + @grep -v testutil coverage-all.txt >coverage.txt; rm -f coverage-all.txt .PHONY: teste2e teste2e: ## run end to end tests diff --git a/cmd/oras/internal/display/status/progress/manager_test.go b/cmd/oras/internal/display/status/progress/manager_test.go index dc0b52890..01d2e4835 100644 --- a/cmd/oras/internal/display/status/progress/manager_test.go +++ b/cmd/oras/internal/display/status/progress/manager_test.go @@ -22,7 +22,7 @@ import ( "testing" "oras.land/oras/cmd/oras/internal/display/status/console" - "oras.land/oras/cmd/oras/internal/display/status/console/testutils" + "oras.land/oras/internal/testutils" ) func Test_manager_render(t *testing.T) { diff --git a/cmd/oras/internal/display/status/progress/status_test.go b/cmd/oras/internal/display/status/progress/status_test.go index 9f8223f6e..8cb8ece6c 100644 --- a/cmd/oras/internal/display/status/progress/status_test.go +++ b/cmd/oras/internal/display/status/progress/status_test.go @@ -23,8 +23,8 @@ import ( ocispec "github.com/opencontainers/image-spec/specs-go/v1" "oras.land/oras/cmd/oras/internal/display/status/console" - "oras.land/oras/cmd/oras/internal/display/status/console/testutils" "oras.land/oras/cmd/oras/internal/display/status/progress/humanize" + "oras.land/oras/internal/testutils" ) func Test_status_String(t *testing.T) { diff --git a/cmd/oras/internal/display/status/text.go b/cmd/oras/internal/display/status/text.go index c2f35af62..5e5b6c20d 100644 --- a/cmd/oras/internal/display/status/text.go +++ b/cmd/oras/internal/display/status/text.go @@ -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. @@ -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 + } + } return ph.printer.PrintStatus(desc, PushPromptUploaded) } } @@ -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 + } + } return ch.printer.PrintStatus(desc, copyPromptCopied) } diff --git a/cmd/oras/internal/display/status/text_test.go b/cmd/oras/internal/display/status/text_test.go new file mode 100644 index 000000000..47d9d6282 --- /dev/null +++ b/cmd/oras/internal/display/status/text_test.go @@ -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") + } +} diff --git a/cmd/oras/internal/display/status/track/target_test.go b/cmd/oras/internal/display/status/track/target_test.go index 3654ba482..7debc6a04 100644 --- a/cmd/oras/internal/display/status/track/target_test.go +++ b/cmd/oras/internal/display/status/track/target_test.go @@ -28,7 +28,7 @@ import ( "oras.land/oras-go/v2" "oras.land/oras-go/v2/content/memory" "oras.land/oras-go/v2/registry/remote" - "oras.land/oras/cmd/oras/internal/display/status/console/testutils" + "oras.land/oras/internal/testutils" ) type testReferenceGraphTarget struct { diff --git a/cmd/oras/internal/display/status/tty.go b/cmd/oras/internal/display/status/tty.go index fa7e4345c..9269717a1 100644 --- a/cmd/oras/internal/display/status/tty.go +++ b/cmd/oras/internal/display/status/tty.go @@ -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" @@ -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 + } + for _, successor := range successors { + err = ph.tracked.Prompt(successor, PushPromptSkipped) + if err != nil { + return err + } + } + return nil } } diff --git a/cmd/oras/internal/display/status/tty_console_test.go b/cmd/oras/internal/display/status/tty_console_test.go index 49166b87a..3b7e853d4 100644 --- a/cmd/oras/internal/display/status/tty_console_test.go +++ b/cmd/oras/internal/display/status/tty_console_test.go @@ -21,8 +21,8 @@ import ( "context" "oras.land/oras-go/v2" "oras.land/oras-go/v2/content/memory" - "oras.land/oras/cmd/oras/internal/display/status/console/testutils" "oras.land/oras/cmd/oras/internal/display/status/track" + "oras.land/oras/internal/testutils" "testing" ) diff --git a/cmd/oras/internal/display/status/tty_test.go b/cmd/oras/internal/display/status/tty_test.go index cc6fc6935..f611bcd33 100644 --- a/cmd/oras/internal/display/status/tty_test.go +++ b/cmd/oras/internal/display/status/tty_test.go @@ -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 { diff --git a/cmd/oras/internal/display/status/utils.go b/cmd/oras/internal/display/status/utils.go index 0b7f244e1..453a1acc6 100644 --- a/cmd/oras/internal/display/status/utils.go +++ b/cmd/oras/internal/display/status/utils.go @@ -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" @@ -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 + } +} diff --git a/cmd/oras/internal/option/common_unix_test.go b/cmd/oras/internal/option/common_unix_test.go index 82d8f0ed4..7f5ae1679 100644 --- a/cmd/oras/internal/option/common_unix_test.go +++ b/cmd/oras/internal/option/common_unix_test.go @@ -20,7 +20,7 @@ package option import ( "testing" - "oras.land/oras/cmd/oras/internal/display/status/console/testutils" + "oras.land/oras/internal/testutils" ) func TestCommon_parseTTY(t *testing.T) { diff --git a/cmd/oras/internal/output/print.go b/cmd/oras/internal/output/print.go index a3f6a42fc..d7e486866 100644 --- a/cmd/oras/internal/output/print.go +++ b/cmd/oras/internal/output/print.go @@ -16,7 +16,6 @@ limitations under the License. package output import ( - "context" "fmt" "io" "sync" @@ -24,12 +23,8 @@ import ( "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 @@ -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 -} diff --git a/cmd/oras/root/blob/fetch_test.go b/cmd/oras/root/blob/fetch_test.go index 3a9e11d1d..d51db2f03 100644 --- a/cmd/oras/root/blob/fetch_test.go +++ b/cmd/oras/root/blob/fetch_test.go @@ -25,7 +25,7 @@ import ( "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/display/status/console/testutils" + "oras.land/oras/internal/testutils" ) func Test_fetchBlobOptions_doFetch(t *testing.T) { diff --git a/cmd/oras/root/blob/push_test.go b/cmd/oras/root/blob/push_test.go index 42edccdc5..154f3ae61 100644 --- a/cmd/oras/root/blob/push_test.go +++ b/cmd/oras/root/blob/push_test.go @@ -27,7 +27,7 @@ import ( "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/display/status/console/testutils" + "oras.land/oras/internal/testutils" ) func Test_pushBlobOptions_doPush(t *testing.T) { diff --git a/cmd/oras/root/cp.go b/cmd/oras/root/cp.go index 5f04adf67..9f0fd6fa4 100644 --- a/cmd/oras/root/cp.go +++ b/cmd/oras/root/cp.go @@ -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" @@ -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 + } + for _, successor := range successors { + if err = tracked.Prompt(successor, promptSkipped); err != nil { + return err + } + } + return nil } extendedCopyOptions.OnMounted = func(ctx context.Context, desc ocispec.Descriptor) error { committed.Store(desc.Digest.String(), desc.Annotations[ocispec.AnnotationTitle]) diff --git a/cmd/oras/root/cp_test.go b/cmd/oras/root/cp_test.go index c347f05c9..c02ef5ea1 100644 --- a/cmd/oras/root/cp_test.go +++ b/cmd/oras/root/cp_test.go @@ -34,7 +34,7 @@ import ( ocispec "github.com/opencontainers/image-spec/specs-go/v1" "oras.land/oras-go/v2/content/memory" "oras.land/oras-go/v2/registry/remote" - "oras.land/oras/cmd/oras/internal/display/status/console/testutils" + "oras.land/oras/internal/testutils" ) var ( diff --git a/internal/descriptor/descriptor.go b/internal/descriptor/descriptor.go index 46ad93249..84806fed2 100644 --- a/internal/descriptor/descriptor.go +++ b/internal/descriptor/descriptor.go @@ -18,7 +18,6 @@ package descriptor import ( "github.com/opencontainers/go-digest" ocispec "github.com/opencontainers/image-spec/specs-go/v1" - "oras.land/oras/internal/docker" ) diff --git a/internal/graph/graph.go b/internal/graph/graph.go index acaf42171..5041c2a2d 100644 --- a/internal/graph/graph.go +++ b/internal/graph/graph.go @@ -130,3 +130,19 @@ func FindPredecessors(ctx context.Context, src oras.ReadOnlyGraphTarget, descs [ } return referrers, nil } + +// FilteredSuccessors fetches successors and returns filtered ones. +func FilteredSuccessors(ctx context.Context, desc ocispec.Descriptor, fetcher content.Fetcher, filter func(ocispec.Descriptor) bool) ([]ocispec.Descriptor, error) { + allSuccessors, err := content.Successors(ctx, fetcher, desc) + if err != nil { + return nil, err + } + + var successors []ocispec.Descriptor + for _, s := range allSuccessors { + if filter(s) { + successors = append(successors, s) + } + } + return successors, nil +} diff --git a/internal/graph/graph_test.go b/internal/graph/graph_test.go index 2a15c5bba..9cd95b2ee 100644 --- a/internal/graph/graph_test.go +++ b/internal/graph/graph_test.go @@ -16,75 +16,19 @@ limitations under the License. package graph import ( - "bytes" "context" - "encoding/json" - "github.com/opencontainers/go-digest" - "oras.land/oras-go/v2/content/memory" "reflect" "testing" ocispec "github.com/opencontainers/image-spec/specs-go/v1" "oras.land/oras-go/v2/content" "oras.land/oras/internal/docker" + "oras.land/oras/internal/testutils" ) -type contentFetcher struct { - content.Fetcher -} - -func newTestFetcher(t *testing.T) (subject, config, ociImage, dockerImage, index ocispec.Descriptor, fetcher content.Fetcher) { - var blobs [][]byte - ctx := context.Background() - memoryStorage := memory.New() - appendBlob := func(mediaType string, blob []byte) ocispec.Descriptor { - blobs = append(blobs, blob) - desc := ocispec.Descriptor{ - MediaType: mediaType, - Digest: digest.FromBytes(blob), - Size: int64(len(blob)), - } - if err := memoryStorage.Push(ctx, desc, bytes.NewReader(blob)); err != nil { - t.Errorf("Error pushing %v\n", err) - } - return desc - } - generateImage := func(subject *ocispec.Descriptor, mediaType string, config ocispec.Descriptor, layers ...ocispec.Descriptor) ocispec.Descriptor { - manifest := ocispec.Manifest{ - MediaType: mediaType, - Subject: subject, - Config: config, - Layers: layers, - } - manifestJSON, err := json.Marshal(manifest) - if err != nil { - t.Fatal(err) - } - return appendBlob(mediaType, manifestJSON) - } - generateIndex := func(manifests ...ocispec.Descriptor) ocispec.Descriptor { - index := ocispec.Index{ - Manifests: manifests, - } - indexJSON, err := json.Marshal(index) - if err != nil { - t.Fatal(err) - } - return appendBlob(ocispec.MediaTypeImageIndex, indexJSON) - } - - subject = appendBlob(ocispec.MediaTypeImageLayer, []byte("blob")) - imageType := "test.image" - config = appendBlob(imageType, []byte("config content")) - ociImage = generateImage(&subject, ocispec.MediaTypeImageManifest, config) - dockerImage = generateImage(&subject, docker.MediaTypeManifest, config) - index = generateIndex(subject) - - return subject, config, ociImage, dockerImage, index, &contentFetcher{Fetcher: memoryStorage} -} - func TestSuccessors(t *testing.T) { - subject, config, ociImage, dockerImage, index, fetcher := newTestFetcher(t) + mockFetcher := testutils.NewMockFetcher(t) + fetcher := mockFetcher.Fetcher ctx := context.Background() type args struct { ctx context.Context @@ -101,9 +45,9 @@ func TestSuccessors(t *testing.T) { }{ {"should failed to get non-existent OCI image", args{ctx, fetcher, ocispec.Descriptor{MediaType: ocispec.MediaTypeImageManifest}}, nil, nil, nil, true}, {"should failed to get non-existent docker image", args{ctx, fetcher, ocispec.Descriptor{MediaType: docker.MediaTypeManifest}}, nil, nil, nil, true}, - {"should get success of a docker image", args{ctx, fetcher, dockerImage}, nil, &subject, &config, false}, - {"should get success of an OCI image", args{ctx, fetcher, ociImage}, nil, &subject, &config, false}, - {"should get success of an index", args{ctx, fetcher, index}, []ocispec.Descriptor{subject}, nil, nil, false}, + {"should get success of a docker image", args{ctx, fetcher, mockFetcher.DockerImage}, nil, &mockFetcher.Subject, &mockFetcher.Config, false}, + {"should get success of an OCI image", args{ctx, fetcher, mockFetcher.OciImage}, nil, &mockFetcher.Subject, &mockFetcher.Config, false}, + {"should get success of an index", args{ctx, fetcher, mockFetcher.Index}, []ocispec.Descriptor{mockFetcher.Subject}, nil, nil, false}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -124,3 +68,46 @@ func TestSuccessors(t *testing.T) { }) } } + +func TestDescriptor_GetSuccessors(t *testing.T) { + mockFetcher := testutils.NewMockFetcher(t) + + allFilter := func(ocispec.Descriptor) bool { + return true + } + got, err := FilteredSuccessors(context.Background(), mockFetcher.OciImage, mockFetcher.Fetcher, allFilter) + if nil != err { + t.Errorf("FilteredSuccessors unexpected error %v", err) + } + if len(got) != 2 { + t.Errorf("Expected 2 successors got %v", len(got)) + } + if mockFetcher.Subject.Digest != got[0].Digest { + t.Errorf("FilteredSuccessors got %v, want %v", got[0], mockFetcher.Subject) + } + if mockFetcher.Config.Digest != got[1].Digest { + t.Errorf("FilteredSuccessors got %v, want %v", got[1], mockFetcher.Subject) + } + + noConfig := func(desc ocispec.Descriptor) bool { + return desc.Digest != mockFetcher.Config.Digest + } + got, err = FilteredSuccessors(context.Background(), mockFetcher.OciImage, mockFetcher.Fetcher, noConfig) + if nil != err { + t.Errorf("FilteredSuccessors unexpected error %v", err) + } + if len(got) != 1 { + t.Errorf("Expected 1 successors got %v", len(got)) + } + if mockFetcher.Subject.Digest != got[0].Digest { + t.Errorf("FilteredSuccessors got %v, want %v", got[0], mockFetcher.Subject) + } + + got, err = FilteredSuccessors(context.Background(), ocispec.Descriptor{MediaType: ocispec.MediaTypeImageManifest}, mockFetcher.Fetcher, allFilter) + if nil == err { + t.Error("FilteredSuccessors expected error") + } + if got != nil { + t.Errorf("FilteredSuccessors unexpected %v", got) + } +} diff --git a/cmd/oras/internal/display/status/console/testutils/testutils.go b/internal/testutils/console.go similarity index 100% rename from cmd/oras/internal/display/status/console/testutils/testutils.go rename to internal/testutils/console.go diff --git a/internal/testutils/fetcher.go b/internal/testutils/fetcher.go new file mode 100644 index 000000000..a0f8b636d --- /dev/null +++ b/internal/testutils/fetcher.go @@ -0,0 +1,101 @@ +/* +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 testutils + +import ( + "bytes" + "context" + "encoding/json" + "github.com/opencontainers/go-digest" + ocispec "github.com/opencontainers/image-spec/specs-go/v1" + "oras.land/oras-go/v2/content" + "oras.land/oras-go/v2/content/memory" + "oras.land/oras/internal/docker" + "testing" +) + +type MockFetcher struct { + t *testing.T + store *memory.Store + Fetcher content.Fetcher + Subject ocispec.Descriptor + Config ocispec.Descriptor + OciImage ocispec.Descriptor + DockerImage ocispec.Descriptor + Index ocispec.Descriptor +} + +// NewMockFetcher creates a MockFetcher and populates it. +func NewMockFetcher(t *testing.T) (mockFetcher MockFetcher) { + mockFetcher = MockFetcher{store: memory.New(), t: t} + mockFetcher.Subject = mockFetcher.PushBlob(ocispec.MediaTypeImageLayer, []byte("blob")) + imageType := "test.image" + mockFetcher.Config = mockFetcher.PushBlob(imageType, []byte("config content")) + mockFetcher.OciImage = mockFetcher.PushOCIImage(&mockFetcher.Subject, mockFetcher.Config) + mockFetcher.DockerImage = mockFetcher.PushDockerImage(&mockFetcher.Subject, mockFetcher.Config) + mockFetcher.Index = mockFetcher.PushIndex(mockFetcher.Subject) + mockFetcher.Fetcher = mockFetcher.store + return mockFetcher +} + +// PushBlob pushes a blob to the memory store. +func (mf *MockFetcher) PushBlob(mediaType string, blob []byte) ocispec.Descriptor { + desc := ocispec.Descriptor{ + MediaType: mediaType, + Digest: digest.FromBytes(blob), + Size: int64(len(blob)), + } + if err := mf.store.Push(context.Background(), desc, bytes.NewReader(blob)); err != nil { + mf.t.Fatal(err) + } + return desc +} + +func (mf *MockFetcher) pushImage(subject *ocispec.Descriptor, mediaType string, config ocispec.Descriptor, layers ...ocispec.Descriptor) ocispec.Descriptor { + manifest := ocispec.Manifest{ + MediaType: mediaType, + Subject: subject, + Config: config, + Layers: layers, + } + manifestJSON, err := json.Marshal(manifest) + if err != nil { + mf.t.Fatal(err) + } + return mf.PushBlob(mediaType, manifestJSON) +} + +// PushOCIImage pushes the given subject, config and layers as a OCI image. +func (mf *MockFetcher) PushOCIImage(subject *ocispec.Descriptor, config ocispec.Descriptor, layers ...ocispec.Descriptor) ocispec.Descriptor { + return mf.pushImage(subject, ocispec.MediaTypeImageManifest, config, layers...) +} + +// PushDockerImage pushes the given subject, config and layers as a Docker image. +func (mf *MockFetcher) PushDockerImage(subject *ocispec.Descriptor, config ocispec.Descriptor, layers ...ocispec.Descriptor) ocispec.Descriptor { + return mf.pushImage(subject, docker.MediaTypeManifest, config, layers...) +} + +// PushIndex pushes the manifests as an index. +func (mf *MockFetcher) PushIndex(manifests ...ocispec.Descriptor) ocispec.Descriptor { + index := ocispec.Index{ + Manifests: manifests, + } + indexJSON, err := json.Marshal(index) + if err != nil { + mf.t.Fatal(err) + } + return mf.PushBlob(ocispec.MediaTypeImageIndex, indexJSON) +}