Skip to content

Commit 13e7926

Browse files
committed
Merge branch 'fix/weather-client-local-stub' into 'main'
test(examples): use local stub for HTTPClient/WeatherClient See merge request arolang/aro!232
2 parents 483bad4 + 71a0d41 commit 13e7926

File tree

7 files changed

+156
-3
lines changed

7 files changed

+156
-3
lines changed

.gitlab-ci.yml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,15 @@ integration:linux:
280280
- timeout 60 ./aro-dist/aro build ./Examples/HelloWorld --verbose 2>&1 || echo "Build failed with exit code $?"
281281
- echo "=== aro build completed ==="
282282

283+
# Start local Open-Meteo stubs (offline HTTPClient / WeatherClient tests).
284+
# Running them here, outside the test harness, avoids IPC::Run keeping
285+
# pipes tied to backgrounded processes when a per-test pre-script exits.
286+
- STUB_TIMEOUT=1800 nohup python3 Examples/HTTPClient/stub.py 18765 >/tmp/httpclient-stub.log 2>&1 </dev/null &
287+
- STUB_TIMEOUT=1800 nohup python3 Examples/WeatherClient/stub.py 18766 >/tmp/weatherclient-stub.log 2>&1 </dev/null &
288+
- for i in $(seq 1 50); do python3 -c 'import socket; s=socket.socket(); s.settimeout(0.2); exit(0 if s.connect_ex(("127.0.0.1",18765))==0 else 1)' && python3 -c 'import socket; s=socket.socket(); s.settimeout(0.2); exit(0 if s.connect_ex(("127.0.0.1",18766))==0 else 1)' && break; sleep 0.2; done
289+
- python3 -c 'import urllib.request; print(urllib.request.urlopen("http://127.0.0.1:18765/v1/forecast").read()[:120])'
290+
- python3 -c 'import urllib.request; print(urllib.request.urlopen("http://127.0.0.1:18766/v1/forecast").read()[:120])'
291+
283292
- chmod +x test-examples.pl
284293
- perl test-examples.pl --verbose
285294
artifacts:

Examples/HTTPClient/main.aro

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,12 @@
44
Log "HTTP Client Demo" to the <console>.
55
Log "Fetching weather data from Open-Meteo API..." to the <console>.
66

7-
(* Create the API URL *)
8-
Create the <api-url> with "https://api.open-meteo.com/v1/forecast?latitude=52.52&longitude=13.41&current_weather=true".
7+
(* Create the API URL.
8+
For reliable, offline-friendly tests this points at a local stub
9+
(see stub.py). To call the real Open-Meteo API instead, replace
10+
the URL below with:
11+
"https://api.open-meteo.com/v1/forecast?latitude=52.52&longitude=13.41&current_weather=true" *)
12+
Create the <api-url> with "http://127.0.0.1:18765/v1/forecast?latitude=52.52&longitude=13.41&current_weather=true".
913

1014
(* Simple GET request — response includes body, status, and headers *)
1115
Request the <response> from the <api-url>.

Examples/HTTPClient/stub.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
#!/usr/bin/env python3
2+
"""Local stub for the Open-Meteo weather API.
3+
4+
Used by the HTTPClient example so that integration tests do not depend on
5+
api.open-meteo.com (which has occasional 502s and breaks unrelated CI runs).
6+
7+
Behaviour:
8+
- Listens on 127.0.0.1:<port> (default 18765, override with $1).
9+
- Responds to any GET with a canned forecast JSON payload that mirrors the
10+
fields the example expects (current_weather, current_weather_units, etc.).
11+
- Self-terminates after STUB_TIMEOUT seconds (default 1800) so a leaked
12+
process cannot survive a CI job.
13+
"""
14+
import http.server
15+
import json
16+
import os
17+
import sys
18+
import threading
19+
20+
PORT = int(sys.argv[1]) if len(sys.argv) > 1 else int(os.environ.get("STUB_PORT", "18765"))
21+
22+
PAYLOAD = {
23+
"latitude": 52.52,
24+
"longitude": 13.41,
25+
"elevation": 38.0,
26+
"timezone": "GMT",
27+
"timezone_abbreviation": "GMT",
28+
"utc_offset_seconds": 0,
29+
"current_weather": {
30+
"interval": 900,
31+
"is_day": 1,
32+
"temperature": 12.3,
33+
"time": "2026-04-07T08:00",
34+
"weathercode": 3,
35+
"winddirection": 210,
36+
"windspeed": 9.4,
37+
},
38+
"current_weather_units": {
39+
"interval": "seconds",
40+
"temperature": "°C",
41+
"time": "iso8601",
42+
"weathercode": "wmo code",
43+
"winddirection": "°",
44+
"windspeed": "km/h",
45+
},
46+
}
47+
48+
49+
class Handler(http.server.BaseHTTPRequestHandler):
50+
def do_GET(self):
51+
body = json.dumps(PAYLOAD).encode("utf-8")
52+
self.send_response(200)
53+
self.send_header("Content-Type", "application/json")
54+
self.send_header("Content-Length", str(len(body)))
55+
self.end_headers()
56+
self.wfile.write(body)
57+
58+
def log_message(self, *_args, **_kwargs):
59+
pass
60+
61+
62+
def main():
63+
# Set SO_REUSEADDR so a quick restart doesn't hit TIME_WAIT.
64+
http.server.HTTPServer.allow_reuse_address = True
65+
server = http.server.HTTPServer(("127.0.0.1", PORT), Handler)
66+
timeout = float(os.environ.get("STUB_TIMEOUT", "1800"))
67+
threading.Timer(timeout, server.shutdown).start()
68+
server.serve_forever()
69+
70+
71+
if __name__ == "__main__":
72+
main()

Examples/HTTPClient/test.hint

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,7 @@
22
# Uses occurrence-check since API responses contain dynamic data (weather, timestamps)
33
# Verifies HTTP client functionality and response structure
44
occurrence-check: true
5+
# The Open-Meteo API is stubbed locally (see stub.py). In CI the stub is
6+
# started by the integration:linux before_script so it outlives each test.
7+
# For local runs we start/poll it from the pre-script.
8+
pre-script: if ! python3 -c 'import socket,sys; s=socket.socket(); s.settimeout(0.1); sys.exit(0 if s.connect_ex(("127.0.0.1",18765))==0 else 1)' 2>/dev/null; then (python3 Examples/HTTPClient/stub.py 18765 >/dev/null 2>&1 </dev/null &); for i in $(seq 1 100); do python3 -c 'import socket,sys; s=socket.socket(); s.settimeout(0.1); sys.exit(0 if s.connect_ex(("127.0.0.1",18765))==0 else 1)' 2>/dev/null && exit 0; sleep 0.1; done; echo "stub failed to start" >&2; exit 1; fi

Examples/WeatherClient/main.aro

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
(* Fetch weather data from an external API *)
22
(Application-Start: Weather Client) {
33
Log "Fetching weather..." to the <console>.
4-
Create the <api-url> with "https://api.open-meteo.com/v1/forecast?latitude=52.52&longitude=13.41&current_weather=true".
4+
(* This points at a local stub (see stub.py) so the example does not
5+
depend on a third-party service. To call the real Open-Meteo API,
6+
use: "https://api.open-meteo.com/v1/forecast?latitude=52.52&longitude=13.41&current_weather=true" *)
7+
Create the <api-url> with "http://127.0.0.1:18766/v1/forecast?latitude=52.52&longitude=13.41&current_weather=true".
58
Request the <response> from the <api-url>.
69
Extract the <weather> from the <response: body>.
710
Log <weather> to the <console>.

Examples/WeatherClient/stub.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
#!/usr/bin/env python3
2+
"""Local stub for the Open-Meteo weather API.
3+
4+
Used by the WeatherClient example so that integration tests do not depend on
5+
api.open-meteo.com (which has occasional 502s and breaks unrelated CI runs).
6+
7+
Behaviour:
8+
- Listens on 127.0.0.1:<port> (default 18766, override with $1).
9+
- Responds to any GET with a canned forecast JSON payload that mirrors the
10+
fields the example expects (current_weather, latitude, longitude, ...).
11+
- Self-terminates after STUB_TIMEOUT seconds (default 1800) so a leaked
12+
process cannot survive a CI job.
13+
"""
14+
import http.server
15+
import json
16+
import os
17+
import sys
18+
import threading
19+
20+
PORT = int(sys.argv[1]) if len(sys.argv) > 1 else int(os.environ.get("STUB_PORT", "18766"))
21+
22+
PAYLOAD = {
23+
"latitude": 52.52,
24+
"longitude": 13.41,
25+
"current_weather": {
26+
"temperature": 12.3,
27+
"windspeed": 9.4,
28+
"winddirection": 210,
29+
"weathercode": 3,
30+
},
31+
}
32+
33+
34+
class Handler(http.server.BaseHTTPRequestHandler):
35+
def do_GET(self):
36+
body = json.dumps(PAYLOAD).encode("utf-8")
37+
self.send_response(200)
38+
self.send_header("Content-Type", "application/json")
39+
self.send_header("Content-Length", str(len(body)))
40+
self.end_headers()
41+
self.wfile.write(body)
42+
43+
def log_message(self, *_args, **_kwargs):
44+
pass
45+
46+
47+
def main():
48+
# Set SO_REUSEADDR so a quick restart doesn't hit TIME_WAIT.
49+
http.server.HTTPServer.allow_reuse_address = True
50+
server = http.server.HTTPServer(("127.0.0.1", PORT), Handler)
51+
timeout = float(os.environ.get("STUB_TIMEOUT", "1800"))
52+
threading.Timer(timeout, server.shutdown).start()
53+
server.serve_forever()
54+
55+
56+
if __name__ == "__main__":
57+
main()

Examples/WeatherClient/test.hint

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,6 @@
11
mode: both
22
occurrence-check: true
3+
# The Open-Meteo API is stubbed locally (see stub.py). In CI the stub is
4+
# started by the integration:linux before_script so it outlives each test.
5+
# For local runs we start/poll it from the pre-script.
6+
pre-script: if ! python3 -c 'import socket,sys; s=socket.socket(); s.settimeout(0.1); sys.exit(0 if s.connect_ex(("127.0.0.1",18766))==0 else 1)' 2>/dev/null; then (python3 Examples/WeatherClient/stub.py 18766 >/dev/null 2>&1 </dev/null &); for i in $(seq 1 100); do python3 -c 'import socket,sys; s=socket.socket(); s.settimeout(0.1); sys.exit(0 if s.connect_ex(("127.0.0.1",18766))==0 else 1)' 2>/dev/null && exit 0; sleep 0.1; done; echo "stub failed to start" >&2; exit 1; fi

0 commit comments

Comments
 (0)