#!/usr/bin/env vpython3
# Copyright 2019 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

import copy
import json
import os
import subprocess
import sys
import unittest
from unittest import mock

import merge_results
import merge_steps
import merge_lib as merger


class MergeProfilesTest(unittest.TestCase):

  # pylint: disable=super-with-arguments
  def __init__(self, *args, **kwargs):
    super(MergeProfilesTest, self).__init__(*args, **kwargs)
    self.maxDiff = None

  # pylint: enable=super-with-arguments

  def test_merge_script_api_parameters(self):
    """Test the step-level merge front-end."""
    build_properties = json.dumps({
        'some': {
            'complicated': ['nested', {
                'json': None,
                'object': 'thing',
            }]
        }
    })
    task_output_dir = 'some/task/output/dir'
    profdata_dir = '/some/different/path/to/profdata/default.profdata'
    profdata_file = os.path.join(profdata_dir, 'base_unittests.profdata')
    args = [
        'script_name', '--output-json', 'output.json', '--build-properties',
        build_properties, '--summary-json', 'summary.json', '--task-output-dir',
        task_output_dir, '--profdata-dir', profdata_dir, '--llvm-profdata',
        'llvm-profdata', 'a.json', 'b.json', 'c.json', '--test-target-name',
        'base_unittests', '--sparse'
    ]
    with mock.patch.object(merger, 'merge_profiles') as mock_merge:
      mock_merge.return_value = None, None
      with mock.patch.object(sys, 'argv', args):
        merge_results.main()
        self.assertEqual(
            mock_merge.call_args,
            mock.call(task_output_dir,
                      profdata_file,
                      '.profraw',
                      'llvm-profdata',
                      sparse=True,
                      skip_validation=False), None)

  def test_merge_steps_parameters(self):
    """Test the build-level merge front-end."""
    input_dir = 'some/task/output/dir'
    output_file = '/some/different/path/to/profdata/merged.profdata'
    args = [
        'script_name', '--input-dir', input_dir, '--output-file', output_file,
        '--llvm-profdata', 'llvm-profdata', '--profdata-filename-pattern', '.*'
    ]
    with mock.patch.object(merger, 'merge_profiles') as mock_merge:
      mock_merge.return_value = [], []
      with mock.patch.object(sys, 'argv', args):
        merge_steps.main()
        self.assertEqual(
            mock_merge.call_args,
            mock.call(input_dir,
                      output_file,
                      '.profdata',
                      'llvm-profdata',
                      '.*',
                      sparse=False,
                      merge_timeout=3600,
                      weights={}))

  @mock.patch('builtins.open', new_callable=mock.mock_open())
  @mock.patch.object(merger, '_validate_and_convert_profraws')
  def test_merge_profraw(self, mock_validate_and_convert_profraws,
                         mock_file_open):
    mock_input_dir_walk = [
        ('/b/some/path', ['0', '1', '2', '3'], ['summary.json']),
        ('/b/some/path/0', [],
         ['output.json', 'default-1.profraw', 'default-2.profraw']),
        ('/b/some/path/1', [],
         ['output.json', 'default-1.profraw', 'default-2.profraw']),
    ]

    mock_validate_and_convert_profraws.return_value = [
        '/b/some/path/0/default-1.profdata',
        '/b/some/path/1/default-2.profdata',
    ], [
        '/b/some/path/0/default-2.profraw',
        '/b/some/path/1/default-1.profraw',
    ], [
        '/b/some/path/1/default-1.profraw',
    ]

    with mock.patch.object(os, 'walk') as mock_walk:
      with mock.patch.object(os, 'remove'):
        mock_walk.return_value = mock_input_dir_walk
        with mock.patch.object(subprocess, 'run') as mock_exec_cmd:
          merger.merge_profiles('/b/some/path',
                                'output/dir/default.profdata',
                                '.profraw',
                                'llvm-profdata',
                                show_profdata=False)
          self.assertEqual(
              mock.call([
                  'llvm-profdata',
                  'merge',
                  '-o',
                  'output/dir/default.profdata',
                  '-f',
                  'output/dir/input-profdata-files.txt',
              ],
                        capture_output=True,
                        check=True,
                        text=True,
                        timeout=3600), mock_exec_cmd.call_args)
          context = mock_file_open()
          self.assertEqual(context.__enter__().write.call_count, 2)
          context.__enter__().write.assert_any_call(
              '/b/some/path/0/default-1.profdata\n')
          context.__enter__().write.assert_any_call(
              '/b/some/path/1/default-2.profdata\n')

    self.assertTrue(mock_validate_and_convert_profraws.called)

  @mock.patch('builtins.open', new_callable=mock.mock_open())
  @mock.patch.object(merger, '_validate_and_convert_profraws')
  def test_profraw_skip_validation(self, mock_validate_and_convert_profraws,
                                   mock_file_open):
    mock_input_dir_walk = [
        ('/b/some/path', ['0', '1', '2', '3'], ['summary.json']),
        ('/b/some/path/0', [],
         ['output.json', 'default-1.profraw', 'default-2.profraw']),
        ('/b/some/path/1', [],
         ['output.json', 'default-1.profraw', 'default-2.profraw']),
    ]

    with mock.patch.object(os, 'walk') as mock_walk:
      with mock.patch.object(os, 'remove'):
        mock_walk.return_value = mock_input_dir_walk
        with mock.patch.object(subprocess, 'run') as mock_exec_cmd:
          merger.merge_profiles('/b/some/path',
                                'output/dir/default.profdata',
                                '.profraw',
                                'llvm-profdata',
                                skip_validation=True,
                                show_profdata=False)
          self.assertEqual(
              mock.call([
                  'llvm-profdata',
                  'merge',
                  '-o',
                  'output/dir/default.profdata',
                  '-f',
                  'output/dir/input-profdata-files.txt',
              ],
                        capture_output=True,
                        check=True,
                        text=True,
                        timeout=3600), mock_exec_cmd.call_args)
          context = mock_file_open()
          self.assertEqual(context.__enter__().write.call_count, 4)
          context.__enter__().write.assert_any_call(
              '/b/some/path/0/default-1.profraw\n')
          context.__enter__().write.assert_any_call(
              '/b/some/path/0/default-2.profraw\n')
          context.__enter__().write.assert_any_call(
              '/b/some/path/1/default-1.profraw\n')
          context.__enter__().write.assert_any_call(
              '/b/some/path/1/default-2.profraw\n')

    # Skip validation should've passed all profraw files directly, and
    # this validate call should not have been invoked.
    self.assertFalse(mock_validate_and_convert_profraws.called)

  def test_merge_profraw_skip_if_there_is_no_file(self):
    mock_input_dir_walk = [
        ('/b/some/path', ['0', '1', '2', '3'], ['summary.json']),
    ]

    with mock.patch.object(os, 'walk') as mock_walk:
      mock_walk.return_value = mock_input_dir_walk
      with mock.patch.object(subprocess, 'check_call') as mock_exec_cmd:
        merger.merge_profiles('/b/some/path',
                              'output/dir/default.profdata',
                              '.profraw',
                              'llvm-profdata',
                              show_profdata=False)
        self.assertFalse(mock_exec_cmd.called)

  @mock.patch('builtins.open', new_callable=mock.mock_open())
  @mock.patch.object(merger, '_validate_and_convert_profraws')
  def test_merge_profdata(self, mock_validate_and_convert_profraws,
                          mock_file_open):
    mock_input_dir_walk = [
        ('/b/some/path', ['base_unittests', 'url_unittests'], ['summary.json']),
        ('/b/some/path/base_unittests', [], ['output.json',
                                             'default.profdata']),
        ('/b/some/path/url_unittests', [], ['output.json', 'default.profdata']),
    ]
    with mock.patch.object(os, 'walk') as mock_walk:
      with mock.patch.object(os, 'remove'):
        mock_walk.return_value = mock_input_dir_walk
        with mock.patch.object(subprocess, 'run') as mock_exec_cmd:
          merger.merge_profiles('/b/some/path',
                                'output/dir/default.profdata',
                                '.profdata',
                                'llvm-profdata',
                                show_profdata=False)
          self.assertEqual(
              mock.call([
                  'llvm-profdata',
                  'merge',
                  '-o',
                  'output/dir/default.profdata',
                  '-f',
                  'output/dir/input-profdata-files.txt',
              ],
                        capture_output=True,
                        check=True,
                        text=True,
                        timeout=3600), mock_exec_cmd.call_args)
          context = mock_file_open()
          self.assertEqual(context.__enter__().write.call_count, 2)
          context.__enter__().write.assert_any_call(
              '/b/some/path/base_unittests/default.profdata\n')
          context.__enter__().write.assert_any_call(
              '/b/some/path/url_unittests/default.profdata\n')

    # The mock method should only apply when merging .profraw files.
    self.assertFalse(mock_validate_and_convert_profraws.called)

  @mock.patch('builtins.open', new_callable=mock.mock_open())
  @mock.patch.object(merger, '_validate_and_convert_profraws')
  def test_merge_profdata_pattern(self, mock_validate_and_convert_profraws,
                                  mock_file_open):
    mock_input_dir_walk = [
        ('/b/some/path', ['base_unittests', 'url_unittests'], ['summary.json']),
        ('/b/some/path/base_unittests', [],
         ['output.json', 'base_unittests.profdata']),
        (
            '/b/some/path/url_unittests',
            [],
            ['output.json', 'url_unittests.profdata'],
        ),
        (
            '/b/some/path/ios_chrome_smoke_eg2tests',
            [],
            ['output.json', 'ios_chrome_smoke_eg2tests.profdata'],
        ),
    ]
    with mock.patch.object(os, 'walk') as mock_walk:
      with mock.patch.object(os, 'remove'):
        mock_walk.return_value = mock_input_dir_walk
        with mock.patch.object(subprocess, 'run') as mock_exec_cmd:
          input_profdata_filename_pattern = r'.+_unittests\.profdata'
          merger.merge_profiles('/b/some/path',
                                'output/dir/default.profdata',
                                '.profdata',
                                'llvm-profdata',
                                input_profdata_filename_pattern,
                                show_profdata=False)
          self.assertEqual(
              mock.call([
                  'llvm-profdata',
                  'merge',
                  '-o',
                  'output/dir/default.profdata',
                  '-f',
                  'output/dir/input-profdata-files.txt',
              ],
                        capture_output=True,
                        check=True,
                        text=True,
                        timeout=3600), mock_exec_cmd.call_args)
          context = mock_file_open()
          self.assertEqual(context.__enter__().write.call_count, 2)
          context.__enter__().write.assert_any_call(
              '/b/some/path/base_unittests/base_unittests.profdata\n')
          context.__enter__().write.assert_any_call(
              '/b/some/path/url_unittests/url_unittests.profdata\n')

    # The mock method should only apply when merging .profraw files.
    self.assertFalse(mock_validate_and_convert_profraws.called)

  @mock.patch('builtins.open', new_callable=mock.mock_open())
  @mock.patch.object(merger, '_validate_and_convert_profraws')
  def test_merge_profiles_with_weights(self, mock_validate_and_convert_profraws,
                                       mock_file_open):
    mock_input_dir_walk = [
        ('/b/some/path', ['speedometer_benchmark', 'motionmark_benchmark'], []),
        ('/b/some/path/speedometer_benchmark', [], ['foo.profdata']),
        ('/b/some/path/motionmark_benchmark', [], ['foo.profdata']),
    ]
    with mock.patch.object(os, 'walk') as mock_walk:
      with mock.patch.object(os, 'remove'):
        mock_walk.return_value = mock_input_dir_walk
        with mock.patch.object(subprocess, 'run') as mock_exec_cmd:
          merger.merge_profiles(
              '/b/some/path',
              'output/dir/default.profdata',
              '.profdata',
              'llvm-profdata',
              '.*',
              show_profdata=False,
              weights={'speedometer_benchmark/foo.profdata': '3'})
          self.assertEqual(
              mock.call([
                  'llvm-profdata',
                  'merge',
                  '-o',
                  'output/dir/default.profdata',
                  '-f',
                  'output/dir/input-profdata-files.txt',
              ],
                        capture_output=True,
                        check=True,
                        text=True,
                        timeout=3600), mock_exec_cmd.call_args)
          context = mock_file_open()
          self.assertEqual(context.__enter__().write.call_count, 2)
          context.__enter__().write.assert_any_call(
              '3,/b/some/path/speedometer_benchmark/foo.profdata\n')
          context.__enter__().write.assert_any_call(
              '/b/some/path/motionmark_benchmark/foo.profdata\n')

    # The mock method should only apply when merging .profraw files.
    self.assertFalse(mock_validate_and_convert_profraws.called)

  @mock.patch('merge_lib._JAVA_PATH', 'java')
  def test_merge_java_exec_files(self):
    mock_input_dir_walk = [
        ('/b/some/path', ['0', '1', '2', '3'], ['summary.json']),
        ('/b/some/path/0', [],
         ['output.json', 'default-1.exec', 'default-2.exec']),
        ('/b/some/path/1', [],
         ['output.json', 'default-3.exec', 'default-4.exec']),
    ]

    with mock.patch.object(os, 'walk') as mock_walk:
      mock_walk.return_value = mock_input_dir_walk
      with mock.patch.object(subprocess, 'check_call') as mock_exec_cmd:
        merger.merge_java_exec_files('/b/some/path', 'output/path',
                                     'path/to/jacococli.jar')
        self.assertEqual(
            mock.call([
                'java',
                '-jar',
                'path/to/jacococli.jar',
                'merge',
                '/b/some/path/0/default-1.exec',
                '/b/some/path/0/default-2.exec',
                '/b/some/path/1/default-3.exec',
                '/b/some/path/1/default-4.exec',
                '--destfile',
                'output/path',
            ],
                      stderr=-2), mock_exec_cmd.call_args)

  def test_merge_java_exec_files_if_there_is_no_file(self):
    mock_input_dir_walk = [
        ('/b/some/path', ['0', '1', '2', '3'], ['summary.json']),
    ]

    with mock.patch.object(os, 'walk') as mock_walk:
      mock_walk.return_value = mock_input_dir_walk
      with mock.patch.object(subprocess, 'check_call') as mock_exec_cmd:
        merger.merge_java_exec_files('/b/some/path', 'output/path',
                                     'path/to/jacococli.jar')
        self.assertFalse(mock_exec_cmd.called)

  def test_calls_merge_js_results_script(self):
    task_output_dir = 'some/task/output/dir'
    profdata_dir = '/some/different/path/to/profdata/default.profdata'

    args = [
        'script_name', '--output-json', 'output.json', '--task-output-dir',
        task_output_dir, '--profdata-dir', profdata_dir, '--llvm-profdata',
        'llvm-profdata', 'a.json', 'b.json', 'c.json', '--test-target-name',
        'v8_unittests', '--sparse', '--javascript-coverage-dir',
        'output/dir/devtools_code_coverage', '--chromium-src-dir',
        'chromium/src', '--build-dir', 'output/dir'
    ]
    with mock.patch.object(merger, 'merge_profiles') as mock_merge:
      mock_merge.return_value = None, None
      with mock.patch.object(sys, 'argv', args):
        with mock.patch.object(subprocess, 'call') as mock_exec_cmd:
          with mock.patch.object(os.path, 'join') as mock_os_path_join:
            mock_merge_js_results_path = 'path/to/js/merge_js_results.py'
            mock_os_path_join.return_value = mock_merge_js_results_path
            python_exec = sys.executable
            rc = merge_results.main()

            self.assertEqual(rc, 1)
            mock_exec_cmd.assert_called_with([
                python_exec, mock_merge_js_results_path, '--task-output-dir',
                task_output_dir, '--javascript-coverage-dir',
                'output/dir/devtools_code_coverage', '--chromium-src-dir',
                'chromium/src', '--build-dir', 'output/dir'
            ])

  def test_argparse_sparse(self):
    """Ensure that sparse flag defaults to true, and is set to correct value"""
    # Basic required args
    build_properties = json.dumps({
        'some': {
            'complicated': ['nested', {
                'json': None,
                'object': 'thing',
            }]
        }
    })
    task_output_dir = 'some/task/output/dir'
    profdata_dir = '/some/different/path/to/profdata/default.profdata'
    profdata_file = os.path.join(profdata_dir, 'base_unittests.profdata')
    args = [
        'script_name', '--output-json', 'output.json', '--build-properties',
        build_properties, '--summary-json', 'summary.json', '--task-output-dir',
        task_output_dir, '--profdata-dir', profdata_dir, '--llvm-profdata',
        'llvm-profdata', 'a.json', 'b.json', 'c.json', '--test-target-name',
        'base_unittests'
    ]

    test_scenarios = [
        {
            # Base set of args should set --sparse to false by default
            'args': None,
            'expected_outcome': False,
        },
        {
            # Sparse should parse True when only --sparse is specified
            'args': ['--sparse'],
            'expected_outcome': True,
        }
    ]

    for scenario in test_scenarios:
      args = copy.deepcopy(args)
      additional_args = scenario['args']
      if additional_args:
        args.extend(additional_args)
      expected_outcome = scenario['expected_outcome']

      with mock.patch.object(merger, 'merge_profiles') as mock_merge:
        mock_merge.return_value = None, None
        with mock.patch.object(sys, 'argv', args):
          merge_results.main()
          self.assertEqual(
              mock_merge.call_args,
              mock.call(task_output_dir,
                        profdata_file,
                        '.profraw',
                        'llvm-profdata',
                        sparse=expected_outcome,
                        skip_validation=False), None)


if __name__ == '__main__':
  unittest.main()
