Testrunner, my unit test framework, has been released for Windows. Supporting both 32 and 64bit binaries. Here is a quick introduction to setup you up.
Introduction
If you don’t know what this is about you can read the introduction, Test Driven Development in C and if you are interested in how it works you can read the Technical Details.
The master branch is generally working but if you want a fixed released I’ve created release v0.4 which supports Windows. There is no binary builds yet.
This version can be built with both Visual Studio 2017 and Visual Studio 2019. It’s been tested with professional and community edition.
Building the software
You can follow the instructions from github - these are the same. NOTE: These instructions are for Windows.
Launch a ‘Developer Command Prompt’ from your Visual Studio installation. Or make sure you have run ‘vcvars.bat’.
- Clone repository
git clone https://github.com/gnilk/testrunner.git
- Create build directory (
mkdir build
) - Enter build directory (
cd build
) - Run
cmake ..
(note the the two dot’s, you are running inside the build directory) - Run
msbuild ALL_BUILD.vcxproj
If you don’t like a command line driven build process it is possible (in Visual Studio 2019) to open the project folder as a CMAKE project and have visual studio handle the build for you.
Details
Launch a ‘Developer Command Prompt’ from your Visual Studio installation.
To build release version:
msbuild ALL_BUILD.vcxproj -p:Configuration=Release
The default will build 64bit with Visual Studio 2019 and 32bit with Visual Studio 2017.
As Windows don’t have a default place to store 3rd party include files you need to copy the ‘testinterface.h’ file somewhere common on your environment. You want to include this file in your unit tests (note: It’s optional).
Once built you can test the build on the example DLL which is being build (placed in the lib folder).
NOTE: To test you need to build your code as a DLL.
Caveats and non-working stuff
The following does not work
- Mixing 32/64 bit binaries
- the 32bit test runner (trun.exe) can NOT execute test functions in a 64bit DLL (this i simply not possible) - the opposite is also true (64bit trun.exe can’t execute 32bit DLL’s)
- Building from cygwin doesn’t work (cmake output path’s are all ‘c:’ based)
- Only Visual Studio is supported
- Numerical exports (i.e. stripped DLL’s) won’t work as nothing would match the testable function pattern
Tips for creating testable code
A few tips and tricks regarding the library.
DLL Shell
Place all test functions (test_module_case) in a DLL. This DLL can either link statically or dynamically against the code to be test. You can keep this in a separate project if you want - as this will not include the test functions in the production binary.
Automatic test execution
If you prefer visual studio (or any other integrated environment) as your environment instead of a command line driven approach you can add the test execution as a post-build step to your project.
Technical details
Basically the testrunner operates in the same manner as on macOS. There is however a small difference in the way the DLL’s are handled. During detection of test functions a DLL is opened like HMODULE lib = LoadLibraryEx(pathName.c_str(), NULL, DONT_RESOLVE_DLL_REFERENCES);
. This step will fail IF the compile target differs (32 - 64 bit) between the runner and the DLL it’s trying to load. Once loaded the testrunner will parse the DLL binary for export functions.
Once the testable functions have been enumerated the DLL is unloaded (FreeLibrary
) and loaded yet again as a normal DLL with LoadLibrary
. This ensures that the DLL-main function will be called only once.
Parsing the DLL
Code for parsing the DLL, from module_win32.cpp
.
bool Module::Open() {
// See: https://stackoverflow.com/questions/1128150/win32-api-to-enumerate-dll-export-functions
HMODULE lib = LoadLibraryEx(pathName.c_str(), NULL, DONT_RESOLVE_DLL_REFERENCES);
if (lib == NULL) {
pLogger->Error("LoadLibraryEx failed, with code: %d\n", GetLastError());
return false;
}
uint64_t pLibStart = (uint64_t)lib;
if (((PIMAGE_DOS_HEADER)lib)->e_magic != IMAGE_DOS_SIGNATURE) {
pLogger->Error("PIMAGE DOS HEADER magic mismatch\n");
return false;
}
assert(((PIMAGE_DOS_HEADER)lib)->e_magic == IMAGE_DOS_SIGNATURE);
PIMAGE_NT_HEADERS header = (PIMAGE_NT_HEADERS)((BYTE *)lib + ((PIMAGE_DOS_HEADER)lib)->e_lfanew);
if (header->Signature != IMAGE_NT_SIGNATURE) {
pLogger->Error("Header signature mistmatch!\n");
return false;
}
assert(header->Signature == IMAGE_NT_SIGNATURE);
switch (header->OptionalHeader.Magic) {
case IMAGE_NT_OPTIONAL_HDR32_MAGIC:
pLogger->Debug("32bit DLL Header found\n");
#ifdef _WIN64
pLogger->Error("32 bit DLL in 64bit context not allowed!\n");
return false;
#endif
break;
case IMAGE_NT_OPTIONAL_HDR64_MAGIC:
pLogger->Debug("64bit DLL Header found\n");
#ifdef _WIN64
#else
pLogger->Error("64 bit DLL in 32bit context not allowed!\n");
return false;
#endif
break;
default:
printf(" - Unknown/Unsupported DLL header type found\n");
return 1;
break;
}
if (header->OptionalHeader.NumberOfRvaAndSizes == 0) {
pLogger->Error("Number of RVA and Sizes is zero!\n");
return false;
}
assert(header->OptionalHeader.NumberOfRvaAndSizes > 0);
PIMAGE_EXPORT_DIRECTORY exports = (PIMAGE_EXPORT_DIRECTORY)((BYTE *)lib + header->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);
assert(exports->AddressOfNames != 0);
BYTE** names = (BYTE**)(pLibStart + exports->AddressOfNames);
pLogger->Debug("Num Exports: %d\n", exports->NumberOfNames);
for (int i = 0; i < exports->NumberOfNames; i++) {
char* ptrName = (char *)((BYTE*)lib + ((DWORD*)names)[i]);
std::string name(ptrName);
if (IsValidTestFunc(name)) {
pLogger->Debug("[OK] '%s' - valid test func\n", ptrName);
this->exports.push_back(name);
}
else {
pLogger->Debug("[NOK] '%s' invalid test func\n", ptrName);
}
}
// Let's free the library and open it properly
FreeLibrary(lib);
handle = LoadLibrary(pathName.c_str());
if (handle == NULL) {
return false;
}
return true;
}