1# Copyright 2017 The Chromium OS Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5from __future__ import print_function
6from __future__ import absolute_import
7
8import mock
9import unittest
10
11import common
12from autotest_lib.server.hosts import host_info
13from autotest_lib.server.hosts import shadowing_store
14
15
16class ShadowingStoreTestCase(unittest.TestCase):
17    """Tests shadowing capabilities of ShadowingStore"""
18
19    def test_init_commits_to_shadow(self):
20        """Initialization updates the shadow store"""
21        info = host_info.HostInfo(labels='blah', attributes='boo')
22        primary = _FakeRaisingStore(info)
23        shadow = _FakeRaisingStore()
24        store = shadowing_store.ShadowingStore(primary, shadow)
25        self.assertEqual(shadow.get(), info)
26
27    def test_commit_commits_to_both_stores(self):
28        """Successful commit involves committing to both stores"""
29        primary = _FakeRaisingStore()
30        shadow = _FakeRaisingStore()
31        store = shadowing_store.ShadowingStore(primary, shadow)
32        info = host_info.HostInfo(labels='blah', attributes='boo')
33        store.commit(info)
34        self.assertEqual(primary.get(), info)
35        self.assertEqual(shadow.get(), info)
36
37    def test_commit_ignores_failure_to_commit_to_shadow(self):
38        """Failure to commit to shadow store does not affect the result"""
39        init_info = host_info.HostInfo(labels='init')
40        primary = _FakeRaisingStore(init_info)
41        shadow = _FakeRaisingStore(init_info, raise_on_commit=True)
42        store = shadowing_store.ShadowingStore(primary, shadow)
43        info = host_info.HostInfo(labels='blah', attributes='boo')
44        store.commit(info)
45        self.assertEqual(primary.get(), info)
46        self.assertEqual(shadow.get(), init_info)
47
48    def test_refresh_validates_matching_stores(self):
49        """Successful validation on refresh returns the correct info"""
50        init_info = host_info.HostInfo(labels='init')
51        primary = _FakeRaisingStore(init_info)
52        shadow = _FakeRaisingStore(init_info)
53        store = shadowing_store.ShadowingStore(primary, shadow)
54        got = store.get(force_refresh=True)
55        self.assertEqual(got, init_info)
56
57    def test_refresh_ignores_failed_refresh_from_shadow_store(self):
58        """Failure to refresh from shadow store does not affect the result"""
59        init_info = host_info.HostInfo(labels='init')
60        primary = _FakeRaisingStore(init_info)
61        shadow = _FakeRaisingStore(init_info, raise_on_refresh=True)
62        store = shadowing_store.ShadowingStore(primary, shadow)
63        got = store.get(force_refresh=True)
64        self.assertEqual(got, init_info)
65
66    def test_refresh_complains_on_mismatching_stores(self):
67        """Store complains on mismatched responses from the primary / shadow"""
68        callback = mock.MagicMock()
69        p_info = host_info.HostInfo('primary')
70        primary = _FakeRaisingStore(p_info)
71        shadow = _FakeRaisingStore()
72        store = shadowing_store.ShadowingStore(primary, shadow,
73                                               mismatch_callback=callback)
74        # ShadowingStore will update shadow on initialization, so we modify it
75        # after creating store.
76        s_info = host_info.HostInfo('shadow')
77        shadow.commit(s_info)
78
79        got = store.get(force_refresh=True)
80        self.assertEqual(got, p_info)
81        callback.assert_called_once_with(p_info, s_info)
82
83    def test_refresh_fixes_mismatch_in_stores(self):
84        """On finding a mismatch, the difference is fixed by the store"""
85        callback = mock.MagicMock()
86        p_info = host_info.HostInfo('primary')
87        primary = _FakeRaisingStore(p_info)
88        shadow = _FakeRaisingStore()
89        store = shadowing_store.ShadowingStore(primary, shadow,
90                                               mismatch_callback=callback)
91        # ShadowingStore will update shadow on initialization, so we modify it
92        # after creating store.
93        s_info = host_info.HostInfo('shadow')
94        shadow.commit(s_info)
95
96        got = store.get(force_refresh=True)
97        self.assertEqual(got, p_info)
98        callback.assert_called_once_with(p_info, s_info)
99        self.assertEqual(got, shadow.get())
100
101        got = store.get(force_refresh=True)
102        self.assertEqual(got, p_info)
103        # No extra calls, just the one we already saw above.
104        callback.assert_called_once_with(p_info, s_info)
105
106
107class _FakeRaisingStore(host_info.InMemoryHostInfoStore):
108    """A fake store that raises an error on refresh / commit if requested"""
109
110    def __init__(self, info=None, raise_on_refresh=False,
111                 raise_on_commit=False):
112        """
113        @param info: A HostInfo to initialize the store with.
114        @param raise_on_refresh: If True, _refresh_impl raises a StoreError.
115        @param raise_on_commit: If True, _commit_impl raises a StoreError.
116        """
117        super(_FakeRaisingStore, self).__init__(info)
118        self._raise_on_refresh = raise_on_refresh
119        self._raise_on_commit = raise_on_commit
120
121    def _refresh_impl(self):
122        print('refresh_impl')
123        if self._raise_on_refresh:
124            raise host_info.StoreError('Test refresh error')
125        return super(_FakeRaisingStore, self)._refresh_impl()
126
127    def _commit_impl(self, info):
128        print('commit_impl: %s' % info)
129        if self._raise_on_commit:
130            raise host_info.StoreError('Test commit error')
131        super(_FakeRaisingStore, self)._commit_impl(info)
132
133
134if __name__ == '__main__':
135    unittest.main()
136