"""Регрессии реального 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 \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 \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 \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()