#!/usr/bin/env python
# Copyright 2015 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.
"""
Unit tests for the contents of shared_prefs.py (mostly SharedPrefs).
"""

import logging
import unittest

from devil import devil_env
from devil.android import device_utils
from devil.android.sdk import shared_prefs
from devil.android.sdk import version_codes

with devil_env.SysPath(devil_env.PYMOCK_PATH):
  import mock  # pylint: disable=import-error

INITIAL_XML = ("<?xml version='1.0' encoding='utf-8' standalone='yes' ?>\n"
               '<map>\n'
               '  <int name="databaseVersion" value="107" />\n'
               '  <boolean name="featureEnabled" value="false" />\n'
               '  <string name="someHashValue">249b3e5af13d4db2</string>\n'
               '</map>')


def MockDeviceWithFiles(files=None):
  if files is None:
    files = {}

  def file_exists(path):
    return path in files

  def write_file(path, contents, **_kwargs):
    files[path] = contents

  def read_file(path, **_kwargs):
    return files[path]

  device = mock.MagicMock(spec=device_utils.DeviceUtils)
  device.FileExists = mock.Mock(side_effect=file_exists)
  device.WriteFile = mock.Mock(side_effect=write_file)
  device.ReadFile = mock.Mock(side_effect=read_file)
  return device


class SharedPrefsTest(unittest.TestCase):
  def setUp(self):
    self.device = MockDeviceWithFiles({
        '/data/data/com.some.package/shared_prefs/prefs.xml': INITIAL_XML
    })
    self.expected_data = {
        'databaseVersion': 107,
        'featureEnabled': False,
        'someHashValue': '249b3e5af13d4db2'
    }

  def testPropertyLifetime(self):
    prefs = shared_prefs.SharedPrefs(self.device, 'com.some.package',
                                     'prefs.xml')
    self.assertEquals(len(prefs), 0)  # collection is empty before loading
    prefs.SetInt('myValue', 444)
    self.assertEquals(len(prefs), 1)
    self.assertEquals(prefs.GetInt('myValue'), 444)
    self.assertTrue(prefs.HasProperty('myValue'))
    prefs.Remove('myValue')
    self.assertEquals(len(prefs), 0)
    self.assertFalse(prefs.HasProperty('myValue'))
    with self.assertRaises(KeyError):
      prefs.GetInt('myValue')

  def testPropertyType(self):
    prefs = shared_prefs.SharedPrefs(self.device, 'com.some.package',
                                     'prefs.xml')
    prefs.SetInt('myValue', 444)
    self.assertEquals(prefs.PropertyType('myValue'), 'int')
    with self.assertRaises(TypeError):
      prefs.GetString('myValue')
    with self.assertRaises(TypeError):
      prefs.SetString('myValue', 'hello')

  def testLoad(self):
    prefs = shared_prefs.SharedPrefs(self.device, 'com.some.package',
                                     'prefs.xml')
    self.assertEquals(len(prefs), 0)  # collection is empty before loading
    prefs.Load()
    self.assertEquals(len(prefs), len(self.expected_data))
    self.assertEquals(prefs.AsDict(), self.expected_data)
    self.assertFalse(prefs.changed)

  def testClear(self):
    prefs = shared_prefs.SharedPrefs(self.device, 'com.some.package',
                                     'prefs.xml')
    prefs.Load()
    self.assertEquals(prefs.AsDict(), self.expected_data)
    self.assertFalse(prefs.changed)
    prefs.Clear()
    self.assertEquals(len(prefs), 0)  # collection is empty now
    self.assertTrue(prefs.changed)

  def testCommit(self):
    type(self.device).build_version_sdk = mock.PropertyMock(
        return_value=version_codes.LOLLIPOP_MR1)
    prefs = shared_prefs.SharedPrefs(self.device, 'com.some.package',
                                     'other_prefs.xml')
    self.assertFalse(self.device.FileExists(prefs.path))  # file does not exist
    prefs.Load()
    self.assertEquals(len(prefs), 0)  # file did not exist, collection is empty
    prefs.SetInt('magicNumber', 42)
    prefs.SetFloat('myMetric', 3.14)
    prefs.SetLong('bigNumner', 6000000000)
    prefs.SetStringSet('apps', ['gmail', 'chrome', 'music'])
    self.assertFalse(self.device.FileExists(prefs.path))  # still does not exist
    self.assertTrue(prefs.changed)
    prefs.Commit()
    self.assertTrue(self.device.FileExists(prefs.path))  # should exist now
    self.device.KillAll.assert_called_once_with(
        prefs.package, exact=True, as_root=True, quiet=True)
    self.assertFalse(prefs.changed)

    prefs = shared_prefs.SharedPrefs(self.device, 'com.some.package',
                                     'other_prefs.xml')
    self.assertEquals(len(prefs), 0)  # collection is empty before loading
    prefs.Load()
    self.assertEquals(
        prefs.AsDict(), {
            'magicNumber': 42,
            'myMetric': 3.14,
            'bigNumner': 6000000000,
            'apps': ['gmail', 'chrome', 'music']
        })  # data survived roundtrip

  def testForceCommit(self):
    prefs = shared_prefs.SharedPrefs(self.device, 'com.some.package',
                                     'prefs.xml')
    prefs.Load()
    new_xml = 'Not valid XML'
    self.device.WriteFile('/data/data/com.some.package/shared_prefs/prefs.xml',
                          new_xml)
    prefs.Commit()
    # Since we didn't change anything, Commit() should be a no-op.
    self.assertEquals(
        self.device.ReadFile(
            '/data/data/com.some.package/shared_prefs/prefs.xml'), new_xml)
    prefs.Commit(force_commit=True)
    # Forcing the commit should restore the originally read XML.
    self.assertEquals(
        self.device.ReadFile(
            '/data/data/com.some.package/shared_prefs/prefs.xml'), INITIAL_XML)

  def testAsContextManager_onlyReads(self):
    with shared_prefs.SharedPrefs(self.device, 'com.some.package',
                                  'prefs.xml') as prefs:
      self.assertEquals(prefs.AsDict(), self.expected_data)  # loaded and ready
    self.assertEquals(self.device.WriteFile.call_args_list, [])  # did not write

  def testAsContextManager_readAndWrite(self):
    type(self.device).build_version_sdk = mock.PropertyMock(
        return_value=version_codes.LOLLIPOP_MR1)
    with shared_prefs.SharedPrefs(self.device, 'com.some.package',
                                  'prefs.xml') as prefs:
      prefs.SetBoolean('featureEnabled', True)
      prefs.Remove('someHashValue')
      prefs.SetString('newString', 'hello')

    self.assertTrue(self.device.WriteFile.called)  # did write
    with shared_prefs.SharedPrefs(self.device, 'com.some.package',
                                  'prefs.xml') as prefs:
      # changes persisted
      self.assertTrue(prefs.GetBoolean('featureEnabled'))
      self.assertFalse(prefs.HasProperty('someHashValue'))
      self.assertEquals(prefs.GetString('newString'), 'hello')
      self.assertTrue(prefs.HasProperty('databaseVersion'))  # still there

  def testAsContextManager_commitAborted(self):
    with self.assertRaises(TypeError):
      with shared_prefs.SharedPrefs(self.device, 'com.some.package',
                                    'prefs.xml') as prefs:
        prefs.SetBoolean('featureEnabled', True)
        prefs.Remove('someHashValue')
        prefs.SetString('newString', 'hello')
        prefs.SetInt('newString', 123)  # oops!

    self.assertEquals(self.device.WriteFile.call_args_list, [])  # did not write
    with shared_prefs.SharedPrefs(self.device, 'com.some.package',
                                  'prefs.xml') as prefs:
      # contents were not modified
      self.assertEquals(prefs.AsDict(), self.expected_data)

  def testEncryptedPath(self):
    type(self.device).build_version_sdk = mock.PropertyMock(
        return_value=version_codes.MARSHMALLOW)
    with shared_prefs.SharedPrefs(
        self.device, 'com.some.package', 'prefs.xml',
        use_encrypted_path=True) as prefs:
      self.assertTrue(prefs.path.startswith('/data/data'))

    type(self.device).build_version_sdk = mock.PropertyMock(
        return_value=version_codes.NOUGAT)
    with shared_prefs.SharedPrefs(
        self.device, 'com.some.package', 'prefs.xml',
        use_encrypted_path=True) as prefs:
      self.assertTrue(prefs.path.startswith('/data/user_de/0'))


if __name__ == '__main__':
  logging.getLogger().setLevel(logging.DEBUG)
  unittest.main(verbosity=2)
