PageRenderTime 60ms CodeModel.GetById 20ms RepoModel.GetById 0ms app.codeStats 1ms

/Client/src/bkr/client/wizard.py

https://github.com/beaker-project/beaker
Python | 3009 lines | 2963 code | 18 blank | 28 comment | 73 complexity | 04e8aa6e00e95032a7861879859deb99 MD5 | raw file
Possible License(s): GPL-2.0, CC-BY-SA-3.0

Large files files are truncated, but you can click here to view the full file

  1. # This program is free software; you can redistribute it and/or modify
  2. # it under the terms of the GNU General Public License as published by
  3. # the Free Software Foundation; either version 2 of the License, or
  4. # (at your option) any later version.
  5. DESCRIPTION = """Beaker Wizard is a tool which can transform that
  6. "create all the necessary files with correct names, values, and paths"
  7. boring phase of every test creation into one-line joy. For power
  8. users there is a lot of inspiration in the man page. For quick start
  9. just ``cd`` to your test package directory and simply run
  10. ``beaker-wizard``.
  11. """
  12. __doc__ = """
  13. beaker-wizard: Tool to ease the creation of a new Beaker task
  14. =============================================================
  15. .. program:: beaker-wizard
  16. Synopsis
  17. --------
  18. | :program:`beaker-wizard` [*options*] <testname> <bug>
  19. The *testname* argument should be specified as::
  20. [[[NAMESPACE/]PACKAGE/]TYPE/][PATH/]NAME
  21. which can be shortened as you need::
  22. TESTNAME
  23. TYPE/TESTNAME
  24. TYPE/PATH/TESTNAME
  25. PACKAGE/TYPE/NAME
  26. PACKAGE/TYPE/PATH/NAME
  27. NAMESPACE/PACKAGE/TYPE/NAME
  28. NAMESPACE/PACKAGE/TYPE/PATH/NAME
  29. | :program:`beaker-wizard` Makefile
  30. This form will run the Wizard in the Makefile edit mode which allows you to
  31. quickly and simply update metadata of an already existing test while trying to
  32. keep the rest of the Makefile untouched.
  33. Description
  34. -----------
  35. %(DESCRIPTION)s
  36. The beaker-wizard was designed to be flexible: it is intended not only for
  37. beginning Beaker users who will welcome questions with hints but also for
  38. experienced test writers who can make use of the extensive command-line
  39. options to push their new-test-creating productivity to the limits.
  40. For basic usage help, see Options_ below or run ``beaker-wizard -h``.
  41. For advanced features and expert usage examples, read on.
  42. Highlights
  43. ~~~~~~~~~~
  44. * provide reasonable defaults wherever possible
  45. * flexible confirmation (``--every``, ``--common``, ``--yes``)
  46. * predefined skeletons (beaker, beakerlib, simple, multihost, empty)
  47. * saved user preferences (defaults, user skeletons, licenses)
  48. * Bugzilla integration (fetch bug info, reproducers, suggest name, description)
  49. * Makefile edit mode (quick adding of bugs, limiting archs or releases...)
  50. * automated adding created files to the git repository
  51. Skeletons
  52. ~~~~~~~~~
  53. Another interesting feature is that you can save your own skeletons into
  54. the preferences file, so that you can automatically populate the new
  55. test scripts with your favourite structure.
  56. All of the test related metadata gathered by the Wizard can be expanded
  57. inside the skeletons using XML tags. For example: use ``<package/>`` for
  58. expanding into the test package name or ``<test/>`` for the full test name.
  59. The following metadata variables are available:
  60. * test namespace package type path testname description
  61. * bugs reproducers requires architectures releases version time
  62. * priority license confidential destructive
  63. * skeleton author email
  64. Options
  65. -------
  66. -h, --help show this help message and exit
  67. -V, --version display version info and quit
  68. Basic metadata:
  69. -d DESCRIPTION short description
  70. -a ARCHS architectures [All]
  71. -r RELEASES releases [All]
  72. -o PACKAGES run for packages [wizard]
  73. -q PACKAGES required packages [wizard]
  74. -t TIME test time [5m]
  75. Extra metadata:
  76. -z VERSION test version [1.0]
  77. -p PRIORITY priority [Normal]
  78. -l LICENSE license [GPLv2+]
  79. -i INTERNAL confidential [No]
  80. -u UGLY destructive [No]
  81. Author info:
  82. -n NAME your name [Petr Splichal]
  83. -m MAIL your email address [psplicha@redhat.com]
  84. Test creation specifics:
  85. -s SKELETON skeleton to use [beakerlib]
  86. -j PREFIX join the bug prefix to the testname [Yes]
  87. -f, --force force without review and overwrite existing files
  88. -w, --write write preferences to ~/.beaker_client/wizard
  89. -b, --bugzilla contact bugzilla to get bug details
  90. -g, --git add created files to the git repository
  91. Confirmation and verbosity:
  92. -v, --verbose display detailed info about every action
  93. -e, --every prompt for each and every available option
  94. -c, --common confirm only commonly used options [Default]
  95. -y, --yes yes, I'm sure, no questions, just do it!
  96. Examples
  97. --------
  98. Some brief examples::
  99. beaker-wizard overload-performance 379791
  100. regression test with specified bug and name
  101. -> /CoreOS/perl/Regression/bz379791-overload-performance
  102. beaker-wizard buffer-overflow 2008-1071 -a i386
  103. security test with specified CVE and name, i386 arch only
  104. -> /CoreOS/perl/Security/CVE-2008-1071-buffer-overflow
  105. beaker-wizard Sanity/options -y -a?
  106. sanity test with given name, ask just for architecture
  107. -> /CoreOS/perl/Sanity/options
  108. beaker-wizard Sanity/server/smoke
  109. add an optional path under test type directory
  110. -> /CoreOS/perl/Sanity/server/smoke
  111. beaker-wizard -by 1234
  112. contact bugzilla for details, no questions, just review
  113. -> /CoreOS/installer/Regression/bz1234-Swap-partition-Installer
  114. beaker-wizard -byf 2007-0455
  115. security test, no questions, no review, overwrite existing files
  116. -> /CoreOS/gd/Security/CVE-2007-0455-gd-buffer-overrun
  117. All of the previous examples assume you're in the package tests
  118. directory (e.g. ``cd git/tests/perl``). All the necessary directories and
  119. files are created under this location.
  120. Bugzilla integration
  121. ~~~~~~~~~~~~~~~~~~~~
  122. The following example creates a regression test for bug #227655.
  123. Option ``-b`` is used to contact Bugzilla to automatically fetch bug
  124. details and ``-y`` to skip unnecessary questions.
  125. ::
  126. # beaker-wizard -by 227655
  127. Contacting bugzilla...
  128. Fetching details for bz227655
  129. Examining attachments for possible reproducers
  130. Adding test.pl (simple test using Net::Config)
  131. Adding libnet.cfg (libnet.cfg test config file)
  132. Ready to create the test, please review
  133. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  134. /CoreOS/perl/Regression/bz227655-libnet-cfg-in-wrong-directory
  135. Namespace : CoreOS
  136. Package : perl
  137. Test type : Regression
  138. Relative path : None
  139. Test name : bz227655-libnet-cfg-in-wrong-directory
  140. Description : Test for bz227655 (libnet.cfg in wrong directory)
  141. Bug or CVE numbers : bz227655
  142. Reproducers to fetch : test.pl, libnet.cfg
  143. Required packages : None
  144. Architectures : All
  145. Releases : All
  146. Version : 1.0
  147. Time : 5m
  148. Priority : Normal
  149. License : GPLv2+
  150. Confidential : No
  151. Destructive : No
  152. Skeleton : beakerlib
  153. Author : Petr Splichal
  154. Email : psplicha@redhat.com
  155. [Everything OK?]
  156. Directory Regression/bz227655-libnet-cfg-in-wrong-directory created
  157. File Regression/bz227655-libnet-cfg-in-wrong-directory/PURPOSE written
  158. File Regression/bz227655-libnet-cfg-in-wrong-directory/runtest.sh written
  159. File Regression/bz227655-libnet-cfg-in-wrong-directory/Makefile written
  160. Attachment test.pl downloaded
  161. Attachment libnet.cfg downloaded
  162. Command line
  163. ~~~~~~~~~~~~
  164. The extensive command line syntax can come in handy for example
  165. when creating a bunch of sanity tests for a component. Let's
  166. create a test skeleton for each of wget's feature areas::
  167. # cd git/tests/wget
  168. # for test in download recursion rules authentication; do
  169. > beaker-wizard -yf $test -t 10m -q httpd,vsftpd \\
  170. > -d "Sanity test for $test options"
  171. > done
  172. ...
  173. /CoreOS/wget/Sanity/authentication
  174. Namespace : CoreOS
  175. Package : wget
  176. Test type : Sanity
  177. Relative path : None
  178. Test name : authentication
  179. Description : Sanity test for authentication options
  180. Bug or CVE numbers : None
  181. Reproducers to fetch : None
  182. Required packages : httpd, vsftpd
  183. Architectures : All
  184. Releases : All
  185. Version : 1.0
  186. Time : 10m
  187. Priority : Normal
  188. License : GPLv2+
  189. Confidential : No
  190. Destructive : No
  191. Skeleton : beakerlib
  192. Author : Petr Splichal
  193. Email : psplicha@redhat.com
  194. Directory Sanity/authentication created
  195. File Sanity/authentication/PURPOSE written
  196. File Sanity/authentication/runtest.sh written
  197. File Sanity/authentication/Makefile written
  198. # tree
  199. .
  200. `-- Sanity
  201. |-- authentication
  202. | |-- Makefile
  203. | |-- PURPOSE
  204. | `-- runtest.sh
  205. |-- download
  206. | |-- Makefile
  207. | |-- PURPOSE
  208. | `-- runtest.sh
  209. |-- recursion
  210. | |-- Makefile
  211. | |-- PURPOSE
  212. | `-- runtest.sh
  213. `-- rules
  214. |-- Makefile
  215. |-- PURPOSE
  216. `-- runtest.sh
  217. Notes
  218. -----
  219. If you provide an option with a "?" you will be given a list of
  220. available options and a prompt to type your choice in.
  221. For working Bugzilla integration you need ``python-bugzilla`` package installed on your system.
  222. If you are trying to access a bug with restricted access, log
  223. in to Bugzilla first with the following command::
  224. bugzilla login
  225. You will be asked for email and password and after successfully logging in a
  226. ``~/.bugzillacookies`` file will be created which then will be used
  227. in all subsequent Bugzilla queries. Logout can be performed with
  228. ``rm ~/.bugzillacookies`` ;-)
  229. Files
  230. -----
  231. All commonly used preferences can be saved into ``~/.beaker_client/wizard``.
  232. Use "write" command to save current settings when reviewing gathered
  233. test data or edit the file with you favourite editor.
  234. All options in the config file are self-explanatory. For confirm level choose
  235. one of: nothing, common or everything.
  236. Library tasks
  237. -------------
  238. The "library" skeleton can be used to create a "library task". It allows you to bundle
  239. together common functionality which may be required across multiple
  240. tasks. To learn more, see `the BeakerLib documentation for library
  241. tasks <https://fedorahosted.org/beakerlib/wiki/libraries>`__.
  242. Bugs
  243. ----
  244. If you encounter an issue or have an idea for enhancement, please `file a new bug`_.
  245. See also `open bugs`_.
  246. .. _file a new bug: https://bugzilla.redhat.com/enter_bug.cgi?product=Beaker&component=command+line&short_desc=beaker-wizard:+&status_whiteboard=BeakerWizard&assigned_to=psplicha@redhat.com
  247. .. _open bugs: https://bugzilla.redhat.com/buglist.cgi?product=Beaker&bug_status=__open__&short_desc=beaker-wizard&short_desc_type=allwordssubstr
  248. See also
  249. --------
  250. * `Beaker documentation <http://beaker-project.org/help.html>`_
  251. * `BeakerLib <https://fedorahosted.org/beakerlib>`_
  252. """ % globals()
  253. from optparse import OptionParser, OptionGroup, IndentedHelpFormatter, SUPPRESS_HELP
  254. from xml.dom.minidom import parse, parseString
  255. from datetime import date
  256. from time import sleep
  257. import subprocess
  258. import textwrap
  259. import pwd
  260. import sys
  261. import re
  262. import os
  263. # Version
  264. WizardVersion = "2.3.0"
  265. # Regular expressions
  266. RegExpPackage = re.compile("^(?![._+-])[.a-zA-Z0-9_+-]+(?<![._-])$")
  267. RegExpRhtsRequires = re.compile("^(?![._+-])[.a-zA-Z0-9_+-/()]+(?<![._-])$")
  268. RegExpPath = re.compile("^(?![/-])[a-zA-Z0-9/_-]+(?<![/-])$")
  269. RegExpTestName = re.compile("^(?!-)[a-zA-Z0-9-_]+(?<!-)$")
  270. RegExpBug = re.compile("^\d+$")
  271. RegExpBugLong = re.compile("^bz\d+$")
  272. RegExpBugPrefix = re.compile("^bz")
  273. RegExpCVE = re.compile("^\d{4}-\d{4}$")
  274. RegExpCVELong = re.compile("^CVE-\d{4}-\d{4}$")
  275. RegExpCVEPrefix = re.compile("^CVE-")
  276. RegExpAuthor = re.compile("^[a-zA-Z]+\.?( [a-zA-Z]+\.?){1,2}$")
  277. RegExpEmail = re.compile("^[a-z._-]+@[a-z.-]+$")
  278. RegExpYes = re.compile("Everything OK|y|ye|jo|ju|ja|ano|da", re.I)
  279. RegExpReproducer = re.compile("repr|test|expl|poc|demo", re.I)
  280. RegExpScript = re.compile("\.(sh|py|pl)$")
  281. RegExpMetadata = re.compile("(\$\(METADATA\):\s+Makefile.*)$", re.S)
  282. RegExpTest = re.compile("TEST=(\S+)", re.S)
  283. RegExpVersion = re.compile("TESTVERSION=([\d.]+)", re.S)
  284. # Suggested test types (these used to be enforced)
  285. SuggestedTestTypes = """Regression Performance Stress Certification
  286. Security Durations Interoperability Standardscompliance
  287. Customeracceptance Releasecriterium Crasher Tier1 Tier2
  288. Alpha KernelTier1 KernelTier2 Multihost MultihostDriver
  289. Install FedoraTier1 FedoraTier2 KernelRTTier1
  290. KernelReporting Sanity Library""".split()
  291. # Guesses
  292. GuessAuthorLogin = pwd.getpwuid(os.getuid())[0]
  293. GuessAuthorDomain = re.sub("^.*\.([^.]+\.[^.]+)$", "\\1", os.uname()[1])
  294. GuessAuthorEmail = "%s@%s" % (GuessAuthorLogin, GuessAuthorDomain)
  295. GuessAuthorName = pwd.getpwuid(os.getuid())[4]
  296. # Make sure guesses are valid values
  297. if not RegExpEmail.match(GuessAuthorEmail):
  298. GuessAuthorEmail = "your@email.com"
  299. if not RegExpAuthor.match(GuessAuthorName):
  300. GuessAuthorName = "Your Name"
  301. # Commands
  302. GitCommand="git add".split()
  303. # Constants
  304. MaxLengthSuggestedDesc = 50
  305. MaxLengthTestName = 50
  306. ReviewWidth = 22
  307. MakefileLineWidth = 17
  308. VimDictionary = "# vim: dict+=/usr/share/beakerlib/dictionary.vim cpt=.,w,b,u,t,i,k"
  309. BugzillaUrl = 'https://bugzilla.redhat.com/show_bug.cgi?id='
  310. BugzillaXmlrpc = 'https://bugzilla.redhat.com/xmlrpc.cgi'
  311. PreferencesDir = os.getenv('HOME') + "/.beaker_client"
  312. PreferencesFile = PreferencesDir + "/wizard"
  313. PreferencesTemplate = """<?xml version="1.0" ?>
  314. <wizard>
  315. <author>
  316. <name>%s</name>
  317. <email>%s</email>
  318. <confirm>common</confirm>
  319. <skeleton>beakerlib</skeleton>
  320. </author>
  321. <test>
  322. <time>5m</time>
  323. <type>Sanity</type>
  324. <prefix>Yes</prefix>
  325. <namespace>CoreOS</namespace>
  326. <priority>Normal</priority>
  327. <license>GPLv2+</license>
  328. <confidential>No</confidential>
  329. <destructive>No</destructive>
  330. </test>
  331. <licenses>
  332. <license name="GPLvX">
  333. This is GPLvX license text.
  334. </license>
  335. <license name="GPLvY">
  336. This is GPLvY license text.
  337. </license>
  338. <license name="GPLvZ">
  339. This is GPLvZ license text.
  340. </license>
  341. </licenses>
  342. <skeletons>
  343. <skeleton name="skel1" requires="gdb" rhtsrequires="library(perl/lib1) library(scl/lib2)">
  344. This is skeleton 1 example.
  345. </skeleton>
  346. <skeleton name="skel2">
  347. This is skeleton 2 example.
  348. </skeleton>
  349. <skeleton name="skel3">
  350. This is skeleton 3 example.
  351. </skeleton>
  352. </skeletons>
  353. </wizard>
  354. """ % (GuessAuthorName, GuessAuthorEmail)
  355. def wrapText(text):
  356. """ Wrapt text to fit default width """
  357. text = re.compile("\s+").sub(" ", text)
  358. return "\n".join(textwrap.wrap(text))
  359. def dedentText(text, count = 12):
  360. """ Remove leading spaces from the beginning of lines """
  361. return re.compile("\n" + " " * count).sub("\n", text)
  362. def indentText(text, count = 12):
  363. """ Insert leading spaces to the beginning of lines """
  364. return re.compile("\n").sub("\n" + " " * count, text)
  365. def shortenText(text, max = 50):
  366. """ Shorten long texts into something more usable """
  367. # if shorter, nothing to do
  368. if not text or len(text) <= max:
  369. return text
  370. # cut the text
  371. text = text[0:max+1]
  372. # remove last non complete word
  373. text = re.sub(" [^ ]*$", "", text)
  374. return text
  375. def shellEscaped(text):
  376. """
  377. Returns the text escaped for inclusion inside a shell double-quoted string.
  378. """
  379. return text.replace('\\', '\\\\')\
  380. .replace('"', r'\"')\
  381. .replace('$', r'\$')\
  382. .replace('`', r'\`')\
  383. .replace('!', r'\!')
  384. def unique(seq):
  385. """ Remove duplicates from the supplied sequence """
  386. dictionary = {}
  387. for i in seq:
  388. dictionary[i] = 1
  389. return dictionary.keys()
  390. def hr(width = 70):
  391. """ Return simple ascii horizontal rule """
  392. if width < 2: return ""
  393. return "# " + (width - 2) * "~"
  394. def comment(text, width = 70, comment = "#", top = True, bottom = True, padding = 3):
  395. """ Create nicely formated comment """
  396. result = ""
  397. # top hrule & padding
  398. if width and top: result += hr(width) + "\n"
  399. result += int(padding/3) * (comment + "\n")
  400. # prepend lines with the comment char and padding
  401. result += re.compile("^(?!#)", re.M).sub(comment + padding * " ", text)
  402. # bottom padding & hrule
  403. result += int(padding/3) * ("\n" + comment)
  404. if width and bottom: result += "\n" + hr(width)
  405. # remove any trailing spaces
  406. result = re.compile("\s+$", re.M).sub("", result)
  407. return result
  408. def dashifyText(text, allowExtraChars = ""):
  409. """ Replace all special chars with dashes, and perhaps shorten """
  410. if not text: return text
  411. # remove the rubbish from the start & end
  412. text = re.sub("^[^a-zA-Z0-9]*", "", text)
  413. text = re.sub("[^a-zA-Z0-9]*$", "", text)
  414. # replace all special chars with dashes
  415. text = re.sub("[^a-zA-Z0-9%s]+" % allowExtraChars, "-", text)
  416. return text
  417. def createNode(node, text):
  418. """ Create a child text node """
  419. # find document root
  420. root = node
  421. while root.nodeType != root.DOCUMENT_NODE:
  422. root = root.parentNode
  423. # append child text node
  424. node.appendChild(root.createTextNode(text))
  425. return node
  426. def getNode(node):
  427. """ Return node value """
  428. try: value = node.firstChild.nodeValue
  429. except: return None
  430. else: return value
  431. def setNode(node, value):
  432. """ Set node value (create a child if necessary) """
  433. try: node.firstChild.nodeValue = value
  434. except: createNode(node, value)
  435. return value
  436. def findNode(parent, tag, name = None):
  437. """ Find a child node with specified tag (and name) """
  438. try:
  439. for child in parent.getElementsByTagName(tag):
  440. if name is None or child.getAttribute("name") == name:
  441. return child
  442. except:
  443. return None
  444. def findNodeNames(node, tag):
  445. """ Return list of all name values of specified tags """
  446. list = []
  447. for child in node.getElementsByTagName(tag):
  448. if child.hasAttribute("name"):
  449. list.append(child.getAttribute("name"))
  450. return list
  451. def parentDir():
  452. """ Get parent directory name for package name suggestion """
  453. dir = re.split("/", os.getcwd())[-1]
  454. if dir == "": return "kernel"
  455. # remove the -tests suffix if present
  456. # (useful if writing tests in the package/package-tests directory)
  457. dir = re.sub("-tests$", "", dir)
  458. return dir
  459. def addToGit(path):
  460. """ Add a file or a directory to Git """
  461. try:
  462. process = subprocess.Popen(GitCommand + [path],
  463. stdout=subprocess.PIPE, stderr=subprocess.PIPE,)
  464. out, err = process.communicate()
  465. if process.wait():
  466. print "Sorry, failed to add %s to git :-(" % path
  467. print out, err
  468. sys.exit(1)
  469. except OSError:
  470. print("Unable to run %s, is %s installed?"
  471. % (" ".join(GitCommand), GitCommand[0]))
  472. sys.exit(1)
  473. def removeEmbargo(summary):
  474. return summary.replace('EMBARGOED ', '')
  475. class Preferences:
  476. """ Test's author preferences """
  477. def __init__(self, load_user_prefs=True):
  478. """ Set (in future get) user preferences / defaults """
  479. self.template = parseString(PreferencesTemplate)
  480. self.firstRun = False
  481. if load_user_prefs:
  482. self.load()
  483. else:
  484. self.xml = self.template
  485. self.parse()
  486. # XXX (ncoghlan): all of these exec invocations should be replaced with
  487. # appropriate usage of setattr and getattr. However, beaker-wizard needs
  488. # decent test coverage before embarking on that kind of refactoring...
  489. def parse(self):
  490. """ Parse values from the xml file """
  491. # parse list nodes
  492. for node in "author test licenses skeletons".split():
  493. exec("self.%s = findNode(self.xml, '%s')" % (node, node))
  494. # parse single value nodes for author
  495. for node in "name email confirm skeleton".split():
  496. exec("self.%s = findNode(self.author, '%s')" % (node, node))
  497. # if the node cannot be found get the default from template
  498. if not eval("self." + node):
  499. print "Could not find <%s> in preferences, using default" % node
  500. exec("self.%s = findNode(self.template, '%s').cloneNode(True)"
  501. % (node, node))
  502. exec("self.author.appendChild(self.%s)" % node)
  503. # parse single value nodes for test
  504. for node in "type namespace time priority confidential destructive " \
  505. "prefix license".split():
  506. exec("self.%s = findNode(self.test, '%s')" % (node, node))
  507. # if the node cannot be found get the default from template
  508. if not eval("self." + node):
  509. print "Could not find <%s> in preferences, using default" % node
  510. exec("self.%s = findNode(self.template, '%s').cloneNode(True)" % (node, node))
  511. exec("self.test.appendChild(self.%s)" % node)
  512. def load(self):
  513. """ Load user preferences (or set to defaults) """
  514. preferences_file = os.environ.get("BEAKER_WIZARD_CONF", PreferencesFile)
  515. try:
  516. self.xml = parse(preferences_file)
  517. except:
  518. if os.path.exists(preferences_file):
  519. print "I'm sorry, the preferences file seems broken.\n" \
  520. "Did you do something ugly to %s?" % preferences_file
  521. sleep(3)
  522. else:
  523. self.firstRun = True
  524. self.xml = self.template
  525. self.parse()
  526. else:
  527. try:
  528. self.parse()
  529. except:
  530. print "Failed to parse %s, falling to defaults." % preferences_file
  531. sleep(3)
  532. self.xml = self.template
  533. self.parse()
  534. def update(self, author, email, confirm, type, namespace, \
  535. time, priority, confidential, destructive, prefix, license, skeleton):
  536. """ Update preferences with current settings """
  537. setNode(self.name, author)
  538. setNode(self.email, email)
  539. setNode(self.confirm, confirm)
  540. setNode(self.type, type)
  541. setNode(self.namespace, namespace)
  542. setNode(self.time, time)
  543. setNode(self.priority, priority)
  544. setNode(self.confidential, confidential)
  545. setNode(self.destructive, destructive)
  546. setNode(self.prefix, prefix)
  547. setNode(self.license, license)
  548. setNode(self.skeleton, skeleton)
  549. def save(self):
  550. """ Save user preferences """
  551. # try to create directory
  552. try:
  553. os.makedirs(PreferencesDir)
  554. except OSError, e:
  555. if e.errno == 17:
  556. pass
  557. else:
  558. print "Cannot create preferences directory %s :-(" % PreferencesDir
  559. return
  560. # try to write the file
  561. try:
  562. file = open(PreferencesFile, "w")
  563. except:
  564. print "Cannot write to %s" % PreferencesFile
  565. else:
  566. file.write((self.xml.toxml() + "\n").encode("utf-8"))
  567. file.close()
  568. print "Preferences saved to %s" % PreferencesFile
  569. sleep(1)
  570. def getAuthor(self): return getNode(self.name)
  571. def getEmail(self): return getNode(self.email)
  572. def getConfirm(self): return getNode(self.confirm)
  573. def getType(self): return getNode(self.type)
  574. def getPackage(self): return parentDir()
  575. def getNamespace(self): return getNode(self.namespace)
  576. def getTime(self): return getNode(self.time)
  577. def getPriority(self): return getNode(self.priority)
  578. def getConfidential(self): return getNode(self.confidential)
  579. def getDestructive(self): return getNode(self.destructive)
  580. def getPrefix(self): return getNode(self.prefix)
  581. def getVersion(self): return "1.0"
  582. def getLicense(self): return getNode(self.license)
  583. def getSkeleton(self): return getNode(self.skeleton)
  584. def getLicenseContent(self, license):
  585. content = findNode(self.licenses, "license", license)
  586. if content:
  587. return re.sub("\n\s+$", "", content.firstChild.nodeValue)
  588. else:
  589. return None
  590. class Help:
  591. """ Help texts """
  592. def __init__(self, options = None):
  593. if options:
  594. # display expert usage page only
  595. if options.expert():
  596. print self.expert();
  597. sys.exit(0)
  598. # show version info
  599. elif options.ver():
  600. print self.version();
  601. sys.exit(0)
  602. def usage(self):
  603. return "beaker-wizard [options] [TESTNAME] [BUG/CVE...] or beaker-wizard Makefile"
  604. def version(self):
  605. return "beaker-wizard %s" % WizardVersion
  606. def description(self):
  607. return DESCRIPTION
  608. def expert(self):
  609. os.execv('/usr/bin/man', ['man', 'beaker-wizard'])
  610. sys.exit(1)
  611. class Makefile:
  612. """
  613. Parse values from an existing Makefile to set the initial values
  614. Used in the Makefile edit mode.
  615. """
  616. def __init__(self, options):
  617. # try to read the original Makefile
  618. self.path = options.arg[0]
  619. try:
  620. # open and read the whole content into self.text
  621. print "Reading the Makefile..."
  622. file = open(self.path)
  623. self.text = "".join(file.readlines())
  624. file.close()
  625. # substitute the old style $TEST sub-variables if present
  626. for var in "TOPLEVEL_NAMESPACE PACKAGE_NAME RELATIVE_PATH".split():
  627. m = re.search("%s=(\S+)" % var, self.text)
  628. if m: self.text = re.sub("\$\(%s\)" % var, m.group(1), self.text)
  629. # locate the metadata section
  630. print "Inspecting the metadata section..."
  631. m = RegExpMetadata.search(self.text)
  632. self.metadata = m.group(1)
  633. # parse the $TEST and $TESTVERSION
  634. print "Checking for the full test name and version..."
  635. m = RegExpTest.search(self.text)
  636. options.arg = [m.group(1)]
  637. m = RegExpVersion.search(self.text)
  638. options.opt.version = m.group(1)
  639. except:
  640. print "Failed to parse the original Makefile"
  641. sys.exit(6)
  642. # disable test name prefixing and set confirm to nothing
  643. options.opt.prefix = "No"
  644. options.opt.confirm = "nothing"
  645. # initialize non-existent options.opt.* vars
  646. options.opt.bug = options.opt.owner = options.opt.runfor = None
  647. # uknown will be used to store unrecognized metadata fields
  648. self.unknown = ""
  649. # map long fields to short versions
  650. map = {
  651. "description" : "desc",
  652. "architectures" : "archs",
  653. "testtime" : "time"
  654. }
  655. # parse info from metadata line by line
  656. print "Parsing the individual metadata..."
  657. for line in self.metadata.split("\n"):
  658. m = re.search("echo\s+[\"'](\w+):\s*(.*)[\"']", line)
  659. # skip non-@echo lines
  660. if not m: continue
  661. # read the key & value pair
  662. try: key = map[m.group(1).lower()]
  663. except: key = m.group(1).lower()
  664. # get the value, unescape escaped double quotes
  665. value = re.sub("\\\\\"", "\"", m.group(2))
  666. # skip fields known to contain variables
  667. if key in ("name", "testversion", "path"): continue
  668. # save known fields into options
  669. for data in "owner desc type archs releases time priority license " \
  670. "confidential destructive bug requires runfor".split():
  671. if data == key:
  672. # if multiple choice, extend the array
  673. if key in "archs bug releases requires runfor".split():
  674. try: exec("options.opt.%s.append(value)" % key)
  675. except: exec("options.opt.%s = [value]" % key)
  676. # otherwise just set the value
  677. else:
  678. exec("options.opt.%s = value" % key)
  679. break
  680. # save unrecognized fields to be able to restore them back
  681. else:
  682. self.unknown += "\n" + line
  683. # parse name & email
  684. m = re.search("(.*)\s+<(.*)>", options.opt.owner)
  685. if m:
  686. options.opt.author = m.group(1)
  687. options.opt.email = m.group(2)
  688. # add bug list to arg
  689. if options.opt.bug:
  690. options.arg.extend(options.opt.bug)
  691. # success
  692. print "Makefile successfully parsed."
  693. def save(self, test, version, content):
  694. # possibly update the $TEST and $TESTVERSION
  695. self.text = RegExpTest.sub("TEST=" + test, self.text)
  696. self.text = RegExpVersion.sub("TESTVERSION=" + version, self.text)
  697. # substitute the new metadata
  698. m = RegExpMetadata.search(content)
  699. self.text = RegExpMetadata.sub(m.group(1), self.text)
  700. # add unknown metadata fields we were not able to parse at init
  701. self.text = re.sub("\n\n\trhts-lint",
  702. self.unknown + "\n\n\trhts-lint", self.text)
  703. # let's write it
  704. try:
  705. file = open(self.path, "w")
  706. file.write(self.text.encode("utf-8"))
  707. file.close()
  708. except:
  709. print "Cannot write to %s" % self.path
  710. sys.exit(3)
  711. else:
  712. print "Makefile successfully written"
  713. class Options:
  714. """
  715. Class maintaining user preferences and options provided on command line
  716. self.opt ... options parsed from command line
  717. self.pref ... user preferences / defaults
  718. """
  719. def __init__(self, argv=None, load_user_prefs=True):
  720. if argv is None:
  721. argv = sys.argv
  722. self.pref = Preferences(load_user_prefs)
  723. formatter = IndentedHelpFormatter(max_help_position=40)
  724. #formatter._long_opt_fmt = "%s"
  725. # parse options
  726. parser = OptionParser(Help().usage(), formatter=formatter)
  727. parser.set_description(Help().description())
  728. # examples and help
  729. parser.add_option("-x", "--expert",
  730. dest="expert",
  731. action="store_true",
  732. help=SUPPRESS_HELP)
  733. parser.add_option("-V", "--version",
  734. dest="ver",
  735. action="store_true",
  736. help="display version info and quit")
  737. # author
  738. groupAuthor = OptionGroup(parser, "Author info")
  739. groupAuthor.add_option("-n",
  740. dest="author",
  741. metavar="NAME",
  742. help="your name [%s]" % self.pref.getAuthor())
  743. groupAuthor.add_option("-m",
  744. dest="email",
  745. metavar="MAIL",
  746. help="your email address [%s]" % self.pref.getEmail())
  747. # create
  748. groupCreate = OptionGroup(parser, "Test creation specifics")
  749. groupCreate.add_option("-s",
  750. dest="skeleton",
  751. help="skeleton to use [%s]" % self.pref.getSkeleton())
  752. groupCreate.add_option("-j",
  753. dest="prefix",
  754. metavar="PREFIX",
  755. help="join the bug prefix to the testname [%s]"
  756. % self.pref.getPrefix())
  757. groupCreate.add_option("-f", "--force",
  758. dest="force",
  759. action="store_true",
  760. help="force without review and overwrite existing files")
  761. groupCreate.add_option("-w", "--write",
  762. dest="write",
  763. action="store_true",
  764. help="write preferences to ~/.beaker_client/wizard")
  765. groupCreate.add_option("-b", "--bugzilla",
  766. dest="bugzilla",
  767. action="store_true",
  768. help="contact bugzilla to get bug details")
  769. groupCreate.add_option("-g", "--git",
  770. dest="git",
  771. action="store_true",
  772. help="add created files to the git repository")
  773. # setup default to correctly display in help
  774. defaultEverything = defaultCommon = defaultNothing = ""
  775. if self.pref.getConfirm() == "everything":
  776. defaultEverything = " [Default]"
  777. elif self.pref.getConfirm() == "common":
  778. defaultCommon = " [Default]"
  779. elif self.pref.getConfirm() == "nothing":
  780. defaultNothing = " [Default]"
  781. # confirm
  782. groupConfirm = OptionGroup(parser, "Confirmation and verbosity")
  783. groupConfirm.add_option("-v", "--verbose",
  784. dest="verbose",
  785. action="store_true",
  786. help="display detailed info about every action")
  787. groupConfirm.add_option("-e", "--every",
  788. dest="confirm",
  789. action="store_const",
  790. const="everything",
  791. help="prompt for each and every available option" + defaultEverything)
  792. groupConfirm.add_option("-c", "--common",
  793. dest="confirm",
  794. action="store_const",
  795. const="common",
  796. help="confirm only commonly used options" + defaultCommon)
  797. groupConfirm.add_option("-y", "--yes",
  798. dest="confirm",
  799. action="store_const",
  800. const="nothing",
  801. help="yes, I'm sure, no questions, just do it!" + defaultNothing)
  802. # test metadata
  803. groupMeta = OptionGroup(parser, "Basic metadata")
  804. groupMeta.add_option("-d",
  805. dest="desc",
  806. metavar="DESCRIPTION",
  807. help="short description")
  808. groupMeta.add_option("-a",
  809. dest="archs",
  810. action="append",
  811. help="architectures [All]")
  812. groupMeta.add_option("-r",
  813. dest="releases",
  814. action="append",
  815. help="releases [All]")
  816. groupMeta.add_option("-o",
  817. dest="runfor",
  818. action="append",
  819. metavar="PACKAGES",
  820. help="run for packages [%s]" % self.pref.getPackage())
  821. groupMeta.add_option("-q",
  822. dest="requires",
  823. action="append",
  824. metavar="PACKAGES",
  825. help="required packages [%s]" % self.pref.getPackage())
  826. groupMeta.add_option("-Q",
  827. dest="rhtsrequires",
  828. action="append",
  829. metavar="TEST",
  830. help="required RHTS tests or libraries")
  831. groupMeta.add_option("-t",
  832. dest="time",
  833. help="test time [%s]" % self.pref.getTime())
  834. # test metadata
  835. groupExtra = OptionGroup(parser, "Extra metadata")
  836. groupExtra.add_option("-z",
  837. dest="version",
  838. help="test version [%s]" % self.pref.getVersion())
  839. groupExtra.add_option("-p",
  840. dest="priority",
  841. help="priority [%s]" % self.pref.getPriority())
  842. groupExtra.add_option("-l",
  843. dest="license",
  844. help="license [%s]" % self.pref.getLicense())
  845. groupExtra.add_option("-i",
  846. dest="confidential",
  847. metavar="INTERNAL",
  848. help="confidential [%s]" % self.pref.getConfidential())
  849. groupExtra.add_option("-u",
  850. dest="destructive",
  851. metavar="UGLY",
  852. help="destructive [%s]" % self.pref.getDestructive())
  853. # put it together
  854. parser.add_option_group(groupMeta)
  855. parser.add_option_group(groupExtra)
  856. parser.add_option_group(groupAuthor)
  857. parser.add_option_group(groupCreate)
  858. parser.add_option_group(groupConfirm)
  859. # convert all args to unicode
  860. uniarg = []
  861. for arg in argv[1:]:
  862. uniarg.append(unicode(arg, "utf-8"))
  863. # and parse it!
  864. (self.opt, self.arg) = parser.parse_args(uniarg)
  865. # parse namespace/package/type/path/test
  866. self.opt.namespace = None
  867. self.opt.package = None
  868. self.opt.type = None
  869. self.opt.path = None
  870. self.opt.name = None
  871. self.opt.bugs = []
  872. self.makefile = False
  873. if self.arg:
  874. # if we're run in the Makefile-edit mode, parse it to get the values
  875. if re.match(".*Makefile$", self.arg[0]):
  876. self.makefile = Makefile(self)
  877. # the first arg looks like bug/CVE -> we take all args as bugs/CVE's
  878. if RegExpBug.match(self.arg[0]) or RegExpBugLong.match(self.arg[0]) or \
  879. RegExpCVE.match(self.arg[0]) or RegExpCVELong.match(self.arg[0]):
  880. self.opt.bugs = self.arg[:]
  881. # otherwise we expect bug/CVE as second and following
  882. else:
  883. self.opt.bugs = self.arg[1:]
  884. # parsing namespace/package/type/path/testname
  885. self.testinfo = self.arg[0]
  886. path_components = os.path.normpath(self.testinfo.rstrip('/')).split('/')
  887. if len(path_components) >= 1:
  888. self.opt.name = path_components.pop(-1)
  889. if len(path_components) >= 3 and re.match(Namespace().match() + '$', path_components[0]):
  890. self.opt.namespace = path_components.pop(0)
  891. self.opt.package = path_components.pop(0)
  892. self.opt.type = path_components.pop(0)
  893. elif len(path_components) >= 2 and path_components[1] in SuggestedTestTypes:
  894. self.opt.package = path_components.pop(0)
  895. self.opt.type = path_components.pop(0)
  896. elif len(path_components) >= 1:
  897. self.opt.type = path_components.pop(0)
  898. if path_components:
  899. self.opt.path = '/'.join(path_components)
  900. # try to connect to bugzilla
  901. self.bugzilla = None
  902. if self.opt.bugzilla:
  903. try:
  904. from bugzilla import Bugzilla
  905. except:
  906. print "Sorry, the bugzilla interface is not available right now, try:\n" \
  907. " yum install python-bugzilla\n" \
  908. "Use 'bugzilla login' command if you wish to access restricted bugs."
  909. sys.exit(8)
  910. else:
  911. try:
  912. print "Contacting bugzilla..."
  913. self.bugzilla = Bugzilla(url=BugzillaXmlrpc)
  914. except:
  915. print "Cannot connect to bugzilla, check your net connection."
  916. sys.exit(9)
  917. # command-line-only option interface
  918. def expert(self): return self.opt.expert
  919. def ver(self): return self.opt.ver
  920. def force(self): return self.opt.force
  921. def write(self): return self.opt.write
  922. def verbose(self): return self.pref.firstRun or self.opt.verbose
  923. def confirm(self): return self.opt.confirm or self.pref.getConfirm()
  924. # return both specified and default values for the rest of options
  925. def author(self): return [ self.opt.author, self.pref.getAuthor() ]
  926. def email(self): return [ self.opt.email, self.pref.getEmail() ]
  927. def skeleton(self): return [ self.opt.skeleton, self.pref.getSkeleton() ]
  928. def archs(self): return [ self.opt.archs, [] ]
  929. def releases(self): return [ self.opt.releases, ['-RHEL4', '-RHELClient5', '-RHELServer5'] ]
  930. def runfor(self): return [ self.opt.runfor, [self.pref.getPackage()] ]
  931. def requires(self): return [ self.opt.requires, [self.pref.getPackage()] ]
  932. def rhtsrequires(self): return [ self.opt.rhtsrequires, [] ]
  933. def time(self): return [ self.opt.time, self.pref.getTime() ]
  934. def priority(self): return [ self.opt.priority, self.pref.getPriority() ]
  935. def confidential(self): return [ self.opt.confidential, self.pref.getConfidential() ]
  936. def destructive(self): return [ self.opt.destructive, self.pref.getDestructive() ]
  937. def prefix(self): return [ self.opt.prefix, self.pref.getPrefix() ]
  938. def license(self): return [ self.opt.license, self.pref.getLicense() ]
  939. def version(self): return [ self.opt.version, self.pref.getVersion() ]
  940. def desc(self): return [ self.opt.desc, "What the test does" ]
  941. def description(self): return [ self.opt.description, "" ]
  942. def namespace(self): return [ self.opt.namespace, self.pref.getNamespace() ]
  943. def package(self): return [ self.opt.package, self.pref.getPackage() ]
  944. def type(self): return [ self.opt.type, self.pref.getType() ]
  945. def path(self): return [ self.opt.path, "" ]
  946. def name(self): return [ self.opt.name, "a-few-descriptive-words" ]
  947. def bugs(self): return [ self.opt.bugs, [] ]
  948. class Inquisitor:
  949. """
  950. Father of all Inquisitors
  951. Well he is not quite real Inquisitor, as he is very
  952. friendly and accepts any answer you give him.
  953. """
  954. def __init__(self, options = None, suggest = None):
  955. # set options & initialize
  956. self.options = options
  957. self.suggest = suggest
  958. self.common = True
  959. self.error = 0
  960. self.init()
  961. if not self.options: return
  962. # finally ask for confirmation or valid value
  963. if self.confirm or not self.valid():
  964. self.ask()
  965. def init(self):
  966. """ Initialize basic stuff """
  967. self.name = "Answer"
  968. self.question = "What is the answer to life, the universe and everything"
  969. self.description = None
  970. self.default()
  971. def default(self, optpref=None):
  972. """ Initialize default option data """
  973. # nothing to do when options not supplied
  974. if not optpref: return
  975. # initialize opt (from command line) & pref (from user preferences)
  976. (self.opt, self.pref) = optpref
  977. # set confirm flag
  978. self.confirm = self.common and self.options.confirm() != "nothing" \
  979. or not self.common and self.options.confirm() == "everything"
  980. # now set the data!
  981. # commandline option overrides both preferences & suggestion
  982. if self.opt:
  983. self.data = self.opt
  984. self.confirm = False
  985. # use suggestion if available (disabled in makefile edit mode)
  986. elif self.suggest and not self.options.makefile:
  987. self.data = self.suggest
  988. # otherwise use the default from user preferences
  989. else:
  990. self.data = self.pref
  991. # reset the user preference if it's not a valid value
  992. # (to prevent suggestions like: x is not valid what about x?)
  993. if not self.valid():
  994. self.pref = "something else"
  995. def defaultify(self):
  996. """ Set data to default/preferred value """
  997. self.data = self.pref
  998. def normalize(self):
  999. """ Remove trailing and double spaces """
  1000. if not self.data: return
  1001. self.data = re.sub("^\s*", "", self.data)
  1002. self.data = re.sub("\s*$", "", self.data)
  1003. self.data = re.sub("\s+", " ", self.data)
  1004. def read(self):
  1005. """ Read an answer from user """
  1006. try:
  1007. answer = unicode(sys.stdin.readline().strip(), "utf-8")
  1008. except KeyboardInterrupt:
  1009. print "\nOk, finishing for now. See you later ;-)"
  1010. sys.exit(4)
  1011. # if just enter pressed, we leave self.data as it is (confirmed)
  1012. if answer != "":
  1013. # append the data if the answer starts with a "+"
  1014. m = re.search("^\+\s*(.*)", answer)
  1015. if m and type(self.data) is list:
  1016. self.data.append(m.group(1))
  1017. else:
  1018. self.data = answer
  1019. self.normalize()
  1020. def heading(self):
  1021. """ Display nice heading with question """
  1022. print "\n" + self.question + "\n" + 77 * "~";
  1023. def value(self):
  1024. """ Return current value """
  1025. return self.data
  1026. def show(self, data = None):
  1027. """ Return current value nicely formatted (redefined in children)"""
  1028. if not data: data = self.data
  1029. if data == "": return "None"
  1030. return data
  1031. def singleName(self):
  1032. """ Return the name in lowercase singular (for error reporting) """
  1033. return re.sub("s$", "", self.name.lower())
  1034. def matchName(self, text):
  1035. """ Return true if the text matches inquisitor's name """
  1036. # remove any special characters from the search string
  1037. text = re.sub("[^\w\s]", "", text)
  1038. return re.search(text, self.name, re.I)
  1039. def describe(self):
  1040. if self.description is not None:
  1041. print wrapText(self.description)
  1042. def format(self, data = None):
  1043. """ Display in a nicely indented style """
  1044. print self.name.rjust(ReviewWidth), ":", (data or self.show())
  1045. def formatMakefileLine(self, name = None, value = None):
  1046. """ Format testinfo line for Makefile inclusion """
  1047. if not (self.value() or value): return ""
  1048. return '\n @echo "%s%s" >> $(METADATA)' % (
  1049. ((name or self.name) + ":").ljust(MakefileLineWidth),
  1050. shellEscaped(value or self.value()))
  1051. def valid(self):
  1052. """ Return true when provided value is a valid answer """
  1053. return self.data not in ["?", ""]
  1054. def suggestion(self):
  1055. """ Provide user with a suggestion or detailed description """
  1056. # if current data is valid, offer is as a suggestion
  1057. if self.valid():
  1058. if self.options.verbose(): self.describe()
  1059. return "%s?" % self.show()
  1060. # otherwise suggest the default value
  1061. else:
  1062. bad = self.data
  1063. self.defaultify()
  1064. # regular suggestion (no question mark for help)
  1065. if bad is None or "".join(bad) != "?":
  1066. self.error += 1
  1067. if self.error > 1 or self.options.verbose(): self.describe()
  1068. return "%s is not a valid %s, what about %s?" \
  1069. % (self.show(bad), self.singleName(), self.show(self.pref))
  1070. # we got question mark ---> display description to help
  1071. else:
  1072. self.describe()
  1073. return "%s?" % self.show()
  1074. def ask(self, force = False, suggest = None):
  1075. """ Ask for valid value """
  1076. if force: self.confirm = True
  1077. if suggest: self.data = suggest
  1078. self.heading()
  1079. # keep asking until we get sane answer
  1080. while self.confirm or not self.valid():
  1081. sys.stdout.write("[%s] " % self.suggestion().encode("utf-8"))
  1082. self.read()
  1083. self.confirm = False
  1084. def edit(self, suggest = None):
  1085. """ Edit = force to ask again
  1086. returns true if changes were made """
  1087. before = self.data
  1088. self.ask(force = True, suggest = suggest)
  1089. return self.data != before
  1090. class SingleChoice(Inquisitor):
  1091. """ This Inquisitor accepts just one value from the given list """
  1092. def init(self):
  1093. self.name = "SingleChoice"
  1094. self.question = "Give a valid answer from the list"
  1095. self.description = "Supply a single value from the list above."
  1096. self.list = ["list", "of", "valid", "values"]
  1097. self.default()
  1098. def propose(self):
  1099. """ Try to find nearest match in the list"""
  1100. if self.data == "?": return
  1101. for item in self.list:
  1102. if re.search(self.data, item, re.I):
  1103. self.pref = item
  1104. return
  1105. def valid(self):
  1106. if self.data in self.list:
  1107. return True
  1108. else:
  1109. self.propose()
  1110. return False
  1111. def heading(self):
  1112. Inquisitor.heading(self)
  1113. if self.list: print wrapText("Possible values: " + ", ".join(self.list))
  1114. class YesNo(SingleChoice):
  1115. """ Inquisitor expecting only two obvious answers """
  1116. def init(self):
  1117. self.name = "Yes or No"
  1118. self.question = "Are you sure?"
  1119. self.description = "All you need to say is simply 'Yes,' or 'No'; \
  1120. anything beyond this comes from the evil one."
  1121. self.list = ["Yes", "No"]
  1122. self.default()
  1123. def normalize(self):
  1124. """ Recognize yes/no abbreviations """
  1125. if not self.data: return
  1126. self.data = re.sub("^y.*$", "Yes", self.data, re.I)
  1127. self.data = re.sub("^n.*$", "No", self.data, re.I)
  1128. def formatMakefileLine(self, name = None, value = None):
  1129. """ Format testinfo line for Makefile inclusion """
  1130. # testinfo requires lowercase yes/no
  1131. return Inquisitor.formatMakefileLine(self,
  1132. name = name, value = self.data.lower())
  1133. def valid(self):
  1134. self.normalize()
  1135. return SingleChoice.valid(self)
  1136. class MultipleChoice(SingleChoice):
  1137. """ This Inquisitor accepts more values but only from the given list """
  1138. def init(self):
  1139. self.name = "MultipleChoice"
  1140. self.question = "Give one or more values from the list"
  1141. self.description = "You can supply more values separated with space or comma\n"\
  1142. "but they all must be from the list above."
  1143. self.list = ["list", "of", "valid", "values"]
  1144. self.emptyListMeaning = "None"
  1145. self.sort = True
  1146. self.default()
  1147. def default(self, optpref):
  1148. # initialize opt & pref
  1149. (self.opt, self.pref) = optpref
  1150. # set confirm flag
  1151. self.confirm = self.common and self.options.confirm() != "nothing" \
  1152. or not self.common and self.options.confirm() == "everything"
  1153. # first initialize data as an empty list
  1154. self.data = []
  1155. # append possible suggestion to the data (disabled in makefile edit mode)
  1156. if self.suggest and

Large files files are truncated, but you can click here to view the full file