Commit 29314ff3 authored by Will Bond's avatar Will Bond

Merge pull request #1669 from wbond/better_tests

Better tests
parents c20ea112 58336fe6
language: python language: python
python: python:
- "3.3" - "3.3"
#command to run tests #command to run tests
script: nosetests script: python -m unittest
...@@ -4,7 +4,6 @@ ...@@ -4,7 +4,6 @@
"./repository.json", "./repository.json",
"http://release.latexing.com/packages.json", "http://release.latexing.com/packages.json",
"http://release.sublimegit.net/packages.json", "http://release.sublimegit.net/packages.json",
"https://sublime.wbond.net/packages_2.json",
"http://wuub.net/packages.json", "http://wuub.net/packages.json",
"https://bitbucket.org/artyom_smirnov/sublimetext2-packages/raw/master/packages.json", "https://bitbucket.org/artyom_smirnov/sublimetext2-packages/raw/master/packages.json",
"https://bitbucket.org/jjones028/p4sublime/raw/tip/packages.json", "https://bitbucket.org/jjones028/p4sublime/raw/tip/packages.json",
...@@ -151,6 +150,7 @@ ...@@ -151,6 +150,7 @@
"https://raw.github.com/yangsu/sublime-io/master/packages.json", "https://raw.github.com/yangsu/sublime-io/master/packages.json",
"https://raw.github.com/yangsu/sublime-octopress/master/packages.json", "https://raw.github.com/yangsu/sublime-octopress/master/packages.json",
"https://raw.github.com/yangsu/sublime-vhdl/master/packages.json", "https://raw.github.com/yangsu/sublime-vhdl/master/packages.json",
"https://raw.github.com/zfkun/sublime-kissy-snippets/master/packages.json" "https://raw.github.com/zfkun/sublime-kissy-snippets/master/packages.json",
"https://sublime.wbond.net/packages_2.json"
] ]
} }
...@@ -142,7 +142,7 @@ ...@@ -142,7 +142,7 @@
{ {
"sublime_text": "*", "sublime_text": "*",
"details": "https://github.com/RobinMalfait/Devrabbit-Paste-Sublime-Text-2/tree/master" "details": "https://github.com/RobinMalfait/Devrabbit-Paste-Sublime-Text-2/tree/master"
} }
] ]
}, },
{ {
......
...@@ -298,31 +298,31 @@ ...@@ -298,31 +298,31 @@
] ]
}, },
{ {
"name": "Google Translate", "name": "Google Spell Check",
"details": "https://github.com/lfont/Sublime-Text-2-GoogleTranslate-Plugin", "details": "https://github.com/noahcoad/google-spell-check",
"releases": [ "releases": [
{ {
"sublime_text": "<3000", "sublime_text": "<3000",
"details": "https://github.com/lfont/Sublime-Text-2-GoogleTranslate-Plugin/tree/master" "details": "https://github.com/noahcoad/google-spell-check/tree/master"
} }
] ]
}, },
{ {
"details": "https://bitbucket.org/nwjlyons/google-search", "name": "Google Translate",
"details": "https://github.com/lfont/Sublime-Text-2-GoogleTranslate-Plugin",
"releases": [ "releases": [
{ {
"sublime_text": "*", "sublime_text": "<3000",
"details": "https://bitbucket.org/nwjlyons/google-search/src/default" "details": "https://github.com/lfont/Sublime-Text-2-GoogleTranslate-Plugin/tree/master"
} }
] ]
}, },
{ {
"name": "Google Spell Check", "details": "https://bitbucket.org/nwjlyons/google-search",
"details": "https://github.com/noahcoad/google-spell-check",
"releases": [ "releases": [
{ {
"sublime_text": "<3000", "sublime_text": "*",
"details": "https://github.com/noahcoad/google-spell-check/tree/master" "details": "https://bitbucket.org/nwjlyons/google-search/src/default"
} }
] ]
}, },
......
...@@ -166,6 +166,16 @@ ...@@ -166,6 +166,16 @@
} }
] ]
}, },
{
"name": "Paste Laravel",
"details": "https://github.com/RobinMalfait/Laravel-paste",
"releases": [
{
"sublime_text": "*",
"details": "https://github.com/RobinMalfait/Laravel-paste/tree/master"
}
]
},
{ {
"name": "Paste PDF Text Block", "name": "Paste PDF Text Block",
"details": "https://github.com/compleatang/sublimetext-pastepdf", "details": "https://github.com/compleatang/sublimetext-pastepdf",
...@@ -205,16 +215,6 @@ ...@@ -205,16 +215,6 @@
} }
] ]
}, },
{
"name": "Paste Laravel",
"details": "https://github.com/RobinMalfait/Laravel-paste",
"releases": [
{
"sublime_text": "*",
"details": "https://github.com/RobinMalfait/Laravel-paste/tree/master"
}
]
},
{ {
"name": "PasteSelOnClick", "name": "PasteSelOnClick",
"details": "https://bitbucket.org/Clams/pasteselonclick", "details": "https://bitbucket.org/Clams/pasteselonclick",
......
...@@ -297,22 +297,22 @@ ...@@ -297,22 +297,22 @@
] ]
}, },
{ {
"name": "SelectionTools", "name": "Select Quoted",
"details": "https://github.com/simonrad/sublime-selection-tools", "details": "https://github.com/int3h/SublimeSelectQuoted",
"releases": [ "releases": [
{ {
"sublime_text": "<3000", "sublime_text": "*",
"details": "https://github.com/simonrad/sublime-selection-tools/tree/master" "details": "https://github.com/int3h/SublimeSelectQuoted/tags"
} }
] ]
}, },
{ {
"name": "Select Quoted", "name": "SelectionTools",
"details": "https://github.com/int3h/SublimeSelectQuoted", "details": "https://github.com/simonrad/sublime-selection-tools",
"releases": [ "releases": [
{ {
"sublime_text": "*", "sublime_text": "<3000",
"details": "https://github.com/int3h/SublimeSelectQuoted/tags" "details": "https://github.com/simonrad/sublime-selection-tools/tree/master"
} }
] ]
}, },
...@@ -792,22 +792,22 @@ ...@@ -792,22 +792,22 @@
] ]
}, },
{ {
"name": "SortBy", "name": "Sort Lines (Numerically)",
"details": "https://github.com/Doi9t/SortBy", "details": "https://github.com/alimony/sublime-sort-numerically",
"releases": [ "releases": [
{ {
"sublime_text": "<3000", "sublime_text": "<3000",
"details": "https://github.com/Doi9t/SortBy/tree/master" "details": "https://github.com/alimony/sublime-sort-numerically/tree/master"
} }
] ]
}, },
{ {
"name": "Sort Lines (Numerically)", "name": "SortBy",
"details": "https://github.com/alimony/sublime-sort-numerically", "details": "https://github.com/Doi9t/SortBy",
"releases": [ "releases": [
{ {
"sublime_text": "<3000", "sublime_text": "<3000",
"details": "https://github.com/alimony/sublime-sort-numerically/tree/master" "details": "https://github.com/Doi9t/SortBy/tree/master"
} }
] ]
}, },
...@@ -1534,7 +1534,7 @@ ...@@ -1534,7 +1534,7 @@
] ]
}, },
{ {
"details": "https://github.com/ostinelli/SublimErl/tree/package", "details": "https://github.com/ostinelli/SublimErl",
"releases": [ "releases": [
{ {
"sublime_text": "<3000", "sublime_text": "<3000",
......
#!/usr/bin/env python3 #!/usr/bin/env python3
"""Tests for the validity of the channel file """Tests for the validity of the channel and repository files.
You can run this file directly or with `notetests` (or `python -m unittest`) You can run this script directly or with `python -m unittest` from this or the
from the root directory. root directory. For some reason `nosetests` does not pick up the generated tests
even though they are generated at load time.
However, only running the script directly will generate tests for all
repositories in channel.json. This is to reduce the load time for every test run
by travis (and reduces unnecessary failures).
""" """
import os import os
import re
import json import json
import unittest import unittest
from collections import OrderedDict
from nose.tools import assert_equal, assert_in, assert_not_in, assert_regexp_matches
# Generator tests can't be part of a class, so for consistency from collections import OrderedDict
# they are all functions from functools import wraps
from urllib.request import urlopen
from urllib.error import HTTPError
def test_channel():
with open("channel.json") as fp:
data = json.load(fp)
keys = sorted(data.keys())
assert_equal(keys, ['repositories', 'schema_version'])
assert_equal(data['schema_version'], '2.0') ################################################################################
assert_equal(type(data['repositories']), list) # Utilities
for repository in data['repositories']:
assert_equal(type(repository), str)
def _open(filepath, *args, **kwargs):
"""Wrapper function that can search one dir above if the desired file
does not exist.
"""
if not os.path.exists(filepath):
filepath = os.path.join("..", filepath)
def test_repository(): return open(filepath, *args, **kwargs)
with open('repository.json') as f:
data = json.load(f, object_pairs_hook=OrderedDict)
keys = sorted(data.keys())
assert_equal(keys, ['includes', 'packages', 'schema_version'])
assert_equal(data['schema_version'], '2.0') def generator_class(cls):
assert_equal(data['packages'], []) """Class decorator for classes that use test generating methods.
assert_equal(type(data['includes']), list)
for include in data['includes']: A class that is decorated with this function will be searched for methods
assert_equal(type(include), str) starting with "generate_" (similar to "test_") and then run like a nosetest
generator.
Note: The generator function must be a classmethod!
Generate tests using the following statement:
yield method, (arg1, arg2, arg3) # ...
"""
for name in list(cls.__dict__.keys()):
generator = getattr(cls, name)
if not name.startswith("generate_") or not callable(generator):
continue
def test_repository_includes(): if not generator.__class__.__name__ == 'method':
with open('repository.json') as f: raise TypeError("Generator methods must be classmethods")
data = json.load(f, object_pairs_hook=OrderedDict)
for include in data['includes']: # Create new methods for each `yield`
yield check_include, include for sub_call in generator():
method, params = sub_call
with open(include) as f: @wraps(method)
include_data = json.load(f, object_pairs_hook=OrderedDict) def wrapper(self, method=method, params=params):
for package in include_data['packages']: return method(self, *params)
yield check_package, package
if 'releases' in package:
for release in package['releases']:
yield check_release, package, release
# Do not attempt to print lists/dicts with printed lenght of 1000 or
# more, they are not interesting for us (probably the whole file)
args = []
for v in params:
string = repr(v)
if len(string) > 1000:
args.append('...')
else:
args.append(repr(v))
def check_include(filename): mname = method.__name__
with open(filename) as f: if mname.startswith("_test"):
data = json.load(f, object_pairs_hook=OrderedDict) mname = mname[1:]
keys = sorted(data.keys()) elif not mname.startswith("test_"):
assert_equal(keys, ['packages', 'schema_version']) mname = "test_" + mname
assert_equal(data['schema_version'], '2.0')
assert_equal(type(data['packages']), list)
name = "%s(%s)" % (mname, ", ".join(args))
setattr(cls, name, wrapper)
def check_package(data): # Remove the generator afterwards, it did its work
for key in data.keys(): delattr(cls, name)
assert_in(key, ['name', 'details', 'releases', 'homepage', 'author',
'readme', 'issues', 'donate', 'buy', 'previous_names', 'labels'])
assert_equal(type(data[key]), map_key_type(key))
if key in ['details', 'homepage', 'readme', 'issues', 'donate', 'buy']:
assert_regexp_matches(data[key], '^https?://')
if 'details' not in data: return cls
assert_in('name', data, 'The key "name" is required if no "details" URL provided')
assert_in('homepage', data, 'The key "homepage" is required if no "details" URL provided')
assert_in('author', data, 'The key "author" is required if no "details" URL provided')
assert_in('releases', data, 'The key "releases" is required if no "details" URL provided')
def check_release(package, data): def get_package_name(data):
for key in data.keys(): """Gets "name" from a package with a workaround when it's not defined.
assert_not_in(key, ['version', 'date', 'url'], 'The version, date and ' + \
'url keys should not be used in the main repository since a pull ' + \
'request would be necessary for every release')
assert_in(key, ['details', 'sublime_text', 'platforms']) Use the last part of details url for the package's name otherwise since
packages must define one of these two keys anyway.
"""
return data.get('name') or data.get('details').rsplit('/', 1)[-1]
if key in ['details', 'url']:
assert_regexp_matches(data[key], '^https?://')
if key == 'sublime_text': ################################################################################
assert_regexp_matches(data[key], '^(\*|<=?\d{4}|>=?\d{4})$') # Tests
if key == 'platforms':
assert_in(type(data[key]), [str, list])
if type(data[key]) == str:
assert_in(data[key], ['*', 'osx', 'linux', 'windows'])
else:
for platform in data[key]:
assert_in(platform, ['*', 'osx', 'linux', 'windows'])
assert_in('details', data, 'A release must have a "details" key if it is in ' + \ class TestContainer(object):
'the main repository. For custom releases, a custom repository.json ' + \ """Contains tests that the generators can easily access (when subclassing).
'file must be hosted elsewhere.')
Does not contain tests itself, must be used as mixin with unittest.TestCase.
"""
def map_key_type(key): package_key_types_map = {
return {
'name': str, 'name': str,
'details': str, 'details': str,
'description': str,
'releases': list, 'releases': list,
'homepage': str, 'homepage': str,
'author': str, 'author': str,
...@@ -123,12 +120,233 @@ def map_key_type(key): ...@@ -123,12 +120,233 @@ def map_key_type(key):
'buy': str, 'buy': str,
'previous_names': list, 'previous_names': list,
'labels': list 'labels': list
}.get(key) }
def _test_repository_keys(self, include, data):
keys = sorted(data.keys())
self.assertEqual(keys, ['packages', 'schema_version'])
self.assertEqual(data['schema_version'], '2.0')
self.assertIsInstance(data['packages'], list)
def _test_repository_package_order(self, include, data):
m = re.search(r"(?:^|/)(0-9|[a-z])\.json$", include)
if not m:
self.fail("Include filename does not match")
# letter = include[-6]
letter = m.group(1)
packages = [get_package_name(pdata) for pdata in data['packages']]
# Check if in the correct file
for package_name in packages:
if letter == '0-9':
self.assertTrue(package_name[0].isdigit())
else:
self.assertEqual(package_name[0].lower(), letter,
"Package inserted in wrong file")
# Check package order
self.assertEqual(packages, sorted(packages, key=str.lower))
def _test_package(self, include, data):
for key in data.keys():
self.assertIn(key, self.package_key_types_map)
self.assertIsInstance(data[key], self.package_key_types_map[key])
if key in ('details', 'homepage', 'readme', 'issues', 'donate',
'buy'):
self.assertRegex(data[key], '^https?://')
if 'details' not in data:
for key in ('name', 'homepage', 'author', 'releases'):
self.assertIn(key, data, '%r is required if no "details" URL '
'provided' % key)
def _test_release(self, package_name, data, main_repo=True):
# Fail early
if main_repo:
self.assertIn('details', data,
'A release must have a "details" key if it is in the '
'main repository. For custom releases, a custom '
'repository.json file must be hosted elsewhere.')
elif not 'details' in data:
for req in ('url', 'version', 'date'):
self.assertIn(req, data,
'A release must provide "url", "version" and '
'"date" keys if it does not specify "details"')
for k, v in data.items():
self.assertIn(k, ('details', 'sublime_text', 'platforms',
'version', 'date', 'url'))
if main_repo:
self.assertNotIn(k, ('version', 'date', 'url'),
'The version, date and url keys should not be '
'used in the main repository since a pull '
'request would be necessary for every release')
else:
if k == 'date':
self.assertRegex(v, r"^\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d$")
if k == 'details':
self.assertRegex(v, '^https?://')
if k == 'sublime_text':
self.assertRegex(v, '^(\*|<=?\d{4}|>=?\d{4})$')
if k == 'platforms':
self.assertIsInstance(v, (str, list))
if isinstance(v, str):
v = [v]
for plat in v:
self.assertRegex(plat,
r"^\*|(osx|linux|windows)(-x(32|64))?$")
def _test_error(self, msg, e=None):
if e:
if isinstance(e, HTTPError):
self.fail("%s: %s" % (msg, e))
else:
self.fail("%s: %r" % (msg, e))
else:
self.fail(msg)
@classmethod
def _fail(cls, *args):
return cls._test_error, args
@generator_class
class ChannelTests(TestContainer, unittest.TestCase):
maxDiff = None
with _open('channel.json') as f:
j = json.load(f)
def test_channel_keys(self):
keys = sorted(self.j.keys())
self.assertEqual(keys, ['repositories', 'schema_version'])
self.assertEqual(self.j['schema_version'], '2.0')
self.assertIsInstance(self.j['repositories'], list)
for repo in self.j['repositories']:
self.assertIsInstance(repo, str)
def test_channel_repo_order(self):
repos = self.j['repositories']
self.assertEqual(repos, sorted(repos, key=str.lower))
@classmethod
def generate_repository_tests(cls):
if __name__ != '__main__':
# Do not generate tests for all repositories (those hosted online)
# when testing with unittest's crawler; only when run directly.
return
for repository in cls.j['repositories']:
if repository.startswith('.'):
continue
if not repository.startswith("http"):
raise
print("fetching %s" % repository)
# Download the repository
try:
with urlopen(repository) as f:
source = f.read().decode("utf-8")
except Exception as e:
yield cls._fail("Downloading %s failed" % repository, e)
continue
if not source:
yield cls._fail("%s is empty" % repository)
continue
# Parse the repository (do not consider their includes)
try:
data = json.loads(source)
except Exception as e:
yield cls._fail("Could not parse %s" % repository ,e)
continue
# Check for the schema version first (and generator failures it's
# badly formatted)
if 'schema_version' not in data:
yield cls._fail("No schema_version found in %s" % repository)
continue
schema = float(data['schema_version'])
if schema not in (1.0, 1.1, 1.2, 2.0):
yield cls._fail("Unrecognized schema version %s in %s"
% (schema, repository))
continue
# Do not generate 1000 failing tests for not yet updated repos
if schema != 2.0:
print("schema version %s, skipping" % data['schema_version'])
continue
# `repository` is for output during tests only
yield cls._test_repository_keys, (repository, data)
for package in data['packages']:
yield cls._test_package, (repository, package)
package_name = get_package_name(package)
if 'releases' in package:
for release in package['releases']:
(yield cls._test_release,
("%s (%s)" % (package_name, repository),
release, False))
@generator_class
class RepositoryTests(TestContainer, unittest.TestCase):
maxDiff = None
with _open('repository.json') as f:
j = json.load(f, object_pairs_hook=OrderedDict)
def test_repository_keys(self):
keys = sorted(self.j.keys())
self.assertEqual(keys, ['includes', 'packages', 'schema_version'])
self.assertEqual(self.j['schema_version'], '2.0')
self.assertEqual(self.j['packages'], [])
self.assertIsInstance(self.j['includes'], list)
for include in self.j['includes']:
self.assertIsInstance(include, str)
@classmethod
def generate_include_tests(cls):
for include in cls.j['includes']:
try:
with _open(include) as f:
data = json.load(f, object_pairs_hook=OrderedDict)
except Exception as e:
yield cls._test_error, ("Error while reading %r" % include, e)
continue
# `include` is for output during tests only
yield cls._test_repository_keys, (include, data)
yield cls._test_repository_package_order, (include, data)
for package in data['packages']:
yield cls._test_package, (include, package)
package_name = get_package_name(package)
if 'releases' in package:
for release in package['releases']:
yield cls._test_release, (package_name, release)
################################################################################
# Main
if __name__ == '__main__': if __name__ == '__main__':
# Manually go up one directory if this file is run explicitly
if not os.path.exists(repo_file):
repo_file = os.path.join("..", repo_file)
unittest.main() unittest.main()
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