From f60135cec62c91a9d7923397fe8796d2b3eaa5cb Mon Sep 17 00:00:00 2001 From: jsmall-nvidia Date: Wed, 12 Sep 2018 16:27:42 -0400 Subject: Feature/memory arena (#631) * First pass at MemoryArena. * First pass at RandomGenerator. * Extract TestContext into external source file. * Fix warning on printf. * Use enum classes for Test enums. OutputMode -> TestOutputMode. * First pass at FreeList unit test. * Auto registering tests. Improvements to RandomGenerator. * Remove the need for unitTest headers - cos can use registering. * Added unitTest for MemoryArena. * Do unit tests. * Fix typo. --- slang.h | 12 + source/core/core.vcxproj | 4 + source/core/core.vcxproj.filters | 14 +- source/core/list.h | 9 + source/core/slang-memory-arena.cpp | 244 ++++++++++ source/core/slang-memory-arena.h | 393 +++++++++++++++ source/core/slang-random-generator.cpp | 143 ++++++ source/core/slang-random-generator.h | 97 ++++ source/core/slang-string-util.cpp | 48 ++ source/core/slang-string-util.h | 14 +- test.bat | 1 + tools/render-test/main.cpp | 2 +- tools/slang-test/main.cpp | 712 +++++----------------------- tools/slang-test/slang-test.vcxproj | 4 + tools/slang-test/slang-test.vcxproj.filters | 14 +- tools/slang-test/test-context.cpp | 450 ++++++++++++++++++ tools/slang-test/test-context.h | 139 ++++++ tools/slang-test/unit-test-free-list.cpp | 55 +++ tools/slang-test/unit-test-memory-arena.cpp | 242 ++++++++++ 19 files changed, 2005 insertions(+), 592 deletions(-) create mode 100644 source/core/slang-memory-arena.cpp create mode 100644 source/core/slang-memory-arena.h create mode 100644 source/core/slang-random-generator.cpp create mode 100644 source/core/slang-random-generator.h create mode 100644 tools/slang-test/test-context.cpp create mode 100644 tools/slang-test/test-context.h create mode 100644 tools/slang-test/unit-test-free-list.cpp create mode 100644 tools/slang-test/unit-test-memory-arena.cpp diff --git a/slang.h b/slang.h index 6dcfbb503..564973356 100644 --- a/slang.h +++ b/slang.h @@ -230,6 +230,9 @@ convention for interface methods. # define SLANG_FORCE_INLINE __forceinline # define SLANG_BREAKPOINT(id) __debugbreak(); # define SLANG_ALIGN_OF(T) __alignof(T) + +# define SLANG_INT64(x) (x##i64) +# define SLANG_UINT64(x) (x##ui64) #endif // SLANG_MICROSOFT_FAMILY #ifndef SLANG_FORCE_INLINE @@ -268,6 +271,14 @@ convention for interface methods. # define SLANG_UNUSED(v) (void)v; #endif +// Used for doing constant literals +#ifndef SLANG_INT64 +# define SLANG_INT64(x) (x##ll) +#endif +#ifndef SLANG_UINT64 +# define SLANG_UINT64(x) (x##ull) +#endif + #ifdef __cplusplus // C++ specific macros // Gcc @@ -300,6 +311,7 @@ convention for interface methods. # if _MSC_VER >= 1700 # define SLANG_HAS_ENUM_CLASS 1 # endif + # endif // SLANG_VC // Set non set diff --git a/source/core/core.vcxproj b/source/core/core.vcxproj index 3dbfaac3f..2944ee9dd 100644 --- a/source/core/core.vcxproj +++ b/source/core/core.vcxproj @@ -185,6 +185,8 @@ + + @@ -197,6 +199,8 @@ + + diff --git a/source/core/core.vcxproj.filters b/source/core/core.vcxproj.filters index 27b0fe82f..9e90aaa77 100644 --- a/source/core/core.vcxproj.filters +++ b/source/core/core.vcxproj.filters @@ -1,4 +1,4 @@ - + @@ -75,6 +75,12 @@ Header Files + + Header Files + + + Header Files + @@ -101,6 +107,12 @@ Source Files + + Source Files + + + Source Files + diff --git a/source/core/list.h b/source/core/list.h index 6df60b74b..cddcbb6c0 100644 --- a/source/core/list.h +++ b/source/core/list.h @@ -199,6 +199,15 @@ namespace Slang return buffer[_count-1]; } + void RemoveLast() + { +#ifdef _DEBUG + if (_count == 0) + throw "Index out of range."; +#endif + _count--; + } + inline void SwapWith(List & other) { T* tmpBuffer = this->buffer; diff --git a/source/core/slang-memory-arena.cpp b/source/core/slang-memory-arena.cpp new file mode 100644 index 000000000..bdfd87602 --- /dev/null +++ b/source/core/slang-memory-arena.cpp @@ -0,0 +1,244 @@ + +#include "slang-memory-arena.h" + +namespace Slang { + +MemoryArena::MemoryArena() +{ + // Mark as invalid so any alloc call will fail + m_blockAlignment = 0; + m_blockSize = 0; + + // Set up as empty + m_blocks = nullptr; + _setCurrentBlock(nullptr); + m_blockFreeList.init(sizeof(Block), sizeof(void*), 16); +} + +MemoryArena::~MemoryArena() +{ + _deallocateBlocks(); +} + +MemoryArena::MemoryArena(size_t blockSize, size_t blockAlignment) +{ + _initialize(blockSize, blockAlignment); +} + +void MemoryArena::init(size_t blockSize, size_t blockAlignment) +{ + _deallocateBlocks(); + m_blockFreeList.reset(); + _initialize(blockSize, blockAlignment); +} + +void MemoryArena::_initialize(size_t blockSize, size_t alignment) +{ + // Alignment must be a power of 2 + assert(((alignment - 1) & alignment) == 0); + + // Must be at least sizeof(void*) in size, as that is the minimum the backing allocator will be + alignment = (alignment < kMinAlignment) ? kMinAlignment : alignment; + + // If alignment required is larger then the backing allocators then + // make larger to ensure when alignment correction takes place it will be aligned + if (alignment > kMinAlignment) + { + blockSize += alignment; + } + + m_blockSize = blockSize; + m_blockAlignment = alignment; + m_blocks = nullptr; + _setCurrentBlock(nullptr); + m_blockFreeList.init(sizeof(Block), sizeof(void*), 16); +} + +void* MemoryArena::_allocateAligned(size_t size, size_t alignment) +{ + assert(size); + // Can't be space in the current block -> so we can either place in next, or in a new block + _newCurrentBlock(size, alignment); + uint8_t* const current = m_current; + // If everything has gone to plan, must be space here... + assert(current + size <= m_end); + m_current = current + size; + return current; +} + +void MemoryArena::_deallocateBlocks() +{ + Block* currentBlock = m_blocks; + while (currentBlock) + { + // Deallocate the block + ::free(currentBlock->m_alloc); + // next block + currentBlock = currentBlock->m_next; + } + // Can deallocate all blocks to + m_blockFreeList.deallocateAll(); +} + +void MemoryArena::_setCurrentBlock(Block* block) +{ + if (block) + { + m_end = block->m_end; + m_start = block->m_start; + m_current = m_start; + } + else + { + m_start = nullptr; + m_end = nullptr; + m_current = nullptr; + } + m_currentBlock = block; +} + +MemoryArena::Block* MemoryArena::_newCurrentBlock(size_t size, size_t alignment) +{ + // Make sure init has been called (or has been set up in parameterized constructor) + assert(m_blockSize > 0); + // Alignment must be a power of 2 + assert(((alignment - 1) & alignment) == 0); + + // Alignment must at a minimum be block alignment (such if reused the constraints hold) + alignment = (alignment < m_blockAlignment) ? m_blockAlignment : alignment; + + const size_t alignMask = alignment - 1; + + // First try the next block (if there is one) + { + Block* next = m_currentBlock ? m_currentBlock->m_next : m_blocks; + if (next) + { + // Align could be done from the actual allocation start, but doing so would mean a pointer which + // didn't hit the constraint of being between start/end + // So have to align conservatively using start + uint8_t* memory = (uint8_t*)((size_t(next->m_start) + alignMask) & ~alignMask); + + // Check if can fit block in + if (memory + size <= next->m_end) + { + _setCurrentBlock(next); + return next; + } + } + } + + // The size of the block must be at least large enough to take into account alignment + size_t allocSize = (alignment <= kMinAlignment) ? size : (size + alignment); + + // The minimum block size should be at least m_blockSize + allocSize = (allocSize < m_blockSize) ? m_blockSize : allocSize; + + // Allocate block + Block* block = (Block*)m_blockFreeList.allocate(); + if (!block) + { + return nullptr; + } + // Allocate the memory + uint8_t* alloc = (uint8_t*)::malloc(allocSize); + if (!alloc) + { + m_blockFreeList.deallocate(block); + return nullptr; + } + // Do the alignment on the allocation + uint8_t* const start = (uint8_t*)((size_t(alloc) + alignMask) & ~alignMask); + + // Setup the block + block->m_alloc = alloc; + block->m_start = start; + block->m_end = alloc + allocSize; + block->m_next = nullptr; + + // Insert block into list + if (m_currentBlock) + { + // Insert after current block + block->m_next = m_currentBlock->m_next; + m_currentBlock->m_next = block; + } + else + { + // Add to start of the list of the blocks + block->m_next = m_blocks; + m_blocks = block; + } + _setCurrentBlock(block); + return block; +} + +MemoryArena::Block* MemoryArena::_findBlock(const void* alloc, Block* endBlock) const +{ + const uint8_t* ptr = (const uint8_t*)alloc; + + Block* block = m_blocks; + while (block != endBlock) + { + if (ptr >= block->m_start && ptr < block->m_end) + { + return block; + } + block = block->m_next; + } + return nullptr; +} + +MemoryArena::Block* MemoryArena::_findPreviousBlock(Block* block) +{ + Block* currentBlock = m_blocks; + while (currentBlock) + { + if (currentBlock->m_next == block) + { + return currentBlock; + } + currentBlock = currentBlock->m_next; + } + return nullptr; +} + +void MemoryArena::deallocateAll() +{ + Block** prev = &m_blocks; + Block* block = m_blocks; + + while (block) + { + if (size_t(block->m_end - block->m_alloc) > m_blockSize) + { + // Oversized block so we need to free it and remove from the list + Block* nextBlock = block->m_next; + *prev = nextBlock; + // Free the backing memory + ::free(block->m_alloc); + // Free the block + m_blockFreeList.deallocate(block); + // prev stays the same, now working on next tho + block = nextBlock; + } + else + { + // Onto next + prev = &block->m_next; + block = block->m_next; + } + } + + // Make the first current (if any) + _setCurrentBlock(m_blocks); +} + +void MemoryArena::reset() +{ + _deallocateBlocks(); + m_blocks = nullptr; + _setCurrentBlock(nullptr); +} + +} // namespace Slang diff --git a/source/core/slang-memory-arena.h b/source/core/slang-memory-arena.h new file mode 100644 index 000000000..acbac51dd --- /dev/null +++ b/source/core/slang-memory-arena.h @@ -0,0 +1,393 @@ +#ifndef SLANG_MEMORY_ARENA_H +#define SLANG_MEMORY_ARENA_H + +#include "../../slang.h" + +#include + +#include +#include + +#include "slang-free-list.h" + +namespace Slang { + +/** Defines arena allocator where allocations are made very quickly, but that deallocations +can only be performed in reverse order, or with the client code knowing a previous deallocation (say with + deallocateAllFrom), automatically deallocates everything after it. + +It works by allocating large blocks and then cutting out smaller pieces as requested. If a piece of memory is +deallocated, it either MUST be in reverse allocation order OR the subsequent allocations are implicitly +deallocated too, and therefore accessing their memory is now undefined behavior. Allocations are made +contiguously from the current block. If there is no space in the current block, the +next block (which is unused) if available is checked. If that works, an allocation is made from the next block. +If not a new block is allocated that can hold at least the allocation with required alignment. + +All memory allocated can be deallocated very quickly and without a client having to track any memory. +All memory allocated will be freed on destruction - or with reset. + +A memory arena can have requests larger than the block size. When that happens they will just be allocated +from the heap. As such 'oversized blocks' are seen as unusual and potentially wasteful they are deallocated +when deallocateAll is called, whereas regular size blocks will remain allocated for fast subsequent allocation. + +It is intentional that blocks information is stored separately from the allocations that store the +user data. This is so that alignment permitting, block allocations sizes can be passed directly to underlying allocator. +For large power of 2 backing allocations this might mean a page/pages directly allocated by the OS for example. +Also means better cache coherency when traversing blocks -> as generally they will be contiguous in memory. + +Also note that allocateUnaligned can be used for slightly faster aligned allocations. All blocks allocated internally +are aligned to the blockAlignment passed to the constructor. If subsequent allocations (of any type) sizes are of that +alignment or larger then no alignment fixing is required (because allocations are contiguous) and so 'allocateUnaligned' +will return allocations of blockAlignment alignment. +*/ +class MemoryArena +{ +public: + typedef MemoryArena ThisType; + + static const size_t kMinAlignment = sizeof(void*); ///< The minimum alignment of the backing memory allocator. + + /** Determines if an allocation is consistent with an allocation from this arena. + + The test cannot say definitively if this was such an allocation, because the exact details + of each allocation is not kept. + @param alloc The start of the allocation + @param sizeInBytes The size of the allocation + @return true if allocation could have been from this Arena */ + bool isValid(const void* alloc, size_t sizeInBytes) const; + + /** Initialize the arena with specified block size and alignment + If the arena has been previously initialized will free and deallocate all memory */ + void init(size_t blockSize, size_t blockAlignment = kMinAlignment); + + /** Allocate some memory of at least size bytes without having any specific alignment. + + Can be used for slightly faster *aligned* allocations if caveats in class description are met. The + Unaligned, means the method will not enforce alignment - but a client call to allocateUnaligned can control + subsequent allocations alignments via it's size. + + @param size The size of the allocation requested (in bytes and must be > 0). + @return The allocation. Can be nullptr if backing allocator was not able to request required memory */ + void* allocate(size_t sizeInBytes); + + /** Allocate some aligned memory of at least size bytes + @param size Size of allocation wanted (must be > 0). + @param alignment Alignment of allocation - must be a power of 2. + @return The allocation (or nullptr if unable to allocate). Will be at least 'alignment' alignment or better. */ + void* allocateAligned(size_t sizeInBytes, size_t alignment); + + /** Allocate some aligned memory of at least size bytes + @param size Size of allocation wanted (must be > 0). + @param alignment Alignment of allocation - must be a power of 2. + @return The allocation (or nullptr if unable to allocate). Will be at least 'alignment' alignment or better. */ + void* allocateUnaligned(size_t sizeInBytes); + + /** Allocates a null terminated string. + @param str A null-terminated string + @return A copy of the string held on the arena */ + const char* allocateString(const char* str); + + /** Allocates a null terminated string. + @param chars Pointer to first character + @param charCount The amount of characters NOT including terminating 0. + @return A copy of the string held on the arena. */ + const char* allocateString(const char* chars, size_t numChars); + + /// Allocate an element of the specified type. Note: Constructor for type is not executed. + template + T* allocate(); + + /// Allocate an array of a specified type. NOTE Constructor of T is NOT executed. + template + T* allocateArray(size_t size); + + /// Allocate an array of a specified type, and copy array passed into it. + template + T* allocateAndCopyArray(const T* src, size_t size); + + /// Allocate an array of a specified type, and zero it. + template + T* allocateAndZeroArray(size_t size); + + /// Deallocate the last allocation. If data is not from the last allocation then the behavior is undefined. + void deallocateLast(void* data); + + /// Deallocate this allocation and all remaining after it. + void deallocateAllFrom(void* dataStart); + + /** Deallocates all allocated memory. That backing memory will generally not be released so + subsequent allocation will be fast, and from the same memory. Note though that 'oversize' blocks + will be deallocated. */ + void deallocateAll(); + + /// Resets to the initial state when constructed (and all backing memory will be deallocated) + void reset(); + /// Adjusts such that the next allocate will be at least to the block alignment. + void adjustToBlockAlignment(); + + /// Gets the block alignment that is passed at initialization otherwise 0 an invalid block alignment. + size_t getBlockAlignment() const { return m_blockAlignment; } + + /// Default Ctor + MemoryArena(); + /// Construct with block size and alignment. Block alignment must be a power of 2. + MemoryArena(size_t blockSize, size_t blockAlignment = kMinAlignment); + + /// Dtor + ~MemoryArena(); + +protected: + struct Block + { + Block* m_next; + uint8_t* m_alloc; + uint8_t* m_start; + uint8_t* m_end; + }; + + void _initialize(size_t blockSize, size_t blockAlignment); + + void* _allocateAligned(size_t size, size_t alignment); + void _deallocateBlocks(); + + void _setCurrentBlock(Block* block); + + Block* _newCurrentBlock(size_t size, size_t alignment); + Block* _findBlock(const void* alloc, Block* endBlock = nullptr) const; + Block* _findPreviousBlock(Block* block); + + uint8_t* m_start; + uint8_t* m_end; + uint8_t* m_current; + size_t m_blockSize; + size_t m_blockAlignment; + Block* m_blocks; + Block* m_currentBlock; + + FreeList m_blockFreeList; + + private: + // Disable + MemoryArena(const ThisType& rhs) = delete; + void operator=(const ThisType& rhs) = delete; +}; + +// -------------------------------------------------------------------------- +inline bool MemoryArena::isValid(const void* data, size_t size) const +{ + assert(size); + + uint8_t* ptr = (uint8_t*)data; + // Is it in current + if (ptr >= m_start && ptr + size <= m_current) + { + return true; + } + // Is it in a previous block? + Block* block = _findBlock(data, m_currentBlock); + return block && (ptr >= block->m_start && (ptr + size) <= block->m_end); +} + +// -------------------------------------------------------------------------- +SLANG_FORCE_INLINE void* MemoryArena::allocateUnaligned(size_t size) +{ + // Align with the minimum alignment + uint8_t* mem = m_current; + uint8_t* end = mem + size; + if (end <= m_end) + { + m_current = end; + return mem; + } + else + { + return _allocateAligned(size, m_blockAlignment); + } +} + +// -------------------------------------------------------------------------- +SLANG_FORCE_INLINE void* MemoryArena::allocate(size_t size) +{ + // Align with the minimum alignment + const size_t alignMask = kMinAlignment - 1; + uint8_t* mem = (uint8_t*)((size_t(m_current) + alignMask) & ~alignMask); + + if (mem + size <= m_end) + { + m_current = mem + size; + return mem; + } + else + { + return _allocateAligned(size, kMinAlignment); + } +} + +// -------------------------------------------------------------------------- +inline void* MemoryArena::allocateAligned(size_t size, size_t alignment) +{ + // Alignment must be a power of 2 + assert(((alignment - 1) & alignment) == 0); + + // Align the pointer + const size_t alignMask = alignment - 1; + uint8_t* memory = (uint8_t*)((size_t(m_current) + alignMask) & ~alignMask); + + if (memory + size <= m_end) + { + m_current = memory + size; + return memory; + } + else + { + return _allocateAligned(size, alignment); + } +} + +// -------------------------------------------------------------------------- +inline const char* MemoryArena::allocateString(const char* str) +{ + size_t size = ::strlen(str); + if (size == 0) + { + return ""; + } + char* dst = (char*)allocateUnaligned(size + 1); + ::memcpy(dst, str, size + 1); + return dst; +} + +// -------------------------------------------------------------------------- +inline const char* MemoryArena::allocateString(const char* chars, size_t charsCount) +{ + if (charsCount == 0) + { + return ""; + } + char* dst = (char*)allocateUnaligned(charsCount + 1); + ::memcpy(dst, chars, charsCount); + + // Add null-terminating zero + dst[charsCount] = 0; + return dst; +} + +// -------------------------------------------------------------------------- +template +inline T* MemoryArena::allocate() +{ + return reinterpret_cast(allocateAligned(sizeof(T), SLANG_ALIGN_OF(T))); +} + +// -------------------------------------------------------------------------- +template +inline T* MemoryArena::allocateArray(size_t count) +{ + return (count > 0) ? reinterpret_cast(allocateAligned(sizeof(T) * count, SLANG_ALIGN_OF(T))) : nullptr; +} + +// -------------------------------------------------------------------------- +template +inline T* MemoryArena::allocateAndCopyArray(const T* arr, size_t size) +{ + if (size > 0) + { + const size_t totalSize = sizeof(T) * size; + void* ptr = allocateAligned(totalSize, SLANG_ALIGN_OF(T)); + ::memcpy(ptr, arr, totalSize); + return reinterpret_cast(ptr); + } + return nullptr; +} + +// --------------------------------------------------------------------------- +template +inline T* MemoryArena::allocateAndZeroArray(size_t size) +{ + if (size > 0) + { + const size_t totalSize = sizeof(T) * size; + void* ptr = allocateAligned(totalSize, SLANG_ALIGN_OF(T)); + ::memset(ptr, 0, totalSize); + return reinterpret_cast(ptr); + } + return nullptr; +} + +// -------------------------------------------------------------------------- +inline void MemoryArena::deallocateLast(void* data) +{ + // See if it's in current block + uint8_t* ptr = (uint8_t*)data; + if (ptr >= m_start && ptr < m_current) + { + // Then just go back + m_current = ptr; + } + else + { + // Only called if not in the current block. Therefore can only be in previous + Block* prevBlock = _findPreviousBlock(m_currentBlock); + if (prevBlock == nullptr || (!(ptr >= prevBlock->m_start && ptr < prevBlock->m_end))) + { + assert(!"Allocation not found"); + return; + } + + // Make the previous block the current + _setCurrentBlock(prevBlock); + // Make the current the alloc freed + m_current = ptr; + } +} + +// -------------------------------------------------------------------------- +inline void MemoryArena::deallocateAllFrom(void* data) +{ + // See if it's in current block, and is allocated (ie < m_current) + uint8_t* ptr = (uint8_t*)data; + if (ptr >= m_start && ptr < m_current) + { + // If it's in current block, then just go back + m_current = ptr; + return; + } + + // Search all blocks prior to current block + Block* block = _findBlock(data, m_currentBlock); + assert(block); + if (!block) + { + return; + } + // Make this current block + _setCurrentBlock(block); + + // Move the pointer to the allocations position + m_current = ptr; +} + +// -------------------------------------------------------------------------- +inline void MemoryArena::adjustToBlockAlignment() +{ + const size_t alignMask = m_blockAlignment - 1; + uint8_t* ptr = (uint8_t*)((size_t(m_current) + alignMask) & ~alignMask); + + // Alignment might push beyond end of block... if so allocate a new block + // This test could be avoided if we aligned m_end, but depending on block alignment that might waste some space + if (ptr > m_end) + { + // We'll need a new block to make this alignment + _newCurrentBlock(0, m_blockAlignment); + } + else + { + // Set the position + m_current = ptr; + } + assert(size_t(m_current) & alignMask); +} + +} // namespace Slang + +#endif // SLANG_MEMORY_ARENA_H \ No newline at end of file diff --git a/source/core/slang-random-generator.cpp b/source/core/slang-random-generator.cpp new file mode 100644 index 000000000..7e8476c30 --- /dev/null +++ b/source/core/slang-random-generator.cpp @@ -0,0 +1,143 @@ + +#include "slang-random-generator.h" + +namespace Slang { + +/* !!!!!!!!!!!!!!!!!!!!!!!!!!!! RandomGenerator !!!!!!!!!!!!!!!!!!!!!!!! */ + +float RandomGenerator::nextUnitFloat32() +{ + int32_t intValue = nextInt32(); + return (intValue & 0x7fffffff) * (1.0f / 0x7fffffff); +} + +bool RandomGenerator::nextBool() +{ + uint32_t bits = uint32_t(nextInt32()); + + // Xor together all bits in each byte + bits = ((bits & 0xaaaaaaaa) >> 1) ^ (bits & 0x55555555); + bits = ((bits & 0x44444444) >> 2) ^ (bits & 0x11111111); + bits = ((bits & 0x10101010) >> 4) ^ (bits & 0x01010101); + + // In effect is the xor of all the bits of the original last byte + return ( bits & 1) != 0; +} + +int64_t RandomGenerator::nextInt64() +{ + const int32_t high = nextInt32(); + const int32_t low = nextInt32(); + + return (int64_t(high) << 32) | low; +} + +int32_t RandomGenerator::nextInt32InRange(int32_t min, int32_t max) +{ + int32_t diff = max - min; + if (diff <= 1) + { + return min; + } + + return (nextPositiveInt32() % diff) + min; +} + +int64_t RandomGenerator::nextInt64InRange(int64_t min, int64_t max) +{ + int64_t diff = max - min; + if (diff <= 1) + { + return min; + } + return (nextPositiveInt64() % diff) + min; +} + +/* static */RandomGenerator* RandomGenerator::create(int32_t seed) +{ + return new DefaultRandomGenerator(seed); +} + +/* !!!!!!!!!!!!!!!!!!!!!!!!!!!! Mt19937RandomGenerator !!!!!!!!!!!!!!!!!!!!!!!! */ + +Mt19937RandomGenerator::Mt19937RandomGenerator() +{ + reset(21452); +} + +Mt19937RandomGenerator::Mt19937RandomGenerator(const ThisType& rhs) +{ + *this = rhs; +} + +Mt19937RandomGenerator::Mt19937RandomGenerator(int32_t seed) +{ + reset(seed); +} + +void Mt19937RandomGenerator::_generate() +{ + const uint32_t xorValue = 2567483615u; + for (int i = 0; i < kNumEntries - 1; ++i) + { + const uint32_t y = (m_mt[i] & 0x80000000) + (m_mt[i + 1] & 0x7fffffff); + + // o = (i + 397) % kNumEntries + int32_t o = i + 397; + o = (o >= kNumEntries) ? (o - kNumEntries) : o; + + m_mt[i] = m_mt[o] ^ (y >> 1); + // If y is odd + if (y & 1) + { + m_mt[i] = m_mt[i] ^ xorValue; + } + } + + // Last + { + const int i = kNumEntries - 1; + const uint32_t y = (m_mt[i] & 0x80000000) + (m_mt[0] & 0x7fffffff); + const int32_t o = ((i + 397) - kNumEntries); + + m_mt[i] = m_mt[o] ^ (y >> 1); + // If y is odd + if (y & 1) + { + m_mt[i] = m_mt[i] ^ xorValue; + } + } + + m_index = 0; +} + +void Mt19937RandomGenerator::reset(int32_t seedIn) +{ + m_index = 0; + m_mt[0] = uint32_t(seedIn); + for (int i = 1; i < kNumEntries; ++i) + { + m_mt[i] = (1812433253 * (m_mt[i - 1] ^ (m_mt[i - 1] >> 30)) + i); + } +} + +int32_t Mt19937RandomGenerator::nextInt32() +{ + if (m_index >= kNumEntries) + { + _generate(); + } + + uint32_t y = m_mt[m_index++]; + y = y ^ (y >> 11); + y = y ^ ((y << 7) & uint32_t(0x9d2c5680u)); + y = y ^ ((y << 15) & uint32_t(0xefc6000u)); + y = y ^ (y >> 18); + + return int32_t(y); +} + + + + +} // namespace Slang diff --git a/source/core/slang-random-generator.h b/source/core/slang-random-generator.h new file mode 100644 index 000000000..7b3e42288 --- /dev/null +++ b/source/core/slang-random-generator.h @@ -0,0 +1,97 @@ +#ifndef SLANG_RANDOM_GENERATOR_H +#define SLANG_RANDOM_GENERATOR_H + +#include "../../slang.h" + +#include + +#include +#include + +#include "smart-pointer.h" + +namespace Slang { + +class RandomGenerator: public RefObject +{ + public: + + /// Make a copy of the generator in the same state + virtual RandomGenerator* clone() = 0; + + /// Reset with a seed + virtual void reset(int32_t seed) = 0; + /// Next int32_t random number + virtual int32_t nextInt32() = 0; + /// Next int64_t random number + virtual int64_t nextInt64(); + + /// Get a 0-1 range floating point + virtual float nextUnitFloat32(); + + /// Get the next bool + virtual bool nextBool(); + + /// Next Int32 which can only be positive + int32_t nextPositiveInt32() { return nextInt32() & 0x7fffffff; } + /// Next Int64 which can only be positive + int64_t nextPositiveInt64() { return nextInt64() & SLANG_INT64(0x7fffffffffffffff); } + + /// Returns value up to BUT NOT INCLUDING maxValue. + int32_t nextInt32UpTo(int32_t maxValue) { assert(maxValue > 0); return (maxValue <= 1) ? 0 : (nextPositiveInt32() % maxValue); } + + /// Returns value from min up to BUT NOT INCLUDING max + int32_t nextInt32InRange(int32_t min, int32_t max); + + /// Returns value up to BUT NOT INCLUDING maxValue + int64_t nextInt64UpTo(int64_t maxValue) { assert(maxValue > 0); return (maxValue <= 1) ? 0 : (nextPositiveInt64() % maxValue); } + + /// Returns value from min up to BUT NOT INCLUDING max + int64_t nextInt64InRange(int64_t min, int64_t max); + + /// Create a RandomGenerator with specified seed using default generator type + static RandomGenerator* create(int32_t seed); +}; + +/* Mersenne Twister random number generator +https://en.wikipedia.org/wiki/Mersenne_Twister +*/ +class Mt19937RandomGenerator: public RandomGenerator +{ + public: + typedef Mt19937RandomGenerator ThisType; + + enum + { + kNumEntries = 624 + }; + + Mt19937RandomGenerator* clone() SLANG_OVERRIDE { return new ThisType(*this); } + void reset(int32_t seed) SLANG_OVERRIDE; + int32_t nextInt32() SLANG_OVERRIDE; + + /// Ctor + Mt19937RandomGenerator(); + Mt19937RandomGenerator(const ThisType& rhs); + explicit Mt19937RandomGenerator(int32_t seed); + + /// Assignment + void operator=(const ThisType& rhs) + { + m_index = rhs.m_index; + ::memcpy(m_mt, rhs.m_mt, sizeof(m_mt)); + } + + protected: + void _generate(); + + uint32_t m_mt[kNumEntries]; ///< The random state vector + int m_index; ///< If set to >= kMaxEntries it means reset + +}; + +typedef Mt19937RandomGenerator DefaultRandomGenerator; + +} // namespace Slang + +#endif // SLANG_RANDOM_GENERATOR_H \ No newline at end of file diff --git a/source/core/slang-string-util.cpp b/source/core/slang-string-util.cpp index c60ad9683..29f8dc0ca 100644 --- a/source/core/slang-string-util.cpp +++ b/source/core/slang-string-util.cpp @@ -26,4 +26,52 @@ namespace Slang { } } + +/* static */void StringUtil::append(const char* format, va_list args, StringBuilder& buf) +{ + int numChars = 0; + +#if SLANG_WINDOWS_FAMILY + numChars = _vscprintf(format, args); +#else + { + va_list argsCopy; + va_copy(argsCopy, args); + numChars = vsnprintf(nullptr, 0, format, argsCopy); + va_end(argsCopy); + } +#endif + + List chars; + chars.SetSize(numChars + 1); + +#if SLANG_WINDOWS_FAMILY + vsnprintf_s(chars.Buffer(), numChars + 1, _TRUNCATE, format, args); +#else + vsnprintf(chars.Buffer(), numChars + 1, format, args); +#endif + + buf.Append(chars.Buffer(), numChars); +} + +/* static */void StringUtil::appendFormat(StringBuilder& buf, const char* format, ...) +{ + va_list args; + va_start(args, format); + append(format, args, buf); + va_end(args); +} + +/* static */String StringUtil::makeStringWithFormat(const char* format, ...) +{ + StringBuilder builder; + + va_list args; + va_start(args, format); + append(format, args, builder); + va_end(args); + + return builder; +} + } // namespace Slang diff --git a/source/core/slang-string-util.h b/source/core/slang-string-util.h index bae576370..fc3258490 100644 --- a/source/core/slang-string-util.h +++ b/source/core/slang-string-util.h @@ -4,14 +4,26 @@ #include "slang-string.h" #include "list.h" +#include + namespace Slang { struct StringUtil { + /// Split in, by specified splitChar into slices out + /// Slices contents will directly address into in, so contents will only stay valid as long as in does. static void split(const UnownedStringSlice& in, char splitChar, List& slicesOut); + + /// Appends formatted string with args into buf + static void append(const char* format, va_list args, StringBuilder& buf); + + /// Appends the formatted string with specified trailing args + static void appendFormat(StringBuilder& buf, const char* format, ...); + + /// Create a string from the format string applying args (like sprintf) + static String makeStringWithFormat(const char* format, ...); }; } // namespace Slang - #endif // SLANG_STRING_UTIL_H diff --git a/test.bat b/test.bat index 6c6e88580..f21dcbbfb 100644 --- a/test.bat +++ b/test.bat @@ -56,3 +56,4 @@ SET "PATH=%PATH%;%SLANG_TEST_BIN_DIR%" :: TODO: Maybe we should actually invoke `msbuild` to make sure all the code is up to date? "%SLANG_TEST_BIN_DIR%slang-test.exe" -bindir "%SLANG_TEST_BIN_DIR%\" %* +"%SLANG_TEST_BIN_DIR%slang-test.exe" -bindir "%SLANG_TEST_BIN_DIR%\" -unitTests diff --git a/tools/render-test/main.cpp b/tools/render-test/main.cpp index 4734c2c8f..77e077b6c 100644 --- a/tools/render-test/main.cpp +++ b/tools/render-test/main.cpp @@ -338,7 +338,7 @@ Result RenderTestApp::writeBindingOutput(const char* fileName) } else { - printf("invalid output type at %d.\n", i); + printf("invalid output type at %d.\n", int(i)); } } } diff --git a/tools/slang-test/main.cpp b/tools/slang-test/main.cpp index 49279e81c..b57dc6d9c 100644 --- a/tools/slang-test/main.cpp +++ b/tools/slang-test/main.cpp @@ -11,6 +11,7 @@ using namespace Slang; #include "os.h" #include "render-api-util.h" +#include "test-context.h" #define STB_IMAGE_IMPLEMENTATION #include "external/stb/stb_image.h" @@ -26,98 +27,6 @@ using namespace Slang; #include #include #include -enum OutputMode -{ - // Default mode is to write test results to the console - kOutputMode_Default = 0, - - // When running under AppVeyor continuous integration, we - // need to output test results in a way that the AppVeyor - // environment can pick up and display. - kOutputMode_AppVeyor, - - // We currently don't specialize for Travis, but maybe - // we should. - kOutputMode_Travis, - - // xUnit original format - // https://nose.readthedocs.io/en/latest/plugins/xunit.html - kOutputMode_xUnit, - - // https://xunit.github.io/docs/format-xml-v2 - kOutputMode_xUnit2, -}; - -enum TestResult -{ - kTestResult_Fail, - kTestResult_Pass, - kTestResult_Ignored, -}; - -enum class MessageType -{ - INFO, ///< General info (may not be shown depending on verbosity setting) - TEST_FAILURE, ///< Describes how a test failure took place - RUN_ERROR, ///< Describes an error that caused a test not to actually correctly run -}; - -struct TestContext -{ - struct TestInfo - { - TestResult testResult = TestResult::kTestResult_Ignored; - String name; - String message; ///< Message that is specific for the testResult - }; - - void addResult(const String& testName, TestResult testResult); - - void startTest(const String& testName); - TestResult endTest(TestResult result); - - // Called for an error in the test-runner (not for an error involving - // a test itself). - void message(MessageType type, const String& errorText); - void messageFormat(MessageType type, char const* message, ...); - - void dumpOutputDifference(const String& expectedOutput, const String& actualOutput); - - bool canWriteStdError() const - { - switch (m_outputMode) - { - case kOutputMode_xUnit: - case kOutputMode_xUnit2: - { - return false; - } - default: return true; - } - } - - /// Ctor - TestContext(OutputMode outputMode); - - List m_testInfos; - - int m_totalTestCount; - int m_passedTestCount; - int m_failedTestCount; - int m_ignoredTestCount; - - OutputMode m_outputMode = kOutputMode_Default; - bool m_dumpOutputOnFailure; - bool m_isVerbose; - - protected: - void _addResult(const TestInfo& info); - - StringBuilder m_currentMessage; - - TestInfo m_currentInfo; - bool m_inTest; -}; // A category that a test can be tagged with struct TestCategory @@ -190,7 +99,7 @@ struct Options bool dumpOutputOnFailure = false; // kind of output to generate - OutputMode outputMode = kOutputMode_Default; + TestOutputMode outputMode = TestOutputMode::eDefault; // Only run tests that match one of the given categories Dictionary includeCategories; @@ -204,6 +113,9 @@ struct Options // By default we potentially synthesize test for all // TODO: Vulkan is disabled by default for now as the majority as vulkan synthesized tests fail RenderApiFlags synthesizedTestApis = RenderApiFlag::AllOf & ~RenderApiFlag::Vulkan; + + // Set this to turn on unit tests + bool unitTests = false; }; // Globals @@ -216,271 +128,8 @@ TestCategory* g_defaultTestCategory; TestCategory* findTestCategory(String const& name); -void append(const char* format, va_list args, StringBuilder& buf); - -void appendFormat(StringBuilder& buf, const char* format, ...); -String makeStringWithFormat(const char* format, ...); - -/* !!!!!!!!!!!!!!!!!!!!!!!!!!!!! TestContext !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! */ - -TestContext::TestContext(OutputMode outputMode): - m_outputMode(outputMode) -{ - m_totalTestCount = 0; - m_passedTestCount = 0; - m_failedTestCount = 0; - m_ignoredTestCount = 0; - - m_inTest = false; - m_dumpOutputOnFailure = false; - m_isVerbose = false; -} - -void TestContext::startTest(const String& testName) -{ - assert(!m_inTest); - m_inTest = true; - - m_currentInfo = TestInfo(); - m_currentInfo.name = testName; - m_currentMessage.Clear(); -} - -TestResult TestContext::endTest(TestResult result) -{ - assert(m_inTest); - - m_currentInfo.testResult = result; - m_currentInfo.message = m_currentMessage; - - _addResult(m_currentInfo); - - m_inTest = false; - - return result; -} - -void TestContext::dumpOutputDifference(const String& expectedOutput, const String& actualOutput) -{ - StringBuilder builder; - - appendFormat(builder, - "ERROR:\n" - "EXPECTED{{{\n%s}}}\n" - "ACTUAL{{{\n%s}}}\n", - expectedOutput.Buffer(), - actualOutput.Buffer()); - - - if (m_dumpOutputOnFailure && canWriteStdError()) - { - fprintf(stderr, "%s", builder.Buffer()); - fflush(stderr); - } - - // Add to the m_currentInfo - message(MessageType::TEST_FAILURE, builder); -} - -void TestContext::_addResult(const TestInfo& info) -{ - m_totalTestCount++; - - switch (info.testResult) - { - case kTestResult_Fail: - m_failedTestCount++; - break; - - case kTestResult_Pass: - m_passedTestCount++; - break; - - case kTestResult_Ignored: - m_ignoredTestCount++; - break; - - default: - assert(!"unexpected"); - break; - } - - m_testInfos.Add(info); - - // printf("OUTPUT_MODE: %d\n", options.outputMode); - switch (m_outputMode) - { - default: - { - char const* resultString = "UNEXPECTED"; - switch (info.testResult) - { - case kTestResult_Fail: resultString = "FAILED"; break; - case kTestResult_Pass: resultString = "passed"; break; - case kTestResult_Ignored: resultString = "ignored"; break; - default: - assert(!"unexpected"); - break; - } - printf("%s test: '%S'\n", resultString, info.name.ToWString().begin()); - break; - } - case kOutputMode_xUnit2: - case kOutputMode_xUnit: - { - // Don't output anything -> we'll output all in one go at the end - break; - } - case kOutputMode_AppVeyor: - { - char const* resultString = "None"; - switch (info.testResult) - { - case kTestResult_Fail: resultString = "Failed"; break; - case kTestResult_Pass: resultString = "Passed"; break; - case kTestResult_Ignored: resultString = "Ignored"; break; - default: - assert(!"unexpected"); - break; - } - - OSProcessSpawner spawner; - spawner.pushExecutableName("appveyor"); - spawner.pushArgument("AddTest"); - spawner.pushArgument(info.name); - spawner.pushArgument("-FileName"); - // TODO: this isn't actually a file name in all cases - spawner.pushArgument(info.name); - spawner.pushArgument("-Framework"); - spawner.pushArgument("slang-test"); - spawner.pushArgument("-Outcome"); - spawner.pushArgument(resultString); - - auto err = spawner.spawnAndWaitForCompletion(); - - if (err != kOSError_None) - { - messageFormat(MessageType::INFO, "failed to add appveyor test results for '%S'\n", info.name.ToWString().begin()); - -#if 0 - fprintf(stderr, "[%d] TEST RESULT: %s {%d} {%s} {%s}\n", err, spawner.commandLine_.Buffer(), - spawner.getResultCode(), - spawner.getStandardOutput().begin(), - spawner.getStandardError().begin()); -#endif - } - - break; - } - } -} - -void TestContext::addResult(const String& testName, TestResult testResult) -{ - // Can't add this way if in test - assert(!m_inTest); - - TestInfo info; - info.name = testName; - info.testResult = testResult; - _addResult(info); -} - -void TestContext::message(MessageType type, const String& message) -{ - if (type == MessageType::INFO) - { - if (m_isVerbose && canWriteStdError()) - { - fputs(message.Buffer(), stderr); - } - - // Just dump out if can dump out - return; - } - - if (canWriteStdError()) - { - if (type == MessageType::RUN_ERROR || type == MessageType::TEST_FAILURE) - { - fprintf(stderr, "error: "); - fputs(message.Buffer(), stderr); - fprintf(stderr, "\n"); - } - else - { - fputs(message.Buffer(), stderr); - } - } - - if (m_currentMessage.Length() > 0) - { - m_currentMessage << "\n"; - } - m_currentMessage.Append(message); -} - -void TestContext::messageFormat(MessageType type, char const* format, ...) -{ - StringBuilder builder; - - va_list args; - va_start(args, format); - append(format, args, builder); - va_end(args); - - message(type, builder); -} - /* !!!!!!!!!!!!!!!!!!!!!!!!!!!!! Functions !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! */ -void append(const char* format, va_list args, StringBuilder& buf) -{ - int numChars = 0; - -#if SLANG_WINDOWS_FAMILY - numChars = _vscprintf(format, args); -#else - { - va_list argsCopy; - va_copy(argsCopy, args); - numChars = vsnprintf(nullptr, 0, format, argsCopy); - va_end(argsCopy); - } -#endif - - List chars; - chars.SetSize(numChars + 1); - -#if SLANG_WINDOWS_FAMILY - vsnprintf_s(chars.Buffer(), numChars + 1, _TRUNCATE, format, args); -#else - vsnprintf(chars.Buffer(), numChars + 1, format, args); -#endif - - buf.Append(chars.Buffer(), numChars); -} - -void appendFormat(StringBuilder& buf, const char* format, ...) -{ - va_list args; - va_start(args, format); - append(format, args, buf); - va_end(args); -} - -String makeStringWithFormat(const char* format, ...) -{ - StringBuilder builder; - - va_list args; - va_start(args, format); - append(format, args, builder); - va_end(args); - - return builder; -} - Result parseOptions(int* argc, char** argv) { @@ -563,21 +212,21 @@ Result parseOptions(int* argc, char** argv) } else if( strcmp(arg, "-appveyor") == 0 ) { - g_options.outputMode = kOutputMode_AppVeyor; + g_options.outputMode = TestOutputMode::eAppVeyor; g_options.dumpOutputOnFailure = true; } else if( strcmp(arg, "-travis") == 0 ) { - g_options.outputMode = kOutputMode_Travis; + g_options.outputMode = TestOutputMode::eTravis; g_options.dumpOutputOnFailure = true; } else if (strcmp(arg, "-xunit") == 0) { - g_options.outputMode = kOutputMode_xUnit; + g_options.outputMode = TestOutputMode::eXUnit; } else if (strcmp(arg, "-xunit2") == 0) { - g_options.outputMode = kOutputMode_xUnit2; + g_options.outputMode = TestOutputMode::eXUnit2; } else if( strcmp(arg, "-category") == 0 ) { @@ -637,6 +286,10 @@ Result parseOptions(int* argc, char** argv) return res; } } + else if (strcmp(arg, "-unitTests") == 0) + { + g_options.unitTests = true; + } else { fprintf(stderr, "unknown option '%s'\n", arg); @@ -831,7 +484,7 @@ TestResult gatherTestOptions( if(!category) { - return kTestResult_Fail; + return TestResult::eFail; } testOptions.categories.Add(category); @@ -846,7 +499,7 @@ TestResult gatherTestOptions( break; case 0: case '\r': case '\n': - return kTestResult_Fail; + return TestResult::eFail; } break; @@ -863,7 +516,7 @@ TestResult gatherTestOptions( cursor++; else { - return kTestResult_Fail; + return TestResult::eFail; } // Next scan for a sub-command name @@ -880,7 +533,7 @@ TestResult gatherTestOptions( break; case 0: case '\r': case '\n': - return kTestResult_Fail; + return TestResult::eFail; } break; @@ -893,7 +546,7 @@ TestResult gatherTestOptions( cursor++; else { - return kTestResult_Fail; + return TestResult::eFail; } // Now scan for arguments. For now we just assume that @@ -909,7 +562,7 @@ TestResult gatherTestOptions( case 0: case '\r': case '\n': skipToEndOfLine(&cursor); testList->tests.Add(testOptions); - return kTestResult_Pass; + return TestResult::ePass; default: break; @@ -950,7 +603,7 @@ TestResult gatherTestsForFile( } catch (Slang::IOException) { - return kTestResult_Fail; + return TestResult::eFail; } @@ -966,12 +619,12 @@ TestResult gatherTestsForFile( // Look for a pattern that matches what we want if(match(&cursor, "//TEST_IGNORE_FILE")) { - return kTestResult_Ignored; + return TestResult::eIgnored; } else if(match(&cursor, "//TEST")) { - if(gatherTestOptions(&cursor, testList) != kTestResult_Pass) - return kTestResult_Fail; + if(gatherTestOptions(&cursor, testList) != TestResult::ePass) + return TestResult::eFail; } else { @@ -979,7 +632,7 @@ TestResult gatherTestsForFile( } } - return kTestResult_Pass; + return TestResult::ePass; } OSError spawnAndWait(TestContext* context, const String& testPath, OSProcessSpawner& spawner) @@ -989,14 +642,14 @@ OSError spawnAndWait(TestContext* context, const String& testPath, OSProcessSpaw if(context->m_isVerbose) { String commandLine = spawner.getCommandLine(); - context->messageFormat(MessageType::INFO, "%s\n", commandLine.begin()); + context->messageFormat(TestMessageType::eInfo, "%s\n", commandLine.begin()); } OSError err = spawner.spawnAndWaitForCompletion(); if (err != kOSError_None) { // fprintf(stderr, "failed to run test '%S'\n", testPath.ToWString()); - context->messageFormat(MessageType::RUN_ERROR, "failed to run test '%S'", testPath.ToWString().begin()); + context->messageFormat(TestMessageType::eRunError, "failed to run test '%S'", testPath.ToWString().begin()); } return err; } @@ -1078,7 +731,7 @@ TestResult runSimpleTest(TestContext* context, TestInput& input) if (spawnAndWait(context, outputStem, spawner) != kOSError_None) { - return kTestResult_Fail; + return TestResult::eFail; } String actualOutput = getOutput(spawner); @@ -1100,19 +753,19 @@ TestResult runSimpleTest(TestContext* context, TestInput& input) expectedOutput = "result code = 0\nstandard error = {\n}\nstandard output = {\n}\n"; } - TestResult result = kTestResult_Pass; + TestResult result = TestResult::ePass; // Otherwise we compare to the expected output if (actualOutput != expectedOutput) { context->dumpOutputDifference(expectedOutput, actualOutput); - result = kTestResult_Fail; + result = TestResult::eFail; } // If the test failed, then we write the actual output to a file // so that we can easily diff it from the command line and // diagnose the problem. - if (result == kTestResult_Fail) + if (result == TestResult::eFail) { String actualOutputPath = outputStem + ".actual"; Slang::File::WriteAllText(actualOutputPath, actualOutput); @@ -1140,7 +793,7 @@ TestResult runReflectionTest(TestContext* context, TestInput& input) if (spawnAndWait(context, outputStem, spawner) != kOSError_None) { - return kTestResult_Fail; + return TestResult::eFail; } String actualOutput = getOutput(spawner); @@ -1162,18 +815,18 @@ TestResult runReflectionTest(TestContext* context, TestInput& input) expectedOutput = "result code = 0\nstandard error = {\n}\nstandard output = {\n}\n"; } - TestResult result = kTestResult_Pass; + TestResult result = TestResult::ePass; // Otherwise we compare to the expected output if (actualOutput != expectedOutput) { - result = kTestResult_Fail; + result = TestResult::eFail; } // If the test failed, then we write the actual output to a file // so that we can easily diff it from the command line and // diagnose the problem. - if (result == kTestResult_Fail) + if (result == TestResult::eFail) { String actualOutputPath = outputStem + ".actual"; Slang::File::WriteAllText(actualOutputPath, actualOutput); @@ -1225,24 +878,24 @@ TestResult runEvalTest(TestContext* context, TestInput& input) if (spawnAndWait(context, outputStem, spawner) != kOSError_None) { - return kTestResult_Fail; + return TestResult::eFail; } String actualOutput = getOutput(spawner); String expectedOutput = getExpectedOutput(outputStem); - TestResult result = kTestResult_Pass; + TestResult result = TestResult::ePass; // Otherwise we compare to the expected output if (actualOutput != expectedOutput) { - result = kTestResult_Fail; + result = TestResult::eFail; } // If the test failed, then we write the actual output to a file // so that we can easily diff it from the command line and // diagnose the problem. - if (result == kTestResult_Fail) + if (result == TestResult::eFail) { String actualOutputPath = outputStem + ".actual"; Slang::File::WriteAllText(actualOutputPath, actualOutput); @@ -1281,7 +934,7 @@ TestResult runCrossCompilerTest(TestContext* context, TestInput& input) if (spawnAndWait(context, outputStem, expectedSpawner) != kOSError_None) { - return kTestResult_Fail; + return TestResult::eFail; } String expectedOutput = getOutput(expectedSpawner); @@ -1292,27 +945,27 @@ TestResult runCrossCompilerTest(TestContext* context, TestInput& input) } catch (Slang::IOException) { - return kTestResult_Fail; + return TestResult::eFail; } if (spawnAndWait(context, outputStem, actualSpawner) != kOSError_None) { - return kTestResult_Fail; + return TestResult::eFail; } String actualOutput = getOutput(actualSpawner); - TestResult result = kTestResult_Pass; + TestResult result = TestResult::ePass; // Otherwise we compare to the expected output if (actualOutput != expectedOutput) { - result = kTestResult_Fail; + result = TestResult::eFail; } // If the test failed, then we write the actual output to a file // so that we can easily diff it from the command line and // diagnose the problem. - if (result == kTestResult_Fail) + if (result == TestResult::eFail) { String actualOutputPath = outputStem + ".actual"; Slang::File::WriteAllText(actualOutputPath, actualOutput); @@ -1346,7 +999,7 @@ TestResult generateHLSLBaseline(TestContext* context, TestInput& input) if (spawnAndWait(context, outputStem, spawner) != kOSError_None) { - return kTestResult_Fail; + return TestResult::eFail; } String expectedOutput = getOutput(spawner); @@ -1357,9 +1010,9 @@ TestResult generateHLSLBaseline(TestContext* context, TestInput& input) } catch (Slang::IOException) { - return kTestResult_Fail; + return TestResult::eFail; } - return kTestResult_Pass; + return TestResult::ePass; } TestResult runHLSLComparisonTest(TestContext* context, TestInput& input) @@ -1394,7 +1047,7 @@ TestResult runHLSLComparisonTest(TestContext* context, TestInput& input) if (spawnAndWait(context, outputStem, spawner) != kOSError_None) { - return kTestResult_Fail; + return TestResult::eFail; } // We ignore output to stdout, and only worry about what the compiler @@ -1426,26 +1079,26 @@ TestResult runHLSLComparisonTest(TestContext* context, TestInput& input) { } - TestResult result = kTestResult_Pass; + TestResult result = TestResult::ePass; // If no expected output file was found, then we // expect everything to be empty if (expectedOutput.Length() == 0) { - if (resultCode != 0) result = kTestResult_Fail; - if (standardError.Length() != 0) result = kTestResult_Fail; - if (standardOutput.Length() != 0) result = kTestResult_Fail; + if (resultCode != 0) result = TestResult::eFail; + if (standardError.Length() != 0) result = TestResult::eFail; + if (standardOutput.Length() != 0) result = TestResult::eFail; } // Otherwise we compare to the expected output else if (actualOutput != expectedOutput) { - result = kTestResult_Fail; + result = TestResult::eFail; } // If the test failed, then we write the actual output to a file // so that we can easily diff it from the command line and // diagnose the problem. - if (result == kTestResult_Fail) + if (result == TestResult::eFail) { String actualOutputPath = outputStem + ".actual"; Slang::File::WriteAllText(actualOutputPath, actualOutput); @@ -1494,7 +1147,7 @@ TestResult doGLSLComparisonTestRun(TestContext* context, if (spawnAndWait(context, outputStem, spawner) != kOSError_None) { - return kTestResult_Fail; + return TestResult::eFail; } OSProcessSpawner::ResultCode resultCode = spawner.getResultCode(); @@ -1517,7 +1170,7 @@ TestResult doGLSLComparisonTestRun(TestContext* context, *outOutput = output; - return kTestResult_Pass; + return TestResult::ePass; } TestResult runGLSLComparisonTest(TestContext* context, TestInput& input) @@ -1534,17 +1187,17 @@ TestResult runGLSLComparisonTest(TestContext* context, TestInput& input) Slang::File::WriteAllText(outputStem + ".expected", expectedOutput); Slang::File::WriteAllText(outputStem + ".actual", actualOutput); - if( hlslResult == kTestResult_Fail ) return kTestResult_Fail; - if( slangResult == kTestResult_Fail ) return kTestResult_Fail; + if( hlslResult == TestResult::eFail ) return TestResult::eFail; + if( slangResult == TestResult::eFail ) return TestResult::eFail; if (actualOutput != expectedOutput) { context->dumpOutputDifference(expectedOutput, actualOutput); - return kTestResult_Fail; + return TestResult::eFail; } - return kTestResult_Pass; + return TestResult::ePass; } @@ -1557,7 +1210,7 @@ TestResult runComputeComparisonImpl(TestContext* context, TestInput& input, cons const String referenceOutput = findExpectedPath(input, ".expected.txt"); if (referenceOutput.Length() <= 0) { - return kTestResult_Fail; + return TestResult::eFail; } OSProcessSpawner spawner; @@ -1581,7 +1234,7 @@ TestResult runComputeComparisonImpl(TestContext* context, TestInput& input, cons if (spawnAndWait(context, outputStem, spawner) != kOSError_None) { printf("error spawning render-test\n"); - return kTestResult_Fail; + return TestResult::eFail; } auto actualOutput = getOutput(spawner); @@ -1593,7 +1246,7 @@ TestResult runComputeComparisonImpl(TestContext* context, TestInput& input, cons String actualOutputPath = outputStem + ".actual"; Slang::File::WriteAllText(actualOutputPath, actualOutput); - return kTestResult_Fail; + return TestResult::eFail; } // check against reference output @@ -1601,24 +1254,24 @@ TestResult runComputeComparisonImpl(TestContext* context, TestInput& input, cons { printf("render-test not producing expected outputs.\n"); printf("render-test output:\n%s\n", actualOutput.Buffer()); - return kTestResult_Fail; + return TestResult::eFail; } if (!File::Exists(referenceOutput)) { printf("referenceOutput %s not found.\n", referenceOutput.Buffer()); - return kTestResult_Fail; + return TestResult::eFail; } auto actualOutputContent = File::ReadAllText(actualOutputFile); auto actualProgramOutput = Split(actualOutputContent, '\n'); auto referenceProgramOutput = Split(File::ReadAllText(referenceOutput), '\n'); auto printOutput = [&]() { - context->messageFormat(MessageType::TEST_FAILURE, "output mismatch! actual output: {\n%s\n}, \n%s\n", actualOutputContent.Buffer(), actualOutput.Buffer()); + context->messageFormat(TestMessageType::eTestFailure, "output mismatch! actual output: {\n%s\n}, \n%s\n", actualOutputContent.Buffer(), actualOutput.Buffer()); }; if (actualProgramOutput.Count() < referenceProgramOutput.Count()) { printOutput(); - return kTestResult_Fail; + return TestResult::eFail; } for (int i = 0; i < (int)referenceProgramOutput.Count(); i++) { @@ -1632,13 +1285,13 @@ TestResult runComputeComparisonImpl(TestContext* context, TestInput& input, cons if (actual != uval) { printOutput(); - return kTestResult_Fail; + return TestResult::eFail; } else - return kTestResult_Pass; + return TestResult::ePass; } } - return kTestResult_Pass; + return TestResult::ePass; } TestResult runSlangComputeComparisonTest(TestContext* context, TestInput& input) @@ -1684,7 +1337,7 @@ TestResult doRenderComparisonTestRun(TestContext* context, TestInput& input, cha if (spawnAndWait(context, outputStem, spawner) != kOSError_None) { - return kTestResult_Fail; + return TestResult::eFail; } OSProcessSpawner::ResultCode resultCode = spawner.getResultCode(); @@ -1707,7 +1360,7 @@ TestResult doRenderComparisonTestRun(TestContext* context, TestInput& input, cha *outOutput = output; - return kTestResult_Pass; + return TestResult::ePass; } TestResult doImageComparison(TestContext* context, String const& filePath) @@ -1729,19 +1382,19 @@ TestResult doImageComparison(TestContext* context, String const& filePath) if(!expectedData) { - context->messageFormat(MessageType::RUN_ERROR, "Unable to load image ;%s'", expectedPath.Buffer()); - return kTestResult_Fail; + context->messageFormat(TestMessageType::eRunError, "Unable to load image ;%s'", expectedPath.Buffer()); + return TestResult::eFail; } if(!actualData) { - context->messageFormat(MessageType::RUN_ERROR, "Unable to load image '%s'", actualPath.Buffer()); - return kTestResult_Fail; + context->messageFormat(TestMessageType::eRunError, "Unable to load image '%s'", actualPath.Buffer()); + return TestResult::eFail; } if(expectedX != actualX || expectedY != actualY || expectedN != actualN) { - context->messageFormat(MessageType::TEST_FAILURE, "Images are different sizes '%s' '%s'", actualPath.Buffer(), expectedPath.Buffer()); - return kTestResult_Fail; + context->messageFormat(TestMessageType::eTestFailure, "Images are different sizes '%s' '%s'", actualPath.Buffer(), expectedPath.Buffer()); + return TestResult::eFail; } unsigned char* expectedCursor = expectedData; @@ -1781,7 +1434,7 @@ TestResult doImageComparison(TestContext* context, String const& filePath) // cases where vertex shader results lead to rendering that is off // by one pixel... - context->messageFormat(MessageType::TEST_FAILURE, "image compare failure at (%d,%d) channel %d. expected %d got %d (absolute error: %d, relative error: %f)\n", + context->messageFormat(TestMessageType::eTestFailure, "image compare failure at (%d,%d) channel %d. expected %d got %d (absolute error: %d, relative error: %f)\n", x, y, n, expectedVal, actualVal, @@ -1789,12 +1442,12 @@ TestResult doImageComparison(TestContext* context, String const& filePath) relativeDiff); // There was a difference we couldn't excuse! - return kTestResult_Fail; + return TestResult::eFail; } } } - return kTestResult_Pass; + return TestResult::ePass; } TestResult runHLSLRenderComparisonTestImpl( @@ -1815,23 +1468,23 @@ TestResult runHLSLRenderComparisonTestImpl( Slang::File::WriteAllText(outputStem + ".expected", expectedOutput); Slang::File::WriteAllText(outputStem + ".actual", actualOutput); - if( hlslResult == kTestResult_Fail ) return kTestResult_Fail; - if( slangResult == kTestResult_Fail ) return kTestResult_Fail; + if( hlslResult == TestResult::eFail ) return TestResult::eFail; + if( slangResult == TestResult::eFail ) return TestResult::eFail; if (actualOutput != expectedOutput) { context->dumpOutputDifference(expectedOutput, actualOutput); - return kTestResult_Fail; + return TestResult::eFail; } // Next do an image comparison on the expected output images! TestResult imageCompareResult = doImageComparison(context, outputStem); - if(imageCompareResult != kTestResult_Pass) + if(imageCompareResult != TestResult::ePass) return imageCompareResult; - return kTestResult_Pass; + return TestResult::ePass; } TestResult runHLSLRenderComparisonTest(TestContext* context, TestInput& input) @@ -1851,7 +1504,7 @@ TestResult runHLSLAndGLSLRenderComparisonTest(TestContext* context, TestInput& i TestResult skipTest(TestContext* /* context */, TestInput& /*input*/) { - return kTestResult_Ignored; + return TestResult::eIgnored; } @@ -1957,7 +1610,7 @@ TestResult runTest( // If this test can be ignored if (canIgnoreTestWithDisabledRenderer(testOptions)) { - return kTestResult_Ignored; + return TestResult::eIgnored; } // based on command name, dispatch to an appropriate callback @@ -2008,13 +1661,19 @@ TestResult runTest( testInput.testOptions = &testOptions; testInput.testList = &testList; - context->startTest(outputStem); - return context->endTest(ii->callback(context, testInput)); + { + TestContext::Scope scope(context, outputStem); + + TestResult testResult = ii->callback(context, testInput); + context->addResult(testResult); + + return testResult; + } } // No actual test runner found! - return kTestResult_Fail; + return TestResult::eFail; } bool testCategoryMatches( @@ -2073,7 +1732,7 @@ void runTestsOnFile( // Gather a list of tests to run FileTestList testList; - if( gatherTestsForFile(filePath, &testList) == kTestResult_Ignored ) + if( gatherTestsForFile(filePath, &testList) == TestResult::eIgnored ) { // Test was explicitly ignored return; @@ -2082,7 +1741,7 @@ void runTestsOnFile( // Note cases where a test file exists, but we found nothing to run if( testList.tests.Count() == 0 ) { - context->addResult(filePath, kTestResult_Ignored); + context->addTest(filePath, TestResult::eIgnored); return; } @@ -2233,62 +1892,6 @@ void runTestsInDirectory( } } -static void appendXmlEncode(char c, StringBuilder& out) -{ - switch (c) - { - case '&': out << "&"; break; - case '<': out << "<"; break; - case '>': out << ">"; break; - case '\'': out << "'"; break; - case '"': out << """; break; - default: out.Append(c); - } -} - -static bool isXmlEncodeChar(char c) -{ - switch (c) - { - case '&': - case '<': - case '>': - { - return true; - } - } - return false; -} - -static void appendXmlEncode(const String& in, StringBuilder& out) -{ - const char* cur = in.Buffer(); - const char* end = cur + in.Length(); - - while (cur < end) - { - const char* start = cur; - // Look for a run of non encoded - while (cur < end && !isXmlEncodeChar(*cur)) - { - cur++; - } - // Write it - if (cur > start) - { - out.Append(start, UInt(end - start)); - } - - // if not at the end, we must be on an xml encoded character, so just output it xml encoded. - if (cur < end) - { - const char encodeChar = *cur++; - assert(isXmlEncodeChar(encodeChar)); - appendXmlEncode(encodeChar, out); - } - } -} - // @@ -2330,7 +1933,7 @@ int main( // Exclude rendering tests when building under AppVeyor. // // TODO: this is very ad hoc, and we should do something cleaner. - if( g_options.outputMode == kOutputMode_AppVeyor ) + if( g_options.outputMode == TestOutputMode::eAppVeyor ) { g_options.excludeCategories.Add(renderTestCategory, renderTestCategory); g_options.excludeCategories.Add(vulkanTestCategory, vulkanTestCategory); @@ -2342,104 +1945,37 @@ int main( context.m_dumpOutputOnFailure = g_options.dumpOutputOnFailure; context.m_isVerbose = g_options.shouldBeVerbose; - // Enumerate test files according to policy - // TODO: add more directories to this list - // TODO: allow for a command-line argument to select a particular directory - runTestsInDirectory(&context, "tests/"); - - auto passCount = context.m_passedTestCount; - auto rawTotal = context.m_totalTestCount; - auto ignoredCount = context.m_ignoredTestCount; - - auto runTotal = rawTotal - ignoredCount; - - switch (g_options.outputMode) + if (g_options.unitTests) { - default: - { - if (!context.m_totalTestCount) - { - printf("no tests run\n"); - return 0; - } - - printf("\n===\n%d%% of tests passed (%d/%d)", (passCount*100) / runTotal, passCount, runTotal); - if(ignoredCount) - { - printf(", %d tests ignored", ignoredCount); - } - printf("\n===\n\n"); + TestContext::set(&context); - if(context.m_failedTestCount) - { - printf("failing tests:\n"); - printf("---\n"); - for(const auto& testInfo : context.m_testInfos) - { - if (testInfo.testResult == kTestResult_Fail) - { - printf("%s\n", testInfo.name.Buffer()); - } - } - printf("---\n"); - } - break; - } - case kOutputMode_xUnit: + // Run the unit tests + TestRegister* cur = TestRegister::s_first; + while (cur) { - // xUnit 1.0 format - - printf("\n"); - printf("\n", context.m_totalTestCount, context.m_failedTestCount, context.m_ignoredTestCount); - printf(" \n", context.m_totalTestCount, context.m_failedTestCount, context.m_ignoredTestCount); + context.startTest(cur->m_name); - for (const auto& testInfo : context.m_testInfos) - { - const int numFailed = (testInfo.testResult == kTestResult_Fail); - const int numIgnored = (testInfo.testResult == kTestResult_Ignored); - //int numPassed = (testInfo.testResult == kTestResult_Pass); + // Run the test function + cur->m_func(); - if (testInfo.testResult == kTestResult_Pass) - { - printf(" \n", testInfo.name.Buffer()); - } - else - { - printf(" \n", testInfo.name.Buffer()); - switch (testInfo.testResult) - { - case kTestResult_Fail: - { - StringBuilder buf; - appendXmlEncode(testInfo.message, buf); - - printf(" \n"); - printf("%s", buf.Buffer()); - printf(" \n"); - break; - } - case kTestResult_Ignored: - { - printf(" Ignored\n"); - break; - } - default: break; - } - printf(" \n"); - } - } + context.endTest(); - printf(" \n"); - printf("\n"); - break; - } - case kOutputMode_xUnit2: - { - // https://xunit.github.io/docs/format-xml-v2 - assert("Not currently supported"); - break; + // Next + cur = cur->m_next; } + + TestContext::set(nullptr); } + else + { + // Enumerate test files according to policy + // TODO: add more directories to this list + // TODO: allow for a command-line argument to select a particular directory + runTestsInDirectory(&context, "tests/"); + + } + + context.outputSummary(); - return passCount == runTotal ? 0 : 1; + return context.didAllSucceed() ? 0 : 1; } diff --git a/tools/slang-test/slang-test.vcxproj b/tools/slang-test/slang-test.vcxproj index 9524529bb..b94f2f627 100644 --- a/tools/slang-test/slang-test.vcxproj +++ b/tools/slang-test/slang-test.vcxproj @@ -164,11 +164,15 @@ + + + + diff --git a/tools/slang-test/slang-test.vcxproj.filters b/tools/slang-test/slang-test.vcxproj.filters index f22903aa6..3d80416f8 100644 --- a/tools/slang-test/slang-test.vcxproj.filters +++ b/tools/slang-test/slang-test.vcxproj.filters @@ -1,4 +1,4 @@ - + @@ -15,6 +15,9 @@ Header Files + + Header Files + @@ -26,5 +29,14 @@ Source Files + + Source Files + + + Source Files + + + Source Files + \ No newline at end of file diff --git a/tools/slang-test/test-context.cpp b/tools/slang-test/test-context.cpp new file mode 100644 index 000000000..f77262001 --- /dev/null +++ b/tools/slang-test/test-context.cpp @@ -0,0 +1,450 @@ +// test-context.cpp +#include "test-context.h" + +#include "os.h" +#include "../../source/core/slang-string-util.h" + +#include +#include +#include + +using namespace Slang; + +/* static */TestContext* TestContext::s_context = nullptr; +/* static */TestRegister* TestRegister::s_first; + +static void appendXmlEncode(char c, StringBuilder& out) +{ + switch (c) + { + case '&': out << "&"; break; + case '<': out << "<"; break; + case '>': out << ">"; break; + case '\'': out << "'"; break; + case '"': out << """; break; + default: out.Append(c); + } +} + +static bool isXmlEncodeChar(char c) +{ + switch (c) + { + case '&': + case '<': + case '>': + { + return true; + } + } + return false; +} + +static void appendXmlEncode(const String& in, StringBuilder& out) +{ + const char* cur = in.Buffer(); + const char* end = cur + in.Length(); + + while (cur < end) + { + const char* start = cur; + // Look for a run of non encoded + while (cur < end && !isXmlEncodeChar(*cur)) + { + cur++; + } + // Write it + if (cur > start) + { + out.Append(start, UInt(end - start)); + } + + // if not at the end, we must be on an xml encoded character, so just output it xml encoded. + if (cur < end) + { + const char encodeChar = *cur++; + assert(isXmlEncodeChar(encodeChar)); + appendXmlEncode(encodeChar, out); + } + } +} + +TestContext::TestContext(TestOutputMode outputMode) : + m_outputMode(outputMode) +{ + m_totalTestCount = 0; + m_passedTestCount = 0; + m_failedTestCount = 0; + m_ignoredTestCount = 0; + + m_maxTestResults = 10; + + m_inTest = false; + m_dumpOutputOnFailure = false; + m_isVerbose = false; +} + +bool TestContext::canWriteStdError() const +{ + switch (m_outputMode) + { + case TestOutputMode::eXUnit: + case TestOutputMode::eXUnit2: + { + return false; + } + default: return true; + } +} + +void TestContext::startTest(const String& testName) +{ + assert(!m_inTest); + m_inTest = true; + + m_numCurrentResults = 0; + m_currentInfo = TestInfo(); + m_currentInfo.name = testName; + m_currentMessage.Clear(); +} + +void TestContext::endTest() +{ + assert(m_inTest); + + m_currentInfo.message = m_currentMessage; + + _addResult(m_currentInfo); + + m_inTest = false; +} + +void TestContext::addResult(TestResult result) +{ + assert(m_inTest); + + m_currentInfo.testResult = combine(m_currentInfo.testResult, result); + m_numCurrentResults++; +} + +void TestContext::addResultWithLocation(TestResult result, const char* testText, const char* file, int line) +{ + assert(m_inTest); + m_numCurrentResults++; + + m_currentInfo.testResult = combine(m_currentInfo.testResult, result); + if (result != TestResult::eFail) + { + // We don't need to output the result if it + return; + } + + if (m_maxTestResults > 0) + { + if (m_numCurrentResults > m_maxTestResults) + { + if (m_numCurrentResults == m_maxTestResults + 1) + { + message(TestMessageType::eInfo, "..."); + } + return; + } + } + + StringBuilder buf; + buf << testText << " - " << file << " (" << line << ")"; + + message(TestMessageType::eTestFailure, buf); +} + +void TestContext::addResultWithLocation(bool testSucceeded, const char* testText, const char* file, int line) +{ + addResultWithLocation(testSucceeded ? TestResult::ePass : TestResult::eFail, testText, file, line); +} + +TestResult TestContext::addTest(const String& testName, bool isPass) +{ + const TestResult res = isPass ? TestResult::ePass : TestResult::eFail; + addTest(testName, res); + return res; +} + +void TestContext::dumpOutputDifference(const String& expectedOutput, const String& actualOutput) +{ + StringBuilder builder; + + StringUtil::appendFormat(builder, + "ERROR:\n" + "EXPECTED{{{\n%s}}}\n" + "ACTUAL{{{\n%s}}}\n", + expectedOutput.Buffer(), + actualOutput.Buffer()); + + + if (m_dumpOutputOnFailure && canWriteStdError()) + { + fprintf(stderr, "%s", builder.Buffer()); + fflush(stderr); + } + + // Add to the m_currentInfo + message(TestMessageType::eTestFailure, builder); +} + +void TestContext::_addResult(const TestInfo& info) +{ + m_totalTestCount++; + + switch (info.testResult) + { + case TestResult::eFail: + m_failedTestCount++; + break; + + case TestResult::ePass: + m_passedTestCount++; + break; + + case TestResult::eIgnored: + m_ignoredTestCount++; + break; + + default: + assert(!"unexpected"); + break; + } + + m_testInfos.Add(info); + + // printf("OUTPUT_MODE: %d\n", options.outputMode); + switch (m_outputMode) + { + default: + { + char const* resultString = "UNEXPECTED"; + switch (info.testResult) + { + case TestResult::eFail: resultString = "FAILED"; break; + case TestResult::ePass: resultString = "passed"; break; + case TestResult::eIgnored: resultString = "ignored"; break; + default: + assert(!"unexpected"); + break; + } + printf("%s test: '%S'\n", resultString, info.name.ToWString().begin()); + break; + } + case TestOutputMode::eXUnit2: + case TestOutputMode::eXUnit: + { + // Don't output anything -> we'll output all in one go at the end + break; + } + case TestOutputMode::eAppVeyor: + { + char const* resultString = "None"; + switch (info.testResult) + { + case TestResult::eFail: resultString = "Failed"; break; + case TestResult::ePass: resultString = "Passed"; break; + case TestResult::eIgnored: resultString = "Ignored"; break; + default: + assert(!"unexpected"); + break; + } + + OSProcessSpawner spawner; + spawner.pushExecutableName("appveyor"); + spawner.pushArgument("AddTest"); + spawner.pushArgument(info.name); + spawner.pushArgument("-FileName"); + // TODO: this isn't actually a file name in all cases + spawner.pushArgument(info.name); + spawner.pushArgument("-Framework"); + spawner.pushArgument("slang-test"); + spawner.pushArgument("-Outcome"); + spawner.pushArgument(resultString); + + auto err = spawner.spawnAndWaitForCompletion(); + + if (err != kOSError_None) + { + messageFormat(TestMessageType::eInfo, "failed to add appveyor test results for '%S'\n", info.name.ToWString().begin()); + +#if 0 + fprintf(stderr, "[%d] TEST RESULT: %s {%d} {%s} {%s}\n", err, spawner.commandLine_.Buffer(), + spawner.getResultCode(), + spawner.getStandardOutput().begin(), + spawner.getStandardError().begin()); +#endif + } + + break; + } + } +} + +void TestContext::addTest(const String& testName, TestResult testResult) +{ + // Can't add this way if in test + assert(!m_inTest); + + TestInfo info; + info.name = testName; + info.testResult = testResult; + _addResult(info); +} + +void TestContext::message(TestMessageType type, const String& message) +{ + if (type == TestMessageType::eInfo) + { + if (m_isVerbose && canWriteStdError()) + { + fputs(message.Buffer(), stderr); + } + + // Just dump out if can dump out + return; + } + + if (canWriteStdError()) + { + if (type == TestMessageType::eRunError || type == TestMessageType::eTestFailure) + { + fprintf(stderr, "error: "); + fputs(message.Buffer(), stderr); + fprintf(stderr, "\n"); + } + else + { + fputs(message.Buffer(), stderr); + } + } + + if (m_currentMessage.Length() > 0) + { + m_currentMessage << "\n"; + } + m_currentMessage.Append(message); +} + +void TestContext::messageFormat(TestMessageType type, char const* format, ...) +{ + StringBuilder builder; + + va_list args; + va_start(args, format); + StringUtil::append(format, args, builder); + va_end(args); + + message(type, builder); +} + +bool TestContext::didAllSucceed() const +{ + return m_passedTestCount == (m_totalTestCount - m_ignoredTestCount); +} + +void TestContext::outputSummary() +{ + auto passCount = m_passedTestCount; + auto rawTotal = m_totalTestCount; + auto ignoredCount = m_ignoredTestCount; + + auto runTotal = rawTotal - ignoredCount; + + switch (m_outputMode) + { + default: + { + if (!m_totalTestCount) + { + printf("no tests run\n"); + return; + } + + int percentPassed = 0; + if (runTotal > 0) + { + percentPassed = (passCount * 100) / runTotal; + } + + printf("\n===\n%d%% of tests passed (%d/%d)", percentPassed, passCount, runTotal); + if (ignoredCount) + { + printf(", %d tests ignored", ignoredCount); + } + printf("\n===\n\n"); + + if (m_failedTestCount) + { + printf("failing tests:\n"); + printf("---\n"); + for (const auto& testInfo : m_testInfos) + { + if (testInfo.testResult == TestResult::eFail) + { + printf("%s\n", testInfo.name.Buffer()); + } + } + printf("---\n"); + } + break; + } + case TestOutputMode::eXUnit: + { + // xUnit 1.0 format + + printf("\n"); + printf("\n", m_totalTestCount, m_failedTestCount, m_ignoredTestCount); + printf(" \n", m_totalTestCount, m_failedTestCount, m_ignoredTestCount); + + for (const auto& testInfo : m_testInfos) + { + const int numFailed = (testInfo.testResult == TestResult::eFail); + const int numIgnored = (testInfo.testResult == TestResult::eIgnored); + //int numPassed = (testInfo.testResult == TestResult::ePass); + + if (testInfo.testResult == TestResult::ePass) + { + printf(" \n", testInfo.name.Buffer()); + } + else + { + printf(" \n", testInfo.name.Buffer()); + switch (testInfo.testResult) + { + case TestResult::eFail: + { + StringBuilder buf; + appendXmlEncode(testInfo.message, buf); + + printf(" \n"); + printf("%s", buf.Buffer()); + printf(" \n"); + break; + } + case TestResult::eIgnored: + { + printf(" Ignored\n"); + break; + } + default: break; + } + printf(" \n"); + } + } + + printf(" \n"); + printf("\n"); + break; + } + case TestOutputMode::eXUnit2: + { + // https://xunit.github.io/docs/format-xml-v2 + assert("Not currently supported"); + break; + } + } +} \ No newline at end of file diff --git a/tools/slang-test/test-context.h b/tools/slang-test/test-context.h new file mode 100644 index 000000000..22042a4e6 --- /dev/null +++ b/tools/slang-test/test-context.h @@ -0,0 +1,139 @@ +// test-context.h + +#include "../../source/core/slang-string-util.h" + +#define SLANG_CHECK(x) TestContext::get()->addResultWithLocation((x), #x, __FILE__, __LINE__); + +struct TestRegister +{ + typedef void (*TestFunc)(); + + TestRegister(const char* name, TestFunc func): + m_next(s_first), + m_name(name), + m_func(func) + { + s_first = this; + } + + TestFunc m_func; + const char* m_name; + TestRegister* m_next; + + static TestRegister* s_first; +}; + +#define SLANG_UNIT_TEST(name, func) static TestRegister s_unitTest##__LINE__(name, func) + +enum class TestOutputMode +{ + eDefault = 0, ///< Default mode is to write test results to the console + eAppVeyor, ///< For AppVeyor continuous integration + eTravis, ///< We currently don't specialize for Travis, but maybe we should. + eXUnit, ///< xUnit original format https://nose.readthedocs.io/en/latest/plugins/xunit.html + eXUnit2, ///< https://xunit.github.io/docs/format-xml-v2 +}; + +enum class TestResult +{ + eIgnored, + ePass, + eFail, +}; + +enum class TestMessageType +{ + eInfo, ///< General info (may not be shown depending on verbosity setting) + eTestFailure, ///< Describes how a test failure took place + eRunError, ///< Describes an error that caused a test not to actually correctly run +}; + +class TestContext +{ + public: + + struct TestInfo + { + TestResult testResult = TestResult::eIgnored; + Slang::String name; + Slang::String message; ///< Message that is specific for the testResult + }; + + class Scope + { + public: + Scope(TestContext* context, const Slang::String& testName) : + m_context(context) + { + context->startTest(testName); + } + ~Scope() + { + m_context->endTest(); + } + + protected: + TestContext* m_context; + }; + + void startTest(const Slang::String& testName); + void addResult(TestResult result); + void addResultWithLocation(TestResult result, const char* testText, const char* file, int line); + void addResultWithLocation(bool testSucceeded, const char* testText, const char* file, int line); + + void endTest(); + + /// Runs start/endTest and outputs the result + TestResult addTest(const Slang::String& testName, bool isPass); + /// Effectively runs start/endTest (so cannot be called inside start/endTest). + void addTest(const Slang::String& testName, TestResult testResult); + + // Called for an error in the test-runner (not for an error involving a test itself). + void message(TestMessageType type, const Slang::String& errorText); + void messageFormat(TestMessageType type, char const* message, ...); + + void dumpOutputDifference(const Slang::String& expectedOutput, const Slang::String& actualOutput); + + /// True if can write output directly to stderr + bool canWriteStdError() const; + + /// Call at end of tests + void outputSummary(); + + /// Returns true if all run tests succeeded + bool didAllSucceed() const; + + /// Ctor + TestContext(TestOutputMode outputMode); + + static TestResult combine(TestResult a, TestResult b) { return (a > b) ? a : b; } + + static TestContext* get() { return s_context; } + static void set(TestContext* context) { s_context = context; } + + Slang::List m_testInfos; + + int m_totalTestCount; + int m_passedTestCount; + int m_failedTestCount; + int m_ignoredTestCount; + + int m_maxTestResults; ///< Maximum amount of results per test. If 0 it's infinite. + + TestOutputMode m_outputMode = TestOutputMode::eDefault; + bool m_dumpOutputOnFailure; + bool m_isVerbose; + +protected: + void _addResult(const TestInfo& info); + + Slang::StringBuilder m_currentMessage; + TestInfo m_currentInfo; + int m_numCurrentResults; + + bool m_inTest; + + static TestContext* s_context; +}; + + diff --git a/tools/slang-test/unit-test-free-list.cpp b/tools/slang-test/unit-test-free-list.cpp new file mode 100644 index 000000000..649c59571 --- /dev/null +++ b/tools/slang-test/unit-test-free-list.cpp @@ -0,0 +1,55 @@ +// unit-test-free-list.cpp + +#include "../../source/core/slang-free-list.h" + +#include +#include +#include + +#include "test-context.h" + +#include "../../source/core/slang-random-generator.h" +#include "../../source/core/list.h" + +using namespace Slang; + +static void freeListUnitTest() +{ + FreeList freeList; + freeList.init(sizeof(int), sizeof(void*), 10); + + DefaultRandomGenerator randGen(0x24343); + + List allocs; + + for (int i = 0; i < 1000; i++) + { + const int numAlloc = randGen.nextInt32UpTo(20); + + for (int j = 0; j < numAlloc; j++) + { + int* ptr = (int*)freeList.allocate(); + *ptr = i; + allocs.Add(ptr); + } + + int numDealloc = randGen.nextInt32UpTo(19); + numDealloc = int(allocs.Count()) < numDealloc ? int(allocs.Count()) : numDealloc; + + for (int j = 0; j < numDealloc; j++) + { + const int index = randGen.nextInt32UpTo(int(allocs.Count())); + + int* alloc = allocs[index]; + + SLANG_CHECK(*alloc <= i); + SLANG_CHECK(*alloc >= 0); + + freeList.deallocate(alloc); + + allocs.FastRemoveAt(index); + } + } +} + +SLANG_UNIT_TEST("FreeList", freeListUnitTest); \ No newline at end of file diff --git a/tools/slang-test/unit-test-memory-arena.cpp b/tools/slang-test/unit-test-memory-arena.cpp new file mode 100644 index 000000000..69eed520a --- /dev/null +++ b/tools/slang-test/unit-test-memory-arena.cpp @@ -0,0 +1,242 @@ +// unit-test-free-list.cpp + +#include "../../source/core/slang-memory-arena.h" + +#include +#include +#include + +#include "test-context.h" + +#include "../../source/core/slang-random-generator.h" +#include "../../source/core/list.h" + +using namespace Slang; + + +namespace // anonymous +{ + +struct Block +{ + void* m_data; + size_t m_size; + uint8_t m_value; +}; + +enum class TestMode +{ + eUnaligned, + eImplicitAligned, ///< Alignment is kept implicitly with Unaligned allocs of the right size + eDefaultAligned, + eExplicitAligned, + eCount, +}; + +} // anonymous + +static size_t getAlignment(TestMode mode) +{ + switch (mode) + { + default: + case TestMode::eUnaligned: + return 1; + case TestMode::eExplicitAligned: + return 16; + case TestMode::eImplicitAligned: + return 32; + case TestMode::eDefaultAligned: + return MemoryArena::kMinAlignment; + } +} + +static bool hasValueShort(const uint8_t* data, size_t size, uint8_t value) +{ + for (size_t i = 0; i < size; ++i) + { + if (data[i] != value) + { + return false; + } + } + return true; +} + +static bool hasValue(const uint8_t* data, size_t size, uint8_t value) +{ + const size_t alignMask = sizeof(size_t) - 1; + + if (size <= sizeof(size_t) * 2) + { + return hasValueShort(data, size, value); + } + + if (size_t(data) & alignMask) + { + size_t firstSize = sizeof(size_t) - (size_t(data) & alignMask); + if (!hasValueShort(data, firstSize, value)) + { + return false; + } + size -= firstSize; + data += firstSize; + + assert((size_t(data) & alignMask) == 0); + } + + // Now do the middle + size_t numWords = size / sizeof(size_t); + + // Expand the byte up to a word size + size_t wordValue = (size_t(value) << 8) | value; + wordValue = (wordValue << 16) | wordValue; + wordValue = (sizeof(size_t) > 4) ? ((wordValue << 32) | wordValue) : wordValue; + + const size_t* wordData = (const size_t*)data; + for (size_t i = 0; i < numWords; ++i) + { + if (wordData[i] != wordValue) + { + return false; + } + } + + // Do the end piece + return hasValueShort(data + sizeof(size_t) * numWords, size & alignMask, value); +} + +static void memoryArenaUnitTest() +{ + DefaultRandomGenerator randGen(0x5346536a); + + { + const size_t blockSize = 1024; + MemoryArena arena; + arena.init(blockSize); + + List blocks; + + blocks.Add(arena.allocate(100)); + blocks.Add(arena.allocate(blockSize * 2)); + blocks.Add(arena.allocate(100)); + blocks.Add(arena.allocate(blockSize * 2)); + blocks.Add(arena.allocate(100)); + + while (blocks.Count()) + { + arena.deallocateLast(blocks.Last()); + blocks.RemoveLast(); + } + } + + { + + const size_t blockSize = 1024; + + for (TestMode mode = TestMode(0); int(mode) < int(TestMode::eCount); mode = TestMode(int(mode) + 1)) + { + const size_t alignment = getAlignment(mode); + + MemoryArena arena; + arena.init(blockSize, alignment); + + List blocks; + + for (int i = 0; i < 1000; i++) + { + int var = randGen.nextInt32() & 0x3ff; + if (var < 3 && blocks.Count() > 0) + { + if (var == 0) + { + // Do a single dealloc + arena.deallocateLast(blocks.Last().m_data); + blocks.RemoveLast(); + } + else if (var == 1) + { + // Deallocate everything + arena.deallocateAll(); + blocks.Clear(); + } + else + { + // Do a multiple dealloc + int index = randGen.nextInt32UpTo(int(blocks.Count())); + + // Deallocate all afterwards + arena.deallocateAllFrom(blocks[index].m_data); + + blocks.SetSize(index); + } + } + else + { + size_t sizeInBytes = (randGen.nextInt32() & 255) + 1; + + // Lets go for an oversized block + if ((randGen.nextInt32() & 0xff) < 2) + { + sizeInBytes += blockSize; + } + + const uint8_t value = uint8_t(randGen.nextInt32()); + + void* mem = nullptr; + switch (mode) + { + default: + case TestMode::eUnaligned: + { + mem = arena.allocateUnaligned(sizeInBytes); + break; + } + case TestMode::eImplicitAligned: + { + // Fix the size to get implicit alignment + sizeInBytes = (sizeInBytes & ~(alignment - 1)) + alignment; + mem = arena.allocateUnaligned(sizeInBytes); + break; + } + case TestMode::eExplicitAligned: + { + mem = arena.allocateAligned(sizeInBytes, alignment); + break; + } + case TestMode::eDefaultAligned: + { + mem = arena.allocate(sizeInBytes); + break; + } + } + + // Check it is aligned + SLANG_CHECK((size_t(mem) & (alignment - 1)) == 0); + + ::memset(mem, value, sizeInBytes); + + Block block; + + block.m_data = mem; + block.m_size = sizeInBytes; + block.m_value = value; + + blocks.Add(block); + } + + // Check the blocks + for (int j = 0; j < int(blocks.Count()); ++j) + { + const Block& block = blocks[j]; + + SLANG_CHECK(arena.isValid(block.m_data, block.m_size)); + + SLANG_CHECK(hasValue((uint8_t*)block.m_data, block.m_size, block.m_value)); + } + } + } + } +} + +SLANG_UNIT_TEST("MemoryArena", memoryArenaUnitTest); \ No newline at end of file -- cgit v1.2.3