import unittest, os, signal, sys, random, time
from commontest import *
from rdiff_backup.log import *
from rdiff_backup import Globals, Main, restore

"""Test consistency by killing rdiff-backup as it is backing up"""

Log.setverbosity(3)

class Local:
	"""Hold some local RPaths"""
	def get_local_rp(ext):
		return RPath(Globals.local_connection, "testfiles/" + ext)

	kt1rp = get_local_rp('killtest1')
	kt2rp = get_local_rp('killtest2')
	kt3rp = get_local_rp('killtest3')
	kt4rp = get_local_rp('killtest4')

	rpout = get_local_rp('output')
	rpout_inc = get_local_rp('output_inc')
	rpout1 = get_local_rp('restoretarget1')
	rpout2 = get_local_rp('restoretarget2')
	rpout3 = get_local_rp('restoretarget3')
	rpout4 = get_local_rp('restoretarget4')
	rpout5 = get_local_rp('restoretarget5')

	back1 = get_local_rp('backup1')
	back2 = get_local_rp('backup2')
	back3 = get_local_rp('backup3')
	back4 = get_local_rp('backup4')
	back5 = get_local_rp('backup5')

class TimingError(Exception):
	"""Indicates timing error - process killed too soon or too late"""
	pass


class ProcessFuncs(unittest.TestCase):
	"""Subclassed by Resume and NoResume"""
	def delete_tmpdirs(self):
		"""Remove any temp directories created by previous tests"""
		assert not os.system(MiscDir + '/myrm testfiles/output* '
							 'testfiles/restoretarget* testfiles/vft_out '
							 'timbar.pyc testfiles/vft2_out')

	def exec_rb(self, time, wait, *args):
		"""Run rdiff-backup return pid.  Wait until done if wait is true"""
		arglist = ['python', '../rdiff-backup', '-v3']
		if time:
			arglist.append("--current-time")
			arglist.append(str(time))
		arglist.extend(args)

		print "Running ", arglist
		if wait: return os.spawnvp(os.P_WAIT, 'python', arglist)
		else: return os.spawnvp(os.P_NOWAIT, 'python', arglist)

	def exec_and_kill(self, min_max_pair, backup_time, arg1, arg2):
		"""Run rdiff-backup, then kill and run again

		Kill after a time between mintime and maxtime.  First process
		should not terminate before maxtime.

		"""
		mintime, maxtime = min_max_pair
		pid = self.exec_rb(backup_time, None, arg1, arg2)
		time.sleep(random.uniform(mintime, maxtime))
		if os.waitpid(pid, os.WNOHANG)[0] != 0:
			# Timing problem, process already terminated (max time too big?)
			return -1
		os.kill(pid, self.killsignal)
		while 1:
			pid, exitstatus = os.waitpid(pid, os.WNOHANG)
			if pid:
				assert exitstatus != 0
				break
			time.sleep(0.2)
		print "---------------------- killed"

	def create_killtest_dirs(self):
		"""Create testfiles/killtest? directories

		They are similar to the testfiles/increment? directories but
		have more files in them so they take a significant time to
		back up.

		"""
		def copy_thrice(input, output):
			"""Copy input directory to output directory three times"""
			assert not os.system("cp -a %s %s" % (input, output))
			assert not os.system("cp -a %s %s/killtesta" % (input, output))
			assert not os.system("cp -a %s %s/killtestb" % (input, output))

		if (Local.kt1rp.lstat() and Local.kt2rp.lstat() and
			Local.kt3rp.lstat() and Local.kt4rp.lstat()): return
		
		assert not os.system("rm -rf testfiles/killtest?")
		for i in [1, 2, 3, 4]:
			copy_thrice("testfiles/increment%d" % i,
						"testfiles/killtest%d" % i)

	def runtest_sequence(self, total_tests,
						 exclude_rbdir, ignore_tmp, compare_links,
						 stop_on_error = None):
		timing_problems, failures = 0, 0
		for i in range(total_tests):
			try:
				result = self.runtest(exclude_rbdir, ignore_tmp, compare_links)
			except TimingError, te:
				print te
				timing_problems += 1
				continue
			if result != 1:
				if stop_on_error: assert 0, "Compare Failure"
				else: failures += 1

		print total_tests, "tests attempted total"
		print "%s setup problems, %s failures, %s successes" % \
			  (timing_problems, failures,
			   total_tests - timing_problems - failures)		

class KillTest(ProcessFuncs):
	"""Test rdiff-backup by killing it, recovering, and then comparing"""
	killsignal = signal.SIGTERM

	# The following are lower and upper bounds on the amount of time
	# rdiff-backup is expected to run.  They are used to determine how
	# long to wait before killing the rdiff-backup process
	time_pairs = [(0.0, 3.7), (0.0, 5.7), (0.0, 3.0), (0.0, 5.0), (0.0, 5.0)]

	def setUp(self):
		"""Create killtest? and backup? directories if necessary"""
		Local.kt1rp.setdata()
		Local.kt2rp.setdata()
		Local.kt3rp.setdata()		
		Local.kt4rp.setdata()
		if (not Local.kt1rp.lstat() or not Local.kt2rp.lstat() or
			not Local.kt3rp.lstat() or not Local.kt4rp.lstat()):
			self.create_killtest_dirs()

	def testTiming(self):
		"""Run each rdiff-backup sequence 10 times, printing average time"""
		time_list = [[], [], [], [], []] # List of time lists
		iterations = 10

		def run_once(current_time, input_rp, index):
			start_time = time.time()
			self.exec_rb(current_time, 1, input_rp.path, Local.rpout.path)
			time_list[index].append(time.time() - start_time)

		for i in range(iterations):
			self.delete_tmpdirs()
			run_once(10000, Local.kt3rp, 0)
			run_once(20000, Local.kt1rp, 1)
			run_once(30000, Local.kt3rp, 2)
			run_once(40000, Local.kt3rp, 3)
			run_once(50000, Local.kt3rp, 4)			

		for i in range(len(time_list)):
			print "%s -> %s" % (i, " ".join(map(str, time_list[i])))

	def mark_incomplete(self, curtime, rp):
		"""Check the date of current mirror

		Return 1 if there are two current_mirror incs and last one has
		time curtime.  Return 0 if only one with time curtime, and
		then add a current_mirror marker.  Return -1 if only one and
		time is not curtime.

		"""
		rbdir = rp.append_path("rdiff-backup-data")
		inclist = restore.get_inclist(rbdir.append("current_mirror"))
		assert 1 <= len(inclist) <= 2, str(map(lambda x: x.path, inclist))

		inc_date_pairs = map(lambda inc: (inc.getinctime(), inc), inclist)
		inc_date_pairs.sort()
		assert inc_date_pairs[-1][0] == curtime, \
			   (inc_date_pairs[-1][0], curtime)
		if len(inclist) == 2: return 1

		if inc_date_pairs[-1][0] == curtime:
			result = 0
			marker_time = curtime - 10000
		else:
			assert inc_date_pairs[-1][0] == curtime - 10000
			marker_time = curtime
			result = -1

		cur_mirror_rp = rbdir.append("current_mirror.%s.data" %
									 (Time.timetostring(marker_time),))
		assert not cur_mirror_rp.lstat()
		cur_mirror_rp.touch()
		return result

	def testTerm(self):
		"""Run rdiff-backup, termining and regressing each time

		Because rdiff-backup must be killed, the timing should be
		updated

		"""
		count, killed_too_soon, killed_too_late = 5, [0]*4, [0]*4
		self.delete_tmpdirs()
		# Back up killtest3 first because it is big and the first case
		# is kind of special (there's no incrementing, so different
		# code)
		self.exec_rb(10000, 1, Local.kt3rp.path, Local.rpout.path)
		assert CompareRecursive(Local.kt3rp, Local.rpout)

		def cycle_once(min_max_time_pair, curtime, input_rp, old_rp):
			"""Backup input_rp, kill, regress, and then compare"""
			time.sleep(1)
			self.exec_and_kill(min_max_time_pair, curtime,
							   input_rp.path, Local.rpout.path)
			result = self.mark_incomplete(curtime, Local.rpout)
			assert not self.exec_rb(None, 1, '--check-destination-dir',
			   						Local.rpout.path)
			assert CompareRecursive(old_rp, Local.rpout, compare_hardlinks = 0)
			return result

		# Keep backing kt1rp, and then regressing to kt3rp.  Then go to kt1rp
		for i in range(count):
			result = cycle_once(self.time_pairs[1], 20000,
								Local.kt1rp, Local.kt3rp)
			if result == 0: killed_too_late[0] += 1
			elif result == -1: killed_too_soon[0] += 1
		self.exec_rb(20000, 1, Local.kt1rp.path, Local.rpout.path)

		# Now keep regressing from kt2rp, only staying there at the end
		for i in range(count):
			result = cycle_once(self.time_pairs[2], 30000,
								Local.kt2rp, Local.kt1rp)
			if result == 0: killed_too_late[1] += 1
			elif result == -1: killed_too_soon[1] += 1
		self.exec_rb(30000, 1, Local.kt2rp.path, Local.rpout.path)

		# Now keep regressing from kt2rp, only staying there at the end
		for i in range(count):
			result = cycle_once(self.time_pairs[3], 40000,
								Local.kt3rp, Local.kt2rp)
			if result == 0: killed_too_late[2] += 1
			elif result == -1: killed_too_soon[2] += 1
		self.exec_rb(40000, 1, Local.kt3rp.path, Local.rpout.path)

		# Now keep regressing from kt2rp, only staying there at the end
		for i in range(count):
			result = cycle_once(self.time_pairs[4], 50000,
								Local.kt4rp, Local.kt3rp)
			if result == 0: killed_too_late[3] += 1
			elif result == -1: killed_too_soon[3] += 1

		print "Killed too soon out of %s: %s" % (count, killed_too_soon)
		print "Killed too late out of %s: %s" % (count, killed_too_late)

if __name__ == "__main__": unittest.main()
