Rietveld Code Review Tool
Help | Bug tracker | Discussion group | Source code

Side by Side Diff: cms-dev/cms_cmp.py

Issue 29588962: Issue 5934 - CMS testing automation (Closed)
Patch Set: Change the usage a bit, add some useful options, add a couple of tests Created Oct. 27, 2017, 5:38 p.m.
Left:
Right:
Use n/p to move between diff chunks; N/P to move between comments.
Jump to:
View unified diff | Download patch
« no previous file with comments | « no previous file | cms-dev/tests/test_cms_cmp.py » ('j') | cms-dev/tests/test_cms_cmp.py » ('J')
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(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()
OLDNEW
« no previous file with comments | « no previous file | cms-dev/tests/test_cms_cmp.py » ('j') | cms-dev/tests/test_cms_cmp.py » ('J')

Powered by Google App Engine
This is Rietveld