| Left: | ||
| Right: |
| OLD | NEW |
|---|---|
| (Empty) | |
| 1 #!/bin/env python | |
| 2 # This file is part of Adblock Plus <https://adblockplus.org/>, | |
| 3 # Copyright (C) 2017-present eyeo GmbH | |
| 4 # | |
| 5 # Adblock Plus is free software: you can redistribute it and/or modify | |
| 6 # it under the terms of the GNU General Public License version 3 as | |
| 7 # published by the Free Software Foundation. | |
| 8 # | |
| 9 # Adblock Plus is distributed in the hope that it will be useful, | |
| 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of | |
| 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
| 12 # GNU General Public License for more details. | |
| 13 # | |
| 14 # You should have received a copy of the GNU General Public License | |
| 15 # along with Adblock Plus. If not, see <http://www.gnu.org/licenses/>. | |
| 16 | |
| 17 """Test CMS by comparing the output of its different revisions.""" | |
| 18 | |
| 19 from __future__ import print_function, unicode_literals | |
| 20 | |
| 21 import argparse | |
| 22 import difflib | |
| 23 import filecmp | |
| 24 import io | |
| 25 import os | |
| 26 import shutil | |
| 27 import subprocess | |
| 28 import sys | |
| 29 | |
| 30 # Path to static generation script. | |
| 31 GENERATE_PATH = 'cms/bin/generate_static_pages.py' | |
| 32 | |
| 33 # Fake revision that indicates "use current working copy". | |
| 34 WORKING_COPY = '[working-copy]' | |
| 35 | |
| 36 | |
| 37 def run_cmd(*cmd, **kw): | |
| 38 """Run a command, print and return its output.""" | |
| 39 silent = kw.get('silent', False) | |
| 40 if 'silent' in kw: | |
| 41 del kw['silent'] | |
| 42 if not silent: | |
| 43 print('$', *cmd) | |
| 44 try: | |
| 45 output = subprocess.check_output(cmd, **kw).decode('utf-8') | |
| 46 except subprocess.CalledProcessError: | |
| 47 sys.exit('Command invocation failed: {}'.format(' '.join(cmd))) | |
| 48 if not silent: | |
| 49 for line in output.splitlines(): | |
| 50 print('>', line) | |
| 51 return output | |
| 52 | |
| 53 | |
| 54 def hg(*args, **kw): | |
| 55 """Run Mercurial and return its output.""" | |
| 56 cmd = ['hg'] | |
| 57 if 'repo' in kw: | |
| 58 cmd += ['-R', kw['repo']] | |
| 59 del kw['repo'] | |
| 60 # Disable default options from local user config. | |
| 61 cmd += ['--config', 'defaults.{}='.format(args[0])] | |
| 62 return run_cmd(*(cmd + list(args)), **kw) | |
| 63 | |
| 64 | |
| 65 def get_current_rev(repo): | |
| 66 """Get the revision ids of the working copy in repository.""" | |
| 67 return hg('id', repo=repo, silent=True).split() | |
| 68 | |
| 69 | |
| 70 def read_file(path): | |
| 71 """Read file, return list of strings.""" | |
| 72 with io.open(path, encoding='utf-8') as f: | |
| 73 return f.read().splitlines() | |
| 74 | |
| 75 | |
| 76 def print_diff(one, two): | |
| 77 """Print unified diff between two files.""" | |
| 78 for line in difflib.unified_diff(read_file(one), read_file(two), | |
| 79 fromfile=one, tofile=two): | |
| 80 print(line) | |
| 81 | |
| 82 | |
| 83 def compare_dirs(one, two, ignore=[]): | |
| 84 """Compare two directories, return True if same, False if not.""" | |
| 85 | |
| 86 def recursive_compare(c): | |
| 87 if c.left_only: | |
| 88 print('The following file(s)/dir(s) are only in base', c.left) | |
| 89 for f in c.left_only: | |
| 90 print('-', f) | |
| 91 return False | |
| 92 if c.right_only: | |
| 93 print('The following file(s)/dir(s) are only in test', c.right) | |
| 94 for f in c.right_only: | |
| 95 print('-', f) | |
| 96 return False | |
| 97 if c.diff_files: | |
| 98 print('The following file(s) are different between', c.left, | |
| 99 'and', c.right) | |
| 100 for f in c.diff_files: | |
| 101 print('-', f) | |
| 102 base = os.path.join(c.left, f) | |
| 103 test = os.path.join(c.right, f) | |
| 104 print_diff(base, test) | |
| 105 return False | |
| 106 return all(recursive_compare(sub) for sub in c.subdirs.values()) | |
| 107 | |
| 108 print('Comparing', one, 'and', two) | |
| 109 comparator = filecmp.dircmp(one, two, ignore=ignore) | |
| 110 return recursive_compare(comparator) | |
| 111 | |
| 112 | |
| 113 class Tester(object): | |
| 114 """Test runner.""" | |
| 115 | |
| 116 def __init__(self, config): | |
| 117 self.__dict__.update(config.__dict__) | |
| 118 | |
| 119 def clone_cms(self): | |
| 120 """Clone CMS repository for to use for tests.""" | |
| 121 self.cms_clone = os.path.join(self.dest, 'cms-cmp.cms-clone') | |
| 122 print('Cloning CMS to', self.cms_clone) | |
| 123 if os.path.exists(self.cms_clone): | |
| 124 shutil.rmtree(self.cms_clone) | |
| 125 hg('clone', self.cms_repo, self.cms_clone) | |
| 126 | |
| 127 def cms_checkout(self, rev): | |
| 128 """Checkout specified revision of CMS. | |
| 129 | |
| 130 Returns revision hash (or working copy marker) and path to where it is. | |
| 131 """ | |
| 132 if rev is WORKING_COPY: | |
| 133 print('Using CMS working copy') | |
| 134 return WORKING_COPY, self.cms_repo | |
| 135 print('Switching CMS to revision:', rev) | |
| 136 hg('co', rev, repo=self.cms_clone) | |
| 137 return get_current_rev(self.cms_clone)[0], self.cms_clone | |
| 138 | |
| 139 def generate(self, cms_rev, website_path): | |
| 140 """Generate the website using specified revision of CMS.""" | |
| 141 name = os.path.basename(website_path) | |
| 142 website_rev = get_current_rev(website_path)[0] | |
| 143 cms_rev, cms_path = self.cms_checkout(cms_rev) | |
| 144 print('Generating', website_path, 'with CMS revision:', cms_rev) | |
| 145 unique_id = '{}-rev-{}-cms-{}'.format(name, website_rev, cms_rev) | |
| 146 dst = os.path.join(self.dest, unique_id) | |
| 147 if os.path.exists(dst): | |
| 148 if self.remove_old or cms_rev == WORKING_COPY: | |
| 149 shutil.rmtree(dst) | |
| 150 else: | |
| 151 print(dst, 'exists, assuming it was generated earlier') | |
| 152 return dst | |
| 153 env = dict(os.environ) | |
| 154 env['PYTHONPATH'] = cms_path | |
| 155 generate = os.path.join(cms_path, GENERATE_PATH) | |
| 156 run_cmd(self.python, generate, website_path, dst, env=env) | |
| 157 return dst | |
| 158 | |
| 159 def run(self): | |
| 160 if not os.path.exists(os.path.join(self.cms_repo, GENERATE_PATH)): | |
| 161 sys.exit('No cms source found in ' + self.cms_repo) | |
| 162 print('Using CMS repository at', self.cms_repo) | |
| 163 self.clone_cms() | |
| 164 for website_path in self.websites: | |
| 165 base = self.generate(self.base_rev, website_path) | |
| 166 test = self.generate(self.test_rev, website_path) | |
| 167 if not compare_dirs(base, test, ignore=self.ignore): | |
| 168 print('Differences found for', website_path) | |
| 169 sys.exit(1) | |
| 170 | |
| 171 | |
| 172 def configure(): | |
|
tlucas
2017/11/24 10:57:46
Since configure()'s only purpose is to specify par
Vasily Kuznetsov
2017/11/28 16:53:37
I would like to keep `Tester` independent from par
tlucas
2017/12/14 10:30:42
ACK, your argumentation seems valid :)
| |
| 173 """Configure the script from arguments.""" | |
| 174 parser = argparse.ArgumentParser(description=__doc__) | |
| 175 parser.add_argument('websites', metavar='WEBSITE', nargs='+', | |
| 176 help='path of website source to use for comparison') | |
| 177 parser.add_argument('-b', '--base-rev', metavar='REVISION', | |
| 178 default='master', | |
| 179 help='base revision of CMS to use as baseline') | |
| 180 parser.add_argument('-c', '--cms-repo', metavar='CMS_REPO', | |
| 181 default=os.getcwd(), | |
| 182 help='Location of CMS repository') | |
| 183 parser.add_argument('-d', '--dest', metavar='OUT_DIR', | |
| 184 default=os.environ.get('TMPDIR', '/tmp'), | |
| 185 help='directory for storing CMS output') | |
| 186 parser.add_argument('-i', '--ignore', metavar='FILENAME', action='append', | |
| 187 default=[], | |
| 188 help='file names to ignore in output comparison') | |
| 189 parser.add_argument('-p', '--python', metavar='PYTHON_EXE', | |
| 190 default=sys.executable, | |
| 191 help='python interpreter to run CMS') | |
| 192 parser.add_argument('-r', '--remove-old', action='store_true', | |
| 193 help='remove previously generated CMS outputs instead' | |
| 194 'of reusing them') | |
| 195 parser.add_argument('-t', '--test-rev', metavar='REVISION', | |
| 196 default=WORKING_COPY, | |
| 197 help='revision of CMS to use for testing (by default ' | |
| 198 'currently checked out working copy including ' | |
| 199 'uncommited changes will be used)') | |
| 200 return parser.parse_args() | |
| 201 | |
| 202 | |
| 203 def main(): | |
| 204 """Main entry point for the script.""" | |
| 205 config = configure() | |
| 206 tester = Tester(config) | |
| 207 tester.run() | |
| 208 | |
| 209 | |
| 210 if __name__ == '__main__': | |
| 211 main() | |
| OLD | NEW |