Packages
snakepit
0.7.0
0.13.0
0.12.0
0.11.1
0.11.0
0.10.1
0.10.0
0.9.1
0.9.0
0.8.9
0.8.8
0.8.7
0.8.6
0.8.5
0.8.4
0.8.3
0.8.2
0.8.1
0.8.0
0.7.7
0.7.6
0.7.5
0.7.4
0.7.3
0.7.2
0.7.1
0.7.0
0.6.11
0.6.10
0.6.9
0.6.8
0.6.7
0.6.6
0.6.5
0.6.4
0.6.3
0.6.2
0.6.1
0.6.0
0.5.1
0.5.0
0.4.3
0.4.2
0.4.1
0.4.0
0.3.3
0.3.2
0.3.1
0.3.0
0.2.1
0.2.0
0.1.2
0.1.1
0.1.0
High-performance pooler and session manager for external language integrations. Supports Python, Node.js, Ruby, and more with gRPC streaming, session management, and production-ready process cleanup.
Current section
Files
Jump to
Current section
Files
docs/code-standards/test-architecture-supertester.md
# Snakepit Test Architecture with Supertester Integration
## Overview
This document outlines the comprehensive test architecture for Snakepit, integrating Supertester's OTP-compliant testing patterns to eliminate race conditions, enable true async testing, and ensure deterministic test execution.
## Core Principles
1. **Zero Process.sleep** - All synchronization must be deterministic
2. **Full Isolation** - Every test runs in complete process/ETS isolation
3. **Async by Default** - Tests must run concurrently without conflicts
4. **Test the Public API** - Focus on user-facing behavior, not implementation
5. **Supervision Testing** - Explicitly test fault tolerance and recovery
## Directory Structure
```
test/
├── unit/ # Isolated component tests
│ ├── bridge/
│ │ ├── session_store_test.exs
│ │ ├── serialization_test.exs
│ │ └── variables_test.exs
│ ├── grpc/
│ │ ├── grpc_worker_test.exs
│ │ ├── bridge_server_test.exs
│ │ └── client_test.exs
│ └── pool/
│ ├── pool_test.exs
│ ├── registry_test.exs
│ └── worker_supervisor_test.exs
├── integration/ # Cross-component tests
│ ├── grpc_bridge_test.exs
│ ├── pool_lifecycle_test.exs
│ ├── session_workflow_test.exs
│ └── streaming_test.exs
├── performance/ # Load and benchmark tests
│ ├── pool_saturation_test.exs
│ ├── grpc_throughput_test.exs
│ └── concurrent_sessions_test.exs
├── chaos/ # Resilience testing
│ ├── worker_crash_test.exs
│ ├── network_failure_test.exs
│ └── supervisor_recovery_test.exs
└── support/
├── test_helpers.ex
├── grpc_test_adapter.ex
└── mock_python_server.ex
```
## Supertester Integration
### 1. Base Test Module
```elixir
defmodule Snakepit.TestCase do
@moduledoc """
Base test case for all Snakepit tests.
Provides Supertester integration and common helpers.
"""
defmacro __using__(opts \\ []) do
quote do
use ExUnit.Case, async: unquote(Keyword.get(opts, :async, true))
use Supertester.UnifiedTestFoundation, isolation: :full_isolation
import Supertester.OTPHelpers
import Supertester.GenServerHelpers
import Supertester.Assertions
import Snakepit.TestHelpers
# Ensure clean application state
setup_all do
Application.ensure_all_started(:snakepit)
on_exit(fn -> cleanup_all_sessions() end)
end
end
end
end
```
### 2. gRPC Worker Testing Pattern
```elixir
defmodule Snakepit.GRPCWorkerTest do
use Snakepit.TestCase
import Supertester.GenServerHelpers
describe "gRPC worker lifecycle" do
setup context do
# Isolated worker with unique port
port = 50100 + :rand.uniform(1000)
worker_id = "test_worker_#{context.test}"
{:ok, worker} = setup_isolated_genserver(
Snakepit.GRPCWorker,
worker_id,
adapter: MockGRPCAdapter,
port: port,
id: worker_id
)
# Wait for server ready without sleep
assert_receive {:grpc_ready, ^port}, 5_000
%{worker: worker, port: port, worker_id: worker_id}
end
test "worker starts gRPC server and connects", %{worker: worker, port: port} do
assert_genserver_responsive(worker)
# Verify server is listening
{:ok, channel} = GRPC.Stub.connect("localhost:#{port}")
assert channel != nil
end
test "worker handles commands", %{worker: worker} do
# Use synchronous call for deterministic behavior
{:ok, result} = call_with_timeout(worker, {:execute, "ping", %{}, 5_000})
assert result["status"] == "pong"
end
test "worker recovers from server crash", %{worker: worker, port: port} do
# Monitor the worker
monitor_ref = Process.monitor(worker)
# Simulate server crash
send(worker, {:EXIT, :simulated_crash})
# Worker should restart
assert_process_restarted(monitor_ref, worker, 5_000)
assert_receive {:grpc_ready, ^port}, 5_000
end
end
end
```
### 3. Pool Testing Pattern
```elixir
defmodule Snakepit.PoolTest do
use Snakepit.TestCase
import Supertester.SupervisorHelpers
describe "pool management" do
setup do
pool_name = :"test_pool_#{System.unique_integer()}"
{:ok, pool} = setup_isolated_supervisor(
Snakepit.Pool,
pool_name,
pool_size: 2,
worker_module: MockWorker
)
%{pool: pool, pool_name: pool_name}
end
test "pool starts configured number of workers", %{pool: pool} do
assert_child_count(pool, 2)
assert_all_children_alive(pool)
end
test "pool handles concurrent requests", %{pool_name: pool_name} do
# Start 10 concurrent tasks
tasks = for i <- 1..10 do
Task.async(fn ->
Snakepit.Pool.execute(pool_name, "echo", %{id: i})
end)
end
# All should complete without error
results = Task.await_many(tasks, 5_000)
assert length(results) == 10
assert Enum.all?(results, &match?({:ok, _}, &1))
end
end
end
```
### 4. Integration Testing Pattern
```elixir
defmodule Snakepit.GRPCBridgeIntegrationTest do
use Snakepit.TestCase, async: false
import Supertester.MessageHelpers
describe "full gRPC bridge integration" do
setup do
# Start isolated application instance
app_name = :"snakepit_test_#{System.unique_integer()}"
# Override configuration for this test
Application.put_env(app_name, :pooling_enabled, true)
Application.put_env(app_name, :pool_config, %{pool_size: 2})
Application.put_env(app_name, :grpc_port, 51000 + :rand.uniform(1000))
{:ok, _} = Application.ensure_all_started(app_name)
on_exit(fn -> Application.stop(app_name) end)
%{app: app_name}
end
test "end-to-end session workflow", %{app: app} do
session_id = "integration_#{System.unique_integer()}"
# Initialize session
{:ok, _} = Snakepit.execute_in_session(session_id, "initialize_session", %{})
# Register variable with proper synchronization
{:ok, var_id} = Snakepit.execute_in_session(
session_id,
"register_variable",
%{name: "counter", type: "integer", initial_value: 0}
)
# Concurrent updates
tasks = for i <- 1..10 do
Task.async(fn ->
Snakepit.execute_in_session(
session_id,
"set_variable",
%{name: "counter", value: i}
)
end)
end
Task.await_many(tasks, 5_000)
# Verify final state
{:ok, var} = Snakepit.execute_in_session(
session_id,
"get_variable",
%{name: "counter"}
)
assert var["value"] in 1..10
end
end
end
```
## Key Testing Patterns
### 1. Synchronization Without Sleep
```elixir
# BAD - Race condition prone
GenServer.cast(worker, :start_server)
Process.sleep(1000) # Arbitrary wait
assert server_started?()
# GOOD - Deterministic synchronization
ref = monitor_genserver_call(worker, :start_server)
assert_receive {:server_started, port}, 5_000
assert_receive {:DOWN, ^ref, :process, _, :normal}
```
### 2. Port Allocation for gRPC
```elixir
defmodule Snakepit.TestHelpers do
@base_test_port 52000
@port_range 1000
def allocate_test_port do
# Use test number to ensure unique ports
test_number = Process.get(:test_number, 0)
Process.put(:test_number, test_number + 1)
@base_test_port + rem(test_number, @port_range)
end
end
```
### 3. Mock Adapters for Testing
```elixir
defmodule MockGRPCAdapter do
@behaviour Snakepit.Adapter
def executable_path, do: "echo"
def script_path, do: "/dev/null"
def script_args, do: []
def init_grpc_connection(port) do
# Simulate server startup
send(self(), {:grpc_ready, port})
{:ok, %{channel: :mock_channel, port: port}}
end
def grpc_execute(_conn, "ping", _args, _timeout) do
{:ok, %{"status" => "pong"}}
end
end
```
### 4. Chaos Testing Patterns
```elixir
defmodule Snakepit.ChaosTest do
use Snakepit.TestCase
import Supertester.ChaosHelpers
test "pool recovers from worker failures" do
{:ok, pool} = start_supervised_pool(size: 4)
# Inject failures
inject_random_failures(pool,
failure_rate: 0.5,
failure_types: [:exit, :timeout, :disconnect]
)
# System should still process requests
results = for _ <- 1..20 do
Task.async(fn -> Snakepit.execute("ping", %{}) end)
end
|> Task.await_many(10_000)
successful = Enum.count(results, &match?({:ok, _}, &1))
assert successful >= 15 # At least 75% success rate
end
end
```
## Supertester Gaps & Enhancements Needed
### What Supertester Provides
- Process isolation mechanisms
- Deterministic synchronization helpers
- GenServer and Supervisor test utilities
- Message tracing and assertions
- Chaos engineering framework
### What's Missing for Snakepit
1. **gRPC-specific helpers** - Waiting for server ready, port management
2. **Python process helpers** - Starting/stopping external processes
3. **Streaming assertions** - Testing gRPC streams
4. **Session cleanup utilities** - Bulk session management
### Proposed Snakepit Test Extensions
```elixir
defmodule Snakepit.TestHelpers do
@moduledoc "Snakepit-specific test helpers extending Supertester"
def assert_grpc_server_ready(port, timeout \\ 5_000) do
assert_receive {:grpc_ready, ^port}, timeout
end
def with_python_server(port, fun) do
# Start Python gRPC server
server = start_python_server(port)
try do
assert_grpc_server_ready(port)
fun.()
after
stop_python_server(server)
end
end
def assert_streaming_response(stream_ref, expected_chunks) do
Enum.each(expected_chunks, fn expected ->
assert_receive {:stream_chunk, ^stream_ref, chunk}, 1_000
assert chunk == expected
end)
assert_receive {:stream_end, ^stream_ref}, 1_000
end
end
```
## Migration Strategy
### Phase 1: Foundation (Week 1)
1. Add Supertester dependency
2. Create base test case module
3. Set up test directory structure
4. Create mock adapters
### Phase 2: Unit Tests (Week 2)
1. Migrate SessionStore tests
2. Migrate GRPCWorker tests
3. Migrate Pool tests
4. Remove all Process.sleep calls
### Phase 3: Integration Tests (Week 3)
1. Rewrite gRPC bridge tests
2. Add end-to-end workflow tests
3. Add streaming tests
4. Enable async: true everywhere
### Phase 4: Advanced Testing (Week 4)
1. Add performance benchmarks
2. Add chaos tests
3. Add supervisor recovery tests
4. Document testing patterns
## Success Metrics
1. **Zero Process.sleep** in test suite
2. **100% async: true** tests
3. **No test interdependence** - random test order passes
4. **< 30s total test time** with parallelization
5. **Zero flaky tests** over 100 runs
## Conclusion
By integrating Supertester and following these patterns, Snakepit will have:
- Deterministic, fast tests
- True isolation enabling parallel execution
- Proper OTP testing patterns
- Comprehensive coverage including chaos scenarios
- A foundation for continuous deployment confidence
The key is treating tests as first-class citizens that demonstrate proper usage patterns while ensuring reliability through deterministic execution.