Skip to content

Commit fdde485

Browse files
authored
Add get_daemon (#19)
* Add get_daemon Signed-off-by: kerthcet <kerthcet@gmail.com> * fix lint Signed-off-by: kerthcet <kerthcet@gmail.com> * add verify to is_busy Signed-off-by: kerthcet <kerthcet@gmail.com> * fix tests Signed-off-by: kerthcet <kerthcet@gmail.com> --------- Signed-off-by: kerthcet <kerthcet@gmail.com>
1 parent 708fba3 commit fdde485

4 files changed

Lines changed: 207 additions & 3 deletions

File tree

python/sandd/server.py

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,32 @@ def get_stats(self) -> ServerStats:
242242
"""
243243
return ServerStats(self._server.get_stats())
244244

245+
def get_daemon(self, daemon_id: str) -> Optional[DaemonInfo]:
246+
"""Get daemon by ID
247+
248+
Args:
249+
daemon_id: Daemon ID to lookup
250+
251+
Returns:
252+
DaemonInfo if found, None otherwise
253+
254+
Example:
255+
>>> daemon = server.get_daemon("daemon-1")
256+
>>> if daemon:
257+
... print(f"Found: {daemon.id}, busy={daemon.is_busy}")
258+
... else:
259+
... print("Daemon not found")
260+
"""
261+
info = self._server.get_daemon(daemon_id)
262+
if info is None:
263+
return None
264+
return DaemonInfo(
265+
id=info.id,
266+
version=info.version,
267+
labels=info.labels,
268+
is_busy=info.is_busy,
269+
)
270+
245271
def _run_interactive(self, session: Session) -> None:
246272
"""Run session in interactive mode with live terminal
247273
@@ -403,8 +429,7 @@ def wait_for_daemon(
403429
"""
404430
start = time.time()
405431
while time.time() - start < timeout:
406-
daemons = self.list_daemons()
407-
if any(d.id == daemon_id for d in daemons):
432+
if self.get_daemon(daemon_id) is not None:
408433
return True
409434
time.sleep(poll_interval)
410435
return False

python/tests/test_integration.py

Lines changed: 145 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,145 @@ def test_execute_python_script(self, server, daemon_process):
279279
assert "Python" in result.stdout
280280

281281

282+
class TestGetDaemon:
283+
"""Test get_daemon functionality with real daemons"""
284+
285+
def test_get_existing_daemon(self, server, daemon_process):
286+
"""Test get_daemon returns DaemonInfo for connected daemon"""
287+
daemon_id, _ = daemon_process
288+
289+
daemon = server.get_daemon(daemon_id)
290+
291+
assert daemon is not None
292+
assert daemon.id == daemon_id
293+
assert isinstance(daemon.version, str)
294+
assert isinstance(daemon.labels, dict)
295+
assert isinstance(daemon.is_busy, bool)
296+
297+
def test_get_nonexistent_daemon(self, server, daemon_process):
298+
"""Test get_daemon returns None for non-existent daemon"""
299+
_, _ = daemon_process
300+
301+
result = server.get_daemon("definitely-not-a-real-daemon-id-12345")
302+
assert result is None
303+
304+
def test_get_daemon_with_labels(self, server, sandd_binary):
305+
"""Test get_daemon returns daemon with labels"""
306+
daemon_id = f"test-labeled-daemon-{os.getpid()}"
307+
server_url = f"ws://127.0.0.1:{server.address.split(':')[1]}/ws"
308+
309+
proc = subprocess.Popen(
310+
[
311+
sandd_binary,
312+
"--server-url", server_url,
313+
"--daemon-id", daemon_id,
314+
"--label", "env=staging",
315+
"--label", "team=backend",
316+
],
317+
stdout=subprocess.DEVNULL,
318+
stderr=subprocess.DEVNULL,
319+
)
320+
321+
try:
322+
assert server.wait_for_daemon(daemon_id, timeout=5.0)
323+
324+
daemon = server.get_daemon(daemon_id)
325+
assert daemon is not None
326+
assert daemon.id == daemon_id
327+
assert daemon.labels == {"env": "staging", "team": "backend"}
328+
329+
finally:
330+
proc.kill()
331+
332+
def test_get_daemon_after_disconnect(self, server, sandd_binary):
333+
"""Test get_daemon returns None after daemon disconnects"""
334+
daemon_id = f"test-disconnect-daemon-{os.getpid()}"
335+
server_url = f"ws://127.0.0.1:{server.address.split(':')[1]}/ws"
336+
337+
proc = subprocess.Popen(
338+
[sandd_binary, "--server-url", server_url, "--daemon-id", daemon_id],
339+
stdout=subprocess.DEVNULL,
340+
stderr=subprocess.DEVNULL,
341+
)
342+
343+
try:
344+
# Wait for connection
345+
assert server.wait_for_daemon(daemon_id, timeout=5.0)
346+
347+
# Verify daemon is there
348+
daemon = server.get_daemon(daemon_id)
349+
assert daemon is not None
350+
assert daemon.id == daemon_id
351+
352+
# Kill the daemon
353+
proc.kill()
354+
proc.wait()
355+
356+
# Give some time for disconnect to register
357+
time.sleep(0.5)
358+
359+
# Daemon should no longer be found
360+
daemon = server.get_daemon(daemon_id)
361+
assert daemon is None
362+
finally:
363+
try:
364+
proc.kill()
365+
except: # noqa: E722
366+
pass
367+
368+
def test_get_daemon_multiple_times(self, server, daemon_process):
369+
"""Test calling get_daemon multiple times returns consistent results"""
370+
daemon_id, _ = daemon_process
371+
372+
# Call multiple times
373+
daemon1 = server.get_daemon(daemon_id)
374+
daemon2 = server.get_daemon(daemon_id)
375+
daemon3 = server.get_daemon(daemon_id)
376+
377+
assert daemon1 is not None
378+
assert daemon2 is not None
379+
assert daemon3 is not None
380+
381+
# All should have the same ID
382+
assert daemon1.id == daemon2.id == daemon3.id == daemon_id
383+
384+
def test_get_daemon_busy_state(self, server, daemon_process):
385+
"""Test get_daemon reflects busy state"""
386+
daemon_id, _ = daemon_process
387+
388+
# Check initial state (should not be busy)
389+
daemon = server.get_daemon(daemon_id)
390+
assert daemon is not None
391+
assert daemon.is_busy is False
392+
393+
# Start a long-running command in background
394+
import threading
395+
396+
def run_long_command():
397+
try:
398+
server.exec(daemon_id, "sleep 2", timeout=5)
399+
except: # noqa: E722
400+
pass
401+
402+
thread = threading.Thread(target=run_long_command)
403+
thread.start()
404+
405+
# Give command time to start
406+
time.sleep(0.2)
407+
408+
# Check if daemon is now busy (might be, depending on timing)
409+
daemon_during = server.get_daemon(daemon_id)
410+
assert daemon_during is not None
411+
assert daemon_during.is_busy is True
412+
413+
# Wait for command to complete
414+
thread.join()
415+
416+
# Daemon should not be busy anymore
417+
daemon_after = server.get_daemon(daemon_id)
418+
assert daemon_after is not None
419+
assert daemon_after.is_busy is False
420+
282421
class TestServerStats:
283422
"""Test server statistics with real connections"""
284423

@@ -400,7 +539,12 @@ def wait_thread():
400539
assert result_holder["connected"] is True
401540

402541
finally:
403-
proc.kill()
542+
proc.terminate()
543+
try:
544+
proc.wait(timeout=2)
545+
except subprocess.TimeoutExpired:
546+
proc.kill()
547+
proc.wait()
404548

405549

406550
@pytest.mark.skipif(

python/tests/test_unit.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,31 @@ def test_download_file_invalid_daemon(self):
123123
server.download_file("invalid", "/tmp/test")
124124

125125

126+
class TestGetDaemon:
127+
"""Test get_daemon method"""
128+
129+
def test_returns_none_when_not_found(self):
130+
"""Test get_daemon returns None for non-existent daemon"""
131+
server = Server()
132+
result = server.get_daemon("nonexistent-daemon-id")
133+
assert result is None
134+
135+
def test_returns_none_with_various_ids(self):
136+
"""Test get_daemon returns None for various non-existent IDs"""
137+
server = Server()
138+
test_ids = ["test-1", "daemon-123", "prod-worker-5", "invalid"]
139+
for daemon_id in test_ids:
140+
result = server.get_daemon(daemon_id)
141+
assert result is None, f"Expected None for {daemon_id}"
142+
143+
def test_accepts_string_id(self):
144+
"""Test get_daemon accepts string daemon ID"""
145+
server = Server()
146+
# Should not raise any exceptions with valid string input
147+
result = server.get_daemon("some-daemon-id")
148+
assert result is None # Will be None since no daemon connected
149+
150+
126151
class TestWaitForDaemon:
127152
"""Test wait_for_daemon method"""
128153

server/src/lib.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,16 @@ impl Server {
257257
oldest_connection_secs: stats.oldest_connection_secs,
258258
})
259259
}
260+
261+
/// Get daemon by ID (returns None if not found)
262+
fn get_daemon(&self, daemon_id: String) -> PyResult<Option<PyDaemonInfo>> {
263+
Ok(self.registry.get(&daemon_id).map(|conn| PyDaemonInfo {
264+
id: conn.id.clone(),
265+
version: conn.metadata.version.clone(),
266+
labels: conn.metadata.labels.clone(),
267+
is_busy: conn.is_busy(),
268+
}))
269+
}
260270
}
261271

262272
/// Session handle

0 commit comments

Comments
 (0)