├── .gitignore ├── .travis.yml ├── README.md ├── conttest ├── __init__.py └── conttest.py └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | dist 3 | *.egg-info 4 | */*.pyc 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.6" 4 | - "2.7" 5 | - "3.2" 6 | - "3.3" 7 | - "pypy" 8 | 9 | env: 10 | - PEP8_IGNORE="" 11 | 12 | # command to install dependencies 13 | install: 14 | - pip install pep8 15 | 16 | # command to run tests 17 | # require 100% coverage (not including test files) to pass Travis CI test 18 | # To skip pypy: - if [[ $TRAVIS_PYTHON_VERSION != 'pypy' ]]; then DOSTUFF ; fi 19 | script: 20 | - if [[ $TRAVIS_PYTHON_VERSION != 'pypy' ]]; then pep8 --ignore=$PEP8_IGNORE -r --show-source . ; fi 21 | - if [[ $TRAVIS_PYTHON_VERSION != 'pypy' ]]; then python -c 'import conttest' ; fi 22 | 23 | notifications: 24 | email: false 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # conttest 2 | 3 | Continuous testing helper, adapted from [1], but which makes no 4 | assumption about what tests you might want to run continuously while 5 | developing. For more information, see 6 | [this blog post](http://zerolib.com/continuous-testing.html). 7 | 8 | **Any command supplied to the script will be run once and then 9 | repeated any time a file in the current working directory changes,** 10 | except for files excluded using `.conttest-excludes` as described below. 11 | 12 | **See also** [2] 13 | 14 | Note that ANY command you supply the script will be run, so be 15 | careful. You have been warned! 16 | 17 | [1] https://github.com/brunobord/tdaemon/blob/master/tdaemon.py 18 | 19 | [2] https://github.com/eigenhombre/conttest (simpler, faster, Go-language version) 20 | 21 | ### Installation 22 | 23 | ./setup.py install # from source 24 | or 25 | 26 | pip install conttest # use sudo if installing globally 27 | 28 | ### Usage examples 29 | 30 | conttest nosetests 31 | conttest 'nosetests -q && pep8 -r .' 32 | 33 | Placing a file `.conttest-excludes` in the current working directory 34 | will exclude subdirectories, e.g.: 35 | 36 | .svn$ 37 | .git 38 | build 39 | vendor/elastic* 40 | install 41 | 42 | will match files against the listed regular expressions and skip checking 43 | for changes in those directories. This can save quite a bit of time and CPU 44 | during normal operation. 45 | 46 | ## Author 47 | 48 | [John Jacobsen](http://zerolib.com) 49 | 50 | ## License 51 | 52 | Eclipse Public License 53 | 54 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 55 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 56 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF THIRD PARTY RIGHTS. IN 57 | NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 58 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 59 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE 60 | OR OTHER DEALINGS IN THE SOFTWARE. 61 | -------------------------------------------------------------------------------- /conttest/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eigenhombre/conttest/f570a8ae10516168f66d98ae015cd89c51ec0d6e/conttest/__init__.py -------------------------------------------------------------------------------- /conttest/conttest.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | Continuous Testing Helper 4 | 5 | Usage: 6 | conttest ... 7 | 8 | """ 9 | 10 | import hashlib 11 | import os.path 12 | import re 13 | import subprocess 14 | import sys 15 | import time 16 | 17 | HASHES = "hashes" 18 | TIMES = "times" 19 | 20 | 21 | def include_file_in_checks(path, excludes): 22 | """ 23 | Determine whether file should be included in checks; reject if 24 | file has an undesired prefix, an undesired file extension, or 25 | lives in an undesired directory. 26 | """ 27 | IGNORE_PREFIXES = ('.', '#') 28 | IGNORE_EXTENSIONS = ('pyc', 'pyo', '_flymake.py') 29 | IGNORE_DIRS = ('.git', '.hg', '.svn') 30 | 31 | basename = os.path.basename(path) 32 | for p in IGNORE_PREFIXES: 33 | if basename.startswith(p): 34 | return False 35 | for extension in IGNORE_EXTENSIONS: 36 | if path.endswith(extension): 37 | return False 38 | parts = path.split(os.path.sep) 39 | for part in parts: 40 | if part in IGNORE_DIRS: 41 | return False 42 | return not excluded_pattern(path, excludes) 43 | 44 | 45 | def excluded_pattern(root, excludes): 46 | for pat in excludes: 47 | if re.search(pat, root): 48 | return True 49 | 50 | 51 | def getstate(full_path, method): 52 | if method == HASHES: 53 | try: 54 | content = open(full_path).read() 55 | except IOError: 56 | return None # will trigger our action 57 | return hashlib.sha224(content).hexdigest() 58 | assert method == TIMES 59 | try: 60 | return os.path.getmtime(full_path) 61 | except OSError: # e.g., broken link 62 | return None 63 | 64 | 65 | def walk(top, method, filestates={}, excludes=[]): 66 | """ 67 | Walk directory recursively, storing a hash value for any 68 | non-excluded file; return a dictionary for all such files. 69 | """ 70 | for root, _, files in os.walk(top, topdown=False): 71 | for name in files: 72 | if excluded_pattern(name, excludes): 73 | continue 74 | full_path = os.path.join(root, name) 75 | if include_file_in_checks(full_path, excludes): 76 | filestates[full_path] = getstate(full_path, method) 77 | return filestates 78 | 79 | 80 | def get_exclude_patterns_from_file(path): 81 | if not os.path.exists(path): 82 | return [] 83 | with file(path) as f: 84 | return [s.strip() for s in f.read().split() 85 | if s.strip() != ""] 86 | 87 | 88 | def show_diffs(a, b): 89 | """ 90 | For debugging: print relevant diffs. 91 | """ 92 | if abs((len(a.keys()) - len(b.keys()))) > 100: 93 | print("Massive change of keys:", len(a.keys()), "->", len(b.keys())) 94 | elif a.keys() != b.keys(): 95 | print(set(a.keys()) - set(b.keys())) 96 | else: 97 | print([(k, a[k], b[k]) for k in a.keys() if a[k] != b[k]]) 98 | 99 | 100 | def watch_dir(dir_, callback, method=HASHES): 101 | """ 102 | Loop continuously, calling function if any non-excluded 103 | file has changed. 104 | """ 105 | filedict = {} 106 | while True: 107 | excludes = get_exclude_patterns_from_file(dir_ + "/.conttest-excludes") 108 | new = walk(dir_, method, {}, excludes=excludes) 109 | if new != filedict: 110 | callback() 111 | # Do it again, in case command changed files (don't retrigger) 112 | filedict = walk(dir_, method, {}, excludes=excludes) 113 | time.sleep(0.3) 114 | 115 | 116 | def do_command_on_update(cmd): 117 | try: 118 | watch_dir(".", lambda: subprocess.call(cmd, shell=True), 119 | method=TIMES) 120 | except KeyboardInterrupt: 121 | print 122 | 123 | 124 | def main(): 125 | cmd = ' '.join(sys.argv[1:]) 126 | if cmd: 127 | do_command_on_update(cmd) 128 | else: 129 | print("Usage: %s command args ..." % __file__) 130 | 131 | 132 | if __name__ == '__main__': 133 | main() 134 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from os.path import exists 4 | try: 5 | # Use setup() from setuptools(/distribute) if available 6 | from setuptools import setup 7 | except ImportError: 8 | from distutils.core import setup 9 | 10 | 11 | setup(name='conttest', 12 | version='0.0.8', 13 | author='John Jacobsen', 14 | author_email='john@mail.npxdesigns.com', 15 | packages=['conttest'], 16 | scripts=[], 17 | url='https://github.com/eigenhombre/continuous-testing-helper', 18 | license='MIT', 19 | description='Simple continuous testing tool', 20 | long_description=open('README.md').read() if exists("README.md") else "", 21 | entry_points=dict(console_scripts=['conttest=conttest.conttest:main']), 22 | install_requires=[]) 23 | --------------------------------------------------------------------------------