1# This file is part of Buildbot.  Buildbot is free software: you can
2# redistribute it and/or modify it under the terms of the GNU General Public
3# License as published by the Free Software Foundation, version 2.
4#
5# This program is distributed in the hope that it will be useful, but WITHOUT
6# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
7# FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
8# details.
9#
10# You should have received a copy of the GNU General Public License along with
11# this program; if not, write to the Free Software Foundation, Inc., 51
12# Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
13#
14# Copyright Buildbot Team Members
15
16from twisted.internet import defer
17from twisted.python import log
18
19from buildbot import config
20from buildbot import util
21from buildbot.process.results import CANCELLED
22from buildbot.process.results import EXCEPTION
23from buildbot.process.results import FAILURE
24from buildbot.process.results import SUCCESS
25from buildbot.process.results import WARNINGS
26from buildbot.process.results import statusToString
27
28
29class BuildStatusGeneratorMixin(util.ComparableMixin):
30
31    possible_modes = ("change", "failing", "passing", "problem", "warnings", "exception",
32                      "cancelled")
33
34    compare_attrs = ['mode', 'tags', 'builders', 'schedulers', 'branches', 'subject', 'add_logs',
35                     'add_patch']
36
37    def __init__(self, mode, tags, builders, schedulers, branches, subject, add_logs, add_patch):
38        self.mode = self._compute_shortcut_modes(mode)
39
40        self.tags = tags
41        self.builders = builders
42        self.schedulers = schedulers
43        self.branches = branches
44        self.subject = subject
45        self.add_logs = add_logs
46        self.add_patch = add_patch
47
48    def check(self):
49        self._verify_build_generator_mode(self.mode)
50
51        if self.subject is not None and '\n' in self.subject:
52            config.error('Newlines are not allowed in message subjects')
53
54        list_or_none_params = [
55            ('tags', self.tags),
56            ('builders', self.builders),
57            ('schedulers', self.schedulers),
58            ('branches', self.branches),
59        ]
60        for name, param in list_or_none_params:
61            self._verify_list_or_none_param(name, param)
62
63        # you should either limit on builders or tags, not both
64        if self.builders is not None and self.tags is not None:
65            config.error("Please specify only builders or tags to include - not both.")
66
67    def generate_name(self):
68        name = self.__class__.__name__
69        if self.tags is not None:
70            name += "_tags_" + "+".join(self.tags)
71        if self.builders is not None:
72            name += "_builders_" + "+".join(self.builders)
73        if self.schedulers is not None:
74            name += "_schedulers_" + "+".join(self.schedulers)
75        if self.branches is not None:
76            name += "_branches_" + "+".join(self.branches)
77        name += "_".join(self.mode)
78        return name
79
80    def _should_attach_log(self, log):
81        if isinstance(self.add_logs, bool):
82            return self.add_logs
83
84        if log['name'] in self.add_logs:
85            return True
86
87        long_name = "{}.{}".format(log['stepname'], log['name'])
88        if long_name in self.add_logs:
89            return True
90
91        return False
92
93    def is_message_needed_by_props(self, build):
94        # here is where we actually do something.
95        builder = build['builder']
96        scheduler = build['properties'].get('scheduler', [None])[0]
97        branch = build['properties'].get('branch', [None])[0]
98
99        if self.builders is not None and builder['name'] not in self.builders:
100            return False
101        if self.schedulers is not None and scheduler not in self.schedulers:
102            return False
103        if self.branches is not None and branch not in self.branches:
104            return False
105        if self.tags is not None and not self._matches_any_tag(builder['tags']):
106            return False
107        return True
108
109    def is_message_needed_by_results(self, build):
110        results = build['results']
111        if "change" in self.mode:
112            prev = build['prev_build']
113            if prev and prev['results'] != results:
114                return True
115        if "failing" in self.mode and results == FAILURE:
116            return True
117        if "passing" in self.mode and results == SUCCESS:
118            return True
119        if "problem" in self.mode and results == FAILURE:
120            prev = build['prev_build']
121            if prev and prev['results'] != FAILURE:
122                return True
123        if "warnings" in self.mode and results == WARNINGS:
124            return True
125        if "exception" in self.mode and results == EXCEPTION:
126            return True
127        if "cancelled" in self.mode and results == CANCELLED:
128            return True
129
130        return False
131
132    def _merge_msgtype(self, msgtype, new_msgtype):
133        if new_msgtype is None:
134            return msgtype, False
135        if msgtype is None:
136            return new_msgtype, True
137        if msgtype != new_msgtype:
138            log.msg(('{}: Incompatible message types for multiple builds ({} and {}). Ignoring'
139                     ).format(self, msgtype, new_msgtype))
140            return msgtype, False
141
142        return msgtype, True
143
144    def _merge_subject(self, subject, new_subject):
145        if subject is None and new_subject is not None:
146            return new_subject
147        return subject
148
149    def _merge_body(self, body, new_body):
150        if body is None:
151            return new_body, True
152        if new_body is None:
153            return body, True
154
155        if isinstance(body, str) and isinstance(new_body, str):
156            return body + new_body, True
157
158        if isinstance(body, list) and isinstance(new_body, list):
159            return body + new_body, True
160
161        log.msg(('{}: Incompatible message body types for multiple builds ({} and {}). Ignoring'
162                 ).format(self, type(body), type(new_body)))
163        return body, False
164
165    def _get_patches_for_build(self, build):
166        if not self.add_patch:
167            return []
168
169        ss_list = build['buildset']['sourcestamps']
170
171        return [ss['patch'] for ss in ss_list
172                if 'patch' in ss and ss['patch'] is not None]
173
174    @defer.inlineCallbacks
175    def build_message(self, formatter, master, reporter, build):
176        patches = self._get_patches_for_build(build)
177
178        logs = yield self._get_logs_for_build(master, build)
179
180        users = yield reporter.getResponsibleUsersForBuild(master, build['buildid'])
181
182        buildmsg = yield formatter.format_message_for_build(master, build, mode=self.mode,
183                                                            users=users)
184
185        results = build['results']
186
187        subject = buildmsg['subject']
188        if subject is None and self.subject is not None:
189            subject = self.subject % {'result': statusToString(results),
190                                      'projectName': master.config.title,
191                                      'title': master.config.title,
192                                      'builder': build['builder']['name']}
193
194        return {
195            'body': buildmsg['body'],
196            'subject': subject,
197            'type': buildmsg['type'],
198            'results': results,
199            'builds': [build],
200            'users': list(users),
201            'patches': patches,
202            'logs': logs
203        }
204
205    @defer.inlineCallbacks
206    def _get_logs_for_build(self, master, build):
207        if not self.add_logs:
208            return []
209
210        all_logs = []
211        steps = yield master.data.get(('builds', build['buildid'], "steps"))
212        for step in steps:
213            logs = yield master.data.get(("steps", step['stepid'], 'logs'))
214            for l in logs:
215                l['stepname'] = step['name']
216                if self._should_attach_log(l):
217                    l['content'] = yield master.data.get(("logs", l['logid'], 'contents'))
218                    all_logs.append(l)
219        return all_logs
220
221    def _verify_build_generator_mode(self, mode):
222        for m in self._compute_shortcut_modes(mode):
223            if m not in self.possible_modes:
224                if m == "all":
225                    config.error("mode 'all' is not valid in an iterator and must be "
226                                 "passed in as a separate string")
227                else:
228                    config.error("mode {} is not a valid mode".format(m))
229
230    def _verify_list_or_none_param(self, name, param):
231        if param is not None and not isinstance(param, list):
232            config.error("{} must be a list or None".format(name))
233
234    def _compute_shortcut_modes(self, mode):
235        if isinstance(mode, str):
236            if mode == "all":
237                mode = ("failing", "passing", "warnings",
238                        "exception", "cancelled")
239            elif mode == "warnings":
240                mode = ("failing", "warnings")
241            else:
242                mode = (mode,)
243        return mode
244
245    def _matches_any_tag(self, tags):
246        return self.tags and any(tag for tag in self.tags if tag in tags)
247