test_options.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385
  1. # Copyright (c) Twisted Matrix Laboratories.
  2. # See LICENSE for details.
  3. """
  4. Tests for L{twisted.application.twist._options}.
  5. """
  6. from sys import stdout, stderr
  7. from twisted.internet import reactor
  8. from twisted.copyright import version
  9. from twisted.python.usage import UsageError
  10. from twisted.logger import LogLevel, textFileLogObserver, jsonFileLogObserver
  11. from twisted.test.proto_helpers import MemoryReactor
  12. from ...reactors import NoSuchReactor
  13. from ...service import ServiceMaker
  14. from ...runner._exit import ExitStatus
  15. from ...runner.test.test_runner import DummyExit
  16. from ...twist import _options
  17. from .._options import TwistOptions
  18. import twisted.trial.unittest
  19. class OptionsTests(twisted.trial.unittest.TestCase):
  20. """
  21. Tests for L{TwistOptions}.
  22. """
  23. def patchExit(self):
  24. """
  25. Patch L{_twist.exit} so we can capture usage and prevent actual exits.
  26. """
  27. self.exit = DummyExit()
  28. self.patch(_options, "exit", self.exit)
  29. def patchOpen(self):
  30. """
  31. Patch L{_options.open} so we can capture usage and prevent actual opens.
  32. """
  33. self.opened = []
  34. def fakeOpen(name, mode=None):
  35. if name == "nocanopen":
  36. raise IOError(None, None, name)
  37. self.opened.append((name, mode))
  38. return NotImplemented
  39. self.patch(_options, "openFile", fakeOpen)
  40. def patchInstallReactor(self):
  41. """
  42. Patch C{_options.installReactor} so we can capture usage and prevent
  43. actual installs.
  44. """
  45. self.installedReactors = {}
  46. def installReactor(name):
  47. if name != "fusion":
  48. raise NoSuchReactor()
  49. reactor = MemoryReactor()
  50. self.installedReactors[name] = reactor
  51. return reactor
  52. self.patch(_options, "installReactor", installReactor)
  53. def test_synopsis(self):
  54. """
  55. L{TwistOptions.getSynopsis} appends arguments.
  56. """
  57. options = TwistOptions()
  58. self.assertTrue(
  59. options.getSynopsis().endswith(" plugin [plugin_options]")
  60. )
  61. def test_version(self):
  62. """
  63. L{TwistOptions.opt_version} exits with L{ExitStatus.EX_OK} and prints
  64. the version.
  65. """
  66. self.patchExit()
  67. options = TwistOptions()
  68. options.opt_version()
  69. self.assertEquals(self.exit.status, ExitStatus.EX_OK)
  70. self.assertEquals(self.exit.message, version)
  71. def test_reactor(self):
  72. """
  73. L{TwistOptions.installReactor} installs the chosen reactor and sets
  74. the reactor name.
  75. """
  76. self.patchInstallReactor()
  77. options = TwistOptions()
  78. options.opt_reactor("fusion")
  79. self.assertEqual(set(self.installedReactors), set(["fusion"]))
  80. self.assertEquals(options["reactorName"], "fusion")
  81. def test_installCorrectReactor(self):
  82. """
  83. L{TwistOptions.installReactor} installs the chosen reactor after the
  84. command line options have been parsed.
  85. """
  86. self.patchInstallReactor()
  87. options = TwistOptions()
  88. options.subCommand = "test-subcommand"
  89. options.parseOptions(["--reactor=fusion"])
  90. self.assertEqual(set(self.installedReactors), set(["fusion"]))
  91. def test_installReactorBogus(self):
  92. """
  93. L{TwistOptions.installReactor} raises UsageError if an unknown reactor
  94. is specified.
  95. """
  96. self.patchInstallReactor()
  97. options = TwistOptions()
  98. self.assertRaises(UsageError, options.opt_reactor, "coal")
  99. def test_installReactorDefault(self):
  100. """
  101. L{TwistOptions.installReactor} returns the currently installed reactor
  102. when the default reactor name is specified.
  103. """
  104. options = TwistOptions()
  105. self.assertIdentical(reactor, options.installReactor('default'))
  106. def test_logLevelValid(self):
  107. """
  108. L{TwistOptions.opt_log_level} sets the corresponding log level.
  109. """
  110. options = TwistOptions()
  111. options.opt_log_level("warn")
  112. self.assertIdentical(options["logLevel"], LogLevel.warn)
  113. def test_logLevelInvalid(self):
  114. """
  115. L{TwistOptions.opt_log_level} with an invalid log level name raises
  116. UsageError.
  117. """
  118. options = TwistOptions()
  119. self.assertRaises(UsageError, options.opt_log_level, "cheese")
  120. def _testLogFile(self, name, expectedStream):
  121. """
  122. Set log file name and check the selected output stream.
  123. @param name: The name of the file.
  124. @param expectedStream: The expected stream.
  125. """
  126. options = TwistOptions()
  127. options.opt_log_file(name)
  128. self.assertIdentical(options["logFile"], expectedStream)
  129. def test_logFileStdout(self):
  130. """
  131. L{TwistOptions.opt_log_file} given C{"-"} as a file name uses stdout.
  132. """
  133. self._testLogFile("-", stdout)
  134. def test_logFileStderr(self):
  135. """
  136. L{TwistOptions.opt_log_file} given C{"+"} as a file name uses stderr.
  137. """
  138. self._testLogFile("+", stderr)
  139. def test_logFileNamed(self):
  140. """
  141. L{TwistOptions.opt_log_file} opens the given file name in append mode.
  142. """
  143. self.patchOpen()
  144. options = TwistOptions()
  145. options.opt_log_file("mylog")
  146. self.assertEqual([("mylog", "a")], self.opened)
  147. def test_logFileCantOpen(self):
  148. """
  149. L{TwistOptions.opt_log_file} exits with L{ExitStatus.EX_IOERR} if
  150. unable to open the log file due to an L{EnvironmentError}.
  151. """
  152. self.patchExit()
  153. self.patchOpen()
  154. options = TwistOptions()
  155. options.opt_log_file("nocanopen")
  156. self.assertEquals(self.exit.status, ExitStatus.EX_IOERR)
  157. self.assertTrue(
  158. self.exit.message.startswith(
  159. "Unable to open log file 'nocanopen': "
  160. )
  161. )
  162. def _testLogFormat(self, format, expectedObserver):
  163. """
  164. Set log file format and check the selected observer.
  165. @param format: The format of the file.
  166. @param expectedObserver: The expected observer.
  167. """
  168. options = TwistOptions()
  169. options.opt_log_format(format)
  170. self.assertIdentical(
  171. options["fileLogObserverFactory"], expectedObserver
  172. )
  173. self.assertEqual(options["logFormat"], format)
  174. def test_logFormatText(self):
  175. """
  176. L{TwistOptions.opt_log_format} given C{"text"} uses a
  177. L{textFileLogObserver}.
  178. """
  179. self._testLogFormat("text", textFileLogObserver)
  180. def test_logFormatJSON(self):
  181. """
  182. L{TwistOptions.opt_log_format} given C{"text"} uses a
  183. L{textFileLogObserver}.
  184. """
  185. self._testLogFormat("json", jsonFileLogObserver)
  186. def test_logFormatInvalid(self):
  187. """
  188. L{TwistOptions.opt_log_format} given an invalid format name raises
  189. L{UsageError}.
  190. """
  191. options = TwistOptions()
  192. self.assertRaises(UsageError, options.opt_log_format, "frommage")
  193. def test_selectDefaultLogObserverNoOverride(self):
  194. """
  195. L{TwistOptions.selectDefaultLogObserver} will not override an already
  196. selected observer.
  197. """
  198. self.patchOpen()
  199. options = TwistOptions()
  200. options.opt_log_format("text") # Ask for text
  201. options.opt_log_file("queso") # File, not a tty
  202. options.selectDefaultLogObserver()
  203. # Because we didn't select a file that is a tty, the default is JSON,
  204. # but since we asked for text, we should get text.
  205. self.assertIdentical(
  206. options["fileLogObserverFactory"], textFileLogObserver
  207. )
  208. self.assertEqual(options["logFormat"], "text")
  209. def test_selectDefaultLogObserverDefaultWithTTY(self):
  210. """
  211. L{TwistOptions.selectDefaultLogObserver} will not override an already
  212. selected observer.
  213. """
  214. class TTYFile(object):
  215. def isatty(self):
  216. return True
  217. # stdout may not be a tty, so let's make sure it thinks it is
  218. self.patch(_options, "stdout", TTYFile())
  219. options = TwistOptions()
  220. options.opt_log_file("-") # stdout, a tty
  221. options.selectDefaultLogObserver()
  222. self.assertIdentical(
  223. options["fileLogObserverFactory"], textFileLogObserver
  224. )
  225. self.assertEqual(options["logFormat"], "text")
  226. def test_selectDefaultLogObserverDefaultWithoutTTY(self):
  227. """
  228. L{TwistOptions.selectDefaultLogObserver} will not override an already
  229. selected observer.
  230. """
  231. self.patchOpen()
  232. options = TwistOptions()
  233. options.opt_log_file("queso") # File, not a tty
  234. options.selectDefaultLogObserver()
  235. self.assertIdentical(
  236. options["fileLogObserverFactory"], jsonFileLogObserver
  237. )
  238. self.assertEqual(options["logFormat"], "json")
  239. def test_pluginsType(self):
  240. """
  241. L{TwistOptions.plugins} is a mapping of available plug-ins.
  242. """
  243. options = TwistOptions()
  244. plugins = options.plugins
  245. for name in plugins:
  246. self.assertIsInstance(name, str)
  247. self.assertIsInstance(plugins[name], ServiceMaker)
  248. def test_pluginsIncludeWeb(self):
  249. """
  250. L{TwistOptions.plugins} includes a C{"web"} plug-in.
  251. This is an attempt to verify that something we expect to be in the list
  252. is in there without enumerating all of the built-in plug-ins.
  253. """
  254. options = TwistOptions()
  255. self.assertIn("web", options.plugins)
  256. def test_subCommandsType(self):
  257. """
  258. L{TwistOptions.subCommands} is an iterable of tuples as expected by
  259. L{twisted.python.usage.Options}.
  260. """
  261. options = TwistOptions()
  262. for name, shortcut, parser, doc in options.subCommands:
  263. self.assertIsInstance(name, str)
  264. self.assertIdentical(shortcut, None)
  265. self.assertTrue(callable(parser))
  266. self.assertIsInstance(doc, str)
  267. def test_subCommandsIncludeWeb(self):
  268. """
  269. L{TwistOptions.subCommands} includes a sub-command for every plug-in.
  270. """
  271. options = TwistOptions()
  272. plugins = set(options.plugins)
  273. subCommands = set(
  274. name for name, shortcut, parser, doc in options.subCommands
  275. )
  276. self.assertEqual(subCommands, plugins)
  277. def test_postOptionsNoSubCommand(self):
  278. """
  279. L{TwistOptions.postOptions} raises L{UsageError} is it has no
  280. sub-command.
  281. """
  282. self.patchInstallReactor()
  283. options = TwistOptions()
  284. self.assertRaises(UsageError, options.postOptions)