test: add end-to-end and load verification

This commit is contained in:
2026-09-20 17:46:42 +03:30
parent 85d9003e43
commit d2a678264e
3 changed files with 1145 additions and 0 deletions

View File

@@ -0,0 +1,242 @@
import json
import os
import random
import time
import uuid
from collections import Counter
from concurrent.futures import ThreadPoolExecutor, as_completed
from copy import deepcopy
from urllib.error import HTTPError, URLError
from urllib.request import Request, urlopen
BASE_URL = os.getenv(
"BASE_URL",
"http://localhost:8000",
)
ENDPOINT = f"{BASE_URL}/api/v1/cdr/raw"
RUN_ID = os.getenv(
"RUN_ID",
uuid.uuid4().hex[:8],
)
def valid_payload(index: int) -> dict:
start = int(time.time())
return {
"raw_id": f"functional-{RUN_ID}-{index:04d}",
"src_ip": "10.240.12.1",
"device_id": "310410998877665",
"caller_num": "+14155552671",
"svc": "4G_DATA",
"up_bytes": 1024,
"down_bytes": 4096,
"rat": 6,
"lac": "4021",
"ci": "8812",
"epoch_start": start,
"epoch_end": start + 300,
}
def send_json(payload: dict) -> int | str:
body = json.dumps(
payload
).encode("utf-8")
return send_raw(body)
def send_raw(body: bytes) -> int | str:
request = Request(
ENDPOINT,
data=body,
method="POST",
headers={
"Content-Type": "application/json",
},
)
try:
with urlopen(
request,
timeout=10,
) as response:
return response.status
except HTTPError as exc:
return exc.code
except URLError as exc:
return f"network_error:{exc.reason}"
def main() -> None:
requests: list[
tuple[str, dict | bytes]
] = []
valid_records = [
valid_payload(index)
for index in range(1000)
]
# 1000 valid CDRs
for payload in valid_records:
requests.append(
("valid", payload)
)
# 20 missing caller_num
for index in range(20):
payload = valid_payload(
2000 + index
)
payload.pop(
"caller_num"
)
requests.append(
("missing_caller_num", payload)
)
# 10 malformed JSON requests
for _ in range(10):
requests.append(
(
"invalid_json",
b'{"raw_id": invalid}',
)
)
# 10 invalid RAT values
for index in range(10):
payload = valid_payload(
3000 + index
)
payload["rat"] = 99
requests.append(
("invalid_rat", payload)
)
# 10 invalid epochs
for index in range(10):
payload = valid_payload(
4000 + index
)
payload["epoch_start"] = (
payload["epoch_end"] + 100
)
requests.append(
("invalid_epoch", payload)
)
# 20 duplicates of already accepted valid records.
for payload in valid_records[:20]:
requests.append(
(
"duplicate",
deepcopy(payload),
)
)
random.shuffle(
requests
)
results = Counter()
started_at = time.perf_counter()
with ThreadPoolExecutor(
max_workers=50
) as executor:
futures = []
for request_type, payload in requests:
if isinstance(
payload,
bytes,
):
future = executor.submit(
send_raw,
payload,
)
else:
future = executor.submit(
send_json,
payload,
)
futures.append(
(
request_type,
future,
)
)
for request_type, future in futures:
status = future.result()
results[
(
request_type,
status,
)
] += 1
elapsed = (
time.perf_counter()
- started_at
)
print()
print(
f"Run ID: {RUN_ID}"
)
print(
f"Total requests: {len(requests)}"
)
print(
f"Elapsed: {elapsed:.2f}s"
)
print()
for (
request_type,
status,
), count in sorted(
results.items(),
key=lambda item: str(
item[0]
),
):
print(
f"{request_type:24} "
f"status={status!s:8} "
f"count={count}"
)
print()
print("Expected:")
print(" valid → 1000 x 202")
print(" duplicate → 20 x 202")
print(" missing_caller → 20 x 400")
print(" invalid_json → 10 x 400")
print(" invalid_rat → 10 x 400")
print(" invalid_epoch → 10 x 400")
if __name__ == "__main__":
main()

131
scripts/load_test.js Normal file
View File

@@ -0,0 +1,131 @@
import http from 'k6/http';
import { check } from 'k6';
import { Counter } from 'k6/metrics';
const accepted = new Counter(
'cdr_accepted'
);
const backpressure = new Counter(
'cdr_backpressure'
);
const unavailable = new Counter(
'cdr_unavailable'
);
const unexpected = new Counter(
'cdr_unexpected'
);
const RATE = Number(
__ENV.RATE || 1000
);
const PRE_ALLOCATED_VUS = Number(
__ENV.PRE_ALLOCATED_VUS || 250
);
const MAX_VUS = Number(
__ENV.MAX_VUS || 1000
);
const DURATION = (
__ENV.DURATION || '10s'
);
const BASE_URL = (
__ENV.BASE_URL ||
'http://localhost:8000'
);
export const options = {
discardResponseBodies: true,
scenarios: {
ingestion: {
executor: 'constant-arrival-rate',
rate: RATE,
timeUnit: '1s',
duration: DURATION,
preAllocatedVUs: PRE_ALLOCATED_VUS,
maxVUs: MAX_VUS,
},
},
};
export default function () {
const now = Math.floor(
Date.now() / 1000
);
const rawId =
`k6-${__VU}-${__ITER}-${Date.now()}`;
const payload = JSON.stringify({
raw_id: rawId,
src_ip: '10.240.12.1',
device_id:
'310410998877665',
caller_num:
'+14155552671',
svc: '4G_DATA',
up_bytes: 1024,
down_bytes: 4096,
rat: 6,
lac: '4021',
ci: '8812',
epoch_start: now,
epoch_end: now + 300,
});
const response = http.post(
`${BASE_URL}/api/v1/cdr/raw`,
payload,
{
headers: {
'Content-Type':
'application/json',
},
tags: {
endpoint: 'cdr_ingest',
},
}
);
if (response.status === 202) {
accepted.add(1);
} else if (response.status === 429) {
backpressure.add(1);
} else if (response.status === 503) {
unavailable.add(1);
} else {
unexpected.add(1);
}
check(response, {
'accepted or gracefully rejected':
(r) =>
r.status === 202 ||
r.status === 429,
});
}