+#include <spirv-tools/libspirv.hpp>
#include <msp/core/algorithm.h>
#include <msp/fs/dir.h>
#include <msp/fs/utils.h>
#include <msp/strings/utils.h>
#include <msp/test/test.h>
-#include <msp/io/print.h>
-
-class GlslCompilerTest: public Msp::Test::RegisteredTest<GlslCompilerTest>
+class GlslCompilerHelper
{
-private:
+protected:
struct TestCase
{
std::string name;
std::string source;
+ Msp::GL::SL::Compiler::Mode compile_mode;
+ std::map<std::string, int> spec_values;
std::map<Msp::GL::SL::Stage::Type, std::string> expected_output;
+ std::string expected_diagnostic;
+ bool expect_success;
+
+ TestCase(): expect_success(true) { }
};
std::list<TestCase> test_cases;
+ void load_all_test_cases(const Msp::FS::Path &);
+ const TestCase &load_test_case(const std::string &);
+
+ void verify_output(const std::string &, const std::string &);
+ void verify_diagnostic(const std::string &, const std::string &);
+ std::string extract_line(const std::string &, const std::string::const_iterator &);
+ virtual void fail(const std::string &) = 0;
+};
+
+class GlslCompilerTest: public Msp::Test::RegisteredTest<GlslCompilerTest>, private GlslCompilerHelper
+{
public:
GlslCompilerTest();
static const char *get_name() { return "GLSL compiler"; }
private:
- const TestCase &load_test_case(const std::string &);
+ void run_test_case(const TestCase *);
+ virtual void fail(const std::string &m) { Test::fail(m); }
+};
+
+class GlslCompilerIdempotence: public Msp::Test::RegisteredTest<GlslCompilerIdempotence>, private GlslCompilerHelper
+{
+public:
+ GlslCompilerIdempotence();
+
+ static const char *get_name() { return "GLSL compiler idempotence"; }
+
+private:
+ void run_test_case(const TestCase *);
+ virtual void fail(const std::string &m) { Test::fail(m); }
+};
+
+class GlslCompilerSpirV: public Msp::Test::RegisteredTest<GlslCompilerSpirV>, private GlslCompilerHelper
+{
+private:
+ spvtools::SpirvTools spirv_tools;
+
+public:
+ GlslCompilerSpirV();
+ static const char *get_name() { return "GLSL to SPIR-V compilation"; }
+
+private:
void run_test_case(const TestCase *);
+ void diagnostic(spv_message_level_t, const char *, const spv_position_t &, const char *);
+ virtual void fail(const std::string &m) { Test::fail(m); }
};
using namespace std;
using namespace Msp;
-GlslCompilerTest::GlslCompilerTest()
+void GlslCompilerHelper::load_all_test_cases(const FS::Path &tests_dir)
{
- FS::Path tests_dir = "glsl";
- list<string> test_files = FS::list_filtered(tests_dir, "\\.glsl$");
- test_files.sort();
+ vector<string> test_files = FS::list_filtered(tests_dir, "\\.glsl$");
+ sort(test_files);
for(const auto &fn: test_files)
load_test_case((tests_dir/fn).str());
-
- for(const auto &tc: test_cases)
- add(&GlslCompilerTest::run_test_case, &tc, tc.name);
}
-const GlslCompilerTest::TestCase &GlslCompilerTest::load_test_case(const string &fn)
+const GlslCompilerHelper::TestCase &GlslCompilerHelper::load_test_case(const string &fn)
{
IO::BufferedFile file(fn);
TestCase test_case;
test_case.name = FS::basename(fn);
+ test_case.compile_mode = GL::SL::Compiler::PROGRAM;
string *target = &test_case.source;
while(!file.eof())
{
if(line=="*/")
continue;
- string::size_type expected = line.find("Expected output:");
- if(expected!=string::npos)
+ string::size_type pos = line.find("Expected output:");
+ if(pos!=string::npos)
{
- string stage = strip(line.substr(expected+16));
+ string stage = strip(line.substr(pos+16));
if(stage=="vertex")
target = &test_case.expected_output[GL::SL::Stage::VERTEX];
else if(stage=="geometry")
continue;
}
+ pos = line.find("Expected error:");
+ if(pos==string::npos)
+ pos = line.find("Expected diagnostic:");
+ if(pos!=string::npos)
+ {
+ target = &test_case.expected_diagnostic;
+ test_case.expect_success = (line[pos+9]!='e');
+ continue;
+ }
+
+ pos = line.find("Compile mode:");
+ if(pos!=string::npos)
+ {
+ string mode = strip(line.substr(pos+13));
+ if(mode=="module")
+ test_case.compile_mode = GL::SL::Compiler::MODULE;
+ else if(mode=="program")
+ test_case.compile_mode = GL::SL::Compiler::PROGRAM;
+ else
+ throw runtime_error("Unknown compile mode "+mode);
+ continue;
+ }
+
+ pos = line.find("Specialize:");
+ if(pos!=string::npos)
+ {
+ vector<string> parts = split(line.substr(pos+11));
+ int value = 0;
+ if(parts[1]=="true")
+ value = 1;
+ else if(parts[1]=="false")
+ value = 0;
+ else
+ value = lexical_cast<int>(parts[1]);
+ test_case.spec_values[parts[0]] = value;
+ continue;
+ }
+
*target += line;
*target += '\n';
}
return test_cases.back();
}
+void GlslCompilerHelper::verify_output(const string &output, const string &expected)
+{
+ GL::SL::Tokenizer tokenizer;
+ tokenizer.begin(output, "<output>");
+
+ GL::SL::Tokenizer expected_tkn;
+ expected_tkn.begin(expected, "<expected>");
+
+ while(1)
+ {
+ string token = expected_tkn.parse_token();
+
+ try
+ {
+ tokenizer.expect(token);
+ }
+ catch(const GL::SL::invalid_shader_source &exc)
+ {
+ fail(exc.what());
+ }
+
+ if(token.empty())
+ break;
+ }
+}
+
+void GlslCompilerHelper::verify_diagnostic(const string &output, const string &expected)
+{
+ auto i = output.begin();
+ auto j = expected.begin();
+ bool space = true;
+ while(i!=output.end() && j!=expected.end())
+ {
+ if(*i==*j)
+ {
+ ++i;
+ ++j;
+ }
+ else if(isspace(*i) && isspace(*j))
+ {
+ ++i;
+ ++j;
+ space = true;
+ }
+ else if(space && isspace(*i))
+ ++i;
+ else if(space && isspace(*j))
+ ++j;
+ else
+ {
+ string out_line = extract_line(output, i);
+ string expect_line = extract_line(expected, j);
+ fail(format("Incorrect diagnostic line:\n%s\nExpected:\n%s", out_line, expect_line));
+ }
+ }
+
+ while(i!=output.end() && isspace(*i))
+ ++i;
+ while(j!=expected.end() && isspace(*j))
+ ++j;
+
+ if(i!=output.end())
+ fail(format("Extra diagnostic line: %s", extract_line(output, i)));
+ if(j!=expected.end())
+ fail(format("Missing diagnostic line: %s", extract_line(expected, j)));
+}
+
+string GlslCompilerHelper::extract_line(const string &text, const string::const_iterator &iter)
+{
+ string::const_iterator begin = iter;
+ for(; (begin!=text.begin() && *begin!='\n'); --begin) ;
+ if(*begin=='\n')
+ ++begin;
+ string::const_iterator end = iter;
+ for(; (end!=text.end() && *end!='\n'); ++end) ;
+ return string(begin, end);
+}
+
+
+GlslCompilerTest::GlslCompilerTest()
+{
+ load_all_test_cases("glsl");
+ for(const auto &tc: test_cases)
+ add(&GlslCompilerTest::run_test_case, &tc, tc.name);
+}
+
void GlslCompilerTest::run_test_case(const TestCase *test_case)
{
- GL::SL::Compiler compiler(GL::SL::Features::all());
- compiler.set_source(test_case->source, "<test>");
- compiler.compile(GL::SL::Compiler::PROGRAM);
+ GL::SL::Compiler compiler(GL::SL::Features::opengl_latest());
+ try
+ {
+ compiler.set_source(test_case->source, "<test>");
+ if(test_case->compile_mode==GL::SL::Compiler::PROGRAM)
+ compiler.specialize(test_case->spec_values);
+ compiler.compile(test_case->compile_mode);
+ }
+ catch(const GL::SL::invalid_shader_source &exc)
+ {
+ if(!test_case->expect_success)
+ {
+ debug("Errors from compile:");
+ debug(exc.what());
+ verify_diagnostic(exc.what(), test_case->expected_diagnostic);
+ return;
+ }
+ throw;
+ }
+
+ if(!test_case->expect_success)
+ fail("Error expected but none thrown");
+
+ verify_diagnostic(compiler.get_diagnostics(), test_case->expected_diagnostic);
auto stages = compiler.get_stages();
for(auto s: stages)
for(unsigned j=0; j<lines.size(); ++j)
debug(format("%3d: %s", j+1, lines[j]));
- GL::SL::Tokenizer tokenizer;
- tokenizer.begin(output, "<output>");
-
- GL::SL::Tokenizer expected_tkn;
- expected_tkn.begin(i->second, "<expected>");
-
- while(1)
- {
- string token = expected_tkn.parse_token();
-
- try
- {
- tokenizer.expect(token);
- }
- catch(const GL::SL::invalid_shader_source &exc)
- {
- fail(exc.what());
- }
-
- if(token.empty())
- break;
- }
+ verify_output(output, i->second);
}
for(const auto &s: test_case->expected_output)
if(find(stages, s.first)==stages.end())
fail(format("Compiler didn't produce stage %s", GL::SL::Stage::get_stage_name(s.first)));
}
+
+
+GlslCompilerIdempotence::GlslCompilerIdempotence()
+{
+ load_all_test_cases("glsl");
+ for(const auto &tc: test_cases)
+ if(tc.expect_success)
+ add(&GlslCompilerIdempotence::run_test_case, &tc, tc.name);
+}
+
+void GlslCompilerIdempotence::run_test_case(const TestCase *test_case)
+{
+ GL::SL::Compiler compiler(GL::SL::Features::opengl_latest());
+ compiler.set_source(test_case->source, "<test>");
+ if(test_case->compile_mode==GL::SL::Compiler::PROGRAM)
+ compiler.specialize(test_case->spec_values);
+ compiler.compile(test_case->compile_mode);
+
+ GL::SL::Compiler compiler2(GL::SL::Features::opengl_latest());
+ compiler2.set_source(compiler.get_combined_glsl(), "<loopback>");
+ compiler2.compile(test_case->compile_mode);
+
+ auto stages = compiler.get_stages();
+ auto stages2 = compiler2.get_stages();
+ auto i = stages.begin();
+ auto j = stages2.begin();
+ for(; (i!=stages.end() && j!=stages2.end() && *i==*j); ++i, ++j)
+ {
+ string output = compiler.get_stage_glsl(*i);
+ string output2 = compiler2.get_stage_glsl(*j);
+
+ verify_output(output2, output);
+ }
+
+ if(i!=stages.end())
+ fail(format("Second pass didn't produce stage %s", GL::SL::Stage::get_stage_name(*i)));
+ if(j!=stages2.end())
+ fail(format("Second pass produced extra stage %s", GL::SL::Stage::get_stage_name(*j)));
+}
+
+
+GlslCompilerSpirV::GlslCompilerSpirV():
+ spirv_tools(SPV_ENV_UNIVERSAL_1_5)
+{
+ load_all_test_cases("glsl");
+ for(const auto &tc: test_cases)
+ if(tc.expect_success)
+ add(&GlslCompilerSpirV::run_test_case, &tc, tc.name);
+
+ using namespace std::placeholders;
+ spirv_tools.SetMessageConsumer(std::bind(std::mem_fn(&GlslCompilerSpirV::diagnostic), this, _1, _2, _3, _4));
+}
+
+void GlslCompilerSpirV::run_test_case(const TestCase *test_case)
+{
+ GL::SL::Compiler compiler(GL::SL::Features::opengl_latest());
+ compiler.set_source(test_case->source, "<test>");
+ compiler.compile(GL::SL::Compiler::SPIRV);
+
+ vector<uint32_t> code = compiler.get_combined_spirv();
+ if(!spirv_tools.Validate(code))
+ fail("Invalid SPIR-V generated");
+}
+
+void GlslCompilerSpirV::diagnostic(spv_message_level_t level, const char *, const spv_position_t &, const char *message)
+{
+ const char *prefix;
+ switch(level)
+ {
+ case SPV_MSG_DEBUG: prefix = "debug: "; break;
+ case SPV_MSG_INFO: prefix = "info: "; break;
+ case SPV_MSG_WARNING: prefix = "warning: "; break;
+ case SPV_MSG_ERROR: prefix = "error: "; break;
+ default: prefix = "";
+ }
+ info(format("%s%s", prefix, message));
+}