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.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
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
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
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
211
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
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 }