www.digitalmars.com         C & C++   DMDScript  

digitalmars.D.bugs - [Issue 4644] New: assertExceptionThrown to assert that a particular exception was thrown

reply d-bugmail puremagic.com writes:
http://d.puremagic.com/issues/show_bug.cgi?id=4644

           Summary: assertExceptionThrown to assert that a particular
                    exception was thrown
           Product: D
           Version: D2
          Platform: Other
        OS/Version: Linux
            Status: NEW
          Severity: enhancement
          Priority: P2
         Component: Phobos
        AssignedTo: nobody puremagic.com
        ReportedBy: jmdavisProg gmail.com


--- Comment #0 from Jonathan M Davis <jmdavisProg gmail.com> 2010-08-14
21:11:36 PDT ---
I would love a function in std.exception which could be used with unit tests to
verify that a particular exception was thrown rather than verify the return
value of a function. So, instead of writing

try
{
    func(/*...*/);
    assert(0, "Exception not thrown");
}
catch(Exception e)
{
}


you'd write something like

assertExceptionThrown(func(/*...*/), Exception);


where you give it the function call and the exception type to catch, which
would then translate to the code above. Now, I'm not sure that you can get
quite that syntax, since you'd be passing a function call with arguments and
all rather a function pointer or delegate with arguments, but there might be a
way to do it more like

assertExceptionThrown(Exception, &func, /*...*/);


and give it a function pointer or delegate with arguments. I'm not enough of a
template wiz to know the best way to build assertExceptionThrown() (I'm not
even sure that I know enough yet to build it all), so I don't know the best way
to do it, but it seems like it should be possible one way or another. And it
would certainly be useful.

-- 
Configure issuemail: http://d.puremagic.com/issues/userprefs.cgi?tab=email
------- You are receiving this mail because: -------
Aug 14 2010
next sibling parent d-bugmail puremagic.com writes:
http://d.puremagic.com/issues/show_bug.cgi?id=4644



--- Comment #1 from Jonathan M Davis <jmdavisProg gmail.com> 2010-08-15
01:49:07 PDT ---
A useful counterpart might be assertExceptionNotThrown, though I think that an
exception escaping the unittest block equates to test failure, so I'm not sure
that it would be necessary. It would make tests clearer, however, since instead
of having seemingly pointless statements, you would be clearly indicating that
you were making sure that that statement didn't result in a thrown exception.

-- 
Configure issuemail: http://d.puremagic.com/issues/userprefs.cgi?tab=email
------- You are receiving this mail because: -------
Aug 15 2010
prev sibling next sibling parent d-bugmail puremagic.com writes:
http://d.puremagic.com/issues/show_bug.cgi?id=4644


Andrej Mitrovic <andrej.mitrovich gmail.com> changed:

           What    |Removed                     |Added
----------------------------------------------------------------------------
                 CC|                            |andrej.mitrovich gmail.com


--- Comment #2 from Andrej Mitrovic <andrej.mitrovich gmail.com> 2010-08-15
14:00:01 PDT ---
Here, I made something similar to your request:

import std.stream, std.stdio;

auto assertExceptionThrown(alias func, alias exc, T...)(T args)
{
    auto funcName = __traits(identifier, func);

    try
    {
        func(args);
    }
    catch (exc e)
    {
    }
    catch (Exception e)
    {
        writefln("Error: Call to %s did not throw %s", funcName, typeid(exc));
        writeln(e);
    }
}

unittest
{
    assertExceptionThrown!(willThrow, core.exception.AssertError)(4);
}

void willThrow(int i)
{
    //~ assert(0);
    writefln("willThrow using its passed argument %s..", i);
    throw new SeekException("Oops a file exploded!");
}

void main()
{
}

If you uncomment the assert(0) statement in the willThrow function, the
unittest will run fine, since it catches an AssertError exception, the one you
passed with the call to assertExceptionThrown.

It's just a prototype, but try it out and let me know if it works (I'm an
exception newbie myself tbh..)

-- 
Configure issuemail: http://d.puremagic.com/issues/userprefs.cgi?tab=email
------- You are receiving this mail because: -------
Aug 15 2010
prev sibling next sibling parent d-bugmail puremagic.com writes:
http://d.puremagic.com/issues/show_bug.cgi?id=4644



--- Comment #3 from Andrej Mitrovic <andrej.mitrovich gmail.com> 2010-08-15
14:14:28 PDT ---
Sorry, that still won't work. I haven't made sure that the function *must*
throw an exception. I'll work on it some more, stand by.. :)

-- 
Configure issuemail: http://d.puremagic.com/issues/userprefs.cgi?tab=email
------- You are receiving this mail because: -------
Aug 15 2010
prev sibling next sibling parent d-bugmail puremagic.com writes:
http://d.puremagic.com/issues/show_bug.cgi?id=4644



--- Comment #4 from Andrej Mitrovic <andrej.mitrovich gmail.com> 2010-08-15
14:34:21 PDT ---
There we go, this should now work:


import std.stream, std.stdio;

void assertExceptionThrown(alias func, alias exc, T...)(T args)
{
    auto funcName = __traits(identifier, func);

    try
    {
        func(args);
    }
    catch (exc e)
    {
        return;     // Expected exception was thrown
    }
    catch (Exception e)
    {
        writeln(e);
    }

    writefln("Error: Call to %s did not throw %s", funcName, typeid(exc));
    assert(0);
}

unittest
{
    assertExceptionThrown!(willThrow, core.exception.AssertError)(4);
}

void willThrow(int i)
{
    //~ assert(0);
    //~ throw new SeekException("Oops a file exploded!");
}

void main()
{
}


So, if you leave it like this it will complain about the expected exception not
being thrown. 

If you uncomment the first line, it will run silently and return. 

I did add another gerenal Exception handler in the templated function, so when
a completely different exception is thrown (in this case just uncomment the
second line here) you will still be notified that the *expected* exception did
not throw even though another one did.

-- 
Configure issuemail: http://d.puremagic.com/issues/userprefs.cgi?tab=email
------- You are receiving this mail because: -------
Aug 15 2010
prev sibling next sibling parent d-bugmail puremagic.com writes:
http://d.puremagic.com/issues/show_bug.cgi?id=4644



--- Comment #5 from Andrej Mitrovic <andrej.mitrovich gmail.com> 2010-08-15
14:36:13 PDT ---
I meant "general", not "gerenal". And when I talk about "first line" and
"second line", I mean the ones in the willThrow function.

-- 
Configure issuemail: http://d.puremagic.com/issues/userprefs.cgi?tab=email
------- You are receiving this mail because: -------
Aug 15 2010
prev sibling next sibling parent d-bugmail puremagic.com writes:
http://d.puremagic.com/issues/show_bug.cgi?id=4644



--- Comment #6 from Jonathan M Davis <jmdavisProg gmail.com> 2010-08-16
01:03:07 PDT ---
Thanks for the help. One thing that I'm finding you need with these types of
functions though is the file and line number. Otherwise, tracking down the
problem can be quite hard. I suppose if the stack trace on linux actually
printed out the functions rather than just their addresses, then it wouldn't be
as big a problem, but as it is, the stack trace is pretty useless. Normally,
I'd tack the file and line number on the end with the default arguments of
__FILE__ and __LINE__, but with the variadic template, I don't think that you
can do that. So, you'd have to be explicit here. In any case, here are my
current versions. They're a bit more concise than yours and work better with
unit tests (given that you shouldn't normally print from unit tests). They're a
bit uglier to use due to the necessity to pass __FILE__ and __LINE__ yourself,
but I think that they work quite well for unit tests at the moment. I see no
need to catch the wrong exception, since it'll just wiz on by and get caught by
the main exception handler like any other exception. The key here is to verify
whether the correct one was thrown. For assertExceptionNotThrown, I would have
printed out the Exception as part of the AssertError that get's thrown, but you
end up with 2 stack traces, and it's ugly. Hopefully, the programmer would have
a fair idea of what Exception would be thrown, and they can go and run the
function without the wrapper if they need to. The function still serves nicely
to indicate test failure however.

void assertExceptionThrown(string file, size_t line, alias E, alias func,
T...)(T args)
    if(__traits(compiles, {try{}catch(E e){}}))
{
    try
        func(args);
    catch(E e)
        return;     // Expected exception was thrown

    throw new AssertError(format("assertExceptionThrown() failed: No %s was
thrown from %s()", E.stringof, __traits(identifier, func)), file, line);
}

void assertExceptionThrown(string msg, string file, size_t line, alias E, alias
func, T...)(T args)
    if(__traits(compiles, {try{}catch(E e){}}))
{
    try
        func(args);
    catch(E e)
        return;     // Expected exception was thrown

    throw new AssertError(format("assertExceptionThrown() failed: No %s was
thrown from %s(): %s", E.stringof, __traits(identifier, func), msg), file,
line);
}

void assertExceptionNotThrown(string file, size_t line, alias E, alias func,
T...)(T args)
    if(__traits(compiles, {try{}catch(E e){}}))
{
    try
        func(args);
    catch(E e)
        throw new AssertError(format("assertExceptionNotThrown() failed: %s was
thrown from %s()", E.stringof, __traits(identifier, func)), file, line);
}

void assertExceptionNotThrown(string msg, string file, size_t line, alias E,
alias func, T...)(T args)
    if(__traits(compiles, {try{}catch(E e){}}))
{
    try
        func(args);
    catch(E e)
        throw new AssertError(format("assertExceptionNotThrown() failed: %s was
thrown from %s(): %s", E.stringof, __traits(identifier, func), msg), file,
line);
}


I really think that we should have more of these types of functions and have
them added to std.exception (or wherever appropriate), so that we can have
better unit testing facilities. assert does the job, but it's not a very
precise instrument. And in this case, it's too verbose due to the necessary
try-catch block.

-- 
Configure issuemail: http://d.puremagic.com/issues/userprefs.cgi?tab=email
------- You are receiving this mail because: -------
Aug 16 2010
prev sibling next sibling parent d-bugmail puremagic.com writes:
http://d.puremagic.com/issues/show_bug.cgi?id=4644



--- Comment #7 from Andrej Mitrovic <andrej.mitrovich gmail.com> 2010-08-16
06:55:46 PDT ---
Nice work!

Btw, don't templates already have __FILE__ and __LINE__ ?

See here:

http://www.digitalmars.com/d/2.0/template.html

Under "Template Value Parameters" it states "The __FILE__ and __LINE__ expand
to the source file name and line number at the point of instantiation."

-- 
Configure issuemail: http://d.puremagic.com/issues/userprefs.cgi?tab=email
------- You are receiving this mail because: -------
Aug 16 2010
prev sibling next sibling parent d-bugmail puremagic.com writes:
http://d.puremagic.com/issues/show_bug.cgi?id=4644



--- Comment #8 from Andrej Mitrovic <andrej.mitrovich gmail.com> 2010-08-16
11:23:52 PDT ---
Ah I see the problem now. __FILE__ and __LINE__ can be used to initialize a
parameter as a default value, but the only way they can be optional is if
they're the last parameters of the template function. I'll see if there can be
a way to work around this..

-- 
Configure issuemail: http://d.puremagic.com/issues/userprefs.cgi?tab=email
------- You are receiving this mail because: -------
Aug 16 2010
prev sibling next sibling parent d-bugmail puremagic.com writes:
http://d.puremagic.com/issues/show_bug.cgi?id=4644



--- Comment #9 from Andrej Mitrovic <andrej.mitrovich gmail.com> 2010-08-16
13:04:56 PDT ---
Good news, I've found a way to pass __LINE__ and __FILE__. It's a tad bit ugly
and uses a struct, I haven't been able to call a struct's custom constructor
without calling it with an extra parameter. I'm trying to call my struct
constructor without passing any arguments, but it doesn't seem to work. So I'm
using a workaround. Anyway, check it out:

void assertExceptionThrown(alias E, alias func, T...)(lineInfo info, T args)
    if(__traits(compiles, {try{}catch(E e){}}))
{
    try
        func(args);
    catch(E e)
        return;     // Expected exception was thrown

    throw new AssertError(format("assertExceptionThrown() failed: No %s was
thrown from %s()", E.stringof, __traits(identifier, func)), info._file,
info._line);
}


import std.stdio, std.stream, core.exception, std.string, std.typecons;

int myfunc(int i)
{
    return i;
}

struct lineInfo
{
    string _file;
    uint _line;

    this(int x, string file = __FILE__, uint line = __LINE__)
    {
        _file = file;
        _line = line;
    }
}


void main()
{
    assertExceptionThrown!(core.exception.AssertError, myfunc)(lineInfo(0) ,
5);
}

-- 
Configure issuemail: http://d.puremagic.com/issues/userprefs.cgi?tab=email
------- You are receiving this mail because: -------
Aug 16 2010
prev sibling next sibling parent d-bugmail puremagic.com writes:
http://d.puremagic.com/issues/show_bug.cgi?id=4644



--- Comment #10 from Andrej Mitrovic <andrej.mitrovich gmail.com> 2010-08-17
08:04:24 PDT ---
I have a revamped edition, this one is much nicer:

void assertExceptionThrown(alias E, alias func, T...)(lineFile info, T args)
    if(__traits(compiles, {try{}catch(E e){}}))
{
    try
        func(args);
    catch(E e)
        return;     // Expected exception was thrown

    throw new AssertError(format("assertExceptionThrown() failed: No %s was
thrown from %s()", E.stringof, __traits(identifier, func)), info._file,
info._line);
}

void assertExceptionThrown(string msg, alias E, alias func, T...)(lineFile
info, T args)
    if(__traits(compiles, {try{}catch(E e){}}))
{
    try
        func(args);
    catch(E e)
        return;     // Expected exception was thrown

    throw new AssertError(format("assertExceptionThrown() failed: No %s was
thrown from %s(): %s", E.stringof, __traits(identifier, func), msg),
info._file, info._line);
}

void assertExceptionNotThrown(alias E, alias func, T...)(lineFile info, T args)
    if(__traits(compiles, {try{}catch(E e){}}))
{
    try
        func(args);
    catch(E e)
        throw new AssertError(format("assertExceptionNotThrown() failed: %s was
thrown from %s()", E.stringof, __traits(identifier, func)), info._file,
info._line);
}

void assertExceptionNotThrown(string msg, alias E, alias func, T...)(lineFile
info, T args)
    if(__traits(compiles, {try{}catch(E e){}}))
{
    try
        func(args);
    catch(E e)
        throw new AssertError(format("assertExceptionNotThrown() failed: %s was
thrown from %s(): %s", E.stringof, __traits(identifier, func), msg),
info._file, info._line);
}


import std.stdio, std.stream, core.exception, std.string, std.typecons;

int myfunc(int i)
{
    return i;
}

struct lineFile
{
    string _file;
    uint _line;

     property
    auto call(string file = __FILE__, uint line = __LINE__)
    {
        _file = file;
        _line = line;
        return this;
    }
}

void main()
{
    lineFile info;
    assertExceptionThrown!(core.exception.AssertError, myfunc)(info.call, 5);
}

Btw, I'm not sure I understand how you're supossed to call the template that
has "string msg" as a type parameter? Isn't that supossed to be next to the
function arguments, as in this:

void assertExceptionThrown(alias E, alias func, T...)(string msg, lineFile
info, T args)

instead of this:

void assertExceptionThrown(string msg, alias E, alias func, T...)(lineFile
info, T args)

?

-- 
Configure issuemail: http://d.puremagic.com/issues/userprefs.cgi?tab=email
------- You are receiving this mail because: -------
Aug 17 2010
prev sibling next sibling parent d-bugmail puremagic.com writes:
http://d.puremagic.com/issues/show_bug.cgi?id=4644



--- Comment #11 from Jonathan M Davis <jmdavisProg gmail.com> 2010-08-17
12:08:49 PDT ---
I'm not sure that I understand your question about the msg parameter. If you
put it in with the function arguments, then you _have_ to give a message every
time because you can't use default parameters for the first argument. And since
it would likely be the rare case to add an additional message, that would be
annoying. With it as an additional template argument, you can have two
templates - the normal one which omits the message and the more rarely used one
which includes it.

I'd argue that for simplicity's sake, your lineFile struct really shouldn't
need to be declared. You should just be able to make a function call (be it a
standalone function or a static one on the struct itself) and have it return
the struct. I think that the struct should get in the way as little as possible
(since ideally, it wouldn't even be there at all). You could probably just use
opCall() and do this:

assertExceptionThrown!(AssertError, myfunc)(LineInfo(), 5);

Also, we might want to rename the template functions to assertExcThrown() and
assertExcNotThrown() just because the names are painfully long, albeit nicely
descriptive.

-- 
Configure issuemail: http://d.puremagic.com/issues/userprefs.cgi?tab=email
------- You are receiving this mail because: -------
Aug 17 2010
prev sibling next sibling parent d-bugmail puremagic.com writes:
http://d.puremagic.com/issues/show_bug.cgi?id=4644



--- Comment #12 from Jonathan M Davis <jmdavisProg gmail.com> 2010-08-17
12:10:30 PDT ---
Oh, and for correctness' sake, I believe that __LINE__ is in fact size_t, not
uint.

-- 
Configure issuemail: http://d.puremagic.com/issues/userprefs.cgi?tab=email
------- You are receiving this mail because: -------
Aug 17 2010
prev sibling next sibling parent d-bugmail puremagic.com writes:
http://d.puremagic.com/issues/show_bug.cgi?id=4644



--- Comment #13 from Andrej Mitrovic <andrej.mitrovich gmail.com> 2010-08-17
12:41:48 PDT ---
(In reply to comment #11)
 I'm not sure that I understand your question about the msg parameter. If you
 put it in with the function arguments, then you _have_ to give a message every
 time because you can't use default parameters for the first argument. And since
 it would likely be the rare case to add an additional message, that would be
 annoying. With it as an additional template argument, you can have two
 templates - the normal one which omits the message and the more rarely used one
 which includes it.
 
 I'd argue that for simplicity's sake, your lineFile struct really shouldn't
 need to be declared. You should just be able to make a function call (be it a
 standalone function or a static one on the struct itself) and have it return
 the struct. I think that the struct should get in the way as little as possible
 (since ideally, it wouldn't even be there at all). You could probably just use
 opCall() and do this:
 
 assertExceptionThrown!(AssertError, myfunc)(LineInfo(), 5);
 
 Also, we might want to rename the template functions to assertExcThrown() and
 assertExcNotThrown() just because the names are painfully long, albeit nicely
 descriptive.
Yeah, I was looking for a way to get rid of having to create a struct. You'll have to forgive me, I'm still learning about structs from TDPL and didn't know about opCall(). :) As for size_t, well usually I try to request a type of an identifier with typeid(), and in this case I get an int back (not sure why I've put uint there). I guess size_t is aliased to int..? As for the template, I didn't know you could put both type parameters and value parameters in the first pair of paranthesis. Btw, I'm getting errors with this call: lineFile info; assertExceptionThrown!("test", core.exception.AssertError, myfunc)(info.call, 5); test.d(86): Error: tuple T is used as a type It only works if I comment out the first overloaded template. -- Configure issuemail: http://d.puremagic.com/issues/userprefs.cgi?tab=email ------- You are receiving this mail because: -------
Aug 17 2010
prev sibling next sibling parent d-bugmail puremagic.com writes:
http://d.puremagic.com/issues/show_bug.cgi?id=4644



--- Comment #14 from Jonathan M Davis <jmdavisProg gmail.com> 2010-08-17
15:48:08 PDT ---
Created an attachment (id=722)
Implementation for assertExcThrown() and assertExcNotThrown()

Thanks, to Andrej's help, I think that I have an acceptable, working version of
these functions. So, I'm attaching them along with unit tests for them. They
also include ddoc comments.

-- 
Configure issuemail: http://d.puremagic.com/issues/userprefs.cgi?tab=email
------- You are receiving this mail because: -------
Aug 17 2010
prev sibling next sibling parent d-bugmail puremagic.com writes:
http://d.puremagic.com/issues/show_bug.cgi?id=4644



--- Comment #15 from Jonathan M Davis <jmdavisProg gmail.com> 2010-08-17
15:57:03 PDT ---
 Andrej

size_t is an alias appropriate for whatever your architecture is. IIRC, it's
the same size as pointers are, but I'm not 100% sure on that. It's also the
type used for indexing arrays. For 32-bit, that would be int or uint. For
64-bit, that's going to be long or ulong.

I'm not sure why you're getting that error, but it works fine with the version
that I just attached to the bug. Overall, I think that the functions look good.
Ideally, you wouldn't have to use LineInfo at all, but thanks to variadic
templates, we're kind of stuck. It was a good idea though. In any case, I think
that we now have a proper, working implementation of these functions. Now if
they'd only be put into Phobos... ;)

-- 
Configure issuemail: http://d.puremagic.com/issues/userprefs.cgi?tab=email
------- You are receiving this mail because: -------
Aug 17 2010
prev sibling next sibling parent d-bugmail puremagic.com writes:
http://d.puremagic.com/issues/show_bug.cgi?id=4644



--- Comment #16 from Andrej Mitrovic <andrej.mitrovich gmail.com> 2010-08-17
16:00:36 PDT ---
Nicely done with the formatting and documentation, I hope it gets added. I can
confirm that the unittests run fine now.

-- 
Configure issuemail: http://d.puremagic.com/issues/userprefs.cgi?tab=email
------- You are receiving this mail because: -------
Aug 17 2010
prev sibling parent d-bugmail puremagic.com writes:
http://d.puremagic.com/issues/show_bug.cgi?id=4644


Jonathan M Davis <jmdavisProg gmx.com> changed:

           What    |Removed                     |Added
----------------------------------------------------------------------------
             Status|NEW                         |RESOLVED
         Resolution|                            |FIXED


--- Comment #17 from Jonathan M Davis <jmdavisProg gmx.com> 2011-03-21 22:54:37
PDT ---
Reviewed on D newsgroup and an improved version added to Phobos:

https://github.com/D-Programming-Language/phobos/pull/12
https://github.com/D-Programming-Language/phobos/commit/169f4225bf8410040165ac38a0e17fc7f5d79b2b

-- 
Configure issuemail: http://d.puremagic.com/issues/userprefs.cgi?tab=email
------- You are receiving this mail because: -------
Mar 21 2011