diff --git a/.editorconfig b/.editorconfig new file mode 100755 index 0000000..500a0fe --- /dev/null +++ b/.editorconfig @@ -0,0 +1,13 @@ +# http://editorconfig.org +root = true + +[*] +#indent_style = tab +tab_width = 4 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.yml] +indent_style = space diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..4546726 --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,60 @@ +name: CI + +on: + push: + branches: [master, main] + pull_request: + +jobs: + build: + name: Build + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + include: + - goos: js + goarch: wasm + - goos: aix + goarch: ppc64 + + steps: + - name: Checkout + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + with: + fetch-depth: 0 + + - name: Set up Go + uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2 + with: + go-version: "1.23" + + - name: Build + run: go build . + env: + GOOS: ${{ matrix.goos }} + GOARCH: ${{ matrix.goarch }} + + release: + name: Release + runs-on: ubuntu-latest + + needs: build + + steps: + - name: Checkout + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + with: + fetch-depth: 0 + - name: Setup node + uses: actions/setup-node@v3 + with: + node-version: 20 + - name: Install dependencies + run: npm install -g semantic-release conventional-changelog-conventionalcommits @semantic-release/exec @semantic-release/git @semantic-release/github @semantic-release/changelog @semantic-release/release-notes-generator + - name: Release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PLATFORMIO_AUTH_TOKEN: ${{ secrets.PLATFORMIO_AUTH_TOKEN }} + run: semantic-release diff --git a/.gitignore b/.gitignore new file mode 100755 index 0000000..fe836f8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,611 @@ +# Compiled JavaScript files +lib/**/*.js +lib/**/*.js.map + +# Local Extras +extras/ + + +# TypeScript v1 declaration files +typings/ + +# Node.js dependency directory +node_modules/ +.env + +.pnpm-store + +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +firebase-debug.log* +firebase-debug.*.log* + +# Firebase cache +.firebase/ + +# Firebase config + +# Uncomment this if you'd like others to create their own Firebase project. +# For a team working on the same Firebase project(s), it is recommended to leave +# it commented so all members can deploy to the same project(s) in .firebaserc. +# .firebaserc + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (http://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env + +dist +.solid +.output +.vercel +.netlify +netlify + +# IDEs and editors +/.idea +.project +.classpath +*.launch +.settings/ + +# Temp +gitignore + +# System Files +.DS_Store +Thumbs.db + +# Logs + +logs +_.log +npm-debug.log_ +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) + +report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json + +# Runtime data + +pids +_.pid +_.seed +\*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover + +lib-cov + +# Coverage directory used by tools like istanbul + +coverage +\*.lcov + +# nyc test coverage + +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) + +.grunt + +# Bower dependency directory (https://bower.io/) + +bower_components + +# node-waf configuration + +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) + +build/Release + +# Dependency directories + +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) + +web_modules/ + +# TypeScript cache + +\*.tsbuildinfo + +# Optional npm cache directory + +.npm + +# Optional eslint cache + +.eslintcache + +# Optional stylelint cache + +.stylelintcache + +# Microbundle cache + +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history + +.node_repl_history + +# Output of 'npm pack' + +\*.tgz + +# Yarn Integrity file + +.yarn-integrity + +# dotenv environment variable files + +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) + +.cache +.parcel-cache + +# Next.js build output + +.next +out + +# Nuxt.js build / generate output + +.nuxt +dist + +# Gatsby files + +.cache/ + +# Comment in the public line in if your project uses Gatsby and not Next.js + +# https://nextjs.org/blog/next-9-1#public-directory-support + +# public + +# vuepress build output + +.vuepress/dist + +# vuepress v2.x temp and cache directory + +.temp +.cache + +# Docusaurus cache and generated files + +.docusaurus + +# Serverless directories + +.serverless/ + +# FuseBox cache + +.fusebox/ + +# DynamoDB Local files + +.dynamodb/ + +# TernJS port file + +.tern-port + +# Stores VSCode versions used for testing VSCode extensions + +.vscode-test + +# yarn v2 + +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.\* + +# wrangler project + +.dev.vars +.wrangler/ + +node_modules +dist +.wrangler +.dev.vars + +# Change them to your taste: +wrangler.toml +package-lock.json +yarn.lock +pnpm-lock.yaml + +# Python +*.pyc +*.cpython-311.pyc +*.pyo +__pycache__/ +__pycache__ + +dump.rdb +.serverless +node_modules +.env.* +.vscode/** +!.vscode/launch.json +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with "go test -c" +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ + +# Go workspace file +go.work +tmp/ + +# IDE specific files +.vscode +.idea + +# .env file +.env + +# Project build +main +./bin +*.db \ No newline at end of file diff --git a/.releaserc b/.releaserc new file mode 100644 index 0000000..07403ce --- /dev/null +++ b/.releaserc @@ -0,0 +1,172 @@ +{ + "branches": [ + "main", + "master" + ], + "plugins": [ + [ + "@semantic-release/commit-analyzer", + { + "preset": "conventionalcommits", + "parserOpts": { + "noteKeywords": [ + "BREAKING CHANGE", + "BREAKING CHANGES", + "BREAKING" + ] + }, + "releaseRules": [ + { + "breaking": true, + "release": "major" + }, + { + "type": "feat", + "release": "minor" + }, + { + "type": "fix", + "release": "patch" + }, + { + "type": "perf", + "release": "patch" + }, + { + "type": "revert", + "release": "patch" + }, + { + "type": "docs", + "release": "minor" + }, + { + "type": "style", + "release": "patch" + }, + { + "type": "refactor", + "release": "patch" + }, + { + "type": "test", + "release": "patch" + }, + { + "type": "build", + "release": "patch" + }, + { + "type": "ci", + "scope": "ci-*", + "release": "patch" + }, + { + "type": "chore", + "release": false + }, + { + "type": "no-release", + "release": false + } + ] + } + ], + [ + "@semantic-release/release-notes-generator", + { + "preset": "conventionalcommits", + "parserOpts": { + "noteKeywords": [ + "BREAKING CHANGE", + "BREAKING CHANGES", + "BREAKING" + ] + }, + "writerOpts": { + "commitsSort": [ + "subject", + "scope" + ] + }, + "presetConfig": { + "types": [ + { + "type": "feat", + "section": "🍕 Features" + }, + { + "type": "feature", + "section": "🍕 Features" + }, + { + "type": "fix", + "section": "🐛 Bug Fixes" + }, + { + "type": "perf", + "section": "🔥 Performance Improvements" + }, + { + "type": "revert", + "section": "⏩ Reverts" + }, + { + "type": "docs", + "section": "📝 Documentation" + }, + { + "type": "style", + "section": "🎨 Styles" + }, + { + "type": "refactor", + "section": "🧑‍💻 Code Refactoring" + }, + { + "type": "test", + "section": "✅ Tests" + }, + { + "type": "build", + "section": "🤖 Build System" + }, + { + "type": "ci", + "section": "🔁 Continuous Integration" + } + ] + } + } + ], + [ + "@semantic-release/changelog", + { + "changelogTitle": "# 📦 Changelog \n[![conventional commits](https://img.shields.io/badge/conventional%20commits-1.0.0-yellow.svg)](https://conventionalcommits.org)\n[![semantic versioning](https://img.shields.io/badge/semantic%20versioning-2.0.0-green.svg)](https://semver.org)\n> All notable changes to this project will be documented in this file" + } + ], + [ + "@semantic-release/exec", + { + "prepareCmd": "echo \"Preparing release ${nextRelease.version}\"", + "publishCmd": "echo \"Publishing release ${nextRelease.version}\"" + } + ], + [ + "@semantic-release/github", + { + "addReleases": "bottom" + } + ], + [ + "@semantic-release/git", + { + "assets": [ + "LICENSE*", + "CHANGELOG.md" + ], + "message": "chore(${nextRelease.type}): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}" + } + ] + ] +} \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100755 index 0000000..113cc90 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 ZanzyTHEbar + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..0a325b0 --- /dev/null +++ b/README.md @@ -0,0 +1,44 @@ +# Assert Go + +A lightweight assertion library for Go, designed for systems and real-time programming with structured logging and flush controls. + +## Installation + +```bash +go get github.com/ZanzyTHEbar/assert-lib +``` + +## Usage + +```go +package main + +import ( + "context" + "github.com/ZanzyTHEbar/assert-lib/assert" +) + +func main() { + handler := assert.NewAssertHandler() + + handler.Assert(context.TODO(), false, "This should fail") +} +``` + +Check out the [examples](/examples/) directory for usage examples. + +## Features + +- **Assertions**: Assert, Nil, NotNil, NoError, Never. +- **Flush Management**: Control output flushes with AssertFlush. +- **Context-Based Logging**: Attach structured logging to your assertion calls. +- **Custom Loggers**: Use your own logger with the AssertHandler interface. + +## Examples + +```bash +go run examples/basic_assertion.go +go run examples/deferred_assertions.go +go run examples/custom_exit.go +go run examples/formater.go +``` diff --git a/assert/assert.go b/assert/assert.go new file mode 100644 index 0000000..972cfd1 --- /dev/null +++ b/assert/assert.go @@ -0,0 +1,186 @@ +package assert + +import ( + "context" + "fmt" + "io" + "log/slog" + "os" + "runtime/debug" + "strings" + "sync" + "time" +) + +// Define the AssertHandler to encapsulate state +type AssertHandler struct { + flushes []AssertFlush + assertData map[string]AssertData + writer io.Writer + flushLock sync.Mutex + exitFunc func(code int) + formatter Formatter + deferredErrors []string + deferAssertions bool +} + +// Define interfaces for logging/asserting +type AssertData interface { + Dump() string +} + +type AssertFlush interface { + Flush() +} + +// Add a constructor for the handler +func NewAssertHandler() *AssertHandler { + return &AssertHandler{ + flushes: []AssertFlush{}, + assertData: make(map[string]AssertData), + writer: os.Stderr, + exitFunc: os.Exit, // Default exit behavior + formatter: &TextFormatter{}, // Default to text formatter + deferredErrors: []string{}, + deferAssertions: false, + } +} + +// SetDeferAssertions allows toggling deferred assertion mode +func (a *AssertHandler) SetDeferAssertions(deferMode bool) { + a.deferAssertions = deferMode +} + +func (a *AssertHandler) SetFormatter(formatter Formatter) { + a.formatter = formatter +} + +func (a *AssertHandler) SetExitFunc(exitFunc func(int)) { + a.exitFunc = exitFunc +} + +func (a *AssertHandler) AddAssertData(key string, value AssertData) { + a.assertData[key] = value +} + +func (a *AssertHandler) RemoveAssertData(key string) { + delete(a.assertData, key) +} + +func (a *AssertHandler) AddAssertFlush(flusher AssertFlush) { + a.flushes = append(a.flushes, flusher) +} + +func (a *AssertHandler) ToWriter(w io.Writer) { + a.writer = w +} + +func (a *AssertHandler) runAssert(ctx context.Context, msg string, args ...interface{}) { + a.flushLock.Lock() + defer a.flushLock.Unlock() + + // Check if the context has been canceled + if err := ctx.Err(); err != nil { + fmt.Fprintln(a.writer, "Context canceled:", err) + return + } + + // Prevent re-entrancy by skipping further flushes + for _, f := range a.flushes { + f.Flush() + } + + data := map[string]interface{}{ + "msg": msg, + "area": "Assert", + } + + // append the args to the data + for i := 0; i < len(args); i += 2 { + if i+1 >= len(args) { + break + } + data[args[i].(string)] = args[i+1] + } + + fmt.Fprintf(a.writer, "ARGS: %+v\n", args) + + for k, v := range a.assertData { + data[k] = v.Dump() + } + + stack := string(debug.Stack()) + + formattedOutput := a.formatter.Format(data, stack) + + fmt.Fprintln(a.writer, "ASSERT") + fmt.Fprintln(a.writer, formattedOutput) + + // If we are in deferred mode, store the error and return + if a.deferAssertions { + a.deferredErrors = append(a.deferredErrors, formattedOutput) + return + } + + // Use the custom exit function instead of os.Exit directly + a.exitFunc(1) +} + +// Process all deferred assertions at once, logging or exiting if needed +func (a *AssertHandler) ProcessDeferredAssertions(ctx context.Context) { + if len(a.deferredErrors) > 0 { + // Combine all errors into a single string + combinedErrors := strings.Join(a.deferredErrors, "\n---\n") + fmt.Fprintln(a.writer, combinedErrors) + + // Clear the deferred errors after processing + a.deferredErrors = []string{} + + // Exit after processing if it's an ERROR level + a.exitFunc(1) + } +} + +func (a *AssertHandler) Assert(ctx context.Context, truth bool, msg string, data ...any) { + if !truth { + a.runAssert(ctx, msg, data...) + } +} + +func (a *AssertHandler) AssertWithTimeout(ctx context.Context, timeout time.Duration, truth bool, msg string, data ...any) { + ctx, cancel := context.WithTimeout(ctx, timeout) + defer cancel() + + if !truth { + a.runAssert(ctx, msg, data...) + } +} + +func (a *AssertHandler) Nil(ctx context.Context, item any, msg string, data ...any) { + slog.InfoContext(ctx, "Nil Check", "item", item) + if item == nil { + a.runAssert(ctx, msg, data...) + return + } + + slog.ErrorContext(ctx, "Nil#not nil encountered") + a.runAssert(ctx, msg, data...) +} + +func (a *AssertHandler) NotNil(ctx context.Context, item any, msg string, data ...any) { + if item == nil { + slog.ErrorContext(ctx, "NotNil#nil encountered") + a.runAssert(ctx, msg, data...) + } +} + +func (a *AssertHandler) Never(ctx context.Context, msg string, data ...any) { + a.runAssert(ctx, msg, data...) +} + +func (a *AssertHandler) NoError(ctx context.Context, err error, msg string, data ...any) { + if err != nil { + data = append(data, "error", err) + a.runAssert(ctx, msg, data...) + } +} diff --git a/assert/assert_test.go b/assert/assert_test.go new file mode 100644 index 0000000..25131b4 --- /dev/null +++ b/assert/assert_test.go @@ -0,0 +1,109 @@ +package assert + +import ( + "bytes" + "context" + "testing" +) + +func TestAssert(t *testing.T) { + var buffer bytes.Buffer + handler := NewAssertHandler() + handler.ToWriter(&buffer) + handler.SetExitFunc(func(code int) {}) // Prevent exit + + handler.Assert(context.TODO(), false, "Test Failure") + + if !bytes.Contains(buffer.Bytes(), []byte("Test Failure")) { + t.Fatalf("Expected failure message not found in output") + } +} + +func TestCustomExitFunc(t *testing.T) { + var buffer bytes.Buffer + handler := NewAssertHandler() + handler.ToWriter(&buffer) + handler.SetExitFunc(func(code int) {}) + + called := false + handler.SetExitFunc(func(code int) { + called = true // Track if the custom exit is called + }) + + handler.Assert(context.TODO(), false, "Test Failure") + + if !called { + t.Fatalf("Custom exit function was not called") + } + if !bytes.Contains(buffer.Bytes(), []byte("Test Failure")) { + t.Fatalf("Expected failure message not found in output") + } +} + +func TestJSONFormatter(t *testing.T) { + var buffer bytes.Buffer + handler := NewAssertHandler() + handler.ToWriter(&buffer) + handler.SetExitFunc(func(code int) {}) + + handler.SetFormatter(&JSONFormatter{}) + handler.Assert(context.TODO(), false, "Test Failure") + + expected := `"msg": "Test Failure"` + if !bytes.Contains(buffer.Bytes(), []byte(expected)) { + t.Fatalf("Expected JSON output not found in output") + } +} + +func TestYAMLFormatter(t *testing.T) { + var buffer bytes.Buffer + handler := NewAssertHandler() + handler.ToWriter(&buffer) + handler.SetExitFunc(func(code int) {}) + + handler.SetFormatter(&YAMLFormatter{}) + handler.Assert(context.TODO(), false, "Test Failure") + + expected := "msg: Test Failure" + if !bytes.Contains(buffer.Bytes(), []byte(expected)) { + t.Fatalf("Expected YAML output not found in output") + } +} + +func TestDeferredAssertions(t *testing.T) { + var buffer bytes.Buffer + handler := NewAssertHandler() + handler.ToWriter(&buffer) + handler.SetExitFunc(func(code int) {}) + + // Enable deferred assertions + handler.SetDeferAssertions(true) + + // These should not be printed immediately + handler.Nil(context.TODO(), nil, "Test Deferred Nil") + handler.Assert(context.TODO(), false, "Test Deferred Assert") + + // Process deferred assertions + handler.ProcessDeferredAssertions(context.TODO()) + + if !bytes.Contains(buffer.Bytes(), []byte("Test Deferred Assert")) { + t.Fatalf("Expected deferred assertion message not found") + } + if !bytes.Contains(buffer.Bytes(), []byte("Test Deferred Nil")) { + t.Fatalf("Expected deferred nil message not found") + } +} + +func TestImmediateAssertions(t *testing.T) { + var buffer bytes.Buffer + handler := NewAssertHandler() + handler.ToWriter(&buffer) + handler.SetExitFunc(func(code int) {}) + + // Disable deferred assertions (default) + handler.Assert(context.TODO(), false, "Test Immediate Assert") + + if !bytes.Contains(buffer.Bytes(), []byte("Test Immediate Assert")) { + t.Fatalf("Expected immediate assertion message not found") + } +} diff --git a/assert/formatter.go b/assert/formatter.go new file mode 100644 index 0000000..ba44418 --- /dev/null +++ b/assert/formatter.go @@ -0,0 +1,49 @@ +package assert + +import ( + "encoding/json" + "fmt" + + "gopkg.in/yaml.v2" +) + +// Formatter interface for structured output +type Formatter interface { + Format(assertData map[string]interface{}, stack string) string +} + +// TextFormatter is the default plain text output format +type TextFormatter struct{} + +func (f *TextFormatter) Format(assertData map[string]interface{}, stack string) string { + output := "ASSERT\n" + for key, value := range assertData { + output += fmt.Sprintf(" %s=%v\n", key, value) + } + output += fmt.Sprintf("%s\n", stack) + return output +} + +// JSONFormatter for JSON output +type JSONFormatter struct{} + +func (f *JSONFormatter) Format(assertData map[string]interface{}, stack string) string { + data := map[string]interface{}{ + "assertData": assertData, + "stack": stack, + } + out, _ := json.MarshalIndent(data, "", " ") + return string(out) +} + +// YAMLFormatter for YAML output +type YAMLFormatter struct{} + +func (f *YAMLFormatter) Format(assertData map[string]interface{}, stack string) string { + data := map[string]interface{}{ + "assertData": assertData, + "stack": stack, + } + out, _ := yaml.Marshal(data) + return string(out) +} diff --git a/examples/basic_assertion.go b/examples/basic_assertion.go new file mode 100644 index 0000000..02a2c61 --- /dev/null +++ b/examples/basic_assertion.go @@ -0,0 +1,14 @@ +package main + +import ( + "context" + + "github.com/ZanzyTHEbar/assert-lib/assert" +) + +func main() { + handler := assert.NewAssertHandler() + + // Basic assertion (this will fail and exit) + handler.Assert(context.TODO(), 2 == 1, "Basic Assertion Failed: 2 is not equal to 1") +} diff --git a/examples/custom_exit.go b/examples/custom_exit.go new file mode 100644 index 0000000..048140a --- /dev/null +++ b/examples/custom_exit.go @@ -0,0 +1,20 @@ +package main + +import ( + "context" + "fmt" + + "github.com/ZanzyTHEbar/assert-lib/assert" +) + +func main() { + handler := assert.NewAssertHandler() + + // Set a custom exit function (this will prevent the program from exiting) + handler.SetExitFunc(func(code int) { + fmt.Printf("Custom exit called with code: %d, but not exiting\n", code) + }) + + // This assertion will trigger the custom exit function but will not exit + handler.Assert(context.TODO(), false, "Custom Exit Assertion Failed") +} diff --git a/examples/deferred_assertions.go b/examples/deferred_assertions.go new file mode 100644 index 0000000..d61954e --- /dev/null +++ b/examples/deferred_assertions.go @@ -0,0 +1,21 @@ +package main + +import ( + "context" + + "github.com/ZanzyTHEbar/assert-lib/assert" +) + +func main() { + handler := assert.NewAssertHandler() + + // Enable deferred assertions + handler.SetDeferAssertions(true) + + // Multiple assertions that will not fail immediately + handler.Nil(context.TODO(), nil, "Deferred Nil Assertion") + handler.Assert(context.TODO(), false, "Deferred Assert Failure") + + // Process all deferred assertions (will print all errors and exit) + handler.ProcessDeferredAssertions(context.TODO()) +} diff --git a/examples/formater.go b/examples/formater.go new file mode 100644 index 0000000..ef84f02 --- /dev/null +++ b/examples/formater.go @@ -0,0 +1,23 @@ +package main + +import ( + "context" + + "github.com/ZanzyTHEbar/assert-lib/assert" +) + +func main() { + handler := assert.NewAssertHandler() + + // Set the formatter to JSON + handler.SetFormatter(&assert.JSONFormatter{}) + + // This will output the assertion failure in JSON format + handler.Assert(context.TODO(), false, "JSON Format Assertion Failed") + + // Set the formatter to YAML + handler.SetFormatter(&assert.YAMLFormatter{}) + + // This will output the assertion failure in YAML format + handler.Assert(context.TODO(), false, "YAML Format Assertion Failed") +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..716bc65 --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module github.com/ZanzyTHEbar/assert-lib + +go 1.23.1 + +require gopkg.in/yaml.v2 v2.4.0 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..7534661 --- /dev/null +++ b/go.sum @@ -0,0 +1,3 @@ +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=