diff --git a/src/main/java/run/myCode/compiler/CompileDiagnosticListener.java b/src/main/java/run/myCode/compiler/CompileDiagnosticListener.java index b9dd0b5..6064655 100644 --- a/src/main/java/run/myCode/compiler/CompileDiagnosticListener.java +++ b/src/main/java/run/myCode/compiler/CompileDiagnosticListener.java @@ -1,23 +1,88 @@ -package run.myCode.compiler; - -import java.util.Locale; - -import javax.tools.Diagnostic; -import javax.tools.DiagnosticListener; -import javax.tools.JavaFileObject; - -@SuppressWarnings("unused") -public class CompileDiagnosticListener implements DiagnosticListener -{ - @Override - public void report(Diagnostic diagnostic) - { - /* - System.out.println("Line Number->" + diagnostic.getLineNumber()); - System.out.println("code->" + diagnostic.getCode()); - System.out.println("Message->" + diagnostic.getMessage(Locale.ENGLISH)); - System.out.println("Source->" + diagnostic.getSource()); - System.out.println(" "); - */ - } -} \ No newline at end of file +package run.myCode.compiler; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +import javax.tools.Diagnostic; +import javax.tools.DiagnosticListener; +import javax.tools.JavaFileObject; + +/** + * Collects diagnostics reported during compilation so that callers can display + * meaningful error messages. + */ +public class CompileDiagnosticListener implements DiagnosticListener { + + private final List> diagnostics = new ArrayList<>(); + + @Override + public void report(Diagnostic diagnostic) { + diagnostics.add(diagnostic); + } + + /** + * Retrieve all diagnostics that were reported during compilation. + * + * @return a list of diagnostics + */ + public List> getDiagnostics() { + return diagnostics; + } + + /** + * Format collected diagnostics into human readable messages that include the + * source line and a caret pointing to the error column when available. + * + * @return list of formatted diagnostic strings + */ + public List getFormattedDiagnostics() { + List messages = new ArrayList<>(); + for (Diagnostic d : diagnostics) { + messages.add(formatDiagnostic(d)); + } + return messages; + } + + private String formatDiagnostic(Diagnostic d) { + StringBuilder sb = new StringBuilder(); + String sourceName = d.getSource() == null ? "Unknown Source" + : new File(d.getSource().getName()).getName(); + + boolean appendedSource = false; + try { + if (d.getSource() != null) { + CharSequence content = d.getSource().getCharContent(true); + String[] lines = content.toString().split("\r?\n"); + long lineNo = d.getLineNumber(); + if (lineNo > 0 && lineNo <= lines.length) { + String line = lines[(int) lineNo - 1]; + sb.append(line).append(System.lineSeparator()); + long col = d.getColumnNumber(); + if (col > 0) { + for (int i = 1; i < col; i++) { + sb.append(' '); + } + sb.append('^'); + } + sb.append(System.lineSeparator()); + appendedSource = true; + } + } + } catch (IOException e) { + // ignore - if we can't read the source, just return the basic message + } + + if (!appendedSource && sb.length() > 0) { + sb.append(System.lineSeparator()); + } + + sb.append(sourceName).append(':').append(d.getLineNumber()).append(':') + .append(' ').append(d.getKind().toString().toLowerCase(Locale.ENGLISH)) + .append(':').append(' ').append(d.getMessage(Locale.ENGLISH)); + + return sb.toString(); + } +} diff --git a/src/main/java/run/myCode/compiler/JavaCodeCompiler.java b/src/main/java/run/myCode/compiler/JavaCodeCompiler.java index 09cdbb1..771b83e 100644 --- a/src/main/java/run/myCode/compiler/JavaCodeCompiler.java +++ b/src/main/java/run/myCode/compiler/JavaCodeCompiler.java @@ -9,15 +9,14 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; -import java.util.Locale; -import java.util.logging.Level; -import java.util.logging.Logger; +import java.util.Locale; +import java.util.logging.Level; +import java.util.logging.Logger; -import javax.tools.JavaCompiler; -import javax.tools.JavaFileObject; -import javax.tools.StandardJavaFileManager; - -import javax.tools.ToolProvider; +import javax.tools.JavaCompiler; +import javax.tools.JavaFileObject; +import javax.tools.StandardJavaFileManager; +import javax.tools.ToolProvider; public class JavaCodeCompiler { @@ -117,10 +116,18 @@ public static FromMemoryClassLoader compile(Iterable f if (DEBUG) { System.out.println("Starting compilation with mem URIs"); } - boolean result = task.call(); - - // Return the classloader containing the compiled classes - return classLoader; - } + boolean result = task.call(); + + if (!result) { + for (String msg : diag.getFormattedDiagnostics()) { + System.out.println(msg); + System.out.println(); + } + throw new ClassNotFoundException("Compilation failed"); + } + + // Return the classloader containing the compiled classes + return classLoader; + } } diff --git a/src/main/java/zss/compiler/CompileDiagnosticListener.java b/src/main/java/zss/compiler/CompileDiagnosticListener.java index f86b384..732a3b4 100644 --- a/src/main/java/zss/compiler/CompileDiagnosticListener.java +++ b/src/main/java/zss/compiler/CompileDiagnosticListener.java @@ -1,19 +1,88 @@ -package zss.compiler; - -import javax.tools.Diagnostic; -import javax.tools.DiagnosticListener; -import javax.tools.JavaFileObject; - -public class CompileDiagnosticListener implements DiagnosticListener { - - @Override - public void report(Diagnostic diagnostic) { - - // System.err.println("Line Number->" + diagnostic.getLineNumber()); - // System.err.println("code->" + diagnostic.getCode()); - // System.err.println("Message->" + diagnostic.getMessage(Locale.ENGLISH)); - // System.err.println("Source->" + diagnostic.getSource()); - // System.err.println(" "); - - } -} +package zss.compiler; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +import javax.tools.Diagnostic; +import javax.tools.DiagnosticListener; +import javax.tools.JavaFileObject; + +/** + * Collects diagnostics reported during compilation so that callers can display + * meaningful error messages. + */ +public class CompileDiagnosticListener implements DiagnosticListener { + + private final List> diagnostics = new ArrayList<>(); + + @Override + public void report(Diagnostic diagnostic) { + diagnostics.add(diagnostic); + } + + /** + * Retrieve all diagnostics that were reported during compilation. + * + * @return a list of diagnostics + */ + public List> getDiagnostics() { + return diagnostics; + } + + /** + * Format collected diagnostics into human readable messages that include the + * source line and a caret pointing to the error column when available. + * + * @return list of formatted diagnostic strings + */ + public List getFormattedDiagnostics() { + List messages = new ArrayList<>(); + for (Diagnostic d : diagnostics) { + messages.add(formatDiagnostic(d)); + } + return messages; + } + + private String formatDiagnostic(Diagnostic d) { + StringBuilder sb = new StringBuilder(); + String sourceName = d.getSource() == null ? "Unknown Source" + : new File(d.getSource().getName()).getName(); + + boolean appendedSource = false; + try { + if (d.getSource() != null) { + CharSequence content = d.getSource().getCharContent(true); + String[] lines = content.toString().split("\r?\n"); + long lineNo = d.getLineNumber(); + if (lineNo > 0 && lineNo <= lines.length) { + String line = lines[(int) lineNo - 1]; + sb.append(line).append(System.lineSeparator()); + long col = d.getColumnNumber(); + if (col > 0) { + for (int i = 1; i < col; i++) { + sb.append(' '); + } + sb.append('^'); + } + sb.append(System.lineSeparator()); + appendedSource = true; + } + } + } catch (IOException e) { + // ignore - if we can't read the source, just return the basic message + } + + if (!appendedSource && sb.length() > 0) { + sb.append(System.lineSeparator()); + } + + sb.append(sourceName).append(':').append(d.getLineNumber()).append(':') + .append(' ').append(d.getKind().toString().toLowerCase(Locale.ENGLISH)) + .append(':').append(' ').append(d.getMessage(Locale.ENGLISH)); + + return sb.toString(); + } +} diff --git a/src/main/java/zss/compiler/MemoryCompiler.java b/src/main/java/zss/compiler/MemoryCompiler.java index 6787a0f..ebd56fc 100644 --- a/src/main/java/zss/compiler/MemoryCompiler.java +++ b/src/main/java/zss/compiler/MemoryCompiler.java @@ -6,13 +6,13 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; -import java.util.Locale; - -import javax.tools.JavaCompiler; -import javax.tools.JavaFileObject; -import javax.tools.StandardJavaFileManager; - -import javax.tools.ToolProvider; +import java.util.Locale; + +import javax.tools.JavaCompiler; +import javax.tools.JavaFileObject; +import javax.tools.StandardJavaFileManager; + +import javax.tools.ToolProvider; /** * Compiles in-memory source files into byte code using the standard JDK @@ -60,10 +60,15 @@ public static FromMemoryClassLoader compile(Iterable f Writer out = new PrintWriter(System.out); JavaCompiler.CompilationTask task = compiler.getTask(out, fileManager, diag, options, null, files); - Boolean result = task.call(); - if (result == true) { - return classLoader; - } - return null; - } -} + Boolean result = task.call(); + if (Boolean.TRUE.equals(result)) { + return classLoader; + } + + for (String msg : diag.getFormattedDiagnostics()) { + System.out.println(msg); + System.out.println(); + } + return null; + } +} diff --git a/src/test/java/example/HelloFailureTest.java b/src/test/java/example/HelloFailureTest.java new file mode 100644 index 0000000..e503afe --- /dev/null +++ b/src/test/java/example/HelloFailureTest.java @@ -0,0 +1,69 @@ +package example; + +import java.io.IOException; +import java.io.InputStream; +import java.io.ByteArrayOutputStream; + +import org.junit.Test; +import static org.junit.Assert.*; + +import com.amazonaws.services.lambda.runtime.Context; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; + +import run.myCode.CompileResponse; +import run.myCode.Hello; + +/** + * Tests compilation failures for the Hello lambda. + */ +public class HelloFailureTest { + private Context createContext() { + TestContext ctx = new TestContext(); + + ctx.setFunctionName("HelloFunction"); + + return ctx; + } + + @Test + public void testCompilationFailureShowsDiagnostics() { + CompileResponse resp = doTest("jsonDataFiles/brokenLocal.json"); + + assertFalse("Compiler returned no response", resp == null); + assertFalse("Compilation unexpectedly succeeded", resp.getSucceeded()); + String diag = resp.getResult(); + assertTrue("Missing source line", diag.contains("System.out.println(\"hi\")")); + assertTrue("Missing caret in diagnostics", diag.contains("^")); + assertTrue("Missing file/line in diagnostics", diag.contains("Broken.java:3")); + + int lineIdx = diag.indexOf("System.out.println(\"hi\")"); + int caretIdx = diag.indexOf("^"); + int fileIdx = diag.indexOf("Broken.java:3"); + assertTrue("Caret should follow source line", lineIdx >= 0 && caretIdx > lineIdx); + assertTrue("File info should follow caret", fileIdx > caretIdx); + + System.out.println("Result: " + diag); + } + + private CompileResponse doTest(String resourceName) { + Hello handler = new Hello(); + + System.out.println("Working Directory: " + System.getProperty("java.io.tmpdir")); + ObjectMapper mapper = new ObjectMapper(); + mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + + ByteArrayOutputStream outContent = new ByteArrayOutputStream(); + CompileResponse resp = null; + + try (InputStream input = this.getClass().getClassLoader().getResourceAsStream(resourceName)) { + handler.handleRequest(input, outContent, createContext()); + resp = mapper.readValue(outContent.toString(), CompileResponse.class); + } + catch (IOException e) { + throw new AssertionError(e); + } + + return resp; + } +} diff --git a/src/test/resources/jsonDataFiles/brokenLocal.json b/src/test/resources/jsonDataFiles/brokenLocal.json new file mode 100644 index 0000000..b23ffb9 --- /dev/null +++ b/src/test/resources/jsonDataFiles/brokenLocal.json @@ -0,0 +1,21 @@ +{ +"body": { + "compile": { + "mainClass": "Broken", + "sourceFiles": [{ + "name": "Broken.java", + "contents": [ + "public class Broken {", + " public static void main(String[] args) {", + " System.out.println(\"hi\")", + " }", + "}" + ] + }] + }, + "test-type": "run" +}, +"params": {}, +"context": {}, +"stage-variables": {} +}