Merge lp:~fginther/auto-package-testing/remove-boottestjob into lp:~canonical-ci-engineering/auto-package-testing/add-boottest-requests
- remove-boottestjob
- Merge into add-boottest-requests
Proposed by
Francis Ginther
Status: | Merged |
---|---|
Merged at revision: | 390 |
Proposed branch: | lp:~fginther/auto-package-testing/remove-boottestjob |
Merge into: | lp:~canonical-ci-engineering/auto-package-testing/add-boottest-requests |
Diff against target: |
541 lines (+40/-461) 3 files modified
jenkins/adtjob.py (+33/-26) jenkins/boottest-britney (+7/-5) jenkins/boottestjob.py (+0/-430) |
To merge this branch: | bzr merge lp:~fginther/auto-package-testing/remove-boottestjob |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Canonical CI Engineering | Pending | ||
Review via email: mp+248047@code.launchpad.net |
Commit message
Remove boottestjob.py, replacing differences with the boottest flag.
Description of the change
Remove boottestjob.py, replacing differences with the boottest flag.
To post a comment you must log in.
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'jenkins/adtjob.py' |
2 | --- jenkins/adtjob.py 2015-01-22 19:12:09 +0000 |
3 | +++ jenkins/adtjob.py 2015-01-29 20:22:15 +0000 |
4 | @@ -219,32 +219,39 @@ |
5 | causes[self.pkgname] = version |
6 | ret = True |
7 | |
8 | - # Verification of the dependencies |
9 | - # Run a test if: |
10 | - # - No package list is specified and |
11 | - # - this is a new dependency |
12 | - # - or version of a dependency in the archive is newer than previous run |
13 | - # - Or if a package list is specified the following conditions must |
14 | - # also be met: |
15 | - # - and the dependency is in the list |
16 | - # - the version in the archive is newer or equal to the version requested |
17 | - # |
18 | - # Note that request files use source package names while dependency |
19 | - # lists returned by get_package_info use binary package names |
20 | - for depname, depver in depends.iteritems(): |
21 | - depsrcname = self._get_sourcename(depname) |
22 | - if (not pkglist or |
23 | - (pkglist and depsrcname in pkglist.keys() and |
24 | - apt_pkg.version_compare(pkglist[depsrcname]['version'], depver) <= 0)): |
25 | - if not depname in self.depends: |
26 | - logging.info("== New dependency '%s'.", depname) |
27 | - causes[depsrcname] = depver |
28 | - ret = True |
29 | - elif apt_pkg.version_compare(self.depends[depname], depver) < 0: |
30 | - logging.info("== New version of dependency '%s %s'.", |
31 | - depname, depver) |
32 | - causes[depsrcname] = depver |
33 | - ret = True |
34 | + if self.boottest: |
35 | + # Verification of the dependencies |
36 | + # Run a test if: |
37 | + # - No package list is specified and |
38 | + # - this is a new dependency |
39 | + # - or version of a dependency in the archive is newer than previous run |
40 | + # - Or if a package list is specified the following conditions must |
41 | + # also be met: |
42 | + # - and the dependency is in the list |
43 | + # - the version in the archive is newer or equal to the version requested |
44 | + # |
45 | + # Note that request files use source package names while dependency |
46 | + # lists returned by get_package_info use binary package names |
47 | + # XXX - fginther 20150129 |
48 | + # Skip this for boottesting because the reverse dependences |
49 | + # will almost always already be a part of the image being tested |
50 | + # The only exception is when the reverse dependence is also in |
51 | + # the proposed pocket. We'll just acknowledge that these are |
52 | + # skipped for now. |
53 | + for depname, depver in depends.iteritems(): |
54 | + depsrcname = self._get_sourcename(depname) |
55 | + if (not pkglist or |
56 | + (pkglist and depsrcname in pkglist.keys() and |
57 | + apt_pkg.version_compare(pkglist[depsrcname]['version'], depver) <= 0)): |
58 | + if not depname in self.depends: |
59 | + logging.info("== New dependency '%s'.", depname) |
60 | + causes[depsrcname] = depver |
61 | + ret = True |
62 | + elif apt_pkg.version_compare(self.depends[depname], depver) < 0: |
63 | + logging.info("== New version of dependency '%s %s'.", |
64 | + depname, depver) |
65 | + causes[depsrcname] = depver |
66 | + ret = True |
67 | |
68 | if ret: |
69 | self.causes = causes |
70 | |
71 | === modified file 'jenkins/boottest-britney' |
72 | --- jenkins/boottest-britney 2015-01-29 01:55:34 +0000 |
73 | +++ jenkins/boottest-britney 2015-01-29 20:22:15 +0000 |
74 | @@ -27,7 +27,7 @@ |
75 | import subprocess |
76 | import yaml |
77 | import argparse |
78 | -from boottestjob import BoottestJob |
79 | +from adtjob import AdtJob |
80 | from aptcache import AptCache |
81 | from time import strftime |
82 | from shutil import copyfile |
83 | @@ -321,8 +321,9 @@ |
84 | deps_path = self.status_pattern % ( |
85 | self.config['release'], self.pocket, |
86 | self.config['arch'], pkgname) |
87 | - job = BoottestJob(self.cache, deps_path, |
88 | - use_proposed=self.args.use_proposed) |
89 | + job = AdtJob(self.cache, deps_path, |
90 | + use_proposed=self.args.use_proposed, |
91 | + boottest=True) |
92 | if not job.package: |
93 | job.release = self.config['release'] |
94 | job.pkgname = pkgname |
95 | @@ -368,8 +369,9 @@ |
96 | deps_path = self.status_pattern % ( |
97 | self.config['release'], self.pocket, |
98 | self.config['arch'], pkgname) |
99 | - job = BoottestJob(self.cache, deps_path, |
100 | - use_proposed=self.args.use_proposed) |
101 | + job = AdtJob(self.cache, deps_path, |
102 | + use_proposed=self.args.use_proposed, |
103 | + boottest=True) |
104 | if ('causes' in pkgprops and |
105 | pkgname in pkgprops['causes'] and |
106 | pkgprops['causes'][pkgname] is None and job.version): |
107 | |
108 | === removed file 'jenkins/boottestjob.py' |
109 | --- jenkins/boottestjob.py 2015-01-29 01:55:34 +0000 |
110 | +++ jenkins/boottestjob.py 1970-01-01 00:00:00 +0000 |
111 | @@ -1,430 +0,0 @@ |
112 | -#! /usr/bin/python |
113 | -""" Interface between britney and autopkgtest running on Jenkins |
114 | - |
115 | -""" |
116 | -# Copyright (C) 2012, Canonical Ltd (http://www.canonical.com/) |
117 | -# |
118 | -# Author: Jean-Baptiste Lallement <jean-baptiste.lallement@canonical.com> |
119 | -# |
120 | -# This software is free software: you can redistribute it |
121 | -# and/or modify it under the terms of the GNU General Public License |
122 | -# as published by the Free Software Foundation, either version 3 of |
123 | -# the License, or (at your option) any later version. |
124 | -# |
125 | -# This software is distributed in the hope that it will |
126 | -# be useful, but WITHOUT ANY WARRANTY; without even the implied warranty |
127 | -# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
128 | -# GNU General Public License for more details. |
129 | -# |
130 | -# You should have received a copy of the GNU General Public License |
131 | -# along with this software. If not, see <http://www.gnu.org/licenses/>. |
132 | -# |
133 | -import logging |
134 | -import os |
135 | -import sys |
136 | -import json |
137 | -import apt_pkg |
138 | -import subprocess |
139 | -import yaml |
140 | -from xml.sax.saxutils import escape |
141 | - |
142 | -if os.path.exists(os.path.join(os.path.dirname(__file__), "../.bzr")): |
143 | - sys.path.insert(0, os.path.dirname(__file__)) |
144 | -from adtnotify import AdtNotify |
145 | - |
146 | -try: |
147 | - import jenkins |
148 | -except ImportError: |
149 | - sys.stderr.write('python-jenkins is not installed. Jenkins support is ' |
150 | - 'disabled\n') |
151 | -try: |
152 | - import jinja2 |
153 | -except ImportError: |
154 | - sys.stderr.write('python-jinja2 is not installed. Jinja support is ' |
155 | - 'disabled\n') |
156 | -from urllib2 import urlopen |
157 | - |
158 | - |
159 | -ARCHS = ('i386', 'amd64', 'all') |
160 | -BINDIR = os.path.dirname(__file__) |
161 | -JENKINS_TMPL = 'jenkins_config.xml.tmpl' |
162 | -JENKINS_TMPL_ARMHF = 'jenkins_config_armhf.xml.tmpl' |
163 | -JENKINS_TMPL_PPC64EL = 'jenkins_config_ppc64el.xml.tmpl' |
164 | -DEFAULT_RECIPIENTS = ["ubuntu-testing-notifications@lists.ubuntu.com", |
165 | - "jean-baptiste.lallement@canonical.com", |
166 | - "martin.pitt@ubuntu.com"] |
167 | - |
168 | - |
169 | -class BoottestJob(object): |
170 | - '''Class that manages jobs''' |
171 | - def __init__(self, cache, state_path, use_proposed=False, credfile=None): |
172 | - global ARCHS |
173 | - |
174 | - self.job_status = { |
175 | - 'status': dict([(arch, None) for arch in ARCHS]), |
176 | - 'release': None, |
177 | - 'package': None, |
178 | - 'version': None, |
179 | - 'depends': {}, # package:version |
180 | - 'causes': {}, # package:version |
181 | - } |
182 | - |
183 | - self.state_path = state_path |
184 | - self.cache = cache |
185 | - |
186 | - self.status = dict([(arch, None) for arch in ARCHS]) |
187 | - self.release = None |
188 | - self.package = None |
189 | - self.pkgname = None |
190 | - self.version = None |
191 | - self.depends = {} |
192 | - self.causes = {} |
193 | - |
194 | - self.use_proposed = use_proposed |
195 | - self.load_status(state_path) |
196 | - |
197 | - if credfile: |
198 | - self.credentials = self.load_jenkins_credentials(credfile) |
199 | - if self.credentials: |
200 | - # "http://10.189.74.2:8080/job/%s%s-adt-%s/build?token=TOKEN" |
201 | - self.jenkins_url = "%s/job/%%s/build?token=%s" \ |
202 | - % (self.credentials['url'], self.credentials['token']) |
203 | - |
204 | - def load_jenkins_credentials(self, path): |
205 | - """ Load Credentials from credentials configuration file """ |
206 | - if not os.path.exists(path): |
207 | - return False |
208 | - |
209 | - logging.debug('Loading credentials from %s', path) |
210 | - cred = yaml.load(file(path, 'r')) |
211 | - return False if not 'jenkins' in cred else cred['jenkins'] |
212 | - |
213 | - def load_status(self, state_path): |
214 | - ''' Loads status of a job from configuration file''' |
215 | - logging.debug("Config path: '%s'", state_path) |
216 | - if not os.path.exists(state_path): |
217 | - logging.debug("'%s' doesn't exist", state_path) |
218 | - else: |
219 | - logging.debug("Loading current status") |
220 | - with open(state_path, 'r') as fp: |
221 | - self.job_status = json.load(fp) |
222 | - |
223 | - self.status = self.job_status['status'] |
224 | - self.release = self.job_status['release'] |
225 | - self.pkgname = self.job_status['package'] |
226 | - self.package = apt_pkg.SourceRecords() |
227 | - self.package.restart() |
228 | - if not self.package.lookup(self.pkgname): |
229 | - logging.warning('No source package named: %s' % self.pkgname) |
230 | - self.version = self.job_status['version'] |
231 | - self.depends = self.job_status['depends'] |
232 | - if 'causes' in self.job_status: |
233 | - self.causes = self.job_status['causes'] |
234 | - |
235 | - def write_status(self): |
236 | - '''Write status to a file''' |
237 | - with open(self.state_path, 'w') as fp: |
238 | - logging.debug("Writing status file to '%s'", self.state_path) |
239 | - self.job_status = { |
240 | - 'status': self.status, |
241 | - 'release': self.release, |
242 | - 'package': self.pkgname, |
243 | - 'version': self.version, |
244 | - 'depends': self.depends, |
245 | - 'causes': self.causes, |
246 | - } |
247 | - json.dump(self.job_status, fp) |
248 | - |
249 | - def update_status(self): |
250 | - '''Update package information''' |
251 | - logging.debug('Updating job status') |
252 | - if not self.status: |
253 | - self.status = dict([(arch, 'NEEDRUN') for arch in ARCHS]) |
254 | - |
255 | - (self.version, pocket, self.depends) = self.get_package_info() |
256 | - |
257 | - def get_package_info(self): |
258 | - '''Get package info for the newest version available in the |
259 | - repository from apt |
260 | - |
261 | - :return: version, pocket, binary dependencies |
262 | - ''' |
263 | - pkg = apt_pkg.SourceRecords() |
264 | - pkg.restart() |
265 | - |
266 | - pocket = None |
267 | - version = None |
268 | - depends = None |
269 | - |
270 | - while pkg.lookup(self.pkgname): |
271 | - if not version or apt_pkg.version_compare(version, pkg.version) < 0: |
272 | - pocket = pkg.index.describe.split(' ', 4)[1] |
273 | - version = pkg.version |
274 | - binaries = pkg.binaries |
275 | - |
276 | - if version: |
277 | - depends = self.binaries_depends(binaries) |
278 | - return (version, pocket, depends) |
279 | - |
280 | - def run_required(self, pkglist={}): |
281 | - '''Return True if the job must run |
282 | - |
283 | - Conditions are: |
284 | - - No version for the package (never ran before) |
285 | - - Version of the package changed |
286 | - - Version of a dependency changed |
287 | - |
288 | - @pkglist: If specified the conditions to run a job are: |
289 | - - package is in the list and no test has been run before (no |
290 | - version) |
291 | - - package is in the list and the version is higher than previous run |
292 | - - package is not in the list but a dependency is and the version is |
293 | - higher than previous run or package has never been tested |
294 | - ''' |
295 | - logging.info('Checking status for: %s %s', self.release, self.pkgname) |
296 | - # Find the newest package in enabled repositories |
297 | - (version, pocket, depends) = self.get_package_info() |
298 | - |
299 | - if not version: |
300 | - logging.warning("Source package not found: %s", self.pkgname) |
301 | - return False |
302 | - |
303 | - logging.debug('Found package version: %s %s', version, pocket) |
304 | - |
305 | - # Never ran the test before |
306 | - # If a package list is specified the package must be in the list |
307 | - if (not self.version and |
308 | - (not pkglist or |
309 | - (pkglist and self.pkgname in pkglist.keys()))): |
310 | - logging.info("== New package '%s %s'", self.pkgname, version) |
311 | - self.causes[self.pkgname] = version |
312 | - return True |
313 | - |
314 | - # New version of the package |
315 | - # Trigger a test if the version in the archive is newer than last run |
316 | - # Or if a package list is specified, the version in the archive must be |
317 | - # new or equal to the version requested which must be newer than the |
318 | - # last version of the package that have been tested |
319 | - ret = False |
320 | - causes = {} |
321 | - |
322 | - if (self.version is not None and apt_pkg.version_compare(self.version, version) < 0 and |
323 | - (not pkglist or |
324 | - (pkglist and self.pkgname in pkglist.keys() and |
325 | - apt_pkg.version_compare(pkglist[self.pkgname]['version'], version) <= 0))): |
326 | - logging.info("== New version of package '%s %s'", |
327 | - self.pkgname, version) |
328 | - causes[self.pkgname] = version |
329 | - ret = True |
330 | - |
331 | - if ret: |
332 | - self.causes = causes |
333 | - else: |
334 | - logging.info('Same version. Skipped') |
335 | - return ret |
336 | - |
337 | - def _get_sourcename(self, pkgname): |
338 | - '''Returns a source package name |
339 | - |
340 | - :param pkgname: Package name which we want to find source name''' |
341 | - |
342 | - try: |
343 | - pkg = self.cache[pkgname] |
344 | - return pkg.candidate.source_name |
345 | - except: |
346 | - logging.error('Unexpected error: %s', sys.exc_info()[0]) |
347 | - return pkgname |
348 | - |
349 | - def submit(self, dest, force=False): |
350 | - '''Run a job is needed |
351 | - |
352 | - :param force: Force run even if there is no dependency update |
353 | - ''' |
354 | - if force or self.run_required(): |
355 | - logging.debug('Starting job') |
356 | - self.status = dict([(arch, 'RUNNING') for arch in ARCHS]) |
357 | - self.update_status() |
358 | - self.write_status() |
359 | - logging.info('Triggering remote job for package %s', self.pkgname) |
360 | - cmd = ['rsync', '-a', self.state_path, dest] |
361 | - logging.debug('Running %s', cmd) |
362 | - try: |
363 | - subprocess.check_call(cmd) |
364 | - except subprocess.CalledProcessError as exc: |
365 | - logging.error('Command failed: %s', exc) |
366 | - return False |
367 | - return True |
368 | - |
369 | - def binaries_depends(self, binaries): |
370 | - ''' |
371 | - Return a dict with the list of dependencies for all the binary |
372 | - packages built from this source |
373 | - ''' |
374 | - depends = {} |
375 | - if not self.pkgname: |
376 | - return None |
377 | - |
378 | - virtpkgs = [] |
379 | - for pkgname in binaries: |
380 | - try: |
381 | - pkgrec = self.cache[pkgname] |
382 | - for dep in pkgrec.candidate.dependencies: |
383 | - for basedep in dep.or_dependencies: |
384 | - try: |
385 | - if self.cache.is_virtual_package(basedep.name): |
386 | - virtpkgs.append(basedep) |
387 | - else: |
388 | - basedeprec = self.cache[basedep.name] |
389 | - depends[basedeprec.name] = \ |
390 | - basedeprec.candidate.version |
391 | - except KeyError, exc: |
392 | - logging.warning(exc) |
393 | - |
394 | - except KeyError, exc: |
395 | - logging.warning(exc) |
396 | - |
397 | - # Process virtual packages |
398 | - # providing packages are only added if there is only 1 package providing |
399 | - # this virtual dependency (for dependency on a version of an ABI) and |
400 | - # if it is not already in the list of binary dependencies |
401 | - deps = set(depends.keys()) |
402 | - for vpkg in virtpkgs: |
403 | - ppkgs = self.cache.get_providing_packages(vpkg.name) |
404 | - if (len(ppkgs) > 1): |
405 | - continue |
406 | - vdeps = set([bpkg.name for bpkg in ppkgs]) |
407 | - if not deps.intersection(vdeps): |
408 | - # No providing package is in the list of dependencies |
409 | - logging.debug("Adding providing package to list of dependencies: '%s'", vdeps) |
410 | - for bpkg in ppkgs: |
411 | - depends[bpkg.name] = bpkg.candidate.version |
412 | - logging.debug('Dependency list: %s', depends) |
413 | - return depends |
414 | - |
415 | - def execute(self, update=False, dryrun=False): |
416 | - '''Execute a jenkins job''' |
417 | - logging.debug('Executing job') |
418 | - |
419 | - # Creates job from template if it doesn't exist |
420 | - # or update it if it already exists |
421 | - # Executes it |
422 | - if not os.path.exists(os.path.join(BINDIR, JENKINS_TMPL)): |
423 | - logging.warning('Template file doesn\'t exist: %s', JENKINS_TMPL) |
424 | - return False |
425 | - |
426 | - templates = jinja2.Environment(loader=jinja2.FileSystemLoader(BINDIR)) |
427 | - |
428 | - opts = '' # Args to pass to run-adt-test |
429 | - jobname = self.release |
430 | - if self.use_proposed: |
431 | - opts += '-P' |
432 | - jobname += '-adt-' + self.pkgname.replace('+', '-') |
433 | - jobname_armhf = jobname + '-armhf' |
434 | - jobname_ppc64el = jobname + '-ppc64el' |
435 | - |
436 | - # Who should be notified |
437 | - notify = AdtNotify(package=self.pkgname, release=self.release) |
438 | - notify.collect() |
439 | - # Space separated list of recipients |
440 | - email_recipients = ",".join(DEFAULT_RECIPIENTS) |
441 | - email_subject = "$DEFAULT_SUBJECT" |
442 | - email_content = "$DEFAULT_CONTENT\n\n" |
443 | - |
444 | - for notification in notify.notifications: |
445 | - email_content += "%s %s uploaded on %s by %s <%s>\n" % notification |
446 | - if notification[4] is not None: |
447 | - email_recipients += ',%s' % notification[4] |
448 | - |
449 | - ctxt = {'release': self.release, |
450 | - 'release_url': "", |
451 | - 'test': self.pkgname, |
452 | - 'opts': opts, |
453 | - 'jobname': jobname, |
454 | - 'email_recipients': email_recipients, |
455 | - 'email_subject': escape(email_subject), |
456 | - 'email_content': escape(email_content) |
457 | - } |
458 | - |
459 | - logging.debug('Generating job: %s', jobname) |
460 | - tmpl = templates.get_template(JENKINS_TMPL) |
461 | - tmpl_armhf = templates.get_template(JENKINS_TMPL_ARMHF) |
462 | - tmpl_ppc64el = templates.get_template(JENKINS_TMPL_PPC64EL) |
463 | - config = tmpl.render(ctxt) |
464 | - config_armhf = tmpl_armhf.render(ctxt) |
465 | - config_ppc64el = tmpl_ppc64el.render(ctxt) |
466 | - self.__update_jenkins_job(jobname_armhf, config_armhf, update, dryrun) |
467 | - self.__update_jenkins_job(jobname_ppc64el, config_ppc64el, update, dryrun) |
468 | - if not self.__update_jenkins_job(jobname, config, update, dryrun): |
469 | - return False |
470 | - |
471 | - job_url = self.jenkins_url % jobname |
472 | - logging.debug('Job URL: %s', job_url) |
473 | - if not dryrun: |
474 | - urlopen(job_url) |
475 | - else: |
476 | - logging.warning('dry run enabled, job won\'t be executed') |
477 | - return True |
478 | - |
479 | - def __update_jenkins_job(self, jobname, jobconfig, update=False, |
480 | - dryrun=False): |
481 | - """Update a jenkins job""" |
482 | - settings = self.credentials |
483 | - if not settings['url']: |
484 | - logging.error("Please provide a URL to the jenkins instance.") |
485 | - sys.exit(1) |
486 | - |
487 | - if 'username' in settings: |
488 | - logging.debug('Logging to jenkins with user %s', |
489 | - settings['username']) |
490 | - jkh = jenkins.Jenkins(settings['url'], |
491 | - username=settings['username'], |
492 | - password=settings['password']) |
493 | - else: |
494 | - logging.debug('Logging to jenkins anonymously') |
495 | - jkh = jenkins.Jenkins(settings['url']) |
496 | - if not jkh.job_exists(jobname): |
497 | - logging.info("Creating Jenkins Job %s ", jobname) |
498 | - if dryrun: |
499 | - logging.warning('dry run enabled, job won\'t be created') |
500 | - else: |
501 | - jkh.create_job(jobname, jobconfig) |
502 | - else: |
503 | - # Do not requeue or reconfigure running/queued jobs |
504 | - try: |
505 | - job = jkh.get_job_info(jobname) |
506 | - if job['inQueue']: |
507 | - logging.debug("Job '%s' already queued. skipped!", jobname) |
508 | - return False |
509 | - elif job['color'].endswith('_anime'): |
510 | - logging.debug("Job '%s' already running. skipped!", jobname) |
511 | - return False |
512 | - except jenkins.JenkinsException as exc: |
513 | - logging.warning('get_job_info failed with exception: %s', exc) |
514 | - return False |
515 | - if update: |
516 | - logging.info("Reconfiguring Jenkins Job %s ", jobname) |
517 | - if dryrun: |
518 | - logging.warning('dry run enabled, job won\'t be ' |
519 | - 'updated') |
520 | - else: |
521 | - jkh.reconfig_job(jobname, jobconfig) |
522 | - else: |
523 | - logging.debug('update set to %s. Skipping reconfiguration ' |
524 | - 'of %s', update, jobname) |
525 | - return True |
526 | - |
527 | - def is_newer_dependency(self, pkgname, pkgversion): |
528 | - ''' Returns true if package in argument is newer than one of the |
529 | - dependency |
530 | - ''' |
531 | - if not self.depends: |
532 | - return True |
533 | - |
534 | - for depname, depversion in self.depends.iteritems(): |
535 | - srcname = self._get_sourcename(depname) |
536 | - if srcname == pkgname: |
537 | - if apt_pkg.version_compare(depversion, pkgversion) < 0: |
538 | - logging.debug('New dependency') |
539 | - return True |
540 | - |
541 | - return False |