summaryrefslogtreecommitdiff
path: root/tests/python/daemon.md
blob: 51c77b4cf81c1d49fdd3f63af12547bf3d52fc59 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
# Introduction

The [Subplot][] library `daemon` for Python provides scenario steps
and their implementations for running a background process and
terminating at the end of the scenario.

[Subplot]: https://subplot.liw.fi/

This document explains the acceptance criteria for the library and how
they're verified. It uses the steps and functions from the
`lib/daemon` library. The scenarios all have the same structure: run a
command, then examine the exit code, verify the process is running.

# Daemon is started and terminated

This scenario starts a background process, verifies it's started, and
verifies it's terminated after the scenario ends.

~~~scenario
given there is no "sleep 12765" process
when I start "sleep 12765" as a background process as sleepyhead
then a process "sleep 12765" is running
when I stop background process sleepyhead
then there is no "sleep 12765" process
~~~


# Daemon takes a while to open its port

This scenario verifies that if the background process doesn't immediately start
listening on its port, the daemon library handles that correctly. We do this
with a helper script that waits 2 seconds before opening the port. The
lib/daemon code will wait for the script by repeatedly trying to connect. Once
successful, it immediately closes the port, which causes the script to
terminate.

~~~scenario
given a daemon helper shell script slow-start-daemon.py
given there is no "slow-start-daemon.py" process
when I try to start "./slow-start-daemon.py" as slow-daemon, on port 8888
then starting the daemon succeeds
when I stop background process slow-daemon
then there is no "slow-start-daemon.py" process
~~~

~~~{#slow-start-daemon.py .file .python .numberLines}
#!/usr/bin/env python3

import socket
import time

time.sleep(2)

s = socket.socket()
s.bind(("127.0.0.1", 8888))
s.listen()

(conn, _) = s.accept()
conn.recv(1)
s.close()

print("OK")
~~~

# Daemon never opens the intended port

This scenario verifies that if the background process never starts
listening on its port, the daemon library handles that correctly.

~~~scenario
given there is no "sleep 12765" process
when I try to start "sleep 12765" as sleepyhead, on port 8888
then starting daemon fails with "ConnectionRefusedError"
then a process "sleep 12765" is running
when I stop background process sleepyhead
then there is no "sleep 12765" process
~~~


# Daemon stdout and stderr are retrievable

Sometimes it's useful for the step functions to be able to retrieve
the stdout or stderr of of the daemon, after it's started, or even
after it's terminated. This scenario verifies that `lib/daemon` can do
that.

~~~scenario
given a daemon helper shell script chatty-daemon.sh
given there is no "chatty-daemon" process
when I start "./chatty-daemon.sh" as a background process as chatty-daemon
when daemon chatty-daemon has produced output
when I stop background process chatty-daemon
then there is no "chatty-daemon" process
then daemon chatty-daemon stdout is "hi there\n"
then daemon chatty-daemon stderr is "hola\n"
~~~

We make for the daemon to exit, to work around a race condition: if
the test program retrieves the daemon's output too fast, it may not
have had time to produce it yet.


~~~{#chatty-daemon.sh .file .sh .numberLines}
#!/usr/bin/env bash

set -euo pipefail

trap 'exit 0' TERM

echo hola 1>&2
echo hi there
~~~

# Can specify additional environment variables for daemon

Some daemons are configured through their environment rather than configuration
files. This scenario verifies that a step can set arbitrary variables in the
daemon's environment.

~~~scenario
when I start "/usr/bin/env" as a background process as env, with environment {"custom_variable": "has a Value"}
when daemon env has produced output
when I stop background process env
then daemon env stdout contains "custom_variable=has a Value"
~~~

~~~scenario
given a daemon helper shell script env-with-port.py
when I try to start "./env-with-port.py 8765" as env-with-port, on port 8765, with environment {"custom_variable": "1337"}
when I stop background process env-with-port
then daemon env-with-port stdout contains "custom_variable=1337"
~~~

~~~scenario
given a daemon helper shell script env-with-port.py
when I start "./env-with-port.py 8766" as a background process as another-env-with-port, on port 8766, with environment {"subplot2": "000"}
when daemon another-env-with-port has produced output
when I stop background process another-env-with-port
then daemon another-env-with-port stdout contains "subplot2=000"
~~~

It's important that these new environment variables are not inherited by the
steps that follow. To verify that, we run one more scenario which *doesn't* set
any variables, but checks that none of the variables we mentioned above are
present.

~~~scenario
when I start "/usr/bin/env" as a background process as env2
when daemon env2 has produced output
when I stop background process env2
then daemon env2 stdout doesn't contain "custom_variable=has a Value"
then daemon env2 stdout doesn't contain "custom_variable=1337"
then daemon env2 stdout doesn't contain "subplot2=000"
~~~

~~~{#env-with-port.py .file .python .numberLines}
#!/usr/bin/env python3

import os
import socket
import sys
import time

for (key, value) in os.environ.items():
    print(f"{key}={value}")

port = int(sys.argv[1])
print(f"port is {port}")

s = socket.socket()
s.bind(("127.0.0.1", port))
s.listen()

(conn, _) = s.accept()
conn.recv(1)
s.close()
~~~


---
title: Acceptance criteria for the lib/daemon Subplot library
author: The Subplot project
bindings:
- lib/daemon.yaml
impls:
  python:
    - lib/daemon.py
    - lib/runcmd.py
...