validate() parsed every value to bounds-check it, then the typed getters re-parsed the same string at read time (with a comment admitting the failure was unreachable). A single parseValue() now validates and converts, finalize() runs it once per option after layering, and the getters just read the stored typed value.
226 lines
7.5 KiB
Go
226 lines
7.5 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{"-x16", "https://e.com"}, Run, map[string]string{"max-connection-per-server": "16"}, []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{"--bt-stop-timeout=120", "magnet:x"}, Run, map[string]string{"bt-stop-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) {
|
|
x := byLong["max-connection-per-server"]
|
|
if _, err := parseValue(x, "16"); err != nil {
|
|
t.Errorf("parseValue 16: %v", err)
|
|
}
|
|
if _, err := parseValue(x, "99"); err == nil {
|
|
t.Errorf("parseValue 99: want out-of-range 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 := byLong["min-split-size"] // 1M..1024M (no G unit)
|
|
if _, err := parseValue(k, "20M"); err != nil {
|
|
t.Errorf("parseValue min-split-size 20M: %v", err)
|
|
}
|
|
if _, err := parseValue(k, "512K"); err == nil {
|
|
t.Errorf("parseValue min-split-size 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 min-split-size 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["min-split-size"] = "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("min-split-size") != 20<<20 {
|
|
t.Errorf("Size min-split-size = %d", o.Size("min-split-size"))
|
|
}
|
|
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"))
|
|
}
|
|
}
|