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
|
use crate::harness::{SiteBuilder, TestServer, test_config_with_site};
use serial_test::serial;
use std::time::Duration;
/// Send SIGHUP to the current process.
fn send_sighup_to_self() {
use nix::sys::signal::{Signal, kill};
use nix::unistd::Pid;
kill(Pid::this(), Signal::SIGHUP).expect("failed to send SIGHUP");
}
/// Install the SIGHUP handler and wait for it to be registered.
async fn install_sighup_handler(server: &TestServer) {
witryna::test_support::setup_sighup_handler(&server.state);
// Yield to allow the spawned handler task to register the signal listener
tokio::task::yield_now().await;
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
}
#[tokio::test]
#[serial]
async fn sighup_reload_keeps_server_healthy() {
let dir = tempfile::tempdir().unwrap().keep();
let site = SiteBuilder::new("my-site", "https://example.com/repo.git", "test-token").build();
let server = TestServer::start(test_config_with_site(dir, site)).await;
install_sighup_handler(&server).await;
// Verify server is healthy before SIGHUP
let resp = TestServer::client()
.get(server.url("/health"))
.send()
.await
.unwrap();
assert_eq!(resp.status().as_u16(), 200);
// Send SIGHUP (reload config)
send_sighup_to_self();
// Give the handler time to process
tokio::time::sleep(std::time::Duration::from_millis(500)).await;
// Server should still be healthy
let resp = TestServer::client()
.get(server.url("/health"))
.send()
.await
.unwrap();
assert_eq!(resp.status().as_u16(), 200);
}
#[tokio::test]
#[serial]
async fn rapid_sighup_does_not_crash() {
let dir = tempfile::tempdir().unwrap().keep();
let site = SiteBuilder::new("my-site", "https://example.com/repo.git", "test-token").build();
let server = TestServer::start(test_config_with_site(dir, site)).await;
install_sighup_handler(&server).await;
// Send multiple SIGHUPs in quick succession
for _ in 0..3 {
send_sighup_to_self();
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
}
// Wait for stabilization
tokio::time::sleep(std::time::Duration::from_millis(500)).await;
// Server should survive
let resp = TestServer::client()
.get(server.url("/health"))
.send()
.await
.unwrap();
assert_eq!(resp.status().as_u16(), 200);
}
#[tokio::test]
#[serial]
async fn sighup_preserves_listen_address() {
let dir = tempfile::tempdir().unwrap().keep();
let site = SiteBuilder::new("my-site", "https://example.com/repo.git", "test-token").build();
let server = TestServer::start(test_config_with_site(dir, site)).await;
install_sighup_handler(&server).await;
// Verify server is healthy before SIGHUP
let resp = TestServer::client()
.get(server.url("/health"))
.send()
.await
.unwrap();
assert_eq!(resp.status().as_u16(), 200);
// Rewrite the on-disk config with a different listen_address (unreachable port)
// and an additional site to verify reloadable fields are updated
let config_path = server.state.config_path.as_ref();
let new_toml = format!(
r#"listen_address = "127.0.0.1:19999"
container_runtime = "podman"
base_dir = "{}"
log_dir = "{}"
log_level = "debug"
[[sites]]
name = "my-site"
repo_url = "https://example.com/repo.git"
branch = "main"
webhook_token = "test-token"
[[sites]]
name = "new-site"
repo_url = "https://example.com/new.git"
branch = "main"
webhook_token = "new-token"
"#,
server
.state
.config
.read()
.expect("config lock poisoned")
.base_dir
.display(),
server
.state
.config
.read()
.expect("config lock poisoned")
.log_dir
.display(),
);
tokio::fs::write(config_path, &new_toml).await.unwrap();
// Send SIGHUP to reload
send_sighup_to_self();
tokio::time::sleep(Duration::from_millis(500)).await;
// Server should still respond on the original port (listen_address preserved)
let resp = TestServer::client()
.get(server.url("/health"))
.send()
.await
.unwrap();
assert_eq!(resp.status().as_u16(), 200);
// Verify the reloadable field (sites) was updated
let (sites_len, has_new_site, listen_addr) = {
let config = server.state.config.read().expect("config lock poisoned");
(
config.sites.len(),
config.find_site("new-site").is_some(),
config.listen_address.clone(),
)
};
assert_eq!(sites_len, 2, "sites should have been reloaded");
assert!(has_new_site, "new-site should exist after reload");
// Verify non-reloadable field was preserved (not overwritten with "127.0.0.1:19999")
assert_ne!(
listen_addr, "127.0.0.1:19999",
"listen_address should be preserved from original config"
);
}
|