diff options
Diffstat (limited to 'lib/testtools/scripts')
-rw-r--r-- | lib/testtools/scripts/README | 3 | ||||
-rw-r--r-- | lib/testtools/scripts/_lp_release.py | 230 | ||||
-rwxr-xr-x | lib/testtools/scripts/all-pythons | 90 | ||||
-rwxr-xr-x | lib/testtools/scripts/update-rtfd | 11 |
4 files changed, 334 insertions, 0 deletions
diff --git a/lib/testtools/scripts/README b/lib/testtools/scripts/README new file mode 100644 index 0000000000..648f105705 --- /dev/null +++ b/lib/testtools/scripts/README @@ -0,0 +1,3 @@ +These are scripts to help with building, maintaining and releasing testtools. + +There is little here for anyone except a testtools contributor. diff --git a/lib/testtools/scripts/_lp_release.py b/lib/testtools/scripts/_lp_release.py new file mode 100644 index 0000000000..20afd0199e --- /dev/null +++ b/lib/testtools/scripts/_lp_release.py @@ -0,0 +1,230 @@ +#!/usr/bin/python + +"""Release testtools on Launchpad. + +Steps: + 1. Make sure all "Fix committed" bugs are assigned to 'next' + 2. Rename 'next' to the new version + 3. Release the milestone + 4. Upload the tarball + 5. Create a new 'next' milestone + 6. Mark all "Fix committed" bugs in the milestone as "Fix released" + +Assumes that NEWS is in the parent directory, that the release sections are +underlined with '~' and the subsections are underlined with '-'. + +Assumes that this file is in the 'scripts' directory a testtools tree that has +already had a tarball built and uploaded with 'python setup.py sdist upload +--sign'. +""" + +from datetime import datetime, timedelta, tzinfo +import logging +import os +import sys + +from launchpadlib.launchpad import Launchpad +from launchpadlib import uris + + +APP_NAME = 'testtools-lp-release' +CACHE_DIR = os.path.expanduser('~/.launchpadlib/cache') +SERVICE_ROOT = uris.LPNET_SERVICE_ROOT + +FIX_COMMITTED = u"Fix Committed" +FIX_RELEASED = u"Fix Released" + +# Launchpad file type for a tarball upload. +CODE_RELEASE_TARBALL = 'Code Release Tarball' + +PROJECT_NAME = 'testtools' +NEXT_MILESTONE_NAME = 'next' + + +class _UTC(tzinfo): + """UTC""" + + def utcoffset(self, dt): + return timedelta(0) + + def tzname(self, dt): + return "UTC" + + def dst(self, dt): + return timedelta(0) + +UTC = _UTC() + + +def configure_logging(): + level = logging.INFO + log = logging.getLogger(APP_NAME) + log.setLevel(level) + handler = logging.StreamHandler() + handler.setLevel(level) + formatter = logging.Formatter("%(levelname)s: %(message)s") + handler.setFormatter(formatter) + log.addHandler(handler) + return log +LOG = configure_logging() + + +def get_path(relpath): + """Get the absolute path for something relative to this file.""" + return os.path.abspath( + os.path.join( + os.path.dirname(os.path.dirname(__file__)), relpath)) + + +def assign_fix_committed_to_next(testtools, next_milestone): + """Find all 'Fix Committed' and make sure they are in 'next'.""" + fixed_bugs = list(testtools.searchTasks(status=FIX_COMMITTED)) + for task in fixed_bugs: + LOG.debug("%s" % (task.title,)) + if task.milestone != next_milestone: + task.milestone = next_milestone + LOG.info("Re-assigning %s" % (task.title,)) + task.lp_save() + + +def rename_milestone(next_milestone, new_name): + """Rename 'next_milestone' to 'new_name'.""" + LOG.info("Renaming %s to %s" % (next_milestone.name, new_name)) + next_milestone.name = new_name + next_milestone.lp_save() + + +def get_release_notes_and_changelog(news_path): + release_notes = [] + changelog = [] + state = None + last_line = None + + def is_heading_marker(line, marker_char): + return line and line == marker_char * len(line) + + LOG.debug("Loading NEWS from %s" % (news_path,)) + with open(news_path, 'r') as news: + for line in news: + line = line.strip() + if state is None: + if is_heading_marker(line, '~'): + milestone_name = last_line + state = 'release-notes' + else: + last_line = line + elif state == 'title': + # The line after the title is a heading marker line, so we + # ignore it and change state. That which follows are the + # release notes. + state = 'release-notes' + elif state == 'release-notes': + if is_heading_marker(line, '-'): + state = 'changelog' + # Last line in the release notes is actually the first + # line of the changelog. + changelog = [release_notes.pop(), line] + else: + release_notes.append(line) + elif state == 'changelog': + if is_heading_marker(line, '~'): + # Last line in changelog is actually the first line of the + # next section. + changelog.pop() + break + else: + changelog.append(line) + else: + raise ValueError("Couldn't parse NEWS") + + release_notes = '\n'.join(release_notes).strip() + '\n' + changelog = '\n'.join(changelog).strip() + '\n' + return milestone_name, release_notes, changelog + + +def release_milestone(milestone, release_notes, changelog): + date_released = datetime.now(tz=UTC) + LOG.info( + "Releasing milestone: %s, date %s" % (milestone.name, date_released)) + release = milestone.createProductRelease( + date_released=date_released, + changelog=changelog, + release_notes=release_notes, + ) + milestone.is_active = False + milestone.lp_save() + return release + + +def create_milestone(series, name): + """Create a new milestone in the same series as 'release_milestone'.""" + LOG.info("Creating milestone %s in series %s" % (name, series.name)) + return series.newMilestone(name=name) + + +def close_fixed_bugs(milestone): + tasks = list(milestone.searchTasks()) + for task in tasks: + LOG.debug("Found %s" % (task.title,)) + if task.status == FIX_COMMITTED: + LOG.info("Closing %s" % (task.title,)) + task.status = FIX_RELEASED + else: + LOG.warning( + "Bug not fixed, removing from milestone: %s" % (task.title,)) + task.milestone = None + task.lp_save() + + +def upload_tarball(release, tarball_path): + with open(tarball_path) as tarball: + tarball_content = tarball.read() + sig_path = tarball_path + '.asc' + with open(sig_path) as sig: + sig_content = sig.read() + tarball_name = os.path.basename(tarball_path) + LOG.info("Uploading tarball: %s" % (tarball_path,)) + release.add_file( + file_type=CODE_RELEASE_TARBALL, + file_content=tarball_content, filename=tarball_name, + signature_content=sig_content, + signature_filename=sig_path, + content_type="application/x-gzip; charset=binary") + + +def release_project(launchpad, project_name, next_milestone_name): + testtools = launchpad.projects[project_name] + next_milestone = testtools.getMilestone(name=next_milestone_name) + release_name, release_notes, changelog = get_release_notes_and_changelog( + get_path('NEWS')) + LOG.info("Releasing %s %s" % (project_name, release_name)) + # Since reversing these operations is hard, and inspecting errors from + # Launchpad is also difficult, do some looking before leaping. + errors = [] + tarball_path = get_path('dist/%s-%s.tar.gz' % (project_name, release_name,)) + if not os.path.isfile(tarball_path): + errors.append("%s does not exist" % (tarball_path,)) + if not os.path.isfile(tarball_path + '.asc'): + errors.append("%s does not exist" % (tarball_path + '.asc',)) + if testtools.getMilestone(name=release_name): + errors.append("Milestone %s exists on %s" % (release_name, project_name)) + if errors: + for error in errors: + LOG.error(error) + return 1 + assign_fix_committed_to_next(testtools, next_milestone) + rename_milestone(next_milestone, release_name) + release = release_milestone(next_milestone, release_notes, changelog) + upload_tarball(release, tarball_path) + create_milestone(next_milestone.series_target, next_milestone_name) + close_fixed_bugs(next_milestone) + return 0 + + +def main(args): + launchpad = Launchpad.login_with(APP_NAME, SERVICE_ROOT, CACHE_DIR) + return release_project(launchpad, PROJECT_NAME, NEXT_MILESTONE_NAME) + + +if __name__ == '__main__': + sys.exit(main(sys.argv)) diff --git a/lib/testtools/scripts/all-pythons b/lib/testtools/scripts/all-pythons new file mode 100755 index 0000000000..aecc9495a6 --- /dev/null +++ b/lib/testtools/scripts/all-pythons @@ -0,0 +1,90 @@ +#!/usr/bin/python + +"""Run the testtools test suite for all supported Pythons. + +Prints output as a subunit test suite. If anything goes to stderr, that is +treated as a test error. If a Python is not available, then it is skipped. +""" + +from datetime import datetime +import os +import subprocess +import sys + +import subunit +from subunit import ( + iso8601, + _make_stream_binary, + TestProtocolClient, + TestProtocolServer, + ) +from testtools import ( + PlaceHolder, + TestCase, + ) +from testtools.compat import BytesIO +from testtools.content import text_content + + +ROOT = os.path.dirname(os.path.dirname(__file__)) + + +def run_for_python(version, result): + # XXX: This could probably be broken up and put into subunit. + python = 'python%s' % (version,) + # XXX: Correct API, but subunit doesn't support it. :( + # result.tags(set(python), set()) + result.time(now()) + test = PlaceHolder(''.join(c for c in python if c != '.')) + process = subprocess.Popen( + '%s -c pass' % (python,), shell=True, + stdout=subprocess.PIPE, stderr=subprocess.PIPE) + process.communicate() + + if process.returncode: + result.startTest(test) + result.addSkip(test, reason='%s not available' % (python,)) + result.stopTest(test) + return + + env = os.environ.copy() + if env.get('PYTHONPATH', None): + env['PYTHONPATH'] = os.pathsep.join([ROOT, env['PYTHONPATH']]) + else: + env['PYTHONPATH'] = ROOT + result.time(now()) + protocol = TestProtocolServer(result) + subunit_path = os.path.join(os.path.dirname(subunit.__file__), 'run.py') + cmd = [ + python, + '-W', 'ignore:Module testtools was already imported', + subunit_path, 'testtools.tests.test_suite'] + process = subprocess.Popen( + cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env) + _make_stream_binary(process.stdout) + _make_stream_binary(process.stderr) + # XXX: This buffers everything. Bad for memory, bad for getting progress + # on jenkins. + output, error = process.communicate() + protocol.readFrom(BytesIO(output)) + if error: + result.startTest(test) + result.addError(test, details={ + 'stderr': text_content(error), + }) + result.stopTest(test) + result.time(now()) + # XXX: Correct API, but subunit doesn't support it. :( + #result.tags(set(), set(python)) + + +def now(): + return datetime.utcnow().replace(tzinfo=iso8601.Utc()) + + + +if __name__ == '__main__': + sys.path.append(ROOT) + result = TestProtocolClient(sys.stdout) + for version in '2.4 2.5 2.6 2.7 3.0 3.1 3.2'.split(): + run_for_python(version, result) diff --git a/lib/testtools/scripts/update-rtfd b/lib/testtools/scripts/update-rtfd new file mode 100755 index 0000000000..92a19daaa6 --- /dev/null +++ b/lib/testtools/scripts/update-rtfd @@ -0,0 +1,11 @@ +#!/usr/bin/python + +from StringIO import StringIO +from urllib2 import urlopen + + +WEB_HOOK = 'http://readthedocs.org/build/588' + + +if __name__ == '__main__': + urlopen(WEB_HOOK, data=' ') |