Du kan inte välja fler än 25 ämnen Ämnen måste starta med en bokstav eller siffra, kan innehålla bindestreck ('-') och vara max 35 tecken långa.

156 lines
4.7KB

  1. from pathlib import Path
  2. import socket
  3. from contextlib import contextmanager, ExitStack, closing
  4. import json
  5. from http.server import SimpleHTTPRequestHandler, HTTPServer
  6. from typing import NamedTuple, Any, Iterator, Callable
  7. from concurrent.futures import ThreadPoolExecutor
  8. from functools import partial
  9. import tempfile
  10. import sys
  11. import subprocess
  12. import pytest
  13. from _pytest.fixtures import FixtureRequest
  14. from _pytest.tmpdir import TempPathFactory
  15. from dds_ci.dds import DDSWrapper
  16. def _unused_tcp_port() -> int:
  17. """Find an unused localhost TCP port from 1024-65535 and return it."""
  18. with closing(socket.socket()) as sock:
  19. sock.bind(('127.0.0.1', 0))
  20. return sock.getsockname()[1]
  21. class DirectoryServingHTTPRequestHandler(SimpleHTTPRequestHandler):
  22. """
  23. A simple HTTP request handler that simply serves files from a directory given to the constructor.
  24. """
  25. def __init__(self, *args: Any, **kwargs: Any) -> None:
  26. self.dir = kwargs.pop('dir')
  27. super().__init__(*args, **kwargs)
  28. def translate_path(self, path: str) -> str:
  29. # Convert the given URL path to a path relative to the directory we are serving
  30. abspath = Path(super().translate_path(path)) # type: ignore
  31. relpath = abspath.relative_to(Path.cwd())
  32. return str(self.dir / relpath)
  33. class ServerInfo(NamedTuple):
  34. """
  35. Information about an HTTP server fixture
  36. """
  37. base_url: str
  38. root: Path
  39. @contextmanager
  40. def run_http_server(dirpath: Path, port: int) -> Iterator[ServerInfo]:
  41. """
  42. Context manager that spawns an HTTP server that serves thegiven directory on
  43. the given TCP port.
  44. """
  45. handler = partial(DirectoryServingHTTPRequestHandler, dir=dirpath)
  46. addr = ('127.0.0.1', port)
  47. pool = ThreadPoolExecutor()
  48. with HTTPServer(addr, handler) as httpd:
  49. pool.submit(lambda: httpd.serve_forever(poll_interval=0.1))
  50. try:
  51. print('Serving at', addr)
  52. yield ServerInfo(f'http://127.0.0.1:{port}', dirpath)
  53. finally:
  54. httpd.shutdown()
  55. HTTPServerFactory = Callable[[Path], ServerInfo]
  56. @pytest.fixture(scope='session')
  57. def http_server_factory(request: FixtureRequest) -> HTTPServerFactory:
  58. """
  59. Spawn an HTTP server that serves the content of a directory.
  60. """
  61. def _make(p: Path) -> ServerInfo:
  62. st = ExitStack()
  63. server = st.enter_context(run_http_server(p, _unused_tcp_port()))
  64. request.addfinalizer(st.pop_all)
  65. return server
  66. return _make
  67. class RepoServer:
  68. """
  69. A fixture handle to a dds HTTP repository, including a path and URL.
  70. """
  71. def __init__(self, dds_exe: Path, info: ServerInfo, repo_name: str) -> None:
  72. self.repo_name = repo_name
  73. self.server = info
  74. self.url = info.base_url
  75. self.dds_exe = dds_exe
  76. def import_json_data(self, data: Any) -> None:
  77. """
  78. Import some packages into the repo for the given JSON data. Uses
  79. mkrepo.py
  80. """
  81. with tempfile.NamedTemporaryFile(delete=False) as f:
  82. f.write(json.dumps(data).encode())
  83. f.close()
  84. self.import_json_file(Path(f.name))
  85. Path(f.name).unlink()
  86. def import_json_file(self, fpath: Path) -> None:
  87. """
  88. Import some package into the repo for the given JSON file. Uses mkrepo.py
  89. """
  90. subprocess.check_call([
  91. sys.executable,
  92. str(Path.cwd() / 'tools/mkrepo.py'),
  93. f'--dds-exe={self.dds_exe}',
  94. f'--dir={self.server.root}',
  95. f'--spec={fpath}',
  96. ])
  97. RepoFactory = Callable[[str], Path]
  98. @pytest.fixture(scope='session')
  99. def repo_factory(tmp_path_factory: TempPathFactory, dds: DDSWrapper) -> RepoFactory:
  100. def _make(name: str) -> Path:
  101. tmpdir = tmp_path_factory.mktemp('test-repo-')
  102. dds.run(['repoman', 'init', tmpdir, f'--name={name}'])
  103. return tmpdir
  104. return _make
  105. HTTPRepoServerFactory = Callable[[str], RepoServer]
  106. @pytest.fixture(scope='session')
  107. def http_repo_factory(dds_exe: Path, repo_factory: RepoFactory,
  108. http_server_factory: HTTPServerFactory) -> HTTPRepoServerFactory:
  109. """
  110. Fixture factory that creates new repositories with an HTTP server for them.
  111. """
  112. def _make(name: str) -> RepoServer:
  113. repo_dir = repo_factory(name)
  114. server = http_server_factory(repo_dir)
  115. return RepoServer(dds_exe, server, name)
  116. return _make
  117. @pytest.fixture()
  118. def http_repo(http_repo_factory: HTTPRepoServerFactory, request: FixtureRequest) -> RepoServer:
  119. """
  120. Fixture that creates a new empty dds repository and an HTTP server to serve
  121. it.
  122. """
  123. return http_repo_factory(f'test-repo-{request.function.__name__}')