Rework command subsystem

- Focus of these changes are to introduce a new command system based on
  real registrations (new way) instead of continuously (old way) resolve
  methods and its parameters via reflection.
- There's a lot of changes as this resolution via reflection had its
  hooks almost everywhere and thus most changes are just refactorings.
- Order to understand real changes I'd start to look classes under
  `org.springframework.shell.command` package as it defines new registration,
  catalog and parser classes. Also samples contain new classes to demonstrate
  new functionality.
- Fixes #380
This commit is contained in:
Janne Valkealahti
2022-05-06 08:32:53 +01:00
parent 81e5bf8c81
commit 8a23518b84
91 changed files with 7026 additions and 2291 deletions

View File

@@ -29,11 +29,11 @@ import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.springframework.core.MethodParameter;
import org.springframework.shell.CommandRegistry;
import org.springframework.shell.CompletionContext;
import org.springframework.shell.CompletionProposal;
import org.springframework.shell.MethodTarget;
import org.springframework.shell.Utils;
import org.springframework.shell.command.CommandCatalog;
import org.springframework.shell.command.CommandRegistration;
import org.springframework.util.ReflectionUtils;
import static org.assertj.core.api.Assertions.assertThat;
@@ -47,7 +47,7 @@ import static org.mockito.Mockito.when;
public class CommandValueProviderTest {
@Mock
private CommandRegistry shell;
private CommandCatalog catalog;
@BeforeEach
public void setUp() {
@@ -56,7 +56,7 @@ public class CommandValueProviderTest {
@Test
public void testValues() {
CommandValueProvider valueProvider = new CommandValueProvider(shell);
CommandValueProvider valueProvider = new CommandValueProvider(catalog);
Method help = ReflectionUtils.findMethod(Command.class, "help", String.class);
MethodParameter methodParameter = Utils.createMethodParameter(help, 0);
@@ -64,12 +64,12 @@ public class CommandValueProviderTest {
boolean supports = valueProvider.supports(methodParameter, completionContext);
assertThat(supports).isEqualTo(true);
Map<String, CommandRegistration> registrations = new HashMap<>();
registrations.put("me", null);
registrations.put("meow", null);
registrations.put("yourself", null);
Map<String, MethodTarget> commands = new HashMap<>();
commands.put("me", null);
commands.put("meow", null);
commands.put("yourself", null);
when(shell.listCommands()).thenReturn(commands);
when(catalog.getRegistrations()).thenReturn(registrations);
List<CompletionProposal> proposals = valueProvider.complete(methodParameter, completionContext, new String[0]);
assertThat(proposals).extracting("value", String.class)

View File

@@ -1,304 +0,0 @@
/*
* Copyright 2017-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.shell.standard;
import java.util.Map;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.shell.Availability;
import org.springframework.shell.ConfigurableCommandRegistry;
import org.springframework.shell.MethodTarget;
import org.springframework.shell.context.DefaultShellContext;
import org.springframework.shell.context.InteractionMode;
import org.springframework.shell.standard.test1.GroupOneCommands;
import org.springframework.shell.standard.test2.GroupThreeCommands;
import org.springframework.shell.standard.test2.GroupTwoCommands;
import org.springframework.util.ReflectionUtils;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
/**
* Unit tests for {@link StandardMethodTargetRegistrar}.
*
* @author Eric Bottard
*/
public class StandardMethodTargetRegistrarTest {
private StandardMethodTargetRegistrar registrar = new StandardMethodTargetRegistrar();
private ConfigurableCommandRegistry registry = new ConfigurableCommandRegistry(new DefaultShellContext());
@Test
public void testRegistrations() {
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(Sample.class);
registrar.setApplicationContext(applicationContext);
registrar.register(registry);
MethodTarget methodTarget = registry.listCommands().get("say-hello");
assertThat(methodTarget).isNotNull();
assertThat(methodTarget.getHelp()).isEqualTo("some command");
assertThat(methodTarget.getMethod()).isEqualTo(ReflectionUtils.findMethod(Sample.class, "sayHello", String.class));
assertThat(methodTarget.getAvailability().isAvailable()).isTrue();
methodTarget = registry.listCommands().get("hi");
assertThat(methodTarget).isNotNull();
assertThat(methodTarget.getHelp()).isEqualTo("method with alias");
assertThat(methodTarget.getMethod()).isEqualTo(ReflectionUtils.findMethod(Sample.class, "greet", String.class));
assertThat(methodTarget.getAvailability().isAvailable()).isTrue();
methodTarget = registry.listCommands().get("alias");
assertThat(methodTarget).isNotNull();
assertThat(methodTarget.getHelp()).isEqualTo("method with alias");
assertThat(methodTarget.getMethod()).isEqualTo(ReflectionUtils.findMethod(Sample.class, "greet", String.class));
assertThat(methodTarget.getAvailability().isAvailable()).isTrue();
}
@ShellComponent
public static class Sample {
@ShellMethod("some command")
public String sayHello(String what) {
return "hello " + what;
}
@ShellMethod(value = "method with alias", key = {"hi", "alias"})
public String greet(String what) {
return "hi " + what;
}
}
@Test
public void testAvailabilityIndicators() {
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(SampleWithAvailability.class);
registrar.setApplicationContext(applicationContext);
registrar.register(registry);
SampleWithAvailability sample = applicationContext.getBean(SampleWithAvailability.class);
MethodTarget methodTarget = registry.listCommands().get("say-hello");
assertThat(methodTarget.getMethod()).isEqualTo(ReflectionUtils.findMethod(SampleWithAvailability.class, "sayHello"));
assertThat(methodTarget.getAvailability().isAvailable()).isTrue();
sample.available = false;
assertThat(methodTarget.getAvailability().isAvailable()).isFalse();
assertThat(methodTarget.getAvailability().getReason()).isEqualTo("sayHelloAvailability");
sample.available = true;
methodTarget = registry.listCommands().get("hi");
assertThat(methodTarget.getMethod()).isEqualTo(ReflectionUtils.findMethod(SampleWithAvailability.class, "hi"));
assertThat(methodTarget.getAvailability().isAvailable()).isTrue();
sample.available = false;
assertThat(methodTarget.getAvailability().isAvailable()).isFalse();
assertThat(methodTarget.getAvailability().getReason()).isEqualTo("customAvailabilityMethod");
sample.available = true;
methodTarget = registry.listCommands().get("bonjour");
assertThat(methodTarget.getMethod()).isEqualTo(ReflectionUtils.findMethod(SampleWithAvailability.class, "bonjour"));
assertThat(methodTarget.getAvailability().isAvailable()).isTrue();
sample.available = false;
assertThat(methodTarget.getAvailability().isAvailable()).isFalse();
assertThat(methodTarget.getAvailability().getReason()).isEqualTo("availabilityForSeveralCommands");
sample.available = true;
}
@ShellComponent
public static class SampleWithAvailability {
private boolean available = true;
@ShellMethod("some command with an implicit availability indicator")
public void sayHello() {
}
public Availability sayHelloAvailability() {
return available ? Availability.available() : Availability.unavailable("sayHelloAvailability");
}
@ShellMethodAvailability("customAvailabilityMethod")
@ShellMethod("some method with an explicit availability indicator")
public void hi() {
}
public Availability customAvailabilityMethod() {
return available ? Availability.available() : Availability.unavailable("customAvailabilityMethod");
}
@ShellMethod(value = "some method with an explicit availability indicator", key = {"bonjour", "salut"})
public void bonjour() {
}
@ShellMethodAvailability({"salut", "other"})
public Availability availabilityForSeveralCommands() {
return available ? Availability.available() : Availability.unavailable("availabilityForSeveralCommands");
}
@ShellMethod("a command whose availability indicator will come from wildcard")
public void wild() {
}
@ShellMethodAvailability("*")
private Availability availabilityFromWildcard() {
return available ? Availability.available() : Availability.unavailable("availabilityFromWildcard");
}
}
@Test
public void testAvailabilityIndicatorErrorMultipleExplicit() {
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(WrongAvailabilityIndicatorOnShellMethod.class);
registrar.setApplicationContext(applicationContext);
assertThatThrownBy(() -> {
registrar.register(registry);
}).isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("When set on a @ShellMethod method, the value of the @ShellMethodAvailability should be a single element")
.hasMessageContaining("Found [one, two]")
.hasMessageContaining("wrong()");
}
@ShellComponent
public static class WrongAvailabilityIndicatorOnShellMethod {
@ShellMethodAvailability({"one", "two"})
@ShellMethod("foo")
public void wrong() {
}
}
@Test
public void testAvailabilityIndicatorWildcardNotAlone() {
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(WrongAvailabilityIndicatorWildcardNotAlone.class);
registrar.setApplicationContext(applicationContext);
assertThatThrownBy(() -> {
registrar.register(registry);
}).isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("When using '*' as a wildcard for ShellMethodAvailability, this can be the only value. Found [one, *]")
.hasMessageContaining("availability()");
}
@ShellComponent
public static class WrongAvailabilityIndicatorWildcardNotAlone {
@ShellMethodAvailability({"one", "*"})
public Availability availability() {
return Availability.available();
}
@ShellMethod("foo")
public void wrong() {
}
}
@Test
public void testAvailabilityIndicatorAmbiguous() {
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(WrongAvailabilityIndicatorAmbiguous.class);
registrar.setApplicationContext(applicationContext);
assertThatThrownBy(() -> {
registrar.register(registry);
}).isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("Found several @ShellMethodAvailability")
.hasMessageContaining("wrong()")
.hasMessageContaining("availability()")
.hasMessageContaining("otherAvailability()");
}
@ShellComponent
public static class WrongAvailabilityIndicatorAmbiguous {
@ShellMethodAvailability({"one", "wrong"})
public Availability availability() {
return Availability.available();
}
@ShellMethodAvailability({"bar", "wrong"})
public Availability otherAvailability() {
return Availability.available();
}
@ShellMethod("foo")
public void wrong() {
}
}
@Test
public void testGrouping() {
ApplicationContext context = new AnnotationConfigApplicationContext(GroupOneCommands.class,
GroupTwoCommands.class, GroupThreeCommands.class);
registrar.setApplicationContext(context);
registrar.register(registry);
Map<String, MethodTarget> commands = registry.listCommands();
Assertions.assertThat(commands.get("explicit1").getGroup()).isEqualTo("Explicit Group Method Level 1");
Assertions.assertThat(commands.get("explicit2").getGroup()).isEqualTo("Explicit Group Method Level 2");
Assertions.assertThat(commands.get("explicit3").getGroup()).isEqualTo("Explicit Group Method Level 3");
Assertions.assertThat(commands.get("implicit1").getGroup()).isEqualTo("Implicit Group Package Level 1");
Assertions.assertThat(commands.get("implicit2").getGroup()).isEqualTo("Group Two Commands");
Assertions.assertThat(commands.get("implicit3").getGroup()).isEqualTo("Explicit Group 3 Class Level");
}
@Test
public void testInteractionModeInteractive() {
DefaultShellContext shellContext = new DefaultShellContext();
shellContext.setInteractionMode(InteractionMode.INTERACTIVE);
registry = new ConfigurableCommandRegistry(shellContext);
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(InteractionModeCommands.class);
registrar.setApplicationContext(applicationContext);
registrar.register(registry);
assertThat(registry.listCommands().get("foo1")).isNotNull();
assertThat(registry.listCommands().get("foo2")).isNull();
assertThat(registry.listCommands().get("foo3")).isNotNull();
}
@Test
public void testInteractionModeNonInteractive() {
DefaultShellContext shellContext = new DefaultShellContext();
shellContext.setInteractionMode(InteractionMode.NONINTERACTIVE);
registry = new ConfigurableCommandRegistry(shellContext);
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(InteractionModeCommands.class);
registrar.setApplicationContext(applicationContext);
registrar.register(registry);
assertThat(registry.listCommands().get("foo1")).isNull();
assertThat(registry.listCommands().get("foo2")).isNotNull();
assertThat(registry.listCommands().get("foo3")).isNotNull();
}
@ShellComponent
public static class InteractionModeCommands {
@ShellMethod(value = "foo1", interactionMode = InteractionMode.INTERACTIVE)
public void foo1() {
}
@ShellMethod(value = "foo2", interactionMode = InteractionMode.NONINTERACTIVE)
public void foo2() {
}
@ShellMethod(value = "foo3")
public void foo3() {
}
}
}

View File

@@ -0,0 +1,326 @@
/*
* Copyright 2017-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.shell.standard;
import java.util.Map;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.shell.Availability;
import org.springframework.shell.command.CommandCatalog;
import org.springframework.shell.command.CommandRegistration;
import org.springframework.shell.context.DefaultShellContext;
import org.springframework.shell.context.InteractionMode;
import org.springframework.shell.standard.test1.GroupOneCommands;
import org.springframework.shell.standard.test2.GroupThreeCommands;
import org.springframework.shell.standard.test2.GroupTwoCommands;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
/**
* Unit tests for {@link StandardMethodTargetRegistrar}.
*
* @author Eric Bottard
*/
public class StandardMethodTargetRegistrarTests {
private StandardMethodTargetRegistrar registrar = new StandardMethodTargetRegistrar();
private AnnotationConfigApplicationContext applicationContext;
private CommandCatalog catalog;
private DefaultShellContext shellContext;
@BeforeEach
public void setup() {
shellContext = new DefaultShellContext();
catalog = CommandCatalog.of(null, shellContext);
}
@AfterEach
public void cleanup() {
if (applicationContext != null) {
applicationContext.close();
}
applicationContext = null;
catalog = null;
}
@Test
public void testRegistrations() {
applicationContext = new AnnotationConfigApplicationContext(Sample.class);
registrar.setApplicationContext(applicationContext);
registrar.register(catalog);
Map<String, CommandRegistration> registrations = catalog.getRegistrations();
assertThat(registrations).hasSize(3);
assertThat(registrations.get("say-hello")).isNotNull();
assertThat(registrations.get("say-hello").getAvailability()).isNotNull();
assertThat(registrations.get("say-hello").getOptions()).hasSize(1);
assertThat(registrations.get("say-hello").getOptions().get(0).getLongNames()).containsExactly("what");
assertThat(registrations.get("hi")).isNotNull();
assertThat(registrations.get("hi").getAvailability()).isNotNull();
assertThat(registrations.get("alias")).isNotNull();
assertThat(registrations.get("alias").getAvailability()).isNotNull();
}
@ShellComponent
public static class Sample {
@ShellMethod("some command")
public String sayHello(String what) {
return "hello " + what;
}
@ShellMethod(value = "method with alias", key = {"hi", "alias"})
public String greet(String what) {
return "hi " + what;
}
}
@Test
public void testAvailabilityIndicators() {
applicationContext = new AnnotationConfigApplicationContext(SampleWithAvailability.class);
SampleWithAvailability sample = applicationContext.getBean(SampleWithAvailability.class);
registrar.setApplicationContext(applicationContext);
registrar.register(catalog);
Map<String, CommandRegistration> registrations = catalog.getRegistrations();
assertThat(registrations.get("say-hello")).isNotNull();
assertThat(registrations.get("say-hello").getAvailability().isAvailable()).isTrue();
sample.available = false;
assertThat(registrations.get("say-hello").getAvailability().isAvailable()).isFalse();
assertThat(registrations.get("say-hello").getAvailability().getReason()).isEqualTo("sayHelloAvailability");
sample.available = true;
assertThat(registrations.get("hi")).isNotNull();
assertThat(registrations.get("hi").getAvailability().isAvailable()).isTrue();
sample.available = false;
assertThat(registrations.get("hi").getAvailability().isAvailable()).isFalse();
assertThat(registrations.get("hi").getAvailability().getReason()).isEqualTo("customAvailabilityMethod");
sample.available = true;
assertThat(registrations.get("bonjour")).isNotNull();
assertThat(registrations.get("bonjour").getAvailability().isAvailable()).isTrue();
sample.available = false;
assertThat(registrations.get("bonjour").getAvailability().isAvailable()).isFalse();
assertThat(registrations.get("bonjour").getAvailability().getReason()).isEqualTo("availabilityForSeveralCommands");
sample.available = true;
}
@ShellComponent
public static class SampleWithAvailability {
private boolean available = true;
@ShellMethod("some command with an implicit availability indicator")
public void sayHello() {
}
public Availability sayHelloAvailability() {
return available ? Availability.available() : Availability.unavailable("sayHelloAvailability");
}
@ShellMethodAvailability("customAvailabilityMethod")
@ShellMethod("some method with an explicit availability indicator")
public void hi() {
}
public Availability customAvailabilityMethod() {
return available ? Availability.available() : Availability.unavailable("customAvailabilityMethod");
}
@ShellMethod(value = "some method with an explicit availability indicator", key = {"bonjour", "salut"})
public void bonjour() {
}
@ShellMethodAvailability({"salut", "other"})
public Availability availabilityForSeveralCommands() {
return available ? Availability.available() : Availability.unavailable("availabilityForSeveralCommands");
}
@ShellMethod("a command whose availability indicator will come from wildcard")
public void wild() {
}
@ShellMethodAvailability("*")
private Availability availabilityFromWildcard() {
return available ? Availability.available() : Availability.unavailable("availabilityFromWildcard");
}
}
@Test
public void testAvailabilityIndicatorErrorMultipleExplicit() {
applicationContext = new AnnotationConfigApplicationContext(WrongAvailabilityIndicatorOnShellMethod.class);
registrar.setApplicationContext(applicationContext);
assertThatThrownBy(() -> {
registrar.register(catalog);
}).isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("When set on a @ShellMethod method, the value of the @ShellMethodAvailability should be a single element")
.hasMessageContaining("Found [one, two]")
.hasMessageContaining("wrong()");
}
@ShellComponent
public static class WrongAvailabilityIndicatorOnShellMethod {
@ShellMethodAvailability({"one", "two"})
@ShellMethod("foo")
public void wrong() {
}
}
@Test
public void testAvailabilityIndicatorWildcardNotAlone() {
applicationContext = new AnnotationConfigApplicationContext(WrongAvailabilityIndicatorWildcardNotAlone.class);
registrar.setApplicationContext(applicationContext);
assertThatThrownBy(() -> {
registrar.register(catalog);
}).isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("When using '*' as a wildcard for ShellMethodAvailability, this can be the only value. Found [one, *]")
.hasMessageContaining("availability()");
}
@ShellComponent
public static class WrongAvailabilityIndicatorWildcardNotAlone {
@ShellMethodAvailability({"one", "*"})
public Availability availability() {
return Availability.available();
}
@ShellMethod("foo")
public void wrong() {
}
}
@Test
public void testAvailabilityIndicatorAmbiguous() {
applicationContext = new AnnotationConfigApplicationContext(WrongAvailabilityIndicatorAmbiguous.class);
registrar.setApplicationContext(applicationContext);
assertThatThrownBy(() -> {
registrar.register(catalog);
}).isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("Found several @ShellMethodAvailability")
.hasMessageContaining("wrong()")
.hasMessageContaining("availability()")
.hasMessageContaining("otherAvailability()");
}
@ShellComponent
public static class WrongAvailabilityIndicatorAmbiguous {
@ShellMethodAvailability({"one", "wrong"})
public Availability availability() {
return Availability.available();
}
@ShellMethodAvailability({"bar", "wrong"})
public Availability otherAvailability() {
return Availability.available();
}
@ShellMethod("foo")
public void wrong() {
}
}
@Test
public void testGrouping() {
applicationContext = new AnnotationConfigApplicationContext(GroupOneCommands.class,
GroupTwoCommands.class, GroupThreeCommands.class);
registrar.setApplicationContext(applicationContext);
registrar.register(catalog);
assertThat(catalog.getRegistrations().get("explicit1")).satisfies(registration -> {
assertThat(registration).isNotNull();
assertThat(registration.getGroup()).isEqualTo("Explicit Group Method Level 1");
});
assertThat(catalog.getRegistrations().get("explicit2")).satisfies(registration -> {
assertThat(registration).isNotNull();
assertThat(registration.getGroup()).isEqualTo("Explicit Group Method Level 2");
});
assertThat(catalog.getRegistrations().get("explicit3")).satisfies(registration -> {
assertThat(registration).isNotNull();
assertThat(registration.getGroup()).isEqualTo("Explicit Group Method Level 3");
});
assertThat(catalog.getRegistrations().get("implicit1")).satisfies(registration -> {
assertThat(registration).isNotNull();
assertThat(registration.getGroup()).isEqualTo("Implicit Group Package Level 1");
});
assertThat(catalog.getRegistrations().get("implicit2")).satisfies(registration -> {
assertThat(registration).isNotNull();
assertThat(registration.getGroup()).isEqualTo("Group Two Commands");
});
assertThat(catalog.getRegistrations().get("implicit3")).satisfies(registration -> {
assertThat(registration).isNotNull();
assertThat(registration.getGroup()).isEqualTo("Explicit Group 3 Class Level");
});
}
@Test
public void testInteractionModeInteractive() {
shellContext.setInteractionMode(InteractionMode.INTERACTIVE);
applicationContext = new AnnotationConfigApplicationContext(InteractionModeCommands.class);
registrar.setApplicationContext(applicationContext);
registrar.register(catalog);
assertThat(catalog.getRegistrations().get("foo1")).isNotNull();
assertThat(catalog.getRegistrations().get("foo2")).isNull();
assertThat(catalog.getRegistrations().get("foo3")).isNotNull();
}
@Test
public void testInteractionModeNonInteractive() {
shellContext.setInteractionMode(InteractionMode.NONINTERACTIVE);
applicationContext = new AnnotationConfigApplicationContext(InteractionModeCommands.class);
registrar.setApplicationContext(applicationContext);
registrar.register(catalog);
assertThat(catalog.getRegistrations().get("foo1")).isNull();
assertThat(catalog.getRegistrations().get("foo2")).isNotNull();
assertThat(catalog.getRegistrations().get("foo3")).isNotNull();
}
@ShellComponent
public static class InteractionModeCommands {
@ShellMethod(value = "foo1", interactionMode = InteractionMode.INTERACTIVE)
public void foo1() {
}
@ShellMethod(value = "foo2", interactionMode = InteractionMode.NONINTERACTIVE)
public void foo2() {
}
@ShellMethod(value = "foo3")
public void foo3() {
}
}
}

View File

@@ -1,272 +0,0 @@
/*
* Copyright 2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.shell.standard;
import java.lang.reflect.Method;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import org.jline.reader.ParsedLine;
import org.jline.reader.impl.DefaultParser;
import org.junit.jupiter.api.Test;
import org.springframework.core.convert.support.DefaultConversionService;
import org.springframework.shell.CompletionContext;
import org.springframework.shell.CompletionProposal;
import org.springframework.shell.ParameterMissingResolutionException;
import org.springframework.shell.UnfinishedParameterResolutionException;
import org.springframework.shell.Utils;
import org.springframework.shell.ValueResult;
import static java.util.Arrays.asList;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.springframework.shell.ValueResultAsserts.assertThat;
import static org.springframework.util.ReflectionUtils.findMethod;
/**
* Unit tests for DefaultParameterResolver.
* @author Eric Bottard
* @author Florent Biville
*/
public class StandardParameterResolverTest {
// private StandardParameterResolver resolver = new StandardParameterResolver(new DefaultConversionService(), Collections.emptySet());
// Tests for resolution
@Test
public void testParses() throws Exception {
StandardParameterResolver resolver = new StandardParameterResolver(new DefaultConversionService(),
Collections.emptySet());
Method method = findMethod(Remote.class, "zap", boolean.class, String.class, String.class, String.class);
List<String> words = asList("--force --name --foo y".split(" "));
ValueResult result0 = resolver.resolve(Utils.createMethodParameter(method, 0), words);
assertThat(result0).hasValue(true).usesWords(0).notUsesWordsForValue();
assertThat(result0.wordsUsed(words)).containsExactly("--force");
ValueResult result1 = resolver.resolve(Utils.createMethodParameter(method, 1), words);
assertThat(result1).hasValue("--foo").usesWords(1, 2).usesWordsForValue(2);
assertThat(result1.wordsUsed(words)).containsExactly("--name", "--foo");
assertThat(result1.wordsUsedForValue(words)).containsExactly("--foo");
ValueResult result2 = resolver.resolve(Utils.createMethodParameter(method, 2), words);
assertThat(result2).hasValue("y").usesWords(3).usesWordsForValue(3);
assertThat(result2.wordsUsed(words)).containsExactly("y");
ValueResult result3 = resolver.resolve(Utils.createMethodParameter(method, 3), words);
assertThat(result3).hasValue("last").notUsesWords().notUsesWordsForValue();
}
@Test
public void testParsesWithMethodPrefix() throws Exception {
StandardParameterResolver resolver = new StandardParameterResolver(new DefaultConversionService(),
Collections.emptySet());
Method method = findMethod(Remote.class, "prefixTest", String.class);
ValueResult result = resolver.resolve(Utils.createMethodParameter(method, 0),
asList("-message abc".split(" ")));
assertThat(result).hasValue("abc").usesWords(0, 1).usesWordsForValue(1);
}
@Test
public void testParameterSpecifiedTwiceViaDifferentAliases() throws Exception {
StandardParameterResolver resolver = new StandardParameterResolver(new DefaultConversionService(),
Collections.emptySet());
Method method = findMethod(Remote.class, "zap", boolean.class, String.class, String.class, String.class);
assertThatThrownBy(() -> {
resolver.resolve(
Utils.createMethodParameter(method, 0),
asList("--force --name --foo y --bar x --baz z".split(" ")));
}).isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("Named parameter has been specified multiple times via '--bar, --baz'");
}
@Test
public void testParameterSpecifiedTwiceViaSameKey() throws Exception {
StandardParameterResolver resolver = new StandardParameterResolver(new DefaultConversionService(),
Collections.emptySet());
Method method = findMethod(Remote.class, "zap", boolean.class, String.class, String.class, String.class);
assertThatThrownBy(() -> {
resolver.resolve(
Utils.createMethodParameter(method, 0),
asList("--force --name --foo y --baz x --baz z".split(" ")));
}).isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("Parameter for '--baz' has already been specified");
}
@Test
public void testTooMuchInput() throws Exception {
StandardParameterResolver resolver = new StandardParameterResolver(new DefaultConversionService(),
Collections.emptySet());
Method method = findMethod(Remote.class, "zap", boolean.class, String.class, String.class, String.class);
assertThatThrownBy(() -> {
resolver.resolve(
Utils.createMethodParameter(method, 0),
asList("--foo hello --name bar --force --bar well leftover".split(" ")));
}).isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("the following could not be mapped to parameters: 'leftover'");
}
@Test
public void testIncompleteCommandResolution() throws Exception {
StandardParameterResolver resolver = new StandardParameterResolver(new DefaultConversionService(),
Collections.emptySet());
Method method = findMethod(Remote.class, "shutdown", Remote.Delay.class);
assertThatThrownBy(() -> {
resolver.resolve(
Utils.createMethodParameter(method, 0),
asList("--delay".split(" ")));
}).isInstanceOf(UnfinishedParameterResolutionException.class)
.hasMessageContaining("Error trying to resolve '--delay delay' using [--delay]");
}
@Test
public void testIncompleteCommandResolutionBigArity() throws Exception {
StandardParameterResolver resolver = new StandardParameterResolver(new DefaultConversionService(),
Collections.emptySet());
Method method = findMethod(Remote.class, "add", List.class);
assertThatThrownBy(() -> {
resolver.resolve(
Utils.createMethodParameter(method, 0),
asList("--numbers 1 2".split(" ")));
}).isInstanceOf(UnfinishedParameterResolutionException.class)
.hasMessageContaining("Error trying to resolve '--numbers list list list' using [--numbers 1 2]");
}
@Test
public void testUnresolvableArg() throws Exception {
StandardParameterResolver resolver = new StandardParameterResolver(new DefaultConversionService(),
Collections.emptySet());
Method method = findMethod(Remote.class, "zap", boolean.class, String.class, String.class, String.class);
assertThatThrownBy(() -> {
resolver.resolve(
Utils.createMethodParameter(method, 1),
asList("--foo hello --force --bar well".split(" ")));
}).isInstanceOf(ParameterMissingResolutionException.class)
.hasMessageContaining("Parameter '--name string' should be specified");
}
// Tests for completion
@Test
public void testParameterKeyNotYetSetAppearsInProposals() {
StandardParameterResolver resolver = new StandardParameterResolver(new DefaultConversionService(),
Collections.emptySet());
Method method = findMethod(Remote.class, "zap", boolean.class, String.class, String.class, String.class);
List<String> completions = resolver.complete(
Utils.createMethodParameter(method, 1),
contextFor("")
).stream().map(CompletionProposal::value).collect(Collectors.toList());
assertThat(completions).contains("--name");
completions = resolver.complete(
Utils.createMethodParameter(method, 1),
contextFor("--force ")
).stream().map(CompletionProposal::value).collect(Collectors.toList());
assertThat(completions).contains("--name");
}
@Test
public void testParameterKeyNotFullySpecified() {
StandardParameterResolver resolver = new StandardParameterResolver(new DefaultConversionService(),
Collections.emptySet());
Method method = findMethod(Remote.class, "zap", boolean.class, String.class, String.class, String.class);
List<String> completions = resolver.complete(
Utils.createMethodParameter(method, 1),
contextFor("--na")
).stream().map(CompletionProposal::value).collect(Collectors.toList());
assertThat(completions).contains("--name");
completions = resolver.complete(
Utils.createMethodParameter(method, 1),
contextFor("--force --na")
).stream().map(CompletionProposal::value).collect(Collectors.toList());
assertThat(completions).contains("--name");
}
@Test
public void testNoMoreAvailableParameters() {
StandardParameterResolver resolver = new StandardParameterResolver(new DefaultConversionService(),
Collections.emptySet());
Method method = findMethod(Remote.class, "zap", boolean.class, String.class, String.class, String.class);
List<String> completions = resolver.complete(
Utils.createMethodParameter(method, 2), // trying to complete --foo
contextFor("--name ") // but input is currently focused on --name
).stream().map(CompletionProposal::value).collect(Collectors.toList());
assertThat(completions).isEmpty();
}
@Test
public void testNotTheRightTimeToCompleteThatParameter() {
StandardParameterResolver resolver = new StandardParameterResolver(new DefaultConversionService(),
Collections.emptySet());
Method method = findMethod(Remote.class, "shutdown", Remote.Delay.class);
List<String> completions = resolver.complete(
Utils.createMethodParameter(method, 0),
contextFor("--delay 323")
).stream().map(CompletionProposal::value).collect(Collectors.toList());
assertThat(completions).isEmpty();
}
@Test
public void testValueCompletionWithNonDefaultArity() {
Set<ValueProvider> valueProviders = new HashSet<>();
valueProviders.add(new Remote.NumberValueProvider("12", "42", "7"));
StandardParameterResolver resolver = new StandardParameterResolver(new DefaultConversionService(),
valueProviders);
Method[] methods = {
findMethod(org.springframework.shell.standard.Remote.class, "add", List.class),
findMethod(org.springframework.shell.standard.Remote.class, "addAsArray", int[].class),
};
for (Method method : methods) {
List<String> completions = resolver
.complete(Utils.createMethodParameter(method, 0), contextFor("--numbers ")).stream()
.map(CompletionProposal::value).collect(Collectors.toList());
assertThat(completions).contains("12", "42", "7");
completions = resolver.complete(Utils.createMethodParameter(method, 0), contextFor("--numbers 42 "))
.stream().map(CompletionProposal::value).collect(Collectors.toList());
assertThat(completions).contains("12", "7");
completions = resolver.complete(Utils.createMethodParameter(method, 0), contextFor("--numbers 42 34 "))
.stream().map(CompletionProposal::value).collect(Collectors.toList());
assertThat(completions).contains("12", "7");
completions = resolver.complete(Utils.createMethodParameter(method, 0), contextFor("--numbers 42 34 66 "))
.stream().map(CompletionProposal::value).collect(Collectors.toList());
assertThat(completions).isEmpty(); // All 3 have already been set
}
}
private CompletionContext contextFor(String input) {
DefaultParser defaultParser = new DefaultParser();
ParsedLine parsed = defaultParser.parse(input, input.length());
List<String> words = parsed.words().stream().filter(w -> w.length() > 0).collect(Collectors.toList());
return new CompletionContext(words, parsed.wordIndex(), parsed.wordCursor());
}
}

View File

@@ -15,26 +15,15 @@
*/
package org.springframework.shell.standard.completion;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.springframework.core.convert.support.DefaultConversionService;
import org.springframework.core.io.DefaultResourceLoader;
import org.springframework.core.io.ResourceLoader;
import org.springframework.shell.CommandRegistry;
import org.springframework.shell.ConfigurableCommandRegistry;
import org.springframework.shell.MethodTarget;
import org.springframework.shell.ParameterResolver;
import org.springframework.shell.context.DefaultShellContext;
import org.springframework.shell.command.CommandCatalog;
import org.springframework.shell.command.CommandRegistration;
import org.springframework.shell.standard.ShellMethod;
import org.springframework.shell.standard.ShellOption;
import org.springframework.shell.standard.StandardParameterResolver;
import org.springframework.shell.standard.completion.AbstractCompletions.CommandModel;
import org.springframework.util.ReflectionUtils;
import static org.assertj.core.api.Assertions.assertThat;
@@ -43,30 +32,50 @@ public class AbstractCompletionsTests {
@Test
public void testBasicModelGeneration() {
DefaultResourceLoader resourceLoader = new DefaultResourceLoader();
ConfigurableCommandRegistry commandRegistry = new ConfigurableCommandRegistry(new DefaultShellContext());
List<ParameterResolver> parameterResolvers = new ArrayList<>();
StandardParameterResolver resolver = new StandardParameterResolver(new DefaultConversionService(),
Collections.emptySet());
parameterResolvers.add(resolver);
CommandCatalog commandCatalog = CommandCatalog.of();
TestCommands commands = new TestCommands();
Method method1 = ReflectionUtils.findMethod(TestCommands.class, "test1", String.class);
Method method2 = ReflectionUtils.findMethod(TestCommands.class, "test2");
Method method3 = ReflectionUtils.findMethod(TestCommands.class, "test3");
Method method4 = ReflectionUtils.findMethod(TestCommands.class, "test4", String.class);
CommandRegistration registration1 = CommandRegistration.builder()
.command("test1")
.withTarget()
.method(commands, "test1")
.and()
.withOption()
.longNames("param1")
.and()
.build();
MethodTarget methodTarget1 = new MethodTarget(method1, commands, "help");
MethodTarget methodTarget2 = new MethodTarget(method2, commands, "help");
MethodTarget methodTarget3 = new MethodTarget(method3, commands, "help");
MethodTarget methodTarget4 = new MethodTarget(method4, commands, "help");
CommandRegistration registration2 = CommandRegistration.builder()
.command("test2")
.withTarget()
.method(commands, "test2")
.and()
.build();
commandRegistry.register("test1", methodTarget1);
commandRegistry.register("test2", methodTarget2);
commandRegistry.register("test3", methodTarget3);
commandRegistry.register("test3 test4", methodTarget4);
CommandRegistration registration3 = CommandRegistration.builder()
.command("test3")
.withTarget()
.method(commands, "test3")
.and()
.build();
TestCompletions completions = new TestCompletions(resourceLoader, commandRegistry, parameterResolvers);
CommandRegistration registration4 = CommandRegistration.builder()
.command("test3", "test4")
.withTarget()
.method(commands, "test4")
.and()
.withOption()
.longNames("param4")
.and()
.build();
commandCatalog.register(registration1);
commandCatalog.register(registration2);
commandCatalog.register(registration3);
commandCatalog.register(registration4);
TestCompletions completions = new TestCompletions(resourceLoader, commandCatalog);
CommandModel commandModel = completions.testCommandModel();
assertThat(commandModel.getCommands()).hasSize(3);
assertThat(commandModel.getCommands().stream().map(c -> c.getMainCommand())).containsExactlyInAnyOrder("test1", "test2",
@@ -92,9 +101,8 @@ public class AbstractCompletionsTests {
@Test
public void testBuilder() {
DefaultResourceLoader resourceLoader = new DefaultResourceLoader();
ConfigurableCommandRegistry commandRegistry = new ConfigurableCommandRegistry(new DefaultShellContext());
List<ParameterResolver> parameterResolvers = new ArrayList<>();
TestCompletions completions = new TestCompletions(resourceLoader, commandRegistry, parameterResolvers);
CommandCatalog commandCatalog = CommandCatalog.of();
TestCompletions completions = new TestCompletions(resourceLoader, commandCatalog);
String result = completions.testBuilder()
.attribute("x", "command")
@@ -106,9 +114,8 @@ public class AbstractCompletionsTests {
private static class TestCompletions extends AbstractCompletions {
public TestCompletions(ResourceLoader resourceLoader, CommandRegistry commandRegistry,
List<ParameterResolver> parameterResolvers) {
super(resourceLoader, commandRegistry, parameterResolvers);
public TestCompletions(ResourceLoader resourceLoader, CommandCatalog commandCatalog) {
super(resourceLoader, commandCatalog);
}
CommandModel testCommandModel() {

View File

@@ -15,17 +15,16 @@
*/
package org.springframework.shell.standard.completion;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.shell.ConfigurableCommandRegistry;
import org.springframework.shell.ParameterResolver;
import org.springframework.shell.context.DefaultShellContext;
import org.springframework.shell.command.CommandCatalog;
import org.springframework.shell.command.CommandContext;
import org.springframework.shell.command.CommandRegistration;
import static org.assertj.core.api.Assertions.assertThat;
@@ -48,11 +47,71 @@ public class BashCompletionsTests {
}
@Test
public void testDoesNotError() {
ConfigurableCommandRegistry commandRegistry = new ConfigurableCommandRegistry(new DefaultShellContext());
List<ParameterResolver> parameterResolvers = new ArrayList<>();
BashCompletions completions = new BashCompletions(context, commandRegistry, parameterResolvers);
public void testNoCommands() {
CommandCatalog commandCatalog = CommandCatalog.of();
BashCompletions completions = new BashCompletions(context, commandCatalog);
String bash = completions.generate("root-command");
assertThat(bash).contains("root-command");
}
@Test
public void testCommandFromMethod() {
CommandCatalog commandCatalog = CommandCatalog.of();
registerFromMethod(commandCatalog);
BashCompletions completions = new BashCompletions(context, commandCatalog);
String bash = completions.generate("root-command");
System.out.println(bash);
assertThat(bash).contains("root-command");
assertThat(bash).contains("commands+=(\"testmethod1\")");
assertThat(bash).contains("_root-command_testmethod1()");
assertThat(bash).contains("two_word_flags+=(\"--arg1\")");
}
@Test
public void testCommandFromFunction() {
CommandCatalog commandCatalog = CommandCatalog.of();
registerFromFunction(commandCatalog, "testmethod1");
BashCompletions completions = new BashCompletions(context, commandCatalog);
String bash = completions.generate("root-command");
assertThat(bash).contains("root-command");
assertThat(bash).contains("commands+=(\"testmethod1\")");
assertThat(bash).contains("_root-command_testmethod1()");
assertThat(bash).contains("two_word_flags+=(\"--arg1\")");
}
private void registerFromMethod(CommandCatalog commandCatalog) {
Pojo1 pojo1 = new Pojo1();
CommandRegistration registration = CommandRegistration.builder()
.command("testmethod1")
.withTarget()
.method(pojo1, "method1")
.and()
.withOption()
.longNames("arg1")
.and()
.build();
commandCatalog.register(registration);
}
private void registerFromFunction(CommandCatalog commandCatalog, String command) {
Function<CommandContext, String> function = ctx -> {
String arg1 = ctx.getOptionValue("arg1");
return String.format("hi, arg1 value is '%s'", arg1);
};
CommandRegistration registration = CommandRegistration.builder()
.command(command)
.withTarget()
.function(function)
.and()
.withOption()
.longNames("arg1")
.and()
.build();
commandCatalog.register(registration);
}
protected static class Pojo1 {
void method1() {}
}
}