zcbot/tests/test_origin_worker.py

1111 lines
37 KiB
Python

from __future__ import annotations
import importlib.util
import json
import tempfile
import unittest
import zlib
from pathlib import Path
from unittest.mock import patch
WORKER_PATH = (
Path(__file__).resolve().parents[1]
/ "windows-node"
/ "adapters"
/ "origin.plot@v2"
/ "worker.py"
)
ADAPTER_MANIFEST_PATH = (
Path(__file__).resolve().parents[1]
/ "windows-node"
/ "adapters"
/ "origin.plot@v2"
/ "adapter.json"
)
SPEC = importlib.util.spec_from_file_location("zcbot_origin_worker", WORKER_PATH)
assert SPEC and SPEC.loader
worker = importlib.util.module_from_spec(SPEC)
SPEC.loader.exec_module(worker)
class OriginWorkerUnitTests(unittest.TestCase):
@staticmethod
def _request() -> dict:
return {
"inputs": [{"key": "sample"}],
"operation": {
"plot": {
"type": "line",
"series": [{"input": "sample", "x": "x", "y": "y"}],
}
},
"outputs": [{"key": "figure_png", "format": "png"}],
}
def test_worker_version_matches_adapter_manifest(self) -> None:
manifest = json.loads(ADAPTER_MANIFEST_PATH.read_text(encoding="utf-8"))
self.assertEqual(worker.ADAPTER_VERSION, manifest["adapter_version"])
def test_registered_origin_version_comes_from_com_server_executable(self) -> None:
class FakeKey:
def __init__(self, path: str):
self.path = path
def __enter__(self):
return self
def __exit__(self, *_):
return None
class FakeRegistry:
HKEY_CLASSES_ROOT = object()
values = {
r"Origin.ApplicationSI\CLSID": "{origin-clsid}",
r"CLSID\{origin-clsid}\LocalServer32": (
r'"C:\Program Files\OriginLab\Origin2025\Origin64.exe" /Automation'
),
}
@classmethod
def OpenKey(cls, _root, path):
return FakeKey(path)
@classmethod
def QueryValueEx(cls, key, _name):
return cls.values[key.path], 1
with patch.object(worker, "_windows_file_version", return_value="2025") as read:
self.assertEqual(worker._registered_origin_version(FakeRegistry), "2025")
read.assert_called_once_with(Path(r"C:\Program Files\OriginLab\Origin2025\Origin64.exe"))
FakeRegistry.values[r"CLSID\{origin-clsid}\LocalServer32"] = "invalid"
self.assertIsNone(worker._registered_origin_version(FakeRegistry))
def test_csv_and_json_inputs_are_read_without_origin(self) -> None:
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
csv_path = root / "input.csv"
csv_path.write_text("x,y\n1,2\n3,4\n", encoding="utf-8")
self.assertEqual(
worker._read_rows(csv_path, None),
(["x", "y"], [["1", "2"], ["3", "4"]]),
)
json_path = root / "input.json"
json_path.write_text(json.dumps([{"x": 1, "y": 2}, {"x": 3, "y": 4}]), encoding="utf-8")
self.assertEqual(worker._read_rows(json_path, None), (["x", "y"], [[1, 2], [3, 4]]))
def test_manifest_uses_stable_id_and_streaming_digest(self) -> None:
with tempfile.TemporaryDirectory() as directory:
path = Path(directory) / "plot-spec.json"
path.write_text("{}", encoding="utf-8")
manifest = worker._manifest(path, "application/json")
self.assertEqual(manifest["artifact_id"], "plot_spec")
self.assertEqual(manifest["sha256"], worker._file_sha256(path))
self.assertEqual(manifest["size_bytes"], 2)
def test_axis_title_includes_units(self) -> None:
self.assertEqual(
worker._axis_title({"title": "Stress", "unit": "MPa"}, "Y"), "Stress (MPa)"
)
self.assertEqual(worker._axis_title(None, "Time"), "Time")
def test_layout_helpers_apply_validated_values(self) -> None:
class FakeAxis:
title = ""
scale = "linear"
limits = None
def set_limits(self, begin, end, step):
self.limits = (begin, end, step)
class FakeLayer:
def __init__(self):
self.axes = {name: FakeAxis() for name in ("x", "y", "z")}
self.values = {}
self.labels = {}
def axis(self, name):
return self.axes[name]
def set_float(self, name, value):
self.values[name] = value
def set_int(self, name, value):
self.values[name] = value
def label(self, name):
class FakeLabel:
def set_int(_, prop, value):
self.labels[(name, prop)] = value
return FakeLabel()
layer = FakeLayer()
worker._apply_axis(
layer,
"x",
{
"title": "Time",
"unit": "d",
"scale": "log10",
"minimum": 1,
"maximum": 100,
"major_step": 1,
"tick_label_angle": 45,
"tick_label_font_size": 10,
"title_font_size": 12,
"grid": "major_minor",
},
"X",
)
self.assertEqual(layer.axes["x"].title, "Time (d)")
self.assertEqual(layer.axes["x"].scale, "log10")
self.assertEqual(layer.axes["x"].limits, (1, 100, 1))
self.assertEqual(
layer.values,
{
"x.label.rotate": 45,
"x.label.pt": 10,
"x.grid.show": 3,
},
)
self.assertEqual(layer.labels, {("xb", "fsize"): 12})
def test_rescale_applies_log_scales_before_deriving_limits(self) -> None:
class FakeAxis:
def __init__(self, name, events):
self.name = name
self.events = events
self._scale = "linear"
@property
def scale(self):
return self._scale
@scale.setter
def scale(self, value):
self._scale = value
self.events.append(("scale", self.name, value))
class FakeLayer:
def __init__(self):
self.events = []
self.axes = {name: FakeAxis(name, self.events) for name in ("x", "y")}
def axis(self, name):
return self.axes[name]
def rescale(self):
self.events.append(("rescale",))
layer = FakeLayer()
worker._rescale_with_axis_scales(
layer,
[
("x", {"scale": "log10", "minimum": 1, "maximum": 100}),
("y", {"scale": "ln"}),
],
)
self.assertEqual(
layer.events,
[
("scale", "x", "log10"),
("scale", "y", "ln"),
("rescale",),
],
)
def test_rescale_does_not_apply_explicit_limits_before_rescaling(self) -> None:
class FakeAxis:
scale = "linear"
def set_limits(self, *_):
raise AssertionError("limits must be applied after rescale")
class FakeLayer:
def __init__(self):
self.axis_value = FakeAxis()
self.rescaled = False
def axis(self, _name):
return self.axis_value
def rescale(self):
self.rescaled = True
layer = FakeLayer()
worker._rescale_with_axis_scales(
layer,
[
("x", {"minimum": 1, "maximum": 100}),
],
)
self.assertTrue(layer.rescaled)
self.assertEqual(layer.axis_value.scale, "linear")
def test_series_style_maps_to_origin_properties(self) -> None:
class FakePlot:
def __init__(self):
self.values = {}
self.color = None
self.symbol_kind = None
self.symbol_size = None
self.transparency = None
def set_float(self, name, value):
self.values[name] = value
def set_int(self, name, value):
self.values[name] = value
plot = FakePlot()
worker._apply_series_style(
plot,
{
"color": "#3366CC",
"line_width": 1.5,
"line_style": "dash_dot",
"symbol": "diamond",
"symbol_size": 8,
"transparency": 20,
},
)
self.assertEqual(plot.color, (51, 102, 204))
self.assertEqual(plot.values, {"line.width": 1.5, "line.type": 4})
self.assertEqual(plot.symbol_kind, 5)
self.assertEqual(plot.symbol_size, 8)
self.assertEqual(plot.transparency, 20)
def test_origin_symbol_ids_match_labtalk_values(self) -> None:
self.assertEqual(
worker.SYMBOLS,
{
"square": 1,
"circle": 2,
"triangle_up": 3,
"diamond": 5,
"plus": 6,
"cross": 7,
},
)
def test_keyed_input_directory_requires_exactly_one_file(self) -> None:
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
keyed = root / "input" / "sample"
keyed.mkdir(parents=True)
path = keyed / "data.csv"
path.write_text("x,y\n1,2\n", encoding="utf-8")
self.assertEqual(worker._input_file(root, "sample"), path)
(keyed / "extra.csv").write_text("x,y\n3,4\n", encoding="utf-8")
with self.assertRaisesRegex(ValueError, "INPUT_FILE_COUNT_INVALID:sample"):
worker._input_file(root, "sample")
def test_series_are_resolved_against_each_input(self) -> None:
input_data = {
"first": (["time", "strength"], [[1, 10]]),
"second": (["temperature", "value"], [[20, 30]]),
}
series = [
{"input": "first", "x": "time", "y": "strength", "label": "7 d"},
{"input": "second", "x": "temperature", "y": "value", "label": "28 d"},
]
resolved, labels = worker._resolve_series(input_data, series)
self.assertEqual(
resolved,
[
{"input": "first", "label": "7 d", "x": 0, "y": 1},
{"input": "second", "label": "28 d", "x": 0, "y": 1},
],
)
self.assertEqual(labels, {("first", 1): "7 d", ("second", 1): "28 d"})
def test_shared_y_column_rejects_conflicting_labels(self) -> None:
input_data = {"sample": (["x1", "x2", "y"], [[1, 2, 3]])}
with self.assertRaisesRegex(ValueError, "SERIES_LABEL_CONFLICT"):
worker._resolve_series(
input_data,
[
{"input": "sample", "x": "x1", "y": "y", "label": "First"},
{"input": "sample", "x": "x2", "y": "y", "label": "Second"},
],
)
def test_worker_owns_cross_field_semantic_validation(self) -> None:
request = self._request()
worker._validate_semantics(request)
request["operation"]["plot"]["x_axis"] = {
"scale": "log10",
"minimum": 0,
"maximum": 100,
}
with self.assertRaisesRegex(ValueError, "X_AXIS_LOG_LIMIT_INVALID"):
worker._validate_semantics(request)
request = self._request()
request["inputs"].append({"key": "unused"})
with self.assertRaisesRegex(ValueError, "INPUT_BINDINGS_MUST_BE_USED_EXACTLY"):
worker._validate_semantics(request)
def test_multi_panel_semantics_cover_layout_axes_and_total_series(self) -> None:
request = {
"inputs": [{"key": "sample"}],
"operation": {
"plot": {
"type": "multi_panel",
"layout": {"rows": 1, "columns": 2, "share_x": True},
"panels": [
{
"key": "strength",
"series": [
{
"input": "sample",
"x": "age",
"y": "strength",
"kind": "scatter",
"y_error": "sd",
}
],
},
{
"key": "thermal",
"series": [
{
"input": "sample",
"x": "temperature",
"y": "tg",
"kind": "line",
},
{
"input": "sample",
"x": "temperature",
"y": "dtg",
"kind": "line",
"y_axis": "right",
},
],
"right_y_axis": {"title": "DTG"},
},
],
}
},
"outputs": [{"key": "figure_png", "format": "png"}],
}
worker._validate_semantics(request)
request["operation"]["plot"]["layout"] = {"rows": 1, "columns": 1}
with self.assertRaisesRegex(ValueError, "PANEL_LAYOUT_TOO_SMALL"):
worker._validate_semantics(request)
request["operation"]["plot"]["layout"] = {
"rows": 1,
"columns": 2,
"share_x": True,
}
request["operation"]["plot"]["panels"][0]["x_axis"] = {
"scale": "log10",
"minimum": 0,
}
with self.assertRaisesRegex(ValueError, "X_AXIS_LOG_LIMIT_INVALID"):
worker._validate_semantics(request)
request["operation"]["plot"]["panels"][0]["x_axis"] = {"minimum": 1}
with self.assertRaisesRegex(ValueError, "SHARED_X_AXIS_CONFIG_MISMATCH"):
worker._validate_semantics(request)
request["operation"]["plot"]["panels"][0].pop("x_axis")
request["operation"]["plot"]["x_axis"] = {
"scale": "log10",
"minimum": 0,
}
with self.assertRaisesRegex(ValueError, "X_AXIS_LOG_LIMIT_INVALID"):
worker._validate_semantics(request)
def test_recipe_uses_the_composition_semantics(self) -> None:
request = {
"inputs": [{"key": "sample"}],
"operation": {
"plot": {
"type": "recipe",
"recipe_version": 1,
"layout": {"rows": 1, "columns": 1},
"panels": [
{
"key": "main",
"series": [
{
"input": "sample",
"x": "age",
"y": "strength",
"kind": "line_scatter",
}
],
}
],
}
},
"outputs": [{"key": "figure_png", "format": "png"}],
}
worker._validate_semantics(request)
self.assertIn("recipe", worker.COMPOSITION_PLOT_TYPES)
def test_panel_settings_inherit_plot_defaults_and_allow_overrides(self) -> None:
plot = {
"x_axis": {"title": "Age", "unit": "d"},
"y_axis": {"title": "Value"},
"legend": {"enabled": False, "font_size": 8},
}
panel = {
"y_axis": {"title": "Strength", "unit": "MPa"},
"legend": {"enabled": True},
}
self.assertEqual(
worker._panel_setting(plot, panel, "x_axis"),
{"title": "Age", "unit": "d"},
)
self.assertEqual(
worker._panel_setting(plot, panel, "y_axis"),
{"title": "Strength", "unit": "MPa"},
)
self.assertEqual(
worker._panel_legend(plot, panel),
{
"enabled": True,
"position": "top_right",
"font_size": 8,
},
)
def test_panel_y_axis_fallback_prefers_title_then_column_name(self) -> None:
series = [{"y": "CS_20C"}]
self.assertEqual(
worker._panel_y_axis_fallback({"title": "抗压强度 (MPa)"}, series),
"抗压强度 (MPa)",
)
self.assertEqual(
worker._panel_y_axis_fallback({}, series),
"CS_20C",
)
def test_mixed_xy_series_pass_kind_axis_and_error_columns_to_origin(self) -> None:
class FakePlot:
pass
class FakeLayer:
def __init__(self):
self.calls = []
self.grouped = False
self.plots = []
def add_plot(self, worksheet, **arguments):
self.calls.append((worksheet, arguments))
plot = FakePlot()
self.plots.append(plot)
if "colxerr" in arguments:
self.plots.append(FakePlot())
if "colyerr" in arguments:
self.plots.append(FakePlot())
return plot
def plot_list(self):
return self.plots
def group(self, enabled=True):
self.grouped = enabled
layer = FakeLayer()
series = [
{
"input": "sample",
"x": "age",
"y": "strength",
"kind": "scatter",
"x_error": "age_sd",
"y_error": "strength_sd",
},
{"input": "sample", "x": "age", "y": "fit", "kind": "line"},
]
resolved = [
{"input": "sample", "x": 0, "y": 1, "x_error": 2, "y_error": 3},
{"input": "sample", "x": 0, "y": 4},
]
worker._add_xy_plots(layer, {"sample": "worksheet"}, series, resolved)
self.assertEqual(
layer.calls,
[
(
"worksheet",
{"coly": 1, "colx": 0, "type": "s", "colxerr": 2, "colyerr": 3},
),
("worksheet", {"coly": 4, "colx": 0, "type": "l"}),
],
)
self.assertFalse(layer.grouped)
def test_series_support_xyz_and_error_roles(self) -> None:
resolved, labels = worker._resolve_series(
{"sample": (["x", "y", "z", "x_sd", "y_sd"], [[0, 1, 2, 0.1, 0.2]])},
[
{
"input": "sample",
"x": "x",
"y": "y",
"z": "z",
"x_error": "x_sd",
"y_error": "y_sd",
"label": "测量值",
}
],
)
self.assertEqual(
resolved,
[
{
"input": "sample",
"label": "测量值",
"x": 0,
"y": 1,
"z": 2,
"x_error": 3,
"y_error": 4,
}
],
)
self.assertEqual(labels, {("sample", 1): "测量值"})
def test_series_support_second_batch_roles(self) -> None:
resolved, _ = worker._resolve_series(
{
"sample": (
["x", "mean", "size", "lower", "upper"],
[[0, 1, 2, 0.5, 1.5]],
)
},
[
{
"input": "sample",
"x": "x",
"y": "mean",
"size": "size",
"lower": "lower",
"upper": "upper",
}
],
)
self.assertEqual(
resolved,
[
{
"input": "sample",
"label": None,
"x": 0,
"y": 1,
"size": 2,
"lower": 3,
"upper": 4,
}
],
)
def test_second_batch_value_validation_rejects_invalid_data(self) -> None:
input_data = {"sample": (["x", "y", "size", "lower", "upper"], [[0, 1, 0, 2, 1]])}
resolved = [
{
"input": "sample",
"x": 0,
"y": 1,
"size": 2,
"lower": 3,
"upper": 4,
}
]
with self.assertRaisesRegex(ValueError, "SIZE_VALUE_NOT_POSITIVE"):
worker._validate_bubble_values(input_data, resolved)
with self.assertRaisesRegex(ValueError, "BAND_BOUNDS_INVERTED"):
worker._validate_band_values(input_data, resolved)
def test_band_graph_adds_bounds_fill_and_center_line(self) -> None:
class FakePlot:
def __init__(self):
self.fill = None
def set_fill_area(self, **arguments):
self.fill = arguments
class FakeLayer:
def __init__(self):
self.calls = []
def add_plot(self, worksheet, **arguments):
plot = FakePlot()
self.calls.append((worksheet, arguments, plot))
return plot
class FakeGraph:
def __init__(self):
self.layer = FakeLayer()
def __getitem__(self, index):
self.assert_index = index
return self.layer
class FakeOp:
def __init__(self):
self.graph = FakeGraph()
def new_graph(self, *, template):
self.template = template
return self.graph
op = FakeOp()
graph, layer, centers, legend = worker._build_band_graph(
op,
{"sample": "worksheet"},
[{"input": "sample", "x": "x", "y": "mean", "label": "95% CI"}],
[{"input": "sample", "x": 0, "y": 1, "lower": 2, "upper": 3}],
)
self.assertIs(graph, op.graph)
self.assertIs(layer, op.graph.layer)
self.assertEqual(op.template, "line")
self.assertEqual([item[1]["coly"] for item in layer.calls], [3, 2, 1])
self.assertEqual(layer.calls[0][2].fill, {"type": 9})
self.assertEqual(centers, [layer.calls[2][2]])
self.assertEqual(legend, r"\l(3) 95% CI")
def test_second_batch_plot_ids_and_templates_are_fixed(self) -> None:
self.assertEqual(worker.PLOT_CONFIG["box"], ("box", 206))
self.assertEqual(worker.PLOT_CONFIG["histogram"], ("hist", 219))
self.assertEqual(worker.PLOT_CONFIG["stacked_column"], ("StackColumn", 213))
self.assertEqual(worker.PLOT_CONFIG["bubble"], ("scatter", "s"))
def test_bubble_execution_parameters(self) -> None:
class FakeOp:
@staticmethod
def modi_col(offset):
return ("modifier", offset)
class FakePlot:
symbol_size = None
symbol_sizefactor = None
plot = FakePlot()
worker._configure_bubble_plot(
FakeOp(), plot, {"style": {"symbol_size": 12}}, {"y": 1, "size": 4}
)
self.assertEqual(plot.symbol_size, ("modifier", 3))
self.assertEqual(plot.symbol_sizefactor, 12)
def test_stacked_graph_uses_one_native_xyy_range(self) -> None:
class FakeSheet:
def __init__(self):
self.columns = []
def from_list(self, index, values, *, lname):
self.columns.append((index, values, lname))
@staticmethod
def lt_range(_use_name):
return "[Book1]1"
class FakeLayer:
def __init__(self):
self.calls = []
self.obj = self
def add_plot(self, data_range, *, type):
self.calls.append((data_range, type))
def activate(self):
self.activated = True
def group(self, enabled, begin, end):
self.group_call = (enabled, begin, end)
def LT_execute(self, command):
self.command = command
@staticmethod
def plot_list():
return ["a", "b"]
class FakeGraph:
def __init__(self):
self.layer = FakeLayer()
def __getitem__(self, index):
self.index = index
return self.layer
class FakeOp:
def __init__(self):
self.sheet = FakeSheet()
self.graph = FakeGraph()
def new_sheet(self, kind, *, lname):
self.sheet_call = (kind, lname)
return self.sheet
def new_graph(self, *, template):
self.template = template
return self.graph
op = FakeOp()
graph, layer, plots = worker._build_stacked_graph(
op,
{"sample": (["x", "a", "b"], [[1, 10, 5], [2, 12, 6]])},
[
{"input": "sample", "x": "x", "y": "a", "label": "A"},
{"input": "sample", "x": "x", "y": "b", "label": "B"},
],
[
{"input": "sample", "x": 0, "y": 1},
{"input": "sample", "x": 0, "y": 2},
],
"StackColumn",
213,
)
self.assertIs(graph, op.graph)
self.assertIs(layer, op.graph.layer)
self.assertEqual(plots, ["a", "b"])
self.assertEqual(op.template, "StackColumn")
self.assertEqual(layer.calls, [("[Book1]1!(1,2:3)", 213)])
self.assertEqual(layer.group_call, (True, 0, 1))
self.assertTrue(layer.activated)
self.assertEqual(layer.command, "layer -b s 1")
self.assertEqual(
op.sheet.columns[1:],
[
(1, [10, 12], "A"),
(2, [5, 6], "B"),
],
)
def test_stacked_graph_rejects_mismatched_x_values(self) -> None:
with self.assertRaisesRegex(ValueError, "STACKED_PLOT_X_VALUES_MISMATCH"):
worker._build_stacked_graph(
object(),
{
"a": (["x", "y"], [[1, 10], [2, 12]]),
"b": (["x", "y"], [[1, 5], [3, 6]]),
},
[
{"input": "a", "x": "x", "y": "y"},
{"input": "b", "x": "x", "y": "y"},
],
[
{"input": "a", "x": 0, "y": 1},
{"input": "b", "x": 0, "y": 1},
],
"StackColumn",
213,
)
def test_third_batch_plot_ids_and_templates_are_fixed(self) -> None:
self.assertEqual(worker.PLOT_CONFIG["area"], ("area", 204))
self.assertEqual(worker.PLOT_CONFIG["stacked_area"], ("stackarea", 214))
self.assertEqual(worker.PLOT_CONFIG["polar"], ("polar", 192))
self.assertEqual(worker.PLOT_CONFIG["pie"], ("pie", 225))
self.assertEqual(worker.PLOT_CONFIG["stacked_bar"], ("bar", 216))
def test_stacked_legend_only_contains_requested_series(self) -> None:
class Label:
text = ""
class Layer:
legend = Label()
def label(self, name):
self.name = name
return self.legend
layer = Layer()
worker._replace_series_legend(
layer,
[
{"y": "a", "label": "Phase A"},
{"y": "phase_b"},
],
)
self.assertEqual(layer.name, "Legend")
self.assertEqual(layer.legend.text, r"\l(1) Phase A" "\n" r"\l(2) phase_b")
def test_multi_panel_legend_uses_actual_data_plot_indices(self) -> None:
class Label:
text = ""
class Layer:
legend = Label()
def label(self, _name):
return self.legend
class Plot:
def __init__(self, value):
self.value = value
def index(self):
return self.value
layer = Layer()
worker._replace_series_legend(
layer,
[
{"y": "a", "label": "5 C"},
{"y": "b", "label": "20 C"},
],
[Plot(0), Plot(2)],
)
self.assertEqual(
layer.legend.text,
r"\l(1) 5 C" "\n" r"\l(3) 20 C",
)
def test_panel_title_and_legend_use_page_pixel_coordinates(self) -> None:
class Label:
def __init__(self):
self.values = {}
self.show = None
def set_int(self, name, value):
self.values[name] = value
def set_float(self, name, value):
self.values[name] = value
class Layer:
def __init__(self):
self.legend = Label()
self.title = None
def label(self, _name):
return self.legend
def add_label(self, _text):
self.title = Label()
return self.title
@staticmethod
def get_float(name):
return {
"left": 10,
"top": 20,
"width": 40,
"height": 30,
}[name]
class Graph:
@staticmethod
def get_float(name):
return {
"width": 1800,
"height": 1450,
"resx": 254,
"resy": 254,
}[name]
layer = Layer()
graph = Graph()
worker._apply_legend(
layer,
{"enabled": True, "position": "top_right"},
attach_to_layer=True,
graph=graph,
geometry=(10, 20, 40, 30),
)
worker._apply_panel_title(
graph,
layer,
(10, 20, 40, 30),
"Strength",
{"font_size": 12},
)
self.assertEqual(layer.legend.values["attach"], 0)
self.assertEqual(layer.legend.values["background"], 0)
self.assertEqual(layer.legend.values["smartpos"], 1)
self.assertNotIn("left", layer.legend.values)
self.assertNotIn("top", layer.legend.values)
self.assertEqual(layer.title.values["attach"], 1)
self.assertEqual(layer.title.values["background"], 0)
self.assertEqual(layer.title.values["fsize"], 12)
self.assertEqual(layer.title.values["left"], 382)
self.assertEqual(layer.title.values["top"], 229)
def test_origin_session_disables_printed_text_baselines(self) -> None:
class Origin:
def __init__(self):
self.variables = []
def set_lt_var(self, name, value):
self.variables.append((name, value))
origin = Origin()
worker._configure_origin_session(origin)
self.assertEqual(origin.variables, [("@U", 1)])
def test_svg_normalizer_removes_only_text_baseline_path(self) -> None:
source = (
b'<svg viewBox="0,0 100,100" width="100" height="100">'
b'<text x="10" y="30" font-size="20">Title</text>\n'
b'<path d="M 10,14.4 L 70,14.4" '
b'style="stroke: black;stroke-width: 2;stroke-linecap:butt" '
b'fill="none"/>'
b'<path d="M 0,90 L 100,90" '
b'style="stroke: black;stroke-width: 2" fill="none"/>'
b'</svg>'
)
normalized, baselines = worker._strip_origin_svg_text_baselines(source)
self.assertEqual(len(baselines), 1)
self.assertNotIn(b'M 10,14.4 L 70,14.4', normalized)
self.assertIn(b'M 0,90 L 100,90', normalized)
def test_pdf_normalizer_preserves_stream_size_and_removes_baseline(self) -> None:
decoded = b"4 w\n0 J\n10 20 m\n90 20 l\nS\nQ\n"
compressed = zlib.compress(decoded)
original = b"%PDF-1.2\nstream\n" + compressed + b"\nendstream\n%%EOF"
with tempfile.TemporaryDirectory() as temp_dir:
path = Path(temp_dir) / "figure.pdf"
path.write_bytes(original)
removed = worker._strip_origin_pdf_text_baselines(path)
content = path.read_bytes()
start = content.index(b"stream\n") + len(b"stream\n")
end = content.index(b"\nendstream", start)
normalized = zlib.decompress(content[start:end])
self.assertEqual(removed, 1)
self.assertNotIn(b"10 20 m", normalized)
self.assertEqual(len(content), len(original))
def test_origin_arranges_panel_layers_and_reports_resulting_geometry(self) -> None:
class Layer:
def __init__(self, geometry):
self.geometry = geometry
self.values = {}
self.commands = []
self.activated = False
self.obj = self
def set_int(self, name, value):
self.values[name] = value
def activate(self):
self.activated = True
def LT_execute(self, command):
self.commands.append(command)
def get_float(self, name):
return self.geometry[name]
layers = [
Layer({"left": 9, "top": 14, "width": 38, "height": 32}),
Layer({"left": 58, "top": 14, "width": 38, "height": 32}),
Layer({"left": 9, "top": 56, "width": 38, "height": 32}),
Layer({"left": 58, "top": 56, "width": 38, "height": 32}),
]
geometries = worker._arrange_panel_layers(layers, (2, 2))
self.assertTrue(layers[0].activated)
self.assertTrue(all(layer.values["unit"] == 1 for layer in layers))
self.assertEqual(
layers[0].commands,
["layarrange row:=2 col:=2 left:=9 right:=4 top:=14 bottom:=12 xgap:=11 ygap:=10;"],
)
self.assertEqual(
geometries,
[
(9, 14, 38, 32),
(58, 14, 38, 32),
(9, 56, 38, 32),
(58, 56, 38, 32),
],
)
def test_shared_x_hides_upper_row_title_and_tick_labels(self) -> None:
class Title:
show = True
class Layer:
def __init__(self):
self.title = Title()
self.commands = []
self.obj = self
def label(self, name):
return self.title if name == "xb" else None
def activate(self):
return None
def LT_execute(self, command):
self.commands.append(command)
upper = Layer()
lower = Layer()
worker._apply_shared_x_presentation(upper, panel_index=0, rows=2, columns=2, share_x=True)
worker._apply_shared_x_presentation(lower, panel_index=2, rows=2, columns=2, share_x=True)
self.assertFalse(upper.title.show)
self.assertEqual(upper.commands, ["axis -ps X L 0;"])
self.assertTrue(lower.title.show)
self.assertEqual(lower.commands, [])
def test_auto_legend_delegates_positioning_to_origin(self) -> None:
class Label:
def __init__(self):
self.values = {}
self.show = None
def set_int(self, name, value):
self.values[name] = value
class Layer:
def __init__(self):
self.legend = Label()
def label(self, _name):
return self.legend
layer = Layer()
worker._apply_legend(layer, {"enabled": True, "position": "auto"})
self.assertEqual(layer.legend.values["smartpos"], 1)
self.assertNotIn("left", layer.legend.values)
self.assertNotIn("top", layer.legend.values)
def test_heatmap_matrix_accepts_complete_unordered_grid(self) -> None:
matrix, xy_map = worker._heatmap_matrix(
[[1, 20, 4], [0, 10, 1], [1, 10, 2], [0, 20, 3]],
{"x": 0, "y": 1, "z": 2},
)
self.assertEqual(matrix, [[1.0, 2.0], [3.0, 4.0]])
self.assertEqual(xy_map, (0.0, 1.0, 10.0, 20.0))
def test_heatmap_matrix_rejects_invalid_grid(self) -> None:
with self.assertRaisesRegex(ValueError, "HEATMAP_GRID_INCOMPLETE"):
worker._heatmap_matrix(
[[0, 10, 1], [1, 10, 2], [0, 20, 3]],
{"x": 0, "y": 1, "z": 2},
)
with self.assertRaisesRegex(ValueError, "HEATMAP_COORDINATES_DUPLICATED"):
worker._heatmap_matrix([[0, 10, 1], [0, 10, 2]], {"x": 0, "y": 1, "z": 2})
with self.assertRaisesRegex(ValueError, "HEATMAP_GRID_NOT_REGULAR"):
worker._heatmap_matrix(
[
[0, 10, 1],
[1, 10, 2],
[3, 10, 3],
[0, 20, 4],
[1, 20, 5],
[3, 20, 6],
],
{"x": 0, "y": 1, "z": 2},
)
if __name__ == "__main__":
unittest.main()