TestRunner For Windows 32/64 bit

October 30, 2019

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 Repository and Code

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’.

  1. Clone repository git clone https://github.com/gnilk/testrunner.git
  2. Create build directory (mkdir build)
  3. Enter build directory (cd build)
  4. Run cmake .. (note the the two dot’s, you are running inside the build directory)
  5. 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;    
}

Profile picture

Written by Fredrik Kling. I live and work in Switzerland. Follow me Twitter