山东大学计算机科学与技术学院软件工程实验日志(更新中)
---
Author: "Inori_333"
Create Date: 2025-03-04
Update Date: 2025-03-09
2025软件工程实验日志持续更新中
---
实验一 团队建立、阅读开源软件
1.队伍创建与分工
队伍最终确定由5人组成,小组成员之间进行了高效的沟通,并确定了各自的负责的部分内容。
2.代码复现与分析
写在前面:由于“小米便签”项目本身过于陈旧,以及伴随指导教程的落后性,对于今天的复现已经没有太大意义,因此从头开始摸索在更新版本的环境中复现方法。
2.1 环境配置与编译运行
2.1.1 Android Studio的下载与安装
访问Android Studio官网,下载最新稳定版本的 Android Studio 安装程序,下载完成后打开,开始安装。安装过程除路径可以自定义以外,其余全部跟随默认选项安装。在安装进行到倒数第二步时,默认会对 Android Studio 需要的SDK组件进行下载,但下载地址指向国际服务器,因此选择使用网络代理跳转IP,或修改DNS配置,或对本地环境变量进行修改,改源到国内镜像站(如阿里云镜像站、清华镜像站)。
安装成功后,打开 Android Studio。
2.2 代码复现与结构分析
访问“小米便签”开源地址,下载zip包或直接通过Git方式将项目源代码拉取至本地。拉取成功后,运行AS(Android Studio ,之后默认缩写为AS),选择“Open”,选择小米便签的根目录,点击打开。打开后,由于版本落后等问题,系统需要一定时间导入项目,之后会提示Gradle同步错误。
首先是无法验证gradle组件下载源的SSL证书,这是由于下载源链接会被重定向到 https://github.com/gradle/gradle-distributions/releases/download/v8.2.0/gradle-7.2-src.zip
,这个地址在 AS 环境下访问过于困难,报错如下:
Cannot connect to host api.soulter.top:443 ssl:True [SSLCertVerificationError: (1, [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:1010) )]
尝试将./app/wrapper/gradle-wrapper.properties中的下载源更改为国内镜像站,即图中的distributionUrl。
修改后再次尝试对Gradle进行同步,下载成功,但同步仍然失败,这一次返回错误:
Caused by: org.gradle.internal.resolve.ModuleVersionResolveException: Could not resolve gradle:gradle:8.7.
经过查询发现仅更改distributionUrl无法切换下载源指向,此链接已经被写死在Gradle组件本身的源代码中:
private
fun createSourceRepository() = ivy {val repoName = repositoryNameFor(gradleVersion)name = "Gradle $repoName"setUrl("https://services.gradle.org/$repoName")metadataSources {artifact()}patternLayout {if (isSnapshot(gradleVersion)) {ivy("/dummy") // avoids a lookup that interferes with version listing}artifact("[module]-[revision](-[classifier])(.[ext])")}
}
要解决这个问题,可以直接为 Gradle 设置代理进行网络加速。但是这样会导致之前设置的 Maven 镜像链接也会经过代理。
选择另一种暴力的解决方案:绕过系统文件完整性检查。
从Gradle组件源代码中可以看到:
privatefun sourceRootsOf(gradleInstallation: File, sourceDistributionResolver: SourceDistributionProvider): Collection<File> =gradleInstallationSources(gradleInstallation) ?: downloadedSources(sourceDistributionResolver)privatefun gradleInstallationSources(gradleInstallation: File) =File(gradleInstallation, "src").takeIf { it.exists() }?.let { subDirsOf(it) }
当 gradleInstallation
在 src
目录中存在的时候 AS 就不会继续下载 gradle-7.2-src.zip
。这是因为这个值就是 project.gradle.gradleHomeDir。
于是直接把gradle-wrapper.properties
里 distributionUrl
的 bin
改为 all
,再向gradle-wrapper.properties中添加 distributionSha256Sum 属性
,把 distributionSha256Sum
修改为gradle-7.2-bin.zip 对应的值。
修改完成后直接对项目进行 Build ,成功通过哈希校验绕开系统文件完整性检查,成功构筑项目,并打开小米便签。
3.问题思考与总结
3.1 案例分析一
下面这段求两数的平均值的代码在低年级可给满分,请思考它还有什么bug?
unsigned average(unsigned a, unsigned b){ return (a+b)/2; }
首先观察函数类型,类型为 unsigned ,也就是 unsigned int 类型的缩写,所以函数的返回值必须在 unsigned int 能够允许的有效值范围内。再观察传入参数,传入参数的类型为( unsigned , unsigned ),因此两个不同的传入参数也需要传入在 unsigned int 允许的有效值范围内的合法值;再看返回值,返回 (a+b) / 2,因为函数类型为 unsigned ,因此需要保证这个值的范围落在 unsigned 类型的区间内。
可以发现,这个函数存在出现数值溢出情况的bug,当a+b的值超出UINT_MAX常量时,程序无法处理溢出部分,因此在不同架构下可能会出现包括但不限于结果返回0(x86架构,average(0x80000000U, 0x80000000U)=0),结果回绕到较小的值等等。
3.1.1 你还自信自己编过的代码没bug、直接应用于社会没有风险吗?
我一直觉得自己写的代码有很多bug=w=,过去的竞赛、科研和开源社区经验也告诉我,bug这种东西是在所难免的。
3.1.2 你如何信任团队同伴(其他人)编写的代码?
1. 代码审查(Code Review)
-
强制审查流程:通过工具(如 GitHub PR、GitLab MR)要求所有代码必须经过至少一名其他成员的审查才能合并。
2. 编码规范与静态分析
-
统一规范:制定团队代码风格(如命名、注释、错误处理),并通过工具(Clang-Format、ESLint)自动化检查。
-
使用静态分析工具:使用工具(如 SonarQube、Coverity)自动检测潜在问题(内存泄漏、空指针解引用)。
3. 文档与注释
-
自解释代码:优先通过清晰的命名和结构减少注释,但对复杂逻辑需添加必要说明。
-
接口文档:对公共 API 或核心模块,通过文档(如 Swagger、Doxygen)明确输入输出和边界条件。
3.1.3 用什么途径能降低代码风险?
自动化测试
-
单元测试:对每个函数/类编写测试用例,覆盖正常逻辑和异常分支。
-
集成测试:验证模块间的交互,如微服务接口调用、数据库读写。
-
模糊测试(Fuzzing):通过随机输入发现边界问题(如 AFL、libFuzzer)。
2. 持续集成(CI)
-
自动化流水线:每次提交代码时自动运行测试、静态分析和构建。
-
门禁策略:若测试覆盖率不足或静态分析报错,禁止代码合并。
3. 防御性编程
-
输入校验:对函数参数、用户输入、外部数据严格校验(如检查
NULL
、范围限制)。 -
错误处理:明确错误码或异常传递路径,避免静默失败。
-
断言(Assert):在关键逻辑中添加断言,如
assert(b != 0)
。
3.1.4 如何做好软件产品的质量保证(QA,Quality Assurance)
1. 分层测试策略
-
测试金字塔:以大量单元测试为基础,辅以集成测试,当开发软件时,还需要少量端到端(E2E)测试。
-
回归测试:每次迭代后运行历史用例,防止功能回退。
2. 环境隔离与监控
-
多环境部署:通过代码版本管理软件创建多个不同分支,区分开发、测试、预发布和生产环境,避免代码直接部署到生产。
-
监控与日志:通过 Prometheus、ELK 等工具监控运行时异常(如内存泄漏、响应超时)。
3. 用户反馈闭环(开发软件时需要)
-
灰度发布:逐步向小部分用户开放新功能,收集反馈后再全量发布。
-
Bug 跟踪:用 Jira、Redmine 等工具管理问题,确保每个缺陷有根因分析和修复验证。
3.2 案例分析二
double64 转 int16 溢出,这是很古老也是很传统的问题。64 位浮点数的范围远大于 16 位有符号整数。当浮点数值超过 32767 或低于 -32768 时,转换为 16 位整数就会溢出。这种情况下,转换结果会是未定义的行为,或者在某些编程语言中可能产生截断,但显然这里没有正确处理溢出。这个问题和刚才的案例一有异曲同工之处,都是很简单的bug,不同的是,这一次这个bug没有再以单独出现的形式让我们检查,而是被嵌入在了火箭生产与发射这样一个复杂的工业环境中,可以想象的是,箭载计算机的程序代码量会是比较庞大的,因此这样一个短小的bug很容易被忽略,尤其是该程序在先前版本的火箭上已经经过了测试,更加容易被认为是已经稳定的代码而不再进行修改。
从逻辑上来说,这是控制变量法的盲区,因为火箭的迭代无法单次控制一个变量进行测试,因此可能工程师不会想到发射速度会意外激活死代码的问题,但归根结底这还是测试和防御性编程缺失导致的。
-
首先,极端场景覆盖不足
火箭发射过程中的高速状态需要复杂的模拟环境,而测试可能更关注“典型”场景:由于测试数据限制可能仅使用预估值或历史数据,未模拟超范围值(如速度超过32767)。由于环境模拟成本高精度模拟火箭加速阶段的极端条件可能成本高昂,导致测试覆盖不全。 -
其次,代码审查的盲区
可以想象,对于这种古老而典型的问题,静态分析工具正常来说不会遗漏此类问题。审查流程缺陷可能导致代码审查可能更关注功能逻辑而非边界条件,尤其是对“看似无害”的类型转换操作。
实验二 团队建立、阅读开源软件 第二周
1.开源项目分析工作
参照小组提交的小米便签开源代码的泛读报告。
2.分析PPT中的案例内容
2.1 皮卡地里电视台广告售卖系统
用例图中的图元素代表的含义
用例图(Use Case Diagram)是一种UML(统一建模语言)图,主要用于描述系统的功能需求及其与用户(角色)之间的交互关系。其主要元素包括:
1. 参与者(Actor)
代表外部用户或系统(人、硬件、其他软件等),它们与系统交互。
在用例图中通常用小人图标表示。
2. 用例(Use Case)
代表系统提供的功能,即用户可以执行的操作。
在用例图中通常用椭圆形表示。
3. 系统(System)
代表整个系统的边界,系统内包含所有的用例。
在用例图中通常用矩形框住所有用例。
4. 关系(Relationships)
关联(Association):连接参与者和用例,表示交互。
泛化(Generalization):表示父用例或父角色被子用例或子角色继承。
包含(<<include>>):表示某个用例必然会调用另一个用例的功能。
扩展(<<extend>>):表示某个用例可能在特定条件下扩展执行另一个用例。
构造型 <<include>> 和 <<extend>> 的含义
1. <<include>>(包含关系)
含义:表示一个用例必须执行另一个用例的功能。
特点:
主要用于功能复用。
被包含的用例不能单独运行,必须被其他用例调用。
例如,在广告售卖系统中,"创建广告活动" 可能会包含 "设定广告价格",因为每个广告活动都必须设置价格。
示例
"创建广告活动" <<include>> "设定广告价格"
2. <<extend>>(扩展关系)
含义:表示一个用例可能在某些条件下扩展执行另一个用例。
特点:
用于描述可选行为(非强制)。
被扩展的用例可以独立运行,而扩展的部分是在特定条件下执行的。
例如,在广告售卖系统中,"处理广告订单" 可能扩展"应用折扣",但应用折扣并不是每个订单都必须执行的。
示例
"处理广告订单" <<extend>> "应用折扣"
应用在皮卡地里电视台广告售卖系统的用例图分析
根据提供的内容,皮卡地里电视台的广告售卖系统涉及多个相关的业务,如广告代理、电视收视率、广告投放等,可能的用例关系如下:
用例1:"广告代理提交广告"
可能包含 <<include>>:"广告审核"
可能包含 <<include>>:"广告定价
用例2:"广告播放"
可能扩展 <<extend>>:"广告内容审核"(如果内容不合格)
用例3:"广告收视率报告"
可能包含 <<include>>:"计算平均收视率"
可能扩展 <<extend>>:"广告投放调整"(如果收视率低)
2.2 实时系统:分析导致failure原因
1.failure 的原因
火箭发射系统的设计师并没有遵循“假设软件存在异常”的原则,对于代码的检查不够到位,同时相关组织也没有相应的要求,就使得程序员对这方面的bug没有仔细排查。在数据转换过程中,一个64 位的浮点数被尝试转换成16 位的有符号整数,但很明显这样转换会导致有符号整数发生溢出等异常,从而使得其得到一个异常的速度,而火箭的检测系统发现这个异常数据后,就认为火箭已经偏离了航向,就导致整个系统发生了解体。
2.改进方案
在测试阶段需要给出更全面的测试集,像这样double 转成int 的情况一定要给出相应的测试样例,从而找到这个错误并将bug 改正。即测试的时候数据要尽可能种类齐全,double,float,char,int,short 等所有可能用到的数据都要给出样例,同时不同的数据大小也要给出相应的测试样例。也要考虑到数据溢出的情况,例如两个比较大的数据加起来超过int 了,可能就会导致异常,这时系统必须要采取相应的措施来梳理错误。测试的过程可以包括单元测试,集成测试,压力测试等,同时需要经过多名技术人员进行多层审查,设置备份系统和冗余机制,持续学习与改进。
3.使用华为云账号完成一个官方实验
3.1 选择要做的实验
选择本地部署Deepseek-1.5b。
3.2 购买ECS弹性云服务器
因为华为云官方提供的免费实验方法太过复杂,选择了直接自费购买ECS弹性云服务器。
购买参数:
服务器地区:华北-北京四
服务器规格:通用计算增强型|c7.xlarge.4|4vCPUs|16GB|linux
服务器系统:Ubuntu 22.04
服务器VPC:默认
子网:默认创建
安全组规则:默认保护
虚拟盘挂载:100GB
网络带宽:100MB/S
3.3 项目部署与运行
3.3.1 部署ollama
检查ollama服务是否成功启动
3.3.2 拉取Deepseek-R1-Lite-Preview-1.5b
3.3.3 本地运行Deepseek-1.5b
尝试进行对话: