PageRenderTime 66ms CodeModel.GetById 15ms 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
  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 not self.options.makefile:
  1157. self.data.append(self.suggest)
  1158. # add items obtained from the command line
  1159. if self.opt:
  1160. self.data.extend(self.opt)
  1161. self.confirm = False
  1162. # default preferences used only if still no data obtained
  1163. if not self.data:
  1164. self.data.extend(self.pref)
  1165. self.listify()
  1166. def defaultify(self):
  1167. self.data = self.pref[:]
  1168. self.listify()
  1169. def listify(self):
  1170. # make sure data is list
  1171. if type(self.data) is not list:
  1172. # special value "none" means an empty list
  1173. if self.data.lower() == "none":
  1174. self.data = []
  1175. return
  1176. # depending on emptyListMeaning "all" can mean
  1177. elif self.data.lower() == "all":
  1178. # no restrictions (releases, archs)
  1179. if self.emptyListMeaning == "All":
  1180. self.data = []
  1181. # all items (reproducers)
  1182. else:
  1183. self.data = self.list[:]
  1184. return
  1185. # otherwise just listify
  1186. else:
  1187. self.data = [ self.data ]
  1188. # expand comma/space separated items
  1189. result = []
  1190. for item in self.data:
  1191. # strip trailing separators
  1192. item = re.sub('[ ,]*$', '', item)
  1193. # split on spaces and commas
  1194. result.extend(re.split('[ ,]+', item))
  1195. self.data = result
  1196. # let's make data unique and sorted
  1197. if self.sort:
  1198. self.data = unique(self.data)
  1199. self.data.sort()
  1200. def normalize(self):
  1201. """ Parse input into a list """
  1202. self.listify()
  1203. def showItem(self, item):
  1204. return item
  1205. def formatMakefileLine(self, name = None, value = None):
  1206. """ Format testinfo line for Makefile inclusion """
  1207. # for multiple choice we produce values joined by spaces
  1208. return Inquisitor.formatMakefileLine(self,
  1209. name = name, value = " ".join(self.data))
  1210. def show(self, data = None):
  1211. if data is None: data = self.data
  1212. if not data: return self.emptyListMeaning
  1213. return ", ".join(map(self.showItem, data))
  1214. def propose(self):
  1215. """ Try to find nearest matches in the list"""
  1216. if self.data[0] == "?": return
  1217. result = []
  1218. try:
  1219. for item in self.list:
  1220. if re.search(self.data[0], item, re.I):
  1221. result.append(item)
  1222. except:
  1223. pass
  1224. if result:
  1225. self.pref = result[:]
  1226. def validItem(self, item):
  1227. return item in self.list
  1228. def valid(self):
  1229. for item in self.data:
  1230. if not self.validItem(item):
  1231. self.data = [item]
  1232. self.propose()
  1233. return False
  1234. return True
  1235. # TODO: Make the licensing organisation configurable
  1236. LICENSE_ORGANISATION = "Red Hat, Inc"
  1237. GPLv2_ONLY_LICENSE = ("""Copyright (c) %s %s.
  1238. This copyrighted material is made available to anyone wishing
  1239. to use, modify, copy, or redistribute it subject to the terms
  1240. and conditions of the GNU General Public License version 2.
  1241. This program is distributed in the hope that it will be
  1242. useful, but WITHOUT ANY WARRANTY; without even the implied
  1243. warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
  1244. PURPOSE. See the GNU General Public License for more details.
  1245. You should have received a copy of the GNU General Public
  1246. License along with this program; if not, write to the Free
  1247. Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
  1248. Boston, MA 02110-1301, USA."""
  1249. % (date.today().year, LICENSE_ORGANISATION))
  1250. GPLv2_OR_LATER_LICENSE = ("""Copyright (c) %s %s.
  1251. This program is free software: you can redistribute it and/or
  1252. modify it under the terms of the GNU General Public License as
  1253. published by the Free Software Foundation, either version 2 of
  1254. the License, or (at your option) any later version.
  1255. This program is distributed in the hope that it will be
  1256. useful, but WITHOUT ANY WARRANTY; without even the implied
  1257. warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
  1258. PURPOSE. See the GNU General Public License for more details.
  1259. You should have received a copy of the GNU General Public License
  1260. along with this program. If not, see http://www.gnu.org/licenses/."""
  1261. % (date.today().year, LICENSE_ORGANISATION))
  1262. GPLv3_OR_LATER_LICENSE = ("""Copyright (c) %s %s.
  1263. This program is free software: you can redistribute it and/or
  1264. modify it under the terms of the GNU General Public License as
  1265. published by the Free Software Foundation, either version 3 of
  1266. the License, or (at your option) any later version.
  1267. This program is distributed in the hope that it will be
  1268. useful, but WITHOUT ANY WARRANTY; without even the implied
  1269. warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
  1270. PURPOSE. See the GNU General Public License for more details.
  1271. You should have received a copy of the GNU General Public License
  1272. along with this program. If not, see http://www.gnu.org/licenses/."""
  1273. % (date.today().year, LICENSE_ORGANISATION))
  1274. PROPRIETARY_LICENSE_TEMPLATE = ("""Copyright (c) %s %s. All rights reserved.
  1275. %%s"""
  1276. % (date.today().year, LICENSE_ORGANISATION))
  1277. DEFINED_LICENSES = {
  1278. # Annoyingly, the bare "GPLv2" and "GPLv3" options differ in whether or not
  1279. # they include the "or later" clause. Unfortunately, changing it now could
  1280. # result in GPLv3 tests intended to be GPLv3+ getting mislabeled.
  1281. "GPLv2" : GPLv2_ONLY_LICENSE,
  1282. "GPLv3" : GPLv3_OR_LATER_LICENSE,
  1283. # The GPLvX+ variants consistently use the "or later" phrasing
  1284. "GPLv2+" : GPLv2_OR_LATER_LICENSE,
  1285. "GPLv3+" : GPLv3_OR_LATER_LICENSE,
  1286. "other" : PROPRIETARY_LICENSE_TEMPLATE,
  1287. }
  1288. class License(Inquisitor):
  1289. """ License to be included in test files """
  1290. def init(self):
  1291. self.name = "License"
  1292. self.question = "What licence should be used?"
  1293. self.description = "Just supply a license GPLv2+, GPLv3+, ..."
  1294. self.common = False
  1295. self.default(self.options.license())
  1296. self.licenses = DEFINED_LICENSES
  1297. def get(self):
  1298. """ Return license corresponding to user choice """
  1299. if self.data != "other" and self.data in self.licenses.keys():
  1300. return dedentText(self.licenses[self.data])
  1301. else:
  1302. license = self.options.pref.getLicenseContent(self.data)
  1303. if license: # user defined license from preferences
  1304. return dedentText(self.licenses["other"] % (
  1305. license,), count = 12)
  1306. else: # anything else
  1307. return dedentText(self.licenses["other"] % (
  1308. "PROVIDE YOUR LICENSE TEXT HERE.",))
  1309. class Time(Inquisitor):
  1310. """ Time for test to run """
  1311. def init(self):
  1312. self.name = "Time"
  1313. self.question = "Time for test to run"
  1314. self.description = """The time must be in format [1-99][m|h|d] for 1-99
  1315. minutes/hours/days (e.g. 3m, 2h, 1d)"""
  1316. self.default(self.options.time())
  1317. def valid(self):
  1318. m = re.match("^(\d{1,2})[mhd]$", self.data)
  1319. return m is not None and int(m.group(1)) > 0
  1320. class Version(Inquisitor):
  1321. """ Time for test to run """
  1322. def init(self):
  1323. self.name = "Version"
  1324. self.question = "Version of the test"
  1325. self.description = "Must be in the format x.y"
  1326. self.common = False
  1327. self.default(self.options.version())
  1328. def valid(self):
  1329. return re.match("^\d+\.\d+$", self.data)
  1330. class Priority(SingleChoice):
  1331. """ Test priority """
  1332. def init(self):
  1333. self.name = "Priority"
  1334. self.question = "Priority"
  1335. self.description = "Test priority for scheduling purposes"
  1336. self.common = False
  1337. self.list = "Low Medium Normal High Manual".split()
  1338. self.default(self.options.priority())
  1339. class Confidential(YesNo):
  1340. """ Confidentiality flag """
  1341. def init(self):
  1342. self.name = "Confidential"
  1343. self.question = "Confidential"
  1344. self.description = "Should the test be kept internal?"
  1345. self.common = False
  1346. self.list = ["Yes", "No"]
  1347. self.default(self.options.confidential())
  1348. def singleName(self):
  1349. return "confidentiality flag"
  1350. class Destructive(YesNo):
  1351. """ Destructivity flag """
  1352. def init(self):
  1353. self.name = "Destructive"
  1354. self.question = "Destructive"
  1355. self.description = "Is it such an ugly test that it can break the system?"
  1356. self.common = False
  1357. self.list = ["Yes", "No"]
  1358. self.default(self.options.destructive())
  1359. def singleName(self):
  1360. return "destructivity flag"
  1361. class Prefix(YesNo):
  1362. """ Bug number prefix """
  1363. def init(self):
  1364. self.name = "Prefix the test name"
  1365. self.question = "Add the bug number to the test name?"
  1366. self.description = "Should we prefix the test name with the bug/CVE number?"
  1367. self.common = False
  1368. self.list = ["Yes", "No"]
  1369. self.default(self.options.prefix())
  1370. def singleName(self):
  1371. return "prefix choice"
  1372. class Releases(MultipleChoice):
  1373. """ List of releases the test should run on """
  1374. def init(self):
  1375. self.name = "Releases"
  1376. self.question = "Releases (choose one or more or \"all\")"
  1377. self.description = """One or more values separated with space or comma
  1378. or "all" for no limitaion. You can also use minus sign for excluding
  1379. a specific release (-RHEL4)"""
  1380. self.list = "RHEL2.1 RHEL3 RHEL4 RHELServer5 RHELClient5".split()
  1381. self.list += ["RHEL{0}".format(id) for id in range(6, 9)]
  1382. self.list += "FC4 FC5 FC6".split()
  1383. self.list += ["F{0}".format(release) for release in range(7, 28)]
  1384. self.sort = True
  1385. self.common = False
  1386. self.emptyListMeaning = "All"
  1387. self.default(self.options.releases())
  1388. def validItem(self, item):
  1389. item = re.sub("^-","", item)
  1390. return item in self.list
  1391. class Architectures(MultipleChoice):
  1392. """ List of architectures the test should run on """
  1393. def init(self):
  1394. self.name = "Architectures"
  1395. self.question = "Architectures (choose one or more or \"all\")"
  1396. self.description = "You can supply more values separated with space or comma\n"\
  1397. "but they all must be from the list of possible values above."
  1398. self.list = "i386 x86_64 ia64 ppc ppc64 ppc64le s390 s390x aarch64".split()
  1399. self.sort = True
  1400. self.common = False
  1401. self.emptyListMeaning = "All"
  1402. self.default(self.options.archs())
  1403. class Namespace(SingleChoice):
  1404. """ Namespace"""
  1405. def init(self):
  1406. self.name = "Namespace"
  1407. self.question = "Namespace"
  1408. self.description = "Provide a root namespace for the test."
  1409. self.list = """distribution installation kernel desktop tools CoreOS
  1410. cluster rhn examples performance ISV virt""".split()
  1411. if self.options: self.default(self.options.namespace())
  1412. def match(self):
  1413. """ Return regular expression matching valid data """
  1414. return "(" + "|".join(self.list) + ")"
  1415. class Package(Inquisitor):
  1416. """ Package for which the test is written """
  1417. def init(self):
  1418. self.name = "Package"
  1419. self.question = "What package is this test for?"
  1420. self.description = "Supply a package name (without version or release number)"
  1421. self.common = False
  1422. self.default(self.options.package())
  1423. def valid(self):
  1424. return RegExpPackage.match(self.data)
  1425. class Type(Inquisitor):
  1426. """ Test type """
  1427. def init(self):
  1428. self.name = "Test type"
  1429. self.question = "What is the type of test?"
  1430. self.description = "Specify the type of the test. Hints above."
  1431. self.proposed = 0
  1432. self.proposedname = ""
  1433. self.list = SuggestedTestTypes
  1434. self.dirs = [os.path.join(o) for o in os.listdir('.') if os.path.isdir(os.path.join('.',o)) and not o.startswith('.')]
  1435. if self.options: self.default(self.options.type())
  1436. def heading(self):
  1437. Inquisitor.heading(self)
  1438. print wrapText("Recommended values: " + ", ".join(sorted(self.dirs)))
  1439. print wrapText("Possible values: " + ", ".join(self.list))
  1440. def propose(self):
  1441. """ Try to find nearest match in the list"""
  1442. self.proposed = 1
  1443. self.proposedname = self.data
  1444. self.description = "Type '%s' does not exist. Confirm creating a new type." % self.proposedname
  1445. self.describe()
  1446. for item in self.list:
  1447. if re.search(self.data, item, re.I):
  1448. self.pref = item
  1449. return
  1450. def suggestSkeleton(self):
  1451. """ For multihost tests and library suggest proper skeleton """
  1452. if self.data == "Multihost":
  1453. return "multihost"
  1454. elif self.data == "Library":
  1455. return "library"
  1456. def valid(self):
  1457. if self.data in self.list or self.data in self.dirs or (self.proposed == 1 and self.proposedname == self.data):
  1458. return True
  1459. else:
  1460. self.propose()
  1461. return False
  1462. class Path(Inquisitor):
  1463. """ Relative path to test """
  1464. def init(self):
  1465. self.name = "Relative path"
  1466. self.question = "Relative path under test type"
  1467. self.description = """Path can be used to organize tests
  1468. for complex packages, e.g. 'server' part in
  1469. /CoreOS/mysql/Regression/server/bz123456-some-test.
  1470. (You can also use dir/subdir for deeper nesting.
  1471. Use "none" for no path.)"""
  1472. self.common = False
  1473. self.default(self.options.path())
  1474. def valid(self):
  1475. return (self.data is None or self.data == ""
  1476. or RegExpPath.match(self.data))
  1477. def normalize(self):
  1478. """ Replace none keyword with real empty path """
  1479. Inquisitor.normalize(self)
  1480. if self.data and re.match('none', self.data, re.I):
  1481. self.data = None
  1482. def value(self):
  1483. if self.data:
  1484. return "/" + self.data
  1485. else:
  1486. return ""
  1487. class Bugs(MultipleChoice):
  1488. """ List of bugs/CVE's related to the test """
  1489. def init(self):
  1490. self.name = "Bug or CVE numbers"
  1491. self.question = "Bugs or CVE's related to the test"
  1492. self.description = """Supply one or more bug or CVE numbers
  1493. (e.g. 123456 or 2009-7890). Use the '+' sign to add
  1494. the bugs instead of replacing the current list."""
  1495. self.list = []
  1496. self.sort = False
  1497. self.emptyListMeaning = "None"
  1498. self.bug = None
  1499. self.default(self.options.bugs())
  1500. self.reproducers = Reproducers(self.options)
  1501. def validItem(self, item):
  1502. return RegExpBug.match(item) \
  1503. or RegExpCVE.match(item)
  1504. def valid(self):
  1505. # let's remove possible (otherwise harmless) bug/CVE prefixes
  1506. for i in range(len(self.data)):
  1507. self.data[i] = re.sub(RegExpBugPrefix, "", self.data[i])
  1508. self.data[i] = re.sub(RegExpCVEPrefix, "", self.data[i])
  1509. # and do the real validation
  1510. return MultipleChoice.valid(self)
  1511. def showItem(self, item):
  1512. if RegExpBug.match(item):
  1513. return "BZ#" + item
  1514. elif RegExpCVE.match(item):
  1515. return "CVE-" + item
  1516. else:
  1517. return item
  1518. def formatMakefileLine(self, name = None, value = None):
  1519. """ Format testinfo line for Makefile inclusion """
  1520. list = []
  1521. # filter bugs only (CVE's are not valid for testinfo.desc)
  1522. for item in self.data:
  1523. if RegExpBug.match(item):
  1524. list.append(item)
  1525. if not list: return ""
  1526. return Inquisitor.formatMakefileLine(self, name = "Bug", value = " ".join(list))
  1527. def getFirstBug(self):
  1528. """ Return first bug/CVE if there is some """
  1529. if self.data: return self.showItem(self.data[0])
  1530. def fetchBugDetails(self):
  1531. """ Fetch details of the first bug from Bugzilla """
  1532. if self.options.bugzilla and self.data:
  1533. # use CVE prefix when searching for CVE's in bugzilla
  1534. if RegExpCVE.match(self.data[0]):
  1535. bugid = "CVE-" + self.data[0]
  1536. else:
  1537. bugid = self.data[0]
  1538. # contact bugzilla and try to fetch the details
  1539. try:
  1540. print "Fetching details for", self.showItem(self.data[0])
  1541. self.bug = self.options.bugzilla.getbug(bugid)
  1542. except Exception, e:
  1543. if re.search('not authorized to access', str(e)):
  1544. print "Sorry, %s has a restricted access.\n"\
  1545. "Use 'bugzilla login' command to set up cookies "\
  1546. "then try again." % self.showItem(self.data[0])
  1547. else:
  1548. print "Sorry, could not get details for %s\n%s" % (bugid, e)
  1549. sleep(3)
  1550. return
  1551. # successfully fetched
  1552. else:
  1553. # for CVE's add the bug id to the list of bugs
  1554. if RegExpCVE.match(self.data[0]):
  1555. self.data.append(str(self.bug.id))
  1556. # else investigate for possible CVE alias
  1557. elif self.bug.alias and RegExpCVELong.match(self.bug.alias[0]):
  1558. cve = re.sub("CVE-", "", self.bug.alias[0])
  1559. self.data[:0] = [cve]
  1560. # and search attachments for possible reproducers
  1561. if self.bug:
  1562. self.reproducers.find(self.bug)
  1563. return True
  1564. def getSummary(self):
  1565. """ Return short summary fetched from bugzilla """
  1566. if self.bug:
  1567. return re.sub("CVE-\d{4}-\d{4}\s*", "", removeEmbargo(self.bug.summary))
  1568. def getComponent(self):
  1569. """ Return bug component fetched from bugzilla """
  1570. if self.bug:
  1571. component = self.bug.component
  1572. # Use the first component if component list given
  1573. if isinstance(component, list):
  1574. component = component[0]
  1575. # Ignore generic CVE component "vulnerability"
  1576. if component != 'vulnerability':
  1577. return component
  1578. def getLink(self):
  1579. """ Return URL of the first bug """
  1580. if self.data:
  1581. if RegExpCVE.match(self.data[0]):
  1582. return "%sCVE-%s" % (BugzillaUrl, self.data[0])
  1583. else:
  1584. return BugzillaUrl + self.data[0]
  1585. def suggestType(self):
  1586. """ Guess test type according to first bug/CVE """
  1587. if self.data:
  1588. if RegExpBug.match(self.data[0]):
  1589. return "Regression"
  1590. elif RegExpCVE.match(self.data[0]):
  1591. return "Security"
  1592. def suggestConfidential(self):
  1593. """ If the first bug is a CVE, suggest as confidential """
  1594. if self.data and RegExpCVE.match(self.data[0]):
  1595. return "Yes"
  1596. else:
  1597. return None
  1598. def suggestTestName(self):
  1599. """ Suggest testname from bugzilla summary """
  1600. return dashifyText(shortenText(self.getSummary(), MaxLengthTestName))
  1601. def suggestDescription(self):
  1602. """ Suggest short description from bugzilla summary """
  1603. if self.getSummary():
  1604. return "Test for %s (%s)" % (
  1605. self.getFirstBug(),
  1606. shortenText(re.sub(":", "", self.getSummary()),
  1607. max=MaxLengthSuggestedDesc))
  1608. def formatBugDetails(self):
  1609. """ Put details fetched from Bugzilla into nice format for PURPOSE file """
  1610. if not self.bug:
  1611. return ""
  1612. else:
  1613. return "Bug summary: %s\nBugzilla link: %s\n" % (
  1614. self.getSummary(), self.getLink())
  1615. class Name(Inquisitor):
  1616. """ Test name """
  1617. def init(self):
  1618. self.name = "Test name"
  1619. self.question = "Test name"
  1620. self.description = """Use few, well chosen words describing
  1621. what the test does. Special chars will be automatically
  1622. converted to dashes."""
  1623. self.default(self.options.name())
  1624. self.data = dashifyText(self.data, allowExtraChars="_")
  1625. self.bugs = Bugs(self.options)
  1626. self.bugs.fetchBugDetails()
  1627. # suggest test name (except when supplied on command line)
  1628. if self.bugs.suggestTestName() and not self.opt:
  1629. self.data = self.bugs.suggestTestName()
  1630. self.prefix = Prefix(self.options)
  1631. def normalize(self):
  1632. """ Add auto-dashify function for name editing """
  1633. if not self.data == "?":
  1634. # when editing the test name --- dashify, but allow
  1635. # using underscore if the user really wants it
  1636. self.data = dashifyText(self.data, allowExtraChars="_")
  1637. def valid(self):
  1638. return self.data is not None and RegExpTestName.match(self.data)
  1639. def value(self):
  1640. """ Return test name (including bug/CVE number) """
  1641. bug = self.bugs.getFirstBug()
  1642. if bug and self.prefix.value() == "Yes":
  1643. return bug.replace('BZ#','bz') + "-" + self.data
  1644. else:
  1645. return self.data
  1646. def format(self, data = None):
  1647. """ When formatting let's display with bug/CVE numbers """
  1648. Inquisitor.format(self, self.value())
  1649. class Reproducers(MultipleChoice):
  1650. """ Possible reproducers from Bugzilla """
  1651. def init(self):
  1652. self.name = "Reproducers to fetch"
  1653. self.question = "Which Bugzilla attachments do you wish to download?"
  1654. self.description = """Wizard can download Bugzilla attachments for you.
  1655. It suggests those which look like reproducers, but you can pick
  1656. the right attachments manually as well."""
  1657. self.bug = None
  1658. self.list = []
  1659. self.sort = True
  1660. self.emptyListMeaning = "None"
  1661. self.common = False
  1662. self.default([[], []])
  1663. self.confirm = False
  1664. def singleName(self):
  1665. return "reproducer"
  1666. def find(self, bug):
  1667. """ Get the list of all attachments (except patches and obsolotes)"""
  1668. if not bug or not bug.attachments:
  1669. return False
  1670. # remember the bug & empty the lists
  1671. self.bug = bug
  1672. self.list = []
  1673. self.pref = []
  1674. self.data = []
  1675. # Provide "None" as a possible choice for attachment download
  1676. self.list.append("None")
  1677. print "Examining attachments for possible reproducers"
  1678. for attachment in self.bug.attachments:
  1679. # skip obsolete and patch attachments
  1680. is_patch = attachment.get("is_patch", attachment.get("ispatch"))
  1681. filename = attachment.get("file_name", attachment.get("filename"))
  1682. is_obsolete = attachment.get(
  1683. "is_obsolete", attachment.get("isobsolete"))
  1684. if is_patch == 0 and is_obsolete == 0:
  1685. self.list.append(filename)
  1686. # add to suggested attachments if it looks like a reproducer
  1687. if RegExpReproducer.search(attachment['description']) or \
  1688. RegExpReproducer.search(filename):
  1689. self.data.append(filename)
  1690. self.pref.append(filename)
  1691. print "Adding",
  1692. else:
  1693. print "Skipping",
  1694. print "%s (%s)" % (filename, attachment['description'])
  1695. sleep(1)
  1696. def download(self, path):
  1697. """ Download selected reproducers """
  1698. if not self.bug:
  1699. return False
  1700. for attachment in self.bug.attachments:
  1701. attachment_filename = attachment.get(
  1702. "file_name", attachment.get("filename"))
  1703. is_obsolete = attachment.get(
  1704. "is_obsolete", attachment.get("isobsolete"))
  1705. if attachment_filename in self.data and is_obsolete == 0:
  1706. print "Attachment", attachment_filename,
  1707. try:
  1708. dirfiles = os.listdir(path)
  1709. filename = path + "/" + attachment_filename
  1710. remote = self.options.bugzilla.openattachment(
  1711. attachment['id'])
  1712. # rename the attachment if it has the same name as one
  1713. # of the files in the current directory
  1714. if attachment_filename in dirfiles:
  1715. print "- file already exists in {0}/".format(path)
  1716. new_name = ""
  1717. while new_name == "":
  1718. print "Choose a new filename for the attachment: ",
  1719. new_name = unicode(
  1720. sys.stdin.readline().strip(), "utf-8")
  1721. filename = path + "/" + new_name
  1722. local = open(filename, 'w')
  1723. local.write(remote.read())
  1724. remote.close()
  1725. local.close()
  1726. # optionally add to the git repository
  1727. if self.options.opt.git:
  1728. addToGit(filename)
  1729. addedToGit = ", added to git"
  1730. else:
  1731. addedToGit = ""
  1732. except:
  1733. print "download failed"
  1734. print "python-bugzilla-0.5 or higher required"
  1735. sys.exit(5)
  1736. else:
  1737. print "downloaded" + addedToGit
  1738. class RunFor(MultipleChoice):
  1739. """ List of packages which this test should be run for """
  1740. def init(self):
  1741. self.name = "Run for packages"
  1742. self.question = "Run for packages"
  1743. self.description = """Provide a list of packages which this test should
  1744. be run for. It's a good idea to add dependent packages here."""
  1745. self.list = []
  1746. self.sort = True
  1747. self.emptyListMeaning = "None"
  1748. self.common = False
  1749. self.default(self.options.runfor())
  1750. def validItem(self, item):
  1751. return RegExpPackage.match(item)
  1752. class Requires(MultipleChoice):
  1753. """ List of packages which should be installed on test system """
  1754. def init(self):
  1755. self.name = "Required packages"
  1756. self.question = "Requires: packages which test depends on"
  1757. self.description = """Just write a list of package names
  1758. which should be automatically installed on the test system."""
  1759. self.list = []
  1760. self.sort = True
  1761. self.emptyListMeaning = "None"
  1762. self.common = False
  1763. self.default(self.options.requires())
  1764. def validItem(self, item):
  1765. return RegExpPackage.match(item)
  1766. class RhtsRequires(MultipleChoice):
  1767. """ List of other RHTS tests or libraries which this test requires """
  1768. def init(self):
  1769. self.name = "Required RHTS tests/libraries"
  1770. self.question = "RhtsRequires: other tests or libraries required by " \
  1771. "this one, e.g. test(/mytests/common) or library(mytestlib)"
  1772. self.description = """Write a list of RPM dependencies which should be
  1773. installed by the package manager. Other tasks provide test(/task/name)
  1774. and libraries provide library(name)."""
  1775. self.list = []
  1776. self.sort = True
  1777. self.emptyListMeaning = "None"
  1778. self.common = False
  1779. self.default(self.options.rhtsrequires())
  1780. def validItem(self, item):
  1781. return RegExpRhtsRequires.match(item)
  1782. class Skeleton(SingleChoice):
  1783. """ Skeleton to be used for creating the runtest.sh """
  1784. def init(self):
  1785. self.name = "Skeleton"
  1786. self.question = "Skeleton to be used for creating the runtest.sh"
  1787. self.description = """There are several runtest.sh skeletons available:
  1788. beaker (general Beaker template),
  1789. beakerlib (BeakerLib structure),
  1790. simple (creates separate script with test logic),
  1791. empty (populates runtest.sh just with header and license) and
  1792. "skelX" (custom skeletons saved in user preferences)."""
  1793. self.skeletons = parseString("""
  1794. <skeletons>
  1795. <skeleton name="beakerlib">
  1796. # Include Beaker environment
  1797. . /usr/bin/rhts-environment.sh || exit 1
  1798. . /usr/share/beakerlib/beakerlib.sh || exit 1
  1799. PACKAGE="<package/>"
  1800. rlJournalStart
  1801. rlPhaseStartSetup
  1802. rlAssertRpm $PACKAGE
  1803. rlRun "TmpDir=\$(mktemp -d)" 0 "Creating tmp directory"
  1804. rlRun "pushd $TmpDir"
  1805. rlPhaseEnd
  1806. rlPhaseStartTest
  1807. rlRun "touch foo" 0 "Creating the foo test file"
  1808. rlAssertExists "foo"
  1809. rlRun "ls -l foo" 0 "Listing the foo test file"
  1810. rlPhaseEnd
  1811. rlPhaseStartCleanup
  1812. rlRun "popd"
  1813. rlRun "rm -r $TmpDir" 0 "Removing tmp directory"
  1814. rlPhaseEnd
  1815. rlJournalPrintText
  1816. rlJournalEnd
  1817. </skeleton>
  1818. <skeleton name="beaker">
  1819. # Include Beaker environment
  1820. . /usr/bin/rhts-environment.sh || exit 1
  1821. PACKAGE="<package/>"
  1822. set -x
  1823. # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  1824. # Setup
  1825. # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  1826. score=0
  1827. rpm -q $PACKAGE || ((score++))
  1828. TmpDir=$(mktemp -d) || ((score++))
  1829. pushd $TmpDir || ((score++))
  1830. ((score == 0)) &amp;&amp; result=PASS || result=FAIL
  1831. echo "Setup finished, result: $result, score: $score"
  1832. report_result $TEST/setup $result $score
  1833. # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  1834. # Test
  1835. # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  1836. score=0
  1837. touch foo || ((score++))
  1838. [ -e foo ] || ((score++))
  1839. ls -l foo || ((score++))
  1840. ((score == 0)) &amp;&amp; result=PASS || result=FAIL
  1841. echo "Testing finished, result: $result, score: $score"
  1842. report_result $TEST/testing $result $score
  1843. # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  1844. # Cleanup
  1845. # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  1846. score=0
  1847. popd || ((score++))
  1848. rm -r "$TmpDir" || ((score++))
  1849. ((score == 0)) &amp;&amp; result=PASS || result=FAIL
  1850. echo "Cleanup finished, result: $result, score: $score"
  1851. report_result $TEST/cleanup $result $score
  1852. </skeleton>
  1853. <skeleton name="multihost">
  1854. # Include Beaker environment
  1855. . /usr/bin/rhts-environment.sh || exit 1
  1856. . /usr/share/beakerlib/beakerlib.sh || exit 1
  1857. PACKAGE="<package/>"
  1858. # set client &amp; server manually if debugging
  1859. # SERVERS="server.example.com"
  1860. # CLIENTS="client.example.com"
  1861. Server() {
  1862. rlPhaseStartTest Server
  1863. # server setup goes here
  1864. rlRun "rhts-sync-set -s READY" 0 "Server ready"
  1865. rlRun "rhts-sync-block -s DONE $CLIENTS" 0 "Waiting for the client"
  1866. rlPhaseEnd
  1867. }
  1868. Client() {
  1869. rlPhaseStartTest Client
  1870. rlRun "rhts-sync-block -s READY $SERVERS" 0 "Waiting for the server"
  1871. # client action goes here
  1872. rlRun "rhts-sync-set -s DONE" 0 "Client done"
  1873. rlPhaseEnd
  1874. }
  1875. rlJournalStart
  1876. rlPhaseStartSetup
  1877. rlAssertRpm $PACKAGE
  1878. rlLog "Server: $SERVERS"
  1879. rlLog "Client: $CLIENTS"
  1880. rlRun "TmpDir=\$(mktemp -d)" 0 "Creating tmp directory"
  1881. rlRun "pushd $TmpDir"
  1882. rlPhaseEnd
  1883. if echo $SERVERS | grep -q $HOSTNAME ; then
  1884. Server
  1885. elif echo $CLIENTS | grep -q $HOSTNAME ; then
  1886. Client
  1887. else
  1888. rlReport "Stray" "FAIL"
  1889. fi
  1890. rlPhaseStartCleanup
  1891. rlRun "popd"
  1892. rlRun "rm -r $TmpDir" 0 "Removing tmp directory"
  1893. rlPhaseEnd
  1894. rlJournalPrintText
  1895. rlJournalEnd
  1896. </skeleton>
  1897. <skeleton name="simple">
  1898. rhts-run-simple-test $TEST ./test
  1899. </skeleton>
  1900. <skeleton name="empty">
  1901. </skeleton>
  1902. <skeleton name="library">
  1903. # Include Beaker environment
  1904. . /usr/bin/rhts-environment.sh || exit 1
  1905. . /usr/share/beakerlib/beakerlib.sh || exit 1
  1906. PACKAGE="<package/>"
  1907. PHASE=${PHASE:-Test}
  1908. rlJournalStart
  1909. rlPhaseStartSetup
  1910. rlRun "rlImport <package/>/<testname/>"
  1911. rlRun "TmpDir=\$(mktemp -d)" 0 "Creating tmp directory"
  1912. rlRun "pushd $TmpDir"
  1913. rlPhaseEnd
  1914. # Create file
  1915. if [[ "$PHASE" =~ "Create" ]]; then
  1916. rlPhaseStartTest "Create"
  1917. fileCreate
  1918. rlPhaseEnd
  1919. fi
  1920. # Self test
  1921. if [[ "$PHASE" =~ "Test" ]]; then
  1922. rlPhaseStartTest "Test default name"
  1923. fileCreate
  1924. rlAssertExists "$fileFILENAME"
  1925. rlPhaseEnd
  1926. rlPhaseStartTest "Test filename in parameter"
  1927. fileCreate "parameter-file"
  1928. rlAssertExists "parameter-file"
  1929. rlPhaseEnd
  1930. rlPhaseStartTest "Test filename in variable"
  1931. FILENAME="variable-file" fileCreate
  1932. rlAssertExists "variable-file"
  1933. rlPhaseEnd
  1934. fi
  1935. rlPhaseStartCleanup
  1936. rlRun "popd"
  1937. rlRun "rm -r $TmpDir" 0 "Removing tmp directory"
  1938. rlPhaseEnd
  1939. rlJournalPrintText
  1940. rlJournalEnd
  1941. </skeleton>
  1942. </skeletons>
  1943. """)
  1944. self.makefile = """
  1945. export TEST=%s
  1946. export TESTVERSION=%s
  1947. BUILT_FILES=
  1948. FILES=$(METADATA) %s
  1949. .PHONY: all install download clean
  1950. run: $(FILES) build
  1951. ./runtest.sh
  1952. build: $(BUILT_FILES)%s
  1953. clean:
  1954. rm -f *~ $(BUILT_FILES)
  1955. include /usr/share/rhts/lib/rhts-make.include
  1956. $(METADATA): Makefile
  1957. @echo "Owner: %s" > $(METADATA)
  1958. @echo "Name: $(TEST)" >> $(METADATA)
  1959. @echo "TestVersion: $(TESTVERSION)" >> $(METADATA)
  1960. @echo "Path: $(TEST_DIR)" >> $(METADATA)%s
  1961. rhts-lint $(METADATA)
  1962. """
  1963. # skeleton for lib.sh file when creating library
  1964. self.library = """
  1965. # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  1966. # library-prefix = %s
  1967. # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  1968. true <<'=cut'
  1969. =pod
  1970. =head1 NAME
  1971. %s/%s - %s
  1972. =head1 DESCRIPTION
  1973. This is a trivial example of a BeakerLib library. It's main goal
  1974. is to provide a minimal template which can be used as a skeleton
  1975. when creating a new library. It implements function fileCreate().
  1976. Please note, that all library functions must begin with the same
  1977. prefix which is defined at the beginning of the library.
  1978. =cut
  1979. # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  1980. # Variables
  1981. # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  1982. true <<'=cut'
  1983. =pod
  1984. =head1 VARIABLES
  1985. Below is the list of global variables. When writing a new library,
  1986. please make sure that all global variables start with the library
  1987. prefix to prevent collisions with other libraries.
  1988. =over
  1989. =item fileFILENAME
  1990. Default file name to be used when no provided ('foo').
  1991. =back
  1992. =cut
  1993. fileFILENAME="foo"
  1994. # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  1995. # Functions
  1996. # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  1997. true <<'=cut'
  1998. =pod
  1999. =head1 FUNCTIONS
  2000. =head2 fileCreate
  2001. Create a new file, name it accordingly and make sure (assert) that
  2002. the file is successfully created.
  2003. fileCreate [filename]
  2004. =over
  2005. =item filename
  2006. Name for the newly created file. Optionally the filename can be
  2007. provided in the FILENAME environment variable. When no file name
  2008. is given 'foo' is used by default.
  2009. =back
  2010. Returns 0 when the file is successfully created, non-zero otherwise.
  2011. =cut
  2012. fileCreate() {
  2013. local filename
  2014. filename=${1:-$FILENAME}
  2015. filename=${filename:-$fileFILENAME}
  2016. rlRun "touch '$filename'" 0 "Creating file '$filename'"
  2017. rlAssertExists "$filename"
  2018. return $?
  2019. }
  2020. # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  2021. # Execution
  2022. # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  2023. true <<'=cut'
  2024. =pod
  2025. =head1 EXECUTION
  2026. This library supports direct execution. When run as a task, phases
  2027. provided in the PHASE environment variable will be executed.
  2028. Supported phases are:
  2029. =over
  2030. =item Create
  2031. Create a new empty file. Use FILENAME to provide the desired file
  2032. name. By default 'foo' is created in the current directory.
  2033. =item Test
  2034. Run the self test suite.
  2035. =back
  2036. =cut
  2037. # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  2038. # Verification
  2039. # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  2040. #
  2041. # This is a verification callback which will be called by
  2042. # rlImport after sourcing the library to make sure everything is
  2043. # all right. It makes sense to perform a basic sanity test and
  2044. # check that all required packages are installed. The function
  2045. # should return 0 only when the library is ready to serve.
  2046. fileLibraryLoaded() {
  2047. if rpm=$(rpm -q coreutils); then
  2048. rlLogDebug "Library coreutils/file running with $rpm"
  2049. return 0
  2050. else
  2051. rlLogError "Package coreutils not installed"
  2052. return 1
  2053. fi
  2054. }
  2055. # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  2056. # Authors
  2057. # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  2058. true <<'=cut'
  2059. =pod
  2060. =head1 AUTHORS
  2061. =over
  2062. =item *
  2063. %s
  2064. =back
  2065. =cut
  2066. """
  2067. self.list = []
  2068. self.list.extend(findNodeNames(self.skeletons, "skeleton"))
  2069. self.list.extend(findNodeNames(self.options.pref.skeletons, "skeleton"))
  2070. self.common = False
  2071. self.default(self.options.skeleton())
  2072. self.requires = None
  2073. self.rhtsrequires = None
  2074. def replaceVariables(self, xml, test = None):
  2075. """ Replace all <variable> tags with their respective values """
  2076. skeleton = ""
  2077. for child in xml.childNodes:
  2078. # regular text node -> just copy
  2079. if child.nodeType == child.TEXT_NODE:
  2080. skeleton += child.nodeValue
  2081. # xml tag -> try to expand value of test.tag.show()
  2082. elif child.nodeType == child.ELEMENT_NODE:
  2083. try:
  2084. name = child.tagName
  2085. # some variables need a special treatment
  2086. if name == "test":
  2087. value = test.fullPath()
  2088. elif name == "bugs":
  2089. value = test.testname.bugs.show()
  2090. elif name == "reproducers":
  2091. value = test.testname.bugs.reproducers.show()
  2092. else:
  2093. # map long names to the real vars
  2094. map = {
  2095. "description" : "desc",
  2096. "architectures" : "archs",
  2097. }
  2098. try: name = map[name]
  2099. except: pass
  2100. # get the value
  2101. value = eval("test." + name + ".show()")
  2102. except:
  2103. # leave unknown xml tags as they are
  2104. skeleton += child.toxml('utf-8')
  2105. else:
  2106. skeleton += value
  2107. return skeleton
  2108. def getRuntest(self, test = None):
  2109. """ Return runtest.sh skeleton corresponding to user choice """
  2110. # get the template from predefined or user skeletons
  2111. skeleton = findNode(self.skeletons, "skeleton", self.data) \
  2112. or findNode(self.options.pref.skeletons, "skeleton", self.data)
  2113. # substitute variables, convert to plain text
  2114. skeleton = self.replaceVariables(skeleton, test)
  2115. # return dedented skeleton without trailing whitespace
  2116. skeleton = re.sub("\n\s+$", "\n", skeleton)
  2117. return dedentText(skeleton)
  2118. def getRhtsRequires(self):
  2119. """ Return packages/libraries listed in the arguments of a skeleton, if any """
  2120. # get the template from predefined or user skeletons
  2121. skeleton = findNode(self.skeletons, "skeleton", self.data) \
  2122. or findNode(self.options.pref.skeletons, "skeleton", self.data)
  2123. if not skeleton:
  2124. return None
  2125. try:
  2126. rhtsrequires = skeleton.getAttribute("rhtsrequires")
  2127. return rhtsrequires
  2128. except:
  2129. print "getRhtsRequires exception"
  2130. def getMakefile(self, type, testname, version, author, reproducers, meta):
  2131. """ Return Makefile skeleton """
  2132. # if test type is Library, include lib.sh to the Makefile instead of PURPOSE
  2133. files = ["runtest.sh", "Makefile"]
  2134. files.append("lib.sh" if type == "Library" else "PURPOSE")
  2135. build = ["runtest.sh"]
  2136. # add "test" file when creating simple test
  2137. if self.data == "simple":
  2138. files.append("test")
  2139. build.append("test")
  2140. # include the reproducers in the lists as well
  2141. if reproducers:
  2142. for reproducer in reproducers:
  2143. files.append(reproducer)
  2144. # add script-like reproducers to build tag
  2145. if RegExpScript.search(reproducer):
  2146. build.append(reproducer)
  2147. chmod = "\n test -x %s || chmod a+x %s"
  2148. return dedentText(self.makefile % (testname, version, " ".join(files),
  2149. "".join([chmod % (file, file) for file in build]), author, meta))
  2150. def getVimHeader(self):
  2151. """ Insert the vim completion header if it's an beakerlib skeleton """
  2152. if re.search("rl[A-Z]", self.getRuntest()):
  2153. return comment(VimDictionary,
  2154. top = False, bottom = False, padding = 0) + "\n"
  2155. else:
  2156. return ""
  2157. def getLibrary(self, testname, description, package, author):
  2158. """ Return lib.sh skeleton """
  2159. return dedentText(self.library.lstrip() %
  2160. (testname, package, testname, description, author))
  2161. class Author(Inquisitor):
  2162. """ Author's name """
  2163. def init(self):
  2164. self.name = "Author"
  2165. self.question = "Author's name"
  2166. self.description = """Put your name [middle name] and surname here,
  2167. abbreviations allowed."""
  2168. # ask for author when run for the first time
  2169. self.common = self.options.pref.firstRun
  2170. self.default(self.options.author())
  2171. def valid(self):
  2172. return self.data is not None \
  2173. and RegExpAuthor.match(self.data)
  2174. class Email(Inquisitor):
  2175. """ Author's email """
  2176. def init(self):
  2177. self.name = "Email"
  2178. self.question = "Author's email"
  2179. self.description = """Email address in lower case letters,
  2180. dots and dashes. Underscore allowed before the "@" only."""
  2181. # ask for author when run for the first time
  2182. self.common = self.options.pref.firstRun
  2183. self.default(self.options.email())
  2184. def valid(self):
  2185. return self.data is not None \
  2186. and RegExpEmail.match(self.data)
  2187. class Desc(Inquisitor):
  2188. """ Description """
  2189. def init(self):
  2190. self.name = "Description"
  2191. self.question = "Short description"
  2192. self.description = "Provide a short sentence describing the test."
  2193. self.default(self.options.desc())
  2194. def valid(self):
  2195. return self.data is not None and self.data not in ["", "?"]
  2196. class Test(SingleChoice):
  2197. """ Test class containing all the information necessary for building a test """
  2198. def init(self):
  2199. self.name = "Test fields"
  2200. if self.options.makefile:
  2201. self.question = "Ready to write the new Makefile, "\
  2202. "please review or make the changes"
  2203. else:
  2204. self.question = "Ready to create the test, please review"
  2205. self.description = "Type a few letters from field name to "\
  2206. "edit or press ENTER to confirm. Use the \"write\" keyword "\
  2207. "to save current settings as preferences."
  2208. self.list = []
  2209. self.default(["", "Everything OK"])
  2210. # possibly print first time welcome message
  2211. if self.options.pref.firstRun:
  2212. print dedentText("""
  2213. Welcome to The Beaker Wizard!
  2214. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  2215. It seems, you're running the beaker-wizard for the first time.
  2216. I'll try to be a little bit more verbose. Should you need
  2217. any help in the future, just try using the "?" character.
  2218. """, count = 16)
  2219. # gather all test data
  2220. self.testname = Name(self.options)
  2221. self.path = Path(self.options)
  2222. self.type = Type(self.options, suggest = self.testname.bugs.suggestType() or
  2223. ('Library' if 'library' in self.options.skeleton() else None))
  2224. self.package = Package(self.options,
  2225. suggest = self.testname.bugs.getComponent())
  2226. self.namespace = Namespace(self.options)
  2227. self.desc = Desc(self.options,
  2228. suggest = self.testname.bugs.suggestDescription())
  2229. self.runfor = RunFor(self.options, suggest = self.package.value())
  2230. self.requires = Requires(self.options, suggest = self.package.value())
  2231. self.archs = Architectures(self.options)
  2232. self.releases = Releases(self.options)
  2233. self.time = Time(self.options)
  2234. self.version = Version(self.options)
  2235. self.priority = Priority(self.options)
  2236. self.confidential = Confidential(self.options,
  2237. suggest = self.testname.bugs.suggestConfidential())
  2238. self.destructive = Destructive(self.options)
  2239. self.license = License(self.options)
  2240. self.skeleton = Skeleton(self.options,
  2241. suggest = self.type.suggestSkeleton())
  2242. self.author = Author(self.options)
  2243. self.email = Email(self.options)
  2244. self.rhtsrequires = RhtsRequires(self.options, suggest = self.skeleton.getRhtsRequires())
  2245. # we escape review only in force mode
  2246. if not self.options.force(): self.confirm = True
  2247. if not self.confirm: self.format()
  2248. def valid(self):
  2249. return self.data is not None \
  2250. and self.data not in ["?"] \
  2251. and self.edit(checkOnly = True)
  2252. def format(self):
  2253. """ Format all test fields into nice table """
  2254. print
  2255. print self.fullPath()
  2256. print
  2257. self.namespace.format()
  2258. self.package.format()
  2259. self.type.format()
  2260. self.path.format()
  2261. self.testname.format()
  2262. self.desc.format()
  2263. print
  2264. self.testname.bugs.format()
  2265. if not self.options.makefile: # skip in makefile edit mode
  2266. self.testname.prefix.format()
  2267. self.testname.bugs.reproducers.format()
  2268. print
  2269. self.runfor.format()
  2270. self.requires.format()
  2271. self.rhtsrequires.format()
  2272. self.archs.format()
  2273. self.releases.format()
  2274. self.version.format()
  2275. self.time.format()
  2276. print
  2277. self.priority.format()
  2278. self.license.format()
  2279. self.confidential.format()
  2280. self.destructive.format()
  2281. print
  2282. if not self.options.makefile:
  2283. self.skeleton.format() # irrelevant in makefile edit mode
  2284. self.author.format()
  2285. self.email.format()
  2286. print
  2287. def heading(self):
  2288. SingleChoice.heading(self)
  2289. self.format()
  2290. def edit(self, checkOnly = False):
  2291. """ Edit test fields (based on just few letters from field name)
  2292. If checkOnly is on then checks only for valid field name """
  2293. # quit
  2294. if re.match("q|exit", self.data, re.I):
  2295. print "Ok, quitting for now. See you later ;-)"
  2296. sys.exit(0)
  2297. # no (seems the user is beginner -> turn on verbosity)
  2298. elif re.match("no?$", self.data, re.I):
  2299. self.options.opt.verbose = True
  2300. return True
  2301. # yes
  2302. elif RegExpYes.match(self.data):
  2303. return True
  2304. # check all fields for matching string (and edit if not checking only)
  2305. for field in self.testname, self.package, self.namespace, self.runfor, \
  2306. self.requires, self.rhtsrequires, self.package, self.releases, self.version, \
  2307. self.time, self.desc, self.destructive, self.archs, \
  2308. self.path, self.priority, self.confidential, self.license, \
  2309. self.skeleton, self.author, self.email, self.testname.prefix, \
  2310. self.testname.bugs.reproducers:
  2311. if field.matchName(self.data):
  2312. if not checkOnly: field.edit()
  2313. return True
  2314. # bugs & type have special treatment
  2315. if self.type.matchName(self.data):
  2316. if not checkOnly and self.type.edit():
  2317. # if type has changed suggest a new skeleton
  2318. self.skeleton = Skeleton(self.options,
  2319. suggest = self.type.suggestSkeleton())
  2320. return True
  2321. elif self.testname.bugs.matchName(self.data):
  2322. if not checkOnly and self.testname.bugs.edit():
  2323. # if bugs changed, suggest new name & desc & reproducers
  2324. if self.testname.bugs.fetchBugDetails():
  2325. self.testname.edit(self.testname.bugs.suggestTestName())
  2326. self.desc.edit(self.testname.bugs.suggestDescription())
  2327. self.testname.bugs.reproducers.edit()
  2328. return True
  2329. # write preferences
  2330. elif re.match("w", self.data, re.I):
  2331. if not checkOnly:
  2332. self.savePreferences(force = True)
  2333. return True
  2334. # bad option
  2335. else:
  2336. return False
  2337. def relativePath(self):
  2338. """ Return relative path from package directory"""
  2339. return "%s%s/%s" % (
  2340. self.type.value(),
  2341. self.path.value(),
  2342. self.testname.value())
  2343. def fullPath(self):
  2344. """ Return complete test path """
  2345. return "/%s/%s/%s" % (
  2346. self.namespace.value(),
  2347. self.package.value(),
  2348. self.relativePath())
  2349. def formatAuthor(self):
  2350. """ Format author with email """
  2351. return "%s <%s>" % (self.author.value(), self.email.value())
  2352. def formatHeader(self, filename):
  2353. """ Format standard header """
  2354. return "%s of %s\nDescription: %s\nAuthor: %s" % (
  2355. filename, self.fullPath(),
  2356. self.desc.value(),
  2357. self.formatAuthor())
  2358. def formatMakefile(self):
  2359. # add 'Provides' to the Makefile when test type is 'Library'
  2360. if self.type.value() == "Library":
  2361. provides = self.formatMakefileLine(
  2362. name="Provides",
  2363. value="library({0}/{1})".format(
  2364. self.package.value(), self.testname.value()))
  2365. else:
  2366. provides = ""
  2367. return (
  2368. comment(self.formatHeader("Makefile")) + "\n" +
  2369. comment(self.license.get(), top = False) + "\n" +
  2370. self.skeleton.getMakefile(
  2371. self.type.value(),
  2372. self.fullPath(),
  2373. self.version.value(),
  2374. self.formatAuthor(),
  2375. self.testname.bugs.reproducers.value(),
  2376. self.desc.formatMakefileLine() +
  2377. self.type.formatMakefileLine(name = "Type") +
  2378. self.time.formatMakefileLine(name = "TestTime") +
  2379. self.runfor.formatMakefileLine(name = "RunFor") +
  2380. self.requires.formatMakefileLine(name = "Requires") +
  2381. self.rhtsrequires.formatMakefileLine(name = "RhtsRequires") +
  2382. provides +
  2383. self.priority.formatMakefileLine() +
  2384. self.license.formatMakefileLine() +
  2385. self.confidential.formatMakefileLine() +
  2386. self.destructive.formatMakefileLine() +
  2387. self.testname.bugs.formatMakefileLine(name = "Bug") +
  2388. self.releases.formatMakefileLine() +
  2389. self.archs.formatMakefileLine()))
  2390. def savePreferences(self, force = False):
  2391. """ Save user preferences (well, maybe :-) """
  2392. # update user preferences with current settings
  2393. self.options.pref.update(
  2394. self.author.value(),
  2395. self.email.value(),
  2396. self.options.confirm(),
  2397. self.type.value(),
  2398. self.namespace.value(),
  2399. self.time.value(),
  2400. self.priority.value(),
  2401. self.confidential.value(),
  2402. self.destructive.value(),
  2403. self.testname.prefix.value(),
  2404. self.license.value(),
  2405. self.skeleton.value())
  2406. # and possibly save them to disk
  2407. if force or self.options.pref.firstRun or self.options.write():
  2408. self.options.pref.save()
  2409. def createFile(self, filename, content, mode=None):
  2410. """ Create single test file with specified content """
  2411. fullpath = self.relativePath() + "/" + filename
  2412. addedToGit = ""
  2413. # overwrite existing?
  2414. if os.path.exists(fullpath):
  2415. sys.stdout.write(fullpath + " already exists, ")
  2416. if self.options.force():
  2417. print "force on -> overwriting"
  2418. else:
  2419. sys.stdout.write("overwrite? [y/n] ")
  2420. answer = unicode(sys.stdin.readline(), "utf-8")
  2421. if not re.match("y", answer, re.I):
  2422. print "Ok skipping. Next time use -f if you want to overwrite files."
  2423. return
  2424. # let's write it
  2425. try:
  2426. file = open(fullpath, "w")
  2427. file.write(content.encode("utf-8"))
  2428. file.close()
  2429. # change mode if provided
  2430. if mode: os.chmod(fullpath, mode)
  2431. # and, optionally, add to Git
  2432. if self.options.opt.git:
  2433. addToGit(fullpath)
  2434. addedToGit = ", added to git"
  2435. except IOError:
  2436. print "Cannot write to %s" % fullpath
  2437. sys.exit(3)
  2438. else:
  2439. print "File", fullpath, "written" + addedToGit
  2440. def create(self):
  2441. """ Create all necessary test files """
  2442. # if in the Makefile edit mode, just save the Makefile
  2443. if self.options.makefile:
  2444. self.options.makefile.save(self.fullPath(), self.version.value(),
  2445. self.formatMakefile())
  2446. return
  2447. # set file vars
  2448. test = self.testname.value()
  2449. package = self.package.value()
  2450. author = self.formatAuthor().encode('utf-8')
  2451. description = self.desc.value()
  2452. path = self.relativePath()
  2453. fullpath = self.fullPath()
  2454. addedToGit = ""
  2455. # create test directory
  2456. class AlreadyExists(Exception): pass
  2457. try:
  2458. # nothing to do if already exists
  2459. if os.path.isdir(path):
  2460. raise AlreadyExists
  2461. # otherwise attempt to create the whole hiearchy
  2462. else:
  2463. os.makedirs(path)
  2464. except OSError, e:
  2465. print "Bad, cannot create test directory %s :-(" % path
  2466. sys.exit(1)
  2467. except AlreadyExists:
  2468. print "Well, directory %s already exists, let's see..." % path
  2469. else:
  2470. print "Directory %s created%s" % (path, addedToGit)
  2471. # if test type is Library, create lib.sh and don't include PURPOSE
  2472. if self.type.value() == "Library":
  2473. self.createFile("lib.sh", content =
  2474. "#!/bin/bash\n" +
  2475. self.skeleton.getVimHeader() +
  2476. comment(self.formatHeader("lib.sh")) + "\n" +
  2477. comment(self.license.get(), top=False, bottom=False) +
  2478. "\n" +
  2479. self.skeleton.getLibrary(
  2480. test, description, package, author))
  2481. # for regular tests create PURPOSE
  2482. else:
  2483. self.createFile("PURPOSE", content =
  2484. self.formatHeader("PURPOSE") + "\n" +
  2485. self.testname.bugs.formatBugDetails())
  2486. # runtest.sh
  2487. self.createFile("runtest.sh", content =
  2488. "#!/bin/bash\n" +
  2489. self.skeleton.getVimHeader() +
  2490. comment(self.formatHeader("runtest.sh")) + "\n" +
  2491. comment(self.license.get(), top = False) + "\n" +
  2492. self.skeleton.getRuntest(self),
  2493. mode=0755
  2494. )
  2495. # Makefile
  2496. self.createFile("Makefile", content = self.formatMakefile())
  2497. # test
  2498. if self.skeleton.value() == "simple":
  2499. self.createFile("test", content = "")
  2500. # download reproducers
  2501. self.testname.bugs.reproducers.download(self.relativePath())
  2502. # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  2503. # Main
  2504. # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  2505. def main():
  2506. # parse options and user preferences
  2507. options = Options()
  2508. # possibly display help or version message
  2509. Help(options)
  2510. # ask for all necessary details
  2511. test = Test(options)
  2512. # keep asking until everything is OK
  2513. while not RegExpYes.match(test.value()):
  2514. test.edit()
  2515. test.default(["", "Everything OK"])
  2516. test.ask(force = True)
  2517. # and finally create the test file structure
  2518. test.create()
  2519. test.savePreferences()
  2520. return 0
  2521. if __name__ == '__main__':
  2522. sys.exit(main())