-
Notifications
You must be signed in to change notification settings - Fork 4
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
EOL tracking with release date of current dependency version #267 #268
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -27,6 +27,10 @@ | |
import java.net.SocketTimeoutException; | ||
import java.net.URI; | ||
import java.nio.charset.StandardCharsets; | ||
import java.nio.file.Files; | ||
import java.nio.file.Path; | ||
import java.nio.file.Paths; | ||
import java.nio.file.StandardOpenOption; | ||
import java.time.Instant; | ||
import java.time.LocalDate; | ||
import java.time.ZoneId; | ||
|
@@ -53,6 +57,7 @@ | |
import org.apache.http.util.EntityUtils; | ||
import org.apache.maven.artifact.Artifact; | ||
import org.apache.maven.artifact.versioning.ArtifactVersion; | ||
import org.apache.maven.artifact.versioning.OverConstrainedVersionException; | ||
import org.apache.maven.execution.MavenSession; | ||
import org.apache.maven.model.Dependency; | ||
import org.apache.maven.plugin.AbstractMojo; | ||
|
@@ -150,6 +155,19 @@ public class LibYearMojo extends AbstractMojo { | |
@Parameter(property = "maxLibYears", defaultValue = "0.0") | ||
private float maxLibYears; | ||
|
||
/** | ||
* Path to the report file, if empty no report file will be generated. | ||
*/ | ||
@Parameter(property = "reportFile") | ||
private String reportFile; | ||
|
||
/** | ||
* Whether the dependency should be included in the report. If it is set to "0", all dependencies will be included, | ||
* otherwise only dependencies older than the specified number of years will be included. | ||
*/ | ||
@Parameter(property = "minLibYearsForReport", defaultValue = "0.0") | ||
private float minLibYearsForReport; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would it be possible to add a test for this setting? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The test "reportFileGenerated" is also the test for minLibYearsForReport. It is checked that not all outdated dependencies end up in the report. Should I write another separate test? What exactly should the test case be? |
||
|
||
/** | ||
* Only take these artifacts into consideration. | ||
* | ||
|
@@ -431,6 +449,8 @@ public void execute() throws MojoExecutionException { | |
"Dependencies", | ||
getLog()); | ||
|
||
generateReport(dependencies); | ||
|
||
// Log anything that's left | ||
thisProjectLibYearsOutdated += processDependencyUpdates( | ||
getHelper().lookupDependenciesUpdates(dependencies.stream(), false, false), "Dependencies"); | ||
|
@@ -491,6 +511,70 @@ public void execute() throws MojoExecutionException { | |
} | ||
} | ||
|
||
private void generateReport(Set<Dependency> dependencies) { | ||
if (StringUtils.isNotBlank(reportFile)) { | ||
|
||
StringBuilder logsToReport = new StringBuilder(); | ||
|
||
if (!Paths.get(reportFile).toFile().exists()) { | ||
logsToReport | ||
.append("All dependencies older than ") | ||
.append(minLibYearsForReport) | ||
.append(" libyears:") | ||
.append(System.lineSeparator()) | ||
.append(System.lineSeparator()); | ||
} | ||
|
||
dependencies.stream().forEach(dependency -> { | ||
try { | ||
Artifact artifact = getHelper().createDependencyArtifact(dependency); | ||
|
||
Optional<LocalDate> currentReleaseDate = getReleaseDate( | ||
artifact.getGroupId(), | ||
artifact.getArtifactId(), | ||
artifact.getSelectedVersion().toString()); | ||
|
||
String depName = dependency.getGroupId() + ":" + dependency.getArtifactId() + ": " | ||
+ dependency.getVersion() + " "; | ||
if (!currentReleaseDate.isEmpty()) { | ||
long libWeeksOutdated = ChronoUnit.WEEKS.between(currentReleaseDate.get(), LocalDate.now()); | ||
float libYearsOutdated = libWeeksOutdated / 52f; | ||
|
||
if (libYearsOutdated > 0 | ||
&& (minLibYearsForReport <= 0 || libYearsOutdated > minLibYearsForReport)) { | ||
String libYearsStr = String.format(" %.2f libyears", libYearsOutdated); | ||
addToReport(depName, libYearsStr, logsToReport); | ||
} | ||
} else { | ||
addToReport(depName, "unknown", logsToReport); | ||
} | ||
} catch (MojoExecutionException | OverConstrainedVersionException e) { | ||
getLog().error("Exception by writing report", e); | ||
} | ||
}); | ||
Path path = Paths.get(reportFile); | ||
|
||
try { | ||
Files.write( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If I run this on a multi-module project, with a path that would resolve the same for multiple projects (e.g. Are there any concurrency issues if this was done in parallel? Example parameters: Would it be simpler to replace the text file writing here with log output, and to solve the file problem globally (separately)? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In my opinion it shouldn't be a problem. The report is written in append mode. |
||
path, logsToReport.toString().getBytes(), StandardOpenOption.CREATE, StandardOpenOption.APPEND); | ||
} catch (IOException e) { | ||
getLog().error("Failed to write report file: " + reportFile, e); | ||
} | ||
} | ||
} | ||
|
||
private static void addToReport(String depName, String libYearsStr, StringBuilder logsToReport) { | ||
if ((depName.length() + libYearsStr.length()) > INFO_PAD_SIZE) { | ||
logsToReport | ||
.append(depName) | ||
.append(System.lineSeparator()) | ||
.append(StringUtils.rightPad(" ", INFO_PAD_SIZE - libYearsStr.length(), ".")); | ||
} else { | ||
logsToReport.append(StringUtils.rightPad(depName, INFO_PAD_SIZE - libYearsStr.length(), ".")); | ||
} | ||
logsToReport.append(libYearsStr).append(System.lineSeparator()); | ||
} | ||
|
||
private VersionsHelper getHelper() throws MojoExecutionException { | ||
if (helper == null) { | ||
helper = new DefaultVersionsHelper.Builder() | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -36,8 +36,11 @@ | |
import static org.junit.jupiter.api.Assertions.assertTrue; | ||
|
||
import com.github.tomakehurst.wiremock.junit5.WireMockTest; | ||
import java.nio.file.Files; | ||
import java.nio.file.Path; | ||
import java.time.LocalDateTime; | ||
import java.time.ZoneOffset; | ||
import java.util.Arrays; | ||
import java.util.HashMap; | ||
import java.util.List; | ||
import java.util.Optional; | ||
|
@@ -55,6 +58,7 @@ | |
import org.junit.jupiter.api.AfterEach; | ||
import org.junit.jupiter.api.Test; | ||
import org.junit.jupiter.api.extension.ExtendWith; | ||
import org.junit.jupiter.api.io.TempDir; | ||
import org.mockito.Mockito; | ||
import org.mockito.junit.jupiter.MockitoExtension; | ||
import org.mockito.junit.jupiter.MockitoSettings; | ||
|
@@ -1212,6 +1216,74 @@ public void projectExceedsMaxLibYearsAndShouldFailTheBuild() throws Exception { | |
l.contains("This module exceeds the maximum" + " dependency age of 0.1 libyears"))); | ||
} | ||
|
||
@Test | ||
public void reportFileGenerated(@TempDir Path tempDir) throws Exception { | ||
Path reportFile = tempDir.resolve("libyear_testreport.txt"); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Currently the plugin doesn't write any of the libyear findings to a file for outdated dependencies. What's the motivation for adding it here? To me it feels like something that should be everywhere or nowhere, I.e we should implement it everywhere else that we're logging currently. Both for things that are outdated and things that are possibly abandoned? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We would use the plugin for the asynchronous builds via Jenkins. Analyzing the log files is tedious; We need a file in which we can look up the results at any time and which we could also send to other project participants. And yes, I also think this can be useful for all expenses. Control is possible via a configuration parameter: either all results are written to a file or to the log output. |
||
|
||
LibYearMojo mojo = | ||
new LibYearMojo(mockRepositorySystem(), mockAetherRepositorySystem(new HashMap<>() { | ||
{ | ||
put("default-dependency", new String[] {"1.0.0", "2.0.0"}); | ||
put("default2-dependency", new String[] {"3.0.0", "4.0.0"}); | ||
} | ||
})) { | ||
{ | ||
Dependency dep1 = DependencyBuilder.newBuilder() | ||
.withGroupId("default-group") | ||
.withArtifactId("default-dependency") | ||
.withVersion("1.0.0") | ||
.build(); | ||
|
||
Dependency dep2 = DependencyBuilder.newBuilder() | ||
.withGroupId("default-group") | ||
.withArtifactId("default2-dependency") | ||
.withVersion("3.0.0") | ||
.build(); | ||
|
||
MavenProject project = new MavenProjectBuilder() | ||
.withDependencies(Arrays.asList(dep1, dep2)) | ||
.build(); | ||
|
||
setProject(project); | ||
allowProcessingAllDependencies(this); | ||
|
||
setVariableValueToObject( | ||
this, "reportFile", reportFile.toAbsolutePath().toString()); | ||
setVariableValueToObject(this, "minLibYearsForReport", 2); | ||
|
||
setPluginContext(new HashMap<>()); | ||
|
||
setSession(mockMavenSession(project)); | ||
setSearchUri("http://localhost:8080"); | ||
|
||
setLog(new InMemoryTestLogger()); | ||
} | ||
}; | ||
|
||
LocalDateTime now = LocalDateTime.now(); | ||
|
||
stubResponseFor("default-group", "default-dependency", "1.0.0", now.minusYears(1)); | ||
stubResponseFor("default-group", "default-dependency", "2.0.0", now); | ||
stubResponseFor("default-group", "default2-dependency", "3.0.0", now.minusYears(3)); | ||
stubResponseFor("default-group", "default2-dependency", "4.0.0", now); | ||
|
||
mojo.execute(); | ||
|
||
assertTrue(((InMemoryTestLogger) mojo.getLog()) | ||
.infoLogs.stream() | ||
.anyMatch( | ||
(l) -> l.contains("default-group:default-dependency") && l.contains("1.00 libyears"))); | ||
assertTrue(((InMemoryTestLogger) mojo.getLog()) | ||
.infoLogs.stream() | ||
.anyMatch( | ||
(l) -> l.contains("default-group:default2-dependency") && l.contains("3.00 libyears"))); | ||
assertTrue(((InMemoryTestLogger) mojo.getLog()).errorLogs.isEmpty()); | ||
|
||
String content = Files.readString(reportFile); | ||
assertFalse(content.contains("default-group:default-dependency") && content.contains("1.00 libyears")); | ||
assertTrue(content.contains("default-group:default2-dependency") && content.contains("3.00 libyears")); | ||
} | ||
|
||
private void allowProcessingAllDependencies(LibYearMojo mojo) throws IllegalAccessException { | ||
setVariableValueToObject(mojo, "ignoredVersions", emptySet()); | ||
setVariableValueToObject(mojo, "processDependencies", true); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this
maxLibYears
setting interact with the years-without-new-update setting?E.g, it might make sense to cause failures if either any dependency is more than X years behind the latest or if it hasn't been updated in X years.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We don't want the build to fail, we just need the information about how old the dependencies are and actually in one file.