| #pragma once |
| |
| #ifndef GEODE_INTEGRATION_TEST_FW_DUNIT_H_ |
| #define GEODE_INTEGRATION_TEST_FW_DUNIT_H_ |
| |
| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You under the Apache License, Version 2.0 |
| * (the "License"); you may not use this file except in compliance with |
| * the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| /*** |
| |
| A Dunit framework provides a structure for executing sequential portions of |
| test code in different processes. This framework provides C++ tests with |
| a tools to define tasks and associating them to different processes. It |
| automatically maintains the sequence order that these tasks are to be |
| executing in. Process startup and termination is handled by the framework. |
| |
| Task control: |
| |
| The framework spawns four child processes. Two processes run with GF_DEFAULT |
| set to distributed system 1, and the other two run with GF_DEFAULT set to |
| distributed system 2. The processes correspond with the ids: |
| |
| s1p1 and s1p2 for system 1, s2p1 and s2p2 for system 2. |
| |
| Two macros are used to define blocks of test code, and what process it will |
| run in. |
| |
| DUNIT_TASK(x,y) - beginning of block to run in process x, with uniq id y. |
| END_TASK(y) - ending of block started by previous DUNIT_TASK call. |
| |
| Tasks are executed in the order, from top to bottom, that they are found in |
| the test source file. If any task throws an uncaught exception, the framework |
| will catch the exception and consider it a test failure. If the exception is |
| of type dunit::TestException, then it will be logged. |
| (@TODO: catch and log apache::geode::client::Exception types, and |
| std::exceptions. ) |
| |
| DUNIT_TASK actually begins the definition of a subclass of dunit::Task. And the |
| task Method doTask. The block of code between DUNIT_TASK and END_TASK is |
| compiled as the body of doTask. END_TASK closes the class definition, while |
| declaring an instance to be initialized at module load time. This sets up the |
| schedule of tasks to execute and the process they are to be executed on. |
| |
| Logging and handle errors: |
| |
| The logging and error reporting macros include file and line number information. |
| |
| ASSERT(x,y) - if condition x is false, throw a TestException with message y. |
| FAIL(y) - throw a TestException with message y. |
| LOG(y) - Log message y. |
| |
| Sharing test data between process is done with a NamingContext which is set up |
| for you automatically. Use the globals() function to get a pointer to it. |
| With the naming context is like a map associating char* values with char* |
| keys. Convenience methods are made available that take and return int values. |
| See class NamingContext in this file. |
| |
| The the covers, the framework is implemented in the fw_dunit.cpp and |
| fw_spawn.hpp sources. fw_dunit.cpp is compiled as a seperate object module |
| without dependence on the product. It is in a seperate module that is linked in |
| two each test executable. The seperation is designed to help keep the test |
| framework from being corrupt by the product, and prevent the framework from |
| mucking with the product in unforseen ways. |
| |
| --------------------------------------- |
| An example dunit test file: |
| --------------------------------------- |
| |
| #include <fw_dunit.hpp> |
| |
| DUNIT_TASK(s1p1,setupp1) |
| { |
| // test code... put ten entries... |
| globals()->rebind( "entryCount", 10 ); |
| } |
| END_TASK(setupp1) |
| |
| DUNIT_TASK(s2p1,validate) |
| { |
| // check results from distribution... |
| int expectedEntries = globals()->getIntValue( "entryCount" ); |
| // ... |
| ASSERT( foundEntries == expectedEntries, "wrong entryCount" ); |
| } |
| END_TASK(validate) |
| |
| ---------------------------------------- |
| |
| */ |
| |
| // Default task timeout in seconds |
| #ifndef TASK_TIMEOUT |
| #define TASK_TIMEOUT 1800 |
| #endif |
| |
| #ifdef WINVER |
| #if WINVER == 0x0500 |
| #undef _WINSOCKAPI_ |
| #define NOMINMAX |
| #include <WinSock2.h> |
| #endif |
| #endif |
| |
| #include <string> |
| |
| #include <ace/ACE.h> |
| #include <signal.h> |
| |
| #define ASSERT(x, y) \ |
| do { \ |
| if (!(x)) { \ |
| throw dunit::TestException(y, __LINE__, __FILE__); \ |
| } \ |
| } while(false) |
| #define XASSERT(x) \ |
| do { \ |
| if (!(x)) { \ |
| throw dunit::TestException(#x, __LINE__, __FILE__); \ |
| } \ |
| } while(false) |
| #define FAIL(y) throw dunit::TestException(y, __LINE__, __FILE__) |
| #define LOG(y) dunit::log(y, __LINE__, __FILE__) |
| #define LOGCOORDINATOR(y) dunit::logCoordinator(y, __LINE__, __FILE__) |
| #define SLEEP(x) dunit::sleep(x) |
| |
| #define SYMJOIN(x, y) SYMJOIN2(x, y) |
| #define SYMJOIN2(x, y) x##y |
| |
| #define DCLASSNAME(y) SYMJOIN(SYMJOIN(Task_, y), __LINE__) |
| #define DMAKE_STR(y) #y |
| #define DTASKDESC(x, y) DMAKE_STR(x line : y) |
| |
| #define DCLASSDEF(y) SYMJOIN(Task_, y) |
| #define DVARNAME(y) SYMJOIN(SYMJOIN(a_, y), __LINE__) |
| |
| // Create a test task instance, x is the process tag identifying which process |
| // to run in. y is a unique task identifier used in generating a class name. |
| #define DUNIT_TASK(x, y) \ |
| class DCLASSNAME(y) : virtual public dunit::Task { \ |
| public: \ |
| DCLASSNAME(y)() { init(x); } \ |
| \ |
| public: \ |
| virtual void doTask() { \ |
| static const char* fwtest_Name = DTASKDESC(y, __LINE__); \ |
| try { |
| // Close the class definition produced by DUNIT_TASK macro. |
| // y is a unique identifier used to generate an instance variable name. |
| #define END_TASK(y) \ |
| } \ |
| catch (apache::geode::client::Exception & ex) { \ |
| LOG(ex.getStackTrace().c_str()); \ |
| FAIL(ex.what()); \ |
| } \ |
| catch (std::exception & ex) { \ |
| FAIL(ex.what()); \ |
| } \ |
| catch (dunit::TestException&) { \ |
| throw; \ |
| } \ |
| catch (...) { \ |
| FAIL("Unknown exception type caught."); \ |
| } \ |
| } \ |
| } \ |
| SYMJOIN(a_, __LINE__); |
| #define ENDTASK \ |
| } \ |
| catch (apache::geode::client::Exception & ex) { \ |
| LOG(ex.getStackTrace().c_str()); \ |
| FAIL(ex.what()); \ |
| } \ |
| catch (std::exception & ex) { \ |
| FAIL(ex.what()); \ |
| } \ |
| catch (dunit::TestException&) { \ |
| throw; \ |
| } \ |
| catch (...) { \ |
| FAIL("Unknown exception type caught."); \ |
| } \ |
| } \ |
| } \ |
| SYMJOIN(a_, __LINE__); |
| |
| #define DUNIT_TASK_DEFINITION(x, y) \ |
| class DCLASSDEF(y) : virtual public dunit::Task { \ |
| public: \ |
| DCLASSDEF(y)() { init(x, true); } \ |
| \ |
| public: \ |
| virtual void doTask() { \ |
| static const char* fwtest_Name = DTASKDESC(y, __LINE__); \ |
| try { |
| #define END_TASK_DEFINITION \ |
| } \ |
| catch (apache::geode::client::Exception & ex) { \ |
| LOG(ex.getStackTrace().c_str()); \ |
| FAIL(ex.what()); \ |
| } \ |
| catch (std::exception & ex) { \ |
| FAIL(ex.what()); \ |
| } \ |
| catch (dunit::TestException&) { \ |
| throw; \ |
| } \ |
| catch (...) { \ |
| FAIL("Unknown exception type caught."); \ |
| } \ |
| } \ |
| } \ |
| ; |
| #define CALL_TASK(y); DCLASSDEF(y) * DVARNAME(y) = new DCLASSDEF(y)() |
| |
| #define DUNIT_MAIN \ |
| class DCLASSNAME(Main) { \ |
| public: \ |
| DCLASSNAME(Main)() { \ |
| try { |
| #define END_MAIN \ |
| } \ |
| catch (apache::geode::client::Exception & ex) { \ |
| LOG(ex.getStackTrace().c_str()); \ |
| FAIL(ex.what()); \ |
| } \ |
| catch (std::exception & ex) { \ |
| FAIL(ex.what()); \ |
| } \ |
| catch (dunit::TestException&) { \ |
| throw; \ |
| } \ |
| catch (...) { \ |
| FAIL("Unknown exception type caught."); \ |
| } \ |
| } \ |
| } \ |
| SYMJOIN(a_, __LINE__); |
| |
| // identifiers for the different processes. |
| #define s1p1 1 |
| #define s1p2 2 |
| #define s2p1 3 |
| #define s2p2 4 |
| |
| namespace dunit { |
| |
| void logCoordinator(std::string s, int lineno, const char* filename); |
| void log(std::string s, int lineno, const char* filename); |
| void sleep(int millis); |
| void HostWaitForDebugger(); |
| |
| // on windows this disables the popups. |
| void setupCRTOutput(); |
| |
| /** base class for all tasks... define a virtual doTest method to be called |
| * when task is to be executed. init method sets up the task in the right |
| * process queue for the workers to execute. |
| */ |
| class Task { |
| public: |
| std::string m_taskName; |
| int m_id; |
| bool m_isHeapAllocated; |
| |
| Task() {} |
| virtual ~Task() { } |
| |
| /** register task with worker. */ |
| void init(int sId); |
| |
| void init(int sId, bool isHeapAllocated); |
| |
| // defined by block of code in DUNIT_TASK macro usage. |
| virtual void doTask() = 0; |
| |
| void setTimeout(int seconds = 0); |
| |
| std::string typeName(); |
| }; |
| |
| /** Shared naming context for storing ints and strings between processes. |
| * To acquire the naming context, use the globals() function which |
| * returns a pointer for the naming context. |
| */ |
| class NamingContext { |
| public: |
| /** |
| * Share a string value, return -1 if there is a failure to store value, |
| * otherwise returns 0. |
| */ |
| virtual int rebind(const char* key, const char* value) = 0; |
| |
| /** |
| * Share an int value, return -1 if there is a failure to store value, |
| * otherwise returns 0. |
| */ |
| virtual int rebind(const char* key, int value) = 0; |
| |
| /** |
| * retreive a value by key, storing the result in the users buf. If the key |
| * is not found, the buf will contain the empty string "". Make sure the |
| * buffer is big enough to hold whatever has have bound. |
| */ |
| virtual void getValue(const char* key, char* buf, size_t sizeOfBuf) = 0; |
| |
| /** |
| * return the value by key, as an int using the string to int conversion |
| * rules of atoi. |
| */ |
| virtual int getIntValue(const char* key) = 0; |
| |
| /** dump the entire context in LOG messages. */ |
| virtual void dump() = 0; |
| |
| virtual ~NamingContext() noexcept = default; |
| }; |
| |
| extern "C" { |
| |
| NamingContext* globals(); |
| } |
| |
| /** |
| * Exception type to use when test framework has trouble or if ASSERT and FAIL |
| * are triggered. Use the macros ASSERT(cond,message) and FAIL(message) to |
| * throw, use TestException.print() to dump the issue to stderr. If not caught |
| * by your test code, the framework will catch it and assume the test block |
| * failed. |
| */ |
| class TestException { |
| public: |
| TestException(const char* msg, int lineno, const char* filename) |
| : m_message(const_cast<char*>(msg)), |
| m_lineno(lineno), |
| m_filename(const_cast<char*>(filename)) {} |
| |
| void print() { |
| fprintf(stdout, "#### TestException: %s in %s at line %d\n", |
| m_message.c_str(), m_filename, m_lineno); |
| fflush(stdout); |
| } |
| std::string m_message; |
| int m_lineno; |
| char* m_filename; |
| }; |
| |
| int dmain(int argc, char* argv[]); |
| |
| } // end namespace dunit. |
| |
| #ifndef __DUNIT_NO_MAIN__ |
| |
| int ACE_TMAIN(int argc, ACE_TCHAR* argv[]) { return dunit::dmain(argc, argv); } |
| |
| #endif // __DUNIT_NO_MAIN__ |
| |
| #include "fw_perf.hpp" |
| |
| namespace test {} // namespace test |
| #endif // GEODE_INTEGRATION_TEST_FW_DUNIT_H_ |