第三单元总结:JML规格定义下的程序设计、验证与测试
JML语言及工具
JML语言理论
JML语言利用前置条件、后置条件、不变式等约束语法,描述了Java程序的数据、方法、类的规格,是一种契约式程序设计的实现工具。
- 常用的JML语言特性
- \result:表示方法的返回值。
- \old(expr):表示在方法执行前的值。一般将所关心的表达式取值整体括起来。
- \forall:全称量词修饰的布尔表达式,可声明局部变量、覆盖变量的取值范围,对目标条件进行验证。
- \exists:存在量词修饰的布尔表达式,类似\forall。
- \sum, \max, \min:对给定范围的表达式进行运算。表达式的由声明变量、取值范围、表达式定义给出。
- <==>, <==, ==>:逻辑推理表达式,含义与数理逻辑中相同。
- \nothing, \everything:当前作用域下的变量范围。
- 数据规格
- 不变式 invariant :在成员处于可见状态下必须满足的特性。其中可见状态可理解为完整的稳定状态。
- 修改约束 constraint :描述前序可见状态 —> 当前可见状态的变化约束。
- 和方法的后置条件一起对数据的变化作出规约。
- 数据规格可被子类继承。
- 方法规格
- 前置条件 requires BoolExpr1 || BoolExpr2 || …;
- 后置条件 ensures BoolExpr1 || BoolExpr2 || …;
- 副作用影响要求 assignable modifiable
- 副作用影响判断 \not_assigned(x,y,…) \not_modified(x,y,…),可应用于后置条件的判断。
- pure方法:可以被JML引用,只需撰写后置条件。
- JML可以调用Java程序中的pure方法进行访问、判断等操作。
- 方法的异常行为:normal_behavior, also, exceptional_behavior, signals () expr, signals_only;
- 方法规格需要考虑到的范围:
- 修改参数
- 返回值
- 修改成员变量
- 修改this的引用
- 方法规格的原则:
- 关注执行效果(需求决定)和造成的其他影响
- 无需关注实现方式本身
- 本质仍是数据约束(让数据产生变化、类间的数据通信)
- requires语句需要覆盖所有可能的情况,包括exceptional_behavior和normal_behavior!
- 条件互斥,并集为全集。
- 类规格
在类内利用一些规格变量对类的数据结构维护进行抽象描述,同样的与具体容器、对象等无关。(如pathList、pathIdList的双数组例子)- 规格变量和类中维护的数据有功能上的联系,但没有实现上的联系。
- 从类的层次化上来看,子类继承父类规格
- 子类可以重写父类的方法规格
- 子类不能违反父类的规格,但是可以进一步收窄
JML工具链
- 使用OpenJML对实现的代码进行:
- JML语法静态检查:给出JML语言上的语法错误,并不关心代码
java -jar specs/openjml.jar -check SourceToCheck.java
- 程序代码静态检查:给出程序中可能出现的潜在问题,并不关心JML语言
java -jar specs/openjml.jar -esc SourceToCheck.java
- 运行时检查:生成一个新的.class文件,其中包含了运行时检查的assertion,在运行和单元测试的时候将发挥作用
java -jar specs/openjml.jar -rac SourceToCheck.java
- JML语法静态检查:给出JML语言上的语法错误,并不关心代码
- 使用JMLUnitNG根据JML语言自动生成TestNG测试
- 基于JML生成测试文件:
java -jar ./specs/jmlunitng.jar SourceToTest.java
- 利用OpenJML的rac,生成含有运行时检查的特殊.class文件并替换原文件
java -jar ./specs/openjml.jar -d bin/ -rac SourceToTest.java
- 运行TestNG测试
java -cp ./specs/jmlunitng.jar:bin SourceToTest_JML_Test
- 基于JML生成测试文件:
- 组合使用,效果为:(对讨论区中的Demo.java进行试运行)
确定自己的JML规格的语法正确性。
运行前给出部分可能的警告:
运行TestNG时针对性的进行测试: - 使用注意:
- 环境应为java-8
- 路径内不能包含中文
- 在开展TestNg测试前要使用OpenJML的RAC(运行时assertion检查)重新编译待测程序字节码
- OpenJML不支持\forall int[] 和 \exists int[]
尝试使用OpenJML的SMT Solver对简单的类进行静态验证
首先,笔者尝试使用OpenJML的静态检查("-esc")对Path.java进行验证:
部分值得关注的规格代码如下:
- 构造函数及其规格:
private /*@spec_public@*/ ArrayList<Integer> nodes;
private /*@spec_public@*/ HashSet<Integer> distinct; // keep a unique set
/*@ public normal_behavior
@ requires nodeList != null && nodeList.length != 0;
@ assignable \everything;
@ ensures (\forall int i; 0<=i && i<nodeList.length; nodeList[i] == nodes.get(i));
@ ensures (\forall int i; 0<=i && i<nodeList.length; distinct.contains(nodeList[i]));
@ ensures (nodes.size() == nodeList.length);
@ also
@ public normal_behavior
@ requires nodeList == null || nodeList.length == 0;
@ assignable \everything;
@ ensures (nodes != null && nodes.size() == 0);
@ ensures (distinct != null && distinct.size() == 0);
@*/
public Path(int... nodeList) {
if (nodeList == null || nodeList.length == 0) {
nodes = new ArrayList<Integer>();
distinct = new HashSet<Integer>();
} else {
nodes = new ArrayList<Integer>(nodeList.length);
distinct = new HashSet<Integer>(nodeList.length);
for (int x : nodeList) {
nodes.add(x);
distinct.add(x);
}
}
} - containsNode()方法及其规格:
//@ ensures \result == distinct.contains(node);
public /*@pure@*/ boolean containsNode(int node) {
return distinct.contains(node);
}
这两个方法和规格只是作为两个例子,剩下的规格大致按照指导书即可。(注意OpenJML不支持 \forall int[] 和 \exists int[] )
当运行静态语法检查时,没有warning和error。
当运行静态规格检查时,出现如下的很多warning:
src/Path.java:17: 警告: The prover cannot establish an assertion (Postcondition: src/Path.java:11: 注: ) in method Path
public Path(int... nodeList) {
^
src/Path.java:11: 警告: Associated declaration: src/Path.java:17: 注:
@ ensures (\forall int i; 0 <= i && i < nodeList.length;
^
src/Path.java:17: 警告: The prover cannot establish an assertion (Postcondition: src/Path.java:13: 注: ) in method Path
public Path(int... nodeList) {
^
src/Path.java:13: 警告: Associated declaration: src/Path.java:17: 注:
@ ensures (\forall int i; 0 <= i && i < nodeList.length;
^
src/Path.java:17: 警告: The prover cannot establish an assertion (Postcondition: src/Path.java:15: 注: ) in method Path
public Path(int... nodeList) {
^
src/Path.java:15: 警告: Associated declaration: src/Path.java:17: 注:
@ ensures (nodes.size() == nodeList.length);
src/Path.java:93: 警告: The prover cannot establish an assertion (Postcondition: src/Path.java:91: 注: ) in method containsNode
return distinct.contains(node);
^
src/Path.java:91: 警告: Associated declaration: src/Path.java:93: 注:
//@ ensures \result == distinct.contains(node);
可以看到,OpenJML的ESC检查认为我没有满足规格,甚至没有满足containsNode()方法中的return语句的规格(这个return和\result的对象是一模一样的)。
而笔者对其他小的程序的验证则没有问题。
由此看来,OpenJML单独作为一个验证器去对类和方法进行静态检查,是不靠谱的。
其根本原因在于OpenJML所支持和识别的类型和类型的方法太少,其对基本数据类型的支持基本可用,但一旦涉及到了ArrayList等高级数据结构类,就表现地十分迷惑。
应该将其的runtime assertion checking(RAC)与JMLUnitNg一起使用,才能发挥其部分功能(为TestNg中的测试提供Assertion)。
使用JMLUnitNg自动生成测试用例并进行测试
首先对Path.java进行RAC注入下的TestNg测试
改写JML和Java代码后的,可用的MyPath.java源文件如下Ubuntu PasteBin链接所示:
https://paste.ubuntu.com/p/zgsSCyRN5M/
首先进行如下生成操作:(可以写作一个AutoGenerate.sh,之后直接运行 ./AutoGenerate.sh src/MyPath.java 即可)
java -jar ./specs/jmlunitng.jar -cp lib/oolib.jar:src "$@" # 生成TestNg和测试策略文件
javac -cp ./specs/jmlunitng.jar:lib/oolib.jar -d bin/ src/*.java # 编译字节码
java -jar ./specs/openjml.jar -d bin/ -cp lib/oolib.jar:src -rac "$@" # 将RAC的assertion注入到Path.class中
之后进行测试:(可以写作一个AutoTest.sh,之后直接运行 ./AutoTest.sh MyPath 即可)
java -cp ./specs/jmlunitng.jar:lib/oolib.jar:bin "$@""_JML_Test" # 运行TestNg主文件
得到如下结果:
可以看到,JMLUnitNg为TestNg生成了31个测试策略/用例。
首先测试Runtime Assertion Checking是否开启,之后对各个方法进行测试(包括构造函数)。
对于参数为对象的方法,其生成的用例常常包括 NULL 和 空。
对于参数为int的方法,其生成的用例常常包括极值边界数据和0。
一般地,JMLUnitNg生成的数据多在参数上和this上作出两种变化,进行组合测试。
之后对PathContainer.java进行RAC注入下的TestNg测试
改写JML和Java代码后的,可用的MyPathContainer.java源文件如下Ubuntu PasteBin链接所示:
https://paste.ubuntu.com/p/RTWW6SmrfB/
首先仍然进行如下生成操作:
./AutoGenerate.sh src/MyPathContainer.java
之后进行测试:
./AutoTest.sh MyPathContainer
得到如下结果:
可以看到,本套件只会盲目的进行边界值、特殊值、NULL、空的测试,最多对this进行某些构造(外界不可知),
但是并不能对其进行针对性的测试,如传入有特定意义的Path对象。
中间会遇到一个“A catastrophic JML internal error occurred.”错误。
经笔者实验,原因为OpenJML不支持如下的forEach + Iterator语言特性:
for (int node : path) { // using the Path iterator, which implements the Iterable<>
// ...
}
应该换成如下显式的Iterator写法:
Iterator<Integer> it = path.iterator();
while (it.hasNext()) {
int node = it.next(); // using the iterator EXPLICITLY !!
// ...
}
恕我直言,如果真的使用这些组合工具,那么程序员在写代码的时候不能首先考虑其美观性、整洁性、(程序和程序员的)高效性,
反而要时刻考虑自己写的代码能否被OpenJML理解,
那么这肯定是违背了我们使用这些工具的初衷的!
由于从Path和PathContainer中看出其测试水平十分平凡,故笔者不再打算继续对Graph类进行自动测试。
结论
经过笔者的测试,OpenJML + JMLUnitNG的实用工具组合——一点也不“实用”!
具体总结如下:
- 对复杂数据结构的支持差,对基本数据类型的支持好,对基本数据类型数组不支持量化表达式。
- 不支持对自定义类的自动智能构造,只能盲目测试null、empty等,更不用说自动构造一些特殊的Path对象来进行测试。
- 对稍稍高级的Java语言特性不支持,写代码的时候居然还要考虑其“可跑性”。
因此,笔者认为:
JML语言是一个好的契约化编程的工具,但它绝不是导致程序员花费额外时间去伺候、适应的理由。
JML语言(甚至混入一些自然语言进行描述)能够显著提升大型工程的正确性,进一步解放程序员和设计师等的工作,
但是其并不一定要用来真正的“跑起来”!
JML的重点是给人看的,而不是给机器看的。只要程序员会看、会写、会读JML,会用它来给自己和产品带来好处,这就够了!
架构设计与迭代
第一次作业
第一次作业十分简单,故没有采取什么特殊的设计。
由于对未来需求的不明朗,暂时没有使用Trie树手写的专一性强的数据结构等维护序列,而是使用了双向HashMap这一兼容性强的结构来维护Path。
类图如下:
第二次作业
第二次作业中,开始出现了图的结构模型。
类似传统算法竞赛中的邻接表结构存储图,笔者定义了若干辅助类对图结构进行管理:
- Pair类:统计无向边使用,本质是一个无序对。即(Node1, Node2)和(Node2, Node1)视作相等。
- Edges类:类似邻接表的数据结构,本质是 HashMap<Node, HashSet<Node>> 。即第一层HashMap为(有向)边的起始点索引用,第二层保存其所邻接的所有结点。
- Matrix类:类似二维不定长数组的数据结构,本质是HashMap<Node, HashMap<Node, Integer>> 。用来维护最短路径结果的dis[][]数组。
在第一次作业扩展上的类图如下:
第三次作业
第三次作业中,出现了带边权的有向图模型(拆点表示换乘后,图成为了有向图)。
在第二次作业的Matrix基础上,笔者将Matrix进行改造,使其:
- 既能表示(u, v)间的dis最短路径距离:(u, v, dis[u][v])
- 又能用于存储有向图及其边权:(u, v, w)
达成了数据结构的复用(源于二者的本质都是点-点-数据关系)。
此外,将原本的Integer表示节点,更改为封装一个Node(NodeId, PathId)类来进行管理:
- NodeId表示原先的结点号
- PathId表示由哪条线路拆点而来
- 拆点算法中,NodeId的公共源点为Node(NodeId, -1)(由于PathId满足PathId>0恒成立)
- 拆点算法中,NodeId的公共汇点为Node(NodeId, 0) (由于PathId满足PathId>0恒成立)
同时,将原先的无序对Pair扩展,变成有序对OrderedPair和无序对Pair,便于对原图(拆点前)和最短路图(拆点后)的边进行统计管理。
将部分操作移出MyRailwaySystem类,分散、归因到负责求最短路的ShortestPath类、负责求联通性的Connectivity类、负责维护图结构的GraphAction类中。
这样设计的直接好处是程序的各个方法和类的复杂度都不高,只有compareTo、查询邻接表等方法稍稍高,
但比起第一单元的多项式程序而言,复杂度控制有了长足的进步。
bug与测试
测试部分,由于本次作业属于传统的非时序输入-非时许输出问题,故可以使用对拍器+数据生成器进行对拍检查。
而对于数据生成器的构造策略,由于本单元的正确性和算法效率要求并存,故采用以下的测试策略:
- 针对算法时间复杂度测试,构造完全包含公测和互测数据规模的数据进行极限测试。
如要求PATH_ADD + PATH_REMOVE总共不超过50条时,构造PATH_ADD 和 PATH_REMOVE 均达到50条的规模。
这样测试才能保证自己的测试 完全覆盖了 正式测试的数据规模。 - 针对正确性测试,由于本单元作业中查询的正确性至关重要,故将数据规模减小,同时增加查询操作条数,加大随机查询点对的覆盖率。
在自我线下检查后,三次作业中均未出现公测和互测BUG。
规格和测试总结
- 撰写JML语言时应当注意对参数的所有不同的pre-condition进行全覆盖,即所有的pre-con互斥,且并集应为全集。
- JML对post-condition的描述不必要考虑实现,应该使用最简单、最本质的描述。
- 在实现功能后,首先利用单元测试框架,对规格中最大的normal_behavior进行几个基础功能测试。
- 之后,应当根据撰写的规格,对每一个方法 分支针对性地 对应开展单元测试。
- 注意组合pre-con(既有参数的状态、也有对象本身this的状态)的情况
- 注意组合post-con的情况,确保每个分支都进行了至少一次测试
- 使用OpenJML可以提示JML的语法错误和可能的简单错误(如溢出等),但不能指望其自动生成的测试数据和策略,还是应该手动根据规格构造样例。
第三单元总结:JML规格定义下的程序设计、验证与测试的更多相关文章
- 2019年北航OO第三单元(JML规格任务)总结
一.JML简介 1.1 JML与契约式设计 说起JML,就不得不提到契约式设计(Design by Contract).这种设计模式的始祖是1986年的Eiffel语言.它是一种限定了软件中每个元素所 ...
- OO第三单元总结——JML规格设计
• 1.JML语言的理论基础.应用工具链情况 JML(Java Modeling Language)—— java建模语言,是一种行为接口规范语言( behavioral interface spec ...
- OO第三单元总结——JML规格
一.JML简介 1.JML语言的理论基础 JML(Java Modeling Language)是用于对Java程序进行规格化设计的一种表示语言.JML是一种行为接口规格语言 (Behavior In ...
- OO第三单元——基于JML的社交网络总结
OO第三单元--基于JML的社交网络总结 一.JML知识梳理 1)JML的语言基础以及基本语法 JML是用于java程序进行规格化设计的一种表示语言,是一种行为接口规格语言.其为严格的程序设计提供了一 ...
- OO第三单元作业(JML)总结
OO第三单元作业(JML)总结 目录 OO第三单元作业(JML)总结 JML语言知识梳理 使用jml的目的 jml注释结构 jml表达式 方法规格 类型规格 SMT Solver 部署JMLUnitN ...
- 第三单元总结——JML契约式编程
OO第三单元博客作业--JML与契约式编程 OO第三单元的三次作业都是在课程组的JML规格下完成.完成作业的过程是契约式编程的过程:设计者完成规格设计,实现者按照规格具体实现.作业正确性的检查同样围绕 ...
- 2020 OO 第三单元总结 JML语言
title: 2020 OO 第三单元总结 date: 2020-05-21 10:10:06 tags: OO categories: 学习 第三单元终于结束了,这是我目前为止最惨的一单元,第十次作 ...
- OO第三单元总结——JML
目录 写在前面 JML理论基础 JML工具链 JMLUnitNG的使用 架构设计 Bug分析 心得体会 写在前面 OO的第三单元学习结束了,本单元我们学习了如何使用JML语言来对我们的程序进行规格化设 ...
- OO随笔之追求完美的第三单元——初试JML
前言 这一章的JML比较简单,那么大家的关注点自然地移到了性能优化上.于是大家一股脑地去利用各种数据结构去做时间上的优化(当然很多人最后还是倒在了正确性上),故称追求完美的一单元.当然这也是得益于JM ...
随机推荐
- iOS 开发之 RunLoop 详解
1)什么是 Runloop ? 1.字面上是运行循环,内部就是 do-while 循环,在这个循环内不断地处理各种任务. 2.一个线程对应一个 Runloop ,主线程的 RunLoop 默认是开启的 ...
- HDFS核心类FileSystem的使用
一.导入jar包 本次使用的是eclipse操作的,所以需要手动导入jar包 在Hadoop.7.7/share/hadoop里有几个文件夹 common为核心类,此次需要引入common和hdfs两 ...
- java集合体系结构总结
好,首先我们根据这张集合体系图来慢慢分析.大到顶层接口,小到具体实现类. 首先,我想说为什么要用集合?简单的说:数组长度固定,且是同种数据类型.不能满足需求.所以我们引入集合(容器)来存储任意数据类型 ...
- JavaScript - jQuery注意点
jQuery统一了不同浏览器之间的DOM操作的差异 1. jQuery === $ // true 1.1 $(x) //将x转换为jQuery对象,便于调用jQuery提供的API 1.2 方便操作 ...
- Ubuntu 16.04 安装Redis服务器端
~ sudo apt-get install redis-server 安装完成后,Redis服务器会自动启动,我们检查Redis服务器程序 检查Redis服务器系统进程 ~ ps -aux|grep ...
- Redis的人门以及使用
1.Redis的安装 1.1centos下安装Redis 1.1.1 安装gcc 1.1.2 安装过程 图一 图三 2.Redis的启动 2.1 前端模式启动(不推荐) 截图 2.2 后端模式(推荐 ...
- Spring源码试读--BeanFactory模拟实现
动机 现在Springboot越来越便捷,如果简单的Spring应用,已无需再配置xml文件,基本可以实现全注解,即使是SpringCloud的那套东西,也都可以通过yaml配置完成.最近一年一直在用 ...
- luogu P1044 火车进出栈问题(Catalan数)
Catalan数就是魔法 火车进出栈问题即: 一个栈(无穷大)的进栈序列为 1,2,3,4,...,n 求有多少个不同的出栈序列? 将问题进行抽象, 假设'+'代表进栈, 则有'-'代表出栈 那么如果 ...
- mybatis 查询标签
语法 参考:http://www.mybatis.org/mybatis-3/zh/dynamic-sql.html <![CDATA[内容]]>: 参考: http://blog.csd ...
- vmware 因误删Linux 虚拟机磁盘,无法启动处理方法
有可能我们在做了以下误操作,导致Linux系统无法启动: 1). 磁盘损坏或虚拟机磁盘被我们删除了,而fstab文件没有更新: 2). 由于误操作或其它原因使动态库错误. 1. 首先准备好系统安装盘, ...