Пример #1
0
class BaseTest(unittest.TestCase):
  """A baseclass useful for tests requiring a temporary buildroot.

  :API: public

  """

  def build_path(self, relpath):
    """Returns the canonical BUILD file path for the given relative build path.

    :API: public
    """
    if os.path.basename(relpath).startswith('BUILD'):
      return relpath
    else:
      return os.path.join(relpath, 'BUILD')

  def create_dir(self, relpath):
    """Creates a directory under the buildroot.

    :API: public

    relpath: The relative path to the directory from the build root.
    """
    path = os.path.join(self.build_root, relpath)
    safe_mkdir(path)
    return path

  def create_workdir_dir(self, relpath):
    """Creates a directory under the work directory.

    :API: public

    relpath: The relative path to the directory from the work directory.
    """
    path = os.path.join(self.pants_workdir, relpath)
    safe_mkdir(path)
    return path

  def create_file(self, relpath, contents='', mode='w'):
    """Writes to a file under the buildroot.

    :API: public

    relpath:  The relative path to the file from the build root.
    contents: A string containing the contents of the file - '' by default..
    mode:     The mode to write to the file in - over-write by default.
    """
    path = os.path.join(self.build_root, relpath)
    with safe_open(path, mode=mode) as fp:
      fp.write(contents)
    return path

  def create_workdir_file(self, relpath, contents='', mode='w'):
    """Writes to a file under the work directory.

    :API: public

    relpath:  The relative path to the file from the work directory.
    contents: A string containing the contents of the file - '' by default..
    mode:     The mode to write to the file in - over-write by default.
    """
    path = os.path.join(self.pants_workdir, relpath)
    with safe_open(path, mode=mode) as fp:
      fp.write(contents)
    return path

  def add_to_build_file(self, relpath, target):
    """Adds the given target specification to the BUILD file at relpath.

    :API: public

    relpath: The relative path to the BUILD file from the build root.
    target:  A string containing the target definition as it would appear in a BUILD file.
    """
    self.create_file(self.build_path(relpath), target, mode='a')
    return BuildFile(self.address_mapper._project_tree, relpath=self.build_path(relpath))

  def make_target(self,
                  spec='',
                  target_type=Target,
                  dependencies=None,
                  derived_from=None,
                  synthetic=False,
                  **kwargs):
    """Creates a target and injects it into the test's build graph.

    :API: public

    :param string spec: The target address spec that locates this target.
    :param type target_type: The concrete target subclass to create this new target from.
    :param list dependencies: A list of target instances this new target depends on.
    :param derived_from: The target this new target was derived from.
    :type derived_from: :class:`pants.build_graph.target.Target`
    """
    address = Address.parse(spec)
    target = target_type(name=address.target_name,
                         address=address,
                         build_graph=self.build_graph,
                         **kwargs)
    dependencies = dependencies or []

    self.build_graph.apply_injectables([target])
    self.build_graph.inject_target(target,
                                   dependencies=[dep.address for dep in dependencies],
                                   derived_from=derived_from,
                                   synthetic=synthetic)

    # TODO(John Sirois): This re-creates a little bit too much work done by the BuildGraph.
    # Fixup the BuildGraph to deal with non BuildFileAddresses better and just leverage it.
    traversables = [target.compute_dependency_specs(payload=target.payload)]

    for dependency_spec in itertools.chain(*traversables):
      dependency_address = Address.parse(dependency_spec, relative_to=address.spec_path)
      dependency_target = self.build_graph.get_target(dependency_address)
      if not dependency_target:
        raise ValueError('Tests must make targets for dependency specs ahead of them '
                         'being traversed, {} tried to traverse {} which does not exist.'
                         .format(target, dependency_address))
      if dependency_target not in target.dependencies:
        self.build_graph.inject_dependency(dependent=target.address,
                                           dependency=dependency_address)
        target.mark_transitive_invalidation_hash_dirty()

    return target

  @property
  def alias_groups(self):
    """
    :API: public
    """
    return BuildFileAliases(targets={'target': Target})

  @property
  def build_ignore_patterns(self):
    """
    :API: public
    """
    return None

  def setUp(self):
    """
    :API: public
    """
    super(BaseTest, self).setUp()
    # Avoid resetting the Runtracker here, as that is specific to fork'd process cleanup.
    clean_global_runtime_state(reset_subsystem=True)

    self.real_build_root = BuildRoot().path

    self.build_root = os.path.realpath(mkdtemp(suffix='_BUILD_ROOT'))
    self.subprocess_dir = os.path.join(self.build_root, '.pids')
    self.addCleanup(safe_rmtree, self.build_root)

    self.pants_workdir = os.path.join(self.build_root, '.pants.d')
    safe_mkdir(self.pants_workdir)

    self.options = defaultdict(dict)  # scope -> key-value mapping.
    self.options[GLOBAL_SCOPE] = {
      'pants_workdir': self.pants_workdir,
      'pants_supportdir': os.path.join(self.build_root, 'build-support'),
      'pants_distdir': os.path.join(self.build_root, 'dist'),
      'pants_configdir': os.path.join(self.build_root, 'config'),
      'pants_subprocessdir': self.subprocess_dir,
      'cache_key_gen_version': '0-test',
    }
    self.options['cache'] = {
      'read_from': [],
      'write_to': [],
    }

    BuildRoot().path = self.build_root
    self.addCleanup(BuildRoot().reset)

    self._build_configuration = BuildConfiguration()
    self._build_configuration.register_aliases(self.alias_groups)
    self.build_file_parser = BuildFileParser(self._build_configuration, self.build_root)
    self.project_tree = FileSystemProjectTree(self.build_root)
    self.reset_build_graph()

  def buildroot_files(self, relpath=None):
    """Returns the set of all files under the test build root.

    :API: public

    :param string relpath: If supplied, only collect files from this subtree.
    :returns: All file paths found.
    :rtype: set
    """
    def scan():
      for root, dirs, files in os.walk(os.path.join(self.build_root, relpath or '')):
        for f in files:
          yield os.path.relpath(os.path.join(root, f), self.build_root)
    return set(scan())

  def reset_build_graph(self):
    """Start over with a fresh build graph with no targets in it."""
    self.address_mapper = BuildFileAddressMapper(self.build_file_parser, self.project_tree,
                                                 build_ignore_patterns=self.build_ignore_patterns)
    self.build_graph = MutableBuildGraph(address_mapper=self.address_mapper)

  def set_options_for_scope(self, scope, **kwargs):
    self.options[scope].update(kwargs)

  def context(self, for_task_types=None, for_subsystems=None, options=None,
              target_roots=None, console_outstream=None, workspace=None,
              scheduler=None, **kwargs):
    """
    :API: public

    :param dict **kwargs: keyword arguments passed in to `create_options_for_optionables`.
    """
    # Many tests use source root functionality via the SourceRootConfig.global_instance().
    # (typically accessed via Target.target_base), so we always set it up, for convenience.
    for_subsystems = set(for_subsystems or ())
    for subsystem in for_subsystems:
      if subsystem.options_scope is None:
        raise TaskError('You must set a scope on your subsystem type before using it in tests.')

    optionables = {SourceRootConfig} | self._build_configuration.optionables() | for_subsystems

    for_task_types = for_task_types or ()
    for task_type in for_task_types:
      scope = task_type.options_scope
      if scope is None:
        raise TaskError('You must set a scope on your task type before using it in tests.')
      optionables.add(task_type)
      # If task is expected to inherit goal-level options, register those directly on the task,
      # by subclassing the goal options registrar and settings its scope to the task scope.
      if issubclass(task_type, GoalOptionsMixin):
        subclass_name = 'test_{}_{}_{}'.format(
          task_type.__name__, task_type.goal_options_registrar_cls.options_scope,
          task_type.options_scope)
        if PY2:
          subclass_name = subclass_name.encode('utf-8')
        optionables.add(type(subclass_name, (task_type.goal_options_registrar_cls, ),
                             {'options_scope': task_type.options_scope}))

    # Now expand to all deps.
    all_optionables = set()
    for optionable in optionables:
      all_optionables.update(si.optionable_cls for si in optionable.known_scope_infos())

    # Now default the option values and override with any caller-specified values.
    # TODO(benjy): Get rid of the options arg, and require tests to call set_options.
    options = options.copy() if options else {}
    for s, opts in self.options.items():
      scoped_opts = options.setdefault(s, {})
      scoped_opts.update(opts)

    fake_options = create_options_for_optionables(
      all_optionables, options=options, **kwargs)

    Subsystem.reset(reset_options=True)
    Subsystem.set_options(fake_options)

    context = create_context_from_options(fake_options,
                                          target_roots=target_roots,
                                          build_graph=self.build_graph,
                                          build_file_parser=self.build_file_parser,
                                          address_mapper=self.address_mapper,
                                          console_outstream=console_outstream,
                                          workspace=workspace,
                                          scheduler=scheduler)
    return context

  def tearDown(self):
    """
    :API: public
    """
    super(BaseTest, self).tearDown()
    BuildFile.clear_cache()
    Subsystem.reset()

  def target(self, spec):
    """Resolves the given target address to a Target object.

    :API: public

    address: The BUILD target address to resolve.

    Returns the corresponding Target or else None if the address does not point to a defined Target.
    """
    address = Address.parse(spec)
    self.build_graph.inject_address_closure(address)
    return self.build_graph.get_target(address)

  def targets(self, spec):
    """Resolves a target spec to one or more Target objects.

    :API: public

    spec: Either BUILD target address or else a target glob using the siblings ':' or
          descendants '::' suffixes.

    Returns the set of all Targets found.
    """

    spec = CmdLineSpecParser(self.build_root).parse_spec(spec)
    addresses = list(self.address_mapper.scan_specs([spec]))
    for address in addresses:
      self.build_graph.inject_address_closure(address)
    targets = [self.build_graph.get_target(address) for address in addresses]
    return targets

  def create_files(self, path, files):
    """Writes to a file under the buildroot with contents same as file name.

    :API: public

     path:  The relative path to the file from the build root.
     files: List of file names.
    """
    for f in files:
      self.create_file(os.path.join(path, f), contents=f)

  def create_library(self, path, target_type, name, sources=None, **kwargs):
    """Creates a library target of given type at the BUILD file at path with sources

    :API: public

     path: The relative path to the BUILD file from the build root.
     target_type: valid pants target type.
     name: Name of the library target.
     sources: List of source file at the path relative to path.
     **kwargs: Optional attributes that can be set for any library target.
       Currently it includes support for resources, java_sources, provides
       and dependencies.
    """
    if sources:
      self.create_files(path, sources)
    self.add_to_build_file(path, dedent('''
          %(target_type)s(name='%(name)s',
            %(sources)s
            %(java_sources)s
            %(provides)s
            %(dependencies)s
          )
        ''' % dict(target_type=target_type,
                   name=name,
                   sources=('sources=%s,' % repr(sources)
                              if sources else ''),
                   java_sources=('java_sources=[%s],'
                                 % ','.join('"%s"' % str_target for str_target in kwargs.get('java_sources'))
                                 if 'java_sources' in kwargs else ''),
                   provides=('provides=%s,' % kwargs.get('provides')
                              if 'provides' in kwargs else ''),
                   dependencies=('dependencies=%s,' % kwargs.get('dependencies')
                              if 'dependencies' in kwargs else ''),
                   )))
    return self.target('%s:%s' % (path, name))

  def create_resources(self, path, name, *sources):
    """
    :API: public
    """
    return self.create_library(path, 'resources', name, sources)

  def assertUnorderedPrefixEqual(self, expected, actual_iter):
    """Consumes len(expected) items from the given iter, and asserts that they match, unordered.

    :API: public
    """
    actual = list(itertools.islice(actual_iter, len(expected)))
    self.assertEqual(sorted(expected), sorted(actual))

  def assertPrefixEqual(self, expected, actual_iter):
    """Consumes len(expected) items from the given iter, and asserts that they match, in order.

    :API: public
    """
    self.assertEqual(expected, list(itertools.islice(actual_iter, len(expected))))

  def assertInFile(self, string, file_path):
    """Verifies that a string appears in a file

    :API: public
    """

    with open(file_path, 'r') as f:
      content = f.read()
      self.assertIn(string, content, '"{}" is not in the file {}:\n{}'.format(string, f.name, content))

  def get_bootstrap_options(self, cli_options=()):
    """Retrieves bootstrap options.

    :param cli_options: An iterable of CLI flags to pass as arguments to `OptionsBootstrapper`.
    """
    # Can't parse any options without a pants.ini.
    self.create_file('pants.ini')
    return OptionsBootstrapper.create(args=cli_options).bootstrap_options.for_global_scope()

  class LoggingRecorder(object):
    """Simple logging handler to record warnings."""

    def __init__(self):
      self._records = []
      self.level = logging.DEBUG

    def handle(self, record):
      self._records.append(record)

    def _messages_for_level(self, levelname):
      return ['{}: {}'.format(record.name, record.getMessage())
              for record in self._records if record.levelname == levelname]

    def infos(self):
      return self._messages_for_level('INFO')

    def warnings(self):
      return self._messages_for_level('WARNING')

  @contextmanager
  def captured_logging(self, level=None):
    root_logger = logging.getLogger()

    old_level = root_logger.level
    root_logger.setLevel(level or logging.NOTSET)

    handler = self.LoggingRecorder()
    root_logger.addHandler(handler)
    try:
      yield handler
    finally:
      root_logger.setLevel(old_level)
      root_logger.removeHandler(handler)
Пример #2
0
class LoaderTest(unittest.TestCase):

  def setUp(self):
    self.build_configuration = BuildConfiguration()
    self.working_set = WorkingSet()
    for entry in working_set.entries:
      self.working_set.add_entry(entry)

  def tearDown(self):
    Goal.clear()

  @contextmanager
  def create_register(self, build_file_aliases=None, register_goals=None, global_subsystems=None,
                      rules=None, module_name='register'):

    package_name = '__test_package_{0}'.format(uuid.uuid4().hex)
    if PY2:
      package_name = package_name.encode('utf-8')
    self.assertFalse(package_name in sys.modules)

    package_module = types.ModuleType(package_name)
    sys.modules[package_name] = package_module
    try:
      register_module_fqn = '{0}.{1}'.format(package_name, module_name)
      if PY2:
        register_module_fqn = register_module_fqn.encode('utf-8')
      register_module = types.ModuleType(register_module_fqn)
      setattr(package_module, module_name, register_module)
      sys.modules[register_module_fqn] = register_module

      def register_entrypoint(function_name, function):
        if function:
          setattr(register_module, function_name, function)

      register_entrypoint('build_file_aliases', build_file_aliases)
      register_entrypoint('global_subsystems', global_subsystems)
      register_entrypoint('register_goals', register_goals)
      register_entrypoint('rules', rules)

      yield package_name
    finally:
      del sys.modules[package_name]

  def assert_empty_aliases(self):
    registered_aliases = self.build_configuration.registered_aliases()
    self.assertEqual(0, len(registered_aliases.target_types))
    self.assertEqual(0, len(registered_aliases.target_macro_factories))
    self.assertEqual(0, len(registered_aliases.objects))
    self.assertEqual(0, len(registered_aliases.context_aware_object_factories))
    self.assertEqual(self.build_configuration.optionables(), set())
    self.assertEqual(0, len(self.build_configuration.rules()))

  def test_load_valid_empty(self):
    with self.create_register() as backend_package:
      load_backend(self.build_configuration, backend_package)
      self.assert_empty_aliases()

  def test_load_valid_partial_aliases(self):
    aliases = BuildFileAliases(targets={'bob': DummyTarget},
                               objects={'obj1': DummyObject1,
                                        'obj2': DummyObject2})
    with self.create_register(build_file_aliases=lambda: aliases) as backend_package:
      load_backend(self.build_configuration, backend_package)
      registered_aliases = self.build_configuration.registered_aliases()
      self.assertEqual(DummyTarget, registered_aliases.target_types['bob'])
      self.assertEqual(DummyObject1, registered_aliases.objects['obj1'])
      self.assertEqual(DummyObject2, registered_aliases.objects['obj2'])
      self.assertEqual(self.build_configuration.optionables(), {DummySubsystem1, DummySubsystem2})

  def test_load_valid_partial_goals(self):
    def register_goals():
      Goal.by_name('jack').install(TaskRegistrar('jill', DummyTask))

    with self.create_register(register_goals=register_goals) as backend_package:
      Goal.clear()
      self.assertEqual(0, len(Goal.all()))

      load_backend(self.build_configuration, backend_package)
      self.assert_empty_aliases()
      self.assertEqual(1, len(Goal.all()))

      task_names = Goal.by_name('jack').ordered_task_names()
      self.assertEqual(1, len(task_names))

      task_name = task_names[0]
      self.assertEqual('jill', task_name)

  def test_load_invalid_entrypoint(self):
    def build_file_aliases(bad_arg):
      return BuildFileAliases()

    with self.create_register(build_file_aliases=build_file_aliases) as backend_package:
      with self.assertRaises(BuildConfigurationError):
        load_backend(self.build_configuration, backend_package)

  def test_load_invalid_module(self):
    with self.create_register(module_name='register2') as backend_package:
      with self.assertRaises(BuildConfigurationError):
        load_backend(self.build_configuration, backend_package)

  def test_load_missing_plugin(self):
    with self.assertRaises(PluginNotFound):
      self.load_plugins(['Foobar'])

  def get_mock_plugin(self, name, version, reg=None, alias=None, after=None, rules=None):
    """Make a fake Distribution (optionally with entry points)

    Note the entry points do not actually point to code in the returned distribution --
    the distribution does not even have a location and does not contain any code, just metadata.

    A module is synthesized on the fly and installed into sys.modules under a random name.
    If optional entry point callables are provided, those are added as methods to the module and
    their name (foo/bar/baz in fake module) is added as the requested entry point to the mocked
    metadata added to the returned dist.

    :param string name: project_name for distribution (see pkg_resources)
    :param string version: version for distribution (see pkg_resources)
    :param callable reg: Optional callable for goal registration entry point
    :param callable alias: Optional callable for build_file_aliases entry point
    :param callable after: Optional callable for load_after list entry point
    :param callable rules: Optional callable for rules entry point
    """

    plugin_pkg = 'demoplugin{0}'.format(uuid.uuid4().hex)
    if PY2:
      plugin_pkg = plugin_pkg.encode('utf-8')
    pkg = types.ModuleType(plugin_pkg)
    sys.modules[plugin_pkg] = pkg
    module_name = '{0}.{1}'.format(plugin_pkg, 'demo')
    if PY2:
      module_name = module_name.encode('utf-8')
    plugin = types.ModuleType(module_name)
    setattr(pkg, 'demo', plugin)
    sys.modules[module_name] = plugin

    metadata = {}
    entry_lines = []

    if reg is not None:
      setattr(plugin, 'foo', reg)
      entry_lines.append('register_goals = {}:foo\n'.format(module_name))

    if alias is not None:
      setattr(plugin, 'bar', alias)
      entry_lines.append('build_file_aliases = {}:bar\n'.format(module_name))

    if after is not None:
      setattr(plugin, 'baz', after)
      entry_lines.append('load_after = {}:baz\n'.format(module_name))

    if rules is not None:
      setattr(plugin, 'qux', rules)
      entry_lines.append('rules = {}:qux\n'.format(module_name))

    if entry_lines:
      entry_data = '[pantsbuild.plugin]\n{}\n'.format('\n'.join(entry_lines))
      metadata = {'entry_points.txt': entry_data}

    return Distribution(project_name=name, version=version, metadata=MockMetadata(metadata))

  def load_plugins(self, plugins):
    load_plugins(self.build_configuration, plugins, self.working_set)

  def test_plugin_load_and_order(self):
    d1 = self.get_mock_plugin('demo1', '0.0.1', after=lambda: ['demo2'])
    d2 = self.get_mock_plugin('demo2', '0.0.3')
    self.working_set.add(d1)

    # Attempting to load 'demo1' then 'demo2' should fail as 'demo1' requires 'after'=['demo2'].
    with self.assertRaises(PluginLoadOrderError):
      self.load_plugins(['demo1', 'demo2'])

    # Attempting to load 'demo2' first should fail as it is not (yet) installed.
    with self.assertRaises(PluginNotFound):
      self.load_plugins(['demo2', 'demo1'])

    # Installing demo2 and then loading in correct order should work though.
    self.working_set.add(d2)
    self.load_plugins(['demo2>=0.0.2', 'demo1'])

    # But asking for a bad (not installed) version fails.
    with self.assertRaises(VersionConflict):
      self.load_plugins(['demo2>=0.0.5'])

  def test_plugin_installs_goal(self):
    def reg_goal():
      Goal.by_name('plugindemo').install(TaskRegistrar('foo', DummyTask))
    self.working_set.add(self.get_mock_plugin('regdemo', '0.0.1', reg=reg_goal))

    # Start without the custom goal.
    self.assertEqual(0, len(Goal.by_name('plugindemo').ordered_task_names()))

    # Load plugin which registers custom goal.
    self.load_plugins(['regdemo'])

    # Now the custom goal exists.
    self.assertEqual(1, len(Goal.by_name('plugindemo').ordered_task_names()))
    self.assertEqual('foo', Goal.by_name('plugindemo').ordered_task_names()[0])

  def test_plugin_installs_alias(self):
    def reg_alias():
      return BuildFileAliases(targets={'pluginalias': DummyTarget},
                              objects={'FROMPLUGIN1': DummyObject1,
                                       'FROMPLUGIN2': DummyObject2})
    self.working_set.add(self.get_mock_plugin('aliasdemo', '0.0.1', alias=reg_alias))

    # Start with no aliases.
    self.assert_empty_aliases()

    # Now load the plugin which defines aliases.
    self.load_plugins(['aliasdemo'])

    # Aliases now exist.
    registered_aliases = self.build_configuration.registered_aliases()
    self.assertEqual(DummyTarget, registered_aliases.target_types['pluginalias'])
    self.assertEqual(DummyObject1, registered_aliases.objects['FROMPLUGIN1'])
    self.assertEqual(DummyObject2, registered_aliases.objects['FROMPLUGIN2'])
    self.assertEqual(self.build_configuration.optionables(), {DummySubsystem1, DummySubsystem2})

  def test_subsystems(self):
    def global_subsystems():
      return {DummySubsystem1, DummySubsystem2}
    with self.create_register(global_subsystems=global_subsystems) as backend_package:
      load_backend(self.build_configuration, backend_package)
      self.assertEqual(self.build_configuration.optionables(),
                       {DummySubsystem1, DummySubsystem2})

  def test_rules(self):
    def backend_rules():
      return [example_rule, RootRule(RootType)]
    with self.create_register(rules=backend_rules) as backend_package:
      load_backend(self.build_configuration, backend_package)
      self.assertEqual(self.build_configuration.rules(),
                       [example_rule.rule, RootRule(RootType)])

    def plugin_rules():
      return [example_plugin_rule]

    self.working_set.add(self.get_mock_plugin('this-plugin-rules', '0.0.1', rules=plugin_rules))
    self.load_plugins(['this-plugin-rules'])
    self.assertEqual(self.build_configuration.rules(),
                     [example_rule.rule, RootRule(RootType), example_plugin_rule.rule])

  def test_backend_plugin_ordering(self):
    def reg_alias():
      return BuildFileAliases(targets={'override-alias': DummyTarget2})
    self.working_set.add(self.get_mock_plugin('pluginalias', '0.0.1', alias=reg_alias))
    plugins=['pluginalias==0.0.1']
    aliases = BuildFileAliases(targets={'override-alias': DummyTarget})
    with self.create_register(build_file_aliases=lambda: aliases) as backend_module:
      backends=[backend_module]
      load_backends_and_plugins(plugins, self.working_set, backends,
                                build_configuration=self.build_configuration)
    # The backend should load first, then the plugins, therefore the alias registered in
    # the plugin will override the alias registered by the backend
    registered_aliases = self.build_configuration.registered_aliases()
    self.assertEqual(DummyTarget2, registered_aliases.target_types['override-alias'])
class LoaderTest(unittest.TestCase):
    def setUp(self):
        self.build_configuration = BuildConfiguration()
        self.working_set = WorkingSet()
        for entry in working_set.entries:
            self.working_set.add_entry(entry)

    def tearDown(self):
        Goal.clear()

    @contextmanager
    def create_register(self,
                        build_file_aliases=None,
                        register_goals=None,
                        global_subsystems=None,
                        rules=None,
                        module_name='register'):

        package_name = '__test_package_{0}'.format(uuid.uuid4().hex)
        if PY2:
            package_name = package_name.encode('utf-8')
        self.assertFalse(package_name in sys.modules)

        package_module = types.ModuleType(package_name)
        sys.modules[package_name] = package_module
        try:
            register_module_fqn = '{0}.{1}'.format(package_name, module_name)
            if PY2:
                register_module_fqn = register_module_fqn.encode('utf-8')
            register_module = types.ModuleType(register_module_fqn)
            setattr(package_module, module_name, register_module)
            sys.modules[register_module_fqn] = register_module

            def register_entrypoint(function_name, function):
                if function:
                    setattr(register_module, function_name, function)

            register_entrypoint('build_file_aliases', build_file_aliases)
            register_entrypoint('global_subsystems', global_subsystems)
            register_entrypoint('register_goals', register_goals)
            register_entrypoint('rules', rules)

            yield package_name
        finally:
            del sys.modules[package_name]

    def assert_empty_aliases(self):
        registered_aliases = self.build_configuration.registered_aliases()
        self.assertEqual(0, len(registered_aliases.target_types))
        self.assertEqual(0, len(registered_aliases.target_macro_factories))
        self.assertEqual(0, len(registered_aliases.objects))
        self.assertEqual(
            0, len(registered_aliases.context_aware_object_factories))
        self.assertEqual(self.build_configuration.optionables(), set())
        self.assertEqual(0, len(self.build_configuration.rules()))

    def test_load_valid_empty(self):
        with self.create_register() as backend_package:
            load_backend(self.build_configuration, backend_package)
            self.assert_empty_aliases()

    def test_load_valid_partial_aliases(self):
        aliases = BuildFileAliases(targets={'bob': DummyTarget},
                                   objects={
                                       'obj1': DummyObject1,
                                       'obj2': DummyObject2
                                   })
        with self.create_register(
                build_file_aliases=lambda: aliases) as backend_package:
            load_backend(self.build_configuration, backend_package)
            registered_aliases = self.build_configuration.registered_aliases()
            self.assertEqual(DummyTarget,
                             registered_aliases.target_types['bob'])
            self.assertEqual(DummyObject1, registered_aliases.objects['obj1'])
            self.assertEqual(DummyObject2, registered_aliases.objects['obj2'])
            self.assertEqual(self.build_configuration.optionables(),
                             {DummySubsystem1, DummySubsystem2})

    def test_load_valid_partial_goals(self):
        def register_goals():
            Goal.by_name('jack').install(TaskRegistrar('jill', DummyTask))

        with self.create_register(
                register_goals=register_goals) as backend_package:
            Goal.clear()
            self.assertEqual(0, len(Goal.all()))

            load_backend(self.build_configuration, backend_package)
            self.assert_empty_aliases()
            self.assertEqual(1, len(Goal.all()))

            task_names = Goal.by_name('jack').ordered_task_names()
            self.assertEqual(1, len(task_names))

            task_name = task_names[0]
            self.assertEqual('jill', task_name)

    def test_load_invalid_entrypoint(self):
        def build_file_aliases(bad_arg):
            return BuildFileAliases()

        with self.create_register(
                build_file_aliases=build_file_aliases) as backend_package:
            with self.assertRaises(BuildConfigurationError):
                load_backend(self.build_configuration, backend_package)

    def test_load_invalid_module(self):
        with self.create_register(module_name='register2') as backend_package:
            with self.assertRaises(BuildConfigurationError):
                load_backend(self.build_configuration, backend_package)

    def test_load_missing_plugin(self):
        with self.assertRaises(PluginNotFound):
            self.load_plugins(['Foobar'])

    def get_mock_plugin(self,
                        name,
                        version,
                        reg=None,
                        alias=None,
                        after=None,
                        rules=None):
        """Make a fake Distribution (optionally with entry points)

    Note the entry points do not actually point to code in the returned distribution --
    the distribution does not even have a location and does not contain any code, just metadata.

    A module is synthesized on the fly and installed into sys.modules under a random name.
    If optional entry point callables are provided, those are added as methods to the module and
    their name (foo/bar/baz in fake module) is added as the requested entry point to the mocked
    metadata added to the returned dist.

    :param string name: project_name for distribution (see pkg_resources)
    :param string version: version for distribution (see pkg_resources)
    :param callable reg: Optional callable for goal registration entry point
    :param callable alias: Optional callable for build_file_aliases entry point
    :param callable after: Optional callable for load_after list entry point
    :param callable rules: Optional callable for rules entry point
    """

        plugin_pkg = 'demoplugin{0}'.format(uuid.uuid4().hex)
        if PY2:
            plugin_pkg = plugin_pkg.encode('utf-8')
        pkg = types.ModuleType(plugin_pkg)
        sys.modules[plugin_pkg] = pkg
        module_name = '{0}.{1}'.format(plugin_pkg, 'demo')
        if PY2:
            module_name = module_name.encode('utf-8')
        plugin = types.ModuleType(module_name)
        setattr(pkg, 'demo', plugin)
        sys.modules[module_name] = plugin

        metadata = {}
        entry_lines = []

        if reg is not None:
            setattr(plugin, 'foo', reg)
            entry_lines.append('register_goals = {}:foo\n'.format(module_name))

        if alias is not None:
            setattr(plugin, 'bar', alias)
            entry_lines.append(
                'build_file_aliases = {}:bar\n'.format(module_name))

        if after is not None:
            setattr(plugin, 'baz', after)
            entry_lines.append('load_after = {}:baz\n'.format(module_name))

        if rules is not None:
            setattr(plugin, 'qux', rules)
            entry_lines.append('rules = {}:qux\n'.format(module_name))

        if entry_lines:
            entry_data = '[pantsbuild.plugin]\n{}\n'.format(
                '\n'.join(entry_lines))
            metadata = {'entry_points.txt': entry_data}

        return Distribution(project_name=name,
                            version=version,
                            metadata=MockMetadata(metadata))

    def load_plugins(self, plugins):
        load_plugins(self.build_configuration, plugins, self.working_set)

    def test_plugin_load_and_order(self):
        d1 = self.get_mock_plugin('demo1', '0.0.1', after=lambda: ['demo2'])
        d2 = self.get_mock_plugin('demo2', '0.0.3')
        self.working_set.add(d1)

        # Attempting to load 'demo1' then 'demo2' should fail as 'demo1' requires 'after'=['demo2'].
        with self.assertRaises(PluginLoadOrderError):
            self.load_plugins(['demo1', 'demo2'])

        # Attempting to load 'demo2' first should fail as it is not (yet) installed.
        with self.assertRaises(PluginNotFound):
            self.load_plugins(['demo2', 'demo1'])

        # Installing demo2 and then loading in correct order should work though.
        self.working_set.add(d2)
        self.load_plugins(['demo2>=0.0.2', 'demo1'])

        # But asking for a bad (not installed) version fails.
        with self.assertRaises(VersionConflict):
            self.load_plugins(['demo2>=0.0.5'])

    def test_plugin_installs_goal(self):
        def reg_goal():
            Goal.by_name('plugindemo').install(TaskRegistrar('foo', DummyTask))

        self.working_set.add(
            self.get_mock_plugin('regdemo', '0.0.1', reg=reg_goal))

        # Start without the custom goal.
        self.assertEqual(0,
                         len(Goal.by_name('plugindemo').ordered_task_names()))

        # Load plugin which registers custom goal.
        self.load_plugins(['regdemo'])

        # Now the custom goal exists.
        self.assertEqual(1,
                         len(Goal.by_name('plugindemo').ordered_task_names()))
        self.assertEqual('foo',
                         Goal.by_name('plugindemo').ordered_task_names()[0])

    def test_plugin_installs_alias(self):
        def reg_alias():
            return BuildFileAliases(targets={'pluginalias': DummyTarget},
                                    objects={
                                        'FROMPLUGIN1': DummyObject1,
                                        'FROMPLUGIN2': DummyObject2
                                    })

        self.working_set.add(
            self.get_mock_plugin('aliasdemo', '0.0.1', alias=reg_alias))

        # Start with no aliases.
        self.assert_empty_aliases()

        # Now load the plugin which defines aliases.
        self.load_plugins(['aliasdemo'])

        # Aliases now exist.
        registered_aliases = self.build_configuration.registered_aliases()
        self.assertEqual(DummyTarget,
                         registered_aliases.target_types['pluginalias'])
        self.assertEqual(DummyObject1,
                         registered_aliases.objects['FROMPLUGIN1'])
        self.assertEqual(DummyObject2,
                         registered_aliases.objects['FROMPLUGIN2'])
        self.assertEqual(self.build_configuration.optionables(),
                         {DummySubsystem1, DummySubsystem2})

    def test_subsystems(self):
        def global_subsystems():
            return {DummySubsystem1, DummySubsystem2}

        with self.create_register(
                global_subsystems=global_subsystems) as backend_package:
            load_backend(self.build_configuration, backend_package)
            self.assertEqual(self.build_configuration.optionables(),
                             {DummySubsystem1, DummySubsystem2})

    def test_rules(self):
        def backend_rules():
            return [example_rule, RootRule(RootType)]

        with self.create_register(rules=backend_rules) as backend_package:
            load_backend(self.build_configuration, backend_package)
            self.assertEqual(
                self.build_configuration.rules(),
                [example_rule.rule, RootRule(RootType)])

        def plugin_rules():
            return [example_plugin_rule]

        self.working_set.add(
            self.get_mock_plugin('this-plugin-rules',
                                 '0.0.1',
                                 rules=plugin_rules))
        self.load_plugins(['this-plugin-rules'])
        self.assertEqual(
            self.build_configuration.rules(),
            [example_rule.rule,
             RootRule(RootType), example_plugin_rule.rule])

    def test_backend_plugin_ordering(self):
        def reg_alias():
            return BuildFileAliases(targets={'override-alias': DummyTarget2})

        self.working_set.add(
            self.get_mock_plugin('pluginalias', '0.0.1', alias=reg_alias))
        plugins = ['pluginalias==0.0.1']
        aliases = BuildFileAliases(targets={'override-alias': DummyTarget})
        with self.create_register(
                build_file_aliases=lambda: aliases) as backend_module:
            backends = [backend_module]
            load_backends_and_plugins(
                plugins,
                self.working_set,
                backends,
                build_configuration=self.build_configuration)
        # The backend should load first, then the plugins, therefore the alias registered in
        # the plugin will override the alias registered by the backend
        registered_aliases = self.build_configuration.registered_aliases()
        self.assertEqual(DummyTarget2,
                         registered_aliases.target_types['override-alias'])
Пример #4
0
class LoaderTest(unittest.TestCase):
    def setUp(self):
        self.build_configuration = BuildConfiguration()
        self.working_set = WorkingSet()
        for entry in working_set.entries:
            self.working_set.add_entry(entry)

    def tearDown(self):
        Goal.clear()

    @contextmanager
    def create_register(
        self,
        build_file_aliases=None,
        register_goals=None,
        global_subsystems=None,
        rules=None,
        module_name="register",
    ):

        package_name = f"__test_package_{uuid.uuid4().hex}"
        self.assertFalse(package_name in sys.modules)

        package_module = types.ModuleType(package_name)
        sys.modules[package_name] = package_module
        try:
            register_module_fqn = f"{package_name}.{module_name}"
            register_module = types.ModuleType(register_module_fqn)
            setattr(package_module, module_name, register_module)
            sys.modules[register_module_fqn] = register_module

            def register_entrypoint(function_name, function):
                if function:
                    setattr(register_module, function_name, function)

            register_entrypoint("build_file_aliases", build_file_aliases)
            register_entrypoint("global_subsystems", global_subsystems)
            register_entrypoint("register_goals", register_goals)
            register_entrypoint("rules", rules)

            yield package_name
        finally:
            del sys.modules[package_name]

    def assert_empty_aliases(self):
        registered_aliases = self.build_configuration.registered_aliases()
        self.assertEqual(0, len(registered_aliases.target_types))
        self.assertEqual(0, len(registered_aliases.target_macro_factories))
        self.assertEqual(0, len(registered_aliases.objects))
        self.assertEqual(
            0, len(registered_aliases.context_aware_object_factories))
        self.assertEqual(self.build_configuration.optionables(), OrderedSet())
        self.assertEqual(0, len(self.build_configuration.rules()))

    def test_load_valid_empty(self):
        with self.create_register() as backend_package:
            load_backend(self.build_configuration,
                         backend_package,
                         is_v1_backend=True)
            self.assert_empty_aliases()

    def test_load_valid_partial_aliases(self):
        aliases = BuildFileAliases(targets={"bob": DummyTarget},
                                   objects={
                                       "obj1": DummyObject1,
                                       "obj2": DummyObject2
                                   })
        with self.create_register(
                build_file_aliases=lambda: aliases) as backend_package:
            load_backend(self.build_configuration,
                         backend_package,
                         is_v1_backend=False)
            self.assert_empty_aliases()

            load_backend(self.build_configuration,
                         backend_package,
                         is_v1_backend=True)
            registered_aliases = self.build_configuration.registered_aliases()
            self.assertEqual(DummyTarget,
                             registered_aliases.target_types["bob"])
            self.assertEqual(DummyObject1, registered_aliases.objects["obj1"])
            self.assertEqual(DummyObject2, registered_aliases.objects["obj2"])
            self.assertEqual(
                self.build_configuration.optionables(),
                OrderedSet([DummySubsystem1, DummySubsystem2]),
            )

    def test_load_valid_partial_goals(self):
        def register_goals():
            Goal.by_name("jack").install(TaskRegistrar("jill", DummyTask))

        with self.create_register(
                register_goals=register_goals) as backend_package:
            Goal.clear()
            self.assertEqual(0, len(Goal.all()))

            load_backend(self.build_configuration,
                         backend_package,
                         is_v1_backend=False)
            self.assertEqual(0, len(Goal.all()))

            load_backend(self.build_configuration,
                         backend_package,
                         is_v1_backend=True)
            self.assert_empty_aliases()
            self.assertEqual(1, len(Goal.all()))

            task_names = Goal.by_name("jack").ordered_task_names()
            self.assertEqual(1, len(task_names))

            task_name = task_names[0]
            self.assertEqual("jill", task_name)

    def test_load_invalid_entrypoint(self):
        def build_file_aliases(bad_arg):
            return BuildFileAliases()

        with self.create_register(
                build_file_aliases=build_file_aliases) as backend_package:
            # Loading as v2 shouldn't raise.
            load_backend(self.build_configuration,
                         backend_package,
                         is_v1_backend=False)
            with self.assertRaises(BuildConfigurationError):
                load_backend(self.build_configuration,
                             backend_package,
                             is_v1_backend=True)

    def test_load_invalid_module(self):
        with self.create_register(module_name="register2") as backend_package:
            with self.assertRaises(BuildConfigurationError):
                load_backend(self.build_configuration,
                             backend_package,
                             is_v1_backend=False)
            with self.assertRaises(BuildConfigurationError):
                load_backend(self.build_configuration,
                             backend_package,
                             is_v1_backend=True)

    def test_load_missing_plugin(self):
        with self.assertRaises(PluginNotFound):
            self.load_plugins(["Foobar"], is_v1_plugin=True)
        with self.assertRaises(PluginNotFound):
            self.load_plugins(["Foobar"], is_v1_plugin=False)

    @staticmethod
    def get_mock_plugin(name,
                        version,
                        reg=None,
                        alias=None,
                        after=None,
                        rules=None):
        """Make a fake Distribution (optionally with entry points)

        Note the entry points do not actually point to code in the returned distribution --
        the distribution does not even have a location and does not contain any code, just metadata.

        A module is synthesized on the fly and installed into sys.modules under a random name.
        If optional entry point callables are provided, those are added as methods to the module and
        their name (foo/bar/baz in fake module) is added as the requested entry point to the mocked
        metadata added to the returned dist.

        :param string name: project_name for distribution (see pkg_resources)
        :param string version: version for distribution (see pkg_resources)
        :param callable reg: Optional callable for goal registration entry point
        :param callable alias: Optional callable for build_file_aliases entry point
        :param callable after: Optional callable for load_after list entry point
        :param callable rules: Optional callable for rules entry point
        """

        plugin_pkg = f"demoplugin{uuid.uuid4().hex}"
        pkg = types.ModuleType(plugin_pkg)
        sys.modules[plugin_pkg] = pkg
        module_name = f"{plugin_pkg}.demo"
        plugin = types.ModuleType(module_name)
        setattr(pkg, "demo", plugin)
        sys.modules[module_name] = plugin

        metadata = {}
        entry_lines = []

        if reg is not None:
            setattr(plugin, "foo", reg)
            entry_lines.append(f"register_goals = {module_name}:foo\n")

        if alias is not None:
            setattr(plugin, "bar", alias)
            entry_lines.append(f"build_file_aliases = {module_name}:bar\n")

        if after is not None:
            setattr(plugin, "baz", after)
            entry_lines.append(f"load_after = {module_name}:baz\n")

        if rules is not None:
            setattr(plugin, "qux", rules)
            entry_lines.append(f"rules = {module_name}:qux\n")

        if entry_lines:
            entry_data = "[pantsbuild.plugin]\n{}\n".format(
                "\n".join(entry_lines))
            metadata = {"entry_points.txt": entry_data}

        return Distribution(project_name=name,
                            version=version,
                            metadata=MockMetadata(metadata))

    def load_plugins(self, plugins, is_v1_plugin):
        load_plugins(self.build_configuration, plugins, self.working_set,
                     is_v1_plugin)

    def test_plugin_load_and_order(self):
        d1 = self.get_mock_plugin("demo1", "0.0.1", after=lambda: ["demo2"])
        d2 = self.get_mock_plugin("demo2", "0.0.3")
        self.working_set.add(d1)

        # Attempting to load 'demo1' then 'demo2' should fail as 'demo1' requires 'after'=['demo2'].
        with self.assertRaises(PluginLoadOrderError):
            self.load_plugins(["demo1", "demo2"], is_v1_plugin=False)
        with self.assertRaises(PluginLoadOrderError):
            self.load_plugins(["demo1", "demo2"], is_v1_plugin=True)

        # Attempting to load 'demo2' first should fail as it is not (yet) installed.
        with self.assertRaises(PluginNotFound):
            self.load_plugins(["demo2", "demo1"], is_v1_plugin=False)
        with self.assertRaises(PluginNotFound):
            self.load_plugins(["demo2", "demo1"], is_v1_plugin=True)

        # Installing demo2 and then loading in correct order should work though.
        self.working_set.add(d2)
        self.load_plugins(["demo2>=0.0.2", "demo1"], is_v1_plugin=False)
        self.load_plugins(["demo2>=0.0.2", "demo1"], is_v1_plugin=True)

        # But asking for a bad (not installed) version fails.
        with self.assertRaises(VersionConflict):
            self.load_plugins(["demo2>=0.0.5"], is_v1_plugin=False)
        with self.assertRaises(VersionConflict):
            self.load_plugins(["demo2>=0.0.5"], is_v1_plugin=True)

    def test_plugin_installs_goal(self):
        def reg_goal():
            Goal.by_name("plugindemo").install(TaskRegistrar("foo", DummyTask))

        self.working_set.add(
            self.get_mock_plugin("regdemo", "0.0.1", reg=reg_goal))

        # Start without the custom goal.
        self.assertEqual(0,
                         len(Goal.by_name("plugindemo").ordered_task_names()))

        # Ensure goal isn't created in a v2 world.
        self.load_plugins(["regdemo"], is_v1_plugin=False)
        self.assertEqual(0,
                         len(Goal.by_name("plugindemo").ordered_task_names()))

        # Load plugin which registers custom goal.
        self.load_plugins(["regdemo"], is_v1_plugin=True)

        # Now the custom goal exists.
        self.assertEqual(1,
                         len(Goal.by_name("plugindemo").ordered_task_names()))
        self.assertEqual("foo",
                         Goal.by_name("plugindemo").ordered_task_names()[0])

    def test_plugin_installs_alias(self):
        def reg_alias():
            return BuildFileAliases(
                targets={"pluginalias": DummyTarget},
                objects={
                    "FROMPLUGIN1": DummyObject1,
                    "FROMPLUGIN2": DummyObject2
                },
            )

        self.working_set.add(
            self.get_mock_plugin("aliasdemo", "0.0.1", alias=reg_alias))

        # Start with no aliases.
        self.assert_empty_aliases()

        # Ensure alias isn't created in a v2 world.
        self.load_plugins(["aliasdemo"], is_v1_plugin=False)
        self.assert_empty_aliases()

        # Now load the plugin which defines aliases.
        self.load_plugins(["aliasdemo"], is_v1_plugin=True)

        # Aliases now exist.
        registered_aliases = self.build_configuration.registered_aliases()
        self.assertEqual(DummyTarget,
                         registered_aliases.target_types["pluginalias"])
        self.assertEqual(DummyObject1,
                         registered_aliases.objects["FROMPLUGIN1"])
        self.assertEqual(DummyObject2,
                         registered_aliases.objects["FROMPLUGIN2"])
        self.assertEqual(self.build_configuration.optionables(),
                         OrderedSet([DummySubsystem1, DummySubsystem2]))

    def test_subsystems(self):
        def global_subsystems():
            return [DummySubsystem1, DummySubsystem2]

        with self.create_register(
                global_subsystems=global_subsystems) as backend_package:
            load_backend(self.build_configuration,
                         backend_package,
                         is_v1_backend=False)
            self.assertEqual(self.build_configuration.optionables(),
                             OrderedSet())

            load_backend(self.build_configuration,
                         backend_package,
                         is_v1_backend=True)
            self.assertEqual(
                self.build_configuration.optionables(),
                OrderedSet([DummySubsystem1, DummySubsystem2]),
            )

    def test_rules(self):
        def backend_rules():
            return [example_rule, RootRule(RootType)]

        with self.create_register(rules=backend_rules) as backend_package:
            load_backend(self.build_configuration,
                         backend_package,
                         is_v1_backend=True)
            self.assertEqual(self.build_configuration.rules(), [])

            load_backend(self.build_configuration,
                         backend_package,
                         is_v1_backend=False)
            self.assertEqual(
                self.build_configuration.rules(),
                [example_rule.rule, RootRule(RootType)])

        def plugin_rules():
            return [example_plugin_rule]

        self.working_set.add(
            self.get_mock_plugin("this-plugin-rules",
                                 "0.0.1",
                                 rules=plugin_rules))
        self.load_plugins(["this-plugin-rules"], is_v1_plugin=True)
        self.assertEqual(
            self.build_configuration.rules(),
            [example_rule.rule, RootRule(RootType)])

        self.load_plugins(["this-plugin-rules"], is_v1_plugin=False)
        self.assertEqual(
            self.build_configuration.rules(),
            [example_rule.rule,
             RootRule(RootType), example_plugin_rule.rule],
        )

    def test_backend_plugin_ordering(self):
        def reg_alias():
            return BuildFileAliases(targets={"override-alias": DummyTarget2})

        self.working_set.add(
            self.get_mock_plugin("pluginalias", "0.0.1", alias=reg_alias))
        plugins = ["pluginalias==0.0.1"]
        aliases = BuildFileAliases(targets={"override-alias": DummyTarget})
        with self.create_register(
                build_file_aliases=lambda: aliases) as backend_module:
            backends = [backend_module]
            load_backends_and_plugins(
                plugins,
                [],
                self.working_set,
                backends,
                [],
                build_configuration=self.build_configuration,
            )
        # The backend should load first, then the plugins, therefore the alias registered in
        # the plugin will override the alias registered by the backend
        registered_aliases = self.build_configuration.registered_aliases()
        self.assertEqual(DummyTarget2,
                         registered_aliases.target_types["override-alias"])