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