Files
meshcore-bot/tests/helpers.py
T
agessaman 0853739f30 fix(weather): review follow-ups: warning units, wx localization, help text
A space bunny review of this branch's commits found:
- gwx's high-wind warning converted the speed to mph for every catalog, so on a km/h bot it read 49 next to a reply saying 80, and Polish (whose template said km/h) regressed in the merge. The threshold stays 30 mph; the warning shows the speed in the configured unit with a {unit} placeholder in every catalog.
- tomorrow_not_available, tomorrow_error, multiday_not_available and multiday_error were still English in seven catalogs; they are translated, and the catalog test now fails on any weather reply left in English.
- wx kept NOAA's English wind letters and English weekday letters while gwx's are translated, so one German bot showed NE and NO for the same wind. wx now translates both through the catalog (commands.wx.day_abbrev added); English output is byte-for-byte unchanged, which the goldens show (their stub translator gives English for these short labels, via tests.helpers.key_name_translate).
- Polish feels_like was ungrammatical (odczuwalna -> odczuwalnie), Russian's used a nonstandard abbreviation, and Russian gwx's MQTT subscriber error lacked wx's hint.
- The help text (commands.wx.description/usage, commands.gwx.usage) described wx as ZIP-only; it now lists cities, coordinates and the options, under 130 bytes in every language.
- The generated default config, the docs' default_state wording, _fit_tomorrow_parts' docstring and the CHANGELOG's stats claim were inaccurate.
2026-10-02 00:48:31 -07:00

183 lines
6.6 KiB
Python

#!/usr/bin/env python3
"""
Test helper functions and factories for creating test data
"""
from datetime import datetime
from typing import Any, Optional
def create_test_repeater(
prefix: str = "01",
name: str = "Test Repeater",
public_key: Optional[str] = None,
latitude: float = 47.6062,
longitude: float = -122.3321,
is_starred: bool = False,
last_heard: Optional[datetime] = None,
last_advert_timestamp: Optional[datetime] = None,
role: str = "repeater"
) -> dict[str, Any]:
"""Factory function to create test repeater data.
Args:
prefix: Two-character hex prefix (default: "01")
name: Repeater name
public_key: Full public key (default: prefix repeated 32 times)
latitude: Latitude coordinate
longitude: Longitude coordinate
is_starred: Whether repeater is starred
last_heard: Last heard timestamp (default: now)
last_advert_timestamp: Last advert timestamp (default: now)
role: Device role (default: "repeater")
Returns:
Dictionary with repeater data matching database schema
"""
if public_key is None:
# Generate a realistic-looking public key from prefix
public_key = (prefix.lower() * 16)[:64] # 64 hex chars = 32 bytes
now = datetime.now()
if last_heard is None:
last_heard = now
if last_advert_timestamp is None:
last_advert_timestamp = now
return {
'name': name,
'public_key': public_key,
'device_type': 'repeater',
'last_seen': last_heard,
'last_heard': last_heard,
'last_advert_timestamp': last_advert_timestamp,
'is_active': True,
'latitude': latitude,
'longitude': longitude,
'city': 'Seattle',
'state': 'WA',
'country': 'USA',
'advert_count': 1,
'signal_strength': None,
'hop_count': 0,
'role': role,
'is_starred': is_starred
}
def create_test_edge(
from_prefix: str,
to_prefix: str,
from_public_key: Optional[str] = None,
to_public_key: Optional[str] = None,
observation_count: int = 1,
first_seen: Optional[datetime] = None,
last_seen: Optional[datetime] = None,
avg_hop_position: Optional[float] = None,
geographic_distance: Optional[float] = None,
prefix_hex_chars: int = 2
) -> dict[str, Any]:
"""Factory function to create test edge data.
Args:
from_prefix: Source node prefix
to_prefix: Destination node prefix
from_public_key: Source public key (default: generated from prefix)
to_public_key: Destination public key (default: generated from prefix)
observation_count: Number of times edge observed
first_seen: First observation time (default: now)
last_seen: Last observation time (default: now)
avg_hop_position: Average hop position in paths
geographic_distance: Distance in km
prefix_hex_chars: Number of hex chars per prefix (default 2). Use bot.prefix_hex_chars when testing with a bot.
Returns:
Dictionary with edge data matching MeshGraph edge structure
"""
now = datetime.now()
if first_seen is None:
first_seen = now
if last_seen is None:
last_seen = now
if from_public_key is None:
from_public_key = (from_prefix.lower() * 16)[:64]
if to_public_key is None:
to_public_key = (to_prefix.lower() * 16)[:64]
return {
'from_prefix': from_prefix.lower()[:prefix_hex_chars],
'to_prefix': to_prefix.lower()[:prefix_hex_chars],
'from_public_key': from_public_key,
'to_public_key': to_public_key,
'observation_count': observation_count,
'first_seen': first_seen,
'last_seen': last_seen,
'avg_hop_position': avg_hop_position,
'geographic_distance': geographic_distance
}
def create_test_path(node_ids: list[str], prefix_hex_chars: int = 2) -> list[str]:
"""Factory function to create test path data.
Args:
node_ids: List of node prefixes in path order
prefix_hex_chars: Number of hex chars per node (default 2). Use bot.prefix_hex_chars when testing with a bot.
Returns:
List of node IDs (normalized to lowercase)
"""
return [node_id.lower()[:prefix_hex_chars] for node_id in node_ids]
def populate_test_graph(mesh_graph, edges: list[dict[str, Any]], prefix_hex_chars: int = 2):
"""Helper to populate a MeshGraph instance with test edges.
Args:
mesh_graph: MeshGraph instance to populate
edges: List of edge dictionaries (from create_test_edge)
prefix_hex_chars: Number of hex chars per prefix (default 2). Must match mesh_graph's bot.prefix_hex_chars when graph uses prefix-based keys.
"""
for edge in edges:
mesh_graph.add_edge(
edge['from_prefix'],
edge['to_prefix'],
from_public_key=edge.get('from_public_key'),
to_public_key=edge.get('to_public_key'),
hop_position=edge.get('avg_hop_position'),
geographic_distance=edge.get('geographic_distance')
)
# Manually set observation_count and timestamps if needed
edge_key = (edge['from_prefix'].lower()[:prefix_hex_chars], edge['to_prefix'].lower()[:prefix_hex_chars])
if edge_key in mesh_graph.edges:
if edge.get('observation_count', 1) > 1:
mesh_graph.edges[edge_key]['observation_count'] = edge['observation_count']
if edge.get('first_seen'):
mesh_graph.edges[edge_key]['first_seen'] = edge['first_seen']
if edge.get('last_seen'):
mesh_graph.edges[edge_key]['last_seen'] = edge['last_seen']
_ENGLISH_CATALOG: dict[str, Any] | None = None
# Short labels inside weather replies; stub translators show them in English so that
# reply lengths (and byte budgets) stay those of a real English bot.
_ENGLISH_LABEL_PREFIXES = ("common.wind_directions.", "commands.wx.day_abbrev.")
def key_name_translate(key: str, **kwargs: Any) -> str:
"""A stub translation: ``<key[kwargs]>``, except short weather labels, which are English."""
global _ENGLISH_CATALOG
if key.startswith(_ENGLISH_LABEL_PREFIXES):
if _ENGLISH_CATALOG is None:
import json
from pathlib import Path
path = Path(__file__).resolve().parents[1] / "translations" / "en.json"
_ENGLISH_CATALOG = json.loads(path.read_text(encoding="utf-8"))
node: Any = _ENGLISH_CATALOG
for part in key.split("."):
node = node[part]
return node
return f"<{key}{sorted(kwargs.items()) or ''}>"