pw_console: Improve pw console --test-mode
- Move test functions into their own source
- Override the window layout for test-mode. By default 3 log panes
are shown, 2 with filters added.
- Remove unused byte_size from LogStore class for PyPy compatibility
Change-Id: Ib6fa4f67f9d74a83df05aebc5bd58a3bf53036cb
Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/108840
Commit-Queue: Anthony DiGirolamo <tonymd@google.com>
Reviewed-by: Armando Montanez <amontanez@google.com>
diff --git a/pw_console/py/BUILD.gn b/pw_console/py/BUILD.gn
index 09f5c2d..1275485 100644
--- a/pw_console/py/BUILD.gn
+++ b/pw_console/py/BUILD.gn
@@ -63,6 +63,7 @@
"pw_console/search_toolbar.py",
"pw_console/style.py",
"pw_console/templates/__init__.py",
+ "pw_console/test_mode.py",
"pw_console/text_formatting.py",
"pw_console/toml_config_loader_mixin.py",
"pw_console/widgets/__init__.py",
diff --git a/pw_console/py/pw_console/__main__.py b/pw_console/py/pw_console/__main__.py
index 97c4e77..58a3d84 100644
--- a/pw_console/py/pw_console/__main__.py
+++ b/pw_console/py/pw_console/__main__.py
@@ -18,12 +18,14 @@
import logging
from pathlib import Path
import sys
+from typing import Optional, Dict
import pw_cli.log
import pw_cli.argument_types
import pw_console
import pw_console.python_logging
+import pw_console.test_mode
from pw_console.log_store import LogStore
from pw_console.plugins.calc_pane import CalcPane
from pw_console.plugins.clock_pane import ClockPane
@@ -95,11 +97,11 @@
_ROOT_LOG.debug('pw_console test-mode starting...')
fake_logger = logging.getLogger(
- pw_console.console_app.FAKE_DEVICE_LOGGER_NAME)
+ pw_console.test_mode.FAKE_DEVICE_LOGGER_NAME)
default_loggers = {
# Don't include pw_console package logs (_LOG) in the log pane UI.
# Add the fake logger for test_mode.
- 'Fake Device Logs': [fake_logger],
+ 'Fake Device': [fake_logger],
'PwConsole Debug': [logging.getLogger('pw_console')],
'All Logs': root_log_store,
}
@@ -129,9 +131,13 @@
config_file_path=args.config_file,
)
- # Add example plugins used to validate behavior in the Pigweed Console
- # manual test procedure: https://pigweed.dev/pw_console/testing.html
+ overriden_window_config: Optional[Dict] = None
+ # Add example plugins and log panes used to validate behavior in the Pigweed
+ # Console manual test procedure: https://pigweed.dev/pw_console/testing.html
if args.test_mode:
+ fake_logger.propagate = False
+ console.setup_python_logging(loggers_with_no_propagation=[fake_logger])
+
_ROOT_LOG.debug('pw_console.PwConsoleEmbed init complete')
_ROOT_LOG.debug('Adding plugins...')
console.add_window_plugin(ClockPane())
@@ -140,7 +146,36 @@
Twenty48Pane(include_resize_handle=False), left=4)
_ROOT_LOG.debug('Starting prompt_toolkit full-screen application...')
- console.embed()
+ overriden_window_config = {
+ 'Split 1 stacked': {
+ 'Fake Device': None,
+ 'Fake Keys': {
+ 'duplicate_of': 'Fake Device',
+ 'filters': {
+ 'keys': {
+ 'regex': '[^ ]+'
+ },
+ },
+ },
+ 'Fake USB': {
+ 'duplicate_of': 'Fake Device',
+ 'filters': {
+ 'module': {
+ 'regex': 'USB'
+ },
+ },
+ },
+ },
+ 'Split 2 tabbed': {
+ 'Python Repl': None,
+ 'All Logs': None,
+ 'PwConsole Debug': None,
+ 'Calculator': None,
+ 'Clock': None,
+ },
+ }
+
+ console.embed(override_window_config=overriden_window_config)
if args.logfile:
print(f'Logs saved to: {args.logfile}')
diff --git a/pw_console/py/pw_console/console_app.py b/pw_console/py/pw_console/console_app.py
index a957100..f83a428 100644
--- a/pw_console/py/pw_console/console_app.py
+++ b/pw_console/py/pw_console/console_app.py
@@ -67,18 +67,14 @@
from pw_console.quit_dialog import QuitDialog
from pw_console.repl_pane import ReplPane
import pw_console.style
+from pw_console.test_mode import start_fake_logger
import pw_console.widgets.checkbox
from pw_console.widgets import FloatingWindowPane
import pw_console.widgets.mouse_handlers
from pw_console.window_manager import WindowManager
_LOG = logging.getLogger(__package__)
-
-# Fake logger for --test-mode
-FAKE_DEVICE_LOGGER_NAME = 'pw_console_fake_device'
-_FAKE_DEVICE_LOG = logging.getLogger(FAKE_DEVICE_LOGGER_NAME)
-# Don't send fake_device logs to the root Python logger.
-_FAKE_DEVICE_LOG.propagate = False
+_ROOT_LOG = logging.getLogger('')
MAX_FPS = 30
MIN_REDRAW_INTERVAL = (60.0 / MAX_FPS) / 60.0
@@ -189,6 +185,7 @@
# Event loop for executing user repl code.
self.user_code_loop = asyncio.new_event_loop()
+ self.test_mode_log_loop = asyncio.new_event_loop()
self.app_title = app_title if app_title else 'Pigweed Console'
@@ -875,6 +872,11 @@
daemon=True)
thread.start()
+ def _test_mode_log_thread_entry(self):
+ """Entry point for the user code thread."""
+ asyncio.set_event_loop(self.test_mode_log_loop)
+ self.test_mode_log_loop.run_forever()
+
def _update_help_window(self):
"""Generate the help window text based on active pane keybindings."""
# Add global mouse bindings to the help text.
@@ -963,7 +965,10 @@
async def run(self, test_mode=False):
"""Start the prompt_toolkit UI."""
if test_mode:
- background_log_task = asyncio.create_task(self.log_forever())
+ background_log_task = start_fake_logger(
+ lines=self.user_guide_window.help_text_area.document.lines,
+ log_thread_entry=self._test_mode_log_thread_entry,
+ log_thread_loop=self.test_mode_log_loop)
# Repl pane has focus by default, if it's hidden switch focus to another
# visible pane.
@@ -977,44 +982,6 @@
if test_mode:
background_log_task.cancel()
- async def log_forever(self):
- """Test mode async log generator coroutine that runs forever."""
- message_count = 0
- # Sample log line format:
- # Log message [= ] # 100
-
- # Fake module column names.
- module_names = ['APP', 'RADIO', 'BAT', 'USB', 'CPU']
- while True:
- if message_count > 32 or message_count < 2:
- await asyncio.sleep(1)
- bar_size = 10
- position = message_count % bar_size
- bar_content = " " * (bar_size - position - 1) + "="
- if position > 0:
- bar_content = "=".rjust(position) + " " * (bar_size - position)
- new_log_line = 'Log message [{}] # {}'.format(
- bar_content, message_count)
- if message_count % 10 == 0:
- new_log_line += (
- ' Lorem ipsum \033[34m\033[1mdolor sit amet\033[0m'
- ', consectetur '
- 'adipiscing elit.') * 8
- if message_count % 11 == 0:
- new_log_line += ' '
- new_log_line += (
- '[PYTHON] START\n'
- 'In []: import time;\n'
- ' def t(s):\n'
- ' time.sleep(s)\n'
- ' return "t({}) seconds done".format(s)\n\n')
-
- module_name = module_names[message_count % len(module_names)]
- _FAKE_DEVICE_LOG.info(new_log_line,
- extra=dict(extra_metadata_fields=dict(
- module=module_name, file='fake_app.cc')))
- message_count += 1
-
# TODO(tonymd): Remove this alias when not used by downstream projects.
def embed(
diff --git a/pw_console/py/pw_console/console_prefs.py b/pw_console/py/pw_console/console_prefs.py
index f5621f6..fb31bbf 100644
--- a/pw_console/py/pw_console/console_prefs.py
+++ b/pw_console/py/pw_console/console_prefs.py
@@ -219,9 +219,12 @@
return self._config.get('window_column_split_method', 'vertical')
@property
- def windows(self) -> dict:
+ def windows(self) -> Dict:
return self._config.get('windows', {})
+ def set_windows(self, new_config: Dict) -> None:
+ self._config['windows'] = new_config
+
@property
def window_column_modes(self) -> List:
return list(column_type for column_type in self.windows.keys())
@@ -300,11 +303,11 @@
return decorator
@property
- def snippets(self) -> dict:
+ def snippets(self) -> Dict:
return self._config.get('snippets', {})
@property
- def user_snippets(self) -> dict:
+ def user_snippets(self) -> Dict:
return self._config.get('user_snippets', {})
def snippet_completions(self) -> List[Tuple[str, str]]:
diff --git a/pw_console/py/pw_console/embed.py b/pw_console/py/pw_console/embed.py
index e6dcddd..c498615 100644
--- a/pw_console/py/pw_console/embed.py
+++ b/pw_console/py/pw_console/embed.py
@@ -281,7 +281,7 @@
for window_title in window_titles:
self.hidden_by_default_windows.append(window_title)
- def embed(self) -> None:
+ def embed(self, override_window_config: Optional[Dict] = None) -> None:
"""Start the console."""
# Create the ConsoleApp instance.
@@ -328,6 +328,8 @@
if self.config_file_path:
self.console_app.load_clean_config(self.config_file_path)
+ if override_window_config:
+ self.console_app.prefs.set_windows(override_window_config)
self.console_app.apply_window_config()
# Hide the repl pane if it's in the hidden windows list.
diff --git a/pw_console/py/pw_console/log_store.py b/pw_console/py/pw_console/log_store.py
index 33c5d68..b06a75d 100644
--- a/pw_console/py/pw_console/log_store.py
+++ b/pw_console/py/pw_console/log_store.py
@@ -16,7 +16,6 @@
from __future__ import annotations
import collections
import logging
-import sys
from datetime import datetime
from typing import Dict, List, Optional, TYPE_CHECKING
@@ -91,9 +90,6 @@
# and end of the iterable.
self.logs: collections.deque = collections.deque()
- # Estimate of the logs in memory.
- self.byte_size: int = 0
-
# Only allow this many log lines in memory.
self.max_history_size: int = 1000000
@@ -146,7 +142,6 @@
def clear_logs(self):
"""Erase all stored pane lines."""
self.logs = collections.deque()
- self.byte_size = 0
self.channel_counts = {}
self.channel_formatted_prefix_widths = {}
self.line_index = 0
@@ -227,12 +222,6 @@
# Check for bigger column widths.
self.table.update_metadata_column_widths(self.logs[-1])
- # Update estimated byte_size.
- self.byte_size += sys.getsizeof(self.logs[-1])
- # If the total log lines is > max_history_size, delete the oldest line.
- if self.get_total_count() > self.max_history_size:
- self.byte_size -= sys.getsizeof(self.logs.popleft())
-
def emit(self, record) -> None:
"""Process a new log record.
diff --git a/pw_console/py/pw_console/plugins/twenty48_pane.py b/pw_console/py/pw_console/plugins/twenty48_pane.py
index 832ce3e..a215d89 100644
--- a/pw_console/py/pw_console/plugins/twenty48_pane.py
+++ b/pw_console/py/pw_console/plugins/twenty48_pane.py
@@ -500,6 +500,9 @@
)
def get_top_level_menus(self) -> List[MenuItem]:
+ def _toggle_dialog() -> None:
+ self.toggle_dialog()
+
return [
MenuItem(
'[2048]',
@@ -509,6 +512,7 @@
disabled=True),
# Menu separator
MenuItem('-', None),
+ MenuItem('Show/Hide 2048 Game', handler=_toggle_dialog),
MenuItem('Restart', handler=self.game.reset_game),
],
),
diff --git a/pw_console/py/pw_console/test_mode.py b/pw_console/py/pw_console/test_mode.py
new file mode 100644
index 0000000..54b0573
--- /dev/null
+++ b/pw_console/py/pw_console/test_mode.py
@@ -0,0 +1,81 @@
+# Copyright 2022 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.
+"""pw_console test mode functions."""
+
+import asyncio
+import time
+import re
+import random
+import logging
+from threading import Thread
+from typing import Dict, List, Tuple
+
+FAKE_DEVICE_LOGGER_NAME = 'pw_console_fake_device'
+
+_ROOT_LOG = logging.getLogger('')
+_FAKE_DEVICE_LOG = logging.getLogger(FAKE_DEVICE_LOGGER_NAME)
+
+
+def start_fake_logger(lines, log_thread_entry, log_thread_loop):
+ fake_log_messages = prepare_fake_logs(lines)
+
+ test_log_thread = Thread(target=log_thread_entry, args=(), daemon=True)
+ test_log_thread.start()
+
+ background_log_task = asyncio.run_coroutine_threadsafe(
+ # This function will be executed in a separate thread.
+ log_forever(fake_log_messages),
+ # Using this asyncio event loop.
+ log_thread_loop) # type: ignore
+ return background_log_task
+
+
+def prepare_fake_logs(lines) -> List[Tuple[str, Dict]]:
+ fake_logs: List[Tuple[str, Dict]] = []
+ key_regex = re.compile(r':kbd:`(?P<key>[^`]+)`')
+ for line in lines:
+ if not line:
+ continue
+
+ keyboard_key = ''
+ search = key_regex.search(line)
+ if search:
+ keyboard_key = search.group(1)
+
+ fake_logs.append((line, {'keys': keyboard_key}))
+ return fake_logs
+
+
+async def log_forever(fake_log_messages: List[Tuple[str, Dict]]):
+ """Test mode async log generator coroutine that runs forever."""
+ _ROOT_LOG.info('Fake log device connected.')
+ start_time = time.time()
+ message_count = 0
+
+ # Fake module column names.
+ module_names = ['APP', 'RADIO', 'BAT', 'USB', 'CPU']
+ while True:
+ if message_count > 32 or message_count < 2:
+ await asyncio.sleep(.1)
+ fake_log = random.choice(fake_log_messages)
+
+ module_name = module_names[message_count % len(module_names)]
+ _FAKE_DEVICE_LOG.info(
+ fake_log[0],
+ extra=dict(extra_metadata_fields=dict(module=module_name,
+ file='fake_app.cc',
+ timestamp=time.time() -
+ start_time,
+ **fake_log[1])))
+ message_count += 1
diff --git a/pw_console/testing.rst b/pw_console/testing.rst
index cfbf6e3..982ad11 100644
--- a/pw_console/testing.rst
+++ b/pw_console/testing.rst
@@ -35,7 +35,7 @@
- ✅
* - 1
- - Click the :guilabel:`Fake Device Logs` window title
+ - Click the :guilabel:`Fake Device` window title
- Log pane is focused
- |checkbox|
@@ -140,7 +140,7 @@
- ✅
* - 1
- - Click the :guilabel:`Fake Device Logs` window title
+ - Click the :guilabel:`Fake Device` window title
- Log pane is focused
- |checkbox|
@@ -402,12 +402,12 @@
- ✅
* - 1
- - | Click the :guilabel:`Fake Device Logs` window title
+ - | Click the :guilabel:`Fake Device` window title
- Log pane is focused
- |checkbox|
* - 2
- - | Click the menu :guilabel:`Windows > #: Fake Device Logs...`
+ - | Click the menu :guilabel:`Windows > #: Fake Device...`
| Click :guilabel:`Duplicate pane`
- | 3 panes are visible:
| Log pane on top
@@ -443,7 +443,7 @@
- |checkbox|
* - 7
- - | Click the menu :guilabel:`Windows > #: Fake Device Logs...`
+ - | Click the menu :guilabel:`Windows > #: Fake Device...`
| Click :guilabel:`Remove pane`
- | 2 panes are visible:
| Repl pane on the top
@@ -520,7 +520,7 @@
- ✅
* - 1
- - | Click the :guilabel:`Fake Device Logs` window
+ - | Click the :guilabel:`Fake Device` window
- Log pane is focused
- |checkbox|
@@ -540,7 +540,7 @@
* - 4
- Click the :guilabel:`View > Move Window Right`
- - :guilabel:`Fake Device Logs` should appear in a right side split
+ - :guilabel:`Fake Device` should appear in a right side split
- |checkbox|
* - 5
@@ -582,7 +582,7 @@
- ✅
* - 1
- - | Click the :guilabel:`Fake Device Logs` window title
+ - | Click the :guilabel:`Fake Device` window title
- Log pane is focused
- |checkbox|