def __init__( self, reporters, index_service, queue_service, phabricator_api, zero_coverage_enabled=True, ): assert settings.try_task_id is not None, "Cannot run without Try task id" assert settings.try_group_id is not None, "Cannot run without Try task id" self.zero_coverage_enabled = zero_coverage_enabled # Use share phabricator API client assert isinstance(phabricator_api, PhabricatorAPI) self.phabricator = phabricator_api # Load reporters to use self.reporters = reporters if not self.reporters: logger.warn("No reporters configured, this analysis will not be published") # Always add debug reporter and Diff reporter self.reporters["debug"] = DebugReporter( output_dir=settings.taskcluster.results_dir ) # Use TC services client self.index_service = index_service self.queue_service = queue_service # Setup Backend API client self.backend_api = BackendAPI()
def test_missing_bugzilla_id(mock_revision, mock_backend, mock_hgmo): """ Test revision creation on the backend without a bugzilla id (None instead) """ # Nothing in backend at first revisions, diffs, issues = mock_backend assert not revisions and not diffs and not issues # Hardcode revision & repo mock_revision.repository = "http://hgmo/test-try" mock_revision.target_repository = "https://hgmo/test" mock_revision.mercurial_revision = "deadbeef1234" # Set bugzilla id as empty string mock_revision.revision["fields"]["bugzilla.bug-id"] = "" assert mock_revision.bugzilla_id is None r = BackendAPI() r.publish_revision(mock_revision) assert len(revisions) == 1 assert 51 in revisions assert revisions[51] == { "bugzilla_id": None, "id": 51, "phid": "PHID-DREV-zzzzz", "repository": "https://hgmo/test", "title": "Static Analysis tests", "diffs_url": "http://code-review-backend.test/v1/revision/51/diffs/", }
def __init__( self, reporters, index_service, queue_service, phabricator_api, zero_coverage_enabled=True, update_build=True, task_failures_ignored=[], ): self.zero_coverage_enabled = zero_coverage_enabled self.update_build = update_build self.task_failures_ignored = task_failures_ignored logger.info("Will ignore task failures", names=self.task_failures_ignored) # Use share phabricator API client assert isinstance(phabricator_api, PhabricatorAPI) self.phabricator = phabricator_api # Load reporters to use self.reporters = reporters if not self.reporters: logger.warn("No reporters configured, this analysis will not be published") # Always add debug reporter and Diff reporter self.reporters["debug"] = DebugReporter( output_dir=settings.taskcluster.results_dir ) # Use TC services client self.index_service = index_service self.queue_service = queue_service # Setup Backend API client self.backend_api = BackendAPI()
def test_publication_failures(mock_coverity_issues, mock_revision, mock_backend, mock_hgmo): """ Test publication of issues on the backend with some bad urls """ # Nothing in backend at first revisions, diffs, issues = mock_backend assert not revisions and not diffs and not issues # Hardcode revision & repo mock_revision.repository = "http://hgmo/test-try" mock_revision.target_repository = "https://hgmo/test" mock_revision.mercurial_revision = "deadbeef1234" assert mock_revision.bugzilla_id == 1234567 r = BackendAPI() assert r.enabled is True # Use a bad relative path in last issue mock_coverity_issues[-1].path = "../../../bad/path.cpp" assert mock_coverity_issues[0].path == "some/file/path" # Only one issue should be published as the bad one is ignored mock_revision.issues_url = "http://code-review-backend.test/v1/diff/42/issues/" published = r.publish_issues(mock_coverity_issues, mock_revision) assert published == 1 # Check the issues in the backend assert len(issues) == 1 assert 42 in issues assert len(issues[42]) == 1 assert issues[42] == [{ "analyzer": "mock-coverity", "check": "flag", "column": None, "hash": "3731a6559c9a72d09f4bad85db3f0416", "id": "9f6aa76a-623d-5096-82ed-876b01f9fbce", "in_patch": False, "level": "warning", "line": None, "message": "Unidentified symbol", "nb_lines": 1, "path": "some/file/path", "publishable": False, "validates": False, "fix": None, }]
def __init__(self): self.reporters = {} self.phabricator_api = None self.index_service = mock_taskcluster_config.get_service("index") self.queue_service = mock_taskcluster_config.get_service("queue") self.zero_coverage_enabled = True self.backend_api = BackendAPI()
def __init__(self): self.reporters = {} self.phabricator_api = None self.index_service = mock_taskcluster_config.get_service("index") self.queue_service = mock_taskcluster_config.get_service("queue") self.zero_coverage_enabled = True self.backend_api = BackendAPI() self.update_build = False self.task_failures_ignored = []
def test_repo_url(mock_coverity_issues, mock_revision, mock_backend, mock_hgmo): """ Check that the backend client verifies repositories are URLs """ mock_revision.mercurial_revision = "deadbeef1234" r = BackendAPI() assert r.enabled is True # Invalid target repo mock_revision.target_repository = "test" with pytest.raises(AssertionError) as e: r.publish_revision(mock_revision) assert str(e.value) == "Repository test is not an url" # Invalid repo mock_revision.target_repository = "http://xxx/test" mock_revision.repository = "somewhere/test-try" with pytest.raises(AssertionError) as e: r.publish_revision(mock_revision) assert str(e.value) == "Repository somewhere/test-try is not an url"
class Workflow(object): """ Full static analysis workflow - setup remote analysis workflow - find issues from remote tasks - publish issues """ def __init__( self, reporters, index_service, queue_service, phabricator_api, zero_coverage_enabled=True, ): assert settings.try_task_id is not None, "Cannot run without Try task id" assert settings.try_group_id is not None, "Cannot run without Try task id" self.zero_coverage_enabled = zero_coverage_enabled # Use share phabricator API client assert isinstance(phabricator_api, PhabricatorAPI) self.phabricator = phabricator_api # Load reporters to use self.reporters = reporters if not self.reporters: logger.warn("No reporters configured, this analysis will not be published") # Always add debug reporter and Diff reporter self.reporters["debug"] = DebugReporter( output_dir=settings.taskcluster.results_dir ) # Use TC services client self.index_service = index_service self.queue_service = queue_service # Setup Backend API client self.backend_api = BackendAPI() def run(self, revision): """ Find all issues on remote tasks and publish them """ # Index ASAP Taskcluster task for this revision self.index(revision, state="started") # Set the Phabricator build as running revision.update_status(state=BuildState.Work) # Analyze revision patch to get files/lines data revision.analyze_patch() # Find issues on remote tasks issues, task_failures = self.find_issues(revision) if not issues and not task_failures: logger.info("No issues, stopping there.") self.index(revision, state="done", issues=0) revision.update_status(BuildState.Pass) return [] # Publish issues on backend to retrieve their comparison state self.backend_api.publish_issues(issues, revision) # Publish all issues self.publish(revision, issues, task_failures) return issues def publish(self, revision, issues, task_failures): """ Publish issues on selected reporters """ # Publish patches on Taskcluster # or write locally for local development for patch in revision.improvement_patches: if settings.taskcluster.local: patch.write() else: patch.publish(self.queue_service) # Report issues publication stats nb_issues = len(issues) nb_publishable = len([i for i in issues if i.is_publishable()]) self.index( revision, state="analyzed", issues=nb_issues, issues_publishable=nb_publishable, ) stats.add_metric("analysis.issues.publishable", nb_publishable) # Publish reports about these issues with stats.timer("runtime.reports"): for reporter in self.reporters.values(): reporter.publish(issues, revision, task_failures) self.index( revision, state="done", issues=nb_issues, issues_publishable=nb_publishable ) # Publish final HarborMaster state revision.update_status( nb_publishable > 0 and BuildState.Fail or BuildState.Pass ) def index(self, revision, **kwargs): """ Index current task on Taskcluster index """ assert isinstance(revision, Revision) if settings.taskcluster.local or self.index_service is None: logger.info("Skipping taskcluster indexing", rev=str(revision), **kwargs) return # Build payload payload = revision.as_dict() payload.update(kwargs) # Always add the indexing now = datetime.utcnow() payload["indexed"] = now.strftime(TASKCLUSTER_DATE_FORMAT) # Always add the source and try config payload["source"] = "try" payload["try_task_id"] = settings.try_task_id payload["try_group_id"] = settings.try_group_id # Always add the repository we are working on # This is mainly used by the frontend to list & filter diffs payload["repository"] = revision.target_repository # Add restartable flag for monitoring payload["monitoring_restart"] = payload["state"] == "error" and payload.get( "error_code" ) in ("watchdog", "mercurial") # Add a sub namespace with the task id to be able to list # tasks from the parent namespace namespaces = revision.namespaces + [ "{}.{}".format(namespace, settings.taskcluster.task_id) for namespace in revision.namespaces ] # Build complete namespaces list, with monitoring update full_namespaces = [ TASKCLUSTER_NAMESPACE.format(channel=settings.app_channel, name=name) for name in namespaces ] full_namespaces.append( "project.relman.tasks.{}".format(settings.taskcluster.task_id) ) # Index for all required namespaces for namespace in full_namespaces: self.index_service.insertTask( namespace, { "taskId": settings.taskcluster.task_id, "rank": 0, "data": payload, "expires": (now + timedelta(days=TASKCLUSTER_INDEX_TTL)).strftime( TASKCLUSTER_DATE_FORMAT ), }, ) def find_issues(self, revision): """ Find all issues on remote Taskcluster task group """ # Load all tasks in task group tasks = self.queue_service.listTaskGroup(settings.try_group_id) assert "tasks" in tasks tasks = {task["status"]["taskId"]: task for task in tasks["tasks"]} assert len(tasks) > 0 logger.info( "Loaded Taskcluster group", id=settings.try_group_id, tasks=len(tasks) ) # Update the local revision with tasks revision.setup_try(tasks) # Store the revision in the backend # It needs to be after setup_try to have a repository value self.backend_api.publish_revision(revision) # Load task description task = tasks.get(settings.try_task_id) assert task is not None, "Missing task {}".format(settings.try_task_id) dependencies = task["task"]["dependencies"] assert len(dependencies) > 0, "No task dependencies to analyze" # Skip dependencies not in group # But log all skipped tasks def _in_group(dep_id): if dep_id not in tasks: # Used for docker images produced in tree # and other artifacts logger.info("Skip dependency not in group", task_id=dep_id) return False return True dependencies = [dep_id for dep_id in dependencies if _in_group(dep_id)] # Do not run parsers when we only have a gecko decision task # That means no analyzer were triggered by the taskgraph decision task # This can happen if the patch only touches file types for which we have no analyzer defined # See issue https://github.com/mozilla/release-services/issues/2055 if len(dependencies) == 1: task = tasks[dependencies[0]] if task["task"]["metadata"]["name"] == "Gecko Decision Task": logger.warn("Only dependency is a Decision Task, skipping analysis") return [], [] # Add zero-coverage task if self.zero_coverage_enabled: dependencies.append(ZeroCoverageTask) # Find issues and patches in dependencies issues = [] task_failures = [] for dep in dependencies: try: if isinstance(dep, type) and issubclass(dep, AnalysisTask): # Build a class instance from its definition and route task = dep.build_from_route(self.index_service, self.queue_service) else: # Use a task from its id & description task = self.build_task(dep, tasks[dep]) if task is None: continue artifacts = task.load_artifacts(self.queue_service) if artifacts is not None: task_issues = task.parse_issues(artifacts, revision) logger.info( "Found {} issues".format(len(task_issues)), task=task.name, id=task.id, ) stats.report_task(task, task_issues) issues += task_issues task_patches = task.build_patches(artifacts) for patch in task_patches: revision.add_improvement_patch(task.name, patch) # Report a problem when tasks in erroneous state are found # but no issue or patch has been processed by the bot if task.state == "failed" and not task_issues and not task_patches: logger.warning( "An erroneous task processed some artifacts and found no issues or patches", task=task.name, id=task.id, ) task_failures.append(task) except Exception as e: logger.warn( "Failure during task analysis", task=settings.taskcluster.task_id, error=e, ) raise return issues, task_failures def build_task(self, task_id, task_status): """ Create a specific implementation of AnalysisTask according to the task name """ try: name = task_status["task"]["metadata"]["name"] except KeyError: raise Exception("Cannot read task name {}".format(task_id)) # Default format is used first when the correct artifact is available if DefaultTask.matches(task_id): return DefaultTask(task_id, task_status) elif name.startswith("source-test-mozlint-"): return MozLintTask(task_id, task_status) elif name == "source-test-clang-tidy": return ClangTidyTask(task_id, task_status) elif name == "source-test-clang-format": return ClangFormatTask(task_id, task_status) elif name in ("source-test-coverity-coverity", "coverity"): return CoverityTask(task_id, task_status) elif name == "source-test-infer-infer": return InferTask(task_id, task_status) elif name.startswith("source-test-"): logger.error(f"Unsupported {name} task: will need a local implementation") else: raise Exception("Unsupported task {}".format(name))
def test_publication(mock_coverity_issues, mock_revision, mock_backend, mock_hgmo): """ Test publication of issues on the backend """ # Nothing in backend at first revisions, diffs, issues = mock_backend assert not revisions and not diffs and not issues # Hardcode revision & repo mock_revision.repository = "http://hgmo/test-try" mock_revision.target_repository = "https://hgmo/test" mock_revision.mercurial_revision = "deadbeef1234" assert mock_revision.bugzilla_id == 1234567 r = BackendAPI() assert r.enabled is True r.publish_revision(mock_revision) # Check the revision in the backend assert len(revisions) == 1 assert 51 in revisions assert revisions[51] == { "bugzilla_id": 1234567, "id": 51, "phid": "PHID-DREV-zzzzz", "repository": "https://hgmo/test", "title": "Static Analysis tests", "diffs_url": "http://code-review-backend.test/v1/revision/51/diffs/", } # Check the diff in the backend assert len(diffs) == 1 assert 42 in diffs assert diffs[42] == { "id": 42, "issues_url": "http://code-review-backend.test/v1/diff/42/issues/", "mercurial_hash": "deadbeef1234", "phid": "PHID-DIFF-test", "review_task_id": "local instance", "repository": "http://hgmo/test-try", } # No issues at that point assert len(issues) == 0 # Let's publish them published = r.publish_issues(mock_coverity_issues, mock_revision) assert published == len(mock_coverity_issues) == 2 # Check the issues in the backend assert len(issues) == 1 assert 42 in issues assert len(issues[42]) == 2 assert issues[42] == [ { "analyzer": "mock-coverity", "check": "flag", "column": None, "hash": "3731a6559c9a72d09f4bad85db3f0416", "id": "9f6aa76a-623d-5096-82ed-876b01f9fbce", "in_patch": False, "level": "warning", "line": None, "message": "Unidentified symbol", "nb_lines": 1, "path": "some/file/path", "publishable": False, "validates": False, "fix": None, }, { "analyzer": "mock-coverity", "check": "flag", "column": None, "hash": "172f015fbc43268d712c2fc7acbf1023", "id": "98d7e3b0-e903-57e3-9973-d11d3a9849f4", "in_patch": False, "level": "error", "line": 1, "message": "Unidentified symbol", "nb_lines": 1, "path": "some/file/path", "publishable": False, "validates": False, "fix": None, }, ]