buildbot_unittest.py revision cad810f21b803229eb11403f9209855525a25d57
1# Copyright (C) 2009 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
29import unittest
30
31from webkitpy.common.net.layouttestresults import LayoutTestResults
32from webkitpy.common.net.buildbot import BuildBot, Builder, Build
33from webkitpy.layout_tests.layout_package import test_results
34from webkitpy.layout_tests.layout_package import test_failures
35from webkitpy.thirdparty.BeautifulSoup import BeautifulSoup
36
37
38class BuilderTest(unittest.TestCase):
39    def _mock_test_result(self, testname):
40        return test_results.TestResult(testname, [test_failures.FailureTextMismatch()])
41
42    def _install_fetch_build(self, failure):
43        def _mock_fetch_build(build_number):
44            build = Build(
45                builder=self.builder,
46                build_number=build_number,
47                revision=build_number + 1000,
48                is_green=build_number < 4
49            )
50            results = [self._mock_test_result(testname) for testname in failure(build_number)]
51            build._layout_test_results = LayoutTestResults(results)
52            return build
53        self.builder._fetch_build = _mock_fetch_build
54
55    def setUp(self):
56        self.buildbot = BuildBot()
57        self.builder = Builder(u"Test Builder \u2661", self.buildbot)
58        self._install_fetch_build(lambda build_number: ["test1", "test2"])
59
60    def test_find_regression_window(self):
61        regression_window = self.builder.find_regression_window(self.builder.build(10))
62        self.assertEqual(regression_window.build_before_failure().revision(), 1003)
63        self.assertEqual(regression_window.failing_build().revision(), 1004)
64
65        regression_window = self.builder.find_regression_window(self.builder.build(10), look_back_limit=2)
66        self.assertEqual(regression_window.build_before_failure(), None)
67        self.assertEqual(regression_window.failing_build().revision(), 1008)
68
69    def test_none_build(self):
70        self.builder._fetch_build = lambda build_number: None
71        regression_window = self.builder.find_regression_window(self.builder.build(10))
72        self.assertEqual(regression_window.build_before_failure(), None)
73        self.assertEqual(regression_window.failing_build(), None)
74
75    def test_flaky_tests(self):
76        self._install_fetch_build(lambda build_number: ["test1"] if build_number % 2 else ["test2"])
77        regression_window = self.builder.find_regression_window(self.builder.build(10))
78        self.assertEqual(regression_window.build_before_failure().revision(), 1009)
79        self.assertEqual(regression_window.failing_build().revision(), 1010)
80
81    def test_failure_and_flaky(self):
82        self._install_fetch_build(lambda build_number: ["test1", "test2"] if build_number % 2 else ["test2"])
83        regression_window = self.builder.find_regression_window(self.builder.build(10))
84        self.assertEqual(regression_window.build_before_failure().revision(), 1003)
85        self.assertEqual(regression_window.failing_build().revision(), 1004)
86
87    def test_no_results(self):
88        self._install_fetch_build(lambda build_number: ["test1", "test2"] if build_number % 2 else ["test2"])
89        regression_window = self.builder.find_regression_window(self.builder.build(10))
90        self.assertEqual(regression_window.build_before_failure().revision(), 1003)
91        self.assertEqual(regression_window.failing_build().revision(), 1004)
92
93    def test_failure_after_flaky(self):
94        self._install_fetch_build(lambda build_number: ["test1", "test2"] if build_number > 6 else ["test3"])
95        regression_window = self.builder.find_regression_window(self.builder.build(10))
96        self.assertEqual(regression_window.build_before_failure().revision(), 1006)
97        self.assertEqual(regression_window.failing_build().revision(), 1007)
98
99    def test_find_blameworthy_regression_window(self):
100        self.assertEqual(self.builder.find_blameworthy_regression_window(10).revisions(), [1004])
101        self.assertEqual(self.builder.find_blameworthy_regression_window(10, look_back_limit=2), None)
102        # Flakey test avoidance requires at least 2 red builds:
103        self.assertEqual(self.builder.find_blameworthy_regression_window(4), None)
104        self.assertEqual(self.builder.find_blameworthy_regression_window(4, avoid_flakey_tests=False).revisions(), [1004])
105        # Green builder:
106        self.assertEqual(self.builder.find_blameworthy_regression_window(3), None)
107
108    def test_build_caching(self):
109        self.assertEqual(self.builder.build(10), self.builder.build(10))
110
111    def test_build_and_revision_for_filename(self):
112        expectations = {
113            "r47483 (1)/" : (47483, 1),
114            "r47483 (1).zip" : (47483, 1),
115        }
116        for filename, revision_and_build in expectations.items():
117            self.assertEqual(self.builder._revision_and_build_for_filename(filename), revision_and_build)
118
119
120class BuildTest(unittest.TestCase):
121    def test_layout_test_results(self):
122        build = Build(None, None, None, None)
123        build._fetch_results_html = lambda: None
124        # Test that layout_test_results() returns None if the fetch fails.
125        self.assertEqual(build.layout_test_results(), None)
126
127
128class BuildBotTest(unittest.TestCase):
129
130    _example_one_box_status = '''
131    <table>
132    <tr>
133    <td class="box"><a href="builders/Windows%20Debug%20%28Tests%29">Windows Debug (Tests)</a></td>
134      <td align="center" class="LastBuild box success"><a href="builders/Windows%20Debug%20%28Tests%29/builds/3693">47380</a><br />build<br />successful</td>
135      <td align="center" class="Activity building">building<br />ETA in<br />~ 14 mins<br />at 13:40</td>
136    <tr>
137    <td class="box"><a href="builders/SnowLeopard%20Intel%20Release">SnowLeopard Intel Release</a></td>
138      <td class="LastBuild box" >no build</td>
139      <td align="center" class="Activity building">building<br />< 1 min</td>
140    <tr>
141    <td class="box"><a href="builders/Qt%20Linux%20Release">Qt Linux Release</a></td>
142      <td align="center" class="LastBuild box failure"><a href="builders/Qt%20Linux%20Release/builds/654">47383</a><br />failed<br />compile-webkit</td>
143      <td align="center" class="Activity idle">idle<br />3 pending</td>
144    <tr>
145    <td class="box"><a href="builders/Qt%20Windows%2032-bit%20Debug">Qt Windows 32-bit Debug</a></td>
146      <td align="center" class="LastBuild box failure"><a href="builders/Qt%20Windows%2032-bit%20Debug/builds/2090">60563</a><br />failed<br />failed<br />slave<br />lost</td>
147      <td align="center" class="Activity building">building<br />ETA in<br />~ 5 mins<br />at 08:25</td>
148    </table>
149'''
150    _expected_example_one_box_parsings = [
151        {
152            'is_green': True,
153            'build_number' : 3693,
154            'name': u'Windows Debug (Tests)',
155            'built_revision': 47380,
156            'activity': 'building',
157            'pending_builds': 0,
158        },
159        {
160            'is_green': False,
161            'build_number' : None,
162            'name': u'SnowLeopard Intel Release',
163            'built_revision': None,
164            'activity': 'building',
165            'pending_builds': 0,
166        },
167        {
168            'is_green': False,
169            'build_number' : 654,
170            'name': u'Qt Linux Release',
171            'built_revision': 47383,
172            'activity': 'idle',
173            'pending_builds': 3,
174        },
175        {
176            'is_green': True,
177            'build_number' : 2090,
178            'name': u'Qt Windows 32-bit Debug',
179            'built_revision': 60563,
180            'activity': 'building',
181            'pending_builds': 0,
182        },
183    ]
184
185    def test_status_parsing(self):
186        buildbot = BuildBot()
187
188        soup = BeautifulSoup(self._example_one_box_status)
189        status_table = soup.find("table")
190        input_rows = status_table.findAll('tr')
191
192        for x in range(len(input_rows)):
193            status_row = input_rows[x]
194            expected_parsing = self._expected_example_one_box_parsings[x]
195
196            builder = buildbot._parse_builder_status_from_row(status_row)
197
198            # Make sure we aren't parsing more or less than we expect
199            self.assertEquals(builder.keys(), expected_parsing.keys())
200
201            for key, expected_value in expected_parsing.items():
202                self.assertEquals(builder[key], expected_value, ("Builder %d parse failure for key: %s: Actual='%s' Expected='%s'" % (x, key, builder[key], expected_value)))
203
204    def test_core_builder_methods(self):
205        buildbot = BuildBot()
206
207        # Override builder_statuses function to not touch the network.
208        def example_builder_statuses(): # We could use instancemethod() to bind 'self' but we don't need to.
209            return BuildBotTest._expected_example_one_box_parsings
210        buildbot.builder_statuses = example_builder_statuses
211
212        buildbot.core_builder_names_regexps = [ 'Leopard', "Windows.*Build" ]
213        self.assertEquals(buildbot.red_core_builders_names(), [])
214        self.assertTrue(buildbot.core_builders_are_green())
215
216        buildbot.core_builder_names_regexps = [ 'SnowLeopard', 'Qt' ]
217        self.assertEquals(buildbot.red_core_builders_names(), [ u'SnowLeopard Intel Release', u'Qt Linux Release' ])
218        self.assertFalse(buildbot.core_builders_are_green())
219
220    def test_builder_name_regexps(self):
221        buildbot = BuildBot()
222
223        # For complete testing, this list should match the list of builders at build.webkit.org:
224        example_builders = [
225            {'name': u'Tiger Intel Release', },
226            {'name': u'Leopard Intel Release (Build)', },
227            {'name': u'Leopard Intel Release (Tests)', },
228            {'name': u'Leopard Intel Debug (Build)', },
229            {'name': u'Leopard Intel Debug (Tests)', },
230            {'name': u'SnowLeopard Intel Release (Build)', },
231            {'name': u'SnowLeopard Intel Release (Tests)', },
232            {'name': u'SnowLeopard Intel Release (WebKit2 Tests)', },
233            {'name': u'SnowLeopard Intel Leaks', },
234            {'name': u'Windows Release (Build)', },
235            {'name': u'Windows Release (Tests)', },
236            {'name': u'Windows Debug (Build)', },
237            {'name': u'Windows Debug (Tests)', },
238            {'name': u'GTK Linux 32-bit Release', },
239            {'name': u'GTK Linux 32-bit Debug', },
240            {'name': u'GTK Linux 64-bit Debug', },
241            {'name': u'GTK Linux 64-bit Release', },
242            {'name': u'Qt Linux Release', },
243            {'name': u'Qt Linux Release minimal', },
244            {'name': u'Qt Linux ARMv5 Release', },
245            {'name': u'Qt Linux ARMv7 Release', },
246            {'name': u'Qt Windows 32-bit Release', },
247            {'name': u'Qt Windows 32-bit Debug', },
248            {'name': u'Chromium Linux Release', },
249            {'name': u'Chromium Mac Release', },
250            {'name': u'Chromium Win Release', },
251            {'name': u'Chromium Linux Release (Tests)', },
252            {'name': u'Chromium Mac Release (Tests)', },
253            {'name': u'Chromium Win Release (Tests)', },
254            {'name': u'New run-webkit-tests', },
255        ]
256        name_regexps = [
257            "SnowLeopard.*Build",
258            "SnowLeopard.*\(Test",
259            "Leopard",
260            "Tiger",
261            "Windows.*Build",
262            "EFL",
263            "GTK.*32",
264            "GTK.*64.*Debug",  # Disallow the 64-bit Release bot which is broken.
265            "Qt",
266            "Chromium.*Release$",
267        ]
268        expected_builders = [
269            {'name': u'Tiger Intel Release', },
270            {'name': u'Leopard Intel Release (Build)', },
271            {'name': u'Leopard Intel Release (Tests)', },
272            {'name': u'Leopard Intel Debug (Build)', },
273            {'name': u'Leopard Intel Debug (Tests)', },
274            {'name': u'SnowLeopard Intel Release (Build)', },
275            {'name': u'SnowLeopard Intel Release (Tests)', },
276            {'name': u'Windows Release (Build)', },
277            {'name': u'Windows Debug (Build)', },
278            {'name': u'GTK Linux 32-bit Release', },
279            {'name': u'GTK Linux 32-bit Debug', },
280            {'name': u'GTK Linux 64-bit Debug', },
281            {'name': u'Qt Linux Release', },
282            {'name': u'Qt Linux Release minimal', },
283            {'name': u'Qt Linux ARMv5 Release', },
284            {'name': u'Qt Linux ARMv7 Release', },
285            {'name': u'Qt Windows 32-bit Release', },
286            {'name': u'Qt Windows 32-bit Debug', },
287            {'name': u'Chromium Linux Release', },
288            {'name': u'Chromium Mac Release', },
289            {'name': u'Chromium Win Release', },
290        ]
291
292        # This test should probably be updated if the default regexp list changes
293        self.assertEquals(buildbot.core_builder_names_regexps, name_regexps)
294
295        builders = buildbot._builder_statuses_with_names_matching_regexps(example_builders, name_regexps)
296        self.assertEquals(builders, expected_builders)
297
298    def test_builder_with_name(self):
299        buildbot = BuildBot()
300
301        builder = buildbot.builder_with_name("Test Builder")
302        self.assertEqual(builder.name(), "Test Builder")
303        self.assertEqual(builder.url(), "http://build.webkit.org/builders/Test%20Builder")
304        self.assertEqual(builder.url_encoded_name(), "Test%20Builder")
305        self.assertEqual(builder.results_url(), "http://build.webkit.org/results/Test%20Builder")
306
307        # Override _fetch_build_dictionary function to not touch the network.
308        def mock_fetch_build_dictionary(self, build_number):
309            build_dictionary = {
310                "sourceStamp": {
311                    "revision" : 2 * build_number,
312                    },
313                "number" : int(build_number),
314                "results" : build_number % 2, # 0 means pass
315            }
316            return build_dictionary
317        buildbot._fetch_build_dictionary = mock_fetch_build_dictionary
318
319        build = builder.build(10)
320        self.assertEqual(build.builder(), builder)
321        self.assertEqual(build.url(), "http://build.webkit.org/builders/Test%20Builder/builds/10")
322        self.assertEqual(build.results_url(), "http://build.webkit.org/results/Test%20Builder/r20%20%2810%29")
323        self.assertEqual(build.revision(), 20)
324        self.assertEqual(build.is_green(), True)
325
326        build = build.previous_build()
327        self.assertEqual(build.builder(), builder)
328        self.assertEqual(build.url(), "http://build.webkit.org/builders/Test%20Builder/builds/9")
329        self.assertEqual(build.results_url(), "http://build.webkit.org/results/Test%20Builder/r18%20%289%29")
330        self.assertEqual(build.revision(), 18)
331        self.assertEqual(build.is_green(), False)
332
333        self.assertEqual(builder.build(None), None)
334
335    _example_directory_listing = '''
336<h1>Directory listing for /results/SnowLeopard Intel Leaks/</h1>
337
338<table>
339        <tr class="alt">
340            <th>Filename</th>
341            <th>Size</th>
342            <th>Content type</th>
343            <th>Content encoding</th>
344        </tr>
345<tr class="directory ">
346    <td><a href="r47483%20%281%29/"><b>r47483 (1)/</b></a></td>
347    <td><b></b></td>
348    <td><b>[Directory]</b></td>
349    <td><b></b></td>
350</tr>
351<tr class="file alt">
352    <td><a href="r47484%20%282%29.zip">r47484 (2).zip</a></td>
353    <td>89K</td>
354    <td>[application/zip]</td>
355    <td></td>
356</tr>
357'''
358    _expected_files = [
359        {
360            "filename" : "r47483 (1)/",
361            "size" : "",
362            "type" : "[Directory]",
363            "encoding" : "",
364        },
365        {
366            "filename" : "r47484 (2).zip",
367            "size" : "89K",
368            "type" : "[application/zip]",
369            "encoding" : "",
370        },
371    ]
372
373    def test_parse_build_to_revision_map(self):
374        buildbot = BuildBot()
375        files = buildbot._parse_twisted_directory_listing(self._example_directory_listing)
376        self.assertEqual(self._expected_files, files)
377
378    # Revision, is_green
379    # Ordered from newest (highest number) to oldest.
380    fake_builder1 = [
381        [2, False],
382        [1, True],
383    ]
384    fake_builder2 = [
385        [2, False],
386        [1, True],
387    ]
388    fake_builders = [
389        fake_builder1,
390        fake_builder2,
391    ]
392    def _build_from_fake(self, fake_builder, index):
393        if index >= len(fake_builder):
394            return None
395        fake_build = fake_builder[index]
396        build = Build(
397            builder=fake_builder,
398            build_number=index,
399            revision=fake_build[0],
400            is_green=fake_build[1],
401        )
402        def mock_previous_build():
403            return self._build_from_fake(fake_builder, index + 1)
404        build.previous_build = mock_previous_build
405        return build
406
407    def _fake_builds_at_index(self, index):
408        return [self._build_from_fake(builder, index) for builder in self.fake_builders]
409
410    def test_last_green_revision(self):
411        buildbot = BuildBot()
412        def mock_builds_from_builders(only_core_builders):
413            return self._fake_builds_at_index(0)
414        buildbot._latest_builds_from_builders = mock_builds_from_builders
415        self.assertEqual(buildbot.last_green_revision(), 1)
416
417
418if __name__ == '__main__':
419    unittest.main()
420