diff --git a/grizzly/common/runner.py b/grizzly/common/runner.py index 9a18855f..6f6ee76d 100644 --- a/grizzly/common/runner.py +++ b/grizzly/common/runner.py @@ -283,7 +283,7 @@ def run( testcase.data_path, continue_cb=self._keep_waiting, forever=wait_for_callback, - optional_files=tuple(testcase.optional), + required_files=tuple(testcase.required), server_map=server_map, ) duration = time() - serve_start diff --git a/grizzly/common/storage.py b/grizzly/common/storage.py index 13062d97..22390a3c 100644 --- a/grizzly/common/storage.py +++ b/grizzly/common/storage.py @@ -534,6 +534,19 @@ def purge_optional(self, keep): for idx in reversed(to_remove): self._files.optional.pop(idx).data_file.unlink() + @property + def required(self): + """Get file paths of required files. + + Args: + None + + Yields: + str: File path of each file. + """ + for test in self._files.required: + yield test.file_name + @staticmethod def sanitize_path(path): """Sanitize given path for use as a URI path. diff --git a/grizzly/common/test_runner.py b/grizzly/common/test_runner.py index d82c9bae..81eae588 100644 --- a/grizzly/common/test_runner.py +++ b/grizzly/common/test_runner.py @@ -67,7 +67,9 @@ def test_runner_02(mocker): target.check_result.return_value = Result.NONE serv_files = ["a.bin"] server.serve_path.return_value = (Served.ALL, serv_files) - testcase = mocker.Mock(spec_set=TestCase, entry_point=serv_files[0], optional=[]) + testcase = mocker.Mock( + spec_set=TestCase, entry_point=serv_files[0], required=serv_files + ) # single run/iteration relaunch (not idle exit) target.is_idle.return_value = False runner = Runner(server, target, relaunch=1) @@ -139,7 +141,7 @@ def test_runner_03(mocker, srv_result, served): server.serve_path.return_value = (srv_result, served) target = mocker.Mock(spec_set=Target) target.check_result.return_value = Result.NONE - testcase = mocker.Mock(spec_set=TestCase, entry_point="x", optional=[]) + testcase = mocker.Mock(spec_set=TestCase, entry_point="x", required=["x"]) runner = Runner(server, target) result = runner.run([], ServerMap(), testcase) assert runner.initial @@ -166,7 +168,7 @@ def test_runner_04(mocker, ignore, status, idle, check_result): """test reporting timeout""" server = mocker.Mock(spec_set=Sapphire) target = mocker.Mock(spec_set=Target) - testcase = mocker.Mock(spec_set=TestCase, entry_point="a.bin", optional=[]) + testcase = mocker.Mock(spec_set=TestCase, entry_point="a.bin", required=["a.bin"]) serv_files = ["a.bin", "/another/file.bin"] server.serve_path.return_value = (Served.TIMEOUT, serv_files) target.check_result.return_value = Result.FOUND @@ -202,7 +204,7 @@ def test_runner_05(mocker, served, attempted, target_result, status): target = mocker.Mock(spec_set=Target, launch_timeout=10) target.check_result.return_value = target_result target.monitor.is_healthy.return_value = False - testcase = mocker.Mock(spec_set=TestCase, entry_point="a.bin", optional=[]) + testcase = mocker.Mock(spec_set=TestCase, entry_point="a.bin", required=["a.bin"]) runner = Runner(server, target) runner.launch("http://a/") result = runner.run([], ServerMap(), testcase) @@ -225,7 +227,7 @@ def test_runner_06(mocker): result = runner.run( [], ServerMap(), - mocker.Mock(spec_set=TestCase, entry_point=serv_files[0], optional=[]), + mocker.Mock(spec_set=TestCase, entry_point=serv_files[0], required=serv_files), ) assert result.status == Result.NONE assert result.attempted diff --git a/grizzly/common/test_storage.py b/grizzly/common/test_storage.py index 06f99463..36c00fb9 100644 --- a/grizzly/common/test_storage.py +++ b/grizzly/common/test_storage.py @@ -33,6 +33,7 @@ def test_testcase_01(tmp_path): assert not any(tcase.contents) assert tcase.pop_assets() is None assert not any(tcase.optional) + assert not any(tcase.required) tcase.dump(tmp_path) assert not any(tmp_path.iterdir()) tcase.dump(tmp_path, include_details=True) @@ -88,6 +89,7 @@ def test_testcase_03(tmp_path, file_paths): src_file.write_text("data") tcase.add_from_file(src_file, file_name=file_path, required=True) assert file_path in tcase.contents + assert file_path in tcase.required assert file_path not in tcase.optional @@ -113,25 +115,25 @@ def test_testcase_05(): tcase.add_from_bytes(b"foo", "testfile2.bin", required=False) tcase.add_from_bytes(b"foo", "testfile3.bin", required=False) tcase.add_from_bytes(b"foo", "not_served.bin", required=False) - assert len(tuple(tcase.optional)) == 3 + assert len(tcase._files.optional) == 3 # nothing to remove - with required tcase.purge_optional(chain(["testfile1.bin"], tcase.optional)) - assert len(tuple(tcase.optional)) == 3 + assert len(tcase._files.optional) == 3 # nothing to remove - use relative path (forced) tcase.purge_optional(x.file_name for x in tcase._files.optional) - assert len(tuple(tcase.optional)) == 3 + assert len(tcase._files.optional) == 3 # nothing to remove - use absolute path tcase.purge_optional(x.data_file.as_posix() for x in tcase._files.optional) - assert len(tuple(tcase.optional)) == 3 + assert len(tcase._files.optional) == 3 # remove not_served.bin tcase.purge_optional(["testfile2.bin", "testfile3.bin"]) - assert len(tuple(tcase.optional)) == 2 + assert len(tcase._files.optional) == 2 assert "testfile2.bin" in tcase.optional assert "testfile3.bin" in tcase.optional assert "not_served.bin" not in tcase.optional # remove remaining optional tcase.purge_optional(["testfile1.bin"]) - assert not any(tcase.optional) + assert not tcase._files.optional def test_testcase_06(): @@ -478,6 +480,7 @@ def test_testcase_19(): assert not dst_file.samefile(src_file) assert dst.env_vars == {"foo": "bar"} assert not set(src.optional) ^ set(dst.optional) + assert not set(src.required) ^ set(dst.required) @mark.parametrize( diff --git a/sapphire/connection_manager.py b/sapphire/connection_manager.py index 0c9e03a8..56cc9f60 100644 --- a/sapphire/connection_manager.py +++ b/sapphire/connection_manager.py @@ -14,7 +14,8 @@ class ConnectionManager: - SHUTDOWN_DELAY = 0.5 # allow extra time before closing socket if needed + # allow extra time before closing socket if needed + SHUTDOWN_DELAY = 0.5 __slots__ = ( "_deadline", @@ -117,7 +118,7 @@ def serve(self, timeout, continue_cb=None, shutdown_delay=SHUTDOWN_DELAY): Returns: bool: True unless the timeout is exceeded. """ - assert self._job.pending + assert self._job.pending or self._job.forever assert self._socket.gettimeout() is not None assert shutdown_delay >= 0 assert timeout >= 0 diff --git a/sapphire/core.py b/sapphire/core.py index c6f51af5..c0a1dc2d 100644 --- a/sapphire/core.py +++ b/sapphire/core.py @@ -186,13 +186,13 @@ def serve_path( path, continue_cb=None, forever=False, - optional_files=None, + required_files=None, server_map=None, ): - """Serve files in path. On completion a list served files and a status + """Serve files in path. On completion a of list served files and a status code will be returned. The status codes include: - - Served.ALL: All files excluding files in optional_files were served + - Served.ALL: All required files were served - Served.NONE: No files were served - Served.REQUEST: Some files were requested @@ -202,8 +202,8 @@ def serve_path( This must be a callable that returns a bool. forever (bool): Continue to handle requests even after all files have been served. This is meant to be used with continue_cb. - optional_files (list(str)): Files that do not need to be served in order - to exit the serve loop. + required_files (list(str)): Files that need to be served in order to exit + the serve loop. server_map (ServerMap): Returns: @@ -216,17 +216,13 @@ def serve_path( path, auto_close=self._auto_close, forever=forever, - optional_files=optional_files, + required_files=required_files, server_map=server_map, ) - if not job.pending: - job.finish() - LOG.debug("nothing to serve") - return (Served.NONE, tuple()) with ConnectionManager(job, self._socket, limit=self._max_workers) as mgr: - was_timeout = not mgr.serve(self.timeout, continue_cb=continue_cb) - LOG.debug("%s, timeout: %r", job.status, was_timeout) - return (Served.TIMEOUT if was_timeout else job.status, tuple(job.served)) + timed_out = not mgr.serve(self.timeout, continue_cb=continue_cb) + LOG.debug("%s, timed out: %r", job.status, timed_out) + return (Served.TIMEOUT if timed_out else job.status, tuple(job.served)) @classmethod def main(cls, args): @@ -240,10 +236,6 @@ def main(cls, args): gethostname() if args.remote else "127.0.0.1", serv.port, ) - status = serv.serve_path(args.path)[0] - if status == Served.ALL: - LOG.info("All test case content was served") - else: - LOG.warning("Failed to serve all test content") + serv.serve_path(args.path, forever=True) except KeyboardInterrupt: LOG.warning("Ctrl+C detected. Shutting down...") diff --git a/sapphire/job.py b/sapphire/job.py index 17236cc1..a2b23952 100644 --- a/sapphire/job.py +++ b/sapphire/job.py @@ -68,7 +68,7 @@ def __init__( wwwroot, auto_close=-1, forever=False, - optional_files=None, + required_files=None, server_map=None, ): self._complete = Event() @@ -82,25 +82,26 @@ def __init__( self.forever = forever self.server_map = server_map self.worker_complete = Event() - self._build_pending(optional_files) + self._build_pending(required_files) + if not self._pending.files and not self.forever: + raise RuntimeError("Empty Job") - def _build_pending(self, optional_files): + def _build_pending(self, required_files): # build file list to track files that must be served # this is intended to only be called once by __init__() - for entry in self._wwwroot.rglob("*"): - location = entry.relative_to(self._wwwroot).as_posix() - # do not add optional files to queue of required files - if optional_files and location in optional_files: - continue - if "?" in str(entry): - LOG.warning("Path cannot contain '?', skipping '%s'", entry) - continue - if entry.is_file(): - self._pending.files.add(str(entry.resolve())) - LOG.debug("required: %r", location) + assert not self._complete.is_set() + assert not self._pending.files + assert not self._served.files + if required_files: + for required in required_files: + assert "?" not in required + entry = self._wwwroot / required + if entry.is_file(): + self._pending.files.add(str(entry.resolve())) + LOG.debug("required: %r", entry) # if nothing was found check if the path exists if not self._pending.files and not self._wwwroot.is_dir(): - raise OSError(f"'{self._wwwroot}' does not exist") + raise OSError(f"wwwroot '{self._wwwroot}' does not exist") if self.server_map: for redirect, resource in self.server_map.redirect.items(): if resource.required: @@ -110,11 +111,7 @@ def _build_pending(self, optional_files): if resource.required: self._pending.files.add(dyn_resp) LOG.debug("required: %r -> %r", dyn_resp, resource.target) - LOG.debug( - "job has %d required and %d optional file(s)", - len(self._pending.files), - len(optional_files) if optional_files else 0, - ) + LOG.debug("job has %d required file(s)", len(self._pending.files)) @classmethod def lookup_mime(cls, url): @@ -144,11 +141,6 @@ def lookup_resource(self, path): # file name is too long to look up so ignore it return None raise # pragma: no cover - except ValueError: # pragma: no cover - # this is for compatibility with python versions < 3.8 - # is_file() will raise if the path contains characters unsupported - # at the OS level - pass # look for path in server map if self.server_map is not None: if path in self.server_map.redirect: @@ -160,11 +152,7 @@ def lookup_resource(self, path): LOG.debug("checking include %r", inc) file = path[len(inc) :].lstrip("/") local = Path(self.server_map.include[inc].target) / file - try: - if not local.is_file(): - continue - except ValueError: # pragma: no cover - # python versions < 3.8 compatibility + if not local.is_file(): continue # file exists, look up resource return Resource( @@ -212,7 +200,7 @@ def pending(self): return len(self._pending.files) def remove_pending(self, file_name): - # return True when all file have been removed + # return True when all files have been removed with self._pending.lock: if self._pending.files: self._pending.files.discard(file_name) diff --git a/sapphire/test_connection_manager.py b/sapphire/test_connection_manager.py index 82c4aff7..36da17ad 100644 --- a/sapphire/test_connection_manager.py +++ b/sapphire/test_connection_manager.py @@ -17,7 +17,7 @@ def test_connection_manager_01(mocker, tmp_path, timeout): """test basic ConnectionManager""" (tmp_path / "testfile").write_bytes(b"test") - job = Job(tmp_path) + job = Job(tmp_path, required_files=["testfile"]) clnt_sock = mocker.Mock(spec_set=socket) clnt_sock.recv.return_value = b"GET /testfile HTTP/1.1" serv_sock = mocker.Mock(spec_set=socket) @@ -37,7 +37,7 @@ def test_connection_manager_02(mocker, tmp_path, worker_limit): (tmp_path / "test1").touch() (tmp_path / "test2").touch() (tmp_path / "test3").touch() - job = Job(tmp_path) + job = Job(tmp_path, required_files=["test1", "test2", "test3"]) clnt_sock = mocker.Mock(spec_set=socket) clnt_sock.recv.side_effect = ( b"GET /test1 HTTP/1.1", @@ -60,8 +60,8 @@ def test_connection_manager_02(mocker, tmp_path, worker_limit): def test_connection_manager_03(mocker, tmp_path): """test ConnectionManager re-raise worker exceptions""" - (tmp_path / "test1").touch() - job = Job(tmp_path) + (tmp_path / "file").touch() + job = Job(tmp_path, required_files=["file"]) clnt_sock = mocker.Mock(spec_set=socket) clnt_sock.recv.side_effect = Exception("worker exception") serv_sock = mocker.Mock(spec_set=socket) @@ -76,8 +76,8 @@ def test_connection_manager_03(mocker, tmp_path): def test_connection_manager_04(mocker, tmp_path): """test ConnectionManager.serve() with callback""" - (tmp_path / "test1").touch() - job = Job(tmp_path) + (tmp_path / "file").touch() + job = Job(tmp_path, required_files=["file"]) with ConnectionManager(job, mocker.Mock(spec_set=socket), poll=0.01) as mgr: # invalid callback with raises(TypeError, match="continue_cb must be callable"): @@ -92,13 +92,12 @@ def test_connection_manager_04(mocker, tmp_path): def test_connection_manager_05(mocker, tmp_path): """test ConnectionManager.serve() with timeout""" mocker.patch("sapphire.connection_manager.time", autospec=True, side_effect=count()) - (tmp_path / "test1").touch() - job = Job(tmp_path) + (tmp_path / "file").touch() clnt_sock = mocker.Mock(spec_set=socket) clnt_sock.recv.return_value = b"" serv_sock = mocker.Mock(spec_set=socket) serv_sock.accept.return_value = (clnt_sock, None) - job = Job(tmp_path) + job = Job(tmp_path, required_files=["file"]) with ConnectionManager(job, serv_sock, poll=0.01) as mgr: assert not mgr.serve(10) assert job.is_complete() @@ -108,11 +107,11 @@ def test_connection_manager_06(mocker, tmp_path): """test ConnectionManager.serve() worker fails to exit""" mocker.patch("sapphire.worker.Thread", autospec=True) mocker.patch("sapphire.connection_manager.time", autospec=True, side_effect=count()) - (tmp_path / "test1").touch() + (tmp_path / "file").touch() clnt_sock = mocker.Mock(spec_set=socket) serv_sock = mocker.Mock(spec_set=socket) serv_sock.accept.return_value = (clnt_sock, None) - job = Job(tmp_path) + job = Job(tmp_path, required_files=["file"]) mocker.patch.object(job, "worker_complete") with ConnectionManager(job, serv_sock) as mgr: with raises(RuntimeError, match="Failed to close workers"): diff --git a/sapphire/test_job.py b/sapphire/test_job.py index 3b37a46d..db75af9a 100644 --- a/sapphire/test_job.py +++ b/sapphire/test_job.py @@ -12,8 +12,12 @@ def test_job_01(tmp_path): - """test creating an empty Job""" - job = Job(tmp_path) + """test creating a simple Job""" + test_file = tmp_path / "test.txt" + test_file.touch() + with raises(RuntimeError, match="Empty Job"): + Job(tmp_path) + job = Job(tmp_path, required_files=[test_file.name]) assert not job.forever assert job.status == Served.NONE assert job.lookup_resource("") is None @@ -23,26 +27,28 @@ def test_job_01(tmp_path): assert job.lookup_resource("\x00\x0B\xAD\xF0\x0D") is None assert not job.is_forbidden(tmp_path) assert not job.is_forbidden(tmp_path / "missing_file") - assert job.pending == 0 + assert job.pending == 1 assert not job.is_complete() - assert job.remove_pending("no_file.test") + assert job.remove_pending(str(test_file)) job.finish() assert not any(job.served) - assert job.is_complete() + assert job.is_complete(wait=0.01) def test_job_02(tmp_path): """test Job proper handling of required and optional files""" - opt1_path = tmp_path / "opt_file_1.txt" - opt1_path.write_bytes(b"a") - req1_path = tmp_path / "req_file_1.txt" - req1_path.write_bytes(b"b") + opt = [] + opt.append(tmp_path / "opt_file_1.txt") + opt[-1].write_bytes(b"a") + req = [] + req.append(tmp_path / "req_file_1.txt") + req[-1].write_bytes(b"b") (tmp_path / "nested").mkdir() - opt2_path = tmp_path / "nested" / "opt_file_2.txt" - opt2_path.write_bytes(b"c") - req2_path = tmp_path / "nested" / "req_file_2.txt" - req2_path.write_bytes(b"d") - job = Job(tmp_path, optional_files=[opt1_path.name, f"nested/{opt2_path.name}"]) + opt.append(tmp_path / "nested" / "opt_file_2.txt") + opt[-1].write_bytes(b"c") + req.append(tmp_path / "nested" / "req_file_2.txt") + req[-1].write_bytes(b"d") + job = Job(tmp_path, required_files=[req[0].name, f"nested/{req[1].name}"]) assert job.status == Served.NONE assert not job.is_complete() resource = job.lookup_resource("req_file_1.txt") @@ -50,27 +56,27 @@ def test_job_02(tmp_path): assert job.pending == 2 assert resource.target == tmp_path / "req_file_1.txt" assert resource.type == Resource.URL_FILE - assert not job.is_forbidden(req1_path) + assert not job.is_forbidden(req[0]) assert not job.remove_pending("no_file.test") assert job.pending == 2 - assert not job.remove_pending(str(req1_path)) - job.mark_served(req1_path) + assert not job.remove_pending(str(req[0])) + job.mark_served(req[0]) assert job.status == Served.REQUEST assert job.pending == 1 - assert job.remove_pending(str(req2_path)) - job.mark_served(req2_path) + assert job.remove_pending(str(req[1])) + job.mark_served(req[1]) assert job.status == Served.ALL assert job.pending == 0 - assert job.remove_pending(str(req1_path)) - job.mark_served(req1_path) + assert job.remove_pending(str(req[0])) + job.mark_served(req[0]) resource = job.lookup_resource("opt_file_1.txt") assert not resource.required - assert resource.target == opt1_path + assert resource.target == opt[0] assert resource.type == Resource.URL_FILE - assert job.remove_pending(str(opt1_path)) - job.mark_served(opt1_path) + assert job.remove_pending(str(opt[0])) + job.mark_served(opt[0]) resource = job.lookup_resource("nested/opt_file_2.txt") - assert resource.target == opt2_path + assert resource.target == opt[1] assert resource.type == Resource.URL_FILE assert not resource.required job.finish() @@ -122,7 +128,7 @@ def test_job_04(mocker, tmp_path): smap.include["testinc/1/2/3"] = Resource(Resource.URL_INCLUDE, srv_include) smap.include[""] = Resource(Resource.URL_INCLUDE, srv_include) smap.set_include("testinc/inc2", str(srv_include_2)) - job = Job(srv_root, server_map=smap) + job = Job(srv_root, server_map=smap, required_files=[test_1.name]) assert job.status == Served.NONE # test include path pointing to a missing file assert job.lookup_resource("testinc/missing") is None @@ -171,7 +177,7 @@ def test_job_05(tmp_path): # test url matching part of the file name smap = ServerMap() smap.include["inc"] = Resource(Resource.URL_INCLUDE, str(inc_dir)) - job = Job(srv_root, server_map=smap) + job = Job(srv_root, server_map=smap, required_files=[req.name]) resource = job.lookup_resource("inc/sub/include.js") assert resource.type == Resource.URL_INCLUDE assert resource.target == inc_file1 @@ -196,11 +202,11 @@ def test_job_05(tmp_path): def test_job_06(tmp_path): """test Job dynamic""" smap = ServerMap() - smap.set_dynamic_response("cb1", lambda _: 0, mime_type="mime_type") + smap.set_dynamic_response("cb1", lambda _: 0, mime_type="mime_type", required=True) smap.set_dynamic_response("cb2", lambda _: 1) job = Job(tmp_path, server_map=smap) assert job.status == Served.NONE - assert job.pending == 0 + assert job.pending == 1 resource = job.lookup_resource("cb1") assert resource.type == Resource.URL_DYNAMIC assert callable(resource.target) @@ -216,17 +222,17 @@ def test_job_07(tmp_path): """test accessing forbidden files""" srv_root = tmp_path / "root" srv_root.mkdir() - test_1 = srv_root / "req_file.txt" - test_1.write_bytes(b"a") + test_file = srv_root / "req_file.txt" + test_file.write_bytes(b"a") no_access = tmp_path / "no_access.txt" no_access.write_bytes(b"a") - job = Job(srv_root) + job = Job(srv_root, required_files=[test_file.name]) assert job.status == Served.NONE assert job.pending == 1 resource = job.lookup_resource("../no_access.txt") assert resource.target == no_access assert resource.type == Resource.URL_FILE - assert not job.is_forbidden(test_1) + assert not job.is_forbidden(test_file) assert job.is_forbidden((srv_root / ".." / "no_access.txt").resolve()) @@ -236,7 +242,7 @@ def test_job_08(tmp_path): test_file = tmp_path / "test.txt" test_file.write_bytes(b"a") (tmp_path / "?_2.txt").write_bytes(b"a") - job = Job(tmp_path) + job = Job(tmp_path, required_files=[test_file.name]) assert job.status == Served.NONE assert job.pending == 1 assert job.lookup_resource("test.txt").target == test_file @@ -245,7 +251,7 @@ def test_job_08(tmp_path): def test_job_09(tmp_path): """test Job.lookup_resource() with file name that is too long""" (tmp_path / "test.txt").touch() - job = Job(tmp_path) + job = Job(tmp_path, required_files=["test.txt"]) assert job.status == Served.NONE assert job.pending == 1 assert job.lookup_resource(f"/{'a' * 8192}.txt") is None @@ -259,7 +265,8 @@ def test_job_10(tmp_path): def test_job_11(tmp_path): """test Job.mark_served() and Job.served""" - job = Job(tmp_path) + (tmp_path / "test.txt").touch() + job = Job(tmp_path, required_files=["test.txt"]) assert not any(job.served) job.mark_served(tmp_path / "a.bin") assert "a.bin" in job.served diff --git a/sapphire/test_sapphire.py b/sapphire/test_sapphire.py index 2427906c..66250a14 100644 --- a/sapphire/test_sapphire.py +++ b/sapphire/test_sapphire.py @@ -3,13 +3,14 @@ """ # pylint: disable=protected-access -import hashlib -import os import socket -import threading +from hashlib import md5 from itertools import repeat +from os import urandom +from pathlib import Path from platform import system from random import choices, getrandbits +from threading import Lock from urllib.parse import quote, urlparse from pytest import mark, raises @@ -33,7 +34,7 @@ def __init__(self, url, url_prefix=None): self.file = url self.len_org = 0 # original file length self.len_srv = 0 # served file length - self.lock = threading.Lock() + self.lock = Lock() self.md5_org = None self.md5_srv = None self.requested = 0 # number of time file was requested @@ -50,134 +51,107 @@ def _create_test(fname, path, data=b"Test!", calc_hash=False, url_prefix=None): test.len_org = out_fp.tell() if calc_hash: out_fp.seek(0) - test.md5_org = hashlib.md5(out_fp.read()).hexdigest() + test.md5_org = md5(out_fp.read()).hexdigest() return test -def test_sapphire_00(client, tmp_path): - """test requesting a single file""" - with Sapphire(timeout=10) as serv: - assert serv.timeout == 10 - assert serv.scheme == "http" - test = _create_test("test_case.html", tmp_path) - client.launch("127.0.0.1", serv.port, [test]) - assert serv.serve_path(tmp_path)[0] == Served.ALL - assert client.wait(timeout=10) - assert test.code == 200 - assert test.len_srv == test.len_org - - -def test_sapphire_01(client, tmp_path): - """test requesting multiple files (test cleanup code)""" +@mark.parametrize("count", [1, 100]) +def test_sapphire_01(client, tmp_path, count): + """test serving files""" + _create_test("unrelated.bin", tmp_path) to_serve = [ - _create_test(f"test_{i}.html", tmp_path, data=os.urandom(5), calc_hash=True) - for i in range(100) + _create_test(f"test_{i:04d}.html", tmp_path, data=urandom(5), calc_hash=True) + for i in range(count) ] + # all files are required + required = [x.file for x in to_serve] with Sapphire(timeout=30) as serv: + assert serv.timeout == 30 + assert serv.scheme == "http" client.launch("127.0.0.1", serv.port, to_serve) - status, files_served = serv.serve_path(tmp_path) + status, served = serv.serve_path(tmp_path, required_files=required) assert status == Served.ALL - assert len(to_serve) == len(files_served) + assert "unrelated.bin" not in served + assert len(required) == len(served) == len(to_serve) assert client.wait(timeout=10) for t_file in to_serve: assert t_file.code == 200 assert t_file.len_srv == t_file.len_org - assert t_file.md5_srv == t_file.md5_org -def test_sapphire_02(client, tmp_path): - """test serving optional file""" - files_to_serve = [_create_test(f"test_{i}.html", tmp_path) for i in range(3)] - optional = [files_to_serve[0].file] +@mark.parametrize( + "count, req_idx", + [ + # multiple files (skip optional) + (5, 0), + # multiple files (serve optional) + (5, 4), + ], +) +def test_sapphire_02(client, tmp_path, count, req_idx): + """test serving files""" + _create_test("unrelated.bin", tmp_path) + to_serve = [ + _create_test(f"test_{i:04d}.html", tmp_path, data=urandom(5), calc_hash=True) + for i in range(count) + ] + required = to_serve[req_idx].file with Sapphire(timeout=10) as serv: - client.launch("127.0.0.1", serv.port, files_to_serve, in_order=True) - status, served_list = serv.serve_path(tmp_path, optional_files=optional) + client.launch("127.0.0.1", serv.port, to_serve, in_order=True) + status, served = serv.serve_path(tmp_path, required_files=[required]) assert status == Served.ALL - assert len(files_to_serve) == len(served_list) + assert "unrelated.bin" not in served + assert required in served + assert len(served) >= (req_idx + 1) assert client.wait(timeout=10) - for t_file in files_to_serve: - assert t_file.code == 200 - assert t_file.len_srv == t_file.len_org + for t_file in to_serve: + if t_file.file in served: + assert t_file.code == 200 + assert t_file.len_srv == t_file.len_org def test_sapphire_03(client, tmp_path): - """test skipping optional file""" - files_to_serve = [_create_test(f"test_{i}.html", tmp_path) for i in range(3)] - optional = [files_to_serve[0].file] - with Sapphire(timeout=10) as serv: - client.launch("127.0.0.1", serv.port, files_to_serve[1:]) - status, served_list = serv.serve_path(tmp_path, optional_files=optional) - assert status == Served.ALL - assert len(served_list) == len(files_to_serve) - 1 - assert client.wait(timeout=10) - assert files_to_serve[0].code is None - assert files_to_serve[0].len_srv == 0 - for t_file in files_to_serve[1:]: - assert t_file.code == 200 - assert t_file.len_srv == t_file.len_org - - -def test_sapphire_04(client, tmp_path): - """test requesting invalid file (404)""" - files_to_serve = [ - _TestFile("does_not_exist.html"), - _create_test("test_case.html", tmp_path), - ] - with Sapphire(timeout=10) as serv: - client.launch("127.0.0.1", serv.port, files_to_serve, in_order=True) - assert serv.serve_path(tmp_path)[0] == Served.ALL - assert client.wait(timeout=10) - assert "does_not_exist.html" in files_to_serve[0].file - assert files_to_serve[0].code == 404 - assert files_to_serve[1].code == 200 - - -def test_sapphire_05(client, tmp_path): - """test requesting a file outside of the server root (403)""" + """test requesting invalid files (404 and 403)""" root_dir = tmp_path / "root" root_dir.mkdir() - files_to_serve = [ + invalid = Path(__file__) + to_serve = [ + # missing file + _TestFile("does_not_exist.html"), # add invalid file - _TestFile(os.path.abspath(__file__)), + _TestFile(str(invalid.resolve())), # add file in parent of root_dir _create_test("no_access.html", tmp_path, data=b"no_access", url_prefix="../"), # add valid test _create_test("test_case.html", root_dir), ] + required = [to_serve[-1].file] assert (tmp_path / "no_access.html").is_file() with Sapphire(timeout=10) as serv: - client.launch("127.0.0.1", serv.port, files_to_serve, in_order=True) - status, files_served = serv.serve_path(root_dir) + client.launch("127.0.0.1", serv.port, to_serve, in_order=True) + status, served = serv.serve_path(root_dir, required_files=required) assert status == Served.ALL - assert len(files_served) == 1 + assert len(served) == 1 assert client.wait(timeout=10) - assert os.path.basename(__file__) in files_to_serve[0].file - assert files_to_serve[0].code == 404 - assert "no_access.html" in files_to_serve[1].file - assert files_to_serve[1].code == 403 - assert files_to_serve[2].code == 200 - - -def test_sapphire_06(client, tmp_path): - """test serving no files... this should never happen but...""" - with Sapphire(timeout=10) as serv: - client.launch("127.0.0.1", serv.port, []) - status, files_served = serv.serve_path(tmp_path) - assert status == Served.NONE - assert not files_served + assert to_serve[0].code == 404 + assert invalid.name in to_serve[1].file + assert to_serve[1].code == 404 + assert "no_access.html" in to_serve[2].file + assert to_serve[2].code == 403 + assert to_serve[3].code == 200 -def test_sapphire_07(tmp_path): +def test_sapphire_04(tmp_path): """test timeout of the server""" with Sapphire(timeout=0.01) as serv: assert serv.timeout == 0.01 - _create_test("test_case.html", tmp_path) - status, files_served = serv.serve_path(tmp_path) + to_serve = [_create_test("test_case.html", tmp_path)] + status, served = serv.serve_path(tmp_path, required_files=[to_serve[0].file]) assert status == Served.TIMEOUT - assert not files_served + assert not served -def test_sapphire_08(client, tmp_path): +def test_sapphire_05(client, tmp_path): """test only serving some files (Served.REQUEST)""" cb_status = {"count": 0} @@ -185,15 +159,17 @@ def is_running(): cb_status["count"] += 1 return cb_status["count"] < 3 # return false after 2nd call - files_to_serve = [_create_test(f"test_{i}.html", tmp_path) for i in range(3)] + to_serve = [_create_test(f"test_{i}.html", tmp_path) for i in range(3)] with Sapphire() as serv: - client.launch("127.0.0.1", serv.port, files_to_serve[1:]) - status, files_served = serv.serve_path(tmp_path, continue_cb=is_running) + client.launch("127.0.0.1", serv.port, to_serve[1:]) + status, served = serv.serve_path( + tmp_path, continue_cb=is_running, required_files=[x.file for x in to_serve] + ) assert status == Served.REQUEST - assert len(files_served) < len(files_to_serve) + assert len(served) < len(to_serve) -def test_sapphire_09(client, tmp_path): +def test_sapphire_06(client, tmp_path): """test serving interesting sized files""" tests = [ {"size": Worker.DEFAULT_TX_SIZE, "name": "even.html"}, @@ -207,10 +183,11 @@ def test_sapphire_09(client, tmp_path): test["file"] = _TestFile(test["name"]) t_data = "".join(choices("ABCD1234", k=test["size"])).encode("ascii") (tmp_path / test["file"].file).write_bytes(t_data) - test["file"].md5_org = hashlib.md5(t_data).hexdigest() + test["file"].md5_org = md5(t_data).hexdigest() + required = [test["file"].file for test in tests] with Sapphire(timeout=10) as serv: client.launch("127.0.0.1", serv.port, [test["file"] for test in tests]) - status, served_list = serv.serve_path(tmp_path) + status, served_list = serv.serve_path(tmp_path, required_files=required) assert status == Served.ALL assert len(served_list) == len(tests) assert client.wait(timeout=10) @@ -220,10 +197,10 @@ def test_sapphire_09(client, tmp_path): assert test["file"].md5_srv == test["file"].md5_org -def test_sapphire_10(client, tmp_path): +def test_sapphire_07(client, tmp_path): """test serving a large (100MB) file""" t_file = _TestFile("test_case.html") - data_hash = hashlib.md5() + data_hash = md5() with (tmp_path / t_file.file).open("wb") as test_fp: # write 100MB of 'A' data = b"A" * (100 * 1024) # 100KB of 'A' @@ -233,78 +210,65 @@ def test_sapphire_10(client, tmp_path): t_file.md5_org = data_hash.hexdigest() with Sapphire(timeout=10) as serv: client.launch("127.0.0.1", serv.port, [t_file]) - assert serv.serve_path(tmp_path)[0] == Served.ALL + assert serv.serve_path(tmp_path, required_files=[t_file.file])[0] == Served.ALL assert client.wait(timeout=10) assert t_file.code == 200 assert t_file.len_srv == (100 * 1024 * 1024) assert t_file.md5_srv == t_file.md5_org -def test_sapphire_11(client, tmp_path): +def test_sapphire_08(client, tmp_path): """test serving a binary file""" - t_file = _create_test( - "test_case.html", tmp_path, data=os.urandom(512), calc_hash=True - ) + t_file = _create_test("test_case.html", tmp_path, data=urandom(512), calc_hash=True) with Sapphire(timeout=10) as serv: client.launch("127.0.0.1", serv.port, [t_file]) - assert serv.serve_path(tmp_path)[0] == Served.ALL + assert serv.serve_path(tmp_path, required_files=[t_file.file])[0] == Served.ALL assert client.wait(timeout=10) assert t_file.code == 200 assert t_file.len_srv == t_file.len_org assert t_file.md5_srv == t_file.md5_org -def test_sapphire_12(): +def test_sapphire_09(): """test requested port is used""" test_port = 0x1337 with Sapphire(port=test_port, timeout=1) as serv: assert test_port == serv.port -def test_sapphire_13(client, tmp_path): +def test_sapphire_10(client, tmp_path): """test serving multiple content types""" - files_to_serve = [ + to_serve = [ _create_test("test_case.html", tmp_path), # create binary file without an extension - _create_test("test_case", tmp_path, data=os.urandom(5)), + _create_test("test_case", tmp_path, data=urandom(5)), ] + required = [x.file for x in to_serve] with Sapphire(timeout=10) as serv: - client.launch("127.0.0.1", serv.port, files_to_serve) - assert serv.serve_path(tmp_path)[0] == Served.ALL + client.launch("127.0.0.1", serv.port, to_serve) + assert serv.serve_path(tmp_path, required_files=required)[0] == Served.ALL assert client.wait(timeout=10) content_types = set() - for test in files_to_serve: + for test in to_serve: assert test.code == 200 assert test.len_srv == test.len_org - file_ext = os.path.splitext(test.file)[-1] - content_type = {".html": "text/html"}.get(file_ext, "application/octet-stream") + if test.file.endswith(".html"): + content_type = "text/html" + else: + content_type = "application/octet-stream" content_types.add(content_type) assert test.content_type == content_type assert len(content_types) == 2 -def test_sapphire_14(tmp_path): - """test callback""" - cb_status = {"count": 0} - - def _test_callback(): - cb_status["count"] += 1 - # return true on first call - return cb_status["count"] < 2 - - with Sapphire(timeout=10) as serv: - _create_test("test_case.html", tmp_path) - assert serv.serve_path(tmp_path, continue_cb=_test_callback)[0] == Served.NONE - assert cb_status["count"] == 2 - - -def test_sapphire_15(client, tmp_path): +def test_sapphire_11(client, tmp_path): """test calling serve_path multiple times""" with Sapphire(timeout=10) as serv: for i in range(3): - test = _create_test(f"test_case_{i}.html", tmp_path) + name = f"test_{i}.html" + test = _create_test(name, tmp_path) client.launch("127.0.0.1", serv.port, [test]) - assert serv.serve_path(tmp_path)[0] == Served.ALL + assert serv.serve_path(tmp_path, required_files=[name])[0] == Served.ALL assert client.wait(timeout=10) client.close() assert test.code == 200 @@ -312,14 +276,17 @@ def test_sapphire_15(client, tmp_path): (tmp_path / test.file).unlink() -def test_sapphire_16(client, tmp_path): +def test_sapphire_12(client, tmp_path): """test non required mapped redirects""" smap = ServerMap() smap.set_redirect("test_url", "blah", required=False) test = _create_test("test_case.html", tmp_path) with Sapphire(timeout=10) as serv: client.launch("127.0.0.1", serv.port, [test]) - assert serv.serve_path(tmp_path, server_map=smap)[0] == Served.ALL + assert ( + serv.serve_path(tmp_path, server_map=smap, required_files=[test.file])[0] + == Served.ALL + ) assert client.wait(timeout=10) assert test.code == 200 assert test.len_srv == test.len_org @@ -338,7 +305,7 @@ def test_sapphire_16(client, tmp_path): ("€d’é-ñÿ", None), ], ) -def test_sapphire_17(client, tmp_path, path, query): +def test_sapphire_13(client, tmp_path, path, query): """test required mapped redirects""" smap = ServerMap() with Sapphire(timeout=10) as serv: @@ -349,7 +316,9 @@ def test_sapphire_17(client, tmp_path, path, query): # point "redirect" at target smap.set_redirect("redirect", target.file, required=True) client.launch("127.0.0.1", serv.port, [redirect]) - status, served = serv.serve_path(tmp_path, server_map=smap) + status, served = serv.serve_path( + tmp_path, server_map=smap, required_files=[target.file] + ) assert status == Served.ALL assert len(served) == 1 assert client.wait(timeout=10) @@ -357,7 +326,7 @@ def test_sapphire_17(client, tmp_path, path, query): assert redirect.len_srv == target.len_org -def test_sapphire_18(client, tmp_path): +def test_sapphire_14(client, tmp_path): """test include directories and permissions""" inc1_path = tmp_path / "inc1" inc2_path = tmp_path / "inc2" @@ -365,12 +334,12 @@ def test_sapphire_18(client, tmp_path): inc1_path.mkdir() inc2_path.mkdir() root_path.mkdir() - files_to_serve = [] + to_serve = [] smap = ServerMap() with Sapphire(timeout=10) as serv: # add files to inc dirs inc1 = _create_test("included_file1.html", inc1_path, data=b"blah....1") - files_to_serve.append(inc1) + to_serve.append(inc1) # add a nested dir nest_path = inc1_path / "nested" nest_path.mkdir() @@ -379,38 +348,40 @@ def test_sapphire_18(client, tmp_path): "nested_file.html", nest_path, data=b"blah... .nested", url_prefix="nested/" ) assert nest_path / "nested_file.html" - files_to_serve.append(nest) + to_serve.append(nest) # test 404 in nested dir in inc1 nest_404 = _TestFile("nested/nested_file_404.html") - files_to_serve.append(nest_404) + to_serve.append(nest_404) # test path mounted somewhere other than / inc2 = _create_test( "included_file2.html", inc2_path, data=b"blah....2", url_prefix="inc_test/" ) - files_to_serve.append(inc2) + to_serve.append(inc2) # test 404 in include dir inc404 = _TestFile("inc_test/included_file_404.html") assert not (nest_path / "included_file_404.html").is_file() - files_to_serve.append(inc404) + to_serve.append(inc404) # test 403 inc403 = _create_test( "no_access.html", tmp_path, data=b"no_access", url_prefix="inc_test/../" ) assert (tmp_path / "no_access.html").is_file() - files_to_serve.append(inc403) + to_serve.append(inc403) # test file (used to keep sever job alive) test = _create_test("test_case.html", root_path) - files_to_serve.append(test) + to_serve.append(test) # add include paths smap.set_include("/", str(inc1_path)) # mount at '/' smap.set_include("inc_test", str(inc2_path)) # mount at '/inc_test' - client.launch("127.0.0.1", serv.port, files_to_serve, in_order=True) - status, files_served = serv.serve_path(root_path, server_map=smap) + client.launch("127.0.0.1", serv.port, to_serve, in_order=True) + status, served = serv.serve_path( + root_path, server_map=smap, required_files=[x.file for x in to_serve] + ) assert status == Served.ALL - assert "test_case.html" in files_served - assert (inc1_path / "included_file1.html").as_posix() in files_served - assert (inc2_path / "included_file2.html").as_posix() in files_served - assert (nest_path / "nested_file.html").as_posix() in files_served + assert "test_case.html" in served + assert (inc1_path / "included_file1.html").as_posix() in served + assert (inc2_path / "included_file2.html").as_posix() in served + assert (nest_path / "nested_file.html").as_posix() in served assert client.wait(timeout=10) assert inc1.code == 200 assert inc2.code == 200 @@ -434,15 +405,12 @@ def test_sapphire_18(client, tmp_path): ("", True), ], ) -def test_sapphire_19(client, tmp_path, query, required): +def test_sapphire_15(client, tmp_path, query, required): """test dynamic response""" _data = b"dynamic response -- TEST DATA!" # build request path = "dyn_test" - if query is not None: - request = "?".join([path, query]) - else: - request = path + request = path if query is None else "?".join([path, query]) # setup custom callback def dr_callback(data): @@ -459,19 +427,19 @@ def dr_callback(data): # create files test_dr = _TestFile(request) test_dr.len_org = len(_data) - test_dr.md5_org = hashlib.md5(_data).hexdigest() + test_dr.md5_org = md5(_data).hexdigest() test = _create_test("test_case.html", tmp_path) if required: - optional = [test.file] + req_files = [] files = [test, test_dr] else: - optional = [test_dr.file] + req_files = [test.file] files = [test_dr, test] # test request with Sapphire(timeout=10) as serv: client.launch("127.0.0.1", serv.port, files, in_order=True) assert ( - serv.serve_path(tmp_path, optional_files=optional, server_map=smap)[0] + serv.serve_path(tmp_path, required_files=req_files, server_map=smap)[0] == Served.ALL ) assert client.wait(timeout=10) @@ -483,12 +451,11 @@ def dr_callback(data): assert test_dr.md5_srv == test_dr.md5_org -def test_sapphire_20(client_factory, tmp_path): +def test_sapphire_16(client_factory, tmp_path): """test pending_files == 0 in worker thread""" client_defer = client_factory(rx_size=2) # server should shutdown while this file is being served test_defer = _create_test("defer_test.html", tmp_path) - optional = [test_defer.file] test = _create_test("test_case.html", tmp_path, data=b"112233") with Sapphire(timeout=10) as serv: # this test needs to wait just long enough to have the required file served @@ -498,32 +465,30 @@ def test_sapphire_20(client_factory, tmp_path): ) client = client_factory(rx_size=2) client.launch("127.0.0.1", serv.port, [test], throttle=0.1) - assert serv.serve_path(tmp_path, optional_files=optional)[0] == Served.ALL + assert serv.serve_path(tmp_path, required_files=[test.file])[0] == Served.ALL assert client_defer.wait(timeout=10) assert client.wait(timeout=10) assert test.code == 200 assert test_defer.code == 0 -def test_sapphire_21(client, tmp_path): +def test_sapphire_17(client, tmp_path): """test handling an invalid request""" bad_test = _TestFile("bad.html") bad_test.custom_request = b"a bad request...0+%\xef\xb7\xba\r\n" - optional = [bad_test.file] test = _create_test("test_case.html", tmp_path) with Sapphire(timeout=10) as serv: client.launch("127.0.0.1", serv.port, [bad_test, test], in_order=True) - assert serv.serve_path(tmp_path, optional_files=optional)[0] == Served.ALL + assert serv.serve_path(tmp_path, required_files=[test.file])[0] == Served.ALL assert client.wait(timeout=10) assert test.code == 200 assert bad_test.code == 400 -def test_sapphire_22(client, tmp_path): +def test_sapphire_18(client, tmp_path): """test handling an empty request""" bad_test = _TestFile("bad.html") bad_test.custom_request = b"" - optional = [bad_test.file] test = _create_test("test_case.html", tmp_path) with Sapphire(timeout=10) as serv: client.launch( @@ -533,16 +498,16 @@ def test_sapphire_22(client, tmp_path): indicate_failure=True, in_order=True, ) - assert serv.serve_path(tmp_path, optional_files=optional)[0] == Served.ALL + assert serv.serve_path(tmp_path, required_files=[test.file])[0] == Served.ALL assert client.wait(timeout=10) assert test.code == 200 assert bad_test.code == 0 -def test_sapphire_23(client_factory, tmp_path): +def test_sapphire_19(client_factory, tmp_path): """test requesting multiple files via multiple connections""" to_serve = [ - _create_test(f"test_{i:0>3d}.html", tmp_path, data=b"AAAA") for i in range(2) + _create_test(f"test_{i:03d}.html", tmp_path, data=b"AAAA") for i in range(2) ] max_workers = 20 with Sapphire(max_workers=max_workers, timeout=60) as serv: @@ -554,7 +519,8 @@ def test_sapphire_23(client_factory, tmp_path): client.launch( "127.0.0.1", serv.port, to_serve, in_order=True, throttle=0.05 ) - status, files_served = serv.serve_path(tmp_path) + required = [x.file for x in to_serve] + status, served = serv.serve_path(tmp_path, required_files=required) # call serv.close() instead of waiting for the clients to timeout serv.close() finally: @@ -562,13 +528,13 @@ def test_sapphire_23(client_factory, tmp_path): assert client.wait(timeout=10) client.close() assert status == Served.ALL - assert len(to_serve) == len(files_served) + assert len(to_serve) == len(served) for t_file in to_serve: assert t_file.code == 200 assert t_file.len_srv == t_file.len_org -def test_sapphire_24(client_factory, tmp_path): +def test_sapphire_20(client_factory, tmp_path): """test all request types via multiple connections""" def _dyn_test_cb(_): @@ -577,25 +543,27 @@ def _dyn_test_cb(_): smap = ServerMap() with Sapphire(max_workers=10, timeout=60) as serv: to_serve = [] + required = [] for i in range(50): # add required files to_serve.append( - _create_test(f"test_{i:0>3d}.html", tmp_path, data=b"A" * ((i % 2) + 1)) + _create_test(f"test_{i:03d}.html", tmp_path, data=b"A" * ((i % 2) + 1)) ) + required.append(to_serve[-1].file) # add a missing files - to_serve.append(_TestFile(f"missing_{i:0>3d}.html")) + to_serve.append(_TestFile(f"missing_{i:03d}.html")) # add optional files - opt_path = tmp_path / f"opt_{i:0>3d}.html" + opt_path = tmp_path / f"opt_{i:03d}.html" opt_path.write_bytes(b"A" * ((i % 2) + 1)) to_serve.append(_TestFile(opt_path.name)) # add redirects - redir_target = _create_test(f"redir_{i:0>3d}.html", tmp_path, data=b"AA") - to_serve.append(_TestFile(f"redir_{i:0>3d}")) + redir_target = _create_test(f"redir_{i:03d}.html", tmp_path, data=b"AA") + to_serve.append(_TestFile(f"redir_{i:03d}")) smap.set_redirect( to_serve[-1].file, redir_target.file, required=getrandbits(1) > 0 ) # add dynamic responses - to_serve.append(_TestFile(f"dynm_{i:0>3d}")) + to_serve.append(_TestFile(f"dynm_{i:03d}")) smap.set_dynamic_response( to_serve[-1].file, _dyn_test_cb, mime_type="text/plain" ) @@ -604,10 +572,13 @@ def _dyn_test_cb(_): clients.append(client_factory(rx_size=1)) throttle = 0.05 if getrandbits(1) else 0 clients[-1].launch("127.0.0.1", serv.port, to_serve, throttle=throttle) - assert serv.serve_path(tmp_path, server_map=smap)[0] == Served.ALL + assert ( + serv.serve_path(tmp_path, server_map=smap, required_files=required)[0] + == Served.ALL + ) -def test_sapphire_25(client, tmp_path): +def test_sapphire_21(client, tmp_path): """test dynamic response with bad callbacks""" test_dr = _TestFile("dynm_test") smap = ServerMap() @@ -616,10 +587,10 @@ def test_sapphire_25(client, tmp_path): with Sapphire(timeout=10) as serv: client.launch("127.0.0.1", serv.port, [test_dr, test], in_order=True) with raises(TypeError): - serv.serve_path(tmp_path, server_map=smap) + serv.serve_path(tmp_path, server_map=smap, required_files=[test.file]) -def test_sapphire_26(client, tmp_path): +def test_sapphire_22(client, tmp_path): """test serving to a slow client""" t_data = "".join(choices("ABCD1234", k=0x19000)).encode("ascii") # 100KB t_file = _create_test("test_case.html", tmp_path, data=t_data, calc_hash=True) @@ -629,30 +600,31 @@ def test_sapphire_26(client, tmp_path): client.rx_size = 0x2800 with Sapphire(timeout=60) as serv: client.launch("127.0.0.1", serv.port, [t_file], throttle=0.25) - assert serv.serve_path(tmp_path)[0] == Served.ALL + assert serv.serve_path(tmp_path, required_files=[t_file.file])[0] == Served.ALL assert client.wait(timeout=10) assert t_file.code == 200 assert t_file.len_srv == t_file.len_org assert t_file.md5_srv == t_file.md5_org -def test_sapphire_27(client, tmp_path): +def test_sapphire_23(client, tmp_path): """test timeout while requesting multiple files""" t_data = "".join(choices("ABCD1234", k=1024)).encode("ascii") - files_to_serve = [ - _create_test(f"test_{i:0>3d}.html", tmp_path, data=t_data) for i in range(50) + to_serve = [ + _create_test(f"test_{i:03d}.html", tmp_path, data=t_data) for i in range(50) ] + required = [x.file for x in to_serve] client.rx_size = 512 with Sapphire(timeout=1) as serv: # minimum timeout is 1 second client.launch( - "127.0.0.1", serv.port, files_to_serve, indicate_failure=True, throttle=0.1 + "127.0.0.1", serv.port, to_serve, indicate_failure=True, throttle=0.1 ) - status, files_served = serv.serve_path(tmp_path) + status, served = serv.serve_path(tmp_path, required_files=required) assert status == Served.TIMEOUT - assert len(files_served) < len(files_to_serve) + assert len(served) < len(to_serve) -def test_sapphire_28(client_factory, tmp_path): +def test_sapphire_24(client_factory, tmp_path): """test Sapphire.serve_path() with forever=True""" with Sapphire(timeout=10) as serv: test = _create_test("test_case.html", tmp_path) @@ -687,17 +659,18 @@ def _test_callback(): "€d’é-ñÿ", ], ) -def test_sapphire_29(client, tmp_path, file_name): +def test_sapphire_25(client, tmp_path, file_name): """test interesting file names""" to_serve = [_create_test(file_name, tmp_path)] + required = [x.file for x in to_serve] with Sapphire(timeout=10) as serv: client.launch("127.0.0.1", serv.port, to_serve) - assert serv.serve_path(tmp_path)[0] == Served.ALL + assert serv.serve_path(tmp_path, required_files=required)[0] == Served.ALL assert client.wait(timeout=10) assert all(t_file.code == 200 for t_file in to_serve) -def test_sapphire_30(client, tmp_path): +def test_sapphire_26(client, tmp_path): """test interesting path string""" path = "".join(chr(i) for i in range(256)) to_serve = [ @@ -706,13 +679,51 @@ def test_sapphire_30(client, tmp_path): # used to keep server running _create_test("a.html", tmp_path), ] + required = [to_serve[-1].file] with Sapphire(timeout=10) as serv: client.launch("127.0.0.1", serv.port, to_serve, in_order=True) - assert serv.serve_path(tmp_path, optional_files=[path])[0] == Served.ALL + assert serv.serve_path(tmp_path, required_files=required)[0] == Served.ALL assert client.wait(timeout=10) assert all(t_file.code is not None for t_file in to_serve) +def test_sapphire_27(mocker): + """test Sapphire.clear_backlog()""" + mocker.patch("sapphire.core.socket", autospec=True) + mocker.patch("sapphire.core.time", autospec=True, return_value=1) + pending = mocker.Mock(spec_set=socket.socket) + with Sapphire(timeout=10) as serv: + serv._socket = mocker.Mock(spec_set=socket.socket) + serv._socket.accept.side_effect = ((pending, None), OSError, BlockingIOError) + serv.clear_backlog() + assert serv._socket.accept.call_count == 3 + assert serv._socket.settimeout.call_count == 2 + assert pending.close.call_count == 1 + + +@mark.skipif(system() != "Windows", reason="Only supported on Windows") +def test_sapphire_28(client, tmp_path): + """test serving from path using Windows short file name""" + wwwroot = tmp_path / "long_path_name_that_can_be_truncated_on_windows" + wwwroot.mkdir() + with Sapphire(timeout=10) as serv: + assert serv.timeout == 10 + test = _create_test("test_case.html", wwwroot) + client.launch("127.0.0.1", serv.port, [test]) + assert serv.serve_path(tmp_path / "LONG_P~1")[0] == Served.ALL + assert client.wait(timeout=10) + assert test.code == 200 + assert test.len_srv == test.len_org + + +def test_sapphire_29(tmp_path): + """test Sapphire with certificates""" + certs = CertificateBundle.create(path=tmp_path) + with Sapphire(timeout=10, certs=certs) as serv: + assert serv.scheme == "https" + certs.cleanup() + + @mark.parametrize( "bind,", [ @@ -722,7 +733,7 @@ def test_sapphire_30(client, tmp_path): (PermissionError("foo", 10013), None), ], ) -def test_sapphire_31(mocker, bind): +def test_create_listening_socket_01(mocker, bind): """test create_listening_socket()""" fake_sleep = mocker.patch("sapphire.core.sleep", autospec=True) fake_sock = mocker.patch("sapphire.core.socket", autospec=True) @@ -746,7 +757,7 @@ def test_sapphire_31(mocker, bind): (repeat(PermissionError("foo", 10013), 2), 2, PermissionError), ], ) -def test_sapphire_32(mocker, bind, attempts, raised): +def test_create_listening_socket_02(mocker, bind, attempts, raised): """test create_listening_socket() - bind/listen failure""" mocker.patch("sapphire.core.sleep", autospec=True) fake_sock = mocker.patch("sapphire.core.socket", autospec=True) @@ -756,7 +767,7 @@ def test_sapphire_32(mocker, bind, attempts, raised): assert fake_sock.return_value.close.call_count == attempts -def test_sapphire_33(mocker): +def test_create_listening_socket_03(mocker): """test create_listening_socket() - fail to find port""" fake_sock = mocker.patch("sapphire.core.socket", autospec=True) # always choose a blocked port @@ -767,43 +778,6 @@ def test_sapphire_33(mocker): assert fake_sock.return_value.close.call_count == 1 -def test_sapphire_34(mocker): - """test Sapphire.clear_backlog()""" - mocker.patch("sapphire.core.socket", autospec=True) - mocker.patch("sapphire.core.time", autospec=True, return_value=1) - pending = mocker.Mock(spec_set=socket.socket) - with Sapphire(timeout=10) as serv: - serv._socket = mocker.Mock(spec_set=socket.socket) - serv._socket.accept.side_effect = ((pending, None), OSError, BlockingIOError) - serv.clear_backlog() - assert serv._socket.accept.call_count == 3 - assert serv._socket.settimeout.call_count == 2 - assert pending.close.call_count == 1 - - -@mark.skipif(system() != "Windows", reason="Only supported on Windows") -def test_sapphire_35(client, tmp_path): - """test serving from path using Windows short file name""" - wwwroot = tmp_path / "long_path_name_that_can_be_truncated_on_windows" - wwwroot.mkdir() - with Sapphire(timeout=10) as serv: - assert serv.timeout == 10 - test = _create_test("test_case.html", wwwroot) - client.launch("127.0.0.1", serv.port, [test]) - assert serv.serve_path(tmp_path / "LONG_P~1")[0] == Served.ALL - assert client.wait(timeout=10) - assert test.code == 200 - assert test.len_srv == test.len_org - - -def test_sapphire_36(tmp_path): - """test Sapphire with certificates""" - certs = CertificateBundle.create(path=tmp_path) - with Sapphire(timeout=10, certs=certs) as serv: - assert serv.scheme == "https" - certs.cleanup() - - def test_main_01(mocker, tmp_path): """test Sapphire.main()""" args = mocker.Mock(path=tmp_path, port=4536, remote=False, timeout=0) diff --git a/sapphire/test_worker.py b/sapphire/test_worker.py index 7d389dcd..4d30ccd5 100644 --- a/sapphire/test_worker.py +++ b/sapphire/test_worker.py @@ -83,7 +83,7 @@ def test_worker_03(mocker): def test_worker_04(mocker, tmp_path, url): """test Worker.launch()""" (tmp_path / "testfile").touch() - job = Job(tmp_path) + job = Job(tmp_path, required_files=["testfile"]) clnt_sock = mocker.Mock(spec_set=socket.socket) clnt_sock.recv.return_value = f"GET {url} HTTP/1.1".encode() serv_sock = mocker.Mock(spec_set=socket.socket) @@ -112,7 +112,7 @@ def test_worker_04(mocker, tmp_path, url): def test_worker_05(mocker, tmp_path, req, response): """test Worker.launch() with invalid/unsupported requests""" (tmp_path / "testfile").touch() - job = Job(tmp_path) + job = Job(tmp_path, required_files=["testfile"]) clnt_sock = mocker.Mock(spec_set=socket.socket) clnt_sock.recv.return_value = req serv_sock = mocker.Mock(spec_set=socket.socket)