<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head><meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>[280869] trunk/Tools</title>
</head>
<body>

<style type="text/css"><!--
#msg dl.meta { border: 1px #006 solid; background: #369; padding: 6px; color: #fff; }
#msg dl.meta dt { float: left; width: 6em; font-weight: bold; }
#msg dt:after { content:':';}
#msg dl, #msg dt, #msg ul, #msg li, #header, #footer, #logmsg { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt;  }
#msg dl a { font-weight: bold}
#msg dl a:link    { color:#fc3; }
#msg dl a:active  { color:#ff0; }
#msg dl a:visited { color:#cc6; }
h3 { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt; font-weight: bold; }
#msg pre { overflow: auto; background: #ffc; border: 1px #fa0 solid; padding: 6px; }
#logmsg { background: #ffc; border: 1px #fa0 solid; padding: 1em 1em 0 1em; }
#logmsg p, #logmsg pre, #logmsg blockquote { margin: 0 0 1em 0; }
#logmsg p, #logmsg li, #logmsg dt, #logmsg dd { line-height: 14pt; }
#logmsg h1, #logmsg h2, #logmsg h3, #logmsg h4, #logmsg h5, #logmsg h6 { margin: .5em 0; }
#logmsg h1:first-child, #logmsg h2:first-child, #logmsg h3:first-child, #logmsg h4:first-child, #logmsg h5:first-child, #logmsg h6:first-child { margin-top: 0; }
#logmsg ul, #logmsg ol { padding: 0; list-style-position: inside; margin: 0 0 0 1em; }
#logmsg ul { text-indent: -1em; padding-left: 1em; }#logmsg ol { text-indent: -1.5em; padding-left: 1.5em; }
#logmsg > ul, #logmsg > ol { margin: 0 0 1em 0; }
#logmsg pre { background: #eee; padding: 1em; }
#logmsg blockquote { border: 1px solid #fa0; border-left-width: 10px; padding: 1em 1em 0 1em; background: white;}
#logmsg dl { margin: 0; }
#logmsg dt { font-weight: bold; }
#logmsg dd { margin: 0; padding: 0 0 0.5em 0; }
#logmsg dd:before { content:'\00bb';}
#logmsg table { border-spacing: 0px; border-collapse: collapse; border-top: 4px solid #fa0; border-bottom: 1px solid #fa0; background: #fff; }
#logmsg table th { text-align: left; font-weight: normal; padding: 0.2em 0.5em; border-top: 1px dotted #fa0; }
#logmsg table td { text-align: right; border-top: 1px dotted #fa0; padding: 0.2em 0.5em; }
#logmsg table thead th { text-align: center; border-bottom: 1px solid #fa0; }
#logmsg table th.Corner { text-align: left; }
#logmsg hr { border: none 0; border-top: 2px dashed #fa0; height: 1px; }
#header, #footer { color: #fff; background: #636; border: 1px #300 solid; padding: 6px; }
#patch { width: 100%; }
#patch h4 {font-family: verdana,arial,helvetica,sans-serif;font-size:10pt;padding:8px;background:#369;color:#fff;margin:0;}
#patch .propset h4, #patch .binary h4 {margin:0;}
#patch pre {padding:0;line-height:1.2em;margin:0;}
#patch .diff {width:100%;background:#eee;padding: 0 0 10px 0;overflow:auto;}
#patch .propset .diff, #patch .binary .diff  {padding:10px 0;}
#patch span {display:block;padding:0 10px;}
#patch .modfile, #patch .addfile, #patch .delfile, #patch .propset, #patch .binary, #patch .copfile {border:1px solid #ccc;margin:10px 0;}
#patch ins {background:#dfd;text-decoration:none;display:block;padding:0 10px;}
#patch del {background:#fdd;text-decoration:none;display:block;padding:0 10px;}
#patch .lines, .info {color:#888;background:#fff;}
--></style>
<div id="msg">
<dl class="meta">
<dt>Revision</dt> <dd><a href="http://trac.webkit.org/projects/webkit/changeset/280869">280869</a></dd>
<dt>Author</dt> <dd>jbedard@apple.com</dd>
<dt>Date</dt> <dd>2021-08-10 15:41:13 -0700 (Tue, 10 Aug 2021)</dd>
</dl>

<h3>Log Message</h3>
<pre>[resultsdbpy] Add results-summary API
https://bugs.webkit.org/show_bug.cgi?id=226894
<rdar://problem/79155181>

Reviewed by Aakash Jain.

* Scripts/libraries/resultsdbpy/resultsdbpy/__init__.py: Bump version.
* Scripts/libraries/resultsdbpy/resultsdbpy/controller/api_routes.py:
(APIRoutes.__init__): Add aggregate-results endpoint.
* Scripts/libraries/resultsdbpy/resultsdbpy/controller/commit_controller.py:
(commit_for_query): Add decorator which converts a set of arguments into a single commit.
* Scripts/libraries/resultsdbpy/resultsdbpy/controller/test_controller.py:
(TestController):
(TestController.summarize_test_results): Given a single commit and suite/test combination, compute
the liklihood of each potential result.
* Scripts/libraries/resultsdbpy/resultsdbpy/controller/test_controller_unittest.py:
(TestControllerTest.test_summarize_general): Added.
(TestControllerTest.test_summarize_specific): Added.
(TestControllerTest.test_summarize_expectations): Added.
* Scripts/libraries/resultsdbpy/resultsdbpy/model/commit_context.py:
(CommitContext.find_commits_in_range): Use ascended table if user only provides lower bound.
* Scripts/libraries/resultsdbpy/resultsdbpy/model/commit_context_unittest.py:
(CommitContextTest.test_stash_commits_before): Verify upper bound.
(CommitContextTest.test_svn_commits_before): Ditto.
(CommitContextTest.test_stash_commits_after): Verify lower bound.
(CommitContextTest.test_svn_commits_after): Ditto.
* Scripts/libraries/resultsdbpy/resultsdbpy/view/templates/documentation.html: Add aggregate-results
documentation.
* Scripts/libraries/resultsdbpy/setup.py: Bump version.</pre>

<h3>Modified Paths</h3>
<ul>
<li><a href="#trunkToolsChangeLog">trunk/Tools/ChangeLog</a></li>
<li><a href="#trunkToolsScriptslibrariesresultsdbpyresultsdbpy__init__py">trunk/Tools/Scripts/libraries/resultsdbpy/resultsdbpy/__init__.py</a></li>
<li><a href="#trunkToolsScriptslibrariesresultsdbpyresultsdbpycontrollerapi_routespy">trunk/Tools/Scripts/libraries/resultsdbpy/resultsdbpy/controller/api_routes.py</a></li>
<li><a href="#trunkToolsScriptslibrariesresultsdbpyresultsdbpycontrollercommit_controllerpy">trunk/Tools/Scripts/libraries/resultsdbpy/resultsdbpy/controller/commit_controller.py</a></li>
<li><a href="#trunkToolsScriptslibrariesresultsdbpyresultsdbpycontrollertest_controllerpy">trunk/Tools/Scripts/libraries/resultsdbpy/resultsdbpy/controller/test_controller.py</a></li>
<li><a href="#trunkToolsScriptslibrariesresultsdbpyresultsdbpycontrollertest_controller_unittestpy">trunk/Tools/Scripts/libraries/resultsdbpy/resultsdbpy/controller/test_controller_unittest.py</a></li>
<li><a href="#trunkToolsScriptslibrariesresultsdbpyresultsdbpymodelcommit_contextpy">trunk/Tools/Scripts/libraries/resultsdbpy/resultsdbpy/model/commit_context.py</a></li>
<li><a href="#trunkToolsScriptslibrariesresultsdbpyresultsdbpymodelcommit_context_unittestpy">trunk/Tools/Scripts/libraries/resultsdbpy/resultsdbpy/model/commit_context_unittest.py</a></li>
<li><a href="#trunkToolsScriptslibrariesresultsdbpyresultsdbpyviewtemplatesdocumentationhtml">trunk/Tools/Scripts/libraries/resultsdbpy/resultsdbpy/view/templates/documentation.html</a></li>
<li><a href="#trunkToolsScriptslibrariesresultsdbpysetuppy">trunk/Tools/Scripts/libraries/resultsdbpy/setup.py</a></li>
</ul>

</div>
<div id="patch">
<h3>Diff</h3>
<a id="trunkToolsChangeLog"></a>
<div class="modfile"><h4>Modified: trunk/Tools/ChangeLog (280868 => 280869)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Tools/ChangeLog    2021-08-10 22:23:17 UTC (rev 280868)
+++ trunk/Tools/ChangeLog       2021-08-10 22:41:13 UTC (rev 280869)
</span><span class="lines">@@ -1,3 +1,35 @@
</span><ins>+2021-08-09  Jonathan Bedard  <jbedard@apple.com>
+
+        [resultsdbpy] Add results-summary API
+        https://bugs.webkit.org/show_bug.cgi?id=226894
+        <rdar://problem/79155181>
+
+        Reviewed by Aakash Jain.
+
+        * Scripts/libraries/resultsdbpy/resultsdbpy/__init__.py: Bump version.
+        * Scripts/libraries/resultsdbpy/resultsdbpy/controller/api_routes.py:
+        (APIRoutes.__init__): Add aggregate-results endpoint.
+        * Scripts/libraries/resultsdbpy/resultsdbpy/controller/commit_controller.py:
+        (commit_for_query): Add decorator which converts a set of arguments into a single commit.
+        * Scripts/libraries/resultsdbpy/resultsdbpy/controller/test_controller.py:
+        (TestController):
+        (TestController.summarize_test_results): Given a single commit and suite/test combination, compute
+        the liklihood of each potential result.
+        * Scripts/libraries/resultsdbpy/resultsdbpy/controller/test_controller_unittest.py:
+        (TestControllerTest.test_summarize_general): Added.
+        (TestControllerTest.test_summarize_specific): Added.
+        (TestControllerTest.test_summarize_expectations): Added.
+        * Scripts/libraries/resultsdbpy/resultsdbpy/model/commit_context.py:
+        (CommitContext.find_commits_in_range): Use ascended table if user only provides lower bound.
+        * Scripts/libraries/resultsdbpy/resultsdbpy/model/commit_context_unittest.py:
+        (CommitContextTest.test_stash_commits_before): Verify upper bound.
+        (CommitContextTest.test_svn_commits_before): Ditto.
+        (CommitContextTest.test_stash_commits_after): Verify lower bound.
+        (CommitContextTest.test_svn_commits_after): Ditto.
+        * Scripts/libraries/resultsdbpy/resultsdbpy/view/templates/documentation.html: Add aggregate-results
+        documentation.
+        * Scripts/libraries/resultsdbpy/setup.py: Bump version.
+
</ins><span class="cx"> 2021-08-10  Tim Horton  <timothy_horton@apple.com>
</span><span class="cx"> 
</span><span class="cx">         macCatalyst: Flexible viewport tests that dump the window size fail because it doesn't match iPad
</span></span></pre></div>
<a id="trunkToolsScriptslibrariesresultsdbpyresultsdbpy__init__py"></a>
<div class="modfile"><h4>Modified: trunk/Tools/Scripts/libraries/resultsdbpy/resultsdbpy/__init__.py (280868 => 280869)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Tools/Scripts/libraries/resultsdbpy/resultsdbpy/__init__.py        2021-08-10 22:23:17 UTC (rev 280868)
+++ trunk/Tools/Scripts/libraries/resultsdbpy/resultsdbpy/__init__.py   2021-08-10 22:41:13 UTC (rev 280869)
</span><span class="lines">@@ -44,7 +44,7 @@
</span><span class="cx">         "Please install webkitcorepy with `pip install webkitcorepy --extra-index-url <package index URL>`"
</span><span class="cx">     )
</span><span class="cx"> 
</span><del>-version = Version(3, 0, 2)
</del><ins>+version = Version(3, 1, 0)
</ins><span class="cx"> 
</span><span class="cx"> import webkitflaskpy
</span><span class="cx"> 
</span></span></pre></div>
<a id="trunkToolsScriptslibrariesresultsdbpyresultsdbpycontrollerapi_routespy"></a>
<div class="modfile"><h4>Modified: trunk/Tools/Scripts/libraries/resultsdbpy/resultsdbpy/controller/api_routes.py (280868 => 280869)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Tools/Scripts/libraries/resultsdbpy/resultsdbpy/controller/api_routes.py   2021-08-10 22:23:17 UTC (rev 280868)
+++ trunk/Tools/Scripts/libraries/resultsdbpy/resultsdbpy/controller/api_routes.py      2021-08-10 22:41:13 UTC (rev 280869)
</span><span class="lines">@@ -70,6 +70,7 @@
</span><span class="cx"> 
</span><span class="cx">         self.add_url_rule('/results/<path:suite>', 'suite-results', self.suite_controller.find_run_results, methods=('GET',))
</span><span class="cx">         self.add_url_rule('/results/<path:suite>/<path:test>', 'test-results', self.test_controller.find_test_result, methods=('GET',))
</span><ins>+        self.add_url_rule('/results-summary/<path:suite>/<path:test>', 'test-aggregate-results', self.test_controller.summarize_test_results, methods=('GET',))
</ins><span class="cx"> 
</span><span class="cx">         self.add_url_rule('/failures/<path:suite>', 'suite-failures', self.failure_controller.failures, methods=('GET',))
</span><span class="cx"> 
</span></span></pre></div>
<a id="trunkToolsScriptslibrariesresultsdbpyresultsdbpycontrollercommit_controllerpy"></a>
<div class="modfile"><h4>Modified: trunk/Tools/Scripts/libraries/resultsdbpy/resultsdbpy/controller/commit_controller.py (280868 => 280869)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Tools/Scripts/libraries/resultsdbpy/resultsdbpy/controller/commit_controller.py    2021-08-10 22:23:17 UTC (rev 280868)
+++ trunk/Tools/Scripts/libraries/resultsdbpy/resultsdbpy/controller/commit_controller.py       2021-08-10 22:41:13 UTC (rev 280869)
</span><span class="lines">@@ -125,6 +125,26 @@
</span><span class="cx">     return decorator
</span><span class="cx"> 
</span><span class="cx"> 
</span><ins>+def commit_for_query():
+    def decorator(method):
+        def real_method(obj, repository_id=None, branch=None, id=None, ref=None, uuid=None, timestamp=None, **kwargs):
+            # We're making an assumption that the class using this decorator actually has a commit_context, if it does not,
+            # this decorator will fail spectacularly
+            with obj.commit_context:
+                if not branch:
+                    branch = [None]
+                return method(obj, branch=branch, commit=_find_comparison(
+                    obj.commit_context, repository_id=repository_id, branch=branch,
+                    ref=ref or id,
+                    uuid=uuid, timestamp=timestamp, priority=max,
+                ), **kwargs)
+
+        real_method.__name__ = method.__name__
+        return real_method
+
+    return decorator
+
+
</ins><span class="cx"> class HasCommitContext(object):
</span><span class="cx">     def __init__(self, commit_context):
</span><span class="cx">         self.commit_context = commit_context
</span></span></pre></div>
<a id="trunkToolsScriptslibrariesresultsdbpyresultsdbpycontrollertest_controllerpy"></a>
<div class="modfile"><h4>Modified: trunk/Tools/Scripts/libraries/resultsdbpy/resultsdbpy/controller/test_controller.py (280868 => 280869)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Tools/Scripts/libraries/resultsdbpy/resultsdbpy/controller/test_controller.py      2021-08-10 22:23:17 UTC (rev 280868)
+++ trunk/Tools/Scripts/libraries/resultsdbpy/resultsdbpy/controller/test_controller.py 2021-08-10 22:41:13 UTC (rev 280869)
</span><span class="lines">@@ -21,10 +21,12 @@
</span><span class="cx"> # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
</span><span class="cx"> 
</span><span class="cx"> from flask import abort, jsonify
</span><del>-from resultsdbpy.controller.commit_controller import uuid_range_for_query, HasCommitContext
</del><ins>+from heapq import merge
+from resultsdbpy.controller.commit_controller import uuid_range_for_query, commit_for_query, HasCommitContext
</ins><span class="cx"> from resultsdbpy.controller.configuration import Configuration
</span><span class="cx"> from resultsdbpy.controller.configuration_controller import configuration_for_query
</span><span class="cx"> from resultsdbpy.controller.suite_controller import time_range_for_query
</span><ins>+from resultsdbpy.model.test_context import Expectations
</ins><span class="cx"> from webkitflaskpy.util import AssertRequest, query_as_kwargs, limit_for_query, boolean_query
</span><span class="cx"> 
</span><span class="cx"> 
</span><span class="lines">@@ -102,3 +104,107 @@
</span><span class="cx">                     results=sorted(results, key=sort_function),
</span><span class="cx">                 ))
</span><span class="cx">             return jsonify(response)
</span><ins>+
+    @query_as_kwargs()
+    @commit_for_query()
+    @limit_for_query(100)
+    @configuration_for_query()
+    def summarize_test_results(
+        self, suite=None, test=None,
+        configurations=None, recent=None,
+        branch=None, commit=None,
+        limit=None, include_expectations=None, **kwargs
+    ):
+        AssertRequest.is_type(['GET'])
+        AssertRequest.query_kwargs_empty(**kwargs)
+
+        recent = boolean_query(*recent)[0] if recent else True
+        include_expectations = boolean_query(*include_expectations)[0] if include_expectations else False
+
+        if not suite:
+            abort(400, description='No suite specified')
+        if not test:
+            abort(400, description='No test specified')
+
+        limit += 1
+        before_commits = []
+        after_commits = []
+        with self.commit_context:
+            for repo_id in self.commit_context.repositories.keys():
+                if commit:
+                    before_commits = sorted(list(reversed(self.commit_context.find_commits_in_range(
+                        repository_id=repo_id,
+                        end=commit, branch=branch[0], limit=limit,
+                    ))) + before_commits)
+                    after_commits = sorted(list(reversed(self.commit_context.find_commits_in_range(
+                        repository_id=repo_id,
+                        begin=commit, branch=branch[0], limit=limit,
+                    ))) + after_commits)
+                else:
+                    before_commits = list(merge(
+                        self.commit_context.find_commits_in_range(repository_id=repo_id, branch=branch[0], limit=limit),
+                        before_commits,
+                    ))
+                    before_commits.reverse()
+                after_commits.reverse()
+
+        before_commits = sorted(before_commits)
+        after_commits = sorted(after_commits)
+
+        before_commits = before_commits[-limit:]
+        after_commits = after_commits[:limit]
+        if before_commits and after_commits and before_commits[-1] == after_commits[0]:
+            del before_commits[-1]
+
+        if not before_commits and not after_commits:
+            return abort(400, description='No commits in specified range')
+
+        # Use the linear distance from the specified commit
+        scale_for_uuid = {}
+        count = limit - 1
+        for c in reversed(before_commits):
+            scale_for_uuid[c.uuid] = count
+            count -= 1
+        count = limit
+        for c in after_commits:
+            scale_for_uuid[c.uuid] = count
+            count -= 1
+
+        # A direct match to the provided commit matters most
+        if commit:
+            scale_for_uuid[commit.uuid] = limit * 2
+
+        response = {}
+        for value in Expectations.STATE_ID_TO_STRING.values():
+            response[value.lower()] = {} if include_expectations else 0
+
+        with self.test_context:
+            for config, results in self.test_context.find_by_commit(
+                suite=suite, test=test,
+                configurations=configurations, recent=recent,
+                branch=branch[0],
+                limit=limit * 4,
+                begin=(before_commits or after_commits)[0], end=(after_commits or before_commits)[-1],
+            ).items():
+                for result in results:
+                    scale = scale_for_uuid.get(int(result['uuid']), 0)
+                    if not scale:
+                        continue
+                    tag = result.get('actual', 'PASS').lower()
+                    if include_expectations:
+                        expected = 'expected' if not result.get('expected') or result['actual'] == result['expected'] else 'unexpected'
+                        response[tag][expected] = response[tag].get(expected, 0) + scale
+                    else:
+                        response[tag] = response[tag] + scale
+
+        aggregate = sum([sum(value.values()) if include_expectations else value for value in response.values()])
+        if not aggregate:
+            return abort(400, description='No results for specified test and configuration in provided commit range')
+        for key in response.keys():
+            if include_expectations:
+                for expectation in response[key].keys():
+                    response[key][expectation] = 100 * response[key][expectation] // (aggregate or 1)
+            else:
+                response[key] = 100 * response[key] // (aggregate or 1)
+
+        return jsonify(response)
</ins></span></pre></div>
<a id="trunkToolsScriptslibrariesresultsdbpyresultsdbpycontrollertest_controller_unittestpy"></a>
<div class="modfile"><h4>Modified: trunk/Tools/Scripts/libraries/resultsdbpy/resultsdbpy/controller/test_controller_unittest.py (280868 => 280869)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Tools/Scripts/libraries/resultsdbpy/resultsdbpy/controller/test_controller_unittest.py     2021-08-10 22:23:17 UTC (rev 280868)
+++ trunk/Tools/Scripts/libraries/resultsdbpy/resultsdbpy/controller/test_controller_unittest.py        2021-08-10 22:41:13 UTC (rev 280869)
</span><span class="lines">@@ -136,3 +136,59 @@
</span><span class="cx">         response = client.get(self.URL + f'/api/results/layout-tests/fast/encoding/css-link-charset.html?platform=iOS&style=Debug&recent=False&after_time={time.time() + 1}')
</span><span class="cx">         self.assertEqual(response.status_code, 200)
</span><span class="cx">         self.assertEqual(len(response.json()), 0)
</span><ins>+
+    @WaitForDockerTestCase.mock_if_no_docker(mock_redis=FakeStrictRedis, mock_cassandra=MockCassandraContext)
+    @FlaskTestCase.run_with_webserver()
+    def test_summarize_general(self, client, **kwargs):
+        self.maxDiff = None
+        response = client.get(
+            self.URL + f'/api/results-summary/layout-tests/fast/encoding/css-link-charset.html?limit=2')
+        self.assertEqual(response.status_code, 200)
+        self.assertDictEqual(response.json(), {
+            'audio': 0,
+            'crash': 0,
+            'error': 0,
+            'fail': 0,
+            'image': 0,
+            'pass': 100,
+            'text': 0,
+            'timeout': 0,
+            'warning': 0,
+        })
+
+    @WaitForDockerTestCase.mock_if_no_docker(mock_redis=FakeStrictRedis, mock_cassandra=MockCassandraContext)
+    @FlaskTestCase.run_with_webserver()
+    def test_summarize_specific(self, client, **kwargs):
+        self.maxDiff = None
+        response = client.get(self.URL + f'/api/results-summary/layout-tests/fast/encoding/css-link-charset.html?ref=1abe25b443e9&limit=2')
+        self.assertEqual(response.status_code, 200)
+        self.assertDictEqual(response.json(), {
+            'audio': 0,
+            'crash': 0,
+            'error': 0,
+            'fail': 0,
+            'image': 0,
+            'pass': 100,
+            'text': 0,
+            'timeout': 0,
+            'warning': 0,
+        })
+
+    @WaitForDockerTestCase.mock_if_no_docker(mock_redis=FakeStrictRedis, mock_cassandra=MockCassandraContext)
+    @FlaskTestCase.run_with_webserver()
+    def test_summarize_expectations(self, client, **kwargs):
+        self.maxDiff = None
+        response = client.get(
+            self.URL + f'/api/results-summary/layout-tests/fast/encoding/css-link-charset.html?limit=2&include_expectations=True')
+        self.assertEqual(response.status_code, 200)
+        self.assertDictEqual(response.json(), {
+            'audio': {},
+            'crash': {},
+            'error': {},
+            'fail': {},
+            'image': {},
+            'pass': dict(expected=100),
+            'text': {},
+            'timeout': {},
+            'warning': {},
+        })
</ins></span></pre></div>
<a id="trunkToolsScriptslibrariesresultsdbpyresultsdbpymodelcommit_contextpy"></a>
<div class="modfile"><h4>Modified: trunk/Tools/Scripts/libraries/resultsdbpy/resultsdbpy/model/commit_context.py (280868 => 280869)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Tools/Scripts/libraries/resultsdbpy/resultsdbpy/model/commit_context.py    2021-08-10 22:23:17 UTC (rev 280868)
+++ trunk/Tools/Scripts/libraries/resultsdbpy/resultsdbpy/model/commit_context.py       2021-08-10 22:41:13 UTC (rev 280869)
</span><span class="lines">@@ -213,16 +213,20 @@
</span><span class="cx">         if branch is None:
</span><span class="cx">             branch = self.repositories[repository_id].default_branch
</span><span class="cx"> 
</span><ins>+        use_ascending = begin and not end
</ins><span class="cx">         begin = self.convert_to_uuid(begin)
</span><span class="cx">         end = self.convert_to_uuid(end, self.timestamp_to_uuid())
</span><span class="cx"> 
</span><span class="cx">         with self:
</span><del>-            return [model.to_commit() for model in self.cassandra.select_from_table(
-                self.CommitByUuidDescending.__table_name__, limit=limit,
-                repository_id=repository_id, branch=branch,
</del><ins>+            result = [model.to_commit() for model in self.cassandra.select_from_table(
+                self.CommitByUuidAscending.__table_name__ if use_ascending else self.CommitByUuidDescending.__table_name__,
+                limit=limit, repository_id=repository_id, branch=branch,
</ins><span class="cx">                 uuid__gte=begin,
</span><span class="cx">                 uuid__lte=end,
</span><span class="cx">             )]
</span><ins>+            if use_ascending:
+                result.reverse()
+            return result
</ins><span class="cx"> 
</span><span class="cx">     def _adjacent_commit(self, commit, ascending=True):
</span><span class="cx">         if not isinstance(commit, Commit):
</span></span></pre></div>
<a id="trunkToolsScriptslibrariesresultsdbpyresultsdbpymodelcommit_context_unittestpy"></a>
<div class="modfile"><h4>Modified: trunk/Tools/Scripts/libraries/resultsdbpy/resultsdbpy/model/commit_context_unittest.py (280868 => 280869)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Tools/Scripts/libraries/resultsdbpy/resultsdbpy/model/commit_context_unittest.py   2021-08-10 22:23:17 UTC (rev 280868)
+++ trunk/Tools/Scripts/libraries/resultsdbpy/resultsdbpy/model/commit_context_unittest.py      2021-08-10 22:41:13 UTC (rev 280869)
</span><span class="lines">@@ -180,6 +180,58 @@
</span><span class="cx">             self.assertEqual(commits, self.database.find_commits_in_range(repository_id='webkit', branch='main', begin=commits[-1], end=commits[0]))
</span><span class="cx"> 
</span><span class="cx">     @WaitForDockerTestCase.mock_if_no_docker(mock_redis=FakeStrictRedis, mock_cassandra=MockCassandraContext)
</span><ins>+    def test_stash_commits_before(self, redis=StrictRedis, cassandra=CassandraContext):
+        with MockModelFactory.safari(), MockModelFactory.webkit():
+            self.init_database(redis=redis, cassandra=cassandra)
+            self.add_all_commits_to_database()
+
+            commits = [
+                self.stash_repository.commit(ref='1abe25b443e9'),
+                self.stash_repository.commit(ref='fff83bb2d917'),
+                self.stash_repository.commit(ref='9b8311f25a77'),
+            ]
+            self.assertEqual(commits, self.database.find_commits_in_range(repository_id='safari', branch='main', end=commits[0], limit=3))
+
+    @WaitForDockerTestCase.mock_if_no_docker(mock_redis=FakeStrictRedis, mock_cassandra=MockCassandraContext)
+    def test_svn_commits_before(self, redis=StrictRedis, cassandra=CassandraContext):
+        with MockModelFactory.safari(), MockModelFactory.webkit():
+            self.init_database(redis=redis, cassandra=cassandra)
+            self.add_all_commits_to_database()
+
+            commits = [
+                self.svn_repository.commit(ref=6),
+                self.svn_repository.commit(ref=4),
+                self.svn_repository.commit(ref=2),
+            ]
+            self.assertEqual(commits, self.database.find_commits_in_range(repository_id='webkit', branch='main', end=commits[0], limit=3))
+
+    @WaitForDockerTestCase.mock_if_no_docker(mock_redis=FakeStrictRedis, mock_cassandra=MockCassandraContext)
+    def test_stash_commits_after(self, redis=StrictRedis, cassandra=CassandraContext):
+        with MockModelFactory.safari(), MockModelFactory.webkit():
+            self.init_database(redis=redis, cassandra=cassandra)
+            self.add_all_commits_to_database()
+
+            commits = [
+                self.stash_repository.commit(ref='1abe25b443e9'),
+                self.stash_repository.commit(ref='fff83bb2d917'),
+                self.stash_repository.commit(ref='9b8311f25a77'),
+            ]
+            self.assertEqual(commits, self.database.find_commits_in_range(repository_id='safari', branch='main', begin=commits[-1], limit=3))
+
+    @WaitForDockerTestCase.mock_if_no_docker(mock_redis=FakeStrictRedis, mock_cassandra=MockCassandraContext)
+    def test_svn_commits_after(self, redis=StrictRedis, cassandra=CassandraContext):
+        with MockModelFactory.safari(), MockModelFactory.webkit():
+            self.init_database(redis=redis, cassandra=cassandra)
+            self.add_all_commits_to_database()
+
+            commits = [
+                self.svn_repository.commit(ref=6),
+                self.svn_repository.commit(ref=4),
+                self.svn_repository.commit(ref=2),
+            ]
+            self.assertEqual(commits, self.database.find_commits_in_range(repository_id='webkit', branch='main', begin=commits[-1], limit=3))
+
+    @WaitForDockerTestCase.mock_if_no_docker(mock_redis=FakeStrictRedis, mock_cassandra=MockCassandraContext)
</ins><span class="cx">     def test_commit_from_stash_repo(self, redis=StrictRedis, cassandra=CassandraContext):
</span><span class="cx">         with MockModelFactory.safari(), MockModelFactory.webkit():
</span><span class="cx">             self.init_database(redis=redis, cassandra=cassandra)
</span></span></pre></div>
<a id="trunkToolsScriptslibrariesresultsdbpyresultsdbpyviewtemplatesdocumentationhtml"></a>
<div class="modfile"><h4>Modified: trunk/Tools/Scripts/libraries/resultsdbpy/resultsdbpy/view/templates/documentation.html (280868 => 280869)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Tools/Scripts/libraries/resultsdbpy/resultsdbpy/view/templates/documentation.html  2021-08-10 22:23:17 UTC (rev 280868)
+++ trunk/Tools/Scripts/libraries/resultsdbpy/resultsdbpy/view/templates/documentation.html     2021-08-10 22:41:13 UTC (rev 280869)
</span><span class="lines">@@ -56,10 +56,9 @@
</span><span class="cx">                     ${queries.map((query) => {
</span><span class="cx">                         return `<div class="badge">
</span><span class="cx">                                 <div class="text block">
</span><del>-                                    <a class="text tiny" href="#Query-Parameters-${query}">${query}</a>
</del><ins>+                                    <a class="text tiny" href="#Query-Parameters-${query.replace(/\s/g, '-')}">${query}</a>
</ins><span class="cx">                                 </div>
</span><span class="cx">                             </div>`;
</span><del>-                        //return `<button class="button" onclick="window.location.href = '/documentation#Query-Parameters-${query}';">${query}</button>`
</del><span class="cx">                     }).join('')}
</span><span class="cx">                 </div>
</span><span class="cx">             </div>
</span><span class="lines">@@ -359,6 +358,20 @@
</span><span class="cx">                     `where &ltconfiguration-object-a&gt and &ltconfiguration-object-b&gt are both ${localLink(['Query Parameters', 'Configuration'], 'configuration objects')}' and &ltrun-a1&gt, &ltrun-a2&gt, &ltrun-b1&gt and &ltrun-b2&gt are all the afformentioned single test result dictionary.`,
</span><span class="cx">                 ],
</span><span class="cx">             ),
</span><ins>+            documentEndpoint(
+                '/api/results-summary/&ltsuite&gt/&lttest&gt',
+                ['GET'],
+                ['Branch', 'Configuration', 'Include Expectations', 'Limit', 'Repository', 'Ref', 'UUID'],
+                [
+                    `Compute the combined results of a given test by aggregating results from a set of runs surrounding a specific commit:`,
+                    codeBlock('{\n' +
+                    '    "pass": 80,\n' +
+                    '    "fail": 15,\n' +
+                    '    "crash": 5,\n' +
+                    '}'),
+                    `These results are always the weighted aggregation of results for the provided configurations, with weights to be understood as percent liklihood a test will have a certain result on a given revision.`,
+                ],
+            ),
</ins><span class="cx">         ], 'Failure Analysis': [
</span><span class="cx">             `Results databases provide a few APIs to assist in the investigation of test failures. These analysis endpoints aggregate data from multiple test runs for consumption by both humans and automated systems.`,
</span><span class="cx">             documentEndpoint(
</span><span class="lines">@@ -508,6 +521,10 @@
</span><span class="cx">             codeBlock('recent=False'),
</span><span class="cx">             `to your query. The downside of searching all configurations is that the default behavior of searching all recent configurations if no query parameters are provided is disabled because the results database cannot search by an unbounded number of configurations.`,
</span><span class="cx">         ],
</span><ins>+        'Include Expectations': [
+            `Some endpoints return different results if the caller requests expectations be taken into consideration. By default, this flag is disabled, but may be enabled on supporting enpoints with this query:`,
+            codeBlock('include_expectations=False'),
+        ],
</ins><span class="cx">         'Limit': [
</span><span class="cx">             `The underlying architecture of the results database does not allow unlimited query sizes. Most endpoints accept a limit query which looks like this:`,
</span><span class="cx">             codeBlock('limit=150'),
</span></span></pre></div>
<a id="trunkToolsScriptslibrariesresultsdbpysetuppy"></a>
<div class="modfile"><h4>Modified: trunk/Tools/Scripts/libraries/resultsdbpy/setup.py (280868 => 280869)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Tools/Scripts/libraries/resultsdbpy/setup.py       2021-08-10 22:23:17 UTC (rev 280868)
+++ trunk/Tools/Scripts/libraries/resultsdbpy/setup.py  2021-08-10 22:41:13 UTC (rev 280869)
</span><span class="lines">@@ -30,7 +30,7 @@
</span><span class="cx"> 
</span><span class="cx"> setup(
</span><span class="cx">     name='resultsdbpy',
</span><del>-    version='3.0.2',
</del><ins>+    version='3.1.0',
</ins><span class="cx">     description='Library for visualizing, processing and storing test results.',
</span><span class="cx">     long_description=readme(),
</span><span class="cx">     classifiers=[
</span></span></pre>
</div>
</div>

</body>
</html>