bisection.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281
  1. import math
  2. import mozinfo
  3. class Bisect(object):
  4. "Class for creating, bisecting and summarizing for --bisect-chunk option."
  5. def __init__(self, harness):
  6. super(Bisect, self).__init__()
  7. self.summary = []
  8. self.contents = {}
  9. self.repeat = 10
  10. self.failcount = 0
  11. self.max_failures = 3
  12. def setup(self, tests):
  13. """This method is used to initialize various variables that are required
  14. for test bisection"""
  15. status = 0
  16. self.contents.clear()
  17. # We need totalTests key in contents for sanity check
  18. self.contents['totalTests'] = tests
  19. self.contents['tests'] = tests
  20. self.contents['loop'] = 0
  21. return status
  22. def reset(self, expectedError, result):
  23. """This method is used to initialize self.expectedError and self.result
  24. for each loop in runtests."""
  25. self.expectedError = expectedError
  26. self.result = result
  27. def get_tests_for_bisection(self, options, tests):
  28. """Make a list of tests for bisection from a given list of tests"""
  29. bisectlist = []
  30. for test in tests:
  31. bisectlist.append(test)
  32. if test.endswith(options.bisectChunk):
  33. break
  34. return bisectlist
  35. def pre_test(self, options, tests, status):
  36. """This method is used to call other methods for setting up variables and
  37. getting the list of tests for bisection."""
  38. if options.bisectChunk == "default":
  39. return tests
  40. # The second condition in 'if' is required to verify that the failing
  41. # test is the last one.
  42. elif ('loop' not in self.contents or not self.contents['tests'][-1].endswith(
  43. options.bisectChunk)):
  44. tests = self.get_tests_for_bisection(options, tests)
  45. status = self.setup(tests)
  46. return self.next_chunk_binary(options, status)
  47. def post_test(self, options, expectedError, result):
  48. """This method is used to call other methods to summarize results and check whether a
  49. sanity check is done or not."""
  50. self.reset(expectedError, result)
  51. status = self.summarize_chunk(options)
  52. # Check whether sanity check has to be done. Also it is necessary to check whether
  53. # options.bisectChunk is present in self.expectedError as we do not want to run
  54. # if it is "default".
  55. if status == -1 and options.bisectChunk in self.expectedError:
  56. # In case we have a debug build, we don't want to run a sanity
  57. # check, will take too much time.
  58. if mozinfo.info['debug']:
  59. return status
  60. testBleedThrough = self.contents['testsToRun'][0]
  61. tests = self.contents['totalTests']
  62. tests.remove(testBleedThrough)
  63. # To make sure that the failing test is dependent on some other
  64. # test.
  65. if options.bisectChunk in testBleedThrough:
  66. return status
  67. status = self.setup(tests)
  68. self.summary.append("Sanity Check:")
  69. return status
  70. def next_chunk_reverse(self, options, status):
  71. "This method is used to bisect the tests in a reverse search fashion."
  72. # Base Cases.
  73. if self.contents['loop'] <= 1:
  74. self.contents['testsToRun'] = self.contents['tests']
  75. if self.contents['loop'] == 1:
  76. self.contents['testsToRun'] = [self.contents['tests'][-1]]
  77. self.contents['loop'] += 1
  78. return self.contents['testsToRun']
  79. if 'result' in self.contents:
  80. if self.contents['result'] == "PASS":
  81. chunkSize = self.contents['end'] - self.contents['start']
  82. self.contents['end'] = self.contents['start'] - 1
  83. self.contents['start'] = self.contents['end'] - chunkSize
  84. # self.contents['result'] will be expected error only if it fails.
  85. elif self.contents['result'] == "FAIL":
  86. self.contents['tests'] = self.contents['testsToRun']
  87. status = 1 # for initializing
  88. # initialize
  89. if status:
  90. totalTests = len(self.contents['tests'])
  91. chunkSize = int(math.ceil(totalTests / 10.0))
  92. self.contents['start'] = totalTests - chunkSize - 1
  93. self.contents['end'] = totalTests - 2
  94. start = self.contents['start']
  95. end = self.contents['end'] + 1
  96. self.contents['testsToRun'] = self.contents['tests'][start:end]
  97. self.contents['testsToRun'].append(self.contents['tests'][-1])
  98. self.contents['loop'] += 1
  99. return self.contents['testsToRun']
  100. def next_chunk_binary(self, options, status):
  101. "This method is used to bisect the tests in a binary search fashion."
  102. # Base cases.
  103. if self.contents['loop'] <= 1:
  104. self.contents['testsToRun'] = self.contents['tests']
  105. if self.contents['loop'] == 1:
  106. self.contents['testsToRun'] = [self.contents['tests'][-1]]
  107. self.contents['loop'] += 1
  108. return self.contents['testsToRun']
  109. # Initialize the contents dict.
  110. if status:
  111. totalTests = len(self.contents['tests'])
  112. self.contents['start'] = 0
  113. self.contents['end'] = totalTests - 2
  114. mid = (self.contents['start'] + self.contents['end']) / 2
  115. if 'result' in self.contents:
  116. if self.contents['result'] == "PASS":
  117. self.contents['end'] = mid
  118. elif self.contents['result'] == "FAIL":
  119. self.contents['start'] = mid + 1
  120. mid = (self.contents['start'] + self.contents['end']) / 2
  121. start = mid + 1
  122. end = self.contents['end'] + 1
  123. self.contents['testsToRun'] = self.contents['tests'][start:end]
  124. if not self.contents['testsToRun']:
  125. self.contents['testsToRun'].append(self.contents['tests'][mid])
  126. self.contents['testsToRun'].append(self.contents['tests'][-1])
  127. self.contents['loop'] += 1
  128. return self.contents['testsToRun']
  129. def summarize_chunk(self, options):
  130. "This method is used summarize the results after the list of tests is run."
  131. if options.bisectChunk == "default":
  132. # if no expectedError that means all the tests have successfully
  133. # passed.
  134. if len(self.expectedError) == 0:
  135. return -1
  136. options.bisectChunk = self.expectedError.keys()[0]
  137. self.summary.append(
  138. "\tFound Error in test: %s" %
  139. options.bisectChunk)
  140. return 0
  141. # If options.bisectChunk is not in self.result then we need to move to
  142. # the next run.
  143. if options.bisectChunk not in self.result:
  144. return -1
  145. self.summary.append("\tPass %d:" % self.contents['loop'])
  146. if len(self.contents['testsToRun']) > 1:
  147. self.summary.append(
  148. "\t\t%d test files(start,end,failing). [%s, %s, %s]" % (len(
  149. self.contents['testsToRun']),
  150. self.contents['testsToRun'][0],
  151. self.contents['testsToRun'][
  152. -2],
  153. self.contents['testsToRun'][
  154. -1]))
  155. else:
  156. self.summary.append(
  157. "\t\t1 test file [%s]" %
  158. self.contents['testsToRun'][0])
  159. return self.check_for_intermittent(options)
  160. if self.result[options.bisectChunk] == "PASS":
  161. self.summary.append("\t\tno failures found.")
  162. if self.contents['loop'] == 1:
  163. status = -1
  164. else:
  165. self.contents['result'] = "PASS"
  166. status = 0
  167. elif self.result[options.bisectChunk] == "FAIL":
  168. if 'expectedError' not in self.contents:
  169. self.summary.append("\t\t%s failed." %
  170. self.contents['testsToRun'][-1])
  171. self.contents['expectedError'] = self.expectedError[
  172. options.bisectChunk]
  173. status = 0
  174. elif self.expectedError[options.bisectChunk] == self.contents['expectedError']:
  175. self.summary.append(
  176. "\t\t%s failed with expected error." % self.contents['testsToRun'][-1])
  177. self.contents['result'] = "FAIL"
  178. status = 0
  179. # This code checks for test-bleedthrough. Should work for any
  180. # algorithm.
  181. numberOfTests = len(self.contents['testsToRun'])
  182. if numberOfTests < 3:
  183. # This means that only 2 tests are run. Since the last test
  184. # is the failing test itself therefore the bleedthrough
  185. # test is the first test
  186. self.summary.append(
  187. "TEST-UNEXPECTED-FAIL | %s | Bleedthrough detected, this test is the "
  188. "root cause for many of the above failures" %
  189. self.contents['testsToRun'][0])
  190. status = -1
  191. else:
  192. self.summary.append(
  193. "\t\t%s failed with different error." % self.contents['testsToRun'][-1])
  194. status = -1
  195. return status
  196. def check_for_intermittent(self, options):
  197. "This method is used to check whether a test is an intermittent."
  198. if self.result[options.bisectChunk] == "PASS":
  199. self.summary.append(
  200. "\t\tThe test %s passed." %
  201. self.contents['testsToRun'][0])
  202. if self.repeat > 0:
  203. # loop is set to 1 to again run the single test.
  204. self.contents['loop'] = 1
  205. self.repeat -= 1
  206. return 0
  207. else:
  208. if self.failcount > 0:
  209. # -1 is being returned as the test is intermittent, so no need to bisect
  210. # further.
  211. return -1
  212. # If the test does not fail even once, then proceed to next chunk for bisection.
  213. # loop is set to 2 to proceed on bisection.
  214. self.contents['loop'] = 2
  215. return 1
  216. elif self.result[options.bisectChunk] == "FAIL":
  217. self.summary.append(
  218. "\t\tThe test %s failed." %
  219. self.contents['testsToRun'][0])
  220. self.failcount += 1
  221. self.contents['loop'] = 1
  222. self.repeat -= 1
  223. # self.max_failures is the maximum number of times a test is allowed
  224. # to fail to be called an intermittent. If a test fails more than
  225. # limit set, it is a perma-fail.
  226. if self.failcount < self.max_failures:
  227. if self.repeat == 0:
  228. # -1 is being returned as the test is intermittent, so no need to bisect
  229. # further.
  230. return -1
  231. return 0
  232. else:
  233. self.summary.append(
  234. "TEST-UNEXPECTED-FAIL | %s | Bleedthrough detected, this test is the "
  235. "root cause for many of the above failures" %
  236. self.contents['testsToRun'][0])
  237. return -1
  238. def print_summary(self):
  239. "This method is used to print the recorded summary."
  240. print "Bisection summary:"
  241. for line in self.summary:
  242. print line