summaryrefslogtreecommitdiff
path: root/lib/testtools/scripts
diff options
context:
space:
mode:
Diffstat (limited to 'lib/testtools/scripts')
-rw-r--r--lib/testtools/scripts/README3
-rw-r--r--lib/testtools/scripts/_lp_release.py230
-rwxr-xr-xlib/testtools/scripts/all-pythons90
-rwxr-xr-xlib/testtools/scripts/update-rtfd11
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=' ')