diff --git a/README.md b/README.md index 40c25df0e..770aaac9e 100644 --- a/README.md +++ b/README.md @@ -89,6 +89,7 @@ $ vcpkg install cpputest (More information: https://github.com/microsoft/vcpkg) * `TEST_SETUP()` - Declare a void setup method in a `TEST_GROUP` - this is the same as declaring void `setup()` * `TEST_TEARDOWN()` - Declare a void setup method in a `TEST_GROUP` * `IMPORT_TEST_GROUP(group)` - Export the name of a test group so it can be linked in from a library. Needs to be done in `main`. +* `TEST_PROPERTY(name, value)` - Attach a key-value string property to the current test. Properties appear as `` elements inside the `` block in JUnit XML output. For use in C files, use `TEST_PROPERTY_C(name, value)` instead. ## Set up and tear down support diff --git a/include/CppUTest/JUnitTestOutput.h b/include/CppUTest/JUnitTestOutput.h index 5e5746f56..79984570e 100644 --- a/include/CppUTest/JUnitTestOutput.h +++ b/include/CppUTest/JUnitTestOutput.h @@ -52,6 +52,7 @@ class JUnitTestOutput: public TestOutput virtual void print(long) CPPUTEST_OVERRIDE; virtual void print(size_t) CPPUTEST_OVERRIDE; virtual void printFailure(const TestFailure& failure) CPPUTEST_OVERRIDE; + virtual void printTestProperty(const char* name, const char* value) CPPUTEST_OVERRIDE; virtual void flush() CPPUTEST_OVERRIDE; diff --git a/include/CppUTest/TestHarness_c.h b/include/CppUTest/TestHarness_c.h index f3a64cf83..8ee962f0c 100644 --- a/include/CppUTest/TestHarness_c.h +++ b/include/CppUTest/TestHarness_c.h @@ -216,6 +216,7 @@ extern void CHECK_EQUAL_C_BITS_LOCATION(unsigned int expected, unsigned int actu extern void FAIL_TEXT_C_LOCATION(const char* text, const char* fileName, size_t lineNumber); extern void FAIL_C_LOCATION(const char* fileName, size_t lineNumber); extern void CHECK_C_LOCATION(int condition, const char* conditionString, const char* text, const char* fileName, size_t lineNumber); +extern void TEST_PROPERTY_C(const char* name, const char* value); extern void* cpputest_malloc(size_t size); extern char* cpputest_strdup(const char* str); diff --git a/include/CppUTest/TestOutput.h b/include/CppUTest/TestOutput.h index 47fbe02ea..e4572f6ce 100644 --- a/include/CppUTest/TestOutput.h +++ b/include/CppUTest/TestOutput.h @@ -71,6 +71,7 @@ class TestOutput virtual void setProgressIndicator(const char*); virtual void printVeryVerbose(const char*); + virtual void printTestProperty(const char* name, const char* value); virtual void flush()=0; @@ -199,6 +200,7 @@ class CompositeTestOutput : public TestOutput virtual void setProgressIndicator(const char*) CPPUTEST_OVERRIDE; virtual void printVeryVerbose(const char*) CPPUTEST_OVERRIDE; + virtual void printTestProperty(const char* name, const char* value) CPPUTEST_OVERRIDE; virtual void flush() CPPUTEST_OVERRIDE; diff --git a/include/CppUTest/TestResult.h b/include/CppUTest/TestResult.h index fc6e63fd6..68618c9d0 100644 --- a/include/CppUTest/TestResult.h +++ b/include/CppUTest/TestResult.h @@ -58,6 +58,7 @@ class TestResult virtual void countFilteredOut(); virtual void countIgnored(); virtual void addFailure(const TestFailure& failure); + virtual void addProperty(const char* name, const char* value); virtual void print(const char* text); virtual void printVeryVerbose(const char* text); diff --git a/include/CppUTest/Utest.h b/include/CppUTest/Utest.h index 02e865b76..1ba6321ba 100644 --- a/include/CppUTest/Utest.h +++ b/include/CppUTest/Utest.h @@ -152,6 +152,7 @@ class UtestShell virtual void print(const char *text, const char *fileName, size_t lineNumber); virtual void print(const SimpleString & text, const char *fileName, size_t lineNumber); virtual void printVeryVerbose(const char* text); + virtual void addTestProperty(const char* name, const char* value); void setFileName(const char *fileName); void setLineNumber(size_t lineNumber); diff --git a/include/CppUTest/UtestMacros.h b/include/CppUTest/UtestMacros.h index 743bf069e..3dbda3161 100644 --- a/include/CppUTest/UtestMacros.h +++ b/include/CppUTest/UtestMacros.h @@ -387,4 +387,7 @@ #define UT_CRASH() do { UtestShell::crash(); } while(0) #define RUN_ALL_TESTS(ac, av) CommandLineTestRunner::RunAllTests(ac, av) +#define TEST_PROPERTY(name, value) \ + do { UtestShell::getCurrent()->addTestProperty(name, value); } while(0) + #endif /*D_UTestMacros_h*/ diff --git a/src/CppUTest/JUnitTestOutput.cpp b/src/CppUTest/JUnitTestOutput.cpp index f509d9b72..8f1aec2b0 100644 --- a/src/CppUTest/JUnitTestOutput.cpp +++ b/src/CppUTest/JUnitTestOutput.cpp @@ -31,10 +31,19 @@ #include "CppUTest/TestFailure.h" #include "CppUTest/PlatformSpecificFunctions.h" +struct TestPropertyResultNode +{ + TestPropertyResultNode() : next_(NULLPTR) {} + SimpleString name_; + SimpleString value_; + TestPropertyResultNode* next_; +}; + struct JUnitTestCaseResultNode { JUnitTestCaseResultNode() : - execTime_(0), failure_(NULLPTR), ignored_(false), lineNumber_ (0), checkCount_ (0), next_(NULLPTR) + execTime_(0), failure_(NULLPTR), ignored_(false), lineNumber_ (0), checkCount_ (0), + properties_(NULLPTR), propertiesTail_(NULLPTR), next_(NULLPTR) { } @@ -45,6 +54,8 @@ struct JUnitTestCaseResultNode SimpleString file_; size_t lineNumber_; size_t checkCount_; + TestPropertyResultNode* properties_; + TestPropertyResultNode* propertiesTail_; JUnitTestCaseResultNode* next_; }; @@ -93,6 +104,12 @@ void JUnitTestOutput::resetTestGroupResult() while (cur) { JUnitTestCaseResultNode* tmp = cur->next_; delete cur->failure_; + TestPropertyResultNode* prop = cur->properties_; + while (prop) { + TestPropertyResultNode* tmpProp = prop->next_; + delete prop; + prop = tmpProp; + } delete cur; cur = tmp; } @@ -233,6 +250,20 @@ void JUnitTestOutput::writeTestCases() impl_->results_.totalCheckCount_ = cur->checkCount_; + if (cur->properties_) { + writeToFile("\n"); + TestPropertyResultNode* prop = cur->properties_; + while (prop) { + SimpleString propBuf = StringFromFormat( + "\n", + encodeXmlText(prop->name_).asCharString(), + encodeXmlText(prop->value_).asCharString()); + writeToFile(propBuf.asCharString()); + prop = prop->next_; + } + writeToFile("\n"); + } + if (cur->failure_) { writeFailure(cur); } @@ -309,6 +340,22 @@ void JUnitTestOutput::printFailure(const TestFailure& failure) } } +void JUnitTestOutput::printTestProperty(const char* name, const char* value) +{ + TestPropertyResultNode* node = new TestPropertyResultNode; + node->name_ = name; + node->value_ = value; + + if (impl_->results_.tail_->propertiesTail_ == NULLPTR) { + impl_->results_.tail_->properties_ = node; + impl_->results_.tail_->propertiesTail_ = node; + } + else { + impl_->results_.tail_->propertiesTail_->next_ = node; + impl_->results_.tail_->propertiesTail_ = node; + } +} + void JUnitTestOutput::openFileForWrite(const SimpleString& fileName) { impl_->file_ = PlatformSpecificFOpen(fileName.asCharString(), "w"); diff --git a/src/CppUTest/TestHarness_c.cpp b/src/CppUTest/TestHarness_c.cpp index 149c8fbb6..7c972c89c 100644 --- a/src/CppUTest/TestHarness_c.cpp +++ b/src/CppUTest/TestHarness_c.cpp @@ -124,6 +124,11 @@ void CHECK_C_LOCATION(int condition, const char* conditionString, const char* te UtestShell::getCurrent()->assertTrue(condition != 0, "CHECK_C", conditionString, text, fileName, lineNumber, UtestShell::getCurrentTestTerminatorWithoutExceptions()); } +void TEST_PROPERTY_C(const char* name, const char* value) +{ + UtestShell::getCurrent()->addTestProperty(name, value); +} + enum { NO_COUNTDOWN = -1, OUT_OF_MEMORRY = 0 }; static int malloc_out_of_memory_counter = NO_COUNTDOWN; static int malloc_count = 0; diff --git a/src/CppUTest/TestOutput.cpp b/src/CppUTest/TestOutput.cpp index cb4ced54e..7c3e3946b 100644 --- a/src/CppUTest/TestOutput.cpp +++ b/src/CppUTest/TestOutput.cpp @@ -271,6 +271,10 @@ void TestOutput::printVeryVerbose(const char* str) printBuffer(str); } +void TestOutput::printTestProperty(const char*, const char*) +{ +} + void ConsoleTestOutput::printBuffer(const char* s) { @@ -406,6 +410,12 @@ void CompositeTestOutput::printVeryVerbose(const char* str) if (outputTwo_) outputTwo_->printVeryVerbose(str); } +void CompositeTestOutput::printTestProperty(const char* name, const char* value) +{ + if (outputOne_) outputOne_->printTestProperty(name, value); + if (outputTwo_) outputTwo_->printTestProperty(name, value); +} + void CompositeTestOutput::flush() { if (outputOne_) outputOne_->flush(); diff --git a/src/CppUTest/TestResult.cpp b/src/CppUTest/TestResult.cpp index 37ff57f84..b98e2985c 100644 --- a/src/CppUTest/TestResult.cpp +++ b/src/CppUTest/TestResult.cpp @@ -82,6 +82,11 @@ void TestResult::addFailure(const TestFailure& failure) failureCount_++; } +void TestResult::addProperty(const char* name, const char* value) +{ + output_.printTestProperty(name, value); +} + void TestResult::countTest() { testCount_++; diff --git a/src/CppUTest/Utest.cpp b/src/CppUTest/Utest.cpp index d74a87c79..ed5c7d662 100644 --- a/src/CppUTest/Utest.cpp +++ b/src/CppUTest/Utest.cpp @@ -576,6 +576,11 @@ void UtestShell::printVeryVerbose(const char* text) getTestResult()->printVeryVerbose(text); } +void UtestShell::addTestProperty(const char* name, const char* value) +{ + getTestResult()->addProperty(name, value); +} + TestResult* UtestShell::testResult_ = NULLPTR; UtestShell* UtestShell::currentTest_ = NULLPTR; diff --git a/tests/CppUTest/JUnitOutputTest.cpp b/tests/CppUTest/JUnitOutputTest.cpp index 32b5654d4..a96dded2f 100644 --- a/tests/CppUTest/JUnitOutputTest.cpp +++ b/tests/CppUTest/JUnitOutputTest.cpp @@ -149,6 +149,14 @@ extern "C" { } } +struct PendingProperty +{ + const char* name_; + const char* value_; + PendingProperty* next_; + PendingProperty(const char* n, const char* v, PendingProperty* nx) : name_(n), value_(v), next_(nx) {} +}; + class JUnitTestOutputTestRunner { TestResult result_; @@ -159,11 +167,13 @@ class JUnitTestOutputTestRunner unsigned int timeTheTestTakes_; unsigned int numberOfChecksInTest_; TestFailure* testFailure_; + PendingProperty* pendingProperties_; + PendingProperty* pendingPropertiesTail_; public: explicit JUnitTestOutputTestRunner(const TestResult& result) : - result_(result), currentGroupName_(NULLPTR), currentTest_(NULLPTR), firstTestInGroup_(true), timeTheTestTakes_(0), numberOfChecksInTest_(0), testFailure_(NULLPTR) + result_(result), currentGroupName_(NULLPTR), currentTest_(NULLPTR), firstTestInGroup_(true), timeTheTestTakes_(0), numberOfChecksInTest_(0), testFailure_(NULLPTR), pendingProperties_(NULLPTR), pendingPropertiesTail_(NULLPTR) { millisTime = 0; theTime = "1978-10-03T00:00:00"; @@ -264,6 +274,16 @@ class JUnitTestOutputTestRunner } numberOfChecksInTest_ = 0; + PendingProperty* prop = pendingProperties_; + pendingProperties_ = NULLPTR; + pendingPropertiesTail_ = NULLPTR; + while (prop) { + PendingProperty* next = prop->next_; + result_.addProperty(prop->name_, prop->value_); + delete prop; + prop = next; + } + if (testFailure_) { result_.addFailure(*testFailure_); delete testFailure_; @@ -308,6 +328,20 @@ class JUnitTestOutputTestRunner result_.print(output); return *this; } + + JUnitTestOutputTestRunner& withProperty(const char* name, const char* value) + { + PendingProperty* node = new PendingProperty(name, value, NULLPTR); + if (pendingPropertiesTail_ == NULLPTR) { + pendingProperties_ = node; + pendingPropertiesTail_ = node; + } + else { + pendingPropertiesTail_->next_ = node; + pendingPropertiesTail_ = node; + } + return *this; + } }; extern "C" { @@ -767,3 +801,84 @@ TEST(JUnitOutputTest, UTPRINTOutputInJUnitOutputWithSpecials) outputFile = fileSystem.file("cpputest_groupname.xml"); STRCMP_EQUAL("The <rain> in "Spain" Goes \\mainly\\ down the Dr&in \n", outputFile->lineFromTheBack(3)); } + +TEST(JUnitOutputTest, TestCaseWithOneProperty) +{ + testCaseRunner->start() + .withGroup("groupname").withTest("testname").withProperty("key", "val") + .end(); + + outputFile = fileSystem.file("cpputest_groupname.xml"); + STRCMP_EQUAL("\n", outputFile->line(5)); + STRCMP_EQUAL("\n", outputFile->line(6)); + STRCMP_EQUAL("\n", outputFile->line(7)); + STRCMP_EQUAL("\n", outputFile->line(8)); + STRCMP_EQUAL("\n", outputFile->line(9)); +} + +TEST(JUnitOutputTest, TestCaseWithMultiplePropertiesInInsertionOrder) +{ + testCaseRunner->start() + .withGroup("groupname").withTest("testname") + .withProperty("first", "one").withProperty("second", "two") + .end(); + + outputFile = fileSystem.file("cpputest_groupname.xml"); + STRCMP_EQUAL("\n", outputFile->line(7)); + STRCMP_EQUAL("\n", outputFile->line(8)); +} + +TEST(JUnitOutputTest, TestCaseWithNoPropertiesHasNoPropertiesBlock) +{ + testCaseRunner->start() + .withGroup("groupname").withTest("testname") + .end(); + + outputFile = fileSystem.file("cpputest_groupname.xml"); + STRCMP_EQUAL("\n", outputFile->line(5)); + STRCMP_EQUAL("\n", outputFile->line(6)); +} + +TEST(JUnitOutputTest, TestCasePropertyValuesAreXmlEncoded) +{ + testCaseRunner->start() + .withGroup("groupname").withTest("testname") + .withProperty("a&b", "") + .end(); + + outputFile = fileSystem.file("cpputest_groupname.xml"); + STRCMP_EQUAL("\n", outputFile->line(7)); +} + +TEST(JUnitOutputTest, PropertiesOnlyAppearOnCorrectTestInGroup) +{ + testCaseRunner->start() + .withGroup("groupname") + .withTest("firstTest") + .withTest("secondTest").withProperty("k", "v") + .end(); + + outputFile = fileSystem.file("cpputest_groupname.xml"); + // firstTest: lines 5-6 (no properties block) + STRCMP_EQUAL("\n", outputFile->line(5)); + STRCMP_EQUAL("\n", outputFile->line(6)); + // secondTest: lines 7-11 (with properties) + STRCMP_EQUAL("\n", outputFile->line(7)); + STRCMP_EQUAL("\n", outputFile->line(8)); + STRCMP_EQUAL("\n", outputFile->line(9)); + STRCMP_EQUAL("\n", outputFile->line(10)); + STRCMP_EQUAL("\n", outputFile->line(11)); +} + +TEST(JUnitOutputTest, PropertiesDontLeakAcrossGroups) +{ + testCaseRunner->start() + .withGroup("groupOne").withTest("testA").withProperty("x", "1") + .endGroupAndClearTest() + .withGroup("groupTwo").withTest("testB") + .end(); + + outputFile = fileSystem.file("cpputest_groupTwo.xml"); + STRCMP_EQUAL("\n", outputFile->line(5)); + STRCMP_EQUAL("\n", outputFile->line(6)); +} diff --git a/tests/CppUTest/TestHarness_cTest.cpp b/tests/CppUTest/TestHarness_cTest.cpp index fadc66f1d..61f1aba23 100644 --- a/tests/CppUTest/TestHarness_cTest.cpp +++ b/tests/CppUTest/TestHarness_cTest.cpp @@ -28,6 +28,7 @@ #include "CppUTest/TestHarness_c.h" #include "CppUTest/TestHarness.h" +#include "CppUTest/TestPlugin.h" #include "CppUTest/TestRegistry.h" #include "CppUTest/TestOutput.h" #include "CppUTest/TestTestingFixture.h" @@ -882,3 +883,59 @@ TEST(TestHarness_c, callocShouldReturnNULLWhenOutOfMemory) cpputest_malloc_set_not_out_of_memory(); } #endif + +class PropertySpyOutput : public StringBufferTestOutput +{ +public: + SimpleString lastPropertyName_; + SimpleString lastPropertyValue_; + int callCount_; + + PropertySpyOutput() : lastPropertyName_(""), lastPropertyValue_(""), callCount_(0) {} + + virtual void printTestProperty(const char* name, const char* value) CPPUTEST_OVERRIDE + { + lastPropertyName_ = name; + lastPropertyValue_ = value; + callCount_++; + } +}; + +static void callTestPropertyMacro_() +{ + TEST_PROPERTY("propname", "propval"); +} + +extern "C" void cpputest_test_property_c_caller(void); + +TEST(TestHarness_c, testPropertyMacroRoutesToOutput) +{ + PropertySpyOutput spy; + TestResult spyResult(spy); + ExecFunctionTestShell shell; + shell.testFunction_ = new ExecFunctionWithoutParameters(callTestPropertyMacro_); + NullTestPlugin plugin; + shell.runOneTestInCurrentProcess(&plugin, spyResult); + delete shell.testFunction_; + shell.testFunction_ = NULLPTR; + + LONGS_EQUAL(1, spy.callCount_); + STRCMP_EQUAL("propname", spy.lastPropertyName_.asCharString()); + STRCMP_EQUAL("propval", spy.lastPropertyValue_.asCharString()); +} + +TEST(TestHarness_c, testPropertyCMacroRoutesToOutput) +{ + PropertySpyOutput spy; + TestResult spyResult(spy); + ExecFunctionTestShell shell; + shell.testFunction_ = new ExecFunctionWithoutParameters(cpputest_test_property_c_caller); + NullTestPlugin plugin; + shell.runOneTestInCurrentProcess(&plugin, spyResult); + delete shell.testFunction_; + shell.testFunction_ = NULLPTR; + + LONGS_EQUAL(1, spy.callCount_); + STRCMP_EQUAL("ckey", spy.lastPropertyName_.asCharString()); + STRCMP_EQUAL("cval", spy.lastPropertyValue_.asCharString()); +} diff --git a/tests/CppUTest/TestHarness_cTestCFile.c b/tests/CppUTest/TestHarness_cTestCFile.c index bfc7d767f..7c59e9a1c 100644 --- a/tests/CppUTest/TestHarness_cTestCFile.c +++ b/tests/CppUTest/TestHarness_cTestCFile.c @@ -9,6 +9,13 @@ void functionWithUnusedParameter(void* PUNUSED(unlessParamater)) } +extern void cpputest_test_property_c_caller(void); + +void cpputest_test_property_c_caller(void) +{ + TEST_PROPERTY_C("ckey", "cval"); +} + /* Declared in the cpp file */ extern int setup_teardown_was_called_in_test_group_in_C; extern int test_was_called_in_test_group_in_C; diff --git a/tests/CppUTest/TestOutputTest.cpp b/tests/CppUTest/TestOutputTest.cpp index bde55df4f..f4e7f26e0 100644 --- a/tests/CppUTest/TestOutputTest.cpp +++ b/tests/CppUTest/TestOutputTest.cpp @@ -282,6 +282,12 @@ TEST(TestOutput, printTestsEndedWithNoTestsRunOrIgnored) mock->getOutput().asCharString()); } +TEST(TestOutput, printTestPropertyIsNoOp) +{ + printer->printTestProperty("key", "value"); + STRCMP_EQUAL("", mock->getOutput().asCharString()); +} + class CompositeTestOutputTestStringBufferTestOutput : public StringBufferTestOutput { public: @@ -478,3 +484,10 @@ TEST(CompositeTestOutput, printVeryVerbose) STRCMP_EQUAL("very-verbose", output1->getOutput().asCharString()); STRCMP_EQUAL("very-verbose", output2->getOutput().asCharString()); } + +TEST(CompositeTestOutput, printTestProperty) +{ + compositeOutput.printTestProperty("key", "value"); + STRCMP_EQUAL("", output1->getOutput().asCharString()); + STRCMP_EQUAL("", output2->getOutput().asCharString()); +}