Skip to content

Commit 44ab1a1

Browse files
committed
testserver: First iteration of the mock Greenlight server
This uses the `gl-testing` library, and builds a standalone server to test against. We currently expose four interfaces: - The scheduler interface as the main entrypoint to the service - The GRPC-Web proxy to develop browser apps and extensions against Greenlight. - The `bitcoind` interface, so you can generate blocks and confirm transactions without lengthy wait times - The node's grpc interface directly to work against a single user's node All of these will listen to random ports initially. We write a small file `metadata.json` which contains the URIs and ports for the first three, while the node's URI can be retrieved from the scheduler, since these are spawned on demand as users register.
1 parent 1c1c748 commit 44ab1a1

File tree

6 files changed

+265
-1
lines changed

6 files changed

+265
-1
lines changed

libs/gl-testserver/README.md

Whitespace-only changes.

libs/gl-testserver/gltestserver/__init__.py

Whitespace-only changes.
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
import json
2+
from dataclasses import dataclass
3+
4+
import time
5+
6+
from rich.console import Console
7+
from rich.pretty import pprint
8+
from rich import inspect
9+
from pathlib import Path
10+
from gltesting import fixtures
11+
import gltesting
12+
from inspect import isgeneratorfunction
13+
import click
14+
import logging
15+
from rich.logging import RichHandler
16+
from pyln.testing.utils import BitcoinD
17+
from typing import Any, List
18+
19+
20+
console = Console()
21+
logging.basicConfig(
22+
level="DEBUG",
23+
format="%(message)s",
24+
datefmt="[%X]",
25+
handlers=[
26+
RichHandler(rich_tracebacks=True, tracebacks_suppress=[click], console=console)
27+
],
28+
)
29+
logger = logging.getLogger("gltestserver")
30+
31+
32+
@dataclass
33+
class TestServer:
34+
directory: Path
35+
bitcoind: BitcoinD
36+
scheduler: gltesting.scheduler.Scheduler
37+
finalizers: List[Any]
38+
clients: gltesting.clients.Clients
39+
grpc_web_proxy: gltesting.grpcweb.GrpcWebProxy
40+
41+
def stop(self):
42+
for f in self.finalizers[::-1]:
43+
try:
44+
f()
45+
except StopIteration:
46+
continue
47+
except Exception as e:
48+
logger.warn(f"Unexpected exception tearing down server: {e}")
49+
50+
def metadata(self):
51+
"""Construct a dict of config values for this TestServer."""
52+
return {
53+
"scheduler_grpc_uri": self.scheduler.grpc_addr,
54+
"grpc_web_proxy_uri": f"http://localhost:{self.grpc_web_proxy.web_port}",
55+
"bitcoind_rpc_uri": f"http://rpcuser:rpcpass@localhost:{self.bitcoind.rpcport}",
56+
}
57+
58+
59+
def build():
60+
# List of teardown functions to call in reverse order.
61+
finalizers = []
62+
63+
def callfixture(f, *args, **kwargs):
64+
"""Small shim to bypass the pytest decorator."""
65+
F = f.__pytest_wrapped__.obj
66+
67+
if isgeneratorfunction(F):
68+
it = F(*args, **kwargs)
69+
v = it.__next__()
70+
finalizers.append(it.__next__)
71+
return v
72+
else:
73+
return F(*args, **kwargs)
74+
75+
directory = Path("/tmp/gl-testserver")
76+
77+
cert_directory = callfixture(fixtures.cert_directory, directory)
78+
root_id = callfixture(fixtures.root_id, cert_directory)
79+
users_id = callfixture(fixtures.users_id)
80+
nobody_id = callfixture(fixtures.nobody_id, cert_directory)
81+
scheduler_id = callfixture(fixtures.scheduler_id, cert_directory)
82+
paths = callfixture(fixtures.paths)
83+
bitcoind = callfixture(
84+
fixtures.bitcoind,
85+
directory=directory,
86+
teardown_checks=None,
87+
)
88+
scheduler = callfixture(
89+
fixtures.scheduler, scheduler_id=scheduler_id, bitcoind=bitcoind
90+
)
91+
92+
clients = callfixture(
93+
fixtures.clients, directory=directory, scheduler=scheduler, nobody_id=nobody_id
94+
)
95+
96+
node_grpc_web_server = callfixture(
97+
fixtures.node_grpc_web_proxy, scheduler=scheduler
98+
)
99+
100+
return TestServer(
101+
directory=directory,
102+
bitcoind=bitcoind,
103+
finalizers=finalizers,
104+
scheduler=scheduler,
105+
clients=clients,
106+
grpc_web_proxy=node_grpc_web_server,
107+
)
108+
109+
110+
@click.group()
111+
def cli():
112+
pass
113+
114+
115+
@cli.command()
116+
def run():
117+
gl = build()
118+
try:
119+
meta = gl.metadata()
120+
metafile = gl.directory / "metadata.json"
121+
logger.debug(f"Writing testserver metadata to {metafile}")
122+
with metafile.open(mode="w") as f:
123+
json.dump(meta, f)
124+
125+
pprint(meta)
126+
logger.info(
127+
f"Server is up and running with the above config values. To stop press Ctrl-C."
128+
)
129+
time.sleep(1800)
130+
except Exception as e:
131+
logger.warning(f"Caught exception running testserver: {e}")
132+
pass
133+
finally:
134+
logger.info("Stopping gl-testserver")
135+
# Now tear things down again.
136+
gl.stop()
137+
138+
139+
if __name__ == "__main__":
140+
cli()

libs/gl-testserver/pyproject.toml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
[project]
2+
name = "gltestserver"
3+
version = "0.1.0"
4+
description = "A standalone test server implementing the public Greenlight interfaces"
5+
readme = "README.md"
6+
requires-python = ">=3.8"
7+
dependencies = [
8+
"click>=8.1.7",
9+
"gltesting",
10+
"rich>=13.9.3",
11+
]
12+
13+
[project.scripts]
14+
gltestserver = 'gltestserver.__main__:cli'
15+
16+
[tool.uv.sources]
17+
gltesting = { workspace = true }
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
# We do not import `gl-testing` or `pyln-testing` since the
2+
# `gl-testserver` is intended to run tests externally from a python
3+
# environment. We will use `gl-client-py` to interact with it though.
4+
# Ok, one exception, `TailableProc` is used to run and tail the
5+
# `gl-testserver`.
6+
7+
import shutil
8+
import tempfile
9+
import os
10+
import pytest
11+
from pyln.testing.utils import TailableProc
12+
import json
13+
import signal
14+
from pathlib import Path
15+
16+
17+
@pytest.fixture
18+
def test_name(request):
19+
yield request.function.__name__
20+
21+
22+
@pytest.fixture(scope="session")
23+
def test_base_dir():
24+
d = os.getenv("TEST_DIR", "/tmp")
25+
directory = tempfile.mkdtemp(prefix="ltests-", dir=d)
26+
print("Running tests in {}".format(directory))
27+
28+
yield directory
29+
30+
31+
@pytest.fixture
32+
def directory(request, test_base_dir, test_name):
33+
"""Return a per-test specific directory.
34+
35+
This makes a unique test-directory even if a test is rerun multiple times.
36+
37+
"""
38+
directory = os.path.join(test_base_dir, test_name)
39+
request.node.has_errors = False
40+
41+
if not os.path.exists(directory):
42+
os.makedirs(directory)
43+
44+
yield directory
45+
46+
# This uses the status set in conftest.pytest_runtest_makereport to
47+
# determine whether we succeeded or failed. Outcome can be None if the
48+
# failure occurs during the setup phase, hence the use to getattr instead
49+
# of accessing it directly.
50+
rep_call = getattr(request.node, "rep_call", None)
51+
outcome = "passed" if rep_call is None else rep_call.outcome
52+
failed = not outcome or request.node.has_errors or outcome != "passed"
53+
54+
if not failed:
55+
try:
56+
shutil.rmtree(directory)
57+
except OSError:
58+
# Usually, this means that e.g. valgrind is still running. Wait
59+
# a little and retry.
60+
files = [
61+
os.path.join(dp, f) for dp, dn, fn in os.walk(directory) for f in fn
62+
]
63+
print("Directory still contains files: ", files)
64+
print("... sleeping then retrying")
65+
time.sleep(10)
66+
shutil.rmtree(directory)
67+
else:
68+
logging.debug(
69+
"Test execution failed, leaving the test directory {} intact.".format(
70+
directory
71+
)
72+
)
73+
74+
75+
class TestServer(TailableProc):
76+
def __init__(self, directory):
77+
TailableProc.__init__(self, outputDir=directory)
78+
self.cmd_line = [
79+
"python3",
80+
str(Path(__file__).parent / ".." / "gltestserver" / "__main__.py"),
81+
"run",
82+
]
83+
84+
def start(self):
85+
TailableProc.start(self)
86+
self.wait_for_log(r"Ctrl-C")
87+
88+
def stop(self):
89+
self.proc.send_signal(signal.SIGTERM)
90+
self.proc.wait()
91+
92+
93+
@pytest.fixture
94+
def testserver(directory):
95+
ts = TestServer(directory=directory)
96+
ts.start()
97+
98+
99+
metadata = json.load(open(f'{directory}/metadata.json'))
100+
pprint(metadata)
101+
102+
yield ts
103+
ts.stop()
104+
105+
106+
def test_start(testserver):
107+
print(TailableProc)

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,4 +40,4 @@ pillow = "^9.5.0"
4040
python-lsp-server = "^1.10.0"
4141

4242
[tool.uv.workspace]
43-
members = ["libs/gl-testing"]
43+
members = ["libs/gl-testing", "libs/gl-testserver"]

0 commit comments

Comments
 (0)