diff --git a/components/collector/src/source_collectors/azure_devops/base.py b/components/collector/src/source_collectors/azure_devops/base.py index 29b58892a3..de6d1e3ade 100644 --- a/components/collector/src/source_collectors/azure_devops/base.py +++ b/components/collector/src/source_collectors/azure_devops/base.py @@ -107,8 +107,8 @@ async def _api_url(self) -> URL: async def _api_pipelines_url(self, pipeline_id: int | None = None) -> URL: """Add the pipelines API, or runs API path if needed.""" extra_path = "" if not pipeline_id else f"/{pipeline_id}/runs" - # currently the pipelines api is not available in any version which is not a -preview version api_url = await SourceCollector._api_url(self) # noqa: SLF001 + # Use the oldest API version in which the endpoint is available: return URL(f"{api_url}/_apis/pipelines{extra_path}?api-version=6.0-preview.1") async def _active_pipelines(self) -> list[int]: @@ -148,6 +148,7 @@ async def _parse_pipeline_entities(pipeline_response: Response) -> Entities: pipeline=pipeline_name, url=pipeline_run["_links"]["web"]["href"], build_date=str(parse_datetime(pipeline_run["finishedDate"])), + build_result=pipeline_run.get("result", "unknown"), build_status=pipeline_run["state"], ), ) diff --git a/components/collector/src/source_collectors/azure_devops/job_runs_within_time_period.py b/components/collector/src/source_collectors/azure_devops/job_runs_within_time_period.py index 65d228d355..a76a9993d4 100644 --- a/components/collector/src/source_collectors/azure_devops/job_runs_within_time_period.py +++ b/components/collector/src/source_collectors/azure_devops/job_runs_within_time_period.py @@ -17,4 +17,5 @@ def _include_entity(self, entity: Entity) -> bool: return False build_age = days_ago(parse_datetime(entity["build_date"])) max_build_age = int(cast(str, self._parameter("lookback_days_pipeline_runs"))) - return build_age <= max_build_age + result_types = cast(list[str], self._parameter("result_type")) + return build_age <= max_build_age and entity["build_result"] in result_types diff --git a/components/collector/src/source_collectors/gitlab/base.py b/components/collector/src/source_collectors/gitlab/base.py index 5eb4a898b7..50b93b0718 100644 --- a/components/collector/src/source_collectors/gitlab/base.py +++ b/components/collector/src/source_collectors/gitlab/base.py @@ -88,14 +88,14 @@ async def _parse_entities(self, responses: SourceResponses) -> Entities: return Entities( [ Entity( - key=job["id"], - name=job["name"], - url=job["web_url"], - build_status=job["status"], branch=job["ref"], - stage=job["stage"], build_date=str(self._build_datetime(job).date()), build_datetime=self._build_datetime(job), + build_result=job["status"], + key=job["id"], + name=job["name"], + stage=job["stage"], + url=job["web_url"], ) for job in await self._jobs(responses) ], diff --git a/components/collector/src/source_collectors/gitlab/failed_jobs.py b/components/collector/src/source_collectors/gitlab/failed_jobs.py index 180f19beff..ee2337c60e 100644 --- a/components/collector/src/source_collectors/gitlab/failed_jobs.py +++ b/components/collector/src/source_collectors/gitlab/failed_jobs.py @@ -18,4 +18,4 @@ async def _api_url(self) -> URL: def _include_entity(self, entity: Entity) -> bool: """Return whether the job has failed.""" failure_types = list(self._parameter("failure_type")) - return super()._include_entity(entity) and entity["build_status"] in failure_types + return super()._include_entity(entity) and entity["build_result"] in failure_types diff --git a/components/collector/src/source_collectors/gitlab/job_runs_within_time_period.py b/components/collector/src/source_collectors/gitlab/job_runs_within_time_period.py index 8c54060dac..fafac198a5 100644 --- a/components/collector/src/source_collectors/gitlab/job_runs_within_time_period.py +++ b/components/collector/src/source_collectors/gitlab/job_runs_within_time_period.py @@ -22,6 +22,7 @@ async def _jobs(responses: SourceResponses) -> list[Job]: def _include_entity(self, entity: Entity) -> bool: """Return whether the job was run within the specified time period.""" + lookback_days = int(cast(str, self._parameter("lookback_days"))) + result_types = cast(list[str], self._parameter("result_type")) build_age = days_ago(entity["build_datetime"]) - within_time_period = build_age <= int(cast(str, self._parameter("lookback_days"))) - return within_time_period and super()._include_entity(entity) + return build_age <= lookback_days and entity["build_result"] in result_types and super()._include_entity(entity) diff --git a/components/collector/src/source_collectors/jenkins/base.py b/components/collector/src/source_collectors/jenkins/base.py index bc7285a736..2110361495 100644 --- a/components/collector/src/source_collectors/jenkins/base.py +++ b/components/collector/src/source_collectors/jenkins/base.py @@ -48,12 +48,12 @@ async def _parse_entities(self, responses: SourceResponses) -> Entities: return Entities( [ Entity( + build_date=self.__build_date(job), + build_datetime=self.__build_datetime(job), + build_result=self.__build_result(job), key=job["name"], name=job["name"], url=job["url"], - build_status=self._build_status(job), - build_date=self._build_date(job), - build_datetime=self._build_datetime(job), ) for job in self._jobs((await responses[0].json())["jobs"]) ], @@ -75,27 +75,27 @@ def _include_entity(self, entity: Entity) -> bool: return False return not match_string_or_regular_expression(entity["name"], self._parameter("jobs_to_ignore")) - def _build_datetime(self, job: Job) -> datetime | None: + def _builds(self, job: Job) -> list[Build]: + """Return the builds of the job.""" + return [build for build in job.get("builds", []) if self._include_build(build)] + + def _include_build(self, build: Build) -> bool: + """Return whether to include this build or not.""" + return True + + def __build_datetime(self, job: Job) -> datetime | None: """Return the datetime of the most recent build of the job.""" builds = self._builds(job) return datetime_from_timestamp(int(builds[0]["timestamp"])) if builds else None - def _build_date(self, job: Job) -> str: + def __build_date(self, job: Job) -> str: """Return the date of the most recent build of the job.""" - build_datetime = self._build_datetime(job) + build_datetime = self.__build_datetime(job) return str(build_datetime.date()) if build_datetime else "" - def _build_status(self, job: Job) -> str: - """Return the status of the most recent build of the job.""" + def __build_result(self, job: Job) -> str: + """Return the result of the most recent build of the job.""" for build in self._builds(job): if status := build.get("result"): return str(status).capitalize().replace("_", " ") return "Not built" - - def _builds(self, job: Job) -> list[Build]: - """Return the builds of the job.""" - return [build for build in job.get("builds", []) if self._include_build(build)] - - def _include_build(self, build: Build) -> bool: - """Return whether to include this build or not.""" - return True diff --git a/components/collector/src/source_collectors/jenkins/change_failure_rate.py b/components/collector/src/source_collectors/jenkins/change_failure_rate.py index 2aab90daf4..9a3ee4837b 100644 --- a/components/collector/src/source_collectors/jenkins/change_failure_rate.py +++ b/components/collector/src/source_collectors/jenkins/change_failure_rate.py @@ -23,12 +23,12 @@ async def _parse_entities(self, responses: SourceResponses) -> Entities: return Entities( [ Entity( + build_date=str(datetime_from_timestamp(build["timestamp"])), + build_datetime=datetime_from_timestamp(build["timestamp"]), + build_result=str(build.get("result", "")).capitalize().replace("_", " "), key=f"{job['name']}-{build['timestamp']}", name=job["name"], url=job["url"], - build_status=str(build.get("result", "")).capitalize().replace("_", " "), - build_date=str(datetime_from_timestamp(build["timestamp"])), - build_datetime=datetime_from_timestamp(build["timestamp"]), ) for build, job in self._builds_with_jobs((await responses[0].json())["jobs"]) ], diff --git a/components/collector/src/source_collectors/jenkins/failed_jobs.py b/components/collector/src/source_collectors/jenkins/failed_jobs.py index 735bc8351e..4682560e32 100644 --- a/components/collector/src/source_collectors/jenkins/failed_jobs.py +++ b/components/collector/src/source_collectors/jenkins/failed_jobs.py @@ -10,4 +10,4 @@ class JenkinsFailedJobs(JenkinsJobs): def _include_entity(self, entity: Entity) -> bool: """Extend to count the job if its build status matches the failure types selected by the user.""" - return super()._include_entity(entity) and entity["build_status"] in self._parameter("failure_type") + return super()._include_entity(entity) and entity["build_result"] in self._parameter("failure_type") diff --git a/components/collector/src/source_collectors/jenkins/job_runs_within_time_period.py b/components/collector/src/source_collectors/jenkins/job_runs_within_time_period.py index 8485213795..6b633a7af7 100644 --- a/components/collector/src/source_collectors/jenkins/job_runs_within_time_period.py +++ b/components/collector/src/source_collectors/jenkins/job_runs_within_time_period.py @@ -5,20 +5,17 @@ from collector_utilities.date_time import datetime_from_timestamp, days_ago from model import Entities, Entity, SourceMeasurement, SourceResponses -from .base import Build, JenkinsJobs, Job +from .base import Build, JenkinsJobs class JenkinsJobRunsWithinTimePeriod(JenkinsJobs): """Collector class to measure the number of Jenkins jobs run within a specified time period.""" - def _include_build(self, build: Build) -> bool: - """Return whether to include this build or not.""" - build_datetime = datetime_from_timestamp(int(build["timestamp"])) - return days_ago(build_datetime) <= int(cast(str, self._parameter("lookback_days"))) - - def _builds_within_timeperiod(self, job: Job) -> int: - """Return the number of job builds within time period.""" - return len(super()._builds(job)) + async def _parse_source_responses(self, responses: SourceResponses) -> SourceMeasurement: + """Count the sum of jobs ran.""" + included_entities = [entity for entity in await self._parse_entities(responses) if self._include_entity(entity)] + job_runs = [int(entity["build_count"]) for entity in included_entities] + return SourceMeasurement(value=str(sum(job_runs)), entities=Entities(included_entities)) async def _parse_entities(self, responses: SourceResponses) -> Entities: """Override to parse the jobs.""" @@ -28,14 +25,15 @@ async def _parse_entities(self, responses: SourceResponses) -> Entities: key=job["name"], name=job["name"], url=job["url"], - build_count=self._builds_within_timeperiod(job), + build_count=str(len(self._builds(job))), ) for job in self._jobs((await responses[0].json())["jobs"]) ], ) - async def _parse_source_responses(self, responses: SourceResponses) -> SourceMeasurement: - """Count the sum of jobs ran.""" - included_entities = [entity for entity in await self._parse_entities(responses) if self._include_entity(entity)] - job_runs = [job["build_count"] for job in included_entities] - return SourceMeasurement(value=str(sum(job_runs)), entities=Entities(included_entities)) + def _include_build(self, build: Build) -> bool: + """Return whether to include this build or not.""" + result_types = [result_type.lower() for result_type in self._parameter("result_type")] + lookback_days = int(cast(str, self._parameter("lookback_days"))) + build_datetime = datetime_from_timestamp(int(build["timestamp"])) + return days_ago(build_datetime) <= lookback_days and build["result"].lower() in result_types diff --git a/components/collector/tests/metric_collectors/test_change_failure_rate.py b/components/collector/tests/metric_collectors/test_change_failure_rate.py index 6fec41bd70..f7e20b0648 100644 --- a/components/collector/tests/metric_collectors/test_change_failure_rate.py +++ b/components/collector/tests/metric_collectors/test_change_failure_rate.py @@ -98,7 +98,7 @@ def gitlab_entity(self, job: str = "job1"): "branch": "main", "build_date": self.DEPLOY_DT.astimezone(tzutc()).date().isoformat(), "build_datetime": self.DEPLOY_DT, - "build_status": "failed", + "build_result": "failed", "failed": True, "key": "1", "name": job, diff --git a/components/collector/tests/source_collectors/azure_devops/base.py b/components/collector/tests/source_collectors/azure_devops/base.py index af3ce87a2a..3c0a2c2b08 100644 --- a/components/collector/tests/source_collectors/azure_devops/base.py +++ b/components/collector/tests/source_collectors/azure_devops/base.py @@ -130,6 +130,7 @@ def setUp(self): "key": f"{self.test_pipeline['id']}-20191015_1", # safe_entity_key "url": f"{self.url}/_build/results?buildId=1", "build_date": "2019-10-15 12:24:10.190586+00:00", + "build_result": "succeeded", "build_status": "completed", }, { @@ -138,6 +139,7 @@ def setUp(self): "key": f"{self.test_pipeline['id']}-20191015_2", # safe_entity_key "url": f"{self.url}/_build/results?buildId=2", "build_date": "2019-10-15 12:34:10.190586+00:00", + "build_result": "succeeded", "build_status": "completed", }, ] diff --git a/components/collector/tests/source_collectors/azure_devops/test_job_runs_within_time_period.py b/components/collector/tests/source_collectors/azure_devops/test_job_runs_within_time_period.py index 75fbfbbf73..ffb9670109 100644 --- a/components/collector/tests/source_collectors/azure_devops/test_job_runs_within_time_period.py +++ b/components/collector/tests/source_collectors/azure_devops/test_job_runs_within_time_period.py @@ -33,6 +33,19 @@ async def test_pipeline_runs_jobs_exclude(self): self.assert_measurement(response, value="0", entities=[]) + async def test_pipeline_runs_jobs_exclude_by_result_type(self): + """Test that the pipeline runs are filtered by result type.""" + self.set_source_parameter("lookback_days_pipeline_runs", "424242") + self.set_source_parameter("result_type", ["succeeded"]) + + response = await self.collect( + get_request_json_return_value=self.pipeline_runs, + get_request_json_side_effect=[self.pipelines, self.pipeline_runs], + ) + + expected_entities = [entity for entity in self.expected_entities if entity.get("build_result") == "succeeded"] + self.assert_measurement(response, value=str(len(expected_entities)), entities=expected_entities) + async def test_pipeline_runs_jobs_empty_include(self): """Test that counting pipeline runs filtered by a not-matching name include, works.""" self.set_source_parameter("lookback_days_pipeline_runs", "424242") @@ -92,6 +105,7 @@ async def test_pipeline_runs_lookback_days(self): "key": f"{self.test_pipeline['id']}-{build_date_str}_1", # safe_entity_key "url": f"{self.url}/_build/results?buildId=6", "build_date": str(now_dt), + "build_result": "succeeded", "build_status": "completed", }, ] diff --git a/components/collector/tests/source_collectors/gitlab/base.py b/components/collector/tests/source_collectors/gitlab/base.py index 516d6defd6..8c72f5bc7d 100644 --- a/components/collector/tests/source_collectors/gitlab/base.py +++ b/components/collector/tests/source_collectors/gitlab/base.py @@ -23,7 +23,7 @@ def setUp(self): self.gitlab_jobs_json = [ { "id": "1", - "status": "failed", + "status": "skipped", "name": "job1", "stage": "stage", "created_at": "2019-03-31T19:40:39.927Z", @@ -50,7 +50,7 @@ def setUp(self): "url": "https://gitlab/job1", "build_date": "2019-03-31", "build_datetime": datetime(2019, 3, 31, 19, 40, 39, 927000, tzinfo=tzutc()), - "build_status": "failed", + "build_result": "skipped", }, { "key": "2", @@ -60,7 +60,7 @@ def setUp(self): "url": "https://gitlab/job2", "build_date": "2019-03-31", "build_datetime": datetime(2019, 3, 31, 19, 40, 39, 927000, tzinfo=tzutc()), - "build_status": "failed", + "build_result": "failed", }, ] diff --git a/components/collector/tests/source_collectors/gitlab/test_job_runs_within_time_period.py b/components/collector/tests/source_collectors/gitlab/test_job_runs_within_time_period.py index 7ae927b40f..97d2b24ab8 100644 --- a/components/collector/tests/source_collectors/gitlab/test_job_runs_within_time_period.py +++ b/components/collector/tests/source_collectors/gitlab/test_job_runs_within_time_period.py @@ -17,6 +17,14 @@ class GitLabJobRunsWithinTimePeriodTest(GitLabJobsTestCase): _job4_url = "https://gitlab/job4" _job5_url = "https://gitlab/job5" + async def test_result_type_filter(self): + """Test that the jobs can be filtered by result type.""" + self.set_source_parameter("lookback_days", "100000") + self.set_source_parameter("result_type", ["skipped"]) + response = await self.collect(get_request_json_return_value=self.gitlab_jobs_json) + entities = [entity for entity in self.expected_entities if entity["build_result"] == "skipped"] + self.assert_measurement(response, value=str(len(entities)), entities=entities, landing_url=self.LANDING_URL) + async def test_job_lookback_days(self): """Test that the job lookback_days are verified.""" just_now = datetime.now(tz=tzutc()) @@ -48,14 +56,14 @@ async def test_job_lookback_days(self): response = await self.collect(get_request_json_return_value=self.gitlab_jobs_json) expected_entities = [ { - "key": "3", - "name": "job3", - "url": self._job3_url, - "build_status": "failed", "branch": "main", - "stage": "stage", "build_date": str(just_now.date()), "build_datetime": just_now, + "build_result": "failed", + "key": "3", + "name": "job3", + "stage": "stage", + "url": self._job3_url, }, ] self.assert_measurement(response, value="1", entities=expected_entities, landing_url=self.LANDING_URL) @@ -79,10 +87,10 @@ async def test_jobs_not_deduplicated(self): { "id": "4", "status": "failed", - "name": "job3", + "name": "job4", "stage": "stage", "created_at": yesterday.isoformat(), - "web_url": self._job3_url, + "web_url": self._job4_url, "ref": "main", }, ], @@ -91,24 +99,24 @@ async def test_jobs_not_deduplicated(self): response = await self.collect(get_request_json_return_value=self.gitlab_jobs_json) expected_entities = [ { + "build_date": str(just_now.date()), + "build_datetime": just_now, + "branch": "main", + "build_result": "failed", "key": "3", "name": "job3", - "url": self._job3_url, - "build_status": "failed", - "branch": "main", "stage": "stage", - "build_date": str(just_now.date()), - "build_datetime": just_now, + "url": self._job3_url, }, { - "key": "4", - "name": "job3", - "url": self._job3_url, - "build_status": "failed", "branch": "main", - "stage": "stage", "build_date": str(yesterday.date()), "build_datetime": just_now - timedelta(days=1), + "build_result": "failed", + "key": "4", + "name": "job4", + "stage": "stage", + "url": self._job4_url, }, ] self.assert_measurement(response, value="2", entities=expected_entities, landing_url=self.LANDING_URL) @@ -154,24 +162,24 @@ async def test_job_lookback_days_on_edge(self): response = await self.collect(get_request_json_return_value=self.gitlab_jobs_json) expected_entities = [ { - "key": "3", - "name": "job3", - "url": self._job3_url, - "build_status": "failed", "branch": "main", - "stage": "stage", "build_date": str(just_now.date()), "build_datetime": just_now, + "build_result": "failed", + "key": "3", + "name": "job3", + "stage": "stage", + "url": self._job3_url, }, { - "key": "4", - "name": "job4", - "url": self._job4_url, - "build_status": "failed", "branch": "main", - "stage": "stage", "build_date": str(just_before_cutoff.date()), "build_datetime": just_before_cutoff, + "build_result": "failed", + "key": "4", + "name": "job4", + "stage": "stage", + "url": self._job4_url, }, ] self.assert_measurement(response, value="2", entities=expected_entities, landing_url=self.LANDING_URL) diff --git a/components/collector/tests/source_collectors/jenkins/base.py b/components/collector/tests/source_collectors/jenkins/base.py index 31e96e3c1b..03efdacc76 100644 --- a/components/collector/tests/source_collectors/jenkins/base.py +++ b/components/collector/tests/source_collectors/jenkins/base.py @@ -12,6 +12,9 @@ def setUp(self): """Extend to set up a Jenkins with a build.""" super().setUp() self.set_source_parameter("failure_type", ["Failure"]) - self.builds = [{"result": "FAILURE", "timestamp": 1552686540953}] + self.builds = [ + {"result": "FAILURE", "timestamp": 1552686540953}, + {"result": "SUCCESS", "timestamp": 1552686531953}, + ] self.job_url = "https://job" self.job2_url = "https://job2" diff --git a/components/collector/tests/source_collectors/jenkins/test_failed_jobs.py b/components/collector/tests/source_collectors/jenkins/test_failed_jobs.py index d4b612c736..eccf83bf62 100644 --- a/components/collector/tests/source_collectors/jenkins/test_failed_jobs.py +++ b/components/collector/tests/source_collectors/jenkins/test_failed_jobs.py @@ -38,7 +38,7 @@ async def test_failed_jobs(self): { "build_date": "2019-03-15", "build_datetime": datetime_from_timestamp(self.builds[0]["timestamp"]), - "build_status": "Failure", + "build_result": "Failure", "key": "job", "name": "job", "url": self.job_url, @@ -54,7 +54,7 @@ async def test_include_jobs(self): { "build_date": "2019-03-15", "build_datetime": datetime_from_timestamp(self.builds[0]["timestamp"]), - "build_status": "Failure", + "build_result": "Failure", "key": "job", "name": "job", "url": self.job_url, @@ -70,7 +70,7 @@ async def test_include_jobs_by_regular_expression(self): { "build_date": "2019-03-15", "build_datetime": datetime_from_timestamp(self.builds[0]["timestamp"]), - "build_status": "Failure", + "build_result": "Failure", "key": "job2", "name": "job2", "url": self.job2_url, @@ -86,7 +86,7 @@ async def test_ignore_jobs(self): { "build_date": "2019-03-15", "build_datetime": datetime_from_timestamp(self.builds[0]["timestamp"]), - "build_status": "Failure", + "build_result": "Failure", "key": "job", "name": "job", "url": self.job_url, @@ -102,7 +102,7 @@ async def test_ignore_jobs_by_regular_expression(self): { "build_date": "2019-03-15", "build_datetime": datetime_from_timestamp(self.builds[0]["timestamp"]), - "build_status": "Failure", + "build_result": "Failure", "key": "job", "name": "job", "url": self.job_url, @@ -122,7 +122,7 @@ async def test_include_and_ignore_jobs(self): { "build_date": "2019-03-15", "build_datetime": datetime_from_timestamp(self.builds[0]["timestamp"]), - "build_status": "Failure", + "build_result": "Failure", "key": "job3", "name": "job3", "url": "https://job3", diff --git a/components/collector/tests/source_collectors/jenkins/test_job_runs_within_time_period.py b/components/collector/tests/source_collectors/jenkins/test_job_runs_within_time_period.py index f78561bd3b..2f84976df1 100644 --- a/components/collector/tests/source_collectors/jenkins/test_job_runs_within_time_period.py +++ b/components/collector/tests/source_collectors/jenkins/test_job_runs_within_time_period.py @@ -12,6 +12,27 @@ class JenkinsJobRunsWithinTimePeriodTest(JenkinsTestCase): METRIC_TYPE = "job_runs_within_time_period" + async def test_all_builds(self): + """Test that all builds are counted if no filtering is done.""" + self.set_source_parameter("lookback_days", "100000") + jenkins_json = { + "jobs": [{"name": "job", "url": self.job_url, "buildable": True, "color": "blue", "builds": self.builds}], + } + response = await self.collect(get_request_json_return_value=jenkins_json) + expected_entities = [{"build_count": "2", "key": "job", "name": "job", "url": self.job_url}] + self.assert_measurement(response, value="2", entities=expected_entities) + + async def test_filter_by_result_type(self): + """Test that the builds can be filtered by result type.""" + self.set_source_parameter("lookback_days", "100000") + self.set_source_parameter("result_type", ["Failure"]) + jenkins_json = { + "jobs": [{"name": "job", "url": self.job_url, "buildable": True, "color": "blue", "builds": self.builds}], + } + response = await self.collect(get_request_json_return_value=jenkins_json) + expected_entities = [{"build_count": "1", "key": "job", "name": "job", "url": self.job_url}] + self.assert_measurement(response, value="1", entities=expected_entities) + async def test_job_lookback_days(self): """Test that the build lookback_days are verified.""" self.set_source_parameter("lookback_days", "3") @@ -31,5 +52,5 @@ async def test_job_lookback_days(self): } response = await self.collect(get_request_json_return_value=jenkins_json) - expected_entities = [{"build_count": 1, "key": "job", "name": "job", "url": self.job_url}] + expected_entities = [{"build_count": "1", "key": "job", "name": "job", "url": self.job_url}] self.assert_measurement(response, value="1", entities=expected_entities) diff --git a/components/collector/tests/source_collectors/jenkins/test_source_up_to_dateness.py b/components/collector/tests/source_collectors/jenkins/test_source_up_to_dateness.py index 61d5b0853f..85478913bf 100644 --- a/components/collector/tests/source_collectors/jenkins/test_source_up_to_dateness.py +++ b/components/collector/tests/source_collectors/jenkins/test_source_up_to_dateness.py @@ -22,7 +22,7 @@ async def test_job(self): { "build_date": "2019-03-15", "build_datetime": expected_dt, - "build_status": "Failure", + "build_result": "Failure", "key": "job", "name": "job", "url": self.job_url, @@ -38,7 +38,7 @@ async def test_job_without_builds(self): { "build_date": "", "build_datetime": None, - "build_status": "Not built", + "build_result": "Not built", "key": "job", "name": "job", "url": self.job_url, @@ -48,9 +48,8 @@ async def test_job_without_builds(self): async def test_ignore_failed_builds(self): """Test that failed builds can be ignored.""" - success_dt = 1553686540953 + success_dt = 1552686531953 self.set_source_parameter("result_type", ["Success"]) - self.builds.append({"result": "SUCCESS", "timestamp": success_dt}) jenkins_json = { "jobs": [{"name": "job", "url": self.job_url, "buildable": True, "color": "red", "builds": self.builds}], } @@ -58,9 +57,9 @@ async def test_ignore_failed_builds(self): expected_dt = datetime_from_timestamp(success_dt) expected_entities = [ { - "build_date": "2019-03-27", + "build_date": "2019-03-15", "build_datetime": expected_dt, - "build_status": "Success", + "build_result": "Success", "key": "job", "name": "job", "url": self.job_url, diff --git a/components/shared_code/src/shared_data_model/parameters.py b/components/shared_code/src/shared_data_model/parameters.py index d185618956..2c70a4832d 100644 --- a/components/shared_code/src/shared_data_model/parameters.py +++ b/components/shared_code/src/shared_data_model/parameters.py @@ -196,6 +196,16 @@ class FailureType(MultipleChoiceParameter): metrics: list[str] = ["failed_jobs"] +class ResultType(MultipleChoiceParameter): + """Build result type parameter.""" + + name: str = "Build result types" + short_name: str = "result types" + help: str = "Limit which build result types to include." + placeholder: str = "all result types" + metrics: list[str] = ["job_runs_within_time_period"] + + def access_parameters( metrics: list[str], include: dict[str, bool] | None = None, diff --git a/components/shared_code/src/shared_data_model/sources/azure_devops.py b/components/shared_code/src/shared_data_model/sources/azure_devops.py index 6da9e7fac3..70d9539818 100644 --- a/components/shared_code/src/shared_data_model/sources/azure_devops.py +++ b/components/shared_code/src/shared_data_model/sources/azure_devops.py @@ -14,6 +14,7 @@ MultipleChoiceParameter, MultipleChoiceWithAdditionParameter, PrivateToken, + ResultType, StringParameter, TargetBranchesToInclude, TestResult, @@ -46,11 +47,15 @@ EntityAttribute( name="Status of most recent build", key="build_status", + ), + # Result types conform https://learn.microsoft.com/en-us/rest/api/azure/devops/pipelines/runs/list?view=azure-devops-rest-6.0#runresult + EntityAttribute( + name="Result of most recent build", + key="build_result", color={ "succeeded": Color.POSITIVE, "failed": Color.NEGATIVE, "canceled": Color.ACTIVE, - "partiallySucceeded": Color.WARNING, }, ), EntityAttribute(name="Date of most recent build", key="build_date", type=EntityAttributeType.DATE), @@ -193,6 +198,8 @@ values=["canceled", "failed", "no result", "partially succeeded"], api_values={"no result": "none", "partially succeeded": "partiallySucceeded"}, ), + # Result types conform https://learn.microsoft.com/en-us/rest/api/azure/devops/pipelines/runs/list?view=azure-devops-rest-6.0#runresult + "result_type": ResultType(values=["canceled", "failed", "succeeded", "unknown"]), "merge_request_state": MergeRequestState( values=["abandoned", "active", "completed", "not set"], api_values={"not set": "notSet"}, diff --git a/components/shared_code/src/shared_data_model/sources/gitlab.py b/components/shared_code/src/shared_data_model/sources/gitlab.py index 11b5a634a7..c04c8ff571 100644 --- a/components/shared_code/src/shared_data_model/sources/gitlab.py +++ b/components/shared_code/src/shared_data_model/sources/gitlab.py @@ -15,6 +15,7 @@ MultipleChoiceParameter, MultipleChoiceWithAdditionParameter, PrivateToken, + ResultType, StringParameter, TargetBranchesToInclude, Upvotes, @@ -39,9 +40,14 @@ EntityAttribute(name="Job stage", key="stage"), EntityAttribute(name="Branch or tag", key="branch"), EntityAttribute( - name="Status of most recent build", - key="build_status", - color={"canceled": Color.ACTIVE, "failed": Color.NEGATIVE, "success": Color.POSITIVE}, + name="Result of most recent build", + key="build_result", + color={ + "canceled": Color.ACTIVE, + "failed": Color.NEGATIVE, + "skipped": Color.WARNING, + "success": Color.POSITIVE, + }, ), EntityAttribute(name="Date of most recent build", key="build_date", type=EntityAttributeType.DATE), ], @@ -146,6 +152,7 @@ metrics=["unused_jobs"], ), "failure_type": FailureType(values=["canceled", "failed", "skipped"]), + "result_type": ResultType(values=["canceled", "failed", "skipped", "success"]), "jobs_to_ignore": MultipleChoiceWithAdditionParameter( name="Jobs to ignore (regular expressions or job names)", short_name="jobs to ignore", diff --git a/components/shared_code/src/shared_data_model/sources/jenkins.py b/components/shared_code/src/shared_data_model/sources/jenkins.py index ec7499d233..1073e9cf66 100644 --- a/components/shared_code/src/shared_data_model/sources/jenkins.py +++ b/components/shared_code/src/shared_data_model/sources/jenkins.py @@ -9,8 +9,8 @@ Branches, Days, FailureType, - MultipleChoiceParameter, MultipleChoiceWithAdditionParameter, + ResultType, StringParameter, TestResult, access_parameters, @@ -54,8 +54,8 @@ def jenkins_access_parameters(*args, **kwargs) -> dict[str, Parameter]: attributes=[ EntityAttribute(name=_JOB_ENTITY_NAME_NAME, key="name", url="url"), EntityAttribute( - name="Status of most recent build", - key="build_status", + name="Result of most recent build", + key="build_result", color={ "Success": Color.POSITIVE, "Failure": Color.NEGATIVE, @@ -136,13 +136,9 @@ def jenkins_access_parameters(*args, **kwargs) -> dict[str, Parameter]: mandatory=True, metrics=["pipeline_duration"], ), - "result_type": MultipleChoiceParameter( - name="Build result types", - short_name="result types", - help="Limit which build result types to include.", - placeholder="all result types", + "result_type": ResultType( values=["Aborted", "Failure", "Not built", "Success", "Unstable"], - metrics=["pipeline_duration", "source_up_to_dateness"], + metrics=["job_runs_within_time_period", "pipeline_duration", "source_up_to_dateness"], ), "failure_type": FailureType(values=["Aborted", "Failure", "Not built", "Unstable"]), **jenkins_access_parameters( @@ -161,7 +157,7 @@ def jenkins_access_parameters(*args, **kwargs) -> dict[str, Parameter]: name="deployment", attributes=[ EntityAttribute(name=_JOB_ENTITY_NAME_NAME, key="name", url="url"), - EntityAttribute(name="Status of most recent build", key="build_status"), + EntityAttribute(name="Result of most recent build", key="build_result"), EntityAttribute(name="Date of most recent build", key="build_date", type=EntityAttributeType.DATE), ], ), diff --git a/docs/src/changelog.md b/docs/src/changelog.md index dbe7882858..1a6e697517 100644 --- a/docs/src/changelog.md +++ b/docs/src/changelog.md @@ -17,6 +17,7 @@ If your currently installed *Quality-time* version is not the latest version, pl ### Added - Allow for measuring the time since the last analysis date of a Bill-of-Materials (BOM) in Dependency-Track using the new 'project event type' parameter of the 'source up-to-dateness' metric. Closes [#9764](https://github.com/ICTU/quality-time/issues/9764). +- Add a result type parameter to the 'jobs within time period' metric to allow for filtering jobs by result type (success, failed, skipped, etc.). Closes [#9926](https://github.com/ICTU/quality-time/issues/9926). - Allow for using Visual Studio test reports (.trx) as source for the metrics 'tests', 'test cases', and 'source up-to-dateness'. Closes [#10009](https://github.com/ICTU/quality-time/issues/10009). ## v5.18.0 - 2024-11-06