/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you 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 org.apache.brooklyn.cli; import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertFalse; import static org.testng.Assert.assertTrue; import static org.testng.Assert.fail; import groovy.lang.GroovyClassLoader; import io.airlift.command.Cli; import io.airlift.command.Command; import io.airlift.command.ParseException; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.InputStream; import java.io.PrintStream; import java.util.Collection; import java.util.List; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; import java.util.regex.Pattern; import org.apache.brooklyn.api.entity.Entity; import org.apache.brooklyn.api.entity.ImplementedBy; import org.apache.brooklyn.api.location.Location; import org.apache.brooklyn.cli.AbstractMain.BrooklynCommand; import org.apache.brooklyn.cli.AbstractMain.BrooklynCommandCollectingArgs; import org.apache.brooklyn.cli.AbstractMain.DefaultInfoCommand; import org.apache.brooklyn.cli.AbstractMain.HelpCommand; import org.apache.brooklyn.cli.Main.AppShutdownHandler; import org.apache.brooklyn.cli.Main.GeneratePasswordCommand; import org.apache.brooklyn.cli.Main.LaunchCommand; import org.apache.brooklyn.core.entity.AbstractApplication; import org.apache.brooklyn.core.entity.AbstractEntity; import org.apache.brooklyn.core.entity.Entities; import org.apache.brooklyn.core.entity.StartableApplication; import org.apache.brooklyn.core.entity.factory.ApplicationBuilder; import org.apache.brooklyn.core.entity.trait.Startable; import org.apache.brooklyn.core.location.SimulatedLocation; import org.apache.brooklyn.core.objs.proxy.EntityProxy; import org.apache.brooklyn.core.test.entity.LocalManagementContextForTests; import org.apache.brooklyn.test.Asserts; import org.apache.brooklyn.util.collections.MutableMap; import org.apache.brooklyn.util.core.ResourceUtils; import org.apache.brooklyn.util.exceptions.Exceptions; import org.apache.brooklyn.util.exceptions.FatalConfigurationRuntimeException; import org.apache.brooklyn.util.exceptions.UserFacingException; import org.apache.brooklyn.util.time.Duration; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.testng.Assert; import org.testng.annotations.AfterMethod; import org.testng.annotations.BeforeMethod; import org.testng.annotations.Test; import com.google.common.base.Splitter; import com.google.common.collect.ImmutableList; import com.google.common.collect.Iterables; import com.google.common.io.Files; public class CliTest { private static final Logger LOG = LoggerFactory.getLogger(CliTest.class); // See testInvokeGroovyScript test for usage public static final AtomicBoolean GROOVY_INVOKED = new AtomicBoolean(false); private ExecutorService executor; private StartableApplication app; private static volatile ExampleEntity exampleEntity; // static so that they can be set from the static classes ExampleApp and ExampleEntity private static volatile boolean exampleAppRunning; private static volatile boolean exampleAppConstructed; private static volatile boolean exampleEntityRunning; @BeforeMethod(alwaysRun=true) public void setUp() throws Exception { executor = Executors.newCachedThreadPool(); exampleAppConstructed = false; exampleAppRunning = false; exampleEntityRunning = false; } @AfterMethod(alwaysRun=true) public void tearDown() throws Exception { if (executor != null) executor.shutdownNow(); if (app != null) Entities.destroyAll(app.getManagementContext()); if (exampleEntity != null && exampleEntity.getApplication() != null) Entities.destroyAll(exampleEntity.getApplication().getManagementContext()); } @Test public void testLoadApplicationFromClasspath() throws Exception { String appName = ExampleApp.class.getName(); Object appBuilder = loadApplicationFromClasspathOrParse(appName); assertTrue(appBuilder instanceof ApplicationBuilder, "app="+appBuilder); assertAppWrappedInBuilder((ApplicationBuilder)appBuilder, ExampleApp.class.getCanonicalName()); } @Test public void testLoadApplicationBuilderFromClasspath() throws Exception { String appName = ExampleAppBuilder.class.getName(); Object appBuilder = loadApplicationFromClasspathOrParse(appName); assertTrue(appBuilder instanceof ExampleAppBuilder, "app="+appBuilder); } @Test public void testLoadEntityFromClasspath() throws Exception { String entityName = ExampleEntity.class.getName(); Object appBuilder = loadApplicationFromClasspathOrParse(entityName); assertTrue(appBuilder instanceof ApplicationBuilder, "app="+appBuilder); app = ((ApplicationBuilder)appBuilder).manage(); Collection<Entity> entities = app.getChildren(); assertEquals(entities.size(), 1, "entities="+entities); assertTrue(Iterables.getOnlyElement(entities) instanceof ExampleEntity, "entities="+entities+"; ifs="+Iterables.getOnlyElement(entities).getClass().getInterfaces()); assertTrue(Iterables.getOnlyElement(entities) instanceof EntityProxy, "entities="+entities); } @Deprecated // Tests deprecated approach of using impl directly @Test public void testLoadEntityImplFromClasspath() throws Exception { String entityName = ExampleEntityImpl.class.getName(); Object appBuilder = loadApplicationFromClasspathOrParse(entityName); assertTrue(appBuilder instanceof ApplicationBuilder, "app="+appBuilder); app = ((ApplicationBuilder)appBuilder).manage(); Collection<Entity> entities = app.getChildren(); assertEquals(entities.size(), 1, "entities="+entities); assertEquals(Iterables.getOnlyElement(entities).getEntityType().getName(), ExampleEntity.class.getCanonicalName(), "entities="+entities); assertTrue(Iterables.getOnlyElement(entities) instanceof EntityProxy, "entities="+entities); } @Test public void testLoadApplicationByParsingGroovyFile() throws Exception { String appName = "ExampleAppInFile.groovy"; // file found in src/test/resources (contains empty app) Object appBuilder = loadApplicationFromClasspathOrParse(appName); assertTrue(appBuilder instanceof ApplicationBuilder, "app="+appBuilder); assertAppWrappedInBuilder((ApplicationBuilder)appBuilder, "ExampleAppInFile"); } private Object loadApplicationFromClasspathOrParse(String appName) throws Exception { LaunchCommand launchCommand = new Main.LaunchCommand(); ResourceUtils resourceUtils = ResourceUtils.create(this); GroovyClassLoader loader = new GroovyClassLoader(CliTest.class.getClassLoader()); return launchCommand.loadApplicationFromClasspathOrParse(resourceUtils, loader, appName); } private void assertAppWrappedInBuilder(ApplicationBuilder builder, String expectedAppTypeName) { StartableApplication app = builder.manage(); try { String typeName = app.getEntityType().getName(); assertEquals(typeName, expectedAppTypeName, "app="+app+"; typeName="+typeName); } finally { Entities.destroyAll(app.getManagementContext()); } } @Test public void testInvokeGroovyScript() throws Exception { File groovyFile = File.createTempFile("testinvokegroovy", "groovy"); try { String contents = CliTest.class.getCanonicalName()+".GROOVY_INVOKED.set(true);"; Files.write(contents.getBytes(), groovyFile); LaunchCommand launchCommand = new Main.LaunchCommand(); ResourceUtils resourceUtils = ResourceUtils.create(this); GroovyClassLoader loader = new GroovyClassLoader(CliTest.class.getClassLoader()); launchCommand.execGroovyScript(resourceUtils, loader, groovyFile.toURI().toString()); assertTrue(GROOVY_INVOKED.get()); } finally { groovyFile.delete(); GROOVY_INVOKED.set(false); } } @Test public void testStopAllApplications() throws Exception { LaunchCommand launchCommand = new Main.LaunchCommand(); ExampleApp app = new ExampleApp(); try { Entities.startManagement(app); app.start(ImmutableList.of(new SimulatedLocation())); assertTrue(app.running); launchCommand.stopAllApps(ImmutableList.of(app)); assertFalse(app.running); } finally { Entities.destroyAll(app.getManagementContext()); } } @Test public void testWaitsForInterrupt() throws Exception { final AppShutdownHandler listener = new AppShutdownHandler(); Thread t = new Thread(new Runnable() { @Override public void run() { listener.waitOnShutdownRequest(); }}); t.start(); t.join(100); assertTrue(t.isAlive()); t.interrupt(); t.join(10*1000); assertFalse(t.isAlive()); } protected Cli<BrooklynCommand> buildCli() { return new Main().cliBuilder().build(); } @Test public void testLaunchCommandParsesArgs() throws ParseException { BrooklynCommand command = buildCli().parse("launch", "--app", "my.App", "--location", "localhost", "--port", "1234", "--bindAddress", "myhostname", "--noConsole", "--noConsoleSecurity", "--stopOnKeyPress", "--localBrooklynProperties", "/path/to/myprops", LaunchCommand.PERSIST_OPTION, LaunchCommand.PERSIST_OPTION_REBIND, "--persistenceDir", "/path/to/mypersist", LaunchCommand.HA_OPTION, LaunchCommand.HA_OPTION_STANDBY); assertTrue(command instanceof LaunchCommand, ""+command); String details = command.toString(); assertTrue(details.contains("app=my.App"), details); assertTrue(details.contains("script=null"), details); assertTrue(details.contains("location=localhost"), details); assertTrue(details.contains("port=1234"), details); assertTrue(details.contains("bindAddress=myhostname"), details); assertTrue(details.contains("noConsole=true"), details); assertTrue(details.contains("noConsoleSecurity=true"), details); assertTrue(details.contains("stopOnKeyPress=true"), details); assertTrue(details.contains("localBrooklynProperties=/path/to/myprops"), details); assertTrue(details.contains("persist=rebind"), details); assertTrue(details.contains("persistenceDir=/path/to/mypersist"), details); assertTrue(details.contains("highAvailability=standby"), details); } @Test public void testLaunchCommandUsesDefaults() throws ParseException { BrooklynCommand command = buildCli().parse("launch"); assertTrue(command instanceof LaunchCommand, ""+command); String details = command.toString(); assertTrue(details.contains("app=null"), details); assertTrue(details.contains("script=null"), details); assertTrue(details.contains("location=null"), details); assertTrue(details.contains("port=null"), details); assertTrue(details.contains("noConsole=false"), details); assertTrue(details.contains("noConsoleSecurity=false"), details); assertTrue(details.contains("stopWhichAppsOnShutdown=theseIfNotPersisted"), details); assertTrue(details.contains("stopOnKeyPress=false"), details); assertTrue(details.contains("localBrooklynProperties=null"), details); assertTrue(details.contains("persist=disabled"), details); assertTrue(details.contains("persistenceDir=null"), details); assertTrue(details.contains("highAvailability=auto"), details); } @Test public void testLaunchCommandComplainsWithInvalidArgs() { Cli<BrooklynCommand> cli = buildCli(); try { BrooklynCommand command = cli.parse("launch", "invalid"); command.call(); Assert.fail("Should have thrown exception; instead got "+command); } catch (ParseException e) { /* expected */ } catch (Exception e) { throw Exceptions.propagate(e); } } @Test public void testAppOptionIsOptional() throws ParseException { Cli<BrooklynCommand> cli = buildCli(); cli.parse("launch", "blah", "my.App"); } @Test public void testHelpCommand() { Cli<BrooklynCommand> cli = buildCli(); BrooklynCommand command = cli.parse("help"); assertTrue(command instanceof HelpCommand, "Command is: "+command); } @Test public void testDefaultInfoCommand() { Cli<BrooklynCommand> cli = buildCli(); BrooklynCommand command = cli.parse(); assertTrue(command instanceof DefaultInfoCommand, "Command is: "+command); } @Test public void testCliSystemPropertyDefines() { Cli<BrooklynCommand> cli = buildCli(); BrooklynCommand command0 = cli.parse( "-Dorg.apache.brooklyn.cli.CliTest.sample1=foo", "-Dorg.apache.brooklyn.cli.CliTest.sample2=bar", "launch", "-Dorg.apache.brooklyn.cli.CliTest.sample3=baz" ); assertTrue(command0 instanceof LaunchCommand); LaunchCommand command = (LaunchCommand) command0; assertEquals(command.getDefines().size(), 3, "Command is: "+command); assertTrue(command.getDefines().get(0).equals("org.apache.brooklyn.cli.CliTest.sample1=foo"), "Command is: "+command); assertTrue(command.getDefines().get(2).equals("org.apache.brooklyn.cli.CliTest.sample3=baz"), "Command is: "+command); assertEquals(command.getDefinesAsMap().get("org.apache.brooklyn.cli.CliTest.sample3"), "baz", "Command is: "+command); } @Test public void testLaunchWillStartAppWhenGivenImpl() throws Exception { Cli<BrooklynCommand> cli = buildCli(); BrooklynCommand command = cli.parse("launch", "--noConsole", "--app", ExampleApp.class.getName(), "--location", "localhost"); submitCommandAndAssertRunnableSucceeds(command, new Runnable() { public void run() { assertTrue(exampleAppConstructed); assertTrue(exampleAppRunning); } }); } @Test public void testLaunchStartsYamlApp() throws Exception { Cli<BrooklynCommand> cli = buildCli(); BrooklynCommand command = cli.parse("launch", "--noConsole", "--app", "example-app-no-location.yaml", "--location", "localhost"); submitCommandAndAssertRunnableSucceeds(command, new Runnable() { public void run() { assertTrue(exampleEntityRunning); } }); } @Test public void testLaunchStartsYamlAppWithCommandLineLocation() throws Exception { Cli<BrooklynCommand> cli = buildCli(); BrooklynCommand command = cli.parse("launch", "--noConsole", "--app", "example-app-no-location.yaml", "--location", "localhost:(name=testLocalhost)"); submitCommandAndAssertRunnableSucceeds(command, new Runnable() { public void run() { assertTrue(exampleEntityRunning); assertTrue(Iterables.getOnlyElement(exampleEntity.getApplication().getLocations()).getDisplayName().equals("testLocalhost")); } }); } @Test public void testLaunchStartsYamlAppWithYamlAppLocation() throws Exception { Cli<BrooklynCommand> cli = buildCli(); BrooklynCommand command = cli.parse("launch", "--noConsole", "--app", "example-app-app-location.yaml"); submitCommandAndAssertRunnableSucceeds(command, new Runnable() { public void run() { assertTrue(exampleEntityRunning); assertTrue(Iterables.getOnlyElement(exampleEntity.getApplication().getLocations()).getDisplayName().equals("appLocalhost")); } }); } @Test public void testLaunchStartsYamlAppWithYamlAndAppCliLocation() throws Exception { Cli<BrooklynCommand> cli = buildCli(); BrooklynCommand command = cli.parse("launch", "--noConsole", "--app", "example-app-app-location.yaml", "--location", "localhost"); submitCommandAndAssertRunnableSucceeds(command, new Runnable() { public void run() { assertTrue(exampleEntityRunning); assertTrue(Iterables.getFirst(exampleEntity.getApplication().getLocations(), null).getDisplayName().equals("appLocalhost")); } }); } @Test public void testGeneratePasswordCommandParsed() throws Exception { Cli<BrooklynCommand> cli = buildCli(); BrooklynCommand command = cli.parse("generate-password", "--user", "myname"); assertTrue(command instanceof GeneratePasswordCommand); } @Test public void testGeneratePasswordFromStdin() throws Exception { List<String> stdoutLines = runCommand(ImmutableList.of("generate-password", "--user", "myname", "--stdin"), "mypassword\nmypassword\n"); System.out.println(stdoutLines); } @Test public void testGeneratePasswordFailsIfPasswordsDontMatch() throws Throwable { Throwable exception = runCommandExpectingException(ImmutableList.of("generate-password", "--user", "myname", "--stdin"), "mypassword\ndifferentpassword\n"); if (exception instanceof UserFacingException && exception.toString().contains("Passwords did not match")) { // success } else { throw new Exception(exception); } } @Test public void testGeneratePasswordFailsIfNoConsole() throws Throwable { Throwable exception = runCommandExpectingException(ImmutableList.of("generate-password", "--user", "myname"), ""); if (exception instanceof FatalConfigurationRuntimeException && exception.toString().contains("No console")) { // success } else { throw new Exception(exception); } } @Test public void testGeneratePasswordFailsIfPasswordBlank() throws Throwable { Throwable exception = runCommandExpectingException(ImmutableList.of("generate-password", "--user", "myname", "--stdin"), "\n\n"); if (exception instanceof UserFacingException && exception.toString().contains("Password must not be blank")) { // success } else { throw new Exception(exception); } } @Test public void testInfoShowsDefaultBanner() throws Exception { List<String> stdoutLines = runCommand(ImmutableList.of("info"), ""); for (String line : Splitter.on("\n").split(Main.DEFAULT_BANNER)) { assertTrue(stdoutLines.contains(line), "out="+stdoutLines); } } @Test public void testInfoSupportsCustomizedBanner() throws Exception { String origBanner = Main.banner; String origBannerFirstLine = Iterables.get(Splitter.on("\n").split(Main.DEFAULT_BANNER), 0); try { String customBanner = "My Custom Banner"; Main.banner = customBanner; List<String> stdoutLines = runCommand(ImmutableList.of("info"), ""); assertTrue(stdoutLines.contains(customBanner), "out="+stdoutLines); assertFalse(stdoutLines.contains(origBannerFirstLine), "out="+stdoutLines); } finally { Main.banner = origBanner; } } @Test public void testCanCustomiseInfoCommand() throws Exception { Main main = new Main() { protected Class<? extends BrooklynCommand> cliInfoCommand() { return CustomInfoCommand.class; } }; List<String> stdoutLines = runCommand(main.cliBuilder().build(), ImmutableList.of("info"), ""); assertTrue(stdoutLines.contains("My Custom Info"), "out="+stdoutLines); } @Command(name = "info", description = "Display information about brooklyn") public static class CustomInfoCommand extends BrooklynCommandCollectingArgs { @Override public Void call() throws Exception { System.out.println("My Custom Info"); return null; } } @Test public void testCanCustomiseLaunchCommand() throws Exception { Main main = new Main() { protected Class<? extends BrooklynCommand> cliLaunchCommand() { return CustomLaunchCommand.class; } }; List<String> stdoutLines = runCommand(main.cliBuilder().build(), ImmutableList.of("launch"), ""); assertTrue(stdoutLines.contains("My Custom Launch"), "out="+stdoutLines); } @Command(name = "launch", description = "Starts a server, optionally with applications") public static class CustomLaunchCommand extends BrooklynCommandCollectingArgs { @Override public Void call() throws Exception { System.out.println("My Custom Launch"); return null; } } protected Throwable runCommandExpectingException(Iterable<String> args, String input) throws Exception { try { List<String> stdout = runCommand(args, input); fail("Expected exception, but got stdout="+stdout); return null; } catch (ExecutionException e) { return e.getCause(); } } protected List<String> runCommand(Iterable<String> args, String input) throws Exception { Cli<BrooklynCommand> cli = buildCli(); return runCommand(cli, args, input); } protected List<String> runCommand(Cli<BrooklynCommand> cli, Iterable<String> args, String input) throws Exception { final BrooklynCommand command = cli.parse(args); final AtomicReference<Exception> exception = new AtomicReference<Exception>(); Thread t= new Thread(new Runnable() { public void run() { try { command.call(); } catch (Exception e) { exception.set(e); throw Exceptions.propagate(e); } }}); InputStream origIn = System.in; PrintStream origOut = System.out; try { InputStream stdin = new ByteArrayInputStream(input.getBytes()); System.setIn(stdin); ByteArrayOutputStream stdoutBytes = new ByteArrayOutputStream(); PrintStream stdout = new PrintStream(stdoutBytes); System.setOut(stdout); t.start(); t.join(10*1000); assertFalse(t.isAlive()); if (exception.get() != null) { throw new ExecutionException(exception.get()); } return ImmutableList.copyOf(Splitter.on(Pattern.compile("\r?\n")).split(new String(stdoutBytes.toByteArray()))); } finally { System.setIn(origIn); System.setOut(origOut); t.interrupt(); } } private void submitCommandAndAssertRunnableSucceeds(final BrooklynCommand command, Runnable runnable) { if (command instanceof LaunchCommand) { ((LaunchCommand)command).useManagementContext(new LocalManagementContextForTests()); } executor.submit(new Callable<Void>() { public Void call() throws Exception { try { LOG.info("Calling command: "+command); command.call(); return null; } catch (Throwable t) { LOG.error("Error executing command: "+t, t); throw Exceptions.propagate(t); } }}); Asserts.succeedsEventually(MutableMap.of("timeout", Duration.ONE_MINUTE), runnable); } // An empty app to be used for testing public static class ExampleApp extends AbstractApplication { volatile boolean running; volatile boolean constructed; @Override public void init() { super.init(); constructed = true; exampleAppConstructed = true; } @Override public void start(Collection<? extends Location> locations) { super.start(locations); running = true; exampleAppRunning = true; } @Override public void stop() { super.stop(); running = false; exampleAppRunning = false; } } // An empty entity to be used for testing @ImplementedBy(ExampleEntityImpl.class) public static interface ExampleEntity extends Entity, Startable { } public static class ExampleEntityImpl extends AbstractEntity implements ExampleEntity { public ExampleEntityImpl() { super(); exampleEntity = this; } @Override public void start(Collection<? extends Location> locations) { exampleEntityRunning = true; } @Override public void stop() { exampleEntityRunning = false; } @Override public void restart() { } } // An empty app builder to be used for testing public static class ExampleAppBuilder extends ApplicationBuilder { @Override protected void doBuild() { // no-op } } }