Skip to content
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

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,10 +66,12 @@ Configuration options can be specified on the command line such as
`-DdependencyExcludes="io.github.mfoo:*"` or as part of the plugin
configuration in `pom.xml`.

| Option | Description | Format |
|----------------------|-------------------------------------------------------------|---------------------------------|
| `dependencyExcludes` | Ignore certain dependencies | `io.github.mfoo:*,org.apache:*` |
| `maxLibYears` | Cause the build to fail if dependencies are older than this | `4` |
Copy link
Owner

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.

Copy link
Author

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.

| Option | Description | Format |
|------------------------|--------------------------------------------------------------|---------------------------------|
| `dependencyExcludes` | Ignore certain dependencies | `io.github.mfoo:*,org.apache:*` |
| `maxLibYears` | Cause the build to fail if dependencies are older than this | `4` |
| `reportFile` | The path to report file | `target/libyear-report.txt` |
| `minLibYearsForReport` | Minimum age of the dependencies to be included in the report | `2` |


A full list of options can be seen as part of the [plugin documentation
Expand Down
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

<groupId>io.github.mfoo</groupId>
<artifactId>libyear-maven-plugin</artifactId>
<version>1.1.0</version>
<version>1.1.1-SNAPSHOT</version>
<name>libyear-maven-plugin Maven Mojo</name>
<description>This plugin helps you see how outdated your Maven project
dependencies are via the libyear dependency freshness measure.</description>
Expand Down
84 changes: 84 additions & 0 deletions src/main/java/io/github/mfoo/libyear/LibYearMojo.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be possible to add a test for this setting?

Copy link
Author

Choose a reason for hiding this comment

The 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.
*
Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -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(
Copy link
Owner

Choose a reason for hiding this comment

The 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. /tmp/foo.txt or ../foo.txt, will the last one overwrite all of the previous ones?

Are there any concurrency issues if this was done in parallel?

Example parameters: -DreportFile=/tmp/foo.txt -T2C.

Would it be simpler to replace the text file writing here with log output, and to solve the file problem globally (separately)?

Copy link
Author

Choose a reason for hiding this comment

The 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()
Expand Down
72 changes: 72 additions & 0 deletions src/test/java/io/github/mfoo/libyear/LibYearMojoTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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");
Copy link
Owner

@mfoo mfoo Sep 15, 2024

Choose a reason for hiding this comment

The 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?

Copy link
Author

Choose a reason for hiding this comment

The 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);
Expand Down