terrain.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460
  1. # Copyright (C) 2011-2014 Internet Systems Consortium.
  2. #
  3. # Permission to use, copy, modify, and distribute this software for any
  4. # purpose with or without fee is hereby granted, provided that the above
  5. # copyright notice and this permission notice appear in all copies.
  6. #
  7. # THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SYSTEMS CONSORTIUM
  8. # DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL
  9. # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL
  10. # INTERNET SYSTEMS CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT,
  11. # INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING
  12. # FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
  13. # NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
  14. # WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  15. #
  16. # This is the 'terrain' in which the lettuce lives. By convention, this is
  17. # where global setup and teardown is defined.
  18. #
  19. # We declare some attributes of the global 'world' variables here, so the
  20. # tests can safely assume they are present.
  21. #
  22. # We also use it to provide scenario invariants, such as resetting data.
  23. #
  24. from lettuce import *
  25. import subprocess
  26. import os
  27. import shutil
  28. import re
  29. import sys
  30. import time
  31. # lettuce cannot directly pass commands to the terrain, so we need to
  32. # use environment variables to influence behaviour
  33. KEEP_OUTPUT = 'LETTUCE_KEEP_OUTPUT'
  34. # In order to make sure we start all tests with a 'clean' environment,
  35. # We perform a number of initialization steps, like restoring configuration
  36. # files, and removing generated data files.
  37. # This approach may not scale; if so we should probably provide specific
  38. # initialization steps for scenarios. But until that is shown to be a problem,
  39. # It will keep the scenarios cleaner.
  40. # This is a list of files that are freshly copied before each scenario
  41. # The first element is the original, the second is the target that will be
  42. # used by the tests that need them
  43. copylist = [
  44. ["configurations/bindctl_commands.config.orig",
  45. "configurations/bindctl_commands.config"],
  46. ["configurations/example.org.config.orig",
  47. "configurations/example.org.config"],
  48. ["configurations/generate.config.orig",
  49. "configurations/generate.config"],
  50. ["configurations/bindctl/bindctl.config.orig",
  51. "configurations/bindctl/bindctl.config"],
  52. ["configurations/auth/auth_basic.config.orig",
  53. "configurations/auth/auth_basic.config"],
  54. ["configurations/auth/auth_badzone.config.orig",
  55. "configurations/auth/auth_badzone.config"],
  56. ["configurations/resolver/resolver_basic.config.orig",
  57. "configurations/resolver/resolver_basic.config"],
  58. ["configurations/multi_instance/multi_auth.config.orig",
  59. "configurations/multi_instance/multi_auth.config"],
  60. ["configurations/ddns/ddns.config.orig",
  61. "configurations/ddns/ddns.config"],
  62. ["configurations/ddns/noddns.config.orig",
  63. "configurations/ddns/noddns.config"],
  64. ["configurations/xfrin/retransfer_master.conf.orig",
  65. "configurations/xfrin/retransfer_master.conf"],
  66. ["configurations/xfrin/retransfer_master_v4.conf.orig",
  67. "configurations/xfrin/retransfer_master_v4.conf"],
  68. ["configurations/xfrin/retransfer_master_nons.conf.orig",
  69. "configurations/xfrin/retransfer_master_nons.conf"],
  70. ["configurations/xfrin/retransfer_slave.conf.orig",
  71. "configurations/xfrin/retransfer_slave.conf"],
  72. ["configurations/xfrin/retransfer_slave_notify.conf.orig",
  73. "configurations/xfrin/retransfer_slave_notify.conf"],
  74. ["configurations/root.config.orig",
  75. "configurations/root.config"],
  76. ["configurations/static.config.orig",
  77. "configurations/static.config"],
  78. ["data/inmem-xfrin.sqlite3.orig",
  79. "data/inmem-xfrin.sqlite3"],
  80. ["data/root.sqlite3.orig",
  81. "data/root.sqlite3"],
  82. ["data/xfrin-before-diffs.sqlite3.orig",
  83. "data/xfrin-before-diffs.sqlite3"],
  84. ["data/xfrin-notify.sqlite3.orig",
  85. "data/xfrin-notify.sqlite3"],
  86. ["data/ddns/example.org.sqlite3.orig",
  87. "data/ddns/example.org.sqlite3"],
  88. ["data/empty_db.sqlite3",
  89. "data/xfrout.sqlite3"]
  90. ]
  91. # This is a list of files that, if present, will be removed before a scenario
  92. removelist = [
  93. "data/test_nonexistent_db.sqlite3"
  94. ]
  95. # When waiting for output data of a running process, use OUTPUT_WAIT_INTERVAL
  96. # as the interval in which to check again if it has not been found yet.
  97. # If we have waited OUTPUT_WAIT_MAX_INTERVALS times, we will abort with an
  98. # error (so as not to hang indefinitely)
  99. OUTPUT_WAIT_INTERVAL = 0.5
  100. OUTPUT_WAIT_MAX_INTERVALS = 120
  101. # class that keeps track of one running process and the files
  102. # we created for it.
  103. class RunningProcess:
  104. def __init__(self, step, process_name, args):
  105. # set it to none first so destructor won't error if initializer did
  106. """
  107. Initialize the long-running process structure, and start the process.
  108. Parameters:
  109. step: The scenario step it was called from. This is used for
  110. determining the output files for redirection of stdout
  111. and stderr.
  112. process_name: The name to refer to this running process later.
  113. args: Array of arguments to pass to Popen().
  114. """
  115. self.process = None
  116. self.step = step
  117. self.process_name = process_name
  118. self.remove_files_on_exit = (os.environ.get(KEEP_OUTPUT) != '1')
  119. self._check_output_dir()
  120. self._create_filenames()
  121. self._start_process(args)
  122. # used in _wait_for_output_str, map from (filename, (strings))
  123. # to a file offset.
  124. self.__file_offsets = {}
  125. def _start_process(self, args):
  126. """
  127. Start the process.
  128. Parameters:
  129. args:
  130. Array of arguments to pass to Popen().
  131. """
  132. stderr_write = open(self.stderr_filename, "w")
  133. stdout_write = open(self.stdout_filename, "w")
  134. self.process = subprocess.Popen(args, 0, None, subprocess.PIPE,
  135. stdout_write, stderr_write)
  136. # open them again, this time for reading
  137. self.stderr = open(self.stderr_filename, "r")
  138. self.stdout = open(self.stdout_filename, "r")
  139. def mangle_filename(self, filebase, extension):
  140. """
  141. Remove whitespace and non-default characters from a base string,
  142. and return the substituted value. Whitespace is replaced by an
  143. underscore. Any other character that is not an ASCII letter, a
  144. number, a dot, or a hyphen or underscore is removed.
  145. Parameter:
  146. filebase: The string to perform the substitution and removal on
  147. extension: An extension to append to the result value
  148. Returns the modified filebase with the given extension
  149. """
  150. filebase = re.sub("\s+", "_", filebase)
  151. filebase = re.sub("[^a-zA-Z0-9.\-_]", "", filebase)
  152. return filebase + "." + extension
  153. def _check_output_dir(self):
  154. # We may want to make this overridable by the user, perhaps
  155. # through an environment variable. Since we currently expect
  156. # lettuce to be run from our lettuce dir, we shall just use
  157. # the relative path 'output/'
  158. """
  159. Make sure the output directory for stdout/stderr redirection
  160. exists.
  161. Fails if it exists but is not a directory, or if it does not
  162. and we are unable to create it.
  163. """
  164. self._output_dir = os.getcwd() + os.sep + "output"
  165. if not os.path.exists(self._output_dir):
  166. os.mkdir(self._output_dir)
  167. assert os.path.isdir(self._output_dir),\
  168. self._output_dir + " is not a directory."
  169. def _create_filenames(self):
  170. """
  171. Derive the filenames for stdout/stderr redirection from the
  172. feature, scenario, and process name. The base will be
  173. "<Feature>-<Scenario>-<process name>.[stdout|stderr]"
  174. """
  175. filebase = self.step.scenario.feature.name + "-" +\
  176. self.step.scenario.name + "-" + self.process_name
  177. self.stderr_filename = self._output_dir + os.sep +\
  178. self.mangle_filename(filebase, "stderr")
  179. self.stdout_filename = self._output_dir + os.sep +\
  180. self.mangle_filename(filebase, "stdout")
  181. def stop_process(self):
  182. """
  183. Stop this process by calling terminate(). Blocks until process has
  184. exited. If remove_files_on_exit is True, redirected output files
  185. are removed.
  186. """
  187. if self.process is not None:
  188. self.process.terminate()
  189. self.process.wait()
  190. self.process = None
  191. if self.remove_files_on_exit:
  192. self._remove_files()
  193. def _remove_files(self):
  194. """
  195. Remove the files created for redirection of stdout/stderr output.
  196. """
  197. os.remove(self.stderr_filename)
  198. os.remove(self.stdout_filename)
  199. def _wait_for_output_str(self, filename, running_file, strings, only_new,
  200. matches=1):
  201. """
  202. Wait for a line of output in this process. This will (if
  203. only_new is False) check all output from the process including
  204. that may have been checked before. If only_new is True, it
  205. only checks output that has not been covered in previous calls
  206. to this method for the file (if there was no such previous call to
  207. this method, it works same as the case of only_new=False).
  208. Care should be taken if only_new is to be set to True, as it may cause
  209. counter-intuitive results. For example, assume the file is expected
  210. to contain a line that has XXX and another line has YYY, but the
  211. ordering is not predictable. If this method is called with XXX as
  212. the search string, but the line containing YYY appears before the
  213. target line, this method remembers the point in the file beyond
  214. the line that has XXX. If a next call to this method specifies
  215. YYY as the search string with only_new being True, the search will
  216. fail. If the same string is expected to appear multiple times
  217. and you want to catch the latest one, a more reliable way is to
  218. specify the match number and set only_new to False, if the number
  219. of matches is predictable.
  220. For each line in the output, the given strings array is checked. If
  221. any output lines checked contains one of the strings in the strings
  222. array, that string (not the line!) is returned.
  223. Parameters:
  224. filename: The filename to read previous output from, if applicable.
  225. running_file: The open file to read new output from.
  226. strings: Array of strings to look for.
  227. only_new: See above.
  228. matches: Check for the string this many times.
  229. Returns a tuple containing the matched string, and the complete line
  230. it was found in.
  231. Fails if none of the strings was read after 10 seconds
  232. (OUTPUT_WAIT_INTERVAL * OUTPUT_WAIT_MAX_INTERVALS).
  233. """
  234. # Identify the start offset of search. if only_new=True, start from
  235. # the farthest point we've reached in the file; otherwise start from
  236. # the beginning.
  237. if not filename in self.__file_offsets:
  238. self.__file_offsets[filename] = 0
  239. offset = self.__file_offsets[filename] if only_new else 0
  240. running_file.seek(offset)
  241. match_count = 0
  242. wait_count = 0
  243. while wait_count < OUTPUT_WAIT_MAX_INTERVALS:
  244. line = running_file.readline()
  245. where = running_file.tell()
  246. if line:
  247. for string in strings:
  248. if line.find(string) != -1:
  249. match_count += 1
  250. if match_count >= matches:
  251. # If we've gone further, update the recorded offset
  252. if where > self.__file_offsets[filename]:
  253. self.__file_offsets[filename] = where
  254. return (string, line)
  255. else:
  256. wait_count += 1
  257. time.sleep(OUTPUT_WAIT_INTERVAL)
  258. running_file.seek(where)
  259. assert False, "Timeout waiting for process output: " + str(strings)
  260. def wait_for_stderr_str(self, strings, only_new = True, matches = 1):
  261. """
  262. Wait for one of the given strings in this process's stderr output.
  263. Parameters:
  264. strings: Array of strings to look for.
  265. only_new: See _wait_for_output_str.
  266. matches: Check for the string this many times.
  267. Returns a tuple containing the matched string, and the complete line
  268. it was found in.
  269. Fails if none of the strings was read after 10 seconds
  270. (OUTPUT_WAIT_INTERVAL * OUTPUT_WAIT_MAX_INTERVALS).
  271. """
  272. return self._wait_for_output_str(self.stderr_filename, self.stderr,
  273. strings, only_new, matches)
  274. def wait_for_stdout_str(self, strings, only_new = True, matches = 1):
  275. """
  276. Wait for one of the given strings in this process's stdout output.
  277. Parameters:
  278. strings: Array of strings to look for.
  279. only_new: See _wait_for_output_str.
  280. matches: Check for the string this many times.
  281. Returns a tuple containing the matched string, and the complete line
  282. it was found in.
  283. Fails if none of the strings was read after 10 seconds
  284. (OUTPUT_WAIT_INTERVAL * OUTPUT_WAIT_MAX_INTERVALS).
  285. """
  286. return self._wait_for_output_str(self.stdout_filename, self.stdout,
  287. strings, only_new, matches)
  288. # Container class for a number of running processes
  289. # i.e. servers like bind10, etc
  290. # one-shot programs like dig or bindctl are started and closed separately
  291. class RunningProcesses:
  292. def __init__(self):
  293. """
  294. Initialize with no running processes.
  295. """
  296. self.processes = {}
  297. def add_process(self, step, process_name, args):
  298. """
  299. Start a process with the given arguments, and store it under the given
  300. name.
  301. Parameters:
  302. step: The scenario step it was called from. This is used for
  303. determining the output files for redirection of stdout
  304. and stderr.
  305. process_name: The name to refer to this running process later.
  306. args: Array of arguments to pass to Popen().
  307. Fails if a process with the given name is already running.
  308. """
  309. assert process_name not in self.processes,\
  310. "Process " + process_name + " already running"
  311. self.processes[process_name] = RunningProcess(step, process_name, args)
  312. def get_process(self, process_name):
  313. """
  314. Return the Process with the given process name.
  315. Parameters:
  316. process_name: The name of the process to return.
  317. Fails if the process is not running.
  318. """
  319. assert process_name in self.processes,\
  320. "Process " + name + " unknown"
  321. return self.processes[process_name]
  322. def stop_process(self, process_name):
  323. """
  324. Stop the Process with the given process name.
  325. Parameters:
  326. process_name: The name of the process to return.
  327. Fails if the process is not running.
  328. """
  329. assert process_name in self.processes,\
  330. "Process " + name + " unknown"
  331. self.processes[process_name].stop_process()
  332. del self.processes[process_name]
  333. def stop_all_processes(self):
  334. """
  335. Stop all running processes.
  336. """
  337. for process in self.processes.values():
  338. process.stop_process()
  339. def keep_files(self):
  340. """
  341. Keep the redirection files for stdout/stderr output of all processes
  342. instead of removing them when they are stopped later.
  343. """
  344. for process in self.processes.values():
  345. process.remove_files_on_exit = False
  346. def wait_for_stderr_str(self, process_name, strings, only_new = True, matches = 1):
  347. """
  348. Wait for one of the given strings in the given process's stderr output.
  349. Parameters:
  350. process_name: The name of the process to check the stderr output of.
  351. strings: Array of strings to look for.
  352. only_new: See _wait_for_output_str.
  353. matches: Check for the string this many times.
  354. Returns the matched string.
  355. Fails if none of the strings was read after 10 seconds
  356. (OUTPUT_WAIT_INTERVAL * OUTPUT_WAIT_MAX_INTERVALS).
  357. Fails if the process is unknown.
  358. """
  359. assert process_name in self.processes,\
  360. "Process " + process_name + " unknown"
  361. return self.processes[process_name].wait_for_stderr_str(strings,
  362. only_new,
  363. matches)
  364. def wait_for_stdout_str(self, process_name, strings, only_new = True, matches = 1):
  365. """
  366. Wait for one of the given strings in the given process's stdout output.
  367. Parameters:
  368. process_name: The name of the process to check the stdout output of.
  369. strings: Array of strings to look for.
  370. only_new: See _wait_for_output_str.
  371. matches: Check for the string this many times.
  372. Returns the matched string.
  373. Fails if none of the strings was read after 10 seconds
  374. (OUTPUT_WAIT_INTERVAL * OUTPUT_WAIT_MAX_INTERVALS).
  375. Fails if the process is unknown.
  376. """
  377. assert process_name in self.processes,\
  378. "Process " + process_name + " unknown"
  379. return self.processes[process_name].wait_for_stdout_str(strings,
  380. only_new,
  381. matches)
  382. @before.each_scenario
  383. def initialize(scenario):
  384. """
  385. Global initialization for each scenario.
  386. """
  387. # Keep track of running processes
  388. world.processes = RunningProcesses()
  389. # Convenience variable to access the last query result from querying.py
  390. world.last_query_result = None
  391. # Convenience variable to access the last HTTP response from http.py
  392. world.last_http_response = None
  393. # For slightly better errors, initialize a process_pids for the relevant
  394. # steps
  395. world.process_pids = None
  396. # Some tests can modify the settings. If the tests fail half-way, or
  397. # don't clean up, this can leave configurations or data in a bad state,
  398. # so we copy them from originals before each scenario
  399. for item in copylist:
  400. shutil.copy(item[0], item[1])
  401. for item in removelist:
  402. if os.path.exists(item):
  403. os.remove(item)
  404. @after.each_scenario
  405. def cleanup(scenario):
  406. """
  407. Global cleanup for each scenario.
  408. """
  409. # Keep output files if the scenario failed
  410. if not scenario.passed:
  411. world.processes.keep_files()
  412. # Stop any running processes we may have had around
  413. world.processes.stop_all_processes()
  414. # Environment check
  415. # Checks if LETTUCE_SETUP_COMPLETED is set in the environment
  416. # If not, abort with an error to use the run-script
  417. if 'LETTUCE_SETUP_COMPLETED' not in os.environ:
  418. print("Environment check failure; LETTUCE_SETUP_COMPLETED not set")
  419. print("Please use the run_lettuce.sh script. If you want to test an")
  420. print("installed version of bind10 with these tests, use")
  421. print("run_lettuce.sh -I [lettuce arguments]")
  422. sys.exit(1)