Repository: mesonbuild/meson

Issue:
testlog.txt etc don't contain output of tap tests anymore
Starting with 702f3fe15 meson-logs/testlog.txt don't contain output from tap test failures anymore. And not just when the test succeed, even when the tap test failed. @trhd I assume this was not intentional?

Reproducer:
```
project('repro', ['c'])

test('ex', find_program('echo'),
     args: ['ok 1 - frak
# diag
not ok 2 - blarg
1..2
'],
     protocol: 'tap',
    )
```
with 0.62 that'll include 
```
----------------------------------- output -----------------------------------
ok 1 - frak
# diag
not ok 2 - blarg
1..2
```
in the log, but with 702f3fe15 that's not included anymore.

Obviously testlog.txt (etc) need to include the test output...


Discussion:
Not intentional. Seems to have slipped in when #9930's commits were reordered for partial merging. Seems to be fixed in commit 89f4666590dc770b1ebbc634586548d2cc77f37a there. Perhaps that could be merged if not as a part of the #9930 then alone just to fix this? It is the first commit in the series.
> Seems to have slipped in when #9930's commits were reordered for partial merging.

It could equally be said that it slipped in when 702f3fe15f767692928073a60bde8b699fdd445e and 89f4666590dc770b1ebbc634586548d2cc77f37a were originally committed as one commit that broke functionality and another commit that then fixed it, such that neither commit was standalone...

Anyway @bonzini opinions?
> > Seems to have slipped in when #9930's commits were reordered for partial merging.
> 
> It could equally be said that it slipped in when [702f3fe](https://github.com/mesonbuild/meson/commit/702f3fe15f767692928073a60bde8b699fdd445e) and [89f4666](https://github.com/mesonbuild/meson/commit/89f4666590dc770b1ebbc634586548d2cc77f37a) were originally committed as one commit that broke functionality and another commit that then fixed it, such that neither commit was standalone...

OT: Three commits involved. Modification A, B and C. A and B are "internal" and C uses the end result. Now C ended up being merged before B and was thus broken.



Resolving patch:
diff --git a/mesonbuild/mtest.py b/mesonbuild/mtest.py
--- a/mesonbuild/mtest.py
+++ b/mesonbuild/mtest.py
@@ -1108,7 +1108,9 @@ def decode(stream: T.Union[None, bytes]) -> str:
     except UnicodeDecodeError:
         return stream.decode('iso-8859-1', errors='ignore')
 
-async def read_decode(reader: asyncio.StreamReader, console_mode: ConsoleUser) -> str:
+async def read_decode(reader: asyncio.StreamReader,
+                      queue: T.Optional['asyncio.Queue[T.Optional[str]]'],
+                      console_mode: ConsoleUser) -> str:
     stdo_lines = []
     try:
         while not reader.at_eof():
@@ -1124,26 +1126,14 @@ async def read_decode(reader: asyncio.StreamReader, console_mode: ConsoleUser) -
                 stdo_lines.append(line)
                 if console_mode is ConsoleUser.STDOUT:
                     print(line, end='', flush=True)
-        return ''.join(stdo_lines)
-    except asyncio.CancelledError:
-        return ''.join(stdo_lines)
-
-# Extract lines out of the StreamReader.  Print them
-# along the way if requested, and at the end collect
-# them all into a future.
-async def read_decode_lines(reader: asyncio.StreamReader,
-                            q: 'asyncio.Queue[T.Optional[str]]') -> str:
-    stdo_lines = []
-    try:
-        while not reader.at_eof():
-            line = decode(await reader.readline())
-            stdo_lines.append(line)
-            await q.put(line)
+                if queue:
+                    await queue.put(line)
         return ''.join(stdo_lines)
     except asyncio.CancelledError:
         return ''.join(stdo_lines)
     finally:
-        await q.put(None)
+        if queue:
+            await queue.put(None)
 
 def run_with_mono(fname: str) -> bool:
     return fname.endswith('.exe') and not (is_windows() or is_cygwin())
@@ -1220,12 +1210,11 @@ def __init__(self, p: asyncio.subprocess.Process,
         self.stde_task = None            # type: T.Optional[asyncio.Future[str]]
         self.postwait_fn = postwait_fn   # type: T.Callable[[], None]
         self.all_futures = []            # type: T.List[asyncio.Future]
+        self.queue = None                # type: T.Optional[asyncio.Queue[T.Optional[str]]]
 
     def stdout_lines(self) -> T.AsyncIterator[str]:
-        q = asyncio.Queue()              # type: asyncio.Queue[T.Optional[str]]
-        decode_coro = read_decode_lines(self._process.stdout, q)
-        self.stdo_task = asyncio.ensure_future(decode_coro)
-        return queue_iter(q)
+        self.queue = asyncio.Queue()
+        return queue_iter(self.queue)
 
     def communicate(self,
                     test: 'TestRun',
@@ -1234,12 +1223,12 @@ def communicate(self,
         async def collect_stdo(test: 'TestRun',
                                reader: asyncio.StreamReader,
                                console_mode: ConsoleUser) -> None:
-            test.stdo = await read_decode(reader, console_mode)
+            test.stdo = await read_decode(reader, self.queue, console_mode)
 
         async def collect_stde(test: 'TestRun',
                                reader: asyncio.StreamReader,
                                console_mode: ConsoleUser) -> None:
-            test.stde = await read_decode(reader, console_mode)
+            test.stde = await read_decode(reader, None, console_mode)
 
         # asyncio.ensure_future ensures that printing can
         # run in the background, even before it is awaited
@@ -1483,11 +1472,10 @@ async def _run_cmd(self, harness: 'TestHarness', cmd: T.List[str]) -> None:
         if self.runobj.needs_parsing:
             parse_coro = self.runobj.parse(harness, p.stdout_lines())
             parse_task = asyncio.ensure_future(parse_coro)
-            stdo_task = stde_task = None
         else:
-            stdo_task, stde_task = p.communicate(self.runobj, self.console_mode)
             parse_task = None
 
+        stdo_task, stde_task = p.communicate(self.runobj, self.console_mode)
         await p.wait(self.runobj)
 
         if parse_task:
diff --git a/unittests/allplatformstests.py b/unittests/allplatformstests.py
--- a/unittests/allplatformstests.py
+++ b/unittests/allplatformstests.py
@@ -577,6 +577,68 @@ def test_verbose(self):
         out = self._run(self.mtest_command + ['--suite', 'verbose'])
         self.assertIn('1/1 subtest 1', out)
 
+    def test_long_output(self):
+        testdir = os.path.join(self.common_test_dir, '253 long output')
+        self.init(testdir)
+        self.build()
+        self.run_tests()
+
+        # Ensure lines are found from testlog.txt when not being verbose.
+
+        i = 1
+        with open(os.path.join(self.logdir, 'testlog.txt'), encoding='utf-8') as f:
+            line = f.readline()
+            while line and i < 100001:
+                if f'# Iteration {i} to stdout' in line:
+                    i += 1
+                line = f.readline()
+            self.assertEqual(i, 100001)
+
+            i = 1
+            while line:
+                if f'# Iteration {i} to stderr' in line:
+                    i += 1
+                line = f.readline()
+        self.assertEqual(i, 100001)
+
+        # Ensure lines are found from both testlog.txt and console when being verbose.
+
+        out = self._run(self.mtest_command + ['-v'])
+        i = 1
+        with open(os.path.join(self.logdir, 'testlog.txt'), encoding='utf-8') as f:
+            line = f.readline()
+            while line and i < 100001:
+                if f'# Iteration {i} to stdout' in line:
+                    i += 1
+                line = f.readline()
+            self.assertEqual(i, 100001)
+
+            i = 1
+            while line:
+                if f'# Iteration {i} to stderr' in line:
+                    i += 1
+                line = f.readline()
+        self.assertEqual(i, 100001)
+
+        lines = out.split('\n')
+        line_number = 0
+        i = 1
+        while line_number < len(lines) and i < 100001:
+            print('---> %s' % lines[line_number])
+            if f'# Iteration {i} to stdout' in lines[line_number]:
+                i += 1
+            line_number += 1
+        self.assertEqual(i, 100001)
+
+        line_number = 0
+        i = 1
+        while line_number < len(lines):
+            if f'# Iteration {i} to stderr' in lines[line_number]:
+                i += 1
+            line_number += 1
+        self.assertEqual(i, 100001)
+
+
     def test_testsetups(self):
         if not shutil.which('valgrind'):
             raise SkipTest('Valgrind not installed.')


Test patch:
diff --git a/test cases/common/253 long output/dumper.c b/test cases/common/253 long output/dumper.c
new file mode 100644
--- /dev/null
+++ b/test cases/common/253 long output/dumper.c	
@@ -0,0 +1,17 @@
+#include <stdio.h>
+
+int main(void)
+{
+    for (int i = 0 ; i < 100000 ; i++)
+        fprintf(stderr, "# Iteration %d to stderr\n", i + 1);
+
+    printf("ok 1 - dumper to stderr\n");
+
+    for (int i = 0 ; i < 100000 ; i++)
+        fprintf(stdout, "# Iteration %d to stdout\n", i + 1);
+
+    printf("ok 2 - dumper to stdout\n1..2\n");
+
+    return 0;
+}
+
diff --git a/test cases/common/253 long output/meson.build b/test cases/common/253 long output/meson.build
new file mode 100644
--- /dev/null
+++ b/test cases/common/253 long output/meson.build	
@@ -0,0 +1,5 @@
+project('long-stderr', 'c')
+
+dumper = executable('dumper', 'dumper.c')
+test('dump-test', dumper)
+test('dump-test-TAP', dumper, protocol : 'tap')