Refactor the binary parser.
The binary parser has a C API, described in binary.h. Eventually we will make it public in libspirv.h. The API is event-driven in the sense that a callback is called when a valid header is parsed, and for each parsed instruction. Classify some operand types as "concrete". The binary parser uses only concrete operand types to describe parsed instructions. The old disassembler APIs are moved into disassemble.cpp TODO: Add unit tests for spvBinaryParse.
This commit is contained in:
Родитель
0981b1514e
Коммит
0ca6b59bfd
|
@ -183,6 +183,7 @@ if (NOT ${SPIRV_SKIP_EXECUTABLES})
|
|||
${CMAKE_CURRENT_SOURCE_DIR}/test/BinaryDestroy.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/test/BinaryEndianness.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/test/BinaryHeaderGet.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/test/BinaryParse.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/test/BinaryToText.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/test/BinaryToText.Literal.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/test/Comment.cpp
|
||||
|
|
|
@ -147,72 +147,95 @@ typedef enum spv_endianness_t {
|
|||
|
||||
// The kinds of operands that an instruction may have.
|
||||
//
|
||||
// In addition to determining what kind of value an operand may be, certain
|
||||
// enums capture the fact that an operand might be optional (may be absent,
|
||||
// or present exactly once), or might occure zero or more times.
|
||||
// Some operand types are "concrete". The binary parser uses a concrete
|
||||
// operand type to describe an operand of a parsed instruction.
|
||||
//
|
||||
// The assembler uses all operand types. In addition to determining what
|
||||
// kind of value an operand may be, non-concrete operand types capture the
|
||||
// fact that an operand might be optional (may be absent, or present exactly
|
||||
// once), or might occure zero or more times.
|
||||
//
|
||||
// Sometimes we also need to be able to express the fact that an operand
|
||||
// is a member of an optional tuple of values. In that case the first member
|
||||
// would be optional, and the subsequent members would be required.
|
||||
typedef enum spv_operand_type_t {
|
||||
SPV_OPERAND_TYPE_NONE = 0,
|
||||
SPV_OPERAND_TYPE_ID,
|
||||
|
||||
#define FIRST_CONCRETE(ENUM) ENUM, SPV_OPERAND_TYPE_FIRST_CONCRETE_TYPE = ENUM
|
||||
#define LAST_CONCRETE(ENUM) ENUM, SPV_OPERAND_TYPE_LAST_CONCRETE_TYPE = ENUM
|
||||
|
||||
// Set 1: Operands that are IDs.
|
||||
FIRST_CONCRETE(SPV_OPERAND_TYPE_ID),
|
||||
SPV_OPERAND_TYPE_TYPE_ID,
|
||||
SPV_OPERAND_TYPE_RESULT_ID,
|
||||
SPV_OPERAND_TYPE_LITERAL_INTEGER,
|
||||
// A literal number that can (but is not required to) expand multiple words.
|
||||
SPV_OPERAND_TYPE_MEMORY_SEMANTICS_ID, // SPIR-V Sec 3.25
|
||||
SPV_OPERAND_TYPE_SCOPE_ID, // SPIR-V Sec 3.27
|
||||
|
||||
// TODO(dneto): Remove these old names.
|
||||
SPV_OPERAND_TYPE_MEMORY_SEMANTICS = SPV_OPERAND_TYPE_MEMORY_SEMANTICS_ID,
|
||||
SPV_OPERAND_TYPE_EXECUTION_SCOPE = SPV_OPERAND_TYPE_SCOPE_ID,
|
||||
|
||||
// Set 2: Operands that are literal numbers.
|
||||
SPV_OPERAND_TYPE_LITERAL_INTEGER, // Always unsigned 32-bits.
|
||||
// The Instruction argument to OpExtInst. It's an unsigned 32-bit literal
|
||||
// number indicating which instruction to use from an extended instruction
|
||||
// set.
|
||||
SPV_OPERAND_TYPE_EXTENSION_INSTRUCTION_NUMBER,
|
||||
// A literal number that occupies one or more words in binary form.
|
||||
SPV_OPERAND_TYPE_MULTIWORD_LITERAL_NUMBER,
|
||||
|
||||
// Set 3: The literal string operand type.
|
||||
SPV_OPERAND_TYPE_LITERAL_STRING,
|
||||
SPV_OPERAND_TYPE_SOURCE_LANGUAGE,
|
||||
SPV_OPERAND_TYPE_EXECUTION_MODEL,
|
||||
SPV_OPERAND_TYPE_ADDRESSING_MODEL,
|
||||
SPV_OPERAND_TYPE_MEMORY_MODEL,
|
||||
SPV_OPERAND_TYPE_EXECUTION_MODE,
|
||||
SPV_OPERAND_TYPE_STORAGE_CLASS,
|
||||
SPV_OPERAND_TYPE_DIMENSIONALITY,
|
||||
SPV_OPERAND_TYPE_SAMPLER_ADDRESSING_MODE,
|
||||
SPV_OPERAND_TYPE_SAMPLER_FILTER_MODE,
|
||||
SPV_OPERAND_TYPE_SAMPLER_IMAGE_FORMAT,
|
||||
SPV_OPERAND_TYPE_IMAGE_CHANNEL_ORDER,
|
||||
SPV_OPERAND_TYPE_IMAGE_CHANNEL_DATA_TYPE,
|
||||
SPV_OPERAND_TYPE_FP_FAST_MATH_MODE,
|
||||
SPV_OPERAND_TYPE_FP_ROUNDING_MODE,
|
||||
SPV_OPERAND_TYPE_LINKAGE_TYPE,
|
||||
SPV_OPERAND_TYPE_ACCESS_QUALIFIER,
|
||||
SPV_OPERAND_TYPE_FUNCTION_PARAMETER_ATTRIBUTE,
|
||||
SPV_OPERAND_TYPE_DECORATION,
|
||||
SPV_OPERAND_TYPE_BUILT_IN,
|
||||
SPV_OPERAND_TYPE_SELECTION_CONTROL,
|
||||
SPV_OPERAND_TYPE_LOOP_CONTROL,
|
||||
SPV_OPERAND_TYPE_FUNCTION_CONTROL,
|
||||
|
||||
// The ID for a memory semantics value.
|
||||
SPV_OPERAND_TYPE_MEMORY_SEMANTICS,
|
||||
// The ID for an execution scope value.
|
||||
// TODO(dneto): Rev 30 changed "Execution Scope" to "Scope". We should
|
||||
// probably do that here too.
|
||||
SPV_OPERAND_TYPE_EXECUTION_SCOPE,
|
||||
// Set 4: Operands that are a single word enumerated value.
|
||||
SPV_OPERAND_TYPE_SOURCE_LANGUAGE, // SPIR-V Sec 3.2
|
||||
SPV_OPERAND_TYPE_EXECUTION_MODEL, // SPIR-V Sec 3.3
|
||||
SPV_OPERAND_TYPE_ADDRESSING_MODEL, // SPIR-V Sec 3.4
|
||||
SPV_OPERAND_TYPE_MEMORY_MODEL, // SPIR-V Sec 3.5
|
||||
SPV_OPERAND_TYPE_EXECUTION_MODE, // SPIR-V Sec 3.6
|
||||
SPV_OPERAND_TYPE_STORAGE_CLASS, // SPIR-V Sec 3.7
|
||||
SPV_OPERAND_TYPE_DIMENSIONALITY, // SPIR-V Sec 3.8
|
||||
SPV_OPERAND_TYPE_SAMPLER_ADDRESSING_MODE, // SPIR-V Sec 3.9
|
||||
SPV_OPERAND_TYPE_SAMPLER_FILTER_MODE, // SPIR-V Sec 3.10
|
||||
SPV_OPERAND_TYPE_SAMPLER_IMAGE_FORMAT, // SPIR-V Sec 3.11
|
||||
SPV_OPERAND_TYPE_IMAGE_CHANNEL_ORDER, // SPIR-V Sec 3.12
|
||||
SPV_OPERAND_TYPE_IMAGE_CHANNEL_DATA_TYPE, // SPIR-V Sec 3.13
|
||||
SPV_OPERAND_TYPE_FP_ROUNDING_MODE, // SPIR-V Sec 3.16
|
||||
SPV_OPERAND_TYPE_LINKAGE_TYPE, // SPIR-V Sec 3.17
|
||||
SPV_OPERAND_TYPE_ACCESS_QUALIFIER, // SPIR-V Sec 3.18
|
||||
SPV_OPERAND_TYPE_FUNCTION_PARAMETER_ATTRIBUTE, // SPIR-V Sec 3.19
|
||||
SPV_OPERAND_TYPE_DECORATION, // SPIR-V Sec 3.20
|
||||
SPV_OPERAND_TYPE_BUILT_IN, // SPIR-V Sec 3.21
|
||||
SPV_OPERAND_TYPE_GROUP_OPERATION, // SPIR-V Sec 3.28
|
||||
SPV_OPERAND_TYPE_KERNEL_ENQ_FLAGS, // SPIR-V Sec 3.29
|
||||
SPV_OPERAND_TYPE_KERNEL_PROFILING_INFO, // SPIR-V Sec 3.30
|
||||
SPV_OPERAND_TYPE_CAPABILITY, // SPIR-V Sec 3.31
|
||||
|
||||
SPV_OPERAND_TYPE_GROUP_OPERATION,
|
||||
SPV_OPERAND_TYPE_KERNEL_ENQ_FLAGS,
|
||||
SPV_OPERAND_TYPE_KERNEL_PROFILING_INFO,
|
||||
SPV_OPERAND_TYPE_CAPABILITY,
|
||||
// Set 5: Operands that are a single word bitmask.
|
||||
// Sometimes a set bit indicates the instruction requires still more operands.
|
||||
SPV_OPERAND_TYPE_OPTIONAL_IMAGE, // SPIR-V Sec 3.14
|
||||
SPV_OPERAND_TYPE_FP_FAST_MATH_MODE, // SPIR-V Sec 3.15
|
||||
SPV_OPERAND_TYPE_SELECTION_CONTROL, // SPIR-V Sec 3.22
|
||||
SPV_OPERAND_TYPE_LOOP_CONTROL, // SPIR-V Sec 3.23
|
||||
SPV_OPERAND_TYPE_FUNCTION_CONTROL, // SPIR-V Sec 3.24
|
||||
LAST_CONCRETE(SPV_OPERAND_TYPE_OPTIONAL_MEMORY_ACCESS), // SPIR-V Sec 3.26
|
||||
#undef FIRST_CONCRETE
|
||||
#undef LAST_CONCRETE
|
||||
|
||||
// The remaining operand types are only used internally by the assembler.
|
||||
|
||||
// An optional operand represents zero or one logical operands.
|
||||
// In an instruction definition, this may only appear at the end of the
|
||||
// operand types.
|
||||
SPV_OPERAND_TYPE_OPTIONAL_ID,
|
||||
// An optional image operands mask. A set bit in the mask may
|
||||
// imply that more arguments are required.
|
||||
SPV_OPERAND_TYPE_OPTIONAL_IMAGE,
|
||||
// An optional literal number. This can expand to either a literal integer or
|
||||
// a literal floating-point number.
|
||||
SPV_OPERAND_TYPE_OPTIONAL_LITERAL_NUMBER,
|
||||
// An optional literal integer.
|
||||
SPV_OPERAND_TYPE_OPTIONAL_LITERAL_INTEGER,
|
||||
// An optional literal string.
|
||||
SPV_OPERAND_TYPE_OPTIONAL_LITERAL_STRING,
|
||||
// An optional memory access qualifier mask, e.g. Volatile, Aligned,
|
||||
// or a combination.
|
||||
SPV_OPERAND_TYPE_OPTIONAL_MEMORY_ACCESS,
|
||||
// An optional execution mode
|
||||
// An optional execution mode.
|
||||
SPV_OPERAND_TYPE_OPTIONAL_EXECUTION_MODE,
|
||||
// A variable operand represents zero or more logical operands.
|
||||
// In an instruction definition, this may only appear at the end of the
|
||||
|
@ -237,14 +260,6 @@ typedef enum spv_operand_type_t {
|
|||
// assemble regardless of where they occur -- literals, IDs, immediate
|
||||
// integers, etc.
|
||||
SPV_OPERAND_TYPE_OPTIONAL_CIV,
|
||||
// An optional literal number. This can expand to either a literal integer or
|
||||
// a literal floating-point number.
|
||||
SPV_OPERAND_TYPE_OPTIONAL_LITERAL_NUMBER,
|
||||
|
||||
// The Instruction argument to OpExtInst. It's an unsigned 32-bit literal
|
||||
// number indicating which instruction to use from an extended instruction
|
||||
// set.
|
||||
SPV_OPERAND_TYPE_EXTENSION_INSTRUCTION_NUMBER,
|
||||
|
||||
// This is a sentinel value, and does not represent an operand type.
|
||||
// It should come last.
|
||||
|
|
|
@ -27,10 +27,12 @@
|
|||
#include "assembly_grammar.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cassert>
|
||||
#include <cstring>
|
||||
|
||||
#include "ext_inst.h"
|
||||
#include "opcode.h"
|
||||
#include "operand.h"
|
||||
|
||||
namespace {
|
||||
|
||||
|
@ -82,10 +84,41 @@ spv_result_t spvTextParseMaskOperand(const spv_operand_table operandTable,
|
|||
*pValue = value;
|
||||
return SPV_SUCCESS;
|
||||
}
|
||||
|
||||
// Returns the operand table.
|
||||
spv_operand_table GetDefaultOperandTable() {
|
||||
spv_operand_table result = nullptr;
|
||||
spvOperandTableGet(&result);
|
||||
assert(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
// Returns the opcode table.
|
||||
spv_opcode_table GetDefaultOpcodeTable() {
|
||||
spv_opcode_table result = nullptr;
|
||||
spvOpcodeTableGet(&result);
|
||||
assert(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
// Returns the extended instruction table.
|
||||
spv_ext_inst_table GetDefaultExtInstTable() {
|
||||
spv_ext_inst_table result = nullptr;
|
||||
spvExtInstTableGet(&result);
|
||||
assert(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
} // anonymous namespace
|
||||
|
||||
namespace libspirv {
|
||||
|
||||
AssemblyGrammar::AssemblyGrammar()
|
||||
: AssemblyGrammar(GetDefaultOperandTable(), GetDefaultOpcodeTable(),
|
||||
GetDefaultExtInstTable()) {
|
||||
assert(isValid());
|
||||
}
|
||||
|
||||
bool AssemblyGrammar::isValid() const {
|
||||
return operandTable_ && opcodeTable_ && extInstTable_;
|
||||
}
|
||||
|
|
|
@ -43,7 +43,10 @@ class AssemblyGrammar {
|
|||
opcodeTable_(opcode_table),
|
||||
extInstTable_(ext_inst_table) {}
|
||||
|
||||
// Returns true if the compilation_data has been initialized with valid data.
|
||||
// Constructor that gets the required data itself.
|
||||
AssemblyGrammar();
|
||||
|
||||
// Returns true if the internal tables have been initialized with valid data.
|
||||
bool isValid() const;
|
||||
|
||||
// Fills in the desc parameter with the information about the opcode
|
||||
|
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -27,10 +27,93 @@
|
|||
#ifndef LIBSPIRV_BINARY_H_
|
||||
#define LIBSPIRV_BINARY_H_
|
||||
|
||||
#include <libspirv/libspirv.h>
|
||||
#include "instruction.h"
|
||||
#include "operand.h"
|
||||
#include "print.h"
|
||||
#include "libspirv/libspirv.h"
|
||||
|
||||
|
||||
// TODO(dneto): Move spvBinaryParse and related type definitions to libspirv.h
|
||||
extern "C" {
|
||||
|
||||
// The parser interface.
|
||||
// This is a C interface because we expect to expose this to clients of the
|
||||
// SPIR-V Tools C API.
|
||||
|
||||
// A pointer to a function that accepts a parsed SPIR-V header.
|
||||
// The integer arguments are the 32-bit words from the header, as specified
|
||||
// in SPIR-V 1.0 Section 2.3 Table 1.
|
||||
// The function should return SPV_SUCCESS if parsing should continue.
|
||||
typedef spv_result_t (*spv_parsed_header_fn_t)(
|
||||
void* user_data, spv_endianness_t endian, uint32_t magic, uint32_t version,
|
||||
uint32_t generator, uint32_t id_bound, uint32_t reserved);
|
||||
|
||||
// Number kind. This determines the format at a high level, but not
|
||||
// the bit width.
|
||||
// In principle, these could probably be folded into new entries in
|
||||
// spv_operand_type_t. But then we'd have some special case differences
|
||||
// between the assembler and disassembler.
|
||||
typedef enum spv_number_kind_t {
|
||||
SPV_NUMBER_NONE = 0, // The default for value initialization.
|
||||
SPV_NUMBER_UNSIGNED_INT,
|
||||
SPV_NUMBER_SIGNED_INT,
|
||||
SPV_NUMBER_FLOATING,
|
||||
} spv_number_kind_t;
|
||||
|
||||
// Information about a parsed operand. Note that the values are not
|
||||
// included. You still need access to the binary to extract the values.
|
||||
typedef struct spv_parsed_operand_t {
|
||||
// Location of the operand, in words from the start of the instruction.
|
||||
uint16_t offset;
|
||||
// Number of words occupied by this operand.
|
||||
uint16_t num_words;
|
||||
// The "concrete" operand type. See the definition of spv_operand_type_t
|
||||
// for details.
|
||||
spv_operand_type_t type;
|
||||
// If type is a literal number type, then number_kind says whether it's
|
||||
// a signed integer, an unsigned integer, or a floating point number.
|
||||
spv_number_kind_t number_kind;
|
||||
// The number of bits for a literal number type.
|
||||
uint32_t number_bit_width;
|
||||
} spv_parsed_operand_info_t;
|
||||
|
||||
// A parsed instruction.
|
||||
typedef struct spv_parsed_instruction_t {
|
||||
// Location of the instruction, in words from the start of the SPIR-V binary.
|
||||
size_t offset;
|
||||
SpvOp opcode;
|
||||
// The extended instruction type, if opcode is OpExtInst. Otherwise
|
||||
// this is the "none" value.
|
||||
spv_ext_inst_type_t ext_inst_type;
|
||||
// The type id, or 0 if this instruction doesn't have one.
|
||||
uint32_t type_id;
|
||||
// The result id, or 0 if this instruction doesn't have one.
|
||||
uint32_t result_id;
|
||||
// The array of parsed operands.
|
||||
const spv_parsed_operand_t* operands;
|
||||
uint16_t num_operands;
|
||||
} spv_parsed_instruction_t;
|
||||
|
||||
// A pointer to a function that accepts a parsed SPIR-V instruction.
|
||||
// The parsed_instruction value is transient: it may be overwritten
|
||||
// or released immediately after the function has returned. The function
|
||||
// should return SPV_SUCCESS if and only if parsing should continue.
|
||||
typedef spv_result_t (*spv_parsed_instruction_fn_t)(
|
||||
void* user_data, const spv_parsed_instruction_t* parsed_instruction);
|
||||
|
||||
// Parses a SPIR-V binary, specified as counted sequence of 32-bit words.
|
||||
// Parsing feedback is provided via two callbacks. In a valid parse, the
|
||||
// parsed-header callback is called once, and then the parsed-instruction
|
||||
// callback once for each instruction in the stream. The user_data parameter
|
||||
// is supplied as context to the callbacks. Returns SPV_SUCCESS on successful
|
||||
// parse where the callbacks always return SPV_SUCCESS. For an invalid parse,
|
||||
// returns SPV_ERROR_INVALID_BINARY and emits a diagnostic. If a callback
|
||||
// returns anything other than SPV_SUCCESS, then that error code is returned
|
||||
// and parsing terminates early.
|
||||
spv_result_t spvBinaryParse(void* user_data, const uint32_t* const words,
|
||||
const size_t num_words,
|
||||
spv_parsed_header_fn_t parse_header,
|
||||
spv_parsed_instruction_fn_t parse_instruction,
|
||||
spv_diagnostic* diagnostic);
|
||||
|
||||
} // extern "C"
|
||||
|
||||
// Functions
|
||||
|
||||
|
@ -59,4 +142,5 @@ spv_operand_type_t spvBinaryOperandInfo(const uint32_t word,
|
|||
const spv_opcode_desc opcodeEntry,
|
||||
const spv_operand_table operandTable,
|
||||
spv_operand_desc* pOperandEntry);
|
||||
|
||||
#endif // LIBSPIRV_BINARY_H_
|
||||
|
|
|
@ -26,11 +26,298 @@
|
|||
|
||||
// This file contains a disassembler: It converts a SPIR-V binary
|
||||
// to text.
|
||||
// TODO(dneto): Right now it's just a stub, but should evolve to using
|
||||
// the binary decoder in binary.cpp to semantically decompose the binary
|
||||
// for us. All this module has to do is convert the result to text.
|
||||
|
||||
#include <cassert>
|
||||
#include <cstring>
|
||||
#include <unordered_map>
|
||||
|
||||
#include "assembly_grammar.h"
|
||||
#include "binary.h"
|
||||
#include "diagnostic.h"
|
||||
#include "endian.h"
|
||||
#include "ext_inst.h"
|
||||
#include "libspirv/libspirv.h"
|
||||
#include "opcode.h"
|
||||
#include "print.h"
|
||||
|
||||
namespace {
|
||||
|
||||
// A Disassembler instance converts a SPIR-V binary to its assembly
|
||||
// representation.
|
||||
class Disassembler {
|
||||
public:
|
||||
Disassembler(const libspirv::AssemblyGrammar& grammar, uint32_t const* words,
|
||||
size_t num_words, spv_assembly_syntax_format_t format,
|
||||
uint32_t options)
|
||||
: words_(words),
|
||||
num_words_(num_words),
|
||||
grammar_(grammar),
|
||||
format_(format),
|
||||
print_(spvIsInBitfield(SPV_BINARY_TO_TEXT_OPTION_PRINT, options)),
|
||||
color_(print_ &&
|
||||
spvIsInBitfield(SPV_BINARY_TO_TEXT_OPTION_COLOR, options)),
|
||||
text_(),
|
||||
out_(print_ ? out_stream() : out_stream(text_)),
|
||||
stream_(out_.get()) {}
|
||||
|
||||
// Emits the assembly header for the module, and sets up internal state
|
||||
// so subsequent callbacks can handle the cases where the entire module
|
||||
// is either big-endian or little-endian.
|
||||
spv_result_t HandleHeader(spv_endianness_t endian, uint32_t version,
|
||||
uint32_t generator, uint32_t id_bound,
|
||||
uint32_t schema);
|
||||
// Emits the assembly text for the given instruction.
|
||||
spv_result_t HandleInstruction(const spv_parsed_instruction_t& inst);
|
||||
|
||||
// If not printing, populates text_result with the accumulated text.
|
||||
// Returns SPV_SUCCESS on success.
|
||||
spv_result_t SaveTextResult(spv_text* text_result) const;
|
||||
|
||||
private:
|
||||
// Emits an operand for the given instruction, where the instruction
|
||||
// is at offset words from the start of the binary.
|
||||
void EmitOperand(const spv_parsed_instruction_t& inst,
|
||||
const uint16_t operand_index);
|
||||
|
||||
// Emits a mask expression for the given mask word of the specified type.
|
||||
void EmitMaskOperand(const spv_operand_type_t type, const uint32_t word);
|
||||
|
||||
// Returns true if using the assignment-oriented format.
|
||||
// Otherwise, emit canonical form.
|
||||
bool IsAssignmentForm() const {
|
||||
return format_ == SPV_ASSEMBLY_SYNTAX_FORMAT_ASSIGNMENT;
|
||||
}
|
||||
|
||||
// Resets the output color, if color is turned on.
|
||||
void ResetColor() {
|
||||
if (color_) out_.get() << clr::reset();
|
||||
}
|
||||
// Sets the output to grey, if color is turned on.
|
||||
void SetGrey() {
|
||||
if (color_) out_.get() << clr::grey();
|
||||
}
|
||||
// Sets the output to blue, if color is turned on.
|
||||
void SetBlue() {
|
||||
if (color_) out_.get() << clr::blue();
|
||||
}
|
||||
// Sets the output to yellow, if color is turned on.
|
||||
void SetYellow() {
|
||||
if (color_) out_.get() << clr::yellow();
|
||||
}
|
||||
// Sets the output to red, if color is turned on.
|
||||
void SetRed() {
|
||||
if (color_) out_.get() << clr::red();
|
||||
}
|
||||
// Sets the output to green, if color is turned on.
|
||||
void SetGreen() {
|
||||
if (color_) out_.get() << clr::green();
|
||||
}
|
||||
|
||||
// The SPIR-V binary. The endianness is not necessarily converted
|
||||
// to native endianness.
|
||||
const uint32_t* const words_;
|
||||
const size_t num_words_;
|
||||
const libspirv::AssemblyGrammar& grammar_;
|
||||
const spv_assembly_syntax_format_t format_;
|
||||
const bool print_; // Should we also print to the standard output stream?
|
||||
const bool color_; // Should we print in colour?
|
||||
spv_endianness_t endian_; // The detected endianness of the binary.
|
||||
std::stringstream text_; // Captures the text, if not printing.
|
||||
out_stream out_; // The Output stream. Either to text_ or standard output.
|
||||
std::ostream& stream_; // The output std::stream.
|
||||
};
|
||||
|
||||
spv_result_t Disassembler::HandleHeader(spv_endianness_t endian,
|
||||
uint32_t version, uint32_t generator,
|
||||
uint32_t id_bound, uint32_t schema) {
|
||||
endian_ = endian;
|
||||
|
||||
SetGrey();
|
||||
stream_ << "; SPIR-V\n"
|
||||
<< "; Version: " << version << "\n"
|
||||
<< "; Generator: " << spvGeneratorStr(generator) << "\n"
|
||||
<< "; Bound: " << id_bound << "\n"
|
||||
<< "; Schema: " << schema << "\n";
|
||||
ResetColor();
|
||||
|
||||
return SPV_SUCCESS;
|
||||
}
|
||||
|
||||
spv_result_t Disassembler::HandleInstruction(
|
||||
const spv_parsed_instruction_t& inst) {
|
||||
if (IsAssignmentForm() && inst.result_id) {
|
||||
SetBlue();
|
||||
stream_ << "%" << inst.result_id << " = ";
|
||||
ResetColor();
|
||||
}
|
||||
|
||||
stream_ << "Op" << spvOpcodeString(inst.opcode);
|
||||
|
||||
for (uint16_t i = 0; i < inst.num_operands; i++) {
|
||||
const spv_operand_type_t type = inst.operands[i].type;
|
||||
assert(type != SPV_OPERAND_TYPE_NONE);
|
||||
if (type == SPV_OPERAND_TYPE_RESULT_ID && IsAssignmentForm()) continue;
|
||||
stream_ << " ";
|
||||
EmitOperand(inst, i);
|
||||
}
|
||||
|
||||
stream_ << "\n";
|
||||
return SPV_SUCCESS;
|
||||
}
|
||||
|
||||
void Disassembler::EmitOperand(const spv_parsed_instruction_t& inst,
|
||||
const uint16_t operand_index) {
|
||||
assert(operand_index < inst.num_operands);
|
||||
const spv_parsed_operand_t& operand = inst.operands[operand_index];
|
||||
const size_t index = inst.offset + operand.offset;
|
||||
const uint32_t word = spvFixWord(words_[index], endian_);
|
||||
switch (operand.type) {
|
||||
case SPV_OPERAND_TYPE_RESULT_ID:
|
||||
SetBlue();
|
||||
stream_ << "%" << word;
|
||||
break;
|
||||
case SPV_OPERAND_TYPE_ID:
|
||||
case SPV_OPERAND_TYPE_TYPE_ID:
|
||||
case SPV_OPERAND_TYPE_EXECUTION_SCOPE:
|
||||
case SPV_OPERAND_TYPE_MEMORY_SEMANTICS:
|
||||
SetYellow();
|
||||
stream_ << "%" << word;
|
||||
break;
|
||||
case SPV_OPERAND_TYPE_EXTENSION_INSTRUCTION_NUMBER: {
|
||||
spv_ext_inst_desc ext_inst;
|
||||
if (grammar_.lookupExtInst(inst.ext_inst_type, word, &ext_inst))
|
||||
assert(false && "should have caught this earlier");
|
||||
SetRed();
|
||||
stream_ << ext_inst->name;
|
||||
} break;
|
||||
case SPV_OPERAND_TYPE_LITERAL_INTEGER:
|
||||
case SPV_OPERAND_TYPE_MULTIWORD_LITERAL_NUMBER: {
|
||||
SetRed();
|
||||
// TODO(dneto): Emit values according to type.
|
||||
if (operand.num_words == 1)
|
||||
stream_ << word;
|
||||
else if (operand.num_words == 2)
|
||||
stream_ << spvFixDoubleWord(words_[index], words_[index + 1], endian_);
|
||||
else {
|
||||
// TODO(dneto): Support more than 64-bits at a time.
|
||||
assert("Unhandled");
|
||||
}
|
||||
} break;
|
||||
case SPV_OPERAND_TYPE_LITERAL_STRING: {
|
||||
// Assumes little-endian.
|
||||
// TODO(dneto): Make and use spvFixString(&words_[index], endian_);
|
||||
const std::string string(reinterpret_cast<const char*>(&words_[index]));
|
||||
stream_ << "\"";
|
||||
SetGreen();
|
||||
for (auto ch : string) {
|
||||
if (ch == '"' || ch == '\\') stream_ << '\\';
|
||||
stream_ << ch;
|
||||
}
|
||||
ResetColor();
|
||||
stream_ << '"';
|
||||
} break;
|
||||
case SPV_OPERAND_TYPE_CAPABILITY:
|
||||
case SPV_OPERAND_TYPE_SOURCE_LANGUAGE:
|
||||
case SPV_OPERAND_TYPE_EXECUTION_MODEL:
|
||||
case SPV_OPERAND_TYPE_ADDRESSING_MODEL:
|
||||
case SPV_OPERAND_TYPE_MEMORY_MODEL:
|
||||
case SPV_OPERAND_TYPE_EXECUTION_MODE:
|
||||
case SPV_OPERAND_TYPE_OPTIONAL_EXECUTION_MODE:
|
||||
case SPV_OPERAND_TYPE_STORAGE_CLASS:
|
||||
case SPV_OPERAND_TYPE_DIMENSIONALITY:
|
||||
case SPV_OPERAND_TYPE_SAMPLER_ADDRESSING_MODE:
|
||||
case SPV_OPERAND_TYPE_SAMPLER_FILTER_MODE:
|
||||
case SPV_OPERAND_TYPE_FP_ROUNDING_MODE:
|
||||
case SPV_OPERAND_TYPE_LINKAGE_TYPE:
|
||||
case SPV_OPERAND_TYPE_ACCESS_QUALIFIER:
|
||||
case SPV_OPERAND_TYPE_FUNCTION_PARAMETER_ATTRIBUTE:
|
||||
case SPV_OPERAND_TYPE_DECORATION:
|
||||
case SPV_OPERAND_TYPE_BUILT_IN:
|
||||
case SPV_OPERAND_TYPE_GROUP_OPERATION:
|
||||
case SPV_OPERAND_TYPE_KERNEL_ENQ_FLAGS:
|
||||
case SPV_OPERAND_TYPE_KERNEL_PROFILING_INFO: {
|
||||
spv_operand_desc entry;
|
||||
if (grammar_.lookupOperand(operand.type, word, &entry))
|
||||
assert(false && "should have caught this earlier");
|
||||
stream_ << entry->name;
|
||||
} break;
|
||||
case SPV_OPERAND_TYPE_FP_FAST_MATH_MODE:
|
||||
case SPV_OPERAND_TYPE_FUNCTION_CONTROL:
|
||||
case SPV_OPERAND_TYPE_LOOP_CONTROL:
|
||||
case SPV_OPERAND_TYPE_OPTIONAL_IMAGE:
|
||||
case SPV_OPERAND_TYPE_OPTIONAL_MEMORY_ACCESS:
|
||||
case SPV_OPERAND_TYPE_SELECTION_CONTROL:
|
||||
EmitMaskOperand(operand.type, word);
|
||||
break;
|
||||
default:
|
||||
assert(false && "unhandled or invalid case");
|
||||
}
|
||||
ResetColor();
|
||||
}
|
||||
|
||||
void Disassembler::EmitMaskOperand(const spv_operand_type_t type,
|
||||
const uint32_t word) {
|
||||
// Scan the mask from least significant bit to most significant bit. For each
|
||||
// set bit, emit the name of that bit. Separate multiple names with '|'.
|
||||
uint32_t remaining_word = word;
|
||||
uint32_t mask;
|
||||
int num_emitted = 0;
|
||||
for (mask = 1; remaining_word; mask <<= 1) {
|
||||
if (remaining_word & mask) {
|
||||
remaining_word ^= mask;
|
||||
spv_operand_desc entry;
|
||||
if (grammar_.lookupOperand(type, mask, &entry))
|
||||
assert(false && "should have caught this earlier");
|
||||
if (num_emitted) stream_ << "|";
|
||||
stream_ << entry->name;
|
||||
num_emitted++;
|
||||
}
|
||||
}
|
||||
if (!num_emitted) {
|
||||
// An operand value of 0 was provided, so represent it by the name
|
||||
// of the 0 value. In many cases, that's "None".
|
||||
spv_operand_desc entry;
|
||||
if (SPV_SUCCESS == grammar_.lookupOperand(type, 0, &entry))
|
||||
stream_ << entry->name;
|
||||
}
|
||||
}
|
||||
|
||||
spv_result_t Disassembler::SaveTextResult(spv_text* text_result) const {
|
||||
if (!print_) {
|
||||
size_t length = text_.str().size();
|
||||
char* str = new char[length + 1];
|
||||
if (!str) return SPV_ERROR_OUT_OF_MEMORY;
|
||||
strncpy(str, text_.str().c_str(), length + 1);
|
||||
spv_text text = new spv_text_t();
|
||||
if (!text) {
|
||||
delete[] str;
|
||||
return SPV_ERROR_OUT_OF_MEMORY;
|
||||
}
|
||||
text->str = str;
|
||||
text->length = length;
|
||||
*text_result = text;
|
||||
}
|
||||
return SPV_SUCCESS;
|
||||
}
|
||||
|
||||
spv_result_t DisassembleHeader(void* user_data, spv_endianness_t endian,
|
||||
uint32_t /* magic */, uint32_t version,
|
||||
uint32_t generator, uint32_t id_bound,
|
||||
uint32_t schema) {
|
||||
assert(user_data);
|
||||
auto disassembler = static_cast<Disassembler*>(user_data);
|
||||
return disassembler->HandleHeader(endian, version, generator, id_bound,
|
||||
schema);
|
||||
}
|
||||
|
||||
spv_result_t DisassembleInstruction(
|
||||
void* user_data, const spv_parsed_instruction_t* parsed_instruction) {
|
||||
assert(user_data);
|
||||
auto disassembler = static_cast<Disassembler*>(user_data);
|
||||
return disassembler->HandleInstruction(*parsed_instruction);
|
||||
}
|
||||
|
||||
} // anonymous namespace
|
||||
|
||||
spv_result_t spvBinaryToText(const uint32_t* code, const uint64_t wordCount,
|
||||
const uint32_t options,
|
||||
|
@ -42,3 +329,26 @@ spv_result_t spvBinaryToText(const uint32_t* code, const uint64_t wordCount,
|
|||
code, wordCount, options, opcodeTable, operandTable, extInstTable,
|
||||
SPV_ASSEMBLY_SYNTAX_FORMAT_DEFAULT, pText, pDiagnostic);
|
||||
}
|
||||
|
||||
spv_result_t spvBinaryToTextWithFormat(
|
||||
uint32_t const* code, const uint64_t wordCount, const uint32_t options,
|
||||
const spv_opcode_table opcode_table, const spv_operand_table operand_table,
|
||||
const spv_ext_inst_table ext_inst_table,
|
||||
spv_assembly_syntax_format_t format, spv_text* pText,
|
||||
spv_diagnostic* pDiagnostic) {
|
||||
// Invalid arguments return error codes, but don't necessarily generate
|
||||
// diagnostics. These are programmer errors, not user errors.
|
||||
if (!pDiagnostic) return SPV_ERROR_INVALID_DIAGNOSTIC;
|
||||
const libspirv::AssemblyGrammar grammar(operand_table, opcode_table,
|
||||
ext_inst_table);
|
||||
if (!grammar.isValid()) return SPV_ERROR_INVALID_TABLE;
|
||||
|
||||
Disassembler disassembler(grammar, code, wordCount, format, options);
|
||||
if (auto error =
|
||||
spvBinaryParse(&disassembler, code, wordCount, DisassembleHeader,
|
||||
DisassembleInstruction, pDiagnostic)) {
|
||||
return error;
|
||||
}
|
||||
|
||||
return disassembler.SaveTextResult(pText);
|
||||
}
|
||||
|
|
|
@ -77,4 +77,12 @@ TEST_F(BinaryHeaderGet, InvalidPointerHeader) {
|
|||
spvBinaryHeaderGet(&binary, SPV_ENDIANNESS_LITTLE, nullptr));
|
||||
}
|
||||
|
||||
TEST_F(BinaryHeaderGet, TruncatedHeader) {
|
||||
for (int i = 1; i < SPV_INDEX_INSTRUCTION; i++) {
|
||||
binary.wordCount = i;
|
||||
ASSERT_EQ(SPV_ERROR_INVALID_BINARY,
|
||||
spvBinaryHeaderGet(&binary, SPV_ENDIANNESS_LITTLE, nullptr));
|
||||
}
|
||||
}
|
||||
|
||||
} // anonymous namespace
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
// Copyright (c) 2015 The Khronos Group Inc.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a
|
||||
// copy of this software and/or associated documentation files (the
|
||||
// "Materials"), to deal in the Materials without restriction, including
|
||||
// without limitation the rights to use, copy, modify, merge, publish,
|
||||
// distribute, sublicense, and/or sell copies of the Materials, and to
|
||||
// permit persons to whom the Materials are furnished to do so, subject to
|
||||
// the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included
|
||||
// in all copies or substantial portions of the Materials.
|
||||
//
|
||||
// MODIFICATIONS TO THIS FILE MAY MEAN IT NO LONGER ACCURATELY REFLECTS
|
||||
// KHRONOS STANDARDS. THE UNMODIFIED, NORMATIVE VERSIONS OF KHRONOS
|
||||
// SPECIFICATIONS AND HEADER INFORMATION ARE LOCATED AT
|
||||
// https://www.khronos.org/registry/
|
||||
//
|
||||
// THE MATERIALS ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
// CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
// MATERIALS OR THE USE OR OTHER DEALINGS IN THE MATERIALS.
|
||||
|
||||
namespace {
|
||||
|
||||
// TODO(dneto): Add tests for spvBinaryParse:
|
||||
// - early return via callback error values.
|
||||
// - test each diagnostic in binary.cpp
|
||||
// - test the data sent via the header callback.
|
||||
// - test the data sent via the instruction callback.
|
||||
|
||||
} // anonymous namespace
|
|
@ -27,8 +27,10 @@
|
|||
|
||||
#include "UnitSPIRV.h"
|
||||
|
||||
#include "gmock/gmock.h"
|
||||
#include <sstream>
|
||||
|
||||
#include "TestFixture.h"
|
||||
#include "gmock/gmock.h"
|
||||
|
||||
using ::testing::Eq;
|
||||
using spvtest::AutoText;
|
||||
|
@ -79,6 +81,14 @@ class BinaryToText : public ::testing::Test {
|
|||
|
||||
virtual void TearDown() { spvBinaryDestroy(binary); }
|
||||
|
||||
// Compiles the given assembly text, and saves it into 'binary'.
|
||||
void CompileSuccessfully(std::string text) {
|
||||
spv_diagnostic diagnostic = nullptr;
|
||||
EXPECT_EQ(SPV_SUCCESS, spvTextToBinary(text.c_str(), text.size(),
|
||||
opcodeTable, operandTable,
|
||||
extInstTable, &binary, &diagnostic));
|
||||
}
|
||||
|
||||
spv_binary binary;
|
||||
spv_opcode_table opcodeTable;
|
||||
spv_operand_table operandTable;
|
||||
|
@ -96,20 +106,61 @@ TEST_F(BinaryToText, Default) {
|
|||
spvTextDestroy(text);
|
||||
}
|
||||
|
||||
TEST_F(BinaryToText, InvalidCode) {
|
||||
spv_binary_t binary = {nullptr, 42};
|
||||
TEST_F(BinaryToText, MissingModule) {
|
||||
spv_text text;
|
||||
spv_diagnostic diagnostic = nullptr;
|
||||
ASSERT_EQ(
|
||||
EXPECT_EQ(
|
||||
SPV_ERROR_INVALID_BINARY,
|
||||
spvBinaryToText(nullptr, 42, SPV_BINARY_TO_TEXT_OPTION_NONE, opcodeTable,
|
||||
operandTable, extInstTable, &text, &diagnostic));
|
||||
EXPECT_THAT(diagnostic->error, Eq(std::string("Missing module.")));
|
||||
if (diagnostic) {
|
||||
spvDiagnosticPrint(diagnostic);
|
||||
spvDiagnosticDestroy(diagnostic);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(BinaryToText, TruncatedModule) {
|
||||
// Make a valid module with zero instructions.
|
||||
CompileSuccessfully("");
|
||||
EXPECT_EQ(SPV_INDEX_INSTRUCTION, binary->wordCount);
|
||||
|
||||
for (int length = 0; length < SPV_INDEX_INSTRUCTION; length++) {
|
||||
spv_text text = nullptr;
|
||||
spv_diagnostic diagnostic = nullptr;
|
||||
EXPECT_EQ(SPV_ERROR_INVALID_BINARY,
|
||||
spvBinaryToText(binary->code, length,
|
||||
SPV_BINARY_TO_TEXT_OPTION_NONE, opcodeTable,
|
||||
operandTable, extInstTable, &text, &diagnostic));
|
||||
ASSERT_NE(nullptr, diagnostic);
|
||||
std::stringstream expected;
|
||||
expected << "Module has incomplete header: only " << length
|
||||
<< " words instead of " << SPV_INDEX_INSTRUCTION;
|
||||
EXPECT_THAT(diagnostic->error, Eq(expected.str()));
|
||||
spvDiagnosticDestroy(diagnostic);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(BinaryToText, InvalidMagicNumber) {
|
||||
CompileSuccessfully("");
|
||||
std::vector<uint32_t> damaged_binary(binary->code,
|
||||
binary->code + binary->wordCount);
|
||||
damaged_binary[SPV_INDEX_MAGIC_NUMBER] ^= 123;
|
||||
|
||||
spv_diagnostic diagnostic = nullptr;
|
||||
spv_text text;
|
||||
EXPECT_EQ(SPV_ERROR_INVALID_BINARY,
|
||||
spvBinaryToText(damaged_binary.data(), damaged_binary.size(),
|
||||
SPV_BINARY_TO_TEXT_OPTION_NONE, opcodeTable,
|
||||
operandTable, extInstTable, &text, &diagnostic));
|
||||
ASSERT_NE(nullptr, diagnostic);
|
||||
std::stringstream expected;
|
||||
expected << "Invalid SPIR-V magic number '" << std::hex
|
||||
<< damaged_binary[SPV_INDEX_MAGIC_NUMBER] << "'.";
|
||||
EXPECT_THAT(diagnostic->error, Eq(expected.str()));
|
||||
spvDiagnosticDestroy(diagnostic);
|
||||
}
|
||||
|
||||
TEST_F(BinaryToText, InvalidTable) {
|
||||
spv_text text;
|
||||
spv_diagnostic diagnostic = nullptr;
|
||||
|
@ -153,20 +204,48 @@ TEST_P(BinaryToTextFail, EncodeSuccessfullyDecodeFailed) {
|
|||
Eq(GetParam().expected_error_message));
|
||||
}
|
||||
|
||||
INSTANTIATE_TEST_CASE_P(InvalidIds, BinaryToTextFail,
|
||||
::testing::ValuesIn(std::vector<FailedDecodeCase>{
|
||||
{"%1 = OpTypeVoid",
|
||||
spvtest::MakeInstruction(SpvOpTypeVoid, {1}),
|
||||
"Id 1 is defined more than once"},
|
||||
{"%1 = OpTypeVoid\n"
|
||||
"%2 = OpNot %1 %foo",
|
||||
spvtest::MakeInstruction(SpvOpNot, {1, 2, 3}),
|
||||
"Id 2 is defined more than once"},
|
||||
{"%1 = OpTypeVoid\n"
|
||||
"%2 = OpNot %1 %foo",
|
||||
spvtest::MakeInstruction(SpvOpNot, {1, 1, 3}),
|
||||
"Id 1 is defined more than once"},
|
||||
}));
|
||||
INSTANTIATE_TEST_CASE_P(
|
||||
InvalidIds, BinaryToTextFail,
|
||||
::testing::ValuesIn(std::vector<FailedDecodeCase>{
|
||||
{"", spvtest::MakeInstruction(SpvOpTypeVoid, {0}),
|
||||
"Error: Result Id is 0"},
|
||||
{"", spvtest::MakeInstruction(SpvOpConstant, {0, 1, 42}),
|
||||
"Error: Type Id is 0"},
|
||||
{"%1 = OpTypeVoid", spvtest::MakeInstruction(SpvOpTypeVoid, {1}),
|
||||
"Id 1 is defined more than once"},
|
||||
{"%1 = OpTypeVoid\n"
|
||||
"%2 = OpNot %1 %foo",
|
||||
spvtest::MakeInstruction(SpvOpNot, {1, 2, 3}),
|
||||
"Id 2 is defined more than once"},
|
||||
{"%1 = OpTypeVoid\n"
|
||||
"%2 = OpNot %1 %foo",
|
||||
spvtest::MakeInstruction(SpvOpNot, {1, 1, 3}),
|
||||
"Id 1 is defined more than once"},
|
||||
// The following are the two failure cases for
|
||||
// Parser::setNumericTypeInfoForType.
|
||||
{"", spvtest::MakeInstruction(SpvOpConstant, {500, 1, 42}),
|
||||
"Type Id 500 is not a type"},
|
||||
{"%1 = OpTypeInt 32 0\n"
|
||||
"%2 = OpTypeVector %1 4",
|
||||
spvtest::MakeInstruction(SpvOpConstant, {2, 3, 999}),
|
||||
"Type Id 2 is not a scalar numeric type"},
|
||||
}));
|
||||
|
||||
INSTANTIATE_TEST_CASE_P(
|
||||
InvalidIdsCheckedDuringLiteralCaseParsing, BinaryToTextFail,
|
||||
::testing::ValuesIn(std::vector<FailedDecodeCase>{
|
||||
{"", spvtest::MakeInstruction(SpvOpSwitch, {1, 2, 3, 4}),
|
||||
"Invalid OpSwitch: selector id 1 has no type"},
|
||||
{"%1 = OpTypeVoid\n",
|
||||
spvtest::MakeInstruction(SpvOpSwitch, {1, 2, 3, 4}),
|
||||
"Invalid OpSwitch: selector id 1 is a type, not a value"},
|
||||
{"%1 = OpConstantTrue !500",
|
||||
spvtest::MakeInstruction(SpvOpSwitch, {1, 2, 3, 4}),
|
||||
"Type Id 500 is not a type"},
|
||||
{"%1 = OpTypeFloat 32\n%2 = OpConstant %1 1.5",
|
||||
spvtest::MakeInstruction(SpvOpSwitch, {2, 3, 4, 5}),
|
||||
"Invalid OpSwitch: selector id 2 is not a scalar integer"},
|
||||
}));
|
||||
|
||||
TEST(BinaryToTextSmall, OneInstruction) {
|
||||
// TODO(dneto): This test could/should be refactored.
|
||||
|
|
Загрузка…
Ссылка в новой задаче