blob: 2acd99ec1b4fa5d9ac18a36253fba91f09346096 [file] [log] [blame]
// Copyright 2017 The LUCI Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package python
import (
"fmt"
"testing"
. "github.com/smartystreets/goconvey/convey"
. "go.chromium.org/luci/common/testing/assertions"
)
func f(flag string, arg ...string) (result CommandLineFlag) {
result.Flag = flag
switch len(arg) {
case 0:
case 1:
result.Arg = arg[0]
default:
panic(fmt.Errorf("unsupported number of arguments in %v", arg))
}
return
}
func TestParseCommandLine(t *testing.T) {
t.Parallel()
successes := []struct {
args []string
cmd CommandLine
build []string
}{
{nil, CommandLine{Target: NoTarget{}}, []string{}},
{[]string{"-a", "-b", "-Q'foo.bar.baz'", "-X", "'foo.bar.baz'", "-Wbar"},
CommandLine{
Target: NoTarget{},
Flags: []CommandLineFlag{f("a"), f("b"), f("Q", "'foo.bar.baz'"), f("X", "'foo.bar.baz'"),
f("W", "bar")},
},
[]string{"-a", "-b", "-Q'foo.bar.baz'", "-X'foo.bar.baz'", "-Wbar"},
},
// Script target with flag separator.
{[]string{"path.py", "--", "foo", "bar"},
CommandLine{
Target: ScriptTarget{"path.py", false},
Args: []string{"--", "foo", "bar"},
},
[]string{"path.py", "--", "foo", "bar"},
},
// Command target with flag separator.
{[]string{"-v", "-c", "<script>", "--", "-foo", "-bar"},
CommandLine{
Flags: []CommandLineFlag{f("v")},
Target: CommandTarget{"<script>"},
Args: []string{"--", "-foo", "-bar"},
},
[]string{"-v", "-c", "<script>", "--", "-foo", "-bar"},
},
// Script target before with flag separator.
{[]string{"-v", "<script>", "--", "-foo", "-bar"},
CommandLine{
Flags: []CommandLineFlag{f("v")},
Target: ScriptTarget{"<script>", false},
Args: []string{"--", "-foo", "-bar"},
},
[]string{"-v", "<script>", "--", "-foo", "-bar"},
},
// Script target after flag separator.
{[]string{"-v", "--", "<script>", "-foo", "-bar"},
CommandLine{
Flags: []CommandLineFlag{f("v")},
Target: ScriptTarget{"<script>", true},
FlagSeparator: true,
Args: []string{"-foo", "-bar"},
},
[]string{"-v", "--", "<script>", "-foo", "-bar"},
},
{[]string{"-v", "-cprint", "foo", "bar"},
CommandLine{
Target: CommandTarget{"print"},
Flags: []CommandLineFlag{f("v")},
Args: []string{"foo", "bar"},
},
[]string{"-v", "-c", "print", "foo", "bar"},
},
{[]string{"-Wbar", "-", "-foo", "-bar"},
CommandLine{
Target: ScriptTarget{"-", false},
Flags: []CommandLineFlag{f("W", "bar")},
Args: []string{"-foo", "-bar"},
},
[]string{"-Wbar", "-", "-foo", "-bar"},
},
// NOTE: This will parse the first positional argument, "-c", as the path
// to the script to run. This is generally not done, but exercises the
// positional argument code.
{[]string{"--", "-c", "-foo", "-bar"},
CommandLine{
Target: ScriptTarget{"-c", true},
Args: []string{"-foo", "-bar"},
FlagSeparator: true,
},
[]string{"--", "-c", "-foo", "-bar"},
},
{[]string{"-a", "-Wfoo", "-", "--", "foo"},
CommandLine{
Target: ScriptTarget{"-", false},
Flags: []CommandLineFlag{f("a"), f("W", "foo")},
Args: []string{"--", "foo"},
},
[]string{"-a", "-Wfoo", "-", "--", "foo"},
},
{[]string{"-a", "-b", "-tt", "-W", "foo", "-Wbar", "-c", "<script>", "--", "arg"},
CommandLine{
Target: CommandTarget{"<script>"},
Flags: []CommandLineFlag{f("a"), f("b"), f("t"), f("t"), f("W", "foo"), f("W", "bar")},
Args: []string{"--", "arg"},
},
[]string{"-a", "-b", "-t", "-t", "-Wfoo", "-Wbar", "-c", "<script>", "--", "arg"},
},
{[]string{"-tt", "-W", "foo", "-Wbar", "-c", "<script>", "-Wbaz", "-v", "--", "arg"},
CommandLine{
Target: CommandTarget{"<script>"},
Flags: []CommandLineFlag{f("t"), f("t"), f("W", "foo"), f("W", "bar")},
Args: []string{"-Wbaz", "-v", "--", "arg"},
},
[]string{"-t", "-t", "-Wfoo", "-Wbar", "-c", "<script>", "-Wbaz", "-v", "--", "arg"},
},
// NOTE: -W-c is invalid argument to "-W", but it parses.
{[]string{"-vWfoo", "-vvvW", "-c", "script", "-arg"},
CommandLine{
Target: ScriptTarget{"script", false},
Flags: []CommandLineFlag{f("v"), f("W", "foo"), f("v"), f("v"), f("v"), f("W", "-c")},
Args: []string{"-arg"},
},
[]string{"-v", "-Wfoo", "-v", "-v", "-v", "-W-c", "script", "-arg"},
},
{[]string{"-vOOWfoo", "-Ovv", "-OOvv", "-c", "script", "-arg"},
CommandLine{
Target: CommandTarget{"script"},
Flags: []CommandLineFlag{
f("v"), f("OO"), f("W", "foo"),
f("O"), f("v"), f("v"),
f("OO"), f("v"), f("v"),
},
Args: []string{"-arg"},
},
[]string{"-v", "-OO", "-Wfoo", "-O", "-v", "-v", "-OO", "-v", "-v", "-c", "script", "-arg"},
},
{[]string{"-a", "-b", "-m'foo.bar.baz'", "arg"},
CommandLine{
Target: ModuleTarget{"'foo.bar.baz'"},
Flags: []CommandLineFlag{f("a"), f("b")},
Args: []string{"arg"},
},
[]string{"-a", "-b", "-m", "'foo.bar.baz'", "arg"},
},
// First separator signals end of flags, next is first positional argument,
// which is the path of the script. Remainder are positional arguments to
// the script.
{[]string{"--", "--", "--", "--"},
CommandLine{
Target: ScriptTarget{"--", true},
FlagSeparator: true,
Args: []string{"--", "--"},
},
[]string{"--", "--", "--", "--"},
},
// --version is a special multi-character flag.
{[]string{"--version"},
CommandLine{
Target: NoTarget{},
Flags: []CommandLineFlag{f("-version")},
},
[]string{"--version"},
},
}
failures := []struct {
args []string
err string
}{
{[]string{"-a", "-b", "-Q"}, "two-value flag missing second value"},
{[]string{"-c"}, "missing second value"},
{[]string{"-vvm"}, "missing second value"},
{[]string{"-\x80"}, "invalid rune in flag"},
}
Convey(`Testing Python command-line parsing`, t, func() {
for i, tc := range successes {
Convey(fmt.Sprintf(`Success case #%d: %v`, i, tc.args), func() {
cmd, err := ParseCommandLine(tc.args)
So(err, ShouldBeNil)
builtArgs := cmd.BuildArgs()
So(cmd, ShouldResemble, &tc.cmd)
So(builtArgs, ShouldResemble, tc.build)
// Round-trip!
roundTripBuiltArgs := cmd.BuildArgs()
cmd, err = ParseCommandLine(builtArgs)
So(err, ShouldBeNil)
So(cmd, ShouldResemble, &tc.cmd)
So(roundTripBuiltArgs, ShouldResemble, tc.build)
So(roundTripBuiltArgs, ShouldResemble, builtArgs)
})
}
for i, tc := range failures {
Convey(fmt.Sprintf(`Error case #%d: %v`, i, tc.args), func() {
_, err := ParseCommandLine(tc.args)
So(err, ShouldErrLike, tc.err)
})
}
})
}
func TestCommandLine(t *testing.T) {
t.Parallel()
testCases := []struct {
desc string
flags []CommandLineFlag
actionFn func(*CommandLine)
expected []CommandLineFlag
}{
{"Can remove a flag, '-OO'",
[]CommandLineFlag{f("c"), f("OO"), f("O"), f("S")},
func(cl *CommandLine) { cl.RemoveAllFlag("OO") },
[]CommandLineFlag{f("c"), f("O"), f("S")},
},
{"Can remove all instances of a flag, '-W'",
[]CommandLineFlag{f("W", "foo"), f("a"), f("b"), f("W"), f("c"), f("W", "bar"),
f("W", "baz"), f("d"), f("W", "qux")},
func(cl *CommandLine) { cl.RemoveAllFlag("W") },
[]CommandLineFlag{f("a"), f("b"), f("c"), f("d")},
},
{"Can remove an exact instance of a flag, '-W'",
[]CommandLineFlag{f("W", "foo"), f("a"), f("b"), f("W"), f("c"), f("W", "bar"),
f("W", "baz"), f("d"), f("W", "qux")},
func(cl *CommandLine) { cl.RemoveFlag(CommandLineFlag{"W", "bar"}) },
[]CommandLineFlag{f("W", "foo"), f("a"), f("b"), f("W"), f("c"),
f("W", "baz"), f("d"), f("W", "qux")},
},
{"Can add a nonexistent flag",
[]CommandLineFlag{f("v"), f("O")},
func(cl *CommandLine) { cl.AddSingleFlag("B") },
[]CommandLineFlag{f("v"), f("O"), f("B")},
},
{"Will not add a redundant flag, but will add a unique one",
[]CommandLineFlag{f("v"), f("O"), f("W", "foo"), f("W")},
func(cl *CommandLine) {
cl.AddFlag(CommandLineFlag{"W", "foo"})
cl.AddSingleFlag("OO")
cl.AddFlag(CommandLineFlag{"W", "bar"})
},
[]CommandLineFlag{f("v"), f("O"), f("W", "foo"), f("W"), f("OO"), f("W", "bar")},
},
}
Convey(`Testing CommandLine functionality`, t, func() {
for i, tc := range testCases {
Convey(fmt.Sprintf(`Test case #%d: %v`, i, tc.desc), func() {
cl := CommandLine{
Flags: tc.flags,
}
tc.actionFn(&cl)
So(cl.Flags, ShouldResemble, tc.expected)
})
}
Convey(`Testing Clone`, func() {
// Create a command-line with all fields populated.
cmd := CommandLine{
Target: ScriptTarget{"script", false},
Flags: []CommandLineFlag{f("OO"), f("v"), f("Q", "warnall")},
Args: []string{"foo"},
}
clone := cmd.Clone()
So(clone, ShouldResemble, &cmd)
clone.Flags = append(clone.Flags[:0], f("B"), f("d"), f("E"), f("H"))
clone.Args = append(clone.Args[:0], "bar", "baz")
So(clone, ShouldNotResemble, &cmd)
})
Convey(`Adding a flag with a '-' causes a panic.`, func() {
var cl CommandLine
So(func() { cl.AddSingleFlag("-") }, ShouldPanic)
So(func() { cl.AddFlag(f("-W", "all")) }, ShouldPanic)
})
Convey(`Adding a flag to a command-line with a flag separator`, func() {
cl, err := ParseCommandLine([]string{"--", "foo", "bar"})
So(err, ShouldBeNil)
cl.AddSingleFlag("B")
cl.AddSingleFlag("E")
So(cl.BuildArgs(), ShouldResemble, []string{"-B", "-E", "--", "foo", "bar"})
})
Convey(`Setting FlagSeparator with a flag target includes the flag in the proper section.`, func() {
cl := CommandLine{
Target: ModuleTarget{Module: "<module>"},
FlagSeparator: true,
Args: []string{"foo", "bar"},
}
So(cl.BuildArgs(), ShouldResemble, []string{"-m", "<module>", "--", "foo", "bar"})
})
})
}