Browse Source

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.
pull/79/head
Phillip Webb 12 years ago
parent
commit
efe102bd51
  1. 1
      spring-boot-cli/src/main/groovy/testers/junit.groovy
  2. 7
      spring-boot-cli/src/main/groovy/testers/tester.groovy
  3. 110
      spring-boot-cli/src/main/java/org/springframework/boot/cli/command/FileOptions.java
  4. 36
      spring-boot-cli/src/main/java/org/springframework/boot/cli/command/RunCommand.java
  5. 142
      spring-boot-cli/src/main/java/org/springframework/boot/cli/command/TestCommand.java
  6. 25
      spring-boot-cli/src/main/java/org/springframework/boot/cli/command/tester/AbstractTester.java
  7. 42
      spring-boot-cli/src/main/java/org/springframework/boot/cli/command/tester/Failure.java
  8. 110
      spring-boot-cli/src/main/java/org/springframework/boot/cli/command/tester/TestResults.java

1
spring-boot-cli/src/main/groovy/testers/junit.groovy

@ -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.

7
spring-boot-cli/src/main/groovy/testers/tester.groovy

@ -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 {
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)
} }

110
spring-boot-cli/src/main/java/org/springframework/boot/cli/command/FileOptions.java

@ -0,0 +1,110 @@
/*
* 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()]);
}
}

36
spring-boot-cli/src/main/java/org/springframework/boot/cli/command/RunCommand.java

@ -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 {
@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 {
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}.

142
spring-boot-cli/src/main/java/org/springframework/boot/cli/command/TestCommand.java

@ -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;
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;
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 {
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 {
@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 {
* 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 {
} }
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 {

25
spring-boot-cli/src/main/java/org/springframework/boot/cli/command/tester/AbstractTester.java

@ -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);
} }

42
spring-boot-cli/src/main/java/org/springframework/boot/cli/command/tester/Failure.java

@ -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;
} }
} }

110
spring-boot-cli/src/main/java/org/springframework/boot/cli/command/tester/TestResults.java

@ -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;
}
}
}

Loading…
Cancel
Save