#!/usr/bin/env python3
#
# unit tests for debugger

import os
import re
import sys
import time
import platform
import threading
import traceback
import subprocess

from struct import unpack

import colorama

sys.path.append('..')
import debugger.lldb as lldb
import debugger.dbgeng as dbgeng
import debugger.DebugAdapter as DebugAdapter
import debugger.gdb as gdb
import debugger.utils as utils

# globals
adapter = None
testbin = None

#--------------------------------------------------------------------------
# UTILITIES
#--------------------------------------------------------------------------

def shellout(cmd):
    process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    (stdout, stderr) = process.communicate()
    stdout = stdout.decode("utf-8")
    stderr = stderr.decode("utf-8")
    #print('stdout: -%s-' % stdout)
    #print('stderr: -%s-' % stderr)
    process.wait()
    return (stdout, stderr)

def parse_image(fpath):
	load_addr = None
	entry_offs = None

	print('finding entrypoint for %s' % fpath)
	with open(fpath, 'rb') as fp:
		data = fp.read()

	# little endian macho
	if data[0:4] == b'\xCF\xFA\xED\xFE':
		assert_equality(data[4:8], b'\x07\x00\x00\x01') # CPU_TYPE_X86_X64
		ncmds = unpack('<I', data[16:20])[0]
		#print('ncmds: %d' % ncmds)
		vmaddr = None
		entryoff1 = None # offset given by COMMAND entry_point_command (priority)
		entryoff2 = None # offset of __text section inside __TEXT segment
		offs = 0x20
		for i in range(ncmds):
			cmd = unpack('<I', data[offs:offs+4])[0]
			cmdsize = unpack('<I', data[offs+4:offs+8])[0]
			if cmd == 0x19: # segment_command_64
				if data[offs+8:offs+16] == b'\x5F\x5F\x54\x45\x58\x54\x00\x00': # __TEXT
					vmaddr = unpack('<Q', data[offs+24:offs+32])[0]
					print('vmaddr: %X' % vmaddr)

					nsects = unpack('<I', data[offs+64:offs+68])[0]
					#print('segment __TEXT nsects: %d' % nsects)

					# advance past command to first section
					o_scn = offs + 0x48
					for i in range(nsects):
						name = data[o_scn+0:o_scn+16]
						#print('__TEXT section %d: %s' % (i, name))
						if name == b'__text\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00':
							entryoff2 = unpack('<I', data[o_scn+0x30:o_scn+0x34])[0]
							break;
						o_scn += 0x50

					if entryoff2 == None:
						raise Exception('couldn\'t locate section __text in segment __TEXT in %s' % fpath)
			if cmd == 0x80000028: # entry_point_command
				entryoff = unpack('<I', data[offs+8:offs+12])[0]
				#print('entryoff: %X' % entryoff)
			offs += cmdsize
		if not vmaddr:
			raise Exception('couldn\'t locate segment_command_64 (where __TEXT loads) in %s' % fpath)
		if entryoff1 == None and entryoff2 == None:
			raise Exception('couldn\'t locate entry_point_command in macho (where main is)' % fpath)

		load_addr = vmaddr
		entry_offs = entryoff1 or entryoff2

	# PE
	elif data[0:2] == b'\x4d\x5a':
		e_lfanew = unpack('<I', data[0x3C:0x40])[0]
		if data[e_lfanew:e_lfanew+6] == b'\x50\x45\x00\x00\x64\x86':
			# x86_64
			entryoff = unpack('<I', data[e_lfanew+0x28:e_lfanew+0x2C])[0]
			vmaddr = unpack('<Q', data[e_lfanew+0x30:e_lfanew+0x38])[0]
		elif data[e_lfanew:e_lfanew+6] == b'\x50\x45\x00\x00\x4c\x01':
			# x86
			entryoff = unpack('<I', data[e_lfanew+0x28:e_lfanew+0x2C])[0]
			vmaddr = unpack('<I', data[e_lfanew+0x34:e_lfanew+0x38])[0]

		load_addr = vmaddr
		entry_offs = entryoff

	# ELF
	elif data[0:4] == b'\x7FELF':
		if data[4] == 1: # EI_CLASS 32-bit
			assert_equality(data[5], 1) # EI_DATA little endian
			assert data[0x10:0x12] in [b'\x02\x00', b'\x03\x00'] # e_type ET_EXEC or ET_DYN (pie)
			#assert_equality(data[0x12:0x14], b'\x3E\x00' # e_machine EM_X86_64)
			e_entry = unpack('<I', data[0x18:0x1C])[0]
			e_phoff = unpack('<I', data[0x1C:0x20])[0]
			e_phentsize = unpack('<H', data[0x2A:0x2C])[0]
			e_phnum = unpack('<H', data[0x2C:0x2E])[0]
			print('e_entry:0x%X e_phoff:0x%X e_phentsize:0x%X e_phnum:0x%X' %
				(e_entry, e_phoff, e_phentsize, e_phnum))

			# find first PT_LOAD
			p_vaddr = None
			offs = e_phoff
			for i in range(e_phnum):
				p_type = unpack('<I', data[offs:offs+4])[0]
				#print('at offset 0x%X p_type:0x%X' % (offs, p_type))
				if p_type == 1:
					p_vaddr = unpack('<I', data[offs+8:offs+12])[0]
					break
				offs += e_phentsize
			if p_vaddr == None:
				raise Exception('couldnt locate a single PT_LOAD program header')

			load_addr = p_vaddr
			entry_offs = e_entry - p_vaddr

		elif data[4] == 2: # EI_CLASS 64-bit
			assert_equality(data[5], 1) # EI_DATA little endian

			assert data[0x10:0x12] in [b'\x02\x00', b'\x03\x00'] # e_type ET_EXEC or ET_DYN (pie)
			#assert_equality(data[0x12:0x14], b'\x3E\x00' # e_machine EM_X86_64)
			e_entry = unpack('<Q', data[0x18:0x20])[0]
			e_phoff = unpack('<Q', data[0x20:0x28])[0]
			e_phentsize = unpack('<H', data[0x36:0x38])[0]
			e_phnum = unpack('<H', data[0x38:0x3a])[0]
			print('e_entry:0x%X e_phoff:0x%X e_phentsize:0x%X e_phnum:0x%X' %
				(e_entry, e_phoff, e_phentsize, e_phnum))

			# find first PT_LOAD
			p_vaddr = None
			offs = e_phoff
			for i in range(e_phnum):
				p_type = unpack('<I', data[offs:offs+4])[0]
				#print('at offset 0x%X p_type:0x%X' % (offs, p_type))
				if p_type == 1:
					p_vaddr = unpack('<Q', data[offs+16:offs+24])[0]
					break
				offs += e_phentsize
			if p_vaddr == None:
				raise Exception('couldnt locate a single PT_LOAD program header')

			load_addr = p_vaddr
			entry_offs = e_entry - p_vaddr

		else:
			raise Exception('expected e_ident[EI_CLASS] to be 1 or 2, got: %d' % data[4])
	else:
		raise Exception('unrecognized file type')

	print('(file) load addr: 0x%X' % load_addr)
	print('(file) entry offset: 0x%X' % entry_offs)
	return (load_addr, entry_offs)

# 'helloworld' -> '.\testbins\helloworld.exe' (windows)
# 'helloworld' -> './testbins/helloworld' (linux, android)
def testbin_to_fpath():
	global testbin
	if testbin.endswith('-win') or testbin.endswith('-windows'):
		testbin = testbin + '.exe'
	tmp =  os.path.join('testbins', testbin)
	if '~' in tmp:
		tmp = os.expanduser(tmp)
	tmp = os.path.abspath(tmp)
	return tmp

# 'helloworld_armv7-android' -> '/data/local/tmp/helloworld_armv7-android'
def testbin_to_mpath():
	global testbin
	m = re.match(r'^.*_(.*)-(.*)$', testbin)
	(mach, os_) = m.group(1, 2)
	if os_ == 'android':
		return '/data/local/tmp/' + testbin
	else:
		return testbin_to_fpath()

def break_into(adapter):
	print('sending break')
	adapter.break_into()

def invoke_adb_gdb_listen(testbin_args, port=31337):
	global testbin

	if '_armv7-' in testbin: gdbserver = 'gdbserver_armv7'
	elif '_aarch64-' in testbin: gdbserver = 'gdbserver_aarch64'
	else: raise Exception('cannot determine gdbserver architecture from %s' % testbin)

	cmdline = []
	cmdline.append('adb')
	cmdline.append('shell')
	cmdline.append('/data/local/tmp/%s :%d /data/local/tmp/%s' % (gdbserver, port, testbin))
	cmdline.extend(testbin_args)

	print('invoke_adb() executing: %s' % ' '.join(cmdline))
	shellout(cmdline)
	print('invoke_adb() done')

def is_wow64():
	global testbin
	if not 'x86' in testbin: return False
	(a,b) = platform.architecture()
	return a=='64bit' and b.startswith('Windows')

def go_initial(adapter):
	global testbin
	if is_wow64():
		(reason, info) = adapter.go()
		assert_equality((reason, info), (DebugAdapter.STOP_REASON.UNKNOWN, 0x4000001f))
	return adapter.go()

def assert_equality(a, b):
	if a == b: return
	utils.red('EXPECTED EQUALITY!')
	utils.red('  actual: %s' % a)
	utils.red('expected: %s' % b)
	traceback.print_stack()
	sys.exit(-1)

# let there be a single check for single-step
# (abstract away OS-exceptional cases)
def expect_single_step(reason):
	global testbin

	if 'macos' in testbin:
		expected = DebugAdapter.STOP_REASON.BREAKPOINT
	else:
		expected = DebugAdapter.STOP_REASON.SINGLE_STEP

	assert_equality(reason, expected)

def expect_bad_instruction(reason):
	global testbin

	# :/ I cannot induce a bad instruction exception on these OS's!
	if 'macos' in testbin or 'windows' in testbin or 'android' in testbin:
		expected = DebugAdapter.STOP_REASON.ACCESS_VIOLATION
	else:
		expected = DebugAdapter.STOP_REASON.ILLEGAL_INSTRUCTION

	assert_equality(reason, expected)

def assert_general_error(func):
	raised = False
	try:
		func()
	except DebugAdapter.GeneralError:
		raised = True
	assert raised

# determines the entrypoint from the
def confirm_initial_module(adapter):
	global testbin

	fpath = testbin_to_fpath()
	mpath = testbin_to_mpath()

	module2addr = adapter.mem_modules()
	#print('module2addr: ', ' '.join(['%s:%X' % (i[0],i[1]) for i in module2addr.items()]))
	#print('      mpath: ', mpath)

	if not mpath in module2addr:
		mpath = os.path.basename(mpath)
		assert mpath in module2addr

	(load_addr, entry_offs) = parse_image(fpath)
	print('  load_addr: 0x%X' % load_addr)
	if '_pie' in testbin:
		# pie: override file's load address with runtime load address
		load_addr = module2addr[mpath]
	else:
		# non-pie: file's load address should match runtime load address
		assert_equality(module2addr[mpath], load_addr)

	return load_addr + entry_offs

def android_test_setup(testbin_args=[]):
	global testbin

	# send file to phone
	fpath = testbin_to_fpath()
	shellout(['adb', 'push', fpath, '/data/local/tmp'])

	# launch adb
	threading.Thread(target=invoke_adb_gdb_listen, args=[testbin_args]).start()

	# connect to adb
	time.sleep(.25)
	adapter = gdb.DebugAdapterGdb()
	adapter.connect('localhost', 31337)
	entry = confirm_initial_module(adapter)

	return (adapter, entry)

#------------------------------------------------------------------------------
# MAIN
#------------------------------------------------------------------------------

if __name__ == '__main__':
	colorama.init()
	arg = sys.argv[1] if sys.argv[1:] else None

	# one-off tests
	if arg == 'oneoff':
		fpath = testbin_to_fpath('helloworld_thread')
		adapter = DebugAdapter.get_adapter_for_current_system()
		adapter.exec(fpath)
		print(adapter.mem_modules())
		print(type(adapter) == dbgeng.DebugAdapterDbgeng)
		sys.exit(0)

	# otherwise test all executables built in the testbins dir
	testbins = []
	for fname in os.listdir('testbins'):
		fpath = os.path.join('testbins', fname)
		if platform.system() == 'Windows':
			if fpath.endswith('.exe'):
				testbins.append(fname)
		elif os.access(fpath, os.X_OK):
			testbins.append(fname)
	print('collected the following tests:\n', testbins)

	#--------------------------------------------------------------------------
	# x86/x64 TESTS
	#--------------------------------------------------------------------------

	# repeat adapter use tests
	for tb in filter(lambda x: x.startswith('helloworld_x64'), testbins):
		testbin = tb
		fpath = testbin_to_fpath()

		def thread_task():
			adapter = DebugAdapter.get_adapter_for_current_system()

			adapter.exec(fpath, ['segfault'])
			# set initial breakpoint
			entry = confirm_initial_module(adapter)
			adapter.breakpoint_set(entry)
			# go to breakpoint
			(reason, extra) = go_initial(adapter)
			assert_equality(reason, DebugAdapter.STOP_REASON.BREAKPOINT)
			# clear, single step a few times
			adapter.breakpoint_clear(entry)
			(reason, extra) = adapter.step_into()
			expect_single_step(reason)
			(reason, extra) = adapter.step_into()
			expect_single_step(reason)
			(reason, extra) = adapter.step_into()
			expect_single_step(reason)
			# go until executing done
			(reason, extra) = adapter.go()
			assert_equality(reason, DebugAdapter.STOP_REASON.PROCESS_EXITED)

			adapter.quit()
			adapter = None

		for i in range(10):
			utils.green('testing %s %d/10' % (fpath, i+1))
			t = threading.Thread(target=thread_task)
			t.start()
			t.join()

	# return code tests
	for tb in [x for x in testbins if x.startswith('exitcode')]:
		testbin = tb
			
		fpath = testbin_to_fpath()

		# some systems return byte, or low byte of 32-bit code and otheres return 32-bit code
		testvals = [('-11',[245,4294967285]), ('-1',[4294967295,255]), ('-3',[4294967293,253]), ('0',[0]), ('3',[3]), ('7',[7]), ('123',[123])]
		for (arg, expected) in testvals:
			adapter = DebugAdapter.get_adapter_for_current_system()

			utils.green('testing %s %s' % (tb, arg))
			adapter.exec(fpath, [arg])
			(reason, extra) = go_initial(adapter)
			assert_equality(reason, DebugAdapter.STOP_REASON.PROCESS_EXITED)
			if not extra in expected:
				raise Exception('expected return code %d to be in %s' % (extra, expected))

	# exception test
	for tb in testbins:
		if not tb.startswith('do_exception'): continue
		if not ('x86' in tb) or ('x64' in tb): continue
		utils.green('testing %s' % tb)
		testbin = tb

		adapter = DebugAdapter.get_adapter_for_current_system()
		fpath = testbin_to_fpath()

		# segfault
		adapter.exec(fpath, ['segfault'])
		(reason, extra) = go_initial(adapter)
		assert_equality(reason, DebugAdapter.STOP_REASON.ACCESS_VIOLATION)
		adapter.quit()

		# illegal instruction
		adapter.exec(fpath, ['illegalinstr'])
		(reason, extra) = go_initial(adapter)
		expect_bad_instruction(reason)
		adapter.quit()

		# breakpoint, single step, exited
		adapter.exec(fpath, ['fakearg'])
		entry = confirm_initial_module(adapter)
		adapter.breakpoint_set(entry)
		(reason, extra) = go_initial(adapter)
		assert_equality(reason, DebugAdapter.STOP_REASON.BREAKPOINT)
		adapter.breakpoint_clear(entry)
		#print('rip: ', adapter.reg_read('rip'))
		(reason, extra) = adapter.step_into()
		#print('rip: ', adapter.reg_read('rip'))
		expect_single_step(reason)

		(reason, extra) = adapter.step_into()
		#print('rip: ', adapter.reg_read('rip'))
		expect_single_step(reason)

		(reason, extra) = adapter.go()
		assert_equality(reason, DebugAdapter.STOP_REASON.PROCESS_EXITED)
		adapter.quit()

		# divzero
		adapter.exec(fpath, ['divzero'])
		(reason, extra) = go_initial(adapter)
		assert_equality(reason, DebugAdapter.STOP_REASON.CALCULATION)
		adapter.quit()

	# assembler x86/x64 tests
	for tb in testbins:
		if not (tb.startswith('asmtest_x64') or tb.startswith('asmtest_x86')): continue
		utils.green('testing %s' % tb)
		testbin = tb

		# parse entrypoint information
		fpath = testbin_to_fpath()
		(load_addr, entry_offs) = parse_image(fpath)
		entry = load_addr + entry_offs

		# tester and testee run on same machine
		adapter = DebugAdapter.get_adapter_for_current_system()
		adapter.exec(fpath, '')

		xip = 'eip' if 'x86' in tb else 'rip'

		loader = adapter.reg_read(xip) != entry
		if loader: print('entrypoint is the program, no library or loader')
		else: print('loader detected, gonna step a few times for fun')

		# a few steps in the loader
		if loader:
			(reason, extra) = adapter.step_into()
			expect_single_step(reason)

		# set bp entry
		print('setting entry breakpoint at 0x%X' % entry)
		adapter.breakpoint_set(entry)

		# few more steps
		if loader:
			(reason, extra) = adapter.step_into()
			expect_single_step(reason)

		# go to entry
		(reason, extra) = go_initial(adapter)
		assert_equality(adapter.reg_read(xip), entry)
		adapter.breakpoint_clear(entry)
		# step into nop
		adapter.step_into()
		assert_equality(adapter.reg_read(xip), entry+1)
		# step into call, return
		adapter.step_into()
		adapter.step_into()
		# back
		assert_equality(adapter.reg_read(xip), entry+6)
		adapter.step_into()
		# step into call, return
		adapter.step_into()
		adapter.step_into()
		# back
		assert_equality(adapter.reg_read(xip), entry+12)

		(reason, extra) = adapter.go()
		assert_equality(reason, DebugAdapter.STOP_REASON.PROCESS_EXITED)

		adapter.quit()
		print('PASS!')

	# helloworld x86/x64, no threads
	for tb in testbins:
		if not tb.startswith('helloworld_'): continue
		if not ('_x64-' in tb or '_x86-' in tb): continue
		if '_thread' in tb: continue
		utils.green('testing %s' % tb)
		testbin = tb

		# tester and testee run on same machine
		adapter = DebugAdapter.get_adapter_for_current_system()
		fpath = testbin_to_fpath()
		adapter.exec(fpath, '')
		entry = confirm_initial_module(adapter)

		if '_x86-' in tb:
			(bits, xip, xax, xbx) = (32, 'eip', 'eax', 'ebx')
			(testval_a, testval_b) = (0xDEADBEEF, 0xCAFEBABE)
		else:
			(bits, xip, xax, xbx) = (64, 'rip', 'rax', 'rbx')
			(testval_a, testval_b) = (0xAAAAAAAADEADBEEF, 0xBBBBBBBBCAFEBABE)

		print('%s: 0x%X' % (xip, adapter.reg_read(xip)))

		# breakpoint set/clear should fail at 0
		print('breakpoint failures')
		try:
			adapter.breakpoint_clear(0)
		except DebugAdapter.BreakpointClearError:
			pass

		try:
			adapter.breakpoint_set(0)
		except DebugAdapter.BreakpointSetError:
			pass

		# breakpoint set/clear should succeed at entrypoint
		print('setting breakpoint at 0x%X' % entry)
		adapter.breakpoint_set(entry)
		print('clearing breakpoint at 0x%X' % entry)
		adapter.breakpoint_clear(entry)
		print('setting breakpoint at 0x%X' % entry)
		adapter.breakpoint_set(entry)

		# proceed to breakpoint
		print('going')
		(reason, extra) = go_initial(adapter)
		assert_equality(reason, DebugAdapter.STOP_REASON.BREAKPOINT)

		assert_equality(adapter.reg_read(xip), entry)
		adapter.breakpoint_clear(entry)

		# single step until it wasn't over a call
		while 1:
			addr = adapter.reg_read(xip)
			data = adapter.mem_read(addr, 15)
			assert_equality(len(data), 15)
			(asmstr, asmlen) = utils.disasm1(data, 0)
			print('%s: 0x%X %s' % (xip, addr, asmstr))

			(reason, info) = adapter.step_into()
			expect_single_step(reason)
			if asmstr.startswith('call'): continue
			if asmstr.startswith('jmp'): continue
			break

		addr2 = adapter.reg_read(xip)
		print('%s: 0x%X' % (xip, addr2))
		assert_equality(addr + asmlen, addr2)

		print('registers')
		for (ridx,rname) in enumerate(adapter.reg_list()):
			width = adapter.reg_bits(rname)
			#print('%d: %s (%d bits)' % (ridx, rname, width))
		assert_equality(adapter.reg_bits(xax), bits)
		assert_equality(adapter.reg_bits(xbx), bits)
		assert_general_error(lambda: adapter.reg_bits('rzx'))

		print('registers read/write')
		rax = adapter.reg_read(xax)
		rbx = adapter.reg_read(xbx)
		assert_general_error(lambda: adapter.reg_read('rzx'))
		adapter.reg_write(xax, testval_a)
		assert_equality(adapter.reg_read(xax), testval_a)
		adapter.reg_write(xbx, testval_b)
		assert_general_error(lambda: adapter.reg_read('rzx'))
		assert_equality(adapter.reg_read(xbx), testval_b)
		adapter.reg_write(xax, rax)
		assert_equality(adapter.reg_read(xax), rax)
		adapter.reg_write(xbx, rbx)
		assert_equality(adapter.reg_read(xbx), rbx)

		print('mem read/write')
		addr = adapter.reg_read(xip)
		data = adapter.mem_read(addr, 256)
		assert_general_error(lambda: adapter.mem_write(0, b'heheHAHAherherHARHAR'))
		data2 = b'\xAA' * 256
		adapter.mem_write(addr, data2)
		assert_general_error(lambda: adapter.mem_read(0, 256))
		assert_equality(adapter.mem_read(addr, 256), data2)
		adapter.mem_write(addr, data)
		assert_equality(adapter.mem_read(addr, 256), data)

		print('quiting')
		adapter.quit()
		adapter = None

	# helloworlds x86/x64 with threads
	for tb in testbins:
		if not tb.startswith('helloworld_thread'): continue
		if not ('_x86-' in tb or '_x64-' in tb): continue
		utils.green('testing %s' % tb)
		testbin = tb

		# for x64 machine, tester and testee run on same machine
		adapter = DebugAdapter.get_adapter_for_current_system()
		fpath = testbin_to_fpath()
		adapter.exec(fpath, '')
		entry = confirm_initial_module(adapter)

		if '_x86-' in tb: xip = 'eip'
		else: xip = 'rip'

		print('scheduling break in 1 second')
		threading.Timer(1, break_into, [adapter]).start()
		print('going')
		(reason, extra) = go_initial(adapter)
		print('back')
		print('switching to bad thread')
		assert_general_error(lambda: adapter.thread_select(999))
		print('asking for threads')
		if platform.system() == 'Windows':
			# main thread at WaitForMultipleObjects() + 4 created threads + debugger thread
			nthreads_expected = 6
		else:
			# main thread at pthread_join() + 4 created threads
			nthreads_expected = 5
		tids = adapter.thread_list()
		if len(tids) != nthreads_expected:
			print('expected %d threads, but len(tids) is %d' % (nthreads_expected, len(tids)))
			assert False
		tid_active = adapter.thread_selected()
		addrs = []
		for tid in tids:
			adapter.thread_select(tid)
			addr = adapter.reg_read(xip)
			addrs.append(addr)
			seltxt = '<--' if tid == tid_active else ''
			print('thread %02d: %s=0x%016X %s' % (tid, xip, addr, seltxt))

		if not is_wow64():
			# on wow64, wow64cpu!TurboDispatchJumpAddressEnd+0x544 becomes common thread jump from point
			assert addrs[0] != addrs[1] # thread at WaitForMultipleObjects()/pthread_join() should be different
		print('switching to bad thread')
		assert_general_error(lambda: adapter.thread_select(999))
		secs = 1
		print('scheduling break in %d second(s)' % secs)
		threading.Timer(secs, break_into, [adapter]).start()
		print('going')
		adapter.go()
		print('back')
		print('checking for %d threads' % nthreads_expected)
		assert_equality(len(adapter.thread_list()), nthreads_expected)
		# ensure the eip/rip are in different locations (that the continue actually continued)
		addrs2 = []
		for tid in tids:
			adapter.thread_select(tid)
			addr2 = adapter.reg_read(xip)
			addrs2.append(addr2)
		if not is_wow64():
			print('checking that at least one thread progressed')
			if list(filter(lambda x: not x, [addrs[i]==addrs2[i] for i in range(len(addrs))])) == []:
				print('did any threads progress?')
				print('addrs: ', map(hex,addrs))
				print('addrs2:  ', map(hex,addrs2))
				assert False
		print('done')
		adapter.quit()

	#--------------------------------------------------------------------------
	# {ARMV7,AARCH64}-ANDROID TESTS
	#--------------------------------------------------------------------------

	# helloworld armv7, no threads
	for tb in testbins:
		if not tb.startswith('helloworld_'): continue
		if not '_armv7-' in tb: continue
		if '_thread' in tb: continue
		utils.green('testing %s' % tb)
		testbin = tb

		(adapter, entry) = android_test_setup()

		print('pc: 0x%X' % adapter.reg_read('pc'))

		# breakpoint set/clear should fail at 0
		print('breakpoint failures')
		try:
			adapter.breakpoint_clear(0)
		except DebugAdapter.BreakpointClearError:
			pass

		try:
			adapter.breakpoint_set(0)
		except DebugAdapter.BreakpointSetError:
			pass

		# breakpoint set/clear should succeed at entrypoint
		print('setting breakpoint at 0x%X' % entry)
		adapter.breakpoint_set(entry)
		print('clearing breakpoint at 0x%X' % entry)
		adapter.breakpoint_clear(entry)
		print('setting breakpoint at 0x%X' % entry)
		adapter.breakpoint_set(entry)

		# proceed to breakpoint
		print('going')
		(reason, info) = adapter.go()
		assert_equality(reason, DebugAdapter.STOP_REASON.BREAKPOINT)
		pc = adapter.reg_read('pc')
		print('pc: 0x%X' % pc)
		assert_equality(pc, entry)

		# single step
		data = adapter.mem_read(pc, 15)
		assert_equality(len(data), 15)
		(asmstr, asmlen) = utils.disasm1(data, 0, 'armv7')
		adapter.breakpoint_clear(entry)
		(reason, info) = adapter.step_into()
		assert_equality(reason, DebugAdapter.STOP_REASON.SINGLE_STEP)
		pc2 = adapter.reg_read('pc')
		print('pc2: 0x%X' % pc2)
		assert_equality(pc + asmlen, pc2)

		print('registers')
		for (ridx,rname) in enumerate(adapter.reg_list()):
			width = adapter.reg_bits(rname)
			#print('%d: %s (%d bits)' % (ridx, rname, width))
		assert_equality(adapter.reg_bits('r0'), 32)
		assert_equality(adapter.reg_bits('r4'), 32)
		assert_general_error(lambda: adapter.reg_bits('rzx'))

		print('registers read/write')
		r0 = adapter.reg_read('r0')
		r4 = adapter.reg_read('r4')
		assert_general_error(lambda: adapter.reg_read('rzx'))
		adapter.reg_write('r0', 0xDEADBEEF)
		assert_equality(adapter.reg_read('r0'), 0xDEADBEEF)
		adapter.reg_write('r4', 0xCAFEBABE)
		assert_general_error(lambda: adapter.reg_read('rzx'))
		assert_equality(adapter.reg_read('r4'), 0xCAFEBABE)
		adapter.reg_write('r0', r0)
		assert_equality(adapter.reg_read('r0'), r0)
		adapter.reg_write('r4', r4)
		assert_equality(adapter.reg_read('r4'), r4)

		print('mem read/write')
		addr = adapter.reg_read('pc')
		data = adapter.mem_read(addr, 256)
		assert_general_error(lambda: adapter.mem_write(0, b'heheHAHAherherHARHAR'))
		data2 = b'\xAA' * 256
		adapter.mem_write(addr, data2)
		assert_general_error(lambda: adapter.mem_read(0, 256))
		assert_equality(adapter.mem_read(addr, 256), data2)
		adapter.mem_write(addr, data)
		assert_equality(adapter.mem_read(addr, 256), data)

		print('quiting')
		adapter.quit()
		adapter = None

	# helloworld with threads
	# architectures: armv7, aarch64
	for tb in testbins:
		if not tb.startswith('helloworld_thread_'): continue
		if not (('_armv7-' in tb) or ('_aarch64-' in tb)): continue
		utils.green('testing %s' % tb)
		testbin = tb

		(adapter, entry) = android_test_setup()

		print('pc: 0x%X' % adapter.reg_read('pc'))
		print('scheduling break in 1 seconds')
		threading.Timer(.3, break_into, [adapter]).start()
		print('going')
		adapter.go()
		print('back')
		print('switching to bad thread')
		assert_general_error(lambda: adapter.thread_select(999))
		print('asking for threads')
		tids = adapter.thread_list()
		assert_equality(len(tids), 5)
		tid_active = adapter.thread_selected()
		pcs = []
		for tid in tids:
			adapter.thread_select(tid)
			pc = adapter.reg_read('pc')
			pcs.append(pc)
			seltxt = '<--' if tid == tid_active else ''
			print('thread %02d: pc=0x%016X %s' % (tid, pc, seltxt))
		assert pcs[0] != pcs[1] # thread at WaitForMultipleObjects()/pthread_join() should be different
		print('switching to bad thread')
		assert_general_error(lambda: adapter.thread_select(999))
		secs = 1
		print('scheduling break in %d second(s)' % secs)
		threading.Timer(secs, break_into, [adapter]).start()
		print('going')
		adapter.go()
		print('back')
		print('checking for %d threads' % 5)
		assert_equality(len(adapter.thread_list()), 5)
		# ensure the pc's are in different locations (that the continue actually continued)
		pcs2 = []
		for tid in tids:
			adapter.thread_select(tid)
			pcs2.append(adapter.reg_read('pc'))
		print('checking that at least one thread progressed')
		#print(' pcs: ', pcs)
		#print('pcs2: ', pcs2)
		if list(filter(lambda x: not x, [pcs[i]==pcs2[i] for i in range(len(pcs))])) == []:
			print('did any threads progress?')
			print(' pcs:  ', pcs)
			print('pcs2:  ', pcs2)
			assert False
		print('done')
		adapter.quit()

	# exception test
	for tb in testbins:
		if not tb.startswith('do_exception'): continue
		if not '-android' in tb: continue
		utils.green('testing %s' % tb)
		testbin = tb


		# segfault
		(adapter, entry) = android_test_setup(['segfault'])
		(reason, extra) = go_initial(adapter)
		assert_equality(reason, DebugAdapter.STOP_REASON.ACCESS_VIOLATION)
		adapter.quit()

		# illegal instruction
		(adapter, entry) = android_test_setup(['illegalinstr'])
		(reason, extra) = go_initial(adapter)
		expect_bad_instruction(reason)
		adapter.quit()

		# breakpoint, single step, exited
		(adapter, entry) = android_test_setup(['fakearg'])
		entry = confirm_initial_module(adapter)
		adapter.breakpoint_set(entry)
		(reason, extra) = go_initial(adapter)
		assert_equality(reason, DebugAdapter.STOP_REASON.BREAKPOINT)
		adapter.breakpoint_clear(entry)
		#print('rip: ', adapter.reg_read('rip'))
		(reason, extra) = adapter.step_into()
		#print('rip: ', adapter.reg_read('rip'))
		expect_single_step(reason)

		(reason, extra) = adapter.step_into()
		#print('rip: ', adapter.reg_read('rip'))
		expect_single_step(reason)

		(reason, extra) = adapter.go()
		assert_equality(reason, DebugAdapter.STOP_REASON.PROCESS_EXITED)
		adapter.quit()

		# divzero
		# https://community.arm.com/developer/ip-products/processors/b/processors-ip-blog/posts/divide-and-conquer
		# ARMv7-A - divide by zero always returns a zero result.
		# ARMv7-R - the SCTLR.DZ bit controls whether you get a zero result or a Undefined Instruction exception when you attempt to divide by zero (the default is to return zero).
		# ARMv7-M -  the CCR.DIV_0_TRP bit controls whether an exception is generated. If this occurs, it will cause a UsageFault and the UFSR.DIVBYZERO bit will indicate the reason for the fault.

		#(adapter, entry) = android_test_setup(['divzero'])
		#if 'aarch64' in tb:
		#	# aarch64 compiled binaries divide by 0 just fine, return "inf" *shrug*
		#	assert_equality(reason, DebugAdapter.STOP_REASON.PROCESS_EXITED)
		#else:
		#	assert_equality(reason, DebugAdapter.STOP_REASON.CALCULATION)
		#adapter.quit()

	# assembler test
	# architectures: armv7, aarch64
	for tb in filter(lambda x: x.startswith('asmtest_armv7') or x.startswith('asmtest_aarch64'), testbins):
		utils.green('testing %s' % tb)
		testbin = tb

		(adapter, entry) = android_test_setup()

		loader = adapter.reg_read('pc') != entry
		if loader:
			print('entrypoint is the program, no library or loader')
		else:
			print('loader detected, gonna step a few times for fun')

		# a few steps in the loader
		if loader:
			(reason, extra) = adapter.step_into()
			expect_single_step(reason)

		# set bp entry
		print('setting entry breakpoint at 0x%X' % entry)
		adapter.breakpoint_set(entry)

		# few more steps
		if loader:
			(reason, extra) = adapter.step_into()
			expect_single_step(reason)

		# go to entry
		adapter.go()
		assert_equality(adapter.reg_read('pc'), entry)
		adapter.breakpoint_clear(entry)
		# step into nop
		adapter.step_into()
		assert_equality(adapter.reg_read('pc'), entry+4)
		# step into call, return
		adapter.step_into()
		adapter.step_into()
		# back
		assert_equality(adapter.reg_read('pc'), entry+8)
		adapter.step_into()
		# step into call, return
		adapter.step_into()
		adapter.step_into()
		# back
		assert_equality(adapter.reg_read('pc'), entry+16)

		(reason, extra) = adapter.go()
		assert_equality(reason, DebugAdapter.STOP_REASON.PROCESS_EXITED)

		adapter.quit()

	# helloworld aarch64, no threads
	for tb in testbins:
		if not tb.startswith('helloworld_'): continue
		if not '_aarch64-' in tb: continue
		if '_thread' in tb: continue
		utils.green('testing %s' % tb)
		testbin = tb

		(adapter, entry) = android_test_setup()

		print('pc: 0x%X' % adapter.reg_read('pc'))

		# breakpoint set/clear should fail at 0
		print('breakpoint failures')
		try:
			adapter.breakpoint_clear(0)
		except DebugAdapter.BreakpointClearError:
			pass

		try:
			adapter.breakpoint_set(0)
		except DebugAdapter.BreakpointSetError:
			pass

		# breakpoint set/clear should succeed at entrypoint
		print('setting breakpoint at 0x%X' % entry)
		adapter.breakpoint_set(entry)
		print('clearing breakpoint at 0x%X' % entry)
		adapter.breakpoint_clear(entry)
		print('setting breakpoint at 0x%X' % entry)
		adapter.breakpoint_set(entry)

		# proceed to breakpoint
		print('going')
		(reason, info) = adapter.go()
		assert_equality(reason, DebugAdapter.STOP_REASON.BREAKPOINT)
		pc = adapter.reg_read('pc')
		print('pc: 0x%X' % pc)
		assert_equality(pc, entry)

		# single step
		data = adapter.mem_read(pc, 15)
		assert_equality(len(data), 15)
		(asmstr, asmlen) = utils.disasm1(data, 0, 'armv7')
		adapter.breakpoint_clear(entry)
		(reason, info) = adapter.step_into()
		expect_single_step(reason)
		pc2 = adapter.reg_read('pc')
		print('pc2: 0x%X' % pc2)
		assert_equality(pc + asmlen, pc2)

		print('registers')
		for (ridx,rname) in enumerate(adapter.reg_list()):
			width = adapter.reg_bits(rname)
			#print('%d: %s (%d bits)' % (ridx, rname, width))
		assert_equality(adapter.reg_bits('x0'), 64)
		assert_equality(adapter.reg_bits('x4'), 64)
		assert_general_error(lambda: adapter.reg_bits('rzx'))

		print('registers read/write')
		x0 = adapter.reg_read('x0')
		x4 = adapter.reg_read('x4')
		assert_general_error(lambda: adapter.reg_read('rzx'))
		adapter.reg_write('x0', 0xDEADBEEF)
		assert_equality(adapter.reg_read('x0'), 0xDEADBEEF)
		adapter.reg_write('x4', 0xCAFEBABE)
		assert_general_error(lambda: adapter.reg_read('rzx'))
		assert_equality(adapter.reg_read('x4'), 0xCAFEBABE)
		adapter.reg_write('x0', x0)
		assert_equality(adapter.reg_read('x0'), x0)
		adapter.reg_write('x4', x4)
		assert_equality(adapter.reg_read('x4'), x4)

		print('mem read/write')
		addr = adapter.reg_read('pc')
		data = adapter.mem_read(addr, 256)
		assert_general_error(lambda: adapter.mem_write(0, b'heheHAHAherherHARHAR'))
		data2 = b'\xAA' * 256
		adapter.mem_write(addr, data2)
		assert_general_error(lambda: adapter.mem_read(0, 256))
		assert_equality(adapter.mem_read(addr, 256), data2)
		adapter.mem_write(addr, data)
		assert_equality(adapter.mem_read(addr, 256), data)

		if not '_loop' in tb:
			print('going')
			(reason, extra) = adapter.go()
			assert_equality(reason, DebugAdapter.STOP_REASON.PROCESS_EXITED)

		print('quiting')
		adapter.quit()
		adapter = None

	utils.green('TESTS PASSED!')