"""Проверки CCS/Keil с интерфейсами pathlib и shutil, доступными в Python 3.7.""" # Воспроизводит ограничения старого Python без установки другого интерпретатора. import importlib.util # Загружает проверяемые инструменты именно из этой библиотеки templates. import json # Проверяет сохранённый счётчик и фактический паспорт компиляции. from pathlib import Path # Моделирует прежнюю сигнатуру удаления временных файлов. import shutil # Моделирует прежнюю сигнатуру копирования каталогов. import tempfile # Ограничивает запись изолированным каталогом теста. import unittest # Проверки запускаются стандартным Python без сторонних зависимостей. from unittest.mock import patch # Подменяет только несовместимые сигнатуры стандартной библиотеки. def load_tool(name): # Импортирует одну точку входа без запуска CLI. spec = importlib.util.spec_from_file_location("compatibility_" + name, Path(__file__).resolve().parents[1] / "tools" / (name + ".py")) # Выбор не зависит от PATH компьютера сборки. module = importlib.util.module_from_spec(spec) # Отдельный экземпляр сохраняет изоляцию других тестов. spec.loader.exec_module(module) # Загружает публичные функции подготовки и копирования. return module # Тесты вызывают те же функции, что BAT-обвязки двух IDE. identity, c2000 = load_tool("identity"), load_tool("c2000_build") # Обе цепочки используют общий формат паспорта. original_unlink, original_copytree = Path.unlink, shutil.copytree # Сохраняет настоящие реализации перед ограничением их сигнатур. def unlink37(path): # Python 3.7 не допускает аргумент missing_ok. return original_unlink(path) # Реальное удаление сохраняет настоящие ошибки доступа и отсутствующего файла. def copytree37(source, destination, symlinks=False, ignore=None, copy_function=shutil.copy2, ignore_dangling_symlinks=False): # Сигнатура Python 3.7 не допускает dirs_exist_ok. return original_copytree(source, destination, symlinks, ignore, copy_function, ignore_dangling_symlinks) # Старый интерфейс продолжает копировать обычные новые каталоги. class Python37CompatibilityTests(unittest.TestCase): # Проверяет успешный prebuild и повторный экспорт вместо одного синтаксического анализа. def setUp(self): # Каждый сценарий начинает с отдельного проекта со включённым итератором версии. temporary = tempfile.TemporaryDirectory(prefix="firmware python37 ") # Пробелы в пути соответствуют обычным пользовательским библиотекам. self.addCleanup(temporary.cleanup) # Очистка выполняется после снятия ограничений стандартной библиотеки. self.root = Path(temporary.name) # Рабочие проекты CCS/Keil не изменяются. self.config = self.root / "firmware_identity.json" # Конфигурация принадлежит только временной проверке. self.config.write_text(json.dumps({"product": "TEST_BOARD", "target": "TMS320F2812", "version": [1, 2, 3], "increment_version": True}), encoding="utf-8") # Этот режим воспроизводит исходную ошибку unlink после увеличения версии. def test_keil_and_ccs_prepare_generate_headers_with_old_unlink(self): # Обе обвязки должны завершить генерацию при старой сигнатуре pathlib. with patch.object(Path, "unlink", unlink37): # Любой оставшийся missing_ok вызовет тот же TypeError, что в журнале пользователя. keil = identity.prepare(self.root, self.root, self.config, self.root / "Generated/target_0") # Keil напрямую вызывает общий prepare. ccs = c2000.prepare(self.root, Path(__file__).resolve().parents[3], self.root / "STEND40_PCH_1/generated", config=self.config) # CCS загружает генератор из полной проверяемой библиотеки templates. for relative, expected in (("Generated/target_0", [1, 2, 4]), ("STEND40_PCH_1/generated", [1, 2, 5])): # Две попытки резервируют разные номера без пропуска после TypeError. output = self.root / relative # Проверяет фактический include-каталог каждой IDE. self.assertIn("#define FIRMWARE_VERSION_PATCH " + str(expected[2]) + "U", (output / "firmware_identity.h").read_text(encoding="utf-8")) # Заголовок существует и содержит зарезервированную версию. self.assertEqual(json.loads((output / "firmware_identity.json").read_text(encoding="utf-8"))["version"], expected) # Паспорт совпадает с константами для компилятора. self.assertEqual((keil["version"], ccs["version"]), ([1, 2, 4], [1, 2, 5])) # Обе публичные функции возвращают успешный результат. def test_archive_export_and_atomic_copy_support_old_file_apis(self): # Проверяет повторное сохранение комплекта после успешного prebuild. generated = self.root / "generated" # Метаданные сохраняются отдельно от исходного JSON. identity.prepare(self.root, self.root, self.config, generated) # Получает настоящий паспорт для проверки SHA-256 и архива. image = self.root / "application.bin" # Произвольные байты достаточны для общего Keil-контракта файловой целостности. image.write_bytes(b"firmware bytes") # Проверяет сохранение точных байтов при нескольких копированиях. with patch.object(Path, "unlink", unlink37), patch.object(shutil, "copytree", copytree37): # Воспроизводит оба ограничения Python 3.7 одновременно. for attempt in range(2): # Второй экспорт должен объединять уже существующие каталоги. result = identity.archive_build(image, generated / "firmware_identity.json", self.root / "archive", export=self.root / "export") # Печать папки соответствует настоящему postcompile Keil. folder = Path(result["directory"]) # Использует точный архив текущего вызова. (folder / "nested").mkdir(exist_ok=True) # Вложенный сопроводительный файл проверяет рекурсивное копирование. (folder / "nested/map.txt").write_text("map " + str(attempt), encoding="utf-8") # Повторный выпуск обновляет существующий файл. identity.copy_tree(folder, self.root / "ccs-output", copy_function=c2000.atomic_copy) # C2000 сохраняет атомарную замену с прежним интерфейсом unlink. self.assertEqual((self.root / "ccs-output/nested/map.txt").read_text(encoding="utf-8"), "map 1") # Вложенные файлы обновились при повторном выпуске. self.assertEqual((Path(result["exported_directory"]) / image.name).read_bytes(), image.read_bytes()) # Экспорт содержит исходный образ без изменения байтов. self.assertFalse(list((self.root / "ccs-output").rglob(".firmware-*"))) # Успешная атомарная замена не оставляет незавершённых файлов. if __name__ == "__main__": # Позволяет запустить регрессию стандартным unittest на компьютере сборки. unittest.main() # Ненулевой результат выявляет несовместимость инструментов до настройки IDE.