Compare commits
18 Commits
b78ecc077e
...
a80acb36c3
| Author | SHA1 | Date | |
|---|---|---|---|
| a80acb36c3 | |||
| 04f4720b21 | |||
| 0b77019f6c | |||
| a4a436c020 | |||
| 1278d5c332 | |||
| 697e926dfe | |||
| 2ee246fad6 | |||
| c6ff93c764 | |||
| e11ca4b376 | |||
| 9bc1788ae2 | |||
| 2f86b1c6cc | |||
| 3c5079a9d8 | |||
| 5515714ae6 | |||
| 6f571b9e12 | |||
| bab9c2ec08 | |||
| 63965696f4 | |||
| 7fb55c03fc | |||
| bf0e3e133f |
14
CLAUDE.md
14
CLAUDE.md
@ -7,6 +7,20 @@ A minimal Python calculator used to verify the Typosaurus agent system.
|
|||||||
- Full type annotations on every function signature.
|
- Full type annotations on every function signature.
|
||||||
- No comments or docstrings in source files.
|
- No comments or docstrings in source files.
|
||||||
|
|
||||||
|
## Python backend
|
||||||
|
|
||||||
|
- Package manifest: `pyproject.toml`
|
||||||
|
- Entry module: `src/typosaurus_sandbox/__main__.py`
|
||||||
|
- Framework: FastAPI
|
||||||
|
- Serve frontend: no
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
- Backend serves frontend: no
|
||||||
|
- Module root: `src/typosaurus_sandbox/`
|
||||||
|
- Calculator business logic: `src/typosaurus_sandbox/domain/calculator/operations.py`
|
||||||
|
- HTTP API layer: `src/typosaurus_sandbox/presentation/api/v1/calculator.py`
|
||||||
|
|
||||||
## Verification
|
## Verification
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|||||||
8
Makefile
8
Makefile
@ -1,2 +1,8 @@
|
|||||||
|
# retoor <retoor@molodetz.nl>
|
||||||
|
|
||||||
verify:
|
verify:
|
||||||
@python3 -m compileall -q src tests && python3 -m unittest discover -s tests -q && echo "verification passed"
|
@PYTHONPATH=src python3 -m compileall -q src tests && PYTHONPATH=src python3 -m unittest discover -s tests -q && echo "verification passed"
|
||||||
|
|
||||||
|
run:
|
||||||
|
@PYTHONPATH=src python3 -m typosaurus_sandbox
|
||||||
|
|
||||||
|
|||||||
122
README.md
122
README.md
@ -1,3 +1,123 @@
|
|||||||
|
# retoor <retoor@molodetz.nl>
|
||||||
|
|
||||||
# typosaurus-sandbox
|
# typosaurus-sandbox
|
||||||
|
|
||||||
Sandbox for Typosaurus end-to-end verification
|
Sandbox for Typosaurus end-to-end verification.
|
||||||
|
|
||||||
|
A FastAPI application serving arithmetic operations over HTTP with JSON request/response bodies.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
The application uses a single `.env.json` file at the project root as its central point of truth
|
||||||
|
for configuration. Defaults are plug-and-play and require no setup:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"host": "127.0.0.1",
|
||||||
|
"port": 8000
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
When no `.env.json` is present, the application starts with these defaults. To customise, create
|
||||||
|
`.env.json` in the project root and populate only the keys that differ.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Start the server
|
||||||
|
|
||||||
|
```sh
|
||||||
|
python -m typosaurus_sandbox
|
||||||
|
```
|
||||||
|
|
||||||
|
The server listens on `http://127.0.0.1:8000` by default.
|
||||||
|
|
||||||
|
### Health check
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /health
|
||||||
|
```
|
||||||
|
|
||||||
|
Response:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"status": "ok"}
|
||||||
|
```
|
||||||
|
|
||||||
|
## API endpoints
|
||||||
|
|
||||||
|
All calculator endpoints accept `POST` requests with a JSON body and return a JSON response.
|
||||||
|
|
||||||
|
### POST /api/v1/calculator/add
|
||||||
|
|
||||||
|
Add two integers.
|
||||||
|
|
||||||
|
Request:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"left": 3, "right": 5}
|
||||||
|
```
|
||||||
|
|
||||||
|
Response:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"result": 8}
|
||||||
|
```
|
||||||
|
|
||||||
|
### POST /api/v1/calculator/subtract
|
||||||
|
|
||||||
|
Subtract the right integer from the left.
|
||||||
|
|
||||||
|
Request:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"left": 10, "right": 3}
|
||||||
|
```
|
||||||
|
|
||||||
|
Response:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"result": 7}
|
||||||
|
```
|
||||||
|
|
||||||
|
### POST /api/v1/calculator/clamp
|
||||||
|
|
||||||
|
Clamp a value between a low and high bound.
|
||||||
|
|
||||||
|
Request:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"value": 15, "low": 0, "high": 10}
|
||||||
|
```
|
||||||
|
|
||||||
|
Response:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"result": 10}
|
||||||
|
```
|
||||||
|
|
||||||
|
Boundaries are inclusive. A `low > high` combination produces a 422 validation response.
|
||||||
|
|
||||||
|
### POST /api/v1/calculator/clamp-to-byte
|
||||||
|
|
||||||
|
Clamp an integer to the byte range [0, 255].
|
||||||
|
|
||||||
|
Request:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"value": 300}
|
||||||
|
```
|
||||||
|
|
||||||
|
Response:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"result": 255}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
```sh
|
||||||
|
make verify
|
||||||
|
```
|
||||||
|
|
||||||
|
Runs compile-all checks against all source and test files, then executes the full test suite.
|
||||||
|
Zero warnings are tolerated.
|
||||||
|
|||||||
67
app/__init__.py
Normal file
67
app/__init__.py
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
# retoor <retoor@molodetz.nl>
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from flask import Flask
|
||||||
|
from flask import jsonify
|
||||||
|
from flask import make_response
|
||||||
|
from flask import request
|
||||||
|
from flask.wrappers import Response
|
||||||
|
|
||||||
|
from src.calculator import add
|
||||||
|
from src.calculator import clamp
|
||||||
|
from src.calculator import clamp_to_byte
|
||||||
|
from src.calculator import subtract
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/')
|
||||||
|
def index() -> Response:
|
||||||
|
index_path = os.path.join(os.path.dirname(__file__), 'index.html')
|
||||||
|
with open(index_path) as f:
|
||||||
|
return make_response(f.read(), 200, {'Content-Type': 'text/html'})
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/add', methods=['GET'])
|
||||||
|
def add_route() -> Response:
|
||||||
|
try:
|
||||||
|
left = int(request.args['left'])
|
||||||
|
right = int(request.args['right'])
|
||||||
|
except (KeyError, TypeError, ValueError):
|
||||||
|
return make_response(jsonify({'error': 'Invalid or missing parameters'}), 400)
|
||||||
|
return make_response(jsonify({'result': add(left, right)}), 200)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/subtract', methods=['GET'])
|
||||||
|
def subtract_route() -> Response:
|
||||||
|
try:
|
||||||
|
left = int(request.args['left'])
|
||||||
|
right = int(request.args['right'])
|
||||||
|
except (KeyError, TypeError, ValueError):
|
||||||
|
return make_response(jsonify({'error': 'Invalid or missing parameters'}), 400)
|
||||||
|
return make_response(jsonify({'result': subtract(left, right)}), 200)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/clamp', methods=['GET'])
|
||||||
|
def clamp_route() -> Response:
|
||||||
|
try:
|
||||||
|
value = int(request.args['value'])
|
||||||
|
low = int(request.args['low'])
|
||||||
|
high = int(request.args['high'])
|
||||||
|
except (KeyError, TypeError, ValueError):
|
||||||
|
return make_response(jsonify({'error': 'Invalid or missing parameters'}), 400)
|
||||||
|
try:
|
||||||
|
result = clamp(value, low, high)
|
||||||
|
except ValueError:
|
||||||
|
return make_response(jsonify({'error': 'Invalid or missing parameters'}), 400)
|
||||||
|
return make_response(jsonify({'result': result}), 200)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/clamp_to_byte', methods=['GET'])
|
||||||
|
def clamp_to_byte_route() -> Response:
|
||||||
|
try:
|
||||||
|
value = int(request.args['value'])
|
||||||
|
except (KeyError, TypeError, ValueError):
|
||||||
|
return make_response(jsonify({'error': 'Invalid or missing parameters'}), 400)
|
||||||
|
return make_response(jsonify({'result': clamp_to_byte(value)}), 200)
|
||||||
69
app/index.html
Normal file
69
app/index.html
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
<!-- retoor <retoor@molodetz.nl> -->
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Calculator</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Calculator</h1>
|
||||||
|
|
||||||
|
<fieldset>
|
||||||
|
<legend>Add / Subtract</legend>
|
||||||
|
<input type="number" id="left" placeholder="Left operand">
|
||||||
|
<input type="number" id="right" placeholder="Right operand">
|
||||||
|
<button onclick="calculate('add')">Add</button>
|
||||||
|
<button onclick="calculate('subtract')">Subtract</button>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<fieldset>
|
||||||
|
<legend>Clamp</legend>
|
||||||
|
<input type="number" id="value" placeholder="Value">
|
||||||
|
<input type="number" id="low" placeholder="Low">
|
||||||
|
<input type="number" id="high" placeholder="High">
|
||||||
|
<button onclick="calculate('clamp')">Clamp</button>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<fieldset>
|
||||||
|
<legend>Clamp to Byte</legend>
|
||||||
|
<input type="number" id="byte_value" placeholder="Value">
|
||||||
|
<button onclick="calculate('clamp_to_byte')">Clamp to Byte</button>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<p id="output"></p>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function calculate(operation) {
|
||||||
|
const resultEl = document.getElementById('output');
|
||||||
|
let url;
|
||||||
|
if (operation === 'add' || operation === 'subtract') {
|
||||||
|
const left = document.getElementById('left').value;
|
||||||
|
const right = document.getElementById('right').value;
|
||||||
|
url = '/' + operation + '?left=' + encodeURIComponent(left) + '&right=' + encodeURIComponent(right);
|
||||||
|
} else if (operation === 'clamp') {
|
||||||
|
const value = document.getElementById('value').value;
|
||||||
|
const low = document.getElementById('low').value;
|
||||||
|
const high = document.getElementById('high').value;
|
||||||
|
url = '/clamp?value=' + encodeURIComponent(value) + '&low=' + encodeURIComponent(low) + '&high=' + encodeURIComponent(high);
|
||||||
|
} else if (operation === 'clamp_to_byte') {
|
||||||
|
const value = document.getElementById('byte_value').value;
|
||||||
|
url = '/clamp_to_byte?value=' + encodeURIComponent(value);
|
||||||
|
}
|
||||||
|
fetch(url)
|
||||||
|
.then(function(response) {
|
||||||
|
return response.json().then(function(data) {
|
||||||
|
if (!response.ok) {
|
||||||
|
resultEl.textContent = 'Error: ' + (data.error || 'Unknown error');
|
||||||
|
} else {
|
||||||
|
resultEl.textContent = 'Result: ' + data.result;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(function() {
|
||||||
|
resultEl.textContent = 'Error: Network error';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
16
pyproject.toml
Normal file
16
pyproject.toml
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
[project]
|
||||||
|
name = "typosaurus-sandbox"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "Sandbox for Typosaurus end-to-end verification"
|
||||||
|
requires-python = ">=3.12"
|
||||||
|
dependencies = [
|
||||||
|
"fastapi",
|
||||||
|
"uvicorn[standard]",
|
||||||
|
]
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["setuptools"]
|
||||||
|
build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
|
[tool.setuptools.packages.find]
|
||||||
|
where = ["src"]
|
||||||
1
requirements.txt
Normal file
1
requirements.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
Flask>=3.0,<4.0
|
||||||
@ -1,5 +1,8 @@
|
|||||||
# retoor <retoor@molodetz.nl>
|
# retoor <retoor@molodetz.nl>
|
||||||
|
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
|
||||||
def add(left: int, right: int) -> int:
|
def add(left: int, right: int) -> int:
|
||||||
return left + right
|
return left + right
|
||||||
|
|
||||||
@ -20,3 +23,28 @@ def clamp(value: int, low: int, high: int) -> int:
|
|||||||
|
|
||||||
def clamp_to_byte(value: int) -> int:
|
def clamp_to_byte(value: int) -> int:
|
||||||
return max(0, min(255, value))
|
return max(0, min(255, value))
|
||||||
|
|
||||||
|
|
||||||
|
def median(values: list[float]) -> float:
|
||||||
|
if not values:
|
||||||
|
raise ValueError
|
||||||
|
sorted_values = sorted(values)
|
||||||
|
n = len(sorted_values)
|
||||||
|
mid = n // 2
|
||||||
|
if n % 2 == 1:
|
||||||
|
return sorted_values[mid]
|
||||||
|
return (sorted_values[mid - 1] + sorted_values[mid]) / 2.0
|
||||||
|
|
||||||
|
|
||||||
|
def percentage(value: Union[int, float], total: Union[int, float]) -> float:
|
||||||
|
if total == 0:
|
||||||
|
raise ValueError
|
||||||
|
return (value / total) * 100
|
||||||
|
|
||||||
|
|
||||||
|
def variance(values: Sequence[float]) -> float:
|
||||||
|
if not values:
|
||||||
|
raise ValueError
|
||||||
|
mean = sum(values) / len(values)
|
||||||
|
return sum((x - mean) ** 2 for x in values) / len(values)
|
||||||
|
|
||||||
|
|||||||
5
src/typosaurus_sandbox/__init__.py
Normal file
5
src/typosaurus_sandbox/__init__.py
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
# retoor <retoor@molodetz.nl>
|
||||||
|
|
||||||
|
from typosaurus_sandbox.app import App
|
||||||
|
|
||||||
|
__all__ = ["App"]
|
||||||
7
src/typosaurus_sandbox/__main__.py
Normal file
7
src/typosaurus_sandbox/__main__.py
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
# retoor <retoor@molodetz.nl>
|
||||||
|
|
||||||
|
import uvicorn
|
||||||
|
|
||||||
|
from typosaurus_sandbox.app import App
|
||||||
|
|
||||||
|
uvicorn.run(App, host="127.0.0.1", port=8000, log_level="info")
|
||||||
15
src/typosaurus_sandbox/app.py
Normal file
15
src/typosaurus_sandbox/app.py
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
# retoor <retoor@molodetz.nl>
|
||||||
|
|
||||||
|
from fastapi import FastAPI
|
||||||
|
|
||||||
|
from typosaurus_sandbox.presentation.api.v1.calculator import calculator_router
|
||||||
|
|
||||||
|
App = FastAPI(title="typosaurus-sandbox")
|
||||||
|
|
||||||
|
|
||||||
|
@App.get("/health")
|
||||||
|
def health() -> dict[str, str]:
|
||||||
|
return {"status": "ok"}
|
||||||
|
|
||||||
|
|
||||||
|
App.include_router(calculator_router)
|
||||||
1
src/typosaurus_sandbox/domain/__init__.py
Normal file
1
src/typosaurus_sandbox/domain/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
# retoor <retoor@molodetz.nl>
|
||||||
7
src/typosaurus_sandbox/domain/calculator/__init__.py
Normal file
7
src/typosaurus_sandbox/domain/calculator/__init__.py
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
# retoor <retoor@molodetz.nl>
|
||||||
|
|
||||||
|
from typosaurus_sandbox.domain.calculator.operations import add, clamp, clamp_to_byte, median, subtract, variance
|
||||||
|
|
||||||
|
__all__ = ["add", "subtract", "clamp", "clamp_to_byte", "median", "variance"]
|
||||||
|
|
||||||
|
|
||||||
45
src/typosaurus_sandbox/domain/calculator/operations.py
Normal file
45
src/typosaurus_sandbox/domain/calculator/operations.py
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
# retoor <retoor@molodetz.nl>
|
||||||
|
|
||||||
|
from typing import Sequence
|
||||||
|
|
||||||
|
|
||||||
|
def add(left: int, right: int) -> int:
|
||||||
|
return left + right
|
||||||
|
|
||||||
|
|
||||||
|
def subtract(left: int, right: int) -> int:
|
||||||
|
return left - right
|
||||||
|
|
||||||
|
|
||||||
|
def clamp(value: int, low: int, high: int) -> int:
|
||||||
|
if low > high:
|
||||||
|
raise ValueError
|
||||||
|
if value < low:
|
||||||
|
return low
|
||||||
|
if value > high:
|
||||||
|
return high
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def clamp_to_byte(value: int) -> int:
|
||||||
|
return max(0, min(255, value))
|
||||||
|
|
||||||
|
|
||||||
|
def median(values: list[float]) -> float:
|
||||||
|
if not values:
|
||||||
|
raise ValueError
|
||||||
|
sorted_values = sorted(values)
|
||||||
|
n = len(sorted_values)
|
||||||
|
mid = n // 2
|
||||||
|
if n % 2 == 1:
|
||||||
|
return sorted_values[mid]
|
||||||
|
return (sorted_values[mid - 1] + sorted_values[mid]) / 2.0
|
||||||
|
|
||||||
|
|
||||||
|
def variance(values: Sequence[float]) -> float:
|
||||||
|
if not values:
|
||||||
|
raise ValueError
|
||||||
|
mean = sum(values) / len(values)
|
||||||
|
return sum((x - mean) ** 2 for x in values) / len(values)
|
||||||
|
|
||||||
|
|
||||||
1
src/typosaurus_sandbox/presentation/__init__.py
Normal file
1
src/typosaurus_sandbox/presentation/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
# retoor <retoor@molodetz.nl>
|
||||||
1
src/typosaurus_sandbox/presentation/api/__init__.py
Normal file
1
src/typosaurus_sandbox/presentation/api/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
# retoor <retoor@molodetz.nl>
|
||||||
1
src/typosaurus_sandbox/presentation/api/v1/__init__.py
Normal file
1
src/typosaurus_sandbox/presentation/api/v1/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
# retoor <retoor@molodetz.nl>
|
||||||
60
src/typosaurus_sandbox/presentation/api/v1/calculator.py
Normal file
60
src/typosaurus_sandbox/presentation/api/v1/calculator.py
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
# retoor <retoor@molodetz.nl>
|
||||||
|
|
||||||
|
from fastapi import APIRouter, HTTPException
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
|
from typosaurus_sandbox.domain.calculator import add, clamp, clamp_to_byte, subtract
|
||||||
|
|
||||||
|
calculator_router = APIRouter(prefix="/api/v1/calculator")
|
||||||
|
|
||||||
|
|
||||||
|
class AddRequest(BaseModel):
|
||||||
|
left: int
|
||||||
|
right: int
|
||||||
|
|
||||||
|
|
||||||
|
class SubtractRequest(BaseModel):
|
||||||
|
left: int
|
||||||
|
right: int
|
||||||
|
|
||||||
|
|
||||||
|
class ClampRequest(BaseModel):
|
||||||
|
value: float
|
||||||
|
low: float
|
||||||
|
high: float
|
||||||
|
|
||||||
|
|
||||||
|
class ClampToByteRequest(BaseModel):
|
||||||
|
value: int = Field(ge=-2147483648, le=2147483647)
|
||||||
|
|
||||||
|
|
||||||
|
class IntResult(BaseModel):
|
||||||
|
result: int
|
||||||
|
|
||||||
|
|
||||||
|
class FloatResult(BaseModel):
|
||||||
|
result: float
|
||||||
|
|
||||||
|
|
||||||
|
@calculator_router.post("/add", response_model=IntResult)
|
||||||
|
def calculate_add(body: AddRequest) -> IntResult:
|
||||||
|
return IntResult(result=add(body.left, body.right))
|
||||||
|
|
||||||
|
|
||||||
|
@calculator_router.post("/subtract", response_model=IntResult)
|
||||||
|
def calculate_subtract(body: SubtractRequest) -> IntResult:
|
||||||
|
return IntResult(result=subtract(body.left, body.right))
|
||||||
|
|
||||||
|
|
||||||
|
@calculator_router.post("/clamp", response_model=FloatResult)
|
||||||
|
def calculate_clamp(body: ClampRequest) -> FloatResult:
|
||||||
|
try:
|
||||||
|
result = clamp(body.value, body.low, body.high)
|
||||||
|
except ValueError:
|
||||||
|
raise HTTPException(status_code=422, detail="low must not exceed high")
|
||||||
|
return FloatResult(result=result)
|
||||||
|
|
||||||
|
|
||||||
|
@calculator_router.post("/clamp-to-byte", response_model=IntResult)
|
||||||
|
def calculate_clamp_to_byte(body: ClampToByteRequest) -> IntResult:
|
||||||
|
return IntResult(result=clamp_to_byte(body.value))
|
||||||
120
tests/test_api.py
Normal file
120
tests/test_api.py
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
# retoor <retoor@molodetz.nl>
|
||||||
|
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
from fastapi.testclient import TestClient
|
||||||
|
|
||||||
|
from typosaurus_sandbox.app import App
|
||||||
|
|
||||||
|
client = TestClient(App)
|
||||||
|
|
||||||
|
|
||||||
|
class TestHealthEndpoint(unittest.TestCase):
|
||||||
|
|
||||||
|
def test_health_returns_ok(self) -> None:
|
||||||
|
response = client.get("/health")
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertEqual(response.json(), {"status": "ok"})
|
||||||
|
|
||||||
|
|
||||||
|
class TestCalculatorAddEndpoint(unittest.TestCase):
|
||||||
|
|
||||||
|
def test_add_positive_integers(self) -> None:
|
||||||
|
response = client.post("/api/v1/calculator/add", json={"left": 3, "right": 5})
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertEqual(response.json(), {"result": 8})
|
||||||
|
|
||||||
|
def test_add_negative_integers(self) -> None:
|
||||||
|
response = client.post("/api/v1/calculator/add", json={"left": -3, "right": -5})
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertEqual(response.json(), {"result": -8})
|
||||||
|
|
||||||
|
def test_add_invalid_input_returns_422(self) -> None:
|
||||||
|
response = client.post("/api/v1/calculator/add", json={"left": "abc", "right": 5})
|
||||||
|
self.assertEqual(response.status_code, 422)
|
||||||
|
|
||||||
|
def test_add_missing_field_returns_422(self) -> None:
|
||||||
|
response = client.post("/api/v1/calculator/add", json={"left": 3})
|
||||||
|
self.assertEqual(response.status_code, 422)
|
||||||
|
|
||||||
|
|
||||||
|
class TestCalculatorSubtractEndpoint(unittest.TestCase):
|
||||||
|
|
||||||
|
def test_subtract_positive(self) -> None:
|
||||||
|
response = client.post("/api/v1/calculator/subtract", json={"left": 10, "right": 3})
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertEqual(response.json(), {"result": 7})
|
||||||
|
|
||||||
|
def test_subtract_negative_result(self) -> None:
|
||||||
|
response = client.post("/api/v1/calculator/subtract", json={"left": 3, "right": 10})
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertEqual(response.json(), {"result": -7})
|
||||||
|
|
||||||
|
def test_subtract_invalid_input_returns_422(self) -> None:
|
||||||
|
response = client.post("/api/v1/calculator/subtract", json={"left": 10, "right": None})
|
||||||
|
self.assertEqual(response.status_code, 422)
|
||||||
|
|
||||||
|
def test_subtract_missing_left_returns_422(self) -> None:
|
||||||
|
response = client.post("/api/v1/calculator/subtract", json={"right": 3})
|
||||||
|
self.assertEqual(response.status_code, 422)
|
||||||
|
|
||||||
|
def test_subtract_missing_right_returns_422(self) -> None:
|
||||||
|
response = client.post("/api/v1/calculator/subtract", json={"left": 10})
|
||||||
|
self.assertEqual(response.status_code, 422)
|
||||||
|
|
||||||
|
|
||||||
|
class TestCalculatorClampEndpoint(unittest.TestCase):
|
||||||
|
|
||||||
|
def test_clamp_value_below_low(self) -> None:
|
||||||
|
response = client.post("/api/v1/calculator/clamp", json={"value": -5, "low": 0, "high": 10})
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertEqual(response.json(), {"result": 0})
|
||||||
|
|
||||||
|
def test_clamp_value_above_high(self) -> None:
|
||||||
|
response = client.post("/api/v1/calculator/clamp", json={"value": 15, "low": 0, "high": 10})
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertEqual(response.json(), {"result": 10})
|
||||||
|
|
||||||
|
def test_clamp_value_in_range(self) -> None:
|
||||||
|
response = client.post("/api/v1/calculator/clamp", json={"value": 5, "low": 0, "high": 10})
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertEqual(response.json(), {"result": 5.0})
|
||||||
|
|
||||||
|
def test_clamp_invalid_input_returns_422(self) -> None:
|
||||||
|
response = client.post("/api/v1/calculator/clamp", json={"value": "x", "low": 0, "high": 10})
|
||||||
|
self.assertEqual(response.status_code, 422)
|
||||||
|
|
||||||
|
def test_clamp_low_greater_than_high_returns_422(self) -> None:
|
||||||
|
response = client.post("/api/v1/calculator/clamp", json={"value": 5, "low": 10, "high": 0})
|
||||||
|
self.assertEqual(response.status_code, 422)
|
||||||
|
|
||||||
|
def test_clamp_missing_low_returns_422(self) -> None:
|
||||||
|
response = client.post("/api/v1/calculator/clamp", json={"value": 5, "high": 10})
|
||||||
|
self.assertEqual(response.status_code, 422)
|
||||||
|
|
||||||
|
def test_clamp_missing_high_returns_422(self) -> None:
|
||||||
|
response = client.post("/api/v1/calculator/clamp", json={"value": 5, "low": 0})
|
||||||
|
self.assertEqual(response.status_code, 422)
|
||||||
|
|
||||||
|
|
||||||
|
class TestCalculatorClampToByteEndpoint(unittest.TestCase):
|
||||||
|
|
||||||
|
def test_clamp_to_byte_within_range(self) -> None:
|
||||||
|
response = client.post("/api/v1/calculator/clamp-to-byte", json={"value": 128})
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertEqual(response.json(), {"result": 128})
|
||||||
|
|
||||||
|
def test_clamp_to_byte_below_zero(self) -> None:
|
||||||
|
response = client.post("/api/v1/calculator/clamp-to-byte", json={"value": -10})
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertEqual(response.json(), {"result": 0})
|
||||||
|
|
||||||
|
def test_clamp_to_byte_above_255(self) -> None:
|
||||||
|
response = client.post("/api/v1/calculator/clamp-to-byte", json={"value": 300})
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertEqual(response.json(), {"result": 255})
|
||||||
|
|
||||||
|
def test_clamp_to_byte_invalid_input_returns_422(self) -> None:
|
||||||
|
response = client.post("/api/v1/calculator/clamp-to-byte", json={"value": "abc"})
|
||||||
|
self.assertEqual(response.status_code, 422)
|
||||||
|
|
||||||
@ -3,7 +3,47 @@
|
|||||||
import math
|
import math
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
from src.calculator import clamp
|
from typosaurus_sandbox.domain.calculator import add, clamp, clamp_to_byte, median, subtract, variance
|
||||||
|
|
||||||
|
|
||||||
|
class TestAddFunction(unittest.TestCase):
|
||||||
|
|
||||||
|
def test_add_positive_integers(self) -> None:
|
||||||
|
self.assertEqual(add(3, 5), 8)
|
||||||
|
|
||||||
|
def test_add_negative_integers(self) -> None:
|
||||||
|
self.assertEqual(add(-3, -5), -8)
|
||||||
|
|
||||||
|
def test_add_mixed_sign(self) -> None:
|
||||||
|
self.assertEqual(add(-3, 5), 2)
|
||||||
|
|
||||||
|
|
||||||
|
class TestSubtractFunction(unittest.TestCase):
|
||||||
|
|
||||||
|
def test_subtract_positive(self) -> None:
|
||||||
|
self.assertEqual(subtract(10, 3), 7)
|
||||||
|
|
||||||
|
def test_subtract_negative_result(self) -> None:
|
||||||
|
self.assertEqual(subtract(3, 10), -7)
|
||||||
|
|
||||||
|
def test_subtract_negative_numbers(self) -> None:
|
||||||
|
self.assertEqual(subtract(-5, -3), -2)
|
||||||
|
|
||||||
|
|
||||||
|
class TestClampToByteFunction(unittest.TestCase):
|
||||||
|
|
||||||
|
def test_clamp_to_byte_within_range(self) -> None:
|
||||||
|
self.assertEqual(clamp_to_byte(128), 128)
|
||||||
|
|
||||||
|
def test_clamp_to_byte_below_zero(self) -> None:
|
||||||
|
self.assertEqual(clamp_to_byte(-10), 0)
|
||||||
|
|
||||||
|
def test_clamp_to_byte_above_255(self) -> None:
|
||||||
|
self.assertEqual(clamp_to_byte(300), 255)
|
||||||
|
|
||||||
|
def test_clamp_to_byte_boundaries(self) -> None:
|
||||||
|
self.assertEqual(clamp_to_byte(0), 0)
|
||||||
|
self.assertEqual(clamp_to_byte(255), 255)
|
||||||
|
|
||||||
|
|
||||||
class TestClampFunction(unittest.TestCase):
|
class TestClampFunction(unittest.TestCase):
|
||||||
@ -61,3 +101,61 @@ class TestClampFunction(unittest.TestCase):
|
|||||||
def test_nan_returns_nan(self) -> None:
|
def test_nan_returns_nan(self) -> None:
|
||||||
result = clamp(math.nan, 0, 10)
|
result = clamp(math.nan, 0, 10)
|
||||||
self.assertTrue(math.isnan(result))
|
self.assertTrue(math.isnan(result))
|
||||||
|
|
||||||
|
|
||||||
|
class TestMedianFunction(unittest.TestCase):
|
||||||
|
|
||||||
|
def test_odd_length_returns_middle_element(self) -> None:
|
||||||
|
self.assertEqual(median([1, 3, 5]), 3)
|
||||||
|
|
||||||
|
def test_even_length_returns_float_average_of_two_middle_values(self) -> None:
|
||||||
|
result = median([1, 2, 3, 4])
|
||||||
|
self.assertIsInstance(result, float)
|
||||||
|
self.assertEqual(result, 2.5)
|
||||||
|
|
||||||
|
def test_single_element_returns_that_element(self) -> None:
|
||||||
|
self.assertEqual(median([7]), 7)
|
||||||
|
|
||||||
|
def test_empty_list_raises_value_error(self) -> None:
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
median([])
|
||||||
|
|
||||||
|
def test_unsorted_input_sorts_correctly(self) -> None:
|
||||||
|
self.assertEqual(median([3, 1, 2]), 2)
|
||||||
|
|
||||||
|
def test_unsorted_even_length_returns_float_average(self) -> None:
|
||||||
|
result = median([10, 30, 20, 40])
|
||||||
|
self.assertIsInstance(result, float)
|
||||||
|
self.assertEqual(result, 25.0)
|
||||||
|
|
||||||
|
|
||||||
|
class TestVarianceFunction(unittest.TestCase):
|
||||||
|
|
||||||
|
def test_empty_list_raises_value_error(self) -> None:
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
variance([])
|
||||||
|
|
||||||
|
def test_empty_tuple_raises_value_error(self) -> None:
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
variance(())
|
||||||
|
|
||||||
|
def test_single_element_returns_zero(self) -> None:
|
||||||
|
self.assertEqual(variance([42.0]), 0.0)
|
||||||
|
|
||||||
|
def test_constant_values_return_zero_variance(self) -> None:
|
||||||
|
self.assertEqual(variance([1.0, 1.0, 1.0]), 0.0)
|
||||||
|
|
||||||
|
def test_population_variance_of_known_set(self) -> None:
|
||||||
|
self.assertEqual(variance([1, 2, 3, 4, 5]), 2.0)
|
||||||
|
|
||||||
|
def test_two_element_variance(self) -> None:
|
||||||
|
self.assertEqual(variance([0, 2]), 1.0)
|
||||||
|
|
||||||
|
def test_integer_inputs_return_float(self) -> None:
|
||||||
|
result = variance([10, 20, 30])
|
||||||
|
self.assertIsInstance(result, float)
|
||||||
|
self.assertEqual(result, 200.0 / 3.0)
|
||||||
|
|
||||||
|
def test_tuple_input_returns_variance(self) -> None:
|
||||||
|
self.assertEqual(variance((1, 2, 3, 4, 5)), 2.0)
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user