232 lines
13 KiB
Python
232 lines
13 KiB
Python
"""Регрессии реального 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()
|