Writing Conformance tests Written by &name-francois-gouget; &email-francois-gouget; Note: This part of the documentation is still very much a work in progress and is in no way complete. Introduction With more The Windows API follows no standard, it is itself a defacto standard, and deviations from that standard, even small ones, often cause applications to crash or misbehave in some way. Furthermore a conformance test suite is the most accurate (if not necessarily the most complete) form of API documentation and can be used to supplement the Windows API documentation. Writing a conformance test suite for more than 10000 APIs is no small undertaking. Fortunately it can prove very useful to the development of Wine way before it is complete. The conformance test suite must run on Windows. This is necessary to provide a reasonable way to verify its accuracy. Furthermore the tests must pass successfully on all Windows platforms (tests not relevant to a given platform should be skipped). A consequence of this is that the test suite will provide a great way to detect variations in the API between different Windows versions. For instance, this can provide insights into the differences between the, often undocumented, Win9x and NT Windows families. However, one must remember that the goal of Wine is to run Windows applications on Linux, not to be a clone of any specific Windows version. So such variations must only be tested for when relevant to that goal. Writing conformance tests is also an easy way to discover bugs in Wine. Of course, before fixing the bugs discovered in this way, one must first make sure that the new tests do pass successfully on at least one Windows 9x and one Windows NT version. Bugs discovered this way should also be easier to fix. Unlike some mysterious application crashes, when a conformance test fails, the expected behavior and APIs tested for are known thus greatly simplifying the diagnosis. To detect regressions. Simply running the test suite regularly in Wine turns it into a great tool to detect regressions. When a test fails, one immediately knows what was the expected behavior and which APIs are involved. Thus regressions caught this way should be detected earlier, because it is easy to run all tests on a regular basis, and easier to fix because of the reduced diagnosis work. Tests written in advance of the Wine development (possibly even by non Wine developpers) can also simplify the work of the futur implementer by making it easier for him to check the correctness of his code. Conformance tests will also come in handy when testing Wine on new (or not as widely used) architectures such as FreeBSD, Solaris x86 or even non-x86 systems. Even when the port does not involve any significant change in the thread management, exception handling or other low-level aspects of Wine, new architectures can expose subtle bugs that can be hard to diagnose when debugging regular (complex) applications. What to test for? The first thing to test for is the documented behavior of APIs and such as CreateFile. For instance one can create a file using a long pathname, check that the behavior is correct when the file already exists, try to open the file using the corresponding short pathname, convert the filename to Unicode and try to open it using CreateFileW, and all other things which are documented and that applications rely on. While the testing framework is not specifically geared towards this type of tests, it is also possible to test the behavior of Windows messages. To do so, create a window, preferably a hidden one so that it does not steal the focus when running the tests, and send messages to that window or to controls in that window. Then, in the message procedure, check that you receive the expected messages and with the correct parameters. For instance you could create an edit control and use WM_SETTEXT to set its contents, possibly check length restrictions, and verify the results using WM_GETTEXT. Similarly one could create a listbox and check the effect of LB_DELETESTRING on the list's number of items, selected items list, highlighted item, etc. However, undocumented behavior should not be tested for unless there is an application that relies on this behavior, and in that case the test should mention that application, or unless one can strongly expect applications to rely on this behavior, typically APIs that return the required buffer size when the buffer pointer is NULL. Running the tests in Wine The simplest way to run the tests in Wine is to type 'make test' in the Wine sources top level directory. This will run all the Wine conformance tests. The tests for a specific Wine library are located in a 'tests' directory in that library's directory. Each test is contained in a file (e.g. dlls/kernel/tests/thread.c). Each file itself contains many checks concerning one or more related APIs. So to run all the tests related to a given Wine library, go to the corresponding 'tests' directory and type 'make test'. This will compile the tests, run them, and create an 'xxx.ok' file for each test that passes successfully. And if you only want to run the tests contained in the thread.c file of the kernel library, you would do: $ cd dlls/kernel/tests $ make thread.ok Note that if the test has already been run and is up to date (i.e. if neither the kernel library nor the thread.c file has changed since the thread.ok file was created), then make will say so. To force the test to be re-run, delete the thread.ok file, and run the make command again. You can also run tests manually using a command similar to the following: $ ../../../tools/runtest -q -M kernel32.dll -p kernel32_test.exe.so thread.c $ ../../../tools/runtest -p kernel32_test.exe.so thread.c thread.c: 86 tests executed, 5 marked as todo, 0 failures. The '-P wine' options defines the platform that is currently being tested. Remove the '-q' option if you want the testing framework to report statistics about the number of successful and failed tests. Run runtest -h for more details. Building and running the tests on Windows Using pre-compiled binaries Unfortunately there are no pre-compiled binaries yet. However if send an email to the Wine development list you can probably get someone to send them to you, and maybe motivate some kind soul to put in place a mechanism for publishing such binaries on a regular basis. With Visual C++ get the Wine sources Run msvcmaker to generate Visual C++ project files for the tests. 'msvcmaker' is a perl script so you may be able to run it on Windows. $ ./tools/winapi/msvcmaker --no-wine If the previous steps were done on your Linux development machine, make the Wine sources accessible to the Windows machine on which you are going to compile them. Typically you would do this using Samba but copying them altogether would work too. On the Windows machine, open the winetest.dsw workspace. This will load each test's project. For each test there are two configurations: one compiles the test with the Wine headers, and the other uses the Visual C++ headers. Some tests will compile fine with the former, but most will require the latter. Open the Build Batch build... menu and select the tests and build configurations you want to build. Then click on Build. To run a specific test from Visual C++, go to Project Settings.... There select that test's project and build configuration and go to the Debug tab. There type the name of the specific test to run (e.g. 'thread') in the Program arguments field. Validate your change by clicking on Ok and start the test by clicking the red exclamation mark (or hitting 'F5' or any other usual method). You can also run the tests from the command line. You will find them in either Output\Win32_Wine_Headers or Output\Win32_MSVC_Headers depending on the build method. So to run the kernel 'path' tests you would do: C:\>cd dlls\kernel\tests\Output\Win32_MSVC_Headers C:\dlls\kernel\tests\Output\Win32_MSVC_Headers>kernel32_test thread With MinGW This needs to be documented. The best may be to ask on the Wine development mailing list and update this documentation with the result of your inquiry. Cross compiling with MinGW on Linux Here is how to generate Windows executables for the tests straight from the comfort of Linux. First you need to get the MinGW cross-compiler. On Debian all you need to do is type apt-get install mingw32. If you had already run configure, then delete config.cache and re-run configure. You can then run make crosstest. To sum up: $ rm config.cache $ ./configure $ make crosstest If you get an error when compiling winsock.h then you probably need to apply the following patch: http://www.winehq.com/hypermail/wine-patches/2002/12/0157.html Inside a test When writing new checks you can either modify an existing test file or add a new one. If your tests are related to the tests performed by an existing file, then add them to that file. Otherwise create a new .c file in the tests directory and add that file to the CTESTS variable in Makefile.in. A new test file will look something like the following: #include <wine/test.h> #include <winbase.h> /* Maybe auxiliary functions and definitions here */ START_TEST(paths) { /* Write your checks there or put them in functions you will call from * there */ } The test's entry point is the START_TEST section. This is where execution will start. You can put all your tests in that section but it may be better to split related checks in functions you will call from the START_TEST section. The parameter to START_TEST must match the name of the C file. So in the above example the C file would be called paths.c. Tests should start by including the wine/test.h header. This header will provide you access to all the testing framework functions. You can then include the windows header you need, but make sure to not include any Unix or Wine specific header: tests must compile on Windows. You can use trace to print informational messages. Note that these messages will only be printed if 'runtest -v' is being used. trace("testing GlobalAddAtomA"); trace("foo=%d",foo); Then just call functions and use ok to make sure that they behaved as expected: ATOM atom = GlobalAddAtomA( "foobar" ); ok( GlobalFindAtomA( "foobar" ) == atom, "could not find atom foobar" ); ok( GlobalFindAtomA( "FOOBAR" ) == atom, "could not find atom FOOBAR" ); The first parameter of ok is an expression which must evaluate to true if the test was successful. The next parameter is a printf-compatible format string which is displayed in case the test failed, and the following optional parameters depend on the format string. Writing good error messages The message that is printed when a test fails is extremely important. Someone will take your test, run it on a Windows platform that you don't have access to, and discover that it fails. They will then post an email with the output of the test, and in particular your error message. Someone, maybe you, will then have to figure out from this error message why the test failed. If the error message contains all the relevant information that will be easy. If not, then it will require modifying the test, finding someone to compile it on Windows, sending the modified version to the original tester and waiting for his reply. In other words, it will be long and painful. So how do you write a good error message? Let's start with an example of a bad error message: ok(GetThreadPriorityBoost(curthread,&disabled)!=0, "GetThreadPriorityBoost Failed"); This will yield: thread.c:123: Test failed: GetThreadPriorityBoost Failed Did you notice how the error message provides no information about why the test failed? We already know from the line number exactly which test failed. In fact the error message gives strictly no information that cannot already be obtained by reading the code. In other words it provides no more information than an empty string! Let's look at how to rewrite it: BOOL rc; ... rc=GetThreadPriorityBoost(curthread,&disabled); ok(rc!=0 && disabled==0,"rc=%d error=%ld disabled=%d", rc,GetLastError(),disabled); This will yield: thread.c:123: Test failed: rc=0 error=120 disabled=0 When receiving such a message, one would check the source, see that it's a call to GetThreadPriorityBoost, that the test failed not because the API returned the wrong value, but because it returned an error code. Furthermore we see that GetLastError() returned 120 which winerror.h defines as ERROR_CALL_NOT_IMPLEMENTED. So the source of the problem is obvious: this Windows platform (here Windows 98) does not support this API and thus the test must be modified to detect such a condition and skip the test. So a good error message should provide all the information which cannot be obtained by reading the source, typically the function return value, error codes, and any function output parameter. Even if more information is needed to fully understand a problem, systematically providing the above is easy and will help cut down the number of iterations required to get to a resolution. It may also be a good idea to dump items that may be hard to retrieve from the source, like the expected value in a test if it is the result of an earlier computation, or comes from a large array of test values (e.g. index 112 of _pTestStrA in vartest.c). In that respect, for some tests you may want to define a macro such as the following: #define eq(received, expected, label, type) \ ok((received) == (expected), "%s: got " type " instead of " type, (label),(received),(expected)) ... eq( b, curr_val, "SPI_{GET,SET}BEEP", "%d" ); Handling platform issues Some checks may be written before they pass successfully in Wine. Without some mechanism, such checks would potentially generate hundred of known failures for months each time the tests are being run. This would make it hard to detect new failures caused by a regression. or to detect that a patch fixed a long standing issue. Thus the Wine testing framework has the concept of platforms and groups of checks can be declared as expected to fail on some of them. In the most common case, one would declare a group of tests as expected to fail in Wine. To do so, use the following construct: todo_wine { SetLastError( 0xdeadbeef ); ok( GlobalAddAtomA(0) == 0 && GetLastError() == 0xdeadbeef, "failed to add atom 0" ); } On Windows the above check would be performed normally, but on Wine it would be expected to fail, and not cause the failure of the whole test. However. If that check were to succeed in Wine, it would cause the test to fail, thus making it easy to detect when something has changed that fixes a bug. Also note that todo checks are accounted separately from regular checks so that the testing statistics remain meaningful. Finally, note that todo sections can be nested so that if a test only fails on the cygwin and reactos platforms, one would write: todo("cygwin") { todo("reactos") { ... } } But specific platforms should not be nested inside a todo_wine section since that would be redundant. When writing tests you will also encounter differences between Windows 9x and Windows NT platforms. Such differences should be treated differently from the platform issues mentioned above. In particular you should remember that the goal of Wine is not to be a clone of any specific Windows version but to run Windows applications on Unix. So, if an API returns a different error code on Windows 9x and Windows NT, your check should just verify that Wine returns one or the other: ok ( GetLastError() == WIN9X_ERROR || GetLastError() == NT_ERROR, ...); If an API is only present on some Windows platforms, then use LoadLibrary and GetProcAddress to check if it is implemented and invoke it. Remember, tests must run on all Windows platforms. Similarly, conformance tests should nor try to correlate the Windows version returned by GetVersion with whether given APIs are implemented or not. Again, the goal of Wine is to run Windows applications (which do not do such checks), and not be a clone of a specific Windows version. FIXME: What about checks that cause the process to crash due to a bug?