Python 68.8%
TypeScript 18.6%
CSS 8.7%
JavaScript 3.3%
HTML 0.6%
1# -----------------------------------------------------------------------------2# Rent-Ka — Rental listings aggregator (Canada, outside Québec)3# Author: Simon-Pierre Boucher — contact@spboucher.ai4# test_kascores.py : barème des KA Scores — décroissance, bornes, labels,5# non-aberration spatiale (deux immeubles voisins → scores proches).6# -----------------------------------------------------------------------------7import json8import math910import pytest1112from rentka import kascores13from rentka.kascores import (14 Grid, TileIndex, _decroissance, label, score_calme, score_global,15 score_services, score_transit, score_walk,16)171819def test_decroissance():20 assert _decroissance(100, 400, 1600) == 1.021 assert _decroissance(400, 400, 1600) == 1.022 assert _decroissance(1600, 400, 1600) == 0.023 assert _decroissance(2500, 400, 1600) == 0.024 assert 0.49 < _decroissance(1000, 400, 1600) < 0.51252627def test_labels():28 assert label(92) == "Exceptionnel"29 assert label(71) == "Excellent"30 assert label(60) == "Très bon"31 assert label(45) == "Moyen"32 assert label(10) == "Faible"33 assert label(None) is None343536def _env_urbain(lat=45.52, lng=-73.58):37 """Micro-quartier synthétique : tout à ~200 m."""38 d = 0.002 # ~200 m39 pois = {cat: [[lat + d, lng]] for cat, *_ in kascores.WALK_BAREME}40 pois["bus"] = [[lat, lng + d]]41 pois["metro"] = [[lat - d, lng]]42 pois["hopital"] = [[lat + 2 * d, lng]]43 return {"lines": {}, "points": {}, "pois": pois}444546def test_walk_urbain_vs_desert():47 idx = TileIndex(_env_urbain())48 s, det = score_walk(idx, 45.52, -73.58)49 assert s is not None and s > 8550 vide = TileIndex({"lines": {}, "points": {}, "pois": {}})51 s2, det2 = score_walk(vide, 45.52, -73.58)52 assert s2 is None and "raison" in det2 # honnêteté : pas de 0 trompeur535455def test_transit_non_desservi_et_blend():56 vide = TileIndex({"lines": {}, "points": {}, "pois": {}})57 s, det = score_transit(vide, 45.52, -73.58, None)58 assert s is None and "raison" in det # « Non desservi », pas 059 idx = TileIndex(_env_urbain())60 proche, _ = score_transit(idx, 45.52, -73.58, 90.0)61 loin, _ = score_transit(idx, 45.52, -73.58, 10.0)62 assert proche is not None and loin is not None and proche > loin636465def test_calme_autoroute_penalise():66 lat, lng = 45.52, -73.5867 calme_env = {"lines": {}, "points": {}, "pois": {}}68 bruyant_env = {69 "lines": {"autoroute": [[[lat + 0.0005, lng - 0.01],70 [lat + 0.0005, lng + 0.01]]]},71 "points": {}, "pois": {},72 }73 s_calme, _ = score_calme(TileIndex(calme_env), lat, lng)74 s_bruyant, d = score_calme(TileIndex(bruyant_env), lat, lng)75 assert s_calme - s_bruyant > 2576 assert any("autoroute" in x["source"] for x in d["sources_bruit"])777879def test_services_rendement_decroissant():80 lat, lng = 45.52, -73.5881 def env(n):82 return {"lines": {}, "points": {}, "pois": {83 "epicerie": [[lat + 0.001 * i, lng] for i in range(1, n + 1)]}}84 s3, _ = score_services(TileIndex(env(3)), lat, lng)85 s6, _ = score_services(TileIndex(env(6)), lat, lng)86 s12, _ = score_services(TileIndex(env(12)), lat, lng)87 assert s3 < s6 < s1288 assert (s6 - s3) > (s12 - s6) # passer de 3→6 vaut plus que 6→12... x2899091def test_global_renormalise_sans_transit():92 s = score_global({"walk": 80, "transit": None, "bike": 60,93 "calme": 70, "services": 50})94 assert s is not None and 60 < s < 7595 assert score_global({"walk": 80, "transit": None, "bike": None,96 "calme": None, "services": None}) is None979899def test_coherence_spatiale_sur_le_parc():100 """Deux immeubles à < 200 m → scores globaux proches (médiane < 8 pts)."""101 from rentka import db102 con = db.connect()103 rows = con.execute(104 "SELECT lat, lng, global AS g FROM kascores"105 " WHERE global IS NOT NULL ORDER BY coord_key LIMIT 3000").fetchall()106 con.close()107 if len(rows) < 200:108 pytest.skip("pas assez de scores calculés")109 g = Grid()110 vals = {}111 for r in rows:112 g.add(r["lat"], r["lng"])113 vals[(round(r["lat"], 5), round(r["lng"], 5))] = r["g"]114 diffs = []115 for r in rows[:800]:116 for d, la, ln in g.near(r["lat"], r["lng"], 200):117 if d < 1:118 continue119 v = vals.get((round(la, 5), round(ln, 5)))120 if v is not None:121 diffs.append(abs(v - r["g"]))122 assert diffs, "aucune paire voisine trouvée"123 diffs.sort()124 mediane = diffs[len(diffs) // 2]125 assert mediane < 8, f"médiane des écarts voisins : {mediane}"126