标签:出发点 priority 结束 person 测试设计 这一 复杂 调用 The
??JML是对Java程序进行规格化设计的一种表示语言。通过规格化描述,精确地描述了代码的功能,并且为测试设计提供了严密的依据。尤其在多人协同开发的时候,相比可能带有内在模糊性的自然语言描述,JML能过更清晰地描述需求,有利于团队开发的交互。
表达式 | 定义 | 示例 |
---|---|---|
\result | 方法执行之后的返回值 | \result == getGroup(id).getConflictSum(); |
\old(expr) | 表达式expr在方法执行前的取值 | \old(getGroup(id2).hasPerson(i)) |
\not_assigned(x,y,...) | 表示括号中的变量在方法执行过程中是否被赋值 | (\forall int i; 0 <= i < groups.length; \not_assigned(groups[i])) |
表达式 | 定义 | 示例 |
---|---|---|
\forall | 对于范围内的元素,每个元素都满足相应的约束 | (\forall int i; 0 <= i < groups.length; \not_assigned(groups[i])) |
\exists | 对于范围的内的元素,存在某一个元素满足相应的约束 | (\exists int i; 0 <= i && i < groups.length; groups[i].getId() == id2) |
\sum | 对于范围内的元素,满足条件的表达式之和 | (\sum int i; 1 <= i && i < pathM.length; pathM[i-1].queryValue(pathM[i]))) |
??方法规格分为正常行为规格和异常行为规格两种,分别用normal_behavior
和exceptional_behavior
表示,两者之间用also
隔开。每一种方法规格包括了前置条件、后置条件和副作用范围限定。
表达式 | 定义 | |
---|---|---|
前置条件 | requires | 描述方法输入的要求 |
后置条件 | ensures | 描述方法执行结束后结果的约束 |
副作用范围限定 | Assignable | 限定方法执行过程修改的范围 |
示例:
/*@ public normal_behavior
@ requires !(\exists int i; 0 <= i && i < groups.length; groups[i].equals(group));
@ assignable groups;
@ ensures groups.length == \old(groups.length) + 1;
@ ensures (\forall int i; 0 <= i && i < \old(groups.length);
@ (\exists int j; 0 <= j && j < groups.length; groups[j] == (\old(groups[i]))));
@ ensures (\exists int i; 0 <= i && i < groups.length; groups[i] == group);
@ also
@ public exceptional_behavior
@ signals (EqualGroupIdException e) (\exists int i; 0 <= i && i < groups.length;
@ groups[i].equals(group));
@*/
定义 | |
---|---|
不变式(invariant) | 描述数据状态应该满足的要求 |
约束(constraint) | 描述数据状态变化应该满足的要求 |
??此次测试可用的工具链包括OpenJML,JMLUnitNG和Junit。前两者是针对JML进行的测试,后者主要用于单元测试。
- OpenJML: OpenJML是一个检查JML语法,验证代码规格的工具。
- JMLUnitNG: JMLUnitNG是基于OpenJML的以以JML语言规格描述作为指导的自动生成单元测试工具。
- Junit: Junit是一个Java语言的单元测试框架,可以根据规格自行构造单元测试。
??OpenJML主要是针对JML语法和规格内容进行检查的工具,提供了三种检查形式。
OpenJML最基础的功能就是对JML的语法进行检查。
java -jar $OPENJML /openjml.jar <options> <files>
较高的Java版本运行OpenJML会出现如下的报错。
该报错出现的原因是OpenJML开发时的java版本较低,依赖类版本较低的包,因此和名称功能功能一样但是版本较高的包发生的冲突,通过将本地java版本换成JDK8就可以解决。
??每次都通过命令行调用openjml.jar来运行有点麻烦,可以通过自定义命令行指令来简化一下操作。
MacOS
??命令行中输入如下指令:
vim ~/.bash_profile
??在.bash_profile中输入:
alias openjml=‘java -jar YOUR_OPENJML_PATH/openjml.jar‘
??运用.bash_profile文件:
source ~/.bash_profile
??这样,我们就可以通过openjml
命令来运行OpenJML了。
openjml <options> <files>
??除了自定义命令行指令外,我们还可以直接在IDEA中通过External Tools配置OpenJML。
??Type-checking仅对JML的语法进行检查,对于规格内容和代码是否符合规格要求时不加检查的,如果需要检查规格内容,就需要用到静态检查(Extended Static Checking)和运行时检查(Runtime Assertion Checking)。
??SMT solver
??SMT是一款基于数理逻辑的用于自动化验证函数正确性的工具,其支持多平台的SMT解释器(SMT-Solver),其中应用最广泛的平台之一是微软所研发的 z3。
??从官方下载的OpenJML文件夹下已经为我们提供了不同平台不同版本的SMT solver。因为SMT solver并不属于OpenJML的一部分,因此在进行静态检查的时候我们需要告知OpenJML需要使用的solver名称以及路径。
openjml -esc -prover provername -exec solver_path <options> <files>
??执行运行时检查的时候,首先会将被检查的代码编译成可RAC的.class文件,然后运行程序观察是否断言有冲突。
openjml -rac <options> <files>
java -cp FILE_PATH:$OPENJML/jmlruntime.jar <.class>
??对于JML规格的检查,一般可以使用静态检查。使用静态检查对Group
中的JML规格进行检查,结果如下:
??可以发现规格中存在两个错误,原因是OpenJML并不不支持三目运算符,为了后续的测试,我们需要将getAgeMean
和getAgeVar
两个方法的规格做如下修改。
/*@ public normal_behavior
@ requires people.size() > 0;
@ ensures \result == ((\sum int i; 0 <= i && i < people.size(); @ people.get(i).getAge()) / people.size());
@ also
@ public normal_behavior
@ requires people.size() == 0;
@ ensures \result == 0;
@*/
public /*@pure@*/ int getAgeMean()
/*@ public normal_behavior
@ requires people.size() > 0;
@ ensures \result == ((\sum int i; 0 <= i && i < people.size();
@ ((people.get(i).getAge() - getAgeMean())*(people.get(i).getAge() - getAgeMean()))) /
@ people.size());
@ also
@ public normal_behavior
@ requires people.size() == 0;
@ ensures \result == 0;
@*/
public /*@pure@*/ int getAgeVar()
??使用MyGroup.java代码来进行JMLUnitNG自动单元测试。为了测试方方便,对规格和代码进行了一定的删改。
??运行JMLUnitNG.jar。
java -jar jmlunitng-1_4.jar MyGroup.java
??编译当前目录下的所有.java文件。
javac -cp jmlunitng-1_4.jar *.java
openjml -rac MyGroup.java MyPerson.java
??执行完这一步之后,工程文件目录树如下:
??可以看到JMLUnitNG基于方法规格自动生成了测试文件。
??运行生成的测试代码。
java -cp jmlunitng-1_4.jar MyGroup_JML_Test
??测试结果如下:
??从JMLUnitNG生成的测试样例来看,大多测试的都是边界数据。因此JMLUnitNG可以作为一个提醒程序员考虑数据边界的辅助测试工具,但是程序正确性的保证,还是需要JUnit进行单元测试以及考虑自动或手动构造更多的测试样例。
??这次作业使用了Junit对部分方法进行了单元测试。使用Junit进行单元测试,在后期进一步优化代码的时候可以方便地进行回归测试,保证了方法实现的正确性。
??第一次作业较为简单,对性能分没有要求,因此我仅是普通地按照JML规格描述实现了方法,isCirclr方法采用DFS进行查找。
??第二次作业对性能分有了要求,因此我对第一次作业对方法实现进行了部分重构,并且进行了优化。
public int getRoot(int id) {
int root = id;
int son = id;
int temp;
while (root != roots.get(root)) {
root = roots.get(root);
}
while (son != root) {
temp = roots.get(son);
roots.put(son, root);
son = temp;
}
return root;
}
public void mergeRoot(int id1, int id2) {
int root1 = getRoot(id1);
int root2 = getRoot(id2);
if (root1 == root2) {
return;
}
roots.put(root2, root1);
}
在MyGroup中,有很多双重循环的方法,如果按照JML规格写,会造成TLE。为了解决这个问题,我采用了缓存的方法,在每次新增人的时候,对relationSum,valueSum,conflictSum,ageSum和sqAgeSum进行更新。在使用缓存的方法时需要注意两个问题:
在每一次Network新增关系的时候,需要对其所属Group中的relationSum和valueSum进行更新。
在计算方差的时候,需要注意精度的问题。(讨论区有大佬分享了这个问题)
public int getAgeVar() {
int mean = getAgeMean();
int peopleSum = people.size();
if (peopleSum == 0) {
return 0;
}
return (sqAgeSum - 2 * mean * ageSum +
peopleSum * mean * mean) / peopleSum;
}
??第三次作业相比前两次,对性能的要求更高了,作业的难度主要在于最短路径算法和点双连通分量算法。
??最短路径
??最短路径算法我采用了Dijkstra算法,并且进行了堆优化,利用PriorityQueue容器来实现堆。
??点双连通分量
??求点双连通分量时,我采用了递归的Tarjan算法,在每一次求出点双连通分量并弹栈时判断两个点是否同时包含在这个点双连通分量中,并且需要特判一下该点双连通分量点数是否大于2,否则只包含两个点的分量不是点双连通分量。
??另外,为了避免MyNetwork类长度超出checkstyle的限制,我将Tarjan算法单独抽象出了一个类,具体实现如下:
public class Tarjan {
private int clock;
private HashMap<Integer, Boolean> visited;
private HashMap<Integer, Integer> dfn;
private HashMap<Integer, Integer> low;
private HashMap<Integer, Integer> parent;
private HashMap<Integer, Person> people;
private Stack<Integer> stack;
public Tarjan(HashMap<Integer, Person> people) {
this.people = people;
clock = 0;
visited = new HashMap<>(1300);
dfn = new HashMap<>(1300);
low = new HashMap<>(1300);
parent = new HashMap<>(1300);
stack = new Stack<>();
}
public Person getPerson(int id) {
return people.get(id);
}
public boolean queryBiconnected(int id1, int id2) {
int biconnected = 0;
stack.push((Integer) id1);
visited.put(id1, true);
if (tarjan(getPerson(id1), id1, id2)) {
return true;
}
while (!stack.empty()) {
int id = stack.pop();
if (id == id1) {
biconnected++;
} else if (id == id2) {
biconnected++;
}
}
return biconnected == 2;
}
public boolean tarjan(Person person, int id1, int id2) {
clock++;
dfn.put(person.getId(), clock);
low.put(person.getId(), clock);
MyPerson myPerson = (MyPerson) person;
HashMap<Integer, Person> acquaintance = myPerson.getAcquaintance();
for (Person nextPerson : acquaintance.values()) {
if (!visited.containsKey(nextPerson.getId())) {
stack.push(nextPerson.getId());
visited.put(nextPerson.getId(), true);
parent.put(nextPerson.getId(), person.getId());
if (tarjan(nextPerson, id1, id2)) {
return true;
}
int currentLow = Math.min(low.get(person.getId()),
low.get(nextPerson.getId()));
low.put(person.getId(), currentLow);
if (low.get(nextPerson.getId()) >= dfn.get(person.getId())) {
if (popStack(person.getId(), nextPerson.getId(),
id1, id2)) {
return true;
}
}
} else if (parent.get(person.getId()) != null
&& parent.get(person.getId()) != nextPerson.getId()) {
int currentLow = Math.min(low.get(person.getId()),
dfn.get(nextPerson.getId()));
low.put(person.getId(), currentLow);
}
}
return false;
}
public boolean popStack(int currentPerson, int nextPerson,
int id1, int id2) {
int biconnected = 0;
int length = 0;
while (stack.peek() != nextPerson) {
int id = stack.pop();
length++;
if (id == id1) {
biconnected++;
} else if (id == id2) {
biconnected++;
}
}
int id = stack.pop();
length++;
if (id == id1) {
biconnected++;
} else if (id == id2) {
biconnected++;
}
length++;
if (currentPerson == id1) {
biconnected++;
} else if (currentPerson == id2) {
biconnected++;
}
return biconnected == 2 && length > 2;
}
}
??前两次作业并没有出现bug,房间也比较安静,唯一是第二次作业有一个同学因为Group中没用进行缓存处理,所以被卡了TLE。
??第三次作业强测TLE了三组,被卡的是queryMinPath这一个方法,经bug修复检查之后发现是因为MyPerson中存储acquaintance的hashmap初始容量开得太大,并且queryMinPath中使用了valueSet对其进行遍历,导致queryMinPath这个方法代码复杂度增大,从而TLE。
??所以,容器不能盲目使用,一定要理解其本质!!!
??BUG修复的手段如下:
BUAA OO Unit3 Summary——万物即可形式化
标签:出发点 priority 结束 person 测试设计 这一 复杂 调用 The
原文地址:https://www.cnblogs.com/liujiahe0v0/p/ljh_BUAA_OO_Unit3_Summary.html