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
MANIFEST
build
dist
.tox
*.egg
.coverage
tags
from __future__ import print_function
from __future__ import unicode_literals
import errno
import glob
import hexagonit.recipe.download
import logging
import os
import re
import shutil
import stat
import subprocess
import urllib
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):
"""zc.buildout recipe for compiling and installing software"""
......@@ -21,9 +29,9 @@ class Recipe(object):
self.log = logging.getLogger(name)
options['location'] = os.path.join(
buildout['buildout']['parts-directory'],
self.name,
)
buildout['buildout']['parts-directory'],
self.name,
)
if 'gems' not in options:
self.log.error("Missing 'gems' option.")
......@@ -37,24 +45,27 @@ class Recipe(object):
def run(self, cmd, environ=None):
"""Run the given ``cmd`` in a child process."""
log = logging.getLogger(self.name)
env = os.environ.copy()
if environ:
env.update(environ)
try:
retcode = subprocess.call(cmd, shell=True, env=env)
if retcode < 0:
log.error('Command received signal %s: %s' % (-retcode, cmd))
subprocess.check_output(cmd, env=env)
except OSError as e:
self.log.error('Command failed: %s: %s' % (e, 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')
elif retcode > 0:
log.error('Command failed with exit code %s: %s' % (retcode, cmd))
elif e.returncode > 0:
self.log.error('Command failed with exit code %s: %s' % (
e.returncode, e.cmd
))
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):
pass
......@@ -65,10 +76,12 @@ class Recipe(object):
def _get_env_override(self, env):
env = filter(None, map(strip, env.splitlines()))
try:
env = [map(strip, line.split('=', 1)) for line in env]
except ValueError: # Unpacking impossible
env = list([(key, val) for key, val in [
map(strip, line.split('=', 1)) for line in env
]])
except ValueError: # Unpacking impossible
self.log.error("Every environment line should contain a '=' sign")
zc.buildout.UserError('Configuration error')
raise zc.buildout.UserError('Configuration error')
return env
def _get_env(self):
......@@ -80,35 +93,37 @@ class Recipe(object):
env = {
'GEM_HOME': '%(PREFIX)s/lib/ruby/gems/1.8' % s,
'RUBYLIB': self._join_paths(
'%(RUBYLIB)s',
'%(PREFIX)s/lib',
'%(PREFIX)s/lib/ruby',
'%(PREFIX)s/lib/site_ruby/1.8',
) % s,
'%(RUBYLIB)s',
'%(PREFIX)s/lib',
'%(PREFIX)s/lib/ruby',
'%(PREFIX)s/lib/site_ruby/1.8',
) % s,
'PATH': self._join_paths(
'%(PATH)s',
'%(PREFIX)s/bin',
) % s,
'%(PATH)s',
'%(PREFIX)s/bin',
) % s,
}
env_override = self.options.get('environment', '')
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
def _get_latest_rubygems(self):
if self.url:
version = self.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)
if self.version:
return ('http://production.cf.rubygems.org/rubygems/'
'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 = unicode(s)
s = str(s)
f.close()
r = re.search(r'http://production.cf.rubygems.org/rubygems/'
r'rubygems-([0-9.]+).zip', s)
......@@ -117,41 +132,50 @@ class Recipe(object):
version = r.group(1)
return (url, version)
else:
return None
self.log.error("Can't find latest rubygems version.")
raise zc.buildout.UserError('Configuration error')
def _install_rubygems(self):
url, version = self._get_latest_rubygems()
opt = self.options.copy()
opt['url'] = url
opt['destination'] = self.buildout['buildout']['parts-directory']
hexagonit.recipe.download.Recipe(self.buildout, self.name,
opt).install()
options = {
'url': url,
'destination': self.buildout['buildout']['parts-directory'],
}
HexagonitDownload = hexagonit.recipe.download.Recipe
recipe = HexagonitDownload(self.buildout, self.name, options)
recipe.install()
current_dir = os.getcwd()
try:
os.mkdir(self.options['location'])
except OSError, e:
except OSError as e:
if e.errno == errno.EEXIST:
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'],
'rubygems-%s' % version)
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:
env = self._get_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)
self.run(cmd, env)
finally:
shutil.rmtree(srcdir)
os.chdir(current_dir)
......@@ -167,9 +191,39 @@ class Recipe(object):
f = open(executable, 'w')
f.write('\n'.join(content) + '\n\n')
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
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):
gem_executable = os.path.join(bindir, 'gem')
gem_executable = glob.glob(gem_executable + '*')
......@@ -188,29 +242,13 @@ class Recipe(object):
gem_executable = self.get_gem_executable(bindir)
for gemname in self.gems:
extra = self.options.get('gem-options', '')
extra = ' '.join(filter(None, map(strip, extra.splitlines())))
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())
self.log.info('installing ruby gem "%s"' % gemname)
self._install_gem(gemname, gem_executable, bindir)
for executable in os.listdir(bindir):
installed_path = self._install_executable(
os.path.join(bindir, executable))
os.path.join(bindir, executable)
)
parts.append(installed_path)
return parts
[nosetests]
with-coverage = 1
cover-package = rubygems
cover-erase = 1
tests = tests
nocapture = 1
......@@ -16,14 +16,6 @@ setup(name=name,
version=version,
description="zc.buildout recipe for installing ruby gems.",
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_email='sirexas@gmail.com',
url='https://bitbucket.org/sirex/rubygemsrecipe',
......@@ -31,13 +23,24 @@ setup(name=name,
py_modules=['rubygems'],
include_package_data=True,
zip_safe=False,
use_2to3 = True,
install_requires=[
'six',
'zc.buildout',
'setuptools',
'hexagonit.recipe.download'
],
tests_require=[
'mock',
'pathlib',
],
entry_points={
'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