44import io
55import json
66import os
7+ import queue
78import re
89import subprocess
910import sys
1011import tempfile
12+ import threading
1113import time
1214import types
1315import unittest
@@ -157,13 +159,30 @@ def plan_hash(*, disk_max_used_pct: int = 90) -> str:
157159 first = plan_hash ()
158160 self .assertEqual (first , plan_hash ())
159161 self .assertNotEqual (first , plan_hash (disk_max_used_pct = 95 ))
162+ cache = root / "data" / "pagila" / "__pycache__"
163+ cache .mkdir (exist_ok = True )
164+ (cache / "generator.cpython-399.pyc" ).write_bytes (b"interpreter by-product" )
165+ self .assertEqual (first , plan_hash ())
160166 profile = root / "data" / "pagila" / "profile.yml"
161167 profile .write_text (
162168 profile .read_text (encoding = "utf-8" ) + "\n # local experiment\n " ,
163169 encoding = "utf-8" ,
164170 )
165171 self .assertNotEqual (first , plan_hash ())
166172
173+ def test_scheduler_commands_reject_a_missing_project_root (self ):
174+ with tempfile .TemporaryDirectory () as tmpdir :
175+ missing = Path (tmpdir ) / "no-such-project"
176+ for command in ("status" , "stop" ):
177+ stdout = io .StringIO ()
178+ with contextlib .redirect_stdout (stdout ):
179+ result = main (["--machine" , command , "--root" , str (missing )])
180+ envelope = json .loads (stdout .getvalue ())
181+ self .assertNotEqual (0 , result )
182+ self .assertEqual ("failed" , envelope ["status" ])
183+ self .assertIn ("does not exist" , envelope ["error" ]["message" ])
184+ self .assertFalse (missing .exists ())
185+
167186 def test_detached_scheduler_has_owned_start_status_stop_lifecycle (self ):
168187 with tempfile .TemporaryDirectory () as tmpdir :
169188 root = Path (tmpdir )
@@ -764,6 +783,7 @@ def test_init_is_idempotent_and_protects_modified_assets(self):
764783 second = self .workload .initialize_project (root )
765784 self .assertGreater (first ["assets" ]["created" ], 0 )
766785 self .assertGreater (second ["assets" ]["unchanged" ], 0 )
786+ self .assertEqual ([], sorted (root .rglob ("__pycache__" )))
767787 self .assertTrue ((root / "schema" / "pg_workload-v1.schema.json" ).is_file ())
768788 manifest = root / "data" / "simple_stock" / "profile.yml"
769789 manifest .write_text (manifest .read_text (encoding = "utf-8" ) + "# local edit\n " , encoding = "utf-8" )
@@ -1379,8 +1399,30 @@ def test_running_scheduler_picks_up_new_profile_from_state_without_restart(self)
13791399 stderr = subprocess .PIPE ,
13801400 text = True ,
13811401 )
1402+ lines = []
1403+ events = queue .Queue ()
1404+
1405+ def read_stdout ():
1406+ for line in proc .stdout :
1407+ lines .append (line )
1408+ events .put (line )
1409+
1410+ reader = threading .Thread (target = read_stdout , daemon = True )
1411+ reader .start ()
1412+
1413+ def wait_for (text ):
1414+ deadline = time .monotonic () + 15
1415+ while time .monotonic () < deadline :
1416+ try :
1417+ line = events .get (timeout = max (0.01 , deadline - time .monotonic ()))
1418+ except queue .Empty :
1419+ break
1420+ if text in line :
1421+ return
1422+ self .fail (f"Timed out waiting for { text !r} : { '' .join (lines )} " )
1423+
13821424 try :
1383- time . sleep ( 0.2 )
1425+ wait_for ( "Scheduler started." )
13841426 self .create_minimal_profile (root , "dynamic_profile" )
13851427 self .workload .update_job_state (
13861428 root / "state" / "workloads.yml" ,
@@ -1389,10 +1431,14 @@ def test_running_scheduler_picks_up_new_profile_from_state_without_restart(self)
13891431 enabled = True ,
13901432 interval = 60 ,
13911433 )
1392- time .sleep (0.5 )
1434+ wait_for ("Running dynamic_profile:main" )
1435+ wait_for ("pgbench" )
13931436 finally :
13941437 proc .terminate ()
1395- stdout , stderr = proc .communicate (timeout = 10 )
1438+ proc .wait (timeout = 10 )
1439+ reader .join (timeout = 5 )
1440+ stdout = "" .join (lines )
1441+ stderr = proc .stderr .read ()
13961442
13971443 self .assertIn ("Scheduler started." , stdout )
13981444 self .assertIn ("Running dynamic_profile:main" , stdout )
0 commit comments