From a31a20890a28b164a64eb0bf7bb7461411b1c8dd Mon Sep 17 00:00:00 2001 From: Aleksa Sarai Date: Fri, 4 Apr 2025 03:45:18 +1100 Subject: [PATCH 1/9] gha: switch from CentOS to AlmaLinux for image tests Signed-off-by: Aleksa Sarai --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 54ea336d..f87a4323 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -149,7 +149,7 @@ jobs: matrix: image: - opensuse/leap:latest - - centos:latest + - almalinux:latest - debian:latest - ubuntu:latest - fedora:latest From b4ca5a10ba1ae1221e9d9871e131d4cdb8f93e78 Mon Sep 17 00:00:00 2001 From: Aleksa Sarai Date: Thu, 3 Apr 2025 23:31:36 +1100 Subject: [PATCH 2/9] repack: remove TODO for non-distributable layers The non-distributable stuff has all been deprecated by image-spec, so there's no need to support it yet. Signed-off-by: Aleksa Sarai --- cmd/umoci/insert.go | 2 -- cmd/umoci/raw-add-layer.go | 2 -- repack.go | 2 -- 3 files changed, 6 deletions(-) diff --git a/cmd/umoci/insert.go b/cmd/umoci/insert.go index 66f2731d..e77c3d4a 100644 --- a/cmd/umoci/insert.go +++ b/cmd/umoci/insert.go @@ -188,8 +188,6 @@ func insert(ctx *cli.Context) error { } } - // TODO: We should add a flag to allow for a new layer to be made - // non-distributable. if _, err := mutator.Add(context.Background(), ispec.MediaTypeImageLayer, reader, history, mutate.GzipCompressor, nil); err != nil { return fmt.Errorf("add diff layer: %w", err) } diff --git a/cmd/umoci/raw-add-layer.go b/cmd/umoci/raw-add-layer.go index 1183c913..15ee4aab 100644 --- a/cmd/umoci/raw-add-layer.go +++ b/cmd/umoci/raw-add-layer.go @@ -154,8 +154,6 @@ func rawAddLayer(ctx *cli.Context) error { } } - // TODO: We should add a flag to allow for a new layer to be made - // non-distributable. if _, err := mutator.Add(context.Background(), ispec.MediaTypeImageLayer, newLayer, history, mutate.GzipCompressor, nil); err != nil { return fmt.Errorf("add diff layer: %w", err) } diff --git a/repack.go b/repack.go index 7b48da4f..a66db075 100644 --- a/repack.go +++ b/repack.go @@ -112,8 +112,6 @@ func Repack(engineExt casext.Engine, tagName string, bundlePath string, meta Met } defer reader.Close() - // TODO: We should add a flag to allow for a new layer to be made - // non-distributable. if _, err := mutator.Add(context.Background(), ispec.MediaTypeImageLayer, reader, history, mutate.GzipCompressor, nil); err != nil { return fmt.Errorf("add diff layer: %w", err) } From 15fa55f64ee480ce5d03ecd8df799c5427219f97 Mon Sep 17 00:00:00 2001 From: Aleksa Sarai Date: Tue, 1 Apr 2025 22:37:59 +1100 Subject: [PATCH 3/9] mutate: merge add and Add add() was only ever used by Add() and the logic was already kind of poorly split between them, so just merge the implementations. Signed-off-by: Aleksa Sarai --- mutate/mutate.go | 50 ++++++++++++++++-------------------------------- 1 file changed, 17 insertions(+), 33 deletions(-) diff --git a/mutate/mutate.go b/mutate/mutate.go index a975110e..eb444491 100644 --- a/mutate/mutate.go +++ b/mutate/mutate.go @@ -254,53 +254,37 @@ func (m *Mutator) appendToConfig(history *ispec.History, layerDiffID digest.Dige } } -// add adds the given layer to the CAS, and mutates the configuration to -// include the diffID. The returned string is the digest of the *compressed* -// layer (which is compressed by us). -func (m *Mutator) add(ctx context.Context, reader io.Reader, history *ispec.History, compressor Compressor) (digest.Digest, int64, error) { +// Add adds a layer to the image, by reading the layer changeset blob from the +// provided reader. The stream must not be compressed, as it is used to +// generate the DiffIDs for the image metatadata. The provided history entry is +// appended to the image's history and should correspond to what operations +// were made to the configuration. +func (m *Mutator) Add(ctx context.Context, mediaType string, r io.Reader, history *ispec.History, compressor Compressor, annotations map[string]string) (ispec.Descriptor, error) { + desc := ispec.Descriptor{} if err := m.cache(ctx); err != nil { - return "", -1, fmt.Errorf("getting cache failed: %w", err) + return desc, fmt.Errorf("getting cache failed: %w", err) } diffidDigester := cas.BlobAlgorithm.Digester() - hashReader := io.TeeReader(reader, diffidDigester.Hash()) + hashReader := io.TeeReader(r, diffidDigester.Hash()) compressed, err := compressor.Compress(hashReader) if err != nil { - return "", -1, fmt.Errorf("couldn't create compression for blob: %w", err) + return desc, fmt.Errorf("couldn't create compression for blob: %w", err) } defer compressed.Close() layerDigest, layerSize, err := m.engine.PutBlob(ctx, compressed) if err != nil { - return "", -1, fmt.Errorf("put layer blob: %w", err) + return desc, fmt.Errorf("put layer blob: %w", err) } // Add DiffID to configuration. - layerDiffID := diffidDigester.Digest() - m.appendToConfig(history, layerDiffID) - return layerDigest, layerSize, nil -} - -// Add adds a layer to the image, by reading the layer changeset blob from the -// provided reader. The stream must not be compressed, as it is used to -// generate the DiffIDs for the image metatadata. The provided history entry is -// appended to the image's history and should correspond to what operations -// were made to the configuration. -func (m *Mutator) Add(ctx context.Context, mediaType string, r io.Reader, history *ispec.History, compressor Compressor, annotations map[string]string) (ispec.Descriptor, error) { - desc := ispec.Descriptor{} - if err := m.cache(ctx); err != nil { - return desc, fmt.Errorf("getting cache failed: %w", err) - } - - digest, size, err := m.add(ctx, r, history, compressor) - if err != nil { - return desc, fmt.Errorf("add layer: %w", err) - } + m.appendToConfig(history, diffidDigester.Digest()) - compressedMediaType := mediaType + // Build the descriptor. if compressor.MediaTypeSuffix() != "" { - compressedMediaType = compressedMediaType + "+" + compressor.MediaTypeSuffix() + mediaType += "+" + compressor.MediaTypeSuffix() } if annotations == nil { @@ -312,9 +296,9 @@ func (m *Mutator) Add(ctx context.Context, mediaType string, r io.Reader, histor // Append to layers. desc = ispec.Descriptor{ - MediaType: compressedMediaType, - Digest: digest, - Size: size, + MediaType: mediaType, + Digest: layerDigest, + Size: layerSize, Annotations: annotations, } m.manifest.Layers = append(m.manifest.Layers, desc) From fa3077f8e8e5ba5eae9c2f9e20684257550a11d9 Mon Sep 17 00:00:00 2001 From: Aleksa Sarai Date: Tue, 1 Apr 2025 22:43:44 +1100 Subject: [PATCH 4/9] mutate: make uncompressed annotation hint not use globals The previous implementation of the BytesRead() would store the value of bytes read in the compressor implementation singleton, meaning that if you were to compress multiple data streams you would end up with a meaningless value from BytesRead(). Not good. It also makes little sense to have the byte-counting logic embedded in the compression code -- there is only one user that cares about this and it's trivial to add a dummy io.Reader wrapper that counts the bytes read. Fixes: 8f65e8f75478 ("mutate: add uncompressed blob size annotation") Signed-off-by: Aleksa Sarai --- mutate/compress.go | 27 +++----------------- mutate/mutate.go | 14 ++++++---- pkg/iohelpers/count_reader.go | 48 +++++++++++++++++++++++++++++++++++ 3 files changed, 61 insertions(+), 28 deletions(-) create mode 100644 pkg/iohelpers/count_reader.go diff --git a/mutate/compress.go b/mutate/compress.go index 4039fd61..b5138a35 100644 --- a/mutate/compress.go +++ b/mutate/compress.go @@ -22,10 +22,6 @@ type Compressor interface { // indicate what compression type is used, e.g. "gzip", or "" for no // compression. MediaTypeSuffix() string - - // BytesRead returns the number of bytes read from the uncompressed input - // stream at the current time, no guarantee of completion. - BytesRead() int64 } type noopCompressor struct{} @@ -48,9 +44,7 @@ var NoopCompressor Compressor = noopCompressor{} // GzipCompressor provides gzip compression. var GzipCompressor Compressor = &gzipCompressor{} -type gzipCompressor struct { - bytesRead int64 -} +type gzipCompressor struct{} func (gz *gzipCompressor) Compress(reader io.Reader) (io.ReadCloser, error) { pipeReader, pipeWriter := io.Pipe() @@ -60,14 +54,13 @@ func (gz *gzipCompressor) Compress(reader io.Reader) (io.ReadCloser, error) { return nil, fmt.Errorf("set concurrency level to %v blocks: %w", 2*runtime.NumCPU(), err) } go func() { - bytesRead, err := system.Copy(gzw, reader) + _, err := system.Copy(gzw, reader) if err != nil { log.Warnf("gzip compress: could not compress layer: %v", err) // #nosec G104 _ = pipeWriter.CloseWithError(fmt.Errorf("compressing layer: %w", err)) return } - gz.bytesRead = bytesRead if err := gzw.Close(); err != nil { log.Warnf("gzip compress: could not close gzip writer: %v", err) // #nosec G104 @@ -88,33 +81,25 @@ func (gz gzipCompressor) MediaTypeSuffix() string { return "gzip" } -func (gz gzipCompressor) BytesRead() int64 { - return gz.bytesRead -} - // ZstdCompressor provides zstd compression. var ZstdCompressor Compressor = &zstdCompressor{} -type zstdCompressor struct { - bytesRead int64 -} +type zstdCompressor struct{} func (zs *zstdCompressor) Compress(reader io.Reader) (io.ReadCloser, error) { - pipeReader, pipeWriter := io.Pipe() zw, err := zstd.NewWriter(pipeWriter) if err != nil { return nil, err } go func() { - bytesRead, err := system.Copy(zw, reader) + _, err := system.Copy(zw, reader) if err != nil { log.Warnf("zstd compress: could not compress layer: %v", err) // #nosec G104 _ = pipeWriter.CloseWithError(fmt.Errorf("compressing layer: %w", err)) return } - zs.bytesRead = bytesRead if err := zw.Close(); err != nil { log.Warnf("zstd compress: could not close gzip writer: %v", err) // #nosec G104 @@ -134,7 +119,3 @@ func (zs *zstdCompressor) Compress(reader io.Reader) (io.ReadCloser, error) { func (zs zstdCompressor) MediaTypeSuffix() string { return "zstd" } - -func (zs zstdCompressor) BytesRead() int64 { - return zs.bytesRead -} diff --git a/mutate/mutate.go b/mutate/mutate.go index eb444491..f41b25d2 100644 --- a/mutate/mutate.go +++ b/mutate/mutate.go @@ -29,11 +29,13 @@ import ( "reflect" "time" + "github.com/opencontainers/umoci/oci/cas" + "github.com/opencontainers/umoci/oci/casext" + "github.com/opencontainers/umoci/pkg/iohelpers" + "github.com/apex/log" "github.com/opencontainers/go-digest" ispec "github.com/opencontainers/image-spec/specs-go/v1" - "github.com/opencontainers/umoci/oci/cas" - "github.com/opencontainers/umoci/oci/casext" ) // UmociUncompressedBlobSizeAnnotation is an umoci-specific annotation to @@ -265,8 +267,10 @@ func (m *Mutator) Add(ctx context.Context, mediaType string, r io.Reader, histor return desc, fmt.Errorf("getting cache failed: %w", err) } + countReader := iohelpers.CountReader(r) + diffidDigester := cas.BlobAlgorithm.Digester() - hashReader := io.TeeReader(r, diffidDigester.Hash()) + hashReader := io.TeeReader(countReader, diffidDigester.Hash()) compressed, err := compressor.Compress(hashReader) if err != nil { @@ -290,8 +294,8 @@ func (m *Mutator) Add(ctx context.Context, mediaType string, r io.Reader, histor if annotations == nil { annotations = make(map[string]string) } - if compressor.BytesRead() >= 0 { - annotations[UmociUncompressedBlobSizeAnnotation] = fmt.Sprintf("%d", compressor.BytesRead()) + if plainSize := countReader.BytesRead(); plainSize != layerSize { + annotations[UmociUncompressedBlobSizeAnnotation] = fmt.Sprintf("%d", plainSize) } // Append to layers. diff --git a/pkg/iohelpers/count_reader.go b/pkg/iohelpers/count_reader.go new file mode 100644 index 00000000..9dd623d7 --- /dev/null +++ b/pkg/iohelpers/count_reader.go @@ -0,0 +1,48 @@ +/* + * umoci: Umoci Modifies Open Containers' Images + * Copyright (C) 2016-2025 SUSE LLC + * + * 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 iohelpers + +import ( + "io" +) + +// CountingReader is an [io.Reader] wrapper that counts how many bytes were read +// from the underlying [io.Reader]. +type CountingReader struct { + R io.Reader // underlying reader + N int64 // number of bytes read +} + +// CountReader returns a new *CountingReader that wraps the given [io.Reader]. +func CountReader(rdr io.Reader) *CountingReader { + return &CountingReader{R: rdr, N: 0} +} + +func (c *CountingReader) Read(p []byte) (n int, err error) { + n, err = c.R.Read(p) + c.N += int64(n) + return +} + +// BytesRead returns the number of bytes read so far from the reader. This is +// just shorthand for c.N. +func (c CountingReader) BytesRead() int64 { + return c.N +} + +// TODO: What about WriteTo? From 245726c74ecb7e6a1fcd118b1a65f58fc5987059 Mon Sep 17 00:00:00 2001 From: Aleksa Sarai Date: Thu, 3 Apr 2025 17:27:15 +1100 Subject: [PATCH 5/9] casext: unify blob compression and decompression Until now we have had separate the compression and decompression logic and algorithm definitions. This has made it easy for folks to forget to add support for both (such as with zstd), and makes it more annoying to try to make the feature generic. Signed-off-by: Aleksa Sarai --- mutate/compress.go | 135 +++++++-------------------- mutate/mutate_test.go | 5 +- oci/casext/blobcompress/algo.go | 86 +++++++++++++++++ oci/casext/blobcompress/algo_test.go | 105 +++++++++++++++++++++ oci/casext/blobcompress/gzip.go | 78 ++++++++++++++++ oci/casext/blobcompress/gzip_test.go | 26 ++++++ oci/casext/blobcompress/noop.go | 43 +++++++++ oci/casext/blobcompress/noop_test.go | 26 ++++++ oci/casext/blobcompress/zstd.go | 80 ++++++++++++++++ oci/casext/blobcompress/zstd_test.go | 26 ++++++ oci/casext/mediatype/compress.go | 25 +++++ oci/layer/unpack.go | 4 +- 12 files changed, 533 insertions(+), 106 deletions(-) create mode 100644 oci/casext/blobcompress/algo.go create mode 100644 oci/casext/blobcompress/algo_test.go create mode 100644 oci/casext/blobcompress/gzip.go create mode 100644 oci/casext/blobcompress/gzip_test.go create mode 100644 oci/casext/blobcompress/noop.go create mode 100644 oci/casext/blobcompress/noop_test.go create mode 100644 oci/casext/blobcompress/zstd.go create mode 100644 oci/casext/blobcompress/zstd_test.go create mode 100644 oci/casext/mediatype/compress.go diff --git a/mutate/compress.go b/mutate/compress.go index b5138a35..f1161ceb 100644 --- a/mutate/compress.go +++ b/mutate/compress.go @@ -1,19 +1,34 @@ +/* + * umoci: Umoci Modifies Open Containers' Images + * Copyright (C) 2016-2024 SUSE LLC + * + * 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 mutate import ( - "fmt" "io" - "io/ioutil" - "runtime" - "github.com/apex/log" - zstd "github.com/klauspost/compress/zstd" - gzip "github.com/klauspost/pgzip" - "github.com/opencontainers/umoci/pkg/system" + "github.com/opencontainers/umoci/oci/casext/blobcompress" ) // Compressor is an interface which users can use to implement different // compression types. +// +// Deprecated: The compression algorithm logic has been moved to [blobcompress] +// to unify the media-type compression handling for both compression and +// decompression. Please switch to [blobcompress.Algorithm]. type Compressor interface { // Compress sets up the streaming compressor for this compression type. Compress(io.Reader) (io.ReadCloser, error) @@ -24,98 +39,14 @@ type Compressor interface { MediaTypeSuffix() string } -type noopCompressor struct{} - -func (nc noopCompressor) Compress(r io.Reader) (io.ReadCloser, error) { - return ioutil.NopCloser(r), nil -} - -func (nc noopCompressor) MediaTypeSuffix() string { - return "" -} - -func (nc noopCompressor) BytesRead() int64 { - return -1 -} - -// NoopCompressor provides no compression. -var NoopCompressor Compressor = noopCompressor{} - -// GzipCompressor provides gzip compression. -var GzipCompressor Compressor = &gzipCompressor{} - -type gzipCompressor struct{} - -func (gz *gzipCompressor) Compress(reader io.Reader) (io.ReadCloser, error) { - pipeReader, pipeWriter := io.Pipe() - - gzw := gzip.NewWriter(pipeWriter) - if err := gzw.SetConcurrency(256<<10, 2*runtime.NumCPU()); err != nil { - return nil, fmt.Errorf("set concurrency level to %v blocks: %w", 2*runtime.NumCPU(), err) - } - go func() { - _, err := system.Copy(gzw, reader) - if err != nil { - log.Warnf("gzip compress: could not compress layer: %v", err) - // #nosec G104 - _ = pipeWriter.CloseWithError(fmt.Errorf("compressing layer: %w", err)) - return - } - if err := gzw.Close(); err != nil { - log.Warnf("gzip compress: could not close gzip writer: %v", err) - // #nosec G104 - _ = pipeWriter.CloseWithError(fmt.Errorf("close gzip writer: %w", err)) - return - } - if err := pipeWriter.Close(); err != nil { - log.Warnf("gzip compress: could not close pipe: %v", err) - // We don't CloseWithError because we cannot override the Close. - return - } - }() - - return pipeReader, nil -} - -func (gz gzipCompressor) MediaTypeSuffix() string { - return "gzip" -} - -// ZstdCompressor provides zstd compression. -var ZstdCompressor Compressor = &zstdCompressor{} - -type zstdCompressor struct{} - -func (zs *zstdCompressor) Compress(reader io.Reader) (io.ReadCloser, error) { - pipeReader, pipeWriter := io.Pipe() - zw, err := zstd.NewWriter(pipeWriter) - if err != nil { - return nil, err - } - go func() { - _, err := system.Copy(zw, reader) - if err != nil { - log.Warnf("zstd compress: could not compress layer: %v", err) - // #nosec G104 - _ = pipeWriter.CloseWithError(fmt.Errorf("compressing layer: %w", err)) - return - } - if err := zw.Close(); err != nil { - log.Warnf("zstd compress: could not close gzip writer: %v", err) - // #nosec G104 - _ = pipeWriter.CloseWithError(fmt.Errorf("close zstd writer: %w", err)) - return - } - if err := pipeWriter.Close(); err != nil { - log.Warnf("zstd compress: could not close pipe: %v", err) - // We don't CloseWithError because we cannot override the Close. - return - } - }() - - return pipeReader, nil -} - -func (zs zstdCompressor) MediaTypeSuffix() string { - return "zstd" -} +// Deprecated: The compression algorithm logic has been moved to [blobcompress] +// to unify the media-type compression handling for both compression and +// decompression. Please switch to blobcompress. +var ( + // NoopCompressor provides no compression. + NoopCompressor Compressor = blobcompress.Noop + // GzipCompressor provides gzip compression. + GzipCompressor Compressor = blobcompress.Gzip + // ZstdCompressor provides zstd compression. + ZstdCompressor Compressor = blobcompress.Zstd +) diff --git a/mutate/mutate_test.go b/mutate/mutate_test.go index 1d46a2a7..8569c846 100644 --- a/mutate/mutate_test.go +++ b/mutate/mutate_test.go @@ -35,6 +35,7 @@ import ( "github.com/opencontainers/umoci/oci/cas" casdir "github.com/opencontainers/umoci/oci/cas/dir" "github.com/opencontainers/umoci/oci/casext" + "github.com/opencontainers/umoci/oci/casext/blobcompress" ) // These come from just running the code. @@ -220,7 +221,7 @@ func TestMutateAdd(t *testing.T) { annotations := map[string]string{"hello": "world"} newLayerDesc, err := mutator.Add(context.Background(), ispec.MediaTypeImageLayer, buffer, &ispec.History{ Comment: "new layer", - }, GzipCompressor, annotations) + }, blobcompress.Gzip, annotations) if err != nil { t.Fatalf("unexpected error adding layer: %+v", err) } @@ -325,7 +326,7 @@ func TestMutateAddExisting(t *testing.T) { // Add a new layer. _, err = mutator.Add(context.Background(), ispec.MediaTypeImageLayer, buffer, &ispec.History{ Comment: "new layer", - }, GzipCompressor, nil) + }, blobcompress.Gzip, nil) if err != nil { t.Fatalf("unexpected error adding layer: %+v", err) } diff --git a/oci/casext/blobcompress/algo.go b/oci/casext/blobcompress/algo.go new file mode 100644 index 00000000..94649c3a --- /dev/null +++ b/oci/casext/blobcompress/algo.go @@ -0,0 +1,86 @@ +/* + * umoci: Umoci Modifies Open Containers' Images + * Copyright (C) 2016-2025 SUSE LLC + * + * 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 blobcompress provides a somewhat implementation-agnostic mechanism for +// blobcompression and deblobcompression of an [io.Reader]. This is mainly intended to +// be used for blobcompressing tar layer blobs. +package blobcompress + +import ( + "fmt" + "io" + "sync" +) + +// Default is the default algorithm used within umoci if unspecified by a user. +var Default = Gzip + +// Algorithm is a generic representation of a compression algorithm that can be +// used by umoci for compressing and decompressing layer blobs. If you create a +// custom algorithm, you should make sure you register it with +// [RegisterAlgorithm] so that layers using the algorithm can be decompressed. +type Algorithm interface { + // MediaTypeSuffix returns the name of the blobcompression algorithm. This name + // will be used when generating and parsing MIME type +-suffixes. + MediaTypeSuffix() string + + // Compress sets up the streaming blobcompressor for this blobcompression type. + Compress(plain io.Reader) (blobcompressed io.ReadCloser, _ error) + + // Deblobcompress sets up the streaming deblobcompressor for this blobcompression type. + Decompress(blobcompressed io.Reader) (plain io.ReadCloser, _ error) +} + +var ( + algorithmsLock sync.RWMutex + algorithms = map[string]Algorithm{} +) + +// RegisterAlgorithm adds the provided [Algorithm] to the set of algorithms +// that umoci can automatically handle when extracting images. Returns an error +// if another [Algorithm] with the same MediaTypeSuffix has already been +// registered. +func RegisterAlgorithm(algo Algorithm) error { + name := algo.MediaTypeSuffix() + + algorithmsLock.Lock() + defer algorithmsLock.Unlock() + + if _, ok := algorithms[name]; ok { + return fmt.Errorf("blob blobcompression algorithm %s already registered", name) + } + algorithms[name] = algo + return nil +} + +// MustRegisterAlgorithm is like [RegisterAlgorithm] but it panics if +// [RegisterAlgorithm] returns an error. Intended for use in init functions. +func MustRegisterAlgorithm(algo Algorithm) { + if err := RegisterAlgorithm(algo); err != nil { + panic(err) + } +} + +// GetAlgorithm looks for a registered [Algorithm] with the given +// MediaTypeSuffix (which doubles as its name). Return nil if no such algorithm +// has been registered. +func GetAlgorithm(name string) Algorithm { + algorithmsLock.RLock() + defer algorithmsLock.RUnlock() + + return algorithms[name] +} diff --git a/oci/casext/blobcompress/algo_test.go b/oci/casext/blobcompress/algo_test.go new file mode 100644 index 00000000..68389cb1 --- /dev/null +++ b/oci/casext/blobcompress/algo_test.go @@ -0,0 +1,105 @@ +/* + * umoci: Umoci Modifies Open Containers' Images + * Copyright (C) 2016-2025 SUSE LLC + * + * 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 blobcompress + +import ( + "bytes" + "fmt" + "io" + "io/ioutil" + "testing" + + "github.com/stretchr/testify/assert" +) + +func testAlgo(t *testing.T, algo Algorithm, expectedName string, expectedDiff bool) { + assert := assert.New(t) + + const data = "meshuggah rocks!!!" + + plainBuf := bytes.NewBufferString(data) + + r, err := algo.Compress(plainBuf) + assert.NoError(err) + assert.Equal(algo.MediaTypeSuffix(), expectedName) + + compressed, err := ioutil.ReadAll(r) + assert.NoError(err) + if expectedDiff { + assert.NotEqual(string(compressed), data) + } else { + assert.Equal(string(compressed), data) + } + + compressedBuf := bytes.NewBuffer(compressed) + + r, err = algo.Decompress(compressedBuf) + assert.NoError(err) + + content, err := ioutil.ReadAll(r) + assert.NoError(err) + + assert.Equal(string(content), data) +} + +type fakeAlgo struct{ name string } + +var _ Algorithm = fakeAlgo{"foo"} + +func (f fakeAlgo) MediaTypeSuffix() string { return f.name } +func (f fakeAlgo) Compress(r io.Reader) (io.ReadCloser, error) { return nil, fmt.Errorf("err") } +func (f fakeAlgo) Decompress(r io.Reader) (io.ReadCloser, error) { return nil, fmt.Errorf("err") } + +func TestRegister(t *testing.T) { + assert := assert.New(t) + + var fakeAlgo Algorithm = fakeAlgo{"fake-algo1"} + fakeAlgoName := fakeAlgo.MediaTypeSuffix() + + err := RegisterAlgorithm(fakeAlgo) + assert.NoError(err) + + gotAlgo := GetAlgorithm(fakeAlgoName) + assert.NotNil(gotAlgo) + assert.Equal(gotAlgo, fakeAlgo) +} + +func TestRegisterFail(t *testing.T) { + assert := assert.New(t) + + var fakeAlgo Algorithm = fakeAlgo{"fake-algo2"} + fakeAlgoName := fakeAlgo.MediaTypeSuffix() + + err1 := RegisterAlgorithm(fakeAlgo) + assert.NoError(err1) + + // Registering the same algorithm again should fail. + err2 := RegisterAlgorithm(fakeAlgo) + assert.Error(err2) + + gotAlgo := GetAlgorithm(fakeAlgoName) + assert.NotNil(gotAlgo) + assert.Equal(gotAlgo, fakeAlgo) +} + +func TestGetFail(t *testing.T) { + assert := assert.New(t) + + algo := GetAlgorithm("doesnotexist") + assert.Nil(algo, "GetAlgorithm for non-existent compression algorithm should return nil") +} diff --git a/oci/casext/blobcompress/gzip.go b/oci/casext/blobcompress/gzip.go new file mode 100644 index 00000000..23cd292e --- /dev/null +++ b/oci/casext/blobcompress/gzip.go @@ -0,0 +1,78 @@ +/* + * umoci: Umoci Modifies Open Containers' Images + * Copyright (C) 2016-2025 SUSE LLC + * + * 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 blobcompress + +import ( + "fmt" + "io" + "runtime" + + "github.com/opencontainers/umoci/oci/casext/mediatype" + "github.com/opencontainers/umoci/pkg/system" + + "github.com/apex/log" + gzip "github.com/klauspost/pgzip" +) + +// Gzip provides concurrent gzip blobcompression and deblobcompression. +var Gzip Algorithm = gzipAlgo{} + +type gzipAlgo struct{} + +func (gz gzipAlgo) MediaTypeSuffix() string { + return mediatype.GzipSuffix +} + +func (gz gzipAlgo) Compress(reader io.Reader) (io.ReadCloser, error) { + pipeReader, pipeWriter := io.Pipe() + + gzw := gzip.NewWriter(pipeWriter) + if err := gzw.SetConcurrency(256<<10, 2*runtime.NumCPU()); err != nil { + return nil, fmt.Errorf("set concurrency level to %v blocks: %w", 2*runtime.NumCPU(), err) + } + go func() { + _, err := system.Copy(gzw, reader) + if err != nil { + log.Warnf("gzip blobcompress: could not blobcompress layer: %v", err) + // #nosec G104 + _ = pipeWriter.CloseWithError(fmt.Errorf("blobcompressing layer: %w", err)) + return + } + if err := gzw.Close(); err != nil { + log.Warnf("gzip blobcompress: could not close gzip writer: %v", err) + // #nosec G104 + _ = pipeWriter.CloseWithError(fmt.Errorf("close gzip writer: %w", err)) + return + } + if err := pipeWriter.Close(); err != nil { + log.Warnf("gzip blobcompress: could not close pipe: %v", err) + // We don't CloseWithError because we cannot override the Close. + return + } + }() + + return pipeReader, nil +} + +func (gz gzipAlgo) Decompress(reader io.Reader) (io.ReadCloser, error) { + return gzip.NewReader(reader) +} + +func init() { + MustRegisterAlgorithm(Gzip) +} diff --git a/oci/casext/blobcompress/gzip_test.go b/oci/casext/blobcompress/gzip_test.go new file mode 100644 index 00000000..3d3e6fcf --- /dev/null +++ b/oci/casext/blobcompress/gzip_test.go @@ -0,0 +1,26 @@ +/* + * umoci: Umoci Modifies Open Containers' Images + * Copyright (C) 2016-2025 SUSE LLC + * + * 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 blobcompress + +import ( + "testing" +) + +func TestGzip(t *testing.T) { + testAlgo(t, Gzip, "gzip", true) +} diff --git a/oci/casext/blobcompress/noop.go b/oci/casext/blobcompress/noop.go new file mode 100644 index 00000000..8350f9e8 --- /dev/null +++ b/oci/casext/blobcompress/noop.go @@ -0,0 +1,43 @@ +/* + * umoci: Umoci Modifies Open Containers' Images + * Copyright (C) 2016-2025 SUSE LLC + * + * 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 blobcompress + +import ( + "io" +) + +// Noop does no compression. +var Noop Algorithm = noopAlgo{} + +type noopAlgo struct{} + +func (n noopAlgo) MediaTypeSuffix() string { + return "" +} + +func (n noopAlgo) Compress(reader io.Reader) (io.ReadCloser, error) { + return io.NopCloser(reader), nil +} + +func (n noopAlgo) Decompress(reader io.Reader) (io.ReadCloser, error) { + return io.NopCloser(reader), nil +} + +func init() { + MustRegisterAlgorithm(Noop) +} diff --git a/oci/casext/blobcompress/noop_test.go b/oci/casext/blobcompress/noop_test.go new file mode 100644 index 00000000..b53f18bf --- /dev/null +++ b/oci/casext/blobcompress/noop_test.go @@ -0,0 +1,26 @@ +/* + * umoci: Umoci Modifies Open Containers' Images + * Copyright (C) 2016-2025 SUSE LLC + * + * 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 blobcompress + +import ( + "testing" +) + +func TestNoop(t *testing.T) { + testAlgo(t, Noop, "", false) +} diff --git a/oci/casext/blobcompress/zstd.go b/oci/casext/blobcompress/zstd.go new file mode 100644 index 00000000..6318752c --- /dev/null +++ b/oci/casext/blobcompress/zstd.go @@ -0,0 +1,80 @@ +/* + * umoci: Umoci Modifies Open Containers' Images + * Copyright (C) 2016-2025 SUSE LLC + * + * 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 blobcompress + +import ( + "fmt" + "io" + + "github.com/opencontainers/umoci/oci/casext/mediatype" + "github.com/opencontainers/umoci/pkg/system" + + "github.com/apex/log" + zstd "github.com/klauspost/compress/zstd" +) + +// Zstd provides zstd blobcompression and deblobcompression. +var Zstd Algorithm = zstdAlgo{} + +type zstdAlgo struct{} + +func (zs zstdAlgo) MediaTypeSuffix() string { + return mediatype.ZstdSuffix +} + +func (zs zstdAlgo) Compress(reader io.Reader) (io.ReadCloser, error) { + pipeReader, pipeWriter := io.Pipe() + zw, err := zstd.NewWriter(pipeWriter) + if err != nil { + return nil, err + } + go func() { + _, err := system.Copy(zw, reader) + if err != nil { + log.Warnf("zstd blobcompress: could not blobcompress layer: %v", err) + // #nosec G104 + _ = pipeWriter.CloseWithError(fmt.Errorf("blobcompressing layer: %w", err)) + return + } + if err := zw.Close(); err != nil { + log.Warnf("zstd blobcompress: could not close gzip writer: %v", err) + // #nosec G104 + _ = pipeWriter.CloseWithError(fmt.Errorf("close zstd writer: %w", err)) + return + } + if err := pipeWriter.Close(); err != nil { + log.Warnf("zstd blobcompress: could not close pipe: %v", err) + // We don't CloseWithError because we cannot override the Close. + return + } + }() + + return pipeReader, nil +} + +func (zs zstdAlgo) Decompress(reader io.Reader) (io.ReadCloser, error) { + plain, err := zstd.NewReader(reader) + if err != nil { + return nil, err + } + return plain.IOReadCloser(), nil +} + +func init() { + MustRegisterAlgorithm(Zstd) +} diff --git a/oci/casext/blobcompress/zstd_test.go b/oci/casext/blobcompress/zstd_test.go new file mode 100644 index 00000000..fd57d542 --- /dev/null +++ b/oci/casext/blobcompress/zstd_test.go @@ -0,0 +1,26 @@ +/* + * umoci: Umoci Modifies Open Containers' Images + * Copyright (C) 2016-2025 SUSE LLC + * + * 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 blobcompress + +import ( + "testing" +) + +func TestZstd(t *testing.T) { + testAlgo(t, Zstd, "zstd", true) +} diff --git a/oci/casext/mediatype/compress.go b/oci/casext/mediatype/compress.go new file mode 100644 index 00000000..ffe80eb0 --- /dev/null +++ b/oci/casext/mediatype/compress.go @@ -0,0 +1,25 @@ +/* + * umoci: Umoci Modifies Open Containers' Images + * Copyright (C) 2016-2025 SUSE LLC + * + * 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 mediatype + +const ( + // GzipSuffix is the standard media-type suffix for gzip compressed blobs. + GzipSuffix = "gzip" + // ZstdSuffix is the standard media-type suffix for zstd compressed blobs. + ZstdSuffix = "zstd" +) diff --git a/oci/layer/unpack.go b/oci/layer/unpack.go index 3ebc4214..f61f8b0d 100644 --- a/oci/layer/unpack.go +++ b/oci/layer/unpack.go @@ -33,12 +33,12 @@ import ( _ "crypto/sha256" "github.com/apex/log" - gzip "github.com/klauspost/pgzip" "github.com/opencontainers/go-digest" ispec "github.com/opencontainers/image-spec/specs-go/v1" rspec "github.com/opencontainers/runtime-spec/specs-go" "github.com/opencontainers/umoci/oci/cas" "github.com/opencontainers/umoci/oci/casext" + "github.com/opencontainers/umoci/oci/casext/blobcompress" iconv "github.com/opencontainers/umoci/oci/config/convert" "github.com/opencontainers/umoci/pkg/fseval" "github.com/opencontainers/umoci/pkg/idtools" @@ -255,7 +255,7 @@ func UnpackRootfs(ctx context.Context, engine cas.Engine, rootfsPath string, man // We have to extract a gzip'd version of the above layer. Also note // that we have to check the DiffID we're extracting (which is the // sha256 sum of the *uncompressed* layer). - layerRaw, err = gzip.NewReader(layerData) + layerRaw, err = blobcompress.Gzip.Decompress(layerData) if err != nil { return fmt.Errorf("create gzip reader: %w", err) } From 6355ec27f6efc5ddb4609cfe14b34a0aa23196c9 Mon Sep 17 00:00:00 2001 From: Aleksa Sarai Date: Thu, 3 Apr 2025 17:33:10 +1100 Subject: [PATCH 6/9] unpack: make layer decompression based on mediatype more generic Rather than hard-coding a set of media-types, parse the "+foo" MIME type suffix to figure out what compression algorithm was used for the layer. This also enables zstd decompression support out of the box (since that is a registered algorithm now) and also improves our error messages -- we now provide a more helpful error to explain whether the issue is that the underlying MIME type of the layer is unsupported (think squashfs) or if it's just an issue with the compression algorithm. Signed-off-by: Aleksa Sarai --- CHANGELOG.md | 6 +++++ oci/casext/mediatype/compress.go | 21 ++++++++++++++++ oci/layer/unpack.go | 41 ++++++++++++++++++++++---------- 3 files changed, 55 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2087ab3c..e47d0123 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,12 @@ and this project adheres to [Semantic Versioning](http://semver.org/). had no real impact on umoci but for safety we implemented the now-recommended media-type embedding and verification. CVE-2021-41190 +### Added ### +- `umoci unpack` now supports handling layers compressed with zstd. This is + something that was added in image-spec v1.2 (which we do not yet support + fully) but at least this will allow users to operate on zstd-compressed + images, which are slowly becoming more common. + ### Changes ### - In this release, the primary development branch was renamed to `main`. - The runtime-spec version of the `config.json` version we generate is no diff --git a/oci/casext/mediatype/compress.go b/oci/casext/mediatype/compress.go index ffe80eb0..897479a3 100644 --- a/oci/casext/mediatype/compress.go +++ b/oci/casext/mediatype/compress.go @@ -17,9 +17,30 @@ package mediatype +import ( + "strings" +) + const ( // GzipSuffix is the standard media-type suffix for gzip compressed blobs. GzipSuffix = "gzip" // ZstdSuffix is the standard media-type suffix for zstd compressed blobs. ZstdSuffix = "zstd" ) + +// SplitMediaTypeSuffix takes an OCI-style MIME media-type and splits it into a +// base type and a "+" suffix. For layer blobs, this is usually the compression +// algorithm ("+gzip", "+zstd"). For JSON blobs this is usually "+json". +// +// The returned media-type suffix does not contain a "+", and media-types not +// containing a "+" are treated as having an empty suffix. +// +// While this usage is technically sanctioned by by RFC 6838, this method +// should only be used for OCI media-types, where this behaviour is +// well-defined. +func SplitMediaTypeSuffix(mediaType string) (baseType, suffix string) { + if suffixStart := strings.Index(mediaType, "+"); suffixStart >= 0 { + return mediaType[:suffixStart], mediaType[suffixStart+1:] + } + return mediaType, "" +} diff --git a/oci/layer/unpack.go b/oci/layer/unpack.go index f61f8b0d..f6d89f91 100644 --- a/oci/layer/unpack.go +++ b/oci/layer/unpack.go @@ -39,6 +39,7 @@ import ( "github.com/opencontainers/umoci/oci/cas" "github.com/opencontainers/umoci/oci/casext" "github.com/opencontainers/umoci/oci/casext/blobcompress" + "github.com/opencontainers/umoci/oci/casext/mediatype" iconv "github.com/opencontainers/umoci/oci/config/convert" "github.com/opencontainers/umoci/pkg/fseval" "github.com/opencontainers/umoci/pkg/idtools" @@ -78,15 +79,25 @@ func UnpackLayer(root string, layer io.Reader, opt *UnpackOptions) error { // generated. const RootfsName = "rootfs" -// isLayerType returns if the given MediaType is the media type of an image -// layer blob. This includes both distributable and non-distributable images. func isLayerType(mediaType string) bool { - return mediaType == ispec.MediaTypeImageLayer || mediaType == ispec.MediaTypeImageLayerNonDistributable || - mediaType == ispec.MediaTypeImageLayerGzip || mediaType == ispec.MediaTypeImageLayerNonDistributableGzip + layerMediaType, _ := mediatype.SplitMediaTypeSuffix(mediaType) + switch layerMediaType { + case ispec.MediaTypeImageLayerNonDistributable: + log.Infof("image contains layers using the deprecated 'non-distributable' media type %q", layerMediaType) + fallthrough + case ispec.MediaTypeImageLayer: + return true + } + return false } -func needsGunzip(mediaType string) bool { - return mediaType == ispec.MediaTypeImageLayerGzip || mediaType == ispec.MediaTypeImageLayerNonDistributableGzip +func getLayerCompressAlgorithm(mediaType string) (string, blobcompress.Algorithm, error) { + _, compressType := mediatype.SplitMediaTypeSuffix(mediaType) + algorithm := blobcompress.GetAlgorithm(compressType) + if algorithm == nil { + return "", nil, fmt.Errorf("unsupported layer media type %q: compression method %q unsupported", mediaType, compressType) + } + return compressType, algorithm, nil } // UnpackManifest extracts all of the layers in the given manifest, as well as @@ -242,7 +253,7 @@ func UnpackRootfs(ctx context.Context, engine cas.Engine, rootfsPath string, man } defer layerBlob.Close() if !isLayerType(layerBlob.Descriptor.MediaType) { - return fmt.Errorf("unpack rootfs: layer %s: blob is not correct mediatype: %s", layerBlob.Descriptor.Digest, layerBlob.Descriptor.MediaType) + return fmt.Errorf("unpack rootfs: layer %s: layer data is an unsupported mediatype: %s", layerBlob.Descriptor.Digest, layerBlob.Descriptor.MediaType) } layerData, ok := layerBlob.Data.(io.ReadCloser) if !ok { @@ -251,13 +262,17 @@ func UnpackRootfs(ctx context.Context, engine cas.Engine, rootfsPath string, man } layerRaw := layerData - if needsGunzip(layerBlob.Descriptor.MediaType) { - // We have to extract a gzip'd version of the above layer. Also note - // that we have to check the DiffID we're extracting (which is the - // sha256 sum of the *uncompressed* layer). - layerRaw, err = blobcompress.Gzip.Decompress(layerData) + + // Pick the decompression algorithm based on the media-type. + if compressType, compressAlgo, err := getLayerCompressAlgorithm(layerBlob.Descriptor.MediaType); err != nil { + return fmt.Errorf("unpack rootfs: layer %s: could not decompress layer: %w", layerBlob.Descriptor.Digest, err) + } else if compressAlgo != nil { + // We have to extract a compressed version of the above layer. Also + // note that we have to check the DiffID we're extracting (which is + // the sha256 sum of the *uncompressed* layer). + layerRaw, err = compressAlgo.Decompress(layerData) if err != nil { - return fmt.Errorf("create gzip reader: %w", err) + return fmt.Errorf("create %s reader: %w", compressType, err) } } From 3373613ca5fa0f0ae97b69522653d46341f594f3 Mon Sep 17 00:00:00 2001 From: Aleksa Sarai Date: Thu, 3 Apr 2025 17:53:04 +1100 Subject: [PATCH 7/9] repack: don't hardcode layer compression algorithm If a user doesn't specify what compression type they would like to (*Mutator).Add(), auto-select the compression algorithm by first trying to use whatever the last layer's compression algorithm was (if we support it) followed by a fallback to a default (gzip for now). It is preferable to try to re-use the previous compression algorithm so that users which operate on zstd-compressed images will produce zstd-compressed layers rather than going with gzip. For now, make this the default for the CLI as well. A future patch will add support for a --compress=... flag that will let you configure this behaviour and explicitly set a compression method. Signed-off-by: Aleksa Sarai --- cmd/umoci/insert.go | 2 +- cmd/umoci/raw-add-layer.go | 2 +- cmd/umoci/repack.go | 2 +- mutate/mutate.go | 38 +- mutate/mutate_test.go | 110 + repack.go | 6 +- .../stretchr/testify/require/doc.go | 29 + .../testify/require/forward_requirements.go | 16 + .../stretchr/testify/require/require.go | 2060 +++++++++++++++++ .../stretchr/testify/require/require.go.tmpl | 6 + .../testify/require/require_forward.go | 1622 +++++++++++++ .../testify/require/require_forward.go.tmpl | 5 + .../stretchr/testify/require/requirements.go | 29 + vendor/modules.txt | 1 + 14 files changed, 3921 insertions(+), 7 deletions(-) create mode 100644 vendor/github.com/stretchr/testify/require/doc.go create mode 100644 vendor/github.com/stretchr/testify/require/forward_requirements.go create mode 100644 vendor/github.com/stretchr/testify/require/require.go create mode 100644 vendor/github.com/stretchr/testify/require/require.go.tmpl create mode 100644 vendor/github.com/stretchr/testify/require/require_forward.go create mode 100644 vendor/github.com/stretchr/testify/require/require_forward.go.tmpl create mode 100644 vendor/github.com/stretchr/testify/require/requirements.go diff --git a/cmd/umoci/insert.go b/cmd/umoci/insert.go index e77c3d4a..6a09c1e8 100644 --- a/cmd/umoci/insert.go +++ b/cmd/umoci/insert.go @@ -188,7 +188,7 @@ func insert(ctx *cli.Context) error { } } - if _, err := mutator.Add(context.Background(), ispec.MediaTypeImageLayer, reader, history, mutate.GzipCompressor, nil); err != nil { + if _, err := mutator.Add(context.Background(), ispec.MediaTypeImageLayer, reader, history, nil, nil); err != nil { return fmt.Errorf("add diff layer: %w", err) } diff --git a/cmd/umoci/raw-add-layer.go b/cmd/umoci/raw-add-layer.go index 15ee4aab..42e28861 100644 --- a/cmd/umoci/raw-add-layer.go +++ b/cmd/umoci/raw-add-layer.go @@ -154,7 +154,7 @@ func rawAddLayer(ctx *cli.Context) error { } } - if _, err := mutator.Add(context.Background(), ispec.MediaTypeImageLayer, newLayer, history, mutate.GzipCompressor, nil); err != nil { + if _, err := mutator.Add(context.Background(), ispec.MediaTypeImageLayer, newLayer, history, nil, nil); err != nil { return fmt.Errorf("add diff layer: %w", err) } diff --git a/cmd/umoci/repack.go b/cmd/umoci/repack.go index 1c5b33fb..fb8cebb1 100644 --- a/cmd/umoci/repack.go +++ b/cmd/umoci/repack.go @@ -176,5 +176,5 @@ func repack(ctx *cli.Context) error { mtreefilter.MaskFilter(maskedPaths), } - return umoci.Repack(engineExt, tagName, bundlePath, meta, history, filters, ctx.Bool("refresh-bundle"), mutator) + return umoci.Repack(engineExt, tagName, bundlePath, meta, history, filters, ctx.Bool("refresh-bundle"), mutator, nil) } diff --git a/mutate/mutate.go b/mutate/mutate.go index f41b25d2..026763da 100644 --- a/mutate/mutate.go +++ b/mutate/mutate.go @@ -31,6 +31,8 @@ import ( "github.com/opencontainers/umoci/oci/cas" "github.com/opencontainers/umoci/oci/casext" + "github.com/opencontainers/umoci/oci/casext/blobcompress" + "github.com/opencontainers/umoci/oci/casext/mediatype" "github.com/opencontainers/umoci/pkg/iohelpers" "github.com/apex/log" @@ -256,6 +258,30 @@ func (m *Mutator) appendToConfig(history *ispec.History, layerDiffID digest.Dige } } +// PickDefaultCompressAlgorithm returns the best option for the compression +// algorithm for new layers. The main preference is to use re-use whatever the +// most recent layer's compression algorithm is (for those we support). As a +// final fallback, we use blobcompress.Default. +func (m *Mutator) PickDefaultCompressAlgorithm(ctx context.Context) (Compressor, error) { + if err := m.cache(ctx); err != nil { + return nil, fmt.Errorf("getting cache failed: %w", err) + } + layers := m.manifest.Layers + for i := len(layers) - 1; i >= 0; i-- { + _, compressType := mediatype.SplitMediaTypeSuffix(layers[i].MediaType) + // Don't generate an uncompressed layer even if the previous one is -- + // there is no reason to automatically generate uncompressed blobs. + if compressType != "" { + candidate := blobcompress.GetAlgorithm(compressType) + if candidate != nil { + return candidate, nil + } + } + } + // No supported, non-plain algorithm found. Just use the default. + return blobcompress.Default, nil +} + // Add adds a layer to the image, by reading the layer changeset blob from the // provided reader. The stream must not be compressed, as it is used to // generate the DiffIDs for the image metatadata. The provided history entry is @@ -272,6 +298,14 @@ func (m *Mutator) Add(ctx context.Context, mediaType string, r io.Reader, histor diffidDigester := cas.BlobAlgorithm.Digester() hashReader := io.TeeReader(countReader, diffidDigester.Hash()) + if compressor == nil { + bestCompressor, err := m.PickDefaultCompressAlgorithm(ctx) + if err != nil { + return desc, fmt.Errorf("find best default layer compression algorithm: %w", err) + } + compressor = bestCompressor + } + compressed, err := compressor.Compress(hashReader) if err != nil { return desc, fmt.Errorf("couldn't create compression for blob: %w", err) @@ -287,8 +321,8 @@ func (m *Mutator) Add(ctx context.Context, mediaType string, r io.Reader, histor m.appendToConfig(history, diffidDigester.Digest()) // Build the descriptor. - if compressor.MediaTypeSuffix() != "" { - mediaType += "+" + compressor.MediaTypeSuffix() + if suffix := compressor.MediaTypeSuffix(); suffix != "" { + mediaType += "+" + suffix } if annotations == nil { diff --git a/mutate/mutate_test.go b/mutate/mutate_test.go index 8569c846..ff9f475f 100644 --- a/mutate/mutate_test.go +++ b/mutate/mutate_test.go @@ -36,6 +36,8 @@ import ( casdir "github.com/opencontainers/umoci/oci/cas/dir" "github.com/opencontainers/umoci/oci/casext" "github.com/opencontainers/umoci/oci/casext/blobcompress" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) // These come from just running the code. @@ -305,6 +307,114 @@ func TestMutateAdd(t *testing.T) { } } +func testMutateAddCompression(t *testing.T, mutator *Mutator, mediaType string, addCompressAlgo, expectedCompressAlgo blobcompress.Algorithm) { + // This test doesn't care about whether the layer is real. + fakeLayerData := `fake tar archive` + fakeLayerTar := bytes.NewBufferString(fakeLayerData) + + newLayerDescriptor, err := mutator.Add( + context.Background(), + mediaType, + fakeLayerTar, + &ispec.History{Comment: "fake layer"}, + addCompressAlgo, + nil, + ) + require.NoError(t, err) + + expectedMediaType := mediaType + if suffix := expectedCompressAlgo.MediaTypeSuffix(); suffix != "" { + expectedMediaType += "+" + suffix + } + + usedCompressName := "auto" + if addCompressAlgo != nil { + if suffix := addCompressAlgo.MediaTypeSuffix(); suffix != "" { + usedCompressName = suffix + } else { + usedCompressName = "plain" + } + } + + // The media-type should be what we expected. + assert.Equalf(t, newLayerDescriptor.MediaType, expectedMediaType, "unexpected media type of new layer with compression algo %q", usedCompressName) + + // Double-check that the blob actually used the expected compression + // algorithm. + layerRdr, err := mutator.engine.GetVerifiedBlob(context.Background(), newLayerDescriptor) + require.NoError(t, err) + defer func() { + require.NoError(t, layerRdr.Close()) + }() + + plainLayerRdr, err := expectedCompressAlgo.Decompress(layerRdr) + require.NoError(t, err) + defer func() { + require.NoError(t, plainLayerRdr.Close()) + }() + + plainLayerData, err := ioutil.ReadAll(plainLayerRdr) + require.NoError(t, err) + + assert.Equal(t, fakeLayerData, string(plainLayerData), "layer data should match after round-trip") +} + +func TestMutateAddCompression(t *testing.T) { + dir := t.TempDir() + + engine, fromDescriptor := setup(t, dir) + defer engine.Close() + + mutator, err := New(engine, casext.DescriptorPath{Walk: []ispec.Descriptor{fromDescriptor}}) + if err != nil { + t.Fatal(err) + } + + // Test that explicitly setting the compression does what you expect: + for _, test := range []struct { + name string + useAlgo, expectedAlgo blobcompress.Algorithm + }{ + // The default with no previous layers should be gzip. + {"DefaultGzip", nil, blobcompress.Gzip}, + // Explicitly setting the algorithms. + {"Noop", blobcompress.Noop, blobcompress.Noop}, + {"Gzip", blobcompress.Gzip, blobcompress.Gzip}, + {"Zstd", blobcompress.Zstd, blobcompress.Zstd}, + } { + test := test // copy iterator + t.Run(test.name, func(t *testing.T) { + testMutateAddCompression(t, mutator, "vendor/TESTING-umoci-fake-layer", test.useAlgo, test.expectedAlgo) + }) + } + + // Check that the auto-selection of compression works properly. + t.Run("Auto", func(t *testing.T) { + for i, test := range []struct { + name string + useAlgo, expectedAlgo blobcompress.Algorithm + }{ + // Basic inheritance for zstd. + {"ExplicitZstd", blobcompress.Zstd, blobcompress.Zstd}, + {"AutoZstd", nil, blobcompress.Zstd}, + // Inheritance skips noop. + {"ExplicitNoop", blobcompress.Noop, blobcompress.Noop}, + {"AutoZstd-SkipNoop", nil, blobcompress.Zstd}, + // Basic inheritance for gzip. + {"ExplicitGzip", blobcompress.Gzip, blobcompress.Gzip}, + {"AutoGzip", nil, blobcompress.Gzip}, + // Inheritance skips noop. + {"ExplicitNoop", blobcompress.Noop, blobcompress.Noop}, + {"AutoGzip-SkipNoop", nil, blobcompress.Gzip}, + } { + test := test // copy iterator + t.Run(fmt.Sprintf("Step%d-%s", i, test.name), func(t *testing.T) { + testMutateAddCompression(t, mutator, "vendor/TESTING-umoci-fake-layer", test.useAlgo, test.expectedAlgo) + }) + } + }) +} + func TestMutateAddExisting(t *testing.T) { dir, err := ioutil.TempDir("", "umoci-TestMutateAddExisting") if err != nil { diff --git a/repack.go b/repack.go index a66db075..d3e8e7eb 100644 --- a/repack.go +++ b/repack.go @@ -36,7 +36,9 @@ import ( // Repack repacks a bundle into an image adding a new layer for the changed // data in the bundle. -func Repack(engineExt casext.Engine, tagName string, bundlePath string, meta Meta, history *ispec.History, filters []mtreefilter.FilterFunc, refreshBundle bool, mutator *mutate.Mutator) error { +// +// If layerCompressor is nil, the compression algorithm is auto-selected. +func Repack(engineExt casext.Engine, tagName string, bundlePath string, meta Meta, history *ispec.History, filters []mtreefilter.FilterFunc, refreshBundle bool, mutator *mutate.Mutator, layerCompressor mutate.Compressor) error { mtreeName := strings.Replace(meta.From.Descriptor().Digest.String(), ":", "_", 1) mtreePath := filepath.Join(bundlePath, mtreeName+".mtree") fullRootfsPath := filepath.Join(bundlePath, layer.RootfsName) @@ -112,7 +114,7 @@ func Repack(engineExt casext.Engine, tagName string, bundlePath string, meta Met } defer reader.Close() - if _, err := mutator.Add(context.Background(), ispec.MediaTypeImageLayer, reader, history, mutate.GzipCompressor, nil); err != nil { + if _, err := mutator.Add(context.Background(), ispec.MediaTypeImageLayer, reader, history, layerCompressor, nil); err != nil { return fmt.Errorf("add diff layer: %w", err) } } diff --git a/vendor/github.com/stretchr/testify/require/doc.go b/vendor/github.com/stretchr/testify/require/doc.go new file mode 100644 index 00000000..96843472 --- /dev/null +++ b/vendor/github.com/stretchr/testify/require/doc.go @@ -0,0 +1,29 @@ +// Package require implements the same assertions as the `assert` package but +// stops test execution when a test fails. +// +// # Example Usage +// +// The following is a complete example using require in a standard test function: +// +// import ( +// "testing" +// "github.com/stretchr/testify/require" +// ) +// +// func TestSomething(t *testing.T) { +// +// var a string = "Hello" +// var b string = "Hello" +// +// require.Equal(t, a, b, "The two words should be the same.") +// +// } +// +// # Assertions +// +// The `require` package have same global functions as in the `assert` package, +// but instead of returning a boolean result they call `t.FailNow()`. +// +// Every assertion function also takes an optional string message as the final argument, +// allowing custom error messages to be appended to the message the assertion method outputs. +package require diff --git a/vendor/github.com/stretchr/testify/require/forward_requirements.go b/vendor/github.com/stretchr/testify/require/forward_requirements.go new file mode 100644 index 00000000..1dcb2338 --- /dev/null +++ b/vendor/github.com/stretchr/testify/require/forward_requirements.go @@ -0,0 +1,16 @@ +package require + +// Assertions provides assertion methods around the +// TestingT interface. +type Assertions struct { + t TestingT +} + +// New makes a new Assertions object for the specified TestingT. +func New(t TestingT) *Assertions { + return &Assertions{ + t: t, + } +} + +//go:generate sh -c "cd ../_codegen && go build && cd - && ../_codegen/_codegen -output-package=require -template=require_forward.go.tmpl -include-format-funcs" diff --git a/vendor/github.com/stretchr/testify/require/require.go b/vendor/github.com/stretchr/testify/require/require.go new file mode 100644 index 00000000..506a82f8 --- /dev/null +++ b/vendor/github.com/stretchr/testify/require/require.go @@ -0,0 +1,2060 @@ +// Code generated with github.com/stretchr/testify/_codegen; DO NOT EDIT. + +package require + +import ( + assert "github.com/stretchr/testify/assert" + http "net/http" + url "net/url" + time "time" +) + +// Condition uses a Comparison to assert a complex condition. +func Condition(t TestingT, comp assert.Comparison, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Condition(t, comp, msgAndArgs...) { + return + } + t.FailNow() +} + +// Conditionf uses a Comparison to assert a complex condition. +func Conditionf(t TestingT, comp assert.Comparison, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Conditionf(t, comp, msg, args...) { + return + } + t.FailNow() +} + +// Contains asserts that the specified string, list(array, slice...) or map contains the +// specified substring or element. +// +// assert.Contains(t, "Hello World", "World") +// assert.Contains(t, ["Hello", "World"], "World") +// assert.Contains(t, {"Hello": "World"}, "Hello") +func Contains(t TestingT, s interface{}, contains interface{}, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Contains(t, s, contains, msgAndArgs...) { + return + } + t.FailNow() +} + +// Containsf asserts that the specified string, list(array, slice...) or map contains the +// specified substring or element. +// +// assert.Containsf(t, "Hello World", "World", "error message %s", "formatted") +// assert.Containsf(t, ["Hello", "World"], "World", "error message %s", "formatted") +// assert.Containsf(t, {"Hello": "World"}, "Hello", "error message %s", "formatted") +func Containsf(t TestingT, s interface{}, contains interface{}, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Containsf(t, s, contains, msg, args...) { + return + } + t.FailNow() +} + +// DirExists checks whether a directory exists in the given path. It also fails +// if the path is a file rather a directory or there is an error checking whether it exists. +func DirExists(t TestingT, path string, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.DirExists(t, path, msgAndArgs...) { + return + } + t.FailNow() +} + +// DirExistsf checks whether a directory exists in the given path. It also fails +// if the path is a file rather a directory or there is an error checking whether it exists. +func DirExistsf(t TestingT, path string, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.DirExistsf(t, path, msg, args...) { + return + } + t.FailNow() +} + +// ElementsMatch asserts that the specified listA(array, slice...) is equal to specified +// listB(array, slice...) ignoring the order of the elements. If there are duplicate elements, +// the number of appearances of each of them in both lists should match. +// +// assert.ElementsMatch(t, [1, 3, 2, 3], [1, 3, 3, 2]) +func ElementsMatch(t TestingT, listA interface{}, listB interface{}, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.ElementsMatch(t, listA, listB, msgAndArgs...) { + return + } + t.FailNow() +} + +// ElementsMatchf asserts that the specified listA(array, slice...) is equal to specified +// listB(array, slice...) ignoring the order of the elements. If there are duplicate elements, +// the number of appearances of each of them in both lists should match. +// +// assert.ElementsMatchf(t, [1, 3, 2, 3], [1, 3, 3, 2], "error message %s", "formatted") +func ElementsMatchf(t TestingT, listA interface{}, listB interface{}, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.ElementsMatchf(t, listA, listB, msg, args...) { + return + } + t.FailNow() +} + +// Empty asserts that the specified object is empty. I.e. nil, "", false, 0 or either +// a slice or a channel with len == 0. +// +// assert.Empty(t, obj) +func Empty(t TestingT, object interface{}, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Empty(t, object, msgAndArgs...) { + return + } + t.FailNow() +} + +// Emptyf asserts that the specified object is empty. I.e. nil, "", false, 0 or either +// a slice or a channel with len == 0. +// +// assert.Emptyf(t, obj, "error message %s", "formatted") +func Emptyf(t TestingT, object interface{}, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Emptyf(t, object, msg, args...) { + return + } + t.FailNow() +} + +// Equal asserts that two objects are equal. +// +// assert.Equal(t, 123, 123) +// +// Pointer variable equality is determined based on the equality of the +// referenced values (as opposed to the memory addresses). Function equality +// cannot be determined and will always fail. +func Equal(t TestingT, expected interface{}, actual interface{}, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Equal(t, expected, actual, msgAndArgs...) { + return + } + t.FailNow() +} + +// EqualError asserts that a function returned an error (i.e. not `nil`) +// and that it is equal to the provided error. +// +// actualObj, err := SomeFunction() +// assert.EqualError(t, err, expectedErrorString) +func EqualError(t TestingT, theError error, errString string, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.EqualError(t, theError, errString, msgAndArgs...) { + return + } + t.FailNow() +} + +// EqualErrorf asserts that a function returned an error (i.e. not `nil`) +// and that it is equal to the provided error. +// +// actualObj, err := SomeFunction() +// assert.EqualErrorf(t, err, expectedErrorString, "error message %s", "formatted") +func EqualErrorf(t TestingT, theError error, errString string, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.EqualErrorf(t, theError, errString, msg, args...) { + return + } + t.FailNow() +} + +// EqualExportedValues asserts that the types of two objects are equal and their public +// fields are also equal. This is useful for comparing structs that have private fields +// that could potentially differ. +// +// type S struct { +// Exported int +// notExported int +// } +// assert.EqualExportedValues(t, S{1, 2}, S{1, 3}) => true +// assert.EqualExportedValues(t, S{1, 2}, S{2, 3}) => false +func EqualExportedValues(t TestingT, expected interface{}, actual interface{}, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.EqualExportedValues(t, expected, actual, msgAndArgs...) { + return + } + t.FailNow() +} + +// EqualExportedValuesf asserts that the types of two objects are equal and their public +// fields are also equal. This is useful for comparing structs that have private fields +// that could potentially differ. +// +// type S struct { +// Exported int +// notExported int +// } +// assert.EqualExportedValuesf(t, S{1, 2}, S{1, 3}, "error message %s", "formatted") => true +// assert.EqualExportedValuesf(t, S{1, 2}, S{2, 3}, "error message %s", "formatted") => false +func EqualExportedValuesf(t TestingT, expected interface{}, actual interface{}, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.EqualExportedValuesf(t, expected, actual, msg, args...) { + return + } + t.FailNow() +} + +// EqualValues asserts that two objects are equal or convertible to the same types +// and equal. +// +// assert.EqualValues(t, uint32(123), int32(123)) +func EqualValues(t TestingT, expected interface{}, actual interface{}, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.EqualValues(t, expected, actual, msgAndArgs...) { + return + } + t.FailNow() +} + +// EqualValuesf asserts that two objects are equal or convertible to the same types +// and equal. +// +// assert.EqualValuesf(t, uint32(123), int32(123), "error message %s", "formatted") +func EqualValuesf(t TestingT, expected interface{}, actual interface{}, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.EqualValuesf(t, expected, actual, msg, args...) { + return + } + t.FailNow() +} + +// Equalf asserts that two objects are equal. +// +// assert.Equalf(t, 123, 123, "error message %s", "formatted") +// +// Pointer variable equality is determined based on the equality of the +// referenced values (as opposed to the memory addresses). Function equality +// cannot be determined and will always fail. +func Equalf(t TestingT, expected interface{}, actual interface{}, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Equalf(t, expected, actual, msg, args...) { + return + } + t.FailNow() +} + +// Error asserts that a function returned an error (i.e. not `nil`). +// +// actualObj, err := SomeFunction() +// if assert.Error(t, err) { +// assert.Equal(t, expectedError, err) +// } +func Error(t TestingT, err error, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Error(t, err, msgAndArgs...) { + return + } + t.FailNow() +} + +// ErrorAs asserts that at least one of the errors in err's chain matches target, and if so, sets target to that error value. +// This is a wrapper for errors.As. +func ErrorAs(t TestingT, err error, target interface{}, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.ErrorAs(t, err, target, msgAndArgs...) { + return + } + t.FailNow() +} + +// ErrorAsf asserts that at least one of the errors in err's chain matches target, and if so, sets target to that error value. +// This is a wrapper for errors.As. +func ErrorAsf(t TestingT, err error, target interface{}, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.ErrorAsf(t, err, target, msg, args...) { + return + } + t.FailNow() +} + +// ErrorContains asserts that a function returned an error (i.e. not `nil`) +// and that the error contains the specified substring. +// +// actualObj, err := SomeFunction() +// assert.ErrorContains(t, err, expectedErrorSubString) +func ErrorContains(t TestingT, theError error, contains string, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.ErrorContains(t, theError, contains, msgAndArgs...) { + return + } + t.FailNow() +} + +// ErrorContainsf asserts that a function returned an error (i.e. not `nil`) +// and that the error contains the specified substring. +// +// actualObj, err := SomeFunction() +// assert.ErrorContainsf(t, err, expectedErrorSubString, "error message %s", "formatted") +func ErrorContainsf(t TestingT, theError error, contains string, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.ErrorContainsf(t, theError, contains, msg, args...) { + return + } + t.FailNow() +} + +// ErrorIs asserts that at least one of the errors in err's chain matches target. +// This is a wrapper for errors.Is. +func ErrorIs(t TestingT, err error, target error, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.ErrorIs(t, err, target, msgAndArgs...) { + return + } + t.FailNow() +} + +// ErrorIsf asserts that at least one of the errors in err's chain matches target. +// This is a wrapper for errors.Is. +func ErrorIsf(t TestingT, err error, target error, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.ErrorIsf(t, err, target, msg, args...) { + return + } + t.FailNow() +} + +// Errorf asserts that a function returned an error (i.e. not `nil`). +// +// actualObj, err := SomeFunction() +// if assert.Errorf(t, err, "error message %s", "formatted") { +// assert.Equal(t, expectedErrorf, err) +// } +func Errorf(t TestingT, err error, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Errorf(t, err, msg, args...) { + return + } + t.FailNow() +} + +// Eventually asserts that given condition will be met in waitFor time, +// periodically checking target function each tick. +// +// assert.Eventually(t, func() bool { return true; }, time.Second, 10*time.Millisecond) +func Eventually(t TestingT, condition func() bool, waitFor time.Duration, tick time.Duration, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Eventually(t, condition, waitFor, tick, msgAndArgs...) { + return + } + t.FailNow() +} + +// EventuallyWithT asserts that given condition will be met in waitFor time, +// periodically checking target function each tick. In contrast to Eventually, +// it supplies a CollectT to the condition function, so that the condition +// function can use the CollectT to call other assertions. +// The condition is considered "met" if no errors are raised in a tick. +// The supplied CollectT collects all errors from one tick (if there are any). +// If the condition is not met before waitFor, the collected errors of +// the last tick are copied to t. +// +// externalValue := false +// go func() { +// time.Sleep(8*time.Second) +// externalValue = true +// }() +// assert.EventuallyWithT(t, func(c *assert.CollectT) { +// // add assertions as needed; any assertion failure will fail the current tick +// assert.True(c, externalValue, "expected 'externalValue' to be true") +// }, 1*time.Second, 10*time.Second, "external state has not changed to 'true'; still false") +func EventuallyWithT(t TestingT, condition func(collect *assert.CollectT), waitFor time.Duration, tick time.Duration, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.EventuallyWithT(t, condition, waitFor, tick, msgAndArgs...) { + return + } + t.FailNow() +} + +// EventuallyWithTf asserts that given condition will be met in waitFor time, +// periodically checking target function each tick. In contrast to Eventually, +// it supplies a CollectT to the condition function, so that the condition +// function can use the CollectT to call other assertions. +// The condition is considered "met" if no errors are raised in a tick. +// The supplied CollectT collects all errors from one tick (if there are any). +// If the condition is not met before waitFor, the collected errors of +// the last tick are copied to t. +// +// externalValue := false +// go func() { +// time.Sleep(8*time.Second) +// externalValue = true +// }() +// assert.EventuallyWithTf(t, func(c *assert.CollectT, "error message %s", "formatted") { +// // add assertions as needed; any assertion failure will fail the current tick +// assert.True(c, externalValue, "expected 'externalValue' to be true") +// }, 1*time.Second, 10*time.Second, "external state has not changed to 'true'; still false") +func EventuallyWithTf(t TestingT, condition func(collect *assert.CollectT), waitFor time.Duration, tick time.Duration, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.EventuallyWithTf(t, condition, waitFor, tick, msg, args...) { + return + } + t.FailNow() +} + +// Eventuallyf asserts that given condition will be met in waitFor time, +// periodically checking target function each tick. +// +// assert.Eventuallyf(t, func() bool { return true; }, time.Second, 10*time.Millisecond, "error message %s", "formatted") +func Eventuallyf(t TestingT, condition func() bool, waitFor time.Duration, tick time.Duration, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Eventuallyf(t, condition, waitFor, tick, msg, args...) { + return + } + t.FailNow() +} + +// Exactly asserts that two objects are equal in value and type. +// +// assert.Exactly(t, int32(123), int64(123)) +func Exactly(t TestingT, expected interface{}, actual interface{}, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Exactly(t, expected, actual, msgAndArgs...) { + return + } + t.FailNow() +} + +// Exactlyf asserts that two objects are equal in value and type. +// +// assert.Exactlyf(t, int32(123), int64(123), "error message %s", "formatted") +func Exactlyf(t TestingT, expected interface{}, actual interface{}, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Exactlyf(t, expected, actual, msg, args...) { + return + } + t.FailNow() +} + +// Fail reports a failure through +func Fail(t TestingT, failureMessage string, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Fail(t, failureMessage, msgAndArgs...) { + return + } + t.FailNow() +} + +// FailNow fails test +func FailNow(t TestingT, failureMessage string, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.FailNow(t, failureMessage, msgAndArgs...) { + return + } + t.FailNow() +} + +// FailNowf fails test +func FailNowf(t TestingT, failureMessage string, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.FailNowf(t, failureMessage, msg, args...) { + return + } + t.FailNow() +} + +// Failf reports a failure through +func Failf(t TestingT, failureMessage string, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Failf(t, failureMessage, msg, args...) { + return + } + t.FailNow() +} + +// False asserts that the specified value is false. +// +// assert.False(t, myBool) +func False(t TestingT, value bool, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.False(t, value, msgAndArgs...) { + return + } + t.FailNow() +} + +// Falsef asserts that the specified value is false. +// +// assert.Falsef(t, myBool, "error message %s", "formatted") +func Falsef(t TestingT, value bool, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Falsef(t, value, msg, args...) { + return + } + t.FailNow() +} + +// FileExists checks whether a file exists in the given path. It also fails if +// the path points to a directory or there is an error when trying to check the file. +func FileExists(t TestingT, path string, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.FileExists(t, path, msgAndArgs...) { + return + } + t.FailNow() +} + +// FileExistsf checks whether a file exists in the given path. It also fails if +// the path points to a directory or there is an error when trying to check the file. +func FileExistsf(t TestingT, path string, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.FileExistsf(t, path, msg, args...) { + return + } + t.FailNow() +} + +// Greater asserts that the first element is greater than the second +// +// assert.Greater(t, 2, 1) +// assert.Greater(t, float64(2), float64(1)) +// assert.Greater(t, "b", "a") +func Greater(t TestingT, e1 interface{}, e2 interface{}, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Greater(t, e1, e2, msgAndArgs...) { + return + } + t.FailNow() +} + +// GreaterOrEqual asserts that the first element is greater than or equal to the second +// +// assert.GreaterOrEqual(t, 2, 1) +// assert.GreaterOrEqual(t, 2, 2) +// assert.GreaterOrEqual(t, "b", "a") +// assert.GreaterOrEqual(t, "b", "b") +func GreaterOrEqual(t TestingT, e1 interface{}, e2 interface{}, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.GreaterOrEqual(t, e1, e2, msgAndArgs...) { + return + } + t.FailNow() +} + +// GreaterOrEqualf asserts that the first element is greater than or equal to the second +// +// assert.GreaterOrEqualf(t, 2, 1, "error message %s", "formatted") +// assert.GreaterOrEqualf(t, 2, 2, "error message %s", "formatted") +// assert.GreaterOrEqualf(t, "b", "a", "error message %s", "formatted") +// assert.GreaterOrEqualf(t, "b", "b", "error message %s", "formatted") +func GreaterOrEqualf(t TestingT, e1 interface{}, e2 interface{}, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.GreaterOrEqualf(t, e1, e2, msg, args...) { + return + } + t.FailNow() +} + +// Greaterf asserts that the first element is greater than the second +// +// assert.Greaterf(t, 2, 1, "error message %s", "formatted") +// assert.Greaterf(t, float64(2), float64(1), "error message %s", "formatted") +// assert.Greaterf(t, "b", "a", "error message %s", "formatted") +func Greaterf(t TestingT, e1 interface{}, e2 interface{}, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Greaterf(t, e1, e2, msg, args...) { + return + } + t.FailNow() +} + +// HTTPBodyContains asserts that a specified handler returns a +// body that contains a string. +// +// assert.HTTPBodyContains(t, myHandler, "GET", "www.google.com", nil, "I'm Feeling Lucky") +// +// Returns whether the assertion was successful (true) or not (false). +func HTTPBodyContains(t TestingT, handler http.HandlerFunc, method string, url string, values url.Values, str interface{}, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.HTTPBodyContains(t, handler, method, url, values, str, msgAndArgs...) { + return + } + t.FailNow() +} + +// HTTPBodyContainsf asserts that a specified handler returns a +// body that contains a string. +// +// assert.HTTPBodyContainsf(t, myHandler, "GET", "www.google.com", nil, "I'm Feeling Lucky", "error message %s", "formatted") +// +// Returns whether the assertion was successful (true) or not (false). +func HTTPBodyContainsf(t TestingT, handler http.HandlerFunc, method string, url string, values url.Values, str interface{}, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.HTTPBodyContainsf(t, handler, method, url, values, str, msg, args...) { + return + } + t.FailNow() +} + +// HTTPBodyNotContains asserts that a specified handler returns a +// body that does not contain a string. +// +// assert.HTTPBodyNotContains(t, myHandler, "GET", "www.google.com", nil, "I'm Feeling Lucky") +// +// Returns whether the assertion was successful (true) or not (false). +func HTTPBodyNotContains(t TestingT, handler http.HandlerFunc, method string, url string, values url.Values, str interface{}, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.HTTPBodyNotContains(t, handler, method, url, values, str, msgAndArgs...) { + return + } + t.FailNow() +} + +// HTTPBodyNotContainsf asserts that a specified handler returns a +// body that does not contain a string. +// +// assert.HTTPBodyNotContainsf(t, myHandler, "GET", "www.google.com", nil, "I'm Feeling Lucky", "error message %s", "formatted") +// +// Returns whether the assertion was successful (true) or not (false). +func HTTPBodyNotContainsf(t TestingT, handler http.HandlerFunc, method string, url string, values url.Values, str interface{}, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.HTTPBodyNotContainsf(t, handler, method, url, values, str, msg, args...) { + return + } + t.FailNow() +} + +// HTTPError asserts that a specified handler returns an error status code. +// +// assert.HTTPError(t, myHandler, "POST", "/a/b/c", url.Values{"a": []string{"b", "c"}} +// +// Returns whether the assertion was successful (true) or not (false). +func HTTPError(t TestingT, handler http.HandlerFunc, method string, url string, values url.Values, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.HTTPError(t, handler, method, url, values, msgAndArgs...) { + return + } + t.FailNow() +} + +// HTTPErrorf asserts that a specified handler returns an error status code. +// +// assert.HTTPErrorf(t, myHandler, "POST", "/a/b/c", url.Values{"a": []string{"b", "c"}} +// +// Returns whether the assertion was successful (true) or not (false). +func HTTPErrorf(t TestingT, handler http.HandlerFunc, method string, url string, values url.Values, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.HTTPErrorf(t, handler, method, url, values, msg, args...) { + return + } + t.FailNow() +} + +// HTTPRedirect asserts that a specified handler returns a redirect status code. +// +// assert.HTTPRedirect(t, myHandler, "GET", "/a/b/c", url.Values{"a": []string{"b", "c"}} +// +// Returns whether the assertion was successful (true) or not (false). +func HTTPRedirect(t TestingT, handler http.HandlerFunc, method string, url string, values url.Values, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.HTTPRedirect(t, handler, method, url, values, msgAndArgs...) { + return + } + t.FailNow() +} + +// HTTPRedirectf asserts that a specified handler returns a redirect status code. +// +// assert.HTTPRedirectf(t, myHandler, "GET", "/a/b/c", url.Values{"a": []string{"b", "c"}} +// +// Returns whether the assertion was successful (true) or not (false). +func HTTPRedirectf(t TestingT, handler http.HandlerFunc, method string, url string, values url.Values, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.HTTPRedirectf(t, handler, method, url, values, msg, args...) { + return + } + t.FailNow() +} + +// HTTPStatusCode asserts that a specified handler returns a specified status code. +// +// assert.HTTPStatusCode(t, myHandler, "GET", "/notImplemented", nil, 501) +// +// Returns whether the assertion was successful (true) or not (false). +func HTTPStatusCode(t TestingT, handler http.HandlerFunc, method string, url string, values url.Values, statuscode int, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.HTTPStatusCode(t, handler, method, url, values, statuscode, msgAndArgs...) { + return + } + t.FailNow() +} + +// HTTPStatusCodef asserts that a specified handler returns a specified status code. +// +// assert.HTTPStatusCodef(t, myHandler, "GET", "/notImplemented", nil, 501, "error message %s", "formatted") +// +// Returns whether the assertion was successful (true) or not (false). +func HTTPStatusCodef(t TestingT, handler http.HandlerFunc, method string, url string, values url.Values, statuscode int, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.HTTPStatusCodef(t, handler, method, url, values, statuscode, msg, args...) { + return + } + t.FailNow() +} + +// HTTPSuccess asserts that a specified handler returns a success status code. +// +// assert.HTTPSuccess(t, myHandler, "POST", "http://www.google.com", nil) +// +// Returns whether the assertion was successful (true) or not (false). +func HTTPSuccess(t TestingT, handler http.HandlerFunc, method string, url string, values url.Values, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.HTTPSuccess(t, handler, method, url, values, msgAndArgs...) { + return + } + t.FailNow() +} + +// HTTPSuccessf asserts that a specified handler returns a success status code. +// +// assert.HTTPSuccessf(t, myHandler, "POST", "http://www.google.com", nil, "error message %s", "formatted") +// +// Returns whether the assertion was successful (true) or not (false). +func HTTPSuccessf(t TestingT, handler http.HandlerFunc, method string, url string, values url.Values, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.HTTPSuccessf(t, handler, method, url, values, msg, args...) { + return + } + t.FailNow() +} + +// Implements asserts that an object is implemented by the specified interface. +// +// assert.Implements(t, (*MyInterface)(nil), new(MyObject)) +func Implements(t TestingT, interfaceObject interface{}, object interface{}, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Implements(t, interfaceObject, object, msgAndArgs...) { + return + } + t.FailNow() +} + +// Implementsf asserts that an object is implemented by the specified interface. +// +// assert.Implementsf(t, (*MyInterface)(nil), new(MyObject), "error message %s", "formatted") +func Implementsf(t TestingT, interfaceObject interface{}, object interface{}, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Implementsf(t, interfaceObject, object, msg, args...) { + return + } + t.FailNow() +} + +// InDelta asserts that the two numerals are within delta of each other. +// +// assert.InDelta(t, math.Pi, 22/7.0, 0.01) +func InDelta(t TestingT, expected interface{}, actual interface{}, delta float64, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.InDelta(t, expected, actual, delta, msgAndArgs...) { + return + } + t.FailNow() +} + +// InDeltaMapValues is the same as InDelta, but it compares all values between two maps. Both maps must have exactly the same keys. +func InDeltaMapValues(t TestingT, expected interface{}, actual interface{}, delta float64, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.InDeltaMapValues(t, expected, actual, delta, msgAndArgs...) { + return + } + t.FailNow() +} + +// InDeltaMapValuesf is the same as InDelta, but it compares all values between two maps. Both maps must have exactly the same keys. +func InDeltaMapValuesf(t TestingT, expected interface{}, actual interface{}, delta float64, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.InDeltaMapValuesf(t, expected, actual, delta, msg, args...) { + return + } + t.FailNow() +} + +// InDeltaSlice is the same as InDelta, except it compares two slices. +func InDeltaSlice(t TestingT, expected interface{}, actual interface{}, delta float64, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.InDeltaSlice(t, expected, actual, delta, msgAndArgs...) { + return + } + t.FailNow() +} + +// InDeltaSlicef is the same as InDelta, except it compares two slices. +func InDeltaSlicef(t TestingT, expected interface{}, actual interface{}, delta float64, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.InDeltaSlicef(t, expected, actual, delta, msg, args...) { + return + } + t.FailNow() +} + +// InDeltaf asserts that the two numerals are within delta of each other. +// +// assert.InDeltaf(t, math.Pi, 22/7.0, 0.01, "error message %s", "formatted") +func InDeltaf(t TestingT, expected interface{}, actual interface{}, delta float64, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.InDeltaf(t, expected, actual, delta, msg, args...) { + return + } + t.FailNow() +} + +// InEpsilon asserts that expected and actual have a relative error less than epsilon +func InEpsilon(t TestingT, expected interface{}, actual interface{}, epsilon float64, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.InEpsilon(t, expected, actual, epsilon, msgAndArgs...) { + return + } + t.FailNow() +} + +// InEpsilonSlice is the same as InEpsilon, except it compares each value from two slices. +func InEpsilonSlice(t TestingT, expected interface{}, actual interface{}, epsilon float64, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.InEpsilonSlice(t, expected, actual, epsilon, msgAndArgs...) { + return + } + t.FailNow() +} + +// InEpsilonSlicef is the same as InEpsilon, except it compares each value from two slices. +func InEpsilonSlicef(t TestingT, expected interface{}, actual interface{}, epsilon float64, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.InEpsilonSlicef(t, expected, actual, epsilon, msg, args...) { + return + } + t.FailNow() +} + +// InEpsilonf asserts that expected and actual have a relative error less than epsilon +func InEpsilonf(t TestingT, expected interface{}, actual interface{}, epsilon float64, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.InEpsilonf(t, expected, actual, epsilon, msg, args...) { + return + } + t.FailNow() +} + +// IsDecreasing asserts that the collection is decreasing +// +// assert.IsDecreasing(t, []int{2, 1, 0}) +// assert.IsDecreasing(t, []float{2, 1}) +// assert.IsDecreasing(t, []string{"b", "a"}) +func IsDecreasing(t TestingT, object interface{}, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.IsDecreasing(t, object, msgAndArgs...) { + return + } + t.FailNow() +} + +// IsDecreasingf asserts that the collection is decreasing +// +// assert.IsDecreasingf(t, []int{2, 1, 0}, "error message %s", "formatted") +// assert.IsDecreasingf(t, []float{2, 1}, "error message %s", "formatted") +// assert.IsDecreasingf(t, []string{"b", "a"}, "error message %s", "formatted") +func IsDecreasingf(t TestingT, object interface{}, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.IsDecreasingf(t, object, msg, args...) { + return + } + t.FailNow() +} + +// IsIncreasing asserts that the collection is increasing +// +// assert.IsIncreasing(t, []int{1, 2, 3}) +// assert.IsIncreasing(t, []float{1, 2}) +// assert.IsIncreasing(t, []string{"a", "b"}) +func IsIncreasing(t TestingT, object interface{}, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.IsIncreasing(t, object, msgAndArgs...) { + return + } + t.FailNow() +} + +// IsIncreasingf asserts that the collection is increasing +// +// assert.IsIncreasingf(t, []int{1, 2, 3}, "error message %s", "formatted") +// assert.IsIncreasingf(t, []float{1, 2}, "error message %s", "formatted") +// assert.IsIncreasingf(t, []string{"a", "b"}, "error message %s", "formatted") +func IsIncreasingf(t TestingT, object interface{}, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.IsIncreasingf(t, object, msg, args...) { + return + } + t.FailNow() +} + +// IsNonDecreasing asserts that the collection is not decreasing +// +// assert.IsNonDecreasing(t, []int{1, 1, 2}) +// assert.IsNonDecreasing(t, []float{1, 2}) +// assert.IsNonDecreasing(t, []string{"a", "b"}) +func IsNonDecreasing(t TestingT, object interface{}, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.IsNonDecreasing(t, object, msgAndArgs...) { + return + } + t.FailNow() +} + +// IsNonDecreasingf asserts that the collection is not decreasing +// +// assert.IsNonDecreasingf(t, []int{1, 1, 2}, "error message %s", "formatted") +// assert.IsNonDecreasingf(t, []float{1, 2}, "error message %s", "formatted") +// assert.IsNonDecreasingf(t, []string{"a", "b"}, "error message %s", "formatted") +func IsNonDecreasingf(t TestingT, object interface{}, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.IsNonDecreasingf(t, object, msg, args...) { + return + } + t.FailNow() +} + +// IsNonIncreasing asserts that the collection is not increasing +// +// assert.IsNonIncreasing(t, []int{2, 1, 1}) +// assert.IsNonIncreasing(t, []float{2, 1}) +// assert.IsNonIncreasing(t, []string{"b", "a"}) +func IsNonIncreasing(t TestingT, object interface{}, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.IsNonIncreasing(t, object, msgAndArgs...) { + return + } + t.FailNow() +} + +// IsNonIncreasingf asserts that the collection is not increasing +// +// assert.IsNonIncreasingf(t, []int{2, 1, 1}, "error message %s", "formatted") +// assert.IsNonIncreasingf(t, []float{2, 1}, "error message %s", "formatted") +// assert.IsNonIncreasingf(t, []string{"b", "a"}, "error message %s", "formatted") +func IsNonIncreasingf(t TestingT, object interface{}, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.IsNonIncreasingf(t, object, msg, args...) { + return + } + t.FailNow() +} + +// IsType asserts that the specified objects are of the same type. +func IsType(t TestingT, expectedType interface{}, object interface{}, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.IsType(t, expectedType, object, msgAndArgs...) { + return + } + t.FailNow() +} + +// IsTypef asserts that the specified objects are of the same type. +func IsTypef(t TestingT, expectedType interface{}, object interface{}, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.IsTypef(t, expectedType, object, msg, args...) { + return + } + t.FailNow() +} + +// JSONEq asserts that two JSON strings are equivalent. +// +// assert.JSONEq(t, `{"hello": "world", "foo": "bar"}`, `{"foo": "bar", "hello": "world"}`) +func JSONEq(t TestingT, expected string, actual string, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.JSONEq(t, expected, actual, msgAndArgs...) { + return + } + t.FailNow() +} + +// JSONEqf asserts that two JSON strings are equivalent. +// +// assert.JSONEqf(t, `{"hello": "world", "foo": "bar"}`, `{"foo": "bar", "hello": "world"}`, "error message %s", "formatted") +func JSONEqf(t TestingT, expected string, actual string, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.JSONEqf(t, expected, actual, msg, args...) { + return + } + t.FailNow() +} + +// Len asserts that the specified object has specific length. +// Len also fails if the object has a type that len() not accept. +// +// assert.Len(t, mySlice, 3) +func Len(t TestingT, object interface{}, length int, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Len(t, object, length, msgAndArgs...) { + return + } + t.FailNow() +} + +// Lenf asserts that the specified object has specific length. +// Lenf also fails if the object has a type that len() not accept. +// +// assert.Lenf(t, mySlice, 3, "error message %s", "formatted") +func Lenf(t TestingT, object interface{}, length int, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Lenf(t, object, length, msg, args...) { + return + } + t.FailNow() +} + +// Less asserts that the first element is less than the second +// +// assert.Less(t, 1, 2) +// assert.Less(t, float64(1), float64(2)) +// assert.Less(t, "a", "b") +func Less(t TestingT, e1 interface{}, e2 interface{}, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Less(t, e1, e2, msgAndArgs...) { + return + } + t.FailNow() +} + +// LessOrEqual asserts that the first element is less than or equal to the second +// +// assert.LessOrEqual(t, 1, 2) +// assert.LessOrEqual(t, 2, 2) +// assert.LessOrEqual(t, "a", "b") +// assert.LessOrEqual(t, "b", "b") +func LessOrEqual(t TestingT, e1 interface{}, e2 interface{}, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.LessOrEqual(t, e1, e2, msgAndArgs...) { + return + } + t.FailNow() +} + +// LessOrEqualf asserts that the first element is less than or equal to the second +// +// assert.LessOrEqualf(t, 1, 2, "error message %s", "formatted") +// assert.LessOrEqualf(t, 2, 2, "error message %s", "formatted") +// assert.LessOrEqualf(t, "a", "b", "error message %s", "formatted") +// assert.LessOrEqualf(t, "b", "b", "error message %s", "formatted") +func LessOrEqualf(t TestingT, e1 interface{}, e2 interface{}, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.LessOrEqualf(t, e1, e2, msg, args...) { + return + } + t.FailNow() +} + +// Lessf asserts that the first element is less than the second +// +// assert.Lessf(t, 1, 2, "error message %s", "formatted") +// assert.Lessf(t, float64(1), float64(2), "error message %s", "formatted") +// assert.Lessf(t, "a", "b", "error message %s", "formatted") +func Lessf(t TestingT, e1 interface{}, e2 interface{}, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Lessf(t, e1, e2, msg, args...) { + return + } + t.FailNow() +} + +// Negative asserts that the specified element is negative +// +// assert.Negative(t, -1) +// assert.Negative(t, -1.23) +func Negative(t TestingT, e interface{}, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Negative(t, e, msgAndArgs...) { + return + } + t.FailNow() +} + +// Negativef asserts that the specified element is negative +// +// assert.Negativef(t, -1, "error message %s", "formatted") +// assert.Negativef(t, -1.23, "error message %s", "formatted") +func Negativef(t TestingT, e interface{}, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Negativef(t, e, msg, args...) { + return + } + t.FailNow() +} + +// Never asserts that the given condition doesn't satisfy in waitFor time, +// periodically checking the target function each tick. +// +// assert.Never(t, func() bool { return false; }, time.Second, 10*time.Millisecond) +func Never(t TestingT, condition func() bool, waitFor time.Duration, tick time.Duration, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Never(t, condition, waitFor, tick, msgAndArgs...) { + return + } + t.FailNow() +} + +// Neverf asserts that the given condition doesn't satisfy in waitFor time, +// periodically checking the target function each tick. +// +// assert.Neverf(t, func() bool { return false; }, time.Second, 10*time.Millisecond, "error message %s", "formatted") +func Neverf(t TestingT, condition func() bool, waitFor time.Duration, tick time.Duration, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Neverf(t, condition, waitFor, tick, msg, args...) { + return + } + t.FailNow() +} + +// Nil asserts that the specified object is nil. +// +// assert.Nil(t, err) +func Nil(t TestingT, object interface{}, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Nil(t, object, msgAndArgs...) { + return + } + t.FailNow() +} + +// Nilf asserts that the specified object is nil. +// +// assert.Nilf(t, err, "error message %s", "formatted") +func Nilf(t TestingT, object interface{}, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Nilf(t, object, msg, args...) { + return + } + t.FailNow() +} + +// NoDirExists checks whether a directory does not exist in the given path. +// It fails if the path points to an existing _directory_ only. +func NoDirExists(t TestingT, path string, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.NoDirExists(t, path, msgAndArgs...) { + return + } + t.FailNow() +} + +// NoDirExistsf checks whether a directory does not exist in the given path. +// It fails if the path points to an existing _directory_ only. +func NoDirExistsf(t TestingT, path string, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.NoDirExistsf(t, path, msg, args...) { + return + } + t.FailNow() +} + +// NoError asserts that a function returned no error (i.e. `nil`). +// +// actualObj, err := SomeFunction() +// if assert.NoError(t, err) { +// assert.Equal(t, expectedObj, actualObj) +// } +func NoError(t TestingT, err error, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.NoError(t, err, msgAndArgs...) { + return + } + t.FailNow() +} + +// NoErrorf asserts that a function returned no error (i.e. `nil`). +// +// actualObj, err := SomeFunction() +// if assert.NoErrorf(t, err, "error message %s", "formatted") { +// assert.Equal(t, expectedObj, actualObj) +// } +func NoErrorf(t TestingT, err error, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.NoErrorf(t, err, msg, args...) { + return + } + t.FailNow() +} + +// NoFileExists checks whether a file does not exist in a given path. It fails +// if the path points to an existing _file_ only. +func NoFileExists(t TestingT, path string, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.NoFileExists(t, path, msgAndArgs...) { + return + } + t.FailNow() +} + +// NoFileExistsf checks whether a file does not exist in a given path. It fails +// if the path points to an existing _file_ only. +func NoFileExistsf(t TestingT, path string, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.NoFileExistsf(t, path, msg, args...) { + return + } + t.FailNow() +} + +// NotContains asserts that the specified string, list(array, slice...) or map does NOT contain the +// specified substring or element. +// +// assert.NotContains(t, "Hello World", "Earth") +// assert.NotContains(t, ["Hello", "World"], "Earth") +// assert.NotContains(t, {"Hello": "World"}, "Earth") +func NotContains(t TestingT, s interface{}, contains interface{}, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.NotContains(t, s, contains, msgAndArgs...) { + return + } + t.FailNow() +} + +// NotContainsf asserts that the specified string, list(array, slice...) or map does NOT contain the +// specified substring or element. +// +// assert.NotContainsf(t, "Hello World", "Earth", "error message %s", "formatted") +// assert.NotContainsf(t, ["Hello", "World"], "Earth", "error message %s", "formatted") +// assert.NotContainsf(t, {"Hello": "World"}, "Earth", "error message %s", "formatted") +func NotContainsf(t TestingT, s interface{}, contains interface{}, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.NotContainsf(t, s, contains, msg, args...) { + return + } + t.FailNow() +} + +// NotEmpty asserts that the specified object is NOT empty. I.e. not nil, "", false, 0 or either +// a slice or a channel with len == 0. +// +// if assert.NotEmpty(t, obj) { +// assert.Equal(t, "two", obj[1]) +// } +func NotEmpty(t TestingT, object interface{}, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.NotEmpty(t, object, msgAndArgs...) { + return + } + t.FailNow() +} + +// NotEmptyf asserts that the specified object is NOT empty. I.e. not nil, "", false, 0 or either +// a slice or a channel with len == 0. +// +// if assert.NotEmptyf(t, obj, "error message %s", "formatted") { +// assert.Equal(t, "two", obj[1]) +// } +func NotEmptyf(t TestingT, object interface{}, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.NotEmptyf(t, object, msg, args...) { + return + } + t.FailNow() +} + +// NotEqual asserts that the specified values are NOT equal. +// +// assert.NotEqual(t, obj1, obj2) +// +// Pointer variable equality is determined based on the equality of the +// referenced values (as opposed to the memory addresses). +func NotEqual(t TestingT, expected interface{}, actual interface{}, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.NotEqual(t, expected, actual, msgAndArgs...) { + return + } + t.FailNow() +} + +// NotEqualValues asserts that two objects are not equal even when converted to the same type +// +// assert.NotEqualValues(t, obj1, obj2) +func NotEqualValues(t TestingT, expected interface{}, actual interface{}, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.NotEqualValues(t, expected, actual, msgAndArgs...) { + return + } + t.FailNow() +} + +// NotEqualValuesf asserts that two objects are not equal even when converted to the same type +// +// assert.NotEqualValuesf(t, obj1, obj2, "error message %s", "formatted") +func NotEqualValuesf(t TestingT, expected interface{}, actual interface{}, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.NotEqualValuesf(t, expected, actual, msg, args...) { + return + } + t.FailNow() +} + +// NotEqualf asserts that the specified values are NOT equal. +// +// assert.NotEqualf(t, obj1, obj2, "error message %s", "formatted") +// +// Pointer variable equality is determined based on the equality of the +// referenced values (as opposed to the memory addresses). +func NotEqualf(t TestingT, expected interface{}, actual interface{}, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.NotEqualf(t, expected, actual, msg, args...) { + return + } + t.FailNow() +} + +// NotErrorIs asserts that at none of the errors in err's chain matches target. +// This is a wrapper for errors.Is. +func NotErrorIs(t TestingT, err error, target error, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.NotErrorIs(t, err, target, msgAndArgs...) { + return + } + t.FailNow() +} + +// NotErrorIsf asserts that at none of the errors in err's chain matches target. +// This is a wrapper for errors.Is. +func NotErrorIsf(t TestingT, err error, target error, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.NotErrorIsf(t, err, target, msg, args...) { + return + } + t.FailNow() +} + +// NotImplements asserts that an object does not implement the specified interface. +// +// assert.NotImplements(t, (*MyInterface)(nil), new(MyObject)) +func NotImplements(t TestingT, interfaceObject interface{}, object interface{}, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.NotImplements(t, interfaceObject, object, msgAndArgs...) { + return + } + t.FailNow() +} + +// NotImplementsf asserts that an object does not implement the specified interface. +// +// assert.NotImplementsf(t, (*MyInterface)(nil), new(MyObject), "error message %s", "formatted") +func NotImplementsf(t TestingT, interfaceObject interface{}, object interface{}, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.NotImplementsf(t, interfaceObject, object, msg, args...) { + return + } + t.FailNow() +} + +// NotNil asserts that the specified object is not nil. +// +// assert.NotNil(t, err) +func NotNil(t TestingT, object interface{}, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.NotNil(t, object, msgAndArgs...) { + return + } + t.FailNow() +} + +// NotNilf asserts that the specified object is not nil. +// +// assert.NotNilf(t, err, "error message %s", "formatted") +func NotNilf(t TestingT, object interface{}, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.NotNilf(t, object, msg, args...) { + return + } + t.FailNow() +} + +// NotPanics asserts that the code inside the specified PanicTestFunc does NOT panic. +// +// assert.NotPanics(t, func(){ RemainCalm() }) +func NotPanics(t TestingT, f assert.PanicTestFunc, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.NotPanics(t, f, msgAndArgs...) { + return + } + t.FailNow() +} + +// NotPanicsf asserts that the code inside the specified PanicTestFunc does NOT panic. +// +// assert.NotPanicsf(t, func(){ RemainCalm() }, "error message %s", "formatted") +func NotPanicsf(t TestingT, f assert.PanicTestFunc, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.NotPanicsf(t, f, msg, args...) { + return + } + t.FailNow() +} + +// NotRegexp asserts that a specified regexp does not match a string. +// +// assert.NotRegexp(t, regexp.MustCompile("starts"), "it's starting") +// assert.NotRegexp(t, "^start", "it's not starting") +func NotRegexp(t TestingT, rx interface{}, str interface{}, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.NotRegexp(t, rx, str, msgAndArgs...) { + return + } + t.FailNow() +} + +// NotRegexpf asserts that a specified regexp does not match a string. +// +// assert.NotRegexpf(t, regexp.MustCompile("starts"), "it's starting", "error message %s", "formatted") +// assert.NotRegexpf(t, "^start", "it's not starting", "error message %s", "formatted") +func NotRegexpf(t TestingT, rx interface{}, str interface{}, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.NotRegexpf(t, rx, str, msg, args...) { + return + } + t.FailNow() +} + +// NotSame asserts that two pointers do not reference the same object. +// +// assert.NotSame(t, ptr1, ptr2) +// +// Both arguments must be pointer variables. Pointer variable sameness is +// determined based on the equality of both type and value. +func NotSame(t TestingT, expected interface{}, actual interface{}, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.NotSame(t, expected, actual, msgAndArgs...) { + return + } + t.FailNow() +} + +// NotSamef asserts that two pointers do not reference the same object. +// +// assert.NotSamef(t, ptr1, ptr2, "error message %s", "formatted") +// +// Both arguments must be pointer variables. Pointer variable sameness is +// determined based on the equality of both type and value. +func NotSamef(t TestingT, expected interface{}, actual interface{}, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.NotSamef(t, expected, actual, msg, args...) { + return + } + t.FailNow() +} + +// NotSubset asserts that the specified list(array, slice...) or map does NOT +// contain all elements given in the specified subset list(array, slice...) or +// map. +// +// assert.NotSubset(t, [1, 3, 4], [1, 2]) +// assert.NotSubset(t, {"x": 1, "y": 2}, {"z": 3}) +func NotSubset(t TestingT, list interface{}, subset interface{}, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.NotSubset(t, list, subset, msgAndArgs...) { + return + } + t.FailNow() +} + +// NotSubsetf asserts that the specified list(array, slice...) or map does NOT +// contain all elements given in the specified subset list(array, slice...) or +// map. +// +// assert.NotSubsetf(t, [1, 3, 4], [1, 2], "error message %s", "formatted") +// assert.NotSubsetf(t, {"x": 1, "y": 2}, {"z": 3}, "error message %s", "formatted") +func NotSubsetf(t TestingT, list interface{}, subset interface{}, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.NotSubsetf(t, list, subset, msg, args...) { + return + } + t.FailNow() +} + +// NotZero asserts that i is not the zero value for its type. +func NotZero(t TestingT, i interface{}, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.NotZero(t, i, msgAndArgs...) { + return + } + t.FailNow() +} + +// NotZerof asserts that i is not the zero value for its type. +func NotZerof(t TestingT, i interface{}, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.NotZerof(t, i, msg, args...) { + return + } + t.FailNow() +} + +// Panics asserts that the code inside the specified PanicTestFunc panics. +// +// assert.Panics(t, func(){ GoCrazy() }) +func Panics(t TestingT, f assert.PanicTestFunc, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Panics(t, f, msgAndArgs...) { + return + } + t.FailNow() +} + +// PanicsWithError asserts that the code inside the specified PanicTestFunc +// panics, and that the recovered panic value is an error that satisfies the +// EqualError comparison. +// +// assert.PanicsWithError(t, "crazy error", func(){ GoCrazy() }) +func PanicsWithError(t TestingT, errString string, f assert.PanicTestFunc, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.PanicsWithError(t, errString, f, msgAndArgs...) { + return + } + t.FailNow() +} + +// PanicsWithErrorf asserts that the code inside the specified PanicTestFunc +// panics, and that the recovered panic value is an error that satisfies the +// EqualError comparison. +// +// assert.PanicsWithErrorf(t, "crazy error", func(){ GoCrazy() }, "error message %s", "formatted") +func PanicsWithErrorf(t TestingT, errString string, f assert.PanicTestFunc, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.PanicsWithErrorf(t, errString, f, msg, args...) { + return + } + t.FailNow() +} + +// PanicsWithValue asserts that the code inside the specified PanicTestFunc panics, and that +// the recovered panic value equals the expected panic value. +// +// assert.PanicsWithValue(t, "crazy error", func(){ GoCrazy() }) +func PanicsWithValue(t TestingT, expected interface{}, f assert.PanicTestFunc, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.PanicsWithValue(t, expected, f, msgAndArgs...) { + return + } + t.FailNow() +} + +// PanicsWithValuef asserts that the code inside the specified PanicTestFunc panics, and that +// the recovered panic value equals the expected panic value. +// +// assert.PanicsWithValuef(t, "crazy error", func(){ GoCrazy() }, "error message %s", "formatted") +func PanicsWithValuef(t TestingT, expected interface{}, f assert.PanicTestFunc, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.PanicsWithValuef(t, expected, f, msg, args...) { + return + } + t.FailNow() +} + +// Panicsf asserts that the code inside the specified PanicTestFunc panics. +// +// assert.Panicsf(t, func(){ GoCrazy() }, "error message %s", "formatted") +func Panicsf(t TestingT, f assert.PanicTestFunc, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Panicsf(t, f, msg, args...) { + return + } + t.FailNow() +} + +// Positive asserts that the specified element is positive +// +// assert.Positive(t, 1) +// assert.Positive(t, 1.23) +func Positive(t TestingT, e interface{}, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Positive(t, e, msgAndArgs...) { + return + } + t.FailNow() +} + +// Positivef asserts that the specified element is positive +// +// assert.Positivef(t, 1, "error message %s", "formatted") +// assert.Positivef(t, 1.23, "error message %s", "formatted") +func Positivef(t TestingT, e interface{}, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Positivef(t, e, msg, args...) { + return + } + t.FailNow() +} + +// Regexp asserts that a specified regexp matches a string. +// +// assert.Regexp(t, regexp.MustCompile("start"), "it's starting") +// assert.Regexp(t, "start...$", "it's not starting") +func Regexp(t TestingT, rx interface{}, str interface{}, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Regexp(t, rx, str, msgAndArgs...) { + return + } + t.FailNow() +} + +// Regexpf asserts that a specified regexp matches a string. +// +// assert.Regexpf(t, regexp.MustCompile("start"), "it's starting", "error message %s", "formatted") +// assert.Regexpf(t, "start...$", "it's not starting", "error message %s", "formatted") +func Regexpf(t TestingT, rx interface{}, str interface{}, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Regexpf(t, rx, str, msg, args...) { + return + } + t.FailNow() +} + +// Same asserts that two pointers reference the same object. +// +// assert.Same(t, ptr1, ptr2) +// +// Both arguments must be pointer variables. Pointer variable sameness is +// determined based on the equality of both type and value. +func Same(t TestingT, expected interface{}, actual interface{}, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Same(t, expected, actual, msgAndArgs...) { + return + } + t.FailNow() +} + +// Samef asserts that two pointers reference the same object. +// +// assert.Samef(t, ptr1, ptr2, "error message %s", "formatted") +// +// Both arguments must be pointer variables. Pointer variable sameness is +// determined based on the equality of both type and value. +func Samef(t TestingT, expected interface{}, actual interface{}, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Samef(t, expected, actual, msg, args...) { + return + } + t.FailNow() +} + +// Subset asserts that the specified list(array, slice...) or map contains all +// elements given in the specified subset list(array, slice...) or map. +// +// assert.Subset(t, [1, 2, 3], [1, 2]) +// assert.Subset(t, {"x": 1, "y": 2}, {"x": 1}) +func Subset(t TestingT, list interface{}, subset interface{}, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Subset(t, list, subset, msgAndArgs...) { + return + } + t.FailNow() +} + +// Subsetf asserts that the specified list(array, slice...) or map contains all +// elements given in the specified subset list(array, slice...) or map. +// +// assert.Subsetf(t, [1, 2, 3], [1, 2], "error message %s", "formatted") +// assert.Subsetf(t, {"x": 1, "y": 2}, {"x": 1}, "error message %s", "formatted") +func Subsetf(t TestingT, list interface{}, subset interface{}, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Subsetf(t, list, subset, msg, args...) { + return + } + t.FailNow() +} + +// True asserts that the specified value is true. +// +// assert.True(t, myBool) +func True(t TestingT, value bool, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.True(t, value, msgAndArgs...) { + return + } + t.FailNow() +} + +// Truef asserts that the specified value is true. +// +// assert.Truef(t, myBool, "error message %s", "formatted") +func Truef(t TestingT, value bool, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Truef(t, value, msg, args...) { + return + } + t.FailNow() +} + +// WithinDuration asserts that the two times are within duration delta of each other. +// +// assert.WithinDuration(t, time.Now(), time.Now(), 10*time.Second) +func WithinDuration(t TestingT, expected time.Time, actual time.Time, delta time.Duration, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.WithinDuration(t, expected, actual, delta, msgAndArgs...) { + return + } + t.FailNow() +} + +// WithinDurationf asserts that the two times are within duration delta of each other. +// +// assert.WithinDurationf(t, time.Now(), time.Now(), 10*time.Second, "error message %s", "formatted") +func WithinDurationf(t TestingT, expected time.Time, actual time.Time, delta time.Duration, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.WithinDurationf(t, expected, actual, delta, msg, args...) { + return + } + t.FailNow() +} + +// WithinRange asserts that a time is within a time range (inclusive). +// +// assert.WithinRange(t, time.Now(), time.Now().Add(-time.Second), time.Now().Add(time.Second)) +func WithinRange(t TestingT, actual time.Time, start time.Time, end time.Time, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.WithinRange(t, actual, start, end, msgAndArgs...) { + return + } + t.FailNow() +} + +// WithinRangef asserts that a time is within a time range (inclusive). +// +// assert.WithinRangef(t, time.Now(), time.Now().Add(-time.Second), time.Now().Add(time.Second), "error message %s", "formatted") +func WithinRangef(t TestingT, actual time.Time, start time.Time, end time.Time, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.WithinRangef(t, actual, start, end, msg, args...) { + return + } + t.FailNow() +} + +// YAMLEq asserts that two YAML strings are equivalent. +func YAMLEq(t TestingT, expected string, actual string, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.YAMLEq(t, expected, actual, msgAndArgs...) { + return + } + t.FailNow() +} + +// YAMLEqf asserts that two YAML strings are equivalent. +func YAMLEqf(t TestingT, expected string, actual string, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.YAMLEqf(t, expected, actual, msg, args...) { + return + } + t.FailNow() +} + +// Zero asserts that i is the zero value for its type. +func Zero(t TestingT, i interface{}, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Zero(t, i, msgAndArgs...) { + return + } + t.FailNow() +} + +// Zerof asserts that i is the zero value for its type. +func Zerof(t TestingT, i interface{}, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Zerof(t, i, msg, args...) { + return + } + t.FailNow() +} diff --git a/vendor/github.com/stretchr/testify/require/require.go.tmpl b/vendor/github.com/stretchr/testify/require/require.go.tmpl new file mode 100644 index 00000000..55e42dde --- /dev/null +++ b/vendor/github.com/stretchr/testify/require/require.go.tmpl @@ -0,0 +1,6 @@ +{{.Comment}} +func {{.DocInfo.Name}}(t TestingT, {{.Params}}) { + if h, ok := t.(tHelper); ok { h.Helper() } + if assert.{{.DocInfo.Name}}(t, {{.ForwardedParams}}) { return } + t.FailNow() +} diff --git a/vendor/github.com/stretchr/testify/require/require_forward.go b/vendor/github.com/stretchr/testify/require/require_forward.go new file mode 100644 index 00000000..eee8310a --- /dev/null +++ b/vendor/github.com/stretchr/testify/require/require_forward.go @@ -0,0 +1,1622 @@ +// Code generated with github.com/stretchr/testify/_codegen; DO NOT EDIT. + +package require + +import ( + assert "github.com/stretchr/testify/assert" + http "net/http" + url "net/url" + time "time" +) + +// Condition uses a Comparison to assert a complex condition. +func (a *Assertions) Condition(comp assert.Comparison, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Condition(a.t, comp, msgAndArgs...) +} + +// Conditionf uses a Comparison to assert a complex condition. +func (a *Assertions) Conditionf(comp assert.Comparison, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Conditionf(a.t, comp, msg, args...) +} + +// Contains asserts that the specified string, list(array, slice...) or map contains the +// specified substring or element. +// +// a.Contains("Hello World", "World") +// a.Contains(["Hello", "World"], "World") +// a.Contains({"Hello": "World"}, "Hello") +func (a *Assertions) Contains(s interface{}, contains interface{}, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Contains(a.t, s, contains, msgAndArgs...) +} + +// Containsf asserts that the specified string, list(array, slice...) or map contains the +// specified substring or element. +// +// a.Containsf("Hello World", "World", "error message %s", "formatted") +// a.Containsf(["Hello", "World"], "World", "error message %s", "formatted") +// a.Containsf({"Hello": "World"}, "Hello", "error message %s", "formatted") +func (a *Assertions) Containsf(s interface{}, contains interface{}, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Containsf(a.t, s, contains, msg, args...) +} + +// DirExists checks whether a directory exists in the given path. It also fails +// if the path is a file rather a directory or there is an error checking whether it exists. +func (a *Assertions) DirExists(path string, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + DirExists(a.t, path, msgAndArgs...) +} + +// DirExistsf checks whether a directory exists in the given path. It also fails +// if the path is a file rather a directory or there is an error checking whether it exists. +func (a *Assertions) DirExistsf(path string, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + DirExistsf(a.t, path, msg, args...) +} + +// ElementsMatch asserts that the specified listA(array, slice...) is equal to specified +// listB(array, slice...) ignoring the order of the elements. If there are duplicate elements, +// the number of appearances of each of them in both lists should match. +// +// a.ElementsMatch([1, 3, 2, 3], [1, 3, 3, 2]) +func (a *Assertions) ElementsMatch(listA interface{}, listB interface{}, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + ElementsMatch(a.t, listA, listB, msgAndArgs...) +} + +// ElementsMatchf asserts that the specified listA(array, slice...) is equal to specified +// listB(array, slice...) ignoring the order of the elements. If there are duplicate elements, +// the number of appearances of each of them in both lists should match. +// +// a.ElementsMatchf([1, 3, 2, 3], [1, 3, 3, 2], "error message %s", "formatted") +func (a *Assertions) ElementsMatchf(listA interface{}, listB interface{}, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + ElementsMatchf(a.t, listA, listB, msg, args...) +} + +// Empty asserts that the specified object is empty. I.e. nil, "", false, 0 or either +// a slice or a channel with len == 0. +// +// a.Empty(obj) +func (a *Assertions) Empty(object interface{}, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Empty(a.t, object, msgAndArgs...) +} + +// Emptyf asserts that the specified object is empty. I.e. nil, "", false, 0 or either +// a slice or a channel with len == 0. +// +// a.Emptyf(obj, "error message %s", "formatted") +func (a *Assertions) Emptyf(object interface{}, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Emptyf(a.t, object, msg, args...) +} + +// Equal asserts that two objects are equal. +// +// a.Equal(123, 123) +// +// Pointer variable equality is determined based on the equality of the +// referenced values (as opposed to the memory addresses). Function equality +// cannot be determined and will always fail. +func (a *Assertions) Equal(expected interface{}, actual interface{}, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Equal(a.t, expected, actual, msgAndArgs...) +} + +// EqualError asserts that a function returned an error (i.e. not `nil`) +// and that it is equal to the provided error. +// +// actualObj, err := SomeFunction() +// a.EqualError(err, expectedErrorString) +func (a *Assertions) EqualError(theError error, errString string, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + EqualError(a.t, theError, errString, msgAndArgs...) +} + +// EqualErrorf asserts that a function returned an error (i.e. not `nil`) +// and that it is equal to the provided error. +// +// actualObj, err := SomeFunction() +// a.EqualErrorf(err, expectedErrorString, "error message %s", "formatted") +func (a *Assertions) EqualErrorf(theError error, errString string, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + EqualErrorf(a.t, theError, errString, msg, args...) +} + +// EqualExportedValues asserts that the types of two objects are equal and their public +// fields are also equal. This is useful for comparing structs that have private fields +// that could potentially differ. +// +// type S struct { +// Exported int +// notExported int +// } +// a.EqualExportedValues(S{1, 2}, S{1, 3}) => true +// a.EqualExportedValues(S{1, 2}, S{2, 3}) => false +func (a *Assertions) EqualExportedValues(expected interface{}, actual interface{}, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + EqualExportedValues(a.t, expected, actual, msgAndArgs...) +} + +// EqualExportedValuesf asserts that the types of two objects are equal and their public +// fields are also equal. This is useful for comparing structs that have private fields +// that could potentially differ. +// +// type S struct { +// Exported int +// notExported int +// } +// a.EqualExportedValuesf(S{1, 2}, S{1, 3}, "error message %s", "formatted") => true +// a.EqualExportedValuesf(S{1, 2}, S{2, 3}, "error message %s", "formatted") => false +func (a *Assertions) EqualExportedValuesf(expected interface{}, actual interface{}, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + EqualExportedValuesf(a.t, expected, actual, msg, args...) +} + +// EqualValues asserts that two objects are equal or convertible to the same types +// and equal. +// +// a.EqualValues(uint32(123), int32(123)) +func (a *Assertions) EqualValues(expected interface{}, actual interface{}, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + EqualValues(a.t, expected, actual, msgAndArgs...) +} + +// EqualValuesf asserts that two objects are equal or convertible to the same types +// and equal. +// +// a.EqualValuesf(uint32(123), int32(123), "error message %s", "formatted") +func (a *Assertions) EqualValuesf(expected interface{}, actual interface{}, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + EqualValuesf(a.t, expected, actual, msg, args...) +} + +// Equalf asserts that two objects are equal. +// +// a.Equalf(123, 123, "error message %s", "formatted") +// +// Pointer variable equality is determined based on the equality of the +// referenced values (as opposed to the memory addresses). Function equality +// cannot be determined and will always fail. +func (a *Assertions) Equalf(expected interface{}, actual interface{}, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Equalf(a.t, expected, actual, msg, args...) +} + +// Error asserts that a function returned an error (i.e. not `nil`). +// +// actualObj, err := SomeFunction() +// if a.Error(err) { +// assert.Equal(t, expectedError, err) +// } +func (a *Assertions) Error(err error, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Error(a.t, err, msgAndArgs...) +} + +// ErrorAs asserts that at least one of the errors in err's chain matches target, and if so, sets target to that error value. +// This is a wrapper for errors.As. +func (a *Assertions) ErrorAs(err error, target interface{}, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + ErrorAs(a.t, err, target, msgAndArgs...) +} + +// ErrorAsf asserts that at least one of the errors in err's chain matches target, and if so, sets target to that error value. +// This is a wrapper for errors.As. +func (a *Assertions) ErrorAsf(err error, target interface{}, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + ErrorAsf(a.t, err, target, msg, args...) +} + +// ErrorContains asserts that a function returned an error (i.e. not `nil`) +// and that the error contains the specified substring. +// +// actualObj, err := SomeFunction() +// a.ErrorContains(err, expectedErrorSubString) +func (a *Assertions) ErrorContains(theError error, contains string, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + ErrorContains(a.t, theError, contains, msgAndArgs...) +} + +// ErrorContainsf asserts that a function returned an error (i.e. not `nil`) +// and that the error contains the specified substring. +// +// actualObj, err := SomeFunction() +// a.ErrorContainsf(err, expectedErrorSubString, "error message %s", "formatted") +func (a *Assertions) ErrorContainsf(theError error, contains string, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + ErrorContainsf(a.t, theError, contains, msg, args...) +} + +// ErrorIs asserts that at least one of the errors in err's chain matches target. +// This is a wrapper for errors.Is. +func (a *Assertions) ErrorIs(err error, target error, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + ErrorIs(a.t, err, target, msgAndArgs...) +} + +// ErrorIsf asserts that at least one of the errors in err's chain matches target. +// This is a wrapper for errors.Is. +func (a *Assertions) ErrorIsf(err error, target error, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + ErrorIsf(a.t, err, target, msg, args...) +} + +// Errorf asserts that a function returned an error (i.e. not `nil`). +// +// actualObj, err := SomeFunction() +// if a.Errorf(err, "error message %s", "formatted") { +// assert.Equal(t, expectedErrorf, err) +// } +func (a *Assertions) Errorf(err error, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Errorf(a.t, err, msg, args...) +} + +// Eventually asserts that given condition will be met in waitFor time, +// periodically checking target function each tick. +// +// a.Eventually(func() bool { return true; }, time.Second, 10*time.Millisecond) +func (a *Assertions) Eventually(condition func() bool, waitFor time.Duration, tick time.Duration, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Eventually(a.t, condition, waitFor, tick, msgAndArgs...) +} + +// EventuallyWithT asserts that given condition will be met in waitFor time, +// periodically checking target function each tick. In contrast to Eventually, +// it supplies a CollectT to the condition function, so that the condition +// function can use the CollectT to call other assertions. +// The condition is considered "met" if no errors are raised in a tick. +// The supplied CollectT collects all errors from one tick (if there are any). +// If the condition is not met before waitFor, the collected errors of +// the last tick are copied to t. +// +// externalValue := false +// go func() { +// time.Sleep(8*time.Second) +// externalValue = true +// }() +// a.EventuallyWithT(func(c *assert.CollectT) { +// // add assertions as needed; any assertion failure will fail the current tick +// assert.True(c, externalValue, "expected 'externalValue' to be true") +// }, 1*time.Second, 10*time.Second, "external state has not changed to 'true'; still false") +func (a *Assertions) EventuallyWithT(condition func(collect *assert.CollectT), waitFor time.Duration, tick time.Duration, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + EventuallyWithT(a.t, condition, waitFor, tick, msgAndArgs...) +} + +// EventuallyWithTf asserts that given condition will be met in waitFor time, +// periodically checking target function each tick. In contrast to Eventually, +// it supplies a CollectT to the condition function, so that the condition +// function can use the CollectT to call other assertions. +// The condition is considered "met" if no errors are raised in a tick. +// The supplied CollectT collects all errors from one tick (if there are any). +// If the condition is not met before waitFor, the collected errors of +// the last tick are copied to t. +// +// externalValue := false +// go func() { +// time.Sleep(8*time.Second) +// externalValue = true +// }() +// a.EventuallyWithTf(func(c *assert.CollectT, "error message %s", "formatted") { +// // add assertions as needed; any assertion failure will fail the current tick +// assert.True(c, externalValue, "expected 'externalValue' to be true") +// }, 1*time.Second, 10*time.Second, "external state has not changed to 'true'; still false") +func (a *Assertions) EventuallyWithTf(condition func(collect *assert.CollectT), waitFor time.Duration, tick time.Duration, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + EventuallyWithTf(a.t, condition, waitFor, tick, msg, args...) +} + +// Eventuallyf asserts that given condition will be met in waitFor time, +// periodically checking target function each tick. +// +// a.Eventuallyf(func() bool { return true; }, time.Second, 10*time.Millisecond, "error message %s", "formatted") +func (a *Assertions) Eventuallyf(condition func() bool, waitFor time.Duration, tick time.Duration, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Eventuallyf(a.t, condition, waitFor, tick, msg, args...) +} + +// Exactly asserts that two objects are equal in value and type. +// +// a.Exactly(int32(123), int64(123)) +func (a *Assertions) Exactly(expected interface{}, actual interface{}, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Exactly(a.t, expected, actual, msgAndArgs...) +} + +// Exactlyf asserts that two objects are equal in value and type. +// +// a.Exactlyf(int32(123), int64(123), "error message %s", "formatted") +func (a *Assertions) Exactlyf(expected interface{}, actual interface{}, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Exactlyf(a.t, expected, actual, msg, args...) +} + +// Fail reports a failure through +func (a *Assertions) Fail(failureMessage string, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Fail(a.t, failureMessage, msgAndArgs...) +} + +// FailNow fails test +func (a *Assertions) FailNow(failureMessage string, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + FailNow(a.t, failureMessage, msgAndArgs...) +} + +// FailNowf fails test +func (a *Assertions) FailNowf(failureMessage string, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + FailNowf(a.t, failureMessage, msg, args...) +} + +// Failf reports a failure through +func (a *Assertions) Failf(failureMessage string, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Failf(a.t, failureMessage, msg, args...) +} + +// False asserts that the specified value is false. +// +// a.False(myBool) +func (a *Assertions) False(value bool, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + False(a.t, value, msgAndArgs...) +} + +// Falsef asserts that the specified value is false. +// +// a.Falsef(myBool, "error message %s", "formatted") +func (a *Assertions) Falsef(value bool, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Falsef(a.t, value, msg, args...) +} + +// FileExists checks whether a file exists in the given path. It also fails if +// the path points to a directory or there is an error when trying to check the file. +func (a *Assertions) FileExists(path string, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + FileExists(a.t, path, msgAndArgs...) +} + +// FileExistsf checks whether a file exists in the given path. It also fails if +// the path points to a directory or there is an error when trying to check the file. +func (a *Assertions) FileExistsf(path string, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + FileExistsf(a.t, path, msg, args...) +} + +// Greater asserts that the first element is greater than the second +// +// a.Greater(2, 1) +// a.Greater(float64(2), float64(1)) +// a.Greater("b", "a") +func (a *Assertions) Greater(e1 interface{}, e2 interface{}, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Greater(a.t, e1, e2, msgAndArgs...) +} + +// GreaterOrEqual asserts that the first element is greater than or equal to the second +// +// a.GreaterOrEqual(2, 1) +// a.GreaterOrEqual(2, 2) +// a.GreaterOrEqual("b", "a") +// a.GreaterOrEqual("b", "b") +func (a *Assertions) GreaterOrEqual(e1 interface{}, e2 interface{}, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + GreaterOrEqual(a.t, e1, e2, msgAndArgs...) +} + +// GreaterOrEqualf asserts that the first element is greater than or equal to the second +// +// a.GreaterOrEqualf(2, 1, "error message %s", "formatted") +// a.GreaterOrEqualf(2, 2, "error message %s", "formatted") +// a.GreaterOrEqualf("b", "a", "error message %s", "formatted") +// a.GreaterOrEqualf("b", "b", "error message %s", "formatted") +func (a *Assertions) GreaterOrEqualf(e1 interface{}, e2 interface{}, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + GreaterOrEqualf(a.t, e1, e2, msg, args...) +} + +// Greaterf asserts that the first element is greater than the second +// +// a.Greaterf(2, 1, "error message %s", "formatted") +// a.Greaterf(float64(2), float64(1), "error message %s", "formatted") +// a.Greaterf("b", "a", "error message %s", "formatted") +func (a *Assertions) Greaterf(e1 interface{}, e2 interface{}, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Greaterf(a.t, e1, e2, msg, args...) +} + +// HTTPBodyContains asserts that a specified handler returns a +// body that contains a string. +// +// a.HTTPBodyContains(myHandler, "GET", "www.google.com", nil, "I'm Feeling Lucky") +// +// Returns whether the assertion was successful (true) or not (false). +func (a *Assertions) HTTPBodyContains(handler http.HandlerFunc, method string, url string, values url.Values, str interface{}, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + HTTPBodyContains(a.t, handler, method, url, values, str, msgAndArgs...) +} + +// HTTPBodyContainsf asserts that a specified handler returns a +// body that contains a string. +// +// a.HTTPBodyContainsf(myHandler, "GET", "www.google.com", nil, "I'm Feeling Lucky", "error message %s", "formatted") +// +// Returns whether the assertion was successful (true) or not (false). +func (a *Assertions) HTTPBodyContainsf(handler http.HandlerFunc, method string, url string, values url.Values, str interface{}, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + HTTPBodyContainsf(a.t, handler, method, url, values, str, msg, args...) +} + +// HTTPBodyNotContains asserts that a specified handler returns a +// body that does not contain a string. +// +// a.HTTPBodyNotContains(myHandler, "GET", "www.google.com", nil, "I'm Feeling Lucky") +// +// Returns whether the assertion was successful (true) or not (false). +func (a *Assertions) HTTPBodyNotContains(handler http.HandlerFunc, method string, url string, values url.Values, str interface{}, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + HTTPBodyNotContains(a.t, handler, method, url, values, str, msgAndArgs...) +} + +// HTTPBodyNotContainsf asserts that a specified handler returns a +// body that does not contain a string. +// +// a.HTTPBodyNotContainsf(myHandler, "GET", "www.google.com", nil, "I'm Feeling Lucky", "error message %s", "formatted") +// +// Returns whether the assertion was successful (true) or not (false). +func (a *Assertions) HTTPBodyNotContainsf(handler http.HandlerFunc, method string, url string, values url.Values, str interface{}, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + HTTPBodyNotContainsf(a.t, handler, method, url, values, str, msg, args...) +} + +// HTTPError asserts that a specified handler returns an error status code. +// +// a.HTTPError(myHandler, "POST", "/a/b/c", url.Values{"a": []string{"b", "c"}} +// +// Returns whether the assertion was successful (true) or not (false). +func (a *Assertions) HTTPError(handler http.HandlerFunc, method string, url string, values url.Values, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + HTTPError(a.t, handler, method, url, values, msgAndArgs...) +} + +// HTTPErrorf asserts that a specified handler returns an error status code. +// +// a.HTTPErrorf(myHandler, "POST", "/a/b/c", url.Values{"a": []string{"b", "c"}} +// +// Returns whether the assertion was successful (true) or not (false). +func (a *Assertions) HTTPErrorf(handler http.HandlerFunc, method string, url string, values url.Values, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + HTTPErrorf(a.t, handler, method, url, values, msg, args...) +} + +// HTTPRedirect asserts that a specified handler returns a redirect status code. +// +// a.HTTPRedirect(myHandler, "GET", "/a/b/c", url.Values{"a": []string{"b", "c"}} +// +// Returns whether the assertion was successful (true) or not (false). +func (a *Assertions) HTTPRedirect(handler http.HandlerFunc, method string, url string, values url.Values, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + HTTPRedirect(a.t, handler, method, url, values, msgAndArgs...) +} + +// HTTPRedirectf asserts that a specified handler returns a redirect status code. +// +// a.HTTPRedirectf(myHandler, "GET", "/a/b/c", url.Values{"a": []string{"b", "c"}} +// +// Returns whether the assertion was successful (true) or not (false). +func (a *Assertions) HTTPRedirectf(handler http.HandlerFunc, method string, url string, values url.Values, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + HTTPRedirectf(a.t, handler, method, url, values, msg, args...) +} + +// HTTPStatusCode asserts that a specified handler returns a specified status code. +// +// a.HTTPStatusCode(myHandler, "GET", "/notImplemented", nil, 501) +// +// Returns whether the assertion was successful (true) or not (false). +func (a *Assertions) HTTPStatusCode(handler http.HandlerFunc, method string, url string, values url.Values, statuscode int, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + HTTPStatusCode(a.t, handler, method, url, values, statuscode, msgAndArgs...) +} + +// HTTPStatusCodef asserts that a specified handler returns a specified status code. +// +// a.HTTPStatusCodef(myHandler, "GET", "/notImplemented", nil, 501, "error message %s", "formatted") +// +// Returns whether the assertion was successful (true) or not (false). +func (a *Assertions) HTTPStatusCodef(handler http.HandlerFunc, method string, url string, values url.Values, statuscode int, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + HTTPStatusCodef(a.t, handler, method, url, values, statuscode, msg, args...) +} + +// HTTPSuccess asserts that a specified handler returns a success status code. +// +// a.HTTPSuccess(myHandler, "POST", "http://www.google.com", nil) +// +// Returns whether the assertion was successful (true) or not (false). +func (a *Assertions) HTTPSuccess(handler http.HandlerFunc, method string, url string, values url.Values, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + HTTPSuccess(a.t, handler, method, url, values, msgAndArgs...) +} + +// HTTPSuccessf asserts that a specified handler returns a success status code. +// +// a.HTTPSuccessf(myHandler, "POST", "http://www.google.com", nil, "error message %s", "formatted") +// +// Returns whether the assertion was successful (true) or not (false). +func (a *Assertions) HTTPSuccessf(handler http.HandlerFunc, method string, url string, values url.Values, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + HTTPSuccessf(a.t, handler, method, url, values, msg, args...) +} + +// Implements asserts that an object is implemented by the specified interface. +// +// a.Implements((*MyInterface)(nil), new(MyObject)) +func (a *Assertions) Implements(interfaceObject interface{}, object interface{}, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Implements(a.t, interfaceObject, object, msgAndArgs...) +} + +// Implementsf asserts that an object is implemented by the specified interface. +// +// a.Implementsf((*MyInterface)(nil), new(MyObject), "error message %s", "formatted") +func (a *Assertions) Implementsf(interfaceObject interface{}, object interface{}, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Implementsf(a.t, interfaceObject, object, msg, args...) +} + +// InDelta asserts that the two numerals are within delta of each other. +// +// a.InDelta(math.Pi, 22/7.0, 0.01) +func (a *Assertions) InDelta(expected interface{}, actual interface{}, delta float64, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + InDelta(a.t, expected, actual, delta, msgAndArgs...) +} + +// InDeltaMapValues is the same as InDelta, but it compares all values between two maps. Both maps must have exactly the same keys. +func (a *Assertions) InDeltaMapValues(expected interface{}, actual interface{}, delta float64, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + InDeltaMapValues(a.t, expected, actual, delta, msgAndArgs...) +} + +// InDeltaMapValuesf is the same as InDelta, but it compares all values between two maps. Both maps must have exactly the same keys. +func (a *Assertions) InDeltaMapValuesf(expected interface{}, actual interface{}, delta float64, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + InDeltaMapValuesf(a.t, expected, actual, delta, msg, args...) +} + +// InDeltaSlice is the same as InDelta, except it compares two slices. +func (a *Assertions) InDeltaSlice(expected interface{}, actual interface{}, delta float64, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + InDeltaSlice(a.t, expected, actual, delta, msgAndArgs...) +} + +// InDeltaSlicef is the same as InDelta, except it compares two slices. +func (a *Assertions) InDeltaSlicef(expected interface{}, actual interface{}, delta float64, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + InDeltaSlicef(a.t, expected, actual, delta, msg, args...) +} + +// InDeltaf asserts that the two numerals are within delta of each other. +// +// a.InDeltaf(math.Pi, 22/7.0, 0.01, "error message %s", "formatted") +func (a *Assertions) InDeltaf(expected interface{}, actual interface{}, delta float64, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + InDeltaf(a.t, expected, actual, delta, msg, args...) +} + +// InEpsilon asserts that expected and actual have a relative error less than epsilon +func (a *Assertions) InEpsilon(expected interface{}, actual interface{}, epsilon float64, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + InEpsilon(a.t, expected, actual, epsilon, msgAndArgs...) +} + +// InEpsilonSlice is the same as InEpsilon, except it compares each value from two slices. +func (a *Assertions) InEpsilonSlice(expected interface{}, actual interface{}, epsilon float64, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + InEpsilonSlice(a.t, expected, actual, epsilon, msgAndArgs...) +} + +// InEpsilonSlicef is the same as InEpsilon, except it compares each value from two slices. +func (a *Assertions) InEpsilonSlicef(expected interface{}, actual interface{}, epsilon float64, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + InEpsilonSlicef(a.t, expected, actual, epsilon, msg, args...) +} + +// InEpsilonf asserts that expected and actual have a relative error less than epsilon +func (a *Assertions) InEpsilonf(expected interface{}, actual interface{}, epsilon float64, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + InEpsilonf(a.t, expected, actual, epsilon, msg, args...) +} + +// IsDecreasing asserts that the collection is decreasing +// +// a.IsDecreasing([]int{2, 1, 0}) +// a.IsDecreasing([]float{2, 1}) +// a.IsDecreasing([]string{"b", "a"}) +func (a *Assertions) IsDecreasing(object interface{}, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + IsDecreasing(a.t, object, msgAndArgs...) +} + +// IsDecreasingf asserts that the collection is decreasing +// +// a.IsDecreasingf([]int{2, 1, 0}, "error message %s", "formatted") +// a.IsDecreasingf([]float{2, 1}, "error message %s", "formatted") +// a.IsDecreasingf([]string{"b", "a"}, "error message %s", "formatted") +func (a *Assertions) IsDecreasingf(object interface{}, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + IsDecreasingf(a.t, object, msg, args...) +} + +// IsIncreasing asserts that the collection is increasing +// +// a.IsIncreasing([]int{1, 2, 3}) +// a.IsIncreasing([]float{1, 2}) +// a.IsIncreasing([]string{"a", "b"}) +func (a *Assertions) IsIncreasing(object interface{}, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + IsIncreasing(a.t, object, msgAndArgs...) +} + +// IsIncreasingf asserts that the collection is increasing +// +// a.IsIncreasingf([]int{1, 2, 3}, "error message %s", "formatted") +// a.IsIncreasingf([]float{1, 2}, "error message %s", "formatted") +// a.IsIncreasingf([]string{"a", "b"}, "error message %s", "formatted") +func (a *Assertions) IsIncreasingf(object interface{}, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + IsIncreasingf(a.t, object, msg, args...) +} + +// IsNonDecreasing asserts that the collection is not decreasing +// +// a.IsNonDecreasing([]int{1, 1, 2}) +// a.IsNonDecreasing([]float{1, 2}) +// a.IsNonDecreasing([]string{"a", "b"}) +func (a *Assertions) IsNonDecreasing(object interface{}, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + IsNonDecreasing(a.t, object, msgAndArgs...) +} + +// IsNonDecreasingf asserts that the collection is not decreasing +// +// a.IsNonDecreasingf([]int{1, 1, 2}, "error message %s", "formatted") +// a.IsNonDecreasingf([]float{1, 2}, "error message %s", "formatted") +// a.IsNonDecreasingf([]string{"a", "b"}, "error message %s", "formatted") +func (a *Assertions) IsNonDecreasingf(object interface{}, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + IsNonDecreasingf(a.t, object, msg, args...) +} + +// IsNonIncreasing asserts that the collection is not increasing +// +// a.IsNonIncreasing([]int{2, 1, 1}) +// a.IsNonIncreasing([]float{2, 1}) +// a.IsNonIncreasing([]string{"b", "a"}) +func (a *Assertions) IsNonIncreasing(object interface{}, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + IsNonIncreasing(a.t, object, msgAndArgs...) +} + +// IsNonIncreasingf asserts that the collection is not increasing +// +// a.IsNonIncreasingf([]int{2, 1, 1}, "error message %s", "formatted") +// a.IsNonIncreasingf([]float{2, 1}, "error message %s", "formatted") +// a.IsNonIncreasingf([]string{"b", "a"}, "error message %s", "formatted") +func (a *Assertions) IsNonIncreasingf(object interface{}, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + IsNonIncreasingf(a.t, object, msg, args...) +} + +// IsType asserts that the specified objects are of the same type. +func (a *Assertions) IsType(expectedType interface{}, object interface{}, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + IsType(a.t, expectedType, object, msgAndArgs...) +} + +// IsTypef asserts that the specified objects are of the same type. +func (a *Assertions) IsTypef(expectedType interface{}, object interface{}, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + IsTypef(a.t, expectedType, object, msg, args...) +} + +// JSONEq asserts that two JSON strings are equivalent. +// +// a.JSONEq(`{"hello": "world", "foo": "bar"}`, `{"foo": "bar", "hello": "world"}`) +func (a *Assertions) JSONEq(expected string, actual string, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + JSONEq(a.t, expected, actual, msgAndArgs...) +} + +// JSONEqf asserts that two JSON strings are equivalent. +// +// a.JSONEqf(`{"hello": "world", "foo": "bar"}`, `{"foo": "bar", "hello": "world"}`, "error message %s", "formatted") +func (a *Assertions) JSONEqf(expected string, actual string, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + JSONEqf(a.t, expected, actual, msg, args...) +} + +// Len asserts that the specified object has specific length. +// Len also fails if the object has a type that len() not accept. +// +// a.Len(mySlice, 3) +func (a *Assertions) Len(object interface{}, length int, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Len(a.t, object, length, msgAndArgs...) +} + +// Lenf asserts that the specified object has specific length. +// Lenf also fails if the object has a type that len() not accept. +// +// a.Lenf(mySlice, 3, "error message %s", "formatted") +func (a *Assertions) Lenf(object interface{}, length int, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Lenf(a.t, object, length, msg, args...) +} + +// Less asserts that the first element is less than the second +// +// a.Less(1, 2) +// a.Less(float64(1), float64(2)) +// a.Less("a", "b") +func (a *Assertions) Less(e1 interface{}, e2 interface{}, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Less(a.t, e1, e2, msgAndArgs...) +} + +// LessOrEqual asserts that the first element is less than or equal to the second +// +// a.LessOrEqual(1, 2) +// a.LessOrEqual(2, 2) +// a.LessOrEqual("a", "b") +// a.LessOrEqual("b", "b") +func (a *Assertions) LessOrEqual(e1 interface{}, e2 interface{}, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + LessOrEqual(a.t, e1, e2, msgAndArgs...) +} + +// LessOrEqualf asserts that the first element is less than or equal to the second +// +// a.LessOrEqualf(1, 2, "error message %s", "formatted") +// a.LessOrEqualf(2, 2, "error message %s", "formatted") +// a.LessOrEqualf("a", "b", "error message %s", "formatted") +// a.LessOrEqualf("b", "b", "error message %s", "formatted") +func (a *Assertions) LessOrEqualf(e1 interface{}, e2 interface{}, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + LessOrEqualf(a.t, e1, e2, msg, args...) +} + +// Lessf asserts that the first element is less than the second +// +// a.Lessf(1, 2, "error message %s", "formatted") +// a.Lessf(float64(1), float64(2), "error message %s", "formatted") +// a.Lessf("a", "b", "error message %s", "formatted") +func (a *Assertions) Lessf(e1 interface{}, e2 interface{}, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Lessf(a.t, e1, e2, msg, args...) +} + +// Negative asserts that the specified element is negative +// +// a.Negative(-1) +// a.Negative(-1.23) +func (a *Assertions) Negative(e interface{}, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Negative(a.t, e, msgAndArgs...) +} + +// Negativef asserts that the specified element is negative +// +// a.Negativef(-1, "error message %s", "formatted") +// a.Negativef(-1.23, "error message %s", "formatted") +func (a *Assertions) Negativef(e interface{}, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Negativef(a.t, e, msg, args...) +} + +// Never asserts that the given condition doesn't satisfy in waitFor time, +// periodically checking the target function each tick. +// +// a.Never(func() bool { return false; }, time.Second, 10*time.Millisecond) +func (a *Assertions) Never(condition func() bool, waitFor time.Duration, tick time.Duration, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Never(a.t, condition, waitFor, tick, msgAndArgs...) +} + +// Neverf asserts that the given condition doesn't satisfy in waitFor time, +// periodically checking the target function each tick. +// +// a.Neverf(func() bool { return false; }, time.Second, 10*time.Millisecond, "error message %s", "formatted") +func (a *Assertions) Neverf(condition func() bool, waitFor time.Duration, tick time.Duration, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Neverf(a.t, condition, waitFor, tick, msg, args...) +} + +// Nil asserts that the specified object is nil. +// +// a.Nil(err) +func (a *Assertions) Nil(object interface{}, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Nil(a.t, object, msgAndArgs...) +} + +// Nilf asserts that the specified object is nil. +// +// a.Nilf(err, "error message %s", "formatted") +func (a *Assertions) Nilf(object interface{}, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Nilf(a.t, object, msg, args...) +} + +// NoDirExists checks whether a directory does not exist in the given path. +// It fails if the path points to an existing _directory_ only. +func (a *Assertions) NoDirExists(path string, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + NoDirExists(a.t, path, msgAndArgs...) +} + +// NoDirExistsf checks whether a directory does not exist in the given path. +// It fails if the path points to an existing _directory_ only. +func (a *Assertions) NoDirExistsf(path string, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + NoDirExistsf(a.t, path, msg, args...) +} + +// NoError asserts that a function returned no error (i.e. `nil`). +// +// actualObj, err := SomeFunction() +// if a.NoError(err) { +// assert.Equal(t, expectedObj, actualObj) +// } +func (a *Assertions) NoError(err error, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + NoError(a.t, err, msgAndArgs...) +} + +// NoErrorf asserts that a function returned no error (i.e. `nil`). +// +// actualObj, err := SomeFunction() +// if a.NoErrorf(err, "error message %s", "formatted") { +// assert.Equal(t, expectedObj, actualObj) +// } +func (a *Assertions) NoErrorf(err error, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + NoErrorf(a.t, err, msg, args...) +} + +// NoFileExists checks whether a file does not exist in a given path. It fails +// if the path points to an existing _file_ only. +func (a *Assertions) NoFileExists(path string, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + NoFileExists(a.t, path, msgAndArgs...) +} + +// NoFileExistsf checks whether a file does not exist in a given path. It fails +// if the path points to an existing _file_ only. +func (a *Assertions) NoFileExistsf(path string, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + NoFileExistsf(a.t, path, msg, args...) +} + +// NotContains asserts that the specified string, list(array, slice...) or map does NOT contain the +// specified substring or element. +// +// a.NotContains("Hello World", "Earth") +// a.NotContains(["Hello", "World"], "Earth") +// a.NotContains({"Hello": "World"}, "Earth") +func (a *Assertions) NotContains(s interface{}, contains interface{}, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + NotContains(a.t, s, contains, msgAndArgs...) +} + +// NotContainsf asserts that the specified string, list(array, slice...) or map does NOT contain the +// specified substring or element. +// +// a.NotContainsf("Hello World", "Earth", "error message %s", "formatted") +// a.NotContainsf(["Hello", "World"], "Earth", "error message %s", "formatted") +// a.NotContainsf({"Hello": "World"}, "Earth", "error message %s", "formatted") +func (a *Assertions) NotContainsf(s interface{}, contains interface{}, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + NotContainsf(a.t, s, contains, msg, args...) +} + +// NotEmpty asserts that the specified object is NOT empty. I.e. not nil, "", false, 0 or either +// a slice or a channel with len == 0. +// +// if a.NotEmpty(obj) { +// assert.Equal(t, "two", obj[1]) +// } +func (a *Assertions) NotEmpty(object interface{}, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + NotEmpty(a.t, object, msgAndArgs...) +} + +// NotEmptyf asserts that the specified object is NOT empty. I.e. not nil, "", false, 0 or either +// a slice or a channel with len == 0. +// +// if a.NotEmptyf(obj, "error message %s", "formatted") { +// assert.Equal(t, "two", obj[1]) +// } +func (a *Assertions) NotEmptyf(object interface{}, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + NotEmptyf(a.t, object, msg, args...) +} + +// NotEqual asserts that the specified values are NOT equal. +// +// a.NotEqual(obj1, obj2) +// +// Pointer variable equality is determined based on the equality of the +// referenced values (as opposed to the memory addresses). +func (a *Assertions) NotEqual(expected interface{}, actual interface{}, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + NotEqual(a.t, expected, actual, msgAndArgs...) +} + +// NotEqualValues asserts that two objects are not equal even when converted to the same type +// +// a.NotEqualValues(obj1, obj2) +func (a *Assertions) NotEqualValues(expected interface{}, actual interface{}, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + NotEqualValues(a.t, expected, actual, msgAndArgs...) +} + +// NotEqualValuesf asserts that two objects are not equal even when converted to the same type +// +// a.NotEqualValuesf(obj1, obj2, "error message %s", "formatted") +func (a *Assertions) NotEqualValuesf(expected interface{}, actual interface{}, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + NotEqualValuesf(a.t, expected, actual, msg, args...) +} + +// NotEqualf asserts that the specified values are NOT equal. +// +// a.NotEqualf(obj1, obj2, "error message %s", "formatted") +// +// Pointer variable equality is determined based on the equality of the +// referenced values (as opposed to the memory addresses). +func (a *Assertions) NotEqualf(expected interface{}, actual interface{}, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + NotEqualf(a.t, expected, actual, msg, args...) +} + +// NotErrorIs asserts that at none of the errors in err's chain matches target. +// This is a wrapper for errors.Is. +func (a *Assertions) NotErrorIs(err error, target error, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + NotErrorIs(a.t, err, target, msgAndArgs...) +} + +// NotErrorIsf asserts that at none of the errors in err's chain matches target. +// This is a wrapper for errors.Is. +func (a *Assertions) NotErrorIsf(err error, target error, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + NotErrorIsf(a.t, err, target, msg, args...) +} + +// NotImplements asserts that an object does not implement the specified interface. +// +// a.NotImplements((*MyInterface)(nil), new(MyObject)) +func (a *Assertions) NotImplements(interfaceObject interface{}, object interface{}, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + NotImplements(a.t, interfaceObject, object, msgAndArgs...) +} + +// NotImplementsf asserts that an object does not implement the specified interface. +// +// a.NotImplementsf((*MyInterface)(nil), new(MyObject), "error message %s", "formatted") +func (a *Assertions) NotImplementsf(interfaceObject interface{}, object interface{}, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + NotImplementsf(a.t, interfaceObject, object, msg, args...) +} + +// NotNil asserts that the specified object is not nil. +// +// a.NotNil(err) +func (a *Assertions) NotNil(object interface{}, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + NotNil(a.t, object, msgAndArgs...) +} + +// NotNilf asserts that the specified object is not nil. +// +// a.NotNilf(err, "error message %s", "formatted") +func (a *Assertions) NotNilf(object interface{}, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + NotNilf(a.t, object, msg, args...) +} + +// NotPanics asserts that the code inside the specified PanicTestFunc does NOT panic. +// +// a.NotPanics(func(){ RemainCalm() }) +func (a *Assertions) NotPanics(f assert.PanicTestFunc, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + NotPanics(a.t, f, msgAndArgs...) +} + +// NotPanicsf asserts that the code inside the specified PanicTestFunc does NOT panic. +// +// a.NotPanicsf(func(){ RemainCalm() }, "error message %s", "formatted") +func (a *Assertions) NotPanicsf(f assert.PanicTestFunc, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + NotPanicsf(a.t, f, msg, args...) +} + +// NotRegexp asserts that a specified regexp does not match a string. +// +// a.NotRegexp(regexp.MustCompile("starts"), "it's starting") +// a.NotRegexp("^start", "it's not starting") +func (a *Assertions) NotRegexp(rx interface{}, str interface{}, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + NotRegexp(a.t, rx, str, msgAndArgs...) +} + +// NotRegexpf asserts that a specified regexp does not match a string. +// +// a.NotRegexpf(regexp.MustCompile("starts"), "it's starting", "error message %s", "formatted") +// a.NotRegexpf("^start", "it's not starting", "error message %s", "formatted") +func (a *Assertions) NotRegexpf(rx interface{}, str interface{}, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + NotRegexpf(a.t, rx, str, msg, args...) +} + +// NotSame asserts that two pointers do not reference the same object. +// +// a.NotSame(ptr1, ptr2) +// +// Both arguments must be pointer variables. Pointer variable sameness is +// determined based on the equality of both type and value. +func (a *Assertions) NotSame(expected interface{}, actual interface{}, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + NotSame(a.t, expected, actual, msgAndArgs...) +} + +// NotSamef asserts that two pointers do not reference the same object. +// +// a.NotSamef(ptr1, ptr2, "error message %s", "formatted") +// +// Both arguments must be pointer variables. Pointer variable sameness is +// determined based on the equality of both type and value. +func (a *Assertions) NotSamef(expected interface{}, actual interface{}, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + NotSamef(a.t, expected, actual, msg, args...) +} + +// NotSubset asserts that the specified list(array, slice...) or map does NOT +// contain all elements given in the specified subset list(array, slice...) or +// map. +// +// a.NotSubset([1, 3, 4], [1, 2]) +// a.NotSubset({"x": 1, "y": 2}, {"z": 3}) +func (a *Assertions) NotSubset(list interface{}, subset interface{}, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + NotSubset(a.t, list, subset, msgAndArgs...) +} + +// NotSubsetf asserts that the specified list(array, slice...) or map does NOT +// contain all elements given in the specified subset list(array, slice...) or +// map. +// +// a.NotSubsetf([1, 3, 4], [1, 2], "error message %s", "formatted") +// a.NotSubsetf({"x": 1, "y": 2}, {"z": 3}, "error message %s", "formatted") +func (a *Assertions) NotSubsetf(list interface{}, subset interface{}, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + NotSubsetf(a.t, list, subset, msg, args...) +} + +// NotZero asserts that i is not the zero value for its type. +func (a *Assertions) NotZero(i interface{}, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + NotZero(a.t, i, msgAndArgs...) +} + +// NotZerof asserts that i is not the zero value for its type. +func (a *Assertions) NotZerof(i interface{}, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + NotZerof(a.t, i, msg, args...) +} + +// Panics asserts that the code inside the specified PanicTestFunc panics. +// +// a.Panics(func(){ GoCrazy() }) +func (a *Assertions) Panics(f assert.PanicTestFunc, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Panics(a.t, f, msgAndArgs...) +} + +// PanicsWithError asserts that the code inside the specified PanicTestFunc +// panics, and that the recovered panic value is an error that satisfies the +// EqualError comparison. +// +// a.PanicsWithError("crazy error", func(){ GoCrazy() }) +func (a *Assertions) PanicsWithError(errString string, f assert.PanicTestFunc, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + PanicsWithError(a.t, errString, f, msgAndArgs...) +} + +// PanicsWithErrorf asserts that the code inside the specified PanicTestFunc +// panics, and that the recovered panic value is an error that satisfies the +// EqualError comparison. +// +// a.PanicsWithErrorf("crazy error", func(){ GoCrazy() }, "error message %s", "formatted") +func (a *Assertions) PanicsWithErrorf(errString string, f assert.PanicTestFunc, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + PanicsWithErrorf(a.t, errString, f, msg, args...) +} + +// PanicsWithValue asserts that the code inside the specified PanicTestFunc panics, and that +// the recovered panic value equals the expected panic value. +// +// a.PanicsWithValue("crazy error", func(){ GoCrazy() }) +func (a *Assertions) PanicsWithValue(expected interface{}, f assert.PanicTestFunc, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + PanicsWithValue(a.t, expected, f, msgAndArgs...) +} + +// PanicsWithValuef asserts that the code inside the specified PanicTestFunc panics, and that +// the recovered panic value equals the expected panic value. +// +// a.PanicsWithValuef("crazy error", func(){ GoCrazy() }, "error message %s", "formatted") +func (a *Assertions) PanicsWithValuef(expected interface{}, f assert.PanicTestFunc, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + PanicsWithValuef(a.t, expected, f, msg, args...) +} + +// Panicsf asserts that the code inside the specified PanicTestFunc panics. +// +// a.Panicsf(func(){ GoCrazy() }, "error message %s", "formatted") +func (a *Assertions) Panicsf(f assert.PanicTestFunc, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Panicsf(a.t, f, msg, args...) +} + +// Positive asserts that the specified element is positive +// +// a.Positive(1) +// a.Positive(1.23) +func (a *Assertions) Positive(e interface{}, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Positive(a.t, e, msgAndArgs...) +} + +// Positivef asserts that the specified element is positive +// +// a.Positivef(1, "error message %s", "formatted") +// a.Positivef(1.23, "error message %s", "formatted") +func (a *Assertions) Positivef(e interface{}, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Positivef(a.t, e, msg, args...) +} + +// Regexp asserts that a specified regexp matches a string. +// +// a.Regexp(regexp.MustCompile("start"), "it's starting") +// a.Regexp("start...$", "it's not starting") +func (a *Assertions) Regexp(rx interface{}, str interface{}, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Regexp(a.t, rx, str, msgAndArgs...) +} + +// Regexpf asserts that a specified regexp matches a string. +// +// a.Regexpf(regexp.MustCompile("start"), "it's starting", "error message %s", "formatted") +// a.Regexpf("start...$", "it's not starting", "error message %s", "formatted") +func (a *Assertions) Regexpf(rx interface{}, str interface{}, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Regexpf(a.t, rx, str, msg, args...) +} + +// Same asserts that two pointers reference the same object. +// +// a.Same(ptr1, ptr2) +// +// Both arguments must be pointer variables. Pointer variable sameness is +// determined based on the equality of both type and value. +func (a *Assertions) Same(expected interface{}, actual interface{}, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Same(a.t, expected, actual, msgAndArgs...) +} + +// Samef asserts that two pointers reference the same object. +// +// a.Samef(ptr1, ptr2, "error message %s", "formatted") +// +// Both arguments must be pointer variables. Pointer variable sameness is +// determined based on the equality of both type and value. +func (a *Assertions) Samef(expected interface{}, actual interface{}, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Samef(a.t, expected, actual, msg, args...) +} + +// Subset asserts that the specified list(array, slice...) or map contains all +// elements given in the specified subset list(array, slice...) or map. +// +// a.Subset([1, 2, 3], [1, 2]) +// a.Subset({"x": 1, "y": 2}, {"x": 1}) +func (a *Assertions) Subset(list interface{}, subset interface{}, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Subset(a.t, list, subset, msgAndArgs...) +} + +// Subsetf asserts that the specified list(array, slice...) or map contains all +// elements given in the specified subset list(array, slice...) or map. +// +// a.Subsetf([1, 2, 3], [1, 2], "error message %s", "formatted") +// a.Subsetf({"x": 1, "y": 2}, {"x": 1}, "error message %s", "formatted") +func (a *Assertions) Subsetf(list interface{}, subset interface{}, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Subsetf(a.t, list, subset, msg, args...) +} + +// True asserts that the specified value is true. +// +// a.True(myBool) +func (a *Assertions) True(value bool, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + True(a.t, value, msgAndArgs...) +} + +// Truef asserts that the specified value is true. +// +// a.Truef(myBool, "error message %s", "formatted") +func (a *Assertions) Truef(value bool, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Truef(a.t, value, msg, args...) +} + +// WithinDuration asserts that the two times are within duration delta of each other. +// +// a.WithinDuration(time.Now(), time.Now(), 10*time.Second) +func (a *Assertions) WithinDuration(expected time.Time, actual time.Time, delta time.Duration, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + WithinDuration(a.t, expected, actual, delta, msgAndArgs...) +} + +// WithinDurationf asserts that the two times are within duration delta of each other. +// +// a.WithinDurationf(time.Now(), time.Now(), 10*time.Second, "error message %s", "formatted") +func (a *Assertions) WithinDurationf(expected time.Time, actual time.Time, delta time.Duration, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + WithinDurationf(a.t, expected, actual, delta, msg, args...) +} + +// WithinRange asserts that a time is within a time range (inclusive). +// +// a.WithinRange(time.Now(), time.Now().Add(-time.Second), time.Now().Add(time.Second)) +func (a *Assertions) WithinRange(actual time.Time, start time.Time, end time.Time, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + WithinRange(a.t, actual, start, end, msgAndArgs...) +} + +// WithinRangef asserts that a time is within a time range (inclusive). +// +// a.WithinRangef(time.Now(), time.Now().Add(-time.Second), time.Now().Add(time.Second), "error message %s", "formatted") +func (a *Assertions) WithinRangef(actual time.Time, start time.Time, end time.Time, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + WithinRangef(a.t, actual, start, end, msg, args...) +} + +// YAMLEq asserts that two YAML strings are equivalent. +func (a *Assertions) YAMLEq(expected string, actual string, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + YAMLEq(a.t, expected, actual, msgAndArgs...) +} + +// YAMLEqf asserts that two YAML strings are equivalent. +func (a *Assertions) YAMLEqf(expected string, actual string, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + YAMLEqf(a.t, expected, actual, msg, args...) +} + +// Zero asserts that i is the zero value for its type. +func (a *Assertions) Zero(i interface{}, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Zero(a.t, i, msgAndArgs...) +} + +// Zerof asserts that i is the zero value for its type. +func (a *Assertions) Zerof(i interface{}, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Zerof(a.t, i, msg, args...) +} diff --git a/vendor/github.com/stretchr/testify/require/require_forward.go.tmpl b/vendor/github.com/stretchr/testify/require/require_forward.go.tmpl new file mode 100644 index 00000000..54124df1 --- /dev/null +++ b/vendor/github.com/stretchr/testify/require/require_forward.go.tmpl @@ -0,0 +1,5 @@ +{{.CommentWithoutT "a"}} +func (a *Assertions) {{.DocInfo.Name}}({{.Params}}) { + if h, ok := a.t.(tHelper); ok { h.Helper() } + {{.DocInfo.Name}}(a.t, {{.ForwardedParams}}) +} diff --git a/vendor/github.com/stretchr/testify/require/requirements.go b/vendor/github.com/stretchr/testify/require/requirements.go new file mode 100644 index 00000000..91772dfe --- /dev/null +++ b/vendor/github.com/stretchr/testify/require/requirements.go @@ -0,0 +1,29 @@ +package require + +// TestingT is an interface wrapper around *testing.T +type TestingT interface { + Errorf(format string, args ...interface{}) + FailNow() +} + +type tHelper interface { + Helper() +} + +// ComparisonAssertionFunc is a common function prototype when comparing two values. Can be useful +// for table driven tests. +type ComparisonAssertionFunc func(TestingT, interface{}, interface{}, ...interface{}) + +// ValueAssertionFunc is a common function prototype when validating a single value. Can be useful +// for table driven tests. +type ValueAssertionFunc func(TestingT, interface{}, ...interface{}) + +// BoolAssertionFunc is a common function prototype when validating a bool value. Can be useful +// for table driven tests. +type BoolAssertionFunc func(TestingT, bool, ...interface{}) + +// ErrorAssertionFunc is a common function prototype when validating an error value. Can be useful +// for table driven tests. +type ErrorAssertionFunc func(TestingT, error, ...interface{}) + +//go:generate sh -c "cd ../_codegen && go build && cd - && ../_codegen/_codegen -output-package=require -template=require.go.tmpl -include-format-funcs" diff --git a/vendor/modules.txt b/vendor/modules.txt index 6b567d06..5919dd71 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -78,6 +78,7 @@ github.com/sirupsen/logrus # github.com/stretchr/testify v1.9.0 ## explicit; go 1.17 github.com/stretchr/testify/assert +github.com/stretchr/testify/require # github.com/urfave/cli v1.22.12 ## explicit; go 1.11 github.com/urfave/cli From f8f12bc74a56f8ba98c6d7b5f0380b6b1c269c95 Mon Sep 17 00:00:00 2001 From: Aleksa Sarai Date: Thu, 3 Apr 2025 18:32:09 +1100 Subject: [PATCH 8/9] cmd: add --compress= option to repack-like commands The default option is --compress=auto, which will try to match the existing layer compression. Signed-off-by: Aleksa Sarai --- CHANGELOG.md | 8 ++ cmd/umoci/insert.go | 12 ++- cmd/umoci/raw-add-layer.go | 12 ++- cmd/umoci/repack.go | 12 ++- cmd/umoci/utils_ux.go | 40 +++++++ doc/man/umoci-insert.1.md | 16 +++ doc/man/umoci-raw-add-layer.1.md | 16 +++ doc/man/umoci-repack.1.md | 16 +++ test/insert.bats | 157 ++++++++++++++++++++++++++++ test/raw-add-layer.bats | 165 +++++++++++++++++++++++++++++ test/repack.bats | 173 +++++++++++++++++++++++++++++++ 11 files changed, 618 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e47d0123..dea4320c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,14 @@ and this project adheres to [Semantic Versioning](http://semver.org/). something that was added in image-spec v1.2 (which we do not yet support fully) but at least this will allow users to operate on zstd-compressed images, which are slowly becoming more common. +- `umoci repack` and `umoci insert` now support creating zstd-compressed + layers. The default behaviour (called `auto`) is to try to match the last + layer's compression algorithm, with a fallback to `gzip` if none of the layer + algorithms were supported. + * Users can specify their preferred compression algorithm using the new + `--compress` flag. You can also disable compression entirely using + `--compress=none` but `--compress=auto` will never automatically choose + `none` compression. ### Changes ### - In this release, the primary development branch was renamed to `main`. diff --git a/cmd/umoci/insert.go b/cmd/umoci/insert.go index 6a09c1e8..5eeb618c 100644 --- a/cmd/umoci/insert.go +++ b/cmd/umoci/insert.go @@ -29,12 +29,13 @@ import ( "github.com/opencontainers/umoci/mutate" "github.com/opencontainers/umoci/oci/cas/dir" "github.com/opencontainers/umoci/oci/casext" + "github.com/opencontainers/umoci/oci/casext/blobcompress" igen "github.com/opencontainers/umoci/oci/config/generate" "github.com/opencontainers/umoci/oci/layer" "github.com/urfave/cli" ) -var insertCommand = uxRemap(uxHistory(uxTag(cli.Command{ +var insertCommand = uxCompress(uxRemap(uxHistory(uxTag(cli.Command{ Name: "insert", Usage: "insert content into an OCI image", ArgsUsage: `--image [:] [--opaque] @@ -107,7 +108,7 @@ Some examples: ctx.App.Metadata["--target-path"] = targetPath return nil }, -}))) +})))) func insert(ctx *cli.Context) error { imagePath := ctx.App.Metadata["--image-path"].(string) @@ -115,6 +116,11 @@ func insert(ctx *cli.Context) error { sourcePath := ctx.App.Metadata["--source-path"].(string) targetPath := ctx.App.Metadata["--target-path"].(string) + var compressAlgo blobcompress.Algorithm + if algo, ok := ctx.App.Metadata["--compress"].(blobcompress.Algorithm); ok { + compressAlgo = algo + } + // By default we clobber the old tag. tagName := fromName if val, ok := ctx.App.Metadata["--tag"]; ok { @@ -188,7 +194,7 @@ func insert(ctx *cli.Context) error { } } - if _, err := mutator.Add(context.Background(), ispec.MediaTypeImageLayer, reader, history, nil, nil); err != nil { + if _, err := mutator.Add(context.Background(), ispec.MediaTypeImageLayer, reader, history, compressAlgo, nil); err != nil { return fmt.Errorf("add diff layer: %w", err) } diff --git a/cmd/umoci/raw-add-layer.go b/cmd/umoci/raw-add-layer.go index 42e28861..76cf4fcb 100644 --- a/cmd/umoci/raw-add-layer.go +++ b/cmd/umoci/raw-add-layer.go @@ -30,11 +30,12 @@ import ( "github.com/opencontainers/umoci/mutate" "github.com/opencontainers/umoci/oci/cas/dir" "github.com/opencontainers/umoci/oci/casext" + "github.com/opencontainers/umoci/oci/casext/blobcompress" igen "github.com/opencontainers/umoci/oci/config/generate" "github.com/urfave/cli" ) -var rawAddLayerCommand = uxHistory(uxTag(cli.Command{ +var rawAddLayerCommand = uxCompress(uxHistory(uxTag(cli.Command{ Name: "add-layer", Usage: "add a layer archive verbatim to an image", ArgsUsage: `--image [:] @@ -65,13 +66,18 @@ only supports uncompressed archives.`, ctx.App.Metadata["newlayer"] = ctx.Args().First() return nil }, -})) +}))) func rawAddLayer(ctx *cli.Context) error { imagePath := ctx.App.Metadata["--image-path"].(string) fromName := ctx.App.Metadata["--image-tag"].(string) newLayerPath := ctx.App.Metadata["newlayer"].(string) + var compressAlgo blobcompress.Algorithm + if algo, ok := ctx.App.Metadata["--compress"].(blobcompress.Algorithm); ok { + compressAlgo = algo + } + // Overide the from tag by default, otherwise use the one specified. tagName := fromName if overrideTagName, ok := ctx.App.Metadata["--tag"]; ok { @@ -154,7 +160,7 @@ func rawAddLayer(ctx *cli.Context) error { } } - if _, err := mutator.Add(context.Background(), ispec.MediaTypeImageLayer, newLayer, history, nil, nil); err != nil { + if _, err := mutator.Add(context.Background(), ispec.MediaTypeImageLayer, newLayer, history, compressAlgo, nil); err != nil { return fmt.Errorf("add diff layer: %w", err) } diff --git a/cmd/umoci/repack.go b/cmd/umoci/repack.go index fb8cebb1..3c011977 100644 --- a/cmd/umoci/repack.go +++ b/cmd/umoci/repack.go @@ -29,12 +29,13 @@ import ( "github.com/opencontainers/umoci/mutate" "github.com/opencontainers/umoci/oci/cas/dir" "github.com/opencontainers/umoci/oci/casext" + "github.com/opencontainers/umoci/oci/casext/blobcompress" igen "github.com/opencontainers/umoci/oci/config/generate" "github.com/opencontainers/umoci/pkg/mtreefilter" "github.com/urfave/cli" ) -var repackCommand = uxHistory(cli.Command{ +var repackCommand = uxCompress(uxHistory(cli.Command{ Name: "repack", Usage: "repacks an OCI runtime bundle into a reference", ArgsUsage: `--image [:] @@ -88,13 +89,18 @@ manifest and configuration information uses the new diff atop the old manifest.` ctx.App.Metadata["bundle"] = ctx.Args().First() return nil }, -}) +})) func repack(ctx *cli.Context) error { imagePath := ctx.App.Metadata["--image-path"].(string) tagName := ctx.App.Metadata["--image-tag"].(string) bundlePath := ctx.App.Metadata["bundle"].(string) + var compressAlgo blobcompress.Algorithm + if algo, ok := ctx.App.Metadata["--compress"].(blobcompress.Algorithm); ok { + compressAlgo = algo + } + // Read the metadata first. meta, err := umoci.ReadBundleMeta(bundlePath) if err != nil { @@ -176,5 +182,5 @@ func repack(ctx *cli.Context) error { mtreefilter.MaskFilter(maskedPaths), } - return umoci.Repack(engineExt, tagName, bundlePath, meta, history, filters, ctx.Bool("refresh-bundle"), mutator, nil) + return umoci.Repack(engineExt, tagName, bundlePath, meta, history, filters, ctx.Bool("refresh-bundle"), mutator, compressAlgo) } diff --git a/cmd/umoci/utils_ux.go b/cmd/umoci/utils_ux.go index df555db2..8e4da39d 100644 --- a/cmd/umoci/utils_ux.go +++ b/cmd/umoci/utils_ux.go @@ -23,6 +23,7 @@ import ( "strings" "github.com/opencontainers/umoci/oci/casext" + "github.com/opencontainers/umoci/oci/casext/blobcompress" "github.com/urfave/cli" ) @@ -88,6 +89,45 @@ func uxHistory(cmd cli.Command) cli.Command { return cmd } +// uxCompress adds the --compress flag to the given cli.Command as well as +// adding relevant validation logic to the .Before of the command. The value +// will be stored in ctx.Metadata["--compress"] as a string (or nil if --tag +// was not specified). +func uxCompress(cmd cli.Command) cli.Command { + cmd.Flags = append(cmd.Flags, cli.StringFlag{ + Name: "compress", + Usage: "compression algorithm for newly created layer blobs", + Value: "auto", + }) + + oldBefore := cmd.Before + cmd.Before = func(ctx *cli.Context) error { + // Verify compression algorithm value. + var layerCompressor blobcompress.Algorithm + if ctx.IsSet("compress") { + compressType := ctx.String("compress") + if compressType == "none" { + compressType = "" + } + if compressType != "auto" { + layerCompressor = blobcompress.GetAlgorithm(compressType) + if layerCompressor == nil { + return fmt.Errorf("invalid --compress: unknown layer compression type %q", ctx.String("compress")) + } + } + } + ctx.App.Metadata["--compress"] = layerCompressor + + // Include any old befores set. + if oldBefore != nil { + return oldBefore(ctx) + } + return nil + } + + return cmd +} + // uxTag adds a --tag flag to the given cli.Command as well as adding relevant // validation logic to the .Before of the command. The value will be stored in // ctx.Metadata["--tag"] as a string (or nil if --tag was not specified). diff --git a/doc/man/umoci-insert.1.md b/doc/man/umoci-insert.1.md index 3446d136..442aaafa 100644 --- a/doc/man/umoci-insert.1.md +++ b/doc/man/umoci-insert.1.md @@ -8,6 +8,7 @@ umoci insert - insert content into an OCI image **umoci insert** **--image**=*image*[:*tag*] [**--tag**=*new-tag*] +[**--compress**=*compression-type*] [**--opaque**] [**--rootless**] [**--uid-map**=*value*] @@ -60,6 +61,21 @@ The global options are defined in **umoci**(1). Tag name for the modified image, if unspecified then the original tag provided to **--image** will be clobbered. +**--compress**=*compression-type* + Specify the compression type to use when creating a new layer. Supported + compression types are *none*, *gzip*, and *zstd*. **umoci-unpack**(1) + transparently supports all compression methods you can specify with + **--compress**. + + The special value *auto* will cause **umoci**(1) to auto-select the most + appropriate compression algorithm based on what previous layers are + compressed with (it will try to use the most recent layer's compression + algorithm which it supports). Note that *auto* will never select *none* + compression automatically, as not compressing **tar**(1) archives is really + not advisable. + + If no *compression-type* is provided, it defaults to *auto*. + **--opaque** (Assuming *target* is a directory.) Add an opaque whiteout entry for *target* so that any child path of *target* in previous layers is masked by the new diff --git a/doc/man/umoci-raw-add-layer.1.md b/doc/man/umoci-raw-add-layer.1.md index 922988d6..ecf910fc 100644 --- a/doc/man/umoci-raw-add-layer.1.md +++ b/doc/man/umoci-raw-add-layer.1.md @@ -8,6 +8,7 @@ umoci raw add-layer - add a layer archive verbatim to an image **umoci raw add-layer** **--image**=*image* [**--tag**=*tag*] +[**--compress**=*compression-type*] [**--no-history**] [**--history.comment**=*comment*] [**--history.created_by**=*created_by*] @@ -39,6 +40,21 @@ The global options are defined in **umoci**(1). tag in the image. If *tag* is not provided it defaults to the *tag* specified in **--image** (overwriting it). +**--compress**=*compression-type* + Specify the compression type to use when creating a new layer. Supported + compression types are *none*, *gzip*, and *zstd*. **umoci-unpack**(1) + transparently supports all compression methods you can specify with + **--compress**. + + The special value *auto* will cause **umoci**(1) to auto-select the most + appropriate compression algorithm based on what previous layers are + compressed with (it will try to use the most recent layer's compression + algorithm which it supports). Note that *auto* will never select *none* + compression automatically, as not compressing **tar**(1) archives is really + not advisable. + + If no *compression-type* is provided, it defaults to *auto*. + **--no-history** Causes no history entry to be added for this operation. **This is not recommended for use with umoci-raw-add-layer(1), since it results in the diff --git a/doc/man/umoci-repack.1.md b/doc/man/umoci-repack.1.md index ea5dac14..5d3ab3c5 100644 --- a/doc/man/umoci-repack.1.md +++ b/doc/man/umoci-repack.1.md @@ -7,6 +7,7 @@ umoci repack - Repacks an OCI runtime bundle into an image tag # SYNOPSIS **umoci repack** **--image**=*image*[:*tag*] +[**--compress**=*compression-type*] [**--no-history**] [**--history.comment**=*comment*] [**--history.created_by**=*created_by*] @@ -45,6 +46,21 @@ The global options are defined in **umoci**(1). the same name as *tag* it will be overwritten. If *tag* is not provided it defaults to "latest". +**--compress**=*compression-type* + Specify the compression type to use when creating a new layer. Supported + compression types are *none*, *gzip*, and *zstd*. **umoci-unpack**(1) + transparently supports all compression methods you can specify with + **--compress**. + + The special value *auto* will cause **umoci**(1) to auto-select the most + appropriate compression algorithm based on what previous layers are + compressed with (it will try to use the most recent layer's compression + algorithm which it supports). Note that *auto* will never select *none* + compression automatically, as not compressing **tar**(1) archives is really + not advisable. + + If no *compression-type* is provided, it defaults to *auto*. + **--no-history** Causes no history entry to be added for this operation. **This is not recommended for use with umoci-repack(1), since it results in the history not diff --git a/test/insert.bats b/test/insert.bats index 38206524..76ec4179 100644 --- a/test/insert.bats +++ b/test/insert.bats @@ -374,3 +374,160 @@ function teardown() { image-verify "${IMAGE}" } + +OCI_MEDIATYPE_LAYER="application/vnd.oci.image.layer.v1.tar" + +@test "umoci insert --compress=gzip" { + # Some things to insert. + INSERTDIR="$(setup_tmpdir)" + mkdir -p "${INSERTDIR}/etc" + touch "${INSERTDIR}/etc/foo" + + # Add layer to the image. + umoci insert --image "${IMAGE}:${TAG}" --compress=gzip "${INSERTDIR}/etc" /etc + [ "$status" -eq 0 ] + image-verify "${IMAGE}" + + umoci stat --image "${IMAGE}:${TAG}" --json + [ "$status" -eq 0 ] + stat_json="$output" + + # Make sure that the last layer had the expected compression based on the + # mediatype. + expected_mediatype="${OCI_MEDIATYPE_LAYER}+gzip" + layer_mediatype="$(jq -SMr '.history[-1].layer.mediaType' <<<"$stat_json")" + [[ "$layer_mediatype" == "$expected_mediatype" ]] + + # Make sure that the actual blob seems to be a gzip + layer_hash="$(jq -SMr '.history[-1].layer.digest' <<<"$stat_json" | tr : /)" + sane_run file -i "$IMAGE/blobs/$layer_hash" + [ "$status" -eq 0 ] + [[ "$output" == *"application/x-gzip"* ]] +} + +@test "umoci insert --compress=zstd" { + # Some things to insert. + INSERTDIR="$(setup_tmpdir)" + mkdir -p "${INSERTDIR}/etc" + touch "${INSERTDIR}/etc/foo" + + # Add layer to the image. + umoci insert --image "${IMAGE}:${TAG}" --compress=zstd "${INSERTDIR}/etc" /etc + [ "$status" -eq 0 ] + #image-verify "${IMAGE}" # image-tools cannot handle zstd + + umoci stat --image "${IMAGE}:${TAG}" --json + [ "$status" -eq 0 ] + stat_json="$output" + + # Make sure that the last layer had the expected compression based on the + # mediatype. + expected_mediatype="${OCI_MEDIATYPE_LAYER}+zstd" + layer_mediatype="$(jq -SMr '.history[-1].layer.mediaType' <<<"$stat_json")" + [[ "$layer_mediatype" == "$expected_mediatype" ]] + + # Make sure that the actual blob seems to be a gzip + layer_hash="$(jq -SMr '.history[-1].layer.digest' <<<"$stat_json" | tr : /)" + sane_run file -i "$IMAGE/blobs/$layer_hash" + [ "$status" -eq 0 ] + [[ "$output" == *"application/x-zstd"* ]] +} + +@test "umoci insert --compress=none" { + # Some things to insert. + INSERTDIR="$(setup_tmpdir)" + mkdir -p "${INSERTDIR}/etc" + touch "${INSERTDIR}/etc/foo" + + # Add layer to the image. + umoci insert --image "${IMAGE}:${TAG}" --compress=none "${INSERTDIR}/etc" /etc + [ "$status" -eq 0 ] + image-verify "${IMAGE}" + + umoci stat --image "${IMAGE}:${TAG}" --json + [ "$status" -eq 0 ] + stat_json="$output" + + # Make sure that the last layer had the expected compression based on the + # mediatype. + expected_mediatype="${OCI_MEDIATYPE_LAYER}" + layer_mediatype="$(jq -SMr '.history[-1].layer.mediaType' <<<"$stat_json")" + [[ "$layer_mediatype" == "$expected_mediatype" ]] + + # Make sure that the actual blob seems to be a gzip + layer_hash="$(jq -SMr '.history[-1].layer.digest' <<<"$stat_json" | tr : /)" + sane_run file -i "$IMAGE/blobs/$layer_hash" + [ "$status" -eq 0 ] + [[ "$output" == *"application/x-tar"* ]] # x-tar means no compression +} + +@test "umoci insert --compress=auto" { + # Some things to insert. + INSERTDIR="$(setup_tmpdir)" + mkdir -p "${INSERTDIR}/etc" + touch "${INSERTDIR}/etc/foo" + + # Add zstd layer to the image. + umoci insert --image "${IMAGE}:${TAG}" --compress=zstd "${INSERTDIR}/etc" /etc + [ "$status" -eq 0 ] + #image-verify "${IMAGE}" # image-tools cannot handle zstd + + umoci stat --image "${IMAGE}:${TAG}" --json + [ "$status" -eq 0 ] + stat_json="$output" + + # Make sure that the last layer had the expected compression based on the + # mediatype. + expected_mediatype="${OCI_MEDIATYPE_LAYER}+zstd" + layer_mediatype="$(jq -SMr '.history[-1].layer.mediaType' <<<"$stat_json")" + [[ "$layer_mediatype" == "$expected_mediatype" ]] + + # Make sure that the actual blob seems to be a gzip + layer_hash="$(jq -SMr '.history[-1].layer.digest' <<<"$stat_json" | tr : /)" + sane_run file -i "$IMAGE/blobs/$layer_hash" + [ "$status" -eq 0 ] + [[ "$output" == *"application/x-zstd"* ]] + + # Add another zstd layer to the image, by making use of the auto selection. + umoci insert --image "${IMAGE}:${TAG}" --compress=auto "${INSERTDIR}/etc" /etc + [ "$status" -eq 0 ] + #image-verify "${IMAGE}" # image-tools cannot handle zstd + + umoci stat --image "${IMAGE}:${TAG}" --json + [ "$status" -eq 0 ] + stat_json="$output" + + # Make sure that the last layer had the expected compression based on the + # mediatype. + expected_mediatype="${OCI_MEDIATYPE_LAYER}+zstd" + layer_mediatype="$(jq -SMr '.history[-1].layer.mediaType' <<<"$stat_json")" + [[ "$layer_mediatype" == "$expected_mediatype" ]] + + # Make sure that the actual blob seems to be a gzip + layer_hash="$(jq -SMr '.history[-1].layer.digest' <<<"$stat_json" | tr : /)" + sane_run file -i "$IMAGE/blobs/$layer_hash" + [ "$status" -eq 0 ] + [[ "$output" == *"application/x-zstd"* ]] + + # Add yet another zstd layer to the image, to show that --compress=auto is + # the default. + umoci insert --image "${IMAGE}:${TAG}" "${INSERTDIR}/etc" /etc + [ "$status" -eq 0 ] + #image-verify "${IMAGE}" # image-tools cannot handle zstd + + umoci stat --image "${IMAGE}:${TAG}" --json + [ "$status" -eq 0 ] + stat_json="$output" + + # Make sure that the last layer had the expected compression based on the + # mediatype. + expected_mediatype="${OCI_MEDIATYPE_LAYER}+zstd" + layer_mediatype="$(jq -SMr '.history[-1].layer.mediaType' <<<"$stat_json")" + [[ "$layer_mediatype" == "$expected_mediatype" ]] + + # Make sure that the actual blob seems to be a gzip + layer_hash="$(jq -SMr '.history[-1].layer.digest' <<<"$stat_json" | tr : /)" + sane_run file -i "$IMAGE/blobs/$layer_hash" + [ "$status" -eq 0 ] + [[ "$output" == *"application/x-zstd"* ]] +} diff --git a/test/raw-add-layer.bats b/test/raw-add-layer.bats index 558f666d..55706cbc 100644 --- a/test/raw-add-layer.bats +++ b/test/raw-add-layer.bats @@ -89,6 +89,171 @@ function teardown() { image-verify "${IMAGE}" } +OCI_MEDIATYPE_LAYER="application/vnd.oci.image.layer.v1.tar" + +@test "umoci raw add-layer --compress=gzip" { + LAYER="$(setup_tmpdir)" + echo "layer" > "$LAYER/file" + sane_run tar cvfC "$UMOCI_TMPDIR/layer.tar" "$LAYER" . + + # Add layer to the image. + umoci new --image "${IMAGE}:${TAG}" + [ "$status" -eq 0 ] + #image-verify "${IMAGE}" + umoci raw add-layer --image "${IMAGE}:${TAG}" --compress=gzip "$UMOCI_TMPDIR/layer.tar" + [ "$status" -eq 0 ] + image-verify "${IMAGE}" + + umoci stat --image "${IMAGE}:${TAG}" --json + [ "$status" -eq 0 ] + stat_json="$output" + + # Make sure that the last layer had the expected compression based on the + # mediatype. + expected_mediatype="${OCI_MEDIATYPE_LAYER}+gzip" + layer_mediatype="$(jq -SMr '.history[-1].layer.mediaType' <<<"$stat_json")" + [[ "$layer_mediatype" == "$expected_mediatype" ]] + + # Make sure that the actual blob seems to be a gzip + layer_hash="$(jq -SMr '.history[-1].layer.digest' <<<"$stat_json" | tr : /)" + sane_run file -i "$IMAGE/blobs/$layer_hash" + [ "$status" -eq 0 ] + [[ "$output" == *"application/x-gzip"* ]] +} + +@test "umoci raw add-layer --compress=zstd" { + LAYER="$(setup_tmpdir)" + echo "layer" > "$LAYER/file" + sane_run tar cvfC "$UMOCI_TMPDIR/layer.tar" "$LAYER" . + + # Add layer to the image. + umoci new --image "${IMAGE}:${TAG}" + [ "$status" -eq 0 ] + #image-verify "${IMAGE}" + umoci raw add-layer --image "${IMAGE}:${TAG}" --compress=zstd "$UMOCI_TMPDIR/layer.tar" + [ "$status" -eq 0 ] + #image-verify "${IMAGE}" # image-tools cannot handle zstd + + umoci stat --image "${IMAGE}:${TAG}" --json + [ "$status" -eq 0 ] + stat_json="$output" + + # Make sure that the last layer had the expected compression based on the + # mediatype. + expected_mediatype="${OCI_MEDIATYPE_LAYER}+zstd" + layer_mediatype="$(jq -SMr '.history[-1].layer.mediaType' <<<"$stat_json")" + [[ "$layer_mediatype" == "$expected_mediatype" ]] + + # Make sure that the actual blob seems to be a gzip + layer_hash="$(jq -SMr '.history[-1].layer.digest' <<<"$stat_json" | tr : /)" + sane_run file -i "$IMAGE/blobs/$layer_hash" + [ "$status" -eq 0 ] + [[ "$output" == *"application/x-zstd"* ]] +} + +@test "umoci raw add-layer --compress=none" { + LAYER="$(setup_tmpdir)" + echo "layer" > "$LAYER/file" + sane_run tar cvfC "$UMOCI_TMPDIR/layer.tar" "$LAYER" . + + # Add layer to the image. + umoci new --image "${IMAGE}:${TAG}" + [ "$status" -eq 0 ] + #image-verify "${IMAGE}" + umoci raw add-layer --image "${IMAGE}:${TAG}" --compress=none "$UMOCI_TMPDIR/layer.tar" + [ "$status" -eq 0 ] + image-verify "${IMAGE}" + + umoci stat --image "${IMAGE}:${TAG}" --json + [ "$status" -eq 0 ] + stat_json="$output" + + # Make sure that the last layer had the expected compression based on the + # mediatype. + expected_mediatype="${OCI_MEDIATYPE_LAYER}" + layer_mediatype="$(jq -SMr '.history[-1].layer.mediaType' <<<"$stat_json")" + [[ "$layer_mediatype" == "$expected_mediatype" ]] + + # Make sure that the actual blob seems to be a gzip + layer_hash="$(jq -SMr '.history[-1].layer.digest' <<<"$stat_json" | tr : /)" + sane_run file -i "$IMAGE/blobs/$layer_hash" + [ "$status" -eq 0 ] + [[ "$output" == *"application/x-tar"* ]] # x-tar means no compression +} + +@test "umoci raw add-layer --compress=auto" { + LAYER="$(setup_tmpdir)" + echo "layer" > "$LAYER/file" + sane_run tar cvfC "$UMOCI_TMPDIR/layer.tar" "$LAYER" . + + # Add zstd layer to the image. + umoci new --image "${IMAGE}:${TAG}" + [ "$status" -eq 0 ] + #image-verify "${IMAGE}" + umoci raw add-layer --image "${IMAGE}:${TAG}" --compress=zstd "$UMOCI_TMPDIR/layer.tar" + [ "$status" -eq 0 ] + #image-verify "${IMAGE}" # image-tools cannot handle zstd + + umoci stat --image "${IMAGE}:${TAG}" --json + [ "$status" -eq 0 ] + stat_json="$output" + + # Make sure that the last layer had the expected compression based on the + # mediatype. + expected_mediatype="${OCI_MEDIATYPE_LAYER}+zstd" + layer_mediatype="$(jq -SMr '.history[-1].layer.mediaType' <<<"$stat_json")" + [[ "$layer_mediatype" == "$expected_mediatype" ]] + + # Make sure that the actual blob seems to be a gzip + layer_hash="$(jq -SMr '.history[-1].layer.digest' <<<"$stat_json" | tr : /)" + sane_run file -i "$IMAGE/blobs/$layer_hash" + [ "$status" -eq 0 ] + [[ "$output" == *"application/x-zstd"* ]] + + # Add another zstd layer to the image, by making use of the auto selection. + umoci raw add-layer --image "${IMAGE}:${TAG}" --compress=auto "$UMOCI_TMPDIR/layer.tar" + [ "$status" -eq 0 ] + #image-verify "${IMAGE}" # image-tools cannot handle zstd + + umoci stat --image "${IMAGE}:${TAG}" --json + [ "$status" -eq 0 ] + stat_json="$output" + + # Make sure that the last layer had the expected compression based on the + # mediatype. + expected_mediatype="${OCI_MEDIATYPE_LAYER}+zstd" + layer_mediatype="$(jq -SMr '.history[-1].layer.mediaType' <<<"$stat_json")" + [[ "$layer_mediatype" == "$expected_mediatype" ]] + + # Make sure that the actual blob seems to be a gzip + layer_hash="$(jq -SMr '.history[-1].layer.digest' <<<"$stat_json" | tr : /)" + sane_run file -i "$IMAGE/blobs/$layer_hash" + [ "$status" -eq 0 ] + [[ "$output" == *"application/x-zstd"* ]] + + # Add yet another zstd layer to the image, to show that --compress=auto is + # the default. + umoci raw add-layer --image "${IMAGE}:${TAG}" "$UMOCI_TMPDIR/layer.tar" + [ "$status" -eq 0 ] + #image-verify "${IMAGE}" # image-tools cannot handle zstd + + umoci stat --image "${IMAGE}:${TAG}" --json + [ "$status" -eq 0 ] + stat_json="$output" + + # Make sure that the last layer had the expected compression based on the + # mediatype. + expected_mediatype="${OCI_MEDIATYPE_LAYER}+zstd" + layer_mediatype="$(jq -SMr '.history[-1].layer.mediaType' <<<"$stat_json")" + [[ "$layer_mediatype" == "$expected_mediatype" ]] + + # Make sure that the actual blob seems to be a gzip + layer_hash="$(jq -SMr '.history[-1].layer.digest' <<<"$stat_json" | tr : /)" + sane_run file -i "$IMAGE/blobs/$layer_hash" + [ "$status" -eq 0 ] + [[ "$output" == *"application/x-zstd"* ]] +} + @test "umoci raw add-layer [invalid arguments]" { LAYERFILE="$UMOCI_TMPDIR/file" touch "$LAYERFILE"{,-extra} diff --git a/test/repack.bats b/test/repack.bats index 6e4eb1e7..0bf716a0 100644 --- a/test/repack.bats +++ b/test/repack.bats @@ -876,3 +876,176 @@ function teardown() { layers1=$(cat "${IMAGE}/oci/blobs/sha256/$manifest1" | jq -r .layers) [ "$layers0" == "$layers1" ] } + +OCI_MEDIATYPE_LAYER="application/vnd.oci.image.layer.v1.tar" + +@test "umoci repack --compress=gzip" { + # Unpack the image. + new_bundle_rootfs + umoci unpack --image "${IMAGE}:${TAG}" "$BUNDLE" + [ "$status" -eq 0 ] + bundle-verify "$BUNDLE" + + # Make sure we make a new tar layer. + touch "$ROOTFS/new-file" + # Add layer to the image. + umoci repack --image "${IMAGE}:${TAG}" --compress=gzip "$BUNDLE" + [ "$status" -eq 0 ] + image-verify "${IMAGE}" + + umoci stat --image "${IMAGE}:${TAG}" --json + [ "$status" -eq 0 ] + stat_json="$output" + + # Make sure that the last layer had the expected compression based on the + # mediatype. + expected_mediatype="${OCI_MEDIATYPE_LAYER}+gzip" + layer_mediatype="$(jq -SMr '.history[-1].layer.mediaType' <<<"$stat_json")" + [[ "$layer_mediatype" == "$expected_mediatype" ]] + + # Make sure that the actual blob seems to be a gzip + layer_hash="$(jq -SMr '.history[-1].layer.digest' <<<"$stat_json" | tr : /)" + sane_run file -i "$IMAGE/blobs/$layer_hash" + [ "$status" -eq 0 ] + [[ "$output" == *"application/x-gzip"* ]] +} + +@test "umoci repack --compress=zstd" { + # Unpack the image. + new_bundle_rootfs + umoci unpack --image "${IMAGE}:${TAG}" "$BUNDLE" + [ "$status" -eq 0 ] + bundle-verify "$BUNDLE" + + # Make sure we make a new tar layer. + touch "$ROOTFS/new-file" + # Add layer to the image. + umoci repack --image "${IMAGE}:${TAG}" --compress=zstd "$BUNDLE" + [ "$status" -eq 0 ] + #image-verify "${IMAGE}" # image-tools cannot handle zstd + + umoci stat --image "${IMAGE}:${TAG}" --json + [ "$status" -eq 0 ] + stat_json="$output" + + # Make sure that the last layer had the expected compression based on the + # mediatype. + expected_mediatype="${OCI_MEDIATYPE_LAYER}+zstd" + layer_mediatype="$(jq -SMr '.history[-1].layer.mediaType' <<<"$stat_json")" + [[ "$layer_mediatype" == "$expected_mediatype" ]] + + # Make sure that the actual blob seems to be a gzip + layer_hash="$(jq -SMr '.history[-1].layer.digest' <<<"$stat_json" | tr : /)" + sane_run file -i "$IMAGE/blobs/$layer_hash" + [ "$status" -eq 0 ] + [[ "$output" == *"application/x-zstd"* ]] +} + +@test "umoci repack --compress=none" { + # Unpack the image. + new_bundle_rootfs + umoci unpack --image "${IMAGE}:${TAG}" "$BUNDLE" + [ "$status" -eq 0 ] + bundle-verify "$BUNDLE" + + # Make sure we make a new tar layer. + touch "$ROOTFS/new-file" + # Add layer to the image. + umoci repack --image "${IMAGE}:${TAG}" --compress=none "$BUNDLE" + [ "$status" -eq 0 ] + image-verify "${IMAGE}" + + umoci stat --image "${IMAGE}:${TAG}" --json + [ "$status" -eq 0 ] + stat_json="$output" + + # Make sure that the last layer had the expected compression based on the + # mediatype. + expected_mediatype="${OCI_MEDIATYPE_LAYER}" + layer_mediatype="$(jq -SMr '.history[-1].layer.mediaType' <<<"$stat_json")" + [[ "$layer_mediatype" == "$expected_mediatype" ]] + + # Make sure that the actual blob seems to be a gzip + layer_hash="$(jq -SMr '.history[-1].layer.digest' <<<"$stat_json" | tr : /)" + sane_run file -i "$IMAGE/blobs/$layer_hash" + [ "$status" -eq 0 ] + [[ "$output" == *"application/x-tar"* ]] # x-tar means no compression +} + +@test "umoci repack --refresh-bundle --compress=auto" { + # Unpack the image. + new_bundle_rootfs + umoci unpack --image "${IMAGE}:${TAG}" "$BUNDLE" + [ "$status" -eq 0 ] + bundle-verify "$BUNDLE" + + # Make sure we make a new tar layer. + touch "$ROOTFS/new-file1" + # Add zstd layer to the image. + umoci repack --image "${IMAGE}:${TAG}" --refresh-bundle --compress=zstd "$BUNDLE" + [ "$status" -eq 0 ] + #image-verify "${IMAGE}" # image-tools cannot handle zstd + + umoci stat --image "${IMAGE}:${TAG}" --json + [ "$status" -eq 0 ] + stat_json="$output" + + # Make sure that the last layer had the expected compression based on the + # mediatype. + expected_mediatype="${OCI_MEDIATYPE_LAYER}+zstd" + layer_mediatype="$(jq -SMr '.history[-1].layer.mediaType' <<<"$stat_json")" + [[ "$layer_mediatype" == "$expected_mediatype" ]] + + # Make sure that the actual blob seems to be a gzip + layer_hash="$(jq -SMr '.history[-1].layer.digest' <<<"$stat_json" | tr : /)" + sane_run file -i "$IMAGE/blobs/$layer_hash" + [ "$status" -eq 0 ] + [[ "$output" == *"application/x-zstd"* ]] + + # Make sure we make a new tar layer. + touch "$ROOTFS/new-file2" + # Add another zstd layer to the image, by making use of the auto selection. + umoci repack --image "${IMAGE}:${TAG}" --refresh-bundle --compress=auto "$BUNDLE" + [ "$status" -eq 0 ] + #image-verify "${IMAGE}" # image-tools cannot handle zstd + + umoci stat --image "${IMAGE}:${TAG}" --json + [ "$status" -eq 0 ] + stat_json="$output" + + # Make sure that the last layer had the expected compression based on the + # mediatype. + expected_mediatype="${OCI_MEDIATYPE_LAYER}+zstd" + layer_mediatype="$(jq -SMr '.history[-1].layer.mediaType' <<<"$stat_json")" + [[ "$layer_mediatype" == "$expected_mediatype" ]] + + # Make sure that the actual blob seems to be a gzip + layer_hash="$(jq -SMr '.history[-1].layer.digest' <<<"$stat_json" | tr : /)" + sane_run file -i "$IMAGE/blobs/$layer_hash" + [ "$status" -eq 0 ] + [[ "$output" == *"application/x-zstd"* ]] + + # Make sure we make a new tar layer. + touch "$ROOTFS/new-file3" + # Add yet another zstd layer to the image, to show that --compress=auto is + # the default. + umoci repack --image "${IMAGE}:${TAG}" --refresh-bundle "$BUNDLE" + [ "$status" -eq 0 ] + #image-verify "${IMAGE}" # image-tools cannot handle zstd + + umoci stat --image "${IMAGE}:${TAG}" --json + [ "$status" -eq 0 ] + stat_json="$output" + + # Make sure that the last layer had the expected compression based on the + # mediatype. + expected_mediatype="${OCI_MEDIATYPE_LAYER}+zstd" + layer_mediatype="$(jq -SMr '.history[-1].layer.mediaType' <<<"$stat_json")" + [[ "$layer_mediatype" == "$expected_mediatype" ]] + + # Make sure that the actual blob seems to be a gzip + layer_hash="$(jq -SMr '.history[-1].layer.digest' <<<"$stat_json" | tr : /)" + sane_run file -i "$IMAGE/blobs/$layer_hash" + [ "$status" -eq 0 ] + [[ "$output" == *"application/x-zstd"* ]] +} From a79788c83a47d090117d73a00a9e193d1844ea37 Mon Sep 17 00:00:00 2001 From: Aleksa Sarai Date: Fri, 4 Apr 2025 03:12:36 +1100 Subject: [PATCH 9/9] test: add mixed-compression unpack tests It would be nice to have a test using an upstream zstd-compressed image, rather than only using stuff we generate, but the tests we already have for --compress=... should be more than enough to verify that we are dealing with properly mixed compression types. Signed-off-by: Aleksa Sarai --- test/unpack.bats | 60 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/test/unpack.bats b/test/unpack.bats index 02a22525..311cb24d 100644 --- a/test/unpack.bats +++ b/test/unpack.bats @@ -354,3 +354,63 @@ function teardown() { [ "$(readlink "$ROOTFS/loop3")" = "link2/loop4" ] [ "$(readlink "$ROOTFS/dir/loop4")" = "../loop1" ] } + +@test "umoci unpack [mixed compression]" { + # Unpack the image. + new_bundle_rootfs + umoci unpack --image "${IMAGE}:${TAG}" "$BUNDLE" + [ "$status" -eq 0 ] + bundle-verify "$BUNDLE" + + # Create a few layers with different compression algorithms. + + # zstd layer + touch "$ROOTFS/zstd1" + umoci repack --image "${IMAGE}:${TAG}" --refresh-bundle --compress=zstd "$BUNDLE" + [ "$status" -eq 0 ] + #image-verify "${IMAGE}" # image-tools cannot handle zstd + + # gzip layer + touch "$ROOTFS/gzip1" + umoci repack --image "${IMAGE}:${TAG}" --refresh-bundle --compress=gzip "$BUNDLE" + [ "$status" -eq 0 ] + #image-verify "${IMAGE}" # image-tools cannot handle zstd + + # plain layer + touch "$ROOTFS/plain1" + umoci repack --image "${IMAGE}:${TAG}" --refresh-bundle --compress=none "$BUNDLE" + [ "$status" -eq 0 ] + #image-verify "${IMAGE}" # image-tools cannot handle zstd + + # zstd layer + touch "$ROOTFS/zstd2" + umoci repack --image "${IMAGE}:${TAG}" --refresh-bundle --compress=zstd "$BUNDLE" + [ "$status" -eq 0 ] + #image-verify "${IMAGE}" # image-tools cannot handle zstd + + # plain layer + touch "$ROOTFS/plain2" + umoci repack --image "${IMAGE}:${TAG}" --refresh-bundle --compress=none "$BUNDLE" + [ "$status" -eq 0 ] + #image-verify "${IMAGE}" # image-tools cannot handle zstd + + # zstd layer (auto) + touch "$ROOTFS/zstd3" + umoci repack --image "${IMAGE}:${TAG}" --refresh-bundle "$BUNDLE" + [ "$status" -eq 0 ] + #image-verify "${IMAGE}" # image-tools cannot handle zstd + + # Re-extract the latest image and make sure all of the files were correctly + # extracted. + new_bundle_rootfs + umoci unpack --image "${IMAGE}:${TAG}" "$BUNDLE" + [ "$status" -eq 0 ] + bundle-verify "$BUNDLE" + + [ -f "$ROOTFS/zstd1" ] + [ -f "$ROOTFS/gzip1" ] + [ -f "$ROOTFS/plain1" ] + [ -f "$ROOTFS/zstd2" ] + [ -f "$ROOTFS/plain2" ] + [ -f "$ROOTFS/zstd3" ] +}