From 44da329376220fb18f8dcea3c48840967535da21 Mon Sep 17 00:00:00 2001 From: Benji Vesterby Date: Fri, 24 Mar 2023 21:16:20 -0400 Subject: [PATCH] feat(scaler.go): add support for a Wait modifier on scaler to increase or decrease duration based on an interval This change also corrects some linter errors and adds my new linter configuration --- .github/workflows/build.yml | 4 +- .golangci.yml | 404 ++++++++++++++++++++++++------------ helper_test.go | 8 +- helpers.go | 2 + scaler.go | 90 +++++++- scaler_test.go | 117 ++++++++++- stream_bench_test.go | 2 +- stream_test.go | 12 +- 8 files changed, 493 insertions(+), 146 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 230d30a..ad24c8c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -39,8 +39,10 @@ jobs: with: go-version: ${{ matrix.go-version }} stable: false + - name: Fuzz + run: go test -fuzz=./... -fuzztime=10s - name: Test - run: go test -failfast ./... -race -coverprofile=coverage.txt -covermode=atomic + run: go test -fuzz=. -fuzztime=10s -failfast ./... -race -coverprofile=coverage.txt -covermode=atomic - name: Push Coverage to codecov.io uses: codecov/codecov-action@v3 with: diff --git a/.golangci.yml b/.golangci.yml index ff47ac9..902bc49 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,146 +1,296 @@ +run: + # Timeout for analysis, e.g. 30s, 5m. + # Default: 1m + timeout: 3m + skip-dirs: + - testdata + build-tags: + - codeanalysis + + +# This file contains only configs which differ from defaults. +# All possible options can be found here https://github.com/golangci/golangci-lint/blob/master/.golangci.reference.yml linters-settings: - depguard: - list-type: blacklist - packages: - # logging is allowed only by logutils.Log, logrus - # is allowed to use only in logutils package - - github.com/sirupsen/logrus - packages-with-error-message: - - github.com/sirupsen/logrus: "logging is allowed only by logutils.Log" - dupl: - threshold: 110 + cyclop: + # The maximal code complexity to report. + # Default: 10 + max-complexity: 30 + # The maximal average package complexity. + # If it's higher than 0.0 (float) the check is enabled + # Default: 0.0 + package-average: 10.0 + + errcheck: + # Report about not checking of errors in type assertions: `a := b.(MyStruct)`. + # Such cases aren't reported by default. + # Default: false + check-type-assertions: true + + exhaustive: + # Program elements to check for exhaustiveness. + # Default: [ switch ] + check: + - switch + - map + funlen: + # Checks the number of lines in a function. + # If lower than 0, disable the check. + # Default: 60 lines: 100 + # Checks the number of statements in a function. + # If lower than 0, disable the check. + # Default: 40 statements: 50 - gci: - local-prefixes: github.com/golangci/golangci-lint - goconst: - min-len: 2 - min-occurrences: 2 - # gocritic: - # enabled-tags: - # - diagnostic - # - experimental - # - opinionated - # - performance - # - style - # disabled-checks: - # - dupImport # https://github.com/go-critic/go-critic/issues/845 - # - ifElseChain - # - octalLiteral - # - whyNoLint - # - wrapperFunc - gocyclo: - min-complexity: 15 - goimports: - local-prefixes: github.com/golangci/golangci-lint - golint: - min-confidence: 0 - gomnd: + + gocognit: + # Minimal code complexity to report + # Default: 30 (but we recommend 10-20) + min-complexity: 40 + + gocritic: + # Settings passed to gocritic. + # The settings key is the name of a supported gocritic checker. + # The list of supported checkers can be find in https://go-critic.github.io/overview. settings: - mnd: - # don't include the "operation" and "assign" - checks: argument,case,condition,return + captLocal: + # Whether to restrict checker to params only. + # Default: true + paramsOnly: false + underef: + # Whether to skip (*x).method() calls where x is a pointer receiver. + # Default: true + skipRecvDeref: false + + gomnd: + # List of function patterns to exclude from analysis. + # Values always ignored: `time.Date`, + # `strconv.FormatInt`, `strconv.FormatUint`, `strconv.FormatFloat`, + # `strconv.ParseInt`, `strconv.ParseUint`, `strconv.ParseFloat`. + # Default: [] + ignored-functions: + - os.Chmod + - os.Mkdir + - os.MkdirAll + - os.OpenFile + - os.WriteFile + - prometheus.ExponentialBuckets + - prometheus.ExponentialBucketsRange + - prometheus.LinearBuckets + + gomodguard: + blocked: + # List of blocked modules. + # Default: [] + modules: + - github.com/golang/protobuf: + recommendations: + - google.golang.org/protobuf + reason: "see https://developers.google.com/protocol-buffers/docs/reference/go/faq#modules" + - github.com/satori/go.uuid: + recommendations: + - github.com/google/uuid + reason: "satori's package is not maintained" + - github.com/gofrs/uuid: + recommendations: + - github.com/google/uuid + reason: "see recommendation from dev-infra team: https://confluence.gtforge.com/x/gQI6Aw" + govet: - check-shadowing: true - settings: - printf: - funcs: - - (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 - lll: - line-length: 140 - maligned: - suggest-new: true - misspell: - locale: US + # Enable all analyzers. + # Default: false + enable-all: true + # Disable analyzers by name. + # Run `go tool vet help` to see all analyzers. + # Default: [] + disable: + - fieldalignment # too strict + - shadow # too strict + # Settings per analyzer. + #settings: + # shadow: + # # Whether to be strict about shadowing; can be noisy. + # # Default: false + # strict: true + + nakedret: + # Make an issue if func has more lines of code than this setting, and it has naked returns. + # Default: 30 + max-func-lines: 0 + nolintlint: - allow-leading-space: true # don't require machine-readable nolint directives (i.e. with no leading space) - allow-unused: false # report any unused nolint directives - require-explanation: false # don't require an explanation for nolint directives - require-specific: false # don't require nolint directives to be specific about which linter is being skipped + # Exclude following linters from requiring an explanation. + # Default: [] + allow-no-explanation: [ funlen, gocognit, lll ] + # Enable to require an explanation of nonzero length after each nolint directive. + # Default: false + require-explanation: true + # Enable to require nolint directives to mention the specific linter being suppressed. + # Default: false + require-specific: true + + rowserrcheck: + # database/sql is always checked + # Default: [] + packages: + - github.com/jmoiron/sqlx + + tenv: + # The option `all` will run against whole test files (`_test.go`) regardless of method/function signatures. + # Otherwise, only methods that take `*testing.T`, `*testing.B`, and `testing.TB` as arguments are checked. + # Default: false + all: true + + tagliatelle: + case: + rules: + # Support string case: + # `camel`, `pascal`, `kebab`, `snake`, `goCamel`, + # `goPascal`, `goKebab`, `goSnake`, `upper`, `lower` + json: snake + yaml: camel + xml: camel + linters: + disable-all: true enable: - - bodyclose - - deadcode - - depguard - - dogsled - - dupl - - errcheck - - exhaustive - - funlen - - goconst - # - gocritic - - gocyclo - - gofmt - - goimports - - revive - - gomnd - - goprintffuncname - - gosec - - gosimple - - govet - - ineffassign - - lll - - misspell - - nakedret - - noctx - - nolintlint - - rowserrcheck - - exportloopref - - staticcheck - - structcheck - - stylecheck - - typecheck - - unconvert - - unparam - - unused - - varcheck - - whitespace + ## enabled by default + - errcheck # checking for unchecked errors, these unchecked errors can be critical bugs in some cases + - gosimple # specializes in simplifying a code + - govet # reports suspicious constructs, such as Printf calls whose arguments do not align with the format string + - ineffassign # detects when assignments to existing variables are not used + - staticcheck # is a go vet on steroids, applying a ton of static analysis checks + - typecheck # like the front-end of a Go compiler, parses and type-checks Go code + - unused # checks for unused constants, variables, functions and types + ## disabled by default + - asasalint # checks for pass []any as any in variadic func(...any) + - asciicheck # checks that your code does not contain non-ASCII identifiers + - bidichk # checks for dangerous unicode character sequences + - bodyclose # checks whether HTTP response body is closed successfully + - cyclop # checks function and package cyclomatic complexity + - dupl # tool for code clone detection + - durationcheck # checks for two durations multiplied together + - errname # checks that sentinel errors are prefixed with the Err and error types are suffixed with the Error + #- errorlint # finds code that will cause problems with the error wrapping scheme introduced in Go 1.13 + - execinquery # checks query string in Query function which reads your Go src files and warning it finds + - exhaustive # checks exhaustiveness of enum switch statements + - exportloopref # checks for pointers to enclosing loop variables + - forbidigo # forbids identifiers + - funlen # tool for detection of long functions + - gochecknoglobals # checks that no global variables exist + #- gochecknoinits # checks that no init functions are present in Go code + - gocognit # computes and checks the cognitive complexity of functions + - goconst # finds repeated strings that could be replaced by a constant + - gocritic # provides diagnostics that check for bugs, performance and style issues + - gocyclo # computes and checks the cyclomatic complexity of functions + - godot # checks if comments end in a period + - goimports # in addition to fixing imports, goimports also formats your code in the same style as gofmt + - gomnd # detects magic numbers + - gomoddirectives # manages the use of 'replace', 'retract', and 'excludes' directives in go.mod + - gomodguard # allow and block lists linter for direct Go module dependencies. This is different from depguard where there are different block types for example version constraints and module recommendations + - goprintffuncname # checks that printf-like functions are named with f at the end + - gosec # inspects source code for security problems + - lll # reports long lines + - loggercheck # checks key value pairs for common logger libraries (kitlog,klog,logr,zap) + - makezero # finds slice declarations with non-zero initial length + - nakedret # finds naked returns in functions greater than a specified function length + - nestif # reports deeply nested if statements + - nilerr # finds the code that returns nil even if it checks that the error is not nil + - nilnil # checks that there is no simultaneous return of nil error and an invalid value + - noctx # finds sending http request without context.Context + - nolintlint # reports ill-formed or insufficient nolint directives + # - nonamedreturns # reports all named returns + - nosprintfhostport # checks for misuse of Sprintf to construct a host with port in a URL + - predeclared # finds code that shadows one of Go's predeclared identifiers + - promlinter # checks Prometheus metrics naming via promlint + - reassign # checks that package variables are not reassigned + - revive # fast, configurable, extensible, flexible, and beautiful linter for Go, drop-in replacement of golint + - rowserrcheck # checks whether Err of rows is checked successfully + - sqlclosecheck # checks that sql.Rows and sql.Stmt are closed + - stylecheck # is a replacement for golint + - tenv # detects using os.Setenv instead of t.Setenv since Go1.17 + - testableexamples # checks if examples are testable (have an expected output) + - tparallel # detects inappropriate usage of t.Parallel() method in your Go test codes + - unconvert # removes unnecessary type conversions + - unparam # reports unused function parameters + - usestdlibvars # detects the possibility to use variables/constants from the Go standard library + - wastedassign # finds wasted assignment statements + - whitespace # detects leading and trailing whitespace + + ## you may want to enable + #- decorder # checks declaration order and count of types, constants, variables and functions + #- exhaustruct # checks if all structure fields are initialized + #- gci # controls golang package import order and makes it always deterministic + #- godox # detects FIXME, TODO and other comment keywords + #- goheader # checks is file header matches to pattern + #- interfacebloat # checks the number of methods inside an interface + #- ireturn # accept interfaces, return concrete types + #- prealloc # [premature optimization, but can be used in some cases] finds slice declarations that could potentially be preallocated + #- varnamelen # [great idea, but too many false positives] checks that the length of a variable's name matches its scope + #- wrapcheck # checks that errors returned from external packages are wrapped + + ## disabled + #- containedctx # detects struct contained context.Context field + #- contextcheck # [too many false positives] checks the function whether use a non-inherited context + #- depguard # [replaced by gomodguard] checks if package imports are in a list of acceptable packages + #- dogsled # checks assignments with too many blank identifiers (e.g. x, _, _, _, := f()) + #- dupword # [useless without config] checks for duplicate words in the source code + #- errchkjson # [don't see profit + I'm against of omitting errors like in the first example https://github.com/breml/errchkjson] checks types passed to the json encoding functions. Reports unsupported types and optionally reports occasions, where the check for the returned error can be omitted + #- forcetypeassert # [replaced by errcheck] finds forced type assertions + #- goerr113 # [too strict] checks the errors handling expressions + #- gofmt # [replaced by goimports] checks whether code was gofmt-ed + #- gofumpt # [replaced by goimports, gofumports is not available yet] checks whether code was gofumpt-ed + #- grouper # analyzes expression groups + #- importas # enforces consistent import aliases + #- maintidx # measures the maintainability index of each function + #- misspell # [useless] finds commonly misspelled English words in comments + #- nlreturn # [too strict and mostly code is not more readable] checks for a new line before return and branch statements to increase code clarity + #- paralleltest # [too many false positives] detects missing usage of t.Parallel() method in your Go test + #- tagliatelle # checks the struct tags + #- thelper # detects golang test helpers without t.Helper() call and checks the consistency of test helpers + #- wsl # [too strict and mostly code is not more readable] whitespace linter forces you to use empty lines + + ## deprecated + #- deadcode # [deprecated, replaced by unused] finds unused code + #- exhaustivestruct # [deprecated, replaced by exhaustruct] checks if all struct's fields are initialized + #- golint # [deprecated, replaced by revive] golint differs from gofmt. Gofmt reformats Go source code, whereas golint prints out style mistakes + #- ifshort # [deprecated] checks that your code uses short syntax for if-statements whenever possible + #- interfacer # [deprecated] suggests narrower interface types + #- maligned # [deprecated, replaced by govet fieldalignment] detects Go structs that would take less memory if their fields were sorted + #- nosnakecase # [deprecated, replaced by revive var-naming] detects snake case of variable naming and function name + #- scopelint # [deprecated, replaced by exportloopref] checks for unpinned variables in go programs + #- structcheck # [deprecated, replaced by unused] finds unused struct fields + #- varcheck # [deprecated, replaced by unused] finds unused global variables and constants + issues: - # Excluding configuration per-path, per-linter, per-text and per-source + # Maximum count of issues with the same text. + # Set to 0 to disable. + # Default: 3 + max-same-issues: 50 + exclude-rules: - - path: _test\.go + - source: "^//\\s*go:generate\\s" + linters: [ lll ] + - source: "(noinspection|TODO)" + linters: [ godot ] + - source: "//noinspection" + linters: [ gocritic ] + - source: "^\\s+if _, ok := err\\.\\([^.]+\\.InternalError\\); ok {" + linters: [ errorlint ] + - path: "_test\\.go" linters: + - bodyclose + - dupl + - funlen + - goconst + - gosec + - noctx + - wrapcheck - gomnd - exportloopref - gocyclo - errcheck - - dupl - - gosec - lll - - # https://github.com/go-critic/go-critic/issues/926 - - linters: - - gocritic - text: "unnecessaryDefer:" - - # TODO temporary rule, must be removed - # related to https://github.com/golangci/golangci-lint/pull/1756 - # must be replaced by '//nolint:staticcheck // require changes in github.com/OpenPeeDeeP/depguard' - - path: pkg/golinters/depguard.go - text: 'SA1019: package golang.org/x/tools/go/loader is deprecated' - - # TODO temporary rule, must be removed - # related to https://github.com/golangci/golangci-lint/pull/1756 - # must be replaced by '///nolint:staticcheck // it's an adapter for golang.org/x/tools/go/packages' - - path: pkg/golinters/goanalysis/adapters.go - text: 'SA1019: package golang.org/x/tools/go/loader is deprecated' - -run: - skip-dirs: - - test/testdata_etc - - internal/cache - - internal/renameio - - internal/robustio - -# golangci.com configuration -# https://github.com/golangci/golangci/wiki/Configuration -service: - golangci-lint-version: 1.23.x # use the fixed version to not introduce new linters unexpectedly - prepare: - - echo "here I can run custom commands, but no preparation needed for this repo" + - gochecknoglobals diff --git a/helper_test.go b/helper_test.go index b67ea18..1ab21f8 100644 --- a/helper_test.go +++ b/helper_test.go @@ -65,21 +65,21 @@ func Floats[T float](size int) []T { return out } -func IntTests[T integer](tests, cap int) [][]T { +func IntTests[T integer](tests, max int) [][]T { out := make([][]T, tests) for i := range out { - out[i] = Ints[T](cap) + out[i] = Ints[T](max) } return out } -func FloatTests[T float](tests, cap int) [][]T { +func FloatTests[T float](tests, max int) [][]T { out := make([][]T, tests) for i := range out { - out[i] = Floats[T](cap) + out[i] = Floats[T](max) } return out diff --git a/helpers.go b/helpers.go index 6a33d41..1570157 100644 --- a/helpers.go +++ b/helpers.go @@ -4,6 +4,8 @@ import "context" // defaultCtx is the default context used by the stream package. This is // hardcoded to context.Background() but can be overridden by the unit tests. +// +//nolint:gochecknoglobals // this is on purpose var defaultCtx = context.Background() // _ctx returns a valid Context with CancelFunc even if it the diff --git a/scaler.go b/scaler.go index 2e692ee..dd91175 100644 --- a/scaler.go +++ b/scaler.go @@ -30,22 +30,38 @@ import ( // is blocking and the Wait time has been reached, then the Scaler will spawn // a new layer2 which will increase throughput for the Scaler, and Scaler // will attempt to send the data to the layer2 channel once more. This process -// will repeat until a successful send occurs. (This should only loop twice) +// will repeat until a successful send occurs. (This should only loop twice). type Scaler[T, U any] struct { Wait time.Duration Life time.Duration Fn InterceptFunc[T, U] + + // WaitModifier is used to modify the Wait time based on the number of + // times the Scaler has scaled up. This is useful for systems + // that are CPU bound and need to scale up more quickly. + WaitModifier DurationScaler + + wScale *DurationScaler } +var ErrFnRequired = fmt.Errorf("nil InterceptFunc, Fn is required") + // Exec starts the internal Scaler routine (the first layer of processing) and // returns the output channel where the resulting data from the Fn function // will be sent. +// +//nolint:funlen // This really can't be broken up any further func (s Scaler[T, U]) Exec(ctx context.Context, in <-chan T) (<-chan U, error) { ctx = _ctx(ctx) + // set the configured tick as a pointer for execution + s.wScale = &s.WaitModifier + // set the original wait time on the ticker + s.wScale.originalDuration = s.Wait + // Fn is REQUIRED! if s.Fn == nil { - return nil, fmt.Errorf("invalid InterceptFunc") + return nil, ErrFnRequired } // Create outbound channel @@ -81,6 +97,8 @@ func (s Scaler[T, U]) Exec(ctx context.Context, in <-chan T) (<-chan U, error) { l2 := make(chan T) ticker := time.NewTicker(s.Wait) defer ticker.Stop() + step := 0 + var stepMu sync.RWMutex scaleLoop: for { @@ -102,8 +120,21 @@ func (s Scaler[T, U]) Exec(ctx context.Context, in <-chan T) (<-chan U, error) { wg.Add(1) wgMu.Unlock() + if !s.WaitModifier.inactive() { + stepMu.Lock() + step++ + stepMu.Unlock() + } + go func() { defer wg.Done() + if !s.WaitModifier.inactive() { + defer func() { + stepMu.Lock() + step-- + stepMu.Unlock() + }() + } Pipe(ctx, s.layer2(ctx, l2), out) }() @@ -112,9 +143,11 @@ func (s Scaler[T, U]) Exec(ctx context.Context, in <-chan T) (<-chan U, error) { } } + stepMu.RLock() // Reset the ticker so that it does not immediately trip the // case statement on loop. - ticker.Reset(s.Wait) + ticker.Reset(s.wScale.scaledDuration(s.Wait, step)) + stepMu.RUnlock() } } }() @@ -181,3 +214,54 @@ func (s Scaler[T, U]) layer2(ctx context.Context, in <-chan T) <-chan U { return out } + +// DurationScaler is used to modify the time.Duration of a ticker or timer based on +// a configured step value and modifier (between -1 and 1) value. +type DurationScaler struct { + // Interval is the number the current step must be divisible by in order + // to modify the time.Duration. + Interval int + + // ScalingFactor is a value between -1 and 1 that is used to modify the + // time.Duration of a ticker or timer. The value is multiplied by + // the ScalingFactor is multiplied by the duration for scaling. + ScalingFactor float64 + + // originalDuration is the time.Duration that was passed to the + // Scaler. This is used to reset the time.Duration of the ticker + // or timer. + originalDuration time.Duration + + // lastInterval is the lastInterval step that was used to modify + // the time.Duration. + lastInterval int +} + +func (t *DurationScaler) inactive() bool { + return t.Interval == 0 || + (t.ScalingFactor == 0 || + t.ScalingFactor <= -1 || + t.ScalingFactor >= 1) +} + +// scaledDuration returns the modified time.Duration based on the current step (cStep). +func (t *DurationScaler) scaledDuration( + dur time.Duration, + currentInterval int, +) time.Duration { + if t.inactive() { + return dur + } + + mod := t.ScalingFactor + if currentInterval <= t.lastInterval { + mod = -mod + } + + if currentInterval%t.Interval == 0 { + t.lastInterval = currentInterval + return dur + time.Duration(float64(t.originalDuration)*mod) + } + + return dur +} diff --git a/scaler_test.go b/scaler_test.go index 689d001..bc0bfed 100644 --- a/scaler_test.go +++ b/scaler_test.go @@ -35,7 +35,7 @@ func ScalerTest[U ~[]T, T comparable]( } // Test that the scaler can be used with a nil context. - //nolint:staticcheck + //nolint:staticcheck // nil context on purpose out, err := s.Exec(nil, testdata.Chan(ctx)) if err != nil { t.Errorf("expected no error, got %v", err) @@ -84,7 +84,7 @@ func Test_Scaler_Exec(t *testing.T) { func Test_Scaler_NilFn(t *testing.T) { s := Scaler[any, any]{} - //nolint:staticcheck + //nolint:staticcheck // nil context on purpose _, err := s.Exec(nil, nil) if err == nil { t.Error("Expected error, got nil") @@ -108,7 +108,7 @@ func Test_Scaler_NilCtx(t *testing.T) { cancel() // Test that the scaler can be used with a nil context. - //nolint:staticcheck + //nolint:staticcheck // nil context on purpose out, err := s.Exec(nil, nil) if err != nil { t.Errorf("expected no error, got %v", err) @@ -133,7 +133,7 @@ func Test_Scaler_CloseIn(t *testing.T) { close(in) // Test that the scaler can be used with a nil context. - //nolint:staticcheck + //nolint:staticcheck // nil context on purpose out, err := s.Exec(nil, in) if err != nil { t.Errorf("expected no error, got %v", err) @@ -286,3 +286,112 @@ func Test_Scaler_layer2_nosend(t *testing.T) { t.Fatalf("expected 0 data to be sent, got 1") } } + +func TestTickDur(t *testing.T) { + testCases := []struct { + name string + tick DurationScaler + duration time.Duration + currentStep int + expected time.Duration + }{ + { + name: "Test case 1", + tick: DurationScaler{Interval: 3, ScalingFactor: 0.1, originalDuration: 10 * time.Second}, + duration: 10 * time.Second, + currentStep: 3, + expected: 11 * time.Second, + }, + { + name: "Test case 2", + tick: DurationScaler{Interval: 5, ScalingFactor: -0.1, originalDuration: 20 * time.Second}, + duration: 20 * time.Second, + currentStep: 10, + expected: 18 * time.Second, + }, + { + name: "Test case 3", + tick: DurationScaler{Interval: 2, ScalingFactor: 0.5, originalDuration: 10 * time.Second}, + duration: 10 * time.Second, + currentStep: 4, + expected: 15 * time.Second, + }, + { + name: "Test case 4", + tick: DurationScaler{Interval: 4, ScalingFactor: -0.5, originalDuration: 30 * time.Second}, + duration: 30 * time.Second, + currentStep: 8, + expected: 15 * time.Second, + }, + { + name: "Test case 5", + tick: DurationScaler{Interval: 3, ScalingFactor: 0.1, originalDuration: 10 * time.Second}, + duration: 10 * time.Second, + currentStep: 2, + expected: 10 * time.Second, + }, + { + name: "Test case 6: Step is divisible, modifier in range", + tick: DurationScaler{Interval: 3, ScalingFactor: 0.1, originalDuration: 10 * time.Second}, + duration: 10 * time.Second, + currentStep: 3, + expected: 11 * time.Second, + }, + { + name: "Test case 7: Step is not divisible, modifier in range", + tick: DurationScaler{Interval: 3, ScalingFactor: 0.1, originalDuration: 10 * time.Second}, + duration: 10 * time.Second, + currentStep: 2, + expected: 10 * time.Second, + }, + { + name: "Test case 8: Step is divisible, modifier is zero", + tick: DurationScaler{Interval: 3, ScalingFactor: 0, originalDuration: 10 * time.Second}, + duration: 10 * time.Second, + currentStep: 3, + expected: 10 * time.Second, + }, + { + name: "Test case 9: Step is divisible, modifier is out of range", + tick: DurationScaler{Interval: 3, ScalingFactor: 1, originalDuration: 10 * time.Second}, + duration: 10 * time.Second, + currentStep: 3, + expected: 10 * time.Second, + }, + { + name: "Test case 10: Step is zero, modifier in range", + tick: DurationScaler{Interval: 0, ScalingFactor: 0.1, originalDuration: 10 * time.Second}, + duration: 10 * time.Second, + currentStep: 3, + expected: 10 * time.Second, + }, + { + name: "Test case 6: Step number decreases", + tick: DurationScaler{ + Interval: 2, + ScalingFactor: 0.5, + originalDuration: 10 * time.Second, + lastInterval: 4, + }, + duration: 15 * time.Second, + currentStep: 2, + expected: 10 * time.Second, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + result := (&tc.tick).scaledDuration(tc.duration, tc.currentStep) + if result != tc.expected { + t.Errorf("Expected: %v, got: %v", tc.expected, result) + } + }) + } +} + +func FuzzTick(f *testing.F) { + f.Fuzz(func(t *testing.T, step, cStep int, mod float64, orig, dur int64) { + tick := &DurationScaler{Interval: step, ScalingFactor: mod, originalDuration: time.Duration(orig)} + _ = tick.scaledDuration(time.Duration(dur), cStep) + }) +} diff --git a/stream_bench_test.go b/stream_bench_test.go index 5a737a2..5f53487 100644 --- a/stream_bench_test.go +++ b/stream_bench_test.go @@ -134,7 +134,7 @@ func Benchmark_Scaler(b *testing.B) { for n := 0; n < b.N; n++ { // Test that the scaler can be used with a nil context. - //nolint:staticcheck + //nolint:staticcheck // nil context on purpose out, err := s.Exec(nil, testdata.Chan(ctx)) if err != nil { b.Errorf("expected no error, got %v", err) diff --git a/stream_test.go b/stream_test.go index e333e50..fcc09ce 100644 --- a/stream_test.go +++ b/stream_test.go @@ -277,7 +277,7 @@ func Test_Intercept_NotOk(t *testing.T) { } } -func Test_Intercept_ClosedChan(t *testing.T) { +func Test_Intercept_ClosedChan(_ *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() @@ -322,7 +322,7 @@ func Test_Intercept_Canceled_On_Wait(t *testing.T) { } } -func Test_FanOut_Canceled_On_Wait(t *testing.T) { +func Test_FanOut_Canceled_On_Wait(_ *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() @@ -415,7 +415,7 @@ func Test_Distribute(t *testing.T) { DistributeTest(t, "float64", FloatTests[float64](100, 1000)) } -func Test_Distribute_Canceled_On_Wait(t *testing.T) { +func Test_Distribute_Canceled_On_Wait(_ *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() @@ -431,7 +431,7 @@ func Test_Distribute_Canceled_On_Wait(t *testing.T) { Distribute(ctx, in, out) } -func Test_Distribute_ZeroOut(t *testing.T) { +func Test_Distribute_ZeroOut(_ *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() @@ -501,7 +501,7 @@ func Test_FanOut(t *testing.T) { } } -func Test_FanOut_ZeroOut(t *testing.T) { +func Test_FanOut_ZeroOut(_ *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() @@ -511,7 +511,7 @@ func Test_FanOut_ZeroOut(t *testing.T) { FanOut(ctx, in) } -func Test_FanIn_ZeroIn(t *testing.T) { +func Test_FanIn_ZeroIn(_ *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel()