Initial SFERA platform baseline
This commit is contained in:
@@ -0,0 +1,279 @@
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
|
||||
import semantic_kernel
|
||||
from semantic_kernel import index_project, parse_bsl_module, parse_bsl_module_from_rust_json
|
||||
from sir import EdgeKind, NodeKind
|
||||
|
||||
|
||||
def test_parse_bsl_module_supports_english_1c_syntax() -> None:
|
||||
source = Path("tests/golden/english_module.bsl").read_text(encoding="utf-8")
|
||||
|
||||
routines = parse_bsl_module(source)
|
||||
|
||||
assert [routine.name for routine in routines] == ["Posting", "CheckStock"]
|
||||
assert routines[1].is_function
|
||||
assert routines[0].calls == (("CheckStock", 2),)
|
||||
assert routines[0].writes[0].target == "StockBalance"
|
||||
assert routines[1].queries[0].tables == ("AccumulationRegister.StockBalance",)
|
||||
|
||||
|
||||
def test_parse_bsl_module_supports_inline_query_assignment_with_pipes() -> None:
|
||||
source = Path("tests/golden/query_inline_pipes.bsl").read_text(encoding="utf-8")
|
||||
|
||||
routines = parse_bsl_module(source)
|
||||
|
||||
assert len(routines) == 1
|
||||
assert routines[0].queries[0].tables == ("Справочник.Номенклатура",)
|
||||
assert routines[0].queries[0].text.startswith("ВЫБРАТЬ")
|
||||
assert "|ИЗ" not in routines[0].queries[0].text
|
||||
|
||||
|
||||
def test_parse_bsl_module_supports_from_and_table_on_same_line() -> None:
|
||||
source = Path("tests/golden/query_inline_from.bsl").read_text(encoding="utf-8")
|
||||
|
||||
routines = parse_bsl_module(source)
|
||||
|
||||
assert len(routines) == 1
|
||||
assert routines[0].queries[0].tables == ("Справочник.Контрагенты",)
|
||||
|
||||
|
||||
def test_parse_bsl_module_extracts_assignment_function_calls() -> None:
|
||||
source = Path("tests/golden/assignment_call.bsl").read_text(encoding="utf-8")
|
||||
|
||||
routines = parse_bsl_module(source)
|
||||
|
||||
assert routines[0].calls == (("ПроверитьОстатки", 2),)
|
||||
|
||||
|
||||
def test_parse_bsl_module_extracts_condition_function_calls() -> None:
|
||||
source = Path("tests/golden/condition_call.bsl").read_text(encoding="utf-8")
|
||||
|
||||
routines = parse_bsl_module(source)
|
||||
|
||||
assert routines[0].calls == (("ПроверитьОстатки", 2),)
|
||||
|
||||
|
||||
def test_parse_bsl_module_extracts_join_tables() -> None:
|
||||
source = Path("tests/golden/query_join_tables.bsl").read_text(encoding="utf-8")
|
||||
|
||||
routines = parse_bsl_module(source)
|
||||
|
||||
assert routines[0].queries[0].tables == (
|
||||
"Документ.ЗаказПокупателя",
|
||||
"Справочник.Контрагенты",
|
||||
)
|
||||
|
||||
|
||||
def test_parse_bsl_module_extracts_object_write_targets() -> None:
|
||||
source = Path("tests/golden/object_write.bsl").read_text(encoding="utf-8")
|
||||
|
||||
routines = parse_bsl_module(source)
|
||||
|
||||
assert routines[0].writes[0].target == "Справочник.Номенклатура"
|
||||
assert routines[0].writes[0].write_type == "OBJECT_WRITE"
|
||||
assert routines[1].writes[0].target == "Документ.CustomerOrder"
|
||||
assert routines[1].writes[0].write_type == "OBJECT_WRITE"
|
||||
|
||||
|
||||
def test_parse_bsl_module_extracts_recordset_write_targets() -> None:
|
||||
source = Path("tests/golden/recordset_write.bsl").read_text(encoding="utf-8")
|
||||
|
||||
routines = parse_bsl_module(source)
|
||||
|
||||
assert routines[0].writes[0].target == "РегистрСведений.Цены"
|
||||
assert routines[0].writes[0].write_type == "REGISTER_WRITE"
|
||||
assert routines[1].writes[0].target == "РегистрНакопления.StockBalance"
|
||||
assert routines[1].writes[0].write_type == "REGISTER_WRITE"
|
||||
|
||||
|
||||
def test_parse_bsl_module_preserves_export_flag() -> None:
|
||||
source = Path("tests/golden/common_module_export.bsl").read_text(encoding="utf-8")
|
||||
|
||||
routines = parse_bsl_module(source)
|
||||
|
||||
assert [routine.export for routine in routines] == [True, True]
|
||||
|
||||
|
||||
def test_parse_bsl_module_extracts_calls_and_writes_inside_control_flow() -> None:
|
||||
source = Path("tests/golden/control_flow_calls.bsl").read_text(encoding="utf-8")
|
||||
|
||||
routines = parse_bsl_module(source)
|
||||
|
||||
assert routines[0].calls == (("ПроверитьСтроку", 3), ("СообщитьОбОшибке", 9))
|
||||
assert routines[0].writes[0].target == "ОстаткиТоваров"
|
||||
|
||||
|
||||
def test_index_project_links_english_register_write(tmp_path: Path) -> None:
|
||||
module = tmp_path / "english_module.bsl"
|
||||
module.write_text(Path("tests/golden/english_module.bsl").read_text(encoding="utf-8"), encoding="utf-8")
|
||||
|
||||
snapshot = index_project(tmp_path, project_id="english")
|
||||
|
||||
register = next(node for node in snapshot.nodes if node.kind == NodeKind.REGISTER)
|
||||
assert register.name == "StockBalance"
|
||||
assert any(edge.kind == EdgeKind.WRITES and edge.target_lineage == register.lineage_id for edge in snapshot.edges)
|
||||
|
||||
|
||||
def test_index_project_links_object_write_to_metadata_node(tmp_path: Path) -> None:
|
||||
module = tmp_path / "object_write.bsl"
|
||||
module.write_text(Path("tests/golden/object_write.bsl").read_text(encoding="utf-8"), encoding="utf-8")
|
||||
|
||||
snapshot = index_project(tmp_path, project_id="object-write")
|
||||
|
||||
catalog = next(node for node in snapshot.nodes if node.kind == NodeKind.CATALOG)
|
||||
document = next(node for node in snapshot.nodes if node.kind == NodeKind.DOCUMENT)
|
||||
assert catalog.qualified_name == "Справочник.Номенклатура"
|
||||
assert document.qualified_name == "Документ.CustomerOrder"
|
||||
assert any(edge.kind == EdgeKind.WRITES and edge.target_lineage == catalog.lineage_id for edge in snapshot.edges)
|
||||
assert any(edge.kind == EdgeKind.WRITES and edge.target_lineage == document.lineage_id for edge in snapshot.edges)
|
||||
|
||||
|
||||
def test_index_project_links_recordset_write_to_register_node(tmp_path: Path) -> None:
|
||||
module = tmp_path / "recordset_write.bsl"
|
||||
module.write_text(Path("tests/golden/recordset_write.bsl").read_text(encoding="utf-8"), encoding="utf-8")
|
||||
|
||||
snapshot = index_project(tmp_path, project_id="recordset-write")
|
||||
|
||||
registers = {node.qualified_name: node for node in snapshot.nodes if node.kind == NodeKind.REGISTER}
|
||||
assert "РегистрСведений.Цены" in registers
|
||||
assert "РегистрНакопления.StockBalance" in registers
|
||||
assert any(
|
||||
edge.kind == EdgeKind.WRITES and edge.target_lineage == registers["РегистрСведений.Цены"].lineage_id
|
||||
for edge in snapshot.edges
|
||||
)
|
||||
assert any(
|
||||
edge.kind == EdgeKind.WRITES and edge.target_lineage == registers["РегистрНакопления.StockBalance"].lineage_id
|
||||
for edge in snapshot.edges
|
||||
)
|
||||
|
||||
|
||||
def test_index_project_stores_routine_export_attribute(tmp_path: Path) -> None:
|
||||
module = tmp_path / "common_module_export.bsl"
|
||||
module.write_text(
|
||||
Path("tests/golden/common_module_export.bsl").read_text(encoding="utf-8"),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
snapshot = index_project(tmp_path, project_id="exports")
|
||||
|
||||
exported = {
|
||||
node.name
|
||||
for node in snapshot.nodes
|
||||
if node.kind in {NodeKind.PROCEDURE, NodeKind.FUNCTION} and node.attributes.get("export")
|
||||
}
|
||||
assert exported == {"ОтправитьЧек", "BuildPayload"}
|
||||
|
||||
|
||||
def test_parse_bsl_module_from_rust_json_contract() -> None:
|
||||
payload = {
|
||||
"procedures": [
|
||||
{
|
||||
"name": "Posting",
|
||||
"is_function": False,
|
||||
"source_range": {"line_start": 1, "line_end": 4},
|
||||
},
|
||||
{
|
||||
"name": "CheckStock",
|
||||
"is_function": True,
|
||||
"source_range": {"line_start": 6, "line_end": 13},
|
||||
},
|
||||
],
|
||||
"calls": [
|
||||
{
|
||||
"caller": "Posting",
|
||||
"callee": "CheckStock",
|
||||
"source_range": {"line_start": 2, "line_end": 2},
|
||||
}
|
||||
],
|
||||
"queries": [
|
||||
{
|
||||
"owner_procedure": "CheckStock",
|
||||
"query_text": "SELECT\nStock.Item\nFROM\nAccumulationRegister.StockBalance AS Stock",
|
||||
"tables": ["AccumulationRegister.StockBalance"],
|
||||
"source_range": {"line_start": 8, "line_end": 12},
|
||||
}
|
||||
],
|
||||
"writes": [
|
||||
{
|
||||
"owner_procedure": "Posting",
|
||||
"target": "StockBalance",
|
||||
"write_type": "REGISTER_WRITE",
|
||||
"source_range": {"line_start": 3, "line_end": 3},
|
||||
}
|
||||
],
|
||||
"diagnostics": [],
|
||||
}
|
||||
|
||||
routines = parse_bsl_module_from_rust_json(payload)
|
||||
|
||||
assert [routine.name for routine in routines] == ["Posting", "CheckStock"]
|
||||
assert routines[0].line_end == 4
|
||||
assert routines[0].calls == (("CheckStock", 2),)
|
||||
assert routines[0].writes[0].target == "StockBalance"
|
||||
assert routines[1].queries[0].tables == ("AccumulationRegister.StockBalance",)
|
||||
|
||||
|
||||
def test_index_project_can_use_rust_parser_contract(monkeypatch, tmp_path: Path) -> None:
|
||||
module = tmp_path / "english_module.bsl"
|
||||
module.write_text(Path("tests/golden/english_module.bsl").read_text(encoding="utf-8"), encoding="utf-8")
|
||||
rust_json = r"""
|
||||
{
|
||||
"source_path": "english_module.bsl",
|
||||
"procedures": [
|
||||
{"name": "Posting", "export": false, "is_function": false, "parameters": [], "source_range": {"line_start": 1, "line_end": 4, "column_start": 1, "column_end": 13}},
|
||||
{"name": "CheckStock", "export": false, "is_function": true, "parameters": [], "source_range": {"line_start": 6, "line_end": 13, "column_start": 1, "column_end": 12}}
|
||||
],
|
||||
"calls": [{"caller": "Posting", "callee": "CheckStock", "source_range": {"line_start": 2, "line_end": 2, "column_start": 1, "column_end": 18}}],
|
||||
"queries": [{"owner_procedure": "CheckStock", "query_text": "SELECT\nStock.Item\nFROM\nAccumulationRegister.StockBalance AS Stock", "tables": ["AccumulationRegister.StockBalance"], "source_range": {"line_start": 8, "line_end": 12, "column_start": 1, "column_end": 53}}],
|
||||
"writes": [{"owner_procedure": "Posting", "target": "StockBalance", "write_type": "REGISTER_WRITE", "source_range": {"line_start": 3, "line_end": 3, "column_start": 1, "column_end": 36}}],
|
||||
"diagnostics": []
|
||||
}
|
||||
"""
|
||||
|
||||
def fake_run(command, check, capture_output, text, encoding):
|
||||
assert command[0] == "bsl-parser"
|
||||
assert Path(command[1]) == module
|
||||
return subprocess.CompletedProcess(command, 0, stdout=rust_json, stderr="")
|
||||
|
||||
monkeypatch.setenv("SFERA_BSL_PARSER", "bsl-parser")
|
||||
monkeypatch.setattr(semantic_kernel.subprocess, "run", fake_run)
|
||||
|
||||
snapshot = index_project(tmp_path, project_id="rust-contract")
|
||||
|
||||
assert any(node.kind == NodeKind.FUNCTION and node.name == "CheckStock" for node in snapshot.nodes)
|
||||
assert any(edge.kind == EdgeKind.CALLS for edge in snapshot.edges)
|
||||
assert any(edge.kind == EdgeKind.WRITES for edge in snapshot.edges)
|
||||
|
||||
|
||||
def test_index_project_auto_discovers_rust_parser(monkeypatch, tmp_path: Path) -> None:
|
||||
module = tmp_path / "english_module.bsl"
|
||||
module.write_text(Path("tests/golden/english_module.bsl").read_text(encoding="utf-8"), encoding="utf-8")
|
||||
rust_json = r"""
|
||||
{
|
||||
"source_path": "english_module.bsl",
|
||||
"procedures": [
|
||||
{"name": "Posting", "export": false, "is_function": false, "parameters": [], "source_range": {"line_start": 1, "line_end": 4, "column_start": 1, "column_end": 13}},
|
||||
{"name": "CheckStock", "export": false, "is_function": true, "parameters": [], "source_range": {"line_start": 6, "line_end": 13, "column_start": 1, "column_end": 12}}
|
||||
],
|
||||
"calls": [{"caller": "Posting", "callee": "CheckStock", "source_range": {"line_start": 2, "line_end": 2, "column_start": 1, "column_end": 18}}],
|
||||
"queries": [{"owner_procedure": "CheckStock", "query_text": "SELECT\nStock.Item\nFROM\nAccumulationRegister.StockBalance AS Stock", "tables": ["AccumulationRegister.StockBalance"], "source_range": {"line_start": 8, "line_end": 12, "column_start": 1, "column_end": 53}}],
|
||||
"writes": [{"owner_procedure": "Posting", "target": "StockBalance", "write_type": "REGISTER_WRITE", "source_range": {"line_start": 3, "line_end": 3, "column_start": 1, "column_end": 36}}],
|
||||
"diagnostics": []
|
||||
}
|
||||
"""
|
||||
|
||||
def fake_run(command, check, capture_output, text, encoding):
|
||||
assert command[0] == "auto-bsl-parser"
|
||||
assert Path(command[1]) == module
|
||||
return subprocess.CompletedProcess(command, 0, stdout=rust_json, stderr="")
|
||||
|
||||
monkeypatch.delenv("SFERA_BSL_PARSER", raising=False)
|
||||
monkeypatch.setattr(semantic_kernel, "_auto_discovered_rust_bsl_parser", lambda source_file: "auto-bsl-parser")
|
||||
monkeypatch.setattr(semantic_kernel.subprocess, "run", fake_run)
|
||||
|
||||
snapshot = index_project(tmp_path, project_id="rust-auto")
|
||||
|
||||
assert any(node.kind == NodeKind.FUNCTION and node.name == "CheckStock" for node in snapshot.nodes)
|
||||
assert any(edge.kind == EdgeKind.CALLS for edge in snapshot.edges)
|
||||
assert any(edge.kind == EdgeKind.WRITES for edge in snapshot.edges)
|
||||
@@ -0,0 +1,152 @@
|
||||
from pathlib import Path
|
||||
|
||||
from semantic_kernel import index_project
|
||||
from sir import EdgeKind, NodeKind, validate_snapshot
|
||||
|
||||
|
||||
def test_index_project_builds_valid_snapshot(tmp_path: Path):
|
||||
module = tmp_path / "demo_module.bsl"
|
||||
module.write_text(
|
||||
"""
|
||||
Процедура Проведение()
|
||||
ПроверитьОстатки();
|
||||
Движения.ОстаткиТоваров.Записать();
|
||||
КонецПроцедуры
|
||||
|
||||
Процедура ПроверитьОстатки()
|
||||
Запрос = Новый Запрос;
|
||||
Запрос.Текст =
|
||||
"ВЫБРАТЬ
|
||||
Остатки.Номенклатура
|
||||
ИЗ
|
||||
РегистрНакопления.ОстаткиТоваров КАК Остатки";
|
||||
КонецПроцедуры
|
||||
""",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
snapshot = index_project(tmp_path, project_id="demo")
|
||||
|
||||
validate_snapshot(snapshot)
|
||||
assert any(node.kind == NodeKind.MODULE for node in snapshot.nodes)
|
||||
assert any(node.kind == NodeKind.PROCEDURE and node.name == "Проведение" for node in snapshot.nodes)
|
||||
assert any(node.kind == NodeKind.QUERY for node in snapshot.nodes)
|
||||
assert any(node.kind == NodeKind.REGISTER and node.name == "ОстаткиТоваров" for node in snapshot.nodes)
|
||||
assert any(edge.kind == EdgeKind.CALLS for edge in snapshot.edges)
|
||||
assert any(edge.kind == EdgeKind.READS_TABLE for edge in snapshot.edges)
|
||||
assert any(edge.kind == EdgeKind.WRITES for edge in snapshot.edges)
|
||||
|
||||
|
||||
def test_index_project_builds_integration_endpoint_nodes(tmp_path: Path):
|
||||
module = tmp_path / "integration.bsl"
|
||||
module.write_text(
|
||||
"""
|
||||
Процедура Отправить()
|
||||
Адрес = "https://api.example.local/orders";
|
||||
Объект = Новый COMОбъект("V83.Application");
|
||||
КонецПроцедуры
|
||||
""",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
snapshot = index_project(tmp_path, project_id="integrations")
|
||||
|
||||
assert any(node.kind == NodeKind.INTEGRATION_ENDPOINT for node in snapshot.nodes)
|
||||
assert any(edge.kind == EdgeKind.USES_INTEGRATION for edge in snapshot.edges)
|
||||
|
||||
|
||||
def test_index_project_extracts_inline_new_query(tmp_path: Path):
|
||||
module = tmp_path / "query_module.bsl"
|
||||
module.write_text(
|
||||
"""
|
||||
Процедура ПроверитьКонтрагента()
|
||||
Запрос = Новый Запрос("ВЫБРАТЬ Контрагенты.Ссылка ИЗ Справочник.Контрагенты КАК Контрагенты");
|
||||
КонецПроцедуры
|
||||
""",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
snapshot = index_project(tmp_path, project_id="inline-query")
|
||||
|
||||
query = next(node for node in snapshot.nodes if node.kind == NodeKind.QUERY)
|
||||
assert "Справочник.Контрагенты" in query.attributes["query_text"]
|
||||
assert any(
|
||||
edge.kind == EdgeKind.READS_TABLE
|
||||
and any(node.lineage_id == edge.target_lineage and node.qualified_name == "Справочник.Контрагенты" for node in snapshot.nodes)
|
||||
for edge in snapshot.edges
|
||||
)
|
||||
|
||||
|
||||
def test_index_project_prefers_same_module_routine_for_duplicate_names(tmp_path: Path):
|
||||
shared = tmp_path / "a_shared.bsl"
|
||||
shared.write_text(
|
||||
"""
|
||||
Процедура ПроверитьОстатки()
|
||||
КонецПроцедуры
|
||||
""",
|
||||
encoding="utf-8",
|
||||
)
|
||||
document_module = tmp_path / "z_document.bsl"
|
||||
document_module.write_text(
|
||||
"""
|
||||
Процедура Проведение()
|
||||
ПроверитьОстатки();
|
||||
КонецПроцедуры
|
||||
|
||||
Процедура ПроверитьОстатки()
|
||||
КонецПроцедуры
|
||||
""",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
snapshot = index_project(tmp_path, project_id="duplicate-routines")
|
||||
|
||||
local_target = next(
|
||||
node for node in snapshot.nodes if node.qualified_name == "z_document.ПроверитьОстатки"
|
||||
)
|
||||
assert any(
|
||||
edge.kind == EdgeKind.CALLS and edge.target_lineage == local_target.lineage_id
|
||||
for edge in snapshot.edges
|
||||
)
|
||||
|
||||
|
||||
def test_index_project_records_malformed_bsl_diagnostics(tmp_path: Path):
|
||||
module = tmp_path / "broken.bsl"
|
||||
module.write_text(
|
||||
"""
|
||||
Процедура Проведение()
|
||||
Запрос = Новый Запрос;
|
||||
Запрос.Текст =
|
||||
"ВЫБРАТЬ
|
||||
Товары.Ссылка
|
||||
ИЗ
|
||||
Справочник.Номенклатура КАК Товары"
|
||||
""",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
snapshot = index_project(tmp_path, project_id="broken")
|
||||
|
||||
assert {diagnostic.code for diagnostic in snapshot.diagnostics} == {
|
||||
"BSL_UNCLOSED_QUERY",
|
||||
"BSL_UNCLOSED_ROUTINE",
|
||||
}
|
||||
|
||||
|
||||
def test_index_project_skips_invalid_xml_and_records_diagnostic(tmp_path: Path):
|
||||
valid_xml = tmp_path / "valid.xml"
|
||||
valid_xml.write_text(
|
||||
"""
|
||||
<Configuration>
|
||||
<Catalog name="Контрагенты" qualifiedName="Справочник.Контрагенты" />
|
||||
</Configuration>
|
||||
""",
|
||||
encoding="utf-8",
|
||||
)
|
||||
broken_xml = tmp_path / "broken.mdo"
|
||||
broken_xml.write_text("<mdclass:Catalog xmlns:mdclass=", encoding="utf-8")
|
||||
|
||||
snapshot = index_project(tmp_path, project_id="broken-xml")
|
||||
|
||||
assert any(node.qualified_name == "Справочник.Контрагенты" for node in snapshot.nodes)
|
||||
assert any(diagnostic.code == "XML_PARSE_ERROR" for diagnostic in snapshot.diagnostics)
|
||||
@@ -0,0 +1,21 @@
|
||||
from pathlib import Path
|
||||
|
||||
from review_engine import review_snapshot
|
||||
from semantic_kernel import index_project
|
||||
|
||||
|
||||
def test_index_project_records_unresolved_calls(tmp_path: Path):
|
||||
module = tmp_path / "demo_module.bsl"
|
||||
module.write_text(
|
||||
"""
|
||||
Процедура Проведение()
|
||||
НеизвестнаяПроцедура();
|
||||
КонецПроцедуры
|
||||
""",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
snapshot = index_project(tmp_path, project_id="demo")
|
||||
|
||||
assert snapshot.unresolved_references[0].target_name == "НеизвестнаяПроцедура"
|
||||
assert review_snapshot(snapshot)[0].title == "Unresolved call"
|
||||
@@ -0,0 +1,383 @@
|
||||
from pathlib import Path
|
||||
|
||||
from semantic_kernel import index_project
|
||||
from sir import EdgeKind, NodeKind
|
||||
from ui_semantics import form_semantics
|
||||
|
||||
|
||||
def test_index_project_extracts_xml_ui_semantics(tmp_path: Path):
|
||||
xml = tmp_path / "form.xml"
|
||||
xml.write_text(
|
||||
"""
|
||||
<Form name="ФормаДокумента" qualifiedName="Документ.Заказ.ФормаДокумента">
|
||||
<Command name="Провести" qualifiedName="Документ.Заказ.ФормаДокумента.Провести" />
|
||||
</Form>
|
||||
""",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
snapshot = index_project(tmp_path, project_id="demo")
|
||||
|
||||
assert any(node.kind == NodeKind.FORM for node in snapshot.nodes)
|
||||
assert any(edge.kind == EdgeKind.HAS_COMMAND for edge in snapshot.edges)
|
||||
assert form_semantics(snapshot)[0].commands[0].name == "Провести"
|
||||
|
||||
|
||||
def test_index_project_extracts_1c_metadata_objects(tmp_path: Path):
|
||||
xml = tmp_path / "metadata.xml"
|
||||
xml.write_text(
|
||||
"""
|
||||
<Configuration>
|
||||
<Catalog name="Номенклатура" qualifiedName="Справочник.Номенклатура">
|
||||
<Attribute name="Артикул" qualifiedName="Справочник.Номенклатура.Артикул" />
|
||||
</Catalog>
|
||||
<Document name="ЗаказПокупателя" qualifiedName="Документ.ЗаказПокупателя">
|
||||
<TabularSection name="Товары" qualifiedName="Документ.ЗаказПокупателя.Товары">
|
||||
<Attribute name="Номенклатура" qualifiedName="Документ.ЗаказПокупателя.Товары.Номенклатура" />
|
||||
</TabularSection>
|
||||
<Form name="ФормаДокумента" qualifiedName="Документ.ЗаказПокупателя.ФормаДокумента">
|
||||
<Command name="Провести" qualifiedName="Документ.ЗаказПокупателя.ФормаДокумента.Провести" />
|
||||
</Form>
|
||||
</Document>
|
||||
<Role name="Менеджер" qualifiedName="Роль.Менеджер" />
|
||||
<MetadataObject type="ScheduledJob">
|
||||
<Name>ОбменСКассовымУзлом</Name>
|
||||
<QualifiedName>РегламентноеЗадание.ОбменСКассовымУзлом</QualifiedName>
|
||||
</MetadataObject>
|
||||
</Configuration>
|
||||
""",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
snapshot = index_project(tmp_path, project_id="metadata")
|
||||
|
||||
assert any(node.kind == NodeKind.CATALOG and node.name == "Номенклатура" for node in snapshot.nodes)
|
||||
assert any(node.kind == NodeKind.DOCUMENT and node.name == "ЗаказПокупателя" for node in snapshot.nodes)
|
||||
assert any(node.kind == NodeKind.TABULAR_SECTION and node.name == "Товары" for node in snapshot.nodes)
|
||||
assert any(node.kind == NodeKind.ROLE and node.name == "Менеджер" for node in snapshot.nodes)
|
||||
assert any(node.kind == NodeKind.SCHEDULED_JOB for node in snapshot.nodes)
|
||||
assert any(edge.kind == EdgeKind.HAS_ATTRIBUTE for edge in snapshot.edges)
|
||||
assert any(edge.kind == EdgeKind.HAS_TABULAR_SECTION for edge in snapshot.edges)
|
||||
tabular_section = next(node for node in snapshot.nodes if node.kind == NodeKind.TABULAR_SECTION)
|
||||
column = next(node for node in snapshot.nodes if node.kind == NodeKind.ATTRIBUTE and node.name == "Номенклатура")
|
||||
assert any(
|
||||
edge.kind == EdgeKind.HAS_ATTRIBUTE
|
||||
and edge.source_lineage == tabular_section.lineage_id
|
||||
and edge.target_lineage == column.lineage_id
|
||||
for edge in snapshot.edges
|
||||
)
|
||||
assert any(edge.kind == EdgeKind.HAS_FORM for edge in snapshot.edges)
|
||||
assert any(edge.kind == EdgeKind.HAS_COMMAND for edge in snapshot.edges)
|
||||
|
||||
|
||||
def test_index_project_remaps_edges_from_duplicate_metadata_nodes(tmp_path: Path):
|
||||
first = tmp_path / "first.xml"
|
||||
first.write_text(
|
||||
"""
|
||||
<Configuration>
|
||||
<MetadataObject type="ChartOfCharacteristicTypes">
|
||||
<Name>СвойстваОбъектов</Name>
|
||||
<QualifiedName>ПланВидовХарактеристик.СвойстваОбъектов</QualifiedName>
|
||||
</MetadataObject>
|
||||
</Configuration>
|
||||
""",
|
||||
encoding="utf-8",
|
||||
)
|
||||
second = tmp_path / "second.xml"
|
||||
second.write_text(
|
||||
"""
|
||||
<Configuration>
|
||||
<MetadataObject type="ChartOfCharacteristicTypes">
|
||||
<Name>СвойстваОбъектов</Name>
|
||||
<QualifiedName>ПланВидовХарактеристик.СвойстваОбъектов</QualifiedName>
|
||||
</MetadataObject>
|
||||
<Attribute name="ТипЗначения" qualifiedName="ПланВидовХарактеристик.СвойстваОбъектов.ТипЗначения" />
|
||||
</Configuration>
|
||||
""",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
snapshot = index_project(tmp_path, project_id="metadata-duplicates")
|
||||
|
||||
chart = next(node for node in snapshot.nodes if node.kind == NodeKind.CHART_OF_CHARACTERISTIC_TYPES)
|
||||
attribute = next(node for node in snapshot.nodes if node.kind == NodeKind.ATTRIBUTE)
|
||||
assert any(
|
||||
edge.kind == EdgeKind.HAS_ATTRIBUTE
|
||||
and edge.source_lineage == chart.lineage_id
|
||||
and edge.target_lineage == attribute.lineage_id
|
||||
for edge in snapshot.edges
|
||||
)
|
||||
|
||||
|
||||
def test_index_project_links_document_metadata_to_object_module(tmp_path: Path):
|
||||
xml = tmp_path / "metadata.xml"
|
||||
xml.write_text(
|
||||
"""
|
||||
<Configuration>
|
||||
<Document name="ЗаказПокупателя" qualifiedName="Документ.ЗаказПокупателя" />
|
||||
</Configuration>
|
||||
""",
|
||||
encoding="utf-8",
|
||||
)
|
||||
module = tmp_path / "Documents" / "ЗаказПокупателя" / "Ext" / "ObjectModule.bsl"
|
||||
module.parent.mkdir(parents=True)
|
||||
module.write_text(
|
||||
"""
|
||||
Процедура Проведение()
|
||||
КонецПроцедуры
|
||||
""",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
snapshot = index_project(tmp_path, project_id="metadata-links")
|
||||
|
||||
document = next(node for node in snapshot.nodes if node.kind == NodeKind.DOCUMENT)
|
||||
module_node = next(node for node in snapshot.nodes if node.kind == NodeKind.MODULE)
|
||||
link = next(
|
||||
edge
|
||||
for edge in snapshot.edges
|
||||
if edge.kind == EdgeKind.CONTAINS
|
||||
and edge.source_lineage == document.lineage_id
|
||||
and edge.target_lineage == module_node.lineage_id
|
||||
)
|
||||
assert link.attributes["module_role"] == "OBJECT_MODULE"
|
||||
|
||||
|
||||
def test_index_project_links_common_module_metadata_to_bsl_module(tmp_path: Path):
|
||||
xml = tmp_path / "metadata.xml"
|
||||
xml.write_text(
|
||||
"""
|
||||
<Configuration>
|
||||
<CommonModule name="ИнтеграцияСКассой" qualifiedName="ОбщийМодуль.ИнтеграцияСКассой" />
|
||||
</Configuration>
|
||||
""",
|
||||
encoding="utf-8",
|
||||
)
|
||||
module = tmp_path / "CommonModules" / "ИнтеграцияСКассой" / "Module.bsl"
|
||||
module.parent.mkdir(parents=True)
|
||||
module.write_text(
|
||||
"""
|
||||
Процедура ОтправитьЧек()
|
||||
КонецПроцедуры
|
||||
""",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
snapshot = index_project(tmp_path, project_id="common-module-links")
|
||||
|
||||
common_module = next(node for node in snapshot.nodes if node.kind == NodeKind.COMMON_MODULE)
|
||||
bsl_module = next(node for node in snapshot.nodes if node.kind == NodeKind.MODULE)
|
||||
assert any(
|
||||
edge.kind == EdgeKind.CONTAINS
|
||||
and edge.source_lineage == common_module.lineage_id
|
||||
and edge.target_lineage == bsl_module.lineage_id
|
||||
for edge in snapshot.edges
|
||||
)
|
||||
|
||||
|
||||
def test_index_project_reads_edt_mdo_metadata(tmp_path: Path):
|
||||
catalog_dir = tmp_path / "src" / "Catalogs" / "Товары"
|
||||
catalog_dir.mkdir(parents=True)
|
||||
(catalog_dir / "Товары.mdo").write_text(
|
||||
"""
|
||||
<mdclass:Catalog xmlns:mdclass="http://g5.1c.ru/v8/dt/metadata/mdclass">
|
||||
<name>Товары</name>
|
||||
<attributes>
|
||||
<name>Артикул</name>
|
||||
</attributes>
|
||||
<forms>
|
||||
<name>ФормаЭлемента</name>
|
||||
</forms>
|
||||
</mdclass:Catalog>
|
||||
""",
|
||||
encoding="utf-8",
|
||||
)
|
||||
report_dir = tmp_path / "src" / "Reports" / "АнализПродаж"
|
||||
report_dir.mkdir(parents=True)
|
||||
(report_dir / "АнализПродаж.mdo").write_text(
|
||||
"""
|
||||
<mdclass:Report xmlns:mdclass="http://g5.1c.ru/v8/dt/metadata/mdclass">
|
||||
<name>АнализПродаж</name>
|
||||
</mdclass:Report>
|
||||
""",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
snapshot = index_project(tmp_path, project_id="edt-mdo")
|
||||
|
||||
catalog = next(node for node in snapshot.nodes if node.kind == NodeKind.CATALOG)
|
||||
attribute = next(node for node in snapshot.nodes if node.kind == NodeKind.ATTRIBUTE and node.name == "Артикул")
|
||||
form = next(node for node in snapshot.nodes if node.kind == NodeKind.FORM)
|
||||
report = next(node for node in snapshot.nodes if node.kind == NodeKind.REPORT)
|
||||
assert catalog.qualified_name == "Справочник.Товары"
|
||||
assert attribute.qualified_name == "Справочник.Товары.Артикул"
|
||||
assert form.qualified_name == "Справочник.Товары.ФормаЭлемента"
|
||||
assert report.qualified_name == "Отчет.АнализПродаж"
|
||||
assert any(edge.kind == EdgeKind.HAS_ATTRIBUTE and edge.source_lineage == catalog.lineage_id for edge in snapshot.edges)
|
||||
assert any(edge.kind == EdgeKind.HAS_FORM and edge.source_lineage == catalog.lineage_id for edge in snapshot.edges)
|
||||
|
||||
|
||||
def test_index_project_preserves_register_dimension_and_resource_roles(tmp_path: Path):
|
||||
register_dir = tmp_path / "src" / "AccumulationRegisters" / "ОстаткиТоваров"
|
||||
register_dir.mkdir(parents=True)
|
||||
(register_dir / "ОстаткиТоваров.mdo").write_text(
|
||||
"""
|
||||
<mdclass:AccumulationRegister xmlns:mdclass="http://g5.1c.ru/v8/dt/metadata/mdclass">
|
||||
<name>ОстаткиТоваров</name>
|
||||
<dimensions>
|
||||
<name>Номенклатура</name>
|
||||
</dimensions>
|
||||
<resources>
|
||||
<name>Количество</name>
|
||||
</resources>
|
||||
</mdclass:AccumulationRegister>
|
||||
""",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
snapshot = index_project(tmp_path, project_id="register-fields")
|
||||
|
||||
register = next(node for node in snapshot.nodes if node.kind == NodeKind.REGISTER)
|
||||
dimension = next(node for node in snapshot.nodes if node.kind == NodeKind.ATTRIBUTE and node.name == "Номенклатура")
|
||||
resource = next(node for node in snapshot.nodes if node.kind == NodeKind.ATTRIBUTE and node.name == "Количество")
|
||||
assert dimension.attributes["attribute_role"] == "DIMENSION"
|
||||
assert resource.attributes["attribute_role"] == "RESOURCE"
|
||||
assert any(
|
||||
edge.kind == EdgeKind.HAS_ATTRIBUTE
|
||||
and edge.source_lineage == register.lineage_id
|
||||
and edge.target_lineage == dimension.lineage_id
|
||||
for edge in snapshot.edges
|
||||
)
|
||||
assert any(
|
||||
edge.kind == EdgeKind.HAS_ATTRIBUTE
|
||||
and edge.source_lineage == register.lineage_id
|
||||
and edge.target_lineage == resource.lineage_id
|
||||
for edge in snapshot.edges
|
||||
)
|
||||
|
||||
|
||||
def test_index_project_links_role_rights_to_metadata_objects(tmp_path: Path):
|
||||
xml = tmp_path / "roles.xml"
|
||||
xml.write_text(
|
||||
"""
|
||||
<Configuration>
|
||||
<Document name="ЗаказПокупателя" qualifiedName="Документ.ЗаказПокупателя" />
|
||||
<Role name="Менеджер" qualifiedName="Роль.Менеджер">
|
||||
<Right object="Документ.ЗаказПокупателя" read="true" write="true" post="true" />
|
||||
</Role>
|
||||
</Configuration>
|
||||
""",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
snapshot = index_project(tmp_path, project_id="role-rights")
|
||||
|
||||
role = next(node for node in snapshot.nodes if node.kind == NodeKind.ROLE)
|
||||
document = next(node for node in snapshot.nodes if node.kind == NodeKind.DOCUMENT)
|
||||
right = next(edge for edge in snapshot.edges if edge.kind == EdgeKind.GRANTS_ACCESS)
|
||||
assert right.source_lineage == role.lineage_id
|
||||
assert right.target_lineage == document.lineage_id
|
||||
assert right.attributes["post"] == "true"
|
||||
|
||||
|
||||
def test_index_project_links_child_element_command_action_and_role_right(tmp_path: Path):
|
||||
xml = tmp_path / "metadata.xml"
|
||||
xml.write_text(
|
||||
"""
|
||||
<Configuration>
|
||||
<Document>
|
||||
<Name>ЗаказПокупателя</Name>
|
||||
<QualifiedName>Документ.ЗаказПокупателя</QualifiedName>
|
||||
<Form>
|
||||
<Name>ФормаДокумента</Name>
|
||||
<Command>
|
||||
<Name>Провести</Name>
|
||||
<Action>ПровестиКоманда</Action>
|
||||
</Command>
|
||||
</Form>
|
||||
</Document>
|
||||
<Role>
|
||||
<Name>Менеджер</Name>
|
||||
<Right read="true">
|
||||
<Object>Документ.ЗаказПокупателя</Object>
|
||||
</Right>
|
||||
</Role>
|
||||
</Configuration>
|
||||
""",
|
||||
encoding="utf-8",
|
||||
)
|
||||
module = tmp_path / "form_module.bsl"
|
||||
module.write_text("Процедура ПровестиКоманда()\nКонецПроцедуры\n", encoding="utf-8")
|
||||
|
||||
snapshot = index_project(tmp_path, project_id="child-element-metadata")
|
||||
|
||||
document = next(node for node in snapshot.nodes if node.kind == NodeKind.DOCUMENT)
|
||||
role = next(node for node in snapshot.nodes if node.kind == NodeKind.ROLE)
|
||||
command = next(node for node in snapshot.nodes if node.kind == NodeKind.COMMAND)
|
||||
assert any(
|
||||
edge.kind == EdgeKind.GRANTS_ACCESS
|
||||
and edge.source_lineage == role.lineage_id
|
||||
and edge.target_lineage == document.lineage_id
|
||||
for edge in snapshot.edges
|
||||
)
|
||||
assert any(
|
||||
edge.kind == EdgeKind.HANDLES
|
||||
and edge.source_lineage == command.lineage_id
|
||||
and edge.attributes["handler_name"] == "ПровестиКоманда"
|
||||
for edge in snapshot.edges
|
||||
)
|
||||
|
||||
|
||||
def test_index_project_links_form_command_to_handler(tmp_path: Path):
|
||||
xml = tmp_path / "form.xml"
|
||||
xml.write_text(
|
||||
"""
|
||||
<Form name="ФормаДокумента" qualifiedName="Документ.Заказ.ФормаДокумента">
|
||||
<Command name="Провести" qualifiedName="Документ.Заказ.ФормаДокумента.Провести" action="ПровестиКоманда" />
|
||||
</Form>
|
||||
""",
|
||||
encoding="utf-8",
|
||||
)
|
||||
module = tmp_path / "form_module.bsl"
|
||||
module.write_text("Процедура ПровестиКоманда()\nКонецПроцедуры\n", encoding="utf-8")
|
||||
|
||||
snapshot = index_project(tmp_path, project_id="ui-handlers")
|
||||
|
||||
assert any(edge.kind == EdgeKind.HANDLES for edge in snapshot.edges)
|
||||
|
||||
|
||||
def test_index_project_links_form_events_to_handlers(tmp_path: Path):
|
||||
xml = tmp_path / "form.xml"
|
||||
xml.write_text(
|
||||
"""
|
||||
<Form
|
||||
name="ФормаДокумента"
|
||||
qualifiedName="Документ.Заказ.ФормаДокумента"
|
||||
onCreate="ПриСозданииНаСервере"
|
||||
beforeWrite="ПередЗаписью"
|
||||
/>
|
||||
""",
|
||||
encoding="utf-8",
|
||||
)
|
||||
module = tmp_path / "form_module.bsl"
|
||||
module.write_text(
|
||||
"""
|
||||
Процедура ПриСозданииНаСервере()
|
||||
КонецПроцедуры
|
||||
|
||||
Процедура ПередЗаписью()
|
||||
КонецПроцедуры
|
||||
""",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
snapshot = index_project(tmp_path, project_id="ui-form-events")
|
||||
|
||||
form = next(node for node in snapshot.nodes if node.kind == NodeKind.FORM)
|
||||
handlers = {
|
||||
edge.attributes["handler_name"]: edge
|
||||
for edge in snapshot.edges
|
||||
if edge.kind == EdgeKind.HANDLES and edge.source_lineage == form.lineage_id
|
||||
}
|
||||
assert set(handlers) == {"ПриСозданииНаСервере", "ПередЗаписью"}
|
||||
assert {edge.attributes["link_type"] for edge in handlers.values()} == {"FORM_EVENT"}
|
||||
Reference in New Issue
Block a user