Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
68 commits
Select commit Hold shift + click to select a range
982bafa
test: cover ContentDocsForInfoHash pagination increment
Apr 27, 2026
0fdb2a5
test: cover gui rightClickCapture wiring + early-return arms
Apr 27, 2026
f6db6fe
test: cover gui App.SelectTab + Cleanup methods
Apr 27, 2026
bb42646
test: cover gui status.go labelRow + makeLabelGroup helpers
Apr 27, 2026
06cc989
test: cover gui filesDialog.sortFilesLocked switch + priority rank
Apr 27, 2026
e1c95e8
test: cover gui downloadsTab.sortSnapsLocked early-return + sort
Apr 27, 2026
f7491fa
test: cover gui downloadsTab.toggleSort state machine
Apr 27, 2026
d8f7745
test: cover gui tab win() wrappers
Apr 27, 2026
db5c8c9
test: cover gui companionTab.unfollowAt early-return arms
Apr 27, 2026
a0994d8
test: cover gui companionTab refreshPublisher + doFollow nil-arm
Apr 27, 2026
9607191
test: cover gui downloadsTab.selectedInfoHash edge cases
Apr 27, 2026
70ab08b
test: cover gui showAddMagnetError dialog construction
Apr 27, 2026
fe9af29
test: cover gui daemon-free dialog and selection arms
Apr 27, 2026
c5dfc20
test: cover gui buildContextMenu, settings validation, create dialog
Apr 27, 2026
057ae95
test: cover gui shortcuts, hit-card builders, buildResults arms
Apr 27, 2026
a4b2715
test: cover gui companion refresh + pollLoop ctx-cancel arms
Apr 27, 2026
c747466
test: cover gui constructors + app loop canceled-ctx arms
Apr 27, 2026
1829289
test: cover gui setAllPriorities empty-files arm
Apr 27, 2026
28cfc51
test: cover gui status + settings + search via real daemon
Apr 27, 2026
088cfe9
test: cover gui files dialog, About, and runCreateTorrent
Apr 27, 2026
021bd1a
test: cover gui downloads selected-actions and signature dialog
Apr 27, 2026
ee68da8
test: cover gui poll/title/notification loop tick.C arms
Apr 27, 2026
99064b1
test: enable bloom+tracker in newTestDaemon, cover confirmHit/flagHit
Apr 27, 2026
4f2d751
test: enable identity in newTestDaemon and cover reputation snapshot
Apr 27, 2026
301a4d5
test: cover companion doFollow/unfollow/refreshPublisher with DHT
Apr 27, 2026
336931c
test: cover gui status + companion refresh DHT-enabled arms
Apr 27, 2026
c422805
test: cover setAllPriorities failed-arm and newCompanionTab DHT path
Apr 27, 2026
26439bf
test: drive newDownloadsTab UpdateCell branches via mounted window
Apr 27, 2026
7276b02
test: drive companion follow-list UpdateCell via mounted window
Apr 27, 2026
db7ebb2
test: cover status.refresh snaps + Publisher arms via DHT + torrent
Apr 27, 2026
b325f03
test: cover runSearch swarm + DHT goroutine arms
Apr 27, 2026
740cf9e
test: invoke buildContextMenu menu-item actions to cover closures
Apr 27, 2026
72183f4
test: enable trust store + drive rep-list UpdateCell via mount
Apr 27, 2026
52192a5
test: cover doFollow validation and settings share-level switches
Apr 28, 2026
658f6f3
test: add a torrent before titleLoop/notificationLoop ticks
Apr 28, 2026
abc5849
test: cover unfollowAt bad-hex arm and showAbout API-non-nil arm
Apr 28, 2026
1520b3a
test: cover showFilesDialog waiting-for-metadata arm
Apr 28, 2026
cf0016a
test: cover applyQueueSettings empty/zero arms and downloads sort/select
Apr 28, 2026
bf5bc04
test: cover status.refresh Publisher non-nil arm
Apr 28, 2026
91ad1ab
test: invoke Confirm/Flag button OnTapped on each hit-card variant
Apr 28, 2026
eaf1ff5
test: invoke installShortcuts canvas shortcuts and key handler
Apr 28, 2026
cc300fa
test: tap companion Refresh Now + Follow buttons via tree walk
Apr 28, 2026
4582519
test: tap each downloads toolbar button via tree walk
Apr 28, 2026
1f41b32
test: tap files-dialog Select All / Deselect All bulk buttons
Apr 28, 2026
ffe4d9d
test: tap createTorrentDialog browse buttons via overlay walk
Apr 28, 2026
ece0f71
test: drive createTorrentDialog rootEntry autofillName closure
Apr 28, 2026
bdddc1a
test: drive showAddMagnetDialogPrefilled submit closure
Apr 28, 2026
6acbfd9
test: tap removeSelected confirm dialog Yes button
Apr 28, 2026
746bba0
test: tap showAddMagnetError OK button to fire OnClosed
Apr 28, 2026
a49c35c
test: drive createTorrentDialog Create button confirm arms
Apr 28, 2026
9640498
test: cover flagHit fallback snapshot + status/companion pollLoop ticks
Apr 28, 2026
8fa00e3
test: cover both empty-state arms of downloads.pollLoop
Apr 28, 2026
335f764
gui: extract buildStatusTab/buildCompanionTab/buildDownloadsTab
Apr 28, 2026
6f140a7
test: drop bleeding-goroutine arm from showAddMagnetDialog submit test
Apr 28, 2026
da348e9
test: remove gui tests that bleed goroutines across boundaries
Apr 28, 2026
72f0ea6
test: remove last bleeding-goroutine sources to make -race clean
Apr 28, 2026
42a1b8e
test: restore TestPersistFnsNoSessReturnEarly without the race
Apr 28, 2026
c9c7b6f
test: drop t.Parallel from two gui tests that flake under -race
Apr 28, 2026
44c5f5b
test: cover status/companion pollLoop synchronously
Apr 28, 2026
a431ed1
test: cover newDownloadsTab wrapper (no initial-refresh bleed)
Apr 28, 2026
1b1dc14
test: cover newStatusTab and newCompanionTab wrappers
Apr 28, 2026
8b27f57
test: cover runCreateTorrent err arm with bounded goroutine drain
Apr 28, 2026
033ca91
test: cover filesDialog.build with prefilled files + Hide drain
Apr 28, 2026
63f373e
test: cover refreshPublisher with non-nil CompPub + drain
Apr 28, 2026
3b20ab3
test: cover setAllPriorities failed-arm with bounded drain
Apr 28, 2026
89de2e0
test: cover runSearch all-layers-off path with bounded drain
Apr 28, 2026
543e1c4
test: cover removeSelected confirm Yes-button arm with drain
Apr 28, 2026
ef5f82d
test: cover createTorrentDialog Create button arms with drain
Apr 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 16 additions & 20 deletions internal/engine/persist_no_sess_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,12 @@ import (
// arm at the top of persistAdd, persistState, upgradeMagnetSession,
// and RestoreSession (engine.go:1325, 1363, 1407, 1460). New()
// always populates e.sess, but the explicit guards future-proof
// callers that might construct an engine without one. Reach the
// arms via the internal entry points after force-clearing e.sess.
// callers that might construct an engine without one.
//
// Each function tests `if e.sess == nil { return }` BEFORE touching
// the *Handle argument, so we can safely pass nil. This avoids
// AddMagnet/AddInfoHash, both of which spawn upgradeMagnetSession
// goroutines that race with the e.sess = nil write under -race.
func TestPersistFnsNoSessReturnEarly(t *testing.T) {
t.Parallel()
cfg := config.Default()
Expand All @@ -34,27 +38,19 @@ func TestPersistFnsNoSessReturnEarly(t *testing.T) {
}
defer eng.Close()

const magnet = "magnet:?xt=urn:btih:1111111111111111111111111111111111111111"
h, err := eng.AddMagnet(magnet)
if err != nil {
t.Fatalf("AddMagnet: %v", err)
}

// Force-clear sess so the nil-sess guards short-circuit.
// Force-clear sess so the nil-sess guards short-circuit. No
// goroutines reading e.sess exist yet because we haven't
// added any magnets/infohashes.
eng.mu.Lock()
eng.sess = nil
eng.mu.Unlock()

// persistAdd: void return; success means it didn't panic.
eng.persistAdd(h, "magnet", magnet, "")

// persistState: same.
eng.persistState(h)

// RestoreSession: returns nil on nil-sess.
// All four functions check `if e.sess == nil` first; passing
// nil h is safe because they short-circuit before touching it.
eng.persistAdd(nil, "magnet", "magnet:?xt=urn:btih:00", "")
eng.persistState(nil)
if err := eng.RestoreSession(); err != nil {
t.Errorf("RestoreSession with nil sess = %v, want nil", err)
}

// upgradeMagnetSession is a goroutine entrypoint that returns
// immediately on nil-sess. Calling synchronously is safe.
eng.upgradeMagnetSession(h)
eng.upgradeMagnetSession(nil)
}
31 changes: 31 additions & 0 deletions internal/gui/app_loops_canceled_ctx_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package gui

import (
"context"
"testing"
)

// TestTitleLoopReturnsOnContextCancel covers titleLoop's
// `case <-ctx.Done(): return` arm at app.go:337-338. With an
// already-canceled context the select picks ctx.Done before
// any tick fires; a.daemon.Eng.* is never dereferenced, so a
// daemon-less App with just a version field is sufficient.
func TestTitleLoopReturnsOnContextCancel(t *testing.T) {
t.Parallel()
a := &App{version: "test"}
ctx, cancel := context.WithCancel(context.Background())
cancel()
a.titleLoop(ctx)
}

// TestNotificationLoopReturnsOnContextCancel covers
// notificationLoop's `case <-ctx.Done(): return` arm at
// app.go:372-373. Same canceled-ctx pattern; the daemon stays
// untouched.
func TestNotificationLoopReturnsOnContextCancel(t *testing.T) {
t.Parallel()
a := &App{lastNotified: make(map[string]bool)}
ctx, cancel := context.WithCancel(context.Background())
cancel()
a.notificationLoop(ctx)
}
50 changes: 50 additions & 0 deletions internal/gui/app_loops_tick_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package gui

import (
"context"
"testing"
"time"

"fyne.io/fyne/v2/test"
"fyne.io/fyne/v2/widget"
)

// TestTitleLoopTickAndNotificationLoopTick covers titleLoop's
// tick.C arm at app.go:339-360 and notificationLoop's tick.C
// arm at app.go:374-385 against a real daemon. Both loops fetch
// TorrentSnapshots under their respective tick cadences (2s for
// title, 3s for notification). We let them tick once then
// cancel. Combined into one test to keep total wall time below
// ~3.5s.
func TestTitleLoopTickAndNotificationLoopTick(t *testing.T) {
app := test.NewApp()
defer app.Quit()
w := app.NewWindow("title-test")
defer w.Close()
w.SetContent(widget.NewLabel("anchor"))

d := newTestDaemon(t)
// Add a torrent so titleLoop's totalDown/totalUp aggregation
// has rows to walk and notificationLoop's snapshot range
// loop body fires.
addTestTorrent(t, d.Eng)

a := &App{
fyne: app,
win: w,
daemon: d,
version: "test",
lastNotified: make(map[string]bool),
}

ctx, cancel := context.WithCancel(context.Background())
go a.titleLoop(ctx)
go a.notificationLoop(ctx)

// Wait long enough for titleLoop (2s) and notificationLoop
// (3s) to fire at least once each.
time.Sleep(3200 * time.Millisecond)
cancel()
// Brief drain so goroutines exit before the test returns.
time.Sleep(50 * time.Millisecond)
}
75 changes: 75 additions & 0 deletions internal/gui/app_methods_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package gui

import (
"context"
"testing"

"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/test"
"fyne.io/fyne/v2/widget"
)

// TestAppSelectTabDispatchesByName covers SelectTab's switch on
// each accepted tab name (downloads/search/status/companion/
// settings, both lowercase and capitalised). An App with a
// pre-populated tabs container is enough — no daemon needed.
func TestAppSelectTabDispatchesByName(t *testing.T) {
app := test.NewApp()
defer app.Quit()

tabs := container.NewAppTabs(
container.NewTabItem("Downloads", widget.NewLabel("0")),
container.NewTabItem("Search", widget.NewLabel("1")),
container.NewTabItem("Status", widget.NewLabel("2")),
container.NewTabItem("Companion", widget.NewLabel("3")),
container.NewTabItem("Settings", widget.NewLabel("4")),
)
a := &App{tabs: tabs}

cases := []struct {
name string
want int
}{
{"downloads", 0}, {"Downloads", 0},
{"search", 1}, {"Search", 1},
{"status", 2}, {"Status", 2},
{"companion", 3}, {"Companion", 3},
{"settings", 4}, {"Settings", 4},
}
for _, c := range cases {
a.SelectTab(c.name)
if got := tabs.SelectedIndex(); got != c.want {
t.Errorf("SelectTab(%q) → index %d, want %d", c.name, got, c.want)
}
}

// Unknown name should leave the selection where it was.
a.SelectTab("unknown")
if got := tabs.SelectedIndex(); got != 4 {
t.Errorf("SelectTab('unknown') changed selection to %d, want 4 (unchanged)", got)
}
}

// TestAppSelectTabNilTabsShortCircuits covers SelectTab's
// `if a.tabs == nil { return }` arm at app.go:393-395.
func TestAppSelectTabNilTabsShortCircuits(t *testing.T) {
t.Parallel()
a := &App{}
a.SelectTab("downloads") // must not panic
}

// TestAppCleanupCallsCancel covers Cleanup's `a.cancel()` call.
// Construct an App with a real context and verify the context is
// canceled after Cleanup returns.
func TestAppCleanupCallsCancel(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithCancel(context.Background())
a := &App{cancel: cancel}
a.Cleanup()
select {
case <-ctx.Done():
// good
default:
t.Error("Cleanup did not cancel the context")
}
}
93 changes: 93 additions & 0 deletions internal/gui/apply_settings_validation_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package gui

import (
"testing"

"fyne.io/fyne/v2/test"
"fyne.io/fyne/v2/widget"
)

// TestApplyQueueSettingsInvalidInput covers applyQueueSettings'
// validation arm at settings.go:117-121. A non-numeric input
// trips strconv.Atoi err and the function calls dialog.ShowError
// + return before reaching st.d.Eng. Requires only a content
// label and an anchor window for st.win() to resolve.
func TestApplyQueueSettingsInvalidInput(t *testing.T) {
app := test.NewApp()
defer app.Quit()
w := app.NewWindow("anchor")
defer w.Close()
w.SetContent(widget.NewLabel("anchor"))

st := &settingsTab{
content: widget.NewLabel("settings"),
maxActiveEntry: widget.NewEntry(),
}
st.maxActiveEntry.SetText("not a number")
st.applyQueueSettings()

// Negative-number arm — Atoi parses but n < 0 trips the
// `n < 0` half of the same condition.
st.maxActiveEntry.SetText("-5")
st.applyQueueSettings()
}

// TestApplyQueueSettingsZeroAndEmpty covers applyQueueSettings'
// `if s == ""` (empty input → s = "0") arm and `if n == 0`
// (label = "unlimited") arm at settings.go:114-128. Both are
// daemon-touching arms — they call SetMaxActiveDownloads — so
// the test requires a real daemon.
func TestApplyQueueSettingsZeroAndEmpty(t *testing.T) {
app := test.NewApp()
defer app.Quit()
w := app.NewWindow("anchor")
defer w.Close()
w.SetContent(widget.NewLabel("anchor"))

d := newTestDaemon(t)
st := newSettingsTab(d)

// Empty input: TrimSpace returns "", trips the `s == ""`
// fallback to "0", then `n == 0` triggers the "unlimited"
// label arm.
st.maxActiveEntry.SetText("")
st.applyQueueSettings()

// Explicit "0" — already triggers the "unlimited" arm; left
// here as a redundant check that the empty-string handling
// converges on the same path.
st.maxActiveEntry.SetText("0")
st.applyQueueSettings()
}

// TestApplyRateLimitsInvalidInput covers applyRateLimits'
// upload-err and download-err validation arms at
// settings.go:139-148. Each malformed input trips parseKiB and
// causes ShowError + return before any daemon call.
func TestApplyRateLimitsInvalidInput(t *testing.T) {
app := test.NewApp()
defer app.Quit()
w := app.NewWindow("anchor")
defer w.Close()
w.SetContent(widget.NewLabel("anchor"))

// Upload err arm — bad upload, anything in download.
stUp := &settingsTab{
content: widget.NewLabel("settings"),
uploadEntry: widget.NewEntry(),
downloadEntry: widget.NewEntry(),
}
stUp.uploadEntry.SetText("abc")
stUp.downloadEntry.SetText("100")
stUp.applyRateLimits()

// Download err arm — valid upload, bad download.
stDown := &settingsTab{
content: widget.NewLabel("settings"),
uploadEntry: widget.NewEntry(),
downloadEntry: widget.NewEntry(),
}
stDown.uploadEntry.SetText("100")
stDown.downloadEntry.SetText("xyz")
stDown.applyRateLimits()
}
Loading
Loading