Github 项目地址
PSP(Personal Software Process)
PSP2.1 | PSP阶段 | 预估耗时实际耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 10 | 17 |
Estimate | 估计这个任务需要多少时间 | 5 | 10 |
Development | 开发 | 545 | 650 |
- Analysis | - 需求分析(包括学习新技术) | 120 | 160 |
- Design Spec | - 生成设计文档 | 60 | 90 |
- Coding Standard | - 代码规范 (为目前的开发制定合适的规范) | 5 | 10 |
- Design | - 具体设计 | 30 | 30 |
- Coding | - 具体编码 | 180 | 210 |
- Code Review | - 代码复审 | 30 | 30 |
- Test | - 测试(自我测试,修改代码,提交修改) | 120 | 120 |
Reporting | 报告 | 105 | 110 |
- Test Report | - 测试报告 | 60 | 90 |
- Size Measurement | - 计算工作量 | 15 | 20 |
- Postmortem & Process Improvement Plan | - 事后总结, 并提出过程改进计划 | 30 | 232 |
合计 | 665 | 787 |
解题思路
由于最近事情比较多,而且之前做过的词法分析器与之有很多类似的地方,可以复用解法。目测需求可能有一定的变更,预估完成时间应该不长,较晚才开始着手 Orz。
题目解读:精确定义问题和处理方法
实际上,要做一个代码数目统计的程序也并不简单,因为要了解不同的语言的一些语法特性。要做一个能统计所有种类的代码的数量的程序,会非常麻烦。譬如下图所示,许多现代语言的字符集的支持非常丰富,而采取的编码和计数方式缤纷多彩。
幸运的是,仔细阅读了老师的题目说明后——了解到老师的要求是:
一个用来检测只包含 ASCII 编码的 .c,.h 等文件的统计工具。
没有中文字符是在讨论区的答疑中看到的,故我个人目前详细定义为只包含 ASCII 编码。
其中 word 的统计方法为了简化工作,采用了比较费解的统计方法——这也导致了方便检测 .sh 类的文件(其语法中很多地方不允许空格分隔),故详细定义为 .c ,.h 等文件。还有一个问题是在代码中的字符串中如果出现 \\n
,而代码本身没有换行,只算作字符,那么是算作转义字符,还是算两个正常字符呢?如果需要检测其他语言,像 .py 中单引号字符串 ‘‘
,双引号字符串""
和多行字符串 """"""
是否需要支持,多行字符串是算作代码呢?还是算作注释呢?其行数又该如何定义和计算呢?
由于其他语言还涉及到一系列复杂的问题,所以我个人详细定义为只处理 .c 和 .h 文件。
- 转义字符的问题已提交讨论区,老师说不考虑转义字符。
- CRLF 字符的问题已提交讨论区,已找到解决方法。
跨平台也带来一些麻烦,对于换行,Windows 使用的是 CRLF \\r\\n
,而 macOS 等主流类 Unix 系统使用的是 \\n
。在计算字符时应该如何处理。这些也没有在题目中详细定义。暂时只把这些问题交给 Java 本身。
最终的测试脚本,如果没有参数,或参数不合法,需要报怎样的错误提示,还是仅仅什么都不干,没有明确定义。如果我们在程序中写了提示,是否会导致老师测试失败,从而零分。
思路
基础功能
- 读取字符数
- 读取单词数
- 读取行数
- 输出文件
计数比较简单。如果不需要考虑处理字符串的问题,可以直接使用例如 aString.length()
等方法获取信息。如果需要考虑,就要便利字符串,分析计数。
扩展功能
-s
需要使用递归实现文件夹的遍历-a
需要分析语法信息,用 DFA 实现-e
需要创建一个屏蔽词的数据结构,在统计 word 时跳过这些屏蔽词
代码说明
由于许多功能没有明确定义,目前看来,基础功能比较简单,就放在了一个类中 WordCounter。用各种方法来组织代码。
重要变量说明
/**
* 基本功能的命令参数
*/
boolean requestChar = false; // -c
boolean requestWord = false; // -w
boolean requestLine = false; // -l
// -o outputFile.txt 必须成对出现
boolean requestOut = false;
String outputFileName = null;
/**
* 扩展功能的命令参数
*/
// -s 是否需要递归处理目录下符合条件的文件
boolean requestRecur = false;
// -a 是否需要返回更复杂的数据(代码行 / 空行 / 注释行)
boolean requestMore = false;
// -e stopList.txt 是否需要提供文件,不计入单词统计
boolean requestIgnore = false;
String stopListFile = null;
// 匹配 .c & .h 文件的正则表达式
String countableFilePattern = "(\\\\w)+.[ch]";
String txtPattern = "(\\\\w)+.txt";
WordCounter wc = new WordCounter();
// 需要计算的所有文件名,支持多个文件
ArrayList<String> countFileNames = null;
// -s 支持递归查看一个文件目录下的所有文件
String folderName = null;
boolean inString = false;
依赖方法说明
/**
* 保留的空构造方法
*/
public WordCounter(){}
/**
* 判断是否是各种空白字符
* @param ch
* The char to be tested.
* @return {@code true} if {@code ch} is blank, otherwise {@code false}
*/
private boolean isBlank(char ch) {
if (ch==‘ ‘ || ch==‘\\t‘ || ch==‘\\n‘) {
return true;
} else {
return false;
}
}
/**
* 判断文件名是否和要求的正则表达式相匹配
* @param filename
* 被判断的文件名
* @param filePattern
* 正则匹配格式
* "(\\\\w)+.txt" *.txt, "(\\\\w)+.[ch]" *.c or *.h
* @return {@code true} if the file can be countable by the program,
* otherwise {@code false}
*/
private boolean isFileMatch(String filename, String filePattern) {
// .c & .h 文件的文件名正则表达式
Pattern pattern = Pattern.compile(filePattern);
// 名字为空显然不匹配
if (filename == null || filename.equals("")) {
return false;
}
Matcher matcher = pattern.matcher(filename);
return matcher.matches();
}
功能方法说明
篇幅原因,只给出函数原型,详情请见源代码。
// 读取行数
private long countLine(File countF)
// 读取单词数
private long countWord(File countW)
// 构造 stopList 的数据结构
private Hashset<String> createStopList(String fileName)
// 按 stopList.txt 读取单词数
private long countCharWithIgnore(File countW)
// 读取字符数
private long countChar(File countC)
// 递归获取一个目录下的所有文件
private ArrayList<File> getAllFiles(String path)
// 获取代码行 / 空行 / 注释行
private int[] getMoreInfo(File countM)
测试设计过程
测试原理
采用课程中介绍的白盒测试用例设计方法来设计测试用例,在测试用例说明中都有具体的体现:
- 精确到函数来设计用例
- 测试边界
- 路径测试
单元测试
单元测试的结构
创建单元测试函数的主要步骤:
- 设置数据(一个假想的正确的E-mail地址);
- 使用被测试类型的功能(用E-mail地址来创建一个User类的实体);
- 比较实际结果和预期的结果(Assert.IsTrue(target!= null);)
记录测试用例
- 使用易读的测试用例名,见名知意。如最小/空测试
test/testCases/empty.c
。 - 在项目的 README.md 中详细说明。如最大单文件测试,来自 Lua 语言源代码中的解析器
test/testCases/lparser.c
- 在项目的 commit 中也写了测试概要
程序高风险说明
- 读写文件权限问题,如果在类 Unix 环境下,可能无写权限,导致
IOException
。 - 递归处理文件夹问题,由于使用了递归方法处理文件夹,如果路径过深,可能导致栈溢出。
- 文件创建失败,或不存在异常处理等
测试用例说明
下列共 14 个测试文件,基本覆盖了所有规定的、可能的路径。
- test
- testCases
- 1 emptyTest.c // 测试空文件 最小测试
- 2 hello.c // 普通程序测试
// 3 业界代码实况测试——Lua 语言的 parser 代码,50 KB,接近 2000 行 - 3 lparser.c
- 4 quoteTest1.c // 4 单独成行的注释测试
- 5 quoteTest2.c // 5 多行代码注释
- 6 quoteInString.c // 6 当 注释符号出现在字符串中,并不认为它是注释
- 7 newLine.c // 7 单个空行测试
- 8 newLines.c // 8 多个空行测试
- 9 crlfLfF.c // 9 跨平台换行测试
- 10 stopEmpty.c // 10 停用词表为空测试
- 11 stopList.c // 11 停用词表非空测试
- 12 iLegalStopList 12 非法停用词表测试
- 13 sqllite.txt // 13 ASCII C 工业项目下载地址 SQLLite 获取一个路径下的所有文件测试
- 14 deepRecur // 14 深目录测试
- testCases
参考资料
- Java 白皮书 version 9 & version 10
- 《Java 测试驱动测试》—— 图灵出版社
- Java SE API
- 邹欣老师关于单元测试的博客