1 Compile-time Unit Testing ÁRON BARÁTH and ZOLTÁN PORKOLÁB, Eötvös Loránd University Unit testing is an essential part in high quality software development. The prerequisite of the system-wide test is that all components are working correctly. Such components are checked by unit tests which are focused on small part of code. Unit tests are isolated from other code fragments and are frequently programmed by using third party tools. In this paper we show how to minimalize the number of the required third party tools, and how to automate unit tests. We aimed to express unit tests as part of the source code, and execute them during the compilation to ensure the quality of the code. Two different approaches are presented in this paper: the first is based on the C++11’s new mechanisms, such as constant expressions, and static assertions; the second is based on our experimental programming language, Welltype, which offers more flexible unit testing than C++11 – however, both technique are similar. The key in both approaches to get the compiler to execute additional, user defined calculations which can result errors during the compilation. In C++11 the static assertions are used to evaluate parts of the unit tests. Since constant expressions are restricted, in this paper we also present a method how to overcome those restrictions, and how to utilize static assertion with constant expressions. Finally, we describe how our experimental language offers compiler support to evaluate pure expressions at compile-time. Categories and Subject Descriptors: D.3.3 [Programming Languages] Language Constructs and Features; D.2.4 [Software Engineering] Software/Program Verification Additional Key Words and Phrases: Programming languages, C++, C++11, Compile-time, Unit testing 1. INTRODUCTION Modern software development have to take testing into account to ensure the reliability of the software product. Test-driven development [Beck 2003] requires to specificate the new features first by writing new test cases, and after implement it to fulfill the test cases. This method provides clear specification for the new features. Moreover, any reported bugs become test cases, and the way of fixing it is the same. Finally, all written test cases are part of the regression test. Two major kind of tests are known: black-box and white-box testing [Schach 2002]. The black-box tests are focused on the input and the output: for specific input, the specific output must be provided, and no matters how. We could say black-box tests are testing the preconditions and the postcondi- tions [Plosch and Pichler 1999]. The white-box tests are dedicated to get as high code coverage as possible by providing different inputs to execute distinct parts of the code. Keeping the white-box tests cases up-to-date requires huge effort when the implementation changes, during a refactor for example. The unit tests can be handled different ways. The tests can be written by hand as any regular program, and refer to the libraries which are tested. This technique is too unflexible, because numerous additional code is required to make detailed error messages. The more general way is to use an existing test framework, because of the detailed error messages, large number of tools which are helpful when Authors’ address: Á. Baráth and Z. Porkoláb, Department of Programming Languages and Compilers, Faculty of Informatics, Eötvös Loránd University, Pázmány Péter sétány 1/C, H-1117 Budapest, Hungary; email: {baratharon, gsd}@caesar.elte.hu Copyright c by the paper’s authors. Copying permitted only for private and academic purposes. In: Z. Budimac, M. Heričko (eds.): Proceedings of the 4th Workshop of Software Quality, Analysis, Monitoring, Improvement, and Applications (SQAMIA 2015), Maribor, Slovenia, 8.-10.6.2015. Also published online by CEUR Workshop Proceedings (CEUR- WS.org, ISSN 1613-0073) 1:2 • Á. Baráth, Z. Porkoláb develop tests. Test frameworks are available for all wide-spread programming language, for example gtest [Google 2015] for C++ and JUnit [Tahchiev et al. 2010] for Java. However, the test frameworks are usually third party extensions for a language, sometimes came with a platform-specific library. Furthermore, the created tests are regular programs, which must be executed manually – or they are part of the build system. When the execution of the tests are neglected, the quality of the software questionable. Therefore, we need compiler support or at least some compile-time mechanism to prevent the compilation of the wrong code at all. For example, the Eiffel programming language introduced the Design by Contract [Meyer 1992]. In this paper we show a new way to write tests cases in C++11, which are checked by the compiler. The method uses features are introduced in C++11: the constant expressions, which are expressions containing only constant expressions or literals, and the compiler is entitled to evaluate them during the compilation; and the static assertions, which are assertions evaluated during the compilation. However, these features are very restricted, and requires preparations in the source code to apply. We present the usage of these mechanisms by an example. Moreover, we present a similar technique in our experimental programming language: our ongoing development in our compiler is to evaluate pure expressions during the compilation. With this ability, our compiler will support a rich mechanism for compile-time unit testing. This paper is organized as follows: In Section 2 we give a detailed guide how to write compile-time unit tests in C++11. The guide is presented with a concrete example. Also, we highlight the limitations of the method. In Section 3 we present our experimental programming language called Welltype, and its capability of compile-time unit testing. In Section 4 we describe our future plans to implement additional functionality for the compile-time unit testing. Our paper concludes in Section 5. 2. IMPLEMENTATION IN C++11 C++ is a multi-paradigm programming language designed for high performance programming [Strous- trup 1994; 2013]. In the continuous evolution of the language first object-oriented features have been introduced, later essential elements of generative programming, like templates and the STL library were invented. In recent versions in C++14, functional programming achieved more and more im- portance [Meyers 2014]. In the same time, C++ compile-time programming proved to be a Turing- complete sublanguage [Veldhuizen 2003], and template metaprogram applications became popular [Alexandrescu 2001]. By design no language support exists in C++ to construct test cases. However, developers can use third-party tools and libraries to write tests. One of these tools is the Google Test, also known as gtest [Google 2015; Langr 2013; Sigerud et al. 2013], and the Boost Test Library [Rosental 2007]. All third-party tools carry the same problems: —it is not ensured the availability on all architectures and operating systems; —the developer’s responsibility to execute the tests during the development. The key idea is to relocate the functionality from third-party tools into the C++ compiler. This en- deavor can be observed in other C++ specific areas, for example generating domain-specific languages at compile-time [Porkoláb and Sinkovics 2011]; or validating STL usages [Pataki et al. 2010]. The C++11 introduced the the static assert mechanisms, and the programmers can write compile- time assertions. Furthermore, the constexpr is introduced for optimization purposes, since the com- piler is entitled to evaluate all constexpr expressions. Note that, before C++11 the compiler could eval- uate functions at compile time as well, but that is done by heavy optimization routines. The constexpr is more, because the compiler will accept a function as constexpr, when the all called functions are Compile-time Unit Testing • 1:3 constexpr, and the functions itself has no side-effect. This definition excludes the usage of global vari- ables. Consequently, the evaluation of a constexpr function depends only on the arguments. If all ar- guments are constants (literals, or return value of a constexpr function), the function will be evaluated at compile-time. Otherwise, it will be evaulated at run-time. The constexpr functions can be named as pure functions, since the return value depends on the arguments, and the result is the same every time. Furthermore, the C++14 relax the restrictions in the constexpr, so more complicated functions can be written [Smith 2013]. Putting the static assert and the constexpr together, we can write tests which are evaluated at compile-time. The compile-time tests are the aid for all problems which came from the third-party tools, because all actions are performed by the compiler. So, there is no additional dependencies, and the C++ project is more portable. Using compile-time tests results more reliability, because the source code will not compile if one of the tests fail. However, the compile-time tests requires specific preparation in the software project. The declaration of the sample class can be seen in Figure 1. Taking advantage of the behavior of the constexpr, all functions are annotated with the constexpr where it was possible. The constructors are emphasized, because it is mandatory to construct pair at compile-time. The targeted operation is the operator< – as it can be seen in the class declaration, it is a friend and a constexpr function. class pair { int x, y; public: constexpr pair() : x(), y() { } constexpr pair(int x, int y) : x(x), y(y) { } constexpr int get_x() const { return x; } friend constexpr bool operator<(const pair & lhs, const pair & rhs); friend std::ostream & operator<<(std::ostream & os, const pair & rhs); }; Fig. 1. Preparations in the class declaration. The implementation of the two declared operations in the class can be seen in Figure 2. The operator< has the ordinal behavior of a less-than operator. Note that, this function can be evaluated in compile- time and in run-time as well, thus, it can be used in static assert statements. constexpr bool operator<(const pair & lhs, const pair & rhs) { return lhs.xto) || (pair(from, 0)= R*old m &&& old n < (R+1)*old m } assert { div(10, 2)==5 } assert { div(0, 1)==0 } assert { div(16, 6)==2 } assert { div(16, 5)==3 } assert { div(16, 4)==4 } //assert { div(1, 0)==0 } // PreconditionViolationException //assert { div(1, 1)==2 } // AssertionFailedException assert { div(2, 0)==0 ; PreconditionViolationException } { return n/m; } Fig. 6. An example to present the elements of the compile-time unit testing in Welltype. 1:6 • Á. Baráth, Z. Porkoláb The compiler will gather the assert clauses during the compilation, and generates an other exe- cutable program in the memory. This new program will contain all of the assert clauses for every pure functions. When the new program is generated, the compiler will pass the binary to the virtual ma- chine. If the virtual machine successfully executed the program, it means, all assertions are passed. It is important to notice, only the compiler program is used during the compilation and assertion eval- uation. So, the tests will be checked at compile-time. Whenever an assertion fails while running the tests in the virtual machine, the output will be dropped. This is the expected behavior, because the implementation contains one or more errors. The internal program, which checks the assertion clauses, will be built with the following algorithm: (1) gather all pure functions with at least one assert clause, (2) generate a function for each assert clause, which contains a regular assertion with the same con- dition, (3) if the assert clause contains an expected exception, add a single exception handler with the desired exception, (4) call all generated functions in the main block. If the assertion in a generated function fails, an AssertionFailedException will be raised. This excep- tion will passed through the main, and will be caught by the virtual machine. This error can be easily processed in the caller. 3.1 Limitations The solution for the compile-time testing in Welltype uses the pure function mechanism – since the re- sult can be guaranteed only for the pure functions – the same problem raised as in C++. In the current situation this limitation can be removed only at the expense of reliability. However, the reliability is the essence of this method. Another limitation is the usage of external functions and types. Since any concrete implementation can be loaded into the virtual machine, the executed test will be meaningless despite of they neces- sarily be pure functions. Also, the dependencies will be required to compile a program, which is not acceptable. This limitation can be solved, if the pre- and post-conditions could be stored in the binary, and these conditions will be validated by the dynamic program loader, and compiler could generate mock objects for the conditions. Thus the testing mechanism could be more flexible. 4. FUTURE WORK Our current method requires handmade test cases, even in case of the recursive test case in Figure 5. Using template metaprogramming, we can improve the method with automatic test case generation. For example, the input for the operator< can be generated by random numbers, and the developer have to write only a logical formula, such as !(a