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 |