Skip to content
This repository was archived by the owner on Jun 16, 2026. It is now read-only.
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
name: Tests

on:
push:
branches: [main]
pull_request:

jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-python@v5
with:
python-version: "3.12"

- name: Install dependencies
run: pip install requests beautifulsoup4 pytest

- name: Run tests
run: pytest tests/ -v
Comment on lines +18 to +22
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ requests>=2.31,<3
beautifulsoup4>=4.12,<5
cartopy>=0.22,<1
matplotlib>=3.8,<4
pytest>=7,<9
2 changes: 1 addition & 1 deletion scripts/update_gpx.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ def sort_key(wpt_block):
def write_gpx(existing_text, wpt_blocks):
"""Write the GPX file with sorted waypoints, preserving header/footer."""
header = '<?xml version="1.0" encoding="UTF-8" standalone="no"?>\n'
header += '<gpx version="1.1" creator="Tezos Protocols Maps (by Copolycube)">\n'
header += '<gpx version="1.1" creator="Tezos Protocol Map">\n'
footer = "</gpx>\n"

sorted_blocks = sorted(wpt_blocks, key=sort_key)
Expand Down
Empty file added tests/__init__.py
Empty file.
25 changes: 25 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
"""Shared fixtures for the test suite."""

from pathlib import Path

import pytest

FIXTURES = Path(__file__).parent / "fixtures"


@pytest.fixture()
def naming_html():
"""Minimal HTML with protocol list items."""
return (FIXTURES / "naming_page.html").read_text()


@pytest.fixture()
def sample_gpx_text():
"""GPX text with 4 sample waypoints."""
return (FIXTURES / "tezos_sample.gpx").read_text()
Comment on lines +11 to +19


@pytest.fixture()
def sample_gpx_path():
"""Path to the sample GPX fixture."""
return str(FIXTURES / "tezos_sample.gpx")
14 changes: 14 additions & 0 deletions tests/fixtures/naming_page.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<!DOCTYPE html>
<html>
<body>
<ul>
<li>001 Genesis</li>
<li>002 Demo_noops</li>
<li>003 Genesistest</li>
<li>004 Athens</li>
<li>005 Babylon</li>
<li>006 Carthage</li>
<li>007 Delphi</li>
</ul>
</body>
</html>
19 changes: 19 additions & 0 deletions tests/fixtures/tezos_sample.gpx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<gpx version="1.1" creator="Tezos Protocol Map">
<wpt lat="37.9838" lon="23.7275">
<name>Athens, Greece</name>
<desc>Capital of Greece</desc>
</wpt>
<wpt lat="32.5439" lon="44.4208">
<name>Babylon, Iraq</name>
<desc>Ancient city in Iraq</desc>
</wpt>
<wpt lat="36.8508" lon="10.1833">
<name>Carthage, Tunisia</name>
<desc>Ancient city in Tunisia</desc>
</wpt>
<wpt lat="35.6895" lon="139.6917">
<name>Edo (Tokyo), Japan</name>
<desc>Former name of Tokyo, Japan</desc>
</wpt>
</gpx>
68 changes: 68 additions & 0 deletions tests/test_generate_map.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
"""Tests for scripts/generate_map.py pure functions."""

from scripts.generate_map import match_protocols, short_label


# ---------- short_label ----------

def test_short_label_strips_country():
assert short_label("Athens, Greece") == "Athens"


def test_short_label_edo():
assert short_label("Edo (Tokyo), Japan") == "Edo"


def test_short_label_quebec():
assert short_label("Quebec City, Canada") == "Quebec"


def test_short_label_rio():
assert short_label("Rio de Janeiro, Brazil") == "Rio"


def test_short_label_no_country():
assert short_label("Athens") == "Athens"


def test_short_label_multi_part():
assert short_label("Lima, Peru") == "Lima"


# ---------- match_protocols ----------

def test_match_protocols_basic():
waypoints = [
("Athens, Greece", 37.9838, 23.7275),
("Babylon, Iraq", 32.5439, 44.4208),
]
protocols = {
"Athens": {"number": 4, "hash": "PtAthens", "mainnet": True},
"Babylon": {"number": 5, "hash": "PsBabylon", "mainnet": True},
}
matched = match_protocols(waypoints, protocols)
assert "Athens" in matched
assert "Babylon" in matched
assert matched["Athens"]["number"] == 4


def test_match_protocols_parisc_alias():
waypoints = [("Paris, France", 48.8566, 2.3522)]
protocols = {
"ParisC": {"number": 20, "hash": "PsParis", "mainnet": True},
}
matched = match_protocols(waypoints, protocols)
assert "Paris" in matched
assert matched["Paris"]["number"] == 20


def test_match_protocols_unmatched_waypoint():
waypoints = [("Unknown City, Nowhere", 0.0, 0.0)]
protocols = {"Athens": {"number": 4}}
matched = match_protocols(waypoints, protocols)
assert len(matched) == 0


def test_match_protocols_empty():
matched = match_protocols([], {})
assert matched == {}
179 changes: 179 additions & 0 deletions tests/test_update_gpx.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
"""Tests for scripts/update_gpx.py pure functions."""

from unittest.mock import MagicMock, patch

from scripts.update_gpx import (
build_protocols_json,
build_wpt,
desc_for,
format_coord,
gpx_coords_for_protocols,
parse_wpts,
scrape_protocols,
sort_key,
)


# ---------- format_coord ----------

def test_format_coord_rounds_to_four_decimals():
assert format_coord(37.98384) == "37.9838"


def test_format_coord_pads_short_decimals():
assert format_coord(10.1) == "10.1000"


def test_format_coord_negative():
assert format_coord(-77.0428) == "-77.0428"


# ---------- desc_for ----------

def test_desc_for_city_country():
assert desc_for("Athens, Greece") == "City in Greece"


def test_desc_for_no_country():
assert desc_for("Athens") == "Athens"


def test_desc_for_multiple_parts():
assert desc_for("Quebec City, Quebec, Canada") == "City in Canada"


# ---------- build_wpt ----------

def test_build_wpt_structure():
wpt = build_wpt("Athens, Greece", 37.9838, 23.7275)
assert '<wpt lat="37.9838" lon="23.7275">' in wpt
assert "<name>Athens, Greece</name>" in wpt
assert "<desc>City in Greece</desc>" in wpt
assert wpt.startswith(" <wpt")
assert wpt.endswith("</wpt>")


# ---------- parse_wpts ----------

def test_parse_wpts_extracts_blocks(sample_gpx_text):
blocks = parse_wpts(sample_gpx_text)
assert len(blocks) == 4
assert "<name>Athens, Greece</name>" in blocks[0]


# ---------- sort_key ----------

def test_sort_key_extracts_lowercase_name():
block = ' <wpt lat="37.9838" lon="23.7275">\n <name>Athens, Greece</name>\n <desc>Capital</desc>\n </wpt>'
assert sort_key(block) == "athens, greece"


def test_sort_key_no_name():
assert sort_key("<wpt></wpt>") == ""


# ---------- gpx_coords_for_protocols ----------

def test_gpx_coords_for_protocols_basic(sample_gpx_text):
scraped = [(4, "Athens"), (5, "Babylon"), (6, "Carthage")]
coords = gpx_coords_for_protocols(scraped, sample_gpx_text)
assert coords["Athens"] == (37.9838, 23.7275)
assert coords["Babylon"] == (32.5439, 44.4208)
assert coords["Carthage"] == (36.8508, 10.1833)


def test_gpx_coords_for_protocols_overrides(sample_gpx_text):
scraped = [(8, "Edo")]
coords = gpx_coords_for_protocols(scraped, sample_gpx_text)
assert coords["Edo"] == (35.6895, 139.6917)


def test_gpx_coords_for_protocols_missing(sample_gpx_text):
scraped = [(10, "Granada")]
coords = gpx_coords_for_protocols(scraped, sample_gpx_text)
assert "Granada" not in coords


# ---------- build_protocols_json ----------

def test_build_protocols_json_mainnet():
scraped = [(4, "Athens"), (5, "Babylon")]
tzkt_data = {
"Athens": {"hash": "PtAthens", "firstLevel": 1, "activationDate": "2019-05-30T00:58:55Z"},
"Babylon": {"hash": "PsBabylon", "firstLevel": 2, "activationDate": "2019-10-18T08:18:28Z"},
}
gpx_coords = {"Athens": (37.9838, 23.7275), "Babylon": (32.5439, 44.4208)}
result = build_protocols_json(scraped, tzkt_data, gpx_coords)
assert result["Athens"]["number"] == 4
assert result["Athens"]["hash"] == "PtAthens"
assert result["Athens"]["mainnet"] is True
assert result["Athens"]["lat"] == 37.9838
assert result["Babylon"]["mainnet"] is True


def test_build_protocols_json_testnet_only():
scraped = [(25, "Ushuaia")]
# TzKT is reachable (non-empty) but doesn't contain this protocol.
tzkt_data = {"Athens": {"hash": "PtAthens", "firstLevel": 1, "activationDate": "2019-05-30T00:58:55Z"}}
gpx_coords = {"Ushuaia": (-54.8073, -68.3084)}
testnet_info = {"Ushuaia": {"rpc_url": "https://rpc.ushuaianet.teztnets.com"}}

with patch("scripts.update_gpx.fetch_protocol_hash", return_value="PsUshuaia"):
result = build_protocols_json(scraped, tzkt_data, gpx_coords, testnet_info)
assert result["Ushuaia"]["mainnet"] is False
assert result["Ushuaia"]["hash"] == "PsUshuaia"


def test_build_protocols_json_no_tzkt():
scraped = [(4, "Athens")]
tzkt_data = {}
gpx_coords = {"Athens": (37.9838, 23.7275)}
result = build_protocols_json(scraped, tzkt_data, gpx_coords)
assert result["Athens"]["mainnet"] is None
assert result["Athens"]["hash"] is None


def test_build_protocols_json_no_coords():
scraped = [(4, "Athens")]
tzkt_data = {"Athens": {"hash": "PtAthens", "firstLevel": 1, "activationDate": "2019-05-30T00:58:55Z"}}
gpx_coords = {}
result = build_protocols_json(scraped, tzkt_data, gpx_coords)
assert result["Athens"]["lat"] is None
assert result["Athens"]["lon"] is None


def test_build_protocols_json_alias_map():
scraped = [(20, "ParisC")]
tzkt_data = {
"Paris C": {"hash": "PsParis", "firstLevel": 100, "activationDate": "2024-06-25T07:30:25Z"},
}
gpx_coords = {"ParisC": (48.8566, 2.3522)}
result = build_protocols_json(scraped, tzkt_data, gpx_coords)
assert result["ParisC"]["hash"] == "PsParis"
assert result["ParisC"]["mainnet"] is True


# ---------- scrape_protocols ----------

def test_scrape_protocols_with_fixture(naming_html):
mock_resp = MagicMock()
mock_resp.text = naming_html
mock_resp.raise_for_status = MagicMock()

with patch("scripts.update_gpx.requests.get", return_value=mock_resp):
result = scrape_protocols("https://example.com/naming.html")

assert result == [(4, "Athens"), (5, "Babylon"), (6, "Carthage"), (7, "Delphi")]


def test_scrape_protocols_skips_below_004(naming_html):
mock_resp = MagicMock()
mock_resp.text = naming_html
mock_resp.raise_for_status = MagicMock()

with patch("scripts.update_gpx.requests.get", return_value=mock_resp):
result = scrape_protocols("https://example.com/naming.html")

names = [name for _, name in result]
assert "Genesis" not in names
assert "Demo_noops" not in names
2 changes: 1 addition & 1 deletion tezos.gpx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<gpx version="1.1" creator="Tezos Protocols Maps (by Copolycube)">
<gpx version="1.1" creator="Tezos Protocol Map">
<wpt lat="37.9838" lon="23.7275">
<name>Athens, Greece</name>
<desc>Capital of Greece</desc>
Expand Down
Loading