diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index 610bdcd..e162c0c 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -3,53 +3,50 @@ name: Benchmark on: push: branches: - - "master" - - "main" - paths-ignore: - - "**.md" - - LICENSE - - ".github/ISSUE_TEMPLATE/*.yml" - - ".github/dependabot.yml" + - master + - main + paths: + - "**" + - "!docs/**" + - "!**.md" pull_request: - branches: - - "*" - paths-ignore: - - "**.md" - - LICENSE - - ".github/ISSUE_TEMPLATE/*.yml" - - ".github/dependabot.yml" + paths: + - "**" + - "!docs/**" + - "!**.md" jobs: Compare: runs-on: ubuntu-latest steps: - - name: Install Go - uses: actions/setup-go@v5 - with: - go-version: 1.20.x + - name: Fetch Repository + uses: actions/checkout@v4 - - name: Fetch Repository - uses: actions/checkout@v4 + - name: Install Go + uses: actions/setup-go@v5 + with: + # NOTE: Keep this in sync with the version from go.mod + go-version: "1.21.x" - - name: Run Benchmark - run: set -o pipefail; go test ./... -benchmem -run=^$ -bench . | tee output.txt + - name: Run Benchmark + run: set -o pipefail; go test ./... -benchmem -run=^$ -bench . | tee output.txt - - name: Get Previous Benchmark Results - uses: actions/cache@v4 - with: - path: ./cache - key: ${{ runner.os }}-benchmark + - name: Get Previous Benchmark Results + uses: actions/cache@v4 + with: + path: ./cache + key: ${{ runner.os }}-benchmark - - name: Save Benchmark Results - uses: benchmark-action/github-action-benchmark@v1 - with: - tool: 'go' - output-file-path: output.txt - github-token: ${{ secrets.BENCHMARK_TOKEN }} - benchmark-data-dir-path: 'benchmarks' - fail-on-alert: true - comment-on-alert: ${{ github.event_name == 'push' || github.event_name == 'workflow_dispatch' }} - # Enable Job Summary for PRs - deactivated because of issues - #summary-always: ${{ github.event_name != 'push' && github.event_name != 'workflow_dispatch' }} - auto-push: ${{ github.event_name == 'push' || github.event_name == 'workflow_dispatch' }} - save-data-file: ${{ github.event_name == 'push' || github.event_name == 'workflow_dispatch' }} + - name: Save Benchmark Results + uses: benchmark-action/github-action-benchmark@v1 + with: + tool: 'go' + output-file-path: output.txt + github-token: ${{ secrets.BENCHMARK_TOKEN }} + benchmark-data-dir-path: 'benchmarks' + fail-on-alert: true + comment-on-alert: ${{ github.event_name == 'push' || github.event_name == 'workflow_dispatch' }} + # Enable Job Summary for PRs - deactivated because of issues + #summary-always: ${{ github.event_name != 'push' && github.event_name != 'workflow_dispatch' }} + auto-push: ${{ github.event_name == 'push' || github.event_name == 'workflow_dispatch' }} + save-data-file: ${{ github.event_name == 'push' || github.event_name == 'workflow_dispatch' }} diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml index 99dd926..5c37ea3 100644 --- a/.github/workflows/linter.yml +++ b/.github/workflows/linter.yml @@ -1,32 +1,43 @@ -name: Golangci Lint Check +name: golangci-lint on: push: branches: - - "master" - - "main" - paths-ignore: - - "**.md" - - LICENSE - - ".github/ISSUE_TEMPLATE/*.yml" - - ".github/dependabot.yml" + - master + - main + paths: + - "**" + - "!docs/**" + - "!**.md" pull_request: - branches: - - "*" - paths-ignore: - - "**.md" - - LICENSE - - ".github/ISSUE_TEMPLATE/*.yml" - - ".github/dependabot.yml" + paths: + - "**" + - "!docs/**" + - "!**.md" + +permissions: + # Required: allow read access to the content for analysis. + contents: read + # Optional: allow read access to pull request. Use with `only-new-issues` option. + pull-requests: read + # Optional: Allow write access to checks to allow the action to annotate code in the PR. + checks: write jobs: - Golint: + golangci: + name: lint runs-on: ubuntu-latest steps: - - name: Fetch Repository - uses: actions/checkout@v4 + - uses: actions/checkout@v4 + + - uses: actions/setup-go@v5 + with: + # NOTE: Keep this in sync with the version from go.mod + go-version: "1.21.x" + cache: false - - name: Run Golint - uses: reviewdog/action-golangci-lint@v2 + - name: golangci-lint + uses: golangci/golangci-lint-action@v4 with: - golangci_lint_flags: "--tests=false" + # NOTE: Keep this in sync with the version from .golangci.yml + version: v1.57.1 \ No newline at end of file diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml deleted file mode 100644 index c6715a7..0000000 --- a/.github/workflows/security.yml +++ /dev/null @@ -1,42 +0,0 @@ -name: Gosec Security Scan - -on: - push: - branches: - - "master" - - "main" - paths-ignore: - - "**.md" - - LICENSE - - ".github/ISSUE_TEMPLATE/*.yml" - - ".github/dependabot.yml" - pull_request: - branches: - - "*" - paths-ignore: - - "**.md" - - LICENSE - - ".github/ISSUE_TEMPLATE/*.yml" - - ".github/dependabot.yml" - -jobs: - gosec-scan: - runs-on: ubuntu-latest - env: - GO111MODULE: on - steps: - - name: Fetch Repository - uses: actions/checkout@v4 - - - name: Install Go - uses: actions/setup-go@v5 - with: - go-version: 'oldstable' - check-latest: true - cache: false - - - name: Install Gosec - run: go install github.com/securego/gosec/v2/cmd/gosec@latest - - - name: Run Gosec - run: gosec -exclude-dir=internal ./... diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2506993..d449bbd 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -18,8 +18,8 @@ jobs: Build: strategy: matrix: - go-version: [1.20.x, 1.21.x, 1.22.x] - platform: [ubuntu-latest, windows-latest, macos-latest] + go-version: [1.21.x, 1.22.x] + platform: [ubuntu-latest, windows-latest, macos-latest, macos-14] runs-on: ${{ matrix.platform }} steps: - name: Fetch Repository @@ -30,9 +30,20 @@ jobs: with: go-version: ${{ matrix.go-version }} - - name: Run Test - uses: nick-fields/retry@v3 + - name: Install gotestsum + run: go install gotest.tools/gotestsum@latest + + - name: Test + run: gotestsum -f testname -- ./... -race -count=1 -shuffle=on + + - name: Test + run: gotestsum -f testname -- ./... -race -count=1 -coverprofile=coverage.txt -covermode=atomic -shuffle=on + + - name: Upload coverage reports to Codecov + if: ${{ matrix.platform == 'ubuntu-latest' && matrix.go-version == '1.22.x' }} + uses: codecov/codecov-action@v4.1.1 with: - max_attempts: 3 - timeout_minutes: 15 - command: go test ./... -v -race -count=1 + token: ${{ secrets.CODECOV_TOKEN }} + file: ./coverage.txt + flags: unittests + slug: gofiber/utils \ No newline at end of file diff --git a/.github/workflows/vulncheck.yml b/.github/workflows/vulncheck.yml deleted file mode 100644 index a36018e..0000000 --- a/.github/workflows/vulncheck.yml +++ /dev/null @@ -1,38 +0,0 @@ -name: Vulnerability Check - -on: - push: - branches: - - master - - main - paths: - - "**" - - "!docs/**" - - "!**.md" - pull_request: - paths: - - "**" - - "!docs/**" - - "!**.md" - -jobs: - govulncheck-check: - runs-on: ubuntu-latest - env: - GO111MODULE: on - steps: - - name: Fetch Repository - uses: actions/checkout@v4 - - - name: Install Go - uses: actions/setup-go@v5 - with: - go-version: "stable" - check-latest: true - cache: false - - - name: Install Govulncheck - run: go install golang.org/x/vuln/cmd/govulncheck@latest - - - name: Run Govulncheck - run: govulncheck ./... diff --git a/.gitignore b/.gitignore index 119b111..fe2fa28 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ # Output of the go coverage tool, specifically when used with LiteIDE *.out +coverage.html # IDE files .vscode diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..91566db --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,383 @@ +# v1.2.0. Created based on golangci-lint v1.57.1 + +run: + timeout: 5m + modules-download-mode: readonly + allow-serial-runners: true + +output: + sort-results: true + uniq-by-line: false + +linters-settings: + depguard: + rules: + all: + list-mode: lax + deny: + - pkg: "flag" + desc: '`flag` package is only allowed in main.go' + - pkg: "log" + desc: 'logging is provided by `pkg/log`' + - pkg: "io/ioutil" + desc: '`io/ioutil` package is deprecated, use the `io` and `os` package instead' + # TODO: Prevent using these without a reason + # - pkg: "reflect" + # desc: '`reflect` package is dangerous to use' + # - pkg: "unsafe" + # desc: '`unsafe` package is dangerous to use' + + errcheck: + check-type-assertions: true + check-blank: true + disable-default-exclusions: true + exclude-functions: + - '(*bytes.Buffer).Write' # always returns nil error + - '(*github.com/valyala/bytebufferpool.ByteBuffer).Write' # always returns nil error + - '(*github.com/valyala/bytebufferpool.ByteBuffer).WriteByte' # always returns nil error + - '(*github.com/valyala/bytebufferpool.ByteBuffer).WriteString' # always returns nil error + + errchkjson: + report-no-exported: true + + exhaustive: + check-generated: true + default-signifies-exhaustive: true + + forbidigo: + forbid: + - ^print(ln)?$ + - ^fmt\.Print(f|ln)?$ + - ^http\.Default(Client|ServeMux|Transport)$ + # TODO: Eventually enable these patterns + # - ^panic$ + # - ^time\.Sleep$ + analyze-types: true + + gci: + sections: + - standard + - prefix(github.com/gofiber/utils) + - default + - blank + - dot + # - alias + custom-order: true + + goconst: + numbers: true + + gocritic: + # TODO: Uncomment the following lines + enabled-tags: + - diagnostic + # - style + # - performance + # - experimental + # - opinionated + disabled-checks: + - ifElseChain + settings: + captLocal: + paramsOnly: false + elseif: + skipBalanced: false + underef: + skipRecvDeref: false + # NOTE: Set this option to false if other projects rely on this project's code + # unnamedResult: + # checkExported: false + + gofumpt: + module-path: github.com/gofiber/utils + extra-rules: true + + gosec: + excludes: + - G104 # TODO: Enable this again. Mostly provided by errcheck + config: + global: + # show-ignored: true # TODO: Enable this + audit: true + + govet: + enable-all: true + disable: + - fieldalignment + - shadow + + grouper: + # const-require-grouping: true # TODO: Enable this + import-require-single-import: true + import-require-grouping: true + # var-require-grouping: true # TODO: Conflicts with gofumpt + + loggercheck: + require-string-key: true + no-printf-like: true + + misspell: + locale: US + + nolintlint: + require-explanation: true + require-specific: true + + nonamedreturns: + report-error-in-defer: true + + perfsprint: + err-error: true + + predeclared: + q: true + + promlinter: + strict: true + + # TODO: Enable this + # reassign: + # patterns: + # - '.*' + + revive: + enable-all-rules: true + rules: + # Provided by gomnd linter + - name: add-constant + disabled: true + - name: argument-limit + disabled: true + # Provided by bidichk + - name: banned-characters + disabled: true + - name: cognitive-complexity + disabled: true + - name: comment-spacings + arguments: + - nolint + disabled: true # TODO: Do not disable + - name: cyclomatic + disabled: true + # TODO: Enable this check. Currently disabled due to upstream bug. + # - name: enforce-repeated-arg-type-style + # arguments: + # - short + - name: enforce-slice-style + arguments: + - make + disabled: true # TODO: Do not disable + - name: exported + disabled: true + - name: file-header + disabled: true + - name: function-result-limit + arguments: [3] + - name: function-length + disabled: true + - name: line-length-limit + disabled: true + - name: max-public-structs + disabled: true + - name: modifies-parameter + disabled: true + - name: nested-structs + disabled: true # TODO: Do not disable + - name: package-comments + disabled: true + - name: optimize-operands-order + disabled: true + - name: unchecked-type-assertion + disabled: true # TODO: Do not disable + - name: unhandled-error + arguments: ['bytes\.Buffer\.Write'] + + stylecheck: + checks: + - all + - -ST1000 + - -ST1020 + - -ST1021 + - -ST1022 + + tagalign: + strict: true + + tagliatelle: + case: + rules: + json: snake + + tenv: + all: true + + testifylint: + enable-all: true + + testpackage: + skip-regexp: "^$" + + unparam: + # NOTE: Set this option to false if other projects rely on this project's code + check-exported: false + + unused: + # TODO: Uncomment these two lines + # parameters-are-used: false + # local-variables-are-used: false + # NOTE: Set these options to true if other projects rely on this project's code + field-writes-are-uses: true + # exported-is-used: true # TODO: Fix issues with this option (upstream) + exported-fields-are-used: true + + usestdlibvars: + http-method: true + http-status-code: true + time-weekday: false # TODO: Set to true + time-month: false # TODO: Set to true + time-layout: false # TODO: Set to true + crypto-hash: true + default-rpc-path: true + os-dev-null: true + sql-isolation-level: true + tls-signature-scheme: true + constant-kind: true + syslog-priority: true + + wrapcheck: + ignorePackageGlobs: + - github.com/gofiber/utils/* + - github.com/valyala/fasthttp + +issues: + exclude-use-default: false + exclude-case-sensitive: true + max-issues-per-linter: 0 + max-same-issues: 0 + exclude-dirs: + - internal # TODO: Do not ignore interal packages + exclude-rules: + - linters: + - goerr113 + text: 'do not define dynamic errors, use wrapped static errors instead*' + - path: log/.*\.go + linters: + - depguard + # Exclude some linters from running on tests files. + - path: _test\.go + linters: + - bodyclose + # fix: true + +linters: + enable: + - asasalint + - asciicheck + - bidichk + - bodyclose + - containedctx + - contextcheck + # - cyclop + - decorder + - depguard + - dogsled + # - dupl + # - dupword # TODO: Enable + - durationcheck + - errcheck + - errchkjson + - errname + - errorlint + - execinquery + - exhaustive + # - exhaustivestruct + # - exhaustruct + - exportloopref + - forbidigo + - forcetypeassert + # - funlen + # - gci # TODO: Enable + - ginkgolinter + # - gocheckcompilerdirectives # TODO: Enable + # - gochecknoglobals # TODO: Enable + # - gochecknoinits # TODO: Enable + - gochecksumtype + # - gocognit + # - goconst # TODO: Enable + - gocritic + # - gocyclo + # - godot + # - godox + - goerr113 + - gofmt + - gofumpt + # - goheader + # - goimports + # - golint + # - gomnd # TODO: Enable + - gomoddirectives + # - gomodguard + - goprintffuncname + - gosec + - gosimple + # - gosmopolitan # TODO: Enable + - govet + - grouper + # - ifshort # TODO: Enable + # - importas + # - inamedparam + - ineffassign + # - interfacebloat + # - interfacer + # - ireturn + # - lll + - loggercheck + # - maintidx + - makezero + # - maligned + - mirror + - misspell + - musttag + - nakedret + # - nestif + - nilerr + - nilnil + # - nlreturn + - noctx + - nolintlint + - nonamedreturns + - nosprintfhostport + # - paralleltest # TODO: Enable + - perfsprint + # - prealloc + - predeclared + - promlinter + - protogetter + - reassign + - revive + - rowserrcheck + # - scopelint # TODO: Enable + - sloglint + - spancheck + - sqlclosecheck + - staticcheck + - stylecheck + # - tagalign # TODO: Enable + - tagliatelle + - tenv + - testableexamples + - testifylint + # - testpackage # TODO: Enable + - thelper + - tparallel + - typecheck + - unconvert + - unparam + - unused + - usestdlibvars + # - varnamelen + # - wastedassign # TODO: Enable + - whitespace + #- wrapcheck + # - wsl + - zerologlint diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..212bb2d --- /dev/null +++ b/Makefile @@ -0,0 +1,44 @@ +## help: 💡 Display available commands +.PHONY: help +help: + @echo '⚡️ GoFiber/Fiber Development:' + @sed -n 's/^##//p' ${MAKEFILE_LIST} | column -t -s ':' | sed -e 's/^/ /' + +## audit: 🚀 Conduct quality checks +.PHONY: audit +audit: + go mod verify + go vet ./... + go run golang.org/x/vuln/cmd/govulncheck@latest ./... + +## benchmark: 📈 Benchmark code performance +.PHONY: benchmark +benchmark: + go test ./... -benchmem -bench=. -run=^Benchmark_$ + +## coverage: ☂️ Generate coverage report +.PHONY: coverage +coverage: + go run gotest.tools/gotestsum@latest -f testname -- ./... -race -count=1 -coverprofile=coverage.out -covermode=atomic + go tool cover -html=coverage.out -o coverage.html + open coverage.html & + +## format: 🎨 Fix code format issues +.PHONY: format +format: + go run mvdan.cc/gofumpt@latest -w -l . + +## lint: 🚨 Run lint checks +.PHONY: lint +lint: + go run github.com/golangci/golangci-lint/cmd/golangci-lint@v1.57.1 run ./... + +## test: 🚦 Execute all tests +.PHONY: test +test: + go run gotest.tools/gotestsum@latest -f testname -- ./... -race -count=1 -shuffle=on + +## tidy: 📌 Clean and tidy dependencies +.PHONY: tidy +tidy: + go mod tidy -v diff --git a/README.md b/README.md index 6e59688..f7ad997 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ # Fiber Utils ![Release](https://img.shields.io/github/release/gofiber/utils.svg) -[![Discord](https://img.shields.io/badge/discord-join%20channel-7289DA)](https://gofiber.io/discord) ![Test](https://github.com/gofiber/utils/workflows/Test/badge.svg) -![Security](https://github.com/gofiber/utils/workflows/Security/badge.svg) +![Codecov](https://img.shields.io/codecov/c/github/gofiber/utils?token=3Cr92CwaPQ&style=flat-square&logo=codecov&label=codecov) ![Linter](https://github.com/gofiber/utils/workflows/Linter/badge.svg) +[![Discord](https://img.shields.io/badge/discord-join%20channel-7289DA)](https://gofiber.io/discord) A collection of common functions but with better performance, less allocations and less dependencies created for [Fiber](https://github.com/gofiber/fiber). diff --git a/bytes_test.go b/bytes_test.go index 4dd1c5d..d9dea24 100644 --- a/bytes_test.go +++ b/bytes_test.go @@ -21,34 +21,35 @@ func Test_ToLowerBytes(t *testing.T) { require.Equal(t, []byte("/my4/name/is/:param/*"), ToLowerBytes([]byte("/MY4/NAME/IS/:PARAM/*"))) } +func Test_ToUpperBytes(t *testing.T) { + t.Parallel() + + require.Equal(t, []byte("/MY/NAME/IS/:PARAM/*"), ToUpperBytes([]byte("/my/name/is/:param/*"))) + require.Equal(t, []byte("/MY1/NAME/IS/:PARAM/*"), ToUpperBytes([]byte("/my1/name/is/:param/*"))) + require.Equal(t, []byte("/MY2/NAME/IS/:PARAM/*"), ToUpperBytes([]byte("/my2/name/is/:param/*"))) + require.Equal(t, []byte("/MY3/NAME/IS/:PARAM/*"), ToUpperBytes([]byte("/my3/name/is/:param/*"))) + require.Equal(t, []byte("/MY4/NAME/IS/:PARAM/*"), ToUpperBytes([]byte("/my4/name/is/:param/*"))) +} + func Benchmark_ToLowerBytes(b *testing.B) { path := []byte(largeStr) want := []byte(lowerStr) var res []byte + b.Run("fiber", func(b *testing.B) { for n := 0; n < b.N; n++ { res = ToLowerBytes(path) } - require.Equal(b, bytes.Equal(want, res), true) + require.True(b, bytes.Equal(want, res)) }) b.Run("default", func(b *testing.B) { for n := 0; n < b.N; n++ { res = bytes.ToLower(path) } - require.Equal(b, bytes.Equal(want, res), true) + require.True(b, bytes.Equal(want, res)) }) } -func Test_ToUpperBytes(t *testing.T) { - t.Parallel() - - require.Equal(t, []byte("/MY/NAME/IS/:PARAM/*"), ToUpperBytes([]byte("/my/name/is/:param/*"))) - require.Equal(t, []byte("/MY1/NAME/IS/:PARAM/*"), ToUpperBytes([]byte("/my1/name/is/:param/*"))) - require.Equal(t, []byte("/MY2/NAME/IS/:PARAM/*"), ToUpperBytes([]byte("/my2/name/is/:param/*"))) - require.Equal(t, []byte("/MY3/NAME/IS/:PARAM/*"), ToUpperBytes([]byte("/my3/name/is/:param/*"))) - require.Equal(t, []byte("/MY4/NAME/IS/:PARAM/*"), ToUpperBytes([]byte("/my4/name/is/:param/*"))) -} - func Benchmark_ToUpperBytes(b *testing.B) { path := []byte(largeStr) want := []byte(upperStr) diff --git a/byteseq_test.go b/byteseq_test.go index 8ef956a..21aed7d 100644 --- a/byteseq_test.go +++ b/byteseq_test.go @@ -8,6 +8,32 @@ import ( "github.com/stretchr/testify/require" ) +func Test_EqualFold(t *testing.T) { + t.Parallel() + testCases := []struct { + Expected bool + S1 string + S2 string + }{ + {Expected: true, S1: "/MY/NAME/IS/:PARAM/*", S2: "/my/name/is/:param/*"}, + {Expected: true, S1: "/MY/NAME/IS/:PARAM/*", S2: "/my/name/is/:param/*"}, + {Expected: true, S1: "/MY1/NAME/IS/:PARAM/*", S2: "/MY1/NAME/IS/:PARAM/*"}, + {Expected: false, S1: "/my2/name/is/:param/*", S2: "/my2/name"}, + {Expected: false, S1: "/dddddd", S2: "eeeeee"}, + {Expected: false, S1: "\na", S2: "*A"}, + {Expected: true, S1: "/MY3/NAME/IS/:PARAM/*", S2: "/my3/name/is/:param/*"}, + {Expected: true, S1: "/MY4/NAME/IS/:PARAM/*", S2: "/my4/nAME/IS/:param/*"}, + } + + for _, tc := range testCases { + res := EqualFold(tc.S1, tc.S2) + require.Equal(t, tc.Expected, res, "string") + + res = EqualFold([]byte(tc.S1), []byte(tc.S2)) + require.Equal(t, tc.Expected, res, "bytes") + } +} + func Benchmark_EqualFoldBytes(b *testing.B) { left := []byte(upperStr) right := []byte(lowerStr) @@ -42,29 +68,3 @@ func Benchmark_EqualFold(b *testing.B) { require.True(b, res) }) } - -func Test_EqualFold(t *testing.T) { - t.Parallel() - testCases := []struct { - Expected bool - S1 string - S2 string - }{ - {Expected: true, S1: "/MY/NAME/IS/:PARAM/*", S2: "/my/name/is/:param/*"}, - {Expected: true, S1: "/MY/NAME/IS/:PARAM/*", S2: "/my/name/is/:param/*"}, - {Expected: true, S1: "/MY1/NAME/IS/:PARAM/*", S2: "/MY1/NAME/IS/:PARAM/*"}, - {Expected: false, S1: "/my2/name/is/:param/*", S2: "/my2/name"}, - {Expected: false, S1: "/dddddd", S2: "eeeeee"}, - {Expected: false, S1: "\na", S2: "*A"}, - {Expected: true, S1: "/MY3/NAME/IS/:PARAM/*", S2: "/my3/name/is/:param/*"}, - {Expected: true, S1: "/MY4/NAME/IS/:PARAM/*", S2: "/my4/nAME/IS/:param/*"}, - } - - for _, tc := range testCases { - res := EqualFold(tc.S1, tc.S2) - require.Equal(t, tc.Expected, res, "string") - - res = EqualFold([]byte(tc.S1), []byte(tc.S2)) - require.Equal(t, tc.Expected, res, "bytes") - } -} diff --git a/common.go b/common.go index 3fa4b05..30a223d 100644 --- a/common.go +++ b/common.go @@ -52,25 +52,25 @@ func UUID() string { } // first 8 bytes differ, taking a slice of the first 16 bytes x := atomic.AddUint64(&uuidCounter, 1) - uuid := uuidSeed - binary.LittleEndian.PutUint64(uuid[:8], x) - uuid[6], uuid[9] = uuid[9], uuid[6] + _uuid := uuidSeed + binary.LittleEndian.PutUint64(_uuid[:8], x) + _uuid[6], _uuid[9] = _uuid[9], _uuid[6] // RFC4122 v4 - uuid[6] = (uuid[6] & 0x0f) | 0x40 - uuid[8] = uuid[8]&0x3f | 0x80 + _uuid[6] = (_uuid[6] & 0x0f) | 0x40 + _uuid[8] = _uuid[8]&0x3f | 0x80 // create UUID representation of the first 128 bits b := make([]byte, 36) - hex.Encode(b[0:8], uuid[0:4]) + hex.Encode(b[0:8], _uuid[0:4]) b[8] = '-' - hex.Encode(b[9:13], uuid[4:6]) + hex.Encode(b[9:13], _uuid[4:6]) b[13] = '-' - hex.Encode(b[14:18], uuid[6:8]) + hex.Encode(b[14:18], _uuid[6:8]) b[18] = '-' - hex.Encode(b[19:23], uuid[8:10]) + hex.Encode(b[19:23], _uuid[8:10]) b[23] = '-' - hex.Encode(b[24:], uuid[10:16]) + hex.Encode(b[24:], _uuid[10:16]) return UnsafeString(b) } @@ -133,9 +133,6 @@ func ConvertToBytes(humanReadableString string) int { } } - if lastNumberPos < 0 { - return 0 - } // fetch the number part and parse it to float size, err := strconv.ParseFloat(humanReadableString[:lastNumberPos+1], 64) if err != nil { diff --git a/common_test.go b/common_test.go index 9727e22..65cbb22 100644 --- a/common_test.go +++ b/common_test.go @@ -7,6 +7,8 @@ package utils import ( "crypto/rand" "fmt" + "net" + "os" "testing" "github.com/stretchr/testify/require" @@ -15,7 +17,6 @@ import ( func Test_FunctionName(t *testing.T) { t.Parallel() require.Equal(t, "github.com/gofiber/utils/v2.Test_UUID", FunctionName(Test_UUID)) - require.Equal(t, "github.com/gofiber/utils/v2.Test_FunctionName.func1", FunctionName(func() {})) dummyint := 20 @@ -25,8 +26,8 @@ func Test_FunctionName(t *testing.T) { func Test_UUID(t *testing.T) { t.Parallel() res := UUID() - require.Equal(t, 36, len(res)) - require.True(t, res != "00000000-0000-0000-0000-000000000000") + require.Len(t, res, 36) + require.NotEqual(t, "00000000-0000-0000-0000-000000000000", res) } func Test_UUID_Concurrency(t *testing.T) { @@ -44,14 +45,14 @@ func Test_UUID_Concurrency(t *testing.T) { res = <-ch results[res] = res } - require.Equal(t, iterations, len(results)) + require.Len(t, results, iterations) } func Test_UUIDv4(t *testing.T) { t.Parallel() res := UUIDv4() - require.Equal(t, 36, len(res)) - require.True(t, res != "00000000-0000-0000-0000-000000000000") + require.Len(t, res, 36) + require.NotEqual(t, "00000000-0000-0000-0000-000000000000", res) } func Test_UUIDv4_Concurrency(t *testing.T) { @@ -69,27 +70,7 @@ func Test_UUIDv4_Concurrency(t *testing.T) { res = <-ch results[res] = res } - require.Equal(t, iterations, len(results)) -} - -// go test -v -run=^$ -bench=Benchmark_UUID -benchmem -count=2 - -func Benchmark_UUID(b *testing.B) { - var res string - b.Run("fiber", func(b *testing.B) { - for n := 0; n < b.N; n++ { - res = UUID() - } - require.Equal(b, 36, len(res)) - }) - b.Run("default", func(b *testing.B) { - rnd := make([]byte, 16) - _, _ = rand.Read(rnd) - for n := 0; n < b.N; n++ { - res = fmt.Sprintf("%x-%x-%x-%x-%x", rnd[0:4], rnd[4:6], rnd[6:8], rnd[8:10], rnd[10:]) - } - require.Equal(b, 36, len(res)) - }) + require.Len(t, results, iterations) } func Test_ConvertToBytes(t *testing.T) { @@ -116,6 +97,42 @@ func Test_ConvertToBytes(t *testing.T) { require.Equal(t, 0, ConvertToBytes("MB")) } +func Test_GetArgument(t *testing.T) { + originalArgs := os.Args + + // Reset os.Args + defer func() { os.Args = originalArgs }() + + testArg := "test-arg" + os.Args = []string{"cmd", testArg} + + require.True(t, GetArgument(testArg)) + require.False(t, GetArgument("missing-arg")) +} + +func Test_IncrementIPRange(t *testing.T) { + t.Parallel() + + cases := []struct { + input net.IP + expected net.IP + }{ + {net.IP{192, 168, 1, 1}, net.IP{192, 168, 1, 2}}, + {net.IP{192, 168, 1, 254}, net.IP{192, 168, 1, 255}}, + {net.IP{192, 168, 1, 255}, net.IP{192, 168, 2, 0}}, + {net.IP{255, 255, 255, 255}, net.IP{0, 0, 0, 0}}, + } + + for _, c := range cases { + c := c + t.Run(c.input.String(), func(t *testing.T) { + t.Parallel() + IncrementIPRange(c.input) + require.Equal(t, c.expected, c.input) + }) + } +} + // go test -v -run=^$ -bench=Benchmark_ConvertToBytes -benchmem -count=2 func Benchmark_ConvertToBytes(b *testing.B) { var res int @@ -126,3 +143,22 @@ func Benchmark_ConvertToBytes(b *testing.B) { require.Equal(b, 42, res) }) } + +// go test -v -run=^$ -bench=Benchmark_UUID -benchmem -count=2 +func Benchmark_UUID(b *testing.B) { + var res string + b.Run("fiber", func(b *testing.B) { + for n := 0; n < b.N; n++ { + res = UUID() + } + require.Len(b, res, 36) + }) + b.Run("default", func(b *testing.B) { + rnd := make([]byte, 16) + _, _ = rand.Read(rnd) //nolint: errcheck // No need to check error + for n := 0; n < b.N; n++ { + res = fmt.Sprintf("%x-%x-%x-%x-%x", rnd[0:4], rnd[4:6], rnd[6:8], rnd[8:10], rnd[10:]) + } + require.Len(b, res, 36) + }) +} diff --git a/convert.go b/convert.go index 643f2e0..e21f322 100644 --- a/convert.go +++ b/convert.go @@ -136,14 +136,14 @@ func ToString(arg any, timeFormat ...string) string { } else if rv.Kind() == reflect.Slice || rv.Kind() == reflect.Array { // handle slices var buf strings.Builder - buf.WriteString("[") + buf.WriteString("[") //nolint: revive,errcheck // no need to check error for i := 0; i < rv.Len(); i++ { if i > 0 { - buf.WriteString(" ") + buf.WriteString(" ") //nolint: revive,errcheck // no need to check error } - buf.WriteString(ToString(rv.Index(i).Interface())) + buf.WriteString(ToString(rv.Index(i).Interface())) //nolint: revive,errcheck // no need to check error } - buf.WriteString("]") + buf.WriteString("]") //nolint: revive,errcheck // no need to check error return buf.String() } diff --git a/convert_test.go b/convert_test.go index b965cb0..c1db239 100644 --- a/convert_test.go +++ b/convert_test.go @@ -12,10 +12,25 @@ import ( "github.com/stretchr/testify/require" ) -var dataTypeExamples []interface{} +var dataTypeExamples []any + +// MyStringer for testing fmt.Stringer support. +type MyStringer struct { + value string +} + +// CustomType not handled by switch cases. +type CustomType struct { + Field1 string + Field2 int +} + +func (ms MyStringer) String() string { + return ms.value +} func init() { - dataTypeExamples = []interface{}{ + dataTypeExamples = []any{ 42, int8(42), int16(42), @@ -32,11 +47,14 @@ func init() { float32(3.14), float64(3.14), time.Now(), + time.Date(2000, 1, 1, 12, 34, 56, 0, time.UTC), []string{"Hello", "World"}, []int{42, 21}, [2]int{42, 21}, [][]int{{42, 21}, {42, 21}}, - []interface{}{[]int{42, 21}, 42, "Hello", true, []string{"Hello", "World"}}, + []any{[]int{42, 21}, 42, "Hello", true, []string{"Hello", "World"}}, + MyStringer{value: "Stringer Value"}, + CustomType{Field1: "example", Field2: 42}, } } @@ -46,50 +64,12 @@ func Test_UnsafeString(t *testing.T) { require.Equal(t, "Hello, World!", res) } -// go test -v -run=^$ -bench=UnsafeString -benchmem -count=2 - -func Benchmark_UnsafeString(b *testing.B) { - hello := []byte("Hello, World!") - var res string - b.Run("unsafe", func(b *testing.B) { - for n := 0; n < b.N; n++ { - res = UnsafeString(hello) - } - require.Equal(b, "Hello, World!", res) - }) - b.Run("default", func(b *testing.B) { - for n := 0; n < b.N; n++ { - res = string(hello) - } - require.Equal(b, "Hello, World!", res) - }) -} - func Test_UnsafeBytes(t *testing.T) { t.Parallel() res := UnsafeBytes("Hello, World!") require.Equal(t, []byte("Hello, World!"), res) } -// go test -v -run=^$ -bench=UnsafeBytes -benchmem -count=4 - -func Benchmark_UnsafeBytes(b *testing.B) { - hello := "Hello, World!" - var res []byte - b.Run("unsafe", func(b *testing.B) { - for n := 0; n < b.N; n++ { - res = UnsafeBytes(hello) - } - require.Equal(b, []byte("Hello, World!"), res) - }) - b.Run("default", func(b *testing.B) { - for n := 0; n < b.N; n++ { - res = []byte(hello) - } - require.Equal(b, []byte("Hello, World!"), res) - }) -} - func Test_CopyString(t *testing.T) { t.Parallel() res := CopyString("Hello, World!") @@ -99,39 +79,63 @@ func Test_CopyString(t *testing.T) { func Test_ToString(t *testing.T) { t.Parallel() + customInstance := CustomType{Field1: "example", Field2: 42} + timeSample := time.Date(2000, 1, 1, 12, 34, 56, 0, time.UTC) + stringerSample := MyStringer{value: "Stringer Value"} + tests := []struct { - input interface{} - expected string + name string + input any + timeFormat []string + expected string }{ - {[]byte("Hello, World!"), "Hello, World!"}, - {true, "true"}, - {uint(100), "100"}, - {int(42), "42"}, - {int8(42), "42"}, - {int16(42), "42"}, - {int32(42), "42"}, - {int64(42), "42"}, - {uint8(100), "100"}, - {uint16(100), "100"}, - {uint32(100), "100"}, - {uint64(100), "100"}, - {"test string", "test string"}, - {float32(3.14), "3.14"}, - {float64(3.14159), "3.14159"}, - {time.Date(2000, 1, 1, 12, 34, 56, 0, time.UTC), "2000-01-01 12:34:56"}, - {struct{ Name string }{"John"}, "{John}"}, - {[]string{"Hello", "World"}, "[Hello World]"}, - {[]int{42, 21}, "[42 21]"}, - {[2]int{42, 21}, "[42 21]"}, - {[][]int{{42, 21}, {42, 21}}, "[[42 21] [42 21]]"}, - {[]interface{}{[]int{42, 21}, 42, "Hello", true, []string{"Hello", "World"}}, "[[42 21] 42 Hello true [Hello World]]"}, + // Primitive types and string + {name: "int", input: int(42), expected: "42"}, + {name: "int8", input: int8(42), expected: "42"}, + {name: "int16", input: int16(42), expected: "42"}, + {name: "int32", input: int32(42), expected: "42"}, + {name: "int64", input: int64(42), expected: "42"}, + {name: "uint", input: uint(100), expected: "100"}, + {name: "uint8", input: uint8(100), expected: "100"}, + {name: "uint16", input: uint16(100), expected: "100"}, + {name: "uint32", input: uint32(100), expected: "100"}, + {name: "uint64", input: uint64(100), expected: "100"}, + {name: "string", input: "test string", expected: "test string"}, + {name: "[]byte", input: []byte("Hello, World!"), expected: "Hello, World!"}, + {name: "bool", input: true, expected: "true"}, + {name: "float32", input: float32(3.14), expected: "3.14"}, + {name: "float64", input: float64(3.14159), expected: "3.14159"}, + + // time.Time + {name: "time.Time default format", input: timeSample, expected: "2000-01-01 12:34:56"}, + {name: "time.Time custom format", input: timeSample, timeFormat: []string{"Jan 02, 2006"}, expected: "Jan 01, 2000"}, + + // reflect.Value + {name: "reflect.Value", input: reflect.ValueOf(42), expected: "42"}, + + // fmt.Stringer + {name: "fmt.Stringer", input: stringerSample, expected: "Stringer Value"}, + + // Composite types (arrays, slices) + {name: "[]string", input: []string{"Hello", "World"}, expected: "[Hello World]"}, + {name: "[]int", input: []int{42, 21}, expected: "[42 21]"}, + {name: "[][]int", input: [][]int{{42, 21}, {42, 21}}, expected: "[[42 21] [42 21]]"}, + {name: "[]any", input: []any{[]int{42, 21}, 42, "Hello", true, []string{"Hello", "World"}}, expected: "[[42 21] 42 Hello true [Hello World]]"}, + + // Custom unhandled type + {name: "CustomType", input: customInstance, expected: "{example 42}"}, } for _, tc := range tests { - tc := tc - t.Run(reflect.TypeOf(tc.input).String(), func(t *testing.T) { + tc := tc // capture range variable + t.Run(tc.name, func(t *testing.T) { t.Parallel() - res := ToString(tc.input) + var res string + if len(tc.timeFormat) > 0 { + res = ToString(tc.input, tc.timeFormat...) + } else { + res = ToString(tc.input) + } require.Equal(t, tc.expected, res) }) } @@ -139,7 +143,7 @@ func Test_ToString(t *testing.T) { // Testing pointer to int intPtr := 42 testsPtr := []struct { - input interface{} + input any expected string }{ {&intPtr, "42"}, @@ -154,6 +158,80 @@ func Test_ToString(t *testing.T) { } } +func TestCopyBytes(t *testing.T) { + t.Run("empty slice", func(t *testing.T) { + input := []byte{} + copied := CopyBytes(input) + require.Equal(t, input, copied) + }) + + t.Run("single element", func(t *testing.T) { + input := []byte{42} + copied := CopyBytes(input) + require.Equal(t, input, copied) + input[0] = 0 // Modify the input to ensure the copied slice does not change + require.NotEqual(t, input[0], copied[0]) + }) + + t.Run("multiple elements", func(t *testing.T) { + input := []byte{1, 2, 3, 4, 5} + copied := CopyBytes(input) + require.Equal(t, input, copied) + input[0] = 0 // Modify the input to ensure the copied slice does not change + require.NotEqual(t, input, copied) + }) + + t.Run("deep copy validation", func(t *testing.T) { + input := []byte{1, 2, 3, 4, 5} + copied := CopyBytes(input) + input[0] = 0 // Modify the input to ensure the copied slice does not change + require.NotEqual(t, input[0], copied[0]) + }) + + t.Run("nil slice", func(t *testing.T) { + copied := CopyBytes(nil) + require.NotNil(t, copied) + require.Empty(t, len(copied)) + require.Equal(t, 0, cap(copied)) + }) +} + +func TestByteSize(t *testing.T) { + tests := []struct { + name string + bytes uint64 + expected string + }{ + {"Zero Byte", 0, "0B"}, + {"One Byte", 1, "1B"}, + {"Kilobytes", 1024, "1KB"}, + {"Megabytes", 1024 * 1024, "1MB"}, + {"Gigabytes", 1024 * 1024 * 1024, "1GB"}, + {"Terabytes", 1024 * 1024 * 1024 * 1024, "1TB"}, + {"Petabytes", 1024 * 1024 * 1024 * 1024 * 1024, "1PB"}, + {"Exabytes", 1024 * 1024 * 1024 * 1024 * 1024 * 1024, "1EB"}, + {"Bytes less than KB", 500, "500B"}, + {"KB less than MB", 500 * 1024, "500KB"}, + {"MB less than GB", 500 * 1024 * 1024, "500MB"}, + {"GB less than TB", 500 * 1024 * 1024 * 1024, "500GB"}, + {"TB less than PB", 500 * 1024 * 1024 * 1024 * 1024, "500TB"}, + {"PB less than EB", 500 * 1024 * 1024 * 1024 * 1024 * 1024, "500PB"}, + {"Fractional KB", 1126, "1.1KB"}, + {"Fractional MB", 1126 * 1024, "1.1MB"}, + {"Fractional GB", 1126 * 1024 * 1024, "1.1GB"}, + {"Fractional TB", 1126 * 1024 * 1024 * 1024, "1.1TB"}, + {"Fractional PB", 1126 * 1024 * 1024 * 1024 * 1024, "1.1PB"}, + {"Fractional EB", 1126 * 1024 * 1024 * 1024 * 1024 * 1024, "1.1EB"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := ByteSize(tt.bytes) + require.Equal(t, tt.expected, result) + }) + } +} + // go test -v -run=^$ -bench=ToString -benchmem -count=4 func Benchmark_ToString(b *testing.B) { for _, value := range dataTypeExamples { @@ -181,3 +259,39 @@ func Benchmark_ToString_concurrency(b *testing.B) { }) } } + +// go test -v -run=^$ -bench=UnsafeBytes -benchmem -count=4 +func Benchmark_UnsafeBytes(b *testing.B) { + hello := "Hello, World!" + var res []byte + b.Run("unsafe", func(b *testing.B) { + for n := 0; n < b.N; n++ { + res = UnsafeBytes(hello) + } + require.Equal(b, []byte("Hello, World!"), res) + }) + b.Run("default", func(b *testing.B) { + for n := 0; n < b.N; n++ { + res = []byte(hello) + } + require.Equal(b, []byte("Hello, World!"), res) + }) +} + +// go test -v -run=^$ -bench=UnsafeString -benchmem -count=2 +func Benchmark_UnsafeString(b *testing.B) { + hello := []byte("Hello, World!") + var res string + b.Run("unsafe", func(b *testing.B) { + for n := 0; n < b.N; n++ { + res = UnsafeString(hello) + } + require.Equal(b, "Hello, World!", res) + }) + b.Run("default", func(b *testing.B) { + for n := 0; n < b.N; n++ { + res = string(hello) + } + require.Equal(b, "Hello, World!", res) + }) +} diff --git a/file.go b/file.go index fe44149..6e5198f 100644 --- a/file.go +++ b/file.go @@ -1,6 +1,7 @@ package utils import ( + "errors" "io" "net/http" "os" @@ -18,7 +19,7 @@ func Walk(fs http.FileSystem, root string, walkFn filepath.WalkFunc) error { if err != nil { return walkFn(root, nil, err) } - return walk(fs, root, info, walkFn) + return walkInternal(fs, root, info, walkFn) } // ReadFile returns the raw content of a file @@ -28,7 +29,7 @@ func ReadFile(path string, fs http.FileSystem) ([]byte, error) { if err != nil { return nil, err } - defer file.Close() + defer file.Close() //nolint: errcheck // No need to check error return io.ReadAll(file) } return os.ReadFile(path) // #nosec G304 @@ -49,11 +50,11 @@ func readDirNames(fs http.FileSystem, dirname string) ([]string, error) { return names, nil } -// walk recursively descends path, calling walkFn. -func walk(fs http.FileSystem, path string, info os.FileInfo, walkFn filepath.WalkFunc) error { +// walkInternal recursively descends path, calling walkFn. +func walkInternal(fs http.FileSystem, path string, info os.FileInfo, walkFn filepath.WalkFunc) error { err := walkFn(path, info, nil) if err != nil { - if info.IsDir() && err == filepath.SkipDir { + if info.IsDir() && errors.Is(err, filepath.SkipDir) { return nil } return err @@ -72,13 +73,13 @@ func walk(fs http.FileSystem, path string, info os.FileInfo, walkFn filepath.Wal filename := pathpkg.Join(path, name) fileInfo, err := stat(fs, filename) if err != nil { - if err := walkFn(filename, fileInfo, err); err != nil && err != filepath.SkipDir { + if err := walkFn(filename, fileInfo, err); err != nil && !errors.Is(err, filepath.SkipDir) { return err } } else { - err = walk(fs, filename, fileInfo, walkFn) + err = walkInternal(fs, filename, fileInfo, walkFn) if err != nil { - if !fileInfo.IsDir() || err != filepath.SkipDir { + if !fileInfo.IsDir() || !errors.Is(err, filepath.SkipDir) { return err } } @@ -94,7 +95,7 @@ func readDir(fs http.FileSystem, name string) ([]os.FileInfo, error) { if err != nil { return nil, err } - defer f.Close() + defer f.Close() //nolint: errcheck // No need to check error return f.Readdir(0) } @@ -104,6 +105,6 @@ func stat(fs http.FileSystem, name string) (os.FileInfo, error) { if err != nil { return nil, err } - defer f.Close() + defer f.Close() //nolint: errcheck // No need to check error return f.Stat() } diff --git a/file_test.go b/file_test.go index f57e6b7..bab750c 100644 --- a/file_test.go +++ b/file_test.go @@ -1,6 +1,7 @@ package utils import ( + "fmt" "io/fs" "net/http" "os" @@ -18,9 +19,9 @@ func Test_ReadFile(t *testing.T) { switch runtime.GOOS { case "windows": - require.Equal(t, string(file), "doe\r\n") + require.Equal(t, "doe\r\n", string(file)) default: - require.Equal(t, string(file), "doe\n") + require.Equal(t, "doe\n", string(file)) } require.NoError(t, err) @@ -36,7 +37,7 @@ func Test_Walk(t *testing.T) { } var files []file - neededResults := []file{ + expectedResults := []file{ { path: "example", name: "example", @@ -55,7 +56,7 @@ func Test_Walk(t *testing.T) { } testFS := http.FS(os.DirFS(".github/tests")) - err := Walk(testFS, ".", func(path string, info fs.FileInfo, err error) error { + err := Walk(testFS, ".", func(path string, info fs.FileInfo, _ error) error { if path != "." { files = append(files, file{ path: path, @@ -68,5 +69,29 @@ func Test_Walk(t *testing.T) { }) require.NoError(t, err) - require.Equal(t, files, neededResults) + require.Equal(t, expectedResults, files) +} + +func Test_Walk_Error(t *testing.T) { + t.Parallel() + + testFS := http.FS(os.DirFS(".github/tests")) + err := Walk(testFS, "nonexistent", func(path string, _ fs.FileInfo, _ error) error { + return fmt.Errorf("file not found: %s", path) + }) + + require.Error(t, err) +} + +func Test_ReadFile_Error(t *testing.T) { + t.Parallel() + + // Test error when file does not exist + testFS := http.FS(os.DirFS(".github/tests")) + _, err := ReadFile("nonexistent.txt", testFS) + require.Error(t, err) + + // Test error when file does not exist and fs is nil + _, err = ReadFile("nonexistent.txt", nil) + require.Error(t, err) } diff --git a/go.mod b/go.mod index 88cff2a..237aa95 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/gofiber/utils/v2 -go 1.20 +go 1.21 require ( github.com/google/uuid v1.6.0 diff --git a/http_test.go b/http_test.go index f960d7b..addc070 100644 --- a/http_test.go +++ b/http_test.go @@ -42,7 +42,6 @@ func Test_GetMIME(t *testing.T) { require.Equal(t, "application/javascript", res) } require.NoError(t, err) - } // go test -v -run=^$ -bench=Benchmark_GetMIME -benchmem -count=2 @@ -93,22 +92,22 @@ func Test_ParseVendorSpecificContentType(t *testing.T) { cType = ParseVendorSpecificContentType("something invalid") require.Equal(t, "something invalid", cType) + + cType = ParseVendorSpecificContentType("invalid+withoutSlash") + require.Equal(t, "invalid+withoutSlash", cType) } func Benchmark_ParseVendorSpecificContentType(b *testing.B) { - var cType string b.Run("vendorContentType", func(b *testing.B) { for n := 0; n < b.N; n++ { - cType = ParseVendorSpecificContentType("application/vnd.api+json; version=1") + ParseVendorSpecificContentType("application/vnd.api+json; version=1") } - require.Equal(b, "application/json", cType) }) b.Run("defaultContentType", func(b *testing.B) { for n := 0; n < b.N; n++ { - cType = ParseVendorSpecificContentType("application/json") + ParseVendorSpecificContentType("application/json") } - require.Equal(b, "application/json", cType) }) } @@ -141,17 +140,14 @@ func Test_StatusMessage(t *testing.T) { // go test -run=^$ -bench=Benchmark_StatusMessage -benchmem -count=2 func Benchmark_StatusMessage(b *testing.B) { - var res string b.Run("fiber", func(b *testing.B) { for n := 0; n < b.N; n++ { - res = StatusMessage(http.StatusNotExtended) + StatusMessage(http.StatusNotExtended) } - require.Equal(b, "Not Extended", res) }) b.Run("default", func(b *testing.B) { for n := 0; n < b.N; n++ { - res = http.StatusText(http.StatusNotExtended) + http.StatusText(http.StatusNotExtended) } - require.Equal(b, "Not Extended", res) }) } diff --git a/ips.go b/ips.go index 499b741..54ab11b 100644 --- a/ips.go +++ b/ips.go @@ -1,6 +1,8 @@ package utils -import "net" +import ( + "net" +) // IsIPv4 works the same way as net.ParseIP, // but without check for IPv6 case and without returning net.IP slice, whereby IsIPv4 makes no allocations. diff --git a/ips_test.go b/ips_test.go index 020f81f..1def4a9 100644 --- a/ips_test.go +++ b/ips_test.go @@ -14,31 +14,69 @@ import ( func Test_IsIPv4(t *testing.T) { t.Parallel() - require.Equal(t, true, IsIPv4("255.255.255.255")) - require.Equal(t, true, IsIPv4("174.23.33.100")) - require.Equal(t, true, IsIPv4("127.0.0.1")) - require.Equal(t, true, IsIPv4("0.0.0.0")) - - require.Equal(t, false, IsIPv4(".0.0.0")) - require.Equal(t, false, IsIPv4("0.0.0.")) - require.Equal(t, false, IsIPv4("0.0.0")) - require.Equal(t, false, IsIPv4(".0.0.0.")) - require.Equal(t, false, IsIPv4("0.0.0.0.0")) - require.Equal(t, false, IsIPv4("0")) - require.Equal(t, false, IsIPv4("")) - require.Equal(t, false, IsIPv4("2345:0425:2CA1::0567:5673:23b5")) - require.Equal(t, false, IsIPv4("invalid")) - require.Equal(t, false, IsIPv4("189.12.34.260")) - require.Equal(t, false, IsIPv4("189.12.260.260")) - require.Equal(t, false, IsIPv4("189.260.260.260")) - require.Equal(t, false, IsIPv4("255.255.255.256")) - require.Equal(t, false, IsIPv4("999.999.999.999")) - require.Equal(t, false, IsIPv4("9999.9999.9999.9999")) + require.True(t, IsIPv4("255.255.255.255")) + require.True(t, IsIPv4("174.23.33.100")) + require.True(t, IsIPv4("127.0.0.1")) + require.True(t, IsIPv4("0.0.0.0")) + require.True(t, IsIPv4("1.1.1.1")) + + require.False(t, IsIPv4(".0.0.0")) + require.False(t, IsIPv4("0.0.0.")) + require.False(t, IsIPv4("0.0.0")) + require.False(t, IsIPv4(".0.0.0.")) + require.False(t, IsIPv4("0.0.0.0.0")) + require.False(t, IsIPv4("0")) + require.False(t, IsIPv4("")) + require.False(t, IsIPv4("2345:0425:2CA1::0567:5673:23b5")) + require.False(t, IsIPv4("invalid")) + require.False(t, IsIPv4("189.12.34.260")) + require.False(t, IsIPv4("189.12.260.260")) + require.False(t, IsIPv4("189.260.260.260")) + require.False(t, IsIPv4("255.255.255.256")) + require.False(t, IsIPv4("999.999.999.999")) + require.False(t, IsIPv4("9999.9999.9999.9999")) + require.False(t, IsIPv4("192168.1.1")) + require.False(t, IsIPv4("192.1681.1")) + require.False(t, IsIPv4("192a168.1.1")) +} + +func Test_IsIPv6(t *testing.T) { + t.Parallel() + // Valid Cases + require.True(t, IsIPv6("9396:9549:b4f7:8ed0:4791:1330:8c06:e62d")) + require.True(t, IsIPv6("2345:0425:2CA1::0567:5673:23b5")) + require.True(t, IsIPv6("2001:1:2:3:4:5:6:7")) + require.True(t, IsIPv6("::1"), "IPv6 loopback address with leading ellipsis") + require.True(t, IsIPv6("1::"), "Shortest possible IPv6 address with trailing ellipsis") + require.True(t, IsIPv6("2001:db8::8a2e:370:7334"), "Valid IPv6 address with ellipsis in the middle") + require.True(t, IsIPv6("::ffff:192.0.2.128"), "IPv4-mapped IPv6 address") + require.True(t, IsIPv6("::192.168.1.1"), "IPv4-compatible IPv6 address") + require.True(t, IsIPv6("ffff::"), "Address with a single non-zero group and leading ellipsis") + + // Boundary values + require.True(t, IsIPv6("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff"), "Maximum value for all groups") + + // Invalid cases + require.False(t, IsIPv6("1::2::3"), "IPv6 address with multiple ellipses") + require.False(t, IsIPv6("2001::25de::cade"), "IPv6 address with multiple ellipses in the middle") + require.False(t, IsIPv6("g:a:b:c:d:e:f:1"), "Invalid character 'g' in address") + require.False(t, IsIPv6("::192.168.1.256"), "Invalid IPv4 segment in IPv6 address") + require.False(t, IsIPv6("1:2:3:4:5:6:7:8:9"), "IPv6 address too long") + require.False(t, IsIPv6("1:2:3"), "IPv6 address too short") + require.False(t, IsIPv6("1:::3:4"), "Consecutive colons not part of a valid ellipsis") + require.False(t, IsIPv6("::ffff:256.0.0.1"), "Embedded IPv4 address with invalid IPv4 segment") + require.False(t, IsIPv6("1111:2222:3333:4444:5555:6666:7777:8888:9999"), "IPv6 address with too many groups") + require.False(t, IsIPv6("2001:db8::8a2e:370g:7334"), "IPv6 address with invalid hex character 'g'") + require.False(t, IsIPv6("1.1.1.1")) + require.False(t, IsIPv6("2001:1:2:3:4:5:6:")) + require.False(t, IsIPv6(":1:2:3:4:5:6:")) + require.False(t, IsIPv6("1:2:3:4:5:6:")) + require.False(t, IsIPv6("")) + require.False(t, IsIPv6("invalid")) } // go test -v -run=^$ -bench=UnsafeString -benchmem -count=2 - func Benchmark_IsIPv4(b *testing.B) { ip := "174.23.33.100" var res bool @@ -47,34 +85,18 @@ func Benchmark_IsIPv4(b *testing.B) { for n := 0; n < b.N; n++ { res = IsIPv4(ip) } - require.Equal(b, true, res) + require.True(b, res) }) b.Run("default", func(b *testing.B) { for n := 0; n < b.N; n++ { res = net.ParseIP(ip) != nil } - require.Equal(b, true, res) + require.True(b, res) }) } -func Test_IsIPv6(t *testing.T) { - t.Parallel() - - require.Equal(t, true, IsIPv6("9396:9549:b4f7:8ed0:4791:1330:8c06:e62d")) - require.Equal(t, true, IsIPv6("2345:0425:2CA1::0567:5673:23b5")) - require.Equal(t, true, IsIPv6("2001:1:2:3:4:5:6:7")) - - require.Equal(t, false, IsIPv6("1.1.1.1")) - require.Equal(t, false, IsIPv6("2001:1:2:3:4:5:6:")) - require.Equal(t, false, IsIPv6(":1:2:3:4:5:6:")) - require.Equal(t, false, IsIPv6("1:2:3:4:5:6:")) - require.Equal(t, false, IsIPv6("")) - require.Equal(t, false, IsIPv6("invalid")) -} - // go test -v -run=^$ -bench=UnsafeString -benchmem -count=2 - func Benchmark_IsIPv6(b *testing.B) { ip := "9396:9549:b4f7:8ed0:4791:1330:8c06:e62d" var res bool @@ -83,13 +105,13 @@ func Benchmark_IsIPv6(b *testing.B) { for n := 0; n < b.N; n++ { res = IsIPv6(ip) } - require.Equal(b, true, res) + require.True(b, res) }) b.Run("default", func(b *testing.B) { for n := 0; n < b.N; n++ { res = net.ParseIP(ip) != nil } - require.Equal(b, true, res) + require.True(b, res) }) } diff --git a/time_test.go b/time_test.go index 64a7a4e..2b41ff6 100644 --- a/time_test.go +++ b/time_test.go @@ -7,9 +7,10 @@ import ( "github.com/stretchr/testify/require" ) -func checkTimeStamp(t testing.TB, expectedCurrent, actualCurrent uint32) { +func checkTimeStamp(tb testing.TB, expectedCurrent, actualCurrent uint32) { + tb.Helper() // test with some buffer in front and back of the expectedCurrent time -> because of the timing on the work machine - require.Equal(t, true, actualCurrent >= expectedCurrent-1 || actualCurrent <= expectedCurrent+1) + require.True(tb, true, actualCurrent >= expectedCurrent-1 || actualCurrent <= expectedCurrent+1) } func Test_TimeStampUpdater(t *testing.T) { @@ -19,28 +20,49 @@ func Test_TimeStampUpdater(t *testing.T) { now := uint32(time.Now().Unix()) checkTimeStamp(t, now, Timestamp()) + // one second later time.Sleep(1 * time.Second) checkTimeStamp(t, now+1, Timestamp()) + // two seconds later time.Sleep(1 * time.Second) checkTimeStamp(t, now+2, Timestamp()) } func Benchmark_CalculateTimestamp(b *testing.B) { + var res uint32 StartTimeStampUpdater() - var res uint32 - b.Run("fiber", func(b *testing.B) { - for n := 0; n < b.N; n++ { + b.Run("fiber", func(bb *testing.B) { + bb.ReportAllocs() + bb.ResetTimer() + for n := 0; n < bb.N; n++ { + _ = Timestamp() + } + }) + b.Run("default", func(bb *testing.B) { + bb.ReportAllocs() + bb.ResetTimer() + for n := 0; n < bb.N; n++ { + _ = uint32(time.Now().Unix()) + } + }) + + b.Run("fiber_asserted", func(bb *testing.B) { + bb.ReportAllocs() + bb.ResetTimer() + for n := 0; n < bb.N; n++ { res = Timestamp() + checkTimeStamp(bb, uint32(time.Now().Unix()), res) } - checkTimeStamp(b, uint32(time.Now().Unix()), res) }) - b.Run("default", func(b *testing.B) { - for n := 0; n < b.N; n++ { + b.Run("default_asserted", func(bb *testing.B) { + bb.ReportAllocs() + bb.ResetTimer() + for n := 0; n < bb.N; n++ { res = uint32(time.Now().Unix()) + checkTimeStamp(bb, uint32(time.Now().Unix()), res) } - checkTimeStamp(b, uint32(time.Now().Unix()), res) }) } diff --git a/xml_test.go b/xml_test.go index ff55753..9a6d85b 100644 --- a/xml_test.go +++ b/xml_test.go @@ -56,7 +56,6 @@ func Test_DefaultXMLEncoder(t *testing.T) { raw, err := xmlEncoder(ss) require.NoError(t, err) - require.Equal(t, string(raw), xmlString) } @@ -70,9 +69,70 @@ func Test_DefaultXMLDecoder(t *testing.T) { ) err := xmlDecoder(xmlBytes, &ss) - require.Nil(t, err) - require.Equal(t, 2, len(ss.Servers)) + require.NoError(t, err) + require.Len(t, ss.Servers, 2) require.Equal(t, "1", ss.Version) require.Equal(t, "fiber one", ss.Servers[0].Name) require.Equal(t, "fiber two", ss.Servers[1].Name) } + +func Benchmark_GolangXMLEncoder(b *testing.B) { + var ( + ss = &serversXMLStructure{ + Version: "1", + Servers: []serverXMLStructure{ + {Name: "fiber one"}, + {Name: "fiber two"}, + }, + } + xmlEncoder XMLMarshal = xml.Marshal + ) + + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, err := xmlEncoder(ss) + if err != nil { + b.Fatal(err) + } + } +} + +func Benchmark_DefaultXMLEncoder(b *testing.B) { + var ( + ss = &serversXMLStructure{ + Version: "1", + Servers: []serverXMLStructure{ + {Name: "fiber one"}, + {Name: "fiber two"}, + }, + } + xmlEncoder XMLMarshal = xml.Marshal + ) + + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, err := xmlEncoder(ss) + if err != nil { + b.Fatal(err) + } + } +} + +func Benchmark_DefaultXMLDecoder(b *testing.B) { + var ( + ss serversXMLStructure + xmlBytes = []byte(xmlString) + xmlDecoder XMLUnmarshal = xml.Unmarshal + ) + + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + err := xmlDecoder(xmlBytes, &ss) + if err != nil { + b.Fatal(err) + } + } +}