diff --git a/.github/workflows/build-pr.yml b/.github/workflows/build-pr.yml index 25b7560d8..451b0fbcf 100644 --- a/.github/workflows/build-pr.yml +++ b/.github/workflows/build-pr.yml @@ -14,3 +14,5 @@ jobs: java-version: '17' - name: Build project with Maven run: mvn -B -ntp -Dstyle.color=always install + - name: Coveralls GitHub Action + uses: coverallsapp/github-action@v2.2.3 \ No newline at end of file diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3c98a4623..da7684b4f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -23,3 +23,5 @@ jobs: SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }} SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} run: mvn --settings .mvn/settings.xml -DskipTests=true -Darchetype.test.skip=true -Dmaven.install.skip=true -Dstyle.color=always -B -ntp deploy + - name: Coveralls GitHub Action + uses: coverallsapp/github-action@v2.2.3 \ No newline at end of file diff --git a/README.adoc b/README.adoc index e64c75c8c..de1ef0d62 100644 --- a/README.adoc +++ b/README.adoc @@ -10,6 +10,7 @@ image:https://img.shields.io/github/license/devonfw/IDEasy.svg?label=License["Ap image:https://img.shields.io/maven-central/v/com.devonfw.tools.ide/ide-cli.svg?label=Maven%20Central["Maven Central",link=https://search.maven.org/search?q=g:com.devonfw.tools.ide] image:https://github.com/devonfw/IDEasy/actions/workflows/build.yml/badge.svg["Build Status",link="https://github.com/devonfw/IDEasy/actions/workflows/build.yml"] image:https://github.com/devonfw/IDEasy/actions/workflows/update-urls.yml/badge.svg["Update URLS Status",link="https://github.com/devonfw/IDEasy/actions/workflows/update-urls.yml"] +image:https://coveralls.io/repos/github/devonfw/IDEasy/badge.svg?branch=main["Coverage Status",link="https://coveralls.io/github/devonfw/IDEasy?branch=main"] toc::[] diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/RepositoryCommandlet.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/RepositoryCommandlet.java index 4587b2161..92c95cf25 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/RepositoryCommandlet.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/RepositoryCommandlet.java @@ -17,7 +17,7 @@ import static com.devonfw.tools.ide.commandlet.RepositoryConfig.loadProperties; /** - * {@link Commandlet} to setup a repository + * {@link Commandlet} to setup one or multiple GIT repositories for development. */ public class RepositoryCommandlet extends Commandlet { @@ -46,8 +46,6 @@ public String getName() { @Override public void run() { - Path repositoriesPath = this.context.getSettingsPath().resolve(context.FOLDER_REPOSITORIES); - Path legacyRepositoriesPath = this.context.getSettingsPath().resolve(context.FOLDER_LEGACY_REPOSITORIES); Path repositoryFile = repository.getValueAsPath(context); if (repositoryFile != null) { @@ -55,6 +53,8 @@ public void run() { doImportRepository(repositoryFile, true); } else { // If no specific repository is provided, check for repositories folder + Path repositoriesPath = this.context.getSettingsPath().resolve(IdeContext.FOLDER_REPOSITORIES); + Path legacyRepositoriesPath = this.context.getSettingsPath().resolve(IdeContext.FOLDER_LEGACY_REPOSITORIES); Path repositories; if (Files.exists(repositoriesPath)) { repositories = repositoriesPath; @@ -99,7 +99,6 @@ private void doImportRepository(Path repositoryFile, boolean forceMode) { } this.context.debug(repositoryConfig.toString()); - this.context.debug("Pull or clone git repository {} ...", repository); String workspace = repositoryConfig.workspace() != null ? repositoryConfig.workspace() : "main"; Path workspacePath = this.context.getIdeHome().resolve("workspaces").resolve(workspace); @@ -127,10 +126,5 @@ private void doImportRepository(Path repositoryFile, boolean forceMode) { this.context.info("Build command not set. Skipping build for repository."); } - if (!Files.exists(repositoryPath.resolve(".project"))) { - for (String ideCommandlet : repositoryConfig.imports()) { - //TODO: import repository to ideCommandlet - } - } } } diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/RepositoryConfig.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/RepositoryConfig.java index 952fde680..89dea973e 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/RepositoryConfig.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/RepositoryConfig.java @@ -45,7 +45,7 @@ public static RepositoryConfig loadProperties(Path filePath) { return new RepositoryConfig(properties.getProperty("path"), properties.getProperty("workingsets"), properties.getProperty("workspace"), properties.getProperty("git_url"), properties.getProperty("git_branch"), properties.getProperty(("build_path")), properties.getProperty("build_cmd"), importsSet, - Boolean.parseBoolean(properties.getProperty("active"))); + Boolean.parseBoolean(properties.getProperty("active").trim())); } private static Set getImports(Properties properties) { diff --git a/cli/src/main/java/com/devonfw/tools/ide/context/AbstractIdeContext.java b/cli/src/main/java/com/devonfw/tools/ide/context/AbstractIdeContext.java index 268a8b241..12da1fec8 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/context/AbstractIdeContext.java +++ b/cli/src/main/java/com/devonfw/tools/ide/context/AbstractIdeContext.java @@ -601,6 +601,7 @@ public void gitPullOrClone(Path target, String gitRepoUrl) { if (!gitRepoUrl.startsWith("http")) { throw new IllegalArgumentException("Invalid git URL '" + gitRepoUrl + "'!"); } + debug("Pull or clone git repository {} ...", gitRepoUrl); ProcessContext pc = newProcess().directory(target).executable("git"); if (Files.isDirectory(target.resolve(".git"))) { ProcessResult result = pc.addArg("remote").run(true); 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/main/java/com/devonfw/tools/ide/io/FileAccess.java b/cli/src/main/java/com/devonfw/tools/ide/io/FileAccess.java index dbf787c8f..8603539dc 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 @@ -60,10 +60,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. @@ -140,4 +158,13 @@ default void copy(Path source, Path target) { */ List getChildrenInDir(Path dir, Predicate filter); + /** + * Finds the existing file with the specified name in the given list of directories. + * + * @param fileName The name of the file to find. + * @param searchDirs The list of directories to search for the file. + * @return The {@code Path} of the existing file, or {@code null} if the file is not found. + */ + Path findExistingFile(String fileName, List searchDirs); + } 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 930685813..ce076f389 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) { @@ -477,4 +609,20 @@ public List getChildrenInDir(Path dir, Predicate filter) { return files; } + @Override + public Path findExistingFile(String fileName, List searchDirs) { + + for (Path dir : searchDirs) { + Path filePath = dir.resolve(fileName); + try { + if (Files.exists(filePath)) { + return filePath; + } + } catch (SecurityException e) { + throw new IllegalStateException("SecurityException while checking file existence."); + } + } + return null; + } + } diff --git a/cli/src/main/java/com/devonfw/tools/ide/property/RepositoryProperty.java b/cli/src/main/java/com/devonfw/tools/ide/property/RepositoryProperty.java index 60be91c18..793a077e3 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/property/RepositoryProperty.java +++ b/cli/src/main/java/com/devonfw/tools/ide/property/RepositoryProperty.java @@ -4,6 +4,7 @@ import java.nio.file.Files; import java.nio.file.Path; +import java.util.Arrays; import java.util.function.Consumer; public class RepositoryProperty extends Property { @@ -47,26 +48,20 @@ public String parse(String valueAsString) { public Path getValueAsPath(IdeContext context) { - Path repositoriesPath = context.getSettingsPath().resolve(context.FOLDER_REPOSITORIES); - Path legacyRepositoriesPath = context.getSettingsPath().resolve(context.FOLDER_LEGACY_REPOSITORIES); - - Path repositoryFile; - if (super.getValue() != null) { - repositoryFile = Path.of(super.getValue()); - } else { + String value = getValue(); + if (value == null) { return null; } - + + Path repositoryFile = Path.of(value); if (!Files.exists(repositoryFile)) { - repositoryFile = repositoriesPath.resolve(repositoryFile.getFileName().toString() + ".properties"); + Path repositoriesPath = context.getSettingsPath().resolve(IdeContext.FOLDER_REPOSITORIES); + Path legacyRepositoriesPath = context.getSettingsPath().resolve(IdeContext.FOLDER_LEGACY_REPOSITORIES); + repositoryFile = context.getFileAccess().findExistingFile(value + ".properties", + Arrays.asList(repositoriesPath, legacyRepositoriesPath)); } - if (!Files.exists(repositoryFile)) { - Path legacyRepositoryFile = legacyRepositoriesPath.resolve(repositoryFile.getFileName().toString()); - if (Files.exists(legacyRepositoryFile)) { - repositoryFile = legacyRepositoryFile; - } else { - throw new IllegalStateException("Could not find " + repositoryFile); - } + if (repositoryFile == null) { + throw new IllegalStateException("Could not find " + value); } return repositoryFile; } diff --git a/cli/src/main/java/com/devonfw/tools/ide/url/model/UrlMetadata.java b/cli/src/main/java/com/devonfw/tools/ide/url/model/UrlMetadata.java index 183cf300e..1597c7ec2 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/url/model/UrlMetadata.java +++ b/cli/src/main/java/com/devonfw/tools/ide/url/model/UrlMetadata.java @@ -115,7 +115,10 @@ public UrlVersion getVersionFolder(String tool, String edition, VersionIdentifie VersionIdentifier resolvedVersion = getVersion(tool, edition, version); UrlVersion urlVersion = getEdition(tool, edition).getChild(resolvedVersion.toString()); - Objects.requireNonNull(urlVersion); + if (urlVersion == null) { + throw new IllegalArgumentException( + "Version " + version + " for tool " + tool + " does not exist in edition " + edition + "."); + } return urlVersion; } 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/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/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/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); 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); + } +} 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 000000000..9a592c991 Binary files /dev/null and b/cli/src/test/resources/__files/java-17.0.6-linux-x64.tgz differ diff --git a/cli/src/test/resources/__files/java-17.0.6-windows-x64.zip b/cli/src/test/resources/__files/java-17.0.6-windows-x64.zip new file mode 100644 index 000000000..eb4f3482e Binary files /dev/null and b/cli/src/test/resources/__files/java-17.0.6-windows-x64.zip differ diff --git a/cli/src/test/resources/ide-projects/_ide/urls/java/java/17.0.6/linux_x64.sha256 b/cli/src/test/resources/ide-projects/_ide/urls/java/java/17.0.6/linux_x64.sha256 new file mode 100644 index 000000000..438dd2a03 --- /dev/null +++ b/cli/src/test/resources/ide-projects/_ide/urls/java/java/17.0.6/linux_x64.sha256 @@ -0,0 +1 @@ +c2de7dfbd9f8faaa21b4cdd8518f826dd558c9ab24a0616b3ed28437a674a97b \ No newline at end of file diff --git a/cli/src/test/resources/ide-projects/_ide/urls/java/java/17.0.6/linux_x64.urls b/cli/src/test/resources/ide-projects/_ide/urls/java/java/17.0.6/linux_x64.urls new file mode 100644 index 000000000..42e8cf6cc --- /dev/null +++ b/cli/src/test/resources/ide-projects/_ide/urls/java/java/17.0.6/linux_x64.urls @@ -0,0 +1 @@ +http://localhost:1111/installTest/linux \ No newline at end of file diff --git a/cli/src/test/resources/ide-projects/_ide/urls/java/java/17.0.6/mac_x64.sha256 b/cli/src/test/resources/ide-projects/_ide/urls/java/java/17.0.6/mac_x64.sha256 new file mode 100644 index 000000000..438dd2a03 --- /dev/null +++ b/cli/src/test/resources/ide-projects/_ide/urls/java/java/17.0.6/mac_x64.sha256 @@ -0,0 +1 @@ +c2de7dfbd9f8faaa21b4cdd8518f826dd558c9ab24a0616b3ed28437a674a97b \ No newline at end of file diff --git a/cli/src/test/resources/ide-projects/_ide/urls/java/java/17.0.6/mac_x64.urls b/cli/src/test/resources/ide-projects/_ide/urls/java/java/17.0.6/mac_x64.urls new file mode 100644 index 000000000..384fe79a4 --- /dev/null +++ b/cli/src/test/resources/ide-projects/_ide/urls/java/java/17.0.6/mac_x64.urls @@ -0,0 +1 @@ +http://localhost:1111/installTest/macOS \ No newline at end of file diff --git a/cli/src/test/resources/ide-projects/_ide/urls/java/java/17.0.6/status.json b/cli/src/test/resources/ide-projects/_ide/urls/java/java/17.0.6/status.json new file mode 100644 index 000000000..b58452d90 --- /dev/null +++ b/cli/src/test/resources/ide-projects/_ide/urls/java/java/17.0.6/status.json @@ -0,0 +1,20 @@ +{ + "manual" : true, + "urls" : { + "-680270697" : { + "success" : { + "timestamp" : "2023-04-28T16:27:32.819394600Z" + } + }, + "-896197542" : { + "success" : { + "timestamp" : "2023-04-28T16:27:47.658175400Z" + } + }, + "-310367019" : { + "success" : { + "timestamp" : "2023-04-28T16:28:02.221367500Z" + } + } + } +} \ No newline at end of file diff --git a/cli/src/test/resources/ide-projects/_ide/urls/java/java/17.0.6/windows_x64.urls b/cli/src/test/resources/ide-projects/_ide/urls/java/java/17.0.6/windows_x64.urls new file mode 100644 index 000000000..93010df08 --- /dev/null +++ b/cli/src/test/resources/ide-projects/_ide/urls/java/java/17.0.6/windows_x64.urls @@ -0,0 +1 @@ +http://localhost:1111/installTest/windows \ No newline at end of file diff --git a/cli/src/test/resources/ide-projects/_ide/urls/java/java/17.0.6/windows_x64.urls.sha256 b/cli/src/test/resources/ide-projects/_ide/urls/java/java/17.0.6/windows_x64.urls.sha256 new file mode 100644 index 000000000..fe3cecaad --- /dev/null +++ b/cli/src/test/resources/ide-projects/_ide/urls/java/java/17.0.6/windows_x64.urls.sha256 @@ -0,0 +1 @@ +aa64bee5f7ba56fbbd60d766f3a652600f81571ae5e996804694c69bf731af8b \ No newline at end of file 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 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.) diff --git a/pom.xml b/pom.xml index 6fbd3dd2e..0a51732b8 100644 --- a/pom.xml +++ b/pom.xml @@ -49,6 +49,26 @@ true + + + org.jacoco + jacoco-maven-plugin + 0.8.11 + + + + prepare-agent + + + + report + prepare-package + + report + + + +