Compare commits
No commits in common. "main" and "typosaurus/9-make-it-a-web-application" have entirely different histories.
main
...
typosaurus
@ -1,21 +0,0 @@
|
|||||||
# retoor <retoor@molodetz.nl>
|
|
||||||
name: CI
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [main, master]
|
|
||||||
jobs:
|
|
||||||
test:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- name: Set up Python 3.12
|
|
||||||
uses: actions/setup-python@v5
|
|
||||||
with:
|
|
||||||
python-version: "3.12"
|
|
||||||
- name: Install dependencies
|
|
||||||
run: |
|
|
||||||
python -m pip install --upgrade pip
|
|
||||||
pip install -e .
|
|
||||||
- name: Run tests
|
|
||||||
run: make verify
|
|
||||||
|
|
||||||
23
CLAUDE.md
23
CLAUDE.md
@ -1,4 +1,3 @@
|
|||||||
# retoor <retoor@molodetz.nl>
|
|
||||||
# typosaurus-sandbox
|
# typosaurus-sandbox
|
||||||
|
|
||||||
A minimal Python calculator used to verify the Typosaurus agent system.
|
A minimal Python calculator used to verify the Typosaurus agent system.
|
||||||
@ -8,30 +7,8 @@ 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
|
||||||
|
|
||||||
```
|
```
|
||||||
make verify
|
make verify
|
||||||
```
|
```
|
||||||
|
|
||||||
## CI
|
|
||||||
|
|
||||||
- Workflow file: `.gitea/workflows/ci.yml`
|
|
||||||
- Trigger: push to `main` or `master` branches
|
|
||||||
- Steps: checkout, Python 3.12 setup, dependency install, `make verify`
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
7
Makefile
7
Makefile
@ -1,8 +1,5 @@
|
|||||||
# retoor <retoor@molodetz.nl>
|
|
||||||
|
|
||||||
verify:
|
verify:
|
||||||
@PYTHONPATH=src python3 -m compileall -q src tests && PYTHONPATH=src python3 -m unittest discover -s tests -q && echo "verification passed"
|
@python3.13 -m compileall -q src tests app && python3.13 -m unittest discover -s tests -q && echo "verification passed"
|
||||||
|
|
||||||
run:
|
run:
|
||||||
@PYTHONPATH=src python3 -m typosaurus_sandbox
|
FLASK_APP=app flask run
|
||||||
|
|
||||||
|
|||||||
122
README.md
122
README.md
@ -1,123 +1,11 @@
|
|||||||
# 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.
|
## Running the web app
|
||||||
|
|
||||||
## Configuration
|
```bash
|
||||||
|
make run
|
||||||
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
|
The development server starts at http://127.0.0.1:5000.
|
||||||
`.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.
|
|
||||||
@ -1,16 +0,0 @@
|
|||||||
[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,8 +1,5 @@
|
|||||||
# 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
|
||||||
|
|
||||||
@ -23,34 +20,3 @@ 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 average(values: list[int | float]) -> float:
|
|
||||||
if not values:
|
|
||||||
raise ValueError
|
|
||||||
return sum(values) / len(values)
|
|
||||||
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +0,0 @@
|
|||||||
# retoor <retoor@molodetz.nl>
|
|
||||||
|
|
||||||
from typosaurus_sandbox.app import App
|
|
||||||
|
|
||||||
__all__ = ["App"]
|
|
||||||
@ -1,7 +0,0 @@
|
|||||||
# 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")
|
|
||||||
@ -1,15 +0,0 @@
|
|||||||
# 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 +0,0 @@
|
|||||||
# retoor <retoor@molodetz.nl>
|
|
||||||
@ -1,6 +0,0 @@
|
|||||||
# retoor <retoor@molodetz.nl>
|
|
||||||
|
|
||||||
from typosaurus_sandbox.domain.calculator.operations import add, average, clamp, clamp_to_byte, median, percentage, subtract, variance
|
|
||||||
|
|
||||||
__all__ = ["add", "average", "clamp", "clamp_to_byte", "median", "percentage", "subtract", "variance"]
|
|
||||||
|
|
||||||
@ -1,56 +0,0 @@
|
|||||||
# retoor <retoor@molodetz.nl>
|
|
||||||
|
|
||||||
from typing import Sequence, Union
|
|
||||||
|
|
||||||
|
|
||||||
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 average(values: list[int | float]) -> float:
|
|
||||||
if not values:
|
|
||||||
raise ValueError
|
|
||||||
return sum(values) / len(values)
|
|
||||||
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
|
|
||||||
def percentage(value: Union[int, float], total: Union[int, float]) -> float:
|
|
||||||
if total == 0:
|
|
||||||
raise ValueError
|
|
||||||
return (value / total) * 100
|
|
||||||
|
|
||||||
@ -1 +0,0 @@
|
|||||||
# retoor <retoor@molodetz.nl>
|
|
||||||
@ -1 +0,0 @@
|
|||||||
# retoor <retoor@molodetz.nl>
|
|
||||||
@ -1 +0,0 @@
|
|||||||
# retoor <retoor@molodetz.nl>
|
|
||||||
@ -1,60 +0,0 @@
|
|||||||
# 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))
|
|
||||||
@ -1,120 +1,87 @@
|
|||||||
# retoor <retoor@molodetz.nl>
|
# retoor <retoor@molodetz.nl>
|
||||||
|
|
||||||
|
import json
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
from fastapi.testclient import TestClient
|
from app import app
|
||||||
|
|
||||||
from typosaurus_sandbox.app import App
|
|
||||||
|
|
||||||
client = TestClient(App)
|
|
||||||
|
|
||||||
|
|
||||||
class TestHealthEndpoint(unittest.TestCase):
|
class TestCalculatorAPI(unittest.TestCase):
|
||||||
|
|
||||||
def test_health_returns_ok(self) -> None:
|
def setUp(self) -> None:
|
||||||
response = client.get("/health")
|
self.client = app.test_client()
|
||||||
|
|
||||||
|
def test_add_success(self) -> None:
|
||||||
|
response = self.client.get('/add?left=3&right=5')
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
self.assertEqual(response.json(), {"status": "ok"})
|
self.assertEqual(response.json, {'result': 8})
|
||||||
|
|
||||||
|
def test_add_missing_param_returns_400(self) -> None:
|
||||||
|
response = self.client.get('/add?left=3')
|
||||||
|
self.assertEqual(response.status_code, 400)
|
||||||
|
self.assertIn('error', response.json)
|
||||||
|
|
||||||
class TestCalculatorAddEndpoint(unittest.TestCase):
|
def test_add_invalid_type_returns_400(self) -> None:
|
||||||
|
response = self.client.get('/add?left=abc&right=5')
|
||||||
|
self.assertEqual(response.status_code, 400)
|
||||||
|
self.assertIn('error', response.json)
|
||||||
|
|
||||||
def test_add_positive_integers(self) -> None:
|
def test_subtract_success(self) -> None:
|
||||||
response = client.post("/api/v1/calculator/add", json={"left": 3, "right": 5})
|
response = self.client.get('/subtract?left=10&right=3')
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
self.assertEqual(response.json(), {"result": 8})
|
self.assertEqual(response.json, {'result': 7})
|
||||||
|
|
||||||
def test_add_negative_integers(self) -> None:
|
def test_subtract_missing_param_returns_400(self) -> None:
|
||||||
response = client.post("/api/v1/calculator/add", json={"left": -3, "right": -5})
|
response = self.client.get('/subtract?right=3')
|
||||||
|
self.assertEqual(response.status_code, 400)
|
||||||
|
self.assertIn('error', response.json)
|
||||||
|
|
||||||
|
def test_subtract_invalid_type_returns_400(self) -> None:
|
||||||
|
response = self.client.get('/subtract?left=10&right=xyz')
|
||||||
|
self.assertEqual(response.status_code, 400)
|
||||||
|
self.assertIn('error', response.json)
|
||||||
|
|
||||||
|
def test_clamp_success(self) -> None:
|
||||||
|
response = self.client.get('/clamp?value=5&low=0&high=10')
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
self.assertEqual(response.json(), {"result": -8})
|
self.assertEqual(response.json, {'result': 5})
|
||||||
|
|
||||||
def test_add_invalid_input_returns_422(self) -> None:
|
def test_clamp_value_below_low_returns_low(self) -> None:
|
||||||
response = client.post("/api/v1/calculator/add", json={"left": "abc", "right": 5})
|
response = self.client.get('/clamp?value=-5&low=0&high=10')
|
||||||
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.status_code, 200)
|
||||||
self.assertEqual(response.json(), {"result": 7})
|
self.assertEqual(response.json, {'result': 0})
|
||||||
|
|
||||||
def test_subtract_negative_result(self) -> None:
|
def test_clamp_missing_param_returns_400(self) -> None:
|
||||||
response = client.post("/api/v1/calculator/subtract", json={"left": 3, "right": 10})
|
response = self.client.get('/clamp?value=5&low=0')
|
||||||
|
self.assertEqual(response.status_code, 400)
|
||||||
|
self.assertIn('error', response.json)
|
||||||
|
|
||||||
|
def test_clamp_invalid_type_returns_400(self) -> None:
|
||||||
|
response = self.client.get('/clamp?value=abc&low=0&high=10')
|
||||||
|
self.assertEqual(response.status_code, 400)
|
||||||
|
self.assertIn('error', response.json)
|
||||||
|
|
||||||
|
def test_clamp_low_greater_than_high_returns_400(self) -> None:
|
||||||
|
response = self.client.get('/clamp?value=5&low=10&high=0')
|
||||||
|
self.assertEqual(response.status_code, 400)
|
||||||
|
self.assertIn('error', response.json)
|
||||||
|
|
||||||
|
def test_clamp_to_byte_success(self) -> None:
|
||||||
|
response = self.client.get('/clamp_to_byte?value=100')
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
self.assertEqual(response.json(), {"result": -7})
|
self.assertEqual(response.json, {'result': 100})
|
||||||
|
|
||||||
def test_subtract_invalid_input_returns_422(self) -> None:
|
def test_clamp_to_byte_above_255_returns_255(self) -> None:
|
||||||
response = client.post("/api/v1/calculator/subtract", json={"left": 10, "right": None})
|
response = self.client.get('/clamp_to_byte?value=300')
|
||||||
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.status_code, 200)
|
||||||
self.assertEqual(response.json(), {"result": 0})
|
self.assertEqual(response.json, {'result': 255})
|
||||||
|
|
||||||
def test_clamp_value_above_high(self) -> None:
|
def test_clamp_to_byte_missing_param_returns_400(self) -> None:
|
||||||
response = client.post("/api/v1/calculator/clamp", json={"value": 15, "low": 0, "high": 10})
|
response = self.client.get('/clamp_to_byte')
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 400)
|
||||||
self.assertEqual(response.json(), {"result": 10})
|
self.assertIn('error', response.json)
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
|
def test_clamp_to_byte_invalid_type_returns_400(self) -> None:
|
||||||
|
response = self.client.get('/clamp_to_byte?value=abc')
|
||||||
|
self.assertEqual(response.status_code, 400)
|
||||||
|
self.assertIn('error', response.json)
|
||||||
|
|||||||
@ -3,69 +3,7 @@
|
|||||||
import math
|
import math
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
from typosaurus_sandbox.domain.calculator import add, average, clamp, clamp_to_byte, median, subtract, variance
|
from src.calculator import clamp
|
||||||
|
|
||||||
|
|
||||||
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 TestAverageFunction(unittest.TestCase):
|
|
||||||
|
|
||||||
def test_empty_sequence_raises_value_error(self) -> None:
|
|
||||||
with self.assertRaises(ValueError):
|
|
||||||
average([])
|
|
||||||
|
|
||||||
def test_single_element(self) -> None:
|
|
||||||
self.assertEqual(average([5]), 5.0)
|
|
||||||
|
|
||||||
def test_positive_values(self) -> None:
|
|
||||||
self.assertEqual(average([1, 2, 3, 4, 5]), 3.0)
|
|
||||||
|
|
||||||
def test_negative_values(self) -> None:
|
|
||||||
self.assertEqual(average([-10, -20, -30]), -20.0)
|
|
||||||
|
|
||||||
def test_mixed_positive_and_negative(self) -> None:
|
|
||||||
self.assertEqual(average([-5, 0, 5]), 0.0)
|
|
||||||
|
|
||||||
def test_float_values(self) -> None:
|
|
||||||
self.assertEqual(average([1.5, 2.5, 3.0]), 7.0 / 3.0)
|
|
||||||
|
|
||||||
|
|
||||||
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):
|
||||||
@ -123,61 +61,3 @@ 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