Files
Noja/tests/tester.c
T

555 lines
14 KiB
C

#include <stdbool.h>
#include <assert.h>
#include <string.h>
#include <dirent.h>
#include <stdio.h>
#include <ctype.h>
#include "../src/noja/utils/source.h"
#include "../src/noja/compiler/parse.h"
#include "../src/noja/compiler/compile.h"
#include "../src/noja/common/executable.h"
#include "../src/noja/assembler/assemble.h"
//Regular text
#define BLK "\e[0;30m"
#define RED "\e[0;31m"
#define GRN "\e[0;32m"
#define YEL "\e[0;33m"
#define BLU "\e[0;34m"
#define MAG "\e[0;35m"
#define CYN "\e[0;36m"
#define WHT "\e[0;37m"
//Regular bold text
#define BBLK "\e[1;30m"
#define BRED "\e[1;31m"
#define BGRN "\e[1;32m"
#define BYEL "\e[1;33m"
#define BBLU "\e[1;34m"
#define BMAG "\e[1;35m"
#define BCYN "\e[1;36m"
#define BWHT "\e[1;37m"
//Regular underline text
#define UBLK "\e[4;30m"
#define URED "\e[4;31m"
#define UGRN "\e[4;32m"
#define UYEL "\e[4;33m"
#define UBLU "\e[4;34m"
#define UMAG "\e[4;35m"
#define UCYN "\e[4;36m"
#define UWHT "\e[4;37m"
//Regular background
#define BLKB "\e[40m"
#define REDB "\e[41m"
#define GRNB "\e[42m"
#define YELB "\e[43m"
#define BLUB "\e[44m"
#define MAGB "\e[45m"
#define CYNB "\e[46m"
#define WHTB "\e[47m"
//High intensty background
#define BLKHB "\e[0;100m"
#define REDHB "\e[0;101m"
#define GRNHB "\e[0;102m"
#define YELHB "\e[0;103m"
#define BLUHB "\e[0;104m"
#define MAGHB "\e[0;105m"
#define CYNHB "\e[0;106m"
#define WHTHB "\e[0;107m"
//High intensty text
#define HBLK "\e[0;90m"
#define HRED "\e[0;91m"
#define HGRN "\e[0;92m"
#define HYEL "\e[0;93m"
#define HBLU "\e[0;94m"
#define HMAG "\e[0;95m"
#define HCYN "\e[0;96m"
#define HWHT "\e[0;97m"
//Bold high intensity text
#define BHBLK "\e[1;90m"
#define BHRED "\e[1;91m"
#define BHGRN "\e[1;92m"
#define BHYEL "\e[1;93m"
#define BHBLU "\e[1;94m"
#define BHMAG "\e[1;95m"
#define BHCYN "\e[1;96m"
#define BHWHT "\e[1;97m"
//Reset
#define reset "\e[0m"
#define CRESET "\e[0m"
#define COLOR_RESET "\e[0m"
// NOTE: From https://gist.github.com/RabaDabaDoba/145049536f815903c79944599c6f952a
static void print_error(const char *type, Error *error)
{
if(type == NULL)
fprintf(stderr, RED "Error" CRESET);
else if(error->internal)
fprintf(stderr, RED "Internal Error" CRESET);
else
fprintf(stderr, RED "%s Error" CRESET, type);
fprintf(stderr, " :: %s.", error->message);
#ifdef DEBUG
if(error->file != NULL)
{
if(error->line > 0 && error->func != NULL)
fprintf(stderr, " (Reported in %s:%d in %s)", error->file, error->line, error->func);
else if(error->line > 0 && error->func == NULL)
fprintf(stderr, " (Reported in %s:%d)", error->file, error->line);
else if(error->line < 1 && error->func != NULL)
fprintf(stderr, " (Reported in %s in %s)", error->file, error->func);
}
#endif
fprintf(stderr, "\n");
}
typedef enum {
TokenType_END,
TokenType_TEXT,
TokenType_DIRECTIVE_SOURCE,
TokenType_DIRECTIVE_BYTECODE,
} TokenType;
typedef struct {
TokenType type;
size_t offset;
size_t length;
} Token;
static bool startsWithSourceDirective(const char *str, size_t len)
{
return (((7 < len && !isalpha(str[7])) || 7 == len)
&& str[0] == '#' && str[1] == 's'
&& str[2] == 'o' && str[3] == 'u'
&& str[4] == 'r' && str[5] == 'c'
&& str[6] == 'e');
}
static bool startsWithBytecodeDirective(const char *str, size_t len)
{
return (((9 < len && !isalpha(str[9])) || 9 == len)
&& str[0] == '#' && str[1] == 'b'
&& str[2] == 'y' && str[3] == 't'
&& str[4] == 'e' && str[5] == 'c'
&& str[6] == 'o' && str[7] == 'd'
&& str[8] == 'e');
}
static bool startsWithDirective(const char *str, size_t len)
{
return startsWithSourceDirective(str, len)
|| startsWithBytecodeDirective(str, len);
}
static Token nextToken(const char *str, size_t len, size_t *cur_)
{
size_t cur = *cur_;
while(cur < len && isspace(str[cur]))
cur += 1;
Token token;
if(cur == len) {
token.type = TokenType_END;
token.offset = cur;
token.length = 0;
} else if(startsWithSourceDirective(str + cur, len - cur)) {
token.type = TokenType_DIRECTIVE_SOURCE;
token.offset = cur;
cur += strlen("#source");
token.length = cur - token.offset;
} else if(startsWithBytecodeDirective(str + cur, len - cur)) {
token.type = TokenType_DIRECTIVE_BYTECODE;
token.offset = cur;
cur += strlen("#bytecode");
token.length = cur - token.offset;
} else {
token.type = TokenType_TEXT;
token.offset = cur;
while(1) {
// Consume all characters until
// the next '#'.
while(cur < len && str[cur] != '#')
cur += 1;
if(cur == len)
break;
// If it's the start of a directive,
// stop the loop. If it was not, skip
// the '#'.
if(startsWithDirective(str + cur, len - cur))
break;
cur += 1; // Skip the '#'
}
token.length = cur - token.offset;
}
*cur_ = cur;
return token;
}
typedef struct {
Source *source;
Source *bytecode;
} TestCase;
static bool parseTestCaseSource(Source *source, TestCase *testcase)
{
const char *str = Source_GetBody(source);
size_t len = Source_GetSize(source);
assert(str != NULL);
bool no_source = true;
size_t source_offset;
size_t source_length;
bool no_bytecode = true;
size_t bytecode_offset;
size_t bytecode_length;
size_t cur = 0;
while(1) {
Token token = nextToken(str, len, &cur);
if(token.type == TokenType_END)
break;
if(token.type == TokenType_TEXT) {
fprintf(stderr, RED "Error" CRESET " :: Expected a directive at offset %ld\n", token.offset);
return false;
}
assert(token.type == TokenType_DIRECTIVE_SOURCE
|| token.type == TokenType_DIRECTIVE_BYTECODE);
Token directive = token;
size_t directive_body_offset;
size_t directive_body_length;
size_t backup = cur;
token = nextToken(str, len, &cur);
switch(token.type) {
case TokenType_END:
case TokenType_DIRECTIVE_SOURCE:
case TokenType_DIRECTIVE_BYTECODE:
// Right after the previous directive
// there's either the end of the file
// or another directive. We assume
// that the directive is followed by
// and empty string.
directive_body_offset = token.offset;
directive_body_length = 0;
// Put the cursor back so that this
// token is tokenized again at the
// start of the next iteration.
cur = backup;
break;
case TokenType_TEXT:
directive_body_offset = token.offset;
directive_body_length = token.length;
break;
}
if(directive.type == TokenType_DIRECTIVE_SOURCE) {
if(no_source == false) {
fprintf(stderr, RED "Error" CRESET " :: #source directive specified twice.\n");
return false;
}
no_source = false;
source_offset = directive_body_offset;
source_length = directive_body_length;
} else {
assert(directive.type == TokenType_DIRECTIVE_BYTECODE);
if(no_bytecode == false) {
fprintf(stderr, RED "Error" CRESET " :: #bytecode directive specified twice.\n");
return false;
}
no_bytecode = false;
bytecode_offset = directive_body_offset;
bytecode_length = directive_body_length;
}
}
if(no_source == true && no_bytecode == true) {
fprintf(stderr, RED "Error" CRESET " :: Missing both #source and #bytecode directives.\n");
return false;
}
if(no_source == true) {
fprintf(stderr, RED "Error" CRESET " :: Missing #source directive.\n");
return false;
}
if(no_bytecode == true) {
fprintf(stderr, RED "Error" CRESET " :: Missing #bytecode directive.\n");
return false;
}
{
Error error;
Error_Init(&error);
testcase->source = Source_FromString("<input>", str + source_offset, source_length, &error);
if(testcase->source == NULL) {
print_error(NULL, &error);
Error_Free(&error);
return false;
}
testcase->bytecode = Source_FromString("<output>", str + bytecode_offset, bytecode_length, &error);
if(testcase->bytecode == NULL) {
print_error(NULL, &error);
Source_Free(testcase->source);
Error_Free(&error);
return false;
}
Error_Free(&error);
}
return true;
}
static Executable *build(Source *src)
{
Executable *exe;
// Create a bump-pointer allocator to hold the AST.
BPAlloc *alloc = BPAlloc_Init(-1);
if(alloc == NULL)
{
fprintf(stderr, RED "Internal Error" CRESET " :: Couldn't allocate bump-pointer allocator to hold the AST.\n");
return 0;
}
Error error;
Error_Init(&error);
// NOTE: The AST is stored in the BPAlloc. It's
// lifetime is the same as the pool.
AST *ast = parse(src, alloc, &error);
if(ast == NULL)
{
assert(error.occurred);
print_error("Parsing", &error);
Error_Free(&error);
BPAlloc_Free(alloc);
return 0;
}
exe = compile(ast, alloc, &error);
// We're done with the AST, independently from
// the compilation result.
BPAlloc_Free(alloc);
if(exe == NULL)
{
assert(error.occurred);
print_error("Compilation", &error);
Error_Free(&error);
return 0;
}
Error_Free(&error);
return exe;
}
typedef enum {
TestResult_PASSED,
TestResult_FAILED,
TestResult_ABORT,
} TestResult;
static TestResult runTest(const char *file)
{
Source *source;
{
Error error;
Error_Init(&error);
source = Source_FromFile(file, &error);
if(source == NULL) {
print_error(NULL, &error);
Error_Free(&error);
return TestResult_ABORT;
}
Error_Free(&error);
}
TestCase testcase;
{
if(!parseTestCaseSource(source, &testcase)) {
Source_Free(source);
return TestResult_ABORT;
}
}
Source_Free(source);
Executable *exe1 = build(testcase.source);
if(exe1 == NULL) {
Source_Free(testcase.source);
Source_Free(testcase.bytecode);
return TestResult_ABORT;
}
Executable *exe2;
{
Error error;
Error_Init(&error);
exe2 = assemble(testcase.bytecode, &error);
if(exe2 == NULL) {
print_error("Assemblation", &error);
Error_Free(&error);
Executable_Free(exe1);
Source_Free(testcase.source);
Source_Free(testcase.bytecode);
return TestResult_ABORT;
}
Error_Free(&error);
}
bool passed = Executable_Equiv(exe1, exe2, stderr, " " WHT "Log" CRESET " :: ");
Executable_Free(exe1);
Executable_Free(exe2);
Source_Free(testcase.source);
Source_Free(testcase.bytecode);
return passed
? TestResult_PASSED
: TestResult_FAILED;
}
static void runTestSuite(const char *folder)
{
DIR *handle = opendir(folder);
if(handle == NULL) {
fprintf(stderr, RED "Error" CRESET " :: Failed to open folder '%s'.\n", folder);
return;
}
size_t total_tests = 0;
size_t passed_tests = 0;
size_t failed_tests = 0;
size_t aborted_tests = 0;
size_t skipped_files = 0;
size_t folder_len = strlen(folder);
// If the folder path ends with a "/",
// but it's not the first character,
// then pop it.
if(folder_len > 0 && folder[folder_len-1] == '/')
folder_len -= 1;
// Now we know for sure that the provided
// folder needs a trailing "/".
char test_path[1024];
// Make sure the buffer can contain the
// folder's name (plus the trailing "/"
// and the null byte).
if(folder_len+2 > sizeof(test_path)) {
fprintf(stderr, RED "Error" CRESET " :: Suite folder name is too long\n");
return;
}
memcpy(test_path, folder, folder_len);
test_path[folder_len] = '/';
test_path[folder_len+1] = '\0';
// Now count the "/".
folder_len += 1;
struct dirent *dir;
while((dir = readdir(handle)) != NULL) {
const char *name = dir->d_name;
size_t name_len = strlen(name);
if(dir->d_type != DT_REG) {
fprintf(stderr, " " WHT "Log" CRESET " :: Skipping '%s' (not a regular file).\n", name);
skipped_files += 1;
continue;
}
if(folder_len + name_len >= sizeof(test_path)) {
fprintf(stderr, " " WHT "Log" CRESET " :: Skipping '%s' (path is too long).\n", name);
skipped_files += 1;
continue;
}
// Remove the previous file's name
// by truncating the parent folder
// name with a null byte.
test_path[folder_len] = '\0';
strcat(test_path, name);
TestResult res = runTest(test_path);
switch(res) {
case TestResult_PASSED:
fprintf(stdout, " " BLU "Test" CRESET " :: Passed (%s)\n", name);
passed_tests += 1;
break;
case TestResult_FAILED:
fprintf(stdout, " " YEL "Test" CRESET " :: Failed (%s)\n", name);
failed_tests += 1;
break;
case TestResult_ABORT:
fprintf(stdout, " " RED "Test" CRESET " :: Aborted (%s)\n", name);
aborted_tests += 1;
break;
}
total_tests += 1;
}
fprintf(stdout, "\n");
fprintf(stdout, "@-----SUMMARY-----@\n");
fprintf(stdout, "| Total : %-3ld |\n", total_tests);
fprintf(stdout, "| Passed : %-3ld |\n", passed_tests);
fprintf(stdout, "| Failed : %-3ld |\n", failed_tests);
fprintf(stdout, "| Aborted : %-3ld |\n", aborted_tests);
fprintf(stdout, "@-----------------@\n");
closedir(handle);
}
int main(int argc, char **argv)
{
if(argc < 2) {
fprintf(stderr, RED "Error" CRESET " :: No test suite folder name was provided.\n");
fprintf(stderr, WHT "Usage" CRESET " :: %s <path>\n", argv[0]);
return -1;
}
const char *folder = argv[1];
runTestSuite(folder);
return 0;
}