Don't treat empty files specially with respect to mtime/digest.

RELNOTES: Bazel no longer regards an empty file as changed if its mtime has changed.

--
MOS_MIGRATED_REVID=127328552
diff --git a/src/main/java/com/google/devtools/build/lib/actions/cache/DigestUtils.java b/src/main/java/com/google/devtools/build/lib/actions/cache/DigestUtils.java
index 377ea43..c61e943 100644
--- a/src/main/java/com/google/devtools/build/lib/actions/cache/DigestUtils.java
+++ b/src/main/java/com/google/devtools/build/lib/actions/cache/DigestUtils.java
@@ -40,17 +40,6 @@
   private DigestUtils() {}
 
   /**
-   * Returns true iff using MD5 digests is appropriate for an artifact.
-   *
-   * @param isFile whether or not Artifact is a file versus a directory, isFile() on its stat.
-   * @param size size of Artifact on filesystem in bytes, getSize() on its stat.
-   */
-  public static boolean useFileDigest(boolean isFile, long size) {
-    // Use timestamps for directories and empty files. Use digests for everything else.
-    return isFile && size != 0;
-  }
-
-  /**
    * Obtain file's MD5 metadata using synchronized method, ensuring that system
    * is not overloaded in case when multiple threads are requesting MD5
    * calculations and underlying file system cannot provide it via extended
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/ActionMetadataHandler.java b/src/main/java/com/google/devtools/build/lib/skyframe/ActionMetadataHandler.java
index a72ef48..f6a0224 100644
--- a/src/main/java/com/google/devtools/build/lib/skyframe/ActionMetadataHandler.java
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/ActionMetadataHandler.java
@@ -24,7 +24,6 @@
 import com.google.devtools.build.lib.actions.Artifact;
 import com.google.devtools.build.lib.actions.Artifact.TreeFileArtifact;
 import com.google.devtools.build.lib.actions.cache.Digest;
-import com.google.devtools.build.lib.actions.cache.DigestUtils;
 import com.google.devtools.build.lib.actions.cache.Metadata;
 import com.google.devtools.build.lib.actions.cache.MetadataHandler;
 import com.google.devtools.build.lib.skyframe.TreeArtifactValue.TreeArtifactException;
@@ -253,10 +252,9 @@
       throw new FileNotFoundException(artifact.prettyPrint() + " does not exist");
     }
     if (!artifact.hasParent()) {
-      // Artifacts may use either the "real" digest or the mtime, if the file is size 0.
+      // Artifacts may use either the "real" digest or the mtime, if the file is a directory.
       boolean isFile = data.isFile();
-      boolean useDigest = DigestUtils.useFileDigest(isFile, isFile ? data.getSize() : 0);
-      if (useDigest && data.getDigest() != null) {
+      if (isFile && data.getDigest() != null) {
         // We do not need to store the FileArtifactValue separately -- the digest is in the
         // file value and that is all that is needed for this file's metadata.
         return new Metadata(data.getDigest());
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/FileArtifactValue.java b/src/main/java/com/google/devtools/build/lib/skyframe/FileArtifactValue.java
index b55639e..5125764 100644
--- a/src/main/java/com/google/devtools/build/lib/skyframe/FileArtifactValue.java
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/FileArtifactValue.java
@@ -33,9 +33,6 @@
  * </li><li>
  *   a directory, in which case we would expect to see an mtime;
  * </li><li>
- *   an empty file corresponding to an Artifact, where we would expect to see a size (=0), mtime,
- *   and digest;
- * </li><li>
  *   an intentionally omitted file which the build system is aware of but doesn't actually exist,
  *   where all access methods are unsupported;
  * </li><li>
@@ -107,13 +104,10 @@
     if (isFile && digest == null) {
       digest = DigestUtils.getDigestOrFail(artifact.getPath(), size);
     }
-    if (!DigestUtils.useFileDigest(isFile, size)) {
-      // In this case, we need to store the mtime because the action cache uses mtime to determine
-      // if this artifact has changed. This is currently true for empty files and directories. We
-      // do not optimize for this code path (by storing the mtime in a FileValue) because we do not
-      // like it and may remove this special-casing for empty files in the future. We want this code
-      // path to go away somehow too for directories (maybe by implementing FileSet
-      // in Skyframe)
+    if (!isFile) {
+      // In this case, we need to store the mtime because the action cache uses mtime for
+      // directories to determine if this artifact has changed. We want this code path to go away
+      // somehow (maybe by implementing FileSet in Skyframe).
       return new FileArtifactValue(digest, artifact.getPath().getLastModifiedTime(), size);
     }
     Preconditions.checkState(digest != null, artifact);
@@ -158,11 +152,7 @@
     return size;
   }
 
-  /**
-   * Gets last modified time of file. Should only be called if {@link DigestUtils#useFileDigest} was
-   * false for this artifact -- namely, either it is a directory or an empty file. Note that since
-   * we store directory sizes as 0, all files for which this method can be called have size 0.
-   */
+  /** Gets last modified time of file. Should only be called if this is a directory. */
   long getModifiedTime() {
     Preconditions.checkState(size == 0, "%s %s %s", digest, mtime, size);
     return mtime;
diff --git a/src/test/java/com/google/devtools/build/lib/skyframe/ArtifactFunctionTest.java b/src/test/java/com/google/devtools/build/lib/skyframe/ArtifactFunctionTest.java
index 789e2b1..84e455c 100644
--- a/src/test/java/com/google/devtools/build/lib/skyframe/ArtifactFunctionTest.java
+++ b/src/test/java/com/google/devtools/build/lib/skyframe/ArtifactFunctionTest.java
@@ -263,8 +263,7 @@
     assertEquals(1L, value.getModifiedTime());
   }
 
-  // Empty files need to store their mtimes, so touching an empty file
-  // can be used to trigger rebuilds.
+  // Empty files are the same as normal files -- mtime is not stored.
   @Test
   public void testEmptyFile() throws Exception {
     Artifact artifact = createDerivedArtifact("empty");
@@ -273,7 +272,7 @@
     path.setLastModifiedTime(1L);
     FileArtifactValue value = create(artifact);
     assertArrayEquals(path.getMD5Digest(), value.getDigest());
-    assertEquals(1L, value.getModifiedTime());
+    assertEquals(-1L, value.getModifiedTime());
     assertEquals(0L, value.getSize());
   }
 
@@ -322,8 +321,7 @@
     EqualsTester equalsTester = new EqualsTester();
     equalsTester
         .addEqualityGroup(create(artifact1), create(artifact2), create(diffMtime))
-        .addEqualityGroup(create(empty1))
-        .addEqualityGroup(create(empty2), create(empty3))
+        .addEqualityGroup(create(empty1), create(empty2), create(empty3))
         .addEqualityGroup(create(dir1))
         .addEqualityGroup(create(dir2), create(dir3))
         .testEquals();
diff --git a/src/test/java/com/google/devtools/build/lib/skyframe/TimestampBuilderMediumTest.java b/src/test/java/com/google/devtools/build/lib/skyframe/TimestampBuilderMediumTest.java
index dba91d6..3c274f9 100644
--- a/src/test/java/com/google/devtools/build/lib/skyframe/TimestampBuilderMediumTest.java
+++ b/src/test/java/com/google/devtools/build/lib/skyframe/TimestampBuilderMediumTest.java
@@ -134,7 +134,8 @@
     buildArtifacts(persistentBuilder(cache), goodbye);
     assertFalse(button.pressed); // not rebuilt
 
-    FileSystemUtils.touchFile(hello.getPath());
+    hello.getPath().setWritable(true);
+    FileSystemUtils.writeContentAsLatin1(hello.getPath(), "new content");
 
     button.pressed = false;
     buildArtifacts(persistentBuilder(cache), goodbye);
@@ -364,7 +365,8 @@
     buildArtifacts(persistentBuilder(cache), hello);
     assertFalse(button.pressed); // not rebuilt
 
-    BlazeTestUtils.changeModtime(hello.getPath());
+    hello.getPath().setWritable(true);
+    FileSystemUtils.writeContentAsLatin1(hello.getPath(), "new content");
 
     button.pressed = false;
     buildArtifacts(persistentBuilder(cache), hello);
@@ -394,7 +396,8 @@
     buildArtifacts(persistentBuilder(cache), hello);
     assertFalse(button.pressed); // not rebuilt
 
-    BlazeTestUtils.changeModtime(hello.getPath());
+    hello.getPath().setWritable(true);
+    FileSystemUtils.writeContentAsLatin1(hello.getPath(), "new content");
 
     button.pressed = false;
     buildArtifacts(persistentBuilder(cache), hello);
@@ -440,7 +443,8 @@
     buildArtifacts(persistentBuilder(cache), hello);
     assertFalse(button.pressed); // not rebuilt
 
-    BlazeTestUtils.changeModtime(hello.getPath());
+    hello.getPath().setWritable(true);
+    FileSystemUtils.writeContentAsLatin1(hello.getPath(), "new content");
 
     button.pressed = false;
     buildArtifacts(persistentBuilder(cache), hello);
diff --git a/src/test/java/com/google/devtools/build/lib/skyframe/TimestampBuilderTest.java b/src/test/java/com/google/devtools/build/lib/skyframe/TimestampBuilderTest.java
index 55f1784..ea10d33 100644
--- a/src/test/java/com/google/devtools/build/lib/skyframe/TimestampBuilderTest.java
+++ b/src/test/java/com/google/devtools/build/lib/skyframe/TimestampBuilderTest.java
@@ -169,8 +169,8 @@
     buildArtifacts(cachingBuilder(), goodbye);
     assertFalse(button.pressed); // not rebuilt
 
-    // inMemoryMetadataCache.useFileDigest is false, so new timestamp is enough to force a rebuild.
-    FileSystemUtils.touchFile(hello.getPath());
+    hello.getPath().setWritable(true);
+    FileSystemUtils.writeContentAsLatin1(hello.getPath(), "new content");
 
     button.pressed = false;
     buildArtifacts(cachingBuilder(), goodbye);
@@ -231,10 +231,10 @@
     buildArtifacts(cachingBuilder(), hello);
     assertFalse(button.pressed); // not rebuilt
 
-    // Touching the *output* file 'hello' causes 'action' to re-execute, to
-    // make things consistent again; this is not what Make would do, but it is
-    // correct according to this Builder.
-    BlazeTestUtils.changeModtime(hello.getPath());
+    // Changing the *output* file 'hello' causes 'action' to re-execute, to make things consistent
+    // again.
+    hello.getPath().setWritable(true);
+    FileSystemUtils.writeContentAsLatin1(hello.getPath(), "new content");
 
     button.pressed = false;
     buildArtifacts(cachingBuilder(), hello);
@@ -251,7 +251,8 @@
     Artifact hello = createSourceArtifact("hello");
     BlazeTestUtils.makeEmptyFile(hello.getPath());
     Artifact wazuup = createDerivedArtifact("wazuup");
-    Button button1 = createActionButton(Sets.newHashSet(hello), Sets.newHashSet(wazuup));
+    Button button1 = new Button();
+    registerAction(new CopyingAction(button1, hello, wazuup));
     Artifact goodbye = createDerivedArtifact("goodbye");
     Button button2 = createActionButton(Sets.newHashSet(wazuup), Sets.newHashSet(goodbye));
 
@@ -275,7 +276,8 @@
     assertFalse(button1.pressed); // wazuup not rebuilt
     assertFalse(button2.pressed); // goodbye not rebuilt
 
-    FileSystemUtils.touchFile(hello.getPath());
+    hello.getPath().setWritable(true);
+    FileSystemUtils.writeContentAsLatin1(hello.getPath(), "new content");
 
     button1.pressed = button2.pressed = false;
     buildArtifacts(cachingBuilder(), goodbye);
diff --git a/src/test/java/com/google/devtools/build/lib/skyframe/TimestampBuilderTestCase.java b/src/test/java/com/google/devtools/build/lib/skyframe/TimestampBuilderTestCase.java
index 51fa9cfa..2c1b7e6 100644
--- a/src/test/java/com/google/devtools/build/lib/skyframe/TimestampBuilderTestCase.java
+++ b/src/test/java/com/google/devtools/build/lib/skyframe/TimestampBuilderTestCase.java
@@ -19,6 +19,7 @@
 import com.google.common.base.Predicates;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Range;
 import com.google.common.collect.Sets;
@@ -26,6 +27,8 @@
 import com.google.devtools.build.lib.actions.Action;
 import com.google.devtools.build.lib.actions.ActionAnalysisMetadata;
 import com.google.devtools.build.lib.actions.ActionCacheChecker;
+import com.google.devtools.build.lib.actions.ActionExecutionContext;
+import com.google.devtools.build.lib.actions.ActionExecutionException;
 import com.google.devtools.build.lib.actions.ActionExecutionStatusReporter;
 import com.google.devtools.build.lib.actions.ActionLogBufferPathGenerator;
 import com.google.devtools.build.lib.actions.Artifact;
@@ -54,6 +57,7 @@
 import com.google.devtools.build.lib.util.Preconditions;
 import com.google.devtools.build.lib.util.io.TimestampGranularityMonitor;
 import com.google.devtools.build.lib.vfs.FileSystem;
+import com.google.devtools.build.lib.vfs.FileSystemUtils;
 import com.google.devtools.build.lib.vfs.Path;
 import com.google.devtools.build.lib.vfs.PathFragment;
 import com.google.devtools.build.skyframe.CycleInfo;
@@ -71,6 +75,7 @@
 
 import org.junit.Before;
 
+import java.io.IOException;
 import java.io.PrintStream;
 import java.util.ArrayList;
 import java.util.Collection;
@@ -357,6 +362,26 @@
     }
   }
 
+  /** {@link TestAction} that copies its single input to its single output. */
+  protected static class CopyingAction extends TestAction {
+    CopyingAction(Runnable effect, Artifact input, Artifact output) {
+      super(effect, ImmutableSet.of(input), ImmutableSet.of(output));
+    }
+
+    @Override
+    public void execute(ActionExecutionContext actionExecutionContext)
+        throws ActionExecutionException {
+      super.execute(actionExecutionContext);
+      try {
+        FileSystemUtils.copyFile(
+            Iterables.getOnlyElement(getInputs()).getPath(),
+            Iterables.getOnlyElement(getOutputs()).getPath());
+      } catch (IOException e) {
+        throw new IllegalStateException(e);
+      }
+    }
+  }
+
   protected static class InMemoryActionCache implements ActionCache {
 
     private final Map<String, Entry> actionCache = new HashMap<>();
diff --git a/src/test/java/com/google/devtools/build/lib/skyframe/TreeArtifactBuildTest.java b/src/test/java/com/google/devtools/build/lib/skyframe/TreeArtifactBuildTest.java
index 0d6d172..b68a86c 100644
--- a/src/test/java/com/google/devtools/build/lib/skyframe/TreeArtifactBuildTest.java
+++ b/src/test/java/com/google/devtools/build/lib/skyframe/TreeArtifactBuildTest.java
@@ -260,11 +260,7 @@
     assertTrue(buttonTwo.pressed);
   }
 
-  /**
-   * TreeArtifacts don't care about mtime, even when the file is empty.
-   * However, actions taking input non-Tree artifacts still care about mtime
-   * (although this behavior should go away).
-   */
+  /** TreeArtifacts don't care about mtime, even when the file is empty. */
   @Test
   public void testMTimeForTreeArtifactsDoesNotMatter() throws Exception {
     // For this test, we only touch the input file.
@@ -291,9 +287,8 @@
     buttonOne.pressed = buttonTwo.pressed = false;
     touchFile(in);
     buildArtifact(outTwo);
-    // Per existing behavior, mtime matters for empty file Artifacts.
-    assertTrue(buttonOne.pressed);
-    // But this should be cached.
+    // mtime does not matter.
+    assertFalse(buttonOne.pressed);
     assertFalse(buttonTwo.pressed);
 
     // None of the below following should result in anything being built.