def test_phabricator_coverage(mock_config, mock_phabricator, mock_try_task):
    '''
    Test Phabricator reporter publication on a mock coverage issue
    '''
    from static_analysis_bot.report.phabricator import PhabricatorReporter
    from static_analysis_bot.revisions import Revision
    from static_analysis_bot.tasks.coverage import CoverageIssue

    def _check_comment(request):
        # Check the Phabricator main comment is well formed
        payload = urllib.parse.parse_qs(request.body)
        assert payload['output'] == ['json']
        assert len(payload['params']) == 1
        details = json.loads(payload['params'][0])
        assert details['message'] == VALID_COVERAGE_MESSAGE.format(
            results=mock_config.taskcluster.results_dir)

        # Outputs dummy empty response
        resp = {
            'error_code': None,
            'result': None,
        }
        return 201, {
            'Content-Type': 'application/json',
            'unittest': 'coverage'
        }, json.dumps(resp)

    responses.add_callback(
        responses.POST,
        'http://phabricator.test/api/differential.createcomment',
        callback=_check_comment,
    )

    with mock_phabricator as api:
        revision = Revision(api, mock_try_task)
        revision.lines = {
            # Add dummy lines diff
            'test.txt': [0],
            'test.cpp': [0],
            'dom/test.cpp': [
                42,
            ],
        }
        reporter = PhabricatorReporter({'analyzers': ['coverage']}, api=api)

    issue = CoverageIssue('test.cpp', 0, 'This file is uncovered', revision)
    assert issue.is_publishable()

    issues, patches = reporter.publish([
        issue,
    ], revision)
    assert len(issues) == 1
    assert len(patches) == 0

    # Check the callback has been used
    assert len(responses.calls) > 0
    call = responses.calls[-1]
    assert call.request.url == 'http://phabricator.test/api/differential.createcomment'
    assert call.response.headers.get('unittest') == 'coverage'
Пример #2
0
def mock_revision():
    '''
    Mock a mercurial revision
    '''
    from static_analysis_bot.revisions import Revision
    rev = Revision()
    rev.mercurial = 'a6ce14f59749c3388ffae2459327a323b6179ef0'
    return rev
Пример #3
0
def test_taskcluster_index(mock_config, mock_workflow):
    '''
    Test the Taskcluster indexing API
    by mocking an online taskcluster state
    '''
    from static_analysis_bot.config import TaskCluster
    from static_analysis_bot.revisions import Revision
    mock_config.taskcluster = TaskCluster('/tmp/dummy', '12345deadbeef', 0,
                                          False)
    mock_workflow.index_service = mock.Mock()
    rev = Revision()
    rev.namespaces = ['mock.1234']
    rev.as_dict = lambda: {
        'id': '1234',
        'someData': 'mock',
        'state': 'done',
    }
    mock_workflow.index(rev, test='dummy')

    assert mock_workflow.index_service.insertTask.call_count == 3
    calls = mock_workflow.index_service.insertTask.call_args_list

    # First call with namespace
    namespace, args = calls[0][0]
    assert namespace == 'project.releng.services.project.test.static_analysis_bot.mock.1234'
    assert args['taskId'] == '12345deadbeef'
    assert args['data']['test'] == 'dummy'
    assert args['data']['id'] == '1234'
    assert args['data']['source'] == 'try'
    assert args['data']['try_task_id'] == 'remoteTryTask'
    assert args['data']['try_group_id'] == 'remoteTryGroup'
    assert args['data']['someData'] == 'mock'
    assert 'indexed' in args['data']

    # Second call with sub namespace
    namespace, args = calls[1][0]
    assert namespace == 'project.releng.services.project.test.static_analysis_bot.mock.1234.12345deadbeef'
    assert args['taskId'] == '12345deadbeef'
    assert args['data']['test'] == 'dummy'
    assert args['data']['id'] == '1234'
    assert args['data']['source'] == 'try'
    assert args['data']['try_task_id'] == 'remoteTryTask'
    assert args['data']['try_group_id'] == 'remoteTryGroup'
    assert args['data']['someData'] == 'mock'
    assert 'indexed' in args['data']

    # Third call for monitoring
    namespace, args = calls[2][0]
    assert namespace == 'project.releng.services.tasks.12345deadbeef'
    assert args['taskId'] == '12345deadbeef'
    assert args['data']['test'] == 'dummy'
    assert args['data']['id'] == '1234'
    assert args['data']['source'] == 'try'
    assert args['data']['try_task_id'] == 'remoteTryTask'
    assert args['data']['try_group_id'] == 'remoteTryGroup'
    assert args['data']['monitoring_restart'] is False
Пример #4
0
def test_taskcluster_index(mock_try_config, mock_try_workflow):
    '''
    Test the Taskcluster indexing API
    by mocking an online taskcluster state
    '''
    from static_analysis_bot.config import TaskCluster
    from static_analysis_bot.revisions import Revision
    mock_try_config.taskcluster = TaskCluster('/tmp/dummy', '12345deadbeef', 0, False)
    mock_try_workflow.index_service = mock.Mock()
    rev = Revision()
    rev.namespaces = ['mock.1234']
    rev.as_dict = lambda: {'id': '1234', 'someData': 'mock', 'state': 'done', }
    mock_try_workflow.index(rev, test='dummy')

    assert mock_try_workflow.index_service.insertTask.call_count == 3
    calls = mock_try_workflow.index_service.insertTask.call_args_list

    # First call with namespace
    namespace, args = calls[0][0]
    assert namespace == 'project.releng.services.project.test.static_analysis_bot.mock.1234'
    assert args['taskId'] == '12345deadbeef'
    assert args['data']['test'] == 'dummy'
    assert args['data']['id'] == '1234'
    assert args['data']['source'] == 'try'
    assert args['data']['try_task_id'] == 'remoteTryTask'
    assert args['data']['try_group_id'] == 'remoteTryGroup'
    assert args['data']['someData'] == 'mock'
    assert 'indexed' in args['data']

    # Second call with sub namespace
    namespace, args = calls[1][0]
    assert namespace == 'project.releng.services.project.test.static_analysis_bot.mock.1234.12345deadbeef'
    assert args['taskId'] == '12345deadbeef'
    assert args['data']['test'] == 'dummy'
    assert args['data']['id'] == '1234'
    assert args['data']['source'] == 'try'
    assert args['data']['try_task_id'] == 'remoteTryTask'
    assert args['data']['try_group_id'] == 'remoteTryGroup'
    assert args['data']['someData'] == 'mock'
    assert 'indexed' in args['data']

    # Third call for monitoring
    namespace, args = calls[2][0]
    assert namespace == 'project.releng.services.tasks.12345deadbeef'
    assert args['taskId'] == '12345deadbeef'
    assert args['data']['test'] == 'dummy'
    assert args['data']['id'] == '1234'
    assert args['data']['source'] == 'try'
    assert args['data']['try_task_id'] == 'remoteTryTask'
    assert args['data']['try_group_id'] == 'remoteTryGroup'
    assert args['data']['monitoring_restart'] is False
Пример #5
0
def mock_revision(mock_phabricator, mock_try_task, mock_config):
    '''
    Mock a mercurial revision
    '''
    from static_analysis_bot.revisions import Revision
    with mock_phabricator as api:
        return Revision(api, mock_try_task, update_build=False)
    def _test_reporter(api, analyzers):
        # Always use the same setup, only varies the analyzers
        revision = Revision(api, mock_try_task)
        revision.lines = {
            'test.cpp': [0, 41, 42, 43],
            'dom/test.cpp': [
                42,
            ],
        }
        reporter = PhabricatorReporter({'analyzers': analyzers}, api=api)

        issues = [
            ClangFormatIssue('dom/test.cpp', 42, 1, revision),
            ClangTidyIssue(revision, 'test.cpp', '42', '51',
                           'modernize-use-nullptr', 'dummy message', 'error'),
            InferIssue(
                {
                    'file': 'test.cpp',
                    'line': 42,
                    'column': 1,
                    'bug_type': 'dummy',
                    'kind': 'whatever',
                    'qualifier': 'dummy message.',
                }, revision),
            MozLintIssue('test.cpp', 1, 'danger', 42, 'flake8', 'Python error',
                         'EXXX', revision),
            CoverageIssue('test.cpp', 0, 'This file is uncovered', revision),
        ]

        assert all(i.is_publishable() for i in issues)

        revision.improvement_patches = [
            ImprovementPatch('dummy', repr(revision), 'Whatever'),
            ImprovementPatch('clang-tidy', repr(revision), 'Some C fixes'),
            ImprovementPatch('clang-format', repr(revision),
                             'Some lint fixes'),
            ImprovementPatch('infer', repr(revision), 'Some java fixes'),
            ImprovementPatch('mozlint', repr(revision), 'Some js fixes'),
        ]
        list(map(lambda p: p.write(),
                 revision.improvement_patches))  # trigger local write

        return reporter.publish(issues, revision)
Пример #7
0
def test_monitoring_restart(mock_config, mock_workflow):
    '''
    Test the Taskcluster indexing API and restart capabilities
    '''
    from static_analysis_bot.config import TaskCluster
    from static_analysis_bot.revisions import Revision
    mock_config.taskcluster = TaskCluster('/tmp/dummy', 'someTaskId', 0, False)
    mock_workflow.index_service = mock.Mock()
    rev = Revision()
    rev.as_dict = dict
    rev.namespaces = []

    # Unsupported error code
    mock_workflow.index(rev, test='dummy', error_code='nope', state='error')
    assert mock_workflow.index_service.insertTask.call_count == 1
    calls = mock_workflow.index_service.insertTask.call_args_list
    namespace, args = calls[0][0]
    assert namespace == 'project.releng.services.tasks.someTaskId'
    assert args['taskId'] == 'someTaskId'
    assert args['data']['monitoring_restart'] is False

    # watchdog should be restated
    mock_workflow.index(rev,
                        test='dummy',
                        error_code='watchdog',
                        state='error')
    assert mock_workflow.index_service.insertTask.call_count == 2
    calls = mock_workflow.index_service.insertTask.call_args_list
    namespace, args = calls[1][0]
    assert namespace == 'project.releng.services.tasks.someTaskId'
    assert args['taskId'] == 'someTaskId'
    assert args['data']['monitoring_restart'] is True

    # Invalid state
    mock_workflow.index(rev, test='dummy', state='running')
    assert mock_workflow.index_service.insertTask.call_count == 3
    calls = mock_workflow.index_service.insertTask.call_args_list
    namespace, args = calls[2][0]
    assert namespace == 'project.releng.services.tasks.someTaskId'
    assert args['taskId'] == 'someTaskId'
    assert args['data']['monitoring_restart'] is False
Пример #8
0
def test_monitoring_restart(mock_try_config, mock_try_workflow):
    '''
    Test the Taskcluster indexing API and restart capabilities
    '''
    from static_analysis_bot.config import TaskCluster
    from static_analysis_bot.revisions import Revision
    mock_try_config.taskcluster = TaskCluster('/tmp/dummy', 'someTaskId', 0, False)
    mock_try_workflow.index_service = mock.Mock()
    rev = Revision()
    rev.as_dict = dict
    rev.namespaces = []

    # Unsupported error code
    mock_try_workflow.index(rev, test='dummy', error_code='nope', state='error')
    assert mock_try_workflow.index_service.insertTask.call_count == 1
    calls = mock_try_workflow.index_service.insertTask.call_args_list
    namespace, args = calls[0][0]
    assert namespace == 'project.releng.services.tasks.someTaskId'
    assert args['taskId'] == 'someTaskId'
    assert args['data']['monitoring_restart'] is False

    # watchdog should be restated
    mock_try_workflow.index(rev, test='dummy', error_code='watchdog', state='error')
    assert mock_try_workflow.index_service.insertTask.call_count == 2
    calls = mock_try_workflow.index_service.insertTask.call_args_list
    namespace, args = calls[1][0]
    assert namespace == 'project.releng.services.tasks.someTaskId'
    assert args['taskId'] == 'someTaskId'
    assert args['data']['monitoring_restart'] is True

    # Invalid state
    mock_try_workflow.index(rev, test='dummy', state='running')
    assert mock_try_workflow.index_service.insertTask.call_count == 3
    calls = mock_try_workflow.index_service.insertTask.call_args_list
    namespace, args = calls[2][0]
    assert namespace == 'project.releng.services.tasks.someTaskId'
    assert args['taskId'] == 'someTaskId'
    assert args['data']['monitoring_restart'] is False
def test_phabricator_clang_tidy(mock_phabricator, mock_try_task):
    '''
    Test Phabricator reporter publication on a mock clang-tidy issue
    '''
    from static_analysis_bot.report.phabricator import PhabricatorReporter
    from static_analysis_bot.revisions import Revision
    from static_analysis_bot.tasks.clang_tidy import ClangTidyIssue

    def _check_comment(request):
        # Check the Phabricator main comment is well formed
        payload = urllib.parse.parse_qs(request.body)
        assert payload['output'] == ['json']
        assert len(payload['params']) == 1
        details = json.loads(payload['params'][0])
        assert details == {
            'revision_id': 51,
            'message': VALID_CLANG_TIDY_MESSAGE,
            'attach_inlines': 1,
            '__conduit__': {
                'token': 'deadbeef'
            },
        }

        # Outputs dummy empty response
        resp = {
            'error_code': None,
            'result': None,
        }
        return 201, {
            'Content-Type': 'application/json',
            'unittest': 'clang-tidy'
        }, json.dumps(resp)

    responses.add_callback(
        responses.POST,
        'http://phabricator.test/api/differential.createcomment',
        callback=_check_comment,
    )

    with mock_phabricator as api:
        revision = Revision(api, mock_try_task)
        revision.lines = {
            # Add dummy lines diff
            'another_test.cpp': [41, 42, 43],
        }
        revision.files = ['another_test.cpp']
        reporter = PhabricatorReporter(
            {
                'analyzers': ['clang-tidy'],
                'modes': ('comment')
            }, api=api)

    issue = ClangTidyIssue(revision, 'another_test.cpp', '42', '51',
                           'modernize-use-nullptr', 'dummy message', 'error')
    assert issue.is_publishable()

    issues, patches = reporter.publish([
        issue,
    ], revision)
    assert len(issues) == 1
    assert len(patches) == 0

    # Check the callback has been used
    assert len(responses.calls) > 0
    call = responses.calls[-1]
    assert call.request.url == 'http://phabricator.test/api/differential.createcomment'
    assert call.response.headers.get('unittest') == 'clang-tidy'
def test_phabricator_harbormaster(mock_phabricator, mock_try_task):
    '''
    Test Phabricator reporter publication on a mock clang-tidy issue
    using harbormaster
    '''
    from static_analysis_bot.report.phabricator import PhabricatorReporter
    from static_analysis_bot.revisions import Revision
    from static_analysis_bot.tasks.clang_tidy import ClangTidyIssue

    def _check_message(request):
        # Check the Phabricator main comment is well formed
        payload = urllib.parse.parse_qs(request.body)
        assert payload['output'] == ['json']
        assert len(payload['params']) == 1
        details = json.loads(payload['params'][0])
        assert details == {
            'buildTargetPHID':
            'PHID-HMBD-deadbeef12456',
            'lint': [{
                'char': 51,
                'code': 'clang-tidy.modernize-use-nullptr',
                'name': 'Clang-Tidy - modernize-use-nullptr',
                'line': 42,
                'path': 'test.cpp',
                'severity': 'warning',
                'description': 'dummy message'
            }],
            'unit': [],
            'type':
            'work',
            '__conduit__': {
                'token': 'deadbeef'
            },
        }

        # Outputs dummy empty response
        resp = {
            'error_code': None,
            'result': None,
        }
        return 201, {
            'Content-Type': 'application/json',
            'unittest': 'clang-tidy'
        }, json.dumps(resp)

    responses.add_callback(
        responses.POST,
        'http://phabricator.test/api/harbormaster.sendmessage',
        callback=_check_message,
    )

    with mock_phabricator as api:
        revision = Revision(api, mock_try_task)
        revision.lines = {
            # Add dummy lines diff
            'test.cpp': [41, 42, 43],
        }
        revision.build_target_phid = 'PHID-HMBD-deadbeef12456'
        reporter = PhabricatorReporter(
            {
                'analyzers': ['clang-tidy'],
                'mode': 'harbormaster'
            }, api=api)

    issue = ClangTidyIssue(revision, 'test.cpp', '42', '51',
                           'modernize-use-nullptr', 'dummy message', 'error')
    assert issue.is_publishable()

    issues, patches = reporter.publish([
        issue,
    ], revision)
    assert len(issues) == 1
    assert len(patches) == 0

    # Check the callback has been used
    assert len(responses.calls) > 0
    call = responses.calls[-1]
    assert call.request.url == 'http://phabricator.test/api/harbormaster.sendmessage'
    assert call.response.headers.get('unittest') == 'clang-tidy'
def test_phabricator_clang_format(mock_config, mock_phabricator,
                                  mock_try_task):
    '''
    Test Phabricator reporter publication on a mock clang-format issue
    '''
    from static_analysis_bot.report.phabricator import PhabricatorReporter
    from static_analysis_bot.revisions import Revision, ImprovementPatch
    from static_analysis_bot.tasks.clang_format import ClangFormatIssue

    def _check_comment(request):
        # Check the Phabricator main comment is well formed
        payload = urllib.parse.parse_qs(request.body)
        assert payload['output'] == ['json']
        assert len(payload['params']) == 1
        details = json.loads(payload['params'][0])
        assert details['message'] == VALID_CLANG_FORMAT_MESSAGE.format(
            results=mock_config.taskcluster.results_dir)

        # Outputs dummy empty response
        resp = {
            'error_code': None,
            'result': None,
        }
        return 201, {
            'Content-Type': 'application/json',
            'unittest': 'clang-format'
        }, json.dumps(resp)

    responses.add_callback(
        responses.POST,
        'http://phabricator.test/api/differential.createcomment',
        callback=_check_comment,
    )

    with mock_phabricator as api:
        revision = Revision(api, mock_try_task)
        revision.lines = {
            # Add dummy lines diff
            'test.cpp': [41, 42, 43],
            'dom/test.cpp': [
                42,
            ],
        }
        reporter = PhabricatorReporter({'analyzers': ['clang-format']},
                                       api=api)

    issue = ClangFormatIssue('dom/test.cpp', 42, 1, revision)
    assert issue.is_publishable()

    revision.improvement_patches = [
        ImprovementPatch('clang-format', repr(revision), 'Some lint fixes'),
    ]
    list(map(lambda p: p.write(),
             revision.improvement_patches))  # trigger local write

    issues, patches = reporter.publish([
        issue,
    ], revision)
    assert len(issues) == 1
    assert len(patches) == 1

    # Check the callback has been used
    assert len(responses.calls) > 0
    call = responses.calls[-1]
    assert call.request.url == 'http://phabricator.test/api/differential.createcomment'
    assert call.response.headers.get('unittest') == 'clang-format'
def test_analyze_patch():
    from static_analysis_bot.revisions import Revision
    from static_analysis_bot import Issue

    class MyIssue(Issue):
        def __init__(self, path, line):
            self.path = path
            self.line = line
            self.nb_lines = 1

        def as_dict():
            return {}

        def as_markdown():
            return ''

        def as_text():
            return ''

        def validates():
            return True

        def as_phabricator_lint():
            return {}

    issue_in_new_file = MyIssue('new.txt', 1)
    issue_in_existing_file_touched_line = MyIssue('modified.txt', 3)
    issue_in_existing_file_not_changed_line = MyIssue('modified.txt', 1)
    issue_in_existing_file_added_line = MyIssue('added.txt', 4)
    issue_in_not_changed_file = MyIssue('notexisting.txt', 1)

    rev = Revision()
    rev.patch = '''
diff --git a/new.txt b/new.txt
new file mode 100644
index 00000000..83db48f8
--- /dev/null
+++ b/new.txt
@@ -0,0 +1,3 @@
+line1
+line2
+line3
diff --git a/modified.txt b/modified.txt
index 84275f99..cbc9b72a 100644
--- a/modified.txt
+++ b/modified.txt
@@ -1,4 +1,4 @@
 line1
 line2
-line3
+line7
 line4
diff --git a/added.txt b/added.txt
index 83db48f8..84275f99 100644
--- a/added.txt
+++ b/added.txt
@@ -1,3 +1,4 @@
 line1
 line2
 line3
+line4
'''

    rev.analyze_patch()
    assert 'new.txt' in rev.lines
    assert rev.lines['new.txt'] == [1, 2, 3]
    assert 'modified.txt' in rev.lines
    assert rev.lines['modified.txt'] == [3]
    assert 'added.txt' in rev.lines
    assert rev.lines['added.txt'] == [4]
    assert 'new.txt' in rev.files
    assert 'modified.txt' in rev.files
    assert 'added.txt' in rev.files

    assert rev.has_file('new.txt')
    assert rev.has_file('modified.txt')
    assert rev.has_file('added.txt')
    assert not rev.has_file('notexisting.txt')

    assert rev.contains(issue_in_new_file)
    assert rev.contains(issue_in_existing_file_touched_line)
    assert not rev.contains(issue_in_existing_file_not_changed_line)
    assert rev.contains(issue_in_existing_file_added_line)
    assert not rev.contains(issue_in_not_changed_file)
Пример #13
0
def main(taskcluster_secret,
         taskcluster_client_id,
         taskcluster_access_token,
         ):

    secrets = get_secrets(taskcluster_secret,
                          config.PROJECT_NAME,
                          required=(
                              'APP_CHANNEL',
                              'REPORTERS',
                              'PHABRICATOR',
                              'ALLOWED_PATHS',
                          ),
                          existing={
                              'APP_CHANNEL': 'development',
                              'REPORTERS': [],
                              'PUBLICATION': 'IN_PATCH',
                              'ALLOWED_PATHS': ['*', ],
                          },
                          taskcluster_client_id=taskcluster_client_id,
                          taskcluster_access_token=taskcluster_access_token,
                          )

    init_logger(config.PROJECT_NAME,
                PAPERTRAIL_HOST=secrets.get('PAPERTRAIL_HOST'),
                PAPERTRAIL_PORT=secrets.get('PAPERTRAIL_PORT'),
                SENTRY_DSN=secrets.get('SENTRY_DSN'),
                MOZDEF=secrets.get('MOZDEF'),
                timestamp=True,
                )

    # Setup settings before stats
    phabricator = secrets['PHABRICATOR']
    settings.setup(
        secrets['APP_CHANNEL'],
        secrets['PUBLICATION'],
        secrets['ALLOWED_PATHS'],
    )
    # Setup statistics
    datadog_api_key = secrets.get('DATADOG_API_KEY')
    if datadog_api_key:
        stats.auth(datadog_api_key)

    # Load reporters
    reporters = get_reporters(
        secrets['REPORTERS'],
        taskcluster_client_id,
        taskcluster_access_token,
    )

    # Load index service
    index_service = get_service(
        'index',
        taskcluster_client_id,
        taskcluster_access_token,
    )

    # Load queue service
    queue_service = get_service(
        'queue',
        taskcluster_client_id,
        taskcluster_access_token,
    )

    # Load Phabricator API
    phabricator_reporting_enabled = 'phabricator' in reporters
    phabricator_api = PhabricatorAPI(phabricator['api_key'], phabricator['url'])
    if phabricator_reporting_enabled:
        reporters['phabricator'].setup_api(phabricator_api)

    # Load unique revision
    revision = Revision(
        phabricator_api,
        try_task=queue_service.task(settings.try_task_id),

        # Update build status only when phabricator reporting is enabled
        update_build=phabricator_reporting_enabled,
    )

    # Run workflow according to source
    w = Workflow(reporters, index_service, queue_service, phabricator_api)
    try:
        w.run(revision)
    except Exception as e:
        # Log errors to papertrail
        logger.error(
            'Static analysis failure',
            revision=revision,
            error=e,
        )

        # Index analysis state
        extras = {}
        if isinstance(e, AnalysisException):
            extras['error_code'] = e.code
            extras['error_message'] = str(e)
        w.index(revision, state='error', **extras)

        # Update Harbormaster status
        revision.update_status(state=BuildState.Fail)

        # Then raise to mark task as erroneous
        raise