480 lines
25 KiB
Python
480 lines
25 KiB
Python
"""Real native associative drawing BOM and persistence regressions."""
|
|
import hashlib
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
import traceback
|
|
|
|
import FreeCAD as App
|
|
import TechDraw
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
sys.path.insert(0, str(ROOT / "src" / "QInventor"))
|
|
import qi_model as model
|
|
import qi_drawing_bom as service
|
|
import bom_suite as kernel
|
|
|
|
|
|
def _public(plan):
|
|
return {key: value for key, value in plan.items() if key not in ("sources", "geometry_views")}
|
|
|
|
|
|
def _fixture(name, flexible=True):
|
|
values = kernel.nested_fixture(name, flexible=flexible)
|
|
doc = values[0]
|
|
return values, model.create_drawing(doc)
|
|
|
|
|
|
class _TracingProxy(service.DrawingBOMProxy):
|
|
def __init__(self, sheet):
|
|
self.depth = self.maximum_depth = self.calls = 0
|
|
self.returns = []
|
|
super().__init__(sheet)
|
|
|
|
def execute(self, sheet):
|
|
self.depth += 1
|
|
self.maximum_depth = max(self.maximum_depth, self.depth)
|
|
self.calls += 1
|
|
try:
|
|
answer = super().execute(sheet)
|
|
self.returns.append(answer)
|
|
return answer
|
|
finally:
|
|
self.depth -= 1
|
|
|
|
|
|
def reopen_check(file, output):
|
|
output = Path(output)
|
|
report = {"passed": False, "gui_up": bool(App.GuiUp), "service_path": service.__file__,
|
|
"service_sha256": hashlib.sha256(Path(service.__file__).read_bytes()).hexdigest()}
|
|
try:
|
|
assert not App.GuiUp
|
|
doc = App.openDocument(str(file))
|
|
doc.UndoMode = 1
|
|
page = doc.getObject("Drawing")
|
|
view = service.table_for_page(page)
|
|
assert view.TypeId == "TechDraw::DrawViewSpreadsheet"
|
|
assert view.Source.TypeId == "Spreadsheet::SheetPython"
|
|
assert isinstance(view.Source.Proxy, service.DrawingBOMProxy)
|
|
doc.recompute()
|
|
original = service.validate_table(page)
|
|
kernel._near(original["summary"]["mass_kg"], .006342)
|
|
assert view.Source.get("H2") == "Сохраняемое примечание"
|
|
bolt = next(obj for obj in doc.Objects if getattr(obj, "QIPartNumber", "") == "BOLT")
|
|
model.set_dimensions(bolt, {"QIHeight": 8})
|
|
updated = service.validate_table(page)
|
|
kernel._near(updated["summary"]["mass_kg"], .007284)
|
|
assert "0,007284" in view.Symbol
|
|
doc.undo(); doc.recompute()
|
|
kernel._near(service.validate_table(page)["summary"]["mass_kg"], .006342)
|
|
assert doc.RedoCount == 1
|
|
doc.redo(); doc.recompute()
|
|
kernel._near(service.validate_table(page)["summary"]["mass_kg"], .007284)
|
|
report.update(passed=True, restored_proxy=type(view.Source.Proxy).__name__,
|
|
initial=_public(original), edited=_public(updated), undo_redo=True)
|
|
except Exception:
|
|
report["error"] = traceback.format_exc()
|
|
finally:
|
|
for name in list(App.listDocuments()):
|
|
App.closeDocument(name)
|
|
output.write_text(json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
return report
|
|
|
|
|
|
def _reopen_child(file, output):
|
|
directory = Path(tempfile.mkdtemp(prefix="drawing-bom-reopen-", dir=str(output)))
|
|
macro = directory / "reopen.FCMacro"
|
|
report_path = directory / "report.json"
|
|
macro.write_text("import sys\nsys.path.insert(0," + repr(str(Path(__file__).parent)) + ")\n"
|
|
"import drawing_bom_suite\ndrawing_bom_suite.reopen_check(" + repr(str(file)) + ","
|
|
+ repr(str(report_path)) + ")\n", encoding="utf-8")
|
|
engine = Path(App.getHomePath()) / "bin" / "FreeCADCmd.exe"
|
|
args = [str(engine), "--user-cfg", str(directory / "user.cfg"), "--system-cfg", str(directory / "system.cfg"),
|
|
"--log-file", str(directory / "native.log"), str(macro)]
|
|
environment = os.environ.copy()
|
|
environment.pop("QI_TEST", None)
|
|
options = {}
|
|
if os.name == "nt":
|
|
options["creationflags"] = subprocess.CREATE_NO_WINDOW
|
|
startup = subprocess.STARTUPINFO()
|
|
startup.dwFlags |= subprocess.STARTF_USESHOWWINDOW
|
|
startup.wShowWindow = subprocess.SW_HIDE
|
|
options["startupinfo"] = startup
|
|
with (directory / "process.log").open("wb") as stream:
|
|
process = subprocess.Popen(args, cwd=str(directory), env=environment, stdin=subprocess.DEVNULL,
|
|
stdout=stream, stderr=subprocess.STDOUT, **options)
|
|
try:
|
|
code = process.wait(timeout=90)
|
|
except subprocess.TimeoutExpired:
|
|
process.kill(); process.wait(timeout=10)
|
|
raise AssertionError("Owned native FCStd reopen timed out: " + str(directory))
|
|
assert code == 0 and report_path.is_file(), (code, str(directory))
|
|
report = json.loads(report_path.read_text(encoding="utf-8"))
|
|
assert report["passed"] and not report["gui_up"], report
|
|
assert report["service_sha256"] == hashlib.sha256(Path(service.__file__).read_bytes()).hexdigest()
|
|
report.update(native_exit_code=code, report_path=str(report_path), engine=str(engine), native_pid=process.pid)
|
|
return report
|
|
|
|
|
|
def run(output_dir):
|
|
output = Path(output_dir).absolute()
|
|
output.mkdir(parents=True, exist_ok=True)
|
|
report_path = output / "drawing-bom-report.json"
|
|
initial_docs = set(App.listDocuments())
|
|
checks = []
|
|
|
|
def check(name, function):
|
|
report_path.write_text(json.dumps({"running_case": name, "checks": checks}, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
try:
|
|
checks.append({"name": name, "passed": True, "evidence": function()})
|
|
except Exception:
|
|
checks.append({"name": name, "passed": False, "error": traceback.format_exc()})
|
|
|
|
def native_creation_scope_and_dag():
|
|
(doc, root, bolt, housing, sub, a, b, direct), page = _fixture("DrawingBOMNative")
|
|
before = kernel._state(doc)
|
|
draft = service.table_snapshot(page)
|
|
assert draft["sheet_name"] is None and draft["view_name"] is None
|
|
assert kernel._state(doc) == before, "Draft changed real native document/history"
|
|
start_undo = doc.UndoCount
|
|
view = service.create_bom_table(page)
|
|
assert doc.UndoCount == start_undo + 1
|
|
sheet = view.Source
|
|
assert sheet.TypeId == "Spreadsheet::SheetPython" and isinstance(sheet.Proxy, service.DrawingBOMProxy)
|
|
assert sheet not in root.Group and page not in sheet.OutList
|
|
assert set(sheet.QIBOMGeometryViews) == set(v for v in page.Views if v.TypeId == "TechDraw::DrawViewPart")
|
|
assert not any(obj.TypeId == "TechDraw::DrawPage" for obj in sheet.OutList)
|
|
plan = service.validate_table(page)
|
|
assert {row["part_number"]: row["quantity"] for row in plan["rows"]} == {"BOLT": 5, "HOUSING": 2}
|
|
kernel._near(plan["summary"]["mass_kg"], .006342)
|
|
assert "0,006342" in view.Symbol and "0,000942" in view.Symbol
|
|
assert all("Invalid" not in obj.State for obj in (sheet, view))
|
|
before = kernel._state(doc)
|
|
assert service.update_bom_table(page) is view
|
|
doc.recompute()
|
|
assert kernel._state(doc) == before, "No-op changed table/history"
|
|
return {"native_types": [sheet.TypeId, view.TypeId], "draft_is_read_only": True,
|
|
"one_create_undo": True, "no_page_backlink": True, "snapshot": _public(plan)}
|
|
|
|
def source_geometry_labels_formulas_and_undo():
|
|
(doc, root, bolt, housing, sub, a, b, direct), page = _fixture("DrawingBOMUpdate")
|
|
view = service.create_bom_table(page)
|
|
sheet = view.Source
|
|
sheet.set("H3", "3.5")
|
|
sheet.set("H1", "=H3*2")
|
|
sheet.set("H2", "'Сохраняемое примечание")
|
|
sheet.setStyle("H2", "bold", "add")
|
|
tracer = _TracingProxy(sheet)
|
|
doc.recompute()
|
|
assert float(sheet.get("H1")) == 7
|
|
initial_symbol = view.Symbol
|
|
model.set_dimensions(bolt, {"QIHeight": 8})
|
|
kernel._near(service.validate_table(page)["summary"]["mass_kg"], .007284)
|
|
assert view.Symbol != initial_symbol and "0,007284" in view.Symbol
|
|
assert float(sheet.get("H1")) == 7
|
|
doc.undo(); doc.recompute()
|
|
kernel._near(service.validate_table(page)["summary"]["mass_kg"], .006342)
|
|
assert doc.RedoCount == 1
|
|
before = kernel._state(doc)
|
|
doc.recompute()
|
|
assert kernel._state(doc) == before
|
|
doc.redo(); doc.recompute()
|
|
kernel._near(service.validate_table(page)["summary"]["mass_kg"], .007284)
|
|
doc.openTransaction("Обозначение и имя")
|
|
bolt.Label = "=Болт"
|
|
bolt.QIPartNumber = "=BOLT"
|
|
doc.recompute(); doc.commitTransaction()
|
|
plan = service.validate_table(page)
|
|
row_index = next(index for index, row in enumerate(plan["rows"], 2) if row["name"] == "=Болт")
|
|
assert sheet.get("C" + str(row_index)) == "=Болт"
|
|
assert sheet.get("B" + str(row_index)) == "=BOLT"
|
|
assert sheet.getContents("C" + str(row_index)).startswith("'=")
|
|
assert "=Болт" in view.Symbol
|
|
assert sheet.get("H2") == "Сохраняемое примечание"
|
|
assert tracer.maximum_depth == 1 and tracer.calls >= 3 and all(answer is False for answer in tracer.returns)
|
|
service.DrawingBOMProxy(sheet); doc.recompute()
|
|
return {"automatic_source_edit": True, "native_formula_outside_generated_range": sheet.get("H1"),
|
|
"callback_max_depth": tracer.maximum_depth, "all_execute_returns_false": True,
|
|
"label_only_observer_and_literal_formula_prefix": True, "undo_redo_preserved": True}
|
|
|
|
def scope_rebind_and_unrelated_invalid_body():
|
|
(doc, root, bolt, housing, sub, a, b, direct), page = _fixture("DrawingBOMScope")
|
|
view = service.create_bom_table(page)
|
|
previous = view.Source.QIBOMCells
|
|
selected = model.create_box(3, 4, 5, doc)
|
|
model.set_material(selected, "Алюминий")
|
|
other = model.create_box(5, 6, 7, doc)
|
|
other.Tip.setExpression("Length", None); other.Tip.Length = 0
|
|
doc.recompute()
|
|
assert "Invalid" in other.Tip.State
|
|
assert service.validate_table(page)["summary"]["components"] == 7
|
|
geometries = [obj for obj in page.Views if obj.TypeId == "TechDraw::DrawViewPart"]
|
|
doc.openTransaction("Источники проекций")
|
|
for obj in geometries:
|
|
obj.Source = [selected]
|
|
doc.recompute(); doc.commitTransaction()
|
|
kernel._reject(lambda: service.validate_table(page))
|
|
assert view.Source.QIBOMCells == previous
|
|
assert service.update_bom_table(page) is view
|
|
plan = service.validate_table(page)
|
|
assert plan["source_names"] == [selected.Name] and plan["summary"]["components"] == 1
|
|
assert plan["summary"]["materials"] == ["Алюминий"]
|
|
assert page.Template.EditableTexts["Material"] == "Алюминий"
|
|
before = kernel._state(doc)
|
|
geometries[0].Source = [bolt]
|
|
# Capture after the authorized native source edit; service rejection
|
|
# must not open another transaction or replace the last good cells.
|
|
before = kernel._state(doc)
|
|
kernel._reject(lambda: service.update_bom_table(page))
|
|
assert kernel._state(doc) == before
|
|
return {"explicit_rebind_repairs_old_sheet_invalid": True, "unrelated_invalid_body_ignored": True,
|
|
"mixed_projection_sources_rejected_before_mutation": True, "material_scope": "Алюминий"}
|
|
|
|
def last_good_error_and_nonhealing_validation():
|
|
(doc, root, bolt, housing, sub, a, b, direct), page = _fixture("DrawingBOMErrors")
|
|
view = service.create_bom_table(page)
|
|
sheet = view.Source
|
|
original_cells, original_data = sheet.QIBOMCells, sheet.QIBOMData
|
|
bolt.QIDensity = 0; doc.recompute()
|
|
assert "Invalid" in sheet.State and sheet.QIBOMError
|
|
assert sheet.QIBOMCells == original_cells and sheet.QIBOMData == original_data
|
|
kernel._reject(lambda: service.validate_table(page))
|
|
bolt.QIDensity = 7850; doc.recompute()
|
|
service.validate_table(page)
|
|
sheet.set("F2", "'999")
|
|
bad = sheet.getContents("F2")
|
|
kernel._reject(lambda: service.validate_table(page))
|
|
assert sheet.getContents("F2") == bad, "Validation silently healed cells"
|
|
service.update_bom_table(page)
|
|
assert sheet.QIBOMCells == original_cells
|
|
pad = bolt.Tip
|
|
pad.setExpression("Length", None); pad.Length = 0
|
|
doc.recompute()
|
|
assert "Invalid" in pad.State and pad.Shape.isValid()
|
|
assert "Invalid" in pad.State
|
|
kernel._reject(lambda: service.validate_table(page))
|
|
assert sheet.QIBOMCells == original_cells
|
|
return {"density_error_native_invalid": True, "cached_valid_pad_rejected": True,
|
|
"last_good_cells_preserved": True, "validate_does_not_heal": True}
|
|
|
|
def structured_mode_and_overflow_redo():
|
|
(doc, root, bolt, housing, sub, a, b, direct), page = _fixture("DrawingBOMStructured", flexible=False)
|
|
view = service.create_bom_table(page, "structured")
|
|
plan = service.validate_table(page)
|
|
parent = next(row for row in plan["rows"] if row["is_assembly"])
|
|
assert parent["quantity"] == parent["quantity_total"] == 2
|
|
child = next(row for row in plan["rows"] if row["part_number"] == "BOLT" and row["parent_position"])
|
|
assert child["quantity"] == 2 and child["quantity_total"] == 4
|
|
assert "Смеш." in view.Symbol and parent["material"] == "Смешанные материалы"
|
|
sheet = view.Source
|
|
structured_e3 = sheet.get("E3")
|
|
assert structured_e3 == "4" and sheet.getTypeIdOfProperty("E3") == "App::PropertyString"
|
|
assert sheet.getTypeIdOfProperty("D3") == "App::PropertyInteger"
|
|
service.update_bom_table(page, "flat")
|
|
flat_e3 = sheet.get("E3")
|
|
assert flat_e3 == "Алюминий" and sheet.getTypeIdOfProperty("E3") == "App::PropertyString"
|
|
doc.undo()
|
|
# Inspect the real evaluated cell before base/native recompute can
|
|
# recreate a property lost by an unsafe Spreadsheet type restoration.
|
|
assert str(sheet.QIBOMMode) == "structured" and sheet.get("E3") == structured_e3
|
|
assert sheet.getTypeIdOfProperty("E3") == "App::PropertyString" and doc.RedoCount == 1
|
|
doc.recompute()
|
|
service.validate_table(page)
|
|
doc.redo()
|
|
assert str(sheet.QIBOMMode) == "flat" and sheet.get("E3") == flat_e3
|
|
assert sheet.getTypeIdOfProperty("E3") == "App::PropertyString"
|
|
doc.recompute()
|
|
service.validate_table(page)
|
|
for index in range(2):
|
|
extra = model.create_box(1 + index, 2, 3, doc)
|
|
extra.Label = "Шайба" + str(index)
|
|
extra.QIPartNumber = "EXTRA" + str(index)
|
|
doc.getObject("Definitions").addObject(extra)
|
|
kernel._link(sub, extra, "Extra" + str(index), 30 + index * 10)
|
|
doc.recompute()
|
|
assert len(service.validate_table(page)["rows"]) == 4
|
|
model.set_dimensions(bolt, {"QIHeight": 8})
|
|
doc.undo(); doc.recompute()
|
|
assert doc.RedoCount == 1
|
|
before = kernel._state(doc)
|
|
kernel._reject(lambda: service.update_bom_table(page, "structured"))
|
|
assert kernel._state(doc) == before and doc.RedoCount == 1
|
|
doc.redo(); doc.recompute()
|
|
assert len(service.validate_table(page)["rows"]) == 4
|
|
return {"structured_local_total_quantities": True, "mixed_material_print_abbreviation": "Смеш.",
|
|
"stable_E_cell_type_immediate_native_undo_redo": True, "D_quantity_native_integer": True,
|
|
"quantity_total_metadata_numeric": True,
|
|
"native_overflow_reject_preserves_redo_branch": True}
|
|
|
|
def rows_removed_and_user_notes_preserved():
|
|
(doc, root, bolt, housing, sub, a, b, direct), page = _fixture("DrawingBOMRows", flexible=False)
|
|
view = service.create_bom_table(page)
|
|
sheet = view.Source
|
|
sheet.set("H2", "'Сохраняемое примечание")
|
|
doc.recompute()
|
|
occurrence = doc.getObject("HousingOccurrence")
|
|
sub.removeObject(occurrence)
|
|
doc.recompute()
|
|
model.prepare_drawing(page)
|
|
plan = service.validate_table(page)
|
|
assert len(plan["rows"]) == 1 and plan["rows"][0]["quantity"] == 5
|
|
assert view.CellEnd == "F3" and sheet.getContents("F4") == ""
|
|
assert sheet.get("H2") == "Сохраняемое примечание"
|
|
sub.addObject(occurrence); doc.recompute(); model.prepare_drawing(page)
|
|
assert len(service.validate_table(page)["rows"]) == 2 and view.CellEnd == "F4"
|
|
assert sheet.get("H2") == "Сохраняемое примечание"
|
|
return {"removed_computed_rows_cleared": True, "printed_range_updates": True, "outside_notes_preserved": True}
|
|
|
|
def top_root_membership_single_native_recompute():
|
|
(doc, root, bolt, housing, sub, a, b, direct), page = _fixture("DrawingBOMRootMembership", flexible=False)
|
|
view = service.create_bom_table(page)
|
|
sheet = view.Source
|
|
assert sheet.QIBOMSources == [root] and all(v.Source == [root] for v in sheet.QIBOMGeometryViews)
|
|
tracer = _TracingProxy(sheet)
|
|
original_calls = tracer.calls
|
|
doc.openTransaction("Remove actual root occurrence")
|
|
root.removeObject(a)
|
|
doc.recompute()
|
|
doc.commitTransaction()
|
|
assert tracer.calls == original_calls + 1 and tracer.maximum_depth == 1
|
|
assert len(root.Shape.Solids) == 4
|
|
reduced = service._build(sheet.QIBOMGeometryViews, "flat", sheet.QIBOMSources, False)
|
|
assert reduced["summary"]["components"] == 4
|
|
assert {row["part_number"]: row["quantity"] for row in reduced["rows"]} == {"BOLT": 3, "HOUSING": 1}
|
|
assert sheet.QIBOMData == service._data(reduced)
|
|
assert all(sheet.getContents(key) == value for key, value in reduced["cells"].items())
|
|
service._validate_symbol(view, reduced)
|
|
original_calls = tracer.calls
|
|
doc.openTransaction("Restore actual root occurrence")
|
|
root.addObject(a)
|
|
doc.recompute()
|
|
doc.commitTransaction()
|
|
assert tracer.calls == original_calls + 1 and len(root.Shape.Solids) == 7
|
|
restored = service._build(sheet.QIBOMGeometryViews, "flat", sheet.QIBOMSources, False)
|
|
assert restored["summary"]["components"] == 7 and sheet.QIBOMData == service._data(restored)
|
|
service._validate_symbol(view, restored)
|
|
model.prepare_drawing(page)
|
|
service.validate_table(page)
|
|
service.DrawingBOMProxy(sheet)
|
|
return {"actual_projection_source": root.Name, "native_solid_counts": [7, 4, 7],
|
|
"sheet_components": [7, reduced["summary"]["components"], restored["summary"]["components"]],
|
|
"one_sheet_execute_per_native_recompute": True, "fresh_native_symbol": True}
|
|
|
|
def insertion_undo_and_fresh_process_restore():
|
|
(doc, root, bolt, housing, sub, a, b, direct), page = _fixture("DrawingBOMPersist")
|
|
start = doc.UndoCount
|
|
view = service.create_bom_table(page)
|
|
sheet_name, view_name = view.Source.Name, view.Name
|
|
assert doc.UndoCount == start + 1
|
|
doc.undo(); doc.recompute()
|
|
assert doc.getObject(sheet_name) is None and doc.getObject(view_name) is None
|
|
assert service.table_for_page(page) is None and len(page.Views) == 3
|
|
assert doc.RedoCount == 1
|
|
doc.redo(); doc.recompute()
|
|
view = service.table_for_page(page)
|
|
assert isinstance(view.Source.Proxy, service.DrawingBOMProxy)
|
|
service.validate_table(page)
|
|
view.Source.set("H2", "'Сохраняемое примечание"); doc.recompute()
|
|
file = output / "drawing-bom-associative.FCStd"
|
|
model.save_project(doc, file)
|
|
return {"create_undo_removes_sheet_and_view": True, "redo_restores_proxy": True,
|
|
"fcstd": str(file), "fresh_native_process": _reopen_child(file, output)}
|
|
|
|
def ordinary_mass_precision_and_label_overflow():
|
|
doc = model.new_part("DrawingBOMCylinder")
|
|
source = model.create_cylinder(3, 7, doc)
|
|
page = model.create_drawing(doc)
|
|
view = service.create_bom_table(page)
|
|
plan = service.validate_table(page)
|
|
assert "0," in view.Source.get("F2") and len(view.Source.get("F2")) <= 13
|
|
assert view.Source.get("F2") != "0,00"
|
|
saved = view.Source.QIBOMCells
|
|
source.Label = "W" * 200
|
|
doc.recompute()
|
|
assert "Invalid" in view.Source.State and view.Source.QIBOMCells == saved
|
|
before = kernel._state(doc)
|
|
kernel._reject(lambda: service.update_bom_table(page))
|
|
assert kernel._state(doc) == before
|
|
return {"ordinary_cylinder_mass_printed": plan["cells"]["F2"], "oversized_text_rejected_without_truncation": True}
|
|
|
|
def native_table_format_position_and_source_guards():
|
|
(doc, root, bolt, housing, sub, a, b, direct), page = _fixture("DrawingBOMTableGuards")
|
|
view = service.create_bom_table(page)
|
|
cells = view.Source.QIBOMCells
|
|
doc.openTransaction("Move table outside printed area")
|
|
view.X = 10
|
|
doc.recompute()
|
|
doc.commitTransaction()
|
|
before = kernel._state(doc)
|
|
kernel._reject(lambda: service.validate_table(page))
|
|
assert kernel._state(doc) == before and view.Source.QIBOMCells == cells
|
|
service.update_bom_table(page)
|
|
service.validate_table(page)
|
|
doc.openTransaction("Change native table font")
|
|
view.TextSize = 2
|
|
doc.recompute()
|
|
doc.commitTransaction()
|
|
before = kernel._state(doc)
|
|
kernel._reject(lambda: service.validate_table(page))
|
|
assert kernel._state(doc) == before
|
|
service.update_bom_table(page)
|
|
service.validate_table(page)
|
|
doc.openTransaction("Rotate native table")
|
|
view.Rotation = 30
|
|
doc.recompute()
|
|
doc.commitTransaction()
|
|
before = kernel._state(doc)
|
|
kernel._reject(lambda: service.validate_table(page))
|
|
assert kernel._state(doc) == before
|
|
service.update_bom_table(page)
|
|
service.validate_table(page)
|
|
assert float(view.Rotation) == 0
|
|
sheet = view.Source
|
|
doc.openTransaction("Remove table source")
|
|
view.Source = None
|
|
doc.recompute()
|
|
doc.commitTransaction()
|
|
before = kernel._state(doc)
|
|
kernel._reject(lambda: service.table_for_page(page))
|
|
kernel._reject(lambda: service.validate_table(page))
|
|
assert kernel._state(doc) == before and sheet.QIBOMCells == cells
|
|
doc.undo(); doc.recompute()
|
|
assert service.table_for_page(page) is view and view.Source is sheet
|
|
service.validate_table(page)
|
|
assert doc.RedoCount == 1
|
|
return {"actual_native_xy_checked": True, "native_font_change_rejected_and_repaired": True,
|
|
"native_rotation_rejected_and_repaired": True,
|
|
"lost_sheet_source_not_treated_as_absent_table": True, "validation_read_only": True,
|
|
"native_undo_restores_source": True}
|
|
|
|
cases = [("Native table explicit projection scope DAG and no-op", native_creation_scope_and_dag),
|
|
("Automatic source labels native formulas and Undo Redo", source_geometry_labels_formulas_and_undo),
|
|
("Explicit scope repair and unrelated invalid body", scope_rebind_and_unrelated_invalid_body),
|
|
("Native invalid last-good cells and nonhealing validation", last_good_error_and_nonhealing_validation),
|
|
("Structured quantities and overflow preserving redo", structured_mode_and_overflow_redo),
|
|
("Changing row count preserves user notes", rows_removed_and_user_notes_preserved),
|
|
("Native top-root membership updates in one recompute", top_root_membership_single_native_recompute),
|
|
("Insert Undo Redo and FCStd fresh native process", insertion_undo_and_fresh_process_restore),
|
|
("Ordinary mass precision and untruncated text errors", ordinary_mass_precision_and_label_overflow),
|
|
("Actual native table format position and missing-source guards", native_table_format_position_and_source_guards)]
|
|
try:
|
|
for name, function in cases:
|
|
check(name, function)
|
|
finally:
|
|
for name in set(App.listDocuments()) - initial_docs:
|
|
App.closeDocument(name)
|
|
report = {"passed": all(item["passed"] for item in checks), "checks": checks,
|
|
"summary": {"total": len(checks), "passed": sum(item["passed"] for item in checks),
|
|
"failed": sum(not item["passed"] for item in checks)}, "gui_exercised": bool(App.GuiUp)}
|
|
report_path.write_text(json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
return report
|
|
|
|
|
|
if __name__ == "__main__":
|
|
report = run(sys.argv[1] if len(sys.argv) > 1 else ROOT / "artifacts/drawing-bom-headless/kernel")
|
|
print(json.dumps(report["summary"]))
|
|
raise SystemExit(0 if report["passed"] else 1)
|