-
Notifications
You must be signed in to change notification settings - Fork 30
Resolve versions using aether, and uncouple existing Resolver from code base. #45
Conversation
Can one of the admins verify this patch? |
331fafd
to
a835e37
Compare
if (isInvalidRangeResult(rangeResult)) { | ||
throw new InvalidArtifactCoordinateException(messageForInvalidArtifact(groupId, artifactId, versionSpec)); | ||
} | ||
return rangeResult.getHighestVersion().toString(); |
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.
I think this should be getRecommendedVersion() (if not null) or getSelectedVersion(artifact).
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.
VersionRangeResult actually does not support either method. [source]
Those methods are associated with org.apache.maven.artifact.versioning.VersionRange. They are independent of aether.
Let me play around with this.
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.
This is still unresolved.
try { | ||
rangeResult = repositorySystem.resolveVersionRange(repositorySystemSession, rangeRequest); | ||
} catch (VersionRangeResolutionException e) { | ||
throw new InvalidArtifactCoordinateException(messageForInvalidArtifact(groupId, artifactId, versionSpec)); |
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.
Add e.getMessage() to the error.
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.
done.
|
||
VersionRangeResult rangeResult; | ||
try { | ||
rangeResult = repositorySystem.resolveVersionRange(repositorySystemSession, rangeRequest); |
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.
If I say 3.0, no one conflicts with 3.0, 3.0 exists... we should just use 3.0, not make a request to Maven Central. How about using some of the existing "find a version" code and, if that can't find anything, we do a network request?
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.
If I say 3.0, no one conflicts with 3.0, 3.0 exists...
How do we determine that 3.0 exists without checking the Maven Central?
Suppose there is some artifact a:b:3.12
we want. Now, suppose we create a mirror of maven central. This a:b:3.12
exists within Maven Central. However, there is no a:b:3.12
in the mirror, but there is a a:b:3.13
. Using the the above heuristic in lieu of a network request, we would select a:b:3.12.
Later, when we try to fetch the artifact, we would find that there is no a:b:3.12
, and then skip resolving its transitive dependencies.
This is incorrect behavior. "3.10" is a soft pin, rather than a hard pin. In the situation, there is no 3.10, it should still be able to use 3.11, 3.12, 3.13, etc.
How about using some of the existing "find a version" code and, if that can't find anything, we do a network request?
This is very much a tradeoff between correctness and performance. IMO, until we can match maven/aether one to one, I think we should err on the side of over correctness. We can freely tune things afterwards.
@@ -14,13 +14,25 @@ | |||
|
|||
package com.google.devtools.build.workspace.maven; | |||
|
|||
import static com.google.devtools.build.workspace.maven.ArtifactBuilder.InvalidArtifactCoordinateException; | |||
import static com.google.devtools.build.workspace.maven.ArtifactBuilder.fromMavenDependency; |
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.
Don't use static imports for methods unless it's a test.
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.
done.
private final RepositorySystem repositorySystem; | ||
private final RepositorySystemSession repositorySystemSession; | ||
|
||
private VersionResolver(RepositorySystem repositorySystem, |
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.
Make AetherUtils non-static, pass it in here, and add helper methods when you need to contact aether (aetherUtils.requestVersionRange()
). Then you can customize repos & test.
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.
I like this. It's basically a facade around aether. I made the necessary changes. I have also made a few hacks to ensure it works with the Resolver and DefaultMoldelResolver, i.e. defaultResolver
@@ -0,0 +1,47 @@ | |||
package com.google.devtools.build.workspace.maven; |
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.
Nit: add licenses at the tops of new files.
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.
done
@@ -1,7 +1,16 @@ | |||
package com.google.devtools.build.workspace.maven; | |||
|
|||
|
|||
import static com.google.devtools.build.workspace.maven.AetherUtils.mavenCentralRepository; |
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.
Remove.
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.
done.
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.
Bah, I always mix up the Aether and Maven versions of classes.
try { | ||
Mockito.when(system.resolveVersionRange(anySession(), anyRangeRequest())) | ||
.thenThrow(new VersionRangeResolutionException(any())); | ||
Aether aether = Aether.builder().systemSession(anySession(), system).build(); |
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.
The point is to do mock(Aether.class) here.
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.
done
public void softPinnedVersions() { | ||
try { | ||
String version = | ||
defaultResolver().resolveVersion("something", "something", "3.4"); |
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.
Unit tests should not contact Maven.
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.
I figured as such. This was more a desperate attempt to at least document the intended behavior.
I am finding testing the actual version resolution, not the failure, to be somewhat prohibitory. Two reasons why. (1) VersionRangeResult can't be mocked since it's a final class. (2) VersionRangeResult is very tightly coupled with other things within Aether, so creating using a constructor is also meh.
What are your thoughts on the following options:
- Using PowerMock. This lets us mock final classes. It is a bit of a pain to import this into the code base, but very doable.
- Modifying the method for our Aether class to return the highest version. I'm not the biggest fan of this.
- Just not test the behavior. "You can't fail unit tests, if you don't have any" - some authoritative source
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.
I am finding testing the actual version resolution, not the failure, to be somewhat prohibitory.
Keep in mind that we don't want to test the actual version resolution. That's Aether's business. We just want to be able to test how your code behaves.
Two reasons why. (1) VersionRangeResult can't be mocked since it's a final class. (2) VersionRangeResult is very tightly coupled with other things within Aether, so creating using a constructor is also meh.
Have it return a Version. Or a list of versions. You don't need a response, here, just the version.
Just not test the behavior. "You can't fail unit tests, if you don't have any" - some authoritative source
ಠ_ಠ
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.
Keep in mind that we don't want to test the actual version resolution. That's Aether's business. We just want to be able to test how your code behaves.
I am not we need to add any further tests or whether we can need the soft pin tests. For full code coverage, I believe we simply need to cover the following cases:
- given a null result, it throws an exception
- given a versionrangeresolution exception, it throws an exception
- given neither, it doesn't throw an exception and returns something.
Have it return a Version. Or a list of versions. You don't need a response, here, just the version.
Again this is a tradeoff. Finer grained tests will mean more responsibility to mimic aether/maven's behavior.
I assume we are trying to match with how aether or maven resolves versions. If so, returning a list of versions means we will need to include code to decide which version to pick. Right now, we can simply write versionRangeResult.getHighestVersion()
and call it a day. However, if we return the list, then we will need to account for any weird behavior aether/maven may have.
For example, Aether internally uses VersionRangeResult.getHighestVersion()
for resolving all versions. For a soft pin, I have found that this returns the soft pinned value provided it exists. This is regardless of whether the soft pinned value is the highest version or not. The behavior when that soft pin does not exist is undocumented. I am guessing it will be the next closest version, but I am not sure on that front.
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.
I assume we are trying to match with how aether or maven resolves versions. If so, returning a list of versions means we will need to include code to decide which version to pick.
I was basing this on VersionRangeResult's API. It doesn't have any "get the right version" method, so your code will have to handle it regardless.
The behavior when that soft pin does not exist is undocumented.
You can try this out pretty easily and see what Aether does. I wrote up a quick sample program to do version requests for different version constraints on Guava and I have bad news for you: it seem that Aether returns the soft pin version, even if it doesn't exist. I'm not sure what you're supposed to do to check if it's valid.
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.
i'll look into it and determine a solution.
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.
Thank you for pointing this out. It's quite unfortunate aether is not as complete as hoped for. That said, I have been playing around with a few ideas. For any given artifact a:b:version-spec
, we can make a version range request for a:b:[,)
and aether will return a list of all potential versions of that artifact.
Here is one approach we could take. Whenever resolveVersion
is called, we can simply make the request for [,)
, i.e. the unbounded request. With all potential versions, we could then manually select the version from the available versions. This would push the brunt of selecting versions on us. However, it is much more reliable than what I have right now.
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.
Thank you for pointing that out. Disappointing news, indeed.
I'm not sure what you're supposed to do to check if it's valid.
I was playing around with it, and found that if you request an unbounded version [,)
, it returns all possible versions for an artifact as a list (ascending from oldest to latest).
For this soft pin issue, here is a potential algorithm.
def resolve_version(group, artifact, version):
if is_soft_pin(version):
allVersions = fetch_all_versions(group, artifact, version)
return select_version(version, all_versions)
# what we have implemented
return resolve_version_range(group, artifact, version)
def select_version(version, all_versions):
if version in all_versions:
return version
# choose one from {fail, find nearest alternative, select highest available}
This issue only comes up with soft pins, so for version ranges, we can default to the implementation in this CL.
As for how we approach it if it's not there, I have a few ideas but let's address that in another pull request. The best approach imo would be to select the nearest valid version.
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.
Here is a simple implementation of what I commented on above. I'll make a separate pull request after this is merged.
So there's good news and bad news. 👍 The good news is that everyone that needs to sign a CLA (the pull request submitter and all commit authors) have done so. Everything is all good there. 😕 The bad news is that it appears that one or more commits were authored by someone other than the pull request submitter. We need to confirm that they're okay with their commits being contributed to this project. Please have them confirm that here in the pull request. Note to project maintainer: This is a terminal state, meaning the |
This is a part of my effort to replace the Resolver, with Aether. Currently, artifacts are built using Resolver.getArtifact(). In addition, the Resolver defines a class of exceptions that is associated with creating a new DefaultArtifact. This functionality can and should be removed from the Resolver. After this change, the resolver is only called from within (1) itself, (2) its tests and (3) the defaultmodelresolver.
This is a part of an ongoing effort to isolate and then replace the existing Resolver with Aether. Version resolution is the only remaining part of the Resolver that is used by any other class, DefaultModelResolver. With this removed the Resolver is currently only used by itself, its tests and GenerateWorkspace. The current implementation of resolving version given a version spec is adhoc and idiosyncratic. This CL isolates this functionality, and provides a canonical/correct version resolution mechanism. Correct version resolution has two components. (1) Pins both hard and soft, e.g. "[3.0]" and "3.0", should select the pinned version, e.g. "3.0", and (2) version ranges should select the highest available version. This functionality is easily accomplished through aether. It involves the construction of a version range request, followed by a call to aether. Tests are implemented for when there is invalid behavior, as well as for pinned version ranges, i.e. "3.2". However, due to limitations of Mockito and Aether (mockito annot mock final classes), tests are not included for correct behavior of version ranges.
All classes make create and make requests through the Aether facade. This convenience class allows us to not have to pass the RepositorySystemSession, RepositorySystem, and RemoteRepository's in multiple classes. There is one major TODO. The older classes are not constructing this object. They are just using default settings.
287f7ad
to
71ab932
Compare
CLAs look good, thanks! |
3563fca
to
92fc31c
Compare
provided by aether
@@ -74,33 +73,44 @@ public void failsOnInvalidVersionRange() { | |||
* Asserts that given a soft pinned version specification, it selects that version, | |||
* and does not get the highest version. "3.4" is an example of a soft pinned version specification. | |||
*/ | |||
//TODO(petros): implicitly using Maven central for resolving version. This is sketchy. | |||
@Test | |||
public void softPinnedVersions() { |
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.
Here are the changed unit tests. Tried rebasing on your last pull request and that wiped out history.
I simply assert that given a list of versions from aether, we select the highest version (i.e last element in list). I included a test for soft pins, where aether simply returns a single version.
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.
Gotta love GitHub code review. Sigh.
If this ends up being too slow, I think a good optimization would be to only start looking up valid versions if getting the artifact fails. However, I'm willing to let that go, for now.
resolver.resolveVersion("something", "something", "1.0"); | ||
assertThat(version).isEqualTo("1.0"); | ||
|
||
} catch ( |
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.
Remove the try/catch.
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.
Done.
Aether aether = Mockito.mock(Aether.class); | ||
|
||
try { | ||
// Using `anyRangeResult()` will ensure that rangeResult.highestVersion() == null. |
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.
Out of date comment.
Also, use https://stackoverflow.com/questions/156503/how-do-you-assert-that-a-certain-exception-is-thrown-in-junit-4-tests here and above.
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.
that's quite neat, actually.
Jenkins, test this please. |
I think it does merge it with HEAD.
…On Thu, Jul 27, 2017 at 3:02 PM, Petros Eskinder ***@***.***> wrote:
I am unsure how to interpret the Jenkins results. All tests pass locally.
Moreover, the specific failure is also peculiar to me. It states that it
fails the nonConflictingDepManagementRange. However, I haven't merged the
changes from your dependency management PR. Unless, of course, it's merging
these and then running the tests.
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
<#45 (comment)>,
or mute the thread
<https://github.com/notifications/unsubscribe-auth/AABCkgl16TKXioUBxgVUPpduze7WgSiYks5sSN6pgaJpZM4OcHZ0>
.
|
Previously version resolution was determined locally, without consideration of the maven central repository. This change removes any hacky calls to maven central.
All version resolution is now conducted by the VersionResolver using aether.
Jenkins, test this please. |
I resolved the issue. I removed the I also resolved the issue with the soft pins. If you want I can add the commit to this PR or another one. The relevant changes can be found here. It was a simple enough solution. Transform "3.0" to "[3.0, )" and return the first element in the list of potential versions. |
@kchodorow can you retract the approval, and rereview? I decided on simply including the soft pin fix in this pull request. Since the change was simple and very closely related to this, I felt it was best to keep it in the same PR. Here is the relevant commit message:
If you prefer I do this in a separate pull request, I can also do that. |
dbf5bdd
to
a1bf4c0
Compare
Currently, even if a soft pinned version, e.g. "3.0", does not exist aether returns that soft pinned version. Instead, it should provide another valid version. This change solves this issue by transforming the soft pin into a version range, and making the aether request using that version range. For example, instead of requesting for "3.0", we transform it to "[3.0,)" providing us all versions between 3.0 and the latest version. If the version exists, we simply take it. If it doesn't, then we select the next closest valid version.
78f1920
to
55289fc
Compare
So there's good news and bad news. 👍 The good news is that everyone that needs to sign a CLA (the pull request submitter and all commit authors) have done so. Everything is all good there. 😕 The bad news is that it appears that one or more commits were authored by someone other than the pull request submitter. We need to confirm that they're okay with their commits being contributed to this project. Please have them confirm that here in the pull request. Note to project maintainer: This is a terminal state, meaning the |
CLAs look good, thanks! |
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.
Jenkins, test this please.
Interesting, I guess Jenkins doesn't see the code review comment? Test this please. |
Context
This is a part of an ongoing effort to isolate and then, replace the existing Resolver with Aether.
Alongside actually resolving artifacts, the existing Resolver also:
(1) builds Artifacts from coordinates,
(2) parses versions from version specifications and decides on a version to use.
Both are orthogonal to the role of the Resolver, and make the Resolver unnecessarily coupled to various classes. This PR isolates (and also corrects) this functionality. Note: as this PR is not intended to lead to regressions, it should be merged to master.
Detailed Design
Building Artifacts
Currently, artifacts are built using
Resolver.getArtifact().
The Resolver also defines a class of exceptions for when this fails. These are both orthogonal to the role of the Resolver. However, since the function/exception is called from the DefaultModelResolver and the GenerateWorkspaceOptions, it unnecessarily couples the Resolver to these two classes. I have removed these and placed them in the ArtifactBuilder.Version Resolution
Version resolution selects a version given a maven coordinate and a version specification. This is also called from the DefaultModelResolver and GenerateWorkspaceOptions.
I have also elected to separate that and also provide a corrected implementation. The current implementation of resolving a version from a version spec is adhoc and idiosyncratic. This CL isolates this functionality, and provides a canonical/correct version resolution mechanism.
Maven's version resolution has two components:
(1) it selects the pinned version from both hard and soft pins, e.g. "[3.0]" and "3.0" should evaluate to "3.0"
(2) it selects the highest available version for version ranges e.g. "[3.0, )" should select 3.9 if its highest available
This functionality is easily implemented through aether using a version range request.
Unit Tests
Tests are implemented for when there is invalid behavior, as well as for pinned version ranges, i.e. "3.2". However, due to limitations of Mockito and Aether (mockito cannot mock final classes), tests
are not included for correct behavior of version ranges. In particular, there are no tests for ensuring that the highest version is selected.
Reviewer: @kchodorow
Related Issues #16