blob: 617bca1f15ae51f57418d05644c25558ef20984b [file] [log] [blame]
#!/usr/bin/env vpython3
# Copyright 2020 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import base64
import json
import mock
import os
import requests
import sys
import unittest
import result_sink_util
import test_runner
THIS_DIR = os.path.abspath(os.path.dirname(__file__))
CHROMIUM_SRC_DIR = os.path.abspath(os.path.join(THIS_DIR, '../../../..'))
sys.path.append(
os.path.abspath(os.path.join(CHROMIUM_SRC_DIR, 'build/util/lib/proto')))
import measures
import exception_recorder
from google.protobuf import json_format
from google.protobuf import any_pb2
SINK_ADDRESS = 'sink/address'
SINK_POST_URL = 'http://%s/prpc/luci.resultsink.v1.Sink/ReportTestResults' % SINK_ADDRESS
UPATE_POST_URL = 'http://%s/prpc/luci.resultsink.v1.Sink/UpdateInvocation' % SINK_ADDRESS
AUTH_TOKEN = 'some_sink_token'
LUCI_CONTEXT_FILE_DATA = """
{
"result_sink": {
"address": "%s",
"auth_token": "%s"
}
}
""" % (SINK_ADDRESS, AUTH_TOKEN)
HEADERS = {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Authorization': 'ResultSink %s' % AUTH_TOKEN
}
CRASH_TEST_LOG = """
Exception Reason:
App crashed and disconnected.
Recovery Suggestion:
"""
_TEST_ID = 'TestCase/testSomething'
_TEST_CLASS = 'TestCase'
_TEST_NAME = 'testSomething'
class UnitTest(unittest.TestCase):
def test_compose_test_result(self):
"""Tests compose_test_result function."""
# Test a test result without log_path.
test_result = result_sink_util._compose_test_result(_TEST_ID, 'PASS', True)
expected = {
'testId': _TEST_ID,
'status': 'PASS',
'expected': True,
'tags': [],
'testIdStructured': {
'caseNameComponents': [_TEST_NAME],
'coarseName': None,
'fineName': _TEST_CLASS
},
'testMetadata': {
'name': _TEST_ID,
'location': None,
},
}
self.assertEqual(test_result, expected)
short_log = 'Some logs.'
# Tests a test result with log_path.
test_result = result_sink_util._compose_test_result(
_TEST_ID,
'PASS',
True,
test_log=short_log,
duration=1233,
file_artifacts={'name': '/path/to/name'})
expected = {
'testId': _TEST_ID,
'status': 'PASS',
'expected': True,
'summaryHtml': '<text-artifact artifact-id="Test Log" />',
'artifacts': {
'Test Log': {
'contents':
base64.b64encode(short_log.encode('utf-8')).decode('utf-8')
},
'name': {
'filePath': '/path/to/name'
},
},
'duration': '1.233000000s',
'tags': [],
'testIdStructured': {
'caseNameComponents': [_TEST_NAME],
'coarseName': None,
'fineName': _TEST_CLASS
},
'testMetadata': {
'name': _TEST_ID,
'location': None,
},
}
self.assertEqual(test_result, expected)
def test_parsing_crash_message(self):
"""Tests parsing crash message from test log and setting it as the
failure reason"""
test_result = result_sink_util._compose_test_result(
_TEST_ID, 'FAIL', False, test_log=CRASH_TEST_LOG)
expected = {
'testId': _TEST_ID,
'status': 'FAIL',
'expected': False,
'summaryHtml': '<text-artifact artifact-id="Test Log" />',
'tags': [],
'failureReason': {
'primaryErrorMessage': 'App crashed and disconnected.'
},
'artifacts': {
'Test Log': {
'contents':
base64.b64encode(CRASH_TEST_LOG.encode('utf-8')
).decode('utf-8')
},
},
'testIdStructured': {
'caseNameComponents': [_TEST_NAME],
'coarseName': None,
'fineName': _TEST_CLASS
},
'testMetadata': {
'name': _TEST_ID,
'location': None,
},
}
self.assertEqual(test_result, expected)
def test_long_test_log(self):
"""Tests long test log is reported as expected."""
len_32_str = 'This is a string in length of 32'
self.assertEqual(len(len_32_str), 32)
len_4128_str = (4 * 32 + 1) * len_32_str
self.assertEqual(len(len_4128_str), 4128)
expected = {
'testId': _TEST_ID,
'status': 'PASS',
'expected': True,
'summaryHtml': '<text-artifact artifact-id="Test Log" />',
'artifacts': {
'Test Log': {
'contents':
base64.b64encode(len_4128_str.encode('utf-8')
).decode('utf-8')
},
},
'tags': [],
'testIdStructured': {
'caseNameComponents': [_TEST_NAME],
'coarseName': None,
'fineName': _TEST_CLASS
},
'testMetadata': {
'name': _TEST_ID,
'location': None,
},
}
test_result = result_sink_util._compose_test_result(
_TEST_ID, 'PASS', True, test_log=len_4128_str)
self.assertEqual(test_result, expected)
def test_compose_test_result_assertions(self):
"""Tests invalid status is rejected"""
with self.assertRaises(AssertionError):
test_result = result_sink_util._compose_test_result(
_TEST_ID, 'SOME_INVALID_STATUS', True)
with self.assertRaises(AssertionError):
test_result = result_sink_util._compose_test_result(
_TEST_ID, 'PASS', True, tags=('a', 'b'))
with self.assertRaises(AssertionError):
test_result = result_sink_util._compose_test_result(
_TEST_ID, 'PASS', True, tags=[('a', 'b', 'c'), ('d', 'e')])
with self.assertRaises(AssertionError):
test_result = result_sink_util._compose_test_result(
_TEST_ID, 'PASS', True, tags=[('a', 'b'), ('c', 3)])
def test_composed_with_tags(self):
"""Tests tags is in correct format."""
expected = {
'testId': _TEST_ID,
'status': 'SKIP',
'expected': True,
'tags': [{
'key': 'disabled_test',
'value': 'true',
}],
'testIdStructured': {
'caseNameComponents': [_TEST_NAME],
'coarseName': None,
'fineName': _TEST_CLASS
},
'testMetadata': {
'name': _TEST_ID,
'location': None,
},
}
test_result = result_sink_util._compose_test_result(
_TEST_ID, 'SKIP', True, tags=[('disabled_test', 'true')])
self.assertEqual(test_result, expected)
def test_composed_with_location(self):
"""Tests with test locations"""
test_loc = {'repo': 'https://test', 'fileName': '//test.cc'}
expected = {
'testId': _TEST_ID,
'status': 'SKIP',
'expected': True,
'tags': [{
'key': 'disabled_test',
'value': 'true',
}],
'testIdStructured': {
'caseNameComponents': [_TEST_NAME],
'coarseName': None,
'fineName': _TEST_CLASS
},
'testMetadata': {
'name': _TEST_ID,
'location': test_loc,
},
}
test_result = result_sink_util._compose_test_result(
_TEST_ID,
'SKIP',
True,
test_loc=test_loc,
tags=[('disabled_test', 'true')])
self.assertEqual(test_result, expected)
def test_get_struct_test_dict(self):
result_dict = result_sink_util._get_struct_test_dict('myclass/testname')
self.assertIsNone(result_dict['coarseName'], None)
self.assertEqual(result_dict['fineName'], 'myclass')
self.assertEqual(result_dict['caseNameComponents'], ['testname'])
# gtest expected format:
# infra/go/src/infra/tools/result_adapter/gtest.go
result_dict = result_sink_util._get_struct_test_dict(
'myclass/param.testname')
self.assertIsNone(result_dict['coarseName'], None)
self.assertEqual(result_dict['fineName'], 'myclass')
self.assertEqual(result_dict['caseNameComponents'], ['testname/param'])
@mock.patch.object(requests.Session, 'post')
@mock.patch('%s.open' % 'result_sink_util',
mock.mock_open(read_data=LUCI_CONTEXT_FILE_DATA))
@mock.patch('os.environ.get', return_value='filename')
def test_post_test_result(self, mock_open_file, mock_session_post):
test_result = {
'testId': _TEST_ID,
'status': 'SKIP',
'expected': True,
'tags': [{
'key': 'disabled_test',
'value': 'true',
}],
'testMetadata': {
'name': _TEST_ID,
'location': None,
},
}
client = result_sink_util.ResultSinkClient()
client._post_test_result(test_result)
mock_session_post.assert_called_with(
url=SINK_POST_URL,
headers=HEADERS,
data=json.dumps({'testResults': [test_result]}))
@mock.patch.object(requests.Session, 'post')
@mock.patch('%s.open' % 'result_sink_util',
mock.mock_open(read_data=LUCI_CONTEXT_FILE_DATA))
@mock.patch('os.environ.get', return_value='filename')
@mock.patch('exception_recorder._record_time')
def test_post_extended_properties(self, _, mock_open_file, mock_session_post):
test_exception = test_runner.XcodeVersionNotFoundError("15abcd")
exception_recorder.register(test_exception)
count = measures.count('test_count')
count.record()
count.record()
inv_data = json.dumps(
{
"invocation": {
"extendedProperties": {
"exception_occurrences": {
"@type": "type.googleapis.com/build.util.lib.proto.ExceptionOccurrences",
"datapoints": [
{
"name": "test_runner.XcodeVersionNotFoundError",
"stacktrace": [
f"test_runner.XcodeVersionNotFoundError: Xcode version not found: 15abcd\n"
]
}
]
},
"test_script_metrics": {
"@type": "type.googleapis.com/build.util.lib.proto.TestScriptMetrics",
"metrics": [
{
"name": "test_count",
"value": 2.0
}
]
}
}
},
"updateMask": "extendedProperties.exceptionOccurrences,extendedProperties.testScriptMetrics",
},
sort_keys=True)
client = result_sink_util.ResultSinkClient()
client.post_extended_properties()
mock_session_post.assert_called_with(
url=UPATE_POST_URL, headers=HEADERS, data=inv_data)
@mock.patch('%s.open' % 'result_sink_util',
mock.mock_open(read_data=LUCI_CONTEXT_FILE_DATA))
@mock.patch('os.environ.get', return_value='filename')
@mock.patch(
'result_sink_util.ResultSinkClient._post_extended_properties',
side_effect=Exception())
def test_post_extended_properties_retries(self, mock_post_ext_props, _):
count = measures.count('test_count')
count.record()
client = result_sink_util.ResultSinkClient()
client.post_extended_properties()
self.assertEqual(mock_post_ext_props.call_count, 2)
@mock.patch.object(requests.Session, 'close')
@mock.patch.object(requests.Session, 'post')
@mock.patch('%s.open' % 'result_sink_util',
mock.mock_open(read_data=LUCI_CONTEXT_FILE_DATA))
@mock.patch('os.environ.get', return_value='filename')
def test_close(self, mock_open_file, mock_session_post, mock_session_close):
client = result_sink_util.ResultSinkClient()
client._post_test_result({'some': 'result'})
mock_session_post.assert_called()
client.close()
mock_session_close.assert_called()
def test_post(self):
client = result_sink_util.ResultSinkClient()
client.sink = 'Make sink not None so _compose_test_result will be called'
client._post_test_result = mock.MagicMock()
client.post(
'testname',
'PASS',
True,
test_log='some_log',
tags=[('tag key', 'tag value')])
client._post_test_result.assert_called_with(
result_sink_util._compose_test_result(
'testname',
'PASS',
True,
test_log='some_log',
tags=[('tag key', 'tag value')]))
client.post('testname', 'PASS', True, test_log='some_log')
client._post_test_result.assert_called_with(
result_sink_util._compose_test_result(
'testname', 'PASS', True, test_log='some_log'))
client.post('testname', 'PASS', True)
client._post_test_result.assert_called_with(
result_sink_util._compose_test_result('testname', 'PASS', True))
if __name__ == '__main__':
unittest.main()