# 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 $