215 lines
6.4 KiB
C++
215 lines
6.4 KiB
C++
|
|
/**
|
|||
|
|
* @file basic_test.cpp
|
|||
|
|
* @brief 基础测试文件 - 使用标准库 assert,无外部依赖
|
|||
|
|
*/
|
|||
|
|
|
|||
|
|
#include "app.hpp"
|
|||
|
|
#include <cassert>
|
|||
|
|
#include <iostream>
|
|||
|
|
#include <vector>
|
|||
|
|
#include <string>
|
|||
|
|
|
|||
|
|
/// 测试枚举转换函数
|
|||
|
|
static void testEnumToString() {
|
|||
|
|
std::cout << "[TEST] 枚举转换函数..." << std::endl;
|
|||
|
|
|
|||
|
|
assert(checkinMethodToString(CheckinMethod::FACE) == "人脸识别");
|
|||
|
|
assert(checkinMethodToString(CheckinMethod::GPS) == "GPS定位");
|
|||
|
|
assert(checkinMethodToString(CheckinMethod::CARD) == "工牌刷卡");
|
|||
|
|
|
|||
|
|
assert(attendanceStatusToString(AttendanceStatus::NORMAL) == "正常");
|
|||
|
|
assert(attendanceStatusToString(AttendanceStatus::LATE) == "迟到");
|
|||
|
|
assert(attendanceStatusToString(AttendanceStatus::EARLY) == "早退");
|
|||
|
|
assert(attendanceStatusToString(AttendanceStatus::ABSENT) == "缺勤");
|
|||
|
|
assert(attendanceStatusToString(AttendanceStatus::OVERTIME) == "加班");
|
|||
|
|
|
|||
|
|
assert(alertTypeToString(AlertType::UNCHECKED) == "未打卡");
|
|||
|
|
assert(alertTypeToString(AlertType::EARLY) == "早退预警");
|
|||
|
|
assert(alertTypeToString(AlertType::LATE) == "迟到预警");
|
|||
|
|
assert(alertTypeToString(AlertType::MISSING) == "连续缺勤");
|
|||
|
|
|
|||
|
|
std::cout << " PASS" << std::endl;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// 测试记录ID生成
|
|||
|
|
static void testGenerateRecordId() {
|
|||
|
|
std::cout << "[TEST] recordId生成..." << std::endl;
|
|||
|
|
std::string id1 = generateRecordId();
|
|||
|
|
std::string id2 = generateRecordId();
|
|||
|
|
|
|||
|
|
assert(!id1.empty());
|
|||
|
|
assert(!id2.empty());
|
|||
|
|
assert(id1 != id2); // 两次生成的ID应该不同
|
|||
|
|
assert(id1.substr(0, 2) == "CK");
|
|||
|
|
|
|||
|
|
std::cout << " PASS: id1=" << id1 << ", id2=" << id2 << std::endl;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// 测试打卡处理
|
|||
|
|
static void testProcessCheckin() {
|
|||
|
|
std::cout << "[TEST] 打卡处理..." << std::endl;
|
|||
|
|
|
|||
|
|
GeoLocation loc = {39.9042, 116.4074};
|
|||
|
|
std::vector<uint8_t> face = {0x01, 0x02, 0x03};
|
|||
|
|
int64_t ts = 1700000000000LL; // 固定时间戳
|
|||
|
|
|
|||
|
|
// 人脸打卡
|
|||
|
|
CheckinRecord rec1 = processCheckin("EMP001", CheckinMethod::FACE, ts, loc, face);
|
|||
|
|
assert(rec1.employeeId == "EMP001");
|
|||
|
|
assert(rec1.status == CheckinStatus::SUCCESS);
|
|||
|
|
assert(!rec1.recordId.empty());
|
|||
|
|
|
|||
|
|
// GPS打卡
|
|||
|
|
CheckinRecord rec2 = processCheckin("EMP002", CheckinMethod::GPS, ts, loc, {});
|
|||
|
|
assert(rec2.employeeId == "EMP002");
|
|||
|
|
assert(rec2.status == CheckinStatus::SUCCESS);
|
|||
|
|
|
|||
|
|
// 工牌刷卡
|
|||
|
|
CheckinRecord rec3 = processCheckin("EMP003", CheckinMethod::CARD, ts, loc, {});
|
|||
|
|
assert(rec3.employeeId == "EMP003");
|
|||
|
|
assert(rec3.status == CheckinStatus::SUCCESS);
|
|||
|
|
|
|||
|
|
std::cout << " PASS: 三种打卡方式均成功" << std::endl;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// 测试考勤计算
|
|||
|
|
static void testCalculateAttendance() {
|
|||
|
|
std::cout << "[TEST] 考勤计算..." << std::endl;
|
|||
|
|
|
|||
|
|
ShiftRule shift;
|
|||
|
|
shift.shiftName = "早班";
|
|||
|
|
shift.startHour = 9;
|
|||
|
|
shift.startMinute = 0;
|
|||
|
|
shift.endHour = 18;
|
|||
|
|
shift.endMinute = 0;
|
|||
|
|
shift.breakDuration = 60;
|
|||
|
|
shift.overtimeThreshold = 30;
|
|||
|
|
|
|||
|
|
// 场景1: 准时打卡 (9:00)
|
|||
|
|
{
|
|||
|
|
struct tm tm_in = {};
|
|||
|
|
tm_in.tm_year = 2025 - 1900;
|
|||
|
|
tm_in.tm_mon = 0;
|
|||
|
|
tm_in.tm_mday = 15;
|
|||
|
|
tm_in.tm_hour = 9;
|
|||
|
|
tm_in.tm_min = 0;
|
|||
|
|
time_t sec = mktime(&tm_in);
|
|||
|
|
CheckinRecord rec;
|
|||
|
|
rec.employeeId = "EMP001";
|
|||
|
|
rec.timestamp = static_cast<int64_t>(sec) * 1000;
|
|||
|
|
|
|||
|
|
AttendanceResult res = calculateAttendance(rec, shift);
|
|||
|
|
assert(res.lateMinutes == 0);
|
|||
|
|
assert(res.status == AttendanceStatus::NORMAL || res.status == AttendanceStatus::NORMAL);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 场景2: 迟到 (9:45)
|
|||
|
|
{
|
|||
|
|
struct tm tm_in = {};
|
|||
|
|
tm_in.tm_year = 2025 - 1900;
|
|||
|
|
tm_in.tm_mon = 0;
|
|||
|
|
tm_in.tm_mday = 15;
|
|||
|
|
tm_in.tm_hour = 9;
|
|||
|
|
tm_in.tm_min = 45;
|
|||
|
|
time_t sec = mktime(&tm_in);
|
|||
|
|
CheckinRecord rec;
|
|||
|
|
rec.employeeId = "EMP002";
|
|||
|
|
rec.timestamp = static_cast<int64_t>(sec) * 1000;
|
|||
|
|
|
|||
|
|
AttendanceResult res = calculateAttendance(rec, shift);
|
|||
|
|
assert(res.lateMinutes > 0);
|
|||
|
|
assert(res.status == AttendanceStatus::LATE);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
std::cout << " PASS: 正常与迟到判定正确" << std::endl;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// 测试异常检测
|
|||
|
|
static void testDetectAnomaly() {
|
|||
|
|
std::cout << "[TEST] 异常检测..." << std::endl;
|
|||
|
|
|
|||
|
|
// 正常情况 - 不应生成预警
|
|||
|
|
{
|
|||
|
|
AttendanceResult normal;
|
|||
|
|
normal.employeeId = "EMP001";
|
|||
|
|
normal.date = "2025-01-15";
|
|||
|
|
normal.lateMinutes = 0;
|
|||
|
|
normal.absent = false;
|
|||
|
|
|
|||
|
|
AlertNotification alert = detectAnomaly(normal);
|
|||
|
|
assert(alert.alertType == AlertType::UNCHECKED);
|
|||
|
|
assert(alert.reason == "正常");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 迟到超过30分钟 - 应生成迟到预警
|
|||
|
|
{
|
|||
|
|
AttendanceResult late;
|
|||
|
|
late.employeeId = "EMP002";
|
|||
|
|
late.date = "2025-01-15";
|
|||
|
|
late.lateMinutes = 45;
|
|||
|
|
late.absent = false;
|
|||
|
|
|
|||
|
|
AlertNotification alert = detectAnomaly(late);
|
|||
|
|
assert(alert.alertType == AlertType::LATE);
|
|||
|
|
assert(!alert.notifiedChannels.empty());
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
std::cout << " PASS" << std::endl;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// 测试报表生成
|
|||
|
|
static void testGenerateReport() {
|
|||
|
|
std::cout << "[TEST] 报表生成..." << std::endl;
|
|||
|
|
|
|||
|
|
ReportRequest req;
|
|||
|
|
req.dimension = ReportDimension::MONTH;
|
|||
|
|
req.startDate = "2025-01-01";
|
|||
|
|
req.endDate = "2025-01-31";
|
|||
|
|
req.format = ExportFormat::EXCEL;
|
|||
|
|
|
|||
|
|
ReportResponse resp = generateReport(req);
|
|||
|
|
assert(resp.success);
|
|||
|
|
assert(!resp.reportUrl.empty());
|
|||
|
|
assert(resp.fileSize > 0);
|
|||
|
|
|
|||
|
|
std::cout << " PASS: URL=" << resp.reportUrl << std::endl;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// 测试加密模块
|
|||
|
|
static void testEncryption() {
|
|||
|
|
std::cout << "[TEST] 人脸特征加密..." << std::endl;
|
|||
|
|
|
|||
|
|
std::vector<uint8_t> original = {0x10, 0x20, 0x30, 0x40, 0x50};
|
|||
|
|
std::vector<uint8_t> encrypted = encryptFaceFeature(original);
|
|||
|
|
|
|||
|
|
assert(encrypted.size() == original.size());
|
|||
|
|
|
|||
|
|
// 加密后应该与原文不同
|
|||
|
|
bool different = false;
|
|||
|
|
for (size_t i = 0; i < original.size(); i++) {
|
|||
|
|
if (encrypted[i] != original[i]) {
|
|||
|
|
different = true;
|
|||
|
|
break;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
assert(different);
|
|||
|
|
|
|||
|
|
std::cout << " PASS: 加密后数据已变更" << std::endl;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// 主测试入口
|
|||
|
|
int main() {
|
|||
|
|
std::cout << "========== 考勤系统单元测试 ==========" << std::endl;
|
|||
|
|
|
|||
|
|
testEnumToString();
|
|||
|
|
testGenerateRecordId();
|
|||
|
|
testProcessCheckin();
|
|||
|
|
testCalculateAttendance();
|
|||
|
|
testDetectAnomaly();
|
|||
|
|
testGenerateReport();
|
|||
|
|
testEncryption();
|
|||
|
|
|
|||
|
|
std::cout << "\n========== 所有测试通过! ==========" << std::endl;
|
|||
|
|
return 0;
|
|||
|
|
}
|