JML语言及工具

JML语言理论

JML语言利用前置条件、后置条件、不变式等约束语法,描述了Java程序的数据、方法、类的规格,是一种契约式程序设计的实现工具。

  1. 常用的JML语言特性

    • \result:表示方法的返回值。
    • \old(expr):表示在方法执行前的值。一般将所关心的表达式取值整体括起来。
    • \forall:全称量词修饰的布尔表达式,可声明局部变量、覆盖变量的取值范围,对目标条件进行验证。
    • \exists:存在量词修饰的布尔表达式,类似\forall。
    • \sum, \max, \min:对给定范围的表达式进行运算。表达式的由声明变量、取值范围、表达式定义给出。
    • <==>, <==, ==>:逻辑推理表达式,含义与数理逻辑中相同。
    • \nothing, \everything:当前作用域下的变量范围。
  2. 数据规格
    • 不变式 invariant :在成员处于可见状态下必须满足的特性。其中可见状态可理解为完整的稳定状态。
    • 修改约束 constraint :描述前序可见状态 —> 当前可见状态的变化约束。
    • 和方法的后置条件一起对数据的变化作出规约
    • 数据规格可被子类继承。
  3. 方法规格
    • 前置条件 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!
      • 条件互斥,并集为全集。
  4. 类规格
    在类内利用一些规格变量对类的数据结构维护进行抽象描述,同样的与具体容器、对象等无关。(如pathList、pathIdList的双数组例子)
    • 规格变量和类中维护的数据有功能上的联系,但没有实现上的联系
    • 从类的层次化上来看,子类继承父类规格
    • 子类可以重写父类的方法规格
    • 子类不能违反父类的规格,但是可以进一步收窄

JML工具链

  • 使用OpenJML对实现的代码进行:
    1. JML语法静态检查:给出JML语言上的语法错误,并不关心代码

      java -jar specs/openjml.jar -check SourceToCheck.java
    2. 程序代码静态检查:给出程序中可能出现的潜在问题,并不关心JML语言
      java -jar specs/openjml.jar -esc SourceToCheck.java
    3. 运行时检查:生成一个新的.class文件,其中包含了运行时检查的assertion,在运行和单元测试的时候将发挥作用
      java -jar specs/openjml.jar -rac SourceToCheck.java
  • 使用JMLUnitNG根据JML语言自动生成TestNG测试
    1. 基于JML生成测试文件:

      java -jar ./specs/jmlunitng.jar SourceToTest.java
    2. 利用OpenJML的rac,生成含有运行时检查的特殊.class文件并替换原文件
      java -jar ./specs/openjml.jar -d bin/ -rac SourceToTest.java
    3. 运行TestNG测试
      java -cp ./specs/jmlunitng.jar:bin SourceToTest_JML_Test
  • 组合使用,效果为:(对讨论区中的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规格定义下的程序设计、验证与测试的更多相关文章

  1. 2019年北航OO第三单元(JML规格任务)总结

    一.JML简介 1.1 JML与契约式设计 说起JML,就不得不提到契约式设计(Design by Contract).这种设计模式的始祖是1986年的Eiffel语言.它是一种限定了软件中每个元素所 ...

  2. OO第三单元总结——JML规格设计

    • 1.JML语言的理论基础.应用工具链情况 JML(Java Modeling Language)—— java建模语言,是一种行为接口规范语言( behavioral interface spec ...

  3. OO第三单元总结——JML规格

    一.JML简介 1.JML语言的理论基础 JML(Java Modeling Language)是用于对Java程序进行规格化设计的一种表示语言.JML是一种行为接口规格语言 (Behavior In ...

  4. OO第三单元——基于JML的社交网络总结

    OO第三单元--基于JML的社交网络总结 一.JML知识梳理 1)JML的语言基础以及基本语法 JML是用于java程序进行规格化设计的一种表示语言,是一种行为接口规格语言.其为严格的程序设计提供了一 ...

  5. OO第三单元作业(JML)总结

    OO第三单元作业(JML)总结 目录 OO第三单元作业(JML)总结 JML语言知识梳理 使用jml的目的 jml注释结构 jml表达式 方法规格 类型规格 SMT Solver 部署JMLUnitN ...

  6. 第三单元总结——JML契约式编程

    OO第三单元博客作业--JML与契约式编程 OO第三单元的三次作业都是在课程组的JML规格下完成.完成作业的过程是契约式编程的过程:设计者完成规格设计,实现者按照规格具体实现.作业正确性的检查同样围绕 ...

  7. 2020 OO 第三单元总结 JML语言

    title: 2020 OO 第三单元总结 date: 2020-05-21 10:10:06 tags: OO categories: 学习 第三单元终于结束了,这是我目前为止最惨的一单元,第十次作 ...

  8. OO第三单元总结——JML

    目录 写在前面 JML理论基础 JML工具链 JMLUnitNG的使用 架构设计 Bug分析 心得体会 写在前面 OO的第三单元学习结束了,本单元我们学习了如何使用JML语言来对我们的程序进行规格化设 ...

  9. OO随笔之追求完美的第三单元——初试JML

    前言 这一章的JML比较简单,那么大家的关注点自然地移到了性能优化上.于是大家一股脑地去利用各种数据结构去做时间上的优化(当然很多人最后还是倒在了正确性上),故称追求完美的一单元.当然这也是得益于JM ...

随机推荐

  1. iOS 开发之 FMDB 源码分析

    概念: FMDB 是用于数据存储的框架,它是 iOS 平台下对 SQLite 数据库的封装.FMDB 是面向对象的,它以 OC 的方式封装了 SQLite 的 C 语言 API,使用起来更加方便. C ...

  2. leetCode练题——26. Remove Duplicates from Sorted Array

    1.题目 26. Remove Duplicates from Sorted Array--Easy Given a sorted array nums, remove the duplicates  ...

  3. Lesson 16 The modern city

    What is the author's main argument about the modern city? In the organization of industrial life the ...

  4. 你是否还在写try-catch-finally?来使用try-with-resources优雅地关闭流吧

    前言 开发中,我们常常需要在最后进行一些资源的关闭.比如读写文件流等,常见的,我们会在最后的finally里进行资源的关闭.但是这种写法是很不简洁的.其实,早在JDK1.7就已经引入了try-with ...

  5. PAT T1003 Universal Travel Sites

    网络流模板~ #include<bits/stdc++.h> using namespace std; ; const int inf=1e9; queue<int> q; i ...

  6. PCF8591 AD/DA模块使用详解

    I2C PCF8591 8位AD/DA转换 BCM2835 Library 1.PCF8591T简述 PCF8591器件图如下: PCF8591是一个8位的CMOS数据采集器件,具有4个模拟输入(其中 ...

  7. Session共享解决方案

    使用nginx做的负载均衡添加一个ip_hash配置 一.开两个Tomcat写测试程序 @WebServlet("/nginxSessionServlet") public cla ...

  8. Linux 添加新磁盘 && 创建分区 && 挂载

    参考: 挂载目录 分区:https://blog.csdn.net/arenn/article/details/78866251 挂载:https://www.jb51.net/article/108 ...

  9. c++读取注册表的实例

    // CRegisterTest.cpp : 定义控制台应用程序的入口点. // #include "stdafx.h" #include <iostream> #in ...

  10. CentOS 7 启动流程

    1.UEFI或BIOS初始化,运行POST开机自检 2.选择启动设备,Boot Sequence 系统启动次序一般含有:光驱.U盘.硬盘A.硬盘B.按次序查找各引导设备,第一个有引导程序的设备即为本次 ...