view vm-byteman/client-cli/src/test/java/com/redhat/thermostat/vm/byteman/client/cli/BytemanControlCommandTest.java @ 2590:3e1ccd4a2d51

Properly handle spaces in paths. Reviewed-by: stooke, neugens Review-thread: http://icedtea.classpath.org/pipermail/thermostat/2017-February/022176.html
author Severin Gehwolf <sgehwolf@redhat.com>
date Thu, 09 Feb 2017 12:28:11 +0100
parents e7fd8adb5f9c
children
line wrap: on
line source

/*
 * Copyright 2012-2017 Red Hat, Inc.
 *
 * This file is part of Thermostat.
 *
 * Thermostat is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published
 * by the Free Software Foundation; either version 2, or (at your
 * option) any later version.
 *
 * Thermostat is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Thermostat; see the file COPYING.  If not see
 * <http://www.gnu.org/licenses/>.
 *
 * Linking this code with other modules is making a combined work
 * based on this code.  Thus, the terms and conditions of the GNU
 * General Public License cover the whole combination.
 *
 * As a special exception, the copyright holders of this code give
 * you permission to link this code with independent modules to
 * produce an executable, regardless of the license terms of these
 * independent modules, and to copy and distribute the resulting
 * executable under terms of your choice, provided that you also
 * meet, for each linked independent module, the terms and conditions
 * of the license of that module.  An independent module is a module
 * which is not derived from or based on this code.  If you modify
 * this code, you may extend this exception to your version of the
 * library, but you are not obligated to do so.  If you do not wish
 * to do so, delete this exception statement from your version.
 */

package com.redhat.thermostat.vm.byteman.client.cli;

import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.instanceOf;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.not;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.junit.matchers.JUnitMatchers.containsString;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.InetSocketAddress;
import java.net.URL;
import java.net.URLDecoder;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.TimeZone;
import java.util.concurrent.CountDownLatch;

import com.redhat.thermostat.common.cli.InvalidSubcommandException;
import com.redhat.thermostat.common.cli.SubcommandExpectedException;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import org.mockito.ArgumentCaptor;

import com.redhat.thermostat.client.command.RequestQueue;
import com.redhat.thermostat.common.cli.Arguments;
import com.redhat.thermostat.common.cli.CliCommandOption;
import com.redhat.thermostat.common.cli.CommandContext;
import com.redhat.thermostat.common.cli.CommandException;
import com.redhat.thermostat.common.cli.FileNameTabCompleter;
import com.redhat.thermostat.common.cli.TabCompleter;
import com.redhat.thermostat.common.command.Request;
import com.redhat.thermostat.common.command.RequestResponseListener;
import com.redhat.thermostat.common.internal.test.TestCommandContextFactory;
import com.redhat.thermostat.common.model.Range;
import com.redhat.thermostat.common.utils.StreamUtils;
import com.redhat.thermostat.storage.core.AgentId;
import com.redhat.thermostat.storage.core.VmId;
import com.redhat.thermostat.storage.dao.AgentInfoDAO;
import com.redhat.thermostat.storage.dao.VmInfoDAO;
import com.redhat.thermostat.storage.model.AgentInformation;
import com.redhat.thermostat.storage.model.VmInfo;
import com.redhat.thermostat.vm.byteman.common.BytemanMetric;
import com.redhat.thermostat.vm.byteman.common.VmBytemanDAO;
import com.redhat.thermostat.vm.byteman.common.VmBytemanStatus;
import com.redhat.thermostat.vm.byteman.common.command.BytemanRequest;
import com.redhat.thermostat.vm.byteman.common.command.BytemanRequest.RequestAction;
import com.redhat.thermostat.vm.byteman.common.command.BytemanRequestResponseListener;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;

public class BytemanControlCommandTest {

    private static final String RULE_OPTION = "rules";
    private static final String STATUS_ACTION = "status";
    private static final String SHOW_METRICS_ACTION = "show-metrics";
    private static final String UNLOAD_ACTION = "unload";
    private static final String LOAD_ACTION = "load";
    private static final String SOME_VM_ID = "some-vm-id";
    private static final String SOME_AGENT_ID = "some-agent-id";
    private static final String EMPTY_STRING = "";
    private static final int SOME_LISTEN_PORT = 333;
    private static final InetSocketAddress REQUEST_QUEUE_ADDRESS = mock(InetSocketAddress.class);
    private BytemanControlCommand command;
    private TestCommandContextFactory ctxFactory;
    private static TimeZone defaultTimeZone;
    private static Locale defaultLocale;

    @Before
    public void setup() {
        command = new BytemanControlCommand() {
            @Override
            void waitWithTimeout(CountDownLatch latch) {
                // return immediately
            }
        };
        VmInfoDAO vmInfoDAO = mock(VmInfoDAO.class);
        VmInfo vmInfo = new VmInfo();
        vmInfo.setAgentId(SOME_AGENT_ID);
        vmInfo.setVmId(SOME_VM_ID);
        when(vmInfoDAO.getVmInfo(any(VmId.class))).thenReturn(vmInfo);
        command.bindVmInfoDao(vmInfoDAO);
        AgentInfoDAO agentInfoDAO = mock(AgentInfoDAO.class);
        AgentInformation agentInfo = mock(AgentInformation.class);
        when(agentInfo.isAlive()).thenReturn(true);
        when(agentInfo.getRequestQueueAddress()).thenReturn(REQUEST_QUEUE_ADDRESS);
        when(agentInfoDAO.getAgentInformation(any(AgentId.class))).thenReturn(agentInfo);
        command.bindAgentInfoDao(agentInfoDAO);
        command.bindVmBytemanDao(mock(VmBytemanDAO.class));
        ctxFactory = new TestCommandContextFactory();
    }

    @BeforeClass
    public static void setUpBeforeClass() {
        defaultTimeZone = TimeZone.getDefault();
        TimeZone.setDefault(TimeZone.getTimeZone("EST"));
        defaultLocale = Locale.getDefault(Locale.Category.FORMAT);
        Locale.setDefault(Locale.Category.FORMAT, Locale.CANADA);
    }

    @AfterClass
    public static void tearDownAfterClass() {
        TimeZone.setDefault(defaultTimeZone);
        Locale.setDefault(Locale.Category.FORMAT, defaultLocale);
    }
    
    @Test
    public void testUnknownAction() {
        String unknownAction = "some-action-that-doesn't-exist";
        try {
            Arguments args = getBasicArgsWithAction(unknownAction);
            CommandContext ctx = ctxFactory.createContext(args);
            command.run(ctx);
            fail("Expected failure due to unknown action");
        } catch (CommandException e) {
            String msg = e.getMessage();
            assertThat(msg, containsString(unknownAction));
            assertTrue(msg.startsWith("Invalid subcommand:"));
        }
    }
    
    @Test
    public void testStatusActionNoRule() throws CommandException {
        String rule = null;
        String expectedLoadedRuleMsg = "<no-loaded-rules>";
        VmBytemanStatus status = new VmBytemanStatus();
        status.setVmId(SOME_VM_ID);
        status.setAgentId(SOME_AGENT_ID);
        status.setListenPort(SOME_LISTEN_PORT);
        status.setRule(rule);
        basicStatusActionTest(rule, expectedLoadedRuleMsg, status);
    }
    
    @Test
    public void testStatusActionNotAttached() throws CommandException {
        String rule = null;
        String expectedLoadedRuleMsg = "<no-loaded-rules>";
        basicStatusActionTest(rule, expectedLoadedRuleMsg, null);
    }
    
    private void basicStatusActionTest(String rule, String expectedRuleMsg, VmBytemanStatus status) throws CommandException {
        VmBytemanDAO dao = mock(VmBytemanDAO.class);
        when(dao.findBytemanStatus(eq(new VmId(SOME_VM_ID)))).thenReturn(status);
        Arguments args = getBasicArgsWithAction(STATUS_ACTION);
        CommandContext ctx = ctxFactory.createContext(args);
        command.unbindVmBytemanDao(dao);
        command.bindVmBytemanDao(dao);
        command.run(ctx);
        String stdErr = ctxFactory.getError();
        String stdOut = ctxFactory.getOutput();
        assertEquals(EMPTY_STRING, stdErr);
        assertTrue(stdOut.contains("Byteman status for VM:"));
        String listenPort = "<unset>";
        if (status != null) {
            listenPort = Integer.toString(status.getListenPort());
        }
        assertTrue(stdOut.contains("Byteman agent listen port: " + listenPort));
        assertTrue(stdOut.contains("Loaded rules:"));
        assertTrue(stdOut.contains(expectedRuleMsg));
    }
    
    @Test
    public void testStatusActionWithRule() throws CommandException {
        String rule = "some-rule-string\nsome more rule lines\nfurther more";
        VmBytemanStatus status = new VmBytemanStatus();
        status.setVmId(SOME_VM_ID);
        status.setAgentId(SOME_AGENT_ID);
        status.setListenPort(SOME_LISTEN_PORT);
        status.setRule(rule);
        basicStatusActionTest(rule, rule, status);
    }
    
    @Test
    public void testAgentNotAlive() throws CommandException {
        AgentInfoDAO agentInfoDAO = mock(AgentInfoDAO.class);
        AgentInformation agentInfo = mock(AgentInformation.class);
        when(agentInfo.isAlive()).thenReturn(false);
        when(agentInfoDAO.getAgentInformation(any(AgentId.class))).thenReturn(agentInfo);
        command.unbindAgentInfoDao(agentInfoDAO);
        command.bindAgentInfoDao(agentInfoDAO);
        Arguments args = getBasicArgsWithAction("no-matter");
        CommandContext ctx = ctxFactory.createContext(args);
        try {
            command.run(ctx);
            fail("Command should have thrown exception");
        } catch (CommandException e) {
            String msg = e.getMessage();
            assertEquals("Agent with id " + SOME_AGENT_ID + " is not alive.", msg);
        }
    }
    
    @Test
    public void testShowMetricsActionNoMetrics() throws CommandException {
        String expectedStdOut = "No metrics available for VM " + SOME_VM_ID + ".\n";
        List<BytemanMetric> returnedList = Collections.emptyList();
        doShowMetricsTest(returnedList, expectedStdOut);
    }
    
    @Test
    public void testShowMetricsActionWithMetrics() throws CommandException {
        String metricData1 = "{ \"foo\": \"bar\" }";
        String metricData2 = "{ \"foo2\": -300 }";
        long timestamp = 1_234_567_890_111L;
        String expectedStdOut = "Currently viewing metrics for: all\n" +
            "Available metrics: [foo, foo2]\n" +
            "+----------------------------+---------+------+--------+\n" +
            "| Timestamp                  | Marker  | Name | Value  |\n" +
            "+----------------------------+---------+------+--------+\n" +
            "| 13-Feb-2009 6:31:30 EST PM | marker1 | foo  | bar    |\n" +
            "+----------------------------+---------+------+--------+\n" +
            "| 13-Feb-2009 6:31:30 EST PM | marker2 | foo2 | -300.0 |\n" +
            "+----------------------------+---------+------+--------+\n";
        BytemanMetric metric1 = new BytemanMetric();
        metric1.setMarker("marker1");
        metric1.setData(metricData1);
        metric1.setTimeStamp(timestamp);
        BytemanMetric metric2 = new BytemanMetric();
        metric2.setMarker("marker2");
        metric2.setData(metricData2);
        metric2.setTimeStamp(timestamp);
        List<BytemanMetric> returnedList = Arrays.asList(metric1, metric2);
        doShowMetricsTest(returnedList, expectedStdOut);
    }
    
    @Test
    public void testUnloadAction() throws CommandException {
        VmBytemanDAO dao = mock(VmBytemanDAO.class);
        VmBytemanStatus status = new VmBytemanStatus();
        int listenPort = 333;
        status.setVmId(SOME_VM_ID);
        status.setAgentId(SOME_AGENT_ID);
        status.setListenPort(listenPort);
        status.setRule(null);
        when(dao.findBytemanStatus(eq(new VmId(SOME_VM_ID)))).thenReturn(status);
        Arguments args = getBasicArgsWithAction(UNLOAD_ACTION);
        CommandContext ctx = ctxFactory.createContext(args);
        command.unbindVmBytemanDao(dao);
        command.bindVmBytemanDao(dao);
        RequestQueue rQueue = mock(RequestQueue.class);
        ArgumentCaptor<Request> requestCaptor = ArgumentCaptor.forClass(Request.class);
        command.bindRequestQueue(rQueue);
        command.run(ctx);
        verify(rQueue).putRequest(requestCaptor.capture());
        Request submittedRequest = requestCaptor.getValue();
        assertEquals(1, submittedRequest.getListeners().size());
        RequestResponseListener respListener = null; 
        for (RequestResponseListener l: submittedRequest.getListeners()) {
            respListener = l;
            break;
        }
        assertTrue(respListener instanceof BytemanRequestResponseListener);
        String rawAction = submittedRequest.getParameter(BytemanRequest.ACTION_PARAM_NAME);
        RequestAction actualAction = RequestAction.fromIntString(rawAction);
        assertEquals(RequestAction.UNLOAD_RULES, actualAction);
        assertEquals(Integer.toString(listenPort), submittedRequest.getParameter(BytemanRequest.LISTEN_PORT_PARAM_NAME));
        assertEquals(SOME_VM_ID, submittedRequest.getParameter(BytemanRequest.VM_ID_PARAM_NAME));
        assertNull(submittedRequest.getParameter(BytemanRequest.RULE_PARAM_NAME));
        assertSame(REQUEST_QUEUE_ADDRESS, submittedRequest.getTarget());
    }
    
    @Test
    public void testLoadActionNoRuleFile() throws CommandException {
        VmBytemanDAO dao = mock(VmBytemanDAO.class);
        VmBytemanStatus status = new VmBytemanStatus();
        int listenPort = 333;
        status.setVmId(SOME_VM_ID);
        status.setAgentId(SOME_AGENT_ID);
        status.setListenPort(listenPort);
        status.setRule(null);
        when(dao.findBytemanStatus(eq(new VmId(SOME_VM_ID)))).thenReturn(status);
        Arguments args = getBasicArgsWithAction(LOAD_ACTION); // rule file arg missing
        CommandContext ctx = ctxFactory.createContext(args);
        command.unbindVmBytemanDao(dao);
        command.bindVmBytemanDao(dao);
        RequestQueue rQueue = mock(RequestQueue.class);
        command.bindRequestQueue(rQueue);
        try {
            command.run(ctx);
            fail("Expected cmd exception due to missing rule argument");
        } catch (CommandException e) {
            String msg = e.getMessage();
            assertEquals("No rule option specified.", msg);
        }
    }
    
    @Test
    public void testLoadActionRuleFileDoesNotExist() throws CommandException {
        VmBytemanDAO dao = mock(VmBytemanDAO.class);
        VmBytemanStatus status = new VmBytemanStatus();
        int listenPort = 333;
        status.setVmId(SOME_VM_ID);
        status.setAgentId(SOME_AGENT_ID);
        status.setListenPort(listenPort);
        status.setRule(null);
        when(dao.findBytemanStatus(eq(new VmId(SOME_VM_ID)))).thenReturn(status);
        Arguments args = getBasicArgsWithAction(LOAD_ACTION);
        when(args.hasArgument(RULE_OPTION)).thenReturn(true);
        String file = "i-do-not-exist";
        when(args.getArgument(RULE_OPTION)).thenReturn(file);
        CommandContext ctx = ctxFactory.createContext(args);
        command.unbindVmBytemanDao(dao);
        command.bindVmBytemanDao(dao);
        RequestQueue rQueue = mock(RequestQueue.class);
        command.bindRequestQueue(rQueue);
        try {
            command.run(ctx);
            fail("Expected cmd exception due to rule file not existing");
        } catch (CommandException e) {
            String msg = e.getMessage();
            assertEquals("Specified rules file '" + file + "' not found.", msg);
        }
    }
    
    @Test
    public void testLoadActionSuccessAgentAttached() throws CommandException, FileNotFoundException, IOException {
        VmBytemanDAO dao = mock(VmBytemanDAO.class);
        VmBytemanStatus status = new VmBytemanStatus();
        int listenPort = 333;
        status.setVmId(SOME_VM_ID);
        status.setAgentId(SOME_AGENT_ID);
        status.setListenPort(listenPort);
        status.setRule(null);
        when(dao.findBytemanStatus(eq(new VmId(SOME_VM_ID)))).thenReturn(status);
        Arguments args = getBasicArgsWithAction(LOAD_ACTION);
        when(args.hasArgument(RULE_OPTION)).thenReturn(true);
        String file = decodeFilePath(getClass().getResource("/testRule.btm"));
        when(args.getArgument(RULE_OPTION)).thenReturn(file);
        CommandContext ctx = ctxFactory.createContext(args);
        command.unbindVmBytemanDao(dao);
        command.bindVmBytemanDao(dao);
        RequestQueue rQueue = mock(RequestQueue.class);
        command.bindRequestQueue(rQueue);
        ArgumentCaptor<Request> requestCaptor = ArgumentCaptor.forClass(Request.class);
        command.run(ctx);
        verify(rQueue).putRequest(requestCaptor.capture());
        Request submittedRequest = requestCaptor.getValue();
        assertEquals(1, submittedRequest.getListeners().size());
        RequestResponseListener respListener = null; 
        for (RequestResponseListener l: submittedRequest.getListeners()) {
            respListener = l;
            break;
        }
        assertTrue(respListener instanceof BytemanRequestResponseListener);
        String rawAction = submittedRequest.getParameter(BytemanRequest.ACTION_PARAM_NAME);
        RequestAction actualAction = RequestAction.fromIntString(rawAction);
        assertEquals(RequestAction.LOAD_RULES, actualAction);
        assertEquals(Integer.toString(listenPort), submittedRequest.getParameter(BytemanRequest.LISTEN_PORT_PARAM_NAME));
        assertEquals(SOME_VM_ID, submittedRequest.getParameter(BytemanRequest.VM_ID_PARAM_NAME));
        String expectedRule = new String(StreamUtils.readAll(new FileInputStream(new File(file))));
        assertEquals(expectedRule, submittedRequest.getParameter(BytemanRequest.RULE_PARAM_NAME));
        assertSame(REQUEST_QUEUE_ADDRESS, submittedRequest.getTarget());
        String out = ctxFactory.getOutput();
        assertEquals("Request submitted successfully.\n", out);
    }
    
    @Test
    public void testLoadActionSuccessAgentNotAttached() throws CommandException, FileNotFoundException, IOException {
        VmBytemanDAO dao = mock(VmBytemanDAO.class);
        // mimic no-agent-attached, by returning a null status
        when(dao.findBytemanStatus(eq(new VmId(SOME_VM_ID)))).thenReturn(null);
        Arguments args = getBasicArgsWithAction(LOAD_ACTION);
        when(args.hasArgument(RULE_OPTION)).thenReturn(true);
        String file = decodeFilePath(getClass().getResource("/testRule.btm"));
        when(args.getArgument(RULE_OPTION)).thenReturn(file);
        CommandContext ctx = ctxFactory.createContext(args);
        command.unbindVmBytemanDao(dao);
        command.bindVmBytemanDao(dao);
        RequestQueue rQueue = mock(RequestQueue.class);
        command.bindRequestQueue(rQueue);
        ArgumentCaptor<Request> requestCaptor = ArgumentCaptor.forClass(Request.class);
        command.run(ctx);
        verify(rQueue).putRequest(requestCaptor.capture());
        Request submittedRequest = requestCaptor.getValue();
        assertEquals(1, submittedRequest.getListeners().size());
        RequestResponseListener respListener = null; 
        for (RequestResponseListener l: submittedRequest.getListeners()) {
            respListener = l;
            break;
        }
        assertTrue(respListener instanceof BytemanRequestResponseListener);
        String rawAction = submittedRequest.getParameter(BytemanRequest.ACTION_PARAM_NAME);
        RequestAction actualAction = RequestAction.fromIntString(rawAction);
        assertEquals(RequestAction.LOAD_RULES, actualAction);
        assertEquals(Integer.toString(BytemanRequest.NOT_ATTACHED_PORT), submittedRequest.getParameter(BytemanRequest.LISTEN_PORT_PARAM_NAME));
        assertEquals(SOME_VM_ID, submittedRequest.getParameter(BytemanRequest.VM_ID_PARAM_NAME));
        String expectedRule = new String(StreamUtils.readAll(new FileInputStream(new File(file))));
        assertEquals(expectedRule, submittedRequest.getParameter(BytemanRequest.RULE_PARAM_NAME));
        assertSame(REQUEST_QUEUE_ADDRESS, submittedRequest.getTarget());
        String out = ctxFactory.getOutput();
        assertEquals("Request submitted successfully.\n", out);
    }

    @Test
    public void testProvidesRulesFileCompletion() {
        FileNameTabCompleter fileNameTabCompleter = mock(FileNameTabCompleter.class);
        command.bindFileNameTabCompleter(fileNameTabCompleter);
        Map<CliCommandOption, ? extends TabCompleter> completerMap = command.getOptionCompleters();
        assertThat(completerMap.size(), is(1));
        assertThat(completerMap.keySet(), is(equalTo(Collections.singleton(BytemanControlCommand.RULES_OPTION))));
        assertThat(BytemanControlCommand.RULES_OPTION.getLongOpt(), is("rules"));
        assertThat(BytemanControlCommand.RULES_OPTION.getOpt(), is("r"));
        assertThat(completerMap.get(BytemanControlCommand.RULES_OPTION), is(not(equalTo(null))));
        assertThat(completerMap.get(BytemanControlCommand.RULES_OPTION), is(instanceOf(FileNameTabCompleter.class)));
    }

    @Test
    public void testProvidesNoCompletionsIfFileNameTabCompleterIsUnavailable() {
        FileNameTabCompleter fileNameTabCompleter = mock(FileNameTabCompleter.class);
        command.unbindFileNameTabCompleter(fileNameTabCompleter);
        Map<CliCommandOption, ? extends TabCompleter> completerMap = command.getOptionCompleters();
        assertThat(completerMap.size(), is(0));
    }
    
    @SuppressWarnings("unchecked")
    private void doShowMetricsTest(List<BytemanMetric> metricsToReturn, String stdOutExpected) throws CommandException {
        VmBytemanDAO dao = mock(VmBytemanDAO.class);
        VmBytemanStatus status = new VmBytemanStatus();
        int listenPort = 333;
        status.setVmId(SOME_VM_ID);
        status.setAgentId(SOME_AGENT_ID);
        status.setListenPort(listenPort);
        status.setRule(null);
        when(dao.findBytemanStatus(eq(new VmId(SOME_VM_ID)))).thenReturn(status);
        when(dao.findBytemanMetrics(any(Range.class), any(VmId.class), any(AgentId.class))).thenReturn(metricsToReturn);
        Arguments args = getBasicArgsWithAction(SHOW_METRICS_ACTION);
        CommandContext ctx = ctxFactory.createContext(args);
        command.unbindVmBytemanDao(dao);
        command.bindVmBytemanDao(dao);
        command.run(ctx);
        String stdErr = ctxFactory.getError();
        String stdOut = ctxFactory.getOutput();
        assertEquals(EMPTY_STRING, stdErr);
        assertEquals(stdOutExpected, stdOut);
    }

    private Arguments getBasicArgsWithAction(final String action) throws SubcommandExpectedException, InvalidSubcommandException {
        final Arguments args = mock(Arguments.class);
        when(args.getArgument("vmId")).thenReturn(SOME_VM_ID);
        when(args.getNonOptionArguments()).thenReturn(Collections.singletonList(action));
        when(args.getSubcommand()).thenAnswer(new Answer<String>() {
            @Override
            public String answer(InvocationOnMock invocationOnMock) throws Throwable {
                List<String> options = Arrays.asList(BytemanControlCommand.SHOW_ACTION, BytemanControlCommand.STATUS_ACTION,
                        BytemanControlCommand.UNLOAD_RULE_ACTION, BytemanControlCommand.INJECT_RULE_ACTION);
                if (!options.contains(action)) {
                    throw new InvalidSubcommandException(action);
                }
                return action;
            }
        });
        return args;
    }

    private static String decodeFilePath(URL url) {
        try {
            // Spaces are encoded as %20 in URLs. Use URLDecoder.decode() so
            // as to handle cases like that.
            return URLDecoder.decode(url.getFile(), "UTF-8");
        } catch (UnsupportedEncodingException e) {
            throw new AssertionError("UTF-8 not supported, huh?");
        }
    }
}