Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

all: build tests lint

COMPONENTS = things/testurl helpers/counter
COMPONENTS = things/testurl helpers/counter fakes/fakectx

# Run tests for all modules
tests:
Expand Down
17 changes: 16 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

![TestLazy Logo](/docs/img/testlazy.png)

**An encyclopedia of test values, fakes, and validators for Go - so you can type less and test more.**
**An encyclopedia of test values, fakes, helpers, and validators for Go - so you can type less and test more.**

![GitHub go.mod Go version](https://img.shields.io/github/go-mod/go-version/madflojo/testlazy)
[![codecov](https://codecov.io/gh/madflojo/testlazy/branch/main/graph/badge.svg?token=0TTTEWHLVN)](https://codecov.io/gh/madflojo/testlazy)
Expand Down Expand Up @@ -94,6 +94,20 @@ if err := <-c.WaitAbove(5, time.Second); err != nil {
}
```

### Contexts without manual cancellation | `github.com/madflojo/testlazy/fakes/fakectx`

Force cancel-aware code paths without wiring up `context.WithCancel` every time.

```go
ctx := fakectx.Cancelled()

if err := doSomething(ctx); err == nil {
t.Fatalf("expected failure for canceled context")
}

<-ctx.Done() // already closed
```

---

## 🧱 Structure
Expand All @@ -105,6 +119,7 @@ It allows you to take on only the dependencies you need.
|---------|-------------|----------------------|
| `github.com/madflojo/testlazy/things/testurl` | Pre-built URLs for common use cases | [![Go Reference](https://pkg.go.dev/badge/github.com/madflojo/testlazy/things/testurl.svg)](https://pkg.go.dev/github.com/madflojo/testlazy/things/testurl) |
| `github.com/madflojo/testlazy/helpers/counter` | Test-focused, thread-safe counter | [![Go Reference](https://pkg.go.dev/badge/github.com/madflojo/testlazy/helpers/counter.svg)](https://pkg.go.dev/github.com/madflojo/testlazy/helpers/counter) |
| `github.com/madflojo/testlazy/fakes/fakectx` | Ready-made contexts for cancellation/deadline tests | [![Go Reference](https://pkg.go.dev/badge/github.com/madflojo/testlazy/fakes/fakectx.svg)](https://pkg.go.dev/github.com/madflojo/testlazy/fakes/fakectx) |

---

Expand Down
57 changes: 57 additions & 0 deletions fakes/fakectx/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
.PHONY: all clean tests lint build format coverage benchmarks

all: build tests lint

# Run tests with coverage
tests:
@echo "Running tests with coverage..."
go test -v -race -covermode=atomic -coverprofile=coverage.out ./...
@go tool cover -func=coverage.out || true
@if command -v go tool cover >/dev/null 2>&1; then \
go tool cover -html=coverage.out -o coverage.html; \
fi

# Run benchmarks
benchmarks:
@echo "Running benchmarks..."
go test -run=^$$ -bench=. -benchmem ./...

# Build the package
build:
@echo "Building package..."
go build ./...

# Format code
format:
@echo "Formatting code..."
@gofmt -s -w .
@if command -v goimports >/dev/null 2>&1; then \
goimports -w .; \
else \
echo "goimports not installed, skipping import reordering"; \
fi
@if command -v golines >/dev/null 2>&1; then \
golines -w .; \
else \
echo "golines not installed, skipping line wrapping"; \
fi

# Lint code
lint:
@echo "Linting code..."
@if command -v golangci-lint >/dev/null 2>&1; then \
golangci-lint run ./...; \
else \
echo "golangci-lint not installed, skipping lint"; \
fi

# Generate coverage report
coverage: tests
@go tool cover -html=coverage.out

# Clean build artifacts
clean:
@echo "Cleaning build artifacts..."
@find . -type f -name "*.test" -delete
@rm -f coverage.out coverage.html
@rm -rf tmp
80 changes: 80 additions & 0 deletions fakes/fakectx/fakectx.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/*
Package fakectx provides curated context.Context helpers for tests that need
specific cancellation or deadline behavior without the boilerplate. The package
focuses on one-liner constructors so test authors can express failure scenarios
clearly and consistently. Start with Cancelled for immediate
context.Canceled states and layer in additional helpers as your tests grow more
complex.
*/
package fakectx

import (
"context"
"sync"
"time"
)

// Cancelled returns a context that has already been canceled. The returned
// context reports context.Canceled, closes Done immediately, and carries no
// deadline or values—perfect for forcing cancel-only branches.
func Cancelled() context.Context {
ctx, cancel := context.WithCancel(context.Background())
cancel()

return ctx
}

// DeadlineExceeded returns a context that has already exceeded its deadline.
// The Deadline reported by the context is always in the past and Err returns
// context.DeadlineExceeded immediately.
func DeadlineExceeded() context.Context {
ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(-time.Minute))
cancel()

return ctx
}

// TimedOut returns a context that has already hit its timeout deadline. It is
// equivalent to a context.WithTimeout call whose timer has fired.
func TimedOut() context.Context {
ctx, cancel := context.WithTimeout(context.Background(), 0)
defer cancel()

return ctx
}

// TimesOutAfter returns a context that will cancel itself after the provided
// duration. The Deadline is set in the future so callers can assert how much
// time remains before it expires.
func TimesOutAfter(timeout time.Duration) context.Context {
ctx, cancel := context.WithTimeout(context.Background(), timeout)

// Ensure the cancel function is called when the context is done to avoid
// potential resource leaks. Even though the context will cancel itself after the
// timeout, it's a good practice to call cancel to clean up resources.
go func() {
<-ctx.Done()
cancel()
}()

return ctx
}

// CancelledWithCallback returns a canceled context alongside a cancel function
// that executes the provided callback when invoked. This allows tests to ensure
// downstream code triggers cancellation.
func CancelledWithCallback(cb func()) (context.Context, context.CancelFunc) {
ctx, cancel := context.WithCancel(context.Background())

var once sync.Once
wrapped := func() {
once.Do(func() {
cancel()
if cb != nil {
cb()
}
})
}

return ctx, wrapped
}
64 changes: 64 additions & 0 deletions fakes/fakectx/fakectx_example_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package fakectx

import (
"context"
"errors"
"fmt"
"time"
)

func ExampleCancelled() {
ctx := Cancelled()

<-ctx.Done()
fmt.Println("Context done")
// Output:
// Context done
}

func ExampleDeadlineExceeded() {
ctx := DeadlineExceeded()
deadline, ok := ctx.Deadline()

if ok && deadline.Before(time.Now()) && errors.Is(ctx.Err(), context.DeadlineExceeded) {
fmt.Println("Deadline exceeded")
}
// Output:
// Deadline exceeded
}

func ExampleTimedOut() {
ctx := TimedOut()

if errors.Is(ctx.Err(), context.DeadlineExceeded) {
fmt.Println("Timed out")
}
// Output:
// Timed out
}

func ExampleTimesOutAfter() {
ctx := TimesOutAfter(5 * time.Millisecond)

if ctx.Err() != nil {
fmt.Println("Timeout too soon")
}

time.Sleep(10 * time.Millisecond)

if errors.Is(ctx.Err(), context.DeadlineExceeded) {
fmt.Println("Timed out")
}
// Output:
// Timed out
}

func ExampleCancelledWithCallback() {
_, cancel := CancelledWithCallback(func() {
fmt.Println("Cancelled callback called")
})

cancel()
// Output:
// Cancelled callback called
}
Loading
Loading