Convert thread to an actual application
User can now use spring boot CLI as before or alternatively just java -jar the deployer app.
This commit is contained in:
21
.travis.yml
21
.travis.yml
@@ -5,26 +5,7 @@ cache:
|
|||||||
language: java
|
language: java
|
||||||
jdk: # TODO: should work with java 1.7
|
jdk: # TODO: should work with java 1.7
|
||||||
- oraclejdk8
|
- oraclejdk8
|
||||||
before_install:
|
|
||||||
- git config user.name "$GIT_NAME"
|
|
||||||
- git config user.email "$GIT_EMAIL"
|
|
||||||
- git config credential.helper "store --file=.git/credentials"
|
|
||||||
- echo "https://$GH_TOKEN:@github.com" > .git/credentials
|
|
||||||
- gem install asciidoctor
|
|
||||||
install:
|
install:
|
||||||
- ./mvnw install -P docs -q -U -DskipTests=true -Dmaven.test.redirectTestOutputToFile=true
|
- ./mvnw install -P docs -q -U -DskipTests=true -Dmaven.test.redirectTestOutputToFile=true
|
||||||
- '[ "${MVN_GOAL}" == "deploy" ] && ./docs/src/main/asciidoc/ghpages.sh || echo "Not updating docs"'
|
|
||||||
script:
|
script:
|
||||||
- './mvnw -s .settings.xml $MVN_GOAL $MVN_PROFILE -nsu -Dmaven.test.redirectTestOutputToFile=true'
|
- './mvnw install -nsu -Dmaven.test.redirectTestOutputToFile=true'
|
||||||
env:
|
|
||||||
global:
|
|
||||||
- GIT_NAME="Dave Syer"
|
|
||||||
- GIT_EMAIL=dsyer@pivotal.io
|
|
||||||
- CI_DEPLOY_USERNAME=buildmaster
|
|
||||||
- FEATURE_BRANCH=$(echo ${TRAVIS_BRANCH} | grep -q "^.*/.*$" && echo true || echo false)
|
|
||||||
- SPRING_CLOUD_BUILD=$(echo ${TRAVIS_REPO_SLUG} | grep -q "^spring-cloud/.*$" && echo true || echo false)
|
|
||||||
- MVN_GOAL=$([ "${TRAVIS_PULL_REQUEST}" == "false" -a "${TRAVIS_TAG}" == "" -a "${FEATURE_BRANCH}" == "false" -a "${SPRING_CLOUD_BUILD}" == "true" ] && echo deploy || echo install)
|
|
||||||
- VERSION=$(mvn validate | grep Building | head -1 | sed -e 's/.* //')
|
|
||||||
- MILESTONE=$(echo ${VERSION} | egrep 'M|RC' && echo true || echo false)
|
|
||||||
- MVN_PROFILE=$([ "${MILESTONE}" == "true" ] && echo -P milestone)
|
|
||||||
- secure: "mGJnfRsxSdjAwedXAB8kvC3ckc1TGHS2ym+8dP7TSPhknpM9To+S6THwTmawOpJFq09A/1OlUWeanpGEIHCXB/Oz73ZQ9xqheqFf3mk83xOV0TjIP3AzT2boTNRVFGe+LXOIRgTwg7jULl9TIf6pBUK3s5iPvLYlZhCq1Q6QEIU="
|
|
||||||
|
|||||||
@@ -17,9 +17,9 @@
|
|||||||
package org.springframework.cloud.launcher.cli;
|
package org.springframework.cloud.launcher.cli;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.lang.reflect.Constructor;
|
|
||||||
import java.net.MalformedURLException;
|
import java.net.MalformedURLException;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
|
import java.net.URL;
|
||||||
import java.net.URLClassLoader;
|
import java.net.URLClassLoader;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
@@ -29,7 +29,7 @@ import java.util.List;
|
|||||||
|
|
||||||
import org.apache.commons.logging.Log;
|
import org.apache.commons.logging.Log;
|
||||||
import org.apache.commons.logging.LogFactory;
|
import org.apache.commons.logging.LogFactory;
|
||||||
import org.codehaus.groovy.control.CompilerConfiguration;
|
|
||||||
import org.springframework.boot.cli.command.HelpExample;
|
import org.springframework.boot.cli.command.HelpExample;
|
||||||
import org.springframework.boot.cli.command.OptionParsingCommand;
|
import org.springframework.boot.cli.command.OptionParsingCommand;
|
||||||
import org.springframework.boot.cli.command.options.OptionHandler;
|
import org.springframework.boot.cli.command.options.OptionHandler;
|
||||||
@@ -41,7 +41,6 @@ import org.springframework.boot.cli.compiler.grape.DependencyResolutionContext;
|
|||||||
import org.springframework.boot.cli.compiler.grape.RepositoryConfiguration;
|
import org.springframework.boot.cli.compiler.grape.RepositoryConfiguration;
|
||||||
import org.springframework.util.StringUtils;
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
import groovy.lang.GroovyClassLoader;
|
|
||||||
import joptsimple.OptionSet;
|
import joptsimple.OptionSet;
|
||||||
import joptsimple.OptionSpec;
|
import joptsimple.OptionSpec;
|
||||||
|
|
||||||
@@ -96,21 +95,17 @@ public class LauncherCommand extends OptionParsingCommand {
|
|||||||
@Override
|
@Override
|
||||||
protected synchronized ExitStatus run(OptionSet options) throws Exception {
|
protected synchronized ExitStatus run(OptionSet options) throws Exception {
|
||||||
if (options.has(this.versionOption)) {
|
if (options.has(this.versionOption)) {
|
||||||
System.out.println("Spring Cloud CLI v"+getVersion());
|
System.out.println("Spring Cloud CLI v" + getVersion());
|
||||||
return ExitStatus.OK;
|
return ExitStatus.OK;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
URLClassLoader classLoader = populateClassloader(options);
|
URLClassLoader classLoader = populateClassloader(options);
|
||||||
|
// This is the main class in the deployer archive:
|
||||||
String name = "org.springframework.cloud.launcher.deployer.DeployerThread";
|
String name = "org.springframework.boot.loader.wrapper.ThinJarWrapper";
|
||||||
Class<?> threadClass = classLoader.loadClass(name);
|
Class<?> threadClass = classLoader.loadClass(name);
|
||||||
|
URL url = classLoader.getURLs()[0];
|
||||||
Constructor<?> constructor = threadClass.getConstructor(ClassLoader.class,
|
threadClass.getMethod("main", String[].class).invoke(null,
|
||||||
String[].class);
|
new Object[] { getArgs(options, url) });
|
||||||
Thread thread = (Thread) constructor.newInstance(classLoader,
|
|
||||||
getArgs(options));
|
|
||||||
thread.start();
|
|
||||||
thread.join();
|
|
||||||
}
|
}
|
||||||
catch (Exception e) {
|
catch (Exception e) {
|
||||||
log.error("Error running spring cloud", e);
|
log.error("Error running spring cloud", e);
|
||||||
@@ -120,9 +115,10 @@ public class LauncherCommand extends OptionParsingCommand {
|
|||||||
return ExitStatus.OK;
|
return ExitStatus.OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
private String[] getArgs(OptionSet options) {
|
private String[] getArgs(OptionSet options, URL url) {
|
||||||
List<Object> args = new ArrayList<>();
|
List<Object> args = new ArrayList<>();
|
||||||
List<String> apps = new ArrayList<>();
|
List<String> apps = new ArrayList<>();
|
||||||
|
args.add("--thin.archive=" + url.toString());
|
||||||
int sourceArgCount = 0;
|
int sourceArgCount = 0;
|
||||||
for (Object option : options.nonOptionArguments()) {
|
for (Object option : options.nonOptionArguments()) {
|
||||||
if (option instanceof String) {
|
if (option instanceof String) {
|
||||||
@@ -157,10 +153,6 @@ public class LauncherCommand extends OptionParsingCommand {
|
|||||||
throws MalformedURLException {
|
throws MalformedURLException {
|
||||||
DependencyResolutionContext resolutionContext = new DependencyResolutionContext();
|
DependencyResolutionContext resolutionContext = new DependencyResolutionContext();
|
||||||
|
|
||||||
GroovyClassLoader loader = new GroovyClassLoader(
|
|
||||||
Thread.currentThread().getContextClassLoader(),
|
|
||||||
new CompilerConfiguration());
|
|
||||||
|
|
||||||
List<RepositoryConfiguration> repositoryConfiguration = RepositoryConfigurationFactory
|
List<RepositoryConfiguration> repositoryConfiguration = RepositoryConfigurationFactory
|
||||||
.createDefaultRepositoryConfiguration();
|
.createDefaultRepositoryConfiguration();
|
||||||
repositoryConfiguration.add(0, new RepositoryConfiguration("local",
|
repositoryConfiguration.add(0, new RepositoryConfiguration("local",
|
||||||
@@ -170,7 +162,7 @@ public class LauncherCommand extends OptionParsingCommand {
|
|||||||
System.setProperty("groovy.grape.report.downloads", "true");
|
System.setProperty("groovy.grape.report.downloads", "true");
|
||||||
}
|
}
|
||||||
|
|
||||||
AetherGrapeEngine grapeEngine = AetherGrapeEngineFactory.create(loader,
|
AetherGrapeEngine grapeEngine = AetherGrapeEngineFactory.create(null,
|
||||||
repositoryConfiguration, resolutionContext);
|
repositoryConfiguration, resolutionContext);
|
||||||
|
|
||||||
HashMap<String, String> dependency = new HashMap<>();
|
HashMap<String, String> dependency = new HashMap<>();
|
||||||
@@ -178,16 +170,16 @@ public class LauncherCommand extends OptionParsingCommand {
|
|||||||
dependency.put("module", "spring-cloud-launcher-deployer");
|
dependency.put("module", "spring-cloud-launcher-deployer");
|
||||||
dependency.put("version", getVersion());
|
dependency.put("version", getVersion());
|
||||||
URI[] uris = grapeEngine.resolve(null, dependency);
|
URI[] uris = grapeEngine.resolve(null, dependency);
|
||||||
for (URI uri : uris) {
|
URLClassLoader loader = new URLClassLoader(new URL[] { uris[0].toURL() },
|
||||||
loader.addURL(uri.toURL());
|
getClass().getClassLoader().getParent());
|
||||||
}
|
|
||||||
log.debug("resolved URIs " + Arrays.asList(loader.getURLs()));
|
log.debug("resolved URIs " + Arrays.asList(loader.getURLs()));
|
||||||
return loader;
|
return loader;
|
||||||
}
|
}
|
||||||
|
|
||||||
private String getVersion() {
|
private String getVersion() {
|
||||||
Package pkg = LauncherCommand.class.getPackage();
|
Package pkg = LauncherCommand.class.getPackage();
|
||||||
String version = (pkg != null ? pkg.getImplementationVersion() : DEFAULT_VERSION);
|
String version = (pkg != null ? pkg.getImplementationVersion()
|
||||||
|
: DEFAULT_VERSION);
|
||||||
return version != null ? version : DEFAULT_VERSION;
|
return version != null ? version : DEFAULT_VERSION;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -39,7 +39,8 @@ public class LauncherCommandTests {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testNonOptionArgsPassedDown() throws Exception {
|
public void testNonOptionArgsPassedDown() throws Exception {
|
||||||
new LauncherCommand().run("--list", "--", "--spring.profiles.active=test");
|
new LauncherCommand().run("--list", "--", "--spring.profiles.active=test",
|
||||||
|
"--spring.config.location=file:./src/test/resources/");
|
||||||
assertThat(output.toString(), containsString("foo"));
|
assertThat(output.toString(), containsString("foo"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
|
|
||||||
<properties>
|
<properties>
|
||||||
<spring-cloud-deployer.version>1.1.1.RELEASE</spring-cloud-deployer.version>
|
<spring-cloud-deployer.version>1.1.1.RELEASE</spring-cloud-deployer.version>
|
||||||
|
<spring-cloud-deployer-thin.version>0.0.1.BUILD-SNAPSHOT</spring-cloud-deployer-thin.version>
|
||||||
</properties>
|
</properties>
|
||||||
|
|
||||||
<dependencyManagement>
|
<dependencyManagement>
|
||||||
@@ -27,6 +28,11 @@
|
|||||||
<artifactId>spring-cloud-deployer-local</artifactId>
|
<artifactId>spring-cloud-deployer-local</artifactId>
|
||||||
<version>${spring-cloud-deployer.version}</version>
|
<version>${spring-cloud-deployer.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.cloud</groupId>
|
||||||
|
<artifactId>spring-cloud-deployer-thin</artifactId>
|
||||||
|
<version>${spring-cloud-deployer-thin.version}</version>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.cloud</groupId>
|
<groupId>org.springframework.cloud</groupId>
|
||||||
<artifactId>spring-cloud-deployer-resource-support</artifactId>
|
<artifactId>spring-cloud-deployer-resource-support</artifactId>
|
||||||
@@ -55,4 +61,12 @@
|
|||||||
</dependency>
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
|
<build>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
</project>
|
</project>
|
||||||
|
|||||||
@@ -16,8 +16,17 @@
|
|||||||
|
|
||||||
package org.springframework.cloud.launcher.deployer;
|
package org.springframework.cloud.launcher.deployer;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import org.springframework.boot.env.YamlPropertySourceLoader;
|
import org.springframework.boot.env.YamlPropertySourceLoader;
|
||||||
import org.springframework.cloud.deployer.spi.app.AppDeployer;
|
import org.springframework.cloud.deployer.spi.app.AppDeployer;
|
||||||
import org.springframework.cloud.deployer.spi.app.AppStatus;
|
import org.springframework.cloud.deployer.spi.app.AppStatus;
|
||||||
@@ -37,14 +46,6 @@ import org.springframework.core.io.Resource;
|
|||||||
import org.springframework.core.io.ResourceLoader;
|
import org.springframework.core.io.ResourceLoader;
|
||||||
import org.springframework.util.StringUtils;
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.LinkedHashMap;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
|
||||||
|
|
||||||
import static org.springframework.util.StringUtils.collectionToCommaDelimitedString;
|
import static org.springframework.util.StringUtils.collectionToCommaDelimitedString;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -102,8 +103,7 @@ public class Deployer {
|
|||||||
logger.debug("Deployables {}", properties.getDeployables());
|
logger.debug("Deployables {}", properties.getDeployables());
|
||||||
|
|
||||||
for (Deployable deployable : deployables) {
|
for (Deployable deployable : deployables) {
|
||||||
deployInternal(deployer, resourceLoader, deployable,
|
deployInternal(deployer, resourceLoader, deployable, properties, environment);
|
||||||
properties, environment);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for (Deployable deployable : deployables) {
|
for (Deployable deployable : deployables) {
|
||||||
@@ -251,10 +251,11 @@ public class Deployer {
|
|||||||
|
|
||||||
private PropertySource<?> extractPropertySource(String path) {
|
private PropertySource<?> extractPropertySource(String path) {
|
||||||
PropertySource<?> source = null;
|
PropertySource<?> source = null;
|
||||||
Resource resource = new ClassPathResource("config" + path, DeployerThread.class);
|
Resource resource = new ClassPathResource("config" + path,
|
||||||
|
DeployerApplication.class);
|
||||||
source = loadPropertySource(resource, path);
|
source = loadPropertySource(resource, path);
|
||||||
if (source == null) {
|
if (source == null) {
|
||||||
resource = new ClassPathResource(path, DeployerThread.class);
|
resource = new ClassPathResource(path, DeployerApplication.class);
|
||||||
source = loadPropertySource(resource, path);
|
source = loadPropertySource(resource, path);
|
||||||
}
|
}
|
||||||
if (source == null) {
|
if (source == null) {
|
||||||
|
|||||||
@@ -16,8 +16,14 @@
|
|||||||
|
|
||||||
package org.springframework.cloud.launcher.deployer;
|
package org.springframework.cloud.launcher.deployer;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import org.springframework.boot.Banner.Mode;
|
import org.springframework.boot.Banner.Mode;
|
||||||
import org.springframework.boot.autoconfigure.PropertyPlaceholderAutoConfiguration;
|
import org.springframework.boot.autoconfigure.PropertyPlaceholderAutoConfiguration;
|
||||||
import org.springframework.boot.builder.SpringApplicationBuilder;
|
import org.springframework.boot.builder.SpringApplicationBuilder;
|
||||||
@@ -27,31 +33,27 @@ import org.springframework.context.ConfigurableApplicationContext;
|
|||||||
import org.springframework.util.ClassUtils;
|
import org.springframework.util.ClassUtils;
|
||||||
import org.springframework.util.StringUtils;
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.Collection;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author Spencer Gibb
|
* @author Spencer Gibb
|
||||||
*/
|
*/
|
||||||
public class DeployerThread extends Thread {
|
public class DeployerApplication {
|
||||||
|
|
||||||
private static final Logger logger = LoggerFactory.getLogger(DeployerThread.class);
|
private static final Logger logger = LoggerFactory
|
||||||
|
.getLogger(DeployerApplication.class);
|
||||||
|
|
||||||
private static final String DEFAULT_VERSION = "1.2.3.BUILD-SNAPSHOT";
|
private static final String DEFAULT_VERSION = "1.3.0.BUILD-SNAPSHOT";
|
||||||
|
|
||||||
private String[] args;
|
private String[] args;
|
||||||
|
|
||||||
public DeployerThread(ClassLoader classLoader, String... args) {
|
public DeployerApplication(String... args) {
|
||||||
super("spring-cloud-launcher");
|
|
||||||
this.args = args;
|
this.args = args;
|
||||||
setContextClassLoader(classLoader);
|
|
||||||
setDaemon(true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
public static void main(String[] args) {
|
||||||
public void run() {
|
new DeployerApplication(args).run();
|
||||||
|
}
|
||||||
|
|
||||||
|
void run() {
|
||||||
List<String> list = Arrays.asList(this.args);
|
List<String> list = Arrays.asList(this.args);
|
||||||
if (list.contains("--launcher.list=true")) {
|
if (list.contains("--launcher.list=true")) {
|
||||||
quiet();
|
quiet();
|
||||||
@@ -64,9 +66,10 @@ public class DeployerThread extends Thread {
|
|||||||
|
|
||||||
private void quiet() {
|
private void quiet() {
|
||||||
try {
|
try {
|
||||||
LogbackLoggingSystem.get(ClassUtils.getDefaultClassLoader()).setLogLevel("ROOT",
|
LogbackLoggingSystem.get(ClassUtils.getDefaultClassLoader())
|
||||||
LogLevel.OFF);
|
.setLogLevel("ROOT", LogLevel.OFF);
|
||||||
} catch (Exception e) {
|
}
|
||||||
|
catch (Exception e) {
|
||||||
logger.error("Unable to turn of ROOT logger for quiet()", e);
|
logger.error("Unable to turn of ROOT logger for quiet()", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -98,7 +101,7 @@ public class DeployerThread extends Thread {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private String getVersion() {
|
private String getVersion() {
|
||||||
Package pkg = DeployerThread.class.getPackage();
|
Package pkg = DeployerApplication.class.getPackage();
|
||||||
return (pkg != null ? pkg.getImplementationVersion() : DEFAULT_VERSION);
|
return (pkg != null ? pkg.getImplementationVersion() : DEFAULT_VERSION);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -25,26 +25,28 @@ import static org.junit.Assert.assertThat;
|
|||||||
/**
|
/**
|
||||||
* @author Spencer Gibb
|
* @author Spencer Gibb
|
||||||
*/
|
*/
|
||||||
public class DeployerThreadTests {
|
public class DeployerApplicationTests {
|
||||||
|
|
||||||
@Rule
|
@Rule
|
||||||
public OutputCapture output = new OutputCapture();
|
public OutputCapture output = new OutputCapture();
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testCreateClassLoaderAndListDeployables() throws Exception {
|
public void testCreateClassLoaderAndListDeployables() throws Exception {
|
||||||
new DeployerThread(DeployerThread.class.getClassLoader(), "--launcher.list=true").run();;
|
new DeployerApplication("--launcher.list=true").run();
|
||||||
assertThat(output.toString(), containsString("configserver"));
|
assertThat(output.toString(), containsString("configserver"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testNonOptionArgsPassedDown() throws Exception {
|
public void testNonOptionArgsPassedDown() throws Exception {
|
||||||
new DeployerThread(DeployerThread.class.getClassLoader(), "--launcher.list=true", "--spring.profiles.active=test").run();
|
new DeployerApplication("--launcher.list=true", "--spring.profiles.active=test")
|
||||||
|
.run();
|
||||||
assertThat(output.toString(), containsString("foo"));
|
assertThat(output.toString(), containsString("foo"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testInvalidDeployableFails() throws Exception {
|
public void testInvalidDeployableFails() throws Exception {
|
||||||
new DeployerThread(DeployerThread.class.getClassLoader(), "--launcher.deploy=foo,bar").run();
|
new DeployerApplication("--launcher.deploy=foo,bar").run();
|
||||||
assertThat(output.toString(), containsString("The following are not valid: 'foo,bar'"));
|
assertThat(output.toString(),
|
||||||
|
containsString("The following are not valid: 'foo,bar'"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user