/* Copyright (C) 2024 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* 0 A.D. is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with 0 A.D. If not, see .
*/
#include "lib/self_test.h"
#include "graphics/PreprocessorWrapper.h"
#include "lib/timer.h"
#include "ps/CStr.h"
#include "third_party/ogre3d_preprocessor/OgreGLSLPreprocessor.h"
#include
#include
class TestPreprocessor : public CxxTest::TestSuite
{
public:
void setUp()
{
Ogre::CPreprocessor::ErrorHandler = CPreprocessorWrapper::PyrogenesisShaderError;
}
struct PreprocessorResult
{
CStr8 output;
CStr8 loggerOutput;
};
// Replaces consecutive spaces/tabs by single space/tab.
CStr CompressWhiteSpaces(const CStr& source)
{
CStr result;
for (char ch : source)
if (!std::isblank(ch) || (result.empty() || result.back() != ch))
result += ch;
return result;
}
PreprocessorResult Parse(const char* in)
{
PreprocessorResult result;
TestLogger logger;
Ogre::CPreprocessor preproc;
size_t len = 0;
char* out = preproc.Parse(in, strlen(in), len);
result.output = std::string(out, len);
result.loggerOutput = logger.GetOutput();
// Free output if it's not inside the source string
if (!(out >= in && out < in + strlen(in)))
free(out);
return result;
}
PreprocessorResult ParseWithIncludes(const char* in, CPreprocessorWrapper::IncludeRetrieverCallback includeCallback)
{
PreprocessorResult result;
TestLogger logger;
CPreprocessorWrapper preprocessor(includeCallback);
result.output = preprocessor.Preprocess(in);
result.loggerOutput = logger.GetOutput();
return result;
}
void test_basic()
{
PreprocessorResult result = Parse("#define TEST 2\n1+1=TEST\n");
TS_ASSERT_EQUALS(result.output, "\n1+1=2\n");
}
void test_error()
{
PreprocessorResult result = Parse("foo\n#if ()\nbar\n");
TS_ASSERT_EQUALS(result.output, "");
TS_ASSERT_STR_CONTAINS(result.loggerOutput, "ERROR: Preprocessor error: line 2: Unclosed parenthesis in #if expression\n");
}
void test_else()
{
PreprocessorResult result = Parse(R"(
#define FOO 0
#if FOO
#define BAR 0
#else
#define BAR 42
#endif
BAR
)");
TS_ASSERT_EQUALS(result.output.Trim(PS_TRIM_BOTH), "42");
}
void test_elif()
{
PreprocessorResult result = Parse(R"(
#define FOO 0
#if FOO
#define BAR 0
#elif 1
#define BAR 42
#endif
BAR
)");
TS_ASSERT_EQUALS(result.output.Trim(PS_TRIM_BOTH), "42");
}
void test_nested_macro()
{
PreprocessorResult result = Parse(R"(
#define FOO(a, b, c, d) func1(a, b + (c * r), 0)
#define BAR func2
FOO(x, y, BAR(u, v), w)
)");
TS_ASSERT_EQUALS(result.output.Trim(PS_TRIM_BOTH), "func1(x, y + (func2(u, v) * r), 0)");
}
void test_division_by_zero_error()
{
PreprocessorResult result = Parse(R"(
#if 2 / 0
42
#endif
)");
TS_ASSERT_EQUALS(result.output.Trim(PS_TRIM_BOTH), "");
TS_ASSERT_STR_CONTAINS(result.loggerOutput, "ERROR: Preprocessor error: line 2: Division by zero");
}
void test_include()
{
bool includeRetrieved = false;
CPreprocessorWrapper::IncludeRetrieverCallback includeCallback = [&includeRetrieved](
const CStr& includePath, CStr& out) {
TS_ASSERT_EQUALS(includePath, "test.h");
out = "42";
includeRetrieved = true;
return true;
};
PreprocessorResult result = ParseWithIncludes(
R"(#include "test.h" // Includes something.)", includeCallback);
TS_ASSERT(includeRetrieved);
TS_ASSERT_EQUALS(result.output.Trim(PS_TRIM_BOTH), "#line 1\n42\n#line 2");
}
void test_include_double()
{
CPreprocessorWrapper::IncludeRetrieverCallback includeCallback = [](
const CStr& includePath, CStr& out) {
TS_ASSERT_EQUALS(includePath, "test.h");
out = "42";
return true;
};
PreprocessorResult result = ParseWithIncludes(R"(
#include "test.h"
#include "test.h"
#include "test.h"
)", includeCallback);
TS_ASSERT_EQUALS(result.output.Trim(PS_TRIM_BOTH), "#line 1\n42\n#line 3\n#line 1\n42\n#line 4\n#line 1\n42\n#line 5");
}
void test_include_double_with_guards()
{
CPreprocessorWrapper::IncludeRetrieverCallback includeCallback = [](
const CStr& includePath, CStr& out) {
TS_ASSERT_EQUALS(includePath, "test.h");
out = R"(#ifndef INCLUDED_TEST
#define INCLUDED_TEST
42
#endif)";
return true;
};
PreprocessorResult result = ParseWithIncludes(R"(
#include "test.h"
#include "test.h"
#include "test.h"
)", includeCallback);
TS_ASSERT_EQUALS(result.output.Trim(PS_TRIM_BOTH), "#line 1\n\n\t\t\t\t\n\t\t\t\t42\n\t\t\t\n#line 3\n#line 1\n\n\n\n\n#line 4\n#line 1\n\n\n\n\n#line 5");
}
void test_include_invalid_argument()
{
int includeRetrievedCounter = 0;
CPreprocessorWrapper::IncludeRetrieverCallback includeCallback = [&includeRetrievedCounter](
const CStr& UNUSED(includePath), CStr& out) {
out = "42";
++includeRetrievedCounter;
return true;
};
PreprocessorResult result = ParseWithIncludes(R"(
#include
#include test.h
)", includeCallback);
TS_ASSERT_EQUALS(includeRetrievedCounter, 0);
}
void test_include_invalid_file()
{
CPreprocessorWrapper::IncludeRetrieverCallback includeCallback = [](
const CStr& UNUSED(includePath), CStr& UNUSED(out)) {
return false;
};
PreprocessorResult result = ParseWithIncludes(R"(
#include "missed_file.h"
)", includeCallback);
TS_ASSERT_STR_CONTAINS(result.loggerOutput, "ERROR: Preprocessor error: line 2: Can't load #include file: 'missed_file.h'");
}
void test_include_with_defines()
{
CPreprocessorWrapper::IncludeRetrieverCallback includeCallback = [](
const CStr& UNUSED(includePath), CStr& out) {
out = R"(
#if defined(A)
#define X 41
#elif defined(B)
#define X 42
#else
#define X 43
#endif
#ifdef Y
#undef Y
#define Y 256
#endif
vec3 color();
)";
return true;
};
PreprocessorResult result = ParseWithIncludes(R"(
#define Y 128
#define B 1
#include "test.h"
X Y
)", includeCallback);
TS_ASSERT_EQUALS(
CompressWhiteSpaces(result.output.Trim(PS_TRIM_BOTH)),
"#line 1\n\n\t\n\n\n\t\n\t\n\n\n\t\n\t\n\t\n\t\n\tvec3 color();\n\t\n#line 5\n\t42 256");
}
void DISABLED_test_performance()
{
CPreprocessorWrapper::IncludeRetrieverCallback includeCallback = [](
const CStr& includePath, CStr& out) {
const size_t dotPosition = includePath.find('.');
TS_ASSERT_DIFFERS(dotPosition, CStr::npos);
const int depth = CStr(includePath.substr(0, dotPosition)).ToInt();
TS_ASSERT_LESS_THAN_EQUALS(0, depth);
if (depth < 4)
{
std::stringstream nextIncludes;
for (int idx = 0; idx < 16; ++idx)
nextIncludes << "#include \"" << depth + 1 << ".h\"\n";
out = nextIncludes.str();
}
else
{
out = R"(
42
)";
}
return true;
};
const double start = timer_Time();
PreprocessorResult result = ParseWithIncludes(R"(
#include "0.h"
)", includeCallback);
const double finish = timer_Time();
printf("Total: %lfs\n", finish - start);
TS_ASSERT_EQUALS(result.output.Trim(PS_TRIM_BOTH).size(), 2075304u);
}
};