Source code for buildtest.buildsystem.builders

"""This module implements the Builder class that is responsible for getting builders
from a buildspec file. The Builder class is invoked once buildspec file has
parsed validation via :class:`buildtest.buildsystem.parser.BuildspecParser`.
"""

import logging
import os
import re

from buildtest.buildsystem.compilerbuilder import CompilerBuilder
from buildtest.buildsystem.scriptbuilder import ScriptBuilder
from buildtest.buildsystem.spack import SpackBuilder
from buildtest.cli.compilers import BuildtestCompilers
from buildtest.exceptions import BuildTestError
from buildtest.system import system
from buildtest.utils.tools import deep_get


[docs]class Builder: """The Builder class creates builder objects based on parsed buildspecs. The builder class is created based on the 'type' field in the test. If test contains ``type: script`` we will create builder by calling :class:`buildtest.buildsystem.scriptbuilder.ScriptBuilder`. Likewise for ``type: compiler`` and ``type: spack`` we will call :class:`buildtest.buildsystem.compilerbuilder.CompilerBuilder` and :class:`buildtest.buildsystem.spack.SpackBuilder`. """ def __init__( self, bp, buildexecutor, filters, testdir, configuration, buildtest_system=None, rebuild=1, ): """Based on a loaded Buildspec file, return the correct builder for each based on the type. Each type is associated with a known Builder class. Args: bp (buildtest.buildsystem.parser.BuildspecParser): Instance of BuildspecParser class buildexecutor (buildtest.executors.setup.BuildExecutor): Instance of BuildExecutor class filters (dict): List of filter fields specified via ``buildtest build --filter`` for filtering tests testdir (str): Test directory where tests will be written which could be specified via ``buildtest build --testdir`` or configuration file configuration (buildtest.config.SiteConfiguration): Instance of SiteConfiguration class buildtest_system (buildtest.system.BuildTestSystem, optional): Instance of BuildTestSystem class rebuild (int, optional): Number of rebuild for test. This is specified via ``buildtest build --rebuild``. Defaults to 1 """ self.configuration = configuration self.system = buildtest_system or system self.logger = logging.getLogger(__name__) self.testdir = testdir self.buildexecutor = buildexecutor if not rebuild: self.rebuild = 1 else: # FIX LINE BELOW self.rebuild = rebuild or 1 self.rebuild = int(rebuild) self.bp = bp self.filters = filters if deep_get(self.filters, "maintainers"): if not self.bp.recipe.get("maintainers"): raise BuildTestError( f"[{self.bp.buildspec}]: skipping test because maintainers field is not specified when using buildtest build --filter maintainers={self.filters['maintainers'] }" ) if self.filters["maintainers"] not in self.bp.recipe.get("maintainers"): raise BuildTestError( f"[{self.bp.buildspec}]: skipping buildspec due to filter by maintainers: {self.filters['maintainers']}" ) self.builders = [] for count in range(self.rebuild): for name in self.get_test_names(): recipe = self.bp.recipe["buildspecs"][name] if recipe.get("skip"): msg = f"[{name}]({self.bp.buildspec}): test is skipped." self.logger.info(msg) print(msg) continue # apply filter by tags or type if --filter option is specified if self.filters: if self._skip_tests_by_tags(recipe, name): continue if self._skip_tests_by_type(recipe, name): continue if self._skip_tests_run_only(recipe, name): continue # Add the builder for the script or spack schema if recipe["type"] in ["script", "spack"]: self.builders += self._generate_builders(recipe, name) elif recipe["type"] == "compiler": self._build_compilers(name, recipe) else: print( "%s is not recognized by buildtest, skipping." % recipe["type"] ) for builder in self.builders: self.logger.debug(builder)
[docs] def _generate_builders(self, recipe, name, compiler_name=None): """This method is responsible for generating builders by applying regular expression specified by ``executor`` field in buildspec with list of executors. If their is a match we generate a builder. Args: name (str): Name of test in buildspec file recipe (dict): Loaded test recipe from buildspec file compiler_name (str, optional): Name of compiler Returns: List of builder objects """ builders = [] self.logger.debug( f"Searching for builders for test: {name} by applying regular expression with available builders: {self.buildexecutor.list_executors()} " ) for executor in self.buildexecutor.list_executors(): builder = None if ( re.fullmatch(recipe.get("executor"), executor) and recipe["type"] == "script" ): self.logger.debug( f"Found a match in buildspec with available executors via re.fullmatch({recipe.get('executor')},{executor})" ) builder = ScriptBuilder( name=name, recipe=recipe, executor=executor, buildspec=self.bp.buildspec, buildexecutor=self.buildexecutor, testdir=self.testdir, ) elif ( re.fullmatch(recipe.get("executor"), executor) and recipe["type"] == "compiler" ): self.logger.debug( f"Found a match in buildspec with available executors via re.fullmatch({recipe.get('executor')},{executor})" ) builder = CompilerBuilder( name=name, recipe=recipe, executor=executor, compiler=compiler_name, buildspec=self.bp.buildspec, configuration=self.configuration, buildexecutor=self.buildexecutor, testdir=self.testdir, ) elif ( re.fullmatch(recipe.get("executor"), executor) and recipe["type"] == "spack" ): builder = SpackBuilder( name=name, recipe=recipe, executor=executor, buildspec=self.bp.buildspec, buildexecutor=self.buildexecutor, testdir=self.testdir, ) if builder: self.logger.debug(builder) builders.append(builder) return builders
[docs] def _build_compilers(self, name, recipe): """This method will perform regular expression with 'name' field in compilers section and retrieve one or more compiler that were defined in buildtest configuration. If any compilers were retrieved we return one or more builder objects that call :class:`buildtest.buildsystem.compilerbuilder.CompilerBuilder` Args: name (str): name of test recipe (dict): Loaded test recipe from buildspec """ self.compilers = {} bc = BuildtestCompilers(configuration=self.configuration) discovered_compilers = bc.list() builders = [] # exclude compiler from search if 'exclude' specified in buildspec if recipe["compilers"].get("exclude"): for exclude in recipe["compilers"]["exclude"]: if exclude in discovered_compilers: msg = f"Excluding compiler: {exclude} from test generation" print(msg) self.logger.debug(msg) discovered_compilers.remove(exclude) # apply regular expression specified by 'name' field against all discovered compilers for compiler_pattern in recipe["compilers"]["name"]: for bc_name in discovered_compilers: if re.match(compiler_pattern, bc_name): builder = self._generate_builders( name=name, recipe=recipe, compiler_name=bc_name ) builders += builder if not builders: msg = f"[{name}][{self.bp.buildspec}]: Unable to find any compilers based on regular expression: {recipe['compilers']['name']} so no tests were created." print(msg) self.logger.debug(msg) return for builder in builders: self.builders.append(builder)
[docs] def _skip_tests_by_tags(self, recipe, name): """This method determines if test should be skipped based on tag names specified in filter field that is specified on command line via ``buildtest build --filter tags=<TAGNAME>`` Args: recipe (dict): Loaded test recipe from buildspec name (str): Name of test Returns: bool: False if ``buildtest build --filter tags`` is not specified. If specified we return ``True`` if ``tags`` field is not in test recipe or there is a matching tag. """ if self.filters.get("tags"): # if tags field in buildspec is empty, then we skip test only if user filters by tags if not recipe.get("tags"): return True found = False # for tagname in self.filters: if self.filters["tags"] in recipe.get("tags"): found = True if not found: msg = f"[{name}][{self.bp.buildspec}]: test is skipped because it is not in tag filter list: {self.filters}" self.logger.info(msg) print(msg) return True return False
[docs] def _skip_tests_by_type(self, recipe, name): """This method determines if test should be skipped based on type field specified in filter field that is specified on command line via ``buildtest build --filter type=<SCHEMATYPE>`` Args: recipe (dict): Loaded test recipe from buildspec name (str): Name of test Returns: bool: False if ``buildtest build --filter type`` is not specified. If there is a match with input filter and ``type`` field in test we return ``True`` """ if self.filters.get("type"): found = self.filters["type"] == recipe["type"] if not found: msg = f"[{name}][{self.bp.buildspec}]: test is skipped because it is not in type filter list: {self.filters['type']}" self.logger.info(msg) print(msg) return True return False
[docs] def _skip_tests_run_only(self, recipe, name): """This method will skip tests based on ``run_only`` field from buildspec. Checks are performed based on conditionals and if any conditional is not met we skip test. Args: recipe (dict): Loaded test recipe from buildspec name (str): Name of test Returns: bool: ``False`` if `run_only` property not specified in buildspec otherwise returns ``True`` based on following condition - True if there is no match with system 'scheduler' and one specified in buildspec - True if there is no match with user specifed by 'user' property and one detected by system using ``os.getenv("USER")`` - True if there is no match with specified 'platform' property and one detected by system platform - True if there is no match with specified 'linux_distro' property and one detected by system """ # if run_only field set, check if all conditions match before proceeding with test if recipe.get("run_only"): # skip test if host scheduler is not one specified via 'scheduler' field if recipe["run_only"].get("scheduler") and ( recipe["run_only"].get("scheduler") not in self.system.system["scheduler"] ): msg = f"[{name}][{self.bp.buildspec}]: test is skipped because ['run_only']['scheduler'] got value: {recipe['run_only']['scheduler']} but detected scheduler: {self.system.system['scheduler']}." print(msg) self.logger.info(msg) return True # skip test if current user is not one specified in 'user' field if recipe["run_only"].get("user") and ( recipe["run_only"].get("user") != os.getenv("USER") ): msg = f"[{name}][{self.bp.buildspec}]: test is skipped because this test is expected to run as user: {recipe['run_only']['user']} but detected user: {os.getenv('USER')}." print(msg) self.logger.info(msg) return True # skip test if host platform is not equal to value specified by 'platform' field if recipe["run_only"].get("platform") and ( recipe["run_only"].get("platform") != self.system.system["platform"] ): msg = f"[{name}][{self.bp.buildspec}]: test is skipped because this test is expected to run on platform: {recipe['run_only']['platform']} but detected platform: {self.system.system['platform']}." print(msg) self.logger.info(msg) return True # skip test if host platform is not equal to value specified by 'platform' field if recipe["run_only"].get("linux_distro"): if self.system.system["os"] not in recipe["run_only"]["linux_distro"]: msg = f"[{name}][{self.bp.buildspec}]: test is skipped because this test is expected to run on linux distro: {recipe['run_only']['linux_distro']} but detected linux distro: {self.system.system['os']}." print(msg) self.logger.info(msg) return True return False
[docs] def get_builders(self): """Return a list of builder objects""" return self.builders
[docs] def get_test_names(self): """Return the list of test names for the loaded Buildspec recipe""" keys = [] if self.bp.recipe: keys = [x for x in self.bp.recipe["buildspecs"].keys()] return keys