结对项目
| 这个作业属于哪个课程 | 软件工程 |
|---|---|
| 这个作业要求在哪里 | 作业要求 |
| 这个作业的目标 | 学会结对完成项目,加强团队合作 |
| 开发者姓名 | 陈霖昊(3123004346) 林裕秋(3123004794) |
|---|---|
| GitHub仓库 | 仓库链接 |
一、PSP2.1 个人软件过程阶段表格
| PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
|---|---|---|---|
| Planning | 计划 | 30 | 40 |
| • Estimate |
估计这个任务需要多少时间 | 30 | 40 |
| Development | 开发 | 410 | 415 |
| • Ana0lysis |
需求分析(包括学习新技术) | 60 | 60 |
| • Design Spec |
生成设计文档 | 20 | 20 |
| • Design Review |
设计复审 | 20 | 15 |
| • Coding Standard |
代码规范 | 10 | 10 |
| • Design |
具体设计 | 30 | 40 |
| • Coding |
具体编码 | 180 | 200 |
| • Code Review |
代码复审 | 30 | 60 |
| • Test |
测试与修改 | 60 | 60 |
| Reporting | 报告 | 140 | 150 |
| • Test Report |
测试报告 | 90 | 100 |
| • Size Measurement |
计算工作量 | 10 | 10 |
| • Postmortem |
事后总结与改进计划 | 40 | 40 |
| 合计 | 580 | 605 |
二、解析与代码说明
代码结构设计
项目采用模块化设计,各文件职责明确:
- equation.py
核心数据结构定义:Operators:运算符枚举(ADD/SUB/MUL/DIV),封装优先级、运算逻辑。Numeric:分数类,支持带分数、约分、运算,确保计算唯一性。用来存操作数,所有生成的数值表示为分子和分母两部分。比如1是1/1,0.5是1/2.EquationNode:表达式树节点,递归构建/求值/标准化/去重。主要包括EquationSet:管理题目集合与答案映射。
- file_processor.py
文件读写与题目解析:question_read():正则表达式拆分 → 双栈法构建表达式树。equations_write():将题目与答案按格式写入文件。
- generate.py
随机生成题目:Generator:深度递归随机生成表达式树 → 标准化去重 → 生成合法题目。
- main.py
CLI入口:- 命令行解析 → 分发至生成/批改模式 → 调用对应模块完成功能。
关键代码流程解析
1. 表达式树构建与去重
# equation.py - EquationNode 关键方法
def normalize(self):
# 递归标准化子树:左子树优先级>右子树 → 消除交换律重复
if self.left: l_order = self.left.normalize()
if self.right: r_order = self.right.normalize()
if l_order < r_order: self.left, self.right = self.right, self.left # 保证左大右小
def compare(self, other_node):
# 递归比较结构是否等价(考虑运算符交换律)
if Operators.ADD/MUL:允许左右子树互换比较 → 结构性等价判断
2. 分数运算与化简逻辑
# Numeric 类关键方法
def simplify(self):
gcd = greatest_common_divisor(numerator, denominator) # 最大公约数化简
def evaluate(self, value1, value2, op):
# 四则运算规则处理分母通分、运算
if OP为加减 → 求分母最小公倍数 → 分子运算后化简
3. 题目生成逻辑
# generate.py - Generator类
def generate_equation_tree(depth):
if depth == 0: return Numeric叶子节点
else: 随机选择运算符 → 递归生成左右子树 → 保证操作数限制
三、效能分析
- 性能图
![]()
- 覆盖率测试
![]()
四、测试运行
1、文件读取测试(TestFileRead)
验证正常文件内容读取
检测不存在的文件处理(触发FileNotFoundError)
处理目录路径(返回None)
class TestFileRead(unittest.TestCase):
def test_read_existing_file(self):
with tempfile.NamedTemporaryFile(mode='w+', delete=False, encoding='utf-8') as tmp:
tmp.write("test content")
tmp_path = tmp.name
self.assertEqual(file_read(tmp_path), "test content")
os.remove(tmp_path)
def test_file_not_found(self):
with self.assertRaises(FileNotFoundError):
file_read("nonexistent_file.txt")
def test_read_directory(self):
with tempfile.TemporaryDirectory() as tmpdir:
self.assertIsNone(file_read(tmpdir))
2、文件追加写入测试(TestFileWrite)
验证数据正确追加到文件末尾
class TestFileWrite(unittest.TestCase):
def test_append_to_file(self):
with tempfile.NamedTemporaryFile(mode='w+', delete=False, encoding='utf-8') as tmp:
tmp_path = tmp.name
file_write(tmp_path, "new data")
with open(tmp_path, 'r', encoding='utf-8') as f:
self.assertIn("new data", f.read())
os.remove(tmp_path)
3、数值转换测试(TestSingleNumericRead)
整数转换(5 → 5/1)
分数转换(3/4 → 3/4)
带分数转换(1'1/3 → 4/3)
非法格式处理(返回None)
class TestSingleNumericRead(unittest.TestCase):
def test_integer(self):
num = single_numeric_read("5")
self.assertEqual(num.numerator, 5)
self.assertEqual(num.denominator, 1)
def test_fraction(self):
num = single_numeric_read("3/4")
self.assertEqual(num.numerator, 3)
self.assertEqual(num.denominator, 4)
def test_mixed_number(self):
num = single_numeric_read("1'1/3")
self.assertEqual(num.numerator, 4)
self.assertEqual(num.denominator, 3)
def test_invalid_format(self):
self.assertIsNone(single_numeric_read("invalid"))
4、答案文件解析测试(TestAnswerRead)
正确解析包含多种数值格式的答案文件
class TestAnswerRead(unittest.TestCase):
def test_answer_parsing(self):
with tempfile.NamedTemporaryFile(mode='w+', delete=False, encoding='utf-8') as tmp:
tmp.write("1. 5\n2. 3/4\n3. 1'1/3\n")
tmp_path = tmp.name
answers = answer_read(tmp_path)
self.assertEqual(len(answers), 3)
self.assertEqual(answers[0].numerator, 5)
self.assertEqual(answers[1].denominator, 4)
os.remove(tmp_path)
5、题目文件解析测试(TestQuestionRead)
验证数学表达式转换为EquationNode并正确计算
class TestQuestionRead(unittest.TestCase):
def test_expression_parsing(self):
with tempfile.NamedTemporaryFile(mode='w+', delete=False, encoding='utf-8') as tmp:
tmp.write("1. 3 + 4 * 2\n")
tmp_path = tmp.name
questions = question_read(tmp_path)
self.assertEqual(len(questions), 1)
# 假设EquationNode有evaluate方法
result = questions[0].evaluate()
self.assertEqual(result.numerator, 11)
os.remove(tmp_path)
6、语法树构建测试(TestBuildTree)
确保操作符栈能正确生成表达式树
class TestBuildTree(unittest.TestCase):
def test_build_addition_tree(self):
op_stack = ['+']
num_stack = [
eq.EquationNode(eq.Numeric(1, 3), None, None),
eq.EquationNode(eq.Numeric(1, 4), None, None)]
build_tree(op_stack, num_stack)
self.assertEqual(len(num_stack), 1)
self.assertEqual(num_stack[0].value, eq.Operators.ADD)
7、题目生成测试(TestGenerate)
验证生成指定数量的数学题目及答案
检查文件写入流程完整性
class TestGenerate(unittest.TestCase):
def setUp(self):
self.test_dir = tempfile.TemporaryDirectory()
self.test_files = {
'question.txt': '',
'answer.txt': '',
'exercise.txt': '',
}
for filename, content in self.test_files.items():
path = os.path.join(self.test_dir.name, filename)
with open(path, 'w', encoding='utf-8') as f:
f.write(content)
def test_generate(self):
question_path = os.path.join(self.test_dir.name, 'question.txt')
answer_path = os.path.join(self.test_dir.name, 'answer.txt')
exercise_path = os.path.join(self.test_dir.name, 'exercise.txt')
es = ge.generate_equations(10, 4, 2,exercise_path)
es.print_equation_set()
for answer in es.answer_dict.values():
fp.file_write(answer_path, str(answer) + '\n')
n = 0
for equation in es.equation_list:
fp.equation_write(equation, question_path, n)
n += 1
self.clean_up()
def clean_up(self):
self.test_dir.cleanup()
8、批改功能测试(TestProofs)
集成测试题目验证流程
验证成绩文件生成机制
class TestProofs(unittest.TestCase):
def setUp(self):
self.test_dir = tempfile.TemporaryDirectory()
self.test_files = {
'question.txt': """12. 1 / 1/8 + 1 + 1/2
13. 4 - 2 + 1'1/2 + 0
14. ( 5 * 3 ) * ( 8 - 0 )
15. 7 + 0 + 2/3 + 2/7
16. ( 4 * 1 ) * ( 7 + 2 )""",
'answer.txt': """11. 3
12. 9'1/2
13. 3'1/2
14. 120
15. 7'20/21""",
'grade.txt': '',
}
for filename, content in self.test_files.items():
path = os.path.join(self.test_dir.name, filename)
with open(path, 'w', encoding='utf-8') as f:
f.write(content)
def test_proofs(self):
question_path = os.path.join(self.test_dir.name, 'question.txt')
answer_path = os.path.join(self.test_dir.name, 'answer.txt')
grade_path = os.path.join(self.test_dir.name, 'grade.txt')
m.proofread_the_questions(question_path, answer_path, grade_path)
file_data = fp.file_open(grade_path)
print(file_data.read())
fp.file_close(file_data)
self.test_dir.cleanup()
9、题目去重测试(TestDeduplication)
测试不同数学表达式在算术等价上的重复检测
class TestDeduplication(unittest.TestCase):
def setUp(self):
self.test_dir = tempfile.TemporaryDirectory()
self.test_files = {
'question1.txt': """1. 1 / 1/8 + 1 + 1/2
2.4 - 2 + 1'1/2 + 0
3.( 5 * 3 ) * ( 8 - 0 )
4.7 + 0 + 2/3 + 2/7""",
'question2.txt': """1. 1 / 1/8 + 1 + 1/2
2.1'1/2 + 0 + 4 - 2
3.( 8 / 1'2/5 ) / ( 4 * 2'1/2 )
4.( 6 * 2/10 ) * ( 9 - 7 )""",
}
for filename, content in self.test_files.items():
path = os.path.join(self.test_dir.name, filename)
with open(path, 'w', encoding='utf-8') as f:
f.write(content)
def test_deduplication(self):
question_path1 = os.path.join(self.test_dir.name, 'question1.txt')
question_path2 = os.path.join(self.test_dir.name, 'question2.txt')
questions1 = fp.question_read(question_path1)
questions2 = fp.question_read(question_path2)
for q1 in questions1:
if ge.deduplication(questions2, q1):
print("重复")
self.test_dir.cleanup()
10、主流程测试(TestMain)
验证完整的生成+批改端到端流程
class TestMain(unittest.TestCase):
def test_main(self):
m.proofread_the_questions(fp.const_question_path,
fp.const_answer_path,
fp.const_grade_path)
ge.generate_equations(10, 4, 1000)
- 单元测试结果
![]()




浙公网安备 33010602011771号