diff --git a/.arclint b/.arclint --- a/.arclint +++ b/.arclint @@ -16,24 +16,13 @@ "(^src/bench/nanobench.h$)" ] }, - "autopep8": { - "type": "autopep8", - "version": ">=1.3.4", - "include": "(^contrib/(buildbot)/.*\\.py$)", - "flags": [ - "--aggressive", - "--ignore=W503,W504", - "--max-line-length=88" - ] - }, "black": { "type": "black", "version": ">=23.0.0", "include": "(\\.py$)", "exclude": [ "(^contrib/gitian-builder/)", - "(^contrib/apple-sdk-tools/)", - "(^contrib/buildbot/)" + "(^contrib/apple-sdk-tools/)" ], "flags": [ "--preview" diff --git a/arcanist/__phutil_library_map__.php b/arcanist/__phutil_library_map__.php --- a/arcanist/__phutil_library_map__.php +++ b/arcanist/__phutil_library_map__.php @@ -13,7 +13,6 @@ 'ArcanistBitcoinABCConfiguration' => 'configuration/ArcanistBitcoinABCConfiguration.php', 'ArcanistLandBotWorkflow' => 'workflow/ArcanistLandBotWorkflow.php', 'AssertWithSideEffectsLinter' => 'linter/AssertWithSideEffectsLinter.php', - 'AutoPEP8FormatLinter' => 'linter/AutoPEP8Linter.php', 'BashShebangLinter' => 'linter/BashShebangLinter.php', 'BlackFormatLinter' => 'linter/BlackLinter.php', 'BoostDependenciesLinter' => 'linter/BoostDependenciesLinter.php', @@ -62,7 +61,6 @@ 'ArcanistBitcoinABCConfiguration' => 'ArcanistConfiguration', 'ArcanistLandBotWorkflow' => 'ArcanistWorkflow', 'AssertWithSideEffectsLinter' => 'ArcanistLinter', - 'AutoPEP8FormatLinter' => 'ArcanistExternalLinter', 'BashShebangLinter' => 'ArcanistLinter', 'BlackFormatLinter' => 'ArcanistExternalLinter', 'BoostDependenciesLinter' => 'AbstractGlobalExternalLinter', diff --git a/arcanist/linter/AutoPEP8Linter.php b/arcanist/linter/AutoPEP8Linter.php deleted file mode 100644 --- a/arcanist/linter/AutoPEP8Linter.php +++ /dev/null @@ -1,99 +0,0 @@ -getExecutableCommand()); - $matches = array(); - - /* Support a.b or a.b.c version numbering scheme */ - $regex = '/^autopep8 (?P\d+\.\d+(?:\.\d+)?)/'; - - /* - * Old autopep8 output the version to stdout, newer output to stderr. - * Try both to determine the version. - */ - if (preg_match($regex, $stdout, $matches)) { - return $matches['version']; - } - if (preg_match($regex, $stderr, $matches)) { - return $matches['version']; - } - - return false; - } - - public function getInstallInstructions() { - return pht('Make sure autopep8 is in directory specified by $PATH'); - } - - public function shouldExpectCommandErrors() { - return false; - } - - protected function getMandatoryFlags() { - return array(); - } - - protected function parseLinterOutput($path, $err, $stdout, $stderr) { - $ok = ($err == 0); - - if (!$ok) { - return false; - } - - $root = $this->getProjectRoot(); - $path = Filesystem::resolvePath($path, $root); - $orig = file_get_contents($path); - if ($orig == $stdout) { - return array(); - } - - $message = id(new ArcanistLintMessage()) - ->setPath($path) - ->setLine(1) - ->setChar(1) - ->setGranularity(ArcanistLinter::GRANULARITY_FILE) - ->setCode('CFMT') - ->setSeverity(ArcanistLintSeverity::SEVERITY_AUTOFIX) - ->setName('Code style violation') - ->setDescription("'$path' has code style errors.") - ->setOriginalText($orig) - ->setReplacementText($stdout); - - return array($message); - } -} diff --git a/contrib/buildbot/abcbot.py b/contrib/buildbot/abcbot.py --- a/contrib/buildbot/abcbot.py +++ b/contrib/buildbot/abcbot.py @@ -23,20 +23,19 @@ db_file_no_ext = os.getenv("DATABASE_FILE_NO_EXT", None) tc_user = os.getenv("TEAMCITY_USERNAME", None) tc_pass = os.getenv("TEAMCITY_PASSWORD", None) -phabricatorUrl = os.getenv( - "PHABRICATOR_URL", "https://reviews.bitcoinabc.org/api/") -slack_token = os.getenv('SLACK_BOT_TOKEN', None) +phabricatorUrl = os.getenv("PHABRICATOR_URL", "https://reviews.bitcoinabc.org/api/") +slack_token = os.getenv("SLACK_BOT_TOKEN", None) -tc = TeamCity('https://build.bitcoinabc.org', tc_user, tc_pass) +tc = TeamCity("https://build.bitcoinabc.org", tc_user, tc_pass) phab = PhabWrapper(host=phabricatorUrl, token=conduit_token) phab.update_interfaces() slack_channels = { # #dev - 'dev': 'C62NSDC6N', + "dev": "C62NSDC6N", # #abcbot-testing - 'test': 'CQMSVCY66', + "test": "CQMSVCY66", # #infra-support - 'infra': 'G016CFAV8KS', + "infra": "G016CFAV8KS", } slackbot = SlackBot(slack.WebClient, slack_token, slack_channels) cirrus = Cirrus() @@ -44,24 +43,29 @@ def main(args): parser = argparse.ArgumentParser( - description='Continuous integration build bot service.') + description="Continuous integration build bot service." + ) parser.add_argument( - '-p', '--port', help='port for server to start', type=int, default=8080) + "-p", "--port", help="port for server to start", type=int, default=8080 + ) parser.add_argument( - '-l', '--log-file', help='log file to dump requests payload', type=str, default='log.log') + "-l", + "--log-file", + help="log file to dump requests payload", + type=str, + default="log.log", + ) args = parser.parse_args() port = args.port log_file = args.log_file app = server.create_server( - tc, - phab, - slackbot, - cirrus, - db_file_no_ext=db_file_no_ext) + tc, phab, slackbot, cirrus, db_file_no_ext=db_file_no_ext + ) formater = logging.Formatter( - '[%(asctime)s] %(levelname)s in %(module)s: %(message)s') + "[%(asctime)s] %(levelname)s in %(module)s: %(message)s" + ) fileHandler = RotatingFileHandler(log_file, maxBytes=10000, backupCount=1) fileHandler.setFormatter(formater) app.logger.addHandler(fileHandler) diff --git a/contrib/buildbot/build.py b/contrib/buildbot/build.py --- a/contrib/buildbot/build.py +++ b/contrib/buildbot/build.py @@ -33,9 +33,8 @@ def update_build_status(self, build_id, status): if build_id not in self.builds: raise AssertionError( - "Attempting to update the build id {} to status {} that does not belong to the build target {}".format( - build_id, status, self.phid - ) + "Attempting to update the build id {} to status {} that does not belong" + " to the build target {}".format(build_id, status, self.phid) ) self.builds[build_id].status = status @@ -56,5 +55,7 @@ return BuildStatus.Running def is_finished(self): - return all((build.status == BuildStatus.Success or build.status == - BuildStatus.Failure) for build in self.builds.values()) + return all( + (build.status == BuildStatus.Success or build.status == BuildStatus.Failure) + for build in self.builds.values() + ) diff --git a/contrib/buildbot/cirrus.py b/contrib/buildbot/cirrus.py --- a/contrib/buildbot/cirrus.py +++ b/contrib/buildbot/cirrus.py @@ -12,7 +12,7 @@ BITCOIN_ABC_SECP256K1_REPO_ID = "6034374039699456" -class Cirrus(): +class Cirrus: def __init__(self, base_url="https://api.cirrus-ci.com/graphql"): self.base_url = base_url self.logger = None @@ -31,11 +31,12 @@ }} """ - response = requests.post(self.base_url, json={'query': query}) + response = requests.post(self.base_url, json={"query": query}) if response.status_code != requests.codes.ok: raise AssertionError( - "Cirrus get_default_branch_status() failed for repository {}\nResponse:\n{}".format( + "Cirrus get_default_branch_status() failed for repository" + " {}\nResponse:\n{}".format( repo_id, vars(response), ) @@ -43,18 +44,33 @@ json_data = json.loads(response.content) - failure_status = ['FAILED', 'ABORTED', 'ERRORED'] - success_status = ['COMPLETED'] - running_status = ['EXECUTING'] - queued_status = ['CREATED', 'TRIGGERED'] + failure_status = ["FAILED", "ABORTED", "ERRORED"] + success_status = ["COMPLETED"] + running_status = ["EXECUTING"] + queued_status = ["CREATED", "TRIGGERED"] try: - status = json_data['data']['repository']['lastDefaultBranchBuild']['status'] or 'UNKNOWN' + status = ( + json_data["data"]["repository"]["lastDefaultBranchBuild"]["status"] + or "UNKNOWN" + ) except KeyError: - status = 'UNKNOWN' + status = "UNKNOWN" - return (BuildStatus.Success if status in success_status else - BuildStatus.Failure if status in failure_status else - BuildStatus.Running if status in running_status else - BuildStatus.Queued if status in queued_status else - BuildStatus.Unknown) + return ( + BuildStatus.Success + if status in success_status + else ( + BuildStatus.Failure + if status in failure_status + else ( + BuildStatus.Running + if status in running_status + else ( + BuildStatus.Queued + if status in queued_status + else BuildStatus.Unknown + ) + ) + ) + ) diff --git a/contrib/buildbot/phabricator_wrapper.py b/contrib/buildbot/phabricator_wrapper.py --- a/contrib/buildbot/phabricator_wrapper.py +++ b/contrib/buildbot/phabricator_wrapper.py @@ -21,10 +21,7 @@ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.logger = None - self.deployment = Deployment( - os.getenv( - "DEPLOYMENT_ENV", - Deployment.DEV)) + self.deployment = Deployment(os.getenv("DEPLOYMENT_ENV", Deployment.DEV)) self.phid = None self.file_cache = {} @@ -56,10 +53,13 @@ }, } data_list = self.differential.diff.search(**diffSearchArgs).data - assert len(data_list) == 1, "differential.diff.search({}): Expected 1 diff, got: {}".format( - diffSearchArgs, data_list) + assert ( + len(data_list) == 1 + ), "differential.diff.search({}): Expected 1 diff, got: {}".format( + diffSearchArgs, data_list + ) diffdata = data_list[0] - revisionPHID = diffdata['fields']['revisionPHID'] + revisionPHID = diffdata["fields"]["revisionPHID"] return revisionPHID def get_revision_info(self, revisionPHID): @@ -68,35 +68,41 @@ "phids": [revisionPHID], }, } - data_list = self.differential.revision.search( - **revisionSearchArgs).data - assert len(data_list) == 1, "differential.revision.search({}): Expected 1 revision, got: {}".format( - revisionSearchArgs, data_list) + data_list = self.differential.revision.search(**revisionSearchArgs).data + assert ( + len(data_list) == 1 + ), "differential.revision.search({}): Expected 1 revision, got: {}".format( + revisionSearchArgs, data_list + ) diffdata = data_list[0] - revisionId = diffdata['id'] - authorPHID = diffdata['fields']['authorPHID'] + revisionId = diffdata["id"] + authorPHID = diffdata["fields"]["authorPHID"] return revisionId, authorPHID def getRevisionAuthor(self, revisionId): # Fetch revision revisionSearchArgs = { "constraints": { - "ids": [int(revisionId.strip('D'))], + "ids": [int(revisionId.strip("D"))], }, } rev_list = self.differential.revision.search(**revisionSearchArgs).data - assert len(rev_list) == 1, "differential.revision.search({}): Expected 1 revision, got: {}".format( - revisionSearchArgs, rev_list) + assert ( + len(rev_list) == 1 + ), "differential.revision.search({}): Expected 1 revision, got: {}".format( + revisionSearchArgs, rev_list + ) # Fetch revision author userSearchArgs = { "constraints": { - "phids": [rev_list[0]['fields']['authorPHID']], + "phids": [rev_list[0]["fields"]["authorPHID"]], }, } author_list = self.user.search(**userSearchArgs).data - assert len(author_list) == 1, "user.search({}): Expected 1 user, got: {}".format( - userSearchArgs, author_list) + assert ( + len(author_list) == 1 + ), f"user.search({userSearchArgs}): Expected 1 user, got: {author_list}" return author_list[0] def getRevisionPHIDsFromCommits(self, commitHashes): @@ -109,12 +115,15 @@ } commits = self.diffusion.commit.search(**commitSearchArgs).data expectedNumCommits = len(commitHashes) - assert len(commits) == expectedNumCommits, "diffusion.commit.search({}): Expected {} commits, got: {}".format( - expectedNumCommits, commitSearchArgs, commits) + assert ( + len(commits) == expectedNumCommits + ), "diffusion.commit.search({}): Expected {} commits, got: {}".format( + expectedNumCommits, commitSearchArgs, commits + ) # Attempt to get revisions for all commit objects (not all commits have # revisions) - commitPHIDs = [commit['phid'] for commit in commits] + commitPHIDs = [commit["phid"] for commit in commits] edgeSearchArgs = { "types": ["commit.revision"], @@ -124,12 +133,12 @@ m = {} for commit in commits: - commitHash = commit['fields']['identifier'] + commitHash = commit["fields"]["identifier"] m[commitHash] = None - for edge in (revisionEdges or {}): - if commit['phid'] == edge['sourcePHID']: - m[commitHash] = edge['destinationPHID'] + for edge in revisionEdges or {}: + if commit["phid"] == edge["sourcePHID"]: + m[commitHash] = edge["destinationPHID"] break return m @@ -138,11 +147,11 @@ # If slack-username is non-empty, use it. Otherwise default to the # author's Phabricator username authorSlackUsername = "" - if 'fields' in author: - if 'custom.abc:slack-username' in author['fields']: - authorSlackUsername = author['fields']['custom.abc:slack-username'] - if not authorSlackUsername and 'username' in author['fields']: - authorSlackUsername = author['fields']['username'] + if "fields" in author: + if "custom.abc:slack-username" in author["fields"]: + authorSlackUsername = author["fields"]["custom.abc:slack-username"] + if not authorSlackUsername and "username" in author["fields"]: + authorSlackUsername = author["fields"]["username"] return authorSlackUsername def decorateCommitMap(self, commitMapIn): @@ -154,14 +163,19 @@ }, } revs = self.differential.revision.search(**revisionSearchArgs).data - assert len(revs) == len(revisionPHIDs), "differential.revision.search({}): Expected {} revisions, got: {}".format( - revisionSearchArgs, len(revisionPHIDs), revs) + assert len(revs) == len( + revisionPHIDs + ), "differential.revision.search({}): Expected {} revisions, got: {}".format( + revisionSearchArgs, len(revisionPHIDs), revs + ) # Decorate revision authors - authorPHIDs = [rev['fields']['authorPHID'] for rev in revs] - authors = self.user.search(constraints={ - 'phids': authorPHIDs, - }).data + authorPHIDs = [rev["fields"]["authorPHID"] for rev in revs] + authors = self.user.search( + constraints={ + "phids": authorPHIDs, + } + ).data # Build map of decorated data commitMap = {} @@ -169,21 +183,23 @@ decoratedCommit = { # TODO: Find a better way to get the commit link from # Phabricator - 'link': f"https://reviews.bitcoinabc.org/rABC{commitHash}", + "link": f"https://reviews.bitcoinabc.org/rABC{commitHash}", } if revisionPHID: for rev in revs: - if revisionPHID == rev['phid']: - decoratedCommit['revision'] = rev - decoratedCommit['link'] = "https://reviews.bitcoinabc.org/D{}".format( - rev['id']) + if revisionPHID == rev["phid"]: + decoratedCommit["revision"] = rev + decoratedCommit["link"] = ( + f"https://reviews.bitcoinabc.org/D{rev['id']}" + ) break for author in authors: - if author['phid'] == rev['fields']['authorPHID']: - decoratedCommit['author'] = author - decoratedCommit['authorSlackUsername'] = self.getAuthorSlackUsername( - author) + if author["phid"] == rev["fields"]["authorPHID"]: + decoratedCommit["author"] = author + decoratedCommit["authorSlackUsername"] = ( + self.getAuthorSlackUsername(author) + ) break commitMap[commitHash] = decoratedCommit return commitMap @@ -203,34 +219,43 @@ return msg def commentOnRevision(self, revisionID, msg, buildName=""): - self.logger.info( - f"Comment on objectIdentifier '{revisionID}': '{msg}'") + self.logger.info(f"Comment on objectIdentifier '{revisionID}': '{msg}'") # Production build-bot posts live comments for builds that are not staging-specific # FIXME: Currently all builds kick off a completion hook in Teamcity. The bot doesn't # have a better mechanism for knowing if that build is high value (worth commenting on) # or low value (staging builds, etc.) to end users. Until there is a more streamlined # way to define Teamcity webhooks to exclude these builds, we are going to look at the # buildName for an ignore keyword. - if self.deployment == Deployment.PROD and BUILDNAME_IGNORE_KEYWORD not in buildName: - self.differential.revision.edit(transactions=[ - {"type": "comment", "value": msg} - ], objectIdentifier=revisionID) + if ( + self.deployment == Deployment.PROD + and BUILDNAME_IGNORE_KEYWORD not in buildName + ): + self.differential.revision.edit( + transactions=[{"type": "comment", "value": msg}], + objectIdentifier=revisionID, + ) else: self.logger.info( "Comment creation skipped due to deployment environment: '{}'".format( - self.deployment)) + self.deployment + ) + ) def getBrokenBuildTaskTitle(self, buildName): return f"Build {buildName} is broken." def getBrokenBuildTask(self, taskTitle): - response = self.maniphest.search(constraints={ - "query": f"\"{taskTitle}\"", - "statuses": ["open"], - }) + response = self.maniphest.search( + constraints={ + "query": f'"{taskTitle}"', + "statuses": ["open"], + } + ) self.logger.info( "Response from 'maniphest.search' querying for title '{}': {}".format( - taskTitle, response)) + taskTitle, response + ) + ) return response def updateBrokenBuildTaskStatus(self, buildName, status): @@ -238,58 +263,82 @@ task_data = self.getBrokenBuildTask(title).data if len(task_data) == 0: self.logger.info( - f"No existing broken build task with title '{title}'. Skipping.") + f"No existing broken build task with title '{title}'. Skipping." + ) return None self.logger.info( - f"Updating broken build task T{task_data[0]['id']} status to '{status}'.") - updatedTask = self.maniphest.edit(transactions=[{ - 'type': 'status', - 'value': status, - }], objectIdentifier=task_data[0]['phid']) + f"Updating broken build task T{task_data[0]['id']} status to '{status}'." + ) + updatedTask = self.maniphest.edit( + transactions=[ + { + "type": "status", + "value": status, + } + ], + objectIdentifier=task_data[0]["phid"], + ) self.logger.info( "Response from 'maniphest.edit' updating status to '{}': {}".format( - status, updatedTask)) - return updatedTask['object'] + status, updatedTask + ) + ) + return updatedTask["object"] def createBrokenBuildTask( - self, buildName, buildURL, branch, gitCommitsIn, repoCallsign): + self, buildName, buildURL, branch, gitCommitsIn, repoCallsign + ): gitCommits = [repoCallsign + commit for commit in gitCommitsIn] title = self.getBrokenBuildTaskTitle(buildName) res = self.getBrokenBuildTask(title) if len(res.data) != 0: self.logger.info( - "Open broken build task (T{}) exists. Skipping creation of a new one.".format( - res.data[0]['id'])) + "Open broken build task (T{}) exists. Skipping creation of a new one." + .format(res.data[0]["id"]) + ) return None - task_body = "[[ {} | {} ]] is broken on branch '{}'\n\nAssociated commits:\n{}".format( - buildURL, buildName, branch, "\n".join(gitCommits)) - newTask = self.maniphest.edit(transactions=[ - {"type": "title", "value": title}, - {"type": "priority", "value": "unbreak"}, - {"type": "description", "value": task_body} - ]) + task_body = ( + "[[ {} | {} ]] is broken on branch '{}'\n\nAssociated commits:\n{}".format( + buildURL, buildName, branch, "\n".join(gitCommits) + ) + ) + newTask = self.maniphest.edit( + transactions=[ + {"type": "title", "value": title}, + {"type": "priority", "value": "unbreak"}, + {"type": "description", "value": task_body}, + ] + ) self.logger.info( - "Response from 'maniphest.edit' creating new task with title '{}': {}".format( - title, newTask)) - return newTask['object'] + "Response from 'maniphest.edit' creating new task with title '{}': {}" + .format(title, newTask) + ) + return newTask["object"] def updateRevisionSummary(self, revisionId, summary): self.logger.info( - f"Updated summary on objectIdentifier '{revisionId}': '{summary}'") + f"Updated summary on objectIdentifier '{revisionId}': '{summary}'" + ) if self.deployment == Deployment.PROD: - self.differential.revision.edit(transactions=[{ - "type": "summary", - "value": summary, - }], objectIdentifier=revisionId) + self.differential.revision.edit( + transactions=[ + { + "type": "summary", + "value": summary, + } + ], + objectIdentifier=revisionId, + ) else: self.logger.info( - "Update of revision summary skipped due to deployment environment: '{}'".format( - self.deployment)) + "Update of revision summary skipped due to deployment environment: '{}'" + .format(self.deployment) + ) def get_project_members(self, project_PHID): - """ Return a list of user PHIDs corresponding to the ABC members """ + """Return a list of user PHIDs corresponding to the ABC members""" project_data = self.project.search( constraints={ "phids": [project_PHID], @@ -302,30 +351,32 @@ if len(project_data) != 1: self.logger.info( "Found {} project(s) while searching for Bitcoin ABC: '{}'".format( - len(project_data), project_data)) + len(project_data), project_data + ) + ) return [] - return [m["phid"] - for m in project_data[0]["attachments"]["members"]["members"]] + return [m["phid"] for m in project_data[0]["attachments"]["members"]["members"]] def get_latest_diff_staging_ref(self, revision_PHID): diff_data = self.differential.diff.search( constraints={ "revisionPHIDs": [revision_PHID], }, - order="newest" + order="newest", ).data if not diff_data: self.logger.info( - f"Failed to retrieve diff data from revision {revision_PHID}") + f"Failed to retrieve diff data from revision {revision_PHID}" + ) return "" # FIXME don't hardcode the staging branch mechanism return f"refs/tags/phabricator/diff/{diff_data[0]['id']}" def get_user_roles(self, user_PHID): - """ Return a list of the user roles for the target user PHID """ + """Return a list of the user roles for the target user PHID""" user_data = self.user.search( constraints={ "phids": [user_PHID], @@ -338,9 +389,7 @@ if len(user_data) != 1: self.logger.info( "Found {} user(s) while searching for {}: '{}'".format( - len(user_data), - user_PHID, - user_data + len(user_data), user_PHID, user_data ) ) return [] @@ -357,28 +406,33 @@ if not commit_data: raise AssertionError( - f"Failed to get last master commit for repository {BITCOIN_ABC_REPO}") + f"Failed to get last master commit for repository {BITCOIN_ABC_REPO}" + ) return commit_data[0]["fields"]["identifier"] def get_revision_changed_files(self, revision_id): - return list(self.differential.getcommitpaths( - revision_id=int(revision_id))) + return list(self.differential.getcommitpaths(revision_id=int(revision_id))) def get_file_content_from_master(self, path): latest_commit_hash = self.get_latest_master_commit_hash() # Level 1 cache: check if the file is cached from the same commit - if path in self.file_cache and self.file_cache[path]['commit'] == latest_commit_hash: - return self.file_cache[path]['content'] + if ( + path in self.file_cache + and self.file_cache[path]["commit"] == latest_commit_hash + ): + return self.file_cache[path]["content"] def file_not_found(data): - raise AssertionError("File {} not found in master commit {} for repository {}:\n{}".format( - path, - latest_commit_hash, - BITCOIN_ABC_REPO, - data, - )) + raise AssertionError( + "File {} not found in master commit {} for repository {}:\n{}".format( + path, + latest_commit_hash, + BITCOIN_ABC_REPO, + data, + ) + ) # Browse the parent directory to extract the file hash. # Use a Diffusion browsequery on the parent directory because the @@ -389,29 +443,29 @@ # searched path. There is an exception for the root directory for which # the '/' path is invalid and will throw an error. browse_data = self.diffusion.browsequery( - path=os.path.join(os.path.dirname(path), '') or None, + path=os.path.join(os.path.dirname(path), "") or None, commit=latest_commit_hash, repository=BITCOIN_ABC_REPO, branch="master", ) # No file in the directory - if not browse_data or 'paths' not in browse_data: + if not browse_data or "paths" not in browse_data: file_not_found("diffusion.browsequery returned no path data") # Loop over the directory content to find our file file_hash = None - for file in browse_data['paths']: - if file['fullPath'] == path: - file_hash = file['hash'] + for file in browse_data["paths"]: + if file["fullPath"] == path: + file_hash = file["hash"] # File not found in it's directory if not file_hash: file_not_found(browse_data) # Level 2 cache: check if the file did not change since last download - if path in self.file_cache and self.file_cache[path]['hash'] == file_hash: - return self.file_cache[path]['content'] + if path in self.file_cache and self.file_cache[path]["hash"] == file_hash: + return self.file_cache[path]["content"] # Limit to 5s or 1MB download file_data = self.diffusion.filecontentquery( @@ -427,54 +481,51 @@ file_not_found(file_data) if file_data["tooSlow"] or file_data["tooHuge"]: - raise AssertionError("File {} from commit {} for repository {} is oversized or took too long to download: {}".format( - path, - latest_commit_hash, - BITCOIN_ABC_REPO, - file_data, - )) + raise AssertionError( + "File {} from commit {} for repository {} is oversized or took too long" + " to download: {}".format( + path, + latest_commit_hash, + BITCOIN_ABC_REPO, + file_data, + ) + ) - file_content = self.file.download( - phid=file_data["filePHID"] - ).response + file_content = self.file.download(phid=file_data["filePHID"]).response if not file_content: - self.logger.info("File {} appear to be empty in commit {} for repository {}".format( - file_data["filePHID"], - latest_commit_hash, - BITCOIN_ABC_REPO, - )) - - self.file_cache.update({ - path: { - 'commit': latest_commit_hash, - 'hash': file_hash, - 'content': b64decode(file_content), + self.logger.info( + "File {} appear to be empty in commit {} for repository {}".format( + file_data["filePHID"], + latest_commit_hash, + BITCOIN_ABC_REPO, + ) + ) + + self.file_cache.update( + { + path: { + "commit": latest_commit_hash, + "hash": file_hash, + "content": b64decode(file_content), + } } - }) + ) - return self.file_cache[path]['content'] + return self.file_cache[path]["content"] def set_text_panel_content(self, panel_id, content): response = self.dashboard.panel.edit( - objectIdentifier=panel_id, - transactions=[ - { - "type": "text", - "value": content - } - ] + objectIdentifier=panel_id, transactions=[{"type": "text", "value": content}] ) if response.get("error", None): raise AssertionError( - "Failed to edit panel {} with content:\n{}\n\nPhabricator responded:\n{}\n".format( - panel_id, content, response - ) + "Failed to edit panel {} with content:\n{}\n\nPhabricator" + " responded:\n{}\n".format(panel_id, content, response) ) - def update_build_target_status( - self, build_target, build_id=None, status=None): + def update_build_target_status(self, build_target, build_id=None, status=None): harbormaster_build_status_mapping = { BuildStatus.Queued: "work", BuildStatus.Running: "work", @@ -487,11 +538,11 @@ self.harbormaster.sendmessage( receiver=build_target.phid, - type=harbormaster_build_status_mapping[build_target.status()] + type=harbormaster_build_status_mapping[build_target.status()], ) def get_object_token(self, object_PHID): - """ Return the current token set by the current user on target object """ + """Return the current token set by the current user on target object""" tokens = self.token.given( authorPHIDs=[self.get_current_user_phid()], objectPHIDs=[object_PHID], @@ -516,7 +567,7 @@ return tokens[0]["tokenPHID"] def set_object_token(self, object_PHID, token_PHID=None): - """ Award or rescind a token for the target object """ + """Award or rescind a token for the target object""" # If no token is given, rescind any previously awarded token if token_PHID is None: token_PHID = "" diff --git a/contrib/buildbot/server.py b/contrib/buildbot/server.py --- a/contrib/buildbot/server.py +++ b/contrib/buildbot/server.py @@ -40,19 +40,12 @@ # logo='data:image/png;base64,{}'.format( # icon.read().strip().decode('utf-8')), # ) -BADGE_TC_BASE = RasterBadge( - label='TC build', - logo='TeamCity' -) +BADGE_TC_BASE = RasterBadge(label="TC build", logo="TeamCity") -BADGE_CIRRUS_BASE = RasterBadge( - label='Cirrus build', - logo='cirrus-ci' -) +BADGE_CIRRUS_BASE = RasterBadge(label="Cirrus build", logo="cirrus-ci") -def create_server(tc, phab, slackbot, cirrus, - db_file_no_ext=None, jsonProvider=None): +def create_server(tc, phab, slackbot, cirrus, db_file_no_ext=None, jsonProvider=None): # Create Flask app for use as decorator app = Flask("abcbot") app.logger.setLevel(logging.INFO) @@ -68,33 +61,34 @@ # Optionally persistable database create_server.db = { # A collection of the known build targets - 'diff_targets': {}, + "diff_targets": {}, # Build status panel data - 'panel_data': {}, + "panel_data": {}, # Whether the last status check of master was green - 'master_is_green': True, + "master_is_green": True, # Coverage panel data - 'coverage_data': {}, + "coverage_data": {}, } # If db_file_no_ext is not None, attempt to restore old database state if db_file_no_ext: app.logger.info( - f"Loading persisted state database with base name '{db_file_no_ext}'...") + f"Loading persisted state database with base name '{db_file_no_ext}'..." + ) try: - with shelve.open(db_file_no_ext, flag='r') as db: + with shelve.open(db_file_no_ext, flag="r") as db: for key in create_server.db.keys(): if key in db: create_server.db[key] = db[key] - app.logger.info( - f"Restored key '{key}' from persisted state") + app.logger.info(f"Restored key '{key}' from persisted state") except BaseException: app.logger.info( - f"Persisted state database with base name '{db_file_no_ext}' could not be opened. A new one will be created when written to.") + f"Persisted state database with base name '{db_file_no_ext}' could not" + " be opened. A new one will be created when written to." + ) app.logger.info("Done") else: - app.logger.warning( - "No database file specified. State will not be persisted.") + app.logger.warning("No database file specified. State will not be persisted.") def persistDatabase(fn): @wraps(fn) @@ -109,9 +103,11 @@ app.logger.debug("Persisted current state") else: app.logger.debug( - "No database file specified. Persisting state is being skipped.") + "No database file specified. Persisting state is being skipped." + ) return fn_ret + return decorated_function # This decorator specifies an HMAC secret environment variable to use for verifying @@ -126,24 +122,24 @@ secret = os.getenv(secret_env, None) if not secret: app.logger.info( - f"Error: HMAC env variable '{secret_env}' does not exist") + f"Error: HMAC env variable '{secret_env}' does not exist" + ) abort(401) data = request.get_data() - digest = hmac.new( - secret.encode(), data, hashlib.sha256).hexdigest() + digest = hmac.new(secret.encode(), data, hashlib.sha256).hexdigest() - hmac_header = request.headers.get( - 'X-Phabricator-Webhook-Signature') + hmac_header = request.headers.get("X-Phabricator-Webhook-Signature") if not hmac_header: abort(401) - if not hmac.compare_digest( - digest.encode(), hmac_header.encode()): + if not hmac.compare_digest(digest.encode(), hmac_header.encode()): abort(401) return fn(*args, **kwargs) + return decorated_function + return decorator def get_json_request_data(request): @@ -153,15 +149,18 @@ def get_master_build_configurations(): # Get the configuration from master - config = yaml.safe_load(phab.get_file_content_from_master( - "contrib/teamcity/build-configurations.yml")) + config = yaml.safe_load( + phab.get_file_content_from_master( + "contrib/teamcity/build-configurations.yml" + ) + ) # Get a list of the templates, if any templates = config.get("templates", {}) # Get a list of the builds build_configs = {} - for build_name, v in config.get('builds', {}).items(): + for build_name, v in config.get("builds", {}).items(): # Merge the templates template_config = {} template_names = v.get("templates", []) @@ -173,50 +172,55 @@ f"template {template_name}, but the template does not " "exist." ) - always_merger.merge( - template_config, templates.get(template_name)) + always_merger.merge(template_config, templates.get(template_name)) # Retrieve the full build configuration by applying the templates build_configs[build_name] = always_merger.merge(template_config, v) return build_configs - @app.route("/getCurrentUser", methods=['GET']) + @app.route("/getCurrentUser", methods=["GET"]) def getCurrentUser(): return request.authorization.username if request.authorization else None - @app.route("/backportCheck", methods=['POST']) - @verify_hmac('HMAC_BACKPORT_CHECK') + @app.route("/backportCheck", methods=["POST"]) + @verify_hmac("HMAC_BACKPORT_CHECK") def backportCheck(): data = get_json_request_data(request) - revisionId = data['object']['phid'] + revisionId = data["object"]["phid"] revisionSearchArgs = { "constraints": { "phids": [revisionId], }, } - data_list = phab.differential.revision.search( - **revisionSearchArgs).data - assert len(data_list) == 1, "differential.revision.search({}): Expected 1 revision, got: {}".format( - revisionSearchArgs, data_list) - summary = data_list[0]['fields']['summary'] + data_list = phab.differential.revision.search(**revisionSearchArgs).data + assert ( + len(data_list) == 1 + ), "differential.revision.search({}): Expected 1 revision, got: {}".format( + revisionSearchArgs, data_list + ) + summary = data_list[0]["fields"]["summary"] foundPRs = 0 multilineCodeBlockDelimiters = 0 newSummary = "" for line in summary.splitlines(keepends=True): - multilineCodeBlockDelimiters += len(re.findall(r'```', line)) + multilineCodeBlockDelimiters += len(re.findall(r"```", line)) # Only link PRs that do not reside in code blocks if multilineCodeBlockDelimiters % 2 == 0: + def replacePRWithLink(baseUrl, prefix): def repl(match): nonlocal foundPRs # This check matches identation-based code blocks (2+ spaces) # and common cases for single-line code blocks (using # both single and triple backticks) - if match.string.startswith(' ') or len( - re.findall(r'`', match.string[:match.start()])) % 2 > 0: + if ( + match.string.startswith(" ") + or len(re.findall(r"`", match.string[: match.start()])) % 2 + > 0 + ): # String remains unchanged return match.group(0) else: @@ -224,22 +228,22 @@ foundPRs += 1 PRNum = match.group(1) - return f'[[{baseUrl}/{PRNum} | {prefix}#{PRNum}]]' + return f"[[{baseUrl}/{PRNum} | {prefix}#{PRNum}]]" + return repl - githubUrl = 'https://github.com/{}/pull' - gitlabUrl = 'https://gitlab.com/{}/merge_requests' + githubUrl = "https://github.com/{}/pull" + gitlabUrl = "https://gitlab.com/{}/merge_requests" supportedRepos = { - 'core': githubUrl.format('bitcoin/bitcoin'), - 'core-gui': githubUrl.format('bitcoin-core/gui'), - 'secp256k1': githubUrl.format('bitcoin-core/secp256k1'), - 'bchn': gitlabUrl.format('bitcoin-cash-node/bitcoin-cash-node'), + "core": githubUrl.format("bitcoin/bitcoin"), + "core-gui": githubUrl.format("bitcoin-core/gui"), + "secp256k1": githubUrl.format("bitcoin-core/secp256k1"), + "bchn": gitlabUrl.format("bitcoin-cash-node/bitcoin-cash-node"), } for prefix, url in supportedRepos.items(): - regEx = r'{}#(\d*)'.format(prefix) - line = re.sub(regEx, replacePRWithLink( - url, prefix), line) + regEx = r"{}#(\d*)".format(prefix) + line = re.sub(regEx, replacePRWithLink(url, prefix), line) newSummary += line @@ -248,60 +252,59 @@ return SUCCESS, 200 - @app.route("/build", methods=['POST']) + @app.route("/build", methods=["POST"]) @persistDatabase def build(): - buildTypeId = request.args.get('buildTypeId', None) - ref = request.args.get('ref', 'refs/heads/master') + buildTypeId = request.args.get("buildTypeId", None) + ref = request.args.get("ref", "refs/heads/master") - PHID = request.args.get('PHID', None) + PHID = request.args.get("PHID", None) - abcBuildName = request.args.get('abcBuildName', None) + abcBuildName = request.args.get("abcBuildName", None) properties = None if abcBuildName: - properties = [{ - 'name': 'env.ABC_BUILD_NAME', - 'value': abcBuildName, - }] - - build_id = tc.trigger_build(buildTypeId, ref, PHID, properties)['id'] - if PHID in create_server.db['diff_targets']: - build_target = create_server.db['diff_targets'][PHID] + properties = [ + { + "name": "env.ABC_BUILD_NAME", + "value": abcBuildName, + } + ] + + build_id = tc.trigger_build(buildTypeId, ref, PHID, properties)["id"] + if PHID in create_server.db["diff_targets"]: + build_target = create_server.db["diff_targets"][PHID] else: build_target = BuildTarget(PHID) build_target.queue_build(build_id, abcBuildName) - create_server.db['diff_targets'][PHID] = build_target + create_server.db["diff_targets"][PHID] = build_target return SUCCESS, 200 - @app.route("/buildDiff", methods=['POST']) + @app.route("/buildDiff", methods=["POST"]) @persistDatabase def build_diff(): def get_mandatory_argument(argument): value = request.args.get(argument, None) if value is None: raise AssertionError( - "Calling /buildDiff endpoint with missing mandatory argument {}:\n{}".format( - argument, - request.args - ) + "Calling /buildDiff endpoint with missing mandatory argument" + " {}:\n{}".format(argument, request.args) ) return value - staging_ref = get_mandatory_argument('stagingRef') - target_phid = get_mandatory_argument('targetPHID') - revision_id = get_mandatory_argument('revisionId') + staging_ref = get_mandatory_argument("stagingRef") + target_phid = get_mandatory_argument("targetPHID") + revision_id = get_mandatory_argument("revisionId") # Get the list of changed files - changedFiles = phab.get_revision_changed_files( - revision_id=revision_id) + changedFiles = phab.get_revision_changed_files(revision_id=revision_id) build_configs = get_master_build_configurations() # Get a list of the builds that should run on diffs builds = [] for build_name, build_config in build_configs.items(): - diffRegexes = build_config.get('runOnDiffRegex', None) - if build_config.get('runOnDiff', False) or diffRegexes is not None: + diffRegexes = build_config.get("runOnDiffRegex", None) + if build_config.get("runOnDiff", False) or diffRegexes is not None: if diffRegexes: # If the regex matches at least one changed file, add this # build to the list. @@ -317,79 +320,83 @@ else: builds.append(build_name) - if target_phid in create_server.db['diff_targets']: - build_target = create_server.db['diff_targets'][target_phid] + if target_phid in create_server.db["diff_targets"]: + build_target = create_server.db["diff_targets"][target_phid] else: build_target = BuildTarget(target_phid) for build_name in builds: - properties = [{ - 'name': 'env.ABC_BUILD_NAME', - 'value': build_name, - }, { - 'name': 'env.ABC_REVISION', - 'value': revision_id, - }] + properties = [ + { + "name": "env.ABC_BUILD_NAME", + "value": build_name, + }, + { + "name": "env.ABC_REVISION", + "value": revision_id, + }, + ] build_id = tc.trigger_build( - 'BitcoinABC_BitcoinAbcStaging', - staging_ref, - target_phid, - properties)['id'] + "BitcoinABC_BitcoinAbcStaging", staging_ref, target_phid, properties + )["id"] build_target.queue_build(build_id, build_name) if len(build_target.builds) > 0: - create_server.db['diff_targets'][target_phid] = build_target + create_server.db["diff_targets"][target_phid] = build_target else: phab.update_build_target_status(build_target) return SUCCESS, 200 - @app.route("/land", methods=['POST']) + @app.route("/land", methods=["POST"]) def land(): data = get_json_request_data(request) - revision = data['revision'] + revision = data["revision"] if not revision: return FAILURE, 400 # conduitToken is expected to be encrypted and will be decrypted by the # land bot. - conduitToken = data['conduitToken'] + conduitToken = data["conduitToken"] if not conduitToken: return FAILURE, 400 - committerName = data['committerName'] + committerName = data["committerName"] if not committerName: return FAILURE, 400 - committerEmail = data['committerEmail'] + committerEmail = data["committerEmail"] if not committerEmail: return FAILURE, 400 - properties = [{ - 'name': 'env.ABC_REVISION', - 'value': revision, - }, { - 'name': 'env.ABC_CONDUIT_TOKEN', - 'value': conduitToken, - }, { - 'name': 'env.ABC_COMMITTER_NAME', - 'value': committerName, - }, { - 'name': 'env.ABC_COMMITTER_EMAIL', - 'value': committerEmail, - }] + properties = [ + { + "name": "env.ABC_REVISION", + "value": revision, + }, + { + "name": "env.ABC_CONDUIT_TOKEN", + "value": conduitToken, + }, + { + "name": "env.ABC_COMMITTER_NAME", + "value": committerName, + }, + { + "name": "env.ABC_COMMITTER_EMAIL", + "value": committerEmail, + }, + ] output = tc.trigger_build( - LANDBOT_BUILD_TYPE, - 'refs/heads/master', - UNRESOLVED, - properties) + LANDBOT_BUILD_TYPE, "refs/heads/master", UNRESOLVED, properties + ) if output: return output return FAILURE, 500 - @app.route("/triggerCI", methods=['POST']) - @verify_hmac('HMAC_TRIGGER_CI') + @app.route("/triggerCI", methods=["POST"]) + @verify_hmac("HMAC_TRIGGER_CI") def triggerCI(): data = get_json_request_data(request) app.logger.info(f"Received /triggerCI POST:\n{data}") @@ -405,25 +412,25 @@ # We are searching for a specially crafted comment to trigger a CI # build. Only comments on revision should be parsed. Also if there is # no transaction, or the object is not what we expect, just return. - if data_object["type"] != "DREV" or not data.get('transactions', []): + if data_object["type"] != "DREV" or not data.get("transactions", []): return SUCCESS, 200 revision_PHID = data_object["phid"] # Retrieve the transactions details from their PHIDs - transaction_PHIDs = [t["phid"] - for t in data["transactions"] if "phid" in t] + transaction_PHIDs = [t["phid"] for t in data["transactions"] if "phid" in t] transactions = phab.transaction.search( objectIdentifier=revision_PHID, constraints={ "phids": transaction_PHIDs, - } + }, ).data # Extract the comments from the transaction list. Each transaction # contains a list of comments. - comments = [c for t in transactions if t["type"] - == "comment" for c in t["comments"]] + comments = [ + c for t in transactions if t["type"] == "comment" for c in t["comments"] + ] # If there is no comment we have no interest in this webhook if not comments: return SUCCESS, 200 @@ -448,19 +455,31 @@ "PHID-TOKN-like-1": "PHID-TOKN-heart-1", "PHID-TOKN-heart-1": "PHID-TOKN-like-1", } - return next_token[current_token] if current_token in next_token else "PHID-TOKN-like-1" + return ( + next_token[current_token] + if current_token in next_token + else "PHID-TOKN-like-1" + ) def is_user_allowed_to_trigger_builds( - user_PHID, current_token, comment_builds, build_configs): + user_PHID, current_token, comment_builds, build_configs + ): if current_token not in [ - "", "PHID-TOKN-coin-1", "PHID-TOKN-coin-2", "PHID-TOKN-coin-3"]: + "", + "PHID-TOKN-coin-1", + "PHID-TOKN-coin-2", + "PHID-TOKN-coin-3", + ]: return False - if not all(role in phab.get_user_roles(user_PHID) for role in [ - "verified", - "approved", - "activated", - ]): + if not all( + role in phab.get_user_roles(user_PHID) + for role in [ + "verified", + "approved", + "activated", + ] + ): return False for build_name in comment_builds: @@ -518,7 +537,8 @@ continue if is_user_allowed_to_trigger_builds( - user, current_token, comment_builds, build_configs): + user, current_token, comment_builds, build_configs + ): builds += comment_builds # If there is no build provided, this request is not what we are after, @@ -539,24 +559,25 @@ tc.trigger_build( "BitcoinABC_BitcoinAbcStaging", staging_ref, - properties=[{ - 'name': 'env.ABC_BUILD_NAME', - 'value': build, - }] + properties=[ + { + "name": "env.ABC_BUILD_NAME", + "value": build, + } + ], ) # If we reach this point, trigger_build did not raise an exception. return SUCCESS, 200 - @app.route("/status", methods=['POST']) + @app.route("/status", methods=["POST"]) @persistDatabase def buildStatus(): out = get_json_request_data(request) app.logger.info(f"Received /status POST with data: {out}") return handle_build_result(**out) - def send_harbormaster_build_link_if_required( - build_link, build_target, build_name): + def send_harbormaster_build_link_if_required(build_link, build_target, build_name): # Check if a link to the build server has already been sent by searching # the artifacts. artifacts = phab.harbormaster.artifact.search( @@ -571,8 +592,10 @@ # If found then the link is already set and there is no need to send it # again. for artifact in artifacts: - if "artifactKey" in (artifact["fields"] or { - }) and artifact["fields"]["artifactKey"] == build_link_artifact_key: + if ( + "artifactKey" in (artifact["fields"] or {}) + and artifact["fields"]["artifactKey"] == build_link_artifact_key + ): return phab.harbormaster.createartifact( @@ -583,7 +606,7 @@ "uri": build_link, "name": build_name, "ui.external": True, - } + }, ) def update_build_status_panel(updated_build_type_id): @@ -597,30 +620,23 @@ # keys. # Returns a tuple of (removed keys, added keys) def dict_xor(target, reference_keys, default_value_callback): - removed_keys = [ - k for k in list( - target.keys()) if k not in reference_keys] + removed_keys = [k for k in list(target.keys()) if k not in reference_keys] for key in removed_keys: del target[key] - added_keys = [ - k for k in reference_keys if k not in list( - target.keys())] + added_keys = [k for k in reference_keys if k not in list(target.keys())] for key in added_keys: target[key] = default_value_callback(key) return (removed_keys, added_keys) - panel_content = '' + panel_content = "" def add_line_to_panel(line): - return panel_content + line + '\n' + return panel_content + line + "\n" def add_project_header_to_panel(project_name): - return panel_content + ( - '| {} | Status |\n' - '|---|---|\n' - ).format(project_name) + return panel_content + ("| {} | Status |\n|---|---|\n").format(project_name) # secp256k1 is a special case because it has a Cirrus build from a # Github repo that is not managed by the build-configurations.yml config. @@ -628,35 +644,52 @@ sepc256k1_cirrus_status = cirrus.get_default_branch_status() cirrus_badge_url = BADGE_CIRRUS_BASE.get_badge_url( message=sepc256k1_cirrus_status.value, - color=('brightgreen' if sepc256k1_cirrus_status == BuildStatus.Success else - 'red' if sepc256k1_cirrus_status == BuildStatus.Failure else - 'blue' if sepc256k1_cirrus_status == BuildStatus.Running else - 'lightblue' if sepc256k1_cirrus_status == BuildStatus.Queued else - 'inactive'), + color=( + "brightgreen" + if sepc256k1_cirrus_status == BuildStatus.Success + else ( + "red" + if sepc256k1_cirrus_status == BuildStatus.Failure + else ( + "blue" + if sepc256k1_cirrus_status == BuildStatus.Running + else ( + "lightblue" + if sepc256k1_cirrus_status == BuildStatus.Queued + else "inactive" + ) + ) + ) + ), ) # Add secp256k1 Cirrus to the status panel. panel_content = add_project_header_to_panel( - 'secp256k1 ([[https://github.com/Bitcoin-ABC/secp256k1 | Github]])') + "secp256k1 ([[https://github.com/Bitcoin-ABC/secp256k1 | Github]])" + ) panel_content = add_line_to_panel( '| [[{} | {}]] | {{image uri="{}", alt="{}"}} |'.format( - 'https://cirrus-ci.com/github/Bitcoin-ABC/secp256k1', - 'master', + "https://cirrus-ci.com/github/Bitcoin-ABC/secp256k1", + "master", cirrus_badge_url, sepc256k1_cirrus_status.value, ) ) - panel_content = add_line_to_panel('') + panel_content = add_line_to_panel("") # Download the build configuration from master - config = yaml.safe_load(phab.get_file_content_from_master( - "contrib/teamcity/build-configurations.yml")) + config = yaml.safe_load( + phab.get_file_content_from_master( + "contrib/teamcity/build-configurations.yml" + ) + ) # Get a list of the builds to display config_build_names = [ - k for k, v in config.get( - 'builds', {}).items() if not v.get( - 'hideOnStatusPanel', False)] + k + for k, v in config.get("builds", {}).items() + if not v.get("hideOnStatusPanel", False) + ] # If there is no build to display, don't update the panel with teamcity # data @@ -666,24 +699,28 @@ # Associate with Teamcity data from the BitcoinABC project associated_builds = tc.associate_configuration_names( - "BitcoinABC", config_build_names) + "BitcoinABC", config_build_names + ) # Get a unique list of the project ids - project_ids = [build["teamcity_project_id"] - for build in list(associated_builds.values())] + project_ids = [ + build["teamcity_project_id"] for build in list(associated_builds.values()) + ] project_ids = list(set(project_ids)) # Construct a dictionary from teamcity project id to project name. # This will make it easier to navigate from one to the other. project_name_map = {} for build in list(associated_builds.values()): - project_name_map[build['teamcity_project_id'] - ] = build['teamcity_project_name'] + project_name_map[build["teamcity_project_id"]] = build[ + "teamcity_project_name" + ] # If the list of project names has changed (project was added, deleted # or renamed, update the panel data accordingly. (removed_projects, added_projects) = dict_xor( - create_server.db['panel_data'], project_ids, lambda key: {}) + create_server.db["panel_data"], project_ids, lambda key: {} + ) # Log the project changes if any if (len(removed_projects) + len(added_projects)) > 0: @@ -698,8 +735,9 @@ # This will make it easier to navigate from one to the other. build_name_map = {} for build in list(associated_builds.values()): - build_name_map[build['teamcity_build_type_id'] - ] = build['teamcity_build_name'] + build_name_map[build["teamcity_build_type_id"]] = build[ + "teamcity_build_name" + ] def get_build_status_and_message(build_type_id): latest_build = tc.getLatestCompletedBuild(build_type_id) @@ -708,20 +746,25 @@ build_status = BuildStatus.Unknown build_status_message = build_status.value else: - - build_info = tc.getBuildInfo(latest_build['id']) - build_status = BuildStatus(build_info['status'].lower()) - build_status_message = build_info.get( - 'statusText', - build_status.value) if build_status == BuildStatus.Failure else build_status.value + build_info = tc.getBuildInfo(latest_build["id"]) + build_status = BuildStatus(build_info["status"].lower()) + build_status_message = ( + build_info.get("statusText", build_status.value) + if build_status == BuildStatus.Failure + else build_status.value + ) return (build_status, build_status_message) # Update the builds for project_id, project_builds in sorted( - create_server.db['panel_data'].items()): - build_type_ids = [build['teamcity_build_type_id'] for build in list( - associated_builds.values()) if build['teamcity_project_id'] == project_id] + create_server.db["panel_data"].items() + ): + build_type_ids = [ + build["teamcity_build_type_id"] + for build in list(associated_builds.values()) + if build["teamcity_project_id"] == project_id + ] # If the list of builds has changed (build was added, deleted, # renamed, added to or removed from the items to display), update @@ -730,13 +773,14 @@ project_builds, build_type_ids, # We need to fetch the satus for each added build - lambda key: get_build_status_and_message(key) + lambda key: get_build_status_and_message(key), ) # Log the build changes if any if (len(removed_builds) + len(added_builds)) > 0: app.logger.info( - "Teamcity build list has changed for project {}.\nRemoved: {}\nAdded: {}".format( + "Teamcity build list has changed for project {}.\nRemoved:" + " {}\nAdded: {}".format( project_id, removed_builds, added_builds, @@ -747,10 +791,13 @@ # updated. Note that it might already be up-to-date if the build was # part of the added ones. # Other data remains valid from the previous calls. - if updated_build_type_id not in added_builds and updated_build_type_id in list( - project_builds.keys()): + if ( + updated_build_type_id not in added_builds + and updated_build_type_id in list(project_builds.keys()) + ): project_builds[updated_build_type_id] = get_build_status_and_message( - updated_build_type_id) + updated_build_type_id + ) # Create a table view of the project: # @@ -759,26 +806,28 @@ # | Link to latest build | Status icon | # | Link to latest build | Status icon | # | Link to latest build | Status icon | - panel_content = add_project_header_to_panel( - project_name_map[project_id]) + panel_content = add_project_header_to_panel(project_name_map[project_id]) - for build_type_id, (build_status, - build_status_message) in project_builds.items(): + for build_type_id, ( + build_status, + build_status_message, + ) in project_builds.items(): url = tc.build_url( "viewLog.html", - { - "buildTypeId": build_type_id, - "buildId": "lastFinished" - } + {"buildTypeId": build_type_id, "buildId": "lastFinished"}, ) # TODO insert Teamcity build failure message badge_url = BADGE_TC_BASE.get_badge_url( message=build_status_message, color=( - 'lightgrey' if build_status == BuildStatus.Unknown - else 'brightgreen' if build_status == BuildStatus.Success - else 'red' + "lightgrey" + if build_status == BuildStatus.Unknown + else ( + "brightgreen" + if build_status == BuildStatus.Success + else "red" + ) ), ) @@ -790,13 +839,15 @@ build_status_message, ) ) - panel_content = add_line_to_panel('') + panel_content = add_line_to_panel("") phab.set_text_panel_content(17, panel_content) def update_coverage_panel(build_type_id, project_name, coverage_summary): - coverage_permalink = "**[[ https://build.bitcoinabc.org/viewLog.html?buildId=lastSuccessful&buildTypeId={}&tab=report__Root_Code_Coverage&guest=1 | {} coverage report ]]**\n\n".format( - build_type_id, project_name) + coverage_permalink = ( + "**[[ https://build.bitcoinabc.org/viewLog.html?buildId=lastSuccessful&buildTypeId={}&tab=report__Root_Code_Coverage&guest=1" + " | {} coverage report ]]**\n\n".format(build_type_id, project_name) + ) coverage_report = "| Granularity | % hit | # hit | # total |\n" coverage_report += "| ----------- | ----- | ----- | ------- |\n" @@ -811,7 +862,10 @@ # functions..: 74.1% (6686 of 9020 functions) # branches...: 45.0% (188886 of 420030 branches) - pattern = r"^\s*(?P\w+)\.+: (?P[0-9.]+%) \((?P\d+) of (?P\d+) .+$" + pattern = ( + r"^\s*(?P\w+)\.+: (?P[0-9.]+%) \((?P\d+) of" + r" (?P\d+) .+$" + ) for line in coverage_summary.splitlines(): match = re.match(pattern, line.strip()) @@ -820,21 +874,30 @@ continue coverage_report += "| {} | {} | {} | {} |\n".format( - match.group('granularity').capitalize(), - match.group('percent'), - match.group('hit'), - match.group('total'), + match.group("granularity").capitalize(), + match.group("percent"), + match.group("hit"), + match.group("total"), ) # Cache the coverage data for this build type - coverage_data = create_server.db['coverage_data'] + coverage_data = create_server.db["coverage_data"] coverage_data[build_type_id] = coverage_permalink + coverage_report # Update the coverage panel with our remarkup content phab.set_text_panel_content(21, "\n".join(coverage_data.values())) - def handle_build_result(buildName, buildTypeId, buildResult, - buildURL, branch, buildId, buildTargetPHID, projectName, **kwargs): + def handle_build_result( + buildName, + buildTypeId, + buildResult, + buildURL, + branch, + buildId, + buildTargetPHID, + projectName, + **kwargs, + ): # Do not report build status for ignored builds if phab.getIgnoreKeyword() in buildTypeId: return SUCCESS, 200 @@ -847,11 +910,12 @@ status = BuildStatus(buildResult) - isMaster = (branch == "refs/heads/master" or branch == "") + isMaster = branch == "refs/heads/master" or branch == "" # If a build completed on master, update the build status panel. if isMaster and ( - status == BuildStatus.Success or status == BuildStatus.Failure): + status == BuildStatus.Success or status == BuildStatus.Failure + ): update_build_status_panel(buildTypeId) # If the build succeeded and there is a coverage report in the build @@ -865,23 +929,19 @@ coverage_summary = None if coverage_summary: - update_coverage_panel( - buildTypeId, projectName, coverage_summary) + update_coverage_panel(buildTypeId, projectName, coverage_summary) # If we have a buildTargetPHID, report the status. - build_target = create_server.db['diff_targets'].get( - buildTargetPHID, None) + build_target = create_server.db["diff_targets"].get(buildTargetPHID, None) if build_target is not None: phab.update_build_target_status(build_target, buildId, status) send_harbormaster_build_link_if_required( - guest_url, - build_target, - build_target.builds[buildId].name + guest_url, build_target, build_target.builds[buildId].name ) if build_target.is_finished(): - del create_server.db['diff_targets'][buildTargetPHID] + del create_server.db["diff_targets"][buildTargetPHID] revisionPHID = phab.get_revisionPHID(branch) @@ -896,47 +956,60 @@ # people with a useful message. buildLog = tc.getBuildLog(buildId) if re.search(re.escape("[Infrastructure Error]"), buildLog): - slackbot.postMessage('infra', - " There was an infrastructure failure in '{}': {}".format( - buildName, guest_url)) + slackbot.postMessage( + "infra", + " There was an infrastructure failure in" + " '{}': {}".format(buildName, guest_url), + ) # Normally a comment of the build status is provided on diffs. Since no useful debug # info can be provided that is actionable to the user, we # give them a short message. if not isMaster: - phab.commentOnRevision(revisionPHID, - "(IMPORTANT) The build failed due to an unexpected infrastructure outage. " - "The administrators have been notified to investigate. Sorry for the inconvenience.", - buildName) + phab.commentOnRevision( + revisionPHID, + ( + "(IMPORTANT) The build failed due to an unexpected" + " infrastructure outage. The administrators have been" + " notified to investigate. Sorry for the inconvenience." + ), + buildName, + ) return SUCCESS, 200 # Handle land bot builds if buildTypeId == LANDBOT_BUILD_TYPE: if status == BuildStatus.Success or status == BuildStatus.Failure: properties = buildInfo.getProperties() - revisionId = properties.get( - 'env.ABC_REVISION', 'MISSING REVISION ID') + revisionId = properties.get("env.ABC_REVISION", "MISSING REVISION ID") author = phab.getRevisionAuthor(revisionId) landBotMessage = "Failed to land your change:" if status == BuildStatus.Success: landBotMessage = "Successfully landed your change:" - landBotMessage = "{}\nRevision: https://reviews.bitcoinabc.org/{}\nBuild: {}".format( - landBotMessage, revisionId, guest_url) + landBotMessage = ( + "{}\nRevision: https://reviews.bitcoinabc.org/{}\nBuild: {}".format( + landBotMessage, revisionId, guest_url + ) + ) # Send a direct message to the revision author authorSlackUsername = phab.getAuthorSlackUsername(author) authorSlackUser = slackbot.getUserByName(authorSlackUsername) - slackChannel = authorSlackUser['id'] if authorSlackUser else None + slackChannel = authorSlackUser["id"] if authorSlackUser else None if not slackChannel: - slackChannel = 'dev' - landBotMessage = "{}: Please set your slack username in your Phabricator profile so the landbot can send you direct messages: {}\n{}".format( - authorSlackUsername, - "https://reviews.bitcoinabc.org/people/editprofile/{}".format( - author['id']), - landBotMessage) + slackChannel = "dev" + landBotMessage = ( + "{}: Please set your slack username in your Phabricator profile" + " so the landbot can send you direct messages: {}\n{}".format( + authorSlackUsername, + "https://reviews.bitcoinabc.org/people/editprofile/{}" + .format(author["id"]), + landBotMessage, + ) + ) slackbot.postMessage(slackChannel, landBotMessage) return SUCCESS, 200 @@ -947,40 +1020,41 @@ latestBuild = tc.getLatestCompletedBuild(buildTypeId) latestBuildId = None if latestBuild: - latestBuildId = latestBuild.get('id', None) + latestBuildId = latestBuild.get("id", None) - logLatestBuildId = 'None' if latestBuildId is None else latestBuildId + logLatestBuildId = "None" if latestBuildId is None else latestBuildId app.logger.info( - "Latest completed build ID of type '{}': {}".format( - buildTypeId, logLatestBuildId)) + f"Latest completed build ID of type '{buildTypeId}': {logLatestBuildId}" + ) if latestBuildId == buildId: if status == BuildStatus.Success: updatedTask = phab.updateBrokenBuildTaskStatus( - buildName, 'resolved') + buildName, "resolved" + ) if updatedTask: # Only message once all of master is green - (buildFailures, testFailures) = tc.getLatestBuildAndTestFailures( - 'BitcoinABC') + (buildFailures, testFailures) = ( + tc.getLatestBuildAndTestFailures("BitcoinABC") + ) if len(buildFailures) == 0 and len(testFailures) == 0: - if not create_server.db['master_is_green']: - create_server.db['master_is_green'] = True - slackbot.postMessage( - 'dev', "Master is green again.") + if not create_server.db["master_is_green"]: + create_server.db["master_is_green"] = True + slackbot.postMessage("dev", "Master is green again.") if status == BuildStatus.Failure: shortBuildUrl = tc.build_url( "viewLog.html", { "buildId": buildId, - } + }, ) # Explicitly ignored log lines. Use with care. buildLog = tc.getBuildLog(buildId) for line in tc.getIgnoreList(): # Skip empty lines and comments in the ignore file - if not line or line.decode().strip()[0] == '#': + if not line or line.decode().strip()[0] == "#": continue # If any of the ignore patterns match any line in the @@ -990,7 +1064,8 @@ # Get number of build failures over the last few days numRecentFailures = tc.getNumAggregateFailuresSince( - buildTypeId, 60 * 60 * 24 * 5) + buildTypeId, 60 * 60 * 24 * 5 + ) if numRecentFailures >= 3: # This build is likely flaky and the channel has @@ -1000,16 +1075,19 @@ if numRecentFailures >= 2: # This build may be flaky. Ping the channel with a # less-noisy message. - slackbot.postMessage('dev', - f"Build '{buildName}' appears to be flaky: {shortBuildUrl}") + slackbot.postMessage( + "dev", + f"Build '{buildName}' appears to be flaky: {shortBuildUrl}", + ) return SUCCESS, 200 # Only mark master as red for failures that are not flaky - create_server.db['master_is_green'] = False + create_server.db["master_is_green"] = False commitHashes = buildInfo.getCommits() newTask = phab.createBrokenBuildTask( - buildName, guest_url, branch, commitHashes, 'rABC') + buildName, guest_url, branch, commitHashes, "rABC" + ) if newTask: # TODO: Add 'Reviewed by: ' line @@ -1018,37 +1096,46 @@ # the author of a change that is unlikely to contain # the root cause of the issue. if tc.checkBuildIsScheduled(buildInfo): - slackbot.postMessage('dev', - "Scheduled build '{}' appears to be broken: {}\n" - "Task: https://reviews.bitcoinabc.org/T{}".format( - buildName, shortBuildUrl, newTask['id'])) + slackbot.postMessage( + "dev", + "Scheduled build '{}' appears to be broken: {}\n" + "Task: https://reviews.bitcoinabc.org/T{}".format( + buildName, shortBuildUrl, newTask["id"] + ), + ) else: - commitMap = phab.getRevisionPHIDsFromCommits( - commitHashes) - decoratedCommits = phab.decorateCommitMap( - commitMap) + commitMap = phab.getRevisionPHIDsFromCommits(commitHashes) + decoratedCommits = phab.decorateCommitMap(commitMap) decoratedCommit = decoratedCommits[commitHashes[0]] - changeLink = decoratedCommit['link'] - authorSlackUsername = decoratedCommit['authorSlackUsername'] + changeLink = decoratedCommit["link"] + authorSlackUsername = decoratedCommit["authorSlackUsername"] authorSlackId = slackbot.formatMentionByName( - authorSlackUsername) + authorSlackUsername + ) if not authorSlackId: authorSlackId = authorSlackUsername - slackbot.postMessage('dev', - "Committer: {}\n" - "Build '{}' appears to be broken: {}\n" - "Task: https://reviews.bitcoinabc.org/T{}\n" - "Diff: {}".format( - authorSlackId, buildName, shortBuildUrl, newTask['id'], changeLink)) + slackbot.postMessage( + "dev", + "Committer: {}\n" + "Build '{}' appears to be broken: {}\n" + "Task: https://reviews.bitcoinabc.org/T{}\n" + "Diff: {}".format( + authorSlackId, + buildName, + shortBuildUrl, + newTask["id"], + changeLink, + ), + ) if not isMaster: revisionId, authorPHID = phab.get_revision_info(revisionPHID) properties = buildInfo.getProperties() - buildConfig = properties.get('env.ABC_BUILD_NAME', None) + buildConfig = properties.get("env.ABC_BUILD_NAME", None) if not buildConfig: - buildConfig = properties.get('env.OS_NAME', 'UNKNOWN') + buildConfig = properties.get("env.OS_NAME", "UNKNOWN") buildName = f"{buildName} ({buildConfig})" if status == BuildStatus.Success: @@ -1062,11 +1149,10 @@ phab.commentOnRevision(revisionPHID, msg, buildName) elif status == BuildStatus.Failure: - msg = phab.createBuildStatusMessage( - status, guest_url, buildName) + msg = phab.createBuildStatusMessage(status, guest_url, buildName) # We add two newlines to break away from the (IMPORTANT) # callout. - msg += '\n\n' + msg += "\n\n" testFailures = tc.getFailedTests(buildId) if len(testFailures) == 0: @@ -1077,18 +1163,21 @@ for line in buildLog.splitlines(keepends=True): logLines.append(line) - msg += "Tail of the build log:\n```lines=16,COUNTEREXAMPLE\n{}```".format( - ''.join(logLines[-60:])) + msg += ( + "Tail of the build log:\n```lines=16,COUNTEREXAMPLE\n{}```" + .format("".join(logLines[-60:])) + ) else: # Print the failure log for each test - msg += 'Failed tests logs:\n' - msg += '```lines=16,COUNTEREXAMPLE' + msg += "Failed tests logs:\n" + msg += "```lines=16,COUNTEREXAMPLE" for failure in testFailures: msg += "\n====== {} ======\n{}".format( - failure['name'], failure['details']) - msg += '```' - msg += '\n\n' - msg += 'Each failure log is accessible here:' + failure["name"], failure["details"] + ) + msg += "```" + msg += "\n\n" + msg += "Each failure log is accessible here:" for failure in testFailures: msg += f"\n[[{failure['logUrl']} | {failure['name']}]]" diff --git a/contrib/buildbot/shieldio.py b/contrib/buildbot/shieldio.py --- a/contrib/buildbot/shieldio.py +++ b/contrib/buildbot/shieldio.py @@ -9,29 +9,32 @@ class Badge: def __init__(self, **kwargs): - self.base_url = 'https://img.shields.io/static/v1' + self.base_url = "https://img.shields.io/static/v1" # Provide some defaults, potentially updated by kwargs self.query = { - 'label': 'shieldio', - 'message': 'unknown', - 'color': 'inactive', + "label": "shieldio", + "message": "unknown", + "color": "inactive", } self.query.update(kwargs) def get_badge_url(self, **kwargs): scheme, netloc, path = urlsplit(self.base_url)[0:3] - return urlunsplit(( - scheme, - netloc, - path, - unquote(urlencode({**self.query, **kwargs}, - doseq=True, quote_via=quote)), - '' - )) + return urlunsplit( + ( + scheme, + netloc, + path, + unquote( + urlencode({**self.query, **kwargs}, doseq=True, quote_via=quote) + ), + "", + ) + ) class RasterBadge(Badge): def __init__(self, **kwargs): super().__init__(**kwargs) - self.base_url = 'https://raster.shields.io/static/v1' + self.base_url = "https://raster.shields.io/static/v1" diff --git a/contrib/buildbot/slackbot.py b/contrib/buildbot/slackbot.py --- a/contrib/buildbot/slackbot.py +++ b/contrib/buildbot/slackbot.py @@ -1,14 +1,14 @@ #!/usr/bin/env python3 -class SlackBot(): +class SlackBot: def __init__(self, clientClass, token, channels): self.client = clientClass(token=token) self.channels = channels def postMessage(self, channelIn, message): channel = None - if channelIn and channelIn[0] == 'U': + if channelIn and channelIn[0] == "U": channel = channelIn if channelIn in self.channels: @@ -16,7 +16,9 @@ if not channel: raise AssertionError( - "Invalid channel: Channel must be a user ID or configured with a channel name") + "Invalid channel: Channel must be a user ID or configured with a" + " channel name" + ) self.client.chat_postMessage(channel=channel, text=message) @@ -24,7 +26,7 @@ # Note: users.list only returns up to 500 users. If this limit is exceeded, # pagination will need to be implemented. users = self.client.users_list() - return users['members'] + return users["members"] def getUserByName(self, username): # Note: The Slack API does NOT provide a way to search for users and @@ -32,12 +34,15 @@ # while we have a low user count in ABC slack. users = self.getUsers() for user in users: - if username in [user['profile'][nameAttribute] for nameAttribute in [ - 'real_name', - 'real_name_normalized', - 'display_name', - 'display_name_normalized', - ]]: + if username in [ + user["profile"][nameAttribute] + for nameAttribute in [ + "real_name", + "real_name_normalized", + "display_name", + "display_name_normalized", + ] + ]: return user return None diff --git a/contrib/buildbot/teamcity_wrapper.py b/contrib/buildbot/teamcity_wrapper.py --- a/contrib/buildbot/teamcity_wrapper.py +++ b/contrib/buildbot/teamcity_wrapper.py @@ -20,34 +20,39 @@ class BuildInfo(UserDict): @staticmethod def fromSingleBuildResponse(json_content): - return BuildInfo(json_content['build'][0]) + return BuildInfo(json_content["build"][0]) def getCommits(self): - return [change['version'] for change in self.data['changes'] - ['change']] if 'changes' in (self.data or {}) else None + return ( + [change["version"] for change in self.data["changes"]["change"]] + if "changes" in (self.data or {}) + else None + ) def getProperties(self): propsList = [] - if 'properties' in (self.data or {}): - propsList = self.data['properties']['property'] + if "properties" in (self.data or {}): + propsList = self.data["properties"]["property"] # Transform list of properties [{'name': name, 'value': value}, ...] into a # dict {name: value, ...} since we only care about the property values. properties = {} for prop in propsList: - properties[prop['name']] = prop['value'] + properties[prop["name"]] = prop["value"] return properties if properties else None -class TeamCity(): +class TeamCity: def __init__(self, base_url, username, password): self.session = requests.Session() self.base_url = base_url self.auth = (username, password) self.logger = None self.mockTime = None - with open(os.path.join(os.path.dirname(__file__), 'ignore-logs.txt'), 'rb') as ignoreList: + with open( + os.path.join(os.path.dirname(__file__), "ignore-logs.txt"), "rb" + ) as ignoreList: self.ignoreList = ignoreList.readlines() def set_logger(self, logger): @@ -74,11 +79,12 @@ if self.logger: self.logger.info( "Request:\n{}\n\nResponse:\n{}".format( - pprint( - vars(request)), pprint( - vars(response)))) + pprint(vars(request)), pprint(vars(response)) + ) + ) raise TeamcityRequestException( - f"Unexpected Teamcity API error! Status code: {response.status_code}") + f"Unexpected Teamcity API error! Status code: {response.status_code}" + ) content = response.content if expectJson: @@ -97,21 +103,21 @@ properties = [] if PHID is not None: - properties.append({ - 'name': 'env.harborMasterTargetPHID', - 'value': PHID, - }) + properties.append( + { + "name": "env.harborMasterTargetPHID", + "value": PHID, + } + ) build = { - 'branchName': ref, - 'buildType': { - 'id': buildTypeId + "branchName": ref, + "buildType": {"id": buildTypeId}, + "properties": { + "property": properties, }, - 'properties': { - 'property': properties, - } } - req = self._request('POST', endpoint, json.dumps(build)) + req = self._request("POST", endpoint, json.dumps(build)) return self.getResponse(req) def get_artifact(self, buildId, path): @@ -119,17 +125,16 @@ f"app/rest/builds/id:{buildId}/artifacts/content/{path}" ) - req = self._request('GET', endpoint) + req = self._request("GET", endpoint) content = self.getResponse(req, expectJson=False) if not content: return None - return content.decode('utf-8') + return content.decode("utf-8") def get_coverage_summary(self, buildId): - return self.get_artifact( - buildId, "coverage.tar.gz!/coverage-summary.txt") + return self.get_artifact(buildId, "coverage.tar.gz!/coverage-summary.txt") def get_clean_build_log(self, buildId): return self.get_artifact(buildId, "artifacts.tar.gz!/build.clean.log") @@ -151,9 +156,9 @@ { "buildId": buildId, "archived": "true", - } + }, ) - req = self._request('GET', endpoint) + req = self._request("GET", endpoint) content = self.getResponse(req, expectJson=False) ret = "" if not content: @@ -162,8 +167,8 @@ z = ZipFile(io.BytesIO(content)) for filename in z.namelist(): for line in z.open(filename).readlines(): - ret += line.decode('utf-8') - return ret.replace('\r\n', '\n') + ret += line.decode("utf-8") + return ret.replace("\r\n", "\n") def getPreviewUrl(self, buildId): try: @@ -179,18 +184,18 @@ { "locator": f"build:(id:{buildId})", "fields": "problemOccurrence(id,details)", - } + }, ) - req = self._request('GET', endpoint) + req = self._request("GET", endpoint) content = self.getResponse(req) - if 'problemOccurrence' in (content or {}): - buildFailures = content['problemOccurrence'] + if "problemOccurrence" in (content or {}): + buildFailures = content["problemOccurrence"] for failure in buildFailures: # Note: Unlike test failures, build "problems" do not have # a well-defined focus line in the build log. For now, we # link to the footer to automatically scroll to the bottom # of the log where failures tend to be. - failure['logUrl'] = self.build_url( + failure["logUrl"] = self.build_url( "viewLog.html", { "tab": "buildLog", @@ -199,7 +204,7 @@ "expand": "all", "buildId": buildId, }, - "footer" + "footer", ) return buildFailures return [] @@ -210,12 +215,12 @@ { "locator": f"build:(id:{buildId}),status:FAILURE", "fields": "testOccurrence(id,details,name)", - } + }, ) - req = self._request('GET', endpoint) + req = self._request("GET", endpoint) content = self.getResponse(req) - if 'testOccurrence' in (content or {}): - testFailures = content['testOccurrence'] + if "testOccurrence" in (content or {}): + testFailures = content["testOccurrence"] for failure in testFailures: params = { "tab": "buildLog", @@ -225,14 +230,11 @@ "buildId": buildId, } - match = re.search(r'id:(\d+)', failure['id']) + match = re.search(r"id:(\d+)", failure["id"]) if match: - params['_focus'] = match.group(1) + params["_focus"] = match.group(1) - failure['logUrl'] = self.build_url( - "viewLog.html", - params - ) + failure["logUrl"] = self.build_url("viewLog.html", params) return testFailures @@ -240,23 +242,20 @@ def getBuildChangeDetails(self, changeId): endpoint = self.build_url(f"app/rest/changes/{changeId}") - req = self._request('GET', endpoint) + req = self._request("GET", endpoint) return self.getResponse(req) or {} def getBuildChanges(self, buildId): endpoint = self.build_url( "app/rest/changes", - { - "locator": f"build:(id:{buildId})", - "fields": "change(id)" - } + {"locator": f"build:(id:{buildId})", "fields": "change(id)"}, ) - req = self._request('GET', endpoint) + req = self._request("GET", endpoint) content = self.getResponse(req) - if 'change' in (content or {}): - changes = content['change'] + if "change" in (content or {}): + changes = content["change"] for i, change in enumerate(changes): - changes[i] = self.getBuildChangeDetails(change['id']) + changes[i] = self.getBuildChangeDetails(change["id"]) return changes return [] @@ -268,25 +267,25 @@ # Note: Wildcard does not match recursively, so if you need data # from a sub-field, be sure to include it in the list. "fields": "build(*,changes(*),properties(*),triggered(*))", - } + }, ) - req = self._request('GET', endpoint) + req = self._request("GET", endpoint) content = self.getResponse(req) - if 'build' in (content or {}): + if "build" in (content or {}): return BuildInfo.fromSingleBuildResponse(content) return BuildInfo() def checkBuildIsAutomated(self, buildInfo): - trigger = buildInfo['triggered'] + trigger = buildInfo["triggered"] # Ignore builds by non-bot users, as these builds may be triggered for # any reason with various unknown configs - return trigger['type'] != 'user' or trigger['user']['username'] == self.auth[0] + return trigger["type"] != "user" or trigger["user"]["username"] == self.auth[0] def checkBuildIsScheduled(self, buildInfo): - trigger = buildInfo['triggered'] - return trigger['type'] == 'schedule' + trigger = buildInfo["triggered"] + return trigger["type"] == "schedule" # For all nested build configurations under a project, fetch the latest # build failures. @@ -296,34 +295,34 @@ { "locator": f"currentlyFailing:true,affectedProject:(id:{projectId})", "fields": "problemOccurrence(*)", - } + }, ) - buildReq = self._request('GET', buildEndpoint) + buildReq = self._request("GET", buildEndpoint) buildContent = self.getResponse(buildReq) buildFailures = [] - if 'problemOccurrence' in (buildContent or {}): - buildFailures = buildContent['problemOccurrence'] + if "problemOccurrence" in (buildContent or {}): + buildFailures = buildContent["problemOccurrence"] testEndpoint = self.build_url( "app/rest/testOccurrences", { "locator": f"currentlyFailing:true,affectedProject:(id:{projectId})", "fields": "testOccurrence(*)", - } + }, ) - testReq = self._request('GET', testEndpoint) + testReq = self._request("GET", testEndpoint) testContent = self.getResponse(testReq) testFailures = [] - if 'testOccurrence' in (testContent or {}): - testFailures = testContent['testOccurrence'] + if "testOccurrence" in (testContent or {}): + testFailures = testContent["testOccurrence"] return (buildFailures, testFailures) def getLatestCompletedBuild(self, buildType, build_fields=None): if not build_fields: - build_fields = ['id'] + build_fields = ["id"] endpoint = self.build_url( "app/rest/builds", @@ -331,12 +330,12 @@ "locator": f"buildType:{buildType}", "fields": f"build({','.join(build_fields)})", "count": 1, - } + }, ) - req = self._request('GET', endpoint) + req = self._request("GET", endpoint) content = self.getResponse(req) - builds = content.get('build', []) + builds = content.get("build", []) # There might be no build completed yet, in this case return None if not builds: @@ -351,7 +350,7 @@ return builds[0] def formatTime(self, seconds): - return time.strftime('%Y%m%dT%H%M%S%z', time.gmtime(seconds)) + return time.strftime("%Y%m%dT%H%M%S%z", time.gmtime(seconds)) # The returned count is the number of groups of back-to-back failures, not # the number of individual failures @@ -360,16 +359,23 @@ endpoint = self.build_url( "app/rest/builds", { - "locator": f"buildType:{buildType},sinceDate:{self.formatTime(sinceTime)}", + "locator": ( + f"buildType:{buildType},sinceDate:{self.formatTime(sinceTime)}" + ), "fields": "build", - } + }, ) - req = self._request('GET', endpoint) + req = self._request("GET", endpoint) content = self.getResponse(req) - if 'build' in (content or {}): - builds = [{'status': 'SUCCESS'}] + content['build'] - return sum([(builds[i - 1]['status'], builds[i]['status']) - == ('SUCCESS', 'FAILURE') for i in range(1, len(builds))]) + if "build" in (content or {}): + builds = [{"status": "SUCCESS"}] + content["build"] + return sum( + [ + (builds[i - 1]["status"], builds[i]["status"]) + == ("SUCCESS", "FAILURE") + for i in range(1, len(builds)) + ] + ) return 0 # For each of the given build name from the configuration file, associate the @@ -383,9 +389,9 @@ { "locator": f"affectedProject:{project_id}", "fields": "buildType(project(id,name),id,name,parameters($locator(name:env.ABC_BUILD_NAME),property))", - } + }, ) - req = self._request('GET', endpoint) + req = self._request("GET", endpoint) content = self.getResponse(req) # Example of output: @@ -425,24 +431,26 @@ # ] associated_config = {} - for build_type in content.get('buildType', {}): - if 'parameters' not in build_type: + for build_type in content.get("buildType", {}): + if "parameters" not in build_type: continue - properties = build_type['parameters'].get('property', []) + properties = build_type["parameters"].get("property", []) for build_property in properties: # Because of our filter, the only possible property is the one we # are after. Looking at the value is enough. - config_name = build_property.get('value', None) + config_name = build_property.get("value", None) if config_name in config_names: - associated_config.update({ - config_name: { - "teamcity_build_type_id": build_type['id'], - "teamcity_build_name": build_type['name'], - "teamcity_project_id": build_type['project']['id'], - "teamcity_project_name": build_type['project']['name'], + associated_config.update( + { + config_name: { + "teamcity_build_type_id": build_type["id"], + "teamcity_build_name": build_type["name"], + "teamcity_project_id": build_type["project"]["id"], + "teamcity_project_name": build_type["project"]["name"], + } } - }) + ) return associated_config @@ -456,42 +464,32 @@ params["guest"] = 1 scheme, netloc = urlsplit(self.base_url)[0:2] - return urlunsplit(( - scheme, - netloc, - path, - urlencode(params, doseq=True), - fragment - )) + return urlunsplit( + (scheme, netloc, path, urlencode(params, doseq=True), fragment) + ) def convert_to_guest_url(self, url): parsed_url = urlsplit(url) # Don't touch unrelated URLs. parsed_base_url = urlsplit(self.base_url) - if parsed_base_url.scheme != parsed_url.scheme or parsed_base_url.netloc != parsed_url.netloc: + if ( + parsed_base_url.scheme != parsed_url.scheme + or parsed_base_url.netloc != parsed_url.netloc + ): return url return self.build_url( - parsed_url.path, - parse_qs(parsed_url.query), - parsed_url.fragment + parsed_url.path, parse_qs(parsed_url.query), parsed_url.fragment ) def _request(self, verb, url, data=None, headers=None): if self.logger: - self.logger.info(f'{verb}: {url}') + self.logger.info(f"{verb}: {url}") if headers is None: - headers = { - 'Accept': 'application/json', - 'Content-Type': 'application/json' - } - req = requests.Request( - verb, - url, - auth=self.auth, - headers=headers) + headers = {"Accept": "application/json", "Content-Type": "application/json"} + req = requests.Request(verb, url, auth=self.auth, headers=headers) req.data = data return req diff --git a/contrib/buildbot/test/abcbot_fixture.py b/contrib/buildbot/test/abcbot_fixture.py --- a/contrib/buildbot/test/abcbot_fixture.py +++ b/contrib/buildbot/test/abcbot_fixture.py @@ -26,7 +26,7 @@ class ABCBotFixture(unittest.TestCase): - def __init__(self, methodName='runTest'): + def __init__(self, methodName="runTest"): super().__init__(methodName) self.hmac_secret = "bmn6cwzynyo55jol2bazt6yz4gfhc7ry" @@ -36,12 +36,12 @@ os.environ["DEPLOYMENT_ENV"] = "prod" self.data_dir = Path(__file__).parent / "data" - self.credentials = base64.b64encode("{}:{}".format( - TEST_USER, TEST_PASSWORD).encode()).decode('utf-8') - self.headers = {'Authorization': 'Basic ' + self.credentials} + self.credentials = base64.b64encode( + f"{TEST_USER}:{TEST_PASSWORD}".encode() + ).decode("utf-8") + self.headers = {"Authorization": "Basic " + self.credentials} - self.test_output_dir = os.path.join( - os.path.dirname(__file__), "test_output") + self.test_output_dir = os.path.join(os.path.dirname(__file__), "test_output") self.db_file_no_ext = None def setUp(self): @@ -57,22 +57,24 @@ self.slackbot, self.cirrus, db_file_no_ext=self.db_file_no_ext, - jsonProvider=test.mocks.fixture.MockJSONProvider).test_client() + jsonProvider=test.mocks.fixture.MockJSONProvider, + ).test_client() def tearDown(self): pass def compute_hmac(self, data): - return hmac.new(self.hmac_secret.encode(), - data.encode(), hashlib.sha256).hexdigest() + return hmac.new( + self.hmac_secret.encode(), data.encode(), hashlib.sha256 + ).hexdigest() def post_data_with_hmac(self, path, headers, data): - headers['X-Phabricator-Webhook-Signature'] = self.compute_hmac(data) + headers["X-Phabricator-Webhook-Signature"] = self.compute_hmac(data) response = self.app.post(path, headers=headers, data=data) return response def post_json_with_hmac(self, path, headers, obj): data = json.dumps(obj) - headers['X-Phabricator-Webhook-Signature'] = self.compute_hmac(data) + headers["X-Phabricator-Webhook-Signature"] = self.compute_hmac(data) response = self.app.post(path, headers=headers, json=obj) return response diff --git a/contrib/buildbot/test/mocks/fixture.py b/contrib/buildbot/test/mocks/fixture.py --- a/contrib/buildbot/test/mocks/fixture.py +++ b/contrib/buildbot/test/mocks/fixture.py @@ -15,6 +15,7 @@ class MockData: pass + # TODO: When Python3.7 becomes the minimum version, remove MockJSONEncoder and # MockData base class. Decorate data classes with @dataclass from package # 'dataclasses' instead. diff --git a/contrib/buildbot/test/mocks/phabricator.py b/contrib/buildbot/test/mocks/phabricator.py --- a/contrib/buildbot/test/mocks/phabricator.py +++ b/contrib/buildbot/test/mocks/phabricator.py @@ -74,11 +74,13 @@ results = [] for i in range(total): revisionId = DEFAULT_REVISION_ID + i - results.append({ - 'id': revisionId, - 'phid': f'PHID-DREV-{revisionId}', - 'fields': { - 'authorPHID': f'PHID-USER-{DEFAULT_USER_ID + i}', + results.append( + { + "id": revisionId, + "phid": f"PHID-DREV-{revisionId}", + "fields": { + "authorPHID": f"PHID-USER-{DEFAULT_USER_ID + i}", + }, } - }) + ) return Result(results) diff --git a/contrib/buildbot/test/mocks/slackbot.py b/contrib/buildbot/test/mocks/slackbot.py --- a/contrib/buildbot/test/mocks/slackbot.py +++ b/contrib/buildbot/test/mocks/slackbot.py @@ -10,23 +10,23 @@ def instance(): channels = { - 'dev': '#test-dev-channel', - 'infra': '#infra-support-channel', + "dev": "#test-dev-channel", + "infra": "#infra-support-channel", } - slackbot = SlackBot(mock.Mock, 'slack-token', channels) + slackbot = SlackBot(mock.Mock, "slack-token", channels) return slackbot DEFAULT_USER_NUM = 1000 -DEFAULT_USER_ID = f'U{DEFAULT_USER_NUM}' +DEFAULT_USER_ID = f"U{DEFAULT_USER_NUM}" def userProfile(attributes=None): profile = { - 'real_name': 'Real Name', - 'real_name_normalized': 'Real Name Normalized', - 'display_name': 'Display Name', - 'display_name_normalized': 'Display Name Normalized', + "real_name": "Real Name", + "real_name_normalized": "Real Name Normalized", + "display_name": "Display Name", + "display_name_normalized": "Display Name Normalized", } if attributes: profile = {**profile, **attributes} @@ -35,21 +35,21 @@ def user(userId=DEFAULT_USER_ID, profile=None): # Slack userIds always begin with a 'U' character - assert userId[0] == 'U' + assert userId[0] == "U" if profile is None: profile = userProfile() return { - 'id': userId, - 'profile': profile, + "id": userId, + "profile": profile, } def users_list(total=1, initialUsers=None): users = initialUsers if initialUsers is not None else [] for i in range(len(users), total): - users.append(user(f'U{DEFAULT_USER_NUM + i}')) + users.append(user(f"U{DEFAULT_USER_NUM + i}")) return { - 'members': users, + "members": users, } diff --git a/contrib/buildbot/test/mocks/teamcity.py b/contrib/buildbot/test/mocks/teamcity.py --- a/contrib/buildbot/test/mocks/teamcity.py +++ b/contrib/buildbot/test/mocks/teamcity.py @@ -10,17 +10,14 @@ import requests from teamcity_wrapper import TeamCity -TEAMCITY_BASE_URL = 'https://teamcity.test' -TEAMCITY_CI_USER = 'teamcity-ci-user' +TEAMCITY_BASE_URL = "https://teamcity.test" +TEAMCITY_CI_USER = "teamcity-ci-user" DEFAULT_BUILD_ID = 123456 def instance(): - teamcity = TeamCity( - TEAMCITY_BASE_URL, - TEAMCITY_CI_USER, - "teamcity-users-password") + teamcity = TeamCity(TEAMCITY_BASE_URL, TEAMCITY_CI_USER, "teamcity-users-password") teamcity.session = mock.Mock() teamcity.session.send.return_value = mock.Mock() teamcity.session.send.return_value.status_code = requests.codes.ok @@ -36,11 +33,11 @@ def buildInfo_changes(commits=None): changes = [] for commit in commits or []: - changes.append({'version': commit}) + changes.append({"version": commit}) return { - 'count': len(changes), - 'change': changes, + "count": len(changes), + "change": changes, } @@ -49,29 +46,29 @@ propsList = [] return { - 'count': len(propsList), - 'property': propsList, + "count": len(propsList), + "property": propsList, } -def buildInfo_triggered(triggerType='vcs', username='test-username'): +def buildInfo_triggered(triggerType="vcs", username="test-username"): triggered = { - 'type': triggerType, + "type": triggerType, } - if triggerType == 'user': - triggered['user'] = { - 'username': username, + if triggerType == "user": + triggered["user"] = { + "username": username, } return triggered -def buildInfo(changes=None, properties=None, triggered=None, - build_id=None, buildqueue=False): +def buildInfo( + changes=None, properties=None, triggered=None, build_id=None, buildqueue=False +): if not changes: - changes = buildInfo_changes( - ['deadbeef00000111222333444555666777888000']) + changes = buildInfo_changes(["deadbeef00000111222333444555666777888000"]) if not triggered: triggered = buildInfo_triggered() @@ -86,31 +83,33 @@ # element, but if we are mocking the buildqueue endpoint, it should not be # there. output = { - 'id': build_id, - 'changes': changes, - 'triggered': triggered, - 'properties': properties, + "id": build_id, + "changes": changes, + "triggered": triggered, + "properties": properties, } if not buildqueue: - output = {'build': [output]} + output = {"build": [output]} return Response(json.dumps(output)) def buildInfo_automatedBuild(): - return buildInfo(triggered=buildInfo_triggered( - triggerType='user', username=TEAMCITY_CI_USER)) + return buildInfo( + triggered=buildInfo_triggered(triggerType="user", username=TEAMCITY_CI_USER) + ) -def buildInfo_userBuild(username='test-username'): - return buildInfo(triggered=buildInfo_triggered( - triggerType='user', username=username)) +def buildInfo_userBuild(username="test-username"): + return buildInfo( + triggered=buildInfo_triggered(triggerType="user", username=username) + ) def buildInfo_scheduledBuild(): - return buildInfo(triggered=buildInfo_triggered(triggerType='schedule')) + return buildInfo(triggered=buildInfo_triggered(triggerType="schedule")) def buildInfo_vcsCheckinBuild(): - return buildInfo(triggered=buildInfo_triggered(triggerType='vcs')) + return buildInfo(triggered=buildInfo_triggered(triggerType="vcs")) diff --git a/contrib/buildbot/test/test_build.py b/contrib/buildbot/test/test_build.py --- a/contrib/buildbot/test/test_build.py +++ b/contrib/buildbot/test/test_build.py @@ -48,18 +48,14 @@ build_ids = list(build_target.builds.keys()) for build_id in build_ids[:-1]: build_target.update_build_status(build_id, BuildStatus.Success) - self.assertEqual( - build_target.builds[build_id].status, - BuildStatus.Success) + self.assertEqual(build_target.builds[build_id].status, BuildStatus.Success) self.assertEqual(build_target.status(), BuildStatus.Running) self.assertEqual(build_target.is_finished(), False) # ... which will change the state to finished/success. build_id = build_ids[-1] build_target.update_build_status(build_id, BuildStatus.Success) - self.assertEqual( - build_target.builds[build_id].status, - BuildStatus.Success) + self.assertEqual(build_target.builds[build_id].status, BuildStatus.Success) self.assertEqual(build_target.status(), BuildStatus.Success) self.assertEqual(build_target.is_finished(), True) @@ -77,5 +73,5 @@ self.assertEqual(build_target.is_finished(), False) -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/contrib/buildbot/test/test_endpoint_backportcheck.py b/contrib/buildbot/test/test_endpoint_backportcheck.py --- a/contrib/buildbot/test/test_endpoint_backportcheck.py +++ b/contrib/buildbot/test/test_endpoint_backportcheck.py @@ -13,144 +13,191 @@ class EndpointBackportcheckTestCase(ABCBotFixture): def test_backportCheck_happyPath(self): - self.phab.differential.revision.search.return_value = test.mocks.phabricator.Result([{ - 'id': '1234', - 'fields': { - 'summary': 'This is a test summary' - }, - }]) + self.phab.differential.revision.search.return_value = ( + test.mocks.phabricator.Result( + [ + { + "id": "1234", + "fields": {"summary": "This is a test summary"}, + } + ] + ) + ) response = self.post_json_with_hmac( - '/backportCheck', self.headers, {'object': {'phid': '1234'}}) + "/backportCheck", self.headers, {"object": {"phid": "1234"}} + ) self.assertEqual(response.status_code, 200) self.phab.differential.revision.search.assert_called_with( - constraints={"phids": ['1234']}) + constraints={"phids": ["1234"]} + ) self.phab.differential.revision.edit.assert_not_called() def test_backportCheck_invalid_json(self): response = self.post_data_with_hmac( - '/backportCheck', self.headers, "not: a valid json") + "/backportCheck", self.headers, "not: a valid json" + ) self.assertEqual(response.status_code, 415) def test_backportCheck_hasNoPRs(self): # Despite potential matches for linking PRs, the phab API should not be # called to update the summary, even if the result would be the same. - self.phab.differential.revision.search.return_value = test.mocks.phabricator.Result([{ - 'id': '1234', - 'fields': { - 'summary': "This is a test summary `Ignore this backport PR2345` some text.\n" - "Some text ```Ignore this PR3456``` Some more text.\n" - "```\nPR4567 in a multi-line code block\nPR5678 in the same code block\n```\n" - " Ignore this indented PR4567" - "This is a test summary `Ignore this secp256k1 backport PR234` some text.\n" - "Some text ```Ignore this secp256k1 PR345``` Some more text.\n" - "```\nsecp256k1 PR456 in a multi-line code block\nsecp256k1 PR567 in the same code block\n```\n" - " Ignore this indented secp256k1 PR456", - }, - }]) + self.phab.differential.revision.search.return_value = test.mocks.phabricator.Result( + [ + { + "id": "1234", + "fields": { + "summary": ( + "This is a test summary `Ignore this backport PR2345` some" + " text.\nSome text ```Ignore this PR3456``` Some more" + " text.\n```\nPR4567 in a multi-line code block\nPR5678 in" + " the same code block\n```\n Ignore this indented" + " PR4567This is a test summary `Ignore this secp256k1" + " backport PR234` some text.\nSome text ```Ignore this" + " secp256k1 PR345``` Some more text.\n```\nsecp256k1 PR456" + " in a multi-line code block\nsecp256k1 PR567 in the same" + " code block\n```\n Ignore this indented secp256k1 PR456" + ), + }, + } + ] + ) response = self.post_json_with_hmac( - '/backportCheck', self.headers, {'object': {'phid': '1234'}}) + "/backportCheck", self.headers, {"object": {"phid": "1234"}} + ) self.assertEqual(response.status_code, 200) self.phab.differential.revision.search.assert_called_with( - constraints={'phids': ['1234']}) + constraints={"phids": ["1234"]} + ) self.phab.differential.revision.edit.assert_not_called() def test_backportCheck_hasPRs(self): - self.phab.differential.revision.search.return_value = test.mocks.phabricator.Result([{ - 'id': '1234', - 'fields': { - 'summary': "This is a test summary\n" - # Bitcoin Core references that are NOT hyperlinked - "Backport of Core PR2345 and PR34567\n" - "Backports with optional separators PR 2345 and PR#34567 and PR #4567\n" - "PR6789 outside of a code block `PR4567 inside a code block`\n" - "```PR4567 in a single-line code block```\n" - "```\nPR4567 in a multi-line code block\n```\n" - " PR4567 in a code block using indentation\n" - "Another backport PR567890\n" - # secp256k1 references that are NOT hyperlinked - "Backport of Secp256k1 PR23 and PR345\n" - "Backport of Secp256k1 PR 23 and PR#345 and PR #45\n" - "SECP256K1 PR678 outside of a code block `secp256k1 PR456 inside a code block`\n" - "```secp256k1 PR456 in a single-line code block```\n" - "```\nsecp256k1 PR456 in a multi-line code block\n```\n" - " secp256k1 PR456 in a code block using indentation\n" - "Another secp backport PR567\n" - # only canonical markdown is hyperlinked now - "random yourname#12345 repo that is not supported is not hyperlinked\n" - "a backport of secp256k1#894\n" - "this is a backport of core#16723 and of core-gui#2\n" - "this is a very unlikely backport of core#16723, core-gui#2 and secp256k1#253431\n" - "malformed backport of core#16400#16458 should only link to the first #\n" - - "```this is a very unlikely backport of core#16723, core-gui#2" - " and secp256k1#253431 in a single-code block```\n" - - "```\nthis is a very unlikely backport of core#16723, core-gui#2 " - "and secp256k1#253431 in a multi-line code block\n```\n" - - " this is a very unlikely backport of core#16723, core-gui#2 and " - "secp256k1#253431 in a code block using indentation\n" - - "this is a port of bchn#1234\n", - }, - }]) + self.phab.differential.revision.search.return_value = test.mocks.phabricator.Result( + [ + { + "id": "1234", + "fields": { + "summary": ( + "This is a test summary\n" + # Bitcoin Core references that are NOT hyperlinked + "Backport of Core PR2345 and PR34567\n" + "Backports with optional separators PR 2345 and PR#34567" + " and PR #4567\n" + "PR6789 outside of a code block `PR4567 inside a code" + " block`\n" + "```PR4567 in a single-line code block```\n" + "```\nPR4567 in a multi-line code block\n```\n" + " PR4567 in a code block using indentation\n" + "Another backport PR567890\n" + # secp256k1 references that are NOT hyperlinked + "Backport of Secp256k1 PR23 and PR345\n" + "Backport of Secp256k1 PR 23 and PR#345 and PR #45\n" + "SECP256K1 PR678 outside of a code block `secp256k1 PR456" + " inside a code block`\n" + "```secp256k1 PR456 in a single-line code block```\n" + "```\nsecp256k1 PR456 in a multi-line code block\n```\n" + " secp256k1 PR456 in a code block using indentation\n" + "Another secp backport PR567\n" + # only canonical markdown is hyperlinked now + "random yourname#12345 repo that is not supported is not" + " hyperlinked\n" + "a backport of secp256k1#894\n" + "this is a backport of core#16723 and of core-gui#2\n" + "this is a very unlikely backport of core#16723, core-gui#2" + " and secp256k1#253431\n" + "malformed backport of core#16400#16458 should only link to" + " the first #\n" + "```this is a very unlikely backport of core#16723," + " core-gui#2" + " and secp256k1#253431 in a single-code block```\n" + "```\nthis is a very unlikely backport of core#16723," + " core-gui#2 " + "and secp256k1#253431 in a multi-line code block\n```\n" + " this is a very unlikely backport of core#16723," + " core-gui#2 and " + "secp256k1#253431 in a code block using indentation\n" + "this is a port of bchn#1234\n" + ), + }, + } + ] + ) response = self.post_json_with_hmac( - '/backportCheck', self.headers, {'object': {'phid': '1234'}}) + "/backportCheck", self.headers, {"object": {"phid": "1234"}} + ) self.assertEqual(response.status_code, 200) self.phab.differential.revision.search.assert_called_with( - constraints={'phids': ['1234']}) - calls = [mock.call(transactions=[{ - "type": "summary", - "value": "This is a test summary\n" - # Bitcoin Core links - "Backport of Core PR2345 and PR34567\n" - "Backports with optional separators PR 2345 and PR#34567 and PR #4567\n" - "PR6789 outside of a code block `PR4567 inside a code block`\n" - "```PR4567 in a single-line code block```\n" - "```\nPR4567 in a multi-line code block\n```\n" - " PR4567 in a code block using indentation\n" - "Another backport PR567890\n" - # secp256k1 links - "Backport of Secp256k1 PR23 and PR345\n" - "Backport of Secp256k1 PR 23 and PR#345 and PR #45\n" - "SECP256K1 PR678 outside of a code block `secp256k1 PR456 inside a code block`\n" - "```secp256k1 PR456 in a single-line code block```\n" - "```\nsecp256k1 PR456 in a multi-line code block\n```\n" - " secp256k1 PR456 in a code block using indentation\n" - "Another secp backport PR567\n" - # only canonical markdown is hyperlinked now - "random yourname#12345 repo that is not supported is not hyperlinked\n" - "a backport of [[https://github.com/bitcoin-core/secp256k1/pull/894 " - "| secp256k1#894]]\n" - - "this is a backport of [[https://github.com/bitcoin/bitcoin/pull/16723 | " - "core#16723]] and of [[https://github.com/bitcoin-core/gui/pull/2 | core-gui#2]]\n" - - "this is a very unlikely backport of [[https://github.com/bitcoin/bitcoin/pull/16723 " - "| core#16723]], [[https://github.com/bitcoin-core/gui/pull/2 | core-gui#2]] " - "and [[https://github.com/bitcoin-core/secp256k1/pull/253431 | secp256k1#253431]]\n" - - "malformed backport of [[https://github.com/bitcoin/bitcoin/pull/16400 | " - "core#16400]]#16458 should only link to the first #\n" - - "```this is a very unlikely backport of core#16723, core-gui#2" - " and secp256k1#253431 in a single-code block```\n" - - "```\nthis is a very unlikely backport of core#16723, core-gui#2 " - "and secp256k1#253431 in a multi-line code block\n```\n" - - " this is a very unlikely backport of core#16723, core-gui#2 and " - "secp256k1#253431 in a code block using indentation\n" - - "this is a port of [[https://gitlab.com/bitcoin-cash-node/bitcoin-cash-node/merge_requests/1234 | " - "bchn#1234]]\n", - }], objectIdentifier='1234')] - self.phab.differential.revision.edit.assert_has_calls( - calls, any_order=True) - - -if __name__ == '__main__': + constraints={"phids": ["1234"]} + ) + calls = [ + mock.call( + transactions=[ + { + "type": "summary", + "value": ( + "This is a test summary\n" + # Bitcoin Core links + "Backport of Core PR2345 and PR34567\n" + "Backports with optional separators PR 2345 and PR#34567" + " and PR #4567\n" + "PR6789 outside of a code block `PR4567 inside a code" + " block`\n" + "```PR4567 in a single-line code block```\n" + "```\nPR4567 in a multi-line code block\n```\n" + " PR4567 in a code block using indentation\n" + "Another backport PR567890\n" + # secp256k1 links + "Backport of Secp256k1 PR23 and PR345\n" + "Backport of Secp256k1 PR 23 and PR#345 and PR #45\n" + "SECP256K1 PR678 outside of a code block `secp256k1 PR456" + " inside a code block`\n" + "```secp256k1 PR456 in a single-line code block```\n" + "```\nsecp256k1 PR456 in a multi-line code block\n```\n" + " secp256k1 PR456 in a code block using indentation\n" + "Another secp backport PR567\n" + # only canonical markdown is hyperlinked now + "random yourname#12345 repo that is not supported is not" + " hyperlinked\n" + "a backport of" + " [[https://github.com/bitcoin-core/secp256k1/pull/894 " + "| secp256k1#894]]\n" + "this is a backport of" + " [[https://github.com/bitcoin/bitcoin/pull/16723 | " + "core#16723]] and of" + " [[https://github.com/bitcoin-core/gui/pull/2 |" + " core-gui#2]]\n" + "this is a very unlikely backport of" + " [[https://github.com/bitcoin/bitcoin/pull/16723 " + "| core#16723]]," + " [[https://github.com/bitcoin-core/gui/pull/2 |" + " core-gui#2]] " + "and [[https://github.com/bitcoin-core/secp256k1/pull/253431" + " | secp256k1#253431]]\n" + "malformed backport of" + " [[https://github.com/bitcoin/bitcoin/pull/16400 | " + "core#16400]]#16458 should only link to the first #\n" + "```this is a very unlikely backport of core#16723," + " core-gui#2" + " and secp256k1#253431 in a single-code block```\n" + "```\nthis is a very unlikely backport of core#16723," + " core-gui#2 " + "and secp256k1#253431 in a multi-line code block\n```\n" + " this is a very unlikely backport of core#16723," + " core-gui#2 and " + "secp256k1#253431 in a code block using indentation\n" + "this is a port of" + " [[https://gitlab.com/bitcoin-cash-node/bitcoin-cash-node/merge_requests/1234 | " + "bchn#1234]]\n" + ), + } + ], + objectIdentifier="1234", + ) + ] + self.phab.differential.revision.edit.assert_has_calls(calls, any_order=True) + + +if __name__ == "__main__": unittest.main() diff --git a/contrib/buildbot/test/test_endpoint_build.py b/contrib/buildbot/test/test_endpoint_build.py --- a/contrib/buildbot/test/test_endpoint_build.py +++ b/contrib/buildbot/test/test_endpoint_build.py @@ -13,69 +13,89 @@ from testutil import AnyWith -class buildRequestQuery(): +class buildRequestQuery: def __init__(self): - self.buildTypeId = 'test-build-type-id' - self.PHID = 'buildPHID' + self.buildTypeId = "test-build-type-id" + self.PHID = "buildPHID" def __str__(self): - return "?{}".format("&".join("{}={}".format(key, value) - for key, value in self.__dict__.items())) + return "?{}".format( + "&".join("{}={}".format(key, value) for key, value in self.__dict__.items()) + ) class EndpointBuildTestCase(ABCBotFixture): def test_build(self): data = buildRequestQuery() triggerBuildResponse = test.mocks.teamcity.buildInfo( - test.mocks.teamcity.buildInfo_changes( - ['test-change']), buildqueue=True) + test.mocks.teamcity.buildInfo_changes(["test-change"]), buildqueue=True + ) self.teamcity.session.send.return_value = triggerBuildResponse - response = self.app.post(f'/build{data}', headers=self.headers) + response = self.app.post(f"/build{data}", headers=self.headers) self.assertEqual(response.status_code, 200) - self.teamcity.session.send.assert_called_with(AnyWith(requests.PreparedRequest, { - 'url': 'https://teamcity.test/app/rest/buildQueue', - 'body': json.dumps({ - 'branchName': 'refs/heads/master', - 'buildType': { - 'id': 'test-build-type-id', + self.teamcity.session.send.assert_called_with( + AnyWith( + requests.PreparedRequest, + { + "url": "https://teamcity.test/app/rest/buildQueue", + "body": json.dumps( + { + "branchName": "refs/heads/master", + "buildType": { + "id": "test-build-type-id", + }, + "properties": { + "property": [ + { + "name": "env.harborMasterTargetPHID", + "value": "buildPHID", + } + ], + }, + } + ), }, - 'properties': { - 'property': [{ - 'name': 'env.harborMasterTargetPHID', - 'value': 'buildPHID', - }], - }, - }), - })) + ) + ) def test_build_withAbcBuildName(self): data = buildRequestQuery() - data.abcBuildName = 'build-diff' + data.abcBuildName = "build-diff" triggerBuildResponse = test.mocks.teamcity.buildInfo( - test.mocks.teamcity.buildInfo_changes( - ['test-change']), buildqueue=True) + test.mocks.teamcity.buildInfo_changes(["test-change"]), buildqueue=True + ) self.teamcity.session.send.return_value = triggerBuildResponse - response = self.app.post(f'/build{data}', headers=self.headers) + response = self.app.post(f"/build{data}", headers=self.headers) self.assertEqual(response.status_code, 200) - self.teamcity.session.send.assert_called_with(AnyWith(requests.PreparedRequest, { - 'url': 'https://teamcity.test/app/rest/buildQueue', - 'body': json.dumps({ - 'branchName': 'refs/heads/master', - 'buildType': { - 'id': 'test-build-type-id', - }, - 'properties': { - 'property': [{ - 'name': 'env.ABC_BUILD_NAME', - 'value': 'build-diff', - }, { - 'name': 'env.harborMasterTargetPHID', - 'value': 'buildPHID', - }], + self.teamcity.session.send.assert_called_with( + AnyWith( + requests.PreparedRequest, + { + "url": "https://teamcity.test/app/rest/buildQueue", + "body": json.dumps( + { + "branchName": "refs/heads/master", + "buildType": { + "id": "test-build-type-id", + }, + "properties": { + "property": [ + { + "name": "env.ABC_BUILD_NAME", + "value": "build-diff", + }, + { + "name": "env.harborMasterTargetPHID", + "value": "buildPHID", + }, + ], + }, + } + ), }, - }), - })) + ) + ) -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/contrib/buildbot/test/test_endpoint_buildDiff.py b/contrib/buildbot/test/test_endpoint_buildDiff.py --- a/contrib/buildbot/test/test_endpoint_buildDiff.py +++ b/contrib/buildbot/test/test_endpoint_buildDiff.py @@ -17,15 +17,16 @@ from testutil import AnyWith -class buildDiffRequestQuery(): +class buildDiffRequestQuery: def __init__(self): self.stagingRef = "refs/tags/phabricator/diff/1234" self.targetPHID = "PHID-HMBT-123456" self.revisionId = "1234" def __str__(self): - return "?{}".format("&".join("{}={}".format(key, value) - for key, value in self.__dict__.items())) + return "?{}".format( + "&".join("{}={}".format(key, value) for key, value in self.__dict__.items()) + ) class EndpointBuildDiffTestCase(ABCBotFixture): @@ -46,12 +47,12 @@ "builds": mergedConfig, } self.phab.get_file_content_from_master = mock.Mock() - self.phab.get_file_content_from_master.return_value = json.dumps( - config) + self.phab.get_file_content_from_master.return_value = json.dumps(config) def call_buildDiff(expectedBuilds): self.teamcity.session.send.side_effect = [ - test.mocks.teamcity.buildInfo(build_id=build.build_id, buildqueue=True) for build in expectedBuilds + test.mocks.teamcity.buildInfo(build_id=build.build_id, buildqueue=True) + for build in expectedBuilds ] self.phab.differential.getcommitpaths = mock.Mock() @@ -61,9 +62,7 @@ "someotherdir/file2.txt", ] - response = self.app.post( - f'/buildDiff{data}', - headers=self.headers) + response = self.app.post(f"/buildDiff{data}", headers=self.headers) self.assertEqual(response.status_code, 200) self.phab.differential.getcommitpaths.assert_called() @@ -71,38 +70,45 @@ if len(expectedBuilds) == 0: self.phab.harbormaster.sendmessage.assert_called_with( - receiver=data.targetPHID, type="pass") + receiver=data.targetPHID, type="pass" + ) expected_calls = [ - call(AnyWith(requests.PreparedRequest, { - "url": "https://teamcity.test/app/rest/buildQueue", - "body": json.dumps({ - "branchName": data.stagingRef, - "buildType": { - "id": "BitcoinABC_BitcoinAbcStaging", - }, - 'properties': { - 'property': [ - { - 'name': 'env.ABC_BUILD_NAME', - 'value': build.name, - }, + call( + AnyWith( + requests.PreparedRequest, + { + "url": "https://teamcity.test/app/rest/buildQueue", + "body": json.dumps( { - 'name': 'env.ABC_REVISION', - 'value': data.revisionId, - }, - { - 'name': 'env.harborMasterTargetPHID', - 'value': data.targetPHID, - }, - ], + "branchName": data.stagingRef, + "buildType": { + "id": "BitcoinABC_BitcoinAbcStaging", + }, + "properties": { + "property": [ + { + "name": "env.ABC_BUILD_NAME", + "value": build.name, + }, + { + "name": "env.ABC_REVISION", + "value": data.revisionId, + }, + { + "name": "env.harborMasterTargetPHID", + "value": data.targetPHID, + }, + ], + }, + } + ), }, - }), - })) + ) + ) for build in expectedBuilds ] - self.teamcity.session.send.assert_has_calls( - expected_calls, any_order=True) + self.teamcity.session.send.assert_has_calls(expected_calls, any_order=True) self.teamcity.session.send.reset_mock() # No diff to run @@ -113,44 +119,51 @@ # Single diff build builds.append(Build(1, BuildStatus.Queued, "build-1")) - set_build_configuration({ - "build-1": { - "runOnDiff": True, - }, - }) + set_build_configuration( + { + "build-1": { + "runOnDiff": True, + }, + } + ) call_buildDiff(builds) # With matching file regex - set_build_configuration({ - "build-1": { - "runOnDiffRegex": ["dir/subdir/.*"], - }, - }) + set_build_configuration( + { + "build-1": { + "runOnDiffRegex": ["dir/subdir/.*"], + }, + } + ) call_buildDiff(builds) # With non-matching file regex - set_build_configuration({ - "build-1": { - "runOnDiffRegex": ["dir/nonmatching/.*"], - }, - }) + set_build_configuration( + { + "build-1": { + "runOnDiffRegex": ["dir/nonmatching/.*"], + }, + } + ) call_buildDiff([]) # Some builds match the file regex builds.append(Build(1, BuildStatus.Queued, "build-2")) - set_build_configuration({ - "build-1": { - "runOnDiffRegex": ["dir/nonmatching/.*"], - }, - "build-2": { - "runOnDiffRegex": ["someotherdir/file2.txt"], - }, - }) + set_build_configuration( + { + "build-1": { + "runOnDiffRegex": ["dir/nonmatching/.*"], + }, + "build-2": { + "runOnDiffRegex": ["someotherdir/file2.txt"], + }, + } + ) call_buildDiff([builds[1]]) # Lot of builds - builds = [Build(i, BuildStatus.Queued, f"build-{i}") - for i in range(10)] + builds = [Build(i, BuildStatus.Queued, f"build-{i}") for i in range(10)] buildConfig = {} for build in builds: buildConfig[build.name] = { @@ -162,22 +175,13 @@ # Using a template builds = [Build(1, BuildStatus.Queued, "build-1")] config = { - "templates": { - "template1": { - "runOnDiffRegex": ["dir/subdir/"] - } - }, - "builds": { - "build-1": { - "templates": ["template1"] - } - } + "templates": {"template1": {"runOnDiffRegex": ["dir/subdir/"]}}, + "builds": {"build-1": {"templates": ["template1"]}}, } self.phab.get_file_content_from_master = mock.Mock() - self.phab.get_file_content_from_master.return_value = json.dumps( - config) + self.phab.get_file_content_from_master.return_value = json.dumps(config) call_buildDiff(builds) -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/contrib/buildbot/test/test_endpoint_getCurrentUser.py b/contrib/buildbot/test/test_endpoint_getCurrentUser.py --- a/contrib/buildbot/test/test_endpoint_getCurrentUser.py +++ b/contrib/buildbot/test/test_endpoint_getCurrentUser.py @@ -10,9 +10,9 @@ class EndpointGetCurrentUserTestCase(ABCBotFixture): def test_currentUser(self): - rv = self.app.get('/getCurrentUser', headers=self.headers) + rv = self.app.get("/getCurrentUser", headers=self.headers) self.assertEqual(rv.data, TEST_USER.encode()) -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/contrib/buildbot/test/test_endpoint_land.py b/contrib/buildbot/test/test_endpoint_land.py --- a/contrib/buildbot/test/test_endpoint_land.py +++ b/contrib/buildbot/test/test_endpoint_land.py @@ -16,71 +16,83 @@ class landRequestData(test.mocks.fixture.MockData): def __init__(self): - self.revision = 'D1234' - self.conduitToken = 'U2FsdGVkX1/RI0AAAAAAAF46wjo3lSAxj1d1iqqkxks=' - self.committerName = 'User Name' - self.committerEmail = 'user@bitcoinabc.org' + self.revision = "D1234" + self.conduitToken = "U2FsdGVkX1/RI0AAAAAAAF46wjo3lSAxj1d1iqqkxks=" + self.committerName = "User Name" + self.committerEmail = "user@bitcoinabc.org" class EndpointLandTestCase(ABCBotFixture): def test_land_happyPath(self): data = landRequestData() triggerBuildResponse = test.mocks.teamcity.buildInfo( - test.mocks.teamcity.buildInfo_changes(['test-change'])) + test.mocks.teamcity.buildInfo_changes(["test-change"]) + ) self.teamcity.session.send.return_value = triggerBuildResponse - response = self.app.post('/land', headers=self.headers, json=data) - self.teamcity.session.send.assert_called_with(AnyWith(requests.PreparedRequest, { - 'url': 'https://teamcity.test/app/rest/buildQueue', - 'body': json.dumps({ - 'branchName': 'refs/heads/master', - 'buildType': { - 'id': 'BitcoinAbcLandBot', + response = self.app.post("/land", headers=self.headers, json=data) + self.teamcity.session.send.assert_called_with( + AnyWith( + requests.PreparedRequest, + { + "url": "https://teamcity.test/app/rest/buildQueue", + "body": json.dumps( + { + "branchName": "refs/heads/master", + "buildType": { + "id": "BitcoinAbcLandBot", + }, + "properties": { + "property": [ + { + "name": "env.ABC_REVISION", + "value": "D1234", + }, + { + "name": "env.ABC_CONDUIT_TOKEN", + "value": "U2FsdGVkX1/RI0AAAAAAAF46wjo3lSAxj1d1iqqkxks=", + }, + { + "name": "env.ABC_COMMITTER_NAME", + "value": "User Name", + }, + { + "name": "env.ABC_COMMITTER_EMAIL", + "value": "user@bitcoinabc.org", + }, + { + "name": "env.harborMasterTargetPHID", + "value": "UNRESOLVED", + }, + ], + }, + } + ), }, - 'properties': { - 'property': [{ - 'name': 'env.ABC_REVISION', - 'value': 'D1234', - }, { - 'name': 'env.ABC_CONDUIT_TOKEN', - 'value': 'U2FsdGVkX1/RI0AAAAAAAF46wjo3lSAxj1d1iqqkxks=', - }, { - 'name': 'env.ABC_COMMITTER_NAME', - 'value': 'User Name', - }, { - 'name': 'env.ABC_COMMITTER_EMAIL', - 'value': 'user@bitcoinabc.org', - }, { - 'name': 'env.harborMasterTargetPHID', - 'value': 'UNRESOLVED', - }], - }, - }), - })) + ) + ) self.assertEqual(response.status_code, 200) - self.assertEqual( - response.get_json(), json.loads( - triggerBuildResponse.content)) + self.assertEqual(response.get_json(), json.loads(triggerBuildResponse.content)) def test_land_invalid_json(self): data = "not: a valid json" - response = self.app.post('/land', headers=self.headers, data=data) + response = self.app.post("/land", headers=self.headers, data=data) self.assertEqual(response.status_code, 415) def test_land_missingArguments(self): # Test otherwise valid requests with each required argument missing. # All of them should fail with status code 400. requiredArgs = [ - 'revision', - 'conduitToken', - 'committerName', - 'committerEmail', + "revision", + "conduitToken", + "committerName", + "committerEmail", ] for arg in requiredArgs: data = landRequestData() - setattr(data, arg, '') - response = self.app.post('/land', headers=self.headers, json=data) + setattr(data, arg, "") + response = self.app.post("/land", headers=self.headers, json=data) self.assertEqual(response.status_code, 400) -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/contrib/buildbot/test/test_endpoint_status.py b/contrib/buildbot/test/test_endpoint_status.py --- a/contrib/buildbot/test/test_endpoint_status.py +++ b/contrib/buildbot/test/test_endpoint_status.py @@ -24,23 +24,24 @@ class statusRequestData(test.mocks.fixture.MockData): def __init__(self): - self.buildName = 'build-name' - self.projectName = 'bitcoin-abc-test' + self.buildName = "build-name" + self.projectName = "bitcoin-abc-test" self.buildId = DEFAULT_BUILD_ID - self.buildTypeId = 'build-type-id' - self.buildResult = 'success' - self.revision = 'commitHash' - self.branch = 'refs/heads/master' - self.buildTargetPHID = 'buildTargetPHID' + self.buildTypeId = "build-type-id" + self.buildResult = "success" + self.revision = "commitHash" + self.branch = "refs/heads/master" + self.buildTargetPHID = "buildTargetPHID" def __setattr__(self, name, value): super().__setattr__(name, value) - if name in ['buildId', 'buildTypeId']: + if name in ["buildId", "buildTypeId"]: self.buildURL = urljoin( test.mocks.teamcity.TEAMCITY_BASE_URL, - 'viewLog.html?buildTypeId={}&buildId={}'.format( - getattr(self, 'buildTypeId', ''), - getattr(self, 'buildId', ''))) + "viewLog.html?buildTypeId={}&buildId={}".format( + getattr(self, "buildTypeId", ""), getattr(self, "buildId", "") + ), + ) class EndpointStatusTestCase(ABCBotFixture): @@ -61,69 +62,96 @@ self.cirrus.get_default_branch_status = mock.Mock() self.cirrus.get_default_branch_status.return_value = BuildStatus.Success - def setup_master_failureAndTaskDoesNotExist(self, latestCompletedBuildId=DEFAULT_BUILD_ID, - numRecentFailedBuilds=0, numCommits=1, - userSearchFields=None, buildLogFile='testlog.zip'): + def setup_master_failureAndTaskDoesNotExist( + self, + latestCompletedBuildId=DEFAULT_BUILD_ID, + numRecentFailedBuilds=0, + numCommits=1, + userSearchFields=None, + buildLogFile="testlog.zip", + ): if userSearchFields is None: userSearchFields = {} self.phab.maniphest.edit.return_value = { - 'object': { - 'id': '890', - 'phid': 'PHID-TASK-890', + "object": { + "id": "890", + "phid": "PHID-TASK-890", }, } - with open(self.data_dir / buildLogFile, 'rb') as f: + with open(self.data_dir / buildLogFile, "rb") as f: buildLog = f.read() - recentBuilds = [] if numRecentFailedBuilds == 0 else [ - {'status': 'FAILURE'}, {'status': 'SUCCESS'}] * numRecentFailedBuilds + recentBuilds = ( + [] + if numRecentFailedBuilds == 0 + else [{"status": "FAILURE"}, {"status": "SUCCESS"}] * numRecentFailedBuilds + ) self.teamcity.session.send.side_effect = [ # Build failures test.mocks.teamcity.Response(), # Latest completed build - test.mocks.teamcity.Response(json.dumps({ - 'build': [{ - 'id': latestCompletedBuildId, - }], - })), + test.mocks.teamcity.Response( + json.dumps( + { + "build": [ + { + "id": latestCompletedBuildId, + } + ], + } + ) + ), test.mocks.teamcity.Response(status_code=requests.codes.not_found), test.mocks.teamcity.Response(buildLog), - test.mocks.teamcity.Response(json.dumps({ - 'build': recentBuilds, - })), + test.mocks.teamcity.Response( + json.dumps( + { + "build": recentBuilds, + } + ) + ), ] commits = [] for i in range(numCommits): commitId = 8000 + i - commits.append({ - 'phid': f'PHID-COMMIT-{commitId}', - 'fields': { - 'identifier': f'deadbeef0000011122233344455566677788{commitId}' - }, - }) + commits.append( + { + "phid": f"PHID-COMMIT-{commitId}", + "fields": { + "identifier": f"deadbeef0000011122233344455566677788{commitId}" + }, + } + ) self.phab.diffusion.commit.search.return_value = test.mocks.phabricator.Result( - commits) + commits + ) - revisionSearchResult = test.mocks.phabricator.differential_revision_search_result( - total=numCommits) + revisionSearchResult = ( + test.mocks.phabricator.differential_revision_search_result(total=numCommits) + ) revisions = [] for i in range(numCommits): - revisions.append({ - 'sourcePHID': f'PHID-COMMIT-{8000 + i}', - 'destinationPHID': revisionSearchResult.data[i]['phid'], - }) - self.phab.edge.search.return_value = test.mocks.phabricator.Result( - revisions) + revisions.append( + { + "sourcePHID": f"PHID-COMMIT-{8000 + i}", + "destinationPHID": revisionSearchResult.data[i]["phid"], + } + ) + self.phab.edge.search.return_value = test.mocks.phabricator.Result(revisions) self.phab.differential.revision.search.return_value = revisionSearchResult - self.phab.user.search.return_value = test.mocks.phabricator.Result([{ - 'id': '5678', - 'phid': revisionSearchResult.data[0]['fields']['authorPHID'], - 'fields': userSearchFields, - }]) + self.phab.user.search.return_value = test.mocks.phabricator.Result( + [ + { + "id": "5678", + "phid": revisionSearchResult.data[0]["fields"]["authorPHID"], + "fields": userSearchFields, + } + ] + ) def configure_build_info(self, **kwargs): self.teamcity.getBuildInfo.return_value = BuildInfo.fromSingleBuildResponse( @@ -132,26 +160,26 @@ def test_status_invalid_json(self): data = "not: a valid json" - response = self.app.post('/status', headers=self.headers, data=data) + response = self.app.post("/status", headers=self.headers, data=data) self.assertEqual(response.status_code, 415) def test_status_noData(self): - response = self.app.post('/status', headers=self.headers) + response = self.app.post("/status", headers=self.headers) self.assertEqual(response.status_code, 415) self.phab.harbormaster.createartifact.assert_not_called() def test_status_unresolved(self): data = statusRequestData() - data.branch = 'UNRESOLVED' - data.buildTargetPHID = 'UNRESOLVED' - response = self.app.post('/status', headers=self.headers, json=data) + data.branch = "UNRESOLVED" + data.buildTargetPHID = "UNRESOLVED" + response = self.app.post("/status", headers=self.headers, json=data) self.assertEqual(response.status_code, 400) self.phab.harbormaster.createartifact.assert_not_called() def test_status_ignoredBuild(self): data = statusRequestData() - data.buildTypeId = 'build-name__BOTIGNORE' - response = self.app.post('/status', headers=self.headers, json=data) + data.buildTypeId = "build-name__BOTIGNORE" + response = self.app.post("/status", headers=self.headers, json=data) self.assertEqual(response.status_code, 200) self.phab.harbormaster.createartifact.assert_not_called() @@ -159,13 +187,19 @@ data = statusRequestData() self.teamcity.session.send.side_effect = [ test.mocks.teamcity.buildInfo_automatedBuild(), - test.mocks.teamcity.Response(json.dumps({ - 'build': [{ - 'id': DEFAULT_BUILD_ID, - }], - })), + test.mocks.teamcity.Response( + json.dumps( + { + "build": [ + { + "id": DEFAULT_BUILD_ID, + } + ], + } + ) + ), ] - response = self.app.post('/status', headers=self.headers, json=data) + response = self.app.post("/status", headers=self.headers, json=data) self.assertEqual(response.status_code, 200) self.phab.differential.revision.edit.assert_not_called() self.phab.maniphest.edit.assert_not_called() @@ -177,251 +211,337 @@ test.mocks.teamcity.Response(), test.mocks.teamcity.Response(), ] - if data.buildResult == 'failure': - with open(self.data_dir / 'testlog.zip', 'rb') as f: + if data.buildResult == "failure": + with open(self.data_dir / "testlog.zip", "rb") as f: buildLog = f.read() afterLatestBuild = [ - test.mocks.teamcity.Response( - status_code=requests.codes.not_found), + test.mocks.teamcity.Response(status_code=requests.codes.not_found), test.mocks.teamcity.Response(buildLog), test.mocks.teamcity.Response(), ] self.teamcity.session.send.side_effect = [ test.mocks.teamcity.buildInfo_automatedBuild(), - test.mocks.teamcity.Response(json.dumps({ - 'build': [{ - 'id': DEFAULT_BUILD_ID, - }], - })), + test.mocks.teamcity.Response( + json.dumps( + { + "build": [ + { + "id": DEFAULT_BUILD_ID, + } + ], + } + ) + ), ] + afterLatestBuild - self.phab.maniphest.search.return_value = test.mocks.phabricator.Result([{ - 'id': '123', - 'phid': 'PHID-TASK-123', - }]) + self.phab.maniphest.search.return_value = test.mocks.phabricator.Result( + [ + { + "id": "123", + "phid": "PHID-TASK-123", + } + ] + ) self.phab.maniphest.edit.return_value = { - 'object': { - 'id': '123', - 'phid': 'PHID-TASK-123', + "object": { + "id": "123", + "phid": "PHID-TASK-123", }, } data = statusRequestData() - data.buildResult = 'failure' + data.buildResult = "failure" setupMockResponses(data) - response = self.app.post('/status', headers=self.headers, json=data) + response = self.app.post("/status", headers=self.headers, json=data) self.assertEqual(response.status_code, 200) # Master should be marked red data = statusRequestData() setupMockResponses(data) - response = self.app.post('/status', headers=self.headers, json=data) + response = self.app.post("/status", headers=self.headers, json=data) self.assertEqual(response.status_code, 200) self.phab.differential.revision.edit.assert_not_called() - self.phab.maniphest.edit.assert_called_with(transactions=[{ - 'type': 'status', - 'value': 'resolved', - }], objectIdentifier='PHID-TASK-123') + self.phab.maniphest.edit.assert_called_with( + transactions=[ + { + "type": "status", + "value": "resolved", + } + ], + objectIdentifier="PHID-TASK-123", + ) self.slackbot.client.chat_postMessage.assert_called_with( - channel='#test-dev-channel', - text="Master is green again.") + channel="#test-dev-channel", text="Master is green again." + ) def test_status_master_resolveBrokenBuildTask_masterStillRed(self): data = statusRequestData() self.configure_build_info( triggered=test.mocks.teamcity.buildInfo_triggered( - triggerType='user', username=TEAMCITY_CI_USER) + triggerType="user", username=TEAMCITY_CI_USER + ) ) # Check build failure self.teamcity.session.send.side_effect = [ - test.mocks.teamcity.Response(json.dumps({ - 'build': [{ - 'id': DEFAULT_BUILD_ID, - }], - })), - test.mocks.teamcity.Response(json.dumps({ - 'problemOccurrence': [{ - 'build': { - 'buildTypeId': 'build-type', - }, - }], - })), + test.mocks.teamcity.Response( + json.dumps( + { + "build": [ + { + "id": DEFAULT_BUILD_ID, + } + ], + } + ) + ), + test.mocks.teamcity.Response( + json.dumps( + { + "problemOccurrence": [ + { + "build": { + "buildTypeId": "build-type", + }, + } + ], + } + ) + ), test.mocks.teamcity.Response(), ] - self.phab.maniphest.search.return_value = test.mocks.phabricator.Result([{ - 'id': '123', - 'phid': 'PHID-TASK-123', - }]) + self.phab.maniphest.search.return_value = test.mocks.phabricator.Result( + [ + { + "id": "123", + "phid": "PHID-TASK-123", + } + ] + ) self.phab.maniphest.edit.return_value = { - 'object': { - 'id': '123', - 'phid': 'PHID-TASK-123', + "object": { + "id": "123", + "phid": "PHID-TASK-123", }, } - response = self.app.post('/status', headers=self.headers, json=data) + response = self.app.post("/status", headers=self.headers, json=data) self.assertEqual(response.status_code, 200) self.phab.differential.revision.edit.assert_not_called() - self.phab.maniphest.edit.assert_called_with(transactions=[{ - 'type': 'status', - 'value': 'resolved', - }], objectIdentifier='PHID-TASK-123') + self.phab.maniphest.edit.assert_called_with( + transactions=[ + { + "type": "status", + "value": "resolved", + } + ], + objectIdentifier="PHID-TASK-123", + ) self.slackbot.client.chat_postMessage.assert_not_called() # Check test failure self.teamcity.session.send.side_effect = [ - test.mocks.teamcity.Response(json.dumps({ - 'build': [{ - 'id': DEFAULT_BUILD_ID, - }], - })), + test.mocks.teamcity.Response( + json.dumps( + { + "build": [ + { + "id": DEFAULT_BUILD_ID, + } + ], + } + ) + ), test.mocks.teamcity.Response(), - test.mocks.teamcity.Response(json.dumps({ - 'testOccurrence': [{ - 'build': { - 'buildTypeId': 'build-type', - }, - }], - })), + test.mocks.teamcity.Response( + json.dumps( + { + "testOccurrence": [ + { + "build": { + "buildTypeId": "build-type", + }, + } + ], + } + ) + ), ] - response = self.app.post('/status', headers=self.headers, json=data) + response = self.app.post("/status", headers=self.headers, json=data) self.assertEqual(response.status_code, 200) self.phab.differential.revision.edit.assert_not_called() - self.phab.maniphest.edit.assert_called_with(transactions=[{ - 'type': 'status', - 'value': 'resolved', - }], objectIdentifier='PHID-TASK-123') + self.phab.maniphest.edit.assert_called_with( + transactions=[ + { + "type": "status", + "value": "resolved", + } + ], + objectIdentifier="PHID-TASK-123", + ) self.slackbot.client.chat_postMessage.assert_not_called() def test_status_master_resolveBrokenBuild_outOfOrderBuilds(self): data = statusRequestData() self.teamcity.session.send.side_effect = [ test.mocks.teamcity.buildInfo_automatedBuild(), - test.mocks.teamcity.Response(json.dumps({ - 'build': [{ - # Another build of the same type that was started after this build - # has already completed. Do not treat master as green/fixed based - # on this build, since the most recent build may have - # failed. - 'id': 234567, - }], - })), + test.mocks.teamcity.Response( + json.dumps( + { + "build": [ + { + # Another build of the same type that was started after this build + # has already completed. Do not treat master as green/fixed based + # on this build, since the most recent build may have + # failed. + "id": 234567, + } + ], + } + ) + ), ] - response = self.app.post('/status', headers=self.headers, json=data) + response = self.app.post("/status", headers=self.headers, json=data) self.assertEqual(response.status_code, 200) self.phab.differential.revision.edit.assert_not_called() self.phab.maniphest.edit.assert_not_called() self.slackbot.client.chat_postMessage.assert_not_called() - self.teamcity.session.send.assert_called_with(AnyWith(requests.PreparedRequest, { - 'url': self.teamcity.build_url( - "app/rest/builds", + self.teamcity.session.send.assert_called_with( + AnyWith( + requests.PreparedRequest, { - "locator": "buildType:build-type-id", - "fields": "build(id)", - "count": 1, - } + "url": self.teamcity.build_url( + "app/rest/builds", + { + "locator": "buildType:build-type-id", + "fields": "build(id)", + "count": 1, + }, + ) + }, ) - })) + ) def test_status_infraFailure(self): # Test an infra failure on master data = statusRequestData() - data.buildResult = 'failure' + data.buildResult = "failure" - with open(self.data_dir / 'testlog_infrafailure.zip', 'rb') as f: + with open(self.data_dir / "testlog_infrafailure.zip", "rb") as f: buildLog = f.read() def setupTeamcity(): self.configure_build_info( triggered=test.mocks.teamcity.buildInfo_triggered( - triggerType='user', username=TEAMCITY_CI_USER) + triggerType="user", username=TEAMCITY_CI_USER + ) ) self.teamcity.session.send.side_effect = [ - test.mocks.teamcity.Response(json.dumps({ - 'problemOccurrence': [{ - 'id': 'id:2500,build:(id:56789)', - }], - })), test.mocks.teamcity.Response( - status_code=requests.codes.not_found), + json.dumps( + { + "problemOccurrence": [ + { + "id": "id:2500,build:(id:56789)", + } + ], + } + ) + ), + test.mocks.teamcity.Response(status_code=requests.codes.not_found), test.mocks.teamcity.Response(buildLog), ] setupTeamcity() - response = self.app.post('/status', headers=self.headers, json=data) + response = self.app.post("/status", headers=self.headers, json=data) self.assertEqual(response.status_code, 200) self.phab.differential.revision.edit.assert_not_called() self.phab.maniphest.edit.assert_not_called() def verifyInfraChannelMessage(): self.slackbot.client.chat_postMessage.assert_called_with( - channel='#infra-support-channel', - text=" There was an infrastructure failure in 'build-name': {}".format( - self.teamcity.build_url( - "viewLog.html", - { - "buildTypeId": data.buildTypeId, - "buildId": DEFAULT_BUILD_ID, - } + channel="#infra-support-channel", + text=( + " There was an infrastructure failure in" + " 'build-name': {}".format( + self.teamcity.build_url( + "viewLog.html", + { + "buildTypeId": data.buildTypeId, + "buildId": DEFAULT_BUILD_ID, + }, + ) ) - )) + ), + ) + verifyInfraChannelMessage() # Test an infra failure on a revision data = statusRequestData() - data.branch = 'phabricator/diff/456' - data.buildResult = 'failure' + data.branch = "phabricator/diff/456" + data.buildResult = "failure" setupTeamcity() - self.phab.differential.diff.search.return_value = test.mocks.phabricator.Result([{ - 'id': '456', - 'fields': { - 'revisionPHID': '789' - }, - }]) + self.phab.differential.diff.search.return_value = test.mocks.phabricator.Result( + [ + { + "id": "456", + "fields": {"revisionPHID": "789"}, + } + ] + ) - response = self.app.post('/status', headers=self.headers, json=data) + response = self.app.post("/status", headers=self.headers, json=data) self.assertEqual(response.status_code, 200) - self.phab.differential.revision.edit.assert_called_with(transactions=[{ - "type": "comment", - "value": "(IMPORTANT) The build failed due to an unexpected infrastructure outage. " - "The administrators have been notified to investigate. Sorry for the inconvenience.", - }], objectIdentifier='789') + self.phab.differential.revision.edit.assert_called_with( + transactions=[ + { + "type": "comment", + "value": ( + "(IMPORTANT) The build failed due to an unexpected" + " infrastructure outage. The administrators have been notified" + " to investigate. Sorry for the inconvenience." + ), + } + ], + objectIdentifier="789", + ) self.phab.maniphest.edit.assert_not_called() verifyInfraChannelMessage() def test_status_master_failureAndTaskDoesNotExist_outOfOrderBuilds(self): data = statusRequestData() - data.buildResult = 'failure' + data.buildResult = "failure" # Another build of the same type that was started after this build # has already completed. Do not treat master as red/broken based # on this build, since the most recent build may have succeeded. - self.setup_master_failureAndTaskDoesNotExist( - latestCompletedBuildId=234567) + self.setup_master_failureAndTaskDoesNotExist(latestCompletedBuildId=234567) - response = self.app.post('/status', headers=self.headers, json=data) + response = self.app.post("/status", headers=self.headers, json=data) self.assertEqual(response.status_code, 200) self.phab.differential.revision.edit.assert_not_called() self.phab.maniphest.edit.assert_not_called() self.slackbot.client.chat_postMessage.assert_not_called() - def test_status_master_failureAndTaskDoesNotExist_doNotIgnoreComments( - self): + def test_status_master_failureAndTaskDoesNotExist_doNotIgnoreComments(self): data = statusRequestData() - data.buildResult = 'failure' + data.buildResult = "failure" - self.setup_master_failureAndTaskDoesNotExist(userSearchFields={ - 'username': 'author-phab-username', - 'custom.abc:slack-username': '', - }) + self.setup_master_failureAndTaskDoesNotExist( + userSearchFields={ + "username": "author-phab-username", + "custom.abc:slack-username": "", + } + ) self.slackbot.client.users_list.return_value = test.mocks.slackbot.users_list( - total=2) + total=2 + ) # Make sure comment patterns do not give false positives - self.teamcity.getIgnoreList.return_value = [b'# TOTAL', b' # TOTAL'] + self.teamcity.getIgnoreList.return_value = [b"# TOTAL", b" # TOTAL"] - response = self.app.post('/status', headers=self.headers, json=data) + response = self.app.post("/status", headers=self.headers, json=data) assert response.status_code == 200 self.phab.differential.revision.edit.assert_not_called() @@ -432,297 +552,395 @@ def test_status_master_failureAndTaskDoesNotExist_authorDefaultName(self): data = statusRequestData() - data.buildResult = 'failure' + data.buildResult = "failure" - self.setup_master_failureAndTaskDoesNotExist(userSearchFields={ - 'username': 'author-phab-username', - 'custom.abc:slack-username': '', - }) + self.setup_master_failureAndTaskDoesNotExist( + userSearchFields={ + "username": "author-phab-username", + "custom.abc:slack-username": "", + } + ) self.slackbot.client.users_list.return_value = test.mocks.slackbot.users_list( - total=2) + total=2 + ) - response = self.app.post('/status', headers=self.headers, json=data) + response = self.app.post("/status", headers=self.headers, json=data) self.assertEqual(response.status_code, 200) self.phab.differential.revision.edit.assert_not_called() - maniphestEditCalls = [mock.call(transactions=[{ - "type": "title", - "value": "Build build-name is broken.", - }, { - "type": "priority", - "value": "unbreak", - }, { - "type": "description", - "value": "[[ {} | build-name ]] is broken on branch 'refs/heads/master'\n\nAssociated commits:\nrABCdeadbeef00000111222333444555666777888000".format( - self.teamcity.build_url( - "viewLog.html", + maniphestEditCalls = [ + mock.call( + transactions=[ { - "buildTypeId": data.buildTypeId, - "buildId": DEFAULT_BUILD_ID, - } - ) - ), - }])] - self.phab.maniphest.edit.assert_has_calls( - maniphestEditCalls, any_order=False) - - self.phab.diffusion.commit.search.assert_called_with(constraints={ - "repositories": [BITCOIN_ABC_REPO], - "identifiers": ['deadbeef00000111222333444555666777888000'], - }) + "type": "title", + "value": "Build build-name is broken.", + }, + { + "type": "priority", + "value": "unbreak", + }, + { + "type": "description", + "value": ( + "[[ {} | build-name ]] is broken on branch" + " 'refs/heads/master'\n\nAssociated" + " commits:\nrABCdeadbeef00000111222333444555666777888000" + .format( + self.teamcity.build_url( + "viewLog.html", + { + "buildTypeId": data.buildTypeId, + "buildId": DEFAULT_BUILD_ID, + }, + ) + ) + ), + }, + ] + ) + ] + self.phab.maniphest.edit.assert_has_calls(maniphestEditCalls, any_order=False) + + self.phab.diffusion.commit.search.assert_called_with( + constraints={ + "repositories": [BITCOIN_ABC_REPO], + "identifiers": ["deadbeef00000111222333444555666777888000"], + } + ) self.phab.edge.search.assert_called_with( - types=['commit.revision'], sourcePHIDs=['PHID-COMMIT-8000']) + types=["commit.revision"], sourcePHIDs=["PHID-COMMIT-8000"] + ) self.phab.differential.revision.search.assert_called_with( - constraints={"phids": ['PHID-DREV-1000']}) + constraints={"phids": ["PHID-DREV-1000"]} + ) self.slackbot.client.chat_postMessage.assert_called_with( - channel='#test-dev-channel', - text="Committer: author-phab-username\n" - "Build 'build-name' appears to be broken: {}\n" - "Task: https://reviews.bitcoinabc.org/T890\n" - "Diff: https://reviews.bitcoinabc.org/D{}".format( - self.teamcity.build_url( - "viewLog.html", - { - "buildId": DEFAULT_BUILD_ID, - } - ), - test.mocks.phabricator.DEFAULT_REVISION_ID, - ) - ) - - def test_status_master_failureAndTaskDoesNotExist_authorSlackUsername( - self): + channel="#test-dev-channel", + text=( + "Committer: author-phab-username\n" + "Build 'build-name' appears to be broken: {}\n" + "Task: https://reviews.bitcoinabc.org/T890\n" + "Diff: https://reviews.bitcoinabc.org/D{}".format( + self.teamcity.build_url( + "viewLog.html", + { + "buildId": DEFAULT_BUILD_ID, + }, + ), + test.mocks.phabricator.DEFAULT_REVISION_ID, + ) + ), + ) + + def test_status_master_failureAndTaskDoesNotExist_authorSlackUsername(self): data = statusRequestData() - data.buildResult = 'failure' + data.buildResult = "failure" slackUserProfile = test.mocks.slackbot.userProfile( - {'real_name': 'author-slack-username'}) - slackUser = test.mocks.slackbot.user( - userId='U8765', profile=slackUserProfile) - self.setup_master_failureAndTaskDoesNotExist(userSearchFields={ - 'username': 'author-phab-username', - 'custom.abc:slack-username': 'author-slack-username', - }) + {"real_name": "author-slack-username"} + ) + slackUser = test.mocks.slackbot.user(userId="U8765", profile=slackUserProfile) + self.setup_master_failureAndTaskDoesNotExist( + userSearchFields={ + "username": "author-phab-username", + "custom.abc:slack-username": "author-slack-username", + } + ) self.slackbot.client.users_list.return_value = test.mocks.slackbot.users_list( - total=2, initialUsers=[slackUser]) + total=2, initialUsers=[slackUser] + ) - response = self.app.post('/status', headers=self.headers, json=data) + response = self.app.post("/status", headers=self.headers, json=data) self.assertEqual(response.status_code, 200) self.phab.differential.revision.edit.assert_not_called() - maniphestEditCalls = [mock.call(transactions=[{ - "type": "title", - "value": "Build build-name is broken.", - }, { - "type": "priority", - "value": "unbreak", - }, { - "type": "description", - "value": "[[ {} | build-name ]] is broken on branch 'refs/heads/master'\n\nAssociated commits:\nrABCdeadbeef00000111222333444555666777888000".format( - self.teamcity.build_url( - "viewLog.html", + maniphestEditCalls = [ + mock.call( + transactions=[ { - "buildTypeId": data.buildTypeId, - "buildId": DEFAULT_BUILD_ID, - } - ) - ), - }])] - self.phab.maniphest.edit.assert_has_calls( - maniphestEditCalls, any_order=False) - - self.phab.diffusion.commit.search.assert_called_with(constraints={ - "repositories": [BITCOIN_ABC_REPO], - "identifiers": ['deadbeef00000111222333444555666777888000'], - }) + "type": "title", + "value": "Build build-name is broken.", + }, + { + "type": "priority", + "value": "unbreak", + }, + { + "type": "description", + "value": ( + "[[ {} | build-name ]] is broken on branch" + " 'refs/heads/master'\n\nAssociated" + " commits:\nrABCdeadbeef00000111222333444555666777888000" + .format( + self.teamcity.build_url( + "viewLog.html", + { + "buildTypeId": data.buildTypeId, + "buildId": DEFAULT_BUILD_ID, + }, + ) + ) + ), + }, + ] + ) + ] + self.phab.maniphest.edit.assert_has_calls(maniphestEditCalls, any_order=False) + + self.phab.diffusion.commit.search.assert_called_with( + constraints={ + "repositories": [BITCOIN_ABC_REPO], + "identifiers": ["deadbeef00000111222333444555666777888000"], + } + ) self.phab.edge.search.assert_called_with( - types=['commit.revision'], sourcePHIDs=['PHID-COMMIT-8000']) + types=["commit.revision"], sourcePHIDs=["PHID-COMMIT-8000"] + ) self.phab.differential.revision.search.assert_called_with( - constraints={"phids": ['PHID-DREV-1000']}) + constraints={"phids": ["PHID-DREV-1000"]} + ) self.slackbot.client.chat_postMessage.assert_called_with( - channel='#test-dev-channel', - text="Committer: <@U8765>\n" - "Build 'build-name' appears to be broken: {}\n" - "Task: https://reviews.bitcoinabc.org/T890\n" - "Diff: https://reviews.bitcoinabc.org/D{}".format( - self.teamcity.build_url( - "viewLog.html", - { - "buildId": DEFAULT_BUILD_ID, - } - ), - test.mocks.phabricator.DEFAULT_REVISION_ID, - ) + channel="#test-dev-channel", + text=( + "Committer: <@U8765>\n" + "Build 'build-name' appears to be broken: {}\n" + "Task: https://reviews.bitcoinabc.org/T890\n" + "Diff: https://reviews.bitcoinabc.org/D{}".format( + self.teamcity.build_url( + "viewLog.html", + { + "buildId": DEFAULT_BUILD_ID, + }, + ), + test.mocks.phabricator.DEFAULT_REVISION_ID, + ) + ), ) def test_status_master_failureAndTaskDoesNotExist_scheduledBuild(self): data = statusRequestData() - data.buildResult = 'failure' + data.buildResult = "failure" self.configure_build_info( - triggered=test.mocks.teamcity.buildInfo_triggered( - triggerType='schedule') + triggered=test.mocks.teamcity.buildInfo_triggered(triggerType="schedule") ) self.setup_master_failureAndTaskDoesNotExist() - response = self.app.post('/status', headers=self.headers, json=data) + response = self.app.post("/status", headers=self.headers, json=data) self.assertEqual(response.status_code, 200) self.phab.differential.revision.edit.assert_not_called() - maniphestEditCalls = [mock.call(transactions=[{ - "type": "title", - "value": "Build build-name is broken.", - }, { - "type": "priority", - "value": "unbreak", - }, { - "type": "description", - "value": "[[ {} | build-name ]] is broken on branch 'refs/heads/master'\n\nAssociated commits:\nrABCdeadbeef00000111222333444555666777888000".format( - self.teamcity.build_url( - "viewLog.html", + maniphestEditCalls = [ + mock.call( + transactions=[ { - "buildTypeId": data.buildTypeId, - "buildId": DEFAULT_BUILD_ID, - } - ) - ), - }])] - self.phab.maniphest.edit.assert_has_calls( - maniphestEditCalls, any_order=False) + "type": "title", + "value": "Build build-name is broken.", + }, + { + "type": "priority", + "value": "unbreak", + }, + { + "type": "description", + "value": ( + "[[ {} | build-name ]] is broken on branch" + " 'refs/heads/master'\n\nAssociated" + " commits:\nrABCdeadbeef00000111222333444555666777888000" + .format( + self.teamcity.build_url( + "viewLog.html", + { + "buildTypeId": data.buildTypeId, + "buildId": DEFAULT_BUILD_ID, + }, + ) + ) + ), + }, + ] + ) + ] + self.phab.maniphest.edit.assert_has_calls(maniphestEditCalls, any_order=False) self.slackbot.client.chat_postMessage.assert_called_with( - channel='#test-dev-channel', - text="Scheduled build 'build-name' appears to be broken: {}\n" - "Task: https://reviews.bitcoinabc.org/T890".format( - self.teamcity.build_url( - "viewLog.html", - { - "buildId": DEFAULT_BUILD_ID, - } - ) - )) + channel="#test-dev-channel", + text=( + "Scheduled build 'build-name' appears to be broken: {}\n" + "Task: https://reviews.bitcoinabc.org/T890".format( + self.teamcity.build_url( + "viewLog.html", + { + "buildId": DEFAULT_BUILD_ID, + }, + ) + ) + ), + ) def test_status_master_failureAndTaskDoesNotExist_multipleChanges(self): data = statusRequestData() - data.buildResult = 'failure' + data.buildResult = "failure" self.configure_build_info( - changes=test.mocks.teamcity.buildInfo_changes([ - 'deadbeef00000111222333444555666777888000', - 'deadbeef00000111222333444555666777888001']), - triggered=test.mocks.teamcity.buildInfo_triggered(triggerType='user', - username=test.mocks.teamcity.TEAMCITY_CI_USER), + changes=test.mocks.teamcity.buildInfo_changes( + [ + "deadbeef00000111222333444555666777888000", + "deadbeef00000111222333444555666777888001", + ] + ), + triggered=test.mocks.teamcity.buildInfo_triggered( + triggerType="user", username=test.mocks.teamcity.TEAMCITY_CI_USER + ), ) self.setup_master_failureAndTaskDoesNotExist( - numCommits=2, userSearchFields={ - 'username': 'author-phab-username', - 'custom.abc:slack-username': '', - }) + numCommits=2, + userSearchFields={ + "username": "author-phab-username", + "custom.abc:slack-username": "", + }, + ) self.slackbot.client.users_list.return_value = test.mocks.slackbot.users_list( - total=2) + total=2 + ) - response = self.app.post('/status', headers=self.headers, json=data) + response = self.app.post("/status", headers=self.headers, json=data) self.assertEqual(response.status_code, 200) self.phab.differential.revision.edit.assert_not_called() - maniphestEditCalls = [mock.call(transactions=[{ - "type": "title", - "value": "Build build-name is broken.", - }, { - "type": "priority", - "value": "unbreak", - }, { - "type": "description", - "value": "[[ {} | build-name ]] is broken on branch 'refs/heads/master'\n\nAssociated commits:\nrABCdeadbeef00000111222333444555666777888000\nrABCdeadbeef00000111222333444555666777888001".format( - self.teamcity.build_url( - "viewLog.html", + maniphestEditCalls = [ + mock.call( + transactions=[ { - "buildTypeId": data.buildTypeId, - "buildId": DEFAULT_BUILD_ID, - } - ) - ), - }])] - self.phab.maniphest.edit.assert_has_calls( - maniphestEditCalls, any_order=False) - - self.phab.diffusion.commit.search.assert_called_with(constraints={ - "repositories": [BITCOIN_ABC_REPO], - "identifiers": ['deadbeef00000111222333444555666777888000', 'deadbeef00000111222333444555666777888001'], - }) + "type": "title", + "value": "Build build-name is broken.", + }, + { + "type": "priority", + "value": "unbreak", + }, + { + "type": "description", + "value": ( + "[[ {} | build-name ]] is broken on branch" + " 'refs/heads/master'\n\nAssociated" + " commits:\nrABCdeadbeef00000111222333444555666777888000\nrABCdeadbeef00000111222333444555666777888001" + .format( + self.teamcity.build_url( + "viewLog.html", + { + "buildTypeId": data.buildTypeId, + "buildId": DEFAULT_BUILD_ID, + }, + ) + ) + ), + }, + ] + ) + ] + self.phab.maniphest.edit.assert_has_calls(maniphestEditCalls, any_order=False) + + self.phab.diffusion.commit.search.assert_called_with( + constraints={ + "repositories": [BITCOIN_ABC_REPO], + "identifiers": [ + "deadbeef00000111222333444555666777888000", + "deadbeef00000111222333444555666777888001", + ], + } + ) self.phab.edge.search.assert_called_with( - types=['commit.revision'], sourcePHIDs=[ - 'PHID-COMMIT-8000', 'PHID-COMMIT-8001']) + types=["commit.revision"], + sourcePHIDs=["PHID-COMMIT-8000", "PHID-COMMIT-8001"], + ) self.phab.differential.revision.search.assert_called_with( - constraints={"phids": ['PHID-DREV-1000', 'PHID-DREV-1001']}) + constraints={"phids": ["PHID-DREV-1000", "PHID-DREV-1001"]} + ) self.slackbot.client.chat_postMessage.assert_called_with( - channel='#test-dev-channel', - text="Committer: author-phab-username\n" - "Build 'build-name' appears to be broken: {}\n" - "Task: https://reviews.bitcoinabc.org/T890\n" - "Diff: https://reviews.bitcoinabc.org/D{}".format( - self.teamcity.build_url( - "viewLog.html", - { - "buildId": DEFAULT_BUILD_ID, - } - ), - test.mocks.phabricator.DEFAULT_REVISION_ID, - )) + channel="#test-dev-channel", + text=( + "Committer: author-phab-username\n" + "Build 'build-name' appears to be broken: {}\n" + "Task: https://reviews.bitcoinabc.org/T890\n" + "Diff: https://reviews.bitcoinabc.org/D{}".format( + self.teamcity.build_url( + "viewLog.html", + { + "buildId": DEFAULT_BUILD_ID, + }, + ), + test.mocks.phabricator.DEFAULT_REVISION_ID, + ) + ), + ) def test_status_master_failureAndTaskDoesNotExist_firstFlakyFailure(self): self.teamcity.setMockTime(1590000000) data = statusRequestData() - data.buildResult = 'failure' + data.buildResult = "failure" self.setup_master_failureAndTaskDoesNotExist(numRecentFailedBuilds=2) - response = self.app.post('/status', headers=self.headers, json=data) + response = self.app.post("/status", headers=self.headers, json=data) self.assertEqual(response.status_code, 200) self.phab.differential.revision.edit.assert_not_called() - self.teamcity.session.send.assert_called_with(AnyWith(requests.PreparedRequest, { - 'url': self.teamcity.build_url( - "app/rest/builds", + self.teamcity.session.send.assert_called_with( + AnyWith( + requests.PreparedRequest, { - "locator": "buildType:{},sinceDate:{}".format(data.buildTypeId, - self.teamcity.formatTime(1590000000 - 60 * 60 * 24 * 5)), - "fields": "build", - } + "url": self.teamcity.build_url( + "app/rest/builds", + { + "locator": "buildType:{},sinceDate:{}".format( + data.buildTypeId, + self.teamcity.formatTime(1590000000 - 60 * 60 * 24 * 5), + ), + "fields": "build", + }, + ) + }, ) - })) + ) self.phab.maniphest.edit.assert_not_called() self.slackbot.client.chat_postMessage.assert_called_with( - channel='#test-dev-channel', + channel="#test-dev-channel", text="Build 'build-name' appears to be flaky: {}".format( self.teamcity.build_url( "viewLog.html", { "buildId": DEFAULT_BUILD_ID, - } - )) + }, + ) + ), ) - def test_status_master_failureAndTaskDoesNotExist_successiveFlakyFailures( - self): + def test_status_master_failureAndTaskDoesNotExist_successiveFlakyFailures(self): self.teamcity.setMockTime(1590000000) data = statusRequestData() - data.buildResult = 'failure' + data.buildResult = "failure" self.setup_master_failureAndTaskDoesNotExist(numRecentFailedBuilds=3) - response = self.app.post('/status', headers=self.headers, json=data) + response = self.app.post("/status", headers=self.headers, json=data) self.assertEqual(response.status_code, 200) self.phab.differential.revision.edit.assert_not_called() - self.teamcity.session.send.assert_called_with(AnyWith(requests.PreparedRequest, { - 'url': self.teamcity.build_url( - "app/rest/builds", + self.teamcity.session.send.assert_called_with( + AnyWith( + requests.PreparedRequest, { - "locator": "buildType:{},sinceDate:{}".format(data.buildTypeId, - self.teamcity.formatTime(1590000000 - 60 * 60 * 24 * 5)), - "fields": "build", - } + "url": self.teamcity.build_url( + "app/rest/builds", + { + "locator": "buildType:{},sinceDate:{}".format( + data.buildTypeId, + self.teamcity.formatTime(1590000000 - 60 * 60 * 24 * 5), + ), + "fields": "build", + }, + ) + }, ) - })) + ) self.phab.maniphest.edit.assert_not_called() self.slackbot.client.chat_postMessage.assert_not_called() @@ -730,107 +948,137 @@ def test_status_master_failureAndTaskDoesNotExist_ignoreFailure(self): testPatterns = [ # Simple match - b'err:ntdll:RtlpWaitForCriticalSection', + b"err:ntdll:RtlpWaitForCriticalSection", # Greedy match with some escaped characters - br'\d*:err:ntdll:RtlpWaitForCriticalSection section .* retrying \(60 sec\)', + rb"\d*:err:ntdll:RtlpWaitForCriticalSection section .* retrying \(60 sec\)", # Less greedy match - br'err:ntdll:RtlpWaitForCriticalSection section \w* "\?" wait timed out in thread \w*, blocked by \w*, retrying', + ( + rb'err:ntdll:RtlpWaitForCriticalSection section \w* "\?" wait timed out' + rb" in thread \w*, blocked by \w*, retrying" + ), ] for pattern in testPatterns: self.teamcity.getIgnoreList.return_value = [ - b'# Some comment', - b' # Another comment followed by an empty line', - b'', + b"# Some comment", + b" # Another comment followed by an empty line", + b"", pattern, ] data = statusRequestData() - data.buildResult = 'failure' + data.buildResult = "failure" self.setup_master_failureAndTaskDoesNotExist( - buildLogFile='testlog_ignore.zip') + buildLogFile="testlog_ignore.zip" + ) - response = self.app.post( - '/status', headers=self.headers, json=data) + response = self.app.post("/status", headers=self.headers, json=data) self.assertEqual(response.status_code, 200) self.phab.differential.revision.edit.assert_not_called() - self.teamcity.session.send.assert_called_with(AnyWith(requests.PreparedRequest, { - 'url': self.teamcity.build_url( - "downloadBuildLog.html", + self.teamcity.session.send.assert_called_with( + AnyWith( + requests.PreparedRequest, { - "buildId": DEFAULT_BUILD_ID, - "archived": "true", - "guest": 1, - } + "url": self.teamcity.build_url( + "downloadBuildLog.html", + { + "buildId": DEFAULT_BUILD_ID, + "archived": "true", + "guest": 1, + }, + ) + }, ) - })) + ) self.phab.maniphest.edit.assert_not_called() self.slackbot.client.chat_postMessage.assert_not_called() def test_status_master_failureAndTaskExists(self): data = statusRequestData() - data.buildResult = 'failure' + data.buildResult = "failure" - with open(self.data_dir / 'testlog.zip', 'rb') as f: + with open(self.data_dir / "testlog.zip", "rb") as f: buildLog = f.read() self.teamcity.session.send.side_effect = [ test.mocks.teamcity.buildInfo_automatedBuild(), - test.mocks.teamcity.Response(json.dumps({ - 'build': [{ - 'id': DEFAULT_BUILD_ID, - }], - })), + test.mocks.teamcity.Response( + json.dumps( + { + "build": [ + { + "id": DEFAULT_BUILD_ID, + } + ], + } + ) + ), test.mocks.teamcity.Response(status_code=requests.codes.not_found), test.mocks.teamcity.Response(buildLog), test.mocks.teamcity.Response(), ] - self.phab.maniphest.search.return_value = test.mocks.phabricator.Result([{ - 'id': '123', - }]) + self.phab.maniphest.search.return_value = test.mocks.phabricator.Result( + [ + { + "id": "123", + } + ] + ) - response = self.app.post('/status', headers=self.headers, json=data) + response = self.app.post("/status", headers=self.headers, json=data) self.assertEqual(response.status_code, 200) self.phab.differential.revision.edit.assert_not_called() self.phab.maniphest.edit.assert_not_called() def test_status_revision_happyPath(self): data = statusRequestData() - data.branch = 'phabricator/diff/456' + data.branch = "phabricator/diff/456" self.configure_build_info( - properties=test.mocks.teamcity.buildInfo_properties(propsList=[{ - 'name': 'env.ABC_BUILD_NAME', - 'value': 'build-diff', - }]) + properties=test.mocks.teamcity.buildInfo_properties( + propsList=[ + { + "name": "env.ABC_BUILD_NAME", + "value": "build-diff", + } + ] + ) ) self.phab.differential.revision.edit = mock.Mock() - self.phab.differential.diff.search.return_value = test.mocks.phabricator.Result([{ - 'id': '456', - 'fields': { - 'revisionPHID': '789' - }, - }]) - self.phab.differential.revision.search.return_value = test.mocks.phabricator.differential_revision_search_result() + self.phab.differential.diff.search.return_value = test.mocks.phabricator.Result( + [ + { + "id": "456", + "fields": {"revisionPHID": "789"}, + } + ] + ) + self.phab.differential.revision.search.return_value = ( + test.mocks.phabricator.differential_revision_search_result() + ) - response = self.app.post('/status', headers=self.headers, json=data) + response = self.app.post("/status", headers=self.headers, json=data) self.assertEqual(response.status_code, 200) def test_status_revision_buildFailed(self): data = statusRequestData() - data.buildResult = 'failure' - data.branch = 'phabricator/diff/456' + data.buildResult = "failure" + data.branch = "phabricator/diff/456" self.teamcity.getBuildLog = mock.Mock() self.teamcity.getBuildLog.return_value = "dummy log" self.configure_build_info( - properties=test.mocks.teamcity.buildInfo_properties(propsList=[{ - 'name': 'env.OS_NAME', - 'value': 'linux', - }]), + properties=test.mocks.teamcity.buildInfo_properties( + propsList=[ + { + "name": "env.OS_NAME", + "value": "linux", + } + ] + ), ) self.teamcity.session.send.side_effect = [ @@ -840,42 +1088,61 @@ ] self.phab.differential.revision.edit = mock.Mock() - self.phab.differential.diff.search.return_value = test.mocks.phabricator.Result([{ - 'id': '456', - 'fields': { - 'revisionPHID': '789' - }, - }]) - self.phab.differential.revision.search.return_value = test.mocks.phabricator.differential_revision_search_result() + self.phab.differential.diff.search.return_value = test.mocks.phabricator.Result( + [ + { + "id": "456", + "fields": {"revisionPHID": "789"}, + } + ] + ) + self.phab.differential.revision.search.return_value = ( + test.mocks.phabricator.differential_revision_search_result() + ) - response = self.app.post('/status', headers=self.headers, json=data) + response = self.app.post("/status", headers=self.headers, json=data) self.assertEqual(response.status_code, 200) - self.phab.differential.revision.edit.assert_called_with(transactions=[{ - "type": "comment", - "value": "(IMPORTANT) Build [[{} | build-name (linux)]] failed.\n\nTail of the build log:\n```lines=16,COUNTEREXAMPLE\ndummy log```".format( - self.teamcity.build_url( - "viewLog.html", - { - "buildTypeId": data.buildTypeId, - "buildId": DEFAULT_BUILD_ID, - } - ) - ), - }], objectIdentifier='789') + self.phab.differential.revision.edit.assert_called_with( + transactions=[ + { + "type": "comment", + "value": ( + "(IMPORTANT) Build [[{} | build-name (linux)]] failed.\n\nTail" + " of the build log:\n```lines=16,COUNTEREXAMPLE\ndummy log```" + .format( + self.teamcity.build_url( + "viewLog.html", + { + "buildTypeId": data.buildTypeId, + "buildId": DEFAULT_BUILD_ID, + }, + ) + ) + ), + } + ], + objectIdentifier="789", + ) def test_status_preview_available(self): data = statusRequestData() - data.buildResult = 'success' - data.branch = 'phabricator/diff/456' + data.buildResult = "success" + data.branch = "phabricator/diff/456" self.teamcity.getPreviewUrl = mock.Mock() - self.teamcity.getPreviewUrl.return_value = "Preview is available at http://127.0.0.1:8080 for the next 10 minutes." + self.teamcity.getPreviewUrl.return_value = ( + "Preview is available at http://127.0.0.1:8080 for the next 10 minutes." + ) self.configure_build_info( - properties=test.mocks.teamcity.buildInfo_properties(propsList=[{ - 'name': 'env.OS_NAME', - 'value': 'linux', - }]), + properties=test.mocks.teamcity.buildInfo_properties( + propsList=[ + { + "name": "env.OS_NAME", + "value": "linux", + } + ] + ), ) self.teamcity.session.send.side_effect = [ @@ -885,122 +1152,162 @@ ] self.phab.differential.revision.edit = mock.Mock() - self.phab.differential.diff.search.return_value = test.mocks.phabricator.Result([{ - 'id': '456', - 'fields': { - 'revisionPHID': '789' - }, - }]) - self.phab.differential.revision.search.return_value = test.mocks.phabricator.differential_revision_search_result() + self.phab.differential.diff.search.return_value = test.mocks.phabricator.Result( + [ + { + "id": "456", + "fields": {"revisionPHID": "789"}, + } + ] + ) + self.phab.differential.revision.search.return_value = ( + test.mocks.phabricator.differential_revision_search_result() + ) - response = self.app.post('/status', headers=self.headers, json=data) + response = self.app.post("/status", headers=self.headers, json=data) self.assertEqual(response.status_code, 200) build_url = self.teamcity.build_url( "viewLog.html", { "buildTypeId": data.buildTypeId, "buildId": DEFAULT_BUILD_ID, - } + }, + ) + self.phab.differential.revision.edit.assert_called_with( + transactions=[ + { + "type": "comment", + "value": ( + f"Build [[{build_url} | build-name (linux)]] passed.\nPreview" + " is available at http://127.0.0.1:8080 for the next 10" + " minutes.\n" + ), + } + ], + objectIdentifier="789", ) - self.phab.differential.revision.edit.assert_called_with(transactions=[{ - "type": "comment", - "value": f"Build [[{build_url} | build-name (linux)]] passed.\nPreview is available at http://127.0.0.1:8080 for the next 10 minutes.\n", - }], objectIdentifier='789') def test_status_revision_testsFailed(self): data = statusRequestData() - data.branch = 'phabricator/diff/456' - data.buildResult = 'failure' + data.branch = "phabricator/diff/456" + data.buildResult = "failure" self.phab.differential.revision.edit = mock.Mock() - self.phab.differential.diff.search.return_value = test.mocks.phabricator.Result([{ - 'id': '456', - 'fields': { - 'revisionPHID': '789' + self.phab.differential.diff.search.return_value = test.mocks.phabricator.Result( + [ + { + "id": "456", + "fields": {"revisionPHID": "789"}, + } + ] + ) + self.phab.differential.revision.search.return_value = ( + test.mocks.phabricator.differential_revision_search_result() + ) + + failures = [ + { + "id": f"id:2500,build:(id:{DEFAULT_BUILD_ID})", + "details": "stacktrace1", + "name": "test name", + }, + { + "id": f"id:2620,build:(id:{DEFAULT_BUILD_ID})", + "details": "stacktrace2", + "name": "other test name", }, - }]) - self.phab.differential.revision.search.return_value = test.mocks.phabricator.differential_revision_search_result() - - failures = [{ - 'id': f'id:2500,build:(id:{DEFAULT_BUILD_ID})', - 'details': 'stacktrace1', - 'name': 'test name', - }, { - 'id': f'id:2620,build:(id:{DEFAULT_BUILD_ID})', - 'details': 'stacktrace2', - 'name': 'other test name', - }] + ] self.configure_build_info( - properties=test.mocks.teamcity.buildInfo_properties(propsList=[{ - 'name': 'env.ABC_BUILD_NAME', - 'value': 'build-diff', - }]) + properties=test.mocks.teamcity.buildInfo_properties( + propsList=[ + { + "name": "env.ABC_BUILD_NAME", + "value": "build-diff", + } + ] + ) ) self.teamcity.session.send.side_effect = [ test.mocks.teamcity.Response(), - test.mocks.teamcity.Response(json.dumps({ - 'testOccurrence': failures, - })) + test.mocks.teamcity.Response( + json.dumps( + { + "testOccurrence": failures, + } + ) + ), ] - response = self.app.post('/status', headers=self.headers, json=data) + response = self.app.post("/status", headers=self.headers, json=data) self.assertEqual(response.status_code, 200) - self.teamcity.session.send.assert_called_with(AnyWith(requests.PreparedRequest, { - 'url': self.teamcity.build_url( - "app/rest/testOccurrences", + self.teamcity.session.send.assert_called_with( + AnyWith( + requests.PreparedRequest, { - "locator": f"build:(id:{DEFAULT_BUILD_ID}),status:FAILURE", - "fields": "testOccurrence(id,details,name)", - } + "url": self.teamcity.build_url( + "app/rest/testOccurrences", + { + "locator": f"build:(id:{DEFAULT_BUILD_ID}),status:FAILURE", + "fields": "testOccurrence(id,details,name)", + }, + ) + }, ) - })) - self.phab.differential.revision.edit.assert_called_with(transactions=[{ - "type": "comment", - "value": "(IMPORTANT) Build [[{} | build-name (build-diff)]] failed.\n\n" - "Failed tests logs:\n" - "```lines=16,COUNTEREXAMPLE" - "\n====== test name ======\n" - "stacktrace1" - "\n====== other test name ======\n" - "stacktrace2" - "```" - "\n\n" - "Each failure log is accessible here:\n" - "[[{} | test name]]\n" - "[[{} | other test name]]".format( - self.teamcity.build_url( - "viewLog.html", - { - "buildTypeId": data.buildTypeId, - "buildId": DEFAULT_BUILD_ID, - } - ), - self.teamcity.build_url( - "viewLog.html", - { - "tab": "buildLog", - "logTab": "tree", - "filter": "debug", - "expand": "all", - "buildId": DEFAULT_BUILD_ID, - "_focus": 2500, - } - ), - self.teamcity.build_url( - "viewLog.html", - { - "tab": "buildLog", - "logTab": "tree", - "filter": "debug", - "expand": "all", - "buildId": DEFAULT_BUILD_ID, - "_focus": 2620, - } - ) - ), - }], objectIdentifier='789') + ) + self.phab.differential.revision.edit.assert_called_with( + transactions=[ + { + "type": "comment", + "value": ( + "(IMPORTANT) Build [[{} | build-name (build-diff)]] failed.\n\n" + "Failed tests logs:\n" + "```lines=16,COUNTEREXAMPLE" + "\n====== test name ======\n" + "stacktrace1" + "\n====== other test name ======\n" + "stacktrace2" + "```" + "\n\n" + "Each failure log is accessible here:\n" + "[[{} | test name]]\n" + "[[{} | other test name]]".format( + self.teamcity.build_url( + "viewLog.html", + { + "buildTypeId": data.buildTypeId, + "buildId": DEFAULT_BUILD_ID, + }, + ), + self.teamcity.build_url( + "viewLog.html", + { + "tab": "buildLog", + "logTab": "tree", + "filter": "debug", + "expand": "all", + "buildId": DEFAULT_BUILD_ID, + "_focus": 2500, + }, + ), + self.teamcity.build_url( + "viewLog.html", + { + "tab": "buildLog", + "logTab": "tree", + "filter": "debug", + "expand": "all", + "buildId": DEFAULT_BUILD_ID, + "_focus": 2620, + }, + ), + ) + ), + } + ], + objectIdentifier="789", + ) def test_status_build_link_to_harbormaster(self): data = statusRequestData() @@ -1008,13 +1315,10 @@ def call_build(build_id=DEFAULT_BUILD_ID, build_name=data.buildName): self.teamcity.session.send.side_effect = [ - test.mocks.teamcity.buildInfo( - build_id=build_id, buildqueue=True), + test.mocks.teamcity.buildInfo(build_id=build_id, buildqueue=True), ] - url = 'build?buildTypeId=staging&ref=refs/tags/phabricator/diffs/{}&PHID={}&abcBuildName={}'.format( - build_id, - data.buildTargetPHID, - build_name + url = "build?buildTypeId=staging&ref=refs/tags/phabricator/diffs/{}&PHID={}&abcBuildName={}".format( + build_id, data.buildTargetPHID, build_name ) response = self.app.post(url, headers=self.headers) self.assertEqual(response.status_code, 200) @@ -1029,8 +1333,7 @@ test.mocks.teamcity.buildInfo_automatedBuild(), test.mocks.teamcity.buildInfo(build_id=build_id), ] - response = self.app.post( - '/status', headers=self.headers, json=data) + response = self.app.post("/status", headers=self.headers, json=data) self.assertEqual(response.status_code, 200) self.phab.harbormaster.artifact.search.assert_called_with( @@ -1039,8 +1342,7 @@ } ) - def check_createartifact(build_id=DEFAULT_BUILD_ID, - build_name=data.buildName): + def check_createartifact(build_id=DEFAULT_BUILD_ID, build_name=data.buildName): self.phab.harbormaster.createartifact.assert_called_with( buildTargetPHID="PHID-HMBT-01234567890123456789", artifactKey=build_name + "-PHID-HMBT-01234567890123456789", @@ -1051,11 +1353,11 @@ { "buildTypeId": data.buildTypeId, "buildId": build_id, - } + }, ), "name": build_name, "ui.external": True, - } + }, ) # On first call the missing URL artifact should be added @@ -1074,10 +1376,11 @@ "dateCreated": 0, "dateModified": 0, "policy": {}, - } + }, } - self.phab.harbormaster.artifact.search.return_value = test.mocks.phabricator.Result( - [artifact_search_return_value]) + self.phab.harbormaster.artifact.search.return_value = ( + test.mocks.phabricator.Result([artifact_search_return_value]) + ) # Reset the call counter self.phab.harbormaster.createartifact.reset_mock() @@ -1091,8 +1394,9 @@ # If the artifact key is not the expected one, the link is added artifact_search_return_value["fields"]["artifactKey"] = "unexpectedArtifactKey" - self.phab.harbormaster.artifact.search.return_value = test.mocks.phabricator.Result( - [artifact_search_return_value]) + self.phab.harbormaster.artifact.search.return_value = ( + test.mocks.phabricator.Result([artifact_search_return_value]) + ) call_status_check_artifact_search() check_createartifact() @@ -1100,7 +1404,7 @@ # Add a few more builds to the build target for i in range(1, 1 + 5): build_id = DEFAULT_BUILD_ID + i - build_name = f'build-{i}' + build_name = f"build-{i}" data.buildName = build_name data.buildId = build_id @@ -1114,17 +1418,19 @@ def test_status_landbot(self): data = statusRequestData() - data.buildTypeId = 'BitcoinAbcLandBot' + data.buildTypeId = "BitcoinAbcLandBot" # Side effects are only valid once per call, so we need to set side_effect # for every call to the endpoint. def setupTeamcity(): self.configure_build_info( properties=test.mocks.teamcity.buildInfo_properties( - propsList=[{ - 'name': 'env.ABC_REVISION', - 'value': 'D1234', - }] + propsList=[ + { + "name": "env.ABC_REVISION", + "value": "D1234", + } + ] ) ) @@ -1133,23 +1439,32 @@ ] def setupUserSearch(slackUsername): - self.phab.user.search.return_value = test.mocks.phabricator.Result([{ - 'id': '5678', - 'phid': revisionSearchResult.data[0]['fields']['authorPHID'], - 'fields': { - 'username': 'author-phab-username', - 'custom.abc:slack-username': slackUsername, - }, - }]) + self.phab.user.search.return_value = test.mocks.phabricator.Result( + [ + { + "id": "5678", + "phid": revisionSearchResult.data[0]["fields"]["authorPHID"], + "fields": { + "username": "author-phab-username", + "custom.abc:slack-username": slackUsername, + }, + } + ] + ) slackUserProfile = test.mocks.slackbot.userProfile( - {'real_name': 'author-slack-username'}) + {"real_name": "author-slack-username"} + ) slackUser = test.mocks.slackbot.user( - userId='U8765', profile=slackUserProfile) - self.slackbot.client.users_list.return_value = test.mocks.slackbot.users_list( - total=2, initialUsers=[slackUser]) + userId="U8765", profile=slackUserProfile + ) + self.slackbot.client.users_list.return_value = ( + test.mocks.slackbot.users_list(total=2, initialUsers=[slackUser]) + ) - revisionSearchResult = test.mocks.phabricator.differential_revision_search_result() + revisionSearchResult = ( + test.mocks.phabricator.differential_revision_search_result() + ) self.phab.differential.revision.search.return_value = revisionSearchResult expectedBuildUrl = self.teamcity.build_url( @@ -1157,74 +1472,94 @@ { "buildTypeId": data.buildTypeId, "buildId": DEFAULT_BUILD_ID, - } + }, ) # Test happy path setupTeamcity() - setupUserSearch(slackUsername='author-slack-username') - response = self.app.post('/status', headers=self.headers, json=data) + setupUserSearch(slackUsername="author-slack-username") + response = self.app.post("/status", headers=self.headers, json=data) self.assertEqual(response.status_code, 200) - self.phab.differential.revision.search.assert_called_with(constraints={ - 'ids': [1234]}) + self.phab.differential.revision.search.assert_called_with( + constraints={"ids": [1234]} + ) self.phab.user.search.assert_called_with( - constraints={'phids': [revisionSearchResult.data[0]['fields']['authorPHID']]}) + constraints={ + "phids": [revisionSearchResult.data[0]["fields"]["authorPHID"]] + } + ) self.slackbot.client.chat_postMessage.assert_called_with( - channel='U8765', - text="Successfully landed your change:\n" - "Revision: https://reviews.bitcoinabc.org/D1234\n" - "Build: {}".format(expectedBuildUrl), + channel="U8765", + text=( + "Successfully landed your change:\n" + "Revision: https://reviews.bitcoinabc.org/D1234\n" + "Build: {}".format(expectedBuildUrl) + ), ) # Test direct message on a landbot build failure - data.buildResult = 'failure' + data.buildResult = "failure" setupTeamcity() - setupUserSearch(slackUsername='author-slack-username') + setupUserSearch(slackUsername="author-slack-username") - response = self.app.post('/status', headers=self.headers, json=data) + response = self.app.post("/status", headers=self.headers, json=data) self.assertEqual(response.status_code, 200) - self.phab.differential.revision.search.assert_called_with(constraints={ - 'ids': [1234]}) + self.phab.differential.revision.search.assert_called_with( + constraints={"ids": [1234]} + ) self.phab.user.search.assert_called_with( - constraints={'phids': [revisionSearchResult.data[0]['fields']['authorPHID']]}) + constraints={ + "phids": [revisionSearchResult.data[0]["fields"]["authorPHID"]] + } + ) self.slackbot.client.chat_postMessage.assert_called_with( - channel='U8765', - text="Failed to land your change:\n" - "Revision: https://reviews.bitcoinabc.org/D1234\n" - "Build: {}".format(expectedBuildUrl), + channel="U8765", + text=( + "Failed to land your change:\n" + "Revision: https://reviews.bitcoinabc.org/D1234\n" + "Build: {}".format(expectedBuildUrl) + ), ) # Test message on build failure when the author's slack username is # unknown - setupUserSearch(slackUsername='') + setupUserSearch(slackUsername="") setupTeamcity() - response = self.app.post('/status', headers=self.headers, json=data) + response = self.app.post("/status", headers=self.headers, json=data) self.assertEqual(response.status_code, 200) - self.phab.differential.revision.search.assert_called_with(constraints={ - 'ids': [1234]}) + self.phab.differential.revision.search.assert_called_with( + constraints={"ids": [1234]} + ) self.phab.user.search.assert_called_with( - constraints={'phids': [revisionSearchResult.data[0]['fields']['authorPHID']]}) + constraints={ + "phids": [revisionSearchResult.data[0]["fields"]["authorPHID"]] + } + ) self.slackbot.client.chat_postMessage.assert_called_with( - channel='#test-dev-channel', - text="author-phab-username: Please set your slack username in your Phabricator profile so the " - "landbot can send you direct messages: https://reviews.bitcoinabc.org/people/editprofile/5678\n" - "Failed to land your change:\n" - "Revision: https://reviews.bitcoinabc.org/D1234\n" - "Build: {}".format(expectedBuildUrl), + channel="#test-dev-channel", + text=( + "author-phab-username: Please set your slack username in your" + " Phabricator profile so the landbot can send you direct messages:" + " https://reviews.bitcoinabc.org/people/editprofile/5678\nFailed to" + " land your change:\nRevision:" + " https://reviews.bitcoinabc.org/D1234\nBuild: {}".format( + expectedBuildUrl + ) + ), ) # Make sure no messages are sent on running status - data.buildResult = 'running' + data.buildResult = "running" setupTeamcity() self.phab.differential.revision.search = mock.Mock() self.phab.user.search = mock.Mock() self.slackbot.client.chat_postMessage = mock.Mock() - response = self.app.post('/status', headers=self.headers, json=data) + response = self.app.post("/status", headers=self.headers, json=data) self.assertEqual(response.status_code, 200) self.phab.differential.revision.search.assert_not_called() @@ -1249,9 +1584,13 @@ # Builds with ID 42 are failures self.teamcity.getLatestCompletedBuild = mock.Mock() self.teamcity.getLatestCompletedBuild.side_effect = lambda build_type_id: ( - {'id': 42} if build_type_id in failing_build_type_ids - else None if build_type_id in no_complete_build_type_ids - else {'id': DEFAULT_BUILD_ID} + {"id": 42} + if build_type_id in failing_build_type_ids + else ( + None + if build_type_id in no_complete_build_type_ids + else {"id": DEFAULT_BUILD_ID} + ) ) build_info = BuildInfo.fromSingleBuildResponse( @@ -1260,9 +1599,11 @@ def _get_build_info(build_id): status = BuildStatus.Failure if build_id == 42 else BuildStatus.Success - build_info['id'] = build_id - build_info['status'] = status.value.upper() - build_info['statusText'] = "Build success" if status == BuildStatus.Success else "Build failure" + build_info["id"] = build_id + build_info["status"] = status.value.upper() + build_info["statusText"] = ( + "Build success" if status == BuildStatus.Success else "Build failure" + ) return build_info self.teamcity.getBuildInfo.side_effect = _get_build_info @@ -1272,12 +1613,14 @@ status = BuildStatus.Success return ( - '| secp256k1 ([[https://github.com/Bitcoin-ABC/secp256k1 | Github]]) | Status |\n' - '|---|---|\n' - '| [[https://cirrus-ci.com/github/Bitcoin-ABC/secp256k1 | master]] | {{image uri="https://raster.shields.io/static/v1?label=Cirrus build&message={}&color={}&logo=cirrus-ci", alt="{}"}} |\n\n' + "| secp256k1 ([[https://github.com/Bitcoin-ABC/secp256k1 | Github]]) |" + " Status |\n|---|---|\n|" + " [[https://cirrus-ci.com/github/Bitcoin-ABC/secp256k1 | master]] |" + ' {{image uri="https://raster.shields.io/static/v1?label=Cirrus' + ' build&message={}&color={}&logo=cirrus-ci", alt="{}"}} |\n\n' ).format( status.value, - 'brightgreen' if status == BuildStatus.Success else 'red', + "brightgreen" if status == BuildStatus.Success else "red", status.value, ) @@ -1289,11 +1632,15 @@ for build_name in names_to_hide: builds[build_name] = {"hideOnStatusPanel": True} - self.phab.get_file_content_from_master.return_value = json.dumps( - config) + self.phab.get_file_content_from_master.return_value = json.dumps(config) - def associate_build(name, teamcity_build_type_id=None, teamcity_build_name=None, - teamcity_project_id=None, teamcity_project_name=None): + def associate_build( + name, + teamcity_build_type_id=None, + teamcity_build_name=None, + teamcity_project_id=None, + teamcity_project_name=None, + ): if not teamcity_build_type_id: teamcity_build_type_id = f"{name}_Type" if not teamcity_build_name: @@ -1311,33 +1658,30 @@ } self.teamcity.associate_configuration_names.return_value = associated_builds - def call_status(build_type_id, status, branch=None, - expected_status_code=None): + def call_status(build_type_id, status, branch=None, expected_status_code=None): data = statusRequestData() data.buildResult = status.value data.buildTypeId = build_type_id if branch: data.branch = branch - response = self.app.post( - '/status', headers=self.headers, json=data) + response = self.app.post("/status", headers=self.headers, json=data) self.assertEqual( response.status_code, - 200 if not expected_status_code else expected_status_code) + 200 if not expected_status_code else expected_status_code, + ) def assert_panel_content(content): - self.phab.set_text_panel_content.assert_called_with( - panel_id, - content - ) + self.phab.set_text_panel_content.assert_called_with(panel_id, content) def header(project_name): - header = f'| {project_name} | Status |\n' - header += '|---|---|\n' + header = f"| {project_name} | Status |\n" + header += "|---|---|\n" return header - def build_line(build_name, status=None, build_type_id=None, - teamcity_build_name=None): + def build_line( + build_name, status=None, build_type_id=None, teamcity_build_name=None + ): if not status: status = BuildStatus.Success if not build_type_id: @@ -1347,18 +1691,17 @@ url = self.teamcity.build_url( "viewLog.html", - { - "buildTypeId": build_type_id, - "buildId": "lastFinished" - } + {"buildTypeId": build_type_id, "buildId": "lastFinished"}, + ) + status_message = ( + "Build failure" if status == BuildStatus.Failure else status.value ) - status_message = "Build failure" if status == BuildStatus.Failure else status.value badge_url = BADGE_TC_BASE.get_badge_url( message=status_message, color=( - 'lightgrey' if status == BuildStatus.Unknown - else 'brightgreen' if status == BuildStatus.Success - else 'red' + "lightgrey" + if status == BuildStatus.Unknown + else "brightgreen" if status == BuildStatus.Success else "red" ), ) return '| [[{} | {}]] | {{image uri="{}", alt="{}"}} |\n'.format( @@ -1371,181 +1714,174 @@ # No build in config file, should bail out and not edit the panel with # teamcity content set_config_file([], []) - call_status('dont_care', BuildStatus.Success) + call_status("dont_care", BuildStatus.Success) assert_panel_content(get_cirrus_panel_content()) # If branch is not master the panel is not updated self.phab.set_text_panel_content.reset_mock() call_status( - 'dont_care', + "dont_care", BuildStatus.Success, - branch='refs/tags/phabricator/diff/42', - expected_status_code=500 + branch="refs/tags/phabricator/diff/42", + expected_status_code=500, ) self.phab.set_text_panel_content.assert_not_called() # Turn cirrus build into failure self.cirrus.get_default_branch_status.return_value = BuildStatus.Failure - call_status('dont_care', BuildStatus.Success) + call_status("dont_care", BuildStatus.Success) assert_panel_content(get_cirrus_panel_content(BuildStatus.Failure)) self.cirrus.get_default_branch_status.return_value = BuildStatus.Success # Some builds in config file but no associated teamcity build set_config_file(["show_me11"], []) - call_status('dont_care', BuildStatus.Success) + call_status("dont_care", BuildStatus.Success) assert_panel_content(get_cirrus_panel_content()) # Set one build to be shown and associate it. This is not the build that # just finished. associate_build("show_me11") - call_status('hide_me_Type', BuildStatus.Success) + call_status("hide_me_Type", BuildStatus.Success) assert_panel_content( - get_cirrus_panel_content() + - - header('Project Name') + - build_line('show_me11') + - '\n' + get_cirrus_panel_content() + + header("Project Name") + + build_line("show_me11") + + "\n" ) # Now with 3 builds from the same project + 1 not shown set_config_file(["show_me11", "show_me12", "show_me13"], ["hidden"]) associate_build("show_me12") associate_build("show_me13") - call_status('hide_me_Type', BuildStatus.Success) + call_status("hide_me_Type", BuildStatus.Success) assert_panel_content( - get_cirrus_panel_content() + - - header('Project Name') + - build_line('show_me11') + - build_line('show_me12') + - build_line('show_me13') + - '\n' + get_cirrus_panel_content() + + header("Project Name") + + build_line("show_me11") + + build_line("show_me12") + + build_line("show_me13") + + "\n" ) # Add 2 more builds from another project. # Check the result is always the same after a few calls - set_config_file(["show_me11", "show_me12", "show_me13", - "show_me21", "show_me22"], []) + set_config_file( + ["show_me11", "show_me12", "show_me13", "show_me21", "show_me22"], [] + ) associate_build( "show_me21", teamcity_project_id="ProjectId2", - teamcity_project_name="Project Name 2") + teamcity_project_name="Project Name 2", + ) associate_build( "show_me22", teamcity_project_id="ProjectId2", - teamcity_project_name="Project Name 2") + teamcity_project_name="Project Name 2", + ) for i in range(10): - call_status('hide_me_Type', BuildStatus.Success) + call_status("hide_me_Type", BuildStatus.Success) assert_panel_content( - get_cirrus_panel_content() + - - header('Project Name') + - build_line('show_me11') + - build_line('show_me12') + - build_line('show_me13') + - '\n' + - - header('Project Name 2') + - build_line('show_me21') + - build_line('show_me22') + - '\n' + get_cirrus_panel_content() + + header("Project Name") + + build_line("show_me11") + + build_line("show_me12") + + build_line("show_me13") + + "\n" + + header("Project Name 2") + + build_line("show_me21") + + build_line("show_me22") + + "\n" ) # Remove a build from teamcity, but not from the config file del associated_builds["show_me12"] - call_status('hide_me_Type', BuildStatus.Success) + call_status("hide_me_Type", BuildStatus.Success) assert_panel_content( - get_cirrus_panel_content() + - - header('Project Name') + - build_line('show_me11') + - build_line('show_me13') + - '\n' + - - header('Project Name 2') + - build_line('show_me21') + - build_line('show_me22') + - '\n' + get_cirrus_panel_content() + + header("Project Name") + + build_line("show_me11") + + build_line("show_me13") + + "\n" + + header("Project Name 2") + + build_line("show_me21") + + build_line("show_me22") + + "\n" ) # Hide a build from the config file (cannot be associated anymore) - set_config_file(["show_me11", "show_me12", - "show_me21", "show_me22"], ["show_me13"]) + set_config_file( + ["show_me11", "show_me12", "show_me21", "show_me22"], ["show_me13"] + ) del associated_builds["show_me13"] - call_status('hide_me_Type', BuildStatus.Success) + call_status("hide_me_Type", BuildStatus.Success) assert_panel_content( - get_cirrus_panel_content() + - - header('Project Name') + - build_line('show_me11') + - '\n' + - - header('Project Name 2') + - build_line('show_me21') + - build_line('show_me22') + - '\n' + get_cirrus_panel_content() + + header("Project Name") + + build_line("show_me11") + + "\n" + + header("Project Name 2") + + build_line("show_me21") + + build_line("show_me22") + + "\n" ) # Remove the last build from a project and check the project is no # longer shown del associated_builds["show_me11"] - call_status('hide_me_Type', BuildStatus.Success) + call_status("hide_me_Type", BuildStatus.Success) assert_panel_content( - get_cirrus_panel_content() + - - header('Project Name 2') + - build_line('show_me21') + - build_line('show_me22') + - '\n' + get_cirrus_panel_content() + + header("Project Name 2") + + build_line("show_me21") + + build_line("show_me22") + + "\n" ) # Check the status of the build is not checked if it didn't finish # through the endpoint - failing_build_type_ids = ['show_me21_Type'] - call_status('hide_me_Type', BuildStatus.Success) + failing_build_type_ids = ["show_me21_Type"] + call_status("hide_me_Type", BuildStatus.Success) assert_panel_content( - get_cirrus_panel_content() + - - header('Project Name 2') + - build_line('show_me21') + - build_line('show_me22') + - '\n' + get_cirrus_panel_content() + + header("Project Name 2") + + build_line("show_me21") + + build_line("show_me22") + + "\n" ) # But having the build to be updated through the endpoint causes the # status to be fetched again. Note that the result is meaningless here, # and will be fetched from Teamcity anyway. - call_status('show_me21_Type', BuildStatus.Success) + call_status("show_me21_Type", BuildStatus.Success) assert_panel_content( - get_cirrus_panel_content() + - - header('Project Name 2') + - build_line('show_me21', status=BuildStatus.Failure) + - build_line('show_me22') + - '\n' + get_cirrus_panel_content() + + header("Project Name 2") + + build_line("show_me21", status=BuildStatus.Failure) + + build_line("show_me22") + + "\n" ) # Check the unknown status of a build if it never completed associate_build( "show_me23", teamcity_project_id="ProjectId2", - teamcity_project_name="Project Name 2") - no_complete_build_type_ids = ['show_me23_Type'] - call_status('show_me21_Type', BuildStatus.Success) + teamcity_project_name="Project Name 2", + ) + no_complete_build_type_ids = ["show_me23_Type"] + call_status("show_me21_Type", BuildStatus.Success) assert_panel_content( - get_cirrus_panel_content() + - - header('Project Name 2') + - build_line('show_me21', status=BuildStatus.Failure) + - build_line('show_me22') + - build_line('show_me23', status=BuildStatus.Unknown) + - '\n' + get_cirrus_panel_content() + + header("Project Name 2") + + build_line("show_me21", status=BuildStatus.Failure) + + build_line("show_me22") + + build_line("show_me23", status=BuildStatus.Unknown) + + "\n" ) def test_update_coverage_panel(self): panel_id = 21 - buildTypeId = 'DummyBuildType' - projectName = 'Dummy Project' + buildTypeId = "DummyBuildType" + projectName = "Dummy Project" self.phab.set_text_panel_content = mock.Mock() @@ -1557,17 +1893,14 @@ data.buildTypeId = buildTypeId data.projectName = projectName - response = self.app.post( - '/status', headers=self.headers, json=data) + response = self.app.post("/status", headers=self.headers, json=data) self.assertEqual( response.status_code, - 200 if not expected_status_code else expected_status_code) + 200 if not expected_status_code else expected_status_code, + ) def assert_panel_content(content): - self.phab.set_text_panel_content.assert_called_with( - panel_id, - content - ) + self.phab.set_text_panel_content.assert_called_with(panel_id, content) def _assert_not_called_with(self, *args, **kwargs): try: @@ -1575,25 +1908,24 @@ except AssertionError: return raise AssertionError( - 'Expected {} to not have been called.'.format( - self._format_mock_call_signature( - args, kwargs))) + "Expected {} to not have been called.".format( + self._format_mock_call_signature(args, kwargs) + ) + ) + mock.Mock.assert_not_called_with = _assert_not_called_with # Build failed: ignore call_status(BuildStatus.Failure, expected_status_code=500) - self.phab.set_text_panel_content.assert_not_called_with( - panel_id=panel_id) + self.phab.set_text_panel_content.assert_not_called_with(panel_id=panel_id) # No coverage report artifact: ignore self.teamcity.get_coverage_summary.return_value = None call_status(BuildStatus.Success, expected_status_code=500) - self.phab.set_text_panel_content.assert_not_called_with( - panel_id=panel_id) + self.phab.set_text_panel_content.assert_not_called_with(panel_id=panel_id) # Generate coverage report for one project - self.teamcity.get_coverage_summary.return_value = \ - """ + self.teamcity.get_coverage_summary.return_value = """ Reading tracefile check-extended_combined.info Summary coverage rate: lines......: 82.3% (91410 of 111040 lines) @@ -1614,11 +1946,10 @@ ) # Generate coverage report for another project - buildTypeId = 'AnotherBuildType' - projectName = 'Another Project' + buildTypeId = "AnotherBuildType" + projectName = "Another Project" - self.teamcity.get_coverage_summary.return_value = \ - """ + self.teamcity.get_coverage_summary.return_value = """ Reading tracefile coverage/lcov.info Summary coverage rate: lines......: 20.0% (261 of 1305 lines) @@ -1647,11 +1978,10 @@ ) # Update one of the existing coverage reports - buildTypeId = 'DummyBuildType' - projectName = 'Renamed Dummy Project' + buildTypeId = "DummyBuildType" + projectName = "Renamed Dummy Project" - self.teamcity.get_coverage_summary.return_value = \ - """ + self.teamcity.get_coverage_summary.return_value = """ Reading tracefile check-extended_combined.info Summary coverage rate: lines......: 82.4% (91411 of 111030 lines) @@ -1680,5 +2010,5 @@ ) -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/contrib/buildbot/test/test_endpoint_triggerCI.py b/contrib/buildbot/test/test_endpoint_triggerCI.py --- a/contrib/buildbot/test/test_endpoint_triggerCI.py +++ b/contrib/buildbot/test/test_endpoint_triggerCI.py @@ -29,40 +29,48 @@ self.phab.phid = self.user_PHID # The current user is an ABC member - self.phab.project.search.return_value = test.mocks.phabricator.Result([{ - "id": 1, - "type": "PROJ", - "phid": BITCOIN_ABC_PROJECT_PHID, - "attachments": { - "members": { - "members": [ - { - "phid": self.user_PHID - }, - ] + self.phab.project.search.return_value = test.mocks.phabricator.Result( + [ + { + "id": 1, + "type": "PROJ", + "phid": BITCOIN_ABC_PROJECT_PHID, + "attachments": { + "members": { + "members": [ + {"phid": self.user_PHID}, + ] + } + }, } - } - }]) + ] + ) - self.phab.user.search.return_value = test.mocks.phabricator.Result([ - { - "id": 1, - "type": "USER", - "phid": "PHID-AUTHORIZED-USER", - "fields": { - "roles": [ - "verified", - "approved", - "activated", - ], + self.phab.user.search.return_value = test.mocks.phabricator.Result( + [ + { + "id": 1, + "type": "USER", + "phid": "PHID-AUTHORIZED-USER", + "fields": { + "roles": [ + "verified", + "approved", + "activated", + ], + }, }, - }, - ]) + ] + ) # Phabricator returns the default diff ID - self.phab.differential.diff.search.return_value = test.mocks.phabricator.Result([{ - "id": self.diff_id, - }]) + self.phab.differential.diff.search.return_value = test.mocks.phabricator.Result( + [ + { + "id": self.diff_id, + } + ] + ) config = { "builds": { @@ -82,8 +90,7 @@ }, } self.phab.get_file_content_from_master = mock.Mock() - self.phab.get_file_content_from_master.return_value = json.dumps( - config) + self.phab.get_file_content_from_master.return_value = json.dumps(config) # Transaction webhook on diff update def call_endpoint(self): @@ -92,24 +99,18 @@ "phid": self.revision_PHID, "type": "DREV", }, - "transactions": [ - { - "phid": self.transaction_PHID - } - ] + "transactions": [{"phid": self.transaction_PHID}], } response = self.post_json_with_hmac( - '/triggerCI', - self.headers, - webhook_transaction + "/triggerCI", self.headers, webhook_transaction ) self.phab.transaction.search.assert_called_with( objectIdentifier=self.revision_PHID, constraints={ "phids": [self.transaction_PHID], - } + }, ) return response @@ -127,103 +128,109 @@ "dateCreated": i, "dateModified": i, "removed": False, - "content": { - "raw": comment - } + "content": {"raw": comment}, } for i, comment in enumerate(comments) ] - self.phab.transaction.search.return_value = test.mocks.phabricator.Result([{ - "id": 42, - "phid": self.transaction_PHID, - "type": "comment", - "authorPHID": "PHID-USER-foobar", - "objectPHID": self.revision_PHID, - "dateCreated": 0, - "dateModified": 0, - "groupID": "abcdef", - "comments": comments_data, - "fields": {} - }]) + self.phab.transaction.search.return_value = test.mocks.phabricator.Result( + [ + { + "id": 42, + "phid": self.transaction_PHID, + "type": "comment", + "authorPHID": "PHID-USER-foobar", + "objectPHID": self.revision_PHID, + "dateCreated": 0, + "dateModified": 0, + "groupID": "abcdef", + "comments": comments_data, + "fields": {}, + } + ] + ) def test_triggerCI_invalid_json(self): # Not a json content response = self.post_data_with_hmac( - '/triggerCI', - self.headers, - "not: a valid json" + "/triggerCI", self.headers, "not: a valid json" ) self.assertEqual(response.status_code, 415) # Missing object response = self.post_json_with_hmac( - '/triggerCI', + "/triggerCI", self.headers, { - "transactions": [{ - "phid": self.revision_PHID, - }] - } + "transactions": [ + { + "phid": self.revision_PHID, + } + ] + }, ) self.assertEqual(response.status_code, 400) # Missing transaction response = self.post_json_with_hmac( - '/triggerCI', - self.headers, - {"object": "dummy"} + "/triggerCI", self.headers, {"object": "dummy"} ) self.assertEqual(response.status_code, 400) # Missing object type response = self.post_json_with_hmac( - '/triggerCI', + "/triggerCI", self.headers, { "object": { "phid": self.revision_PHID, }, - "transactions": [{ - "phid": self.revision_PHID, - }], - } + "transactions": [ + { + "phid": self.revision_PHID, + } + ], + }, ) self.assertEqual(response.status_code, 400) # Missing object phid response = self.post_json_with_hmac( - '/triggerCI', + "/triggerCI", self.headers, { "object": { "type": "DREV", }, - "transactions": [{ - "phid": self.revision_PHID, - }] - } + "transactions": [ + { + "phid": self.revision_PHID, + } + ], + }, ) self.assertEqual(response.status_code, 400) # Wrong object type response = self.post_json_with_hmac( - '/triggerCI', + "/triggerCI", self.headers, { "object": { "phid": "PHID-TASK-123456", "type": "TASK", }, - "transactions": [{ - "phid": self.revision_PHID, - }] - } + "transactions": [ + { + "phid": self.revision_PHID, + } + ], + }, ) self.assertEqual(response.status_code, 200) # Empty transactions response = self.post_json_with_hmac( - '/triggerCI', + "/triggerCI", self.headers, { "object": { @@ -231,7 +238,7 @@ "type": "TASK", }, "transactions": [], - } + }, ) self.assertEqual(response.status_code, 200) @@ -250,28 +257,34 @@ self.assertEqual(response.status_code, 200) # Any user, 1 comment not targeting the bot - test_no_build_user_independent([ - "This is a benign comment", - ]) + test_no_build_user_independent( + [ + "This is a benign comment", + ] + ) # Any user, 3 comments not targeting the bot - test_no_build_user_independent([ - "Useless comment 1", - "Useless @bot comment 2", - "Useless comment @bot 3", - ]) + test_no_build_user_independent( + [ + "Useless comment 1", + "Useless @bot comment 2", + "Useless comment @bot 3", + ] + ) # Any user, 1 comment targeting the bot but no build - test_no_build_user_independent([ - "@bot", - ]) + test_no_build_user_independent( + [ + "@bot", + ] + ) # Unauthorized user, 1 comment targeting the bot with 1 build self.set_transaction_return_value( [ "@bot build-1", ], - "PHID-USER-nonabc" + "PHID-USER-nonabc", ) response = self.call_endpoint() self.teamcity.session.send.assert_not_called() @@ -284,7 +297,7 @@ "@bot build-21 build-22 build-23", "@bot build-31 build-32 build-33", ], - "PHID-USER-nonabc" + "PHID-USER-nonabc", ) response = self.call_endpoint() self.teamcity.session.send.assert_not_called() @@ -298,7 +311,7 @@ "@bot build-4 build-11 build-12 build-13 build-2 build-3", "@bot build-11 build-12 build-13 build-2 build-3 build-4", ], - "PHID-AUTHORIZED-USER" + "PHID-AUTHORIZED-USER", ) response = self.call_endpoint() self.teamcity.session.send.assert_not_called() @@ -311,7 +324,7 @@ "@bot build-docker build-11 build-12 build-13 build-2 build-3", "@bot build-11 build-12 build-13 build-2 build-3 build-docker", ], - "PHID-AUTHORIZED-USER" + "PHID-AUTHORIZED-USER", ) response = self.call_endpoint() self.teamcity.session.send.assert_not_called() @@ -326,15 +339,16 @@ call( "BitcoinABC_BitcoinAbcStaging", f"refs/tags/phabricator/diff/{self.diff_id}", - properties=[{ - 'name': 'env.ABC_BUILD_NAME', - 'value': build_id, - }] + properties=[ + { + "name": "env.ABC_BUILD_NAME", + "value": build_id, + } + ], ) for build_id in queued_builds ] - self.teamcity.trigger_build.assert_has_calls( - expected_calls, any_order=True) + self.teamcity.trigger_build.assert_has_calls(expected_calls, any_order=True) self.assertEqual(response.status_code, 200) # ABC user, 1 comment targeting the bot with 1 build @@ -344,7 +358,7 @@ ], [ "build-1", - ] + ], ) # ABC user, 1 comment targeting the bot with 3 builds @@ -356,7 +370,7 @@ "build-1", "build-2", "build-3", - ] + ], ) # ABC user, 3 comments targeting the bot with 3 builds each @@ -367,10 +381,16 @@ "@bot build-31 build-32 build-33", ], [ - "build-11", "build-12", "build-13", - "build-21", "build-22", "build-23", - "build-31", "build-32", "build-33", - ] + "build-11", + "build-12", + "build-13", + "build-21", + "build-22", + "build-23", + "build-31", + "build-32", + "build-33", + ], ) # ABC user, 1 comment targeting the bot with duplicated builds @@ -382,7 +402,7 @@ "build-1", "build-2", "build-3", - ] + ], ) # ABC user, some comments targeting the bot with 3 builds involving docker @@ -391,32 +411,47 @@ "@bot build-docker build-1 build-2 build-3", ], [ - "build-docker", "build-1", "build-2", "build-3", - ] + "build-docker", + "build-1", + "build-2", + "build-3", + ], ) assert_teamcity_queued_builds( [ "@bot build-1 build-2 build-docker build-3", ], [ - "build-1", "build-2", "build-docker", "build-3", - ] + "build-1", + "build-2", + "build-docker", + "build-3", + ], ) assert_teamcity_queued_builds( [ "@bot build-1 build-2 build-3 build-docker", ], [ - "build-1", "build-2", "build-3", "build-docker", - ] + "build-1", + "build-2", + "build-3", + "build-docker", + ], ) assert_teamcity_queued_builds( [ - "@bot build-docker build-1 build-docker build-2 build-docker build-3 build-docker", + ( + "@bot build-docker build-1 build-docker build-2 build-docker" + " build-3 build-docker" + ), ], [ - "build-docker", "build-1", "build-2", "build-3", - ] + "build-docker", + "build-1", + "build-2", + "build-3", + ], ) def test_triggerCI_check_user_roles(self): @@ -438,24 +473,28 @@ self.teamcity.trigger_build.assert_called_once_with( "BitcoinABC_BitcoinAbcStaging", f"refs/tags/phabricator/diff/{self.diff_id}", - properties=[{ - 'name': 'env.ABC_BUILD_NAME', - 'value': "build-1", - }] + properties=[ + { + "name": "env.ABC_BUILD_NAME", + "value": "build-1", + } + ], ) self.assertEqual(response.status_code, 200) def set_user_roles(roles): - self.phab.user.search.return_value = test.mocks.phabricator.Result([ - { - "id": 1, - "type": "USER", - "phid": user_PHID, - "fields": { - "roles": roles, + self.phab.user.search.return_value = test.mocks.phabricator.Result( + [ + { + "id": 1, + "type": "USER", + "phid": user_PHID, + "fields": { + "roles": roles, + }, }, - }, - ]) + ] + ) roles = [ "verified", @@ -532,5 +571,5 @@ self.phab.token.give.reset_mock() -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/contrib/buildbot/test/test_persist_database.py b/contrib/buildbot/test/test_persist_database.py --- a/contrib/buildbot/test/test_persist_database.py +++ b/contrib/buildbot/test/test_persist_database.py @@ -19,15 +19,14 @@ from build import BuildStatus from teamcity_wrapper import BuildInfo -BUILD_NAME = 'build-name' -BUILD_TYPE_ID = 'build-type-id' -BUILD_TARGET_PHID = 'build-target-PHID' +BUILD_NAME = "build-name" +BUILD_TYPE_ID = "build-type-id" +BUILD_TARGET_PHID = "build-target-PHID" class PersistDataTestCase(ABCBotFixture): def setUp(self): - self.db_file_no_ext = os.path.join( - self.test_output_dir, "test_database") + self.db_file_no_ext = os.path.join(self.test_output_dir, "test_database") super().setUp() self.phab.get_file_content_from_master = mock.Mock() @@ -53,30 +52,31 @@ queryData.PHID = BUILD_TARGET_PHID triggerBuildResponse = test.mocks.teamcity.buildInfo( - test.mocks.teamcity.buildInfo_changes( - ['test-change']), buildqueue=True) + test.mocks.teamcity.buildInfo_changes(["test-change"]), buildqueue=True + ) self.teamcity.session.send.return_value = triggerBuildResponse - response = self.app.post( - f'/build{queryData}', - headers=self.headers) + response = self.app.post(f"/build{queryData}", headers=self.headers) self.assertEqual(response.status_code, 200) # Check the diff target state was persisted - with shelve.open(self.db_file_no_ext, flag='r') as db: - self.assertIn('diff_targets', db) - self.assertIn(BUILD_TARGET_PHID, db['diff_targets']) + with shelve.open(self.db_file_no_ext, flag="r") as db: + self.assertIn("diff_targets", db) + self.assertIn(BUILD_TARGET_PHID, db["diff_targets"]) self.assertIn( - DEFAULT_BUILD_ID, - db['diff_targets'][BUILD_TARGET_PHID].builds) + DEFAULT_BUILD_ID, db["diff_targets"][BUILD_TARGET_PHID].builds + ) self.assertEqual( - db['diff_targets'][BUILD_TARGET_PHID].builds[DEFAULT_BUILD_ID].build_id, - DEFAULT_BUILD_ID) + db["diff_targets"][BUILD_TARGET_PHID].builds[DEFAULT_BUILD_ID].build_id, + DEFAULT_BUILD_ID, + ) self.assertEqual( - db['diff_targets'][BUILD_TARGET_PHID].builds[DEFAULT_BUILD_ID].status, - BuildStatus.Queued) + db["diff_targets"][BUILD_TARGET_PHID].builds[DEFAULT_BUILD_ID].status, + BuildStatus.Queued, + ) self.assertEqual( - db['diff_targets'][BUILD_TARGET_PHID].builds[DEFAULT_BUILD_ID].name, - BUILD_NAME) + db["diff_targets"][BUILD_TARGET_PHID].builds[DEFAULT_BUILD_ID].name, + BUILD_NAME, + ) # Restart the server, which we expect to restore the persisted state del self.app @@ -86,15 +86,15 @@ self.slackbot, self.cirrus, db_file_no_ext=self.db_file_no_ext, - jsonProvider=test.mocks.fixture.MockJSONProvider).test_client() + jsonProvider=test.mocks.fixture.MockJSONProvider, + ).test_client() data = statusRequestData() data.buildName = BUILD_NAME data.buildId = DEFAULT_BUILD_ID data.buildTypeId = BUILD_TYPE_ID data.buildTargetPHID = BUILD_TARGET_PHID - statusResponse = self.app.post( - '/status', headers=self.headers, json=data) + statusResponse = self.app.post("/status", headers=self.headers, json=data) self.assertEqual(statusResponse.status_code, 200) self.phab.harbormaster.createartifact.assert_called_with( @@ -115,9 +115,9 @@ ) # Check the diff target was cleared from persisted state - with shelve.open(self.db_file_no_ext, flag='r') as db: - self.assertNotIn(BUILD_TARGET_PHID, db['diff_targets']) + with shelve.open(self.db_file_no_ext, flag="r") as db: + self.assertNotIn(BUILD_TARGET_PHID, db["diff_targets"]) -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/contrib/buildbot/test/test_phabricator.py b/contrib/buildbot/test/test_phabricator.py --- a/contrib/buildbot/test/test_phabricator.py +++ b/contrib/buildbot/test/test_phabricator.py @@ -22,28 +22,24 @@ pass def test_get_project_members(self): - self.phab.project.search.return_value = test.mocks.phabricator.Result([ - { - "id": 1, - "type": "PROJ", - "phid": BITCOIN_ABC_PROJECT_PHID, - "attachments": { - "members": { - "members": [ - { - "phid": "PHID-USER-usernumber1" - }, - { - "phid": "PHID-USER-usernumber2" - }, - { - "phid": "PHID-USER-usernumber3" - }, - ] - } + self.phab.project.search.return_value = test.mocks.phabricator.Result( + [ + { + "id": 1, + "type": "PROJ", + "phid": BITCOIN_ABC_PROJECT_PHID, + "attachments": { + "members": { + "members": [ + {"phid": "PHID-USER-usernumber1"}, + {"phid": "PHID-USER-usernumber2"}, + {"phid": "PHID-USER-usernumber3"}, + ] + } + }, } - } - ]) + ] + ) abc_members = self.phab.get_project_members(BITCOIN_ABC_PROJECT_PHID) self.phab.project.search.assert_called_with( @@ -60,7 +56,7 @@ "PHID-USER-usernumber1", "PHID-USER-usernumber2", "PHID-USER-usernumber3", - ] + ], ) def test_get_latest_diff_staging_ref(self): @@ -71,7 +67,7 @@ constraints={ "revisionPHIDs": [revision_PHID], }, - order="newest" + order="newest", ) # No diff associated to the revision @@ -81,18 +77,20 @@ # 2 diffs associated with the revision. Ordering is guaranteed by the # "order" request parameter. - self.phab.differential.diff.search.return_value = test.mocks.phabricator.Result([ - { - "id": 42, - "type": "DIFF", - "phid": "PHID-DIFF-123456", - }, - { - "id": 41, - "type": "DIFF", - "phid": "PHID-DIFF-abcdef", - }, - ]) + self.phab.differential.diff.search.return_value = test.mocks.phabricator.Result( + [ + { + "id": 42, + "type": "DIFF", + "phid": "PHID-DIFF-123456", + }, + { + "id": 41, + "type": "DIFF", + "phid": "PHID-DIFF-abcdef", + }, + ] + ) ref = self.phab.get_latest_diff_staging_ref(revision_PHID) assert_diff_searched_called() @@ -115,33 +113,52 @@ self.assertEqual(phid, user_PHID) def test_getRevisionAuthor(self): - self.phab.differential.revision.search.return_value = test.mocks.phabricator.Result([{ - 'fields': { - 'authorPHID': 'PHID-USER-2345', - }, - }]) + self.phab.differential.revision.search.return_value = ( + test.mocks.phabricator.Result( + [ + { + "fields": ( + { + "authorPHID": "PHID-USER-2345", + } + ), + } + ] + ) + ) expectedAuthor = { - "phid": 'PHID-USER-2345', + "phid": "PHID-USER-2345", } - self.phab.user.search.return_value = test.mocks.phabricator.Result([ - expectedAuthor]) - actualAuthor = self.phab.getRevisionAuthor('D1234') + self.phab.user.search.return_value = test.mocks.phabricator.Result( + [expectedAuthor] + ) + actualAuthor = self.phab.getRevisionAuthor("D1234") self.assertEqual(actualAuthor, expectedAuthor) def test_getAuthorSlackUsername(self): self.assertEqual("", self.phab.getAuthorSlackUsername({})) - self.assertEqual("", self.phab.getAuthorSlackUsername({'fields': {}})) - self.assertEqual("test-slack-name", self.phab.getAuthorSlackUsername({ - 'fields': { - 'custom.abc:slack-username': 'test-slack-name', - 'username': 'test-username', - }, - })) - self.assertEqual("test-username", self.phab.getAuthorSlackUsername({ - 'fields': { - 'username': 'test-username', - }, - })) + self.assertEqual("", self.phab.getAuthorSlackUsername({"fields": {}})) + self.assertEqual( + "test-slack-name", + self.phab.getAuthorSlackUsername( + { + "fields": { + "custom.abc:slack-username": "test-slack-name", + "username": "test-username", + }, + } + ), + ) + self.assertEqual( + "test-username", + self.phab.getAuthorSlackUsername( + { + "fields": { + "username": "test-username", + }, + } + ), + ) def test_user_roles(self): user_PHID = "PHID-USER-abcdef" @@ -159,26 +176,28 @@ self.assertEqual(user_roles, []) # User found - self.phab.user.search.return_value = test.mocks.phabricator.Result([ - { - "id": 1, - "type": "USER", - "phid": user_PHID, - "fields": { - "username": "foobar", - "realName": "Foo Bar", - "roles": [ - "admin", - "verified", - "approved", - "activated", - ], - "dateCreated": 0, - "dateModified": 0, - "custom.abc:slack-username": "Foobar", + self.phab.user.search.return_value = test.mocks.phabricator.Result( + [ + { + "id": 1, + "type": "USER", + "phid": user_PHID, + "fields": { + "username": "foobar", + "realName": "Foo Bar", + "roles": [ + "admin", + "verified", + "approved", + "activated", + ], + "dateCreated": 0, + "dateModified": 0, + "custom.abc:slack-username": "Foobar", + }, }, - }, - ]) + ] + ) user_roles = self.phab.get_user_roles(user_PHID) assert_user_search_called() self.assertEqual( @@ -188,33 +207,35 @@ "verified", "approved", "activated", - ] + ], ) # If more than 1 user is returned (should never occur), check no role is # returned to prevent privilege exploits. - self.phab.user.search.return_value = test.mocks.phabricator.Result([ - { - "id": 1, - "type": "USER", - "phid": user_PHID, - "fields": { - "roles": [ - "verified", - ], + self.phab.user.search.return_value = test.mocks.phabricator.Result( + [ + { + "id": 1, + "type": "USER", + "phid": user_PHID, + "fields": { + "roles": [ + "verified", + ], + }, }, - }, - { - "id": 2, - "type": "USER", - "phid": user_PHID, - "fields": { - "roles": [ - "admin", - ], + { + "id": 2, + "type": "USER", + "phid": user_PHID, + "fields": { + "roles": [ + "admin", + ], + }, }, - }, - ]) + ] + ) user_roles = self.phab.get_user_roles(user_PHID) assert_user_search_called() self.assertEqual(user_roles, []) @@ -223,17 +244,19 @@ with self.assertRaises(AssertionError): self.phab.get_latest_master_commit_hash() - self.phab.diffusion.commit.search.return_value = test.mocks.phabricator.Result([ - { - "id": 1234, - "type": "CMIT", - "phid": "PHID-CMIT-abcdef", - "fields": { - "identifier": "0000000000000000000000000000000123456789", - "repositoryPHID": "PHID-REPO-abcrepo", - }, - } - ]) + self.phab.diffusion.commit.search.return_value = test.mocks.phabricator.Result( + [ + { + "id": 1234, + "type": "CMIT", + "phid": "PHID-CMIT-abcdef", + "fields": { + "identifier": "0000000000000000000000000000000123456789", + "repositoryPHID": "PHID-REPO-abcrepo", + }, + } + ] + ) commit_hash = self.phab.get_latest_master_commit_hash() self.phab.diffusion.commit.search.assert_called_with( @@ -242,9 +265,7 @@ }, limit=1, ) - self.assertEqual( - commit_hash, - "0000000000000000000000000000000123456789") + self.assertEqual(commit_hash, "0000000000000000000000000000000123456789") def test_get_revision_changed_files(self): self.phab.differential.getcommitpaths.return_value = [ @@ -256,7 +277,8 @@ [ "file1", "dir/file2", - ]) + ], + ) def test_get_file_content_from_master(self): commit_hash = "0000000000000000000000000000000123456789" @@ -271,18 +293,9 @@ def configure_browsequery(file_path=path, file_hash="abcdef"): self.phab.diffusion.browsequery.return_value = { "paths": [ - { - "fullPath": "some/file/1", - "hash": "1234" - }, - { - "fullPath": "some/file/2", - "hash": "5678" - }, - { - "fullPath": file_path, - "hash": file_hash - }, + {"fullPath": "some/file/1", "hash": "1234"}, + {"fullPath": "some/file/2", "hash": "5678"}, + {"fullPath": file_path, "hash": file_hash}, ] } @@ -296,7 +309,8 @@ ) def configure_file_content_query( - file_phid=file_phid, too_slow=False, too_huge=False): + file_phid=file_phid, too_slow=False, too_huge=False + ): output = { "tooSlow": too_slow, "tooHuge": too_huge, @@ -324,13 +338,13 @@ assert_diffusion_browsequery_called() # Browse query returns no file - self.phab.diffusion.browsequery.return_value = {'paths': []} + self.phab.diffusion.browsequery.return_value = {"paths": []} with self.assertRaisesRegex(AssertionError, "File .+ not found in master"): self.phab.get_file_content_from_master(path) assert_diffusion_browsequery_called() # Browse query failed to find our file - configure_browsequery(file_path='something/else') + configure_browsequery(file_path="something/else") with self.assertRaisesRegex(AssertionError, "File .+ not found in master"): self.phab.get_file_content_from_master(path) assert_diffusion_browsequery_called() @@ -345,18 +359,22 @@ # Too long configure_file_content_query(too_slow=True) - with self.assertRaisesRegex(AssertionError, "is oversized or took too long to download"): + with self.assertRaisesRegex( + AssertionError, "is oversized or took too long to download" + ): self.phab.get_file_content_from_master(path) assert_file_commit_and_file_searched() # Too huge configure_file_content_query(too_huge=True) - with self.assertRaisesRegex(AssertionError, "is oversized or took too long to download"): + with self.assertRaisesRegex( + AssertionError, "is oversized or took too long to download" + ): self.phab.get_file_content_from_master(path) assert_file_commit_and_file_searched() # Check the file content can be retrieved - expected_content = b'Some nice content' + expected_content = b"Some nice content" result = test.mocks.phabricator.Result([]) result.response = b64encode(expected_content) self.phab.file.download.return_value = result @@ -413,25 +431,16 @@ "object": { "id": panel_id, "phid": "PHID-DSHP-123456789", - "transactions": [ - { - "phid": "PHID-XACT-DSHP-abcdefghi" - } - ] + "transactions": [{"phid": "PHID-XACT-DSHP-abcdefghi"}], } - } + }, } def call_set_text_panel_content(): self.phab.set_text_panel_content(panel_id, panel_content) self.phab.dashboard.panel.edit.assert_called_with( objectIdentifier=panel_id, - transactions=[ - { - "type": "text", - "value": panel_content - } - ] + transactions=[{"type": "text", "value": panel_content}], ) # Happy path @@ -448,34 +457,44 @@ # With no builds queued, default to pass self.phab.update_build_target_status(build_target) self.phab.harbormaster.sendmessage.assert_called_with( - receiver=build_target.phid, type="pass") + receiver=build_target.phid, type="pass" + ) # Queue a build build_target.queue_build("build-1", "build-name") self.phab.update_build_target_status(build_target) self.phab.harbormaster.sendmessage.assert_called_with( - receiver=build_target.phid, type="work") + receiver=build_target.phid, type="work" + ) # Test various statuses self.phab.update_build_target_status( - build_target, "build-1", BuildStatus.Queued) + build_target, "build-1", BuildStatus.Queued + ) self.phab.harbormaster.sendmessage.assert_called_with( - receiver=build_target.phid, type="work") + receiver=build_target.phid, type="work" + ) self.phab.update_build_target_status( - build_target, "build-1", BuildStatus.Running) + build_target, "build-1", BuildStatus.Running + ) self.phab.harbormaster.sendmessage.assert_called_with( - receiver=build_target.phid, type="work") + receiver=build_target.phid, type="work" + ) self.phab.update_build_target_status( - build_target, "build-1", BuildStatus.Failure) + build_target, "build-1", BuildStatus.Failure + ) self.phab.harbormaster.sendmessage.assert_called_with( - receiver=build_target.phid, type="fail") + receiver=build_target.phid, type="fail" + ) self.phab.update_build_target_status( - build_target, "build-1", BuildStatus.Success) + build_target, "build-1", BuildStatus.Success + ) self.phab.harbormaster.sendmessage.assert_called_with( - receiver=build_target.phid, type="pass") + receiver=build_target.phid, type="pass" + ) def test_get_object_token(self): user_PHID = "PHID-USER-foobarbaz" @@ -548,5 +567,5 @@ assert_token_give_called(token_PHID) -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/contrib/buildbot/test/test_slackbot.py b/contrib/buildbot/test/test_slackbot.py --- a/contrib/buildbot/test/test_slackbot.py +++ b/contrib/buildbot/test/test_slackbot.py @@ -13,9 +13,9 @@ def mockSlackBot(): channels = { - 'test': '#test-channel', + "test": "#test-channel", } - slackbot = SlackBot(mock.Mock, 'slack-token', channels) + slackbot = SlackBot(mock.Mock, "slack-token", channels) return slackbot @@ -28,57 +28,67 @@ def test_postMessage(self): message = "test message" - expectedAssertionMessage = "Invalid channel: Channel must be a user ID or configured with a channel name" + expectedAssertionMessage = ( + "Invalid channel: Channel must be a user ID or configured with a channel" + " name" + ) self.assertRaisesRegex( AssertionError, expectedAssertionMessage, self.slackbot.postMessage, None, - message) + message, + ) self.assertRaisesRegex( AssertionError, expectedAssertionMessage, self.slackbot.postMessage, - 'doesnt-exist', - message) + "doesnt-exist", + message, + ) - self.slackbot.postMessage('U1234', message) + self.slackbot.postMessage("U1234", message) self.slackbot.client.chat_postMessage.assert_called_with( - channel='U1234', text=message) + channel="U1234", text=message + ) - self.slackbot.postMessage('test', message) + self.slackbot.postMessage("test", message) self.slackbot.client.chat_postMessage.assert_called_with( - channel='#test-channel', text=message) + channel="#test-channel", text=message + ) def test_getUserByName(self): user = test.mocks.slackbot.user() self.slackbot.client.users_list.return_value = test.mocks.slackbot.users_list( - initialUsers=[user]) - self.assertIsNone(self.slackbot.getUserByName('Other Name')) - self.assertEqual(self.slackbot.getUserByName('Real Name'), user) - self.assertEqual(self.slackbot.getUserByName( - 'Real Name Normalized'), user) - self.assertEqual(self.slackbot.getUserByName('Display Name'), user) - self.assertEqual(self.slackbot.getUserByName( - 'Display Name Normalized'), user) + initialUsers=[user] + ) + self.assertIsNone(self.slackbot.getUserByName("Other Name")) + self.assertEqual(self.slackbot.getUserByName("Real Name"), user) + self.assertEqual(self.slackbot.getUserByName("Real Name Normalized"), user) + self.assertEqual(self.slackbot.getUserByName("Display Name"), user) + self.assertEqual(self.slackbot.getUserByName("Display Name Normalized"), user) def test_formatMentionByName(self): user = test.mocks.slackbot.user() expectedMention = f"<@{user['id']}>" self.slackbot.client.users_list.return_value = test.mocks.slackbot.users_list( - initialUsers=[user]) - self.assertIsNone(self.slackbot.formatMentionByName('testname')) + initialUsers=[user] + ) + self.assertIsNone(self.slackbot.formatMentionByName("testname")) self.assertEqual( - self.slackbot.formatMentionByName('Real Name'), - expectedMention) - self.assertEqual(self.slackbot.formatMentionByName( - 'Real Name Normalized'), expectedMention) + self.slackbot.formatMentionByName("Real Name"), expectedMention + ) self.assertEqual( - self.slackbot.formatMentionByName('Display Name'), - expectedMention) - self.assertEqual(self.slackbot.formatMentionByName( - 'Display Name Normalized'), expectedMention) + self.slackbot.formatMentionByName("Real Name Normalized"), expectedMention + ) + self.assertEqual( + self.slackbot.formatMentionByName("Display Name"), expectedMention + ) + self.assertEqual( + self.slackbot.formatMentionByName("Display Name Normalized"), + expectedMention, + ) -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/contrib/buildbot/test/test_teamcity.py b/contrib/buildbot/test/test_teamcity.py --- a/contrib/buildbot/test/test_teamcity.py +++ b/contrib/buildbot/test/test_teamcity.py @@ -24,7 +24,7 @@ pass def test_ignoreList(self): - expectedList = [b'test'] + expectedList = [b"test"] self.teamcity.ignoreList = expectedList self.assertListEqual(self.teamcity.getIgnoreList(), expectedList) @@ -37,90 +37,105 @@ def test_build_url(self): self.assertEqual( - self.teamcity.build_url(), - urljoin( - self.teamcity.base_url, - "?guest=1")) + self.teamcity.build_url(), urljoin(self.teamcity.base_url, "?guest=1") + ) self.assertEqual( self.teamcity.build_url("foo.html"), - urljoin( - self.teamcity.base_url, - "foo.html?guest=1")) - self.assertEqual(self.teamcity.build_url( - "foo.html", - { - "foo": "bar", - "bar": "baz", - }), - urljoin(self.teamcity.base_url, "foo.html?foo=bar&bar=baz&guest=1")) - self.assertEqual(self.teamcity.build_url( - "foo.html", - { - "foo": "bar", - "baz": 42, - }), - urljoin(self.teamcity.base_url, "foo.html?foo=bar&baz=42&guest=1")) - self.assertEqual(self.teamcity.build_url( - "foo.html", - { - "foo": "bar", - "baz": 42 - }, - "anchor"), - urljoin(self.teamcity.base_url, "foo.html?foo=bar&baz=42&guest=1#anchor")) - # No path, a fragment but no query + urljoin(self.teamcity.base_url, "foo.html?guest=1"), + ) self.assertEqual( self.teamcity.build_url( - fragment="anchor"), urljoin( - self.teamcity.base_url, "?guest=1#anchor")) - # Some path, a fragment but no query + "foo.html", + { + "foo": "bar", + "bar": "baz", + }, + ), + urljoin(self.teamcity.base_url, "foo.html?foo=bar&bar=baz&guest=1"), + ) self.assertEqual( self.teamcity.build_url( - "foo.html", fragment="anchor"), urljoin( - self.teamcity.base_url, "foo.html?guest=1#anchor")) + "foo.html", + { + "foo": "bar", + "baz": 42, + }, + ), + urljoin(self.teamcity.base_url, "foo.html?foo=bar&baz=42&guest=1"), + ) + self.assertEqual( + self.teamcity.build_url("foo.html", {"foo": "bar", "baz": 42}, "anchor"), + urljoin(self.teamcity.base_url, "foo.html?foo=bar&baz=42&guest=1#anchor"), + ) + # No path, a fragment but no query + self.assertEqual( + self.teamcity.build_url(fragment="anchor"), + urljoin(self.teamcity.base_url, "?guest=1#anchor"), + ) + # Some path, a fragment but no query + self.assertEqual( + self.teamcity.build_url("foo.html", fragment="anchor"), + urljoin(self.teamcity.base_url, "foo.html?guest=1#anchor"), + ) # Use RFC 3986 compliant chars - self.assertEqual(self.teamcity.build_url( - "foo.html", - { - "valid": "build($changes(*),properties(?),'triggered([a]:!b&c)')" - }), - urljoin(self.teamcity.base_url, "foo.html?valid=build%28%24changes%28%2A%29%2Cproperties%28%3F%29%2C%27triggered%28%5Ba%5D%3A%21b%26c%29%27%29&guest=1")) + self.assertEqual( + self.teamcity.build_url( + "foo.html", + {"valid": "build($changes(*),properties(?),'triggered([a]:!b&c)')"}, + ), + urljoin( + self.teamcity.base_url, + "foo.html?valid=build%28%24changes%28%2A%29%2Cproperties%28%3F%29%2C%27triggered%28%5Ba%5D%3A%21b%26c%29%27%29&guest=1", + ), + ) # Check other chars are also quoted/unquoted correctly - self.assertEqual(self.teamcity.build_url( - "foo.html", - { - "invalid": "space space,slash/slash,doublequote\"doublequote" - }), - urljoin(self.teamcity.base_url, "foo.html?invalid=space+space%2Cslash%2Fslash%2Cdoublequote%22doublequote&guest=1")) + self.assertEqual( + self.teamcity.build_url( + "foo.html", + {"invalid": 'space space,slash/slash,doublequote"doublequote'}, + ), + urljoin( + self.teamcity.base_url, + "foo.html?invalid=space+space%2Cslash%2Fslash%2Cdoublequote%22doublequote&guest=1", + ), + ) # The guest is already set to any value - self.assertEqual(self.teamcity.build_url( - "foo.html", - { - "foo": "bar", - "guest": 0, - }), - urljoin(self.teamcity.base_url, "foo.html?foo=bar&guest=0")) - self.assertEqual(self.teamcity.build_url( - "foo.html", - { - "foo": "bar", - "guest": 1, - }), - urljoin(self.teamcity.base_url, "foo.html?foo=bar&guest=1")) + self.assertEqual( + self.teamcity.build_url( + "foo.html", + { + "foo": "bar", + "guest": 0, + }, + ), + urljoin(self.teamcity.base_url, "foo.html?foo=bar&guest=0"), + ) + self.assertEqual( + self.teamcity.build_url( + "foo.html", + { + "foo": "bar", + "guest": 1, + }, + ), + urljoin(self.teamcity.base_url, "foo.html?foo=bar&guest=1"), + ) # No guest=1 parameter is appended when calling the rest API - self.assertEqual(self.teamcity.build_url( - "app/rest/foo", - { - "foo": "bar", - }), - urljoin(self.teamcity.base_url, "app/rest/foo?foo=bar")) + self.assertEqual( + self.teamcity.build_url( + "app/rest/foo", + { + "foo": "bar", + }, + ), + urljoin(self.teamcity.base_url, "app/rest/foo?foo=bar"), + ) def test_convert_to_guest_url(self): expect_no_update = [ # Not a valid teamcity URL "", "http://foo.bar", - # Already a guest urljoin(self.teamcity.base_url, "?guest=1"), urljoin(self.teamcity.base_url, "?foo=bar&guest=1"), @@ -128,35 +143,30 @@ ] expect_update = [ - ( - self.teamcity.base_url, - urljoin(self.teamcity.base_url, "?guest=1") - ), + (self.teamcity.base_url, urljoin(self.teamcity.base_url, "?guest=1")), ( urljoin(self.teamcity.base_url, "?"), - urljoin(self.teamcity.base_url, "?guest=1") + urljoin(self.teamcity.base_url, "?guest=1"), ), ( urljoin(self.teamcity.base_url, "?foo=bar"), - urljoin(self.teamcity.base_url, "?foo=bar&guest=1") + urljoin(self.teamcity.base_url, "?foo=bar&guest=1"), ), ( urljoin(self.teamcity.base_url, "?foo=bar&bar=baz"), - urljoin(self.teamcity.base_url, "?foo=bar&bar=baz&guest=1") + urljoin(self.teamcity.base_url, "?foo=bar&bar=baz&guest=1"), ), ( urljoin(self.teamcity.base_url, "#anchor"), - urljoin(self.teamcity.base_url, "?guest=1#anchor") + urljoin(self.teamcity.base_url, "?guest=1#anchor"), ), ( urljoin(self.teamcity.base_url, "?foo=bar#anchor"), - urljoin(self.teamcity.base_url, "?foo=bar&guest=1#anchor") + urljoin(self.teamcity.base_url, "?foo=bar&guest=1#anchor"), ), ( urljoin(self.teamcity.base_url, "?foo=bar&bar=baz#anchor"), - urljoin( - self.teamcity.base_url, - "?foo=bar&bar=baz&guest=1#anchor") + urljoin(self.teamcity.base_url, "?foo=bar&bar=baz&guest=1#anchor"), ), ] @@ -164,454 +174,594 @@ self.assertEqual(self.teamcity.convert_to_guest_url(url), url) for url_in, url_out in expect_update: - self.assertEqual( - self.teamcity.convert_to_guest_url(url_in), url_out) + self.assertEqual(self.teamcity.convert_to_guest_url(url_in), url_out) def test_requestFailure(self): self.teamcity.session.send.return_value.status_code = requests.codes.bad_request - req = self.teamcity._request('GET', 'https://endpoint') - self.assertRaises( - TeamcityRequestException, - self.teamcity.getResponse, - req) + req = self.teamcity._request("GET", "https://endpoint") + self.assertRaises(TeamcityRequestException, self.teamcity.getResponse, req) def test_getBuildProblems_noProblems(self): self.teamcity.session.send.return_value.content = json.dumps({}) - output = self.teamcity.getBuildProblems('1234') + output = self.teamcity.getBuildProblems("1234") self.assertListEqual(output, []) - self.teamcity.session.send.assert_called_with(AnyWith(requests.PreparedRequest, { - 'url': self.teamcity.build_url( - "app/rest/problemOccurrences", + self.teamcity.session.send.assert_called_with( + AnyWith( + requests.PreparedRequest, { - "locator": "build:(id:1234)", - "fields": "problemOccurrence(id,details)", - } + "url": self.teamcity.build_url( + "app/rest/problemOccurrences", + { + "locator": "build:(id:1234)", + "fields": "problemOccurrence(id,details)", + }, + ) + }, ) - })) + ) def test_getBuildProblems_hasProblems(self): - problems = [{ - 'id': 'id:2500,build:(id:12345)', - 'details': 'test-details', - }] - self.teamcity.session.send.return_value.content = json.dumps({ - 'problemOccurrence': problems, - }) - output = self.teamcity.getBuildProblems('1234') - self.assertEqual(output[0]['id'], problems[0]['id']) - self.assertEqual(output[0]['details'], problems[0]['details']) - self.assertEqual(output[0]['logUrl'], self.teamcity.build_url( - "viewLog.html", + problems = [ { - "tab": "buildLog", - "logTab": "tree", - "filter": "debug", - "expand": "all", - "buildId": 1234, - }, - "footer" - )) - self.teamcity.session.send.assert_called_with(AnyWith(requests.PreparedRequest, { - 'url': self.teamcity.build_url( - "app/rest/problemOccurrences", + "id": "id:2500,build:(id:12345)", + "details": "test-details", + } + ] + self.teamcity.session.send.return_value.content = json.dumps( + { + "problemOccurrence": problems, + } + ) + output = self.teamcity.getBuildProblems("1234") + self.assertEqual(output[0]["id"], problems[0]["id"]) + self.assertEqual(output[0]["details"], problems[0]["details"]) + self.assertEqual( + output[0]["logUrl"], + self.teamcity.build_url( + "viewLog.html", { - "locator": "build:(id:1234)", - "fields": "problemOccurrence(id,details)", - } + "tab": "buildLog", + "logTab": "tree", + "filter": "debug", + "expand": "all", + "buildId": 1234, + }, + "footer", + ), + ) + self.teamcity.session.send.assert_called_with( + AnyWith( + requests.PreparedRequest, + { + "url": self.teamcity.build_url( + "app/rest/problemOccurrences", + { + "locator": "build:(id:1234)", + "fields": "problemOccurrence(id,details)", + }, + ) + }, ) - })) + ) def test_getFailedTests_noTestFailures(self): self.teamcity.session.send.return_value.content = json.dumps({}) - output = self.teamcity.getFailedTests('1234') + output = self.teamcity.getFailedTests("1234") self.assertListEqual(output, []) - self.teamcity.session.send.assert_called_with(AnyWith(requests.PreparedRequest, { - 'url': self.teamcity.build_url( - "app/rest/testOccurrences", + self.teamcity.session.send.assert_called_with( + AnyWith( + requests.PreparedRequest, { - "locator": "build:(id:1234),status:FAILURE", - "fields": "testOccurrence(id,details,name)", - } + "url": self.teamcity.build_url( + "app/rest/testOccurrences", + { + "locator": "build:(id:1234),status:FAILURE", + "fields": "testOccurrence(id,details,name)", + }, + ) + }, ) - })) + ) def test_getFailedTests_hasTestFailures(self): - failures = [{ - 'id': 'id:2500,build:(id:12345)', - 'details': 'stacktrace', - 'name': 'test name', - }] - self.teamcity.session.send.return_value.content = json.dumps({ - 'testOccurrence': failures, - }) - output = self.teamcity.getFailedTests('1234') - self.assertEqual(output[0]['id'], failures[0]['id']) - self.assertEqual(output[0]['details'], failures[0]['details']) - self.assertEqual(output[0]['name'], failures[0]['name']) - self.assertEqual(output[0]['logUrl'], self.teamcity.build_url( - "viewLog.html", + failures = [ + { + "id": "id:2500,build:(id:12345)", + "details": "stacktrace", + "name": "test name", + } + ] + self.teamcity.session.send.return_value.content = json.dumps( { - "tab": "buildLog", - "logTab": "tree", - "filter": "debug", - "expand": "all", - "buildId": 1234, - "_focus": 2500, + "testOccurrence": failures, } - )) - self.teamcity.session.send.assert_called_with(AnyWith(requests.PreparedRequest, { - 'url': self.teamcity.build_url( - "app/rest/testOccurrences", + ) + output = self.teamcity.getFailedTests("1234") + self.assertEqual(output[0]["id"], failures[0]["id"]) + self.assertEqual(output[0]["details"], failures[0]["details"]) + self.assertEqual(output[0]["name"], failures[0]["name"]) + self.assertEqual( + output[0]["logUrl"], + self.teamcity.build_url( + "viewLog.html", { - "locator": "build:(id:1234),status:FAILURE", - "fields": "testOccurrence(id,details,name)", - } + "tab": "buildLog", + "logTab": "tree", + "filter": "debug", + "expand": "all", + "buildId": 1234, + "_focus": 2500, + }, + ), + ) + self.teamcity.session.send.assert_called_with( + AnyWith( + requests.PreparedRequest, + { + "url": self.teamcity.build_url( + "app/rest/testOccurrences", + { + "locator": "build:(id:1234),status:FAILURE", + "fields": "testOccurrence(id,details,name)", + }, + ) + }, ) - })) + ) def test_triggerBuild(self): triggerBuildResponse = test.mocks.teamcity.buildInfo( - test.mocks.teamcity.buildInfo_changes(['test-change'])) + test.mocks.teamcity.buildInfo_changes(["test-change"]) + ) self.teamcity.session.send.return_value = triggerBuildResponse - output = self.teamcity.trigger_build('1234', 'branch-name', 'test-phid', [{ - 'name': 'another-property', - 'value': 'some value', - }]) + output = self.teamcity.trigger_build( + "1234", + "branch-name", + "test-phid", + [ + { + "name": "another-property", + "value": "some value", + } + ], + ) self.assertEqual(output, json.loads(triggerBuildResponse.content)) - self.teamcity.session.send.assert_called_with(AnyWith(requests.PreparedRequest, { - 'url': self.teamcity.build_url("app/rest/buildQueue"), - 'body': json.dumps({ - 'branchName': 'branch-name', - 'buildType': { - 'id': '1234', - }, - 'properties': { - 'property': [{ - 'name': 'another-property', - 'value': 'some value', - }, { - 'name': 'env.harborMasterTargetPHID', - 'value': 'test-phid', - }], + self.teamcity.session.send.assert_called_with( + AnyWith( + requests.PreparedRequest, + { + "url": self.teamcity.build_url("app/rest/buildQueue"), + "body": json.dumps( + { + "branchName": "branch-name", + "buildType": { + "id": "1234", + }, + "properties": { + "property": [ + { + "name": "another-property", + "value": "some value", + }, + { + "name": "env.harborMasterTargetPHID", + "value": "test-phid", + }, + ], + }, + } + ), }, - }), - })) + ) + ) def test_getBuildChangeDetails(self): expectedOutput = { - 'username': 'email@bitcoinabc.org', - 'user': { - 'name': 'Author Name', + "username": "email@bitcoinabc.org", + "user": { + "name": "Author Name", }, } - self.teamcity.session.send.return_value.content = json.dumps( - expectedOutput) - output = self.teamcity.getBuildChangeDetails('1234') + self.teamcity.session.send.return_value.content = json.dumps(expectedOutput) + output = self.teamcity.getBuildChangeDetails("1234") self.assertEqual(output, expectedOutput) - self.teamcity.session.send.assert_called_with(AnyWith(requests.PreparedRequest, { - 'url': self.teamcity.build_url("app/rest/changes/1234") - })) + self.teamcity.session.send.assert_called_with( + AnyWith( + requests.PreparedRequest, + {"url": self.teamcity.build_url("app/rest/changes/1234")}, + ) + ) def test_getBuildChanges(self): self.teamcity.session.send.side_effect = [ - test.mocks.teamcity.Response(json.dumps({ - 'change': [{ - 'id': '1234', - }], - })), - test.mocks.teamcity.Response(json.dumps({ - 'username': 'email@bitcoinabc.org', - 'user': { - 'name': 'Author Name', - }, - })), + test.mocks.teamcity.Response( + json.dumps( + { + "change": [ + { + "id": "1234", + } + ], + } + ) + ), + test.mocks.teamcity.Response( + json.dumps( + { + "username": "email@bitcoinabc.org", + "user": { + "name": "Author Name", + }, + } + ) + ), + ] + output = self.teamcity.getBuildChanges("2345") + self.assertEqual(output[0]["username"], "email@bitcoinabc.org") + self.assertEqual(output[0]["user"]["name"], "Author Name") + calls = [ + mock.call( + AnyWith( + requests.PreparedRequest, + { + "url": self.teamcity.build_url( + "app/rest/changes", + { + "locator": "build:(id:2345)", + "fields": "change(id)", + }, + ) + }, + ) + ), + mock.call( + AnyWith( + requests.PreparedRequest, + {"url": self.teamcity.build_url("app/rest/changes/1234")}, + ) + ), ] - output = self.teamcity.getBuildChanges('2345') - self.assertEqual(output[0]['username'], 'email@bitcoinabc.org') - self.assertEqual(output[0]['user']['name'], 'Author Name') - calls = [mock.call(AnyWith(requests.PreparedRequest, { - 'url': self.teamcity.build_url( - "app/rest/changes", - { - "locator": "build:(id:2345)", - "fields": "change(id)", - } - ) - })), mock.call(AnyWith(requests.PreparedRequest, { - 'url': self.teamcity.build_url("app/rest/changes/1234") - }))] self.teamcity.session.send.assert_has_calls(calls, any_order=False) def test_getBuildInfo(self): self.teamcity.session.send.return_value = test.mocks.teamcity.buildInfo( - properties=test.mocks.teamcity.buildInfo_properties([{ - 'name': 'env.ABC_BUILD_NAME', - 'value': 'build-diff', - }]), + properties=test.mocks.teamcity.buildInfo_properties( + [ + { + "name": "env.ABC_BUILD_NAME", + "value": "build-diff", + } + ] + ), changes=test.mocks.teamcity.buildInfo_changes( - ['101298f9325ddbac7e5a8f405e5e2f24a64e5171']), + ["101298f9325ddbac7e5a8f405e5e2f24a64e5171"] + ), + ) + buildInfo = self.teamcity.getBuildInfo("1234") + self.assertEqual(buildInfo["triggered"]["type"], "vcs") + self.assertEqual( + buildInfo.getProperties().get("env.ABC_BUILD_NAME"), "build-diff" ) - buildInfo = self.teamcity.getBuildInfo('1234') - self.assertEqual(buildInfo['triggered']['type'], 'vcs') - self.assertEqual(buildInfo.getProperties().get( - 'env.ABC_BUILD_NAME'), 'build-diff') self.assertEqual( - buildInfo.getCommits()[0], - '101298f9325ddbac7e5a8f405e5e2f24a64e5171') - self.teamcity.session.send.assert_called_with(AnyWith(requests.PreparedRequest, { - 'url': self.teamcity.build_url( - "app/rest/builds", + buildInfo.getCommits()[0], "101298f9325ddbac7e5a8f405e5e2f24a64e5171" + ) + self.teamcity.session.send.assert_called_with( + AnyWith( + requests.PreparedRequest, { - "locator": "id:1234", - "fields": "build(*,changes(*),properties(*),triggered(*))", - } + "url": self.teamcity.build_url( + "app/rest/builds", + { + "locator": "id:1234", + "fields": "build(*,changes(*),properties(*),triggered(*))", + }, + ) + }, ) - })) + ) def test_getBuildInfo_noInfo(self): self.teamcity.session.send.return_value = test.mocks.teamcity.Response( - json.dumps({})) - buildInfo = self.teamcity.getBuildInfo('1234') - self.assertIsNone(buildInfo.get('triggered', None)) + json.dumps({}) + ) + buildInfo = self.teamcity.getBuildInfo("1234") + self.assertIsNone(buildInfo.get("triggered", None)) self.assertIsNone(buildInfo.getProperties()) - self.teamcity.session.send.assert_called_with(AnyWith(requests.PreparedRequest, { - 'url': self.teamcity.build_url( - "app/rest/builds", + self.teamcity.session.send.assert_called_with( + AnyWith( + requests.PreparedRequest, { - "locator": "id:1234", - "fields": "build(*,changes(*),properties(*),triggered(*))", - } + "url": self.teamcity.build_url( + "app/rest/builds", + { + "locator": "id:1234", + "fields": "build(*,changes(*),properties(*),triggered(*))", + }, + ) + }, ) - })) + ) def test_buildTriggeredByAutomatedUser(self): - self.teamcity.session.send.return_value = test.mocks.teamcity.buildInfo_automatedBuild() - buildInfo = self.teamcity.getBuildInfo('1234') + self.teamcity.session.send.return_value = ( + test.mocks.teamcity.buildInfo_automatedBuild() + ) + buildInfo = self.teamcity.getBuildInfo("1234") self.assertTrue(self.teamcity.checkBuildIsAutomated(buildInfo)) self.assertFalse(self.teamcity.checkBuildIsScheduled(buildInfo)) def test_buildTriggeredManually(self): - self.teamcity.session.send.return_value = test.mocks.teamcity.buildInfo_userBuild() - buildInfo = self.teamcity.getBuildInfo('1234') + self.teamcity.session.send.return_value = ( + test.mocks.teamcity.buildInfo_userBuild() + ) + buildInfo = self.teamcity.getBuildInfo("1234") self.assertFalse(self.teamcity.checkBuildIsAutomated(buildInfo)) self.assertFalse(self.teamcity.checkBuildIsScheduled(buildInfo)) def test_buildTriggeredBySchedule(self): - self.teamcity.session.send.return_value = test.mocks.teamcity.buildInfo_scheduledBuild() - buildInfo = self.teamcity.getBuildInfo('1234') + self.teamcity.session.send.return_value = ( + test.mocks.teamcity.buildInfo_scheduledBuild() + ) + buildInfo = self.teamcity.getBuildInfo("1234") self.assertTrue(self.teamcity.checkBuildIsAutomated(buildInfo)) self.assertTrue(self.teamcity.checkBuildIsScheduled(buildInfo)) def test_buildTriggeredByVcsCheckin(self): - self.teamcity.session.send.return_value = test.mocks.teamcity.buildInfo_vcsCheckinBuild() - buildInfo = self.teamcity.getBuildInfo('1234') + self.teamcity.session.send.return_value = ( + test.mocks.teamcity.buildInfo_vcsCheckinBuild() + ) + buildInfo = self.teamcity.getBuildInfo("1234") self.assertTrue(self.teamcity.checkBuildIsAutomated(buildInfo)) self.assertFalse(self.teamcity.checkBuildIsScheduled(buildInfo)) def test_getLatestBuildAndTestFailures(self): self.teamcity.session.send.side_effect = [ - test.mocks.teamcity.Response(json.dumps({ - 'problemOccurrence': [{ - 'id': 'id:2500,build:(id:1000)', - 'details': 'build-details', - 'build': { - 'buildTypeId': 'build1', - }, - }, { - 'id': 'id:2501,build:(id:1001)', - 'details': 'build-details', - 'build': { - 'buildTypeId': 'build2', - }, - }], - })), - test.mocks.teamcity.Response(json.dumps({ - 'testOccurrence': [{ - 'id': 'id:2501,build:(id:1001)', - 'details': 'test-details', - 'build': { - 'buildTypeId': 'build2', - }, - }, { - 'id': 'id:2502,build:(id:1002)', - 'details': 'test-details', - 'build': { - 'buildTypeId': 'build3', - }, - }], - })), + test.mocks.teamcity.Response( + json.dumps( + { + "problemOccurrence": [ + { + "id": "id:2500,build:(id:1000)", + "details": "build-details", + "build": { + "buildTypeId": "build1", + }, + }, + { + "id": "id:2501,build:(id:1001)", + "details": "build-details", + "build": { + "buildTypeId": "build2", + }, + }, + ], + } + ) + ), + test.mocks.teamcity.Response( + json.dumps( + { + "testOccurrence": [ + { + "id": "id:2501,build:(id:1001)", + "details": "test-details", + "build": { + "buildTypeId": "build2", + }, + }, + { + "id": "id:2502,build:(id:1002)", + "details": "test-details", + "build": { + "buildTypeId": "build3", + }, + }, + ], + } + ) + ), ] (buildFailures, testFailures) = self.teamcity.getLatestBuildAndTestFailures( - 'BitcoinABC_Master') + "BitcoinABC_Master" + ) self.assertEqual(len(buildFailures), 2) self.assertEqual(len(testFailures), 2) - teamcityCalls = [mock.call(AnyWith(requests.PreparedRequest, { - 'url': self.teamcity.build_url( - "app/rest/problemOccurrences", - { - "locator": "currentlyFailing:true,affectedProject:(id:BitcoinABC_Master)", - "fields": "problemOccurrence(*)", - } - ) - })), mock.call(AnyWith(requests.PreparedRequest, { - 'url': self.teamcity.build_url( - "app/rest/testOccurrences", - { - "locator": "currentlyFailing:true,affectedProject:(id:BitcoinABC_Master)", - "fields": "testOccurrence(*)", - } - ) - }))] - self.teamcity.session.send.assert_has_calls( - teamcityCalls, any_order=False) + teamcityCalls = [ + mock.call( + AnyWith( + requests.PreparedRequest, + { + "url": self.teamcity.build_url( + "app/rest/problemOccurrences", + { + "locator": "currentlyFailing:true,affectedProject:(id:BitcoinABC_Master)", + "fields": "problemOccurrence(*)", + }, + ) + }, + ) + ), + mock.call( + AnyWith( + requests.PreparedRequest, + { + "url": self.teamcity.build_url( + "app/rest/testOccurrences", + { + "locator": "currentlyFailing:true,affectedProject:(id:BitcoinABC_Master)", + "fields": "testOccurrence(*)", + }, + ) + }, + ) + ), + ] + self.teamcity.session.send.assert_has_calls(teamcityCalls, any_order=False) def test_getLatestCompletedBuild(self): def call_getLastCompletedBuild(): - output = self.teamcity.getLatestCompletedBuild('1234') - self.teamcity.session.send.assert_called_with(AnyWith(requests.PreparedRequest, { - 'url': self.teamcity.build_url( - "app/rest/builds", + output = self.teamcity.getLatestCompletedBuild("1234") + self.teamcity.session.send.assert_called_with( + AnyWith( + requests.PreparedRequest, { - "locator": "buildType:1234", - "fields": "build(id)", - "count": 1, - } + "url": self.teamcity.build_url( + "app/rest/builds", + { + "locator": "buildType:1234", + "fields": "build(id)", + "count": 1, + }, + ) + }, ) - })) + ) return output # No build completed yet - self.teamcity.session.send.return_value.content = json.dumps({ - 'build': [], - }) + self.teamcity.session.send.return_value.content = json.dumps( + { + "build": [], + } + ) self.assertEqual(call_getLastCompletedBuild(), None) # A build completed - self.teamcity.session.send.return_value.content = json.dumps({ - 'build': [{ - 'id': 1234, - }], - }) + self.teamcity.session.send.return_value.content = json.dumps( + { + "build": [ + { + "id": 1234, + } + ], + } + ) build = call_getLastCompletedBuild() self.assertEqual(build["id"], 1234) def test_formatTime(self): - self.assertEqual( - self.teamcity.formatTime(1590000000), - '20200520T184000+0000') + self.assertEqual(self.teamcity.formatTime(1590000000), "20200520T184000+0000") def test_getNumAggregateFailuresSince(self): self.teamcity.setMockTime(1590000000) - self.teamcity.session.send.return_value.content = json.dumps({ - 'build': [], - }) - self.assertEqual( - self.teamcity.getNumAggregateFailuresSince( - 'buildType', 0), 0) - - self.teamcity.session.send.return_value.content = json.dumps({ - 'build': [ - {'status': 'SUCCESS'}, - {'status': 'SUCCESS'}, - {'status': 'SUCCESS'}, - ], - }) - self.assertEqual( - self.teamcity.getNumAggregateFailuresSince( - 'buildType', 0), 0) + self.teamcity.session.send.return_value.content = json.dumps( + { + "build": [], + } + ) + self.assertEqual(self.teamcity.getNumAggregateFailuresSince("buildType", 0), 0) - self.teamcity.session.send.return_value.content = json.dumps({ - 'build': [{'status': 'FAILURE'}], - }) - self.assertEqual( - self.teamcity.getNumAggregateFailuresSince( - 'buildType', 0), 1) - - self.teamcity.session.send.return_value.content = json.dumps({ - 'build': [ - {'status': 'FAILURE'}, - {'status': 'FAILURE'}, - {'status': 'FAILURE'}, - ] - }) - self.assertEqual( - self.teamcity.getNumAggregateFailuresSince( - 'buildType', 0), 1) - - self.teamcity.session.send.return_value.content = json.dumps({ - 'build': [ - {'status': 'FAILURE'}, - {'status': 'FAILURE'}, - {'status': 'SUCCESS'}, - {'status': 'FAILURE'}, - ] - }) - self.assertEqual( - self.teamcity.getNumAggregateFailuresSince( - 'buildType', 0), 2) - - self.teamcity.session.send.return_value.content = json.dumps({ - 'build': [ - {'status': 'SUCCESS'}, - {'status': 'FAILURE'}, - {'status': 'FAILURE'}, - {'status': 'SUCCESS'}, - {'status': 'FAILURE'}, - {'status': 'FAILURE'}, - {'status': 'FAILURE'}, - {'status': 'SUCCESS'}, - {'status': 'FAILURE'}, - {'status': 'SUCCESS'}, - ] - }) + self.teamcity.session.send.return_value.content = json.dumps( + { + "build": [ + {"status": "SUCCESS"}, + {"status": "SUCCESS"}, + {"status": "SUCCESS"}, + ], + } + ) + self.assertEqual(self.teamcity.getNumAggregateFailuresSince("buildType", 0), 0) + + self.teamcity.session.send.return_value.content = json.dumps( + { + "build": [{"status": "FAILURE"}], + } + ) + self.assertEqual(self.teamcity.getNumAggregateFailuresSince("buildType", 0), 1) + + self.teamcity.session.send.return_value.content = json.dumps( + { + "build": [ + {"status": "FAILURE"}, + {"status": "FAILURE"}, + {"status": "FAILURE"}, + ] + } + ) + self.assertEqual(self.teamcity.getNumAggregateFailuresSince("buildType", 0), 1) + + self.teamcity.session.send.return_value.content = json.dumps( + { + "build": [ + {"status": "FAILURE"}, + {"status": "FAILURE"}, + {"status": "SUCCESS"}, + {"status": "FAILURE"}, + ] + } + ) + self.assertEqual(self.teamcity.getNumAggregateFailuresSince("buildType", 0), 2) + + self.teamcity.session.send.return_value.content = json.dumps( + { + "build": [ + {"status": "SUCCESS"}, + {"status": "FAILURE"}, + {"status": "FAILURE"}, + {"status": "SUCCESS"}, + {"status": "FAILURE"}, + {"status": "FAILURE"}, + {"status": "FAILURE"}, + {"status": "SUCCESS"}, + {"status": "FAILURE"}, + {"status": "SUCCESS"}, + ] + } + ) self.assertEqual( - self.teamcity.getNumAggregateFailuresSince( - 'buildType', 10000000), 3) + self.teamcity.getNumAggregateFailuresSince("buildType", 10000000), 3 + ) - self.teamcity.session.send.assert_called_with(AnyWith(requests.PreparedRequest, { - 'url': self.teamcity.build_url( - "app/rest/builds", + self.teamcity.session.send.assert_called_with( + AnyWith( + requests.PreparedRequest, { - "locator": "buildType:{},sinceDate:{}".format('buildType', - self.teamcity.formatTime(1580000000)), - "fields": "build", - } + "url": self.teamcity.build_url( + "app/rest/builds", + { + "locator": "buildType:{},sinceDate:{}".format( + "buildType", self.teamcity.formatTime(1580000000) + ), + "fields": "build", + }, + ) + }, ) - })) + ) def test_associate_configuration_names(self): project_id = "Project" def configure_build_types(start=0, stop=10, project=project_id): - self.teamcity.session.send.return_value.content = json.dumps({ - "buildType": [ - { - "id": f"{project}_Build{i}", - "name": f"My build {i}", - "project": { - "id": f"Root_{project}", - "name": f"My project {project}" - }, - "parameters": { - "property": [ - { - "name": "env.ABC_BUILD_NAME", - "value": f"build-{i}" - } - ] + self.teamcity.session.send.return_value.content = json.dumps( + { + "buildType": [ + { + "id": f"{project}_Build{i}", + "name": f"My build {i}", + "project": { + "id": f"Root_{project}", + "name": f"My project {project}", + }, + "parameters": { + "property": [ + { + "name": "env.ABC_BUILD_NAME", + "value": f"build-{i}", + } + ] + }, } - } - for i in range(start, stop) - ] - }) + for i in range(start, stop) + ] + } + ) - def call_associate_configuration_names( - build_names, project=project_id): - config = self.teamcity.associate_configuration_names( - project, build_names) + def call_associate_configuration_names(build_names, project=project_id): + config = self.teamcity.associate_configuration_names(project, build_names) self.teamcity.session.send.assert_called() return config @@ -630,45 +780,46 @@ # Partial match configure_build_types(2, 10) config = call_associate_configuration_names(build_names) - self.assertDictEqual(config, - { - "build-2": { - "teamcity_build_type_id": "Project_Build2", - "teamcity_build_name": "My build 2", - "teamcity_project_id": "Root_Project", - "teamcity_project_name": "My project Project", - }, - } - ) + self.assertDictEqual( + config, + { + "build-2": { + "teamcity_build_type_id": "Project_Build2", + "teamcity_build_name": "My build 2", + "teamcity_project_id": "Root_Project", + "teamcity_project_name": "My project Project", + }, + }, + ) # Full match, change project name project_id = "OtherProject" configure_build_types(0, 10, project=project_id) - config = call_associate_configuration_names( - build_names, project=project_id) - self.assertDictEqual(config, - { - "build-0": { - "teamcity_build_type_id": "OtherProject_Build0", - "teamcity_build_name": "My build 0", - "teamcity_project_id": "Root_OtherProject", - "teamcity_project_name": "My project OtherProject", - }, - "build-1": { - "teamcity_build_type_id": "OtherProject_Build1", - "teamcity_build_name": "My build 1", - "teamcity_project_id": "Root_OtherProject", - "teamcity_project_name": "My project OtherProject", - }, - "build-2": { - "teamcity_build_type_id": "OtherProject_Build2", - "teamcity_build_name": "My build 2", - "teamcity_project_id": "Root_OtherProject", - "teamcity_project_name": "My project OtherProject", - }, - } - ) - - -if __name__ == '__main__': + config = call_associate_configuration_names(build_names, project=project_id) + self.assertDictEqual( + config, + { + "build-0": { + "teamcity_build_type_id": "OtherProject_Build0", + "teamcity_build_name": "My build 0", + "teamcity_project_id": "Root_OtherProject", + "teamcity_project_name": "My project OtherProject", + }, + "build-1": { + "teamcity_build_type_id": "OtherProject_Build1", + "teamcity_build_name": "My build 1", + "teamcity_project_id": "Root_OtherProject", + "teamcity_project_name": "My project OtherProject", + }, + "build-2": { + "teamcity_build_type_id": "OtherProject_Build2", + "teamcity_build_name": "My build 2", + "teamcity_project_id": "Root_OtherProject", + "teamcity_project_name": "My project OtherProject", + }, + }, + ) + + +if __name__ == "__main__": unittest.main() diff --git a/contrib/buildbot/test/test_testutil.py b/contrib/buildbot/test/test_testutil.py --- a/contrib/buildbot/test/test_testutil.py +++ b/contrib/buildbot/test/test_testutil.py @@ -9,10 +9,10 @@ from testutil import AnyWith -class TestObject(): - mystr = 'value' +class TestObject: + mystr = "value" mydict = { - 'item': 'value', + "item": "value", } @@ -27,50 +27,82 @@ self.assertRaisesRegex( AssertionError, "Argument class type did not match", - AnyWith( - TestObject, - None).__eq__, - {}) + AnyWith(TestObject, None).__eq__, + {}, + ) def test_happyPaths(self): self.assertRaisesRegex( - AssertionError, "Argument missing expected attribute", TestAnyWith, { - 'does-not-exist': None}) + AssertionError, + "Argument missing expected attribute", + TestAnyWith, + {"does-not-exist": None}, + ) + self.assertRaisesRegex( + AssertionError, + "Argument missing expected attribute", + TestAnyWith, + {"does-not-exist": "value"}, + ) self.assertRaisesRegex( - AssertionError, "Argument missing expected attribute", TestAnyWith, { - 'does-not-exist': 'value'}) - self.assertRaisesRegex(AssertionError, - "Argument missing expected attribute", - TestAnyWith, - {'does-not-exist': {'item': 'value'}}) + AssertionError, + "Argument missing expected attribute", + TestAnyWith, + {"does-not-exist": {"item": "value"}}, + ) - TestAnyWith({'mystr': 'value'}) + TestAnyWith({"mystr": "value"}) self.assertRaisesRegex( - AssertionError, "Argument attribute type did not match", TestAnyWith, { - 'mystr': None}) + AssertionError, + "Argument attribute type did not match", + TestAnyWith, + {"mystr": None}, + ) self.assertRaisesRegex( - AssertionError, "Argument attribute type did not match", TestAnyWith, { - 'mystr': {}}) + AssertionError, + "Argument attribute type did not match", + TestAnyWith, + {"mystr": {}}, + ) self.assertRaisesRegex( - AssertionError, "Argument attribute value did not match", TestAnyWith, { - 'mystr': 'wrong value'}) + AssertionError, + "Argument attribute value did not match", + TestAnyWith, + {"mystr": "wrong value"}, + ) - TestAnyWith({'mydict': { - 'item': 'value', - }}) + TestAnyWith( + { + "mydict": { + "item": "value", + } + } + ) self.assertRaisesRegex( - AssertionError, "Argument attribute type did not match", TestAnyWith, { - 'mydict': 'value'}) - self.assertRaisesRegex(AssertionError, "Argument attribute value did not match", TestAnyWith, {'mydict': { - 'item-does-not-exist': 'value' - }}) - self.assertRaisesRegex(AssertionError, "Argument attribute value did not match", TestAnyWith, {'mydict': { - 'item': None - }}) - self.assertRaisesRegex(AssertionError, "Argument attribute value did not match", TestAnyWith, {'mydict': { - 'item': 'wrong value' - }}) + AssertionError, + "Argument attribute type did not match", + TestAnyWith, + {"mydict": "value"}, + ) + self.assertRaisesRegex( + AssertionError, + "Argument attribute value did not match", + TestAnyWith, + {"mydict": {"item-does-not-exist": "value"}}, + ) + self.assertRaisesRegex( + AssertionError, + "Argument attribute value did not match", + TestAnyWith, + {"mydict": {"item": None}}, + ) + self.assertRaisesRegex( + AssertionError, + "Argument attribute value did not match", + TestAnyWith, + {"mydict": {"item": "wrong value"}}, + ) -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/contrib/buildbot/testutil.py b/contrib/buildbot/testutil.py --- a/contrib/buildbot/testutil.py +++ b/contrib/buildbot/testutil.py @@ -11,22 +11,35 @@ class AnyWith(cls): def __eq__(self, other): if not isinstance(other, cls): - raise AssertionError("Argument class type did not match.\nExpected:\n{}\n\nActual:\n{}".format( - pformat(cls), pformat(other))) + raise AssertionError( + "Argument class type did not match.\nExpected:\n{}\n\nActual:\n{}" + .format(pformat(cls), pformat(other)) + ) if attrs is not None: for attr, expectedValue in attrs.items(): if not hasattr(other, attr): - raise AssertionError("Argument missing expected attribute:\n{}\n\nArgument has:\n{}".format( - pformat(attr), pformat(dir(other)))) + raise AssertionError( + "Argument missing expected attribute:\n{}\n\nArgument" + " has:\n{}".format(pformat(attr), pformat(dir(other))) + ) actualValue = getattr(other, attr) if not isinstance(expectedValue, type(actualValue)): raise AssertionError( - "Argument attribute type did not match.\nExpected:\n{}\n\nActual:\n{}\nFor expected value:\n{}".format( + "Argument attribute type did not" + " match.\nExpected:\n{}\n\nActual:\n{}\nFor expected" + " value:\n{}".format( type(expectedValue).__name__, type(actualValue).__name__, - pformat(expectedValue))) + pformat(expectedValue), + ) + ) if expectedValue != actualValue: - raise AssertionError("Argument attribute value did not match.\nExpected:\n{}\n\nActual:\n{}".format( - pformat(expectedValue), pformat(actualValue))) + raise AssertionError( + "Argument attribute value did not" + " match.\nExpected:\n{}\n\nActual:\n{}".format( + pformat(expectedValue), pformat(actualValue) + ) + ) return True + return AnyWith()