Commit efe102bd authored by Phillip Webb's avatar Phillip Webb

Polish CLI tester code

Reduce duplication by extracting FileOptions class to be shared by
both the RunCommand and the TestCommand.

Also applied minor code convention tweaks.
parent 910202b0
...@@ -22,6 +22,7 @@ import org.springframework.boot.cli.command.tester.TestResults ...@@ -22,6 +22,7 @@ import org.springframework.boot.cli.command.tester.TestResults
import java.lang.annotation.Annotation import java.lang.annotation.Annotation
import java.lang.reflect.Method import java.lang.reflect.Method
/** /**
* Groovy script to run JUnit tests inside the {@link TestCommand}. * Groovy script to run JUnit tests inside the {@link TestCommand}.
* Needs to be compiled along with the actual code to work properly. * Needs to be compiled along with the actual code to work properly.
......
...@@ -16,6 +16,7 @@ ...@@ -16,6 +16,7 @@
import org.springframework.boot.cli.command.tester.TestResults import org.springframework.boot.cli.command.tester.TestResults
/** /**
* Groovy script define abstract basis for automated testers for {@link TestCommand}. * Groovy script define abstract basis for automated testers for {@link TestCommand}.
* Needs to be compiled along with the actual code to work properly. * Needs to be compiled along with the actual code to work properly.
...@@ -28,14 +29,14 @@ public abstract class AbstractTester { ...@@ -28,14 +29,14 @@ public abstract class AbstractTester {
Set<Class<?>> testable = findTestableClasses(compiled) Set<Class<?>> testable = findTestableClasses(compiled)
if (testable.size() == 0) { if (testable.size() == 0) {
return TestResults.none return TestResults.NONE
} }
return test(testable.toArray(new Class<?>[0])) return test(testable.toArray(new Class<?>[0]))
} }
abstract protected Set<Class<?>> findTestableClasses(List<Class<?>> compiled) protected abstract Set<Class<?>> findTestableClasses(List<Class<?>> compiled)
abstract protected TestResults test(Class<?>[] testable) protected abstract TestResults test(Class<?>[] testable)
} }
/*
* Copyright 2012-2013 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 org.springframework.boot.cli.command;
import java.io.File;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import joptsimple.OptionSet;
/**
* Extract file options (anything following '--' in an {@link OptionSet}).
*
* @author Phillip Webb
* @author Dave Syer
* @author Greg Turnquist
*/
public class FileOptions {
private List<File> files;
private List<?> args;
/**
* Create a new {@link FileOptions} instance.
* @param options the source option set
*/
public FileOptions(OptionSet options) {
this(options, null);
}
/**
* Create a new {@link FileOptions} instance.
* @param optionSet the source option set
* @param classLoader an optional classloader used to try and load files that are not
* found directly.
*/
public FileOptions(OptionSet optionSet, ClassLoader classLoader) {
List<?> nonOptionArguments = optionSet.nonOptionArguments();
List<File> files = new ArrayList<File>();
for (Object option : nonOptionArguments) {
if (option instanceof String) {
String filename = (String) option;
if ("--".equals(filename)) {
break;
}
if (filename.endsWith(".groovy") || filename.endsWith(".java")) {
File file = getFile(filename, classLoader);
if (file == null) {
throw new RuntimeException("Can't find " + filename);
}
files.add(file);
}
}
}
if (files.size() == 0) {
throw new RuntimeException("Please specify a file to run");
}
this.files = Collections.unmodifiableList(files);
this.args = Collections.unmodifiableList(nonOptionArguments.subList(files.size(),
nonOptionArguments.size()));
}
private File getFile(String filename, ClassLoader classLoader) {
File file = new File(filename);
if (file.isFile() && file.canRead()) {
return file;
}
if (classLoader != null) {
URL url = classLoader.getResource(filename);
if (url != null && url.toString().startsWith("file:")) {
return new File(url.toString().substring("file:".length()));
}
}
return null;
}
public List<?> getArgs() {
return this.args;
}
public String[] getArgsArray() {
return this.args.toArray(new String[this.args.size()]);
}
public List<File> getFiles() {
return this.files;
}
public File[] getFilesArray() {
return this.files.toArray(new File[this.files.size()]);
}
}
...@@ -17,9 +17,6 @@ ...@@ -17,9 +17,6 @@
package org.springframework.boot.cli.command; package org.springframework.boot.cli.command;
import java.awt.Desktop; import java.awt.Desktop;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level; import java.util.logging.Level;
import joptsimple.OptionSet; import joptsimple.OptionSet;
...@@ -94,13 +91,10 @@ public class RunCommand extends OptionParsingCommand { ...@@ -94,13 +91,10 @@ public class RunCommand extends OptionParsingCommand {
@Override @Override
protected void run(OptionSet options) throws Exception { protected void run(OptionSet options) throws Exception {
List<?> nonOptionArguments = options.nonOptionArguments(); FileOptions fileOptions = new FileOptions(options);
File[] files = getFileArguments(nonOptionArguments);
List<?> args = nonOptionArguments.subList(files.length,
nonOptionArguments.size());
if (options.has(this.editOption)) { if (options.has(this.editOption)) {
Desktop.getDesktop().edit(files[0]); Desktop.getDesktop().edit(fileOptions.getFiles().get(0));
} }
SpringApplicationRunnerConfiguration configuration = new SpringApplicationRunnerConfigurationAdapter( SpringApplicationRunnerConfiguration configuration = new SpringApplicationRunnerConfigurationAdapter(
...@@ -108,33 +102,11 @@ public class RunCommand extends OptionParsingCommand { ...@@ -108,33 +102,11 @@ public class RunCommand extends OptionParsingCommand {
if (configuration.isLocal() && System.getProperty("grape.root") == null) { if (configuration.isLocal() && System.getProperty("grape.root") == null) {
System.setProperty("grape.root", "."); System.setProperty("grape.root", ".");
} }
this.runner = new SpringApplicationRunner(configuration, files, this.runner = new SpringApplicationRunner(configuration,
args.toArray(new String[args.size()])); fileOptions.getFilesArray(), fileOptions.getArgsArray());
this.runner.compileAndRun(); this.runner.compileAndRun();
} }
private File[] getFileArguments(List<?> nonOptionArguments) {
List<File> files = new ArrayList<File>();
for (Object option : nonOptionArguments) {
if (option instanceof String) {
String filename = (String) option;
if ("--".equals(filename)) {
break;
}
if (filename.endsWith(".groovy") || filename.endsWith(".java")) {
File file = new File(filename);
if (file.isFile() && file.canRead()) {
files.add(file);
}
}
}
}
if (files.size() == 0) {
throw new RuntimeException("Please specify a file to run");
}
return files.toArray(new File[files.size()]);
}
/** /**
* Simple adapter class to present the {@link OptionSet} as a * Simple adapter class to present the {@link OptionSet} as a
* {@link SpringApplicationRunnerConfiguration}. * {@link SpringApplicationRunnerConfiguration}.
......
...@@ -21,10 +21,10 @@ import groovy.lang.GroovyObject; ...@@ -21,10 +21,10 @@ import groovy.lang.GroovyObject;
import java.io.File; import java.io.File;
import java.io.FileNotFoundException; import java.io.FileNotFoundException;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter; import java.io.PrintWriter;
import java.net.URL;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashSet; import java.util.LinkedHashSet;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
import java.util.logging.Level; import java.util.logging.Level;
...@@ -32,6 +32,7 @@ import java.util.logging.Level; ...@@ -32,6 +32,7 @@ import java.util.logging.Level;
import joptsimple.OptionSet; import joptsimple.OptionSet;
import org.apache.ivy.util.FileUtil; import org.apache.ivy.util.FileUtil;
import org.codehaus.groovy.control.CompilationFailedException;
import org.springframework.boot.cli.Log; import org.springframework.boot.cli.Log;
import org.springframework.boot.cli.command.tester.Failure; import org.springframework.boot.cli.command.tester.Failure;
import org.springframework.boot.cli.command.tester.TestResults; import org.springframework.boot.cli.command.tester.TestResults;
...@@ -39,7 +40,7 @@ import org.springframework.boot.cli.compiler.GroovyCompiler; ...@@ -39,7 +40,7 @@ import org.springframework.boot.cli.compiler.GroovyCompiler;
import org.springframework.boot.cli.compiler.GroovyCompilerConfiguration; import org.springframework.boot.cli.compiler.GroovyCompilerConfiguration;
/** /**
* Invokes testing for autocompiled scripts * Invokes testing for auto-compiled scripts
* *
* @author Greg Turnquist * @author Greg Turnquist
*/ */
...@@ -63,6 +64,7 @@ public class TestCommand extends OptionParsingCommand { ...@@ -63,6 +64,7 @@ public class TestCommand extends OptionParsingCommand {
private static class TestGroovyCompilerConfiguration implements private static class TestGroovyCompilerConfiguration implements
GroovyCompilerConfiguration { GroovyCompilerConfiguration {
@Override @Override
public boolean isGuessImports() { public boolean isGuessImports() {
return true; return true;
...@@ -99,10 +101,8 @@ public class TestCommand extends OptionParsingCommand { ...@@ -99,10 +101,8 @@ public class TestCommand extends OptionParsingCommand {
@Override @Override
protected void run(OptionSet options) throws Exception { protected void run(OptionSet options) throws Exception {
List<?> nonOptionArguments = options.nonOptionArguments(); FileOptions fileOptions = new FileOptions(options, getClass()
.getClassLoader());
Set<File> testerFiles = new HashSet<File>();
File[] files = getFileArguments(nonOptionArguments, testerFiles);
/* /*
* Need to compile the code twice: The first time automatically pulls in * Need to compile the code twice: The first time automatically pulls in
...@@ -112,44 +112,24 @@ public class TestCommand extends OptionParsingCommand { ...@@ -112,44 +112,24 @@ public class TestCommand extends OptionParsingCommand {
* context. Then the testers can be fetched and invoked through reflection * context. Then the testers can be fetched and invoked through reflection
* against the composite AST. * against the composite AST.
*/ */
// Compile - Pass 1
Object[] sources = this.compiler.sources(files);
boolean testing = false;
try {
check("org.junit.Test", sources);
testerFiles.add(locateSourceFromUrl("junit", "testers/junit.groovy"));
testing = true;
}
catch (ClassNotFoundException e) {
}
try { // Compile - Pass 1 - collect testers
check("spock.lang.Specification", sources); Object[] sources = this.compiler.sources(fileOptions.getFilesArray());
testerFiles.add(locateSourceFromUrl("spock", "testers/spock.groovy")); Set<File> testerFiles = compileAndCollectTesterFiles(sources);
testing = true;
}
catch (ClassNotFoundException e) {
}
if (testing) {
testerFiles.add(locateSourceFromUrl("tester", "testers/tester.groovy"));
}
// Compile - Pass 2 - with appropriate testers added in // Compile - Pass 2 - with appropriate testers added in
files = getFileArguments(nonOptionArguments, testerFiles); List<File> files = new ArrayList<File>(fileOptions.getFiles());
sources = this.compiler.sources(files); files.addAll(testerFiles);
sources = this.compiler.sources(files.toArray(new File[files.size()]));
if (sources.length == 0) { if (sources.length == 0) {
throw new RuntimeException("No classes found in '" + files + "'"); throw new RuntimeException("No classes found in '" + files + "'");
} }
List<Class<?>> testers = new ArrayList<Class<?>>();
// Extract list of compiled classes // Extract list of compiled classes
List<Class<?>> compiled = new ArrayList<Class<?>>(); List<Class<?>> compiled = new ArrayList<Class<?>>();
List<Class<?>> testers = new ArrayList<Class<?>>();
for (Object source : sources) { for (Object source : sources) {
if (source.getClass() == Class.class) { if (source instanceof Class) {
Class<?> sourceClass = (Class<?>) source; Class<?> sourceClass = (Class<?>) source;
if (sourceClass.getSuperclass().getName().equals("AbstractTester")) { if (sourceClass.getSuperclass().getName().equals("AbstractTester")) {
testers.add(sourceClass); testers.add(sourceClass);
...@@ -161,85 +141,53 @@ public class TestCommand extends OptionParsingCommand { ...@@ -161,85 +141,53 @@ public class TestCommand extends OptionParsingCommand {
} }
this.results = new TestResults(); this.results = new TestResults();
for (Class<?> tester : testers) { for (Class<?> tester : testers) {
GroovyObject obj = (GroovyObject) tester.newInstance(); GroovyObject obj = (GroovyObject) tester.newInstance();
this.results.add((TestResults) obj.invokeMethod("findAndTest", compiled)); this.results.add((TestResults) obj.invokeMethod("findAndTest", compiled));
} }
printReport(this.results); printReport(this.results);
} }
private File locateSourceFromUrl(String name, String path) { private Set<File> compileAndCollectTesterFiles(Object[] sources)
try { throws CompilationFailedException, IOException {
File file = File.createTempFile(name, ".groovy"); Set<File> testerFiles = new LinkedHashSet<File>();
file.deleteOnExit(); addTesterOnClass(sources, "org.junit.Test", "junit", testerFiles);
FileUtil.copy(getClass().getClassLoader().getResourceAsStream(path), addTesterOnClass(sources, "spock.lang.Specification", "spock", testerFiles);
file, null); if (!testerFiles.isEmpty()) {
return file; testerFiles.add(createTempTesterFile("tester"));
} }
catch (IOException ex) {
throw new IllegalStateException("Could not create temp file for source: "
+ name);
}
}
private Class<?> check(String className, Object[] sources) return testerFiles;
throws ClassNotFoundException {
Class<?> classToReturn = null;
ClassNotFoundException classNotFoundException = null;
for (Object source : sources) {
try {
classToReturn = ((Class<?>) source).getClassLoader().loadClass(
className);
}
catch (ClassNotFoundException e) {
classNotFoundException = e;
}
}
if (classToReturn != null) {
return classToReturn;
}
throw classNotFoundException;
} }
private File[] getFileArguments(List<?> nonOptionArguments, Set<File> testerFiles) { private void addTesterOnClass(Object[] sources, String className,
List<File> files = new ArrayList<File>(); String testerName, Set<File> testerFiles) {
for (Object option : nonOptionArguments) { for (Object source : sources) {
if (option instanceof String) { if (source instanceof Class<?>) {
String filename = (String) option; try {
if ("--".equals(filename)) { ((Class<?>) source).getClassLoader().loadClass(className);
break; testerFiles.add(createTempTesterFile(testerName));
return;
} }
if (filename.endsWith(".groovy") || filename.endsWith(".java")) { catch (ClassNotFoundException ex) {
File file = new File(filename);
if (file.isFile() && file.canRead()) {
files.add(file);
}
else {
URL url = getClass().getClassLoader().getResource(filename);
if (url != null) {
if (url.toString().startsWith("file:")) {
files.add(new File(url.toString().substring(
"file:".length())));
}
}
else {
throw new RuntimeException("Can't find " + filename);
}
}
} }
} }
} }
if (files.size() == 0) { }
throw new RuntimeException("Please specify a file to run");
}
for (File testerFile : testerFiles) { private File createTempTesterFile(String name) {
files.add(testerFile); try {
File file = File.createTempFile(name, ".groovy");
file.deleteOnExit();
InputStream resource = getClass().getClassLoader().getResourceAsStream(
"testers/" + name + ".groovy");
FileUtil.copy(resource, file, null);
return file;
}
catch (IOException ex) {
throw new IllegalStateException("Could not create temp file for source: "
+ name);
} }
return files.toArray(new File[files.size()]);
} }
private void printReport(TestResults results) throws FileNotFoundException { private void printReport(TestResults results) throws FileNotFoundException {
......
...@@ -20,20 +20,23 @@ import java.io.FileNotFoundException; ...@@ -20,20 +20,23 @@ import java.io.FileNotFoundException;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
/**
* Abstract base class for tester implementations.
*
* @author Greg Turnquist
*/
public abstract class AbstractTester { public abstract class AbstractTester {
public TestResults findAndTest(List<Class<?>> compiled) throws FileNotFoundException { public TestResults findAndTest(List<Class<?>> compiled) throws FileNotFoundException {
Set<Class<?>> testable = findTestableClasses(compiled); Set<Class<?>> testable = findTestableClasses(compiled);
if (testable.size() == 0) {
if (testable.size() == 0) { return TestResults.NONE;
return TestResults.none; }
} return test(testable.toArray(new Class<?>[] {}));
}
return test(testable.toArray(new Class<?>[]{}));
}
abstract protected Set<Class<?>> findTestableClasses(List<Class<?>> compiled); protected abstract Set<Class<?>> findTestableClasses(List<Class<?>> compiled);
abstract protected TestResults test(Class<?>[] testable); protected abstract TestResults test(Class<?>[] testable);
} }
...@@ -18,31 +18,35 @@ package org.springframework.boot.cli.command.tester; ...@@ -18,31 +18,35 @@ package org.springframework.boot.cli.command.tester;
/** /**
* Platform neutral way to capture a test failure * Platform neutral way to capture a test failure
* *
* NOTE: This is needed to avoid having to add JUnit jar file to the deployable artifacts * NOTE: This is needed to avoid having to add JUnit jar file to the deployable artifacts
*
* @author Greg Turnquist
*/ */
public class Failure { public class Failure {
private String description;
private String trace;
public Failure(String description, String trace) { private String description;
this.description = description;
this.trace = trace; private String trace;
}
public Failure(String description, String trace) {
this.description = description;
this.trace = trace;
}
public String getDescription() { public String getDescription() {
return description; return this.description;
} }
public void setDescription(String description) { public void setDescription(String description) {
this.description = description; this.description = description;
} }
public String getTrace() { public String getTrace() {
return trace; return this.trace;
} }
public void setTrace(String trace) { public void setTrace(String trace) {
this.trace = trace; this.trace = trace;
} }
} }
...@@ -21,74 +21,78 @@ import java.util.List; ...@@ -21,74 +21,78 @@ import java.util.List;
/** /**
* Platform neutral way to collect test results * Platform neutral way to collect test results
* *
* NOTE: This is needed to avoid having to add JUnit's jar file to the deployable artifacts * NOTE: This is needed to avoid having to add JUnit's jar file to the deployable
* artifacts
*
* @author Greg Turnquist
*/ */
public class TestResults { public class TestResults {
public static final NoTestResults none = new NoTestResults(); public static final TestResults NONE = new TestResults() {
@Override
public int getRunCount() {
return 0;
}
@Override
public int getFailureCount() {
return 0;
}
private int runCount; @Override
private int failureCount; public Failure[] getFailures() {
private Failure[] failures = new Failure[0]; return new Failure[0];
}
public void add(TestResults results) { @Override
this.runCount += results.getRunCount(); public boolean wasSuccessful() {
this.failureCount += results.getFailureCount(); return true;
}
List<Failure> failures = Arrays.asList(this.failures); };
failures.addAll(Arrays.asList(results.getFailures()));
this.failures = failures.toArray(new Failure[]{});
}
private int runCount;
public boolean wasSuccessful() { private int failureCount;
return this.failureCount == 0;
}
public int getRunCount() { private Failure[] failures = new Failure[0];
return runCount;
}
public void setRunCount(int runCount) { public void add(TestResults results) {
this.runCount = runCount; this.runCount += results.getRunCount();
} this.failureCount += results.getFailureCount();
List<Failure> failures = Arrays.asList(this.failures);
failures.addAll(Arrays.asList(results.getFailures()));
this.failures = failures.toArray(new Failure[] {});
}
public int getFailureCount() { public boolean wasSuccessful() {
return failureCount; return this.failureCount == 0;
} }
public void setFailureCount(int failureCount) { public int getRunCount() {
this.failureCount = failureCount; return this.runCount;
} }
public Failure[] getFailures() { public void setRunCount(int runCount) {
return failures; this.runCount = runCount;
} }
public void setFailures(Failure[] failures) { public int getFailureCount() {
this.failures = failures; return this.failureCount;
} }
private static class NoTestResults extends TestResults { public void setFailureCount(int failureCount) {
@Override this.failureCount = failureCount;
public int getRunCount() { }
return 0;
}
@Override public Failure[] getFailures() {
public int getFailureCount() { return this.failures;
return 0; }
}
@Override public void setFailures(Failure[] failures) {
public Failure[] getFailures() { this.failures = failures;
return new Failure[0]; }
}
@Override }
public boolean wasSuccessful() {
return true;
}
}
}
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment