Добавить очереди операций PBUS/SBUS, общую консоль, настройки Candle и SLCAN, декодеры DSView и обработку плотных графиков. Обновить протокол Бальзам и проверки общих компонентов.
108 lines
8.0 KiB
Python
108 lines
8.0 KiB
Python
# Проверки plot: фиксируют ожидаемое поведение на воспроизводимых входных данных.
|
||
# Сценарии для навигации по файлу:
|
||
# test_shared_numeric_fixtures
|
||
# test_markers_stay_in_data_coordinates_and_cross
|
||
# test_invalid_numeric_inputs_do_not_escape_to_painter
|
||
# test_locked_viewport_and_decibel_delta
|
||
# Тестовые значения и подмены окружения относятся к проверке контракта; они не являются
|
||
# настройками рабочего устройства.
|
||
|
||
"""Cross-port contract: Python/ctypes and Kotlin/JNI consume the same fixtures."""
|
||
import ctypes
|
||
import json
|
||
import math
|
||
import os
|
||
from pathlib import Path
|
||
import unittest
|
||
from dataclasses import replace
|
||
|
||
from protocan.plot import Bounds, Marker, Markers, PlotMath, Viewport
|
||
|
||
|
||
@unittest.skipUnless(os.environ.get("SETPROTOCOL_LIBRARY"), "Set SETPROTOCOL_LIBRARY to the built C library")
|
||
class PlotTests(unittest.TestCase):
|
||
@classmethod
|
||
def setUpClass(cls):
|
||
cls.core = PlotMath(ctypes.CDLL(os.environ["SETPROTOCOL_LIBRARY"]))
|
||
|
||
def test_shared_numeric_fixtures(self):
|
||
path = Path(__file__).resolve().parents[2] / "c/set-protocol/tests/fixtures/plot-v1.json"
|
||
for case in json.loads(path.read_text())["cases"]:
|
||
with self.subTest(case=case["name"]):
|
||
if case["output"] is None:
|
||
with self.assertRaises(ValueError):
|
||
self.core.call(case["op"], *case["input"])
|
||
else:
|
||
actual = self.core.call(case["op"], *case["input"])
|
||
self.assertEqual(len(actual), len(case["output"]))
|
||
for a, b in zip(actual, case["output"]):
|
||
self.assertAlmostEqual(a, b, places=10)
|
||
|
||
def test_markers_stay_in_data_coordinates_and_cross(self):
|
||
bounds = Bounds(1000, 2000, -10, 10)
|
||
markers = Markers().positioned(self.core, bounds)
|
||
self.assertTrue(markers.enabled(Marker.A))
|
||
self.assertFalse(markers.enabled(Marker.C))
|
||
self.assertTrue(replace(markers, x_pairs=2).enabled(Marker.C))
|
||
zoomed = bounds.visible(self.core, Viewport(.25, .25, .5, .5))
|
||
self.assertEqual(markers, markers.positioned(self.core, zoomed))
|
||
moved = markers.drag(self.core, Marker.A, 50, 500, zoomed)
|
||
self.assertAlmostEqual(moved.a, markers.a + 50)
|
||
self.assertEqual(markers.b, moved.b)
|
||
self.assertEqual(Marker.A, moved.hit(self.core, 0, 50, 500, 100, 20, zoomed))
|
||
crossed = markers.move(Marker.A, 1900).move(Marker.B, 1100)
|
||
self.assertEqual(-800, self.core.delta(crossed.a, crossed.b))
|
||
|
||
def test_deep_x_zoom_preserves_anchor_and_allows_pan_and_zoom_out(self):
|
||
view = Viewport()
|
||
for _ in range(65):
|
||
view = view.transform(self.core, zoom_x=1.25, focus_x=.3)
|
||
self.assertAlmostEqual(.3, view.x + .3 * view.width)
|
||
self.assertLess(view.width * 5_000_000, 3)
|
||
moved = view.transform(self.core, pan_x=.2)
|
||
self.assertLess(moved.x, view.x)
|
||
self.assertEqual(view.width, moved.width)
|
||
expanded = moved.transform(self.core, zoom_x=.8)
|
||
self.assertGreater(expanded.width, moved.width)
|
||
limited = view.transform(self.core, zoom_x=1e15) # Заведомо превышаем новый предел 10^12 раз, чтобы проверить ограничение без обнуления ширины.
|
||
self.assertEqual(1e-12, limited.width) # Минимальная доля полного диапазона X должна позволять приближение в 100 000 раз сильнее прежнего.
|
||
self.assertEqual(limited, limited.transform(self.core, zoom_x=2))
|
||
|
||
def test_deep_zoom_keeps_both_axes_navigable(self): # Проверяем совместное приближение X и Y, независимое перемещение, границы и возвращение к полному диапазону.
|
||
view = Viewport() # Начинаем с полного диапазона обеих осей без фиксации.
|
||
for _ in range(160): # 160 шагов с коэффициентом 1,25 проходят оба прежних предела и достигают нового ограничения 10^12 раз.
|
||
view = view.transform(self.core, zoom_x=1.25, zoom_y=1.25, focus_x=.3, focus_y=.7) # Увеличиваем вокруг разных опорных точек, чтобы обнаружить смешение осей.
|
||
self.assertAlmostEqual(.3, view.x + .3 * view.width, places=14) # Координата опоры X должна оставаться на месте даже при минимальной ширине.
|
||
self.assertAlmostEqual(.7, view.y + .7 * view.height, places=14) # Независимая опора Y не должна дрейфовать при многократном увеличении.
|
||
self.assertEqual((1e-12, 1e-12), (view.width, view.height)) # Обе оси достигают триллионной доли полного диапазона и не превращаются в нулевой интервал.
|
||
self.assertEqual(view, view.transform(self.core, zoom_x=2, zoom_y=2)) # Дополнительное увеличение на пределе сохраняет область без накопления смещения.
|
||
moved = view.transform(self.core, pan_x=.2, pan_y=-.2) # Сдвигаем X и Y в противоположные стороны на пятую часть текущего окна.
|
||
self.assertLess(moved.x, view.x) # Положительное экранное перемещение X уменьшает смещение области.
|
||
self.assertGreater(moved.y, view.y) # Отрицательное перемещение Y увеличивает её нормированное смещение.
|
||
self.assertEqual((view.width, view.height), (moved.width, moved.height)) # Перемещение не должно самопроизвольно менять достигнутое увеличение.
|
||
for direction in (-1e15, 1e15): # Большое перемещение в обе стороны проверяет ограничение краями полного диапазона.
|
||
edge = moved.transform(self.core, pan_x=direction, pan_y=direction) # Заведомо выходим за исходные границы по каждой оси.
|
||
self.assertGreaterEqual(min(edge.x, edge.y), 0) # Начало области остаётся внутри диапазона, а не уходит в отрицательные координаты.
|
||
self.assertLessEqual(max(edge.x + edge.width, edge.y + edge.height), 1) # Конец области не выходит за исходные данные.
|
||
self.assertEqual(Viewport(), moved.transform(self.core, zoom_x=1e-15, zoom_y=1e-15)) # Уменьшение с большим запасом возвращает полный диапазон и нулевые смещения.
|
||
|
||
def test_invalid_numeric_inputs_do_not_escape_to_painter(self):
|
||
for value in (math.nan, math.inf, -math.inf):
|
||
self.assertEqual(Viewport(), Viewport().transform(self.core, zoom_x=value))
|
||
with self.assertRaises(ValueError):
|
||
self.core.pinch_axis(value, 10, 8)
|
||
|
||
def test_locked_viewport_and_decibel_delta(self):
|
||
locked = Viewport(locked=True)
|
||
self.assertEqual(locked, locked.transform(self.core, zoom_x=2, pan_y=.2))
|
||
self.assertAlmostEqual(20, self.core.db_delta(1, 10))
|
||
self.assertAlmostEqual(-20, self.core.db_delta(10, 1))
|
||
self.assertIsNone(self.core.db_delta(0, 1))
|
||
self.assertEqual(Bounds(0, 500, -2, 2), self.core.limits(0, 500, -2, 2))
|
||
with self.assertRaises(ValueError):
|
||
self.core.limits(1, 1, -2, 2)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
unittest.main()
|