Skip to content

Commit

Permalink
fix defect in get all runs
Browse files Browse the repository at this point in the history
  • Loading branch information
Leow, Max committed May 17, 2022
1 parent 9febad5 commit e2c8655
Show file tree
Hide file tree
Showing 3 changed files with 130 additions and 41 deletions.
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

setuptools.setup(
name="testrail-data",
version="0.0.8",
version="0.0.9",
install_requires=[
"pandas",
"testrail-api>=1.10",
Expand Down
155 changes: 117 additions & 38 deletions testrail_data/_category.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,23 +24,30 @@


def auto_offset(f):
"""
A decorator to work with pagination and connection error retry.
:param f:
:return:
"""
@functools.wraps(f)
def wrap(*args, **kwargs):
offset = 0
if kwargs.get('offset'):
assert False, 'offset has been auto managed'

def auto_reset_connection(*args, **kwargs):
def auto_reset_connection(*_args, **_kwargs):
trial = retry_total
while trial > 0:
try:
return f(*args, **kwargs)
return f(*_args, **_kwargs)
except ConnectionError:
trial -= 1
if trial == 0:
raise
time.sleep(retry_sleep)
continue

df = auto_reset_connection(*args, **kwargs, offset=offset)
data_size = df.shape[0]
frames = [df]
Expand All @@ -56,13 +63,18 @@ def auto_reset_connection(*args, **kwargs):

class Metas(_MetaCategory):

def fill_custom_fields(self, project_id: int, df: DataFrame):
def fill_custom_fields(self, project_id: int, df: DataFrame, warning=False):
"""
A helper to resolve meta data fill up for custom-columns
A helper to resolve metadata fill up for custom-columns.
Unmatched columns is assigned with `UNKNOWN <config_id>`.
:param project_id:
The ID of the project
:param df:
Dataframe contains custom-columns
:param warning:
False to turn off warning for unmatched columns, True is otherwise.
:return:
"""
lookup_case_field = CaseFields(self._session).get_configs()
Expand All @@ -88,15 +100,16 @@ def list_type(y):
return lookup_custom_field[project_id][x]
return lookup_custom_field[project_id][x]
except KeyError:
print(column, x, type(x))
if warning:
print(column, x, type(x))
return f'UNKNOWN {x}'

for col in [c for c in df.columns if 'custom_' in c]:
df[col] = df[col].apply(fill, column=col)

def fill_id_fields(self, project_id: int, suite_id: int, df: DataFrame):
"""
A helper to resolve meta data fill up for Ids columns
A helper to resolve metadata fill up for Ids columns
:param project_id:
The ID of the project
:param suite_id:
Expand All @@ -106,6 +119,7 @@ def fill_id_fields(self, project_id: int, suite_id: int, df: DataFrame):
Dataframe contains custom-columns
:return:
"""

def lookup_wrapper(key, lookup: dict):
try:
return lookup[key]
Expand Down Expand Up @@ -133,35 +147,43 @@ class Runs(TR_Runs):
def get_runs_by_milestone(
self,
*milestone_ids: int,
project_id: int,
include_plan=False
project_id: int
) -> DataFrame:
"""
Returns a list of run on an existing milestones.
:param milestone_ids
:param project_id
Returns a list of run on an existing milestones,
including those runs in sub-milestones and plans.
:param milestone_ids:
:param project_id:
The ID of the project
:param include_plan:
True to retrieve all runs under each plan if there are any.
:return: response
:return: DataFrame
"""
dfs = []
for mid in milestone_ids:
df_run1 = self.to_dataframe(project_id=project_id, milestone_id=mid)
if include_plan:
def get_runs(*mile_ids):
dataframes = []
for mid in mile_ids:
# Get all run
df_run1 = self.to_dataframe(project_id=project_id, milestone_id=mid)
if df_run1.shape[0] > 0:
dataframes.append(df_run1)
# Get all plan
plans = Plans(self._session).get_plans(project_id=project_id, milestone_id=mid)
plan_ids = [plan['id'] for plan in plans]
df_run2 = self.dataframe_from_plan(*plan_ids)
dfs.append(pd.concat([df_run1, df_run2]))
else:
dfs.append(df_run1)
if df_run2.shape[0] > 0:
dataframes.append(df_run2)
return dataframes

dfs = get_runs(milestone_ids)
milestone = Milestones(self._session)
df_milestone = milestone.sub_milestones_to_dataframe(*milestone_ids).filter(['id'])
dfs.extend(get_runs(*df_milestone['id'].to_list()))
return pd.concat(dfs).reset_index(drop=False)

def get_runs_by_plan(self, *plan_ids: int) -> list:
"""
Returns a list of run on an existing test plan.
:param plan_ids:
param plan_ids:
The ID or IDs of the test plan
:return: response
"""
Expand All @@ -172,7 +194,7 @@ def get_runs_by_plan(self, *plan_ids: int) -> list:
def to_dataframe(self, project_id: int, **kwargs) -> DataFrame:
"""
Returns a List of test runs for a project as DataFrame. Only returns those test runs that
are not part of a test plan (please see get_plans/get_plan for this).
are not part of a test plan (please see get_plans/get_plan for this)
:param project_id: int
The ID of the project
Expand All @@ -194,7 +216,7 @@ def to_dataframe(self, project_id: int, **kwargs) -> DataFrame:
A single Reference ID (e.g. TR-a, 4291, etc.)
:key suite_id: List[int] or comma-separated string
A comma-separated list of test suite IDs to filter by.
:return: response`
:return: DataFrame
"""
return DataFrame(self.get_runs(project_id, **kwargs))

Expand Down Expand Up @@ -406,7 +428,7 @@ def to_dataframe(self, project_id: int) -> DataFrame:

def get_template_lookup(self, project_id: int) -> dict:
"""
Returns a lookup map for each templates as follow:
Returns a lookup map for each template as follow:
{
<TEMPLATE_ID>: <TEMPLATE_NAME>
Expand Down Expand Up @@ -463,9 +485,22 @@ def split_by_comma(i: str, index):

class CaseTypes(TR_CaseType):
def to_dataframe(self) -> DataFrame:
"""
Returns a list of available case types.
The response includes an array of test case types.
Each case type has a unique ID and a name.
The is_default field is true for the default case type and false otherwise.
:return: DataFrame
"""
return DataFrame(self.get_case_types())

def get_case_types_lookup(self) -> dict:
"""
Return a dictionary which mapped with `id` and `name`
:return:
"""
df = self.to_dataframe()
return dict(zip(df['id'], df['name']))

Expand All @@ -483,6 +518,24 @@ class Results(TR_Results):

@auto_offset
def dataframe_from_case(self, run_id: int, case_id: int, **kwargs) -> DataFrame:
"""
Returns a list of test results for a test run and case combination in Dataframe
:param run_id:
The ID of the test run
:param case_id:
The ID of the test case
:param kwargs:
:key offset: int
unsupported due to reserve for auto pagination feature
:key limit: int
unsupported due to reserve for auto pagination feature
:key defects_filter: str
A single Defect ID (e.g. TR-1, 4291, etc.)
:key status_id: List[int] or comma-separated string
A comma-separated list of status IDs to filter by.
:return: DataFrame
"""
return DataFrame(self.get_results_for_case(run_id, case_id, **kwargs))

@auto_offset
Expand All @@ -492,13 +545,6 @@ def dataframe_from_test(self, test_id: int, **kwargs) -> DataFrame:
:param test_id:
The ID of the test
:param limit:
Number that sets the limit of test results to be shown on the response
(Optional parameter. The response size limit is 250 by default)
(requires TestRail 6.7 or later)
:param offset:
Number that sets the position where the response should start from
(Optional parameter) (requires TestRail 6.7 or later)
:param kwargs: filters
:key defects_filter: str
A single Defect ID (e.g. TR-1, 4291, etc.)
Expand All @@ -514,7 +560,7 @@ def dataframe_from_run(self, run_id: int, **kwargs) -> DataFrame:
Returns a list of test results for a test run.
This method will return up to all entries in the response array.
:param run_ids:
:param run_id:
The ID of the test run
:param kwargs: filters
:key created_after: int/datetime
Expand Down Expand Up @@ -551,18 +597,17 @@ def dataframe_from_runs(self, *run_ids: int, **kwargs) -> DataFrame:
A comma-separated list of status IDs to filter by.
:return: DataFrame
"""
dfs = [self.dataframe_from_run(run_id) for run_id in run_ids]
dfs = [self.dataframe_from_run(run_id, **kwargs) for run_id in run_ids]
return pd.concat(dfs).reset_index(drop=True) if dfs else None


def dataframe_from_milestone(self, project_id: int, *milestone_ids: int, **kwargs) -> DataFrame:
"""
Returns a list of test results from milestone(s) which contains run(s).
This method will return up to all entries in the response array.
:param project_id:
The ID of the project
:param *milestone_id:
:param milestone_ids:
The ID or IDs of the milestone(s)
:param kwargs: filters
:key created_after: int/datetime
Expand All @@ -582,23 +627,57 @@ def dataframe_from_milestone(self, project_id: int, *milestone_ids: int, **kwarg
df_runs = Runs(self._session).to_dataframe(
project_id=project_id, milestone_id=milestone_id)
_ = [results.append(self.dataframe_from_run(run_id, **kwargs))
for run_id in df_runs['id'].to_list()]
for run_id in df_runs['id'].to_list()]
return pd.concat(results).reset_index(drop=True) if results else None


class Suites(TR_Suites):
def to_dataframe(self, project_id: int):
def to_dataframe(self, project_id: int) -> DataFrame:
"""
Returns a list of test suites for a project in DataFrame.
:param project_id:
The ID of the project
:return: DataFrame
"""
return DataFrame(self.get_suites(project_id))

def get_suites_lookup(self, project_id: int) -> dict:
"""
A Suite dictionary lookup per project for suite_id to suite_name mapping.
:param project_id:
The ID of the project
:return: dict
Examples
--------
{
1: 'suite_name1',
2: 'suite_name2,
}
"""
df = self.to_dataframe(project_id)
return dict(zip(df['id'], df['name']))


class Statuses(TR_Statuses):
def to_dataframe(self) -> DataFrame:
"""
Returns a list of available test statuses in DataFrame
:return: DataFrame
"""
return DataFrame(self.get_statuses())

def get_statuses_lookup(self, column='name') -> dict:
"""
A dictionary lookup for all statuses.
:param column:
Refer https://www.gurock.com/testrail/docs/api/reference/statuses/ for list of columns.
Default is `name`, commonly switch with `label`.
:return:
"""
df = self.to_dataframe()
return dict(zip(df['id'], df[column]))
14 changes: 12 additions & 2 deletions tests/test_results.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,16 @@ def test_dataframe_from_run_when_has_no_record(api, host):

assert df.shape[0] == 0

@responses.activate
def test_dataframe_from_runs_when_has_no_record(api, host):
responses.add(
responses.GET,
'{}index.php?/api/v2/get_results_for_run/12&limit=250&offset=0'.format(host),
json=[], status=200)

df = api.results.dataframe_from_run(12)

assert df.shape[0] == 0

@responses.activate
def test_dataframe_from_milestone_when_has_record(api, host):
Expand All @@ -101,7 +111,7 @@ def test_dataframe_from_milestone_when_has_record(api, host):

print(responses.calls)

df = api.results.dataframe_from_milestone(9,1)
df = api.results.dataframe_from_milestone(9, 1)

assert df['status_id'][0] == 2
assert df.shape[0] == 1
assert df.shape[0] == 1

0 comments on commit e2c8655

Please sign in to comment.