| # Copyright (C) 2009 Google Inc. All rights reserved. |
| # |
| # Redistribution and use in source and binary forms, with or without |
| # modification, are permitted provided that the following conditions are |
| # met: |
| # |
| # * Redistributions of source code must retain the above copyright |
| # notice, this list of conditions and the following disclaimer. |
| # * Redistributions in binary form must reproduce the above |
| # copyright notice, this list of conditions and the following disclaimer |
| # in the documentation and/or other materials provided with the |
| # distribution. |
| # * Neither the name of Google Inc. nor the names of its |
| # contributors may be used to endorse or promote products derived from |
| # this software without specific prior written permission. |
| # |
| # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
| # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
| # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
| # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT |
| # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
| # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
| # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
| # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
| # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
| # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
| # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| |
| import unittest |
| |
| from webkitpy.common.net.layouttestresults import LayoutTestResults |
| from webkitpy.common.net.buildbot import BuildBot, Builder, Build |
| from webkitpy.layout_tests.models import test_results |
| from webkitpy.layout_tests.models import test_failures |
| from webkitpy.thirdparty.BeautifulSoup import BeautifulSoup |
| |
| |
| class BuilderTest(unittest.TestCase): |
| def _mock_test_result(self, testname): |
| return test_results.TestResult(testname, [test_failures.FailureTextMismatch()]) |
| |
| def _install_fetch_build(self, failure): |
| def _mock_fetch_build(build_number): |
| build = Build( |
| builder=self.builder, |
| build_number=build_number, |
| revision=build_number + 1000, |
| is_green=build_number < 4 |
| ) |
| results = [self._mock_test_result(testname) for testname in failure(build_number)] |
| layout_test_results = LayoutTestResults(test_results=results, did_exceed_test_failure_limit=False) |
| |
| def mock_layout_test_results(): |
| return layout_test_results |
| build.layout_test_results = mock_layout_test_results |
| return build |
| self.builder._fetch_build = _mock_fetch_build |
| |
| def setUp(self): |
| self.buildbot = BuildBot() |
| self.builder = Builder(u"Test Builder \u2661", self.buildbot) |
| self._install_fetch_build(lambda build_number: ["test1", "test2"]) |
| |
| def test_latest_layout_test_results(self): |
| self.builder.fetch_layout_test_results = lambda results_url: LayoutTestResults(test_results=[self._mock_test_result(testname) for testname in ["test1", "test2"]], did_exceed_test_failure_limit=False) |
| self.builder.accumulated_results_url = lambda: "http://dummy_url.org" |
| self.assertTrue(self.builder.latest_layout_test_results()) |
| |
| def test_find_regression_window(self): |
| regression_window = self.builder.find_regression_window(self.builder.build(10)) |
| self.assertEqual(regression_window.build_before_failure().revision(), 1003) |
| self.assertEqual(regression_window.failing_build().revision(), 1004) |
| |
| regression_window = self.builder.find_regression_window(self.builder.build(10), look_back_limit=2) |
| self.assertIsNone(regression_window.build_before_failure()) |
| self.assertEqual(regression_window.failing_build().revision(), 1008) |
| |
| def test_none_build(self): |
| self.builder._fetch_build = lambda build_number: None |
| regression_window = self.builder.find_regression_window(self.builder.build(10)) |
| self.assertIsNone(regression_window.build_before_failure()) |
| self.assertIsNone(regression_window.failing_build()) |
| |
| def test_flaky_tests(self): |
| self._install_fetch_build(lambda build_number: ["test1"] if build_number % 2 else ["test2"]) |
| regression_window = self.builder.find_regression_window(self.builder.build(10)) |
| self.assertEqual(regression_window.build_before_failure().revision(), 1009) |
| self.assertEqual(regression_window.failing_build().revision(), 1010) |
| |
| def test_failure_and_flaky(self): |
| self._install_fetch_build(lambda build_number: ["test1", "test2"] if build_number % 2 else ["test2"]) |
| regression_window = self.builder.find_regression_window(self.builder.build(10)) |
| self.assertEqual(regression_window.build_before_failure().revision(), 1003) |
| self.assertEqual(regression_window.failing_build().revision(), 1004) |
| |
| def test_no_results(self): |
| self._install_fetch_build(lambda build_number: ["test1", "test2"] if build_number % 2 else ["test2"]) |
| regression_window = self.builder.find_regression_window(self.builder.build(10)) |
| self.assertEqual(regression_window.build_before_failure().revision(), 1003) |
| self.assertEqual(regression_window.failing_build().revision(), 1004) |
| |
| def test_failure_after_flaky(self): |
| self._install_fetch_build(lambda build_number: ["test1", "test2"] if build_number > 6 else ["test3"]) |
| regression_window = self.builder.find_regression_window(self.builder.build(10)) |
| self.assertEqual(regression_window.build_before_failure().revision(), 1006) |
| self.assertEqual(regression_window.failing_build().revision(), 1007) |
| |
| def test_find_blameworthy_regression_window(self): |
| self.assertEqual(self.builder.find_blameworthy_regression_window(10).revisions(), [1004]) |
| self.assertIsNone(self.builder.find_blameworthy_regression_window(10, look_back_limit=2)) |
| # Flakey test avoidance requires at least 2 red builds: |
| self.assertIsNone(self.builder.find_blameworthy_regression_window(4)) |
| self.assertEqual(self.builder.find_blameworthy_regression_window(4, avoid_flakey_tests=False).revisions(), [1004]) |
| # Green builder: |
| self.assertIsNone(self.builder.find_blameworthy_regression_window(3)) |
| |
| def test_build_caching(self): |
| self.assertEqual(self.builder.build(10), self.builder.build(10)) |
| |
| def test_build_and_revision_for_filename(self): |
| expectations = { |
| "r47483 (1)/": (47483, 1), |
| "r47483 (1).zip": (47483, 1), |
| "random junk": None, |
| } |
| for filename, revision_and_build in expectations.items(): |
| self.assertEqual(self.builder._revision_and_build_for_filename(filename), revision_and_build) |
| |
| def test_file_info_list_to_revision_to_build_list(self): |
| file_info_list = [ |
| {"filename": "r47483 (1)/"}, |
| {"filename": "r47483 (1).zip"}, |
| {"filename": "random junk"}, |
| ] |
| builds_and_revisions_list = [(47483, 1), (47483, 1)] |
| self.assertEqual(self.builder._file_info_list_to_revision_to_build_list(file_info_list), builds_and_revisions_list) |
| |
| def test_fetch_build(self): |
| buildbot = BuildBot() |
| builder = Builder(u"Test Builder \u2661", buildbot) |
| |
| def mock_fetch_build_dictionary(self, build_number): |
| build_dictionary = { |
| "sourceStamp": { |
| "revision": None, # revision=None means a trunk build started from the force-build button on the builder page. |
| }, |
| "number": int(build_number), |
| # Intentionally missing the 'results' key, meaning it's a "pass" build. |
| } |
| return build_dictionary |
| buildbot._fetch_build_dictionary = mock_fetch_build_dictionary |
| self.assertIsNotNone(builder._fetch_build(1)) |
| |
| |
| class BuildTest(unittest.TestCase): |
| def test_layout_test_results(self): |
| buildbot = BuildBot() |
| builder = Builder(u"Foo Builder (test)", buildbot) |
| builder._fetch_file_from_results = lambda results_url, file_name: None |
| build = Build(builder, None, None, None) |
| # Test that layout_test_results() returns None if the fetch fails. |
| self.assertIsNone(build.layout_test_results()) |
| |
| |
| class BuildBotTest(unittest.TestCase): |
| |
| _example_one_box_status = ''' |
| <table> |
| <tr> |
| <td class="box"><a href="builders/Windows%20Debug%20%28Tests%29">Windows Debug (Tests)</a></td> |
| <td align="center" class="LastBuild box success"><a href="builders/Windows%20Debug%20%28Tests%29/builds/3693">47380</a><br />build<br />successful</td> |
| <td align="center" class="Activity building">building<br />ETA in<br />~ 14 mins<br />at 13:40</td> |
| <tr> |
| <td class="box"><a href="builders/SnowLeopard%20Intel%20Release">SnowLeopard Intel Release</a></td> |
| <td class="LastBuild box" >no build</td> |
| <td align="center" class="Activity building">building<br />< 1 min</td> |
| <tr> |
| <td class="box"><a href="builders/Qt%20Linux%20Release">Qt Linux Release</a></td> |
| <td align="center" class="LastBuild box failure"><a href="builders/Qt%20Linux%20Release/builds/654">47383</a><br />failed<br />compile-webkit</td> |
| <td align="center" class="Activity idle">idle<br />3 pending</td> |
| <tr> |
| <td class="box"><a href="builders/Qt%20Windows%2032-bit%20Debug">Qt Windows 32-bit Debug</a></td> |
| <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> |
| <td align="center" class="Activity building">building<br />ETA in<br />~ 5 mins<br />at 08:25</td> |
| </table> |
| ''' |
| _expected_example_one_box_parsings = [ |
| { |
| 'is_green': True, |
| 'build_number': 3693, |
| 'name': u'Windows Debug (Tests)', |
| 'built_revision': 47380, |
| 'activity': 'building', |
| 'pending_builds': 0, |
| }, |
| { |
| 'is_green': False, |
| 'build_number': None, |
| 'name': u'SnowLeopard Intel Release', |
| 'built_revision': None, |
| 'activity': 'building', |
| 'pending_builds': 0, |
| }, |
| { |
| 'is_green': False, |
| 'build_number': 654, |
| 'name': u'Qt Linux Release', |
| 'built_revision': 47383, |
| 'activity': 'idle', |
| 'pending_builds': 3, |
| }, |
| { |
| 'is_green': True, |
| 'build_number': 2090, |
| 'name': u'Qt Windows 32-bit Debug', |
| 'built_revision': 60563, |
| 'activity': 'building', |
| 'pending_builds': 0, |
| }, |
| ] |
| |
| def test_status_parsing(self): |
| buildbot = BuildBot() |
| |
| soup = BeautifulSoup(self._example_one_box_status) |
| status_table = soup.find("table") |
| input_rows = status_table.findAll('tr') |
| |
| for x in range(len(input_rows)): |
| status_row = input_rows[x] |
| expected_parsing = self._expected_example_one_box_parsings[x] |
| |
| builder = buildbot._parse_builder_status_from_row(status_row) |
| |
| # Make sure we aren't parsing more or less than we expect |
| self.assertEqual(builder.keys(), expected_parsing.keys()) |
| |
| for key, expected_value in expected_parsing.items(): |
| self.assertEqual(builder[key], expected_value, ("Builder %d parse failure for key: %s: Actual='%s' Expected='%s'" % (x, key, builder[key], expected_value))) |
| |
| def test_builder_with_name(self): |
| buildbot = BuildBot() |
| |
| builder = buildbot.builder_with_name("Test Builder") |
| self.assertEqual(builder.name(), "Test Builder") |
| self.assertEqual(builder.url(), "https://build.webkit.org/builders/Test%20Builder") |
| self.assertEqual(builder.url_encoded_name(), "Test%20Builder") |
| self.assertEqual(builder.results_url(), "https://build.webkit.org/results/Test%20Builder") |
| |
| # Override _fetch_build_dictionary function to not touch the network. |
| def mock_fetch_build_dictionary(self, build_number): |
| build_dictionary = { |
| "sourceStamp": { |
| "revision": 2 * build_number, |
| }, |
| "number": int(build_number), |
| "results": build_number % 2, # 0 means pass |
| } |
| return build_dictionary |
| buildbot._fetch_build_dictionary = mock_fetch_build_dictionary |
| |
| build = builder.build(10) |
| self.assertEqual(build.builder(), builder) |
| self.assertEqual(build.url(), "https://build.webkit.org/builders/Test%20Builder/builds/10") |
| self.assertEqual(build.results_url(), "https://build.webkit.org/results/Test%20Builder/r20%20%2810%29") |
| self.assertEqual(build.revision(), 20) |
| self.assertTrue(build.is_green()) |
| |
| build = build.previous_build() |
| self.assertEqual(build.builder(), builder) |
| self.assertEqual(build.url(), "https://build.webkit.org/builders/Test%20Builder/builds/9") |
| self.assertEqual(build.results_url(), "https://build.webkit.org/results/Test%20Builder/r18%20%289%29") |
| self.assertEqual(build.revision(), 18) |
| self.assertFalse(build.is_green()) |
| |
| self.assertIsNone(builder.build(None)) |
| |
| _example_directory_listing = ''' |
| <h1>Directory listing for /results/SnowLeopard Intel Leaks/</h1> |
| |
| <table> |
| <tr class="alt"> |
| <th>Filename</th> |
| <th>Size</th> |
| <th>Content type</th> |
| <th>Content encoding</th> |
| </tr> |
| <tr class="directory "> |
| <td><a href="r47483%20%281%29/"><b>r47483 (1)/</b></a></td> |
| <td><b></b></td> |
| <td><b>[Directory]</b></td> |
| <td><b></b></td> |
| </tr> |
| <tr class="file alt"> |
| <td><a href="r47484%20%282%29.zip">r47484 (2).zip</a></td> |
| <td>89K</td> |
| <td>[application/zip]</td> |
| <td></td> |
| </tr> |
| ''' |
| _expected_files = [ |
| { |
| "filename": "r47483 (1)/", |
| "size": "", |
| "type": "[Directory]", |
| "encoding": "", |
| }, |
| { |
| "filename": "r47484 (2).zip", |
| "size": "89K", |
| "type": "[application/zip]", |
| "encoding": "", |
| }, |
| ] |
| |
| def test_parse_build_to_revision_map(self): |
| buildbot = BuildBot() |
| files = buildbot._parse_twisted_directory_listing(self._example_directory_listing) |
| self.assertEqual(self._expected_files, files) |
| |
| _fake_builder_page = ''' |
| <body> |
| <div class="content"> |
| <h1>Some Builder</h1> |
| <p>(<a href="../waterfall?show=Some Builder">view in waterfall</a>)</p> |
| <div class="column"> |
| <h2>Recent Builds:</h2> |
| <table class="info"> |
| <tr> |
| <th>Time</th> |
| <th>Revision</th> |
| <th>Result</th> <th>Build #</th> |
| <th>Info</th> |
| </tr> |
| <tr class="alt"> |
| <td>Jan 10 15:49</td> |
| <td><span class="revision" title="Revision 104643"><a href="https://trac.webkit.org/changeset/104643">104643</a></span></td> |
| <td class="success">failure</td> <td><a href=".../37604">#37604</a></td> |
| <td class="left">Build successful</td> |
| </tr> |
| <tr class=""> |
| <td>Jan 10 15:32</td> |
| <td><span class="revision" title="Revision 104636"><a href="https://trac.webkit.org/changeset/104636">104636</a></span></td> |
| <td class="success">failure</td> <td><a href=".../37603">#37603</a></td> |
| <td class="left">Build successful</td> |
| </tr> |
| <tr class="alt"> |
| <td>Jan 10 15:18</td> |
| <td><span class="revision" title="Revision 104635"><a href="https://trac.webkit.org/changeset/104635">104635</a></span></td> |
| <td class="success">success</td> <td><a href=".../37602">#37602</a></td> |
| <td class="left">Build successful</td> |
| </tr> |
| <tr class=""> |
| <td>Jan 10 14:51</td> |
| <td><span class="revision" title="Revision 104633"><a href="https://trac.webkit.org/changeset/104633">104633</a></span></td> |
| <td class="failure">failure</td> <td><a href=".../37601">#37601</a></td> |
| <td class="left">Failed compile-webkit</td> |
| </tr> |
| </table> |
| </body>''' |
| _fake_builder_page_without_success = ''' |
| <body> |
| <table> |
| <tr class="alt"> |
| <td>Jan 10 15:49</td> |
| <td><span class="revision" title="Revision 104643"><a href="https://trac.webkit.org/changeset/104643">104643</a></span></td> |
| <td class="success">failure</td> |
| </tr> |
| <tr class=""> |
| <td>Jan 10 15:32</td> |
| <td><span class="revision" title="Revision 104636"><a href="https://trac.webkit.org/changeset/104636">104636</a></span></td> |
| <td class="success">failure</td> |
| </tr> |
| <tr class="alt"> |
| <td>Jan 10 15:18</td> |
| <td><span class="revision" title="Revision 104635"><a href="https://trac.webkit.org/changeset/104635">104635</a></span></td> |
| <td class="success">failure</td> |
| </tr> |
| <tr class=""> |
| <td>Jan 10 11:58</td> |
| <td><span class="revision" title="Revision ??"><a href="https://trac.webkit.org/changeset/%3F%3F">??</a></span></td> |
| <td class="retry">retry</td> |
| </tr> |
| <tr class=""> |
| <td>Jan 10 14:51</td> |
| <td><span class="revision" title="Revision 104633"><a href="https://trac.webkit.org/changeset/104633">104633</a></span></td> |
| <td class="failure">failure</td> |
| </tr> |
| </table> |
| </body>''' |
| |
| def test_revisions_for_builder(self): |
| buildbot = BuildBot() |
| buildbot._fetch_builder_page = lambda builder: builder.page |
| builder_with_success = Builder('Some builder', None) |
| builder_with_success.page = self._fake_builder_page |
| self.assertEqual(buildbot._revisions_for_builder(builder_with_success), [(104643, False), (104636, False), (104635, True), (104633, False)]) |
| |
| builder_without_success = Builder('Some builder', None) |
| builder_without_success.page = self._fake_builder_page_without_success |
| self.assertEqual(buildbot._revisions_for_builder(builder_without_success), [(104643, False), (104636, False), (104635, False), (104633, False)]) |
| |
| def test_find_green_revision(self): |
| buildbot = BuildBot() |
| self.assertEqual(buildbot._find_green_revision({ |
| 'Builder 1': [(1, True), (3, True)], |
| 'Builder 2': [(1, True), (3, False)], |
| 'Builder 3': [(1, True), (3, True)], |
| }), 1) |
| self.assertEqual(buildbot._find_green_revision({ |
| 'Builder 1': [(1, False), (3, True)], |
| 'Builder 2': [(1, True), (3, True)], |
| 'Builder 3': [(1, True), (3, True)], |
| }), 3) |
| self.assertEqual(buildbot._find_green_revision({ |
| 'Builder 1': [(1, True), (2, True)], |
| 'Builder 2': [(1, False), (2, True), (3, True)], |
| 'Builder 3': [(1, True), (3, True)], |
| }), None) |
| self.assertEqual(buildbot._find_green_revision({ |
| 'Builder 1': [(1, True), (2, True)], |
| 'Builder 2': [(1, True), (2, True), (3, True)], |
| 'Builder 3': [(1, True), (3, True)], |
| }), 2) |
| self.assertEqual(buildbot._find_green_revision({ |
| 'Builder 1': [(1, False), (2, True)], |
| 'Builder 2': [(1, True), (3, True)], |
| 'Builder 3': [(1, True), (3, True)], |
| }), None) |
| self.assertEqual(buildbot._find_green_revision({ |
| 'Builder 1': [(1, True), (3, True)], |
| 'Builder 2': [(1, False), (2, True), (3, True), (4, True)], |
| 'Builder 3': [(2, True), (4, True)], |
| }), 3) |
| self.assertEqual(buildbot._find_green_revision({ |
| 'Builder 1': [(1, True), (3, True)], |
| 'Builder 2': [(1, False), (2, True), (3, True), (4, False)], |
| 'Builder 3': [(2, True), (4, True)], |
| }), None) |
| self.assertEqual(buildbot._find_green_revision({ |
| 'Builder 1': [(1, True), (3, True)], |
| 'Builder 2': [(1, False), (2, True), (3, True), (4, False)], |
| 'Builder 3': [(2, True), (3, True), (4, True)], |
| }), 3) |
| self.assertEqual(buildbot._find_green_revision({ |
| 'Builder 1': [(1, True), (2, True)], |
| 'Builder 2': [], |
| 'Builder 3': [(1, True), (2, True)], |
| }), None) |
| self.assertEqual(buildbot._find_green_revision({ |
| 'Builder 1': [(1, True), (3, False), (5, True), (10, True), (12, False)], |
| 'Builder 2': [(1, True), (3, False), (7, True), (9, True), (12, False)], |
| 'Builder 3': [(1, True), (3, True), (7, True), (11, False), (12, True)], |
| }), 7) |
| |
| def _fetch_build(self, build_number): |
| if build_number == 5: |
| return "correct build" |
| return "wrong build" |
| |
| def _fetch_revision_to_build_map(self): |
| return {'r5': 5, 'r2': 2, 'r3': 3} |
| |
| def test_latest_cached_build(self): |
| b = Builder('builder', BuildBot()) |
| b._fetch_build = self._fetch_build |
| b._fetch_revision_to_build_map = self._fetch_revision_to_build_map |
| self.assertEqual("correct build", b.latest_cached_build()) |
| |
| def results_url(self): |
| return "some-url" |
| |
| def test_results_zip_url(self): |
| b = Build(None, 123, 123, False) |
| b.results_url = self.results_url |
| self.assertEqual("some-url.zip", b.results_zip_url()) |