chromium/ios/build/bots/scripts/result_sink_util_test.py

#!/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

# import protos for exceptions reporting
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 exception_occurrences_pb2

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:
"""


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(
        'TestCase/testSomething', 'PASS', True)
    expected = {
        'testId': 'TestCase/testSomething',
        'status': 'PASS',
        'expected': True,
        'tags': [],
        'testMetadata': {
            'name': 'TestCase/testSomething',
            '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(
        'TestCase/testSomething',
        'PASS',
        True,
        test_log=short_log,
        duration=1233,
        file_artifacts={'name': '/path/to/name'})
    expected = {
        'testId': 'TestCase/testSomething',
        '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': [],
        'testMetadata': {
            'name': 'TestCase/testSomething',
            'location': None,
        },
    }
    self.assertEqual(test_result, expected)

  def test_compose_exception_occurrence(self):
    """Tests compose_exception_occurrence function."""
    test_exception = test_runner.XcodeVersionNotFoundError("15abcd")

    occurrence = result_sink_util._compose_exception_occurrence(test_exception)
    self.assertEqual(occurrence.name, "XcodeVersionNotFoundError")
    self.assertIn("Xcode version not found: 15abcd",
                  '\n'.join(occurrence.stacktrace))

  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(
        'TestCase/testSomething', 'FAIL', False, test_log=CRASH_TEST_LOG)
    expected = {
        'testId': 'TestCase/testSomething',
        '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')
            },
        },
        'testMetadata': {
            'name': 'TestCase/testSomething',
            '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': 'TestCase/testSomething',
        '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': [],
        'testMetadata': {
            'name': 'TestCase/testSomething',
            'location': None,
        },
    }
    test_result = result_sink_util._compose_test_result(
        'TestCase/testSomething', '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(
          'TestCase/testSomething', 'SOME_INVALID_STATUS', True)

    with self.assertRaises(AssertionError):
      test_result = result_sink_util._compose_test_result(
          'TestCase/testSomething', 'PASS', True, tags=('a', 'b'))

    with self.assertRaises(AssertionError):
      test_result = result_sink_util._compose_test_result(
          'TestCase/testSomething',
          'PASS',
          True,
          tags=[('a', 'b', 'c'), ('d', 'e')])

    with self.assertRaises(AssertionError):
      test_result = result_sink_util._compose_test_result(
          'TestCase/testSomething', 'PASS', True, tags=[('a', 'b'), ('c', 3)])

  def test_composed_with_tags(self):
    """Tests tags is in correct format."""
    expected = {
        'testId': 'TestCase/testSomething',
        'status': 'SKIP',
        'expected': True,
        'tags': [{
            'key': 'disabled_test',
            'value': 'true',
        }],
        'testMetadata': {
            'name': 'TestCase/testSomething',
            'location': None,
        },
    }
    test_result = result_sink_util._compose_test_result(
        'TestCase/testSomething',
        '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': 'TestCase/testSomething',
        'status': 'SKIP',
        'expected': True,
        'tags': [{
            'key': 'disabled_test',
            'value': 'true',
        }],
        'testMetadata': {
            'name': 'TestCase/testSomething',
            'location': test_loc,
        },
    }
    test_result = result_sink_util._compose_test_result(
        'TestCase/testSomething',
        'SKIP',
        True,
        test_loc=test_loc,
        tags=[('disabled_test', 'true')])
    self.assertEqual(test_result, expected)

  @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': 'TestCase/testSomething',
        'status': 'SKIP',
        'expected': True,
        'tags': [{
            'key': 'disabled_test',
            'value': 'true',
        }],
        'testMetadata': {
            'name': 'TestCase/testSomething',
            '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')
  def test_post_exceptions(self, mock_open_file, mock_session_post):
    test_exception = test_runner.XcodeVersionNotFoundError("15abcd")
    occurrences = [
        result_sink_util._compose_exception_occurrence(test_exception)
    ]
    exception_occurrences = exception_occurrences_pb2.ExceptionOccurrences()
    exception_occurrences.datapoints.extend(occurrences)
    any_msg = any_pb2.Any()
    any_msg.Pack(exception_occurrences)
    inv_data = json.dumps(
        {
            'invocation': {
                'extended_properties': {
                    'exception_occurrences':
                        json_format.MessageToDict(
                            any_msg, preserving_proto_field_name=True)
                }
            },
            'update_mask': {
                'paths': ['extended_properties.exception_occurrences'],
            }
        },
        sort_keys=True)
    client = result_sink_util.ResultSinkClient()

    client._post_exceptions(occurrences)
    mock_session_post.assert_called_with(
        url=UPATE_POST_URL, headers=HEADERS, data=inv_data)

  @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()