#2296: Refactor ToolCommandlet to determine InstalledEdition and Version in a Single Cached Lookup - #2306
Conversation
Coverage Report for CI Build 32465591394Coverage increased (+0.1%) to 73.077%Details
Uncovered ChangesNo uncovered changes found. Coverage Regressions224 previously-covered lines in 11 files lost coverage.
Coverage Stats💛 - Coveralls |
laert-ll
left a comment
There was a problem hiding this comment.
Thanks for your work! The overall direction is nice, and the EditionAndVersion with a single cached lookup is exactly what the issue was after. I've left a few inline comments on some details that need some improvement.
The main thing I noticed is that the branch seems to accidentally include some unrelated work (the GUI changes from #2278 and some changelog changes as well). Make sure to rebase to main to clean this up and fix the merge conflicts. Apart from that I would also suggest adding some tests for this.
Other than that, thanks for your contribution again!
) Introduce EditionAndVersion to resolve a tool's installed edition and version in a single cached lookup, with a protected computeInstalledEditionAndVersion() hook. - KubeCtl: read version from 'kubectl version --client' and route through the public getInstalledEditionAndVersion() override so it is actually used; add KubeCtlTest. - LocalToolCommandlet: resolve edition and version in one tool-path lookup. - Docker: keep the resolved edition consistent ('docker'/'rancher') across all OSes instead of the bogus 'desktop'; detect the Docker Desktop version on macOS from the Docker.app bundle; add DockerTest. - CHANGELOG: add devonfw#2296 line under 2026.08.002. Rebased onto upstream/main (drops unrelated devonfw#2278 work).
a9c8cee to
160440a
Compare
hohwille
left a comment
There was a problem hiding this comment.
@krystynaShatkovska thanks for your PR. You got the story right and made your way perfectly through this. Great that you added missing tests for docker and kubectl. Maybe your mockito approach is worth sharing and presenting to the team. Great job 👍
I left some comments for rework.
Follow up (not part of this PR):
Once comments are resolved and this is merged, we should have the foundation to rework our .ide.software.version issues. For software installed via repository we can then resolve the version together with the edition from the symlink. A fallback to .ide.software.version could only happen for devonfw-ide legacy compatibility if no (sym)link was found.
For other tools like Python we could simply add some code that determines the version from cli (e.g. python --version) and auto-recovers a lost .ide.software.version` file.
| /** | ||
| * @deprecated Override {@link #computeInstalledEditionAndVersion()} instead. | ||
| * @return the currently installed version. | ||
| */ | ||
| @Deprecated | ||
| protected VersionIdentifier computeInstalledVersion() { | ||
|
|
||
| return getInstalledVersionDeprecated(); | ||
| } | ||
|
|
||
| /** | ||
| * @deprecated Override {@link #computeInstalledEditionAndVersion()} instead. | ||
| * @return the installed edition. | ||
| */ | ||
| @Deprecated | ||
| protected String computeInstalledEdition() { | ||
|
|
||
| return getInstalledEditionDeprecated(); | ||
| } | ||
|
|
||
| /** | ||
| * @deprecated Override {@link #computeInstalledEditionAndVersion()} instead. | ||
| * @return the installed version. | ||
| */ | ||
| @Deprecated | ||
| protected VersionIdentifier getInstalledVersionDeprecated() { | ||
|
|
||
| return null; | ||
| } | ||
|
|
||
| /** | ||
| * @deprecated Override {@link #computeInstalledEditionAndVersion()} instead. | ||
| * @return the installed edition. | ||
| */ | ||
| @Deprecated | ||
| protected String getInstalledEditionDeprecated() { | ||
|
|
||
| return null; | ||
| } |
There was a problem hiding this comment.
do we still need this or was that only and intermediate state during refactoring?
| /** Invalidates the cached installed edition and version so the next call to {@link #getInstalledEditionAndVersion()} recomputes the result. */ | ||
| protected void invalidateInstalledEditionAndVersion() { | ||
|
|
||
| this.installedEditionAndVersion = null; |
There was a problem hiding this comment.
| this.installedEditionAndVersion = null; | |
| if (this.installedEditionAndVersion != null) { | |
| this.installedEditionAndVersion.invalidate(); | |
| } |
BTW: I would even suggest to make installedEditionAndVersion final and initialize it in the constructor since the creation is cheap and does not compute the value. Then you can skip such if statements and also the lazy init in getInstalledEditionAndVersion() is not needed any more. BTW: In the GUI we will have more concurrency and initialization in constructor is always thread-safe while lazy init in getter could cause race condition.
| // Resolve edition and version from a single tool-path lookup (one pass) instead of two separate lookups. | ||
| String edition = getInstalledEdition(toolPath); | ||
| VersionIdentifier version = getInstalledVersion(toolPath); | ||
| if ((edition == null) && (version == null)) { |
There was a problem hiding this comment.
Just for the record: having an edition here but no version seems like an inconsistent state to me.
Hopefully never happens but wouldn't this be more consistent?
| if ((edition == null) && (version == null)) { | |
| if (version == null) { |
| // Log a warning and return null (instead of throwing) when the command produces no usable output, e.g. when | ||
| // Docker Desktop is not installed via apt. | ||
| String output = this.context.newProcess().runAndGetSingleOutput(IdeLogLevel.WARNING, "bash", "-lc", dockerDesktopVersionLinuxCommand); | ||
| return (output != null) ? super.resolveVersionWithPattern(output, DOCKER_DESKTOP_VERSION_PATTERN) : null; |
There was a problem hiding this comment.
Not your fault - was already like this before but prefixing method calls with super should only be used when overridden as otherwise this is only causing confusion:
| return (output != null) ? super.resolveVersionWithPattern(output, DOCKER_DESKTOP_VERSION_PATTERN) : null; | |
| return (output != null) ? resolveVersionWithPattern(output, DOCKER_DESKTOP_VERSION_PATTERN) : null; |
| // Log a warning and return null (instead of throwing) when the command produces no usable output, e.g. when | ||
| // Docker Desktop is not installed at /Applications/Docker.app. | ||
| String output = this.context.newProcess().runAndGetSingleOutput(IdeLogLevel.WARNING, "bash", "-lc", dockerDesktopVersionMacCommand); | ||
| return (output != null) ? super.resolveVersionWithPattern(output, DOCKER_DESKTOP_VERSION_PATTERN) : null; |
There was a problem hiding this comment.
| return (output != null) ? super.resolveVersionWithPattern(output, DOCKER_DESKTOP_VERSION_PATTERN) : null; | |
| return (output != null) ? resolveVersionWithPattern(output, DOCKER_DESKTOP_VERSION_PATTERN) : null; |
|
|
||
| @Override | ||
| public VersionIdentifier getInstalledVersion() { | ||
| public EditionAndVersion getInstalledEditionAndVersion() { |
There was a problem hiding this comment.
Shouldn't this happen in computeInstalledEditionAndVersion()?
If you make getInstalledEditionAndVersion() final the compiler will help you to find such things.
This PR fixes #2296
Implemented changes:
This PR refactors the way IDEasy determines the installed edition and version of tools. Previously,
getInstalledEdition()andgetInstalledVersion()were separate methodsthat often performed redundant expensive lookups (registry queries, process execution, etc.). The new combined approach computes both values in a single call.
EditionAndVersion(String edition, VersionIdentifier version)to hold both values togethergetInstalledEditionAndVersion()withCachedValuetoToolCommandlet— delegates tocomputeInstalledEditionAndVersion()getInstalledVersion()andgetInstalledEdition()final— they now delegate to the combined methodcomputeInstalledEditionAndVersion()inLocalToolCommandlet(resolves edition + version via software link target)GlobalToolCommandlet:getWindowsRegistryAppNames()returnsMap<String, String>mapping edition → registry app nameDocker: returnsMap.of(\"docker\", \"Docker Desktop\", \"rancher\", \"Rancher Desktop\")— enabling correct detection of Rancher Desktop editioncomputeInstalledEditionAndVersion()inLocalToolCommandlet(resolves edition + version via software link target)GlobalToolCommandlet:getWindowsRegistryAppNames()returnsMap<String, String>mapping edition → registry app nameDocker: returnsMap.of(\"docker\", \"Docker Desktop\", \"rancher\", \"Rancher Desktop\")— enabling correct detection of Rancher Desktop editioninvalidateInstalledEditionAndVersion()IdeContext.findProjectswas package-private instead of public)Backward compatibility is maintained via deprecated hooks (
computeInstalledEdition(),computeInstalledVersion(),getInstalledVersionDeprecated(),getInstalledEditionDeprecated()).Testing instructions
Checklist for this PR