Han-Wen Nienhuys | 81b9083 | 2015-10-26 16:57:27 +0000 | [diff] [blame] | 1 | // Copyright 2015 The Bazel Authors. All rights reserved. |
| 2 | // |
| 3 | // Licensed under the Apache License, Version 2.0 (the "License"); |
| 4 | // you may not use this file except in compliance with the License. |
| 5 | // You may obtain a copy of the License at |
| 6 | // |
| 7 | // http://www.apache.org/licenses/LICENSE-2.0 |
| 8 | // |
| 9 | // Unless required by applicable law or agreed to in writing, software |
| 10 | // distributed under the License is distributed on an "AS IS" BASIS, |
| 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 12 | // See the License for the specific language governing permissions and |
| 13 | // limitations under the License. |
| 14 | package com.google.devtools.build.lib.skyframe; |
Janak Ramakrishnan | a5578af | 2017-03-21 17:28:39 +0000 | [diff] [blame] | 15 | |
Han-Wen Nienhuys | 81b9083 | 2015-10-26 16:57:27 +0000 | [diff] [blame] | 16 | import static com.google.common.truth.Truth.assertThat; |
shahan | 602cc85 | 2018-06-06 20:09:57 -0700 | [diff] [blame] | 17 | import static com.google.devtools.build.lib.actions.FileArtifactValue.create; |
jcater | 83130f4 | 2019-04-30 14:29:28 -0700 | [diff] [blame] | 18 | import static com.google.devtools.build.lib.testutil.MoreAsserts.assertThrows; |
Han-Wen Nienhuys | 81b9083 | 2015-10-26 16:57:27 +0000 | [diff] [blame] | 19 | |
Han-Wen Nienhuys | 81b9083 | 2015-10-26 16:57:27 +0000 | [diff] [blame] | 20 | import com.google.common.collect.ImmutableList; |
| 21 | import com.google.common.collect.ImmutableMap; |
| 22 | import com.google.common.collect.ImmutableSet; |
| 23 | import com.google.common.collect.Iterables; |
Han-Wen Nienhuys | 81b9083 | 2015-10-26 16:57:27 +0000 | [diff] [blame] | 24 | import com.google.devtools.build.lib.actions.Action; |
Rumou Duan | 33bab46 | 2016-04-25 17:55:12 +0000 | [diff] [blame] | 25 | import com.google.devtools.build.lib.actions.ActionAnalysisMetadata.MiddlemanType; |
Rumou Duan | 7387620 | 2016-06-06 18:52:08 +0000 | [diff] [blame] | 26 | import com.google.devtools.build.lib.actions.ActionInputHelper; |
janakr | 93e3eea | 2017-03-30 22:09:37 +0000 | [diff] [blame] | 27 | import com.google.devtools.build.lib.actions.ActionLookupData; |
| 28 | import com.google.devtools.build.lib.actions.ActionLookupValue; |
janakr | 0175ce3 | 2018-02-26 15:54:57 -0800 | [diff] [blame] | 29 | import com.google.devtools.build.lib.actions.Actions; |
Han-Wen Nienhuys | 81b9083 | 2015-10-26 16:57:27 +0000 | [diff] [blame] | 30 | import com.google.devtools.build.lib.actions.Artifact; |
Rumou Duan | 7387620 | 2016-06-06 18:52:08 +0000 | [diff] [blame] | 31 | import com.google.devtools.build.lib.actions.Artifact.SpecialArtifact; |
| 32 | import com.google.devtools.build.lib.actions.Artifact.SpecialArtifactType; |
| 33 | import com.google.devtools.build.lib.actions.Artifact.TreeFileArtifact; |
janakr | 0c42fc8 | 2018-09-14 10:37:25 -0700 | [diff] [blame] | 34 | import com.google.devtools.build.lib.actions.ArtifactFileMetadata; |
tomlu | 1cdcdf9 | 2018-01-16 11:07:51 -0800 | [diff] [blame] | 35 | import com.google.devtools.build.lib.actions.ArtifactRoot; |
shahan | e35e8cf | 2018-06-18 08:14:01 -0700 | [diff] [blame] | 36 | import com.google.devtools.build.lib.actions.ArtifactSkyKey; |
cparsons | e2d200f | 2018-03-06 16:15:11 -0800 | [diff] [blame] | 37 | import com.google.devtools.build.lib.actions.BasicActionLookupValue; |
shahan | 602cc85 | 2018-06-06 20:09:57 -0700 | [diff] [blame] | 38 | import com.google.devtools.build.lib.actions.FileArtifactValue; |
janakr | e82933c | 2019-01-02 14:41:50 -0800 | [diff] [blame] | 39 | import com.google.devtools.build.lib.actions.FilesetOutputSymlink; |
Han-Wen Nienhuys | 81b9083 | 2015-10-26 16:57:27 +0000 | [diff] [blame] | 40 | import com.google.devtools.build.lib.actions.MissingInputFileException; |
janakr | 0175ce3 | 2018-02-26 15:54:57 -0800 | [diff] [blame] | 41 | import com.google.devtools.build.lib.actions.MutableActionGraph.ActionConflictException; |
Rumou Duan | 7387620 | 2016-06-06 18:52:08 +0000 | [diff] [blame] | 42 | import com.google.devtools.build.lib.actions.util.ActionsTestUtil; |
Han-Wen Nienhuys | 81b9083 | 2015-10-26 16:57:27 +0000 | [diff] [blame] | 43 | import com.google.devtools.build.lib.actions.util.TestAction.DummyAction; |
| 44 | import com.google.devtools.build.lib.events.NullEventHandler; |
janakr | e82933c | 2019-01-02 14:41:50 -0800 | [diff] [blame] | 45 | import com.google.devtools.build.lib.skyframe.serialization.testutils.SerializationTester; |
Han-Wen Nienhuys | 81b9083 | 2015-10-26 16:57:27 +0000 | [diff] [blame] | 46 | import com.google.devtools.build.lib.util.Pair; |
Han-Wen Nienhuys | 81b9083 | 2015-10-26 16:57:27 +0000 | [diff] [blame] | 47 | import com.google.devtools.build.lib.vfs.FileStatus; |
janakr | e82933c | 2019-01-02 14:41:50 -0800 | [diff] [blame] | 48 | import com.google.devtools.build.lib.vfs.FileSystem; |
Han-Wen Nienhuys | 81b9083 | 2015-10-26 16:57:27 +0000 | [diff] [blame] | 49 | import com.google.devtools.build.lib.vfs.FileSystemUtils; |
| 50 | import com.google.devtools.build.lib.vfs.Path; |
| 51 | import com.google.devtools.build.lib.vfs.PathFragment; |
tomlu | ee6a686 | 2018-01-17 14:36:26 -0800 | [diff] [blame] | 52 | import com.google.devtools.build.lib.vfs.Root; |
Googler | 1002867 | 2018-10-25 12:14:34 -0700 | [diff] [blame] | 53 | import com.google.devtools.build.skyframe.EvaluationContext; |
Han-Wen Nienhuys | 81b9083 | 2015-10-26 16:57:27 +0000 | [diff] [blame] | 54 | import com.google.devtools.build.skyframe.EvaluationResult; |
Han-Wen Nienhuys | 81b9083 | 2015-10-26 16:57:27 +0000 | [diff] [blame] | 55 | import com.google.devtools.build.skyframe.SkyFunction; |
Han-Wen Nienhuys | 81b9083 | 2015-10-26 16:57:27 +0000 | [diff] [blame] | 56 | import com.google.devtools.build.skyframe.SkyKey; |
| 57 | import com.google.devtools.build.skyframe.SkyValue; |
Han-Wen Nienhuys | 81b9083 | 2015-10-26 16:57:27 +0000 | [diff] [blame] | 58 | import java.io.IOException; |
Michajlo Matijkiw | 528957e | 2016-01-19 21:17:45 +0000 | [diff] [blame] | 59 | import java.nio.charset.StandardCharsets; |
| 60 | import java.security.MessageDigest; |
buchgr | d4d3d50 | 2018-08-02 06:47:19 -0700 | [diff] [blame] | 61 | import java.util.ArrayList; |
Han-Wen Nienhuys | 81b9083 | 2015-10-26 16:57:27 +0000 | [diff] [blame] | 62 | import java.util.Arrays; |
| 63 | import java.util.HashMap; |
Han-Wen Nienhuys | 81b9083 | 2015-10-26 16:57:27 +0000 | [diff] [blame] | 64 | import java.util.Map; |
Janak Ramakrishnan | ad77f97 | 2016-07-29 20:58:42 +0000 | [diff] [blame] | 65 | import org.junit.Before; |
| 66 | import org.junit.Test; |
| 67 | import org.junit.runner.RunWith; |
| 68 | import org.junit.runners.JUnit4; |
Han-Wen Nienhuys | 81b9083 | 2015-10-26 16:57:27 +0000 | [diff] [blame] | 69 | |
| 70 | /** |
| 71 | * Tests for {@link ArtifactFunction}. |
| 72 | */ |
| 73 | // Doesn't actually need any particular Skyframe, but is only relevant to Skyframe full mode. |
Han-Wen Nienhuys | 3b2eae3 | 2015-10-28 16:35:08 +0000 | [diff] [blame] | 74 | @RunWith(JUnit4.class) |
Michael Thvedt | 8d5a7bb | 2016-02-09 03:06:34 +0000 | [diff] [blame] | 75 | public class ArtifactFunctionTest extends ArtifactFunctionTestCase { |
Han-Wen Nienhuys | 81b9083 | 2015-10-26 16:57:27 +0000 | [diff] [blame] | 76 | |
Han-Wen Nienhuys | 3b2eae3 | 2015-10-28 16:35:08 +0000 | [diff] [blame] | 77 | @Before |
Florian Weikert | 92b2236 | 2015-12-03 10:17:18 +0000 | [diff] [blame] | 78 | public final void setUp() throws Exception { |
Michael Thvedt | 8d5a7bb | 2016-02-09 03:06:34 +0000 | [diff] [blame] | 79 | delegateActionExecutionFunction = new SimpleActionExecutionFunction(); |
Han-Wen Nienhuys | 81b9083 | 2015-10-26 16:57:27 +0000 | [diff] [blame] | 80 | } |
| 81 | |
| 82 | private void assertFileArtifactValueMatches(boolean expectDigest) throws Throwable { |
| 83 | Artifact output = createDerivedArtifact("output"); |
| 84 | Path path = output.getPath(); |
| 85 | file(path, "contents"); |
olaola | bfd1d33 | 2017-06-19 16:55:24 -0400 | [diff] [blame] | 86 | assertValueMatches(path.stat(), expectDigest ? path.getDigest() : null, evaluateFAN(output)); |
Han-Wen Nienhuys | 81b9083 | 2015-10-26 16:57:27 +0000 | [diff] [blame] | 87 | } |
| 88 | |
Han-Wen Nienhuys | 3b2eae3 | 2015-10-28 16:35:08 +0000 | [diff] [blame] | 89 | @Test |
Han-Wen Nienhuys | 81b9083 | 2015-10-26 16:57:27 +0000 | [diff] [blame] | 90 | public void testBasicArtifact() throws Throwable { |
| 91 | fastDigest = false; |
| 92 | assertFileArtifactValueMatches(/*expectDigest=*/ true); |
| 93 | } |
| 94 | |
Han-Wen Nienhuys | 3b2eae3 | 2015-10-28 16:35:08 +0000 | [diff] [blame] | 95 | @Test |
Han-Wen Nienhuys | 81b9083 | 2015-10-26 16:57:27 +0000 | [diff] [blame] | 96 | public void testBasicArtifactWithXattr() throws Throwable { |
| 97 | fastDigest = true; |
| 98 | assertFileArtifactValueMatches(/*expectDigest=*/ true); |
| 99 | } |
| 100 | |
Han-Wen Nienhuys | 3b2eae3 | 2015-10-28 16:35:08 +0000 | [diff] [blame] | 101 | @Test |
Han-Wen Nienhuys | 81b9083 | 2015-10-26 16:57:27 +0000 | [diff] [blame] | 102 | public void testMissingNonMandatoryArtifact() throws Throwable { |
| 103 | Artifact input = createSourceArtifact("input1"); |
lberki | e355e77 | 2017-05-31 14:34:53 +0200 | [diff] [blame] | 104 | assertThat(evaluateArtifactValue(input, /*mandatory=*/ false)).isNotNull(); |
Han-Wen Nienhuys | 81b9083 | 2015-10-26 16:57:27 +0000 | [diff] [blame] | 105 | } |
| 106 | |
Han-Wen Nienhuys | 3b2eae3 | 2015-10-28 16:35:08 +0000 | [diff] [blame] | 107 | @Test |
Michajlo Matijkiw | 528957e | 2016-01-19 21:17:45 +0000 | [diff] [blame] | 108 | public void testUnreadableInputWithFsWithAvailableDigest() throws Throwable { |
| 109 | final byte[] expectedDigest = MessageDigest.getInstance("md5").digest( |
| 110 | "someunreadablecontent".getBytes(StandardCharsets.UTF_8)); |
| 111 | setupRoot( |
| 112 | new CustomInMemoryFs() { |
| 113 | @Override |
ccalvarin | dd9f60e | 2018-07-23 18:16:18 -0700 | [diff] [blame] | 114 | public byte[] getDigest(Path path) throws IOException { |
| 115 | return path.getBaseName().equals("unreadable") ? expectedDigest : super.getDigest(path); |
Michajlo Matijkiw | 528957e | 2016-01-19 21:17:45 +0000 | [diff] [blame] | 116 | } |
| 117 | }); |
| 118 | |
| 119 | Artifact input = createSourceArtifact("unreadable"); |
| 120 | Path inputPath = input.getPath(); |
| 121 | file(inputPath, "dummynotused"); |
| 122 | inputPath.chmod(0); |
| 123 | |
| 124 | FileArtifactValue value = |
| 125 | (FileArtifactValue) evaluateArtifactValue(input, /*mandatory=*/ true); |
| 126 | |
| 127 | FileStatus stat = inputPath.stat(); |
| 128 | assertThat(value.getSize()).isEqualTo(stat.getSize()); |
| 129 | assertThat(value.getDigest()).isEqualTo(expectedDigest); |
| 130 | } |
| 131 | |
| 132 | @Test |
Han-Wen Nienhuys | 81b9083 | 2015-10-26 16:57:27 +0000 | [diff] [blame] | 133 | public void testMissingMandatoryArtifact() throws Throwable { |
| 134 | Artifact input = createSourceArtifact("input1"); |
jcater | 83130f4 | 2019-04-30 14:29:28 -0700 | [diff] [blame] | 135 | assertThrows( |
| 136 | MissingInputFileException.class, () -> evaluateArtifactValue(input, /*mandatory=*/ true)); |
Han-Wen Nienhuys | 81b9083 | 2015-10-26 16:57:27 +0000 | [diff] [blame] | 137 | } |
| 138 | |
Han-Wen Nienhuys | 3b2eae3 | 2015-10-28 16:35:08 +0000 | [diff] [blame] | 139 | @Test |
Han-Wen Nienhuys | 81b9083 | 2015-10-26 16:57:27 +0000 | [diff] [blame] | 140 | public void testMiddlemanArtifact() throws Throwable { |
Janak Ramakrishnan | a5578af | 2017-03-21 17:28:39 +0000 | [diff] [blame] | 141 | Artifact output = createMiddlemanArtifact("output"); |
Han-Wen Nienhuys | 81b9083 | 2015-10-26 16:57:27 +0000 | [diff] [blame] | 142 | Artifact input1 = createSourceArtifact("input1"); |
| 143 | Artifact input2 = createDerivedArtifact("input2"); |
Benjamin Peterson | 63748e4 | 2018-06-03 22:11:16 -0700 | [diff] [blame] | 144 | SpecialArtifact tree = createDerivedTreeArtifactWithAction("treeArtifact"); |
buchgr | d4d3d50 | 2018-08-02 06:47:19 -0700 | [diff] [blame] | 145 | TreeFileArtifact treeFile1 = createFakeTreeFileArtifact(tree, "child1", "hello1"); |
| 146 | TreeFileArtifact treeFile2 = createFakeTreeFileArtifact(tree, "child2", "hello2"); |
| 147 | file(treeFile1.getPath(), "src1"); |
| 148 | file(treeFile2.getPath(), "src2"); |
Han-Wen Nienhuys | 81b9083 | 2015-10-26 16:57:27 +0000 | [diff] [blame] | 149 | Action action = |
| 150 | new DummyAction( |
Benjamin Peterson | 63748e4 | 2018-06-03 22:11:16 -0700 | [diff] [blame] | 151 | ImmutableList.of(input1, input2, tree), output, MiddlemanType.AGGREGATING_MIDDLEMAN); |
Han-Wen Nienhuys | 81b9083 | 2015-10-26 16:57:27 +0000 | [diff] [blame] | 152 | actions.add(action); |
| 153 | file(input2.getPath(), "contents"); |
| 154 | file(input1.getPath(), "source contents"); |
janakr | 8541f6d | 2019-06-11 14:40:21 -0700 | [diff] [blame] | 155 | evaluate( |
| 156 | Iterables.toArray( |
| 157 | ArtifactSkyKey.mandatoryKeys(ImmutableSet.of(input2, input1, input2, tree)), |
| 158 | SkyKey.class)); |
Janak Ramakrishnan | ad77f97 | 2016-07-29 20:58:42 +0000 | [diff] [blame] | 159 | SkyValue value = evaluateArtifactValue(output); |
buchgr | d4d3d50 | 2018-08-02 06:47:19 -0700 | [diff] [blame] | 160 | ArrayList<Pair<Artifact, ?>> inputs = new ArrayList<>(); |
| 161 | inputs.addAll(((AggregatingArtifactValue) value).getFileArtifacts()); |
| 162 | inputs.addAll(((AggregatingArtifactValue) value).getTreeArtifacts()); |
| 163 | assertThat(inputs) |
Benjamin Peterson | 63748e4 | 2018-06-03 22:11:16 -0700 | [diff] [blame] | 164 | .containsExactly( |
| 165 | Pair.of(input1, create(input1)), |
| 166 | Pair.of(input2, create(input2)), |
buchgr | d4d3d50 | 2018-08-02 06:47:19 -0700 | [diff] [blame] | 167 | Pair.of(tree, ((TreeArtifactValue) evaluateArtifactValue(tree)))); |
Han-Wen Nienhuys | 81b9083 | 2015-10-26 16:57:27 +0000 | [diff] [blame] | 168 | } |
| 169 | |
Han-Wen Nienhuys | 81b9083 | 2015-10-26 16:57:27 +0000 | [diff] [blame] | 170 | /** |
| 171 | * Tests that ArtifactFunction rethrows transitive {@link IOException}s as |
| 172 | * {@link MissingInputFileException}s. |
| 173 | */ |
Han-Wen Nienhuys | 3b2eae3 | 2015-10-28 16:35:08 +0000 | [diff] [blame] | 174 | @Test |
Han-Wen Nienhuys | 81b9083 | 2015-10-26 16:57:27 +0000 | [diff] [blame] | 175 | public void testIOException_EndToEnd() throws Throwable { |
| 176 | final IOException exception = new IOException("beep"); |
| 177 | setupRoot( |
| 178 | new CustomInMemoryFs() { |
| 179 | @Override |
felly | a205ed8 | 2018-09-10 11:52:34 -0700 | [diff] [blame] | 180 | public FileStatus statIfFound(Path path, boolean followSymlinks) throws IOException { |
Han-Wen Nienhuys | 81b9083 | 2015-10-26 16:57:27 +0000 | [diff] [blame] | 181 | if (path.getBaseName().equals("bad")) { |
| 182 | throw exception; |
| 183 | } |
felly | a205ed8 | 2018-09-10 11:52:34 -0700 | [diff] [blame] | 184 | return super.statIfFound(path, followSymlinks); |
Han-Wen Nienhuys | 81b9083 | 2015-10-26 16:57:27 +0000 | [diff] [blame] | 185 | } |
| 186 | }); |
jcater | 83130f4 | 2019-04-30 14:29:28 -0700 | [diff] [blame] | 187 | MissingInputFileException e = |
| 188 | assertThrows( |
| 189 | MissingInputFileException.class, |
| 190 | () -> evaluateArtifactValue(createSourceArtifact("bad"))); |
| 191 | assertThat(e).hasMessageThat().contains(exception.getMessage()); |
Han-Wen Nienhuys | 81b9083 | 2015-10-26 16:57:27 +0000 | [diff] [blame] | 192 | } |
| 193 | |
Han-Wen Nienhuys | 3b2eae3 | 2015-10-28 16:35:08 +0000 | [diff] [blame] | 194 | @Test |
Rumou Duan | 7387620 | 2016-06-06 18:52:08 +0000 | [diff] [blame] | 195 | public void testActionTreeArtifactOutput() throws Throwable { |
cpeyser | ac09f0a | 2018-02-05 09:33:15 -0800 | [diff] [blame] | 196 | SpecialArtifact artifact = createDerivedTreeArtifactWithAction("treeArtifact"); |
janakr | efb3f15 | 2019-06-05 17:42:34 -0700 | [diff] [blame] | 197 | TreeFileArtifact treeFileArtifact1 = createFakeTreeFileArtifact(artifact, "child1", "hello1"); |
| 198 | TreeFileArtifact treeFileArtifact2 = createFakeTreeFileArtifact(artifact, "child2", "hello2"); |
Rumou Duan | 7387620 | 2016-06-06 18:52:08 +0000 | [diff] [blame] | 199 | |
| 200 | TreeArtifactValue value = (TreeArtifactValue) evaluateArtifactValue(artifact); |
janakr | 45b308a | 2018-06-08 12:51:58 -0700 | [diff] [blame] | 201 | assertThat(value.getChildValues()).containsKey(treeFileArtifact1); |
| 202 | assertThat(value.getChildValues()).containsKey(treeFileArtifact2); |
lberki | aea56b3 | 2017-05-30 12:35:33 +0200 | [diff] [blame] | 203 | assertThat(value.getChildValues().get(treeFileArtifact1).getDigest()).isNotNull(); |
| 204 | assertThat(value.getChildValues().get(treeFileArtifact2).getDigest()).isNotNull(); |
Rumou Duan | 7387620 | 2016-06-06 18:52:08 +0000 | [diff] [blame] | 205 | } |
| 206 | |
| 207 | @Test |
| 208 | public void testSpawnActionTemplate() throws Throwable { |
| 209 | // artifact1 is a tree artifact generated by normal action. |
cpeyser | ac09f0a | 2018-02-05 09:33:15 -0800 | [diff] [blame] | 210 | SpecialArtifact artifact1 = createDerivedTreeArtifactWithAction("treeArtifact1"); |
Rumou Duan | 7387620 | 2016-06-06 18:52:08 +0000 | [diff] [blame] | 211 | createFakeTreeFileArtifact(artifact1, "child1", "hello1"); |
| 212 | createFakeTreeFileArtifact(artifact1, "child2", "hello2"); |
| 213 | |
Rumou Duan | 7387620 | 2016-06-06 18:52:08 +0000 | [diff] [blame] | 214 | // artifact2 is a tree artifact generated by action template. |
cpeyser | ac09f0a | 2018-02-05 09:33:15 -0800 | [diff] [blame] | 215 | SpecialArtifact artifact2 = createDerivedTreeArtifactOnly("treeArtifact2"); |
janakr | 45b308a | 2018-06-08 12:51:58 -0700 | [diff] [blame] | 216 | TreeFileArtifact treeFileArtifact1 = |
| 217 | createFakeTreeFileArtifact( |
| 218 | artifact2, |
janakr | 45b308a | 2018-06-08 12:51:58 -0700 | [diff] [blame] | 219 | "child1", |
| 220 | "hello1"); |
| 221 | TreeFileArtifact treeFileArtifact2 = |
| 222 | createFakeTreeFileArtifact( |
| 223 | artifact2, |
janakr | 45b308a | 2018-06-08 12:51:58 -0700 | [diff] [blame] | 224 | "child2", |
| 225 | "hello2"); |
Rumou Duan | 7387620 | 2016-06-06 18:52:08 +0000 | [diff] [blame] | 226 | |
| 227 | actions.add( |
| 228 | ActionsTestUtil.createDummySpawnActionTemplate(artifact1, artifact2)); |
| 229 | |
| 230 | TreeArtifactValue value = (TreeArtifactValue) evaluateArtifactValue(artifact2); |
janakr | 45b308a | 2018-06-08 12:51:58 -0700 | [diff] [blame] | 231 | assertThat(value.getChildValues()).containsKey(treeFileArtifact1); |
| 232 | assertThat(value.getChildValues()).containsKey(treeFileArtifact2); |
lberki | aea56b3 | 2017-05-30 12:35:33 +0200 | [diff] [blame] | 233 | assertThat(value.getChildValues().get(treeFileArtifact1).getDigest()).isNotNull(); |
| 234 | assertThat(value.getChildValues().get(treeFileArtifact2).getDigest()).isNotNull(); |
Rumou Duan | 7387620 | 2016-06-06 18:52:08 +0000 | [diff] [blame] | 235 | } |
| 236 | |
| 237 | @Test |
| 238 | public void testConsecutiveSpawnActionTemplates() throws Throwable { |
| 239 | // artifact1 is a tree artifact generated by normal action. |
cpeyser | ac09f0a | 2018-02-05 09:33:15 -0800 | [diff] [blame] | 240 | SpecialArtifact artifact1 = createDerivedTreeArtifactWithAction("treeArtifact1"); |
Rumou Duan | 7387620 | 2016-06-06 18:52:08 +0000 | [diff] [blame] | 241 | createFakeTreeFileArtifact(artifact1, "child1", "hello1"); |
| 242 | createFakeTreeFileArtifact(artifact1, "child2", "hello2"); |
| 243 | |
| 244 | // artifact2 is a tree artifact generated by action template. |
cpeyser | ac09f0a | 2018-02-05 09:33:15 -0800 | [diff] [blame] | 245 | SpecialArtifact artifact2 = createDerivedTreeArtifactOnly("treeArtifact2"); |
Rumou Duan | 7387620 | 2016-06-06 18:52:08 +0000 | [diff] [blame] | 246 | createFakeTreeFileArtifact(artifact2, "child1", "hello1"); |
| 247 | createFakeTreeFileArtifact(artifact2, "child2", "hello2"); |
| 248 | actions.add( |
| 249 | ActionsTestUtil.createDummySpawnActionTemplate(artifact1, artifact2)); |
| 250 | |
| 251 | // artifact3 is a tree artifact generated by action template. |
cpeyser | ac09f0a | 2018-02-05 09:33:15 -0800 | [diff] [blame] | 252 | SpecialArtifact artifact3 = createDerivedTreeArtifactOnly("treeArtifact3"); |
janakr | 45b308a | 2018-06-08 12:51:58 -0700 | [diff] [blame] | 253 | TreeFileArtifact treeFileArtifact1 = |
| 254 | createFakeTreeFileArtifact( |
| 255 | artifact3, |
janakr | 45b308a | 2018-06-08 12:51:58 -0700 | [diff] [blame] | 256 | "child1", |
| 257 | "hello1"); |
| 258 | TreeFileArtifact treeFileArtifact2 = |
| 259 | createFakeTreeFileArtifact( |
| 260 | artifact3, |
janakr | 45b308a | 2018-06-08 12:51:58 -0700 | [diff] [blame] | 261 | "child2", |
| 262 | "hello2"); |
Rumou Duan | 7387620 | 2016-06-06 18:52:08 +0000 | [diff] [blame] | 263 | actions.add( |
| 264 | ActionsTestUtil.createDummySpawnActionTemplate(artifact2, artifact3)); |
| 265 | |
| 266 | TreeArtifactValue value = (TreeArtifactValue) evaluateArtifactValue(artifact3); |
janakr | 45b308a | 2018-06-08 12:51:58 -0700 | [diff] [blame] | 267 | assertThat(value.getChildValues()).containsKey(treeFileArtifact1); |
| 268 | assertThat(value.getChildValues()).containsKey(treeFileArtifact2); |
lberki | aea56b3 | 2017-05-30 12:35:33 +0200 | [diff] [blame] | 269 | assertThat(value.getChildValues().get(treeFileArtifact1).getDigest()).isNotNull(); |
| 270 | assertThat(value.getChildValues().get(treeFileArtifact2).getDigest()).isNotNull(); |
Rumou Duan | 7387620 | 2016-06-06 18:52:08 +0000 | [diff] [blame] | 271 | } |
| 272 | |
janakr | e82933c | 2019-01-02 14:41:50 -0800 | [diff] [blame] | 273 | @Test |
| 274 | public void actionExecutionValueSerialization() throws Exception { |
janakr | efb3f15 | 2019-06-05 17:42:34 -0700 | [diff] [blame] | 275 | ActionLookupData dummyData = ActionLookupData.create(ALL_OWNER, 0); |
| 276 | Artifact.DerivedArtifact artifact1 = createDerivedArtifact("one"); |
janakr | efb3f15 | 2019-06-05 17:42:34 -0700 | [diff] [blame] | 277 | Artifact.DerivedArtifact artifact2 = createDerivedArtifact("two"); |
janakr | e82933c | 2019-01-02 14:41:50 -0800 | [diff] [blame] | 278 | ArtifactFileMetadata metadata1 = |
| 279 | ActionMetadataHandler.fileMetadataFromArtifact(artifact1, null, null); |
| 280 | SpecialArtifact treeArtifact = createDerivedTreeArtifactOnly("tree"); |
janakr | efb3f15 | 2019-06-05 17:42:34 -0700 | [diff] [blame] | 281 | treeArtifact.setGeneratingActionKey(dummyData); |
| 282 | TreeFileArtifact treeFileArtifact = ActionInputHelper.treeFileArtifact(treeArtifact, "subpath"); |
| 283 | Path path = treeFileArtifact.getPath(); |
| 284 | FileSystemUtils.createDirectoryAndParents(path.getParentDirectory()); |
| 285 | writeFile(path, "contents"); |
janakr | e82933c | 2019-01-02 14:41:50 -0800 | [diff] [blame] | 286 | TreeArtifactValue treeArtifactValue = |
| 287 | TreeArtifactValue.create( |
| 288 | ImmutableMap.of(treeFileArtifact, FileArtifactValue.create(treeFileArtifact))); |
janakr | efb3f15 | 2019-06-05 17:42:34 -0700 | [diff] [blame] | 289 | Artifact.DerivedArtifact artifact3 = createDerivedArtifact("three"); |
janakr | e82933c | 2019-01-02 14:41:50 -0800 | [diff] [blame] | 290 | FilesetOutputSymlink filesetOutputSymlink = |
| 291 | FilesetOutputSymlink.createForTesting( |
| 292 | PathFragment.EMPTY_FRAGMENT, PathFragment.EMPTY_FRAGMENT, PathFragment.EMPTY_FRAGMENT); |
| 293 | ActionExecutionValue actionExecutionValue = |
| 294 | ActionExecutionValue.create( |
| 295 | ImmutableMap.of(artifact1, metadata1, artifact2, ArtifactFileMetadata.PLACEHOLDER), |
| 296 | ImmutableMap.of(treeArtifact, treeArtifactValue), |
| 297 | ImmutableMap.of(artifact3, FileArtifactValue.DEFAULT_MIDDLEMAN), |
| 298 | ImmutableList.of(filesetOutputSymlink), |
| 299 | null, |
| 300 | true); |
| 301 | ActionExecutionValue valueWithFingerprint = |
| 302 | ActionExecutionValue.create( |
| 303 | ImmutableMap.of(artifact1, metadata1, artifact2, ArtifactFileMetadata.PLACEHOLDER), |
| 304 | ImmutableMap.of(treeArtifact, treeArtifactValue), |
| 305 | ImmutableMap.of(artifact3, FileArtifactValue.DEFAULT_MIDDLEMAN), |
| 306 | ImmutableList.of(filesetOutputSymlink), |
| 307 | null, |
| 308 | true); |
| 309 | valueWithFingerprint.getValueFingerprint(); |
| 310 | new SerializationTester(actionExecutionValue, valueWithFingerprint) |
| 311 | .addDependency(FileSystem.class, root.getFileSystem()) |
| 312 | .runTests(); |
| 313 | } |
| 314 | |
Han-Wen Nienhuys | 81b9083 | 2015-10-26 16:57:27 +0000 | [diff] [blame] | 315 | private void file(Path path, String contents) throws Exception { |
| 316 | FileSystemUtils.createDirectoryAndParents(path.getParentDirectory()); |
| 317 | writeFile(path, contents); |
| 318 | } |
| 319 | |
| 320 | private Artifact createSourceArtifact(String path) { |
janakr | aea0560 | 2019-05-22 15:41:29 -0700 | [diff] [blame] | 321 | return ActionsTestUtil.createArtifactWithExecPath( |
| 322 | ArtifactRoot.asSourceRoot(Root.fromPath(root)), PathFragment.create(path)); |
Han-Wen Nienhuys | 81b9083 | 2015-10-26 16:57:27 +0000 | [diff] [blame] | 323 | } |
| 324 | |
janakr | efb3f15 | 2019-06-05 17:42:34 -0700 | [diff] [blame] | 325 | private Artifact.DerivedArtifact createDerivedArtifact(String path) { |
nharmata | b4060b6 | 2017-04-04 17:11:39 +0000 | [diff] [blame] | 326 | PathFragment execPath = PathFragment.create("out").getRelative(path); |
janakr | efb3f15 | 2019-06-05 17:42:34 -0700 | [diff] [blame] | 327 | Artifact.DerivedArtifact output = |
janakr | aea0560 | 2019-05-22 15:41:29 -0700 | [diff] [blame] | 328 | new Artifact.DerivedArtifact( |
janakr | 3290e22 | 2019-05-29 16:34:22 -0700 | [diff] [blame] | 329 | ArtifactRoot.asDerivedRoot(root, root.getRelative("out")), execPath, ALL_OWNER); |
Han-Wen Nienhuys | 81b9083 | 2015-10-26 16:57:27 +0000 | [diff] [blame] | 330 | actions.add(new DummyAction(ImmutableList.<Artifact>of(), output)); |
janakr | 8541f6d | 2019-06-11 14:40:21 -0700 | [diff] [blame] | 331 | output.setGeneratingActionKey(ActionLookupData.create(ALL_OWNER, actions.size() - 1)); |
Han-Wen Nienhuys | 81b9083 | 2015-10-26 16:57:27 +0000 | [diff] [blame] | 332 | return output; |
| 333 | } |
| 334 | |
Janak Ramakrishnan | a5578af | 2017-03-21 17:28:39 +0000 | [diff] [blame] | 335 | private Artifact createMiddlemanArtifact(String path) { |
tomlu | 1cdcdf9 | 2018-01-16 11:07:51 -0800 | [diff] [blame] | 336 | ArtifactRoot middlemanRoot = |
| 337 | ArtifactRoot.middlemanRoot(middlemanPath, middlemanPath.getRelative("out")); |
janakr | aea0560 | 2019-05-22 15:41:29 -0700 | [diff] [blame] | 338 | return new Artifact.DerivedArtifact( |
janakr | 3290e22 | 2019-05-29 16:34:22 -0700 | [diff] [blame] | 339 | middlemanRoot, middlemanRoot.getExecPath().getRelative(path), ALL_OWNER); |
Janak Ramakrishnan | a5578af | 2017-03-21 17:28:39 +0000 | [diff] [blame] | 340 | } |
| 341 | |
cpeyser | ac09f0a | 2018-02-05 09:33:15 -0800 | [diff] [blame] | 342 | private SpecialArtifact createDerivedTreeArtifactWithAction(String path) { |
| 343 | SpecialArtifact treeArtifact = createDerivedTreeArtifactOnly(path); |
Rumou Duan | 7387620 | 2016-06-06 18:52:08 +0000 | [diff] [blame] | 344 | actions.add(new DummyAction(ImmutableList.<Artifact>of(), treeArtifact)); |
| 345 | return treeArtifact; |
| 346 | } |
| 347 | |
cpeyser | ac09f0a | 2018-02-05 09:33:15 -0800 | [diff] [blame] | 348 | private SpecialArtifact createDerivedTreeArtifactOnly(String path) { |
nharmata | b4060b6 | 2017-04-04 17:11:39 +0000 | [diff] [blame] | 349 | PathFragment execPath = PathFragment.create("out").getRelative(path); |
Rumou Duan | 7387620 | 2016-06-06 18:52:08 +0000 | [diff] [blame] | 350 | return new SpecialArtifact( |
tomlu | 1cdcdf9 | 2018-01-16 11:07:51 -0800 | [diff] [blame] | 351 | ArtifactRoot.asDerivedRoot(root, root.getRelative("out")), |
Rumou Duan | 7387620 | 2016-06-06 18:52:08 +0000 | [diff] [blame] | 352 | execPath, |
janakr | 3290e22 | 2019-05-29 16:34:22 -0700 | [diff] [blame] | 353 | ALL_OWNER, |
Rumou Duan | 7387620 | 2016-06-06 18:52:08 +0000 | [diff] [blame] | 354 | SpecialArtifactType.TREE); |
| 355 | } |
| 356 | |
cpeyser | ac09f0a | 2018-02-05 09:33:15 -0800 | [diff] [blame] | 357 | private TreeFileArtifact createFakeTreeFileArtifact( |
janakr | 45b308a | 2018-06-08 12:51:58 -0700 | [diff] [blame] | 358 | SpecialArtifact treeArtifact, |
janakr | 45b308a | 2018-06-08 12:51:58 -0700 | [diff] [blame] | 359 | String parentRelativePath, |
| 360 | String content) |
| 361 | throws Exception { |
| 362 | TreeFileArtifact treeFileArtifact = |
janakr | efb3f15 | 2019-06-05 17:42:34 -0700 | [diff] [blame] | 363 | ActionsTestUtil.createTreeFileArtifactWithNoGeneratingAction( |
| 364 | treeArtifact, parentRelativePath); |
Rumou Duan | 7387620 | 2016-06-06 18:52:08 +0000 | [diff] [blame] | 365 | Path path = treeFileArtifact.getPath(); |
| 366 | FileSystemUtils.createDirectoryAndParents(path.getParentDirectory()); |
| 367 | writeFile(path, content); |
| 368 | return treeFileArtifact; |
| 369 | } |
| 370 | |
Han-Wen Nienhuys | 81b9083 | 2015-10-26 16:57:27 +0000 | [diff] [blame] | 371 | private void assertValueMatches(FileStatus file, byte[] digest, FileArtifactValue value) |
| 372 | throws IOException { |
lberki | aea56b3 | 2017-05-30 12:35:33 +0200 | [diff] [blame] | 373 | assertThat(value.getSize()).isEqualTo(file.getSize()); |
Han-Wen Nienhuys | 81b9083 | 2015-10-26 16:57:27 +0000 | [diff] [blame] | 374 | if (digest == null) { |
lberki | aea56b3 | 2017-05-30 12:35:33 +0200 | [diff] [blame] | 375 | assertThat(value.getDigest()).isNull(); |
| 376 | assertThat(value.getModifiedTime()).isEqualTo(file.getLastModifiedTime()); |
Han-Wen Nienhuys | 81b9083 | 2015-10-26 16:57:27 +0000 | [diff] [blame] | 377 | } else { |
lberki | aea56b3 | 2017-05-30 12:35:33 +0200 | [diff] [blame] | 378 | assertThat(value.getDigest()).isEqualTo(digest); |
Han-Wen Nienhuys | 81b9083 | 2015-10-26 16:57:27 +0000 | [diff] [blame] | 379 | } |
| 380 | } |
| 381 | |
| 382 | private FileArtifactValue evaluateFAN(Artifact artifact) throws Throwable { |
| 383 | return ((FileArtifactValue) evaluateArtifactValue(artifact)); |
| 384 | } |
| 385 | |
Janak Ramakrishnan | ad77f97 | 2016-07-29 20:58:42 +0000 | [diff] [blame] | 386 | private SkyValue evaluateArtifactValue(Artifact artifact) throws Throwable { |
cushon | 03e7018 | 2017-09-15 09:33:27 +0200 | [diff] [blame] | 387 | return evaluateArtifactValue(artifact, /* mandatory= */ true); |
Han-Wen Nienhuys | 81b9083 | 2015-10-26 16:57:27 +0000 | [diff] [blame] | 388 | } |
| 389 | |
Janak Ramakrishnan | ad77f97 | 2016-07-29 20:58:42 +0000 | [diff] [blame] | 390 | private SkyValue evaluateArtifactValue(Artifact artifact, boolean mandatory) throws Throwable { |
| 391 | SkyKey key = ArtifactSkyKey.key(artifact, mandatory); |
| 392 | EvaluationResult<SkyValue> result = evaluate(ImmutableList.of(key).toArray(new SkyKey[0])); |
Han-Wen Nienhuys | 81b9083 | 2015-10-26 16:57:27 +0000 | [diff] [blame] | 393 | if (result.hasError()) { |
| 394 | throw result.getError().getException(); |
| 395 | } |
janakr | 8541f6d | 2019-06-11 14:40:21 -0700 | [diff] [blame] | 396 | SkyValue value = result.get(key); |
| 397 | if (value instanceof ActionExecutionValue) { |
| 398 | return ArtifactFunction.createSimpleFileArtifactValue( |
| 399 | (Artifact.DerivedArtifact) artifact, (ActionExecutionValue) value); |
| 400 | } |
| 401 | return value; |
Han-Wen Nienhuys | 81b9083 | 2015-10-26 16:57:27 +0000 | [diff] [blame] | 402 | } |
| 403 | |
janakr | 0175ce3 | 2018-02-26 15:54:57 -0800 | [diff] [blame] | 404 | private void setGeneratingActions() throws InterruptedException, ActionConflictException { |
janakr | 573807d | 2018-01-11 14:02:35 -0800 | [diff] [blame] | 405 | if (evaluator.getExistingValue(ALL_OWNER) == null) { |
janakr | 93e3eea | 2017-03-30 22:09:37 +0000 | [diff] [blame] | 406 | differencer.inject( |
| 407 | ImmutableMap.of( |
janakr | 573807d | 2018-01-11 14:02:35 -0800 | [diff] [blame] | 408 | ALL_OWNER, |
cparsons | e2d200f | 2018-03-06 16:15:11 -0800 | [diff] [blame] | 409 | new BasicActionLookupValue( |
janakr | efb3f15 | 2019-06-05 17:42:34 -0700 | [diff] [blame] | 410 | Actions.assignOwnersAndFilterSharedActionsAndThrowActionConflict( |
| 411 | actionKeyContext, |
| 412 | ImmutableList.copyOf(actions), |
| 413 | ALL_OWNER, |
| 414 | /*outputFiles=*/ null), |
janakr | a81bb95 | 2019-01-28 17:30:06 -0800 | [diff] [blame] | 415 | /*nonceVersion=*/ null))); |
Han-Wen Nienhuys | 81b9083 | 2015-10-26 16:57:27 +0000 | [diff] [blame] | 416 | } |
| 417 | } |
| 418 | |
| 419 | private <E extends SkyValue> EvaluationResult<E> evaluate(SkyKey... keys) |
janakr | 0175ce3 | 2018-02-26 15:54:57 -0800 | [diff] [blame] | 420 | throws InterruptedException, ActionConflictException { |
Han-Wen Nienhuys | 81b9083 | 2015-10-26 16:57:27 +0000 | [diff] [blame] | 421 | setGeneratingActions(); |
Googler | 1002867 | 2018-10-25 12:14:34 -0700 | [diff] [blame] | 422 | EvaluationContext evaluationContext = |
| 423 | EvaluationContext.newBuilder() |
| 424 | .setKeepGoing(false) |
| 425 | .setNumThreads(SkyframeExecutor.DEFAULT_THREAD_COUNT) |
| 426 | .setEventHander(NullEventHandler.INSTANCE) |
| 427 | .build(); |
| 428 | return driver.evaluate(Arrays.asList(keys), evaluationContext); |
Han-Wen Nienhuys | 81b9083 | 2015-10-26 16:57:27 +0000 | [diff] [blame] | 429 | } |
| 430 | |
Han-Wen Nienhuys | 81b9083 | 2015-10-26 16:57:27 +0000 | [diff] [blame] | 431 | /** Value Builder for actions that just stats and stores the output file (which must exist). */ |
Rumou Duan | 7387620 | 2016-06-06 18:52:08 +0000 | [diff] [blame] | 432 | private static class SimpleActionExecutionFunction implements SkyFunction { |
Han-Wen Nienhuys | 81b9083 | 2015-10-26 16:57:27 +0000 | [diff] [blame] | 433 | @Override |
janakr | 93e3eea | 2017-03-30 22:09:37 +0000 | [diff] [blame] | 434 | public SkyValue compute(SkyKey skyKey, Environment env) throws InterruptedException { |
janakr | 0c42fc8 | 2018-09-14 10:37:25 -0700 | [diff] [blame] | 435 | Map<Artifact, ArtifactFileMetadata> artifactData = new HashMap<>(); |
Rumou Duan | 7387620 | 2016-06-06 18:52:08 +0000 | [diff] [blame] | 436 | Map<Artifact, TreeArtifactValue> treeArtifactData = new HashMap<>(); |
| 437 | Map<Artifact, FileArtifactValue> additionalOutputData = new HashMap<>(); |
janakr | 93e3eea | 2017-03-30 22:09:37 +0000 | [diff] [blame] | 438 | ActionLookupData actionLookupData = (ActionLookupData) skyKey.argument(); |
| 439 | ActionLookupValue actionLookupValue = |
janakr | baf52ae | 2018-02-14 09:03:18 -0800 | [diff] [blame] | 440 | (ActionLookupValue) env.getValue(actionLookupData.getActionLookupKey()); |
janakr | 93e3eea | 2017-03-30 22:09:37 +0000 | [diff] [blame] | 441 | Action action = actionLookupValue.getAction(actionLookupData.getActionIndex()); |
Han-Wen Nienhuys | 81b9083 | 2015-10-26 16:57:27 +0000 | [diff] [blame] | 442 | Artifact output = Iterables.getOnlyElement(action.getOutputs()); |
Rumou Duan | 7387620 | 2016-06-06 18:52:08 +0000 | [diff] [blame] | 443 | |
| 444 | try { |
| 445 | if (output.isTreeArtifact()) { |
| 446 | TreeFileArtifact treeFileArtifact1 = ActionInputHelper.treeFileArtifact( |
cpeyser | ac09f0a | 2018-02-05 09:33:15 -0800 | [diff] [blame] | 447 | (SpecialArtifact) output, PathFragment.create("child1")); |
Rumou Duan | 7387620 | 2016-06-06 18:52:08 +0000 | [diff] [blame] | 448 | TreeFileArtifact treeFileArtifact2 = ActionInputHelper.treeFileArtifact( |
cpeyser | ac09f0a | 2018-02-05 09:33:15 -0800 | [diff] [blame] | 449 | (SpecialArtifact) output, PathFragment.create("child2")); |
Rumou Duan | 7387620 | 2016-06-06 18:52:08 +0000 | [diff] [blame] | 450 | TreeArtifactValue treeArtifactValue = TreeArtifactValue.create(ImmutableMap.of( |
| 451 | treeFileArtifact1, FileArtifactValue.create(treeFileArtifact1), |
| 452 | treeFileArtifact2, FileArtifactValue.create(treeFileArtifact2))); |
| 453 | treeArtifactData.put(output, treeArtifactValue); |
| 454 | } else if (action.getActionType() == MiddlemanType.NORMAL) { |
janakr | 0c42fc8 | 2018-09-14 10:37:25 -0700 | [diff] [blame] | 455 | ArtifactFileMetadata fileValue = |
| 456 | ActionMetadataHandler.fileMetadataFromArtifact(output, null, null); |
Han-Wen Nienhuys | 81b9083 | 2015-10-26 16:57:27 +0000 | [diff] [blame] | 457 | artifactData.put(output, fileValue); |
Rumou Duan | 7387620 | 2016-06-06 18:52:08 +0000 | [diff] [blame] | 458 | additionalOutputData.put(output, FileArtifactValue.create(output, fileValue)); |
| 459 | } else { |
| 460 | additionalOutputData.put(output, FileArtifactValue.DEFAULT_MIDDLEMAN); |
Han-Wen Nienhuys | 81b9083 | 2015-10-26 16:57:27 +0000 | [diff] [blame] | 461 | } |
Rumou Duan | 7387620 | 2016-06-06 18:52:08 +0000 | [diff] [blame] | 462 | } catch (IOException e) { |
| 463 | throw new IllegalStateException(e); |
Han-Wen Nienhuys | 81b9083 | 2015-10-26 16:57:27 +0000 | [diff] [blame] | 464 | } |
janakr | b9d8d58 | 2018-06-13 21:57:19 -0700 | [diff] [blame] | 465 | return ActionExecutionValue.create( |
| 466 | artifactData, |
| 467 | treeArtifactData, |
| 468 | additionalOutputData, |
| 469 | /*outputSymlinks=*/ null, |
shahan | ef6f4cf | 2018-06-26 11:24:59 -0700 | [diff] [blame] | 470 | /*discoveredModules=*/ null, |
janakr | 9f496f3 | 2018-10-24 15:08:09 -0700 | [diff] [blame] | 471 | /*actionDependsOnBuildId=*/ false); |
Han-Wen Nienhuys | 81b9083 | 2015-10-26 16:57:27 +0000 | [diff] [blame] | 472 | } |
| 473 | |
| 474 | @Override |
| 475 | public String extractTag(SkyKey skyKey) { |
| 476 | return null; |
| 477 | } |
| 478 | } |
Han-Wen Nienhuys | 81b9083 | 2015-10-26 16:57:27 +0000 | [diff] [blame] | 479 | } |