aboutsummaryrefslogtreecommitdiffstats
path: root/binarysizetest.py
blob: 09ffe1fdc52a8d1a4b8d1fa947b071e7790e6a98 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
# Copyright (C) 2024 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
""" Module contains logic for updating binary sizes into database """
import os
import json
from datetime import datetime
import urllib.request
import tarfile
import tempfile
from pathlib import Path
from urllib.parse import urlparse
import database
import coin_api


EMAIL_ALERT_TOPIC = "Binary size increased over threshold"


class BinarySizeTest():
    """ Class for fetching, unpacking and pushing binary size results into database """
    def __init__(
            self,
            tests_json: str,
            binary_size_database: database.Database):

        with open(tests_json, 'r', encoding="utf-8") as f:
            test_cases = json.load(f)
        self.binary_size_database = binary_size_database
        self.branch = test_cases['branch']
        self.series = test_cases['series']
        self.integration = test_cases['integration']
        self.builds_to_check = test_cases['builds_to_check']
        self.coin_id = test_cases['coin_id']

    def email_content(self, coin_task_id, start_date, end_date, shas):
        """ Composes email content """
        email_body = (
            f"Hi, This alert comes from qt-binary-size-bot.\n\n"
            f"The bot is monitoring {self.coin_id} for {self.integration} integration in {self.branch} branch "
            f"with the following configuration:\n{self.builds_to_check}\n"
            f"You can find the histogram at: http://testresults.qt.io/grafana/goto/JDaMrUaSR?orgId=1\n"
            f"The failed build used artifacts from COIN job id: "
            f"https://coin.ci.qt.io/coin/integration/{self.integration}/tasks/{coin_task_id}\n"
            f"It's possible that the issue was introduced between {start_date} and {end_date} UTC.\n"
            f"Related commits: {shas}\n\n"
            f"For now, this alert is just an informal notification.\n"
            f"If you could add functionality behind existing or new Qt feature flags, please check your commit.\n"
            f"For more information, feel free to ask the person who is CC'd."
        )

        return EMAIL_ALERT_TOPIC, email_body

    def matches(self, project, branch) -> bool:
        """ Check if integration and branch matches with this instance """
        return project == self.integration and branch == self.branch

    def run(self, coin_task_id: str, coin_task_datetime: datetime, commits) -> bool:
        """ Executes class logic """
        send_email = False
        for build in self.builds_to_check:
            with tempfile.TemporaryDirectory() as tmpdirname:
                tempdir = Path(tmpdirname)
                self._fetch_and_unpack_tarball(coin_task_id, build['name'], tempdir)
                query = ' OR '.join([f'commit:{commit}' for commit in commits])
                url = f"https://codereview.qt-project.org/q/{query}"
                for test_param in build['size_comparision']:
                    send_email |= self._size_comparision_test(
                        url,
                        coin_task_datetime,
                        tempdir,
                        test_param["file"],
                        test_param["threshold"])

        return send_email

    def _fetch_and_unpack_tarball(
            self, coin_task_id: str, build_name: str, target_directory: str) -> None:
        artifacts_url = coin_api.get_artifacts_url(
            coin_task_id,
            build_name,
            self.branch,
            self.coin_id)

        if not os.path.exists(target_directory):
            os.makedirs(target_directory)

        print(f"Fetching {artifacts_url}")
        artifacts_filename = os.path.basename(urlparse(artifacts_url).path)
        artifacts_filename = os.path.join(target_directory, artifacts_filename)
        urllib.request.urlretrieve(artifacts_url, artifacts_filename)

        print(f"Unpacking {artifacts_filename}")
        with tarfile.open(artifacts_filename) as tarball:
            tarball.extractall(target_directory)

    def _size_comparision_test(
            self,
            commit_url: str,
            coin_task_datetime: datetime,
            path: str,
            file: str,
            threshold_percentage: float) -> bool:
        # pylint: disable=R0913
        send_email = False
        file_with_path = os.path.join(path, 'install', file)
        previous_value = self.binary_size_database.pull(self.series, file)
        new_value = os.stat(os.path.expanduser(file_with_path)).st_size

        if previous_value == 0:
            print(f"Pushing initial value for {file}: {new_value}")
        else:
            print(f"Pushing value for {file}: {new_value} (previous value was:{previous_value})")
            threshold_value = previous_value * (threshold_percentage + 1)
            if new_value > threshold_value:
                send_email = True

        self.binary_size_database.push(self.series, commit_url, coin_task_datetime, file, new_value)
        return send_email