This program scans the source files that are inside a Visual Studio project searching for tests functions inside #ifdef TEST.
Then an output file is generated with forward declarations and calling each test.
Usage: Inside Visual Studio External Tools
Command: your exe path
Arguments: $(ProjectFileName) unit_test.c
Initial Directory: $(ProjectDir)
[x] Use output window
Source:
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <stdbool.h>
#include <sys/stat.h>
struct Test
{
char name[100];
struct Test* pNext;
int bFileName;
};
struct TestList
{
struct Test* pHead;
struct Test* pTail;
};
static void Destroy(struct TestList* list)
{
struct Test* pCurrent = list->pHead;
while (pCurrent)
{
struct Test* pNext = pCurrent->pNext;
free(pCurrent);
pCurrent = pNext;
}
}
static void Append(struct TestList* list, struct Test* p)
{
if (list->pTail == NULL)
{
list->pHead = p;
list->pTail = p;
}
else {
list->pTail->pNext = p;
list->pTail = p;
}
}
enum Token
{
IDENTIFER,
OTHER,
PRE,
COMMENT,
STRING,
SPACES,
NUMBER,
};
static const char* GetTokenName(enum Token e)
{
switch (e)
{
case IDENTIFER: return "IDENTIFER";
case OTHER: return " OTHER";
case PRE: return " PRE";
case COMMENT: return " COMMENT";
case STRING: return " STRING";
case SPACES: return " SPACES";
case NUMBER: return " NUMBER";
}
return "??";
}
static enum Token Match(FILE* f, char* dest, int destsize)
{
dest[0] = 0;
if (ferror(f) || feof(f))
return OTHER;
REPEAT:;
enum Token tk = OTHER;
int count = 0;
char ch = fgetc(f);
if ((ch >= 'a' && ch <= 'z') ||
(ch >= 'A' && ch <= 'Z') ||
ch == '_')
{
tk = IDENTIFER;
if (count < destsize - 1)
{
dest[count] = ch;
count++;
}
while (
((ch = fgetc(f)) != EOF) &&
(ch >= 'a' && ch <= 'z') ||
(ch >= 'A' && ch <= 'Z') ||
(ch >= '0' && ch <= '9') ||
ch == '_')
{
if (count < destsize - 1)
{
dest[count] = ch;
count++;
}
}
ungetc(ch, f);
}
else if (ch >= '0' && ch <= '9')
{
tk = NUMBER;
dest[count] = ch;
if (count < destsize - 1)
{
dest[count] = ch;
count++;
}
while (((ch = fgetc(f)) != EOF) &&
(ch >= '0' && ch <= '9'))
{
if (count < destsize - 1)
{
dest[count] = ch;
count++;
}
}
ungetc(ch, f);
}
else if (ch == '\'' || ch == '"')
{
char type = ch;
tk = STRING;
if (count < destsize - 1)
{
dest[count] = ch;
count++;
}
while (((ch = fgetc(f)) != EOF))
{
if (ch == '\\')
{
if (count < destsize - 1)
{
dest[count] = ch;
count++;
}
ch = fgetc(f);
if (count < destsize - 1)
{
dest[count] = ch;
count++;
}
}
else if (ch == type)
{
if (count < destsize - 1)
{
dest[count] = ch;
count++;
}
break;
}
else
{
if (count < destsize - 1)
{
dest[count] = ch;
count++;
}
}
}
}
else if (ch == ' ' || ch == '\n' || ch == '\r')
{
tk = SPACES;
if (count < destsize - 1)
{
dest[count] = ch;
count++;
}
while ((ch = getc(f)) != EOF)
{
if (!(ch == ' ' || ch == '\n' || ch == '\r'))
{
ungetc(ch, f);
break;
}
if (count < destsize - 1)
{
dest[count] = ch;
count++;
}
}
goto REPEAT;
}
else if (ch == '/')
{
tk = OTHER;
if (count < destsize - 1)
{
dest[count] = ch;
count++;
}
ch = fgetc(f);
if (ch == '/')
{
if (count < destsize - 1)
{
dest[count] = ch;
count++;
}
tk = COMMENT;
while (
((ch = fgetc(f)) != EOF) &&
ch != '\r' &&
ch != '\n' &&
ch != '\0')
{
if (count < destsize - 1)
{
dest[count] = ch;
count++;
}
}
}
else if (ch == '*')
{
tk = COMMENT;
if (count < destsize - 1)
{
dest[count] = ch;
count++;
}
ch = fgetc(f);
while (!feof(f))
{
if (ch == '*')
{
if (count < destsize - 1)
{
dest[count] = ch;
count++;
}
ch = fgetc(f);
if (ch == '/')
{
if (count < destsize - 1)
{
dest[count] = ch;
count++;
}
break;
}
}
else
{
if (count < destsize - 1)
{
dest[count] = ch;
count++;
}
}
ch = fgetc(f);
}
goto REPEAT;
}
else
{
if (count < destsize - 1)
{
dest[count] = ch;
count++;
}
ungetc(ch, f);
}
}
else if (ch == '#')
{
tk = PRE;
if (count < destsize - 1)
{
dest[count] = ch;
count++;
}
while (((ch = fgetc(f)) != EOF) && ch != '\r' && ch != '\n')
{
if (count < destsize - 1)
{
dest[count] = ch;
count++;
}
}
if (ch == '\r')
{
ch = fgetc(f); // \n
}
}
else
{
if (count < destsize - 1)
{
dest[count] = ch;
count++;
}
}
dest[count] = '\0';
return tk;
}
static void CollectTests(FILE* f, struct TestList* list)
{
int ifdefcount = 0;
int ActiveSession = 0;
while (!feof(f))
{
char lexeme[100];
enum Token tk = Match(f, lexeme, sizeof lexeme);
if (tk == PRE)
{
if (strcmp(lexeme, "#ifdef TEST") == 0)
{
ifdefcount = 1;
ActiveSession = 1;
}
else if (strncmp(lexeme, "#ifdef", sizeof("#ifdef") - 1) == 0 ||
strncmp(lexeme, "#if", sizeof("#if") - 1) == 0 ||
strncmp(lexeme, "#ifndef", sizeof("#ifndef") - 1) == 0)
{
ifdefcount++;
}
else if (strcmp(lexeme, "#endif") == 0)
{
ifdefcount--;
if (ifdefcount == 0)
{
/*we are leaving the #ifdef TEST*/
ActiveSession = 0;
}
}
}
else if (tk == IDENTIFER && ActiveSession == 1)
{
//template
// void name ( optional void ) {
do
{
if (strcmp(lexeme, "void") != 0) break;
tk = Match(f, lexeme, sizeof lexeme);
if (tk != IDENTIFER) break;
char name[200] = { 0 };
strncpy(name, lexeme, sizeof name);
tk = Match(f, lexeme, sizeof lexeme);
if (strcmp(lexeme, "(") != 0) break;
tk = Match(f, lexeme, sizeof lexeme);
if (strcmp(lexeme, "void") == 0)
{
/*optional*/
tk = Match(f, lexeme, sizeof lexeme);
}
if (strcmp(lexeme, ")") != 0) break;
tk = Match(f, lexeme, sizeof lexeme);
if (strcmp(lexeme, "{") != 0) break;
//Pattern match! void name ( void ) {
struct Test* p = calloc(1, sizeof * p);
if (p)
{
p->bFileName = 0;
strcpy(p->name, name);
printf(" %s\n", p->name);
Append(list, p);
}
}
while (0);
}
}
}
void CollectTestsFile(const char* file, struct TestList* list)
{
FILE* f = fopen(file, "r");
if (f)
{
/*while (!feof(f))
{
char lexeme[100];
enum Token tk = Match(f, lexeme, sizeof lexeme);
printf("%s '%s'\n", GetTokenName(tk), lexeme);
}*/
struct Test* p = calloc(1, sizeof * p);
if (p)
{
p->bFileName = 1;
strcpy(p->name, file);
Append(list, p);
}
printf("%s\n", file);
CollectTests(f, list);
fclose(f);
}
}
void Generate(const char* output, struct TestList* list)
{
FILE* fout = fopen(output, "w");
if (fout)
{
fprintf(fout, "#ifdef TEST\n\n");
fprintf(fout, "//forward declarations\n\n");
struct Test* pCurrent = list->pHead;
while (pCurrent)
{
if (pCurrent->bFileName)
{
if (pCurrent->pNext != NULL && pCurrent->pNext->bFileName == 0)
fprintf(fout, "//%s\n", pCurrent->name);
}
else
fprintf(fout, "void %s(void);\n", pCurrent->name);
pCurrent = pCurrent->pNext;
}
fprintf(fout, "\n");
fprintf(fout, "void DoUnitTests(void)\n{\n");
pCurrent = list->pHead;
while (pCurrent)
{
if (pCurrent->bFileName)
{
if (pCurrent->pNext != NULL && pCurrent->pNext->bFileName == 0)
{
fprintf(fout, "\n");
fprintf(fout, " //%s\n", pCurrent->name);
}
}
else
fprintf(fout, " %s();\n", pCurrent->name);
pCurrent = pCurrent->pNext;
}
fprintf(fout, "}\n");
fprintf(fout, "#else\n");
fprintf(fout, "; //removes warning C4206: nonstandard extension used: translation unit is empty\n");
fprintf(fout, "#endif\n");
fclose(fout);
printf("file '%s' was updated\n", output);
}
else
{
printf("cannot open the ouput '%s' file\n", output);
}
}
struct stream {
/*cursor*/
const char* p;
/*
name of the current node
*/
const char* node;
struct TestList* list;
const char* out;
};
#define current(stream) (stream)->p[0]
#define next(stream) (stream)->p[1]
#define match(stream) stream->p++
#define is_space(stream) (current(stream) == ' ' || current(stream) == '\r' || current(stream) == '\n')
#define skip_spaces(p) while (is_space(p)) { match(p); }
const char* parse_id(struct stream* p)
{
const char* start = p->p;
while (current(p) >= 'a' && current(p) <= 'z' ||
current(p) >= 'A' && current(p) <= 'Z' ||
current(p) >= '0' && current(p) <= '9' ||
current(p) >= '_')
{
match(p);
}
return start;
}
void parse_attribute(struct stream* p)
{
/*
attribute ::= <id> '=' '"' <string> '"'
*/
if (current(p) >= 'a' && current(p) <= 'z' ||
current(p) >= 'A' && current(p) <= 'Z' ||
current(p) >= '_') /*first of <id>*/
{
const char* start = parse_id(p);
//printf("attribute '%.*s'", (int)(p->p - start), start);
skip_spaces(p);
match(p); // '='
skip_spaces(p);
match(p); // '"'
start = p->p;
while (current(p) != '\"') match(p);
//printf(" = value '%.*s'\n", (int)(p->p - start), start);
if (strcmp(p->node, "ClCompile") == 0)
{
char fileName[200] = { 0 };
strncpy(fileName, start, (int)(p->p - start));
//we dont collect tests from the ouput itself
if (strcmp(fileName, p->out) != 0)
{
CollectTestsFile(fileName, p->list);
}
//printf("%.*s\n", (int)(p->p - start), start);
}
match(p); // '"'
}
}
void parse_attribute_list(struct stream* p)
{
/*
attribute_list ::= <attribute> <attribute_list> | (empty)
*/
skip_spaces(p);
while (current(p) >= 'a' && current(p) <= 'z' ||
current(p) >= 'A' && current(p) <= 'Z' ||
current(p) >= '_')
{
parse_attribute(p);
skip_spaces(p);
}
}
void parse_node(struct stream* p);
void parse_node_list(struct stream* p)
{
/*
node_list ::= <node> <node_list> | (empty)
*/
while (current(p) == '<' && next(p) != '/')
{
parse_node(p);
skip_spaces(p);
}
}
const char* parse_string(struct stream* p)
{
const char* start = p->p;
if (start != 0)
{
while (current(p) != '<' && current(p) != '>') match(p);
//printf("string = '%.*s'\n", (int)(p->p - start), start);
}
return start;
}
void parse_element(struct stream* p)
{
/*
element ::= <node_list> | <string>
*/
skip_spaces(p);
if (current(p) == '<' && next(p) != '/') /*first of node*/
parse_node_list(p);
else
parse_string(p);
}
void parse_node_end(struct stream* p, const char* nodeid, int nodeid_size)
{
/*
* node_end ::= '/>' | '>' <element> '</' <id> '>'
*/
skip_spaces(p);
if (current(p) == '/' && next(p) == '>')
{
match(p); // '/'
match(p); // ''>'
}
else if (current(p) == '>')
{
match(p); // '>'
parse_element(p);
match(p); // '<'
match(p); // '/'
const char* start = parse_id(p);
match(p); // '>'
//printf("end_node </'%.*s'>\n", nodeid_size, nodeid);
}
}
void parse_node(struct stream* p)
{
/*
node ::= '<' <id> <attribute_list> <node_end> | (empty)
*/
skip_spaces(p);
if (current(p) == '<')
{
match(p);
skip_spaces(p);
const char* start = parse_id(p);
const char* nodeid = start;
int nodeid_size = (int)(p->p - start);
char node[100] = { 0 };
strncpy(node, nodeid, nodeid_size);
const char* oldnode = p->node;
p->node = node;
//printf(" node <'%.*s'>\n", nodeid_size, nodeid);
skip_spaces(p);
parse_attribute_list(p);
parse_node_end(p, nodeid, nodeid_size);
p->node = oldnode;
}
}
bool fread2(void* buffer, size_t size, size_t count, FILE* stream, size_t* sz)
{
*sz = 0;//out
bool result = false;
size_t n = fread(buffer, size, count, stream);
if (n == count) {
*sz = n;
result = true;
}
else if (n < count) {
if (feof(stream))
{
*sz = n;
result = true;
}
}
return result;
}
char* readfile(const char* path)
{
char* result = NULL;
struct stat info;
if (stat(path, &info) == 0)
{
char* data = (char*)malloc(sizeof(char) * info.st_size + 1);
if (data != NULL)
{
FILE* file = fopen(path, "r");
if (file != NULL)
{
if (info.st_size >= 3)
{
size_t n = 0;
if (fread2(data, 1, 3, file, &n))
{
if (n == 3)
{
if (data[0] == (char)0xEF &&
data[1] == (char)0xBB &&
data[2] == (char)0xBF)
{
if (fread2(data, 1, info.st_size - 3, file, &n))
{
//ok
data[n] = 0;
result = data; data = 0;
}
}
else if (fread2(data + 3, 1, info.st_size - 3, file, &n))
{
data[3 + n] = 0;
result = data; data = 0;
}
}
else
{
data[n] = 0;
result = data; data = 0;
}
}
}
else
{
size_t n = 0;
if (fread2(data, 1, info.st_size, file, &n))
{
data[n] = 0;
result = data; data = 0;
}
}
fclose(file);
}
free(data);
}
}
return result;
}
int main(int argc, char** argv) {
if (argc < 3)
{
printf("usage: projec.vcxproj output.c\n");
return EXIT_FAILURE;
}
printf("Searching for tests in '%s'\n", argv[1]);
const char* p = readfile(argv[1]);
while (*p != '\n')
{
p++;
}
struct TestList list = { 0 };
struct stream s = { .p = p , .list = &list, .out = argv[2] };
parse_node(&s);
Generate(argv[2], &list);
}