rebaselineserver.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290
  1. # Copyright (c) 2010 Google Inc. All rights reserved.
  2. #
  3. # Redistribution and use in source and binary forms, with or without
  4. # modification, are permitted provided that the following conditions are
  5. # met:
  6. #
  7. # * Redistributions of source code must retain the above copyright
  8. # notice, this list of conditions and the following disclaimer.
  9. # * Redistributions in binary form must reproduce the above
  10. # copyright notice, this list of conditions and the following disclaimer
  11. # in the documentation and/or other materials provided with the
  12. # distribution.
  13. # * Neither the name of Google Inc. nor the names of its
  14. # contributors may be used to endorse or promote products derived from
  15. # this software without specific prior written permission.
  16. #
  17. # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
  18. # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
  19. # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
  20. # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
  21. # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
  22. # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
  23. # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
  24. # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
  25. # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
  26. # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
  27. # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  28. import fnmatch
  29. import os
  30. import os.path
  31. import BaseHTTPServer
  32. from webkitpy.common.host import Host # FIXME: This should not be needed!
  33. from webkitpy.common.system.executive import ScriptError
  34. from webkitpy.port.base import Port
  35. from webkitpy.tool.servers.reflectionhandler import ReflectionHandler
  36. STATE_NEEDS_REBASELINE = 'needs_rebaseline'
  37. STATE_REBASELINE_FAILED = 'rebaseline_failed'
  38. STATE_REBASELINE_SUCCEEDED = 'rebaseline_succeeded'
  39. def _get_actual_result_files(test_file, test_config):
  40. test_name, _ = os.path.splitext(test_file)
  41. test_directory = os.path.dirname(test_file)
  42. test_results_directory = test_config.filesystem.join(
  43. test_config.results_directory, test_directory)
  44. actual_pattern = os.path.basename(test_name) + '-actual.*'
  45. actual_files = []
  46. for filename in test_config.filesystem.listdir(test_results_directory):
  47. if fnmatch.fnmatch(filename, actual_pattern):
  48. actual_files.append(filename)
  49. actual_files.sort()
  50. return tuple(actual_files)
  51. def _rebaseline_test(test_file, baseline_target, baseline_move_to, test_config, log):
  52. test_name, _ = os.path.splitext(test_file)
  53. test_directory = os.path.dirname(test_name)
  54. log('Rebaselining %s...' % test_name)
  55. actual_result_files = _get_actual_result_files(test_file, test_config)
  56. filesystem = test_config.filesystem
  57. scm = test_config.scm
  58. layout_tests_directory = test_config.layout_tests_directory
  59. results_directory = test_config.results_directory
  60. target_expectations_directory = filesystem.join(
  61. layout_tests_directory, 'platform', baseline_target, test_directory)
  62. test_results_directory = test_config.filesystem.join(
  63. test_config.results_directory, test_directory)
  64. # If requested, move current baselines out
  65. current_baselines = get_test_baselines(test_file, test_config)
  66. if baseline_target in current_baselines and baseline_move_to != 'none':
  67. log(' Moving current %s baselines to %s' %
  68. (baseline_target, baseline_move_to))
  69. # See which ones we need to move (only those that are about to be
  70. # updated), and make sure we're not clobbering any files in the
  71. # destination.
  72. current_extensions = set(current_baselines[baseline_target].keys())
  73. actual_result_extensions = [
  74. os.path.splitext(f)[1] for f in actual_result_files]
  75. extensions_to_move = current_extensions.intersection(
  76. actual_result_extensions)
  77. if extensions_to_move.intersection(
  78. current_baselines.get(baseline_move_to, {}).keys()):
  79. log(' Already had baselines in %s, could not move existing '
  80. '%s ones' % (baseline_move_to, baseline_target))
  81. return False
  82. # Do the actual move.
  83. if extensions_to_move:
  84. if not _move_test_baselines(
  85. test_file,
  86. list(extensions_to_move),
  87. baseline_target,
  88. baseline_move_to,
  89. test_config,
  90. log):
  91. return False
  92. else:
  93. log(' No current baselines to move')
  94. log(' Updating baselines for %s' % baseline_target)
  95. filesystem.maybe_make_directory(target_expectations_directory)
  96. for source_file in actual_result_files:
  97. source_path = filesystem.join(test_results_directory, source_file)
  98. destination_file = source_file.replace('-actual', '-expected')
  99. destination_path = filesystem.join(
  100. target_expectations_directory, destination_file)
  101. filesystem.copyfile(source_path, destination_path)
  102. try:
  103. scm.add(destination_path)
  104. log(' Updated %s' % destination_file)
  105. except ScriptError, error:
  106. log(' Could not update %s in SCM, exit code %d' %
  107. (destination_file, error.exit_code))
  108. return False
  109. return True
  110. def _move_test_baselines(test_file, extensions_to_move, source_platform, destination_platform, test_config, log):
  111. test_file_name = os.path.splitext(os.path.basename(test_file))[0]
  112. test_directory = os.path.dirname(test_file)
  113. filesystem = test_config.filesystem
  114. # Want predictable output order for unit tests.
  115. extensions_to_move.sort()
  116. source_directory = os.path.join(
  117. test_config.layout_tests_directory,
  118. 'platform',
  119. source_platform,
  120. test_directory)
  121. destination_directory = os.path.join(
  122. test_config.layout_tests_directory,
  123. 'platform',
  124. destination_platform,
  125. test_directory)
  126. filesystem.maybe_make_directory(destination_directory)
  127. for extension in extensions_to_move:
  128. file_name = test_file_name + '-expected' + extension
  129. source_path = filesystem.join(source_directory, file_name)
  130. destination_path = filesystem.join(destination_directory, file_name)
  131. filesystem.copyfile(source_path, destination_path)
  132. try:
  133. test_config.scm.add(destination_path)
  134. log(' Moved %s' % file_name)
  135. except ScriptError, error:
  136. log(' Could not update %s in SCM, exit code %d' %
  137. (file_name, error.exit_code))
  138. return False
  139. return True
  140. def get_test_baselines(test_file, test_config):
  141. # FIXME: This seems like a hack. This only seems used to access the Port.expected_baselines logic.
  142. class AllPlatformsPort(Port):
  143. def __init__(self, host):
  144. super(AllPlatformsPort, self).__init__(host, 'mac')
  145. self._platforms_by_directory = dict([(self._webkit_baseline_path(p), p) for p in test_config.platforms])
  146. def baseline_search_path(self):
  147. return self._platforms_by_directory.keys()
  148. def platform_from_directory(self, directory):
  149. return self._platforms_by_directory[directory]
  150. test_path = test_config.filesystem.join(test_config.layout_tests_directory, test_file)
  151. # FIXME: This should get the Host from the test_config to be mockable!
  152. host = Host()
  153. host.initialize_scm()
  154. host.filesystem = test_config.filesystem
  155. all_platforms_port = AllPlatformsPort(host)
  156. all_test_baselines = {}
  157. for baseline_extension in ('.txt', '.checksum', '.png'):
  158. test_baselines = test_config.test_port.expected_baselines(test_file, baseline_extension)
  159. baselines = all_platforms_port.expected_baselines(test_file, baseline_extension, all_baselines=True)
  160. for platform_directory, expected_filename in baselines:
  161. if not platform_directory:
  162. continue
  163. if platform_directory == test_config.layout_tests_directory:
  164. platform = 'base'
  165. else:
  166. platform = all_platforms_port.platform_from_directory(platform_directory)
  167. platform_baselines = all_test_baselines.setdefault(platform, {})
  168. was_used_for_test = (platform_directory, expected_filename) in test_baselines
  169. platform_baselines[baseline_extension] = was_used_for_test
  170. return all_test_baselines
  171. class RebaselineHTTPServer(BaseHTTPServer.HTTPServer):
  172. def __init__(self, httpd_port, config):
  173. server_name = ""
  174. BaseHTTPServer.HTTPServer.__init__(self, (server_name, httpd_port), RebaselineHTTPRequestHandler)
  175. self.test_config = config['test_config']
  176. self.results_json = config['results_json']
  177. self.platforms_json = config['platforms_json']
  178. class RebaselineHTTPRequestHandler(ReflectionHandler):
  179. STATIC_FILE_NAMES = frozenset([
  180. "index.html",
  181. "loupe.js",
  182. "main.js",
  183. "main.css",
  184. "queue.js",
  185. "util.js",
  186. ])
  187. STATIC_FILE_DIRECTORY = os.path.join(os.path.dirname(__file__), "data", "rebaselineserver")
  188. def results_json(self):
  189. self._serve_json(self.server.results_json)
  190. def test_config(self):
  191. self._serve_json(self.server.test_config)
  192. def platforms_json(self):
  193. self._serve_json(self.server.platforms_json)
  194. def rebaseline(self):
  195. test = self.query['test'][0]
  196. baseline_target = self.query['baseline-target'][0]
  197. baseline_move_to = self.query['baseline-move-to'][0]
  198. test_json = self.server.results_json['tests'][test]
  199. if test_json['state'] != STATE_NEEDS_REBASELINE:
  200. self.send_error(400, "Test %s is in unexpected state: %s" % (test, test_json["state"]))
  201. return
  202. log = []
  203. success = _rebaseline_test(
  204. test,
  205. baseline_target,
  206. baseline_move_to,
  207. self.server.test_config,
  208. log=lambda l: log.append(l))
  209. if success:
  210. test_json['state'] = STATE_REBASELINE_SUCCEEDED
  211. self.send_response(200)
  212. else:
  213. test_json['state'] = STATE_REBASELINE_FAILED
  214. self.send_response(500)
  215. self.send_header('Content-type', 'text/plain')
  216. self.end_headers()
  217. self.wfile.write('\n'.join(log))
  218. def test_result(self):
  219. test_name, _ = os.path.splitext(self.query['test'][0])
  220. mode = self.query['mode'][0]
  221. if mode == 'expected-image':
  222. file_name = test_name + '-expected.png'
  223. elif mode == 'actual-image':
  224. file_name = test_name + '-actual.png'
  225. if mode == 'expected-checksum':
  226. file_name = test_name + '-expected.checksum'
  227. elif mode == 'actual-checksum':
  228. file_name = test_name + '-actual.checksum'
  229. elif mode == 'diff-image':
  230. file_name = test_name + '-diff.png'
  231. if mode == 'expected-text':
  232. file_name = test_name + '-expected.txt'
  233. elif mode == 'actual-text':
  234. file_name = test_name + '-actual.txt'
  235. elif mode == 'diff-text':
  236. file_name = test_name + '-diff.txt'
  237. elif mode == 'diff-text-pretty':
  238. file_name = test_name + '-pretty-diff.html'
  239. file_path = os.path.join(self.server.test_config.results_directory, file_name)
  240. # Let results be cached for 60 seconds, so that they can be pre-fetched
  241. # by the UI
  242. self._serve_file(file_path, cacheable_seconds=60)