DefaultSubcommandEndToEndTests.swift 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378
  1. //===----------------------------------------------------------------------===//
  2. //
  3. // This source file is part of the Swift Argument Parser open source project
  4. //
  5. // Copyright (c) 2020 Apple Inc. and the Swift project authors
  6. // Licensed under Apache License v2.0 with Runtime Library Exception
  7. //
  8. // See https://swift.org/LICENSE.txt for license information
  9. //
  10. //===----------------------------------------------------------------------===//
  11. import ArgumentParserTestHelpers
  12. import ArgumentParserToolInfo
  13. import XCTest
  14. @testable import ArgumentParser
  15. final class DefaultSubcommandEndToEndTests: XCTestCase {
  16. }
  17. // MARK: -
  18. private struct Main: ParsableCommand {
  19. static let configuration = CommandConfiguration(
  20. subcommands: [Default.self, Foo.self, Bar.self],
  21. defaultSubcommand: Default.self
  22. )
  23. }
  24. private struct Default: ParsableCommand {
  25. enum Mode: String, CaseIterable, ExpressibleByArgument {
  26. case foo, bar, baz
  27. }
  28. @Option var mode: Mode = .foo
  29. }
  30. private struct Foo: ParsableCommand {}
  31. private struct Bar: ParsableCommand {}
  32. extension DefaultSubcommandEndToEndTests {
  33. func testDefaultSubcommand() {
  34. AssertParseCommand(Main.self, Default.self, []) { def in
  35. XCTAssertEqual(.foo, def.mode)
  36. }
  37. AssertParseCommand(Main.self, Default.self, ["--mode=bar"]) { def in
  38. XCTAssertEqual(.bar, def.mode)
  39. }
  40. AssertParseCommand(Main.self, Default.self, ["--mode", "bar"]) { def in
  41. XCTAssertEqual(.bar, def.mode)
  42. }
  43. AssertParseCommand(Main.self, Default.self, ["--mode", "baz"]) { def in
  44. XCTAssertEqual(.baz, def.mode)
  45. }
  46. }
  47. func testNonDefaultSubcommand() {
  48. AssertParseCommand(Main.self, Foo.self, ["foo"]) { _ in }
  49. AssertParseCommand(Main.self, Bar.self, ["bar"]) { _ in }
  50. AssertParseCommand(Main.self, Default.self, ["default", "--mode", "bar"]) {
  51. def in
  52. XCTAssertEqual(.bar, def.mode)
  53. }
  54. }
  55. func testParsingFailure() {
  56. XCTAssertThrowsError(try Main.parseAsRoot(["--mode", "qux"]))
  57. XCTAssertThrowsError(try Main.parseAsRoot(["qux"]))
  58. }
  59. }
  60. extension DefaultSubcommandEndToEndTests {
  61. fileprivate struct MyCommand: ParsableCommand {
  62. static let configuration = CommandConfiguration(
  63. subcommands: [
  64. Plugin.self, NonDefault.self, Other.self, Child.self, BadParent.self,
  65. ],
  66. defaultSubcommand: Plugin.self
  67. )
  68. @Option var foo: String?
  69. @OptionGroup
  70. var options: CommonOptions
  71. }
  72. fileprivate struct CommonOptions: ParsableArguments {
  73. @Flag(
  74. name: [.customLong("verbose"), .customShort("v")],
  75. help: "Enable verbose aoutput.")
  76. var verbose = false
  77. }
  78. fileprivate struct Plugin: ParsableCommand {
  79. @OptionGroup var options: CommonOptions
  80. @Argument var pluginName: String
  81. @Argument(parsing: .captureForPassthrough)
  82. var pluginArguments: [String] = []
  83. }
  84. fileprivate struct NonDefault: ParsableCommand {
  85. @OptionGroup var options: CommonOptions
  86. @Argument var pluginName: String
  87. @Argument(parsing: .captureForPassthrough)
  88. var pluginArguments: [String] = []
  89. }
  90. fileprivate struct Other: ParsableCommand {
  91. @OptionGroup var options: CommonOptions
  92. }
  93. fileprivate struct Child: ParsableCommand {
  94. @ParentCommand var parent: MyCommand
  95. }
  96. fileprivate struct BadParent: ParsableCommand {
  97. @ParentCommand var notMyParent: Other
  98. }
  99. func testAccessToParent() throws {
  100. AssertParseCommand(
  101. MyCommand.self, Child.self, ["--verbose", "--foo=bar", "child"]
  102. ) { child in
  103. XCTAssertEqual(child.parent.foo, "bar")
  104. XCTAssertEqual(child.parent.options.verbose, true)
  105. }
  106. }
  107. func testNotMyParent() throws {
  108. AssertParseCommandErrorMessage(
  109. MyCommand.self, BadParent.self, ["--verbose", "bad-parent"],
  110. "Command 'Other' is not a parent of the current command.")
  111. }
  112. func testNotLeakingParentOptions() throws {
  113. // Verify that the help for the child command doesn't leak the parent command's options in the help
  114. let childHelp = MyCommand.message(for: CleanExit.helpRequest(Child.self))
  115. XCTAssertEqual(
  116. childHelp,
  117. """
  118. USAGE: my-command child
  119. OPTIONS:
  120. -h, --help Show help information.
  121. """)
  122. // Now check that the foo option doesn't leak into the JSON dump
  123. let toolInfo = ToolInfoV0(commandStack: [MyCommand.self.asCommand])
  124. let arguments = toolInfo.command.arguments
  125. guard let arguments else {
  126. XCTFail(
  127. "MyCommand is expected to have a top-level command arguments in its tool info"
  128. )
  129. return
  130. }
  131. let subcommands = toolInfo.command.subcommands
  132. guard let subcommands else {
  133. XCTFail(
  134. "MyCommand is expected to have a top-level command arguments in its tool info"
  135. )
  136. return
  137. }
  138. // The foo option is present int he parent
  139. XCTAssertNotNil(arguments.first { $0.valueName == "foo" })
  140. let childInfo = subcommands.first { cmd in
  141. cmd.commandName == "child"
  142. }
  143. guard let childInfo else {
  144. XCTFail("The child subcommand is expected to be present in the tool info")
  145. return
  146. }
  147. guard let childArguments = childInfo.arguments else {
  148. XCTFail(
  149. "The child subcommand is expected to have arguments in the tool info")
  150. return
  151. }
  152. // It's not there in the child subcommand
  153. XCTAssertNil(childArguments.first { $0.valueName == "foo" })
  154. }
  155. func testRemainingDefaultImplicit() throws {
  156. AssertParseCommand(MyCommand.self, Plugin.self, ["my-plugin"]) { plugin in
  157. XCTAssertEqual(plugin.pluginName, "my-plugin")
  158. XCTAssertEqual(plugin.pluginArguments, [])
  159. XCTAssertEqual(plugin.options.verbose, false)
  160. }
  161. AssertParseCommand(MyCommand.self, Plugin.self, ["my-plugin", "--verbose"])
  162. { plugin in
  163. XCTAssertEqual(plugin.pluginName, "my-plugin")
  164. XCTAssertEqual(plugin.pluginArguments, ["--verbose"])
  165. XCTAssertEqual(plugin.options.verbose, false)
  166. }
  167. AssertParseCommand(
  168. MyCommand.self, Plugin.self, ["--verbose", "my-plugin", "--verbose"]
  169. ) { plugin in
  170. XCTAssertEqual(plugin.pluginName, "my-plugin")
  171. XCTAssertEqual(plugin.pluginArguments, ["--verbose"])
  172. XCTAssertEqual(plugin.options.verbose, true)
  173. }
  174. AssertParseCommand(MyCommand.self, Plugin.self, ["my-plugin", "--help"]) {
  175. plugin in
  176. XCTAssertEqual(plugin.pluginName, "my-plugin")
  177. XCTAssertEqual(plugin.pluginArguments, ["--help"])
  178. XCTAssertEqual(plugin.options.verbose, false)
  179. }
  180. }
  181. func testRemainingDefaultExplicit() throws {
  182. AssertParseCommand(MyCommand.self, Plugin.self, ["plugin", "my-plugin"]) {
  183. plugin in
  184. XCTAssertEqual(plugin.pluginName, "my-plugin")
  185. XCTAssertEqual(plugin.pluginArguments, [])
  186. XCTAssertEqual(plugin.options.verbose, false)
  187. }
  188. AssertParseCommand(
  189. MyCommand.self, Plugin.self, ["plugin", "my-plugin", "--verbose"]
  190. ) { plugin in
  191. XCTAssertEqual(plugin.pluginName, "my-plugin")
  192. XCTAssertEqual(plugin.pluginArguments, ["--verbose"])
  193. XCTAssertEqual(plugin.options.verbose, false)
  194. }
  195. AssertParseCommand(
  196. MyCommand.self, Plugin.self,
  197. ["--verbose", "plugin", "my-plugin", "--verbose"]
  198. ) { plugin in
  199. XCTAssertEqual(plugin.pluginName, "my-plugin")
  200. XCTAssertEqual(plugin.pluginArguments, ["--verbose"])
  201. XCTAssertEqual(plugin.options.verbose, true)
  202. }
  203. AssertParseCommand(
  204. MyCommand.self, Plugin.self,
  205. ["--verbose", "plugin", "my-plugin", "--help"]
  206. ) { plugin in
  207. XCTAssertEqual(plugin.pluginName, "my-plugin")
  208. XCTAssertEqual(plugin.pluginArguments, ["--help"])
  209. XCTAssertEqual(plugin.options.verbose, true)
  210. }
  211. }
  212. func testRemainingNonDefault() throws {
  213. AssertParseCommand(
  214. MyCommand.self, NonDefault.self, ["non-default", "my-plugin"]
  215. ) { nondef in
  216. XCTAssertEqual(nondef.pluginName, "my-plugin")
  217. XCTAssertEqual(nondef.pluginArguments, [])
  218. XCTAssertEqual(nondef.options.verbose, false)
  219. }
  220. AssertParseCommand(
  221. MyCommand.self, NonDefault.self,
  222. ["non-default", "my-plugin", "--verbose"]
  223. ) { nondef in
  224. XCTAssertEqual(nondef.pluginName, "my-plugin")
  225. XCTAssertEqual(nondef.pluginArguments, ["--verbose"])
  226. XCTAssertEqual(nondef.options.verbose, false)
  227. }
  228. AssertParseCommand(
  229. MyCommand.self, NonDefault.self,
  230. ["--verbose", "non-default", "my-plugin", "--verbose"]
  231. ) { nondef in
  232. XCTAssertEqual(nondef.pluginName, "my-plugin")
  233. XCTAssertEqual(nondef.pluginArguments, ["--verbose"])
  234. XCTAssertEqual(nondef.options.verbose, true)
  235. }
  236. AssertParseCommand(
  237. MyCommand.self, NonDefault.self,
  238. ["--verbose", "non-default", "my-plugin", "--help"]
  239. ) { nondef in
  240. XCTAssertEqual(nondef.pluginName, "my-plugin")
  241. XCTAssertEqual(nondef.pluginArguments, ["--help"])
  242. XCTAssertEqual(nondef.options.verbose, true)
  243. }
  244. }
  245. func testRemainingDefaultOther() throws {
  246. AssertParseCommand(MyCommand.self, Other.self, ["other"]) { other in
  247. XCTAssertEqual(other.options.verbose, false)
  248. }
  249. AssertParseCommand(MyCommand.self, Other.self, ["other", "--verbose"]) {
  250. other in
  251. XCTAssertEqual(other.options.verbose, true)
  252. }
  253. }
  254. func testRemainingDefaultFailure() {
  255. XCTAssertThrowsError(try MyCommand.parseAsRoot([]))
  256. XCTAssertThrowsError(try MyCommand.parseAsRoot(["--verbose"]))
  257. XCTAssertThrowsError(
  258. try MyCommand.parseAsRoot(["plugin", "--verbose", "my-plugin"]))
  259. }
  260. }
  261. extension DefaultSubcommandEndToEndTests {
  262. struct RootWithPassthroughDefault: ParsableCommand {
  263. static let configuration = CommandConfiguration(
  264. subcommands: [PassthroughDefault.self],
  265. defaultSubcommand: PassthroughDefault.self,
  266. helpNames: [.short, .long, .customLong("help", withSingleDash: true)]
  267. )
  268. }
  269. struct PassthroughDefault: ParsableCommand {
  270. @Argument(parsing: .captureForPassthrough)
  271. var remaining: [String] = []
  272. }
  273. // Test fix for https://github.com/apple/swift-package-manager/issues/7218
  274. func testHelpWithPassthroughDefault() throws {
  275. AssertParseCommand(
  276. RootWithPassthroughDefault.self, HelpCommand.self, ["-h"]
  277. ) { _ in }
  278. AssertParseCommand(
  279. RootWithPassthroughDefault.self, HelpCommand.self, ["-help"]
  280. ) { _ in }
  281. AssertParseCommand(
  282. RootWithPassthroughDefault.self, HelpCommand.self, ["--help"]
  283. ) { _ in }
  284. }
  285. }
  286. extension DefaultSubcommandEndToEndTests {
  287. struct NestedDefaultSubcommandHelp: ParsableCommand {
  288. static let configuration = CommandConfiguration(
  289. subcommands: [Default.self, Nested.self],
  290. defaultSubcommand: Default.self
  291. )
  292. struct Default: ParsableCommand {}
  293. struct Nested: ParsableCommand {
  294. static let configuration = CommandConfiguration(
  295. commandName: "nested",
  296. subcommands: [NestedDefault.self, NestedOther.self],
  297. defaultSubcommand: NestedDefault.self
  298. )
  299. }
  300. struct NestedDefault: ParsableCommand {}
  301. struct NestedOther: ParsableCommand {}
  302. }
  303. // Test fix for https://github.com/apple/swift-argument-parser/issues/865
  304. func testHelpWithNestedDefaultSubcommand() throws {
  305. AssertParseCommand(
  306. NestedDefaultSubcommandHelp.self, HelpCommand.self, ["--help"]
  307. ) { command in
  308. XCTAssert(command.commandStack.count == 1)
  309. XCTAssert(
  310. type(of: command.commandStack[0])
  311. == NestedDefaultSubcommandHelp.Type.self)
  312. }
  313. AssertParseCommand(
  314. NestedDefaultSubcommandHelp.self, HelpCommand.self, ["nested", "--help"]
  315. ) { command in
  316. XCTAssert(command.commandStack.count == 2)
  317. XCTAssert(
  318. type(of: command.commandStack[0])
  319. == NestedDefaultSubcommandHelp.Type.self)
  320. XCTAssert(
  321. type(of: command.commandStack[1])
  322. == NestedDefaultSubcommandHelp.Nested.Type.self)
  323. }
  324. }
  325. }