Changes between Version 7 and Version 8 of BestPracticeHandbook


Ignore:
Timestamp:
May 5, 2015, 1:47:10 PM (7 years ago)
Author:
Niall Douglas
Comment:

--

Legend:

Unmodified
Added
Removed
Modified
  • BestPracticeHandbook

    v7 v8  
    224224
    225225
    226 == 4. Consider making it possible to use an XML outputting unit testing framework, even if not enabled by default ==
     226== 4. Strongly consider using free CI per-commit testing, even if you have a private CI ==
     227
     228Despite that [https://travis-ci.org/ Travis] provides free of cost per-commit continuous integration testing for Linux and OS X, and that [http://www.appveyor.com/ Appveyor] provides the same for Microsoft Windows, there were still libraries in those reviewed which made use of neither and furthermore had no per-commit CI testing whatsoever.
     229
     230I'll be blunt: not having per-commit CI testing is '''unacceptable''' in this modern day and age and is an excellent sign of a library author not committed to software quality. Especially when such CI services are free of cost, and it's purely your laziness that you haven't configured them yet.
     231
     232So first things first, if your C++ 11/14 library is not using any form of per-commit testing yet, '''go add Travis and Appveyor right support now'''. Configuration is extremely easy if your project lives on Github, simply login into both services using your github account and enable your project. Next add a suitable .travis.yml and an .appveyor.yml file to the root of your project, and push the commit. Watch Travis and Appveyor build and run your CI suitable unit test suite, and report back on Github (especially if it's a pull request) whether the build and tests passed or failed. From now on when someone issues a pull request fixing a bug, you'll instantly know if that pull request compiles and passes all unit tests on Linux, OS X and Windows, and much more importantly, ''so will the pull request submitter'' and they usually will go fix problems themselves so you the maintainer never need find out a pull request is defective on some build target.
     233
     234Example travis.yml's to clone from:
     235
     236* https://github.com/krzysztof-jusiak/di/blob/cpp14/.travis.yml
     237* https://github.com/BoostGSoC13/boost.afio/blob/master/.travis.yml
     238
     239Example appveyor.yml's to clone from:
     240
     241* https://github.com/krzysztof-jusiak/di/blob/cpp14/.appveyor.yml
     242* https://github.com/BoostGSoC13/boost.afio/blob/master/appveyor.yml
     243
     244Both Travis and Appveyor are excellent for getting an 90% confidence signal that some commit did not break something. For a free service with little configuration effort that's fantastic. However if you want a 98% confidence signal you will need to spend a few months of your life configuring your own [https://jenkins-ci.org/ Jenkins CI installation], most of that time will be learning how not to configure Jenkins as Jenkins is a black, black art indeed - but again great for being free of cost given the return on investment. Once mastered, Jenkins can do almost anything from per-commit testing to soak testing to input fuzz testing to automating a long list of tasks for you (e.g. diffing and synchronising two forks of a repo for you by bisecting commit histories against unit testing), but it '''will''' take many dedicated months to acquire the skills to configure a maintainable and stable Jenkins install.
     245
     246Should you add Travis and Appveyor CI support if you already are using your own private Jenkins CI?
     247
     248I think the answer is uncategorically yes. The reasons are these:
     249
     2501. Having Travis + Appveyor badges (see https://raw.githubusercontent.com/krzysztof-jusiak/di/cpp14/README.md for example Markdown for badges) on your open source project is a universally recognised signal of attention to quality.
     2512. Other free tooling such as [https://coveralls.io/ Coveralls.io] have built in integration for github and travis. Hooking Jenkins into Coveralls isn't hard, but it "just works" with Travis instead and that's a similar pattern with most free tooling which consumes CI results.
     2523. Future tooling by Boost which dashboards Boost libraries and/or ranks libraries by a quality score will almost certainly automate on Travis and Appveyor being queryable by their RESTful APIs. In other words, placing your library in Github and adding Travis and Appveyor CI support has the highest chance of working immediately with any future Boost tooling with minimal additional effort by you.
     253
     254
     255== 5. Strongly consider compiling your code with static analysis tools ==
     256
     257== 5. Strongly consider running a pass of your unit tests under valgrind and the runtime sanitisers ==
     258
     259== 5. Consider making it possible to use an XML outputting unit testing framework, even if not enabled by default ==
    227260
    228261A very noticeable trend in the libraries above is that around half use good old C assert() and static_assert() instead of a unit testing framework.
     
    303336* https://github.com/ned14/Boost.APIBind
    304337
    305 == 5. Consider not doing compiler feature detection yourself ==
    306 
    307 TODO
     338
     339== 6. Consider breaking up your testing into per-commit CI testing, 24 hour soak testing, and input fuzz testing ==
     340
     341When a library is small, you can generally get away with running all tests per commit, and as that is easier that is usually what one does.
     342
     343However as a library grows and matures, you should really start thinking about categorising your tests into quick ones suitable for per-commit testing, long ones suitable for 24 hour soak testing, and adding fuzz testing whereby an AST analysis tool will try executing your functions with input deliberately designed to exercise least followed code path combinations ("coverage informed fuzz testing"). I haven't mentioned the distinction between http://stackoverflow.com/questions/4904096/whats-the-difference-between-unit-functional-acceptance-and-integration-test unit testing and functional testing and integration testing] here as I personally think that distinction not useful for libraries mostly developed in a person's free time (due to lack of resources, and the fact we all prefer to develop instead of test, one tends to fold unit and functional and integration testing into a single amorphous set of tests which don't strictly delineate as really we should, and instead of proper unit testing one tends to substitute automated fuzz testing, which really isn't the same thing but it does end up covering similar enough ground to make do).
     344
     345There are two main techniques to categorising tests, and each has substantial pros and cons.
     346
     347The first technique is that you tag tests in your test suite with keyword tags, so "ci-quick", "ci-slow", "soak-test" and so on. The unit test framework then lets you select at execution time which set of tags you want. This sounds great, but there are two big drawbacks. The first is that each test framework has its own way of doing tags, and these are invariably not compatible so if you have a switchable Boost.Test/CATCH/Google Test generic test code setup then you'll have a problem with the tagging. One nasty but portable workaround I use is magic test naming and then using a regex test selector string, this is why I have categorised slashes in the test names exampled in the section above so I can select tests by category via their name. The second drawback is that you will find that tests often end up calling some generic implementation with different parameters, and you have to go spell out many sets of parameters in individual test cases when one's gut feeling is that those parameters really should be fuzz variables directly controlled by the test runner. Most test frameworks support passing variables into tests from the command line, but again this varies strongly across test frameworks in a way hard to write generic test code.
     348
     349The second technique is a hack, but a very effective one. One simply parameterises tests with environment variables, and then code calling the unit test program can configure special behaviour by setting environment variables before each test iteration. This technique is especially valuable for converting per-commit tests into soak tests because you simply configure an environment variable which means ITERATIONS to something much larger, and now the same per-commit tests are magically transformed into soak tests. The big drawback here is that just iterating per commit tests a lot more does not a proper soak test suite make, and one can fool oneself into believing your code is highly stable and reliable when it is really only highly stable and reliable at running per commit tests, which obviously it will always be because you run those exact same patterns per commit so those are always the use patterns which will behave the best. Boost.AFIO is 24 hour soak tested on its per-commit tests, and yet I have been more than once surprised at segfaults caused by someone simply doing operations in a different order than the tests did them :(
     350
     351Regarding fuzz testing, there are a number of tools available for C++, though all are not quick and require lots of sustained CPU time to calculate and execute all possible code path variations. One of the most promising going into the long term is LLVM's fuzz testing facilities which are summarised at http://llvm.org/docs/LibFuzzer.html as they make excellent use of the clang sanitisers to find the bad code paths. I haven't played with it yet with Boost.AFIO, though it is very high on my todo list as I have very little unit testing in AFIO (only functional and integration testing), and fuzz testing of my internal routines would be an excellent way of implementing comprehensive exception safety testing which I am also missing (and feel highly unmotivated to implement by hand).
     352
     353
     354== 7. Consider not doing compiler feature detection yourself ==
     355
     356Something extremely noticeable about nearly all the reviewed C++ 11/14 libraries is that they manually do compiler feature detection in their config.hpp, usually via old fashioned compiler version checking. This tendency is not unsurprising as the number of potential C++ compilers your code usually needs to handle has essentially shrunk to three, and the chances are very high that three compiler will be upper bound going into the long term future. This makes compiler version checking a lot more tractable than say fifteen years ago.
     357
     358However, C++ 1z is expected to provide a number of feature detection macros via the work of SG-10, and GCC and clang already partially support these, especially in very recent compiler releases. To fill in the gaps in older editions of GCC and clang, and indeed MSVC at all, you might consider making use of the header file at https://github.com/ned14/Boost.APIBind/blob/master/include/cpp_feature.h which provides the following SG-10 feature detection macros on all versions of GCC, clang and MSVC:
     359
     360 __cpp_exceptions:: Whether C++ exceptions are available
     361 __cpp_rtti:: Whether C++ RTTI is available
     362
     363 __cpp_alias_templates::
     364 __cpp_alignas::
     365 __cpp_decltype::
     366 __cpp_default_function_template_args::
     367 __cpp_defaulted_functions::
     368 __cpp_delegated_constructors::
     369 __cpp_deleted_functions::
     370 __cpp_explicit_conversions::
     371 __cpp_generalized_initializers::
     372 __cpp_implicit_moves::
     373 __cpp_inheriting_constructors::
     374 __cpp_inline_namespaces::
     375 __cpp_lambdas::
     376 __cpp_local_type_template_args::
     377 __cpp_noexcept::
     378 __cpp_nonstatic_member_init::
     379 __cpp_nullptr::
     380 __cpp_override_control::
     381 __cpp_reference_qualified_functions::
     382 __cpp_range_for::
     383 __cpp_raw_strings::
     384 __cpp_rvalue_references::
     385 __cpp_static_assert::
     386 __cpp_thread_local::
     387 __cpp_auto_type::
     388 __cpp_strong_enums::
     389 __cpp_trailing_return::
     390 __cpp_unicode_literals::
     391 __cpp_unrestricted_unions::
     392 __cpp_user_defined_literals::
     393 __cpp_variadic_templates::
     394
     395 __cpp_contextual_conversions::
     396 __cpp_decltype_auto::
     397 __cpp_aggregate_nsdmi::
     398 __cpp_digit_separators::
     399 __cpp_init_captures::
     400 __cpp_generic_lambdas::
     401 __cpp_relaxed_constexpr::
     402 __cpp_return_type_deduction::
     403 __cpp_runtime_arrays::
     404 __cpp_variable_templates::
     405
     406The advantage of using these SG-10 macros in C++ 11/14 code is threefold:
     407
     4081. It should be future proof.
     4092. It's a lot nicer than testing compiler versions.
     4103. It expands better if a fourth C++ compiler suddenly turned up.
     411
     412Why use the https://github.com/ned14/Boost.APIBind/blob/master/include/cpp_feature.h header file instead of doing it by hand?
     413
     4141. Complete compiler support for GCC, clang and MSVC all versions.
     4152. Updates in compiler support will get reflected into cpp_feature.h for you.
     4163. You benefit from any extra compilers added automatically.
     4174. If you're using Boost.APIBind you automatically get cpp_feature.h included for you as soon as you include any APIBind header file.
     418
     419Incidentally Boost.APIBind wraps these macros into Boost.Config compatible macros in https://github.com/ned14/Boost.APIBind/blob/master/include/boost/config.hpp which would be included, as with Boost, using "boost/config.hpp".