Repository: mesonbuild/meson

Issue:
Meson Build crash on unit test on TimeoutError
**Describe the bug**
Meson build crashes when running unit test for my project resulting in a Timeout Error crashing Python that runs Meson Build.

**To Reproduce**
Check out git repo at: https://codeberg.org/TylerLinuxDev/STL-For-C/src/branch/reproducebug under "reproducebug" branch and simply do the followings:

```
git checkout https://codeberg.org/TylerLinuxDev/STL-For-C.git
cd STL-For-C
git checkout reproducebug
git submodule update --init --recursive
meson setup build
meson test -C build
```

and Meson will simply crash after one of the unit test reach 30 seconds time out.

**Expected behavior**
Not to crash meson and simply say test case was timed out and to continue on to the next test case.

**system parameters**
* Plain native build on Arch Linux
* Arch Linux, updated yesterday.
* Python Version: 3.10.6
* Meson Version: 0.63.1
* Ninja build Version: 1.11.0

**Error Output:**

```
24/26 stlc:queue / Generic Queue Test - Test Circular Buffer                             OK             17.96s
Exception in callback TestHarness._run_tests.<locals>.test_done(<Task finishe...imeoutError()>) at /usr/lib/python3.10/site-packages/mesonbuild/mtest.py:1882
handle: <Handle TestHarness._run_tests.<locals>.test_done(<Task finishe...imeoutError()>) at /usr/lib/python3.10/site-packages/mesonbuild/mtest.py:1882>
Traceback (most recent call last):
  File "/usr/lib/python3.10/asyncio/subprocess.py", line 134, in wait
    return await self._transport._wait()
  File "/usr/lib/python3.10/asyncio/base_subprocess.py", line 235, in _wait
    return await waiter
asyncio.exceptions.CancelledError

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/usr/lib/python3.10/asyncio/tasks.py", line 456, in wait_for
    return fut.result()
asyncio.exceptions.CancelledError

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "/usr/lib/python3.10/asyncio/events.py", line 80, in _run
    self._context.run(self._callback, *self._args)
  File "/usr/lib/python3.10/site-packages/mesonbuild/mtest.py", line 1884, in test_done
    f.result()
  File "/usr/lib/python3.10/site-packages/mesonbuild/mtest.py", line 1879, in run_test
    res = await test.run(self)
  File "/usr/lib/python3.10/site-packages/mesonbuild/mtest.py", line 1416, in run
    await self._run_cmd(harness, cmd)
  File "/usr/lib/python3.10/site-packages/mesonbuild/mtest.py", line 1483, in _run_cmd
    await p.wait(self.runobj)
  File "/usr/lib/python3.10/site-packages/mesonbuild/mtest.py", line 1305, in wait
    test.additional_error += await self._kill() or ''
  File "/usr/lib/python3.10/site-packages/mesonbuild/mtest.py", line 1266, in _kill
    await asyncio.wait_for(p.wait(), timeout=0.5)
  File "/usr/lib/python3.10/asyncio/tasks.py", line 458, in wait_for
    raise exceptions.TimeoutError() from exc
asyncio.exceptions.TimeoutError

Ok:                 24  
Expected Fail:      0   
Fail:               0   
Unexpected Pass:    0   
Skipped:            0   
Timeout:            0   

Full log written to /home/tyler/Repos/STL-For-C/build/meson-logs/testlog.txt
Traceback (most recent call last):
  File "/usr/lib/python3.10/asyncio/subprocess.py", line 134, in wait
    return await self._transport._wait()
  File "/usr/lib/python3.10/asyncio/base_subprocess.py", line 235, in _wait
    return await waiter
asyncio.exceptions.CancelledError

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/usr/lib/python3.10/asyncio/tasks.py", line 456, in wait_for
    return fut.result()
asyncio.exceptions.CancelledError

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "/usr/lib/python3.10/site-packages/mesonbuild/mesonmain.py", line 153, in run
    return options.run_func(options)
  File "/usr/lib/python3.10/site-packages/mesonbuild/mtest.py", line 2040, in run
    return th.doit()
  File "/usr/lib/python3.10/site-packages/mesonbuild/mtest.py", line 1717, in doit
    self.run_tests(runners)
  File "/usr/lib/python3.10/site-packages/mesonbuild/mtest.py", line 1856, in run_tests
    loop.run_until_complete(self._run_tests(runners))
  File "/usr/lib/python3.10/asyncio/base_events.py", line 646, in run_until_complete
    return future.result()
  File "/usr/lib/python3.10/site-packages/mesonbuild/mtest.py", line 1949, in _run_tests
    await complete(future)
  File "/usr/lib/python3.10/site-packages/mesonbuild/mtest.py", line 1170, in complete
    await future
asyncio.exceptions.TimeoutError

ERROR: Unhandled python exception

    This is a Meson bug and should be reported!
```


Discussion:

https://user-images.githubusercontent.com/107034654/187063895-c94d338b-18af-4389-8384-5298783a6695.mp4


It seems to occur on high contention test case where all of the threads on PC is occupied or in use.
Here is a somewhat minimal [reproducer](https://git.sr.ht/~lattis/muon/tree/master/item/tests/project/muon/timeout).
Gentoo discovered this in https://bugs.gentoo.org/880863

Resolving patch:
diff --git a/mesonbuild/mtest.py b/mesonbuild/mtest.py
--- a/mesonbuild/mtest.py
+++ b/mesonbuild/mtest.py
@@ -1265,14 +1265,14 @@ async def _kill(self) -> T.Optional[str]:
 
                 # Make sure the termination signal actually kills the process
                 # group, otherwise retry with a SIGKILL.
-                with suppress(TimeoutError):
+                with suppress(asyncio.TimeoutError):
                     await asyncio.wait_for(p.wait(), timeout=0.5)
                 if p.returncode is not None:
                     return None
 
                 os.killpg(p.pid, signal.SIGKILL)
 
-            with suppress(TimeoutError):
+            with suppress(asyncio.TimeoutError):
                 await asyncio.wait_for(p.wait(), timeout=1)
             if p.returncode is not None:
                 return None
@@ -1281,7 +1281,7 @@ async def _kill(self) -> T.Optional[str]:
             # Try to kill it one last time with a direct call.
             # If the process has spawned children, they will remain around.
             p.kill()
-            with suppress(TimeoutError):
+            with suppress(asyncio.TimeoutError):
                 await asyncio.wait_for(p.wait(), timeout=1)
             if p.returncode is not None:
                 return None
diff --git a/unittests/linuxliketests.py b/unittests/linuxliketests.py
--- a/unittests/linuxliketests.py
+++ b/unittests/linuxliketests.py
@@ -1820,3 +1820,11 @@ def test_isystem_default_removal_with_symlink(self):
                 default_symlinks.append(symlink)
                 os.symlink(default_dirs[i], symlink)
             self.assertFalse(cpp.compiler_args([f'-isystem{symlink}' for symlink in default_symlinks]).to_native())
+
+    def test_freezing(self):
+        testdir = os.path.join(self.unit_test_dir, '109 freeze')
+        self.init(testdir)
+        self.build()
+        with self.assertRaises(subprocess.CalledProcessError) as e:
+            self.run_tests()
+        self.assertNotIn('Traceback', e.exception.output)


Test patch:
diff --git a/test cases/unit/109 freeze/freeze.c b/test cases/unit/109 freeze/freeze.c
new file mode 100644
--- /dev/null
+++ b/test cases/unit/109 freeze/freeze.c	
@@ -0,0 +1,21 @@
+#include<stdio.h>
+#include <signal.h>
+#include <string.h>
+#include <stdlib.h>
+
+static void do_nothing(int signo, siginfo_t *info, void *context) {
+}
+
+int main(int argc, char **argv) {
+    struct sigaction sa;
+    memset(&sa, 0, sizeof(struct sigaction));
+    sa.sa_sigaction = do_nothing;
+    if (sigaction(SIGTERM, &sa, NULL) == -1) {
+        printf("Could not set up signal handler.\n");
+        return 1;
+    }
+    printf("Freezing forever.\n");
+    while(1) {
+    }
+    return 0;
+}
diff --git a/test cases/unit/109 freeze/meson.build b/test cases/unit/109 freeze/meson.build
new file mode 100644
--- /dev/null
+++ b/test cases/unit/109 freeze/meson.build	
@@ -0,0 +1,4 @@
+project('freeze', 'c')
+
+e = executable('freeze', 'freeze.c')
+test('freeze', e, timeout: 1)