概述
IVR 系统的测试一般需要电话或软电话,拨号经由呼叫中心平台进入 IVR 系统进行测试,这种测试方式比较慢,且无法进行自动化测试。呼叫中心平台与 IVR 系统的交互使用VoiceXML标准协议,通过编写一个简易的 VoiceXML 解析器,实现对 IVR 系统的模拟请求与根据返回报文自动执行。
指令集
通过模仿计算机组成原理的相关知识,我们将每个标签转化为一个由操作码,操作数(一个或多个)的基本操作指令。
精简的 VoiceXML 报文示例
1 | <vxml version="2.1"> |
上述报文,我们可以定义出以下操作指令
- [ "var" , "_avayaExitReason" , "''" ]
- [ "catch" , "error.runtime" , 0 ]
- [ "goto" , "example" ]
- [ "form" , 0 ]
- [ "block" ]
- [ "throw" , "error.runtimeException" ]
vxml 中标签从上向下顺序执行,若子标签内有其他标签,会先进入子标签内执行,即以深度优先遍历的方式生成指令集。有些标签执行需要满足条件,若catch
,需要当前有对应的事件抛起时,才会进入标签内执行,所以我们还需要在catch
处,计算当条件不满足时,下一条指令的位置。按照上文的 demo 报文,我们可以生成如下报文;
var _avayaExitReason ''
var _avayaExitInfo1 ''
var _avayaExitInfo2 ''
var _avayaExitCustomerId ''
var _avayaExitPreferredPath '1'
var _avayaExitTopic ''
var _avayaExitParentId ''
catch error.runtime 9
goto example
form 12
block 12
throw error.runtime.Exception
end
当报文比较复杂时,我们很难从生成的指令集中很好的观察流程走向,且难以观察程序的层级结构,一些特殊标签,例如 catch(捕获当前标签内抛出的事件,若没有合适的 catch 去处理,则转交父标签处理) 标签的功能难以实现。
广度遍历优先
所以我们使用广度优先遍历的方式去生成指令集,在遇到有子标签的情况,我们插入一条Call
指令,以调用子程序的方式去解释执行子标签,同时在子标签指令集尾部,插入 Return
指令,使其返回调用子程序处。通过这种方式产生的报文如下:
var _avayaExitReason ''
var _avayaExitInfo1 ''
var _avayaExitInfo2 ''
var _avayaExitCustomerId ''
var _avayaExitPreferredPath '1'
var _avayaExitTopic ''
var _avayaExitParentId ''
catch error.runtime
call 12
form
call 14
end
goto example
return 9
block 12
call 17
return 11
throw error.runtime.Exception
return 16
这样的报文结构清晰,容易理解,且可以使用栈来实现子程序的调用。例如:我们可以在执行 call 指令时,向下扫描所有 catch,直到 return,这样我们就可以得到当前作用域的所有 catch 事件
下面是示例代码
1 | private void scan(List<Object[]> cmd, DOMElement tag, int callPC) { |
中断系统
为了及时处理事件或 I/O 工作,我们需要在解析器在出现抛出事件,需要打印或者输入参数时,暂时中断现行程序,转而去执行中断服务程序,那么就要求我们设定的指令执行周期尽可能小,在一条原子性指令执行结束后去判断是否需要响应中断事件。