class RegressionTest: """Base class for regression tests. All regression tests must eventually inherit from this class. This class provides the implementation of the pipeline phases that the regression test goes through during its lifetime. :arg name: The name of the test. This is the only argument that the users may specify freely. :arg prefix: The directory prefix of the test. You should initialize this to the directory containing the file that defines the regression test. You can achieve this by always passing ``os.path.dirname(__file__)``. :arg system: The system that this regression test will run on. The framework takes care of initializing and passing correctly this argument. :arg resources: An object managing the framework's resources. The framework takes care of initializing and passing correctly this argument. Concrete regression test subclasses should call the base constructor as follows: :: class MyTest(RegressionTest): def __init__(self, my_test_args, **kwargs): super().__init__('mytest', os.path.dirname(__file__), **kwargs) """ #: The name of the test. #: #: :type: Alphanumeric string. name = fields.AlphanumericField('name') #: List of programming environments supported by this test. #: #: :type: :class:`list[str]` #: :default: ``[]`` #: #: .. note:: #: .. versionchanged:: 2.12 #: Programming environments can now be specified using wildcards. valid_prog_environs = fields.TypedListField('valid_prog_environs', str) #: List of systems supported by this test. #: The general syntax for systems is ``<sysname>[:<partname]``. #: #: :type: :class:`list[str]` #: :default: ``[]`` valid_systems = fields.TypedListField('valid_systems', str) #: A detailed description of the test. #: #: :type: :class:`str` #: :default: ``self.name`` descr = fields.StringField('descr') #: The path to the source file or source directory of the test. #: #: It must be a path relative to the :attr:`sourcesdir`, pointing to a #: subfolder or a file contained in :attr:`sourcesdir`. This applies also #: in the case where :attr:`sourcesdir` is a Git repository. #: #: If it refers to a regular file, this file will be compiled (its language #: will be automatically recognized). #: If it refers to a directory, ``make`` will be invoked in that directory. #: #: :type: :class:`str` #: :default: ``''`` sourcepath = fields.StringField('sourcepath') #: The directory containing the test's resources. #: #: This directory may be specified with an absolute path or with a path #: relative to the location of the test. Its contents will always be copied #: to the stage directory of the test. #: #: This attribute may also accept a URL, in which case ReFrame will treat it #: as a Git repository and will try to clone its contents in the stage #: directory of the test. #: #: If set to :class:`None`, the test has no resources an no action is taken. #: #: :type: :class:`str` or :class:`None` #: :default: ``'src'`` #: #: .. note:: #: .. versionchanged:: 2.9 #: Allow :class:`None` values to be set also in regression tests #: with a compilation phase #: #: .. versionchanged:: 2.10 #: Support for Git repositories was added. sourcesdir = fields.StringField('sourcesdir', allow_none=True) #: List of shell commands to be executed before compiling. #: #: These commands are executed during the compilation phase and from #: inside the stage directory. **Each entry in the list spawns a new shell.** #: #: :type: :class:`list[str]` #: :default: ``[]`` prebuild_cmd = fields.TypedListField('prebuild_cmd', str) #: List of shell commands to be executed after a successful compilation. #: #: These commands are executed during the compilation phase and from inside #: the stage directory. **Each entry in the list spawns a new shell.** #: #: :type: :class:`list[str]` #: :default: ``[]`` postbuild_cmd = fields.TypedListField('postbuild_cmd', str) #: The name of the executable to be launched during the run phase. #: #: :type: :class:`str` #: :default: ``os.path.join('.', self.name)`` executable = fields.StringField('executable') #: List of options to be passed to the :attr:`executable`. #: #: :type: :class:`list[str]` #: :default: ``[]`` executable_opts = fields.TypedListField('executable_opts', str) #: List of shell commands to execute before launching this job. #: #: These commands do not execute in the context of ReFrame. #: Instead, they are emitted in the generated job script just before the #: actual job launch command. #: #: :type: :class:`list` of :class:`str` #: :default: ``[]`` #: #: .. note:: #: .. versionadded:: 2.10 pre_run = fields.TypedListField('pre_run', str) #: List of shell commands to execute after launching this job. #: #: See :attr:`pre_run` for a more detailed description of the semantics. #: #: :type: :class:`list` of :class:`str` #: :default: ``[]`` #: #: .. note:: #: .. versionadded:: 2.10 post_run = fields.TypedListField('post_run', str) #: List of files to be kept after the test finishes. #: #: By default, the framework saves the standard output, the standard error #: and the generated shell script that was used to run this test. #: #: These files will be copied over to the framework’s output directory #: during the :func:`cleanup` phase. #: #: Directories are also accepted in this field. #: #: Relative path names are resolved against the stage directory. #: #: :type: :class:`list[str]` #: :default: ``[]`` keep_files = fields.TypedListField('keep_files', str) #: List of files or directories (relative to the :attr:`sourcesdir`) that #: will be symlinked in the stage directory and not copied. #: #: You can use this variable to avoid copying very large files to the stage #: directory. #: #: :type: :class:`list[str]` #: :default: ``[]`` readonly_files = fields.TypedListField('readonly_files', str) #: Set of tags associated with this test. #: #: This test can be selected from the frontend using any of these tags. #: #: :type: :class:`set[str]` #: :default: an empty set tags = fields.TypedSetField('tags', str) #: List of people responsible for this test. #: #: When the test fails, this contact list will be printed out. #: #: :type: :class:`list[str]` #: :default: ``[]`` maintainers = fields.TypedListField('maintainers', str) #: Mark this test as a strict performance test. #: #: If a test is marked as non-strict, the performance checking phase will #: always succeed, unless the ``--strict`` command-line option is passed #: when invoking ReFrame. #: #: :type: boolean #: :default: :class:`True` strict_check = fields.BooleanField('strict_check') #: Number of tasks required by this test. #: #: If the number of tasks is set to ``0``, ReFrame will try to use all #: the available nodes of a reservation. A reservation *must* be specified #: through the `--reservation` command-line option, otherwise the #: regression test will fail during submission. ReFrame will try to run the #: test on all the nodes of the reservation that satisfy the selection #: criteria of the current #: `virtual partition <configure.html#partition-configuration>`__ #: (i.e., constraints and/or partitions). #: #: :type: integral #: :default: ``1`` #: #: .. note:: #: .. versionchanged:: 2.9 #: Added support for running the test using all the nodes of the #: specified reservation if the number of tasks is set to ``0``. num_tasks = fields.IntegerField('num_tasks') #: Number of tasks per node required by this test. #: #: Ignored if :class:`None`. #: #: :type: integral or :class:`None` #: :default: :class:`None` num_tasks_per_node = fields.IntegerField('num_tasks_per_node', allow_none=True) #: Number of GPUs per node required by this test. #: #: :type: integral #: :default: ``0`` num_gpus_per_node = fields.IntegerField('num_gpus_per_node') #: Number of CPUs per task required by this test. #: #: Ignored if :class:`None`. #: #: :type: integral or :class:`None` #: :default: :class:`None` num_cpus_per_task = fields.IntegerField('num_cpus_per_task', allow_none=True) #: Number of tasks per core required by this test. #: #: Ignored if :class:`None`. #: #: :type: integral or :class:`None` #: :default: :class:`None` num_tasks_per_core = fields.IntegerField('num_tasks_per_core', allow_none=True) #: Number of tasks per socket required by this test. #: #: Ignored if :class:`None`. #: #: :type: integral or :class:`None` #: :default: :class:`None` num_tasks_per_socket = fields.IntegerField('num_tasks_per_socket', allow_none=True) #: Specify whether this tests needs simultaneous multithreading enabled. #: #: Ignored if :class:`None`. #: #: :type: boolean or :class:`None` #: :default: :class:`None` use_multithreading = fields.BooleanField('use_multithreading', allow_none=True) #: Specify whether this test needs exclusive access to nodes. #: #: :type: boolean #: :default: :class:`False` exclusive_access = fields.BooleanField('exclusive_access') #: Always execute this test locally. #: #: :type: boolean #: :default: :class:`False` local = fields.BooleanField('local') #: The set of reference values for this test. #: #: Refer to the :doc:`ReFrame Tutorial </tutorial>` for concrete usage #: examples. #: #: :type: A scoped dictionary with system names as scopes or :class:`None` #: :default: ``{}`` reference = fields.ScopedDictField('reference', (tuple, object)) # FIXME: There is not way currently to express tuples of `float`s or # `None`s, so we just use the very generic `object` #: #: Refer to the :doc:`ReFrame Tutorial </tutorial>` for concrete usage #: examples. #: #: If set to :class:`None`, a sanity error will be raised during sanity #: checking. #: #: :type: A deferrable expression (i.e., the result of a :doc:`sanity #: function </sanity_functions_reference>`) or :class:`None` #: :default: :class:`None` #: #: .. note:: #: .. versionchanged:: 2.9 #: The default behaviour has changed and it is now considered a #: sanity failure if this attribute is set to :class:`None`. #: #: If a test doesn't care about its output, this must be stated #: explicitly as follows: #: #: :: #: #: self.sanity_patterns = sn.assert_found(r'.*', self.stdout) #: sanity_patterns = fields.TypedField('sanity_patterns', _DeferredExpression, allow_none=True) #: Patterns for verifying the performance of this test. #: #: Refer to the :doc:`ReFrame Tutorial </tutorial>` for concrete usage #: examples. #: #: If set to :class:`None`, no performance checking will be performed. #: #: :type: A dictionary with keys of type :class:`str` and deferrable #: expressions (i.e., the result of a :doc:`sanity function #: </sanity_functions_reference>`) as values. #: :class:`None` is also allowed. #: :default: :class:`None` perf_patterns = fields.TypedDictField('perf_patterns', str, _DeferredExpression, allow_none=True) #: List of modules to be loaded before running this test. #: #: These modules will be loaded during the :func:`setup` phase. #: #: :type: :class:`list[str]` #: :default: ``[]`` modules = fields.TypedListField('modules', str) #: Environment variables to be set before running this test. #: #: These variables will be set during the :func:`setup` phase. #: #: :type: :class:`dict[str, str]` #: :default: ``{}`` variables = fields.TypedDictField('variables', str, str) #: Time limit for this test. #: #: Time limit is specified as a three-tuple in the form ``(hh, mm, ss)``, #: with ``hh >= 0``, ``0 <= mm <= 59`` and ``0 <= ss <= 59``. #: #: :type: :class:`tuple[int]` #: :default: ``(0, 10, 0)`` time_limit = fields.TimerField('time_limit') #: Extra resources for this test. #: #: This field is for specifying custom resources needed by this test. #: These resources are defined in the :doc:`configuration </configure>` #: of a system partition. #: For example, assume that two additional resources, named ``gpu`` and #: ``datawarp``, are defined in the configuration file as follows: #: #: :: #: #: 'resources': { #: 'gpu': [ #: '--gres=gpu:{num_gpus_per_node}' #: ], #: 'datawarp': [ #: '#DW jobdw capacity={capacity}', #: '#DW stage_in source={stagein_src}' #: ] #: } #: #: A regression test then may instantiate the above resources by setting the #: :attr:`extra_resources` attribute as follows: #: #: :: #: #: self.extra_resources = { #: 'gpu': {'num_gpus_per_node': 2} #: 'datawarp': { #: 'capacity': '100GB', #: 'stagein_src': '/foo' #: } #: } #: #: The generated batch script (for Slurm) will then contain the following #: lines: #: #: :: #: #: #SBATCH --gres=gpu:2 #: #DW jobdw capacity=100GB #: #DW stage_in source=/foo #: #: Notice that if the resource specified in the configuration uses an #: alternative directive prefix (in this case ``#DW``), this will replace #: the standard prefix of the backend scheduler (in this case ``#SBATCH``) #: #: If the resource name specified in this variable does not match a resource #: name in the partition configuration, it will be simply ignored. #: The :attr:`num_gpus_per_node` attribute translates internally to the #: ``_rfm_gpu`` resource, so that setting #: ``self.num_gpus_per_node = 2`` is equivalent to the following: #: #: :: #: #: self.extra_resources = {'_rfm_gpu': {'num_gpus_per_node': 2}} #: #: :type: :class:`dict[str, dict[str, object]]` #: :default: ``{}`` #: #: .. note:: #: .. versionadded:: 2.8 #: .. versionchanged:: 2.9 #: #: A new more powerful syntax was introduced #: that allows also custom job script directive prefixes. #: extra_resources = fields.AggregateTypeField('extra_resources', (dict, (str, (dict, (str, object))))) # Private properties _prefix = fields.StringField('_prefix') _stagedir = fields.StringField('_stagedir', allow_none=True) _stdout = fields.StringField('_stdout', allow_none=True) _stderr = fields.StringField('_stderr', allow_none=True) _perf_logfile = fields.StringField('_perf_logfile', allow_none=True) _current_system = fields.TypedField('_current_system', System) _current_partition = fields.TypedField('_current_partition', SystemPartition, allow_none=True) _current_environ = fields.TypedField('_current_environ', Environment, allow_none=True) _job = fields.TypedField('_job', Job, allow_none=True) def __init__(self, name, prefix, system, resources): self.name = name self.descr = name self.valid_prog_environs = [] self.valid_systems = [] self.sourcepath = '' self.prebuild_cmd = [] self.postbuild_cmd = [] self.executable = os.path.join('.', self.name) self.executable_opts = [] self.pre_run = [] self.post_run = [] self.keep_files = [] self.readonly_files = [] self.tags = set() self.maintainers = [] # Strict performance check, if applicable self.strict_check = True # Default is a single node check self.num_tasks = 1 self.num_tasks_per_node = None self.num_gpus_per_node = 0 self.num_cpus_per_task = None self.num_tasks_per_core = None self.num_tasks_per_socket = None self.use_multithreading = None self.exclusive_access = False # True only if check is to be run locally self.local = False # Static directories of the regression check self._prefix = os.path.abspath(prefix) self.sourcesdir = 'src' # Output patterns self.sanity_patterns = None # Performance patterns: None -> no performance checking self.perf_patterns = None self.reference = {} # Environment setup self.modules = [] self.variables = {} # Time limit for the check self.time_limit = (0, 10, 0) # Runtime information of the test self._current_system = system self._current_partition = None self._current_environ = None # Associated job self._job = None self.extra_resources = {} # Dynamic paths of the regression check; will be set in setup() self._resources_mgr = resources self._stagedir = None self._stdout = None self._stderr = None # Compilation task output self._compile_task = None # Performance logging self._perf_logger = logging.null_logger self._perf_logfile = None # Export read-only views to interesting fields @property def current_environ(self): """The programming environment that the regression test is currently executing with. This is set by the framework during the :func:`setup` phase. :type: :class:`reframe.core.environments.Environment`. """ return self._current_environ @property def current_partition(self): """The system partition the regression test is currently executing on. This is set by the framework during the :func:`setup` phase. :type: :class:`reframe.core.systems.SystemPartition`. """ return self._current_partition @property def current_system(self): """The system the regression test is currently executing on. This is set by the framework during the initialization phase. :type: :class:`reframe.core.systems.System`. """ return self._current_system @property def job(self): """The job descriptor associated with this test. This is set by the framework during the :func:`setup` phase. :type: :class:`reframe.core.schedulers.Job`. """ return self._job @property def logger(self): """A logger associated with the this test. You can use this logger to log information for your test. """ return logging.getlogger() @property def prefix(self): """The prefix directory of the test. :type: :class:`str`. """ return self._prefix @property def stagedir(self): """The stage directory of the test. This is set during the :func:`setup` phase. :type: :class:`str`. """ return self._stagedir @property @deferrable def stdout(self): """The name of the file containing the standard output of the test. This is set during the :func:`setup` phase. This attribute is evaluated lazily, so it can by used inside sanity expressions. :type: :class:`str`. """ return self._stdout @property @deferrable def stderr(self): """The name of the file containing the standard error of the test. This is set during the :func:`setup` phase. This attribute is evaluated lazily, so it can by used inside sanity expressions. :type: :class:`str`. """ return self._stderr def __repr__(self): return debug.repr(self) def info(self): """Provide live information of a running test. This method is used by the front-end to print the status message during the test's execution. This function is also called to provide the message for the ``check_info`` `logging attribute <running.html#logging>`__. By default, it returns a message reporting the test name, the current partition and the current programming environment that the test is currently executing on. :returns: a string with an informational message about this test .. note :: When overriding this method, you should pay extra attention on how you use the :class:`RegressionTest`'s attributes, because this method may be called at any point of the test's lifetime. .. versionadded:: 2.10 """ ret = self.name if self.current_partition: ret += ' on %s' % self.current_partition.fullname if self.current_environ: ret += ' using %s' % self.current_environ.name return ret def supports_system(self, partition_name): if '*' in self.valid_systems: return True if self._current_system.name in self.valid_systems: return True # Check if this is a relative name if partition_name.find(':') == -1: partition_name = '%s:%s' % (self._current_system.name, partition_name) return partition_name in self.valid_systems def supports_environ(self, env_name): for env in self.valid_prog_environs: if fnmatch.fnmatch(env_name, env): return True return False def is_local(self): """Check if the test will execute locally. A test executes locally if the :attr:`local` attribute is set or if the current partition's scheduler does not support job submission. """ if self._current_partition is None: return self.local return self.local or self._current_partition.scheduler.is_local def _sanitize_basename(self, name): """Create a basename safe to be used as path component Replace all path separator characters in `name` with underscores.""" return name.replace(os.sep, '_') def _setup_environ(self, environ): """Setup the current environment and load it.""" self._current_environ = environ # Add user modules and variables to the environment for m in self.modules: self._current_environ.add_module(m) for k, v in self.variables.items(): self._current_environ.set_variable(k, v) # First load the local environment of the partition self.logger.debug('loading environment for the current partition') self._current_partition.local_env.load() self.logger.debug("loading test's environment") self._current_environ.load() def _setup_paths(self): """Setup the check's dynamic paths.""" self.logger.debug('setting up paths') try: self._stagedir = self._resources_mgr.stagedir( self._sanitize_basename(self._current_partition.name), self.name, self._sanitize_basename(self._current_environ.name)) self.outputdir = self._resources_mgr.outputdir( self._sanitize_basename(self._current_partition.name), self.name, self._sanitize_basename(self._current_environ.name)) except OSError as e: raise PipelineError('failed to set up paths') from e self._stdout = os.path.join(self._stagedir, '%s.out' % self.name) self._stderr = os.path.join(self._stagedir, '%s.err' % self.name) def _setup_job(self, **job_opts): """Setup the job related to this check.""" self.logger.debug('setting up the job descriptor') msg = 'job scheduler backend: {0}' self.logger.debug( msg.format('local' if self.is_local else self._current_partition. scheduler.registered_name)) # num_gpus_per_node is a managed resource if self.num_gpus_per_node > 0: self.extra_resources.setdefault( '_rfm_gpu', {'num_gpus_per_node': self.num_gpus_per_node}) if self.local: scheduler_type = getscheduler('local') launcher_type = getlauncher('local') else: scheduler_type = self._current_partition.scheduler launcher_type = self._current_partition.launcher job_name = '%s_%s_%s_%s' % ( self.name, self._sanitize_basename(self._current_system.name), self._sanitize_basename(self._current_partition.name), self._sanitize_basename(self._current_environ.name)) job_script_filename = os.path.join(self._stagedir, job_name + '.sh') self._job = scheduler_type( name=job_name, command=' '.join([self.executable] + self.executable_opts), launcher=launcher_type(), environs=[ self._current_partition.local_env, self._current_environ ], workdir=self._stagedir, num_tasks=self.num_tasks, num_tasks_per_node=self.num_tasks_per_node, num_tasks_per_core=self.num_tasks_per_core, num_tasks_per_socket=self.num_tasks_per_socket, num_cpus_per_task=self.num_cpus_per_task, use_smt=self.use_multithreading, time_limit=self.time_limit, script_filename=job_script_filename, stdout=self._stdout, stderr=self._stderr, pre_run=self.pre_run, post_run=self.post_run, sched_exclusive_access=self.exclusive_access, **job_opts) # Get job options from managed resources and prepend them to # job_opts. We want any user supplied options to be able to # override those set by the framework. resources_opts = [] for r, v in self.extra_resources.items(): resources_opts.extend(self._current_partition.get_resource(r, **v)) self._job.options = (self._current_partition.access + resources_opts + self._job.options) # FIXME: This is a temporary solution to address issue #157 def _setup_perf_logging(self): self.logger.debug('setting up performance logging') self._perf_logfile = os.path.join( self._resources_mgr.logdir(self._current_partition.name), self.name + '.log') perf_logging_config = { 'level': 'INFO', 'handlers': { self._perf_logfile: { 'level': 'DEBUG', 'format': '[%(asctime)s] reframe %(version)s: ' '%(check_info)s ' '(jobid=%(check_jobid)s): %(message)s', 'append': True, } } } self._perf_logger = logging.LoggerAdapter( logger=logging.load_from_dict(perf_logging_config), check=self) def setup(self, partition, environ, **job_opts): """The setup phase of the regression test pipeline. :arg partition: The system partition to set up this test for. :arg environ: The environment to set up this test for. :arg job_opts: Options to be passed through to the backend scheduler. When overriding this method users should always pass through ``job_opts`` to the base class method. :raises reframe.core.exceptions.ReframeError: In case of errors. """ self._current_partition = partition self._setup_environ(environ) self._setup_paths() self._setup_job(**job_opts) if self.perf_patterns is not None: self._setup_perf_logging() def _copy_to_stagedir(self, path): self.logger.debug('copying %s to stage directory (%s)' % (path, self._stagedir)) self.logger.debug('symlinking files: %s' % self.readonly_files) try: os_ext.copytree_virtual(path, self._stagedir, self.readonly_files) except (OSError, ValueError, TypeError) as e: raise PipelineError('virtual copying of files failed') from e def _clone_to_stagedir(self, url): self.logger.debug('cloning URL %s to stage directory (%s)' % (url, self._stagedir)) os_ext.git_clone(self.sourcesdir, self._stagedir) def prebuild(self): for cmd in self.prebuild_cmd: self.logger.debug('executing prebuild commands') os_ext.run_command(cmd, check=True, shell=True) def postbuild(self): for cmd in self.postbuild_cmd: self.logger.debug('executing postbuild commands') os_ext.run_command(cmd, check=True, shell=True) def compile(self, **compile_opts): """The compilation phase of the regression test pipeline. :arg compile_opts: Extra options to be passed to the programming environment for compiling the source code of the test. :raises reframe.core.exceptions.ReframeError: In case of errors. """ if not self._current_environ: raise PipelineError('no programming environment set') # Copy the check's resources to the stage directory if self.sourcesdir: try: commonpath = os.path.commonpath( [self.sourcesdir, self.sourcepath]) except ValueError: commonpath = None if commonpath: self.logger.warn( "sourcepath (`%s') seems to be a subdirectory of " "sourcesdir (`%s'), but it will be interpreted " "as relative to it." % (self.sourcepath, self.sourcesdir)) if os_ext.is_url(self.sourcesdir): self._clone_to_stagedir(self.sourcesdir) else: self._copy_to_stagedir( os.path.join(self._prefix, self.sourcesdir)) # Verify the sourcepath and determine the sourcepath in the stagedir if (os.path.isabs(self.sourcepath) or os.path.normpath(self.sourcepath).startswith('..')): raise PipelineError( 'self.sourcepath is an absolute path or does not point to a ' 'subfolder or a file contained in self.sourcesdir: ' + self.sourcepath) staged_sourcepath = os.path.join(self._stagedir, self.sourcepath) self.logger.debug('Staged sourcepath: %s' % staged_sourcepath) # Remove source and executable from compile_opts compile_opts.pop('source', None) compile_opts.pop('executable', None) # Change working dir to stagedir although absolute paths are used # everywhere in the compilation process. This is done to ensure that # any other files (besides the executable) generated during the the # compilation will remain in the stage directory with os_ext.change_dir(self._stagedir): self.prebuild() if os.path.isdir(staged_sourcepath): includedir = staged_sourcepath else: includedir = os.path.dirname(staged_sourcepath) self._current_environ.include_search_path.append(includedir) self._compile_task = self._current_environ.compile( sourcepath=staged_sourcepath, executable=os.path.join(self._stagedir, self.executable), **compile_opts) self.logger.debug('compilation stdout:\n%s' % self._compile_task.stdout) self.logger.debug('compilation stderr:\n%s' % self._compile_task.stderr) self.postbuild() self.logger.debug('compilation finished') def run(self): """The run phase of the regression test pipeline. This call is non-blocking. It simply submits the job associated with this test and returns. """ if not self._current_system or not self._current_partition: raise PipelineError('no system or system partition is set') with os_ext.change_dir(self._stagedir): try: self._job.prepare(BashScriptBuilder(login=True)) except OSError as e: raise PipelineError('failed to prepare job') from e self._job.submit() msg = ('spawned job (%s=%s)' % ('pid' if self.is_local() else 'jobid', self._job.jobid)) self.logger.debug(msg) def poll(self): """Poll the test's state. :returns: :class:`True` if the associated job has finished, :class:`False` otherwise. If no job descriptor is yet associated with this test, :class:`True` is returned. :raises reframe.core.exceptions.ReframeError: In case of errors. """ if not self._job: return True return self._job.finished() def wait(self): """Wait for this test to finish. :raises reframe.core.exceptions.ReframeError: In case of errors. """ self._job.wait() self.logger.debug('spawned job finished') def sanity(self): self.check_sanity() def performance(self): try: self.check_performance() except SanityError: if self.strict_check: raise def check_sanity(self): """The sanity checking phase of the regression test pipeline. :raises reframe.core.exceptions.SanityError: If the sanity check fails. """ if self.sanity_patterns is None: raise SanityError('sanity_patterns not set') with os_ext.change_dir(self._stagedir): success = evaluate(self.sanity_patterns) if not success: raise SanityError('sanity failure') def check_performance(self): """The performance checking phase of the regression test pipeline. :raises reframe.core.exceptions.SanityError: If the performance check fails. """ if self.perf_patterns is None: return with os_ext.change_dir(self._stagedir): for tag, expr in self.perf_patterns.items(): value = evaluate(expr) key = '%s:%s' % (self._current_partition.fullname, tag) try: ref, low_thres, high_thres = self.reference[key] self._perf_logger.info('value: %s, reference: %s' % (value, self.reference[key])) except KeyError: raise SanityError( "tag `%s' not resolved in references for `%s'" % (tag, self._current_partition.fullname)) evaluate(assert_reference(value, ref, low_thres, high_thres)) def _copy_to_outputdir(self): """Copy checks interesting files to the output directory.""" self.logger.debug('copying interesting files to output directory') shutil.copy(self._stdout, self.outputdir) shutil.copy(self._stderr, self.outputdir) if self._job: shutil.copy(self._job.script_filename, self.outputdir) # Copy files specified by the user for f in self.keep_files: if not os.path.isabs(f): f = os.path.join(self._stagedir, f) shutil.copy(f, self.outputdir) def cleanup(self, remove_files=False, unload_env=True): """The cleanup phase of the regression test pipeline. :arg remove_files: If :class:`True`, the stage directory associated with this test will be removed. :arg unload_env: If :class:`True`, the environment that was used to run this test will be unloaded. """ aliased = os.path.samefile(self._stagedir, self.outputdir) if aliased: self.logger.debug('skipping copy to output dir ' 'since they alias each other') else: self._copy_to_outputdir() if remove_files: self.logger.debug('removing stage directory') shutil.rmtree(self._stagedir) if unload_env: self.logger.debug("unloading test's environment") self._current_environ.unload() self._current_partition.local_env.unload() def __str__(self): return ('%s (%s)\n' ' tags: [%s], maintainers: [%s]' % (self.name, self.descr, ', '.join(self.tags), ', '.join( self.maintainers)))
class FieldTester: field = fields.TypedListField('field', int) def __init__(self, value): self.field = value
class Job(abc.ABC): """A job descriptor. .. note:: This is an abstract class. Users may not create jobs directly. """ #: Options to be passed to the backend job scheduler. #: #: :type: :class:`list` of :class:`str` #: :default: ``[]`` options = fields.TypedListField('options', str) #: The parallel program launcher that will be used to launch the parallel #: executable of this job. #: #: :type: :class:`reframe.core.launchers.JobLauncher` launcher = fields.TypedField('launcher', JobLauncher) _jobid = fields.IntegerField('_jobid', allow_none=True) _exitcode = fields.IntegerField('_exitcode', allow_none=True) _state = fields.TypedField('_state', JobState, allow_none=True) # The sched_* arguments are exposed also to the frontend def __init__(self, name, command, launcher, environs=[], workdir='.', num_tasks=1, num_tasks_per_node=None, num_tasks_per_core=None, num_tasks_per_socket=None, num_cpus_per_task=None, use_smt=None, time_limit=(0, 10, 0), script_filename=None, stdout=None, stderr=None, pre_run=[], post_run=[], sched_account=None, sched_partition=None, sched_reservation=None, sched_nodelist=None, sched_exclude_nodelist=None, sched_exclusive_access=None, sched_options=[]): # Mutable fields self.options = list(sched_options) # Commands to be run before and after the job is launched self._pre_run = list(pre_run) self._post_run = list(post_run) self.launcher = launcher self._name = 'rfm_' + name self._command = command self._environs = list(environs) self._workdir = workdir self._num_tasks = num_tasks self._num_tasks_per_node = num_tasks_per_node self._num_tasks_per_core = num_tasks_per_core self._num_tasks_per_socket = num_tasks_per_socket self._num_cpus_per_task = num_cpus_per_task self._use_smt = use_smt self._script_filename = script_filename or '%s.sh' % name self._stdout = stdout or '%s.out' % name self._stderr = stderr or '%s.err' % name self._time_limit = time_limit # Backend scheduler related information self._sched_nodelist = sched_nodelist self._sched_exclude_nodelist = sched_exclude_nodelist self._sched_partition = sched_partition self._sched_reservation = sched_reservation self._sched_account = sched_account self._sched_exclusive_access = sched_exclusive_access # Live job information; to be filled during job's lifetime by the # scheduler self._jobid = None self._exitcode = None self._state = None def __repr__(self): return debug.repr(self) # Read-only properties @property def exitcode(self): return self._exitcode @property def jobid(self): return self._jobid @property def state(self): return self._state @property def name(self): return self._name @property def command(self): return self._command @property def workdir(self): return self._workdir @property def environs(self): return self._environs @property def num_tasks(self): return self._num_tasks @property def script_filename(self): return self._script_filename @property def stdout(self): return self._stdout @property def stderr(self): return self._stderr @property def time_limit(self): return self._time_limit @property def num_cpus_per_task(self): return self._num_cpus_per_task @property def num_tasks_per_core(self): return self._num_tasks_per_core @property def num_tasks_per_node(self): return self._num_tasks_per_node @property def num_tasks_per_socket(self): return self._num_tasks_per_socket @property def use_smt(self): return self._use_smt @property def sched_nodelist(self): return self._sched_nodelist @property def sched_exclude_nodelist(self): return self._sched_exclude_nodelist @property def sched_partition(self): return self._sched_partition @property def sched_reservation(self): return self._sched_reservation @property def sched_account(self): return self._sched_account @property def sched_exclusive_access(self): return self._sched_exclusive_access def emit_environ(self, builder): for e in self._environs: e.emit_load_instructions(builder) def emit_pre_run(self, builder): for c in self._pre_run: builder.verbatim(c) def emit_post_run(self, script_builder): for c in self._post_run: script_builder.verbatim(c) def prepare(self, script_builder): self.emit_preamble(script_builder) self.emit_environ(script_builder) self.emit_pre_run(script_builder) self.launcher.emit_run_command(self, script_builder) self.emit_post_run(script_builder) with open(self.script_filename, 'w') as fp: fp.write(script_builder.finalise()) @abc.abstractmethod def emit_preamble(self, builder): pass @abc.abstractmethod def submit(self): pass @abc.abstractmethod def wait(self): if self._jobid is None: raise JobNotStartedError('cannot wait an unstarted job') @abc.abstractmethod def cancel(self): if self._jobid is None: raise JobNotStartedError('cannot cancel an unstarted job') @abc.abstractmethod def finished(self): if self._jobid is None: raise JobNotStartedError('cannot poll an unstarted job')
class ProgEnvironment(Environment): """A class representing a programming environment. This type of environment adds also attributes for setting the compiler and compilation flags. If compilation flags are set to :class:`None` (the default, if not set otherwise in ReFrame's `configuration <configure.html#environments-configuration>`__), they are not passed to the ``make`` invocation. If you want to disable completely the propagation of the compilation flags to the ``make`` invocation, even if they are set, you should set the :attr:`propagate` attribute to :class:`False`. """ #: The C compiler of this programming environment. #: #: :type: :class:`str` cc = fields.StringField('cc') #: The C++ compiler of this programming environment. #: #: :type: :class:`str` or :class:`None` cxx = fields.StringField('cxx', allow_none=True) #: The Fortran compiler of this programming environment. #: #: :type: :class:`str` or :class:`None` ftn = fields.StringField('ftn', allow_none=True) #: The preprocessor flags of this programming environment. #: #: :type: :class:`str` or :class:`None` cppflags = fields.StringField('cppflags', allow_none=True) #: The C compiler flags of this programming environment. #: #: :type: :class:`str` or :class:`None` cflags = fields.StringField('cflags', allow_none=True) #: The C++ compiler flags of this programming environment. #: #: :type: :class:`str` or :class:`None` cxxflags = fields.StringField('cxxflags', allow_none=True) #: The Fortran compiler flags of this programming environment. #: #: :type: :class:`str` or :class:`None` fflags = fields.StringField('fflags', allow_none=True) #: The linker flags of this programming environment. #: #: :type: :class:`str` or :class:`None` ldflags = fields.StringField('ldflags', allow_none=True) #: The include search path of this programming environment. #: #: :type: :class:`list` of :class:`str` #: :default: ``[]`` include_search_path = fields.TypedListField('include_search_path', str) #: Propagate the compilation flags to the ``make`` invocation. #: #: :type: :class:`bool` #: :default: :class:`True` propagate = fields.BooleanField('propagate') def __init__(self, name, modules=[], variables={}, cc='cc', cxx='CC', ftn='ftn', cppflags=None, cflags=None, cxxflags=None, fflags=None, ldflags=None, **kwargs): super().__init__(name, modules, variables) self.cc = cc self.cxx = cxx self.ftn = ftn self.cppflags = cppflags self.cflags = cflags self.cxxflags = cxxflags self.fflags = fflags self.ldflags = ldflags self.include_search_path = [] self.propagate = True def guess_language(self, filename): ext = filename.split('.')[-1] if ext in ['c']: return 'C' if ext in ['cc', 'cp', 'cxx', 'cpp', 'CPP', 'c++', 'C']: return 'C++' if ext in [ 'f', 'for', 'ftn', 'F', 'FOR', 'fpp', 'FPP', 'FTN', 'f90', 'f95', 'f03', 'f08', 'F90', 'F95', 'F03', 'F08' ]: return 'Fortran' if ext in ['cu']: return 'CUDA' def compile(self, sourcepath, makefile=None, executable=None, lang=None, options=''): if not os.path.exists(sourcepath): raise FileNotFoundError(errno.ENOENT, os.strerror(errno.ENOENT), sourcepath) if os.path.isdir(sourcepath): return self._compile_dir(sourcepath, makefile, options) else: return self._compile_file(sourcepath, executable, lang, options) def _compile_file(self, source_file, executable, lang, options): if not executable: # default executable, same as source_file without the extension executable = os.path.join(os.path.dirname(source_file), source_file.rsplit('.')[:-1][0]) if not lang: lang = self.guess_language(source_file) # Replace None's with empty strings cppflags = self.cppflags or '' cflags = self.cflags or '' cxxflags = self.cxxflags or '' fflags = self.fflags or '' ldflags = self.ldflags or '' flags = [cppflags] if lang == 'C': compiler = self.cc flags.append(cflags) elif lang == 'C++': compiler = self.cxx flags.append(cxxflags) elif lang == 'Fortran': compiler = self.ftn flags.append(fflags) elif lang == 'CUDA': compiler = 'nvcc' flags.append(cxxflags) else: raise EnvironError('Unknown language: %s' % lang) # Append include search path flags += ['-I' + d for d in self.include_search_path] cmd = ('%s %s %s -o %s %s %s' % (compiler, ' '.join(flags), source_file, executable, ldflags, options)) try: return os_ext.run_command(cmd, check=True) except SpawnedProcessError as e: # Re-raise as compilation error raise CompilationError(command=e.command, stdout=e.stdout, stderr=e.stderr, exitcode=e.exitcode) from None def _compile_dir(self, source_dir, makefile, options): if makefile: cmd = 'make -C %s -f %s %s ' % (source_dir, makefile, options) else: cmd = 'make -C %s %s ' % (source_dir, options) # Pass a set of predefined options to the Makefile if self.propagate: flags = [ "CC='%s'" % self.cc, "CXX='%s'" % self.cxx, "FC='%s'" % self.ftn ] # Explicitly check against None here; the user may explicitly want # to clear the flags if self.cppflags is not None: flags.append("CPPFLAGS='%s'" % self.cppflags) if self.cflags is not None: flags.append("CFLAGS='%s'" % self.cflags) if self.cxxflags is not None: flags.append("CXXFLAGS='%s'" % self.cxxflags) if self.fflags is not None: flags.append("FFLAGS='%s'" % self.fflags) if self.ldflags is not None: flags.append("LDFLAGS='%s'" % self.ldflags) cmd += ' '.join(flags) try: return os_ext.run_command(cmd, check=True) except SpawnedProcessError as e: # Re-raise as compilation error raise CompilationError(command=e.command, stdout=e.stdout, stderr=e.stderr, exitcode=e.exitcode) from None
class Environment: """This class abstracts away an environment to run regression tests. It is simply a collection of modules to be loaded and environment variables to be set when this environment is loaded by the framework. Users may not create or modify directly environments. """ name = fields.NonWhitespaceField('name') modules = fields.TypedListField('modules', str) variables = fields.TypedDictField('variables', str, str) def __init__(self, name, modules=[], variables={}, **kwargs): self._name = name self._modules = list(modules) self._variables = dict(variables) self._loaded = False self._saved_variables = {} self._conflicted = [] self._preloaded = set() self._load_stmts = [] @property def name(self): """The name of this environment. :type: :class:`str` """ return self._name @property def modules(self): """The modules associated with this environment. :type: :class:`list` of :class:`str` """ return self._modules @property def variables(self): """The environment variables associated with this environment. :type: dictionary of :class:`str` keys/values. """ return self._variables @property def is_loaded(self): """:class:`True` if this environment is loaded, :class:`False` otherwise. """ return self._loaded # Add module to the list of modules to be loaded. def add_module(self, name): self._modules.append(name) # Set environment variable to name. # # If variable exists, its value will be saved internally and restored # during unloading. def set_variable(self, name, value): self._variables[name] = value def load(self): # conflicted module list must be filled at the time of load for m in self._modules: if get_modules_system().is_module_loaded(m): self._preloaded.add(m) self._conflicted += get_modules_system().load_module(m, force=True) for conflict in self._conflicted: stmts = get_modules_system().emit_unload_commands(conflict) self._load_stmts += stmts self._load_stmts += get_modules_system().emit_load_commands(m) for k, v in self._variables.items(): if k in os.environ: self._saved_variables[k] = os.environ[k] os.environ[k] = os.path.expandvars(v) self._loaded = True def unload(self): if not self._loaded: return for k, v in self._variables.items(): if k in self._saved_variables: os.environ[k] = self._saved_variables[k] elif k in os.environ: del os.environ[k] # Unload modules in reverse order for m in reversed(self._modules): if m not in self._preloaded: get_modules_system().unload_module(m) # Reload the conflicted packages, previously removed for m in self._conflicted: get_modules_system().load_module(m) self._loaded = False def emit_load_instructions(self, builder): for stmt in self._load_stmts: builder.verbatim(stmt) for k, v in self._variables.items(): builder.set_variable(k, v, export=True) # FIXME: Does not correspond to the actual process in unload() def emit_unload_instructions(self, builder): for k, v in self._variables.items(): builder.unset_variable(k) for m in self._modules: builder.verbatim('module unload %s' % m) for m in self._conflicted: builder.verbatim('module load %s' % m) def __eq__(self, other): if not isinstance(other, type(self)): return NotImplemented return (self._name == other._name and set(self._modules) == set(other._modules) and self._variables == other._variables) def __repr__(self): return debug.repr(self) def __str__(self): ret = "{0}(name='{1}', modules={2}, variables={3})" return ret.format( type(self).__name__, self.name, self.modules, self.variables)
class SystemPartition: """A representation of a system partition inside ReFrame.""" _name = fields.NonWhitespaceField('_name') _descr = fields.StringField('_descr') _access = fields.TypedListField('_access', str) _environs = fields.TypedListField('_environs', Environment) _resources = fields.TypedDictField('_resources', str, (list, str)) _local_env = fields.TypedField('_local_env', Environment, allow_none=True) # maximum concurrent jobs _max_jobs = fields.IntegerField('_max_jobs') def __init__(self, name, descr=None, scheduler=None, launcher=None, access=[], environs=[], resources={}, local_env=None, max_jobs=1): self._name = name self._descr = descr or name self._scheduler = scheduler self._launcher = launcher self._access = list(access) self._environs = list(environs) self._resources = dict(resources) self._max_jobs = max_jobs self._local_env = local_env self._active = True # Parent system self._system = None def enable(self): self._active = True def disable(self): self._active = False @property def access(self): return self._access @property def active(self): return self._active @property def descr(self): """A detailed description of this partition.""" return self._descr @property def environs(self): return self._environs @property def fullname(self): """Return the fully-qualified name of this partition. The fully-qualified name is of the form ``<parent-system-name>:<partition-name>``. :type: `str` """ if self._system is None: return self._name else: return '%s:%s' % (self._system.name, self._name) @property def local_env(self): return self._local_env @property def max_jobs(self): return self._max_jobs @property def name(self): """The name of this partition. :type: `str` """ return self._name @property def resources(self): return self._resources @property def scheduler(self): """The type of the backend scheduler of this partition. :returns: a subclass of :class:`reframe.core.schedulers.Job`. .. note:: .. versionchanged:: 2.8 Prior versions returned a string representing the scheduler and job launcher combination. """ return self._scheduler @property def launcher(self): """The type of the backend launcher of this partition. :returns: a subclass of :class:`reframe.core.launchers.JobLauncher`. .. note:: .. versionadded:: 2.8 """ return self._launcher # Instantiate managed resource `name` with `value`. def get_resource(self, name, **values): ret = [] for r in self._resources.get(name, []): try: ret.append(r.format(**values)) except KeyError: pass return ret def environment(self, name): for e in self._environs: if e.name == name: return e return None def __eq__(self, other): if not isinstance(other, type(self)): return NotImplemented return (self._name == other.name and self._scheduler == other._scheduler and self._launcher == other._launcher and self._access == other._access and self._environs == other._environs and self._resources == other._resources and self._local_env == other._local_env) def __str__(self): return self._name def __repr__(self): return debug.repr(self)
class System: """A representation of a system inside ReFrame.""" _name = fields.NonWhitespaceField('_name') _descr = fields.StringField('_descr') _hostnames = fields.TypedListField('_hostnames', str) _partitions = fields.TypedListField('_partitions', SystemPartition) _modules_system = fields.AlphanumericField('_modules_system', allow_none=True) prefix = fields.StringField('prefix') stagedir = fields.StringField('stagedir', allow_none=True) outputdir = fields.StringField('outputdir', allow_none=True) logdir = fields.StringField('logdir', allow_none=True) #: Global resources directory for this system #: #: You may use this directory for storing large resource files of your #: regression tests. #: See `here <configure.html#system-configuration>`__ on how to configure this. #: #: :type: :class:`str` resourcesdir = fields.StringField('resourcesdir') def __init__(self, name, descr=None, hostnames=[], partitions=[], prefix='.', stagedir=None, outputdir=None, logdir=None, resourcesdir='.', modules_system=None): self._name = name self._descr = descr or name self._hostnames = list(hostnames) self._partitions = list(partitions) self._modules_system = modules_system self.prefix = prefix self.stagedir = stagedir self.outputdir = outputdir self.logdir = logdir self.resourcesdir = resourcesdir # Set parent system for the given partitions for p in partitions: p._system = self @property def descr(self): """The description of this system.""" return self._descr @property def hostnames(self): return self._hostnames @property def modules_system(self): return self._modules_system @property def name(self): """The name of this system.""" return self._name @property def partitions(self): """Get all the active partitions of this system. :returns: a list of :class:`SystemPartition`. """ return [p for p in self._partitions if p.active] def partition(self, name): """Get system partition with ``name``. :returns: the requested :class:`SystemPartition`, or :class:`None` if not found. """ for p in self._partitions: if p.name == name and p.active: return p return None def add_partition(self, partition): partition._system = self self._partitions.append(partition) def __eq__(self, other): if not isinstance(other, type(self)): return NotImplemented return (self._name == other._name and self._hostnames == other._hostnames and self._partitions == other._partitions) def __repr__(self): return debug.repr(self) def __str__(self): return '%s (partitions: %s)' % (self._name, [str(p) for p in self._partitions])