测试方法
专项测试
针对复杂度较高的特定方法,基于互测与公测限制进行数据构造
queryBlockSum
:
/*@ ensures \result ==
@ (\sum int i; 0 <= i && i < people.length &&
@ (\forall int j; 0 <= j && j < i; !isCircle(people[i].getId(), people[j].getId()));
@ 1);
@*/
public /*@ pure @*/ int queryBlockSum();
其中 isCircle
判断两个节点是否可达,也是一个比较复杂的方法,因此,若完全按照JML规格用二重循环书写上述qbs
方法必然会超时。在数据要求限制下构造极端数据:666条add Person,334条qbs
qgvs
,它调用了Group类内部的 getValueSum
方法:
/*@ ensures \result == (\sum int i; 0 <= i && i < people.length;
@ (\sum int j; 0 <= j && j < people.length &&
@ people[i].isLinked(people[j]); people[i].queryValue(people[j])));
@*/
public /*@ pure @*/ int getValueSum();
JML规格中体现的是一个 n^2复杂度的方法,也可能超时。通过计算复杂度,在数据限制下,选择1111条add Person,1111条add to Group与2778条qgvs来hack
普遍测试
在专项测试没有结果时,可以构造随机数据轰炸程序,进而寻找bug
以下借鉴了wxg同学的参数宏与测试主题
int personId = 0, groupId = 0, messageId = 0, emojiId = 0, sendId = 0, currentEmojiId = 0;
int maxag = 25, maxap = 5000, maxqci = 333, maxqlc = 100, maxsim = 1000, maxEmojiId = 10;
//常量宏,以控制人。群组。消息等类的数量
int cntag = 0, cntap = 0, cntqci = 0, cntqlc = 0, cntsim = 0;//指令数量,针对不同指令进行压力测试
int type = 0; // 0 normal, 1 message, 2 group, 3 graph, 4 exception, 5 complete, 6 sim, 7 emoji, 8 notice, 9 super
//不同类型数据,以针对异常、消息、群组等不同类别进行测试
bool noGroup = false, emojiFull = false, oneGroup = false;
通过数据生成器与自动化脚本相结合实现,以下是脚本
通过控制文件数组。循环的起始变量,就可以很方便地有针对性地测试,针对性地改进特定方法
JML工具链
OpenJML
OpenJML是可用于Java程序的程序验证工具,它可以检查使用JML语言进行注释的程序的正确性,支持静态检查、运行时动态检查,以及通过SMT Solvers对程序进行更深层次的验证
JMLUnit
可以检查数据覆盖率以尽可能接近全覆盖
示例(来自qs同学)
架构设计
本单元主要是基于JML规格建立一个社交网络并进行各种查询操作,核心类有Network、Group与Person,其中Network为整个网络图、Group为点集、而Person为图中的点。但若完全基于JML规格进行设计,必然会导致超时问题,故需要进行一些查询算法的优化以及建图过程中的动态维护
储存
按照JML规格设计是遍历,不过显式遍历必然会导致TLE,因此引入hashmap高效查询
并查集
第九次作业中的isCircle判断两个人是否可达、queryBlockSum则查询不可达集合的数目,JML规格使用了二重循环,直接按照其来实现必然会超时。从本质上讲,社交网络中的人可以分为并查集,而前者即为查询两者是否处于同一并查集,后者则是返回并查集的数目。
基于此原则动态维护,在network中增设变量记录并查集数目,在加人时+1,在addrelation时合并并查集,从而将qci简化为判断并查集的父节点是否一样,将qbs简化为返回记录变量的值
//MyPerson.java
public MyPerson getFa() {
if (fa.equals(this)) {
return fa;
}
setFa(fa.getFa());
return fa;
}
public void setFa(MyPerson fa) {
this.fa = fa;
}
public int add(Person person, int value) {
int flag = 0;
acquaintance.put(person.getId(), person);
this.value.put(person.getId(), value);
if (!((MyPerson) person).getFa().equals(getFa())) {
flag = 1;
}
((MyPerson) person).getFa().setFa(getFa());
return flag;
}
最小生成树
第九次作业建立起了并查集,第十次作业中的queryLeastConnection指令则在此基础上,查询并查集中点构成的图中的最小生成树
我采用的是基于边查询的kruskal方法,首先从边集中提取出本图的边并排序,然后从小到大遍历,并用变量标记是否成环,当已构建好了生成树时直接退出
最短路径
第十一次作业中sim指令旨在让两个人经历过并查集中的每个人不直接地发送消息,查询路径权重最小的一个,分析可知为寻求两点间最短路径。由于其为单源点而非多源点,因此采用dijkstra算法,提取出并查集图中的边,每轮循环更新到各点路径权重的最小值直至最终寻找到目标点