View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.maven.buildcache;
20  
21  import java.util.ArrayList;
22  import java.util.HashMap;
23  import java.util.LinkedList;
24  import java.util.List;
25  import java.util.Map;
26  import java.util.Objects;
27  import java.util.Optional;
28  import java.util.Set;
29  import java.util.stream.Collectors;
30  
31  import org.apache.commons.lang3.Strings;
32  import org.apache.maven.buildcache.xml.CacheConfig;
33  import org.apache.maven.buildcache.xml.build.Build;
34  import org.apache.maven.buildcache.xml.build.CompletedExecution;
35  import org.apache.maven.buildcache.xml.build.DigestItem;
36  import org.apache.maven.buildcache.xml.build.ProjectsInputInfo;
37  import org.apache.maven.buildcache.xml.build.PropertyValue;
38  import org.apache.maven.buildcache.xml.diff.Diff;
39  import org.apache.maven.buildcache.xml.diff.Mismatch;
40  
41  /**
42   * Utility class for comparing 2 builds
43   */
44  public class CacheDiff {
45  
46      private final CacheConfig config;
47      private final Build current;
48      private final Build baseline;
49      private final LinkedList<Mismatch> report;
50  
51      public CacheDiff(Build current, Build baseline, CacheConfig config) {
52          this.current = current;
53          this.baseline = baseline;
54          this.config = config;
55          this.report = new LinkedList<>();
56      }
57  
58      public Diff compare() {
59          if (!Strings.CS.equals(current.getHashFunction(), baseline.getHashFunction())) {
60              addNewMismatch(
61                      "hashFunction",
62                      current.getHashFunction(),
63                      baseline.getHashFunction(),
64                      "Different algorithms render caches not comparable and cached could not be reused",
65                      "Ensure the same algorithm as remote");
66          }
67          compareEffectivePoms(current.getProjectsInputInfo(), baseline.getProjectsInputInfo());
68          compareExecutions(current.getExecutions(), baseline.getExecutions());
69          compareFiles(current.getProjectsInputInfo(), baseline.getProjectsInputInfo());
70          compareDependencies(current.getProjectsInputInfo(), baseline.getProjectsInputInfo());
71          comparePluginDependencies(current.getProjectsInputInfo(), baseline.getProjectsInputInfo());
72  
73          final Diff buildDiffType = new Diff();
74          buildDiffType.getMismatches().addAll(report);
75          return buildDiffType;
76      }
77  
78      private void compareEffectivePoms(ProjectsInputInfo current, ProjectsInputInfo baseline) {
79          Optional<DigestItem> currentPom = findPom(current);
80          String currentPomHash = currentPom.map(DigestItem::getHash).orElse(null);
81  
82          Optional<DigestItem> baseLinePom = findPom(baseline);
83          String baselinePomHash = baseLinePom.map(DigestItem::getHash).orElse(null);
84  
85          if (!Strings.CS.equals(currentPomHash, baselinePomHash)) {
86              addNewMismatch(
87                      "effectivePom",
88                      currentPomHash,
89                      baselinePomHash,
90                      "Difference in effective pom suggests effectively different builds which cannot be reused",
91                      "Compare raw content of effective poms and eliminate differences. "
92                              + "See How-To for common techniques");
93          }
94      }
95  
96      public static Optional<DigestItem> findPom(ProjectsInputInfo projectInputs) {
97          for (DigestItem digestItemType : projectInputs.getItems()) {
98              if ("pom".equals(digestItemType.getType())) {
99                  return Optional.of(digestItemType);
100             }
101         }
102         return Optional.empty();
103     }
104 
105     @SuppressWarnings("checkstyle:LineLength")
106     private void compareFiles(ProjectsInputInfo current, ProjectsInputInfo baseline) {
107         final Map<String, DigestItem> currentFiles = current.getItems().stream()
108                 .filter(item -> "file".equals(item.getType()))
109                 .collect(Collectors.toMap(DigestItem::getValue, item -> item));
110 
111         final Map<String, DigestItem> baselineFiles = baseline.getItems().stream()
112                 .filter(item -> "file".equals(item.getType()))
113                 .collect(Collectors.toMap(DigestItem::getValue, item -> item));
114 
115         if (!Objects.equals(currentFiles.keySet(), baselineFiles.keySet())) {
116             Set<String> currentVsBaseline = diff(currentFiles.keySet(), baselineFiles.keySet());
117             Set<String> baselineVsCurrent = diff(baselineFiles.keySet(), currentFiles.keySet());
118 
119             addNewMismatch(
120                     "source files",
121                     "Remote and local cache contain different sets of input files. " + "Added files: "
122                             + currentVsBaseline + ". Removed files: " + baselineVsCurrent,
123                     "To match remote and local caches should have identical file sets."
124                             + " Unnecessary and transient files must be filtered out to make file sets match"
125                             + " - see configuration guide");
126             return;
127         }
128 
129         for (Map.Entry<String, DigestItem> entry : currentFiles.entrySet()) {
130             String filePath = entry.getKey();
131             DigestItem currentFile = entry.getValue();
132             // should be null safe because sets are compared above for differences
133             final DigestItem baselineFile = baselineFiles.get(filePath);
134             if (!Strings.CS.equals(currentFile.getHash(), baselineFile.getHash())) {
135                 String reason = "File content is different.";
136                 if (currentFile.getEol() != null
137                         && baselineFile.getEol() != null
138                         && !Strings.CS.equals(baselineFile.getEol(), currentFile.getEol())) {
139                     reason += " Different line endings detected (text files relevant). " + "Remote: "
140                             + baselineFile.getEol() + ", local: " + currentFile.getEol() + ".";
141                 }
142                 if (currentFile.getCharset() != null
143                         && baselineFile.getCharset() != null
144                         && !Strings.CS.equals(baselineFile.getCharset(), currentFile.getCharset())) {
145                     reason += " Different charset detected (text files relevant). " + "Remote: " + baselineFile.getEol()
146                             + ", local: " + currentFile.getEol() + ".";
147                 }
148 
149                 addNewMismatch(
150                         filePath,
151                         currentFile.getHash(),
152                         baselineFile.getHash(),
153                         reason,
154                         "Different content manifests different build outcome. "
155                                 + "Ensure that difference is not caused by environment specifics, like line separators");
156             }
157         }
158     }
159 
160     private void compareDependencies(ProjectsInputInfo current, ProjectsInputInfo baseline) {
161         compareDependencies(
162                 "dependencies files",
163                 current.getItems().stream()
164                         .filter(item -> "dependency".equals(item.getType()))
165                         .collect(Collectors.toList()),
166                 baseline.getItems().stream()
167                         .filter(item -> "dependency".equals(item.getType()))
168                         .collect(Collectors.toList()));
169     }
170 
171     private void comparePluginDependencies(ProjectsInputInfo current, ProjectsInputInfo baseline) {
172         compareDependencies(
173                 "plugin dependencies files",
174                 current.getItems().stream()
175                         .filter(item -> "pluginDependency".equals(item.getType()))
176                         .collect(Collectors.toList()),
177                 baseline.getItems().stream()
178                         .filter(item -> "pluginDependency".equals(item.getType()))
179                         .collect(Collectors.toList()));
180     }
181 
182     private void compareDependencies(String property, List<DigestItem> current, List<DigestItem> baseline) {
183         final Map<String, DigestItem> currentDependencies =
184                 current.stream().collect(Collectors.toMap(DigestItem::getValue, item -> item));
185 
186         final Map<String, DigestItem> baselineDependencies =
187                 baseline.stream().collect(Collectors.toMap(DigestItem::getValue, item -> item));
188 
189         if (!Objects.equals(currentDependencies.keySet(), baselineDependencies.keySet())) {
190             Set<String> currentVsBaseline = diff(currentDependencies.keySet(), baselineDependencies.keySet());
191             Set<String> baselineVsCurrent = diff(baselineDependencies.keySet(), currentDependencies.keySet());
192 
193             addNewMismatch(
194                     property,
195                     "Remote and local builds contain different sets of dependencies and cannot be matched. "
196                             + "Added dependencies: " + currentVsBaseline + ". Removed dependencies: "
197                             + baselineVsCurrent,
198                     "Remote and local builds should have identical dependencies. "
199                             + "The difference manifests changes in downstream dependencies or introduced snapshots.");
200             return;
201         }
202 
203         for (Map.Entry<String, DigestItem> entry : currentDependencies.entrySet()) {
204             String dependencyKey = entry.getKey();
205             DigestItem currentDependency = entry.getValue();
206             // null safe - sets compared for differences above
207             final DigestItem baselineDependency = baselineDependencies.get(dependencyKey);
208             if (!Strings.CS.equals(currentDependency.getHash(), baselineDependency.getHash())) {
209                 addNewMismatch(
210                         dependencyKey,
211                         currentDependency.getHash(),
212                         baselineDependency.getHash(),
213                         "Downstream project or snapshot changed",
214                         "Find downstream project and investigate difference in the downstream project. "
215                                 + "Enable fail fast mode and single threaded execution to simplify debug.");
216             }
217         }
218     }
219 
220     private void compareExecutions(List<CompletedExecution> current, List<CompletedExecution> baseline) {
221         Map<String, CompletedExecution> baselineExecutionsByKey = new HashMap<>();
222         for (CompletedExecution completedExecutionType : baseline) {
223             baselineExecutionsByKey.put(completedExecutionType.getExecutionKey(), completedExecutionType);
224         }
225 
226         Map<String, CompletedExecution> currentExecutionsByKey = new HashMap<>();
227         for (CompletedExecution e1 : current) {
228             currentExecutionsByKey.put(e1.getExecutionKey(), e1);
229         }
230 
231         // such situation normally means different poms and mismatch in effective poms,
232         // but in any case it is helpful to report
233         for (CompletedExecution baselineExecution : baseline) {
234             if (!currentExecutionsByKey.containsKey(baselineExecution.getExecutionKey())) {
235                 addNewMismatch(
236                         baselineExecution.getExecutionKey(),
237                         "Baseline build contains excessive plugin " + baselineExecution.getExecutionKey(),
238                         "Different set of plugins produces different build results. "
239                                 + "Exclude non-critical plugins or make sure plugin sets match");
240             }
241         }
242 
243         for (CompletedExecution currentExecution : current) {
244             if (!baselineExecutionsByKey.containsKey(currentExecution.getExecutionKey())) {
245                 addNewMismatch(
246                         currentExecution.getExecutionKey(),
247                         "Cached build doesn't contain plugin " + currentExecution.getExecutionKey(),
248                         "Different set of plugins produces different build results. "
249                                 + "Filter out non-critical plugins or make sure remote cache always run full build "
250                                 + "with all plugins");
251                 continue;
252             }
253 
254             final CompletedExecution baselineExecution =
255                     baselineExecutionsByKey.get(currentExecution.getExecutionKey());
256             comparePlugins(currentExecution, baselineExecution);
257         }
258     }
259 
260     private void comparePlugins(CompletedExecution current, CompletedExecution baseline) {
261         // TODO add support for skip values
262         final List<PropertyValue> trackedProperties = new ArrayList<>();
263         for (PropertyValue propertyValueType : current.getProperties()) {
264             if (propertyValueType.isTracked()) {
265                 trackedProperties.add(propertyValueType);
266             }
267         }
268         if (trackedProperties.isEmpty()) {
269             return;
270         }
271 
272         final Map<String, PropertyValue> baselinePropertiesByName = new HashMap<>();
273         for (PropertyValue propertyValueType : baseline.getProperties()) {
274             baselinePropertiesByName.put(propertyValueType.getName(), propertyValueType);
275         }
276 
277         for (PropertyValue p : trackedProperties) {
278             final PropertyValue baselineValue = baselinePropertiesByName.get(p.getName());
279             if (baselineValue == null || !Strings.CS.equals(baselineValue.getValue(), p.getValue())) {
280                 addNewMismatch(
281                         p.getName(),
282                         p.getValue(),
283                         baselineValue == null ? null : baselineValue.getValue(),
284                         "Plugin: " + current.getExecutionKey()
285                                 + " has mismatch in tracked property and cannot be reused",
286                         "Align properties between remote and local build or remove property from tracked "
287                                 + "list if mismatch could be tolerated. In some cases it is possible to add skip value "
288                                 + "to ignore lax mismatch");
289             }
290         }
291     }
292 
293     private void addNewMismatch(String item, String current, String baseline, String reason, String resolution) {
294         final Mismatch mismatch = new Mismatch();
295         mismatch.setItem(item);
296         mismatch.setCurrent(current);
297         mismatch.setBaseline(baseline);
298         mismatch.setReason(reason);
299         mismatch.setResolution(resolution);
300         report.add(mismatch);
301     }
302 
303     private void addNewMismatch(String property, String reason, String resolution) {
304         final Mismatch mismatchType = new Mismatch();
305         mismatchType.setItem(property);
306         mismatchType.setReason(reason);
307         mismatchType.setResolution(resolution);
308         report.add(mismatchType);
309     }
310 
311     private static <T> Set<T> diff(Set<T> a, Set<T> b) {
312         return a.stream().filter(v -> !b.contains(v)).collect(Collectors.toSet());
313     }
314 }