Repository: pantsbuild/pants

Issue:
[pantsd] stale sources for target when globs are modified during invalidation
seen @ 242cc015 and prior and documented for posterity:

```
[omerta pants (master)]$ ./pants clean-all >/dev/null 2>&1
[omerta pants (master)]$ rm -rf src/python/pants/daemon_correctness_test
[omerta pants (master)]$ mkdir src/python/pants/daemon_correctness_test
[omerta pants (master)]$ echo "python_library(sources=globs('some_non_existent_file.py'))" > src/python/pants/daemon_correctness_test/BUILD
[omerta pants (master)]$ PANTS_ENABLE_PANTSD=True ./pants export --export-libraries-sources --export-libraries-javadocs src/python/pants/daemon_correctness_test 2>&1 |grep source_root
[omerta pants (master)]$ echo "python_library(sources=globs('*.py'))" > src/python/pants/daemon_correctness_test/BUILD
[omerta pants (master)]$ PANTS_ENABLE_PANTSD=True ./pants export --export-libraries-sources --export-libraries-javadocs src/python/pants/daemon_correctness_test 2>&1 |grep source_root
[omerta pants (master)]$ touch src/python/pants/daemon_correctness_test/a_source_file.py
[omerta pants (master)]$ PANTS_ENABLE_PANTSD=True ./pants export --export-libraries-sources --export-libraries-javadocs src/python/pants/daemon_correctness_test 2>&1 |grep source_root
[omerta pants (master)]$
```

vs expected:
```
[omerta pants (master)]$ PANTS_ENABLE_PANTSD=False ./pants export --export-libraries-sources --export-libraries-javadocs src/python/pants/daemon_correctness_test 2>&1 |grep source_root
                    "source_root": "/Users/kwilson/dev/pants/src/python/pants/daemon_correctness_test",
[omerta pants (master)]$ 
```


Discussion:

Resolving patch:
diff --git a/src/python/pants/engine/legacy/structs.py b/src/python/pants/engine/legacy/structs.py
--- a/src/python/pants/engine/legacy/structs.py
+++ b/src/python/pants/engine/legacy/structs.py
@@ -84,14 +84,8 @@ class SourcesField(datatype('SourcesField', ['address', 'arg', 'filespecs', 'pat
   :param path_globs: A PathGlobs describing included files.
   """
 
-  def __eq__(self, other):
-    return type(self) == type(other) and self.address == other.address and self.arg == other.arg
-
-  def __ne__(self, other):
-    return not (self == other)
-
   def __hash__(self):
-    return hash(self.address)
+    return hash((self.address, self.arg))
 
   def __repr__(self):
     return str(self)


Test patch:
diff --git a/tests/python/pants_test/pantsd/test_pantsd_integration.py b/tests/python/pants_test/pantsd/test_pantsd_integration.py
--- a/tests/python/pants_test/pantsd/test_pantsd_integration.py
+++ b/tests/python/pants_test/pantsd/test_pantsd_integration.py
@@ -19,7 +19,7 @@
 from pants.pantsd.process_manager import ProcessManager
 from pants.util.collections import combined_dict
 from pants.util.contextutil import environment_as, temporary_dir
-from pants.util.dirutil import touch
+from pants.util.dirutil import rm_rf, safe_file_dump, safe_mkdir, touch
 from pants_test.pants_run_integration_test import PantsRunIntegrationTest
 from pants_test.testutils.process_test_util import no_lingering_process_by_command
 
@@ -410,3 +410,34 @@ def test_pantsd_invalidation_file_tracking(self):
       checker.assert_stopped()
 
       self.assertIn('saw file events covered by invalidation globs', full_pantsd_log(workdir))
+
+  def test_pantsd_invalidation_stale_sources(self):
+    test_path = 'tests/python/pants_test/daemon_correctness_test_0001'
+    test_build_file = os.path.join(test_path, 'BUILD')
+    test_src_file = os.path.join(test_path, 'some_file.py')
+    has_source_root_regex = r'"source_root": ".*/{}"'.format(test_path)
+    export_cmd = ['export', test_path]
+
+    try:
+      with self.pantsd_successful_run_context() as (pantsd_run, checker, workdir):
+        safe_mkdir(test_path, clean=True)
+
+        pantsd_run(['help'])
+        checker.assert_started()
+
+        safe_file_dump(test_build_file, "python_library(sources=globs('some_non_existent_file.py'))")
+        result = pantsd_run(export_cmd)
+        checker.assert_running()
+        self.assertNotRegexpMatches(result.stdout_data, has_source_root_regex)
+
+        safe_file_dump(test_build_file, "python_library(sources=globs('*.py'))")
+        result = pantsd_run(export_cmd)
+        checker.assert_running()
+        self.assertNotRegexpMatches(result.stdout_data, has_source_root_regex)
+
+        safe_file_dump(test_src_file, 'import this\n')
+        result = pantsd_run(export_cmd)
+        checker.assert_running()
+        self.assertRegexpMatches(result.stdout_data, has_source_root_regex)
+    finally:
+      rm_rf(test_path)