mirror of https://github.com/hyperledger/besu
NC-1737 Enabled warning on CLI dependent options (#679)
fixes NC-1737 by adding an option dependency mechanism for "enabled" options This mechanism is aimed at preventing the use of options if the associated service is disabled. This is not a generic system as our need is simple here. But in future version of PicoCLI some options dependency mechanism may be implemented that could replace this. See https://github.com/remkop/picocli/issues/295 Our current mechanism apply to p2p, rpc (http+ws), mining, privacy and metrics. It logs a warning wherever some options are used that require another one to be enabled. Also renamed inconsistent isRpcHttpEnabled option variable and moved --privacy-enabled option to be on top of other privacy options. injects a Logger in PantheonCommand constructor to log and also provides injection capabilities for a mock in tests. adds unit tests updates tests logger name to TEST_LOGGER to prevent confusion with the mock remove --max-trailing-peers after merge with master where it was deleted updated rules for metrics Signed-off-by: Adrian Sutton <adrian.sutton@consensys.net>pull/2/head
parent
82a771fa60
commit
a7ad81da35
@ -0,0 +1,71 @@ |
||||
/* |
||||
* Copyright 2019 ConsenSys AG. |
||||
* |
||||
* 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 |
||||
* |
||||
* http://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 tech.pegasys.pantheon.cli; |
||||
|
||||
import tech.pegasys.pantheon.util.StringUtils; |
||||
|
||||
import java.util.Arrays; |
||||
import java.util.List; |
||||
import java.util.stream.Collectors; |
||||
|
||||
import org.apache.logging.log4j.Logger; |
||||
import picocli.CommandLine; |
||||
|
||||
class CommandLineUtils { |
||||
/** |
||||
* Check if options are passed that require an option to be true to have any effect and log a |
||||
* warning with the list of affected options. |
||||
* |
||||
* <p>Note that in future version of PicoCLI some options dependency mechanism may be implemented |
||||
* that could replace this. See https://github.com/remkop/picocli/issues/295
|
||||
* |
||||
* @param logger the logger instance used to log the warning |
||||
* @param commandLine the command line containing the options we want to check |
||||
* @param mainOptionName the name of the main option to test dependency against. Only used for |
||||
* display. |
||||
* @param isMainOptionCondition the condition to test the options dependencies, if true will test |
||||
* if not won't |
||||
* @param dependentOptionsNames a list of option names that can't be used if condition is met. |
||||
* Example: if --miner-coinbase is in the list and condition is that --miner-enabled should |
||||
* not be false, we log a warning. |
||||
*/ |
||||
static void checkOptionDependencies( |
||||
final Logger logger, |
||||
final CommandLine commandLine, |
||||
final String mainOptionName, |
||||
final boolean isMainOptionCondition, |
||||
final List<String> dependentOptionsNames) { |
||||
if (isMainOptionCondition) { |
||||
String affectedOptions = |
||||
commandLine |
||||
.getCommandSpec() |
||||
.options() |
||||
.stream() |
||||
.filter( |
||||
option -> |
||||
Arrays.stream(option.names()).anyMatch(dependentOptionsNames::contains) |
||||
&& !option.stringValues().isEmpty()) |
||||
.map(option -> option.names()[0]) |
||||
.collect( |
||||
Collectors.collectingAndThen( |
||||
Collectors.toList(), StringUtils.joiningWithLastDelimiter(", ", " and "))); |
||||
|
||||
if (!affectedOptions.isEmpty()) { |
||||
logger.warn( |
||||
"{} will have no effect unless {} is defined on the command line.", |
||||
affectedOptions, |
||||
mainOptionName); |
||||
} |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,38 @@ |
||||
/* |
||||
* Copyright 2019 ConsenSys AG. |
||||
* |
||||
* 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 |
||||
* |
||||
* http://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 tech.pegasys.pantheon.util; |
||||
|
||||
import java.util.List; |
||||
import java.util.function.Function; |
||||
|
||||
/** some useful tools to display strings in command line help or error messages */ |
||||
public class StringUtils { |
||||
|
||||
/** |
||||
* Joins a list into string elements with a delimiter but having a last different delimiter |
||||
* Example: "this thing, that thing and this other thing" |
||||
* |
||||
* @param delimiter delimiter for all the items except before the last one |
||||
* @param lastDelimiter delimiter before the last item |
||||
* @return a delimited string representation of the list |
||||
*/ |
||||
public static Function<List<String>, String> joiningWithLastDelimiter( |
||||
final String delimiter, final String lastDelimiter) { |
||||
return list -> { |
||||
final int last = list.size() - 1; |
||||
if (last < 1) return String.join(delimiter, list); |
||||
return String.join( |
||||
lastDelimiter, String.join(delimiter, list.subList(0, last)), list.get(last)); |
||||
}; |
||||
} |
||||
} |
@ -0,0 +1,186 @@ |
||||
/* |
||||
* Copyright 2019 ConsenSys AG. |
||||
* |
||||
* 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 |
||||
* |
||||
* http://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 tech.pegasys.pantheon.cli; |
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat; |
||||
import static org.mockito.Mockito.verify; |
||||
import static org.mockito.Mockito.verifyNoMoreInteractions; |
||||
import static picocli.CommandLine.defaultExceptionHandler; |
||||
|
||||
import java.util.ArrayList; |
||||
import java.util.Arrays; |
||||
|
||||
import org.apache.logging.log4j.Logger; |
||||
import org.junit.Test; |
||||
import org.junit.runner.RunWith; |
||||
import org.mockito.ArgumentCaptor; |
||||
import org.mockito.Mock; |
||||
import org.mockito.junit.MockitoJUnitRunner; |
||||
import picocli.CommandLine; |
||||
import picocli.CommandLine.Command; |
||||
import picocli.CommandLine.Option; |
||||
import picocli.CommandLine.RunLast; |
||||
|
||||
@RunWith(MockitoJUnitRunner.class) |
||||
public class CommandLineUtilsTest { |
||||
@Mock Logger mockLogger; |
||||
|
||||
@Command(description = "This command is for testing.", name = "testcommand") |
||||
private abstract static class AbstractTestCommand implements Runnable { |
||||
|
||||
final Logger logger; |
||||
final CommandLine commandLine; |
||||
|
||||
AbstractTestCommand(final Logger logger) { |
||||
this.logger = logger; |
||||
commandLine = new CommandLine(this); |
||||
} |
||||
|
||||
// Completely disables p2p within Pantheon.
|
||||
@Option( |
||||
names = {"--option-enabled"}, |
||||
arity = "1" |
||||
) |
||||
final Boolean optionEnabled = true; |
||||
|
||||
@Option(names = {"--option2"}) |
||||
private final Integer option2 = 2; |
||||
|
||||
@Option(names = {"--option3"}) |
||||
private final Integer option3 = 3; |
||||
|
||||
@Option(names = {"--option4"}) |
||||
private final Integer option4 = 4; |
||||
} |
||||
|
||||
private static class TestCommandWithDeps extends AbstractTestCommand { |
||||
|
||||
TestCommandWithDeps(final Logger logger) { |
||||
super(logger); |
||||
} |
||||
|
||||
@Override |
||||
public void run() { |
||||
// Check that mining options are able top work or send an error
|
||||
CommandLineUtils.checkOptionDependencies( |
||||
logger, |
||||
commandLine, |
||||
"--option-enabled", |
||||
!optionEnabled, |
||||
Arrays.asList("--option2", "--option3")); |
||||
} |
||||
} |
||||
|
||||
private static class TestCommandWithoutDeps extends AbstractTestCommand { |
||||
|
||||
TestCommandWithoutDeps(final Logger logger) { |
||||
super(logger); |
||||
} |
||||
|
||||
@Override |
||||
public void run() { |
||||
// Check that mining options are able top work or send an error
|
||||
CommandLineUtils.checkOptionDependencies( |
||||
logger, commandLine, "--option-enabled", !optionEnabled, new ArrayList<>()); |
||||
} |
||||
} |
||||
|
||||
@Test |
||||
public void optionsAreNotExpected() { |
||||
AbstractTestCommand testCommand = new TestCommandWithDeps(mockLogger); |
||||
testCommand.commandLine.parseWithHandlers( |
||||
new RunLast(), |
||||
defaultExceptionHandler(), |
||||
"--option-enabled", |
||||
"false", |
||||
"--option2", |
||||
"20", |
||||
"--option3", |
||||
"30", |
||||
"--option4", |
||||
"40"); |
||||
verifyOptionsConstraintLoggerCall(mockLogger, "--option2 and --option3", "--option-enabled"); |
||||
} |
||||
|
||||
@Test |
||||
public void optionIsNotExpected() { |
||||
AbstractTestCommand testCommand = new TestCommandWithDeps(mockLogger); |
||||
testCommand.commandLine.parseWithHandlers( |
||||
new RunLast(), |
||||
defaultExceptionHandler(), |
||||
"--option-enabled", |
||||
"false", |
||||
"--option2", |
||||
"20", |
||||
"--option4", |
||||
"40"); |
||||
verifyOptionsConstraintLoggerCall(mockLogger, "--option2", "--option-enabled"); |
||||
} |
||||
|
||||
@Test |
||||
public void optionsAreExpected() { |
||||
AbstractTestCommand testCommand = new TestCommandWithDeps(mockLogger); |
||||
testCommand.commandLine.parseWithHandlers( |
||||
new RunLast(), |
||||
defaultExceptionHandler(), |
||||
"--option2", |
||||
"20", |
||||
"--option3", |
||||
"30", |
||||
"--option4", |
||||
"40"); |
||||
verifyNoMoreInteractions(mockLogger); |
||||
} |
||||
|
||||
@Test |
||||
public void noDependencies() { |
||||
AbstractTestCommand testCommand = new TestCommandWithoutDeps(mockLogger); |
||||
testCommand.commandLine.parseWithHandlers( |
||||
new RunLast(), |
||||
defaultExceptionHandler(), |
||||
"--option-enabled", |
||||
"false", |
||||
"--option2", |
||||
"20", |
||||
"--option3", |
||||
"30", |
||||
"--option4", |
||||
"40"); |
||||
verifyNoMoreInteractions(mockLogger); |
||||
} |
||||
|
||||
/** |
||||
* Check logger calls |
||||
* |
||||
* <p>Here we check the calls to logger and not the result of the log line as we don't test the |
||||
* logger itself but the fact that we call it. |
||||
* |
||||
* @param dependentOptions the string representing the list of dependent options names |
||||
* @param mainOption the main option name |
||||
*/ |
||||
private void verifyOptionsConstraintLoggerCall( |
||||
final Logger logger, final String dependentOptions, final String mainOption) { |
||||
|
||||
ArgumentCaptor<String> stringArgumentCaptor = ArgumentCaptor.forClass(String.class); |
||||
|
||||
verify(logger) |
||||
.warn( |
||||
stringArgumentCaptor.capture(), |
||||
stringArgumentCaptor.capture(), |
||||
stringArgumentCaptor.capture()); |
||||
assertThat(stringArgumentCaptor.getAllValues().get(0)) |
||||
.isEqualTo("{} will have no effect unless {} is defined on the command line."); |
||||
assertThat(stringArgumentCaptor.getAllValues().get(1)).isEqualTo(dependentOptions); |
||||
assertThat(stringArgumentCaptor.getAllValues().get(2)).isEqualTo(mainOption); |
||||
} |
||||
} |
@ -0,0 +1,47 @@ |
||||
/* |
||||
* Copyright 2019 ConsenSys AG. |
||||
* |
||||
* 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 |
||||
* |
||||
* http://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 tech.pegasys.pantheon.util; |
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat; |
||||
|
||||
import java.util.Arrays; |
||||
import java.util.HashMap; |
||||
import java.util.List; |
||||
import java.util.Map; |
||||
import java.util.Map.Entry; |
||||
import java.util.stream.Collectors; |
||||
|
||||
import org.junit.Test; |
||||
|
||||
public class StringUtilsTest { |
||||
|
||||
@Test |
||||
public void joiningWithLastDelimiter() { |
||||
Map<List<String>, String> testCases = new HashMap<>(); |
||||
//noinspection ArraysAsListWithZeroOrOneArgument
|
||||
testCases.put(Arrays.asList("item1"), "item1"); |
||||
testCases.put(Arrays.asList("item1", "item2"), "item1 and item2"); |
||||
testCases.put(Arrays.asList("item1", "item2", "item3"), "item1, item2 and item3"); |
||||
|
||||
for (Entry<List<String>, String> entry : testCases.entrySet()) { |
||||
String joinedResult = |
||||
entry |
||||
.getKey() |
||||
.stream() |
||||
.collect( |
||||
Collectors.collectingAndThen( |
||||
Collectors.toList(), StringUtils.joiningWithLastDelimiter(", ", " and "))); |
||||
assertThat(joinedResult).isEqualTo(entry.getValue()); |
||||
} |
||||
} |
||||
} |
Loading…
Reference in new issue