diff --git a/contrib/buildbot/test/test_endpoint_backportcheck.py b/contrib/buildbot/test/test_endpoint_backportcheck.py index 7ef352414..a397fa46b 100755 --- a/contrib/buildbot/test/test_endpoint_backportcheck.py +++ b/contrib/buildbot/test/test_endpoint_backportcheck.py @@ -1,126 +1,126 @@ #!/usr/bin/env python3 # # Copyright (c) 2017-2020 The Bitcoin ABC developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. import mock import unittest from test.abcbot_fixture import ABCBotFixture import test.mocks.phabricator 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' }, }]) response = self.post_json_with_hmac( '/backportCheck', self.headers, {'object': {'phid': '1234'}}) - assert response.status_code == 200 + self.assertEqual(response.status_code, 200) self.phab.differential.revision.search.assert_called_with( 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") 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" # Note that short numbered PRs are much more common when referencing non-bitcoin PRs, # so we'll ignore them for now. "Ignore short numbered PRs: PR123" # But we do support secp256k1 PRs with 2-3 digits, so make # sure they're also ignored properly "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" "Ignore long numbered PRs for secp256k1: PR1234" "Ignore short numbered PRs for secp256k1: PR1", }, }]) response = self.post_json_with_hmac( '/backportCheck', self.headers, {'object': {'phid': '1234'}}) - assert response.status_code == 200 + self.assertEqual(response.status_code, 200) self.phab.differential.revision.search.assert_called_with( 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 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", }, }]) response = self.post_json_with_hmac( '/backportCheck', self.headers, {'object': {'phid': '1234'}}) - assert response.status_code == 200 + 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 [[https://github.com/bitcoin/bitcoin/pull/2345 | PR2345]] and " "[[https://github.com/bitcoin/bitcoin/pull/34567 | PR34567]]\n" "Backports with optional separators [[https://github.com/bitcoin/bitcoin/pull/2345 | PR2345]] and " "[[https://github.com/bitcoin/bitcoin/pull/34567 | PR34567]] and " "[[https://github.com/bitcoin/bitcoin/pull/4567 | PR4567]]\n" "[[https://github.com/bitcoin/bitcoin/pull/6789 | 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 [[https://github.com/bitcoin/bitcoin/pull/567890 | PR567890]]\n" # secp256k1 links "Backport of Secp256k1 [[https://github.com/bitcoin-core/secp256k1/pull/23 | PR23]] and " "[[https://github.com/bitcoin-core/secp256k1/pull/345 | PR345]]\n" "Backport of Secp256k1 [[https://github.com/bitcoin-core/secp256k1/pull/23 | PR23]] and " "[[https://github.com/bitcoin-core/secp256k1/pull/345 | PR345]] and " "[[https://github.com/bitcoin-core/secp256k1/pull/45 | PR45]]\n" "SECP256K1 [[https://github.com/bitcoin-core/secp256k1/pull/678 | 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 [[https://github.com/bitcoin-core/secp256k1/pull/567 | PR567]]", }], 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 index fec8276ac..11895ae46 100755 --- a/contrib/buildbot/test/test_endpoint_build.py +++ b/contrib/buildbot/test/test_endpoint_build.py @@ -1,81 +1,81 @@ #!/usr/bin/env python3 # # Copyright (c) 2017-2020 The Bitcoin ABC developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. import json import requests import unittest from test.abcbot_fixture import ABCBotFixture import test.mocks.teamcity from testutil import AnyWith class buildRequestQuery(): def __init__(self): 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())) class EndpointBuildTestCase(ABCBotFixture): def test_build(self): data = buildRequestQuery() triggerBuildResponse = test.mocks.teamcity.buildInfo( test.mocks.teamcity.buildInfo_changes( ['test-change']), buildqueue=True) self.teamcity.session.send.return_value = triggerBuildResponse response = self.app.post('/build{}'.format(data), headers=self.headers) - assert response.status_code == 200 + 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.harborMasterTargetPHID', 'value': 'buildPHID', }], }, }), })) def test_build_withAbcBuildName(self): data = buildRequestQuery() data.abcBuildName = 'build-diff' triggerBuildResponse = test.mocks.teamcity.buildInfo( test.mocks.teamcity.buildInfo_changes( ['test-change']), buildqueue=True) self.teamcity.session.send.return_value = triggerBuildResponse response = self.app.post('/build{}'.format(data), headers=self.headers) - assert response.status_code == 200 + 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', }], }, }), })) if __name__ == '__main__': unittest.main() diff --git a/contrib/buildbot/test/test_endpoint_buildDiff.py b/contrib/buildbot/test/test_endpoint_buildDiff.py index 5f1772f74..4a17b68fb 100755 --- a/contrib/buildbot/test/test_endpoint_buildDiff.py +++ b/contrib/buildbot/test/test_endpoint_buildDiff.py @@ -1,106 +1,106 @@ #!/usr/bin/env python3 # # Copyright (c) 2020 The Bitcoin ABC developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. import json import mock import requests import unittest from unittest.mock import call from build import Build, BuildStatus from test.abcbot_fixture import ABCBotFixture import test.mocks.teamcity from testutil import AnyWith class buildDiffRequestQuery(): def __init__(self): self.stagingRef = "refs/tags/phabricator/diff/1234" self.targetPHID = "PHID-HMBT-123456" def __str__(self): return "?{}".format("&".join("{}={}".format(key, value) for key, value in self.__dict__.items())) class EndpointBuildDiffTestCase(ABCBotFixture): def test_buildDiff(self): data = buildDiffRequestQuery() def set_build_configuration(builds): config = { "builds": { } } for build in builds: config["builds"][build.name] = { "runOnDiff": True } self.phab.get_file_content_from_master = mock.Mock() self.phab.get_file_content_from_master.return_value = json.dumps( config) def call_buildDiff(builds): self.teamcity.session.send.side_effect = [ test.mocks.teamcity.buildInfo(build_id=build.build_id, buildqueue=True) for build in builds ] response = self.app.post( '/buildDiff{}'.format(data), headers=self.headers) - assert response.status_code == 200 + self.assertEqual(response.status_code, 200) self.phab.get_file_content_from_master.assert_called() 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, }, { 'name': 'env.harborMasterTargetPHID', 'value': data.targetPHID, }, ], }, }), })) for build in builds ] self.teamcity.session.send.assert_has_calls( expected_calls, any_order=True) self.teamcity.session.send.reset_mock() # No diff to run builds = [] set_build_configuration(builds) call_buildDiff(builds) self.teamcity.session.send.assert_not_called() # Single diff builds.append(Build(1, BuildStatus.Queued, "build-1")) set_build_configuration(builds) call_buildDiff(builds) # Lot of builds builds = [Build(i, BuildStatus.Queued, "build-{}".format(i)) for i in range(10)] set_build_configuration(builds) call_buildDiff(builds) if __name__ == '__main__': unittest.main() diff --git a/contrib/buildbot/test/test_endpoint_getCurrentUser.py b/contrib/buildbot/test/test_endpoint_getCurrentUser.py index b51c69082..d3ba3e309 100755 --- a/contrib/buildbot/test/test_endpoint_getCurrentUser.py +++ b/contrib/buildbot/test/test_endpoint_getCurrentUser.py @@ -1,18 +1,18 @@ #!/usr/bin/env python3 # # Copyright (c) 2017-2020 The Bitcoin ABC developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. from test.abcbot_fixture import ABCBotFixture, TEST_USER import unittest class EndpointGetCurrentUserTestCase(ABCBotFixture): def test_currentUser(self): rv = self.app.get('/getCurrentUser', headers=self.headers) - assert rv.data == TEST_USER.encode() + self.assertEqual(rv.data, TEST_USER.encode()) if __name__ == '__main__': unittest.main() diff --git a/contrib/buildbot/test/test_endpoint_land.py b/contrib/buildbot/test/test_endpoint_land.py index 106b6239e..b3b2e8eba 100755 --- a/contrib/buildbot/test/test_endpoint_land.py +++ b/contrib/buildbot/test/test_endpoint_land.py @@ -1,84 +1,86 @@ #!/usr/bin/env python3 # # Copyright (c) 2017-2020 The Bitcoin ABC developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. import json import requests import unittest from test.abcbot_fixture import ABCBotFixture import test.mocks.fixture import test.mocks.teamcity from testutil import AnyWith 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' class EndpointLandTestCase(ABCBotFixture): def test_land_happyPath(self): data = landRequestData() triggerBuildResponse = test.mocks.teamcity.buildInfo( 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', }, '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', }], }, }), })) - assert response.status_code == 200 - assert response.get_json() == json.loads(triggerBuildResponse.content) + self.assertEqual(response.status_code, 200) + 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) 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', ] for arg in requiredArgs: data = landRequestData() setattr(data, arg, '') response = self.app.post('/land', headers=self.headers, json=data) - assert response.status_code == 400 + self.assertEqual(response.status_code, 400) if __name__ == '__main__': unittest.main() diff --git a/contrib/buildbot/test/test_endpoint_status.py b/contrib/buildbot/test/test_endpoint_status.py index 6ab2a2afd..dbf14be3a 100755 --- a/contrib/buildbot/test/test_endpoint_status.py +++ b/contrib/buildbot/test/test_endpoint_status.py @@ -1,1543 +1,1545 @@ #!/usr/bin/env python3 # # Copyright (c) 2017-2019 The Bitcoin ABC developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. import json import mock import requests import unittest from urllib.parse import urljoin from build import BuildStatus from phabricator_wrapper import BITCOIN_ABC_REPO from server import BADGE_TC_BASE from teamcity_wrapper import BuildInfo from testutil import AnyWith from test.abcbot_fixture import ABCBotFixture import test.mocks.fixture import test.mocks.phabricator import test.mocks.teamcity from test.mocks.teamcity import DEFAULT_BUILD_ID, TEAMCITY_CI_USER class statusRequestData(test.mocks.fixture.MockData): def __init__(self): self.buildName = 'build-name' self.project = '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' def __setattr__(self, name, value): super().__setattr__(name, value) 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', ''))) class EndpointStatusTestCase(ABCBotFixture): def setUp(self): super().setUp() self.phab.get_file_content_from_master = mock.Mock() self.phab.get_file_content_from_master.return_value = json.dumps({}) self.phab.set_text_panel_content = mock.Mock() self.teamcity.getBuildInfo = mock.Mock() self.configure_build_info() self.teamcity.get_coverage_summary = mock.Mock() self.teamcity.get_coverage_summary.return_value = None self.teamcity.getIgnoreList = mock.Mock() self.teamcity.getIgnoreList.return_value = [] self.travis.get_branch_status = mock.Mock() self.travis.get_branch_status.return_value = BuildStatus.Success 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', }, } with open(self.data_dir / buildLogFile, 'rb') as f: buildLog = f.read() 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(status_code=requests.codes.not_found), test.mocks.teamcity.Response(buildLog), test.mocks.teamcity.Response(json.dumps({ 'build': recentBuilds, })), ] commits = [] for i in range(numCommits): commitId = 8000 + i commits.append({ 'phid': 'PHID-COMMIT-{}'.format(commitId), 'fields': { 'identifier': 'deadbeef0000011122233344455566677788{}'.format(commitId) }, }) self.phab.diffusion.commit.search.return_value = test.mocks.phabricator.Result( commits) revisionSearchResult = test.mocks.phabricator.differential_revision_search_result( total=numCommits) revisions = [] for i in range(numCommits): revisions.append({ 'sourcePHID': 'PHID-COMMIT-{}'.format(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, }]) def configure_build_info(self, **kwargs): self.teamcity.getBuildInfo.return_value = BuildInfo.fromSingleBuildResponse( json.loads(test.mocks.teamcity.buildInfo(**kwargs).content) ) def test_status_invalid_json(self): data = "not: a valid json" 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) - assert response.status_code == 415 + 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) - assert response.status_code == 400 + 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) - assert response.status_code == 200 + self.assertEqual(response.status_code, 200) self.phab.harbormaster.createartifact.assert_not_called() def test_status_master(self): data = statusRequestData() self.teamcity.session.send.side_effect = [ test.mocks.teamcity.buildInfo_automatedBuild(), test.mocks.teamcity.Response(json.dumps({ 'build': [{ 'id': DEFAULT_BUILD_ID, }], })), ] response = self.app.post('/status', headers=self.headers, json=data) - assert response.status_code == 200 + 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_resolveBrokenBuildTask_masterGreen(self): def setupMockResponses(data): afterLatestBuild = [ test.mocks.teamcity.Response(), test.mocks.teamcity.Response(), ] 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(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, }], })), ] + afterLatestBuild 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', }, } data = statusRequestData() data.buildResult = 'failure' setupMockResponses(data) response = self.app.post('/status', headers=self.headers, json=data) - assert response.status_code == 200 + 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) - assert response.status_code == 200 + 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.slackbot.client.chat_postMessage.assert_called_with( 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) ) # 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(), ] 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', }, } response = self.app.post('/status', headers=self.headers, json=data) - assert response.status_code == 200 + 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.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(), test.mocks.teamcity.Response(json.dumps({ 'testOccurrence': [{ 'build': { 'buildTypeId': 'build-type', }, }], })), ] response = self.app.post('/status', headers=self.headers, json=data) - assert response.status_code == 200 + 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.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, }], })), ] response = self.app.post('/status', headers=self.headers, json=data) - assert response.status_code == 200 + 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", { "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' 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) ) 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), test.mocks.teamcity.Response(buildLog), ] setupTeamcity() response = self.app.post('/status', headers=self.headers, json=data) - assert response.status_code == 200 + 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, } ) )) verifyInfraChannelMessage() # Test an infra failure on a revision data = statusRequestData() 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' }, }]) response = self.app.post('/status', headers=self.headers, json=data) - assert response.status_code == 200 + 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.maniphest.edit.assert_not_called() verifyInfraChannelMessage() def test_status_master_failureAndTaskDoesNotExist_outOfOrderBuilds(self): data = statusRequestData() 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) response = self.app.post('/status', headers=self.headers, json=data) - assert response.status_code == 200 + 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_authorDefaultName(self): data = statusRequestData() data.buildResult = 'failure' 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) response = self.app.post('/status', headers=self.headers, json=data) - assert response.status_code == 200 + 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", { "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']) self.phab.differential.revision.search.assert_called_with( 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): data = statusRequestData() 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', }) self.slackbot.client.users_list.return_value = test.mocks.slackbot.users_list( total=2, initialUsers=[slackUser]) response = self.app.post('/status', headers=self.headers, json=data) - assert response.status_code == 200 + 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", { "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']) self.phab.differential.revision.search.assert_called_with( 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, ) ) def test_status_master_failureAndTaskDoesNotExist_scheduledBuild(self): data = statusRequestData() data.buildResult = 'failure' self.configure_build_info( triggered=test.mocks.teamcity.buildInfo_triggered( triggerType='schedule') ) self.setup_master_failureAndTaskDoesNotExist() response = self.app.post('/status', headers=self.headers, json=data) - assert response.status_code == 200 + 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", { "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, } ) )) def test_status_master_failureAndTaskDoesNotExist_multipleChanges(self): data = statusRequestData() 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), ) self.setup_master_failureAndTaskDoesNotExist( 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) response = self.app.post('/status', headers=self.headers, json=data) - assert response.status_code == 200 + 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", { "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']) self.phab.differential.revision.search.assert_called_with( 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, )) def test_status_master_failureAndTaskDoesNotExist_firstFlakyFailure(self): self.teamcity.setMockTime(1590000000) data = statusRequestData() data.buildResult = 'failure' self.setup_master_failureAndTaskDoesNotExist(numRecentFailedBuilds=2) response = self.app.post('/status', headers=self.headers, json=data) - assert response.status_code == 200 + 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", { "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', 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): self.teamcity.setMockTime(1590000000) data = statusRequestData() data.buildResult = 'failure' self.setup_master_failureAndTaskDoesNotExist(numRecentFailedBuilds=3) response = self.app.post('/status', headers=self.headers, json=data) - assert response.status_code == 200 + 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", { "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() def test_status_master_failureAndTaskDoesNotExist_ignoreFailure(self): testPatterns = [ # Simple match b'err:ntdll:RtlpWaitForCriticalSection', # Greedy match with some escaped characters br'\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', ] for pattern in testPatterns: self.teamcity.getIgnoreList.return_value = [ b'# Some comment followed by an empty line', b'', pattern, ] data = statusRequestData() data.buildResult = 'failure' self.setup_master_failureAndTaskDoesNotExist( buildLogFile='testlog_ignore.zip') response = self.app.post( '/status', headers=self.headers, json=data) - assert response.status_code == 200 + 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", { "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' 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(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', }]) response = self.app.post('/status', headers=self.headers, json=data) - assert response.status_code == 200 + 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' self.configure_build_info( 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() response = self.app.post('/status', headers=self.headers, json=data) - assert response.status_code == 200 + self.assertEqual(response.status_code, 200) def test_status_revision_buildFailed(self): data = statusRequestData() 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', }]), ) self.teamcity.session.send.side_effect = [ test.mocks.teamcity.Response(), test.mocks.teamcity.Response(), test.mocks.teamcity.Response(), ] 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() response = self.app.post('/status', headers=self.headers, json=data) - assert response.status_code == 200 + 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') def test_status_revision_testsFailed(self): data = statusRequestData() 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.revision.search.return_value = test.mocks.phabricator.differential_revision_search_result() failures = [{ 'id': 'id:2500,build:(id:{})'.format(DEFAULT_BUILD_ID), 'details': 'stacktrace1', 'name': 'test name', }, { 'id': 'id:2620,build:(id:{})'.format(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', }]) ) self.teamcity.session.send.side_effect = [ test.mocks.teamcity.Response(), test.mocks.teamcity.Response(json.dumps({ 'testOccurrence': failures, })) ] response = self.app.post('/status', headers=self.headers, json=data) - assert response.status_code == 200 + self.assertEqual(response.status_code, 200) self.teamcity.session.send.assert_called_with(AnyWith(requests.PreparedRequest, { 'url': self.teamcity.build_url( "app/rest/testOccurrences", { "locator": "build:(id:{}),status:FAILURE".format(DEFAULT_BUILD_ID), "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') def test_status_build_link_to_harbormaster(self): data = statusRequestData() data.buildTargetPHID = "PHID-HMBT-01234567890123456789" 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), ] 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) - assert response.status_code == 200 + self.assertEqual(response.status_code, 200) # Set the status to 'running' to prevent target removal on completion. data.buildResult = "running" # Add some build target or there is no harbormaster build to link. call_build() def call_status_check_artifact_search(build_id=DEFAULT_BUILD_ID): self.teamcity.session.send.side_effect = [ test.mocks.teamcity.buildInfo_automatedBuild(), test.mocks.teamcity.buildInfo(build_id=build_id), ] response = self.app.post( '/status', headers=self.headers, json=data) - assert response.status_code == 200 + self.assertEqual(response.status_code, 200) self.phab.harbormaster.artifact.search.assert_called_with( constraints={ "buildTargetPHIDs": ["PHID-HMBT-01234567890123456789"], } ) 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", artifactType="uri", artifactData={ "uri": self.teamcity.build_url( "viewLog.html", { "buildTypeId": data.buildTypeId, "buildId": build_id, } ), "name": build_name, "ui.external": True, } ) # On first call the missing URL artifact should be added call_status_check_artifact_search() check_createartifact() # Furher calls to artifact.search will return our added URL artifact artifact_search_return_value = { "id": 123, "phid": "PHID-HMBA-abcdefghijklmnopqrst", "fields": { "buildTargetPHID": "PHID-HMBT-01234567890123456789", "artifactType": "uri", "artifactKey": data.buildName + "-PHID-HMBT-01234567890123456789", "isReleased": True, "dateCreated": 0, "dateModified": 0, "policy": {}, } } 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() # Call /status again a few time for i in range(10): call_status_check_artifact_search() # But since the artifact already exists it is not added again self.phab.harbormaster.createartifact.assert_not_called() # 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]) call_status_check_artifact_search() check_createartifact() # Add a few more builds to the build target for i in range(1, 1 + 5): build_id = DEFAULT_BUILD_ID + i build_name = 'build-{}'.format(i) data.buildName = build_name data.buildId = build_id data.buildTypeId = data.buildTypeId call_build(build_id, build_name) # Check the artifact is searched and add for each build call_status_check_artifact_search(build_id) check_createartifact(build_id, build_name) def test_status_landbot(self): data = statusRequestData() 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', }] ) ) self.teamcity.session.send.side_effect = [ test.mocks.teamcity.Response(), ] 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, }, }]) slackUserProfile = test.mocks.slackbot.userProfile( {'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]) revisionSearchResult = test.mocks.phabricator.differential_revision_search_result() self.phab.differential.revision.search.return_value = revisionSearchResult expectedBuildUrl = self.teamcity.build_url( "viewLog.html", { "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) - assert response.status_code == 200 + self.assertEqual(response.status_code, 200) 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']]}) 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), ) # Test direct message on a landbot build failure data.buildResult = 'failure' setupTeamcity() setupUserSearch(slackUsername='author-slack-username') response = self.app.post('/status', headers=self.headers, json=data) - assert response.status_code == 200 + self.assertEqual(response.status_code, 200) 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']]}) 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), ) # Test message on build failure when the author's slack username is # unknown setupUserSearch(slackUsername='') setupTeamcity() response = self.app.post('/status', headers=self.headers, json=data) - assert response.status_code == 200 + self.assertEqual(response.status_code, 200) 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']]}) 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), ) # Make sure no messages are sent on running status 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) - assert response.status_code == 200 + self.assertEqual(response.status_code, 200) self.phab.differential.revision.search.assert_not_called() self.phab.user.search.assert_not_called() self.slackbot.client.chat_postMessage.assert_not_called() def test_update_build_status_panel(self): panel_id = 17 self.phab.get_file_content_from_master = mock.Mock() self.phab.set_text_panel_content = mock.Mock() associated_builds = {} self.teamcity.associate_configuration_names = mock.Mock() self.teamcity.associate_configuration_names.return_value = associated_builds # List of failing build types failing_build_type_ids = [] # List of builds that did not complete yet no_complete_build_type_ids = [] # 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} ) build_info = BuildInfo.fromSingleBuildResponse( json.loads(test.mocks.teamcity.buildInfo().content) ) 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" return build_info self.teamcity.getBuildInfo.side_effect = _get_build_info def get_travis_panel_content(status=None): if not status: status = BuildStatus.Success return ( '| secp256k1 ([[https://github.com/Bitcoin-ABC/secp256k1 | Github]]) | Status |\n' '|---|---|\n' '| [[https://travis-ci.org/github/bitcoin-abc/secp256k1 | master]] | {{image uri="https://raster.shields.io/static/v1?label=Travis build&message={}&color={}&logo=travis", alt="{}"}} |\n\n' ).format( status.value, 'brightgreen' if status == BuildStatus.Success else 'red', status.value, ) def set_config_file(names_to_display, names_to_hide): config = {"builds": {}} builds = config["builds"] for build_name in names_to_display: builds[build_name] = {"hideOnStatusPanel": False} for build_name in names_to_hide: builds[build_name] = {"hideOnStatusPanel": True} 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): if not teamcity_build_type_id: teamcity_build_type_id = "{}_Type".format(name) if not teamcity_build_name: teamcity_build_name = "My Build {}".format(name) if not teamcity_project_id: teamcity_project_id = "ProjectId" if not teamcity_project_name: teamcity_project_name = "Project Name" associated_builds[name] = { "teamcity_build_type_id": teamcity_build_type_id, "teamcity_build_name": teamcity_build_name, "teamcity_project_id": teamcity_project_id, "teamcity_project_name": teamcity_project_name, } self.teamcity.associate_configuration_names.return_value = associated_builds 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) - assert response.status_code == ( + self.assertEqual( + response.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 ) def header(project_name): header = '| {} | Status |\n'.format(project_name) header += '|---|---|\n' return header 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: build_type_id = "{}_Type".format(build_name) if not teamcity_build_name: teamcity_build_name = "My Build {}".format(build_name) url = self.teamcity.build_url( "viewLog.html", { "buildTypeId": build_type_id, "buildId": "lastFinished" } ) 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' ), ) return '| [[{} | {}]] | {{image uri="{}", alt="{}"}} |\n'.format( url, teamcity_build_name, badge_url, status_message, ) # 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) assert_panel_content(get_travis_panel_content()) # If branch is not master the panel is not updated self.phab.set_text_panel_content.reset_mock() call_status( 'dont_care', BuildStatus.Success, branch='refs/tags/phabricator/diff/42', expected_status_code=500 ) self.phab.set_text_panel_content.assert_not_called() # Turn travis build into failure self.travis.get_branch_status.return_value = BuildStatus.Failure call_status('dont_care', BuildStatus.Success) assert_panel_content(get_travis_panel_content(BuildStatus.Failure)) self.travis.get_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) assert_panel_content(get_travis_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) assert_panel_content( get_travis_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) assert_panel_content( get_travis_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"], []) associate_build( "show_me21", teamcity_project_id="ProjectId2", teamcity_project_name="Project Name 2") associate_build( "show_me22", teamcity_project_id="ProjectId2", teamcity_project_name="Project Name 2") for i in range(10): call_status('hide_me_Type', BuildStatus.Success) assert_panel_content( get_travis_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) assert_panel_content( get_travis_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"]) del associated_builds["show_me13"] call_status('hide_me_Type', BuildStatus.Success) assert_panel_content( get_travis_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) assert_panel_content( get_travis_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) assert_panel_content( get_travis_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) assert_panel_content( get_travis_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) assert_panel_content( get_travis_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 self.phab.set_text_panel_content = mock.Mock() self.teamcity.get_coverage_summary.return_value = "Dummy" def call_status(status, expected_status_code=None): data = statusRequestData() data.buildResult = status.value response = self.app.post( '/status', headers=self.headers, json=data) - assert response.status_code == ( + self.assertEqual( + response.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 ) def _assert_not_called_with(self, *args, **kwargs): try: self.assert_called_with(*args, **kwargs) except AssertionError: return raise AssertionError( '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) # 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.teamcity.get_coverage_summary.return_value = \ """ Reading tracefile check-extended_combined.info Summary coverage rate: lines......: 82.3% (91410 of 111040 lines) functions..: 74.1% (6686 of 9020 functions) branches...: 45.0% (188886 of 420030 branches) """ call_status(BuildStatus.Success, expected_status_code=500) assert_panel_content( """**[[ https://build.bitcoinabc.org/viewLog.html?buildId=lastSuccessful&buildTypeId=BitcoinABC_Master_BitcoinAbcMasterCoverage&tab=report__Root_Code_Coverage&guest=1 | HTML coverage report ]]** | Granularity | % hit | # hit | # total | | ----------- | ----- | ----- | ------- | | Lines | 82.3% | 91410 | 111040 | | Functions | 74.1% | 6686 | 9020 | | Branches | 45.0% | 188886 | 420030 | """ ) if __name__ == '__main__': unittest.main() diff --git a/contrib/buildbot/test/test_slackbot.py b/contrib/buildbot/test/test_slackbot.py index 02fa590e9..0cafb3979 100755 --- a/contrib/buildbot/test/test_slackbot.py +++ b/contrib/buildbot/test/test_slackbot.py @@ -1,81 +1,85 @@ #!/usr/bin/env python3 # # Copyright (c) 2019 The Bitcoin ABC developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. import mock import unittest from slackbot import SlackBot import test.mocks.slackbot def mockSlackBot(): channels = { 'test': '#test-channel', } slackbot = SlackBot(mock.Mock, 'slack-token', channels) return slackbot class SlackbotTestCase(unittest.TestCase): def setUp(self): self.slackbot = mockSlackBot() def tearDown(self): pass def test_postMessage(self): message = "test message" expectedAssertionMessage = "Invalid channel: Channel must be a user ID or configured with a channel name" self.assertRaisesRegex( AssertionError, expectedAssertionMessage, self.slackbot.postMessage, None, message) self.assertRaisesRegex( AssertionError, expectedAssertionMessage, self.slackbot.postMessage, 'doesnt-exist', message) self.slackbot.postMessage('U1234', message) self.slackbot.client.chat_postMessage.assert_called_with( channel='U1234', text=message) self.slackbot.postMessage('test', message) self.slackbot.client.chat_postMessage.assert_called_with( 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]) - assert self.slackbot.getUserByName('Other Name') is None - assert self.slackbot.getUserByName('Real Name') == user - assert self.slackbot.getUserByName('Real Name Normalized') == user - assert self.slackbot.getUserByName('Display Name') == user - assert self.slackbot.getUserByName('Display Name Normalized') == 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 = '<@{}>'.format(user['id']) self.slackbot.client.users_list.return_value = test.mocks.slackbot.users_list( initialUsers=[user]) - assert self.slackbot.formatMentionByName('testname') is None - assert self.slackbot.formatMentionByName( - 'Real Name') == expectedMention - assert self.slackbot.formatMentionByName( - 'Real Name Normalized') == expectedMention - assert self.slackbot.formatMentionByName( - 'Display Name') == expectedMention - assert self.slackbot.formatMentionByName( - 'Display Name Normalized') == expectedMention + self.assertIsNone(self.slackbot.formatMentionByName('testname')) + self.assertEqual( + self.slackbot.formatMentionByName('Real Name'), + expectedMention) + self.assertEqual(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__': unittest.main() diff --git a/contrib/buildbot/test/test_teamcity.py b/contrib/buildbot/test/test_teamcity.py index 0bb7e2a81..091fa9b6c 100755 --- a/contrib/buildbot/test/test_teamcity.py +++ b/contrib/buildbot/test/test_teamcity.py @@ -1,641 +1,675 @@ #!/usr/bin/env python3 # # Copyright (c) 2019 The Bitcoin ABC developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. import json import mock import requests import time import unittest from urllib.parse import urljoin from teamcity_wrapper import TeamcityRequestException from testutil import AnyWith import test.mocks.teamcity class TeamcityTests(unittest.TestCase): def setUp(self): self.teamcity = test.mocks.teamcity.instance() def tearDown(self): pass def test_ignoreList(self): expectedList = [b'test'] self.teamcity.ignoreList = expectedList - assert self.teamcity.getIgnoreList() == expectedList + self.assertListEqual(self.teamcity.getIgnoreList(), expectedList) def test_mockTime(self): currentTime = int(time.time()) - 1 - assert self.teamcity.getTime() >= currentTime + self.assertGreaterEqual(self.teamcity.getTime(), currentTime) self.teamcity.setMockTime(1593635000) - assert self.teamcity.getTime() == 1593635000 + self.assertEqual(self.teamcity.getTime(), 1593635000) def test_build_url(self): - assert self.teamcity.build_url() == urljoin(self.teamcity.base_url, "?guest=1") - assert self.teamcity.build_url("foo.html") == urljoin( - self.teamcity.base_url, "foo.html?guest=1") - assert self.teamcity.build_url( + self.assertEqual( + 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") - assert self.teamcity.build_url( + }), + 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") - assert self.teamcity.build_url( + }), + 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") + "anchor"), + urljoin(self.teamcity.base_url, "foo.html?foo=bar&baz=42&guest=1#anchor")) # No path, a fragment but no query - assert self.teamcity.build_url( - fragment="anchor") == urljoin(self.teamcity.base_url, "?guest=1#anchor") + self.assertEqual( + self.teamcity.build_url( + fragment="anchor"), urljoin( + self.teamcity.base_url, "?guest=1#anchor")) # Some path, a fragment but no query - assert self.teamcity.build_url( - "foo.html", - fragment="anchor") == urljoin(self.teamcity.base_url, "foo.html?guest=1#anchor") + 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 - assert self.teamcity.build_url( + 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") + }), + 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 - assert self.teamcity.build_url( + 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") + }), + urljoin(self.teamcity.base_url, "foo.html?invalid=space+space%2Cslash%2Fslash%2Cdoublequote%22doublequote&guest=1")) # The guest is already set to any value - assert self.teamcity.build_url( + self.assertEqual(self.teamcity.build_url( "foo.html", { "foo": "bar", "guest": 0, - }) == urljoin(self.teamcity.base_url, "foo.html?foo=bar&guest=0") - assert self.teamcity.build_url( + }), + 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") + }), + urljoin(self.teamcity.base_url, "foo.html?foo=bar&guest=1")) # No guest=1 parameter is appended when calling the rest API - assert self.teamcity.build_url( + self.assertEqual(self.teamcity.build_url( "app/rest/foo", { "foo": "bar", - }) == urljoin(self.teamcity.base_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"), urljoin(self.teamcity.base_url, "?foo=bar&guest=1#anchor"), ] expect_update = [ ( 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, "?foo=bar"), 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, "#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&bar=baz#anchor"), urljoin( self.teamcity.base_url, "?foo=bar&bar=baz&guest=1#anchor") ), ] for url in expect_no_update: - assert self.teamcity.convert_to_guest_url(url) == url + self.assertEqual(self.teamcity.convert_to_guest_url(url), url) for url_in, url_out in expect_update: - assert 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) def test_getBuildProblems_noProblems(self): self.teamcity.session.send.return_value.content = json.dumps({}) output = self.teamcity.getBuildProblems('1234') - assert output == [] + self.assertListEqual(output, []) 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_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') - assert output[0]['id'] == problems[0]['id'] - assert output[0]['details'] == problems[0]['details'] - assert output[0]['logUrl'] == self.teamcity.build_url( + 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", { "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') - assert output == [] + self.assertListEqual(output, []) 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_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') - assert output[0]['id'] == failures[0]['id'] - assert output[0]['details'] == failures[0]['details'] - assert output[0]['name'] == failures[0]['name'] - assert output[0]['logUrl'] == self.teamcity.build_url( + 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", { "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'])) self.teamcity.session.send.return_value = triggerBuildResponse output = self.teamcity.trigger_build('1234', 'branch-name', 'test-phid', [{ 'name': 'another-property', 'value': 'some value', }]) - assert output == json.loads(triggerBuildResponse.content) + 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', }], }, }), })) def test_getBuildChangeDetails(self): expectedOutput = { 'username': 'email@bitcoinabc.org', 'user': { 'name': 'Author Name', }, } self.teamcity.session.send.return_value.content = json.dumps( expectedOutput) output = self.teamcity.getBuildChangeDetails('1234') - assert output == expectedOutput + self.assertEqual(output, expectedOutput) 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', }, })), ] output = self.teamcity.getBuildChanges('2345') - assert output[0]['username'] == 'email@bitcoinabc.org' - assert output[0]['user']['name'] == 'Author Name' + 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', }]), changes=test.mocks.teamcity.buildInfo_changes( ['101298f9325ddbac7e5a8f405e5e2f24a64e5171']), ) buildInfo = self.teamcity.getBuildInfo('1234') - assert buildInfo['triggered']['type'] == 'vcs' - assert buildInfo.getProperties().get('env.ABC_BUILD_NAME') == 'build-diff' - assert buildInfo.getCommits( - )[0] == '101298f9325ddbac7e5a8f405e5e2f24a64e5171' + 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", { "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') - assert buildInfo.get('triggered', None) is None - assert buildInfo.getProperties() is None + 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", { "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.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.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.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.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', }, }], })), ] (buildFailures, testFailures) = self.teamcity.getLatestBuildAndTestFailures( 'BitcoinABC_Master') - assert len(buildFailures) == 2 - assert len(testFailures) == 2 + 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) 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", { "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.assertEqual(call_getLastCompletedBuild(), None) # A build completed self.teamcity.session.send.return_value.content = json.dumps({ 'build': [{ 'id': 1234, }], }) build = call_getLastCompletedBuild() self.assertEqual(build["id"], 1234) def test_formatTime(self): - assert 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': [], }) - assert self.teamcity.getNumAggregateFailuresSince('buildType', 0) == 0 + self.assertEqual( + self.teamcity.getNumAggregateFailuresSince( + 'buildType', 0), 0) self.teamcity.session.send.return_value.content = json.dumps({ 'build': [ {'status': 'SUCCESS'}, {'status': 'SUCCESS'}, {'status': 'SUCCESS'}, ], }) - assert self.teamcity.getNumAggregateFailuresSince('buildType', 0) == 0 + self.assertEqual( + self.teamcity.getNumAggregateFailuresSince( + 'buildType', 0), 0) self.teamcity.session.send.return_value.content = json.dumps({ 'build': [{'status': 'FAILURE'}], }) - assert self.teamcity.getNumAggregateFailuresSince('buildType', 0) == 1 + self.assertEqual( + self.teamcity.getNumAggregateFailuresSince( + 'buildType', 0), 1) self.teamcity.session.send.return_value.content = json.dumps({ 'build': [ {'status': 'FAILURE'}, {'status': 'FAILURE'}, {'status': 'FAILURE'}, ] }) - assert self.teamcity.getNumAggregateFailuresSince('buildType', 0) == 1 + 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'}, ] }) - assert self.teamcity.getNumAggregateFailuresSince('buildType', 0) == 2 + 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'}, ] }) - assert self.teamcity.getNumAggregateFailuresSince( - 'buildType', 10000000) == 3 + self.assertEqual( + self.teamcity.getNumAggregateFailuresSince( + 'buildType', 10000000), 3) self.teamcity.session.send.assert_called_with(AnyWith(requests.PreparedRequest, { '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": "{}_Build{}".format(project, i), "name": "My build {}".format(i), "project": { "id": "Root_{}".format(project), "name": "My project {}".format(project) }, "parameters": { "property": [ { "name": "env.ABC_BUILD_NAME", "value": "build-{}".format(i) } ] } } 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) self.teamcity.session.send.assert_called() return config build_names = ["build-{}".format(i) for i in range(3)] # No build type configured configure_build_types(0, 0) config = call_associate_configuration_names(build_names) self.assertDictEqual(config, {}) # No matching build configuration configure_build_types(4, 10) config = call_associate_configuration_names(build_names) self.assertDictEqual(config, {}) # 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", }, } ) # 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__': unittest.main()