pax_global_header00006660000000000000000000000064151654737630014533gustar00rootroot0000000000000052 comment=90da471f1f4a5581bbe45a54603e1307406050f9 anchore-go-sync-da8a02e/000077500000000000000000000000001516547376300152155ustar00rootroot00000000000000anchore-go-sync-da8a02e/.binny.yaml000066400000000000000000000006471516547376300173050ustar00rootroot00000000000000# only pull in version updates that were released more than a week ago (low-pass filter for quickly-retracted releases) cooldown: 7d # other binary versions inherited from .binny.yaml file in the anchore/go-make repo (version pinned in .make). # If you need to override a tool version, please do so by adding an entry here and go-make will # account for the local definition over the go-make shared definitions. tools: [] anchore-go-sync-da8a02e/.bouncer.yaml000066400000000000000000000003251516547376300176140ustar00rootroot00000000000000permit: - BSD.* - MIT.* - Apache.* - MPL.* - CC0-1.0 ignore-packages: # crypto/internal/boring is released under the openSSL license as a part of the Golang Standard Libary - crypto/internal/boring anchore-go-sync-da8a02e/.chronicle.yaml000066400000000000000000000002651516547376300201300ustar00rootroot00000000000000# no matter the change, stay v0 for now enforce-v0: true # since github release pages already have titles that are the tag, we don't need to repeat that in the changelog title: "" anchore-go-sync-da8a02e/.github/000077500000000000000000000000001516547376300165555ustar00rootroot00000000000000anchore-go-sync-da8a02e/.github/dependabot.yml000066400000000000000000000027201516547376300214060ustar00rootroot00000000000000# Dependabot configuration # # Grouping behavior (see inline comments for details): # - Minor + patch updates: grouped into a single PR per ecosystem # - Major version bumps: individual PR per dependency # - Security updates: individual PR per dependency # # Note: "patch" refers to semver version bumps (1.2.3 -> 1.2.4), not security fixes. # Security updates are identified separately via GitHub's Advisory Database and # can be any version bump (patch, minor, or major) that fixes a known CVE. version: 2 updates: - package-ecosystem: gomod directories: - "/" - "/.make" cooldown: default-days: 7 schedule: interval: "weekly" day: "friday" open-pull-requests-limit: 10 labels: - "dependencies" groups: go-minor-patch: applies-to: version-updates # security updates get individual PRs patterns: - "*" update-types: # major omitted, gets individual PRs - "minor" - "patch" - package-ecosystem: "github-actions" directories: - "/" cooldown: default-days: 7 schedule: interval: "weekly" day: "friday" open-pull-requests-limit: 10 labels: - "dependencies" groups: actions-minor-patch: applies-to: version-updates # security updates get individual PRs patterns: - "*" update-types: # major omitted, gets individual PRs - "minor" - "patch" anchore-go-sync-da8a02e/.github/workflows/000077500000000000000000000000001516547376300206125ustar00rootroot00000000000000anchore-go-sync-da8a02e/.github/workflows/codeql.yaml000066400000000000000000000026211516547376300227460ustar00rootroot00000000000000name: "CodeQL" on: push: branches: [ "main" ] pull_request: branches: [ "main" ] schedule: - cron: '38 11 * * 3' jobs: analyze: name: Analyze (${{ matrix.language }}) runs-on: ubuntu-latest permissions: security-events: write packages: read actions: read contents: read strategy: fail-fast: false matrix: include: - language: actions build-mode: none - language: go build-mode: manual steps: - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: Setup Go if: matrix.language == 'go' uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 with: go-version-file: go.mod - name: Initialize CodeQL uses: github/codeql-action/init@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1 with: languages: ${{ matrix.language }} build-mode: ${{ matrix.build-mode }} - name: Build (Go) if: matrix.build-mode == 'manual' shell: bash run: go build ./... - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1 with: category: "/language:${{matrix.language}}" anchore-go-sync-da8a02e/.github/workflows/dependabot-automation.yaml000066400000000000000000000002611516547376300257600ustar00rootroot00000000000000name: Dependabot Automation on: pull_request: permissions: pull-requests: write jobs: run: uses: anchore/workflows/.github/workflows/dependabot-automation.yaml@main anchore-go-sync-da8a02e/.github/workflows/oss-project-board-add.yaml000066400000000000000000000004611516547376300255620ustar00rootroot00000000000000name: Add to OSS board on: issues: types: - opened - reopened - transferred - labeled permissions: contents: read jobs: run: uses: "anchore/workflows/.github/workflows/oss-project-board-add.yaml@main" secrets: token: ${{ secrets.OSS_PROJECT_GH_TOKEN }} anchore-go-sync-da8a02e/.github/workflows/release.yaml000066400000000000000000000036671516547376300231320ustar00rootroot00000000000000name: "Release" permissions: contents: read # there should never be two releases in progress at the same time concurrency: group: release cancel-in-progress: false on: workflow_dispatch: inputs: version: description: tag the latest commit on main with the given version (prefixed with v) required: true jobs: version-available: uses: anchore/workflows/.github/workflows/check-version-available.yaml@4f25313f96311410cad4173f74617654a3e46d48 # v0.3.0 with: version: ${{ github.event.inputs.version }} check-gate: permissions: checks: read # required for getting the status of specific check names uses: anchore/workflows/.github/workflows/check-gate.yaml@4f25313f96311410cad4173f74617654a3e46d48 # v0.3.0 with: # these are checks that should be run on pull-request and merges to main. # we do NOT want to kick off a release if these have not been verified on main. # Please see the validations.yaml workflow for the names that should be used here. checks: '["Static analysis", "Unit tests"]' release: needs: [check-gate, version-available] environment: release # contains secrets needed for release runs-on: ubuntu-24.04 permissions: contents: write # needed for creating github release objects steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2 with: fetch-depth: 0 # we need the full history to reason about changelogs and tags persist-credentials: true # needed for pushing a tag # setup checkout, go, go-make, binny, and cache go modules - uses: anchore/go-make/.github/actions/setup@383ef7852b8ae43a30f424896b52479186d2ea4d # v0.1.0 - name: Create release env: GITHUB_TOKEN: ${{ github.token }} DEPLOY_KEY: ${{ secrets.DEPLOY_KEY }} RELEASE_VERSION: ${{ github.event.inputs.version }} run: make ci-release anchore-go-sync-da8a02e/.github/workflows/remove-awaiting-response-label.yaml000066400000000000000000000004261516547376300275070ustar00rootroot00000000000000name: "Manage Awaiting Response Label" on: issue_comment: types: [created] permissions: contents: read jobs: run: uses: "anchore/workflows/.github/workflows/remove-awaiting-response-label.yaml@main" secrets: token: ${{ secrets.OSS_PROJECT_GH_TOKEN }} anchore-go-sync-da8a02e/.github/workflows/validate-github-actions.yaml000066400000000000000000000015011516547376300262020ustar00rootroot00000000000000name: "Validate GitHub Actions" on: workflow_dispatch: pull_request: push: branches: - main paths: - '.github/workflows/**' - '.github/actions/**' permissions: contents: read jobs: zizmor: name: "Lint" runs-on: ubuntu-latest permissions: contents: read security-events: write # for uploading SARIF results steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: "Run zizmor" uses: zizmorcore/zizmor-action@71321a20a9ded102f6e9ce5718a2fcec2c4f70d8 # v0.5.2 with: # there is a pass/fail gate as a repo ruleset (if there is no ruleset configured then the action will pass by default) advanced-security: true inputs: .github anchore-go-sync-da8a02e/.github/workflows/validations.yaml000066400000000000000000000017211516547376300240140ustar00rootroot00000000000000name: "Validations" on: workflow_dispatch: push: branches: - main pull_request: permissions: contents: read jobs: Static-Analysis: # Note: changing this job name requires making the same update in the .github/workflows/release.yaml pipeline name: "Static analysis" runs-on: ubuntu-24.04 steps: # setup checkout, go, go-make, binny, and cache go modules - uses: anchore/go-make/.github/actions/setup@383ef7852b8ae43a30f424896b52479186d2ea4d # v0.1.0 - name: Run static analysis run: make static-analysis Unit-Test: # Note: changing this job name requires making the same update in the .github/workflows/release.yaml pipeline name: "Unit tests" runs-on: ubuntu-24.04 steps: # setup checkout, go, go-make, binny, and cache go modules - uses: anchore/go-make/.github/actions/setup@383ef7852b8ae43a30f424896b52479186d2ea4d # v0.1.0 - name: Run unit tests run: make unit anchore-go-sync-da8a02e/.github/zizmor.yml000066400000000000000000000002751516547376300206360ustar00rootroot00000000000000rules: unpinned-uses: ignore: # Allow unpinned uses of trusted internal anchore/workflows actions - oss-project-board-add.yaml - remove-awaiting-response-label.yaml anchore-go-sync-da8a02e/.gitignore000066400000000000000000000010671516547376300172110ustar00rootroot00000000000000# local development go.work go.work.sum mise.toml /specs/ # IDEs .idea/ .vscode/ .history/ # tools and aux data .tool .tmp .task # release info /CHANGELOG.md /VERSION # archives and fixtures /test/results /dist /snapshot .server/ *.fingerprint *.tar *.jar *.war *.ear *.jpi *.hpi *.zip *.log .images .tmp/ coverage.txt bin/ # Binaries for programs and plugins *.exe *.exe~ *.dll *.so *.dylib # Test binary, build with `go test -c` *.test # Output of the go coverage tool, specifically when used with LiteIDE *.out # macOS Finder metadata .DS_STORE *.profile anchore-go-sync-da8a02e/.golangci.yaml000066400000000000000000000055701516547376300177510ustar00rootroot00000000000000version: "2" run: tests: false linters: default: none enable: - asciicheck - bodyclose - copyloopvar - dogsled - dupl - errcheck - funlen - gocognit - goconst - gocritic - gocyclo - goprintffuncname - gosec - govet - ineffassign - misspell - nakedret - nolintlint - revive - staticcheck - unconvert - unparam - unused - whitespace settings: funlen: lines: 100 statements: 60 gocognit: min-complexity: 40 gocritic: enabled-checks: - deferInLoop gosec: excludes: - G115 exclusions: generated: lax presets: - comments - common-false-positives - legacy - std-error-handling paths: - third_party$ - builtin$ - examples$ # do not enable... # - deadcode # The owner seems to have abandoned the linter. Replaced by "unused". # - depguard # We don't have a configuration for this yet # - goprintffuncname # does not catch all cases and there are exceptions # - nakedret # does not catch all cases and should not fail a build # - gochecknoglobals # - gochecknoinits # this is too aggressive # - rowserrcheck disabled per generics https://github.com/golangci/golangci-lint/issues/2649 # - godot # - godox # - goerr113 # - goimports # we're using gosimports now instead to account for extra whitespaces (see https://github.com/golang/go/issues/20818) # - golint # deprecated # - gomnd # this is too aggressive # - interfacer # this is a good idea, but is no longer supported and is prone to false positives # - lll # without a way to specify per-line exception cases, this is not usable # - maligned # this is an excellent linter, but tricky to optimize and we are not sensitive to memory layout optimizations # - nestif # - prealloc # following this rule isn't consistently a good idea, as it sometimes forces unnecessary allocations that result in less idiomatic code # - rowserrcheck # not in a repo with sql, so this is not useful # - scopelint # deprecated # - structcheck # The owner seems to have abandoned the linter. Replaced by "unused". # - testpackage # - varcheck # The owner seems to have abandoned the linter. Replaced by "unused". # - wsl # this doens't have an auto-fixer yet and is pretty noisy (https://github.com/bombsimon/wsl/issues/90) issues: max-same-issues: 25 uniq-by-line: false # TODO: enable this when we have coverage on docstring comments # # The list of ids of default excludes to include or disable. # include: # - EXC0002 # disable excluding of issues about comments from golint formatters: enable: - gofmt - goimports exclusions: generated: lax paths: - third_party$ - builtin$ - examples$ anchore-go-sync-da8a02e/.make/000077500000000000000000000000001516547376300162105ustar00rootroot00000000000000anchore-go-sync-da8a02e/.make/go.mod000066400000000000000000000004001516547376300173100ustar00rootroot00000000000000module local go 1.25.0 require github.com/anchore/go-make v0.1.0 require ( github.com/bmatcuk/doublestar/v4 v4.10.0 // indirect github.com/goccy/go-yaml v1.19.2 // indirect golang.org/x/mod v0.34.0 // indirect golang.org/x/sys v0.42.0 // indirect ) anchore-go-sync-da8a02e/.make/go.sum000066400000000000000000000014771516547376300173540ustar00rootroot00000000000000github.com/anchore/go-make v0.1.0 h1:w/DTKznE1s0u5H1ahAnLaHRbCyNuOn5sJd0UltKWCIA= github.com/anchore/go-make v0.1.0/go.mod h1:JafD2Md95wih7aGmA12yVoReZW3mOgIuwHbw5aOcIOQ= github.com/bmatcuk/doublestar/v4 v4.10.0 h1:zU9WiOla1YA122oLM6i4EXvGW62DvKZVxIe6TYWexEs= github.com/bmatcuk/doublestar/v4 v4.10.0/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM= github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI= golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY= golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= anchore-go-sync-da8a02e/.make/main.go000066400000000000000000000004741516547376300174700ustar00rootroot00000000000000package main import ( . "github.com/anchore/go-make" "github.com/anchore/go-make/tasks/golint" "github.com/anchore/go-make/tasks/gotest" "github.com/anchore/go-make/tasks/release" ) func main() { Makefile( gotest.Tasks(), golint.Tasks(), release.ChangelogTask(), release.TagAndCreateGHRelease(), ) } anchore-go-sync-da8a02e/CONTRIBUTING.md000066400000000000000000000072341516547376300174540ustar00rootroot00000000000000# Contributing to go-sync If you are looking to contribute to this project and want to open a GitHub pull request ("PR"), there are a few guidelines of what we are looking for in patches. Make sure you go through this document and ensure that your code proposal is aligned. ## Sign off your work The `sign-off` is an added line at the end of the explanation for the commit, certifying that you wrote it or otherwise have the right to submit it as an open-source patch. By submitting a contribution, you agree to be bound by the terms of the DCO Version 1.1 and Apache License Version 2.0. Signing off a commit certifies the below Developer's Certificate of Origin (DCO): ```text Developer's Certificate of Origin 1.1 By making a contribution to this project, I certify that: (a) The contribution was created in whole or in part by me and I have the right to submit it under the open source license indicated in the file; or (b) The contribution is based upon previous work that, to the best of my knowledge, is covered under an appropriate open source license and I have the right under that license to submit that work with modifications, whether created in whole or in part by me, under the same open source license (unless I am permitted to submit under a different license), as indicated in the file; or (c) The contribution was provided directly to me by some other person who certified (a), (b) or (c) and I have not modified it. (d) I understand and agree that this project and the contribution are public and that a record of the contribution (including all personal information I submit with it, including my sign-off) is maintained indefinitely and may be redistributed consistent with this project or the open source license(s) involved. ``` All contributions to this project are licensed under the [Apache License Version 2.0, January 2004](http://www.apache.org/licenses/). When committing your change, you can add the required line manually so that it looks like this: ```text Signed-off-by: John Doe ``` Alternatively, configure your Git client with your name and email to use the `-s` flag when creating a commit: ```text $ git config --global user.name "John Doe" $ git config --global user.email "john.doe@example.com" ``` Creating a signed-off commit is then possible with `-s` or `--signoff`: ```text $ git commit -s -m "this is a commit message" ``` To double-check that the commit was signed-off, look at the log output: ```text $ git log -1 commit 37ceh170e4hb283bb73d958f2036ee5k07e7fde7 (HEAD -> issue-35, origin/main, main) Author: John Doe Date: Mon Aug 1 11:27:13 2020 -0400 this is a commit message Signed-off-by: John Doe ``` [//]: # (TODO: Commit guidelines, granular commits) [//]: # (TODO: Commit guidelines, descriptive messages) [//]: # (TODO: Commit guidelines, commit title, extra body description) [//]: # (TODO: PR title and description) ## Test your changes This project has a `Makefile` which includes many helpers running both unit and integration tests. Although PRs will have automatic checks for these, it is useful to run them locally, ensuring they pass before submitting changes. Ensure you've bootstrapped once before running tests: ```text $ make bootstrap ``` You only need to bootstrap once. After the bootstrap process, you can run the tests as many times as needed: ```text $ make unit ``` ## Document your changes When proposed changes are modifying user-facing functionality or output, it is expected the PR will include updates to the documentation as well. anchore-go-sync-da8a02e/LICENSE000066400000000000000000000261351516547376300162310ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] 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. anchore-go-sync-da8a02e/Makefile000066400000000000000000000000561516547376300166560ustar00rootroot00000000000000.PHONY: * .DEFAULT: %: @go run -C .make . $@ anchore-go-sync-da8a02e/README.md000066400000000000000000000017111516547376300164740ustar00rootroot00000000000000# go-sync [![Go Report Card](https://goreportcard.com/badge/github.com/anchore/go-sync)](https://goreportcard.com/report/github.com/anchore/go-sync) [![GitHub go.mod Go version](https://img.shields.io/github/go-mod/go-version/anchore/go-sync.svg)](https://github.com/anchore/go-sync) [![License: Apache-2.0](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://github.com/anchore/go-sync/blob/main/LICENSE) [![Slack Invite](https://img.shields.io/badge/Slack-Join-blue?logo=slack)](https://anchore.com/slack) A collection of synchronization utilities. ## Status ***Consider this project to be in alpha. The API is not stable and may change at any time.*** ## Overview `sync.Executor` - a simple executor interface, with a bounded executor implementation available by using `sync.NewExecutor` `sync.Collector` - a simple interface to concurrently execute tasks and get the results `sync.List` - a concurrent list, queue, and stack implementation anchore-go-sync-da8a02e/collector.go000066400000000000000000000057251516547376300175430ustar00rootroot00000000000000package sync import ( "context" "errors" "iter" "runtime/debug" "sync" ) // Collect iterates over the provided iterator, executing the processor in parallel to map each incoming value to a result. // The accumulator is used to apply the results, with an exclusive lock; accumulator will never execute in parallel. // All errors returned from processor functions will be joined with errors.Join as the returned error. Panics are also // captured as errors from processor and accumulator functions func Collect[From, To any](ctx *context.Context, executorName string, iterator iter.Seq[From], processor func(From) (To, error), accumulator func(From, To)) error { if processor == nil { panic("no processor provided to Collect") } if ctx == nil || *ctx == nil { ctx = emptyContextPtr } var errs []error var lock sync.Mutex var wg sync.WaitGroup executor := ContextExecutor(ctx, executorName) for i := range iterator { // skip queuing any more values if (*ctx).Err() != nil { break } wg.Add(1) executor.Go(func() { defer func() { wg.Done() if err := recover(); err != nil { lock.Lock() defer lock.Unlock() errs = append(errs, PanicError{Value: err, Stack: string(debug.Stack())}) } }() // we may have queued many functions when canceled if (*ctx).Err() != nil { return } result, err := processor(i) lock.Lock() defer lock.Unlock() if err != nil { errs = append(errs, err) } if accumulator != nil { accumulator(i, result) } }) } done := make(chan struct{}) go func() { wg.Wait() close(done) }() select { case <-(*ctx).Done(): case <-done: } return errors.Join(errs...) } // CollectSlice is a specialized Collect call which appends results to a slice func CollectSlice[From, To any](ctx *context.Context, executorName string, values iter.Seq[From], processor func(From) (To, error), slice *[]To) error { return Collect(ctx, executorName, values, processor, func(_ From, value To) { *slice = append(*slice, value) }) } // CollectMap is a specialized Collect call which fills a map using the incoming value as a key, mapped to the result func CollectMap[From comparable, To any](ctx *context.Context, executorName string, values iter.Seq[From], processor func(From) (To, error), result map[From]To) error { return Collect(ctx, executorName, values, processor, func(key From, value To) { result[key] = value }) } // Collect2 is a specialized Collect call which accepts an iter.Seq2 and maps to processor and accumulator taking 2 input parameters func Collect2[From1, From2, To any](ctx *context.Context, executorName string, iterator iter.Seq2[From1, From2], processor func(From1, From2) (To, error), accumulator func(From1, From2, To)) error { return Collect[keyValue[From1, From2], To](ctx, executorName, toKeyValueIterator(iterator), func(k keyValue[From1, From2]) (To, error) { return processor(k.Key, k.Value) }, func(k keyValue[From1, From2], to To) { accumulator(k.Key, k.Value, to) }) } anchore-go-sync-da8a02e/collector_test.go000066400000000000000000000143451516547376300206000ustar00rootroot00000000000000package sync import ( "context" "errors" "fmt" "iter" "sync" "testing" "time" "github.com/stretchr/testify/require" "github.com/anchore/go-sync/internal/stats" ) func Test_CollectHandlesPanics(t *testing.T) { tests := []struct { name string collector func(from int) (string, error) accumulator func(i int, s string) assert require.ErrorAssertionFunc }{ { name: "no panics", collector: func(from int) (string, error) { return "", nil }, accumulator: func(i int, s string) {}, assert: func(t require.TestingT, err error, i ...interface{}) { require.NoError(t, err) }, }, { name: "single panic", collector: func(from int) (string, error) { if from == 1 { panic(fmt.Errorf("a single panic")) } return "", nil }, accumulator: func(i int, s string) {}, assert: func(t require.TestingT, err error, i ...interface{}) { require.Error(t, err) p := PanicError{} if errors.As(err, &p) { e := p.Unwrap() require.ErrorContains(t, e, "a single panic") require.Contains(t, p.Stack, "github.com/anchore/go-sync") } else { require.Fail(t, "should be a PanicError") } }, }, { name: "no panics with error", collector: func(from int) (string, error) { return "", fmt.Errorf("an error") }, accumulator: func(i int, s string) {}, assert: func(t require.TestingT, err error, i ...interface{}) { require.Error(t, err) require.ErrorContains(t, err, "an error") }, }, { name: "collector panics", collector: func(from int) (string, error) { panic("oh no collector!") }, accumulator: func(i int, s string) {}, assert: func(t require.TestingT, err error, i ...interface{}) { require.Error(t, err) require.ErrorContains(t, err, "oh no collector") // assert the stack trace require.ErrorContains(t, err, "github.com/anchore/go-sync") }, }, { name: "accumulator panics", collector: func(from int) (string, error) { return "", nil }, accumulator: func(i int, s string) { panic("oh no accumulator!") }, assert: func(t require.TestingT, err error, i ...interface{}) { require.Error(t, err) require.ErrorContains(t, err, "oh no accumulator") }, }, { name: "both panics", collector: func(from int) (string, error) { if from != 1 { return "", nil } panic("oh no collector!") }, accumulator: func(i int, s string) { panic("oh no accumulator!") }, assert: func(t require.TestingT, err error, i ...interface{}) { require.Error(t, err) require.ErrorContains(t, err, "oh no collector") require.ErrorContains(t, err, "oh no accumulator") }, }, { name: "collector panics and errors", collector: func(from int) (string, error) { if from == 1 { panic("oh no collector") } return "", fmt.Errorf("an error") }, accumulator: func(i int, s string) {}, assert: func(t require.TestingT, err error, i ...interface{}) { require.Error(t, err) require.ErrorContains(t, err, "an error") require.ErrorContains(t, err, "oh no collector") }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { ctx := context.TODO() err := Collect(&ctx, "", ToSeq([]int{1, 2, 3}), tt.collector, tt.accumulator) tt.assert(t, err) }) } } func Test_CollectCancelRepeat(t *testing.T) { // iterating these tests many times tends to make problems apparent much more quickly, // when they may succeed under certain conditions for i := 0; i < 1000; i++ { Test_CollectCancel(t) } } func Test_CollectCancel(t *testing.T) { ctx, cancel := context.WithCancel(context.TODO()) e := &errGroupExecutor{} // use errgroup executor as it will block before executing 3 e.g.SetLimit(2) ctx = SetContextExecutor(ctx, "", e) executed3 := false wg := sync.WaitGroup{} wg.Add(1) err := Collect(&ctx, "", ToSeq([]int{1, 2, 3}), func(i int) (string, error) { switch i { case 1: // cancel cancel() // ensure 2 doesn't block wg.Done() case 2: // ensure only 1 and 2 execute by waiting here wg.Wait() case 3: executed3 = true } return "", nil }, func(i int, s string) {}) // should not have an error, even though context was canceled require.NoError(t, err) // should not have executed 3 require.False(t, executed3) } func Test_CollectSlice(t *testing.T) { const count = 1000 const maxConcurrency = 5 concurrency := stats.Tracked[int]{} var values []int ctx := SetContextExecutor(context.Background(), "", NewExecutor(maxConcurrency)) err := CollectSlice(&ctx, "", countIter(count), func(i int) (int, error) { defer concurrency.Incr()() time.Sleep(1 * time.Millisecond) return i * 10, nil }, &values) require.NoError(t, err) require.Len(t, values, count) for i := 0; i < count; i++ { require.Contains(t, values, i*10) } require.LessOrEqual(t, concurrency.Max(), maxConcurrency) } func Test_CollectMap(t *testing.T) { const count = 1000 const maxConcurrency = 5 concurrency := stats.Tracked[int]{} values := map[int]int{} ctx := SetContextExecutor(context.Background(), "", NewExecutor(maxConcurrency)) err := CollectMap(&ctx, "", countIter(count), func(i int) (int, error) { defer concurrency.Incr()() time.Sleep(1 * time.Millisecond) return i * 10, nil }, values) require.NoError(t, err) require.Len(t, values, count) for i := 0; i < count; i++ { require.Equal(t, values[i], i*10) } require.LessOrEqual(t, concurrency.Max(), maxConcurrency) } func Test_Collect2(t *testing.T) { const count = 1000 const maxConcurrency = 5 concurrency := stats.Tracked[int]{} values := map[int]int{} ctx := SetContextExecutor(context.Background(), "", NewExecutor(maxConcurrency)) err := Collect2(&ctx, "", ToIndexSeq(ToSlice(countIter(count))), func(idx, i int) (int, error) { defer concurrency.Incr()() time.Sleep(1 * time.Millisecond) return i * 10, nil }, func(idx int, i int, out int) { values[i] = out + idx }) require.NoError(t, err) require.Len(t, values, count) for i := 0; i < count; i++ { require.Equal(t, values[i], (i*10)+i) } require.LessOrEqual(t, concurrency.Max(), maxConcurrency) } func countIter(count int) iter.Seq[int] { return func(yield func(int) bool) { for i := 0; i < count; i++ { if !yield(i) { return } } } } anchore-go-sync-da8a02e/context.go000066400000000000000000000024171516547376300172340ustar00rootroot00000000000000package sync import ( "context" ) const ExecutorDefault = "" type executorKey struct { name string } // HasContextExecutor returns true when the named executor is available in the context func HasContextExecutor(ctx context.Context, name string) bool { return ctx.Value(executorKey{name: name}) != nil } // ContextExecutor returns an executor in context with the given name, or a serial executor if none exists // and replaces the context with one that contains a new executor which won't deadlock func ContextExecutor(ctx *context.Context, name string) Executor { if ctx == nil || *ctx == nil { return serialExecutor{} } executor, ok := (*ctx).Value(executorKey{name: name}).(Executor) if !ok || executor == nil { if name != ExecutorDefault { return ContextExecutor(ctx, ExecutorDefault) } return serialExecutor{} } if e, _ := executor.(ChildExecutor); e != nil { *ctx = SetContextExecutor(*ctx, name, e.ChildExecutor()) } return executor } // SetContextExecutor returns a context with the named executor for use with GetExecutor func SetContextExecutor(ctx context.Context, name string, executor Executor) context.Context { return context.WithValue(ctx, executorKey{name: name}, executor) } var emptyContext = context.TODO() var emptyContextPtr = &emptyContext anchore-go-sync-da8a02e/context_test.go000066400000000000000000000055471516547376300203020ustar00rootroot00000000000000package sync import ( "context" "testing" "github.com/stretchr/testify/require" ) func Test_defaultExecutor(t *testing.T) { t.Run("only default executor", func(t *testing.T) { ctx := SetContextExecutor(context.Background(), ExecutorDefault, &unboundedExecutor{}) e := ContextExecutor(&ctx, "cpu") require.IsType(t, &unboundedExecutor{}, e) }) t.Run("default executor with named", func(t *testing.T) { ctx := SetContextExecutor(context.Background(), ExecutorDefault, &unboundedExecutor{}) ctx = SetContextExecutor(ctx, "cpu", &queuedExecutor{}) e := ContextExecutor(&ctx, "cpu") require.IsType(t, &queuedExecutor{}, e) }) t.Run("no default executor with named", func(t *testing.T) { ctx := SetContextExecutor(context.Background(), "cpu", &queuedExecutor{}) e := ContextExecutor(&ctx, "cpu") require.IsType(t, &queuedExecutor{}, e) }) t.Run("no default executor with different named", func(t *testing.T) { ctx := SetContextExecutor(context.Background(), "cpu", &queuedExecutor{}) e := ContextExecutor(&ctx, "io") require.IsType(t, serialExecutor{}, e) }) t.Run("no executor", func(t *testing.T) { ctx := context.Background() e := ContextExecutor(&ctx, "io") require.IsType(t, serialExecutor{}, e) }) t.Run("no executor get default", func(t *testing.T) { ctx := context.Background() e := ContextExecutor(&ctx, ExecutorDefault) require.IsType(t, serialExecutor{}, e) }) t.Run("no context", func(t *testing.T) { e := ContextExecutor(nil, "cpu") require.IsType(t, serialExecutor{}, e) }) t.Run("no context typed nil", func(t *testing.T) { var ctx context.Context e := ContextExecutor(&ctx, "cpu") require.IsType(t, serialExecutor{}, e) }) } func Test_HasContextExecutor(t *testing.T) { t.Run("WithExecutorInContext", func(t *testing.T) { ctx := SetContextExecutor(context.Background(), "cpu", &unboundedExecutor{}) require.True(t, HasContextExecutor(ctx, "cpu")) }) t.Run("WithOtherExecutorInContext", func(t *testing.T) { ctx := SetContextExecutor(context.Background(), "cpu", &unboundedExecutor{}) require.False(t, HasContextExecutor(ctx, "io")) }) } func Test_ContextExecutor(t *testing.T) { t.Run("WithExecutorInContext", func(t *testing.T) { ctx := SetContextExecutor(context.Background(), "cpu", &unboundedExecutor{}) result := ContextExecutor(&ctx, "cpu") require.NotNil(t, result) require.IsType(t, &unboundedExecutor{}, result) }) t.Run("WithoutExecutorInContext", func(t *testing.T) { ctx := context.Background() result := ContextExecutor(&ctx, "cpu") require.NotNil(t, result) require.IsType(t, serialExecutor{}, result) }) t.Run("WithDifferentExecutorInContext", func(t *testing.T) { ctx := SetContextExecutor(context.Background(), "io", &unboundedExecutor{}) result := ContextExecutor(&ctx, "cpu") require.NotNil(t, result) require.IsType(t, serialExecutor{}, result) }) } anchore-go-sync-da8a02e/errors.go000066400000000000000000000004721516547376300170630ustar00rootroot00000000000000package sync import "fmt" type PanicError struct { Value any Stack string } func (p PanicError) Error() string { return fmt.Sprintf("panic: %v at:\n%s", p.Value, p.Stack) } func (p PanicError) Unwrap() error { if e, ok := p.Value.(error); ok { return e } return nil } var _ error = (*PanicError)(nil) anchore-go-sync-da8a02e/executor.go000066400000000000000000000027731516547376300174130ustar00rootroot00000000000000package sync import ( "context" "math" ) // Executor the executor interface allows for different strategies to execute units of work and wait for all units // of work to be completed type Executor interface { // Go adds a unit of work to be executed by the executor. Depending on the execution strategy this may be blocking // or may execute the function directly Go(func()) // Wait blocks and waits for all the executing functions to be completed before returning, or the context is cancelled. // if more functions are added to be executed by this executor after the Wait call, these will also complete before Wait proceeds // If the context is canceled, any queued functions will not be executed Wait(context.Context) } // ChildExecutor interface, if implemented, will cause ContextExecutor calls to replace the provided context with one // containing a child executor returned from this function. This is used when it is not safe to nest Go calls type ChildExecutor interface { ChildExecutor() Executor } // NewExecutor returns an Executor based on the desired concurrency: // // < 0: unbounded, spawn a new goroutine for each Go call // 0: serial, executes in the same thread/routine as the caller of Go // > 0: a bounded executor with the maximum concurrency provided func NewExecutor(maxConcurrency int) Executor { if maxConcurrency < 0 || maxConcurrency > math.MaxInt32 { return &unboundedExecutor{} } if maxConcurrency == 0 { return serialExecutor{} } return newErrGroupExecutor(maxConcurrency) } anchore-go-sync-da8a02e/executor_errgroup.go000066400000000000000000000027731516547376300213400ustar00rootroot00000000000000package sync import ( "context" "sync" "sync/atomic" "golang.org/x/sync/errgroup" ) // errGroupExecutor is an Executor that executes units of work, blocking when Go is called once the maxConcurrency // is reached, only continuing subsequent Go calls when the nuber of executing functions drops below maxConcurrency type errGroupExecutor struct { maxConcurrency int canceled atomic.Bool g errgroup.Group wg sync.WaitGroup childLock sync.RWMutex childExecutor *errGroupExecutor } func newErrGroupExecutor(maxConcurrency int) *errGroupExecutor { e := &errGroupExecutor{ maxConcurrency: maxConcurrency, } e.g.SetLimit(maxConcurrency) return e } func (e *errGroupExecutor) Go(f func()) { e.wg.Add(1) fn := func() error { defer e.wg.Done() if e.canceled.Load() { return nil } f() return nil } e.g.Go(fn) } func (e *errGroupExecutor) Wait(ctx context.Context) { e.canceled.Store(ctx.Err() != nil) done := make(chan struct{}) go func() { e.wg.Wait() close(done) }() select { case <-ctx.Done(): e.canceled.Store(true) case <-done: } } func (e *errGroupExecutor) ChildExecutor() Executor { e.childLock.RLock() child := e.childExecutor e.childLock.RUnlock() if child != nil { return child } e.childLock.Lock() defer e.childLock.Unlock() if e.childExecutor == nil { // create child executor with same bound e.childExecutor = newErrGroupExecutor(e.maxConcurrency) } return e.childExecutor } var _ Executor = (*errGroupExecutor)(nil) anchore-go-sync-da8a02e/executor_errgroup_test.go000066400000000000000000000060161516547376300223710ustar00rootroot00000000000000package sync import ( "context" "sync" "testing" "github.com/stretchr/testify/require" ) func Test_errGroupExecutorRepeated(t *testing.T) { // iterating these tests many times tends to make problems apparent much more quickly, // when they may succeed under certain conditions for i := 0; i < 1000; i++ { Test_errGroupExecutor(t) } } func Test_errGroupExecutor(t *testing.T) { // this test sets up specific wait groups to ensure that the maximum concurrency is honored // by stepping through and holding specific locks while conditions are verified e := errGroupExecutor{maxConcurrency: 2} e.g.SetLimit(2) wg1 := &sync.WaitGroup{} wg1.Add(1) wg2 := &sync.WaitGroup{} wg2.Add(1) wg3 := &sync.WaitGroup{} wg3.Add(1) order := List[string]{} executed := "" wgReady := &sync.WaitGroup{} wgReady.Add(2) e.Go(func() { order.Append("pre wg1") wgReady.Done() wg1.Wait() order.Append("post wg1") executed += "1_" wg3.Done() }) e.Go(func() { order.Append("pre wg2") wgReady.Done() wg2.Wait() order.Append("post wg2") executed += "2_" }) wgReady.Wait() // errgroup execution is blocking, so the next e.Go will block, so continue on the first before we deadlock wg1.Done() e.Go(func() { order.Append("pre wg3") wg3.Wait() order.Append("post wg3") executed += "3_" wg2.Done() }) e.Wait(context.Background()) require.Equal(t, "1_3_2_", executed) require.True(t, order.indexOf("post wg1") < order.indexOf("post wg3") && order.indexOf("post wg3") < order.indexOf("post wg2"), ) } func Test_errGroupExecutorCancelRepeat(t *testing.T) { for i := 0; i < 100; i++ { Test_errGroupExecutorCancel(t) } } func Test_errGroupExecutorCancel(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) e := &errGroupExecutor{} e.g.SetLimit(2) wgs := [3]sync.WaitGroup{} wns := [3]sync.WaitGroup{} for i := range wgs { wns[i].Add(1) wgs[i].Add(1) } executed := [3]bool{} e.Go(func() { t.Logf("waiting 0") wns[0].Done() wgs[0].Wait() t.Logf("done 0") executed[0] = true }) e.Go(func() { t.Logf("waiting 1") wns[1].Done() wgs[1].Wait() t.Logf("done 1") executed[1] = true }) go func() { wns[0].Wait() wns[1].Wait() // 0 and 1 are currently executing, waiting cancel() wns[2].Wait() e.Go(func() { t.Logf("waiting 2") wgs[2].Wait() t.Logf("done 2") executed[2] = true }) for i := range wgs { wgs[i].Done() } }() // should be waiting in 0, 1 not executed 2 e.Wait(ctx) wns[2].Done() // should not have executed 2 require.False(t, executed[2]) } func Test_errGroupExecutorSubcontext(t *testing.T) { wg := sync.WaitGroup{} wg.Add(1) ctx := context.TODO() ctx = SetContextExecutor(ctx, "", newErrGroupExecutor(1)) ContextExecutor(&ctx, "").Go(func() { // context should be replaced with a secondary executor ContextExecutor(&ctx, "").Go(func() { // context should be replaced again with a tertiary executor ContextExecutor(&ctx, "").Go(func() { wg.Done() }) }) }) wg.Wait() // only done by sub-executor } anchore-go-sync-da8a02e/executor_queue.go000066400000000000000000000030351516547376300206070ustar00rootroot00000000000000package sync import ( "context" "sync" "sync/atomic" ) // queuedExecutor is an Executor that accepts units of work to execute asynchronously, queuing them rather than blocking type queuedExecutor struct { canceled atomic.Bool maxConcurrency int executing atomic.Int32 queue List[*func()] wg sync.WaitGroup childLock sync.RWMutex childExecutor *errGroupExecutor } var _ Executor = (*queuedExecutor)(nil) func (e *queuedExecutor) Go(f func()) { if e.canceled.Load() { return } e.wg.Add(1) fn := func() { defer e.wg.Done() if e.canceled.Load() { return } f() } e.queue.Enqueue(&fn) if int(e.executing.Load()) < e.maxConcurrency { go e.exec() } } func (e *queuedExecutor) Wait(ctx context.Context) { e.canceled.Store(ctx.Err() != nil) done := make(chan struct{}) go func() { e.wg.Wait() close(done) }() select { case <-ctx.Done(): e.canceled.Store(true) case <-done: } } func (e *queuedExecutor) ChildExecutor() Executor { e.childLock.RLock() child := e.childExecutor e.childLock.RUnlock() if child != nil { return child } e.childLock.Lock() defer e.childLock.Unlock() if e.childExecutor == nil { // create child executor with same bound e.childExecutor = newErrGroupExecutor(e.maxConcurrency) } return e.childExecutor } func (e *queuedExecutor) exec() { e.executing.Add(1) defer e.executing.Add(-1) if int(e.executing.Load()) > e.maxConcurrency { return } for { f, ok := e.queue.Dequeue() if !ok { return } if f != nil { (*f)() } } } anchore-go-sync-da8a02e/executor_queue_test.go000066400000000000000000000104661516547376300216540ustar00rootroot00000000000000package sync import ( "context" "sync" "testing" "time" "github.com/stretchr/testify/require" "github.com/anchore/go-sync/internal/atomic" "github.com/anchore/go-sync/internal/stats" ) func Test_queuedExecutor(t *testing.T) { concurrency := 25 count := 1000 wgs := make([]sync.WaitGroup, count) concurrent := stats.Tracked[int64]{} total := atomic.Uint64{} makeFunc := func(idx int) func() { wgs[idx].Add(1) return func() { defer total.Add(1) concurrent.Incr() wgs[idx].Wait() concurrent.Decr() } } var expected []int e := &queuedExecutor{maxConcurrency: concurrency} for i := 0; i < count; i++ { expected = append(expected, i) e.Go(makeFunc(i)) } go func() { for i := 0; i < count; i++ { wgs[i].Done() } }() e.Wait(context.Background()) require.LessOrEqual(t, concurrent.Max(), int64(concurrency)) require.Equal(t, total.Load(), uint64(count)) } func Test_queuedExecutorSmall(t *testing.T) { concurrency := 2 count := 4 wgs := make([]sync.WaitGroup, count) waiting := sync.WaitGroup{} waiting.Add(int(concurrency)) concurrent := stats.Tracked[int]{} total := atomic.Uint64{} makeFunc := func(idx int) func() { wgs[idx].Add(1) return func() { if idx < int(concurrency) { waiting.Done() } defer total.Add(1) concurrent.Incr() wgs[idx].Wait() concurrent.Decr() } } e := &queuedExecutor{maxConcurrency: concurrency} for i := 0; i < count; i++ { e.Go(makeFunc(i)) } time.Sleep(10 * time.Millisecond) waiting.Wait() require.Equal(t, 2, concurrent.Val()) go func() { for i := 0; i < count; i++ { wgs[i].Done() } }() e.Wait(context.Background()) require.LessOrEqual(t, concurrent.Max(), concurrency) require.Equal(t, total.Load(), uint64(count)) } func Test_explicitExecutorLimiting(t *testing.T) { // this test sets up specific wait groups to ensure that the maximum concurrency is honored // by stepping through and holding specific locks while conditions are verified e := queuedExecutor{maxConcurrency: 2} wg1 := &sync.WaitGroup{} wg1.Add(1) wg2 := &sync.WaitGroup{} wg2.Add(1) wg3 := &sync.WaitGroup{} wg3.Add(1) order := List[string]{} executed := "" wgReady := &sync.WaitGroup{} wgReady.Add(2) e.Go(func() { order.Append("pre wg1") wgReady.Done() wg1.Wait() order.Append("post wg1") executed += "1_" }) e.Go(func() { order.Append("pre wg2") wgReady.Done() wg2.Wait() order.Append("post wg2") executed += "2_" wg3.Done() }) wgReady.Wait() e.Go(func() { order.Append("pre wg3") wg3.Wait() order.Append("post wg3") executed += "3_" wg1.Done() }) wg2.Done() e.Wait(context.Background()) require.Equal(t, "2_3_1_", executed) require.True(t, order.indexOf("post wg2") < order.indexOf("post wg3") && order.indexOf("post wg3") < order.indexOf("post wg1"), ) } func Test_queuedExecutorCancelRepeat(t *testing.T) { // iterating these tests many times tends to make problems apparent much more quickly, // when they may succeed under certain conditions for i := 0; i < 1000; i++ { Test_queuedExecutorCancel(t) } } func Test_queuedExecutorCancel(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) e := &queuedExecutor{maxConcurrency: 2} wgs := [3]sync.WaitGroup{} wns := [3]sync.WaitGroup{} for i := range wgs { wns[i].Add(1) wgs[i].Add(1) } executed := [3]bool{} e.Go(func() { wns[0].Done() wgs[0].Wait() executed[0] = true }) e.Go(func() { wns[1].Done() wgs[1].Wait() executed[1] = true }) go func() { wns[0].Wait() wns[1].Wait() // 0 and 1 are currently executing, waiting cancel() wns[2].Wait() e.Go(func() { wgs[2].Wait() executed[2] = true }) for i := range wgs { wgs[i].Done() } }() // should be waiting in 0, 1 not executed 2 e.Wait(ctx) wns[2].Done() // should not have executed 2 require.False(t, executed[2]) } func Test_queuedExecutorSubcontext(t *testing.T) { wg := sync.WaitGroup{} wg.Add(1) ctx := context.TODO() ctx = SetContextExecutor(ctx, "", &queuedExecutor{maxConcurrency: 1}) ContextExecutor(&ctx, "").Go(func() { // context should be replaced with a secondary executor ContextExecutor(&ctx, "").Go(func() { // context should be replaced again with a tertiary executor ContextExecutor(&ctx, "").Go(func() { wg.Done() }) }) }) wg.Wait() // only done by sub-executor } anchore-go-sync-da8a02e/executor_serial.go000066400000000000000000000004341516547376300207420ustar00rootroot00000000000000package sync import "context" // serialExecutor is an Executor that executes serially, without any goroutines type serialExecutor struct{} func (u serialExecutor) Go(fn func()) { fn() } func (u serialExecutor) Wait(_ context.Context) { } var _ Executor = (*serialExecutor)(nil) anchore-go-sync-da8a02e/executor_serial_test.go000066400000000000000000000007421516547376300220030ustar00rootroot00000000000000package sync import ( "context" "sync" "testing" ) func Test_serialSubcontext(t *testing.T) { wg := sync.WaitGroup{} wg.Add(1) ctx := context.TODO() ctx = SetContextExecutor(ctx, "", serialExecutor{}) ContextExecutor(&ctx, "").Go(func() { // context should be able to continue ContextExecutor(&ctx, "").Go(func() { // context should be able to continue ContextExecutor(&ctx, "").Go(func() { wg.Done() }) }) }) wg.Wait() // only done by sub-executor } anchore-go-sync-da8a02e/executor_test.go000066400000000000000000000027601516547376300204460ustar00rootroot00000000000000package sync import ( "context" "sync/atomic" "testing" "time" "github.com/stretchr/testify/require" "github.com/anchore/go-sync/internal/stats" ) func Test_Executors(t *testing.T) { for i := 0; i < 100; i++ { Test_Executor(t) } } func Test_Executor(t *testing.T) { const count = 1000 tests := []struct { name string maxConcurrency int }{ { name: "sequential", maxConcurrency: 0, }, { name: "unbounded concurrency", maxConcurrency: -1, }, { name: "single execution", maxConcurrency: 1, }, { name: "dual execution", maxConcurrency: 2, }, { name: "ten-x execution", maxConcurrency: 10, }, } for _, test := range tests { for _, errGroup := range []bool{false, true} { t.Run(test.name, func(t *testing.T) { e := NewExecutor(test.maxConcurrency) if !errGroup && test.maxConcurrency > 1 { e = &queuedExecutor{ maxConcurrency: test.maxConcurrency, } } executed := atomic.Int32{} concurrency := stats.Tracked[int]{} for i := 0; i < count; i++ { e.Go(func() { defer concurrency.Incr()() executed.Add(1) time.Sleep(10 * time.Nanosecond) }) } e.Wait(context.Background()) require.Equal(t, count, int(executed.Load())) if test.maxConcurrency > 0 { require.LessOrEqual(t, concurrency.Max(), test.maxConcurrency) } else { require.GreaterOrEqual(t, concurrency.Max(), 1) } }) } } } anchore-go-sync-da8a02e/executor_unbounded.go000066400000000000000000000011251516547376300214440ustar00rootroot00000000000000package sync import ( "context" "sync" "sync/atomic" ) // unboundedExecutor executes all Go calls without any specific bound type unboundedExecutor struct { canceled atomic.Bool wg sync.WaitGroup } func (e *unboundedExecutor) Go(f func()) { e.wg.Add(1) go func() { defer e.wg.Done() if e.canceled.Load() { return } f() }() } func (e *unboundedExecutor) Wait(ctx context.Context) { e.canceled.Store(ctx.Err() != nil) done := make(chan struct{}) go func() { e.wg.Wait() close(done) }() select { case <-ctx.Done(): e.canceled.Store(true) case <-done: } } anchore-go-sync-da8a02e/executor_unbounded_test.go000066400000000000000000000007511516547376300225070ustar00rootroot00000000000000package sync import ( "context" "sync" "testing" ) func Test_unboundedSubcontext(t *testing.T) { wg := sync.WaitGroup{} wg.Add(1) ctx := context.TODO() ctx = SetContextExecutor(ctx, "", &unboundedExecutor{}) ContextExecutor(&ctx, "").Go(func() { // context should be able to continue ContextExecutor(&ctx, "").Go(func() { // context should be able to continue ContextExecutor(&ctx, "").Go(func() { wg.Done() }) }) }) wg.Wait() // only done by sub-executor } anchore-go-sync-da8a02e/go.mod000066400000000000000000000004151516547376300163230ustar00rootroot00000000000000module github.com/anchore/go-sync go 1.25.0 require ( github.com/stretchr/testify v1.11.1 golang.org/x/sync v0.20.0 ) require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) anchore-go-sync-da8a02e/go.sum000066400000000000000000000020161516547376300163470ustar00rootroot00000000000000github.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.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= 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= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= anchore-go-sync-da8a02e/interfaces.go000066400000000000000000000015141516547376300176700ustar00rootroot00000000000000package sync // Provider returns a single item type Provider[T any] interface { Get() T } // Iterable provides a function to iterate a series of values type Iterable[T any] interface { // Seq provides an iter.Seq compatible iterator Seq(fn func(value T) bool) } // Appender to allow values to be appended type Appender[T any] interface { Append(value T) } // Collection is a generic collection of values, which can be added to, removed from, and provide a length type Collection[T any] interface { Iterable[T] Appender[T] Remove(value T) Contains(value T) bool Len() int } // Queue is a generic queue interface type Queue[T any] interface { Enqueue(value T) Dequeue() (value T, ok bool) } // Stack is a generic stack interface type Stack[T any] interface { Push(value T) Pop() (value T, ok bool) Peek() (value T, ok bool) } anchore-go-sync-da8a02e/internal/000077500000000000000000000000001516547376300170315ustar00rootroot00000000000000anchore-go-sync-da8a02e/internal/atomic/000077500000000000000000000000001516547376300203055ustar00rootroot00000000000000anchore-go-sync-da8a02e/internal/atomic/exports.go000066400000000000000000000003311516547376300223350ustar00rootroot00000000000000package atomic import "sync/atomic" // for import convenience, export things from sync/atomic that we use type Int32 = atomic.Int32 type Int64 = atomic.Int64 type Uint32 = atomic.Uint32 type Uint64 = atomic.Uint64 anchore-go-sync-da8a02e/internal/atomic/float64.go000066400000000000000000000022571516547376300221210ustar00rootroot00000000000000package atomic import ( "math" "sync/atomic" ) // Float64 provides a struct implementing an atomic version of a float64 value, like sync/atomic.Int64 type Float64 struct { value atomic.Uint64 } // Load atomically loads and returns the value stored in x. func (x *Float64) Load() float64 { return math.Float64frombits(x.value.Load()) } // Store atomically stores val into x. func (x *Float64) Store(val float64) { x.value.Store(math.Float64bits(val)) } // Swap atomically stores new into x and returns the previous value. func (x *Float64) Swap(newVal float64) (oldVal float64) { return math.Float64frombits(x.value.Swap(math.Float64bits(newVal))) } // CompareAndSwap executes the compare-and-swap operation for x. func (x *Float64) CompareAndSwap(oldVal, newVal float64) (swapped bool) { return x.value.CompareAndSwap(math.Float64bits(oldVal), math.Float64bits(newVal)) } // Add atomically adds delta to x and returns the new value. func (x *Float64) Add(delta float64) (updated float64) { current := x.Load() if delta == 0 { return current } for { updated = current + delta if !x.CompareAndSwap(current, updated) { current = x.Load() continue } break } return } anchore-go-sync-da8a02e/internal/atomic/float64_test.go000066400000000000000000000010431516547376300231500ustar00rootroot00000000000000package atomic import ( "sync" "sync/atomic" "testing" "github.com/stretchr/testify/require" ) func Test_Float64(t *testing.T) { val := Float64{} val2 := &atomic.Int64{} concurrency := 3925 num := 3 wg := sync.WaitGroup{} for i := 0; i < concurrency; i++ { wg.Add(1) go func() { val.Add(float64(num)) val2.Add(int64(num)) wg.Done() }() } wg.Wait() require.Equal(t, val2.Load(), int64(val.Load())) require.Equal(t, int64(concurrency*num), int64(val.Load())) require.Equal(t, int64(concurrency*num), val2.Load()) } anchore-go-sync-da8a02e/internal/atomic/slice.go000066400000000000000000000000171516547376300217310ustar00rootroot00000000000000package atomic anchore-go-sync-da8a02e/internal/channel/000077500000000000000000000000001516547376300204415ustar00rootroot00000000000000anchore-go-sync-da8a02e/internal/channel/tee.go000066400000000000000000000013561516547376300215520ustar00rootroot00000000000000package channel import ( "github.com/anchore/go-sync" ) func Tee[T any](in chan T, receivers ...func(events chan T)) (writer chan T, add func(func(events chan T)) (remove func())) { clones := sync.List[chan T]{} add = func(receiver func(events chan T)) (remove func()) { clone := make(chan T) clones.Append(clone) go receiver(clone) return func() { clones.Remove(clone) close(clone) } } for _, receiver := range receivers { _ = add(receiver) } go func() { defer func() { for clone := range clones.Seq { close(clone) } }() for val := range in { go func(val T) { defer func() { _ = recover() }() for clone := range clones.Seq { clone <- val } }(val) } }() return in, add } anchore-go-sync-da8a02e/internal/channel/tee_test.go000066400000000000000000000031741516547376300226110ustar00rootroot00000000000000package channel import ( "fmt" "sync" "sync/atomic" "testing" "github.com/stretchr/testify/require" gosync "github.com/anchore/go-sync" ) func Test_ChannelTee(t *testing.T) { events := make(chan any) received := &gosync.List[int]{} wg := &sync.WaitGroup{} closing := atomic.Bool{} closing.Store(false) makeReceiver := func(number int) func(events chan any) { return func(events chan any) { for { select { case event, open := <-events: if event != nil { // t.Logf("%d: %v", number, event) received.Append(number) wg.Done() } if !open { if closing.Load() { wg.Done() } // t.Logf("end %d", number) return } } } } } events, add := Tee(events, makeReceiver(1), makeReceiver(2)) remove3 := add(makeReceiver(3)) _ = add(makeReceiver(4)) remove5 := add(makeReceiver(5)) _ = add(makeReceiver(6)) remove7 := add(makeReceiver(7)) wg.Add(7) go func() { events <- fmt.Errorf("hello") }() wg.Wait() require.ElementsMatch(t, received.Values(), []int{1, 2, 3, 4, 5, 6, 7}) remove5() received.Clear() wg.Add(6) go func() { events <- fmt.Errorf("goodbye") }() wg.Wait() require.ElementsMatch(t, received.Values(), []int{1, 2, 3, 4, 6, 7}) remove3() received.Clear() wg.Add(5) go func() { events <- fmt.Errorf("cats") }() wg.Wait() require.ElementsMatch(t, received.Values(), []int{1, 2, 4, 6, 7}) remove7() received.Clear() wg.Add(4) go func() { events <- fmt.Errorf("dogs") }() wg.Wait() require.ElementsMatch(t, received.Values(), []int{1, 2, 4, 6}) closing.Store(true) wg.Add(4) close(events) wg.Wait() } anchore-go-sync-da8a02e/internal/index/000077500000000000000000000000001516547376300201405ustar00rootroot00000000000000anchore-go-sync-da8a02e/internal/index/key_split.go000066400000000000000000000154261516547376300225020ustar00rootroot00000000000000package index import ( "bytes" "encoding/json" "strings" "github.com/anchore/go-sync" ) // ----------------------- KeySplitIndex ----------------------- type KeySplitIndex[T any] struct { Node[T] } var _ interface { json.Marshaler json.Unmarshaler } = (*KeySplitIndex[int])(nil) func (n *KeySplitIndex[T]) MarshalJSON() ([]byte, error) { values := map[string]T{} collectValues(values, "", &n.Node) buf := &bytes.Buffer{} enc := json.NewEncoder(buf) enc.SetEscapeHTML(false) err := enc.Encode(values) if err != nil { return nil, err } return buf.Bytes(), nil } func (n *KeySplitIndex[T]) UnmarshalJSON(bytes []byte) error { values := map[string]T{} err := json.Unmarshal(bytes, &values) if err != nil { return err } for k, v := range values { n.Set(k, v) } return nil } func collectValues[T any](values map[string]T, key string, node *Node[T]) { if node.set { values[key] = node.value } for segment, child := range node.keyMap { collectValues(values, key+segment, child) } } // ----------------------- Node ----------------------- type NodeMap[T any] map[string]*Node[T] type Node[T any] struct { sync.Locking set bool value T keyMap NodeMap[T] keyByChar map[rune]string } type NodeUpdateFunc[T any] func(current T) (newValue T) func (n *Node[T]) Value() (v T) { unlock := n.RLock() v = n.value unlock() return } func (n *Node[T]) Contains(s string) bool { _, equal := n._find(s) return equal } func (n *Node[T]) Get(s string) (out T) { v, equal := n._find(s) if !equal || v == nil { return } out = v.Value() return } func (n *Node[T]) Set(name string, value T) { node := n._makeNodeP(nil, name, nil) node.SetValue(value) } func (n *Node[T]) Remove(s string) (out T) { v, equal := n._find(s) if !equal || v == nil { return } out = v.Value() v.UnsetValue() return out } func (n *Node[T]) ByPrefix(s string) []T { v, _ := n._find(s) if v == nil { return nil } return v.Collect() } func (n *Node[T]) Update(name string, f NodeUpdateFunc[T]) { unlock := n.Lock() node := n._makeNodeP(&unlock, name, nil) v := f(node.value) node.SetValue(v) unlock() } func (n *Node[T]) SetValue(value T) { unlock := n.Lock() n.value = value n.set = true unlock() } func (n *Node[T]) UnsetValue() { unlock := n.Lock() var zero T n.value = zero n.set = false unlock() } func (n *Node[T]) Collect() (values []T) { n._collect(&values) return values } func (n *Node[T]) _collect(values *[]T) { unlock := n.RLock() if n.set { *values = append(*values, n.value) } for _, v := range n.keyMap { v._collect(values) } unlock() } // _startsWith returns the node starting with the given string func (n *Node[T]) _find(s string) (node *Node[T], equal bool) { defer n.RLock()() if s == "" { return n, true } ch := rune(s[0]) key := n.keyByChar[ch] if key == "" { return nil, false } offset := len(key) // check our part of the key matches if offset > 1 { for i := 1; i < len(s) && i < offset; i++ { if s[i] != key[i] { return nil, false } } } next := n.keyMap[key] // our key matched, it's equal -- just return the node if offset == len(s) { return next, true } // our key matched, it's longer -- just return the node as a non-equality match if offset >= len(s) { return next, false } // our key matched, but it's shorter -- return what we find for the next portion return next._find(s[offset:]) } //nolint:funlen func (n *Node[T]) _makeNodeP(unlock *sync.UnlockFunc, name string, nodeIfEmpty *Node[T]) *Node[T] { if name == "" { return n } if unlock == nil { rUnlock := n.RLock() n = n._makeNodeP(&rUnlock, name, nodeIfEmpty) rUnlock() return n } ch := rune(name[0]) if n.keyByChar == nil || n.keyMap == nil { n._exclusiveLock(unlock) if n.keyByChar == nil { n.keyByChar = map[rune]string{} } if n.keyMap == nil { n.keyMap = NodeMap[T]{} } } key, ok := n.keyByChar[ch] // no entry for the given character if !ok { // check again with an exclusive lock before creating a new entry n._exclusiveLock(unlock) key, ok = n.keyByChar[ch] if !ok { // no entry for the given character, create one; this is all we have to do newNode := nodeIfEmpty if newNode == nil { newNode = &Node[T]{} } n.keyMap[name] = newNode n.keyByChar[ch] = name return newNode } } switch { case key == name: existingNode := n.keyMap[key] if existingNode == nil { // try this again, something changed between read checks and exclusive lock return n._makeNodeP(unlock, name, nodeIfEmpty) } return existingNode case strings.HasPrefix(key, name): // existing key is longer than my key, we can just use the existing and make a new sub-entry for the longer key n._exclusiveLock(unlock) existingNode := n.keyMap[key] if existingNode == nil { // try this again, something changed between read checks and exclusive lock return n._makeNodeP(unlock, name, nodeIfEmpty) } delete(n.keyMap, key) newNode := nodeIfEmpty if newNode == nil { newNode = &Node[T]{} } n.keyMap[name] = newNode n.keyByChar[ch] = name next := key[len(name):] _ = newNode._makeNodeP(nil, next, existingNode) return newNode case strings.HasPrefix(name, key): // existing key is shorter than my key, we can just take the substring to remove the // existing string prefix and set the new node as a child of the existing node n._exclusiveLock(unlock) existingNode := n.keyMap[key] if existingNode == nil { // try this again, something changed between read checks and exclusive lock return n._makeNodeP(unlock, name, nodeIfEmpty) } next := name[len(key):] return existingNode._makeNodeP(nil, next, nodeIfEmpty) default: // neither the existing key nor my key contains a prefix of the other, so we find // the longest common prefix and split BOTH entries as children of a new entry with this common prefix n._exclusiveLock(unlock) commonLength := 1 // the first character already matches minLength := len(key) if len(name) < minLength { minLength = len(name) } for ; commonLength < minLength; commonLength++ { if key[commonLength] != name[commonLength] { break } } existingNode := n.keyMap[key] if existingNode == nil { // try this again, something changed between read checks and exclusive lock return n._makeNodeP(unlock, name, nodeIfEmpty) } delete(n.keyMap, key) newNode := nodeIfEmpty if newNode == nil { newNode = &Node[T]{} } parentNode := &Node[T]{} parentNode._makeNodeP(nil, key[commonLength:], existingNode) parentNode._makeNodeP(nil, name[commonLength:], newNode) common := key[:commonLength] n.keyMap[common] = parentNode n.keyByChar[ch] = common return newNode } } func (n *Node[T]) _exclusiveLock(unlocker *sync.UnlockFunc) { if !n.IsExclusiveLock(*unlocker) { (*unlocker)() *unlocker = n.Lock() } } anchore-go-sync-da8a02e/internal/index/key_split_test.go000066400000000000000000000071411516547376300235340ustar00rootroot00000000000000package index import ( "encoding/json" "fmt" "testing" "github.com/stretchr/testify/require" ) func Test_KeySplitIndex_Create(t *testing.T) { fi := KeySplitIndex[int]{} fi.Set("one", 1) requireEqualValues(t, &fi, m{ "one": 1, }) fi.Set("onesie", 101) requireEqualValues(t, &fi, m{ "one": m{ // there is a value here "sie": 101, }, }) fi.Set("apple", 99) requireEqualValues(t, &fi, m{ "one": m{ "sie": 101, }, "apple": 99, }) fi.Set("alice", 999) requireEqualValues(t, &fi, m{ "one": m{ "sie": 101, }, "a": m{ "pple": 99, "lice": 999, }, }) } type m = map[string]any func requireEqualValues[T comparable](t *testing.T, value *KeySplitIndex[T], expected m) { _requireNodeValues(t, &value.Node, "", expected) } func _requireNodeValues[T comparable](t *testing.T, value *Node[T], path string, expected any) { switch expected := expected.(type) { case T: if value.value != expected { t.Fatalf("values not equal: %v != %v", value.value, value) } case m: if len(value.keyMap) != len(expected) { t.Fatalf("number of children not equal: %v != %v", len(value.keyMap), len(expected)) } for k, v := range expected { v2, ok := value.keyMap[k] if !ok { t.Fatalf("missing key: %s", k) } else { _requireNodeValues(t, v2, path, v) } } default: panic(fmt.Errorf("invalid type: %#v", expected)) } } func Test_KeySplitIndex_Get(t *testing.T) { fi := KeySplitIndex[int]{} const ( one = 1 two = 2 once = 11 onesie = 11 ) fi.Set("one", one) require.Equal(t, one, fi.Get("one")) fi.Set("two", two) require.Equal(t, one, fi.Get("one")) require.Equal(t, two, fi.Get("two")) fi.Set("once", once) require.Equal(t, one, fi.Get("one")) require.Equal(t, two, fi.Get("two")) require.Equal(t, once, fi.Get("once")) fi.Set("onesie", onesie) require.Equal(t, one, fi.Get("one")) require.Equal(t, two, fi.Get("two")) require.Equal(t, once, fi.Get("once")) require.Equal(t, onesie, fi.Get("onesie")) fi.Set("apple", 1000) require.Equal(t, one, fi.Get("one")) require.Equal(t, two, fi.Get("two")) require.Equal(t, once, fi.Get("once")) require.Equal(t, onesie, fi.Get("onesie")) fi.Set("alice", 1001) require.Equal(t, one, fi.Get("one")) require.Equal(t, two, fi.Get("two")) require.Equal(t, once, fi.Get("once")) require.Equal(t, onesie, fi.Get("onesie")) require.ElementsMatch(t, []int{one, once, onesie}, fi.ByPrefix("o")) require.ElementsMatch(t, []int{one, once, onesie}, fi.ByPrefix("on")) require.ElementsMatch(t, []int{one, onesie}, fi.ByPrefix("one")) require.ElementsMatch(t, []int{onesie}, fi.ByPrefix("ones")) require.ElementsMatch(t, []int{onesie}, fi.ByPrefix("onesi")) require.ElementsMatch(t, []int{onesie}, fi.ByPrefix("onesie")) require.ElementsMatch(t, nil, fi.ByPrefix("onesies")) require.ElementsMatch(t, []int{two}, fi.ByPrefix("t")) require.ElementsMatch(t, []int{two}, fi.ByPrefix("tw")) require.ElementsMatch(t, []int{two}, fi.ByPrefix("two")) require.ElementsMatch(t, []int{}, fi.ByPrefix("twos")) require.Equal(t, 0, fi.Get("invalid")) // returns the zero value } func Test_KeySplitIndex_serialization(t *testing.T) { index1 := KeySplitIndex[int]{} const ( one = 1 two = 2 once = 11 onesie = 111 ) index1.Set("one", one) index1.Set("two", two) index1.Set("once", once) index1.Set("onesie", onesie) serialized, err := json.Marshal(&index1) require.NoError(t, err) require.JSONEq(t, `{"one":1,"two":2,"once":11,"onesie":111}`, string(serialized)) var index2 KeySplitIndex[int] err = json.Unmarshal(serialized, &index2) require.NoError(t, err) require.Equal(t, &index1, &index2) } anchore-go-sync-da8a02e/internal/index/prefix_suffix.go000066400000000000000000000022201516547376300233440ustar00rootroot00000000000000package index // PrefixSuffix index combines two KeySplitIndexes to simplify indexing and searching by prefix and suffix type PrefixSuffix[T any] struct { forwardIndex KeySplitIndex[T] reverseIndex KeySplitIndex[T] } func (f *PrefixSuffix[T]) Set(key string, value T) { f.forwardIndex.Set(key, value) f.reverseIndex.Set(reverse(key), value) } func (f *PrefixSuffix[T]) Remove(key string) { f.forwardIndex.Remove(key) f.reverseIndex.Remove(reverse(key)) } func (f *PrefixSuffix[T]) Update(key string, fn NodeUpdateFunc[T]) { f.forwardIndex.Update(key, fn) f.reverseIndex.Update(reverse(key), fn) } func (f *PrefixSuffix[T]) Get(key string) T { return f.forwardIndex.Get(key) } func (f *PrefixSuffix[T]) Contains(key string) bool { return f.forwardIndex.Contains(key) } func (f *PrefixSuffix[T]) ByPrefix(prefix string) []T { return f.forwardIndex.ByPrefix(prefix) } func (f *PrefixSuffix[T]) BySuffix(suffix string) []T { return f.reverseIndex.ByPrefix(reverse(suffix)) } func reverse(s string) string { runes := []rune(s) for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 { runes[i], runes[j] = runes[j], runes[i] } return string(runes) } anchore-go-sync-da8a02e/internal/index/prefix_suffix_test.go000066400000000000000000000014131516547376300244060ustar00rootroot00000000000000package index import ( "testing" "github.com/stretchr/testify/require" ) func Test_PrefixSuffix(t *testing.T) { i := PrefixSuffix[int]{} i.Set("one", 1) i.Set("once", 11) i.Set("onesie", 111) i.Set("two", 2) i.Set("done", 99) require.Equal(t, 1, i.Get("one")) require.Equal(t, 11, i.Get("once")) require.Equal(t, 111, i.Get("onesie")) require.Equal(t, 2, i.Get("two")) require.ElementsMatch(t, i.ByPrefix("on"), []int{1, 11, 111}) require.ElementsMatch(t, i.BySuffix("e"), []int{1, 11, 111, 99}) require.ElementsMatch(t, i.BySuffix(""), []int{1, 2, 11, 111, 99}) require.ElementsMatch(t, i.BySuffix("one"), []int{1, 99}) require.ElementsMatch(t, i.BySuffix("sie"), []int{111}) } func Test_reverse(t *testing.T) { require.Equal(t, "case", reverse("esac")) } anchore-go-sync-da8a02e/internal/stats/000077500000000000000000000000001516547376300201675ustar00rootroot00000000000000anchore-go-sync-da8a02e/internal/stats/stats.go000066400000000000000000000052651516547376300216640ustar00rootroot00000000000000package stats import ( "sync" "github.com/anchore/go-sync/internal/atomic" ) type Stat uint32 // Stats provides some additional statistics for each node type Stats interface { // Add adds the value to the given stat line Add(stat Stat, delta float64) // Set sets the value for the given stat line Set(stat Stat, value float64) // Get gets the value of the given stat line Get(stat Stat) (value float64) } // ---------- statsSlice implementation ---------- type statsSlice []atomic.Float64 var _ Stats = (*statsSlice)(nil) func NewStats(stats ...Stat) Stats { maxStatNum := -1 for _, s := range stats { if int(s) > maxStatNum { maxStatNum = int(s) } } if maxStatNum < 0 { panic("no stats provided") } s := make(statsSlice, maxStatNum+1) return &s } func (s *statsSlice) Add(stat Stat, value float64) { if s == nil || int(stat) >= len(*s) { return } (&(*s)[stat]).Add(value) } func (s *statsSlice) Set(stat Stat, value float64) { if s == nil || int(stat) >= len(*s) { return } (&(*s)[stat]).Store(value) } func (s *statsSlice) Get(stat Stat) (value float64) { if s == nil || int(stat) >= len(*s) { return 0 } return (&(*s)[stat]).Load() } // --- statsMap --- type statsMap struct { sync.RWMutex stats map[Stat]float64 } var _ Stats = (*statsMap)(nil) func (s *statsMap) Add(stat Stat, value float64) { s.Lock() defer s.Unlock() if s.stats == nil { s.stats = map[Stat]float64{} } s.stats[stat] += value } func (s *statsMap) Set(stat Stat, value float64) { s.Lock() defer s.Unlock() if s.stats == nil { s.stats = map[Stat]float64{} } s.stats[stat] = value } func (s *statsMap) Get(stat Stat) (value float64) { s.RLock() defer s.RUnlock() if s.stats == nil { return 0 } return s.stats[stat] } // --- statsAtomic --- type statsAtomic struct { sync.RWMutex stats map[Stat]*atomic.Float64 } var _ Stats = (*statsAtomic)(nil) func (s *statsAtomic) _get(stat Stat) *atomic.Float64 { s.RLock() rUnlock := s.RUnlock if s.stats == nil { rUnlock() s.Lock() unlock := s.Unlock if s.stats == nil { s.stats = map[Stat]*atomic.Float64{} } unlock() s.RLock() rUnlock = s.RUnlock } v := s.stats[stat] if v == nil { rUnlock() s.Lock() unlock := s.Unlock v = s.stats[stat] if v == nil { v = &atomic.Float64{} s.stats[stat] = v } unlock() } else { rUnlock() } return v } func (s *statsAtomic) Add(stat Stat, delta float64) { s._get(stat).Add(delta) } func (s *statsAtomic) Set(stat Stat, value float64) { s._get(stat).Store(value) } func (s *statsAtomic) Get(stat Stat) (value float64) { s.RLock() defer s.RUnlock() if s.stats == nil { return 0 } v := s.stats[stat] if v != nil { return v.Load() } return 0 } anchore-go-sync-da8a02e/internal/stats/stats_test.go000066400000000000000000000010171516547376300227120ustar00rootroot00000000000000package stats import ( "testing" "github.com/stretchr/testify/require" ) func Test_Stats(t *testing.T) { const ( stat1 Stat = iota stat2 ) s := NewStats( stat1, stat2, ) s.Set(stat1, 1) require.Equal(t, 1.0, s.Get(stat1)) s.Set(stat2, 2) require.Equal(t, 1.0, s.Get(stat1)) require.Equal(t, 2.0, s.Get(stat2)) s.Add(stat2, 6) require.Equal(t, 1.0, s.Get(stat1)) require.Equal(t, 8.0, s.Get(stat2)) s.Add(stat1, -2) require.Equal(t, -1.0, s.Get(stat1)) require.Equal(t, 8.0, s.Get(stat2)) } anchore-go-sync-da8a02e/internal/stats/tracked.go000066400000000000000000000042101516547376300221300ustar00rootroot00000000000000package stats import ( "github.com/anchore/go-sync/internal/atomic" ) type Tracked[T int | int32 | int64] struct { count atomic.Int64 val atomic.Int64 max atomic.Int64 min atomic.Int64 } func (t *Tracked[T]) Add(val T) { // each time a value is added, track a count so we can provide an average t.count.Add(1) c := t.val.Add(int64(val)) m := t.max.Load() for c > m { if !t.max.CompareAndSwap(m, c) { m = t.max.Load() continue } break } m = t.min.Load() for c < m { if !t.min.CompareAndSwap(m, c) { m = t.min.Load() continue } break } } func (t *Tracked[T]) Incr() func() { t.Add(1) return t.Decr } func (t *Tracked[T]) Decr() { t.Add(-1) } func (t *Tracked[T]) Val() T { return T(t.val.Load()) } func (t *Tracked[T]) Max() T { return T(t.max.Load()) } func (t *Tracked[T]) Min() T { return T(t.min.Load()) } func (t *Tracked[T]) Avg() float64 { return float64(t.val.Load()) / float64(t.count.Load()) } // --- TrackedFloat --- type TrackedFloat[T int | int32 | int64 | float32 | float64] struct { count atomic.Float64 val atomic.Float64 max atomic.Float64 min atomic.Float64 maxSet func(val T) } func (t *TrackedFloat[T]) Add(val T) { // each time a value is added, track a count so we can provide an average t.count.Add(1) c := t.val.Add(float64(val)) m := t.max.Load() for c > m { if !t.max.CompareAndSwap(m, c) { m = t.max.Load() continue } if t.maxSet != nil { t.maxSet(T(c)) } break } m = t.min.Load() for c < m { if !t.min.CompareAndSwap(m, c) { m = t.min.Load() continue } break } } func (t *TrackedFloat[T]) Incr() func() { t.Add(1) return t.Decr } func (t *TrackedFloat[T]) Decr() { t.Add(-1) } func (t *TrackedFloat[T]) Val() T { return T(t.val.Load()) } func (t *TrackedFloat[T]) Max() T { return T(t.max.Load()) } func (t *TrackedFloat[T]) Min() T { return T(t.min.Load()) } func (t *TrackedFloat[T]) Avg() float64 { return t.val.Load() / t.count.Load() } func (t *TrackedFloat[T]) OnMaxSet(f func(T)) { if t.maxSet != nil { existing := t.maxSet t.maxSet = func(val T) { existing(val) f(val) } return } t.maxSet = f } anchore-go-sync-da8a02e/list.go000066400000000000000000000062541516547376300165260ustar00rootroot00000000000000package sync import "iter" type List[T comparable] struct { Locking values []T } // ----------------- Collection functions ----------------- func (s *List[T]) Append(value T) { defer s.Lock()() s.values = append(s.values, value) } func (s *List[T]) Remove(value T) { defer s.Lock()() idx := s.indexOf(value) if idx >= 0 { _, _ = s.removeIndex(idx) } } func (s *List[T]) Contains(value T) bool { defer s.RLock()() return s.indexOf(value) >= 0 } func (s *List[T]) Len() int { defer s.RLock()() return len(s.values) } // ----------------- Queue functions ----------------- func (s *List[T]) Enqueue(value T) { s.Append(value) } func (s *List[T]) Dequeue() (value T, ok bool) { defer s.Lock()() if len(s.values) == 0 { return value, false } value = (s.values)[0] _, _ = s.removeIndex(0) return value, true } // ----------------- Stack functions ----------------- func (s *List[T]) Push(value T) { s.Append(value) } func (s *List[T]) Pop() (value T, ok bool) { defer s.Lock()() last := len(s.values) - 1 if last >= 0 { v := (s.values)[last] s.values = (s.values)[0:last] return v, true } return value, false } func (s *List[T]) Peek() (value T, ok bool) { defer s.RLock()() last := len(s.values) - 1 if last >= 0 { return (s.values)[last], true } return value, false } // ----------------- Iterator functions ----------------- // Seq is an iter.Seq compatible iterator function with a read lock, as such it is not possible to // modify this list during the loop -- use Values() to obtain a copy for those purposes func (s *List[T]) Seq(fn func(value T) bool) { defer s.RLock()() for _, v := range s.values { if !fn(v) { return } } } // ----------------- other utility functions ----------------- // Values returns a slice containing all the values at the time of the call, this should be used // sparingly as it is only a snapshot of the current values func (s *List[T]) Values() []T { defer s.RLock()() return s.copyValues() } // copyValues creates a copy of the values and returns it, without any locking func (s *List[T]) copyValues() []T { out := make([]T, len(s.values)) copy(out, s.values) return out } // Clear removes all values func (s *List[T]) Clear() { defer s.Lock()() s.values = nil } func (s *List[T]) RemoveAll(values iter.Seq[T]) { defer s.Lock()() for value := range values { s.Remove(value) } } func (s *List[T]) Update(updater func(values []T) []T) { defer s.Lock()() s.values = updater(s.values) } // removeIndex removes the index from the list, returns the value and true if a value was removed func (s *List[T]) removeIndex(index int) (value T, ok bool) { last := len(s.values) - 1 if index < 0 || index > last { return value, false } value = (s.values)[index] switch index { case 0: if last == 0 { s.values = nil } else { s.values = (s.values)[1:] } case last: s.values = (s.values)[:last] default: s.values = append((s.values)[0:index], (s.values)[index+1:]...) } return value, true } func (s *List[T]) indexOf(value T) (index int) { for i, v := range s.values { if value == v { return i } } return -1 } var _ interface { Lockable Collection[int] Queue[int] Stack[int] } = (*List[int])(nil) anchore-go-sync-da8a02e/list_test.go000066400000000000000000000027161516547376300175640ustar00rootroot00000000000000package sync import ( "testing" "github.com/stretchr/testify/require" ) func Test_List(t *testing.T) { expected := []int{0, 1, 2, 3, 4, 5} // as a stack: var sl = &List[int]{} var s Stack[int] = sl for _, e := range expected { s.Push(e) } require.Equal(t, expected, sl.Values()) for i := range expected { ev := len(expected) - 1 - i exp := expected[:ev] v, ok := s.Pop() require.True(t, ok) require.Equal(t, expected[ev], v) require.Equal(t, exp, sl.Values()) } // as a List: ls := &List[int]{} for _, v := range expected { ls.Append(v) } require.Equal(t, expected, ls.Values()) for i, e := range expected { exp := expected[i+1:] ls.Remove(e) require.Equal(t, exp, ls.Values()) } // as a queue: sl = &List[int]{} var q Queue[int] = sl for _, e := range expected { q.Enqueue(e) } require.Equal(t, expected, sl.Values()) for i, e := range expected { exp := expected[i+1:] got, ok := q.Dequeue() require.True(t, ok) require.Equal(t, e, got) require.Equal(t, exp, sl.Values()) } // from the middle: sl = &List[int]{} for _, value := range expected { sl.Append(value) } require.Equal(t, expected, sl.Values()) sl.Remove(4) require.Equal(t, []int{0, 1, 2, 3, 5}, sl.Values()) sl.Remove(2) require.Equal(t, []int{0, 1, 3, 5}, sl.Values()) sl.Remove(1) require.Equal(t, []int{0, 3, 5}, sl.Values()) sl.Remove(5) require.Equal(t, []int{0, 3}, sl.Values()) sl.Remove(0) require.Equal(t, []int{3}, sl.Values()) } anchore-go-sync-da8a02e/llms.txt000066400000000000000000000020021516547376300167170ustar00rootroot00000000000000# go-sync A collection of Go synchronization utilities by Anchore. ## Project Overview This Go library provides synchronization primitives and utilities: - `sync.Executor` - A simple executor interface with bounded executor implementation - `sync.Collector` - Interface to concurrently execute tasks and collect results - `sync.List` - Concurrent list, queue, and stack implementation - Various executor implementations (serial, bounded, unbounded, errgroup-based) - Parallel writer utilities - Context management utilities ## Key Files - `executor.go` - Main executor interface and factory functions - `collector.go` - Task collection and concurrent execution - `list.go` - Thread-safe list/queue/stack implementation - `parallel_writer.go` - Utilities for parallel writing operations - `context.go` - Context management helpers ## Status This project is in alpha status with an unstable API that may change. ## Dependencies - Go 1.23.0+ - golang.org/x/sync v0.16.0 - github.com/stretchr/testify v1.11.0 (testing)anchore-go-sync-da8a02e/lockable.go000066400000000000000000000013341516547376300173210ustar00rootroot00000000000000package sync import ( "reflect" "sync" ) type UnlockFunc func() // Lockable implementors provide the ability to RW lock a resource type Lockable interface { Lock() (unlock UnlockFunc) RLock() (unlock UnlockFunc) } // Locking is a utility to add Lockable behavior to a struct type Locking struct { lock sync.RWMutex } var _ Lockable = (*Locking)(nil) func (l *Locking) Lock() (unlock UnlockFunc) { l.lock.Lock() return l.lock.Unlock } func (l *Locking) RLock() (unlock UnlockFunc) { l.lock.RLock() return l.lock.RUnlock } func (l *Locking) IsExclusiveLock(unlockFunc UnlockFunc) (exclusive bool) { unlock := reflect.ValueOf(l.lock.Unlock).Pointer() f := reflect.ValueOf(unlockFunc).Pointer() return unlock == f } anchore-go-sync-da8a02e/parallel_writer.go000066400000000000000000000016601516547376300207370ustar00rootroot00000000000000package sync import ( "context" "errors" "io" "sync" ) type parallelWriter struct { ctx context.Context executor Executor writers []io.Writer } // ParallelWriter returns a writer that writes the contents of each write call in parallel // to all provided writers func ParallelWriter(ctx context.Context, executorName string, writers ...io.Writer) io.Writer { executor := ContextExecutor(&ctx, executorName) return ¶llelWriter{ ctx: ctx, executor: executor, writers: writers, } } func (w *parallelWriter) Write(p []byte) (int, error) { errs := List[error]{} wg := sync.WaitGroup{} wg.Add(len(w.writers)) for _, writer := range w.writers { w.executor.Go(func() { defer wg.Done() _, err := writer.Write(p) if err != nil { errs.Append(err) } }) } wg.Wait() if errs.Len() > 0 { return 0, errors.Join(errs.Values()...) } return len(p), nil } var _ io.Writer = (*parallelWriter)(nil) anchore-go-sync-da8a02e/parallel_writer_test.go000066400000000000000000000046151516547376300220010ustar00rootroot00000000000000package sync import ( "bytes" "context" "sync/atomic" "testing" "github.com/stretchr/testify/require" "github.com/anchore/go-sync/internal/stats" ) func Test_ParallelWriter(t *testing.T) { tests := []struct { name string maxConcurrency int bufferSize int }{ { name: "unbounded concurrency", maxConcurrency: 0, bufferSize: 4, }, { name: "single execution", maxConcurrency: 1, bufferSize: 100, }, { name: "dual execution", maxConcurrency: 2, bufferSize: 4, }, { name: "ten-x execution", maxConcurrency: 10, bufferSize: 4, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { executed := atomic.Int32{} concurrency := stats.Tracked[int]{} buf1 := &bytes.Buffer{} w1 := funcWriter{ fn: func(contents []byte) (int, error) { defer concurrency.Incr()() executed.Add(1) buf1.Write(contents) return len(contents), nil }, } buf2 := &bytes.Buffer{} w2 := funcWriter{ fn: func(contents []byte) (int, error) { defer concurrency.Incr()() executed.Add(1) buf2.Write(contents) return len(contents), nil }, } buf3 := &bytes.Buffer{} w3 := funcWriter{ fn: func(contents []byte) (int, error) { defer concurrency.Incr()() executed.Add(1) buf3.Write(contents) return len(contents), nil }, } contents := "some complicated contents" ctx := SetContextExecutor(context.Background(), "", NewExecutor(test.maxConcurrency)) w := ParallelWriter(ctx, "", w1, w2, w3) iterations := 0 for i := 0; i < len(contents); i += test.bufferSize { iterations++ end := i + test.bufferSize if end > len(contents) { end = len(contents) } buf := contents[i:end] n, err := w.Write([]byte(buf)) require.NoError(t, err) require.Equal(t, len(buf), n) } require.Equal(t, 3*iterations, int(executed.Load())) if test.maxConcurrency > 0 { require.LessOrEqual(t, concurrency.Max(), test.maxConcurrency) } else { require.GreaterOrEqual(t, concurrency.Max(), 1) } require.Equal(t, contents, buf1.String()) require.Equal(t, contents, buf2.String()) require.Equal(t, contents, buf3.String()) }) } } type funcWriter struct { fn func([]byte) (int, error) } func (f funcWriter) Write(p []byte) (int, error) { return f.fn(p) } anchore-go-sync-da8a02e/util.go000066400000000000000000000026631516547376300165300ustar00rootroot00000000000000package sync import "iter" // ToSeq converts a slice to an iter.Seq func ToSeq[T any](values []T) iter.Seq[T] { return func(yield func(T) bool) { for _, value := range values { if !yield(value) { return } } } } // ToIndexSeq converts a []T to an iter.Seq2[int,T] where the index is the first parameter func ToIndexSeq[T any](values []T) iter.Seq2[int, T] { return func(yield func(int, T) bool) { for index, value := range values { if !yield(index, value) { return } } } } // ToSlice takes an iter.Seq and returns a slice of the values returned func ToSlice[T any](values iter.Seq[T]) (everything []T) { for v := range values { everything = append(everything, v) } return everything } // ToSeq2 converts a map[K]V to an iter.Seq2[K,V] func ToSeq2[K comparable, V any](values map[K]V) iter.Seq2[K, V] { return func(yield func(K, V) bool) { for key, value := range values { if !yield(key, value) { return } } } } // keyValue is used to for Seq2 and related sequence conversions type keyValue[K, V any] struct { Key K Value V } // toKeyValueIterator converts an iter.Seq2[K,V] to an iter.Seq[keyValue[K,V]] func toKeyValueIterator[From1, From2 any](iterator iter.Seq2[From1, From2]) iter.Seq[keyValue[From1, From2]] { return func(yield func(keyValue[From1, From2]) bool) { for key, value := range iterator { if !yield(keyValue[From1, From2]{Key: key, Value: value}) { return } } } } anchore-go-sync-da8a02e/util_test.go000066400000000000000000000017551516547376300175700ustar00rootroot00000000000000package sync import ( "iter" "testing" "github.com/stretchr/testify/require" ) func Test_ToSeqToSlice(t *testing.T) { expected := []int{10, 11, 12, 13, 14} seq := ToSeq(expected) got := ToSlice(seq) require.EqualValues(t, expected, got) } func Test_ToKeyValueSeq(t *testing.T) { expected := map[string]int{"zero": 0, "one": 1, "two": 2, "three": 3, "four": 4} seq := ToSeq2(expected) got := keyValueSeqToMap(toKeyValueIterator(seq)) require.EqualValues(t, expected, got) } func Test_ToIndexSeq(t *testing.T) { slice := []int{10, 11, 12, 13, 14} expected := map[int]int{ 0: 10, 1: 11, 2: 12, 3: 13, 4: 14, } seq := ToIndexSeq(slice) got := keyValueSeqToMap(toKeyValueIterator(seq)) require.EqualValues(t, expected, got) } // keyValueSeqToMap converts an iter.Seq[KeyValue[K,V]] to a map[K]V func keyValueSeqToMap[K comparable, V any](values iter.Seq[keyValue[K, V]]) map[K]V { out := map[K]V{} for kv := range values { out[kv.Key] = kv.Value } return out }