Testing Guide
This guide covers testing strategies for the LLM-based scanno detection system in Codexis.
Test Structure
Unit Tests
defmodule Codexis.QualityChecker.ScannoDetectorTest do
use ExUnit.Case, async: true
alias Codexis.QualityChecker.ScannoDetector
alias Codexis.LLM.MockClient
describe "detect_scannos/2" do
test "detects common OCR errors" do
content = """
The quickbrown fox jumps over tlie lazy dog. This is a
cornmon OCR error where "the" becomes "tlie" and words
merge together. Also, "rn" often becomes "m" in words
like "modern" which might appear as "modem".
"""
{:ok, results} = ScannoDetector.detect_scannos(content,
model: MockClient,
parallel: false
)
assert length(results.high) >= 2
assert Enum.any?(results.high, fn c ->
c["original"] == "quickbrown" && c["corrected"] == "quick brown"
end)
end
test "handles empty content" do
{:ok, results} = ScannoDetector.detect_scannos("", model: MockClient)
assert results.high == []
assert results.medium == []
assert results.low == []
end
test "respects confidence thresholds" do
content = "Test content with errors"
{:ok, results} = ScannoDetector.detect_scannos(content,
model: MockClient,
confidence_threshold: 0.9
)
assert Enum.all?(results.high, fn c -> c["confidence"] >= 0.9 end)
end
end
end
Integration Tests
defmodule Codexis.Integration.ScannoDetectionTest do
use ExUnit.Case
@moduletag :integration
setup do
# Start Ollama container for tests
{:ok, _} = start_ollama_container()
on_exit(fn ->
stop_ollama_container()
end)
:ok
end
test "end-to-end scanno detection with real LLM" do
manuscript_path = "test/fixtures/sample_manuscript.tex"
{:ok, result} = Codexis.BuildManager.ScannoIntegration.check_manuscript_for_scannos(
manuscript_path,
interactive: false,
model: "qwen2.5:3b" # Use smaller model for tests
)
assert result.corrections_applied > 0
assert File.exists?(result.output_path)
end
test "handles LaTeX-specific errors" do
latex_content = """
\\sectiori{Introduction}
This is a test docurnent with LaTeX errors.
\\cite{smith2O23}
"""
{:ok, corrections} = ScannoDetector.detect_scannos(latex_content)
assert Enum.any?(corrections.high, fn c ->
c["original"] == "\\sectiori" && c["corrected"] == "\\section"
end)
end
end
Mock LLM Client
defmodule Codexis.LLM.MockClient do
@behaviour Codexis.LLM.ClientBehaviour
def generate(prompt, opts \\ []) do
cond do
String.contains?(prompt, "quickbrown") ->
{:ok, quickbrown_response()}
String.contains?(prompt, "tlie") ->
{:ok, common_errors_response()}
String.contains?(prompt, "\\sectiori") ->
{:ok, latex_errors_response()}
true ->
{:ok, default_response()}
end
end
defp quickbrown_response do
"""
```json
{
"corrections": [
{
"original": "quickbrown",
"corrected": "quick brown",
"reason": "Words appear merged",
"confidence": 0.95,
"position": 4
}
]
}
```
"""
end
defp common_errors_response do
"""
```json
{
"corrections": [
{
"original": "tlie",
"corrected": "the",
"reason": "Common OCR error: tlie→the",
"confidence": 0.98,
"position": 32
},
{
"original": "cornmon",
"corrected": "common",
"reason": "Common OCR error: rn→m",
"confidence": 0.92,
"position": 54
}
]
}
```
"""
end
defp latex_errors_response do
"""
```json
{
"corrections": [
{
"original": "\\\\sectiori",
"corrected": "\\\\section",
"reason": "LaTeX command misspelling",
"confidence": 0.96,
"position": 0
}
]
}
```
"""
end
defp default_response do
"""
```json
{
"corrections": []
}
```
"""
end
end
Property-Based Tests
defmodule Codexis.Property.ScannoDetectorTest do
use ExUnit.Case
use ExUnitProperties
property "never suggests empty corrections" do
check all text <- string(:printable, min_length: 1) do
{:ok, results} = ScannoDetector.detect_scannos(text, model: MockClient)
all_corrections = results.high ++ results.medium ++ results.low
assert Enum.all?(all_corrections, fn c ->
String.trim(c["corrected"]) != ""
end)
end
end
property "correction positions are within text bounds" do
check all text <- string(:printable, min_length: 10) do
{:ok, results} = ScannoDetector.detect_scannos(text, model: MockClient)
all_corrections = results.high ++ results.medium ++ results.low
text_length = String.length(text)
assert Enum.all?(all_corrections, fn c ->
pos = c["absolute_position"]
orig_length = String.length(c["original"])
pos >= 0 && pos + orig_length <= text_length
end)
end
end
property "corrections preserve text structure" do
check all text <- string(:printable, min_length: 5) do
{:ok, results} = ScannoDetector.detect_scannos(text, model: MockClient)
# Apply corrections shouldn't change overall structure drastically
{:ok, corrected} = apply_corrections(text, results)
# Same number of lines
assert length(String.split(text, "\n")) ==
length(String.split(corrected, "\n"))
end
end
end
Performance Tests
defmodule Codexis.Performance.ScannoDetectorTest do
use ExUnit.Case
@moduletag :performance
test "processes large document within timeout" do
large_document = generate_large_document(100_000) # 100k characters
{time, {:ok, _results}} = :timer.tc(fn ->
ScannoDetector.detect_scannos(large_document,
model: MockClient,
parallel: true
)
end)
# Should complete within 30 seconds
assert time < 30_000_000
end
test "parallel processing is faster than sequential" do
document = generate_large_document(50_000)
{seq_time, _} = :timer.tc(fn ->
ScannoDetector.detect_scannos(document, parallel: false)
end)
{par_time, _} = :timer.tc(fn ->
ScannoDetector.detect_scannos(document, parallel: true)
end)
# Parallel should be at least 20% faster
assert par_time < seq_time * 0.8
end
defp generate_large_document(size) do
# Generate realistic document with known errors
String.duplicate("This is a test docurnent with sorne errors. ", div(size, 40))
end
end
Test Fixtures
defmodule Codexis.Test.Fixtures do
def sample_manuscript do
"""
\\documentclass{article}
\\begin{document}
\\sectiori{Introduction}
This is a sarnple manuscript with OCR errors.
The quickbrown fox jumps over tlie lazy dog.
\\subsection{Cornmon Errors}
Sorne common OCR errors include:
- Character substitutions (rn→m)
- Merged words (ofthe)
- Number confusion (2O23 instead of 2023)
\\end{document}
"""
end
def expected_corrections do
[
%{
"original" => "\\sectiori",
"corrected" => "\\section",
"confidence" => 0.95
},
%{
"original" => "sarnple",
"corrected" => "sample",
"confidence" => 0.92
},
%{
"original" => "quickbrown",
"corrected" => "quick brown",
"confidence" => 0.90
}
]
end
def mathematical_content do
"""
\\[
\\int_0^\\infty e^{-x^2} dx = \\frac{\\sqrt{\\pi}}{2}
\\]
The surn of the series is:
\\[
\\surn_{n=1}^\\infty \\frac{1}{n^2} = \\frac{\\pi^2}{6}
\\]
"""
end
end
UI Testing
defmodule Codexis.UI.ReviewInterfaceTest do
use ExUnit.Case
import ExUnit.CaptureIO
alias Codexis.Review.InteractiveScannoReview
test "displays correction with context" do
correction = %{
"original" => "tlie",
"corrected" => "the",
"absolute_position" => 20,
"confidence" => 0.95,
"reason" => "Common OCR error"
}
content = "This is a test with tlie error in it."
output = capture_io(fn ->
InteractiveScannoReview.display_correction(correction, content)
end)
assert output =~ "Original: tlie"
assert output =~ "Suggested: the"
assert output =~ "Confidence: 0.95"
end
test "handles user input correctly" do
input = "a\n" # Accept
result = capture_io([input: input, capture_prompt: false], fn ->
InteractiveScannoReview.get_user_decision()
end)
assert result == :accept
end
end
Error Handling Tests
defmodule Codexis.ErrorHandling.ScannoDetectorTest do
use ExUnit.Case
test "handles LLM timeout gracefully" do
slow_client = fn _prompt, _opts ->
Process.sleep(1000)
{:ok, "timeout"}
end
{:error, :timeout} = ScannoDetector.detect_scannos(
"test content",
model: slow_client,
timeout: 500
)
end
test "handles malformed LLM responses" do
bad_response_client = fn _prompt, _opts ->
{:ok, "not valid json"}
end
{:error, :invalid_response} = ScannoDetector.detect_scannos(
"test content",
model: bad_response_client
)
end
test "recovers from partial failures in parallel processing" do
flaky_client = fn prompt, _opts ->
if String.contains?(prompt, "fail") do
{:error, "simulated failure"}
else
{:ok, valid_response()}
end
end
content = "Normal text. Text that will fail. More normal text."
{:ok, results} = ScannoDetector.detect_scannos(
content,
model: flaky_client,
parallel: true
)
# Should still get results from successful blocks
assert length(results.high) > 0
end
end
Test Helpers
defmodule Codexis.Test.Helpers do
def with_temp_file(content, fun) do
temp_file = Path.join(System.tmp_dir!(), "test_#{:rand.uniform(1000)}.tex")
try do
File.write!(temp_file, content)
fun.(temp_file)
after
File.rm(temp_file)
end
end
def assert_correction_valid(correction) do
assert is_map(correction)
assert Map.has_key?(correction, "original")
assert Map.has_key?(correction, "corrected")
assert Map.has_key?(correction, "confidence")
assert correction["confidence"] >= 0 && correction["confidence"] <= 1
end
def count_corrections(results) do
length(results.high) + length(results.medium) + length(results.low)
end
def apply_test_corrections(content, corrections) do
# Apply corrections for testing
Enum.reduce(corrections, content, fn correction, acc ->
String.replace(acc, correction["original"], correction["corrected"])
end)
end
end
CI Test Configuration
# .github/workflows/test.yml
name: Test
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
services:
ollama:
image: ollama/ollama
ports:
- 11434:11434
steps:
- uses: actions/checkout@v2
- name: Set up Elixir
uses: erlef/setup-beam@v1
with:
elixir-version: '1.14'
otp-version: '25'
- name: Install dependencies
run: mix deps.get
- name: Pull test model
run: |
curl http://localhost:11434/api/pull -d '{"name":"qwen2.5:3b"}'
- name: Run tests
run: mix test --include integration
- name: Run performance tests
run: mix test --only performance
Test Configuration
# config/test.exs
config :codexis, :scanno_detection,
enabled: true,
default_model: "test_model",
mock_llm: true,
cache_directory: "test/support/cache",
interactive: false,
timeout: 5_000
# test/test_helper.exs
ExUnit.start(
exclude: [:integration, :performance],
capture_log: true
)
# Setup test directories
File.mkdir_p!("test/support/cache")
Acceptance Tests
defmodule Codexis.Acceptance.ScannoWorkflowTest do
use ExUnit.Case
@moduletag :acceptance
test "complete workflow from detection to correction" do
# 1. Create test manuscript with known errors
manuscript = create_test_manuscript()
# 2. Run scanno detection
{:ok, corrections} = detect_scannos(manuscript)
# 3. Simulate review process
reviewed = review_corrections(corrections)
# 4. Apply corrections
{:ok, corrected} = apply_corrections(manuscript, reviewed)
# 5. Verify results
assert_corrections_applied(manuscript, corrected)
assert_quality_improved(manuscript, corrected)
end
defp create_test_manuscript do
"""
\\documentclass{article}
\\title{Test Docurnent}
\\author{John Srnith}
\\begin{document}
\\maketitle
\\sectiori{Introduction}
This docurnent has sorne OCR errors that need correctiori.
\\end{document}
"""
end
defp assert_corrections_applied(original, corrected) do
refute corrected =~ "docurnent"
refute corrected =~ "sorne"
refute corrected =~ "correctiori"
refute corrected =~ "\\sectiori"
refute corrected =~ "Srnith"
end
defp assert_quality_improved(original, corrected) do
# Count remaining potential errors
original_errors = count_potential_errors(original)
corrected_errors = count_potential_errors(corrected)
assert corrected_errors < original_errors * 0.5
end
end
Coverage Reports
# mix.exs
def project do
[
app: :codexis,
# ...
test_coverage: [tool: ExCoveralls],
preferred_cli_env: [
coveralls: :test,
"coveralls.detail": :test,
"coveralls.post": :test,
"coveralls.html": :test
]
]
end
# Run coverage
# mix coveralls.html
Benchmarking
defmodule Codexis.Benchmark do
def run do
content = File.read!("test/fixtures/large_manuscript.tex")
Benchee.run(%{
"sequential" => fn ->
ScannoDetector.detect_scannos(content, parallel: false)
end,
"parallel" => fn ->
ScannoDetector.detect_scannos(content, parallel: true)
end,
"cached" => fn ->
ScannoDetector.detect_scannos(content, use_cache: true)
end
})
end
end
Test Best Practices
- Test Isolation: Each test should be independent
- Mock External Services: Use mocks for LLM calls in unit tests
- Real Integration Tests: Have separate integration tests with real Ollama
- Property-Based Testing: Use for edge cases and invariants
- Performance Monitoring: Regular performance regression tests
- CI/CD Integration: Automated testing in CI pipeline
- Test Data Management: Maintain realistic test fixtures