diff --git a/bots/notify/src/main/java/org/openjdk/skara/bots/notify/issue/Backports.java b/bots/notify/src/main/java/org/openjdk/skara/bots/notify/issue/Backports.java new file mode 100644 index 000000000..2b7d65c48 --- /dev/null +++ b/bots/notify/src/main/java/org/openjdk/skara/bots/notify/issue/Backports.java @@ -0,0 +1,333 @@ +/* + * Copyright (c) 2020, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package org.openjdk.skara.bots.notify.issue; + +import org.openjdk.skara.issuetracker.Issue; +import org.openjdk.skara.json.JSONValue; + +import java.util.*; +import java.util.logging.Logger; +import java.util.stream.*; + +public class Backports { + private final static Set primaryTypes = Set.of("Bug", "New Feature", "Enhancement", "Task", "Sub-task"); + private final static Logger log = Logger.getLogger("org.openjdk.skara.bots.notify"); + + private static boolean isPrimaryIssue(Issue issue) { + var properties = issue.properties(); + if (!properties.containsKey("issuetype")) { + throw new RuntimeException("Unknown type for issue " + issue.id()); + } + var type = properties.get("issuetype"); + return primaryTypes.contains(type.asString()); + } + + private static boolean isNonScratchVersion(String version) { + return !version.startsWith("tbd") && !version.toLowerCase().equals("unknown"); + } + + private static Set fixVersions(Issue issue) { + if (!issue.properties().containsKey("fixVersions")) { + return Set.of(); + } + return issue.properties().get("fixVersions").stream() + .map(JSONValue::asString) + .collect(Collectors.toSet()); + } + + /** + * Returns the single non-scratch fixVersion entry for an issue. If the issue has either none ore more than one, + * no version is returned. + * @param issue + * @return + */ + static Optional mainFixVersion(Issue issue) { + var versionString = fixVersions(issue).stream() + .filter(Backports::isNonScratchVersion) + .collect(Collectors.toList()); + if (versionString.isEmpty()) { + return Optional.empty(); + } + if (versionString.size() > 1) { + log.warning("Issue " + issue.id() + " has multiple valid fixVersions - ignoring"); + return Optional.empty(); + } + if (issue.properties().containsKey("customfield_10006")) { + return Optional.of(JdkVersion.parse(versionString.get(0), issue.properties().get("customfield_10006").asString())); + } else { + return Optional.of(JdkVersion.parse(versionString.get(0))); + } + } + + /** + * Return the main issue for this backport. + * Harmless when called with the main issue + */ + static Optional findMainIssue(Issue issue) { + if (isPrimaryIssue(issue)) { + return Optional.of(issue); + } + + for (var link : issue.links()) { + if (link.issue().isPresent() && link.relationship().isPresent()) { + if (link.relationship().get().equals("backported by") || link.relationship().get().equals("backport of")) { + var linkedIssue = link.issue().get(); + if (isPrimaryIssue(linkedIssue)) { + return Optional.of(linkedIssue); + } + } + } + } + + log.warning("Failed to find main issue for " + issue.id()); + return Optional.empty(); + } + + /** + * Return true if the issue's fixVersionList matches fixVersion. + * + * fixVersionsList must contain one entry that is an exact match for fixVersions; any + * other entries must be scratch values. + */ + private static boolean matchVersion(Issue issue, JdkVersion fixVersion) { + var mainVersion = mainFixVersion(issue); + if (mainVersion.isEmpty()) { + return false; + } + return mainVersion.get().equals(fixVersion); + } + + /** + * Return true if the issue's fixVersionList is a match for fixVersion, using "-pool" or "-open". + * + * If fixVersion has a major release of , it matches the fixVersionList has an + * -pool or -open entry and all other entries are scratch values. + */ + private static boolean matchPoolVersion(Issue issue, JdkVersion fixVersion) { + var majorVersion = fixVersion.feature(); + var poolVersion = JdkVersion.parse(majorVersion + "-pool"); + var openVersion = JdkVersion.parse(majorVersion + "-open"); + + var mainVersion = mainFixVersion(issue); + if (mainVersion.isEmpty()) { + return false; + } + return mainVersion.get().equals(poolVersion) || mainVersion.get().equals(openVersion); + } + + /** + * Return true if fixVersionList is empty or contains only scratch values. + */ + private static boolean matchScratchVersion(Issue issue) { + var nonScratch = fixVersions(issue).stream() + .filter(Backports::isNonScratchVersion) + .collect(Collectors.toList()); + return nonScratch.size() == 0; + } + + /** + * Return issue or one of its backports that applies to fixVersion. + * + * If the main issue has the correct fixVersion, use it. + * If an existing Backport has the correct fixVersion, use it. + * If the main issue has a matching -pool/open fixVersion, use it. + * If an existing Backport has a matching -pool/open fixVersion, use it. + * If the main issue has a "scratch" fixVersion, use it. + * If an existing Backport has a "scratch" fixVersion, use it. + * + * Otherwise, create a new Backport. + * + * A "scratch" fixVersion is empty, "tbd.*", or "unknown". + */ + static Optional findIssue(Issue primary, JdkVersion fixVersion) { + log.fine("Searching for properly versioned issue for primary issue " + primary.id()); + var candidates = Stream.concat(Stream.of(primary), findBackports(primary).stream()).collect(Collectors.toList()); + candidates.forEach(c -> log.fine("Candidate: " + c.id() + " with versions: " + String.join(",", fixVersions(c)))); + var matchingVersionIssue = candidates.stream() + .filter(i -> matchVersion(i, fixVersion)) + .findFirst(); + if (matchingVersionIssue.isPresent()) { + log.fine("Issue " + matchingVersionIssue.get().id() + " has a correct fixVersion"); + return matchingVersionIssue; + } + + var matchingPoolVersionIssue = candidates.stream() + .filter(i -> matchPoolVersion(i, fixVersion)) + .findFirst(); + if (matchingPoolVersionIssue.isPresent()) { + log.fine("Issue " + matchingPoolVersionIssue.get().id() + " has a matching pool version"); + return matchingPoolVersionIssue; + } + + var matchingScratchVersionIssue = candidates.stream() + .filter(Backports::matchScratchVersion) + .findFirst(); + if (matchingScratchVersionIssue.isPresent()) { + log.fine("Issue " + matchingScratchVersionIssue.get().id() + " has a scratch fixVersion"); + return matchingScratchVersionIssue; + } + + log.fine("No suitable existing issue for " + primary.id() + " with version " + fixVersion + " found"); + return Optional.empty(); + } + + static List findBackports(Issue primary) { + var links = primary.links(); + return links.stream() + .filter(l -> l.issue().isPresent()) + .map(l -> l.issue().get()) + .filter(i -> i.properties().containsKey("issuetype")) + .filter(i -> i.properties().get("issuetype").asString().equals("Backport")) + .collect(Collectors.toList()); + } + + /** + * Classifies a given version as belonging to one or more release streams. + * + * For the JDK 7 and 8 release trains, this is determined by the feature version (8 in 8u240 for example) + * combined with the build number. Build numbers between 31 and 60 are considered to be part of the bpr stream. + * + * For JDK 9 and subsequent releases, release streams branch into Oracle and OpenJDK updates after the second + * update version is released. Oracle updates that has a patch version are considered to be part of the bpr stream. + * @param jdkVersion + * @return + */ + private static List releaseStreams(JdkVersion jdkVersion) { + var ret = new ArrayList(); + try { + var numericFeature = Integer.parseInt(jdkVersion.feature()); + if (numericFeature >= 9) { + if (jdkVersion.update().isPresent()) { + var numericUpdate = Integer.parseInt(jdkVersion.update().get()); + if (numericUpdate == 1 || numericUpdate == 2) { + ret.add(jdkVersion.feature() + "+updates-oracle"); + ret.add(jdkVersion.feature() + "+updates-openjdk"); + } else if (numericUpdate > 2) { + if (jdkVersion.opt().isPresent() && jdkVersion.opt().get().equals("oracle")) { + if (jdkVersion.patch().isPresent()) { + ret.add(jdkVersion.feature()+ "+bpr"); + } else { + ret.add(jdkVersion.feature() + "+updates-oracle"); + } + } else { + ret.add(jdkVersion.feature() + "+updates-openjdk"); + } + } + } else { + ret.add("features"); + ret.add(jdkVersion.feature() + "+updates-oracle"); + ret.add(jdkVersion.feature() + "+updates-openjdk"); + } + } else if (numericFeature == 7 || numericFeature == 8) { + var resolvedInBuild = jdkVersion.resolvedInBuild(); + if (resolvedInBuild.isPresent()) { + if (!resolvedInBuild.get().equals("team")) { // Special case - team build resolved are ignored + int resolvedInBuildNumber = jdkVersion.resolvedInBuildNumber(); + if (resolvedInBuildNumber < 31) { + ret.add(jdkVersion.feature()); + } else if (resolvedInBuildNumber < 60) { + ret.add(jdkVersion.feature() + "+bpr"); + } + } + } else { + ret.add(jdkVersion.feature()); + } + } else { + log.warning("Ignoring issue with unknown version: " + jdkVersion); + } + } catch (NumberFormatException ignored) { + log.info("Cannot determine release streams for version: " + jdkVersion); + } + return ret; + } + + // Split the issue list depending on the release stream + private static List> groupByReleaseStream(List issues) { + var streamIssues = new HashMap>(); + for (var issue : issues) { + var fixVersion = mainFixVersion(issue); + if (fixVersion.isEmpty()) { + log.info("Issue " + issue.id() + " does not a fixVersion set - ignoring"); + continue; + } + var streams = releaseStreams(fixVersion.get()); + for (var stream : streams) { + if (!streamIssues.containsKey(stream)) { + streamIssues.put(stream, new ArrayList()); + } + streamIssues.get(stream).add(issue); + } + } + + var ret = new ArrayList>(); + for (var issuesInStream : streamIssues.values()) { + if (issuesInStream.size() < 2) { + // It's not a release stream unless it has more than one entry + continue; + } + issuesInStream.sort(Comparator.comparing(i -> mainFixVersion(i).orElseThrow())); + ret.add(issuesInStream); + } + return ret; + } + + /** + * Applies a label to later releases in a release stream. + * + * The label should not be applied to the first release in a specific stream where a fix ships. I.e. + * it should only be applied to issues in any given stream if the fix version of the issue *is not* the first + * release where the fix has shipped *within that stream*. + * + * @param issue + * @param label + */ + static void labelReleaseStreamDuplicates(Issue issue, String label) { + var mainIssue = Backports.findMainIssue(issue); + if (mainIssue.isEmpty()) { + return; + } + var related = Backports.findBackports(mainIssue.get()); + + var allIssues = new ArrayList(); + allIssues.add(mainIssue.get()); + allIssues.addAll(related); + + for (var streamIssues : groupByReleaseStream(allIssues)) { + // First entry should not have the label + var first = streamIssues.get(0); + if (first.labels().contains(label)) { + first.removeLabel(label); + } + + // But all the following ones should + if (streamIssues.size() > 1) { + var rest = streamIssues.subList(1, streamIssues.size()); + for (var i : rest) { + if (!i.labels().contains(label)) { + i.addLabel(label); + } + } + } + } + } +} diff --git a/bots/notify/src/main/java/org/openjdk/skara/bots/notify/issue/IssueNotifier.java b/bots/notify/src/main/java/org/openjdk/skara/bots/notify/issue/IssueNotifier.java index ae8273341..12d1f41ab 100644 --- a/bots/notify/src/main/java/org/openjdk/skara/bots/notify/issue/IssueNotifier.java +++ b/bots/notify/src/main/java/org/openjdk/skara/bots/notify/issue/IssueNotifier.java @@ -26,33 +26,51 @@ import org.openjdk.skara.email.EmailAddress; import org.openjdk.skara.forge.*; import org.openjdk.skara.issuetracker.*; +import org.openjdk.skara.jcheck.JCheckConfiguration; +import org.openjdk.skara.json.JSON; import org.openjdk.skara.vcs.*; import org.openjdk.skara.vcs.openjdk.CommitMessageParsers; +import java.io.IOException; import java.net.URI; +import java.nio.file.Path; import java.util.*; import java.util.logging.Logger; -class IssueNotifier implements Notifier, PullRequestListener { +class IssueNotifier implements Notifier, PullRequestListener, RepositoryListener { private final IssueProject issueProject; private final boolean reviewLink; private final URI reviewIcon; private final boolean commitLink; private final URI commitIcon; + private final boolean setFixVersion; + private final Map fixVersions; + private final JbsBackport jbsBackport; + private final Logger log = Logger.getLogger("org.openjdk.skara.bots.notify"); - IssueNotifier(IssueProject issueProject, boolean reviewLink, URI reviewIcon, boolean commitLink, URI commitIcon) { + IssueNotifier(IssueProject issueProject, boolean reviewLink, URI reviewIcon, boolean commitLink, URI commitIcon, + boolean setFixVersion, Map fixVersions, JbsBackport jbsBackport) { this.issueProject = issueProject; this.reviewLink = reviewLink; this.reviewIcon = reviewIcon; this.commitLink = commitLink; this.commitIcon = commitIcon; + this.setFixVersion = setFixVersion; + this.fixVersions = fixVersions; + this.jbsBackport = jbsBackport; } static IssueNotifierBuilder newBuilder() { return new IssueNotifierBuilder(); } + private Optional findIssueUsername(Commit commit) { + return findIssueUsername(new CommitMetadata(commit.hash(), commit.parents(), commit.author(), + commit.authored(), commit.committer(), commit.committed(), + commit.message())); + } + private Optional findIssueUsername(CommitMetadata commit) { var authorEmail = EmailAddress.from(commit.author().email()); if (authorEmail.domain().equals("openjdk.org")) { @@ -70,6 +88,7 @@ private Optional findIssueUsername(CommitMetadata commit) { @Override public void attachTo(Emitter e) { e.registerPullRequestListener(this); + e.registerRepositoryListener(this); } @Override @@ -98,19 +117,6 @@ public void onIntegratedPullRequest(PullRequest pr, Hash hash) { } issue.addLink(linkBuilder.build()); } - - if (issue.state() == Issue.State.OPEN) { - issue.setState(Issue.State.RESOLVED); - if (issue.assignees().isEmpty()) { - var username = findIssueUsername(commit); - if (username.isPresent()) { - var assignee = issueProject.issueTracker().user(username.get()); - if (assignee.isPresent()) { - issue.setAssignees(List.of(assignee.get())); - } - } - } - } } } @@ -145,4 +151,94 @@ public void onRemovedIssue(PullRequest pr, org.openjdk.skara.vcs.openjdk.Issue i var link = Link.create(pr.webUrl(), "").build(); realIssue.get().removeLink(link); } + + @Override + public void onNewCommits(HostedRepository repository, Repository localRepository, List commits, Branch branch) { + for (var commit : commits) { + var commitNotification = CommitFormatters.toTextBrief(repository, commit); + var commitMessage = CommitMessageParsers.v1.parse(commit); + var username = findIssueUsername(commit); + + for (var commitIssue : commitMessage.issues()) { + var optionalIssue = issueProject.issue(commitIssue.shortId()); + if (optionalIssue.isEmpty()) { + log.severe("Cannot update issue " + commitIssue.id() + " with commit " + commit.hash().abbreviate() + + " - issue not found in issue project"); + continue; + } + + var issue = optionalIssue.get(); + var mainIssue = Backports.findMainIssue(issue); + if (mainIssue.isEmpty()) { + log.severe("Issue " + issue.id() + " is not the main issue - bot no corresponding main issue found"); + continue; + } else { + if (!mainIssue.get().id().equals(issue.id())) { + log.warning("Issue " + issue.id() + " is not the main issue - using " + mainIssue.get().id() + " instead");; + issue = mainIssue.get(); + } + } + + String requestedVersion = null; + // The actual issue to be updated can change depending on the fix version + if (setFixVersion) { + requestedVersion = fixVersions != null ? fixVersions.getOrDefault(branch.name(), null) : null; + if (requestedVersion == null) { + try { + var conf = localRepository.lines(Path.of(".jcheck/conf"), commit.hash()); + if (conf.isPresent()) { + var parsed = JCheckConfiguration.parse(conf.get()); + var version = parsed.general().version(); + requestedVersion = version.orElse(null); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + if (requestedVersion != null) { + var fixVersion = JdkVersion.parse(requestedVersion); + var existing = Backports.findIssue(issue, fixVersion); + if (existing.isEmpty()) { + issue = jbsBackport.createBackport(issue, requestedVersion, username.orElse(null)); + } else { + issue = existing.get(); + } + } + } + + var existingComments = issue.comments(); + var hashUrl = repository.webUrl(commit.hash()).toString(); + var alreadyPostedComment = existingComments.stream() + .filter(comment -> comment.author().equals(issueProject.issueTracker().currentUser())) + .anyMatch(comment -> comment.body().contains(hashUrl)); + if (!alreadyPostedComment) { + issue.addComment(commitNotification); + } + if (issue.state() == Issue.State.OPEN) { + issue.setState(Issue.State.RESOLVED); + if (issue.assignees().isEmpty()) { + if (username.isPresent()) { + var assignee = issueProject.issueTracker().user(username.get()); + if (assignee.isPresent()) { + issue.setAssignees(List.of(assignee.get())); + } + } + } + } + + if (setFixVersion) { + if (requestedVersion != null) { + issue.setProperty("fixVersions", JSON.of(requestedVersion)); + Backports.labelReleaseStreamDuplicates(issue, "hgupdater-sync"); + } + } + } + } + } + + @Override + public String name() { + return "issue"; + } } diff --git a/bots/notify/src/main/java/org/openjdk/skara/bots/notify/issue/IssueNotifierBuilder.java b/bots/notify/src/main/java/org/openjdk/skara/bots/notify/issue/IssueNotifierBuilder.java index 348f77b0d..78c733fae 100644 --- a/bots/notify/src/main/java/org/openjdk/skara/bots/notify/issue/IssueNotifierBuilder.java +++ b/bots/notify/src/main/java/org/openjdk/skara/bots/notify/issue/IssueNotifierBuilder.java @@ -33,6 +33,10 @@ class IssueNotifierBuilder { private URI reviewIcon = null; private boolean commitLink = true; private URI commitIcon = null; + private boolean setFixVersion = false; + private Map fixVersions = null; + private JbsVault vault = null; + private String securityLevel = null; IssueNotifierBuilder issueProject(IssueProject issueProject) { this.issueProject = issueProject; @@ -59,7 +63,29 @@ IssueNotifierBuilder commitIcon(URI commitIcon) { return this; } + public IssueNotifierBuilder setFixVersion(boolean setFixVersion) { + this.setFixVersion = setFixVersion; + return this; + } + + public IssueNotifierBuilder fixVersions(Map fixVersions) { + this.fixVersions = fixVersions; + return this; + } + + public IssueNotifierBuilder vault(JbsVault vault) { + this.vault = vault; + return this; + } + + public IssueNotifierBuilder securityLevel(String securityLevel) { + this.securityLevel = securityLevel; + return this; + } + IssueNotifier build() { - return new IssueNotifier(issueProject, reviewLink, reviewIcon, commitLink, commitIcon); + var jbsBackport = new JbsBackport(issueProject.webUrl(), vault, securityLevel); + return new IssueNotifier(issueProject, reviewLink, reviewIcon, commitLink, commitIcon, + setFixVersion, fixVersions, jbsBackport); } } diff --git a/bots/notify/src/main/java/org/openjdk/skara/bots/notify/issue/IssueNotifierFactory.java b/bots/notify/src/main/java/org/openjdk/skara/bots/notify/issue/IssueNotifierFactory.java index 5344a3ceb..b6ee8ec93 100644 --- a/bots/notify/src/main/java/org/openjdk/skara/bots/notify/issue/IssueNotifierFactory.java +++ b/bots/notify/src/main/java/org/openjdk/skara/bots/notify/issue/IssueNotifierFactory.java @@ -2,9 +2,12 @@ import org.openjdk.skara.bot.BotConfiguration; import org.openjdk.skara.bots.notify.*; +import org.openjdk.skara.host.Credential; import org.openjdk.skara.json.JSONObject; +import org.openjdk.skara.network.URIBuilder; import java.net.URI; +import java.util.stream.Collectors; public class IssueNotifierFactory implements NotifierFactory { @Override @@ -36,6 +39,30 @@ public Notifier create(BotConfiguration botConfiguration, JSONObject notifierCon builder.commitLink(notifierConfiguration.get("commitlink").asBoolean()); } + if (notifierConfiguration.contains("fixversions")) { + builder.setFixVersion(true); + builder.fixVersions(notifierConfiguration.get("fixversions").fields().stream() + .collect(Collectors.toMap(JSONObject.Field::name, + f -> f.value().asString()))); + } + + if (notifierConfiguration.contains("vault")) { + var vaultConfiguration = notifierConfiguration.get("vault").asObject(); + var credential = new Credential(vaultConfiguration.get("username").asString(), vaultConfiguration.get("password").asString()); + + if (credential.username().startsWith("https://")) { + var vaultUrl = URIBuilder.base(credential.username()).build(); + var jbsVault = new JbsVault(vaultUrl, credential.password()); + builder.vault(jbsVault); + } else { + throw new RuntimeException("basic authentication not implemented yet"); + } + } + + if (notifierConfiguration.contains("security")) { + builder.securityLevel(notifierConfiguration.get("security").asString()); + } + return builder.build(); } } diff --git a/bots/notify/src/main/java/org/openjdk/skara/bots/notify/issue/JbsBackport.java b/bots/notify/src/main/java/org/openjdk/skara/bots/notify/issue/JbsBackport.java new file mode 100644 index 000000000..788578253 --- /dev/null +++ b/bots/notify/src/main/java/org/openjdk/skara/bots/notify/issue/JbsBackport.java @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2020, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package org.openjdk.skara.bots.notify.issue; + +import org.openjdk.skara.issuetracker.*; +import org.openjdk.skara.json.JSON; +import org.openjdk.skara.network.*; + +import java.net.URI; +import java.util.*; +import java.util.stream.Collectors; + +public class JbsBackport { + private final String securityLevel; + private final RestRequest backportRequest; + + private static URI backportRequest(URI uri) { + return URIBuilder.base(uri) + .setPath("/rest/jbs/1.0/backport/") + .build(); + } + + JbsBackport(URI uri, JbsVault vault, String securityLevel) { + this.securityLevel = securityLevel; + if (vault != null) { + backportRequest = new RestRequest(backportRequest(uri), vault.authId(), () -> Arrays.asList("Cookie", vault.getCookie())); + } else { + backportRequest = null; + } + } + + private Issue createBackportIssue(Issue primary) { + var finalProperties = new HashMap<>(primary.properties()); + finalProperties.put("issuetype", JSON.of("Backport")); + + var backport = primary.project().createIssue(primary.title(), primary.body().lines().collect(Collectors.toList()), finalProperties); + + var backportLink = Link.create(backport, "backported by").build(); + primary.addLink(backportLink); + return backport; + } + + public Issue createBackport(Issue primary, String fixVersion, String assignee) { + if (backportRequest == null) { + if (primary.project().webUrl().toString().contains("openjdk.java.net")) { + throw new RuntimeException("Backports on JBS require vault authentication"); + } else { + return createBackportIssue(primary); + } + } + + var request = backportRequest.post() + .body("parentIssueKey", primary.id()) + .body("fixVersion", fixVersion); + if (assignee != null) { + request.body("assignee", assignee); + } + if (securityLevel != null) { + request.body("level", securityLevel); + } + var response = request.execute(); + return primary.project().issue(response.get("key").asString()).orElseThrow(); + } +} diff --git a/bots/notify/src/main/java/org/openjdk/skara/bots/notify/issue/JbsVault.java b/bots/notify/src/main/java/org/openjdk/skara/bots/notify/issue/JbsVault.java new file mode 100644 index 000000000..eda0a1bae --- /dev/null +++ b/bots/notify/src/main/java/org/openjdk/skara/bots/notify/issue/JbsVault.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2020, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package org.openjdk.skara.bots.notify.issue; + +import org.openjdk.skara.network.RestRequest; + +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.security.*; +import java.time.*; +import java.util.*; +import java.util.logging.Logger; + +public class JbsVault { + private final RestRequest request; + private final String authId; + private static final Logger log = Logger.getLogger("org.openjdk.skara.bots.notify"); + + private String cookie; + private Instant expires; + + private String checksum(String body) { + try { + var digest = MessageDigest.getInstance("SHA-256"); + digest.update(body.getBytes(StandardCharsets.UTF_8)); + return Base64.getUrlEncoder().encodeToString(digest.digest()); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException("Cannot find SHA-256"); + } + } + + JbsVault(URI vaultUri, String vaultToken) { + authId = checksum(vaultToken); + request = new RestRequest(vaultUri, authId, () -> Arrays.asList( + "X-Vault-Token", vaultToken + )); + } + + String getCookie() { + if ((cookie == null) || Instant.now().isAfter(expires)) { + var result = request.get("").execute(); + cookie = result.get("data").get("cookie.name").asString() + "=" + result.get("data").get("cookie.value").asString(); + expires = Instant.now().plus(Duration.ofSeconds(result.get("lease_duration").asInt()).dividedBy(2)); + log.info("Renewed Jira token (" + cookie + ") - expires " + expires); + } + return cookie; + } + + String authId() { + return authId; + } +} diff --git a/bots/notify/src/main/java/org/openjdk/skara/bots/notify/issue/JdkVersion.java b/bots/notify/src/main/java/org/openjdk/skara/bots/notify/issue/JdkVersion.java new file mode 100644 index 000000000..12f73d693 --- /dev/null +++ b/bots/notify/src/main/java/org/openjdk/skara/bots/notify/issue/JdkVersion.java @@ -0,0 +1,231 @@ +/* + * Copyright (c) 2020, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package org.openjdk.skara.bots.notify.issue; + +import java.util.*; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +public class JdkVersion implements Comparable { + private final String raw; + private final List components; + private final String opt; + private final String build; + + private final static Pattern jdkVersionPattern = Pattern.compile("(5\\.0|[1-9][0-9]?)(u([0-9]{1,3}))?$"); + private final static Pattern hsxVersionPattern = Pattern.compile("(hs[1-9][0-9]{1,2})(\\\\.([0-9]{1,3}))?$"); + private final static Pattern embVersionPattern = Pattern.compile("(emb-[8-9])(u([0-9]{1,3}))?$"); + private final static Pattern ojVersionPattern = Pattern.compile("(openjdk[1-9][0-9]?)(u([0-9]{1,3}))?$"); + + private final static Pattern legacyPrefixPattern = Pattern.compile("^([^\\d]*)\\d+$"); + + private static List splitComponents(String raw) { + var finalComponents = new ArrayList(); + + // First check for the legacy patterns + for (var legacyPattern : List.of(jdkVersionPattern, hsxVersionPattern, embVersionPattern, ojVersionPattern)) { + var legacyMatcher = legacyPattern.matcher(raw); + if (legacyMatcher.matches()) { + finalComponents.add(legacyMatcher.group(1)); + if (legacyMatcher.group(3) != null) { + finalComponents.add(legacyMatcher.group(3)); + } + break; + } + } + + // If no legacy match, use the JEP322 scheme + if (finalComponents.isEmpty()) { + var optionalStart = raw.lastIndexOf("-"); + String optional = null; + if (optionalStart >= 0) { + optional = raw.substring(optionalStart + 1); + raw = raw.substring(0, optionalStart); + } + + finalComponents.addAll(Arrays.asList(raw.split("\\."))); + if (optional != null) { + finalComponents.add(null); + finalComponents.add(optional); + } + } + + // Never leave a trailing 'u' in the major version + if (finalComponents.get(0).endsWith("u")) { + finalComponents.set(0, finalComponents.get(0).substring(0, finalComponents.get(0).length() - 1)); + } + + return finalComponents; + } + + private JdkVersion(String raw, String build) { + this.raw = raw; + this.build = build; + + var rawComponents = splitComponents(raw); + components = rawComponents.stream() + .takeWhile(Objects::nonNull) + .collect(Collectors.toList()); + opt = rawComponents.stream() + .dropWhile(Objects::nonNull) + .filter(Objects::nonNull) + .findAny().orElse(null); + } + + public static JdkVersion parse(String raw) { + return new JdkVersion(raw, null); + } + + public static JdkVersion parse(String raw, String build) { + return new JdkVersion(raw, build); + } + + public List components() { + return new ArrayList<>(components); + } + + // JEP-322 + public String feature() { + return components.get(0); + } + + public Optional interim() { + if (components.size() > 1) { + return Optional.of(components.get(1)); + } else { + return Optional.empty(); + } + } + + public Optional update() { + if (components.size() > 2) { + return Optional.of(components.get(2)); + } else { + return Optional.empty(); + } + } + + public Optional patch() { + if (components.size() > 3) { + return Optional.of(components.get(3)); + } else { + return Optional.empty(); + } + } + + public Optional opt() { + return Optional.ofNullable(opt); + } + + public Optional resolvedInBuild() { + return Optional.ofNullable(build); + } + + // Return the number from a numbered build (e.g., 'b12' -> 12), or -1 if not a numbered build. + public int resolvedInBuildNumber() { + if (build == null || build.length() < 2 || build.charAt(0) != 'b') { + return -1; + } else { + return Integer.parseInt(build.substring(1)); + } + } + + private String legacyFeaturePrefix() { + var legacyPrefixMatcher = legacyPrefixPattern.matcher(feature()); + if (legacyPrefixMatcher.matches()) { + return legacyPrefixMatcher.group(1); + } else { + return ""; + } + } + + @Override + public int compareTo(JdkVersion o) { + // Filter out the legacy prefix (if they are the same) to enable numerical comparison + var prefix = legacyFeaturePrefix(); + var otherPrefix = o.legacyFeaturePrefix(); + + var myComponents = new ArrayList<>(components); + var otherComponents = new ArrayList<>(o.components); + if (!prefix.isBlank() && prefix.equals(otherPrefix)) { + myComponents.set(0, myComponents.get(0).substring(prefix.length())); + otherComponents.set(0, otherComponents.get(0).substring(prefix.length())); + } + + // Compare element by element, numerically if possible + for (int i = 0; i < Math.min(myComponents.size(), otherComponents.size()); ++i) { + var elementComparison = 0; + var myComponent = myComponents.get(i); + var otherComponent = otherComponents.get(i); + try { + elementComparison = Integer.compare(Integer.parseInt(myComponent), Integer.parseInt(otherComponent)); + } catch (NumberFormatException e) { + elementComparison = myComponent.compareTo(otherComponent); + } + if (elementComparison != 0) { + return elementComparison; + } + } + + // A version with additional components comes after an otherwise identical one (12.1.1 > 12.1) + var sizeDiff = Integer.compare(myComponents.size(), otherComponents.size()); + if (sizeDiff != 0) { + return sizeDiff; + } + + // Finally, check the opt part + if (opt != null) { + if (o.opt == null) { + return 1; + } else { + return opt.compareTo(o.opt); + } + } else { + if (o.opt == null) { + return 0; + } else { + return -1; + } + } + } + + @Override + public String toString() { + return "Version{" + + "raw='" + raw + '\'' + + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + JdkVersion jdkVersion = (JdkVersion) o; + return raw.equals(jdkVersion.raw); + } + + @Override + public int hashCode() { + return Objects.hash(raw); + } +} diff --git a/bots/notify/src/test/java/org/openjdk/skara/bots/notify/issue/BackportsTests.java b/bots/notify/src/test/java/org/openjdk/skara/bots/notify/issue/BackportsTests.java new file mode 100644 index 000000000..b496b60cd --- /dev/null +++ b/bots/notify/src/test/java/org/openjdk/skara/bots/notify/issue/BackportsTests.java @@ -0,0 +1,383 @@ +/* + * Copyright (c) 2020, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package org.openjdk.skara.bots.notify.issue; + +import org.junit.jupiter.api.*; +import org.openjdk.skara.issuetracker.*; +import org.openjdk.skara.json.JSON; +import org.openjdk.skara.test.HostCredentials; + +import java.io.IOException; +import java.util.*; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class BackportsTests { + @Test + void mainIssue(TestInfo testInfo) throws IOException { + try (var credentials = new HostCredentials(testInfo)) { + var issueProject = credentials.getIssueProject(); + + var issue1 = credentials.createIssue(issueProject, "Issue 1"); + issue1.setProperty("issuetype", JSON.of("Bug")); + + var issue2 = credentials.createIssue(issueProject, "Issue 2"); + issue2.setProperty("issuetype", JSON.of("Backport")); + issue1.addLink(Link.create(issue2, "backported by").build()); + + var issue3 = credentials.createIssue(issueProject, "Issue 3"); + issue3.setProperty("issuetype", JSON.of("Backport")); + issue3.addLink(Link.create(issue1, "backport of").build()); + + assertEquals(issue1, Backports.findMainIssue(issue1).orElseThrow()); + assertEquals(issue1, Backports.findMainIssue(issue2).orElseThrow()); + assertEquals(issue1, Backports.findMainIssue(issue3).orElseThrow()); + } + } + + @Test + void noMainIssue(TestInfo testInfo) throws IOException { + try (var credentials = new HostCredentials(testInfo)) { + var issueProject = credentials.getIssueProject(); + + var issue1 = credentials.createIssue(issueProject, "Issue 1"); + issue1.setProperty("issuetype", JSON.of("Bug")); + + var issue2 = credentials.createIssue(issueProject, "Issue 2"); + issue2.setProperty("issuetype", JSON.of("Backport")); + + var issue3 = credentials.createIssue(issueProject, "Issue 3"); + issue3.setProperty("issuetype", JSON.of("Backport")); + issue2.addLink(Link.create(issue3, "backported by").build()); + + assertEquals(issue1, Backports.findMainIssue(issue1).orElseThrow()); + assertEquals(Optional.empty(), Backports.findMainIssue(issue2)); + assertEquals(Optional.empty(), Backports.findMainIssue(issue3)); + } + } + + @Test + void nonBackportLink(TestInfo testInfo) throws IOException { + try (var credentials = new HostCredentials(testInfo)) { + var issueProject = credentials.getIssueProject(); + + var issue1 = credentials.createIssue(issueProject, "Issue 1"); + issue1.setProperty("issuetype", JSON.of("Bug")); + + var issue2 = credentials.createIssue(issueProject, "Issue 2"); + issue2.setProperty("issuetype", JSON.of("Bug")); + issue1.addLink(Link.create(issue2, "duplicated by").build()); + + var issue3 = credentials.createIssue(issueProject, "Issue 3"); + issue3.setProperty("issuetype", JSON.of("CSR")); + issue1.addLink(Link.create(issue3, "CSRed by").build()); + + assertEquals(issue1, Backports.findMainIssue(issue1).orElseThrow()); + assertEquals(issue2, Backports.findMainIssue(issue2).orElseThrow()); + assertEquals(Optional.empty(), Backports.findMainIssue(issue3)); + } + } + + @Test + void findMainVersion(TestInfo testInfo) throws IOException { + try (var credentials = new HostCredentials(testInfo)) { + var issueProject = credentials.getIssueProject(); + var issue = credentials.createIssue(issueProject, "Issue"); + + issue.setProperty("fixVersions", JSON.array().add("tbd")); + assertEquals(Optional.empty(), Backports.mainFixVersion(issue)); + + issue.setProperty("fixVersions", JSON.array().add("tbd_minor")); + assertEquals(Optional.empty(), Backports.mainFixVersion(issue)); + + issue.setProperty("fixVersions", JSON.array().add("unknown")); + assertEquals(Optional.empty(), Backports.mainFixVersion(issue)); + + issue.setProperty("fixVersions", JSON.array().add("11.3")); + assertEquals(List.of("11", "3"), Backports.mainFixVersion(issue).orElseThrow().components()); + + issue.setProperty("fixVersions", JSON.array().add("unknown").add("11.3")); + assertEquals(List.of("11", "3"), Backports.mainFixVersion(issue).orElseThrow().components()); + + issue.setProperty("fixVersions", JSON.array().add("11.3").add("unknown")); + assertEquals(List.of("11", "3"), Backports.mainFixVersion(issue).orElseThrow().components()); + + issue.setProperty("fixVersions", JSON.array().add("11.3").add("12.1")); + assertEquals(Optional.empty(), Backports.mainFixVersion(issue)); + + issue.setProperty("fixVersions", JSON.array().add("12.1").add("11.3")); + assertEquals(Optional.empty(), Backports.mainFixVersion(issue)); + } + } + + @Test + void findIssue(TestInfo testInfo) throws IOException { + try (var credentials = new HostCredentials(testInfo)) { + var issueProject = credentials.getIssueProject(); + var issue = credentials.createIssue(issueProject, "Issue"); + issue.setProperty("issuetype", JSON.of("Bug")); + var backport = credentials.createIssue(issueProject, "Backport"); + backport.setProperty("issuetype", JSON.of("Backport")); + issue.addLink(Link.create(backport, "backported by").build()); + + issue.setProperty("fixVersions", JSON.array().add("11-pool")); + backport.setProperty("fixVersions", JSON.array().add("12-pool")); + assertEquals(issue, Backports.findIssue(issue, JdkVersion.parse("11.1")).orElseThrow()); + assertEquals(backport, Backports.findIssue(issue, JdkVersion.parse("12.2")).orElseThrow()); + assertEquals(Optional.empty(), Backports.findIssue(issue, JdkVersion.parse("13.3"))); + + issue.setProperty("fixVersions", JSON.array().add("tbd")); + assertEquals(issue, Backports.findIssue(issue, JdkVersion.parse("11.1")).orElseThrow()); + + issue.setProperty("fixVersions", JSON.array().add("12.2")); + backport.setProperty("fixVersions", JSON.array().add("tbd")); + assertEquals(issue, Backports.findIssue(issue, JdkVersion.parse("12.2")).orElseThrow()); + assertEquals(backport, Backports.findIssue(issue, JdkVersion.parse("11.1")).orElseThrow()); + + issue.setProperty("fixVersions", JSON.array().add("12.2")); + backport.setProperty("fixVersions", JSON.array().add("11.1")); + assertEquals(issue, Backports.findIssue(issue, JdkVersion.parse("12.2")).orElseThrow()); + assertEquals(backport, Backports.findIssue(issue, JdkVersion.parse("11.1")).orElseThrow()); + assertEquals(Optional.empty(), Backports.findIssue(issue, JdkVersion.parse("13.3"))); + } + } + + private static class BackportManager { + private final HostCredentials credentials; + private final IssueProject issueProject; + private final List issues; + + private void setVersion(Issue issue, String version) { + var resolvedInBuild = ""; + if (version.contains("/")) { + resolvedInBuild = version.split("/", 2)[1]; + version = version.split("/", 2)[0]; + } + issue.setProperty("fixVersions", JSON.array().add(version)); + if (!resolvedInBuild.isEmpty()) { + issue.setProperty("customfield_10006", JSON.of(resolvedInBuild)); + } + } + + BackportManager(HostCredentials credentials, String initialVersion) { + this.credentials = credentials; + issueProject = credentials.getIssueProject(); + issues = new ArrayList<>(); + + issues.add(credentials.createIssue(issueProject, "Main issue")); + issues.get(0).setProperty("issuetype", JSON.of("Bug")); + setVersion(issues.get(0), initialVersion); + } + + void addBackports(String... versions) { + for (int backportIndex = 0; backportIndex < versions.length; ++backportIndex) { + var issue = credentials.createIssue(issueProject, "Backport issue " + backportIndex); + issue.setProperty("issuetype", JSON.of("Backport")); + setVersion(issue, versions[backportIndex]); + issues.get(0).addLink(Link.create(issue, "backported by").build()); + issues.add(issue); + } + } + + void assertLabeled(String... labeledVersions) { + Backports.labelReleaseStreamDuplicates(issues.get(0), "hgupdater-sync"); + + var labels = new HashSet<>(Arrays.asList(labeledVersions)); + var labeledIssues = new HashSet(); + for (var issue : issues) { + var version = issue.properties().get("fixVersions").get(0).asString(); + if (issue.labels().contains("hgupdater-sync")) { + labeledIssues.add(version); + } + } + assertEquals(labels, labeledIssues); + } + } + + @Test + void labelFeatureReleaseStream(TestInfo testInfo) throws IOException { + try (var credentials = new HostCredentials(testInfo)) { + var backports = new BackportManager(credentials, "15"); + backports.assertLabeled(); + + backports.addBackports("14", "16"); + backports.assertLabeled("15", "16"); + } + } + + @Test + void labelOpenJfxFeatureReleaseStream(TestInfo testInfo) throws IOException { + try (var credentials = new HostCredentials(testInfo)) { + var backports = new BackportManager(credentials, "openjfx15"); + backports.assertLabeled(); + + backports.addBackports("openjfx14", "openjfx16"); + backports.assertLabeled(); + } + } + + @Test + void labelUpdateReleaseStream(TestInfo testInfo) throws IOException { + try (var credentials = new HostCredentials(testInfo)) { + var backports = new BackportManager(credentials, "14"); + backports.assertLabeled(); + + backports.addBackports("14.0.1", "14.0.2"); + backports.assertLabeled("14.0.1", "14.0.2"); + + backports.addBackports("15", "15.0.1", "15.0.2"); + backports.assertLabeled("14.0.1", "14.0.2", "15.0.1", "15.0.2"); + } + } + + @Test + void labelOpenJdkUpdateReleaseStream(TestInfo testInfo) throws IOException { + try (var credentials = new HostCredentials(testInfo)) { + var backports = new BackportManager(credentials, "11"); + backports.assertLabeled(); + + backports.addBackports("11.0.1", "11.0.2"); + backports.assertLabeled("11.0.1", "11.0.2"); + + backports.addBackports("11.0.3", "11.0.3-oracle"); + backports.assertLabeled("11.0.1", "11.0.2", "11.0.3", "11.0.3-oracle"); + } + } + + @Test + void labelBprStream8(TestInfo testInfo) throws IOException { + try (var credentials = new HostCredentials(testInfo)) { + var backports = new BackportManager(credentials, "8u251"); + backports.assertLabeled(); + + backports.addBackports("8u241/b31"); + backports.assertLabeled(); + } + } + + @Test + void labelBprStream11(TestInfo testInfo) throws IOException { + try (var credentials = new HostCredentials(testInfo)) { + var backports = new BackportManager(credentials, "11"); + backports.assertLabeled(); + + backports.addBackports("11.0.7.0.3-oracle"); + backports.assertLabeled(); + + backports.addBackports("11.0.8.0.1-oracle", "12.0.3.0.1-oracle"); + backports.assertLabeled("11.0.8.0.1-oracle"); + } + } + + @Test + void labelTest8229219(TestInfo testInfo) throws IOException { + try (var credentials = new HostCredentials(testInfo)) { + var backports = new BackportManager(credentials, "13/b33"); + backports.assertLabeled(); + + backports.addBackports("14/b10"); + backports.assertLabeled("14"); + + backports.addBackports("13.0.1/b06", "13.0.2/b01"); + backports.assertLabeled("14", "13.0.1", "13.0.2"); + } + } + + @Test + void labelTest8244004(TestInfo testInfo) throws IOException { + try (var credentials = new HostCredentials(testInfo)) { + var backports = new BackportManager(credentials, "8u271/master"); + backports.assertLabeled(); + + backports.addBackports("8u251/b34"); + backports.assertLabeled(); + + backports.addBackports("8u260/master", "8u261/b06"); + backports.assertLabeled("8u261", "8u271"); + } + } + + @Test + void labelTest8077707(TestInfo testInfo) throws IOException { + try (var credentials = new HostCredentials(testInfo)) { + var backports = new BackportManager(credentials, "9/b78"); + backports.assertLabeled(); + + backports.addBackports("emb-9/team"); + backports.assertLabeled(); + + backports.addBackports("openjdk8u242/team", "openjdk8u232/master"); + backports.assertLabeled(); + + backports.addBackports("8u261/b04", "8u251/b01", "8u241/b31", "8u231/b34"); + backports.assertLabeled("8u261", "8u241"); + + backports.addBackports("emb-8u251/team", "7u261/b01"); + backports.assertLabeled("8u261", "8u241"); + } + } + + @Test + void labelTest8239803(TestInfo testInfo) throws IOException { + try (var credentials = new HostCredentials(testInfo)) { + var backports = new BackportManager(credentials, "openjfx15"); + backports.assertLabeled(); + + backports.addBackports("8u261/b01", "8u251/b31", "8u241/b33"); + backports.assertLabeled("8u251"); + } + } + + @Test + void labelTest7092821(TestInfo testInfo) throws IOException { + try (var credentials = new HostCredentials(testInfo)) { + var backports = new BackportManager(credentials, "12/b24"); + backports.assertLabeled(); + + backports.addBackports("13/team", "11.0.8-oracle/b01", "11.0.7/b02"); + backports.assertLabeled("13"); + + backports.addBackports("8u261/b01", "8u251/b33", "8u241/b61"); + backports.assertLabeled("13"); + } + } + + @Test + void labelTest8222913(TestInfo testInfo) throws IOException { + try (var credentials = new HostCredentials(testInfo)) { + var backports = new BackportManager(credentials, "13"); + backports.assertLabeled(); + + backports.addBackports("11.0.6-oracle"); + + backports.addBackports("11.0.5.0.1-oracle", "11.0.5-oracle", "11.0.5"); + backports.assertLabeled("11.0.6-oracle"); + + backports.addBackports("11.0.4.0.1-oracle", "11.0.4-oracle", "11.0.4"); + backports.assertLabeled("11.0.6-oracle", "11.0.5.0.1-oracle", "11.0.5-oracle", "11.0.5"); + + backports.addBackports("11.0.3.0.1-oracle"); + backports.assertLabeled("11.0.4.0.1-oracle", "11.0.6-oracle", "11.0.5.0.1-oracle", "11.0.5-oracle", "11.0.5"); + } + } +} diff --git a/bots/notify/src/test/java/org/openjdk/skara/bots/notify/issue/IssueNotifierTests.java b/bots/notify/src/test/java/org/openjdk/skara/bots/notify/issue/IssueNotifierTests.java index 7346e1785..8581206fe 100644 --- a/bots/notify/src/test/java/org/openjdk/skara/bots/notify/issue/IssueNotifierTests.java +++ b/bots/notify/src/test/java/org/openjdk/skara/bots/notify/issue/IssueNotifierTests.java @@ -24,21 +24,58 @@ import org.junit.jupiter.api.*; import org.openjdk.skara.bots.notify.NotifyBot; -import org.openjdk.skara.issuetracker.Issue; -import org.openjdk.skara.json.JSON; +import org.openjdk.skara.forge.HostedRepository; +import org.openjdk.skara.issuetracker.*; +import org.openjdk.skara.json.*; import org.openjdk.skara.test.*; import java.io.IOException; import java.net.URI; +import java.nio.file.Path; import java.util.*; import java.util.regex.Pattern; +import java.util.stream.Collectors; -import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.*; import static org.openjdk.skara.bots.notify.TestUtils.*; +import static org.openjdk.skara.issuetracker.Issue.State.*; public class IssueNotifierTests { + private Set fixVersions(Issue issue) { + if (!issue.properties().containsKey("fixVersions")) { + return Set.of(); + } + return issue.properties().get("fixVersions").stream() + .map(JSONValue::asString) + .collect(Collectors.toSet()); + } + + private TestBotFactory testBotBuilder(HostedRepository hostedRepository, IssueProject issueProject, Path storagePath, JSONObject notifierConfig) throws IOException { + if (!notifierConfig.contains("project")) { + notifierConfig.put("project", "issueproject"); + } + return TestBotFactory.newBuilder() + .addHostedRepository("hostedrepo", hostedRepository) + .addIssueProject("issueproject", issueProject) + .storagePath(storagePath) + .addConfiguration("database", JSON.object() + .put("repository", "hostedrepo:history") + .put("name", "duke") + .put("email", "duke@openjdk.org")) + .addConfiguration("ready", JSON.object() + .put("labels", JSON.array()) + .put("comments", JSON.array())) + .addConfiguration("integrator", JSON.of(hostedRepository.forge().currentUser().id())) + .addConfiguration("repositories", JSON.object() + .put("hostedrepo", JSON.object() + .put("basename", "test") + .put("branches", "master") + .put("issue", notifierConfig))) + .build(); + } + @Test - void testIssueIdempotence(TestInfo testInfo) throws IOException { + void testIssueLinkIdempotence(TestInfo testInfo) throws IOException { try (var credentials = new HostCredentials(testInfo); var tempFolder = new TemporaryDirectory()) { var repo = credentials.getHostedRepository(); @@ -342,4 +379,345 @@ void testPullRequestPROnly(TestInfo testInfo) throws IOException { assertEquals(2, links.size()); } } + + @Test + void testIssue(TestInfo testInfo) throws IOException { + try (var credentials = new HostCredentials(testInfo); + var tempFolder = new TemporaryDirectory()) { + + var repo = credentials.getHostedRepository(); + var repoFolder = tempFolder.path().resolve("repo"); + var localRepo = CheckableRepository.init(repoFolder, repo.repositoryType()); + credentials.commitLock(localRepo); + localRepo.pushAll(repo.url()); + + var issueProject = credentials.getIssueProject(); + var storageFolder = tempFolder.path().resolve("storage"); + var jbsNotifierConfig = JSON.object().put("fixversions", JSON.object()); + var notifyBot = testBotBuilder(repo, issueProject, storageFolder, jbsNotifierConfig).create("notify", JSON.object()); + + // Initialize history + TestBotRunner.runPeriodicItems(notifyBot); + + // Create an issue and commit a fix + var authorEmailAddress = issueProject.issueTracker().currentUser().userName() + "@openjdk.org"; + var issue = issueProject.createIssue("This is an issue", List.of("Indeed"), Map.of("issuetype", JSON.of("Enhancement"))); + var editHash = CheckableRepository.appendAndCommit(localRepo, "Another line", issue.id() + ": Fix that issue", "Duke", authorEmailAddress); + localRepo.push(editHash, repo.url(), "master"); + TestBotRunner.runPeriodicItems(notifyBot); + + // The changeset should be reflected in a comment + var updatedIssue = issueProject.issue(issue.id()).orElseThrow(); + + var comments = updatedIssue.comments(); + assertEquals(1, comments.size()); + var comment = comments.get(0); + assertTrue(comment.body().contains(editHash.abbreviate())); + + // As well as a fixVersion + assertEquals(Set.of("0.1"), fixVersions(updatedIssue)); + + // The issue should be assigned and resolved + assertEquals(RESOLVED, updatedIssue.state()); + assertEquals(List.of(issueProject.issueTracker().currentUser()), updatedIssue.assignees()); + } + } + + @Test + void testIssueNoVersion(TestInfo testInfo) throws IOException { + try (var credentials = new HostCredentials(testInfo); + var tempFolder = new TemporaryDirectory()) { + var repo = credentials.getHostedRepository(); + var repoFolder = tempFolder.path().resolve("repo"); + var localRepo = CheckableRepository.init(repoFolder, repo.repositoryType(), Path.of("appendable.txt"), Set.of(), null); + credentials.commitLock(localRepo); + localRepo.pushAll(repo.url()); + + var storageFolder = tempFolder.path().resolve("storage"); + var issueProject = credentials.getIssueProject(); + var jbsNotifierConfig = JSON.object().put("fixversions", JSON.object()); + var notifyBot = testBotBuilder(repo, issueProject, storageFolder, jbsNotifierConfig).create("notify", JSON.object()); + + // Initialize history + TestBotRunner.runPeriodicItems(notifyBot); + + // Create an issue and commit a fix + var issue = issueProject.createIssue("This is an issue", List.of("Indeed"), Map.of("issuetype", JSON.of("Enhancement"))); + var editHash = CheckableRepository.appendAndCommit(localRepo, "Another line", issue.id() + ": Fix that issue"); + localRepo.push(editHash, repo.url(), "master"); + TestBotRunner.runPeriodicItems(notifyBot); + + // The changeset should be reflected in a comment + var comments = issue.comments(); + assertEquals(1, comments.size()); + var comment = comments.get(0); + assertTrue(comment.body().contains(editHash.abbreviate())); + + // But not in the fixVersion + assertEquals(Set.of(), fixVersions(issue)); + } + } + + @Test + void testIssueConfiguredVersionNoCommit(TestInfo testInfo) throws IOException { + try (var credentials = new HostCredentials(testInfo); + var tempFolder = new TemporaryDirectory()) { + var repo = credentials.getHostedRepository(); + var repoFolder = tempFolder.path().resolve("repo"); + var localRepo = CheckableRepository.init(repoFolder, repo.repositoryType(), Path.of("appendable.txt"), Set.of(), null); + credentials.commitLock(localRepo); + localRepo.pushAll(repo.url()); + + var storageFolder = tempFolder.path().resolve("storage"); + var issueProject = credentials.getIssueProject(); + var jbsNotifierConfig = JSON.object().put("fixversions", JSON.object().put("master", "2.0")); + var notifyBot = testBotBuilder(repo, issueProject, storageFolder, jbsNotifierConfig).create("notify", JSON.object()); + + // Initialize history + TestBotRunner.runPeriodicItems(notifyBot); + + // Create an issue and commit a fix + var issue = issueProject.createIssue("This is an issue", List.of("Indeed"), Map.of("issuetype", JSON.of("Enhancement"))); + var editHash = CheckableRepository.appendAndCommit(localRepo, "Another line", issue.id() + ": Fix that issue"); + localRepo.push(editHash, repo.url(), "master"); + TestBotRunner.runPeriodicItems(notifyBot); + + // The changeset should not reflected in a comment + var comments = issue.comments(); + assertEquals(1, comments.size()); + var comment = comments.get(0); + assertTrue(comment.body().contains(editHash.abbreviate())); + + // As well as a fixVersion - but not the one from the repo + assertEquals(Set.of("2.0"), fixVersions(issue)); + + // And no commit link + var links = issue.links(); + assertEquals(0, links.size()); + } + } + + @Test + void testIssueIdempotence(TestInfo testInfo) throws IOException { + try (var credentials = new HostCredentials(testInfo); + var tempFolder = new TemporaryDirectory()) { + var repo = credentials.getHostedRepository(); + var repoFolder = tempFolder.path().resolve("repo"); + var localRepo = CheckableRepository.init(repoFolder, repo.repositoryType()); + credentials.commitLock(localRepo); + localRepo.pushAll(repo.url()); + + var storageFolder = tempFolder.path().resolve("storage"); + var issueProject = credentials.getIssueProject(); + var jbsNotifierConfig = JSON.object().put("fixversions", JSON.object()); + var notifyBot = testBotBuilder(repo, issueProject, storageFolder, jbsNotifierConfig).create("notify", JSON.object()); + + // Initialize history + TestBotRunner.runPeriodicItems(notifyBot); + + // Save the state + var historyState = localRepo.fetch(repo.url(), "history"); + + // Create an issue and commit a fix + var issue = issueProject.createIssue("This is an issue", List.of("Indeed"), Map.of("issuetype", JSON.of("Enhancement"))); + var editHash = CheckableRepository.appendAndCommit(localRepo, "Another line", issue.id() + ": Fix that issue"); + localRepo.push(editHash, repo.url(), "master"); + TestBotRunner.runPeriodicItems(notifyBot); + + // The changeset should be reflected in a comment + var comments = issue.comments(); + assertEquals(1, comments.size()); + var comment = comments.get(0); + assertTrue(comment.body().contains(editHash.abbreviate())); + + // As well as a fixVersion + assertEquals(Set.of("0.1"), fixVersions(issue)); + + // Wipe the history + localRepo.push(historyState, repo.url(), "history", true); + + // Run it again + TestBotRunner.runPeriodicItems(notifyBot); + + // There should be no new comments or fixVersions + var updatedIssue = issueProject.issue(issue.id()).orElseThrow(); + assertEquals(1, updatedIssue.comments().size()); + assertEquals(Set.of("0.1"), fixVersions(updatedIssue)); + } + } + + @Test + void testIssuePoolVersion(TestInfo testInfo) throws IOException { + try (var credentials = new HostCredentials(testInfo); + var tempFolder = new TemporaryDirectory()) { + var repo = credentials.getHostedRepository(); + var repoFolder = tempFolder.path().resolve("repo"); + var localRepo = CheckableRepository.init(repoFolder, repo.repositoryType(), Path.of("appendable.txt"), Set.of(), null); + credentials.commitLock(localRepo); + localRepo.pushAll(repo.url()); + + var storageFolder = tempFolder.path().resolve("storage"); + var issueProject = credentials.getIssueProject(); + var jbsNotifierConfig = JSON.object().put("fixversions", JSON.object().put("master", "12u14")); + var notifyBot = testBotBuilder(repo, issueProject, storageFolder, jbsNotifierConfig).create("notify", JSON.object()); + + // Initialize history + TestBotRunner.runPeriodicItems(notifyBot); + + // Create an issue and commit a fix + var issue = issueProject.createIssue("This is an issue", List.of("Indeed"), Map.of("issuetype", JSON.of("Enhancement"))); + issue.setProperty("fixVersions", JSON.array().add("12-pool").add("tbd13").add("unknown")); + + var editHash = CheckableRepository.appendAndCommit(localRepo, "Another line", issue.id() + ": Fix that issue"); + localRepo.push(editHash, repo.url(), "master"); + TestBotRunner.runPeriodicItems(notifyBot); + + // The fixVersion should have been updated + assertEquals(Set.of("12u14"), fixVersions(issue)); + } + } + + @Test + void testIssuePoolOpenVersion(TestInfo testInfo) throws IOException { + try (var credentials = new HostCredentials(testInfo); + var tempFolder = new TemporaryDirectory()) { + var repo = credentials.getHostedRepository(); + var repoFolder = tempFolder.path().resolve("repo"); + var localRepo = CheckableRepository.init(repoFolder, repo.repositoryType(), Path.of("appendable.txt"), Set.of(), null); + credentials.commitLock(localRepo); + localRepo.pushAll(repo.url()); + + var storageFolder = tempFolder.path().resolve("storage"); + var issueProject = credentials.getIssueProject(); + var jbsNotifierConfig = JSON.object().put("fixversions", JSON.object().put("master", "12u14")); + var notifyBot = testBotBuilder(repo, issueProject, storageFolder, jbsNotifierConfig).create("notify", JSON.object()); + + // Initialize history + TestBotRunner.runPeriodicItems(notifyBot); + + // Create an issue and commit a fix + var issue = issueProject.createIssue("This is an issue", List.of("Indeed"), Map.of("issuetype", JSON.of("Enhancement"))); + issue.setProperty("fixVersions", JSON.array().add("12-pool").add("tbd13").add("unknown")); + + var editHash = CheckableRepository.appendAndCommit(localRepo, "Another line", issue.id() + ": Fix that issue"); + localRepo.push(editHash, repo.url(), "master"); + TestBotRunner.runPeriodicItems(notifyBot); + + // The fixVersion should have been updated + assertEquals(Set.of("12u14"), fixVersions(issue)); + } + } + + @Test + void testIssueBackport(TestInfo testInfo) throws IOException { + try (var credentials = new HostCredentials(testInfo); + var tempFolder = new TemporaryDirectory()) { + var repo = credentials.getHostedRepository(); + var repoFolder = tempFolder.path().resolve("repo"); + var localRepo = CheckableRepository.init(repoFolder, repo.repositoryType(), Path.of("appendable.txt"), Set.of(), null); + credentials.commitLock(localRepo); + localRepo.pushAll(repo.url()); + + var storageFolder = tempFolder.path().resolve("storage"); + var issueProject = credentials.getIssueProject(); + var jbsNotifierConfig = JSON.object().put("fixversions", JSON.object().put("master", "12.0.2")); + var notifyBot = testBotBuilder(repo, issueProject, storageFolder, jbsNotifierConfig).create("notify", JSON.object()); + + // Initialize history + TestBotRunner.runPeriodicItems(notifyBot); + + // Create an issue and commit a fix + var issue = issueProject.createIssue("This is an issue", List.of("Indeed"), + Map.of("issuetype", JSON.of("Enhancement"), + "customfield_10008", JSON.object() + .put("id", 244) + .put("name", "java.io"), + "customfield_10005", JSON.array() + .add(JSON.object() + .put("id", "17010") + .put("value", "generic")) + .add(JSON.object() + .put("id", "17019") + .put("value", "other")) + )); + issue.setProperty("fixVersions", JSON.array().add("13.0.1")); + issue.setProperty("priority", JSON.of("1")); + + var authorEmailAddress = issueProject.issueTracker().currentUser().userName() + "@openjdk.org"; + var editHash = CheckableRepository.appendAndCommit(localRepo, "Another line", issue.id() + ": Fix that issue", "Duke", authorEmailAddress); + localRepo.push(editHash, repo.url(), "master"); + TestBotRunner.runPeriodicItems(notifyBot); + + // The fixVersion should not have been updated + var updatedIssue = issueProject.issue(issue.id()).orElseThrow(); + assertEquals(Set.of("13.0.1"), fixVersions(updatedIssue)); + assertEquals(OPEN, updatedIssue.state()); + assertEquals(List.of(), updatedIssue.assignees()); + + // There should be a link + var links = updatedIssue.links(); + assertEquals(1, links.size()); + var link = links.get(0); + var backport = link.issue().orElseThrow(); + + // The backport issue should have a correct fixVersion and assignee + assertEquals(Set.of("12.0.2"), fixVersions(backport)); + assertEquals(RESOLVED, backport.state()); + assertEquals(List.of(issueProject.issueTracker().currentUser()), backport.assignees()); + + // Custom properties should also propagate + assertEquals("1", backport.properties().get("priority").asString()); + assertEquals(244, backport.properties().get("customfield_10008").get("id").asInt()); + assertEquals("java.io", backport.properties().get("customfield_10008").get("name").asString()); + assertEquals(2, backport.properties().get("customfield_10005").asArray().size()); + } + } + + @Test + void testSyncLabels(TestInfo testInfo) throws IOException { + try (var credentials = new HostCredentials(testInfo); + var tempFolder = new TemporaryDirectory()) { + var repo = credentials.getHostedRepository(); + var repoFolder = tempFolder.path().resolve("repo"); + var localRepo = CheckableRepository.init(repoFolder, repo.repositoryType()); + credentials.commitLock(localRepo); + localRepo.pushAll(repo.url()); + + var storageFolder = tempFolder.path().resolve("storage"); + var issueProject = credentials.getIssueProject(); + var jbsNotifierConfig = JSON.object().put("fixversions", JSON.object().put("master", "8u192")); + var notifyBot = testBotBuilder(repo, issueProject, storageFolder, jbsNotifierConfig).create("notify", JSON.object()); + + // Initialize database + TestBotRunner.runPeriodicItems(notifyBot); + + var issue1 = credentials.createIssue(issueProject, "Issue 1"); + issue1.setProperty("issuetype", JSON.of("Bug")); + + var issue2 = credentials.createIssue(issueProject, "Issue 2"); + issue2.setProperty("fixVersions", JSON.array().add(JSON.of("8u162"))); + issue2.setProperty("issuetype", JSON.of("Backport")); + issue1.addLink(Link.create(issue2, "backported by").build()); + + var issue3 = credentials.createIssue(issueProject, "Issue 3"); + issue3.setProperty("fixVersions", JSON.array().add(JSON.of("10"))); + issue3.setProperty("issuetype", JSON.of("Backport")); + issue1.addLink(Link.create(issue3, "backported by").build()); + + var issue4 = credentials.createIssue(issueProject, "Issue 4"); + issue4.setProperty("fixVersions", JSON.array().add(JSON.of("11"))); + issue4.setProperty("issuetype", JSON.of("Backport")); + issue1.addLink(Link.create(issue4, "backported by").build()); + + // Mention one of the issues + var commit = CheckableRepository.appendAndCommit(localRepo, "Hello there", issue1.id() + ": A fix"); + localRepo.push(commit, repo.url(), "master"); + TestBotRunner.runPeriodicItems(notifyBot); + + assertEquals(List.of("hgupdater-sync"), issue1.labels()); + assertEquals(List.of(), issue2.labels()); + assertEquals(List.of(), issue3.labels()); + assertEquals(List.of("hgupdater-sync"), issue4.labels()); + } + } } diff --git a/bots/notify/src/test/java/org/openjdk/skara/bots/notify/issue/JdkVersionTests.java b/bots/notify/src/test/java/org/openjdk/skara/bots/notify/issue/JdkVersionTests.java new file mode 100644 index 000000000..d97fe4c0b --- /dev/null +++ b/bots/notify/src/test/java/org/openjdk/skara/bots/notify/issue/JdkVersionTests.java @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2020, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package org.openjdk.skara.bots.notify.issue; + +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class JdkVersionTests { + @Test + void jep223() { + assertEquals(List.of("8"), JdkVersion.parse("8").components()); + assertEquals(List.of("9", "0", "4"), JdkVersion.parse("9.0.4").components()); + assertEquals(List.of("10", "0", "2"), JdkVersion.parse("10.0.2").components()); + assertEquals(List.of("11"), JdkVersion.parse("11").components()); + assertEquals(List.of("11", "0", "3"), JdkVersion.parse("11.0.3").components()); + assertEquals(List.of("12", "0", "2"), JdkVersion.parse("12.0.2").components()); + } + + @Test + void jep322() { + assertEquals(List.of("11", "0", "2", "0", "1"), JdkVersion.parse("11.0.2.0.1-oracle").components()); + assertEquals("oracle", JdkVersion.parse("11.0.2.0.1-oracle").opt().orElseThrow()); + assertEquals(List.of("11", "0", "3"), JdkVersion.parse("11.0.3-oracle").components()); + assertEquals("oracle", JdkVersion.parse("11.0.3-oracle").opt().orElseThrow()); + assertEquals(List.of("12"), JdkVersion.parse("12u-cpu").components()); + assertEquals("cpu", JdkVersion.parse("12u-cpu").opt().orElseThrow()); + assertEquals(List.of("13"), JdkVersion.parse("13u-open").components()); + assertEquals("open", JdkVersion.parse("13u-open").opt().orElseThrow()); + } + + @Test + void legacy() { + assertEquals(List.of("5.0", "45"), JdkVersion.parse("5.0u45").components()); + assertEquals(List.of("6", "201"), JdkVersion.parse("6u201").components()); + assertEquals(List.of("7", "40"), JdkVersion.parse("7u40").components()); + assertEquals(List.of("8", "211"), JdkVersion.parse("8u211").components()); + assertEquals(List.of("emb-8", "171"), JdkVersion.parse("emb-8u171").components()); + assertEquals(List.of("hs22", "4"), JdkVersion.parse("hs22.4").components()); + assertEquals(List.of("hs23"), JdkVersion.parse("hs23").components()); + assertEquals(List.of("openjdk7"), JdkVersion.parse("openjdk7u").components()); + assertEquals(List.of("openjdk8"), JdkVersion.parse("openjdk8").components()); + assertEquals(List.of("openjdk8", "211"), JdkVersion.parse("openjdk8u211").components()); + } + + @Test + void order() { + assertEquals(0, JdkVersion.parse("5.0u45").compareTo(JdkVersion.parse("5.0u45"))); + assertEquals(0, JdkVersion.parse("11.0.3").compareTo(JdkVersion.parse("11.0.3"))); + assertEquals(0, JdkVersion.parse("11.0.2.0.1-oracle").compareTo(JdkVersion.parse("11.0.2.0.1-oracle"))); + + assertEquals(1, JdkVersion.parse("6u201").compareTo(JdkVersion.parse("5.0u45"))); + assertEquals(-1, JdkVersion.parse("5.0u45").compareTo(JdkVersion.parse("6u201"))); + + assertEquals(-1, JdkVersion.parse("11.0.2.0.1").compareTo(JdkVersion.parse("11.0.2.0.1-oracle"))); + assertEquals(1, JdkVersion.parse("11.0.2.0.1-oracle").compareTo(JdkVersion.parse("11.0.2.0.1"))); + + assertEquals(-1, JdkVersion.parse("9.0.4").compareTo(JdkVersion.parse("10.0.2"))); + assertEquals(-1, JdkVersion.parse("11").compareTo(JdkVersion.parse("11.0.3"))); + assertEquals(-1, JdkVersion.parse("emb-8u171").compareTo(JdkVersion.parse("emb-8u175"))); + assertEquals(-1, JdkVersion.parse("emb-8u71").compareTo(JdkVersion.parse("emb-8u170"))); + assertEquals(-1, JdkVersion.parse("openjdk7u").compareTo(JdkVersion.parse("openjdk7u42"))); + assertEquals(-1, JdkVersion.parse("hs22.4").compareTo(JdkVersion.parse("hs23"))); + } + + @Test + void nonConforming() { + assertEquals("bla", JdkVersion.parse("bla").feature()); + assertEquals("", JdkVersion.parse("").feature()); + } +}