Repository: pantsbuild/pants

Issue:
Passthru args not fingerprinted for pytest
Passthru args are passed directly to pytest, but can cause tests to be skipped, etc. Running:
```
./pants test something -- -k non_existent_test
```
...and then running:
```
./pants test something
```
...will cause the second run to hit the cache and not invoke any tests.

----

This won't matter post #4676, but is probably pretty easy to fix.


Discussion:
This appears to be a generic options fingerprinting bug for tasks. Its probably the case that any task that supports passthru args should fingerprint them to be safe.

Resolving patch:
diff --git a/src/python/pants/option/options.py b/src/python/pants/option/options.py
--- a/src/python/pants/option/options.py
+++ b/src/python/pants/option/options.py
@@ -317,14 +317,25 @@ def for_scope(self, scope, inherit_from_enclosing_scope=True):
 
     return values
 
-  def get_fingerprintable_for_scope(self, scope):
+  def get_fingerprintable_for_scope(self, scope, include_passthru=False):
     """Returns a list of fingerprintable (option type, option value) pairs for the given scope.
 
     Fingerprintable options are options registered via a "fingerprint=True" kwarg.
 
+    :param str scope: The scope to gather fingerprintable options for.
+    :param bool include_passthru: Whether to include passthru args captured by `scope` in the
+                                  fingerprintable options.
+
     :API: public
     """
     pairs = []
+
+    if include_passthru:
+      # Passthru args can only be sent to outermost scopes so we gather them once here up-front.
+      passthru_args = self.passthru_args_for_scope(scope)
+      # NB: We can't sort passthru args, the underlying consumer may be order-sensitive.
+      pairs.extend((str, passthru_arg) for passthru_arg in passthru_args)
+
     # Note that we iterate over options registered at `scope` and at all enclosing scopes, since
     # option-using code can read those values indirectly via its own OptionValueContainer, so
     # they can affect that code's output.
diff --git a/src/python/pants/task/task.py b/src/python/pants/task/task.py
--- a/src/python/pants/task/task.py
+++ b/src/python/pants/task/task.py
@@ -214,7 +214,10 @@ def workdir(self):
     return self._workdir
 
   def _options_fingerprint(self, scope):
-    pairs = self.context.options.get_fingerprintable_for_scope(scope)
+    pairs = self.context.options.get_fingerprintable_for_scope(
+      scope,
+      include_passthru=self.supports_passthru_args()
+    )
     hasher = sha1()
     for (option_type, option_val) in pairs:
       fp = self._options_fingerprinter.fingerprint(option_type, option_val)


Test patch:
diff --git a/tests/python/pants_test/option/test_options.py b/tests/python/pants_test/option/test_options.py
--- a/tests/python/pants_test/option/test_options.py
+++ b/tests/python/pants_test/option/test_options.py
@@ -1044,16 +1044,28 @@ def test_complete_scopes(self):
                        intermediate('qux'), task('qux.quux')},
                       Options.complete_scopes({task('foo.bar.baz'), task('qux.quux')}))
 
-  def test_get_fingerprintable_for_scope(self):
+  def test_get_fingerprintable_for_scope_ignore_passthru(self):
     # Note: tests handling recursive and non-recursive options from enclosing scopes correctly.
     options = self._parse('./pants --store-true-flag --num=88 compile.scala --num=77 '
-                          '--modifycompile="blah blah blah" --modifylogs="durrrr"')
+                          '--modifycompile="blah blah blah" --modifylogs="durrrr" -- -d -v')
 
     pairs = options.get_fingerprintable_for_scope('compile.scala')
-    self.assertEquals(len(pairs), 3)
-    self.assertEquals((str, 'blah blah blah'), pairs[0])
-    self.assertEquals((bool, True), pairs[1])
-    self.assertEquals((int, 77), pairs[2])
+    self.assertEquals([(str, 'blah blah blah'),
+                       (bool, True),
+                       (int, 77)],
+                      pairs)
+
+  def test_get_fingerprintable_for_scope_include_passthru(self):
+    options = self._parse('./pants --store-true-flag --num=88 compile.scala --num=77 '
+                          '--modifycompile="blah blah blah" --modifylogs="durrrr" -- -d -v')
+
+    pairs = options.get_fingerprintable_for_scope('compile.scala', include_passthru=True)
+    self.assertEquals([(str, '-d'),
+                       (str, '-v'),
+                       (str, 'blah blah blah'),
+                       (bool, True),
+                       (int, 77)],
+                      pairs)
 
   def assert_fromfile(self, parse_func, expected_append=None, append_contents=None):
     def _do_assert_fromfile(dest, expected, contents):
diff --git a/tests/python/pants_test/option/util/fakes.py b/tests/python/pants_test/option/util/fakes.py
--- a/tests/python/pants_test/option/util/fakes.py
+++ b/tests/python/pants_test/option/util/fakes.py
@@ -109,10 +109,14 @@ def items(self):
     def scope_to_flags(self):
       return {}
 
-    def get_fingerprintable_for_scope(self, scope):
+    def get_fingerprintable_for_scope(self, scope, include_passthru=False):
+      pairs = []
+      if include_passthru and passthru_args:
+        pairs.extend((str, passthru_arg) for passthru_arg in passthru_args)
       option_values = self.for_scope(scope)
-      return [(option_type, option_values[option_name])
-              for option_name, option_type in fingerprintable[scope].items()]
+      pairs.extend((option_type, option_values[option_name])
+                   for option_name, option_type in fingerprintable[scope].items())
+      return pairs
 
     def __getitem__(self, key):
       return self.for_scope(key)