Files
2026-09-16 10:01:43 +03:00

248 lines
13 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Регрессии реального SDCC/linker: код, коллизии, банк, stale и ошибки."""
import json
import os
from pathlib import Path
import shutil
import subprocess
import sys
import tempfile
import unittest
ROOT = Path(__file__).resolve().parents[2]
os.environ['SPRINTER_PYTHON'] = sys.executable
sys.path.insert(0, str(ROOT/'toolchain'))
from sdbg.build import normalize
from sdbg.model import DebugMap
class Integration(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.temp = tempfile.TemporaryDirectory(prefix='sdbg-tests-')
cls.work = Path(cls.temp.name)
cls.fixtures = cls.work/'src'
shutil.copytree(ROOT/'tests/sdbg/fixtures', cls.fixtures)
@classmethod
def tearDownClass(cls): cls.temp.cleanup()
def build(self, name, sources, *flags, success=True):
output = self.work/(name+'.exe')
result = subprocess.run([str(ROOT/'bin/sprinter-cc'), '-o', str(output),
*flags, *map(str,sources)], cwd=ROOT,
text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
if success: self.assertEqual(result.returncode,0,result.stdout)
else: self.assertNotEqual(result.returncode,0,result.stdout)
return output, result
def model(self, name): return DebugMap(self.work/('.sprinter-cc-'+name))
def test_01_raw_collision_and_fixed_byte_identity(self):
sources = [self.fixtures/'main.c', self.fixtures/'helper.c']
sdcc = str(ROOT/'third_party/sdcc/bin/sdcc')
objects=[]
for source in sources:
obj=self.work/(source.stem+'.rel')
subprocess.run([sdcc,'-mz80','--debug','-c','-o',str(obj),str(source)],check=True)
objects.append(str(obj))
raw=subprocess.run([sdcc,'-mz80','--debug','--no-std-crt0','-o',str(self.work/'raw.ihx'),*objects],
text=True,stdout=subprocess.PIPE,stderr=subprocess.STDOUT)
self.assertNotEqual(raw.returncode,0)
self.assertIn('Multiple definition of C$',raw.stdout)
plain,_=self.build('plain',sources)
debug,_=self.build('debug',sources,'--src-debug')
self.assertEqual(plain.read_bytes(),debug.read_bytes())
self.assertIn(b'Fmain$text$0_0$0 Lmain.fake', debug.read_bytes())
model=self.model('debug')
matches=[f for f in model.functions if f['name']=='transform']
self.assertEqual(len(matches),2)
self.assertNotEqual(matches[0]['start'],matches[1]['start'])
for f in matches:
self.assertTrue(model.addr2line(f['start'])['sources'])
self.assertEqual(model.addr2line(0xffff)['status'],'unknown')
total=next(v for v in model.variables if v['name']=='total')
counter=next(v for v in model.variables if v['name']=='counter')
self.assertEqual(total['size'],2)
self.assertTrue(counter['signed'])
self.assertEqual(counter['size'],1)
model.verify_executable()
def test_02_banks_and_windows(self):
source=ROOT/'tests/banked'
for mode,base in [('huge',0xc000),('big',0x4000)]:
flags=['--memory',mode,'--bank','1='+str(source/'bank1.c'),
'--bank','2='+str(source/'bank2.c')]
plain,_=self.build(mode+'plain',[source/'banked.c'],*flags)
debug,_=self.build(mode,[source/'banked.c'],'--src-debug',*flags)
self.assertEqual(plain.read_bytes(),debug.read_bytes())
model=self.model(mode)
for bank in (1,2):
function=next(f for f in model.functions if f['name']==f'bank{bank}_func')
self.assertEqual(function['bank'],bank)
self.assertEqual(function['logical_address'],base+12)
# У sprinter_page_w1 в каждом TU существуют начало и epilogue.
locations=model.line_locations('sprinter.h',111)['locations']
self.assertEqual(len(locations),6)
self.assertEqual({l['bank'] for l in locations},{None,1,2})
def test_03_failure_preserves_published_package(self):
source=self.fixtures/'main.c'
sources=[source,self.fixtures/'helper.c']
exe,_=self.build('transaction',sources,'--src-debug')
before=exe.read_bytes()
manifest=(self.work/'.sprinter-cc-transaction/manifest.json').read_bytes()
broken=self.work/'broken.c'
broken.write_text('extern void missing(void); int main(void) { missing(); return 0; }')
_,result=self.build('transaction',[broken],'--src-debug',success=False)
self.assertIn('missing',result.stdout)
self.assertEqual(exe.read_bytes(),before)
self.assertEqual((self.work/'.sprinter-cc-transaction/manifest.json').read_bytes(),manifest)
self.model('transaction').verify_executable()
def test_04_same_basename_and_bank_data(self):
main=self.work/'names.c'
main.write_text('int left(void); int right(void); int main(void) {return left()+right();}')
self.build('names',[main,self.fixtures/'left/utils.c',self.fixtures/'right/utils.c'],'--src-debug')
model=self.model('names')
self.assertEqual(len([f for f in model.functions if f['name'] in ('left','right')]),2)
self.assertEqual(len([v for v in model.variables if v['name']=='counter']),2)
bank=self.work/'data.c'
bank.write_text('volatile int value; void fun(void) __banked {value=42;}')
main.write_text('void fun(void) __banked; int main(void) {fun();return 0;}')
self.build('data',[main],'--src-debug','--memory','big','--bank','1='+str(bank),'--bank-data')
value=next(v for v in self.model('data').variables if v['name']=='value')
self.assertEqual(value['bank'],1)
self.assertEqual(value['window'],1)
def test_04a_absolute_variable_is_not_confused_with_code(self):
source=self.work/'absolute.c'
source.write_text('''
__at (0x4000) volatile unsigned char scratch;
int main(void) { scratch = 42; return 0; }
''')
self.build('absolute',[source],'--src-debug')
model=self.model('absolute')
scratch=next(v for v in model.variables if v['name']=='scratch')
self.assertEqual(scratch['section'],'_ABS')
self.assertEqual(scratch['logical_address'],0x4000)
self.assertEqual(scratch['window'],1)
self.assertIsNone(scratch['bank'])
with self.assertRaisesRegex(ValueError,'секция'):
model.location(0x4000)
def test_05_stale_sources_and_corrupt_artifacts(self):
source=self.work/'stale.c'
source.write_text('int main(void) {return 42;}')
self.build('stale',[source],'--src-debug')
source.write_text('int main(void) {return 0;}')
model=self.model('stale')
self.assertIn(str(source.resolve()),model.stale_sources)
self.assertIn('42',model.source_text(str(source.resolve()),1))
asm=next((self.work/'.sprinter-cc-stale').glob('*.asm'))
asm.write_text('corrupted')
with self.assertRaisesRegex(ValueError,'артефакт'): self.model('stale')
def test_06_selected_tu_and_argument_errors(self):
sources=[self.fixtures/'main.c',self.fixtures/'helper.c']
self.build('selected',sources,'--src-debug-file',str(sources[0]))
self.assertEqual(len(self.model('selected').units),1)
self.build('invalid',sources,'--src-debug-file','nonexistent.c',success=False)
self.build('invalid',sources,'--src-debug','--src-debug-file',str(sources[0]),success=False)
def test_07_make_configuration_and_header_dependency(self):
directory=self.work/'make-project'
shutil.copytree(self.fixtures,directory)
(directory/'Makefile').write_text(
f'PROJ_ROOT := {ROOT}\nEXAMPLE := main\nEXTRA_SRCS := helper.c\n'
f'include {ROOT}/app.mk\n')
def make(*options):
result=subprocess.run(['make','SRC_DEBUG=1','PYTHON='+sys.executable,*options],
cwd=directory,text=True,stdout=subprocess.PIPE,stderr=subprocess.STDOUT)
self.assertEqual(result.returncode,0,result.stdout)
return result.stdout
self.assertIn('sprinter-cc: wrote',make())
self.assertNotIn('sprinter-cc: wrote',make())
before=json.loads((directory/'.sprinter-cc-main/manifest.json').read_text())['build_id']
header=directory/'common.h'
header.write_text(header.read_text()+'\n/* Правка включённого заголовка. */\n')
self.assertIn('sprinter-cc: wrote',make())
after=json.loads((directory/'.sprinter-cc-main/manifest.json').read_text())['build_id']
self.assertNotEqual(before,after)
self.assertNotIn('sprinter-cc: wrote',make())
self.assertIn('sprinter-cc: wrote',make('SRC_DEBUG=0'))
self.assertNotIn('sprinter-cc: wrote',make('SRC_DEBUG=0'))
def test_09_log_macro_has_linked_anchor_without_exe_growth(self):
source = self.fixtures/'logmacro.c'
plain, _ = self.build('logmacro_plain', [source])
debug, _ = self.build('logmacro_debug', [source], '--src-debug')
self.assertEqual(plain.read_bytes(), debug.read_bytes())
self.assertNotIn(b'total=', debug.read_bytes())
model = self.model('logmacro_debug')
self.assertEqual([item['tag'] for item in model.logpoints], ['after_one'])
anchor = model.logpoints[0]
self.assertTrue(anchor['verified'], anchor)
self.assertEqual(anchor['message'], 'total={total}')
self.assertEqual(anchor['line'], 10)
self.assertIn(anchor['link_address'], model.instructions)
self.assertEqual(model.symbols[anchor['symbol']], anchor['link_address'])
def test_10_duplicate_macro_tag_preserves_published_package(self):
source = self.fixtures/'logmacro.c'
exe, _ = self.build('logmacro_transaction', [source], '--src-debug')
before = exe.read_bytes()
manifest = (self.work/'.sprinter-cc-logmacro_transaction/manifest.json').read_bytes()
duplicate = self.work/'duplicate_log.c'
duplicate.write_text('#include <sdbg.h>\nint main(void) {\n'
'SDBG_LOG(same, "one");\nSDBG_LOG(same, "two");\nreturn 0;\n}\n')
_, result = self.build('logmacro_transaction', [duplicate], '--src-debug', success=False)
self.assertIn('tag должен быть уникальным', result.stdout)
self.assertEqual(exe.read_bytes(), before)
self.assertEqual((self.work/'.sprinter-cc-logmacro_transaction/manifest.json').read_bytes(), manifest)
invalid_format = self.work/'invalid_log_format.c'
invalid_format.write_text('#include <sdbg.h>\nint value; int main(void) {'
'SDBG_LOG(value_log,"value={value:04X}");return 0;}')
_, result = self.build('logmacro_transaction', [invalid_format], '--src-debug', success=False)
self.assertIn('только подстановки', result.stdout)
self.assertEqual(exe.read_bytes(), before)
self.assertEqual((self.work/'.sprinter-cc-logmacro_transaction/manifest.json').read_bytes(), manifest)
def test_11_banked_macro_uses_typed_location_without_exe_growth(self):
main = self.work/'bank_log_main.c'
bank = self.work/'bank_log_worker.c'
main.write_text('void worker(void) __banked; int main(void) {worker();return 0;}')
bank.write_text('#include <sdbg.h>\nvolatile int bank_value;\n'
'void worker(void) __banked {bank_value=1;'
'SDBG_LOG(bank_hit,"bank_value={bank_value}");bank_value=2;}')
flags = ('--memory', 'big', '--bank', '1='+str(bank))
plain, _ = self.build('bank_log_plain', [main], *flags)
debug, _ = self.build('bank_log_debug', [main], '--src-debug', *flags)
self.assertEqual(plain.read_bytes(), debug.read_bytes())
anchor = self.model('bank_log_debug').logpoints[0]
self.assertTrue(anchor['verified'], anchor)
self.assertEqual((anchor['bank'], anchor['window']), (1, 1))
self.assertEqual(anchor['logical_address'], anchor['link_address'] & 0xffff)
def test_08_archive_debug_descriptions_are_not_imported(self):
directory=self.work/'archive'
directory.mkdir()
sdcc=ROOT/'third_party/sdcc/bin/sdcc'
from sdbg.build import compile_unit
compile_unit(sdcc,ROOT/'third_party/sdcc/bin/sdasz80',ROOT/'libc/string/strlwr.c',
directory/'strlwr.rel',['-mz80','--std-c99','-I',str(ROOT/'libc/include')])
subprocess.run([str(ROOT/'third_party/sdcc/bin/sdar'),'rcs',str(directory/'probe.lib'),
str(directory/'strlwr.rel')],check=True)
source=directory/'app.c'
source.write_text('extern char *strlwr(char *); int main(void) {return *strlwr((char*)0x9000);}')
subprocess.run([str(sdcc),'-mz80','--debug','-c','-o',str(directory/'app.rel'),str(source)],check=True)
subprocess.run([str(sdcc),'-mz80','--debug','--no-std-crt0','-o',str(directory/'app.ihx'),
str(directory/'app.rel'),'-L'+str(directory),'-lprobe'],check=True)
records=(directory/'app.cdb').read_text()
self.assertIn('L:G$strlwr$',records)
self.assertNotIn('F:G$strlwr$',records)
self.assertIn('F:G$strlwr$',(directory/'strlwr.adb').read_text())
if __name__ == '__main__': unittest.main()