prompto.declaration.TestMethodDeclaration Maven / Gradle / Ivy
The newest version!
package prompto.declaration;
import java.io.PrintStream;
import java.lang.reflect.Modifier;
import prompto.compiler.ClassFile;
import prompto.compiler.CompilerUtils;
import prompto.compiler.Descriptor;
import prompto.compiler.ExceptionHandler;
import prompto.compiler.FieldConstant;
import prompto.compiler.Flags;
import prompto.compiler.IInstructionListener;
import prompto.compiler.MethodConstant;
import prompto.compiler.MethodInfo;
import prompto.compiler.OffsetListenerConstant;
import prompto.compiler.Opcode;
import prompto.compiler.StackState;
import prompto.compiler.StringConstant;
import prompto.error.ExecutionError;
import prompto.error.NativeError;
import prompto.error.PromptoError;
import prompto.expression.IAssertion;
import prompto.expression.SymbolExpression;
import prompto.grammar.Identifier;
import prompto.intrinsic.PromptoException;
import prompto.runtime.Context;
import prompto.statement.DeclarationStatement;
import prompto.statement.IStatement;
import prompto.statement.StatementList;
import prompto.transpiler.Transpiler;
import prompto.type.IType;
import prompto.type.VoidType;
import prompto.utils.AssertionList;
import prompto.utils.CodeWriter;
import prompto.value.IInstance;
import prompto.value.IValue;
public class TestMethodDeclaration extends BaseDeclaration {
StatementList statements;
AssertionList assertions;
SymbolExpression error;
public TestMethodDeclaration(Identifier name, StatementList statements, AssertionList assertions, SymbolExpression error) {
super(name);
if(statements==null)
statements = new StatementList();
this.statements = statements;
this.assertions = assertions;
this.error = error;
registerClosures();
}
@SuppressWarnings("unchecked")
private void registerClosures() {
statements.stream()
.filter(s->s instanceof DeclarationStatement)
.map(s->(DeclarationStatement)s)
.forEach(s->s.getDeclaration().setClosureOf(this));
}
@Override
public DeclarationType getDeclarationType() {
return DeclarationType.TEST;
}
public StatementList getStatements() {
return statements;
}
public AssertionList getAssertions() {
return assertions;
}
@Override
public IType check(Context context) {
context = context.newLocalContext();
for(IStatement statement : statements)
checkStatement(context, statement);
if(assertions!=null) {
for(IAssertion assertion : assertions)
context = assertion.checkAssert(context);
}
return VoidType.instance();
}
private void checkStatement(Context context, IStatement statement) {
IType type = statement.check(context);
if(type!=null && type!=VoidType.instance()) // null indicates SyntaxError
context.getProblemListener().reportIllegalReturn(statement);
}
@Override
public void register(Context context) {
context.registerDeclaration(this);
}
@Override
public IType getType(Context context) {
return VoidType.instance();
}
public void interpret(Context context) throws PromptoError {
if(interpretBody(context)) {
interpretError(context);
interpretAsserts(context);
}
}
private void interpretError(Context context) {
// we land here only if no error was raised
if(error!=null)
printFailedAssertion(context, error.getName().toString(), "no error");
}
private void interpretAsserts(Context context) throws PromptoError {
if(assertions==null)
return;
context.enterTest(this);
try {
boolean success = true;
for(IAssertion assertion : assertions)
success &= assertion.interpretAssert(context, this);
if(success)
printSuccess(context);
} finally {
context.leaveSection(this);
}
}
public void printFailedAssertion(Context context, String expected, String actual) {
String message = buildFailedAssertionMessagePrefix(expected);
System.out.println(message + actual); // TODO use collector but NOT logger
}
public String buildFailedAssertionMessagePrefix(String expected) {
return getName() + " test failed while verifying: " + expected + ", found: ";
}
public void printMissingError(Context context, String expected, String actual) {
String message = buildMissingErrorMessagePrefix(expected);
System.out.println(message + actual); // TODO use collector but NOT logger
}
public String buildMissingErrorMessagePrefix(String expected) {
return getName() + " test failed while expecting: " + expected + ", found: ";
}
private void printSuccess(Context context) {
System.out.println(buildSuccessMessage()); // TODO use collector but NOT logger
}
public String buildSuccessMessage() {
return getName() + " test successful";
}
private boolean interpretBody(Context context) throws PromptoError {
context.enterTest(this);
try {
statements.interpret(context);
return true;
} catch(ExecutionError e) {
interpretError(context, e);
// no more to execute
return false;
} finally {
context.leaveSection(this);
}
}
private void interpretError(Context context, ExecutionError e) throws PromptoError {
IValue actual = e.interpret(context, new Identifier("__test_error__"));
IValue expectedError = error==null ? null : error.interpret(context);
if(expectedError!=null && expectedError.equals(actual))
printSuccess(context);
else {
String actualName = getErrorName(context, e, actual);
String expectedName = error==null ? "SUCCESS" : error.getName().toString();
printMissingError(context, expectedName, actualName);
}
}
private String getErrorName(Context context, ExecutionError e, IValue actual) {
if(actual instanceof IInstance)
return ((IInstance)actual).getMember(context, new Identifier("name"), false).toString();
else if(e instanceof NativeError)
return "NATIVE_ERROR";
else
return actual.toString();
}
@Override
public void declarationToDialect(CodeWriter writer) {
if(writer.isGlobalContext())
writer = writer.newLocalWriter();
switch(writer.getDialect()) {
case E:
toEDialect(writer);
break;
case O:
toODialect(writer);
break;
case M:
toMDialect(writer);
break;
}
}
protected void toMDialect(CodeWriter writer) {
writer.append("def test ");
writer.append(getName());
writer.append(" ():\n");
writer.indent();
statements.toDialect(writer);
writer.dedent();
writer.append("verifying:");
if(error!=null) {
writer.append(" ");
error.toDialect(writer);
writer.append("\n");
} else {
writer.append("\n");
writer.indent();
assertions.toDialect(writer);
writer.dedent();
}
}
protected void toEDialect(CodeWriter writer) {
writer.append("define ");
writer.append(getName());
writer.append(" as test method doing:\n");
writer.indent();
statements.toDialect(writer);
writer.dedent();
writer.append("and verifying");
if(error!=null) {
writer.append(" ");
error.toDialect(writer);
writer.append("\n");
} else {
writer.append(":\n");
writer.indent();
assertions.toDialect(writer);
writer.dedent();
}
}
protected void toODialect(CodeWriter writer) {
writer.append("test method ");
writer.append(getName());
writer.append(" () {\n");
writer.indent();
statements.toDialect(writer);
writer.dedent();
writer.append("} verifying ");
if(error!=null) {
error.toDialect(writer);
writer.append(";\n");
} else {
writer.append("{\n");
writer.indent();
assertions.toDialect(writer);
writer.dedent();
writer.append("}\n");
}
}
public ClassFile compile(Context context, String fullName) {
context = context.newLocalContext();
java.lang.reflect.Type type = CompilerUtils.abstractTypeFrom(fullName);
ClassFile classFile = new ClassFile(type);
classFile.addModifier(Modifier.ABSTRACT);
Descriptor.Method proto = new Descriptor.Method(void.class);
MethodInfo method = classFile.newMethod("run", proto);
method.addModifier(Modifier.STATIC);
if(error!=null)
compileTestWithError(context, method, new Flags());
else
compileTestWithAsserts(context, method, new Flags());
return classFile;
}
private void compileTestWithAsserts(Context context, MethodInfo method, Flags flags) {
// don't use statements.compile because we need the locals for the assertions
statements.forEach((s)->
s.compile(context, method, flags));
method.addInstruction(Opcode.ICONST_0); // failures counter
assertions.forEach((a)->
a.compileAssert(context, method, flags, this));
compileCheckSuccess(context, method, flags);
method.addInstruction(Opcode.RETURN);
}
private void compileCheckSuccess(Context context, MethodInfo method, Flags flags) {
IInstructionListener finalListener = method.addOffsetListener(new OffsetListenerConstant());
method.activateOffsetListener(finalListener);
method.addInstruction(Opcode.IFNE, finalListener); // 0 = no failures
StackState finalState = method.captureStackState();
compileSuccess(context, method, flags);
// final
method.restoreFullStackState(finalState);
method.placeLabel(finalState);
method.inhibitOffsetListener(finalListener);
}
public void compileSuccess(Context context, MethodInfo method, Flags flags) {
String message = buildSuccessMessage();
method.addInstruction(Opcode.LDC, new StringConstant(message));
compilePrintResult(context, method, flags);
}
public void compileFailure(Context context, MethodInfo method, Flags flags) {
compilePrintResult(context, method, flags);
}
public void compilePrintResult(Context context, MethodInfo method, Flags flags) {
// the message is on top of the stack
FieldConstant fc = new FieldConstant(System.class, "out", PrintStream.class);
method.addInstruction(Opcode.GETSTATIC, fc);
method.addInstruction(Opcode.SWAP);
MethodConstant mc = new MethodConstant(PrintStream.class, "println", String.class, void.class);
method.addInstruction(Opcode.INVOKEVIRTUAL, mc);
}
private void compileTestWithError(Context context, MethodInfo method, Flags flags) {
ExceptionHandler expected = installExpectedExceptionHandler(context, method, flags);
ExceptionHandler unexpected = installUnexpectedExceptionHandler(context, method, flags);
statements.compile(context, method, flags);
// missing exception
compileMissingExceptionHandler(context, method, flags);
method.addInstruction(Opcode.RETURN);
// expected exception
compileExpectedExceptionHandler(context, method, flags, expected);
method.addInstruction(Opcode.RETURN);
// unexpected exception
compileUnexpectedExceptionHandler(context, method, flags, unexpected);
method.addInstruction(Opcode.RETURN);
}
private void compileUnexpectedExceptionHandler(Context context, MethodInfo method, Flags flags, ExceptionHandler handler) {
method.placeExceptionHandler(handler);
// get actual exception type name
MethodConstant mc = new MethodConstant(PromptoException.class, "getExceptionTypeName", Object.class, String.class);
method.addInstruction(Opcode.INVOKESTATIC, mc);
// produce failure message
String message = buildMissingErrorMessagePrefix(error.getName().toString());
method.addInstruction(Opcode.LDC, new StringConstant(message));
method.addInstruction(Opcode.SWAP);
mc = new MethodConstant(String.class, "concat", String.class, String.class);
method.addInstruction(Opcode.INVOKEVIRTUAL, mc);
// done
compilePrintResult(context, method, flags);
}
private void compileExpectedExceptionHandler(Context context, MethodInfo method, Flags flags, ExceptionHandler handler) {
method.placeExceptionHandler(handler);
method.addInstruction(Opcode.POP); // the thrown exception
compileSuccess(context, method, flags);
}
private void compileMissingExceptionHandler(Context context, MethodInfo method, Flags flags) {
// produce failure
String message = buildMissingErrorMessagePrefix(error.getName().toString()) + "no error";
method.addInstruction(Opcode.LDC, new StringConstant(message));
compilePrintResult(context, method, flags);
}
private ExceptionHandler installUnexpectedExceptionHandler(Context context, MethodInfo method, Flags flags) {
ExceptionHandler handler = method.registerExceptionHandler(Throwable.class);
method.activateOffsetListener(handler);
return handler;
}
private ExceptionHandler installExpectedExceptionHandler(Context context, MethodInfo method, Flags flags) {
java.lang.reflect.Type type = null;
switch(error.getName()) {
case "DIVIDE_BY_ZERO":
type = ArithmeticException.class;
break;
case "INDEX_OUT_OF_RANGE":
type = IndexOutOfBoundsException.class;
break;
case "NULL_REFERENCE":
type = NullPointerException.class;
break;
default:
type = error.getJavaType(context);
}
ExceptionHandler handler = method.registerExceptionHandler(type);
method.activateOffsetListener(handler);
return handler;
}
@Override
public void declare(Transpiler transpiler) {
transpiler.require("NativeError");
transpiler.declare(this);
transpiler = transpiler.newLocalTranspiler();
this.statements.declare(transpiler);
if(this.assertions!=null)
this.assertions.declare(transpiler);
if(this.error!=null)
this.error.declare(transpiler);
}
@Override
public boolean transpile(Transpiler transpiler) {
transpiler = transpiler.newLocalTranspiler();
if (this.error!=null)
this.transpileExpectedError(transpiler);
else
this.transpileAssertions(transpiler);
transpiler.flush();
return true;
}
private void transpileAssertions(Transpiler transpiler) {
transpiler.append("function ").append(this.getTranspiledName()).append("() {");
transpiler.indent();
transpiler.append("try {");
transpiler.indent();
statements.transpile(transpiler);
transpiler.append("var success = true;").newLine();
assertions.forEach(assertion -> {
transpiler.append("if(");
assertion.transpile(transpiler);
transpiler.append(")").indent();
transpiler.append("success &= true;").dedent();
transpiler.append("else {").indent();
transpiler.append("success = false;").newLine();
transpiler.printTestName(this.getName()).append("failed while verifying: ");
transpiler.escape();
transpiler.append(assertion.getExpected(transpiler.getContext(), this.getDialect(), transpiler.getEscapeMode()));
transpiler.unescape();
transpiler.append(", found: ' + ");
transpiler.escape();
assertion.transpileFound(transpiler, this.getDialect());
transpiler.unescape();
transpiler.append(");");
transpiler.dedent();
transpiler.append("}").newLine();
});
transpiler.append("if (success)").indent().printTestName(this.getName()).append("successful');").dedent();
transpiler.dedent();
transpiler.append("} catch (e) {");
transpiler.indent();
transpiler.printTestName(this.getName()).append("failed with error: ' + e.name);");
transpiler.dedent();
transpiler.append("}");
transpiler.dedent();
transpiler.append("}");
transpiler.newLine();
transpiler.flush();
}
public String getTranspiledName() {
String name = this.getName();
return name.substring(1, name.length()-1).replaceAll("\\W","_");
}
private void transpileExpectedError(Transpiler transpiler) {
transpiler.append("function ").append(this.getTranspiledName()).append("() {");
transpiler.indent();
transpiler.append("try {");
transpiler.indent();
this.statements.transpile(transpiler);
transpiler.printTestName(this.getName()).append("failed while expecting: ").append(this.error.getName()).append(", found: no error');");
transpiler.dedent();
transpiler.append("} catch (e) {");
transpiler.indent();
transpiler.append("if(e instanceof NativeErrors.").append(this.error.getName()).append(") {").indent();
transpiler.printTestName(this.getName()).append("successful');").dedent();
transpiler.append("} else {").indent();
transpiler.printTestName(this.getName()).append("failed while expecting: ").append(this.error.getName()).append(", found: ' + translateError(e));").dedent();
transpiler.append("}");
transpiler.dedent();
transpiler.append("}");
transpiler.dedent();
transpiler.append("}");
transpiler.newLine();
transpiler.flush();
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy