1 /*
2  * Copyright (C) 2023 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.systemui.statusbar.commandline
18 
19 import androidx.test.filters.SmallTest
20 import com.android.systemui.SysuiTestCase
21 import com.google.common.truth.Truth.assertThat
22 import java.io.PrintWriter
23 import org.junit.Assert.assertThrows
24 import org.junit.Assert.assertTrue
25 import org.junit.Before
26 import org.junit.Test
27 import org.mockito.Mock
28 import org.mockito.MockitoAnnotations
29 
30 @SmallTest
31 class ParseableCommandTest : SysuiTestCase() {
32     @Mock private lateinit var pw: PrintWriter
33 
34     @Before
35     fun setup() {
36         MockitoAnnotations.initMocks(this)
37     }
38 
39     /**
40      * A little change-detector-y, but this is just a general assertion that building up a command
41      * parser via its wrapper works as expected.
42      */
43     @Test
44     fun testFactoryMethods() {
45         val mySubCommand =
46             object : ParseableCommand("subCommand") {
47                 val flag by flag("flag")
48                 override fun execute(pw: PrintWriter) {}
49             }
50 
51         val mySubCommand2 =
52             object : ParseableCommand("subCommand2") {
53                 val flag by flag("flag")
54                 override fun execute(pw: PrintWriter) {}
55             }
56 
57         // Verify that the underlying parser contains the correct types
58         val myCommand =
59             object : ParseableCommand("testName") {
60                 val flag by flag("flag", shortName = "f")
61                 val requiredParam by
62                     param(longName = "required-param", shortName = "r", valueParser = Type.String)
63                         .required()
64                 val optionalParam by
65                     param(longName = "optional-param", shortName = "o", valueParser = Type.Boolean)
66                 val optionalSubCommand by subCommand(mySubCommand)
67                 val requiredSubCommand by subCommand(mySubCommand2).required()
68 
69                 override fun execute(pw: PrintWriter) {}
70             }
71 
72         val flags = myCommand.parser.flags
73         val params = myCommand.parser.params
74         val subCommands = myCommand.parser.subCommands
75 
76         assertThat(flags).hasSize(2)
77         assertThat(flags[0]).isInstanceOf(Flag::class.java)
78         assertThat(flags[1]).isInstanceOf(Flag::class.java)
79 
80         assertThat(params).hasSize(2)
81         val req = params.filter { it is SingleArgParam<*> }
82         val opt = params.filter { it is SingleArgParamOptional<*> }
83         assertThat(req).hasSize(1)
84         assertThat(opt).hasSize(1)
85 
86         val reqSub = subCommands.filter { it is RequiredSubCommand<*> }
87         val optSub = subCommands.filter { it is OptionalSubCommand<*> }
88         assertThat(reqSub).hasSize(1)
89         assertThat(optSub).hasSize(1)
90     }
91 
92     @Test
93     fun factoryMethods_enforceShortNameRules() {
94         // Short names MUST be one character long
95         assertThrows(IllegalArgumentException::class.java) {
96             val myCommand =
97                 object : ParseableCommand("test-command") {
98                     val flag by flag("longName", "invalidShortName")
99 
100                     override fun execute(pw: PrintWriter) {}
101                 }
102         }
103 
104         assertThrows(IllegalArgumentException::class.java) {
105             val myCommand =
106                 object : ParseableCommand("test-command") {
107                     val param by param("longName", "invalidShortName", valueParser = Type.String)
108 
109                     override fun execute(pw: PrintWriter) {}
110                 }
111         }
112     }
113 
114     @Test
115     fun factoryMethods_enforceLongNames_notPrefixed() {
116         // Long names must not start with "-", since they will be added
117         assertThrows(IllegalArgumentException::class.java) {
118             val myCommand =
119                 object : ParseableCommand("test-command") {
120                     val flag by flag("--invalid")
121 
122                     override fun execute(pw: PrintWriter) {}
123                 }
124         }
125 
126         assertThrows(IllegalArgumentException::class.java) {
127             val myCommand =
128                 object : ParseableCommand("test-command") {
129                     val param by param("-invalid", valueParser = Type.String)
130 
131                     override fun execute(pw: PrintWriter) {}
132                 }
133         }
134     }
135 
136     @Test
137     fun executeDoesNotPropagateExceptions() {
138         val cmd =
139             object : ParseableCommand("test-command") {
140                 val flag by flag("flag")
141                 override fun execute(pw: PrintWriter) {}
142             }
143 
144         val throwingCommand = listOf("unknown-token")
145 
146         // Given a command that would cause an ArgParseError
147         assertThrows(ArgParseError::class.java) { cmd.parser.parse(throwingCommand) }
148 
149         // The parser consumes that error
150         cmd.execute(pw, throwingCommand)
151     }
152 
153     @Test
154     fun executeFailingCommand_callsOnParseFailed() {
155         val cmd =
156             object : ParseableCommand("test-command") {
157                 val flag by flag("flag")
158 
159                 var onParseFailedCalled = false
160 
161                 override fun execute(pw: PrintWriter) {}
162                 override fun onParseFailed(error: ArgParseError) {
163                     onParseFailedCalled = true
164                 }
165             }
166 
167         val throwingCommand = listOf("unknown-token")
168         cmd.execute(pw, throwingCommand)
169 
170         assertTrue(cmd.onParseFailedCalled)
171     }
172 
173     @Test
174     fun baseCommand() {
175         val myCommand = MyCommand()
176         myCommand.execute(pw, baseCommand)
177 
178         assertThat(myCommand.flag1).isFalse()
179         assertThat(myCommand.singleParam).isNull()
180     }
181 
182     @Test
183     fun commandWithFlags() {
184         val command = MyCommand()
185         command.execute(pw, cmdWithFlags)
186 
187         assertThat(command.flag1).isTrue()
188         assertThat(command.flag2).isTrue()
189     }
190 
191     @Test
192     fun commandWithArgs() {
193         val cmd = MyCommand()
194         cmd.execute(pw, cmdWithSingleArgParam)
195 
196         assertThat(cmd.singleParam).isEqualTo("single_param")
197     }
198 
199     @Test
200     fun commandWithRequiredParam_provided() {
201         val cmd =
202             object : ParseableCommand(name) {
203                 val singleRequiredParam: String by
204                     param(
205                             longName = "param1",
206                             shortName = "p",
207                             valueParser = Type.String,
208                         )
209                         .required()
210 
211                 override fun execute(pw: PrintWriter) {}
212             }
213 
214         val cli = listOf("-p", "value")
215         cmd.execute(pw, cli)
216 
217         assertThat(cmd.singleRequiredParam).isEqualTo("value")
218     }
219 
220     @Test
221     fun commandWithRequiredParam_not_provided_throws() {
222         val cmd =
223             object : ParseableCommand(name) {
224                 val singleRequiredParam by
225                     param(shortName = "p", longName = "param1", valueParser = Type.String)
226                         .required()
227 
228                 override fun execute(pw: PrintWriter) {}
229 
230                 override fun execute(pw: PrintWriter, args: List<String>) {
231                     parser.parse(args)
232                     execute(pw)
233                 }
234             }
235 
236         val cli = listOf("")
237         assertThrows(ArgParseError::class.java) { cmd.execute(pw, cli) }
238     }
239 
240     @Test
241     fun commandWithSubCommand() {
242         val subName = "sub-command"
243         val subCmd =
244             object : ParseableCommand(subName) {
245                 val singleOptionalParam: String? by param("param", valueParser = Type.String)
246 
247                 override fun execute(pw: PrintWriter) {}
248             }
249 
250         val cmd =
251             object : ParseableCommand(name) {
252                 val subCmd by subCommand(subCmd)
253                 override fun execute(pw: PrintWriter) {}
254             }
255 
256         cmd.execute(pw, listOf("sub-command", "--param", "test"))
257         assertThat(cmd.subCmd?.singleOptionalParam).isEqualTo("test")
258     }
259 
260     @Test
261     fun complexCommandWithSubCommands_reusedNames() {
262         val commandLine = "-f --param1 arg1 sub-command1 -f -p arg2 --param2 arg3".split(" ")
263 
264         val subName = "sub-command1"
265         val subCmd =
266             object : ParseableCommand(subName) {
267                 val flag1 by flag("flag", shortName = "f")
268                 val param1: String? by param("param1", shortName = "p", valueParser = Type.String)
269 
270                 override fun execute(pw: PrintWriter) {}
271             }
272 
273         val myCommand =
274             object : ParseableCommand(name) {
275                 val flag1 by flag(longName = "flag", shortName = "f")
276                 val param1 by param("param1", shortName = "p", valueParser = Type.String).required()
277                 val param2: String? by param(longName = "param2", valueParser = Type.String)
278                 val subCommand by subCommand(subCmd)
279 
280                 override fun execute(pw: PrintWriter) {}
281             }
282 
283         myCommand.execute(pw, commandLine)
284 
285         assertThat(myCommand.flag1).isTrue()
286         assertThat(myCommand.param1).isEqualTo("arg1")
287         assertThat(myCommand.param2).isEqualTo("arg3")
288         assertThat(myCommand.subCommand).isNotNull()
289         assertThat(myCommand.subCommand?.flag1).isTrue()
290         assertThat(myCommand.subCommand?.param1).isEqualTo("arg2")
291     }
292 
293     class MyCommand(
294         private val onExecute: ((MyCommand) -> Unit)? = null,
295     ) : ParseableCommand(name) {
296 
297         val flag1 by flag(shortName = "f", longName = "flag1", description = "flag 1 for test")
298         val flag2 by flag(shortName = "g", longName = "flag2", description = "flag 2 for test")
299         val singleParam: String? by
300             param(
301                 shortName = "a",
302                 longName = "arg1",
303                 valueParser = Type.String,
304             )
305 
306         override fun execute(pw: PrintWriter) {
307             onExecute?.invoke(this)
308         }
309     }
310 
311     companion object {
312         const val name = "my_command"
313         val baseCommand = listOf("")
314         val cmdWithFlags = listOf("-f", "--flag2")
315         val cmdWithSingleArgParam = listOf("--arg1", "single_param")
316     }
317 }
318