Commit d6aa81f0 authored by sirex's avatar sirex

Tests using tox for python 2.7 and 3.4 support

While fixing failing tests, did some code refactoring.
parent 5dfa1418
...@@ -4,3 +4,7 @@ syntax: glob ...@@ -4,3 +4,7 @@ syntax: glob
MANIFEST MANIFEST
build build
dist dist
.tox
*.egg
.coverage
tags
from __future__ import print_function
from __future__ import unicode_literals
import errno import errno
import glob import glob
import hexagonit.recipe.download
import logging import logging
import os import os
import re import re
import shutil import shutil
import stat
import subprocess import subprocess
import urllib
import zc.buildout import zc.buildout
strip = lambda x:x.strip() import six.moves.urllib as urllib
from six import text_type as str
import hexagonit.recipe.download
strip = lambda x:x.strip() # noqa
class Recipe(object): class Recipe(object):
"""zc.buildout recipe for compiling and installing software""" """zc.buildout recipe for compiling and installing software"""
...@@ -21,9 +29,9 @@ class Recipe(object): ...@@ -21,9 +29,9 @@ class Recipe(object):
self.log = logging.getLogger(name) self.log = logging.getLogger(name)
options['location'] = os.path.join( options['location'] = os.path.join(
buildout['buildout']['parts-directory'], buildout['buildout']['parts-directory'],
self.name, self.name,
) )
if 'gems' not in options: if 'gems' not in options:
self.log.error("Missing 'gems' option.") self.log.error("Missing 'gems' option.")
...@@ -37,24 +45,27 @@ class Recipe(object): ...@@ -37,24 +45,27 @@ class Recipe(object):
def run(self, cmd, environ=None): def run(self, cmd, environ=None):
"""Run the given ``cmd`` in a child process.""" """Run the given ``cmd`` in a child process."""
log = logging.getLogger(self.name)
env = os.environ.copy() env = os.environ.copy()
if environ: if environ:
env.update(environ) env.update(environ)
try: try:
retcode = subprocess.call(cmd, shell=True, env=env) subprocess.check_output(cmd, env=env)
except OSError as e:
if retcode < 0: self.log.error('Command failed: %s: %s' % (e, cmd))
log.error('Command received signal %s: %s' % (-retcode, cmd)) raise zc.buildout.UserError('System error')
except subprocess.CalledProcessError as e:
self.log.error(e.output)
if e.returncode < 0:
self.log.error('Command received signal %s: %s' % (
-e.returncode, e.cmd
))
raise zc.buildout.UserError('System error') raise zc.buildout.UserError('System error')
elif retcode > 0: elif e.returncode > 0:
log.error('Command failed with exit code %s: %s' % (retcode, cmd)) self.log.error('Command failed with exit code %s: %s' % (
e.returncode, e.cmd
))
raise zc.buildout.UserError('System error') raise zc.buildout.UserError('System error')
except OSError, e:
log.error('Command failed: %s: %s' % (e, cmd))
raise zc.buildout.UserError('System error')
def update(self): def update(self):
pass pass
...@@ -65,10 +76,12 @@ class Recipe(object): ...@@ -65,10 +76,12 @@ class Recipe(object):
def _get_env_override(self, env): def _get_env_override(self, env):
env = filter(None, map(strip, env.splitlines())) env = filter(None, map(strip, env.splitlines()))
try: try:
env = [map(strip, line.split('=', 1)) for line in env] env = list([(key, val) for key, val in [
except ValueError: # Unpacking impossible map(strip, line.split('=', 1)) for line in env
]])
except ValueError: # Unpacking impossible
self.log.error("Every environment line should contain a '=' sign") self.log.error("Every environment line should contain a '=' sign")
zc.buildout.UserError('Configuration error') raise zc.buildout.UserError('Configuration error')
return env return env
def _get_env(self): def _get_env(self):
...@@ -80,35 +93,37 @@ class Recipe(object): ...@@ -80,35 +93,37 @@ class Recipe(object):
env = { env = {
'GEM_HOME': '%(PREFIX)s/lib/ruby/gems/1.8' % s, 'GEM_HOME': '%(PREFIX)s/lib/ruby/gems/1.8' % s,
'RUBYLIB': self._join_paths( 'RUBYLIB': self._join_paths(
'%(RUBYLIB)s', '%(RUBYLIB)s',
'%(PREFIX)s/lib', '%(PREFIX)s/lib',
'%(PREFIX)s/lib/ruby', '%(PREFIX)s/lib/ruby',
'%(PREFIX)s/lib/site_ruby/1.8', '%(PREFIX)s/lib/site_ruby/1.8',
) % s, ) % s,
'PATH': self._join_paths( 'PATH': self._join_paths(
'%(PATH)s', '%(PATH)s',
'%(PREFIX)s/bin', '%(PREFIX)s/bin',
) % s, ) % s,
} }
env_override = self.options.get('environment', '') env_override = self.options.get('environment', '')
env_override = self._get_env_override(env_override) env_override = self._get_env_override(env_override)
env.update({k: v % env for k, v in env_override}) env.update({k: (v % env) for k, v in env_override})
return env return env
def _get_latest_rubygems(self): def _get_latest_rubygems(self):
if self.url: if self.url:
version = self.version version = self.version
if not version: if not version:
version = re.search(r'rubygems-([0-9.]+).zip$', self.url).group(1) version = (
re.search(r'rubygems-([0-9.]+).zip$', self.url).group(1)
)
return (self.url, version) return (self.url, version)
if self.version: if self.version:
return ('http://production.cf.rubygems.org/rubygems/' return ('http://production.cf.rubygems.org/rubygems/'
'rubygems-%s.zip' % self.version, self.version) 'rubygems-%s.zip' % self.version, self.version)
f = urllib.urlopen('http://rubygems.org/pages/download') f = urllib.request.urlopen('http://rubygems.org/pages/download')
s = f.read() s = f.read()
s = unicode(s) s = str(s)
f.close() f.close()
r = re.search(r'http://production.cf.rubygems.org/rubygems/' r = re.search(r'http://production.cf.rubygems.org/rubygems/'
r'rubygems-([0-9.]+).zip', s) r'rubygems-([0-9.]+).zip', s)
...@@ -117,41 +132,50 @@ class Recipe(object): ...@@ -117,41 +132,50 @@ class Recipe(object):
version = r.group(1) version = r.group(1)
return (url, version) return (url, version)
else: else:
return None self.log.error("Can't find latest rubygems version.")
raise zc.buildout.UserError('Configuration error')
def _install_rubygems(self): def _install_rubygems(self):
url, version = self._get_latest_rubygems() url, version = self._get_latest_rubygems()
opt = self.options.copy() options = {
opt['url'] = url 'url': url,
opt['destination'] = self.buildout['buildout']['parts-directory'] 'destination': self.buildout['buildout']['parts-directory'],
hexagonit.recipe.download.Recipe(self.buildout, self.name, }
opt).install() HexagonitDownload = hexagonit.recipe.download.Recipe
recipe = HexagonitDownload(self.buildout, self.name, options)
recipe.install()
current_dir = os.getcwd() current_dir = os.getcwd()
try: try:
os.mkdir(self.options['location']) os.mkdir(self.options['location'])
except OSError, e: except OSError as e:
if e.errno == errno.EEXIST: if e.errno == errno.EEXIST:
pass pass
else:
self.log.error((
"IO error while creating '%s' directory."
) % self.options['location'])
raise zc.buildout.UserError('Configuration error')
srcdir = os.path.join(self.buildout['buildout']['parts-directory'], srcdir = os.path.join(self.buildout['buildout']['parts-directory'],
'rubygems-%s' % version) 'rubygems-%s' % version)
os.chdir(srcdir) os.chdir(srcdir)
env = self._get_env()
env['PREFIX'] = self.options['location']
cmd = [
self.ruby_executable,
'setup.rb',
'all',
'--prefix=%s' % self.options['location'],
'--no-rdoc',
'--no-ri',
]
try: try:
env = self._get_env() self.run(cmd, env)
env['PREFIX'] = self.options['location']
s = {
'OPTIONS': ' '.join([
'--prefix=%s' % self.options['location'],
'--no-rdoc',
'--no-ri',
]),
'RUBY': self.ruby_executable,
}
self.run('%(RUBY)s setup.rb all %(OPTIONS)s' % s, env)
finally: finally:
shutil.rmtree(srcdir) shutil.rmtree(srcdir)
os.chdir(current_dir) os.chdir(current_dir)
...@@ -167,9 +191,39 @@ class Recipe(object): ...@@ -167,9 +191,39 @@ class Recipe(object):
f = open(executable, 'w') f = open(executable, 'w')
f.write('\n'.join(content) + '\n\n') f.write('\n'.join(content) + '\n\n')
f.close() f.close()
os.chmod(executable, 0755) os.chmod(executable, (
# rwx rw- rw-
stat.S_IRWXU |
stat.S_IRGRP | stat.S_IWGRP |
stat.S_IROTH | stat.S_IWOTH
))
return executable return executable
def _install_gem(self, gemname, gem_executable, bindir):
cmd = [
gem_executable,
'install',
'--no-rdoc',
'--no-ri',
'--bindir=%s' % bindir,
]
if '==' in gemname:
gemname, version = map(strip, gemname.split('==', 1))
cmd.append(gemname)
cmd.append('--version=%s' % version)
else:
cmd.append(gemname)
extra = self.options.get('gem-options', '')
extra = filter(None, map(strip, extra.splitlines()))
cmd.append('--')
cmd.extend(extra)
self.run(cmd, self._get_env())
def get_gem_executable(self, bindir): def get_gem_executable(self, bindir):
gem_executable = os.path.join(bindir, 'gem') gem_executable = os.path.join(bindir, 'gem')
gem_executable = glob.glob(gem_executable + '*') gem_executable = glob.glob(gem_executable + '*')
...@@ -188,29 +242,13 @@ class Recipe(object): ...@@ -188,29 +242,13 @@ class Recipe(object):
gem_executable = self.get_gem_executable(bindir) gem_executable = self.get_gem_executable(bindir)
for gemname in self.gems: for gemname in self.gems:
extra = self.options.get('gem-options', '') self.log.info('installing ruby gem "%s"' % gemname)
extra = ' '.join(filter(None, map(strip, extra.splitlines()))) self._install_gem(gemname, gem_executable, bindir)
s = {
'GEM': gem_executable,
'OPTIONS': ' '.join([
'--no-rdoc',
'--no-ri',
'--bindir=%s' % bindir,
]),
'EXTRA': extra,
}
if '==' in gemname:
gemname, version = map(strip, gemname.split('==', 1))
s['GEMNAME'] = gemname
s['OPTIONS'] += ' --version %s' % version
else:
s['GEMNAME'] = gemname
self.run('%(GEM)s install %(OPTIONS)s %(GEMNAME)s -- %(EXTRA)s' % s,
self._get_env())
for executable in os.listdir(bindir): for executable in os.listdir(bindir):
installed_path = self._install_executable( installed_path = self._install_executable(
os.path.join(bindir, executable)) os.path.join(bindir, executable)
)
parts.append(installed_path) parts.append(installed_path)
return parts return parts
[nosetests]
with-coverage = 1
cover-package = rubygems
cover-erase = 1
tests = tests
nocapture = 1
...@@ -16,14 +16,6 @@ setup(name=name, ...@@ -16,14 +16,6 @@ setup(name=name,
version=version, version=version,
description="zc.buildout recipe for installing ruby gems.", description="zc.buildout recipe for installing ruby gems.",
long_description=(read('README.rst') + '\n' + read('CHANGES.rst')), long_description=(read('README.rst') + '\n' + read('CHANGES.rst')),
classifiers=[
'Framework :: Buildout',
'Intended Audience :: Developers',
'License :: OSI Approved :: GNU General Public License (GPL)',
'Topic :: Software Development :: Libraries :: Ruby Modules',
'Programming Language :: Python :: 2',
'Programming Language :: Python :: 3',
],
author='Mantas Zimnickas', author='Mantas Zimnickas',
author_email='sirexas@gmail.com', author_email='sirexas@gmail.com',
url='https://bitbucket.org/sirex/rubygemsrecipe', url='https://bitbucket.org/sirex/rubygemsrecipe',
...@@ -31,13 +23,24 @@ setup(name=name, ...@@ -31,13 +23,24 @@ setup(name=name,
py_modules=['rubygems'], py_modules=['rubygems'],
include_package_data=True, include_package_data=True,
zip_safe=False, zip_safe=False,
use_2to3 = True,
install_requires=[ install_requires=[
'six',
'zc.buildout', 'zc.buildout',
'setuptools', 'setuptools',
'hexagonit.recipe.download' 'hexagonit.recipe.download'
], ],
tests_require=[
'mock',
'pathlib',
],
entry_points={ entry_points={
'zc.buildout': ['default = rubygems:Recipe'] 'zc.buildout': ['default = rubygems:Recipe']
}) },
classifiers=[
'Framework :: Buildout',
'Intended Audience :: Developers',
'License :: OSI Approved :: GNU General Public License (GPL)',
'Topic :: Software Development :: Libraries :: Ruby Modules',
'Programming Language :: Python :: 2',
'Programming Language :: Python :: 3',
])
[buildout]
parts =
rubygems
develop =
..
[rubygems]
recipe = rubygemsrecipe
gems =
sass
compass==0.10
#!/usr/bin/env python
import re
import os.path
import logging
import subprocess
def sh(cmd):
retcode = subprocess.call(cmd, shell=True)
assert retcode == 0
def shr(cmd):
return subprocess.check_output(cmd, shell=True)
def clean():
logging.info('Cleaning environment...')
paths = (
'.installed.cfg',
'bin',
'bootstrap.py',
'develop-eggs',
'include',
'lib',
'local',
'parts',
)
for path in paths:
if os.path.exists(path):
sh('rm -r %s' % path)
def main():
logging.basicConfig(
format='%(message)s',
level=logging.INFO
)
clean()
sh('wget http://downloads.buildout.org/2/bootstrap.py')
sh('virtualenv --no-site-packages .')
sh('bin/pip install --upgrade setuptools')
sh('bin/python bootstrap.py')
sh('bin/buildout')
assert re.match(
r'^Sass \d+(\.\d+){2} \([a-zA-Z ]+\)$',
shr('bin/sass --version').strip()
)
if __name__ == '__main__':
main()
from __future__ import unicode_literals
import errno
import functools
import mock
import os
import pathlib
import shutil
import subprocess
import tempfile
import unittest
from six import StringIO
import zc.buildout
import rubygems
def touch(path):
with path.open('w') as f:
f.write('')
class fixture(object):
def __init__(self, options=None, version='1.0'):
self.options = options or {}
self.version = version
def __call__(self, func):
@functools.wraps(func)
def wrapper(test):
buildout, name, options = self.set_up()
cwd = os.getcwd()
os.chdir(str(self.tempdir))
func(test, self.tempdir, self.patches, buildout, name, options)
os.chdir(cwd)
self.tear_down()
return wrapper
def patch(self, modules):
self.patchers = {}
self.patches = {}
for name, module in modules:
self.patchers[name] = mock.patch(module)
self.patches[name] = self.patchers[name].start()
def makedirs(self, dirs):
self.tempdir = pathlib.Path(tempfile.mkdtemp())
for directory in dirs:
os.makedirs(str(self.tempdir / directory))
def set_up(self):
name = 'rubygems'
version = self.options.get('return', {}).get('version', self.version)
self.patch((
('check_output', 'rubygems.subprocess.check_output'),
('urlopen', 'rubygems.urllib.request.urlopen'),
('hexagonit', 'rubygems.hexagonit.recipe.download.Recipe'),
))
self.patches['urlopen'].return_value = StringIO(
'http://production.cf.rubygems.org/rubygems/rubygems-1.0.zip'
)
self.makedirs((
'bin',
'ruby-%s' % version,
'rubygems-%s' % version,
'rubygems/bin',
))
buildout = {'buildout': dict({
'parts-directory': str(self.tempdir),
'bin-directory': str(self.tempdir / 'bin'),
}, **self.options.get('buildout', {}))}
options = self.options.get('recipe', {})
return buildout, name, options
def tear_down(self):
for patcher in self.patchers.values():
patcher.stop()
shutil.rmtree(str(self.tempdir))
class RubyGemsTests(unittest.TestCase):
@fixture({'recipe': {'gems': 'sass'}})
def test_success(self, path, patches, buildout, name, options):
recipe = rubygems.Recipe(buildout, name, options)
recipe.install()
# One urlopen call to get latest version
self.assertEqual(patches['urlopen'].call_count, 1)
args = patches['urlopen'].mock_calls[0][1]
self.assertEqual(args, ('http://rubygems.org/pages/download',))
# One hexagonit.recipe.download call to download rubygems
self.assertEqual(patches['hexagonit'].call_count, 1)
args = patches['hexagonit'].mock_calls[0][1]
self.assertEqual(args[2], {
'url': (
'http://production.cf.rubygems.org/rubygems/rubygems-1.0.zip'
),
'destination': str(path),
})
# Two check_output calls to install rubygems and specified gem
self.assertEqual(patches['check_output'].call_count, 2)
args = patches['check_output'].mock_calls[0][1]
self.assertEqual(args[0], [
'ruby', 'setup.rb', 'all', '--prefix=%s/rubygems' % path,
'--no-rdoc', '--no-ri',
])
args = patches['check_output'].mock_calls[1][1]
self.assertEqual(args[0], [
None, 'install', '--no-rdoc', '--no-ri',
'--bindir=%s/rubygems/bin' % path,
'sass', '--'
])
@fixture({'recipe': {}})
def test_missing_gems(self, path, patches, buildout, name, options):
self.assertRaises(
zc.buildout.UserError,
rubygems.Recipe, buildout, name, options
)
@fixture({'recipe': {'gems': 'sass'}})
def test_oserror(self, path, patches, buildout, name, options):
patches['check_output'].side_effect = OSError
recipe = rubygems.Recipe(buildout, name, options)
self.assertRaises(zc.buildout.UserError, recipe.install)
@fixture({'recipe': {'gems': 'sass'}})
def test_signal_received(self, path, patches, buildout, name, options):
exception = subprocess.CalledProcessError(-1, '')
patches['check_output'].side_effect = exception
recipe = rubygems.Recipe(buildout, name, options)
self.assertRaises(zc.buildout.UserError, recipe.install)
@fixture({'recipe': {'gems': 'sass'}})
def test_non_zero_exitcode(self, path, patches, buildout, name, options):
exception = subprocess.CalledProcessError(1, '')
patches['check_output'].side_effect = exception
recipe = rubygems.Recipe(buildout, name, options)
self.assertRaises(zc.buildout.UserError, recipe.install)
@fixture({'recipe': {'gems': 'sass'}})
def test_update(self, path, patches, buildout, name, options):
recipe = rubygems.Recipe(buildout, name, options)
recipe.update()
@fixture({'recipe': {'gems': 'sass', 'environment': 'invalid'}})
def test_invalid_environment(self, path, patches, buildout, name, options):
recipe = rubygems.Recipe(buildout, name, options)
self.assertRaises(zc.buildout.UserError, recipe.install)
@fixture({'recipe': {
'gems': 'sass',
'url': 'http://production.cf.rubygems.org/rubygems/rubygems-1.0.zip',
}})
def test_version_from_url(self, path, patches, buildout, name, options):
recipe = rubygems.Recipe(buildout, name, options)
recipe.install()
@fixture({'recipe': {'gems': 'sass', 'version': '1.0'}})
def test_version(self, path, patches, buildout, name, options):
recipe = rubygems.Recipe(buildout, name, options)
recipe.install()
@fixture({'recipe': {'gems': 'sass'}})
def test_no_version(self, path, patches, buildout, name, options):
patches['urlopen'].return_value = StringIO('')
recipe = rubygems.Recipe(buildout, name, options)
self.assertRaises(zc.buildout.UserError, recipe.install)
@fixture({'recipe': {'gems': 'sass'}})
@mock.patch('rubygems.os.mkdir')
def test_mkdir_error(self, path, patches, buildout, name, options, mkdir):
mkdir.side_effect = OSError(errno.EIO)
recipe = rubygems.Recipe(buildout, name, options)
self.assertRaises(zc.buildout.UserError, recipe.install)
@fixture({'recipe': {'gems': 'sass'}})
def test_executables(self, path, patches, buildout, name, options):
recipe = rubygems.Recipe(buildout, name, options)
touch(pathlib.Path(recipe.options['location']) / 'bin/sass')
recipe.install()
@fixture({'recipe': {'gems': 'sass==1.0'}})
def test_pinned_versions(self, path, patches, buildout, name, options):
recipe = rubygems.Recipe(buildout, name, options)
touch(path / 'rubygems/bin/gem')
recipe.install()
args = patches['check_output'].mock_calls[0][1]
self.assertEqual(args[0], [
'%s/rubygems/bin/gem' % path, 'install', '--no-rdoc', '--no-ri',
'--bindir=%s/rubygems/bin' % path,
'sass', '--version=1.0', '--'
])
# Tox (http://tox.testrun.org/) is a tool for running tests
# in multiple virtualenvs. This configuration file will run the
# test suite on all supported python versions. To use it, "pip install tox"
# and then run "tox" from this directory.
[tox]
envlist = py27, py34
[testenv]
commands = python setup.py nosetests
deps =
coverage==3.7.1
nose==1.3.4
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment