CompletionScriptTests.swift 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255
  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 XCTest
  13. @testable import ArgumentParser
  14. private func candidates(prefix: String) -> [String] {
  15. switch CompletionShell.requesting {
  16. case CompletionShell.bash:
  17. return ["\(prefix)1_bash", "\(prefix)2_bash", "\(prefix)3_bash"]
  18. case CompletionShell.fish:
  19. return ["\(prefix)1_fish", "\(prefix)2_fish", "\(prefix)3_fish"]
  20. case CompletionShell.zsh:
  21. return ["\(prefix)1_zsh", "\(prefix)2_zsh", "\(prefix)3_zsh"]
  22. default:
  23. return []
  24. }
  25. }
  26. private func candidatesAsync(prefix: String) async -> [String] {
  27. candidates(prefix: prefix)
  28. }
  29. final class CompletionScriptTests: XCTestCase {}
  30. // swift-format-ignore: AlwaysUseLowerCamelCase
  31. // https://github.com/apple/swift-argument-parser/issues/710
  32. extension CompletionScriptTests {
  33. struct Path: ExpressibleByArgument {
  34. var path: String
  35. init?(argument: String) {
  36. self.path = argument
  37. }
  38. static var defaultCompletionKind: CompletionKind {
  39. .file()
  40. }
  41. }
  42. enum Kind:
  43. String,
  44. ExpressibleByArgument,
  45. EnumerableFlag,
  46. CustomStringConvertible
  47. {
  48. case one, two
  49. case three = "custom-three"
  50. }
  51. struct NestedArguments: ParsableArguments {
  52. @Argument(completion: .custom { _, _, _ in candidates(prefix: "a") })
  53. var nestedArgument: String
  54. }
  55. struct Base: ParsableCommand {
  56. static let configuration = CommandConfiguration(
  57. commandName: "base-test",
  58. subcommands: [SubCommand.self, HiddenChild.self, EscapedCommand.self])
  59. @Option(help: "The user's name.") var name: String
  60. @Option() var kind: Kind
  61. @Option(completion: .list(candidates(prefix: "b"))) var otherKind: Kind
  62. @Option() var path1: Path
  63. @Option() var path2: Path?
  64. @Option(completion: .list(candidates(prefix: "c"))) var path3: Path
  65. @Flag(help: .hidden) var verbose = false
  66. @Flag var allowedKinds: [Kind] = []
  67. @Flag var kindCounter: Int
  68. @Option() var rep1: [String]
  69. @Option(name: [.short, .long]) var rep2: [String]
  70. @Argument(completion: .custom { _, _, _ in candidates(prefix: "d") })
  71. var argument: String
  72. @OptionGroup var nested: NestedArguments
  73. struct SubCommand: ParsableCommand {
  74. static let configuration = CommandConfiguration(
  75. commandName: "sub-command")
  76. }
  77. struct HiddenChild: ParsableCommand {
  78. static let configuration = CommandConfiguration(shouldDisplay: false)
  79. }
  80. struct EscapedCommand: ParsableCommand {
  81. @Option(
  82. name: .customLong("o:n[e"),
  83. help: ArgumentHelp(
  84. #"Escaped chars: '[]\."#, valueName: "path[:options]"
  85. )
  86. )
  87. var one: String
  88. @Argument(completion: .custom { _, _, _ in candidates(prefix: "i") })
  89. var two: String
  90. }
  91. }
  92. func testBase_Zsh() throws {
  93. let script1 = try CompletionsGenerator(command: Base.self, shell: .zsh)
  94. .generateCompletionScript()
  95. try assertSnapshot(actual: script1, extension: "zsh")
  96. let script2 = try CompletionsGenerator(command: Base.self, shellName: "zsh")
  97. .generateCompletionScript()
  98. try assertSnapshot(actual: script2, extension: "zsh")
  99. let script3 = Base.completionScript(for: .zsh)
  100. try assertSnapshot(actual: script3, extension: "zsh")
  101. }
  102. func testBase_Bash() throws {
  103. let script1 = try CompletionsGenerator(command: Base.self, shell: .bash)
  104. .generateCompletionScript()
  105. try assertSnapshot(actual: script1, extension: "bash")
  106. let script2 = try CompletionsGenerator(
  107. command: Base.self, shellName: "bash"
  108. )
  109. .generateCompletionScript()
  110. try assertSnapshot(actual: script2, extension: "bash")
  111. let script3 = Base.completionScript(for: .bash)
  112. try assertSnapshot(actual: script3, extension: "bash")
  113. }
  114. func testBase_Fish() throws {
  115. let script1 = try CompletionsGenerator(command: Base.self, shell: .fish)
  116. .generateCompletionScript()
  117. try assertSnapshot(actual: script1, extension: "fish")
  118. let script2 = try CompletionsGenerator(
  119. command: Base.self, shellName: "fish"
  120. )
  121. .generateCompletionScript()
  122. try assertSnapshot(actual: script2, extension: "fish")
  123. let script3 = Base.completionScript(for: .fish)
  124. try assertSnapshot(actual: script3, extension: "fish")
  125. }
  126. }
  127. extension CompletionScriptTests {
  128. struct Custom: ParsableCommand {
  129. @Option(
  130. name: .shortAndLong,
  131. completion: .custom { _, _, _ in candidates(prefix: "e") }
  132. )
  133. var one: String
  134. @Argument(completion: .custom { _, _, _ in candidates(prefix: "f") })
  135. var two: String
  136. @Option(
  137. name: .customShort("z"),
  138. completion: .custom { _, _, _ in candidates(prefix: "g") }
  139. )
  140. var three: String
  141. @OptionGroup var nested: NestedArguments
  142. struct NestedArguments: ParsableArguments {
  143. @Argument(completion: .custom { _, _, _ in candidates(prefix: "h") })
  144. var four: String
  145. }
  146. @Argument(
  147. completion: .custom { _, _, _ in await candidatesAsync(prefix: "j") }
  148. )
  149. var five: String
  150. }
  151. func assertCustomCompletion(
  152. _ arg: String,
  153. shell: CompletionShell,
  154. prefix: String = "",
  155. file: StaticString = #filePath,
  156. line: UInt = #line
  157. ) throws {
  158. #if !os(Windows) && !os(WASI)
  159. do {
  160. Platform.Environment[.shellName, as: CompletionShell.self] = shell
  161. defer { Platform.Environment[.shellName] = nil }
  162. _ = try Custom.parse(["---completion", "--", arg, "0", "0"])
  163. XCTFail("Didn't error as expected", file: file, line: line)
  164. } catch let error as CommandError {
  165. guard case .completionScriptCustomResponse(let output) = error.parserError
  166. else {
  167. throw error
  168. }
  169. AssertEqualStrings(
  170. actual: output,
  171. expected: shell.format(completions: [
  172. "\(prefix)1_\(shell.rawValue)",
  173. "\(prefix)2_\(shell.rawValue)",
  174. "\(prefix)3_\(shell.rawValue)",
  175. ]),
  176. file: file,
  177. line: line)
  178. }
  179. #endif
  180. }
  181. func assertCustomCompletions(
  182. shell: CompletionShell,
  183. file: StaticString = #filePath,
  184. line: UInt = #line
  185. ) throws {
  186. #if !os(Windows) && !os(WASI)
  187. try assertCustomCompletion(
  188. "-o", shell: shell, prefix: "e", file: file, line: line)
  189. try assertCustomCompletion(
  190. "--one", shell: shell, prefix: "e", file: file, line: line)
  191. try assertCustomCompletion(
  192. "two", shell: shell, prefix: "f", file: file, line: line)
  193. try assertCustomCompletion(
  194. "-z", shell: shell, prefix: "g", file: file, line: line)
  195. try assertCustomCompletion(
  196. "nested.four", shell: shell, prefix: "h", file: file, line: line)
  197. try assertCustomCompletion(
  198. "five", shell: shell, prefix: "j", file: file, line: line)
  199. XCTAssertThrowsError(
  200. try assertCustomCompletion("--bad", shell: shell, file: file, line: line))
  201. XCTAssertThrowsError(
  202. try assertCustomCompletion("four", shell: shell, file: file, line: line))
  203. #endif
  204. }
  205. func testBashCustomCompletions() throws {
  206. try assertCustomCompletions(shell: .bash)
  207. }
  208. func testFishCustomCompletions() throws {
  209. try assertCustomCompletions(shell: .fish)
  210. }
  211. func testZshCustomCompletions() throws {
  212. try assertCustomCompletions(shell: .zsh)
  213. }
  214. }