Сохранить состояние TESTLAMP при кратких провалах входа и исключить повторный вход Timer1 во время передачи индикации. Явно сбрасывать служебный обмен CAN при запуске и выбирать соседнюю фазу по локальному индексу канала АЦП. Сохранить текущую последовательность DATA/CLK и проверочные варианты таймингов, добавить поведенческие тесты и обновить образ прошивки с картой памяти.
45 lines
7.3 KiB
Python
45 lines
7.3 KiB
Python
# Проверяет передачу фаз и диагностику на настоящих функциях measure.c без аппаратуры C28x.
|
||
import argparse # Принимает путь к компилятору хостовой модели, не привязывая тест к одной машине.
|
||
import os # Добавляет каталог компилятора в PATH для запуска его вспомогательных программ.
|
||
from pathlib import Path # Разрешает пути относительно теста независимо от рабочего каталога.
|
||
import re # Находит границы функций и единственное место исторической мутации.
|
||
import subprocess # Компилирует модель и проверяет коды завершения исполняемых проверок.
|
||
import tempfile # Размещает сгенерированный C и исполняемые файлы вне исходного дерева.
|
||
|
||
ROOT = Path(__file__).resolve().parents[2] # Поднимается от tests/current_measure к корню репозитория.
|
||
SOURCE = ROOT / "Source/Internal/measure.c" # Выбирает текущую production-реализацию измерений.
|
||
|
||
def extract(source, name): # Извлекает одну настоящую функцию, чтобы тест не содержал копию алгоритма.
|
||
match = re.search(r"(?:void|int|float)\s+" + name + r"\([^;{}]*\)\s*\{", source) # Отличает определение от прототипа и вызовов.
|
||
assert match is not None, name # Явно завершает тест, если production-функция исчезла или сменила форму.
|
||
end = source.index("{", match.start()) + 1 # Начинает подсчёт вложенности сразу после открывающей скобки.
|
||
depth = 1 # Учитывает уже найденный внешний блок функции.
|
||
while depth: # Читает определение целиком вместе с вложенными условиями.
|
||
depth += (source[end] == "{") - (source[end] == "}") # Находит соответствующую закрывающую скобку без обрезки вложенных блоков.
|
||
end += 1 # Включает последнюю обработанную скобку в извлекаемый диапазон.
|
||
return source[match.start():end] # Возвращает production-код без изменения его вычислений.
|
||
|
||
parser = argparse.ArgumentParser(description="Проверка фаз COMM и LOAD на хосте") # Описывает назначение самостоятельного запуска.
|
||
parser.add_argument("--cc", required=True, help="Путь к GCC") # Позволяет выбрать доступный C-компилятор без изменения теста.
|
||
args = parser.parse_args() # Разбирает аргументы до создания временных файлов.
|
||
source = re.sub(r"/\*.*?\*/|//[^\n]*", "", SOURCE.read_text(encoding="utf-8"), flags=re.S) # Убирает комментарии с примерами скобок, сохраняя исполняемый код.
|
||
body = "\n".join(extract(source, name) for name in ("im_calc", "er_anal", "reset_errs", "Current_count")) # Включает реальные расчёты, накопление ошибки и запись диагностических флагов.
|
||
before, after = Path(__file__).with_name("model.inc").read_text(encoding="utf-8").split("/* HANDLERS */") # Вставляет production-функции между моделью окружения и сценариями.
|
||
legacy, replacements = re.subn(r"\bpair\s*=\s*chan\s*\^\s*1\s*;", "pair = sens_pair[chan];", body) # Возвращает старую ошибку только во временную мутационную сборку.
|
||
assert replacements == 1, "Не найдено единственное место выбора пары фаз" # Не допускает фиктивной мутационной проверки неизменённого кода.
|
||
environment = dict(os.environ) # Сохраняет окружение вызывающего процесса для компилятора.
|
||
environment["PATH"] = str(Path(args.cc).resolve().parent) + os.pathsep + environment.get("PATH", "") # Делает библиотеки и вспомогательные программы GCC доступными дочерним процессам.
|
||
with tempfile.TemporaryDirectory(prefix="current-measure-") as directory: # Автоматически удаляет только временные артефакты этого запуска.
|
||
for name, functions in (("production", body), ("legacy", legacy)): # Прогоняет одинаковые поведенческие сценарии на исправлении и исторической ошибке.
|
||
c_path = Path(directory) / (name + ".c") # Разделяет исходники двух сборок для однозначных диагностик компилятора.
|
||
exe_path = Path(directory) / (name + ".exe") # Даёт каждой модели собственный исполняемый файл.
|
||
c_path.write_text(before + functions + after, encoding="utf-8") # Сохраняет временный C с русскими пояснениями в UTF-8.
|
||
subprocess.run([args.cc, "-std=c99", "-O3", "-fno-strict-aliasing", "-Werror=implicit-function-declaration", "-I", str(ROOT / "Source/Internal/Include"), str(c_path), "-lm", "-o", str(exe_path)], check=True, env=environment) # Компилирует с O3; отключение alias-оптимизации сохраняет production-псевдонимы ERROR поверх Modbus на хосте.
|
||
result = subprocess.run([str(exe_path)], capture_output=True, text=True, env=environment, timeout=20) # Ограничивает зависание и сохраняет результат каждого поведенческого сценария.
|
||
if name == "production": # Исправленная реализация обязана пройти все проверки без исключений.
|
||
print(result.stdout, end="") # Показывает сценарии, успешно проверенные настоящими функциями прошивки.
|
||
assert result.returncode == 0, result.stdout + result.stderr # Возвращает сбой запуска при любой регрессии исправленного кода.
|
||
else: # Старая строка должна воспроизводимо ломать и значения фаз, и диагностику.
|
||
assert result.returncode == 1 and "FAIL Modbus" in result.stdout and "FAIL Wry" in result.stdout, result.stdout + result.stderr # Доказывает чувствительность теста к обоим последствиям именно этой ошибки.
|
||
print("PASS mutation: legacy pair rejected by Modbus and Wry checks") # Сообщает об ожидаемом провале временной сборки со старой строкой.
|