This is a personal torrent client, not a web client or daemon, so the rarely-used HTTP-client knobs and redundant tuning flags are gone — flag, config field, implementation, and tests removed for each. The option surface drops from 54 to 28. Web-client cluster removed: --all-proxy (HTTP_PROXY env still works), load/save-cookies, --referer, -U/--user-agent, --http-user/--http-passwd, --conditional-get, --remote-time, --check-certificate, --ca-certificate, --lowest-speed-limit. Pass --header for a one-off auth/cookie/UA header. Redundant knobs removed: -x and -k (folded into -s), --connect-timeout, --retry-wait, per-item --max-download-limit/-u, --bt-max-peers, --bt-stop-timeout, --disable-ipv6, --auto-save-interval, --human-readable, --stop, -T (a positional .torrent works). follow-torrent and human-readable are now unconditional defaults.
231 lines
7.7 KiB
Go
231 lines
7.7 KiB
Go
package cli
|
|
|
|
import (
|
|
"path/filepath"
|
|
"testing"
|
|
)
|
|
|
|
func TestParseArgs(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
args []string
|
|
action Action
|
|
wantVal map[string]string
|
|
uris []string
|
|
}{
|
|
{"short attached", []string{"-m3", "https://e.com"}, Run, map[string]string{"max-tries": "3"}, []string{"https://e.com"}},
|
|
{"long equals + bool", []string{"--split=8", "-c", "u"}, Run, map[string]string{"split": "8", "continue": "true"}, []string{"u"}},
|
|
{"long separate value", []string{"--out", "f.bin", "u"}, Run, map[string]string{"out": "f.bin"}, []string{"u"}},
|
|
{"bool bundle", []string{"-cq", "u"}, Run, map[string]string{"continue": "true", "quiet": "true"}, []string{"u"}},
|
|
{"repeatable header", []string{"--header", "A: 1", "--header", "B: 2", "u"}, Run, map[string]string{"header": "A: 1\nB: 2"}, []string{"u"}},
|
|
{"two uris", []string{"a", "b"}, Run, map[string]string{}, []string{"a", "b"}},
|
|
{"permute flag after uri", []string{"u", "--split=8"}, Run, map[string]string{"split": "8"}, []string{"u"}},
|
|
{"permute interleaved", []string{"a", "-c", "b", "--quiet"}, Run, map[string]string{"continue": "true", "quiet": "true"}, []string{"a", "b"}},
|
|
{"dash dash terminator", []string{"--split=8", "--", "--not-a-flag", "u"}, Run, map[string]string{"split": "8"}, []string{"--not-a-flag", "u"}},
|
|
{"long int + magnet uri", []string{"--timeout=120", "magnet:x"}, Run, map[string]string{"timeout": "120"}, []string{"magnet:x"}},
|
|
{"help", []string{"--help"}, ShowHelp, nil, nil},
|
|
{"help short", []string{"-h"}, ShowHelp, nil, nil},
|
|
{"version", []string{"-v"}, ShowVersion, nil, nil},
|
|
}
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
vals, uris, action, _, err := parseArgs(tc.args)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if action != tc.action {
|
|
t.Fatalf("action = %v, want %v", action, tc.action)
|
|
}
|
|
if tc.action != Run {
|
|
return
|
|
}
|
|
for k, want := range tc.wantVal {
|
|
if vals[k] != want {
|
|
t.Errorf("vals[%q] = %q, want %q", k, vals[k], want)
|
|
}
|
|
}
|
|
if len(uris) != len(tc.uris) {
|
|
t.Fatalf("uris = %v, want %v", uris, tc.uris)
|
|
}
|
|
for i := range uris {
|
|
if uris[i] != tc.uris[i] {
|
|
t.Errorf("uris[%d] = %q, want %q", i, uris[i], tc.uris[i])
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestParseArgsErrors(t *testing.T) {
|
|
for _, args := range [][]string{
|
|
{"--unknown-flag"},
|
|
{"-Q"}, // -Q is not a defined short flag
|
|
{"--out"}, // missing value
|
|
} {
|
|
if _, _, _, _, err := parseArgs(args); err == nil {
|
|
t.Errorf("parseArgs(%v) = nil error, want error", args)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestParseSize(t *testing.T) {
|
|
tests := []struct {
|
|
in string
|
|
want int64
|
|
}{
|
|
{"", 0},
|
|
{"100", 100},
|
|
{"512K", 512 << 10},
|
|
{"20M", 20 << 20},
|
|
{"1024M", 1 << 30}, // there is no G unit; the 1G bound is typed as 1024M
|
|
{"1Mi", 1 << 20}, // the first K/M is taken and the rest discarded
|
|
{"10MB", 10 << 20},
|
|
{"2k", 2 << 10},
|
|
}
|
|
for _, tc := range tests {
|
|
got, err := parseSize(tc.in)
|
|
if err != nil {
|
|
t.Errorf("parseSize(%q) error: %v", tc.in, err)
|
|
continue
|
|
}
|
|
if got != tc.want {
|
|
t.Errorf("parseSize(%q) = %d, want %d", tc.in, got, tc.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestParseValueRange(t *testing.T) {
|
|
// A synthetic bounded Int exercises the Min/Max range check independent of
|
|
// which options happen to carry bounds.
|
|
o := &Opt{Long: "n", Kind: Int, Min: 1, Max: 16}
|
|
if _, err := parseValue(o, "16"); err != nil {
|
|
t.Errorf("parseValue 16: %v", err)
|
|
}
|
|
if _, err := parseValue(o, "99"); err == nil {
|
|
t.Errorf("parseValue 99: want out-of-range error")
|
|
}
|
|
if _, err := parseValue(o, "0"); err == nil {
|
|
t.Errorf("parseValue 0: want below-minimum error")
|
|
}
|
|
a := byLong["file-allocation"]
|
|
if _, err := parseValue(a, "bogus"); err == nil {
|
|
t.Errorf("parseValue enum bogus: want error")
|
|
}
|
|
}
|
|
|
|
func TestParseValueIntFloor(t *testing.T) {
|
|
// Min:0 enforces a floor of 0: negatives are rejected.
|
|
m := byLong["max-tries"] // Min 0, "0 = unlimited" runtime meaning preserved
|
|
if _, err := parseValue(m, "0"); err != nil {
|
|
t.Errorf("parseValue max-tries 0: %v, want nil (0 stays a valid input)", err)
|
|
}
|
|
if _, err := parseValue(m, "-1"); err == nil {
|
|
t.Errorf("parseValue max-tries -1: want below-minimum error")
|
|
}
|
|
}
|
|
|
|
func TestParseValueFloatNonNegative(t *testing.T) {
|
|
// Float options (seed-time, seed-ratio) reject negatives: a negative
|
|
// SeedTime would fire the seed timer immediately and a negative SeedRatio
|
|
// is silently ignored, so neither is a meaningful input.
|
|
for _, name := range []string{"seed-ratio", "seed-time"} {
|
|
o := byLong[name]
|
|
if _, err := parseValue(o, "-1"); err == nil {
|
|
t.Errorf("parseValue %s -1: want below-minimum error", name)
|
|
}
|
|
if _, err := parseValue(o, "0"); err != nil {
|
|
t.Errorf("parseValue %s 0: %v, want nil", name, err)
|
|
}
|
|
if _, err := parseValue(o, "1.5"); err != nil {
|
|
t.Errorf("parseValue %s 1.5: %v, want nil", name, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestParseValueBool(t *testing.T) {
|
|
b := byLong["enable-dht"]
|
|
// Only literal true/false are accepted (case-sensitive), with surrounding
|
|
// whitespace trimmed.
|
|
for _, ok := range []string{"true", "false", " true ", "false\t"} {
|
|
if _, err := parseValue(b, ok); err != nil {
|
|
t.Errorf("parseValue bool %q: %v", ok, err)
|
|
}
|
|
}
|
|
// The invented spellings and any case variant must fail.
|
|
for _, bad := range []string{"yes", "no", "1", "0", "on", "off", "flase",
|
|
"True", "TRUE", "tRuE", "False", "FALSE"} {
|
|
if _, err := parseValue(b, bad); err == nil {
|
|
t.Errorf("parseValue bool %q: want error (only true/false accepted)", bad)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestParseValueSizeBounds(t *testing.T) {
|
|
k := &Opt{Long: "sz", Kind: Size, Min: 1 << 20, Max: 1 << 30} // 1M..1024M
|
|
if _, err := parseValue(k, "20M"); err != nil {
|
|
t.Errorf("parseValue 20M: %v", err)
|
|
}
|
|
if _, err := parseValue(k, "512K"); err == nil {
|
|
t.Errorf("parseValue 512K: want below-minimum error")
|
|
}
|
|
// 1025M exceeds the 1<<30 cap and still parses, so it tests the bounds path
|
|
// (where "2G" would have failed earlier as an unknown unit).
|
|
if _, err := parseValue(k, "1025M"); err == nil {
|
|
t.Errorf("parseValue 1025M: want above-maximum error")
|
|
}
|
|
}
|
|
|
|
func TestParseSizeNegative(t *testing.T) {
|
|
if _, err := parseSize("-1"); err == nil {
|
|
t.Errorf("parseSize(-1): want error (size parsing rejects negative values)")
|
|
}
|
|
if _, err := parseSize("-5M"); err == nil {
|
|
t.Errorf("parseSize(-5M): want error")
|
|
}
|
|
}
|
|
|
|
// size parsing knows only K and M; a "G" suffix is not a unit and must be
|
|
// rejected rather than silently treated as 2^30.
|
|
func TestParseSizeNoGigaUnit(t *testing.T) {
|
|
for _, in := range []string{"1G", "1g", "1Gi", "5GB"} {
|
|
if _, err := parseSize(in); err == nil {
|
|
t.Errorf("parseSize(%q): want error (no G unit)", in)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestExplicitConfMissingFatal(t *testing.T) {
|
|
missing := filepath.Join(t.TempDir(), "no-such-file.conf")
|
|
// Default (non-explicit) missing path is silently ignored.
|
|
if err := applyConfig(newOptions(), missing, false); err != nil {
|
|
t.Errorf("applyConfig default missing: %v, want nil", err)
|
|
}
|
|
// Explicit missing path is fatal.
|
|
if err := applyConfig(newOptions(), missing, true); err == nil {
|
|
t.Errorf("applyConfig explicit missing: want error")
|
|
}
|
|
}
|
|
|
|
func TestOptionsGetters(t *testing.T) {
|
|
o := newOptions()
|
|
o.vals["split"] = "8"
|
|
o.vals["max-overall-download-limit"] = "20M"
|
|
o.vals["continue"] = "true"
|
|
o.vals["seed-ratio"] = "1.5"
|
|
if err := o.finalize(); err != nil { // parse the raw values once, as Parse does
|
|
t.Fatalf("finalize: %v", err)
|
|
}
|
|
if o.Int("split") != 8 {
|
|
t.Errorf("Int split = %d", o.Int("split"))
|
|
}
|
|
if o.Size("max-overall-download-limit") != 20<<20 {
|
|
t.Errorf("Size max-overall-download-limit = %d", o.Size("max-overall-download-limit"))
|
|
}
|
|
if !o.Bool("continue") {
|
|
t.Errorf("Bool continue = false")
|
|
}
|
|
if o.Float("seed-ratio") != 1.5 {
|
|
t.Errorf("Float seed-ratio = %v", o.Float("seed-ratio"))
|
|
}
|
|
}
|