[ATOM-13679] ShaderAssetBuilder: Create UnitTest To Validate (#3096)
* [ATOM-13679] ShaderAssetBuilder: Create UnitTest To Validate STDOUT Data Capture From MCPP azvsnprintf was being used improperly, in particular in windows if the data to print was larger than the local buffer in the stack, then azvsnprintf returns -1. Also azvsnprintf needs a +1 in buffer size to accomodate for the '\0' character at the end and that was not done. Added UnitTest to validate all cases: 1. Data to print is smaller than the local buffer. 2. Data to print is the same size as the local buffer. 3. Data to print is bigger than the local buffer. Signed-off-by: garrieta <garrieta@amazon.com> * Fix for MacOS & Linux, they require va_start to be called each time azvsnprintf is called Signed-off-by: garrieta <garrieta@amazon.com>
This commit is contained in:
@@ -8,11 +8,6 @@
|
||||
|
||||
#include <AzCore/PlatformDef.h>
|
||||
|
||||
#define MCPP_DLL_IMPORT 1
|
||||
#define MCPP_DONT_USE_SHORT_NAMES 1
|
||||
#include <mcpp_lib.h>
|
||||
#undef MCPP_DLL_IMPORT
|
||||
|
||||
#include <CommonFiles/Preprocessor.h>
|
||||
|
||||
#include <AzCore/std/string/string.h>
|
||||
@@ -31,8 +26,6 @@
|
||||
|
||||
#include <AtomCore/Serialization/Json/JsonUtils.h>
|
||||
|
||||
#include <sstream>
|
||||
|
||||
namespace AZ
|
||||
{
|
||||
namespace ShaderBuilder
|
||||
@@ -83,124 +76,125 @@ namespace AZ
|
||||
}
|
||||
}
|
||||
|
||||
//! Binder helper to Matsui C-Pre-Processor library
|
||||
class McppBinder
|
||||
///////////////////////////////////////////////////////////////////////
|
||||
// McppBinder starts
|
||||
bool McppBinder::StartPreprocessWithCommandLine(int argc, const char* argv[])
|
||||
{
|
||||
public:
|
||||
McppBinder(PreprocessorData& out, bool plugERR)
|
||||
: m_outputData(out),
|
||||
m_plugERR(plugERR)
|
||||
int errorCode = mcpp_lib_main(argc, argv);
|
||||
// convert from std::ostringstring to AZStd::string
|
||||
m_outputData.code = m_outStream.str().c_str();
|
||||
m_outputData.diagnostics = m_errStream.str().c_str();
|
||||
return errorCode == 0;
|
||||
}
|
||||
|
||||
int McppBinder::Putc_StaticHinge(int c, MCPP_OUTDEST od)
|
||||
{
|
||||
char asString[2] = { aznumeric_cast<char>(c), 0 };
|
||||
return Fputs_StaticHinge(asString, od);
|
||||
}
|
||||
|
||||
int McppBinder::Fputs_StaticHinge(const char* s, MCPP_OUTDEST od)
|
||||
{
|
||||
if (!OkToLog(od))
|
||||
{
|
||||
// single live instance
|
||||
s_mcppExclusiveProtection.lock();
|
||||
s_currentInstance = this;
|
||||
SetupMcppCallbacks();
|
||||
return 0;
|
||||
}
|
||||
~McppBinder()
|
||||
// chose the proper stream
|
||||
auto& selectedStream = od == MCPP_OUT ? s_currentInstance->m_outStream : s_currentInstance->m_errStream;
|
||||
auto tellBefore = selectedStream.tellp();
|
||||
// append that message to it
|
||||
selectedStream << s;
|
||||
return aznumeric_cast<int>(selectedStream.tellp() - tellBefore);
|
||||
}
|
||||
|
||||
int McppBinder::Fprintf_StaticHinge(MCPP_OUTDEST od, const char* format, ...)
|
||||
{
|
||||
if (!OkToLog(od))
|
||||
{
|
||||
s_currentInstance = nullptr;
|
||||
s_mcppExclusiveProtection.unlock();
|
||||
return 0;
|
||||
}
|
||||
// run the formatting on stack memory first, in case it's enough
|
||||
char localBuffer[DefaultFprintfBufferSize];
|
||||
|
||||
bool StartPreprocessWithCommandLine(int argc, const char* argv[])
|
||||
va_list args;
|
||||
|
||||
va_start(args, format);
|
||||
int count = azvsnprintf(localBuffer, DefaultFprintfBufferSize, format, args);
|
||||
va_end(args);
|
||||
|
||||
char* result = localBuffer;
|
||||
|
||||
// @result will be bound to @biggerData in case @localBuffer is not big enough.
|
||||
std::unique_ptr<char[]> biggerData;
|
||||
// ">=" is the right comparison because in case count == bufferSize
|
||||
// We will need an extra byte to accomodate the '\0' ending character.
|
||||
if (count >= DefaultFprintfBufferSize)
|
||||
{
|
||||
int errorCode = mcpp_lib_main(argc, argv);
|
||||
// convert from std::ostringstring to AZStd::string
|
||||
m_outputData.code = m_outStream.str().c_str();
|
||||
m_outputData.diagnostics = m_errStream.str().c_str();
|
||||
return errorCode == 0;
|
||||
}
|
||||
|
||||
private:
|
||||
|
||||
// ====== C-API compatible "Static Hinges" (plain free functions) ======
|
||||
// : capturing-lambdas, function-objects, bind-expression; can't be decayed to function pointers,
|
||||
// because they hold runtime-dynamic type-erased states. So we need intermediates
|
||||
|
||||
// entry point from mcpp. hijacking its output
|
||||
static int Putc_StaticHinge(int c, MCPP_OUTDEST od)
|
||||
{
|
||||
char asString[2] = { aznumeric_cast<char>(c), 0 };
|
||||
return Fputs_StaticHinge(asString, od);
|
||||
}
|
||||
|
||||
// entry point from mcpp. hijacking its output
|
||||
static int Fputs_StaticHinge(const char* s, MCPP_OUTDEST od)
|
||||
{
|
||||
if (!OkToLog(od))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
// chose the proper stream
|
||||
auto& selectedStream = od == MCPP_OUT ? s_currentInstance->m_outStream : s_currentInstance->m_errStream;
|
||||
auto tellBefore = selectedStream.tellp();
|
||||
// append that message to it
|
||||
selectedStream << s;
|
||||
return aznumeric_cast<int>(selectedStream.tellp() - tellBefore);
|
||||
}
|
||||
|
||||
// entry point from mcpp. hijacking its output
|
||||
static int Fprintf_StaticHinge(MCPP_OUTDEST od, const char* format, ...)
|
||||
{
|
||||
if (!OkToLog(od))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
// run the formatting on stack memory first, in case it's enough
|
||||
constexpr int bufferSize = 256;
|
||||
char localBuffer[bufferSize];
|
||||
va_list args;
|
||||
// There wasn't enough space in the local store.
|
||||
count++; // vsnprintf returns a size that doesn't include the null character.
|
||||
biggerData.reset(new char[count]);
|
||||
result = &biggerData[0];
|
||||
|
||||
// Remark: for MacOS & Linux it is important to call va_start again before
|
||||
// each call to azvsnprintf. Not required for Windows.
|
||||
va_start(args, format);
|
||||
int count = azvsnprintf(localBuffer, 256, format, args);
|
||||
AZStd::unique_ptr<char[]> biggerData; // will be bound to a bigger array if necessary.
|
||||
char* result = localBuffer;
|
||||
if (count > bufferSize)
|
||||
{ // there wasn't enough space in the local store.
|
||||
biggerData.reset(new char[count]);
|
||||
result = &biggerData[0]; // change `result`'s pointee
|
||||
count = azvsnprintf(result, count, format, args);
|
||||
}
|
||||
AZ_Error("Preprocessor", count >= 0, "String formatting of pre-precessor output failed");
|
||||
count = azvsnprintf(result, count, format, args);
|
||||
va_end(args);
|
||||
return Fputs_StaticHinge(result, od);
|
||||
}
|
||||
|
||||
static void IncludeReport_StaticHinge(FILE*, const char*, const char*, const char* path)
|
||||
else if (count == -1)
|
||||
{
|
||||
s_currentInstance->m_outputData.includedPaths.insert(path);
|
||||
// In Windows azvsnprintf will always return -1 if @localBuffer is not big enough,
|
||||
// But it will write in @localBuffer what it could.
|
||||
// See:
|
||||
// https://docs.microsoft.com/en-us/cpp/c-runtime-library/reference/vsnprintf-vsnprintf-vsnprintf-l-vsnwprintf-vsnwprintf-l?view=msvc-160
|
||||
// In particular: "If the number of characters to write is greater than count,
|
||||
// these functions return -1 indicating that output has been truncated."
|
||||
|
||||
// There wasn't enough space in the local store.
|
||||
// Remark: for MacOS & Linux it is important to call va_start again before
|
||||
// each call to azvsnprintf. Not required for Windows.
|
||||
va_start(args, format);
|
||||
count = azvscprintf(format, args) + 1; // vscprintf returns a size that doesn't include the null character.
|
||||
va_end(args);
|
||||
|
||||
biggerData.reset(new char[count]);
|
||||
result = &biggerData[0];
|
||||
|
||||
va_start(args, format);
|
||||
count = azvsnprintf(result, count, format, args);
|
||||
va_end(args);
|
||||
}
|
||||
|
||||
// ====== utility methods =====
|
||||
AZ_Error("Preprocessor", count >= 0, "String formatting of pre-precessor output failed");
|
||||
return Fputs_StaticHinge(result, od);
|
||||
}
|
||||
|
||||
static bool OkToLog(MCPP_OUTDEST od)
|
||||
{
|
||||
bool isErrButOk = od == MCPP_ERR && s_currentInstance->m_plugERR;
|
||||
return od == MCPP_OUT || isErrButOk;
|
||||
}
|
||||
void McppBinder::IncludeReport_StaticHinge(FILE*, const char*, const char*, const char* path)
|
||||
{
|
||||
s_currentInstance->m_outputData.includedPaths.insert(path);
|
||||
}
|
||||
|
||||
static void SetupMcppCallbacks()
|
||||
{
|
||||
// callback for header included notification
|
||||
mcpp_set_report_include_callback(IncludeReport_StaticHinge);
|
||||
// callback for output redirection
|
||||
mcpp_set_out_func(Putc_StaticHinge, Fputs_StaticHinge, Fprintf_StaticHinge);
|
||||
}
|
||||
bool McppBinder::OkToLog(MCPP_OUTDEST od)
|
||||
{
|
||||
bool isErrButOk = od == MCPP_ERR && s_currentInstance->m_plugERR;
|
||||
return od == MCPP_OUT || isErrButOk;
|
||||
}
|
||||
|
||||
// ====== instance data ======
|
||||
PreprocessorData& m_outputData;
|
||||
std::ostringstream m_outStream, m_errStream;
|
||||
bool m_plugERR;
|
||||
|
||||
// ====== shared data ======
|
||||
// MCPP is a library with tons of non TLS global states, it can only be accessed by one client at a time.
|
||||
static AZStd::mutex s_mcppExclusiveProtection;
|
||||
static McppBinder* s_currentInstance;
|
||||
};
|
||||
void McppBinder::SetupMcppCallbacks()
|
||||
{
|
||||
// callback for header included notification
|
||||
mcpp_set_report_include_callback(IncludeReport_StaticHinge);
|
||||
// callback for output redirection
|
||||
mcpp_set_out_func(Putc_StaticHinge, Fputs_StaticHinge, Fprintf_StaticHinge);
|
||||
}
|
||||
|
||||
// definitions for the linker
|
||||
AZStd::mutex McppBinder::s_mcppExclusiveProtection;
|
||||
McppBinder* McppBinder::s_currentInstance = nullptr;
|
||||
|
||||
// McppBinder ends
|
||||
///////////////////////////////////////////////////////////////////////
|
||||
|
||||
bool PreprocessFile(const AZStd::string& fullPath, PreprocessorData& outputData, const PreprocessorOptions& options
|
||||
, bool collectDiagnostics, bool preprocessIncludedFiles)
|
||||
{
|
||||
|
||||
@@ -14,6 +14,18 @@
|
||||
#include <AzCore/RTTI/ReflectContext.h>
|
||||
#include <AzCore/Serialization/DataPatch.h>
|
||||
|
||||
#define MCPP_DLL_IMPORT 1
|
||||
#define MCPP_DONT_USE_SHORT_NAMES 1
|
||||
#include <mcpp_lib.h>
|
||||
#undef MCPP_DLL_IMPORT
|
||||
|
||||
#include <sstream>
|
||||
|
||||
namespace UnitTest
|
||||
{
|
||||
class McppBinderTests;
|
||||
}
|
||||
|
||||
namespace AZ
|
||||
{
|
||||
namespace ShaderBuilder
|
||||
@@ -93,5 +105,64 @@ namespace AZ
|
||||
AZStd::string& sourceCode,
|
||||
AZStd::string newFileOrigin);
|
||||
|
||||
//! Binder helper to Matsui C-Pre-Processor library
|
||||
class McppBinder
|
||||
{
|
||||
public:
|
||||
McppBinder(PreprocessorData& out, bool plugERR)
|
||||
: m_outputData(out)
|
||||
, m_plugERR(plugERR)
|
||||
{
|
||||
// single live instance
|
||||
s_mcppExclusiveProtection.lock();
|
||||
s_currentInstance = this;
|
||||
SetupMcppCallbacks();
|
||||
}
|
||||
~McppBinder()
|
||||
{
|
||||
s_currentInstance = nullptr;
|
||||
s_mcppExclusiveProtection.unlock();
|
||||
}
|
||||
|
||||
// This constant is in the header so McppBinderTests can see it.
|
||||
static constexpr int DefaultFprintfBufferSize = 256;
|
||||
|
||||
bool StartPreprocessWithCommandLine(int argc, const char* argv[]);
|
||||
|
||||
private:
|
||||
friend class ::UnitTest::McppBinderTests;
|
||||
|
||||
// ====== C-API compatible "Static Hinges" (plain free functions) ======
|
||||
// : capturing-lambdas, function-objects, bind-expression; can't be decayed to function pointers,
|
||||
// because they hold runtime-dynamic type-erased states. So we need intermediates
|
||||
|
||||
// entry point from mcpp. hijacking its output
|
||||
static int Putc_StaticHinge(int c, MCPP_OUTDEST od);
|
||||
|
||||
// entry point from mcpp. hijacking its output
|
||||
static int Fputs_StaticHinge(const char* s, MCPP_OUTDEST od);
|
||||
|
||||
// entry point from mcpp. hijacking its output
|
||||
static int Fprintf_StaticHinge(MCPP_OUTDEST od, const char* format, ...);
|
||||
|
||||
static void IncludeReport_StaticHinge(FILE*, const char*, const char*, const char* path);
|
||||
|
||||
// ====== utility methods =====
|
||||
|
||||
static bool OkToLog(MCPP_OUTDEST od);
|
||||
|
||||
static void SetupMcppCallbacks();
|
||||
|
||||
// ====== instance data ======
|
||||
PreprocessorData& m_outputData;
|
||||
std::ostringstream m_outStream, m_errStream;
|
||||
bool m_plugERR;
|
||||
|
||||
// ====== shared data ======
|
||||
// MCPP is a library with tons of non TLS global states, it can only be accessed by one client at a time.
|
||||
static AZStd::mutex s_mcppExclusiveProtection;
|
||||
static McppBinder* s_currentInstance;
|
||||
};
|
||||
|
||||
} // ShaderBuilder
|
||||
} // AZ
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
/*
|
||||
* Copyright (c) Contributors to the Open 3D Engine Project.
|
||||
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||
*
|
||||
*/
|
||||
|
||||
#include <AzTest/AzTest.h>
|
||||
#include <AzCore/UnitTest/TestTypes.h>
|
||||
|
||||
#include "Common/ShaderBuilderTestFixture.h"
|
||||
|
||||
#include <CommonFiles/Preprocessor.h>
|
||||
|
||||
namespace UnitTest
|
||||
{
|
||||
using namespace AZ;
|
||||
|
||||
// The main purpose of this class is to test ShaderBuilder::McppBinder::Fprintf_StaticHinge()
|
||||
// Which has three common scenarios to validate.
|
||||
// 1- The formatted string is expected to yield less bytes than McppBinder::DefaultFprintfBufferSize.
|
||||
// 2- The formatted string is expected to yield exactly McppBinder::DefaultFprintfBufferSize number of bytes.
|
||||
// 3- The formatted string is expectedc to yield more bytes than McppBinder::DefaultFprintfBufferSize.
|
||||
class McppBinderTests : public ShaderBuilderTestFixture
|
||||
{
|
||||
public:
|
||||
|
||||
// Fills @buffer with 'a' to 'z' for up to @bufferSize number of bytes.
|
||||
// This function will null('\0') char terminate @buffer.
|
||||
void FillBufferWithAlphabet(char* buffer, int bufferSize)
|
||||
{
|
||||
for (int bufferPos = 0, rollback = 0; bufferPos < (bufferSize - 1); ++bufferPos)
|
||||
{
|
||||
const char value = 'a' + rollback++;
|
||||
buffer[bufferPos] = value;
|
||||
if (value == 'z')
|
||||
{
|
||||
rollback = 0;
|
||||
}
|
||||
}
|
||||
buffer[bufferSize - 1] = '\0';
|
||||
}
|
||||
|
||||
// Pushes the null terminated string, @inputString, into McppBinder capture stream
|
||||
// using McppBinder::Fprintf_StaticHinge().
|
||||
// Returns the content of the McppBinder capture stream as a string.
|
||||
AZStd::string PrintStringThroughStaticHinge(const char* inputString)
|
||||
{
|
||||
ShaderBuilder::PreprocessorData preprocessorData;
|
||||
ShaderBuilder::McppBinder mcppBinder(preprocessorData, false);
|
||||
ShaderBuilder::McppBinder::Fprintf_StaticHinge(MCPP_OUTDEST::MCPP_OUT, "%s", inputString);
|
||||
// convert from std::ostringstring to AZStd::string
|
||||
return AZStd::string(mcppBinder.m_outStream.str().c_str());
|
||||
}
|
||||
}; // class McppBinderTests
|
||||
|
||||
|
||||
TEST_F(McppBinderTests, ShouldPrintLessBytesThanDefaultSize)
|
||||
{
|
||||
constexpr int bufferSize = (ShaderBuilder::McppBinder::DefaultFprintfBufferSize / 2) + 1;
|
||||
EXPECT_TRUE(bufferSize > 0);
|
||||
char buffer[bufferSize] = "";
|
||||
FillBufferWithAlphabet(buffer, bufferSize);
|
||||
auto printedString = PrintStringThroughStaticHinge(buffer);
|
||||
EXPECT_EQ(AZStd::string(buffer), printedString);
|
||||
}
|
||||
|
||||
TEST_F(McppBinderTests, ShouldPrintSameBytesAsDefaultSize)
|
||||
{
|
||||
constexpr int bufferSize = ShaderBuilder::McppBinder::DefaultFprintfBufferSize + 1;
|
||||
EXPECT_TRUE(bufferSize > 0);
|
||||
char buffer[bufferSize] = "";
|
||||
FillBufferWithAlphabet(buffer, bufferSize);
|
||||
auto printedString = PrintStringThroughStaticHinge(buffer);
|
||||
EXPECT_EQ(AZStd::string(buffer), printedString);
|
||||
}
|
||||
|
||||
TEST_F(McppBinderTests, ShouldPrintMoreBytesThanDefaultSize)
|
||||
{
|
||||
constexpr int bufferSize = (ShaderBuilder::McppBinder::DefaultFprintfBufferSize * 2) + 1;
|
||||
EXPECT_TRUE(bufferSize > 0);
|
||||
char buffer[bufferSize] = "";
|
||||
FillBufferWithAlphabet(buffer, bufferSize);
|
||||
auto printedString = PrintStringThroughStaticHinge(buffer);
|
||||
EXPECT_EQ(AZStd::string(buffer), printedString);
|
||||
}
|
||||
|
||||
} //namespace UnitTest
|
||||
|
||||
//AZ_UNIT_TEST_HOOK(DEFAULT_UNIT_TEST_ENV);
|
||||
|
||||
@@ -10,4 +10,5 @@ set(FILES
|
||||
Tests/Common/ShaderBuilderTestFixture.h
|
||||
Tests/Common/ShaderBuilderTestFixture.cpp
|
||||
Tests/SupervariantCmdArgumentTests.cpp
|
||||
Tests/McppBinderTests.cpp
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user