Question

This question was asked by a different user earlier:

Copying Test Cases and Test Folder using Rally Python or Ruby API [closed]

but closed by moderators as being an overly broad question. However, given the inability to copy Test Folders and their member Test Cases within the Rally UI, this is a common need for Rally Users.

Thus - I'll re-pose the question, hopefully with enough detail to stand as a valid question. I'll also re-post the answers that I developed for the original question.

Question: As a Rally user and developer in the Rally Python and Ruby REST APIs: how can I leverage the Rally API toolkits to accomplish this task?

Was it helpful?

Solution 2

Ruby:

This Ruby script will copy all Test Cases from a Source Test Folder identified by FormattedID, to a Target Test Folder, also identified by FormattedID. It will copy all Test Steps and Attachments as well. The Target Test Folder must exist, i.e. the script will not create a Test Folder for you if the Target is not found.

The script does not associate the new Test Case to original Test Case's Work Product (i.e. Defect, User Story), or copy Discussion items, Test Case Results, or Last Build, Verdict, etc. as it is assumed the new Test Case is desired to be in a "blank" state.

For those needing to install and configure the Ruby REST Toolkit, links are here:

Developer Portal: Rally REST API for Ruby

Github

    # Copyright 2002-2012 Rally Software Development Corp. All Rights Reserved.

    require 'rally_api'

    $my_base_url       = "https://rally1.rallydev.com/slm"
    $my_username       = "user@company.com"
    $my_password       = "password"
    $my_workspace      = "My Workspace"
    $my_project        = "My Project"
    $wsapi_version     = "1.37"

    # Test Folders
    $source_test_folder_formatted_id = "TF4"
    $target_test_folder_formatted_id = "TF8"

    # Load (and maybe override with) my personal/private variables from a file...
    my_vars= File.dirname(__FILE__) + "/my_vars.rb"
    if FileTest.exist?( my_vars ) then require my_vars end

    #==================== Make a connection to Rally ====================
    config                  = {:base_url => $my_base_url}
    config[:username]       = $my_username
    config[:password]       = $my_password
    config[:workspace]      = $my_workspace
    config[:project]        = $my_project
    config[:version]        = $wsapi_version

    @rally = RallyAPI::RallyRestJson.new(config)

    begin

      # Lookup source Test Folder
      source_test_folder_query = RallyAPI::RallyQuery.new()
      source_test_folder_query.type = :testfolder
      source_test_folder_query.fetch = true
      source_test_folder_query.query_string = "(FormattedID = \"" + $source_test_folder_formatted_id + "\")"

      source_test_folder_result = @rally.find(source_test_folder_query)

      # Lookup Target Test Folder
      target_test_folder_query = RallyAPI::RallyQuery.new()
      target_test_folder_query.type = :testfolder
      target_test_folder_query.fetch = true
      target_test_folder_query.query_string = "(FormattedID = \"" + $target_test_folder_formatted_id + "\")"

      target_test_folder_result = @rally.find(target_test_folder_query)

      if source_test_folder_result.total_result_count == 0
        puts "Source Test Folder: " + $source_test_folder_formatted_id + "not found. Exiting."
        exit
      end

      if target_test_folder_result.total_result_count == 0
        puts "Target Test Folder: " + $target_test_folder_formatted_id + "not found. Target must exist before copying."
        exit
      end

      source_test_folder = source_test_folder_result.first()
      target_test_folder = target_test_folder_result.first()

      # Populate full object for Target Test Folder
      full_target_test_folder = target_test_folder.read

      # Get Target Project
      target_project = full_target_test_folder["Project"]

      # Grab collection of Source Test Cases
      source_test_cases = source_test_folder["TestCases"]

      # Loop through Source Test Cases and Copy to Target
      source_test_cases.each do |source_test_case|
        # Get full object for Source Test Case
        full_source_test_case = source_test_case.read

        # Check if there's an Owner
        if !full_source_test_case["Owner"].nil?
          source_owner = full_source_test_case["Owner"]
        else
          source_owner = nil
        end

        # Populate field data from Source to Target
        target_test_case_fields = {}
        target_test_case_fields["Package"] = full_source_test_case["Package"]
        target_test_case_fields["Description"] = full_source_test_case["Description"]
        target_test_case_fields["Method"] = full_source_test_case["Method"]
        target_test_case_fields["Name"] = full_source_test_case["Name"]
        target_test_case_fields["Objective"] = full_source_test_case["Objective"]
        target_test_case_fields["Owner"] = source_owner
        target_test_case_fields["PostConditions"] = full_source_test_case["PostConditions"]
        target_test_case_fields["PreConditions"] = full_source_test_case["PreConditions"]
        target_test_case_fields["Priority"] = full_source_test_case["Priority"]
        target_test_case_fields["Project"] = target_project
        target_test_case_fields["Risk"] = full_source_test_case["Risk"]
        target_test_case_fields["ValidationInput"] = full_source_test_case["ValidationInput"]
        target_test_case_fields["ValidationExpectedResult"] = full_source_test_case["ValidationExpectedResult"]
        target_test_case_fields["Tags"] = full_source_test_case["Tags"]
        target_test_case_fields["TestFolder"] = target_test_folder

        # Create the Target Test Case
        begin
          target_test_case = @rally.create(:testcase, target_test_case_fields)
          puts "Test Case: #{full_source_test_case["FormattedID"]} successfully copied to #{full_target_test_folder["FormattedID"]}"
        rescue => ex
          puts "Test Case: #{full_source_test_case["FormattedID"]} not copied due to error"
          puts ex
        end

        # Now Copy Test Steps
        # Add Test Case Steps
        source_test_case_steps = full_source_test_case["Steps"]

        source_test_case_steps.each do |source_test_case_step|
          full_source_step = source_test_case_step.read
          target_step_fields = {}
          target_step_fields["TestCase"] = target_test_case
          target_step_fields["StepIndex"] = full_source_step["StepIndex"]
          target_step_fields["Input"] = full_source_step["Input"]
          target_step_fields["ExpectedResult"] = full_source_step["ExpectedResult"]
          begin
            target_test_case_step = @rally.create(:testcasestep, target_step_fields)
            puts "===> Copied TestCaseStep: #{target_test_case_step["_ref"]}"
          rescue => ex
            puts "Test Case Step not copied due to error:"
            puts ex
          end
        end

          # Now Copy Attachments
          source_attachments = full_source_test_case["Attachments"]

        source_attachments.each do |source_attachment|
          full_source_attachment = source_attachment.read
          source_attachment_content = full_source_attachment["Content"]
          full_source_attachment_content = source_attachment_content.read

          # Create AttachmentContent Object for Target
          target_attachment_content_fields = {}
          target_attachment_content_fields["Content"] = full_source_attachment_content["Content"]
          begin
            target_attachment_content = @rally.create(:attachmentcontent, target_attachment_content_fields)
            puts "===> Copied AttachmentContent: #{target_attachment_content["_ref"]}"
          rescue => ex
            puts "AttachmentContent not copied due to error:"
            puts ex
          end

          # Now Create Attachment Container
          target_attachment_fields = {}
          target_attachment_fields["Name"] = full_source_attachment["Name"]
          target_attachment_fields["Description"] = full_source_attachment["Description"]
          target_attachment_fields["Content"] = target_attachment_content
          target_attachment_fields["ContentType"] = full_source_attachment["ContentType"]
          target_attachment_fields["Size"] = full_source_attachment["Size"]
          target_attachment_fields["Artifact"] = target_test_case
          target_attachment_fields["User"] = full_source_attachment["User"]
          begin
            target_attachment = @rally.create(:attachment, target_attachment_fields)
            puts "===> Copied Attachment: #{target_attachment["_ref"]}"
          rescue => ex
            puts "Attachment not copied due to error:"
            puts ex
          end
        end
      end
    end

OTHER TIPS

Python:

Here is a script that performs this task - it will copy all Test Cases from a Source Test Folder identified by FormattedID, to a Target Test Folder, also identified by FormattedID. It will copy all Test Steps and Attachments as well. The Target Test Folder must exist, i.e. the script will not create a Test Folder for you if the Target is not found.

The script does not associate the new Test Case to original Test Case's Work Product (i.e. Defect, User Story), or copy Discussion items, Test Case Results, or Last Build, Verdict, etc. as it is assumed the new Test Case is desired to be in a "blank" state.

For those needing to install and configure the Rally Python REST Library:

Rally Developer Portal: Pyral Download and Installation

Pyral Documentation

#!/usr/bin/env python

#################################################################################################
#
#  copy_test_folder.py -- Copy all Test Cases in Source Test Folder to Target. Includes Test Steps
#                         and attachments. Target Test Folder must exist (i.e. the script will not
#                         create a new targeet Test Folder for you)
#
USAGE = """
Usage: copy_test_folder.py
"""
#################################################################################################
# import needed python libs
import sys, os
import re
import string
import base64

from pprint import pprint

# import needed pyral libs
from pyral import Rally, rallySettings, RallyRESTAPIError

errout = sys.stderr.write

my_server      = "rally1.rallydev.com"
my_user        = "user@company.com"
my_password    = "topsecret"
my_workspace   = "My Workspace"
my_project     = "My Project"

source_test_folder_formatted_id = "TF1"
target_test_folder_formatted_id = "TF4"

rally = Rally(my_server, my_user, my_password, workspace=my_workspace, project=my_project)
# rally = Rally(my_server, my_user, my_password, workspace=my_workspace, project=my_project, debug=True)
rally.enableLogging('copy_test_folder.log')

# Query for source and target test folders
source_test_folder_response = rally.get('TestFolder', fetch=True, query='FormattedID = %s' % source_test_folder_formatted_id)
target_test_folder_response = rally.get('TestFolder', fetch=True, query='FormattedID = %s' % target_test_folder_formatted_id)

# Check to make sure folders exist
if source_test_folder_response.resultCount == 0:
                 errout('No Source Test Folder Found matching Formatted ID: %s\n' % (source_test_folder_formatted_id))
                 sys.exit(4)

if target_test_folder_response.resultCount == 0:
                 errout('No Target Test Folder Found matching Formatted ID: %s\n. Target Test Folder must be created before copying.' % (target_test_folder_formatted_id))
                 sys.exit(4)

# Get references to source Test Folder and Test Cases, etc.                 
source_test_folder = source_test_folder_response.next()
source_test_cases = source_test_folder.TestCases

# Get reference to target Test Folder
target_test_folder = target_test_folder_response.next()

for source_test_case in source_test_cases:
                 # Create update fields for target Test Case
                 # Does NOT associate new Test Case to original Test Case's work product (i.e. Defect, User Story)
                 # Does NOT copy Discussion items - as the old Discussions are likely not desired on new Test Case
                 # Does NOT copy Last Build, Last Run, Last Update Date, Last Verdict as new Test Case will effectively
                 # be "blank" and not have any results associated to it
                 if source_test_case.Owner != None:
                                  target_owner = source_test_case.Owner.ref
                 else:
                                  target_owner = None
                 target_test_case_fields = {
                                  "Package": source_test_case.Package,
                                  "Description": source_test_case.Description,
                                  "Method": source_test_case.Method,
                                  "Name": source_test_case.Name,
                                  "Objective": source_test_case.Objective,
                                  "Owner": target_owner,
                                  "PostConditions": source_test_case.PostConditions,
                                  "PreConditions": source_test_case.PreConditions,
                                  "Priority": source_test_case.Priority,
                                  "Project": source_test_case.Project.ref,
                                  "Risk": source_test_case.Risk,
                                  "ValidationInput": source_test_case.ValidationInput,
                                  "ValidationExpectedResult": source_test_case.ValidationExpectedResult,
                                  "TestFolder": target_test_folder.ref,
                 }

                 # Create the target test case
                 try:
                                  target_test_case = rally.create("TestCase", target_test_case_fields)
                                  message = "Copied Source Test Case: " + source_test_case.FormattedID + \
                                            " To: " + target_test_folder.FormattedID + ": " + target_test_folder.Name + \
                                            ": " + target_test_case.FormattedID
                                  print message

                 except RallyRESTAPIError, details:
                                  sys.stderr.write('ERROR: %s \n' % details)
                                  sys.exit(2)


                 # Copy Test Steps
                 # Add Test Case Steps
                 source_test_case_steps = source_test_case.Steps
                 for source_step in source_test_case_steps:
                                  target_step_fields = {
                                                  "TestCase"          : target_test_case.ref,
                                                  "StepIndex"         : source_step.StepIndex,
                                                  "Input"             : source_step.Input,
                                                  "ExpectedResult"    : source_step.ExpectedResult                                                   
                                  }
                                  target_test_case_step = rally.put('TestCaseStep', target_step_fields)
                                  print "===> Copied  TestCaseStep: %s   OID: %s" % (target_test_case_step.StepIndex, target_test_case_step.oid)                 

                 # Copy Attachments
                 source_attachments = rally.getAttachments(source_test_case)

                 for source_attachment in source_attachments:

                                  # First copy the content
                                  source_attachment_content = source_attachment.Content
                                  target_attachment_content_fields = {
                                                   "Content": base64.encodestring(source_attachment_content)
                                  }

                                  try:
                                                   target_attachment_content = rally.put('AttachmentContent', target_attachment_content_fields)
                                                   print "===> Copied AttachmentContent: %s" % target_attachment_content.ref
                                  except RallyRESTAPIError, details:
                                                   sys.stderr.write('ERROR: %s \n' % details)
                                                   sys.exit(2)

                                  # Next copy the attachment object
                                  target_attachment_fields = {
                                                   "Name": source_attachment.Name,
                                                   "Description": source_attachment.Description,
                                                   "Content": target_attachment_content.ref,
                                                   "ContentType": source_attachment.ContentType,
                                                   "Size": source_attachment.Size,
                                                   "Artifact": target_test_case.ref,
                                                   "User": source_attachment.User.ref
                                  }
                                  try:
                                                   target_attachment = rally.put('Attachment', target_attachment_fields)
                                                   print "===> Copied Attachment: %s" % target_attachment.ref
                                  except RallyRESTAPIError, details:
                                                   sys.stderr.write('ERROR: %s \n' % details)
                                                   sys.exit(2)


                 # Copy Tags
                 source_tags = source_test_case.Tags;
                 target_tags = list()
                 for source_tag in source_tags:
                                  target_tags.append({"_ref":source_tag.ref})


                 target_test_case_fields = {
                                  "FormattedID": target_test_case.FormattedID,
                                  "Tags": target_tags
                 }

                 try:
                                  update_response = rally.update('TestCase', target_test_case_fields)
                 except RallyRESTAPIError, details:
                                  sys.stderr.write('ERROR: %s \n' % details)
                                  sys.exit(2)

An improvement of the same Python script that adds the following features

  • Test Folders (source and destination) are passed in arguments instead of hard coded
  • Creation is done recursively (i.e. copy Test Folders and Test Cases in it)
  • Test Cases are replaced if they already exist (based on the Name)
  • Usage of API Key to connect rather than password
  • Try 5 times to connect to Rally before giving up

To use it to copy TF10 to TF99 (which already exists): python copy_test_folder TF10 TF99

#!/usr/bin/env python

#################################################################################################
#
#  copy_test_folder.py -- Copy all Test Cases in Source Test Folder to Target. Includes Test Steps
#                         and attachments. Target Test Folder must exist (i.e. the script will not
#                         create a new targeet Test Folder for you)
#
USAGE = """
Usage: copy_test_folder.py TF_src TF_dest
"""
#################################################################################################
# import needed python libs
import sys, os
import re
import string
import base64

from pprint import pprint

# import needed pyral libs
from pyral import Rally, rallySettings, RallyRESTAPIError

errout = sys.stderr.write

my_server      = "rally1.rallydev.com"
my_user        = "name@domain.com"
my_password    = "<PASSWORD>"
my_workspace   = "<WORKSPACE>"
my_project     = "<PROJECT>"
my_key         = "<API KEY>"


def connect(nb_attempts=5):
    '''Connect to Rally'''
    global is_connected
    global rally
    #Connect
    if not is_connected and nb_attempts>0:
        print "...Attempting to connect to Rally for project %s..." % my_project
        try:
            # rally = Rally(my_server, my_user, my_password, workspace=my_workspace, project=my_project)
            rally = Rally(my_server, apikey=my_key, workspace=my_workspace, project=my_project)
            print "Connected to Rally for project %s (%s)" % (my_project, my_workspace)
            is_connected=True
            rally.enableLogging('copy_test_folder.log')
        #Errors during connection (attempting to connect again)
        # except Exception, details: 
        except AttributeError: 
            if nb_attempts>1:
                connect(nb_attempts-1)
            else:
                errout('Error during connection to Rally (%s)\n' % details)
                exit(4)
    else: pass


def copyTF(src_TF, dest_TF):
    '''Copy Test Cases from one folder into another (including children Test Folders)'''
    # List names and FormattedID of existing Test Cases in destination
    dest_TCs = dest_TF.TestCases
    existing_dest_TC = {tc.Name:tc.FormattedID for tc in dest_TCs} 

    # Copy Test Cases to destination folder (replace if Test Case with same name exists)
    src_TCs = src_TF.TestCases
    for src_TC in src_TCs:
        # Create update fields for target Test Case
        # Does NOT associate new Test Case to original Test Case's WorkProduct (i.e. Defect, User Story)
        # Does NOT copy Discussion items - as the old Discussions are likely not desired on new Test Case
        # Does NOT copy Last Build, Last Run, Last Update Date, Last Verdict as new Test Case will effectively
        # be "blank" and not have any results associated to it
        tcName = src_TC.Name
        dest_TC_fields = {
            "Package": src_TC.Package,
            "Description": src_TC.Description,
            "Method": src_TC.Method,
            "Name": tcName,
            "Objective": src_TC.Objective,
            "Owner": getattr(src_TC.Owner, 'ref', None),
            "PostConditions": src_TC.PostConditions,
            "PreConditions": src_TC.PreConditions,
            "Priority": src_TC.Priority,
            "Project": src_TC.Project.ref,
            "Risk": src_TC.Risk,
            "ValidationInput": src_TC.ValidationInput,
            "ValidationExpectedResult": src_TC.ValidationExpectedResult,
            "TestFolder": dest_TF.ref,
         }

        # Create/Update the target test case
        try:
            if existing_dest_TC.has_key(tcName):
                operation = "Update"
                dest_TC_fields['FormattedID'] = existing_dest_TC[tcName]
                dest_TC = rally.update("TestCase", dest_TC_fields)
            else:
                #Create
                operation = "Create"
                dest_TC = rally.create("TestCase", dest_TC_fields)

            message = operation + "d Source Test Case: " + src_TC.FormattedID + \
                " To: " + dest_TF.FormattedID + ": " + dest_TF.Name + \
                ": " + dest_TC.FormattedID
            print message

        except RallyRESTAPIError, details:
            sys.stderr.write('ERROR: %s \n' % details)
            sys.exit(2)


        # Copy Test Steps
        #Cleared-up all Test Steps in destination if Test Case is updated
        if operation == "Update":
            dest_TC_steps = dest_TC.Steps
            for dest_step in dest_TC_steps:
                rally.delete('TestCaseStep', dest_step.oid)
            print "===> Cleared all Test Steps"

        # Add Test Case Steps
        src_TC_steps = src_TC.Steps
        for src_step in src_TC_steps:
            target_step_fields = {
                "TestCase"          : dest_TC.ref,
                "StepIndex"         : src_step.StepIndex,
                "Input"             : src_step.Input,
                "ExpectedResult"    : src_step.ExpectedResult                                                   
            }
            dest_TC_step = rally.put('TestCaseStep', target_step_fields)
            print "===> Copied  TestCaseStep: %s   OID: %s" % (dest_TC_step.StepIndex, dest_TC_step.oid)                 

        # Copy Attachments
        source_attachments = rally.getAttachments(src_TC)

        for source_attachment in source_attachments:
            # First copy the content
            source_attachment_content = source_attachment.Content
            target_attachment_content_fields = {
                "Content": base64.encodestring(source_attachment_content)
            }

            try:
                target_attachment_content = rally.put('AttachmentContent', target_attachment_content_fields)
                print "===> Copied AttachmentContent: %s" % target_attachment_content.ref
            except RallyRESTAPIError, details:
                sys.stderr.write('ERROR: %s \n' % details)
                sys.exit(2)

            # Next copy the attachment object
            target_attachment_fields = {
                "Name": source_attachment.Name,
                "Description": source_attachment.Description,
                "Content": target_attachment_content.ref,
                "ContentType": source_attachment.ContentType,
                "Size": source_attachment.Size,
                "Artifact": dest_TC.ref,
                "User": source_attachment.User.ref
            }
            try:
                target_attachment = rally.put('Attachment', target_attachment_fields)
                print "===> Copied Attachment: %s" % target_attachment.ref
            except RallyRESTAPIError, details:
                sys.stderr.write('ERROR: %s \n' % details)
                sys.exit(2)


        # Copy Tags
        source_tags = src_TC.Tags;
        target_tags = list()
        for source_tag in source_tags:
            target_tags.append({"_ref":source_tag.ref})

        dest_TC_fields = {
            "FormattedID": dest_TC.FormattedID,
            "Tags": target_tags
         }

        try:
            update_response = rally.update('TestCase', dest_TC_fields)
        except RallyRESTAPIError, details:
            sys.stderr.write('ERROR: %s \n' % details)
            sys.exit(2)

    # Recursive call for each child Test Folder (after creating it)
    src_children_TFs = src_TF.Children
    dest_children_TFs = dest_TF.Children
    existing_dest_TF = {tf.Name:tf for tf in dest_children_TFs} 
    for src_child_TF in src_children_TFs:
        tfName = src_child_TF.Name
        # Create Test Folder if needed
        if existing_dest_TF.has_key(tfName):
            dest_child_TF = existing_dest_TF[tfName]
        else:
            target_TF_fields = {
                'Name': tfName,
                'Parent': dest_TF.ref
            }
            dest_child_TF = rally.put('TestFolder', target_TF_fields)
            print "Created Test folder %s (%s)" % (tfName, dest_child_TF.FormattedID)
        # Copy Test Cases of this folder
        copyTF(src_child_TF, dest_child_TF)

if '__main__' in __name__:

    # Get source and destination test folders
    src_TF_formatted_id = sys.argv[1]
    dest_TF_formatted_id = sys.argv[2]

    # Connect to Rally
    global is_connected
    is_connected = False
    connect(5)

    # Query for source and target test folders
    global rally
    src_TF_response = rally.get('TestFolder', fetch=True, query='FormattedID = %s' % src_TF_formatted_id)
    dest_TF_response = rally.get('TestFolder', fetch=True, query='FormattedID = %s' % dest_TF_formatted_id)

    # Check to make sure folders exist
    if src_TF_response.resultCount == 0:
        errout('No Source Test Folder Found matching Formatted ID: %s\n' % (src_TF_formatted_id))
        sys.exit(4)

    if dest_TF_response.resultCount == 0:
        errout('No Target Test Folder Found matching Formatted ID: %s\n. Target Test Folder must be created before copying.' % (dest_TF_formatted_id))
        sys.exit(4)

    # Get Objects of source and target Test Folders                
    src_TF = src_TF_response.next()
    dest_TF = dest_TF_response.next()

    # Copy file
    copyTF(src_TF, dest_TF)
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top