from pathlib import Path from one_c_normalizer import ( COMMON_BRANCH_CHILDREN, METADATA_TYPE_BY_BRANCH, METADATA_TYPE_BY_CODE, build_normalized_project, normalize_bsl_source, normalize_one_c_project, normalize_source_path, parse_one_c_xml_file, ) def test_normalize_bsl_source_removes_bom_and_normalizes_newlines(): assert normalize_bsl_source("\ufeffПроцедура X()\r\n A = 1; \r\n\r\n") == ( "Процедура X()\n A = 1;\n" ) def test_normalize_source_path_uses_forward_slashes(): assert normalize_source_path("src\\module.bsl") == "src/module.bsl" def test_parse_one_c_xml_file_extracts_ui_objects(tmp_path: Path): xml = tmp_path / "form.xml" xml.write_text( """
""", encoding="utf-8", ) objects = parse_one_c_xml_file(xml) assert [item.object_kind for item in objects] == ["FORM", "COMMAND", "ATTRIBUTE"] assert objects[0].qualified_name == "Документ.Заказ.ФормаДокумента" def test_parse_one_c_xml_file_extracts_metadata_objects(tmp_path: Path): xml = tmp_path / "metadata.xml" xml.write_text( """ ЗаказПокупателя Документ.ЗаказПокупателя
ОстаткиТоваров РегистрНакопления.ОстаткиТоваров """, encoding="utf-8", ) objects = parse_one_c_xml_file(xml) assert [item.object_kind for item in objects] == [ "CATALOG", "ATTRIBUTE", "DOCUMENT", "TABULAR_SECTION", "FORM", "ACCUMULATION_REGISTER", ] assert objects[2].qualified_name == "Документ.ЗаказПокупателя" assert objects[5].name == "ОстаткиТоваров" def test_normalize_one_c_project_preserves_configuration_root_metadata(tmp_path: Path): xml = tmp_path / "configuration.xml" xml.write_text( """ """, encoding="utf-8", ) normalized = normalize_one_c_project(tmp_path, project_id="configuration-root") assert normalized.configuration.name == "УправлениеТорговлей" assert normalized.configuration.metadata["synonym"] == "Управление торговлей" assert normalized.configuration.metadata["platformVersion"] == "8.3.24" assert normalized.configuration.metadata["compatibilityMode"] == "8.3.20" assert normalized.configuration.metadata["source_path"].endswith("configuration.xml") assert [group.name for group in normalized.configuration.groups] == ["Справочники"] def test_parse_edt_mdo_derives_qualified_names(tmp_path: Path): mdo = tmp_path / "Товары.mdo" mdo.write_text( """ Товары Артикул Цены ВидЦены ФормаЭлемента """, encoding="utf-8", ) objects = parse_one_c_xml_file(mdo) by_name = {item.name: item for item in objects} assert by_name["Товары"].qualified_name == "Справочник.Товары" assert by_name["Артикул"].qualified_name == "Справочник.Товары.Артикул" assert by_name["Цены"].qualified_name == "Справочник.Товары.Цены" assert by_name["ВидЦены"].qualified_name == "Справочник.Товары.Цены.ВидЦены" assert by_name["ФормаЭлемента"].qualified_name == "Справочник.Товары.ФормаЭлемента" def test_normalize_http_service_keeps_url_templates_and_methods(tmp_path: Path): mdo = tmp_path / "ПубличныйAPI.mdo" mdo.write_text( """ ПубличныйAPI api Orders GET ПолучитьЗаказ """, encoding="utf-8", ) normalized = normalize_one_c_project(tmp_path, project_id="http-service") http_group = next(group for group in normalized.configuration.groups if group.name == "HTTP-сервисы") service = http_group.objects[0] assert service.qualified_name == "HTTPСервис.ПубличныйAPI" assert service.metadata["rootURL"] == "api" assert service.url_templates[0].name == "Orders" assert service.url_templates[0].attributes["template"] == "/orders/{id}" assert service.url_templates[0].children[0].kind == "METHOD" assert service.url_templates[0].children[0].name == "ПолучитьЗаказ" def test_normalize_edt_project_keeps_tabular_section_columns_nested(tmp_path: Path): mdo = tmp_path / "ЗаказПокупателя.mdo" mdo.write_text( """ ЗаказПокупателя Товары Номенклатура Количество """, encoding="utf-8", ) normalized = normalize_one_c_project(tmp_path, project_id="edt-tabular") documents = next(group for group in normalized.configuration.groups if group.name == "Документы") document = documents.objects[0] assert document.attributes == [] assert document.tabular_sections[0].qualified_name == "Документ.ЗаказПокупателя.Товары" assert [child.name for child in document.tabular_sections[0].children] == ["Номенклатура", "Количество"] assert [child.qualified_name for child in document.tabular_sections[0].children] == [ "Документ.ЗаказПокупателя.Товары.Номенклатура", "Документ.ЗаказПокупателя.Товары.Количество", ] def test_normalize_edt_project_preserves_source_path_and_common_object_descriptions(tmp_path: Path): common_form = tmp_path / "ФормаПодбора.mdo" common_form.write_text( """ ФормаПодбора Форма подбора Используется в подборе товаров """, encoding="utf-8", ) normalized = normalize_one_c_project(tmp_path, project_id="edt-common") groups = {group.name: group for group in normalized.configuration.groups} common_forms = groups["Общие формы"].objects assert common_forms[0].qualified_name == "ОбщаяФорма.ФормаПодбора" assert common_forms[0].source_path.endswith("ФормаПодбора.mdo") assert common_forms[0].metadata["synonym"] == "Форма подбора" assert common_forms[0].metadata["comment"] == "Используется в подборе товаров" def test_normalize_edt_project_knows_full_common_metadata_catalog(tmp_path: Path): for file_name, class_name, object_name in [ ("Продажи.mdo", "Subsystem", "Продажи"), ("Менеджер.mdo", "Role", "Менеджер"), ("ПроведениеДокументов.mdo", "Sequence", "ПроведениеДокументов"), ("ОбщийНумератор.mdo", "DocumentNumerator", "ОбщийНумератор"), ("ДоступностьСкидок.mdo", "FunctionalOption", "ДоступностьСкидок"), ("ФормаПодбора.mdo", "CommonForm", "ФормаПодбора"), ("ПубличныйAPI.mdo", "HTTPService", "ПубличныйAPI"), ]: (tmp_path / file_name).write_text( f""" {object_name} """, encoding="utf-8", ) normalized = normalize_one_c_project(tmp_path, project_id="edt-full-common") objects = { item.qualified_name for group in normalized.configuration.groups for item in group.objects } assert { "Подсистема.Продажи", "Роль.Менеджер", "Последовательность.ПроведениеДокументов", "НумераторДокументов.ОбщийНумератор", "ФункциональнаяОпция.ДоступностьСкидок", "ОбщаяФорма.ФормаПодбора", "HTTPСервис.ПубличныйAPI", }.issubset(objects) def test_normalize_project_loads_access_profiles_groups_and_users(tmp_path: Path): xml = tmp_path / "access.xml" xml.write_text( """ """, encoding="utf-8", ) normalized = normalize_one_c_project(tmp_path, project_id="access-data") assert normalized.access.profiles[0].name == "МенеджерПродаж" assert normalized.access.profiles[0].roles[0].role_qualified_name == "Роль.ЧтениеПродаж" assert normalized.access.groups[0].profile == "МенеджерПродаж" assert normalized.access.groups[0].users == ["ivanov"] assert normalized.access.users[0].full_name == "Иванов Иван" def test_normalize_edt_project_preserves_localized_descriptions(tmp_path: Path): catalog = tmp_path / "Контрагенты.mdo" catalog.write_text( """ Контрагенты ru Контрагенты ru Описание справочника контрагентов ИНН ru ИНН ru Идентификационный номер налогоплательщика """, encoding="utf-8", ) normalized = normalize_one_c_project(tmp_path, project_id="edt-localized") catalogs = next(group for group in normalized.configuration.groups if group.name == "Справочники") metadata_object = catalogs.objects[0] attribute = metadata_object.attributes[0] assert metadata_object.metadata["synonym"] == "Контрагенты" assert metadata_object.metadata["synonym_localized"] == {"ru": "Контрагенты"} assert metadata_object.metadata["comment"] == "Описание справочника контрагентов" assert metadata_object.metadata["comment_localized"] == {"ru": "Описание справочника контрагентов"} assert attribute.attributes["synonym"] == "ИНН" assert attribute.attributes["comment"] == "Идентификационный номер налогоплательщика" def test_normalize_edt_project_attaches_bsl_modules_to_metadata_objects(tmp_path: Path): catalog_dir = tmp_path / "Catalogs" / "Контрагенты" module_dir = catalog_dir / "Ext" module_dir.mkdir(parents=True) (catalog_dir / "Контрагенты.mdo").write_text( """ Контрагенты """, encoding="utf-8", ) (module_dir / "ObjectModule.bsl").write_text( """ Процедура ПроверитьКонтрагента() Экспорт КонецПроцедуры """, encoding="utf-8", ) (module_dir / "ManagerModule.bsl").write_text( """ Процедура Создать() Экспорт КонецПроцедуры """, encoding="utf-8", ) normalized = normalize_one_c_project(tmp_path, project_id="edt-modules") catalogs = next(group for group in normalized.configuration.groups if group.name == "Справочники") catalog = catalogs.objects[0] assert [module.module_kind for module in catalog.modules] == ["MANAGER_MODULE", "OBJECT_MODULE"] assert all(module.source_path.endswith(".bsl") for module in catalog.modules) assert all(module.attributes["source_hash"] for module in catalog.modules) def test_normalize_edt_project_attaches_form_modules_to_owner_with_form_name(tmp_path: Path): catalog_dir = tmp_path / "Catalogs" / "Контрагенты" form_dir = catalog_dir / "Forms" / "ФормаЭлемента" / "Ext" form_dir.mkdir(parents=True) (catalog_dir / "Контрагенты.mdo").write_text( """ Контрагенты ФормаЭлемента """, encoding="utf-8", ) (form_dir / "Module.bsl").write_text( """ Процедура ПриОткрытии() КонецПроцедуры """, encoding="utf-8", ) normalized = normalize_one_c_project(tmp_path, project_id="edt-form-module") catalog = next(group for group in normalized.configuration.groups if group.name == "Справочники").objects[0] assert catalog.modules[0].module_kind == "FORM_MODULE" assert catalog.modules[0].attributes["form_name"] == "ФормаЭлемента" assert catalog.modules[0].qualified_name == "Справочник.Контрагенты.Форма.ФормаЭлемента.Модуль" def test_normalize_edt_project_attaches_common_form_modules(tmp_path: Path): common_form_dir = tmp_path / "CommonForms" / "ФормаПодбора" module_dir = common_form_dir / "Ext" module_dir.mkdir(parents=True) (common_form_dir / "ФормаПодбора.mdo").write_text( """ ФормаПодбора """, encoding="utf-8", ) (module_dir / "Module.bsl").write_text( """ Процедура ПриСозданииНаСервере() Экспорт КонецПроцедуры """, encoding="utf-8", ) normalized = normalize_one_c_project(tmp_path, project_id="edt-common-form-module") common_forms = next(group for group in normalized.configuration.groups if group.name == "Общие формы") common_form = common_forms.objects[0] assert common_form.qualified_name == "ОбщаяФорма.ФормаПодбора" assert len(common_form.modules) == 1 assert common_form.modules[0].module_kind == "MODULE" assert common_form.modules[0].source_path.endswith("Module.bsl") def test_parse_register_dimensions_and_resources_preserves_attribute_roles(tmp_path: Path): mdo = tmp_path / "ОстаткиТоваров.mdo" mdo.write_text( """ ОстаткиТоваров Номенклатура Количество Комментарий """, encoding="utf-8", ) objects = parse_one_c_xml_file(mdo) by_name = {item.name: item for item in objects} assert by_name["ОстаткиТоваров"].object_kind == "ACCUMULATION_REGISTER" assert by_name["ОстаткиТоваров"].qualified_name == "РегистрНакопления.ОстаткиТоваров" assert by_name["Номенклатура"].attributes["attribute_role"] == "DIMENSION" assert by_name["Количество"].attributes["attribute_role"] == "RESOURCE" assert by_name["Комментарий"].attributes["attribute_role"] == "REQUISITE" def test_normalize_edt_project_preserves_specific_register_kinds(tmp_path: Path): for file_name, tag, name in ( ("Цены.mdo", "InformationRegister", "Цены"), ("ОстаткиТоваров.mdo", "AccumulationRegister", "ОстаткиТоваров"), ("Хозрасчетный.mdo", "AccountingRegister", "Хозрасчетный"), ("Начисления.mdo", "CalculationRegister", "Начисления"), ): (tmp_path / file_name).write_text( f""" {name} """, encoding="utf-8", ) normalized = normalize_one_c_project(tmp_path, project_id="edt-register-kinds") groups = {group.name: group for group in normalized.configuration.groups} assert groups["Регистры сведений"].objects[0].object_kind == "INFORMATION_REGISTER" assert groups["Регистры накопления"].objects[0].qualified_name == "РегистрНакопления.ОстаткиТоваров" assert groups["Регистры бухгалтерии"].objects[0].object_kind == "ACCOUNTING_REGISTER" assert groups["Регистры расчета"].objects[0].object_kind == "CALCULATION_REGISTER" def test_parse_one_c_xml_file_extracts_role_rights(tmp_path: Path): xml = tmp_path / "roles.xml" xml.write_text( """ """, encoding="utf-8", ) objects = parse_one_c_xml_file(xml) assert [item.object_kind for item in objects] == ["ROLE", "RIGHT"] assert objects[1].name == "Документ.ЗаказПокупателя" assert objects[1].attributes["role"] == "Роль.Менеджер" assert objects[1].attributes["post"] == "true" def test_normalized_project_attaches_role_rights(tmp_path: Path): xml = tmp_path / "roles.xml" xml.write_text( """ """, encoding="utf-8", ) normalized = build_normalized_project(parse_one_c_xml_file(xml), project_id="roles", source_path=str(tmp_path)) roles_group = next(group for group in normalized.configuration.groups if group.name == "Роли") role = roles_group.objects[0] assert role.name == "Менеджер" assert role.rights[0].target == "Документ.ЗаказПокупателя" assert role.rights[0].permissions["write"] == "true" def test_parse_one_c_xml_file_extracts_child_element_command_action_and_role_right(tmp_path: Path): xml = tmp_path / "metadata.xml" xml.write_text( """ ЗаказПокупателя Документ.ЗаказПокупателя ФормаДокумента Провести ПровестиКоманда Менеджер Документ.ЗаказПокупателя
""", encoding="utf-8", ) objects = parse_one_c_xml_file(xml) by_kind = {item.object_kind: item for item in objects} command = next(item for item in objects if item.object_kind == "COMMAND") right = next(item for item in objects if item.object_kind == "RIGHT") assert by_kind["FORM"].qualified_name == "Документ.ЗаказПокупателя.ФормаДокумента" assert command.attributes["Action"] == "ПровестиКоманда" assert right.name == "Документ.ЗаказПокупателя" assert right.attributes["role"] == "Роль.Менеджер" def test_normalized_project_groups_extended_1c_objects(tmp_path: Path): xml = tmp_path / "extended.xml" xml.write_text( """ """, encoding="utf-8", ) objects = parse_one_c_xml_file(xml) normalized = build_normalized_project(objects, project_id="extended", source_path=str(tmp_path)) groups = {group.name: group for group in normalized.configuration.groups} document = next(item for item in groups["Документы"].objects if item.name == "ЗаказПокупателя") assert "Подсистемы" in groups assert "HTTP-сервисы" in groups assert "XDTO-пакеты" in groups assert document.layouts[0].name == "ПечатнаяФорма" assert document.movements[0].name == "Остатки" assert normalized.configuration.extensions[0].name == "CRM" def test_normalize_one_c_project_skips_unreadable_or_invalid_metadata(tmp_path: Path): (tmp_path / "valid.xml").write_text( """ """, encoding="utf-8", ) (tmp_path / "broken.xml").write_text("", encoding="utf-8") normalized = normalize_one_c_project(tmp_path, project_id="skip-invalid") catalogs = next(group for group in normalized.configuration.groups if group.name == "Справочники") assert catalogs.objects[0].qualified_name == "Справочник.Контрагенты" def test_configuration_extension_has_own_metadata_tree_structure(tmp_path: Path): xml = tmp_path / "extension.mdo" xml.write_text( """ CRM 1.0 КонтрагентыCRM ВнешнийКод CRMСервер """, encoding="utf-8", ) objects = parse_one_c_xml_file(xml) normalized = build_normalized_project(objects, project_id="extension-structure", source_path=str(tmp_path)) assert normalized.configuration.groups == [] extension = normalized.configuration.extensions[0] assert extension.name == "CRM" assert extension.version == "1.0" extension_groups = {group.name: group for group in extension.groups} assert extension_groups["Справочники"].objects[0].name == "КонтрагентыCRM" assert extension_groups["Справочники"].objects[0].attributes[0].name == "ВнешнийКод" assert extension_groups["Общие модули"].objects[0].name == "CRMСервер" def test_report_metadata_parts_include_dcs_variants_settings_and_tabular_documents(tmp_path: Path): xml = tmp_path / "report.mdo" xml.write_text( """ АнализПродаж Период Показатели ФормаОтчета ПечатнаяФорма ТабличныйДокумент ОсновнаяСхемаКомпоновкиДанных Основной НастройкиПоУмолчанию """, encoding="utf-8", ) objects = parse_one_c_xml_file(xml) normalized = build_normalized_project(objects, project_id="report-parts", source_path=str(tmp_path)) reports = next(group for group in normalized.configuration.groups if group.name == "Отчеты") report = reports.objects[0] assert report.qualified_name == "Отчет.АнализПродаж" assert report.attributes[0].name == "Период" assert report.tabular_sections[0].name == "Показатели" assert report.forms[0].name == "ФормаОтчета" assert report.layouts[0].name == "ПечатнаяФорма" assert report.tabular_documents[0].name == "ТабличныйДокумент" assert report.data_composition_schemas[0].name == "ОсновнаяСхемаКомпоновкиДанных" assert report.report_variants[0].name == "Основной" assert report.report_settings[0].name == "НастройкиПоУмолчанию" def test_metadata_catalog_describes_core_1c_tree_branches(): assert "Общие модули" in COMMON_BRANCH_CHILDREN assert "HTTP-сервисы" in COMMON_BRANCH_CHILDREN document = METADATA_TYPE_BY_CODE["DOCUMENT"] assert document.tree_branch == "Документы" assert "Реквизиты" in document.child_groups assert "Табличные части" in document.child_groups assert "Движения" in document.child_groups assert document.module_kinds == ("Модуль объекта", "Модуль менеджера") register = METADATA_TYPE_BY_CODE["ACCUMULATION_REGISTER"] assert register.tree_branch == "Регистры накопления" assert "Измерения" in register.child_groups assert "Ресурсы" in register.child_groups common_form = METADATA_TYPE_BY_CODE["COMMON_FORM"] assert common_form.tree_branch == "Общие формы" assert "Модуль формы" in common_form.child_groups report = METADATA_TYPE_BY_CODE["REPORT"] assert "СКД" in report.child_groups assert "Табличные документы" in report.child_groups assert "Варианты отчета" in report.child_groups assert "Настройки" in report.child_groups assert METADATA_TYPE_BY_BRANCH["XDTO-пакеты"].code == "XDTO_PACKAGE" assert METADATA_TYPE_BY_BRANCH["Сервисы интеграции"].code == "INTEGRATION_SERVICE"