Add meson test --interactive

This is very similar to --gdb, except it doesn't spawn GDB, but
connects stdin/stdout/stderr directly to the test itself. This allows
interacting with integration tests that spawn a shell in a container
or virtual machine when the test fails.

In systemd we're migrating our integration tests to run using the
meson test runner. We want to allow interactive debugging of failed
tests directly in the virtual machine or container that is spawned
to run the test. To make this possible, we need meson test to connect
stdin/stdout/stderr of the test directly to the user's terminal, just
like is done with the --gdb option.
diff --git a/data/shell-completions/bash/meson b/data/shell-completions/bash/meson
index 55c9c00..dc437f1 100644
--- a/data/shell-completions/bash/meson
+++ b/data/shell-completions/bash/meson
@@ -566,6 +566,7 @@
     no-rebuild
     gdb
     gdb-path
+    interactive
     list
     wrapper
     suite
diff --git a/data/shell-completions/zsh/_meson b/data/shell-completions/zsh/_meson
index e6f50f1..402539f 100644
--- a/data/shell-completions/zsh/_meson
+++ b/data/shell-completions/zsh/_meson
@@ -181,6 +181,7 @@
   '--no-rebuild[do not rebuild before running tests]'
   '--gdb[run tests under gdb]'
   '--gdb-path=[program to run for gdb (can be wrapper or compatible program)]:program:_path_commands'
+  '(--interactive -i)'{'--interactive','-i'}'[run tests with interactive input/output]'
   '--list[list available tests]'
   '(--wrapper --wrap)'{'--wrapper=','--wrap='}'[wrapper to run tests with]:wrapper program:_path_commands'
   "$__meson_cd"
diff --git a/docs/markdown/Unit-tests.md b/docs/markdown/Unit-tests.md
index dc509a8..73e58dc 100644
--- a/docs/markdown/Unit-tests.md
+++ b/docs/markdown/Unit-tests.md
@@ -256,6 +256,16 @@
 $ meson test --print-errorlogs
 ```
 
+Running tests interactively can be done with the `--interactive` option.
+`meson test --interactive` invokes tests with stdout, stdin and stderr
+connected directly to the calling terminal. This can be useful if your test is
+an integration test running in a container or virtual machine where a debug
+shell is spawned if it fails *(added 1.5.0)*:
+
+```console
+$ meson test --interactive testname
+```
+
 Meson will report the output produced by the failing tests along with
 other useful information as the environmental variables. This is
 useful, for example, when you run the tests on Travis-CI, Jenkins and
diff --git a/docs/markdown/snippets/test_interactive.md b/docs/markdown/snippets/test_interactive.md
new file mode 100644
index 0000000..907147f
--- /dev/null
+++ b/docs/markdown/snippets/test_interactive.md
@@ -0,0 +1,6 @@
+## The Meson test program supports a new "--interactive" argument
+
+`meson test --interactive` invokes tests with stdout, stdin and stderr
+connected directly to the calling terminal. This can be useful when running
+integration tests that run in containers or virtual machines which can spawn a
+debug shell if a test fails.
diff --git a/mesonbuild/mtest.py b/mesonbuild/mtest.py
index b9f72fa..311274f 100644
--- a/mesonbuild/mtest.py
+++ b/mesonbuild/mtest.py
@@ -127,6 +127,8 @@
                         help='Run test under gdb.')
     parser.add_argument('--gdb-path', default='gdb', dest='gdb_path',
                         help='Path to the gdb binary (default: gdb).')
+    parser.add_argument('-i', '--interactive', default=False, dest='interactive',
+                        action='store_true', help='Run tests with interactive input/output.')
     parser.add_argument('--list', default=False, dest='list', action='store_true',
                         help='List available tests.')
     parser.add_argument('--wrapper', default=None, dest='wrapper', type=split_args,
@@ -233,8 +235,8 @@
     # the logger can use the console
     LOGGER = 0
 
-    # the console is used by gdb
-    GDB = 1
+    # the console is used by gdb or the user
+    INTERACTIVE = 1
 
     # the console is used to write stdout/stderr
     STDOUT = 2
@@ -1417,7 +1419,7 @@
         if ('MSAN_OPTIONS' not in env or not env['MSAN_OPTIONS']):
             env['MSAN_OPTIONS'] = 'halt_on_error=1:abort_on_error=1:print_summary=1:print_stacktrace=1'
 
-        if self.options.gdb or self.test.timeout is None or self.test.timeout <= 0:
+        if self.options.interactive or self.test.timeout is None or self.test.timeout <= 0:
             timeout = None
         elif self.options.timeout_multiplier is None:
             timeout = self.test.timeout
@@ -1426,12 +1428,12 @@
         else:
             timeout = self.test.timeout * self.options.timeout_multiplier
 
-        is_parallel = test.is_parallel and self.options.num_processes > 1 and not self.options.gdb
+        is_parallel = test.is_parallel and self.options.num_processes > 1 and not self.options.interactive
         verbose = (test.verbose or self.options.verbose) and not self.options.quiet
         self.runobj = TestRun(test, env, name, timeout, is_parallel, verbose)
 
-        if self.options.gdb:
-            self.console_mode = ConsoleUser.GDB
+        if self.options.interactive:
+            self.console_mode = ConsoleUser.INTERACTIVE
         elif self.runobj.direct_stdout:
             self.console_mode = ConsoleUser.STDOUT
         else:
@@ -1499,13 +1501,13 @@
                               stdout: T.Optional[int], stderr: T.Optional[int],
                               env: T.Dict[str, str], cwd: T.Optional[str]) -> TestSubprocess:
         # Let gdb handle ^C instead of us
-        if self.options.gdb:
+        if self.options.interactive:
             previous_sigint_handler = signal.getsignal(signal.SIGINT)
             # Make the meson executable ignore SIGINT while gdb is running.
             signal.signal(signal.SIGINT, signal.SIG_IGN)
 
         def preexec_fn() -> None:
-            if self.options.gdb:
+            if self.options.interactive:
                 # Restore the SIGINT handler for the child process to
                 # ensure it can handle it.
                 signal.signal(signal.SIGINT, signal.SIG_DFL)
@@ -1516,7 +1518,7 @@
                 os.setsid()
 
         def postwait_fn() -> None:
-            if self.options.gdb:
+            if self.options.interactive:
                 # Let us accept ^C again
                 signal.signal(signal.SIGINT, previous_sigint_handler)
 
@@ -1530,7 +1532,7 @@
                               postwait_fn=postwait_fn if not is_windows() else None)
 
     async def _run_cmd(self, harness: 'TestHarness', cmd: T.List[str]) -> None:
-        if self.console_mode is ConsoleUser.GDB:
+        if self.console_mode is ConsoleUser.INTERACTIVE:
             stdout = None
             stderr = None
         else:
@@ -1591,7 +1593,7 @@
         self.ninja: T.List[str] = None
 
         self.logfile_base: T.Optional[str] = None
-        if self.options.logbase and not self.options.gdb:
+        if self.options.logbase and not self.options.interactive:
             namebase = None
             self.logfile_base = os.path.join(self.options.wd, 'meson-logs', self.options.logbase)
 
@@ -1691,6 +1693,7 @@
         if not options.gdb:
             options.gdb = current.gdb
         if options.gdb:
+            options.interactive = True
             options.verbose = True
         if options.timeout_multiplier is None:
             options.timeout_multiplier = current.timeout_multiplier
@@ -2143,7 +2146,7 @@
     return True
 
 def run(options: argparse.Namespace) -> int:
-    if options.benchmark:
+    if options.benchmark or options.interactive:
         options.num_processes = 1
 
     if options.verbose and options.quiet:
@@ -2152,12 +2155,15 @@
 
     check_bin = None
     if options.gdb:
-        options.verbose = True
+        options.interactive = True
         if options.wrapper:
             print('Must not specify both a wrapper and gdb at the same time.')
             return 1
         check_bin = 'gdb'
 
+    if options.interactive:
+        options.verbose = True
+
     if options.wrapper:
         check_bin = options.wrapper[0]