diff --git a/pom.xml b/pom.xml index 0992d00a..3b510209 100644 --- a/pom.xml +++ b/pom.xml @@ -26,6 +26,24 @@ 3.12.1.GA provided + + io.cucumber + cucumber-java + 2.3.1 + test + + + io.cucumber + cucumber-junit + 2.3.1 + test + + + junit + junit + 4.12 + test + diff --git a/src/test/java/javacg/JARBuilder.java b/src/test/java/javacg/JARBuilder.java new file mode 100644 index 00000000..2d63f615 --- /dev/null +++ b/src/test/java/javacg/JARBuilder.java @@ -0,0 +1,117 @@ +package javacg; + +import java.io.BufferedInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.StringWriter; +import java.net.URI; +import java.util.Arrays; +import java.util.Collection; +import java.util.LinkedList; +import java.util.jar.Attributes; +import java.util.jar.JarEntry; +import java.util.jar.JarOutputStream; +import java.util.jar.Manifest; + +import javax.tools.Diagnostic; +import javax.tools.DiagnosticCollector; +import javax.tools.JavaCompiler; +import javax.tools.JavaCompiler.CompilationTask; +import javax.tools.JavaFileObject; +import javax.tools.SimpleJavaFileObject; +import javax.tools.StandardJavaFileManager; +import javax.tools.StandardLocation; +import javax.tools.ToolProvider; + +public class JARBuilder { + private static final String TEMP_DIR = System.getProperty("java.io.tmpdir"); + + private final JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); + private final DiagnosticCollector diagnostics = new DiagnosticCollector(); + private final StandardJavaFileManager fileManager = compiler.getStandardFileManager(diagnostics, null, null); + private final LinkedList compilationUnits = new LinkedList<>(); + private final Collection classFiles = new LinkedList<>(); + + public JARBuilder() throws IOException { + fileManager.setLocation(StandardLocation.CLASS_OUTPUT, Arrays.asList(new File(TEMP_DIR))); + } + + public void add(String className, String classCode) throws IOException { + compilationUnits.add(createJavaFile(className, classCode)); + classFiles.add(new File(TEMP_DIR, className + ".class")); + } + + public File build() throws FileNotFoundException, IOException { + CompilationTask task = compiler.getTask(null, fileManager, diagnostics, null, null, compilationUnits); + boolean success = task.call(); + if (!success) { + displayDiagnostic(diagnostics); + throw new RuntimeException("Cannot compile classes for the JAR"); + } + + File file = File.createTempFile("test", ".jar"); + JarOutputStream jar = new JarOutputStream(new FileOutputStream(file), createManifest()); + for (File classFile : classFiles) { + add(classFile, jar); + } + jar.close(); + return file; + } + + private void displayDiagnostic(DiagnosticCollector diagnostics) { + for (Diagnostic diagnostic : diagnostics.getDiagnostics()) { + JavaSourceFromString sourceClass = (JavaSourceFromString) diagnostic.getSource(); + System.err.println("-----"); + System.err.println("Source: " + sourceClass.getName()); + System.err.println("Message: " + diagnostic.getMessage(null)); + System.err.println("Position: " + diagnostic.getPosition()); + System.err.println(diagnostic.getKind() + " " + diagnostic.getCode()); + } + } + + private JavaFileObject createJavaFile(String className, String classCode) throws IOException { + StringWriter writer = new StringWriter(); + writer.append(classCode); + writer.close(); + JavaFileObject file = new JavaSourceFromString(className, writer.toString()); + return file; + } + + private Manifest createManifest() { + Manifest manifest = new Manifest(); + manifest.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, "1.0"); + return manifest; + } + + private void add(File classFile, JarOutputStream jar) throws IOException { + JarEntry entry = new JarEntry(classFile.getPath().replace("\\", "/")); + jar.putNextEntry(entry); + try (BufferedInputStream in = new BufferedInputStream(new FileInputStream(classFile))) { + byte[] buffer = new byte[1024]; + while (true) { + int count = in.read(buffer); + if (count == -1) + break; + jar.write(buffer, 0, count); + } + jar.closeEntry(); + } + } +} + +class JavaSourceFromString extends SimpleJavaFileObject { + final String code; + + JavaSourceFromString(String name, String code) throws IOException { + super(URI.create("string:///" + name.replace('.', '/') + Kind.SOURCE.extension), Kind.SOURCE); + this.code = code; + } + + @Override + public CharSequence getCharContent(boolean ignoreEncodingErrors) { + return code; + } +} \ No newline at end of file diff --git a/src/test/java/javacg/RunCucumberTest.java b/src/test/java/javacg/RunCucumberTest.java new file mode 100644 index 00000000..9de478d1 --- /dev/null +++ b/src/test/java/javacg/RunCucumberTest.java @@ -0,0 +1,10 @@ +package javacg; + +import cucumber.api.CucumberOptions; +import cucumber.api.junit.Cucumber; +import org.junit.runner.RunWith; + +@RunWith(Cucumber.class) +@CucumberOptions(plugin = { "pretty" }) +public class RunCucumberTest { +} \ No newline at end of file diff --git a/src/test/java/javacg/StepDefinitions.java b/src/test/java/javacg/StepDefinitions.java new file mode 100644 index 00000000..05680698 --- /dev/null +++ b/src/test/java/javacg/StepDefinitions.java @@ -0,0 +1,48 @@ +package javacg; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.PrintStream; + +import cucumber.api.java.en.Given; +import cucumber.api.java.en.Then; +import cucumber.api.java.en.When; +import gr.gousiosg.javacg.stat.JCallGraph; + +public class StepDefinitions { + private final JARBuilder jarBuilder; + private String result; + + public StepDefinitions() throws IOException { + jarBuilder = new JARBuilder(); + } + + @Given("^I have the class \"([^\"]*)\" with code:$") + public void i_have_the_class_with_code(String className, String classCode) throws Exception { + jarBuilder.add(className, classCode); + } + + @When("^I run the analyze$") + public void i_analyze_it() throws Exception { + File jarFile = jarBuilder.build(); + + PrintStream oldOut = System.out; + ByteArrayOutputStream resultBuffer = new ByteArrayOutputStream(); + System.setOut(new PrintStream(resultBuffer)); + JCallGraph.main(new String[] { jarFile.getPath() }); + System.setOut(oldOut); + + result = resultBuffer.toString(); + } + + @Then("^the result should contain:$") + public void the_result_should_contain(String line) throws Exception { + if (result.contains(line)) { + // OK + } else { + System.err.println(result); + throw new RuntimeException("Cannot found: " + line); + } + } +} diff --git a/src/test/resources/javacg/lambda.feature b/src/test/resources/javacg/lambda.feature new file mode 100644 index 00000000..14d8f3fb --- /dev/null +++ b/src/test/resources/javacg/lambda.feature @@ -0,0 +1,37 @@ +#Author: matthieu.vergne@gmail.com +Feature: Lambda + I want to identify all lambdas within the analyzed code. + + Background: + # Introduce the lambda we will use + Given I have the class "Runner" with code: + """ + @FunctionalInterface + public interface Runner { + public void run(); + } + """ + + Scenario: Retrieve lambda in method + Given I have the class "LambdaTest" with code: + """ + public class LambdaTest { + public void methodA() { + Runner r = () -> methodB(); + r.run(); + } + + public void methodB() {} + } + """ + When I run the analyze + # Creation of r in methodA + Then the result should contain: + """ + M:LambdaTest:methodA() (D)Runner:run(LambdaTest) + """ + # Call of methodB in r + And the result should contain: + """ + M:LambdaTest:lambda$methodA$0() (M)LambdaTest:methodB() + """