Package qm :: Package test :: Package classes :: Module process_target
[hide private]
[frames] | no frames]

Source Code for Module qm.test.classes.process_target

  1  ######################################################################## 
  2  # 
  3  # File:   process_target.py 
  4  # Author: Mark Mitchell 
  5  # Date:   07/24/2002 
  6  # 
  7  # Contents: 
  8  #   ProcessTarget 
  9  # 
 10  # Copyright (c) 2002, 2003 by CodeSourcery, LLC.  All rights reserved.  
 11  # 
 12  ######################################################################## 
 13   
 14  ######################################################################## 
 15  # Imports 
 16  ######################################################################## 
 17   
 18  import cPickle 
 19  import os 
 20  import qm.executable 
 21  import qm.test.cmdline 
 22  from   qm.test.target import * 
 23   
 24  ######################################################################## 
 25  # Classes 
 26  ######################################################################## 
 27   
28 -class ProcessTarget(Target):
29 """A 'ProcessTarget' runs tests in child processes.""" 30 31 arguments = [ 32 qm.fields.IntegerField( 33 name="processes", 34 title="Number of Processes", 35 description="""The number of processes to devote to running tests. 36 37 A positive integer that indicates the number of processes to 38 use when running tests. Larger numbers will allow more 39 tests to be run at once. You can experiment with this 40 value to find the number that results in the fastest 41 execution.""", 42 default_value=1), 43 qm.fields.TextField( 44 name="database_path", 45 title="Database Path", 46 description="""The path to the test database. 47 48 A string giving the directory containing the test 49 database. If this value is the empty string, QMTest uses 50 the path provided on the command line.""", 51 default_value=""), 52 qm.fields.TextField( 53 name="qmtest", 54 title="QMTest Path", 55 description="""The path to the QMTest executable. 56 57 A string giving the file name of the 'qmtest' executable 58 program. This path is used to invoke QMTest.""", 59 default_value=""), 60 ] 61
62 - class QMTestExecutable(qm.executable.Executable):
63 """A 'QMTestExecutable' redirects commands to a child process.""" 64
65 - def _InitializeParent(self):
66 67 self.command_pipe = os.pipe() 68 self.response_pipe = os.pipe()
69 70
71 - def _InitializeChild(self):
72 73 # Close the write end of the command pipe. 74 os.close(self.command_pipe[1]) 75 # And the read end of the response pipe. 76 os.close(self.response_pipe[0]) 77 # Connect the pipes to the standard input and standard 78 # output for the child. 79 os.dup2(self.command_pipe[0], sys.stdin.fileno()) 80 os.dup2(self.response_pipe[1], sys.stdout.fileno())
81 82 83
84 - def __init__(self, database, properties):
85 """Construct a new 'ProcessTarget'. 86 87 'database' -- The 'Database' containing the tests that will be 88 run. 89 90 'properties' -- A dictionary mapping strings (property names) 91 to strings (property values).""" 92 93 # Initialize the base class. 94 Target.__init__(self, database, properties)
95 96
97 - def IsIdle(self):
98 """Return true if the target is idle. 99 100 returns -- True if the target is idle. If the target is idle, 101 additional tasks may be assigned to it.""" 102 103 return self.__idle_children
104 105
106 - def Start(self, response_queue, engine=None):
107 """Start the target. 108 109 'response_queue' -- The 'Queue' in which the results of test 110 executions are placed. 111 112 'engine' -- The 'ExecutionEngine' that is starting the target, 113 or 'None' if this target is being started without an 114 'ExecutionEngine'.""" 115 116 Target.Start(self, response_queue, engine) 117 118 # There are no children yet. 119 self.__children = [] 120 self.__idle_children = [] 121 self.__busy_children = [] 122 self.__children_by_fd = {} 123 124 # Determine the test database path to use. 125 database_path = self.database_path 126 if not database_path: 127 database_path = self.GetDatabase().GetPath() 128 # See if the path to the QMTest binary was set in the 129 # target configuration. 130 qmtest_path = self.qmtest 131 if not qmtest_path: 132 # If not, fall back to the value determined when 133 # QMTest was invoked. 134 qmtest_path \ 135 = qm.test.cmdline.get_qmtest().GetExecutablePath() 136 # If there is no such value, use a default value. 137 if not qmtest_path: 138 qmtest_path = "/usr/local/bin/qmtest" 139 # Construct the command we want to invoke. 140 arg_list = (self._GetInterpreter() + 141 [ qmtest_path, '-D', database_path, "remote" ]) 142 143 # Create the subprocesses. 144 for x in xrange(self.processes): 145 # Create two pipes: one to write commands to the remote 146 # QMTest, and one to read responses. 147 e = ProcessTarget.QMTestExecutable() 148 child_pid = e.Spawn(arg_list) 149 150 # Close the read end of the command pipe. 151 os.close(e.command_pipe[0]) 152 # And the write end of the response pipe. 153 os.close(e.response_pipe[1]) 154 155 # Remember the child. 156 child = (child_pid, 157 os.fdopen(e.response_pipe[0], "r"), 158 os.fdopen(e.command_pipe[1], "w", 0)) 159 self.__children.append(child) 160 self.__idle_children.append(child) 161 self.__children_by_fd[e.response_pipe[0]] = child 162 engine.AddInputHandler(e.response_pipe[0], self.__ReadResults)
163 164
165 - def Stop(self):
166 """Stop the target. 167 168 postconditions -- The target may no longer be used.""" 169 170 # Stop the children. 171 for child in self.__children: 172 try: 173 cPickle.dump("Stop", child[2]) 174 child[2].close() 175 except: 176 pass 177 # Read any remaining results. 178 while self.__busy_children: 179 self.__ReadResults(self.__busy_children[0][1].fileno()) 180 # Wait for the children to terminate. 181 while self.__children: 182 child = self.__children.pop() 183 os.waitpid(child[0], 0) 184 185 Target.Stop(self)
186 187
188 - def RunTest(self, descriptor, context):
189 """Run the test given by 'test_id'. 190 191 'descriptor' -- The 'TestDescriptor' for the test. 192 193 'context' -- The 'Context' in which to run the test.""" 194 195 # Use the child process at the head of the list. 196 child = self.__idle_children.pop(0) 197 self.__busy_children.append(child) 198 # Write the test to the file. 199 try: 200 cPickle.dump(("RunTest", descriptor.GetId(), context), 201 child[2]) 202 except: 203 # We could not write to the child. (One situation in 204 # which this happens is that the child process has been 205 # killed.) 206 result = Result(Result.TEST, descriptor.GetId()) 207 result.NoteException() 208 self._RecordResult(result) 209 self.__idle_children.append(child)
210 211
212 - def _GetInterpreter(self):
213 """Return the interpreter to use. 214 215 returns -- A list giving the path to an interpreter, and 216 arguments to provide the interpreter. This interpreter is 217 used to run QMTest. If '[]' is returned, then no intepreter 218 is used.""" 219 220 return []
221 222
223 - def __ReadResults(self, fd):
224 """Read results from one of the children. 225 226 'fd' -- The descriptor from which the results should be read.""" 227 228 child = self.__children_by_fd[fd] 229 try: 230 results = cPickle.load(child[1]) 231 idle = None 232 for result in results: 233 self._RecordResult(result) 234 if not idle and result.GetKind() == Result.TEST: 235 self.__idle_children.append(child) 236 self.__busy_children.remove(child) 237 idle = 1 238 except EOFError: 239 self.__idle_children.append(child) 240 if child in self.__busy_children: 241 self.__busy_children.remove(child)
242