目录
1 实验目标概述 1
2 实验环境配置 1
3 实验过程 2
3.1 Error and Exception Handling 2
3.1.1 处理输入文本中的三类错误 2
3.1.2 处理客户端操作时产生的异常 6
3.2 Assertion and Defensive Programming 7
3.2.1 checkRep()检查rep invariants 7
3.2.2 Assertion/异常机制来保障pre-/post-condition 7
3.2.3 你的代码的防御式策略概述 8
3.3 Logging 9
3.3.1 异常处理的日志功能 9
3.3.2 应用层操作的日志功能 9
3.3.3 日志查询功能 11
3.4 Testing for Robustness and Correctness 12
3.4.1 Testing strategy 12
3.4.2 测试用例设计 12
3.4.3 测试运行结果与EclEmma覆盖度报告 13
3.5 SpotBugs tool 14
3.6 Debugging 15
3.6.1 EventManager程序 15
3.6.2 LowestPrice程序 16
3.6.3 FlightClient/Flight/Plane程序 16
4 实验进度记录 18
5 实验过程中遇到的困难与解决途径 18
6 实验过程中收获的经验、教训、感想 19
6.1 实验过程中收获的经验和教训 19
6.2 针对以下方面的感受 19
1 实验目标概述
本次实验重点训练学生面向健壮性和正确性的编程技能,利用错误和异常处 理、断言与防御式编程技术、日志/断点等调试技术、黑盒测试编程技术,使程序可在不同的健壮性/正确性需求下能恰当的处理各种例外与错误情况,在出错后可优雅的退出或继续执行,发现错误之后可有效的定位错误并做出修改。实验针对 Lab 3 中写好的 ADT 代码和基于该 ADT 的三个应用的代码,使用以下技术进行改造,提高其健壮性和正确性:
(1)错误处理
(2)异常处理
(3)Assertion 和防御式编程
(4)日志
(5)调试技术
(6)黑盒测试及代码覆盖度
2 实验环境配置参考了实验手册
(1) 阅读 http://web.mit.edu/6.031/www/fa18/getting-started/,在本地机器安装了相应的开发环境(JDK、Eclipse、Git)
(2) 阅读https://github.com/junit-team/junit4/wiki/Download-and-Install,并在自 己的 Eclipse IDE 中安装配置 JUnit4。
(3) 阅读 https://github.com/junit-team/junit4/wiki/Getting-started,了解如何使 用 JUnit4 为 Java 程序编写测试代码并执行测试
(4) 安装SpotBugs插件:在eclipse中的工具栏的“帮助”选项,选择其中的Eclipse Marketplace,在其中的查找栏输入SpotBugs进行查找,找到后,安装下载即可 在这里给出你的GitHub Lab4仓库的URL地址(Lab4-1180301002)。https://github.com/ComputerScienceHIT/Lab4-1180301002.git3
实验过程
3.1 Error and Exception Handling
3.1.1 处理输入文本中的三类错误
对于每一个异常类,都有对应的测试文本,放在Text文件夹下,如:异常AgeLineException对应的是testAgeLineException.txt文件。特别的,文件除了最后一个空行外,不能包括其他的空行
3.1.1.1 输入文件中存在不符合语法规则的语句
会抛出异常,抛出的异常均放在BreakRulesExceptions包下。并且,在处理的时候,可以给出抛出异常的具体行数、具体信息等等
对于每一个异常类:
①异常类是不可变的,是Exception的继承类
②AF(line) = 该异常类可以提示抛出异常的行数
③RI:line 是一个大于等于1的正整数
④Safety from rep exposure:
数据类型不可变(没有修改值的方法)
属性用final和private修饰
⑤rep:
private static final long serialVersionUID;是已经生成的串行版本标识
final private int line;是文件中抛出异常的行数
例如:
Flight:2020-01-16,AA018
{
DepartureAirport:Hongkong
ArrivalAirport:Shenyang
DepatureTime:2020-01-16 22:40
ArrivalTime:2020-01-17 03:51
Plane:B6967
{
Type:A340
Seats:332
Age:23.7
}
}
对于上述的具体的航班信息,可以抛出如下的异常:
(1)AgeLineException
航班信息(第十一行)异常:不是形如Age:23.7的数据
(2)AgeLineOverRangeException
航班信息(第十一行)异常:数据超出范围[0, 30]
(3)ArrivalAirportLineException
航班信息(第四行)异常:不是形如ArrivalAirport:Shenyang的数据
(4)ArrivalTimeLineException
航班信息(第六行)异常:不是形如ArrivalTime:2020-01-17 03:51的数据
(5)DepartureAirportLineException
航班信息(第三行)异常:不是形如DepartureAirport:Hongkong的数据
(6)DepatureTimeLineException
航班信息(第五行)异常:不是形如DepatureTime:2020-01-16 22:40的数据
(7)EmptyLineException
空行异常:文件中除了最后一个空行外,还包含空行
(8)FlightLineException
航班信息(第一行)异常:不是形如Flight:2020-01-16,AA018的数据
(9)IncompleteInformationException
航班信息不完整异常:航班信息在某一行后缺失
(10)NotLeftCurlyBracketException
航班信息(第二行、第八行)异常:不是左大括号
(11)NotRightCurlyBracketException
航班信息(第十二行、第十三行)异常:不是右大括号
(12)PlaneLineException
航班信息(第七行)异常:不是形如Plane:B6967的数据
(13)SeatsLineException
航班信息(第十行)异常:不是形如Seats:332的数据
(14)SeatsLineOverRangeException
航班信息(第十行)异常:数据超出范围[50, 600]
(15)TypeLineException
航班信息(第九行)异常:不是形如Type:A340的数据
3.1.1.2 输入文件中存在标签完全一样的元素
会抛出异常,抛出的异常均放在SameLabelsExceptions包下。并且,在处理的时候,可以给出抛出异常的两个航班的具体行数、具体信息等等
对于文件中存在多个航班计划项的“日期,航班号”信息完全一样,可以抛出如下的异常:SameDatesAndFlightNumbersException
存在多个航班计划项的“日期,航班号”信息完全一样
①异常类是不可变的,是Exception的继承类
②AF(line, i) = 该异常类可以提示导致抛出异常的双方的行数
③RI:
line 是一个大于等于1的正整数
i 是一个大于等于0的整数
④Safety from rep exposure:
数据类型不可变(没有修改值的方法)
属性用final和private修饰
⑤rep:
private static final long serialVersionUID;是已经生成的串行版本标识
final private int line;是文件中抛出异常的行数
final private int i;第i个添加的航班与第line行表示的航班导致的抛出异常
3.1.1.3 输入文件中各元素之间的依赖关系不正确
会抛出异常,抛出的异常均放在IncorrectDependencyExceptions包下。并且,在处理的时候,可以给出抛出异常的两个航班的具体行数、具体信息等等
对于 InconsistentDatesException 类和 LargeDifferentDatesException类:AF、RI等信息与3.1.1.1相同;对于SameFlightNumbersButDifferentOthersException类和SamePlaneNumbersButDifferentOthersException类:AF、RI等信息与3.1.1.2相同
可以抛出如下的异常:
(1)SamePlaneNumbersButDifferentOthersException
在不同航班计划项中出现编号一样的飞机,但飞机的类型、座位数或机龄却不一致
(2)SameFlightNumbersButDifferentOthersException
同一个航班号,虽然日期不同,但其出发或到达机场、出发或到达时间有差异
(3) LargeDifferentDatesException
日期异常:降落时间中的日期与航班日期差距大于 1 天
(4) InconsistentDatesException
日期异常:第一行出现的航班日期与内部出现的起飞时间中的日期不一致
3.1.1.4 测试用例
对上述的异常进行了测试,测试包括异常的类型以及测试的提示信息
测试用例通过如下:
部分测试输出如下(由于太多,只截取部分):
3.1.2 处理客户端操作时产生的异常
会抛出异常,抛出的异常均放在OperatingExceptions包下。并且,在处理的时候,可以给出抛出异常的原因、具体信息等等
对于每一个异常类:
①异常类是不可变的,是Exception的继承类
②AF(reason) = 该异常类可以提示异常原因
③RI:reason不能为空、空字符串
④Safety from rep exposure:
数据类型不可变(没有修改值的方法)
属性用final和private修饰
防御式拷贝
⑤rep:
private static final long serialVersionUID;是已经生成的串行版本标识
final private String reason; 是抛出该异常的原因
(1)LocationConflictException
用户操作异常:在为某计划项变更位置的时候,变更后会导致与已有的其他计划项产生“位置独占冲突”
(2)NotCancelPlanningEntryNowException
用户操作异常:在取消某计划项的时候,该计划项的当前状态不允许取消
(3)OccupiedLocationException
用户操作异常:在删除某地点的时候,有尚未结束(状态为WAITING、ALLOCATED、RUNNING、BLOCKED)的计划项正在占用该地点
(4)OccupiedResourceException
用户操作异常:在删除某资源的时候,有尚未结束(状态为ALLOCATED、RUNNING、BLOCKED)的计划项正在占用该资源
(5)ResourceExclusiveConflictException
用户操作异常:在为某计划项分配某资源的时候,分配后会导致与已有的其他计划项产生“资源独占冲突”
某个测试截图:
3.2 Assertion and Defensive Programming
3.2.1 checkRep()检查rep invariants
对于任何RI不是true的ADT都进行checkRep检查
例如:
所有的字符串都不能为null、空字符串;
异常中提示信息的行数必须是正整数;
任何计划项中的地点不能重复;
任何计划项中的时间必须是从早到晚;
高铁计划中,所有的车厢资源都不能有相同的编号
3.2.2 Assertion/异常机制来保障pre-/post-condition
(一)pre-condition(前置条件)
写在@param中,比如:
Spec
添加一个新的计划项(可以添加重名的计划项)
如果出现以下情况之一:地点不存在;地点个数不符合要求;时间对个数不符合要求,则不能添加计划
@param 是资源
@param locationNames 是地点的名字列表,必须要是已经添加的地点
@param startTimes 是开始时间列表,元素不能为空、空字符串,必须符合 yyyy-MM-dd HH:mm 的语法规则,且元素个数必须与结束列表的元素个数相同
@param endTimes 是结束时间列表,元素不能为空、空字符串,必须符合 yyyy-MM-dd HH:mm 的语法规则,且元素个数必须与开始列表的元素个数相同
@param name 是计划项的名称
@return 若为真,添加计划成功;若为假,添加计划失败
public boolean addPlanningEntry(List locationNames, List startTimes, List endTimes, String name)
对于不同的计划项,前提条件中的地点列表和时间对列表都有要求:航班计划的地点数目要求2个,时间对数目要求一对;高铁计划的地点数目要求至少3个,时间对数目要求至少2对;课程计划的地点数目要求至少3个,时间对数目要求至少2对。若违反,则会抛出IllegalArgumentException,捕获提示用户信息
(二)post-condition(后置条件)
写在@return中,比如:
Spec
提示抛出该异常的原因
@return 原因,不能为空、空字符串
public String getReason()
该方法的返回值用于提示用户操作异常的原因,必须是可读信息,所以原因不能是空字符串;由于该方法还使用防御式拷贝,因此原因不能为空。若违反,说明是这个方法出了问题,则直接assert false,立刻终止程序
3.2.3 你的代码的防御式策略概述我的代码有五层防御
(一)APP
在用户输入数据时,给出正确输入的提示,包括格式、要求。比如: 对于必须完成的操作(比如:询问是否需要读取文件数据,要么是读、要么是不读这两种情况;主菜单的选择,必须是选项之一),如果用户输入的数据有误,则会要求用户再次输入对于不是必须完成的操作(比如:用户选择选项后进行操作),一旦用户没有按照提示输入正确的数据,则提示非法输入,阻止该操作,返回主菜单
(二)APIs
由于采用了facade模式,APP只会调用这个类,很大程度上保护了其他类的安全,也算一种防御式策略
(三)类之间相互调用
类与类中的调用,是通过接口来实现的,相当于在具体类上加了一层接口用于保护该类的数据
(四)类
任何RI不为true的类,都会有checkRep(),一旦表示不变量变动了,立刻assert false;此外还有Assertion/异常机制来保障pre-/post-condition
(五)数据类型
类中的数据类型都是private,凡是可变的数据类型通过get方法被其他类获取,都需要防御式拷贝。如:高铁计划的get资源方法返回的是List<车厢>类型
3.3 Logging
每一条的日志都是中文的,且可以设置日志写入的文件、日志写入的格式、日志不在控制台输出。在APP结束后,需要关闭所有的Handler,防止日志被锁
3.3.1 异常处理的日志功能
(一)读取文件时
抛出异常导致方法提前终止,记录时需要记录两行:
第一行是时间,抛出异常的类名、方法名
第二行是严重性、异常名、具体信息、结果
其中,严重性记录为“Level.SEVERE”(严重)
例如:
六月 02, 2020 4:10:30 下午 APP.FlightApp initializeAPP
严重: 读取后缀为2的文件时,发生【ResourceExclusiveConflictException】,具体信息【分配资源失败(分配后出现资源独占冲突:开始于2020-01-06 03:25的名为CX1909的计划【已分配该资源】与开始于2020-01-06 05:55的名为CZ29的计划【将要分配该资源】)】,结果【终止文件读入,保存之前读入的数据,提示用户是否重新读取该文件】
(二)操作APP时
在3.3.2中具体分析,这里不再赘述
3.3.2 应用层操作的日志功能
(一)禁止的操作
(1)抛出异常抛出异常导致方法提前终止,记录时需要记录两行:
第一行是时间,抛出异常的类名、方法名
第二行是严重性、操作序号、操作信息、异常名、具体信息、结果
其中,严重性记录为“Level.SEVERE”(严重)
例如:
六月 05, 2020 1:35:28 上午 PlanningEntryAPIs.PlanningEntryAPIs deleteLocation
严重: 【2.2】操作APP【删除可用的位置】,发生【OccupiedLocationException】,具体信息【删除地点失败(该地点已被开始时间为2020-01-16 22:40的名为AA18的占用)】,结果【阻止该操作,返回主菜单】
(2)未抛出异常
未抛出异常,方法正常结束,记录时需要记录两行:
第一行是时间,抛出异常的类名、方法名
第二行是严重性、操作序号、操作信息、错误信息、具体信息、结果
其中,严重性记录为“Level.WARNING”(警告)
例如:
六月 02, 2020 4:50:22 下午 PlanningEntryAPIs.PlanningEntryAPIs addPlanningEntry
警告: 【3】操作APP【增加一条新的计划项】,发生【地点名称重复错误】,具体信息【添加的地点中有重复的地点】,结果【阻止该操作,返回主菜单】
(二)正常操作
(1)读取文件操作记录时需要记录两行:
第一行是时间,抛出异常的类名、方法名
第二行是严重性、用户是否选择读取文件,读取的哪个文件
其中,严重性记录为“Level.INFO”(信息)
例如:
六月 05, 2020 1:33:51 上午 APP.FlightApp initializeAPP信息: 用户选择读取后缀为1的文件中的数据
(2)APP操作记录时
需要记录两行:
第一行是时间,抛出异常的类名、方法名
第二行是严重性、操作序号、操作信息、结果
其中,严重性记录为“Level.INFO”(信息)
例如:
六月 05, 2020 1:34:37 上午 APP.FlightApp executeAPP信息: 【4】操作APP【取消开始于1的名为1计划项】,结果【false,返回主菜单】
3.3.3 日志查询功能
给出了七种查询方式以及组合
操作前,需要先读取日志文件中的数据,首先进行数据处理,利用正则表达式来去除一些不能让用户获取的内部信息,如异常名、方法名、类名等等,并且摘取一些关键信息,比如:时间、操作序号等等
操作时,根据用户所选来进行匹配,比如:用户选择时间段,则会要求用户输入一个开始时间、结束时间,并且给出提示,如果违反,则阻止该log操作,并返回主菜单。若符合则输出对应的日志,记录本次操作,返回主菜单如图:(不全)
3.4 Testing for Robustness and Correctness
3.4.1 Testing strategy
在每一个测试类的测试方法的开头都以注释的形式进行标注,比如:
PlanningEntryAPIsTest中的测试方法
@Test public void testAllocatePlanningEntry() throws ResourceExclusiveConflictException//测试分配资源给计划项的方法
Testing strategy
测试:分配资源成功;分配资源失败:选定的计划项名称不存在导致的,给定的计划名称与给定的计划开始日期不匹配导致的,资源分配达到上限导致的,分配资源重复导致的,当前状态不能分配资源导致的,资源未添加导致的
测试:不同的计划项APIs
测试:抛出异常类型
3.4.2 测试用例设计
采用等价类划分的思想,覆盖每个取值,每个至少一次
(1)对于返回为真假的方法,测试不同情况而导致的返回值的真假,例如上述3.4.2中的测试方法
(2)对于返回值不为真假的方法,测试不同情况的输入参数,比较预期与方法返回值,例如:
PlanningEntryTest 中的测试方法
@Test public void testGetState()//测试获取计划状态的方法
Testing strategy
先构造一个新的高铁计划(可以阻塞,测试更加全面)
测试:六种不同状态
测试:状态转变成功时,操作前后状态与预期
测试:状态转换失败时,操作前后状态与预期
注意:由于状态转变不能转变为WAITING状态,每次状态转变只有五种可能情况,如果改不回正在测试的状态,需要重新新建一个新的高铁计划
(3)对于抛出异常而没有正常返回的方法,测试不同异常类型、异常输出提示信息。注意,在测试类的测试方法中,抛出异常的操作要放在最后
(4)对于部分不易测试的类,比如Board类的方法输出的是一个窗口,不可用JUnit测试,则采用在该类中添加main方法,手动进行测试
3.4.3 测试运行结果与EclEmma覆盖度报告
实验三、实验四一共设计了96个测试用例(包括3.6),测试用例通过如下:
部分测试输出如下:
使用 EclEmma 查看语句覆盖度(Instruction Counters)
使用 EclEmma 查看分支覆盖度(Branch Counters)
使用 EclEmma 查看路径覆盖度(Complexity)
可以看到覆盖度不是特别高
原因有三:
(1)APP类和Board类由于不方便用JUnit测试,故为0。尤其是APP中所有类的代码行数总计3000行,着实拉低了覆盖度
(2)抛出异常的语句,没有记录在覆盖度中
(3)由于assert的语句不会被记录,大量的checkRep方法(每一个RI不为true的方法都有checkRep方法)没有记录在覆盖度中
3.5 SpotBugs tool
在默认的基础上,勾选了所有选项,如下:
并未发现bug,并且在完成3.6以后,同样没有发现bug,如下:
3.6 Debugging
非常抱歉,可能您就是希望这部分才打开这个网页,但是由于反抄袭规则的限制,这部分不能给出
4 实验进度
略
5 实验过程中遇到的困难与解决途径
略
6 实验过程中收获的经验、教训、感想
6.1 实验过程中收获的经验和教训
说实话,以前并没有这么注意健壮性。大概是由于程序员是我,用户也是我,我自然不会输入乱七八糟的参数。然而最终构造的软件必然不是自己用,使用软件的用户可能是恶意的或是不知情的,常常会有意无意输入错误的数据。这时候健壮性的必要性就体现出来了。这也是这个实验、软件构造这门课教会我的。
6.2 针对以下方面的感受
(1) 健壮性和正确性,二者对编程中程序员的思路有什么不同的影响?
健壮性:由于用户可能是恶意的或是不知情的,常常会有意无意输入错误的数据,健壮性就是为了保证在非法输入的情况下,程序不是单单终止,抛出红色的警告就结束,而是要能给出提示信息,优雅的继续或结束。这考验的是程序员的应对异常的能力
正确性:只要输入的参数满足前提条件,就能给用户正确的结果,这考验的是程序员设计算法的能力
(2) 为了应对1%可能出现的错误或异常,需要增加很多行的代码,这是否划算?(考虑这个反例:民航飞机上为何不安装降落伞?)
并不划算。
先考虑反例:为什么飞机上不给乘客降落伞?
一降落伞需要很多钱,以及改造飞机的部分结构,成本太高
二发生灾难的几率小之又小
三就算安装了降落伞,乘客也不一定会用,可能导致其他的问题
类比到代码中的错误或异常也是如此,不划算:
一需要增加很多行的代码,成本太高
二发生错误或异常的几率小之又小
三就算修复了这个错误或异常,也不能保证修复过程中不会又有新的错误或异常,进而可能导致其他的问题
(3) “让自己的程序能应对更多的异常情况”和“让客户端/程序的用户承担确保正确性的职责”,二者有什么差异?你在哪些编程场景下会考虑遵循前者、在哪些场景下考虑遵循后者?
前者是健壮性,后者是正确性,两者都很重要。
但在开发APP时,为了用户的体验,会优先考虑前者;在做一个精密计算(导弹落点的计算),为了更加精确,会优先考虑正确性
(4) 过分谨慎的“防御”(excessively defensive)真的有必要吗?你如何看待过分防御所带来的性能损耗?如何在二者之间取得平衡?
过分谨慎的防御,我认为没必要,就如问题(1)中所说,耗时耗力。过分防御带来的性能损耗也不一定全是坏的,要辩证的看待:注重性能的时候,防御可以适当减少。注重防御的时候,性能可以适当损失
(5) 通过调试发现并定位错误,你自己的编程经历中有总结出一些有效的方法吗?请分享之。Assertion和log技术是否会帮助你更有效的定位错误?
最简单的定位错误就是静态检查,直接找到红色×的地方,按照提示修改即可。
其次,我觉得就是“输出大法”,即添加system.out。
最后,可以利用IDE的单步调试,由于不同的IDE调试方法不同,我并不习惯用这个
Assertion和log技术会帮助我更有效的定位错误,assertion可以直接定位到错误的行,log可以记录类名、方法名,都比较实用
(6) 怎么才是“充分的测试”?代码覆盖度100%是否就意味着100%充分的测试?
我觉得没有充分的测试,代码覆盖度100%也不是100%充分的测试,因为测试是程序员写的,只能写出想到的非法情况,虽然是100%,但其实还有意想不到的非法输入
(7) Debug一个错误的程序,有乐趣吗?体验一下无注释、无文档的程序修改。
确实有乐趣,尤其是在JUnit测试一次次未通过后,突然通过,那时候确实很兴奋。不过无注释、无文档的程序修改,属实难搞。
(8) 关于本实验的工作量、难度、deadline。
由于实验四的3.1-3.5部分主要是基于实验三,工作量不是很大。最后的3.6部分只要理解了算法,难度也不高。时间给了三周,也很充裕
(9) 到目前为止你对《软件构造》课程的评价和建议。
我觉得这门课确实让我学到了很多,从无到有,我也会自己构造软件了
(10) 期末考试临近,你对占成绩60%的闭卷考试有什么预期?
有一说一,学分课真不适合线上考。不过还是希望能有个好成绩吧