summaryrefslogtreecommitdiffstats
path: root/tests
diff options
context:
space:
mode:
Diffstat (limited to 'tests')
-rw-r--r--tests/abi_probe.S102
-rwxr-xr-xtests/run.sh32
-rwxr-xr-xtests/server.py222
-rw-r--r--tests/test_cepis.c721
4 files changed, 1077 insertions, 0 deletions
diff --git a/tests/abi_probe.S b/tests/abi_probe.S
new file mode 100644
index 0000000..0e07ec4
--- /dev/null
+++ b/tests/abi_probe.S
@@ -0,0 +1,102 @@
+/*
+ * cepis_abi_probe(fn, a0, a1, a2, a3)
+ *
+ * Calls fn(a0, a1, a2, a3) with every callee-saved register holding a
+ * distinctive sentinel and returns a bitmask of the ones it came back
+ * clobbered, plus bits for a moved stack pointer and a left-over direction
+ * flag. 0 means the callee honoured the System V AMD64 ABI.
+ *
+ * Not part of the library: this file is only linked into the test driver.
+ */
+
+ .text
+ .globl cepis_abi_probe
+ .type cepis_abi_probe, @function
+ .p2align 4
+cepis_abi_probe:
+ push %rbp
+ mov %rsp, %rbp
+ push %rbx
+ push %r12
+ push %r13
+ push %r14
+ push %r15
+ sub $8, %rsp
+ mov %rbp, cepis__probe_bp(%rip)
+ mov %rsp, cepis__probe_sp(%rip)
+ mov %rdi, %rax
+ mov %rsi, %rdi
+ mov %rdx, %rsi
+ mov %rcx, %rdx
+ mov %r8, %rcx
+ movabs $0x1111111111111111, %rbx
+ movabs $0x2222222222222222, %r12
+ movabs $0x3333333333333333, %r13
+ movabs $0x4444444444444444, %r14
+ movabs $0x5555555555555555, %r15
+ movabs $0x6666666666666666, %rbp
+ call *%rax
+ xor %r8d, %r8d
+ movabs $0x1111111111111111, %r9
+ cmp %r9, %rbx
+ je .Lp_r12
+ or $1, %r8d
+.Lp_r12:
+ movabs $0x2222222222222222, %r9
+ cmp %r9, %r12
+ je .Lp_r13
+ or $2, %r8d
+.Lp_r13:
+ movabs $0x3333333333333333, %r9
+ cmp %r9, %r13
+ je .Lp_r14
+ or $4, %r8d
+.Lp_r14:
+ movabs $0x4444444444444444, %r9
+ cmp %r9, %r14
+ je .Lp_r15
+ or $8, %r8d
+.Lp_r15:
+ movabs $0x5555555555555555, %r9
+ cmp %r9, %r15
+ je .Lp_rbp
+ or $16, %r8d
+.Lp_rbp:
+ movabs $0x6666666666666666, %r9
+ cmp %r9, %rbp
+ je .Lp_sp
+ or $32, %r8d
+.Lp_sp:
+ mov cepis__probe_sp(%rip), %r9
+ cmp %r9, %rsp
+ je .Lp_df
+ or $64, %r8d
+ mov %r9, %rsp
+.Lp_df:
+ pushfq
+ pop %r9
+ test $0x400, %r9d
+ jz .Lp_done
+ or $128, %r8d
+ cld
+.Lp_done:
+ mov cepis__probe_bp(%rip), %rbp
+ mov %r8d, %eax
+ lea -40(%rbp), %rsp
+ pop %r15
+ pop %r14
+ pop %r13
+ pop %r12
+ pop %rbx
+ pop %rbp
+ ret
+ .size cepis_abi_probe, . - cepis_abi_probe
+
+ .data
+ .p2align 3
+cepis__probe_bp:
+ .quad 0
+cepis__probe_sp:
+ .quad 0
+
+ .section .note.GNU-stack,"",@progbits
diff --git a/tests/run.sh b/tests/run.sh
new file mode 100755
index 0000000..52e50d2
--- /dev/null
+++ b/tests/run.sh
@@ -0,0 +1,32 @@
+#!/bin/sh
+# Bring the local API stand-in up, run the driver against it, tear it down.
+# Exits non-zero when anything fails. $1 is the driver, the rest is a prefix
+# to run it under (valgrind, gdb, ...).
+
+set -eu
+
+here=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
+driver=$1
+shift
+
+fifo=$(mktemp -u)
+mkfifo "$fifo"
+
+python3 "$here/server.py" 0 >"$fifo" &
+server=$!
+
+cleanup() {
+ kill "$server" 2>/dev/null || true
+ wait "$server" 2>/dev/null || true
+ rm -f "$fifo"
+}
+trap cleanup EXIT INT TERM
+
+port=$(head -n 1 "$fifo")
+if [ -z "$port" ]; then
+ echo "server did not start" >&2
+ exit 1
+fi
+
+echo "server on 127.0.0.1:$port"
+"$@" "$driver" "http://127.0.0.1:$port"
diff --git a/tests/server.py b/tests/server.py
new file mode 100755
index 0000000..c77d7f6
--- /dev/null
+++ b/tests/server.py
@@ -0,0 +1,222 @@
+#!/usr/bin/env python3
+"""Local stand-in for the cep.is API, used by the test driver.
+
+Behaviour is selected by the first path segment, so a test points the client
+at a different ``base_url`` to get a different server: ``/fail2`` fails twice
+then answers, ``/always503`` never answers, and so on. Every prefix counts the
+requests it saw; ``/_stats/<prefix>/health`` reports that count in the ``ceps``
+field so the C driver can assert on it without speaking HTTP itself.
+"""
+
+import json
+import sys
+import threading
+from datetime import datetime, timedelta, timezone
+from email.utils import format_datetime
+from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
+from urllib.parse import parse_qs, urlparse
+
+COLUMNS = [
+ "cep", "logradouro", "complemento", "unidade", "bairro", "localidade",
+ "uf", "estado", "regiao", "ibge", "gia", "ddd", "siafi",
+]
+
+DB = {
+ "01001000": {
+ "cep": "01001000", "logradouro": "Praça da Sé",
+ "complemento": "lado ímpar", "unidade": "", "bairro": "Sé",
+ "localidade": "São Paulo", "uf": "SP", "estado": "São Paulo",
+ "regiao": "Sudeste", "ibge": "3550308", "gia": "1004", "ddd": "11",
+ "siafi": "7107",
+ "geo": {"lat": -23.567202, "lon": -46.649659, "raio_m": 1084,
+ "precisao": "logradouro"},
+ },
+ "20040020": {
+ "cep": "20040020", "logradouro": "Avenida Rio Branco",
+ "complemento": "", "unidade": "", "bairro": "Centro",
+ "localidade": "Rio de Janeiro", "uf": "RJ",
+ "estado": "Rio de Janeiro", "regiao": "Sudeste", "ibge": "3304557",
+ "gia": "", "ddd": "21", "siafi": "6001",
+ },
+ "69900000": {
+ "cep": "69900000", "logradouro": "", "complemento": "", "unidade": "",
+ "bairro": "", "localidade": "Rio Branco", "uf": "AC",
+ "estado": "Acre", "regiao": "Norte", "ibge": "1200401", "gia": "",
+ "ddd": "68", "siafi": "0605",
+ "geo": {"lat": -9.97499, "lon": -67.8243, "raio_m": 18420,
+ "precisao": "localidade"},
+ },
+ "12345678": {
+ "cep": "12345678", "logradouro": 'Rua A, B "quoted"',
+ "complemento": "até 100", "unidade": "", "bairro": "Vila, Nova",
+ "localidade": "Campinas", "uf": "SP", "estado": "São Paulo",
+ "regiao": "Sudeste", "ibge": "3509502", "gia": "2446", "ddd": "19",
+ "siafi": "6291",
+ },
+}
+
+for _n in range(600):
+ _c = "%08d" % (70000000 + _n)
+ DB[_c] = {
+ "cep": _c, "logradouro": "Rua %d" % _n, "complemento": "",
+ "unidade": "", "bairro": "Centro", "localidade": "Cidade %d" % _n,
+ "uf": "SP", "estado": "São Paulo", "regiao": "Sudeste",
+ "ibge": "3550308", "gia": "", "ddd": "11", "siafi": "7107",
+ }
+
+HITS = {}
+LOCK = threading.Lock()
+
+
+def bump(prefix):
+ with LOCK:
+ HITS[prefix] = HITS.get(prefix, 0) + 1
+ return HITS[prefix]
+
+
+def csv_escape(value):
+ if any(c in value for c in ',"\r\n'):
+ return '"' + value.replace('"', '""') + '"'
+ return value
+
+
+def csv_row(cep):
+ entry = DB.get(cep)
+ if entry is None:
+ return ",".join([cep] + [""] * (len(COLUMNS) - 1))
+ return ",".join(csv_escape(entry.get(c, "")) for c in COLUMNS)
+
+
+class Handler(BaseHTTPRequestHandler):
+ protocol_version = "HTTP/1.1"
+
+ def log_message(self, fmt, *args):
+ pass
+
+ def send(self, code, body, ctype="application/json; charset=utf-8",
+ headers=None):
+ raw = body.encode("utf-8") if isinstance(body, str) else body
+ self.send_response(code)
+ self.send_header("Content-Type", ctype)
+ self.send_header("Content-Length", str(len(raw)))
+ for key, value in (headers or {}).items():
+ self.send_header(key, value)
+ self.end_headers()
+ self.wfile.write(raw)
+
+ def do_GET(self):
+ parsed = urlparse(self.path)
+ parts = [p for p in parsed.path.split("/") if p]
+
+ if parts and parts[0] == "_stats":
+ name = parts[1] if len(parts) > 1 else ""
+ with LOCK:
+ count = HITS.get(name, 0)
+ self.send(200, json.dumps({
+ "status": "ok", "dne": "test", "ceps": count, "geo": 0,
+ "base_gerada_em": "2026-07-24T03:48:28Z",
+ }))
+ return
+
+ prefix = ""
+ if parts and not parts[0].isdigit() and parts[0] not in ("csv", "health"):
+ prefix = parts.pop(0)
+ count = bump(prefix)
+
+ if prefix == "always503":
+ self.send(503, '{"erro":"interno","mensagem":"falha interna"}')
+ return
+ if prefix == "always429":
+ self.send(429, '{"erro":"limite","mensagem":"devagar"}',
+ headers={"Retry-After": "1"})
+ return
+ if prefix == "date429":
+ when = datetime.now(timezone.utc) + timedelta(seconds=2)
+ self.send(429, '{"erro":"limite","mensagem":"devagar"}',
+ headers={"Retry-After": format_datetime(when,
+ usegmt=True)})
+ return
+ if prefix == "hugeretry":
+ self.send(429, '{"erro":"limite","mensagem":"devagar"}',
+ headers={"Retry-After": "3600"})
+ return
+ if prefix == "fail2" and count <= 2:
+ self.send(503, '{"erro":"interno","mensagem":"falha interna"}')
+ return
+ if prefix == "flaky" and count == 1:
+ self.send(500, '{"erro":"interno","mensagem":"falha interna"}')
+ return
+ if prefix == "always400":
+ self.send(400, '{"erro":"cep_invalido",'
+ '"mensagem":"CEP deve ter 8 dígitos"}')
+ return
+ if prefix == "badjson":
+ self.send(200, "<html>captive portal</html>", ctype="text/html")
+ return
+ if prefix == "badgeo":
+ entry = dict(DB["01001000"])
+ entry["geo"] = {"lat": "nao-e-numero", "precisao": "logradouro"}
+ entry["campo_novo"] = {"nested": [1, 2, 3]}
+ self.send(200, json.dumps(entry, ensure_ascii=False))
+ return
+ if prefix == "surrogate":
+ entry = dict(DB["01001000"])
+ entry["logradouro"] = "Rua \U0001F600 Feliz"
+ self.send(200, json.dumps(entry, ensure_ascii=True))
+ return
+ if prefix == "escaped":
+ self.send(200, json.dumps(DB["01001000"], ensure_ascii=True))
+ return
+
+ if parts and parts[0] == "health":
+ body = {"status": "ok", "dne": "26071", "ceps": 1596168,
+ "geo": 894489, "base_gerada_em": "2026-07-24T03:48:28Z"}
+ if prefix == "degraded":
+ body["status"] = "degraded"
+ self.send(503, json.dumps(body))
+ return
+ self.send(200, json.dumps(body))
+ return
+
+ if parts and parts[0] == "csv":
+ query = parse_qs(parsed.query)
+ ceps = [c for c in query.get("ceps", [""])[0].split(",") if c]
+ if len(ceps) > 500:
+ self.send(400, '{"erro":"limite","mensagem":"máximo 500"}')
+ return
+ if prefix == "short":
+ ceps = ceps[:1]
+ rows = [",".join(COLUMNS)] + [csv_row(c) for c in ceps]
+ body = "\r\n".join(rows) + "\r\n"
+ if prefix == "bom":
+ body = "\ufeff" + body
+ self.send(200, body, ctype="text/csv; charset=utf-8")
+ return
+
+ if not parts:
+ self.send(404, '{"erro":"cep_nao_encontrado",'
+ '"mensagem":"CEP não existe na base do DNE"}')
+ return
+
+ cep = parts[0]
+ if not (len(cep) == 8 and cep.isdigit()):
+ self.send(400, '{"erro":"cep_invalido",'
+ '"mensagem":"CEP deve ter 8 dígitos"}')
+ return
+ entry = DB.get(cep)
+ if entry is None:
+ self.send(404, '{"erro":"cep_nao_encontrado",'
+ '"mensagem":"CEP não existe na base do DNE"}')
+ return
+ self.send(200, json.dumps(entry, ensure_ascii=False))
+
+
+def main():
+ port = int(sys.argv[1]) if len(sys.argv) > 1 else 0
+ server = ThreadingHTTPServer(("127.0.0.1", port), Handler)
+ print(server.server_address[1], flush=True)
+ server.serve_forever()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/tests/test_cepis.c b/tests/test_cepis.c
new file mode 100644
index 0000000..3dd6dc6
--- /dev/null
+++ b/tests/test_cepis.c
@@ -0,0 +1,721 @@
+/*
+ * Test driver. Every case runs against the local server in tests/server.py,
+ * reached through base_url; nothing here mocks the HTTP layer.
+ *
+ * Usage: test_cepis http://127.0.0.1:<port>
+ */
+
+#define _POSIX_C_SOURCE 200809L
+
+#include <stdarg.h>
+#include <stddef.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <time.h>
+
+#include "cepis.h"
+
+_Static_assert(sizeof(cepis_geo) == 32, "cepis_geo layout");
+_Static_assert(offsetof(cepis_geo, precisao) == 24, "cepis_geo layout");
+_Static_assert(sizeof(cepis_address) == 120, "cepis_address layout");
+_Static_assert(offsetof(cepis_address, siafi) == 96, "cepis_address layout");
+_Static_assert(offsetof(cepis_address, geo) == 104, "cepis_address layout");
+_Static_assert(offsetof(cepis_address, raw) == 112, "cepis_address layout");
+_Static_assert(sizeof(struct cepis_health) == 48, "cepis_health layout");
+_Static_assert(offsetof(struct cepis_health, base_gerada_em) == 32,
+ "cepis_health layout");
+_Static_assert(sizeof(cepis_error) == 272, "cepis_error layout");
+_Static_assert(offsetof(cepis_error, retry_after) == 8, "cepis_error layout");
+_Static_assert(offsetof(cepis_error, message) == 16, "cepis_error layout");
+_Static_assert(sizeof(cepis_config) == 56, "cepis_config layout");
+_Static_assert(offsetof(cepis_config, max_attempts) == 16,
+ "cepis_config layout");
+_Static_assert(offsetof(cepis_config, user_agent) == 48, "cepis_config layout");
+
+static const char *base;
+static int failures;
+static int checks;
+static const char *current;
+
+#define CHECK(cond) \
+ do { \
+ checks++; \
+ if (!(cond)) { \
+ failures++; \
+ fprintf(stderr, " FAIL %s:%d [%s]: %s\n", __FILE__, __LINE__, \
+ current, #cond); \
+ } \
+ } while (0)
+
+#define CHECK_STR(got, want) \
+ do { \
+ checks++; \
+ const char *g_ = (got); \
+ if (g_ == NULL || strcmp(g_, (want)) != 0) { \
+ failures++; \
+ fprintf(stderr, " FAIL %s:%d [%s]: want \"%s\" got \"%s\"\n", \
+ __FILE__, __LINE__, current, (want), \
+ g_ ? g_ : "(null)"); \
+ } \
+ } while (0)
+
+static void begin(const char *name)
+{
+ current = name;
+ printf("- %s\n", name);
+}
+
+static char *urlf(const char *fmt, ...)
+{
+ static char buf[8][512];
+ static int slot;
+ char *out = buf[slot++ % 8];
+ va_list ap;
+ va_start(ap, fmt);
+ vsnprintf(out, 512, fmt, ap);
+ va_end(ap);
+ return out;
+}
+
+static cepis_client *client_at(const char *url, int attempts, double backoff)
+{
+ cepis_config cfg;
+ cepis_client *client = NULL;
+
+ memset(&cfg, 0, sizeof cfg);
+ cfg.base_url = url;
+ cfg.max_attempts = attempts;
+ cfg.backoff_base = backoff;
+ cfg.backoff_max = backoff * 4.0;
+ cfg.timeout = 5.0;
+ if (cepis_client_new(&cfg, &client) != CEPIS_OK)
+ return NULL;
+ return client;
+}
+
+static long hits(const char *prefix)
+{
+ cepis_client *client;
+
+ client = client_at(urlf("%s/_stats/%s", base, prefix), 1, 0.001);
+ struct cepis_health *health = NULL;
+ long count = -1;
+
+ if (client == NULL)
+ return -1;
+ if (cepis_health(client, &health, NULL) == CEPIS_OK && health != NULL)
+ count = (long)health->ceps;
+ cepis_health_free(health);
+ cepis_client_free(client);
+ return count;
+}
+
+static double now_seconds(void)
+{
+ struct timespec ts;
+ clock_gettime(CLOCK_MONOTONIC, &ts);
+ return (double)ts.tv_sec + (double)ts.tv_nsec / 1e9;
+}
+
+static void test_normalize(void)
+{
+ char out[16];
+
+ begin("normalize_cep");
+ CHECK(cepis_normalize_cep("01001-000", out) == CEPIS_OK);
+ CHECK_STR(out, "01001000");
+ CHECK(cepis_normalize_cep("01001000", out) == CEPIS_OK);
+ CHECK_STR(out, "01001000");
+ CHECK(cepis_normalize_cep(" 01001-000 ", out) == CEPIS_OK);
+ CHECK_STR(out, "01001000");
+ CHECK(cepis_normalize_cep("01.001-000", out) == CEPIS_OK);
+ CHECK_STR(out, "01001000");
+ CHECK(cepis_normalize_cep("0100100", out) == CEPIS_ERR_INVALID_CEP);
+ CHECK_STR(out, "");
+ CHECK(cepis_normalize_cep("010010000", out) == CEPIS_ERR_INVALID_CEP);
+ CHECK(cepis_normalize_cep("0100100a", out) == CEPIS_ERR_INVALID_CEP);
+ CHECK(cepis_normalize_cep("", out) == CEPIS_ERR_INVALID_CEP);
+ CHECK(cepis_normalize_cep(NULL, out) == CEPIS_ERR_INVALID_CEP);
+ CHECK(cepis_normalize_cep("01001000", NULL) == CEPIS_ERR_USAGE);
+}
+
+static void test_lookup_geo(void)
+{
+ cepis_client *client = client_at(base, 3, 0.001);
+ cepis_address *address = NULL;
+ cepis_error err;
+ char masked[16];
+
+ begin("lookup with geo");
+ CHECK(cepis_lookup(client, "01001-000", &address, &err) == CEPIS_OK);
+ if (address != NULL) {
+ CHECK_STR(address->cep, "01001000");
+ CHECK_STR(address->logradouro, "Praça da Sé");
+ CHECK_STR(address->complemento, "lado ímpar");
+ CHECK_STR(address->unidade, "");
+ CHECK_STR(address->bairro, "Sé");
+ CHECK_STR(address->localidade, "São Paulo");
+ CHECK_STR(address->uf, "SP");
+ CHECK_STR(address->estado, "São Paulo");
+ CHECK_STR(address->regiao, "Sudeste");
+ CHECK_STR(address->ibge, "3550308");
+ CHECK_STR(address->gia, "1004");
+ CHECK_STR(address->ddd, "11");
+ CHECK_STR(address->siafi, "7107");
+ CHECK(address->raw != NULL);
+ CHECK(strstr(address->raw, "logradouro") != NULL);
+ CHECK(address->geo != NULL);
+ if (address->geo != NULL) {
+ CHECK(address->geo->lat < -23.5 && address->geo->lat > -23.6);
+ CHECK(address->geo->lon < -46.6 && address->geo->lon > -46.7);
+ CHECK(address->geo->raio_m == 1084);
+ CHECK_STR(address->geo->precisao, "logradouro");
+ CHECK(cepis_geo_is_precise(address->geo) == 1);
+ }
+ cepis_address_formatted_cep(address, masked);
+ CHECK_STR(masked, "01001-000");
+ }
+ cepis_address_free(address);
+
+ address = NULL;
+ CHECK(cepis_lookup(client, "69900000", &address, &err) == CEPIS_OK);
+ if (address != NULL) {
+ CHECK(address->geo != NULL);
+ if (address->geo != NULL) {
+ CHECK_STR(address->geo->precisao, "localidade");
+ CHECK(cepis_geo_is_precise(address->geo) == 0);
+ CHECK(address->geo->raio_m == 18420);
+ }
+ }
+ cepis_address_free(address);
+ cepis_client_free(client);
+}
+
+static void test_lookup_no_geo(void)
+{
+ cepis_client *client = client_at(base, 3, 0.001);
+ cepis_address *address = NULL;
+
+ begin("lookup without geo");
+ CHECK(cepis_lookup(client, "20040020", &address, NULL) == CEPIS_OK);
+ if (address != NULL) {
+ CHECK(address->geo == NULL);
+ CHECK_STR(address->localidade, "Rio de Janeiro");
+ CHECK_STR(address->gia, "");
+ }
+ cepis_address_free(address);
+ cepis_client_free(client);
+}
+
+static void test_escaped_json(void)
+{
+ cepis_client *client = client_at(urlf("%s/escaped", base), 1, 0.001);
+ cepis_address *address = NULL;
+
+ begin("\\u escapes decoded to UTF-8");
+ CHECK(cepis_lookup(client, "01001000", &address, NULL) == CEPIS_OK);
+ if (address != NULL) {
+ CHECK_STR(address->logradouro, "Praça da Sé");
+ CHECK_STR(address->localidade, "São Paulo");
+ }
+ cepis_address_free(address);
+ cepis_client_free(client);
+}
+
+static void test_surrogate_pair(void)
+{
+ cepis_client *client = client_at(urlf("%s/surrogate", base), 1, 0.001);
+ cepis_address *address = NULL;
+
+ begin("a surrogate pair decodes to one UTF-8 sequence");
+ CHECK(cepis_lookup(client, "01001000", &address, NULL) == CEPIS_OK);
+ if (address != NULL)
+ CHECK_STR(address->logradouro, "Rua \xf0\x9f\x98\x80 Feliz");
+ cepis_address_free(address);
+ cepis_client_free(client);
+}
+
+static void test_csv_bom(void)
+{
+ const char *ceps[] = {"01001000", "20040020"};
+ cepis_client *client = client_at(urlf("%s/bom", base), 1, 0.001);
+ cepis_address **out = NULL;
+
+ begin("a UTF-8 BOM on the CSV is stripped");
+ CHECK(cepis_lookup_many(client, ceps, 2, &out, NULL) == CEPIS_OK);
+ if (out != NULL) {
+ CHECK(out[0] != NULL);
+ CHECK(out[1] != NULL);
+ if (out[0] != NULL)
+ CHECK_STR(out[0]->cep, "01001000");
+ }
+ cepis_address_list_free(out, 2);
+ cepis_client_free(client);
+}
+
+static void test_not_found_not_retried(void)
+{
+ cepis_client *client = client_at(urlf("%s/nf", base), 3, 0.001);
+ cepis_address *address = NULL;
+ cepis_error err;
+
+ begin("404 is not retried");
+ CHECK(cepis_lookup(client, "99999999", &address, &err) ==
+ CEPIS_ERR_NOT_FOUND);
+ CHECK(address == NULL);
+ CHECK(err.status == CEPIS_ERR_NOT_FOUND);
+ CHECK(strstr(err.message, "99999999") != NULL);
+ CHECK(hits("nf") == 1);
+ cepis_client_free(client);
+}
+
+static void test_bad_cep_from_api(void)
+{
+ cepis_client *client = client_at(urlf("%s/always400", base), 3, 0.001);
+ cepis_address *address = NULL;
+ cepis_error err;
+
+ begin("400 is not retried and carries the API message");
+ CHECK(cepis_lookup(client, "00000000", &address, &err) ==
+ CEPIS_ERR_INVALID_CEP);
+ CHECK(address == NULL);
+ CHECK(strstr(err.message, "8 d") != NULL);
+ CHECK(hits("always400") == 1);
+ cepis_address_free(address);
+ cepis_client_free(client);
+}
+
+static void test_local_validation_makes_no_request(void)
+{
+ cepis_client *client = client_at(urlf("%s/novalid", base), 3, 0.001);
+ cepis_address *address = NULL;
+ cepis_error err;
+
+ begin("local validation makes no request");
+ CHECK(cepis_lookup(client, "123", &address, &err) == CEPIS_ERR_INVALID_CEP);
+ CHECK(address == NULL);
+ CHECK(strstr(err.message, "8 digits") != NULL);
+ CHECK(hits("novalid") == 0);
+ cepis_client_free(client);
+}
+
+static void test_retry_then_success(void)
+{
+ cepis_client *client = client_at(urlf("%s/fail2", base), 3, 0.001);
+ cepis_address *address = NULL;
+ cepis_error err;
+
+ begin("retry then success");
+ CHECK(cepis_lookup(client, "01001000", &address, &err) == CEPIS_OK);
+ CHECK(address != NULL);
+ CHECK(hits("fail2") == 3);
+ cepis_address_free(address);
+ cepis_client_free(client);
+}
+
+static void test_retry_exhausted(void)
+{
+ cepis_client *client = client_at(urlf("%s/always503", base), 2, 0.001);
+ cepis_address *address = NULL;
+ cepis_error err;
+
+ begin("retry exhausted on 5xx");
+ CHECK(cepis_lookup(client, "01001000", &address, &err) == CEPIS_ERR_SERVER);
+ CHECK(address == NULL);
+ CHECK(err.status == CEPIS_ERR_SERVER);
+ CHECK(err.http_status == 503);
+ CHECK(strstr(err.message, "503") != NULL);
+ CHECK(hits("always503") == 2);
+ cepis_client_free(client);
+}
+
+static void test_rate_limited(void)
+{
+ cepis_client *client = client_at(urlf("%s/always429", base), 2, 0.001);
+ cepis_address *address = NULL;
+ cepis_error err;
+
+ begin("429 exposes Retry-After in seconds");
+ CHECK(cepis_lookup(client, "01001000", &address, &err) ==
+ CEPIS_ERR_RATE_LIMITED);
+ CHECK(address == NULL);
+ CHECK(err.http_status == 429);
+ CHECK(err.retry_after > 0.5 && err.retry_after < 1.5);
+ CHECK(hits("always429") == 2);
+ cepis_client_free(client);
+}
+
+static void test_rate_limited_http_date(void)
+{
+ cepis_client *client = client_at(urlf("%s/date429", base), 2, 0.001);
+ cepis_address *address = NULL;
+ cepis_error err;
+
+ begin("429 with an HTTP-date Retry-After");
+ CHECK(cepis_lookup(client, "01001000", &address, &err) ==
+ CEPIS_ERR_RATE_LIMITED);
+ CHECK(err.retry_after > 0.0 && err.retry_after <= 3.0);
+ CHECK(hits("date429") == 2);
+ cepis_client_free(client);
+}
+
+static void test_retry_after_is_capped(void)
+{
+ cepis_client *client = client_at(urlf("%s/hugeretry", base), 2, 0.01);
+ cepis_address *address = NULL;
+ cepis_error err;
+ double started = now_seconds();
+
+ begin("a huge Retry-After is capped at backoff_max");
+ CHECK(cepis_lookup(client, "01001000", &address, &err) ==
+ CEPIS_ERR_RATE_LIMITED);
+ CHECK(now_seconds() - started < 5.0);
+ CHECK(err.retry_after > 3000.0);
+ cepis_client_free(client);
+}
+
+static void test_total_timeout(void)
+{
+ cepis_config cfg;
+ cepis_client *client = NULL;
+ cepis_address *address = NULL;
+ cepis_error err;
+ double started;
+
+ begin("total_timeout bounds the whole call");
+ memset(&cfg, 0, sizeof cfg);
+ cfg.base_url = urlf("%s/always503", base);
+ cfg.max_attempts = 50;
+ cfg.backoff_base = 0.05;
+ cfg.backoff_max = 0.05;
+ cfg.total_timeout = 0.4;
+ CHECK(cepis_client_new(&cfg, &client) == CEPIS_OK);
+ started = now_seconds();
+ CHECK(cepis_lookup(client, "01001000", &address, &err) == CEPIS_ERR_SERVER);
+ CHECK(now_seconds() - started < 4.0);
+ cepis_client_free(client);
+}
+
+static void test_invalid_response(void)
+{
+ cepis_client *client = client_at(urlf("%s/badjson", base), 1, 0.001);
+ cepis_address *address = NULL;
+ cepis_error err;
+
+ begin("a 200 with an unparseable body");
+ CHECK(cepis_lookup(client, "01001000", &address, &err) ==
+ CEPIS_ERR_INVALID_RESPONSE);
+ CHECK(address == NULL);
+ cepis_client_free(client);
+}
+
+static void test_bad_geo_kept_address(void)
+{
+ cepis_client *client = client_at(urlf("%s/badgeo", base), 1, 0.001);
+ cepis_address *address = NULL;
+
+ begin("a malformed geo drops the geo, not the address");
+ CHECK(cepis_lookup(client, "01001000", &address, NULL) == CEPIS_OK);
+ if (address != NULL) {
+ CHECK(address->geo == NULL);
+ CHECK_STR(address->localidade, "São Paulo");
+ CHECK(strstr(address->raw, "campo_novo") != NULL);
+ }
+ cepis_address_free(address);
+ cepis_client_free(client);
+}
+
+static void test_network_error(void)
+{
+ cepis_client *client = client_at("http://127.0.0.1:1", 2, 0.001);
+ cepis_address *address = NULL;
+ cepis_error err;
+
+ begin("a refused connection is a network error");
+ CHECK(cepis_lookup(client, "01001000", &address, &err) ==
+ CEPIS_ERR_NETWORK);
+ CHECK(address == NULL);
+ CHECK(err.message[0] != '\0');
+ cepis_client_free(client);
+}
+
+static void test_health(void)
+{
+ cepis_client *client = client_at(base, 2, 0.001);
+ struct cepis_health *health = NULL;
+
+ begin("health");
+ CHECK(cepis_health(client, &health, NULL) == CEPIS_OK);
+ if (health != NULL) {
+ CHECK_STR(health->status, "ok");
+ CHECK_STR(health->dne, "26071");
+ CHECK(health->ceps == 1596168);
+ CHECK(health->geo == 894489);
+ CHECK_STR(health->base_gerada_em, "2026-07-24T03:48:28Z");
+ CHECK(health->raw != NULL);
+ }
+ cepis_health_free(health);
+ cepis_client_free(client);
+}
+
+static void test_health_degraded(void)
+{
+ cepis_client *client = client_at(urlf("%s/degraded", base), 2, 0.001);
+ struct cepis_health *health = NULL;
+
+ begin("a degraded API is not an error");
+ CHECK(cepis_health(client, &health, NULL) == CEPIS_OK);
+ if (health != NULL)
+ CHECK_STR(health->status, "degraded");
+ CHECK(hits("degraded") == 1);
+ cepis_health_free(health);
+ cepis_client_free(client);
+}
+
+static void test_batch_order(void)
+{
+ const char *ceps[] = {"01001000", "99999999", "20040020"};
+ cepis_client *client = client_at(urlf("%s/batch", base), 2, 0.001);
+ cepis_address **out = NULL;
+
+ begin("batch keeps order and nulls the missing entry");
+ CHECK(cepis_lookup_many(client, ceps, 3, &out, NULL) == CEPIS_OK);
+ if (out != NULL) {
+ CHECK(out[0] != NULL);
+ CHECK(out[1] == NULL);
+ CHECK(out[2] != NULL);
+ if (out[0] != NULL) {
+ CHECK_STR(out[0]->localidade, "São Paulo");
+ CHECK(out[0]->geo == NULL);
+ CHECK(out[0]->raw == NULL);
+ }
+ if (out[2] != NULL)
+ CHECK_STR(out[2]->localidade, "Rio de Janeiro");
+ }
+ CHECK(hits("batch") == 1);
+ cepis_address_list_free(out, 3);
+ cepis_client_free(client);
+}
+
+static void test_batch_quoting(void)
+{
+ const char *ceps[] = {"12345678"};
+ cepis_client *client = client_at(base, 2, 0.001);
+ cepis_address **out = NULL;
+
+ begin("batch parses quoted CSV fields");
+ CHECK(cepis_lookup_many(client, ceps, 1, &out, NULL) == CEPIS_OK);
+ if (out != NULL && out[0] != NULL) {
+ CHECK_STR(out[0]->logradouro, "Rua A, B \"quoted\"");
+ CHECK_STR(out[0]->bairro, "Vila, Nova");
+ CHECK_STR(out[0]->complemento, "até 100");
+ }
+ cepis_address_list_free(out, 1);
+ cepis_client_free(client);
+}
+
+static void test_batch_malformed_entry(void)
+{
+ const char *ceps[] = {"01001000", "abc", "20040020"};
+ cepis_client *client = client_at(base, 2, 0.001);
+ cepis_address **out = NULL;
+
+ begin("a malformed CEP in a batch is null, not an error");
+ CHECK(cepis_lookup_many(client, ceps, 3, &out, NULL) == CEPIS_OK);
+ if (out != NULL) {
+ CHECK(out[0] != NULL);
+ CHECK(out[1] == NULL);
+ CHECK(out[2] != NULL);
+ }
+ cepis_address_list_free(out, 3);
+ cepis_client_free(client);
+}
+
+static void test_batch_chunking(void)
+{
+ cepis_client *client = client_at(urlf("%s/chunk", base), 2, 0.001);
+ cepis_address **out = NULL;
+ const char **ceps;
+ char *storage;
+ size_t i;
+ size_t n = 501;
+ int ok = 1;
+
+ begin("batches above 500 are chunked");
+ ceps = malloc(n * sizeof *ceps);
+ storage = malloc(n * 9);
+ for (i = 0; i < n; i++) {
+ snprintf(storage + i * 9, 9, "%08zu", (size_t)70000000 + i);
+ ceps[i] = storage + i * 9;
+ }
+ CHECK(cepis_lookup_many(client, ceps, n, &out, NULL) == CEPIS_OK);
+ if (out != NULL) {
+ for (i = 0; i < n; i++) {
+ if (out[i] == NULL || strcmp(out[i]->cep, ceps[i]) != 0) {
+ ok = 0;
+ break;
+ }
+ }
+ CHECK(ok);
+ }
+ CHECK(hits("chunk") == 2);
+ cepis_address_list_free(out, n);
+ free(storage);
+ free(ceps);
+ cepis_client_free(client);
+}
+
+static void test_batch_short_response(void)
+{
+ const char *ceps[] = {"01001000", "20040020", "12345678"};
+ cepis_client *client = client_at(urlf("%s/short", base), 2, 0.001);
+ cepis_address **out = NULL;
+
+ begin("a short batch response is padded, not shifted");
+ CHECK(cepis_lookup_many(client, ceps, 3, &out, NULL) == CEPIS_OK);
+ if (out != NULL) {
+ CHECK(out[0] != NULL);
+ CHECK(out[1] == NULL);
+ CHECK(out[2] == NULL);
+ if (out[0] != NULL)
+ CHECK_STR(out[0]->cep, "01001000");
+ }
+ cepis_address_list_free(out, 3);
+ cepis_client_free(client);
+}
+
+static void test_batch_empty(void)
+{
+ cepis_client *client = client_at(base, 2, 0.001);
+ cepis_address **out = NULL;
+
+ begin("an empty batch is a valid empty result");
+ CHECK(cepis_lookup_many(client, NULL, 0, &out, NULL) == CEPIS_OK);
+ CHECK(out != NULL);
+ cepis_address_list_free(out, 0);
+ cepis_client_free(client);
+}
+
+static void test_trailing_slash(void)
+{
+ cepis_client *client = client_at(urlf("%s/", base), 2, 0.001);
+ cepis_address *address = NULL;
+
+ begin("a trailing slash in base_url is stripped");
+ CHECK(cepis_lookup(client, "01001000", &address, NULL) == CEPIS_OK);
+ CHECK(address != NULL);
+ cepis_address_free(address);
+ cepis_client_free(client);
+}
+
+int cepis_abi_probe(void *fn, void *a0, void *a1, void *a2, void *a3);
+
+static void probe(const char *name, void *fn, void *a0, void *a1, void *a2,
+ void *a3)
+{
+ int mask = cepis_abi_probe(fn, a0, a1, a2, a3);
+
+ checks++;
+ if (mask != 0) {
+ failures++;
+ fprintf(stderr, " FAIL [%s]: %s violated the ABI, mask 0x%02x\n",
+ current, name, mask);
+ }
+}
+
+static void test_abi(void)
+{
+ cepis_client *client = client_at(base, 2, 0.001);
+ cepis_address *address = NULL;
+ struct cepis_health *health = NULL;
+ cepis_address **many = NULL;
+ cepis_error err;
+ char out[16];
+
+ begin("callee-saved registers, stack pointer and DF survive every call");
+ probe("cepis_normalize_cep", (void *)cepis_normalize_cep,
+ (void *)"01001-000", out, NULL, NULL);
+ probe("cepis_strerror", (void *)cepis_strerror, (void *)(long)4, NULL,
+ NULL, NULL);
+ probe("cepis_lookup(invalid)", (void *)cepis_lookup, client, (void *)"1",
+ &address, &err);
+ probe("cepis_lookup", (void *)cepis_lookup, client, (void *)"01001000",
+ &address, &err);
+ probe("cepis_address_free", (void *)cepis_address_free, address, NULL,
+ NULL, NULL);
+ probe("cepis_health", (void *)cepis_health, client, &health, &err, NULL);
+ probe("cepis_health_free", (void *)cepis_health_free, health, NULL, NULL,
+ NULL);
+ probe("cepis_lookup_many", (void *)cepis_lookup_many, client, NULL, NULL,
+ &many);
+ cepis_address_list_free(many, 0);
+ probe("cepis_default_client", (void *)cepis_default_client, NULL, NULL,
+ NULL, NULL);
+ probe("cepis_client_free", (void *)cepis_client_free, client, NULL, NULL,
+ NULL);
+}
+
+static void test_usage(void)
+{
+ cepis_client *client = client_at(base, 2, 0.001);
+ cepis_config cfg;
+ cepis_client *bad = NULL;
+
+ begin("argument errors");
+ CHECK(cepis_lookup(client, "01001000", NULL, NULL) == CEPIS_ERR_USAGE);
+ CHECK(cepis_health(client, NULL, NULL) == CEPIS_ERR_USAGE);
+ CHECK(cepis_lookup_many(client, NULL, 0, NULL, NULL) == CEPIS_ERR_USAGE);
+ memset(&cfg, 0, sizeof cfg);
+ cfg.max_attempts = -1;
+ CHECK(cepis_client_new(&cfg, &bad) == CEPIS_ERR_USAGE);
+ CHECK(bad == NULL);
+ CHECK(strcmp(cepis_version(), CEPIS_VERSION) == 0);
+ CHECK(strcmp(cepis_strerror(CEPIS_ERR_NOT_FOUND), "CEP not found") == 0);
+ cepis_client_free(client);
+}
+
+int main(int argc, char **argv)
+{
+ if (argc < 2) {
+ fprintf(stderr, "usage: %s <base-url>\n", argv[0]);
+ return 2;
+ }
+ base = argv[1];
+ cepis_global_init();
+
+ test_normalize();
+ test_lookup_geo();
+ test_lookup_no_geo();
+ test_escaped_json();
+ test_surrogate_pair();
+ test_csv_bom();
+ test_not_found_not_retried();
+ test_bad_cep_from_api();
+ test_local_validation_makes_no_request();
+ test_retry_then_success();
+ test_retry_exhausted();
+ test_rate_limited();
+ test_rate_limited_http_date();
+ test_retry_after_is_capped();
+ test_total_timeout();
+ test_invalid_response();
+ test_bad_geo_kept_address();
+ test_network_error();
+ test_health();
+ test_health_degraded();
+ test_batch_order();
+ test_batch_quoting();
+ test_batch_malformed_entry();
+ test_batch_chunking();
+ test_batch_short_response();
+ test_batch_empty();
+ test_trailing_slash();
+ test_abi();
+ test_usage();
+
+ cepis_global_cleanup();
+
+ printf("\n%d checks, %d failures\n", checks, failures);
+ return failures == 0 ? 0 : 1;
+}