pw_console: Add test-mode fake logger task

Adds an asyncio background task to generate fake log messages. This
can be enabled via the --test-mode command line flag.

Test: pw-console --test-mode
No-Docs-Update-Reason: Test mode is temporary for log viewer dev.
Change-Id: I4bbb7221c4340d46899d2e65b6855ea533f036ef
Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/49025
Pigweed-Auto-Submit: Anthony DiGirolamo <tonymd@google.com>
Commit-Queue: Auto-Submit <auto-submit@pigweed.google.com.iam.gserviceaccount.com>
Reviewed-by: Joe Ethier <jethier@google.com>
diff --git a/pw_console/py/pw_console/__main__.py b/pw_console/py/pw_console/__main__.py
index c516897..05171f6 100644
--- a/pw_console/py/pw_console/__main__.py
+++ b/pw_console/py/pw_console/__main__.py
@@ -23,7 +23,7 @@
 import pw_cli.log
 import pw_cli.argument_types
 
-from pw_console.console_app import embed
+from pw_console.console_app import embed, FAKE_DEVICE_LOGGER_NAME
 
 _LOG = logging.getLogger(__package__)
 
@@ -80,16 +80,20 @@
 
     pw_cli.log.install(args.loglevel, True, False, args.logfile)
 
+    global_vars = None
     default_loggers: List = []
-    # TODO: Add test-mode loggers here.
-    # if args.test_mode:
-    #     default_loggers = [
-    #         # Don't include pw_console package logs (_LOG) in the log pane UI.
-    #         # Add the fake logger for test_mode.
-    #         logging.getLogger(FAKE_DEVICE_LOGGER_NAME)
-    #     ]
+    if args.test_mode:
+        default_loggers = [
+            # Don't include pw_console package logs (_LOG) in the log pane UI.
+            # Add the fake logger for test_mode.
+            logging.getLogger(FAKE_DEVICE_LOGGER_NAME)
+        ]
+        # Give access to adding log messages from the repl via: `LOG.warning()`
+        global_vars = dict(LOG=default_loggers[0])
 
-    embed(loggers=default_loggers)
+    embed(global_vars=global_vars,
+          loggers=default_loggers,
+          test_mode=args.test_mode)
 
     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 fe2e0da..d2447df 100644
--- a/pw_console/py/pw_console/console_app.py
+++ b/pw_console/py/pw_console/console_app.py
@@ -49,6 +49,10 @@
 
 _LOG = logging.getLogger(__package__)
 
+# Fake logger for --test-mode
+FAKE_DEVICE_LOGGER_NAME = 'fake_device.1'
+_FAKE_DEVICE_LOG = logging.getLogger(FAKE_DEVICE_LOGGER_NAME)
+
 
 class FloatingMessageBar(ConditionalContainer):
     """Floating message bar for showing status messages."""
@@ -267,14 +271,54 @@
         """Redraw the prompt_toolkit UI."""
         self.application.invalidate()
 
-    async def run(
-        self,
-        # TODO: remove pylint disable line.
-        test_mode=False  # pylint: disable=unused-argument
-    ):
+    async def run(self, test_mode=False):
         """Start the prompt_toolkit UI."""
-        unused_result = await self.application.run_async(
-            set_exception_handler=True)
+        if test_mode:
+            background_log_task = asyncio.create_task(self.log_forever())
+
+        try:
+            unused_result = await self.application.run_async(
+                set_exception_handler=True)
+        finally:
+            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 lines:
+        # Log message [=         ] # 291
+        # Log message [ =        ] # 292
+        # Log message [  =       ] # 293
+        # Log message [   =      ] # 294
+        # Log message [    =     ] # 295
+        # Log message [     =    ] # 296
+        # Log message [      =   ] # 297
+        # Log message [       =  ] # 298
+        # Log message [        = ] # 299
+        # Log message [         =] # 300
+        while True:
+            await asyncio.sleep(2)
+            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 dolor sit amet, consectetur "
+                                 "adipiscing elit.") * 8
+            # TODO: Test log lines that include linebreaks.
+            # if message_count % 11 == 0:
+            #     new_log_line += inspect.cleandoc(""" [PYTHON] START
+            #         In []: import time;
+            #                 def t(s):
+            #                     time.sleep(s)
+            #                     return 't({}) seconds done'.format(s)""")
+
+            message_count += 1
+            _FAKE_DEVICE_LOG.info(new_log_line)
 
 
 def embed(