/*- * -\-\- * Helios Tools * -- * Copyright (C) 2016 Spotify AB * -- * 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 com.spotify.helios.cli.command; import static com.google.common.util.concurrent.Futures.immediateFuture; import static com.spotify.helios.common.descriptors.HostStatus.Status.DOWN; import static com.spotify.helios.common.descriptors.HostStatus.Status.UP; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertThat; import static org.mockito.Matchers.anyMapOf; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.util.concurrent.Futures; import com.spotify.helios.cli.TestUtils; import com.spotify.helios.client.HeliosClient; import com.spotify.helios.common.descriptors.AgentInfo; import com.spotify.helios.common.descriptors.Deployment; import com.spotify.helios.common.descriptors.DockerVersion; import com.spotify.helios.common.descriptors.Goal; import com.spotify.helios.common.descriptors.HostInfo; import com.spotify.helios.common.descriptors.HostStatus; import com.spotify.helios.common.descriptors.Job; import com.spotify.helios.common.descriptors.JobId; import com.spotify.helios.common.descriptors.TaskStatus; import java.io.ByteArrayOutputStream; import java.io.PrintStream; import java.text.ParseException; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import net.sourceforge.argparse4j.ArgumentParsers; import net.sourceforge.argparse4j.inf.ArgumentParser; import net.sourceforge.argparse4j.inf.ArgumentParserException; import net.sourceforge.argparse4j.inf.Namespace; import net.sourceforge.argparse4j.inf.Subparser; import org.junit.Before; import org.junit.Test; public class HostListCommandTest { private final HeliosClient client = mock(HeliosClient.class); private final ByteArrayOutputStream baos = new ByteArrayOutputStream(); private final PrintStream out = new PrintStream(baos); private static final String JOB_NAME = "job"; private static final String JOB_VERSION1 = "1-aaa"; private static final String JOB_VERSION2 = "3-ccc"; private static final String JOB_VERSION3 = "2-bbb"; private static final JobId JOB_ID1 = new JobId(JOB_NAME, JOB_VERSION1); private static final JobId JOB_ID2 = new JobId(JOB_NAME, JOB_VERSION2); private static final JobId JOB_ID3 = new JobId(JOB_NAME, JOB_VERSION3); private static final Job JOB1 = Job.newBuilder().setName(JOB_NAME).setVersion(JOB_VERSION1).build(); private static final Job JOB2 = Job.newBuilder().setName(JOB_NAME).setVersion(JOB_VERSION2).build(); private static final Job JOB3 = Job.newBuilder().setName(JOB_NAME).setVersion(JOB_VERSION3).build(); private static final Map<JobId, Deployment> JOBS = ImmutableMap.of( JOB_ID1, Deployment.newBuilder().build(), JOB_ID2, Deployment.newBuilder().build(), JOB_ID3, Deployment.newBuilder().build() ); private static final Map<JobId, TaskStatus> JOB_STATUSES = ImmutableMap.of( JOB_ID1, TaskStatus.newBuilder().setJob(JOB1).setGoal(Goal.START) .setState(TaskStatus.State.RUNNING).build(), JOB_ID2, TaskStatus.newBuilder().setJob(JOB2).setGoal(Goal.START) .setState(TaskStatus.State.RUNNING).build(), JOB_ID3, TaskStatus.newBuilder().setJob(JOB3).setGoal(Goal.START) .setState(TaskStatus.State.RUNNING).build() ); private static final Map<String, String> LABELS = ImmutableMap.of("foo", "bar", "baz", "qux"); private static final List<String> EXPECTED_ORDER = ImmutableList.of("host1.", "host2.", "host3."); private HostStatus upStatus; private HostStatus downStatus; @Before public void setUp() throws ParseException { // purposefully in non-sorted order so that tests that verify that the output is sorted are // actually testing HostListCommand behavior and not accidentally testing what value the // mock returns final List<String> hosts = ImmutableList.of("host3.", "host1.", "host2."); when(client.listHosts()).thenReturn(immediateFuture(hosts)); final HostInfo hostInfo = HostInfo.newBuilder() .setCpus(4) .setMemoryTotalBytes((long) Math.pow(1024, 3)) .setMemoryFreeBytes(500000000) .setLoadAvg(0.1) .setOsName("OS foo") .setOsVersion("0.1.0") .setDockerVersion(DockerVersion.builder().version("1.7.0").apiVersion("1.18").build()) .build(); final long dayMilliseconds = TimeUnit.MILLISECONDS.convert(1, TimeUnit.DAYS); final long startTime = System.currentTimeMillis() - 2 * dayMilliseconds; final AgentInfo agentInfo = AgentInfo.newBuilder() .setVersion("0.8.420") .setUptime(dayMilliseconds) .setStartTime(startTime) .build(); upStatus = HostStatus.newBuilder() .setJobs(JOBS) .setStatuses(JOB_STATUSES) .setStatus(UP) .setHostInfo(hostInfo) .setAgentInfo(agentInfo) .setLabels(LABELS) .build(); downStatus = HostStatus.newBuilder() .setJobs(JOBS) .setStatuses(JOB_STATUSES) .setStatus(DOWN) .setHostInfo(hostInfo) .setAgentInfo(agentInfo) .setLabels(LABELS) .build(); final Map<String, HostStatus> statuses = ImmutableMap.of( "host3.", downStatus, "host1.", upStatus, "host2.", upStatus ); when(client.hostStatuses(eq(hosts), anyMapOf(String.class, String.class))) .thenReturn(immediateFuture(statuses)); } @Test public void testCommand() throws Exception { final int ret = runCommand(); final String output = baos.toString(); assertEquals(0, ret); assertThat(output, containsString( "HOST STATUS DEPLOYED RUNNING CPUS MEM LOAD AVG MEM USAGE " + "OS HELIOS DOCKER LABELS")); assertThat(output, containsString( "host1. Up 2 days 3 3 4 1 gb 0.10 0.53 " + "OS foo 0.1.0 0.8.420 1.7.0 (1.18) foo=bar, baz=qux")); assertThat(output, containsString( "host2. Up 2 days 3 3 4 1 gb 0.10 0.53 " + "OS foo 0.1.0 0.8.420 1.7.0 (1.18) foo=bar, baz=qux")); assertThat(output, containsString( "host3. Down 1 day 3 3 4 1 gb 0.10 0.53 " + "OS foo 0.1.0 0.8.420 1.7.0 (1.18) foo=bar, baz=qux")); } private int runCommand(String... commandArgs) throws ExecutionException, InterruptedException, ArgumentParserException { final String[] args = new String[1 + commandArgs.length]; args[0] = "hosts"; System.arraycopy(commandArgs, 0, args, 1, commandArgs.length); // use a real, dummy Subparser impl to avoid having to mock out every single call final ArgumentParser parser = ArgumentParsers.newArgumentParser("test"); final Subparser subparser = parser.addSubparsers().addParser("hosts"); final HostListCommand command = new HostListCommand(subparser); final Namespace options = parser.parseArgs(args); return command.run(options, client, out, false, null); } @Test public void testQuietOutputIsSorted() throws Exception { final int ret = runCommand("-q"); assertEquals(0, ret); assertEquals(EXPECTED_ORDER, TestUtils.readFirstColumnFromOutput(baos.toString(), false)); } @Test public void testNonQuietOutputIsSorted() throws Exception { final int ret = runCommand(); assertEquals(0, ret); assertEquals(EXPECTED_ORDER, TestUtils.readFirstColumnFromOutput(baos.toString(), true)); } @Test(expected = ArgumentParserException.class) public void testInvalidStatusThrowsError() throws Exception { runCommand("--status", "DWN"); } @Test public void testPatternFilter() throws Exception { final String hostname = "host1.example.com"; final List<String> hosts = ImmutableList.of(hostname); when(client.listHosts("host1")).thenReturn(Futures.immediateFuture(hosts)); final Map<String, HostStatus> statusResponse = ImmutableMap.of(hostname, upStatus); when(client.hostStatuses(eq(hosts), anyMapOf(String.class, String.class))) .thenReturn(Futures.immediateFuture(statusResponse)); final int ret = runCommand("host1"); assertEquals(0, ret); assertEquals(ImmutableList.of("HOST", hostname + "."), TestUtils.readFirstColumnFromOutput(baos.toString(), false)); } @Test public void testSelectorFilter() throws Exception { final String hostname = "foo1.example.com"; final List<String> hosts = ImmutableList.of(hostname); when(client.listHosts(ImmutableSet.of("foo=bar"))).thenReturn(Futures.immediateFuture(hosts)); final Map<String, HostStatus> statusResponse = ImmutableMap.of(hostname, upStatus); when(client.hostStatuses(eq(hosts), anyMapOf(String.class, String.class))) .thenReturn(Futures.immediateFuture(statusResponse)); final int ret = runCommand("--selector", "foo=bar"); assertEquals(0, ret); assertEquals(ImmutableList.of("HOST", hostname + "."), TestUtils.readFirstColumnFromOutput(baos.toString(), false)); } /** * Verify that the configuration of the '--selector' argument does not cause positional arguments * specified after the optional argument to be greedily parsed. This verifies a fix for a bug * where `nargs("+")` was used where it was not necessary, which causes a command like `--selector * foo=bar pattern` to be interpreted as two selectors of `foo=bar` and `pattern`. */ @Test public void testSelectorSlurping() throws Exception { final List<String> hosts = ImmutableList.of("host-1"); when(client.hostStatuses(eq(hosts), anyMapOf(String.class, String.class))) .thenReturn(Futures.immediateFuture(Collections.<String, HostStatus>emptyMap())); when(client.listHosts("blah", ImmutableSet.of("foo=bar"))) .thenReturn(Futures.immediateFuture(hosts)); assertThat(runCommand("-s", "foo=bar", "blah"), equalTo(0)); assertThat(runCommand("blah", "-s", "foo=bar"), equalTo(0)); when(client.listHosts("blarp", ImmutableSet.of("a=b", "z=1"))) .thenReturn(Futures.immediateFuture(hosts)); assertThat(runCommand("-s", "a=b", "-s", "z=1", "blarp"), equalTo(0)); assertThat(runCommand("blarp", "--selector", "a=b", "--selector", "z=1"), equalTo(0)); } }