terrain.py 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212
  1. #
  2. # This is the 'terrain' in which the lettuce lives. By convention, this is
  3. # where global setup and teardown is defined.
  4. #
  5. # We declare some attributes of the global 'world' variables here, so the
  6. # tests can safely assume they are present.
  7. #
  8. # We also use it to provide scenario invariants, such as resetting data.
  9. #
  10. from lettuce import *
  11. import subprocess
  12. import os.path
  13. import shutil
  14. import re
  15. import time
  16. # In order to make sure we start all tests with a 'clean' environment,
  17. # We perform a number of initialization steps, like restoring configuration
  18. # files, and removing generated data files.
  19. # This approach may not scale; if so we should probably provide specific
  20. # initialization steps for scenarios. But until that is shown to be a problem,
  21. # It will keep the scenarios cleaner.
  22. # This is a list of files that are freshly copied before each scenario
  23. # The first element is the original, the second is the target that will be
  24. # used by the tests that need them
  25. copylist = [
  26. ["configurations/example.org.config.orig", "configurations/example.org.config"]
  27. ]
  28. # This is a list of files that, if present, will be removed before a scenario
  29. removelist = [
  30. "data/test_nonexistent_db.sqlite3"
  31. ]
  32. # When waiting for output data of a running process, use OUTPUT_WAIT_INTERVAL
  33. # as the interval in which to check again if it has not been found yet.
  34. # If we have waited OUTPUT_WAIT_MAX_INTERVALS times, we will abort with an
  35. # error (so as not to hang indefinitely)
  36. OUTPUT_WAIT_INTERVAL = 0.5
  37. OUTPUT_WAIT_MAX_INTERVALS = 20
  38. # class that keeps track of one running process and the files
  39. # we created for it.
  40. class RunningProcess:
  41. def __init__(self, step, process_name, args):
  42. # set it to none first so destructor won't error if initializer did
  43. self.process = None
  44. self.step = step
  45. self.process_name = process_name
  46. self.remove_files_on_exit = True
  47. self._check_output_dir()
  48. self._create_filenames()
  49. self._start_process(args)
  50. def _start_process(self, args):
  51. stderr_write = open(self.stderr_filename, "w")
  52. stdout_write = open(self.stdout_filename, "w")
  53. self.process = subprocess.Popen(args, 1, None, subprocess.PIPE,
  54. stdout_write, stderr_write)
  55. # open them again, this time for reading
  56. self.stderr = open(self.stderr_filename, "r")
  57. self.stdout = open(self.stdout_filename, "r")
  58. def mangle_filename(self, filebase, extension):
  59. filebase = re.sub("\s+", "_", filebase)
  60. filebase = re.sub("[^a-zA-Z0-9.\-_]", "", filebase)
  61. return filebase + "." + extension
  62. def _check_output_dir(self):
  63. # We may want to make this overridable by the user, perhaps
  64. # through an environment variable. Since we currently expect
  65. # lettuce to be run from our lettuce dir, we shall just use
  66. # the relative path 'output/'
  67. self._output_dir = os.getcwd() + os.sep + "output"
  68. if not os.path.exists(self._output_dir):
  69. os.mkdir(self._output_dir)
  70. assert os.path.isdir(self._output_dir),\
  71. self._output_dir + " is not a directory."
  72. def _create_filenames(self):
  73. filebase = self.step.scenario.feature.name + "-" +\
  74. self.step.scenario.name + "-" + self.process_name
  75. self.stderr_filename = self._output_dir + os.sep +\
  76. self.mangle_filename(filebase, "stderr")
  77. self.stdout_filename = self._output_dir + os.sep +\
  78. self.mangle_filename(filebase, "stdout")
  79. def stop_process(self):
  80. if self.process is not None:
  81. self.process.terminate()
  82. self.process.wait()
  83. self.process = None
  84. if self.remove_files_on_exit:
  85. self._remove_files()
  86. def _remove_files(self):
  87. os.remove(self.stderr_filename)
  88. os.remove(self.stdout_filename)
  89. def _wait_for_output_str(self, filename, running_file, strings, only_new):
  90. if not only_new:
  91. full_file = open(filename, "r")
  92. for line in full_file:
  93. for string in strings:
  94. if line.find(string) != -1:
  95. full_file.close()
  96. return string
  97. wait_count = 0
  98. while wait_count < OUTPUT_WAIT_MAX_INTERVALS:
  99. where = running_file.tell()
  100. line = running_file.readline()
  101. if line:
  102. for string in strings:
  103. if line.find(string) != -1:
  104. return string
  105. else:
  106. wait_count += 1
  107. time.sleep(OUTPUT_WAIT_INTERVAL)
  108. running_file.seek(where)
  109. assert False, "Timeout waiting for process output: " + str(strings)
  110. def wait_for_stderr_str(self, strings, only_new = True):
  111. return self._wait_for_output_str(self.stderr_filename, self.stderr,
  112. strings, only_new)
  113. def wait_for_stdout_str(self, strings, only_new = True):
  114. return self._wait_for_output_str(self.stdout_filename, self.stdout,
  115. strings, only_new)
  116. # Container class for a number of running processes
  117. # i.e. servers like bind10, etc
  118. # one-shot programs like dig or bindctl are started and closed separately
  119. class RunningProcesses:
  120. def __init__(self):
  121. self.processes = {}
  122. def add_process(self, step, process_name, args):
  123. assert process_name not in self.processes,\
  124. "Process " + name + " already running"
  125. self.processes[process_name] = RunningProcess(step, process_name, args)
  126. def get_process(self, process_name):
  127. assert process_name in self.processes,\
  128. "Process " + name + " unknown"
  129. return self.processes[process_name]
  130. def stop_process(self, process_name):
  131. assert process_name in self.processes,\
  132. "Process " + name + " unknown"
  133. self.processes[process_name].stop_process()
  134. del self.processes[process_name]
  135. def stop_all_processes(self):
  136. for process in self.processes.values():
  137. process.stop_process()
  138. def keep_files(self):
  139. for process in self.processes.values():
  140. process.remove_files_on_exit = False
  141. def wait_for_stderr_str(self, process_name, strings, only_new = True):
  142. """Wait for any of the given strings in the given processes stderr
  143. output. If only_new is True, it will only look at the lines that are
  144. printed to stderr since the last time this method was called. If
  145. False, it will also look at the previously printed lines. This will
  146. block until one of the strings is found. TODO: we may want to put in
  147. a timeout for this... Returns the string that is found"""
  148. assert process_name in self.processes,\
  149. "Process " + process_name + " unknown"
  150. return self.processes[process_name].wait_for_stderr_str(strings,
  151. only_new)
  152. def wait_for_stdout_str(self, process_name, strings, only_new = True):
  153. """Wait for any of the given strings in the given processes stderr
  154. output. If only_new is True, it will only look at the lines that are
  155. printed to stderr since the last time this method was called. If
  156. False, it will also look at the previously printed lines. This will
  157. block until one of the strings is found. TODO: we may want to put in
  158. a timeout for this... Returns the string that is found"""
  159. assert process_name in self.processes,\
  160. "Process " + process_name + " unknown"
  161. return self.processes[process_name].wait_for_stdout_str(strings,
  162. only_new)
  163. @before.each_scenario
  164. def initialize(scenario):
  165. # Keep track of running processes
  166. world.processes = RunningProcesses()
  167. # Convenience variable to access the last query result from querying.py
  168. world.last_query_result = None
  169. # Some tests can modify the settings. If the tests fail half-way, or
  170. # don't clean up, this can leave configurations or data in a bad state,
  171. # so we copy them from originals before each scenario
  172. for item in copylist:
  173. shutil.copy(item[0], item[1])
  174. for item in removelist:
  175. if os.path.exists(item):
  176. os.remove(item)
  177. @after.each_scenario
  178. def cleanup(scenario):
  179. # Keep output files if the scenario failed
  180. if not scenario.passed:
  181. world.processes.keep_files()
  182. # Stop any running processes we may have had around
  183. world.processes.stop_all_processes()