Rally APIs: How to Copy Test Folder and Member Test Cases

Rally APIs: How to copy Test Folder and member Test Cases

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

Rally APIs: How to Move A Test Folder

The following script will perform this task - it will move all Test Cases from a Source Test Folder identified by FormattedID, to a Target Test Folder, also identified by FormattedID. The Source Test Folder and Target Test Folder can be in different Projects (although they must be within the same Workspace). Like the Copy script referenced in the question, the Target Test Folder must exist, i.e. the script will not create a Test Folder for you if the Target is not found.

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.39"

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

#==================== 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 moving."
exit
end

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

# Populate full object for both Source and Target Test Folders
full_source_test_folder = source_test_folder.read
full_target_test_folder = target_test_folder.read

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

# Loop through Source Test Cases and Move to Target
source_test_cases.each do |source_test_case|
begin

test_case_to_update = source_test_case.read
source_test_case_formatted_id = test_case_to_update["FormattedID"]

target_project = full_target_test_folder["Project"]
target_project_full_object = target_project.read
target_project_name = target_project_full_object["Name"]

source_project = full_source_test_folder["Project"]
source_project_full_object = source_project.read
source_project_name = source_project_full_object["Name"]

puts "Source Project Name: #{source_project_name}"
puts "Target Project Name: #{target_project_name}"

# Test if the source project and target project are the same
source_target_proj_match = source_project_name.eql?(target_project_name)

# If the target Test Folder is in a different Project, we have to do some homework first:
# "un-Test Folder" the project
# Assign the Test Case to the Target Project
# Assign the Test Case to the Target Test Folder
if !source_target_proj_match then
fields = {}
fields["TestFolder"] = ""
test_case_updated = @rally.update(:testcase, test_case_to_update.ObjectID, fields) #by ObjectID
puts "Test Case #{source_test_case_formatted_id} successfully dissociated from: #{$source_test_folder_formatted_id}"

# Get full object on Target Project and assign Test Case to Target Project
fields = {}
fields["Project"] = target_project_full_object
test_case_updated = @rally.update(:testcase, test_case_to_update.ObjectID, fields) #by ObjectID
puts "Test Case #{source_test_case_formatted_id} successfully assigned to Project: #{target_project_name}"
end

# Change the Test Folder attribute on the Test Case
fields = {}
fields["TestFolder"] = target_test_folder
test_case_updated = @rally.update(:testcase, test_case_to_update.ObjectID, fields) #by ObjectID
puts "Test Case #{source_test_case_formatted_id} successfully moved to #{$target_test_folder_formatted_id}"
rescue => ex
puts "Test Case #{source_test_case_formatted_id} not updated due to error"
puts ex
end
end
end

Getting all the test cases inside the test folder using Pyral api

It's not clear what you are trying to achieve but please take a look at the code below. Probably, it will solve your issue:

query = 'FormattedID = %s'

test_folder_req = rally.get('TestFolder', fetch=True, projectScopeDown=True,
query=query % folder_id) # folder_id == "TF111"
if test_folder_req.resultCount == 0:
print('Rally returned nothing for folder: %s', folder_id)
sys.exit(1)

test_folder = test_folder_req.next()

test_cases = test_folder.TestCases
print('Start working with %s' % folder_id)
print('Test Folder %s contains %s TestCases' % (folder_id, len(test_cases)))

for test_case in test_cases:
print(test_case.FormattedID)
print(test_case.Name)

How to query for test cases by test folder name using Ruby REST API?

Let's say you have the test folder name as a result of previous query:

folder_name = tf_results.first.Name

The test cases that are associated with this test folder can be found using this syntax:

tc_query.query_string = "(TestFolder.Name = \"#{folder_name}\")" 

Here is the full code that uses Rally Ruby REST API (rally_api gem).

require 'rally_api'

#Setup custom app information
headers = RallyAPI::CustomHttpHeader.new()
headers.name = "find test cases by test folder name"
headers.vendor = "Nick M RallyLab"
headers.version = "1.0"

# Connection to Rally
config = {:base_url => "https://rally1.rallydev.com/slm"}
config[:username] = "user@co.com"
config[:password] = "secret"
config[:workspace] = "W"
config[:project] = "P"
config[:headers] = headers #from RallyAPI::CustomHttpHeader.new()

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

tf_query = RallyAPI::RallyQuery.new()
tf_query.type = :testfolder
tf_query.fetch = "Name,FormattedID"
tf_query.workspace = {"_ref" => "https://rally1.rallydev.com/slm/webservice/1.43/workspace/11111.js" } #optional
tf_query.project = {"_ref" => "https://rally1.rallydev.com/slm/webservice/1.43/project/22222.js" } #optional
tf_query.page_size = 200 #optional - default is 200
tf_query.limit = 1000 #optional - default is 99999
tf_query.project_scope_up = false
tf_query.project_scope_down = true
tf_query.order = "Name Asc"
tf_query.query_string = "(Name = \"My Test Folder 1\")"

tf_results = @rally.find(tf_query)

tf_results.each do |t|
puts "Name: #{t["Name"]}, FormattedID: #{t["FormattedID"]}"
t.read
end

folder_name = tf_results.first.Name

tc_query = RallyAPI::RallyQuery.new()
tc_query.type = :testcase
tc_query.fetch = "Name,FormattedID"
tc_query.workspace = {"_ref" => "https://rally1.rallydev.com/slm/webservice/1.43/workspace/12352608129.js" } #optional
tc_query.project = {"_ref" => "https://rally1.rallydev.com/slm/webservice/1.43/project/12352608219.js" } #optional
tc_query.page_size = 200 #optional - default is 200
tc_query.limit = 1000 #optional - default is 99999
tc_query.project_scope_up = false
tc_query.project_scope_down = true
tc_query.order = "Name Asc"

tc_query.query_string = "(TestFolder.Name = \"#{folder_name}\")"

puts "same string" if ("My Test Folder 1" == folder_name)

tc_results = @rally.find(tc_query)

tc_results.each do |t|
puts "Name: #{t["Name"]}, FormattedID: #{t["FormattedID"]}"
t.read
end

How to copy a test set with its test cases and maintain the original relative ranking of test cases within the test set

It is a defect that was first mentioned in this post.
Unfortunately the defect is still open.

Preferred method to copy a defect using Rally REST API in Python

Final solution including Mark W's code for copying attachments

def getDataCopy( data ):

""" Given a piece of data, figure out how to copy it. If it's a native python type
like a string or numeric, just return the value. If it's a rally object, return
the ref to it. If it's a list, iterate and call ourself recursively for the
list members. """

if isinstance( data, types.ListType ):
copyData = []
for entry in data:
copyData.append( getDataCopy(entry) )

elif hasattr( data, "ref" ):
copyData = { "_ref": data.ref }

else:
copyData = data

return copyData

def getArtifactCopy( artifact ):

""" Build a dictionary based on the values in the specified artifact. This dictionary
can then be passed to a rallyConn.put() call to actually create the new entry in
Rally. Attachments and Tasks must be copied seperately, since they require creation
of additional artifacts """

newArtifact = {}

for attrName in artifact.attributes():

# Skip the attributes that we can't or shouldn't handle directly
if attrName.startswith("_") or attrName == "oid" or attrName == "Iteration" or attrName == "Attachments":
continue

attrValue = getattr( artifact, attrName )
newArtifact[attrName] = getDataCopy( attrValue )

if getattr( artifact, "Iteration", None ) != None:
newArtifact["Iteration"] = { "_ref": "iteration/" + artifact.Iteration.oid }

return newArtifact

def copyAttachments( rallyConn, oldArtifact, newArtifact ):

""" For each attachment in the old artifact, create new attachments and attach them to the new artifact"""

# Copy Attachments
source_attachments = rallyConn.getAttachments(oldArtifact)

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 = rallyConn.put( 'AttachmentContent', target_attachment_content_fields )
print "\t===> Copied AttachmentContent: %s" % target_attachment_content.ref
except pyral.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,
"User": source_attachment.User.ref
}

# Attach it to the new artifact
target_attachment_fields["Artifact"] = newArtifact.ref

try:
target_attachment = rallyConn.put( source_attachment._type, target_attachment_fields)
print "\t===> Copied Attachment: '%s'" % target_attachment.Name
except pyral.RallyRESTAPIError, details:
sys.stderr.write('ERROR: %s \n' % details)
sys.exit(2)

def copyTasks( rallyConn, oldArtifact, newArtifact ):

""" Iterate over the old artifacts tasks and create new ones, attaching them to the new artifact """

for currentTask in oldArtifact.Tasks:

newTask = getArtifactCopy( currentTask )

# Assign the new task to the new artifact
newTask["WorkProduct"] = newArtifact.ref

# Push the new task into rally
newTaskObj = rallyConn.put( currentTask._type, newTask )

# Copy any attachments the task had
copyAttachments( rallyConn, currentTask, newTaskObj )

def copyDefect( rallyConn, currentDefect, addlUpdates = {} ):

""" Copy a defect including its attachments and tasks. Add the new defect as a
duplicate to the original """

newArtifact = getArtifactCopy( currentDefect )

# Add the current defect as a duplicate for the new one
newArtifact["Duplicates"].append( { "_ref": currentDefect.ref } )

# Copy in any updates that might be needed
for (attrName, attrValue) in addlUpdates.items():
newArtifact[attrName] = attrValue

print "Copying %s: %s..." % (currentDefect.Project.Name, currentDefect.FormattedID),
newDefect = rallyConn.create( currentDefect._type, newArtifact )

print "done, new item", newDefect.FormattedID

print "\tCopying attachments"
copyAttachments( rallyConn, currentDefect, newDefect )

print "\tCopying tasks"
copyTasks( rallyConn, currentDefect, newDefect )


Related Topics



Leave a reply



Submit