├── .github └── workflows │ └── codeql.yml ├── .gitignore ├── .golangci.yaml ├── LICENSE ├── Makefile ├── README.md ├── assert.go ├── assert_debug.go ├── assert_debug_nop.go ├── assert_test.go ├── go.mod └── types.go /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL Advanced" 13 | 14 | on: 15 | push: 16 | branches: [ "main" ] 17 | pull_request: 18 | branches: [ "main" ] 19 | schedule: 20 | - cron: '18 5 * * 2' 21 | 22 | jobs: 23 | analyze: 24 | name: Analyze (${{ matrix.language }}) 25 | # Runner size impacts CodeQL analysis time. To learn more, please see: 26 | # - https://gh.io/recommended-hardware-resources-for-running-codeql 27 | # - https://gh.io/supported-runners-and-hardware-resources 28 | # - https://gh.io/using-larger-runners (GitHub.com only) 29 | # Consider using larger runners or machines with greater resources for possible analysis time improvements. 30 | runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} 31 | permissions: 32 | # required for all workflows 33 | security-events: write 34 | 35 | # required to fetch internal or private CodeQL packs 36 | packages: read 37 | 38 | # only required for workflows in private repositories 39 | actions: read 40 | contents: read 41 | 42 | strategy: 43 | fail-fast: false 44 | matrix: 45 | include: 46 | - language: go 47 | build-mode: autobuild 48 | # CodeQL supports the following values keywords for 'language': 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift' 49 | # Use `c-cpp` to analyze code written in C, C++ or both 50 | # Use 'java-kotlin' to analyze code written in Java, Kotlin or both 51 | # Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both 52 | # To learn more about changing the languages that are analyzed or customizing the build mode for your analysis, 53 | # see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning. 54 | # If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how 55 | # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages 56 | steps: 57 | - name: Checkout repository 58 | uses: actions/checkout@v4 59 | 60 | # Add any setup steps before running the `github/codeql-action/init` action. 61 | # This includes steps like installing compilers or runtimes (`actions/setup-node` 62 | # or others). This is typically only required for manual builds. 63 | # - name: Setup runtime (example) 64 | # uses: actions/setup-example@v1 65 | 66 | # Initializes the CodeQL tools for scanning. 67 | - name: Initialize CodeQL 68 | uses: github/codeql-action/init@v3 69 | with: 70 | languages: ${{ matrix.language }} 71 | build-mode: ${{ matrix.build-mode }} 72 | # If you wish to specify custom queries, you can do so here or in a config file. 73 | # By default, queries listed here will override any specified in a config file. 74 | # Prefix the list here with "+" to use these queries and those in the config file. 75 | 76 | # For more 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 77 | # queries: security-extended,security-and-quality 78 | 79 | # If the analyze step fails for one of the languages you are analyzing with 80 | # "We were unable to automatically build your code", modify the matrix above 81 | # to set the build mode to "manual" for that language. Then modify this step 82 | # to build your code. 83 | # ℹ️ Command-line programs to run using the OS shell. 84 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 85 | - if: matrix.build-mode == 'manual' 86 | shell: bash 87 | run: | 88 | echo 'If you are using a "manual" build mode for one or more of the' \ 89 | 'languages you are analyzing, replace this with the commands to build' \ 90 | 'your code, for example:' 91 | echo ' make bootstrap' 92 | echo ' make release' 93 | exit 1 94 | 95 | - name: Perform CodeQL Analysis 96 | uses: github/codeql-action/analyze@v3 97 | with: 98 | category: "/language:${{matrix.language}}" 99 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # If you prefer the allow list template instead of the deny list, see community template: 2 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore 3 | # 4 | # Binaries for programs and plugins 5 | *.exe 6 | *.exe~ 7 | *.dll 8 | *.so 9 | *.dylib 10 | 11 | # Test binary, built with `go test -c` 12 | *.test 13 | 14 | # Output of the go coverage tool, specifically when used with LiteIDE 15 | *.out 16 | 17 | # Dependency directories (remove the comment below to include it) 18 | # vendor/ 19 | 20 | # Go workspace file 21 | go.work 22 | go.work.sum 23 | 24 | # env file 25 | .env 26 | -------------------------------------------------------------------------------- /.golangci.yaml: -------------------------------------------------------------------------------- 1 | # This code is licensed under the terms of the MIT license https://opensource.org/license/mit 2 | # Copyright (c) 2021 Marat Reymers 3 | 4 | ## Golden config for golangci-lint v1.62.0 5 | # 6 | # This is the best config for golangci-lint based on my experience and opinion. 7 | # It is very strict, but not extremely strict. 8 | # Feel free to adapt and change it for your needs. 9 | 10 | run: 11 | # Timeout for analysis, e.g. 30s, 5m. 12 | # Default: 1m 13 | timeout: 3m 14 | 15 | # This file contains only configs which differ from defaults. 16 | # All possible options can be found here https://github.com/golangci/golangci-lint/blob/master/.golangci.reference.yml 17 | linters-settings: 18 | cyclop: 19 | # The maximal code complexity to report. 20 | # Default: 10 21 | max-complexity: 30 22 | # The maximal average package complexity. 23 | # If it's higher than 0.0 (float) the check is enabled 24 | # Default: 0.0 25 | package-average: 10.0 26 | 27 | errcheck: 28 | # Report about not checking of errors in type assertions: `a := b.(MyStruct)`. 29 | # Such cases aren't reported by default. 30 | # Default: false 31 | check-type-assertions: true 32 | 33 | exhaustive: 34 | # Program elements to check for exhaustiveness. 35 | # Default: [ switch ] 36 | check: 37 | - switch 38 | - map 39 | 40 | exhaustruct: 41 | # List of regular expressions to exclude struct packages and their names from checks. 42 | # Regular expressions must match complete canonical struct package/name/structname. 43 | # Default: [] 44 | exclude: 45 | # std libs 46 | - "^net/http.Client$" 47 | - "^net/http.Cookie$" 48 | - "^net/http.Request$" 49 | - "^net/http.Response$" 50 | - "^net/http.Server$" 51 | - "^net/http.Transport$" 52 | - "^net/url.URL$" 53 | - "^os/exec.Cmd$" 54 | - "^reflect.StructField$" 55 | # public libs 56 | - "^github.com/Shopify/sarama.Config$" 57 | - "^github.com/Shopify/sarama.ProducerMessage$" 58 | - "^github.com/mitchellh/mapstructure.DecoderConfig$" 59 | - "^github.com/prometheus/client_golang/.+Opts$" 60 | - "^github.com/spf13/cobra.Command$" 61 | - "^github.com/spf13/cobra.CompletionOptions$" 62 | - "^github.com/stretchr/testify/mock.Mock$" 63 | - "^github.com/testcontainers/testcontainers-go.+Request$" 64 | - "^github.com/testcontainers/testcontainers-go.FromDockerfile$" 65 | - "^golang.org/x/tools/go/analysis.Analyzer$" 66 | - "^google.golang.org/protobuf/.+Options$" 67 | - "^gopkg.in/yaml.v3.Node$" 68 | 69 | funlen: 70 | # Checks the number of lines in a function. 71 | # If lower than 0, disable the check. 72 | # Default: 60 73 | lines: 100 74 | # Checks the number of statements in a function. 75 | # If lower than 0, disable the check. 76 | # Default: 40 77 | statements: 50 78 | # Ignore comments when counting lines. 79 | # Default false 80 | ignore-comments: true 81 | 82 | gochecksumtype: 83 | # Presence of `default` case in switch statements satisfies exhaustiveness, if all members are not listed. 84 | # Default: true 85 | default-signifies-exhaustive: false 86 | 87 | gocognit: 88 | # Minimal code complexity to report. 89 | # Default: 30 (but we recommend 10-20) 90 | min-complexity: 20 91 | 92 | gocritic: 93 | # Settings passed to gocritic. 94 | # The settings key is the name of a supported gocritic checker. 95 | # The list of supported checkers can be find in https://go-critic.github.io/overview. 96 | settings: 97 | captLocal: 98 | # Whether to restrict checker to params only. 99 | # Default: true 100 | paramsOnly: false 101 | underef: 102 | # Whether to skip (*x).method() calls where x is a pointer receiver. 103 | # Default: true 104 | skipRecvDeref: false 105 | 106 | gomodguard: 107 | blocked: 108 | # List of blocked modules. 109 | # Default: [] 110 | modules: 111 | - github.com/golang/protobuf: 112 | recommendations: 113 | - google.golang.org/protobuf 114 | reason: "see https://developers.google.com/protocol-buffers/docs/reference/go/faq#modules" 115 | - github.com/satori/go.uuid: 116 | recommendations: 117 | - github.com/google/uuid 118 | reason: "satori's package is not maintained" 119 | - github.com/gofrs/uuid: 120 | recommendations: 121 | - github.com/gofrs/uuid/v5 122 | reason: "gofrs' package was not go module before v5" 123 | 124 | govet: 125 | # Enable all analyzers. 126 | # Default: false 127 | enable-all: true 128 | # Disable analyzers by name. 129 | # Run `go tool vet help` to see all analyzers. 130 | # Default: [] 131 | disable: 132 | - fieldalignment # too strict 133 | # Settings per analyzer. 134 | settings: 135 | shadow: 136 | # Whether to be strict about shadowing; can be noisy. 137 | # Default: false 138 | strict: true 139 | 140 | inamedparam: 141 | # Skips check for interface methods with only a single parameter. 142 | # Default: false 143 | skip-single-param: true 144 | 145 | mnd: 146 | # List of function patterns to exclude from analysis. 147 | # Values always ignored: `time.Date`, 148 | # `strconv.FormatInt`, `strconv.FormatUint`, `strconv.FormatFloat`, 149 | # `strconv.ParseInt`, `strconv.ParseUint`, `strconv.ParseFloat`. 150 | # Default: [] 151 | ignored-functions: 152 | - args.Error 153 | - flag.Arg 154 | - flag.Duration.* 155 | - flag.Float.* 156 | - flag.Int.* 157 | - flag.Uint.* 158 | - os.Chmod 159 | - os.Mkdir.* 160 | - os.OpenFile 161 | - os.WriteFile 162 | - prometheus.ExponentialBuckets.* 163 | - prometheus.LinearBuckets 164 | 165 | nakedret: 166 | # Make an issue if func has more lines of code than this setting, and it has naked returns. 167 | # Default: 30 168 | max-func-lines: 0 169 | 170 | nolintlint: 171 | # Exclude following linters from requiring an explanation. 172 | # Default: [] 173 | allow-no-explanation: [funlen, gocognit, lll] 174 | # Enable to require an explanation of nonzero length after each nolint directive. 175 | # Default: false 176 | require-explanation: true 177 | # Enable to require nolint directives to mention the specific linter being suppressed. 178 | # Default: false 179 | require-specific: true 180 | 181 | perfsprint: 182 | # Optimizes into strings concatenation. 183 | # Default: true 184 | strconcat: false 185 | 186 | reassign: 187 | # Patterns for global variable names that are checked for reassignment. 188 | # See https://github.com/curioswitch/go-reassign#usage 189 | # Default: ["EOF", "Err.*"] 190 | patterns: 191 | - ".*" 192 | 193 | rowserrcheck: 194 | # database/sql is always checked 195 | # Default: [] 196 | packages: 197 | - github.com/jmoiron/sqlx 198 | 199 | sloglint: 200 | # Enforce not using global loggers. 201 | # Values: 202 | # - "": disabled 203 | # - "all": report all global loggers 204 | # - "default": report only the default slog logger 205 | # https://github.com/go-simpler/sloglint?tab=readme-ov-file#no-global 206 | # Default: "" 207 | no-global: "all" 208 | # Enforce using methods that accept a context. 209 | # Values: 210 | # - "": disabled 211 | # - "all": report all contextless calls 212 | # - "scope": report only if a context exists in the scope of the outermost function 213 | # https://github.com/go-simpler/sloglint?tab=readme-ov-file#context-only 214 | # Default: "" 215 | context: "scope" 216 | 217 | tenv: 218 | # The option `all` will run against whole test files (`_test.go`) regardless of method/function signatures. 219 | # Otherwise, only methods that take `*testing.T`, `*testing.B`, and `testing.TB` as arguments are checked. 220 | # Default: false 221 | all: true 222 | 223 | linters: 224 | disable-all: true 225 | enable: 226 | ## enabled by default 227 | - errcheck # checking for unchecked errors, these unchecked errors can be critical bugs in some cases 228 | - gosimple # specializes in simplifying a code 229 | - govet # reports suspicious constructs, such as Printf calls whose arguments do not align with the format string 230 | - ineffassign # detects when assignments to existing variables are not used 231 | - staticcheck # is a go vet on steroids, applying a ton of static analysis checks 232 | - typecheck # like the front-end of a Go compiler, parses and type-checks Go code 233 | - unused # checks for unused constants, variables, functions and types 234 | ## disabled by default 235 | - asasalint # checks for pass []any as any in variadic func(...any) 236 | - asciicheck # checks that your code does not contain non-ASCII identifiers 237 | - bidichk # checks for dangerous unicode character sequences 238 | - bodyclose # checks whether HTTP response body is closed successfully 239 | - canonicalheader # checks whether net/http.Header uses canonical header 240 | - copyloopvar # detects places where loop variables are copied (Go 1.22+) 241 | - cyclop # checks function and package cyclomatic complexity 242 | - dupl # tool for code clone detection 243 | - durationcheck # checks for two durations multiplied together 244 | - errname # checks that sentinel errors are prefixed with the Err and error types are suffixed with the Error 245 | - errorlint # finds code that will cause problems with the error wrapping scheme introduced in Go 1.13 246 | - exhaustive # checks exhaustiveness of enum switch statements 247 | - fatcontext # detects nested contexts in loops 248 | - forbidigo # forbids identifiers 249 | - funlen # tool for detection of long functions 250 | - gocheckcompilerdirectives # validates go compiler directive comments (//go:) 251 | - gochecknoglobals # checks that no global variables exist 252 | - gochecknoinits # checks that no init functions are present in Go code 253 | - gochecksumtype # checks exhaustiveness on Go "sum types" 254 | - gocognit # computes and checks the cognitive complexity of functions 255 | - goconst # finds repeated strings that could be replaced by a constant 256 | - gocritic # provides diagnostics that check for bugs, performance and style issues 257 | - gocyclo # computes and checks the cyclomatic complexity of functions 258 | - godot # checks if comments end in a period 259 | - goimports # in addition to fixing imports, goimports also formats your code in the same style as gofmt 260 | - gomoddirectives # manages the use of 'replace', 'retract', and 'excludes' directives in go.mod 261 | - 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 262 | - goprintffuncname # checks that printf-like functions are named with f at the end 263 | - gosec # inspects source code for security problems 264 | - iface # checks the incorrect use of interfaces, helping developers avoid interface pollution 265 | - intrange # finds places where for loops could make use of an integer range 266 | - lll # reports long lines 267 | - loggercheck # checks key value pairs for common logger libraries (kitlog,klog,logr,zap) 268 | - makezero # finds slice declarations with non-zero initial length 269 | - mirror # reports wrong mirror patterns of bytes/strings usage 270 | - mnd # detects magic numbers 271 | - musttag # enforces field tags in (un)marshaled structs 272 | - nakedret # finds naked returns in functions greater than a specified function length 273 | - nestif # reports deeply nested if statements 274 | - nilerr # finds the code that returns nil even if it checks that the error is not nil 275 | - nilnil # checks that there is no simultaneous return of nil error and an invalid value 276 | - noctx # finds sending http request without context.Context 277 | - nolintlint # reports ill-formed or insufficient nolint directives 278 | - nonamedreturns # reports all named returns 279 | - nosprintfhostport # checks for misuse of Sprintf to construct a host with port in a URL 280 | - perfsprint # checks that fmt.Sprintf can be replaced with a faster alternative 281 | - predeclared # finds code that shadows one of Go's predeclared identifiers 282 | - promlinter # checks Prometheus metrics naming via promlint 283 | - protogetter # reports direct reads from proto message fields when getters should be used 284 | - reassign # checks that package variables are not reassigned 285 | - recvcheck # checks for receiver type consistency 286 | - revive # fast, configurable, extensible, flexible, and beautiful linter for Go, drop-in replacement of golint 287 | - rowserrcheck # checks whether Err of rows is checked successfully 288 | - sloglint # ensure consistent code style when using log/slog 289 | - spancheck # checks for mistakes with OpenTelemetry/Census spans 290 | - sqlclosecheck # checks that sql.Rows and sql.Stmt are closed 291 | - stylecheck # is a replacement for golint 292 | - tenv # detects using os.Setenv instead of t.Setenv since Go1.17 293 | - testableexamples # checks if examples are testable (have an expected output) 294 | - testifylint # checks usage of github.com/stretchr/testify 295 | # - testpackage # makes you use a separate _test package # We have a single test file and that one requires access to private fields 296 | - tparallel # detects inappropriate usage of t.Parallel() method in your Go test codes 297 | - unconvert # removes unnecessary type conversions 298 | - unparam # reports unused function parameters 299 | - usestdlibvars # detects the possibility to use variables/constants from the Go standard library 300 | - wastedassign # finds wasted assignment statements 301 | - whitespace # detects leading and trailing whitespace 302 | 303 | ## you may want to enable 304 | #- decorder # checks declaration order and count of types, constants, variables and functions 305 | #- exhaustruct # [highly recommend to enable] checks if all structure fields are initialized 306 | #- gci # controls golang package import order and makes it always deterministic 307 | #- ginkgolinter # [if you use ginkgo/gomega] enforces standards of using ginkgo and gomega 308 | #- godox # detects FIXME, TODO and other comment keywords 309 | #- goheader # checks is file header matches to pattern 310 | #- inamedparam # [great idea, but too strict, need to ignore a lot of cases by default] reports interfaces with unnamed method parameters 311 | #- interfacebloat # checks the number of methods inside an interface 312 | #- ireturn # accept interfaces, return concrete types 313 | #- prealloc # [premature optimization, but can be used in some cases] finds slice declarations that could potentially be preallocated 314 | #- tagalign # checks that struct tags are well aligned 315 | #- varnamelen # [great idea, but too many false positives] checks that the length of a variable's name matches its scope 316 | #- wrapcheck # checks that errors returned from external packages are wrapped 317 | #- zerologlint # detects the wrong usage of zerolog that a user forgets to dispatch zerolog.Event 318 | 319 | ## disabled 320 | #- containedctx # detects struct contained context.Context field 321 | #- contextcheck # [too many false positives] checks the function whether use a non-inherited context 322 | #- depguard # [replaced by gomodguard] checks if package imports are in a list of acceptable packages 323 | #- dogsled # checks assignments with too many blank identifiers (e.g. x, _, _, _, := f()) 324 | #- dupword # [useless without config] checks for duplicate words in the source code 325 | #- err113 # [too strict] checks the errors handling expressions 326 | #- 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 327 | #- exportloopref # [not necessary from Go 1.22] checks for pointers to enclosing loop variables 328 | #- forcetypeassert # [replaced by errcheck] finds forced type assertions 329 | #- gofmt # [replaced by goimports] checks whether code was gofmt-ed 330 | #- gofumpt # [replaced by goimports, gofumports is not available yet] checks whether code was gofumpt-ed 331 | #- gosmopolitan # reports certain i18n/l10n anti-patterns in your Go codebase 332 | #- grouper # analyzes expression groups 333 | #- importas # enforces consistent import aliases 334 | #- maintidx # measures the maintainability index of each function 335 | #- misspell # [useless] finds commonly misspelled English words in comments 336 | #- nlreturn # [too strict and mostly code is not more readable] checks for a new line before return and branch statements to increase code clarity 337 | #- paralleltest # [too many false positives] detects missing usage of t.Parallel() method in your Go test 338 | #- tagliatelle # checks the struct tags 339 | #- thelper # detects golang test helpers without t.Helper() call and checks the consistency of test helpers 340 | #- wsl # [too strict and mostly code is not more readable] whitespace linter forces you to use empty lines 341 | 342 | issues: 343 | # Maximum count of issues with the same text. 344 | # Set to 0 to disable. 345 | # Default: 3 346 | max-same-issues: 50 347 | 348 | exclude-rules: 349 | - source: "(noinspection|TODO)" 350 | linters: [godot] 351 | - source: "//noinspection" 352 | linters: [gocritic] 353 | - path: "_test\\.go" 354 | linters: 355 | - bodyclose 356 | - dupl 357 | - errcheck 358 | - funlen 359 | - goconst 360 | - gosec 361 | - noctx 362 | - wrapcheck 363 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | ################################################################################ 2 | # Code Quality Targets 3 | ################################################################################ 4 | fmt: 5 | @go run mvdan.cc/gofumpt@latest -l -w . 6 | .PHONY: fmt 7 | 8 | lint: 9 | @go run github.com/golangci/golangci-lint/cmd/golangci-lint@v1.62.2 run --fix --timeout 5m 10 | .PHONY: lint 11 | 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 |   4 |

assert-go

5 |

Tiny (~100 LoC) Go assertion library focused on crystal-clear failure messages and thoughtful source context.

6 | 7 |   8 | 9 | [![go.dev reference](https://img.shields.io/badge/go.dev-reference-007d9c?logo=go&logoColor=white&style=flat)](https://pkg.go.dev/github.com/nikoksr/assert-go) 10 |
11 | 12 |   13 | 14 | ## About 15 | 16 | - 🔍 Crystal-clear failure messages with contextual values 17 | - 📚 Rich source context showing the exact failure location 18 | - 🛠 Tiny and free of dependencies (~100 lines of Go) 19 | - 💡 Elegant, idiomatic Go API 20 | - 🎯 Two-tier assertion system with build tag support 21 | - ⚙️ Configurable source context behavior 22 | 23 | Inspired by [Tiger Style](https://github.com/tigerbeetle/tigerbeetle/blob/main/docs/TIGER_STYLE.md#safety). 24 | 25 | ## Installation 26 | 27 | ```bash 28 | go get github.com/nikoksr/assert-go 29 | ``` 30 | 31 | ## Usage 32 | 33 | ### Basic Usage 34 | 35 | ```go 36 | import "github.com/nikoksr/assert-go" 37 | 38 | func PaymentProcessing() { 39 | payment := processPayment(PaymentRequest{ 40 | Amount: 99.99, 41 | CustomerID: "cust_123", 42 | Currency: "USD", 43 | }) 44 | 45 | // Assert payment was processed successfully 46 | assert.Assert(payment.Status == "completed", "payment should be completed", 47 | // Optionally, add context to the panic 48 | "payment_id", payment.ID, 49 | "status", payment.Status, 50 | "amount", payment.Amount, 51 | "error", payment.Error, 52 | "timestamp", payment.ProcessedAt, 53 | ) 54 | } 55 | ``` 56 | 57 | On failure, you get: 58 | 59 | ``` 60 | Assertion failed at payment_test.go:43 61 | Message: payment should be completed 62 | 63 | Relevant values: 64 | [payment_id]: "pmt_789" 65 | [status]: "failed" 66 | [amount]: 99.99 67 | [error]: "insufficient_funds" 68 | [timestamp]: "2024-12-06T15:04:05Z" 69 | 70 | Source context: 71 | 37 | payment := processPayment(PaymentRequest{ 72 | 38 | Amount: 99.99, 73 | 39 | CustomerID: "cust_123", 74 | 40 | Currency: "USD", 75 | 41 | }) 76 | 42 | 77 | → 43 | assert.Assert(payment.Status == "completed", "payment should be completed", 78 | 44 | "payment_id", payment.ID, 79 | 45 | "status", payment.Status, 80 | 46 | "amount", payment.Amount, 81 | 47 | "error", payment.Error, 82 | 48 | "timestamp", payment.ProcessedAt, 83 | 49 | ) 84 | 85 | goroutine 1 [running]: 86 | github.com/nikoksr/assert-go.PaymentProcessing(0xc00011c000) 87 | /app/payment.go:43 +0x1b4 88 | # ... regular Go stacktrace continues 89 | ``` 90 | 91 | ### Two-Tier Assertion System 92 | 93 | The library provides two types of assertions: 94 | 95 | 1. `Assert()` - Always active, meant for critical checks that should run in all environments 96 | 2. `Debug()` - Development-time assertions that can be disabled in production 97 | 98 | #### Using Debug Assertions 99 | 100 | Debug assertions are disabled by default. To enable them, use the `assertdebug` build tag: 101 | 102 | ```bash 103 | go test -tags assertdebug ./... 104 | go run -tags assertdebug main.go 105 | ``` 106 | 107 | Example usage: 108 | 109 | ```go 110 | // This will only be evaluated when built with -tags assertdebug 111 | assert.Debug(len(items) < 1000, "items list too large", 112 | "current_length", len(items), 113 | "max_allowed", 1000, 114 | ) 115 | 116 | // This will always be evaluated regardless of build tags 117 | assert.Assert(response != nil, "HTTP response cannot be nil", 118 | "status_code", response.StatusCode, 119 | ) 120 | ``` 121 | 122 | ### Configuration 123 | 124 | You can configure the assertion behavior: 125 | 126 | ```go 127 | // Configure assertion behavior 128 | assert.SetConfig(assert.Config{ 129 | // Enable/disable source context in error messages 130 | IncludeSource: true, 131 | // Number of context lines to show before and after the failing line 132 | ContextLines: 5, 133 | }) 134 | ``` 135 | 136 | ## A Personal Perspective on Assertions in Go 137 | 138 | I initially shared the common view that assertions don't align well with Go's philosophy of explicit error handling. Reading [TigerStyle's perspective on assertions](https://github.com/tigerbeetle/tigerbeetle/blob/main/docs/TIGER_STYLE.md#safety) made me reconsider this stance and experiment with them in my own code. 139 | 140 | I've found that assertions serve a distinct and valuable purpose alongside traditional error handling. While I handle operational failures - like network issues or invalid user input - through error returns, I now use assertions to catch programmer mistakes that should never occur in correct code. I used to write sanity checks that would return errors, wondering why I'm burdening users with checks for conditions that should be impossible to be false anyway given my code's structure – like a logger that I initialized and passed down myself just three function calls earlier. It feels weird to check for nil because I know that I just initialized this logger, it feels weird to return these types of errors to users, but at the same time, I always had this urge of checking for the "impossible". These aren't cases where graceful error handling makes sense; they're cases where continuing execution would only mask a fundamental bug in my code. 141 | 142 | I'm selective about where I use assertions. They belong in application code where I can make strong guarantees about internal state and invariants, particularly during system initialization. I don't use them in libraries or for validating application input - that's firmly error handling territory. But when I know something must be true for my program to be correct, assertions help me catch bugs early and prevent corrupted state from silently spreading. 143 | 144 | What started as an experiment has become an essential part of how I write Go. At this point, my own experience has convinced me that thoughtful use of assertions makes my code more reliable and bugs easier to diagnose. 145 | 146 | ## Philosophy 147 | 148 | - **Minimal**: Single-purpose library that does one thing well 149 | - **Context over complexity**: Rich debugging information without complex APIs 150 | - **Clear failures**: Source context shows exactly where and why things went wrong 151 | - **Idiomatic Go**: Feels natural in your Go codebase 152 | -------------------------------------------------------------------------------- /assert.go: -------------------------------------------------------------------------------- 1 | package assert 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "os" 7 | "path/filepath" 8 | "runtime" 9 | "strings" 10 | ) 11 | 12 | //nolint:gochecknoglobals // read-only global 13 | var activeConfig = Config{ 14 | IncludeSource: true, 15 | ContextLines: 5, //nolint:gomnd,mnd // default value 16 | } 17 | 18 | // SetConfig sets the configuration for the assertion library. 19 | func SetConfig(config Config) { 20 | activeConfig = config 21 | } 22 | 23 | // Assert panics if the condition is false. Gloabbly configurable via SetConfig. 24 | // 25 | // Assert is intended for critical checks that should always be active, regardless of the build configuration. Use 26 | // assert.Debug for non-critical checks that should only be active during development. 27 | // 28 | // WARN: This assertion is live! 29 | func Assert(condition bool, msg string, values ...any) { 30 | // We tell assert() to skip 2 frames here: 31 | // 1. The assert() function itself 32 | // 2. This Assert() function that calls assert() 33 | assert(condition, msg, 2, values...) //nolint:mnd // Explained in comment 34 | } 35 | 36 | // Assert panics if the condition is false. Configurable via SetConfig. 37 | // skipFrames is the number of stack frames to skip when getting the source context. 38 | func assert(condition bool, msg string, skipFrames int, values ...any) { 39 | if condition { 40 | return // Assertion met 41 | } 42 | 43 | 44 | _, file, line, ok := runtime.Caller(skipFrames) 45 | 46 | // Could not get Caller info 47 | if !ok { 48 | panic(AssertionError{ 49 | Message: msg, 50 | }) 51 | } 52 | // If values were provided for dumping 53 | numValues := len(values) 54 | if numValues%2 != 0 { 55 | values = append(values, "(MISSING)") 56 | } 57 | 58 | var dumpInfo string 59 | if numValues > 0 { 60 | dumpInfo = "\n\nRelevant values:\n" 61 | for i := 0; i < numValues; i += 2 { 62 | dumpInfo += fmt.Sprintf(" [%s]: %#v\n", values[i], values[i+1]) 63 | } 64 | } 65 | 66 | // Get source context if enabled 67 | var sourceContext string 68 | if activeConfig.IncludeSource { 69 | sourceContext = getSourceContext(file, line, activeConfig.ContextLines) 70 | } 71 | 72 | err := AssertionError{ 73 | Message: msg + dumpInfo, 74 | File: filepath.Base(file), 75 | Line: line, 76 | SourceContext: sourceContext, 77 | } 78 | 79 | panic(err) 80 | } 81 | 82 | // getSourceContext reads the source file and returns lines around the failure. 83 | func getSourceContext(file string, line int, contextLines int) string { 84 | f, err := os.Open(file) 85 | if err != nil { 86 | return "" 87 | } 88 | defer f.Close() 89 | 90 | scanner := bufio.NewScanner(f) 91 | 92 | start := max(1, line-contextLines) 93 | end := line + contextLines 94 | 95 | var lines []string 96 | currentLine := 1 97 | 98 | for scanner.Scan() { 99 | if currentLine >= start && currentLine <= end { 100 | prefix := " " 101 | if currentLine == line { 102 | prefix = "→ " 103 | } 104 | lines = append(lines, fmt.Sprintf("%s%4d | %s", prefix, currentLine, scanner.Text())) 105 | } 106 | 107 | if currentLine > end { 108 | break 109 | } 110 | 111 | currentLine++ 112 | } 113 | 114 | return strings.Join(lines, "\n") 115 | } 116 | -------------------------------------------------------------------------------- /assert_debug.go: -------------------------------------------------------------------------------- 1 | //go:build assertdebug 2 | // +build assertdebug 3 | 4 | package assert 5 | 6 | // Debug panics if the condition is false. Globally configurable via SetConfig. 7 | // 8 | // Debug is intended for non-critical checks that should only be active during development. For critical checks that 9 | // should always be active, regardless of the build configuration, use assert.Assert instead. 10 | // 11 | // WARN: Under the current build configuration, this assertion is enabled. 12 | func Debug(condition bool, msg string, values ...any) { 13 | // We tell assert() to skip 2 frames here: 14 | // 1. The assert() function itself 15 | // 2. This Debug() function that calls assert() 16 | assert(condition, msg, 2, values...) //nolint:mnd // Explained in comment 17 | } 18 | -------------------------------------------------------------------------------- /assert_debug_nop.go: -------------------------------------------------------------------------------- 1 | //go:build !assertdebug 2 | // +build !assertdebug 3 | 4 | package assert 5 | 6 | // Debug is a no-op. The `assertdebug` build tag is not set. 7 | // 8 | // To learn more about build tags, see https://pkg.go.dev/go/build#hdr-Build_Constraints. 9 | func Debug(_ bool, _ string, _ ...any) {} 10 | -------------------------------------------------------------------------------- /assert_test.go: -------------------------------------------------------------------------------- 1 | package assert 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | ) 7 | 8 | func TestAssert_SuccessCase(t *testing.T) { 9 | t.Parallel() 10 | Assert(true, "this should not panic") 11 | } 12 | 13 | func TestAssert_BasicFailure(t *testing.T) { 14 | t.Parallel() 15 | 16 | defer func() { 17 | r := recover() 18 | if r == nil { 19 | t.Fatal("expected panic but got none") 20 | } 21 | 22 | err, ok := r.(AssertionError) 23 | if !ok { 24 | t.Fatalf("expected AssertionError but got %T", r) 25 | } 26 | 27 | // Check basic error properties 28 | if err.Message != "basic failure message" { 29 | t.Errorf("expected message 'basic failure message' but got '%s'", err.Message) 30 | } 31 | 32 | // Only verify that line number exists and is positive 33 | if err.Line <= 0 { 34 | t.Error("expected positive line number") 35 | } 36 | 37 | // Only verify file name format, not specific line 38 | if !strings.HasSuffix(err.File, "_test.go") { 39 | t.Errorf("expected file name to end with _test.go, got %s", err.File) 40 | } 41 | 42 | // Verify source context exists and contains the failure line 43 | if !strings.Contains(err.SourceContext, "Assert(false, \"basic failure message\")") { 44 | t.Error("expected source context to contain the failing assertion") 45 | } 46 | }() 47 | 48 | Assert(false, "basic failure message") 49 | } 50 | 51 | func TestConfig_DisableSourceContext(t *testing.T) { 52 | t.Parallel() 53 | 54 | // Save original config and restore after test 55 | originalConfig := activeConfig 56 | defer func() { 57 | SetConfig(originalConfig) 58 | }() 59 | 60 | // Disable source context 61 | SetConfig(Config{ 62 | IncludeSource: false, 63 | ContextLines: 5, 64 | }) 65 | 66 | defer func() { 67 | r := recover() 68 | if r == nil { 69 | t.Fatal("expected panic but got none") 70 | } 71 | 72 | err, ok := r.(AssertionError) 73 | if !ok { 74 | t.Fatalf("expected AssertionError but got %T", r) 75 | } 76 | 77 | // Source context should be empty when disabled 78 | if err.SourceContext != "" { 79 | t.Error("expected empty source context when disabled") 80 | } 81 | }() 82 | 83 | Assert(false, "failure with disabled source") 84 | } 85 | 86 | func TestConfig_CustomContextLines(t *testing.T) { 87 | t.Parallel() 88 | 89 | // Save original config and restore after test 90 | originalConfig := activeConfig 91 | defer func() { 92 | SetConfig(originalConfig) 93 | }() 94 | 95 | // Set custom context lines 96 | customLines := 2 97 | SetConfig(Config{ 98 | IncludeSource: true, 99 | ContextLines: customLines, 100 | }) 101 | 102 | defer func() { 103 | r := recover() 104 | if r == nil { 105 | t.Fatal("expected panic but got none") 106 | } 107 | 108 | err, ok := r.(AssertionError) 109 | if !ok { 110 | t.Fatalf("expected AssertionError but got %T", r) 111 | } 112 | 113 | // Count the number of lines in source context 114 | lines := strings.Count(err.SourceContext, "\n") + 1 115 | expectedLines := customLines*2 + 1 // context before + current line + context after 116 | if lines != expectedLines { 117 | t.Errorf("expected %d lines of context, got %d", expectedLines, lines) 118 | } 119 | }() 120 | 121 | Assert(false, "failure with custom context lines") 122 | } 123 | 124 | func TestAssert_WithValues(t *testing.T) { 125 | t.Parallel() 126 | 127 | type testStruct struct { 128 | Field string 129 | } 130 | 131 | defer func() { 132 | r := recover() 133 | if r == nil { 134 | t.Fatal("expected panic but got none") 135 | } 136 | 137 | err, ok := r.(AssertionError) 138 | if !ok { 139 | t.Fatalf("expected AssertionError but got %T", r) 140 | } 141 | 142 | expectedValues := []string{ 143 | "[string_key]: \"string_value\"", 144 | "[int_key]: 42", 145 | "[struct_key]:", 146 | } 147 | 148 | for _, expected := range expectedValues { 149 | if !strings.Contains(err.Message, expected) { 150 | t.Errorf("expected message to contain '%s'", expected) 151 | } 152 | } 153 | 154 | // Verify source context exists and contains the failure line 155 | if !strings.Contains(err.SourceContext, "Assert(false, \"failure with values\"") { 156 | t.Error("expected source context to contain the failing assertion") 157 | } 158 | }() 159 | 160 | Assert(false, "failure with values", 161 | "string_key", "string_value", 162 | "int_key", 42, 163 | "struct_key", testStruct{Field: "value"}, 164 | ) 165 | } 166 | 167 | func TestAssert_OddNumberOfValues(t *testing.T) { 168 | t.Parallel() 169 | 170 | defer func() { 171 | r := recover() 172 | if r == nil { 173 | t.Fatal("expected panic but got none") 174 | } 175 | 176 | err, ok := r.(AssertionError) 177 | if !ok { 178 | t.Fatalf("expected AssertionError but got %T", r) 179 | } 180 | 181 | if !strings.Contains(err.Message, "(MISSING)") { 182 | t.Error("expected message to contain (MISSING) for odd number of values") 183 | } 184 | 185 | // Verify source context exists 186 | if err.SourceContext == "" { 187 | t.Error("expected non-empty source context") 188 | } 189 | }() 190 | 191 | Assert(false, "odd values", 192 | "key1", "value1", 193 | "key2", // Missing value 194 | ) 195 | } 196 | 197 | func TestAssertionError_Error(t *testing.T) { 198 | t.Parallel() 199 | 200 | err := AssertionError{ 201 | Message: "test message", 202 | File: "test_file.go", 203 | Line: 42, 204 | SourceContext: " 41 | func TestExample(t *testing.T) {\n" + 205 | "→ 42 | \tAssert(false, \"test message\")\n" + 206 | " 43 | }", 207 | } 208 | 209 | errStr := err.Error() 210 | 211 | expectedParts := []string{ 212 | "test_file.go:42", 213 | "test message", 214 | "Source context:", 215 | "→ 42", 216 | } 217 | 218 | for _, part := range expectedParts { 219 | if !strings.Contains(errStr, part) { 220 | t.Errorf("expected error string to contain '%s'", part) 221 | } 222 | } 223 | } 224 | 225 | func TestAssert_NilValues(t *testing.T) { 226 | t.Parallel() 227 | 228 | defer func() { 229 | r := recover() 230 | if r == nil { 231 | t.Fatal("expected panic but got none") 232 | } 233 | 234 | err, ok := r.(AssertionError) 235 | if !ok { 236 | t.Fatalf("expected AssertionError but got %T", r) 237 | } 238 | 239 | if !strings.Contains(err.Message, "[nil_key]: ") { 240 | t.Error("expected message to handle nil values correctly") 241 | } 242 | 243 | // Verify source context exists 244 | if err.SourceContext == "" { 245 | t.Error("expected non-empty source context") 246 | } 247 | }() 248 | 249 | Assert(false, "nil value test", 250 | "nil_key", nil, 251 | ) 252 | } 253 | 254 | func TestAssert_EmptyValues(t *testing.T) { 255 | t.Parallel() 256 | 257 | defer func() { 258 | r := recover() 259 | if r == nil { 260 | t.Fatal("expected panic but got none") 261 | } 262 | 263 | err, ok := r.(AssertionError) 264 | if !ok { 265 | t.Fatalf("expected AssertionError but got %T", r) 266 | } 267 | 268 | expectedValues := []string{ 269 | "[empty_string]: \"\"", 270 | "[empty_slice]: []string{}", 271 | "[empty_map]: map[string]int{}", 272 | } 273 | 274 | for _, expected := range expectedValues { 275 | if !strings.Contains(err.Message, expected) { 276 | t.Errorf("expected message to contain '%s'", expected) 277 | } 278 | } 279 | 280 | // Verify source context exists 281 | if err.SourceContext == "" { 282 | t.Error("expected non-empty source context") 283 | } 284 | }() 285 | 286 | Assert(false, "empty values test", 287 | "empty_string", "", 288 | "empty_slice", []string{}, 289 | "empty_map", map[string]int{}, 290 | ) 291 | } 292 | 293 | func TestAssertCallerFailure(t *testing.T) { 294 | assertMessage := "This should fail to get caller info" 295 | // Capture and verify the panic 296 | defer func() { 297 | r := recover() 298 | if r == nil { 299 | t.Fatal("Expected panic, but none occurred") 300 | } 301 | 302 | ae, ok := r.(AssertionError) 303 | if !ok { 304 | t.Fatalf("Expected AssertionError, got %T", r) 305 | } 306 | 307 | // Verify we got the simplified error without file/line info 308 | if ae.File != "" { 309 | t.Errorf("Expected empty file, got %q", ae.File) 310 | } 311 | if ae.Line != 0 { 312 | t.Errorf("Expected line to be 0, got %d", ae.Line) 313 | } 314 | if ae.SourceContext != "" { 315 | t.Errorf("Expected empty source context, got %q", ae.SourceContext) 316 | } 317 | // Message should still be included 318 | if ae.Message != assertMessage { 319 | t.Errorf("Expected %q as error message, got %q", assertMessage, ae.Message) 320 | } 321 | }() 322 | 323 | // Using the assert function (only accessible internally) instead of Assert 324 | // to pass a specific skipFrames value 325 | assert(false, assertMessage, 1000) 326 | } -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/nikoksr/assert-go 2 | 3 | go 1.23.3 4 | -------------------------------------------------------------------------------- /types.go: -------------------------------------------------------------------------------- 1 | package assert 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | // Config is used to configure the behavior of the assertion library. 9 | type Config struct { 10 | // IncludeSource determines if source context is included in errors. 11 | // 12 | // Default: true 13 | IncludeSource bool 14 | 15 | // ContextLines determines how many lines of context to show. 16 | // 17 | // Default: 5 18 | ContextLines int 19 | } 20 | 21 | // Assert is the main assertion function. It panics if the condition is false. 22 | type AssertionError struct { 23 | Message string 24 | File string 25 | SourceContext string 26 | Line int 27 | } 28 | 29 | // Error returns the error message. 30 | func (e AssertionError) Error() string { 31 | var sb strings.Builder 32 | 33 | if e.File != "" { 34 | sb.WriteString(fmt.Sprintf("Assertion failed at %s:%d\n", e.File, e.Line)) 35 | } else { 36 | sb.WriteString("Assertion failed (Runtime caller info is not available)\n") 37 | } 38 | sb.WriteString(fmt.Sprintf("Message: %s\n", e.Message)) 39 | 40 | if e.SourceContext != "" { 41 | sb.WriteString("Source context:\n") 42 | sb.WriteString(e.SourceContext) 43 | } 44 | 45 | return sb.String() 46 | } 47 | --------------------------------------------------------------------------------