Добавить генерацию карт COFF для TMS320F28335 и TMS320F2812, проверку CMD/MAP и русскую инструкцию. Проверки: 14 тестов, включая четыре реальные линковки TI для RAM и Flash.
113 lines
20 KiB
Python
113 lines
20 KiB
Python
"""Регрессии карт памяти и необязательная реальная COFF-линковка четырёх профилей TI.""" # Проверяет полезные свойства карты, не повторяя реализацию parser.
|
|
import argparse # Принимает необязательный путь к установленному TI CGT для smoke.
|
|
import contextlib # Перехватывает stdout CLI при проверке отказа перезаписи.
|
|
import importlib.util # Импортирует helper относительно каталога теста без установки пакета.
|
|
import re # Разбирает контекст диагностической строки MAP при отказе smoke, не меняя рабочий parser.
|
|
import io # Сохраняет русскую диагностику CLI в памяти теста.
|
|
from pathlib import Path # Временные CMD/MAP не пишутся в пользовательский проект.
|
|
import subprocess # Вызывает настоящий TI compiler/linker только по явному параметру --ti.
|
|
import sys # Даёт возможность запускать тесты обычным Python3.
|
|
import tempfile # Удаляет только собственные временные сборки после тестов.
|
|
import unittest # Стандартная библиотека не требует pytest или скачивания зависимостей.
|
|
ROOT = Path(__file__).resolve().parents[1] # Находит установленную утилиту независимо от рабочего каталога.
|
|
SPEC = importlib.util.spec_from_file_location('c2000_linker', ROOT / 'c2000_linker.py') # Загружает именно проверяемый файл helper.
|
|
HELPER = importlib.util.module_from_spec(SPEC) # Создаёт модуль без запуска CLI.
|
|
sys.modules[SPEC.name] = HELPER # dataclass находит собственный модуль при аннотировании Region.
|
|
SPEC.loader.exec_module(HELPER) # Исполняет только определения функций и числовых профилей.
|
|
TI_ROOT = None # Путь compiler задаётся пользователем тестов, не ищется в глобальной системе молча.
|
|
|
|
|
|
def checked(text, device='28335', boot='flash'): # Проверяет сгенерированный или намеренно повреждённый CMD реальными функциями helper.
|
|
regions, sections, issues = HELPER.parse_cmd(text) # Использует рабочий parser, а не его тестовую копию.
|
|
return regions, sections, issues + HELPER.validate_cmd(regions, sections, device, boot) # Сохраняет и неполный синтаксис, и аппаратные нарушения.
|
|
|
|
|
|
class MapsTest(unittest.TestCase): # Проверяет границы ответственности и основные причины реальных ошибок linker.
|
|
def test_four_profiles(self): # Обе модели и оба boot mode должны генерироваться без неполной проверки.
|
|
for device in ('28335', '2812'): # F2812 не наследует адреса Flash или зеркала F28335.
|
|
for boot in ('ram', 'flash'): # RAM boot и внутренняя Flash имеют разные BEGIN.
|
|
with self.subTest(device=device, boot=boot): # Показывает конкретный аппаратный профиль при регрессии.
|
|
regions, sections, issues = checked(HELPER.generate(device, boot), device, boot) # Проверяет готовый текст, включая каждый русский комментарий CMD.
|
|
self.assertEqual(issues, []) # Собственные профили обязаны поддерживаться полностью.
|
|
self.assertEqual(next(r for r in regions if r.name == 'BEGIN').origin, HELPER.DEVICES[device]['begin' if boot == 'flash' else 'ram_entry']) # BEGIN соответствует первичным адресам TI.
|
|
def test_overlap_across_pages(self): # PAGE0/PAGE1 не дают две независимые копии L0.
|
|
text = HELPER.generate('28335', 'flash').replace('RAM_DATA : origin = 0x009000', 'RAM_DATA : origin = 0x008000') # Намеренно размещает данные поверх RAM-функций на другой странице.
|
|
self.assertIn('PHYSICAL_OVERLAP', [i['code'] for i in checked(text)[2]]) # Ошибка обнаруживается до запуска TI.
|
|
def test_high_alias(self): # Высокое зеркало L0–L3 также не добавляет физической RAM.
|
|
text = HELPER.generate('28335', 'ram').replace('RAM_DATA : origin = 0x00C000', 'RAM_DATA : origin = 0x3F8000') # Данные используют высокое зеркало того же L0–L3, где лежит код.
|
|
self.assertIn('PHYSICAL_OVERLAP', [i['code'] for i in checked(text, boot='ram')[2]]) # Проверка сравнивает физические адреса, а не строки чисел.
|
|
def test_flash_reserved(self): # Широкий FLASH_APP не может поглотить CSM_RSVD, BEGIN и пароль.
|
|
text = HELPER.generate('2812', 'flash').replace('length = 0x01FF80', 'length = 0x020000') # Расширяет код до физического конца Flash поверх служебных слов.
|
|
self.assertIn('RESERVED_REGION', [i['code'] for i in checked(text, '2812')[2]]) # Адреса 2812 сверяются по собственному профилю.
|
|
def test_otp_write(self): # Отдельное имя OTP не разрешает программировать его обычной секцией.
|
|
text = HELPER.generate('28335', 'flash').replace('PAGE 0:', 'PAGE 0:\n OTP : origin = 0x380400, length = 0x400', 1).replace(' .reset :', ' unsafe : > OTP, PAGE = 0\n .reset :') # Вводит потенциально необратимую загрузку данных в OTP.
|
|
self.assertIn('RESERVED_SECTION', [i['code'] for i in checked(text)[2]]) # Наличие резервного MEMORY не делает его записываемой RAM.
|
|
def test_flash_text_cannot_load_only_to_ram(self): # Рабочий после JTAG код не обязательно сохраняется при холодном Flash reset.
|
|
text = HELPER.generate('28335', 'flash').replace('.text : > FLASH_APP', '.text : > RAM_CODE') # Сохраняет корректные codestart и ramfuncs, но намеренно теряет постоянную копию основного кода.
|
|
self.assertIn('FLASH_LOAD', [i['code'] for i in checked(text)[2]]) # Flash-профиль не должен подтверждать такую карту полным успехом.
|
|
def test_copy_contract(self): # Flash RAM-функции требуют источника, назначения и количества слов.
|
|
text = HELPER.generate('28335', 'flash').replace('LOAD_SIZE(_RamfuncsLoadSize),', '').replace('LOAD_END(_RamfuncsLoadEnd),', '') # Удаляет только размер копирования, сохраняя остальные корректные назначения.
|
|
self.assertIn('RAMFUNCS_COPY', [i['code'] for i in checked(text)[2]]) # Отсутствие одного обязательного символа действительно обнаруживается.
|
|
def test_copy_end_alternative(self): # Исторический memcpy вправе вычислять размер как LoadEnd-LoadStart.
|
|
text = HELPER.generate('28335', 'flash').replace('LOAD_SIZE(_RamfuncsLoadSize),', '') # Оставляет действующие LOAD_END и оба начальных адреса.
|
|
self.assertEqual(checked(text)[2], []) # Не требует лишнего LOAD_SIZE у совместимых старых проектов TI.
|
|
def test_incomplete_preprocessor(self): # include и выражения не должны исчезать из итогового статуса.
|
|
text = '#include "board.cmd"\n' + HELPER.generate('28335', 'ram') # Неизвестный файл мог бы изменить физическую карту.
|
|
self.assertIn('PREPROCESSOR', [i['code'] for i in checked(text, boot='ram')[2]]) # Пользователь получает явно неполную проверку.
|
|
def test_unknown_syntax(self): # GROUP/UNION и split не интерпретируются ограниченным parser как обычный регион.
|
|
text = HELPER.generate('28335', 'ram').replace('.text : > RAM_CODE', '.text : >> RAM_CODE') # Требует поддерживаемого только TI разбиения секции.
|
|
self.assertIn('ADVANCED_SYNTAX', [i['code'] for i in checked(text, boot='ram')[2]]) # Поддерживаемая часть карты не даёт ложного полного успеха.
|
|
def test_unparsed_extra_blocks(self): # Грамматика не может молча проверить только начало сложного CMD.
|
|
text = HELPER.generate('28335', 'ram') # Использует заведомо корректную исходную карту.
|
|
extra = text + 'MEMORY { PAGE 1: OTHER : origin = 0x8000, length = 0x100 }' # Второй блок мог бы занять уже используемые физические слова.
|
|
self.assertIn('MULTIPLE_BLOCKS', [i['code'] for i in checked(extra, boot='ram')[2]]) # Даже численно понятный первый блок не даёт полного успеха.
|
|
prefix = text.replace('{ /* Начало выходных секций. */', '{ BROKEN_DIRECTIVE /* Начало выходных секций. */') # Вставляет неизвестный префикс, который не является телом предыдущей секции.
|
|
self.assertIn('SECTION_PREFIX', [i['code'] for i in checked(prefix, boot='ram')[2]]) # Произвольное начальное содержимое явно делает проверку неполной.
|
|
def test_no_overwrite(self): # Файл пользователя сохраняется без явно указанного --force.
|
|
with tempfile.TemporaryDirectory() as tmp: # Выделяет отдельный безопасный каталог для проверки записи.
|
|
target = Path(tmp) / 'board.cmd' # Существующий linker-файл не связан с рабочим проектом.
|
|
target.write_text('USER FILE', encoding='utf-8') # Контрольное содержимое должно остаться неизменным.
|
|
with contextlib.redirect_stdout(io.StringIO()), contextlib.redirect_stderr(io.StringIO()): # Не засоряет успешный вывод ожидаемой ошибкой CLI.
|
|
result = HELPER.main(['generate', '--device', '2812', '--boot', 'ram', '--output', str(target)]) # Проверяет настоящий режим x, а не предварительный exists.
|
|
self.assertEqual(result, 1) # Отказ перезаписи имеет ненулевой код.
|
|
self.assertEqual(target.read_text(encoding='utf-8'), 'USER FILE') # Пользовательские данные не изменены.
|
|
def test_unknown_map(self): # Произвольный лог не считается успешно проверенным MAP.
|
|
regions, sections, unused = checked(HELPER.generate('28335', 'ram'), boot='ram') # Даёт анализатору корректный исходный CMD.
|
|
issues, allocations = HELPER.inspect_map('not a TI map', regions, sections, '28335', 'ram') # Пробует заведомо чужой текстовый формат.
|
|
self.assertEqual(issues[0]['code'], 'MAP_FORMAT') # Отказ объясняет нужный формат TI.
|
|
|
|
|
|
class TiSmokeTest(unittest.TestCase): # Тестирует реальную грамматику и MAP TI вместо тестовых выдуманных адресов.
|
|
def test_real_ti_four_profiles(self): # Четыре настоящие COFF-сборки выполняются только с указанным установленным CGT.
|
|
if TI_ROOT is None: # Обычные stdlib-тесты остаются переносимыми без проприетарного компилятора.
|
|
self.skipTest('Для четырёх настоящих TI линковок задайте --ti <compiler-root>.') # Пропуск явно виден в отчёте.
|
|
compiler = TI_ROOT / 'bin/cl2000.exe' # На Windows smoke использует согласованный пользователем установленный TI CGT.
|
|
self.assertTrue(compiler.is_file(), str(compiler)) # Не подменяет отсутствие компилятора успешной проверкой Python.
|
|
with tempfile.TemporaryDirectory(prefix='c2000-linker-ti-') as tmp: # Все объектные файлы и MAP находятся только во временном каталоге.
|
|
folder = Path(tmp) # Минимальная сборка не зависит от SDK или пользовательского проекта.
|
|
source = folder / 'fixture.c' # Исходник содержит типичные секции COFF обоих устройств.
|
|
source.write_text('volatile int observed; /* Создаёт изменяемую .ebss для проверки RAM. */\nconst int setting = 17; /* Создаёт .econst с числовой константой без аппаратного адреса. */\n#pragma CODE_SECTION(ram_old, "ramfuncs") /* Проверяет историческое имя секции RAM-кода. */\nint ram_old(void) { return observed + setting; } /* Даёт непустой LOAD/RUN для проверки размера копирования. */\n#pragma CODE_SECTION(ram_new, ".TI.ramfunc") /* Проверяет современную входную секцию в том же копируемом блоке. */\nint ram_new(void) { return observed + 2; } /* Не требует FPU32, аппаратных заголовков или RTS-арифметики. */\nint main(void) { observed = ram_old() + ram_new(); return 0; } /* Только проверяет линковку; этот тест никогда не запускается на MCU. */\n', encoding='utf-8') # Каждая строка fixture имеет осмысленный русский комментарий.
|
|
start = folder / 'start.asm' # Собственный минимальный branch исключает зависимость теста от TI SDK.
|
|
start.write_text(' .sect "codestart" ; Помещает проверочную двухсловную ветвь в аппаратный BEGIN.\n .global _boot_start ; Задаёт явную точку входа COFF для теста линкера.\n .ref _main ; Объявляет цель из минимального C-файла.\n_boot_start: LB _main ; Длинная ветвь занимает два слова C28x без настройки периферии.\n', encoding='utf-8') # Не выдаёт smoke branch за полноценный CodeStartBranch промышленного приложения.
|
|
for device in ('28335', '2812'): # Оба устройства собираются как C28x без FPU32.
|
|
for boot in ('ram', 'flash'): # Проверяет отдельный BEGIN, RAM и Flash LOAD/RUN.
|
|
with self.subTest(device=device, boot=boot): # Ошибка указывает конкретный профиль.
|
|
prefix = folder / (device + '_' + boot) # Разные OUT/MAP исключают использование предыдущей сборки.
|
|
cmd = prefix.with_suffix('.cmd') # Выбирает самостоятельную карту для данной линковки.
|
|
cmd.write_text(HELPER.generate(device, boot), encoding='utf-8') # В тест идёт именно пользовательский генератор.
|
|
subprocess.run([str(compiler), '--abi=coffabi', '-v28', '--float_support=softlib', '--compile_only', str(source), str(start)], cwd=folder, check=True, capture_output=True, text=True) # Настоящий TI компилирует оба fixture без FPU/SDK.
|
|
result = subprocess.run([str(compiler), '-v28', '--abi=coffabi', '--run_linker', '--rom_model', '--entry_point=_boot_start', '--stack_size=0x100', '--heap_size=0x40', '--output_file=' + str(prefix.with_suffix('.out')), '--map_file=' + str(prefix.with_suffix('.map')), str(folder / 'fixture.obj'), str(folder / 'start.obj'), str(cmd)], cwd=folder, capture_output=True, text=True) # TI проверяет секции, длины и синтаксис сгенерированной карты.
|
|
self.assertEqual(result.returncode, 0, result.stdout + result.stderr) # Любая ошибка linker блокирует подтверждение профиля.
|
|
regions, sections, issues = checked(cmd.read_text(encoding='utf-8'), device, boot) # Проверяет ту же карту, что фактически получил TI.
|
|
extra, allocations = HELPER.inspect_map(prefix.with_suffix('.map').read_text(encoding='utf-8'), regions, sections, device, boot) # Разбирает настоящий, а не нарисованный под parser MAP.
|
|
self.assertEqual(issues + extra, [], str(prefix) + ': ' + str(issues + extra) + '; allocations=' + str([r.name for r in allocations]) + '; codestart=' + str(re.findall(r'codestart[^\n]*\n[^\n]*\n[^\n]*', prefix.with_suffix('.map').read_text(encoding='utf-8')))) # Подтверждает совместимость исходного CMD, LOAD/RUN и реальных copy-symbols TI.
|
|
self.assertTrue(allocations) # Пустой MAP-парсер не может дать положительный smoke.
|
|
print('TI PASS TMS320F' + device + ' ' + boot + ': COFF, codestart, data, ramfuncs, MAP/copy symbols') # Сохраняет короткое подтверждение каждого реального профиля.
|
|
|
|
if __name__ == '__main__': # Импорт теста не запускает компилятор автоматически.
|
|
parser = argparse.ArgumentParser(description=__doc__) # Справка описывает явное подключение TI smoke.
|
|
parser.add_argument('--ti', type=Path, help='Каталог установленного TI C2000 CGT; включает 4 реальные линковки.') # Не скачивает компилятор и не требует платы.
|
|
arguments, unittest_args = parser.parse_known_args() # Допускает обычные флаги unittest вроде-v.
|
|
TI_ROOT = arguments.ti.resolve() if arguments.ti else None # Фиксирует согласованный путь перед выполнением тестов.
|
|
unittest.main(argv=[sys.argv[0], *unittest_args]) # Возвращает ненулевой код при любой Python/TI регрессии.
|