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 16import copy 17import json 18 19from twisted.internet import defer 20 21from buildbot.test.fakedb.base import FakeDBComponent 22from buildbot.test.fakedb.row import Row 23from buildbot.util import datetime2epoch 24from buildbot.util import epoch2datetime 25 26 27class Change(Row): 28 table = "changes" 29 30 lists = ('files', 'uids') 31 dicts = ('properties',) 32 id_column = 'changeid' 33 34 def __init__(self, changeid=None, author='frank', committer='steve', 35 comments='test change', branch='master', revision='abcd', 36 revlink='http://vc/abcd', when_timestamp=1200000, category='cat', 37 repository='repo', codebase='', project='proj', sourcestampid=92, 38 parent_changeids=None): 39 super().__init__(changeid=changeid, author=author, committer=committer, comments=comments, 40 branch=branch, revision=revision, revlink=revlink, 41 when_timestamp=when_timestamp, category=category, repository=repository, 42 codebase=codebase, project=project, sourcestampid=sourcestampid, 43 parent_changeids=parent_changeids) 44 45 46class ChangeFile(Row): 47 table = "change_files" 48 49 foreignKeys = ('changeid',) 50 required_columns = ('changeid',) 51 52 def __init__(self, changeid=None, filename=None): 53 super().__init__(changeid=changeid, filename=filename) 54 55 56class ChangeProperty(Row): 57 table = "change_properties" 58 59 foreignKeys = ('changeid',) 60 required_columns = ('changeid',) 61 62 def __init__(self, changeid=None, property_name=None, property_value=None): 63 super().__init__(changeid=changeid, property_name=property_name, 64 property_value=property_value) 65 66 67class ChangeUser(Row): 68 table = "change_users" 69 70 foreignKeys = ('changeid',) 71 required_columns = ('changeid',) 72 73 def __init__(self, changeid=None, uid=None): 74 super().__init__(changeid=changeid, uid=uid) 75 76 77class FakeChangesComponent(FakeDBComponent): 78 79 def setUp(self): 80 self.changes = {} 81 82 def insertTestData(self, rows): 83 for row in rows: 84 if isinstance(row, Change): 85 # copy this since we'll be modifying it (e.g., adding files) 86 ch = self.changes[row.changeid] = copy.deepcopy(row.values) 87 ch['files'] = [] 88 ch['properties'] = {} 89 ch['uids'] = [] 90 91 elif isinstance(row, ChangeFile): 92 ch = self.changes[row.changeid] 93 ch['files'].append(row.filename) 94 95 elif isinstance(row, ChangeProperty): 96 ch = self.changes[row.changeid] 97 n, vs = row.property_name, row.property_value 98 v, s = json.loads(vs) 99 ch['properties'][n] = (v, s) 100 101 elif isinstance(row, ChangeUser): 102 ch = self.changes[row.changeid] 103 ch['uids'].append(row.uid) 104 105 # component methods 106 107 @defer.inlineCallbacks 108 def addChange(self, author=None, committer=None, files=None, comments=None, is_dir=None, 109 revision=None, when_timestamp=None, branch=None, 110 category=None, revlink='', properties=None, repository='', 111 codebase='', project='', uid=None): 112 if properties is None: 113 properties = {} 114 115 if self.changes: 116 changeid = max(list(self.changes)) + 1 117 else: 118 changeid = 500 119 120 ssid = yield self.db.sourcestamps.findSourceStampId( 121 revision=revision, branch=branch, repository=repository, 122 codebase=codebase, project=project) 123 124 parent_changeids = yield self.getParentChangeIds(branch, repository, project, codebase) 125 126 self.changes[changeid] = ch = dict( 127 changeid=changeid, 128 parent_changeids=parent_changeids, 129 author=author, 130 committer=committer, 131 comments=comments, 132 revision=revision, 133 when_timestamp=datetime2epoch(when_timestamp), 134 branch=branch, 135 category=category, 136 revlink=revlink, 137 repository=repository, 138 project=project, 139 codebase=codebase, 140 uids=[], 141 files=files, 142 properties=properties, 143 sourcestampid=ssid) 144 145 if uid: 146 ch['uids'].append(uid) 147 148 return changeid 149 150 def getLatestChangeid(self): 151 if self.changes: 152 return defer.succeed(max(list(self.changes))) 153 return defer.succeed(None) 154 155 def getParentChangeIds(self, branch, repository, project, codebase): 156 if self.changes: 157 for changeid, change in self.changes.items(): 158 if (change['branch'] == branch and 159 change['repository'] == repository and 160 change['project'] == project and 161 change['codebase'] == codebase): 162 return defer.succeed([change['changeid']]) 163 return defer.succeed([]) 164 165 def getChange(self, key, no_cache=False): 166 try: 167 row = self.changes[key] 168 except KeyError: 169 return defer.succeed(None) 170 171 return defer.succeed(self._chdict(row)) 172 173 def getChangeUids(self, changeid): 174 try: 175 ch_uids = self.changes[changeid]['uids'] 176 except KeyError: 177 ch_uids = [] 178 return defer.succeed(ch_uids) 179 180 def getChanges(self, resultSpec=None): 181 if resultSpec is not None and resultSpec.limit is not None: 182 ids = sorted(self.changes.keys()) 183 chdicts = [self._chdict(self.changes[id]) for id in ids[-resultSpec.limit:]] 184 return defer.succeed(chdicts) 185 chdicts = [self._chdict(v) for v in self.changes.values()] 186 return defer.succeed(chdicts) 187 188 def getChangesCount(self): 189 return defer.succeed(len(self.changes)) 190 191 def getChangesForBuild(self, buildid): 192 # the algorithm is too complicated to be worth faked, better patch it 193 # ad-hoc 194 raise NotImplementedError( 195 "Please patch in tests to return appropriate results") 196 197 def getChangeFromSSid(self, ssid): 198 chdicts = [self._chdict(v) for v in self.changes.values() 199 if v['sourcestampid'] == ssid] 200 if chdicts: 201 return defer.succeed(chdicts[0]) 202 return defer.succeed(None) 203 204 def _chdict(self, row): 205 chdict = row.copy() 206 del chdict['uids'] 207 if chdict['parent_changeids'] is None: 208 chdict['parent_changeids'] = [] 209 210 chdict['when_timestamp'] = epoch2datetime(chdict['when_timestamp']) 211 return chdict 212 213 # assertions 214 215 def assertChange(self, changeid, row): 216 row_only = self.changes[changeid].copy() 217 del row_only['files'] 218 del row_only['properties'] 219 del row_only['uids'] 220 if not row_only['parent_changeids']: 221 # Convert [] to None 222 # None is the value stored in the DB. 223 # We need this kind of conversion, because for the moment we only support 224 # 1 parent for a change. 225 # When we will support multiple parent for change, then we will have a 226 # table parent_changes with at least 2 col: "changeid", "parent_changeid" 227 # And the col 'parent_changeids' of the table changes will be 228 # dropped 229 row_only['parent_changeids'] = None 230 self.t.assertEqual(row_only, row.values) 231 232 def assertChangeUsers(self, changeid, expectedUids): 233 self.t.assertEqual(self.changes[changeid]['uids'], expectedUids) 234 235 # fake methods 236 237 def fakeAddChangeInstance(self, change): 238 if not hasattr(change, 'number') or not change.number: 239 if self.changes: 240 changeid = max(list(self.changes)) + 1 241 else: 242 changeid = 500 243 else: 244 changeid = change.number 245 246 # make a row from the change 247 row = dict( 248 changeid=changeid, 249 author=change.who, 250 files=change.files, 251 comments=change.comments, 252 revision=change.revision, 253 when_timestamp=change.when, 254 branch=change.branch, 255 category=change.category, 256 revlink=change.revlink, 257 properties=change.properties, 258 repository=change.repository, 259 codebase=change.codebase, 260 project=change.project, 261 uids=[]) 262 self.changes[changeid] = row 263