diff --git a/.dockerignore b/.dockerignore index 30fb91f..42c6acf 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,17 +1,19 @@ # THIS FILE WAS AUTOMATICALLY GENERATED, PLEASE DO NOT EDIT. # -# Generated on 2022-11-28T15:27:14Z by kres 3ac53a8. +# Generated on 2023-08-21T16:35:57Z by kres latest. * !channel !containers +!ensure !maps !optional !pair !slices !value !xerrors -!xsync +!xslices +!xtesting !go.mod !go.sum !.golangci.yml diff --git a/.drone.yml b/.drone.yml index 575e303..11f07e5 100644 --- a/.drone.yml +++ b/.drone.yml @@ -1,7 +1,7 @@ --- # THIS FILE WAS AUTOMATICALLY GENERATED, PLEASE DO NOT EDIT. # -# Generated on 2023-05-11T13:47:02Z by kres latest. +# Generated on 2023-08-21T14:49:21Z by kres latest. kind: pipeline type: kubernetes @@ -174,7 +174,7 @@ steps: services: - name: docker - image: docker:23.0-dind + image: docker:24.0-dind entrypoint: - dockerd commands: diff --git a/.golangci.yml b/.golangci.yml index 4634156..70878c8 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,6 +1,6 @@ # THIS FILE WAS AUTOMATICALLY GENERATED, PLEASE DO NOT EDIT. # -# Generated on 2023-05-11T13:47:02Z by kres latest. +# Generated on 2023-08-21T14:49:21Z by kres latest. # options for analysis running run: @@ -36,7 +36,7 @@ linters-settings: lines: 60 statements: 40 gci: - local-prefixes: github.com/siderolabs/gen + local-prefixes: github.com/siderolabs/gen/ gocognit: min-complexity: 30 ireturn: @@ -65,7 +65,7 @@ linters-settings: gofmt: simplify: true goimports: - local-prefixes: github.com/siderolabs/gen + local-prefixes: github.com/siderolabs/gen/ golint: min-confidence: 0.8 gomnd: @@ -74,9 +74,8 @@ linters-settings: govet: check-shadowing: true enable-all: true - depguard: - list-type: blacklist - include-go-root: false + disable: + - loopclosure lll: line-length: 200 tab-width: 4 @@ -118,6 +117,10 @@ linters-settings: cyclop: # the maximal code complexity to report max-complexity: 20 + # depguard: + # Main: + # deny: + # - github.com/OpenPeeDeeP/depguard # this is just an example linters: enable-all: true @@ -145,6 +148,8 @@ linters: - typecheck - varnamelen - wrapcheck + - depguard # Disabled because starting with golangci-lint 1.53.0 it doesn't allow denylist alone anymore + - tagalign # abandoned linters for which golangci shows the warning that the repo is archived by the owner - interfacer - maligned diff --git a/Dockerfile b/Dockerfile index 952adaa..4c87ed8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,8 +1,8 @@ -# syntax = docker/dockerfile-upstream:1.5.2-labs +# syntax = docker/dockerfile-upstream:1.6.0-labs # THIS FILE WAS AUTOMATICALLY GENERATED, PLEASE DO NOT EDIT. # -# Generated on 2023-05-11T13:47:02Z by kres latest. +# Generated on 2023-08-21T16:35:57Z by kres latest. ARG TOOLCHAIN @@ -10,9 +10,9 @@ ARG TOOLCHAIN FROM scratch AS generate # runs markdownlint -FROM docker.io/node:20.1.0-alpine3.17 AS lint-markdown +FROM docker.io/node:20.5.1-alpine3.18 AS lint-markdown WORKDIR /src -RUN npm i -g markdownlint-cli@0.34.0 +RUN npm i -g markdownlint-cli@0.35.0 RUN npm i sentences-per-line@0.2.1 COPY .markdownlint.json . COPY ./README.md ./README.md @@ -27,38 +27,45 @@ FROM --platform=${BUILDPLATFORM} toolchain AS tools ENV GO111MODULE on ARG CGO_ENABLED ENV CGO_ENABLED ${CGO_ENABLED} +ARG GOTOOLCHAIN +ENV GOTOOLCHAIN ${GOTOOLCHAIN} +ARG GOEXPERIMENT +ENV GOEXPERIMENT ${GOEXPERIMENT} ENV GOPATH /go +ARG DEEPCOPY_VERSION +RUN --mount=type=cache,target=/root/.cache/go-build --mount=type=cache,target=/go/pkg go install github.com/siderolabs/deep-copy@${DEEPCOPY_VERSION} \ + && mv /go/bin/deep-copy /bin/deep-copy ARG GOLANGCILINT_VERSION RUN --mount=type=cache,target=/root/.cache/go-build --mount=type=cache,target=/go/pkg go install github.com/golangci/golangci-lint/cmd/golangci-lint@${GOLANGCILINT_VERSION} \ && mv /go/bin/golangci-lint /bin/golangci-lint -ARG GOFUMPT_VERSION -RUN go install mvdan.cc/gofumpt@${GOFUMPT_VERSION} \ - && mv /go/bin/gofumpt /bin/gofumpt RUN --mount=type=cache,target=/root/.cache/go-build --mount=type=cache,target=/go/pkg go install golang.org/x/vuln/cmd/govulncheck@latest \ && mv /go/bin/govulncheck /bin/govulncheck ARG GOIMPORTS_VERSION RUN --mount=type=cache,target=/root/.cache/go-build --mount=type=cache,target=/go/pkg go install golang.org/x/tools/cmd/goimports@${GOIMPORTS_VERSION} \ && mv /go/bin/goimports /bin/goimports -ARG DEEPCOPY_VERSION -RUN --mount=type=cache,target=/root/.cache/go-build --mount=type=cache,target=/go/pkg go install github.com/siderolabs/deep-copy@${DEEPCOPY_VERSION} \ - && mv /go/bin/deep-copy /bin/deep-copy +ARG GOFUMPT_VERSION +RUN go install mvdan.cc/gofumpt@${GOFUMPT_VERSION} \ + && mv /go/bin/gofumpt /bin/gofumpt # tools and sources FROM tools AS base WORKDIR /src -COPY ./go.mod . -COPY ./go.sum . +COPY go.mod go.mod +COPY go.sum go.sum +RUN cd . RUN --mount=type=cache,target=/go/pkg go mod download RUN --mount=type=cache,target=/go/pkg go mod verify COPY ./channel ./channel COPY ./containers ./containers +COPY ./ensure ./ensure COPY ./maps ./maps COPY ./optional ./optional COPY ./pair ./pair COPY ./slices ./slices COPY ./value ./value COPY ./xerrors ./xerrors -COPY ./xsync ./xsync +COPY ./xslices ./xslices +COPY ./xtesting ./xtesting RUN --mount=type=cache,target=/go/pkg go list -mod=readonly all >/dev/null # runs gofumpt @@ -67,28 +74,32 @@ RUN FILES="$(gofumpt -l .)" && test -z "${FILES}" || (echo -e "Source code is no # runs goimports FROM base AS lint-goimports -RUN FILES="$(goimports -l -local github.com/siderolabs/gen .)" && test -z "${FILES}" || (echo -e "Source code is not formatted with 'goimports -w -local github.com/siderolabs/gen .':\n${FILES}"; exit 1) +RUN FILES="$(goimports -l -local github.com/siderolabs/gen/ .)" && test -z "${FILES}" || (echo -e "Source code is not formatted with 'goimports -w -local github.com/siderolabs/gen/ .':\n${FILES}"; exit 1) # runs golangci-lint FROM base AS lint-golangci-lint +WORKDIR /src COPY .golangci.yml . ENV GOGC 50 RUN --mount=type=cache,target=/root/.cache/go-build --mount=type=cache,target=/root/.cache/golangci-lint --mount=type=cache,target=/go/pkg golangci-lint run --config .golangci.yml # runs govulncheck FROM base AS lint-govulncheck +WORKDIR /src RUN --mount=type=cache,target=/root/.cache/go-build --mount=type=cache,target=/go/pkg govulncheck ./... # runs unit-tests with race detector FROM base AS unit-tests-race +WORKDIR /src ARG TESTPKGS RUN --mount=type=cache,target=/root/.cache/go-build --mount=type=cache,target=/go/pkg --mount=type=cache,target=/tmp CGO_ENABLED=1 go test -v -race -count 1 ${TESTPKGS} # runs unit-tests FROM base AS unit-tests-run +WORKDIR /src ARG TESTPKGS RUN --mount=type=cache,target=/root/.cache/go-build --mount=type=cache,target=/go/pkg --mount=type=cache,target=/tmp go test -v -covermode=atomic -coverprofile=coverage.txt -coverpkg=${TESTPKGS} -count 1 ${TESTPKGS} FROM scratch AS unit-tests -COPY --from=unit-tests-run /src/coverage.txt /coverage.txt +COPY --from=unit-tests-run /src/coverage.txt /coverage-unit-tests.txt diff --git a/Makefile b/Makefile index 67d0bee..5aa2b01 100644 --- a/Makefile +++ b/Makefile @@ -1,11 +1,12 @@ # THIS FILE WAS AUTOMATICALLY GENERATED, PLEASE DO NOT EDIT. # -# Generated on 2023-05-11T13:47:02Z by kres latest. +# Generated on 2023-08-21T14:49:21Z by kres latest. # common variables SHA := $(shell git describe --match=none --always --abbrev=8 --dirty) TAG := $(shell git describe --tag --always --dirty) +ABBREV_TAG := $(shell git describe --tags >/dev/null 2>/dev/null && git describe --tag --always --match v[0-9]\* --abbrev=0 || echo 'undefined') BRANCH := $(shell git rev-parse --abbrev-ref HEAD) ARTIFACTS := _out WITH_DEBUG ?= false @@ -13,18 +14,20 @@ WITH_RACE ?= false REGISTRY ?= ghcr.io USERNAME ?= siderolabs REGISTRY_AND_USERNAME ?= $(REGISTRY)/$(USERNAME) -GOLANGCILINT_VERSION ?= v1.52.2 -GOFUMPT_VERSION ?= v0.5.0 -GO_VERSION ?= 1.20 -GOIMPORTS_VERSION ?= v0.9.1 PROTOBUF_GO_VERSION ?= 1.28.1 GRPC_GO_VERSION ?= 1.3.0 -GRPC_GATEWAY_VERSION ?= 2.15.2 +GRPC_GATEWAY_VERSION ?= 2.16.2 VTPROTOBUF_VERSION ?= 0.4.0 DEEPCOPY_VERSION ?= v0.5.5 +GOLANGCILINT_VERSION ?= v1.54.2 +GOFUMPT_VERSION ?= v0.5.0 +GO_VERSION ?= 1.21 +GOIMPORTS_VERSION ?= v0.12.0 GO_BUILDFLAGS ?= GO_LDFLAGS ?= CGO_ENABLED ?= 0 +GOTOOLCHAIN ?= local +GOEXPERIMENT ?= loopvar TESTPKGS ?= ./... KRES_IMAGE ?= ghcr.io/siderolabs/kres:latest CONFORMANCE_IMAGE ?= ghcr.io/siderolabs/conform:latest @@ -37,28 +40,32 @@ PROGRESS ?= auto PUSH ?= false CI_ARGS ?= COMMON_ARGS = --file=Dockerfile +COMMON_ARGS += --provenance=false COMMON_ARGS += --progress=$(PROGRESS) COMMON_ARGS += --platform=$(PLATFORM) COMMON_ARGS += --push=$(PUSH) COMMON_ARGS += --build-arg=ARTIFACTS="$(ARTIFACTS)" COMMON_ARGS += --build-arg=SHA="$(SHA)" COMMON_ARGS += --build-arg=TAG="$(TAG)" +COMMON_ARGS += --build-arg=ABBREV_TAG="$(ABBREV_TAG)" COMMON_ARGS += --build-arg=USERNAME="$(USERNAME)" COMMON_ARGS += --build-arg=REGISTRY="$(REGISTRY)" COMMON_ARGS += --build-arg=TOOLCHAIN="$(TOOLCHAIN)" COMMON_ARGS += --build-arg=CGO_ENABLED="$(CGO_ENABLED)" COMMON_ARGS += --build-arg=GO_BUILDFLAGS="$(GO_BUILDFLAGS)" COMMON_ARGS += --build-arg=GO_LDFLAGS="$(GO_LDFLAGS)" -COMMON_ARGS += --build-arg=GOLANGCILINT_VERSION="$(GOLANGCILINT_VERSION)" -COMMON_ARGS += --build-arg=GOFUMPT_VERSION="$(GOFUMPT_VERSION)" -COMMON_ARGS += --build-arg=GOIMPORTS_VERSION="$(GOIMPORTS_VERSION)" +COMMON_ARGS += --build-arg=GOTOOLCHAIN="$(GOTOOLCHAIN)" +COMMON_ARGS += --build-arg=GOEXPERIMENT="$(GOEXPERIMENT)" COMMON_ARGS += --build-arg=PROTOBUF_GO_VERSION="$(PROTOBUF_GO_VERSION)" COMMON_ARGS += --build-arg=GRPC_GO_VERSION="$(GRPC_GO_VERSION)" COMMON_ARGS += --build-arg=GRPC_GATEWAY_VERSION="$(GRPC_GATEWAY_VERSION)" COMMON_ARGS += --build-arg=VTPROTOBUF_VERSION="$(VTPROTOBUF_VERSION)" COMMON_ARGS += --build-arg=DEEPCOPY_VERSION="$(DEEPCOPY_VERSION)" +COMMON_ARGS += --build-arg=GOLANGCILINT_VERSION="$(GOLANGCILINT_VERSION)" +COMMON_ARGS += --build-arg=GOIMPORTS_VERSION="$(GOIMPORTS_VERSION)" +COMMON_ARGS += --build-arg=GOFUMPT_VERSION="$(GOFUMPT_VERSION)" COMMON_ARGS += --build-arg=TESTPKGS="$(TESTPKGS)" -TOOLCHAIN ?= docker.io/golang:1.20-alpine +TOOLCHAIN ?= docker.io/golang:1.21-alpine # help menu @@ -126,7 +133,8 @@ lint-gofumpt: ## Runs gofumpt linter. .PHONY: fmt fmt: ## Formats the source code @docker run --rm -it -v $(PWD):/src -w /src golang:$(GO_VERSION) \ - bash -c "export GO111MODULE=on; export GOPROXY=https://proxy.golang.org; \ + bash -c "export GOEXPERIMENT=loopvar; export GOTOOLCHAIN=local; \ + export GO111MODULE=on; export GOPROXY=https://proxy.golang.org; \ go install mvdan.cc/gofumpt@$(GOFUMPT_VERSION) && \ gofumpt -w ." @@ -150,7 +158,7 @@ unit-tests-race: ## Performs unit tests with race detection enabled. .PHONY: coverage coverage: ## Upload coverage data to codecov.io. - bash -c "bash <(curl -s https://codecov.io/bash) -f $(ARTIFACTS)/coverage.txt -X fix" + bash -c "bash <(curl -s https://codecov.io/bash) -f $(ARTIFACTS)/coverage-unit-tests.txt -X fix" .PHONY: lint-markdown lint-markdown: ## Runs markdownlint. diff --git a/channel/recv.go b/channel/recv.go index 190e9de..b7a9d2a 100644 --- a/channel/recv.go +++ b/channel/recv.go @@ -9,6 +9,8 @@ import "context" // RecvWithContext tries to receive a value from a channel which is aborted if the context is canceled. // // Function returns true if the value was received, false if the context was canceled or the channel was closed. +// +// Deprecated: use plain old select instead. func RecvWithContext[T any](ctx context.Context, ch <-chan T) (T, bool) { select { case <-ctx.Done(): diff --git a/containers/map_test.go b/containers/map_test.go index 226defd..45c2d75 100644 --- a/containers/map_test.go +++ b/containers/map_test.go @@ -7,12 +7,12 @@ package containers_test import ( "fmt" "math/rand" + "sync" "testing" "github.com/stretchr/testify/require" "github.com/siderolabs/gen/containers" - "github.com/siderolabs/gen/xsync" ) func TestConcurrentMap(t *testing.T) { @@ -191,31 +191,31 @@ func Example_benchConcurrentMap() { benchResult := testing.Benchmark(func(b *testing.B) { b.ReportAllocs() - var m containers.ConcurrentMap[int, *xsync.Once[int]] + var m containers.ConcurrentMap[int, func() int] for i := 0; i < b.N; i++ { variable := 0 - res, _ := m.GetOrCall(10, func() *xsync.Once[int] { - return &xsync.Once[int]{} - }) - - sink = res.Do(func() int { - variable++ + res, _ := m.GetOrCall(10, func() func() int { + return sync.OnceValue(func() int { + variable++ - return variable + return variable + }) }) + + sink = res() } }) - if benchResult.AllocsPerOp() > 0 { - fmt.Println("this benchmark should not allocate memory") + if allocsPerOp := benchResult.AllocsPerOp(); allocsPerOp > 1 { + fmt.Printf("this benchmark should not make more than one allocation, but it makes %d allocations per operation", allocsPerOp) } fmt.Println("ok") + fmt.Println(sink) // Output: // ok - - _ = sink + // 1 } diff --git a/ensure/ensure.go b/ensure/ensure.go new file mode 100644 index 0000000..514d190 --- /dev/null +++ b/ensure/ensure.go @@ -0,0 +1,31 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +// Package ensure provides a set of functions that panic if the error is not nil. +package ensure + +// NoError panics if the error is not nil. +func NoError(err error) { + if err != nil { + panic(err) + } +} + +// Value return value or panics if the error is not nil. +func Value[V any](v V, err error) V { + if err != nil { + panic(err) + } + + return v +} + +// Values returns values or panics if the error is not nil. +func Values[V, V2 any](v V, v2 V2, err error) (V, V2) { + if err != nil { + panic(err) + } + + return v, v2 +} diff --git a/ensure/ensure_test.go b/ensure/ensure_test.go new file mode 100644 index 0000000..c07dd67 --- /dev/null +++ b/ensure/ensure_test.go @@ -0,0 +1,37 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package ensure_test + +import ( + "errors" + "net" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/siderolabs/gen/ensure" +) + +func TestNoError(t *testing.T) { + require.Panics(t, func() { ensure.NoError(errors.New("test error")) }) + require.NotPanics(t, func() { ensure.NoError(nil) }) +} + +func TestValue(t *testing.T) { + require.Panics(t, func() { ensure.Value(net.ParseMAC("--02-00-5e-10-00-00-00-01")) }) + require.NotPanics(t, func() { + require.Equal(t, "02:00:5e:10:00:00:00:01", ensure.Value(net.ParseMAC("02-00-5e-10-00-00-00-01")).String()) + }) +} + +func TestValues(t *testing.T) { + require.Panics(t, func() { ensure.Values(net.ParseCIDR("192.-0.2.1--/24")) }) + require.NotPanics(t, func() { + i, n := ensure.Values(net.ParseCIDR("192.0.2.1/24")) + + require.Equal(t, "192.0.2.1", i.String()) + require.Equal(t, "192.0.2.0/24", n.String()) + }) +} diff --git a/go.mod b/go.mod index dcc87a2..469fbd3 100644 --- a/go.mod +++ b/go.mod @@ -1,8 +1,8 @@ module github.com/siderolabs/gen -go 1.19 +go 1.21.0 // Starting with Go 1.21 you have to provide the third digit too. -require github.com/stretchr/testify v1.8.1 +require github.com/stretchr/testify v1.8.4 require ( github.com/davecgh/go-spew v1.1.1 // indirect diff --git a/go.sum b/go.sum index 2ec90f7..fa4b6e6 100644 --- a/go.sum +++ b/go.sum @@ -1,17 +1,10 @@ -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/slices/slices.go b/slices/slices.go index 5879162..c0cfcc3 100644 --- a/slices/slices.go +++ b/slices/slices.go @@ -5,6 +5,12 @@ // Package slices contains a utility functions to work with slices. package slices +import ( + "slices" + + "github.com/siderolabs/gen/xslices" +) + // NOTE(DmitriyMV): I tried to implement this generic functions to be as performant as possible. // However, I couldn't find a way to do it, since Go (1.18 at the time of writing) cannot inline closures if (generic) // function, which accepts the closure, was not inlined itself. @@ -12,186 +18,76 @@ package slices // Somewhat relevant issue: https://github.com/golang/go/issues/41988 // Map applies the function fn to each element of the slice and returns a new slice with the results. -func Map[T, R any](slc []T, fn func(T) R) []R { - if len(slc) == 0 { - return nil - } - - r := make([]R, 0, len(slc)) - - for _, v := range slc { - r = append(r, fn(v)) - } - - return r -} +// +// Deprecated: Use [xslices.Map] instead. +func Map[T, R any](slc []T, fn func(T) R) []R { return xslices.Map(slc, fn) } // FlatMap applies the function fn to each element of the slice and returns a new slice with the results. // It flattens the result of fn into the result slice. -func FlatMap[T, R any](slc []T, fn func(T) []R) []R { - if len(slc) == 0 { - return nil - } - - r := make([]R, 0, len(slc)) - - for _, v := range slc { - r = append(r, fn(v)...) - } - - return r -} +// +// Deprecated: Use [xslices.FlatMap] instead. +func FlatMap[T, R any](slc []T, fn func(T) []R) []R { return xslices.FlatMap(slc, fn) } // Filter returns a slice containing all the elements of s that satisfy fn. -func Filter[S ~[]T, T any](slc S, fn func(T) bool) S { - // NOTE(DmitriyMV): We use type parameter S here to return exactly the same type as the input slice. - if len(slc) == 0 { - return nil - } - - r := make(S, 0, len(slc)) - - for _, v := range slc { - if fn(v) { - r = append(r, v) - } - } - - // No reason to return empty slice if we filtered everything out. - if len(r) == 0 { - return nil - } - - return r[:len(r):len(r)] -} +// +// Deprecated: Use [xslices.Filter] instead. +func Filter[S ~[]T, T any](slc S, fn func(T) bool) S { return xslices.Filter(slc, fn) } // FilterInPlace filters the slice in place. -func FilterInPlace[S ~[]V, V any](slc S, fn func(V) bool) S { - // NOTE(DmitriyMV): We use type parameter S here to return exactly the same type as the input slice. - if len(slc) == 0 { - // We return original empty slice instead of a nil slice unlike Filter function. - return slc - } - - r := slc[:0] - - for _, v := range slc { - if fn(v) { - r = append(r, v) - } - } - - // We return original slice even if we filtered everything out unlike Filter function. - return r -} +// +// Deprecated: Use [xslices.FilterInPlace] instead. +func FilterInPlace[S ~[]V, V any](slc S, fn func(V) bool) S { return xslices.FilterInPlace(slc, fn) } // ToMap converts a slice to a map. +// +// Deprecated: Use [xslices.ToMap] instead. func ToMap[T any, K comparable, V any](slc []T, fn func(T) (K, V)) map[K]V { - if len(slc) == 0 { - return nil - } - - r := make(map[K]V, len(slc)) - - for _, v := range slc { - key, val := fn(v) - r[key] = val - } - - return r + return xslices.ToMap(slc, fn) } // ToSet converts a slice to a set. -func ToSet[T comparable](slc []T) map[T]struct{} { - if len(slc) == 0 { - return nil - } - - r := make(map[T]struct{}, len(slc)) - - for _, v := range slc { - r[v] = struct{}{} - } - - return r -} +// +// Deprecated: Use [xslices.ToSet] instead. +func ToSet[T comparable](slc []T) map[T]struct{} { return xslices.ToSet(slc) } // ToSetFunc converts a slice to a set using the function fn. +// +// Deprecated: Use [xslices.ToSetFunc] instead. func ToSetFunc[T any, K comparable](slc []T, fn func(T) K) map[K]struct{} { - if len(slc) == 0 { - return nil - } - - r := make(map[K]struct{}, len(slc)) - - for _, v := range slc { - r[fn(v)] = struct{}{} - } - - return r + return xslices.ToSetFunc(slc, fn) } // IndexFunc returns the first index satisfying fn(slc[i]), // or -1 if none do. -func IndexFunc[T any](slc []T, fn func(T) bool) int { - for i := range slc { - if fn(slc[i]) { - return i - } - } - - return -1 -} +// +// Deprecated: Use [slices.IndexFunc] instead. +func IndexFunc[T any](slc []T, fn func(T) bool) int { return slices.IndexFunc(slc, fn) } // Contains reports whether v is present in s. -func Contains[T any](s []T, fn func(T) bool) bool { - return IndexFunc(s, fn) >= 0 -} +// +// Deprecated: Use [slices.ContainsFunc] instead. +func Contains[T any](s []T, fn func(T) bool) bool { return slices.ContainsFunc(s, fn) } // Copy copies first n elements. If n is greater than the length of the slice, it will copy the whole slice. -func Copy[S ~[]V, V any](s S, n int) S { - if s == nil { - return nil - } - - if n > len(s) { - n = len(s) - } - - result := make([]V, n) - copy(result, s) - - return result -} +// +// Deprecated: Use [xslices.CopyN] instead. +func Copy[S ~[]V, V any](s S, n int) S { return xslices.CopyN(s, n) } // Clone returns a copy of the slice. // The elements are copied using assignment, so this is a shallow clone. -func Clone[S ~[]E, E any](s S) S { - // Preserve nil in case it matters. - if s == nil { - return nil - } - - return append(S([]E{}), s...) -} +// +// Deprecated: Use [slices.Clone] instead. +func Clone[S ~[]E, E any](s S) S { return slices.Clone(s) } // Clip removes unused capacity from the slice, returning s[:len(s):len(s)]. -func Clip[S ~[]E, E any](s S) S { - return s[:len(s):len(s)] -} +// +// Deprecated: Use [slices.Clip] instead. +func Clip[S ~[]E, E any](s S) S { return slices.Clip(s) } // Grow increases the slice's capacity, if necessary, to guarantee space for // another n elements. After Grow(n), at least n elements can be appended // to the slice without another allocation. If n is negative or too large to // allocate the memory, Grow panics. -func Grow[S ~[]E, E any](s S, n int) S { - if n < 0 { - panic("cannot be negative") - } - - if n -= cap(s) - len(s); n > 0 { - s = append([]E(s)[:cap(s)], make([]E, n)...)[:len(s)] - } - - return s -} +// +// Deprecated: Use [slices.Grow] instead. +func Grow[S ~[]E, E any](s S, n int) S { return slices.Grow(s, n) } diff --git a/xslices/xslices.go b/xslices/xslices.go new file mode 100644 index 0000000..34e01da --- /dev/null +++ b/xslices/xslices.go @@ -0,0 +1,142 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +// Package xslices contains a utility functions to work with slices. +package xslices + +// Map applies the function fn to each element of the slice and returns a new slice with the results. +func Map[T, R any](slc []T, fn func(T) R) []R { + if len(slc) == 0 { + return nil + } + + r := make([]R, 0, len(slc)) + + for _, v := range slc { + r = append(r, fn(v)) + } + + return r +} + +// FlatMap applies the function fn to each element of the slice and returns a new slice with the results. +// It flattens the result of fn into the result slice. +func FlatMap[T, R any](slc []T, fn func(T) []R) []R { + if len(slc) == 0 { + return nil + } + + r := make([]R, 0, len(slc)) + + for _, v := range slc { + r = append(r, fn(v)...) + } + + return r +} + +// Filter returns a slice containing all the elements of s that satisfy fn. +func Filter[S ~[]T, T any](slc S, fn func(T) bool) S { + // NOTE(DmitriyMV): We use type parameter S here to return exactly the same type as the input slice. + if len(slc) == 0 { + return nil + } + + r := make(S, 0, len(slc)) + + for _, v := range slc { + if fn(v) { + r = append(r, v) + } + } + + // No reason to return empty slice if we filtered everything out. + if len(r) == 0 { + return nil + } + + return r[:len(r):len(r)] +} + +// FilterInPlace filters the slice in place. +func FilterInPlace[S ~[]V, V any](slc S, fn func(V) bool) S { + // NOTE(DmitriyMV): We use type parameter S here to return exactly the same type as the input slice. + if len(slc) == 0 { + // We return original empty slice instead of a nil slice unlike Filter function. + return slc + } + + r := slc[:0] + + for _, v := range slc { + if fn(v) { + r = append(r, v) + } + } + + // We return original slice even if we filtered everything out unlike Filter function. + return r +} + +// ToMap converts a slice to a map. +func ToMap[T any, K comparable, V any](slc []T, fn func(T) (K, V)) map[K]V { + if len(slc) == 0 { + return nil + } + + r := make(map[K]V, len(slc)) + + for _, v := range slc { + key, val := fn(v) + r[key] = val + } + + return r +} + +// ToSet converts a slice to a set. +func ToSet[T comparable](slc []T) map[T]struct{} { + if len(slc) == 0 { + return nil + } + + r := make(map[T]struct{}, len(slc)) + + for _, v := range slc { + r[v] = struct{}{} + } + + return r +} + +// ToSetFunc converts a slice to a set using the function fn. +func ToSetFunc[T any, K comparable](slc []T, fn func(T) K) map[K]struct{} { + if len(slc) == 0 { + return nil + } + + r := make(map[K]struct{}, len(slc)) + + for _, v := range slc { + r[fn(v)] = struct{}{} + } + + return r +} + +// CopyN copies first n elements. If n is greater than the length of the slice, it will copy the whole slice. +func CopyN[S ~[]V, V any](s S, n int) S { + if s == nil { + return nil + } + + if n > len(s) { + n = len(s) + } + + result := make([]V, n) + copy(result, s) + + return result +} diff --git a/slices/slices_test.go b/xslices/xslices_test.go similarity index 92% rename from slices/slices_test.go rename to xslices/xslices_test.go index d96fd6f..0741e66 100644 --- a/slices/slices_test.go +++ b/xslices/xslices_test.go @@ -2,14 +2,14 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. -package slices_test +package xslices_test import ( "testing" "github.com/stretchr/testify/assert" - "github.com/siderolabs/gen/slices" + "github.com/siderolabs/gen/xslices" ) func TestFilterInPlace(t *testing.T) { @@ -72,7 +72,7 @@ func TestFilterInPlace(t *testing.T) { t.Run(name, func(t *testing.T) { t.Parallel() - got := slices.FilterInPlace(tt.args.slice, func(i int) bool { + got := xslices.FilterInPlace(tt.args.slice, func(i int) bool { return i%2 == 0 }) assert.Equal(t, tt.want, got) @@ -140,7 +140,7 @@ func TestFilter(t *testing.T) { t.Run(name, func(t *testing.T) { t.Parallel() - got := slices.Filter(tt.args.slice, func(i int) bool { + got := xslices.Filter(tt.args.slice, func(i int) bool { return i%2 == 0 }) assert.Equal(t, tt.want, got) diff --git a/xsync/once.go b/xsync/once.go deleted file mode 100644 index b583198..0000000 --- a/xsync/once.go +++ /dev/null @@ -1,25 +0,0 @@ -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at http://mozilla.org/MPL/2.0/. - -// Package xsync contains the additions to std sync package. -package xsync - -import ( - "sync" -) - -// Once is small wrapper around [sync.Once]. It stores the result inside. -type Once[T any] struct { - val T - once sync.Once -} - -// Do runs the function only once. -func (o *Once[T]) Do(fn func() T) T { - o.once.Do(func() { - o.val = fn() - }) - - return o.val -} diff --git a/xtesting/check/check.go b/xtesting/check/check.go new file mode 100644 index 0000000..dfd1218 --- /dev/null +++ b/xtesting/check/check.go @@ -0,0 +1,72 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +// Package check provides a set of functions that can be used for error checking in table-driven test. +package check + +import ( + "github.com/stretchr/testify/require" + + "github.com/siderolabs/gen/xerrors" + "github.com/siderolabs/gen/xtesting" +) + +// Check is a function that checks for an error or absence of it. +type Check func(t xtesting.T, err error) + +// NoError returns a function that checks that no error happened. +func NoError() Check { + return func(t xtesting.T, err error) { + require.NoError(t, err) + } +} + +// EqualError returns a function that checks for a specific error text. +func EqualError(msg string) Check { + return func(t xtesting.T, err error) { + require.EqualError(t, err, msg) + } +} + +// ErrorContains returns a function that checks that error message contains a specific text. +func ErrorContains(msg string) Check { + return func(t xtesting.T, err error) { + require.ErrorContains(t, err, msg) + } +} + +// ErrorRegexp returns a function that checks that error message matches a specific regexp. +func ErrorRegexp(rx any) Check { + return func(t xtesting.T, err error) { + require.Error(t, err) + require.NotZero(t, rx) + require.Regexp(t, rx, err.Error()) + } +} + +// ErrorAs returns a function that checks that error can be converted to a specific type. +func ErrorAs[T error]() Check { + var target T + + return func(t xtesting.T, err error) { + require.Error(t, err) + require.ErrorAs(t, err, &target) + } +} + +// ErrorIs returns a function that checks that error is equal to a specific error. +func ErrorIs(target error) Check { + return func(t xtesting.T, err error) { + require.Error(t, err) + require.ErrorIs(t, err, target) + } +} + +// ErrorTagIs returns a function that checks that error has a specific tag. +func ErrorTagIs[T xerrors.Tag]() Check { + return func(t xtesting.T, err error) { + require.Error(t, err) + require.True(t, xerrors.TagIs[T](err)) + } +} diff --git a/xtesting/check/check_test.go b/xtesting/check/check_test.go new file mode 100644 index 0000000..c95ea4f --- /dev/null +++ b/xtesting/check/check_test.go @@ -0,0 +1,59 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package check_test + +import ( + "strconv" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/siderolabs/gen/xtesting" + "github.com/siderolabs/gen/xtesting/check" +) + +func TestChecker(t *testing.T) { + //nolint:govet + tests := map[string]struct { + arg string + expected int + check func(t xtesting.T, err error) + }{ + "no error": { + arg: "1", + expected: 1, + check: check.NoError(), + }, + "error": { + arg: "a", + expected: 0, + check: check.EqualError("strconv.Atoi: parsing \"a\": invalid syntax"), + }, + "error contains": { + arg: "a", + expected: 0, + check: check.ErrorContains("invalid syntax"), + }, + "error regexp": { + arg: "a", + expected: 0, + check: check.ErrorRegexp("strconv.*invalid syntax"), + }, + "error as": { + arg: "a", + expected: 0, + check: check.ErrorAs[*strconv.NumError](), + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + atoi, err := strconv.Atoi(test.arg) + test.check(t, err) + + require.Equal(t, test.expected, atoi) + }) + } +} diff --git a/xtesting/must/must.go b/xtesting/must/must.go new file mode 100644 index 0000000..77b2f46 --- /dev/null +++ b/xtesting/must/must.go @@ -0,0 +1,30 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +// Package must provide a set of functions that fail test if the error is not nil. +package must + +import ( + "github.com/stretchr/testify/require" + + "github.com/siderolabs/gen/xtesting" +) + +// Value returns a function that accepts parameter that implements [xtesting.T] and uses [require.NoError] to ensure there was no error. +func Value[V any](v V, err error) func(t xtesting.T) V { + return func(t xtesting.T) V { + require.NoError(t, err) + + return v + } +} + +// Values returns a function that accepts parameter that implements [xtesting.T] and uses [require.NoError] to ensure there was no error. +func Values[V, V2 any](v V, v2 V2, err error) func(t xtesting.T) (V, V2) { + return func(t xtesting.T) (V, V2) { + require.NoError(t, err) + + return v, v2 + } +} diff --git a/xtesting/must/must_test.go b/xtesting/must/must_test.go new file mode 100644 index 0000000..5d917dc --- /dev/null +++ b/xtesting/must/must_test.go @@ -0,0 +1,27 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package must_test + +import ( + "io" + "net" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/siderolabs/gen/xtesting/must" +) + +func TestValue(t *testing.T) { + value := must.Value(io.WriteString(io.Discard, "Hello, World!"))(t) + require.Equal(t, 13, value) +} + +func TestValues(t *testing.T) { + i, n := must.Values(net.ParseCIDR("192.0.2.1/24"))(t) + + require.Equal(t, "192.0.2.1", i.String()) + require.Equal(t, "192.0.2.0/24", n.String()) +} diff --git a/xtesting/testing.go b/xtesting/testing.go new file mode 100644 index 0000000..8f317a9 --- /dev/null +++ b/xtesting/testing.go @@ -0,0 +1,12 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +// Package xtesting provides a T interface wrapper around *testing.T +package xtesting + +// T is an interface wrapper around *testing.T. +type T interface { + Errorf(format string, args ...interface{}) + FailNow() +}