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