Skip to content

Commit

Permalink
Add result type parameter to the 'jobs within time period' metric.
Browse files Browse the repository at this point in the history
Add a result type parameter to the 'jobs within time period' metric for the sources:
- Azure DevOps
- GitLab
- Jenkins

Closes #9926.
  • Loading branch information
fniessink committed Nov 19, 2024
1 parent dfc8abb commit c206d00
Show file tree
Hide file tree
Showing 23 changed files with 173 additions and 104 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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]:
Expand Down Expand Up @@ -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"],
),
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
10 changes: 5 additions & 5 deletions components/collector/src/source_collectors/gitlab/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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)
32 changes: 16 additions & 16 deletions components/collector/src/source_collectors/jenkins/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"])
],
Expand All @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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"])
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand All @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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",
},
{
Expand All @@ -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",
},
]
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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",
},
]
Expand Down
6 changes: 3 additions & 3 deletions components/collector/tests/source_collectors/gitlab/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand All @@ -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",
},
]

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down Expand Up @@ -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)
Expand All @@ -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",
},
],
Expand All @@ -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)
Expand Down Expand Up @@ -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)
5 changes: 4 additions & 1 deletion components/collector/tests/source_collectors/jenkins/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Loading

0 comments on commit c206d00

Please sign in to comment.