Repository: radiasoft/pykern

Issue:
support for sirepo#6636
FileNotFoundError needs to be matched by pkio.exception_is_not_found.

Discussion:
e-carlin: Can you fix the title? I don't think #434 is the correct issue.

Patch:
diff --git a/pykern/pkdebug.py b/pykern/pkdebug.py
index 7f05bb6..3f153e2 100644
--- a/pykern/pkdebug.py
+++ b/pykern/pkdebug.py
@@ -174,7 +174,7 @@ def pkdc(fmt, *args, **kwargs):
         _printer._write(fmt, args, kwargs, with_control=True)
 
 
-def pkdexc():
+def pkdexc(exc_info=None):
     """Return last exception and stack as a string
 
     Must be called from an ``except``. This function removes
@@ -189,13 +189,15 @@ def pkdexc():
         try:
             something
         except:
-            pkdp(pkdexc())
+            pkdlog("exception={}", pkdexc())
 
+    Args:
+        exc_info (tuple): (type(e), e, traceback) [sys.exc_info]
     Returns:
         str: formatted exception and stack trace
     """
     try:
-        e = sys.exc_info()
+        e = exc_info or sys.exc_info()
         if hasattr(traceback, "TracebackException"):
             return "".join(
                 traceback.format_exception(*e)
diff --git a/pykern/pkinspect.py b/pykern/pkinspect.py
index 41bb68d..9abd700 100644
--- a/pykern/pkinspect.py
+++ b/pykern/pkinspect.py
@@ -151,6 +151,17 @@ def caller(ignore_modules=None, exclude_first=True):
             del frame
 
 
+def caller_func_name():
+    """Name of function one frame back
+
+    Useful for inter-module dispatch and errors.
+
+    Returns:
+        str: function name
+    """
+    return inspect.currentframe().f_back.f_back.f_code.co_name
+
+
 def caller_module(exclude_first=True):
     """Which module is calling the caller of this function.
 
diff --git a/pykern/pkio.py b/pykern/pkio.py
index 9f2355a..f1d917a 100644
--- a/pykern/pkio.py
+++ b/pykern/pkio.py
@@ -80,7 +80,9 @@ def compare_files(path1, path2, force=False):
 
 
 def exception_is_not_found(exc):
-    """True if exception is IOError and ENOENT
+    """True if exception is one various file not found errors
+
+    Checks file `FileNotFoundError`, `IO
 
     Args:
         exc (BaseException): to check
@@ -89,7 +91,8 @@ def exception_is_not_found(exc):
         bool: True if is a file not found exception.
     """
     return (
-        isinstance(exc, IOError)
+        isinstance(exc, FileNotFoundError)
+        or isinstance(exc, IOError)
         and exc.errno == errno.ENOENT
         or isinstance(exc, py.error.ENOENT)
     )
diff --git a/pykern/pkunit.py b/pykern/pkunit.py
index b7d201b..5921ecf 100644
--- a/pykern/pkunit.py
+++ b/pykern/pkunit.py
@@ -18,7 +18,9 @@ import json
 import os
 import py
 import pytest
+import random
 import re
+import socket
 import subprocess
 import sys
 import traceback
@@ -32,6 +34,9 @@ RESTARTABLE = "PYKERN_PKUNIT_RESTARTABLE"
 #: Where persistent input files are stored (test_base_name_data)
 DATA_DIR_SUFFIX = "_data"
 
+#: Used to create test servers
+LOCALHOST_IP = "127.0.0.1"
+
 #: Where to write temporary files (test_base_name_work)
 WORK_DIR_SUFFIX = "_work"
 
@@ -278,6 +283,31 @@ def file_eq(expect_path, *args, **kwargs):
     _FileEq(expect_path, *args, **kwargs)
 
 
+def unbound_localhost_tcp_port(start, stop):
+    """Looks for AF_INET SOCK_STREAM port for which bind succeeds
+
+    Args:
+        start (int): first port
+        stop (int): one greater than last port (passed to range)
+    Returns:
+        int: port is available or raises ValueError
+    """
+
+    def _check_port(port):
+        with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
+            s.bind((LOCALHOST_IP, int(port)))
+        return port
+
+    for p in random.sample(range(start, stop), 100):
+        try:
+            return _check_port(p)
+        except Exception:
+            pass
+    raise ValueError(
+        f"unable find port random sample range={start}-{stop} tries=100 ip={LOCALHOST_IP}"
+    )
+
+
 def is_test_run():
     """Running in a test?
 


Test patch:
diff --git a/tests/pkasyncio_test.py b/tests/pkasyncio_test.py
index 6354f34..0e9840c 100644
--- a/tests/pkasyncio_test.py
+++ b/tests/pkasyncio_test.py
@@ -4,7 +4,6 @@
 :license: http://www.apache.org/licenses/LICENSE-2.0.html
 """
 
-_LOCALHOST = "127.0.0.1"
 _URI = "/echo"
 
 _PORT = None
@@ -12,14 +11,18 @@ _PORT = None
 
 def test_websocket():
     import os, signal, time
+    from pykern import pkunit, pkdebug
 
     global _PORT
 
-    _PORT = _port()
+    _PORT = pkunit.unbound_localhost_tcp_port(37000, 38000)
     p = os.fork()
     if p == 0:
         try:
+            pkdebug.pkdlog("start server")
             _server()
+        except Exception as e:
+            pkdebug.pkdlog("server exception={} stack={}", e, pkdebug.pkdexc())
         finally:
             os._exit(0)
     try:
@@ -30,23 +33,23 @@ def test_websocket():
 
 
 def _client():
-    from pykern import pkdebug
+    from pykern import pkdebug, pkunit
     import asyncio
 
     async def _all():
         w = await _open()
         pkdebug.pkdlog("open")
-        await _send(w)
+        await _send(w, "hello")
         pkdebug.pkdlog("send")
-        pkdebug.pkdlog("recv={}", await _recv(w))
-        return
+        pkunit.pkeq("hello", await _recv(w))
+        pkdebug.pkdlog("recv")
 
     async def _open():
         from tornado import httpclient, websocket
 
         return await websocket.websocket_connect(
             httpclient.HTTPRequest(
-                url=f"ws://{_LOCALHOST}:{_PORT}{_URI}",
+                url=f"ws://{pkunit.LOCALHOST_IP}:{_PORT}{_URI}",
             ),
             ping_interval=100,
             ping_timeout=1000,
@@ -55,8 +58,8 @@ def _client():
     async def _recv(ws):
         return await ws.read_message()
 
-    async def _send(ws):
-        await ws.write_message("hello")
+    async def _send(ws, text):
+        await ws.write_message(text)
 
     asyncio.run(_all())
 
@@ -71,32 +74,16 @@ def _client():
     #     pkdebug.pkdlog("exception={} stack={}", e, pkdebug.pkdexc())
 
 
-def _port():
-    import random
-
-    def _check_port(port):
-        import socket
-
-        with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
-            s.bind((_LOCALHOST, int(port)))
-        return port
-
-    for p in random.sample(range(37000, 37100), 10):
-        try:
-            return _check_port(p)
-        except Exception:
-            pass
-    raise AssertionError(
-        f"ip={_LOCALHOST} unable to bind to port in range={37000-37100}"
-    )
-
-
 def _server():
-    from pykern import pkasyncio, pkdebug
+    from pykern import pkasyncio, pkdebug, pkunit
     from pykern.pkcollections import PKDict
     from tornado import websocket
 
     class _Echo(websocket.WebSocketHandler):
+        def open(self):
+            # just to print something
+            pkdebug.pkdlog("remote_ip={}", self.request.remote_ip)
+
         async def on_message(self, msg):
             import asyncio
 
@@ -112,7 +99,7 @@ def _server():
         PKDict(
             uri_map=((_URI, _Echo),),
             tcp_port=_PORT,
-            tcp_ip=_LOCALHOST,
+            tcp_ip=pkunit.LOCALHOST_IP,
         )
     )
     l.start()
diff --git a/tests/pkinspect_test.py b/tests/pkinspect_test.py
index 3502c5f..1e2c83b 100644
--- a/tests/pkinspect_test.py
+++ b/tests/pkinspect_test.py
@@ -53,20 +53,23 @@ def test_caller():
     import inspect
     import sys
 
+    def _func_name(expect):
+        pkunit.pkeq(expect, pkinspect.caller_func_name())
+
     m1 = pkunit.import_module_from_data_dir("p1.m1")
     c = m1.caller()
     expect = inspect.currentframe().f_lineno - 1
-    assert expect == c.lineno, "{}: unexpected lineno, should be {}".format(
-        c.lineno, expect
-    )
+    pkunit.pkeq(expect, c.lineno)
     expect = "test_caller"
-    assert expect == c.name, "{}: expected function name {}".format(c.name, expect)
+    pkunit.pkeq(expect, c.name)
+    _func_name(expect)
     this_module = sys.modules[__name__]
     c = m1.caller(ignore_modules=[this_module])
-    assert expect != c.name, "{}: should not be {}".format(c.name, expect)
+    pkunit.pkne(expect, c.name)
     my_caller = pkinspect.caller()
     expect = my_caller._module
-    assert expect == c._module, "{}: should be {}".format(c._module, expect)
+    pkunit.pkeq(expect, c._module)
+    pkunit.pkeq(expect, c._module)
 
 
 def test_caller_module():