Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CLI: Close subcommands in MultiCommand #28954

Merged
merged 16 commits into from
Mar 15, 2018
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions server/cli/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ archivesBaseName = 'elasticsearch-cli'

dependencies {
compile 'net.sf.jopt-simple:jopt-simple:5.0.2'
compile "org.elasticsearch:elasticsearch-core:${version}"
}

test.enabled = false
Expand Down
10 changes: 10 additions & 0 deletions server/cli/src/main/java/org/elasticsearch/cli/MultiCommand.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,17 @@

package org.elasticsearch.cli;

import java.io.Closeable;
import java.io.IOException;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.Map;

import joptsimple.NonOptionArgumentSpec;
import joptsimple.OptionSet;

import org.elasticsearch.core.internal.io.IOUtils;

/**
* A cli tool which is made up of multiple subcommands.
*/
Expand Down Expand Up @@ -74,4 +78,10 @@ protected void execute(Terminal terminal, OptionSet options) throws Exception {
}
subcommand.mainWithoutErrorHandling(Arrays.copyOfRange(args, 1, args.length), terminal);
}

@Override
public void close() throws IOException {
IOUtils.close(subcommands.values());
}

}
47 changes: 47 additions & 0 deletions server/src/test/java/org/elasticsearch/cli/MultiCommandTests.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,22 +22,46 @@
import joptsimple.OptionSet;
import org.junit.Before;

import java.io.IOException;
import java.util.concurrent.atomic.AtomicBoolean;

public class MultiCommandTests extends CommandTestCase {

static class DummyMultiCommand extends MultiCommand {

AtomicBoolean closed = new AtomicBoolean();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

make this field final and add a line after for readability

DummyMultiCommand() {
super("A dummy multi command", () -> {});
}
@Override
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a personal preference but given that we're adding to this class, I find it hard to read without empty lines between methods/constructor/member fields. Can you please add them?

public void close() throws IOException {
super.close();
this.closed.compareAndSet(false, true);
}
}

static class DummySubCommand extends Command {
AtomicBoolean throwsExceptionOnClose = new AtomicBoolean();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why does this need to be an AtomicBoolean? I think it can just be a final boolean

AtomicBoolean closed = new AtomicBoolean();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

make this final

DummySubCommand() {
super("A dummy subcommand", () -> {});
}
DummySubCommand(boolean throwsExceptionOnClose) {
super("A dummy subcommand", () -> {});
this.throwsExceptionOnClose.compareAndSet(false, throwsExceptionOnClose);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

when you use compareAndSet you need to check the value returned by this method. In this case there is something really wrong if it returns false.

}
@Override
protected void execute(Terminal terminal, OptionSet options) throws Exception {
terminal.println("Arguments: " + options.nonOptionArguments().toString());
}
@Override
public void close() throws IOException {
if (throwsExceptionOnClose.get()) {
throw new IOException();
} else {
closed.compareAndSet(false, true);
}
}
}

DummyMultiCommand multiCommand;
Expand Down Expand Up @@ -102,4 +126,27 @@ public void testSubcommandArguments() throws Exception {
assertFalse(output, output.contains("command1"));
assertTrue(output, output.contains("Arguments: [foo, bar]"));
}

public void testClose() throws Exception {
DummySubCommand subCommand1 = new DummySubCommand();
DummySubCommand subCommand2 = new DummySubCommand();
multiCommand.subcommands.put("command1", subCommand1);
multiCommand.subcommands.put("command2", subCommand2);
multiCommand.close();
assertTrue("MultiCommand must have been closed when close method is invoked", multiCommand.closed.get());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for me must have been closed points me in the wrong direction; I'd use should have been or was not closed

assertTrue("SubCommand1 must have been closed when close method is invoked", subCommand1.closed.get());
assertTrue("SubCommand2 must have been closed when close method is invoked", subCommand2.closed.get());
}

public void testCloseWhenSubCommandCloseThrowsException() {
boolean throwExceptionWhenClosed = true;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be nice to test the other scenarios or add some randomization to this test in terms of which subcommand(s) throws an exception:

final boolean command1Throws = randomBoolean();
final boolean command2Throws = randomBoolean();
...
assertTrue(command1Throws ^ subCommand1.closed.get());
assertTrue(command2Throws ^ subCommand2.closed.get());

DummySubCommand subCommand1 = new DummySubCommand(throwExceptionWhenClosed);
DummySubCommand subCommand2 = new DummySubCommand();
multiCommand.subcommands.put("command1", subCommand1);
multiCommand.subcommands.put("command2", subCommand2);
// verify exception is thrown, as well as other non failed sub-commands closed
// properly.
expectThrows(IOException.class, () -> multiCommand.close());
assertTrue("SubCommand2 must have been closed when close method is invoked", subCommand2.closed.get());
}
}