| # Copyright 2021 The Pigweed Authors |
| # |
| # 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 |
| # |
| # https://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. |
| """Tests for pw_console.console_app""" |
| |
| import asyncio |
| import builtins |
| import inspect |
| import io |
| import sys |
| import threading |
| import unittest |
| from unittest.mock import MagicMock, call |
| |
| from prompt_toolkit.application import create_app_session |
| from prompt_toolkit.output import ( |
| ColorDepth, |
| # inclusive-language: ignore |
| DummyOutput as FakeOutput, |
| ) |
| |
| from pw_console.console_app import ConsoleApp |
| from pw_console.console_prefs import ConsolePrefs |
| from pw_console.repl_pane import ReplPane |
| from pw_console.pw_ptpython_repl import PwPtPythonRepl |
| |
| _PYTHON_3_8 = sys.version_info >= ( |
| 3, |
| 8, |
| ) |
| |
| if _PYTHON_3_8: |
| from unittest import IsolatedAsyncioTestCase # type: ignore # pylint: disable=no-name-in-module |
| |
| class TestReplPane(IsolatedAsyncioTestCase): |
| """Tests for ReplPane.""" |
| def setUp(self): # pylint: disable=invalid-name |
| self.maxDiff = None # pylint: disable=invalid-name |
| |
| def test_repl_code_return_values(self) -> None: |
| """Test stdout, return values, and exceptions can be returned from |
| running user repl code.""" |
| app = MagicMock() |
| |
| global_vars = { |
| '__name__': '__main__', |
| '__package__': None, |
| '__doc__': None, |
| '__builtins__': builtins, |
| } |
| |
| pw_ptpython_repl = PwPtPythonRepl( |
| get_globals=lambda: global_vars, |
| get_locals=lambda: global_vars, |
| color_depth=ColorDepth.DEPTH_8_BIT) |
| repl_pane = ReplPane( |
| application=app, |
| python_repl=pw_ptpython_repl, |
| ) |
| # Check pw_ptpython_repl has a reference to the parent repl_pane. |
| self.assertEqual(repl_pane, pw_ptpython_repl.repl_pane) |
| |
| # Define a function, should return nothing. |
| code = inspect.cleandoc(""" |
| def run(): |
| print('The answer is ', end='') |
| return 1+1+4+16+20 |
| """) |
| temp_stdout = io.StringIO() |
| temp_stderr = io.StringIO() |
| # pylint: disable=protected-access |
| result = asyncio.run( |
| pw_ptpython_repl._run_user_code(code, temp_stdout, |
| temp_stderr)) |
| self.assertEqual(result, { |
| 'stdout': '', |
| 'stderr': '', |
| 'result': None |
| }) |
| |
| temp_stdout = io.StringIO() |
| temp_stderr = io.StringIO() |
| # Check stdout and return value |
| result = asyncio.run( |
| pw_ptpython_repl._run_user_code('run()', temp_stdout, |
| temp_stderr)) |
| self.assertEqual(result, { |
| 'stdout': 'The answer is ', |
| 'stderr': '', |
| 'result': 42 |
| }) |
| |
| temp_stdout = io.StringIO() |
| temp_stderr = io.StringIO() |
| # Check for repl exception |
| result = asyncio.run( |
| pw_ptpython_repl._run_user_code('return "blah"', temp_stdout, |
| temp_stderr)) |
| self.assertIn("SyntaxError: 'return' outside function", |
| pw_ptpython_repl._last_exception) # type: ignore |
| |
| async def test_user_thread(self) -> None: |
| """Test user code thread.""" |
| |
| with create_app_session(output=FakeOutput()): |
| # Setup Mocks |
| prefs = ConsolePrefs(project_file=False, |
| project_user_file=False, |
| user_file=False) |
| prefs.set_code_theme('default') |
| app = ConsoleApp(color_depth=ColorDepth.DEPTH_8_BIT, |
| prefs=prefs) |
| |
| app.start_user_code_thread() |
| |
| pw_ptpython_repl = app.pw_ptpython_repl |
| repl_pane = app.repl_pane |
| |
| # Mock update_output_buffer to track number of update calls |
| repl_pane.update_output_buffer = MagicMock( # type: ignore |
| wraps=repl_pane.update_output_buffer) |
| |
| # Mock complete callback |
| pw_ptpython_repl.user_code_complete_callback = ( # type: ignore |
| MagicMock( |
| wraps=pw_ptpython_repl.user_code_complete_callback)) |
| |
| # Repl done flag for tests |
| user_code_done = threading.Event() |
| |
| # Run some code |
| code = inspect.cleandoc(""" |
| import time |
| def run(): |
| for i in range(2): |
| time.sleep(0.5) |
| print(i) |
| print('The answer is ', end='') |
| return 1+1+4+16+20 |
| """) |
| input_buffer = MagicMock(text=code) |
| pw_ptpython_repl._accept_handler(input_buffer) # pylint: disable=protected-access |
| |
| # Get last executed code object. |
| user_code1 = repl_pane.executed_code[-1] |
| # Wait for repl code to finish. |
| user_code1.future.add_done_callback( |
| lambda future: user_code_done.set()) |
| # Wait for stdout monitoring to complete. |
| if user_code1.stdout_check_task: |
| await user_code1.stdout_check_task |
| # Wait for test done callback. |
| user_code_done.wait() |
| |
| # Check user_code1 results |
| # NOTE: Avoid using assert_has_calls. Thread timing can make the |
| # test flaky. |
| expected_calls = [ |
| # Initial exec start |
| call('pw_ptpython_repl._accept_handler'), |
| # Code finishes |
| call('repl_pane.append_result_to_executed_code'), |
| # Complete callback |
| call('pw_ptpython_repl.user_code_complete_callback'), |
| ] |
| for expected_call in expected_calls: |
| self.assertIn(expected_call, |
| repl_pane.update_output_buffer.mock_calls) |
| |
| pw_ptpython_repl.user_code_complete_callback.assert_called_once( |
| ) |
| |
| self.assertIsNotNone(user_code1) |
| self.assertTrue(user_code1.future.done()) |
| self.assertEqual(user_code1.input, code) |
| self.assertEqual(user_code1.output, None) |
| # stdout / stderr may be '' or None |
| self.assertFalse(user_code1.stdout) |
| self.assertFalse(user_code1.stderr) |
| |
| # Reset mocks |
| user_code_done.clear() |
| pw_ptpython_repl.user_code_complete_callback.reset_mock() |
| repl_pane.update_output_buffer.reset_mock() |
| |
| # Run some code |
| input_buffer = MagicMock(text='run()') |
| pw_ptpython_repl._accept_handler(input_buffer) # pylint: disable=protected-access |
| |
| # Get last executed code object. |
| user_code2 = repl_pane.executed_code[-1] |
| # Wait for repl code to finish. |
| user_code2.future.add_done_callback( |
| lambda future: user_code_done.set()) |
| # Wait for stdout monitoring to complete. |
| if user_code2.stdout_check_task: |
| await user_code2.stdout_check_task |
| # Wait for test done callback. |
| user_code_done.wait() |
| |
| # Check user_code2 results |
| # NOTE: Avoid using assert_has_calls. Thread timing can make the |
| # test flaky. |
| expected_calls = [ |
| # Initial exec start |
| call('pw_ptpython_repl._accept_handler'), |
| # Periodic checks, should be a total of 4: |
| # Code should take 1.0 second to run. |
| # Periodic checks every 0.3 seconds |
| # 1.0 / 0.3 = 3.33 (4) checks |
| call('repl_pane.periodic_check'), |
| call('repl_pane.periodic_check'), |
| call('repl_pane.periodic_check'), |
| # Code finishes |
| call('repl_pane.append_result_to_executed_code'), |
| # Complete callback |
| call('pw_ptpython_repl.user_code_complete_callback'), |
| # Final periodic check |
| call('repl_pane.periodic_check'), |
| ] |
| for expected_call in expected_calls: |
| self.assertIn(expected_call, |
| repl_pane.update_output_buffer.mock_calls) |
| |
| pw_ptpython_repl.user_code_complete_callback.assert_called_once( |
| ) |
| self.assertIsNotNone(user_code2) |
| self.assertTrue(user_code2.future.done()) |
| self.assertEqual(user_code2.input, 'run()') |
| self.assertEqual(user_code2.output, '42') |
| self.assertEqual(user_code2.stdout, '0\n1\nThe answer is ') |
| self.assertFalse(user_code2.stderr) |
| |
| # Reset mocks |
| user_code_done.clear() |
| pw_ptpython_repl.user_code_complete_callback.reset_mock() |
| repl_pane.update_output_buffer.reset_mock() |
| |
| |
| if __name__ == '__main__': |
| unittest.main() |