# perforce_sample_data.py -- generate random activity in Perforce.
# Gareth Rees, Ravenbrook Limited, 2000-09-25
# $Id: //info.ravenbrook.com/project/p4dti/version/1.1/test/perforce_sample_data.py#1 $
#
# This file is copyright (c) 2001 Perforce Software, Inc.  All rights
# reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
# met:
#
# 1.  Redistributions of source code must retain the above copyright
#     notice, this list of conditions and the following disclaimer.
#
# 2.  Redistributions in binary form must reproduce the above copyright
#     notice, this list of conditions and the following disclaimer in
#     the documentation and/or other materials provided with the
#     distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
# HOLDERS AND CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
# OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
# TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
# USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
# DAMAGE.

import sys
sys.path.append('../code/replicator')
import logger
import os
import p4
import random
import re
import socket
import string
import time

# Configuration.  tester is the person testing the software and domain is their
# e-mail domain.  rid is the replicator identifier for the replicator that's
# going to replicate the sample repository.  p4_user is the userid for the
# replicator.  p4_client is the client that the replicator uses.
# p4_client_root is the root directory of the sample repository.

tester = 'gdr'
domain = 'ravenbrook.com'
rid = 'replicator0'
p4_user = 'P4DTI-%s' % rid
p4_client = 'p4dti-%s' % socket.gethostname()
p4_client_root = 'd:\\p4dti-test'
p4_server = 'sandpiper.ravenbrook.com:1666'
logger = logger.file_logger()

# p4_run(...).  This is just a wrapper for p4.run that provides defaults for
# the input, user and client arguments.  See the p4 module for details.

def p4_run(arguments, input=None, user=p4_user, client=p4_client):
    return p4.p4( user = user,
                  client = client,
                  port = p4_server,
                  logger = logger).run(arguments, input=input)

# The structure of the sample repository, represented as a list whose
# first element is a directory name, and the remaining elements are the
# entries in that directory (which may be other directories).

repository = [ '//depot/',
               [ 'project/',
                 [ 'compiler/',
                   [ 'doc/', 'goals', 'requirements', 'plan', 'design' ],
                   [ 'src/',
                     'assembler.c', 'assembler.h', 'compiler.h', 'lexer.c',
                     'lexer.h', 'main.c', 'parser.c', 'parser.h', 'register.c',
                     'register.h' ],
                   [ 'build/', 'cc' ] ],
                 [ 'editor/',
                   [ 'doc/', 'goals', 'requirements', 'plan', 'design' ],
                   [ 'src/', 'input.c', 'input.h', 'output.c', 'output.h',
                     'buffer.c', 'buffer.h', 'commands.c', 'commands.h',
                     'main.c' ],
                   [ 'build/', 'vi' ] ] ] ]

# Transform the repository structure so that all filespecs include the full
# path.

def expand_filespecs(parent, self):
    self[0] = parent + self[0]
    for i in range(len(self) - 1):
        if (type(self[i+1]) == type([])):
            expand_filespecs(self[0], self[i+1])
        else:
            self[i+1] = self[0] + self[i+1]

expand_filespecs('', repository)

# choose_k(choices, k).  Choose k of the choices randomly and return them as a
# list.

def choose_k(choices, k):
    n = len(choices)
    i = 0
    results = []
    while i < n and k > 0:
        if (random.uniform(0,1) < float(k)/float(n-i)):
            results.append(choices[i])
            k = k - 1
        i = i + 1
    return results

# random_filespecs(min_level).  Returns a random list of filespecs from the
# repository.  The distribution is skewed so that generally sensible results
# are returned.

def random_filespecs(min_level = 0):
    # The deepest directory in the repository (0 is the depot).
    max_level = 3
    # Distribution of lengths of paths in returned filespecs (less 1).
    levels = [0,1,2,2,2,3,3,3,3,3,3,3,3,3,3,3,3,3]
    # Distribution of number of filespecs to return.
    n_filespecs = [0,0,0,0,1,1,1,1,2,2,3,4,5]
    # How many filespecs to return?
    k = random.choice(n_filespecs)
    # Pick a level at which to choose a directory.
    if (k > 2):
        level = max_level
    else:
        level = random.choice(levels)
        if level < min_level:
            level = min_level
    # Pick a directory from which to choose the filespecs.
    dir = repository
    for i in range(level):
        dir = random.choice(dir[1:])
    # Now choose k out of n filespecs from the selected directory.
    filespecs = choose_k(dir[1:], k)
    if level < max_level:
        filespecs = map(lambda (f): f[0] + '...', filespecs)
    return filespecs

# add_file(filespec).  If the file doesn't exist, create it and add it to the
# Perforce repository (but don't submit the addition).  Return the number of
# files added to Perforce.

def add_file(filespec):
    # Make the file exist in the client.
    name = string.split(p4_run('where ' + filespec)[0]['data'])[2]
    if not os.access(name, 0555):
        stream = open(name, 'w')
        stream.write('Created.\n')
        stream.close()
    # Now make the file exist on the server.
    try:
        p4_run('files ' + filespec)
    except:
        p4_run('add ' + name)
        print 'Added', name
        return 1
    return 0

# add_directory(dir).  Add files and directories to Perforce.  The dir argument
# should be a repository structure (see above).  If the directory specified by
# the first argument doesn't exist, create it.  Then recurse to add the
# subdirectories or files.  Don't submit the additions.  Return the number of
# files added to Perforce.

def add_directory(dir):
    if dir[0] == '//depot/':
        name = p4_client_root
    else:
        # The [:-1] is necessary because p4 where doesn't understand directory
        # names ending in slashes.
        name = string.split(p4_run('where ' + dir[0][:-1])[0]['data'])[2]
    if not os.access(name, 0777):
        os.mkdir(name)
        print 'Created directory', name
    n_added = 0
    for i in dir[1:]:
        if type(i) == type(''):
            n_added = n_added + add_file(i)
        else:
            n_added = n_added + add_directory(i)
    return n_added

# init_repository().  Adds all the files in the 'repository' structure to the
# sample Perforce repository.  Submit the additions.

def init_repository():
    if add_directory(repository):
        change = p4_run('change -o')[0]
        change['Jobs'] = ''
        change['Description'] = 'Files automatically added to sample repository'
        p4_run('submit -i', change)

# users.  A list of structures describing the sample TeamTrack users (from the
# TeamShare demo).  The 'name' key is their user name in both TeamTrack and
# Perforce.  The 'id' key is their userid in TeamTrack.  The 'client' key is
# the Perforce client they use to edit files in the sample repository.

users = [
    { 'name': 'gdr', 'client': 'gdr-grouse' },
    { 'name': 'rb',  'client': 'rb-skylark' },
    { 'name': 'nb',  'client': 'nb-thrush' },
    { 'name': 'lmb', 'client': 'lmb-blackbird' },
    ]

# make_user(user).  Make a Perforce user and set their e-mail address (unless
# it already exists).

def make_user(user):
    u = p4_run('user -o', user=user)[0]
    email = '%s+%s@%s' % (tester, user, domain)
    if u['Email'] != email:
        u['Email'] = email
        p4_run('user -i', u, user=user)
        print 'Added user', user

# make_client(user, clientname).  Make a Perforce client (unless it already
# exists) belonging to the given user and with the given name.  Its root
# directory is given by p4_client_root, plus the user name.

def make_client(user, clientname):
    # Get the list of clients.
    clients = p4_run('clients')
    exists = 0
    for c in clients:
        if c['client'] == clientname:
            exists = 1
    client = p4_run('client -o', user=user, client=clientname)[0]
    if not exists:
        client['Root'] = '%s\\%s' % (p4_client_root, user)
        client['View0'] = '//depot/... //%s/...' % clientname
        p4_run('client -i', client, user=user, client=clientname)
        print 'Added client', clientname, 'for user', user
    if not os.access(client['Root'], 0777):
        os.mkdir(client['Root'])
        print 'Created directory', client['Root']

# init_clients().  Create Perforce clients for the replicator (p4_user,
# p4_client) and the sample users specified in the users data structure (if the
# clients don't already exist).

def init_clients():
    make_user(p4_user)
    make_client(p4_user, p4_client)
    for u in users:
        make_user(u['name'])
        make_client(u['name'], u['client'])

# Status values for Perforce jobs in the sample repository.

statuses = [ '_new', 'assigned', 'closed', 'verified' ]

# make_change(jobs).  Make a change in the sample repository by picking some
# files, editing them, submitting the edits, and making fix records associating
# the change with some jobs.  The jobs argument should be a list of jobs for
# which fix records can be added.  The Status field in the fixed jobs is
# changed to reflect the new status (but the other fields are not changed and
# may be out of date).

def make_change(all_jobs):
    # User who will make the change.
    user = random.choice(users)
    # Sync their workspace.  Note that sync returns an error if everything is
    # up to date.
    try:
        p4_run('sync', user=user['name'], client=user['client'])
    except p4.error:
        pass
    # Files affected by the change.
    filespecs = []
    while not filespecs:
        filespecs = random_filespecs(min_level = 3)
    # Jobs fixed by the change.
    k = random.choice([0,1,1,1,1,1,1,2,2,3])
    jobs = choose_k(all_jobs, k)
    # Edit the files.
    for f in filespecs:
        p4_run('edit ' + f, user=user['name'], client=user['client'])
        where = p4_run('where ' + f, user=user['name'], client=user['client'])
        filename = string.split(where[0]['data'])[2]
        assert(os.access(filename, 0777))
        stream = open(filename, 'a')
        now = time.asctime(time.localtime(time.time()))
        stream.write('Edited at %s by %s\n' % (now, user['name']))
        stream.close()
        print filename, 'edited at', now, 'by', user['name']
    # Submit the change (don't do the fixes at this stage since the Perforce
    # job interface doesn't allow you to specify the status).
    change = p4_run('change -o', user=user['name'], client=user['client'])[0]
    change['Jobs'] = ''
    change['Description'] = ('Automatically generated change comment.\n'
                             'Edited files: ' + str(filespecs))
    results = p4_run('submit -i', change, user=user['name'],
                     client=user['client'])
    # Work out the change number from the message returned by Perforce, which
    # should be the last element in the results array.
    match = re.compile('Change ([0-9]+) submitted\\.$').match(results[-1]['data'])
    assert(match)
    change_number = int(match.group(1))
    # Now make the fix records.
    for job in jobs:
        # What's the current status of the job?
        status = statuses.index(job['State'])
        # Change the status keyword by moving it forward in the statuses array
        # with probability 90%.
        if (status == 0 or (status < len(statuses) - 1
                            and random.uniform(0,1) < 0.9)):
            status = status + 1
        else:
            status = status - 1
        # Fix the job.
        p4_run('fix -s %s -c %d %s'
               % (statuses[status], change_number, job['Job']),
               user=user['name'], client=user['client'])
        # Record the changed status so that we don't have to query Perforce
        # again.
        job['State'] = statuses[status]

def run(n_changes = 10):
    init_clients()
    init_repository()
    # Get the set of jobs.
    jobs = p4_run('jobs')
    # Make some changes that fix those jobs.
    for _ in range(n_changes):
        make_change(jobs)

if __name__ == '__main__':
    run()

# B. Document History
#
# 2000-09-25 GDR Created.
#
# 2000-09-26 GDR Creates clients and workspaces for the sample users, edits
# files, and fixes jobs.
#
# 2000-10-03 GDR Only report "Adding client" when actually doing so.
#
# 2000-10-10 GDR Changed Perforce job status fields so that they correspond
# with the issue states in the TeamTrack sample database.
#
# 2001-02-26 GDR Make users with appropriate e-mail addresses.
#
# 2001-02-27 GDR Don't use os.access() to test if a file exists on the server;
# use 'p4 files' instead.
#
# 2001-03-02 RB Transferred copyright to Perforce under their license.
#
# 2001-03-15 GDR Use revised p4 interface.
