项目 | 内容 |
---|---|
这个作业属于哪个课程 | 2022 年北航敏捷软件工程 |
这个作业的要求在哪里 | 结对编程项目-最长英语单词链 |
我在这个课程的目标是 | 了解并体验软件工程,实现从「程序」到「软件」的进展。 |
这个作业在哪个具体方面帮助我实现目标 | 体验结对编程,初步实践工程化开发。 |
教学班级及项目地址
教学班级:周四班
PSP表格-预期
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) |
---|---|---|
Planning | 计划 | 60 |
· Estimate | · 估计这个任务需要多少时间 | 10 |
Development | 开发 | 2200 |
· Analysis | · 需求分析 (包括学习新技术) | 120 |
· Design Spec | · 生成设计文档 | 60 |
· Design Review | · 设计复审 (和同事审核设计文档) | 250 |
· Coding Standard | · 代码规范 (为目前的开发制定合适的规范) | 60 |
· Design | · 具体设计 | 140 |
· Coding | · 具体编码 | 720 |
· Code Review | · 代码复审 | 360 |
· Test | · 测试 (自我测试,修改代码,提交修改) | 490 |
Reporting | 报告 | 160 |
· Test Report | · 测试报告 | 60 |
· Size Measurement | · 计算工作量 | 20 |
· Postmortem & Process Improvement Plan | · 事后总结, 并提出过程改进计划 | 80 |
合计 | 2420 |
设计理念
-
信息隐藏(Infromation Hiding):信息隐藏类似封装,将对象内部的操作封装使对对象的操作只需要考虑对象之间的关系,而不用关注具体实现细节。也有观点认为信息隐藏主要应用于将变化较多的模块与其他模块分隔开,保证每次修改集中于变化较多的模块即可。
我们依据这个原则将模块分成了负责文件IO的IO模块、负责图算法的graph模块、负责处理参数的controller模块,每个对象的数据都进行了隐藏和保护,只保留了供外接调用的接口,保证用户无法直接获取修改数据,提高了程序的安全性。
-
接口设计(Interface Design):
-
功能正交:我们根据不同的功能模块划分了若干个正交类,每个类中的方法也尽量保证正交性。
-
命名规范:我们遵循了google的命名规范Google C++ Style Guide
-
异常处理:对于接口中可能出现的异常,我们提供了明确的异常处理机制保证使用者能够正确处理异常,保证程序的稳定性和可靠性。
-
-
松耦合(loose coupling):指模块上功能的正交性,这一点在上文信息隐藏部分已经提及,保证我们修改模块时待修改的全集最小化。
计算模块接口的设计与实现过程
我们的计算模块接口设计如下:
int gen_chains_all(char* words[], int len, char* result[]);
int gen_chain_word(char* words[], int len, char* result[], char head, char tail, char reject, bool enable_loop);
int gen_chain_char(char* words[], int len, char* result[], char head, char tail, char reject, bool enable_loop);
-
gen_chains_all 函数对应功能性参数 -n ,获取所有单词链。
-
words 为输入的单词列表。要求已经转换为全小写,但不要求去重。
-
len 为输入单词列表的长度。
-
result 存放计算得到的全部单词链。
-
函数返回值为单词链个数。
-
-
gen_chain_word 函数对应功能性参数 -w ,获取单词个数最多的单词链。
-
前三个参数意义同上。
-
head 对应附加参数 -h ,为指定的开头字母,若为 0 则表示无指定开头字母。
-
tail 对应附加参数 -t ,为指定的结尾字母,若为 0 则表示无指定结尾字母。
-
reject 对应附加参数 -j ,为指定的禁止开头字母,若为 0 则表示无指定的禁止开头字母。
-
enable_loop 对应附加参数 -r ,表示是否允许有环。
-
函数返回值为最长单词链的单词个数。
-
-
gen_chain_char 函数对应功能性参数 -c ,获取字母个数最多的单词链。
-
七个参数意义同上。
-
函数返回值为最长单词链的字母个数。
-
计算模块主要由以下文件构成:
-
core.h core.cpp:接口的声明和定义。
-
controller.h controller.cpp:定义 Controller 类,负责命令行参数的解析。
-
file_io.h file_io.cpp:定义 FileIo 类,负责文件的输入和输出。
-
graph.h graph.cpp:定义 Graph 类,负责图算法内部实现。
-
types.h:定义异常码、异常类型等。 每个类内部的函数即关联关系详见下文 UML 图。
计算流程如下: 首先读入单词列表,去重,调用 Graph 类的 AddWord 方法建图。每个小写字母为一个节点,单词为一条从首字母指向尾字母的边。
-
若调用接口 gen_chains_all,则调用 Graph 类的 FindAllWordChains 函数。 具体算法为先按照拓扑倒序 dp 求出总单词链数。然后 dfs 输出所有链。
-
若调用接口 gen_chain_word 或 gen_chain_word ,则首先检查和设置功能参数,调用 Graph 类的 DetectLoop 方法检测有无环。判断环的算法为 Tarjan,同时可以得到拓扑序,特别地,对于自环情况需要特别处理,只有当一个点有多于一个自环的时候算作有环。然后调用 Graph 类的 FindLongestChain 方法:其中 gen_chain_word 对应参数 weighted = false;gen_chain_word 对应参数 weighted = true。 根据有无环选择调用 FindLongestChainWithLoops 方法或者 FindLongestChainWithoutLoops 方法。 对于无环的情况,则按照拓扑倒序 dp。并且由于规定单词链必须至少由两个单词组成,所以在 dp 之后要枚举所有边作为第一条边的情况,取能得到的最长链为答案。 对于有环的情况,使用状压 dp 求解。状态只需要记录在一个连通块内经过的边,跨越连通块时将状态清零。并且由于最多只有 100 条边,所以可以用两个 long long int 表示所有状态,进行记忆化搜索。与无环情况相同,在搜索一遍后要枚举所有边作为第一条边的情况,取能得到的最长链为答案。
开发环境下编译通过无警告
UML图
计算模块接口部分的性能改进
对于无环的情况,已经可以做到线性复杂度,所以无需进一步优化。
对于有环的情况,是一个 NP 问题,为保证正确性不能采用近似算法。可以在一些细节处优化,但复杂度无法降低:首先将重边排序,优先走最长边;存在自环则一定先走自环,不需要尝试;状态只需要保存同一个连通块内走过的边,跨连通块时清零。若图为完全有向图时算法跑满最多情况(而且内存会炸),经实验,当点数为 5 时时间尚较短,而到 6 时已无法接受。
不过对于随机样例,普遍表现还是可以让人接受的。 构造一个 5 个点的完全有向图(每个点带自环),性能分析如下:
主要性能瓶颈在于 DfsLongestChain 方法,这是符合预期的。
关于Design by Contract / Code Contract的思考
契约式编程关注前置条件、后置条件、不变式,前置条件描述方法传入参数应当满足一定的规范;后置条件描述经过函数/方法后将产生什么效果;不变式描述在程序运行过程中需要保证的不变内容。优点在于可以进行更多规范,消除兼容性问题;缺点在于实现过程中保证前束范式需要assert,assert本身如何进行、assert失败后行为是什么都需进行考量。
我们实现中在controller里进行了参数异常处理,如果参数异常将返回对应的异常码而非直接抛出异常终止程序。这种做法可以让用户获知错误并纠正行为。另一点是UI与dll文件的对接,传入words参数需要保证已经初始化、所有单词均为小写、可以没有进行去重;从dll文件返回的res最后一个words后加了一个nullptr表示结束。上述规约需要函数调用者和被调用者一同遵守保证。
单元测试
测试使用了1.12.1版本的google gtest,分为正确性测试、鲁棒性测试两部分。我们共构造有不同特征的12个testcase.txt,21种测试参数组合,分别针对合法参数、非法参数、有环场景来设计正确性测试。对于鲁棒性测试,我们共设计了12种异常,48组测试参数进行测试。
我们的dll文件调用的所有代码被包含在./compute路径下,因此可以用该文件夹下的覆盖率表示该接口的单元测试覆盖率。我们使用clion整合的gcov进行测试覆盖率分析,行覆盖率达到97%,分支覆盖率达到了93%。
正确性测试
testcase | 自环 | 环 | 长为1单词 | 重复单词 | 混淆字符 | 数据合法性 | 描述 | 测试参数 |
---|---|---|---|---|---|---|---|---|
1 | 有 | 无 | 无 | 无 | 无 | 合法 | 图中只有自环 | [-n] |
2 | 有 | 有 | 有 | 有 | 有 | 合法 | 测试中文字符、希腊字母 | [-r -w -h C -j V][-c -r -j h -t J] |
3 | 有 | 无 | 有 | 有 | 有 | 合法 | 自环在最长链首 | [-w] |
4 | 有 | 无 | 有 | 有 | 有 | 合法 | 自环在最长链尾 | [-c][-c -h a -t a -j b][-c -h a -j b][-c -h a][-c -t a] |
5 | 无 | 有 | 无 | 有 | 无 | 合法 | 最长链有多个环 | [-c -j h -r][-w -h a -t a -r][-w -t a -j b -r][-w -h a -t a -j z -r] |
6 | 无 | 有 | 无 | 有 | 无 | 合法 | 多个孤立环/链 | [-w -t t -r][-w -h n -r] |
7 | 有 | 有 | 有 | 有 | 有 | 合法 | 有自环的完全图 | [-w -r][-w -t b -r] |
8 | 有 | 有 | 无 | 有 | 有 | 合法 | 只有一个环,每个单词都有自环 | [-c -r] |
9 | 有 | 有 | 有 | 有 | 有 | 合法 | 平平无奇 | [-c -h j -t z -r] |
10 | 无 | 无 | 无 | 无 | 无 | 合法 | 只有一个单词,长度很长 | [-w -j b] |
11 | 不合法 | 非txt文件 | ||||||
12 | 合法 | 文件不含单词 | [-n] | |||||
13 | 有 | 有 | 有 | 无 | 无 | 不合法 | 输出结果有20001个单词 |
正确性参数见上表测试参数一列。部分testcase代码如下。
TEST(correctness_test, testcase1) {
const char *file_name = "../testcase/testcase1.txt";
const char *argv[] = {"Wordlist.exe", "-n", file_name};
WordChain word_chain((std::string(file_name)));
word_chain.BuildGraph();
int std_res = word_chain.GetChainCnt();
word_chain.OutputFile("../output/output1_std.txt");
int res;
int ret = controller.Cmd(sizeof(argv) / sizeof(argv[0]), const_cast<char **>(argv), &res, "../output/output1.txt");
EXPECT_EQ(ret, 0);
EXPECT_EQ(res, std_res);
}
TEST(correctness_test, testcase2_1) {
const char *file_name = "../testcase/testcase2.txt";
const char *argv[] = {"Wordlist.exe", "-r", "-h", "c", "-j", "v", "-w", file_name};
WordChain word_chain((std::string(file_name)));
word_chain.BuildGraph();
int std_res = word_chain.GetMostWordChain('c', '0', 'v');
word_chain.OutputFile("../output/output2_1_std.txt");
int res;
int ret = controller.Cmd(sizeof(argv) / sizeof(argv[0]), const_cast<char **>(argv), &res,
"../output/output2_1.txt");
EXPECT_EQ(ret, 0);
EXPECT_EQ(res, std_res);
}
TEST(correctness_test, testcase2_2) {
const char *file_name = "../testcase/testcase2.txt";
const char *argv[] = {"Wordlist.exe", "-r", "-j", "h", "-t", "j", "-c", file_name};
WordChain word_chain((std::string(file_name)));
word_chain.BuildGraph();
int std_res = word_chain.GetMostCharChain('0', 'j', 'h');
word_chain.OutputFile("../output/output2_2_std.txt");
int res;
int ret = controller.Cmd(sizeof(argv) / sizeof(argv[0]), const_cast<char **>(argv), &res,
"../output/output2_2.txt");
EXPECT_EQ(ret, 0);
EXPECT_EQ(res, std_res);
}
鲁棒性测试
见下文异常处理部分。
异常处理
我们共支持了以下12种异常,每一种异常都进行了充分的单元测试。
case | intsr | 场景 | expcode |
---|---|---|---|
1 | Wordlist.exe -n | 参数中没有文件 | NO_FILE_PATH |
2 | Wordlist.exe -n testcase1.txt testcase2.txt | 参数中多个文件 | MULTI_FILE_PATH |
3 | Wordlist.exe -n testcase0.txt | 参数中文件不存在 | FILE_NOT_EXISTS |
4 | Wordlist.exe -n testcase11.c | 参数中文件不是txt文件 | FILE_TYPE_ERROR |
5 | Wordlist.exe -q testcase1.txt | 非法参数 | ILLEGAL_PARAM |
6 | Wordlist.exe -h a -t s testcase1.txt | 无功能性参数 | NO_FUNCTIONAL_PARAM |
7 | Wordlist.exe -n -w testcase1.txt | 参数冲突 | PARAMS_CONFLICT |
8 | Wordlist.exe -w -w testcase1.txt | 多次指定相同参数 | DUPLICATE_PARAM |
9 | Wordlist.exe -h | -h -t -j参数没有接字符串 | CHAR_NOT_ASSIGN |
10 | Wordlist.exe -h AB | -h -t -j参数接的字符串不合法 | ILLEGAL_CHAR |
11 | Wordlist.exe -w testcase5.txt | 未指定-r但出现环 | UNEXPECTED_LOOP |
12 | Wordlist.exe -w testcase13.txt | 输出单词数超过20000 | LENGTH_OVERFLOW |
对应的testcase代码如下。
TEST(robustness_test, testcase1) {
const char *argv[] = {"Wordlist.exe", "-n"};
Controller controller{};
int res;
int ret = controller.Cmd(sizeof(argv) / sizeof(argv[0]), const_cast<char **>(argv), &res, "../exp1.txt");
EXPECT_EQ(ret, kNoFilePath);
}
TEST(robustness_test, testcase2_1) {
const char *argv[] = {"Wordlist.exe", "-n", "../testcase/testcase2.txt", "../testcase/testcase2.txt"};
Controller controller{};
int res;
int ret = controller.Cmd(sizeof(argv) / sizeof(argv[0]), const_cast<char **>(argv), &res, "../exp2_1.txt");
EXPECT_EQ(ret, kMultiFilePath);
}
TEST(robustness_test, testcase2_2) {
const char *argv[] = {"Wordlist.exe", "-w", "../testcase/testcase2.txt", "../testcase/testcase2.txt"};
Controller controller{};
int res;
int ret = controller.Cmd(sizeof(argv) / sizeof(argv[0]), const_cast<char **>(argv), &res, "../exp2_2.txt");
EXPECT_EQ(ret, kMultiFilePath);
}
TEST(robustness_test, testcase2_3) {
const char *argv[] = {"Wordlist.exe", "-c", "../testcase/testcase2.txt", "../testcase/testcase2.txt"};
Controller controller{};
int res;
int ret = controller.Cmd(sizeof(argv) / sizeof(argv[0]), const_cast<char **>(argv), &res, "../exp2_3.txt");
EXPECT_EQ(ret, kMultiFilePath);
}
TEST(robustness_test, testcase3_1) {
const char *argv[] = {"Wordlist.exe", "-n", "../testcase/testcase0.txt"};
Controller controller{};
int res;
int ret = controller.Cmd(sizeof(argv) / sizeof(argv[0]), const_cast<char **>(argv), &res, "../exp3_1.txt");
EXPECT_EQ(ret, kFileNotExists);
}
TEST(robustness_test, testcase3_2) {
const char *argv[] = {"Wordlist.exe", "-w", "../testcase/testcase0.txt"};
Controller controller{};
int res;
int ret = controller.Cmd(sizeof(argv) / sizeof(argv[0]), const_cast<char **>(argv), &res, "../exp3_2.txt");
EXPECT_EQ(ret, kFileNotExists);
}
TEST(robustness_test, testcase3_3) {
const char *argv[] = {"Wordlist.exe", "-c", "../testcase/testcase0.txt"};
Controller controller{};
int res;
int ret = controller.Cmd(sizeof(argv) / sizeof(argv[0]), const_cast<char **>(argv), &res, "../exp3_3.txt");
EXPECT_EQ(ret, kFileNotExists);
}
TEST(robustness_test, testcase4_1) {
const char *argv[] = {"Wordlist.exe", "-n", "../testcase/testcase11.c"};
Controller controller{};
int res;
int ret = controller.Cmd(sizeof(argv) / sizeof(argv[0]), const_cast<char **>(argv), &res, "../exp4_1.txt");
EXPECT_EQ(ret, kFileTypeError);
}
TEST(robustness_test, testcase4_2) {
const char *argv[] = {"Wordlist.exe", "-w", "../testcase/testcase11.c"};
Controller controller{};
int res;
int ret = controller.Cmd(sizeof(argv) / sizeof(argv[0]), const_cast<char **>(argv), &res, "../exp4_2.txt");
EXPECT_EQ(ret, kFileTypeError);
}
TEST(robustness_test, testcase4_3) {
const char *argv[] = {"Wordlist.exe", "-c", "../testcase/testcase11.c"};
Controller controller{};
int res;
int ret = controller.Cmd(sizeof(argv) / sizeof(argv[0]), const_cast<char **>(argv), &res, "../exp4_3.txt");
EXPECT_EQ(ret, kFileTypeError);
}
TEST(robustness_test, testcase4_4) {
const char *argv[] = {"Wordlist.exe", "-n", "t.c"};
Controller controller{};
int res;
int ret = controller.Cmd(sizeof(argv) / sizeof(argv[0]), const_cast<char **>(argv), &res, "../exp4_4.txt");
EXPECT_EQ(ret, kFileTypeError);
}
TEST(robustness_test, testcase4_5) {
const char *argv[] = {"Wordlist.exe", "-n", "t.c"};
Controller controller{};
int res;
int ret = controller.Cmd(sizeof(argv) / sizeof(argv[0]), const_cast<char **>(argv), &res, "../exp4_5.txt");
EXPECT_EQ(ret, kFileTypeError);
}
TEST(robustness_test, testcase4_6) {
const char *argv[] = {"Wordlist.exe", "-n", "t.c"};
Controller controller{};
int res;
int ret = controller.Cmd(sizeof(argv) / sizeof(argv[0]), const_cast<char **>(argv), &res, "../exp4_6.txt");
EXPECT_EQ(ret, kFileTypeError);
}
TEST(robustness_test, testcase5_1) {
const char *argv[] = {"Wordlist.exe", "-q", "../testcase/testcase1.txt"};
Controller controller{};
int res;
int ret = controller.Cmd(sizeof(argv) / sizeof(argv[0]), const_cast<char **>(argv), &res, "../exp5_1.txt");
EXPECT_EQ(ret, kIllegalParam);
}
TEST(robustness_test, testcase5_2) {
const char *argv[] = {"Wordlist.exe", "-r", "a", "-n", "../testcase/testcase1.txt"};
Controller controller{};
int res;
int ret = controller.Cmd(sizeof(argv) / sizeof(argv[0]), const_cast<char **>(argv), &res, "../exp5_2.txt");
EXPECT_EQ(ret, kIllegalParam);
}
TEST(robustness_test, testcase5_3) {
const char *argv[] = {"Wordlist.exe", "a", "-n", "../testcase/testcase1.txt"};
Controller controller{};
int res;
int ret = controller.Cmd(sizeof(argv) / sizeof(argv[0]), const_cast<char **>(argv), &res, "../exp5_3.txt");
EXPECT_EQ(ret, kIllegalParam);
}
TEST(robustness_test, testcase6_1) {
const char *argv[] = {"Wordlist.exe", "-h", "a", "-t", "s", "../testcase/testcase1.txt"};
Controller controller{};
int res;
int ret = controller.Cmd(sizeof(argv) / sizeof(argv[0]), const_cast<char **>(argv), &res, "../exp6_1.txt");
EXPECT_EQ(ret, kNoFunctionalParam);
}
TEST(robustness_test, testcase6_2) {
const char *argv[] = {"Wordlist.exe", "../testcase/testcase1.txt"};
Controller controller{};
int res;
int ret = controller.Cmd(sizeof(argv) / sizeof(argv[0]), const_cast<char **>(argv), &res, "../exp6_2.txt");
EXPECT_EQ(ret, kNoFunctionalParam);
}
TEST(robustness_test, testcase6_3) {
const char *argv[] = {"Wordlist.exe", "-h", "a", "-j", "s", "../testcase/testcase1.txt"};
Controller controller{};
int res;
int ret = controller.Cmd(sizeof(argv) / sizeof(argv[0]), const_cast<char **>(argv), &res, "../exp6_3.txt");
EXPECT_EQ(ret, kNoFunctionalParam);
}
TEST(robustness_test, testcase7_1) {
const char *argv[] = {"Wordlist.exe", "-w", "../testcase/testcase1.txt", "-n"};
Controller controller{};
int res;
int ret = controller.Cmd(sizeof(argv) / sizeof(argv[0]), const_cast<char **>(argv), &res, "../exp7_1.txt");
EXPECT_EQ(ret, kParamsConflict);
}
TEST(robustness_test, testcase7_2) {
const char *argv[] = {"Wordlist.exe", "-c", "../testcase/testcase1.txt", "-n"};
Controller controller{};
int res;
int ret = controller.Cmd(sizeof(argv) / sizeof(argv[0]), const_cast<char **>(argv), &res, "../exp7_2.txt");
EXPECT_EQ(ret, kParamsConflict);
}
TEST(robustness_test, testcase7_3) {
const char *argv[] = {"Wordlist.exe", "-n", "../testcase/testcase1.txt", "-w"};
Controller controller{};
int res;
int ret = controller.Cmd(sizeof(argv) / sizeof(argv[0]), const_cast<char **>(argv), &res, "../exp7_3.txt");
EXPECT_EQ(ret, kParamsConflict);
}
TEST(robustness_test, testcase7_4) {
const char *argv[] = {"Wordlist.exe", "-c", "../testcase/testcase1.txt", "-w"};
Controller controller{};
int res;
int ret = controller.Cmd(sizeof(argv) / sizeof(argv[0]), const_cast<char **>(argv), &res, "../exp7_4.txt");
EXPECT_EQ(ret, kParamsConflict);
}
TEST(robustness_test, testcase7_5) {
const char *argv[] = {"Wordlist.exe", "-n", "../testcase/testcase1.txt", "-c"};
Controller controller{};
int res;
int ret = controller.Cmd(sizeof(argv) / sizeof(argv[0]), const_cast<char **>(argv), &res, "../exp7_5.txt");
EXPECT_EQ(ret, kParamsConflict);
}
TEST(robustness_test, testcase7_6) {
const char *argv[] = {"Wordlist.exe", "-w", "../testcase/testcase1.txt", "-c"};
Controller controller{};
int res;
int ret = controller.Cmd(sizeof(argv) / sizeof(argv[0]), const_cast<char **>(argv), &res, "../exp7_6.txt");
EXPECT_EQ(ret, kParamsConflict);
}
TEST(robustness_test, testcase7_7) {
const char *argv[] = {"Wordlist.exe", "-n", "../testcase/testcase1.txt", "-h", "h"};
Controller controller{};
int res;
int ret = controller.Cmd(sizeof(argv) / sizeof(argv[0]), const_cast<char **>(argv), &res, "../exp7_7.txt");
EXPECT_EQ(ret, kParamsConflict);
}
TEST(robustness_test, testcase7_8) {
const char *argv[] = {"Wordlist.exe", "-n", "../testcase/testcase1.txt", "-t", "h"};
Controller controller{};
int res;
int ret = controller.Cmd(sizeof(argv) / sizeof(argv[0]), const_cast<char **>(argv), &res, "../exp7_8.txt");
EXPECT_EQ(ret, kParamsConflict);
}
TEST(robustness_test, testcase7_9) {
const char *argv[] = {"Wordlist.exe", "-n", "../testcase/testcase1.txt", "-j", "h"};
Controller controller{};
int res;
int ret = controller.Cmd(sizeof(argv) / sizeof(argv[0]), const_cast<char **>(argv), &res, "../exp7_9.txt");
EXPECT_EQ(ret, kParamsConflict);
}
TEST(robustness_test, testcase8_1) {
const char *argv[] = {"Wordlist.exe", "-n", "../testcase/testcase1.txt", "-n"};
Controller controller{};
int res;
int ret = controller.Cmd(sizeof(argv) / sizeof(argv[0]), const_cast<char **>(argv), &res, "../exp8_1.txt");
EXPECT_EQ(ret, kDuplicateParam);
}
TEST(robustness_test, testcase8_2) {
const char *argv[] = {"Wordlist.exe", "-w", "../testcase/testcase1.txt", "-w"};
Controller controller{};
int res;
int ret = controller.Cmd(sizeof(argv) / sizeof(argv[0]), const_cast<char **>(argv), &res, "../exp8_2.txt");
EXPECT_EQ(ret, kDuplicateParam);
}
TEST(robustness_test, testcase8_3) {
const char *argv[] = {"Wordlist.exe", "-c", "../testcase/testcase1.txt", "-c"};
Controller controller{};
int res;
int ret = controller.Cmd(sizeof(argv) / sizeof(argv[0]), const_cast<char **>(argv), &res, "../exp8_3.txt");
EXPECT_EQ(ret, kDuplicateParam);
}
TEST(robustness_test, testcase9_1) {
const char *argv[] = {"Wordlist.exe", "-h", "-n", "../testcase/testcase1.txt"};
Controller controller{};
int res;
int ret = controller.Cmd(sizeof(argv) / sizeof(argv[0]), const_cast<char **>(argv), &res, "../exp9_1.txt");
EXPECT_EQ(ret, kCharNotAssign);
}
TEST(robustness_test, testcase9_2) {
const char *argv[] = {"Wordlist.exe", "-t", "-n", "../testcase/testcase1.txt"};
Controller controller{};
int res;
int ret = controller.Cmd(sizeof(argv) / sizeof(argv[0]), const_cast<char **>(argv), &res, "../exp9_2.txt");
EXPECT_EQ(ret, kCharNotAssign);
}
TEST(robustness_test, testcase9_3) {
const char *argv[] = {"Wordlist.exe", "-j", "-n", "../testcase/testcase1.txt"};
Controller controller{};
int res;
int ret = controller.Cmd(sizeof(argv) / sizeof(argv[0]), const_cast<char **>(argv), &res, "../exp9_3.txt");
EXPECT_EQ(ret, kCharNotAssign);
}
TEST(robustness_test, testcase10_1) {
const char *argv[] = {"Wordlist.exe", "-h", "AB", "-n", "../testcase/testcase1.txt"};
Controller controller{};
int res;
int ret = controller.Cmd(sizeof(argv) / sizeof(argv[0]), const_cast<char **>(argv), &res, "../exp10_1.txt");
EXPECT_EQ(ret, kIllegalChar);
}
TEST(robustness_test, testcase10_2) {
const char *argv[] = {"Wordlist.exe", "-t", "AB", "-n", "../testcase/testcase1.txt"};
Controller controller{};
int res;
int ret = controller.Cmd(sizeof(argv) / sizeof(argv[0]), const_cast<char **>(argv), &res, "../exp10_2.txt");
EXPECT_EQ(ret, kIllegalChar);
}
TEST(robustness_test, testcase10_3) {
const char *argv[] = {"Wordlist.exe", "-j", "AB", "-n", "../testcase/testcase1.txt"};
Controller controller{};
int res;
int ret = controller.Cmd(sizeof(argv) / sizeof(argv[0]), const_cast<char **>(argv), &res, "../exp10_3.txt");
EXPECT_EQ(ret, kIllegalChar);
}
TEST(robustness_test, testcase10_4) {
const char *argv[] = {"Wordlist.exe", "-h", "1", "../testcase/testcase1.txt"};
Controller controller{};
int res;
int ret = controller.Cmd(sizeof(argv) / sizeof(argv[0]), const_cast<char **>(argv), &res, "../exp10_4.txt");
EXPECT_EQ(ret, kIllegalChar);
}
TEST(robustness_test, testcase10_5) {
const char *argv[] = {"Wordlist.exe", "-t", "1", "../testcase/testcase1.txt"};
Controller controller{};
int res;
int ret = controller.Cmd(sizeof(argv) / sizeof(argv[0]), const_cast<char **>(argv), &res, "../exp10_5.txt");
EXPECT_EQ(ret, kIllegalChar);
}
TEST(robustness_test, testcase10_6) {
const char *argv[] = {"Wordlist.exe", "-j", "1", "../testcase/testcase1.txt"};
Controller controller{};
int res;
int ret = controller.Cmd(sizeof(argv) / sizeof(argv[0]), const_cast<char **>(argv), &res, "../exp10_6.txt");
EXPECT_EQ(ret, kIllegalChar);
}
TEST(robustness_test, testcase10_7) {
const char *argv[] = {"Wordlist.exe", "-h", "a", "a", "../testcase/testcase1.txt"};
Controller controller{};
int res;
int ret = controller.Cmd(sizeof(argv) / sizeof(argv[0]), const_cast<char **>(argv), &res, "../exp10_7.txt");
EXPECT_EQ(ret, kIllegalChar);
}
TEST(robustness_test, testcase10_8) {
const char *argv[] = {"Wordlist.exe", "-t", "a", "a", "../testcase/testcase1.txt"};
Controller controller{};
int res;
int ret = controller.Cmd(sizeof(argv) / sizeof(argv[0]), const_cast<char **>(argv), &res, "../exp10_8.txt");
EXPECT_EQ(ret, kIllegalChar);
}
TEST(robustness_test, testcase10_9) {
const char *argv[] = {"Wordlist.exe", "-j", "a", "a", "../testcase/testcase1.txt"};
Controller controller{};
int res;
int ret = controller.Cmd(sizeof(argv) / sizeof(argv[0]), const_cast<char **>(argv), &res, "../exp10_9.txt");
EXPECT_EQ(ret, kIllegalChar);
}
TEST(robustness_test, testcase11_1) {
const char *argv[] = {"Wordlist.exe", "-n", "../testcase/testcase5.txt"};
Controller controller{};
int res;
int ret = controller.Cmd(sizeof(argv) / sizeof(argv[0]), const_cast<char **>(argv), &res, "../exp11_1.txt");
EXPECT_EQ(ret, kUnexpectedLoop);
}
TEST(robustness_test, testcase11_2) {
const char *argv[] = {"Wordlist.exe", "-w", "../testcase/testcase5.txt"};
Controller controller{};
int res;
int ret = controller.Cmd(sizeof(argv) / sizeof(argv[0]), const_cast<char **>(argv), &res, "../exp11_2.txt");
EXPECT_EQ(ret, kUnexpectedLoop);
}
TEST(robustness_test, testcase11_3) {
const char *argv[] = {"Wordlist.exe", "-c", "../testcase/testcase5.txt"};
Controller controller{};
int res;
int ret = controller.Cmd(sizeof(argv) / sizeof(argv[0]), const_cast<char **>(argv), &res, "../exp11_3.txt");
EXPECT_EQ(ret, kUnexpectedLoop);
}
TEST(robustness_test, testcase12_1) {
const char *argv[] = {"Wordlist.exe", "-r", "-w", "../testcase/testcase13.txt"};
Controller controller{};
int res;
int ret = controller.Cmd(sizeof(argv) / sizeof(argv[0]), const_cast<char **>(argv), &res, "../exp12_1.txt");
EXPECT_EQ(ret, kLengthOverflow);
}
TEST(robustness_test, testcase12_2) {
const char *argv[] = {"Wordlist.exe", "-r", "-c", "../testcase/testcase13.txt"};
Controller controller{};
int res;
int ret = controller.Cmd(sizeof(argv) / sizeof(argv[0]), const_cast<char **>(argv), &res, "../exp12_2.txt");
EXPECT_EQ(ret, kLengthOverflow);
}
UI设计
界面模块采用C++的Qt6实现(后改成Qt5),整体效果如下:
-
MacOS运行实例
-
Windows运行实例
UI布局及使用流程
UI分成两个部分。上侧用于选择目标功能、进行限制、点击求解、导入待处理txt文件、保存求解结果;下侧分成两部分,左侧用于展示待处理文本,右侧用于展示求解结果。用户使用流程为:
-
点击“导入”导入txt文件,或者在左侧手动输入待处理数据。待处理数据可以包括非英文字符,按照
单词的定义为:被非英文字符间隔的连续英文字符序列
处理 -
选择上方功能性参数和中间辅助性参数
-
点击求解,求解结果将显示在右下方窗口
-
如需保存求解结果,点击“导出”并在弹出窗口中设置保存文件路径及文件名
UI部分实现
UI层面添加约束解决异常
对异常的处理通常有两种:1. UI进行较少的限制,用户触发异常提示用户重新输入 或2. UI直接进行约束
对大部分异常(expcode = 1, 2, 3, 4, 5, 6, 7, 8, 9, 10),我们采用了第二种方式添加约束保证用户无法触发;对于需要根据是否形成环路判断的异常(expcode = 11)和需要根据结果数组长度判定的异常(expcode = 12)则采用第一种方式在用户触发异常后提示用户。对各异常的实现如下:
// expcode=1,参数中没有文件,求解按钮点击后从inputContentTextEdit处读取文件,如果为空则按空文件处理
std::string inputContent = inputContentTextEdit->toPlainText().toStdString();
// expcode=2,参数中多个文件,每次点击导入按钮后将txt文件内容映射到inputContentTextEdit
// expcode=3,参数中文件不存在,点击导入按钮后使用QFileDialog::getOpenFileName弹出对话框筛选文件,无法选择不存在文件
// expcode=4,参数中文件不是txt文件,设置QFileDialog::getOpenFileName的filter参数为"文本文件(*.txt)",限定选择文件只能是txt文件
QString inputPath = QFileDialog::getOpenFileName(this, dlgTitle, curPath, filter);
if (!inputPath.isEmpty()) {
QFile inputFile(inputPath);
if (!inputFile.open(QIODevice::ReadOnly | QIODevice::Text)) return;
QTextStream inputContentTextStream(&inputFile);
QString line = inputContentTextStream.readLine();
QString inputContent;
while (!line.isNull()) {
inputContent.append(line);
line = inputContentTextStream.readLine();
}
inputPathLineEdit->setText(inputPath);
inputContentTextEdit->setText(inputContent);
}
// expcode=5,非法参数,只有控制面板的参数可以选择
// expcode=6,无功能性参数,默认选择-w
functionalParamsRadio[1]->setChecked(true);
// expcode=7,参数冲突,功能性参数使用radioButton组,只能选择一个
functionalParamsGroup = new QButtonGroup;
for (int i = 0; i < NumFunctions; ++i) {
functionalParamsRadio[i] = new QRadioButton(functions[i]);
functionalParamsGroup->addButton(functionalParamsRadio[i]);
layout->addWidget(functionalParamsRadio[i], 0, 4 * i, 1, 4);
}
// expcode=7,-n不能同时选择-h -t -j -r,设置选择-n时无法选择这四个参数
todo
// expcode=8,多次指定相同参数,UI只有选择与不选择两个状态,没有选择次数
// expcode=9,-h -t -j参数没有接字符串,保证这三个参数后面的选择框要么不选表示未指定,要么输入一个英文字母
// expcode=10,-h -t -j参数接的字符串不合法,通过Regex限定输入字符一定为英文字母
limitChar[i]->setPlaceholderText("允许所有");
QRegularExpression regex("[a-zA-Z]{1}");
QValidator *validator = new QRegularExpressionValidator(regex);
limitChar[i]->setValidator(validator);
// expcode=11,未指定-r但出现环,求解出现环后弹出对话框提示用户
// expcode=12,输出单词数超过20000,求解输出单词过多弹出对话框提示用户
if (ret < 0) {
if (ret == -kUnexpectedLoop) {
QMessageBox::information(nullptr, "提示", "输入存在环,请勾选\"允许出现环\"");
} else if (ret == -kLengthOverflow) {
QMessageBox::information(nullptr, "提示", "输出单词链过长");
}
return;
}
UI信号控制事件实现
void WordChainUI::onInputPathChooseButtonClicked() {
QString curPath = QDir::currentPath();
QString dlgTitle = "选择待导入文件";
QString filter = "文本文件(*.txt)";
QString inputPath = QFileDialog::getOpenFileName(this, dlgTitle, curPath, filter);
if (!inputPath.isEmpty()) {
QFile inputFile(inputPath);
if (!inputFile.open(QIODevice::ReadOnly | QIODevice::Text)) return;
QTextStream inputContentTextStream(&inputFile);
QString line = inputContentTextStream.readLine();
QString inputContent;
while (!line.isNull()) {
inputContent.append(line);
line = inputContentTextStream.readLine();
}
inputPathLineEdit->setText(inputPath);
inputContentTextEdit->setText(inputContent);
}
}
void WordChainUI::onSolveButtonClicked() {
char functionalParam = functionalParamsRadio[0]->isChecked() ? 'n' :
functionalParamsRadio[1]->isChecked() ? 'w' :
functionalParamsRadio[2]->isChecked() ? 'c' : 0;
char head = limitChar[0]->text().toStdString().length() > 0 ? tolower(limitChar[0]->text().toStdString()[0]) : 0;
char tail = limitChar[1]->text().toStdString().length() > 0 ? tolower(limitChar[1]->text().toStdString()[0]) : 0;
char reject = limitChar[2]->text().toStdString().length() > 0 ? tolower(limitChar[2]->text().toStdString()[0]) : 0;
bool enable_loop = allowRingsRadio->isChecked();
char *words[200000];
char *res[20000];
std::string inputContent = inputContentTextEdit->toPlainText().toStdString();
std::string s;
int len = 0;
for (int i = 0; i < inputContent.length(); ++i) {
char c = inputContent[i];
if (isupper(c)) s += (char) tolower(c);
else if (islower(c)) s += c;
else {
if (s.length() > 0) {
words[len] = new char[s.length() + 1];
for (int j = 0; j < s.length(); ++j) {
words[len][j] = s[j];
}
words[len++][s.length()] = '\0';
s = "";
}
}
}
QElapsedTimer timer;
timer.start();
int ret;
switch (functionalParam) {
case 'n':
ret = gen_chains_all(words, len, res);
break;
case 'w':
ret = gen_chain_word(words, len, res, head, tail, reject, enable_loop);
break;
case 'c':
ret = gen_chain_char(words, len, res, head, tail, reject, enable_loop);
break;
default:
// never hit here
ret = -1;
break;
}
qint64 elapsed = timer.nsecsElapsed();
if (ret < 0) {
if (ret == -kUnexpectedLoop) {
QMessageBox::information(nullptr, "提示", "输入存在环,请勾选\"允许出现环\"");
} else if (ret == -kLengthOverflow) {
QMessageBox::information(nullptr, "提示", "输出单词链过长");
}
return;
}
std::string usedTimePrompt = "用时: " + std::to_string(abs(elapsed / 1000)) + "秒";
QString usedTimePromptQ = QString::fromStdString(usedTimePrompt);
usedTimeLabel->setText(usedTimePromptQ);
QStringList strList;
int i = 0;
while (res[i] != nullptr) strList << QString(res[i++]);
QString outputContent = strList.join("\n");
outputContentTextEdit->setText(outputContent);
}
void WordChainUI::onOutputPathChooseButtonClicked() {
QString curPath = QDir::currentPath();
QString dlgTitle = "保存文件";
QString filter = "文本文件(*.txt)";
QString outputPath = QFileDialog::getSaveFileName(this, dlgTitle, curPath, filter);
if (!outputPath.isEmpty()) {
QFile outputFile(outputPath);
if (!outputFile.open(QIODevice::ReadWrite)) return;
QString outputContent = outputContentTextEdit->toPlainText();
outputFile.write(outputContent.toUtf8());
outputFile.close();
}
}
界面模块与计算模块的对接
UI设计/布局/使用流程及运行实例截图见上文。
UI和计算模块对接通过onSolveButtonClicked函数中的这部分代码,通过解析功能型参数调用dll的三个接口对计算模块进行调用。调用前后分别使用QElapsedTimer记时,返回结果保存在res数组中,展示在outputContentTextEdit的文本框中。
EXPOSED_FUNCTION int gen_chains_all(char* words[], int len, char* result[]);
EXPOSED_FUNCTION int gen_chain_word(char* words[], int len, char* result[], char head, char tail, char reject, bool enable_loop);
EXPOSED_FUNCTION int gen_chain_char(char* words[], int len, char* result[], char head, char tail, char reject, bool enable_loop);
void WordChainUIQt5::onSolveButtonClicked() {
char functionalParam = functionalParamsRadio[0]->isChecked() ? 'n' :
functionalParamsRadio[1]->isChecked() ? 'w' :
functionalParamsRadio[2]->isChecked() ? 'c' : 0;
...
QElapsedTimer timer;
timer.start();
int ret;
switch (functionalParam) {
case 'n':
ret = gen_chains_all(words, len, res);
break;
case 'w':
ret = gen_chain_word(words, len, res, head, tail, reject, enable_loop);
break;
case 'c':
ret = gen_chain_char(words, len, res, head, tail, reject, enable_loop);
break;
default:
// never hit here
ret = -1;
break;
}
qint64 elapsed = timer.nsecsElapsed();
...
}
结对过程
通常过程是线下交流、线上书写代码。
优缺点
-
结对编程优缺点
-
优点:可以提高代码质量、更加高效、减少对接带来的时间消耗、共同提升知识和技能
-
缺点:需要两个人协调时间、协调编程风格和工作习惯
-
-
jyz优缺点
-
优点:算法功底扎实、工程能力较好,熟练掌握C++、CMake,有良好的代码风格和开发习惯
-
缺点:电脑不完全装不了在线安装必须换源、离线安装没有二进制编译文件只能从源码构建、编译运行要求 mingw 11.x 和 c++ 17、占内存巨大 的qt6
-
-
ljc优缺点
-
优点:在 mac 上使用 windows上在线安装必须换源、离线安装没有二进制编译文件只能从源码构建、编译运行要求 mingw 11.x 和 c++ 17、占内存巨大 的qt6 开发 gui 模块。
-
缺点:没有坚持在 mac 上使用 windows上在线安装必须换源、离线安装没有二进制编译文件只能从源码构建、编译运行要求 mingw 11.x 和 c++ 17、占内存巨大 的qt6 开发 gui 模块。
-
三明治法则应用实例
jyz:“你真棒,你代码写的像坨x,我也想像你一样自信”
PSP表格-实际
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 60 | 60 |
· Estimate | · 估计这个任务需要多少时间 | 10 | 10 |
Development | 开发 | 2200 | 3100 |
· Analysis | · 需求分析 (包括学习新技术) | 120 | 150 |
· Design Spec | · 生成设计文档 | 60 | 100 |
· Design Review | · 设计复审 (和同事审核设计文档) | 250 | 120 |
· Coding Standard | · 代码规范 (为目前的开发制定合适的规范) | 60 | 40 |
· Design | · 具体设计 | 140 | 220 |
· Coding | · 具体编码 | 720 | 1460 |
· Code Review | · 代码复审 | 360 | 400 |
· Test | · 测试 (自我测试,修改代码,提交修改) | 490 | 620 |
Reporting | 报告 | 160 | 250 |
· Test Report | · 测试报告 | 60 | 140 |
· Size Measurement | · 计算工作量 | 20 | 20 |
· Postmortem & Process Improvement Plan | · 事后总结, 并提出过程改进计划 | 80 | 90 |
合计 | 2420 | 3410 |
附加-模块松耦合
我们和 19375263 和 20373788 小组的同学互换了 core 模块。虽然他们已经和别的组互换过了,但 ntr 战神一刀一个纯爱人。
由于我们的接口都和作业中给出的建议基本相同,所以没有遇到较大困难,只有具体异常码和 -c 模式下返回值意义不同,在外部做转换即可。
这是我们的 GUI 链接他们的计算模块运行截图:
不过在对拍中被发现了算法实现的各种细节问题……逐一改之。
表示最后的版本gui.exe才能用