class Analysis(Base):
    """Table for Analysis."""

    __tablename__ = 'analyses'

    id = Column(Integer, primary_key=True)
    version_id = Column(Integer, ForeignKey(Version.id), index=True)
    access_count = Column(Integer, default=0)
    started_at = Column(DateTime)
    finished_at = Column(DateTime)
    # debugging and stuff
    subtasks = Column(JSONB)
    release = Column(String(255))  # TODO: is length 255 enough for release?
    audit = Column(JSONB)

    version = relationship(Version, back_populates='analyses', lazy='joined')

    @property
    def analyses(self):
        """Get all worker results for this analysis."""
        s = Session.object_session(self)
        if s:
            worker_results = s.query(WorkerResult).filter(
                WorkerResult.analysis_id == self.id)
            return {wr.worker: wr.task_result for wr in worker_results}
        return {}

    @property
    def raw_analyses(self):
        """Get all worker results for this analysis."""
        s = Session.object_session(self)
        if s:
            return s.query(WorkerResult).filter(WorkerResult.analysis_id == self.id)
        return []

    @property
    def package_info(self):
        """Get dependents_count and relative_usage.

        This property is DEPRECATED, it will be removed in future.
        """
        return {'dependents_count': -1, 'relative_usage': 'not used'}

    @property
    def dependents_count(self):
        """Get dependents_count.

        This property is DEPRECATED, it will be removed in future.
        """
        return -1

    def to_dict(self, omit_analyses=False):
        """Convert to dictionary."""
        res = Base.to_dict(self)
        res['analyses'] = {} if omit_analyses else self.analyses
        res.pop('version_id')
        res['version'] = self.version.identifier
        res['package'] = self.version.package.name
        res['ecosystem'] = self.version.package.ecosystem.name
        res['package_info'] = self.package_info
        res['dependents_count'] = self.dependents_count
        return res class Package(Base):
    """Table for Package."""

    __tablename__ = 'packages'
    # ecosystem_id together with name must be unique
    __table_args__ = (UniqueConstraint(
        'ecosystem_id', 'name', name='ep_unique'),)

    id = Column(Integer, primary_key=True)
    ecosystem_id = Column(Integer, ForeignKey(Ecosystem.id))
    name = Column(String(2048), index=True)

    ecosystem = relationship(
        Ecosystem, back_populates='packages', lazy='joined')
    versions = relationship('Version', back_populates='package')

    @classmethod
    def by_name(cls, session, name):
        """Get a row with specified name."""
        # TODO: this is dangerous at is does not consider Ecosystem
        try:
            return cls._by_attrs(session, name=name)
        except NoResultFound:
            # What to do here ?
            raise class Version(Base):
    """Table for Version."""

    __tablename__ = 'versions'
    # package_id together with version identifier must be unique
    __table_args__ = (UniqueConstraint(
        'package_id', 'identifier', name='pv_unique'),)

    id = Column(Integer, primary_key=True)
    package_id = Column(Integer, ForeignKey(Package.id))
    identifier = Column(String(255), index=True)
    synced2graph = Column(Boolean, nullable=False, default=False, index=True)

    package = relationship(Package, back_populates='versions', lazy='joined')
    analyses = relationship('Analysis', back_populates='version')

    @classmethod
    def by_identifier(cls, session, identifier):
        """Get a row with specified identifier."""
        try:
            return cls._by_attrs(session, identifier=identifier)
        except NoResultFound:
            # What to do here ?
            raise def get_ecosystem_solver(ecosystem, with_parser=None, with_fetcher=None):
    """Get Solver subclass instance for particular ecosystem.

    :param ecosystem: Ecosystem
    :param with_parser: DependencyParser instance
    :param with_fetcher: ReleasesFetcher instance
    :return: Solver
    """
    if ecosystem.is_backed_by(EcosystemBackend.maven):
        if with_parser is None:
            return MavenSolver()
        else:
            return MavenManualSolver(ecosystem, with_parser, with_fetcher)
    elif ecosystem.is_backed_by(EcosystemBackend.npm):
        return NpmSolver(ecosystem, with_parser, with_fetcher)
    elif ecosystem.is_backed_by(EcosystemBackend.pypi):
        return PypiSolver(ecosystem, with_parser, with_fetcher)
    elif ecosystem.is_backed_by(EcosystemBackend.nuget):
        return NugetSolver(ecosystem, with_parser, with_fetcher)
    elif ecosystem.is_backed_by(EcosystemBackend.go):
        return GolangSolver(ecosystem, with_parser, with_fetcher)

    raise ValueError('Unknown ecosystem: {}'.format(ecosystem.name)) class Dependency(object):
    """A Dependency consists of (package) name and version spec."""

    def __init__(self, name, spec):
        """Initialize instance."""
        self._name = name
        # spec is a list where each item is either 2-tuple (operator, version) or list of these
        # example: [[('>=', '0.6.0'), ('<', '0.7.0')], ('>', '1.0.0')] means:
        # (>=0.6.0 and <0.7.0) or >1.0.0
        self._spec = spec

    @property
    def name(self):
        """Get name property."""
        return self._name

    @property
    def spec(self):
        """Get version spec property."""
        return self._spec

    def __contains__(self, item):
        """Implement 'in' operator."""
        return self.check(item)

    def __repr__(self):
        """Return string representation of this instance."""
        return "{} {}".format(self.name, self.spec)

    def __eq__(self, other):
        """Implement '==' operator."""
        return self.name == other.name and self.spec == other.spec

    def check(self, version):
        """Check if `version` fits into our dependency specification.

        :param version: str
        :return: bool
        """
        def _compare_spec(spec):
            if len(spec) == 1:
                spec = ('=', spec[0])

            token = Tokens.operators.index(spec[0])
            comparison = compare_version(version, spec[1])
            if token in [Tokens.EQ1, Tokens.EQ2]:
                return comparison == 0
            elif token == Tokens.GT:
                return comparison == 1
            elif token == Tokens.LT:
                return comparison == -1
            elif token == Tokens.GTE:
                return comparison >= 0
            elif token == Tokens.LTE:
                return comparison <= 0
            elif token == Tokens.NEQ:
                return comparison != 0
            else:
                raise ValueError('Invalid comparison token')

        def _all(spec_):
            return all(_all(s) if isinstance(s, list) else _compare_spec(s) for s in spec_)

        return any(_all(s) if isinstance(s, list) else _compare_spec(s) for s in self.spec) class PypiDependencyParser(DependencyParser):
    """Pypi Dependency parsing."""

    @staticmethod
    def _parse_python(spec):
        """Parse PyPI specification of a single dependency.

        :param spec: str, for example "Django>=1.5,<1.8"
        :return: [Django [[('>=', '1.5'), ('<', '1.8')]]]
        """
        def _extract_op_version(spec):
            # https://www.python.org/dev/peps/pep-0440/#compatible-release
            if spec.operator == '~=':
                version = _version_split(spec.version)
                if len(version) > 1:
                    # ignore pre-release, post-release or developmental release
                    while not version[-1].isdigit():
                        del version[-1]
                    del version[-1]  # will increase the last but one in next line
                    version[-1] = str(int(version[-1]) + 1)
                else:
                    raise ValueError('%r must not be used with %r' % (spec.operator, spec.version))
                return [('>=', spec.version), ('<', '.'.join(version))]
            # Trailing .* is permitted per
            # https://www.python.org/dev/peps/pep-0440/#version-matching
            elif spec.operator == '==' and spec.version.endswith('.*'):
                try:
                    result = check_output(['/usr/bin/semver-ranger', spec.version],
                                          universal_newlines=True).strip()
                    gte, lt = result.split()
                    return [('>=', gte.lstrip('>=')), ('<', lt.lstrip('<'))]
                except ValueError:
                    logger.info("couldn't resolve ==%s", spec.version)
                    return spec.operator, spec.version
            # https://www.python.org/dev/peps/pep-0440/#arbitrary-equality
            # Use of this operator is heavily discouraged, so just convert it to 'Version matching'
            elif spec.operator == '===':
                return '==', spec.version
            else:
                return spec.operator, spec.version

        def _get_pip_spec(requirements):
            """There's no `specs` field In Pip 8+, take info from `specifier` field."""
            if hasattr(requirements, 'specs'):
                return requirements.specs
            elif hasattr(requirements, 'specifier'):
                specs = [_extract_op_version(spec) for spec in requirements.specifier]
                if len(specs) == 0:
                    specs = [('>=', '0.0.0')]
                elif len(specs) > 1:
                    specs = [specs]
                return specs

        # create a temporary file and store the spec there since
        # `parse_requirements` requires a file
        with NamedTemporaryFile(mode='w+', suffix='pysolve') as f:
            f.write(spec)
            f.flush()
            parsed = parse_requirements(f.name, session=f.name)
            dependency = [Dependency(x.name, _get_pip_spec(x.req)) for x in parsed].pop()

        return dependency

    def parse(self, specs):
        """Parse specs."""
        return [self._parse_python(s) for s in specs]

    @staticmethod
    def compose(deps):
        """Compose deps."""
        return DependencyParser.compose_sep(deps, ',')

    @staticmethod
    def restrict_versions(deps):
        """Not implemented."""
        return deps  # TODO class NpmDependencyParser(DependencyParser):
    """NPM Dependency parsing."""

    @staticmethod
    def _parse_npm_tokens(spec):
        """Parse npm tokens."""
        for token in Tokens.operators:
            if token in spec:
                return token, spec.split(token)[1]
        return spec,

    def _parse_npm(self, name, spec):
        """Parse NPM specification of a single dependency.

        :param name: str
        :param spec: str
        :return: Dependency
        """
        if spec == 'latest':
            specs = '*'
        else:
            specs = check_output(['/usr/bin/semver-ranger', spec], universal_newlines=True).strip()
            if specs == 'null':
                logger.info("invalid version specification for %s = %s", name, spec)
                return None

        ret = []
        for s in specs.split('||'):
            if ' ' in s:
                spaced = s.split(' ')
                assert len(spaced) == 2
                left, right = spaced
                ret.append([self._parse_npm_tokens(left), self._parse_npm_tokens(right)])
            elif s == '*':
                ret.append(('>=', '0.0.0'))
            else:
                ret.append(self._parse_npm_tokens(s))

        return Dependency(name, ret)

    def parse(self, specs):
        """Transform list of dependencies (strings) to list of Dependency."""
        deps = []
        for spec in specs:
            name, ver = spec.split(' ', 1)
            parsed = self._parse_npm(name, ver)
            if parsed:
                deps.append(parsed)

        return deps

    @staticmethod
    def compose(deps):
        """Oposite of parse()."""
        return DependencyParser.compose_sep(deps, ' ')

    @staticmethod
    def restrict_versions(deps):
        """From list of semver ranges select only the most restricting ones for each operator.

        :param deps:  list of Dependency(), example:
                           [node [('>=', '0.6.0')], node [('<', '1.0.0')], node [('>=', '0.8.0')]]
        :return: list of Dependency() with only the most restrictive versions, example:
                           [node [('<', '1.0.0')], node [('>=', '0.8.0')]]
        """
        # list to dict
        # {
        #    'node' : {
        #            '>=': ['0.8.0', '0.6.0'],
        #            '<': ['1.0.0']
        #        }
        #  }
        dps_dict = defaultdict(dict)
        for dp in deps:
            if dp.name not in dps_dict:
                dps_dict[dp.name] = defaultdict(list)
            for spec in dp.spec:
                if len(spec) != 2:
                    continue
                operator, version = spec
                dps_dict[dp.name][operator].append(version)

        # select only the most restrictive versions
        result = []
        for name, version_spec_dict in dps_dict.items():
            specs = []
            for operator, versions in version_spec_dict.items():
                if operator in ['>', '>=']:  # select highest version
                    version = sorted(versions, key=cmp_to_key(compare_version))[-1]
                elif operator in ['<', '<=']:  # select lowest version
                    version = sorted(versions, key=cmp_to_key(compare_version))[0]
                specs.append((operator, version))
            # dict back to list
            result.append(Dependency(name, specs))

        return result class OSSIndexDependencyParser(NpmDependencyParser):
    """Parse OSS Index version specification."""

    def _parse_npm(self, name, spec):
        """Parse OSS Index version specification. It's similar to NPM semver, with few tweaks."""
        # sometimes there's '|' instead of  '||', but the meaning seems to be the same
        spec = spec.replace(' | ', ' || ')
        # remove superfluous brackets
        spec = spec.replace('(', '').replace(')', '')
        return super()._parse_npm(name, spec) class NugetDependencyParser(object):
    """Nuget version specification parsing."""

    def parse(self, specs):
        """Transform list of dependencies (strings) to list of Dependency.

        https://docs.microsoft.com/en-us/nuget/create-packages/dependency-versions#version-ranges
        :param specs:  list of dependencies (strings)
        :return: list of Dependency
        """
        # TODO: reduce cyclomatic complexity
        deps = []
        for spec in specs:
            name, version_range = spec.split(' ', 1)

            # 1.0 -> 1.0≤x
            if re.search(r'[,()\[\]]', version_range) is None:
                dep = Dependency(name, [('>=', version_range)])
            # [1.0,2.0] -> 1.0≤x≤2.0
            elif re.fullmatch(r'\[(.+),(.+)\]', version_range):
                m = re.fullmatch(r'\[(.+),(.+)\]', version_range)
                dep = Dependency(name, [[('>=', m.group(1)), ('<=', m.group(2))]])
            # (1.0,2.0) -> 1.0<x<2.0
            elif re.fullmatch(r'\((.+),(.+)\)', version_range):
                m = re.fullmatch(r'\((.+),(.+)\)', version_range)
                dep = Dependency(name, [[('>', m.group(1)), ('<', m.group(2))]])
            # The following one is not in specification,
            # so we can just guess what was the intention.
            # Seen in NLog:5.0.0-beta08 dependencies
            # [1.0, ) -> 1.0≤x
            elif re.fullmatch(r'\[(.+), \)', version_range):
                m = re.fullmatch(r'\[(.+), \)', version_range)
                dep = Dependency(name, [('>=', m.group(1))])
            # [1.0,2.0) -> 1.0≤x<2.0
            elif re.fullmatch(r'\[(.+),(.+)\)', version_range):
                m = re.fullmatch(r'\[(.+),(.+)\)', version_range)
                dep = Dependency(name, [[('>=', m.group(1)), ('<', m.group(2))]])
            # (1.0,) -> 1.0<x
            elif re.fullmatch(r'\((.+),\)', version_range):
                m = re.fullmatch(r'\((.+),\)', version_range)
                dep = Dependency(name, [('>', m.group(1))])
            # [1.0] -> x==1.0
            elif re.fullmatch(r'\[(.+)\]', version_range):
                m = re.fullmatch(r'\[(.+)\]', version_range)
                dep = Dependency(name, [('==', m.group(1))])
            # (,1.0] -> x≤1.0
            elif re.fullmatch(r'\(,(.+)\]', version_range):
                m = re.fullmatch(r'\(,(.+)\]', version_range)
                dep = Dependency(name, [('<=', m.group(1))])
            # (,1.0) -> x<1.0
            elif re.fullmatch(r'\(,(.+)\)', version_range):
                m = re.fullmatch(r'\(,(.+)\)', version_range)
                dep = Dependency(name, [('<', m.group(1))])
            elif re.fullmatch(r'\((.+)\)', version_range):
                raise ValueError("invalid version range %r" % version_range)

            deps.append(dep)

        return deps class GolangDependencyParser(DependencyParser):
    """Dependency parser for Golang."""

    def parse(self, specs):
        """Transform list of dependencies (strings) to list of Dependency."""
        dependencies = []
        for spec in specs:
            spec_list = spec.split(' ')
            if len(spec_list) > 1:
                dependencies.append(Dependency(spec_list[0], spec_list[1]))
            else:
                dependencies.append(Dependency(spec_list[0], ''))
        return dependencies

    @staticmethod
    def compose(deps):
        """Opposite of parse()."""
        return DependencyParser.compose_sep(deps, ' ')

    @staticmethod
    def restrict_versions(deps):
        """Not implemented."""
        return deps class MavenReleasesFetcher(ReleasesFetcher):
    """Releases fetcher for Maven."""

    def __init__(self, ecosystem):
        """Initialize instance."""
        super().__init__(ecosystem)

    def releases_from_maven_org(self, group_id, artifact_id):
        """Fetch releases versions for group_id/artifact_id."""
        metadata_filenames = ['maven-metadata.xml', 'maven-metadata-local.xml']

        group_id_path = group_id.replace('.', '/')
        versions = set()
        we_good = False
        for filename in metadata_filenames:

            url = urljoin(
                self.ecosystem.fetch_url,
                '{g}/{a}/{f}'.format(g=group_id_path, a=artifact_id, f=filename)
            )
            try:
                metadata_xml = etree.parse(urlopen(url))
                we_good = True  # We successfully downloaded at least one of the metadata files
                version_elements = metadata_xml.findall('.//version')
                versions = versions.union({x.text for x in version_elements})
            except OSError:
                # Not both XML files have to exist, so don't freak out yet
                pass

        if not we_good:
            logger.error('Unable to obtain a list of versions for {g}:{a}'.format(
                g=group_id, a=artifact_id)
            )

        return list(versions)

    def fetch_releases(self, package):
        """Fetch package releases versions."""
        if not package:
            raise ValueError("package not specified")
        try:
            group_id, artifact_id = package.split(':')
        except ValueError as exc:
            raise ValueError("Invalid Maven coordinates: {a}".format(a=package)) from exc

        return package, self.releases_from_maven_org(group_id, artifact_id) class NpmReleasesFetcher(ReleasesFetcher):
    """Releases fetcher for NPM."""

    def __init__(self, ecosystem):
        """Initialize instance."""
        super(NpmReleasesFetcher, self).__init__(ecosystem)

    def fetch_releases(self, package):
        """Fetch package releases versions.

        Example output from the NPM endpoint:
        {
            ...
            versions: {
               "0.1.0": {},
               "0.1.2": {}
               ...
            }
        }
        """
        if not package:
            raise ValueError("package")

        # quote '/' (but not '@') in scoped package name, e.g. in '@slicemenice/item-layouter'
        r = get(self.ecosystem.fetch_url + quote(package, safe='@'))

        if r.status_code == 200 and r.content:
            return package, list(r.json().get('versions', {}).keys())
        return package, [] class NugetReleasesFetcher(ReleasesFetcher):
    """Releases fetcher for Nuget."""

    def __init__(self, ecosystem):
        """Initialize instance."""
        super(NugetReleasesFetcher, self).__init__(ecosystem)

    def scrape_versions_from_nuget_org(self, package, sort_by_downloads=False):
        """Scrape 'Version History' from Nuget."""
        releases = []
        nuget_packages_url = 'https://www.nuget.org/packages/'
        page = get(nuget_packages_url + package)
        page = BeautifulSoup(page.text, 'html.parser')
        version_history = page.find(class_="version-history")
        for version in version_history.find_all(href=re.compile('/packages/')):
            version_text = version.text.replace('(current)', '').strip()
            try:
                semver_version.coerce(version_text)
                downloads = int(version.find_next('td').text.strip().replace(',', ''))
            except ValueError:
                pass
            else:
                releases.append((version_text, downloads))
        if sort_by_downloads:
            releases.sort(key=itemgetter(1))
        return package, [p[0] for p in reversed(releases)]

    def fetch_releases(self, package):
        """Fetch package releases versions."""
        if not package:
            raise ValueError("package not specified")

        # There's an API interface which lists available releases at
        # https://api.nuget.org/v3-flatcontainer/{package}/index.json
        # But it lists also unlisted/deprecated/shouldn't-be-used versions,
        # so we don't use it.

        return self.scrape_versions_from_nuget_org(package) class F8aReleasesFetcher(ReleasesFetcher):
    """Releases fetcher for internal database."""

    def __init__(self, ecosystem, database):
        """Initialize instance."""
        super(F8aReleasesFetcher, self).__init__(ecosystem)
        self.database = database

    def fetch_releases(self, package):
        """Fetch analysed versions for specific ecosystem + package from f8a."""
        query = self.database.query(Version).\
            join(Analysis).join(Package).join(Ecosystem).\
            filter(Package.name == package,
                   Ecosystem.name == self.ecosystem.name,
                   Analysis.finished_at.isnot(None))
        versions = {v.identifier for v in query}
        return package, list(sorted(versions, key=cmp_to_key(compare_version))) class GolangReleasesFetcher(ReleasesFetcher):
    """Releases fetcher for Golang."""

    def __init__(self, ecosystem):
        """Initialize instance."""
        super(GolangReleasesFetcher, self).__init__(ecosystem)

    def fetch_releases(self, package):
        """Fetch package releases versions."""
        if not package:
            raise ValueError('package not specified')

        parts = package.split("/")[:3]
        if len(parts) == 3:  # this assumes github.com/org/project like structure
            host, org, proj = parts
            repo_url = 'git://{host}/{org}/{proj}.git'.format(host=host, org=org, proj=proj)
        elif len(parts) == 2 and parts[0] == 'gopkg.in':  # specific to gopkg.in/packages
            host, proj = parts
            repo_url = 'https://{host}/{proj}.git'.format(host=host, proj=proj)
        else:
            raise ValueError("Package {} is invalid git repository".format(package))

        output = Git.ls_remote(repo_url, args=['-q'], refs=['HEAD'])
        version, ref = output[0].split()

        if not version:
            raise ValueError("Package {} does not have associated versions".format(package))

        return package, [version] class PypiReleasesFetcher(ReleasesFetcher):
    """Releases fetcher for Pypi."""

    def __init__(self, ecosystem):
        """Initialize instance."""
        super(PypiReleasesFetcher, self).__init__(ecosystem)

    def fetch_releases(self, package):
        """Fetch package releases versions.

        XML-RPC API Documentation: https://wiki.python.org/moin/PyPIXmlRpc
        Signature: package_releases(package_name, show_hidden=False)
        """
        if not package:
            raise ValueError("package")

        package = package.lower()

        pypi_package_url = urljoin(
            self.ecosystem.fetch_url, '{pkg_name}/json'.format(pkg_name=package)
        )

        response = requests.get(pypi_package_url)
        if response.status_code != 200:
            logger.error('Unable to obtain a list of versions for {pkg_name}'.format(
                pkg_name=package
            ))
            return package, []

        return package, list({x for x in response.json().get('releases', {})})
import pytest
import datetime
import flexmock
from f8a_worker.models import Analysis, Package, Version
from f8a_worker.solver import\
    (get_ecosystem_solver, Dependency,
     PypiDependencyParser, NpmDependencyParser, OSSIndexDependencyParser, NugetDependencyParser,
     GolangDependencyParser, MavenReleasesFetcher, NpmReleasesFetcher, NugetReleasesFetcher,
     F8aReleasesFetcher, GolangReleasesFetcher, PypiReleasesFetcher)
                       'six': '1.7.3',
                       'coverage': '3.5.3',
                       'pyasn1': '0.2.3',
                       'requests': '2.16.2',
                       'click': '0.7'}

    def test_nuget_solver(self, nuget):
        """Test NugetSolver."""
        solver = get_ecosystem_solver(nuget)
        deps = ['jQuery [1.4.4, 1.6)',
                'NUnit 3.2.1',
                'NETStandard.Library [1.6.0, )']
        out = solver.solve(deps)
        # nuget resolves to lowest version by default, see
        # https://docs.microsoft.com/en-us/nuget/release-notes/nuget-2.8#-dependencyversion-switch
        assert out == {'jQuery': '1.4.4',
                       'NUnit': '3.2.1',
                       'NETStandard.Library': '1.6.0'}

    @pytest.mark.parametrize('dependencies, expected', [
        ([], {}),
        (['github.com/msrb/mux'],
         {'github.com/msrb/mux': 'bdd5a5a1b0b489d297b73eb62b5f6328df198bfc'}),
        (['github.com/msrb/mux bdd5a5a1b0b489d297b73eb62b5f6328df198bfc'],
         {'github.com/msrb/mux': 'bdd5a5a1b0b489d297b73eb62b5f6328df198bfc'})
    ])
    def test_golang_solver(self, go, dependencies, expected):
        """Test GolangSolver."""
        solver = get_ecosystem_solver(go)
        solver_result = solver.solve(dependencies)
        assert len(solver_result) == len(dependencies)
        for name, version in solver_result.items():
            assert expected.get(name, '') == version, '"{}" "{}" "{}"'.format(
                name, version, expected)


class TestFetcher(object):
    """Tests for ReleasesFetcher subclasses."""

    @pytest.mark.parametrize('package, expected', [
        ('org.apache.flex.blazeds:flex-messaging-core',
         {'4.7.0', '4.7.1', '4.7.2', '4.7.3'})
    ])
    def test_maven_fetcher(self, maven, package, expected):
        """Test MavenReleasesFetcher."""
        f = MavenReleasesFetcher(maven)
        _, releases = f.fetch_releases(package)
        assert set(releases) >= expected

    @pytest.mark.parametrize('package, expected', [
        ('serve-static', {'1.7.1', '1.7.2', '1.13.2'}),
        ('@slicemenice/event-utils', {'1.0.0', '1.0.1', '1.1.0', '1.1.1'}),
        ('somereallydummynonexistentpackage', set())
    ])
    def test_npm_fetcher(self, npm, package, expected):
        """Test NpmReleasesFetcher."""
        f = NpmReleasesFetcher(npm)
        _, releases = f.fetch_releases(package)
        assert set(releases) >= expected

    @pytest.mark.parametrize('package, expected', [
        ('anymarkup', {
            '0.1.0', '0.1.1', '0.2.0', '0.3.0', '0.3.1', '0.4.0',
            '0.4.1', '0.4.2', '0.4.3', '0.5.0', '0.6.0', '0.7.0'
        }),
        ('somereallydummynonexistentpackage', set())
    ])
    def test_pypi_fetcher(self, pypi, package, expected):
        """Test NpmReleasesFetcher."""
        f = PypiReleasesFetcher(pypi)
        _, releases = f.fetch_releases(package)
        assert set(releases) >= expected

    @pytest.mark.parametrize('package, expected', [
        ('AjaxControlToolkit',
         {'4.1.60919', '7.1005.0', '17.1.1'}),
        ('Bootstrap',
         {'2.3.2', '3.3.6', '4.0.0-alpha6'}),
        ('log4net',
         {'1.2.10', '2.0.3', '2.0.8'}),
        ('Microsoft.AspNet.Mvc',
         {'5.0.0', '5.2.0', '5.2.3'}),
        ('NUnit',
         {'2.6.4', '3.0.0', '3.7.1'})
    ])
    def test_nuget_fetcher(self, nuget, package, expected):
        """Test NugetReleasesFetcher."""
        f = NugetReleasesFetcher(nuget)
        _, releases = f.fetch_releases(package)
        assert set(releases) >= expected

    @pytest.mark.parametrize('package, expected', [
        ('github.com/msrb/mux',
         {'bdd5a5a1b0b489d297b73eb62b5f6328df198bfc'}),
        ('github.com/jpopelka/flynn/bootstrap',
         {'2811174335f819551ec3de4f21ce75aa3e97be69'})
    ])
    def test_golang_fetcher(self, go, package, expected):
        """Test GolangReleasesFetcher."""
        f = GolangReleasesFetcher(go)
        _, releases = f.fetch_releases(package)
        assert set(releases) == expected

    def test_f8a_fetcher(self, rdb, npm):
        """Test F8aReleasesFetcher."""
        # create initial dataset
        package = Package(ecosystem=npm, name='f8a')
        rdb.add(package)
        rdb.commit()
        versions = {'0.5.0', '0.5.1', '0.6.0', '0.6.4', '0.7.0', '0.8.0', '0.9.0', '1.0.0', '1.0.5'}
        for v in versions:
            version = Version(package=package, identifier=v)
            rdb.add(version)
            rdb.commit()
            analysis = Analysis(version=version)
            # Fetcher only selects finished analyses
            analysis.finished_at = datetime.datetime.utcnow()
            rdb.add(analysis)
            rdb.commit()


        f = F8aReleasesFetcher(npm, rdb)