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

Unified 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.
Use n/p to move between diff chunks; N/P to move between comments.
Jump to:
View side-by-side diff with in-line comments
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')
Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
Index: cms-dev/cms_cmp.py
===================================================================
new file mode 100644
--- /dev/null
+++ b/cms-dev/cms_cmp.py
@@ -0,0 +1,211 @@
+#!/bin/env python
+# This file is part of Adblock Plus <https://adblockplus.org/>,
+# Copyright (C) 2017-present eyeo GmbH
+#
+# Adblock Plus is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 3 as
+# published by the Free Software Foundation.
+#
+# Adblock Plus is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Adblock Plus. If not, see <http://www.gnu.org/licenses/>.
+
+"""Test CMS by comparing the output of its different revisions."""
+
+from __future__ import print_function, unicode_literals
+
+import argparse
+import difflib
+import filecmp
+import io
+import os
+import shutil
+import subprocess
+import sys
+
+# Path to static generation script.
+GENERATE_PATH = 'cms/bin/generate_static_pages.py'
+
+# Fake revision that indicates "use current working copy".
+WORKING_COPY = '[working-copy]'
+
+
+def run_cmd(*cmd, **kw):
+ """Run a command, print and return its output."""
+ silent = kw.get('silent', False)
+ if 'silent' in kw:
+ del kw['silent']
+ if not silent:
+ print('$', *cmd)
+ try:
+ output = subprocess.check_output(cmd, **kw).decode('utf-8')
+ except subprocess.CalledProcessError:
+ sys.exit('Command invocation failed: {}'.format(' '.join(cmd)))
+ if not silent:
+ for line in output.splitlines():
+ print('>', line)
+ return output
+
+
+def hg(*args, **kw):
+ """Run Mercurial and return its output."""
+ cmd = ['hg']
+ if 'repo' in kw:
+ cmd += ['-R', kw['repo']]
+ del kw['repo']
+ # Disable default options from local user config.
+ cmd += ['--config', 'defaults.{}='.format(args[0])]
+ return run_cmd(*(cmd + list(args)), **kw)
+
+
+def get_current_rev(repo):
+ """Get the revision ids of the working copy in repository."""
+ return hg('id', repo=repo, silent=True).split()
+
+
+def read_file(path):
+ """Read file, return list of strings."""
+ with io.open(path, encoding='utf-8') as f:
+ return f.read().splitlines()
+
+
+def print_diff(one, two):
+ """Print unified diff between two files."""
+ for line in difflib.unified_diff(read_file(one), read_file(two),
+ fromfile=one, tofile=two):
+ print(line)
+
+
+def compare_dirs(one, two, ignore=[]):
+ """Compare two directories, return True if same, False if not."""
+
+ def recursive_compare(c):
+ if c.left_only:
+ print('The following file(s)/dir(s) are only in base', c.left)
+ for f in c.left_only:
+ print('-', f)
+ return False
+ if c.right_only:
+ print('The following file(s)/dir(s) are only in test', c.right)
+ for f in c.right_only:
+ print('-', f)
+ return False
+ if c.diff_files:
+ print('The following file(s) are different between', c.left,
+ 'and', c.right)
+ for f in c.diff_files:
+ print('-', f)
+ base = os.path.join(c.left, f)
+ test = os.path.join(c.right, f)
+ print_diff(base, test)
+ return False
+ return all(recursive_compare(sub) for sub in c.subdirs.values())
+
+ print('Comparing', one, 'and', two)
+ comparator = filecmp.dircmp(one, two, ignore=ignore)
+ return recursive_compare(comparator)
+
+
+class Tester(object):
+ """Test runner."""
+
+ def __init__(self, config):
+ self.__dict__.update(config.__dict__)
+
+ def clone_cms(self):
+ """Clone CMS repository for to use for tests."""
+ self.cms_clone = os.path.join(self.dest, 'cms-cmp.cms-clone')
+ print('Cloning CMS to', self.cms_clone)
+ if os.path.exists(self.cms_clone):
+ shutil.rmtree(self.cms_clone)
+ hg('clone', self.cms_repo, self.cms_clone)
+
+ def cms_checkout(self, rev):
+ """Checkout specified revision of CMS.
+
+ Returns revision hash (or working copy marker) and path to where it is.
+ """
+ if rev is WORKING_COPY:
+ print('Using CMS working copy')
+ return WORKING_COPY, self.cms_repo
+ print('Switching CMS to revision:', rev)
+ hg('co', rev, repo=self.cms_clone)
+ return get_current_rev(self.cms_clone)[0], self.cms_clone
+
+ def generate(self, cms_rev, website_path):
+ """Generate the website using specified revision of CMS."""
+ name = os.path.basename(website_path)
+ website_rev = get_current_rev(website_path)[0]
+ cms_rev, cms_path = self.cms_checkout(cms_rev)
+ print('Generating', website_path, 'with CMS revision:', cms_rev)
+ unique_id = '{}-rev-{}-cms-{}'.format(name, website_rev, cms_rev)
+ dst = os.path.join(self.dest, unique_id)
+ if os.path.exists(dst):
+ if self.remove_old or cms_rev == WORKING_COPY:
+ shutil.rmtree(dst)
+ else:
+ print(dst, 'exists, assuming it was generated earlier')
+ return dst
+ env = dict(os.environ)
+ env['PYTHONPATH'] = cms_path
+ generate = os.path.join(cms_path, GENERATE_PATH)
+ run_cmd(self.python, generate, website_path, dst, env=env)
+ return dst
+
+ def run(self):
+ if not os.path.exists(os.path.join(self.cms_repo, GENERATE_PATH)):
+ sys.exit('No cms source found in ' + self.cms_repo)
+ print('Using CMS repository at', self.cms_repo)
+ self.clone_cms()
+ for website_path in self.websites:
+ base = self.generate(self.base_rev, website_path)
+ test = self.generate(self.test_rev, website_path)
+ if not compare_dirs(base, test, ignore=self.ignore):
+ print('Differences found for', website_path)
+ sys.exit(1)
+
+
+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 :)
+ """Configure the script from arguments."""
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument('websites', metavar='WEBSITE', nargs='+',
+ help='path of website source to use for comparison')
+ parser.add_argument('-b', '--base-rev', metavar='REVISION',
+ default='master',
+ help='base revision of CMS to use as baseline')
+ parser.add_argument('-c', '--cms-repo', metavar='CMS_REPO',
+ default=os.getcwd(),
+ help='Location of CMS repository')
+ parser.add_argument('-d', '--dest', metavar='OUT_DIR',
+ default=os.environ.get('TMPDIR', '/tmp'),
+ help='directory for storing CMS output')
+ parser.add_argument('-i', '--ignore', metavar='FILENAME', action='append',
+ default=[],
+ help='file names to ignore in output comparison')
+ parser.add_argument('-p', '--python', metavar='PYTHON_EXE',
+ default=sys.executable,
+ help='python interpreter to run CMS')
+ parser.add_argument('-r', '--remove-old', action='store_true',
+ help='remove previously generated CMS outputs instead'
+ 'of reusing them')
+ parser.add_argument('-t', '--test-rev', metavar='REVISION',
+ default=WORKING_COPY,
+ help='revision of CMS to use for testing (by default '
+ 'currently checked out working copy including '
+ 'uncommited changes will be used)')
+ return parser.parse_args()
+
+
+def main():
+ """Main entry point for the script."""
+ config = configure()
+ tester = Tester(config)
+ tester.run()
+
+
+if __name__ == '__main__':
+ main()
« 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