UnitThreeSummary
目录
- 一、JML理论基础与应用工具链情况
- 二、SMT Solver
- 三、部署JMLUnitNG/JMLUnit
- 四、作业架构设计
- 五、作业代码实现的Bug和修复情况
- 六、规格撰写和理解上的心得体会
一、JML理论基础与应用工具链情况
-
1、方法规格:
- 前置条件:通过requires子句来表示: requires P; 。其中requires是JML关键词,表达的意思是“要求调用者确保P为真”。方法规格中可以有多个requires子句,是并列关系,即调用者必须同时满足所有的并列子句要求。
- 后置条件:通过ensures子句来表示,是对方法执行结果的限制,如果执行结果满足后置条件,则表示方法执行正确,方法规格中可以有多个ensures子句,为并列关系,实现者必须同时满足有所并列ensures子句的要求。
- 副作用 :副作用指方法在执行过程中会修改对象的属性数据或者类的静态成员数据,从而给后续方法的执行带来影响。从方法规格的角度,必须要明确给出副作用范围。
- public normal_behavior:方法的正常处理部分。
- public exceptional_behavior:方法的异常处理部分。
- signals:后加表达式,抛出某异常以及抛出异常的条件语句为该表达式,当表达式为真时抛出异常。
- signals_only:强调满足前置条件时候抛出相应异常。
- 举个栗子
/*@ public normal_behavior @ requires contains(id); @ ensures \result == getPerson(id).getAcquaintanceSum(); @ also @ public exceptional_behavior @ signals (PersonIdNotFoundException e) !contains(id); @*/
可以发现,如果我们的书写JML时就可以将各种可能出现的情况以public normal/exceptional_behavior进行分割,那么在代码具体实现时,就会拥有一个很清晰的if-else逻辑,本次官方代码中给的规格就是如此,值得我们去学习。
-
2、常用表达式:
- \result表达式 :表示一个非 void 类型的方法执行所获得的结果,即方法执行后的返回值。
例:\result == null;
- \old( expr )表达式 :用来表示一个表达式 expr 在相应方法执行前的取值。
例:people.length == \old(people.length);
- \not_assigned(x,y,...)表达式 :用来表示括号中的变量是否在方法执行过程中被赋值。如果没有被赋值,返回为 true ,否则返回 false
例:(\forall int i; 0 <= i < groups.length; \not_assigned(groups[i]));
- \forall表达式 :全称量词修饰的表达式,表示对于给定范围内的元素,每个元素都满足相应的约束。
例:同上 - \exists表达式 :存在量词修饰的表达式,表示对于给定范围内的元素,存在某个元素满足相应的约束。
例:(\exists int i; 0 <= i && i < people.length && people[i].getId() == id2;money[i] == \old(money[i] + value));
- \sum表达式 :返回给定范围内的表达式的和。
例:(\sum int i; 0 <= i < people.length && people[i].getAge() >= l && people[i].getAge() <= r; 1);
- 推理操作符 :
b_expr1==>b_expr2
或者b_expr2<==b_expr1
。对于表达式b_expr1==>b_expr2
而言,当b_expr1==false
,或者b_expr1==true
且b_expr2==true
时,整个表达式的值为 true 。
- \result表达式 :表示一个非 void 类型的方法执行所获得的结果,即方法执行后的返回值。
-
3、类型规格:
- 不变式(invariant):是要求在所有可见状态下都必须满足的特性,语法上定义 invariant P ,其中invariant 为关键词, P 为谓词。
- 状态变化约束(constraint):对前序可见状态和当前可见状态的关系约束,invariant和constraint可以直接被子类继承获得。
-
4、应用工具链
- openjml :进行jml语法检查
- Junit :进行单元测试,可检查覆盖率以查看测试是否全面。
- JMLUnitNG :自动生成测试样例检查各个方法
二、SMT Solver
-
openjml:
部署好openjml后,命令行输入
java -jar openjml.jar -check -dir Person.java MyPerson.java
对MyPerson进行规格检查:
发现没有出现问题如果在MyPerson中继续写规格但是开头并没有加also,可以看到出现以下报错:
说明openjml还是有一定的查错能力的。
命令行输入java -jar openjml.jar -check -dir ./src
对整个项目进行检查,结果如下:
应用-esc
进行静态检查结果亦是如此,发现是三目运算符的问题总体而言,感觉openjml并不成熟,体验不佳,一些显而易见的错误也可能查不出来(有时候甚至瞎报错)。
三、部署JMLUnitNG
部署方法:
- 1、将课程组下发的官方代码打成jar包,此处称为P11_1.jar
- 2、下载openjml和jmlunitng的jar包
- 3、执行以下命令
java -jar jmlunitng.jar -cp P11_1.jar MyGroup.java
javac -cp jmlunitng.jar:P11_1.jar *.java
java -jar openjml.jar -cp P11_1.jar -rac MyGroup.java
java -cp jmlunitng.jar:P11_1.jar MyGroup_JML_Test
可以看出,Junitng的生成的数据大多数是边界数据,如null,但是对于一些情景来说,并不是很受用。
比如Group,addPerson的时候,add的都是null,那么对于后面Group其他方法而言,基本只能测人列表为空的情况,测试的并不全面。但是它也的确是我们看到了一些边界情况下代码可能会发生的错误。
分析图中Failed的数据。可以发现有四个方法:addPerson、delPerson、hasPerson和updatesum
其中addPerson、delPerson和updatesum 由Network.java中的addtoGroup、delFromGroup和addRelation可以判定出不会存在传入参数为null的情况。
对于hasperson来讲,发现只在updatesum中被调用,updatenum保证传入其的参数不为null,自然就保证了传入hasperson的参数不为null。因此可见Group.java中并未出现问题。
四、作业架构设计
第九次作业:
第九次作业大多数方法按照JML规格即可完成。唯二需要考虑的是容器的选择,以及iscircle()方法的选择。笔者在第一次作业并没有想那么多,容器基本用的是arraylist,iscircle()的方法用的是bfs,由于数据量很小,因此没有出现问题。但是在第十次作业中,笔者这种做法的弊端就显现出来了,因此对这两块进行了修改,在此提前说明。
1、容器选择:
可以发现在这两次作业中,Network里都出现了contains()这个方法,并且在多处被调用。那么如果用arraylist作为容器,查找某个人存不存在就变成了遍历这个列表的过程,复杂度飙升,因而可以看出,选Hashmap作为容器是明智的。
2、iscircle()的并查集+压缩路径做法:
find() 为查找id对应的人的祖宗,并在查找的路上将经过的点的father设为找到的祖宗进行路径压缩。
merge() 在每次两个人添加关系时,判断二人祖宗一不一样,如果不一样,则将一人祖宗设为另外一个人的father。
第十次作业:
第十次作用中除了上述两个点需要注意,剩下的就是Group中获得RelationSum,ValueSum,ConflictSum,AgeMean,Agevar,如果每次调用这些方法的时候都重新遍历,那么将会及其浪费时间。相应的做法是在addPerson的时候将对应的变量进行缓存,边加边算,最后调用方法的时候直接返回缓存的量/对缓存的量再做一些处理即可。
注:
- 使用此方法在计算Agevar的时候要注意精度的问题。
- 使用此方法要充分考虑先加人再加关系和先加关系再加人两种情况,我的处理方法是在MyGroup中设置一个updatesum方法,新加关系需要更新relation和value时则触发此方法。
第十一次作业:
第十一次作业的难点有二,qmp和qsl,即查找最短路径以及查找点双连通分量。
qmp:
采用优先队列优化Dijsktra算法,可以减少遍历寻找miniweight的复杂度。我新建了Pweight类,保存人的id以及路径长度weight。记得要重写优先队列的compareTo方法。
qsl:
由于以前从来没有接触过tarjan,自学后感觉回溯的过程没完全明白,害怕出Bug,因此采用了暴力枚举所有点,把它从图中删除然后遍历的做法。
具体实现:
- 首先如果两个点!iscircle()那么肯定不存在,返回false
- 其次如果两个点islinked()那么把他俩的关系删掉,进行一次dfs,若两人依然连通,则返回true,否则返回false,记得返回前再把关系加回来!
- 剩下的就是遍历除了那两个点崴外的所有点,依次从peopleMap中删除,dfs判断id1和id2(即参数中两点)连通性,若都连通返回true,否则返回false,也记得要在返回前把人加回到peopleMap之中。
此外,blocksum也可以设置一个sum变量。sum在addPerson时++,在merge时,如果发现两人祖宗不一样(即两人原来在两个块里,合并后在一个块里,块数减少了一个)sum--。调用blocksum时,只需返回sum即可。
五、作业代码实现的Bug和修复情况
第九次作业:
第九次作业比较简单,但是在解读iscircle()的jml时将array.length>=2理解为了people.size()>=2,实际上array中的元素是可以重复的,因而在people只有一个人的时候会出现bug。此bug通过对拍测试查出,因此强测和互测中均未出错。
第十次作业:
第十次作业中,通过对拍和Junit测试发现,Group中先加人后加关系的情况没有考虑,造成了问题,以及agevar的结果出现了问题,但都在中测截止前发现并改正了。因此强测和互测中均未出错。
互测中hack到了三个CPU_TLE的问题,主要原因还是在Group的处理中,每次调用一些方法都会从头算起两重循环,复杂度过高爆炸。
第十一次作业:
第十一次作业中,通过对拍和Junit测试发现,在qsl仅两人且不连接的情况没有考虑到。以及没有了解优先队列同一个对象能够加两次的特征,qmp的时候出现了错误,但是此错误被触发的概率极小(上万组的数据才测出了一次)。这些问题都在中测截止前发现并改正了。因此强测和互测中均未出错。
互测中hack到了一个和我犯了一样的错误的同学,即qsl仅两人且不连接,大胆猜想此位同学仅进行了大规模的黑箱测试而没有进行Junit单元测试,忽略了小数据,造成了大错误。
由我这次在我的代码中查到的错误来看,Junit单元测试+黑箱测试是最佳选择。Junit保证了小数据的全覆盖性检测,以及边界数据的检测。黑箱则是通过海量数据,依靠其数据随机性测出一些触发几率较小的bug。
六、规格撰写和理解上的心得体会
由于本次大多数是根据JML书写代码,只有在实验中写了一部分的JML,因此对于规格撰写感触不是很大。
对于对规格的理解,我感觉重点应当放在对于规格行为的理解,而非将它看作注释来一句一句翻译。我们应当在理解规格要干的事情后,选择合适的实现方式,进行架构设计,写出完善的代码。