JML语言利用前置条件、后置条件、不变式等约束语法,描述了Java程序的数据、方法、类的规格,是一种契约式程序设计的实现工具。
java -jar specs/openjml.jar -check SourceToCheck.java
java -jar specs/openjml.jar -esc SourceToCheck.java
java -jar specs/openjml.jar -rac SourceToCheck.java
java -jar ./specs/jmlunitng.jar SourceToTest.java
java -jar ./specs/openjml.jar -d bin/ -rac SourceToTest.java
java -cp ./specs/jmlunitng.jar:bin SourceToTest_JML_Test
首先,笔者尝试使用OpenJML的静态检查("-esc")对Path.java进行验证:
部分值得关注的规格代码如下:
1 private /*@spec_public@*/ ArrayList<Integer> nodes; 2 private /*@spec_public@*/ HashSet<Integer> distinct; // keep a unique set
3 /*@ public normal_behavior 4 @ requires nodeList != null && nodeList.length != 0; 5 @ assignable \everything; 6 @ ensures (\forall int i; 0<=i && i<nodeList.length; nodeList[i] == nodes.get(i)); 7 @ ensures (\forall int i; 0<=i && i<nodeList.length; distinct.contains(nodeList[i])); 8 @ ensures (nodes.size() == nodeList.length); 9 @ also 10 @ public normal_behavior 11 @ requires nodeList == null || nodeList.length == 0; 12 @ assignable \everything; 13 @ ensures (nodes != null && nodes.size() == 0); 14 @ ensures (distinct != null && distinct.size() == 0); 15 @*/ 17 public Path(int... nodeList) { 18 if (nodeList == null || nodeList.length == 0) { 19 nodes = new ArrayList<Integer>(); 20 distinct = new HashSet<Integer>(); 21 } else { 22 nodes = new ArrayList<Integer>(nodeList.length); 23 distinct = new HashSet<Integer>(nodeList.length); 24 for (int x : nodeList) { 25 nodes.add(x); 26 distinct.add(x); 27 } 28 } 29 }
//@ 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)。
改写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上作出两种变化,进行组合测试。
改写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语言特性:
1 for (int node : path) { // using the Path iterator, which implements the Iterable<> 2 // ... 3 }
应该换成如下显式的Iterator写法:
1 Iterator<Integer> it = path.iterator(); 2 while (it.hasNext()) { 3 int node = it.next(); // using the iterator EXPLICITLY !! 4 // ... 5 }
恕我直言,如果真的使用这些组合工具,那么程序员在写代码的时候不能首先考虑其美观性、整洁性、(程序和程序员的)高效性,
反而要时刻考虑自己写的代码能否被OpenJML理解,
那么这肯定是违背了我们使用这些工具的初衷的!
由于从Path和PathContainer中看出其测试水平十分平凡,故笔者不再打算继续对Graph类进行自动测试。
经过笔者的测试,OpenJML + JMLUnitNG的实用工具组合——一点也不“实用”!
具体总结如下:
因此,笔者认为:
JML语言是一个好的契约化编程的工具,但它绝不是导致程序员花费额外时间去伺候、适应的理由。
JML语言(甚至混入一些自然语言进行描述)能够显著提升大型工程的正确性,进一步解放程序员和设计师等的工作,
但是其并不一定要用来真正的“跑起来”!
JML的重点是给人看的,而不是给机器看的。只要程序员会看、会写、会读JML,会用它来给自己和产品带来好处,这就够了!
第一次作业十分简单,故没有采取什么特殊的设计。
由于对未来需求的不明朗,暂时没有使用Trie树手写的专一性强的数据结构等维护序列,而是使用了双向HashMap这一兼容性强的结构来维护Path。
类图如下:
第二次作业中,开始出现了图的结构模型。
类似传统算法竞赛中的邻接表结构存储图,笔者定义了若干辅助类对图结构进行管理:
在第一次作业扩展上的类图如下:
第三次作业中,出现了带边权的有向图模型(拆点表示换乘后,图成为了有向图)。
在第二次作业的Matrix基础上,笔者将Matrix进行改造,使其:
达成了数据结构的复用(源于二者的本质都是点-点-数据关系)。
此外,将原本的Integer表示节点,更改为封装一个Node(NodeId, PathId)类来进行管理:
同时,将原先的无序对Pair扩展,变成有序对OrderedPair和无序对Pair,便于对原图(拆点前)和最短路图(拆点后)的边进行统计管理。
将部分操作移出MyRailwaySystem类,分散、归因到负责求最短路的ShortestPath类、负责求联通性的Connectivity类、负责维护图结构的GraphAction类中。
这样设计的直接好处是程序的各个方法和类的复杂度都不高,只有compareTo、查询邻接表等方法稍稍高,
但比起第一单元的多项式程序而言,复杂度控制有了长足的进步。
测试部分,由于本次作业属于传统的非时序输入-非时许输出问题,故可以使用对拍器+数据生成器进行对拍检查。
而对于数据生成器的构造策略,由于本单元的正确性和算法效率要求并存,故采用以下的测试策略:
在自我线下检查后,三次作业中均未出现公测和互测BUG。
原文:https://www.cnblogs.com/FuturexGO/p/10884454.html