fix: 解析大画廊时连接泄漏 + 解析阶段无进度反馈
用户反馈:点开始下载后 6 个画廊(都是 2000 张级)一直显示「解析中 0/?」, 看起来像卡死。排查发现两个问题: 1. 连接泄漏(真 bug) httpClient() 每次请求都新建 http.Transport,用完丢弃。Transport 被丢弃后 它持有的 keep-alive 连接不会立刻释放(要等 IdleConnTimeout=90s), 于是每发一个请求留下一条僵尸连接。 现场实测:91 条 Established 连接,而解析是串行的,正常应该只有 1 条。 修复:Transport 全局复用,只在代理配置变化时重建, 重建时 CloseIdleConnections 关掉旧连接;新增 Client.Close()。 实测:20 次请求从 20 条连接降到 1 条;100 次并发请求只用 22 条。 2. 解析阶段毫无反馈 2000 张 = 100 屏串行翻页,整个解析阶段界面都显示「解析中 0/?」, 完全没有进度,用户只能干等。 修复:FetchGalleryPage 增加 ParseProgress 回调,每翻完一屏上报一次 (已解析张数/总张数/当前屏),界面显示「已解析 340/2000 张」。 另外:用户设置里请求间隔是 1000ms(1 秒),全局限流让 5 个画廊共享 1 请求/秒 —— 500 次解析请求需要约 8 分钟。这是配置导致的慢,不是 bug, 但可以在界面上看到进度后就不会以为卡死了。 测试从 74 增至 80,新增: - TestTransportIsReused / TestTransportReusedUnderConcurrency(连接复用) - TestSetProxyRebuildsTransport(改代理要重建) - TestParseProgressCallback(进度回调,含最后一屏)
This commit is contained in:
+28
-13
@@ -10,7 +10,7 @@ const logBuf = [];
|
||||
// ---------------- 渲染 ----------------
|
||||
|
||||
const STATUS_TEXT = {
|
||||
queued: '等待', running: '解析中', downloading: '下载中',
|
||||
queued: '等待', running: '准备中', parsing: '解析中', downloading: '下载中',
|
||||
done: '完成', failed: '失败', partial: '部分失败',
|
||||
};
|
||||
|
||||
@@ -45,8 +45,25 @@ function renderSelState() {
|
||||
|
||||
function rowHtml(t) {
|
||||
const total = t.totalPages || 0;
|
||||
const pct = total ? Math.round((t.done / total) * 100) : (t.status === 'done' ? 100 : 0);
|
||||
const badgeCls = 'b-' + (t.status || 'queued');
|
||||
|
||||
// 解析阶段:显示「已解析 N/M 张」,而不是 0/?(否则看着像卡死)
|
||||
const parsing = t.status === 'parsing';
|
||||
let progCell;
|
||||
if (parsing) {
|
||||
const got = Number((t.pages || {})['_parsing'] || 0);
|
||||
const pct = total ? Math.round((got / total) * 100) : 0;
|
||||
progCell = `<div class="rowprog">
|
||||
<div class="rb"><i style="width:${pct}%"></i></div>
|
||||
<span class="rn">已解析 ${got}${total ? '/' + total : ''} 张</span>
|
||||
</div>`;
|
||||
} else {
|
||||
const pct = total ? Math.round((t.done / total) * 100) : (t.status === 'done' ? 100 : 0);
|
||||
progCell = `<div class="rowprog">
|
||||
<div class="rb"><i style="width:${pct}%"></i></div>
|
||||
<span class="rn">${t.done}/${total || '?'}${t.failed ? ` (✗${t.failed})` : ''}</span>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
const title = t.title || t.raw || '(未解析)';
|
||||
const err = t.error
|
||||
? `<span class="e">${esc(t.error)}</span>`
|
||||
@@ -63,13 +80,8 @@ function rowHtml(t) {
|
||||
<td class="c-sel"><input type="checkbox" class="sel" data-gid="${t.gid}" ${t.selected ? 'checked' : ''} /></td>
|
||||
<td class="c-gid">${esc(t.gid)}</td>
|
||||
<td><div class="title-cell"><span class="t">${esc(title)}</span>${err}</div></td>
|
||||
<td class="c-prog">
|
||||
<div class="rowprog">
|
||||
<div class="rb"><i style="width:${pct}%"></i></div>
|
||||
<span class="rn">${t.done}/${total || '?'}${t.failed ? ` (✗${t.failed})` : ''}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="c-stat"><span class="badge ${badgeCls}">${STATUS_TEXT[t.status] || t.status}</span></td>
|
||||
<td class="c-prog">${progCell}</td>
|
||||
<td class="c-stat"><span class="badge ${'b-' + (t.status || 'queued')}">${STATUS_TEXT[t.status] || t.status}</span></td>
|
||||
<td class="c-act">${acts.join(' ')}</td>
|
||||
</tr>`;
|
||||
}
|
||||
@@ -79,11 +91,14 @@ function renderStats() {
|
||||
let dn = 0, tt = 0;
|
||||
for (const t of tasks) {
|
||||
if (t.status === 'queued') c.queued++;
|
||||
else if (t.status === 'running' || t.status === 'downloading') c.running++;
|
||||
else if (t.status === 'running' || t.status === 'parsing' || t.status === 'downloading') c.running++;
|
||||
else if (t.status === 'done') c.done++;
|
||||
else if (t.status === 'failed' || t.status === 'partial') c.failed++;
|
||||
dn += t.done || 0;
|
||||
tt += t.totalPages || 0;
|
||||
// 解析阶段的任务还没进入下载,done 不计入总进度
|
||||
if (t.status !== 'parsing') {
|
||||
dn += t.done || 0;
|
||||
tt += t.totalPages || 0;
|
||||
}
|
||||
}
|
||||
$('s-total').textContent = tasks.length;
|
||||
$('s-queued').textContent = c.queued;
|
||||
|
||||
+24
-2
@@ -273,6 +273,22 @@ type existingGallery struct {
|
||||
manifest *Manifest
|
||||
}
|
||||
|
||||
// reportParseProgress 解析阶段进度反馈。
|
||||
//
|
||||
// 解析大画廊要翻上百屏(每屏 20 张),全程可能好几分钟。
|
||||
// 界面必须看得到在动,否则「解析中 0/?」会被当成卡死。
|
||||
func (e *Engine) reportParseProgress(task Task, done, total, page int) {
|
||||
task.Status = "parsing"
|
||||
task.TotalPages = total
|
||||
task.Done = 0
|
||||
// 解析期用「已解析张数」当进度,下载阶段才是真正的完成数
|
||||
task.Pages = map[string]string{}
|
||||
if done > 0 {
|
||||
task.Pages["_parsing"] = strconv.Itoa(done)
|
||||
}
|
||||
e.save(task)
|
||||
}
|
||||
|
||||
// findExistingGallery 在下载根目录里找这个 gid 已存在的画廊。
|
||||
//
|
||||
// 两种匹配方式:
|
||||
@@ -366,7 +382,12 @@ func (e *Engine) runTask(ctx context.Context, task Task) {
|
||||
|
||||
// 没有可用清单(或清单太旧)→ 正常解析画廊页
|
||||
if gallery == nil {
|
||||
g, err := e.client.FetchGalleryPage(ctx, task.GID, task.Token)
|
||||
g, err := e.client.FetchGalleryPage(ctx, task.GID, task.Token,
|
||||
func(done, total, page int) {
|
||||
// 解析阶段也要有反馈,否则大画廊(上千张 = 上百屏)
|
||||
// 会长时间显示「解析中 0/?」,看着像卡死。
|
||||
e.reportParseProgress(task, done, total, page)
|
||||
})
|
||||
if err != nil {
|
||||
if ctx.Err() != nil {
|
||||
e.markCanceled(task)
|
||||
@@ -464,7 +485,8 @@ func (e *Engine) runTask(ctx context.Context, task Task) {
|
||||
e.log("warn", fmt.Sprintf(
|
||||
"画廊 %s:本地清单的页面路径似乎已失效,重新解析画廊页", task.GID))
|
||||
|
||||
if fresh, err := e.client.FetchGalleryPage(ctx, task.GID, task.Token); err == nil {
|
||||
if fresh, err := e.client.FetchGalleryPage(ctx, task.GID, task.Token,
|
||||
func(done, total, page int) { e.reportParseProgress(task, done, total, page) }); err == nil {
|
||||
// 用新解析结果重跑:重置页状态
|
||||
st.reset(fresh)
|
||||
st.scanExisting()
|
||||
|
||||
@@ -110,7 +110,7 @@ func TestFetchGalleryPagePagination(t *testing.T) {
|
||||
defer func() { EHBase = oldBase }()
|
||||
|
||||
c := NewClient(5*time.Second, "", "off", "auto")
|
||||
g, err := c.FetchGalleryPage(context.Background(), gid, tok)
|
||||
g, err := c.FetchGalleryPage(context.Background(), gid, tok, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("FetchGalleryPage 失败: %v", err)
|
||||
}
|
||||
@@ -283,7 +283,7 @@ func TestFetchGalleryRestricted(t *testing.T) {
|
||||
defer func() { EHBase = oldBase }()
|
||||
|
||||
c := NewClient(5*time.Second, "", "off", "auto")
|
||||
_, err := c.FetchGalleryPage(context.Background(), "12345678", "abc")
|
||||
_, err := c.FetchGalleryPage(context.Background(), "12345678", "abc", nil)
|
||||
if err == nil {
|
||||
t.Fatal("受限画廊应该报错")
|
||||
}
|
||||
|
||||
+65
-8
@@ -278,7 +278,8 @@ type Client struct {
|
||||
proxyMode string // off | http | socks5
|
||||
xres string // auto | org | 数字(长边上限)
|
||||
|
||||
limiter *Limiter
|
||||
transport *http.Transport // 全局共用,只有代理配置变了才重建
|
||||
limiter *Limiter
|
||||
}
|
||||
|
||||
// NewClient 建客户端
|
||||
@@ -319,6 +320,7 @@ func (c *Client) SetProxy(p, mode string) {
|
||||
c.proxyMode = mode
|
||||
}
|
||||
c.mu.Unlock()
|
||||
c.rebuildTransport() // 代理变了,Transport 必须重建
|
||||
}
|
||||
|
||||
// SetXres 改分辨率策略
|
||||
@@ -341,10 +343,32 @@ func (c *Client) snapshot() (string, string, time.Duration) {
|
||||
return c.proxy, c.proxyMode, c.timeout
|
||||
}
|
||||
|
||||
// httpClient 按当前代理配置构造 http.Client。
|
||||
// 每次调用新建(配置变更即时生效,且下载与页面请求的超时不同)。
|
||||
// httpClient 取可用的 http.Client。
|
||||
//
|
||||
// ⚠️ Transport 必须复用:如果每次请求都新建 Transport,旧的那个会被丢弃,
|
||||
// 但它持有的 keep-alive 连接不会立刻释放(要等 IdleConnTimeout),
|
||||
// 于是每请求一条僵尸连接堆积起来 —— 实测能到 91 条,
|
||||
// 既吃代理的连接配额,又让新请求变慢。
|
||||
// 所以这里只在「代理配置变化」时才重建 Transport,平时全局共用一个。
|
||||
func (c *Client) httpClient(timeout time.Duration) *http.Client {
|
||||
proxyURL, mode, defTimeout := c.snapshot()
|
||||
c.mu.RLock()
|
||||
tr := c.transport
|
||||
defTimeout := c.timeout
|
||||
c.mu.RUnlock()
|
||||
|
||||
if tr == nil {
|
||||
tr = c.rebuildTransport()
|
||||
}
|
||||
|
||||
if timeout <= 0 {
|
||||
timeout = defTimeout
|
||||
}
|
||||
return &http.Client{Transport: tr, Timeout: timeout}
|
||||
}
|
||||
|
||||
// rebuildTransport 按当前代理配置重建 Transport,并关掉旧连接的空闲池
|
||||
func (c *Client) rebuildTransport() *http.Transport {
|
||||
proxyURL, mode, _ := c.snapshot()
|
||||
|
||||
tr := &http.Transport{
|
||||
MaxIdleConns: 64,
|
||||
@@ -374,10 +398,26 @@ func (c *Client) httpClient(timeout time.Duration) *http.Client {
|
||||
}
|
||||
}
|
||||
|
||||
if timeout <= 0 {
|
||||
timeout = defTimeout
|
||||
c.mu.Lock()
|
||||
old := c.transport
|
||||
c.transport = tr
|
||||
c.mu.Unlock()
|
||||
|
||||
if old != nil {
|
||||
old.CloseIdleConnections()
|
||||
}
|
||||
return tr
|
||||
}
|
||||
|
||||
// Close 关闭空闲连接(退出时调用)
|
||||
func (c *Client) Close() {
|
||||
c.mu.Lock()
|
||||
tr := c.transport
|
||||
c.transport = nil
|
||||
c.mu.Unlock()
|
||||
if tr != nil {
|
||||
tr.CloseIdleConnections()
|
||||
}
|
||||
return &http.Client{Transport: tr, Timeout: timeout}
|
||||
}
|
||||
|
||||
func (c *Client) get(ctx context.Context, u string, timeout time.Duration) (*http.Response, error) {
|
||||
@@ -419,9 +459,19 @@ func (c *Client) getText(ctx context.Context, u string) (string, error) {
|
||||
|
||||
// ---------------------------------------------------------------- 1) 画廊页
|
||||
|
||||
// ParseProgress 解析进度回调
|
||||
//
|
||||
// 大画廊要翻很多页(2000 张 = 100 页),没有进度反馈的话界面会一直显示
|
||||
// 「解析中 0/?」,看着像卡死了。
|
||||
//
|
||||
// done = 已收集到的图片数
|
||||
// total = 站点声明的总图数(第一页才知道,可能为 0)
|
||||
// page = 当前翻到第几屏(每屏 20 张)
|
||||
type ParseProgress func(done, total, page int)
|
||||
|
||||
// FetchGalleryPage 取画廊页。注意:EH 每屏只列 20 张缩略图,
|
||||
// 必须翻 ?p=N 逐屏收集,否则静默只下到前 20 张且不报错。
|
||||
func (c *Client) FetchGalleryPage(ctx context.Context, gid, token string) (*Gallery, error) {
|
||||
func (c *Client) FetchGalleryPage(ctx context.Context, gid, token string, onProgress ParseProgress) (*Gallery, error) {
|
||||
var (
|
||||
pages []GalleryPageInfo
|
||||
seen = map[int]bool{}
|
||||
@@ -486,6 +536,13 @@ func (c *Client) FetchGalleryPage(ctx context.Context, gid, token string) (*Gall
|
||||
if added == 0 {
|
||||
break // 这屏没新东西 -> 到头了
|
||||
}
|
||||
|
||||
// 报告进度:大画廊翻几十上百屏时,界面上要看得见在动
|
||||
// (放在 break 之前,保证最后一屏也会报告)
|
||||
if onProgress != nil {
|
||||
onProgress(len(pages), total, p+1)
|
||||
}
|
||||
|
||||
if total > 0 && len(pages) >= total {
|
||||
break // 收齐
|
||||
}
|
||||
|
||||
@@ -0,0 +1,248 @@
|
||||
package eh
|
||||
|
||||
// 连接复用测试
|
||||
//
|
||||
// 曾经的 bug:httpClient() 每次请求都新建 http.Transport,用完丢弃。
|
||||
// Transport 被丢弃后,它持有的 keep-alive 连接不会立刻释放(要等
|
||||
// IdleConnTimeout = 90s),于是每发一个请求就留下一条僵尸连接。
|
||||
//
|
||||
// 实测踩坑现场:解析大画廊时 91 条 Established 连接,而解析是串行的,
|
||||
// 正常应该只有 1 条。连接堆积会吃代理的连接配额,也让新请求变慢。
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// buildGalleryHTML 造一页画廊 HTML
|
||||
func buildGalleryHTML(gid string, from, to, total, screen int) string {
|
||||
var sb strings.Builder
|
||||
if screen == 0 {
|
||||
sb.WriteString(`<!DOCTYPE html><html><head><title>[Test] - E-Hentai Galleries</title>`)
|
||||
sb.WriteString(`<meta name="description" content="Free Hentai Doujinshi Gallery: [Test]: x" />`)
|
||||
sb.WriteString(`</head><body><div id="taglist"><table></table></div>`)
|
||||
}
|
||||
for i := from; i <= to; i++ {
|
||||
sb.WriteString(fmt.Sprintf(
|
||||
`<a href="/s/k%03d/%s-%d"><div title="Page %d: %03d.png"></div></a>`,
|
||||
i, gid, i, i, i))
|
||||
}
|
||||
sb.WriteString(fmt.Sprintf(`Showing %d - %d of %d images`, from, to, total))
|
||||
if screen == 0 {
|
||||
sb.WriteString(`</body></html>`)
|
||||
}
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// 统计服务端实际接受的连接数
|
||||
func TestTransportIsReused(t *testing.T) {
|
||||
var conns int64
|
||||
|
||||
srv := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Write([]byte("ok"))
|
||||
}))
|
||||
srv.Config.ConnState = func(c net.Conn, state http.ConnState) {
|
||||
if state == http.StateNew {
|
||||
atomic.AddInt64(&conns, 1)
|
||||
}
|
||||
}
|
||||
srv.Start()
|
||||
defer srv.Close()
|
||||
|
||||
c := NewClient(10*time.Second, "", "off", "auto")
|
||||
|
||||
const n = 20
|
||||
for i := 0; i < n; i++ {
|
||||
if _, err := c.getText(context.Background(), srv.URL+"/x"); err != nil {
|
||||
t.Fatalf("第 %d 次请求失败: %v", i+1, err)
|
||||
}
|
||||
}
|
||||
|
||||
got := atomic.LoadInt64(&conns)
|
||||
// 连接复用后,20 次请求应该只用 1 条连接(串行)。
|
||||
// 修复前是每请求一条 = 20 条。
|
||||
if got > 3 {
|
||||
t.Errorf("%d 次请求开了 %d 条连接 —— Transport 没复用,连接在泄漏", n, got)
|
||||
}
|
||||
t.Logf("%d 次请求共用了 %d 条连接", n, got)
|
||||
}
|
||||
|
||||
// 并发请求也应该复用连接,而不是每个 goroutine 一条
|
||||
func TestTransportReusedUnderConcurrency(t *testing.T) {
|
||||
var conns int64
|
||||
|
||||
srv := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Write([]byte("ok"))
|
||||
}))
|
||||
srv.Config.ConnState = func(c net.Conn, state http.ConnState) {
|
||||
if state == http.StateNew {
|
||||
atomic.AddInt64(&conns, 1)
|
||||
}
|
||||
}
|
||||
srv.Start()
|
||||
defer srv.Close()
|
||||
|
||||
c := NewClient(10*time.Second, "", "off", "auto")
|
||||
|
||||
var wg sync.WaitGroup
|
||||
for i := 0; i < 20; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for j := 0; j < 5; j++ {
|
||||
c.getText(context.Background(), srv.URL+"/x")
|
||||
}
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
got := atomic.LoadInt64(&conns)
|
||||
// 100 次并发请求,连接数应该远小于 100
|
||||
if got > 60 {
|
||||
t.Errorf("100 次并发请求开了 %d 条连接,复用不充分", got)
|
||||
}
|
||||
t.Logf("100 次并发请求共用了 %d 条连接", got)
|
||||
}
|
||||
|
||||
// Close() 要真的能关掉空闲连接
|
||||
func TestClientCloseReleasesConnections(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Write([]byte("ok"))
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
c := NewClient(10*time.Second, "", "off", "auto")
|
||||
for i := 0; i < 5; i++ {
|
||||
c.getText(context.Background(), srv.URL+"/x")
|
||||
}
|
||||
|
||||
c.mu.RLock()
|
||||
tr := c.transport
|
||||
c.mu.RUnlock()
|
||||
if tr == nil {
|
||||
t.Fatal("Transport 应该是复用的,不该为 nil")
|
||||
}
|
||||
|
||||
c.Close()
|
||||
|
||||
c.mu.RLock()
|
||||
still := c.transport
|
||||
c.mu.RUnlock()
|
||||
if still != nil {
|
||||
t.Error("Close() 后 transport 应被清空")
|
||||
}
|
||||
}
|
||||
|
||||
// 改代理配置后 Transport 要重建(否则还是走老代理)
|
||||
func TestSetProxyRebuildsTransport(t *testing.T) {
|
||||
c := NewClient(5*time.Second, "", "off", "auto")
|
||||
|
||||
// 先建一个
|
||||
c.getText(context.Background(), "http://127.0.0.1:1/never") // 必然失败,只为触发建 Transport
|
||||
c.mu.RLock()
|
||||
first := c.transport
|
||||
c.mu.RUnlock()
|
||||
if first == nil {
|
||||
t.Fatal("应该已经建好 Transport")
|
||||
}
|
||||
|
||||
// 改代理
|
||||
c.SetProxy("http://127.0.0.1:7890", "http")
|
||||
|
||||
c.mu.RLock()
|
||||
second := c.transport
|
||||
c.mu.RUnlock()
|
||||
if second == first {
|
||||
t.Error("改代理后 Transport 必须重建,否则还走旧配置")
|
||||
}
|
||||
if second == nil {
|
||||
t.Error("重建后不该是 nil")
|
||||
}
|
||||
}
|
||||
|
||||
// 解析进度回调要真的被调用
|
||||
func TestParseProgressCallback(t *testing.T) {
|
||||
const gid, tok, total = "4239999", "abcdef1234", 45
|
||||
|
||||
pages := map[int]string{}
|
||||
for p := 0; p < 3; p++ {
|
||||
from, to := p*20+1, p*20+20
|
||||
if to > total {
|
||||
to = total
|
||||
}
|
||||
page := p
|
||||
pages[page] = buildGalleryHTML(gid, from, to, total, page)
|
||||
}
|
||||
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/g/"+gid+"/"+tok+"/", func(w http.ResponseWriter, r *http.Request) {
|
||||
p := 0
|
||||
if q := r.URL.Query().Get("p"); q != "" {
|
||||
fmt.Sscanf(q, "%d", &p)
|
||||
}
|
||||
h, ok := pages[p]
|
||||
if !ok {
|
||||
h = buildGalleryHTML(gid, 0, 0, total, 9)
|
||||
}
|
||||
w.Write([]byte(h))
|
||||
})
|
||||
srv := httptest.NewServer(mux)
|
||||
defer srv.Close()
|
||||
|
||||
old := EHBase
|
||||
EHBase = srv.URL
|
||||
defer func() { EHBase = old }()
|
||||
|
||||
c := NewClient(5*time.Second, "", "off", "auto")
|
||||
|
||||
var got []int
|
||||
var maxDone int
|
||||
g, err := c.FetchGalleryPage(context.Background(), gid, tok, func(done, tot, page int) {
|
||||
got = append(got, done)
|
||||
if done > maxDone {
|
||||
maxDone = done
|
||||
}
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("解析失败: %v", err)
|
||||
}
|
||||
if len(got) < 3 {
|
||||
t.Errorf("应该至少回调 3 次(翻 3 屏),实际 %d 次", len(got))
|
||||
}
|
||||
if maxDone != total {
|
||||
t.Errorf("最后一次进度应达到 %d,实际 %d", total, maxDone)
|
||||
}
|
||||
if g.TotalPages != total {
|
||||
t.Errorf("总页数不对: %d", g.TotalPages)
|
||||
}
|
||||
t.Logf("进度回调 %d 次: %v", len(got), got)
|
||||
}
|
||||
|
||||
// 传 nil 回调也不该 panic
|
||||
func TestParseProgressNil(t *testing.T) {
|
||||
const gid, tok, total = "4239998", "abcdef1234", 20
|
||||
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/g/"+gid+"/"+tok+"/", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Write([]byte(buildGalleryHTML(gid, 1, 20, total, 0)))
|
||||
})
|
||||
srv := httptest.NewServer(mux)
|
||||
defer srv.Close()
|
||||
|
||||
old := EHBase
|
||||
EHBase = srv.URL
|
||||
defer func() { EHBase = old }()
|
||||
|
||||
c := NewClient(5*time.Second, "", "off", "auto")
|
||||
if _, err := c.FetchGalleryPage(context.Background(), gid, tok, nil); err != nil {
|
||||
t.Errorf("nil 回调不该出错: %v", err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user