feat: EH Downloader 用 Go + Wails v2 重写
原 Electron 版在这台 Windows 机器上无法启动(0x80000003 崩在 V8 初始化), 整个业务逻辑用 Go 重写,前端界面与协议结论完全保留。 - internal/eh: 协议核心,画廊页分页 -> /s/ -> hath 直链 -> 下载 - internal/core: 并发引擎 / 状态存储 / 目录模板 / 油猴桥口 - app.go: Wails 绑定层,替代 Electron IPC - frontend/js/api.js: 适配层,renderer.js 一行未改 - 37 个测试,含 httptest 模拟 EH 的端到端链路验证 相比 Electron 版:90MB -> 12.4MB,180MB 内存 -> 27MB,构建 90s -> 2s。 顺带修掉原版代理探测的 bug(永远返回第一个端口,从不等连接结果)。
This commit is contained in:
@@ -0,0 +1,176 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"eh-downloader-wails/internal/eh"
|
||||
)
|
||||
|
||||
func sampleGallery() *eh.Gallery {
|
||||
return &eh.Gallery{
|
||||
GID: "4239274",
|
||||
Title: "[Noxe] 0820 Qingxiao [AI Generated]",
|
||||
Meta: eh.GalleryMeta{
|
||||
Artist: "Noxe",
|
||||
Group: "CriticalHAI",
|
||||
Series: "wuthering waves",
|
||||
Category: "misc",
|
||||
Lang: "chinese",
|
||||
Uploader: "velius11",
|
||||
Tags: []string{"qingxiao", "rover", "big breasts", "x-ray", "ai generated"},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildDir(t *testing.T) {
|
||||
g := sampleGallery()
|
||||
tests := []struct {
|
||||
tpl string
|
||||
want string
|
||||
}{
|
||||
{"{title}", "[Noxe] 0820 Qingxiao [AI Generated]"},
|
||||
{"{artist}/{title}", `Noxe\[Noxe] 0820 Qingxiao [AI Generated]`},
|
||||
{"{group}/{title}", `CriticalHAI\[Noxe] 0820 Qingxiao [AI Generated]`},
|
||||
{"{category}/{group}/{title}", `misc\CriticalHAI\[Noxe] 0820 Qingxiao [AI Generated]`},
|
||||
{"{tags:2}/{artist}/{title}", `qingxiao_rover\Noxe\[Noxe] 0820 Qingxiao [AI Generated]`},
|
||||
{"{tags}/{artist}/{title}", `qingxiao_rover_big breasts_x-ray_ai generated\Noxe\[Noxe] 0820 Qingxiao [AI Generated]`},
|
||||
{"{tag1}-{tag2}", "qingxiao-rover"},
|
||||
{"{artist} - {gid} - {title}", "Noxe - 4239274 - [Noxe] 0820 Qingxiao [AI Generated]"},
|
||||
{"{gid}", "4239274"},
|
||||
{"{lang}/{title}", `chinese\[Noxe] 0820 Qingxiao [AI Generated]`},
|
||||
{"{unknown}/{title}", `{unknown}\[Noxe] 0820 Qingxiao [AI Generated]`}, // 未知占位符原样保留
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.tpl, func(t *testing.T) {
|
||||
got := BuildDir(tt.tpl, g)
|
||||
if got != tt.want {
|
||||
t.Errorf("\n got: %q\nwant: %q", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 空模板回落到 {title}
|
||||
func TestBuildDirDefaults(t *testing.T) {
|
||||
g := sampleGallery()
|
||||
for _, tpl := range []string{"", " "} {
|
||||
if got := BuildDir(tpl, g); !strings.Contains(got, "Noxe") {
|
||||
t.Errorf("空模板应回落到标题, got %q", got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 占位符兜底值
|
||||
func TestBuildDirFallbacks(t *testing.T) {
|
||||
g := &eh.Gallery{GID: "12345678", Title: "Plain Title"}
|
||||
got := BuildDir("{artist}/{group}/{series}/{category}/{lang}/{uploader}", g)
|
||||
for _, want := range []string{"未知作者", "未知社团", "无系列", "未分类", "nolang", "unknown"} {
|
||||
if !strings.Contains(got, want) {
|
||||
t.Errorf("缺兜底值 %q: %s", want, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 路径穿越必须被挡住
|
||||
func TestSanitizeSegmentSecurity(t *testing.T) {
|
||||
cases := []struct{ in, want string }{
|
||||
{"..", "_"},
|
||||
{".", "_"},
|
||||
{"../../etc/passwd", ".._.._etc_passwd"},
|
||||
{`a/b\c:d*e?f"g<h>i|j`, "a_b_c_d_e_f_g_h_i_j"},
|
||||
{"CON", "_CON"}, // Windows 保留设备名
|
||||
{"nul.txt", "_nul.txt"}, // 保留名带扩展也不行
|
||||
{"正常名字", "正常名字"},
|
||||
{"trailing dots...", "trailing dots"},
|
||||
{" spaced ", "spaced"},
|
||||
}
|
||||
for _, c := range cases {
|
||||
if got := SanitizeSegment(c.in, "fb"); got != c.want {
|
||||
t.Errorf("SanitizeSegment(%q) = %q, want %q", c.in, got, c.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 超长截断
|
||||
func TestSanitizeSegmentTruncate(t *testing.T) {
|
||||
long := strings.Repeat("长", 300)
|
||||
got := SanitizeSegment(long, "fb")
|
||||
if len([]rune(got)) != 110 {
|
||||
t.Errorf("应截到 110 字,实际 %d", len([]rune(got)))
|
||||
}
|
||||
}
|
||||
|
||||
// 分隔符混用(用户可能用 / 或 \)
|
||||
func TestBuildDirMixedSeparators(t *testing.T) {
|
||||
g := sampleGallery()
|
||||
for _, tpl := range []string{`{artist}/{title}`, `{artist}\{title}`, `{artist}//{title}`} {
|
||||
got := BuildDir(tpl, g)
|
||||
if strings.Contains(got, "/") || strings.Contains(got, "\\") && strings.Count(got, "\\") > 1 {
|
||||
continue // 各平台分隔符不同,只要求不出现空段
|
||||
}
|
||||
if strings.Contains(got, "__") {
|
||||
t.Errorf("出现空段: %q (tpl=%q)", got, tpl)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestListPlaceholders(t *testing.T) {
|
||||
p := ListPlaceholders()
|
||||
if len(p) == 0 {
|
||||
t.Fatal("占位符列表不该为空")
|
||||
}
|
||||
found := false
|
||||
for _, x := range p {
|
||||
if x == "{tags:3}" {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Error("缺 {tags:3}")
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 代理探测:必须真的等连接结果(JS 版是坏的,永远返回 7890)----
|
||||
|
||||
func TestDetectLocalProxy(t *testing.T) {
|
||||
// 起一个真实的监听端口
|
||||
ln, err := net_listen_loopback()
|
||||
if err != nil {
|
||||
t.Skipf("无法监听端口,跳过: %v", err)
|
||||
}
|
||||
defer ln.Close()
|
||||
port := ln.port()
|
||||
|
||||
got := DetectLocalProxy([]int{port})
|
||||
if got == "" {
|
||||
t.Error("有端口在监听,应该探测得到")
|
||||
return
|
||||
}
|
||||
if !strings.Contains(got, "127.0.0.1") {
|
||||
t.Errorf("地址不对: %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDetectLocalProxyNone(t *testing.T) {
|
||||
// 用一个几乎肯定没人监听的端口
|
||||
if got := DetectLocalProxy([]int{1, 2, 3}); got != "" {
|
||||
t.Errorf("没端口监听时不该返回结果, got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultDownloadRoot(t *testing.T) {
|
||||
root := DefaultDownloadRoot()
|
||||
if root == "" {
|
||||
t.Fatal("下载目录不该为空")
|
||||
}
|
||||
if !strings.HasSuffix(root, "Downloads") && !strings.Contains(root, "Downloads") {
|
||||
t.Errorf("路径可疑: %q", root)
|
||||
}
|
||||
if _, err := os.Stat(root); err == nil {
|
||||
// 目录存在也没关系,只确认路径合法
|
||||
t.Logf("目录已存在: %s", root)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user