Добавить CAN heartbeat и передачу по изменению с повтором через 1,3 с, компактный логгер v4, паспорт прошивки и блочное чтение EEPROM. Подключить автоматическую версию и проверяемую сборку BIN. Сохранить локальные исправления стробов и защиты Timer1. Обновить тесты, документацию, UTF-8 и исключения временных файлов. Проверки: CAN, R3, EEPROM, паспорт, TESTLAMP, логгер, START/STOP; 9 сценариев сборки, включая реальную конверсию TI. Все 17 внутренних модулей скомпилированы TI C2000 без ошибок.
179 lines
31 KiB
Python
179 lines
31 KiB
Python
"""Проверяет перенос трёх файлов сборки, итератор JSON и запрет выпуска неверного BIN.""" # Не меняет рабочую версию или файлы прошивки пользователя.
|
|
import hashlib # Создаёт независимый эталон SHA-256 для проверяемого транспортного файла.
|
|
import importlib.util # Загружает production-сценарий без запуска его CLI.
|
|
import json # Готовит конфигурацию другого изделия и проверяет сохраняемый счётчик.
|
|
import os # Задаёт относительный путь к templates для временного проекта на том же диске.
|
|
from pathlib import Path # Находит файлы проекта и изолирует пути с пробелами.
|
|
import shutil # Копирует только три файла, необходимые для подключения сборки.
|
|
import struct # Формирует независимый минимальный SCI8 с известным паспортом.
|
|
import subprocess # Запускает настоящий BAT из постороннего рабочего каталога.
|
|
import tempfile # Удаляет только временные файлы проверяемого проекта.
|
|
import unittest # Проверяет успешные сборки, переполнение и ошибки выпуска.
|
|
from unittest.mock import patch # Подменяет внешнюю конверсию для воспроизведения неверных результатов.
|
|
import xml.etree.ElementTree as ET # Читает реальные команды Debug и Release из настроек CCS.
|
|
ROOT = Path(__file__).resolve().parents[2] # Использует репозиторий только для поиска переносимых исходников и библиотеки.
|
|
PROJECT = ROOT / "UKSSTMS320F28335" # Здесь лежат три копируемых файла подключения сборки.
|
|
TEMPLATES = ROOT.parent / "newProject/templates" # Настоящая общая библиотека используется тестами prebuild без копирования реализации.
|
|
spec = importlib.util.spec_from_file_location("portable_build", PROJECT / "build_identity.py") # Проверяет тот же код, который запускает BAT проекта.
|
|
builder = importlib.util.module_from_spec(spec) # Создаёт отдельный импорт без изменения версии.
|
|
spec.loader.exec_module(builder) # Загружает реальные функции счётчика и проверки выпуска.
|
|
|
|
|
|
class BuildStepsTest(unittest.TestCase): # Проверяет контракт переноса независимо от названия текущего изделия.
|
|
def setUp(self): # Создаёт другой проект только из трёх переносимых файлов.
|
|
self.temporary = tempfile.TemporaryDirectory(prefix="portable build ", dir=ROOT) # Тот же диск позволяет проверить относительный путь к templates, а пробелы проверяют кавычки BAT.
|
|
self.addCleanup(self.temporary.cleanup) # Удаляет только собственные временные данные даже при отказе проверки.
|
|
self.project = Path(self.temporary.name) / "another controller" # Имя временного проекта отличается от имени production-проекта.
|
|
self.project.mkdir() # Создаёт каталог назначения до копирования переносимых файлов.
|
|
for name in ("firmware_build.bat", "build_identity.py", "firmware_identity.json"): # Никакие вспомогательные заголовки версии или prebuild-скрипты не требуются.
|
|
shutil.copy2(PROJECT / name, self.project / name) # Копирует исходные сценарии без исправления их кода.
|
|
self.config = self.project / "firmware_identity.json" # Единственный изменяемый файл настройки переносимого подключения.
|
|
self.settings = {"product": "Portable-Test-Controller", "target": "TMS320F28335", "version": [1, 0, 0], "increment_version": True, "templates": os.path.relpath(TEMPLATES, self.project), "max_image_bytes": 64, "copy_to": "../images copy"} # Предел 64 байта относится только к минимальному тестовому BIN, а путь копирования проверяет разрешение от JSON.
|
|
self.save_settings() # Записывает другое изделие и размещение зависимости без изменения Python или BAT.
|
|
self.build_dir = self.project / "Debug with spaces" # Все generated и firmware отделены от пользовательских результатов.
|
|
self.metadata = {"product": "Portable-Test-Controller", "target": "TMS320F28335", "version": [1, 0, 0], "build_id": "1234abcd", "built_at_utc": "2026-10-17T09:00:00+00:00"} # Независимый паспорт для сценариев конверсии и отказов.
|
|
|
|
def save_settings(self): # Позволяет каждому сценарию явно задать входную конфигурацию.
|
|
self.config.write_text(json.dumps(self.settings), encoding="utf-8") # Хранит проверочные настройки в требуемой кодировке.
|
|
|
|
def prepare_metadata(self): # Готовит результат prebuild без изменения рабочего проекта.
|
|
generated = self.build_dir / "generated" # Использует стандартный путь, который ожидает production-postbuild.
|
|
generated.mkdir(parents=True) # Создаёт изолированный контекст компиляции.
|
|
(generated / "firmware_identity.json").write_text(json.dumps(self.metadata), encoding="utf-8") # Фиксирует паспорт компиляции независимо от поведения проверяемого кода.
|
|
|
|
def image_bytes(self, patch_number=0): # Создаёт эталон SCI8 без использования production-упаковщика.
|
|
words = [1, 1, 0, patch_number, 2026, 0x0A11, 0x0900, 0, 0x3132, 0x3334, 0x6162, 0x6364] # Двенадцать слов задают контракт, версию, UTC и строку 1234abcd в регистровом порядке.
|
|
return b"\xaa\x08" + bytes(20) + struct.pack("<HHH12HH", 12, 0, 0x8000, *words, 0) # Один блок из 12 слов по адресу 0x8000 и нулевой терминатор занимают 54 байта.
|
|
|
|
def convert(self, command, check): # Имитирует только внешний инструмент, оставляя настоящими все проверки выпуска.
|
|
self.assertTrue(check) # Ошибки общего BAT обязаны останавливать вызывающий postbuild.
|
|
self.assertEqual(Path(command[4]), TEMPLATES / "tools/firmware-build/ccs/postcompile.bat") # Путь к общей зависимости разрешается из настроек нового проекта.
|
|
stage = Path(command[-1]) # Результаты должны оставаться во временном каталоге до проверки паспорта.
|
|
image = stage / (Path(command[8]).name + "_1234abcd.bin") # Имя берётся из переданного artifact, а не из имени исходного изделия.
|
|
data = self.image_bytes() # Возвращает минимальный корректный паспорт текущего тестового проекта.
|
|
image.write_bytes(data) # Создаёт только результат этого вызова конвертора.
|
|
digest = hashlib.sha256(data).hexdigest() # Вычисляет независимую контрольную сумму ожидаемых байтов.
|
|
manifest = dict(self.metadata, file=image.name, size_bytes=len(data), sha256=digest) # Связывает паспорт, имя и размер в стандартном manifest templates.
|
|
image.with_suffix(".bin.manifest.json").write_text(json.dumps(manifest), encoding="utf-8") # Готовит реальный вход проверки контрольных файлов.
|
|
image.with_suffix(".bin.sha256").write_text(f"{digest} {image.name}\n", encoding="utf-8") # Сохраняет стандартную строку SHA-256 с двумя пробелами.
|
|
(stage / (Path(command[8]).name + ".hex")).write_text("HEX", encoding="utf-8") # Имитирует сопутствующий HEX, не проверяемый как самостоятельный транспортный файл.
|
|
(stage / (Path(command[8]).name + ".hex.map")).write_text("MAP", encoding="utf-8") # Проверяет перенос карты под именем произвольного artifact.
|
|
archive = stage / "1234abcd_2026-10-17_12-00-00_UTC+3" # Проверяет сохранение папки архива текущей сборки.
|
|
archive.mkdir() # Архив создаёт внешний общий postcompile до проверки и публикации.
|
|
shutil.copy2(image, archive / image.name) # Даёт независимый наблюдаемый файл для проверки переноса архива.
|
|
|
|
def test_real_bat_portable_prebuild_and_iterator(self): # Проверяет BAT → Python → общий precompile из другого рабочего каталога.
|
|
for expected in ([1, 0, 1], [1, 0, 2]): # Последовательные попытки должны сохранять и увеличивать номер в JSON.
|
|
subprocess.run(["cmd.exe", "/d", "/c", "call", str(self.project / "firmware_build.bat"), "prepare", "--build-dir", str(self.build_dir)], cwd=ROOT, check=True, capture_output=True) # Копии трёх файлов достаточно для фактической генерации с пробелами в путях.
|
|
settings = json.loads(self.config.read_text(encoding="utf-8")) # Читает сохраняемый источник следующего номера.
|
|
metadata = json.loads((self.build_dir / "generated/firmware_identity.json").read_text(encoding="utf-8")) # Читает паспорт, который затем использует postcompile.
|
|
self.assertEqual(settings["version"], expected) # Проверяет сохранение счётчика для следующей сборки и после Clean.
|
|
self.assertEqual(metadata["version"], expected) # Не допускает расхождение номера в настройках и метаданных компиляции.
|
|
self.assertEqual(metadata["product"], "Portable-Test-Controller") # Не допускает зашитое имя исходного изделия в переносимых сценариях.
|
|
self.assertIn(f"#define FIRMWARE_VERSION_PATCH {expected[2]}U", (self.build_dir / "generated/firmware_identity.h").read_text(encoding="utf-8")) # Проверяет номер также в заголовке, включаемом компилятором TI.
|
|
|
|
def test_disabled_iterator_keeps_config_unchanged(self): # Общая схема с ручной версией остаётся доступной одной настройкой.
|
|
self.settings["increment_version"] = False # Отключает итератор без исправления BAT или Python.
|
|
self.save_settings() # Передаёт выбранный режим через тот же JSON.
|
|
previous = self.config.read_bytes() # Сохраняет исходные байты для проверки отсутствия записи счётчика.
|
|
self.assertEqual(builder.prepare(self.config, self.build_dir)["version"], [1, 0, 0]) # Настоящий общий генератор должен принять текущую версию.
|
|
self.assertEqual(self.config.read_bytes(), previous) # Ручной режим не переформатирует и не изменяет настройки.
|
|
|
|
def test_carry_and_exhaustion(self): # Проверяет обе границы переноса и запрет переполнения полного номера.
|
|
for previous, expected in (([1, 9, 999], [1, 10, 0]), ([1, 255, 999], [2, 0, 0])): # Эталонные значения заданы независимо от production-счётчика.
|
|
self.settings["version"] = list(previous) # Передаёт граничный номер общему итератору через штатный JSON проекта.
|
|
self.save_settings() # Сохраняет исходный номер только во временном проекте проверки.
|
|
self.assertEqual(builder.prepare(self.config, self.build_dir)["version"], expected) # Проверяет перенос через реальную цепочку проектный BAT → общий precompile.
|
|
self.settings["version"] = [255, 255, 999] # Задаёт последний допустимый номер контракта.
|
|
self.save_settings() # Устанавливает границу в сохраняемой конфигурации.
|
|
previous = self.config.read_bytes() # Полное переполнение не должно портить исходный JSON.
|
|
with self.assertRaises(subprocess.CalledProcessError): builder.prepare(self.config, self.build_dir) # Общий precompile отклоняет переполнение и передаёт проектной обёртке ненулевой код процесса.
|
|
self.assertEqual(self.config.read_bytes(), previous) # Проверяет отсутствие сброса версии к нулю или частичной записи.
|
|
|
|
def test_failure_consumes_number_and_bat_returns_error(self): # Сохраняет правило занятых номеров при неудачной сборке и передачу ошибки CCS.
|
|
self.build_dir.mkdir() # Готовит каталог только временного проекта для воспроизведения отказа записи заголовка.
|
|
(self.build_dir / "generated").write_text("blocked", encoding="utf-8") # Файл вместо каталога generated вызывает отказ общего генератора после резервирования номера.
|
|
result = subprocess.run(["cmd.exe", "/d", "/c", "call", str(self.project / "firmware_build.bat"), "prepare", "--build-dir", str(self.build_dir)], cwd=ROOT, capture_output=True) # Реальный BAT проверяет передачу ошибки templates без подмены процесса, владеющего счётчиком.
|
|
self.assertNotEqual(result.returncode, 0) # Неудачная генерация заголовка обязана остановить pre-build.
|
|
self.assertEqual(json.loads(self.config.read_text(encoding="utf-8"))["version"], [1, 0, 1]) # Следующая попытка не должна повторять номер неудачной сборки.
|
|
result = subprocess.run(["cmd.exe", "/d", "/c", "call", str(self.project / "firmware_build.bat"), "build"], cwd=ROOT, capture_output=True) # Намеренно пропускает обязательные аргументы CLI через настоящий BAT.
|
|
self.assertNotEqual(result.returncode, 0) # BAT не должен маскировать ошибку Python успешным завершением.
|
|
|
|
def test_portable_postbuild_copies_only_verified_package(self): # Проверяет выпуск другого изделия и другого имени OUT без изменения сценариев.
|
|
self.prepare_metadata() # Готовит независимый паспорт текущей компиляции.
|
|
with patch.object(builder.subprocess, "run", side_effect=self.convert): # Подменяет только конвертор, оставляя проверки и копирование настоящими.
|
|
image = builder.build(self.project / "compiler", self.build_dir, "../external output/OtherFirmware", self.config) # Относительный путь OUT и его имя отличаются от production-проекта.
|
|
self.assertEqual(image.read_bytes(), self.image_bytes()) # Постоянный firmware содержит точные байты текущего проверенного BIN.
|
|
destination = self.project.parent / "images copy" # Каталог копирования должен отсчитываться от JSON, а не от cwd или BuildDirectory.
|
|
self.assertEqual((destination / image.name).read_bytes(), self.image_bytes()) # Проверяет содержимое дополнительной копии нового изделия.
|
|
self.assertTrue((destination / "OtherFirmware.map").is_file()) # Имя карты также следует artifact без зашитого имени прошивки.
|
|
self.assertTrue((image.parent / "1234abcd_2026-10-17_12-00-00_UTC+3" / image.name).is_file()) # Архив текущей сборки переносится вместе с плоскими файлами.
|
|
|
|
def test_stale_or_oversize_image_is_not_published(self): # Сохраняет защиту EEPROM и запрет нового manifest поверх старой прошивки.
|
|
self.prepare_metadata() # Готовит паспорт, с которым обязана совпасть конверсия.
|
|
for stale, limit in ((True, 64), (False, 53)): # Первый вариант подменяет версию, второй превышает проектный предел ровно на байт.
|
|
with self.subTest(stale=stale): # Показывает конкретную нарушенную защиту при ошибке.
|
|
self.settings["max_image_bytes"] = limit # Меняет только ограничение платы в JSON.
|
|
self.save_settings() # Передаёт ограничение реальному postbuild.
|
|
def convert(command, check): # Создаёт ожидаемые результаты внешнего инструмента для выбранного дефекта.
|
|
self.convert(command, check) # Готовит полноценный комплект, который затем намеренно повреждается.
|
|
if stale: (Path(command[-1]) / "OtherFirmware_1234abcd.bin").write_bytes(self.image_bytes(1)) # Встраивает patch 1 вместо patch 0 текущего prebuild.
|
|
with patch.object(builder.subprocess, "run", side_effect=convert): # Реальные проверки должны отклонить результат до публикации.
|
|
with self.assertRaises(ValueError): builder.build(self.project / "compiler", self.build_dir, "OtherFirmware", self.config) # Успешный внешний конвертор не разрешает неверный выпуск.
|
|
self.assertFalse((self.build_dir / "firmware").exists()) # Даже постоянный архив не должен появляться при ошибке.
|
|
self.assertFalse((self.project.parent / "images copy").exists()) # Дополнительная папка также не получает непроверенный BIN.
|
|
|
|
def test_changed_config_and_bad_hash_are_rejected(self): # Проверяет рассогласование настроек и повреждение контрольного комплекта.
|
|
self.prepare_metadata() # Фиксирует паспорт исходной конфигурации.
|
|
self.settings["product"] = "Another-Product" # Изменение изделия после компиляции требует нового prebuild.
|
|
self.save_settings() # Сохраняет намеренно несовпадающую конфигурацию.
|
|
with patch.object(builder.subprocess, "run") as convert: # До проверки настроек внешняя конверсия запускаться не должна.
|
|
with self.assertRaises(ValueError): builder.build(self.project / "compiler", self.build_dir, "OtherFirmware", self.config) # Не принимает generated от другого изделия.
|
|
convert.assert_not_called() # Подтверждает отказ до конверсии или записи результатов.
|
|
self.settings["product"] = self.metadata["product"] # Восстанавливает совпадающий паспорт для независимой проверки SHA-256.
|
|
self.save_settings() # Сохраняет корректные входные настройки.
|
|
def bad_hash(command, check): # Имитирует повреждённый sidecar при корректном паспорте BIN.
|
|
self.convert(command, check) # Создаёт корректный комплект общего конвертора.
|
|
(Path(command[-1]) / "OtherFirmware_1234abcd.bin.sha256").write_text("broken", encoding="utf-8") # Подменяет только контрольную сумму, не трогая BIN.
|
|
with patch.object(builder.subprocess, "run", side_effect=bad_hash): # Проверку контрольного файла выполняет production-код.
|
|
with self.assertRaises(ValueError): builder.build(self.project / "compiler", self.build_dir, "OtherFirmware", self.config) # Выпуск обязан остановиться даже при правильном встроенном паспорте.
|
|
self.assertFalse((self.build_dir / "firmware").exists()) # Повреждённый комплект не должен попадать в постоянный выпуск.
|
|
|
|
@unittest.skipUnless(os.environ.get("C2000_COMPILER"), "Set C2000_COMPILER to run real TI conversion") # Реальная конверсия требует установленный toolchain, остальные проверки доступны без него.
|
|
def test_real_ti_compile_and_bat_postbuild(self): # Проверяет скопированные BAT и Python с настоящим COFF другого изделия.
|
|
compiler = Path(os.environ["C2000_COMPILER"]) / "bin/cl2000.exe" # Использует явно выбранную тестом установку без поиска версии в production-коде.
|
|
self.settings["max_image_bytes"] = 4096 # Четырёх килобайт достаточно для минимального проверочного COFF, не задаёт предел настоящей платы.
|
|
self.save_settings() # Передаёт тестовый предел тому же JSON переносимого проекта.
|
|
metadata = builder.prepare(self.config, self.build_dir) # Настоящий precompile создаёт паспорт для последующей компиляции TI.
|
|
source = self.project / "fixture.c" # Минимальный C-файл не зависит от исходников или библиотек текущей платы.
|
|
source.write_text('#include "firmware_identity.h" /* Подключает константы именно текущего prebuild. */\nconst unsigned int identity[12] = {1U, FIRMWARE_VERSION_MAJOR, FIRMWARE_VERSION_MINOR, FIRMWARE_VERSION_PATCH, FI_YEAR, FI_MONTH * 256U + FI_DAY, FI_HOUR * 256U + FI_MINUTE, FI_SECOND, FI_ID_0 * 256U + FI_ID_1, FI_ID_2 * 256U + FI_ID_3, FI_ID_4 * 256U + FI_ID_5, FI_ID_6 * 256U + FI_ID_7}; /* Формирует двенадцать регистров независимым C-инициализатором. */\nvolatile unsigned int probe; void entry(void) { for (;;) { probe = identity[probe & 7U]; } } /* Удерживает паспорт обращением по изменяемому индексу 0..7 и даёт линкеру точку входа без аппаратных зависимостей. */\n', encoding="utf-8") # Каждая строка тестового C содержит объяснение на русском языке.
|
|
linker = self.project / "fixture.cmd" # Собственная карта памяти отделяет интеграционный тест от F28335.cmd изделия.
|
|
linker.write_text('MEMORY { RAM : origin = 0x8000, length = 0x1000 } /* Отводит 4096 слов RAM под минимальный пример C28x. */\nSECTIONS { .text > RAM .econst > RAM .const > RAM .cinit > RAM .bss > RAM .ebss > RAM .stack > RAM .data > RAM } /* Размещает все секции примера в загрузочном блоке RAM. */\n', encoding="utf-8") # Проверяет транспорт SCI8, не меняя размещение пользовательской прошивки.
|
|
subprocess.run([str(compiler), "-v28", "--abi=coffabi", "--compile_only", "--include_path=" + str(self.build_dir / "generated"), "--obj_directory=" + str(self.build_dir), str(source)], check=True) # Компилирует настоящий паспорт из generated в объект C28x COFF.
|
|
artifact = self.project / "external output/PortableFirmware" # Полный путь OUT с пробелами проверяет стандартный макрос BuildArtifactFileBaseName.
|
|
artifact.parent.mkdir() # Выходной OUT специально находится за пределами BuildDirectory.
|
|
subprocess.run([str(compiler), "-v28", "--abi=coffabi", "--run_linker", "--rom_model", "--entry_point=_entry", "--output_file=" + str(artifact) + ".out", "--map_file=" + str(artifact) + ".map", str(self.build_dir / "fixture.obj"), str(linker)], check=True) # Линкует минимальный пример без библиотек конкретной платы и сохраняет секцию паспорта.
|
|
result = subprocess.run(["cmd.exe", "/d", "/c", "call", str(self.project / "firmware_build.bat"), "build", "--compiler", str(compiler.parents[1]), "--build-dir", str(self.build_dir), "--artifact", str(artifact)], cwd=ROOT, capture_output=True) # Полностью проходит BAT → Python → общий postcompile → hex2000 → проверки → копирование.
|
|
self.assertEqual(result.returncode, 0, repr(result.stdout) + repr(result.stderr)) # При ошибке показывает фактический вывод общей конверсии без потери диагностики.
|
|
image = self.build_dir / "firmware" / (artifact.name + "_" + metadata["build_id"] + ".bin") # Выбирает точный результат фактического prebuild вместо поиска старого BIN.
|
|
builder.verify_embedded(image, metadata, 4096) # Проверяет реальную транспортную таблицу и встроенные слова паспорта.
|
|
builder.verify_package(self.project.parent / "images copy" / image.name, metadata) # Проверяет SHA-256 и метаданные дополнительной копии.
|
|
self.assertEqual(json.loads(self.config.read_text(encoding="utf-8"))["version"], [1, 0, 1]) # Postbuild не увеличивает номер повторно после prebuild.
|
|
|
|
def test_ccs_prebuild_increments_in_both_configurations(self): # Проверяет фактический итератор и при прямом подключении templates, и через проектную обёртку CCS.
|
|
configurations = ET.parse(PROJECT / ".cproject").findall("./storageModule/cconfiguration/storageModule/configuration") # Проверяет реальные настройки проекта.
|
|
self.assertEqual({item.get("name") for item in configurations}, {"Debug", "Release"}) # Требует обнаружения обеих конфигураций.
|
|
self.settings["version"] = [1, 0, 4] # Воспроизводит рабочий номер, на котором прямое подключение templates перестало увеличивать версию.
|
|
self.save_settings() # Не меняет настоящий JSON и не занимает номера пользовательских прошивок.
|
|
for configuration, expected in zip(configurations, ([1, 0, 5], [1, 0, 6])): # Два запуска с одним JSON должны увеличивать номер ровно по одному разу.
|
|
build_dir = self.build_dir / configuration.get("name") # Каждая конфигурация получает собственные generated, как при сборке в IDE.
|
|
command = configuration.get("prebuildStep").rstrip(";").replace("${PROJECT_LOC}", str(self.project)).replace("${BuildDirectory}", str(build_dir)) # Подставляет макросы CCS и удаляет завершающий разделитель списка команд IDE.
|
|
result = subprocess.run(command, cwd=ROOT, capture_output=True) # Выполняет команду из настоящего XML через cmd с путями, содержащими пробелы.
|
|
self.assertEqual(result.returncode, 0, repr(result.stdout) + repr(result.stderr)) # Показывает диагностику фактического BAT, если подключение templates неработоспособно.
|
|
self.assertEqual(json.loads(self.config.read_text(encoding="utf-8"))["version"], expected) # Проверяет сохраняемый счётчик после прямого вызова и обёртки без двойного увеличения.
|
|
self.assertEqual(json.loads((build_dir / "generated/firmware_identity.json").read_text(encoding="utf-8"))["version"], expected) # Паспорт компиляции обязан содержать тот же номер, который зарезервирован в JSON.
|
|
self.assertIn(f"#define FIRMWARE_VERSION_PATCH {expected[2]}U", (build_dir / "generated/firmware_identity.h").read_text(encoding="utf-8")) # Проверяет, что компилятор получит обновлённую версию, а не только новый manifest.
|
|
|
|
|
|
if __name__ == "__main__": unittest.main() # Любое нарушение версии, переносимости или защиты BIN возвращает ненулевой код.
|