From 64c05b9a7a9d44b11566fbd04818b491acaed9c7 Mon Sep 17 00:00:00 2001 From: Laurent Demailly Date: Mon, 20 Feb 2023 18:24:47 -0800 Subject: [PATCH] initial implementation, coming from dflag MR and delta actually --- .github/dependabot.yml | 16 +++ .github/workflows/codecov.yml | 21 +++ .github/workflows/codeql-analysis.yml | 67 ++++++++++ .golangci.yml | 176 ++++++++++++++++++++++++++ README.md | 7 +- go.mod | 5 + go.sum | 2 + sets.go | 81 ++++++++++++ sets_test.go | 42 ++++++ 9 files changed, 416 insertions(+), 1 deletion(-) create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/codecov.yml create mode 100644 .github/workflows/codeql-analysis.yml create mode 100644 .golangci.yml create mode 100644 go.mod create mode 100644 go.sum create mode 100644 sets.go create mode 100644 sets_test.go diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..8c2407e --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,16 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates + +version: 2 +updates: + - package-ecosystem: "gomod" + directory: "/" # Location of package manifests + schedule: + interval: "daily" + - package-ecosystem: "github-actions" + directory: "/" + schedule: + # Check for updates to GitHub Actions every week + interval: "weekly" diff --git a/.github/workflows/codecov.yml b/.github/workflows/codecov.yml new file mode 100644 index 0000000..2f85fe3 --- /dev/null +++ b/.github/workflows/codecov.yml @@ -0,0 +1,21 @@ +name: "Code Coverage" + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + coverage: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@master + - uses: actions/setup-go@v3 + with: + go-version: '1.19' + - name: Run test coverage + run: go test -race -coverprofile=coverage.out -covermode=atomic ./... + - uses: codecov/codecov-action@v3 + with: + files: coverage.out diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml new file mode 100644 index 0000000..7ec5538 --- /dev/null +++ b/.github/workflows/codeql-analysis.yml @@ -0,0 +1,67 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL" + +on: + push: + branches: [ main ] + pull_request: + # The branches below must be a subset of the branches above + branches: [ main ] + schedule: + - cron: '42 20 * * 3' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: [ 'go' ] + # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] + # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support + + steps: + - name: Checkout repository + uses: actions/checkout@755da8c3cf115ac066823e79a1e1788f8940201b # pin@v3 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@17573ee1cc1b9d061760f3a006fc4aac4f944fd5 # pin@v2 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + # queries: security-extended,security-and-quality + + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@17573ee1cc1b9d061760f3a006fc4aac4f944fd5 # pin@v2 + + # ℹī¸ Command-line programs to run using the OS shell. + # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun + # If the Autobuild fails above, remove it and uncomment the following three lines. + # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. + # - run: | + # echo "Run, Build Application using script" + # ./location_of_script_within_repo/buildscript.sh + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@17573ee1cc1b9d061760f3a006fc4aac4f944fd5 # pin@v2 diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..86686d3 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,176 @@ +# Config for golanglint-ci + +# output configuration options + +# all available settings of specific linters +linters-settings: + gocritic: + disabled-checks: + - ifElseChain + dupl: + # tokens count to trigger issue, 150 by default + threshold: 100 + exhaustive: + # indicates that switch statements are to be considered exhaustive if a + # 'default' case is present, even if all enum members aren't listed in the + # switch + default-signifies-exhaustive: false + funlen: + lines: 140 + statements: 70 + gocognit: + # minimal code complexity to report, 30 by default (but we recommend 10-20) + min-complexity: 42 + nestif: + # minimal complexity of if statements to report, 5 by default + min-complexity: 4 + gocyclo: + # minimal code complexity to report, 30 by default (but we recommend 10-20) + min-complexity: 30 + godot: + # check all top-level comments, not only declarations + check-all: false + govet: + # report about shadowed variables + check-shadowing: true + # settings per analyzer + settings: + printf: # analyzer name, run `go tool vet help` to see all analyzers + funcs: # run `go tool vet help printf` to see available settings for `printf` analyzer + - (github.com/golangci/golangci-lint/pkg/logutils.Log).Infof + - (github.com/golangci/golangci-lint/pkg/logutils.Log).Warnf + - (github.com/golangci/golangci-lint/pkg/logutils.Log).Errorf + - (github.com/golangci/golangci-lint/pkg/logutils.Log).Fatalf + - (github.com/golangci/golangci-lint/pkg/logutils.Log).Printf + - (github.com/golangci/golangci-lint/pkg/logutils.Log).FErrf + enable-all: true + disable-all: false + depguard: + list-type: blacklist + include-go-root: false + packages: + - github.com/sirupsen/logrus + packages-with-error-message: + # specify an error message to output when a blacklisted package is used + - github.com/sirupsen/logrus: "logging is allowed only by fortio.log" + lll: + # max line length, lines longer will be reported. Default is 120. + # '\t' is counted as 1 character by default, and can be changed with the tab-width option + line-length: 132 + # tab width in spaces. Default to 1. + tab-width: 1 + misspell: + # Correct spellings using locale preferences for US or UK. + # Default is to use a neutral variety of English. + # Setting locale to US will correct the British spelling of 'colour' to 'color'. + locale: US + ignore-words: + - fortio + nakedret: + # make an issue if func has more lines of code than this setting and it has naked returns; default is 30 + max-func-lines: 30 + nolintlint: + require-specific: true + whitespace: + multi-if: false # Enforces newlines (or comments) after every multi-line if statement + multi-func: false # Enforces newlines (or comments) after every multi-line function signature + gofumpt: + # Choose whether or not to use the extra rules that are disabled + # by default + extra-rules: false + + +linters: + disable: + # bad ones: + - musttag + # Deprecated ones: + - scopelint + - golint + - interfacer + - maligned + - varcheck + - structcheck + - nosnakecase + - deadcode + # Weird/bad ones: + - wsl + - nlreturn + - gochecknoinits + - gochecknoglobals + - gomnd + - testpackage + - wrapcheck + - exhaustivestruct + - tagliatelle + - nonamedreturns + - varnamelen + - exhaustruct # seems like a good idea at first but actually a pain and go does have zero values for a reason. +# TODO consider putting these back, when they stop being bugged (ifshort, wastedassign,...) + - paralleltest + - thelper + - forbidigo + - ifshort + - wastedassign + - cyclop + - forcetypeassert + - ireturn + enable-all: true + disable-all: false + # Must not use fast: true in newer golangci-lint or it'll just skip a bunch of linter instead of doing caching like before (!) + fast: false + + +issues: + # Excluding configuration per-path, per-linter, per-text and per-source + exclude-rules: + # Exclude some linters from running on tests files. + - path: _test\.go + linters: + - gocyclo + - errcheck + - dupl + - gosec + - gochecknoinits + - gochecknoglobals + - forcetypeassert + - nosnakecase + - noctx + + # Exclude lll issues for long lines with go:generate + - linters: + - lll + source: "^//go:generate " + - linters: + - goerr113 + text: "do not define dynamic errors" + - linters: + - govet + text: "fieldalignment:" + - linters: + - godox + text: "TODO" + - linters: + - nosnakecase + text: "grpc_|_SERVING|O_" + + # Maximum issues count per one linter. Set to 0 to disable. Default is 50. + max-issues-per-linter: 0 + + # Maximum count of issues with the same text. Set to 0 to disable. Default is 3. + max-same-issues: 0 + +severity: + # Default value is empty string. + # Set the default severity for issues. If severity rules are defined and the issues + # do not match or no severity is provided to the rule this will be the default + # severity applied. Severities should match the supported severity names of the + # selected out format. + # - Code climate: https://docs.codeclimate.com/docs/issues#issue-severity + # - Checkstyle: https://checkstyle.sourceforge.io/property_types.html#severity + # - Github: https://help.github.com/en/actions/reference/workflow-commands-for-github-actions#setting-an-error-message + default-severity: error + + # The default value is false. + # If set to true severity-rules regular expressions become case sensitive. + case-sensitive: false diff --git a/README.md b/README.md index 2f90410..0f2365e 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,7 @@ +[![Go Reference](https://pkg.go.dev/badge/fortio.org/sets.svg)](https://pkg.go.dev/fortio.org/sets) +[![Go Report Card](https://goreportcard.com/badge/fortio.org/sets)](https://goreportcard.com/report/fortio.org/sets) +[![GitHub Release](https://img.shields.io/github/release/fortio/sets.svg?style=flat)](https://github.com/fortio/sets/releases/) +[![Coverage](https://codecov.io/github/fortio/delta/branch/main/graph/badge.svg?token=LONYZDFQ7C)](https://codecov.io/github/fortio/sets) + # sets -Sets and Set operations in golang (ie map[T]struct{} but with helper/nicer names, hiding the struct{}{}...) +Sets and Set operations in golang (ie `map[T]struct{}` but with helper functions/nicer names, hiding the `struct{}{}` etc...) diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..34402cf --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module fortio.org/sets + +go 1.19 + +require fortio.org/assert v1.1.4 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..6da96f9 --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +fortio.org/assert v1.1.4 h1:Za1RaG+OjsTMpQS3J3UCvTF6wc4+IOHCz+jAOU37Y4o= +fortio.org/assert v1.1.4/go.mod h1:039mG+/iYDPO8Ibx8TrNuJCm2T2SuhwRI3uL9nHTTls= diff --git a/sets.go b/sets.go new file mode 100644 index 0000000..bb2cbbf --- /dev/null +++ b/sets.go @@ -0,0 +1,81 @@ +// (Fortio) Sets. +// +// (c) 2023 Fortio Authors +// See LICENSE + +// Sets and Set type and operations in go 1.18+ generics. +// (pending built in support in golang core) +package sets // import "fortio.org/sets" + +import ( + "fmt" + "sort" + "strings" +) + +type Set[T comparable] map[T]struct{} + +// SetFromSlice constructs a Set from a slice. +func FromSlice[T comparable](items []T) Set[T] { + // best pre-allocation if there are no duplicates + res := make(map[T]struct{}, len(items)) + for _, item := range items { + res[item] = struct{}{} + } + return res +} + +func (s Set[T]) Clone() Set[T] { + res := make(Set[T], len(s)) + for k := range s { + res.Add(k) + } + return res +} + +func (s Set[T]) Add(item ...T) { + for _, i := range item { + s[i] = struct{}{} + } +} + +func (s Set[T]) Has(item T) bool { + _, found := s[item] + return found +} + +func (s Set[T]) Remove(item ...T) { + for _, i := range item { + delete(s, i) + } +} + +func New[T comparable](item ...T) Set[T] { + res := make(Set[T], len(item)) + res.Add(item...) + return res +} + +func (s Set[T]) String() string { + keys := make([]string, 0, len(s)) + for k := range s { + keys = append(keys, fmt.Sprintf("%v", k)) + } + sort.Strings(keys) + return strings.Join(keys, ",") +} + +// RemoveCommon removes elements from both sets that are in both, +// leaving only the delta. Useful for Notifier on Set so that +// oldValue has what has been removed and newValue has what has been added. +func RemoveCommon[T comparable](a, b Set[T]) { + if len(a) > len(b) { + a, b = b, a + } + for e := range a { + if _, found := b[e]; found { + delete(a, e) + delete(b, e) + } + } +} diff --git a/sets_test.go b/sets_test.go new file mode 100644 index 0000000..31e6fd5 --- /dev/null +++ b/sets_test.go @@ -0,0 +1,42 @@ +// Copyright (c) Fortio Authors, All Rights Reserved +// See LICENSE for licensing terms. (Apache-2.0) + +package sets_test + +import ( + "testing" + + "fortio.org/assert" + "fortio.org/sets" +) + +func TestSetToString(t *testing.T) { + s := sets.Set[string]{"z": {}, "a": {}, "c": {}, "b": {}} + assert.Equal(t, "a,b,c,z", s.String()) +} + +func TestArrayToSet(t *testing.T) { + a := []string{"z", "a", "c", "b"} + s := sets.FromSlice(a) + assert.Equal(t, "a,b,c,z", s.String()) +} + +func TestRemoveCommon(t *testing.T) { + setA := sets.New("a", "b", "c", "d") + setB := sets.New("b", "d", "e", "f", "g") + setAA := setA.Clone() + setBB := setB.Clone() + sets.RemoveCommon(setAA, setBB) + assert.Equal(t, "a,c", setAA.String()) // removed + assert.Equal(t, "e,f,g", setBB.String()) // added + // Swap order to exercise the optimization on length of iteration + // also check clone is not modifying the original etc + setAA = setB.Clone() // putting B in AA on purpose and vice versa + setBB = setA.Clone() + sets.RemoveCommon(setAA, setBB) + assert.Equal(t, "a,c", setBB.String()) + assert.Equal(t, "e,f,g", setAA.String()) + assert.True(t, setBB.Has("c")) + setBB.Remove("c") + assert.False(t, setBB.Has("c")) +}