Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Incremental strategy for running/skipping targets via tag #6947

Closed
wants to merge 9 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions src/python/pants/engine/round_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,6 @@ def attempt(self, explain):
with self._context.new_workunit(name=name, labels=[WorkUnitLabel.TASK], log_config=log_config):
if explain:
self._context.log.debug('Skipping execution of {} in explain mode'.format(name))
elif task.skip_execution:
self._context.log.info('Skipping {}'.format(name))
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ref 1

Need to remove this because if a target says no-...-skip in the tag, we can only find out at the task level by filtering targets.

else:
task.execute()

Expand Down
4 changes: 4 additions & 0 deletions src/python/pants/task/target_restriction_mixins.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@ class HasSkipOptionMixin(object):
def skip_execution(self):
return self.get_options().skip

@property
def supports_skipping_target_by_tag(self):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd call this can_skip_individual_targets(), so that it's not so closely bound to the fact that we happen to use tags to express how we choose which targets to skip.

return True


class SkipOptionRegistrar(object):
"""Registrar of --skip."""
Expand Down
37 changes: 35 additions & 2 deletions src/python/pants/task/task.py
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,15 @@ def skip_execution(self):
"""
return False

@property
def supports_skipping_target_by_tag(self):
"""
Whether this task supports forced skip/no-skip on targets based on tags.

:API: public
"""
return False

@property
def act_transitively(self):
"""Whether this task should act on the transitive closure of the target roots.
Expand Down Expand Up @@ -237,8 +246,32 @@ def get_targets(self, predicate=None):

:API: public
"""
return (self.context.targets(predicate) if self.act_transitively
else list(filter(predicate, self.context.target_roots)))
relevant_targets = self.context.targets(predicate) if self.act_transitively else list(
filter(predicate, self.context.target_roots))
if not self.supports_skipping_target_by_tag:
return relevant_targets

force_run_tag = 'no-{}.skip'.format(self.options_scope)
force_run_targets = set(t for t in relevant_targets if force_run_tag in t.tags)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can use a set comprehension with {}.


if self.skip_execution:
if not force_run_targets:
self.context.log.debug('Skipping by returning empty targets '
'because task level is skipped and there is no forced run targets')

return force_run_targets

force_skip_tag = '{}.skip'.format(self.options_scope)
force_skip_targets = set(t for t in relevant_targets if force_skip_tag in t.tags)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto on set comprehension

if force_skip_targets:
self.context.log.debug("Force skipping targets by tag: {}\n{}".format(force_skip_tag, force_skip_targets))
if force_run_targets:
self.context.log.debug("Force running targets by tag: {}\n{}".format(force_run_tag, force_run_targets))

final_targets = list(set(relevant_targets).union(force_run_targets).difference(force_skip_targets))

return final_targets

@memoized_property
def workdir(self):
Expand Down
6 changes: 4 additions & 2 deletions tests/python/pants_test/task/echo_plugin/register.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,10 @@ class EchoTaskBase(HasSkipAndTransitiveGoalOptionsMixin, Task):
prefix = None

def execute(self):
with open(os.path.join(self.workdir, 'output'), 'w') as fp:
fp.write('\n'.join(t.address.spec for t in self.get_targets()))
relevant_targets = self.get_targets()
if relevant_targets:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we not want to write an empty file if relevant_targets is empty? Seems more consistent.

with open(os.path.join(self.workdir, 'output'), 'w') as fp:
fp.write('\n'.join(t.address.spec for t in relevant_targets))


class EchoOne(EchoTaskBase):
Expand Down
45 changes: 33 additions & 12 deletions tests/python/pants_test/task/test_goal_options_mixin_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,15 @@ class TestGoalOptionsMixinIntegration(PantsRunIntegrationTest):
def hermetic(cls):
return True

def _do_test_goal_options(self, flags, expected_one, expected_two):
def _do_test_goal_options(self, flags, expected_one, expected_two, a_tag=set(), b_tag=set()):
with temporary_dir(root_dir=get_buildroot()) as src_dir:
foo_dir = os.path.join(src_dir, 'foo')
os.mkdir(foo_dir)
with open(os.path.join(foo_dir, 'BUILD'), 'w') as fp:
fp.write(textwrap.dedent("""
target(name='a', dependencies=[':b'])
target(name='b')
"""))
target(name='a', dependencies=[':b'], tags={})
target(name='b', tags={})
""").format(a_tag, b_tag))

config = {
'GLOBAL': {
Expand All @@ -43,21 +43,42 @@ def get_echo(which):
return None
else:
with open(path, 'r') as fp:
return [os.path.basename(x.strip()) for x in fp.readlines()]
self.assertEqual(expected_one, get_echo('one'))
self.assertEqual(expected_two, get_echo('two'))
return {os.path.basename(x.strip()) for x in fp.readlines()}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

changed these to sets since the target list ordering isn't consistent.


self.assertEqual(expected_one, get_echo('one'),
'one expected {}, but got {}'.format(expected_one, get_echo('one')))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why add these messages? assertEqual is supposed to already generate a sensible message.

self.assertEqual(expected_two, get_echo('two'),
'two expected {}, but got {}'.format(expected_two, get_echo('two')))

def test_defaults(self):
self._do_test_goal_options([], ['foo:a', 'foo:b'], ['foo:a', 'foo:b'])
self._do_test_goal_options([], {'foo:a', 'foo:b'}, {'foo:a', 'foo:b'})

def test_set_at_goal_level(self):
self._do_test_goal_options(['--skip'], None, None)

def test_set_at_task_level(self):
self._do_test_goal_options(['--echo-one-skip'], None, ['foo:a', 'foo:b'])
self._do_test_goal_options(['--no-echo-two-transitive'], ['foo:a', 'foo:b'], ['foo:a'])
self._do_test_goal_options(['--echo-one-skip'], None, {'foo:a', 'foo:b'})
self._do_test_goal_options(['--no-echo-two-transitive'], {'foo:a', 'foo:b'}, {'foo:a'})

def test_set_at_goal_and_task_level(self):
self._do_test_goal_options(['--skip', '--no-echo-one-skip'], ['foo:a', 'foo:b'], None)
self._do_test_goal_options(['--skip', '--no-echo-one-skip'], {'foo:a', 'foo:b'}, None)
self._do_test_goal_options(['--no-transitive', '--echo-two-transitive'],
['foo:a'], ['foo:a', 'foo:b'])
{'foo:a'}, {'foo:a', 'foo:b'})

def test_task_skip_but_target_tag_no_skip(self):
self._do_test_goal_options(['--echo-one-skip', '--echo-two-skip'],
expected_one={'foo:a'},
expected_two={'foo:a'},
a_tag={"no-echo.one.skip", "no-echo.two.skip"})

def test_task_no_skip_but_target_tag_skip_a(self):
self._do_test_goal_options(['--no-echo-one-skip', '--no-echo-two-skip'],
expected_one={'foo:b'},
expected_two={'foo:b'},
a_tag={"echo.one.skip", "echo.two.skip"})

def test_task_no_skip_but_target_tag_skip_b(self):
self._do_test_goal_options(['--no-echo-one-skip', '--no-echo-two-skip'],
expected_one={'foo:a'},
expected_two={'foo:a'},
b_tag={"echo.one.skip", "echo.two.skip"})