PATCH: RemoteHost support
Mark Mitchell
mark at codesourcery.com
Fri Jun 10 21:30:44 UTC 2005
This patch adds a new abstraction (called RemoteHost) that can be used
for running programs remotely. (This facility is in contrast to the
QMTest "Target" abstraction, which allows you to run *tests* remotely;
this facility allows a test running on one machine to run a program on
another.)
The basic facilities provided by a RemoteHost are the abilitiies to
run programs remotely, as well as copy files back and forth between
the local and remove hosts. There are presently three incarnations:
* LocalHost, which is the special case of a RemoteHost that happens to
the machine on which QMTest is running
* SSHHost, which is a host accessible by "ssh" or "rsh"
* Simulator, which is a semi-hosted simulator, using the same
filesystem as the local host, but with a simulator used to run
programs.
These facilities are tied into CompilerTable/CompilerTest, so that you
can now set a context variable to run your tests remotely.
For example, we have a PowerPC machine called "knight". I created a
remote host file for "knight" like so:
qmtest create --attribute host_name=knight remote_host ssh_host.SSHHost
> knight
Then, I created a context file like so:
CompilerTable.languages=cplusplus
CompilerTable.cplusplus_kind=GCC
CompilerTable.cplusplus_path=powerpc-none-linux-gnu-g++
CompilerTable.target=knight
Then, "qmtest run" (in an appropriate database, on another machine)
automatically runs the programs created by the cross-compiler on
knight, using SSH.
--
Mark Mitchell
CodeSourcery, LLC
mark at codesourcery.com
2005-06-10 Mark Mitchell <mark at codesourcery.com>
* qm/remote_host.py: New file.
* qm/test/base.py (__extension_bases): Add "remote_host".
(extension_kinds): Derive automatically from __extension_base.
* qm/test/classes/classes.qmc: Add local_host.LocalHost,
ssh_host.SSHHost, ssh_host.RSHHost, and simulator.Simulator.
* qm/test/classes/compiler.py (Compiler.GetExecutableExtension):
New method.
(Compiler.GetObjectExtension): Likewise.
* qm/test/classes/compiler_table.py (CompilerTable.Setup): Fix
typos and thinkos. Set up CompilerTable.target.
* qm/test/classes/compiler_test.py (CompiledExecutable): Remove.
(CompilerTest._GetObjectFileName): Fix typo in comment.
(CompilerTest.Run): Save the compiler.
(CompilerTest._RunExecutable): Use the RemoteHost provided by the
compiler table.
* qm/test/classes/local_host.py: New file.
* qm/test/classes/simulator.py: Likewise.
* qm/test/classes/ssh_host.py: Likewise.
Index: qm/remote_host.py
===================================================================
RCS file: qm/remote_host.py
diff -N qm/remote_host.py
*** /dev/null 1 Jan 1970 00:00:00 -0000
--- qm/remote_host.py 10 Jun 2005 21:21:05 -0000
***************
*** 0 ****
--- 1,159 ----
+ ########################################################################
+ #
+ # File: remote_host.py
+ # Author: Mark Mitchell
+ # Date: 2005-06-03
+ #
+ # Contents:
+ # RemoteHost
+ #
+ # Copyright (c) 2005 by CodeSourcery, LLC. All rights reserved.
+ #
+ ########################################################################
+
+ ########################################################################
+ # Imports
+ #######################################################################
+
+ from qm.executable import RedirectedExecutable
+ from qm.extension import Extension
+ import os.path
+
+ ########################################################################
+ # Classes
+ #######################################################################
+
+ class RemoteHost(Extension):
+ """A 'RemoteHost' is a logical machine.
+
+ Each logical machine has a default directory. When a file is
+ uploaded to or downloaded from the machine, and a relative patch
+ is specified, the patch is relative to the default directory.
+ Similarly, when a program is run on the remote machine, its
+ initial working directory is the default directory.
+
+ The interface presented by 'RemoteHost' is a lowest common
+ denominator. The objective is not to expose all the functionality
+ of any host; rather it is to provide an interface that can be used
+ on many hosts."""
+
+ kind = "remote_host"
+
+ class Executable(RedirectedExecutable):
+ """An 'Executable' is a simple redirected executable.
+
+ The standard error and standard output streams are combined
+ into a single stream.
+
+ The standard input is not closed before
+ invoking the program because SSH hangs if its standard input
+ is closed before it is invoked. For example, running:
+
+ ssh machine echo hi <&-
+
+ will hang with some versions of SSH."""
+
+ def _StderrPipe(self):
+
+ return None
+
+
+
+ def Run(self, arguments, environment = None, timeout = -1):
+ """Run a program on the remote host.
+
+ 'path' -- The name of the program to run, on the remote host.
+ If 'path' is an absolute path or contains no directory
+ separators it is used unmodified; otherwise (i.e., if it is a
+ relative path containing at least one separator) it is
+ interpreted relative to the default directory.
+
+ 'arguments' -- The sequence of arguments that should be passed
+ to the program.
+
+ 'environment' -- If not 'None', a dictionary of pairs of
+ strings to add to the environment of the running program.
+
+ 'timeout' -- The number of seconds the program is permitted
+ to execute. After the 'timeout' expires, the program will be
+ terminated. However, in some cases (such as when using 'rsh')
+ it will be the local side of the connection that is closed.
+ The remote side of the connection may or may not continue to
+ operate, depending on the vagaries of the remote operating
+ system.
+
+ returns -- A pair '(status, output)'. The 'status' is the
+ exit status returned by the program, or 'None' if the exit
+ status is not available. The 'output' is a string giving the
+ combined standard output and standard error output from the
+ program."""
+
+ raise NotImplementedError
+
+
+ def UploadFile(self, local_file, remote_file = None):
+ """Copy 'local_file' to 'remote_file'.
+
+ 'local_file' -- The name of the file on the local machine.
+
+ 'remote_file' -- The name of the file on the remote machine.
+ The 'remote_file' must be a relative path. It is interpreted
+ relative to the default directory. If 'None', the
+ 'remote_file' is placed in the default directory using the
+ basename of the 'local_file'.
+
+ If the 'local_file' and 'remote_file' are the same, then this
+ function succeeds, but takes no action."""
+
+ raise NotImplementedError
+
+
+ def DownloadFile(self, remote_file, local_file):
+ """Copy 'remote_file' to 'local_file'.
+
+ 'remote_file' -- The name of the file on the remote machine.
+ The 'remote_file' must be a relative path. It is interpreted
+ relative to the default directory.
+
+ 'local_file' -- The name of the file on the local machine. If
+ 'None', the 'local_file' is placed in the current directory
+ using the basename of the 'remote_file'.
+
+ If the 'local_file' and 'remote_file' are the same, then this
+ function succeeds, but takes no action."""
+
+ raise NotImplementedError
+
+
+ def UploadAndRun(self, path, arguments, environment = None,
+ timeout = -1):
+ """Run a program on the remote host.
+
+ 'path' -- The name of the program to run, as a path on the
+ local machine.
+
+ 'arguments' -- As for 'Run'.
+
+ 'environment' -- As for 'Run'.
+
+ 'timeout' -- As for 'Run'.
+
+ returns -- As for 'Run'.
+
+ The program is uploaded to the default directory on the remote
+ host."""
+
+ self.UploadFile(path)
+ return self.Run(os.path.join(os.path.curdir,
+ os.path.basename(path)),
+ arguments,
+ environment,
+ timeout)
+
+
+ def DeleteFile(self, remote_file):
+ """Delete the 'remote_file'.
+
+ 'remote_file' -- A relative path to the file to be deleted."""
+
+ raise NotImplementedError
Index: qm/test/base.py
===================================================================
RCS file: /home/qm/Repository/qm/qm/test/base.py,v
retrieving revision 1.98
diff -c -5 -p -r1.98 base.py
*** qm/test/base.py 25 Aug 2004 10:11:38 -0000 1.98
--- qm/test/base.py 10 Jun 2005 21:21:05 -0000
*************** def _result_from_dom(node):
*** 384,425 ****
########################################################################
# variables
########################################################################
- extension_kinds = [ 'database',
- 'label',
- 'resource',
- 'result_reader',
- 'result_stream',
- 'suite',
- 'target',
- 'test', ]
- """Names of different kinds of QMTest extension classes."""
-
- __class_caches = {}
- """A dictionary of loaded class caches.
-
- The keys are the kinds in 'extension_kinds'. The associated value
- is itself a dictionary mapping class names to class objects."""
-
- # Initialize the caches.
- for kind in extension_kinds:
- __class_caches[kind] = {}
-
import qm.test.database
import qm.label
import qm.test.resource
import qm.test.result_reader
import qm.test.result_stream
import qm.test.suite
import qm.test.target
import qm.test.test
__extension_bases = {
'database' : qm.test.database.Database,
'label' : qm.label.Label,
'resource' : qm.test.resource.Resource,
'result_reader' : qm.test.result_reader.ResultReader,
'result_stream' : qm.test.result_stream.ResultStream,
'suite' : qm.test.suite.Suite,
'target' : qm.test.target.Target,
--- 384,407 ----
########################################################################
# variables
########################################################################
import qm.test.database
import qm.label
+ import qm.remote_host
import qm.test.resource
import qm.test.result_reader
import qm.test.result_stream
import qm.test.suite
import qm.test.target
import qm.test.test
__extension_bases = {
'database' : qm.test.database.Database,
'label' : qm.label.Label,
+ 'remote_host' : qm.remote_host.RemoteHost,
'resource' : qm.test.resource.Resource,
'result_reader' : qm.test.result_reader.ResultReader,
'result_stream' : qm.test.result_stream.ResultStream,
'suite' : qm.test.suite.Suite,
'target' : qm.test.target.Target,
*************** __extension_bases = {
*** 427,436 ****
--- 409,432 ----
}
"""A map from extension class kinds to base classes.
An extension class of a particular 'kind' must be derived from
'extension_bases[kind]'."""
+
+ extension_kinds = __extension_bases.keys()
+ """Names of different kinds of QMTest extension classes."""
+ extension_kinds.sort()
+
+ __class_caches = {}
+ """A dictionary of loaded class caches.
+
+ The keys are the kinds in 'extension_kinds'. The associated value
+ is itself a dictionary mapping class names to class objects."""
+
+ # Initialize the caches.
+ for kind in extension_kinds:
+ __class_caches[kind] = {}
########################################################################
# Local Variables:
# mode: python
# indent-tabs-mode: nil
Index: qm/test/classes/classes.qmc
===================================================================
RCS file: /home/qm/Repository/qm/qm/test/classes/classes.qmc,v
retrieving revision 1.17
diff -c -5 -p -r1.17 classes.qmc
*** qm/test/classes/classes.qmc 14 Apr 2005 21:40:01 -0000 1.17
--- qm/test/classes/classes.qmc 10 Jun 2005 21:21:05 -0000
***************
*** 25,30 ****
--- 25,34 ----
<class kind="test" name="python.ExecTest"/>
<class kind="test" name="python.StringExceptionTest"/>
<class kind="label" name="file_label.FileLabel"/>
<class kind="label" name="python_label.PythonLabel"/>
<class kind="suite" name="explicit_suite.ExplicitSuite"/>
+ <class kind="remote_host" name="local_host.LocalHost"/>
+ <class kind="remote_host" name="ssh_host.SSHHost"/>
+ <class kind="remote_host" name="ssh_host.RSHHost"/>
+ <class kind="remote_host" name="simulator.Simulator"/>
</class-directory>
Index: qm/test/classes/compiler.py
===================================================================
RCS file: /home/qm/Repository/qm/qm/test/classes/compiler.py,v
retrieving revision 1.3
diff -c -5 -p -r1.3 compiler.py
*** qm/test/classes/compiler.py 6 Jun 2005 19:00:49 -0000 1.3
--- qm/test/classes/compiler.py 10 Jun 2005 21:21:06 -0000
*************** class Compiler:
*** 269,279 ****
--- 269,303 ----
'ldflags' -- A list of strings indicating options to the
linker, or 'None' if there are no flags."""
self._ldflags = ldflags
+
+ def GetExecutableExtension(self):
+ """Return the extension for executables.
+
+ returns -- The extension (including leading '.', if
+ applicable) for executable files created by this compiler."""
+
+ if sys.platform == "win32":
+ return ".exe"
+ else:
+ return ""
+
+ def GetObjectExtension(self):
+ """Return the extension for object files.
+
+ returns -- The extension (including leading '.', if
+ applicable) for object files created by this compiler."""
+
+ if sys.platform == "win32":
+ return ".obj"
+ else:
+ return ".o"
+
+
def _GetModeSwitches(self, mode):
"""Return the compilation switches for the compilation 'mode'.
'mode' -- The compilation mode (one of 'Compiler.modes').
Index: qm/test/classes/compiler_table.py
===================================================================
RCS file: /home/qm/Repository/qm/qm/test/classes/compiler_table.py,v
retrieving revision 1.2
diff -c -5 -p -r1.2 compiler_table.py
*** qm/test/classes/compiler_table.py 16 Apr 2005 00:06:03 -0000 1.2
--- qm/test/classes/compiler_table.py 10 Jun 2005 21:21:06 -0000
*************** based on context variables provided by t
*** 20,30 ****
--- 20,32 ----
########################################################################
# Imports
########################################################################
import compiler
+ import qm
from qm.test.resource import Resource
+ from local_host import LocalHost
########################################################################
# Classes
########################################################################
*************** class CompilerTable(Resource):
*** 62,77 ****
are run with these options, followed by any test-specific
options. For example, if the user wants to test the compiler
when run with '-O2', the user would put '-O2' in the 'l_options'
context variable.
! The 'CompilerTable' resource provides a context variable called
! 'CompilerTable.compilers' to all tests that depend upon the
! resource. The 'compilers' variable is a map from language names
! to instances of 'Compiler'. Test classes should obtain the
! 'Compiler' to use when compiling source files by using this
! map."""
def SetUp(self, context, result):
# There are no compilers yet.
compilers = {}
--- 64,87 ----
are run with these options, followed by any test-specific
options. For example, if the user wants to test the compiler
when run with '-O2', the user would put '-O2' in the 'l_options'
context variable.
! The 'CompilerTable' resource provides the following context
! variables to all tests that depend upon the resource:
!
! - 'CompilerTable.compilers'
!
! The 'compilers' variable is a map from language names to
! instances of 'Compiler'. Test classes should obtain the
! 'Compiler' to use when compiling source files by using this
! map.
!
! - 'CompilerTable.target'
!
! An instance of 'RemoteHost' that can be used to run compiler
! programs."""
def SetUp(self, context, result):
# There are no compilers yet.
compilers = {}
*************** class CompilerTable(Resource):
*** 83,99 ****
for l in languages:
# Retrieve information from the context.
kind = context["CompilerTable." + l + "_kind"].strip()
path = context["CompilerTable." + l + "_path"].strip()
# Look for (optional) command-line options.
! opts = context.get("CompilerTable." + l + "_options", []).split()
! ldflags = context.get("CompilerTable." + l + "_ldflags", []).split()
# Find the Python class corresponding to this compiler.
compiler_class = compiler.__dict__[kind]
# Instantiate the compiler.
c = compiler_class(path, opts)
c.SetLDFlags(ldflags)
# Store it in the compilers map.
compilers[l] = c
# Make the table available to tests.
! context["CompilerTable.compiler_table"] = compilers
--- 93,132 ----
for l in languages:
# Retrieve information from the context.
kind = context["CompilerTable." + l + "_kind"].strip()
path = context["CompilerTable." + l + "_path"].strip()
# Look for (optional) command-line options.
! opts = context.get("CompilerTable." + l + "_options",
! "").split()
! ldflags = context.get("CompilerTable." + l + "_ldflags",
! "").split()
# Find the Python class corresponding to this compiler.
compiler_class = compiler.__dict__[kind]
# Instantiate the compiler.
c = compiler_class(path, opts)
c.SetLDFlags(ldflags)
# Store it in the compilers map.
compilers[l] = c
# Make the table available to tests.
! context["CompilerTable.compilers"] = compilers
!
! # For backwards compatibility, we recognize this old
! # context variable here.
! interpreter = context.get("CompilerTest.interpreter")
! if interpreter:
! interpreter = interpreter.split()
! arguments = { simulator : interpreter[0],
! simulator_args : interpreter[1:] }
! target = qm.test.classes.Simulator(arguments)
! else:
! target_desc = context.get("CompilerTable.target")
! if target_desc is None:
! target = LocalHost({})
! else:
! f = lambda n: qm.test.base.get_extension_class(n,
! "remote_host",
! None)
! host_class, arguments \
! = qm.extension.parse_descriptor(target_desc, f)
! target = host_class(arguments)
! context["CompilerTable.target"] = target
Index: qm/test/classes/compiler_test.py
===================================================================
RCS file: /home/qm/Repository/qm/qm/test/classes/compiler_test.py,v
retrieving revision 1.1
diff -c -5 -p -r1.1 compiler_test.py
*** qm/test/classes/compiler_test.py 14 Apr 2005 21:40:01 -0000 1.1
--- qm/test/classes/compiler_test.py 10 Jun 2005 21:21:06 -0000
*************** import string
*** 21,44 ****
########################################################################
# Classes
########################################################################
- class CompiledExecutable(TimeoutRedirectedExecutable):
- """A 'CompiledExecutable' is one generated by a compiler."""
-
- def _StdinPipe(self):
- """Return a pipe to which to redirect the standard input.
-
- returns -- A pipe, or 'None' if the standard input should be
- closed in the child."""
-
- # There is no input available for the child.
- return None
-
-
-
class CompilationStep:
"""A single compilation step."""
def __init__(self, mode, files, options, output, diagnostics):
"""Construct a new 'CompilationStep'.
--- 21,30 ----
*************** class CompilerBase:
*** 181,191 ****
# reason for the test to fail.
pass
def _GetObjectFileName(self, source_file_name, object_extension):
! """Return the default object file name for 'soruce_file_name'.
'source_file_name' -- A string giving the name of a source
file.
'object_extension' -- The extension used for object files.
--- 167,177 ----
# reason for the test to fail.
pass
def _GetObjectFileName(self, source_file_name, object_extension):
! """Return the default object file name for 'source_file_name'.
'source_file_name' -- A string giving the name of a source
file.
'object_extension' -- The extension used for object files.
*************** class CompilerTest(Test, CompilerBase):
*** 215,225 ****
modified by this method to indicate outcomes other than
'Result.PASS' or to add annotations."""
# Get the compiler to use for this test.
compiler = self._GetCompiler(context)
!
# If an executable is generated, executable_path will contain
# the generated path.
executable_path = None
# See what we need to run this test.
steps = self._GetCompilationSteps(context)
--- 201,212 ----
modified by this method to indicate outcomes other than
'Result.PASS' or to add annotations."""
# Get the compiler to use for this test.
compiler = self._GetCompiler(context)
! self._compiler = compiler
!
# If an executable is generated, executable_path will contain
# the generated path.
executable_path = None
# See what we need to run this test.
steps = self._GetCompilationSteps(context)
*************** class CompilerTest(Test, CompilerBase):
*** 334,361 ****
'result' -- A 'Result' object. The outcome will be
'Result.PASS' when this method is called. The 'result' may be
modified by this method to indicate outcomes other than
'Result.PASS' or to add annotations."""
- # Create an object representing the executable.
- timeout = context.get("CompilerTest.execution_timeout", -1)
- executable = CompiledExecutable(timeout)
- # Compute the command line for the executable.
- interpreter = context.get("CompilerTest.interpreter")
- if interpreter:
- arguments = interpreter.split() + [path]
- else:
- arguments = [path]
# Compute the result annotation prefix.
prefix = self._GetAnnotationPrefix() + "execution_"
# Record the command line.
! result[prefix + "command"] = \
! "<tt>" + string.join(arguments) + "</tt>"
# Compute the environment.
library_dirs = self._GetLibraryDirectories(context)
if library_dirs:
- environment = os.environ.copy()
# Update LD_LIBRARY_PATH. On IRIX 6, this variable
# goes by other names, so we update them too. It is
# harmless to do this on other systems.
for variable in ['LD_LIBRARY_PATH',
'LD_LIBRARYN32_PATH',
--- 321,339 ----
'result' -- A 'Result' object. The outcome will be
'Result.PASS' when this method is called. The 'result' may be
modified by this method to indicate outcomes other than
'Result.PASS' or to add annotations."""
# Compute the result annotation prefix.
prefix = self._GetAnnotationPrefix() + "execution_"
# Record the command line.
! path = os.path.join(self._GetDirectory(context), path)
! result[prefix + "command"] = "<tt>" + path + "</tt>"
!
# Compute the environment.
library_dirs = self._GetLibraryDirectories(context)
if library_dirs:
# Update LD_LIBRARY_PATH. On IRIX 6, this variable
# goes by other names, so we update them too. It is
# harmless to do this on other systems.
for variable in ['LD_LIBRARY_PATH',
'LD_LIBRARYN32_PATH',
*************** class CompilerTest(Test, CompilerBase):
*** 367,382 ****
environment[variable] = new_path
else:
# Use the default values.
environment = None
! status = executable.Run(arguments,
! environment = environment,
! dir = self._GetDirectory(context))
! # Remember the output streams.
! result[prefix + "stdout"] = result.Quote(executable.stdout)
! result[prefix + "stderr"] = result.Quote(executable.stderr)
# Check the output status.
self._CheckStatus(result, prefix, "Executable", status)
def _CheckOutput(self, context, result, prefix, output, diagnostics):
--- 345,363 ----
environment[variable] = new_path
else:
# Use the default values.
environment = None
! target = context["CompilerTable.target"]
! timeout = context.get("CompilerTest.execution_timeout", -1)
! status, output = target.UploadAndRun(path,
! [],
! environment,
! timeout)
! target.DeleteFile(path)
! # Record the output.
! result[prefix + "output"] = result.Quote(output)
# Check the output status.
self._CheckStatus(result, prefix, "Executable", status)
def _CheckOutput(self, context, result, prefix, output, diagnostics):
Index: qm/test/classes/local_host.py
===================================================================
RCS file: qm/test/classes/local_host.py
diff -N qm/test/classes/local_host.py
*** /dev/null 1 Jan 1970 00:00:00 -0000
--- qm/test/classes/local_host.py 10 Jun 2005 21:21:06 -0000
***************
*** 0 ****
--- 1,75 ----
+ ########################################################################
+ #
+ # File: local_host.py
+ # Author: Mark Mitchell
+ # Date: 2005-06-03
+ #
+ # Contents:
+ # LocalHost
+ #
+ # Copyright (c) 2005 by CodeSourcery, LLC. All rights reserved.
+ #
+ ########################################################################
+
+ ########################################################################
+ # Imports
+ #######################################################################
+
+ import os
+ import os.path
+ from qm.remote_host import RemoteHost
+ import shutil
+
+ ########################################################################
+ # Classes
+ #######################################################################
+
+ class LocalHost(RemoteHost):
+ """A 'LocalHost' is the machine on which Python is running.
+
+ The default directory for a 'LocalHost' is the current working
+ directory for this Python process."""
+
+ def Run(self, path, arguments, environment = None, timeout = -1):
+
+ # Compute the full environment for the child.
+ if environment is not None:
+ new_environment = os.environ.copy()
+ new_environment.update(environment)
+ environment = new_environment
+ executable = self.Executable(timeout)
+ status = executable.Run([path] + arguments, environment)
+ return (status, executable.stdout)
+
+
+ def UploadFile(self, local_file, remote_file = None):
+
+ if remote_file is None:
+ remote_file = os.path.basename(local_file)
+ # Do not copy the files if they are the same.
+ if not self._SameFile(local_file, remote_file):
+ shutil.copy(local_file, remote_file)
+
+
+ def DownloadFile(self, remote_file, local_file = None):
+
+ return self.UploadFile(remote_file, local_file)
+
+
+ def _SameFile(self, file1, file2):
+ """Return true iff 'file1' and 'file2' are the same file.
+
+ returns -- True iff 'file1' and 'file2' are the same file,
+ even if they have different names."""
+
+ if not os.path.exists(file1) or not os.path.exists(file2):
+ return False
+ if hasattr(os.path, "samefile"):
+ return os.path.samefile(file1, file2)
+ return (os.path.normcase(os.path.abspath(file1))
+ == os.path.normcase(os.path.abspath(file2)))
+
+
+ def DeleteFile(self, remote_file):
+
+ os.remove(remote_file)
Index: qm/test/classes/simulator.py
===================================================================
RCS file: qm/test/classes/simulator.py
diff -N qm/test/classes/simulator.py
*** /dev/null 1 Jan 1970 00:00:00 -0000
--- qm/test/classes/simulator.py 10 Jun 2005 21:21:06 -0000
***************
*** 0 ****
--- 1,45 ----
+ ########################################################################
+ #
+ # File: simulator.py
+ # Author: Mark Mitchell
+ # Date: 2005-06-03
+ #
+ # Contents:
+ # Simulator
+ #
+ # Copyright (c) 2005 by CodeSourcery, LLC. All rights reserved.
+ #
+ ########################################################################
+
+ ########################################################################
+ # Imports
+ #######################################################################
+
+ from local_host import LocalHost
+ from qm.fields import TextField, SetField
+
+ ########################################################################
+ # Classes
+ #######################################################################
+
+ class Simulator(LocalHost):
+ """A 'Simulator' is a semi-hosted simulation environment.
+
+ The local file system is shared with the simulated machine. A
+ simulator is used to execute programs."""
+
+ simulator = TextField(
+ description = """The simulation program."""
+ )
+
+ # Any arguments that must be provided to the simulator.
+ simulator_args = SetField(
+ TextField(description = """Arguments to the simulation program."""))
+
+ def Run(self, path, arguments, environment = None, timeout = -1):
+
+ arguments = self.simulator_args + [path] + arguments
+ return super(Simulator, self.Run(self.simulator,
+ arguments,
+ environment,
+ timeout))
Index: qm/test/classes/ssh_host.py
===================================================================
RCS file: qm/test/classes/ssh_host.py
diff -N qm/test/classes/ssh_host.py
*** /dev/null 1 Jan 1970 00:00:00 -0000
--- qm/test/classes/ssh_host.py 10 Jun 2005 21:21:06 -0000
***************
*** 0 ****
--- 1,211 ----
+ ########################################################################
+ #
+ # File: ssh_host.py
+ # Author: Mark Mitchell
+ # Date: 2005-06-03
+ #
+ # Contents:
+ # SSHHost, RSHHost
+ #
+ # Copyright (c) 2005 by CodeSourcery, LLC. All rights reserved.
+ #
+ ########################################################################
+
+ ########################################################################
+ # Imports
+ #######################################################################
+
+ from local_host import LocalHost
+ import os
+ import os.path
+ from qm.fields import TextField, SetField
+ import qm.common
+ import sys
+
+ ########################################################################
+ # Classes
+ #######################################################################
+
+ class SSHHost(LocalHost):
+ """An 'SSHHost' is accessible via 'ssh' or a similar program."""
+
+ # If not empty, the name of the remote host.
+ host_name = TextField()
+ # The path to "ssh".
+ ssh_program = TextField(
+ default_value = "ssh",
+ description = """The path to the remote shell program."""
+ )
+ # Any arguments that must be provided to "ssh".
+ ssh_args = SetField(
+ TextField(description =
+ """The arguments to the remote shell program."""))
+ # The path to "scp".
+ scp_program = TextField(
+ default_value = "scp",
+ description = """The path to the remote copy program."""
+ )
+ # Any arguments that must be provided to "scp".
+ scp_args = SetField(
+ TextField(description =
+ """The arguments to the remote copy program."""))
+ # The default directory on the remote system.
+ default_dir = TextField(
+ description = """The default directory on the remote system."""
+ )
+
+ nfs_dir = TextField(
+ description = """The default directory, as seen from the local host.
+
+ If not empty, 'nfs_dir' is a directory on the local machine
+ that is equivalent to the default directory on the remote
+ machine. In that case, files will be copied to and from this
+ directory on the local machine, rather than by using
+ 'scp'."""
+ )
+
+ user_name = TextField(
+ description = """The user name on the remote host.
+
+ If not empty, the user name that should be used when
+ connecting to the remote host."""
+ )
+
+ def Run(self, path, arguments, environment = None, timeout = -1):
+
+ if self.default_dir and not os.path.isabs(path):
+ if (path.find(os.path.sep) != -1
+ or (os.path.altsep
+ and path.find(os.path.altsep) != -1)):
+ path = os.path.join(self.default_dir, path)
+ path, arguments = self._FormSSHCommandLine(path, arguments,
+ environment)
+ return super(SSHHost, self).Run(path, arguments, None, timeout)
+
+
+ def UploadFile(self, local_file, remote_file = None):
+
+ if remote_file is None:
+ remote_file = os.path.basename(local_file)
+ if self.nfs_dir:
+ remote_file = os.path.join(self.nfs_dir, remote_file)
+ super(SSHHost, self).UploadFile(local_file, remote_file)
+ else:
+ if self.default_dir:
+ remote_file = os.path.join(self.default_dir, remote_file)
+ command = self._FormSCPCommandLine(True, local_file,
+ remote_file)
+ executable = self.Executable()
+ status = executable.Run(command)
+ if ((sys.platform != "win32"
+ and (not os.WIFEXITED(status)
+ or os.WEXITSTATUS(status) != 0))
+ or (sys.platform == "win32" and status != 0)):
+ raise qm.common.QMException("could not upload file")
+
+
+ def DownloadFile(self, remote_file, local_file = None):
+
+ if local_file is None:
+ local_file = os.path.basename(remote_file)
+ if self.nfs_dir:
+ remote_file = os.path.join(self.nfs_dir, remote_file)
+ super(SSHHost, self).DownloadFile(remote_file, local_file)
+ else:
+ if self.default_dir:
+ remote_file = os.path.join(self.default_dir, remote_file)
+ command = self._FormSCPCommandLine(False, local_file,
+ remote_file)
+ executable = self.Executable()
+ executable.Run(command)
+
+
+ def DeleteFile(self, remote_file):
+
+ if self.default_dir:
+ remote_file = os.path.join(self.default_dir, remote_file)
+ return self.Run("rm", [remote_file])
+
+
+ def _FormSSHCommandLine(self, path, arguments, environment = None):
+ """Form the 'ssh' command line.
+
+ 'path' -- The remote command, in the same format expected by
+ 'Run'.
+
+ 'arguments' -- The arguments to the remote command.
+
+ 'environment' -- As for 'Run'.
+
+ returns -- A pair '(path, arguments)' describing the command
+ to run on the local machine that will execute the remote
+ command."""
+
+ command = self.ssh_args + [self.host_name]
+ if self.user_name:
+ command += ["-l", self.user_name]
+ if environment is not None:
+ command.append("env")
+ for (k, v) in environment.iteritems():
+ command.append("%s=%s" % (k, v))
+ command.append(path)
+ command += arguments
+
+ return self.ssh_program, command
+
+
+ def _FormSCPCommandLine(self, upload, local_file, remote_file):
+ """Form the 'scp' command line.
+
+ 'upload' -- True iff the 'local_file' should be copied to the
+ remote host.
+
+ 'local_file' -- The path to the local file.
+
+ 'remote_file' -- The path to the remote file.
+
+ returns -- The list of arguments for a command to run on the
+ local machine that will perform the file copy."""
+
+ if self.default_dir:
+ remote_file = os.path.join(self.default_dir, remote_file)
+ remote_file = self.host_name + ":" + remote_file
+ if self.user_name:
+ remote_file = self.user_name + "@" + remote_file
+ command = [self.scp_program] + self.scp_args
+ if upload:
+ command += [local_file, remote_file]
+ else:
+ command += [remote_file, local_file]
+
+ return command
+
+
+
+ class RSHHost(SSHHost):
+ """An 'RSHHost' is an 'SSHHost' that uses 'rsh' instead of 'ssh'.
+
+ The reason that 'RSHHost' is a separate class is that (a) that
+ makes it easier for users to construct an 'SSHHost', and (b) 'rsh'
+ does not return the exit code of the remote program, so 'Run'
+ requires adjustment."""
+
+ # Override the default values.
+ ssh_program = TextField(
+ default_value = "rsh",
+ description = """The path to the remote shell program."""
+ )
+ scp_program = TextField(
+ default_value = "rcp",
+ description = """The path to the remote copy program."""
+ )
+
+ def Run(self, path, arguments, environment = None, timeout = -1):
+
+ status, output = \
+ super(RSHHost, self).Run(path, arguments,
+ environment, timeout)
+ # The exit status of 'rsh' is not the exit status of the
+ # remote program. The exit status of the remote program is
+ # unavailable.
+ return (None, output)
More information about the qmtest
mailing list