"""Регрессии карт памяти и необязательная реальная 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 = 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 регрессии.