55import shutil
66import signal
77import subprocess
8+ import sys
89import time
910
1011import pytest
1314from .utils import setup_wallet
1415
1516
17+ def wait_for_node_start (process , pattern , timestamp : int = None ):
18+ for line in process .stdout :
19+ print (line .strip ())
20+ # 20 min as timeout
21+ timestamp = timestamp or int (time .time ())
22+ if int (time .time ()) - timestamp > 20 * 60 :
23+ pytest .fail ("Subtensor not started in time" )
24+ if pattern .search (line ):
25+ print ("Node started!" )
26+ break
27+
28+
1629# Fixture for setting up and tearing down a localnet.sh chain between tests
1730@pytest .fixture (scope = "function" )
1831def local_chain (request ):
32+ """Determines whether to run the localnet.sh script in a subprocess or a Docker container."""
33+ args = request .param if hasattr (request , "param" ) else None
34+ params = "" if args is None else f"{ args } "
35+ if shutil .which ("docker" ) and not os .getenv ("USE_DOCKER" ) == "0" :
36+ yield from docker_runner (params )
37+ else :
38+ if not os .getenv ("USE_DOCKER" ) == "0" :
39+ if sys .platform .startswith ("linux" ):
40+ docker_command = (
41+ "Install docker with command "
42+ "[blue]sudo apt-get update && sudo apt-get install docker.io -y[/blue]"
43+ " or use documentation [blue]https://docs.docker.com/engine/install/[/blue]"
44+ )
45+ elif sys .platform == "darwin" :
46+ docker_command = (
47+ "Install docker with command [blue]brew install docker[/blue]"
48+ )
49+ else :
50+ docker_command = "[blue]Unknown OS, install Docker manually: https://docs.docker.com/get-docker/[/blue]"
51+
52+ logging .warning ("Docker not found in the operating system!" )
53+ logging .warning (docker_command )
54+ logging .warning ("Tests are run in legacy mode." )
55+ yield from legacy_runner (request )
56+
57+
58+ def legacy_runner (request ):
1959 param = request .param if hasattr (request , "param" ) else None
2060 # Get the environment variable for the script path
2161 script_path = os .getenv ("LOCALNET_SH_PATH" )
@@ -41,18 +81,6 @@ def local_chain(request):
4181 # Install neuron templates
4282 logging .info ("Downloading and installing neuron templates from github" )
4383
44- timestamp = int (time .time ())
45-
46- def wait_for_node_start (process , pattern ):
47- for line in process .stdout :
48- print (line .strip ())
49- # 20 min as timeout
50- if int (time .time ()) - timestamp > 20 * 60 :
51- pytest .fail ("Subtensor not started in time" )
52- if pattern .search (line ):
53- print ("Node started!" )
54- break
55-
5684 wait_for_node_start (process , pattern )
5785
5886 # Run the test, passing in substrate interface
@@ -72,6 +100,108 @@ def wait_for_node_start(process, pattern):
72100 process .wait ()
73101
74102
103+ def docker_runner (params ):
104+ """Starts a Docker container before tests and gracefully terminates it after."""
105+
106+ def is_docker_running ():
107+ """Check if Docker has been run."""
108+ try :
109+ subprocess .run (
110+ ["docker" , "info" ],
111+ stdout = subprocess .DEVNULL ,
112+ stderr = subprocess .DEVNULL ,
113+ check = True ,
114+ )
115+ return True
116+ except subprocess .CalledProcessError :
117+ return False
118+
119+ def try_start_docker ():
120+ """Run docker based on OS."""
121+ try :
122+ subprocess .run (["open" , "-a" , "Docker" ], check = True ) # macOS
123+ except (FileNotFoundError , subprocess .CalledProcessError ):
124+ try :
125+ subprocess .run (["systemctl" , "start" , "docker" ], check = True ) # Linux
126+ except (FileNotFoundError , subprocess .CalledProcessError ):
127+ try :
128+ subprocess .run (
129+ ["sudo" , "service" , "docker" , "start" ], check = True
130+ ) # Linux alternative
131+ except (FileNotFoundError , subprocess .CalledProcessError ):
132+ print ("Failed to start Docker. Manual start may be required." )
133+ return False
134+
135+ # Wait Docker run 10 attempts with 3 sec waits
136+ for _ in range (10 ):
137+ if is_docker_running ():
138+ return True
139+ time .sleep (3 )
140+
141+ print ("Docker wasn't run. Manual start may be required." )
142+ return False
143+
144+ container_name = f"test_local_chain_{ str (time .time ()).replace ('.' , '_' )} "
145+ image_name = "ghcr.io/opentensor/subtensor-localnet:latest"
146+
147+ # Command to start container
148+ cmds = [
149+ "docker" ,
150+ "run" ,
151+ "--rm" ,
152+ "--name" ,
153+ container_name ,
154+ "-p" ,
155+ "9944:9944" ,
156+ "-p" ,
157+ "9945:9945" ,
158+ image_name ,
159+ params ,
160+ ]
161+
162+ try_start_docker ()
163+
164+ # Start container
165+ with subprocess .Popen (
166+ cmds ,
167+ stdout = subprocess .PIPE ,
168+ stderr = subprocess .PIPE ,
169+ text = True ,
170+ start_new_session = True ,
171+ ) as process :
172+ try :
173+ substrate = None
174+ try :
175+ pattern = re .compile (r"Imported #1" )
176+ wait_for_node_start (process , pattern , int (time .time ()))
177+ except TimeoutError :
178+ raise
179+
180+ result = subprocess .run (
181+ ["docker" , "ps" , "-q" , "-f" , f"name={ container_name } " ],
182+ capture_output = True ,
183+ text = True ,
184+ )
185+ if not result .stdout .strip ():
186+ raise RuntimeError ("Docker container failed to start." )
187+
188+ substrate = AsyncSubstrateInterface (url = "ws://127.0.0.1:9944" )
189+ yield substrate
190+
191+ finally :
192+ try :
193+ if substrate :
194+ substrate .close ()
195+ except Exception :
196+ pass
197+
198+ try :
199+ subprocess .run (["docker" , "kill" , container_name ])
200+ process .wait ()
201+ except subprocess .TimeoutExpired :
202+ os .killpg (os .getpgid (process .pid ), signal .SIGKILL )
203+
204+
75205@pytest .fixture (scope = "function" )
76206def wallet_setup ():
77207 wallet_paths = []
0 commit comments