summaryrefslogtreecommitdiffstats
path: root/bitbake/lib/prserv
diff options
context:
space:
mode:
authorRichard Purdie <richard.purdie@linuxfoundation.org>2025-11-07 13:31:53 +0000
committerRichard Purdie <richard.purdie@linuxfoundation.org>2025-11-07 13:31:53 +0000
commit8c22ff0d8b70d9b12f0487ef696a7e915b9e3173 (patch)
treeefdc32587159d0050a69009bdf2330a531727d95 /bitbake/lib/prserv
parentd412d2747595c1cc4a5e3ca975e3adc31b2f7891 (diff)
downloadpoky-8c22ff0d8b70d9b12f0487ef696a7e915b9e3173.tar.gz
The poky repository master branch is no longer being updated.
You can either: a) switch to individual clones of bitbake, openembedded-core, meta-yocto and yocto-docs b) use the new bitbake-setup You can find information about either approach in our documentation: https://docs.yoctoproject.org/ Note that "poky" the distro setting is still available in meta-yocto as before and we continue to use and maintain that. Long live Poky! Some further information on the background of this change can be found in: https://lists.openembedded.org/g/openembedded-architecture/message/2179 Signed-off-by: Richard Purdie <richard.purdie@linuxfoundation.org>
Diffstat (limited to 'bitbake/lib/prserv')
-rw-r--r--bitbake/lib/prserv/__init__.py95
-rw-r--r--bitbake/lib/prserv/client.py72
-rw-r--r--bitbake/lib/prserv/db.py301
-rw-r--r--bitbake/lib/prserv/serv.py482
-rw-r--r--bitbake/lib/prserv/tests.py388
5 files changed, 0 insertions, 1338 deletions
diff --git a/bitbake/lib/prserv/__init__.py b/bitbake/lib/prserv/__init__.py
deleted file mode 100644
index ffc5a40a28..0000000000
--- a/bitbake/lib/prserv/__init__.py
+++ /dev/null
@@ -1,95 +0,0 @@
1#
2# Copyright BitBake Contributors
3#
4# SPDX-License-Identifier: GPL-2.0-only
5#
6
7
8__version__ = "2.0.0"
9
10import logging
11logger = logging.getLogger("BitBake.PRserv")
12
13from bb.asyncrpc.client import parse_address, ADDR_TYPE_UNIX, ADDR_TYPE_WS
14
15def create_server(addr, dbpath, upstream=None, read_only=False):
16 from . import serv
17
18 s = serv.PRServer(dbpath, upstream=upstream, read_only=read_only)
19 host, port = addr.split(":")
20 s.start_tcp_server(host, int(port))
21
22 return s
23
24def increase_revision(ver):
25 """Take a revision string such as "1" or "1.2.3" or even a number and increase its last number
26 This fails if the last number is not an integer"""
27
28 fields=str(ver).split('.')
29 last = fields[-1]
30
31 try:
32 val = int(last)
33 except Exception as e:
34 logger.critical("Unable to increase revision value %s: %s" % (ver, e))
35 raise e
36
37 return ".".join(fields[0:-1] + [ str(val + 1) ])
38
39def _revision_greater_or_equal(rev1, rev2):
40 """Compares x.y.z revision numbers, using integer comparison
41 Returns True if rev1 is greater or equal to rev2"""
42
43 fields1 = rev1.split(".")
44 fields2 = rev2.split(".")
45 l1 = len(fields1)
46 l2 = len(fields2)
47
48 for i in range(l1):
49 val1 = int(fields1[i])
50 if i < l2:
51 val2 = int(fields2[i])
52 if val2 < val1:
53 return True
54 elif val2 > val1:
55 return False
56 else:
57 return True
58 return True
59
60def revision_smaller(rev1, rev2):
61 """Compares x.y.z revision numbers, using integer comparison
62 Returns True if rev1 is strictly smaller than rev2"""
63 return not(_revision_greater_or_equal(rev1, rev2))
64
65def revision_greater(rev1, rev2):
66 """Compares x.y.z revision numbers, using integer comparison
67 Returns True if rev1 is strictly greater than rev2"""
68 return _revision_greater_or_equal(rev1, rev2) and (rev1 != rev2)
69
70def create_client(addr):
71 from . import client
72
73 c = client.PRClient()
74
75 try:
76 (typ, a) = parse_address(addr)
77 c.connect_tcp(*a)
78 return c
79 except Exception as e:
80 c.close()
81 raise e
82
83async def create_async_client(addr):
84 from . import client
85
86 c = client.PRAsyncClient()
87
88 try:
89 (typ, a) = parse_address(addr)
90 await c.connect_tcp(*a)
91 return c
92
93 except Exception as e:
94 await c.close()
95 raise e
diff --git a/bitbake/lib/prserv/client.py b/bitbake/lib/prserv/client.py
deleted file mode 100644
index 9f5794c433..0000000000
--- a/bitbake/lib/prserv/client.py
+++ /dev/null
@@ -1,72 +0,0 @@
1#
2# Copyright BitBake Contributors
3#
4# SPDX-License-Identifier: GPL-2.0-only
5#
6
7import logging
8import bb.asyncrpc
9from . import create_async_client
10
11logger = logging.getLogger("BitBake.PRserv")
12
13class PRAsyncClient(bb.asyncrpc.AsyncClient):
14 def __init__(self):
15 super().__init__("PRSERVICE", "1.0", logger)
16
17 async def getPR(self, version, pkgarch, checksum, history=False):
18 response = await self.invoke(
19 {"get-pr": {"version": version, "pkgarch": pkgarch, "checksum": checksum, "history": history}}
20 )
21 if response:
22 return response["value"]
23
24 async def test_pr(self, version, pkgarch, checksum, history=False):
25 response = await self.invoke(
26 {"test-pr": {"version": version, "pkgarch": pkgarch, "checksum": checksum, "history": history}}
27 )
28 if response:
29 return response["value"]
30
31 async def test_package(self, version, pkgarch):
32 response = await self.invoke(
33 {"test-package": {"version": version, "pkgarch": pkgarch}}
34 )
35 if response:
36 return response["value"]
37
38 async def max_package_pr(self, version, pkgarch):
39 response = await self.invoke(
40 {"max-package-pr": {"version": version, "pkgarch": pkgarch}}
41 )
42 if response:
43 return response["value"]
44
45 async def importone(self, version, pkgarch, checksum, value):
46 response = await self.invoke(
47 {"import-one": {"version": version, "pkgarch": pkgarch, "checksum": checksum, "value": value}}
48 )
49 if response:
50 return response["value"]
51
52 async def export(self, version, pkgarch, checksum, colinfo, history=False):
53 response = await self.invoke(
54 {"export": {"version": version, "pkgarch": pkgarch, "checksum": checksum, "colinfo": colinfo, "history": history}}
55 )
56 if response:
57 return (response["metainfo"], response["datainfo"])
58
59 async def is_readonly(self):
60 response = await self.invoke(
61 {"is-readonly": {}}
62 )
63 if response:
64 return response["readonly"]
65
66class PRClient(bb.asyncrpc.Client):
67 def __init__(self):
68 super().__init__()
69 self._add_methods("getPR", "test_pr", "test_package", "max_package_pr", "importone", "export", "is_readonly")
70
71 def _get_async_client(self):
72 return PRAsyncClient()
diff --git a/bitbake/lib/prserv/db.py b/bitbake/lib/prserv/db.py
deleted file mode 100644
index 2da493ddf5..0000000000
--- a/bitbake/lib/prserv/db.py
+++ /dev/null
@@ -1,301 +0,0 @@
1#
2# Copyright BitBake Contributors
3#
4# SPDX-License-Identifier: GPL-2.0-only
5#
6
7import logging
8import os.path
9import errno
10import prserv
11import sqlite3
12
13from contextlib import closing
14from . import increase_revision, revision_greater, revision_smaller
15
16logger = logging.getLogger("BitBake.PRserv")
17
18#
19# "No History" mode - for a given query tuple (version, pkgarch, checksum),
20# the returned value will be the largest among all the values of the same
21# (version, pkgarch). This means the PR value returned can NOT be decremented.
22#
23# "History" mode - Return a new higher value for previously unseen query
24# tuple (version, pkgarch, checksum), otherwise return historical value.
25# Value can decrement if returning to a previous build.
26
27class PRTable(object):
28 def __init__(self, conn, table, read_only):
29 self.conn = conn
30 self.read_only = read_only
31 self.table = table
32
33 # Creating the table even if the server is read-only.
34 # This avoids a race condition if a shared database
35 # is accessed by a read-only server first.
36
37 with closing(self.conn.cursor()) as cursor:
38 cursor.execute("CREATE TABLE IF NOT EXISTS %s \
39 (version TEXT NOT NULL, \
40 pkgarch TEXT NOT NULL, \
41 checksum TEXT NOT NULL, \
42 value TEXT, \
43 PRIMARY KEY (version, pkgarch, checksum, value));" % self.table)
44 self.conn.commit()
45
46 def _extremum_value(self, rows, is_max):
47 value = None
48
49 for row in rows:
50 current_value = row[0]
51 if value is None:
52 value = current_value
53 else:
54 if is_max:
55 is_new_extremum = revision_greater(current_value, value)
56 else:
57 is_new_extremum = revision_smaller(current_value, value)
58 if is_new_extremum:
59 value = current_value
60 return value
61
62 def _max_value(self, rows):
63 return self._extremum_value(rows, True)
64
65 def _min_value(self, rows):
66 return self._extremum_value(rows, False)
67
68 def test_package(self, version, pkgarch):
69 """Returns whether the specified package version is found in the database for the specified architecture"""
70
71 # Just returns the value if found or None otherwise
72 with closing(self.conn.cursor()) as cursor:
73 data=cursor.execute("SELECT value FROM %s WHERE version=? AND pkgarch=?;" % self.table,
74 (version, pkgarch))
75 row=data.fetchone()
76 if row is not None:
77 return True
78 else:
79 return False
80
81 def test_checksum_value(self, version, pkgarch, checksum, value):
82 """Returns whether the specified value is found in the database for the specified package, architecture and checksum"""
83
84 with closing(self.conn.cursor()) as cursor:
85 data=cursor.execute("SELECT value FROM %s WHERE version=? AND pkgarch=? and checksum=? and value=?;" % self.table,
86 (version, pkgarch, checksum, value))
87 row=data.fetchone()
88 if row is not None:
89 return True
90 else:
91 return False
92
93 def test_value(self, version, pkgarch, value):
94 """Returns whether the specified value is found in the database for the specified package and architecture"""
95
96 # Just returns the value if found or None otherwise
97 with closing(self.conn.cursor()) as cursor:
98 data=cursor.execute("SELECT value FROM %s WHERE version=? AND pkgarch=? and value=?;" % self.table,
99 (version, pkgarch, value))
100 row=data.fetchone()
101 if row is not None:
102 return True
103 else:
104 return False
105
106
107 def find_package_max_value(self, version, pkgarch):
108 """Returns the greatest value for (version, pkgarch), or None if not found. Doesn't create a new value"""
109
110 with closing(self.conn.cursor()) as cursor:
111 data = cursor.execute("SELECT value FROM %s where version=? AND pkgarch=?;" % (self.table),
112 (version, pkgarch))
113 rows = data.fetchall()
114 value = self._max_value(rows)
115 return value
116
117 def find_value(self, version, pkgarch, checksum, history=False):
118 """Returns the value for the specified checksum if found or None otherwise."""
119
120 if history:
121 return self.find_min_value(version, pkgarch, checksum)
122 else:
123 return self.find_max_value(version, pkgarch, checksum)
124
125
126 def _find_extremum_value(self, version, pkgarch, checksum, is_max):
127 """Returns the maximum (if is_max is True) or minimum (if is_max is False) value
128 for (version, pkgarch, checksum), or None if not found. Doesn't create a new value"""
129
130 with closing(self.conn.cursor()) as cursor:
131 data = cursor.execute("SELECT value FROM %s where version=? AND pkgarch=? AND checksum=?;" % (self.table),
132 (version, pkgarch, checksum))
133 rows = data.fetchall()
134 return self._extremum_value(rows, is_max)
135
136 def find_max_value(self, version, pkgarch, checksum):
137 return self._find_extremum_value(version, pkgarch, checksum, True)
138
139 def find_min_value(self, version, pkgarch, checksum):
140 return self._find_extremum_value(version, pkgarch, checksum, False)
141
142 def find_new_subvalue(self, version, pkgarch, base):
143 """Take and increase the greatest "<base>.y" value for (version, pkgarch), or return "<base>.0" if not found.
144 This doesn't store a new value."""
145
146 with closing(self.conn.cursor()) as cursor:
147 data = cursor.execute("SELECT value FROM %s where version=? AND pkgarch=? AND value LIKE '%s.%%';" % (self.table, base),
148 (version, pkgarch))
149 rows = data.fetchall()
150 value = self._max_value(rows)
151
152 if value is not None:
153 return increase_revision(value)
154 else:
155 return base + ".0"
156
157 def store_value(self, version, pkgarch, checksum, value):
158 """Store value in the database"""
159
160 if not self.read_only and not self.test_checksum_value(version, pkgarch, checksum, value):
161 with closing(self.conn.cursor()) as cursor:
162 cursor.execute("INSERT INTO %s VALUES (?, ?, ?, ?);" % (self.table),
163 (version, pkgarch, checksum, value))
164 self.conn.commit()
165
166 def _get_value(self, version, pkgarch, checksum, history):
167
168 max_value = self.find_package_max_value(version, pkgarch)
169
170 if max_value is None:
171 # version, pkgarch completely unknown. Return initial value.
172 return "0"
173
174 value = self.find_value(version, pkgarch, checksum, history)
175
176 if value is None:
177 # version, pkgarch found but not checksum. Create a new value from the maximum one
178 return increase_revision(max_value)
179
180 if history:
181 return value
182
183 # "no history" mode - If the value is not the maximum value for the package, need to increase it.
184 if max_value > value:
185 return increase_revision(max_value)
186 else:
187 return value
188
189 def get_value(self, version, pkgarch, checksum, history):
190 value = self._get_value(version, pkgarch, checksum, history)
191 if not self.read_only:
192 self.store_value(version, pkgarch, checksum, value)
193 return value
194
195 def importone(self, version, pkgarch, checksum, value):
196 self.store_value(version, pkgarch, checksum, value)
197 return value
198
199 def export(self, version, pkgarch, checksum, colinfo, history=False):
200 metainfo = {}
201 with closing(self.conn.cursor()) as cursor:
202 #column info
203 if colinfo:
204 metainfo["tbl_name"] = self.table
205 metainfo["core_ver"] = prserv.__version__
206 metainfo["col_info"] = []
207 data = cursor.execute("PRAGMA table_info(%s);" % self.table)
208 for row in data:
209 col = {}
210 col["name"] = row["name"]
211 col["type"] = row["type"]
212 col["notnull"] = row["notnull"]
213 col["dflt_value"] = row["dflt_value"]
214 col["pk"] = row["pk"]
215 metainfo["col_info"].append(col)
216
217 #data info
218 datainfo = []
219
220 if history:
221 sqlstmt = "SELECT * FROM %s as T1 WHERE 1=1 " % self.table
222 else:
223 sqlstmt = "SELECT T1.version, T1.pkgarch, T1.checksum, T1.value FROM %s as T1, \
224 (SELECT version, pkgarch, max(value) as maxvalue FROM %s GROUP BY version, pkgarch) as T2 \
225 WHERE T1.version=T2.version AND T1.pkgarch=T2.pkgarch AND T1.value=T2.maxvalue " % (self.table, self.table)
226 sqlarg = []
227 where = ""
228 if version:
229 where += "AND T1.version=? "
230 sqlarg.append(str(version))
231 if pkgarch:
232 where += "AND T1.pkgarch=? "
233 sqlarg.append(str(pkgarch))
234 if checksum:
235 where += "AND T1.checksum=? "
236 sqlarg.append(str(checksum))
237
238 sqlstmt += where + ";"
239
240 if len(sqlarg):
241 data = cursor.execute(sqlstmt, tuple(sqlarg))
242 else:
243 data = cursor.execute(sqlstmt)
244 for row in data:
245 if row["version"]:
246 col = {}
247 col["version"] = row["version"]
248 col["pkgarch"] = row["pkgarch"]
249 col["checksum"] = row["checksum"]
250 col["value"] = row["value"]
251 datainfo.append(col)
252 return (metainfo, datainfo)
253
254 def dump_db(self, fd):
255 writeCount = 0
256 for line in self.conn.iterdump():
257 writeCount = writeCount + len(line) + 1
258 fd.write(line)
259 fd.write("\n")
260 return writeCount
261
262class PRData(object):
263 """Object representing the PR database"""
264 def __init__(self, filename, read_only=False):
265 self.filename=os.path.abspath(filename)
266 self.read_only = read_only
267 #build directory hierarchy
268 try:
269 os.makedirs(os.path.dirname(self.filename))
270 except OSError as e:
271 if e.errno != errno.EEXIST:
272 raise e
273 uri = "file:%s%s" % (self.filename, "?mode=ro" if self.read_only else "")
274 logger.debug("Opening PRServ database '%s'" % (uri))
275 self.connection=sqlite3.connect(uri, uri=True)
276 self.connection.row_factory=sqlite3.Row
277 self.connection.execute("PRAGMA synchronous = OFF;")
278 self.connection.execute("PRAGMA journal_mode = WAL;")
279 self.connection.commit()
280 self._tables={}
281
282 def disconnect(self):
283 self.connection.commit()
284 self.connection.close()
285
286 def __getitem__(self, tblname):
287 if not isinstance(tblname, str):
288 raise TypeError("tblname argument must be a string, not '%s'" %
289 type(tblname))
290 if tblname in self._tables:
291 return self._tables[tblname]
292 else:
293 tableobj = self._tables[tblname] = PRTable(self.connection, tblname, self.read_only)
294 return tableobj
295
296 def __delitem__(self, tblname):
297 if tblname in self._tables:
298 del self._tables[tblname]
299 logger.info("drop table %s" % (tblname))
300 self.connection.execute("DROP TABLE IF EXISTS %s;" % tblname)
301 self.connection.commit()
diff --git a/bitbake/lib/prserv/serv.py b/bitbake/lib/prserv/serv.py
deleted file mode 100644
index e175886308..0000000000
--- a/bitbake/lib/prserv/serv.py
+++ /dev/null
@@ -1,482 +0,0 @@
1#
2# Copyright BitBake Contributors
3#
4# SPDX-License-Identifier: GPL-2.0-only
5#
6
7import os,sys,logging
8import signal, time
9import socket
10import io
11import sqlite3
12import prserv
13import prserv.db
14import errno
15from . import create_async_client, revision_smaller, increase_revision
16import bb.asyncrpc
17
18logger = logging.getLogger("BitBake.PRserv")
19
20PIDPREFIX = "/tmp/PRServer_%s_%s.pid"
21singleton = None
22
23class PRServerClient(bb.asyncrpc.AsyncServerConnection):
24 def __init__(self, socket, server):
25 super().__init__(socket, "PRSERVICE", server.logger)
26 self.server = server
27
28 self.handlers.update({
29 "get-pr": self.handle_get_pr,
30 "test-pr": self.handle_test_pr,
31 "test-package": self.handle_test_package,
32 "max-package-pr": self.handle_max_package_pr,
33 "import-one": self.handle_import_one,
34 "export": self.handle_export,
35 "is-readonly": self.handle_is_readonly,
36 })
37
38 def validate_proto_version(self):
39 return (self.proto_version == (1, 0))
40
41 async def dispatch_message(self, msg):
42 try:
43 return await super().dispatch_message(msg)
44 except:
45 raise
46
47 async def handle_test_pr(self, request):
48 '''Finds the PR value corresponding to the request. If not found, returns None and doesn't insert a new value'''
49 version = request["version"]
50 pkgarch = request["pkgarch"]
51 checksum = request["checksum"]
52 history = request["history"]
53
54 value = self.server.table.find_value(version, pkgarch, checksum, history)
55 return {"value": value}
56
57 async def handle_test_package(self, request):
58 '''Tells whether there are entries for (version, pkgarch) in the db. Returns True or False'''
59 version = request["version"]
60 pkgarch = request["pkgarch"]
61
62 value = self.server.table.test_package(version, pkgarch)
63 return {"value": value}
64
65 async def handle_max_package_pr(self, request):
66 '''Finds the greatest PR value for (version, pkgarch) in the db. Returns None if no entry was found'''
67 version = request["version"]
68 pkgarch = request["pkgarch"]
69
70 value = self.server.table.find_package_max_value(version, pkgarch)
71 return {"value": value}
72
73 async def handle_get_pr(self, request):
74 version = request["version"]
75 pkgarch = request["pkgarch"]
76 checksum = request["checksum"]
77 history = request["history"]
78
79 if self.upstream_client is None:
80 value = self.server.table.get_value(version, pkgarch, checksum, history)
81 return {"value": value}
82
83 # We have an upstream server.
84 # Check whether the local server already knows the requested configuration.
85 # If the configuration is a new one, the generated value we will add will
86 # depend on what's on the upstream server. That's why we're calling find_value()
87 # instead of get_value() directly.
88
89 value = self.server.table.find_value(version, pkgarch, checksum, history)
90 upstream_max = await self.upstream_client.max_package_pr(version, pkgarch)
91
92 if value is not None:
93
94 # The configuration is already known locally.
95
96 if history:
97 value = self.server.table.get_value(version, pkgarch, checksum, history)
98 else:
99 existing_value = value
100 # In "no history", we need to make sure the value doesn't decrease
101 # and is at least greater than the maximum upstream value
102 # and the maximum local value
103
104 local_max = self.server.table.find_package_max_value(version, pkgarch)
105 if revision_smaller(value, local_max):
106 value = increase_revision(local_max)
107
108 if revision_smaller(value, upstream_max):
109 # Ask upstream whether it knows the checksum
110 upstream_value = await self.upstream_client.test_pr(version, pkgarch, checksum)
111 if upstream_value is None:
112 # Upstream doesn't have our checksum, let create a new one
113 value = upstream_max + ".0"
114 else:
115 # Fine to take the same value as upstream
116 value = upstream_max
117
118 if not value == existing_value and not self.server.read_only:
119 self.server.table.store_value(version, pkgarch, checksum, value)
120
121 return {"value": value}
122
123 # The configuration is a new one for the local server
124 # Let's ask the upstream server whether it knows it
125
126 known_upstream = await self.upstream_client.test_package(version, pkgarch)
127
128 if not known_upstream:
129
130 # The package is not known upstream, must be a local-only package
131 # Let's compute the PR number using the local-only method
132
133 value = self.server.table.get_value(version, pkgarch, checksum, history)
134 return {"value": value}
135
136 # The package is known upstream, let's ask the upstream server
137 # whether it knows our new output hash
138
139 value = await self.upstream_client.test_pr(version, pkgarch, checksum)
140
141 if value is not None:
142
143 # Upstream knows this output hash, let's store it and use it too.
144
145 if not self.server.read_only:
146 self.server.table.store_value(version, pkgarch, checksum, value)
147 # If the local server is read only, won't be able to store the new
148 # value in the database and will have to keep asking the upstream server
149 return {"value": value}
150
151 # The output hash doesn't exist upstream, get the most recent number from upstream (x)
152 # Then, we want to have a new PR value for the local server: x.y
153
154 upstream_max = await self.upstream_client.max_package_pr(version, pkgarch)
155 # Here we know that the package is known upstream, so upstream_max can't be None
156 subvalue = self.server.table.find_new_subvalue(version, pkgarch, upstream_max)
157
158 if not self.server.read_only:
159 self.server.table.store_value(version, pkgarch, checksum, subvalue)
160
161 return {"value": subvalue}
162
163 async def process_requests(self):
164 if self.server.upstream is not None:
165 self.upstream_client = await create_async_client(self.server.upstream)
166 else:
167 self.upstream_client = None
168
169 try:
170 await super().process_requests()
171 finally:
172 if self.upstream_client is not None:
173 await self.upstream_client.close()
174
175 async def handle_import_one(self, request):
176 response = None
177 if not self.server.read_only:
178 version = request["version"]
179 pkgarch = request["pkgarch"]
180 checksum = request["checksum"]
181 value = request["value"]
182
183 value = self.server.table.importone(version, pkgarch, checksum, value)
184 if value is not None:
185 response = {"value": value}
186
187 return response
188
189 async def handle_export(self, request):
190 version = request["version"]
191 pkgarch = request["pkgarch"]
192 checksum = request["checksum"]
193 colinfo = request["colinfo"]
194 history = request["history"]
195
196 try:
197 (metainfo, datainfo) = self.server.table.export(version, pkgarch, checksum, colinfo, history)
198 except sqlite3.Error as exc:
199 self.logger.error(str(exc))
200 metainfo = datainfo = None
201
202 return {"metainfo": metainfo, "datainfo": datainfo}
203
204 async def handle_is_readonly(self, request):
205 return {"readonly": self.server.read_only}
206
207class PRServer(bb.asyncrpc.AsyncServer):
208 def __init__(self, dbfile, read_only=False, upstream=None):
209 super().__init__(logger)
210 self.dbfile = dbfile
211 self.table = None
212 self.read_only = read_only
213 self.upstream = upstream
214
215 def accept_client(self, socket):
216 return PRServerClient(socket, self)
217
218 def start(self):
219 tasks = super().start()
220 self.db = prserv.db.PRData(self.dbfile, read_only=self.read_only)
221 self.table = self.db["PRMAIN"]
222
223 self.logger.info("Started PRServer with DBfile: %s, Address: %s, PID: %s" %
224 (self.dbfile, self.address, str(os.getpid())))
225
226 if self.upstream is not None:
227 self.logger.info("And upstream PRServer: %s " % (self.upstream))
228
229 return tasks
230
231 async def stop(self):
232 self.db.disconnect()
233 await super().stop()
234
235class PRServSingleton(object):
236 def __init__(self, dbfile, logfile, host, port, upstream):
237 self.dbfile = dbfile
238 self.logfile = logfile
239 self.host = host
240 self.port = port
241 self.upstream = upstream
242
243 def start(self):
244 self.prserv = PRServer(self.dbfile, upstream=self.upstream)
245 self.prserv.start_tcp_server(socket.gethostbyname(self.host), self.port)
246 self.process = self.prserv.serve_as_process(log_level=logging.WARNING)
247
248 if not self.prserv.address:
249 raise PRServiceConfigError
250 if not self.port:
251 self.port = int(self.prserv.address.rsplit(":", 1)[1])
252
253def run_as_daemon(func, pidfile, logfile):
254 """
255 See Advanced Programming in the UNIX, Sec 13.3
256 """
257 try:
258 pid = os.fork()
259 if pid > 0:
260 os.waitpid(pid, 0)
261 #parent return instead of exit to give control
262 return pid
263 except OSError as e:
264 raise Exception("%s [%d]" % (e.strerror, e.errno))
265
266 os.setsid()
267 """
268 fork again to make sure the daemon is not session leader,
269 which prevents it from acquiring controlling terminal
270 """
271 try:
272 pid = os.fork()
273 if pid > 0: #parent
274 os._exit(0)
275 except OSError as e:
276 raise Exception("%s [%d]" % (e.strerror, e.errno))
277
278 os.chdir("/")
279
280 sys.stdout.flush()
281 sys.stderr.flush()
282
283 # We could be called from a python thread with io.StringIO as
284 # stdout/stderr or it could be 'real' unix fd forking where we need
285 # to physically close the fds to prevent the program launching us from
286 # potentially hanging on a pipe. Handle both cases.
287 si = open("/dev/null", "r")
288 try:
289 os.dup2(si.fileno(), sys.stdin.fileno())
290 except (AttributeError, io.UnsupportedOperation):
291 sys.stdin = si
292 so = open(logfile, "a+")
293 try:
294 os.dup2(so.fileno(), sys.stdout.fileno())
295 except (AttributeError, io.UnsupportedOperation):
296 sys.stdout = so
297 try:
298 os.dup2(so.fileno(), sys.stderr.fileno())
299 except (AttributeError, io.UnsupportedOperation):
300 sys.stderr = so
301
302 # Clear out all log handlers prior to the fork() to avoid calling
303 # event handlers not part of the PRserver
304 for logger_iter in logging.Logger.manager.loggerDict.keys():
305 logging.getLogger(logger_iter).handlers = []
306
307 # Ensure logging makes it to the logfile
308 streamhandler = logging.StreamHandler()
309 streamhandler.setLevel(logging.DEBUG)
310 formatter = bb.msg.BBLogFormatter("%(levelname)s: %(message)s")
311 streamhandler.setFormatter(formatter)
312 logger.addHandler(streamhandler)
313
314 # write pidfile
315 pid = str(os.getpid())
316 with open(pidfile, "w") as pf:
317 pf.write("%s\n" % pid)
318
319 func()
320 os.remove(pidfile)
321 os._exit(0)
322
323def start_daemon(dbfile, host, port, logfile, read_only=False, upstream=None):
324 ip = socket.gethostbyname(host)
325 pidfile = PIDPREFIX % (ip, port)
326 try:
327 with open(pidfile) as pf:
328 pid = int(pf.readline().strip())
329 except IOError:
330 pid = None
331
332 if pid:
333 sys.stderr.write("pidfile %s already exist. Daemon already running?\n"
334 % pidfile)
335 return 1
336
337 dbfile = os.path.abspath(dbfile)
338 def daemon_main():
339 server = PRServer(dbfile, read_only=read_only, upstream=upstream)
340 server.start_tcp_server(ip, port)
341 server.serve_forever()
342
343 run_as_daemon(daemon_main, pidfile, os.path.abspath(logfile))
344 return 0
345
346def stop_daemon(host, port):
347 import glob
348 ip = socket.gethostbyname(host)
349 pidfile = PIDPREFIX % (ip, port)
350 try:
351 with open(pidfile) as pf:
352 pid = int(pf.readline().strip())
353 except IOError:
354 pid = None
355
356 if not pid:
357 # when server starts at port=0 (i.e. localhost:0), server actually takes another port,
358 # so at least advise the user which ports the corresponding server is listening
359 ports = []
360 portstr = ""
361 for pf in glob.glob(PIDPREFIX % (ip, "*")):
362 bn = os.path.basename(pf)
363 root, _ = os.path.splitext(bn)
364 ports.append(root.split("_")[-1])
365 if len(ports):
366 portstr = "Wrong port? Other ports listening at %s: %s" % (host, " ".join(ports))
367
368 sys.stderr.write("pidfile %s does not exist. Daemon not running? %s\n"
369 % (pidfile, portstr))
370 return 1
371
372 try:
373 if is_running(pid):
374 print("Sending SIGTERM to pr-server.")
375 os.kill(pid, signal.SIGTERM)
376 time.sleep(0.1)
377
378 try:
379 os.remove(pidfile)
380 except FileNotFoundError:
381 # The PID file might have been removed by the exiting process
382 pass
383
384 except OSError as e:
385 err = str(e)
386 if err.find("No such process") <= 0:
387 raise e
388
389 return 0
390
391def is_running(pid):
392 try:
393 os.kill(pid, 0)
394 except OSError as err:
395 if err.errno == errno.ESRCH:
396 return False
397 return True
398
399def is_local_special(host, port):
400 if (host == "localhost" or host == "127.0.0.1") and not port:
401 return True
402 else:
403 return False
404
405class PRServiceConfigError(Exception):
406 pass
407
408def auto_start(d):
409 global singleton
410
411 host_params = list(filter(None, (d.getVar("PRSERV_HOST") or "").split(":")))
412 if not host_params:
413 # Shutdown any existing PR Server
414 auto_shutdown()
415 return None
416
417 if len(host_params) != 2:
418 # Shutdown any existing PR Server
419 auto_shutdown()
420 logger.critical("\n".join(["PRSERV_HOST: incorrect format",
421 'Usage: PRSERV_HOST = "<hostname>:<port>"']))
422 raise PRServiceConfigError
423
424 host = host_params[0].strip().lower()
425 port = int(host_params[1])
426
427 upstream = d.getVar("PRSERV_UPSTREAM") or None
428
429 if is_local_special(host, port):
430 import bb.utils
431 cachedir = (d.getVar("PERSISTENT_DIR") or d.getVar("CACHE"))
432 if not cachedir:
433 logger.critical("Please set the 'PERSISTENT_DIR' or 'CACHE' variable")
434 raise PRServiceConfigError
435 dbfile = os.path.join(cachedir, "prserv.sqlite3")
436 logfile = os.path.join(cachedir, "prserv.log")
437 if singleton:
438 if singleton.dbfile != dbfile:
439 # Shutdown any existing PR Server as doesn't match config
440 auto_shutdown()
441 if not singleton:
442 bb.utils.mkdirhier(cachedir)
443 singleton = PRServSingleton(os.path.abspath(dbfile), os.path.abspath(logfile), host, port, upstream)
444 singleton.start()
445 if singleton:
446 host = singleton.host
447 port = singleton.port
448
449 try:
450 ping(host, port)
451 return str(host) + ":" + str(port)
452
453 except Exception:
454 logger.critical("PRservice %s:%d not available" % (host, port))
455 raise PRServiceConfigError
456
457def auto_shutdown():
458 global singleton
459 if singleton and singleton.process:
460 singleton.process.terminate()
461 singleton.process.join()
462 singleton = None
463
464def ping(host, port):
465 from . import client
466
467 with client.PRClient() as conn:
468 conn.connect_tcp(host, port)
469 return conn.ping()
470
471def connect(host, port):
472 from . import client
473
474 global singleton
475
476 if host.strip().lower() == "localhost" and not port:
477 host = "localhost"
478 port = singleton.port
479
480 conn = client.PRClient()
481 conn.connect_tcp(host, port)
482 return conn
diff --git a/bitbake/lib/prserv/tests.py b/bitbake/lib/prserv/tests.py
deleted file mode 100644
index df0c003003..0000000000
--- a/bitbake/lib/prserv/tests.py
+++ /dev/null
@@ -1,388 +0,0 @@
1#! /usr/bin/env python3
2#
3# Copyright (C) 2024 BitBake Contributors
4#
5# SPDX-License-Identifier: GPL-2.0-only
6#
7
8from . import create_server, create_client, increase_revision, revision_greater, revision_smaller, _revision_greater_or_equal
9import prserv.db as db
10from bb.asyncrpc import InvokeError
11import logging
12import os
13import sys
14import tempfile
15import unittest
16import socket
17import subprocess
18from pathlib import Path
19
20THIS_DIR = Path(__file__).parent
21BIN_DIR = THIS_DIR.parent.parent / "bin"
22
23version = "dummy-1.0-r0"
24pkgarch = "core2-64"
25other_arch = "aarch64"
26
27checksumX = "51bf8189dbe9ea81fa6dd89608bf19380c437a9cf12f6c6239887801ba4ab4f0"
28checksum0 = "51bf8189dbe9ea81fa6dd89608bf19380c437a9cf12f6c6239887801ba4ab4a0"
29checksum1 = "51bf8189dbe9ea81fa6dd89608bf19380c437a9cf12f6c6239887801ba4ab4a1"
30checksum2 = "51bf8189dbe9ea81fa6dd89608bf19380c437a9cf12f6c6239887801ba4ab4a2"
31checksum3 = "51bf8189dbe9ea81fa6dd89608bf19380c437a9cf12f6c6239887801ba4ab4a3"
32checksum4 = "51bf8189dbe9ea81fa6dd89608bf19380c437a9cf12f6c6239887801ba4ab4a4"
33checksum5 = "51bf8189dbe9ea81fa6dd89608bf19380c437a9cf12f6c6239887801ba4ab4a5"
34checksum6 = "51bf8189dbe9ea81fa6dd89608bf19380c437a9cf12f6c6239887801ba4ab4a6"
35checksum7 = "51bf8189dbe9ea81fa6dd89608bf19380c437a9cf12f6c6239887801ba4ab4a7"
36checksum8 = "51bf8189dbe9ea81fa6dd89608bf19380c437a9cf12f6c6239887801ba4ab4a8"
37checksum9 = "51bf8189dbe9ea81fa6dd89608bf19380c437a9cf12f6c6239887801ba4ab4a9"
38checksum10 = "51bf8189dbe9ea81fa6dd89608bf19380c437a9cf12f6c6239887801ba4ab4aa"
39
40def server_prefunc(server, name):
41 logging.basicConfig(level=logging.DEBUG, filename='prserv-%s.log' % name, filemode='w',
42 format='%(levelname)s %(filename)s:%(lineno)d %(message)s')
43 server.logger.debug("Running server %s" % name)
44 sys.stdout = open('prserv-stdout-%s.log' % name, 'w')
45 sys.stderr = sys.stdout
46
47class PRTestSetup(object):
48
49 def start_server(self, name, dbfile, upstream=None, read_only=False, prefunc=server_prefunc):
50
51 def cleanup_server(server):
52 if server.process.exitcode is not None:
53 return
54 server.process.terminate()
55 server.process.join()
56
57 server = create_server(socket.gethostbyname("localhost") + ":0",
58 dbfile,
59 upstream=upstream,
60 read_only=read_only)
61
62 server.serve_as_process(prefunc=prefunc, args=(name,))
63 self.addCleanup(cleanup_server, server)
64
65 return server
66
67 def start_client(self, server_address):
68 def cleanup_client(client):
69 client.close()
70
71 client = create_client(server_address)
72 self.addCleanup(cleanup_client, client)
73
74 return client
75
76class FunctionTests(unittest.TestCase):
77
78 def setUp(self):
79 self.temp_dir = tempfile.TemporaryDirectory(prefix='bb-prserv')
80 self.addCleanup(self.temp_dir.cleanup)
81
82 def test_increase_revision(self):
83 self.assertEqual(increase_revision("1"), "2")
84 self.assertEqual(increase_revision("1.0"), "1.1")
85 self.assertEqual(increase_revision("1.1.1"), "1.1.2")
86 self.assertEqual(increase_revision("1.1.1.3"), "1.1.1.4")
87 self.assertEqual(increase_revision("9"), "10")
88 self.assertEqual(increase_revision("1.9"), "1.10")
89 self.assertRaises(ValueError, increase_revision, "1.a")
90 self.assertRaises(ValueError, increase_revision, "1.")
91 self.assertRaises(ValueError, increase_revision, "")
92
93 def test_revision_greater_or_equal(self):
94 self.assertTrue(_revision_greater_or_equal("2", "2"))
95 self.assertTrue(_revision_greater_or_equal("2", "1"))
96 self.assertTrue(_revision_greater_or_equal("10", "2"))
97 self.assertTrue(_revision_greater_or_equal("1.10", "1.2"))
98 self.assertFalse(_revision_greater_or_equal("1.2", "1.10"))
99 self.assertTrue(_revision_greater_or_equal("1.10", "1"))
100 self.assertTrue(_revision_greater_or_equal("1.10.1", "1.10"))
101 self.assertFalse(_revision_greater_or_equal("1.10.1", "1.10.2"))
102 self.assertTrue(_revision_greater_or_equal("1.10.1", "1.10.1"))
103 self.assertTrue(_revision_greater_or_equal("1.10.1", "1"))
104 self.assertTrue(revision_greater("1.20", "1.3"))
105 self.assertTrue(revision_smaller("1.3", "1.20"))
106
107 # DB tests
108
109 def test_db(self):
110 dbfile = os.path.join(self.temp_dir.name, "testtable.sqlite3")
111
112 self.db = db.PRData(dbfile)
113 self.table = self.db["PRMAIN"]
114
115 self.table.store_value(version, pkgarch, checksum0, "0")
116 self.table.store_value(version, pkgarch, checksum1, "1")
117 # "No history" mode supports multiple PRs for the same checksum
118 self.table.store_value(version, pkgarch, checksum0, "2")
119 self.table.store_value(version, pkgarch, checksum2, "1.0")
120
121 self.assertTrue(self.table.test_package(version, pkgarch))
122 self.assertFalse(self.table.test_package(version, other_arch))
123
124 self.assertTrue(self.table.test_value(version, pkgarch, "0"))
125 self.assertTrue(self.table.test_value(version, pkgarch, "1"))
126 self.assertTrue(self.table.test_value(version, pkgarch, "2"))
127
128 self.assertEqual(self.table.find_package_max_value(version, pkgarch), "2")
129
130 self.assertEqual(self.table.find_min_value(version, pkgarch, checksum0), "0")
131 self.assertEqual(self.table.find_max_value(version, pkgarch, checksum0), "2")
132
133 # Test history modes
134 self.assertEqual(self.table.find_value(version, pkgarch, checksum0, True), "0")
135 self.assertEqual(self.table.find_value(version, pkgarch, checksum0, False), "2")
136
137 self.assertEqual(self.table.find_new_subvalue(version, pkgarch, "3"), "3.0")
138 self.assertEqual(self.table.find_new_subvalue(version, pkgarch, "1"), "1.1")
139
140 # Revision comparison tests
141 self.table.store_value(version, pkgarch, checksum1, "1.3")
142 self.table.store_value(version, pkgarch, checksum1, "1.20")
143 self.assertEqual(self.table.find_min_value(version, pkgarch, checksum1), "1")
144 self.assertEqual(self.table.find_max_value(version, pkgarch, checksum1), "1.20")
145
146class PRBasicTests(PRTestSetup, unittest.TestCase):
147
148 def setUp(self):
149 self.temp_dir = tempfile.TemporaryDirectory(prefix='bb-prserv')
150 self.addCleanup(self.temp_dir.cleanup)
151
152 dbfile = os.path.join(self.temp_dir.name, "prtest-basic.sqlite3")
153
154 self.server1 = self.start_server("basic", dbfile)
155 self.client1 = self.start_client(self.server1.address)
156
157 def test_basic(self):
158
159 # Checks on non existing configuration
160
161 result = self.client1.test_pr(version, pkgarch, checksum0)
162 self.assertIsNone(result, "test_pr should return 'None' for a non existing PR")
163
164 result = self.client1.test_package(version, pkgarch)
165 self.assertFalse(result, "test_package should return 'False' for a non existing PR")
166
167 result = self.client1.max_package_pr(version, pkgarch)
168 self.assertIsNone(result, "max_package_pr should return 'None' for a non existing PR")
169
170 # Add a first configuration
171
172 result = self.client1.getPR(version, pkgarch, checksum0)
173 self.assertEqual(result, "0", "getPR: initial PR of a package should be '0'")
174
175 result = self.client1.test_pr(version, pkgarch, checksum0)
176 self.assertEqual(result, "0", "test_pr should return '0' here, matching the result of getPR")
177
178 result = self.client1.test_package(version, pkgarch)
179 self.assertTrue(result, "test_package should return 'True' for an existing PR")
180
181 result = self.client1.max_package_pr(version, pkgarch)
182 self.assertEqual(result, "0", "max_package_pr should return '0' in the current test series")
183
184 # Check that the same request gets the same value
185
186 result = self.client1.getPR(version, pkgarch, checksum0)
187 self.assertEqual(result, "0", "getPR: asking for the same PR a second time in a row should return the same value.")
188
189 # Add new configurations
190
191 result = self.client1.getPR(version, pkgarch, checksum1)
192 self.assertEqual(result, "1", "getPR: second PR of a package should be '1'")
193
194 result = self.client1.test_pr(version, pkgarch, checksum1)
195 self.assertEqual(result, "1", "test_pr should return '1' here, matching the result of getPR")
196
197 result = self.client1.max_package_pr(version, pkgarch)
198 self.assertEqual(result, "1", "max_package_pr should return '1' in the current test series")
199
200 result = self.client1.getPR(version, pkgarch, checksum2)
201 self.assertEqual(result, "2", "getPR: second PR of a package should be '2'")
202
203 result = self.client1.test_pr(version, pkgarch, checksum2)
204 self.assertEqual(result, "2", "test_pr should return '2' here, matching the result of getPR")
205
206 result = self.client1.max_package_pr(version, pkgarch)
207 self.assertEqual(result, "2", "max_package_pr should return '2' in the current test series")
208
209 result = self.client1.getPR(version, pkgarch, checksum3)
210 self.assertEqual(result, "3", "getPR: second PR of a package should be '3'")
211
212 result = self.client1.test_pr(version, pkgarch, checksum3)
213 self.assertEqual(result, "3", "test_pr should return '3' here, matching the result of getPR")
214
215 result = self.client1.max_package_pr(version, pkgarch)
216 self.assertEqual(result, "3", "max_package_pr should return '3' in the current test series")
217
218 # Ask again for the first configuration
219
220 result = self.client1.getPR(version, pkgarch, checksum0)
221 self.assertEqual(result, "4", "getPR: should return '4' in this configuration")
222
223 # Ask again with explicit "no history" mode
224
225 result = self.client1.getPR(version, pkgarch, checksum0, False)
226 self.assertEqual(result, "4", "getPR: should return '4' in this configuration")
227
228 # Ask again with explicit "history" mode. This should return the first recorded PR for checksum0
229
230 result = self.client1.getPR(version, pkgarch, checksum0, True)
231 self.assertEqual(result, "0", "getPR: should return '0' in this configuration")
232
233 # Check again that another pkgarg resets the counters
234
235 result = self.client1.test_pr(version, other_arch, checksum0)
236 self.assertIsNone(result, "test_pr should return 'None' for a non existing PR")
237
238 result = self.client1.test_package(version, other_arch)
239 self.assertFalse(result, "test_package should return 'False' for a non existing PR")
240
241 result = self.client1.max_package_pr(version, other_arch)
242 self.assertIsNone(result, "max_package_pr should return 'None' for a non existing PR")
243
244 # Now add the configuration
245
246 result = self.client1.getPR(version, other_arch, checksum0)
247 self.assertEqual(result, "0", "getPR: initial PR of a package should be '0'")
248
249 result = self.client1.test_pr(version, other_arch, checksum0)
250 self.assertEqual(result, "0", "test_pr should return '0' here, matching the result of getPR")
251
252 result = self.client1.test_package(version, other_arch)
253 self.assertTrue(result, "test_package should return 'True' for an existing PR")
254
255 result = self.client1.max_package_pr(version, other_arch)
256 self.assertEqual(result, "0", "max_package_pr should return '0' in the current test series")
257
258 result = self.client1.is_readonly()
259 self.assertFalse(result, "Server should not be described as 'read-only'")
260
261class PRUpstreamTests(PRTestSetup, unittest.TestCase):
262
263 def setUp(self):
264
265 self.temp_dir = tempfile.TemporaryDirectory(prefix='bb-prserv')
266 self.addCleanup(self.temp_dir.cleanup)
267
268 dbfile2 = os.path.join(self.temp_dir.name, "prtest-upstream2.sqlite3")
269 self.server2 = self.start_server("upstream2", dbfile2)
270 self.client2 = self.start_client(self.server2.address)
271
272 dbfile1 = os.path.join(self.temp_dir.name, "prtest-upstream1.sqlite3")
273 self.server1 = self.start_server("upstream1", dbfile1, upstream=self.server2.address)
274 self.client1 = self.start_client(self.server1.address)
275
276 dbfile0 = os.path.join(self.temp_dir.name, "prtest-local.sqlite3")
277 self.server0 = self.start_server("local", dbfile0, upstream=self.server1.address)
278 self.client0 = self.start_client(self.server0.address)
279 self.shared_db = dbfile0
280
281 def test_upstream_and_readonly(self):
282
283 # For identical checksums, all servers should return the same PR
284
285 result = self.client2.getPR(version, pkgarch, checksum0)
286 self.assertEqual(result, "0", "getPR: initial PR of a package should be '0'")
287
288 result = self.client1.getPR(version, pkgarch, checksum0)
289 self.assertEqual(result, "0", "getPR: initial PR of a package should be '0' (same as upstream)")
290
291 result = self.client0.getPR(version, pkgarch, checksum0)
292 self.assertEqual(result, "0", "getPR: initial PR of a package should be '0' (same as upstream)")
293
294 # Now introduce new checksums on server1 for, same version
295
296 result = self.client1.getPR(version, pkgarch, checksum1)
297 self.assertEqual(result, "0.0", "getPR: first PR of a package which has a different checksum upstream should be '0.0'")
298
299 result = self.client1.getPR(version, pkgarch, checksum2)
300 self.assertEqual(result, "0.1", "getPR: second PR of a package that has a different checksum upstream should be '0.1'")
301
302 # Now introduce checksums on server0 for, same version
303
304 result = self.client1.getPR(version, pkgarch, checksum1)
305 self.assertEqual(result, "0.2", "getPR: can't decrease for known PR")
306
307 result = self.client1.getPR(version, pkgarch, checksum2)
308 self.assertEqual(result, "0.3")
309
310 result = self.client1.max_package_pr(version, pkgarch)
311 self.assertEqual(result, "0.3")
312
313 result = self.client0.getPR(version, pkgarch, checksum3)
314 self.assertEqual(result, "0.3.0", "getPR: first PR of a package that doesn't exist upstream should be '0.3.0'")
315
316 result = self.client0.getPR(version, pkgarch, checksum4)
317 self.assertEqual(result, "0.3.1", "getPR: second PR of a package that doesn't exist upstream should be '0.3.1'")
318
319 result = self.client0.getPR(version, pkgarch, checksum3)
320 self.assertEqual(result, "0.3.2")
321
322 # More upstream updates
323 # Here, we assume no communication between server2 and server0. server2 only impacts server0
324 # after impacting server1
325
326 self.assertEqual(self.client2.getPR(version, pkgarch, checksum5), "1")
327 self.assertEqual(self.client1.getPR(version, pkgarch, checksum6), "1.0")
328 self.assertEqual(self.client1.getPR(version, pkgarch, checksum7), "1.1")
329 self.assertEqual(self.client0.getPR(version, pkgarch, checksum8), "1.1.0")
330 self.assertEqual(self.client0.getPR(version, pkgarch, checksum9), "1.1.1")
331
332 # "history" mode tests
333
334 self.assertEqual(self.client2.getPR(version, pkgarch, checksum0, True), "0")
335 self.assertEqual(self.client1.getPR(version, pkgarch, checksum2, True), "0.1")
336 self.assertEqual(self.client0.getPR(version, pkgarch, checksum3, True), "0.3.0")
337
338 # More "no history" mode tests
339
340 self.assertEqual(self.client2.getPR(version, pkgarch, checksum0), "2")
341 self.assertEqual(self.client1.getPR(version, pkgarch, checksum0), "2") # Same as upstream
342 self.assertEqual(self.client0.getPR(version, pkgarch, checksum0), "2") # Same as upstream
343 self.assertEqual(self.client1.getPR(version, pkgarch, checksum7), "3") # This could be surprising, but since the previous revision was "2", increasing it yields "3".
344 # We don't know how many upstream servers we have
345 # Start read-only server with server1 as upstream
346 self.server_ro = self.start_server("local-ro", self.shared_db, upstream=self.server1.address, read_only=True)
347 self.client_ro = self.start_client(self.server_ro.address)
348
349 self.assertTrue(self.client_ro.is_readonly(), "Database should be described as 'read-only'")
350
351 # Checks on non existing configurations
352 self.assertIsNone(self.client_ro.test_pr(version, pkgarch, checksumX))
353 self.assertFalse(self.client_ro.test_package("unknown", pkgarch))
354
355 # Look up existing configurations
356 self.assertEqual(self.client_ro.getPR(version, pkgarch, checksum0), "3") # "no history" mode
357 self.assertEqual(self.client_ro.getPR(version, pkgarch, checksum0, True), "0") # "history" mode
358 self.assertEqual(self.client_ro.getPR(version, pkgarch, checksum3), "3")
359 self.assertEqual(self.client_ro.getPR(version, pkgarch, checksum3, True), "0.3.0")
360 self.assertEqual(self.client_ro.max_package_pr(version, pkgarch), "2") # normal as "3" was never saved
361
362 # Try to insert a new value. Here this one is know upstream.
363 self.assertEqual(self.client_ro.getPR(version, pkgarch, checksum7), "3")
364 # Try to insert a completely new value. As the max upstream value is already "3", it should be "3.0"
365 self.assertEqual(self.client_ro.getPR(version, pkgarch, checksum10), "3.0")
366 # Same with another value which only exists in the upstream upstream server
367 # This time, as the upstream server doesn't know it, it will ask its upstream server. So that's a known one.
368 self.assertEqual(self.client_ro.getPR(version, pkgarch, checksum9), "3")
369
370class ScriptTests(unittest.TestCase):
371
372 def setUp(self):
373
374 self.temp_dir = tempfile.TemporaryDirectory(prefix='bb-prserv')
375 self.addCleanup(self.temp_dir.cleanup)
376 self.dbfile = os.path.join(self.temp_dir.name, "prtest.sqlite3")
377
378 def test_1_start_bitbake_prserv(self):
379 try:
380 subprocess.check_call([BIN_DIR / "bitbake-prserv", "--start", "-f", self.dbfile])
381 except subprocess.CalledProcessError as e:
382 self.fail("Failed to start bitbake-prserv: %s" % e.returncode)
383
384 def test_2_stop_bitbake_prserv(self):
385 try:
386 subprocess.check_call([BIN_DIR / "bitbake-prserv", "--stop"])
387 except subprocess.CalledProcessError as e:
388 self.fail("Failed to stop bitbake-prserv: %s" % e.returncode)