summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRed_M <1468433+Red-M@users.noreply.github.com>2023-03-14 07:21:54 +1000
committerGitHub <noreply@github.com>2023-03-14 07:21:54 +1000
commitdd3cc5c8ea71e1400f0e21fbcc3749c49af5362b (patch)
tree9a74b1ac8ce67b814bd7663998fbd043c076593c
parent5c59b0bc6e1ccc3082f114f2a8615198b86064c9 (diff)
parent571ca4b49d2c469aaaa57bfea6d8a28d8a91c4fc (diff)
downloadpexpect-dd3cc5c8ea71e1400f0e21fbcc3749c49af5362b.tar.gz
Merge pull request #745 from tapple/socket_spawn
Socket Pexpect (for Windows support)
-rw-r--r--doc/api/index.rst1
-rw-r--r--doc/api/socket_pexpect.rst20
-rw-r--r--pexpect/fdpexpect.py6
-rw-r--r--pexpect/socket_pexpect.py145
-rw-r--r--tests/PexpectTestCase.py35
-rwxr-xr-xtests/test_ctrl_chars.py5
-rw-r--r--tests/test_pxssh.py4
-rwxr-xr-xtests/test_run.py6
-rw-r--r--tests/test_socket.py40
-rw-r--r--tests/test_socket_fd.py64
-rw-r--r--tests/test_socket_pexpect.py72
11 files changed, 350 insertions, 48 deletions
diff --git a/doc/api/index.rst b/doc/api/index.rst
index 5277d1c..747c812 100644
--- a/doc/api/index.rst
+++ b/doc/api/index.rst
@@ -6,6 +6,7 @@ API documentation
pexpect
fdpexpect
+ socket_pexpect
popen_spawn
replwrap
pxssh
diff --git a/doc/api/socket_pexpect.rst b/doc/api/socket_pexpect.rst
new file mode 100644
index 0000000..726a999
--- /dev/null
+++ b/doc/api/socket_pexpect.rst
@@ -0,0 +1,20 @@
+socket_pexpect - use pexpect with a socket
+==========================================
+
+.. automodule:: pexpect.socket_pexpect
+
+SocketSpawn class
+-----------------
+
+.. autoclass:: SocketSpawn
+ :show-inheritance:
+
+ .. automethod:: __init__
+ .. automethod:: isalive
+ .. automethod:: close
+
+ .. method:: expect
+ expect_exact
+ expect_list
+
+ As :class:`pexpect.spawn`.
diff --git a/pexpect/fdpexpect.py b/pexpect/fdpexpect.py
index cddd50e..140bdfe 100644
--- a/pexpect/fdpexpect.py
+++ b/pexpect/fdpexpect.py
@@ -1,7 +1,11 @@
-'''This is like pexpect, but it will work with any file descriptor that you
+'''This is like :mod:`pexpect`, but it will work with any file descriptor that you
pass it. You are responsible for opening and close the file descriptor.
This allows you to use Pexpect with sockets and named pipes (FIFOs).
+.. note::
+ socket.fileno() does not give a readable file descriptor on windows.
+ Use :mod:`pexpect.socket_pexpect` for cross-platform socket support
+
PEXPECT LICENSE
This license is approved by the OSI and FSF as GPL-compatible.
diff --git a/pexpect/socket_pexpect.py b/pexpect/socket_pexpect.py
new file mode 100644
index 0000000..cb11ac2
--- /dev/null
+++ b/pexpect/socket_pexpect.py
@@ -0,0 +1,145 @@
+"""This is like :mod:`pexpect`, but it will work with any socket that you
+pass it. You are responsible for opening and closing the socket.
+
+PEXPECT LICENSE
+
+ This license is approved by the OSI and FSF as GPL-compatible.
+ http://opensource.org/licenses/isc-license.txt
+
+ Copyright (c) 2012, Noah Spurrier <noah@noah.org>
+ PERMISSION TO USE, COPY, MODIFY, AND/OR DISTRIBUTE THIS SOFTWARE FOR ANY
+ PURPOSE WITH OR WITHOUT FEE IS HEREBY GRANTED, PROVIDED THAT THE ABOVE
+ COPYRIGHT NOTICE AND THIS PERMISSION NOTICE APPEAR IN ALL COPIES.
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+"""
+
+import socket
+from contextlib import contextmanager
+
+from .exceptions import TIMEOUT, EOF
+from .spawnbase import SpawnBase
+
+__all__ = ["SocketSpawn"]
+
+
+class SocketSpawn(SpawnBase):
+ """This is like :mod:`pexpect.fdpexpect` but uses the cross-platform python socket api,
+ rather than the unix-specific file descriptor api. Thus, it works with
+ remote connections on both unix and windows."""
+
+ def __init__(
+ self,
+ socket: socket.socket,
+ args=None,
+ timeout=30,
+ maxread=2000,
+ searchwindowsize=None,
+ logfile=None,
+ encoding=None,
+ codec_errors="strict",
+ use_poll=False,
+ ):
+ """This takes an open socket."""
+
+ self.args = None
+ self.command = None
+ SpawnBase.__init__(
+ self,
+ timeout,
+ maxread,
+ searchwindowsize,
+ logfile,
+ encoding=encoding,
+ codec_errors=codec_errors,
+ )
+ self.socket = socket
+ self.child_fd = socket.fileno()
+ self.closed = False
+ self.name = "<socket %s>" % socket
+ self.use_poll = use_poll
+
+ def close(self):
+ """Close the socket.
+
+ Calling this method a second time does nothing, but if the file
+ descriptor was closed elsewhere, :class:`OSError` will be raised.
+ """
+ if self.child_fd == -1:
+ return
+
+ self.flush()
+ self.socket.shutdown(socket.SHUT_RDWR)
+ self.socket.close()
+ self.child_fd = -1
+ self.closed = True
+
+ def isalive(self):
+ """ Alive if the fileno is valid """
+ return self.socket.fileno() >= 0
+
+ def send(self, s) -> int:
+ """Write to socket, return number of bytes written"""
+ s = self._coerce_send_string(s)
+ self._log(s, "send")
+
+ b = self._encoder.encode(s, final=False)
+ self.socket.sendall(b)
+ return len(b)
+
+ def sendline(self, s) -> int:
+ """Write to socket with trailing newline, return number of bytes written"""
+ s = self._coerce_send_string(s)
+ return self.send(s + self.linesep)
+
+ def write(self, s):
+ """Write to socket, return None"""
+ self.send(s)
+
+ def writelines(self, sequence):
+ "Call self.write() for each item in sequence"
+ for s in sequence:
+ self.write(s)
+
+ @contextmanager
+ def _timeout(self, timeout):
+ saved_timeout = self.socket.gettimeout()
+ try:
+ self.socket.settimeout(timeout)
+ yield
+ finally:
+ self.socket.settimeout(saved_timeout)
+
+ def read_nonblocking(self, size=1, timeout=-1):
+ """
+ Read from the file descriptor and return the result as a string.
+
+ The read_nonblocking method of :class:`SpawnBase` assumes that a call
+ to os.read will not block (timeout parameter is ignored). This is not
+ the case for POSIX file-like objects such as sockets and serial ports.
+
+ Use :func:`select.select`, timeout is implemented conditionally for
+ POSIX systems.
+
+ :param int size: Read at most *size* bytes.
+ :param int timeout: Wait timeout seconds for file descriptor to be
+ ready to read. When -1 (default), use self.timeout. When 0, poll.
+ :return: String containing the bytes read
+ """
+ if timeout == -1:
+ timeout = self.timeout
+ try:
+ with self._timeout(timeout):
+ s = self.socket.recv(size)
+ if s == b'':
+ self.flag_eof = True
+ raise EOF("Socket closed")
+ return s
+ except socket.timeout:
+ raise TIMEOUT("Timeout exceeded.")
diff --git a/tests/PexpectTestCase.py b/tests/PexpectTestCase.py
index 5d7a168..a762d8f 100644
--- a/tests/PexpectTestCase.py
+++ b/tests/PexpectTestCase.py
@@ -49,22 +49,25 @@ class PexpectTestCase(unittest.TestCase):
print('\n', self.id(), end=' ')
sys.stdout.flush()
- # some build agents will ignore SIGHUP and SIGINT, which python
- # inherits. This causes some of the tests related to terminate()
- # to fail. We set them to the default handlers that they should
- # be, and restore them back to their SIG_IGN value on tearDown.
- #
- # I'm not entirely convinced they need to be restored, only our
- # test runner is affected.
- self.restore_ignored_signals = [
- value for value in (signal.SIGHUP, signal.SIGINT,)
- if signal.getsignal(value) == signal.SIG_IGN]
- if signal.SIGHUP in self.restore_ignored_signals:
- # sighup should be set to default handler
- signal.signal(signal.SIGHUP, signal.SIG_DFL)
- if signal.SIGINT in self.restore_ignored_signals:
- # SIGINT should be set to signal.default_int_handler
- signal.signal(signal.SIGINT, signal.default_int_handler)
+ if sys.platform != 'win32':
+ # some build agents will ignore SIGHUP and SIGINT, which python
+ # inherits. This causes some of the tests related to terminate()
+ # to fail. We set them to the default handlers that they should
+ # be, and restore them back to their SIG_IGN value on tearDown.
+ #
+ # I'm not entirely convinced they need to be restored, only our
+ # test runner is affected.
+ self.restore_ignored_signals = [
+ value for value in (signal.SIGHUP, signal.SIGINT,)
+ if signal.getsignal(value) == signal.SIG_IGN]
+ if signal.SIGHUP in self.restore_ignored_signals:
+ # sighup should be set to default handler
+ signal.signal(signal.SIGHUP, signal.SIG_DFL)
+ if signal.SIGINT in self.restore_ignored_signals:
+ # SIGINT should be set to signal.default_int_handler
+ signal.signal(signal.SIGINT, signal.default_int_handler)
+ else:
+ self.restore_ignored_signals = []
unittest.TestCase.setUp(self)
def tearDown(self):
diff --git a/tests/test_ctrl_chars.py b/tests/test_ctrl_chars.py
index 0719fc7..11fb55c 100755
--- a/tests/test_ctrl_chars.py
+++ b/tests/test_ctrl_chars.py
@@ -26,8 +26,9 @@ from . import PexpectTestCase
import time
import sys
-from ptyprocess import ptyprocess
-ptyprocess._make_eof_intr()
+if sys.platform != 'win32':
+ from ptyprocess import ptyprocess
+ ptyprocess._make_eof_intr()
if sys.version_info[0] >= 3:
def byte(i):
diff --git a/tests/test_pxssh.py b/tests/test_pxssh.py
index c6ec4e2..ba700c8 100644
--- a/tests/test_pxssh.py
+++ b/tests/test_pxssh.py
@@ -1,10 +1,12 @@
#!/usr/bin/env python
+import sys
import os
import shutil
import tempfile
import unittest
-from pexpect import pxssh
+if sys.platform != 'win32':
+ from pexpect import pxssh
from .PexpectTestCase import PexpectTestCase
class SSHTestBase(PexpectTestCase):
diff --git a/tests/test_run.py b/tests/test_run.py
index baa6b49..15d0c40 100755
--- a/tests/test_run.py
+++ b/tests/test_run.py
@@ -52,7 +52,8 @@ def function_events_callback(values):
class RunFuncTestCase(PexpectTestCase.PexpectTestCase):
- runfunc = staticmethod(pexpect.run)
+ if sys.platform != 'win32':
+ runfunc = staticmethod(pexpect.run)
cr = b'\r'
empty = b''
prep_subprocess_out = staticmethod(lambda x: x)
@@ -160,7 +161,8 @@ class RunFuncTestCase(PexpectTestCase.PexpectTestCase):
class RunUnicodeFuncTestCase(RunFuncTestCase):
- runfunc = staticmethod(pexpect.runu)
+ if sys.platform != 'win32':
+ runfunc = staticmethod(pexpect.runu)
cr = b'\r'.decode('ascii')
empty = b''.decode('ascii')
prep_subprocess_out = staticmethod(lambda x: x.decode('utf-8', 'replace'))
diff --git a/tests/test_socket.py b/tests/test_socket.py
index 548d90a..b801b00 100644
--- a/tests/test_socket.py
+++ b/tests/test_socket.py
@@ -19,7 +19,7 @@ PEXPECT LICENSE
'''
import pexpect
-from pexpect import fdpexpect
+from pexpect import socket_pexpect
import unittest
from . import PexpectTestCase
import multiprocessing
@@ -133,12 +133,16 @@ class ExpectTestCase(PexpectTestCase.PexpectTestCase):
pass
exit(0)
+ def spawn(self, socket, timeout=30, use_poll=False):
+ """override me with other ways of spawning on a socket"""
+ return socket_pexpect.SocketSpawn(socket, timeout=timeout, use_poll=use_poll)
+
def socket_fn(self, timed_out, all_read):
result = 0
try:
sock = socket.socket(self.af, socket.SOCK_STREAM)
sock.connect((self.host, self.port))
- session = fdpexpect.fdspawn(sock, timeout=10)
+ session = self.spawn(sock, timeout=10)
# Get all data from server
session.read_nonblocking(size=4096)
all_read.set()
@@ -152,7 +156,7 @@ class ExpectTestCase(PexpectTestCase.PexpectTestCase):
def test_socket(self):
sock = socket.socket(self.af, socket.SOCK_STREAM)
sock.connect((self.host, self.port))
- session = fdpexpect.fdspawn(sock.fileno(), timeout=10)
+ session = self.spawn(sock, timeout=10)
session.expect(self.prompt1)
self.assertEqual(session.before, self.motd)
session.send(self.enter)
@@ -166,7 +170,7 @@ class ExpectTestCase(PexpectTestCase.PexpectTestCase):
def test_socket_with_write(self):
sock = socket.socket(self.af, socket.SOCK_STREAM)
sock.connect((self.host, self.port))
- session = fdpexpect.fdspawn(sock.fileno(), timeout=10)
+ session = self.spawn(sock, timeout=10)
session.expect(self.prompt1)
self.assertEqual(session.before, self.motd)
session.write(self.enter)
@@ -177,19 +181,11 @@ class ExpectTestCase(PexpectTestCase.PexpectTestCase):
session.expect(pexpect.EOF)
self.assertEqual(session.before, b'')
- def test_not_int(self):
- with self.assertRaises(pexpect.ExceptionPexpect):
- session = fdpexpect.fdspawn('bogus', timeout=10)
-
- def test_not_file_descriptor(self):
- with self.assertRaises(pexpect.ExceptionPexpect):
- session = fdpexpect.fdspawn(-1, timeout=10)
-
def test_timeout(self):
with self.assertRaises(pexpect.TIMEOUT):
sock = socket.socket(self.af, socket.SOCK_STREAM)
sock.connect((self.host, self.port))
- session = fdpexpect.fdspawn(sock, timeout=10)
+ session = self.spawn(sock, timeout=10)
session.expect(b'Bogus response')
def test_interrupt(self):
@@ -223,7 +219,7 @@ class ExpectTestCase(PexpectTestCase.PexpectTestCase):
def test_maxread(self):
sock = socket.socket(self.af, socket.SOCK_STREAM)
sock.connect((self.host, self.port))
- session = fdpexpect.fdspawn(sock.fileno(), timeout=10)
+ session = self.spawn(sock, timeout=10)
session.maxread = 1100
session.expect(self.prompt1)
self.assertEqual(session.before, self.motd)
@@ -238,7 +234,7 @@ class ExpectTestCase(PexpectTestCase.PexpectTestCase):
def test_fd_isalive(self):
sock = socket.socket(self.af, socket.SOCK_STREAM)
sock.connect((self.host, self.port))
- session = fdpexpect.fdspawn(sock.fileno(), timeout=10)
+ session = self.spawn(sock, timeout=10)
assert session.isalive()
sock.close()
assert not session.isalive(), "Should not be alive after close()"
@@ -246,7 +242,7 @@ class ExpectTestCase(PexpectTestCase.PexpectTestCase):
def test_fd_isalive_poll(self):
sock = socket.socket(self.af, socket.SOCK_STREAM)
sock.connect((self.host, self.port))
- session = fdpexpect.fdspawn(sock.fileno(), timeout=10, use_poll=True)
+ session = self.spawn(sock, timeout=10, use_poll=True)
assert session.isalive()
sock.close()
assert not session.isalive(), "Should not be alive after close()"
@@ -254,25 +250,17 @@ class ExpectTestCase(PexpectTestCase.PexpectTestCase):
def test_fd_isatty(self):
sock = socket.socket(self.af, socket.SOCK_STREAM)
sock.connect((self.host, self.port))
- session = fdpexpect.fdspawn(sock.fileno(), timeout=10)
+ session = self.spawn(sock, timeout=10)
assert not session.isatty()
session.close()
def test_fd_isatty_poll(self):
sock = socket.socket(self.af, socket.SOCK_STREAM)
sock.connect((self.host, self.port))
- session = fdpexpect.fdspawn(sock.fileno(), timeout=10, use_poll=True)
+ session = self.spawn(sock, timeout=10, use_poll=True)
assert not session.isatty()
session.close()
- def test_fileobj(self):
- sock = socket.socket(self.af, socket.SOCK_STREAM)
- sock.connect((self.host, self.port))
- session = fdpexpect.fdspawn(sock, timeout=10) # Should get the fileno from the socket
- session.expect(self.prompt1)
- session.close()
- assert not session.isalive()
- session.close() # Smoketest - should be able to call this again
if __name__ == '__main__':
unittest.main()
diff --git a/tests/test_socket_fd.py b/tests/test_socket_fd.py
new file mode 100644
index 0000000..5be733c
--- /dev/null
+++ b/tests/test_socket_fd.py
@@ -0,0 +1,64 @@
+#!/usr/bin/env python
+'''
+PEXPECT LICENSE
+
+ This license is approved by the OSI and FSF as GPL-compatible.
+ http://opensource.org/licenses/isc-license.txt
+
+ Copyright (c) 2012, Noah Spurrier <noah@noah.org>
+ PERMISSION TO USE, COPY, MODIFY, AND/OR DISTRIBUTE THIS SOFTWARE FOR ANY
+ PURPOSE WITH OR WITHOUT FEE IS HEREBY GRANTED, PROVIDED THAT THE ABOVE
+ COPYRIGHT NOTICE AND THIS PERMISSION NOTICE APPEAR IN ALL COPIES.
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+'''
+import pexpect
+from pexpect import fdpexpect
+import unittest
+from . import test_socket
+import multiprocessing
+import os
+import signal
+import socket
+import time
+import errno
+
+
+class SocketServerError(Exception):
+ pass
+
+
+class ExpectTestCase(test_socket.ExpectTestCase):
+ """ duplicate of test_socket, but using fdpexpect rather than socket_expect """
+
+ def spawn(self, socket, timeout=30, use_poll=False):
+ return fdpexpect.fdspawn(socket.fileno(), timeout=timeout, use_poll=use_poll)
+
+ def test_not_int(self):
+ with self.assertRaises(pexpect.ExceptionPexpect):
+ session = fdpexpect.fdspawn('bogus', timeout=10)
+
+ def test_not_file_descriptor(self):
+ with self.assertRaises(pexpect.ExceptionPexpect):
+ session = fdpexpect.fdspawn(-1, timeout=10)
+
+ def test_fileobj(self):
+ sock = socket.socket(self.af, socket.SOCK_STREAM)
+ sock.connect((self.host, self.port))
+ session = fdpexpect.fdspawn(sock, timeout=10) # Should get the fileno from the socket
+ session.expect(self.prompt1)
+ session.close()
+ assert not session.isalive()
+ session.close() # Smoketest - should be able to call this again
+
+
+if __name__ == '__main__':
+ unittest.main()
+
+suite = unittest.TestLoader().loadTestsFromTestCase(ExpectTestCase)
diff --git a/tests/test_socket_pexpect.py b/tests/test_socket_pexpect.py
new file mode 100644
index 0000000..8fbcebf
--- /dev/null
+++ b/tests/test_socket_pexpect.py
@@ -0,0 +1,72 @@
+#!/usr/bin/env python
+'''
+PEXPECT LICENSE
+
+ This license is approved by the OSI and FSF as GPL-compatible.
+ http://opensource.org/licenses/isc-license.txt
+
+ Copyright (c) 2012, Noah Spurrier <noah@noah.org>
+ PERMISSION TO USE, COPY, MODIFY, AND/OR DISTRIBUTE THIS SOFTWARE FOR ANY
+ PURPOSE WITH OR WITHOUT FEE IS HEREBY GRANTED, PROVIDED THAT THE ABOVE
+ COPYRIGHT NOTICE AND THIS PERMISSION NOTICE APPEAR IN ALL COPIES.
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+'''
+import pexpect
+from pexpect import socket_pexpect
+import unittest
+from . import PexpectTestCase
+import socket
+
+def open_file_socket(filename):
+ read_socket, write_socket = socket.socketpair()
+ with open(filename, "rb") as file:
+ write_socket.sendall(file.read())
+ write_socket.close()
+ return read_socket
+
+class ExpectTestCase(PexpectTestCase.PexpectTestCase):
+ def setUp(self):
+ print(self.id())
+ PexpectTestCase.PexpectTestCase.setUp(self)
+
+ def test_socket (self):
+ socket = open_file_socket('TESTDATA.txt')
+ s = socket_pexpect.SocketSpawn(socket)
+ s.expect(b'This is the end of test data:')
+ s.expect(pexpect.EOF)
+ self.assertEqual(s.before, b' END\n')
+
+ def test_maxread (self):
+ socket = open_file_socket('TESTDATA.txt')
+ s = socket_pexpect.SocketSpawn(socket)
+ s.maxread = 100
+ s.expect('2')
+ s.expect ('This is the end of test data:')
+ s.expect (pexpect.EOF)
+ self.assertEqual(s.before, b' END\n')
+
+ def test_socket_isalive (self):
+ socket = open_file_socket('TESTDATA.txt')
+ s = socket_pexpect.SocketSpawn(socket)
+ assert s.isalive()
+ s.close()
+ assert not s.isalive(), "Should not be alive after close()"
+
+ def test_socket_isatty (self):
+ socket = open_file_socket('TESTDATA.txt')
+ s = socket_pexpect.SocketSpawn(socket)
+ assert not s.isatty()
+ s.close()
+
+
+if __name__ == '__main__':
+ unittest.main()
+
+suite = unittest.TestLoader().loadTestsFromTestCase(ExpectTestCase)