321 lines
12 KiB
Python
321 lines
12 KiB
Python
# coding=utf-8
|
|
"""Tests de l'extension « Relief Pattern Fill » (pytest)."""
|
|
|
|
import os
|
|
import re
|
|
import xml.etree.ElementTree as ET
|
|
|
|
import pytest
|
|
|
|
import math
|
|
|
|
from relief_core import (CELL_EDGES, clip_segment, fill_rings, parse_color,
|
|
pattern_segments, point_in_rings, polylines_to_d,
|
|
rotate_point)
|
|
|
|
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
SHAPES = os.path.join(HERE, "tests", "data", "shapes.svg")
|
|
SCALED = os.path.join(HERE, "tests", "data", "scaled.svg")
|
|
|
|
|
|
# --------------------------------------------------------------------------
|
|
# Noyau (sans inkex)
|
|
# --------------------------------------------------------------------------
|
|
|
|
def test_parse_color_inkscape_integer():
|
|
assert parse_color("3014898687") == ("#b3b3b3", 1.0)
|
|
assert parse_color(str(0xFF000080)) == ("#ff0000", round(0x80 / 255, 4))
|
|
|
|
|
|
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"
|
|
|
|
|
|
SQUARE = [(0, 0), (100, 0), (100, 100), (0, 100)]
|
|
HOLED = [SQUARE, [(25, 25), (75, 25), (75, 75), (25, 75)]]
|
|
|
|
|
|
def length(segment):
|
|
(x1, y1), (x2, y2) = segment
|
|
return math.hypot(x2 - x1, y2 - y1)
|
|
|
|
|
|
def key(segment):
|
|
(x1, y1), (x2, y2) = segment
|
|
return round(x1, 3), round(y1, 3), round(x2, 3), round(y2, 3)
|
|
|
|
|
|
def test_cell_edges_are_valid_lattice_edges():
|
|
assert len(set(CELL_EDGES)) == len(CELL_EDGES) == 39
|
|
for i, j, di, dj in CELL_EDGES:
|
|
assert (i + j) % 2 == 0
|
|
assert (di, dj) in ((0, 2), (1, 1), (1, -1))
|
|
|
|
|
|
def test_pattern_directions_and_periodicity():
|
|
"""Segments verticaux ou a +/-30 degres ; motif invariant par translation."""
|
|
edge = 10.0
|
|
segments = pattern_segments((0, 0, 200, 200), edge)
|
|
for (x1, y1), (x2, y2) in segments:
|
|
angle = round(math.degrees(math.atan2(y2 - y1, x2 - x1))) % 180
|
|
assert angle in (30, 90, 150)
|
|
keys = {key(s) for s in segments}
|
|
inner = [k for k in keys if all(40 < k[n] < 160 for n in (0, 2))
|
|
and all(40 < k[n] < 150 for n in (1, 3))]
|
|
assert inner
|
|
for x1, y1, x2, y2 in inner:
|
|
assert (x1, round(y1 + 5 * edge, 3), x2, round(y2 + 5 * edge, 3)) in keys
|
|
|
|
|
|
def test_pattern_merges_collinear_edges():
|
|
"""Deux aretes bout a bout sur la meme droite forment un seul segment."""
|
|
edge = 10.0
|
|
segments = pattern_segments((0, 0, 200, 200), edge)
|
|
assert max(length(s) for s in segments) > 1.5 * edge
|
|
at_node = {}
|
|
for p, q in segments:
|
|
for a, b in ((p, q), (q, p)):
|
|
at_node.setdefault((round(a[0], 3), round(a[1], 3)), []).append(b)
|
|
for point, others in at_node.items():
|
|
for k, a in enumerate(others):
|
|
for b in others[k + 1:]:
|
|
cross = ((a[0] - point[0]) * (b[1] - point[1])
|
|
- (a[1] - point[1]) * (b[0] - point[0]))
|
|
dot = ((a[0] - point[0]) * (b[0] - point[0])
|
|
+ (a[1] - point[1]) * (b[1] - point[1]))
|
|
assert not (abs(cross) < 1e-6 and dot < 0)
|
|
|
|
|
|
def test_pattern_rejects_bad_edge():
|
|
with pytest.raises(ValueError):
|
|
pattern_segments((0, 0, 10, 10), 0)
|
|
|
|
|
|
def test_pattern_too_many_segments():
|
|
with pytest.raises(OverflowError):
|
|
pattern_segments((0, 0, 1e6, 1e6), 1.0)
|
|
|
|
|
|
def test_point_in_rings_with_hole():
|
|
assert point_in_rings((10, 10), HOLED)
|
|
assert not point_in_rings((50, 50), HOLED)
|
|
assert not point_in_rings((150, 50), HOLED)
|
|
|
|
|
|
def test_clip_segment_square():
|
|
pieces = clip_segment((-50, 50), (150, 50), [SQUARE])
|
|
assert len(pieces) == 1
|
|
(x1, y1), (x2, y2) = pieces[0]
|
|
assert (round(x1, 6), round(x2, 6), y1, y2) == (0, 100, 50, 50)
|
|
assert clip_segment((200, 0), (300, 100), [SQUARE]) == []
|
|
|
|
|
|
def test_clip_segment_hole_splits_segment():
|
|
pieces = clip_segment((-10, 50), (110, 50), HOLED)
|
|
xs = sorted(round(p[0], 6) for piece in pieces for p in piece)
|
|
assert xs == [0, 25, 75, 100]
|
|
|
|
|
|
@pytest.mark.parametrize("angle", [0, 17, 90])
|
|
def test_fill_stays_inside_shape(angle):
|
|
triangle = [(0, 0), (120, 10), (40, 90)]
|
|
segments = fill_rings([triangle], 6.0, angle, (50, 30))
|
|
assert len(segments) > 50
|
|
for a, b in segments:
|
|
mid = ((a[0] + b[0]) / 2, (a[1] + b[1]) / 2)
|
|
assert point_in_rings(mid, [triangle])
|
|
|
|
|
|
def test_fill_avoids_hole():
|
|
segments = fill_rings(HOLED, 4.0)
|
|
assert segments
|
|
for a, b in segments:
|
|
mid = ((a[0] + b[0]) / 2, (a[1] + b[1]) / 2)
|
|
assert not (25 < mid[0] < 75 and 25 < mid[1] < 75)
|
|
|
|
|
|
def test_fill_rotation_turns_directions():
|
|
segments = fill_rings([SQUARE], 6.0, 90.0, (50, 50))
|
|
angles = {round(math.degrees(math.atan2(b[1] - a[1], b[0] - a[0]))) % 180
|
|
for a, b in segments}
|
|
assert angles == {0, 60, 120}
|
|
|
|
|
|
def test_fill_anchor_is_a_node():
|
|
"""Meme motif, relativement a l'ancre, quand la forme est translatee."""
|
|
base = {key(s) for s in fill_rings([SQUARE], 6.0, 0.0, (50, 50))}
|
|
shifted = [[(x + 7, y + 3) for x, y in SQUARE]]
|
|
moved = {key(((a[0] - 7, a[1] - 3), (b[0] - 7, b[1] - 3)))
|
|
for a, b in fill_rings(shifted, 6.0, 0.0, (57, 53))}
|
|
assert base == moved
|
|
|
|
|
|
def test_fill_degenerate_inputs():
|
|
assert fill_rings([], 5.0) == []
|
|
assert fill_rings([[(0, 0), (1, 1)]], 5.0) == []
|
|
fill_rings([SQUARE], 500.0) # arete enorme : pas d'exception
|
|
|
|
|
|
def test_rotate_point_clockwise_on_screen():
|
|
x, y = rotate_point((1, 0), 90)
|
|
assert (round(x, 6), round(y, 6)) == (0, 1)
|
|
|
|
|
|
# --------------------------------------------------------------------------
|
|
# Bout en bout (necessite inkex)
|
|
# --------------------------------------------------------------------------
|
|
|
|
def run_extension(tmp_path, *args):
|
|
pytest.importorskip("inkex")
|
|
from relief_fill import ReliefFill
|
|
out = tmp_path / "out.svg"
|
|
ReliefFill().run([*args, "--output={}".format(out), SHAPES])
|
|
return out.read_text(encoding="utf-8")
|
|
|
|
|
|
def test_end_to_end_default(tmp_path):
|
|
svg = run_extension(tmp_path, "--id=rect1")
|
|
assert "Relief fill" in svg
|
|
assert 'id="rect1"' in svg
|
|
assert len(list(generated_paths(svg))) == 1 # un seul chemin pour un rectangle
|
|
|
|
|
|
def test_end_to_end_non_standard_document_scale(tmp_path):
|
|
"""Page de 400 mm pour 538,72 unites : svg.unittouu() s'y tromperait."""
|
|
pytest.importorskip("inkex")
|
|
from relief_fill import ReliefFill
|
|
out = tmp_path / "out.svg"
|
|
ReliefFill().run(["--id=rect1", "--output={}".format(out), SCALED])
|
|
width = re.search(r"stroke-width:([\d.]+)", out.read_text(encoding="utf-8"))
|
|
assert float(width.group(1)) == pytest.approx(0.3 * 538.72053 / 400, abs=0.001)
|
|
|
|
|
|
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)
|
|
|
|
|
|
def generated_paths(svg_text):
|
|
"""Points (x, y) de chaque chemin du groupe « Relief fill »."""
|
|
root = ET.fromstring(svg_text)
|
|
for group in root.iter("{http://www.w3.org/2000/svg}g"):
|
|
if "Relief fill" in group.attrib.values():
|
|
for path in group.iter("{http://www.w3.org/2000/svg}path"):
|
|
nums = [float(n) for n in re.findall(r"-?\d+\.?\d*", path.get("d"))]
|
|
yield [(nums[k], nums[k + 1]) for k in range(0, len(nums) - 1, 2)]
|
|
|
|
|
|
@pytest.mark.parametrize("shape,box", [
|
|
("rect1", (10, 10, 60, 50)),
|
|
("circle1", (78, 8, 122, 52)),
|
|
("path1", (120, 0, 200, 60)),
|
|
])
|
|
def test_end_to_end_stays_in_bounding_box(tmp_path, shape, box):
|
|
svg = run_extension(tmp_path, "--id=" + shape, "--edge=3")
|
|
points = [p for pts in generated_paths(svg) for p in pts]
|
|
assert points
|
|
for x, y in points:
|
|
assert box[0] - 0.01 <= x <= box[2] + 0.01
|
|
assert box[1] - 0.01 <= y <= box[3] + 0.01
|
|
|
|
|
|
def test_end_to_end_hole_stays_empty(tmp_path):
|
|
svg = run_extension(tmp_path, "--id=path2", "--edge=3")
|
|
count = 0
|
|
for pts in generated_paths(svg):
|
|
for k in range(0, len(pts) - 1, 2):
|
|
count += 1
|
|
mid = ((pts[k][0] + pts[k + 1][0]) / 2, (pts[k][1] + pts[k + 1][1]) / 2)
|
|
assert not (36 < mid[0] < 64 and 81 < mid[1] < 99)
|
|
assert count > 20
|
|
|
|
|
|
def test_end_to_end_group_with_transform(tmp_path):
|
|
"""Groupe transforme : le motif suit l'ellipse, decalee de (100, 65)."""
|
|
svg = run_extension(tmp_path, "--id=group1", "--edge=3")
|
|
points = [p for pts in generated_paths(svg) for p in pts]
|
|
assert points
|
|
for x, y in points:
|
|
assert 105 - 0.01 <= x <= 185 + 0.01
|
|
assert 70 - 0.01 <= y <= 110 + 0.01
|
|
|
|
|
|
def test_end_to_end_outline_option(tmp_path):
|
|
svg = run_extension(tmp_path, "--id=rect1", "--draw_outline=true")
|
|
assert len(list(generated_paths(svg))) == 2 # contour + trait
|
|
|
|
|
|
def test_end_to_end_too_many_segments(tmp_path, capsys):
|
|
pytest.importorskip("inkex")
|
|
from relief_fill import ReliefFill
|
|
ReliefFill().run(["--id=rect1", "--edge=0.01",
|
|
"--output={}".format(tmp_path / "out.svg"), SHAPES])
|
|
assert "too many lines" in capsys.readouterr().err
|
|
|
|
|
|
# --------------------------------------------------------------------------
|
|
# 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 "Relief Pattern 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, et inversement."""
|
|
root = ET.parse(os.path.join(HERE, "relief_fill.inx")).getroot()
|
|
params = {elem.get("name") for elem in root.iter()
|
|
if elem.tag.rsplit("}", 1)[-1] == "param"}
|
|
with open(os.path.join(HERE, "relief_fill.py"), encoding="utf-8") as handle:
|
|
arguments = set(re.findall(r'add_argument\("--(\w+)"', handle.read()))
|
|
assert params == arguments
|
|
|
|
|
|
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, "relief_fill.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
|