- 标注:AI 预标注直写 labels_json(去候选确认两阶段);重叠去重(minIoU);全量标注按钮 - 训练:脚本迁移入 server/training/(Go 化 prepare_yolo/analyze_rfdetr,保留 train_server.py);tflite 产物自检并入训练流程(check_tflite) - 数据目录/权重不进 git;.gitignore 迁移至仓库根
230 lines
7.5 KiB
Go
230 lines
7.5 KiB
Go
package service
|
||
|
||
// App 版本管理白盒测试:下发(APK 上传 + 记录)、最新版本查询、重复版本拒绝、
|
||
// APK 固定文件覆盖(目录永远只有一个文件)。
|
||
// 运行方式同 admin_test.go:cd server && GF_GCFG_FILE=biz/service/testdata/config.yml go test ./biz/service/
|
||
|
||
import (
|
||
"bytes"
|
||
"mime/multipart"
|
||
"os"
|
||
"path/filepath"
|
||
"strconv"
|
||
"testing"
|
||
"time"
|
||
|
||
"github.com/gogf/gf/v2/net/ghttp"
|
||
|
||
"observer-server/biz/dao"
|
||
"observer-server/biz/model/dto"
|
||
"observer-server/common"
|
||
)
|
||
|
||
// uniqueVersion 每次运行生成唯一版本号,避免测试库残留数据冲突
|
||
func uniqueVersion() string {
|
||
return "9." + strconv.FormatInt(time.Now().UnixNano()%100000000, 10) + ".0"
|
||
}
|
||
|
||
// newApkUpload 构造真实 multipart 上传文件(Save 需要可读的文件内容)
|
||
func newApkUpload(t *testing.T, filename, content string) *ghttp.UploadFile {
|
||
t.Helper()
|
||
var buf bytes.Buffer
|
||
w := multipart.NewWriter(&buf)
|
||
fw, err := w.CreateFormFile("file", filename)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if _, err := fw.Write([]byte(content)); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
w.Close()
|
||
form, err := multipart.NewReader(&buf, w.Boundary()).ReadForm(1 << 20)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
return &ghttp.UploadFile{FileHeader: form.File["file"][0]}
|
||
}
|
||
|
||
// countApk 目录下 apk 文件数(不含临时文件)
|
||
func countApk(t *testing.T) int {
|
||
t.Helper()
|
||
entries, err := os.ReadDir(common.ApkDir(ctx()))
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
n := 0
|
||
for _, e := range entries {
|
||
if filepath.Ext(e.Name()) == ".apk" {
|
||
n++
|
||
}
|
||
}
|
||
return n
|
||
}
|
||
|
||
// TestAppVersionUpdate 下发:落库 + APK 保存固定文件,更新检查返回最新记录
|
||
func TestAppVersionUpdate(t *testing.T) {
|
||
ver := uniqueVersion()
|
||
if _, err := AppVersion.AdminAddVersion(ctx(), &dto.AdminAppVersionAddReq{
|
||
Notes: "修复识别准确率", File: newApkUpload(t, "observer-"+ver+".apk", "apk-v1"),
|
||
}); err != nil {
|
||
t.Fatalf("add version: %v", err)
|
||
}
|
||
res, err := AppVersion.GetUpdate(ctx(), &dto.AppUpdateReq{})
|
||
if err != nil {
|
||
t.Fatalf("get update: %v", err)
|
||
}
|
||
if res.Version != ver || res.Notes != "修复识别准确率" {
|
||
t.Fatalf("update = %+v", res)
|
||
}
|
||
// APK 已保存为固定文件名
|
||
content, err := os.ReadFile(common.ApkFilePath(ctx()))
|
||
if err != nil || string(content) != "apk-v1" {
|
||
t.Fatalf("apk file = %q, %v", content, err)
|
||
}
|
||
if n := countApk(t); n != 1 {
|
||
t.Fatalf("apk count = %d, want 1", n)
|
||
}
|
||
}
|
||
|
||
// TestAppVersionOverwrite 再次下发:固定文件被新内容覆盖,目录仍只有一个文件
|
||
func TestAppVersionOverwrite(t *testing.T) {
|
||
ver1 := uniqueVersion()
|
||
ver2 := uniqueVersion() + "1"
|
||
req1 := &dto.AdminAppVersionAddReq{File: newApkUpload(t, "observer-"+ver1+".apk", "apk-old")}
|
||
req2 := &dto.AdminAppVersionAddReq{File: newApkUpload(t, "observer-"+ver2+".apk", "apk-new")}
|
||
if _, err := AppVersion.AdminAddVersion(ctx(), req1); err != nil {
|
||
t.Fatalf("add v1: %v", err)
|
||
}
|
||
if _, err := AppVersion.AdminAddVersion(ctx(), req2); err != nil {
|
||
t.Fatalf("add v2: %v", err)
|
||
}
|
||
content, err := os.ReadFile(common.ApkFilePath(ctx()))
|
||
if err != nil || string(content) != "apk-new" {
|
||
t.Fatalf("apk file = %q, %v; want new content", content, err)
|
||
}
|
||
if n := countApk(t); n != 1 {
|
||
t.Fatalf("apk count = %d, want 1", n)
|
||
}
|
||
}
|
||
|
||
// TestAppVersionInvalidFile 无文件 / 文件名不含版本号(格式不符)拒绝
|
||
func TestAppVersionInvalidFile(t *testing.T) {
|
||
if _, err := AppVersion.AdminAddVersion(ctx(), &dto.AdminAppVersionAddReq{}); err == nil {
|
||
t.Fatal("missing file should fail")
|
||
}
|
||
for _, name := range []string{"app.txt", "myapp-1.0.apk", "observer-1.0.apk", "observer-abc.apk"} {
|
||
if _, err := AppVersion.AdminAddVersion(ctx(), &dto.AdminAppVersionAddReq{
|
||
File: newApkUpload(t, name, "not an apk"),
|
||
}); err == nil {
|
||
t.Fatalf("file %s should fail (version not parseable)", name)
|
||
}
|
||
}
|
||
}
|
||
|
||
// TestAppVersionDuplicate 同版本号重复下发报错
|
||
func TestAppVersionDuplicate(t *testing.T) {
|
||
ver := uniqueVersion()
|
||
req := &dto.AdminAppVersionAddReq{File: newApkUpload(t, "observer-"+ver+".apk", "apk")}
|
||
if _, err := AppVersion.AdminAddVersion(ctx(), req); err != nil {
|
||
t.Fatalf("add version: %v", err)
|
||
}
|
||
if _, err := AppVersion.AdminAddVersion(ctx(), req); err == nil {
|
||
t.Fatal("duplicate version should fail")
|
||
}
|
||
}
|
||
|
||
// TestAppVersionList 分页列表:新增多条后按下发时间倒序
|
||
func TestAppVersionList(t *testing.T) {
|
||
base := uniqueVersion()
|
||
for _, v := range []string{base, base + "1"} {
|
||
if _, err := AppVersion.AdminAddVersion(ctx(), &dto.AdminAppVersionAddReq{
|
||
File: newApkUpload(t, "observer-"+v+".apk", "apk"),
|
||
}); err != nil {
|
||
t.Fatalf("add version %s: %v", v, err)
|
||
}
|
||
}
|
||
res, err := AppVersion.AdminListVersions(ctx(), &dto.AdminAppVersionListReq{Size: 10})
|
||
if err != nil {
|
||
t.Fatalf("list versions: %v", err)
|
||
}
|
||
if res.Total < 2 {
|
||
t.Fatalf("total = %d, want >= 2", res.Total)
|
||
}
|
||
// 倒序:最新一条为 base+1
|
||
if res.List[0].Version != base+"1" {
|
||
t.Fatalf("latest = %s, want %s", res.List[0].Version, base+"1")
|
||
}
|
||
}
|
||
|
||
// TestAppVersionDeleteLatest 删除最新版本:记录删除 + APK 文件联动删除
|
||
func TestAppVersionDeleteLatest(t *testing.T) {
|
||
ver := uniqueVersion()
|
||
if _, err := AppVersion.AdminAddVersion(ctx(), &dto.AdminAppVersionAddReq{
|
||
File: newApkUpload(t, "observer-"+ver+".apk", "apk-v1"),
|
||
}); err != nil {
|
||
t.Fatalf("add version: %v", err)
|
||
}
|
||
latest, err := dao.AppVersion.Latest(ctx())
|
||
if err != nil || latest == nil {
|
||
t.Fatalf("latest = %+v, %v", latest, err)
|
||
}
|
||
if _, err := AppVersion.AdminDeleteVersion(ctx(), &dto.AdminAppVersionDeleteReq{Id: latest.Id}); err != nil {
|
||
t.Fatalf("delete version: %v", err)
|
||
}
|
||
// 记录已删(测试库有历史残留记录,update 不再返回被删版本即可)
|
||
gone, err := dao.AppVersion.GetById(ctx(), latest.Id)
|
||
if err != nil || gone != nil {
|
||
t.Fatalf("record should be deleted: %+v, %v", gone, err)
|
||
}
|
||
res, err := AppVersion.GetUpdate(ctx(), &dto.AppUpdateReq{})
|
||
if err != nil {
|
||
t.Fatalf("get update: %v", err)
|
||
}
|
||
if res.Version == ver {
|
||
t.Fatalf("update still returns deleted version %s", ver)
|
||
}
|
||
// APK 文件联动删除(下载 404)
|
||
if _, err := os.Stat(common.ApkFilePath(ctx())); !os.IsNotExist(err) {
|
||
t.Fatalf("apk file should be removed, stat err = %v", err)
|
||
}
|
||
}
|
||
|
||
// TestAppVersionDeleteHistoric 删除历史版本:仅删记录,APK 文件保留(仍对应最新版本)
|
||
func TestAppVersionDeleteHistoric(t *testing.T) {
|
||
ver1 := uniqueVersion()
|
||
ver2 := uniqueVersion() + "1"
|
||
for _, v := range []string{ver1, ver2} {
|
||
if _, err := AppVersion.AdminAddVersion(ctx(), &dto.AdminAppVersionAddReq{
|
||
File: newApkUpload(t, "observer-"+v+".apk", "apk"),
|
||
}); err != nil {
|
||
t.Fatalf("add version %s: %v", v, err)
|
||
}
|
||
}
|
||
old, err := dao.AppVersion.GetByVersion(ctx(), ver1)
|
||
if err != nil || old == nil {
|
||
t.Fatalf("get by version: %+v, %v", old, err)
|
||
}
|
||
if _, err := AppVersion.AdminDeleteVersion(ctx(), &dto.AdminAppVersionDeleteReq{Id: old.Id}); err != nil {
|
||
t.Fatalf("delete version: %v", err)
|
||
}
|
||
// 最新记录仍是 ver2
|
||
res, err := AppVersion.GetUpdate(ctx(), &dto.AppUpdateReq{})
|
||
if err != nil {
|
||
t.Fatalf("get update: %v", err)
|
||
}
|
||
if res.Version != ver2 {
|
||
t.Fatalf("update = %+v, want %s", res, ver2)
|
||
}
|
||
// APK 文件保留
|
||
if _, err := os.Stat(common.ApkFilePath(ctx())); err != nil {
|
||
t.Fatalf("apk file should remain: %v", err)
|
||
}
|
||
}
|
||
|
||
// TestAppVersionDeleteNotFound 删除不存在的版本报错
|
||
func TestAppVersionDeleteNotFound(t *testing.T) {
|
||
if _, err := AppVersion.AdminDeleteVersion(ctx(), &dto.AdminAppVersionDeleteReq{Id: 999999999}); err == nil {
|
||
t.Fatal("delete missing version should fail")
|
||
}
|
||
}
|