Skip to content
This repository was archived by the owner on Oct 12, 2022. It is now read-only.
/ druntime Public archive

Comments

Modify unittest handling#1685

Merged
andralex merged 2 commits intodlang:masterfrom
schveiguy:exitafterunittest
Dec 5, 2017
Merged

Modify unittest handling#1685
andralex merged 2 commits intodlang:masterfrom
schveiguy:exitafterunittest

Conversation

@schveiguy
Copy link
Member

Currently, if unit tests run and all pass, the program is started. Typically users expect the main function to exit immediately, so they instrument their main program with

version(unittest) { void main() {} }
else int main(string[] args)
{
   ...
}

But this sucks. D shouldn't be running the main program after unit testing (by default). This can be an opt-in for the user, but should default to exiting.

This modification changes the unit test handler to return int instead of bool to signify different handling of the unit test result. If the handler function returns 0, then the main function is run. If the handler returns nonzero, the main function is not run. If the result is int.min, then the program exits with a failure exit code. If the result is negative, the program prints the negated value as the number of modules that failed, and exits with a failure exit code. If the result is positive, that number is printed as the number of modules that were tested, and exits with a success exit code.

the API for setting the handler routine should be backwards compatible, as I allow setting of the handler routine as a bool return or an int return. The one place where it may be incorrect is if you fetch the handler routine and you set a bool version. This code casts it to an int-returning routine. The effect may be very negligible, as I believe both would return using a register. I did the cast mainly to allow code that checks to see which exact routine is installed will work. I'm expecting there's almost no usage of the getter property there anyway.

Possible improvements -- we may want to only enable this behavior with a -DRT switch. Or we may want to allow the previous behavior with such a switch. We may want to separate the bool/int handlers into a different API so the getter makes more sense if complaints arise.

I'm adding this for testing and also to allow for discussion.

@schveiguy
Copy link
Member Author

ping @wilzbach circleci seems very sensitive to line number changes. Can we fix this? I don't like the idea of having the build break because you added some lines of code!

@jacob-carlborg
Copy link
Contributor

Not sure I fully follow when the different int values will be used, but can't you use an enum?

@schveiguy
Copy link
Member Author

Not sure I fully follow when the different int values will be used

So let's say 5 modules are tested. In the default routine, if all 5 unit test routines pass, then the function returns 5. Then the runtime sees that some unit tests were run, prints "5 Module unit tests passed", and exits.

If 2 of the module unit tests fail, stack traces are printed, then the routine returns -2. The runtime then prints "2 Module unit tests failed", and exits.

I could change to enum, but then I'll just print "All unit tests passed". The extra info of how many modules passed may be trivia, but it may also be useful. I'm open to being convinced 😉

@schveiguy
Copy link
Member Author

Fixed issue with trace expectation. This will probably fix circleci tests too.

@jacob-carlborg
Copy link
Contributor

I see. Another thing, how does this work with custom unit test runners? For example, a unit test runner that knows how many actual unit tests were run instead of the number of tested modules. That unit test runner would most likely not want print the number of modules tested.

@schveiguy
Copy link
Member Author

schveiguy commented Oct 27, 2016

The custom runner can return 0 or int.min, and nothing will be printed. However, you may want the runtime not to run main, but still return 0 without printing anything. That case isn't handled. Perhaps int.max should do that.

@schveiguy
Copy link
Member Author

Perhaps int.max should do that

Now does that. Code looks more even also.

@schveiguy
Copy link
Member Author

BTW, part of the WIP, I need to add testing for these cases.

@jacob-carlborg
Copy link
Contributor

The int.max addition should be documented as well.

@schveiguy
Copy link
Member Author

@jacob-carlborg updated docs.

* value of this routine indicates to the runtime whether the tests ran
* without error.
*
* There are two options for handlers. The `bool` version is deprecated but
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this say "schedule for deprecation" unless deprecated is added to the deprecated overload?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is going to be permanently left in place, in my opinion. It doesn't cause much grief, the translation to the new version is backwards compatible, and so we don't have to do anything more.

We may want to simply undocument it at some point.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, fair enough.

@deadalnix
Copy link
Contributor

I'm not convinced this is the right approach. The thing will still fail to link when no main function is provided.

IMO, it is better to do this as proposed by basil. I plan to do a DMD PR.

@schveiguy
Copy link
Member Author

The thing will still fail to link when no main function is provided.

dmd -main should work just fine.

@wilzbach
Copy link
Contributor

ping @wilzbach circleci seems very sensitive to line number changes. Can we fix this? I don't like the idea of having the build break because you added some lines of code!

This is the intended behavior. There is no "forced percentage" on the code coverage results, it is a mere help for the reviewers that they maybe should have a closer look at the untested lines.
Of course it can be ignored.

Btw in case someone missed the info, there's a CodeCov browser extension for user convenience. For example in the diff view it will highlight all the missed lines in red ;-)

@schveiguy
Copy link
Member Author

@wilzbach the issue actually wasn't circleci, it was the build itself (see my later comment). I didn't realize this right away, so false alarm!

*
* The default unit tester does not return `int.min` or `int.max`, but
* rather the number of tests passed or failed (or 0 if none are run).
*
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, so for the new handler:

  • 0 means all good run main
  • int.min failure with no extra information
  • int.max all good don't run main
  • 0 these many unittests passed, none failed, don't run main

  • <0 the negation of these many unittests failed, print and exit(1)

That's subtle but nothing we can't handle. How about an enum as was suggested already:

How about this:

enum UnittestResult { failStop = int.min, successStop, successContinue = 0 }

Then everything outside these values would obey the <0 / >0 rules. For nice symmetry successStop could be int.max.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will still make the return be int, but also define an enum that can be returned for convenience.

static @property ModuleUnitTester moduleUnitTester()
{
if(sm_moduleUnitTester == null)
return cast(ModuleUnitTester)sm_boolModuleUnitTester;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of the cast use a small lambda?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So I'm a bit concerned about doing it that way. I don't know what code exists out there, but there is the possibility that code is checking to see if their particular unit tester is set. If I wrap in a lambda, that will always be false.

Now, code that checks against the function address of wrong type is going to fail to compile. But there will be no way to rectify the situation. At least with the way I have implemented, you can cast to the correct type to figure out if you've set the function.

Alternatively, I can just expose the "new" style unit tester via a new property (and make them mutually exclusive).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also note, that there isn't any danger of improperly calling the function, since it takes no parameters. Just the return type is different, and won't crash the code that uses the wrong function type.

catch( Throwable e )
{
_d_print_throwable(e);
--failed;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cool :) prolly clearer if the name were negativeFailed

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will fix the code to increment instead (and return -failed at the end).

src/rt/dmain2.d Outdated
else if (utResult > 0)
{
if(utResult != int.max)
.fprintf(.stderr, "%d Module unit tests passed\n", utResult);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use the actual "unittest" keyword:

.fprintf(.stderr, "%d unittests passed\n", utResult);

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok

src/rt/dmain2.d Outdated
else
{
if (utResult != int.min)
.fprintf(.stderr, "%d Module Unit tests failed\n", -utResult);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

similar here, maybe FAILED in uppercase

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok

@schveiguy
Copy link
Member Author

I will finish this up with some testing soon!

@andralex
Copy link
Member

So this would be complemented by a separate flag (e.g. '-unittest-only') in the compiler.

@CyberShadow
Copy link
Member

So this would be complemented by a separate flag (e.g. '-unittest-only') in the compiler.

Would it be possible to make it so that DMD's -main and rdmd's --main, when combined with -unittest, both continue to do the right thing after the change?

@andralex
Copy link
Member

@CyberShadow I was thinking one uses either -unittest or -unittest-only` but not both.

@schveiguy schveiguy changed the title [WIP] Modify unittest handling Modify unittest handling Dec 27, 2016
@schveiguy
Copy link
Member Author

Made suggested edits.
Added test harness for custom unittest handler
Added support for --DRT-unittestmode option:

  • runmain: run main even on passing unittests (currently default, change in 2 versions).
  • unittestonly: if any unittests present, do not run main. (will be default on 2.074)

@schveiguy
Copy link
Member Author

Note, automated coverage is not going to work here, because of the nature of how we have to test these things. The added tests should cover all the new code however.

else switch (rt_configOption("unittestmode"))
{
case "":
// By default, run main. Switch to only doing unit tests in 2.074
Copy link
Contributor

@jacob-carlborg jacob-carlborg Dec 27, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we please call this run-main.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Assuming you are backing out of that request?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. I accidentally commented on the wrong line. I left the comment because I hate when I get a notification and then I get find it on GitHub because it was removed.

return Runtime.sm_moduleUnitTester();
import rt.config : rt_configOption;
if (failed != 0)
return -failed;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we please call this unit-test-mode or unittest-mode.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm game for anything in terms of naming, I agree it's long but I couldn't think of something better. My original does follow precedent of all lowercase no-space or hyphen.

CC @MartinNowak thoughts?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think the name is too long, I just like to have a hyphen (or something else) to separate the words. I see three existing usages of rt_configOption, two with lowercase no-space or hyphen gcopt and oncycle and one with camel case no-space or hyphen callStructDtorsDuringGC.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm... if there is precedent, I'd prefer the camel case. I was going off this comment when I was doing the cycle detection parameters.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I cannot recall ever seeing a program using camel case for the command line flags, most use a hyphen. I don't feel strongly about it, I just thought we should do what most other programs are doing.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with @jacob-carlborg here. Command line flags usually use a hyphen.

src/rt/dmain2.d Outdated
}
else if (utResult > 0)
{
if(utResult != int.max)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Space after if
  • How about using the enum instead of int.max/min

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Space after if

ok

How about using the enum

The enum is in the public interface, not imported here. I could re-create the enum, but I don't think it's worth doing that. I will put a comment to clarify that it is attached to the enum.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The enum is in the public interface, not imported here. I could re-create the enum, but I don't think it's worth doing that. I will put a comment to clarify that it is attached to the enum.

Works for me. I didn't noticed they were in separate modules.

@schveiguy
Copy link
Member Author

Fixed formatting nit, and added comments to clarify enum usage. Also squashed into 2 commits.

/**
* Should we print a summary of the results. Ignored if 0 tests executed.
*/
bool summarize;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure what the summary actually contains but many frameworks print the number of tests run even if 0 tests were run.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, so D's determination if we are in "unittest mode" is whether there exists any unit tests at all. So if there are no unit tests run, the runtime thinks that you are not testing.

But thinking about this some more, we can easily add an option for "don't ever run main, even without unit tests" as well, as we are already processing a runtime switch. I'll add it, and let you know.

BTW, the summary is done in the dmain2 file, I added it in this PR, look for the printf statements

* the original behavior of the unit testing system.
*
* If no unittest custom handlers are registered, the following algorithm is
* executed (the behavior can be affected by the `--DRT-unittestmode` switch
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure what the naming conventions are when it comes to --DRT flags but I think it should be called --DRT-unit-test-mode since that's what most Posix commands use.

*
* If the --DRT-unittestmode is set to "runmain", then even if unit tests are
* run (and all pass), main is still run. This is currently the default. If
* --DRT-unittestmode is set to "unittestonly", then any unit tests present
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here, I think unittestonly should be unit-test-only.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

well unittest is already considered a special word so prolly unittest-only etc

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, will do.

@dlang-bot
Copy link
Contributor

Thanks for your pull request, @schveiguy!

Bugzilla references

Your PR doesn't reference any Bugzilla issue.

If your PR contains non-trivial changes, please reference a Bugzilla issue or create a manual changelog.

@schveiguy
Copy link
Member Author

I added a third option for the DRT parameter. I changed the parameter to just say "test" and not "unittest", since it was very verbose otherwise. Let me know what you think. I still need to make a changelog entry.

* The current module unit tester handler or null if none has been
* set.
*/

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please remove empty newline between Ddoc comment and method declaration. It's not consistent with the already existing declarations.

* }
* ---------
*/
static @property void extModuleUnitTester( ExtendedModuleUnitTester h )
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would prefer that the name, ext, was not shortened. Most likely you only going to call this method once, there's no reason to make it less clear to save a few letters.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wished to call it moduleUnitTester, but since there is an accessor, I'm reluctant to do that (the previous version of this PR did that, but the function pointer was returning a value compatible with bool).

I can change this.

* the original behavior of the unit testing system.
*
* If no unittest custom handlers are registered, the following algorithm is
* executed (the behavior can be affected by the `--DRT-testmode` switch
Copy link
Contributor

@jacob-carlborg jacob-carlborg Dec 1, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would have hoped for --DRT-testmode to be renamed to --DRT-test-mode as well.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So far, the DRT options are gcopt, oncycle, and it looks like some other newer camelCase ones: callStructDtorsDuringGC, scanDataSeg

So I don't know which precedent to follow. ping @MartinNowak @andralex

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll note none of the precedents have dashes.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I try to follow what I see in most command line applications. In these case I like to look at how Git is doing things. Git contains quite a lot of subcommands and flags. There's also this, which I think is from the Posix standard.

"The arguments that consist of characters and single letters or digits" [1].

[1] http://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap12.html

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't recall that I've event seen command line flags that use camel casing.

}
else switch (rt_configOption("testmode"))
{
case "":
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't everything in this block be indented one level?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm... I generally don't indent switch case labels. I'm not sure what the style guide says. I see switch statements elsewhere that don't have case labels indented: https://github.com/dlang/druntime/blob/master/src/core/time.d#L352

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I was surprised now to see how many place in Phobos that don't indent cases in switch statements. I always indent the content of a block.

src/rt/dmain2.d Outdated
{
auto utResult = runModuleUnitTests();
assert(utResult.passed <= utResult.executed);
if(utResult.passed == utResult.executed)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Space after if before the opening parenthesis .

src/rt/dmain2.d Outdated
else
{
if (utResult.summarize)
.fprintf(.stderr, "%d/%d unittests FAILED\n", cast(int)(utResult.executed - utResult.passed), cast(int)utResult.executed);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Most test frameworks will print the same summary regardless if something failed or not. That is, they usually print the total number of tests, the number of passed and the number of failed. I'm not sure how we would like it to behave.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If nothing failed, then the number of tests run and passed is printed (a few lines above this).

This behavior is totally up for grabs if someone wants to make the effort to repaint the bikeshed. My goal is simply to stop the runtime from always executing main after running unit tests.

@jacob-carlborg
Copy link
Contributor

I added a third option for the DRT parameter.

👍

I changed the parameter to just say "test" and not "unittest", since it was very verbose otherwise.

I like that, then we don't have to argue if it should be unit-test or unittest 😃.

@jacob-carlborg
Copy link
Contributor

Please squash all commits when done.

* true if execution should continue after testing is complete, false if
* not.
*/
bool opCast(T : bool)() const
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't this too cute? A named method should be better.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not to be cute :)

It's to not break code. Existing code that may have their own runtime startup could easily be written like:

if(runModuleUnitTests())
{
   dmain();
}

* if(result.executed != result.passed)
* result.summarize = true; // print failure
* else
* result.runMain = true; // all UT passed
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

result.runMain = result.executed != result.passed;

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm... I didn't set summarize or runMain if they were to be false (I did originally, similarly to what you had, but realized it defaults to false anyway). I can be more explicit.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oops, didn't realize this was the example. The real code is more complex due to the DRT options. I've updated it anyway.

* }
* }
* return failed == 0;
* if(result.executed != result.passed)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

space after if, mmmmkay? :)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could use torture on me, and I still won't ever remember to do this 😆

tests are run (at all) and no failures occurred (with nice message).
@dlang-bot
Copy link
Contributor

Thanks for your pull request, @schveiguy!

Bugzilla references

Your PR doesn't reference any Bugzilla issue.

If your PR contains non-trivial changes, please reference a Bugzilla issue or create a manual changelog.

@dlang-bot
Copy link
Contributor

Thanks for your pull request, @schveiguy!

Bugzilla references

Your PR doesn't reference any Bugzilla issue.

If your PR contains non-trivial changes, please reference a Bugzilla issue or create a manual changelog.

@schveiguy
Copy link
Member Author

OK, I think this is good. Please let me know if I missed any spacing issues!
I added a changelog entry, please review also.

@dlang-bot
Copy link
Contributor

Thanks for your pull request, @schveiguy!

Bugzilla references

Your PR doesn't reference any Bugzilla issue.

If your PR contains non-trivial changes, please reference a Bugzilla issue or create a manual changelog.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants