Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 8 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# simpletest
# simpletest-EmbeddedSafety

_**A super simple framwork for implementing Unit Tests**_
_**A super simple framwork for implementing Unit Tests ON embedded systems with functional safety requirements**_

[![Build Status](https://travis-ci.org/kudaba/simpletest_test.svg?branch=master)](https://travis-ci.org/kudaba/simpletest_test)
[![Build Status](https://ci.appveyor.com/api/projects/status/github/kudaba/simpletest_test?branch=master&svg=true)](https://ci.appveyor.com/project/kudaba/simpletest-test)
Expand All @@ -10,12 +10,14 @@ _**A super simple framwork for implementing Unit Tests**_
src="https://scan.coverity.com/projects/15803/badge.svg"/>
</a>

Tested with ARM compiler 6.6 and 6.6.4 using ARM FuSa (Functional Safety) library.

A lot of c++ unit tests claim to be simple, but when I went searching for the perfect one there was always something that prevented me from using it. They were either overly complicated or had some critical flaw like excess memory allocations or dependencies on external programs. So here is the simplest form of unit test I could come up with to cover basic development. My rules for simple were the following:
* Basic test features only: fixtures and test
* Simple, isolated test declaration
* No memory allocations, at all
* Very few, if any dependencies
* Bonus: Threadable
* Bonus: Threadable (not in the EmbeddedSafety version)

Head over to [simpletest_test](https://github.com/kudaba/simpletest_test) for more complete usage examples.

Expand Down Expand Up @@ -94,7 +96,7 @@ One of the main reason I want a unit test is to make sure my code doesn't leak.
# Very few dependencies
After seeing unit tests that need perl or python to generate test harnesses, or other crazy code dependencies, I wanted to use the most limited set of dependencies I could. I didn't go as far as a single header implementation, but even the cpp only depends on two standard headers, stdio.h and string.h.

# Threadable
# Threadable (not applicable to the EmbeddedSafety version)
By keeping the fixture, test and results in a single object it means that the execution of a single test is threadable as long that the test code itself is contained and threadable. There is no default threaded implementation of test execution, but you can see the simpletest_test project for more advanced examples.

# Notable Differences
Expand All @@ -113,7 +115,7 @@ Global configuration is generally done by defininy macros before including simpl
If you want all tests to use the same fixture, for default memory checking or exception catching, then simply define the macro BASE_FIXTURE.

## Static memory usage
To achieve allocation free tests I needed to give each test a memory area to write their error messages. The default is (probably out of date) 10k per test. You can override this by defining the MESSAGE_SPACE macro.
To achieve allocation free tests I needed to give each test a memory area to write their error messages. The default is (probably out of date) 10k per test (in the EmbeddedSafety version it is much smaller at 100 bytes). You can override this by defining the MESSAGE_SPACE macro.

## Temporary string length
The buffer size of the temporary string object can be set by defining STRING_LENGTH. I figured 64 bytes is a decent size for anything that isn't already a string.
The buffer size of the temporary string object can be set by defining STRING_LENGTH. I figured 64 bytes is a decent size for anything that isn't already a string.
233 changes: 57 additions & 176 deletions simpletest.cpp
Original file line number Diff line number Diff line change
@@ -1,20 +1,53 @@
#include "simpletest.h"
#include <stdio.h>
#include <string.h>
#include <cstdint>
#include <stdarg.h>
#include <time.h>

#ifdef __cplusplus
extern "C" {
#include <stdio.h>
#include <string.h>
#include <stdarg.h>
} ;
#endif // __cplusplus

extern "C" void __cxa_pure_virtual() { while (1); } // added to support setting virtual functions as pure (by adding ' = 0 ' at the end of their declaration.

//---------------------------------------------------------------------------------
// statics
//---------------------------------------------------------------------------------
void DefaultPrint(char const* string) { printf("%s", string); }
void DefaultPrint(char const* string) { printf("%s", string); } // switch printf with any other printing solution you want.

TestFixture* TestFixture::ourFirstTest;
TestFixture* TestFixture::ourLastTest;
thread_local TestFixture* TestFixture::ourCurrentTest;
//thread_local TestFixture* TestFixture::ourCurrentTest;
TestFixture* TestFixture::ourCurrentTest;
void (*TestFixture::Print)(char const* string) = DefaultPrint;

//---------------------------------------------------------------------------------
// helper to get the number of decimal places to print for floats and doubles
//---------------------------------------------------------------------------------
template <typename T>
static int locDecimals(T value)
{
const T eps = 0.00001f;
T remainder = value - (int)value;
if (remainder == 0)
return 0;

int decimals = 0;

// add decimals until hitting the first non-zero number that shouldn't be rounded (close to 0 or 1)
bool hitsomething = int(remainder * 10) != 0;
while (!hitsomething ||
((remainder > eps && remainder < (1 - eps)) ||
(remainder < -eps && remainder >(-1 + eps))))
{
remainder = remainder * 10;
remainder = remainder - (int)remainder;
hitsomething |= int(remainder * 10) != 0;
++decimals;
}
return decimals;
}

//---------------------------------------------------------------------------------
// Standard type printers
//---------------------------------------------------------------------------------
Expand All @@ -33,67 +66,37 @@ TempString::TempString(const TempString& other)
TempString TypeToString(int value)
{
TempString tempString;
if (TestFixture::GetCurrentTest()->GetPrintMethod() == TestFixture::PrintHexadecimal)
snprintf(tempString.myTextBuffer, STRING_LENGTH, "0x%08X", value);
else
snprintf(tempString.myTextBuffer, STRING_LENGTH, "%d", value);
return tempString;
}
TempString TypeToString(unsigned int value)
{
TempString tempString;
if (TestFixture::GetCurrentTest()->GetPrintMethod() == TestFixture::PrintHexadecimal)
snprintf(tempString.myTextBuffer, STRING_LENGTH, "0x%08X", value);
else
snprintf(tempString.myTextBuffer, STRING_LENGTH, "%u", value);
return tempString;
}
TempString TypeToString(long value)
{
TempString tempString;
if (TestFixture::GetCurrentTest()->GetPrintMethod() == TestFixture::PrintHexadecimal)
snprintf(tempString.myTextBuffer, STRING_LENGTH, "0x%016lX", value);
else
snprintf(tempString.myTextBuffer, STRING_LENGTH, "%ld", value);
sprintf(tempString.myTextBuffer, "%d", value);
return tempString;
}
TempString TypeToString(unsigned long value)
TempString TypeToString(int64 value)
{
TempString tempString;
if (TestFixture::GetCurrentTest()->GetPrintMethod() == TestFixture::PrintHexadecimal)
snprintf(tempString.myTextBuffer, STRING_LENGTH, "0x%016lX", value);
else
snprintf(tempString.myTextBuffer, STRING_LENGTH, "%lu", value);
sprintf(tempString.myTextBuffer, "%lld", value);
return tempString;
}
TempString TypeToString(long long value)
TempString TypeToString(uint value)
{
TempString tempString;
if (TestFixture::GetCurrentTest()->GetPrintMethod() == TestFixture::PrintHexadecimal)
snprintf(tempString.myTextBuffer, STRING_LENGTH, "0x%016llX", value);
else
snprintf(tempString.myTextBuffer, STRING_LENGTH, "%lld", value);
sprintf(tempString.myTextBuffer, "%u", value);
return tempString;
}
TempString TypeToString(unsigned long long value)
TempString TypeToString(uint64 value)
{
TempString tempString;
if (TestFixture::GetCurrentTest()->GetPrintMethod() == TestFixture::PrintHexadecimal)
snprintf(tempString.myTextBuffer, STRING_LENGTH, "0x%016llX", value);
else
snprintf(tempString.myTextBuffer, STRING_LENGTH, "%llu", value);
sprintf(tempString.myTextBuffer, "%llu", value);
return tempString;
}
TempString TypeToString(float value)
{
TempString tempString;
snprintf(tempString.myTextBuffer, STRING_LENGTH, "%.16g", value);
sprintf(tempString.myTextBuffer, "%0.*f", locDecimals(value), value);
return tempString;
}
TempString TypeToString(double value)
{
TempString tempString;
snprintf(tempString.myTextBuffer, STRING_LENGTH, "%.16g", value);
sprintf(tempString.myTextBuffer, "%0.*f", locDecimals(value), value);
return tempString;
}
TempString TypeToString(bool value)
Expand All @@ -109,15 +112,7 @@ TempString TypeToString(void const* value)
if (value == nullptr)
return TempString("(nullptr)");
TempString tempString;
snprintf(tempString.myTextBuffer, STRING_LENGTH, "0x%p", value);
return tempString;
}
TempString TypeToString(void const* value, char const* extra)
{
if (value == nullptr)
return TempString("(nullptr)");
TempString tempString;
snprintf(tempString.myTextBuffer, STRING_LENGTH, "(0x%p) %s", value, extra);
sprintf(tempString.myTextBuffer, "0x%p", value);
return tempString;
}

Expand All @@ -126,10 +121,9 @@ TempString TypeToString(void const* value, char const* extra)
//---------------------------------------------------------------------------------
TestFixture::TestFixture()
: myNextTest(nullptr)
, myNextError(nullptr)
, myNumTestsChecked(0)
, myNumErrors(0)
, myPrintMethod(PrintDefault)
, myNextError(nullptr)
{
// global link list registration, add in order of discovery
if (ourFirstTest == nullptr)
Expand All @@ -143,121 +137,14 @@ TestFixture::TestFixture()
ourLastTest = this;
}
}
//---------------------------------------------------------------------------------
bool TestFixture::ExecuteTest()
{
myNumTestsChecked = myNumErrors = 0;
myNextError = (TestError*)myMessageSpace;

TestFixture* lastCurrent = ourCurrentTest;
ourCurrentTest = this;
Setup();
RunTest();
TearDown();
ourCurrentTest = lastCurrent;

return myNumErrors == 0;
}
//---------------------------------------------------------------------------------
// Utility to print a part of a string to show where the error is and put elipse
// where the string is truncated
//---------------------------------------------------------------------------------
static void locCopyStringWithElipse(char dest[STRING_EQ_PRINT_LENGTH], char const* string, size_t offset = 0)
{
char const* start = string + offset - STRING_EQ_PRINT_LENGTH / 2;
if (start < string)
start = string;

int i = 0;
for (; i < STRING_EQ_PRINT_LENGTH - 1 && start[i]; ++i)
{
if (i < 3 && start > string)
dest[i] = '.';
else if (start[i] == '\r' || start[i] == '\n' || start[i] == '\t')
dest[i] = '\\'; // simply replace this with '\', we're just aiming for a general idea not an exact representation
else
dest[i] = start[i];
}

dest[i] = 0;

if (i == STRING_EQ_PRINT_LENGTH - 1 && start[i])
{
dest[i - 1] = '.';
dest[i - 2] = '.';
dest[i - 3] = '.';
}
}
//---------------------------------------------------------------------------------
// Instead of just check for error and printing the string, try go be smart about
// how the information is written out:
// ... quick brown fox jumps over ...
// ^
// ... quick brown fox jamps over ...
//---------------------------------------------------------------------------------
bool TestFixture::TestStrings(char const* left, char const* right, char const* prefix, char const* condition)
{
AddTest();
if (left == right)
{
return true;
}

char leftLine[STRING_EQ_PRINT_LENGTH];
char rightLine[STRING_EQ_PRINT_LENGTH];
char locationLine[STRING_EQ_PRINT_LENGTH];

if (left == nullptr || right == nullptr)
{
locationLine[0] = '^';
locationLine[1] = 0;
if (left == nullptr)
{
strcpy(leftLine, "nullptr");
locCopyStringWithElipse(rightLine, right);
}
else
{
locCopyStringWithElipse(leftLine, left);
strcpy(rightLine, "nullptr");
}
}
else
{
char const* testLeft = left;
char const* testRight = right;

int offset = 0;
for (; *testLeft && *testRight; ++offset, ++testLeft, ++testRight)
{
if (*testLeft != *testRight)
break;
}

// reached the end of both strings, so they're the same
if (!*testLeft && !*testRight)
return true;

locCopyStringWithElipse(leftLine, left, offset);
locCopyStringWithElipse(rightLine, right, offset);

if (offset > STRING_EQ_PRINT_LENGTH / 2)
offset = STRING_EQ_PRINT_LENGTH / 2;

memset(locationLine, ' ', offset);
locationLine[offset] = '^';
locationLine[offset + 1] = 0;
}

AddError();
LogMessage(prefix, condition, leftLine, locationLine, rightLine);
return false;
}
//---------------------------------------------------------------------------------
// Write error into current error object and advance pointer if there's still enough space
//---------------------------------------------------------------------------------
void TestFixture::LogMessage(char const* string, ...)
void TestFixture::LogError(char const* string, ...)
{
++myNumErrors;

uintptr_t spaceLeft = (myMessageSpace + MESSAGE_SPACE) - (char*)myNextError;

if (spaceLeft == 0)
Expand Down Expand Up @@ -302,20 +189,13 @@ TestFixture const* TestFixture::LinkTest(TestFixture* test)
//---------------------------------------------------------------------------------
static bool locExecuteTest(TestFixture* test, TestFixture::OutputMode output)
{
clock_t start = 0;
if (output == TestFixture::Verbose)
{
TestFixture::Printf("Running [%s/%s]", test->TestGroup(), test->TestName());
start = clock();
}

if (test->ExecuteTest())
{
if (output == TestFixture::Verbose)
{
clock_t end = clock();
TestFixture::Printf(": Passed %d out of %d tests in %g seconds\n", test->NumTests(), test->NumTests(), float(end - start) / (float)CLOCKS_PER_SEC);
}
TestFixture::Printf(": Passed %d out of %d tests\n", test->NumTests(), test->NumTests());
return true;
}

Expand All @@ -334,9 +214,10 @@ static bool locExecuteTest(TestFixture* test, TestFixture::OutputMode output)

return false;
}

void TestFixture::Printf(char const* string, ...)
{
char tempSpace[4096];
char tempSpace[100];
va_list args;

va_start(args, string);
Expand Down
Loading