早在做第二单元的电梯作业时,便和同学开玩笑道,幸好没有让我们做地铁的排班、线路优化。没想到一语成谶,在第三单元地铁系统就迫不及待地跑来与我们相见。
JML语言基础及应用工具链
1.1 JML语言基础
JML是对java程序进行规格化设计的一种表示语言。它主要有两种用途:(1)开展规格化设计 (2)针对已有代码实现,书写规格,增加代码可维护性。
JML表达式
-
old(expr):表示expr在方法执行前的值
-
esult:表示方法的返回值
-
ot_assigned:表示括号中变量在方法执行过程中是否被赋值
-
onnullelements(container):表示container对象中存储的对象没有null
-
ype(type):返回type对应的class
-
ypeof(expr):返回expr对应返回的准确类型
-
forall:全称量词修饰的表达式,对于给定范围内元素,每个元素都满足相应约束
-
exists:存在量词修饰表达式,存在元素满足相应约束
-
sum,product,max,min, um_of:给定范围内表达式求和、求连乘积、最大值、最小值和满足相应条件取值的个数
-
<:子类型关系操作符:E1<:E2代表E1是E2的子类型
-
<==>,<=!=>:等价不等价操作符
-
==>:推理操作符
方法规格
-
前置条件:用requires 子句表示
-
后置条件:用ensures 子句表示
-
副作用范围限定:assignable表示可赋值,modifiable表示可修改
-
方法异常行为:signals子句表示
类型规格
-
不变式限制:在所有课件状态下都必须满足的特性
-
约束限制:对前序可见状态和当前可见状态的关系进行约束
1.2 应用工具链简介
OpenJML:专门针对JML语言设置的验证工具,它可以检查JML规格语法的正确性,也可以根据方法的JML规格和具体实现来初步验证一个方法的实现是否其符合规格。
JMLUnitNG:可以自动生成测试数据来检验代码是否正确。
JUnit:单元测试工具,我们需要自己针对代码设置规范化样例。可以比较有针对性地检验单个方法的正确性。
SMT Solver:验证代码是否符合JML规范的工具
部署JML UnitNG
部署过程
-
从官方网站获取jar包:jmluniting-1_4.jar
-
运行命令:java -jar jmluniting-1_4.jar package/test.java
(package 是 和jar包在同一级的文件夹,test.java是package文件夹下的待生成测试样例文件)
-
生成测试的文件如图,将其整个文件夹拷贝黏贴到idea工程中,运行test文件main方法,即可得到测试结果。
测试代码
/*@ public normal_behaviour
@ ensures a >= b ==>
esult == a;
@ ensures b > a ==>
esult == b;
@*/
public static int getBigger(int a, int b) {
if (a >= b) {
return a;
} else {
return b;
}
}
/*@ public normal_behaviour
@ ensures a <= b ==>
esult == a;
@ ensures b < a ==>
esult == b;
@*/
public static int getSmaller(int a, int b) {
if (a <= b) {
return a;
} else {
return b;
}
}
/*@ public normal_behaviour
@ ensures
esult == (a == b);
@*/
public static boolean isEqual(int a, int b) {
return (a == b);
}
public static void main(String[] args) {
getBigger(114514,1919810);
getSmaller(3498,438373);
isEqual(1,1);
isEqual(334,473);
}
测试结果
[TestNG] Running:
Command line suite
Failed: racEnabled()
Passed: constructor Demo()
Passed: static getBigger(-2147483648, -2147483648)
Passed: static getBigger(0, -2147483648)
Passed: static getBigger(2147483647, -2147483648)
Passed: static getBigger(-2147483648, 0)
Passed: static getBigger(0, 0)
Passed: static getBigger(2147483647, 0)
Passed: static getBigger(-2147483648, 2147483647)
Passed: static getBigger(0, 2147483647)
Passed: static getBigger(2147483647, 2147483647)
Passed: static getSmaller(-2147483648, -2147483648)
Passed: static getSmaller(0, -2147483648)
Passed: static getSmaller(2147483647, -2147483648)
Passed: static getSmaller(-2147483648, 0)
Passed: static getSmaller(0, 0)
Passed: static getSmaller(2147483647, 0)
Passed: static getSmaller(-2147483648, 2147483647)
Passed: static getSmaller(0, 2147483647)
Passed: static getSmaller(2147483647, 2147483647)
Passed: static isEqual(-2147483648, -2147483648)
Passed: static isEqual(0, -2147483648)
Passed: static isEqual(2147483647, -2147483648)
Passed: static isEqual(-2147483648, 0)
Passed: static isEqual(0, 0)
Passed: static isEqual(2147483647, 0)
Passed: static isEqual(-2147483648, 2147483647)
Passed: static isEqual(0, 2147483647)
Passed: static isEqual(2147483647, 2147483647)
Passed: static main(null)
===============================================
Command line suite
Total tests run: 30, Failures: 1, Skips: 0
===============================================
作业分析
第一次JML规格作业
架构设计
这次作业只根据官方接口实现了若干个类和方法,架构设计也非常简单,只有Main、MyPath、MyPathContainer三个类。类图如下。
MyPath类中的数据结构:
private ArrayList<Integer> pathNodes = new ArrayList<>();//存储一条路径的各个节点
private HashSet<Integer> disNodes = new HashSet<>();//存储不同的结点
MyPathContainer类中的数据结构,采用双向map减少查找路径时间:
private HashMap<Integer, Path> pid2path;//存储path的id和path的对应关系
private HashMap<Path, Integer> path2pid;//存储path和id的对应关系
private HashMap<Integer, Integer> containNodes;//存储每个结点及其出现的次数
private int current;//当前新增path的节点编号
bug分析和修复情况
本次作业没有出现bug。
发现bug的策略
与他人程序对拍;JUnit对每一个类,每一个方法实现针对性测试。
可能会出现bug的地方
这次作业CPU limit是10s,采用一些查找时间复杂度较高的容器类可能会超出CPU时间。
第二次JML规格作业
架构设计
本次作业基本上根据第一次作业的实现进行扩充,有Main、MyPath、MyGraph三个类。程序的类图如下
相比第一次作业,增加了几个容器,来描述节点是否相连,节点之间最短距离等特性。
bug分析和修复情况
本次作业没有出现bug。
发现bug的策略
与他人程序对拍;JUnit对每一个类,每一个方法实现针对性测试。
可能会出现bug的地方
这次作业我采用Floyd算法计算节点间的最短距离,每增加一条path时都需要重新建图。这时倘若采用静态数组,需要存储一下各个节点与其在数组中下标的对应关系。有同学因为将新增节点下标增加,造成了数组越界的情况。
第三次JML规格作业
架构设计
本次作业根据第二次规格作业进行扩充,有Main、MyPath、MyRailwaySystem、Floyd四个类。其中MyRailWaySystem根据新的要求在MyGraph的基础上进行了一些扩充。project的类图如下
为实现新增的四个方法要求,添加了Floyd类,在每添加一个path的时候,都为leastPrice,leastTransfer,leastUnpleasant三个方法重新建图,建图采用Floyd算法。
bug分析和修复情况
本次作业强测未出现bug。
但是在这次作业的课下测试阶段,由于设计思路有些不清晰,出现了如下bug,之后和同学交流,发现很多没有用拆点方法的同学在此处均出现了错误:
由于针对每个path采用Floyd算法单独建图,但这样要保证每个path的独立性,比如说在两条path中同时具有编号为16 30 20三个节点,在第一条path中16 与 30相连,第二条path中30 与 20 相连。这时两条path之间的节点间权值更新就可能会杂糅,最后的票价计算和不满意度计算出现错误。解决方法是,每条path单独初始化距离矩阵,之后再更新到总的距离矩阵中。
测试样例如下:
PATH_ADD 30 24 2 19 30 49 3 54 73 73 54 20 30
PATH_ADD 59 2 30 80 30 16 99 24 20 81 20
LEAST_TICKET_PRICE 20 16
发现bug的策略
与他人程序对拍;JUnit对每一个类,每一个方法实现针对性测试;构造一些极端数据(带环、自圈等等)进行测试。
可能会出现bug的地方
见上上part。
一点点心得
可能因为os逐渐硬核的os任务,老师、猪脚大大们的体谅,这三次作业难度稍有下降(除了令人头痛的第三次作业)。在这一单元中,我们初次接触了JML language ,首次使用JUnit对代码进行单元测试。
JML language是java建模语言,它就如我们os中的CML语言,能够对代码将要实现的功能进行描述,从而明晰设计思路,使写出来的代码更加美观,更符合规范,减少写出bug的可能性。但是设计JML语言规范往往需要花费更多的时间,也可能因为考虑不周出现一些规格设计上的错误,这些将有助于训练我们的思维全面性,也将有助于提高我们写代码的能力。
JUnit是针对每一个方法进行单元测试的工具。在最开始接触到的时候,我错误地这东西没有什么用,效率不若直接测试,通读代码来得快。但是,随着设计越来越复杂,往往难以发现真正的bug出现在哪里,一但出现bug,想要通过调试找到它往往需要耗费大量的时间。此时,JUnit的强大功能便发挥出作用。针对可能出现错误的方法构造极端样例进行测试,往往能够使debug之路更加通畅。
最后,感谢老师猪脚们的辛勤付出,祝oo这一门课程越变越好~