blob: d3b33b34e34b2bbea2621191466a01aea9aafdf3 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.maven.buildcache;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import org.apache.commons.lang3.StringUtils;
import org.apache.maven.buildcache.xml.CacheConfig;
import org.apache.maven.buildcache.xml.build.Build;
import org.apache.maven.buildcache.xml.build.CompletedExecution;
import org.apache.maven.buildcache.xml.build.DigestItem;
import org.apache.maven.buildcache.xml.build.ProjectsInputInfo;
import org.apache.maven.buildcache.xml.build.PropertyValue;
import org.apache.maven.buildcache.xml.diff.Diff;
import org.apache.maven.buildcache.xml.diff.Mismatch;
/**
* Utility class for comparing 2 builds
*/
public class CacheDiff
{
private final CacheConfig config;
private final Build current;
private final Build baseline;
private final LinkedList<Mismatch> report;
public CacheDiff( Build current, Build baseline, CacheConfig config )
{
this.current = current;
this.baseline = baseline;
this.config = config;
this.report = new LinkedList<>();
}
public Diff compare()
{
if ( !StringUtils.equals( current.getHashFunction(), baseline.getHashFunction() ) )
{
addNewMismatch(
"hashFunction",
current.getHashFunction(),
baseline.getHashFunction(),
"Different algorithms render caches not comparable and cached could not be reused",
"Ensure the same algorithm as remote" );
}
compareEffectivePoms( current.getProjectsInputInfo(), baseline.getProjectsInputInfo() );
compareExecutions( current.getExecutions(), baseline.getExecutions() );
compareFiles( current.getProjectsInputInfo(), baseline.getProjectsInputInfo() );
compareDependencies( current.getProjectsInputInfo(), baseline.getProjectsInputInfo() );
final Diff buildDiffType = new Diff();
buildDiffType.getMismatches().addAll( report );
return buildDiffType;
}
private void compareEffectivePoms( ProjectsInputInfo current, ProjectsInputInfo baseline )
{
Optional<DigestItem> currentPom = findPom( current );
String currentPomHash = currentPom.map( DigestItem::getHash ).orElse( null );
Optional<DigestItem> baseLinePom = findPom( baseline );
String baselinePomHash = baseLinePom.map( DigestItem::getHash ).orElse( null );
if ( !StringUtils.equals( currentPomHash, baselinePomHash ) )
{
addNewMismatch(
"effectivePom", currentPomHash, baselinePomHash,
"Difference in effective pom suggests effectively different builds which cannot be reused",
"Compare raw content of effective poms and eliminate differences. "
+ "See How-To for common techniques" );
}
}
public static Optional<DigestItem> findPom( ProjectsInputInfo projectInputs )
{
for ( DigestItem digestItemType : projectInputs.getItems() )
{
if ( "pom".equals( digestItemType.getType() ) )
{
return Optional.of( digestItemType );
}
}
return Optional.empty();
}
@SuppressWarnings( "checkstyle:LineLength" )
private void compareFiles( ProjectsInputInfo current, ProjectsInputInfo baseline )
{
final Map<String, DigestItem> currentFiles = current.getItems().stream()
.filter( item -> "file".equals( item.getType() ) )
.collect( Collectors.toMap( DigestItem::getValue, item -> item ) );
final Map<String, DigestItem> baselineFiles = baseline.getItems().stream()
.filter( item -> "file".equals( item.getType() ) )
.collect( Collectors.toMap( DigestItem::getValue, item -> item ) );
if ( !Objects.equals( currentFiles.keySet(), baselineFiles.keySet() ) )
{
Set<String> currentVsBaseline = diff( currentFiles.keySet(), baselineFiles.keySet() );
Set<String> baselineVsCurrent = diff( baselineFiles.keySet(), currentFiles.keySet() );
addNewMismatch( "source files",
"Remote and local cache contain different sets of input files. "
+ "Added files: " + currentVsBaseline + ". Removed files: "
+ baselineVsCurrent,
"To match remote and local caches should have identical file sets."
+ " Unnecessary and transient files must be filtered out to make file sets match"
+ " - see configuration guide" );
return;
}
for ( Map.Entry<String, DigestItem> entry : currentFiles.entrySet() )
{
String filePath = entry.getKey();
DigestItem currentFile = entry.getValue();
// should be null safe because sets are compared above for differences
final DigestItem baselineFile = baselineFiles.get( filePath );
if ( !StringUtils.equals( currentFile.getHash(), baselineFile.getHash() ) )
{
String reason = "File content is different.";
if ( currentFile.getEol() != null && baselineFile.getEol() != null && !StringUtils.equals(
baselineFile.getEol(), currentFile.getEol() ) )
{
reason += " Different line endings detected (text files relevant). "
+ "Remote: " + baselineFile.getEol() + ", local: " + currentFile.getEol() + ".";
}
if ( currentFile.getCharset() != null && baselineFile.getCharset() != null && !StringUtils.equals(
baselineFile.getCharset(), currentFile.getCharset() ) )
{
reason += " Different charset detected (text files relevant). "
+ "Remote: " + baselineFile.getEol() + ", local: " + currentFile.getEol() + ".";
}
addNewMismatch( filePath, currentFile.getHash(), baselineFile.getHash(), reason,
"Different content manifests different build outcome. "
+ "Ensure that difference is not caused by environment specifics, like line separators" );
}
}
}
private void compareDependencies( ProjectsInputInfo current, ProjectsInputInfo baseline )
{
final Map<String, DigestItem> currentDependencies = current.getItems().stream()
.filter( item -> "dependency".equals( item.getType() ) )
.collect( Collectors.toMap( DigestItem::getValue, item -> item ) );
final Map<String, DigestItem> baselineDependencies = baseline.getItems().stream()
.filter( item -> "dependency".equals( item.getType() ) )
.collect( Collectors.toMap( DigestItem::getValue, item -> item ) );
if ( !Objects.equals( currentDependencies.keySet(), baselineDependencies.keySet() ) )
{
Set<String> currentVsBaseline = diff( currentDependencies.keySet(), baselineDependencies.keySet() );
Set<String> baselineVsCurrent = diff( baselineDependencies.keySet(), currentDependencies.keySet() );
addNewMismatch( "dependencies files",
"Remote and local builds contain different sets of dependencies and cannot be matched. "
+ "Added dependencies: " + currentVsBaseline + ". Removed dependencies: "
+ baselineVsCurrent,
"Remote and local builds should have identical dependencies. "
+ "The difference manifests changes in downstream dependencies or "
+ "introduced snapshots." );
return;
}
for ( Map.Entry<String, DigestItem> entry : currentDependencies.entrySet() )
{
String dependencyKey = entry.getKey();
DigestItem currentDependency = entry.getValue();
// null safe - sets compared for differences above
final DigestItem baselineDependency = baselineDependencies.get( dependencyKey );
if ( !StringUtils.equals( currentDependency.getHash(), baselineDependency.getHash() ) )
{
addNewMismatch( dependencyKey, currentDependency.getHash(), baselineDependency.getHash(),
"Downstream project or snapshot changed",
"Find downstream project and investigate difference in the downstream project. "
+ "Enable fail fast mode and single threaded execution to "
+ "simplify debug." );
}
}
}
private void compareExecutions( List<CompletedExecution> current, List<CompletedExecution> baseline )
{
Map<String, CompletedExecution> baselineExecutionsByKey = new HashMap<>();
for ( CompletedExecution completedExecutionType : baseline )
{
baselineExecutionsByKey.put( completedExecutionType.getExecutionKey(), completedExecutionType );
}
Map<String, CompletedExecution> currentExecutionsByKey = new HashMap<>();
for ( CompletedExecution e1 : current )
{
currentExecutionsByKey.put( e1.getExecutionKey(), e1 );
}
// such situation normally means different poms and mismatch in effective poms,
// but in any case it is helpful to report
for ( CompletedExecution baselineExecution : baseline )
{
if ( !currentExecutionsByKey.containsKey( baselineExecution.getExecutionKey() ) )
{
addNewMismatch(
baselineExecution.getExecutionKey(),
"Baseline build contains excessive plugin " + baselineExecution.getExecutionKey(),
"Different set of plugins produces different build results. "
+ "Exclude non-critical plugins or make sure plugin sets match" );
}
}
for ( CompletedExecution currentExecution : current )
{
if ( !baselineExecutionsByKey.containsKey( currentExecution.getExecutionKey() ) )
{
addNewMismatch(
currentExecution.getExecutionKey(),
"Cached build doesn't contain plugin " + currentExecution.getExecutionKey(),
"Different set of plugins produces different build results. "
+ "Filter out non-critical plugins or make sure remote cache always "
+ "run full build with all plugins" );
continue;
}
final CompletedExecution baselineExecution = baselineExecutionsByKey
.get( currentExecution.getExecutionKey() );
comparePlugins( currentExecution, baselineExecution );
}
}
private void comparePlugins( CompletedExecution current, CompletedExecution baseline )
{
// TODO add support for skip values
final List<PropertyValue> trackedProperties = new ArrayList<>();
for ( PropertyValue propertyValueType : current.getProperties() )
{
if ( propertyValueType.isTracked() )
{
trackedProperties.add( propertyValueType );
}
}
if ( trackedProperties.isEmpty() )
{
return;
}
final Map<String, PropertyValue> baselinePropertiesByName = new HashMap<>();
for ( PropertyValue propertyValueType : baseline.getProperties() )
{
baselinePropertiesByName.put( propertyValueType.getName(), propertyValueType );
}
for ( PropertyValue p : trackedProperties )
{
final PropertyValue baselineValue = baselinePropertiesByName.get( p.getName() );
if ( baselineValue == null || !StringUtils.equals( baselineValue.getValue(), p.getValue() ) )
{
addNewMismatch(
p.getName(),
p.getValue(),
baselineValue == null ? null : baselineValue.getValue(),
"Plugin: " + current.getExecutionKey()
+ " has mismatch in tracked property and cannot be reused",
"Align properties between remote and local build or remove property from tracked "
+ "list if mismatch could be tolerated. In some cases it is possible to add "
+ "skip value to ignore lax mismatch" );
}
}
}
private void addNewMismatch( String item, String current, String baseline, String reason,
String resolution )
{
final Mismatch mismatch = new Mismatch();
mismatch.setItem( item );
mismatch.setCurrent( current );
mismatch.setBaseline( baseline );
mismatch.setReason( reason );
mismatch.setResolution( resolution );
report.add( mismatch );
}
private void addNewMismatch( String property, String reason, String resolution )
{
final Mismatch mismatchType = new Mismatch();
mismatchType.setItem( property );
mismatchType.setReason( reason );
mismatchType.setResolution( resolution );
report.add( mismatchType );
}
private static <T> Set<T> diff( Set<T> a, Set<T> b )
{
return a.stream()
.filter( v -> !b.contains( v ) )
.collect( Collectors.toSet() );
}
}