Skip to content

Commit f597050

Browse files
authored
Add Unit Testing, Debugging Info and NTP Client and Weather HTTP Client code samples from https://vala.gitbook.io/vala (#210)
* Add Unit Testing page to tutorial and expand Debugging Page of tutorial * Add NTP Client and Weather HTTP CLient sample * Fix typo in Construction page of main tutorial
1 parent 9521ef8 commit f597050

9 files changed

Lines changed: 345 additions & 1 deletion

File tree

docs/.vitepress/config.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -473,6 +473,10 @@ export default {
473473
text: "8.2. Using GLib",
474474
link: "/tutorials/programming-language/main/08-00-techniques/08-02-using-glib",
475475
},
476+
{
477+
text: "8.3. Unit Testing",
478+
link: "/tutorials/programming-language/main/08-00-techniques/08-03-unit-testing",
479+
},
476480
],
477481
},
478482
],
@@ -1613,6 +1617,14 @@ export default {
16131617
text: "Lua embedding",
16141618
link: "/sample-code/other/lua-sample",
16151619
},
1620+
{
1621+
text: "NTP client (Posix UDP)",
1622+
link: "/sample-code/other/ntp-client-sample",
1623+
},
1624+
{
1625+
text: "Weather HTTP client (GIO)",
1626+
link: "/sample-code/other/weather-http-client-sample",
1627+
},
16161628
{
16171629
text: "TIFF",
16181630
link: "/sample-code/other/tiff-sample",

docs/sample-code/glib-samples/testing-samples.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Testing Samples
22

3+
For a short narrative introduction to the same APIs, see
4+
[8.3. Unit Testing](/tutorials/programming-language/main/08-00-techniques/08-03-unit-testing)
5+
in the main tutorial.
6+
37
This example shows how to register a minimal unit test with
48
[`GLib.Test`](https://valadoc.org/glib-2.0/GLib.Test.html), run it, and exit
59
with a useful status code.

docs/sample-code/other/index.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ These pages port miscellaneous Vala examples from the archived GNOME Wiki into t
1010
<li><a href="./interfaces-implemented-in-c">Interfaces implemented in C</a></li>
1111
<li><a href="./loudmouth-sample">Loudmouth (XMPP)</a></li>
1212
<li><a href="./lua-sample">Lua embedding</a></li>
13+
<li><a href="./ntp-client-sample">NTP client (Posix UDP)</a></li>
14+
<li><a href="./weather-http-client-sample">Weather HTTP client (GIO socket)</a></li>
1315
<li><a href="./tiff-sample">TIFF (libtiff)</a></li>
1416
<li><a href="./usb-sample">USB (libusb)</a></li>
1517
<li><a href="./xml-sample">XML (libxml2)</a></li>
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
# NTP client (Posix UDP)
2+
3+
Minimal [NTP](https://www.ntp.org/) client over UDP using only the Posix VAPI
4+
(`--pkg posix`): `Posix.getaddrinfo` for host and port lookup (IPv4 and IPv6),
5+
UDP `socket` / `connect` / `read` / `write`, byte order on the transmit timestamp,
6+
and `Posix.ctime` / `time_t` for printing.
7+
8+
The service name `"123"` is passed to `getaddrinfo` so the port does not need
9+
to be filled in by hand. `Posix.getnameinfo` prints the numeric address that was
10+
actually used. Always call `Posix.freeaddrinfo` on the result list.
11+
12+
## Program
13+
14+
Save as `ntp-client.vala`:
15+
16+
```vala
17+
struct NtpPacket {
18+
uint8 li_vn_mode;
19+
uint8 stratum;
20+
uint8 poll;
21+
uint8 precision;
22+
uint32 root_delay;
23+
uint32 root_dispersion;
24+
uint32 ref_id;
25+
uint32 ref_tm_s;
26+
uint32 ref_tm_f;
27+
uint32 orig_tm_s;
28+
uint32 orig_tm_f;
29+
uint32 rx_tm_s;
30+
uint32 rx_tm_f;
31+
uint32 tx_tm_s;
32+
uint32 tx_tm_f;
33+
}
34+
35+
int main () {
36+
const string HOSTNAME = "pool.ntp.org";
37+
Posix.AddrInfo hints = Posix.AddrInfo ();
38+
hints.ai_family = Posix.AF_UNSPEC;
39+
hints.ai_socktype = Posix.SOCK_DGRAM;
40+
hints.ai_protocol = Posix.IPProto.UDP;
41+
42+
Posix.AddrInfo* res;
43+
int gai = Posix.getaddrinfo (HOSTNAME, "123", hints, out res);
44+
if (gai != 0) {
45+
stderr.printf ("getaddrinfo: %s\n", Posix.gai_strerror (gai));
46+
return 1;
47+
}
48+
49+
int sockfd = -1;
50+
unowned Posix.AddrInfo* chosen = null;
51+
for (unowned Posix.AddrInfo* ai = res; ai != null; ai = ai.ai_next) {
52+
int fd = Posix.socket (ai.ai_family, ai.ai_socktype, ai.ai_protocol);
53+
if (fd < 0) {
54+
continue;
55+
}
56+
if (Posix.connect (fd, ai.ai_addr, (Posix.socklen_t) ai.ai_addrlen) == 0) {
57+
sockfd = fd;
58+
chosen = ai;
59+
break;
60+
}
61+
Posix.close (fd);
62+
}
63+
64+
if (sockfd < 0 || chosen == null) {
65+
stderr.printf ("Could not connect to any resolved address\n");
66+
Posix.freeaddrinfo (res);
67+
return 1;
68+
}
69+
70+
var hostbuf = new char[256];
71+
var servbuf = new char[64];
72+
if (Posix.getnameinfo (*chosen.ai_addr, (Posix.socklen_t) chosen.ai_addrlen,
73+
hostbuf, servbuf, Posix.NI_NUMERICHOST | Posix.NI_NUMERICSERV) == 0) {
74+
stderr.printf ("Using %s port %s\n", (string) hostbuf, (string) servbuf);
75+
}
76+
77+
var packet = NtpPacket ();
78+
assert (sizeof (NtpPacket) == 48);
79+
packet.li_vn_mode = 0x1b;
80+
81+
if (Posix.write (sockfd, &packet, sizeof (NtpPacket)) < 0) {
82+
stderr.printf ("Can't send UDP packet: errno %d\n", Posix.errno);
83+
Posix.close (sockfd);
84+
Posix.freeaddrinfo (res);
85+
return 1;
86+
}
87+
88+
if (Posix.read (sockfd, &packet, sizeof (NtpPacket)) < 0) {
89+
stderr.printf ("Can't read from socket: errno %d\n", Posix.errno);
90+
Posix.close (sockfd);
91+
Posix.freeaddrinfo (res);
92+
return 1;
93+
}
94+
Posix.close (sockfd);
95+
Posix.freeaddrinfo (res);
96+
97+
packet.tx_tm_s = Posix.ntohl (packet.tx_tm_s);
98+
packet.tx_tm_f = Posix.ntohl (packet.tx_tm_f);
99+
const uint64 NTP_TIMESTAMP_DELTA = 2208988800ULL;
100+
time_t tx_tm = (time_t) ((int64) packet.tx_tm_s - (int64) NTP_TIMESTAMP_DELTA);
101+
unowned string? utc_str = Posix.ctime (ref tx_tm);
102+
stdout.printf ("Current UTC time is %s", utc_str ?? "unknown\n");
103+
return 0;
104+
}
105+
```
106+
107+
## Compile and run
108+
109+
```shell
110+
valac --pkg posix ntp-client.vala
111+
./ntp-client
112+
```
113+
114+
`Posix.ctime` returns a libc-formatted string (usually including a trailing newline).
115+
Outbound UDP to port 123 must be allowed. The program prints diagnostics on
116+
`stderr` and the time string on `stdout`.
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
# Weather HTTP client (GIO socket)
2+
3+
HTTP GET over a plain TCP socket using GIO's `Resolver`, `SocketClient`, and
4+
`DataInputStream`.
5+
6+
## Program
7+
8+
Save as `weather-client.vala`:
9+
10+
```vala
11+
void main (string[] args) {
12+
string key = Environment.get_variable ("WEATHER_API_KEY") ?? "";
13+
string city = "London";
14+
if (key == "") {
15+
stderr.printf ("Set WEATHER_API_KEY to a key from https://www.weatherapi.com/\n");
16+
return;
17+
}
18+
if (args.length > 1) {
19+
city = args[1];
20+
}
21+
const string HOST = "api.weatherapi.com";
22+
const uint16 PORT = 80;
23+
string query = @"/v1/current.json?key=$key&q=$(Uri.escape_string (city, null, false))";
24+
string message = @"GET $query HTTP/1.1\r\nHost: $HOST\r\nConnection: close\r\n\r\n";
25+
26+
try {
27+
var resolver = Resolver.get_default ();
28+
var addresses = resolver.lookup_by_name (HOST, null);
29+
var address = addresses.nth_data (0);
30+
stderr.printf (@"Resolved $HOST to $address\n");
31+
32+
var client = new SocketClient ();
33+
var conn = client.connect (new InetSocketAddress (address, PORT));
34+
stderr.printf (@"Connected to $HOST\n");
35+
36+
conn.output_stream.write (message.data);
37+
stderr.printf ("Wrote HTTP request\n");
38+
39+
var response = new DataInputStream (conn.input_stream);
40+
var status_line = response.read_line (null).strip ();
41+
stderr.printf (@"Status: '$status_line'\n");
42+
43+
if (!("200" in status_line)) {
44+
stderr.printf ("Expected 200 OK\n");
45+
return;
46+
}
47+
48+
var headers = new HashTable<string, string> (str_hash, str_equal);
49+
while (true) {
50+
string? raw = response.read_line (null);
51+
if (raw == null) {
52+
break;
53+
}
54+
string line = raw.strip ();
55+
if (line.length == 0) {
56+
break;
57+
}
58+
var parts = line.split (":", 2);
59+
if (parts.length == 2) {
60+
headers[parts[0].strip ()] = parts[1].strip ();
61+
}
62+
}
63+
if (!headers.contains ("Content-Length")) {
64+
stderr.printf ("No Content-Length header\n");
65+
return;
66+
}
67+
int content_length = int.parse (headers["Content-Length"]);
68+
var body = new uint8[content_length];
69+
size_t actual_length = 0;
70+
response.read_all (body, out actual_length);
71+
stdout.write (body[0:actual_length]);
72+
stdout.putc ('\n');
73+
} catch (Error e) {
74+
stderr.printf ("%s\n", e.message);
75+
}
76+
}
77+
```
78+
79+
## Compile and run
80+
81+
Create a free API key at [weatherapi.com](https://www.weatherapi.com/), then:
82+
83+
```shell
84+
export WEATHER_API_KEY="your-key-here"
85+
valac --pkg gio-2.0 weather-client.vala
86+
./weather-client
87+
./weather-client "New York"
88+
```
89+
90+
The JSON body is written to `stdout`; progress messages go to `stderr`.
91+
92+
For production HTTP you would normally use [Soup](https://valadoc.org/libsoup-3.0/Soup.html)
93+
or another high-level client instead of hand-parsing headers.

docs/tutorials/programming-language/main/03-00-object-oriented-programming/03-02-construction.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# 3.2. Construction
22

33
Vala supports two construction schemes that both target the same GObject type system:
4-
- Java/C#-style** constructors (named creation methods with bodies where you assign fields and call helpers)
4+
- Java/C#-style constructors (named creation methods with bodies where you assign fields and call helpers)
55
- GObject-style construction (`Object (...)`, **construct properties**, and `construct { }` blocks).
66

77
They are equally central to writing GObject types; which you emphasize depends on the API you publish and how subclasses and bindings should interact with your type.

docs/tutorials/programming-language/main/08-00-techniques.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,7 @@
1212
</li>
1313
</ul>
1414
</li>
15+
<li>
16+
<a href="08-00-techniques/08-03-unit-testing">8.3. Unit Testing</a>
17+
</li>
1518
</ul>

docs/tutorials/programming-language/main/08-00-techniques/08-01-debugging.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,3 +48,25 @@ Program received signal SIGSEGV, Segmentation fault.
4848
7 stdout.printf ("%d\n", foo.field);
4949
(gdb)
5050
```
51+
52+
## LLDB (common on macOS)
53+
54+
The same binary built with `valac -g` can be loaded into LLVM’s
55+
[`lldb`](https://lldb.llvm.org/). After `lldb debug-demo`, use `run` like in
56+
`gdb`, then `bt` (backtrace) to see Vala line numbers that were embedded at
57+
compile time.
58+
59+
## GLib and GObject runtime checks
60+
61+
Many crashes or warnings in GLib-based code come from misuse of reference
62+
counting, main-loop re-entrancy, or invalid object state. While debugging:
63+
64+
- Set [`G_DEBUG`](https://docs.gtk.org/glib/running.html#environment-variables)
65+
(for example `G_DEBUG=fatal-warnings` or `gc-friendly`) to turn selected
66+
warnings into breakpoints or clearer logs.
67+
- Use `G_MESSAGES_DEBUG` to enable extra log output from components that honor
68+
`G_LOG_DOMAIN` (see the GLib “Running GLib Applications” documentation).
69+
70+
These do not replace a debugger, but they narrow down whether a failure is a
71+
plain segfault or a GLib `g_return_if_fail` path.
72+
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
# 8.3. Unit Testing
2+
3+
Vala programs use the same unit-test harness as GLib and GTK: the
4+
[`GLib.Test`](https://valadoc.org/glib-2.0/GLib.Test.html) API. Tests are ordinary
5+
Vala functions registered with the harness; when you run the compiled binary, GLib
6+
prints [TAP](https://testanything.org/)-style output and sets the process exit code
7+
so CI systems (and [`meson test`](https://mesonbuild.com/Unit-tests.html)) can tell
8+
pass from fail.
9+
10+
For a Unit Testing Sample Code see: [Testing Samples](../../../../sample-code/glib-samples/testing-samples).
11+
12+
## Minimal harness
13+
14+
Every test executable should call `Test.init` before registering tests, then
15+
`Test.run` after registration:
16+
17+
```vala
18+
void add_tests () {
19+
Test.add_func ("/example/math/adds", () => {
20+
assert (1 + 1 == 2);
21+
});
22+
}
23+
24+
void main (string[] args) {
25+
Test.init (ref args);
26+
add_tests ();
27+
Test.run ();
28+
}
29+
```
30+
31+
Compile and run:
32+
33+
```shell
34+
valac --pkg glib-2.0 tests.vala
35+
./tests
36+
```
37+
38+
`Test.add_func` takes a hierarchy path (by convention starting with `/`) and
39+
a callback. Paths group related cases in the TAP log and in tools that understand
40+
GLib’s test tree.
41+
42+
## Assertions and messages
43+
44+
Plain `assert (condition)` is the usual choice in Vala tests. When you need a
45+
custom failure explanation, record context with `Test.message` and stop the case
46+
with `Test.fail`, or skip unsupported platforms with `Test.skip`:
47+
48+
```vala
49+
Test.add_func ("/example/strings/concat", () => {
50+
string got = "foo" + "bar";
51+
if (got != "foobar") {
52+
Test.message ("expected foobar, got %s", got);
53+
Test.fail ();
54+
}
55+
});
56+
```
57+
58+
The C GLib headers also expose `g_assert_cmpstr`-style macros for richer logs;
59+
those are not always wrapped in Vala’s `glib-2.0` vapi, so many projects stick to
60+
`assert` or explicit `Test.message` blocks.
61+
62+
## Suites and shared setup
63+
64+
For several cases that share expensive setup, register a
65+
[`TestSuite`](https://valadoc.org/glib-2.0/GLib.TestSuite.html) or split setup into
66+
helpers called from each `Test.add_func` callback. GLib also provides
67+
`Test.add_data_func` / `Test.add_data_func_full` for table-driven cases where the
68+
same function runs with different user data (see the GLib documentation for the
69+
exact C→Vala naming you need in your Vala version).
70+
71+
## Subprocess and timing helpers
72+
73+
GLib can re-run the test binary in a subprocess to isolate crashes or to test
74+
`MainLoop`-driven code. See the subprocess and trap-related symbols on
75+
[`GLib.Test`](https://valadoc.org/glib-2.0/GLib.Test.html) (for example
76+
`Test.trap_subprocess`) when a unit must simulate a separate process or capture
77+
fatal errors.
78+
79+
## Meson and CI
80+
81+
Meson’s `test()` target runs an executable and interprets its exit status. A
82+
minimal `meson.build` is shown on the
83+
[Testing Samples](../../../../sample-code/glib-samples/testing-samples#meson) page.
84+
In CI, run `meson test -C build` (or your build directory) so every registered
85+
`Test.add_func` is executed automatically.
86+
87+
## See also
88+
89+
- [Testing Samples](../../../../sample-code/glib-samples/testing-samples) — full
90+
copy-paste example and Meson snippet
91+
- [GLib.Test reference](https://valadoc.org/glib-2.0/GLib.Test.html)
92+
- [8.1. Debugging](08-01-debugging) — using debuggers when a test or app crashes

0 commit comments

Comments
 (0)