480 lines
20 KiB
Python
480 lines
20 KiB
Python
# coding=utf-8
|
|
"""Tests de l'extension « Living Hinge Fill » (pytest)."""
|
|
|
|
import math
|
|
import os
|
|
import random
|
|
import re
|
|
import xml.etree.ElementTree as ET
|
|
|
|
import pytest
|
|
|
|
from living_hinge_core import (bounding_box, clear_intervals, hinge_slots, parse_color,
|
|
polylines_to_d, random_spans, regular_spans, rotate,
|
|
slot_length, slot_outline, slots_to_d)
|
|
|
|
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
SHAPES = os.path.join(HERE, "tests", "data", "shapes.svg")
|
|
|
|
RECT = [[(0.0, 0.0), (60.0, 0.0), (60.0, 80.0), (0.0, 80.0)]]
|
|
# Rectangle troue (pair-impair)
|
|
HOLED = [RECT[0], [(20.0, 30.0), (40.0, 30.0), (40.0, 50.0), (20.0, 50.0)]]
|
|
DIMS = dict(length=20.0, width=3.0, bridge=2.0, pitch=5.0)
|
|
|
|
|
|
def circle(cx, cy, r, n=180):
|
|
return [(cx + r * math.cos(2 * math.pi * k / n), cy + r * math.sin(2 * math.pi * k / n))
|
|
for k in range(n)]
|
|
|
|
|
|
def distance_to_segment(p, a, b):
|
|
ex, ey = b[0] - a[0], b[1] - a[1]
|
|
norm = ex * ex + ey * ey
|
|
t = 0.0 if norm == 0 else max(0.0, min(1.0, ((p[0] - a[0]) * ex + (p[1] - a[1]) * ey) / norm))
|
|
return math.hypot(p[0] - a[0] - t * ex, p[1] - a[1] - t * ey)
|
|
|
|
|
|
def distance_to_boundary(p, rings):
|
|
return min(distance_to_segment(p, ring[k], ring[(k + 1) % len(ring)])
|
|
for ring in rings for k in range(len(ring)))
|
|
|
|
|
|
def is_inside(p, rings):
|
|
inside = False
|
|
for ring in rings:
|
|
for k in range(len(ring)):
|
|
(x1, y1), (x2, y2) = ring[k], ring[(k + 1) % len(ring)]
|
|
if (y1 <= p[1]) != (y2 <= p[1]) and p[0] < x1 + (p[1] - y1) * (x2 - x1) / (y2 - y1):
|
|
inside = not inside
|
|
return inside
|
|
|
|
|
|
# --------------------------------------------------------------------------
|
|
# Noyau (sans inkex) : utilitaires
|
|
# --------------------------------------------------------------------------
|
|
|
|
def test_parse_color_inkscape_integer():
|
|
assert parse_color("3014898687") == ("#b3b3b3", 1.0)
|
|
assert parse_color(str(0xFF000080)) == ("#ff0000", round(0x80 / 255, 4))
|
|
assert parse_color("255") == ("#000000", 1.0)
|
|
|
|
|
|
def test_parse_color_hex_and_invalid():
|
|
assert parse_color("#123456") == ("#123456", 1.0)
|
|
assert parse_color("#abc") == ("#aabbcc", 1.0)
|
|
assert parse_color("pas une couleur") == ("#b3b3b3", 1.0)
|
|
|
|
|
|
def test_polylines_to_d():
|
|
d = polylines_to_d([[(0, 0), (1, 2)], [(3, 4), (5, 6)]], precision=1)
|
|
assert d == "M 0.0,0.0 L 1.0,2.0 M 3.0,4.0 L 5.0,6.0"
|
|
|
|
|
|
def test_bounding_box():
|
|
assert bounding_box(HOLED) == (0.0, 0.0, 60.0, 80.0)
|
|
assert bounding_box([]) is None
|
|
|
|
|
|
def test_rotate_counter_clockwise_on_screen():
|
|
# y vers le bas : un point a droite du centre monte (y diminue).
|
|
x, y = rotate((1.0, 0.0), 90)
|
|
assert abs(x) < 1e-12 and abs(y + 1.0) < 1e-12
|
|
x, y = rotate((3.0, 2.0), 180, center=(2.0, 2.0))
|
|
assert abs(x - 1.0) < 1e-12 and abs(y - 2.0) < 1e-12
|
|
|
|
|
|
def test_clear_intervals_rectangle():
|
|
assert clear_intervals(RECT, 30.0) == [(0.0, 80.0)]
|
|
(y0, y1), = clear_intervals(RECT, 30.0, 5.0)
|
|
assert abs(y0 - 5.0) < 1e-6 and abs(y1 - 75.0) < 1e-6
|
|
# Trop pres du bord gauche, ou hors de la forme
|
|
assert clear_intervals(RECT, 3.0, 5.0) == []
|
|
assert clear_intervals(RECT, 70.0) == []
|
|
|
|
|
|
def test_clear_intervals_hole_and_corner():
|
|
low, high = clear_intervals(HOLED, 30.0, 2.0)
|
|
assert abs(low[1] - 28.0) < 1e-6 and abs(high[0] - 52.0) < 1e-6
|
|
# A cote du trou : la zone interdite s'arrondit autour de ses coins.
|
|
(_y0, y1), (y2, _y3) = clear_intervals(HOLED, 18.0, 4.0)
|
|
reach = math.sqrt(4.0 ** 2 - 2.0 ** 2)
|
|
assert abs(y1 - (30.0 - reach)) < 1e-6 and abs(y2 - (50.0 + reach)) < 1e-6
|
|
|
|
|
|
# --------------------------------------------------------------------------
|
|
# Noyau (sans inkex) : motif
|
|
# --------------------------------------------------------------------------
|
|
|
|
def test_slots_stay_inside_with_margin():
|
|
for rings in (RECT, HOLED, [circle(50, 50, 40), circle(50, 50, 10)]):
|
|
for angle in (0.0, 30.0, 90.0):
|
|
slots = hinge_slots(rings, margin=2.0, angle=angle, **DIMS)
|
|
assert slots
|
|
for slot in slots:
|
|
for point in slot_outline(slot, 1.5):
|
|
assert is_inside(point, rings)
|
|
assert distance_to_boundary(point, rings) >= 2.0 - 1e-6
|
|
|
|
|
|
def test_slot_lengths_and_minimum():
|
|
slots = hinge_slots(RECT, margin=2.0, min_length=6.0, **DIMS)
|
|
lengths = [slot_length(slot, 3.0) for slot in slots]
|
|
assert max(lengths) <= 20.0 + 1e-9
|
|
assert min(lengths) >= 6.0 - 1e-9
|
|
assert any(abs(value - 20.0) < 1e-9 for value in lengths) # lumieres entieres
|
|
assert any(value < 20.0 - 1e-6 for value in lengths) # et raccourcies
|
|
# Un minimum plus haut ecarte des lumieres raccourcies, jamais les entieres.
|
|
full_only = hinge_slots(RECT, margin=2.0, min_length=20.0, **DIMS)
|
|
assert 0 < len(full_only) < len(slots)
|
|
assert all(abs(slot_length(slot, 3.0) - 20.0) < 1e-9 for slot in full_only)
|
|
|
|
|
|
def test_columns_pitch_bridge_and_stagger():
|
|
slots = hinge_slots(RECT, margin=2.0, **DIMS)
|
|
columns = {}
|
|
for (x0, y0), (x1, y1) in slots:
|
|
assert x0 == x1 and y0 <= y1 # lumieres verticales
|
|
columns.setdefault(round(x0, 6), []).append((y0 - 1.5, y1 + 1.5))
|
|
xs = sorted(columns)
|
|
assert all(abs(b - a - 5.0) < 1e-9 for a, b in zip(xs, xs[1:]))
|
|
assert 30.0 in xs # motif centre sur la forme
|
|
assert xs[0] >= 3.5 and xs[-1] <= 56.5 # demi-largeur + marge
|
|
for spans in columns.values():
|
|
spans.sort()
|
|
for (_a, end), (start, _b) in zip(spans, spans[1:]):
|
|
assert abs(start - end - 2.0) < 1e-9 # pont entre deux lumieres
|
|
# Quinconce : decalage d'une demi-periode (22 / 2) entre colonnes voisines.
|
|
even = {round(end % 22.0, 6) for _start, end in columns[30.0][:-1]}
|
|
odd = {round(end % 22.0, 6) for _start, end in columns[35.0][:-1]}
|
|
assert len(even) == 1 and len(odd) == 1
|
|
assert abs(abs(even.pop() - odd.pop()) - 11.0) < 1e-6
|
|
|
|
|
|
def test_stagger_zero_aligns_columns():
|
|
slots = hinge_slots(RECT, margin=2.0, stagger=0.0, **DIMS)
|
|
spans = {}
|
|
for (x0, y0), (_x1, y1) in slots:
|
|
spans.setdefault(round(x0, 6), []).append((round(y0, 6), round(y1, 6)))
|
|
assert len(spans) > 1
|
|
assert len(set(map(tuple, spans.values()))) == 1
|
|
|
|
|
|
def test_angle_90_gives_horizontal_slots():
|
|
slots = hinge_slots(RECT, margin=2.0, angle=90.0, **DIMS)
|
|
assert slots
|
|
for (x0, y0), (x1, y1) in slots:
|
|
assert abs(y0 - y1) < 1e-9 and abs(x1 - x0) > 1.0
|
|
|
|
|
|
def test_hole_is_avoided():
|
|
slots = hinge_slots(HOLED, margin=2.0, **DIMS)
|
|
for slot in slots:
|
|
for x, y in slot_outline(slot, 1.5):
|
|
assert not (18.0 + 1e-6 < x < 42.0 - 1e-6 and 28.0 + 1e-6 < y < 52.0 - 1e-6)
|
|
assert len(slots) != len(hinge_slots(RECT, margin=2.0, **DIMS))
|
|
|
|
|
|
def test_regular_spans():
|
|
spans = regular_spans(0.0, 100.0, 5.0, 20.0, 2.0)
|
|
assert all(size == 20.0 for _start, size in spans)
|
|
starts = [start for start, _size in spans]
|
|
assert 5.0 in starts and starts[0] + 20.0 > 0.0 and starts[-1] <= 100.0
|
|
assert all(abs(b - a - 22.0) < 1e-9 for a, b in zip(starts, starts[1:]))
|
|
|
|
|
|
def test_random_spans_fill_exactly():
|
|
# Toutes sortes de hauteurs : la zone est remplie pile, d'un bout a l'autre.
|
|
for k in range(200):
|
|
low, high = 10.0, 10.0 + 25.0 + 1.37 * k
|
|
spans = random_spans(low, high, 3.0, 20.0, 2.0, random.Random(k))
|
|
assert abs(spans[0][0] - low) < 1e-9
|
|
assert abs(spans[-1][0] + spans[-1][1] - high) < 1e-9
|
|
assert all(3.0 - 1e-9 <= size <= 20.0 + 1e-9 for _start, size in spans)
|
|
for (start, size), (following, _s) in zip(spans, spans[1:]):
|
|
assert abs(following - start - size - 2.0) < 1e-9 # pont constant
|
|
# Reproductible, et different d'un tirage a l'autre.
|
|
args = (0.0, 500.0, 3.0, 20.0, 2.0)
|
|
assert random_spans(*args, random.Random(1)) == random_spans(*args, random.Random(1))
|
|
assert random_spans(*args, random.Random(1)) != random_spans(*args, random.Random(2))
|
|
|
|
|
|
def test_random_spans_tight_cases():
|
|
rng = random.Random(0)
|
|
# Zone plus courte que la plus courte lumiere : une seule, qui la remplit.
|
|
assert random_spans(0.0, 4.0, 6.0, 20.0, 2.0, rng) == [(0.0, 4.0)]
|
|
# Zone d'une lumiere pile.
|
|
assert random_spans(5.0, 15.0, 3.0, 20.0, 2.0, rng) == [(5.0, 10.0)]
|
|
# Aucun compte ne tombe juste (bornes egales) : lumieres egales, zone remplie.
|
|
spans = random_spans(0.0, 76.0, 20.0, 20.0, 2.0, rng)
|
|
assert len({round(size, 9) for _start, size in spans}) == 1
|
|
assert spans[0][0] == 0.0 and abs(spans[-1][0] + spans[-1][1] - 76.0) < 1e-9
|
|
assert random_spans(3.0, 3.0, 3.0, 20.0, 2.0, rng) == []
|
|
|
|
|
|
def test_random_columns_start_and_end_at_margin():
|
|
# Dans un rectangle, toutes les colonnes commencent et finissent au meme niveau.
|
|
slots = hinge_slots(RECT, margin=2.0, random_min=3.0, seed=4, **DIMS)
|
|
columns = {}
|
|
for (x0, y0), (_x1, y1) in slots:
|
|
columns.setdefault(round(x0, 6), []).append((y0 - 1.5, y1 + 1.5))
|
|
assert len(columns) == 11
|
|
for spans in columns.values():
|
|
assert abs(min(a for a, _b in spans) - 2.0) < 1e-6
|
|
assert abs(max(b for _a, b in spans) - 78.0) < 1e-6
|
|
# Autour d'un trou aussi : chaque zone libre est remplie d'un bord a l'autre.
|
|
slots = hinge_slots(HOLED, margin=2.0, random_min=3.0, seed=4, **DIMS)
|
|
middle = sorted((y0 - 1.5, y1 + 1.5) for (x0, y0), (_x1, y1) in slots if x0 == 30.0)
|
|
ends = [b for _a, b in middle]
|
|
starts = [a for a, _b in middle]
|
|
assert abs(starts[0] - 2.0) < 1e-6 and abs(ends[-1] - 78.0) < 1e-6
|
|
assert any(abs(b - 28.0) < 1e-6 for b in ends)
|
|
assert any(abs(a - 52.0) < 1e-6 for a in starts)
|
|
|
|
|
|
def test_random_lengths():
|
|
big = [[(0.0, 0.0), (200.0, 0.0), (200.0, 300.0), (0.0, 300.0)]]
|
|
slots = hinge_slots(big, margin=2.0, random_min=3.0, seed=7, **DIMS)
|
|
assert slots == hinge_slots(big, margin=2.0, random_min=3.0, seed=7, **DIMS)
|
|
assert slots != hinge_slots(big, margin=2.0, random_min=3.0, seed=8, **DIMS)
|
|
lengths = [slot_length(slot, 3.0) for slot in slots]
|
|
assert min(lengths) >= 3.0 - 1e-9 and max(lengths) <= 20.0 + 1e-9
|
|
assert min(lengths) < 6.0 and max(lengths) > 17.0 # toute la plage sert
|
|
assert len({round(value, 3) for value in lengths}) > len(lengths) / 2
|
|
# Pas des colonnes et pont inchanges, lumieres a la marge.
|
|
columns = {}
|
|
for (x0, y0), (x1, y1) in slots:
|
|
assert x0 == x1
|
|
assert 3.5 - 1e-6 <= y0 and y1 <= 296.5 + 1e-6
|
|
columns.setdefault(round(x0, 6), []).append((y0 - 1.5, y1 + 1.5))
|
|
xs = sorted(columns)
|
|
assert all(abs(b - a - 5.0) < 1e-9 for a, b in zip(xs, xs[1:]))
|
|
for spans in columns.values():
|
|
spans.sort()
|
|
for (_a, end), (start, _b) in zip(spans, spans[1:]):
|
|
assert abs(start - end - 2.0) < 1e-9
|
|
# Les colonnes ne sont pas des copies les unes des autres.
|
|
assert len({tuple(round(v, 3) for span in spans for v in span)
|
|
for spans in columns.values()}) == len(columns)
|
|
|
|
|
|
def test_random_min_is_clamped_and_respects_shape():
|
|
# Minimum sous la largeur : ramene a la largeur (trou rond au plus petit).
|
|
slots = hinge_slots(HOLED, margin=2.0, random_min=0.0, seed=3, **DIMS)
|
|
assert slots
|
|
for slot in slots:
|
|
assert slot_length(slot, 3.0) >= 3.0 - 1e-9
|
|
for point in slot_outline(slot, 1.5):
|
|
assert is_inside(point, HOLED)
|
|
assert distance_to_boundary(point, HOLED) >= 2.0 - 1e-6
|
|
# Minimum ramene a la longueur : plus de hasard, lumieres egales d'une marge a l'autre.
|
|
slots = hinge_slots(RECT, margin=2.0, random_min=50.0, seed=3, **DIMS)
|
|
assert slots and len({round(slot_length(slot, 3.0), 6) for slot in slots}) == 1
|
|
|
|
|
|
def test_full_slot_shorter_than_minimum_is_kept():
|
|
# min_length ne vise que les lumieres raccourcies par le bord.
|
|
slots = hinge_slots(RECT, length=5.0, width=3.0, bridge=2.0, pitch=5.0, min_length=9.0)
|
|
assert slots and all(abs(slot_length(slot, 3.0) - 5.0) < 1e-9 for slot in slots)
|
|
|
|
|
|
def test_degenerate_inputs():
|
|
assert hinge_slots([], **DIMS) == []
|
|
assert hinge_slots([[(0, 0), (1, 1)]], **DIMS) == []
|
|
assert hinge_slots([[(0, 0), (4, 0), (4, 4), (0, 4)]], margin=2.0, **DIMS) == []
|
|
assert hinge_slots(RECT, length=2.0, width=3.0, bridge=2.0, pitch=5.0) == []
|
|
assert hinge_slots(RECT, length=20.0, width=0.0, bridge=2.0, pitch=5.0) == []
|
|
assert hinge_slots(RECT, length=20.0, width=3.0, bridge=2.0, pitch=0.0) == []
|
|
|
|
|
|
def test_slot_outline_is_at_radius_of_axis():
|
|
slot = ((10.0, 10.0), (10.0, 30.0))
|
|
outline = slot_outline(slot, 2.0, segments=8)
|
|
assert all(abs(a - b) < 1e-9 for a, b in zip(outline[0], outline[-1]))
|
|
for point in outline:
|
|
assert abs(distance_to_segment(point, *slot) - 2.0) < 1e-9
|
|
ys = [y for _x, y in outline]
|
|
assert abs(min(ys) - 8.0) < 1e-9 and abs(max(ys) - 32.0) < 1e-9
|
|
|
|
|
|
def test_slots_to_d():
|
|
d = slots_to_d([((10.0, 10.0), (10.0, 30.0))], 2.0, precision=1)
|
|
assert d == ("M 8.0,10.0 L 8.0,30.0 A 2.0 2.0 0 0 0 12.0,30.0 "
|
|
"L 12.0,10.0 A 2.0 2.0 0 0 0 8.0,10.0 Z")
|
|
# Lumiere reduite a un cercle : deux arcs, pas de segment nul.
|
|
circle_d = slots_to_d([((5.0, 5.0), (5.0, 5.0))], 1.0, precision=0)
|
|
assert circle_d == "M 4,5 A 1 1 0 0 0 6,5 A 1 1 0 0 0 4,5 Z"
|
|
assert slots_to_d([], 1.0) == ""
|
|
assert slots_to_d([((0, 0), (0, 9)), ((5, 0), (5, 9))], 1.0).count("Z") == 2
|
|
|
|
|
|
# --------------------------------------------------------------------------
|
|
# Bout en bout (necessite inkex)
|
|
# --------------------------------------------------------------------------
|
|
|
|
def run_extension(tmp_path, *args):
|
|
pytest.importorskip("inkex")
|
|
from living_hinge import LivingHinge
|
|
out = tmp_path / "out.svg"
|
|
if out.exists():
|
|
out.unlink()
|
|
LivingHinge().run([*args, "--output={}".format(out), SHAPES])
|
|
# inkex n'ecrit rien quand le document n'a pas change (erreur utilisateur).
|
|
return out.read_text(encoding="utf-8") if out.exists() else ""
|
|
|
|
|
|
def generated_paths(svg):
|
|
"""Chemins ajoutes par l'extension (ceux du fichier d'exemple ont un id connu)."""
|
|
root = ET.fromstring(svg)
|
|
return [elem for elem in root.iter()
|
|
if elem.tag.endswith("}path") and elem.get("id") not in ("path1", "path2")]
|
|
|
|
|
|
def test_end_to_end_default(tmp_path):
|
|
svg = run_extension(tmp_path, "--id=rect1")
|
|
assert "Living hinge" in svg
|
|
assert 'id="rect1"' in svg
|
|
paths = generated_paths(svg)
|
|
assert len(paths) == 1 # un seul chemin pour toutes les lumieres
|
|
assert paths[0].get("d").count("Z") >= 10
|
|
assert "fill:none" in paths[0].get("style")
|
|
|
|
|
|
def test_end_to_end_slots_inside_rect(tmp_path):
|
|
svg = run_extension(tmp_path, "--id=rect1", "--margin=2")
|
|
d = generated_paths(svg)[0].get("d")
|
|
points = [(float(x), float(y)) for x, y in re.findall(r"(-?[\d.]+),(-?[\d.]+)", d)]
|
|
assert points
|
|
# rect1 : x 10..60, y 10..50 ; les raccords droite / arc restent a la marge
|
|
# en x, et a marge + rayon en y (le bout arrondi depasse d'un rayon).
|
|
assert all(12.0 - 1e-3 <= x <= 58.0 + 1e-3 for x, _y in points)
|
|
assert all(13.5 - 1e-3 <= y <= 46.5 + 1e-3 for _x, y in points)
|
|
|
|
|
|
def test_end_to_end_all_shapes_and_group(tmp_path):
|
|
svg = run_extension(tmp_path, "--id=circle1", "--id=path1", "--id=path2", "--id=group1")
|
|
assert svg.count("Living hinge") == 4
|
|
assert len(generated_paths(svg)) == 4
|
|
# Le groupe d'accueil neutralise la translation du groupe parent.
|
|
assert re.search(r'transform="translate\(-100,? ?-65\)"', svg)
|
|
|
|
|
|
def test_end_to_end_units_and_angle(tmp_path):
|
|
svg = run_extension(tmp_path, "--id=rect1", "--unit=cm", "--slot_length=2",
|
|
"--slot_width=0.3", "--bridge=0.2", "--pitch=0.5",
|
|
"--margin=0.2", "--min_length=0.6", "--stroke_width=0.02",
|
|
"--angle=90")
|
|
assert "A 1.5000 1.5000" in svg
|
|
assert re.search(r"stroke-width:0\.2\d*[;\"]", svg)
|
|
|
|
|
|
def test_end_to_end_errors(tmp_path, capsys):
|
|
svg = run_extension(tmp_path) # pas de selection
|
|
assert "Living hinge" not in svg
|
|
svg = run_extension(tmp_path, "--id=rect1", "--pitch=2") # pas <= largeur
|
|
assert "Living hinge" not in svg
|
|
svg = run_extension(tmp_path, "--id=rect1", "--slot_length=1") # longueur < largeur
|
|
assert "Living hinge" not in svg
|
|
svg = run_extension(tmp_path, "--id=rect1", "--margin=30") # rien ne tient
|
|
assert "Living hinge" not in svg
|
|
errors = capsys.readouterr().err
|
|
assert "Select at least one shape" in errors
|
|
assert "column pitch" in errors and "slot length" in errors and "too small" in errors
|
|
|
|
|
|
def arc_starts(svg):
|
|
"""Debut de chaque lumiere du chemin genere (un « M x,y » par lumiere)."""
|
|
return re.findall(r"M (-?[\d.]+,-?[\d.]+)", generated_paths(svg)[0].get("d"))
|
|
|
|
|
|
def test_end_to_end_random(tmp_path):
|
|
regular = run_extension(tmp_path, "--id=path2")
|
|
first = run_extension(tmp_path, "--id=path2", "--random_lengths=true")
|
|
again = run_extension(tmp_path, "--id=path2", "--random_lengths=true")
|
|
other = run_extension(tmp_path, "--id=path2", "--random_lengths=true", "--seed=2")
|
|
assert arc_starts(first) == arc_starts(again) # meme graine, meme motif
|
|
assert arc_starts(first) != arc_starts(other)
|
|
assert arc_starts(first) != arc_starts(regular)
|
|
assert len(arc_starts(first)) > len(arc_starts(regular)) # lumieres plus courtes
|
|
|
|
|
|
def test_end_to_end_random_error(tmp_path, capsys):
|
|
svg = run_extension(tmp_path, "--id=rect1", "--random_lengths=true", "--random_min=25")
|
|
assert "Living hinge" not in svg
|
|
assert "shortest random slot" in capsys.readouterr().err
|
|
|
|
|
|
def test_end_to_end_removes_original(tmp_path):
|
|
svg = run_extension(tmp_path, "--id=rect1", "--keep_original=false")
|
|
assert 'id="rect1"' not in svg
|
|
|
|
|
|
def test_end_to_end_color(tmp_path):
|
|
svg = run_extension(tmp_path, "--id=rect1", "--stroke_color={}".format(0x336699FF))
|
|
assert re.search(r"stroke:#336699", svg)
|
|
|
|
|
|
# --------------------------------------------------------------------------
|
|
# Traductions
|
|
# --------------------------------------------------------------------------
|
|
|
|
def test_translations_up_to_date_and_complete():
|
|
"""Chaque texte du .inx et des .py a sa traduction dans chaque catalogue."""
|
|
import gettext
|
|
import i18n
|
|
|
|
msgids = [msgid for msgid, _refs in i18n.extract()]
|
|
assert "Living Hinge Fill" in msgids
|
|
assert "mm" not in msgids # unites marquees translatable="no"
|
|
|
|
for language in i18n.LANGUAGES:
|
|
entries = i18n.read_po(i18n.po_path(language))
|
|
missing = [m for m in msgids if not entries.get(m, ("", False))[0]]
|
|
assert not missing, "{}.po incomplet : {}".format(language, missing)
|
|
|
|
catalog = gettext.translation(i18n.DOMAIN, i18n.LOCALE_DIR, [language])
|
|
for msgid in msgids:
|
|
assert catalog.gettext(msgid) == entries[msgid][0], \
|
|
"{} : .mo a recompiler (python i18n.py)".format(language)
|
|
|
|
|
|
def test_po_roundtrip(tmp_path):
|
|
import i18n
|
|
|
|
messages = [("Simple", ["a"]), ('Quote "x" and \\ back', ["a"]),
|
|
("Two\nlines", ["a"]), ("Tab\tend\n", ["a"])]
|
|
path = str(tmp_path / "xx.po")
|
|
i18n.write_po(path, "fr", messages,
|
|
{m: ("<" + m + ">", False) for m, _r in messages})
|
|
entries = i18n.read_po(path)
|
|
for msgid, _refs in messages:
|
|
assert entries[msgid] == ("<" + msgid + ">", False)
|
|
|
|
|
|
def test_inx_matches_arguments():
|
|
"""Chaque <param> du .inx a son add_argument, memes noms et memes defauts."""
|
|
root = ET.parse(os.path.join(HERE, "living_hinge.inx")).getroot()
|
|
params = {elem.get("name"): (elem.text or "").strip() for elem in root.iter()
|
|
if elem.tag.rsplit("}", 1)[-1] == "param"}
|
|
with open(os.path.join(HERE, "living_hinge.py"), encoding="utf-8") as handle:
|
|
source = handle.read()
|
|
arguments = dict(re.findall(r'add_argument\("--(\w+)",.*?default=([^)]+)\)', source))
|
|
assert set(params) == set(arguments)
|
|
for name, default in arguments.items():
|
|
if name in ("tab", "unit"):
|
|
continue
|
|
value = default.strip('"')
|
|
if params[name] in ("true", "false"):
|
|
assert value.lower() == params[name], name
|
|
elif name == "stroke_color":
|
|
assert value == params[name], name
|
|
else:
|
|
assert float(value) == float(params[name]), name
|
|
|
|
|
|
def test_inx_images_exist():
|
|
"""Les images de la boite de dialogue sont presentes (chemin relatif au .inx)."""
|
|
root = ET.parse(os.path.join(HERE, "living_hinge.inx")).getroot()
|
|
for elem in root.iter():
|
|
if elem.tag.rsplit("}", 1)[-1] == "image":
|
|
assert os.path.isfile(os.path.join(HERE, elem.text.strip())), elem.text
|