/*
* Copyright (c) 2009-present the original author or authors.
*
* 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.planet57.gshell.testharness;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import com.google.inject.Guice;
import com.google.inject.Injector;
import com.google.inject.Module;
import com.google.inject.Stage;
import com.planet57.gshell.branding.Branding;
import com.planet57.gshell.command.Command;
import com.planet57.gshell.command.CommandAction;
import com.planet57.gshell.command.CommandRegistry;
import com.planet57.gshell.command.CommandRegistryImpl;
import com.planet57.gshell.help.HelpPageManagerImpl;
import com.planet57.gshell.internal.BeanContainer;
import com.planet57.gshell.logging.logback.TargetConsoleAppender;
import com.planet57.gshell.shell.Shell;
import com.planet57.gshell.shell.ShellBuilder;
import com.planet57.gshell.variables.Variables;
import com.planet57.gshell.variables.VariablesSupport;
import org.apache.felix.gogo.runtime.threadio.ThreadIOImpl;
import org.apache.felix.service.threadio.ThreadIO;
import org.eclipse.sisu.space.BeanScanning;
import org.eclipse.sisu.space.SpaceModule;
import org.eclipse.sisu.space.URLClassSpace;
import org.eclipse.sisu.wire.WireModule;
import org.fusesource.jansi.Ansi;
import org.jline.terminal.Terminal;
import org.jline.terminal.TerminalBuilder;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.bridge.SLF4JBridgeHandler;
import org.sonatype.goodies.testsupport.TestTracer;
import org.sonatype.goodies.testsupport.TestUtil;
import javax.annotation.Nonnull;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.nullValue;
import static org.junit.Assert.assertThat;
// FIXME: Goodies TestSupport initMocks() is causing some issues in mvnsh
/**
* Support for testing {@link CommandAction} instances.
*
* @since 3.0
*/
public abstract class CommandTestSupport
{
static {
SLF4JBridgeHandler.removeHandlersForRootLogger();
SLF4JBridgeHandler.install();
Ansi.setEnabled(false);
}
protected final TestUtil util = new TestUtil(getClass());
protected final Logger log = LoggerFactory.getLogger(getClass());
/**
* The name of the command under-test.
*/
private final String name;
@Rule
public final TestTracer tracer = new TestTracer(this);
private BeanContainer container;
private Injector injector;
private Terminal terminal;
private BufferIO io;
private Shell shell;
private CommandRegistryImpl commandRegistry;
private Variables variables;
private Branding branding;
protected final Map<String, Class> requiredCommands = new HashMap<>();
private final ThreadIOImpl threadIO = new ThreadIOImpl();
protected CommandTestSupport(final String name, final Class<?> type) {
this.name = checkNotNull(name);
checkNotNull(type);
requiredCommands.put(name, type);
}
protected CommandTestSupport(final Class<?> type) {
this(type.getAnnotation(Command.class).name(), type);
}
@Before
public void setUp() throws Exception {
terminal = TerminalBuilder.builder().dumb(true).build();
io = new BufferIO(terminal);
variables = new VariablesSupport();
branding = new TestBranding(util.resolveFile("target/shell-home"));
container = new BeanContainer();
List<Module> modules = new ArrayList<>();
modules.add(binder -> {
binder.bind(BeanContainer.class).toInstance(container);
binder.bind(ThreadIO.class).toInstance(threadIO);
binder.bind(Branding.class).toInstance(branding);
});
configureModules(modules);
modules.add(createSpaceModule());
injector = Guice.createInjector(Stage.DEVELOPMENT, new WireModule(modules));
container.add(injector, 0);
shell = injector.getInstance(ShellBuilder.class)
.branding(branding)
.io(io)
.variables(variables)
.build();
variables = shell.getVariables();
commandRegistry = injector.getInstance(CommandRegistryImpl.class);
commandRegistry.setDiscoveryEnabled(false);
// disable default help-page discovery
HelpPageManagerImpl helpPageManager = injector.getInstance(HelpPageManagerImpl.class);
helpPageManager.setDiscoveryEnabled(false);
// force logging to resolve to specific stream and not re-resolve System.out
TargetConsoleAppender.setTarget(System.out);
threadIO.start();
shell.start();
// register required commands
for (Map.Entry<String, Class> entry : requiredCommands.entrySet()) {
commandRegistry.registerCommand(entry.getKey(), entry.getValue());
}
// allow test to become aware of injection
injector.injectMembers(this);
}
/**
* Extension-point for sub-class to configure any additional modules for test environment.
*/
protected void configureModules(@Nonnull final List<Module> modules) {
// empty
}
/**
* Expose ability to adjust the {@link SpaceModule}; for most cases this should be left ASIS.
*/
protected SpaceModule createSpaceModule() {
URLClassSpace space = new URLClassSpace(getClass().getClassLoader());
return new SpaceModule(space, BeanScanning.INDEX);
}
@After
public void tearDown() throws Exception {
threadIO.stop();
if (shell != null) {
shell.stop();
shell = null;
}
if (terminal != null) {
terminal.close();
terminal = null;
}
if (container != null) {
container.clear();
container = null;
}
commandRegistry = null;
variables = null;
io = null;
injector = null;
}
protected Shell getShell() {
checkState(shell != null);
return shell;
}
protected BufferIO getIo() {
checkState(io != null);
return io;
}
protected <T> T lookup(final Class<T> type) {
checkState(injector != null);
return injector.getInstance(type);
}
/**
* Execute a raw shell line.
*/
protected Object executeLine(final String line) throws Exception {
checkNotNull(line);
try {
return getShell().execute(line);
}
finally {
io.dump(log);
}
}
/**
* Execute command registered for the test.
*/
protected Object executeCommand(final String... args) throws Exception {
checkNotNull(args);
try {
return getShell().execute(name + " " + String.join(" ", args));
}
finally {
io.dump(log);
}
}
//
// Default tests for all commands
//
/**
* All commands should be registered with {@link CommandRegistry} component.
*/
@Test
public void testRegistered() throws Exception {
assertThat(commandRegistry.containsCommand(name), is(true));
}
/**
* All commands must provide {@code --help} and {@code -h} handling.
*/
@Test
public void testHelp() throws Exception {
Object result;
result = executeCommand("--help");
assertThat(result, nullValue());
result = executeCommand("-h");
assertThat(result, nullValue());
}
}