mirror of
https://github.com/baldurk/renderdoc.git
synced 2026-08-18 04:26:49 +00:00
Add D3D11 handling of swizzled UAV loads/stores
* Ensuring we properly clamp for non-typed UAV stores. Typed UAV stores are 'easy' because they must always write all components.
This commit is contained in:
@@ -3342,6 +3342,7 @@ void ThreadState::StepNext(ShaderDebugState *state, DebugAPIWrapper *apiWrapper,
|
||||
firstElem = 0;
|
||||
if(resIndex > global.groupshared.size())
|
||||
{
|
||||
RDCERR("Invalid dxbc bytecode - garbage groupshared register being referenced");
|
||||
numElems = 0;
|
||||
stride = 4;
|
||||
data = NULL;
|
||||
@@ -3439,34 +3440,86 @@ void ThreadState::StepNext(ShaderDebugState *state, DebugAPIWrapper *apiWrapper,
|
||||
{
|
||||
data += dataOffset;
|
||||
|
||||
int maxIndex = fmt.numComps;
|
||||
|
||||
uint32_t srcIdx = 1;
|
||||
if(op.operation == OPCODE_STORE_STRUCTURED || op.operation == OPCODE_LD_STRUCTURED)
|
||||
if(op.operation == OPCODE_LD_STRUCTURED)
|
||||
{
|
||||
srcIdx = 2;
|
||||
maxIndex = (stride - structOffset) / sizeof(uint32_t);
|
||||
fmt.byteWidth = 4;
|
||||
|
||||
fmt.numComps = 4;
|
||||
|
||||
if(op.operands[0].comps[0] != 0xff && op.operands[0].comps[1] == 0xff &&
|
||||
op.operands[0].comps[2] == 0xff && op.operands[0].comps[3] == 0xff)
|
||||
fmt.numComps = 1;
|
||||
}
|
||||
else if(op.operation == OPCODE_STORE_STRUCTURED)
|
||||
{
|
||||
srcIdx = 2;
|
||||
fmt.byteWidth = 4;
|
||||
|
||||
// set number of components based on output register write mask
|
||||
if(op.operands[0].comps[1] == 0xff)
|
||||
fmt.numComps = 1;
|
||||
else if(op.operands[0].comps[2] == 0xff)
|
||||
fmt.numComps = 2;
|
||||
else if(op.operands[0].comps[3] == 0xff)
|
||||
fmt.numComps = 3;
|
||||
else
|
||||
fmt.numComps = 4;
|
||||
|
||||
// do not allow writing beyond the stride (we don't expect fxc to emit writes like this anyway)
|
||||
fmt.numComps = RDCMIN(fmt.numComps, int((stride - structOffset) / sizeof(uint32_t)));
|
||||
|
||||
for(int c = 0; c < 4; c++)
|
||||
{
|
||||
if(c < fmt.numComps)
|
||||
RDCASSERTEQUAL(op.operands[0].comps[c], c);
|
||||
else
|
||||
RDCASSERT(op.operands[0].comps[c] == 0xff, c, op.operands[0].comps[c]);
|
||||
}
|
||||
|
||||
fmt.fmt = CompType::UInt;
|
||||
}
|
||||
// raw loads/stores can come from any component (as long as it's within range of the data!)
|
||||
if(op.operation == OPCODE_LD_RAW || op.operation == OPCODE_STORE_RAW)
|
||||
else if(op.operation == OPCODE_LD_RAW)
|
||||
{
|
||||
fmt.byteWidth = 4;
|
||||
|
||||
// normally we can read 4 elements
|
||||
fmt.numComps = 4;
|
||||
|
||||
// clamp to out of bounds based on numElems
|
||||
fmt.numComps = RDCMIN(fmt.numComps, int(numElems - elemIdx) / 4);
|
||||
maxIndex = fmt.numComps;
|
||||
|
||||
if(op.operands[0].comps[0] != 0xff && op.operands[0].comps[1] == 0xff &&
|
||||
op.operands[0].comps[2] == 0xff && op.operands[0].comps[3] == 0xff)
|
||||
fmt.numComps = 1;
|
||||
}
|
||||
else if(op.operation == OPCODE_STORE_RAW)
|
||||
{
|
||||
fmt.byteWidth = 4;
|
||||
|
||||
// set number of components based on output register write mask
|
||||
if(op.operands[0].comps[1] == 0xff)
|
||||
fmt.numComps = 1;
|
||||
else if(op.operands[0].comps[2] == 0xff)
|
||||
fmt.numComps = 2;
|
||||
else if(op.operands[0].comps[3] == 0xff)
|
||||
fmt.numComps = 3;
|
||||
else
|
||||
fmt.numComps = 4;
|
||||
|
||||
// clamp to out of bounds based on numElems
|
||||
fmt.numComps = RDCMIN(fmt.numComps, int(numElems - elemIdx) / 4);
|
||||
|
||||
for(int c = 0; c < 4; c++)
|
||||
{
|
||||
if(c < fmt.numComps)
|
||||
RDCASSERTEQUAL(op.operands[0].comps[c], c);
|
||||
else
|
||||
RDCASSERT(op.operands[0].comps[c] == 0xff, c, op.operands[0].comps[c]);
|
||||
}
|
||||
|
||||
fmt.fmt = CompType::UInt;
|
||||
}
|
||||
|
||||
@@ -3500,15 +3553,15 @@ void ThreadState::StepNext(ShaderDebugState *state, DebugAPIWrapper *apiWrapper,
|
||||
}
|
||||
else if(!Finished()) // helper/inactive pixels can't modify UAVs
|
||||
{
|
||||
for(int i = 0; i < 4; i++)
|
||||
{
|
||||
uint8_t comp = op.operands[0].comps[i];
|
||||
// masks must be contiguous from x, if we reach the 'end' we're done
|
||||
if(comp == 0xff || comp >= maxIndex)
|
||||
break;
|
||||
|
||||
TypedUAVStore(fmt, data, srcOpers[srcIdx]);
|
||||
}
|
||||
// from the spec on a typed UAV store:
|
||||
// dstUAV always has a .xyzw write mask.
|
||||
// All components must be written.
|
||||
//
|
||||
// for raw/structured stores, we've already set the format to be the right number of
|
||||
// components
|
||||
//
|
||||
// so we can ignore op.operands[0].comps[] entirely and just write the data
|
||||
TypedUAVStore(fmt, data, srcOpers[srcIdx]);
|
||||
|
||||
if(gsm && state)
|
||||
{
|
||||
|
||||
@@ -538,7 +538,7 @@ rdcstr toString(const uint32_t values[], uint32_t numComps)
|
||||
|
||||
if(floatOutput)
|
||||
{
|
||||
str += ToStr(vf[0]);
|
||||
str += StringFormat::Fmt("%0.6f", vf[0]);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -261,8 +261,8 @@ float4 main(v2f IN) : SV_Target0
|
||||
// use this to ensure the compiler doesn't know we're using fixed locations
|
||||
uint z = intval - IN.tri - 7;
|
||||
|
||||
return float4(asfloat(byterotest.Load(z+40).x), asfloat(byterotest.Load(z+44).x),
|
||||
asfloat(byterotest.Load(z+48).x), float(byterotest.Load(z+4096).x));
|
||||
return float4(asfloat(byterotest.Load(z+88).x), asfloat(byterotest.Load(z+92).x),
|
||||
asfloat(byterotest.Load(z+96).x), float(byterotest.Load(z+4096).x));
|
||||
}
|
||||
// 4-uint load
|
||||
if(IN.tri == 37)
|
||||
@@ -280,7 +280,7 @@ float4 main(v2f IN) : SV_Target0
|
||||
uint z = intval - IN.tri - 7;
|
||||
|
||||
// test a 4-uint load
|
||||
return asfloat(byterotest.Load4(z+40));
|
||||
return asfloat(byterotest.Load4(z+88));
|
||||
}
|
||||
// 4-uint load out of view bounds
|
||||
if(IN.tri == 39)
|
||||
@@ -289,7 +289,7 @@ float4 main(v2f IN) : SV_Target0
|
||||
uint z = intval - IN.tri - 7;
|
||||
|
||||
// test a 4-uint load
|
||||
return asfloat(byterotest.Load4(z+48));
|
||||
return asfloat(byterotest.Load4(z+96));
|
||||
}
|
||||
|
||||
// mis-aligned store
|
||||
@@ -325,13 +325,13 @@ float4 main(v2f IN) : SV_Target0
|
||||
uint z = intval - IN.tri - 7;
|
||||
uint z2 = uint(zero);
|
||||
|
||||
byterwtest.Store(z+40, asuint(1.2345f));
|
||||
byterwtest.Store(z+44, asuint(9.8765f));
|
||||
byterwtest.Store(z+48, asuint(1.81818f));
|
||||
byterwtest.Store(z+88, asuint(1.2345f));
|
||||
byterwtest.Store(z+92, asuint(9.8765f));
|
||||
byterwtest.Store(z+96, asuint(1.81818f));
|
||||
byterwtest.Store(z+4096, asuint(5.55555f));
|
||||
|
||||
return float4(asfloat(byterwtest.Load(z2+40).x), asfloat(byterwtest.Load(z2+44).x),
|
||||
asfloat(byterwtest.Load(z2+48).x), float(byterwtest.Load(z2+4096).x));
|
||||
return float4(asfloat(byterwtest.Load(z2+88).x), asfloat(byterwtest.Load(z2+92).x),
|
||||
asfloat(byterwtest.Load(z2+96).x), float(byterwtest.Load(z2+4096).x));
|
||||
}
|
||||
// 4-uint store
|
||||
if(IN.tri == 43)
|
||||
@@ -342,7 +342,7 @@ float4 main(v2f IN) : SV_Target0
|
||||
|
||||
byterwtest.Store4(z+24, uint4(99, 88, 77, 66));
|
||||
|
||||
return asfloat(byterotest.Load4(z2+24));
|
||||
return asfloat(byterwtest.Load4(z2+24));
|
||||
}
|
||||
// 4-uint store crossing view bounds
|
||||
if(IN.tri == 44)
|
||||
@@ -351,9 +351,9 @@ float4 main(v2f IN) : SV_Target0
|
||||
uint z = intval - IN.tri - 7;
|
||||
uint z2 = uint(zero);
|
||||
|
||||
byterwtest.Store4(z+40, uint4(99, 88, 77, 66));
|
||||
byterwtest.Store4(z+88, uint4(99, 88, 77, 66));
|
||||
|
||||
return asfloat(byterotest.Load4(z2+40));
|
||||
return asfloat(byterwtest.Load4(z2+88));
|
||||
}
|
||||
// 4-uint store out of view bounds
|
||||
if(IN.tri == 45)
|
||||
@@ -362,9 +362,9 @@ float4 main(v2f IN) : SV_Target0
|
||||
uint z = intval - IN.tri - 7;
|
||||
uint z2 = uint(zero);
|
||||
|
||||
byterwtest.Store4(z+48, uint4(99, 88, 77, 66));
|
||||
byterwtest.Store4(z+96, uint4(99, 88, 77, 66));
|
||||
|
||||
return asfloat(byterotest.Load4(z2+48));
|
||||
return asfloat(byterwtest.Load4(z2+96));
|
||||
}
|
||||
|
||||
// test reading/writing structured data
|
||||
@@ -751,6 +751,71 @@ float4 main(v2f IN) : SV_Target0
|
||||
float2 uv = posone * float2(0.55f, 0.48f);
|
||||
return smiley.SampleBias(unboundsamp, uv, 0.5f);
|
||||
}
|
||||
// test UAV loads and stores only write the data they should
|
||||
#ifdef TYPED_UAV_EXT
|
||||
if(IN.tri == 93)
|
||||
{
|
||||
// typed UAVs have to write all components so this is a fairly degenerate test
|
||||
typedrwtest[uint(zero) + 20] = 9.99999f.xxxx;
|
||||
return typedrwtest[uint(posone) + 19];
|
||||
}
|
||||
#endif
|
||||
if(IN.tri == 94)
|
||||
{
|
||||
uint z = intval - IN.tri - 7;
|
||||
uint z2 = uint(zero);
|
||||
uint z3 = uint(posone) - 1;
|
||||
|
||||
// fill the first component, to ensure we return the real result and not a trashed-zero
|
||||
byterwtest.Store(z3+48, asuint(1.1f));
|
||||
|
||||
// unaligned raw store of less than float4
|
||||
byterwtest.Store3(z+52, asuint(float3(9.9f, 8.8f, 7.7f)));
|
||||
|
||||
return asfloat(byterwtest.Load4(z2+48));
|
||||
}
|
||||
if(IN.tri == 95)
|
||||
{
|
||||
uint z = intval - IN.tri - 7;
|
||||
uint z2 = uint(zero);
|
||||
uint z3 = uint(posone) - 1;
|
||||
|
||||
// fill the last component, to ensure we return the real result and not a trashed-zero
|
||||
byterwtest.Store(z3+44, asuint(1.1f));
|
||||
|
||||
// unaligned raw store of less than float4
|
||||
byterwtest.Store3(z+32, asuint(float3(9.9f, 8.8f, 7.7f)));
|
||||
|
||||
return asfloat(byterwtest.Load4(z2+32));
|
||||
}
|
||||
if(IN.tri == 96)
|
||||
{
|
||||
uint z = intval - IN.tri - 7;
|
||||
uint z2 = uint(zero);
|
||||
uint z3 = uint(posone) - 1;
|
||||
|
||||
// fill the last component, to ensure we return the real result and not a trashed-zero
|
||||
structrwtest[z+4].b.w = 1.1f;
|
||||
|
||||
// aligned store of float3
|
||||
structrwtest[z3+4].b.xzy = float3(1.234f, 5.678f, 9.999f);
|
||||
|
||||
return structrwtest[z2+4].b;
|
||||
}
|
||||
if(IN.tri == 97)
|
||||
{
|
||||
uint z = intval - IN.tri - 7;
|
||||
uint z2 = uint(zero);
|
||||
uint z3 = uint(posone) - 1;
|
||||
|
||||
// fill the first component, to ensure we return the real result and not a trashed-zero
|
||||
structrwtest[z+5].b.x = 1.1f;
|
||||
|
||||
// unaligned store of float3
|
||||
structrwtest[z3+5].b.wzy = float3(1.234f, 5.678f, 9.999f);
|
||||
|
||||
return structrwtest[z2+5].b;
|
||||
}
|
||||
|
||||
return float4(0.4f, 0.4f, 0.4f, 0.4f);
|
||||
}
|
||||
@@ -943,7 +1008,6 @@ float4 main(v2f IN, uint samp : SV_SampleIndex) : SV_Target0
|
||||
common += "\n#define TYPED_UAV_EXT 1\n";
|
||||
|
||||
ID3DBlobPtr vsblob = Compile(common + vertex, "main", "vs_5_0");
|
||||
ID3DBlobPtr psblob = Compile(common + pixel, "main", "ps_5_0");
|
||||
|
||||
D3D11_INPUT_ELEMENT_DESC layoutdesc[] = {
|
||||
{
|
||||
@@ -989,12 +1053,13 @@ float4 main(v2f IN, uint samp : SV_SampleIndex) : SV_Target0
|
||||
vsblob->GetBufferSize(), &layout));
|
||||
|
||||
ID3D11VertexShaderPtr vs = CreateVS(vsblob);
|
||||
ID3D11PixelShaderPtr ps = CreatePS(psblob);
|
||||
ID3D11PixelShaderPtr ps = CreatePS(Compile(common + pixel, "main", "ps_5_0", true));
|
||||
ID3D11PixelShaderPtr psopt = CreatePS(Compile(common + pixel, "main", "ps_5_0", false));
|
||||
ID3D11PixelShaderPtr flowps = CreatePS(Compile(common + flowPixel, "main", "ps_5_0"));
|
||||
|
||||
static const uint32_t texDim = AlignUp(numTests, 64U) * 4;
|
||||
|
||||
ID3D11Texture2DPtr fltTex = MakeTexture(DXGI_FORMAT_R32G32B32A32_FLOAT, texDim, 8).RTV();
|
||||
ID3D11Texture2DPtr fltTex = MakeTexture(DXGI_FORMAT_R32G32B32A32_FLOAT, texDim, 12).RTV();
|
||||
ID3D11RenderTargetViewPtr fltRT = MakeRTV(fltTex);
|
||||
|
||||
float triWidth = 8.0f / float(texDim);
|
||||
@@ -1035,7 +1100,7 @@ float4 main(v2f IN, uint samp : SV_SampleIndex) : SV_Target0
|
||||
|
||||
ID3D11BufferPtr rawBuf2 = MakeBuffer().UAV().ByteAddressed().Size(1024);
|
||||
ID3D11UnorderedAccessViewPtr rawuav =
|
||||
MakeUAV(rawBuf2).Format(DXGI_FORMAT_R32_TYPELESS).FirstElement(4).NumElements(12);
|
||||
MakeUAV(rawBuf2).Format(DXGI_FORMAT_R32_TYPELESS).FirstElement(4).NumElements(24);
|
||||
|
||||
float structdata[220];
|
||||
for(int i = 0; i < 220; i++)
|
||||
@@ -1050,7 +1115,7 @@ float4 main(v2f IN, uint samp : SV_SampleIndex) : SV_Target0
|
||||
|
||||
ID3D11BufferPtr structBuf2 = MakeBuffer().UAV().Structured(11 * sizeof(float)).Size(880);
|
||||
ID3D11UnorderedAccessViewPtr structuav =
|
||||
MakeUAV(structBuf2).Format(DXGI_FORMAT_UNKNOWN).FirstElement(3).NumElements(5);
|
||||
MakeUAV(structBuf2).Format(DXGI_FORMAT_UNKNOWN).FirstElement(3).NumElements(6);
|
||||
|
||||
ID3D11BufferPtr rgbuavBuf = MakeBuffer().UAV().Data(structdata);
|
||||
ID3D11UnorderedAccessViewPtr typeuav = MakeUAV(rgbuavBuf).Format(DXGI_FORMAT_R32G32B32A32_FLOAT);
|
||||
@@ -1121,6 +1186,11 @@ float4 main(v2f IN, uint samp : SV_SampleIndex) : SV_Target0
|
||||
ctx->DrawInstanced(3, numTests, 0, 0);
|
||||
|
||||
RSSetViewport({0.0f, 4.0f, (float)texDim, 4.0f, 0.0f, 1.0f});
|
||||
ctx->PSSetShader(psopt, NULL, 0);
|
||||
setMarker("Optimised Test");
|
||||
ctx->DrawInstanced(3, numTests, 0, 0);
|
||||
|
||||
RSSetViewport({0.0f, 8.0f, (float)texDim, 4.0f, 0.0f, 1.0f});
|
||||
ctx->PSSetShader(flowps, NULL, 0);
|
||||
setMarker("Flow Test");
|
||||
ctx->DrawInstanced(3, 1, 0, 0);
|
||||
|
||||
@@ -642,7 +642,7 @@ ID3DBlobPtr D3D11GraphicsTest::Compile(std::string src, std::string entry, std::
|
||||
if(skipoptimise)
|
||||
flags |= D3DCOMPILE_SKIP_OPTIMIZATION | D3DCOMPILE_OPTIMIZATION_LEVEL0;
|
||||
else
|
||||
flags |= D3DCOMPILE_OPTIMIZATION_LEVEL0;
|
||||
flags |= D3DCOMPILE_OPTIMIZATION_LEVEL1;
|
||||
|
||||
HRESULT hr = dyn_D3DCompile(src.c_str(), src.length(), "", NULL, NULL, entry.c_str(),
|
||||
profile.c_str(), flags, 0, &blob, &error);
|
||||
|
||||
@@ -7,43 +7,51 @@ class D3D11_Shader_Debug_Zoo(rdtest.TestCase):
|
||||
demos_test_name = 'D3D11_Shader_Debug_Zoo'
|
||||
|
||||
def check_capture(self):
|
||||
# Jump to the action
|
||||
action = self.find_action("Main Test").next
|
||||
|
||||
undefined_tests = [int(test) for test in self.find_action("Undefined tests: ").customName.split(" ")[2:]]
|
||||
|
||||
self.controller.SetFrameEvent(action.eventId, False)
|
||||
# Jump to the action
|
||||
for idx, action in enumerate([self.find_action("Main Test"), self.find_action("Optimised Test")]):
|
||||
name = action.customName
|
||||
|
||||
pipe: rd.PipeState = self.controller.GetPipelineState()
|
||||
action = action.next
|
||||
|
||||
failed = False
|
||||
self.controller.SetFrameEvent(action.eventId, False)
|
||||
|
||||
# Loop over every test
|
||||
rdtest.log.begin_section("General tests")
|
||||
for test in range(action.numInstances):
|
||||
# Debug the shader
|
||||
trace: rd.ShaderDebugTrace = self.controller.DebugPixel(4 * test, 0, rd.DebugPixelInputs())
|
||||
pipe: rd.PipeState = self.controller.GetPipelineState()
|
||||
|
||||
cycles, variables = self.process_trace(trace)
|
||||
failed = False
|
||||
|
||||
output = self.find_output_source_var(trace, rd.ShaderBuiltin.ColorOutput, 0)
|
||||
# Loop over every test
|
||||
rdtest.log.begin_section(name)
|
||||
for test in range(action.numInstances):
|
||||
# Debug the shader
|
||||
trace: rd.ShaderDebugTrace = self.controller.DebugPixel(4 * test, 4 * idx, rd.DebugPixelInputs())
|
||||
|
||||
debugged = self.evaluate_source_var(output, variables)
|
||||
if trace.debugger is None:
|
||||
rdtest.log.error("Test {} failed to debug.".format(test))
|
||||
self.controller.FreeTrace(trace)
|
||||
continue
|
||||
|
||||
try:
|
||||
self.check_pixel_value(pipe.GetOutputTargets()[0].resource, 4 * test, 0, debugged.value.f32v[0:4])
|
||||
except rdtest.TestFailureException as ex:
|
||||
if test in undefined_tests:
|
||||
rdtest.log.comment("Undefined test {} did not match. {}".format(test, str(ex)))
|
||||
else:
|
||||
rdtest.log.error("Test {} did not match. {}".format(test, str(ex)))
|
||||
failed = True
|
||||
continue
|
||||
finally:
|
||||
self.controller.FreeTrace(trace)
|
||||
cycles, variables = self.process_trace(trace)
|
||||
|
||||
rdtest.log.success("Test {} matched as expected".format(test))
|
||||
rdtest.log.end_section("General tests")
|
||||
output = self.find_output_source_var(trace, rd.ShaderBuiltin.ColorOutput, 0)
|
||||
|
||||
debugged = self.evaluate_source_var(output, variables)
|
||||
|
||||
try:
|
||||
self.check_pixel_value(pipe.GetOutputTargets()[0].resource, 4 * test, 4 * idx, debugged.value.f32v[0:4])
|
||||
except rdtest.TestFailureException as ex:
|
||||
if test in undefined_tests:
|
||||
rdtest.log.comment("Undefined test {} did not match. {}".format(test, str(ex)))
|
||||
else:
|
||||
rdtest.log.error("Test {} did not match. {}".format(test, str(ex)))
|
||||
failed = True
|
||||
continue
|
||||
finally:
|
||||
self.controller.FreeTrace(trace)
|
||||
|
||||
rdtest.log.success("Test {} matched as expected".format(test))
|
||||
rdtest.log.end_section(name)
|
||||
|
||||
rdtest.log.begin_section("Flow tests")
|
||||
action = self.find_action("Flow Test").next
|
||||
@@ -51,7 +59,7 @@ class D3D11_Shader_Debug_Zoo(rdtest.TestCase):
|
||||
pipe: rd.PipeState = self.controller.GetPipelineState()
|
||||
|
||||
# Debug the shader
|
||||
trace: rd.ShaderDebugTrace = self.controller.DebugPixel(0, 4, rd.DebugPixelInputs())
|
||||
trace: rd.ShaderDebugTrace = self.controller.DebugPixel(0, 8, rd.DebugPixelInputs())
|
||||
|
||||
cycles, variables = self.process_trace(trace)
|
||||
|
||||
@@ -60,8 +68,8 @@ class D3D11_Shader_Debug_Zoo(rdtest.TestCase):
|
||||
debugged = self.evaluate_source_var(output, variables)
|
||||
|
||||
try:
|
||||
self.check_pixel_value(pipe.GetOutputTargets()[0].resource, 0, 4, debugged.value.f32v[0:4])
|
||||
self.check_pixel_value(pipe.GetOutputTargets()[0].resource, 0, 4, [9.0, 66.0, 4.0, 18.0])
|
||||
self.check_pixel_value(pipe.GetOutputTargets()[0].resource, 0, 8, debugged.value.f32v[0:4])
|
||||
self.check_pixel_value(pipe.GetOutputTargets()[0].resource, 0, 8, [9.0, 66.0, 4.0, 18.0])
|
||||
except rdtest.TestFailureException as ex:
|
||||
raise rdtest.TestFailureException("Flow test did not match. {}".format(str(ex)))
|
||||
finally:
|
||||
|
||||
Reference in New Issue
Block a user