diff --git a/CMakeLists.txt b/CMakeLists.txt index 3e084c1cf..bf1648517 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -132,9 +132,10 @@ set(MIP_LIBRARIES ${MIP_LIBRARIES_TMP} CACHE STRING "List of all requested MIP l # TESTING # -include(CTest) if(MICROSTRAIN_BUILD_TESTS) + include(CTest) + enable_testing() add_subdirectory("test") endif() diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index 1a88e06d0..f7ac3878c 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -58,11 +58,11 @@ macro(add_mip_example name sources) endmacro() -add_mip_example(MipPacketC "${EXAMPLE_DIR}/mip_packet_example.c") +add_mip_example(MipPacketExampleC "${EXAMPLE_DIR}/mip_packet_example.c") if(MICROSTRAIN_ENABLE_CPP) - add_mip_example(MipPacket "${EXAMPLE_DIR}/mip_packet_example.cpp") + add_mip_example(MipPacketExample "${EXAMPLE_DIR}/mip_packet_example.cpp") # C++ examples that need either serial or TCP support if(MICROSTRAIN_ENABLE_SERIAL OR MICROSTRAIN_ENABLE_TCP) diff --git a/src/cpp/mip/mip_packet.hpp b/src/cpp/mip/mip_packet.hpp index d65142788..cc3f9c4b4 100644 --- a/src/cpp/mip/mip_packet.hpp +++ b/src/cpp/mip/mip_packet.hpp @@ -90,6 +90,24 @@ class PacketView : public C::mip_packet_view //int finishLastField(uint8_t* payloadPtr, uint8_t newPayloadLength) { return C::mip_packet_realloc_last_field(this, payloadPtr, newPayloadLength); } ///<@copydoc mip::C::mip_packet_realloc_last_field //int cancelLastField(uint8_t* payloadPtr) { return C::mip_packet_cancel_last_field(this, payloadPtr); } ///<@copydoc mip::C::mip_packet_cancel_last_field + void finalize() { C::mip_packet_finalize(this); } ///<@copydoc mip::C::mip_packet_finalize + + void reset(uint8_t descSet) { C::mip_packet_reset(this, descSet); } ///<@copydoc mip::C::mip_packet_reset + void reset() { reset(descriptorSet()); } ///<@brief Resets the packet using the same descriptor set. + + // + // C++ additions + // + + ///@brief Gets a span over the whole packet. + /// + microstrain::Span totalSpan() const { return {pointer(), totalLength()}; } + + ///@brief Gets a span over just the payload. + /// + microstrain::Span payloadSpan() const { return {payload(), payloadLength()}; } + + class AllocatedField : public Serializer { public: @@ -112,17 +130,14 @@ class PacketView : public C::mip_packet_view return ok; } + void cancel() { if(basePointer()) C::mip_packet_cancel_last_field(&m_packet, basePointer()); } + private: PacketView& m_packet; }; AllocatedField createField(uint8_t fieldDescriptor) { uint8_t* ptr; size_t max_size = std::max(0, C::mip_packet_create_field(this, fieldDescriptor, 0, &ptr)); return {*this, ptr, max_size}; } - void finalize() { C::mip_packet_finalize(this); } ///<@copydoc mip::C::mip_packet_finalize - - void reset(uint8_t descSet) { C::mip_packet_reset(this, descSet); } ///<@copydoc mip::C::mip_packet_reset - void reset() { reset(descriptorSet()); } ///<@brief Resets the packet using the same descriptor set. - uint8_t operator[](unsigned int index) const { return pointer()[index]; } // @@ -243,6 +258,28 @@ class PacketView : public C::mip_packet_view FieldView mField; }; + ///@brief Copies this packet to an external buffer. + /// + /// This packet must be sane (see isSane()). Undefined behavior otherwise due to lookup of totalLength(). + /// + ///@param buffer Data is copied into this location. + ///@param maxLength Maximum number of bytes to copy. + /// + ///@returns true if successful. + ///@returns false if maxLength is too short. + /// + bool copyPacketTo(uint8_t* buffer, size_t maxLength) { assert(isSane()); size_t copyLength = this->totalLength(); if(copyLength > maxLength) return false; std::memcpy(buffer, pointer(), copyLength); return true; } + + ///@brief Copies this packet to an external buffer (span version). + /// + /// This packet must be sane (see isSane()). Undefined behavior otherwise due to lookup of totalLength(). + /// + ///@param buffer Data is copied to this buffer. + /// + ///@returns true if successful. + ///@returns false if maxLength is too short. + /// + bool copyPacketTo(microstrain::Span buffer) { return copyPacketTo(buffer.data(), buffer.size()); } }; @@ -262,9 +299,13 @@ class SizedPacketBuf : public PacketView explicit SizedPacketBuf(const uint8_t* data, size_t length) : PacketView(mData, sizeof(mData)) { copyFrom(data, length); } explicit SizedPacketBuf(const PacketView& packet) : PacketView(mData, sizeof(mData)) { copyFrom(packet); } + ///@brief Construct from a span. + explicit SizedPacketBuf(microstrain::Span data) : SizedPacketBuf(data.data(), data.size()) {} + ///@brief Copy constructor SizedPacketBuf(const SizedPacketBuf& other) : PacketView(mData, sizeof(mData)) { copyFrom(other); } + ///@brief Copy constructor (required to insert packets into std::vector in some cases). template explicit SizedPacketBuf(const SizedPacketBuf& other) : PacketView(mData, sizeof(mData)) { copyFrom(other); }; @@ -306,6 +347,10 @@ class SizedPacketBuf : public PacketView /// This is technically the same as PacketRef::pointer but is writable. uint8_t* buffer() { return mData; } + ///@brief Returns a Span covering the entire buffer. + /// + microstrain::Span bufferSpan() { return microstrain::Span{buffer(), BufferSize}; } + ///@brief Copies the data from the pointer to this buffer. The data is not inspected. /// ///@param data Pointer to the start of the packet. @@ -319,18 +364,6 @@ class SizedPacketBuf : public PacketView /// void copyFrom(const PacketView& packet) { assert(packet.isSane()); copyFrom(packet.pointer(), packet.totalLength()); } - ///@brief Copies this packet to an external buffer. - /// - /// This packet must be sane (see isSane()). Undefined behavior otherwise due to lookup of totalLength(). - /// - ///@param buffer Data is copied into this location. - ///@param maxLength Maximum number of bytes to copy. - /// - ///@returns true if successful. - ///@returns false if maxLength is too short. - /// - bool copyTo(uint8_t* buffer, size_t maxLength) { assert(isSane()); size_t copyLength = this->totalLength(); if(copyLength > maxLength) return false; std::memcpy(buffer, mData, copyLength); return true; } - private: uint8_t mData[BufferSize]; }; diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index aced30acf..6169a59a2 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -1,5 +1,7 @@ set(TEST_DIR "${CMAKE_CURRENT_LIST_DIR}") +add_library(MicrostrainTest "${TEST_DIR}/test.h" "${TEST_DIR}/test.c") + # # MIP # @@ -7,15 +9,24 @@ set(TEST_DIR "${CMAKE_CURRENT_LIST_DIR}") macro(add_mip_test name sources command) add_executable(${name} ${sources}) - target_include_directories(${name} PRIVATE ${MICROSTRAIN_SRC_DIR}) - target_link_libraries(${name} ${MIP_LIBRARY}) + target_include_directories(${name} PRIVATE ${CMAKE_CURRENT_LIST_DIR} ${MICROSTRAIN_SRC_DIR}) + target_link_libraries(${name} MicrostrainTest ${MIP_LIBRARY}) add_test(${name} ${command} ${ARGN}) endmacro() -add_mip_test(TestMipPacketBuilding "${TEST_DIR}/mip/test_mip_packet_builder.c" TestMipPacketBuilding) -add_mip_test(TestMipParsing "${TEST_DIR}/mip/test_mip_parser.c" TestMipParsing "${TEST_DIR}/data/mip_data.bin") -add_mip_test(TestMipRandom "${TEST_DIR}/mip/test_mip_random.c" TestMipRandom) -add_mip_test(TestMipFields "${TEST_DIR}/mip/test_mip_fields.c" TestMipFields) -add_mip_test(TestMipCpp "${TEST_DIR}/mip/test_mip.cpp" TestMipCpp) -add_mip_test(TestMipPerf "${TEST_DIR}/mip/mip_parser_performance.cpp" TestMipPerf) +add_mip_test(TestMipPacketBuilding "${TEST_DIR}/c/mip/test_mip_packet_builder.c" TestMipPacketBuilding) +add_mip_test(TestMipParsing "${TEST_DIR}/c/mip/test_mip_parser.c" TestMipParsing "${TEST_DIR}/data/mip_data.bin") +add_mip_test(TestMipRandom "${TEST_DIR}/c/mip/test_mip_random.c" TestMipRandom) +add_mip_test(TestMipFields "${TEST_DIR}/c/mip/test_mip_fields.c" TestMipFields) +add_mip_test(TestMipCpp "${TEST_DIR}/cpp/mip/test_mip.cpp" TestMipCpp) +add_mip_test(TestMipPerf "${TEST_DIR}/cpp/mip/mip_parser_performance.cpp" TestMipPerf) +add_mip_test(TestMipPacket "${TEST_DIR}/cpp/mip/test_packet_interface.cpp" TestMipPacket) + +#if(MICROSTRAIN_BUILD_EXAMPLES) +# set(OUT_FILE "${CMAKE_CURRENT_BINARY_DIR}/MipPacketExampleOutput.txt") +# add_test( +# NAME TestMipPacketExample +# COMMAND diff -u "${CMAKE_CURRENT_LIST_DIR}/data/packet_example_cpp_check.txt" <(examples/MipPacketExample) +# ) +#endif() diff --git a/test/mip/test_mip_fields.c b/test/c/mip/test_mip_fields.c similarity index 100% rename from test/mip/test_mip_fields.c rename to test/c/mip/test_mip_fields.c diff --git a/test/mip/test_mip_packet_builder.c b/test/c/mip/test_mip_packet_builder.c similarity index 85% rename from test/mip/test_mip_packet_builder.c rename to test/c/mip/test_mip_packet_builder.c index 9eeb52c79..c3b16cdf4 100644 --- a/test/mip/test_mip_packet_builder.c +++ b/test/c/mip/test_mip_packet_builder.c @@ -1,7 +1,10 @@ +#include "test.h" + #include #include + #include #include #include @@ -9,56 +12,6 @@ #define EXTRA 1 uint8_t buffer[MIP_PACKET_LENGTH_MAX+EXTRA]; -int num_errors = 0; - -void print_buffer(FILE* file) -{ - for(unsigned int i=0; i +#include + +#include +#include +#include + +// A ping command +const uint8_t PING[] = { 0x75, 0x65, 0x01, 0x02, 0x02, 0x01, 0xE0, 0xC6 }; +// Sample data field +const mip::data_sensor::ScaledAccel ACCEL_FIELD = { {1.f, 2.f, -3.f} }; +const uint8_t ACCEL_PKT[] = { 0x75, 0x65, 0x80, 0x0e, 0x0e, 0x04, 0x3f, 0x80, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0xC0, 0x40, 0x00, 0x00, 0x79, 0xed }; +//const mip::data_shared::ReferenceTimestamp REFTIME = { 1'234'567'890 }; + +void print_packet(const mip::PacketView& packet) +{ + print_buffer(stderr, packet.pointer(), packet.totalLength()); +} + +bool checkPacketView(const mip::PacketView& packet, microstrain::Span compare, const char* method) +{ + bool ok = true; + + ok &= check(packet.isSane(), "Insane packet from %s", method); + ok &= check(packet.descriptorSet() == compare[mip::C::MIP_INDEX_DESCSET], "Wrong descriptor set from %s", method); + ok &= check(packet.payloadLength() == compare[mip::C::MIP_INDEX_LENGTH], "Wrong payload length from %s", method); + ok &= check(packet.totalLength() == packet.payloadLength()+mip::C::MIP_PACKET_LENGTH_MIN, "Wrong total length from %s", method); + ok &= check_equal(packet.payload(), packet.pointer() + mip::C::MIP_INDEX_PAYLOAD, "Wrong payload pointer from %s", method); + ok &= check(packet.pointer() != nullptr, "PacketRef shouldn't have NULL pointer from %s", method); + ok &= check_equal(packet.totalSpan().data(), packet.pointer(), "totalSpan().data() should match pointer()"); + ok &= check_equal(packet.totalSpan().size(), packet.totalLength(), "totalSpan().size() should match totalLength()"); + ok &= check_equal(packet.payloadSpan().data(), packet.payload(), "payloadSpan().data() should match payload()"); + ok &= check_equal(packet.payloadSpan().size(), packet.payloadLength(), "payloadSpan().size() should match payloadLength()"); + ok &= check_equal(packet.remainingSpace(), packet.bufferSize()-packet.totalLength(), "remainingSpace() is wrong"); + + if(std::memcmp(packet.pointer(), compare.data(), compare.size()) != 0) + { + fprintf(stderr, "Data mismatch from %s:\n", method); + print_packet(packet); + print_buffer(stderr, compare.data(), compare.size()); + ok = false; + } + + return ok; +} + +template +bool checkPacketBuf(mip::SizedPacketBuf& packet, microstrain::Span compare, const char* method) +{ + bool ok = checkPacketView(packet, compare, method); + + ok &= check(packet.buffer() != nullptr, "NULL buffer from %s", method); + ok &= check_equal(packet.bufferSize(), Size, "bufferSize() should match templated size from %s", method); + ok &= check_equal(packet.pointer(), packet.buffer(), "Packet buffer/pointer mismatch from %s", method); + ok &= check(packet.pointer() != compare.data(), "PacketBuf shouldn't point to original data buffer from %s", method); + ok &= check_equal(packet.bufferSpan().data(), packet.buffer(), "BufferSpan().data() doesn't match .buffer()"); + ok &= check(packet.bufferSpan().size() == Size, "BufferSpan().data() doesn't match .buffer()"); + + return ok; +} + +bool checkPingPacketView(const mip::PacketView& packet, const char* method) +{ + return checkPacketView(packet, microstrain::Span(PING), method); +} + +template +bool checkPingPacketBuf(mip::SizedPacketBuf& packet, const char* method) +{ + return checkPacketBuf(packet, microstrain::Span(PING), method); +} + + +void testPacketView() +{ + // + // Construction + // + + uint8_t buffer[mip::PACKET_LENGTH_MAX]; + + mip::PacketView packet1(buffer, sizeof(buffer), mip::commands_base::DESCRIPTOR_SET); + checkPacketView(packet1, buffer, "PacketView initializing constructor"); + check(packet1.isEmpty(), "Packet should be empty after packetView initializing constructor"); + check_equal(packet1.bufferSize(), sizeof(buffer), "Wrong buffer size from PacketView initializing constructor"); + check_equal(packet1.remainingSpace(), mip::C::MIP_PACKET_PAYLOAD_LENGTH_MAX, "remainingSpace wrong after PacketView initializing constructor"); + //bool ok = packet1.addField(mip::commands_base::Ping::FIELD_DESCRIPTOR, nullptr, 0); + //check(ok, "Failed to add field to packet1"); + //check_equal(packet1.remainingSpace(), mip::C::MIP_PACKET_PAYLOAD_LENGTH_MAX-2, "remainingSpace wrong after adding Ping command to packet1"); + //check_equal(packet1.bufferSize(), sizeof(buffer), "Wrong buffer size after adding Ping command to packet1"); + packet1.finalize(); + if(!check(packet1.isValid(), "Packet1 not valid after finalization")) + print_packet(packet1); + + mip::PacketView packet2(PING, sizeof(PING)); + checkPingPacketView(packet2, "PacketView existing constructor"); + + mip::C::mip_packet_view packet3c; + mip::C::mip_packet_from_buffer(&packet3c, PING, sizeof(PING)); + mip::PacketView packet3(packet3c); + checkPingPacketView(packet3, "PacketView C constructor"); + + mip::PacketView packet4(microstrain::Span(buffer), mip::commands_base::DESCRIPTOR_SET); + checkPacketView(packet4, buffer, "PacketView initializing constructor (span version)"); + + mip::PacketView packet5(microstrain::Span(PING, sizeof(PING))); + checkPingPacketView(packet5, "PacketView existing constructor (span version)"); + + +} + +void testPacketBuf() +{ + // + // Construction + // + + // Default constructor + mip::PacketBuf packet1; + check( !mip::isValidDescriptorSet(packet1.descriptorSet()), "PacketBuf should default-construct to invalid descriptor set"); + + // Specify descriptor set + mip::PacketBuf packet2(mip::data_sensor::DESCRIPTOR_SET); + check( packet2.descriptorSet() == mip::data_sensor::DESCRIPTOR_SET, "PacketBuf constructor with descriptor set doesn't work"); + + // Construct from raw buffer, span, or existing view. + mip::PacketBuf packet3(PING, sizeof(PING)); + mip::PacketBuf packet4(microstrain::Span(PING, sizeof(PING))); + mip::PacketBuf packet5(mip::PacketView(const_cast(PING), sizeof(PING))); + + checkPingPacketBuf(packet3, "PacketBuf raw buffer constructor"); + checkPingPacketBuf(packet4, "PacketBuf span constructor"); + checkPingPacketBuf(packet5, "PacketBuf PacketView constructor"); + + // Regular copy constructor + mip::PacketBuf packet6(packet5); + checkPingPacketBuf(packet6, "PacketBuf copy constructor"); + check(packet6.buffer() != packet5.buffer(), "Packet6 shouldn't point to packet5's data buffer from copy constructor"); + + // Construct from SizedPacketBuf of differing size + mip::SizedPacketBuf<8> packet7(packet5); + checkPingPacketView(packet7, "SizedPacketBuf<8> copy constructor"); + + // Construction from PacketView + mip::PacketBuf packet8(packet5.ref()); + checkPingPacketBuf(packet8, "PacketBuf copy constructor (PacketView)"); + check(packet8.buffer() != packet5.buffer(), "Packet6 shouldn't point to packet5's data buffer from copy constructor"); + + // Assignment + mip::PacketBuf packet9; + packet9 = packet5; + checkPingPacketBuf(packet9, "PacketBuf operator="); + check(packet9.buffer() != packet5.buffer(), "Packet9 shouldn't point to packet5's data buffer from operator="); + + // Create from field + mip::PacketBuf packet10(ACCEL_FIELD); + checkPacketBuf(packet10, ACCEL_PKT, "PacketBuf field constructor"); + + // .ref() already tested + // .buffer() already tested + // .bufferSpan() already tested + // .copyFrom tested by constructors + + // Test copying to destination + uint8_t tmp[sizeof(PING)]; + packet5.copyPacketTo(tmp); + check(std::memcmp(PING, tmp, sizeof(PING))==0, "Temporary buffer doesn't match after calling packet.copyPacketTo"); + + std::memset(tmp, 0x00, sizeof(tmp)); + packet5.copyPacketTo(microstrain::Span(tmp)); + check(std::memcmp(PING, tmp, sizeof(PING))==0, "Temporary buffer doesn't match after calling packet.copyPacketTo (span version)"); + +} + + +int main() +{ + testPacketView(); + testPacketBuf(); + + return num_errors; +} diff --git a/test/data/packet_example_cpp_check.txt b/test/data/packet_example_cpp_check.txt new file mode 100644 index 000000000..d64ecc668 --- /dev/null +++ b/test/data/packet_example_cpp_check.txt @@ -0,0 +1,48 @@ + +Create packet from scratch +Empty, un-finalized packet: desc_set=0x01 tot_len=6 pay_len=0 chk=invalid [756501000000] +Empty packet with checksum: desc_set=0x01 tot_len=6 pay_len=0 chk=valid [75650100DB05] +Unfinished packet with 2 fields: desc_set=0x01 tot_len=16 pay_len=10 chk=invalid [7565010A0201080901010001C2000000] + (01,01): payload=[] + (01,09): payload=[01010001C200] +Unfinished packet with 3 fields: desc_set=0x01 tot_len=24 pay_len=18 chk=invalid [756501120201080901010001C200080901010001C2000000] + (01,01): payload=[] + (01,09): payload=[01010001C200] + (01,09): payload=[01010001C200] +Finished packet with 4 fields: desc_set=0x01 tot_len=32 pay_len=26 chk=valid [7565011A0201080901010001C200080901010001C200080901010001C2007A91] + (01,01): payload=[] + (01,09): payload=[01010001C200] + (01,09): payload=[01010001C200] + (01,09): payload=[01010001C200] +Empty packet after reset: desc_set=0x0C tot_len=6 pay_len=0 chk=invalid [75650C000201] +3DM Message Format command: desc_set=0x0C tot_len=20 pay_len=14 chk=valid [75650C0E0E0F018003D5000A04000A05000A91CE] + (0C,0F): payload=[018003D5000A04000A05000A] +3DM Poll Data command: desc_set=0x0C tot_len=14 pay_len=8 chk=valid [75650C08080D008003D504056446] + (0C,0D): payload=[008003D50405] + +Create packet from buffer +Packet from buffer: desc_set=0x80 tot_len=82 pay_len=76 chk=valid [7565804C0AD5000000055EE67CC00AD6000000014E434A000E043D9EE88D387FDB00BF7AAF030E05BB0C1E30BB572E68BBAA24AE0E07BC8AAC80BC72C50EBCC4E2C10E083EEE3D9FBD66DADDC0AFDEF59196] + (80,D5): payload=[000000055EE67CC0] + (80,D6): payload=[000000014E434A00] + (80,04): payload=[3D9EE88D387FDB00BF7AAF03] + (80,05): payload=[BB0C1E30BB572E68BBAA24AE] + (80,07): payload=[BC8AAC80BC72C50EBCC4E2C1] + (80,08): payload=[3EEE3D9FBD66DADDC0AFDEF5] +Packet from span: desc_set=0x80 tot_len=82 pay_len=76 chk=valid [7565804C0AD5000000055EE67CC00AD6000000014E434A000E043D9EE88D387FDB00BF7AAF030E05BB0C1E30BB572E68BBAA24AE0E07BC8AAC80BC72C50EBCC4E2C10E083EEE3D9FBD66DADDC0AFDEF59196] + (80,D5): payload=[000000055EE67CC0] + (80,D6): payload=[000000014E434A00] + (80,04): payload=[3D9EE88D387FDB00BF7AAF03] + (80,05): payload=[BB0C1E30BB572E68BBAA24AE] + (80,07): payload=[BC8AAC80BC72C50EBCC4E2C1] + (80,08): payload=[3EEE3D9FBD66DADDC0AFDEF5] +Packet from C packet: desc_set=0x80 tot_len=82 pay_len=76 chk=valid [7565804C0AD5000000055EE67CC00AD6000000014E434A000E043D9EE88D387FDB00BF7AAF030E05BB0C1E30BB572E68BBAA24AE0E07BC8AAC80BC72C50EBCC4E2C10E083EEE3D9FBD66DADDC0AFDEF59196] + (80,D5): payload=[000000055EE67CC0] + (80,D6): payload=[000000014E434A00] + (80,04): payload=[3D9EE88D387FDB00BF7AAF03] + (80,05): payload=[BB0C1E30BB572E68BBAA24AE] + (80,07): payload=[BC8AAC80BC72C50EBCC4E2C1] + (80,08): payload=[3EEE3D9FBD66DADDC0AFDEF5] +Sensor Data packet: + Ref Time = 23067000000 + Scaled Accel = (0.077592, 0.000061, -0.979233) + Scaled Gyro = (-0.002138, -0.003283, -0.005192) diff --git a/test/test.c b/test/test.c new file mode 100644 index 000000000..2a3158411 --- /dev/null +++ b/test/test.c @@ -0,0 +1,56 @@ + +#include "test.h" + +#include +#include + + +unsigned int num_errors = 0; + +void print_buffer(FILE* file, const uint8_t* buffer, size_t length) +{ + for(unsigned int i=0; i +#include +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +extern unsigned int num_errors; + +void print_buffer(FILE* file, const uint8_t* buffer, size_t length); + +bool check(bool value, const char* fmt, ...); +bool check_equal(int a, int b, const char* fmt, ...); + + +#ifdef __cplusplus +} // extern "C" + +void printT(FILE* file, int value) { fprintf(file, "%d", value); } +void printT(FILE* file, unsigned int value) { fprintf(file, "%u", value); } +void printT(FILE* file, size_t value) { fprintf(file, "%zu", value); } +void printT(FILE* file, const void* value) { fprintf(file, "%p", value); } + +template +bool check_equal(A a, B b, const char* fmt, ...) +{ + if( a == b ) + return true; + + va_list argptr; + va_start(argptr, fmt); + vfprintf(stderr, fmt, argptr); + va_end(argptr); + + fprintf(stderr, "( "); + printT(stderr, a); + fprintf(stderr, " != "); + printT(stderr, b); + fprintf(stderr, ")\n"); + + num_errors++; + return false; +} + +#endif