Skip to content
Draft
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
1,711 changes: 784 additions & 927 deletions coverage/combined.out

Large diffs are not rendered by default.

846 changes: 423 additions & 423 deletions coverage/integration/coverage.out

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion coverage/merged/.gitignore
Original file line number Diff line number Diff line change
@@ -1 +1 @@
raw/
raw/
1 change: 1 addition & 0 deletions coverage/unit/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
raw/
158 changes: 158 additions & 0 deletions tests/scripts/coverage/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
# Coverage Capture and Merge Workflow

This directory contains scripts for capturing and merging code coverage from both unit tests and end-to-end integration tests.

## Overview

The coverage workflow uses Go 1.20+'s `GOCOVERDIR` support to capture raw coverage data from instrumented binaries. This allows teams to:

1. Run unit tests with coverage
2. Run integration/e2e tests with coverage (using instrumented CLI binaries)
3. Merge both coverage datasets into a combined report

## Scripts

### `run-e2e-coverage.sh`

Builds an instrumented `git-drs` binary and runs end-to-end tests while capturing coverage data.

**Usage:**
```bash
./tests/scripts/coverage/run-e2e-coverage.sh
```

**What it does:**
1. Builds `git-drs` with coverage instrumentation (`-cover -covermode=atomic -coverpkg=./...`)
2. Places the binary in `build/coverage/git-drs`
3. Sets `GOCOVERDIR` to capture raw coverage to `coverage/integration/raw/`
4. Runs the e2e test script (`tests/scripts/end-to-end/e2e.sh`)
5. Converts raw coverage to a profile at `coverage/integration/coverage.out`

**Environment Variables:**
- `COVERAGE_ROOT` - Base coverage directory (default: `<repo>/coverage`)
- `INTEGRATION_COV_DIR` - Raw integration coverage directory (default: `<coverage>/integration/raw`)
- `INTEGRATION_PROFILE` - Integration coverage profile output (default: `<coverage>/integration/coverage.out`)
- `BUILD_DIR` - Build directory for instrumented binary (default: `<repo>/build/coverage`)
- `GOFLAGS_EXTRA` - Additional Go build flags

### `combine-coverage.sh`

Merges raw coverage data from unit tests and integration tests into a single combined coverage profile.

**Usage:**
```bash
./tests/scripts/coverage/combine-coverage.sh [unit_dir] [integration_dir] [merged_dir] [output_profile]
```

**Parameters (all optional):**
- `unit_dir` - Unit test raw coverage directory (default: `coverage/unit/raw`)
- `integration_dir` - Integration test raw coverage directory (default: `coverage/integration/raw`)
- `merged_dir` - Output directory for merged raw coverage (default: `coverage/merged/raw`)
- `output_profile` - Output combined coverage profile (default: `coverage/combined.out`)

**What it does:**
1. Validates that both unit and integration raw coverage directories exist
2. Merges raw coverage using `go tool covdata merge`
3. Converts merged coverage to a text profile using `go tool covdata textfmt`

**Environment Variables:**
All parameters can also be set via environment variables:
- `COVERAGE_ROOT` - Base coverage directory
- `UNIT_COV_DIR` - Unit test raw coverage directory
- `INTEGRATION_COV_DIR` - Integration test raw coverage directory
- `MERGED_COV_DIR` - Merged raw coverage directory
- `COMBINED_PROFILE` - Combined coverage profile output

### `assert-coverage-timestamp.sh`

Validates that coverage files are newer than the most recent `.go` source file, ensuring coverage is up-to-date.

## Workflow Example

### Step 1: Run unit tests with raw coverage

```bash
# Create the raw coverage directory
mkdir -p coverage/unit/raw

# Run tests with coverage (use atomic mode to match integration tests)
go test -cover -covermode=atomic ./... -args -test.gocoverdir=$PWD/coverage/unit/raw
```

### Step 2: Run integration tests with coverage

```bash
./tests/scripts/coverage/run-e2e-coverage.sh
```

### Step 3: Combine coverage reports

```bash
./tests/scripts/coverage/combine-coverage.sh
```

### Step 4: View combined coverage

```bash
# Summary view
go tool cover -func=coverage/combined.out

# HTML view
go tool cover -html=coverage/combined.out -o coverage/combined.html
```

## Coverage Modes

**Important:** Both unit and integration tests must use the same coverage mode (e.g., `atomic`). If they use different modes, the merge will fail with a "counter mode clash" error.

The scripts default to `atomic` mode, which is thread-safe and appropriate for concurrent tests.

## Directory Structure

```
coverage/
├── integration/
│ ├── .gitignore # Ignores raw/ directory
│ ├── raw/ # Raw coverage data from e2e tests (not committed)
│ └── coverage.out # Integration coverage profile (committed)
├── unit/
│ ├── .gitignore # Ignores raw/ directory
│ ├── raw/ # Raw coverage data from unit tests (not committed)
│ └── coverage.out # Unit coverage profile (committed)
├── merged/
│ ├── .gitignore # Ignores raw/ directory
│ └── raw/ # Merged raw coverage (not committed)
├── combined.out # Combined coverage profile (committed)
└── combined.html # Combined coverage HTML report (committed)
```

## Troubleshooting

### "counter mode clash" error

This occurs when unit and integration tests use different coverage modes. Ensure both use the same mode:

```bash
# For unit tests
go test -cover -covermode=atomic ./... -args -test.gocoverdir=$PWD/coverage/unit/raw

# For integration tests (handled by run-e2e-coverage.sh)
go build -cover -covermode=atomic -coverpkg=./... -o build/coverage/git-drs .
```

### "coverage directory not found" error

The raw coverage directories must exist before running tests. Create them with:

```bash
mkdir -p coverage/unit/raw coverage/integration/raw
```

### E2E script not found

The `run-e2e-coverage.sh` script looks for `tests/scripts/end-to-end/e2e.sh` (or `end-2-end/e2e.sh` as fallback). Ensure your e2e test script exists and is executable.

## References

- [Go Coverage Profiling](https://go.dev/blog/cover)
- [Go 1.20 Coverage Improvements](https://go.dev/testing/coverage/)
84 changes: 31 additions & 53 deletions tests/scripts/coverage/combine-coverage.sh
Original file line number Diff line number Diff line change
@@ -1,59 +1,37 @@
#!/usr/bin/env bash
# File: `tests/scripts/coverage/combine-coverage.sh`
set -euo pipefail

COV_INT='coverage/integration/coverage.out'
COV_UNIT='coverage/unit/coverage.out'

# ensure coverage files exist
if [ ! -f "$COV_INT" ]; then
echo "Missing coverage file: $COV_INT" >&2
echo "Run integration tests to produce $COV_INT" >&2
exit 1

ROOT_DIR=$(git rev-parse --show-toplevel)
COVERAGE_ROOT="${COVERAGE_ROOT:-${ROOT_DIR}/coverage}"

UNIT_COV_DIR="${1:-${UNIT_COV_DIR:-${COVERAGE_ROOT}/unit/raw}}"
INTEGRATION_COV_DIR="${2:-${INTEGRATION_COV_DIR:-${COVERAGE_ROOT}/integration/raw}}"
MERGED_COV_DIR="${3:-${MERGED_COV_DIR:-${COVERAGE_ROOT}/merged/raw}}"
COMBINED_PROFILE="${4:-${COMBINED_PROFILE:-${COVERAGE_ROOT}/combined.out}}"

if [[ ! -d "${UNIT_COV_DIR}" ]]; then
echo "Unit coverage directory not found: ${UNIT_COV_DIR}" >&2
echo "Run unit tests with raw coverage output, e.g.:" >&2
echo " go test -cover -covermode=atomic ./... -args -test.gocoverdir=\${PWD}/coverage/unit/raw" >&2
exit 1
fi

if [ ! -f "$COV_UNIT" ]; then
echo "Missing coverage file: $COV_UNIT" >&2
echo "Run unit tests to produce $COV_UNIT" >&2
exit 1

if [[ ! -d "${INTEGRATION_COV_DIR}" ]]; then
echo "Integration coverage directory not found: ${INTEGRATION_COV_DIR}" >&2
exit 1
fi

# defaults (can be overridden in env)
COMBINED_PROFILE="${COMBINED_PROFILE:-coverage/combined.out}"
COMBINED_HTML="${COMBINED_HTML:-coverage/combined.html}"

# ensure gocovmerge is available; attempt install and add bin dir to PATH if needed
if ! command -v gocovmerge >/dev/null 2>&1; then
echo "gocovmerge not found — attempting to install..."
if ! go install go.shabbyrobe.org/gocovmerge/cmd/gocovmerge@latest; then
echo "go install failed" >&2
exit 1
fi

# determine install dir: prefer GOBIN, fallback to GOPATH/bin, fallback to $HOME/go/bin
BIN_DIR="$(go env GOBIN 2>/dev/null || true)"
if [ -z "$BIN_DIR" ]; then
GOPATH="$(go env GOPATH 2>/dev/null || true)"
BIN_DIR="${GOPATH:-$HOME/go}/bin"
fi

if [ -d "$BIN_DIR" ]; then
PATH="$BIN_DIR:$PATH"
fi

if ! command -v gocovmerge >/dev/null 2>&1; then
echo "gocovmerge still not found after install. Binary likely at: ${BIN_DIR}" >&2
echo "Add ${BIN_DIR} to your PATH and re-run the script." >&2
exit 1
fi

mkdir -p "${MERGED_COV_DIR}" "$(dirname "${COMBINED_PROFILE}")"

# Validate MERGED_COV_DIR is within coverage directory before cleaning
if [[ "${MERGED_COV_DIR}" == "${COVERAGE_ROOT}"* ]]; then
rm -rf "${MERGED_COV_DIR:?}"/*
else
echo "Error: MERGED_COV_DIR (${MERGED_COV_DIR}) is not within COVERAGE_ROOT (${COVERAGE_ROOT})" >&2
exit 1
fi

gocovmerge "$COV_INT" "$COV_UNIT" > "${COMBINED_PROFILE}"


go tool covdata merge -i="${UNIT_COV_DIR},${INTEGRATION_COV_DIR}" -o "${MERGED_COV_DIR}"
go tool covdata textfmt -i="${MERGED_COV_DIR}" -o "${COMBINED_PROFILE}"

echo "Combined coverage profile saved to ${COMBINED_PROFILE}"

go tool cover -html="${COMBINED_PROFILE}" -o "${COMBINED_HTML}"
echo "Combined coverage html saved to ${COMBINED_HTML}"

coverage=$(go tool cover -func="${COMBINED_PROFILE}" | grep total | awk '{print substr($3, 1, length($3)-1)}')
echo "Total combined coverage: ${coverage}%"
40 changes: 40 additions & 0 deletions tests/scripts/coverage/run-e2e-coverage.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
#!/usr/bin/env bash
# File: tests/scripts/coverage/run-e2e-coverage.sh
# Purpose: Builds an instrumented git-drs binary and runs end-to-end tests with coverage capture
set -euo pipefail

ROOT_DIR=$(git rev-parse --show-toplevel)
COVERAGE_ROOT="${COVERAGE_ROOT:-${ROOT_DIR}/coverage}"
INTEGRATION_COV_DIR="${INTEGRATION_COV_DIR:-${COVERAGE_ROOT}/integration/raw}"
INTEGRATION_PROFILE="${INTEGRATION_PROFILE:-${COVERAGE_ROOT}/integration/coverage.out}"
BUILD_DIR="${BUILD_DIR:-${ROOT_DIR}/build/coverage}"

E2E_SCRIPT="${ROOT_DIR}/tests/scripts/end-to-end/e2e.sh"
if [[ ! -x "${E2E_SCRIPT}" ]]; then
ALT_E2E_SCRIPT="${ROOT_DIR}/tests/scripts/end-2-end/e2e.sh"
if [[ -x "${ALT_E2E_SCRIPT}" ]]; then
E2E_SCRIPT="${ALT_E2E_SCRIPT}"
else
echo "Unable to find executable e2e.sh at ${E2E_SCRIPT} or ${ALT_E2E_SCRIPT}." >&2
exit 1
fi
fi

mkdir -p "${BUILD_DIR}" "${INTEGRATION_COV_DIR}" "$(dirname "${INTEGRATION_PROFILE}")"

GOFLAGS=()
if [[ -n "${GOFLAGS_EXTRA:-}" ]]; then
# shellcheck disable=SC2206
GOFLAGS=(${GOFLAGS_EXTRA})
fi

go build "${GOFLAGS[@]}" -cover -covermode=atomic -coverpkg=./... -o "${BUILD_DIR}/git-drs" .

export PATH="${BUILD_DIR}:${PATH}"
export GOCOVERDIR="${INTEGRATION_COV_DIR}"

"${E2E_SCRIPT}"

go tool covdata textfmt -i="${INTEGRATION_COV_DIR}" -o "${INTEGRATION_PROFILE}"

echo "Integration coverage profile saved to ${INTEGRATION_PROFILE}"
31 changes: 31 additions & 0 deletions tests/scripts/end-to-end/e2e.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
#!/usr/bin/env bash
# Simple e2e test script for testing coverage capture
set -euo pipefail

echo "Running simple e2e tests..."

# Test 1: Check that git-drs exists and is executable
if ! command -v git-drs &> /dev/null; then
echo "ERROR: git-drs not found in PATH" >&2
exit 1
fi

echo "✓ git-drs found in PATH"

# Test 2: Run git-drs version command
if git-drs version &> /dev/null; then
echo "✓ git-drs version command succeeded"
else
echo "ERROR: git-drs version command failed" >&2
exit 1
fi

# Test 3: Run git-drs help command
if git-drs --help &> /dev/null; then
echo "✓ git-drs help command succeeded"
else
echo "ERROR: git-drs help command failed" >&2
exit 1
fi

echo "All e2e tests passed!"