1#!/usr/bin/env python
2# Copyright 2015 The Chromium Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6"""
7Unit tests for the contents of shared_prefs.py (mostly SharedPrefs).
8"""
9
10import logging
11import unittest
12
13from devil import devil_env
14from devil.android import device_utils
15from devil.android.sdk import shared_prefs
16from devil.android.sdk import version_codes
17
18with devil_env.SysPath(devil_env.PYMOCK_PATH):
19  import mock  # pylint: disable=import-error
20
21
22INITIAL_XML = ("<?xml version='1.0' encoding='utf-8' standalone='yes' ?>\n"
23               '<map>\n'
24               '  <int name="databaseVersion" value="107" />\n'
25               '  <boolean name="featureEnabled" value="false" />\n'
26               '  <string name="someHashValue">249b3e5af13d4db2</string>\n'
27               '</map>')
28
29
30def MockDeviceWithFiles(files=None):
31  if files is None:
32    files = {}
33
34  def file_exists(path):
35    return path in files
36
37  def write_file(path, contents, **_kwargs):
38    files[path] = contents
39
40  def read_file(path, **_kwargs):
41    return files[path]
42
43  device = mock.MagicMock(spec=device_utils.DeviceUtils)
44  device.FileExists = mock.Mock(side_effect=file_exists)
45  device.WriteFile = mock.Mock(side_effect=write_file)
46  device.ReadFile = mock.Mock(side_effect=read_file)
47  return device
48
49
50class SharedPrefsTest(unittest.TestCase):
51
52  def setUp(self):
53    self.device = MockDeviceWithFiles({
54      '/data/data/com.some.package/shared_prefs/prefs.xml': INITIAL_XML})
55    self.expected_data = {'databaseVersion': 107,
56                          'featureEnabled': False,
57                          'someHashValue': '249b3e5af13d4db2'}
58
59  def testPropertyLifetime(self):
60    prefs = shared_prefs.SharedPrefs(
61        self.device, 'com.some.package', 'prefs.xml')
62    self.assertEquals(len(prefs), 0)  # collection is empty before loading
63    prefs.SetInt('myValue', 444)
64    self.assertEquals(len(prefs), 1)
65    self.assertEquals(prefs.GetInt('myValue'), 444)
66    self.assertTrue(prefs.HasProperty('myValue'))
67    prefs.Remove('myValue')
68    self.assertEquals(len(prefs), 0)
69    self.assertFalse(prefs.HasProperty('myValue'))
70    with self.assertRaises(KeyError):
71      prefs.GetInt('myValue')
72
73  def testPropertyType(self):
74    prefs = shared_prefs.SharedPrefs(
75        self.device, 'com.some.package', 'prefs.xml')
76    prefs.SetInt('myValue', 444)
77    self.assertEquals(prefs.PropertyType('myValue'), 'int')
78    with self.assertRaises(TypeError):
79      prefs.GetString('myValue')
80    with self.assertRaises(TypeError):
81      prefs.SetString('myValue', 'hello')
82
83  def testLoad(self):
84    prefs = shared_prefs.SharedPrefs(
85        self.device, 'com.some.package', 'prefs.xml')
86    self.assertEquals(len(prefs), 0)  # collection is empty before loading
87    prefs.Load()
88    self.assertEquals(len(prefs), len(self.expected_data))
89    self.assertEquals(prefs.AsDict(), self.expected_data)
90    self.assertFalse(prefs.changed)
91
92  def testClear(self):
93    prefs = shared_prefs.SharedPrefs(
94        self.device, 'com.some.package', 'prefs.xml')
95    prefs.Load()
96    self.assertEquals(prefs.AsDict(), self.expected_data)
97    self.assertFalse(prefs.changed)
98    prefs.Clear()
99    self.assertEquals(len(prefs), 0)  # collection is empty now
100    self.assertTrue(prefs.changed)
101
102  def testCommit(self):
103    type(self.device).build_version_sdk = mock.PropertyMock(
104        return_value=version_codes.LOLLIPOP_MR1)
105    prefs = shared_prefs.SharedPrefs(
106        self.device, 'com.some.package', 'other_prefs.xml')
107    self.assertFalse(self.device.FileExists(prefs.path))  # file does not exist
108    prefs.Load()
109    self.assertEquals(len(prefs), 0)  # file did not exist, collection is empty
110    prefs.SetInt('magicNumber', 42)
111    prefs.SetFloat('myMetric', 3.14)
112    prefs.SetLong('bigNumner', 6000000000)
113    prefs.SetStringSet('apps', ['gmail', 'chrome', 'music'])
114    self.assertFalse(self.device.FileExists(prefs.path))  # still does not exist
115    self.assertTrue(prefs.changed)
116    prefs.Commit()
117    self.assertTrue(self.device.FileExists(prefs.path))  # should exist now
118    self.device.KillAll.assert_called_once_with(prefs.package, exact=True,
119                                                as_root=True, quiet=True)
120    self.assertFalse(prefs.changed)
121
122    prefs = shared_prefs.SharedPrefs(
123        self.device, 'com.some.package', 'other_prefs.xml')
124    self.assertEquals(len(prefs), 0)  # collection is empty before loading
125    prefs.Load()
126    self.assertEquals(prefs.AsDict(), {
127        'magicNumber': 42,
128        'myMetric': 3.14,
129        'bigNumner': 6000000000,
130        'apps': ['gmail', 'chrome', 'music']})  # data survived roundtrip
131
132  def testForceCommit(self):
133    prefs = shared_prefs.SharedPrefs(
134        self.device, 'com.some.package', 'prefs.xml')
135    prefs.Load()
136    new_xml = 'Not valid XML'
137    self.device.WriteFile('/data/data/com.some.package/shared_prefs/prefs.xml',
138        new_xml)
139    prefs.Commit()
140    # Since we didn't change anything, Commit() should be a no-op.
141    self.assertEquals(self.device.ReadFile(
142        '/data/data/com.some.package/shared_prefs/prefs.xml'), new_xml)
143    prefs.Commit(force_commit=True)
144    # Forcing the commit should restore the originally read XML.
145    self.assertEquals(self.device.ReadFile(
146        '/data/data/com.some.package/shared_prefs/prefs.xml'), INITIAL_XML)
147
148  def testAsContextManager_onlyReads(self):
149    with shared_prefs.SharedPrefs(
150        self.device, 'com.some.package', 'prefs.xml') as prefs:
151      self.assertEquals(prefs.AsDict(), self.expected_data)  # loaded and ready
152    self.assertEquals(self.device.WriteFile.call_args_list, [])  # did not write
153
154  def testAsContextManager_readAndWrite(self):
155    type(self.device).build_version_sdk = mock.PropertyMock(
156        return_value=version_codes.LOLLIPOP_MR1)
157    with shared_prefs.SharedPrefs(
158        self.device, 'com.some.package', 'prefs.xml') as prefs:
159      prefs.SetBoolean('featureEnabled', True)
160      prefs.Remove('someHashValue')
161      prefs.SetString('newString', 'hello')
162
163    self.assertTrue(self.device.WriteFile.called)  # did write
164    with shared_prefs.SharedPrefs(
165        self.device, 'com.some.package', 'prefs.xml') as prefs:
166      # changes persisted
167      self.assertTrue(prefs.GetBoolean('featureEnabled'))
168      self.assertFalse(prefs.HasProperty('someHashValue'))
169      self.assertEquals(prefs.GetString('newString'), 'hello')
170      self.assertTrue(prefs.HasProperty('databaseVersion'))  # still there
171
172  def testAsContextManager_commitAborted(self):
173    with self.assertRaises(TypeError):
174      with shared_prefs.SharedPrefs(
175          self.device, 'com.some.package', 'prefs.xml') as prefs:
176        prefs.SetBoolean('featureEnabled', True)
177        prefs.Remove('someHashValue')
178        prefs.SetString('newString', 'hello')
179        prefs.SetInt('newString', 123)  # oops!
180
181    self.assertEquals(self.device.WriteFile.call_args_list, [])  # did not write
182    with shared_prefs.SharedPrefs(
183        self.device, 'com.some.package', 'prefs.xml') as prefs:
184      # contents were not modified
185      self.assertEquals(prefs.AsDict(), self.expected_data)
186
187  def testEncryptedPath(self):
188    type(self.device).build_version_sdk = mock.PropertyMock(
189        return_value=version_codes.MARSHMALLOW)
190    with shared_prefs.SharedPrefs(self.device, 'com.some.package',
191        'prefs.xml', use_encrypted_path=True) as prefs:
192      self.assertTrue(prefs.path.startswith('/data/data'))
193
194    type(self.device).build_version_sdk = mock.PropertyMock(
195        return_value=version_codes.NOUGAT)
196    with shared_prefs.SharedPrefs(self.device, 'com.some.package',
197        'prefs.xml', use_encrypted_path=True) as prefs:
198      self.assertTrue(prefs.path.startswith('/data/user_de/0'))
199
200if __name__ == '__main__':
201  logging.getLogger().setLevel(logging.DEBUG)
202  unittest.main(verbosity=2)
203