// Copyright (c) 2015-2016 The Khronos Group Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. #include #include #include #include #include #include "gmock/gmock.h" #include "source/latest_version_opencl_std_header.h" #include "source/message.h" #include "source/table.h" #include "test/test_fixture.h" #include "test/unit_spirv.h" // Returns true if two spv_parsed_operand_t values are equal. // To use this operator, this definition must appear in the same namespace // as spv_parsed_operand_t. static bool operator==(const spv_parsed_operand_t& a, const spv_parsed_operand_t& b) { return a.offset == b.offset && a.num_words == b.num_words && a.type == b.type && a.number_kind == b.number_kind && a.number_bit_width == b.number_bit_width; } namespace spvtools { namespace { using ::spvtest::Concatenate; using ::spvtest::MakeInstruction; using ::spvtest::MakeVector; using ::spvtest::ScopedContext; using ::testing::_; using ::testing::AnyOf; using ::testing::Eq; using ::testing::InSequence; using ::testing::Return; // An easily-constructible and comparable object for the contents of an // spv_parsed_instruction_t. Unlike spv_parsed_instruction_t, owns the memory // of its components. struct ParsedInstruction { explicit ParsedInstruction(const spv_parsed_instruction_t& inst) : words(inst.words, inst.words + inst.num_words), opcode(static_cast(inst.opcode)), ext_inst_type(inst.ext_inst_type), type_id(inst.type_id), result_id(inst.result_id), operands(inst.operands, inst.operands + inst.num_operands) {} std::vector words; SpvOp opcode; spv_ext_inst_type_t ext_inst_type; uint32_t type_id; uint32_t result_id; std::vector operands; bool operator==(const ParsedInstruction& b) const { return words == b.words && opcode == b.opcode && ext_inst_type == b.ext_inst_type && type_id == b.type_id && result_id == b.result_id && operands == b.operands; } }; // Prints a ParsedInstruction object to the given output stream, and returns // the stream. std::ostream& operator<<(std::ostream& os, const ParsedInstruction& inst) { os << "\nParsedInstruction( {"; spvtest::PrintTo(spvtest::WordVector(inst.words), &os); os << "}, opcode: " << int(inst.opcode) << " ext_inst_type: " << int(inst.ext_inst_type) << " type_id: " << inst.type_id << " result_id: " << inst.result_id; for (const auto& operand : inst.operands) { os << " { offset: " << operand.offset << " num_words: " << operand.num_words << " type: " << int(operand.type) << " number_kind: " << int(operand.number_kind) << " number_bit_width: " << int(operand.number_bit_width) << "}"; } os << ")"; return os; } // Sanity check for the equality operator on ParsedInstruction. TEST(ParsedInstruction, ZeroInitializedAreEqual) { spv_parsed_instruction_t pi = {}; ParsedInstruction a(pi); ParsedInstruction b(pi); EXPECT_THAT(a, ::testing::TypedEq(b)); } // Googlemock class receiving Header/Instruction calls from spvBinaryParse(). class MockParseClient { public: MOCK_METHOD6(Header, spv_result_t(spv_endianness_t endian, uint32_t magic, uint32_t version, uint32_t generator, uint32_t id_bound, uint32_t reserved)); MOCK_METHOD1(Instruction, spv_result_t(const ParsedInstruction&)); }; // Casts user_data as MockParseClient and invokes its Header(). spv_result_t invoke_header(void* user_data, spv_endianness_t endian, uint32_t magic, uint32_t version, uint32_t generator, uint32_t id_bound, uint32_t reserved) { return static_cast(user_data)->Header( endian, magic, version, generator, id_bound, reserved); } // Casts user_data as MockParseClient and invokes its Instruction(). spv_result_t invoke_instruction( void* user_data, const spv_parsed_instruction_t* parsed_instruction) { return static_cast(user_data)->Instruction( ParsedInstruction(*parsed_instruction)); } // The SPIR-V module header words for the Khronos Assembler generator, // for a module with an ID bound of 1. const uint32_t kHeaderForBound1[] = { SpvMagicNumber, SpvVersion, SPV_GENERATOR_WORD(SPV_GENERATOR_KHRONOS_ASSEMBLER, 0), 1 /*bound*/, 0 /*schema*/}; // Returns the expected SPIR-V module header words for the Khronos // Assembler generator, and with a given Id bound. std::vector ExpectedHeaderForBound(uint32_t bound) { return {SpvMagicNumber, 0x10000, SPV_GENERATOR_WORD(SPV_GENERATOR_KHRONOS_ASSEMBLER, 0), bound, 0}; } // Returns a parsed operand for a non-number value at the given word offset // within an instruction. spv_parsed_operand_t MakeSimpleOperand(uint16_t offset, spv_operand_type_t type) { return {offset, 1, type, SPV_NUMBER_NONE, 0}; } // Returns a parsed operand for a literal unsigned integer value at the given // word offset within an instruction. spv_parsed_operand_t MakeLiteralNumberOperand(uint16_t offset) { return {offset, 1, SPV_OPERAND_TYPE_LITERAL_INTEGER, SPV_NUMBER_UNSIGNED_INT, 32}; } // Returns a parsed operand for a literal string value at the given // word offset within an instruction. spv_parsed_operand_t MakeLiteralStringOperand(uint16_t offset, uint16_t length) { return {offset, length, SPV_OPERAND_TYPE_LITERAL_STRING, SPV_NUMBER_NONE, 0}; } // Returns a ParsedInstruction for an OpTypeVoid instruction that would // generate the given result Id. ParsedInstruction MakeParsedVoidTypeInstruction(uint32_t result_id) { const auto void_inst = MakeInstruction(SpvOpTypeVoid, {result_id}); const auto void_operands = std::vector{ MakeSimpleOperand(1, SPV_OPERAND_TYPE_RESULT_ID)}; const spv_parsed_instruction_t parsed_void_inst = { void_inst.data(), static_cast(void_inst.size()), SpvOpTypeVoid, SPV_EXT_INST_TYPE_NONE, 0, // type id result_id, void_operands.data(), static_cast(void_operands.size())}; return ParsedInstruction(parsed_void_inst); } // Returns a ParsedInstruction for an OpTypeInt instruction that generates // the given result Id for a 32-bit signed integer scalar type. ParsedInstruction MakeParsedInt32TypeInstruction(uint32_t result_id) { const auto i32_inst = MakeInstruction(SpvOpTypeInt, {result_id, 32, 1}); const auto i32_operands = std::vector{ MakeSimpleOperand(1, SPV_OPERAND_TYPE_RESULT_ID), MakeLiteralNumberOperand(2), MakeLiteralNumberOperand(3)}; spv_parsed_instruction_t parsed_i32_inst = { i32_inst.data(), static_cast(i32_inst.size()), SpvOpTypeInt, SPV_EXT_INST_TYPE_NONE, 0, // type id result_id, i32_operands.data(), static_cast(i32_operands.size())}; return ParsedInstruction(parsed_i32_inst); } class BinaryParseTest : public spvtest::TextToBinaryTestBase<::testing::Test> { protected: void Parse(const SpirvVector& words, spv_result_t expected_result, bool flip_words = false) { SpirvVector flipped_words(words); SCOPED_TRACE(flip_words ? "Flipped Endianness" : "Normal Endianness"); if (flip_words) { std::transform(flipped_words.begin(), flipped_words.end(), flipped_words.begin(), [](const uint32_t raw_word) { return spvFixWord(raw_word, I32_ENDIAN_HOST == I32_ENDIAN_BIG ? SPV_ENDIANNESS_LITTLE : SPV_ENDIANNESS_BIG); }); } EXPECT_EQ(expected_result, spvBinaryParse(ScopedContext().context, &client_, flipped_words.data(), flipped_words.size(), invoke_header, invoke_instruction, &diagnostic_)); } spv_diagnostic diagnostic_ = nullptr; MockParseClient client_; }; // Adds an EXPECT_CALL to client_->Header() with appropriate parameters, // including bound. Returns the EXPECT_CALL result. #define EXPECT_HEADER(bound) \ EXPECT_CALL( \ client_, \ Header(AnyOf(SPV_ENDIANNESS_LITTLE, SPV_ENDIANNESS_BIG), SpvMagicNumber, \ 0x10000, SPV_GENERATOR_WORD(SPV_GENERATOR_KHRONOS_ASSEMBLER, 0), \ bound, 0 /*reserved*/)) static const bool kSwapEndians[] = {false, true}; TEST_F(BinaryParseTest, EmptyModuleHasValidHeaderAndNoInstructionCallbacks) { for (bool endian_swap : kSwapEndians) { const auto words = CompileSuccessfully(""); EXPECT_HEADER(1).WillOnce(Return(SPV_SUCCESS)); EXPECT_CALL(client_, Instruction(_)).Times(0); // No instruction callback. Parse(words, SPV_SUCCESS, endian_swap); EXPECT_EQ(nullptr, diagnostic_); } } TEST_F(BinaryParseTest, NullDiagnosticsIsOkForGoodParse) { const auto words = CompileSuccessfully(""); EXPECT_HEADER(1).WillOnce(Return(SPV_SUCCESS)); EXPECT_CALL(client_, Instruction(_)).Times(0); // No instruction callback. EXPECT_EQ( SPV_SUCCESS, spvBinaryParse(ScopedContext().context, &client_, words.data(), words.size(), invoke_header, invoke_instruction, nullptr)); } TEST_F(BinaryParseTest, NullDiagnosticsIsOkForBadParse) { auto words = CompileSuccessfully(""); words.push_back(0xffffffff); // Certainly invalid instruction header. EXPECT_HEADER(1).WillOnce(Return(SPV_SUCCESS)); EXPECT_CALL(client_, Instruction(_)).Times(0); // No instruction callback. EXPECT_EQ( SPV_ERROR_INVALID_BINARY, spvBinaryParse(ScopedContext().context, &client_, words.data(), words.size(), invoke_header, invoke_instruction, nullptr)); } // Make sure that we don't blow up when both the consumer and the diagnostic are // null. TEST_F(BinaryParseTest, NullConsumerNullDiagnosticsForBadParse) { auto words = CompileSuccessfully(""); auto ctx = spvContextCreate(SPV_ENV_UNIVERSAL_1_1); SetContextMessageConsumer(ctx, nullptr); words.push_back(0xffffffff); // Certainly invalid instruction header. EXPECT_HEADER(1).WillOnce(Return(SPV_SUCCESS)); EXPECT_CALL(client_, Instruction(_)).Times(0); // No instruction callback. EXPECT_EQ(SPV_ERROR_INVALID_BINARY, spvBinaryParse(ctx, &client_, words.data(), words.size(), invoke_header, invoke_instruction, nullptr)); spvContextDestroy(ctx); } TEST_F(BinaryParseTest, SpecifyConsumerNullDiagnosticsForGoodParse) { const auto words = CompileSuccessfully(""); auto ctx = spvContextCreate(SPV_ENV_UNIVERSAL_1_1); int invocation = 0; SetContextMessageConsumer( ctx, [&invocation](spv_message_level_t, const char*, const spv_position_t&, const char*) { ++invocation; }); EXPECT_HEADER(1).WillOnce(Return(SPV_SUCCESS)); EXPECT_CALL(client_, Instruction(_)).Times(0); // No instruction callback. EXPECT_EQ(SPV_SUCCESS, spvBinaryParse(ctx, &client_, words.data(), words.size(), invoke_header, invoke_instruction, nullptr)); EXPECT_EQ(0, invocation); spvContextDestroy(ctx); } TEST_F(BinaryParseTest, SpecifyConsumerNullDiagnosticsForBadParse) { auto words = CompileSuccessfully(""); auto ctx = spvContextCreate(SPV_ENV_UNIVERSAL_1_1); int invocation = 0; SetContextMessageConsumer( ctx, [&invocation](spv_message_level_t level, const char* source, const spv_position_t& position, const char* message) { ++invocation; EXPECT_EQ(SPV_MSG_ERROR, level); EXPECT_STREQ("input", source); EXPECT_EQ(0u, position.line); EXPECT_EQ(0u, position.column); EXPECT_EQ(5u, position.index); EXPECT_STREQ("Invalid opcode: 65535", message); }); words.push_back(0xffffffff); // Certainly invalid instruction header. EXPECT_HEADER(1).WillOnce(Return(SPV_SUCCESS)); EXPECT_CALL(client_, Instruction(_)).Times(0); // No instruction callback. EXPECT_EQ(SPV_ERROR_INVALID_BINARY, spvBinaryParse(ctx, &client_, words.data(), words.size(), invoke_header, invoke_instruction, nullptr)); EXPECT_EQ(1, invocation); spvContextDestroy(ctx); } TEST_F(BinaryParseTest, SpecifyConsumerSpecifyDiagnosticsForGoodParse) { const auto words = CompileSuccessfully(""); auto ctx = spvContextCreate(SPV_ENV_UNIVERSAL_1_1); int invocation = 0; SetContextMessageConsumer( ctx, [&invocation](spv_message_level_t, const char*, const spv_position_t&, const char*) { ++invocation; }); EXPECT_HEADER(1).WillOnce(Return(SPV_SUCCESS)); EXPECT_CALL(client_, Instruction(_)).Times(0); // No instruction callback. EXPECT_EQ(SPV_SUCCESS, spvBinaryParse(ctx, &client_, words.data(), words.size(), invoke_header, invoke_instruction, &diagnostic_)); EXPECT_EQ(0, invocation); EXPECT_EQ(nullptr, diagnostic_); spvContextDestroy(ctx); } TEST_F(BinaryParseTest, SpecifyConsumerSpecifyDiagnosticsForBadParse) { auto words = CompileSuccessfully(""); auto ctx = spvContextCreate(SPV_ENV_UNIVERSAL_1_1); int invocation = 0; SetContextMessageConsumer( ctx, [&invocation](spv_message_level_t, const char*, const spv_position_t&, const char*) { ++invocation; }); words.push_back(0xffffffff); // Certainly invalid instruction header. EXPECT_HEADER(1).WillOnce(Return(SPV_SUCCESS)); EXPECT_CALL(client_, Instruction(_)).Times(0); // No instruction callback. EXPECT_EQ(SPV_ERROR_INVALID_BINARY, spvBinaryParse(ctx, &client_, words.data(), words.size(), invoke_header, invoke_instruction, &diagnostic_)); EXPECT_EQ(0, invocation); EXPECT_STREQ("Invalid opcode: 65535", diagnostic_->error); spvContextDestroy(ctx); } TEST_F(BinaryParseTest, ModuleWithSingleInstructionHasValidHeaderAndInstructionCallback) { for (bool endian_swap : kSwapEndians) { const auto words = CompileSuccessfully("%1 = OpTypeVoid"); InSequence calls_expected_in_specific_order; EXPECT_HEADER(2).WillOnce(Return(SPV_SUCCESS)); EXPECT_CALL(client_, Instruction(MakeParsedVoidTypeInstruction(1))) .WillOnce(Return(SPV_SUCCESS)); Parse(words, SPV_SUCCESS, endian_swap); EXPECT_EQ(nullptr, diagnostic_); } } TEST_F(BinaryParseTest, NullHeaderCallbackIsIgnored) { const auto words = CompileSuccessfully("%1 = OpTypeVoid"); EXPECT_CALL(client_, Header(_, _, _, _, _, _)) .Times(0); // No header callback. EXPECT_CALL(client_, Instruction(MakeParsedVoidTypeInstruction(1))) .WillOnce(Return(SPV_SUCCESS)); EXPECT_EQ(SPV_SUCCESS, spvBinaryParse(ScopedContext().context, &client_, words.data(), words.size(), nullptr, invoke_instruction, &diagnostic_)); EXPECT_EQ(nullptr, diagnostic_); } TEST_F(BinaryParseTest, NullInstructionCallbackIsIgnored) { const auto words = CompileSuccessfully("%1 = OpTypeVoid"); EXPECT_HEADER((2)).WillOnce(Return(SPV_SUCCESS)); EXPECT_CALL(client_, Instruction(_)).Times(0); // No instruction callback. EXPECT_EQ(SPV_SUCCESS, spvBinaryParse(ScopedContext().context, &client_, words.data(), words.size(), invoke_header, nullptr, &diagnostic_)); EXPECT_EQ(nullptr, diagnostic_); } // Check the result of multiple instruction callbacks. // // This test exercises non-default values for the following members of the // spv_parsed_instruction_t struct: words, num_words, opcode, result_id, // operands, num_operands. TEST_F(BinaryParseTest, TwoScalarTypesGenerateTwoInstructionCallbacks) { for (bool endian_swap : kSwapEndians) { const auto words = CompileSuccessfully( "%1 = OpTypeVoid " "%2 = OpTypeInt 32 1"); InSequence calls_expected_in_specific_order; EXPECT_HEADER(3).WillOnce(Return(SPV_SUCCESS)); EXPECT_CALL(client_, Instruction(MakeParsedVoidTypeInstruction(1))) .WillOnce(Return(SPV_SUCCESS)); EXPECT_CALL(client_, Instruction(MakeParsedInt32TypeInstruction(2))) .WillOnce(Return(SPV_SUCCESS)); Parse(words, SPV_SUCCESS, endian_swap); EXPECT_EQ(nullptr, diagnostic_); } } TEST_F(BinaryParseTest, EarlyReturnWithZeroPassingCallbacks) { for (bool endian_swap : kSwapEndians) { const auto words = CompileSuccessfully( "%1 = OpTypeVoid " "%2 = OpTypeInt 32 1"); InSequence calls_expected_in_specific_order; EXPECT_HEADER(3).WillOnce(Return(SPV_ERROR_INVALID_BINARY)); // Early exit means no calls to Instruction(). EXPECT_CALL(client_, Instruction(_)).Times(0); Parse(words, SPV_ERROR_INVALID_BINARY, endian_swap); // On error, the binary parser doesn't generate its own diagnostics. EXPECT_EQ(nullptr, diagnostic_); } } TEST_F(BinaryParseTest, EarlyReturnWithZeroPassingCallbacksAndSpecifiedResultCode) { for (bool endian_swap : kSwapEndians) { const auto words = CompileSuccessfully( "%1 = OpTypeVoid " "%2 = OpTypeInt 32 1"); InSequence calls_expected_in_specific_order; EXPECT_HEADER(3).WillOnce(Return(SPV_REQUESTED_TERMINATION)); // Early exit means no calls to Instruction(). EXPECT_CALL(client_, Instruction(_)).Times(0); Parse(words, SPV_REQUESTED_TERMINATION, endian_swap); // On early termination, the binary parser doesn't generate its own // diagnostics. EXPECT_EQ(nullptr, diagnostic_); } } TEST_F(BinaryParseTest, EarlyReturnWithOnePassingCallback) { for (bool endian_swap : kSwapEndians) { const auto words = CompileSuccessfully( "%1 = OpTypeVoid " "%2 = OpTypeInt 32 1 " "%3 = OpTypeFloat 32"); InSequence calls_expected_in_specific_order; EXPECT_HEADER(4).WillOnce(Return(SPV_SUCCESS)); EXPECT_CALL(client_, Instruction(MakeParsedVoidTypeInstruction(1))) .WillOnce(Return(SPV_REQUESTED_TERMINATION)); Parse(words, SPV_REQUESTED_TERMINATION, endian_swap); // On early termination, the binary parser doesn't generate its own // diagnostics. EXPECT_EQ(nullptr, diagnostic_); } } TEST_F(BinaryParseTest, EarlyReturnWithTwoPassingCallbacks) { for (bool endian_swap : kSwapEndians) { const auto words = CompileSuccessfully( "%1 = OpTypeVoid " "%2 = OpTypeInt 32 1 " "%3 = OpTypeFloat 32"); InSequence calls_expected_in_specific_order; EXPECT_HEADER(4).WillOnce(Return(SPV_SUCCESS)); EXPECT_CALL(client_, Instruction(MakeParsedVoidTypeInstruction(1))) .WillOnce(Return(SPV_SUCCESS)); EXPECT_CALL(client_, Instruction(MakeParsedInt32TypeInstruction(2))) .WillOnce(Return(SPV_REQUESTED_TERMINATION)); Parse(words, SPV_REQUESTED_TERMINATION, endian_swap); // On early termination, the binary parser doesn't generate its own // diagnostics. EXPECT_EQ(nullptr, diagnostic_); } } TEST_F(BinaryParseTest, InstructionWithStringOperand) { const std::string str = "the future is already here, it's just not evenly distributed"; const auto str_words = MakeVector(str); const auto instruction = MakeInstruction(SpvOpName, {99}, str_words); const auto words = Concatenate({ExpectedHeaderForBound(100), instruction}); InSequence calls_expected_in_specific_order; EXPECT_HEADER(100).WillOnce(Return(SPV_SUCCESS)); const auto operands = std::vector{ MakeSimpleOperand(1, SPV_OPERAND_TYPE_ID), MakeLiteralStringOperand(2, static_cast(str_words.size()))}; EXPECT_CALL(client_, Instruction(ParsedInstruction(spv_parsed_instruction_t{ instruction.data(), static_cast(instruction.size()), SpvOpName, SPV_EXT_INST_TYPE_NONE, 0 /*type id*/, 0 /* No result id for OpName*/, operands.data(), static_cast(operands.size())}))) .WillOnce(Return(SPV_SUCCESS)); // Since we are actually checking the output, don't test the // endian-swapped version. Parse(words, SPV_SUCCESS, false); EXPECT_EQ(nullptr, diagnostic_); } // Checks for non-zero values for the result_id and ext_inst_type members // spv_parsed_instruction_t. TEST_F(BinaryParseTest, ExtendedInstruction) { const auto words = CompileSuccessfully( "%extcl = OpExtInstImport \"OpenCL.std\" " "%result = OpExtInst %float %extcl sqrt %x"); EXPECT_HEADER(5).WillOnce(Return(SPV_SUCCESS)); EXPECT_CALL(client_, Instruction(_)).WillOnce(Return(SPV_SUCCESS)); // We're only interested in the second call to Instruction(): const auto operands = std::vector{ MakeSimpleOperand(1, SPV_OPERAND_TYPE_TYPE_ID), MakeSimpleOperand(2, SPV_OPERAND_TYPE_RESULT_ID), MakeSimpleOperand(3, SPV_OPERAND_TYPE_ID), // Extended instruction set Id MakeSimpleOperand(4, SPV_OPERAND_TYPE_EXTENSION_INSTRUCTION_NUMBER), MakeSimpleOperand(5, SPV_OPERAND_TYPE_ID), // Id of the argument }; const auto instruction = MakeInstruction( SpvOpExtInst, {2, 3, 1, static_cast(OpenCLLIB::Entrypoints::Sqrt), 4}); EXPECT_CALL(client_, Instruction(ParsedInstruction(spv_parsed_instruction_t{ instruction.data(), static_cast(instruction.size()), SpvOpExtInst, SPV_EXT_INST_TYPE_OPENCL_STD, 2 /*type id*/, 3 /*result id*/, operands.data(), static_cast(operands.size())}))) .WillOnce(Return(SPV_SUCCESS)); // Since we are actually checking the output, don't test the // endian-swapped version. Parse(words, SPV_SUCCESS, false); EXPECT_EQ(nullptr, diagnostic_); } // A binary parser diagnostic test case where we provide the words array // pointer and word count explicitly. struct WordsAndCountDiagnosticCase { const uint32_t* words; size_t num_words; std::string expected_diagnostic; }; using BinaryParseWordsAndCountDiagnosticTest = spvtest::TextToBinaryTestBase< ::testing::TestWithParam>; TEST_P(BinaryParseWordsAndCountDiagnosticTest, WordAndCountCases) { EXPECT_EQ( SPV_ERROR_INVALID_BINARY, spvBinaryParse(ScopedContext().context, nullptr, GetParam().words, GetParam().num_words, nullptr, nullptr, &diagnostic)); ASSERT_NE(nullptr, diagnostic); EXPECT_THAT(diagnostic->error, Eq(GetParam().expected_diagnostic)); } INSTANTIATE_TEST_CASE_P( BinaryParseDiagnostic, BinaryParseWordsAndCountDiagnosticTest, ::testing::ValuesIn(std::vector{ {nullptr, 0, "Missing module."}, {kHeaderForBound1, 0, "Module has incomplete header: only 0 words instead of 5"}, {kHeaderForBound1, 1, "Module has incomplete header: only 1 words instead of 5"}, {kHeaderForBound1, 2, "Module has incomplete header: only 2 words instead of 5"}, {kHeaderForBound1, 3, "Module has incomplete header: only 3 words instead of 5"}, {kHeaderForBound1, 4, "Module has incomplete header: only 4 words instead of 5"}, }), ); // A binary parser diagnostic test case where a vector of words is // provided. We'll use this to express cases that can't be created // via the assembler. Either we want to make a malformed instruction, // or an invalid case the assembler would reject. struct WordVectorDiagnosticCase { std::vector words; std::string expected_diagnostic; }; using BinaryParseWordVectorDiagnosticTest = spvtest::TextToBinaryTestBase< ::testing::TestWithParam>; TEST_P(BinaryParseWordVectorDiagnosticTest, WordVectorCases) { const auto& words = GetParam().words; EXPECT_THAT(spvBinaryParse(ScopedContext().context, nullptr, words.data(), words.size(), nullptr, nullptr, &diagnostic), AnyOf(SPV_ERROR_INVALID_BINARY, SPV_ERROR_INVALID_ID)); ASSERT_NE(nullptr, diagnostic); EXPECT_THAT(diagnostic->error, Eq(GetParam().expected_diagnostic)); } INSTANTIATE_TEST_CASE_P( BinaryParseDiagnostic, BinaryParseWordVectorDiagnosticTest, ::testing::ValuesIn(std::vector{ {Concatenate({ExpectedHeaderForBound(1), {spvOpcodeMake(0, SpvOpNop)}}), "Invalid instruction word count: 0"}, {Concatenate( {ExpectedHeaderForBound(1), {spvOpcodeMake(1, static_cast( std::numeric_limits::max()))}}), "Invalid opcode: 65535"}, {Concatenate({ExpectedHeaderForBound(1), MakeInstruction(SpvOpNop, {42})}), "Invalid instruction OpNop starting at word 5: expected " "no more operands after 1 words, but stated word count is 2."}, // Supply several more unexpectd words. {Concatenate({ExpectedHeaderForBound(1), MakeInstruction(SpvOpNop, {42, 43, 44, 45, 46, 47})}), "Invalid instruction OpNop starting at word 5: expected " "no more operands after 1 words, but stated word count is 7."}, {Concatenate({ExpectedHeaderForBound(1), MakeInstruction(SpvOpTypeVoid, {1, 2})}), "Invalid instruction OpTypeVoid starting at word 5: expected " "no more operands after 2 words, but stated word count is 3."}, {Concatenate({ExpectedHeaderForBound(1), MakeInstruction(SpvOpTypeVoid, {1, 2, 5, 9, 10})}), "Invalid instruction OpTypeVoid starting at word 5: expected " "no more operands after 2 words, but stated word count is 6."}, {Concatenate({ExpectedHeaderForBound(1), MakeInstruction(SpvOpTypeInt, {1, 32, 1, 9})}), "Invalid instruction OpTypeInt starting at word 5: expected " "no more operands after 4 words, but stated word count is 5."}, {Concatenate({ExpectedHeaderForBound(1), MakeInstruction(SpvOpTypeInt, {1})}), "End of input reached while decoding OpTypeInt starting at word 5:" " expected more operands after 2 words."}, // Check several cases for running off the end of input. // Detect a missing single word operand. {Concatenate({ExpectedHeaderForBound(1), {spvOpcodeMake(2, SpvOpTypeStruct)}}), "End of input reached while decoding OpTypeStruct starting at word" " 5: missing result ID operand at word offset 1."}, // Detect this a missing a multi-word operand to OpConstant. // We also lie and say the OpConstant instruction has 5 words when // it only has 3. Corresponds to something like this: // %1 = OpTypeInt 64 0 // %2 = OpConstant %1 {Concatenate({ExpectedHeaderForBound(3), {MakeInstruction(SpvOpTypeInt, {1, 64, 0})}, {spvOpcodeMake(5, SpvOpConstant), 1, 2}}), "End of input reached while decoding OpConstant starting at word" " 9: missing possibly multi-word literal number operand at word " "offset 3."}, // Detect when we provide only one word from the 64-bit literal, // and again lie about the number of words in the instruction. {Concatenate({ExpectedHeaderForBound(3), {MakeInstruction(SpvOpTypeInt, {1, 64, 0})}, {spvOpcodeMake(5, SpvOpConstant), 1, 2, 42}}), "End of input reached while decoding OpConstant starting at word" " 9: truncated possibly multi-word literal number operand at word " "offset 3."}, // Detect when a required string operand is missing. // Also, lie about the length of the instruction. {Concatenate({ExpectedHeaderForBound(3), {spvOpcodeMake(3, SpvOpString), 1}}), "End of input reached while decoding OpString starting at word" " 5: missing literal string operand at word offset 2."}, // Detect when a required string operand is truncated: it's missing // a null terminator. Catching the error avoids a buffer overrun. {Concatenate({ExpectedHeaderForBound(3), {spvOpcodeMake(4, SpvOpString), 1, 0x41414141, 0x41414141}}), "End of input reached while decoding OpString starting at word" " 5: truncated literal string operand at word offset 2."}, // Detect when an optional string operand is truncated: it's missing // a null terminator. Catching the error avoids a buffer overrun. // (It is valid for an optional string operand to be absent.) {Concatenate({ExpectedHeaderForBound(3), {spvOpcodeMake(6, SpvOpSource), static_cast(SpvSourceLanguageOpenCL_C), 210, 1 /* file id */, /*start of string*/ 0x41414141, 0x41414141}}), "End of input reached while decoding OpSource starting at word" " 5: truncated literal string operand at word offset 4."}, // (End of input exhaustion test cases.) // In this case the instruction word count is too small, where // it would truncate a multi-word operand to OpConstant. {Concatenate({ExpectedHeaderForBound(3), {MakeInstruction(SpvOpTypeInt, {1, 64, 0})}, {spvOpcodeMake(4, SpvOpConstant), 1, 2, 44, 44}}), "Invalid word count: OpConstant starting at word 9 says it has 4" " words, but found 5 words instead."}, // Word count is to small, where it would truncate a literal string. {Concatenate({ExpectedHeaderForBound(2), {spvOpcodeMake(3, SpvOpString), 1, 0x41414141, 0}}), "Invalid word count: OpString starting at word 5 says it has 3" " words, but found 4 words instead."}, // Word count is too large. The string terminates before the last // word. {Concatenate({ExpectedHeaderForBound(2), {spvOpcodeMake(4, SpvOpString), 1 /* result id */}, MakeVector("abc"), {0 /* this word does not belong*/}}), "Invalid instruction OpString starting at word 5: expected no more" " operands after 3 words, but stated word count is 4."}, // Word count is too large. There are too many words after the string // literal. A linkage attribute decoration is the only case in SPIR-V // where a string operand is followed by another operand. {Concatenate({ExpectedHeaderForBound(2), {spvOpcodeMake(6, SpvOpDecorate), 1 /* target id */, static_cast(SpvDecorationLinkageAttributes)}, MakeVector("abc"), {static_cast(SpvLinkageTypeImport), 0 /* does not belong */}}), "Invalid instruction OpDecorate starting at word 5: expected no more" " operands after 5 words, but stated word count is 6."}, // Like the previous case, but with 5 extra words. {Concatenate({ExpectedHeaderForBound(2), {spvOpcodeMake(10, SpvOpDecorate), 1 /* target id */, static_cast(SpvDecorationLinkageAttributes)}, MakeVector("abc"), {static_cast(SpvLinkageTypeImport), /* don't belong */ 0, 1, 2, 3, 4}}), "Invalid instruction OpDecorate starting at word 5: expected no more" " operands after 5 words, but stated word count is 10."}, // Like the previous two cases, but with OpMemberDecorate. {Concatenate({ExpectedHeaderForBound(2), {spvOpcodeMake(7, SpvOpMemberDecorate), 1 /* target id */, 42 /* member index */, static_cast(SpvDecorationLinkageAttributes)}, MakeVector("abc"), {static_cast(SpvLinkageTypeImport), 0 /* does not belong */}}), "Invalid instruction OpMemberDecorate starting at word 5: expected no" " more operands after 6 words, but stated word count is 7."}, {Concatenate({ExpectedHeaderForBound(2), {spvOpcodeMake(11, SpvOpMemberDecorate), 1 /* target id */, 42 /* member index */, static_cast(SpvDecorationLinkageAttributes)}, MakeVector("abc"), {static_cast(SpvLinkageTypeImport), /* don't belong */ 0, 1, 2, 3, 4}}), "Invalid instruction OpMemberDecorate starting at word 5: expected no" " more operands after 6 words, but stated word count is 11."}, // Word count is too large. There should be no more words // after the RelaxedPrecision decoration. {Concatenate({ExpectedHeaderForBound(2), {spvOpcodeMake(4, SpvOpDecorate), 1 /* target id */, static_cast(SpvDecorationRelaxedPrecision), 0 /* does not belong */}}), "Invalid instruction OpDecorate starting at word 5: expected no" " more operands after 3 words, but stated word count is 4."}, // Word count is too large. There should be only one word after // the SpecId decoration enum word. {Concatenate({ExpectedHeaderForBound(2), {spvOpcodeMake(5, SpvOpDecorate), 1 /* target id */, static_cast(SpvDecorationSpecId), 42 /* the spec id */, 0 /* does not belong */}}), "Invalid instruction OpDecorate starting at word 5: expected no" " more operands after 4 words, but stated word count is 5."}, {Concatenate({ExpectedHeaderForBound(2), {spvOpcodeMake(2, SpvOpTypeVoid), 0}}), "Error: Result Id is 0"}, {Concatenate({ ExpectedHeaderForBound(2), {spvOpcodeMake(2, SpvOpTypeVoid), 1}, {spvOpcodeMake(2, SpvOpTypeBool), 1}, }), "Id 1 is defined more than once"}, {Concatenate({ExpectedHeaderForBound(3), MakeInstruction(SpvOpExtInst, {2, 3, 100, 4, 5})}), "OpExtInst set Id 100 does not reference an OpExtInstImport result " "Id"}, {Concatenate({ExpectedHeaderForBound(101), MakeInstruction(SpvOpExtInstImport, {100}, MakeVector("OpenCL.std")), // OpenCL cos is #14 MakeInstruction(SpvOpExtInst, {2, 3, 100, 14, 5, 999})}), "Invalid instruction OpExtInst starting at word 10: expected no " "more operands after 6 words, but stated word count is 7."}, // In this case, the OpSwitch selector refers to an invalid ID. {Concatenate({ExpectedHeaderForBound(3), MakeInstruction(SpvOpSwitch, {1, 2, 42, 3})}), "Invalid OpSwitch: selector id 1 has no type"}, // In this case, the OpSwitch selector refers to an ID that has // no type. {Concatenate({ExpectedHeaderForBound(3), MakeInstruction(SpvOpLabel, {1}), MakeInstruction(SpvOpSwitch, {1, 2, 42, 3})}), "Invalid OpSwitch: selector id 1 has no type"}, {Concatenate({ExpectedHeaderForBound(3), MakeInstruction(SpvOpTypeInt, {1, 32, 0}), MakeInstruction(SpvOpSwitch, {1, 3, 42, 3})}), "Invalid OpSwitch: selector id 1 is a type, not a value"}, {Concatenate({ExpectedHeaderForBound(3), MakeInstruction(SpvOpTypeFloat, {1, 32}), MakeInstruction(SpvOpConstant, {1, 2, 0x78f00000}), MakeInstruction(SpvOpSwitch, {2, 3, 42, 3})}), "Invalid OpSwitch: selector id 2 is not a scalar integer"}, {Concatenate({ExpectedHeaderForBound(3), MakeInstruction(SpvOpExtInstImport, {1}, MakeVector("invalid-import"))}), "Invalid extended instruction import 'invalid-import'"}, {Concatenate({ ExpectedHeaderForBound(3), MakeInstruction(SpvOpTypeInt, {1, 32, 0}), MakeInstruction(SpvOpConstant, {2, 2, 42}), }), "Type Id 2 is not a type"}, {Concatenate({ ExpectedHeaderForBound(3), MakeInstruction(SpvOpTypeBool, {1}), MakeInstruction(SpvOpConstant, {1, 2, 42}), }), "Type Id 1 is not a scalar numeric type"}, }), ); // A binary parser diagnostic case generated from an assembly text input. struct AssemblyDiagnosticCase { std::string assembly; std::string expected_diagnostic; }; using BinaryParseAssemblyDiagnosticTest = spvtest::TextToBinaryTestBase< ::testing::TestWithParam>; TEST_P(BinaryParseAssemblyDiagnosticTest, AssemblyCases) { auto words = CompileSuccessfully(GetParam().assembly); EXPECT_THAT(spvBinaryParse(ScopedContext().context, nullptr, words.data(), words.size(), nullptr, nullptr, &diagnostic), AnyOf(SPV_ERROR_INVALID_BINARY, SPV_ERROR_INVALID_ID)); ASSERT_NE(nullptr, diagnostic); EXPECT_THAT(diagnostic->error, Eq(GetParam().expected_diagnostic)); } INSTANTIATE_TEST_CASE_P( BinaryParseDiagnostic, BinaryParseAssemblyDiagnosticTest, ::testing::ValuesIn(std::vector{ {"%1 = OpConstant !0 42", "Error: Type Id is 0"}, // A required id is 0. {"OpName !0 \"foo\"", "Id is 0"}, // An optional id is 0, in this case the optional // initializer. {"%2 = OpVariable %1 CrossWorkgroup !0", "Id is 0"}, {"OpControlBarrier !0 %1 %2", "scope ID is 0"}, {"OpControlBarrier %1 !0 %2", "scope ID is 0"}, {"OpControlBarrier %1 %2 !0", "memory semantics ID is 0"}, {"%import = OpExtInstImport \"GLSL.std.450\" " "%result = OpExtInst %type %import !999999 %x", "Invalid extended instruction number: 999999"}, {"%2 = OpSpecConstantOp %1 !1000 %2", "Invalid OpSpecConstantOp opcode: 1000"}, {"OpCapability !9999", "Invalid capability operand: 9999"}, {"OpSource !9999 100", "Invalid source language operand: 9999"}, {"OpEntryPoint !9999", "Invalid execution model operand: 9999"}, {"OpMemoryModel !9999", "Invalid addressing model operand: 9999"}, {"OpMemoryModel Logical !9999", "Invalid memory model operand: 9999"}, {"OpExecutionMode %1 !9999", "Invalid execution mode operand: 9999"}, {"OpTypeForwardPointer %1 !9999", "Invalid storage class operand: 9999"}, {"%2 = OpTypeImage %1 !9999", "Invalid dimensionality operand: 9999"}, {"%2 = OpTypeImage %1 1D 0 0 0 0 !9999", "Invalid image format operand: 9999"}, {"OpDecorate %1 FPRoundingMode !9999", "Invalid floating-point rounding mode operand: 9999"}, {"OpDecorate %1 LinkageAttributes \"C\" !9999", "Invalid linkage type operand: 9999"}, {"%1 = OpTypePipe !9999", "Invalid access qualifier operand: 9999"}, {"OpDecorate %1 FuncParamAttr !9999", "Invalid function parameter attribute operand: 9999"}, {"OpDecorate %1 !9999", "Invalid decoration operand: 9999"}, {"OpDecorate %1 BuiltIn !9999", "Invalid built-in operand: 9999"}, {"%2 = OpGroupIAdd %1 %3 !9999", "Invalid group operation operand: 9999"}, {"OpDecorate %1 FPFastMathMode !63", "Invalid floating-point fast math mode operand: 63 has invalid mask " "component 32"}, {"%2 = OpFunction %2 !31", "Invalid function control operand: 31 has invalid mask component 16"}, {"OpLoopMerge %1 %2 !1027", "Invalid loop control operand: 1027 has invalid mask component 1024"}, {"%2 = OpImageFetch %1 %image %coord !511", "Invalid image operand: 511 has invalid mask component 256"}, {"OpSelectionMerge %1 !7", "Invalid selection control operand: 7 has invalid mask component 4"}, }), ); } // namespace } // namespace spvtools