ClientUtils

An ExUnit formatter with JSON output and distributed test coordination. Designed for editor integrations and CI/CD pipelines that need machine-readable test results and the ability to handle concurrent test requests.

Features

Installation

def deps do
[{:client_utils, "~> 0.1.0"}]
end

Basic Usage

Configure the formatter in test/test_helper.exs:

ExUnit.start(formatters: [ClientUtils.TestFormatter])

File Output

ExUnit.start(formatters: [{ClientUtils.TestFormatter, output_file: "test-results.json"}])

Or via environment variable:

EXUNIT_JSON_OUTPUT_FILE=test-results.json mix test

Streaming Mode

Stream test events in real-time while writing the final summary to a file:

ExUnit.start(formatters: [{ClientUtils.TestFormatter, streaming: true, output_file: "test-results.json"}])

JSON Output Format

Final Summary

{
"stats": {
"duration": 1234.56,
"start": "2024-01-15T10:30:00.000000",
"end": "2024-01-15T10:30:01.234000",
"passes": 40,
"failures": 2,
"pending": 3,
"tests": 45,
"suites": 5
},
"tests": [
{"title": "test name", "fullTitle": "ModuleName: test name"}
],
"failures": [
{
"title": "failing test",
"fullTitle": "ModuleName: failing test",
"error": {
"file": "test/my_test.exs",
"line": 42,
"message": "Assertion failed"
}
}
],
"pending": [
{"title": "skipped test", "fullTitle": "ModuleName: skipped test", "pending": true}
]
}

Streaming Events

{"type":"suite:start","start":"2024-01-15T10:30:00.000000"}
{"type":"test:pass","test":{"title":"works","fullTitle":"MyModule: works"}}
{"type":"test:fail","test":{"title":"breaks","fullTitle":"MyModule: breaks","error":{...}}}
{"type":"suite:end","stats":{...}}

Distributed Test Coordination

The mix agent_test task coordinates multiple concurrent test requests. This is useful for editor integrations where multiple "run test" commands might be triggered in quick succession.

How It Works

sequenceDiagram
participant A1 as Agent 1 (Runner)
participant A2 as Agent 2 (Waiter)
participant Lock as Lock File
participant Cache as Event Cache
participant Tests as Mix Test
A1->>Lock: Create caller file
A2->>Lock: Create caller file
A1->>Lock: Acquire lock
A2->>Lock: Fail acquire lock
A2->>A2: Wait
A1->>Tests: Run mix test with custom formatter
Tests-->>A1: Stream test events to terminal
Tests->>Cache: Store events
A1->>Lock: Release lock
A2->>Cache: Get events for my files after my timestamp
Cache-->>A2: Return cached events
A2->>A2: Replay events to CLI formatter

Usage

mix agent_test test/my_test.exs

Multiple concurrent invocations are automatically coordinated—only one runs tests at a time, others receive cached results.

Cache API

Query the test cache programmatically:

alias ClientUtils.TestFormatter.TestCache
# Check if files were tested recently
TestCache.file_tested_after?("test/my_test.exs", datetime)
TestCache.files_tested_after?(["test/a.exs", "test/b.exs"], datetime)
# Retrieve cached events
TestCache.get_events_for_file("test/my_test.exs", since)
TestCache.get_events_after(since)

Configuration

Environment VariableDescription
EXUNIT_JSON_OUTPUT_FILEPath for JSON output file
EXUNIT_JSON_STREAMINGEnable streaming mode
AGENT_TEST_EVENTS_FILECustom path for event cache

License

Apache 2.0