Joel Kitching | 5825663 | 2016-11-15 15:47:42 +0800 | [diff] [blame] | 1 | # Copyright 2016 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 | |
| 5 | """A collection of mock classes for plugin unittests.""" |
| 6 | |
| 7 | # TODO(kitching): Add locks to ensure multi-threading support. |
Joel Kitching | 5825663 | 2016-11-15 15:47:42 +0800 | [diff] [blame] | 8 | |
| 9 | from __future__ import print_function |
| 10 | |
chuntsen | 71a9251 | 2017-04-20 14:10:45 +0800 | [diff] [blame] | 11 | import copy |
Joel Kitching | 5825663 | 2016-11-15 15:47:42 +0800 | [diff] [blame] | 12 | import logging |
Joel Kitching | 9405014 | 2017-01-30 12:20:04 +0800 | [diff] [blame] | 13 | import os |
| 14 | import shutil |
| 15 | import tempfile |
Joel Kitching | 5825663 | 2016-11-15 15:47:42 +0800 | [diff] [blame] | 16 | |
Wei-Han Chen | d665b00 | 2019-10-03 16:16:09 +0800 | [diff] [blame] | 17 | from cros.factory.instalog import plugin_base |
| 18 | from cros.factory.instalog import plugin_sandbox |
| 19 | from cros.factory.instalog.utils import file_utils |
Joel Kitching | 5825663 | 2016-11-15 15:47:42 +0800 | [diff] [blame] | 20 | |
| 21 | |
| 22 | class MockCore(plugin_sandbox.CoreAPI): |
| 23 | """Implements CoreAPI as a mock object for testing. |
| 24 | |
| 25 | Allows low-level access to BufferEventStreams, as well as storing and playing |
| 26 | back Emit call history. |
| 27 | |
| 28 | BufferEventStreams: Any number of BufferEventStreams can be created. For |
| 29 | example, if the plugin is single-threaded, one BufferEventStream is |
| 30 | sufficient. However, if you specifically want to separate Events into two |
| 31 | batches that are separately read by the plugin as separate BufferEventStreams, |
| 32 | you can create two BufferEventStream objects. Usage: Use Queue to add Events |
| 33 | to the MockBufferEventStream object returned by GetStream. Example: |
| 34 | |
| 35 | mock_core.GetStream(0).Queue([datatypes.Event({})]) |
| 36 | |
| 37 | Emit call history: Stored in the self.emit_calls instance variable as a list. |
| 38 | The Event list from each call is appended to the emit_calls list every time |
| 39 | Emit is called. Example: |
| 40 | |
| 41 | self.assertEqual(mock_core.emit_calls[0], [datatypes.Event({})]) |
| 42 | """ |
| 43 | |
| 44 | def __init__(self): |
chuntsen | e014a2c | 2017-03-17 14:02:49 +0800 | [diff] [blame] | 45 | self._att_dir = tempfile.mkdtemp(prefix='instalog_testing_') |
Joel Kitching | 5825663 | 2016-11-15 15:47:42 +0800 | [diff] [blame] | 46 | self.emit_calls = [] |
| 47 | self.streams = [] |
| 48 | |
Joel Kitching | 9405014 | 2017-01-30 12:20:04 +0800 | [diff] [blame] | 49 | def Close(self): |
| 50 | """Performs any final operations.""" |
| 51 | shutil.rmtree(self._att_dir) |
| 52 | |
Joel Kitching | e1e276f | 2016-12-13 13:05:33 +0800 | [diff] [blame] | 53 | def AllStreamsExpired(self): |
| 54 | """Returns True if all streams are currently expired.""" |
| 55 | return all([stream.expired for stream in self.streams]) |
| 56 | |
Joel Kitching | 5825663 | 2016-11-15 15:47:42 +0800 | [diff] [blame] | 57 | def Emit(self, plugin, events): |
| 58 | """Stores the events from this Emit call into self.emit_calls.""" |
| 59 | del plugin |
Joel Kitching | 9405014 | 2017-01-30 12:20:04 +0800 | [diff] [blame] | 60 | for event in events: |
| 61 | # Move attachments to a temporary directory to simulate buffer. |
Yilin Yang | 879fbda | 2020-05-14 13:52:30 +0800 | [diff] [blame^] | 62 | for att_id, att_path in event.attachments.items(): |
Joel Kitching | 9405014 | 2017-01-30 12:20:04 +0800 | [diff] [blame] | 63 | # Use a filename that contains the original one for clarity. |
Hung-Te Lin | 3afccc7 | 2017-06-01 22:37:33 +0800 | [diff] [blame] | 64 | tmp_path = file_utils.CreateTemporaryFile( |
chuntsen | e014a2c | 2017-03-17 14:02:49 +0800 | [diff] [blame] | 65 | prefix=os.path.basename(att_path) + '_', dir=self._att_dir) |
Joel Kitching | 9405014 | 2017-01-30 12:20:04 +0800 | [diff] [blame] | 66 | # Relocate the attachment and update the event path. |
| 67 | logging.debug('Moving attachment %s --> %s...', att_path, tmp_path) |
| 68 | shutil.move(att_path, tmp_path) |
| 69 | event.attachments[att_id] = tmp_path |
Joel Kitching | 5825663 | 2016-11-15 15:47:42 +0800 | [diff] [blame] | 70 | self.emit_calls.append(events) |
Joel Kitching | 043db1a | 2017-01-29 14:52:03 +0800 | [diff] [blame] | 71 | return True |
Joel Kitching | 5825663 | 2016-11-15 15:47:42 +0800 | [diff] [blame] | 72 | |
| 73 | def GetStream(self, stream_id): |
| 74 | """Retrieves the stream with the given ID, creating if necessary.""" |
| 75 | assert stream_id >= 0 and stream_id <= len(self.streams) |
| 76 | if stream_id < len(self.streams): |
| 77 | return self.streams[stream_id] |
Yilin Yang | 15a3f8f | 2020-01-03 17:49:00 +0800 | [diff] [blame] | 78 | stream = MockBufferEventStream() |
| 79 | self.streams.append(stream) |
| 80 | return stream |
Joel Kitching | 5825663 | 2016-11-15 15:47:42 +0800 | [diff] [blame] | 81 | |
| 82 | def NewStream(self, plugin): |
| 83 | """Returns the next available EventStream (with Events in it).""" |
| 84 | del plugin |
| 85 | ret_stream = None |
| 86 | # First, look for an expired stream with events in it. |
| 87 | for stream in self.streams: |
Joel Kitching | 75fe8b3 | 2016-11-25 13:11:49 +0800 | [diff] [blame] | 88 | if stream.expired and not stream.Empty(): |
Joel Kitching | 5825663 | 2016-11-15 15:47:42 +0800 | [diff] [blame] | 89 | ret_stream = stream |
| 90 | |
| 91 | # Next, fall back to an expired stream without events. |
| 92 | if not ret_stream: |
| 93 | for stream in self.streams: |
| 94 | if stream.expired: |
| 95 | ret_stream = stream |
| 96 | |
| 97 | # Finally, if all streams are in use, create a new one. |
| 98 | if not ret_stream: |
| 99 | ret_stream = self.GetStream(len(self.streams)) |
| 100 | |
| 101 | # Set to expired and return. |
| 102 | ret_stream.expired = False |
| 103 | logging.debug('NewStream returns: %s', ret_stream) |
| 104 | return ret_stream |
| 105 | |
Joel Kitching | 75fe8b3 | 2016-11-25 13:11:49 +0800 | [diff] [blame] | 106 | def GetProgress(self, plugin): |
| 107 | """Returns the progress of the plugin through available events. |
| 108 | |
| 109 | Normally returns a tuple (completed_count, total_count), but for testing we |
| 110 | don't require such a fine granularity of information. Thus, we will simply |
| 111 | return (0, 1) for incomplete, and (1, 1) for complete. Completion is |
| 112 | defined by all streams being empty. |
| 113 | """ |
| 114 | del plugin |
| 115 | for stream in self.streams: |
| 116 | if not stream.Empty(): |
| 117 | return 0, 1 |
| 118 | return 1, 1 |
| 119 | |
Joel Kitching | 8c3d31a | 2016-12-12 16:09:48 +0800 | [diff] [blame] | 120 | def GetNodeID(self): |
| 121 | """Returns a fake node ID.""" |
| 122 | return 'testing' |
| 123 | |
Joel Kitching | 5825663 | 2016-11-15 15:47:42 +0800 | [diff] [blame] | 124 | |
| 125 | class MockBufferEventStream(plugin_base.BufferEventStream): |
| 126 | """Implements a mock BufferEventStream class.""" |
| 127 | |
| 128 | def __init__(self): |
| 129 | self.expired = True |
Joel Kitching | 75fe8b3 | 2016-11-25 13:11:49 +0800 | [diff] [blame] | 130 | self.queue = [] |
| 131 | self.consumed = [] |
Joel Kitching | 5825663 | 2016-11-15 15:47:42 +0800 | [diff] [blame] | 132 | |
| 133 | def Queue(self, events): |
Joel Kitching | 75fe8b3 | 2016-11-25 13:11:49 +0800 | [diff] [blame] | 134 | """Queues the supplied events.""" |
| 135 | logging.debug('%s: Pushing %d events...', self, len(events)) |
| 136 | self.queue.extend(events) |
| 137 | |
| 138 | def Empty(self): |
| 139 | """Returns whether or not there are events in this EventStream.""" |
| 140 | return len(self.queue) == 0 and len(self.consumed) == 0 |
Joel Kitching | 5825663 | 2016-11-15 15:47:42 +0800 | [diff] [blame] | 141 | |
| 142 | def Next(self): |
Joel Kitching | 75fe8b3 | 2016-11-25 13:11:49 +0800 | [diff] [blame] | 143 | """Pops the next available Event or returns None if not available.""" |
Joel Kitching | 5825663 | 2016-11-15 15:47:42 +0800 | [diff] [blame] | 144 | if self.expired: |
| 145 | raise plugin_base.EventStreamExpired |
Peter Shih | 19a938f | 2018-02-26 14:26:16 +0800 | [diff] [blame] | 146 | if not self.queue: |
Joel Kitching | 5825663 | 2016-11-15 15:47:42 +0800 | [diff] [blame] | 147 | logging.debug('%s: Nothing to pop', self) |
| 148 | return None |
Joel Kitching | 75fe8b3 | 2016-11-25 13:11:49 +0800 | [diff] [blame] | 149 | ret = self.queue.pop(0) |
| 150 | self.consumed.append(ret) |
Joel Kitching | 9405014 | 2017-01-30 12:20:04 +0800 | [diff] [blame] | 151 | logging.debug('%s: Popping next event...', self) |
chuntsen | 71a9251 | 2017-04-20 14:10:45 +0800 | [diff] [blame] | 152 | return copy.deepcopy(ret) |
Joel Kitching | 5825663 | 2016-11-15 15:47:42 +0800 | [diff] [blame] | 153 | |
| 154 | def Commit(self): |
Joel Kitching | 75fe8b3 | 2016-11-25 13:11:49 +0800 | [diff] [blame] | 155 | """Marks the EventStream as committed and expired.""" |
Joel Kitching | 5825663 | 2016-11-15 15:47:42 +0800 | [diff] [blame] | 156 | if self.expired: |
| 157 | raise plugin_base.EventStreamExpired |
Joel Kitching | 75fe8b3 | 2016-11-25 13:11:49 +0800 | [diff] [blame] | 158 | self.consumed = [] |
Joel Kitching | 5825663 | 2016-11-15 15:47:42 +0800 | [diff] [blame] | 159 | self.expired = True |
| 160 | |
| 161 | def Abort(self): |
Joel Kitching | 75fe8b3 | 2016-11-25 13:11:49 +0800 | [diff] [blame] | 162 | """Marks the EventStream as aborted and expired.""" |
Joel Kitching | 5825663 | 2016-11-15 15:47:42 +0800 | [diff] [blame] | 163 | if self.expired: |
| 164 | raise plugin_base.EventStreamExpired |
Joel Kitching | 75fe8b3 | 2016-11-25 13:11:49 +0800 | [diff] [blame] | 165 | self.queue = self.consumed + self.queue |
| 166 | self.consumed = [] |
Joel Kitching | 5825663 | 2016-11-15 15:47:42 +0800 | [diff] [blame] | 167 | self.expired = True |