blob: a7dbc0d9ff542ace8eb763907c09b255e68e90de [file] [log] [blame]
# Copyright (C) 2019 Apple 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:
# 1. Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# 2. 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.
#
# THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS 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 APPLE INC. OR ITS 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 calendar
import collections
import json
import time
from cassandra.cqlengine import columns
from cassandra.cqlengine.models import Model
from datetime import datetime
from resultsdbpy.controller.commit import Commit
from resultsdbpy.model.commit_context import CommitContext
from resultsdbpy.model.configuration_context import ClusteredByConfiguration
from resultsdbpy.model.upload_context import UploadCallbackContext
from resultsdbpy.model.test_context import Expectations
class FailureContext(UploadCallbackContext):
DEFAULT_LIMIT = 100
class TestFailuresBase(ClusteredByConfiguration):
suite = columns.Text(partition_key=True, required=True)
branch = columns.Text(partition_key=True, required=True)
tests = columns.Text(required=True)
def unpack(self):
results = json.loads(self.tests) if self.tests else {}
for test in results.keys():
results[test] = Expectations.state_ids_to_string([results[test]])
results['uuid'] = self.uuid
results['start_time'] = calendar.timegm(self.start_time.timetuple())
return results
class TestFailuresByCommit(TestFailuresBase):
__table_name__ = 'test_failures_by_commit'
uuid = columns.BigInt(primary_key=True, required=True, clustering_order='DESC')
sdk = columns.Text(primary_key=True, required=True)
start_time = columns.DateTime(primary_key=True, required=True)
class TestFailuresByStartTime(TestFailuresBase):
__table_name__ = 'test_failures_by_start_time'
start_time = columns.DateTime(primary_key=True, required=True, clustering_order='DESC')
sdk = columns.Text(primary_key=True, required=True)
uuid = columns.BigInt(primary_key=True, required=True)
class UnexpectedTestFailuresByCommit(TestFailuresBase):
__table_name__ = 'unexpected_test_failures_by_commit'
uuid = columns.BigInt(primary_key=True, required=True, clustering_order='DESC')
sdk = columns.Text(primary_key=True, required=True)
start_time = columns.DateTime(primary_key=True, required=True)
class UnexpectedTestFailuresByStartTime(TestFailuresBase):
__table_name__ = 'unexpected_test_failures_by_start_time'
start_time = columns.DateTime(primary_key=True, required=True, clustering_order='DESC')
sdk = columns.Text(primary_key=True, required=True)
uuid = columns.BigInt(primary_key=True, required=True)
def __init__(self, *args, **kwargs):
super(FailureContext, self).__init__('test-failures', *args, **kwargs)
with self:
self.cassandra.create_table(self.TestFailuresByCommit)
self.cassandra.create_table(self.TestFailuresByStartTime)
self.cassandra.create_table(self.UnexpectedTestFailuresByCommit)
self.cassandra.create_table(self.UnexpectedTestFailuresByStartTime)
def register(self, configuration, commits, suite, test_results, timestamp=None):
try:
if not isinstance(suite, str):
raise TypeError(f'Expected type {str}, got {type(suite)}')
timestamp = timestamp or time.time()
if isinstance(timestamp, datetime):
timestamp = calendar.timegm(timestamp.timetuple())
with self:
uuid = self.commit_context.uuid_for_commits(commits)
ttl = int((uuid // Commit.TIMESTAMP_TO_UUID_MULTIPLIER) + self.ttl_seconds - time.time()) if self.ttl_seconds else None
def callback(test, result, failures, unexpected):
failed_result = Expectations.string_to_state_ids(result.get('actual', ''))
expected = set(Expectations.string_to_state_ids(result.get('expected', '')))
if Expectations.STRING_TO_STATE_ID[Expectations.FAIL] in expected:
expected.add(Expectations.STRING_TO_STATE_ID[Expectations.TEXT])
expected.add(Expectations.STRING_TO_STATE_ID[Expectations.AUDIO])
expected.add(Expectations.STRING_TO_STATE_ID[Expectations.IMAGE])
unexpected_result = set(failed_result) - expected
if failed_result:
worst = min(failed_result)
if worst < Expectations.STRING_TO_STATE_ID[Expectations.WARNING]:
failures[test] = min(worst, failures.get(test, Expectations.STRING_TO_STATE_ID[Expectations.PASS]))
if unexpected_result:
worst = min(unexpected_result)
if worst < Expectations.STRING_TO_STATE_ID[Expectations.WARNING]:
unexpected[test] = min(worst, unexpected.get(test, Expectations.STRING_TO_STATE_ID[Expectations.PASS]))
with self.cassandra.batch_query_context():
for branch in self.commit_context.branch_keys_for_commits(commits):
failures = {}
unexpected = {}
Expectations.iterate_through_nested_results(
test_results.get('results'),
lambda test, result: callback(test, result, failures=failures, unexpected=unexpected),
)
for table in [self.TestFailuresByCommit, self.TestFailuresByStartTime]:
self.configuration_context.insert_row_with_configuration(
table.__table_name__, configuration=configuration, suite=suite,
branch=branch, uuid=uuid, ttl=ttl,
sdk=configuration.sdk or '?', start_time=timestamp,
tests=json.dumps(failures),
)
for table in [self.UnexpectedTestFailuresByCommit, self.UnexpectedTestFailuresByStartTime]:
self.configuration_context.insert_row_with_configuration(
table.__table_name__, configuration=configuration, suite=suite,
branch=branch, uuid=uuid, ttl=ttl,
sdk=configuration.sdk or '?', start_time=timestamp,
tests=json.dumps(unexpected),
)
except Exception as e:
return self.partial_status(e)
return self.partial_status()
def _failures(
self, all_table, unexpected_table, configurations, suite, recent=True,
branch=None, begin=None, end=None,
begin_query_time=None, end_query_time=None,
unexpected=True, collapsed=True, limit=DEFAULT_LIMIT,
):
table = unexpected_table if unexpected else all_table
if not isinstance(suite, str):
raise TypeError(f'Expected type {str} for suite, got {type(suite)}')
def get_time(time):
if isinstance(time, datetime):
return time
elif time:
return datetime.utcfromtimestamp(int(time))
return None
with self:
if collapsed:
result = set()
else:
result = {}
for configuration in configurations:
for config, values in self.configuration_context.select_from_table_with_configurations(
table.__table_name__, configurations=[configuration], recent=recent,
suite=suite, sdk=configuration.sdk, branch=branch or self.commit_context.DEFAULT_BRANCH_KEY,
uuid__gte=CommitContext.convert_to_uuid(begin),
uuid__lte=CommitContext.convert_to_uuid(end, CommitContext.timestamp_to_uuid()),
start_time__gte=get_time(begin_query_time), start_time__lte=get_time(end_query_time),
limit=limit,
).items():
if collapsed:
for value in values:
for test in value.unpack():
if test not in ['uuid', 'start_time']:
result.add(test)
else:
result.update({config: [value.unpack() for value in values]})
return result
def failures_by_commit(self, *args, **kwargs):
return self._failures(self.TestFailuresByCommit, self.UnexpectedTestFailuresByCommit, *args, **kwargs)
def failures_by_start_time(self, *args, **kwargs):
return self._failures(self.TestFailuresByStartTime, self.UnexpectedTestFailuresByStartTime, *args, **kwargs)