diff options
author | wbond <will@wbond.net> | 2019-09-09 01:53:00 -0400 |
---|---|---|
committer | wbond <will@wbond.net> | 2019-09-13 06:41:24 -0400 |
commit | bba5d621c393b86e6fb0838e631ccea252852189 (patch) | |
tree | 1e700c6b17079456926febf5f646911828d685c7 | |
parent | 35d686750d96fddcdef6c44d7514dde71dfb0ca0 (diff) | |
download | asn1crypto-bba5d621c393b86e6fb0838e631ccea252852189.tar.gz |
Create asn1crypto_tests package, along with supporting tooling
Adds the following tasks:
- python run.py build
- python run.py version {pep440_version}
Tests may now be executed a number of different ways and will
automatically ensure the local copy of asn1crypto is used, if run from
a Git working copy, or archive of a working copy.
Versioning scheme switched from SemVer to PEP 440 since that is what
the Python ecosystem tooling supports.
-rw-r--r-- | MANIFEST.in | 3 | ||||
-rw-r--r-- | asn1crypto/version.py | 4 | ||||
-rw-r--r-- | dev/__init__.py | 3 | ||||
-rw-r--r-- | dev/_import.py | 93 | ||||
-rw-r--r-- | dev/build.py | 89 | ||||
-rw-r--r-- | dev/ci.py | 23 | ||||
-rw-r--r-- | dev/deps.py | 84 | ||||
-rw-r--r-- | dev/release.py | 19 | ||||
-rw-r--r-- | dev/tests.py | 58 | ||||
-rw-r--r-- | dev/version.py | 80 | ||||
-rw-r--r-- | readme.md | 38 | ||||
-rw-r--r-- | requires/release | 4 | ||||
-rw-r--r-- | run.py | 16 | ||||
-rw-r--r-- | setup.cfg | 5 | ||||
-rw-r--r-- | setup.py | 97 | ||||
-rw-r--r-- | tests/LICENSE | 19 | ||||
-rw-r--r-- | tests/__init__.py | 61 | ||||
-rw-r--r-- | tests/__main__.py | 14 | ||||
-rw-r--r-- | tests/readme.md | 9 | ||||
-rw-r--r-- | tests/setup.py | 153 |
20 files changed, 764 insertions, 108 deletions
diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index 40e672e..0000000 --- a/MANIFEST.in +++ /dev/null @@ -1,3 +0,0 @@ -include LICENSE -include readme.md changelog.md -recursive-include docs *.md diff --git a/asn1crypto/version.py b/asn1crypto/version.py index 1874cfe..aa36a93 100644 --- a/asn1crypto/version.py +++ b/asn1crypto/version.py @@ -2,5 +2,5 @@ from __future__ import unicode_literals, division, absolute_import, print_function -__version__ = '0.25.0-alpha' -__version_info__ = (0, 25, 0, 'alpha') +__version__ = '0.25.0.dev1' +__version_info__ = (0, 25, 0, 'dev1') diff --git a/dev/__init__.py b/dev/__init__.py index 403922e..02e9c6c 100644 --- a/dev/__init__.py +++ b/dev/__init__.py @@ -15,6 +15,9 @@ other_packages = [ "ocspbuilder" ] +requires_oscrypto = False +has_tests_package = True + package_root = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) build_root = os.path.abspath(os.path.join(package_root, '..')) diff --git a/dev/_import.py b/dev/_import.py new file mode 100644 index 0000000..2599588 --- /dev/null +++ b/dev/_import.py @@ -0,0 +1,93 @@ +# coding: utf-8 +from __future__ import unicode_literals, division, absolute_import, print_function + +import imp +import sys +import os + +from . import build_root + + +def _import_from(mod, path, mod_dir=None): + """ + Imports a module from a specific path + + :param mod: + A unicode string of the module name + + :param path: + A unicode string to the directory containing the module + + :param mod_dir: + If the sub directory of "path" is different than the "mod" name, + pass the sub directory as a unicode string + + :return: + None if not loaded, otherwise the module + """ + + if mod_dir is None: + mod_dir = mod + + if not os.path.exists(path): + return None + + if not os.path.exists(os.path.join(path, mod_dir)): + return None + + try: + mod_info = imp.find_module(mod_dir, [path]) + return imp.load_module(mod, *mod_info) + except ImportError: + return None + + +def _preload(require_oscrypto, print_info): + """ + Preloads asn1crypto and optionally oscrypto from a local source checkout, + or from a normal install + + :param require_oscrypto: + A bool if oscrypto needs to be preloaded + + :param print_info: + A bool if info about asn1crypto and oscrypto should be printed + """ + + if print_info: + print('Python ' + sys.version.replace('\n', '')) + + asn1crypto = None + oscrypto = None + + if require_oscrypto: + oscrypto_dir = os.path.join(build_root, 'oscrypto') + oscrypto_tests = None + if os.path.exists(oscrypto_dir): + oscrypto_tests = _import_from('oscrypto_tests', oscrypto_dir, 'tests') + if oscrypto_tests is None: + import oscrypto_tests + asn1crypto, oscrypto = oscrypto_tests.local_oscrypto() + + else: + asn1crypto_dir = os.path.join(build_root, 'asn1crypto') + if os.path.exists(asn1crypto_dir): + asn1crypto = _import_from('asn1crypto', asn1crypto_dir) + if asn1crypto is None: + import asn1crypto + + if print_info: + print( + '\nasn1crypto: %s, %s' % ( + asn1crypto.__version__, + os.path.dirname(asn1crypto.__file__) + ) + ) + if require_oscrypto: + print( + 'oscrypto: %s backend, %s, %s' % ( + oscrypto.backend(), + oscrypto.__version__, + os.path.dirname(oscrypto.__file__) + ) + ) diff --git a/dev/build.py b/dev/build.py new file mode 100644 index 0000000..4899594 --- /dev/null +++ b/dev/build.py @@ -0,0 +1,89 @@ +# coding: utf-8 +from __future__ import unicode_literals, division, absolute_import, print_function + +import imp +import os +import tarfile +import zipfile + +import setuptools.sandbox + +from . import package_root, package_name, has_tests_package + + +def _list_zip(filename): + """ + Prints all of the files in a .zip file + """ + + zf = zipfile.ZipFile(filename, 'r') + for name in zf.namelist(): + print(' %s' % name) + + +def _list_tgz(filename): + """ + Prints all of the files in a .tar.gz file + """ + + tf = tarfile.open(filename, 'r:gz') + for name in tf.getnames(): + print(' %s' % name) + + +def run(): + """ + Creates a sdist .tar.gz and a bdist_wheel --univeral .whl + + :return: + A bool - if the packaging process was successful + """ + + setup = os.path.join(package_root, 'setup.py') + tests_root = os.path.join(package_root, 'tests') + tests_setup = os.path.join(tests_root, 'setup.py') + + # Trying to call setuptools.sandbox.run_setup(setup, ['--version']) + # resulted in a segfault, so we do this instead + module_info = imp.find_module('version', [os.path.join(package_root, package_name)]) + version_mod = imp.load_module('%s.version' % package_name, *module_info) + + pkg_name_info = (package_name, version_mod.__version__) + print('Building %s-%s' % pkg_name_info) + + sdist = '%s-%s.tar.gz' % pkg_name_info + whl = '%s-%s-py2.py3-none-any.whl' % pkg_name_info + setuptools.sandbox.run_setup(setup, ['-q', 'sdist']) + print(' - created %s' % sdist) + _list_tgz(os.path.join(package_root, 'dist', sdist)) + setuptools.sandbox.run_setup(setup, ['-q', 'bdist_wheel', '--universal']) + print(' - created %s' % whl) + _list_zip(os.path.join(package_root, 'dist', whl)) + setuptools.sandbox.run_setup(setup, ['-q', 'clean']) + + if has_tests_package: + print('Building %s_tests-%s' % (package_name, version_mod.__version__)) + + tests_sdist = '%s_tests-%s.tar.gz' % pkg_name_info + tests_whl = '%s_tests-%s-py2.py3-none-any.whl' % pkg_name_info + setuptools.sandbox.run_setup(tests_setup, ['-q', 'sdist']) + print(' - created %s' % tests_sdist) + _list_tgz(os.path.join(tests_root, 'dist', tests_sdist)) + setuptools.sandbox.run_setup(tests_setup, ['-q', 'bdist_wheel', '--universal']) + print(' - created %s' % tests_whl) + _list_zip(os.path.join(tests_root, 'dist', tests_whl)) + setuptools.sandbox.run_setup(tests_setup, ['-q', 'clean']) + + dist_dir = os.path.join(package_root, 'dist') + tests_dist_dir = os.path.join(tests_root, 'dist') + os.rename( + os.path.join(tests_dist_dir, tests_sdist), + os.path.join(dist_dir, tests_sdist) + ) + os.rename( + os.path.join(tests_dist_dir, tests_whl), + os.path.join(dist_dir, tests_whl) + ) + os.rmdir(tests_dist_dir) + + return True @@ -3,9 +3,9 @@ from __future__ import unicode_literals, division, absolute_import, print_functi import sys import os -import imp -from . import build_root +from . import build_root, requires_oscrypto +from ._import import _preload deps_dir = os.path.join(build_root, 'modularcrypto-deps') @@ -34,24 +34,7 @@ def run(): A bool - if the linter and tests ran successfully """ - print('Python ' + sys.version.replace('\n', '')) - - oscrypto_tests_module_info = imp.find_module('tests', [os.path.join(build_root, 'oscrypto')]) - oscrypto_tests = imp.load_module('oscrypto.tests', *oscrypto_tests_module_info) - asn1crypto, oscrypto = oscrypto_tests.local_oscrypto() - print( - '\nasn1crypto: %s, %s' % ( - asn1crypto.__version__, - os.path.dirname(asn1crypto.__file__) - ) - ) - print( - 'oscrypto: %s backend, %s, %s' % ( - oscrypto.backend(), - oscrypto.__version__, - os.path.dirname(oscrypto.__file__) - ) - ) + _preload(requires_oscrypto, True) if run_lint: print('') diff --git a/dev/deps.py b/dev/deps.py index d995c55..d4865cf 100644 --- a/dev/deps.py +++ b/dev/deps.py @@ -205,7 +205,7 @@ def _extract_info(archive, info): return None -def _extract_package(deps_dir, pkg_path): +def _extract_package(deps_dir, pkg_path, pkg_dir): """ Extract a .whl, .zip, .tar.gz or .tar.bz2 into a package path to use when running CI tasks @@ -215,6 +215,9 @@ def _extract_package(deps_dir, pkg_path): :param pkg_path: A unicode string of the path to the archive + + :param pkg_dir: + If running setup.py, change to this dir first - a unicode string """ if pkg_path.endswith('.exe'): @@ -249,51 +252,61 @@ def _extract_package(deps_dir, pkg_path): zf.close() return - # Source archives may contain a bunch of other things. - # The following code works for the packages coverage and - # configparser, which are the two we currently require that - # do not provide wheels + # Source archives may contain a bunch of other things, including mutliple + # packages, so we must use setup.py/setuptool to install/extract it + ar = None + staging_dir = os.path.join(deps_dir, '_staging') try: - ar = None ar = _open_archive(pkg_path) - pkg_name = None - base_path = _archive_single_dir(ar) or '' - if len(base_path): - if '-' in base_path: - pkg_name, _ = base_path.split('-', 1) - base_path += '/' - - base_pkg_path = None - if pkg_name is not None: - base_pkg_path = base_path + pkg_name + '/' - src_path = base_path + 'src/' + common_root = _archive_single_dir(ar) members = [] for info in _list_archive_members(ar): - fn = _info_name(info) - if base_pkg_path is not None and fn.startswith(base_pkg_path): - dst_path = fn[len(base_pkg_path) - len(pkg_name) - 1:] - members.append((info, dst_path)) - continue - if fn.startswith(src_path): - members.append((info, fn[len(src_path):])) - continue + dst_rel_path = _info_name(info) + if common_root is not None: + dst_rel_path = dst_rel_path[len(common_root) + 1:] + members.append((info, dst_rel_path)) + + if not os.path.exists(staging_dir): + os.makedirs(staging_dir) - for info, path in members: + for info, rel_path in members: info_data = _extract_info(ar, info) # Dirs won't return a file if info_data is not None: - dst_path = os.path.join(deps_dir, path) + dst_path = os.path.join(staging_dir, rel_path) dst_dir = os.path.dirname(dst_path) if not os.path.exists(dst_dir): os.makedirs(dst_dir) with open(dst_path, 'wb') as f: f.write(info_data) + + setup_dir = staging_dir + if pkg_dir: + setup_dir = os.path.join(staging_dir, pkg_dir) + + root = os.path.abspath(os.path.join(deps_dir, '..')) + install_lib = os.path.basename(deps_dir) + + _execute( + [ + 'python', + 'setup.py', + 'install', + '--root=%s' % root, + '--install-lib=%s' % install_lib, + '--no-compile' + ], + setup_dir + ) + finally: if ar: ar.close() + if staging_dir: + shutil.rmtree(staging_dir) def _stage_requirements(deps_dir, path): @@ -306,7 +319,7 @@ def _stage_requirements(deps_dir, path): A unicode path to a temporary diretory to use for downloads :param path: - A unicoe filesystem path to a requirements file + A unicode filesystem path to a requirements file """ valid_tags = _pep425tags() @@ -320,7 +333,20 @@ def _stage_requirements(deps_dir, path): packages = _parse_requires(path) for p in packages: pkg = p['pkg'] + pkg_sub_dir = None if p['type'] == 'url': + anchor = None + if '#' in pkg: + pkg, anchor = pkg.split('#', 1) + if '&' in anchor: + parts = anchor.split('&') + else: + parts = [anchor] + for part in parts: + param, value = part.split('=') + if param == 'subdirectory': + pkg_sub_dir = value + if pkg.endswith('.zip') or pkg.endswith('.tar.gz') or pkg.endswith('.tar.bz2') or pkg.endswith('.whl'): url = pkg else: @@ -383,7 +409,7 @@ def _stage_requirements(deps_dir, path): local_path = _download(url, deps_dir) - _extract_package(deps_dir, local_path) + _extract_package(deps_dir, local_path, pkg_sub_dir) os.remove(local_path) diff --git a/dev/release.py b/dev/release.py index d5a3c7e..a854196 100644 --- a/dev/release.py +++ b/dev/release.py @@ -1,14 +1,13 @@ # coding: utf-8 from __future__ import unicode_literals, division, absolute_import, print_function -import os import subprocess import sys -import setuptools.sandbox import twine.cli -from . import package_name, package_root +from . import package_name, package_root, has_tests_package +from .build import run as build def run(): @@ -20,8 +19,6 @@ def run(): A bool - if the packaging and upload process was successful """ - setup_file = os.path.join(package_root, 'setup.py') - git_wc_proc = subprocess.Popen( ['git', 'status', '--porcelain', '-uno'], stdout=subprocess.PIPE, @@ -54,14 +51,10 @@ def run(): tag = tag.decode('ascii').strip() - setuptools.sandbox.run_setup( - setup_file, - ['sdist', 'bdist_wheel', '--universal'] - ) + build() twine.cli.dispatch(['upload', 'dist/%s-%s*' % (package_name, tag)]) + if has_tests_package: + twine.cli.dispatch(['upload', 'dist/%s_tests-%s*' % (package_name, tag)]) - setuptools.sandbox.run_setup( - setup_file, - ['clean'] - ) + return True diff --git a/dev/tests.py b/dev/tests.py index e05d4a2..a065c38 100644 --- a/dev/tests.py +++ b/dev/tests.py @@ -1,16 +1,23 @@ # coding: utf-8 from __future__ import unicode_literals, division, absolute_import, print_function -import os import unittest import re import sys +from . import requires_oscrypto +from ._import import _preload + from tests import test_classes -import asn1crypto + +if sys.version_info < (3,): + range = xrange # noqa + from cStringIO import StringIO +else: + from io import StringIO -def run(matcher=None, ci=False): +def run(matcher=None, repeat=1, ci=False): """ Runs the tests @@ -18,24 +25,51 @@ def run(matcher=None, ci=False): A unicode string containing a regular expression to use to filter test names by. A value of None will cause no filtering. + :param repeat: + An integer - the number of times to run the tests + + :param ci: + A bool, indicating if the tests are being run as part of CI + :return: A bool - if the tests succeeded """ - if not ci: - print('Python ' + sys.version.replace('\n', '')) - print('\nasn1crypto: %s, %s\n' % (asn1crypto.__version__, os.path.dirname(asn1crypto.__file__))) + _preload(requires_oscrypto, not ci) - suite = unittest.TestSuite() loader = unittest.TestLoader() + # We have to manually track the list of applicable tests because for + # some reason with Python 3.4 on Windows, the tests in a suite are replaced + # with None after being executed. This breaks the repeat functionality. + test_list = [] for test_class in test_classes(): if matcher: names = loader.getTestCaseNames(test_class) for name in names: if re.search(matcher, name): - suite.addTest(test_class(name)) + test_list.append(test_class(name)) else: - suite.addTest(loader.loadTestsFromTestCase(test_class)) - verbosity = 2 if matcher else 1 - result = unittest.TextTestRunner(stream=sys.stdout, verbosity=verbosity).run(suite) - return result.wasSuccessful() + test_list.append(loader.loadTestsFromTestCase(test_class)) + + stream = sys.stdout + verbosity = 1 + if matcher and repeat == 1: + verbosity = 2 + elif repeat > 1: + stream = StringIO() + + for _ in range(0, repeat): + suite = unittest.TestSuite() + for test in test_list: + suite.addTest(test) + result = unittest.TextTestRunner(stream=stream, verbosity=verbosity).run(suite) + + if len(result.errors) > 0 or len(result.failures) > 0: + if repeat > 1: + print(stream.getvalue()) + return False + + if repeat > 1: + stream.truncate(0) + + return True diff --git a/dev/version.py b/dev/version.py new file mode 100644 index 0000000..3027431 --- /dev/null +++ b/dev/version.py @@ -0,0 +1,80 @@ +# coding: utf-8 +from __future__ import unicode_literals, division, absolute_import, print_function + +import codecs +import os +import re + +from . import package_root, package_name, has_tests_package + + +def run(new_version): + """ + Updates the package version in the various locations + + :param new_version: + A unicode string of the new library version as a PEP 440 version + + :return: + A bool - if the version number was successfully bumped + """ + + # We use a restricted form of PEP 440 versions + version_match = re.match( + r'(\d+)\.(\d+)\.(\d)+(?:\.((?:dev|a|b|rc)\d+))?$', + new_version + ) + if not version_match: + raise ValueError('Invalid PEP 440 version: %s' % new_version) + + new_version_info = ( + int(version_match.group(1)), + int(version_match.group(2)), + int(version_match.group(3)), + ) + if version_match.group(4): + new_version_info += (version_match.group(4),) + + version_path = os.path.join(package_root, package_name, 'version.py') + setup_path = os.path.join(package_root, 'setup.py') + setup_tests_path = os.path.join(package_root, 'tests', 'setup.py') + tests_path = os.path.join(package_root, 'tests', '__init__.py') + + file_paths = [version_path, setup_path] + if has_tests_package: + file_paths.extend([setup_tests_path, tests_path]) + + for file_path in file_paths: + orig_source = '' + with codecs.open(file_path, 'r', encoding='utf-8') as f: + orig_source = f.read() + + found = 0 + new_source = '' + for line in orig_source.splitlines(True): + if line.startswith('__version__ = '): + found += 1 + new_source += '__version__ = %r\n' % new_version + elif line.startswith('__version_info__ = '): + found += 1 + new_source += '__version_info__ = %r\n' % (new_version_info,) + elif line.startswith('PACKAGE_VERSION = '): + found += 1 + new_source += 'PACKAGE_VERSION = %r\n' % new_version + else: + new_source += line + + if found == 0: + raise ValueError('Did not find any versions in %s' % file_path) + + s = 's' if found > 1 else '' + rel_path = file_path[len(package_root) + 1:] + was_were = 'was' if found == 1 else 'were' + if new_source != orig_source: + print('Updated %d version%s in %s' % (found, s, rel_path)) + with codecs.open(file_path, 'w', encoding='utf-8') as f: + f.write(new_source) + else: + print('%d version%s in %s %s up-to-date' % (found, s, rel_path, was_were)) + + return True @@ -13,6 +13,7 @@ A fast, pure Python library for parsing and serializing ASN.1 structures. - [Continuous Integration](#continuous-integration) - [Testing](#testing) - [Development](#development) + - [CI Tasks](#ci-tasks) [![Travis CI](https://api.travis-ci.org/wbond/asn1crypto.svg?branch=master)](https://travis-ci.org/wbond/asn1crypto) [![AppVeyor](https://ci.appveyor.com/api/projects/status/github/wbond/asn1crypto?branch=master&svg=true)](https://ci.appveyor.com/project/wbond/asn1crypto) @@ -161,7 +162,15 @@ links to the source for the various pre-defined type classes. ## Testing -Tests are written using `unittest` and require no third-party packages: +Tests are written using `unittest` and require no third-party packages. + +Depending on what type of source is available for the package, the following +commands can be used to run the test suite. + +### Git Repository + +When working within a Git working copy, or an archive of the Git repository, +the full test suite is run via: ```bash python run.py tests @@ -173,6 +182,25 @@ To run only some tests, pass a regular expression as a parameter to `tests`. python run.py tests ocsp ``` +### PyPi Source Distribution + +When working within an extracted source distribution (aka `.tar.gz`) from +PyPi, the full test suite is run via: + +```bash +python setup.py test +``` + +### Package + +When the package has been installed via pip (or another method), the package +`asn1crypto_tests` may be installed and invoked to run the full test suite: + +```bash +pip install asn1crypto_tests +python -m asn1crypto_tests +``` + ## Development To install the package used for linting, execute: @@ -199,6 +227,12 @@ Coverage is measured by running: python run.py coverage ``` +To change the version number of the package, run: + +```bash +python run.py version {pep440_version} +``` + To install the necessary packages for releasing a new version on PyPI, run: ```bash @@ -207,7 +241,7 @@ pip install --user -r requires/release Releases are created by: - - Making a git tag in [semver](http://semver.org/) format + - Making a git tag in [PEP 440](https://www.python.org/dev/peps/pep-0440/#examples-of-compliant-version-schemes) format - Running the command: ```bash diff --git a/requires/release b/requires/release index af996cf..91cff65 100644 --- a/requires/release +++ b/requires/release @@ -1 +1,3 @@ -twine +wheel>=0.31.0 +twine>=1.11.0 +setuptools>=38.6.0 @@ -11,7 +11,7 @@ else: def show_usage(): - print('Usage: run.py (lint | tests [regex] | coverage | deps | ci | release)', file=sys.stderr) + print('Usage: run.py (lint | tests [regex] | coverage | deps | ci | version {pep440_version} | build | release)', file=sys.stderr) sys.exit(1) @@ -29,10 +29,10 @@ if len(sys.argv) < 2 or len(sys.argv) > 3: task, next_arg = get_arg(1) -if task not in set(['lint', 'tests', 'coverage', 'deps', 'ci', 'release']): +if task not in set(['lint', 'tests', 'coverage', 'deps', 'ci', 'version', 'build', 'release']): show_usage() -if task != 'tests' and len(sys.argv) == 3: +if task != 'tests' and task != 'version' and len(sys.argv) == 3: show_usage() params = [] @@ -54,6 +54,16 @@ elif task == 'deps': elif task == 'ci': from dev.ci import run +elif task == 'version': + from dev.version import run + if len(sys.argv) != 3: + show_usage() + pep440_version, next_arg = get_arg(next_arg) + params.append(pep440_version) + +elif task == 'build': + from dev.build import run + elif task == 'release': from dev.release import run diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index ed8a958..0000000 --- a/setup.cfg +++ /dev/null @@ -1,5 +0,0 @@ -[bdist_wheel] -universal = 1 - -[metadata] -license_file = LICENSE @@ -1,9 +1,69 @@ +import codecs import os import shutil +import sys +import warnings + +import setuptools +from setuptools import setup, Command +from setuptools.command.egg_info import egg_info + + +PACKAGE_NAME = 'asn1crypto' +PACKAGE_VERSION = '0.25.0.dev1' +PACKAGE_ROOT = os.path.dirname(os.path.abspath(__file__)) + + +# setuptools 38.6.0 and newer know about long_description_content_type, but +# distutils still complains about it, so silence the warning +sv = setuptools.__version__ +svi = tuple(int(o) if o.isdigit() else o for o in sv.split('.')) +if svi >= (38, 6): + warnings.filterwarnings( + 'ignore', + "Unknown distribution option: 'long_description_content_type'", + module='distutils.dist' + ) + + +# Try to load the tests first from the source repository layout. If that +# doesn't work, we assume this file is in the release package, and the tests +# are part of the package {PACKAGE_NAME}_tests. +if os.path.exists(os.path.join(PACKAGE_ROOT, 'tests')): + tests_require = [] + test_suite = 'tests.make_suite' +else: + tests_require = ['%s_tests' % PACKAGE_NAME] + test_suite = '%s_tests.make_suite' % PACKAGE_NAME + + +# This allows us to send the LICENSE and docs when creating a sdist. Wheels +# automatically include the LICENSE, and don't need the docs. For these +# to be included, the command must be "python setup.py sdist". +package_data = {} +if sys.argv[1:] == ['sdist'] or sorted(sys.argv[1:]) == ['-q', 'sdist']: + package_data[PACKAGE_NAME] = [ + '../LICENSE', + '../*.md', + '../docs/*.md', + ] -from setuptools import setup, find_packages, Command -from asn1crypto import version +# Ensures a copy of the LICENSE is included with the egg-info for +# install and bdist_egg commands +class EggInfoCommand(egg_info): + def run(self): + egg_info_path = os.path.join( + PACKAGE_ROOT, + '%s.egg-info' % PACKAGE_NAME + ) + if not os.path.exists(egg_info_path): + os.mkdir(egg_info_path) + shutil.copy2( + os.path.join(PACKAGE_ROOT, 'LICENSE'), + os.path.join(egg_info_path, 'LICENSE') + ) + egg_info.run(self) class CleanCommand(Command): @@ -18,30 +78,38 @@ class CleanCommand(Command): pass def run(self): - folder = os.path.dirname(os.path.abspath(__file__)) - for sub_folder in ['build', 'dist', 'asn1crypto.egg-info']: - full_path = os.path.join(folder, sub_folder) + sub_folders = ['build', 'temp', '%s.egg-info' % PACKAGE_NAME] + if self.all: + sub_folders.append('dist') + for sub_folder in sub_folders: + full_path = os.path.join(PACKAGE_ROOT, sub_folder) if os.path.exists(full_path): shutil.rmtree(full_path) - for root, dirnames, filenames in os.walk(os.path.join(folder, 'asn1crypto')): - for filename in filenames: + for root, dirs, files in os.walk(os.path.join(PACKAGE_ROOT, PACKAGE_NAME)): + for filename in files: if filename[-4:] == '.pyc': os.unlink(os.path.join(root, filename)) - for dirname in list(dirnames): + for dirname in list(dirs): if dirname == '__pycache__': shutil.rmtree(os.path.join(root, dirname)) +readme = '' +with codecs.open(os.path.join(PACKAGE_ROOT, 'readme.md'), 'r', 'utf-8') as f: + readme = f.read() + + setup( - name='asn1crypto', - version=version.__version__, + name=PACKAGE_NAME, + version=PACKAGE_VERSION, description=( 'Fast ASN.1 parser and serializer with definitions for private keys, ' 'public keys, certificates, CRL, OCSP, CMS, PKCS#3, PKCS#7, PKCS#8, ' 'PKCS#12, PKCS#5, X.509 and TSP' ), - long_description='Docs for this project are maintained at https://github.com/wbond/asn1crypto#readme.', + long_description=readme, + long_description_content_type='text/markdown', url='https://github.com/wbond/asn1crypto', @@ -76,11 +144,14 @@ setup( keywords='asn1 crypto pki x509 certificate rsa dsa ec dh', - packages=find_packages(exclude=['tests*', 'dev*']), + packages=[PACKAGE_NAME], + package_data=package_data, - test_suite='tests.make_suite', + tests_require=tests_require, + test_suite=test_suite, cmdclass={ 'clean': CleanCommand, + 'egg_info': EggInfoCommand, } ) diff --git a/tests/LICENSE b/tests/LICENSE new file mode 100644 index 0000000..8038d9a --- /dev/null +++ b/tests/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2015-2019 Will Bond <will@wbond.net> + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/tests/__init__.py b/tests/__init__.py index 783a20f..1af7e94 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -6,6 +6,44 @@ import os import unittest +__version__ = '0.25.0.dev1' +__version_info__ = (0, 25, 0, 'dev1') + + +def _import_from(mod, path, mod_dir=None): + """ + Imports a module from a specific path + + :param mod: + A unicode string of the module name + + :param path: + A unicode string to the directory containing the module + + :param mod_dir: + If the sub directory of "path" is different than the "mod" name, + pass the sub directory as a unicode string + + :return: + None if not loaded, otherwise the module + """ + + if mod_dir is None: + mod_dir = mod + + if not os.path.exists(path): + return None + + if not os.path.exists(os.path.join(path, mod_dir)): + return None + + try: + mod_info = imp.find_module(mod_dir, [path]) + return imp.load_module(mod, *mod_info) + except ImportError: + return None + + def make_suite(): """ Constructs a unittest.TestSuite() of all tests for the package. For use @@ -31,11 +69,24 @@ def test_classes(): A list of unittest.TestCase classes """ - # Make sure the module is loaded from this source folder - module_name = 'asn1crypto' - src_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), '..') - module_info = imp.find_module(module_name, [src_dir]) - imp.load_module(module_name, *module_info) + # If we are in a source folder and these tests aren't installed as a + # package, we want to load asn1crypto from this source folder + tests_dir = os.path.dirname(os.path.abspath(__file__)) + + asn1crypto = None + if os.path.basename(tests_dir) == 'tests': + asn1crypto = _import_from( + 'asn1crypto', + os.path.join(tests_dir, '..') + ) + if asn1crypto is None: + import asn1crypto + + if asn1crypto.__version__ != __version__: + raise AssertionError( + ('asn1crypto_tests version %s can not be run with ' % __version__) + + ('asn1crypto version %s' % asn1crypto.__version__) + ) from .test_algos import AlgoTests from .test_cms import CMSTests diff --git a/tests/__main__.py b/tests/__main__.py new file mode 100644 index 0000000..644391e --- /dev/null +++ b/tests/__main__.py @@ -0,0 +1,14 @@ +# coding: utf-8 +from __future__ import unicode_literals, division, absolute_import, print_function + +import sys +import unittest + +from . import test_classes + + +suite = unittest.TestSuite() +loader = unittest.TestLoader() +for test_class in test_classes(): + suite.addTest(loader.loadTestsFromTestCase(test_class)) +unittest.TextTestRunner(stream=sys.stdout, verbosity=2).run(suite) diff --git a/tests/readme.md b/tests/readme.md new file mode 100644 index 0000000..930f7cb --- /dev/null +++ b/tests/readme.md @@ -0,0 +1,9 @@ +# asn1crypto_tests + +Run the test suite via: + +```bash +python -m asn1crypto_tests +``` + +Full documentation a <https://github.com/wbond/asn1crypto#readme>. diff --git a/tests/setup.py b/tests/setup.py new file mode 100644 index 0000000..078c7e4 --- /dev/null +++ b/tests/setup.py @@ -0,0 +1,153 @@ +import codecs +import os +import shutil +import sys +import warnings + +import setuptools +from setuptools import setup, Command +from setuptools.command.egg_info import egg_info + + +PACKAGE_NAME = 'asn1crypto' +PACKAGE_VERSION = '0.25.0.dev1' +TEST_PACKAGE_NAME = '%s_tests' % PACKAGE_NAME + + +# setuptools 38.6.0 and newer know about long_description_content_type, but +# distutils still complains about it, so silence the warning +sv = setuptools.__version__ +svi = tuple(int(o) if o.isdigit() else o for o in sv.split('.')) +if svi >= (38, 6): + warnings.filterwarnings( + 'ignore', + "Unknown distribution option: 'long_description_content_type'", + module='distutils.dist' + ) + + +package_data = { + TEST_PACKAGE_NAME: [ + 'fixtures/*', + 'fixtures/*/*', + ] +} +# This allows us to send the LICENSE when creating a sdist. Wheels +# automatically include the license, and don't need the docs. For these +# to be included, the command must be "python setup.py sdist". +if sys.argv[1:] == ['sdist'] or sorted(sys.argv[1:]) == ['-q', 'sdist']: + package_data[TEST_PACKAGE_NAME].extend([ + 'LICENSE', + 'readme.md', + ]) + + +tests_root = os.path.dirname(os.path.abspath(__file__)) +package_root = os.path.abspath(os.path.join(tests_root, '..')) + + +# Ensures a copy of the LICENSE is included with the egg-info for +# install and bdist_egg commands +class EggInfoCommand(egg_info): + def run(self): + egg_info_path = os.path.join( + tests_root, + '%s.egg-info' % TEST_PACKAGE_NAME + ) + if not os.path.exists(egg_info_path): + os.mkdir(egg_info_path) + shutil.copy2( + os.path.join(package_root, 'LICENSE'), + os.path.join(egg_info_path, 'LICENSE') + ) + egg_info.run(self) + + +class CleanCommand(Command): + user_options = [ + ('all', 'a', '(Compatibility with original clean command)'), + ] + + def initialize_options(self): + self.all = False + + def finalize_options(self): + pass + + def run(self): + sub_folders = ['build', 'temp', '%s.egg-info' % TEST_PACKAGE_NAME] + if self.all: + sub_folders.append('dist') + for sub_folder in sub_folders: + full_path = os.path.join(tests_root, sub_folder) + if os.path.exists(full_path): + shutil.rmtree(full_path) + for root, dirs, files in os.walk(tests_root): + for filename in files: + if filename[-4:] == '.pyc': + os.unlink(os.path.join(root, filename)) + for dirname in list(dirs): + if dirname == '__pycache__': + shutil.rmtree(os.path.join(root, dirname)) + + +readme = '' +with codecs.open(os.path.join(tests_root, 'readme.md'), 'r', 'utf-8') as f: + readme = f.read() + + +setup( + name=TEST_PACKAGE_NAME, + version=PACKAGE_VERSION, + + description=( + 'Test suite for asn1crypto, separated due to file size' + ), + long_description=readme, + long_description_content_type='text/markdown', + + url='https://github.com/wbond/asn1crypto', + + author='wbond', + author_email='will@wbond.net', + + license='MIT', + + classifiers=[ + 'Development Status :: 4 - Beta', + + 'Intended Audience :: Developers', + + 'License :: OSI Approved :: MIT License', + + 'Programming Language :: Python', + 'Programming Language :: Python :: 2', + 'Programming Language :: Python :: 2.6', + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.2', + 'Programming Language :: Python :: 3.3', + 'Programming Language :: Python :: 3.4', + 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: Implementation :: CPython', + 'Programming Language :: Python :: Implementation :: PyPy', + + 'Topic :: Security :: Cryptography', + ], + + keywords='asn1 crypto pki x509 certificate rsa dsa ec dh', + packages=[TEST_PACKAGE_NAME], + package_dir={TEST_PACKAGE_NAME: '.'}, + package_data=package_data, + + install_requires=[ + '%s==%s' % (PACKAGE_NAME, PACKAGE_VERSION), + ], + + cmdclass={ + 'clean': CleanCommand, + 'egg_info': EggInfoCommand, + } +) |