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