mirror of
https://github.com/tiennm99/telegram-exporter.git
synced 2026-10-11 03:13:49 +00:00
A run reported one aggregate line, which could say how much was done but never what was happening: which files were moving, whether a stall was a slow download or a slow upload, or how long the rest would take. On a transfer measured in hours those are the only questions worth answering. The pipeline now emits a per-item lifecycle rather than only Stats, and a terminal renders it as an overall bar plus one bar per file in flight. Uploads get a spinner rather than a bar because rclone's MoveFile is a single blocking call with no byte callbacks; naming the file is still the point, since a run that looks stalled is usually waiting on one large object. Before the transfer, the run states what it found and what it will do. The survey breaks the outstanding set down by reason — never fetched, zero-byte, wrong size, unarchivable — because that is the difference between a run that will converge and one that cannot, and the old "N to fetch" hid it. The plan states the cap, staging path and concurrency, so a wrong setting is visible before hours of transfer rather than after. Redirected output keeps plain lines and gains one per archived file. Bars are continuous cursor movement, and a captured log of them is what tdl's progress bar did to the shell pipeline's logs. cmd/uidemo renders the whole thing against fake data. It is how the layout was checked without a session, and it earned its place immediately: the overall bar sat at zero because Stats only tracked the file count and never advanced it.
96 lines
3.2 KiB
Go
96 lines
3.2 KiB
Go
package report
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/iyear/tdl/core/tmedia"
|
|
|
|
"github.com/tiennm99dev/telegram-exporter/internal/pipeline"
|
|
"github.com/tiennm99dev/telegram-exporter/internal/tgsource"
|
|
"github.com/tiennm99dev/telegram-exporter/internal/verify"
|
|
)
|
|
|
|
func TestSurveyBreaksOutdWhyFilesAreOutstanding(t *testing.T) {
|
|
r := verify.Report{Expected: 12000, Present: 11400, Bytes: 518 << 30}
|
|
r.Absent = append(r.Absent, 1, 2, 3)
|
|
r.ZeroByte = append(r.ZeroByte, 4)
|
|
r.Mismatched = append(r.Mismatched, verify.Mismatch{MessageID: 5})
|
|
r.Unsafe = append(r.Unsafe, verify.Unsafe{MessageID: 3})
|
|
|
|
var sb strings.Builder
|
|
Survey(&sb, r)
|
|
out := sb.String()
|
|
|
|
for _, want := range []string{"12,000 media", "11,400", "zero-byte", "wrong size", "unarchivable"} {
|
|
if !strings.Contains(out, want) {
|
|
t.Errorf("survey missing %q:\n%s", want, out)
|
|
}
|
|
}
|
|
// Unsafe ids are inside Absent, so counting both would double-count them and
|
|
// the rows would not add up to the outstanding total.
|
|
if !strings.Contains(out, "never fetched 2") {
|
|
t.Errorf("never-fetched should exclude the unarchivable id:\n%s", out)
|
|
}
|
|
}
|
|
|
|
func TestSurveyOmitsEmptyRows(t *testing.T) {
|
|
var sb strings.Builder
|
|
Survey(&sb, verify.Report{Expected: 10, Present: 10})
|
|
if strings.Contains(sb.String(), "zero-byte") {
|
|
t.Errorf("a clean archive should list no failure rows:\n%s", sb.String())
|
|
}
|
|
}
|
|
|
|
func TestPlanStatesTheCap(t *testing.T) {
|
|
var sb strings.Builder
|
|
Plan(&sb, PlanInfo{Files: 2613, Bytes: 79 << 30, Largest: 2 << 30,
|
|
Staging: "./staging", Threads: 4, Downloads: 2, Uploads: 2, Destination: "remote:x"})
|
|
if !strings.Contains(sb.String(), "uncapped") {
|
|
t.Errorf("a run with no budget must say so:\n%s", sb.String())
|
|
}
|
|
|
|
sb.Reset()
|
|
Plan(&sb, PlanInfo{Files: 1, Budget: 40 << 30, Staging: "./staging", Destination: "remote:x"})
|
|
if !strings.Contains(sb.String(), "40.0 GiB") {
|
|
t.Errorf("plan should state the cap:\n%s", sb.String())
|
|
}
|
|
}
|
|
|
|
// The redirected path must stay free of cursor movement: bars in a captured log
|
|
// are megabytes of control characters, which is what tdl's progress bar did to
|
|
// the shell pipeline's logs.
|
|
func TestReporterWritesNoAnsi(t *testing.T) {
|
|
var sb strings.Builder
|
|
r := newReporter(&sb, 3, 300)
|
|
it := tgsource.Item{MessageID: 1, Name: "a.mp4", Media: &tmedia.Media{Size: 100}}
|
|
|
|
r.DownloadStart(it)
|
|
for i := range 500 {
|
|
r.DownloadBytes(it, int64(i))
|
|
r.Stats(pipeline.Stats{Done: 1, BytesDone: int64(i)})
|
|
}
|
|
r.DownloadDone(it, nil)
|
|
r.UploadStart(it)
|
|
r.UploadDone(it, nil)
|
|
r.Finish(pipeline.Stats{Done: 1, BytesDone: 100})
|
|
|
|
out := sb.String()
|
|
if strings.ContainsAny(out, "\r\033") {
|
|
t.Errorf("redirected output contains control characters: %q", out)
|
|
}
|
|
// A completed file is still recorded, so a captured run says what moved.
|
|
if !strings.Contains(out, "archived") || !strings.Contains(out, "a.mp4") {
|
|
t.Errorf("redirected output should record each archived file:\n%s", out)
|
|
}
|
|
}
|
|
|
|
// Events picks the renderer from the writer: a strings.Builder is not a
|
|
// terminal, so it must never get bars.
|
|
func TestEventsChoosesTheLineRendererOffTerminal(t *testing.T) {
|
|
var sb strings.Builder
|
|
if _, ok := Events(&sb, 1, 1).(*Reporter); !ok {
|
|
t.Error("a non-terminal writer got the bar renderer")
|
|
}
|
|
}
|