Sprinter: добавить отладку C-исходников и интеграцию VS Code

This commit is contained in:
2026-09-15 17:58:41 +03:00
parent 50c6e56b7b
commit e4695b8281
62 changed files with 7147 additions and 27 deletions
+231
View File
@@ -0,0 +1,231 @@
"""Регрессии реального 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_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()