diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index e0af77bd39..755a0cc68a 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -7,7 +7,7 @@ This file documents all notable changes to https://github.com/devonfw/IDEasy[IDE Release with new features and bugfixes: * https://github.com/devonfw/IDEasy/issues/1964[#1964]: Fixed gui not launching with older project java versions - +* https://github.com/devonfw/IDEasy/issues/1457[#1457]: Improve CLI error messages on invalid args or commandlets not available in current context The full list of changes for this release can be found in https://github.com/devonfw/IDEasy/milestone/45?closed=1[milestone 2026.06.001]. diff --git a/cli/src/main/java/com/devonfw/tools/ide/cli/CliSuggester.java b/cli/src/main/java/com/devonfw/tools/ide/cli/CliSuggester.java new file mode 100644 index 0000000000..6d4ebbc1aa --- /dev/null +++ b/cli/src/main/java/com/devonfw/tools/ide/cli/CliSuggester.java @@ -0,0 +1,314 @@ +package com.devonfw.tools.ide.cli; + +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.devonfw.tools.ide.commandlet.Commandlet; +import com.devonfw.tools.ide.context.IdeContext; +import com.devonfw.tools.ide.log.IdeLogLevel; +import com.devonfw.tools.ide.property.EnumProperty; +import com.devonfw.tools.ide.property.Property; +import com.devonfw.tools.ide.step.StepImpl; +import com.devonfw.tools.ide.tool.ToolCommandlet; +import com.devonfw.tools.ide.validation.ValidationState; + +/** + * Helper class to suggest corrections for mistyped CLI options, commands, and tools using Levenshtein distance. + */ +public class CliSuggester { + + private static final Logger LOG = LoggerFactory.getLogger(CliSuggester.class); + + private final IdeContext context; + + /** + * The constructor. + * + * @param context the {@link IdeContext}. + */ + public CliSuggester(IdeContext context) { + this.context = context; + } + + + /** + * Handles the case where a commandlet requires an IDEasy project context (IDE_ROOT/IDE_HOME) but the user is not inside one. + * + * @param commandlet the {@link Commandlet} that the user tried to run. + * @param step the current {@link StepImpl} for error reporting. + * @return {@code true} if handled (message printed), {@code false} otherwise. + */ + public boolean isMissingProjectContextHandled(Commandlet commandlet, StepImpl step) { + + if (commandlet == null) { + return false; + } + + boolean missingIdeRoot = commandlet.isIdeRootRequired() && (this.context.getIdeRoot() == null); + boolean missingIdeHome = commandlet.isIdeHomeRequired() && (this.context.getIdeHome() == null); + + if (!(missingIdeRoot || missingIdeHome)) { + return false; + } + + String name = commandlet.getName(); + + step.error("The {} commandlet requires to be an IDEasy project to work.", name); + IdeLogLevel.INTERACTION.log(LOG, "Please run \"icd \" before calling \"ide {}\".", name); + IdeLogLevel.INTERACTION.log(LOG, "Call \"ide help\" for additional details."); + + return true; + } + + + /** + * Handles invalid option errors and suggests corrections. + * + * @param result the {@link com.devonfw.tools.ide.validation.ValidationResult} from option parsing. + * @param commandlet the {@link Commandlet} that was being executed. + * @param step the current {@link StepImpl} for error reporting. + * @return {@code true} if handled (suggestion provided), {@code false} otherwise. + */ + public boolean isInvalidOptionHandled(ValidationState result, Commandlet commandlet, StepImpl step) { + + if ((result == null) || (commandlet == null)) { + return false; + } + String message = result.getErrorMessage(); + if ((message == null) || !message.contains("Invalid option \"") || result.getInvalidOption() == null) { + return false; + } + String invalidOption = result.getInvalidOption(); + if (invalidOption == null) { + return false; + } + List options = getAllOptionNames(commandlet); + String suggestion = bestSuggestion(invalidOption, options); + + step.error("Option \"{}\" not found for commandlet \"{}\"", invalidOption, commandlet.getName()); + if (suggestion != null) { + IdeLogLevel.INTERACTION.log(LOG, "Did you mean \"{}\"?", suggestion); + } + IdeLogLevel.INTERACTION.log(LOG, "Available options are: {}.", String.join(", ", options)); + IdeLogLevel.INTERACTION.log(LOG, "Call \"ide help {}\" for additional details.", commandlet.getName()); + return true; + } + + + /** + * Handles missing commandlet errors and suggests corrections. + * + * @param commandKey the command name that was not found. + * @param step the current {@link StepImpl} for error reporting. + * @return {@code true} if handled (suggestion provided), {@code false} otherwise. + */ + public boolean isMissingCommandletHandled(String commandKey, StepImpl step) { + + // Try to find a suggestion among commandlets + List commandletNames = getAllCommandletNames(); + String commandletSuggestion = bestSuggestion(commandKey, commandletNames); + + // Try to find a suggestion among tools (if not found in commandlets) + if (commandletSuggestion == null) { + List toolNames = getAllToolNames(); + commandletSuggestion = bestSuggestion(commandKey, toolNames); + } + + if (commandletSuggestion != null) { + step.error("Unknown command \"{}\".", commandKey); + IdeLogLevel.INTERACTION.log(LOG, "Did you mean \"{}\"?", commandletSuggestion); + IdeLogLevel.INTERACTION.log(LOG, "Call \"ide help\" for additional details."); + return true; + } + + return false; + } + + + /** + * Handles invalid argument value errors for properties and suggests corrections. + * + * @param result the {@link com.devonfw.tools.ide.validation.ValidationResult} from argument parsing. + * @param commandlet the {@link Commandlet} that was being executed. + * @return {@code true} if handled (suggestion provided), {@code false} otherwise. + */ + public boolean isInvalidArgumentHandled(ValidationState result, Commandlet commandlet) { + + if ((result == null) || (commandlet == null)) { + return false; + } + String invalidValue = result.getInvalidArgument(); + String invalidProperty = result.getInvalidArgumentProperty(); + if (invalidValue == null || invalidProperty == null) { + return false; + } + // Find the property in the commandlet + Property property = null; + for (Property prop : commandlet.getProperties()) { + if (prop.getName().equals(invalidProperty) || (prop.getAlias() != null && prop.getAlias().equals(invalidProperty))) { + property = prop; + break; + } + } + if (property == null) { + return false; + } + // Get valid values for the property + List validValues = getValidValuesForProperty(property); + if (validValues == null || validValues.isEmpty()) { + return false; + } + + String suggestion = bestSuggestion(invalidValue, validValues); + + if (suggestion != null) { + IdeLogLevel.INTERACTION.log(LOG, "Did you mean \"{}={}\"?", property.getName(), suggestion); + } + IdeLogLevel.INTERACTION.log(LOG, "Valid values for '{}' are: {}.", invalidProperty, String.join(", ", validValues)); + IdeLogLevel.INTERACTION.log(LOG, "Call \"ide help {}\" for additional details.", commandlet.getName()); + return true; + } + + //------------------------- Helper methods------------------------- + + /** + * Gets valid values for a property (especially for Enum properties). + * + * @param property the {@link Property}. + * @return a {@link List} of valid values, or {@code null} if the property doesn't have a limited set of valid values. + */ + private List getValidValuesForProperty(Property property) { + + List validValues = new ArrayList<>(); + + // Check if the property is an EnumProperty + if (property instanceof EnumProperty enumProperty) { + Enum[] enumConstants = enumProperty.getValueType().getEnumConstants(); + if (enumConstants != null) { + for (Enum enumConstant : enumConstants) { + validValues.add(enumConstant.name().toLowerCase(Locale.ROOT)); + } + } + } + + return validValues.isEmpty() ? null : validValues; + } + + /** + * Gets all option names for a commandlet. + * + * @param cmd the {@link Commandlet}. + * @return {@link List} of all option names and aliases. + */ + private List getAllOptionNames(Commandlet cmd) { + + List opts = new ArrayList<>(); + for (Property p : cmd.getProperties()) { + if (p.isOption()) { + if (!opts.contains(p.getName())) { + opts.add(p.getName()); + } + if (p.getAlias() != null) { + if (!opts.contains(p.getAlias())) { + opts.add(p.getAlias()); + } + } + } + } + return opts; + } + + /** + * Gets all available commandlet names. + * + * @return {@link List} of commandlet names. + */ + private List getAllCommandletNames() { + + List names = new ArrayList<>(); + for (Commandlet cmd : this.context.getCommandletManager().getCommandlets()) { + names.add(cmd.getName()); + } + return names; + } + + /** + * Gets all available tool names. + * + * @return {@link List} of tool names. + */ + private List getAllToolNames() { + + List names = new ArrayList<>(); + for (Commandlet cmd : this.context.getCommandletManager().getCommandlets()) { + if (cmd instanceof ToolCommandlet) { + names.add(cmd.getName()); + } + } + return names; + } + + /** + * Finds the best matching suggestion from candidates using Levenshtein distance. + * + * @param input the input string to match. + * @param candidates the list of candidate strings. + * @return the best matching candidate, or {@code null} if no suitable match is found. + */ + public String bestSuggestion(String input, List candidates) { + + String best = null; + int bestDist = Integer.MAX_VALUE; + for (String c : candidates) { + int d = levenshtein(input, c); + if (d < bestDist) { + bestDist = d; + best = c; + } + } + if (best != null) { + int threshold = Math.max(3, input.length() / 2); + if (bestDist <= threshold) { + return best; + } + } + return null; + } + + /** + * Calculates the Levenshtein distance between two strings. + * + * @param a the first string. + * @param b the second string. + * @return the edit distance between the strings. + */ + private int levenshtein(String a, String b) { + if (a == null) { + a = ""; + } + if (b == null) { + b = ""; + } + int[] costs = new int[b.length() + 1]; + for (int j = 0; j < costs.length; j++) { + costs[j] = j; + } + for (int i = 1; i <= a.length(); i++) { + costs[0] = i; + int nw = i - 1; + for (int j = 1; j <= b.length(); j++) { + int cj = Math.min(1 + Math.min(costs[j], costs[j - 1]), + a.charAt(i - 1) == b.charAt(j - 1) ? nw : nw + 1); + nw = costs[j]; + costs[j] = cj; + } + } + return costs[b.length()]; + } + +} + 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 1d06087894..d85518760f 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 @@ -29,6 +29,7 @@ import com.devonfw.tools.ide.cli.CliArgument; import com.devonfw.tools.ide.cli.CliArguments; import com.devonfw.tools.ide.cli.CliException; +import com.devonfw.tools.ide.cli.CliSuggester; import com.devonfw.tools.ide.commandlet.Commandlet; import com.devonfw.tools.ide.commandlet.CommandletManager; import com.devonfw.tools.ide.commandlet.CommandletManagerImpl; @@ -175,6 +176,8 @@ public abstract class AbstractIdeContext implements IdeContext, IdeLogArgFormatt private Path logfile; + private CliSuggester cliSuggester; + /** * The constructor. * @@ -1135,6 +1138,10 @@ public void endStep(StepImpl step) { public int run(CliArguments arguments) { CliArgument current = arguments.current(); + if (current.isStart()) { + arguments.next(); + current = arguments.current(); + } assert (this.currentStep == null); boolean supressStepSuccess = false; StepImpl step = newStep(true, "ide", (Object[]) current.asArray()); @@ -1153,9 +1160,36 @@ public int run(CliArguments arguments) { } activateLogging(cmd); verifyIdeMinVersion(false); - if (result != null) { - LOG.error(result.getErrorMessage()); + String commandKey = current.getKey(); + + if (commandKey == null || commandKey.isBlank()) { + return 0; + } + Commandlet commandletByName = findCommandletByName(commandKey); + // Missing commandlet + if (commandletByName == null) { + if (getCliSuggester().isMissingCommandletHandled(commandKey, step)) { + return 1; + } + return 0; + } + // Missing project context + if (getCliSuggester().isMissingProjectContextHandled(commandletByName, step)) { + return 1; } + // Only validate options/arguments if same commandlet and proper type + if (cmd != commandletByName || !(result instanceof ValidationState validationState)) { + return 0; + } + // Invalid option + if (getCliSuggester().isInvalidOptionHandled(validationState, commandletByName, step)) { + return 1; + } + // Invalid argument + if (getCliSuggester().isInvalidArgumentHandled(validationState, commandletByName)) { + return 1; + } + LOG.error(result.getErrorMessage()); step.error("Invalid arguments: {}", current.getArgs()); IdeLogLevel.INTERACTION.log(LOG, "For additional details run ide help {}", cmd == null ? "" : cmd.getName()); return 1; @@ -1173,6 +1207,34 @@ public int run(CliArguments arguments) { } } + /** + * Finds the {@link Commandlet} with the given name. + * + * @param name the name of the {@link Commandlet} to find. + * @return the {@link Commandlet} with the given name or {@code null} if no such {@link Commandlet} exists. + */ + private Commandlet findCommandletByName(String name) { + if (name == null) { + return null; + } + for (Commandlet c : this.commandletManager.getCommandlets()) { + if (name.equals(c.getName())) { + return c; + } + } + return null; + } + + /** + * @return the {@link CliSuggester} for CLI suggestions. + */ + private CliSuggester getCliSuggester() { + if (this.cliSuggester == null) { + this.cliSuggester = new CliSuggester(this); + } + return this.cliSuggester; + } + /** * Ensure the logging system is initialized. */ @@ -1545,6 +1607,14 @@ public ValidationResult apply(CliArguments arguments, Commandlet cmd) { Property option = cmd.getOption(currentArgument.getKey()); if (option != null) { currentProperty = option; + } else { + boolean allowDashedValue = (property != null && property.isValue() && property.isMultiValued()); + if (!allowDashedValue && currentArgument.isOption()) { + ValidationState state = new ValidationState(null); + state.addInvalidOption(currentArgument.getKey()); + state.addErrorMessage("Invalid option \"" + currentArgument.getKey() + "\""); + return state; + } } } if (currentProperty == null) { @@ -1568,6 +1638,15 @@ public ValidationResult apply(CliArguments arguments, Commandlet cmd) { } boolean matches = currentProperty.apply(arguments, this, cmd, null); if (!matches) { + String invalidValue = currentProperty.getLastInvalidValue(); + if (invalidValue != null) { + ValidationState state = new ValidationState(null); + state.addInvalidArgument(invalidValue, currentProperty.getNameOrAlias()); + state.addErrorMessage( + "Invalid CLI argument '" + invalidValue + "' for property '" + currentProperty.getNameOrAlias() + "' of commandlet '" + cmd.getName() + "'"); + currentProperty.clearLastInvalidValue(); + return state; + } ValidationState state = new ValidationState(null); state.addErrorMessage("No matching property found"); return state; diff --git a/cli/src/main/java/com/devonfw/tools/ide/property/Property.java b/cli/src/main/java/com/devonfw/tools/ide/property/Property.java index dbda7b1271..016841458b 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/property/Property.java +++ b/cli/src/main/java/com/devonfw/tools/ide/property/Property.java @@ -53,6 +53,9 @@ public abstract class Property { /** @see #getValue() */ protected final List value = new ArrayList<>(); + /** The last invalid value that was attempted to be parsed. */ + private String lastInvalidValue; + /** * The constructor. * @@ -308,8 +311,10 @@ public final boolean assignValueAsString(String valueAsString, IdeContext contex try { setValueAsString(valueAsString, context); + this.lastInvalidValue = null; return true; } catch (Exception e) { + this.lastInvalidValue = valueAsString; if (e instanceof IllegalArgumentException) { LOG.warn(INVALID_ARGUMENT, valueAsString, getNameOrAlias(), commandlet.getName()); } else { @@ -319,6 +324,20 @@ public final boolean assignValueAsString(String valueAsString, IdeContext contex } } + /** + * @return the last invalid value that was attempted to be parsed, or {@code null} if no invalid value was attempted or the last attempt was successful. + */ + public String getLastInvalidValue() { + return this.lastInvalidValue; + } + + /** + * Clears the last invalid value. + */ + public void clearLastInvalidValue() { + this.lastInvalidValue = null; + } + /** * @return the {@code null} value. */ diff --git a/cli/src/main/java/com/devonfw/tools/ide/validation/ValidationState.java b/cli/src/main/java/com/devonfw/tools/ide/validation/ValidationState.java index 5758143e60..3b9ff7e36d 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/validation/ValidationState.java +++ b/cli/src/main/java/com/devonfw/tools/ide/validation/ValidationState.java @@ -9,6 +9,13 @@ public class ValidationState implements ValidationResult { private StringBuilder errorMessage; + private String invalidOption; + + private String invalidArgument; + + private String invalidArgumentProperty; + + /** * The default constructor for no property. */ @@ -36,6 +43,54 @@ public String getErrorMessage() { return this.errorMessage.toString(); } + + /** + * @return the invalid option that caused the error, if applicable, for enhanced error reporting and suggestions. May be {@code null}. + */ + public String getInvalidOption() { + if (this.invalidOption == null) { + return null; + } + return this.invalidOption; + } + + /** + * @param invalidOption the invalid option that caused the error, if applicable, for enhanced error reporting and suggestions. + */ + public void addInvalidOption(String invalidOption) { + this.invalidOption = invalidOption; + } + + /** + * @return the invalid argument value that caused the error, if applicable, for enhanced error reporting and suggestions. May be {@code null}. + */ + public String getInvalidArgument() { + if (this.invalidArgument == null) { + return null; + } + return this.invalidArgument; + } + + /** + * @return the property name associated with the invalid argument, if applicable. May be {@code null}. + */ + public String getInvalidArgumentProperty() { + if (this.invalidArgumentProperty == null) { + return null; + } + return this.invalidArgumentProperty; + } + + /** + * @param invalidArgument the invalid argument value that caused the error, if applicable, for enhanced error reporting and suggestions. + * @param propertyName the property name associated with the invalid argument. + */ + public void addInvalidArgument(String invalidArgument, String propertyName) { + this.invalidArgument = invalidArgument; + this.invalidArgumentProperty = propertyName; + } + + /** * @param error the error message to add to this {@link ValidationState}. */ diff --git a/cli/src/main/java/com/devonfw/tools/ide/version/VersionIdentifier.java b/cli/src/main/java/com/devonfw/tools/ide/version/VersionIdentifier.java index bafadc950e..f00a6c5d15 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/version/VersionIdentifier.java +++ b/cli/src/main/java/com/devonfw/tools/ide/version/VersionIdentifier.java @@ -1,7 +1,9 @@ package com.devonfw.tools.ide.version; +import java.util.ArrayList; import java.util.List; import java.util.Objects; +import java.util.stream.Collectors; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -88,8 +90,39 @@ public static VersionIdentifier resolveVersionPattern(GenericVersionRange versio return vi; } } + List closest = findClosestVersions(version, versions, 5); + String closestStr = closest.stream().map(Object::toString).collect(Collectors.joining(", ")); throw new CliException( - "Could not find any version matching '" + version + "' - there are " + versions.size() + " version(s) available but none matched!"); + "Could not find any version matching '" + version + "' - there are " + versions.size() + + " version(s) available but none matched!\nDid you mean one of: " + closestStr + "?"); + } + + /** + * Finds the closest versions to the requested version pattern by matching the major version segment. + * + * @param version the requested version pattern or version. + * @param versions the available versions to choose from. + * @param maxCount the maximum number of versions to return. + * @return a list of the closest matching versions. + */ + private static List findClosestVersions(GenericVersionRange version, List versions, int maxCount) { + + if (version instanceof VersionIdentifier vi && !vi.isPattern()) { + long requestedMajor = vi.getStart().getNumber(); + List majorMatches = new ArrayList<>(); + for (VersionIdentifier v : versions) { + if (v.getStart().getNumber() == requestedMajor) { + majorMatches.add(v); + if (majorMatches.size() >= maxCount) { + break; + } + } + } + if (!majorMatches.isEmpty()) { + return majorMatches; + } + } + return versions.size() <= maxCount ? versions : versions.subList(0, maxCount); } /** diff --git a/cli/src/test/java/com/devonfw/tools/ide/cli/CliSuggestionTest.java b/cli/src/test/java/com/devonfw/tools/ide/cli/CliSuggestionTest.java new file mode 100644 index 0000000000..35f7a0907a --- /dev/null +++ b/cli/src/test/java/com/devonfw/tools/ide/cli/CliSuggestionTest.java @@ -0,0 +1,129 @@ +package com.devonfw.tools.ide.cli; + +import org.junit.jupiter.api.Test; + +import com.devonfw.tools.ide.context.AbstractIdeContextTest; +import com.devonfw.tools.ide.context.IdeTestContext; +import com.devonfw.tools.ide.log.IdeLogEntry; +import com.devonfw.tools.ide.log.IdeLogLevel; + +/** + * Tests for CLI suggestion behavior: + *
    + *
  • Missing IDE project context
  • + *
  • Invalid option with suggestion
  • + *
  • Unknown commandlet with suggestion
  • + *
  • Invalid enum property value with suggestion
  • + *
+ */ +class CliSuggestionTest extends AbstractIdeContextTest { + + private static final String PROJECT_BASIC = "basic"; + + + @Test + void testMissingProjectContextSuggestsProblem() { + + IdeTestContext context = new IdeTestContext(); + context.getTestStartContext().getEntries().clear(); + + CliArguments args = new CliArguments("update"); + args.next(); + int exit = context.run(args); + + assertThat(exit).isEqualTo(1); + + assertThat(context).log().hasEntries( + new IdeLogEntry(IdeLogLevel.ERROR, "The update commandlet requires to be an IDEasy project to work.", true), + new IdeLogEntry(IdeLogLevel.INTERACTION, "Please run \"icd \" before calling \"ide update\".", true), + new IdeLogEntry(IdeLogLevel.INTERACTION, "Call \"ide help\" for additional details.", true) + ); + } + + @Test + void testInvalidOptionSuggestsClosestMatch() { + + IdeTestContext context = newContext(PROJECT_BASIC); + context.getTestStartContext().getEntries().clear(); + + CliArguments args = new CliArguments("upgrade", "--mdoe"); + args.next(); + + int exit = context.run(args); + + assertThat(exit).isEqualTo(1); + + assertThat(context).log().hasEntries( + new IdeLogEntry(IdeLogLevel.ERROR, "Option \"--mdoe\" not found for commandlet \"upgrade\"", true), + new IdeLogEntry(IdeLogLevel.INTERACTION, "Did you mean \"--mode\"?", true), + new IdeLogEntry(IdeLogLevel.INTERACTION, "Available options are: --mode.", true), + new IdeLogEntry(IdeLogLevel.INTERACTION, "Call \"ide help upgrade\" for additional details.", true) + ); + + } + + @Test + void testUnknownCommandSuggestsClosestCommand() { + + IdeTestContext context = new IdeTestContext(); + context.getTestStartContext().getEntries().clear(); + + CliArguments args = new CliArguments("updtae"); + args.next(); + + int exit = context.run(args); + + assertThat(exit).isEqualTo(1); + assertThat(context).log().hasEntries( + new IdeLogEntry(IdeLogLevel.ERROR, "Unknown command \"updtae\".", true), + new IdeLogEntry(IdeLogLevel.INTERACTION, "Did you mean \"update\"?", true), + new IdeLogEntry(IdeLogLevel.INTERACTION, "Call \"ide help\" for additional details.", true) + ); + } + + @Test + void testInvalidEnumValueSuggestsCorrectValue() { + + IdeTestContext context = newContext(PROJECT_BASIC); + context.getTestStartContext().getEntries().clear(); + + // Test set-edition commandlet with invalid --cfg value 'config' + // Valid values are: user, settings, workspace, conf (from EnvironmentVariablesFiles enum) + CliArguments args = new CliArguments("set-edition", "java", "11", "--cfg", "config"); + args.next(); + + int exit = context.run(args); + + assertThat(exit).isEqualTo(1); + + assertThat(context).log().hasEntries( + new IdeLogEntry(IdeLogLevel.WARNING, + "Invalid CLI argument 'config' for property '--cfg' of commandlet 'set-edition'", true), + new IdeLogEntry(IdeLogLevel.INTERACTION, "Did you mean \"--cfg=conf\"?", true), + new IdeLogEntry(IdeLogLevel.INTERACTION, "Valid values for '--cfg' are: user, settings, workspace, conf.", true), + new IdeLogEntry(IdeLogLevel.INTERACTION, "Call \"ide help set-edition\" for additional details.", true) + ); + } + + + @Test + void testInvalidEnumValueWithoutSuggestionStillShowsAllowedValues() { + + IdeTestContext context = newContext(PROJECT_BASIC); + context.getTestStartContext().getEntries().clear(); + + CliArguments args = new CliArguments("set-edition", "java", "11", "--cfg", "zzzzzz"); + args.next(); + + int exit = context.run(args); + + assertThat(exit).isEqualTo(1); + assertThat(context).log().hasEntries( + new IdeLogEntry(IdeLogLevel.WARNING, + "Invalid CLI argument 'zzzzzz' for property '--cfg' of commandlet 'set-edition'", true), + new IdeLogEntry(IdeLogLevel.INTERACTION, "Valid values for '--cfg' are: user, settings, workspace, conf.", true), + new IdeLogEntry(IdeLogLevel.INTERACTION, "Call \"ide help set-edition\" for additional details.", true) + ); + assertThat(context).log().hasNoMessageContaining("Did you mean"); + } +} 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 c0abaefbde..7c57fd68cd 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 @@ -9,6 +9,8 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; +import com.devonfw.tools.ide.cli.CliException; + /** * Test of {@link VersionIdentifier}. */ @@ -358,4 +360,39 @@ void testIsStable() { assertThat(VersionIdentifier.LATEST_UNSTABLE.isStable()).isFalse(); } + /** Test of {@link VersionIdentifier#resolveVersionPattern(GenericVersionRange, List)} - exact version found. */ + @Test + void testResolveVersionPatternVersionFound() { + + final VersionIdentifier identifier = VersionIdentifier.of("2025.01.002"); + final List availableVersions = List.of(identifier); + final VersionIdentifier resolvedVersion = VersionIdentifier.resolveVersionPattern(identifier, availableVersions); + assertThat(resolvedVersion).isEqualTo(identifier); + } + + /** Test of {@link VersionIdentifier#resolveVersionPattern(GenericVersionRange, List)} - pattern version found. */ + @Test + void testResolveVersionPatternVersionFoundPattern() { + + final VersionIdentifier identifier = VersionIdentifier.of("2025.01.002"); + final VersionIdentifier identifierPattern = VersionIdentifier.of("2025.01.*"); + final List availableVersions = List.of(identifier); + final VersionIdentifier resolvedVersion = VersionIdentifier.resolveVersionPattern(identifierPattern, availableVersions); + assertThat(resolvedVersion).isEqualTo(identifier); + } + + /** Test of {@link VersionIdentifier#resolveVersionPattern(GenericVersionRange, List)} - no version found with suggestions. */ + @Test + void testResolveVersionPatternNoVersionFound() { + + final VersionIdentifier identifier = VersionIdentifier.of("2025.01.002"); + final VersionIdentifier identifierPattern = VersionIdentifier.of("2026.01.*"); + final List availableVersions = List.of(identifier); + final String expectedMessage = "Could not find any version matching '" + identifierPattern + + "' - there are " + availableVersions.size() + " version(s) available but none matched!\nDid you mean one of: " + identifier + "?"; + + assertThatThrownBy(() -> VersionIdentifier.resolveVersionPattern(identifierPattern, availableVersions)) + .isInstanceOf(CliException.class) + .hasMessage(expectedMessage); + } }