Exemple #1
0
class CodeCov(object):

    def __init__(self, revision, cache_root, client_id, access_token):
        # List of test-suite, sorted alphabetically.
        # This way, the index of a suite in the array should be stable enough.
        self.suites = [
            'web-platform-tests',
        ]

        self.cache_root = cache_root

        assert os.path.isdir(cache_root), 'Cache root {} is not a dir.'.format(cache_root)
        self.repo_dir = os.path.join(cache_root, 'mozilla-central')

        self.client_id = client_id
        self.access_token = access_token

        self.index_service = get_service('index', client_id, access_token)

        self.githubUtils = GitHubUtils(cache_root, client_id, access_token)

        if revision is None:
            # Retrieve revision of latest codecov build
            github_revision = uploader.get_latest_codecov()
            self.revision = self.githubUtils.get_mercurial(github_revision)
            self.from_pulse = False
            suites_to_ignore = []
        else:
            self.revision = revision
            self.from_pulse = True
            suites_to_ignore = ['awsy', 'talos']
            self.notifier = Notifier(revision, client_id, access_token)

        logger.info('Mercurial revision', revision=self.revision)

        task_ids = {
            'linux': taskcluster.get_task('mozilla-central', self.revision, 'linux'),
            'windows': taskcluster.get_task('mozilla-central', self.revision, 'win'),
        }

        self.artifactsHandler = ArtifactsHandler(task_ids, suites_to_ignore)

    def clone_mozilla_central(self, revision):
        shared_dir = self.repo_dir + '-shared'
        cmd = hglib.util.cmdbuilder('robustcheckout',
                                    'https://hg.mozilla.org/mozilla-central',
                                    self.repo_dir,
                                    purge=True,
                                    sharebase=shared_dir,
                                    branch=b'tip')

        cmd.insert(0, hglib.HGPATH)

        def do_clone():
            proc = hglib.util.popen(cmd)
            out, err = proc.communicate()
            if proc.returncode:
                raise hglib.error.CommandError(cmd, proc.returncode, out, err)

            hg = hglib.open(self.repo_dir)

            hg.update(rev=revision, clean=True)

        retry(do_clone)

        logger.info('mozilla-central cloned')

    def generate_suite_reports(self):
        for suite in self.suites:
            output = grcov.report(self.artifactsHandler.get(suite=suite), out_format='lcov')

            info_file = '%s.info' % suite

            with open(info_file, 'wb') as f:
                f.write(output)

            run_check([
                'genhtml',
                '-o', os.path.join(os.getcwd(), suite),
                '--show-details', '--highlight', '--ignore-errors', 'source',
                '--legend', os.path.join(os.getcwd(), info_file),
                '--prefix', self.repo_dir
            ], cwd=self.repo_dir)

            os.remove('%s.info' % suite)

            with tarfile.open('code-coverage-reports/%s.tar.xz' % suite, 'w:xz') as tar:
                tar.add(suite)
            shutil.rmtree(os.path.join(os.getcwd(), suite))

            logger.info('Suite report generated', suite=suite)

    def generate_chunk_mapping(self):
        with ThreadPoolExecutor(max_workers=4) as executor:
            futures = {}
            for platform in ['linux', 'windows']:
                for chunk in self.artifactsHandler.get_chunks():
                    future = executor.submit(grcov.files_list, self.artifactsHandler.get(platform=platform, chunk=chunk), source_dir=self.repo_dir)
                    futures[future] = (platform, chunk)

            with sqlite3.connect('chunk_mapping.sqlite') as conn:
                c = conn.cursor()
                c.execute('CREATE TABLE file_to_chunk (path text, platform text, chunk text)')
                c.execute('CREATE TABLE chunk_to_test (platform text, chunk text, path text)')

                for future in concurrent.futures.as_completed(futures):
                    (platform, chunk) = futures[future]
                    files = future.result()
                    c.executemany('INSERT INTO file_to_chunk VALUES (?,?,?)', ((f, platform, chunk) for f in files))

                try:
                    # Retrieve chunk -> tests mapping from ActiveData.
                    r = requests.post('https://activedata.allizom.org/query', data=json.dumps({
                        'from': 'unittest',
                        'where': {'and': [
                            {'eq': {'repo.branch.name': 'mozilla-central'}},
                            {'eq': {'repo.changeset.id12': self.revision[:12]}},
                            {'or': [
                                {'prefix': {'run.key': 'test-linux64-ccov'}},
                                {'prefix': {'run.key': 'test-windows10-64-ccov'}}
                            ]}
                        ]},
                        'limit': 50000,
                        'select': ['result.test', 'run.key']
                    }))

                    tests_data = r.json()['data']

                    task_names = tests_data['run.key']
                    test_iter = enumerate(tests_data['result.test'])
                    chunk_test_iter = ((taskcluster.get_platform(task_names[i]), taskcluster.get_chunk(task_names[i]), test) for i, test in test_iter)
                    c.executemany('INSERT INTO chunk_to_test VALUES (?,?,?)', chunk_test_iter)
                except KeyError:
                    # ActiveData is failing too often, so we need to ignore the error here.
                    logger.error('Failed to retrieve chunk to tests mapping from ActiveData.')

        with tarfile.open('chunk_mapping.tar.xz', 'w:xz') as tar:
            tar.add('chunk_mapping.sqlite')

    def go(self):
        with ThreadPoolExecutorResult(max_workers=2) as executor:
            # Thread 1 - Download coverage artifacts.
            executor.submit(self.artifactsHandler.download_all)

            # Thread 2 - Clone mozilla-central.
            executor.submit(self.clone_mozilla_central, self.revision)

        if self.from_pulse:
            self.githubUtils.update_geckodev_repo()

            commit_sha = self.githubUtils.get_commit(self.revision)
            logger.info('GitHub revision', revision=commit_sha)

            self.githubUtils.post_github_status(commit_sha)

            r = requests.get('https://hg.mozilla.org/mozilla-central/json-rev/%s' % self.revision)
            r.raise_for_status()
            push_id = r.json()['pushid']

            output = grcov.report(
                self.artifactsHandler.get(),
                source_dir=self.repo_dir,
                service_number=push_id,
                commit_sha=commit_sha,
                token=secrets[secrets.COVERALLS_TOKEN]
            )
            logger.info('Report generated successfully')

            with ThreadPoolExecutorResult(max_workers=2) as executor:
                executor.submit(uploader.coveralls, output)
                executor.submit(uploader.codecov, output, commit_sha)

            logger.info('Waiting for build to be ingested by Codecov...')
            # Wait until the build has been ingested by Codecov.
            if uploader.codecov_wait(commit_sha):
                logger.info('Build ingested by codecov.io')
                self.notifier.notify()
            else:
                logger.error('codecov.io took too much time to ingest data.')
        else:
            os.makedirs('code-coverage-reports', exist_ok=True)

            self.generate_suite_reports()

            report_generators.zero_coverage(self.artifactsHandler.get())

            self.generate_chunk_mapping()

            # Index the task in the TaskCluster index.
            self.index_service.insertTask(
                'project.releng.services.project.{}.shipit_code_coverage.{}'.format(secrets[secrets.APP_CHANNEL], self.revision),
                {
                    'taskId': os.environ['TASK_ID'],
                    'rank': 0,
                    'data': {},
                    'expires': (datetime.utcnow() + timedelta(180)).strftime('%Y-%m-%dT%H:%M:%S.%fZ'),
                }
            )

            os.chdir('code-coverage-reports')
            self.githubUtils.update_codecoveragereports_repo()
Exemple #2
0
class CodeCov(object):
    def __init__(self, revision, cache_root, client_id, access_token):
        # List of test-suite, sorted alphabetically.
        # This way, the index of a suite in the array should be stable enough.
        self.suites = [
            'cppunit',
            'gtest',
            'web-platform-tests',
            'talos',
        ]

        self.cache_root = cache_root

        assert os.path.isdir(cache_root), 'Cache root {} is not a dir.'.format(
            cache_root)
        self.repo_dir = os.path.join(cache_root, 'mozilla-central')

        self.client_id = client_id
        self.access_token = access_token

        self.githubUtils = GitHubUtils(cache_root, client_id, access_token)

        if revision is None:
            # Retrieve revision of latest codecov build
            github_revision = uploader.get_latest_codecov()
            self.revision = self.githubUtils.get_mercurial(github_revision)
            self.from_pulse = False
            suites_to_ignore = []
        else:
            self.revision = revision
            self.from_pulse = True
            suites_to_ignore = ['awsy', 'talos']
            self.notifier = Notifier(revision, client_id, access_token)

        logger.info('Mercurial revision', revision=self.revision)

        task_ids = {
            'linux':
            taskcluster.get_task('mozilla-central', self.revision, 'linux'),
            'windows':
            taskcluster.get_task('mozilla-central', self.revision, 'win'),
        }

        self.artifactsHandler = ArtifactsHandler(task_ids, suites_to_ignore)

    def generate_info(self,
                      commit_sha=None,
                      platform=None,
                      suite=None,
                      chunk=None,
                      out_format='coveralls',
                      options=[]):
        cmd = [
            'grcov',
            '-t',
            out_format,
            '-s',
            self.repo_dir,
            '-p',
            '/home/worker/workspace/build/src/',
            '--ignore-dir',
            'gcc',
            '--ignore-not-existing',
        ]

        if 'coveralls' in out_format:
            r = requests.get(
                'https://hg.mozilla.org/mozilla-central/json-rev/%s' %
                self.revision)
            r.raise_for_status()
            push_id = r.json()['pushid']

            cmd.extend([
                '--service-name',
                'TaskCluster',
                '--service-number',
                str(push_id),
                '--commit-sha',
                commit_sha,
                '--token',
                secrets[secrets.COVERALLS_TOKEN]
                if self.from_pulse else 'NONE',
            ])

            if suite is not None:
                cmd.extend([
                    '--service-job-number',
                    str(self.suites.index(suite) + 1)
                ])
            else:
                cmd.extend(['--service-job-number', '1'])

        cmd.extend(self.artifactsHandler.get(platform, suite, chunk))
        cmd.extend(options)

        return run_check(cmd)

    def generate_report(self, output, suite):
        info_file = '%s.info' % suite

        with open(info_file, 'wb') as f:
            f.write(output)

        run_check([
            'genhtml', '-o',
            os.path.join(os.getcwd(), suite), '--show-details', '--highlight',
            '--ignore-errors', 'source', '--legend',
            os.path.join(os.getcwd(), info_file), '--prefix', self.repo_dir
        ],
                  cwd=self.repo_dir)

    def clone_mozilla_central(self, revision):
        shared_dir = self.repo_dir + '-shared'
        cmd = hglib.util.cmdbuilder('robustcheckout',
                                    'https://hg.mozilla.org/mozilla-central',
                                    self.repo_dir,
                                    purge=True,
                                    sharebase=shared_dir,
                                    branch=b'tip')

        cmd.insert(0, hglib.HGPATH)

        def do_clone():
            proc = hglib.util.popen(cmd)
            out, err = proc.communicate()
            if proc.returncode:
                raise hglib.error.CommandError(cmd, proc.returncode, out, err)

            hg = hglib.open(self.repo_dir)

            hg.update(rev=revision, clean=True)

        retry(do_clone)

        logger.info('mozilla-central cloned')

    def generate_per_suite_reports(self):
        def generate_suite_report(suite):
            output = self.generate_info(suite=suite, out_format='lcov')

            self.generate_report(output, suite)
            os.remove('%s.info' % suite)

            with tarfile.open('code-coverage-reports/%s.tar.bz2' % suite,
                              'w:bz2') as tar:
                tar.add(suite)
            shutil.rmtree(os.path.join(os.getcwd(), suite))

            logger.info('Suite report generated', suite=suite)

        with ThreadPoolExecutor(max_workers=2) as executor:
            for suite in self.suites:
                executor.submit(generate_suite_report, suite)

    def generate_zero_coverage_report(self):
        report = self.generate_info(self.revision, out_format='coveralls+')
        report = json.loads(report.decode(
            'utf-8'))  # Decoding is only necessary until Python 3.6.

        zero_coverage_files = []
        zero_coverage_functions = {}
        for sf in report['source_files']:
            name = sf['name']

            # For C/C++ source files, we can consider a file as being uncovered
            # when all its source lines are uncovered.
            all_lines_uncovered = all(c is None or c == 0
                                      for c in sf['coverage'])
            # For JavaScript files, we can't do the same, as the top-level is always
            # executed, even if it just contains declarations. So, we need to check if
            # all its functions, except the top-level, are uncovered.
            all_functions_uncovered = True
            for f in sf['functions']:
                f_name = f['name']
                if f_name == 'top-level':
                    continue

                if not f['exec']:
                    if name in zero_coverage_functions:
                        zero_coverage_functions[name].append(f['name'])
                    else:
                        zero_coverage_functions[name] = [f['name']]
                else:
                    all_functions_uncovered = False

            if all_lines_uncovered or (len(sf['functions']) > 1
                                       and all_functions_uncovered):
                zero_coverage_files.append(name)

        with open('code-coverage-reports/zero_coverage_files.json', 'w') as f:
            json.dump(zero_coverage_files, f)

        mkdir('code-coverage-reports/zero_coverage_functions')

        zero_coverage_function_counts = []
        for fname, functions in zero_coverage_functions.items():
            zero_coverage_function_counts.append({
                'name': fname,
                'funcs': len(functions),
            })
            with open(
                    'code-coverage-reports/zero_coverage_functions/%s.json' %
                    fname.replace('/', '_'), 'w') as f:
                json.dump(functions, f)

        with open('code-coverage-reports/zero_coverage_functions.json',
                  'w') as f:
            json.dump(zero_coverage_function_counts, f)

    def generate_files_list(self, covered=True, platform=None, chunk=None):
        options = ['--filter-covered', '--threads', '2']
        files = self.generate_info(platform=platform,
                                   chunk=chunk,
                                   out_format='files',
                                   options=options)
        return files.splitlines()

    def generate_chunk_mapping(self):
        with ThreadPoolExecutor(max_workers=4) as executor:
            futures = {}
            for platform in ['linux', 'windows']:
                for chunk in self.artifactsHandler.get_chunks():
                    future = executor.submit(self.generate_files_list,
                                             True,
                                             platform=platform,
                                             chunk=chunk)
                    futures[future] = (platform, chunk)

            with sqlite3.connect('chunk_mapping.sqlite') as conn:
                c = conn.cursor()
                c.execute(
                    'CREATE TABLE file_to_chunk (path text, platform text, chunk text)'
                )
                c.execute(
                    'CREATE TABLE chunk_to_test (platform text, chunk text, path text)'
                )

                for future in concurrent.futures.as_completed(futures):
                    (platform, chunk) = futures[future]
                    files = future.result()
                    c.executemany('INSERT INTO file_to_chunk VALUES (?,?,?)',
                                  ((f, platform, chunk) for f in files))

                try:
                    # Retrieve chunk -> tests mapping from ActiveData.
                    r = requests.post(
                        'https://activedata.allizom.org/query',
                        data=json.dumps({
                            'from': 'unittest',
                            'where': {
                                'and': [{
                                    'eq': {
                                        'repo.branch.name': 'mozilla-central'
                                    }
                                }, {
                                    'eq': {
                                        'repo.changeset.id12':
                                        self.revision[:12]
                                    }
                                }, {
                                    'or': [{
                                        'prefix': {
                                            'run.key': 'test-linux64-ccov'
                                        }
                                    }, {
                                        'prefix': {
                                            'run.key': 'test-windows10-64-ccov'
                                        }
                                    }]
                                }]
                            },
                            'limit': 50000,
                            'select': ['result.test', 'run.key']
                        }))

                    tests_data = r.json()['data']

                    task_names = tests_data['run.key']
                    test_iter = enumerate(tests_data['result.test'])
                    chunk_test_iter = ((taskcluster.get_platform(
                        task_names[i]), taskcluster.get_chunk(task_names[i]),
                                        test) for i, test in test_iter)
                    c.executemany('INSERT INTO chunk_to_test VALUES (?,?,?)',
                                  chunk_test_iter)
                except:
                    # ActiveData is failing too often, so we need to ignore the error here.
                    pass

        with tarfile.open('code-coverage-reports/chunk_mapping.tar.xz',
                          'w:xz') as tar:
            tar.add('chunk_mapping.sqlite')

    def go(self):
        with ThreadPoolExecutorResult(max_workers=2) as executor:
            # Thread 1 - Download coverage artifacts.
            executor.submit(self.artifactsHandler.download_all)

            # Thread 2 - Clone mozilla-central.
            executor.submit(self.clone_mozilla_central, self.revision)

        if self.from_pulse:
            self.githubUtils.update_geckodev_repo()

            commit_sha = self.githubUtils.get_commit(self.revision)
            logger.info('GitHub revision', revision=commit_sha)

            self.githubUtils.post_github_status(commit_sha)

            output = self.generate_info(commit_sha)
            logger.info('Report generated successfully')

            with ThreadPoolExecutorResult(max_workers=2) as executor:
                executor.submit(uploader.coveralls, output)
                executor.submit(uploader.codecov, output, commit_sha)

            logger.info('Waiting for build to be ingested by Codecov...')
            # Wait until the build has been ingested by Codecov.
            if uploader.codecov_wait(commit_sha):
                logger.info('Build ingested by codecov.io')
                self.notifier.notify()
            else:
                logger.info('codecov.io took too much time to ingest data.')
        else:
            mkdir('code-coverage-reports')

            self.generate_per_suite_reports()

            self.generate_zero_coverage_report()

            self.generate_chunk_mapping()

            os.chdir('code-coverage-reports')
            self.githubUtils.update_codecoveragereports_repo()