diff --git a/include/gtest/gtest.h b/include/gtest/gtest.h index cd01c7ba..432ee2d6 100644 --- a/include/gtest/gtest.h +++ b/include/gtest/gtest.h @@ -1152,6 +1152,10 @@ class GTEST_API_ UnitTest { // Gets the number of tests that should run. int test_to_run_count() const; + // Gets the time of the test program start, in ms from the start of the + // UNIX epoch. + TimeInMillis start_timestamp() const; + // Gets the elapsed time, in milliseconds. TimeInMillis elapsed_time() const; diff --git a/src/gtest-internal-inl.h b/src/gtest-internal-inl.h index 65a2101a..8a85724b 100644 --- a/src/gtest-internal-inl.h +++ b/src/gtest-internal-inl.h @@ -112,6 +112,12 @@ GTEST_API_ bool ShouldUseColor(bool stdout_is_tty); // Formats the given time in milliseconds as seconds. GTEST_API_ std::string FormatTimeInMillisAsSeconds(TimeInMillis ms); +// Converts the given time in milliseconds to a date string in the ISO 8601 +// format, without the timezone information. N.B.: due to the use the +// non-reentrant localtime() function, this function is not thread safe. Do +// not use it in any code that can be called from multiple threads. +GTEST_API_ std::string FormatEpochTimeInMillisAsIso8601(TimeInMillis ms); + // Parses a string for an Int32 flag, in the form of "--flag=value". // // On success, stores the value of the flag in *value, and returns @@ -548,6 +554,10 @@ class GTEST_API_ UnitTestImpl { // Gets the number of tests that should run. int test_to_run_count() const; + // Gets the time of the test program start, in ms from the start of the + // UNIX epoch. + TimeInMillis start_timestamp() const { return start_timestamp_; } + // Gets the elapsed time, in milliseconds. TimeInMillis elapsed_time() const { return elapsed_time_; } @@ -880,6 +890,10 @@ class GTEST_API_ UnitTestImpl { // Our random number generator. internal::Random random_; + // The time of the test program start, in ms from the start of the + // UNIX epoch. + TimeInMillis start_timestamp_; + // How long the test took to run, in milliseconds. TimeInMillis elapsed_time_; diff --git a/src/gtest.cc b/src/gtest.cc index 6f7216f9..7bdd28a6 100644 --- a/src/gtest.cc +++ b/src/gtest.cc @@ -39,6 +39,7 @@ #include #include #include +#include #include #include @@ -3195,6 +3196,32 @@ std::string FormatTimeInMillisAsSeconds(TimeInMillis ms) { return ss.str(); } +// Converts the given epoch time in milliseconds to a date string in the ISO +// 8601 format, without the timezone information. +std::string FormatEpochTimeInMillisAsIso8601(TimeInMillis ms) { + // Using non-reentrant version as localtime_r is not portable. + time_t seconds = static_cast(ms / 1000); +#ifdef _MSC_VER +# pragma warning(push) // Saves the current warning state. +# pragma warning(disable:4996) // Temporarily disables warning 4996 + // (function or variable may be unsafe). + const struct tm* const time_struct = localtime(&seconds); // NOLINT +# pragma warning(pop) // Restores the warning state again. +#else + const struct tm* const time_struct = localtime(&seconds); // NOLINT +#endif + if (time_struct == NULL) + return ""; // Invalid ms value + + return String::Format("%d-%02d-%02dT%02d:%02d:%02d", // YYYY-MM-DDThh:mm:ss + time_struct->tm_year + 1900, + time_struct->tm_mon + 1, + time_struct->tm_mday, + time_struct->tm_hour, + time_struct->tm_min, + time_struct->tm_sec); +} + // Streams an XML CDATA section, escaping invalid CDATA sequences as needed. void XmlUnitTestResultPrinter::OutputXmlCDataSection(::std::ostream* stream, const char* data) { @@ -3291,10 +3318,11 @@ void XmlUnitTestResultPrinter::PrintXmlUnitTest(FILE* out, fprintf(out, "\n"); fprintf(out, "total_test_count(); } // Gets the number of tests that should run. int UnitTest::test_to_run_count() const { return impl()->test_to_run_count(); } +// Gets the time of the test program start, in ms from the start of the +// UNIX epoch. +internal::TimeInMillis UnitTest::start_timestamp() const { + return impl()->start_timestamp(); +} + // Gets the elapsed time, in milliseconds. internal::TimeInMillis UnitTest::elapsed_time() const { return impl()->elapsed_time(); @@ -3961,6 +3995,7 @@ UnitTestImpl::UnitTestImpl(UnitTest* parent) post_flag_parse_init_performed_(false), random_seed_(0), // Will be overridden by the flag before first use. random_(0), // Will be reseeded before first use. + start_timestamp_(0), elapsed_time_(0), #if GTEST_HAS_DEATH_TEST internal_run_death_test_flag_(NULL), @@ -4192,6 +4227,7 @@ bool UnitTestImpl::RunAllTests() { TestEventListener* repeater = listeners()->repeater(); + start_timestamp_ = GetTimeInMillis(); repeater->OnTestProgramStart(*parent_); // How many times to repeat the tests? We don't want to repeat them diff --git a/test/gtest_unittest.cc b/test/gtest_unittest.cc index 23d6860e..0d624fd7 100644 --- a/test/gtest_unittest.cc +++ b/test/gtest_unittest.cc @@ -71,6 +71,7 @@ TEST(CommandLineFlagsTest, CanBeAccessedInCodeOnceGTestHIsIncluded) { #include // For INT_MAX. #include +#include #include #include @@ -141,6 +142,7 @@ using testing::TestPartResult; using testing::TestPartResultArray; using testing::TestProperty; using testing::TestResult; +using testing::TimeInMillis; using testing::UnitTest; using testing::kMaxStackTraceDepth; using testing::internal::AddReference; @@ -156,6 +158,7 @@ using testing::internal::CountIf; using testing::internal::EqFailure; using testing::internal::FloatingPoint; using testing::internal::ForEach; +using testing::internal::FormatEpochTimeInMillisAsIso8601; using testing::internal::FormatTimeInMillisAsSeconds; using testing::internal::GTestFlagSaver; using testing::internal::GetCurrentOsStackTraceExceptTop; @@ -163,6 +166,7 @@ using testing::internal::GetElementOr; using testing::internal::GetNextRandomSeed; using testing::internal::GetRandomSeedFromFlag; using testing::internal::GetTestTypeId; +using testing::internal::GetTimeInMillis; using testing::internal::GetTypeId; using testing::internal::GetUnitTestImpl; using testing::internal::ImplicitlyConvertible; @@ -308,6 +312,103 @@ TEST(FormatTimeInMillisAsSecondsTest, FormatsNegativeNumber) { EXPECT_EQ("-3", FormatTimeInMillisAsSeconds(-3000)); } +// Tests FormatEpochTimeInMillisAsIso8601(). The correctness of conversion +// for particular dates below was verified in Python using +// datetime.datetime.fromutctimestamp(/1000). + +// FormatEpochTimeInMillisAsIso8601 depends on the current timezone, so we +// have to set up a particular timezone to obtain predictable results. +class FormatEpochTimeInMillisAsIso8601Test : public Test { + public: + // On Cygwin, GCC doesn't allow unqualified integer literals to exceed + // 32 bits, even when 64-bit integer types are available. We have to + // force the constants to have a 64-bit type here. + static const TimeInMillis kMillisPerSec = 1000; + + private: + virtual void SetUp() { + saved_tz_ = NULL; +#if _MSC_VER +# pragma warning(push) // Saves the current warning state. +# pragma warning(disable:4996) // Temporarily disables warning 4996 + // (function or variable may be unsafe + // for getenv, function is deprecated for + // strdup). + if (getenv("TZ")) + saved_tz_ = strdup(getenv("TZ")); +# pragma warning(pop) // Restores the warning state again. +#else + if (getenv("TZ")) + saved_tz_ = strdup(getenv("TZ")); +#endif + + // Set up the time zone for FormatEpochTimeInMillisAsIso8601 to use. We + // cannot use the local time zone because the function's output depends + // on the time zone. + SetTimeZone("UTC+00"); + } + + virtual void TearDown() { + SetTimeZone(saved_tz_); + free(const_cast(saved_tz_)); + saved_tz_ = NULL; + } + + static void SetTimeZone(const char* time_zone) { + // tzset() distinguishes between the TZ variable being present and empty + // and not being present, so we have to consider the case of time_zone + // being NULL. +#if _MSC_VER + // ...Unless it's MSVC, whose standard library's _putenv doesn't + // distinguish between an empty and a missing variable. + const std::string env_var = + std::string("TZ=") + (time_zone ? time_zone : ""); + _putenv(env_var.c_str()); +# pragma warning(push) // Saves the current warning state. +# pragma warning(disable:4996) // Temporarily disables warning 4996 + // (function is deprecated). + tzset(); +# pragma warning(pop) // Restores the warning state again. +#else + if (time_zone) { + setenv(("TZ"), time_zone, 1); + } else { + unsetenv("TZ"); + } + tzset(); +#endif + } + + const char* saved_tz_; +}; + +const TimeInMillis FormatEpochTimeInMillisAsIso8601Test::kMillisPerSec; + +TEST_F(FormatEpochTimeInMillisAsIso8601Test, PrintsTwoDigitSegments) { + EXPECT_EQ("2011-10-31T18:52:42", + FormatEpochTimeInMillisAsIso8601(1320087162 * kMillisPerSec)); +} + +TEST_F(FormatEpochTimeInMillisAsIso8601Test, MillisecondsDoNotAffectResult) { + EXPECT_EQ( + "2011-10-31T18:52:42", + FormatEpochTimeInMillisAsIso8601(1320087162 * kMillisPerSec + 234)); +} + +TEST_F(FormatEpochTimeInMillisAsIso8601Test, PrintsLeadingZeroes) { + EXPECT_EQ("2011-09-03T05:07:02", + FormatEpochTimeInMillisAsIso8601(1315026422 * kMillisPerSec)); +} + +TEST_F(FormatEpochTimeInMillisAsIso8601Test, Prints24HourTime) { + EXPECT_EQ("2011-09-28T17:08:22", + FormatEpochTimeInMillisAsIso8601(1317229702 * kMillisPerSec)); +} + +TEST_F(FormatEpochTimeInMillisAsIso8601Test, PrintsEpochStart) { + EXPECT_EQ("1970-01-01T00:00:00", FormatEpochTimeInMillisAsIso8601(0)); +} + #if GTEST_CAN_COMPARE_NULL # ifdef __BORLANDC__ @@ -2130,6 +2231,11 @@ TEST(UnitTestTest, CanGetOriginalWorkingDir) { EXPECT_STRNE(UnitTest::GetInstance()->original_working_dir(), ""); } +TEST(UnitTestTest, ReturnsPlausibleTimestamp) { + EXPECT_LT(0, UnitTest::GetInstance()->start_timestamp()); + EXPECT_LE(UnitTest::GetInstance()->start_timestamp(), GetTimeInMillis()); +} + // This group of tests is for predicate assertions (ASSERT_PRED*, etc) // of various arities. They do not attempt to be exhaustive. Rather, // view them as smoke tests that can be easily reviewed and verified. diff --git a/test/gtest_xml_outfiles_test.py b/test/gtest_xml_outfiles_test.py index 0fe947f0..524e437e 100755 --- a/test/gtest_xml_outfiles_test.py +++ b/test/gtest_xml_outfiles_test.py @@ -45,7 +45,7 @@ GTEST_OUTPUT_1_TEST = "gtest_xml_outfile1_test_" GTEST_OUTPUT_2_TEST = "gtest_xml_outfile2_test_" EXPECTED_XML_1 = """ - + @@ -53,7 +53,7 @@ EXPECTED_XML_1 = """ """ EXPECTED_XML_2 = """ - + diff --git a/test/gtest_xml_output_unittest.py b/test/gtest_xml_output_unittest.py index 06637e5b..83903002 100755 --- a/test/gtest_xml_output_unittest.py +++ b/test/gtest_xml_output_unittest.py @@ -33,8 +33,10 @@ __author__ = 'eefacm@gmail.com (Sean Mcafee)' +import datetime import errno import os +import re import sys from xml.dom import minidom, Node @@ -55,7 +57,7 @@ else: STACK_TRACE_TEMPLATE = '' EXPECTED_NON_EMPTY_XML = """ - + @@ -128,7 +130,7 @@ Invalid characters in brackets []%(stack)s]]> EXPECTED_EMPTY_XML = """ - + """ GTEST_PROGRAM_PATH = gtest_test_utils.GetTestExecutablePath(GTEST_PROGRAM_NAME) @@ -153,13 +155,39 @@ class GTestXMLOutputUnitTest(gtest_xml_test_utils.GTestXMLTestCase): self._TestXmlOutput(GTEST_PROGRAM_NAME, EXPECTED_NON_EMPTY_XML, 1) def testEmptyXmlOutput(self): - """ + """Verifies XML output for a Google Test binary without actual tests. + Runs a test program that generates an empty XML output, and tests that the XML output is expected. """ self._TestXmlOutput('gtest_no_test_unittest', EXPECTED_EMPTY_XML, 0) + def testTimestampValue(self): + """Checks whether the timestamp attribute in the XML output is valid. + + Runs a test program that generates an empty XML output, and checks if + the timestamp attribute in the testsuites tag is valid. + """ + actual = self._GetXmlOutput('gtest_no_test_unittest', 0) + date_time_str = actual.documentElement.getAttributeNode('timestamp').value + # datetime.strptime() is only available in Python 2.5+ so we have to + # parse the expected datetime manually. + match = re.match(r'(\d+)-(\d\d)-(\d\d)T(\d\d):(\d\d):(\d\d)', date_time_str) + self.assertTrue( + re.match, + 'XML datettime string %s has incorrect format' % date_time_str) + date_time_from_xml = datetime.datetime( + year=int(match.group(1)), month=int(match.group(2)), + day=int(match.group(3)), hour=int(match.group(4)), + minute=int(match.group(5)), second=int(match.group(6))) + + time_delta = abs(datetime.datetime.now() - date_time_from_xml) + # timestamp value should be near the current local time + self.assertTrue(time_delta < datetime.timedelta(seconds=600), + 'time_delta is %s' % time_delta) + actual.unlink() + def testDefaultOutputFile(self): """ Confirms that Google Test produces an XML output file with the expected @@ -198,8 +226,10 @@ class GTestXMLOutputUnitTest(gtest_xml_test_utils.GTestXMLTestCase): '--shut_down_xml'] p = gtest_test_utils.Subprocess(command) if p.terminated_by_signal: - self.assert_(False, - '%s was killed by signal %d' % (gtest_prog_name, p.signal)) + # p.signal is avalable only if p.terminated_by_signal is True. + self.assertFalse( + p.terminated_by_signal, + '%s was killed by signal %d' % (GTEST_PROGRAM_NAME, p.signal)) else: self.assert_(p.exited) self.assertEquals(1, p.exit_code, @@ -209,13 +239,10 @@ class GTestXMLOutputUnitTest(gtest_xml_test_utils.GTestXMLTestCase): self.assert_(not os.path.isfile(xml_path)) - - def _TestXmlOutput(self, gtest_prog_name, expected_xml, expected_exit_code): + def _GetXmlOutput(self, gtest_prog_name, expected_exit_code): """ - Asserts that the XML document generated by running the program - gtest_prog_name matches expected_xml, a string containing another - XML document. Furthermore, the program's exit code must be - expected_exit_code. + Returns the xml output generated by running the program gtest_prog_name. + Furthermore, the program's exit code must be expected_exit_code. """ xml_path = os.path.join(gtest_test_utils.GetTempDir(), gtest_prog_name + 'out.xml') @@ -232,15 +259,24 @@ class GTestXMLOutputUnitTest(gtest_xml_test_utils.GTestXMLTestCase): "'%s' exited with code %s, which doesn't match " 'the expected exit code %s.' % (command, p.exit_code, expected_exit_code)) + actual = minidom.parse(xml_path) + return actual + def _TestXmlOutput(self, gtest_prog_name, expected_xml, expected_exit_code): + """ + Asserts that the XML document generated by running the program + gtest_prog_name matches expected_xml, a string containing another + XML document. Furthermore, the program's exit code must be + expected_exit_code. + """ + + actual = self._GetXmlOutput(gtest_prog_name, expected_exit_code) expected = minidom.parseString(expected_xml) - actual = minidom.parse(xml_path) self.NormalizeXml(actual.documentElement) self.AssertEquivalentNodes(expected.documentElement, actual.documentElement) expected.unlink() - actual .unlink() - + actual.unlink() if __name__ == '__main__': diff --git a/test/gtest_xml_test_utils.py b/test/gtest_xml_test_utils.py index 0f55c164..f94d634b 100755 --- a/test/gtest_xml_test_utils.py +++ b/test/gtest_xml_test_utils.py @@ -39,8 +39,8 @@ from xml.dom import minidom, Node import gtest_test_utils -GTEST_OUTPUT_FLAG = "--gtest_output" -GTEST_DEFAULT_OUTPUT_FILE = "test_detail.xml" +GTEST_OUTPUT_FLAG = '--gtest_output' +GTEST_DEFAULT_OUTPUT_FILE = 'test_detail.xml' class GTestXMLTestCase(gtest_test_utils.TestCase): """ @@ -80,23 +80,23 @@ class GTestXMLTestCase(gtest_test_utils.TestCase): actual_attributes = actual_node .attributes self.assertEquals( expected_attributes.length, actual_attributes.length, - "attribute numbers differ in element " + actual_node.tagName) + 'attribute numbers differ in element ' + actual_node.tagName) for i in range(expected_attributes.length): expected_attr = expected_attributes.item(i) actual_attr = actual_attributes.get(expected_attr.name) self.assert_( actual_attr is not None, - "expected attribute %s not found in element %s" % + 'expected attribute %s not found in element %s' % (expected_attr.name, actual_node.tagName)) self.assertEquals(expected_attr.value, actual_attr.value, - " values of attribute %s in element %s differ" % + ' values of attribute %s in element %s differ' % (expected_attr.name, actual_node.tagName)) expected_children = self._GetChildren(expected_node) actual_children = self._GetChildren(actual_node) self.assertEquals( len(expected_children), len(actual_children), - "number of child elements differ in element " + actual_node.tagName) + 'number of child elements differ in element ' + actual_node.tagName) for child_id, child in expected_children.iteritems(): self.assert_(child_id in actual_children, '<%s> is not in <%s> (in element %s)' % @@ -104,10 +104,10 @@ class GTestXMLTestCase(gtest_test_utils.TestCase): self.AssertEquivalentNodes(child, actual_children[child_id]) identifying_attribute = { - "testsuites": "name", - "testsuite": "name", - "testcase": "name", - "failure": "message", + 'testsuites': 'name', + 'testsuite': 'name', + 'testcase': 'name', + 'failure': 'message', } def _GetChildren(self, element): @@ -127,20 +127,20 @@ class GTestXMLTestCase(gtest_test_utils.TestCase): for child in element.childNodes: if child.nodeType == Node.ELEMENT_NODE: self.assert_(child.tagName in self.identifying_attribute, - "Encountered unknown element <%s>" % child.tagName) + 'Encountered unknown element <%s>' % child.tagName) childID = child.getAttribute(self.identifying_attribute[child.tagName]) self.assert_(childID not in children) children[childID] = child elif child.nodeType in [Node.TEXT_NODE, Node.CDATA_SECTION_NODE]: - if "detail" not in children: + if 'detail' not in children: if (child.nodeType == Node.CDATA_SECTION_NODE or not child.nodeValue.isspace()): - children["detail"] = child.ownerDocument.createCDATASection( + children['detail'] = child.ownerDocument.createCDATASection( child.nodeValue) else: - children["detail"].nodeValue += child.nodeValue + children['detail'].nodeValue += child.nodeValue else: - self.fail("Encountered unexpected node type %d" % child.nodeType) + self.fail('Encountered unexpected node type %d' % child.nodeType) return children def NormalizeXml(self, element): @@ -151,6 +151,8 @@ class GTestXMLTestCase(gtest_test_utils.TestCase): * The "time" attribute of , and elements is replaced with a single asterisk, if it contains only digit characters. + * The "timestamp" attribute of elements is replaced with a + single asterisk, if it contains a valid ISO8601 datetime value. * The "type_param" attribute of elements is replaced with a single asterisk (if it sn non-empty) as it is the type name returned by the compiler and is platform dependent. @@ -160,20 +162,24 @@ class GTestXMLTestCase(gtest_test_utils.TestCase): * The stack traces are removed. """ - if element.tagName in ("testsuites", "testsuite", "testcase"): - time = element.getAttributeNode("time") - time.value = re.sub(r"^\d+(\.\d+)?$", "*", time.value) - type_param = element.getAttributeNode("type_param") + if element.tagName == 'testsuites': + timestamp = element.getAttributeNode('timestamp') + timestamp.value = re.sub(r'^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d$', + '*', timestamp.value) + if element.tagName in ('testsuites', 'testsuite', 'testcase'): + time = element.getAttributeNode('time') + time.value = re.sub(r'^\d+(\.\d+)?$', '*', time.value) + type_param = element.getAttributeNode('type_param') if type_param and type_param.value: - type_param.value = "*" - elif element.tagName == "failure": + type_param.value = '*' + elif element.tagName == 'failure': for child in element.childNodes: if child.nodeType == Node.CDATA_SECTION_NODE: # Removes the source line number. - cdata = re.sub(r"^.*[/\\](.*:)\d+\n", "\\1*\n", child.nodeValue) + cdata = re.sub(r'^.*[/\\](.*:)\d+\n', '\\1*\n', child.nodeValue) # Removes the actual stack trace. - child.nodeValue = re.sub(r"\nStack trace:\n(.|\n)*", - "", cdata) + child.nodeValue = re.sub(r'\nStack trace:\n(.|\n)*', + '', cdata) for child in element.childNodes: if child.nodeType == Node.ELEMENT_NODE: self.NormalizeXml(child)