Skip to content

Commit

Permalink
Example of an additional planner that produces a Classpath for targets
Browse files Browse the repository at this point in the history
This demonstrates an explosion when multiple Classpath products are produced for a single target, which will likely need to be dealt with via [#2484](#2484). I'll start working on a way to cause the _absence_ of required Configuration for a Planner to explode early during planning, rather than later when there is ambiguity due to too-much Configuration.

- Add a semi-realistic BuildPropertiesPlanner, which promises to produce a Classpath entry
- Add an `xfail`d test that covers a target with both JavaSources and a BuildProperties config

Testing Done:
https://travis-ci.org/pantsbuild/pants/builds/89165764

Bugs closed: 2484, 2495

Reviewed at https://rbcommons.com/s/twitter/r/3075/
  • Loading branch information
stuhood committed Nov 5, 2015
1 parent c9177d4 commit 570939f
Show file tree
Hide file tree
Showing 6 changed files with 91 additions and 26 deletions.
43 changes: 41 additions & 2 deletions src/python/pants/engine/exp/examples/planners.py
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,42 @@ def gen_apache_thrift(sources, rev, gen, strict):
'gen:{}, strict: {}'.format(sources, rev, gen, strict))


class BuildPropertiesConfiguration(Configuration):
pass


class BuildPropertiesPlanner(TaskPlanner):
"""A planner that adds a Classpath entry for all targets configured for build_properties.
NB: In the absence of support for merging multiple Promises for a particular product_type,
this serves as a valid example that explodes when it should succeed.
"""

@property
def goal_name(self):
return None

@property
def product_types(self):
yield Classpath

def plan(self, scheduler, product_type, subject, configuration=None):
if not isinstance(subject, Target):
return
name_config = filter(lambda x: isinstance(x, BuildPropertiesConfiguration), subject.configurations)
if not name_config:
return
assert product_type == Classpath

return Plan(func_or_task_type=write_name_file, subjects=(subject,), name=subject.name)


def write_name_file(name):
# Write a file containing the name in CWD
with safe_open('build.properties') as f:
f.write('name={}\n'.format(name))


class ScroogeConfiguration(ThriftConfiguration):
def __init__(self, rev=None, lang=None, strict=True, **kwargs):
"""
Expand Down Expand Up @@ -408,7 +444,8 @@ def setup_json_scheduler(build_root):
'requirement': Requirement,
'scrooge_configuration': ScroogeConfiguration,
'sources': AddressableSources,
'target': Target}
'target': Target,
'build_properties': BuildPropertiesConfiguration}
json_parser = functools.partial(parse_json, symbol_table=symbol_table)
graph = Graph(AddressMapper(build_root=build_root,
build_pattern=r'^BLD.json$',
Expand All @@ -418,6 +455,8 @@ def setup_json_scheduler(build_root):
GlobalIvyResolvePlanner(),
JavacPlanner(),
ScalacPlanner(),
ScroogePlanner()])
ScroogePlanner(),
BuildPropertiesPlanner()
])
scheduler = LocalScheduler(graph, planners)
return graph, scheduler
46 changes: 24 additions & 22 deletions src/python/pants/engine/exp/scheduler.py
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,26 @@ class SchedulingError(Exception):
"""Indicates inability to make a required scheduling promise."""


class NoProducersError(SchedulingError):
"""Indicates no planners were able to promise a required product for a given subject."""

def __init__(self, product_type, subject=None):
msg = ('No plans to generate {!r}{} could be made.'
.format(product_type.__name__, ' {!r}'.format(subject) if subject else ''))
super(NoProducersError, self).__init__(msg)


class ConflictingProducersError(SchedulingError):
"""Indicates more than one planner was able to promise a product for a given subject."""

def __init__(self, product_type, subject, planners):
msg = ('Collected the following plans for generating {!r} from {!r}\n\t{}'
.format(product_type.__name__,
subject,
'\n\t'.join(type(p).__name__ for p in planners)))
super(ConflictingProducersError, self).__init__(msg)


class Scheduler(AbstractClass):
"""Schedule the creation of products."""

Expand Down Expand Up @@ -604,24 +624,6 @@ def promised(self, promise):
class LocalScheduler(Scheduler):
"""A scheduler that formulates an execution graph locally."""

class NoProducersError(SchedulingError):
"""Indicates no planners were able to promise a required product for a given subject."""

def __init__(self, product_type, subject=None):
msg = ('No plans to generate {!r}{} could be made.'
.format(product_type.__name__, ' {!r}'.format(subject) if subject else ''))
super(LocalScheduler.NoProducersError, self).__init__(msg)

class ConflictingProducersError(SchedulingError):
"""Indicates more than one planner was able to promise a product for a given subject."""

def __init__(self, product_type, subject, planners):
msg = ('Collected the following plans for generating {!r} from {!r}\n\t{}'
.format(product_type.__name__,
subject,
'\n\t'.join(type(p).__name__ for p in planners)))
super(LocalScheduler.ConflictingProducersError, self).__init__(msg)

def __init__(self, graph, planners):
"""
:param graph: The BUILD graph build requests will execute against.
Expand Down Expand Up @@ -694,12 +696,12 @@ def promise(self, subject, product_type, configuration=None, required=True):
if plan is not None:
# The `NO_PLAN` plan may have been decided in a non-required round.
if required and promise is self.NO_PLAN:
raise self.NoProducersError(product_type, subject)
raise NoProducersError(product_type, subject)
return None if promise is self.NO_PLAN else promise

planners = self._planners.for_product_type(product_type)
if required and not planners:
raise self.NoProducersError(product_type)
raise NoProducersError(product_type)

plans = []
for planner in planners:
Expand All @@ -709,10 +711,10 @@ def promise(self, subject, product_type, configuration=None, required=True):

if len(plans) > 1:
planners = [planner for planner, plan in plans]
raise self.ConflictingProducersError(product_type, subject, planners)
raise ConflictingProducersError(product_type, subject, planners)
elif not plans:
if required:
raise self.NoProducersError(product_type, subject)
raise NoProducersError(product_type, subject)
self._product_mapper.register_promises(product_type, self.NO_PLAN)
return None

Expand Down
3 changes: 2 additions & 1 deletion tests/python/pants_test/engine/exp/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ python_tests(
'src/python/pants/engine/exp:engine',
'src/python/pants/engine/exp:scheduler',
'src/python/pants/engine/exp/examples:planners',
]
],
timeout=10,
)

python_tests(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"type_alias": "target",
"name": "multiple_classpath_entries",
"sources": {
"files": ["Example.java"]
},
"dependencies": [
"3rdparty/jvm:guava"
],
"configurations": [
{ "type_alias": "build_properties" }
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
// Placeholder file.
11 changes: 10 additions & 1 deletion tests/python/pants_test/engine/exp/test_scheduler.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@
import os
import unittest

import pytest

from pants.build_graph.address import Address
from pants.engine.exp.examples.planners import (Classpath, IvyResolve, Jar, Javac, Sources,
gen_apache_thrift, setup_json_scheduler)
from pants.engine.exp.scheduler import BuildRequest, Plan, Promise
from pants.engine.exp.scheduler import BuildRequest, ConflictingProducersError, Plan, Promise


class SchedulerTest(unittest.TestCase):
Expand All @@ -22,6 +24,7 @@ def setUp(self):
self.guava = self.graph.resolve(Address.parse('3rdparty/jvm:guava'))
self.thrift = self.graph.resolve(Address.parse('src/thrift/codegen/simple'))
self.java = self.graph.resolve(Address.parse('src/java/codegen/simple'))
self.java_multi = self.graph.resolve(Address.parse('src/java/multiple_classpath_entries'))

def extract_product_type_and_plan(self, plan):
promise, plan = plan
Expand Down Expand Up @@ -114,3 +117,9 @@ def test_codegen_simple(self):
classpath=[Promise(Classpath, self.guava),
Promise(Classpath, self.thrift)])),
self.extract_product_type_and_plan(plans[3]))

@pytest.mark.xfail(raises=ConflictingProducersError)
def test_multiple_classpath_entries(self):
"""Multiple Classpath products for a single subject currently cause a failure."""
build_request = BuildRequest(goals=['compile'], addressable_roots=[self.java_multi.address])
execution_graph = self.scheduler.execution_graph(build_request)

0 comments on commit 570939f

Please sign in to comment.