diff --git a/README.md b/README.md index bc1ff6e..902a1cd 100644 --- a/README.md +++ b/README.md @@ -92,17 +92,17 @@ MPcurses( Executing the code above results in the following: ![example](https://raw.githubusercontent.com/soda480/mpcurses/master/docs/images/example-multi.gif) -Serveral [examples](/examples) are included to help introduce the mpcurses library. Note the functions contained in all the examples are Python functions that have no context about multiprocessing or curses, they simply perform a function on a given dataset. Mpcurses takes care of setting up the multiprocessing, configuring the curses screen and maintaining the thread-safe queues that are required for inter-process communication. +Serveral [examples](https://github.com/soda480/mpcurses/tree/master/examples) are included to help introduce the mpcurses library. Note the functions contained in all the examples are Python functions that have no context about multiprocessing or curses, they simply perform a function on a given dataset. Mpcurses takes care of setting up the multiprocessing, configuring the curses screen and maintaining the thread-safe queues that are required for inter-process communication. -#### [example1](/examples/example1.py) +#### [example1](https://github.com/soda480/mpcurses/blob/master/examples/example1.py) Execute a function that processes a list of random items. The screen maintains indicators showing the number of items that have been processed. Two lists are maintained displaying the items that had errors and warnings. ![example1](https://raw.githubusercontent.com/soda480/mpcurses/master/docs/images/example1.gif) -#### [example2](/examples/example2.py) +#### [example2](https://github.com/soda480/mpcurses/blob/master/examples/example2.py) Execute a function that processes a list of random items. Execution is scaled across three processes where each is responsible for processing items for a particular group. The screen maintains indicators displaying the items that had errors and warnings for each group. ![example2](https://raw.githubusercontent.com/soda480/mpcurses/master/docs/images/example2.gif) -#### [example3](/examples/example3.py) +#### [example3](https://github.com/soda480/mpcurses/blob/master/examples/example3.py) Execute a function that calculates prime numbers for a set range of integers. Execution is scaled across 10 different processes where each process computes the primes on a different set of numbers. For example, the first process computes primes for the set 1-10K, second process 10K-20K, third process 20K-30K, etc. The screen keeps track of the number of prime numbers encountered for each set and maintains a progress bar for each process. ![example3](https://raw.githubusercontent.com/soda480/mpcurses/master/docs/images/example3.gif) diff --git a/build.py b/build.py index 98ba31e..9c2f896 100644 --- a/build.py +++ b/build.py @@ -31,7 +31,7 @@ Author('Emilio Reyes', 'emilio.reyes@intel.com')] summary = 'Mpcurses is an abstraction of the Python curses and multiprocessing libraries providing function execution and runtime visualization capabilities' url = 'https://github.com/soda480/mpcurses' -version = '0.2.2' +version = '0.2.3' default_task = [ 'clean', 'analyze', diff --git a/examples/example5.py b/examples/example5.py index 3c43c90..3b437ac 100644 --- a/examples/example5.py +++ b/examples/example5.py @@ -20,7 +20,23 @@ def get_hex(): return uuid.uuid4().hex.upper() -def get_servers(bays): +def get_servers(bays=None): + """ getting server data from enclosure + """ + servers = [] + for bay in bays: + servers.append({ + 'bay': str(bay).zfill(2), + 'firmware version': get_current_firmware(), + 'servername': 'srv{}.company.com'.format(get_hex()[0:6]), + }) + sleep(5) + return servers + +def get_servers2(**kwargs): + """ getting server data from enclosure if being passed as get_process_data value to MPcurses then should accept **kwargs as argument + """ + bays = range(1, 17) servers = [] for bay in bays: servers.append({ @@ -28,9 +44,11 @@ def get_servers(bays): 'firmware version': get_current_firmware(), 'servername': 'srv{}.company.com'.format(get_hex()[0:6]), }) + sleep(5) return servers + def simulate_error(value, message): if value > 9: raise Exception(message) @@ -96,15 +114,15 @@ def get_current_firmware(): def main(): """ main program """ - bays = range(1,17) - process_data = get_servers(bays) - MPcurses( + mpcurses = MPcurses( function=update_firmware, - process_data=process_data, + get_process_data=get_servers, processes_to_start=5, - screen_layout=get_screen_layout()).execute() + screen_layout=get_screen_layout(), + shared_data={'bays': range(1, 17)}) + mpcurses.execute() - if any([process['result'] for process in process_data]): + if any([process['result'] for process in mpcurses.process_data]): sys.exit(-1) diff --git a/src/main/python/mpcurses/__init__.py b/src/main/python/mpcurses/__init__.py index bab2ce3..5c8be0d 100644 --- a/src/main/python/mpcurses/__init__.py +++ b/src/main/python/mpcurses/__init__.py @@ -15,5 +15,4 @@ from .handler import queue_handler -from .process import execute from .mpcurses import MPcurses diff --git a/src/main/python/mpcurses/mpcurses.py b/src/main/python/mpcurses/mpcurses.py index 56a1a59..7fc0fde 100644 --- a/src/main/python/mpcurses/mpcurses.py +++ b/src/main/python/mpcurses/mpcurses.py @@ -24,11 +24,12 @@ from queue import Empty from .screen import initialize_screen +from .screen import initialize_screen_offsets from .screen import finalize_screen from .screen import update_screen from .screen import echo_to_screen from .screen import refresh_screen -from .screen import validate_screen_layout +from .screen import validate_screen_layout_processes from .screen import update_screen_status from .screen import blink from .handler import queue_handler @@ -77,28 +78,38 @@ def pop(self, *args): class MPcurses(): """ mpcurses process pool """ - def __init__(self, function, *, process_data=None, shared_data=None, processes_to_start=None, screen_layout=None, init_messages=None, setup_process_queue=True): + def __init__(self, function, *, process_data=None, shared_data=None, processes_to_start=None, screen_layout=None, init_messages=None, get_process_data=None): """ MPCstate constructor """ - if getattr(function, '__name__', None) == '_queue_handler': - # enable backwards compatibility for use cases where - # function was already decorated with queue_handler - # NOTE: this does not work for functions with multiple decorators - logger.debug('function is already decorated with queue_handler') - self.function = function - else: - logger.debug(f'decorating function {function.__name__} with queue_handler') - self.function = queue_handler(function) + if process_data and get_process_data: + raise ValueError('process_data and get_process_data values cannot both be set') - self.process_data = [{}] if process_data is None else process_data + if get_process_data and not callable(get_process_data): + raise ValueError('get_process_data value must be a callable function') - self.shared_data = {} if shared_data is None else shared_data + if get_process_data and not screen_layout: + raise ValueError('get_process_data can only be set if screen_layout value is provided') - self.processes_to_start = len(self.process_data) if processes_to_start is None else processes_to_start + logger.debug(f'decorating function {function.__name__} with queue_handler') + self.function = queue_handler(function) - self.active_processes = OnDict(on_change=self.on_state_change) + self.screen_layout = screen_layout + + self.processes_to_start = processes_to_start + + self.get_process_data = get_process_data + + if self.get_process_data: + self.process_data = None + else: + # things we can set when get_process_data is not specified + self.process_data = [{}] if process_data is None else process_data + if not self.processes_to_start: + self.processes_to_start = len(self.process_data) + + self.shared_data = {} if shared_data is None else shared_data - self.process_data_offset = [(self.process_data.index(item), item) for item in self.process_data] + self.active_processes = OnDict(on_change=self.on_state_change) self.message_queue = Queue() @@ -106,15 +117,8 @@ def __init__(self, function, *, process_data=None, shared_data=None, processes_t self.process_queue = SimpleQueue() - if screen_layout: - validate_screen_layout(len(self.process_data), self.processes_to_start, screen_layout) - self.screen_layout = screen_layout - self.init_messages = [] if init_messages is None else init_messages - if setup_process_queue: - self.setup_process_queue() - self.completed_processes = 0 self.screen = None @@ -129,15 +133,6 @@ def __init__(self, function, *, process_data=None, shared_data=None, processes_t if self.blink_screen: self.blink_queue = Queue() - def setup_process_queue(self): - """ return queue containing data for each process - """ - logger.debug('populating the process queue') - for item in self.process_data_offset: - logger.debug(f'adding {item} to the process queue') - self.process_queue.put(item) - logger.debug(f'added {self.process_queue.qsize()} items to the process queue') - def start_blink_process(self): """ start blink process """ @@ -156,9 +151,21 @@ def stop_blink_process(self): self.blink_process.terminate() logger.debug('terminated blink process') + def populate_process_queue(self): + """ populate process queue from process data offset + """ + logger.debug('populating the process queue') + for offset, data in enumerate(self.process_data): + item = (offset, data) + logger.debug(f'adding {item} to the process queue') + self.process_queue.put(item) + logger.debug(f'added {self.process_queue.qsize()} items to the process queue') + def start_processes(self): """ start processes """ + self.populate_process_queue() + logger.debug(f'there are {self.process_queue.qsize()} items in the process queue') logger.debug(f'starting {self.processes_to_start} background processes') for _ in range(self.processes_to_start): @@ -180,8 +187,7 @@ def start_next_process(self): kwargs={ 'message_queue': self.message_queue, 'offset': offset, - 'result_queue': self.result_queue - }) + 'result_queue': self.result_queue}) # logger.debug(f'starting background process at offset {offset} with data {process_data}') process.start() logger.info(f'started background process at offset {offset} with process id {process.pid}') @@ -240,22 +246,39 @@ def on_state_change(self, process_completed=True): queued=self.process_queue.qsize(), completed=self.completed_processes) + def execute_get_process_data(self): + """ execute get_process_data function + """ + if self.get_process_data: + doc = self.get_process_data.__doc__ + update_screen_status(self.screen, 'get-process-data', self.screen_layout['_screen'], data=doc) + kwargs = self.shared_data if self.shared_data else {} + self.process_data = self.get_process_data(**kwargs) + update_screen_status(self.screen, 'get-process-data', self.screen_layout['_screen']) + if not self.processes_to_start: + self.processes_to_start = len(self.process_data) + def setup_screen(self): """ setup screen """ - initialize_screen(self.screen, self.screen_layout, len(self.process_data_offset)) + initialize_screen(self.screen, self.screen_layout) + + self.execute_get_process_data() + + # initialize screen with offsets + initialize_screen_offsets(self.screen, self.screen_layout, len(self.process_data), self.processes_to_start) # update screen with all initialization messages if they were provided for message in self.init_messages: update_screen(message, self.screen, self.screen_layout) - # echo all process data to screen - for index, data in enumerate(self.process_data_offset): - echo_to_screen(self.screen, data[1], self.screen_layout, offset=index) - # echo shared data to screen echo_to_screen(self.screen, self.shared_data, self.screen_layout) + # echo all process data to screen + for offset, data in enumerate(self.process_data): + echo_to_screen(self.screen, data, self.screen_layout, offset=offset) + self.start_blink_process() def teardown_screen(self): @@ -292,10 +315,7 @@ def get_message(self): # if blink is enabled then process blink message first blink_message = self.get_blink_message() if blink_message: - update_screen_status( - self.screen, - blink_message, - self.screen_layout['_screen']) + update_screen_status(self.screen, blink_message, self.screen_layout['_screen']) offset = None control = None @@ -307,8 +327,7 @@ def get_message(self): return { 'offset': offset, 'control': control, - 'message': message - } + 'message': message} def process_control_message(self, offset, control): """ process control message diff --git a/src/main/python/mpcurses/process.py b/src/main/python/mpcurses/process.py deleted file mode 100644 index d25e620..0000000 --- a/src/main/python/mpcurses/process.py +++ /dev/null @@ -1,48 +0,0 @@ - -# Copyright (c) 2021 Intel Corporation - -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at - -# http://www.apache.org/licenses/LICENSE-2.0 - -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import logging -from .mpcurses import MPcurses - -logger = logging.getLogger(__name__) - - -def execute(function=None, process_data=None, shared_data=None, number_of_processes=None, init_messages=None, screen_layout=None): - """ public execute api - spawns child processes as dictated by process data and manages displaying spawned process messages to screen if screen layout is defined - - wrapped with KeyboardInterrupt exception to enable user to submit Ctrl+C interrupt to kill all running processes - supports 'silent mode' if caller does not specify a screen layout - - Parameters: - function (callable): callable object that each spawned process will execute as their target - process_data (list): list of dict where each dict contains meta-data specific to a process - [{}, {}, {}] - shared_data (dict): data to provide all spawned processes - number_of_processes (int): number of processes to spawn - init_messages (list): list of messages to send screen upon startup - screen_layout (dict): dictionary containing meta-data for how logged messages for each spawned process should be displayed on screen - Returns: - - - - THIS METHOD WILL BE DEPCRATED - use MPcurses class instead as dictated below - """ - mpcurses = MPcurses( - function=function, - process_data=process_data, - shared_data=shared_data, - processes_to_start=number_of_processes, - init_messages=init_messages, - screen_layout=screen_layout) - mpcurses.execute() diff --git a/src/main/python/mpcurses/screen.py b/src/main/python/mpcurses/screen.py index eab0937..8945262 100644 --- a/src/main/python/mpcurses/screen.py +++ b/src/main/python/mpcurses/screen.py @@ -108,7 +108,7 @@ def initialize_keep_count(category, offsets, screen_layout): screen_layout[category]['_count'] = 0 -def update_screen_status(screen, state, config, running=None, queued=None, completed=None): +def update_screen_status(screen, state, config, running=None, queued=None, completed=None, data=None): """ update screen status """ height, width = screen.getmaxyx() @@ -116,37 +116,39 @@ def update_screen_status(screen, state, config, running=None, queued=None, compl color = config['color'] if state == 'initialize': - title = config['title'] + text = config['title'] screen.addstr(0, 0, ' ' * (width - 1), curses.color_pair(color)) - screen.addstr(0, width - len(title) - 1, title, curses.color_pair(color)) - + screen.addstr(0, width - len(text) - 1, text, curses.color_pair(color)) elif state == 'finalize': - qtext = '[Press q to exit]' - screen.addstr(0, 1, qtext, curses.color_pair(color)) - + text = '[Press q to exit]' + screen.addstr(0, 1, text, curses.color_pair(color)) elif state == 'blink-on': - rtext = 'RUNNING' - screen.addstr(0, 1, rtext, curses.color_pair(color)) - + text = 'RUNNING' + screen.addstr(0, 1, text, curses.color_pair(color)) elif state == 'blink-off': - rtext = 'RUNNING' - screen.addstr(0, 1, ' ' * len(rtext), curses.color_pair(color)) + text = 'RUNNING' + screen.addstr(0, 1, ' ' * len(text), curses.color_pair(color)) + elif state == 'get-process-data': + if data: + text = f'{data.splitlines()[0].strip().capitalize()}... this may take awhile' + y_pos = int((height // 2) - 2) + x_pos = int((width // 2) - (len(text) // 2) - len(text) % 2) + screen.addstr(y_pos, x_pos, text, curses.color_pair(color)) + else: + y_pos = int((height // 2) - 2) + x_pos = 0 + screen.move(y_pos, x_pos) + screen.clrtoeol() if state in ('initialize', 'process-update'): - - if config['show_process_status']: - - zfill = config['zfill'] - + if config.get('show_process_status'): if running is None: running = 0 - if queued is None: queued = 0 - if completed is None: completed = 0 - + zfill = config['zfill'] rtext = f' Running: {str(running).zfill(zfill)}' screen.addstr(height - 4, 1, rtext, curses.color_pair(color)) qtext = f' Queued: {str(queued).zfill(zfill)}' @@ -157,13 +159,26 @@ def update_screen_status(screen, state, config, running=None, queued=None, compl screen.refresh() -def initialize_screen(screen, screen_layout, offsets): +def initialize_screen(screen, screen_layout): """ initialize screen """ logger.debug('initializing screen') + + set_screen_defaults(screen_layout) validate_screen_size(screen, screen_layout) + initialize_colors() curses.curs_set(0) + update_screen_status(screen, 'initialize', screen_layout['_screen']) + + +def initialize_screen_offsets(screen, screen_layout, offsets, processes_to_start): + """ initialize screen offsets + """ + logger.debug('initializing screen offsets') + + set_screen_defaults_processes(offsets, processes_to_start, screen_layout) + validate_screen_layout_processes(offsets, screen_layout) for category, data in screen_layout.items(): if category == '_counter_': @@ -176,13 +191,14 @@ def initialize_screen(screen, screen_layout, offsets): if data.get('keep_count'): initialize_keep_count(category, offsets, screen_layout) - update_screen_status(screen, 'initialize', screen_layout['_screen']) + update_screen_status(screen, 'process-update', screen_layout['_screen']) def finalize_screen(screen, screen_layout): """ finalize screen """ logger.debug('finalizing screen') + update_screen_status(screen, 'finalize', screen_layout['_screen']) while True: char = screen.getch() @@ -467,32 +483,40 @@ def squash_table(screen_layout, delta): update_positions(screen_layout, positions) -def set_screen_defaults(processes, processes_to_start, screen_layout): +def set_screen_defaults(screen_layout): """ set screen defaults """ + logger.debug('setting screen defaults') + if '_screen' not in screen_layout: screen_layout['_screen'] = {} if 'title' not in screen_layout['_screen']: screen_layout['_screen']['title'] = sys.argv[0] - if 'zfill' not in screen_layout['_screen']: - screen_layout['_screen']['zfill'] = len(str(processes)) - if 'color' not in screen_layout['_screen']: screen_layout['_screen']['color'] = 11 - if 'show_process_status' not in screen_layout['_screen']: - screen_layout['_screen']['show_process_status'] = processes_to_start < processes - if 'blink' not in screen_layout['_screen']: screen_layout['_screen']['blink'] = True -def validate_screen_layout(processes, processes_to_start, screen_layout): +def set_screen_defaults_processes(processes, processes_to_start, screen_layout): + """ set screen defaults + """ + logger.debug('setting screen defaults processes') + + if 'zfill' not in screen_layout['_screen']: + screen_layout['_screen']['zfill'] = len(str(processes)) + + if 'show_process_status' not in screen_layout['_screen']: + screen_layout['_screen']['show_process_status'] = processes_to_start < processes + + +def validate_screen_layout_processes(processes, screen_layout): """ validate screen layout """ - set_screen_defaults(processes, processes_to_start, screen_layout) + logger.debug('validating screen layout processes') table = screen_layout.get('table') if not table: @@ -513,6 +537,8 @@ def validate_screen_layout(processes, processes_to_start, screen_layout): def validate_screen_size(screen, screen_layout): """ validate current screen size is large enough for screen layout """ + logger.debug('validating screen size') + screen_height, screen_width = screen.getmaxyx() max_y_pos = 0 max_x_pos = 0 diff --git a/src/unittest/python/test_mpcurses.py b/src/unittest/python/test_mpcurses.py index 99afb79..1d9a784 100644 --- a/src/unittest/python/test_mpcurses.py +++ b/src/unittest/python/test_mpcurses.py @@ -43,45 +43,50 @@ def tearDown(self): """ pass - @patch('mpcurses.MPcurses.setup_process_queue') - def test__init_Should_CallSetupProcessQueue_When_SetupProcessQueue(self, setup_process_queue_patch, *patches): - MPcurses(function=Mock(__name__='mockfunc')) - setup_process_queue_patch.assert_called_once_with() - - @patch('mpcurses.mpcurses.validate_screen_layout') - def test__init_Should_CallValidateScreenLayout_When_ScreenLayout(self, validate_screen_layout_patch, *patches): - screen_layout_mock = {'_screen': {}} - MPcurses(function=Mock(__name__='mockfunc'), screen_layout=screen_layout_mock, setup_process_queue=False) - validate_screen_layout_patch.assert_called_once_with(1, 1, screen_layout_mock) - def test__init_Should_SetDefaults_When_Called(self, *patches): - client = MPcurses(function=Mock(__name__='mockfunc'), setup_process_queue=False) + client = MPcurses(function=Mock(__name__='mockfunc')) self.assertEqual(client.process_data, [{}]) self.assertEqual(client.shared_data, {}) self.assertEqual(client.processes_to_start, 1) self.assertEqual(client.init_messages, []) - def test__init_Should_SetDefaults_When_FunctionWrapped(self, *patches): - function_mock = Mock(__name__='_queue_handler') - client = MPcurses(function=function_mock, setup_process_queue=False) - self.assertEqual(client.process_data, [{}]) - self.assertEqual(client.shared_data, {}) - self.assertEqual(client.processes_to_start, 1) - self.assertEqual(client.function, function_mock) - @patch('mpcurses.mpcurses.queue_handler') def test__init_Should_SetDefaults_When_FunctionNotWrapped(self, queue_handler_patch, *patches): function_mock = Mock(__name__='mockfunc') - client = MPcurses(function=function_mock, setup_process_queue=False) + client = MPcurses(function=function_mock) self.assertEqual(client.process_data, [{}]) self.assertEqual(client.shared_data, {}) self.assertEqual(client.processes_to_start, 1) self.assertEqual(client.function, queue_handler_patch(function_mock)) - def test__setup_process_queue_Should_AddToProcessQueue_When_Called(self, *patches): + def test__init__Should_RaiseException_When_ProcessDataAndGetProcessData(self, *patches): + function_mock = Mock(__name__='_queue_handler') + with self.assertRaises(Exception): + MPcurses(function_mock, process_data=[{}], get_process_data=Mock()) + + def test__init__Should_RaiseException_When_GetProcessDataNotCallable(self, *patches): + function_mock = Mock(__name__='_queue_handler') + get_process_data_mock = 'mock' + with self.assertRaises(Exception): + MPcurses(function_mock, get_process_data=get_process_data_mock) + + def test__init__Should_RaiseException_When_GetProcessDataNoScreenLayout(self, *patches): + function_mock = Mock(__name__='_queue_handler') + get_process_data_mock = Mock() + with self.assertRaises(Exception): + MPcurses(function_mock, get_process_data=get_process_data_mock) + + def test__init_Should_SetDefaults_When_GetProcessData(self, *patches): + function_mock = Mock(__name__='mockfunc') + get_process_data_mock = Mock() + client = MPcurses(function=function_mock, get_process_data=get_process_data_mock, screen_layout={'_screen': {}}) + self.assertIsNone(client.process_data) + self.assertEqual(client.get_process_data, get_process_data_mock) + + def test__populate_process_queue_Should_AddToProcessQueue_When_Called(self, *patches): process_data = [{'range': '0-1'}, {'range': '2-3'}, {'range': '4-5'}] - client = MPcurses(function=Mock(__name__='mockfunc'), process_data=process_data, setup_process_queue=False) - client.setup_process_queue() + client = MPcurses(function=Mock(__name__='mockfunc'), process_data=process_data) + client.populate_process_queue() self.assertEqual(client.process_queue.qsize(), 3) @patch('mpcurses.mpcurses.blink') @@ -135,15 +140,12 @@ def test__start_processes_Should_CallStartNextProcess_When_Called(self, start_ne client.start_processes() self.assertEqual(len(start_next_process_patch.mock_calls), 2) + @patch('mpcurses.MPcurses.populate_process_queue') @patch('mpcurses.MPcurses.start_next_process') def test__start_processes_Should_CallStartNextProcess_When_ProcessesToStartGreaterThanProcessQueueSize(self, start_next_process_patch, *patches): function_mock = Mock(__name__='mockfunc') process_data = [{'range': '0-1'}, {'range': '2-3'}, {'range': '4-5'}] client = MPcurses(function=function_mock, process_data=process_data) - # remove all processes from queue - client.process_queue.get() - client.process_queue.get() - client.process_queue.get() client.start_processes() self.assertEqual(len(start_next_process_patch.mock_calls), 0) @@ -156,6 +158,7 @@ def test__start_next_process_Should_CallExpected_When_Called(self, process_patch function_mock = Mock(__name__='mockfunc') process_data = [{'range': '0-1'}, {'range': '2-3'}, {'range': '4-5'}] client = MPcurses(function=function_mock, process_data=process_data, shared_data='--shared-data--') + client.populate_process_queue() client.start_next_process() process_patch.assert_called_once_with( @@ -182,6 +185,7 @@ def test__purge_process_queue_Should_PurgeProcessQueue_When_Called(self, *patche function_mock = Mock(__name__='mockfunc') process_data = [{'range': '0-1'}, {'range': '2-3'}, {'range': '4-5'}] client = MPcurses(function=function_mock, process_data=process_data) + client.populate_process_queue() self.assertEqual(client.process_queue.qsize(), 3) client.purge_process_queue() self.assertTrue(client.process_queue.empty()) @@ -240,7 +244,46 @@ def test__on_state_change_Should_CallExpected_When_NoScreen(self, update_screen_ client.on_state_change(process_completed=False) update_screen_patch.assert_not_called() - @patch('mpcurses.mpcurses.validate_screen_layout') + @patch('mpcurses.mpcurses.update_screen_status') + def test__execute_get_process_data_Should_CallExpected_When_NoGetProcessData(self, update_screen_status_patch, *patches): + function_mock = Mock(__name__='mockfunc') + client = MPcurses(function=function_mock) + client.execute_get_process_data() + update_screen_status_patch.assert_not_called() + + @patch('mpcurses.mpcurses.update_screen_status') + def test__execute_get_process_data_Should_CallExpected_When_GetProcessData(self, update_screen_status_patch, *patches): + function_mock = Mock(__name__='mockfunc') + get_process_data_mock = Mock(__doc__='getting data') + get_process_data_mock.return_value = [{'data': 1}, {'data': 2}] + client = MPcurses(function=function_mock, get_process_data=get_process_data_mock, screen_layout={'_screen': {}}) + client.execute_get_process_data() + client.screen = None + call1 = call(client.screen, 'get-process-data', {}, data='getting data') + self.assertTrue(call1 in update_screen_status_patch.mock_calls) + get_process_data_mock.assert_called_once_with() + self.assertEqual(client.process_data, get_process_data_mock.return_value) + call2 = call(client.screen, 'get-process-data', {}) + self.assertTrue(call2 in update_screen_status_patch.mock_calls) + self.assertEqual(client.processes_to_start, 2) + + @patch('mpcurses.mpcurses.update_screen_status') + def test__execute_get_process_data_Should_CallExpected_When_GetProcessDataStartProcesses(self, update_screen_status_patch, *patches): + function_mock = Mock(__name__='mockfunc') + get_process_data_mock = Mock(__doc__='getting data') + get_process_data_mock.return_value = [{'data': 1}, {'data': 2}] + client = MPcurses(function=function_mock, get_process_data=get_process_data_mock, screen_layout={'_screen': {}}, processes_to_start=1, shared_data={'arg1': 'value1', 'arg2': 'value2'}) + client.execute_get_process_data() + client.screen = None + call1 = call(client.screen, 'get-process-data', {}, data='getting data') + self.assertTrue(call1 in update_screen_status_patch.mock_calls) + get_process_data_mock.assert_called_once_with(arg1='value1', arg2='value2') + self.assertEqual(client.process_data, get_process_data_mock.return_value) + call2 = call(client.screen, 'get-process-data', {}) + self.assertTrue(call2 in update_screen_status_patch.mock_calls) + self.assertEqual(client.processes_to_start, 1) + + @patch('mpcurses.mpcurses.initialize_screen_offsets') @patch('mpcurses.MPcurses.start_blink_process') @patch('mpcurses.mpcurses.initialize_screen') @patch('mpcurses.mpcurses.echo_to_screen') @@ -263,7 +306,7 @@ def test__setup_screen_Should_CallExpected_When_Called(self, update_screen_patch echo_to_screen_call2 = call(screen_mock, '--shared-data--', screen_layout_mock) self.assertTrue(echo_to_screen_call2 in echo_to_screen_patch.mock_calls) - @patch('mpcurses.mpcurses.validate_screen_layout') + @patch('mpcurses.mpcurses.initialize_screen_offsets') @patch('mpcurses.MPcurses.start_blink_process') @patch('mpcurses.mpcurses.initialize_screen') @patch('mpcurses.mpcurses.echo_to_screen') @@ -280,7 +323,7 @@ def test__setup_screen_Should_CallExpected_When_NoInitMessages(self, update_scre echo_to_screen_call1 = call(screen_mock, {'range': '0-1'}, screen_layout_mock, offset=0) self.assertTrue(echo_to_screen_call1 in echo_to_screen_patch.mock_calls) - @patch('mpcurses.mpcurses.validate_screen_layout') + @patch('mpcurses.mpcurses.initialize_screen_offsets') @patch('mpcurses.MPcurses.stop_blink_process') @patch('mpcurses.mpcurses.finalize_screen') @patch('mpcurses.mpcurses.update_screen') @@ -304,7 +347,7 @@ def test__active_processes_empty_Should_ReturnExpected_When_Called(self, *patche client.active_processes = {} self.assertTrue(client.active_processes_empty()) - @patch('mpcurses.mpcurses.validate_screen_layout') + @patch('mpcurses.mpcurses.initialize_screen_offsets') def test__get_blink_message_Should_ReturnExpected_When_Empty(self, *patches): function_mock = Mock(__name__='_queue_handler') screen_layout = { @@ -476,7 +519,7 @@ def test__run_Should_ProcessControlMessage_When_ControlMessage(self, get_message client.run() process_control_message_patch.assert_called_once_with('0', 'DONE') - @patch('mpcurses.mpcurses.validate_screen_layout') + @patch('mpcurses.mpcurses.validate_screen_layout_processes') @patch('mpcurses.MPcurses.teardown_screen') @patch('mpcurses.MPcurses.setup_screen') @patch('mpcurses.MPcurses.start_processes') @@ -504,7 +547,6 @@ def test__run_screen_Should_CallExpected_When_Called(self, get_message_patch, pr screen_mock = Mock() client.run_screen(screen_mock) client.screen = screen_mock - print(f"***** {update_screen_patch.mock_calls}") # 1 update_screen_call1 = call('#0-this is message1', client.screen, client.screen_layout) self.assertTrue(update_screen_call1 in update_screen_patch.mock_calls) @@ -518,7 +560,7 @@ def test__run_screen_Should_CallExpected_When_Called(self, get_message_patch, pr # 5 logger_patch.info.assert_called_once_with('there are no more active processses - quitting') - @patch('mpcurses.mpcurses.validate_screen_layout') + @patch('mpcurses.mpcurses.validate_screen_layout_processes') @patch('mpcurses.MPcurses.terminate_processes') @patch('mpcurses.mpcurses.sys') @patch('mpcurses.mpcurses.wrapper') diff --git a/src/unittest/python/test_process.py b/src/unittest/python/test_process.py deleted file mode 100644 index a7a7e5d..0000000 --- a/src/unittest/python/test_process.py +++ /dev/null @@ -1,51 +0,0 @@ - -# Copyright (c) 2021 Intel Corporation - -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at - -# http://www.apache.org/licenses/LICENSE-2.0 - -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import unittest -from mock import patch -from mock import call -from mock import Mock -from mock import MagicMock - -from mpcurses.process import execute - -import logging -logger = logging.getLogger(__name__) - - -class TestProcess(unittest.TestCase): - - def setUp(self): - """ - """ - pass - - def tearDown(self): - """ - """ - pass - - @patch('mpcurses.process.MPcurses') - def test__execute_Should_CallExpected_When_Called(self, mpcurses_patch, *patches): - function_mock = Mock() - process_data = [{'range': '0-1'}] - execute(function=function_mock, process_data=process_data) - mpcurses_patch.assert_called_once_with( - function=function_mock, - process_data=process_data, - shared_data=None, - processes_to_start=None, - init_messages=None, - screen_layout=None) diff --git a/src/unittest/python/test_screen.py b/src/unittest/python/test_screen.py index bef9d3d..b63781c 100644 --- a/src/unittest/python/test_screen.py +++ b/src/unittest/python/test_screen.py @@ -24,6 +24,7 @@ from mpcurses.screen import initialize_keep_count from mpcurses.screen import update_screen_status from mpcurses.screen import initialize_screen +from mpcurses.screen import initialize_screen_offsets from mpcurses.screen import finalize_screen from mpcurses.screen import get_category_values from mpcurses.screen import sanitize_message @@ -43,7 +44,8 @@ from mpcurses.screen import update_positions from mpcurses.screen import squash_table from mpcurses.screen import set_screen_defaults -from mpcurses.screen import validate_screen_layout +from mpcurses.screen import set_screen_defaults_processes +from mpcurses.screen import validate_screen_layout_processes from mpcurses.screen import validate_screen_size from mpcurses.screen import blink @@ -270,12 +272,64 @@ def test__update_screen_status_Should_CallExpected_When_ProcessUpdate(self, colo self.assertTrue(call2 in screen_mock.addstr.mock_calls) self.assertTrue(call3 in screen_mock.addstr.mock_calls) - @patch('mpcurses.screen.update_screen_status') + @patch('mpcurses.screen.curses.color_pair') + def test__update_screen_status_Should_CallExpected_When_GetProcessDataWithData(self, color_pair_patch, *patches): + screen_mock = Mock() + screen_mock.getmaxyx.return_value = (100, 200) + config_mock = { + 'color': 0 + } + update_screen_status(screen_mock, 'get-process-data', config_mock, data=' getting some data\nrest of\n docstr\n') + call1 = call(48, 79, 'Getting some data... this may take awhile', color_pair_patch.return_value) + self.assertTrue(call1 in screen_mock.addstr.mock_calls) + + @patch('mpcurses.screen.curses.color_pair') + def test__update_screen_status_Should_CallExpected_When_GetProcessDataWithoutData(self, color_pair_patch, *patches): + screen_mock = Mock() + screen_mock.getmaxyx.return_value = (100, 200) + config_mock = { + 'color': 0 + } + update_screen_status(screen_mock, 'get-process-data', config_mock) + screen_mock.move.assert_called_once_with(48, 0) + screen_mock.clrtoeol.assert_called_once_with() + @patch('mpcurses.screen.curses.curs_set') - @patch('mpcurses.screen.validate_screen_size') @patch('mpcurses.screen.initialize_colors') + @patch('mpcurses.screen.update_screen_status') + @patch('mpcurses.screen.validate_screen_size') + @patch('mpcurses.screen.set_screen_defaults') + def test__initialize_screen_Should_CallExpected_When_Called(self, set_screen_defaults_patch, validate_screen_size_patch, *patches): + screen_mock = Mock() + screen_layout_mock = { + '_screen': { + }, + '_counter_': { + } + } + initialize_screen(screen_mock, screen_layout_mock) + set_screen_defaults_patch.assert_called_once_with(screen_layout_mock) + validate_screen_size_patch.assert_called_once_with(screen_mock, screen_layout_mock) + + @patch('mpcurses.screen.update_screen_status') + @patch('mpcurses.screen.validate_screen_layout_processes') + @patch('mpcurses.screen.set_screen_defaults_processes') + def test__initialize_screen_offsets_Should_CallExpected_When_Called(self, set_screen_defaults_processes_patch, validate_screen_layout_processes_patch, update_screen_status_patch, *patches): + screen_mock = Mock() + screen_layout_mock = { + '_screen': { + } + } + initialize_screen_offsets(screen_mock, screen_layout_mock, 100, 10) + set_screen_defaults_processes_patch.assert_called_once_with(100, 10, screen_layout_mock) + validate_screen_layout_processes_patch.assert_called_once_with(100, screen_layout_mock) + update_screen_status_patch.assert_called_once_with(screen_mock, 'process-update', screen_layout_mock['_screen']) + + @patch('mpcurses.screen.update_screen_status') + @patch('mpcurses.screen.validate_screen_layout_processes') + @patch('mpcurses.screen.set_screen_defaults_processes') @patch('mpcurses.screen.initialize_counter') - def test__initialize_screen_Should_CallExpected_When_CounterCategory(self, initialize_counter_patch, *patches): + def test__initialize_screen_offsets_Should_CallExpected_When_CounterCategory(self, initialize_counter_patch, *patches): screen_mock = Mock() screen_layout_mock = { '_screen': { @@ -283,15 +337,14 @@ def test__initialize_screen_Should_CallExpected_When_CounterCategory(self, initi '_counter_': { } } - initialize_screen(screen_mock, screen_layout_mock, 1) + initialize_screen_offsets(screen_mock, screen_layout_mock, 1, 1) initialize_counter_patch.assert_called_once_with(1, screen_layout_mock) @patch('mpcurses.screen.update_screen_status') - @patch('mpcurses.screen.curses.curs_set') - @patch('mpcurses.screen.validate_screen_size') - @patch('mpcurses.screen.initialize_colors') + @patch('mpcurses.screen.validate_screen_layout_processes') + @patch('mpcurses.screen.set_screen_defaults_processes') @patch('mpcurses.screen.initialize_text') - def test__initialize_screen_Should_CallExpected_When_Text(self, initialize_text_patch, *patches): + def test__initialize_screen_offsets_Should_CallExpected_When_Text(self, initialize_text_patch, *patches): screen_mock = Mock() screen_layout_mock = { '_screen': { @@ -300,14 +353,13 @@ def test__initialize_screen_Should_CallExpected_When_Text(self, initialize_text_ 'text': 'Text Label' } } - initialize_screen(screen_mock, screen_layout_mock, 1) + initialize_screen_offsets(screen_mock, screen_layout_mock, 1, 1) initialize_text_patch.assert_called_once_with(1, 'category_with_text', screen_layout_mock, screen_mock) @patch('mpcurses.screen.update_screen_status') - @patch('mpcurses.screen.curses.curs_set') - @patch('mpcurses.screen.validate_screen_size') - @patch('mpcurses.screen.initialize_colors') - def test__initialize_screen_Should_AddKeepCountToCategory_When_List(self, *patches): + @patch('mpcurses.screen.validate_screen_layout_processes') + @patch('mpcurses.screen.set_screen_defaults_processes') + def test__initialize_screen_offsets_Should_AddKeepCountToCategory_When_List(self, *patches): screen_mock = Mock() screen_layout_mock = { '_screen': { @@ -316,15 +368,14 @@ def test__initialize_screen_Should_AddKeepCountToCategory_When_List(self, *patch 'list': True } } - initialize_screen(screen_mock, screen_layout_mock, 1) + initialize_screen_offsets(screen_mock, screen_layout_mock, 1, 1) self.assertTrue(screen_layout_mock['category_with_list']['keep_count']) @patch('mpcurses.screen.update_screen_status') - @patch('mpcurses.screen.curses.curs_set') - @patch('mpcurses.screen.validate_screen_size') - @patch('mpcurses.screen.initialize_colors') + @patch('mpcurses.screen.validate_screen_layout_processes') + @patch('mpcurses.screen.set_screen_defaults_processes') @patch('mpcurses.screen.initialize_keep_count') - def test__initialize_screen_Should_CallExpected_When_KeepCount(self, initialize_keep_count_patch, *patches): + def test__initialize_screen_offsets_Should_CallExpected_When_KeepCount(self, initialize_keep_count_patch, *patches): screen_mock = Mock() screen_layout_mock = { '_screen': { @@ -333,7 +384,7 @@ def test__initialize_screen_Should_CallExpected_When_KeepCount(self, initialize_ 'keep_count': True } } - initialize_screen(screen_mock, screen_layout_mock, 1) + initialize_screen_offsets(screen_mock, screen_layout_mock, 1, 1) initialize_keep_count_patch.assert_called_once_with('category_with_keep_count', 1, screen_layout_mock) @patch('mpcurses.screen.update_screen_status') @@ -1202,13 +1253,11 @@ def test__squash_table_Should_CallExpected_When_Called(self, update_positions_pa @patch('mpcurses.screen.sys.argv', ['scripta']) def test__set_screen_defaults_Should_SetDefaults_When_Called(self, *patches): screen_layout = {} - set_screen_defaults(10, 5, screen_layout) + set_screen_defaults(screen_layout) expected_screen_layout = { '_screen': { 'title': 'scripta', - 'zfill': 2, 'color': 11, - 'show_process_status': True, 'blink': True } } @@ -1217,27 +1266,52 @@ def test__set_screen_defaults_Should_SetDefaults_When_Called(self, *patches): def test__set_screen_defaults_Should_NotSetDefaults_When_Called(self, *patches): screen_layout = { '_screen': { - 'title': 'scripta', + 'title': 'scriptb', + 'color': 12, + 'blink': False + } + } + set_screen_defaults(screen_layout) + expected_screen_layout = { + '_screen': { + 'title': 'scriptb', + 'color': 12, + 'blink': False + } + } + self.assertEqual(screen_layout, expected_screen_layout) + + def test__set_screen_defaults_processes_Should_SetDefaults_When_Called(self, *patches): + screen_layout = { + '_screen': { + } + } + set_screen_defaults_processes(10, 2, screen_layout) + expected_screen_layout = { + '_screen': { 'zfill': 2, - 'color': 11, - 'show_process_status': True, - 'blink': True + 'show_process_status': True + } + } + self.assertEqual(screen_layout, expected_screen_layout) + + def test__set_screen_defaults_processes_Should_NotSetDefaults_When_Called(self, *patches): + screen_layout = { + '_screen': { + 'zfill': 2, + 'show_process_status': False } } - set_screen_defaults(10, 5, screen_layout) + set_screen_defaults_processes(10, 2, screen_layout) expected_screen_layout = { '_screen': { - 'title': 'scripta', 'zfill': 2, - 'color': 11, - 'show_process_status': True, - 'blink': True + 'show_process_status': False } } self.assertEqual(screen_layout, expected_screen_layout) - @patch('mpcurses.screen.set_screen_defaults') - def test__validate_screen_layout_RaiseException_When_MoreProcessesThanTableEntries(self, *patches): + def test__validate_screen_layout_processes_RaiseException_When_MoreProcessesThanTableEntries(self, *patches): screen_layout_mock = { 'table': { 'rows': 30, @@ -1245,11 +1319,10 @@ def test__validate_screen_layout_RaiseException_When_MoreProcessesThanTableEntri } } with self.assertRaises(Exception): - validate_screen_layout(100, 10, screen_layout_mock) + validate_screen_layout_processes(100, screen_layout_mock) - @patch('mpcurses.screen.set_screen_defaults') @patch('mpcurses.screen.squash_table') - def test__validate_screen_layout_Should_CallExpected_When_Squash(self, squash_table_patch, *patches): + def test__validate_screen_layout_processes_Should_CallExpected_When_Squash(self, squash_table_patch, *patches): screen_layout_mock = { 'table': { 'rows': 30, @@ -1257,12 +1330,11 @@ def test__validate_screen_layout_Should_CallExpected_When_Squash(self, squash_ta 'squash': True } } - validate_screen_layout(11, 11, screen_layout_mock) + validate_screen_layout_processes(11, screen_layout_mock) squash_table_patch.assert_called_once_with(screen_layout_mock, 19) - @patch('mpcurses.screen.set_screen_defaults') @patch('mpcurses.screen.squash_table') - def test__validate_screen_layout_Should_CallExpected_When_SquashFalse(self, squash_table_patch, *patches): + def test__validate_screen_layout_processes_Should_CallExpected_When_SquashFalse(self, squash_table_patch, *patches): screen_layout_mock = { 'table': { 'rows': 30, @@ -1270,12 +1342,11 @@ def test__validate_screen_layout_Should_CallExpected_When_SquashFalse(self, squa 'squash': False } } - validate_screen_layout(11, 11, screen_layout_mock) + validate_screen_layout_processes(11, screen_layout_mock) squash_table_patch.assert_not_called() - @patch('mpcurses.screen.set_screen_defaults') @patch('mpcurses.screen.squash_table') - def test__validate_screen_layout_Should_CallExpected_When_ProcessesGreaterThanRows(self, squash_table_patch, *patches): + def test__validate_screen_layout_processes_Should_CallExpected_When_ProcessesGreaterThanRows(self, squash_table_patch, *patches): screen_layout_mock = { 'table': { 'rows': 30, @@ -1283,26 +1354,24 @@ def test__validate_screen_layout_Should_CallExpected_When_ProcessesGreaterThanRo 'squash': True } } - validate_screen_layout(30, 30, screen_layout_mock) + validate_screen_layout_processes(30, screen_layout_mock) squash_table_patch.assert_not_called() - @patch('mpcurses.screen.set_screen_defaults') @patch('mpcurses.screen.squash_table') - def test__validate_screen_layout_Should_CallExpected_When_NoTable(self, squash_table_patch, *patches): + def test__validate_screen_layout_processes_Should_CallExpected_When_NoTable(self, squash_table_patch, *patches): screen_layout_mock = { } - validate_screen_layout(30, 30, screen_layout_mock) + validate_screen_layout_processes(30, screen_layout_mock) squash_table_patch.assert_not_called() - @patch('mpcurses.screen.set_screen_defaults') @patch('mpcurses.screen.squash_table') - def test__validate_screen_layout_Should_CallExpected_When_HorizontalTable(self, squash_table_patch, *patches): + def test__validate_screen_layout_processes_Should_CallExpected_When_HorizontalTable(self, squash_table_patch, *patches): screen_layout_mock = { 'table': { 'orientation': 'horizontal' } } - validate_screen_layout(30, 30, screen_layout_mock) + validate_screen_layout_processes(30, screen_layout_mock) def test__validate_screen_size_Should_RaiseExeption_When_ScreenNotTallEnough(self, *patches): screen_mock = Mock()