From 6f1be945d3e0cfaa73eb7e73e31da5e198e45d4a Mon Sep 17 00:00:00 2001 From: moritzLanger <44358321+moritzLanger@users.noreply.github.com> Date: Thu, 4 Jan 2024 17:45:37 +0100 Subject: [PATCH 1/5] #5: added contributen rules and guideines (#156) --- ...contribution-rules-and-guidelines.asciidoc | 110 ++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 documentation/IDEasy-contribution-rules-and-guidelines.asciidoc diff --git a/documentation/IDEasy-contribution-rules-and-guidelines.asciidoc b/documentation/IDEasy-contribution-rules-and-guidelines.asciidoc new file mode 100644 index 000000000..0e5d26b24 --- /dev/null +++ b/documentation/IDEasy-contribution-rules-and-guidelines.asciidoc @@ -0,0 +1,110 @@ +:toc: macro +toc::[] + +== Project Board +The IDEasy *Project Board* with its Columns should be used as followed: + +* *New*: [.underline]#Issues# that are newly created and have yet to be +refined +* *Backlog*: [.underline]#Issues# that are refined but not yet being +worked on +* *Research*: [.underline]#Issues# that are blocked or need complex +research/analysis to make progress. Typically, these are issues that +somebody tried to solve but it turned out to be hard to make progress +and find a solution. +* *In Progress*: [.underline]#Issues# that are currently being worked +on, they must be assigned to the person (or people) working on it. You can +see on the board if there is a pull-request linked to it. If not, the +developer is still working on the story “in the dark”. Otherwise, there +is already a solution implemented. The PR may be in draft state, +otherwise the PR should be in one of the following two columns. +* *Team Review*: [.underline]#Pull Request# that is to be or currently +under review by a member of the dev team. The reviewer is the assignee +of the PR. +* *In Review*: [.underline]#Pull Request# that is to be or currently +under review by a final reviewer that should also be the assignee of the +PR. Typically the final reviewer is the Project Owner (currently +hohwille) but it may also be done by team members with longer experience +in the project. +* *Done*: [.underline]#Issues# and [.underline]#Pull Request# that have +been completed and merged. + +To better organize the board and avoid overload, only pull request are allowed in the review columns. +Issues remain `in progress` until completed via merge of PR. + +General conventions for contributions to devonfw can be found +https://github.com/devonfw/.github/blob/master/CONTRIBUTING.asciidoc#code-changes[here]. +The following conventions are added on top by the IDEasy team from the +learnings & retros for our best way of working together: + +== Pull Request + +* The title starts with a hashtag and the corresponding issue number (in +case there is no issue to the PR, either create one or use a fitting +epic) +* The title describes what the PR contains, if an issue is fully +completed with the PR, the issue title could be used for example. +* The description starts with a link to the related issue, if this PR +finishes the issue, the following keyword should be used to +automatically link and close the issue upon merging the PR: +** Closes +** For more info see +(https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue) +* The description can also contain additional information for the PR and +changed files. +* More complicated pull request should contain a short summary of the +changed files and commits. +* In addition, other issue or PRs can be linked, most commonly with +keyword like: +** Blocked by +** Related +** Merge first +* Comments requested changes and other conversations in a pull request +should only be resolved by the person that started them and NOT the +creator of the pull request. (As the permissions might not always allow +this, a final comment by the creator of the conversations, saying it can +be resolved is the second 0ption.) +* Conversations should be answered by the owner of the pull request, so +to show that the suggestion was either implemented or acknowledged. +* The pull request should be assigned to the person that has work on the +PR, either the reviewer or the owner depending on whether a review is to +be done or the review needs to be addressed. +* A pull request should not be a draft when it is in a review. + +== Commit + +* Commit messages should always start with the issue number and a +hashtag, so to automatically link the corresponding issue. +* The title of a commit should be kept short and informative. +* The description of a commit message can be used to elaborate on the +title. + +== Issue + +* Issues should be written clearly and easy to comprehend. +* Issues should use the existing template. +* The goal or requirements of the issue should be explained first. +* Potential or desired implementations can or should be described after +the preview point. +* A very good practice and nice to have, are acceptance criteria for the +issue. +* Other issues can be linked using a hashtag and the issue number, most +commonly used keywords: +** Related to +** Blocked by + +== Review + +* The reviewer should be assigned to the PR, if a review is needed, or +the requested changes need to be checked and conversations need to be +resolved. +* After completing the review, the owner of the PR should be assigned. +* After the team review is finished the PO (hohwille) should be +assigned. +* While reviewing a useful tool is the web ide provided by github. +Simply open the `files changed` tab and press `.` on the keyboard. +* Another useful tool is to use the feature “insert a suggestion” while +writing a comment (for more detail see +https://haacked.com/archive/2019/06/03/suggested-changes/) (This feature +does not reformat the code, so be aware that you need to manually add +the spaces etc.) From bae97bc1d4a34c491f35cd14d3f95481ee82ab48 Mon Sep 17 00:00:00 2001 From: Mattes Mrzik Date: Thu, 4 Jan 2024 19:12:35 +0100 Subject: [PATCH 2/5] #139: feature for making symlinks relative (#140) --- .../com/devonfw/tools/ide/io/FileAccess.java | 22 +- .../devonfw/tools/ide/io/FileAccessImpl.java | 162 +++++- .../tools/ide/io/FileAccessImplTest.java | 472 ++++++++++++++++++ .../ide/version/VersionIdentifierTest.java | 2 +- 4 files changed, 640 insertions(+), 18 deletions(-) create mode 100644 cli/src/test/java/com/devonfw/tools/ide/io/FileAccessImplTest.java diff --git a/cli/src/main/java/com/devonfw/tools/ide/io/FileAccess.java b/cli/src/main/java/com/devonfw/tools/ide/io/FileAccess.java index 8d2874471..d20361128 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/io/FileAccess.java +++ b/cli/src/main/java/com/devonfw/tools/ide/io/FileAccess.java @@ -59,10 +59,28 @@ public interface FileAccess { void move(Path source, Path targetDir); /** - * @param source the source {@link Path} to link to. + * Creates a symbolic link. If the given {@code targetLink} already exists and is a symbolic link or a Windows + * junction, it will be replaced. In case of missing privileges, Windows Junctions may be used as fallback, which must + * point to absolute paths. Therefore, the created link will be absolute instead of relative. + * + * @param source the source {@link Path} to link to, may be relative or absolute. * @param targetLink the {@link Path} where the symbolic link shall be created pointing to {@code source}. + * @param relative - {@code true} if the symbolic link shall be relative, {@code false} if it shall be absolute. */ - void symlink(Path source, Path targetLink); + void symlink(Path source, Path targetLink, boolean relative); + + /** + * Creates a relative symbolic link. If the given {@code targetLink} already exists and is a symbolic link or a + * Windows junction, it will be replaced. In case of missing privileges, Windows Junctions may be used as fallback, + * which must point to absolute paths. Therefore, the created link will be absolute instead of relative. + * + * @param source the source {@link Path} to link to, may be relative or absolute. + * @param targetLink the {@link Path} where the symbolic link shall be created pointing to {@code source}. + */ + default void symlink(Path source, Path targetLink) { + + symlink(source, targetLink, true); + } /** * @param source the source {@link Path file or folder} to copy. diff --git a/cli/src/main/java/com/devonfw/tools/ide/io/FileAccessImpl.java b/cli/src/main/java/com/devonfw/tools/ide/io/FileAccessImpl.java index e341ba43a..08985d6d5 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/io/FileAccessImpl.java +++ b/cli/src/main/java/com/devonfw/tools/ide/io/FileAccessImpl.java @@ -13,8 +13,11 @@ import java.net.http.HttpResponse; import java.nio.file.FileSystemException; import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.NoSuchFileException; import java.nio.file.Path; import java.nio.file.Paths; +import java.nio.file.attribute.BasicFileAttributes; import java.security.DigestInputStream; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; @@ -290,28 +293,158 @@ private void copyRecursive(Path source, Path target, FileCopyMode mode) throws I } } + /** + * Deletes the given {@link Path} if it is a symbolic link or a Windows junction. And throws an + * {@link IllegalStateException} if there is a file at the given {@link Path} that is neither a symbolic link nor a + * Windows junction. + * + * @param path the {@link Path} to delete. + * @throws IOException if the actual {@link Files#delete(Path) deletion} fails. + */ + private void deleteLinkIfExists(Path path) throws IOException { + + boolean exists = false; + boolean isJunction = false; + if (this.context.getSystemInfo().isWindows()) { + try { // since broken junctions are not detected by Files.exists(brokenJunction) + BasicFileAttributes attr = Files.readAttributes(path, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS); + exists = true; + isJunction = attr.isOther() && attr.isDirectory(); + } catch (NoSuchFileException e) { + // ignore, since there is no previous file at the location, so nothing to delete + return; + } + } + exists = exists || Files.exists(path); // "||" since broken junctions are not detected by + // Files.exists(brokenJunction) + boolean isSymlink = exists && Files.isSymbolicLink(path); + + assert !(isSymlink && isJunction); + + if (exists) { + if (isJunction || isSymlink) { + this.context.info("Deleting previous " + (isJunction ? "junction" : "symlink") + " at " + path); + Files.delete(path); + } else { + throw new IllegalStateException( + "The file at " + path + " was not deleted since it is not a symlink or a Windows junction"); + } + } + } + + /** + * Adapts the given {@link Path} to be relative or absolute depending on the given {@code relative} flag. + * Additionally, {@link Path#toRealPath(LinkOption...)} is applied to {@code source}. + * + * @param source the {@link Path} to adapt. + * @param targetLink the {@link Path} used to calculate the relative path to the {@code source} if {@code relative} is + * set to {@code true}. + * @param relative the {@code relative} flag. + * @return the adapted {@link Path}. + * @see FileAccessImpl#symlink(Path, Path, boolean) + */ + private Path adaptPath(Path source, Path targetLink, boolean relative) throws IOException { + + if (source.isAbsolute()) { + try { + source = source.toRealPath(LinkOption.NOFOLLOW_LINKS); // to transform ../d1/../d2 to ../d2 + } catch (IOException e) { + throw new IOException( + "Calling toRealPath() on the source (" + source + ") in method FileAccessImpl.adaptPath() failed.", e); + } + if (relative) { + source = targetLink.getParent().relativize(source); + // to make relative links like this work: dir/link -> dir + source = (source.toString().isEmpty()) ? Paths.get(".") : source; + } + } else { // source is relative + if (relative) { + // even though the source is already relative, toRealPath should be called to transform paths like + // this ../d1/../d2 to ../d2 + source = targetLink.getParent() + .relativize(targetLink.resolveSibling(source).toRealPath(LinkOption.NOFOLLOW_LINKS)); + source = (source.toString().isEmpty()) ? Paths.get(".") : source; + } else { // !relative + try { + source = targetLink.resolveSibling(source).toRealPath(LinkOption.NOFOLLOW_LINKS); + } catch (IOException e) { + throw new IOException("Calling toRealPath() on " + targetLink + ".resolveSibling(" + source + + ") in method FileAccessImpl.adaptPath() failed.", e); + } + } + } + return source; + } + + /** + * Creates a Windows junction at {@code targetLink} pointing to {@code source}. + * + * @param source must be another Windows junction or a directory. + * @param targetLink the location of the Windows junction. + */ + private void createWindowsJunction(Path source, Path targetLink) { + + this.context.trace("Creating a Windows junction at " + targetLink + " with " + source + " as source."); + Path fallbackPath; + if (!source.isAbsolute()) { + this.context.warning( + "You are on Windows and you do not have permissions to create symbolic links. Junctions are used as an " + + "alternative, however, these can not point to relative paths. So the source (" + source + + ") is interpreted as an absolute path."); + try { + fallbackPath = targetLink.resolveSibling(source).toRealPath(LinkOption.NOFOLLOW_LINKS); + } catch (IOException e) { + throw new IllegalStateException( + "Since Windows junctions are used, the source must be an absolute path. The transformation of the passed " + + "source (" + source + ") to an absolute path failed.", + e); + } + + } else { + fallbackPath = source; + } + if (!Files.isDirectory(fallbackPath)) { // if source is a junction. This returns true as well. + throw new IllegalStateException( + "These junctions can only point to directories or other junctions. Please make sure that the source (" + + fallbackPath + ") is one of these."); + } + this.context.newProcess().executable("cmd") + .addArgs("/c", "mklink", "/d", "/j", targetLink.toString(), fallbackPath.toString()).run(); + } + @Override - public void symlink(Path source, Path targetLink) { + public void symlink(Path source, Path targetLink, boolean relative) { - this.context.trace("Creating symbolic link {} pointing to {}", targetLink, source); + Path adaptedSource = null; try { - if (Files.exists(targetLink) && Files.isSymbolicLink(targetLink)) { - this.context.debug("Deleting symbolic link to be re-created at {}", targetLink); - Files.delete(targetLink); - } - Files.createSymbolicLink(targetLink, source); + adaptedSource = adaptPath(source, targetLink, relative); + } catch (IOException e) { + throw new IllegalStateException("Failed to adapt source for source (" + source + ") target (" + targetLink + + ") and relative (" + relative + ")", e); + } + this.context.trace("Creating {} symbolic link {} pointing to {}", adaptedSource.isAbsolute() ? "" : "relative", + targetLink, adaptedSource); + + try { + deleteLinkIfExists(targetLink); + } catch (IOException e) { + throw new IllegalStateException("Failed to delete previous symlink or Windows junction at " + targetLink, e); + } + + try { + Files.createSymbolicLink(targetLink, adaptedSource); } catch (FileSystemException e) { if (this.context.getSystemInfo().isWindows()) { - this.context.info( - "Due to lack of permissions, Microsofts mklink with junction has to be used to create a Symlink. See https://github.com/devonfw/IDEasy/blob/main/documentation/symlinks.asciidoc for further details. Error was: " - + e.getMessage()); - this.context.newProcess().executable("cmd") - .addArgs("/c", "mklink", "/d", "/j", targetLink.toString(), source.toString()).run(); + this.context.info("Due to lack of permissions, Microsoft's mklink with junction had to be used to create " + + "a Symlink. See https://github.com/devonfw/IDEasy/blob/main/documentation/symlinks.asciidoc for " + + "further details. Error was: " + e.getMessage()); + createWindowsJunction(adaptedSource, targetLink); } else { throw new RuntimeException(e); } } catch (IOException e) { - throw new IllegalStateException("Failed to create a symbolic link " + targetLink + " pointing to " + source, e); + throw new IllegalStateException("Failed to create a " + (adaptedSource.isAbsolute() ? "" : "relative") + + "symbolic link " + targetLink + " pointing to " + source, e); } } @@ -398,8 +531,7 @@ public void delete(Path path) { try { if (Files.isSymbolicLink(path)) { Files.delete(path); - } - else { + } else { deleteRecursive(path); } } catch (IOException e) { diff --git a/cli/src/test/java/com/devonfw/tools/ide/io/FileAccessImplTest.java b/cli/src/test/java/com/devonfw/tools/ide/io/FileAccessImplTest.java new file mode 100644 index 000000000..c2f0bae8c --- /dev/null +++ b/cli/src/test/java/com/devonfw/tools/ide/io/FileAccessImplTest.java @@ -0,0 +1,472 @@ +package com.devonfw.tools.ide.io; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.NoSuchFileException; +import java.nio.file.Path; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import com.devonfw.tools.ide.context.AbstractIdeContextTest; +import com.devonfw.tools.ide.context.IdeContext; +import com.devonfw.tools.ide.context.IdeTestContextMock; + +/** + * Test of {@link FileAccessImpl}. + */ +public class FileAccessImplTest extends AbstractIdeContextTest { + + /** + * Checks if Windows junctions are used. + * + * @param context the {@link IdeContext} to get system info and file access from. + * @param dir the {@link Path} to the directory which is used as temp directory. + * @return {@code true} if Windows junctions are used, {@code false} otherwise. + */ + private boolean windowsJunctionsAreUsed(IdeContext context, Path dir) { + + if (!context.getSystemInfo().isWindows()) { + return false; + } + + Path source = dir.resolve("checkIfWindowsJunctionsAreUsed"); + Path link = dir.resolve("checkIfWindowsJunctionsAreUsedLink"); + context.getFileAccess().mkdirs(source); + try { + Files.createSymbolicLink(link, source); + return false; + } catch (IOException e) { + return true; + } + } + + /** + * Test of {@link FileAccessImpl#symlink(Path, Path, boolean)} with "relative = false". Passing absolute paths as + * source. + */ + @Test + public void testSymlinkAbsolute(@TempDir Path tempDir) { + + // relative links are checked in testRelativeLinksWorkAfterMoving + + // arrange + IdeContext context = IdeTestContextMock.get(); + FileAccess fileAccess = new FileAccessImpl(context); + Path dir = tempDir.resolve("parent"); + createDirs(fileAccess, dir); + boolean readLinks = !windowsJunctionsAreUsed(context, tempDir); + boolean relative = false; + + // act + createSymlinks(fileAccess, dir, relative); + + // assert + assertSymlinksExist(dir); + assertSymlinksWork(dir, readLinks); + } + + /** + * Test of {@link FileAccessImpl#symlink(Path, Path, boolean)} with "relative = false". Passing relative paths as + * source. + */ + @Test + public void testSymlinkAbsolutePassingRelativeSource(@TempDir Path tempDir) { + + // arrange + IdeContext context = IdeTestContextMock.get(); + FileAccess fileAccess = new FileAccessImpl(context); + Path dir = tempDir.resolve("parent"); + createDirs(fileAccess, dir); + boolean readLinks = !windowsJunctionsAreUsed(context, tempDir); + boolean relative = false; + + // act + createSymlinksByPassingRelativeSource(fileAccess, dir, relative); + + // assert + assertSymlinksExist(dir); + assertSymlinksWork(dir, readLinks); + } + + /** + * Test of {@link FileAccessImpl#symlink(Path, Path, boolean)} with "relative = true". But Windows junctions are used + * and therefore the fallback from relative to absolute paths is tested. + */ + @Test + public void testSymlinkAbsoluteAsFallback(@TempDir Path tempDir) { + + // arrange + IdeContext context = IdeTestContextMock.get(); + if (!windowsJunctionsAreUsed(context, tempDir)) { + context.info( + "Can not check the Test: testSymlinkAbsoluteAsFallback since windows junctions are not used and fallback " + + "from relative to absolute paths as link target is not used."); + return; + } + FileAccess fileAccess = new FileAccessImpl(context); + Path dir = tempDir.resolve("parent"); + createDirs(fileAccess, dir); + boolean readLinks = false; // bc windows junctions are used, which can't be read with Files.readSymbolicLink(link); + boolean relative = true; // set to true, such that the fallback to absolute paths is used since junctions are used + + // act + createSymlinks(fileAccess, dir, relative); + + // assert + assertSymlinksExist(dir); + assertSymlinksWork(dir, readLinks); + } + + /** + * Test of {@link FileAccessImpl#symlink(Path, Path, boolean)} with "relative = false". Furthermore, it is tested that + * the links are broken after moving them. + */ + @Test + public void testSymlinkAbsoluteBreakAfterMoving(@TempDir Path tempDir) throws IOException { + + // arrange + IdeContext context = IdeTestContextMock.get(); + FileAccess fileAccess = new FileAccessImpl(context); + Path dir = tempDir.resolve("parent"); + createDirs(fileAccess, dir); + boolean relative = false; + createSymlinks(fileAccess, dir, relative); + boolean readLinks = !windowsJunctionsAreUsed(context, tempDir); + + // act + Path sibling = dir.resolveSibling("parent2"); + fileAccess.move(dir, sibling); + + // assert + assertSymlinksExist(sibling); + assertSymlinksAreBroken(sibling, readLinks); + } + + /** + * Test of {@link FileAccessImpl#symlink(Path, Path, boolean)} with "relative = true". Furthermore, it is tested that + * the links still work after moving them. Passing relative paths as source. + */ + @Test + public void testSymlinkRelativeWorkAfterMovingPassingRelativeSource(@TempDir Path tempDir) { + + // arrange + IdeContext context = IdeTestContextMock.get(); + if (windowsJunctionsAreUsed(context, tempDir)) { + context.info("Can not check the Test: testRelativeLinksWorkAfterMoving since windows junctions are used."); + return; + } + FileAccess fileAccess = new FileAccessImpl(context); + Path dir = tempDir.resolve("parent"); + createDirs(fileAccess, dir); + boolean relative = true; + createSymlinksByPassingRelativeSource(fileAccess, dir, relative); + boolean readLinks = true; // junctions are not used, so links can be read with Files.readSymbolicLink(link); + + // act + Path sibling = dir.resolveSibling("parent2"); + fileAccess.move(dir, sibling); + + // assert + assertSymlinksExist(sibling); + assertSymlinksWork(sibling, readLinks); + } + + /** + * Test of {@link FileAccessImpl#symlink(Path, Path, boolean)} with "relative = true". Furthermore, it is tested that + * the links still work after moving them. + */ + @Test + public void testSymlinkRelativeWorkAfterMoving(@TempDir Path tempDir) { + + // arrange + IdeContext context = IdeTestContextMock.get(); + if (windowsJunctionsAreUsed(context, tempDir)) { + context.info("Can not check the Test: testRelativeLinksWorkAfterMoving since windows junctions are used."); + return; + } + FileAccess fileAccess = new FileAccessImpl(context); + Path dir = tempDir.resolve("parent"); + createDirs(fileAccess, dir); + boolean relative = true; + createSymlinks(fileAccess, dir, relative); + boolean readLinks = true; // junctions are not used, so links can be read with Files.readSymbolicLink(link); + + // act + Path sibling = dir.resolveSibling("parent2"); + fileAccess.move(dir, sibling); + + // assert + assertSymlinksExist(sibling); + assertSymlinksWork(sibling, readLinks); + } + + /** + * Test of {@link FileAccessImpl#symlink(Path, Path, boolean)} when Windows junctions are used and the source is a + * file. + */ + @Test + public void testSymlinkWindowsJunctionsCanNotPointToFiles(@TempDir Path tempDir) throws IOException { + + // arrange + IdeContext context = IdeTestContextMock.get(); + if (!windowsJunctionsAreUsed(context, tempDir)) { + context + .info("Can not check the Test: testWindowsJunctionsCanNotPointToFiles since windows junctions are not used."); + return; + } + Path file = tempDir.resolve("file"); + Files.createFile(file); + FileAccess fileAccess = new FileAccessImpl(context); + + // act & assert + IllegalStateException e1 = assertThrows(IllegalStateException.class, () -> { + fileAccess.symlink(file, tempDir.resolve("linkToFile")); + }); + assertThat(e1).hasMessageContaining("These junctions can only point to directories or other junctions"); + } + + /** + * Test of {@link FileAccessImpl#symlink(Path, Path, boolean)} and whether the source paths are simplified correctly + * by {@link Path#toRealPath(LinkOption...)}. + */ + @Test + public void testSymlinkShortcutPaths(@TempDir Path tempDir) { + + // arrange + IdeContext context = IdeTestContextMock.get(); + FileAccess fileAccess = new FileAccessImpl(context); + Path dir = tempDir.resolve("parent"); + createDirs(fileAccess, dir); + fileAccess.mkdirs(dir.resolve("d3")); + boolean readLinks = !windowsJunctionsAreUsed(context, tempDir); + + // act + fileAccess.symlink(dir.resolve("d3/../d1"), dir.resolve("link1"), false); + fileAccess.symlink(Path.of("d3/../d1"), dir.resolve("link2"), false); + fileAccess.symlink(dir.resolve("d3/../d1"), dir.resolve("link3"), true); + fileAccess.symlink(Path.of("d3/../d1"), dir.resolve("link4"), true); + fileAccess.delete(dir.resolve("d3")); + + // assert + assertSymlinkToRealPath(dir.resolve("link1"), dir.resolve("d1")); + assertSymlinkToRealPath(dir.resolve("link2"), dir.resolve("d1")); + assertSymlinkToRealPath(dir.resolve("link3"), dir.resolve("d1")); + assertSymlinkToRealPath(dir.resolve("link4"), dir.resolve("d1")); + if (readLinks) { + assertSymlinkRead(dir.resolve("link1"), dir.resolve("d1")); + assertSymlinkRead(dir.resolve("link2"), dir.resolve("d1")); + assertSymlinkRead(dir.resolve("link3"), dir.resolve("d1")); + assertSymlinkRead(dir.resolve("link4"), dir.resolve("d1")); + } + } + + private void createDirs(FileAccess fileAccess, Path dir) { + + fileAccess.mkdirs(dir.resolve("d1/d11/d111/d1111")); + fileAccess.mkdirs(dir.resolve("d2/d22/d222")); + } + + /** + * Creates the symlinks with passing relative paths as source. This is used by the tests of + * {@link FileAccessImpl#symlink(Path, Path, boolean)}. + * + * @param fa the {@link FileAccess} to use. + * @param dir the {@link Path} to the directory where the symlinks shall be created. + * @param relative - {@code true} if the symbolic link shall be relative, {@code false} if it shall be absolute. + */ + private void createSymlinksByPassingRelativeSource(FileAccess fa, Path dir, boolean relative) { + + fa.symlink(Path.of("."), dir.resolve("d1/d11/link_to_d1"), relative); + // test if symbolic links or junctions can be overwritten with symlink() + fa.symlink(Path.of(".."), dir.resolve("d1/d11/link_to_d1"), relative); + + fa.symlink(Path.of("."), dir.resolve("d1/d11/link_to_d11"), relative); + fa.symlink(Path.of("d111"), dir.resolve("d1/d11/link_to_d111"), relative); + fa.symlink(Path.of("d111/d1111"), dir.resolve("d1/d11/link_to_d1111"), relative); + fa.symlink(Path.of("../../d1/../d2"), dir.resolve("d1/d11/link_to_d2"), relative); + fa.symlink(Path.of("../../d2/d22"), dir.resolve("d1/d11/link_to_d22"), relative); + fa.symlink(Path.of("../../d2/d22/d222"), dir.resolve("d1/d11/link_to_d222"), relative); + + fa.symlink(Path.of("../../d1/d11/link_to_d1"), dir.resolve("d2/d22/link_to_link_to_d1"), relative); + fa.symlink(Path.of("../d1/d11/link_to_d1"), dir.resolve("d2/another_link_to_link_to_d1"), relative); + fa.symlink(Path.of("d2/another_link_to_link_to_d1"), dir.resolve("link_to_another_link_to_link_to_d1"), relative); + } + + /** + * Creates the symlinks with passing absolute paths as source. This is used by the tests of + * {@link FileAccessImpl#symlink(Path, Path, boolean)}. + * + * @param fa the {@link FileAccess} to use. + * @param dir the {@link Path} to the directory where the symlinks shall be created. + * @param relative - {@code true} if the symbolic link shall be relative, {@code false} if it shall be absolute. + */ + private void createSymlinks(FileAccess fa, Path dir, boolean relative) { + + fa.symlink(dir.resolve("d1/d11"), dir.resolve("d1/d11/link_to_d1"), relative); + // test if symbolic links or junctions can be overwritten with symlink() + fa.symlink(dir.resolve("d1"), dir.resolve("d1/d11/link_to_d1"), relative); + + fa.symlink(dir.resolve("d1/d11"), dir.resolve("d1/d11/link_to_d11"), relative); + fa.symlink(dir.resolve("d1/d11/d111"), dir.resolve("d1/d11/link_to_d111"), relative); + fa.symlink(dir.resolve("d1/d11/d111/d1111"), dir.resolve("d1/d11/link_to_d1111"), relative); + fa.symlink(dir.resolve("d1/../d2"), dir.resolve("d1/d11/link_to_d2"), relative); + fa.symlink(dir.resolve("d2/d22"), dir.resolve("d1/d11/link_to_d22"), relative); + fa.symlink(dir.resolve("d2/d22/d222"), dir.resolve("d1/d11/link_to_d222"), relative); + + fa.symlink(dir.resolve("d1/d11/link_to_d1"), dir.resolve("d2/d22/link_to_link_to_d1"), relative); + fa.symlink(dir.resolve("d1/d11/link_to_d1"), dir.resolve("d2/another_link_to_link_to_d1"), relative); + fa.symlink(dir.resolve("d2/another_link_to_link_to_d1"), dir.resolve("link_to_another_link_to_link_to_d1"), + relative); + } + + /** + * Checks if the symlinks exist. This is used by the tests of {@link FileAccessImpl#symlink(Path, Path, boolean)}. + * + * @param dir the {@link Path} to the directory where the symlinks are expected. + */ + private void assertSymlinksExist(Path dir) { + + assertThat(dir.resolve("d1/d11/link_to_d1")).existsNoFollowLinks(); + assertThat(dir.resolve("d1/d11/link_to_d11")).existsNoFollowLinks(); + assertThat(dir.resolve("d1/d11/link_to_d111")).existsNoFollowLinks(); + assertThat(dir.resolve("d1/d11/link_to_d1111")).existsNoFollowLinks(); + assertThat(dir.resolve("d1/d11/link_to_d2")).existsNoFollowLinks(); + assertThat(dir.resolve("d1/d11/link_to_d22")).existsNoFollowLinks(); + assertThat(dir.resolve("d1/d11/link_to_d222")).existsNoFollowLinks(); + assertThat(dir.resolve("d2/d22/link_to_link_to_d1")).existsNoFollowLinks(); + assertThat(dir.resolve("d2/another_link_to_link_to_d1")).existsNoFollowLinks(); + assertThat(dir.resolve("link_to_another_link_to_link_to_d1")).existsNoFollowLinks(); + } + + /** + * Checks if the symlinks are broken. This is used by the tests of + * {@link FileAccessImpl#symlink(Path, Path, boolean)}. + * + * @param dir the {@link Path} to the directory where the symlinks are expected. + * @param readLinks - {@code true} if the symbolic link shall be read with {@link Files#readSymbolicLink(Path)}, this + * does not work for Windows junctions. + */ + private void assertSymlinksAreBroken(Path dir, boolean readLinks) throws IOException { + + assertSymlinkIsBroken(dir.resolve("d1/d11/link_to_d1"), readLinks); + assertSymlinkIsBroken(dir.resolve("d1/d11/link_to_d11"), readLinks); + assertSymlinkIsBroken(dir.resolve("d1/d11/link_to_d111"), readLinks); + assertSymlinkIsBroken(dir.resolve("d1/d11/link_to_d1111"), readLinks); + assertSymlinkIsBroken(dir.resolve("d1/d11/link_to_d2"), readLinks); + assertSymlinkIsBroken(dir.resolve("d1/d11/link_to_d22"), readLinks); + assertSymlinkIsBroken(dir.resolve("d1/d11/link_to_d222"), readLinks); + assertSymlinkIsBroken(dir.resolve("d2/d22/link_to_link_to_d1"), readLinks); + assertSymlinkIsBroken(dir.resolve("d2/another_link_to_link_to_d1"), readLinks); + assertSymlinkIsBroken(dir.resolve("link_to_another_link_to_link_to_d1"), readLinks); + } + + /** + * Checks if the symlink is broken. This is used by the tests of {@link FileAccessImpl#symlink(Path, Path, boolean)}. + * + * @param link the {@link Path} to the link. + * @param readLinks - {@code true} if the symbolic link shall be read with {@link Files#readSymbolicLink(Path)}, this + * does not work for Windows junctions. + */ + private void assertSymlinkIsBroken(Path link, boolean readLinks) throws IOException { + + try { + Path realPath = link.toRealPath(); + if (Files.exists(realPath)) { + fail("The link target " + realPath + " (from toRealPath) should not exist"); + } + } catch (IOException e) { // toRealPath() throws exception for junctions + assertThat(e).isInstanceOf(NoSuchFileException.class); + } + if (readLinks) { + Path readPath = Files.readSymbolicLink(link); + if (Files.exists(readPath)) { + fail("The link target " + readPath + " (from readSymbolicLink) should not exist"); + } + } + } + + /** + * Checks if the symlinks work. This is used by the tests of {@link FileAccessImpl#symlink(Path, Path, boolean)}. + * + * @param dir the {@link Path} to the directory where the symlinks are expected. + * @param readLinks - {@code true} if the symbolic link shall be read with {@link Files#readSymbolicLink(Path)}, this + * does not work for Windows junctions. + */ + private void assertSymlinksWork(Path dir, boolean readLinks) { + + assertSymlinkToRealPath(dir.resolve("d1/d11/link_to_d1"), dir.resolve("d1")); + assertSymlinkToRealPath(dir.resolve("d1/d11/link_to_d11"), dir.resolve("d1/d11")); + assertSymlinkToRealPath(dir.resolve("d1/d11/link_to_d111"), dir.resolve("d1/d11/d111")); + assertSymlinkToRealPath(dir.resolve("d1/d11/link_to_d1111"), dir.resolve("d1/d11/d111/d1111")); + assertSymlinkToRealPath(dir.resolve("d1/d11/link_to_d2"), dir.resolve("d2")); + assertSymlinkToRealPath(dir.resolve("d1/d11/link_to_d22"), dir.resolve("d2/d22")); + assertSymlinkToRealPath(dir.resolve("d1/d11/link_to_d222"), dir.resolve("d2/d22/d222")); + assertSymlinkToRealPath(dir.resolve("d2/d22/link_to_link_to_d1"), dir.resolve("d1")); + assertSymlinkToRealPath(dir.resolve("d2/another_link_to_link_to_d1"), dir.resolve("d1")); + assertSymlinkToRealPath(dir.resolve("link_to_another_link_to_link_to_d1"), dir.resolve("d1")); + + if (readLinks) { + assertSymlinkRead(dir.resolve("d1/d11/link_to_d1"), dir.resolve("d1")); + assertSymlinkRead(dir.resolve("d1/d11/link_to_d11"), dir.resolve("d1/d11")); + assertSymlinkRead(dir.resolve("d1/d11/link_to_d111"), dir.resolve("d1/d11/d111")); + assertSymlinkRead(dir.resolve("d1/d11/link_to_d1111"), dir.resolve("d1/d11/d111/d1111")); + assertSymlinkRead(dir.resolve("d1/d11/link_to_d2"), dir.resolve("d2")); + assertSymlinkRead(dir.resolve("d1/d11/link_to_d22"), dir.resolve("d2/d22")); + assertSymlinkRead(dir.resolve("d1/d11/link_to_d222"), dir.resolve("d2/d22/d222")); + assertSymlinkRead(dir.resolve("d2/d22/link_to_link_to_d1"), dir.resolve("d1/d11/link_to_d1")); + assertSymlinkRead(dir.resolve("d2/another_link_to_link_to_d1"), dir.resolve("d1/d11/link_to_d1")); + assertSymlinkRead(dir.resolve("link_to_another_link_to_link_to_d1"), + dir.resolve("d2/another_link_to_link_to_d1")); + } + } + + /** + * Checks if the symlink works by checking {@link Path#toRealPath(LinkOption...)}} against the {@code trueTarget}. . + * This is used by the tests of {@link FileAccessImpl#symlink(Path, Path, boolean)}. + * + * @param link the {@link Path} to the link. + * @param trueTarget the {@link Path} to the true target. + */ + private void assertSymlinkToRealPath(Path link, Path trueTarget) { + + Path realPath = null; + try { + realPath = link.toRealPath(); + } catch (IOException e) { + fail("In method assertSymlinkToRealPath() could not call toRealPath() on link " + link, e); + } + assertThat(realPath).exists(); + assertThat(realPath).existsNoFollowLinks(); + assertThat(realPath).isEqualTo(trueTarget); + } + + /** + * Checks if the symlink works by checking {@link Files#readSymbolicLink(Path)} against the {@code trueTarget}. This + * is used by the tests of {@link FileAccessImpl#symlink(Path, Path, boolean)}. Only call this method if junctions are + * not used, since junctions can not be read with {@link Files#readSymbolicLink(Path)}. + * + * @param link the {@link Path} to the link. + * @param trueTarget the {@link Path} to the true target. + */ + private void assertSymlinkRead(Path link, Path trueTarget) { + + Path readPath = null; + try { + readPath = Files.readSymbolicLink(link); + } catch (IOException e) { + fail("In method assertSymlinkRead() could not call readSymbolicLink() on link " + link, e); + } + assertThat(link.resolveSibling(readPath)).existsNoFollowLinks(); + assertThat(link.resolveSibling(readPath)).exists(); + try { + assertThat(link.resolveSibling(readPath).toRealPath(LinkOption.NOFOLLOW_LINKS)).isEqualTo(trueTarget); + } catch (IOException e) { + fail("In method assertSymlinkRead() could not call toRealPath() on link.resolveSibling(readPath) for link " + link + + " and readPath " + readPath, e); + } + } +} diff --git a/cli/src/test/java/com/devonfw/tools/ide/version/VersionIdentifierTest.java b/cli/src/test/java/com/devonfw/tools/ide/version/VersionIdentifierTest.java index 9256e6e7d..20fa0ed8d 100644 --- a/cli/src/test/java/com/devonfw/tools/ide/version/VersionIdentifierTest.java +++ b/cli/src/test/java/com/devonfw/tools/ide/version/VersionIdentifierTest.java @@ -93,7 +93,7 @@ public void testIllegal() { for (String version : illegalVersions) { try { VersionIdentifier.of(version); - fail("Illegal verion '" + version + "' did not cause an exception!"); + fail("Illegal version '" + version + "' did not cause an exception!"); } catch (Exception e) { assertThat(e).isInstanceOf(IllegalArgumentException.class); assertThat(e).hasMessageContaining(version); From b1493b67c96753978100cb8cab9daaa9c005cb00 Mon Sep 17 00:00:00 2001 From: moritzLanger <44358321+moritzLanger@users.noreply.github.com> Date: Fri, 5 Jan 2024 09:06:30 +0100 Subject: [PATCH 3/5] #127: Increase commandlet test coverage for InstallCommandlet (#137) --- .../ide/commandlet/InstallCommandletTest.java | 112 ++++++++++++++++++ .../__files/java-17.0.6-linux-x64.tgz | Bin 0 -> 350 bytes .../__files/java-17.0.6-windows-x64.zip | Bin 0 -> 447 bytes .../urls/java/java/17.0.6/linux_x64.sha256 | 1 + .../_ide/urls/java/java/17.0.6/linux_x64.urls | 1 + .../_ide/urls/java/java/17.0.6/mac_x64.sha256 | 1 + .../_ide/urls/java/java/17.0.6/mac_x64.urls | 1 + .../_ide/urls/java/java/17.0.6/status.json | 20 ++++ .../urls/java/java/17.0.6/windows_x64.urls | 1 + .../java/java/17.0.6/windows_x64.urls.sha256 | 1 + 10 files changed, 138 insertions(+) create mode 100644 cli/src/test/java/com/devonfw/tools/ide/commandlet/InstallCommandletTest.java create mode 100644 cli/src/test/resources/__files/java-17.0.6-linux-x64.tgz create mode 100644 cli/src/test/resources/__files/java-17.0.6-windows-x64.zip create mode 100644 cli/src/test/resources/ide-projects/_ide/urls/java/java/17.0.6/linux_x64.sha256 create mode 100644 cli/src/test/resources/ide-projects/_ide/urls/java/java/17.0.6/linux_x64.urls create mode 100644 cli/src/test/resources/ide-projects/_ide/urls/java/java/17.0.6/mac_x64.sha256 create mode 100644 cli/src/test/resources/ide-projects/_ide/urls/java/java/17.0.6/mac_x64.urls create mode 100644 cli/src/test/resources/ide-projects/_ide/urls/java/java/17.0.6/status.json create mode 100644 cli/src/test/resources/ide-projects/_ide/urls/java/java/17.0.6/windows_x64.urls create mode 100644 cli/src/test/resources/ide-projects/_ide/urls/java/java/17.0.6/windows_x64.urls.sha256 diff --git a/cli/src/test/java/com/devonfw/tools/ide/commandlet/InstallCommandletTest.java b/cli/src/test/java/com/devonfw/tools/ide/commandlet/InstallCommandletTest.java new file mode 100644 index 000000000..6633aff01 --- /dev/null +++ b/cli/src/test/java/com/devonfw/tools/ide/commandlet/InstallCommandletTest.java @@ -0,0 +1,112 @@ +package com.devonfw.tools.ide.commandlet; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.get; +import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import com.devonfw.tools.ide.context.AbstractIdeContextTest; +import com.devonfw.tools.ide.context.IdeContext; +import com.github.tomakehurst.wiremock.WireMockServer; +import com.github.tomakehurst.wiremock.core.WireMockConfiguration; + +/** + * Integration test of {@link InstallCommandlet}. + */ + +public class InstallCommandletTest extends AbstractIdeContextTest { + + private static WireMockServer server; + + private static Path resourcePath = Paths.get("src/test/resources"); + + @BeforeAll + static void setUp() throws IOException { + + server = new WireMockServer(WireMockConfiguration.wireMockConfig().port(1111)); + server.start(); + } + + @AfterAll + static void tearDown() throws IOException { + + server.shutdownServer(); + } + + private void mockWebServer() throws IOException { + + Path windowsFilePath = resourcePath.resolve("__files").resolve("java-17.0.6-windows-x64.zip"); + String windowsLength = String.valueOf(Files.size(windowsFilePath)); + server.stubFor( + get(urlPathEqualTo("/installTest/windows")).willReturn(aResponse().withHeader("Content-Type", "application/zip") + .withHeader("Content-Length", windowsLength).withStatus(200).withBodyFile("java-17.0.6-windows-x64.zip"))); + + Path linuxFilePath = resourcePath.resolve("__files").resolve("java-17.0.6-linux-x64.tgz"); + String linuxLength = String.valueOf(Files.size(linuxFilePath)); + server.stubFor( + get(urlPathEqualTo("/installTest/linux")).willReturn(aResponse().withHeader("Content-Type", "application/tgz") + .withHeader("Content-Length", linuxLength).withStatus(200).withBodyFile("java-17.0.6-linux-x64.tgz"))); + + server.stubFor( + get(urlPathEqualTo("/installTest/macOS")).willReturn(aResponse().withHeader("Content-Type", "application/tgz") + .withHeader("Content-Length", linuxLength).withStatus(200).withBodyFile("java-17.0.6-linux-x64.tgz"))); + } + + /** + * Test of {@link InstallCommandlet} run, when Installed Version is null. + */ + @Test + public void testInstallCommandletRunWithVersion() throws IOException { + + // arrange + String path = "workspaces/foo-test/my-git-repo"; + IdeContext context = newContext("basic", path, true); + InstallCommandlet install = context.getCommandletManager().getCommandlet(InstallCommandlet.class); + install.tool.setValueAsString("java"); + mockWebServer(); + // act + install.run(); + // assert + assertTestInstall(context); + } + + /** + * Test of {@link InstallCommandlet} run, when Installed Version is set. + */ + @Test + public void testInstallCommandletRunWithVersionAndVersionIdentifier() throws IOException { + + // arrange + String path = "workspaces/foo-test/my-git-repo"; + IdeContext context = newContext("basic", path, true); + InstallCommandlet install = context.getCommandletManager().getCommandlet(InstallCommandlet.class); + install.tool.setValueAsString("java"); + install.version.setValueAsString("17.0.6"); + mockWebServer(); + + // act + install.run(); + // assert + assertTestInstall(context); + } + + private void assertTestInstall(IdeContext context) { + + assertThat(context.getSoftwarePath().resolve("java")).exists(); + assertThat(context.getSoftwarePath().resolve("java/InstallTest.txt")).hasContent("This is a test file."); + assertThat(context.getSoftwarePath().resolve("java/bin/HelloWorld.txt")).hasContent("Hello World!"); + if(context.getSystemInfo().isWindows()){ + assertThat(context.getSoftwarePath().resolve("java/bin/java.cmd")).exists(); + } else if (context.getSystemInfo().isLinux() || context.getSystemInfo().isMac()) { + assertThat(context.getSoftwarePath().resolve("java/bin/java")).exists(); + } + } +} diff --git a/cli/src/test/resources/__files/java-17.0.6-linux-x64.tgz b/cli/src/test/resources/__files/java-17.0.6-linux-x64.tgz new file mode 100644 index 0000000000000000000000000000000000000000..9a592c991a7398a6ab70ce32d2793383d56293bf GIT binary patch literal 350 zcmV-k0ipgMiwFP!000001MSwaPQx$|24K%ftjtIEq-V!*UVs5uSSrC9N}#EnL~2r0 zHlBm0#SRD2(o{VAo12#t z%`DyM=idpb(eIBmd5QB7QC6*=XCFiE3CTHX;D7lp8=2j|r0mT#y&GiiQ z%oOtT6v{L6Qu51-l|Y6;oH7UK6#4x5AH{%%fG|H0d*&6FB Date: Mon, 8 Jan 2024 16:30:15 +0100 Subject: [PATCH 4/5] #104: proper evaluation of variables (#147) --- .../AbstractEnvironmentVariables.java | 66 +++++++++++++++++-- .../ide/environment/EnvironmentVariables.java | 2 +- .../EnvironmentVariablesPropertiesFile.java | 4 +- .../commandlet/VersionSetCommandletTest.java | 11 +++- .../environment/EnvironmentVariablesTest.java | 60 +++++++++++++++++ .../ide-projects/basic/conf/ide.properties | 11 +++- .../basic/home/.ide/ide.properties | 11 +++- .../basic/settings/ide.properties | 12 +++- .../basic/workspaces/foo-test/ide.properties | 12 ++++ 9 files changed, 176 insertions(+), 13 deletions(-) create mode 100644 cli/src/test/java/com/devonfw/tools/ide/environment/EnvironmentVariablesTest.java create mode 100644 cli/src/test/resources/ide-projects/basic/workspaces/foo-test/ide.properties diff --git a/cli/src/main/java/com/devonfw/tools/ide/environment/AbstractEnvironmentVariables.java b/cli/src/main/java/com/devonfw/tools/ide/environment/AbstractEnvironmentVariables.java index dad65e308..4b8bd6551 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/environment/AbstractEnvironmentVariables.java +++ b/cli/src/main/java/com/devonfw/tools/ide/environment/AbstractEnvironmentVariables.java @@ -28,6 +28,8 @@ public abstract class AbstractEnvironmentVariables implements EnvironmentVariabl // Variable surrounded with "${" and "}" such as "${JAVA_HOME}" 1......2........ private static final Pattern VARIABLE_SYNTAX = Pattern.compile("(\\$\\{([^}]+)})"); + private static final String SELF_REFERENCING_NOT_FOUND = ""; + private static final int MAX_RECURSION = 9; private static final String VARIABLE_PREFIX = "${"; @@ -161,19 +163,44 @@ public EnvironmentVariables resolved() { @Override public String resolve(String string, Object src) { - return resolve(string, src, 0, src, string); + return resolve(string, src, 0, src, string, this); } - private String resolve(String value, Object src, int recursion, Object rootSrc, String rootValue) { + /** + * This method is called recursively. This allows you to resolve variables that are defined by other variables. + * + * @param value the {@link String} that potentially contains variables in the syntax "${«variable«}". Those will be + * resolved by this method and replaced with their {@link #get(String) value}. + * @param src the source where the {@link String} to resolve originates from. Should have a reasonable + * {@link Object#toString() string representation} that will be used in error or log messages if a variable + * could not be resolved. + * @param recursion the current recursion level. This is used to interrupt endless recursion. + * @param rootSrc the root source where the {@link String} to resolve originates from. + * @param rootValue the root value to resolve. + * @param resolvedVars this is a reference to an object of {@link EnvironmentVariablesResolved} being the lowest level + * in the {@link EnvironmentVariablesType hierarchy} of variables. In case of a self-referencing variable + * {@code x} the resolving has to continue one level higher in the {@link EnvironmentVariablesType hierarchy} + * to avoid endless recursion. The {@link EnvironmentVariablesResolved} is then used if another variable + * {@code y} must be resolved, since resolving this variable has to again start at the lowest level. For + * example: For levels {@code l1, l2} with {@code l1 < l2} and {@code x=${x} foo} and {@code y=bar} defined at + * level {@code l1} and {@code x=test ${y}} defined at level {@code l2}, {@code x} is first resolved at level + * {@code l1} and then up the {@link EnvironmentVariablesType hierarchy} at {@code l2} to avoid endless + * recursion. However, {@code y} must be resolved starting from the lowest level in the + * {@link EnvironmentVariablesType hierarchy} and therefore {@link EnvironmentVariablesResolved} is used. + * @return the given {@link String} with the variables resolved. + */ + private String resolve(String value, Object src, int recursion, Object rootSrc, String rootValue, + AbstractEnvironmentVariables resolvedVars) { if (value == null) { return null; } if (recursion > MAX_RECURSION) { - throw new IllegalStateException("Reached maximum recursion resolving " + value + " for root valiable " + rootSrc + throw new IllegalStateException("Reached maximum recursion resolving " + value + " for root variable " + rootSrc + " with value '" + rootValue + "'."); } recursion++; + Matcher matcher = VARIABLE_SYNTAX.matcher(value); if (!matcher.find()) { return value; @@ -181,16 +208,43 @@ private String resolve(String value, Object src, int recursion, Object rootSrc, StringBuilder sb = new StringBuilder(value.length() + EXTRA_CAPACITY); do { String variableName = matcher.group(2); - String variableValue = getValue(variableName); + String variableValue = resolvedVars.getValue(variableName); if (variableValue == null) { this.context.warning("Undefined variable {} in '{}={}' for root '{}={}'", variableName, src, value, rootSrc, rootValue); - } else { - String replacement = resolve(variableValue, variableName, recursion, rootSrc, rootValue); + continue; + } + EnvironmentVariables lowestFound = findVariable(variableName); + boolean isNotSelfReferencing = lowestFound == null || !lowestFound.getFlat(variableName).equals(value); + + if (isNotSelfReferencing) { + // looking for "variableName" starting from resolved upwards the hierarchy + String replacement = resolvedVars.resolve(variableValue, variableName, recursion, rootSrc, rootValue, + resolvedVars); + matcher.appendReplacement(sb, Matcher.quoteReplacement(replacement)); + } else { // is self referencing + // finding next occurrence of "variableName" up the hierarchy of EnvironmentVariablesType + EnvironmentVariables next = lowestFound.getParent(); + while (next != null) { + if (next.getFlat(variableName) != null) { + break; + } + next = next.getParent(); + } + if (next == null) { + matcher.appendReplacement(sb, Matcher.quoteReplacement(SELF_REFERENCING_NOT_FOUND)); + continue; + } + // resolving a self referencing variable one level up the hierarchy of EnvironmentVariablesType, i.e. at "next", + // to avoid endless recursion + String replacement = ((AbstractEnvironmentVariables) next).resolve(next.getFlat(variableName), variableName, + recursion, rootSrc, rootValue, resolvedVars); matcher.appendReplacement(sb, Matcher.quoteReplacement(replacement)); + } } while (matcher.find()); matcher.appendTail(sb); + String resolved = sb.toString(); return resolved; } diff --git a/cli/src/main/java/com/devonfw/tools/ide/environment/EnvironmentVariables.java b/cli/src/main/java/com/devonfw/tools/ide/environment/EnvironmentVariables.java index 5e5fc6aff..d99e67d1f 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/environment/EnvironmentVariables.java +++ b/cli/src/main/java/com/devonfw/tools/ide/environment/EnvironmentVariables.java @@ -186,7 +186,7 @@ default EnvironmentVariables findVariable(String name) { * @param source the source where the {@link String} to resolve originates from. Should have a reasonable * {@link Object#toString() string representation} that will be used in error or log messages if a variable * could not be resolved. - * @return the the given {@link String} with the variables resolved. + * @return the given {@link String} with the variables resolved. * @see com.devonfw.tools.ide.tool.ide.IdeToolCommandlet */ String resolve(String string, Object source); diff --git a/cli/src/main/java/com/devonfw/tools/ide/environment/EnvironmentVariablesPropertiesFile.java b/cli/src/main/java/com/devonfw/tools/ide/environment/EnvironmentVariablesPropertiesFile.java index a106f5f3b..963a67996 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/environment/EnvironmentVariablesPropertiesFile.java +++ b/cli/src/main/java/com/devonfw/tools/ide/environment/EnvironmentVariablesPropertiesFile.java @@ -222,9 +222,9 @@ public String set(String name, String value, boolean export) { String oldValue = this.variables.put(name, value); boolean flagChanged = export != this.exportedVariables.contains(name); if (Objects.equals(value, oldValue) && !flagChanged) { - this.context.trace("Set valiable '{}={}' caused no change in {}", name, value, this.propertiesFilePath); + this.context.trace("Set variable '{}={}' caused no change in {}", name, value, this.propertiesFilePath); } else { - this.context.debug("Set valiable '{}={}' in {}", name, value, this.propertiesFilePath); + this.context.debug("Set variable '{}={}' in {}", name, value, this.propertiesFilePath); this.modifiedVariables.add(name); if (export && (value != null)) { this.exportedVariables.add(name); diff --git a/cli/src/test/java/com/devonfw/tools/ide/commandlet/VersionSetCommandletTest.java b/cli/src/test/java/com/devonfw/tools/ide/commandlet/VersionSetCommandletTest.java index bc82d1f35..a43227b28 100644 --- a/cli/src/test/java/com/devonfw/tools/ide/commandlet/VersionSetCommandletTest.java +++ b/cli/src/test/java/com/devonfw/tools/ide/commandlet/VersionSetCommandletTest.java @@ -44,6 +44,15 @@ public void testVersionSetCommandletRun() throws IOException { IDE_TOOLS=mvn,eclipse BAR=bar-${SOME} - """); + + TEST_ARGS1=${TEST_ARGS1} settings1 + TEST_ARGS4=${TEST_ARGS4} settings4 + TEST_ARGS5=${TEST_ARGS5} settings5 + TEST_ARGS6=${TEST_ARGS6} settings6 + TEST_ARGS7=${TEST_ARGS7} settings7 + TEST_ARGS8=settings8 + TEST_ARGS9=settings9 + TEST_ARGSb=${TEST_ARGS10} settingsb ${TEST_ARGSa} ${TEST_ARGSb} + TEST_ARGSc=${TEST_ARGSc} settingsc"""); } } diff --git a/cli/src/test/java/com/devonfw/tools/ide/environment/EnvironmentVariablesTest.java b/cli/src/test/java/com/devonfw/tools/ide/environment/EnvironmentVariablesTest.java new file mode 100644 index 000000000..7f001c490 --- /dev/null +++ b/cli/src/test/java/com/devonfw/tools/ide/environment/EnvironmentVariablesTest.java @@ -0,0 +1,60 @@ +package com.devonfw.tools.ide.environment; + +import com.devonfw.tools.ide.context.AbstractIdeContextTest; +import com.devonfw.tools.ide.context.IdeTestContext; +import org.junit.jupiter.api.Test; + +/** + * Test of {@link EnvironmentVariables}. + */ +public class EnvironmentVariablesTest extends AbstractIdeContextTest { + + /** + * Test of {@link EnvironmentVariables#resolve(String, Object)} with self referencing variables. + */ + @Test + public void testProperEvaluationOfVariables() { + + // arrange + String path = "workspaces/foo-test/my-git-repo"; + IdeTestContext context = newContext(PROJECT_BASIC, path, false); + EnvironmentVariables variables = context.getVariables(); + + // act + String TEST_ARGS1 = variables.get("TEST_ARGS1"); + String TEST_ARGS2 = variables.get("TEST_ARGS2"); + String TEST_ARGS3 = variables.get("TEST_ARGS3"); + String TEST_ARGS4 = variables.get("TEST_ARGS4"); + String TEST_ARGS5 = variables.get("TEST_ARGS5"); + String TEST_ARGS6 = variables.get("TEST_ARGS6"); + String TEST_ARGS7 = variables.get("TEST_ARGS7"); + String TEST_ARGS8 = variables.get("TEST_ARGS8"); + String TEST_ARGS9 = variables.get("TEST_ARGS9"); + String TEST_ARGS10 = variables.get("TEST_ARGS10"); + // some more advanced cases + String TEST_ARGSa = variables.get("TEST_ARGSa"); + String TEST_ARGSb = variables.get("TEST_ARGSb"); + String TEST_ARGSc = variables.get("TEST_ARGSc"); + String TEST_ARGSd = variables.get("TEST_ARGSd"); + + // assert + assertThat(TEST_ARGS1).isEqualTo(" user1 settings1 workspace1 conf1"); + assertThat(TEST_ARGS2).isEqualTo(" user2 conf2"); + assertThat(TEST_ARGS3).isEqualTo(" user3 workspace3"); + assertThat(TEST_ARGS4).isEqualTo(" settings4"); + assertThat(TEST_ARGS5).isEqualTo(" settings5 conf5"); + assertThat(TEST_ARGS6).isEqualTo(" settings6 workspace6 conf6"); + + assertThat(TEST_ARGS7).isEqualTo("user7 settings7 workspace7 conf7"); + assertThat(TEST_ARGS8).isEqualTo("settings8 workspace8 conf8"); + assertThat(TEST_ARGS9).isEqualTo("settings9 workspace9"); + assertThat(TEST_ARGS10).isEqualTo("user10 workspace10"); + + assertThat(TEST_ARGSa).isEqualTo(" user1 settings1 workspace1 conf1 user3 workspace3 confa"); + assertThat(TEST_ARGSb) + .isEqualTo("user10 workspace10 settingsb user1 settings1 workspace1 conf1 user3 workspace3 confa userb"); + + assertThat(TEST_ARGSc).isEqualTo(" user1 settings1 workspace1 conf1 userc settingsc confc"); + assertThat(TEST_ARGSd).isEqualTo(" user1 settings1 workspace1 conf1 userd workspaced"); + } +} diff --git a/cli/src/test/resources/ide-projects/basic/conf/ide.properties b/cli/src/test/resources/ide-projects/basic/conf/ide.properties index 7b2a83438..224a2cf25 100644 --- a/cli/src/test/resources/ide-projects/basic/conf/ide.properties +++ b/cli/src/test/resources/ide-projects/basic/conf/ide.properties @@ -4,4 +4,13 @@ M2_REPO=~/.m2/repository -SOME=some-${UNDEFINED} \ No newline at end of file +SOME=some-${UNDEFINED} + +TEST_ARGS1=${TEST_ARGS1} conf1 +TEST_ARGS2=${TEST_ARGS2} conf2 +TEST_ARGS5=${TEST_ARGS5} conf5 +TEST_ARGS6=${TEST_ARGS6} conf6 +TEST_ARGS7=${TEST_ARGS7} conf7 +TEST_ARGS8=${TEST_ARGS8} conf8 +TEST_ARGSa=${TEST_ARGS1} ${TEST_ARGS3} confa +TEST_ARGSc=${TEST_ARGSc} confc \ No newline at end of file diff --git a/cli/src/test/resources/ide-projects/basic/home/.ide/ide.properties b/cli/src/test/resources/ide-projects/basic/home/.ide/ide.properties index a61d5066f..ec0c3e7f6 100644 --- a/cli/src/test/resources/ide-projects/basic/home/.ide/ide.properties +++ b/cli/src/test/resources/ide-projects/basic/home/.ide/ide.properties @@ -3,4 +3,13 @@ #******************************************************************************** DOCKER_EDITION=docker -FOO=foo-${BAR} \ No newline at end of file +FOO=foo-${BAR} + +TEST_ARGS1=${TEST_ARGS1} user1 +TEST_ARGS2=${TEST_ARGS2} user2 +TEST_ARGS3=${TEST_ARGS3} user3 +TEST_ARGS7=user7 +TEST_ARGS10=user10 +TEST_ARGSb=userb +TEST_ARGSc=${TEST_ARGS1} userc +TEST_ARGSd=${TEST_ARGS1} userd \ No newline at end of file diff --git a/cli/src/test/resources/ide-projects/basic/settings/ide.properties b/cli/src/test/resources/ide-projects/basic/settings/ide.properties index bfa447f6a..c80f1e604 100644 --- a/cli/src/test/resources/ide-projects/basic/settings/ide.properties +++ b/cli/src/test/resources/ide-projects/basic/settings/ide.properties @@ -9,4 +9,14 @@ INTELLIJ_EDITION=ultimate IDE_TOOLS=mvn,eclipse -BAR=bar-${SOME} \ No newline at end of file +BAR=bar-${SOME} + +TEST_ARGS1=${TEST_ARGS1} settings1 +TEST_ARGS4=${TEST_ARGS4} settings4 +TEST_ARGS5=${TEST_ARGS5} settings5 +TEST_ARGS6=${TEST_ARGS6} settings6 +TEST_ARGS7=${TEST_ARGS7} settings7 +TEST_ARGS8=settings8 +TEST_ARGS9=settings9 +TEST_ARGSb=${TEST_ARGS10} settingsb ${TEST_ARGSa} ${TEST_ARGSb} +TEST_ARGSc=${TEST_ARGSc} settingsc \ No newline at end of file diff --git a/cli/src/test/resources/ide-projects/basic/workspaces/foo-test/ide.properties b/cli/src/test/resources/ide-projects/basic/workspaces/foo-test/ide.properties new file mode 100644 index 000000000..fefd6bede --- /dev/null +++ b/cli/src/test/resources/ide-projects/basic/workspaces/foo-test/ide.properties @@ -0,0 +1,12 @@ +#******************************************************************************** +# Type of {@link EnvironmentVariables} from the +# {@link com.devonfw.tools.ide.context.IdeContext#getWorkspacePath() workspace directory}. +#******************************************************************************** +TEST_ARGS1=${TEST_ARGS1} workspace1 +TEST_ARGS3=${TEST_ARGS3} workspace3 +TEST_ARGS6=${TEST_ARGS6} workspace6 +TEST_ARGS7=${TEST_ARGS7} workspace7 +TEST_ARGS8=${TEST_ARGS8} workspace8 +TEST_ARGS9=${TEST_ARGS9} workspace9 +TEST_ARGS10=${TEST_ARGS10} workspace10 +TEST_ARGSd=${TEST_ARGSd} workspaced \ No newline at end of file From 0e84aff43e501ff2ceba7289a7cc494fc7afe3a4 Mon Sep 17 00:00:00 2001 From: Mattes Mrzik Date: Mon, 8 Jan 2024 16:45:39 +0100 Subject: [PATCH 5/5] #158: VersionRange with open boundaries (#159) --- .../tools/ide/version/BoundaryType.java | 19 ++ .../tools/ide/version/VersionObject.java | 7 +- .../tools/ide/version/VersionRange.java | 181 +++++++++++++++++- .../tools/ide/version/VersionRangeTest.java | 152 +++++++++++++++ 4 files changed, 348 insertions(+), 11 deletions(-) create mode 100644 cli/src/main/java/com/devonfw/tools/ide/version/BoundaryType.java create mode 100644 cli/src/test/java/com/devonfw/tools/ide/version/VersionRangeTest.java diff --git a/cli/src/main/java/com/devonfw/tools/ide/version/BoundaryType.java b/cli/src/main/java/com/devonfw/tools/ide/version/BoundaryType.java new file mode 100644 index 000000000..c8abed59a --- /dev/null +++ b/cli/src/main/java/com/devonfw/tools/ide/version/BoundaryType.java @@ -0,0 +1,19 @@ +package com.devonfw.tools.ide.version; + +/** + * Enum representing the type of interval regarding its boundaries. + */ +public enum BoundaryType { + + /** Closed interval - includes the specified values at the boundaries. */ + CLOSED, + + /** Open interval - excludes the specified values at the boundaries. */ + OPEN, + + /** Left open interval - excludes the lower bound but includes the upper bound. */ + LEFT_OPEN, + + /** Right open interval - includes the lower bound but excludes the upper bound. */ + RIGHT_OPEN +} diff --git a/cli/src/main/java/com/devonfw/tools/ide/version/VersionObject.java b/cli/src/main/java/com/devonfw/tools/ide/version/VersionObject.java index 5b4b4c871..c72e9d9f8 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/version/VersionObject.java +++ b/cli/src/main/java/com/devonfw/tools/ide/version/VersionObject.java @@ -3,12 +3,11 @@ /** * Abstract base interface for a version object such as {@link VersionIdentifier} and {@link VersionSegment}. * - * * {@link Comparable} for versions with an extended contract. If two versions are not strictly comparable (e.g. * "1.apple" and "1.banana") we fall back to some heuristics (e.g. lexicographical comparison for - * {@link VersionSegment#getLettersString() letters} that we do not understand (e.g. "apple" < "banana"). Therefore you can - * use {@link #compareVersion(Object)} to get a {@link VersionComparisonResult} that contains the additional information - * as {@link VersionComparisonResult#isUnsafe() unsafe} flag. + * {@link VersionSegment#getLettersString() letters} that we do not understand (e.g. "apple" < "banana"). Therefore, you + * can use {@link #compareVersion(Object)} to get a {@link VersionComparisonResult} that contains the additional + * information as {@link VersionComparisonResult#isUnsafe() unsafe} flag. * * @param type of the object to compare (this class itself). */ diff --git a/cli/src/main/java/com/devonfw/tools/ide/version/VersionRange.java b/cli/src/main/java/com/devonfw/tools/ide/version/VersionRange.java index d09021a2b..df27e3612 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/version/VersionRange.java +++ b/cli/src/main/java/com/devonfw/tools/ide/version/VersionRange.java @@ -1,7 +1,12 @@ package com.devonfw.tools.ide.version; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; + /** - * Container for a range of versions. + * Container for a range of versions. The lower and upper bounds can be exclusive or inclusive. If a bound is null, it + * means that this direction is unbounded. The boolean defining whether this bound is inclusive or exclusive is ignored + * in this case. */ public final class VersionRange implements Comparable { @@ -9,6 +14,45 @@ public final class VersionRange implements Comparable { private final VersionIdentifier max; + private final boolean leftIsExclusive; + + private final boolean rightIsExclusive; + + private static final String VERSION_SEPARATOR = ">"; + + private static final String START_EXCLUDING_PREFIX = "("; + + private static final String START_INCLUDING_PREFIX = "["; + + private static final String END_EXCLUDING_SUFFIX = ")"; + + private static final String END_INCLUDING_SUFFIX = "]"; + + public static String getVersionSeparator() { + + return VERSION_SEPARATOR; + } + + public static String getStartExcludingPrefix() { + + return START_EXCLUDING_PREFIX; + } + + public static String getStartIncludingPrefix() { + + return START_INCLUDING_PREFIX; + } + + public static String getEndExcludingSuffix() { + + return END_EXCLUDING_SUFFIX; + } + + public static String getEndIncludingSuffix() { + + return END_INCLUDING_SUFFIX; + } + /** * The constructor. * @@ -20,6 +64,42 @@ public VersionRange(VersionIdentifier min, VersionIdentifier max) { super(); this.min = min; this.max = max; + this.leftIsExclusive = false; + this.rightIsExclusive = false; + } + + /** + * The constructor. + * + * @param min the {@link #getMin() minimum}. + * @param max the {@link #getMax() maximum}. + * @param boundaryType the {@link BoundaryType} defining whether the boundaries of the range are inclusive or + * exclusive. + */ + public VersionRange(VersionIdentifier min, VersionIdentifier max, BoundaryType boundaryType) { + + super(); + this.min = min; + this.max = max; + this.leftIsExclusive = BoundaryType.LEFT_OPEN.equals(boundaryType) || BoundaryType.OPEN.equals(boundaryType); + this.rightIsExclusive = BoundaryType.RIGHT_OPEN.equals(boundaryType) || BoundaryType.OPEN.equals(boundaryType); + } + + /** + * The constructor. + * + * @param min the {@link #getMin() minimum}. + * @param max the {@link #getMax() maximum}. + * @param leftIsExclusive - {@code true} if the {@link #getMin() minimum} is exclusive, {@code false} otherwise. + * @param rightIsExclusive - {@code true} if the {@link #getMax() maximum} is exclusive, {@code false} otherwise. + */ + public VersionRange(VersionIdentifier min, VersionIdentifier max, boolean leftIsExclusive, boolean rightIsExclusive) { + + super(); + this.min = min; + this.max = max; + this.leftIsExclusive = leftIsExclusive; + this.rightIsExclusive = rightIsExclusive; } /** @@ -38,6 +118,38 @@ public VersionIdentifier getMax() { return this.max; } + /** + * @return {@code true} if the {@link #getMin() minimum} is exclusive, {@code false} otherwise. + */ + public boolean isLeftExclusive() { + + return this.leftIsExclusive; + } + + /** + * @return {@code true} if the {@link #getMax() maximum} is exclusive, {@code false} otherwise. + */ + public boolean isRightExclusive() { + + return this.rightIsExclusive; + } + + /** + * @return the {@link BoundaryType} defining whether the boundaries of the range are inclusive or exclusive. + */ + public BoundaryType getBoundaryType() { + + if (this.leftIsExclusive && this.rightIsExclusive) { + return BoundaryType.OPEN; + } else if (this.leftIsExclusive) { + return BoundaryType.LEFT_OPEN; + } else if (this.rightIsExclusive) { + return BoundaryType.RIGHT_OPEN; + } else { + return BoundaryType.CLOSED; + } + } + /** * @param version the {@link VersionIdentifier} to check. * @return {@code true} if the given {@link VersionIdentifier} is contained in this {@link VersionRange}, @@ -46,12 +158,18 @@ public VersionIdentifier getMax() { public boolean contains(VersionIdentifier version) { if (this.min != null) { - if (version.isLess(this.min)) { + VersionComparisonResult compareMin = version.compareVersion(this.min); + if (compareMin.isLess()) { + return false; + } else if (compareMin.isEqual() && this.leftIsExclusive) { return false; } } if (this.max != null) { - if (version.isGreater(this.max)) { + VersionComparisonResult compareMax = version.compareVersion(this.max); + if (compareMax.isGreater()) { + return false; + } else if (compareMax.isEqual() && this.rightIsExclusive) { return false; } } @@ -69,20 +187,48 @@ public int compareTo(VersionRange o) { } return -1; } - return this.min.compareTo(o.min); + int compareMins = this.min.compareTo(o.min); + if (compareMins == 0) { + return this.leftIsExclusive == o.leftIsExclusive ? 0 : this.leftIsExclusive ? 1 : -1; + } else { + return compareMins; + } } @Override + public boolean equals(Object obj) { + + if (this == obj) { + return true; + } else if ((obj == null) || (getClass() != obj.getClass())) { + return false; + } + VersionRange o = (VersionRange) obj; + if (this.min == null && this.max == null) { + return o.min == null && o.max == null; + } else if (this.min == null) { + return o.min == null && this.max.equals(o.max) && this.rightIsExclusive == o.rightIsExclusive; + } else if (this.max == null) { + return this.min.equals(o.min) && o.max == null && this.leftIsExclusive == o.leftIsExclusive; + } + return this.min.equals(o.min) && this.leftIsExclusive == o.leftIsExclusive && this.max.equals(o.max) + && this.rightIsExclusive == o.rightIsExclusive; + } + + @Override + @JsonValue public String toString() { StringBuilder sb = new StringBuilder(); + sb.append(this.leftIsExclusive ? START_EXCLUDING_PREFIX : START_INCLUDING_PREFIX); if (this.min != null) { sb.append(this.min); } - sb.append('>'); + sb.append(VERSION_SEPARATOR); if (this.max != null) { sb.append(this.max); } + sb.append(this.rightIsExclusive ? END_EXCLUDING_SUFFIX : END_INCLUDING_SUFFIX); return sb.toString(); } @@ -90,12 +236,18 @@ public String toString() { * @param value the {@link #toString() string representation} of a {@link VersionRange} to parse. * @return the parsed {@link VersionRange}. */ + @JsonCreator public static VersionRange of(String value) { - int index = value.indexOf('>'); + boolean leftIsExclusive = value.startsWith(START_EXCLUDING_PREFIX); + boolean rightIsExclusive = value.endsWith(END_EXCLUDING_SUFFIX); + value = removeAffixes(value); + + int index = value.indexOf(VERSION_SEPARATOR); if (index == -1) { return null; // log warning? } + VersionIdentifier min = null; if (index > 0) { min = VersionIdentifier.of(value.substring(0, index)); @@ -105,7 +257,22 @@ public static VersionRange of(String value) { if (!maxString.isEmpty()) { max = VersionIdentifier.of(maxString); } - return new VersionRange(min, max); + return new VersionRange(min, max, leftIsExclusive, rightIsExclusive); + } + + private static String removeAffixes(String value) { + + if (value.startsWith(START_EXCLUDING_PREFIX)) { + value = value.substring(START_EXCLUDING_PREFIX.length()); + } else if (value.startsWith(START_INCLUDING_PREFIX)) { + value = value.substring(START_INCLUDING_PREFIX.length()); + } + if (value.endsWith(END_EXCLUDING_SUFFIX)) { + value = value.substring(0, value.length() - END_EXCLUDING_SUFFIX.length()); + } else if (value.endsWith(END_INCLUDING_SUFFIX)) { + value = value.substring(0, value.length() - END_EXCLUDING_SUFFIX.length()); + } + return value; } } diff --git a/cli/src/test/java/com/devonfw/tools/ide/version/VersionRangeTest.java b/cli/src/test/java/com/devonfw/tools/ide/version/VersionRangeTest.java new file mode 100644 index 000000000..0a305f8a6 --- /dev/null +++ b/cli/src/test/java/com/devonfw/tools/ide/version/VersionRangeTest.java @@ -0,0 +1,152 @@ +package com.devonfw.tools.ide.version; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; + +/** + * Test of {@link VersionRange}. + */ +public class VersionRangeTest extends Assertions { + + /** + * Test of {@link VersionRange#of(String)}. + */ + @Test + public void testOf() { + + // arrange + String v1String = "1.2>3"; + String v2String = "1>)"; + String v3String = "(1.2>3.4]"; + + // act + VersionRange v1 = VersionRange.of(v1String); + VersionRange v2 = VersionRange.of(v2String); + VersionRange v3 = VersionRange.of(v3String); + + // assert + // v1 + assertThat(v1.getMin()).isEqualTo(VersionIdentifier.of("1.2")); + assertThat(v1.getMax()).isEqualTo(VersionIdentifier.of("3")); + assertThat(v1.isLeftExclusive()).isFalse(); + assertThat(v1.isRightExclusive()).isFalse(); + // v2 + assertThat(v2.getMin()).isEqualTo(VersionIdentifier.of("1")); + assertThat(v2.getMax()).isEqualTo(null); + assertThat(v2.isLeftExclusive()).isFalse(); + assertThat(v2.isRightExclusive()).isTrue(); + // v3 + assertThat(v3.getMin()).isEqualTo(VersionIdentifier.of("1.2")); + assertThat(v3.getMax()).isEqualTo(VersionIdentifier.of("3.4")); + assertThat(v3.isLeftExclusive()).isTrue(); + assertThat(v3.isRightExclusive()).isFalse(); + } + + /** + * Test of {@link VersionRange#toString()}. + */ + @Test + public void testToString() { + + assertThat(VersionRange.of("1.2>3").toString()).isEqualTo("[1.2>3]"); + assertThat(VersionRange.of("1>)").toString()).isEqualTo("[1>)"); + assertThat(VersionRange.of("(1.2>3.4]").toString()).isEqualTo("(1.2>3.4]"); + } + + /** + * Test of {@link VersionRange#equals(Object)}. + */ + @Test + public void testEquals() { + + // assert + // equals + assertThat(VersionRange.of("1.2>")).isEqualTo(VersionRange.of("1.2>")); + assertThat(VersionRange.of("(1.2>")).isEqualTo(VersionRange.of("(1.2>)")); + assertThat(VersionRange.of("1.2>3")).isEqualTo(VersionRange.of("1.2>3")); + assertThat(VersionRange.of("[1.2>3")).isEqualTo(VersionRange.of("1.2>3]")); + assertThat(VersionRange.of(">3)")).isEqualTo(VersionRange.of(">3)")); + assertThat(VersionRange.of(">")).isEqualTo(VersionRange.of(">")); + assertThat(VersionRange.of("[>)")).isEqualTo(VersionRange.of("(>]")); + assertThat(VersionRange.of("8u302b08>11.0.14_9")).isEqualTo(VersionRange.of("8u302b08>11.0.14_9")); + // not equals + assertThat(VersionRange.of("1>")).isNotEqualTo(null); + assertThat(VersionRange.of("1.2>")).isNotEqualTo(VersionRange.of("1>")); + assertThat(VersionRange.of("1.2>3")).isNotEqualTo(VersionRange.of("1.2>")); + assertThat(VersionRange.of("(1.2>3")).isNotEqualTo(VersionRange.of("1.2.3>")); + assertThat(VersionRange.of("1.2>3")).isNotEqualTo(VersionRange.of(">3")); + assertThat(VersionRange.of("[1.2>")).isNotEqualTo(VersionRange.of("[1.2>3")); + assertThat(VersionRange.of(">3")).isNotEqualTo(VersionRange.of("1.2>3")); + assertThat(VersionRange.of(">3")).isNotEqualTo(VersionRange.of(">")); + assertThat(VersionRange.of(">")).isNotEqualTo(VersionRange.of(">3")); + assertThat(VersionRange.of("8u302b08>11.0.14_9")).isNotEqualTo(VersionRange.of("(8u302b08>11.0.14_9)")); + assertThat(VersionRange.of("8u302b08>11.0.14_9")).isNotEqualTo(VersionRange.of("8u302b08>11.0.15_9")); + assertThat(VersionRange.of("8u302b08>11.0.14_9")).isNotEqualTo(VersionRange.of("8u302b08>11.0.14_0")); + } + + /** + * Test of {@link VersionRange#contains(VersionIdentifier)} and testing if a {@link VersionIdentifier version} is + * contained in the {@link VersionRange}. + */ + @Test + public void testContains() { + + // assert + assertThat(VersionRange.of("1.2>3.4").contains(VersionIdentifier.of("1.2"))).isTrue(); + assertThat(VersionRange.of("1.2>3.4").contains(VersionIdentifier.of("2"))).isTrue(); + assertThat(VersionRange.of("1.2>3.4").contains(VersionIdentifier.of("3.4"))).isTrue(); + + assertThat(VersionRange.of("(1.2>3.4)").contains(VersionIdentifier.of("1.2.1"))).isTrue(); + assertThat(VersionRange.of("(1.2>3.4)").contains(VersionIdentifier.of("2"))).isTrue(); + assertThat(VersionRange.of("(1.2>3.4)").contains(VersionIdentifier.of("3.3.9"))).isTrue(); + } + + /** + * Test of {@link VersionRange#contains(VersionIdentifier)} and testing if a {@link VersionIdentifier version} is not + * contained in the {@link VersionRange}. + */ + @Test + public void testNotContains() { + + // assert + assertThat(VersionRange.of("1.2>3.4").contains(VersionIdentifier.of("1.1"))).isFalse(); + assertThat(VersionRange.of("1.2>3.4").contains(VersionIdentifier.of("3.4.1"))).isFalse(); + + assertThat(VersionRange.of("(1.2>3.4)").contains(VersionIdentifier.of("1.2"))).isFalse(); + assertThat(VersionRange.of("(1.2>3.4)").contains(VersionIdentifier.of("3.4"))).isFalse(); + } + + /** + * Test of {@link VersionRange#compareTo(VersionRange)} and testing if versions are compared to be the same. + */ + @Test + public void testCompareToIsSame() { + + // assert + assertThat(VersionRange.of("1.2>3").compareTo(VersionRange.of("1.2>3"))).isEqualTo(0); + assertThat(VersionRange.of("(1.2>3").compareTo(VersionRange.of("(1.2>3"))).isEqualTo(0); + assertThat(VersionRange.of("[1.2>3]").compareTo(VersionRange.of("[1.2>4)"))).isEqualTo(0); + } + + /** + * Test of {@link VersionRange#compareTo(VersionRange)} and testing if first version is smaller than second. + */ + @Test + public void testCompareToIsSmaller() { + + // assert + assertThat(VersionRange.of("1.1.2>3").compareTo(VersionRange.of("1.2>3"))).isEqualTo(-1); + assertThat(VersionRange.of("[1.2>3").compareTo(VersionRange.of("(1.2>4"))).isEqualTo(-1); + } + + /** + * Test of {@link VersionRange#compareTo(VersionRange)} and testing if first version is larger than second. + */ + @Test + public void testCompareToIsLarger() { + + // assert + assertThat(VersionRange.of("1.2.1>3").compareTo(VersionRange.of("1.2>3"))).isEqualTo(1); + assertThat(VersionRange.of("(1.2>3").compareTo(VersionRange.of("1.2>4"))).isEqualTo(1); + } +}