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.StringUtils;
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 (!StringUtils.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  
72          final Diff buildDiffType = new Diff();
73          buildDiffType.getMismatches().addAll(report);
74          return buildDiffType;
75      }
76  
77      private void compareEffectivePoms(ProjectsInputInfo current, ProjectsInputInfo baseline) {
78          Optional<DigestItem> currentPom = findPom(current);
79          String currentPomHash = currentPom.map(DigestItem::getHash).orElse(null);
80  
81          Optional<DigestItem> baseLinePom = findPom(baseline);
82          String baselinePomHash = baseLinePom.map(DigestItem::getHash).orElse(null);
83  
84          if (!StringUtils.equals(currentPomHash, baselinePomHash)) {
85              addNewMismatch(
86                      "effectivePom",
87                      currentPomHash,
88                      baselinePomHash,
89                      "Difference in effective pom suggests effectively different builds which cannot be reused",
90                      "Compare raw content of effective poms and eliminate differences. "
91                              + "See How-To for common techniques");
92          }
93      }
94  
95      public static Optional<DigestItem> findPom(ProjectsInputInfo projectInputs) {
96          for (DigestItem digestItemType : projectInputs.getItems()) {
97              if ("pom".equals(digestItemType.getType())) {
98                  return Optional.of(digestItemType);
99              }
100         }
101         return Optional.empty();
102     }
103 
104     @SuppressWarnings("checkstyle:LineLength")
105     private void compareFiles(ProjectsInputInfo current, ProjectsInputInfo baseline) {
106         final Map<String, DigestItem> currentFiles = current.getItems().stream()
107                 .filter(item -> "file".equals(item.getType()))
108                 .collect(Collectors.toMap(DigestItem::getValue, item -> item));
109 
110         final Map<String, DigestItem> baselineFiles = baseline.getItems().stream()
111                 .filter(item -> "file".equals(item.getType()))
112                 .collect(Collectors.toMap(DigestItem::getValue, item -> item));
113 
114         if (!Objects.equals(currentFiles.keySet(), baselineFiles.keySet())) {
115             Set<String> currentVsBaseline = diff(currentFiles.keySet(), baselineFiles.keySet());
116             Set<String> baselineVsCurrent = diff(baselineFiles.keySet(), currentFiles.keySet());
117 
118             addNewMismatch(
119                     "source files",
120                     "Remote and local cache contain different sets of input files. " + "Added files: "
121                             + currentVsBaseline + ". Removed files: " + baselineVsCurrent,
122                     "To match remote and local caches should have identical file sets."
123                             + " Unnecessary and transient files must be filtered out to make file sets match"
124                             + " - see configuration guide");
125             return;
126         }
127 
128         for (Map.Entry<String, DigestItem> entry : currentFiles.entrySet()) {
129             String filePath = entry.getKey();
130             DigestItem currentFile = entry.getValue();
131             // should be null safe because sets are compared above for differences
132             final DigestItem baselineFile = baselineFiles.get(filePath);
133             if (!StringUtils.equals(currentFile.getHash(), baselineFile.getHash())) {
134                 String reason = "File content is different.";
135                 if (currentFile.getEol() != null
136                         && baselineFile.getEol() != null
137                         && !StringUtils.equals(baselineFile.getEol(), currentFile.getEol())) {
138                     reason += " Different line endings detected (text files relevant). " + "Remote: "
139                             + baselineFile.getEol() + ", local: " + currentFile.getEol() + ".";
140                 }
141                 if (currentFile.getCharset() != null
142                         && baselineFile.getCharset() != null
143                         && !StringUtils.equals(baselineFile.getCharset(), currentFile.getCharset())) {
144                     reason += " Different charset detected (text files relevant). " + "Remote: " + baselineFile.getEol()
145                             + ", local: " + currentFile.getEol() + ".";
146                 }
147 
148                 addNewMismatch(
149                         filePath,
150                         currentFile.getHash(),
151                         baselineFile.getHash(),
152                         reason,
153                         "Different content manifests different build outcome. "
154                                 + "Ensure that difference is not caused by environment specifics, like line separators");
155             }
156         }
157     }
158 
159     private void compareDependencies(ProjectsInputInfo current, ProjectsInputInfo baseline) {
160         final Map<String, DigestItem> currentDependencies = current.getItems().stream()
161                 .filter(item -> "dependency".equals(item.getType()))
162                 .collect(Collectors.toMap(DigestItem::getValue, item -> item));
163 
164         final Map<String, DigestItem> baselineDependencies = baseline.getItems().stream()
165                 .filter(item -> "dependency".equals(item.getType()))
166                 .collect(Collectors.toMap(DigestItem::getValue, item -> item));
167 
168         if (!Objects.equals(currentDependencies.keySet(), baselineDependencies.keySet())) {
169             Set<String> currentVsBaseline = diff(currentDependencies.keySet(), baselineDependencies.keySet());
170             Set<String> baselineVsCurrent = diff(baselineDependencies.keySet(), currentDependencies.keySet());
171 
172             addNewMismatch(
173                     "dependencies files",
174                     "Remote and local builds contain different sets of dependencies and cannot be matched. "
175                             + "Added dependencies: " + currentVsBaseline + ". Removed dependencies: "
176                             + baselineVsCurrent,
177                     "Remote and local builds should have identical dependencies. "
178                             + "The difference manifests changes in downstream dependencies or introduced snapshots.");
179             return;
180         }
181 
182         for (Map.Entry<String, DigestItem> entry : currentDependencies.entrySet()) {
183             String dependencyKey = entry.getKey();
184             DigestItem currentDependency = entry.getValue();
185             // null safe - sets compared for differences above
186             final DigestItem baselineDependency = baselineDependencies.get(dependencyKey);
187             if (!StringUtils.equals(currentDependency.getHash(), baselineDependency.getHash())) {
188                 addNewMismatch(
189                         dependencyKey,
190                         currentDependency.getHash(),
191                         baselineDependency.getHash(),
192                         "Downstream project or snapshot changed",
193                         "Find downstream project and investigate difference in the downstream project. "
194                                 + "Enable fail fast mode and single threaded execution to simplify debug.");
195             }
196         }
197     }
198 
199     private void compareExecutions(List<CompletedExecution> current, List<CompletedExecution> baseline) {
200         Map<String, CompletedExecution> baselineExecutionsByKey = new HashMap<>();
201         for (CompletedExecution completedExecutionType : baseline) {
202             baselineExecutionsByKey.put(completedExecutionType.getExecutionKey(), completedExecutionType);
203         }
204 
205         Map<String, CompletedExecution> currentExecutionsByKey = new HashMap<>();
206         for (CompletedExecution e1 : current) {
207             currentExecutionsByKey.put(e1.getExecutionKey(), e1);
208         }
209 
210         // such situation normally means different poms and mismatch in effective poms,
211         // but in any case it is helpful to report
212         for (CompletedExecution baselineExecution : baseline) {
213             if (!currentExecutionsByKey.containsKey(baselineExecution.getExecutionKey())) {
214                 addNewMismatch(
215                         baselineExecution.getExecutionKey(),
216                         "Baseline build contains excessive plugin " + baselineExecution.getExecutionKey(),
217                         "Different set of plugins produces different build results. "
218                                 + "Exclude non-critical plugins or make sure plugin sets match");
219             }
220         }
221 
222         for (CompletedExecution currentExecution : current) {
223             if (!baselineExecutionsByKey.containsKey(currentExecution.getExecutionKey())) {
224                 addNewMismatch(
225                         currentExecution.getExecutionKey(),
226                         "Cached build doesn't contain plugin " + currentExecution.getExecutionKey(),
227                         "Different set of plugins produces different build results. "
228                                 + "Filter out non-critical plugins or make sure remote cache always run full build "
229                                 + "with all plugins");
230                 continue;
231             }
232 
233             final CompletedExecution baselineExecution =
234                     baselineExecutionsByKey.get(currentExecution.getExecutionKey());
235             comparePlugins(currentExecution, baselineExecution);
236         }
237     }
238 
239     private void comparePlugins(CompletedExecution current, CompletedExecution baseline) {
240         // TODO add support for skip values
241         final List<PropertyValue> trackedProperties = new ArrayList<>();
242         for (PropertyValue propertyValueType : current.getProperties()) {
243             if (propertyValueType.isTracked()) {
244                 trackedProperties.add(propertyValueType);
245             }
246         }
247         if (trackedProperties.isEmpty()) {
248             return;
249         }
250 
251         final Map<String, PropertyValue> baselinePropertiesByName = new HashMap<>();
252         for (PropertyValue propertyValueType : baseline.getProperties()) {
253             baselinePropertiesByName.put(propertyValueType.getName(), propertyValueType);
254         }
255 
256         for (PropertyValue p : trackedProperties) {
257             final PropertyValue baselineValue = baselinePropertiesByName.get(p.getName());
258             if (baselineValue == null || !StringUtils.equals(baselineValue.getValue(), p.getValue())) {
259                 addNewMismatch(
260                         p.getName(),
261                         p.getValue(),
262                         baselineValue == null ? null : baselineValue.getValue(),
263                         "Plugin: " + current.getExecutionKey()
264                                 + " has mismatch in tracked property and cannot be reused",
265                         "Align properties between remote and local build or remove property from tracked "
266                                 + "list if mismatch could be tolerated. In some cases it is possible to add skip value "
267                                 + "to ignore lax mismatch");
268             }
269         }
270     }
271 
272     private void addNewMismatch(String item, String current, String baseline, String reason, String resolution) {
273         final Mismatch mismatch = new Mismatch();
274         mismatch.setItem(item);
275         mismatch.setCurrent(current);
276         mismatch.setBaseline(baseline);
277         mismatch.setReason(reason);
278         mismatch.setResolution(resolution);
279         report.add(mismatch);
280     }
281 
282     private void addNewMismatch(String property, String reason, String resolution) {
283         final Mismatch mismatchType = new Mismatch();
284         mismatchType.setItem(property);
285         mismatchType.setReason(reason);
286         mismatchType.setResolution(resolution);
287         report.add(mismatchType);
288     }
289 
290     private static <T> Set<T> diff(Set<T> a, Set<T> b) {
291         return a.stream().filter(v -> !b.contains(v)).collect(Collectors.toSet());
292     }
293 }