| // Copyright 2025 The Bazel Authors. All rights reserved. |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| package com.google.devtools.build.lib.skyframe; |
| |
| import static com.google.common.truth.Truth.assertThat; |
| import static org.junit.Assert.assertThrows; |
| import static org.mockito.ArgumentMatchers.any; |
| import static org.mockito.Mockito.mock; |
| import static org.mockito.Mockito.never; |
| import static org.mockito.Mockito.verify; |
| import static org.mockito.Mockito.when; |
| |
| import com.google.common.collect.ImmutableMap; |
| import com.google.devtools.build.lib.actions.ActionLookupData; |
| import com.google.devtools.build.lib.actions.ActionLookupKey; |
| import com.google.devtools.build.lib.actions.Artifact.DerivedArtifact; |
| import com.google.devtools.build.lib.actions.ArtifactRoot; |
| import com.google.devtools.build.lib.actions.ArtifactRoot.RootType; |
| import com.google.devtools.build.lib.actions.FileArtifactValue; |
| import com.google.devtools.build.lib.actions.MissingDepExecException; |
| import com.google.devtools.build.lib.actions.StaticInputMetadataProvider; |
| import com.google.devtools.build.lib.actions.util.ActionsTestUtil; |
| import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder; |
| import com.google.devtools.build.lib.collect.nestedset.Order; |
| import com.google.devtools.build.lib.testutil.FoundationTestCase; |
| import com.google.devtools.build.lib.vfs.FileSystemUtils; |
| import com.google.devtools.build.lib.vfs.PathFragment; |
| import com.google.devtools.build.skyframe.MemoizingEvaluator; |
| import com.google.devtools.build.skyframe.SkyFunction; |
| import org.junit.Test; |
| import org.junit.runner.RunWith; |
| import org.junit.runners.JUnit4; |
| |
| @RunWith(JUnit4.class) |
| public class SkyframeInputMetadataProviderTest extends FoundationTestCase { |
| // The behavior this test verifies (that SkyValues are memoized over multiple restarts) is not |
| // actually necessary for the SkyframeInputMetadataProvider, but only due to a pretty brittle |
| // combination of happenstances: action rewinding may remove a value from the graph at any time |
| // and then MemoizingEvaluator.getExistingValue() will return null. However, Skyframe stores |
| // previously requested direct dependencies in SkyFunction.Environment, so when that happens, |
| // the requested metadata is still returned. But this relies on the particular implementation of |
| // Skyframe *and* SkyframeInputMetadataProvider and memoizing over restarts isn't that costly so |
| // it's useful to signal to someone who would remove this memoization, either accidentally or |
| // intentionally. |
| @Test |
| public void skyframeLookupsMemoizedOverMultipleRestarts() throws Exception { |
| StaticInputMetadataProvider perBuild = new StaticInputMetadataProvider(ImmutableMap.of()); |
| |
| ActionLookupKey owner = ActionsTestUtil.createActionLookupKey("owner"); |
| ActionLookupData actionKey = ActionLookupData.create(owner, 0); |
| DerivedArtifact artifact = |
| DerivedArtifact.create( |
| ArtifactRoot.asDerivedRoot(root.asPath(), RootType.OUTPUT, "out"), |
| PathFragment.create("out/foo"), |
| owner); |
| artifact.getPath().getParentDirectory().createDirectoryAndParents(); |
| FileSystemUtils.writeContentAsLatin1(artifact.getPath(), "test"); |
| artifact.setGeneratingActionKey(actionKey); |
| |
| MemoizingEvaluator evaluator = mock(MemoizingEvaluator.class); |
| SkyframeInputMetadataProvider simp = |
| new SkyframeInputMetadataProvider(evaluator, perBuild, "out"); |
| |
| // On the first iteration, the dependency is not available yet. getInputMetadataChecked() |
| // should accordingly throw. |
| SkyFunction.Environment env1 = mock(SkyFunction.Environment.class); |
| when(evaluator.getExistingValue(actionKey)).thenReturn(null); |
| when(env1.getValue(actionKey)).thenReturn(null); |
| try (var unused = simp.withSkyframeAllowed(env1)) { |
| assertThrows(MissingDepExecException.class, () -> simp.getInputMetadataChecked(artifact)); |
| } |
| |
| FileArtifactValue metadata = FileArtifactValue.createForTesting(artifact); |
| ActionExecutionValue aev = |
| ActionExecutionValue.create( |
| ImmutableMap.of(artifact, metadata), |
| ImmutableMap.of(), |
| null, |
| NestedSetBuilder.emptySet(Order.STABLE_ORDER)); |
| |
| // Now the artifact in in Skyframe. Its metadata should be returned. |
| SkyFunction.Environment env2 = mock(SkyFunction.Environment.class); |
| when(evaluator.getExistingValue(actionKey)).thenReturn(aev); |
| when(env2.getValue(actionKey)).thenReturn(aev); |
| try (var unused = simp.withSkyframeAllowed(env2)) { |
| assertThat(simp.getInputMetadataChecked(artifact)).isEqualTo(metadata); |
| } |
| |
| // No further methods on env3 or the evaluator should be called and the metadata should still be |
| // returned as normal. |
| when(evaluator.getExistingValue(actionKey)).thenThrow(IllegalStateException.class); |
| SkyFunction.Environment env3 = mock(SkyFunction.Environment.class); |
| try (var unused = simp.withSkyframeAllowed(env3)) { |
| assertThat(simp.getInputMetadataChecked(artifact)).isEqualTo(metadata); |
| } |
| |
| verify(env3, never()).getValue(actionKey); |
| } |
| |
| @Test |
| public void skyframeLookupsMemoizedWithinASingleRestart() throws Exception { |
| StaticInputMetadataProvider perBuild = new StaticInputMetadataProvider(ImmutableMap.of()); |
| |
| ActionLookupKey owner = ActionsTestUtil.createActionLookupKey("owner"); |
| ActionLookupData actionKey = ActionLookupData.create(owner, 0); |
| DerivedArtifact artifact = |
| DerivedArtifact.create( |
| ArtifactRoot.asDerivedRoot(root.asPath(), RootType.OUTPUT, "out"), |
| PathFragment.create("out/foo"), |
| owner); |
| artifact.getPath().getParentDirectory().createDirectoryAndParents(); |
| FileSystemUtils.writeContentAsLatin1(artifact.getPath(), "test"); |
| artifact.setGeneratingActionKey(actionKey); |
| FileArtifactValue metadata = FileArtifactValue.createForTesting(artifact); |
| ActionExecutionValue aev = |
| ActionExecutionValue.create( |
| ImmutableMap.of(artifact, metadata), |
| ImmutableMap.of(), |
| null, |
| NestedSetBuilder.emptySet(Order.STABLE_ORDER)); |
| |
| MemoizingEvaluator evaluator = mock(MemoizingEvaluator.class); |
| SkyframeInputMetadataProvider simp = |
| new SkyframeInputMetadataProvider(evaluator, perBuild, "out"); |
| |
| SkyFunction.Environment env = mock(SkyFunction.Environment.class); |
| when(evaluator.getExistingValue(actionKey)) |
| .thenReturn(aev) // first call |
| .thenThrow(IllegalStateException.class); // Subsequent calls |
| |
| try (var unused = simp.withSkyframeAllowed(env)) { |
| assertThat(simp.getInputMetadataChecked(artifact)).isEqualTo(metadata); |
| assertThat(simp.getInputMetadataChecked(artifact)).isEqualTo(metadata); |
| } |
| |
| verify(env, never()).getValue(any()); |
| } |
| } |