| #!/usr/bin/env python |
| # |
| # |
| # Licensed to the Apache Software Foundation (ASF) under one |
| # or more contributor license agreements. See the NOTICE file |
| # distributed with this work for additional information |
| # regarding copyright ownership. The ASF licenses this file |
| # to you under the Apache License, Version 2.0 (the |
| # "License"); you may not use this file except in compliance |
| # with the License. You may obtain a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, |
| # software distributed under the License is distributed on an |
| # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| # KIND, either express or implied. See the License for the |
| # specific language governing permissions and limitations |
| # under the License. |
| # |
| # |
| # |
| # scramble-tree.py: (See scramble-tree.py --help.) |
| # |
| # Makes multiple random file changes to a directory tree, for testing. |
| # |
| # This script will add some new files, remove some existing files, add |
| # text to some existing files, and delete text from some existing |
| # files. It will also leave some files completely untouched. |
| # |
| # The exact set of changes made is always the same for identical trees, |
| # where "identical" means the names of files and directories are the |
| # same, and they are arranged in the same tree structure (the actual |
| # contents of files may differ). If two are not identical, the sets of |
| # changes scramble-tree.py will make may differ arbitrarily. |
| # |
| # Directories named .svn/ and CVS/ are ignored. |
| # |
| # Example scenario, starting with a pristine Subversion working copy: |
| # |
| # $ ls |
| # foo/ |
| # $ svn st foo |
| # $ cp -r foo bar |
| # $ svn st bar |
| # $ scramble-tree.py foo |
| # $ svn st foo |
| # [... see lots of scary status output ...] |
| # $ scramble-tree.py bar |
| # [... see the exact same scary status output ...] |
| # $ scramble-tree.py foo |
| # [... see a new bunch of scary status output ...] |
| # $ |
| |
| import os |
| import sys |
| import getopt |
| try: |
| my_getopt = getopt.gnu_getopt |
| except AttributeError: |
| my_getopt = getopt.getopt |
| import random |
| from hashlib import md5 as hashlib_md5 |
| import base64 |
| |
| |
| class VCActions: |
| def __init__(self): |
| pass |
| def add_file(self, path): |
| """Add an existing file to version control.""" |
| pass |
| def remove_file(self, path): |
| """Remove an existing file from version control, and delete it.""" |
| pass |
| |
| |
| class NoVCActions(VCActions): |
| def remove_file(self, path): |
| os.unlink(path) |
| |
| |
| class CVSActions(VCActions): |
| def add_file(self, path): |
| cwd = os.getcwd() |
| try: |
| dirname, basename = os.path.split(path) |
| os.chdir(os.path.join(cwd, dirname)) |
| os.system('cvs -Q add -m "Adding file to repository" "%s"' % (basename)) |
| finally: |
| os.chdir(cwd) |
| def remove_file(self, path): |
| cwd = os.getcwd() |
| try: |
| dirname, basename = os.path.split(path) |
| os.chdir(os.path.join(cwd, dirname)) |
| os.system('cvs -Q rm -f "%s"' % (basename)) |
| finally: |
| os.chdir(cwd) |
| |
| |
| class SVNActions(VCActions): |
| def add_file(self, path): |
| os.system('svn add --quiet "%s"' % (path)) |
| def remove_file(self, path): |
| os.remove(path) |
| os.system('svn rm --quiet --force "%s"' % (path)) |
| |
| |
| class hashDir: |
| """Given a directory, creates a string containing all directories |
| and files under that directory (sorted alphanumerically) and makes a |
| base64-encoded md5 hash of the resulting string. Call |
| hashDir.gen_seed() to generate a seed value for this tree.""" |
| |
| def __init__(self, rootdir): |
| self.allfiles = [] |
| for dirpath, dirs, files in os.walk(rootdir): |
| self.walker_callback(len(rootdir), dirpath, dirs + files) |
| |
| def gen_seed(self): |
| # Return a base64-encoded (kinda ... strip the '==\n' from the |
| # end) MD5 hash of sorted tree listing. |
| self.allfiles.sort() |
| return base64.encodestring(hashlib_md5(''.join(self.allfiles)).digest())[:-3] |
| |
| def walker_callback(self, baselen, dirname, fnames): |
| if ((dirname == '.svn') or (dirname == 'CVS')): |
| return |
| self.allfiles.append(dirname[baselen:]) |
| for filename in fnames: |
| path = os.path.join(dirname, filename) |
| if not os.path.isdir(path): |
| self.allfiles.append(path[baselen:]) |
| |
| |
| class Scrambler: |
| def __init__(self, seed, vc_actions, dry_run, quiet): |
| if not quiet: |
| print('SEED: ' + seed) |
| |
| self.rand = random.Random(seed) |
| self.vc_actions = vc_actions |
| self.dry_run = dry_run |
| self.quiet = quiet |
| self.ops = [] ### ["add" | "munge", path] |
| self.greeking = """ |
| ====================================================================== |
| This is some text that was inserted into this file by the lovely and |
| talented scramble-tree.py script. |
| ====================================================================== |
| """ |
| |
| ### Helpers |
| def shrink_list(self, list, remove_count): |
| if len(list) <= remove_count: |
| return [] |
| for i in range(remove_count): |
| j = self.rand.randrange(len(list) - 1) |
| del list[j] |
| return list |
| |
| def _make_new_file(self, dir): |
| i = 0 |
| path = None |
| for i in range(99999): |
| path = os.path.join(dir, "newfile.%05d.txt" % i) |
| if not os.path.exists(path): |
| open(path, 'w').write(self.greeking) |
| return path |
| raise Exception("Ran out of unique new filenames in directory '%s'" % dir) |
| |
| ### File Mungers |
| def _mod_append_to_file(self, path): |
| if not self.quiet: |
| print('append_to_file: %s' % path) |
| if self.dry_run: |
| return |
| fh = open(path, "a") |
| fh.write(self.greeking) |
| fh.close() |
| |
| def _mod_remove_from_file(self, path): |
| if not self.quiet: |
| print('remove_from_file: %s' % path) |
| if self.dry_run: |
| return |
| lines = self.shrink_list(open(path, "r").readlines(), 5) |
| open(path, "w").writelines(lines) |
| |
| def _mod_delete_file(self, path): |
| if not self.quiet: |
| print('delete_file: %s' % path) |
| if self.dry_run: |
| return |
| self.vc_actions.remove_file(path) |
| |
| ### Public Interfaces |
| def get_randomizer(self): |
| return self.rand |
| |
| def schedule_munge(self, path): |
| self.ops.append(tuple(["munge", path])) |
| |
| def schedule_addition(self, dir): |
| self.ops.append(tuple(["add", dir])) |
| |
| def enact(self, limit): |
| num_ops = len(self.ops) |
| if limit == 0: |
| return |
| elif limit > 0 and limit <= num_ops: |
| self.ops = self.shrink_list(self.ops, num_ops - limit) |
| for op, path in self.ops: |
| if op == "add": |
| path = self._make_new_file(path) |
| if not self.quiet: |
| print("add_file: %s" % path) |
| if self.dry_run: |
| return |
| self.vc_actions.add_file(path) |
| elif op == "munge": |
| file_mungers = [self._mod_append_to_file, |
| self._mod_append_to_file, |
| self._mod_append_to_file, |
| self._mod_remove_from_file, |
| self._mod_remove_from_file, |
| self._mod_remove_from_file, |
| self._mod_delete_file, |
| ] |
| self.rand.choice(file_mungers)(path) |
| |
| |
| def usage(retcode=255): |
| print('Usage: %s [OPTIONS] DIRECTORY' % (sys.argv[0])) |
| print('') |
| print('Options:') |
| print(' --help, -h : Show this usage message.') |
| print(' --seed ARG : Use seed ARG to scramble the tree.') |
| print(' --use-svn : Use Subversion (as "svn") to perform file additions') |
| print(' and removals.') |
| print(' --use-cvs : Use CVS (as "cvs") to perform file additions') |
| print(' and removals.') |
| print(' --dry-run : Don\'t actually change the disk.') |
| print(' --limit N : Limit the scrambling to a maximum of N operations.') |
| print(' --quiet, -q : Run in stealth mode!') |
| sys.exit(retcode) |
| |
| |
| def walker_callback(scrambler, dirname, fnames): |
| if ((dirname.find('.svn') != -1) or dirname.find('CVS') != -1): |
| return |
| rand = scrambler.get_randomizer() |
| if rand.randrange(5) == 1: |
| scrambler.schedule_addition(dirname) |
| for filename in fnames: |
| path = os.path.join(dirname, filename) |
| if not os.path.isdir(path) and rand.randrange(3) == 1: |
| scrambler.schedule_munge(path) |
| |
| |
| def main(): |
| seed = None |
| vc_actions = NoVCActions() |
| dry_run = 0 |
| quiet = 0 |
| limit = None |
| |
| # Mm... option parsing. |
| optlist, args = my_getopt(sys.argv[1:], "hq", |
| ['seed=', 'use-svn', 'use-cvs', |
| 'help', 'quiet', 'dry-run', 'limit=']) |
| for opt, arg in optlist: |
| if opt == '--help' or opt == '-h': |
| usage(0) |
| if opt == '--seed': |
| seed = arg |
| if opt == '--use-svn': |
| vc_actions = SVNActions() |
| if opt == '--use-cvs': |
| vc_actions = CVSActions() |
| if opt == '--dry-run': |
| dry_run = 1 |
| if opt == '--limit': |
| limit = int(arg) |
| if opt == '--quiet' or opt == '-q': |
| quiet = 1 |
| |
| # We need at least a path to work with, here. |
| argc = len(args) |
| if argc < 1 or argc > 1: |
| usage() |
| rootdir = args[0] |
| |
| # If a seed wasn't provide, calculate one. |
| if seed is None: |
| seed = hashDir(rootdir).gen_seed() |
| scrambler = Scrambler(seed, vc_actions, dry_run, quiet) |
| for dirpath, dirs, files in os.walk(rootdir): |
| walker_callback(scrambler, dirpath, dirs + files) |
| scrambler.enact(limit) |
| |
| if __name__ == '__main__': |
| main() |