Загрузка данных


constants/architecture_constants.py
    CONFIG_PATH: str = f"/data/{STAND_ENV_NAMING}/configs"
    SIGNAL_UNIT_CONVERSION_RULES_FILE_NAME: str = "signal_unit_conversion_rules.json"
    SIGNAL_UNIT_CONVERSION_RULES_BACKUP_DIR: str = "original_conversion_rules"


constants/enums.py

внизу 

class MeasureConversionRule(Enum):
    MPA_MEASURE = "MPA_MEASURE"
    KG_CM_MEASURE = "KG_CM_MEASURE"
    




constants/test_constants.py

внизу
class MeasureUnitConstants:
    MPA_MEASURE: str = "MPa"
    KG_CM_MEASURE: str = "kgf/cm^2"





infra/cmd_generator.py
class SignalUnitConversionCmdGenerator(BaseCmdGenerator):
    def generate_scp_signal_rules_from_stand_cmd(self) -> str:
        """
        Генерирует команду копирования signal_unit_conversion_rules.json со стенда на runner
        """
        remote_path = self._generate_path_to_remote_signal_rules()
        return f"{self._scp_cmd} {self._username}@{self._host}:{remote_path} ."

    def generate_scp_signal_rules_to_stand_cmd(self, local_file_path: str) -> str:
        """
        Генерирует команду копирования локального файла на стенд 
        """
        remote_path = self._generate_path_to_remote_signal_rules()
        return f"{self._scp_cmd} {local_file_path} {self._username}@{self._host}:{remote_path}"

    @staticmethod
    def _generate_path_to_remote_signal_rules() -> PurePosixPath:
        return PurePosixPath(Im_const.CONFIG_PATH) / Im_const.SIGNAL_UNIT_CONVERSION_RULES_FILE_NAME





infra/signal_unit_conversion_manager.py
import json
import logging
import shutil
from pathlib import Path
from typing import Any

from clients.subprocess_client import SubprocessClient
from constants.architecture_constants import ClickhouseConstants as CH_const
from constants.architecture_constants import ImitatorConstants as Im_const
from constants.enums import MeasureConversionRule
from infra.cmd_generator import SignalUnitConversionCmdGenerator
from utils.helpers.signal_unit_conversion_utils import (
    apply_measure_conversion_rule,
    conversion_rules_need_update,
)

logger = logging.getLogger(__name__)


class SignalUnitConversionManager:
    """
    Управляет signal_unit_conversion_rules.json на стенде:
    - скачивает оригинал в original_conversion_rules/ перед прогоном набора
    - подкладывает изменённую версию в CONFIG_PATH (имя на сервере не меняется)
    - восстанавливает оригинал в teardown
    """

    def __init__(
        self,
        stand_client: SubprocessClient,
        measure_conversion_rule: MeasureConversionRule | None,
    ) -> None:
        self._stand_client = stand_client
        self._measure_conversion_rule = measure_conversion_rule
        self._cmd_generator = SignalUnitConversionCmdGenerator(stand_client.username, stand_client.host)
        self._local_file = Path(Im_const.SIGNAL_UNIT_CONVERSION_RULES_FILE_NAME)
        self._backup_file = (
            Path(Im_const.SIGNAL_UNIT_CONVERSION_RULES_BACKUP_DIR) / Im_const.SIGNAL_UNIT_CONVERSION_RULES_FILE_NAME
        )
        self._modified = False

    def setup_signal_unit_conversion_rules(self) -> None:
        """
        Скачивает файл со стенда, при необходимости меняет единицы и загружает обратно.
        """
        if self._measure_conversion_rule is None:
            logger.info("[SETUP] [SKIP] measure_conversion_rules не задан для набора данных")
            return

        try:
            self._download_from_stand()
            rules_json = self._read_local_file()

            if not conversion_rules_need_update(rules_json, self._measure_conversion_rule):
                logger.info(
                    "[SETUP] [OK] signal_unit_conversion_rules.json уже настроен корректно для набора данных "
                    f"(правило {self._measure_conversion_rule.name})"
                )
                return

            self._save_backup()
            modified_rules = apply_measure_conversion_rule(rules_json, self._measure_conversion_rule)
            self._write_local_file(modified_rules)
            self._upload_to_stand(self._local_file)
            self._modified = True
            logger.info(
                "[SETUP] [OK] signal_unit_conversion_rules.json обновлён по правилу "
                f"{self._measure_conversion_rule.name}"
            )
        except Exception as error:
            error_msg = "[SETUP] [ERROR] Ошибка при подготовке signal_unit_conversion_rules.json"
            logger.exception(error_msg)
            raise RuntimeError(error_msg) from error

    def restore_signal_unit_conversion_rules(self) -> None:
        """
        Возвращает оригинальный signal_unit_conversion_rules.json на стенд
        """
        if not self._modified:
            logger.info("[TEARDOWN] [SKIP] signal_unit_conversion_rules.json не изменялся")
            return

        if not self._backup_file.exists():
            error_msg = (
                "[TEARDOWN] [ERROR] Оригинал signal_unit_conversion_rules.json не найден: "
                f"{self._backup_file}"
            )
            logger.error(error_msg)
            raise RuntimeError(error_msg)

        try:
            self._upload_to_stand(self._backup_file)
            self._modified = False
            logger.info(
                "[TEARDOWN] [OK] signal_unit_conversion_rules.json восстановлен на стенде "
                f"из {self._backup_file}"
            )
        except Exception as error:
            error_msg = "[TEARDOWN] [ERROR] Ошибка при восстановлении signal_unit_conversion_rules.json"
            logger.exception(error_msg)
            raise RuntimeError(error_msg) from error

    def _download_from_stand(self) -> None:
        copy_cmd = self._cmd_generator.generate_scp_signal_rules_from_stand_cmd()
        self._stand_client.run_cmd(copy_cmd, timeout=CH_const.LONG_PROCESS_TIMEOUT_S, use_ssh=False)
        if not self._local_file.exists():
            raise FileNotFoundError(
                f"Не удалось скачать {Im_const.SIGNAL_UNIT_CONVERSION_RULES_FILE_NAME} со стенда"
            )

    def _save_backup(self) -> None:
        self._backup_file.parent.mkdir(parents=True, exist_ok=True)
        shutil.copy2(self._local_file, self._backup_file)

    def _upload_to_stand(self, local_file: Path) -> None:
        upload_cmd = self._cmd_generator.generate_scp_signal_rules_to_stand_cmd(local_file.as_posix())
        self._stand_client.run_cmd(upload_cmd, timeout=CH_const.LONG_PROCESS_TIMEOUT_S, use_ssh=False)

    def _read_local_file(self) -> dict[str, Any]:
        error_msg = (
            f"[SETUP] [ERROR] Не удалось декодировать файл {self._local_file} "
            f"в кодировках {Im_const.DEFAULT_ENCODINGS}"
        )
        for encoding in Im_const.DEFAULT_ENCODINGS:
            try:
                with open(self._local_file, "r", encoding=encoding, errors="strict") as rules_file:
                    data = json.load(rules_file)
                    if not data:
                        raise ValueError(f"Пустой json (кодировка:{encoding})")
                    return data
            except UnicodeDecodeError:
                continue
            except json.JSONDecodeError as error:
                logger.exception(error_msg)
                raise OSError(error_msg) from error
        logger.exception(error_msg)
        raise OSError(error_msg)

    @staticmethod
    def _write_local_file(rules_json: dict[str, Any]) -> None:
        with open(
            Im_const.SIGNAL_UNIT_CONVERSION_RULES_FILE_NAME,
            "w",
            encoding=Im_const.ENCODING_UTF_8,
        ) as rules_file:
            json.dump(rules_json, rules_file, ensure_ascii=False, indent=2)



















infra/stand_setup_manager.py


from constants.enums import MeasureConversionRule, TU




from infra.signal_unit_conversion_manager import SignalUnitConversionManager



        test_data_name: str,  # Название архива данных имитатора для загрузки из TestOps
        tu_id: int,
        measure_conversion_rules: MeasureConversionRule | None = None,




        self._test_data_name = test_data_name
        self._tu_id = tu_id
        self._measure_conversion_rules = measure_conversion_rules




                self._uploader.upload_with_confirm()

            self.stop_all_containers()
            self._signal_unit_conversion_manager.setup_signal_unit_conversion_rules()







            logger.exception(error_msg)
            raise RuntimeError(error_msg) from error

    def restore_signal_unit_conversion_rules(self) -> None:
        """
        Возвращает оригинальный signal_unit_conversion_rules.json на стенд.
        """
        self._signal_unit_conversion_manager.restore_signal_unit_conversion_rules()





            self._configuration_manager = ConfigurationManager(self._configuration_file_name)
            self._signal_unit_conversion_manager = SignalUnitConversionManager(
                self._stand_client, self._measure_conversion_rules
            )






































test_config/models_for_tests.py

    LdsStatus,
    MeasureConversionRule,




    # ===== Технологический участок =====
    technological_unit: TU = TU.TIKHORETSK_NOVOROSSIYSK_3

    # ===== Правила конвертации единиц измерения давления на стенде =====
    measure_conversion_rules: Optional[MeasureConversionRule] = None
















utils/helpers/signal_unit_conversion_utils.py

import copy
from typing import Any

from constants.enums import MeasureConversionRule
from constants.test_constants import MeasureUnitConstants


def _resolve_unit_mapping(rule: MeasureConversionRule) -> tuple[str, str]:
    if rule == MeasureConversionRule.MPA_MEASURE:
        return MeasureUnitConstants.KG_CM_MEASURE, MeasureUnitConstants.MPA_MEASURE
    if rule == MeasureConversionRule.KG_CM_MEASURE:
        return MeasureUnitConstants.MPA_MEASURE, MeasureUnitConstants.KG_CM_MEASURE
    raise ValueError(f"Неизвестное правило конвертации единиц измерения: {rule}")


def conversion_rules_need_update(rules_json: dict[str, Any], rule: MeasureConversionRule) -> bool:
    """
    Проверяет, есть ли в файле единицы давления, которые нужно заменить по правилу набора.
    """
    source_unit, _ = _resolve_unit_mapping(rule)
    return any(signal.get("OriginUnit") == source_unit for signal in rules_json.get("Signals", []))


def apply_measure_conversion_rule(rules_json: dict[str, Any], rule: MeasureConversionRule) -> tuple[dict[str, Any], int]:
    """
    Возвращает копию rules_json с заменой единиц давления и число заменённых сигналов.

    Замена строго по полному совпадению OriginUnit с исходной единицей давления
    (kgf/cm^2 <-> MPa). Остальные единицы (cSt, m^3/h, rpm и т.д.) не затрагиваются.
    """
    source_unit, target_unit = _resolve_unit_mapping(rule)
    result = copy.deepcopy(rules_json)

    for signal in result.get("Signals", []):
        origin_unit = signal.get("OriginUnit")
        if origin_unit == source_unit:
            signal["OriginUnit"] = target_unit
            
    return result























conftest.py


from test_config.datasets import ALL_SMOKE_CONFIGS, get_config_by_name




def _find_config_by_suite_name(suite_name: str):
    """Находит конфиг по имени набора данных."""
    try:
        return get_config_by_name(suite_name)
    except ValueError:
        return None





        if stand_manager := cfg["stand_manager"]:
            stand_manager.stop_imitator_wrapper()
            try:
                stand_manager.restore_signal_unit_conversion_rules()
            except Exception:
                logger.exception(
                    "[ERROR] [SETUP] Ошибка при восстановлении signal_unit_conversion_rules.json "
                    "перед запуском нового набора"
                )







        imitator_duration = compute_imitator_duration(item, current_test_suite)

        suite_config = _find_config_by_suite_name(current_test_suite)
        measure_conversion_rules = (
            suite_config.measure_conversion_rules if suite_config is not None else None
        )







        stand_manager = StandSetupManager(
            duration_m=imitator_duration,
            test_data_id=data_id,
            test_data_name=test_data_name,
            tu_id=tu_id,
            measure_conversion_rules=measure_conversion_rules,
        )






        if stand_manager := cfg["stand_manager"]:
            stand_manager.stop_imitator_wrapper()
            try:
                stand_manager.restore_signal_unit_conversion_rules()
            except Exception:
                logger.exception(
                    "[ERROR] [TEARDOWN] Ошибка при восстановлении signal_unit_conversion_rules.json"
                )






                stand_manager.stop_imitator_wrapper()
            except Exception:
                logger.exception("[ERROR] [TEARDOWN] Ошибка при остановке имитатора")
            try:
                stand_manager.restore_signal_unit_conversion_rules()
            except Exception:
                logger.exception(
                    "[ERROR] [TEARDOWN] Ошибка при восстановлении signal_unit_conversion_rules.json"
                )