[Dexter] Add a simple logging class to Dexter

Adds a basic logging class to Dexter that uses the existing PrettyOutput
class for printing and supports 3 levels of verbosity (note, warning,
error). Intended to consolidate the logging logic for Dexter into one
place, removing the need for conditional log statements and making it
easier for us later if we wish to use a more complete logging class.

Reviewed By: Orlando

Differential Revision: https://reviews.llvm.org/D144983
diff --git a/cross-project-tests/debuginfo-tests/dexter/dex/debugger/Debuggers.py b/cross-project-tests/debuginfo-tests/dexter/dex/debugger/Debuggers.py
index be48e7c..48cb7e1 100644
--- a/cross-project-tests/debuginfo-tests/dexter/dex/debugger/Debuggers.py
+++ b/cross-project-tests/debuginfo-tests/dexter/dex/debugger/Debuggers.py
@@ -18,7 +18,6 @@
 from dex.utils import get_root_directory, Timer
 from dex.utils.Environment import is_native_windows
 from dex.utils.Exceptions import ToolArgumentError
-from dex.utils.Warning import warn
 from dex.utils.Exceptions import DebuggerException
 
 from dex.debugger.DebuggerControllers.DefaultController import DefaultController
@@ -48,9 +47,10 @@
     if hasattr(context.options, 'list_debuggers'):
         return
 
-    warn(context,
-         'option <y>"{}"</> is meaningless with this debugger'.format(option),
-         '--debugger={}'.format(context.options.debugger))
+    context.logger.warning(
+         f'option "{option}" is meaningless with this debugger',
+         enable_prefix=True,
+         flag=f'--debugger={context.options.debugger}')
 
 
 def add_debugger_tool_base_arguments(parser, defaults):
diff --git a/cross-project-tests/debuginfo-tests/dexter/dex/debugger/visualstudio/VisualStudio.py b/cross-project-tests/debuginfo-tests/dexter/dex/debugger/visualstudio/VisualStudio.py
index 4ce0142..1db20c7 100644
--- a/cross-project-tests/debuginfo-tests/dexter/dex/debugger/visualstudio/VisualStudio.py
+++ b/cross-project-tests/debuginfo-tests/dexter/dex/debugger/visualstudio/VisualStudio.py
@@ -10,7 +10,7 @@
 import imp
 import os
 import sys
-from pathlib import PurePath
+from pathlib import PurePath, Path
 from collections import defaultdict, namedtuple
 
 from dex.command.CommandBase import StepExpectInfo
@@ -249,9 +249,13 @@
         assert False, "Couldn't find property {}".format(name)
 
     def launch(self, cmdline):
+        exe_path = Path(self.context.options.executable)
+        self.context.logger.note(f"VS: Using executable: '{exe_path}'")
         cmdline_str = ' '.join(cmdline)
         if self.context.options.target_run_args:
           cmdline_str += f" {self.context.options.target_run_args}"
+        if cmdline_str:
+          self.context.logger.note(f"VS: Using executable args: '{cmdline_str}'")
 
         # In a slightly baroque manner, lookup the VS project that runs when
         # you click "run", and set its command line options to the desired
@@ -261,6 +265,7 @@
         ActiveConfiguration = self._fetch_property(project.Properties, 'ActiveConfiguration').Object
         ActiveConfiguration.DebugSettings.CommandArguments = cmdline_str
 
+        self.context.logger.note("Launching VS debugger...")
         self._fn_go(False)
 
     def step(self):
diff --git a/cross-project-tests/debuginfo-tests/dexter/dex/tools/Main.py b/cross-project-tests/debuginfo-tests/dexter/dex/tools/Main.py
index 78fb4f7..c69ffab 100644
--- a/cross-project-tests/debuginfo-tests/dexter/dex/tools/Main.py
+++ b/cross-project-tests/debuginfo-tests/dexter/dex/tools/Main.py
@@ -18,6 +18,7 @@
 from dex.utils import ExtArgParse as argparse
 from dex.utils import get_root_directory
 from dex.utils.Exceptions import Error, ToolArgumentError
+from dex.utils.Logging import Logger
 from dex.utils.UnitTests import unit_tests_ok
 from dex.utils.Version import version
 from dex.utils import WorkingDirectory
@@ -145,6 +146,11 @@
             context.o.green('{}\n'.format(context.version))
             return ReturnCode.OK
 
+        if options.verbose:
+            context.logger.verbosity = 2
+        elif options.no_warnings:
+            context.logger.verbosity = 0
+
         if (options.unittest != 'off' and not unit_tests_ok(context)):
             raise Error('<d>unit test failures</>')
 
@@ -171,6 +177,7 @@
 
     def __init__(self):
         self.o: PrettyOutput = None
+        self.logger: Logger = None
         self.working_directory: str = None
         self.options: dict = None
         self.version: str = None
@@ -182,6 +189,7 @@
     context = Context()
 
     with PrettyOutput() as context.o:
+        context.logger = Logger(context.o)
         try:
             context.root_directory = get_root_directory()
             # Flag some strings for auto-highlighting.
@@ -192,8 +200,7 @@
             module = _import_tool_module(tool_name)
             return tool_main(context, module.Tool(context), args)
         except Error as e:
-            context.o.auto(
-                '\nerror: {}\n'.format(str(e)), stream=PrettyOutput.stderr)
+            context.logger.error(str(e))
             try:
                 if context.options.error_debug:
                     raise
diff --git a/cross-project-tests/debuginfo-tests/dexter/dex/tools/TestToolBase.py b/cross-project-tests/debuginfo-tests/dexter/dex/tools/TestToolBase.py
index 5d976ea..349a6f0 100644
--- a/cross-project-tests/debuginfo-tests/dexter/dex/tools/TestToolBase.py
+++ b/cross-project-tests/debuginfo-tests/dexter/dex/tools/TestToolBase.py
@@ -17,7 +17,7 @@
 from dex.debugger.Debuggers import handle_debugger_tool_options
 from dex.heuristic.Heuristic import add_heuristic_tool_arguments
 from dex.tools.ToolBase import ToolBase
-from dex.utils import get_root_directory, warn
+from dex.utils import get_root_directory
 from dex.utils.Exceptions import Error, ToolArgumentError
 from dex.utils.ReturnCode import ReturnCode
 
@@ -53,8 +53,9 @@
         options = self.context.options
 
         if not options.builder and (options.cflags or options.ldflags):
-            warn(self.context, '--cflags and --ldflags will be ignored when not'
-                               ' using --builder')
+            self.context.logger.warning(
+                '--cflags and --ldflags will be ignored when not using --builder',
+                enable_prefix=True)
 
         if options.vs_solution:
             options.vs_solution = os.path.abspath(options.vs_solution)
diff --git a/cross-project-tests/debuginfo-tests/dexter/dex/tools/ToolBase.py b/cross-project-tests/debuginfo-tests/dexter/dex/tools/ToolBase.py
index eb6ba94..53274d3 100644
--- a/cross-project-tests/debuginfo-tests/dexter/dex/tools/ToolBase.py
+++ b/cross-project-tests/debuginfo-tests/dexter/dex/tools/ToolBase.py
@@ -60,7 +60,7 @@
             '--verbose',
             action='store_true',
             default=False,
-            help='enable verbose output')
+            help='enable verbose output (overrides --no-warnings)')
         self.parser.add_argument(
             '-V',
             '--version',
diff --git a/cross-project-tests/debuginfo-tests/dexter/dex/utils/Logging.py b/cross-project-tests/debuginfo-tests/dexter/dex/utils/Logging.py
new file mode 100644
index 0000000..11386b4
--- /dev/null
+++ b/cross-project-tests/debuginfo-tests/dexter/dex/utils/Logging.py
@@ -0,0 +1,44 @@
+# DExTer : Debugging Experience Tester
+# ~~~~~~   ~         ~~         ~   ~~
+#
+# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+# See https://llvm.org/LICENSE.txt for license information.
+# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+"""Utility functions for producing command line warnings."""
+
+from dex.utils import PrettyOutput
+
+class Logger(object):
+    def __init__(self, pretty_output: PrettyOutput):
+        self.o = pretty_output
+        self.error_color = self.o.red
+        self.warning_color = self.o.yellow
+        self.note_color = self.o.default
+        self.verbosity = 1
+
+    def error(self, msg, enable_prefix=True, flag=None):
+        if self.verbosity < 0:
+            return
+        if enable_prefix:
+            msg = f'error: {msg}'
+        if flag:
+           msg = f'{msg} <y>[{flag}]</>'
+        self.error_color('{}\n'.format(msg), stream=PrettyOutput.stderr)
+
+    def warning(self, msg, enable_prefix=True, flag=None):
+        if self.verbosity < 1:
+            return
+        if enable_prefix:
+            msg = f'warning: {msg}'
+        if flag:
+           msg = f'{msg} <y>[{flag}]</>'
+        self.warning_color('{}\n'.format(msg), stream=PrettyOutput.stderr)
+
+    def note(self, msg, enable_prefix=True, flag=None):
+        if self.verbosity < 2:
+            return
+        if enable_prefix:
+            msg = f'note: {msg}'
+        if flag:
+           msg = f'{msg} <y>[{flag}]</>'
+        self.note_color('{}\n'.format(msg), stream=PrettyOutput.stderr)
diff --git a/cross-project-tests/debuginfo-tests/dexter/dex/utils/Warning.py b/cross-project-tests/debuginfo-tests/dexter/dex/utils/Warning.py
deleted file mode 100644
index 402861a..0000000
--- a/cross-project-tests/debuginfo-tests/dexter/dex/utils/Warning.py
+++ /dev/null
@@ -1,18 +0,0 @@
-# DExTer : Debugging Experience Tester
-# ~~~~~~   ~         ~~         ~   ~~
-#
-# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
-# See https://llvm.org/LICENSE.txt for license information.
-# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
-"""Utility functions for producing command line warnings."""
-
-
-def warn(context, msg, flag=None):
-    if context.options.no_warnings:
-        return
-
-    msg = msg.rstrip()
-    if flag:
-        msg = '{} <y>[{}]</>'.format(msg, flag)
-
-    context.o.auto('warning: <d>{}</>\n'.format(msg))
diff --git a/cross-project-tests/debuginfo-tests/dexter/dex/utils/WorkingDirectory.py b/cross-project-tests/debuginfo-tests/dexter/dex/utils/WorkingDirectory.py
index 04f9b64..06d776f 100644
--- a/cross-project-tests/debuginfo-tests/dexter/dex/utils/WorkingDirectory.py
+++ b/cross-project-tests/debuginfo-tests/dexter/dex/utils/WorkingDirectory.py
@@ -12,7 +12,6 @@
 import time
 
 from dex.utils.Exceptions import Error
-from dex.utils.Warning import warn
 
 class WorkingDirectory(object):
     def __init__(self, context, *args, **kwargs):
@@ -42,5 +41,5 @@
             except OSError:
                 time.sleep(0.1)
 
-        warn(self.context, '"{}" left in place (couldn\'t delete)\n'.format(self.path))
+        self.context.logger.warning(f'"{self.path}" left in place (couldn\'t delete)', enable_prefix=True)
         return
diff --git a/cross-project-tests/debuginfo-tests/dexter/dex/utils/__init__.py b/cross-project-tests/debuginfo-tests/dexter/dex/utils/__init__.py
index ac08139..ae2dea3 100644
--- a/cross-project-tests/debuginfo-tests/dexter/dex/utils/__init__.py
+++ b/cross-project-tests/debuginfo-tests/dexter/dex/utils/__init__.py
@@ -12,10 +12,11 @@
 from dex.utils.PrettyOutputBase import PreserveAutoColors
 from dex.utils.RootDirectory import get_root_directory
 from dex.utils.Timer import Timer
-from dex.utils.Warning import warn
 from dex.utils.WorkingDirectory import WorkingDirectory
 
 if is_native_windows():
     from dex.utils.windows.PrettyOutput import PrettyOutput
 else:
     from dex.utils.posix.PrettyOutput import PrettyOutput
+
+from dex.utils.Logging import Logger