Initial SFERA platform baseline

This commit is contained in:
2026-05-16 19:03:49 +03:00
commit 3b845c8fce
282 changed files with 55045 additions and 0 deletions
+11
View File
@@ -0,0 +1,11 @@
# sfera-semantic-kernel
Semantic indexing kernel for 1C projects.
Provides:
- BSL module indexing into SIR;
- Rust BSL parser JSON contract consumption when the CLI is configured or auto-discovered;
- Python BSL parser fallback for development and tests;
- XML metadata indexing for core 1C objects;
- metadata-to-module links and semantic edges for calls, queries, writes, handlers, scheduled jobs, roles, and integrations.
+11
View File
@@ -0,0 +1,11 @@
[project]
name = "sfera-semantic-kernel"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = [
"sfera-one-c-normalizer",
"sfera-sir",
]
[tool.uv]
package = true
File diff suppressed because it is too large Load Diff
@@ -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"}