Simon Warta
Simon Warta

Reputation: 11408

String representation of an expression for assertion implementation

Since Java's assert keyword is fundamentally broken on Android, I am about to implement an assertion class that can be configured to check assertions in release builds as well.

Now I can do something like:

MyAssertion.assert(a != 2)

which throws an AssertionException when the expression is false. But how can I get a String representation of the expression to pass to the error message?

Upvotes: 2

Views: 1015

Answers (4)

Kafkaesque
Kafkaesque

Reputation: 1283

Annotation processing can do this. You'd create an annotation e.g. @InlineAssertExpressions. Then write a processor that parses your source file and creates a string representing the expression and adds it to the call to your assert method, which you could overload to take an optional String argument. This way is quite optimal performance-wise, since the inlining happens compile-time. Annotation processing is a bit overlooked, but I think this is a great use for it.

Upvotes: 4

Michal
Michal

Reputation: 1163

I think you cannot access the internal java expression, which was passed to method call.

You can, however, use some expression language libraries and do a custom expression handling for your assertion implementation.

Here is a list of some expression language libraries:

http://java-source.net/open-source/expression-languages

Hope that helps :)

Upvotes: 2

ursa
ursa

Reputation: 4591

(1) Simple to use, but hard to implement. Java 8 required. You can use lambda expressions:

Assert.isTrue(() => a != 2)

On evaluation failure your implementation of Assert.isTrue method should repeat all steps as IDEs do - (a) discover bytecode of lambda class, (b) decompile e.g. with JAD, (c) discover sources if available

(2) Simple to use, simple to implement, but does not fully cover your requirements. You can use CodeStyle rules to check & force correct assertions usage. One regexp will check there is no single-argument assertions, the second (using regexp back refs) will check code and text description are similar.

(3) Simple to use, simple to implement, but relies on your build system. You can automatically check and fix source code during project build.

E.g. for Maven build system you can create your own plugin to check and fix assertions usage in sources on process-sources stage.

Upvotes: 2

Tunaki
Tunaki

Reputation: 137084

The only way is to add a String parameter to your assert method:

MyAssertion.assert(a != 2, "a must not be equal to 2");

What you get as input for assert is either true or false so you can't build a representative String from that.

Otherwise, you could implement assert like this:

MyAssertion.assertNotEquals(a, 2);

When this fails, you know that it is because what you tested was equal to 2 and you can build an informative message (though you won't know what specifically was equal to 2).


If you want to somehow be able to construct a meaningful message from an assertion, the only way I see it possible is to construct an String expression, ask the JavaScript engine to evaluate it and build a message if the expression evaluates to false. Note that will degrade a lot performance as launching the JavaScript engine takes a lot of time. This could be solved with a mechanism of disabling assertions in production.

The following is an example of that. Note that I'm using the new Java 8 Nashorn JavaScript engine but this should work with the older Rhino.

Usage example:

int value = 3;
String str = "test";
Assertions.assertTrue("$1 == 3", value);
Assertions.assertTrue("$1 == 3 && $2 == 'test'", value, str);
Assertions.assertTrue("$1 == 4 && $2 == 'test'", value, str);

This will throw for the 3rd assertion:

An assertion has failed: 3 == 4 && 'test' == 'test'

The idea is that you can write any JavaScript-friendly expression that can be evaluated to a boolean. The placeholders $i will be replaced by what's given as a parameter to the method ($1 will be replaced by the first parameter, etc.).

This is the class. It could be improved (handle error conditions like not enough parameters, etc.) but this should be enough to get you started.

public final class Assertions {

    private static final ScriptEngine ENGINE = new ScriptEngineManager().getEngineByName("nashorn");

    private Assertions() { }

    public static void assertTrue(String expression, Object... values) {
        for (int i = 0; i < values.length; i++) {
            ENGINE.put("$" + (i+1), values[i]);
        }
        try {
            boolean pass = (Boolean) ENGINE.eval(expression);
            if (!pass) {
                for (int i = 0; i < values.length; i++) {
                    expression = expression.replace("$" + (i+1), stringRepresentation(values[i]));
                }
                throw new AssertionError("An assertion has failed: " + expression);
            }
        } catch (ScriptException e) {
            throw new InternalError(e);
        } finally {
            for (int i = 0; i < values.length; i++) {
                ENGINE.getBindings(ScriptContext.ENGINE_SCOPE).remove("$" + (i+1));
            }
        }
    }

    private static String stringRepresentation(Object o) {
        if (o instanceof String) {
            return "'" + o + "'";
        }
        return o.toString();
    }

}

Upvotes: 5

Related Questions