1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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
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
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
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
232
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
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 }