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

import unittest

import list_class_verification_failures as list_verification

import devil_chromium  # pylint: disable=unused-import
from devil.android import device_errors
from devil.android import device_utils
from devil.android.ndk import abis
from devil.android.sdk import version_codes

import mock  # pylint: disable=import-error


def _CreateOdexLine(java_class_name, type_idx, verification_status):
  """Create a rough approximation of a line of oatdump output."""
  return ('{type_idx}: L{java_class}; (offset=0xac) (type_idx={type_idx}) '
          '({verification}) '
          '(OatClassNoneCompiled)'.format(type_idx=type_idx,
                                          java_class=java_class_name,
                                          verification=verification_status))


def _ClassForName(name, classes):
  return next(c for c in classes if c.name == name)


class _DetermineDeviceToUseTest(unittest.TestCase):

  def testDetermineDeviceToUse_emptyListWithOneAttachedDevice(self):
    fake_attached_devices = ['123']
    user_specified_devices = []
    device_utils.DeviceUtils.HealthyDevices = mock.MagicMock(
        return_value=fake_attached_devices)
    result = list_verification.DetermineDeviceToUse(user_specified_devices)
    self.assertEqual(result, fake_attached_devices[0])
    # pylint: disable=no-member
    device_utils.DeviceUtils.HealthyDevices.assert_called_with(device_arg=None)
    # pylint: enable=no-member

  def testDetermineDeviceToUse_emptyListWithNoAttachedDevices(self):
    user_specified_devices = []
    device_utils.DeviceUtils.HealthyDevices = mock.MagicMock(
        side_effect=device_errors.NoDevicesError())
    with self.assertRaises(device_errors.NoDevicesError) as _:
      list_verification.DetermineDeviceToUse(user_specified_devices)
    # pylint: disable=no-member
    device_utils.DeviceUtils.HealthyDevices.assert_called_with(device_arg=None)
    # pylint: enable=no-member

  def testDetermineDeviceToUse_oneElementListWithOneAttachedDevice(self):
    user_specified_devices = ['123']
    fake_attached_devices = ['123']
    device_utils.DeviceUtils.HealthyDevices = mock.MagicMock(
        return_value=fake_attached_devices)
    result = list_verification.DetermineDeviceToUse(user_specified_devices)
    self.assertEqual(result, fake_attached_devices[0])
    # pylint: disable=no-member
    device_utils.DeviceUtils.HealthyDevices.assert_called_with(
        device_arg=user_specified_devices)
    # pylint: enable=no-member


class _ListClassVerificationFailuresTest(unittest.TestCase):

  def testPathToDexForPlatformVersion_noPaths(self):
    sdk_int = version_codes.LOLLIPOP
    paths_to_apk = []
    package_name = 'package.name'
    arch = abis.ARM_64

    device = mock.Mock(build_version_sdk=sdk_int, product_cpu_abi=arch)
    device.GetApplicationPaths = mock.MagicMock(return_value=paths_to_apk)

    with self.assertRaises(list_verification.DeviceOSError) as cm:
      list_verification.FindOdexFiles(device, package_name)
    message = str(cm.exception)
    self.assertIn('Could not find data directory', message)

  def testPathToDexForPlatformVersion_multiplePaths(self):
    sdk_int = version_codes.LOLLIPOP
    paths_to_apk = ['/first/path', '/second/path']
    package_name = 'package.name'
    arch = abis.ARM_64

    device = mock.Mock(build_version_sdk=sdk_int, product_cpu_abi=arch)
    device.GetApplicationPaths = mock.MagicMock(return_value=paths_to_apk)

    odex_files = list_verification.FindOdexFiles(device, package_name)
    self.assertEqual(odex_files, [
        '/data/dalvik-cache/arm64/data@app@first@base.apk@classes.dex',
        '/data/dalvik-cache/arm64/data@app@second@base.apk@classes.dex'
    ])

  def testPathToDexForPlatformVersion_dalvikApiLevel(self):
    sdk_int = version_codes.KITKAT
    paths_to_apk = ['/some/path']
    package_name = 'package.name'
    arch = abis.ARM_64

    device = mock.Mock(build_version_sdk=sdk_int, product_cpu_abi=arch)
    device.GetApplicationPaths = mock.MagicMock(return_value=paths_to_apk)

    with self.assertRaises(list_verification.UnsupportedDeviceError) as _:
      list_verification.FindOdexFiles(device, package_name)

  def testPathToDexForPlatformVersion_lollipopArm(self):
    sdk_int = version_codes.LOLLIPOP
    package_name = 'package.name'
    paths_to_apk = ['/some/path/{}-1/base.apk'.format(package_name)]
    arch = 'arm'

    device = mock.Mock(build_version_sdk=sdk_int, product_cpu_abi=arch)
    device.GetApplicationPaths = mock.MagicMock(return_value=paths_to_apk)
    device.FileExists = mock.MagicMock(return_value=True)

    odex_files = list_verification.FindOdexFiles(device, package_name)
    self.assertEqual(
        odex_files,
        ['/data/dalvik-cache/arm/data@app@package.name-1@base.apk@classes.dex'])

  def testPathToDexForPlatformVersion_mashmallowArm(self):
    sdk_int = version_codes.MARSHMALLOW
    package_name = 'package.name'
    paths_to_apk = ['/some/path/{}-1/base.apk'.format(package_name)]
    arch = 'arm'

    device = mock.Mock(build_version_sdk=sdk_int, product_cpu_abi=arch)
    device.GetApplicationPaths = mock.MagicMock(return_value=paths_to_apk)
    device.FileExists = mock.MagicMock(return_value=True)

    odex_files = list_verification.FindOdexFiles(device, package_name)
    self.assertEqual(odex_files,
                     ['/some/path/package.name-1/oat/arm/base.odex'])

  def testPathToDexForPlatformVersion_mashmallowArm64(self):
    sdk_int = version_codes.MARSHMALLOW
    package_name = 'package.name'
    paths_to_apk = ['/some/path/{}-1/base.apk'.format(package_name)]
    arch = abis.ARM_64

    device = mock.Mock(build_version_sdk=sdk_int, product_cpu_abi=arch)
    device.GetApplicationPaths = mock.MagicMock(return_value=paths_to_apk)
    device.FileExists = mock.MagicMock(return_value=True)

    odex_files = list_verification.FindOdexFiles(device, package_name)
    self.assertEqual(odex_files,
                     ['/some/path/package.name-1/oat/arm64/base.odex'])

  def testPathToDexForPlatformVersion_pieNoOdexFile(self):
    sdk_int = version_codes.PIE
    package_name = 'package.name'
    paths_to_apk = ['/some/path/{}-1/base.apk'.format(package_name)]
    arch = abis.ARM_64

    device = mock.Mock(build_version_sdk=sdk_int, product_cpu_abi=arch)
    device.GetApplicationPaths = mock.MagicMock(return_value=paths_to_apk)
    device.FileExists = mock.MagicMock(return_value=False)

    with self.assertRaises(list_verification.DeviceOSError) as cm:
      list_verification.FindOdexFiles(device, package_name)
    message = str(cm.exception)
    self.assertIn('you must run dex2oat on debuggable apps on >= P', message)

  def testPathToDexForPlatformVersion_lowerApiLevelNoOdexFile(self):
    sdk_int = version_codes.MARSHMALLOW
    package_name = 'package.name'
    paths_to_apk = ['/some/path/{}-1/base.apk'.format(package_name)]
    arch = abis.ARM_64

    device = mock.Mock(build_version_sdk=sdk_int, product_cpu_abi=arch)
    device.GetApplicationPaths = mock.MagicMock(return_value=paths_to_apk)
    device.FileExists = mock.MagicMock(return_value=False)

    with self.assertRaises(list_verification.DeviceOSError) as _:
      list_verification.FindOdexFiles(device, package_name)

  def testListClasses_noProguardMap(self):
    oatdump_output = [
        _CreateOdexLine('a.b.JavaClass1', 6, 'StatusVerified'),
        _CreateOdexLine('a.b.JavaClass2', 7,
                        'StatusRetryVerificationAtRuntime'),
    ]

    classes = list_verification.ParseOatdump(oatdump_output, None)
    self.assertEqual(2, len(classes))
    java_class_1 = _ClassForName('a.b.JavaClass1', classes)
    java_class_2 = _ClassForName('a.b.JavaClass2', classes)
    self.assertEqual(java_class_1.verification_status, 'Verified')
    self.assertEqual(java_class_2.verification_status,
                     'RetryVerificationAtRuntime')

  def testListClasses_proguardMap(self):
    oatdump_output = [
        _CreateOdexLine('a.b.ObfuscatedJavaClass1', 6, 'StatusVerified'),
        _CreateOdexLine('a.b.ObfuscatedJavaClass2', 7,
                        'StatusRetryVerificationAtRuntime'),
    ]

    mapping = {
        'a.b.ObfuscatedJavaClass1': 'a.b.JavaClass1',
        'a.b.ObfuscatedJavaClass2': 'a.b.JavaClass2',
    }
    classes = list_verification.ParseOatdump(oatdump_output, mapping)
    self.assertEqual(2, len(classes))
    java_class_1 = _ClassForName('a.b.JavaClass1', classes)
    java_class_2 = _ClassForName('a.b.JavaClass2', classes)
    self.assertEqual(java_class_1.verification_status, 'Verified')
    self.assertEqual(java_class_2.verification_status,
                     'RetryVerificationAtRuntime')

  def testListClasses_noStatusPrefix(self):
    oatdump_output = [
        _CreateOdexLine('a.b.JavaClass1', 6, 'Verified'),
        _CreateOdexLine('a.b.JavaClass2', 7, 'RetryVerificationAtRuntime'),
    ]

    classes = list_verification.ParseOatdump(oatdump_output, None)
    self.assertEqual(2, len(classes))
    java_class_1 = _ClassForName('a.b.JavaClass1', classes)
    java_class_2 = _ClassForName('a.b.JavaClass2', classes)
    self.assertEqual(java_class_1.verification_status, 'Verified')
    self.assertEqual(java_class_2.verification_status,
                     'RetryVerificationAtRuntime')

if __name__ == '__main__':
  # Suppress logging messages.
  unittest.main(buffer=True)
