diff --git a/.conform.yaml b/.conform.yaml index 39f1881..97d02ed 100644 --- a/.conform.yaml +++ b/.conform.yaml @@ -1,37 +1,48 @@ # THIS FILE WAS AUTOMATICALLY GENERATED, PLEASE DO NOT EDIT. # -# Generated on 2022-09-21T14:24:31Z by kres 943ad34. +# Generated on 2024-03-11T19:57:58Z by kres latest. ---- policies: -- type: commit - spec: - dco: true - gpg: - required: true - identity: - gitHubOrganization: siderolabs - spellcheck: - locale: US - maximumOfOneCommit: true - header: - length: 89 - imperative: true - case: lower - invalidLastCharacters: . - body: - required: true - conventional: - types: ["chore","docs","perf","refactor","style","test","release"] - scopes: [".*"] -- type: license - spec: - skipPaths: - - .git/ - - testdata/ - includeSuffixes: - - .go - excludeSuffixes: - - .pb.go - - .pb.gw.go - header: "// This Source Code Form is subject to the terms of the Mozilla Public\u000A// License, v. 2.0. If a copy of the MPL was not distributed with this\u000A// file, You can obtain one at http://mozilla.org/MPL/2.0/.\u000A" + - type: commit + spec: + dco: true + gpg: + required: true + identity: + gitHubOrganization: siderolabs + spellcheck: + locale: US + maximumOfOneCommit: true + header: + length: 89 + imperative: true + case: lower + invalidLastCharacters: . + body: + required: true + conventional: + types: + - chore + - docs + - perf + - refactor + - style + - test + - release + scopes: + - .* + - type: license + spec: + root: . + skipPaths: + - .git/ + - testdata/ + includeSuffixes: + - .go + excludeSuffixes: + - .pb.go + - .pb.gw.go + header: | + // 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/. diff --git a/.dockerignore b/.dockerignore index 42c6acf..b2db58a 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,6 +1,6 @@ # THIS FILE WAS AUTOMATICALLY GENERATED, PLEASE DO NOT EDIT. # -# Generated on 2023-08-21T16:35:57Z by kres latest. +# Generated on 2024-03-11T19:57:58Z by kres latest. * !channel @@ -9,7 +9,6 @@ !maps !optional !pair -!slices !value !xerrors !xslices diff --git a/.drone.yml b/.drone.yml deleted file mode 100644 index 11f07e5..0000000 --- a/.drone.yml +++ /dev/null @@ -1,258 +0,0 @@ ---- -# THIS FILE WAS AUTOMATICALLY GENERATED, PLEASE DO NOT EDIT. -# -# Generated on 2023-08-21T14:49:21Z by kres latest. - -kind: pipeline -type: kubernetes -name: default - -platform: - os: linux - arch: amd64 - -steps: -- name: setup-ci - pull: always - image: autonomy/build-container:latest - commands: - - sleep 5 - - git fetch --tags - - install-ci-key - - docker buildx create --driver docker-container --platform linux/amd64 --name local --use unix:///var/outer-run/docker.sock - - docker buildx inspect --bootstrap - environment: - SSH_KEY: - from_secret: ssh_key - volumes: - - name: outer-docker-socket - path: /var/outer-run - - name: docker-socket - path: /var/run - - name: buildx - path: /root/.docker/buildx - - name: ssh - path: /root/.ssh - -- name: base - pull: always - image: autonomy/build-container:latest - commands: - - make base - volumes: - - name: outer-docker-socket - path: /var/outer-run - - name: docker-socket - path: /var/run - - name: buildx - path: /root/.docker/buildx - - name: ssh - path: /root/.ssh - depends_on: - - setup-ci - -- name: unit-tests - pull: always - image: autonomy/build-container:latest - commands: - - make unit-tests - volumes: - - name: outer-docker-socket - path: /var/outer-run - - name: docker-socket - path: /var/run - - name: buildx - path: /root/.docker/buildx - - name: ssh - path: /root/.ssh - depends_on: - - base - -- name: unit-tests-race - pull: always - image: autonomy/build-container:latest - commands: - - make unit-tests-race - volumes: - - name: outer-docker-socket - path: /var/outer-run - - name: docker-socket - path: /var/run - - name: buildx - path: /root/.docker/buildx - - name: ssh - path: /root/.ssh - depends_on: - - base - -- name: coverage - pull: always - image: autonomy/build-container:latest - commands: - - make coverage - environment: - CODECOV_TOKEN: - from_secret: CODECOV_TOKEN - volumes: - - name: outer-docker-socket - path: /var/outer-run - - name: docker-socket - path: /var/run - - name: buildx - path: /root/.docker/buildx - - name: ssh - path: /root/.ssh - depends_on: - - unit-tests - -- name: lint - pull: always - image: autonomy/build-container:latest - commands: - - make lint - volumes: - - name: outer-docker-socket - path: /var/outer-run - - name: docker-socket - path: /var/run - - name: buildx - path: /root/.docker/buildx - - name: ssh - path: /root/.ssh - depends_on: - - base - -- name: release-notes - pull: always - image: autonomy/build-container:latest - commands: - - make release-notes - volumes: - - name: outer-docker-socket - path: /var/outer-run - - name: docker-socket - path: /var/run - - name: buildx - path: /root/.docker/buildx - - name: ssh - path: /root/.ssh - when: - event: - - tag - depends_on: - - unit-tests - - coverage - - lint - -- name: release - pull: always - image: plugins/github-release - settings: - api_key: - from_secret: github_token - checksum: - - sha256 - - sha512 - draft: true - files: - - _out/* - note: _out/RELEASE_NOTES.md - volumes: - - name: outer-docker-socket - path: /var/outer-run - - name: docker-socket - path: /var/run - - name: buildx - path: /root/.docker/buildx - - name: ssh - path: /root/.ssh - when: - event: - - tag - depends_on: - - release-notes - -services: -- name: docker - image: docker:24.0-dind - entrypoint: - - dockerd - commands: - - --dns=8.8.8.8 - - --dns=8.8.4.4 - - --mtu=1500 - - --log-level=error - privileged: true - volumes: - - name: outer-docker-socket - path: /var/outer-run - - name: docker-socket - path: /var/run - - name: buildx - path: /root/.docker/buildx - - name: ssh - path: /root/.ssh - -volumes: -- name: outer-docker-socket - host: - path: /var/ci-docker -- name: docker-socket - temp: - medium: memory -- name: buildx - temp: - medium: memory -- name: ssh - temp: - medium: memory - -trigger: - branch: - exclude: - - renovate/* - - dependabot/* - event: - exclude: - - promote - - cron - ---- -kind: pipeline -type: kubernetes -name: notify - -platform: - os: linux - arch: amd64 - -clone: - disable: true - -steps: -- name: slack - image: plugins/slack - settings: - channel: proj-talos-maintainers - link_names: true - template: "{{#if build.pull }}\n*{{#success build.status}}✓ Success{{else}}✕ Fail{{/success}}*: {{ repo.owner }}/{{ repo.name }} - \n{{else}}\n*{{#success build.status}}✓ Success{{else}}✕ Fail{{/success}}: {{ repo.owner }}/{{ repo.name }} - Build #{{ build.number }}* (type: `{{ build.event }}`)\n{{/if}}\nCommit: \nBranch: \nAuthor: {{ build.author }}\n<{{ build.link }}|Visit build page>" - webhook: - from_secret: slack_webhook - when: - status: - - success - - failure - -trigger: - branch: - exclude: - - renovate/* - - dependabot/* - status: - - success - - failure - -depends_on: -- default - -... diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 9b0c427..2dc54b6 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -1,47 +1,55 @@ # THIS FILE WAS AUTOMATICALLY GENERATED, PLEASE DO NOT EDIT. # -# Generated on 2023-09-17T20:55:00Z by kres latest. +# Generated on 2024-03-11T19:57:58Z by kres latest. name: default +concurrency: + group: ${{ github.head_ref || github.run_id }} + cancel-in-progress: true "on": push: branches: - main + - release-* tags: - v* - pull_request: {} -env: - CI_ARGS: --cache-from=type=registry,ref=registry.dev.siderolabs.io/${GITHUB_REPOSITORY}:buildcache --cache-to=type=registry,ref=registry.dev.siderolabs.io/${GITHUB_REPOSITORY}:buildcache,mode=max + pull_request: + branches: + - main + - release-* jobs: default: permissions: + actions: read contents: write + issues: read packages: write + pull-requests: read runs-on: - self-hosted - - X64 - if: ${{ !startsWith(github.head_ref, 'renovate/') || !startsWith(github.head_ref, 'renovate/') }} + - generic + if: (!startsWith(github.head_ref, 'renovate/') && !startsWith(github.head_ref, 'dependabot/')) + services: + buildkitd: + image: moby/buildkit:v0.12.5 + options: --privileged + ports: + - 1234:1234 + volumes: + - /var/lib/buildkit/${{ github.repository }}:/var/lib/buildkit + - /usr/etc/buildkit/buildkitd.toml:/etc/buildkit/buildkitd.toml steps: - name: checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Unshallow run: | git fetch --prune --unshallow - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 + uses: docker/setup-buildx-action@v3 with: - config-inline: | - [worker.oci] - gc = true - gckeepstorage = 100000 # 100 GiB - - [[worker.oci.gcpolicy]] - keepBytes = 32212254720 # 30 GiB - keepDuration = 604800 - filters = [ "type==source.local", "type==exec.cachemount", "type==source.git.checkout"] - [[worker.oci.gcpolicy]] - all = true - keepBytes = 107374182400 # 100 GiB + driver: remote + endpoint: tcp://127.0.0.1:1234 + timeout-minutes: 1 - name: base run: | make base @@ -57,21 +65,13 @@ jobs: - name: lint run: | make lint - - name: Generate Checksums - if: startsWith(github.ref, 'refs/tags/') - run: | - sha256sum _out/* > _out/sha256sum.txt - sha512sum _out/* > _out/sha512sum.txt - name: release-notes if: startsWith(github.ref, 'refs/tags/') run: | make release-notes - name: Release if: startsWith(github.ref, 'refs/tags/') - uses: crazy-max/ghaction-github-release@v1 + uses: crazy-max/ghaction-github-release@v2 with: body_path: _out/RELEASE_NOTES.md draft: "true" - files: |- - _out/* - _out/sha*.txt diff --git a/.github/workflows/slack-notify.yaml b/.github/workflows/slack-notify.yaml new file mode 100644 index 0000000..c0dca32 --- /dev/null +++ b/.github/workflows/slack-notify.yaml @@ -0,0 +1,92 @@ +# THIS FILE WAS AUTOMATICALLY GENERATED, PLEASE DO NOT EDIT. +# +# Generated on 2024-03-11T19:57:58Z by kres latest. + +name: slack-notify +"on": + workflow_run: + workflows: + - default + types: + - completed +jobs: + slack-notify: + runs-on: + - self-hosted + - generic + if: github.event.workflow_run.conclusion != 'skipped' + steps: + - name: Get PR number + id: get-pr-number + if: github.event.workflow_run.event == 'pull_request' + env: + GH_TOKEN: ${{ github.token }} + run: | + echo pull_request_number=$(gh pr view -R ${{ github.repository }} ${{ github.event.workflow_run.head_repository.owner.login }}:${{ github.event.workflow_run.head_branch }} --json number --jq .number) >> $GITHUB_OUTPUT + - name: Slack Notify + uses: slackapi/slack-github-action@v1 + with: + channel-id: proj-talos-maintainers + payload: | + { + "attachments": [ + { + "color": "${{ github.event.workflow_run.conclusion == 'success' && '#2EB886' || github.event.workflow_run.conclusion == 'failure' && '#A30002' || '#FFCC00' }}", + "fallback": "test", + "blocks": [ + { + "type": "section", + "fields": [ + { + "type": "mrkdwn", + "text": "${{ github.event.workflow_run.event == 'pull_request' && format('*Pull Request:* {0} (`{1}`)\n<{2}/pull/{3}|{4}>', github.repository, github.ref_name, github.event.repository.html_url, steps.get-pr-number.outputs.pull_request_number, github.event.workflow_run.display_title) || format('*Build:* {0} (`{1}`)\n<{2}/commit/{3}|{4}>', github.repository, github.ref_name, github.event.repository.html_url, github.sha, github.event.workflow_run.display_title) }}" + }, + { + "type": "mrkdwn", + "text": "*Status:*\n`${{ github.event.workflow_run.conclusion }}`" + } + ] + }, + { + "type": "section", + "fields": [ + { + "type": "mrkdwn", + "text": "*Author:*\n`${{ github.actor }}`" + }, + { + "type": "mrkdwn", + "text": "*Event:*\n`${{ github.event.workflow_run.event }}`" + } + ] + }, + { + "type": "divider" + }, + { + "type": "actions", + "elements": [ + { + "type": "button", + "text": { + "type": "plain_text", + "text": "Logs" + }, + "url": "${{ github.event.workflow_run.html_url }}" + }, + { + "type": "button", + "text": { + "type": "plain_text", + "text": "Commit" + }, + "url": "${{ github.event.repository.html_url }}/commit/${{ github.sha }}" + } + ] + } + ] + } + ] + } + env: + SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }} diff --git a/.golangci.yml b/.golangci.yml index 70878c8..41af67e 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,6 +1,6 @@ # THIS FILE WAS AUTOMATICALLY GENERATED, PLEASE DO NOT EDIT. # -# Generated on 2023-08-21T14:49:21Z by kres latest. +# Generated on 2024-03-11T19:57:58Z by kres latest. # options for analysis running run: @@ -74,8 +74,6 @@ linters-settings: govet: check-shadowing: true enable-all: true - disable: - - loopclosure lll: line-length: 200 tab-width: 4 @@ -150,6 +148,10 @@ linters: - wrapcheck - depguard # Disabled because starting with golangci-lint 1.53.0 it doesn't allow denylist alone anymore - tagalign + - inamedparam + - testifylint # complains about our assert recorder and has a number of false positives for assert.Greater(t, thing, 1) + - protogetter # complains about us using Value field on typed spec, instead of GetValue which has a different signature + - perfsprint # complains about us using fmt.Sprintf in non-performance critical code, updating just kres took too long # 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 4c87ed8..1474d75 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,8 +1,8 @@ -# syntax = docker/dockerfile-upstream:1.6.0-labs +# syntax = docker/dockerfile-upstream:1.7.0-labs # THIS FILE WAS AUTOMATICALLY GENERATED, PLEASE DO NOT EDIT. # -# Generated on 2023-08-21T16:35:57Z by kres latest. +# Generated on 2024-03-11T19:57:58Z by kres latest. ARG TOOLCHAIN @@ -10,9 +10,9 @@ ARG TOOLCHAIN FROM scratch AS generate # runs markdownlint -FROM docker.io/node:20.5.1-alpine3.18 AS lint-markdown +FROM docker.io/node:21.6.2-alpine3.19 AS lint-markdown WORKDIR /src -RUN npm i -g markdownlint-cli@0.35.0 +RUN npm i -g markdownlint-cli@0.39.0 RUN npm i sentences-per-line@0.2.1 COPY .markdownlint.json . COPY ./README.md ./README.md @@ -61,7 +61,6 @@ COPY ./ensure ./ensure COPY ./maps ./maps COPY ./optional ./optional COPY ./pair ./pair -COPY ./slices ./slices COPY ./value ./value COPY ./xerrors ./xerrors COPY ./xslices ./xslices diff --git a/Makefile b/Makefile index 5cb9d53..a60d7d8 100644 --- a/Makefile +++ b/Makefile @@ -1,11 +1,11 @@ # THIS FILE WAS AUTOMATICALLY GENERATED, PLEASE DO NOT EDIT. # -# Generated on 2023-09-17T20:55:00Z by kres latest. +# Generated on 2024-03-11T19:51:28Z by kres latest. # common variables SHA := $(shell git describe --match=none --always --abbrev=8 --dirty) -TAG := $(shell git describe --tag --always --dirty) +TAG := $(shell git describe --tag --always --dirty --match v[0-9]\*) 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 @@ -14,20 +14,19 @@ WITH_RACE ?= false REGISTRY ?= ghcr.io USERNAME ?= siderolabs REGISTRY_AND_USERNAME ?= $(REGISTRY)/$(USERNAME) -PROTOBUF_GO_VERSION ?= 1.31.0 +PROTOBUF_GO_VERSION ?= 1.33.0 GRPC_GO_VERSION ?= 1.3.0 -GRPC_GATEWAY_VERSION ?= 2.17.1 -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 +GRPC_GATEWAY_VERSION ?= 2.19.1 +VTPROTOBUF_VERSION ?= 0.6.0 +DEEPCOPY_VERSION ?= v0.5.6 +GOLANGCILINT_VERSION ?= v1.56.2 +GOFUMPT_VERSION ?= v0.6.0 +GO_VERSION ?= 1.22.1 +GOIMPORTS_VERSION ?= v0.19.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 @@ -65,7 +64,7 @@ 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.21-alpine +TOOLCHAIN ?= docker.io/golang:1.22-alpine # help menu @@ -88,6 +87,23 @@ To create a builder instance, run: docker buildx create --name local --use +If running builds that needs to be cached aggresively create a builder instance with the following: + + docker buildx create --name local --use --config=config.toml + +config.toml contents: + +[worker.oci] + gc = true + gckeepstorage = 50000 + + [[worker.oci.gcpolicy]] + keepBytes = 10737418240 + keepDuration = 604800 + filters = [ "type==source.local", "type==exec.cachemount", "type==source.git.checkout"] + [[worker.oci.gcpolicy]] + all = true + keepBytes = 53687091200 If you already have a compatible builder instance, you may use that instead. @@ -109,7 +125,7 @@ endif ifneq (, $(filter $(WITH_DEBUG), t true TRUE y yes 1)) GO_BUILDFLAGS += -tags sidero.debug else -GO_LDFLAGS += -s -w +GO_LDFLAGS += -s endif all: unit-tests lint @@ -133,7 +149,7 @@ 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 GOEXPERIMENT=loopvar; export GOTOOLCHAIN=local; \ + bash -c "export GOTOOLCHAIN=local; \ export GO111MODULE=on; export GOPROXY=https://proxy.golang.org; \ go install mvdan.cc/gofumpt@$(GOFUMPT_VERSION) && \ gofumpt -w ." @@ -170,7 +186,7 @@ lint: lint-golangci-lint lint-gofumpt lint-govulncheck lint-goimports lint-markd .PHONY: rekres rekres: @docker pull $(KRES_IMAGE) - @docker run --rm -v $(PWD):/src -w /src -e GITHUB_TOKEN $(KRES_IMAGE) + @docker run --rm --net=host --user $(shell id -u):$(shell id -g) -v $(PWD):/src -w /src -e GITHUB_TOKEN $(KRES_IMAGE) .PHONY: help help: ## This help menu. diff --git a/channel/recv.go b/channel/recv.go index b7a9d2a..8988788 100644 --- a/channel/recv.go +++ b/channel/recv.go @@ -4,28 +4,6 @@ package channel -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(): - var zero T - - return zero, false - case val, ok := <-ch: - if !ok { - return val, false - } - - return val, true - } -} - // RecvState is the state of a channel after receiving a value. type RecvState int diff --git a/channel/recv_test.go b/channel/recv_test.go index 2760ef3..fa39209 100644 --- a/channel/recv_test.go +++ b/channel/recv_test.go @@ -5,7 +5,6 @@ package channel_test import ( - "context" "testing" "github.com/stretchr/testify/assert" @@ -13,44 +12,6 @@ import ( "github.com/siderolabs/gen/channel" ) -func TestRecvWithContext(t *testing.T) { - t.Parallel() - - ch := make(chan int, 1) - ch <- 42 - - ctx, cancel := context.WithCancel(context.Background()) - t.Cleanup(cancel) - - val, ok := channel.RecvWithContext(ctx, ch) - assert.Equal(t, 42, val) - assert.True(t, ok) - - cancel() - - val, ok = channel.RecvWithContext(ctx, ch) - assert.Zero(t, val) - assert.False(t, ok) -} - -func TestRecvWithContextCloseCh(t *testing.T) { - t.Parallel() - - ch := make(chan int, 1) - ch <- 42 - - ctx := context.Background() - - val, ok := channel.RecvWithContext(ctx, ch) - assert.Equal(t, 42, val) - assert.True(t, ok) - - close(ch) - val, ok = channel.RecvWithContext(ctx, ch) - assert.Zero(t, val) - assert.False(t, ok) -} - func TestTryRecv(t *testing.T) { t.Parallel() diff --git a/containers/lazymap_test.go b/containers/lazymap_test.go index 78b2364..1180b7d 100644 --- a/containers/lazymap_test.go +++ b/containers/lazymap_test.go @@ -66,7 +66,9 @@ func TestLazyBiMap(t *testing.T) { create, err := m.GetOrCreate(11) require.NoError(t, err) require.Equal(t, 100, create) + val, ok := m.Get(1) + require.False(t, ok) require.Equal(t, 0, val) }) @@ -111,8 +113,7 @@ func TestLazyBiMap(t *testing.T) { }) t.Run("should iterate over entries", func(t *testing.T) { - var keys []int - var values []int + var keys, values []int m.ForEach(func(k int, v int) { keys = append(keys, k) @@ -141,12 +142,11 @@ func TestLazyBiMap(t *testing.T) { }) t.Run("should filter entries", func(t *testing.T) { - m.FilterInPlace(func(k int, v int) bool { + m.FilterInPlace(func(k int, _ int) bool { return k == 1 }) - var keys []int - var values []int + var keys, values []int m.ForEach(func(k int, v int) { keys = append(keys, k) @@ -177,7 +177,7 @@ func TestLazyMap(t *testing.T) { }, } - t.Run("should delete nothing if there is nothing to delete", func(t *testing.T) { + t.Run("should delete nothing if there is nothing to delete", func(*testing.T) { m.Remove(0) }) @@ -226,8 +226,7 @@ func TestLazyMap(t *testing.T) { }) t.Run("should iterate over entries", func(t *testing.T) { - var keys []int - var values []int + var keys, values []int m.ForEach(func(k int, v int) { keys = append(keys, k) @@ -256,12 +255,9 @@ func TestLazyMap(t *testing.T) { }) t.Run("should filter entries", func(t *testing.T) { - m.FilterInPlace(func(k int, v int) bool { - return k == 4 - }) + m.FilterInPlace(func(k int, _ int) bool { return k == 4 }) - var keys []int - var values []int + var keys, values []int m.ForEach(func(k int, v int) { keys = append(keys, k) diff --git a/containers/map_test.go b/containers/map_test.go index 67da150..3e67885 100644 --- a/containers/map_test.go +++ b/containers/map_test.go @@ -78,9 +78,9 @@ func TestConcurrentMap(t *testing.T) { m.Set(3, 3) var count int - m.ForEach(func(key int, value int) { - count++ - }) + + m.ForEach(func(int, int) { count++ }) + require.Equal(t, 3, count) }) diff --git a/containers/syncmap.go b/containers/syncmap.go new file mode 100644 index 0000000..005e2b6 --- /dev/null +++ b/containers/syncmap.go @@ -0,0 +1,89 @@ +// 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 containers + +import "sync" + +// SyncMap is a wrapper around sync.Map that provides type safety. +type SyncMap[K comparable, V any] struct { + m sync.Map +} + +// Load returns the value stored in the map for a key, or zero value if no value is +// present. +func (m *SyncMap[K, V]) Load(key K) (value V, ok bool) { + v, ok := m.m.Load(key) + + return castOrZero[V](v), ok +} + +// Store sets the value for a key. +func (m *SyncMap[K, V]) Store(key K, value V) { + m.m.Store(key, value) +} + +// Delete deletes the value for a key. +func (m *SyncMap[K, V]) Delete(key K) { + m.m.Delete(key) +} + +// Range calls f sequentially for each key and value present in the map. If f +// returns false, range stops the iteration. +func (m *SyncMap[K, V]) Range(f func(key K, value V) bool) { + m.m.Range(func(key, value any) bool { + return f(castOrZero[K](key), castOrZero[V](value)) + }) +} + +// Swap swaps the value for a key and returns the previous value if any. +// The loaded result reports whether the key was present. +func (m *SyncMap[K, V]) Swap(key K, value V) (previous V, loaded bool) { + val, loaded := m.m.Swap(key, value) + + return castOrZero[V](val), loaded +} + +// CompareAndSwap swaps the old and new values for key +// if the value stored in the map is equal to old. +// The old value must be of a comparable type. +func (m *SyncMap[K, V]) CompareAndSwap(key K, old, newValue V) bool { + return m.m.CompareAndSwap(key, old, newValue) +} + +// CompareAndDelete deletes the entry for key if its value is equal to old. +// The old value must be of a comparable type. +// +// If there is no current value for key in the map, CompareAndDelete +// returns false. +func (m *SyncMap[K, V]) CompareAndDelete(key K, old V) (deleted bool) { + return m.m.CompareAndDelete(key, old) +} + +// LoadOrStore returns the existing value for the key if present. +// Otherwise, it stores and returns the given value. +// The loaded result is true if the value was loaded, false if stored. +func (m *SyncMap[K, V]) LoadOrStore(key K, value V) (actual V, loaded bool) { + val, loaded := m.m.LoadOrStore(key, value) + + return castOrZero[V](val), loaded +} + +// LoadAndDelete deletes the value for a key, returning the previous value if any. +// The loaded result reports whether the key was present. +func (m *SyncMap[K, V]) LoadAndDelete(key K) (value V, loaded bool) { + val, loaded := m.m.LoadAndDelete(key) + + return castOrZero[V](val), loaded +} + +func castOrZero[T any](val any) T { + if val == nil { + var zero T + + return zero + } + + return val.(T) //nolint:forcetypeassert +} diff --git a/containers/syncmap_test.go b/containers/syncmap_test.go new file mode 100644 index 0000000..3f2d05e --- /dev/null +++ b/containers/syncmap_test.go @@ -0,0 +1,121 @@ +// 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 containers_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/siderolabs/gen/containers" +) + +func TestSyncMapInterface(t *testing.T) { + var m containers.SyncMap[any, any] + + m.Store("foo", nil) + m.Store(nil, 10) + + val, ok := m.Load("foo") + assert.True(t, ok) + assert.Nil(t, val) + + val, ok = m.Load(nil) + assert.True(t, ok) + assert.Equal(t, 10, val) + + m.Range(func(key any, value any) bool { + switch { + case key == nil && value == 10: + case key == "foo" && value == nil: + default: + t.Fatalf("unexpected key/value: %v/%v", key, value) + } + + return true + }) + + actual, loaded := m.LoadOrStore(nil, "some-text") + assert.True(t, loaded) + assert.Equal(t, 10, actual) + + actual, loaded = m.LoadAndDelete(nil) + assert.True(t, loaded) + assert.Equal(t, 10, actual) + + actual, loaded = m.LoadAndDelete(nil) + assert.False(t, loaded) + assert.Zero(t, actual) + + actual, loaded = m.LoadOrStore("333", "some-text") + assert.False(t, loaded) + assert.Equal(t, "some-text", actual) + + previous, loaded := m.Swap("333", "new-text") + assert.True(t, loaded) + assert.Equal(t, "some-text", previous) + + previous, loaded = m.Swap("777", "new-text") + assert.False(t, loaded) + assert.Zero(t, previous) +} + +func TestSyncMapPtr(t *testing.T) { + var m containers.SyncMap[*string, *int] + + fooPtr := ptrTo("foo") + ptrToTen := ptrTo(10) + + m.Store(fooPtr, nil) + m.Store(nil, ptrToTen) + + val, ok := m.Load(fooPtr) + assert.True(t, ok) + assert.Nil(t, val) + val, ok = m.Load(nil) + assert.True(t, ok) + assert.Equal(t, ptrToTen, val) + + m.Range(func(key *string, value *int) bool { + switch { + case key == nil && value == ptrToTen: + case key == fooPtr && value == nil: + default: + t.Fatalf("unexpected key/value: %v/%v", key, value) + } + + return true + }) + + ptrToEleven := ptrTo(11) + + actual, loaded := m.LoadOrStore(nil, ptrToEleven) + assert.True(t, loaded) + assert.Equal(t, ptrToTen, actual) + + actual, loaded = m.LoadAndDelete(nil) + assert.True(t, loaded) + assert.Equal(t, ptrToTen, actual) + + actual, loaded = m.LoadAndDelete(nil) + assert.False(t, loaded) + assert.Zero(t, actual) + + m.Delete(fooPtr) + + actual, loaded = m.LoadOrStore(fooPtr, ptrToEleven) + assert.False(t, loaded) + assert.Equal(t, ptrToEleven, actual) + + previous, loaded := m.Swap(fooPtr, ptrToTen) + assert.True(t, loaded) + assert.Equal(t, ptrToEleven, previous) + + previous, loaded = m.Swap(nil, ptrToEleven) + assert.False(t, loaded) + assert.Zero(t, previous) +} + +func ptrTo[V any](v V) *V { return &v } diff --git a/go.mod b/go.mod index 469fbd3..11d642c 100644 --- a/go.mod +++ b/go.mod @@ -1,8 +1,8 @@ module github.com/siderolabs/gen -go 1.21.0 // Starting with Go 1.21 you have to provide the third digit too. +go 1.22.0 // Starting with Go 1.21 you have to provide the third digit too. -require github.com/stretchr/testify v1.8.4 +require github.com/stretchr/testify v1.9.0 require ( github.com/davecgh/go-spew v1.1.1 // indirect diff --git a/go.sum b/go.sum index fa4b6e6..60ce688 100644 --- a/go.sum +++ b/go.sum @@ -2,8 +2,8 @@ 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/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/maps/maps.go b/maps/maps.go index b6973cc..6804718 100644 --- a/maps/maps.go +++ b/maps/maps.go @@ -5,8 +5,6 @@ // Package maps contains the generic functions for maps. package maps -import "maps" - // NOTE(DmitriyMV): I tried to implement this generic functions to be as performant as possible. // However, I couldn't find any 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. @@ -182,22 +180,3 @@ func FilterInPlace[M ~map[K]V, K comparable, V any](m M, fn func(K, V) bool) M { // We return original map even if we filtered everything out unlike Filter function. return m } - -// Clear removes all entries from m, leaving it empty. -// -// Deprecated: Use built-in clear function instead. -func Clear[M ~map[K]V, K comparable, V any](m M) { clear(m) } - -// Clone returns a copy of m. This is a shallow clone: -// the new keys and values are set using ordinary assignment. -// -// Deprecated: Use [maps.Clone] function instead. -func Clone[M ~map[K]V, K comparable, V any](m M) M { return maps.Clone(m) } - -// Copy copies all key/value pairs in src adding them to dst. -// When a key in src is already present in dst, -// the value in dst will be overwritten by the value associated -// with the key in src. -// -// Deprecated: Use [maps.Copy] instead. -func Copy[M1 ~map[K]V, M2 ~map[K]V, K comparable, V any](dst M1, src M2) { maps.Copy(dst, src) } diff --git a/maps/maps_test.go b/maps/maps_test.go index 34efeb3..03ad901 100644 --- a/maps/maps_test.go +++ b/maps/maps_test.go @@ -61,7 +61,9 @@ func TestFilterInPlace(t *testing.T) { t.Run(name, func(t *testing.T) { t.Parallel() - got := maps.FilterInPlace(tt.args.m, func(k, v string) bool { return k == "foo" }) + + got := maps.FilterInPlace(tt.args.m, func(k, _ string) bool { return k == "foo" }) + assert.Equal(t, tt.want, got) }) } @@ -115,7 +117,9 @@ func TestFilter(t *testing.T) { t.Run(name, func(t *testing.T) { t.Parallel() - got := maps.Filter(tt.args.m, func(k, v string) bool { return k == "foo" }) + + got := maps.Filter(tt.args.m, func(k, _ string) bool { return k == "foo" }) + assert.Equal(t, tt.want, got) }) } @@ -163,7 +167,9 @@ func TestKeys(t *testing.T) { t.Run(name, func(t *testing.T) { t.Parallel() + got := maps.Keys(tt.args.m) + slices.Sort(got) assert.Equal(t, tt.want, got) }) @@ -212,7 +218,9 @@ func TestKeysFunc(t *testing.T) { t.Run(name, func(t *testing.T) { t.Parallel() + got := maps.KeysFunc(tt.args.m, func(k string) string { return k + " func" }) + slices.Sort(got) assert.Equal(t, tt.want, got) }) @@ -261,7 +269,9 @@ func TestToSlice(t *testing.T) { t.Run(name, func(t *testing.T) { t.Parallel() + got := maps.ToSlice(tt.args.m, func(k, v string) string { return k + " " + v }) + slices.Sort(got) assert.Equal(t, tt.want, got) }) @@ -310,7 +320,9 @@ func TestValuesFunc(t *testing.T) { t.Run(name, func(t *testing.T) { t.Parallel() + got := maps.ValuesFunc(tt.args.m, func(v string) string { return v }) + slices.Sort(got) assert.Equal(t, tt.want, got) }) @@ -414,7 +426,9 @@ func TestIntersection(t *testing.T) { t.Run(name, func(t *testing.T) { t.Parallel() + got := maps.Intersect(tt.args.maps...) + slices.Sort(got) assert.Equal(t, tt.want, got) }) diff --git a/slices/slices.go b/slices/slices.go deleted file mode 100644 index c0cfcc3..0000000 --- a/slices/slices.go +++ /dev/null @@ -1,93 +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 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. -// And inlining budget of 80 is quite small, since most of it is going towards closure call. -// 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. -// -// 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. -// -// 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. -// -// 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. -// -// 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 { - return xslices.ToMap(slc, fn) -} - -// ToSet converts a slice to a set. -// -// 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{} { - return xslices.ToSetFunc(slc, fn) -} - -// IndexFunc returns the first index satisfying fn(slc[i]), -// or -1 if none do. -// -// 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. -// -// 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. -// -// 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. -// -// 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)]. -// -// 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. -// -// 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_test.go b/xslices/xslices_test.go index 0741e66..38a1f0b 100644 --- a/xslices/xslices_test.go +++ b/xslices/xslices_test.go @@ -72,9 +72,11 @@ func TestFilterInPlace(t *testing.T) { t.Run(name, func(t *testing.T) { t.Parallel() + got := xslices.FilterInPlace(tt.args.slice, func(i int) bool { return i%2 == 0 }) + assert.Equal(t, tt.want, got) }) } @@ -140,9 +142,9 @@ func TestFilter(t *testing.T) { t.Run(name, func(t *testing.T) { t.Parallel() - got := xslices.Filter(tt.args.slice, func(i int) bool { - return i%2 == 0 - }) + + got := xslices.Filter(tt.args.slice, func(i int) bool { return i%2 == 0 }) + assert.Equal(t, tt.want, got) }) }