# Perforce Defect Tracking Integration Project
#
#
# CONFIGURE_TRACKER.PY -- BUILD P4DTI CONFIGURATION FOR TRACKER
#
# Robert Cowham, Vaccaperna Systems Limited, 2003-11-20
#
#
# 1. INTRODUCTION
#
# This module defines a configuration generator for the Tracker
# integration. Configuration generators are documented in detail in
# [GDR 2000-10-16, 8].
#
# The intended readership of this document is project developers.
#
# This document is not confidential.
import os
import catalog
import check_config
import dt_tracker
import logger
import re
import string
import tracker
import time
import translator
import types
error = "Tracker configuration error"
# 2. BUILD THE MAPPING BETWEEN STATES IN TRACKER AND PERFORCE
#
# The make_state_pairs function takes a Tracker connection and the
# "closed state", and returns a list of pairs of state names (Tracker
# state, Perforce state). This list will be used to translate between
# states, and also to generate the possible values for the State field
# in Perforce.
#
# The closed_state argument is the Tracker state which maps to the
# special state 'closed' in Perforce, or None if there is no such state.
# See requirement 45. See the decision decision [RB 2000-11-28b].
#
# The case of state names in these pairs is normalized for usability in
# Perforce: see the design decision [RB 2000-11-28a].
keyword_translator = translator.keyword_translator()
date_translator = dt_tracker.date_translator()
# 3. CONVERT DATE/TIME TO SECONDS
#
# This function converts a date/time in standard format, like
# '2001-02-12 19:19:24' [ISO 8601] into seconds since the epoch.
#
# We use this to convert the start_date configuration parameter. It is
# specified as an date/time for ease of entry, but Tracker represents
# date/times as seconds since the epoch. (Note that we specify -1 for
# the DST flag -- see job000381).
def convert_isodate_to_secs(isodate):
assert isinstance(isodate, types.StringType)
date_re = "^(\d\d\d\d)-(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d)$"
match = re.match(date_re, isodate)
assert match
return time.mktime(tuple(map(int, match.groups()) + [0,0,-1]))
# 4. MIGRATION FUNCTIONS
def prepare_issue_advanced(config, tr, p4, dict, job):
# Call user hook (see [GDR 2001-11-14, 3]).
config.prepare_issue(dict, job)
def translate_jobspec_advanced(config, dt, p4, job):
return config.translate_jobspec(job)
# Function to be called if fix updated
# User Hook
def fix_update_p4_to_dt(replicator, issue, job, changes):
if not issue.bug.has_key('Date Fixed') or issue.bug['Date Fixed'] == '':
changes['Date Fixed'] = date_translator.translate_time_to_dt(time.localtime())
if not issue.bug.has_key('Fixed By') or issue.bug['Fixed By'] in ['', replicator.config.p4_user]:
changes['Date Fixed'] = date_translator.translate_time_to_dt(time.localtime())
p4_user = replicator.job_modifier(job)
dt_user = replicator.config.user_translator.translate_p4_to_dt(
p4_user, replicator.dt, replicator.dt_p4)
changes['Fixed By'] = dt_user
# Function which does global mapping of different fields from p4 jobs to issues
# User Hook
def translate_p4_to_dt(replicator, issue, job, changes):
if job[replicator.config.job_status_field] in ['closed', 'fixed']:
if not issue.bug.has_key('Status') or issue.bug['Status'] == 'Assigned':
changes['Status'] = 'Fixed'
if not issue.bug.has_key('Date Fixed') or issue.bug['Date Fixed'] == '':
changes['Date Fixed'] = date_translator.translate_time_to_dt(time.localtime())
if not issue.bug.has_key('Fixed By') or issue.bug['Fixed By'] in ['', replicator.config.p4_user]:
changes['Date Fixed'] = date_translator.translate_time_to_dt(time.localtime())
p4_user = replicator.job_modifier(job)
dt_user = replicator.config.user_translator.translate_p4_to_dt(
p4_user, replicator.dt, replicator.dt_p4)
changes['Fixed By'] = dt_user
# Function which does global mapping of from issues to jobs
# User Hook
def translate_dt_to_p4(replicator, issue, job, changes):
if issue['State'] == 'Closed':
if job[replicator.config.job_status_field] <> 'closed':
changes[replicator.config.job_status_field] = 'closed'
elif changes.has_key(replicator.config.job_status_field):
del changes[replicator.config.job_status_field]
# 5. BUILD P4DTI CONFIGURATION FOR TRACKER
def configuration(config):
# 5.1. Check Tracker-specific configuration parameters
check_config.check_string(config, 'tracker_user')
check_config.check_string(config, 'tracker_password')
check_config.check_string(config, 'tracker_project')
check_config.check_string(config, 'tracker_server')
check_config.check_string(config, 'email_domain')
check_config.check_string(config, 'query_all_scrs')
check_config.check_string(config, 'query_all_scrs_changed_in_last_day')
check_config.check_string(config, 'query_all_scrs_changed_in_last_week')
check_config.check_bool(config, 'use_windows_event_log')
if os.name <> 'nt':
# "Tracker integration only runs on Windows"
raise error, catalog.msg(1300)
# Set up loggers
loggers = []
log_params = {
'priority': config.log_level,
'max_length': config.log_max_message_length,
}
# The log messages should go to (up to) three places:
# 1. to standard output (if running from a command line);
if config.use_stdout_log:
loggers.append(apply(logger.file_logger, (), log_params))
# 2. to the file named by the log_file configuration parameter (if
# not None);
if config.log_file != None:
loggers.append(apply(logger.file_logger,
(open(config.log_file, "a"),),
log_params))
# 3. to the Windows event log (if use_windows_event_log is true).
if config.use_windows_event_log:
loggers.append(apply(logger.win32_event_logger,
(config.rid,),
log_params))
config.logger = logger.multi_logger(loggers)
# 5.2. Open a connection to the Tracker server
trk = tracker.tracker(config,
config.tracker_user,
config.tracker_password,
config.tracker_project,
config.tracker_server)
config.trk = trk
config.trk.login(config.tracker_user, config.tracker_password)
if not trk.field_exists('rid'):
# "Tracker field 'rid' must be created by administrator"
raise error, catalog.msg(1301)
config.jobname_function = lambda bug: 'SCR%06d' % int(bug['Id'])
# 5.3. Translators
#
# elapsed_time_translator = dt_tracker.elapsed_time_translator()
# int_translator = dt_tracker.int_translator()
# float_translator = dt_tracker.float_translator()
text_translator = dt_tracker.text_translator()
text_line_translator = dt_tracker.text_line_translator()
# strict user translator doesn't allow unknown users
strict_user_translator = dt_tracker.user_translator(
config.replicator_address, config.p4_user, allow_unknown = 0)
# lax user translator does allow unknown users
lax_user_translator = dt_tracker.user_translator(
config.replicator_address, config.p4_user, allow_unknown = 1)
user_translator = lax_user_translator
# Valid statuses - note duplicate 'open' for Perforce
state_pairs = [('Not Started', 'open'),
('Started', 'started'),
('Evaluated', 'evaluated'),
('Designed', 'designed'),
('Fixed', 'closed'),
('Tested', 'tested'),
('Resubmitted', 'open'),
]
# 5.6. Make values for the State field in Perforce
#
# Work out the legal values of the State field in Perforce. Note
# that "closed" must be a legal state because "p4 fix -c CHANGE
# JOBNAME" always sets the State to "closed" even if "closed" is not
# a legal value. See job000225.
legal_states = map((lambda x: x[1]), state_pairs)
if 'closed' not in legal_states:
legal_states.append('closed')
legal_states = filter(lambda s: s != None, legal_states)
unique_legal_states = []
for s in legal_states:
if s not in unique_legal_states:
unique_legal_states.append(s)
state_values = string.join(unique_legal_states, '/')
state_translator = dt_tracker.status_translator(state_pairs)
severity_pairs = []
severities = config.trk.get_choices('Severity')
for dt_s in severities:
p4_s = string.lower(dt_s)
p4_s = string.replace(p4_s, " ", "_")
severity_pairs.append((dt_s, p4_s))
severity_translator = dt_tracker.status_translator(severity_pairs)
legal_severities = map((lambda x: x[1]), severity_pairs)
severity_values = string.join(legal_severities, '/')
# Tracker fields that are read and their type.
# Used to read appropriate type in Tracker.py
#
# int - integer
# desc - description (can be greater than 255 - only 1 allowed per record
# string - string (up to 255)
#
tr_fields = {
'Id': ('int'),
'Title': ('string'),
'Assigned To': ('string'),
'Description': ('desc'),
'Perforce Note': ('note'),
'Fixed in Rel.Ver.Bld[Patch]': ('string'),
'Release in Ver.Bld[Patch]': ('string'),
'Assigned Project': ('string'),
'State': ('string'),
'Status': ('string'),
'Severity': ('string'),
'Date Fixed': ('string'),
'Fixed By': ('string'),
'Submit Date': ('string'),
'rid': ('string'),
}
for field in tr_fields.keys():
if tr_fields[field] <> 'note' and not trk.field_exists(field):
# "Tracker field '%s' doesn't exist - configuration error"
raise error, catalog.msg(1302, field)
# Some Tracker fields should not be changed from Perforce.
# Use Tracker field names.
read_only_fields = ['Id',
'Title',
'Severity',
'Description',
'Assigned To',
'Assigned Project',
'Submit Date',
'Release in Ver.Bld[Patch]']
# 5.7. Fields that always appear in the Perforce jobspec
#
# The 'p4_fields' table maps Tracker field name to a definition of
# the corresponding field in Perforce. The table also has entries
# for field not replicated from Tracker: these appear under dummy
# Tracker field names in parentheses.
#
# Perforce field definitions have nine elements:
#
# 1. Field number;
# 2. Field name;
# 3. Field type (word, line, select, date, text);
# 4. Field length;
# 5. Field disposition (always, required, optional, default);
# 6. The default value for the field, or None if there isn't one
# (field Preset);
# 7. Legal values for the field (if it's a "select" field) or None
# otherwise (field Values);
# 8. Help text for the field;
# 9. Translator for the field (if the field is replicated from
# Tracker), or None (if the field is not replicated).
#
# The five fields 101 to 105 are predefined because they are
# required by Perforce. The fields Job and Date are special: they
# are required by Perforce but are not replicated from Tracker.
# Note that their help text is given (the other help texts will be
# fetched from Tracker).
#
# We extend this table with fields from the "replicated_fields"
# configuration parameter (section 5.8). Next we use the table to
# buid the Perforce jobspec (section 5.9). Finally, we use the
# table to build the "field_map" configuration parameter which the
# replicator module uses to replicate the field (section 5.10).
p4_fields = {
'(JOB)': ( 101, 'Job', 'word', 32, 'required',
None, None,
"The job name.",
None ),
'Status': ( 102, 'STATUS', 'select', 32, 'required',
state_pairs[0][1], state_values,
"Issue's fixed status in Tracker",
state_translator ),
'Assigned To': ( 103, 'Assigned_to', 'word', 32, 'optional',
'$user', None,
"Owner of issue in Tracker",
user_translator ),
'(DATE)': ( 104, 'Date', 'date', 20, 'always',
'$now', None,
"The date this job was last modified.",
None ),
'Title': ( 105, 'Title', 'line', 0, 'required',
'$blank', None,
"Title of Issue",
text_line_translator ),
'Severity': ( 110, 'Severity', 'word', 32, 'required',
severity_pairs[0][1], severity_values,
"Severity of Issue",
severity_translator ),
'Assigned Project': ( 115, 'Assigned_project', 'line', 0, 'optional',
None, None,
"Project issue is assigned to.",
text_line_translator ),
'Fixed in Rel.Ver.Bld[Patch]': ( 120, 'FIXED_IN_REL.VER.BLD[PATCH]', 'line', 0, 'optional',
None, None,
"Which release/ver fixed in.",
text_line_translator ),
'Description': ( 125, 'Description', 'text', 0, 'optional',
None, None,
"Description of issue.",
text_translator ),
'Perforce Note': ( 130, 'PERFORCE_NOTE', 'text', 0, 'optional',
None, None,
"Notes about resolution of issue.",
text_translator ),
'Submit Date': ( 135, 'Submit_date', 'date', 20, 'optional',
'$now', None,
"The date this job was submitted.",
date_translator ),
'Release in Ver.Bld[Patch]': ( 140, 'Release_In_Ver.Bld[Patch]', 'line', 0, 'optional',
None, None,
"Targeted release.",
text_line_translator ),
'(RID)': ( 192, 'P4DTI-rid', 'word', 32, 'required',
'None', None,
"P4DTI replicator identifier. Do not edit!",
None ),
'(ISSUE)': ( 193, 'P4DTI-issue-id', 'word', 32, 'required',
'None', None,
"Tracker issue database identifier. Do not "
"edit!",
None ),
'(USER)': ( 194, 'P4DTI-user', 'word', 32, 'always',
'$user', None,
"Last user to edit this job. You can't edit "
"this!",
None ),
}
# 5.9. Make jobspec description
comment = ("# A Perforce Job Specification automatically "
"produced by the\n"
"# Perforce Defect Tracking Integration\n")
jobspec = (comment, p4_fields.values())
# 5.10. Generate configuration parameters
# Set configuration parameters needed by dt_tracker.
config.start_date = convert_isodate_to_secs(config.start_date)
config.state_pairs = state_pairs
config.read_only_fields = read_only_fields
config.tr_fields = tr_fields
# Initial fields to read to decide on issue being replicated
config.initial_fields = ["Id", "State", "Assigned Project"]
# Set configuration parameters needed by the replicator.
config.date_translator = date_translator
config.job_owner_field = 'Assigned To'
config.job_status_field = 'STATUS'
config.job_date_field = 'Date'
config.jobspec = jobspec
config.prepare_issue_advanced = prepare_issue_advanced
config.translate_jobspec_advanced = translate_jobspec_advanced
config.text_translator = text_translator
config.user_translator = user_translator
config.translate_p4_to_dt = translate_p4_to_dt
config.translate_dt_to_p4 = translate_dt_to_p4
config.fix_update_p4_to_dt = fix_update_p4_to_dt
# The field_map parameter is a list of triples (Tracker database
# field name, Perforce field name, translator) required by the
# replicator.
#
# This is generated from the p4_field table by filtering out fields
# that aren't replicated (these have no translator) and selecting
# only the three elements of interest.
config.field_map = \
map(lambda item: (item[0], item[1][1], item[1][8]),
filter(lambda item: item[1][8] != None, p4_fields.items()))
# Logout to ensure Tracker all clean and tidy
config.trk.logout()
return config
# A. REFERENCES
#
# [GDR 2001-11-14] "Perforce Defect Tracking Integration Advanced
# Administrator's Guide"; Gareth Rees; Ravenbrook Limited; 2001-11-14;
# .
#
# [GDR 2000-10-16] "Perforce Defect Tracking Integration Integrator's
# Guide"; Richard Brooksby; Ravenbrook Limited; 2000-10-16;
# .
#
# [GDR 2000-10-16] "Perforce Defect Tracking Integration Integrator's
# Guide"; Gareth Rees; Ravenbrook Limited; 2000-10-16;
# .
#
# [ISO 8601] "Representation of dates and times"; ISO; 1988-06-15.
#
# [RB 2000-11-28a] "Case of state names" (e-mail message); Richard
# Brooksby; Ravenbrook; 2000-11-28;
# .
#
#
# B. DOCUMENT HISTORY
#
# 2002-04-05 NB job000501: handle creation of new jobs when
# LASTMODIFIEDDATE or SUBMITDATE are replicated
#
#
# C. COPYRIGHT AND LICENCE
#
# This file is copyright (c) 2003 Vaccaperna Systems Ltd. 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.
#
#
# $Id: //info.ravenbrook.com/project/p4dti/version/1.5/code/replicator/configure_teamtrack.py#3 $