From e2c86550c5527f37ca079d9ad584f5c1c97fcd0a Mon Sep 17 00:00:00 2001 From: "Leow, Max" Date: Tue, 17 May 2022 16:19:10 +0800 Subject: [PATCH] fix defect in get all runs --- setup.py | 2 +- testrail_data/_category.py | 155 ++++++++++++++++++++++++++++--------- tests/test_results.py | 14 +++- 3 files changed, 130 insertions(+), 41 deletions(-) diff --git a/setup.py b/setup.py index 54e9729..064b4eb 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setuptools.setup( name="testrail-data", - version="0.0.8", + version="0.0.9", install_requires=[ "pandas", "testrail-api>=1.10", diff --git a/testrail_data/_category.py b/testrail_data/_category.py index 0200152..aa70d0e 100644 --- a/testrail_data/_category.py +++ b/testrail_data/_category.py @@ -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] @@ -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 `. + :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() @@ -88,7 +100,8 @@ 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]: @@ -96,7 +109,7 @@ def list_type(y): 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: @@ -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] @@ -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 """ @@ -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 @@ -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)) @@ -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: { : @@ -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'])) @@ -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 @@ -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.) @@ -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 @@ -551,10 +597,9 @@ 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). @@ -562,7 +607,7 @@ def dataframe_from_milestone(self, project_id: int, *milestone_ids: int, **kwarg :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 @@ -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])) diff --git a/tests/test_results.py b/tests/test_results.py index a594ab8..8c0a53b 100644 --- a/tests/test_results.py +++ b/tests/test_results.py @@ -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): @@ -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 \ No newline at end of file + assert df.shape[0] == 1