From bb0184c004d2c91c36affc298074981682b60915 Mon Sep 17 00:00:00 2001 From: Antoine Lambert Date: Wed, 20 Jan 2021 14:17:15 +0100 Subject: [PATCH] debian: Reimplement lister using new Lister API Port debian lister to `swh.lister.pattern.Lister` API. The new implementation will produce one instance of ListedOrigin model per package, notably containing the set of parameters expected by the debian loader. The lister is also stateful, meaning only new packages and those with new found versions since the last listing will be returned. Closes T2979 --- swh/lister/debian/__init__.py | 66 +-- swh/lister/debian/lister.py | 421 ++++++++++-------- swh/lister/debian/models.py | 230 ---------- swh/lister/debian/tasks.py | 6 +- swh/lister/debian/tests/conftest.py | 61 --- swh/lister/debian/tests/data/Sources_bullseye | 107 +++++ swh/lister/debian/tests/data/Sources_buster | 78 ++++ swh/lister/debian/tests/data/Sources_stretch | 113 +++++ ...n__dists_stretch_contrib_source_Sources.xz | Bin 44704 -> 0 bytes ...bian__dists_stretch_main_source_Sources.xz | Bin 3284 -> 0 bytes swh/lister/debian/tests/test_init.py | 92 ---- swh/lister/debian/tests/test_lister.py | 208 ++++++++- swh/lister/debian/tests/test_models.py | 32 -- swh/lister/debian/tests/test_tasks.py | 22 +- swh/lister/debian/utils.py | 83 ---- 15 files changed, 732 insertions(+), 787 deletions(-) delete mode 100644 swh/lister/debian/models.py delete mode 100644 swh/lister/debian/tests/conftest.py create mode 100644 swh/lister/debian/tests/data/Sources_bullseye create mode 100644 swh/lister/debian/tests/data/Sources_buster create mode 100644 swh/lister/debian/tests/data/Sources_stretch delete mode 100644 swh/lister/debian/tests/data/http_deb.debian.org/debian__dists_stretch_contrib_source_Sources.xz delete mode 100644 swh/lister/debian/tests/data/http_deb.debian.org/debian__dists_stretch_main_source_Sources.xz delete mode 100644 swh/lister/debian/tests/test_init.py delete mode 100644 swh/lister/debian/tests/test_models.py delete mode 100644 swh/lister/debian/utils.py diff --git a/swh/lister/debian/__init__.py b/swh/lister/debian/__init__.py index d121a86..0d483d9 100644 --- a/swh/lister/debian/__init__.py +++ b/swh/lister/debian/__init__.py @@ -1,76 +1,16 @@ -# Copyright (C) 2019 The Software Heritage developers +# Copyright (C) 2019-2021 The Software Heritage developers # See the AUTHORS file at the top-level directory of this distribution # License: GNU General Public License version 3, or any later version # See top-level LICENSE file for more information -import logging -from typing import Any, List, Mapping - -logger = logging.getLogger(__name__) - - -def debian_init( - db_engine, - override_conf: Mapping[str, Any] = {}, - distribution_name: str = "Debian", - suites: List[str] = ["stretch", "buster", "bullseye"], - components: List[str] = ["main", "contrib", "non-free"], -): - """Initialize the debian data model. - - Args: - db_engine: SQLAlchemy manipulation database object - override_conf: Override conf to pass to instantiate a lister - distribution_name: Distribution to initialize - suites: Default suites to register with the lister - components: Default components to register per suite - - """ - from sqlalchemy.orm import sessionmaker - - from swh.lister.debian.models import Area, Distribution - - db_session = sessionmaker(bind=db_engine)() - distrib = ( - db_session.query(Distribution) - .filter(Distribution.name == distribution_name) - .one_or_none() - ) - - if distrib is None: - distrib = Distribution( - name=distribution_name, - type="deb", - mirror_uri="http://deb.debian.org/debian/", - ) - db_session.add(distrib) - - # Check the existing - existing_area = db_session.query(Area).filter(Area.distribution == distrib).all() - existing_area = set([a.name for a in existing_area]) - - logger.debug("Area already known: %s", ", ".join(existing_area)) - - # Create only the new ones - for suite in suites: - for component in components: - area_name = f"{suite}/{component}" - if area_name in existing_area: - logger.debug("Area '%s' already set, skipping", area_name) - continue - area = Area(name=area_name, distribution=distrib) - db_session.add(area) - - db_session.commit() - db_session.close() +from typing import Any, Mapping def register() -> Mapping[str, Any]: from .lister import DebianLister return { - "models": [DebianLister.MODEL], + "models": [], "lister": DebianLister, "task_modules": ["%s.tasks" % __name__], - "init": debian_init, } diff --git a/swh/lister/debian/lister.py b/swh/lister/debian/lister.py index 758c656..f62183d 100644 --- a/swh/lister/debian/lister.py +++ b/swh/lister/debian/lister.py @@ -1,260 +1,287 @@ -# Copyright (C) 2017-2019 The Software Heritage developers +# Copyright (C) 2017-2021 The Software Heritage developers # See the AUTHORS file at the top-level directory of this distribution # License: GNU General Public License version 3, or any later version # See top-level LICENSE file for more information + import bz2 from collections import defaultdict -import datetime +from dataclasses import dataclass, field import gzip +from itertools import product import logging import lzma -from typing import Any, Dict, Mapping, Optional +from typing import Any, Callable, Dict, Iterator, List, Set, Tuple +from urllib.parse import urljoin from debian.deb822 import Sources -from requests import Response -from sqlalchemy.orm import joinedload, load_only -from sqlalchemy.schema import CreateTable, DropTable +import requests -from swh.lister.core.lister_base import FetchError, ListerBase -from swh.lister.core.lister_transports import ListerHttpTransport -from swh.lister.debian.models import ( - AreaSnapshot, - Distribution, - DistributionSnapshot, - Package, - TempPackage, -) +from swh.scheduler.interface import SchedulerInterface +from swh.scheduler.model import ListedOrigin -decompressors = { +from .. import USER_AGENT +from ..pattern import Lister + +logger = logging.getLogger(__name__) + +decompressors: Dict[str, Callable[[Any], Any]] = { "gz": lambda f: gzip.GzipFile(fileobj=f), "bz2": bz2.BZ2File, "xz": lzma.LZMAFile, } - -logger = logging.getLogger(__name__) +Suite = str +Component = str +PkgName = str +PkgVersion = str +DebianOrigin = str +DebianPageType = Iterator[Sources] -class DebianLister(ListerHttpTransport, ListerBase): - MODEL = Package - PATH_TEMPLATE = None +@dataclass +class DebianListerState: + """State of debian lister""" + + package_versions: Dict[PkgName, Set[PkgVersion]] = field(default_factory=dict) + """Dictionary mapping a package name to all the versions found during + last listing""" + + +class DebianLister(Lister[DebianListerState, DebianPageType]): + """ + List source packages for a given debian or derivative distribution. + + The lister will create a snapshot for each package name from all its + available versions. + + If a package snapshot is different from the last listing operation, + it will be send to the scheduler that will create a loading task + to archive newly found source code. + + Args: + scheduler: instance of SchedulerInterface + distribution: identifier of listed distribution (e.g. Debian, Ubuntu) + mirror_url: debian package archives mirror URL + suites: list of distribution suites to process + components: list of package components to process + """ + LISTER_NAME = "debian" - instance = "debian" def __init__( self, + scheduler: SchedulerInterface, distribution: str = "Debian", - date: Optional[datetime.datetime] = None, - override_config: Mapping = {}, + mirror_url: str = "http://deb.debian.org/debian/", + suites: List[Suite] = ["stretch", "buster", "bullseye"], + components: List[Component] = ["main", "contrib", "non-free"], ): - """Initialize the debian lister for a given distribution at a given - date. - - Args: - distribution: name of the distribution (e.g. "Debian") - date: date the snapshot is taken (defaults to now if empty) - override_config: Override configuration (which takes precedence - over the parameters if provided) - - """ - ListerHttpTransport.__init__(self, url="notused") - ListerBase.__init__(self, override_config=override_config) - self.distribution = override_config.get("distribution", distribution) - self.date = override_config.get("date", date) or datetime.datetime.now( - tz=datetime.timezone.utc + super().__init__( + scheduler=scheduler, url=mirror_url, instance=distribution, ) - def transport_request(self, identifier) -> Response: - """Subvert ListerHttpTransport.transport_request, to try several - index URIs in turn. + # to ensure urljoin will produce valid Sources URL + if not self.url.endswith("/"): + self.url += "/" - The Debian repository format supports several compression algorithms - across the ages, so we try several URIs. + self.distribution = distribution + self.suites = suites + self.components = components - Once we have found a working URI, we break and set `self.decompressor` - to the one that matched. + self.session = requests.Session() + self.session.headers.update({"User-Agent": USER_AGENT}) - Returns: - a requests Response object. + # will hold all listed origins info + self.listed_origins: Dict[DebianOrigin, ListedOrigin] = {} + # will contain origin urls that have already been listed + # in a previous page + self.sent_origins: Set[DebianOrigin] = set() + # will contain already listed package info that need to be sent + # to the scheduler for update in the commit_page method + self.origins_to_update: Dict[DebianOrigin, ListedOrigin] = {} + # will contain the lister state after a call to run + self.package_versions: Dict[PkgName, Set[PkgVersion]] = {} - Raises: - FetchError: when all the URIs failed to be retrieved. - """ - response = None - compression = None + def state_from_dict(self, d: Dict[str, Any]) -> DebianListerState: + return DebianListerState(package_versions={k: set(v) for k, v in d.items()}) - for uri, compression in self.area.index_uris(): - response = super().transport_request(uri) + def state_to_dict(self, state: DebianListerState) -> Dict[str, Any]: + return {k: list(v) for k, v in state.package_versions.items()} + + def debian_index_urls( + self, suite: Suite, component: Component + ) -> Iterator[Tuple[str, str]]: + """Return an iterator on possible Sources file URLs as multiple compression + formats can be used.""" + compression_exts = ("xz", "bz2", "gz") + base_url = urljoin(self.url, f"dists/{suite}/{component}/source/Sources") + for ext in compression_exts: + yield (f"{base_url}.{ext}", ext) + yield (base_url, "") + + def page_request(self, suite: Suite, component: Component) -> DebianPageType: + """Return parsed package Sources file for a given debian suite and component.""" + for url, compression in self.debian_index_urls(suite, component): + response = requests.get(url, stream=True) + logging.debug("Fetched URL: %s, status code: %s", url, response.status_code) if response.status_code == 200: break else: - raise FetchError("Could not retrieve index for %s" % self.area) - self.decompressor = decompressors.get(compression) - return response + raise Exception( + "Could not retrieve sources index for %s/%s", suite, component + ) - def request_uri(self, identifier): - # In the overridden transport_request, we pass - # ListerBase.transport_request() the full URI as identifier, so we - # need to return it here. - return identifier - - def request_params(self, identifier) -> Dict[str, Any]: - # Enable streaming to allow wrapping the response in the decompressor - # in transport_response_simplified. - params = super().request_params(identifier) - params["stream"] = True - return params - - def transport_response_simplified(self, response): - """Decompress and parse the package index fetched in `transport_request`. - - For each package, we "pivot" the file list entries (Files, - Checksums-Sha1, Checksums-Sha256), to return a files dict mapping - filenames to their checksums. - """ - if self.decompressor: - data = self.decompressor(response.raw) + decompressor = decompressors.get(compression) + if decompressor: + data = decompressor(response.raw) else: data = response.raw - for src_pkg in Sources.iter_paragraphs(data.readlines()): - files = defaultdict(dict) + return Sources.iter_paragraphs(data.readlines()) - for field in src_pkg._multivalued_fields: - if field.startswith("checksums-"): - sum_name = field[len("checksums-") :] + def get_pages(self) -> Iterator[DebianPageType]: + """Return an iterator on parsed debian package Sources files, one per combination + of debian suite and component.""" + for suite, component in product(self.suites, self.components): + logger.debug( + "Processing %s %s source packages info for %s component.", + self.instance, + suite, + component, + ) + self.current_suite = suite + self.current_component = component + yield self.page_request(suite, component) + + def origin_url_for_package(self, package_name: PkgName) -> DebianOrigin: + """Return the origin url for the given package""" + return f"deb://{self.instance}/packages/{package_name}" + + def get_origins_from_page(self, page: DebianPageType) -> Iterator[ListedOrigin]: + """Convert a page of debian package sources into an iterator of ListedOrigin. + + Please note that the returned origins correspond to packages only + listed for the first time in order to get an accurate origins counter + in the statistics returned by the run method of the lister. + + Packages already listed in another page but with different versions will + be put in cache by the method and updated ListedOrigin objects will + be sent to the scheduler later in the commit_page method. + + Indeed as multiple debian suites can be processed, a similar set of + package names can be listed for two different package source pages, + only their version will differ, resulting in origins counted multiple + times in lister statistics. + """ + assert self.lister_obj.id is not None + + origins_to_send = {} + self.origins_to_update = {} + + # iterate on each package source info + for src_pkg in page: + # gather package files info that will be used by the debian loader + files: Dict[str, Dict[str, Any]] = defaultdict(dict) + for field_ in src_pkg._multivalued_fields: + if field_.startswith("checksums-"): + sum_name = field_[len("checksums-") :] else: sum_name = "md5sum" - if field in src_pkg: - for entry in src_pkg[field]: + if field_ in src_pkg: + for entry in src_pkg[field_]: name = entry["name"] files[name]["name"] = entry["name"] files[name]["size"] = int(entry["size"], 10) files[name][sum_name] = entry[sum_name] - yield { - "name": src_pkg["Package"], - "version": src_pkg["Version"], - "directory": src_pkg["Directory"], - "files": files, - } + # extract package name and version + package_name = src_pkg["Package"] + package_version = src_pkg["Version"] + # build origin url + origin_url = self.origin_url_for_package(package_name) - def inject_repo_data_into_db(self, models_list): - """Generate the Package entries that didn't previously exist. - - Contrary to ListerBase, we don't actually insert the data in - database. `schedule_missing_tasks` does it once we have the - origin and task identifiers. - """ - by_name_version = {} - temp_packages = [] - - area_id = self.area.id - - for model in models_list: - name = model["name"] - version = model["version"] - temp_packages.append( - {"area_id": area_id, "name": name, "version": version,} - ) - by_name_version[name, version] = model - - # Add all the listed packages to a temporary table - self.db_session.execute(CreateTable(TempPackage.__table__)) - self.db_session.bulk_insert_mappings(TempPackage, temp_packages) - - def exists_tmp_pkg(db_session, model): - return ( - db_session.query(model) - .filter(Package.area_id == TempPackage.area_id) - .filter(Package.name == TempPackage.name) - .filter(Package.version == TempPackage.version) - .exists() + # create package version key as expected by the debian loader + package_version_key = ( + f"{self.current_suite}/{self.current_component}/{package_version}" ) - # Filter out the packages that already exist in the main Package table - new_packages = ( - self.db_session.query(TempPackage) - .options(load_only("name", "version")) - .filter(~exists_tmp_pkg(self.db_session, Package)) - .all() - ) + # this is the first time a package is listed + if origin_url not in self.listed_origins: + # create a ListedOrigin object for it that can be later + # updated with new package versions info + self.listed_origins[origin_url] = ListedOrigin( + lister_id=self.lister_obj.id, + url=origin_url, + visit_type="deb", + extra_loader_arguments={"date": None, "packages": {}}, + ) + # origin will be yielded at the end of that method + origins_to_send[origin_url] = self.listed_origins[origin_url] + # init set that will contain all listed package versions + self.package_versions[package_name] = set() - self.old_area_packages = ( - self.db_session.query(Package) - .filter(exists_tmp_pkg(self.db_session, TempPackage)) - .all() - ) + # package has already been listed in a previous page or current page + elif origin_url not in origins_to_send: + # if package has been listed in a previous page, its new versions + # will be added to its ListedOrigin object but the update will + # be sent to the scheduler in the commit_page method + self.origins_to_update[origin_url] = self.listed_origins[origin_url] - self.db_session.execute(DropTable(TempPackage.__table__)) + # update package versions data in parameter that will be provided + # to the debian loader + self.listed_origins[origin_url].extra_loader_arguments["packages"].update( + { + package_version_key: { + "name": package_name, + "version": package_version, + "files": files, + } + } + ) - added_packages = [] - for package in new_packages: - model = by_name_version[package.name, package.version] + # add package version key to the set of found versions + self.package_versions[package_name].add(package_version_key) - added_packages.append(Package(area=self.area, **model)) + # package has already been listed during a previous listing process + if package_name in self.state.package_versions: + new_versions = ( + self.package_versions[package_name] + - self.state.package_versions[package_name] + ) + # no new versions so far, no need to send the origin to the scheduler + if not new_versions: + origins_to_send.pop(origin_url, None) + self.origins_to_update.pop(origin_url, None) + # new versions found, ensure the origin will be sent to the scheduler + elif origin_url not in self.sent_origins: + self.origins_to_update.pop(origin_url, None) + origins_to_send[origin_url] = self.listed_origins[origin_url] - self.db_session.add_all(added_packages) - return added_packages - - def schedule_missing_tasks(self, models_list, added_packages): - """We create tasks at the end of the full snapshot processing""" - return - - def create_tasks_for_snapshot(self, snapshot): - tasks = [ - snapshot.task_for_package(name, versions) - for name, versions in snapshot.get_packages().items() - ] - - return self.scheduler.create_tasks(tasks) - - def run(self): - """Run the lister for a given (distribution, area) tuple. - - """ - distribution = ( - self.db_session.query(Distribution) - .options(joinedload(Distribution.areas)) - .filter(Distribution.name == self.distribution) - .one_or_none() - ) - - if not distribution: - logger.error("Distribution %s is not registered" % self.distribution) - return {"status": "failed"} - - if not distribution.type == "deb": - logger.error("Distribution %s is not a Debian derivative" % distribution) - return {"status": "failed"} - - date = self.date + # update already counted origins with changes since last page + self.sent_origins.update(origins_to_send.keys()) logger.debug( - "Creating snapshot for distribution %s on date %s" % (distribution, date) + "Found %s new packages, %s packages with new versions.", + len(origins_to_send), + len(self.origins_to_update), + ) + logger.debug( + "Current total number of listed packages is equal to %s.", + len(self.listed_origins), ) - snapshot = DistributionSnapshot(date=date, distribution=distribution) + yield from origins_to_send.values() - self.db_session.add(snapshot) + def get_origins_to_update(self) -> Iterator[ListedOrigin]: + yield from self.origins_to_update.values() - for area in distribution.areas: - if not area.active: - continue + def commit_page(self, page: DebianPageType): + """Send to scheduler already listed origins where new versions have been found + in current page.""" + self.send_origins(self.get_origins_to_update()) - self.area = area - - logger.debug("Processing area %s" % area) - - _, new_area_packages = self.ingest_data(None) - area_snapshot = AreaSnapshot(snapshot=snapshot, area=area) - self.db_session.add(area_snapshot) - area_snapshot.packages.extend(new_area_packages) - area_snapshot.packages.extend(self.old_area_packages) - - self.create_tasks_for_snapshot(snapshot) - - self.db_session.commit() - - return {"status": "eventful"} + def finalize(self): + # set mapping between listed package names and versions as lister state + self.state.package_versions = self.package_versions + self.updated = len(self.sent_origins) > 0 diff --git a/swh/lister/debian/models.py b/swh/lister/debian/models.py deleted file mode 100644 index e1ffe39..0000000 --- a/swh/lister/debian/models.py +++ /dev/null @@ -1,230 +0,0 @@ -# Copyright (C) 2017-2019 The Software Heritage developers -# See the AUTHORS file at the top-level directory of this distribution -# License: GNU General Public License version 3, or any later version -# See top-level LICENSE file for more information - -import binascii -from collections import defaultdict -import datetime -from typing import Any, Mapping - -from sqlalchemy import ( - Boolean, - Column, - DateTime, - Enum, - ForeignKey, - Integer, - LargeBinary, - String, - Table, - UniqueConstraint, -) - -try: - from sqlalchemy import JSON -except ImportError: - # SQLAlchemy < 1.1 - from sqlalchemy.dialects.postgresql import JSONB as JSON - -from sqlalchemy.orm import relationship - -from swh.lister.core.models import SQLBase - - -class Distribution(SQLBase): - """A distribution (e.g. Debian, Ubuntu, Fedora, ...)""" - - __tablename__ = "distribution" - - id = Column(Integer, primary_key=True) - name = Column(String, unique=True, nullable=False) - type = Column(Enum("deb", "rpm", name="distribution_types"), nullable=False) - mirror_uri = Column(String, nullable=False) - - areas = relationship("Area", back_populates="distribution") - - def origin_for_package(self, package_name: str) -> str: - """Return the origin url for the given package - - """ - return "%s://%s/packages/%s" % (self.type, self.name, package_name) - - def __repr__(self): - return "Distribution(%s (%s) on %s)" % (self.name, self.type, self.mirror_uri,) - - -class Area(SQLBase): - __tablename__ = "area" - __table_args__ = (UniqueConstraint("distribution_id", "name"),) - - id = Column(Integer, primary_key=True) - distribution_id = Column(Integer, ForeignKey("distribution.id"), nullable=False) - name = Column(String, nullable=False) - active = Column(Boolean, nullable=False, default=True) - - distribution = relationship("Distribution", back_populates="areas") - - def index_uris(self): - """Get possible URIs for this component's package index""" - if self.distribution.type == "deb": - compression_exts = ("xz", "bz2", "gz", None) - base_uri = "%s/dists/%s/source/Sources" % ( - self.distribution.mirror_uri, - self.name, - ) - for ext in compression_exts: - if ext: - yield (base_uri + "." + ext, ext) - else: - yield (base_uri, None) - else: - raise NotImplementedError( - "Do not know how to build index URI for Distribution type %s" - % self.distribution.type - ) - - def __repr__(self): - return "Area(%s of %s)" % (self.name, self.distribution.name,) - - -class Package(SQLBase): - __tablename__ = "package" - __table_args__ = (UniqueConstraint("area_id", "name", "version"),) - - id = Column(Integer, primary_key=True) - area_id = Column(Integer, ForeignKey("area.id"), nullable=False) - name = Column(String, nullable=False) - version = Column(String, nullable=False) - directory = Column(String, nullable=False) - files = Column(JSON, nullable=False) - - origin_id = Column(Integer) - task_id = Column(Integer) - - revision_id = Column(LargeBinary(20)) - - area = relationship("Area") - - @property - def distribution(self): - return self.area.distribution - - def fetch_uri(self, filename): - """Get the URI to fetch the `filename` file associated with the - package""" - if self.distribution.type == "deb": - return "%s/%s/%s" % ( - self.distribution.mirror_uri, - self.directory, - filename, - ) - else: - raise NotImplementedError( - "Do not know how to build fetch URI for Distribution type %s" - % self.distribution.type - ) - - def loader_dict(self): - ret = { - "id": self.id, - "name": self.name, - "version": self.version, - } - if self.revision_id: - ret["revision_id"] = binascii.hexlify(self.revision_id).decode() - else: - files = {name: checksums.copy() for name, checksums in self.files.items()} - for name in files: - files[name]["uri"] = self.fetch_uri(name) - - ret.update( - {"revision_id": None, "files": files,} - ) - return ret - - def __repr__(self): - return "Package(%s_%s of %s %s)" % ( - self.name, - self.version, - self.distribution.name, - self.area.name, - ) - - -class DistributionSnapshot(SQLBase): - __tablename__ = "distribution_snapshot" - - id = Column(Integer, primary_key=True) - date = Column(DateTime, nullable=False, index=True) - distribution_id = Column(Integer, ForeignKey("distribution.id"), nullable=False) - - distribution = relationship("Distribution") - areas = relationship("AreaSnapshot", back_populates="snapshot") - - def task_for_package( - self, package_name: str, package_versions: Mapping - ) -> Mapping[str, Any]: - """Return the task dictionary for the given list of package versions - - """ - origin_url = self.distribution.origin_for_package(package_name) - - return { - "policy": "oneshot", - "type": "load-%s-package" % self.distribution.type, - "next_run": datetime.datetime.now(tz=datetime.timezone.utc), - "arguments": { - "args": [], - "kwargs": { - "url": origin_url, - "date": self.date.isoformat(), - "packages": package_versions, - }, - }, - "retries_left": 3, - } - - def get_packages(self): - packages = defaultdict(dict) - for area_snapshot in self.areas: - area_name = area_snapshot.area.name - for package in area_snapshot.packages: - ref_name = "%s/%s" % (area_name, package.version) - packages[package.name][ref_name] = package.loader_dict() - - return packages - - -area_snapshot_package_assoc = Table( - "area_snapshot_package", - SQLBase.metadata, - Column("area_snapshot_id", Integer, ForeignKey("area_snapshot.id"), nullable=False), - Column("package_id", Integer, ForeignKey("package.id"), nullable=False), -) - - -class AreaSnapshot(SQLBase): - __tablename__ = "area_snapshot" - - id = Column(Integer, primary_key=True) - snapshot_id = Column( - Integer, ForeignKey("distribution_snapshot.id"), nullable=False - ) - area_id = Column(Integer, ForeignKey("area.id"), nullable=False) - - snapshot = relationship("DistributionSnapshot", back_populates="areas") - area = relationship("Area") - packages = relationship("Package", secondary=area_snapshot_package_assoc) - - -class TempPackage(SQLBase): - __tablename__ = "temp_package" - __table_args__ = { - "prefixes": ["TEMPORARY"], - } - - id = Column(Integer, primary_key=True) - area_id = Column(Integer) - name = Column(String) - version = Column(String) diff --git a/swh/lister/debian/tasks.py b/swh/lister/debian/tasks.py index 3099e61..fe62a78 100644 --- a/swh/lister/debian/tasks.py +++ b/swh/lister/debian/tasks.py @@ -1,4 +1,4 @@ -# Copyright (C) 2017-2018 the Software Heritage developers +# Copyright (C) 2017-2021 the Software Heritage developers # License: GNU General Public License version 3, or any later version # See top-level LICENSE file for more information @@ -8,9 +8,9 @@ from .lister import DebianLister @shared_task(name=__name__ + ".DebianListerTask") -def list_debian_distribution(distribution, **lister_args): +def list_debian_distribution(**lister_args): """List a Debian distribution""" - return DebianLister(distribution=distribution, **lister_args).run() + return DebianLister.from_configfile(**lister_args).run().dict() @shared_task(name=__name__ + ".ping") diff --git a/swh/lister/debian/tests/conftest.py b/swh/lister/debian/tests/conftest.py deleted file mode 100644 index 865e9e1..0000000 --- a/swh/lister/debian/tests/conftest.py +++ /dev/null @@ -1,61 +0,0 @@ -# Copyright (C) 2019-2020 The Software Heritage developers -# See the AUTHORS file at the top-level directory of this distribution -# License: GNU General Public License version 3, or any later version -# See top-level LICENSE file for more information - -import os - -import pytest -from sqlalchemy.orm import sessionmaker - -from swh.core.db.pytest_plugin import postgresql_fact - -from swh.lister.debian import debian_init -import swh.scheduler - -SQL_DIR = os.path.join(os.path.dirname(swh.scheduler.__file__), "sql") -postgresql_scheduler = postgresql_fact( - "postgresql_proc", - db_name="scheduler-lister", - dump_files=os.path.join(SQL_DIR, "*.sql"), - # do not truncate the task tables, it's required in between test - no_truncate_tables={"dbversion", "priority_ratio", "task"}, -) - - -@pytest.fixture -def swh_scheduler_config(postgresql_scheduler): - return {"db": postgresql_scheduler.dsn} - - -@pytest.fixture -def lister_under_test(): - return "debian" - - -@pytest.fixture -def lister_debian(swh_lister): - # Initialize the debian data model - debian_init( - swh_lister.db_engine, suites=["stretch"], components=["main", "contrib"] - ) - - # Add the load-deb-package in the scheduler backend - swh_lister.scheduler.create_task_type( - { - "type": "load-deb-package", - "description": "Load a Debian package", - "backend_name": "swh.loader.packages.debian.tasks.LoaderDebianPackage", - "default_interval": "1 day", - } - ) - - return swh_lister - - -@pytest.fixture -def session(lister_db_url, engine): - session = sessionmaker(bind=engine)() - yield session - session.close() - engine.dispose() diff --git a/swh/lister/debian/tests/data/Sources_bullseye b/swh/lister/debian/tests/data/Sources_bullseye new file mode 100644 index 0000000..bcec67a --- /dev/null +++ b/swh/lister/debian/tests/data/Sources_bullseye @@ -0,0 +1,107 @@ +Package: git +Binary: git, git-man, git-doc, git-cvs, git-svn, git-mediawiki, git-email, git-daemon-run, git-daemon-sysvinit, git-gui, gitk, git-el, gitweb, git-all +Version: 1:2.29.2-1 +Maintainer: Jonathan Nieder +Uploaders: Anders Kaseorg +Build-Depends: libz-dev, gettext, libpcre2-dev | libpcre3-dev, libcurl4-gnutls-dev, libexpat1-dev, subversion, libsvn-perl, libyaml-perl, tcl, python3, libhttp-date-perl | libtime-parsedate-perl, libcgi-pm-perl, liberror-perl, libmailtools-perl, cvs, cvsps, libdbd-sqlite3-perl, unzip, libio-pty-perl, debhelper-compat (= 10), dh-exec (>= 0.7), dh-apache2, dpkg-dev (>= 1.16.2~) +Build-Depends-Indep: asciidoc (>= 8.6.10), xmlto, docbook-xsl +Architecture: any all +Standards-Version: 4.3.0.1 +Format: 3.0 (quilt) +Files: + ef246c390b2673819cd55085984fb6bc 2867 git_2.29.2-1.dsc + f5f9d4e7a3c633bc7a9178cfd822045f 6187988 git_2.29.2.orig.tar.xz + cfed1fd3dffd4fb31a0319e51471877f 663292 git_2.29.2-1.debian.tar.xz +Vcs-Browser: https://repo.or.cz/w/git/debian.git/ +Vcs-Git: https://repo.or.cz/r/git/debian.git/ +Checksums-Sha256: + 9f2203314f0d076e24750fa29f38d1bb49d4124f3e8d8789b751c84473e57ead 2867 git_2.29.2-1.dsc + f2fc436ebe657821a1360bcd1e5f4896049610082419143d60f6fa13c2f607c1 6187988 git_2.29.2.orig.tar.xz + ad79671893257ca6205156c7c58d06e265d793f076c0efc8e225e832217f760a 663292 git_2.29.2-1.debian.tar.xz +Homepage: https://git-scm.com/ +Package-List: + git deb vcs optional arch=any + git-all deb vcs optional arch=all + git-cvs deb vcs optional arch=all + git-daemon-run deb vcs optional arch=all + git-daemon-sysvinit deb vcs optional arch=all + git-doc deb doc optional arch=all + git-el deb vcs optional arch=all + git-email deb vcs optional arch=all + git-gui deb vcs optional arch=all + git-man deb doc optional arch=all + git-mediawiki deb vcs optional arch=all + git-svn deb vcs optional arch=all + gitk deb vcs optional arch=all + gitweb deb vcs optional arch=all +Directory: pool/main/g/git +Priority: source +Section: vcs + +Package: subversion +Binary: subversion, libsvn1, libsvn-dev, libsvn-doc, libapache2-mod-svn, python3-subversion, subversion-tools, libsvn-java, libsvn-perl, ruby-svn +Version: 1.14.0-3 +Maintainer: James McCoy +Build-Depends: autoconf, bash-completion, chrpath, debhelper-compat (= 12), default-jdk-headless (>= 2:1.8) [!hurd-i386 !hppa !sparc] , dh-apache2, dh-python, doxygen, junit4 [!hurd-i386 !hppa !sparc] , libapr1-dev, libaprutil1-dev, libdb5.3-dev, libdbus-1-dev, liblz4-dev (>= 0.0~r129), libkf5coreaddons-dev , libkf5i18n-dev , libkf5wallet-dev , libperl-dev, libsasl2-dev, libsecret-1-dev, libserf-dev (>= 1.3.9-4~), libsqlite3-dev (>= 3.8.7), libtool, libutf8proc-dev, perl, py3c-dev, python3-all-dev, rename, ruby , ruby-dev , swig (>= 3.0.10), zlib1g-dev +Build-Conflicts: libsvn-dev (>= 1.15~), libsvn-dev (<< 1.14~), libsvn1 (>= 1.15~), libsvn1 (<< 1.14~) +Architecture: any all +Standards-Version: 4.5.0 +Format: 3.0 (quilt) +Files: + 65f7c225ddbcc855b57341954268098b 3807 subversion_1.14.0-3.dsc + 0136e67d8f58731b2858b9f2dba7c536 11519871 subversion_1.14.0.orig.tar.gz + f68b938ba71e19f333069bfd3c6ec236 3917 subversion_1.14.0.orig.tar.gz.asc + de6248e80a7f8b6481606ff16a9e9237 427396 subversion_1.14.0-3.debian.tar.xz +Vcs-Browser: https://salsa.debian.org/jamessan/subversion +Vcs-Git: https://salsa.debian.org/jamessan/subversion.git +Checksums-Sha256: + ebe6e2417a79ad5254072d994ccf6313489a90f299304ee2ccfb6ebe1392c580 3807 subversion_1.14.0-3.dsc + ef3d1147535e41874c304fb5b9ea32745fbf5d7faecf2ce21d4115b567e937d0 11519871 subversion_1.14.0.orig.tar.gz + 98333df38d29a64500d4ad1693741d3d087485555207289b4e53af309abac71a 3917 subversion_1.14.0.orig.tar.gz.asc + fd5383bf82ccf89acd7caf0fd80dc01ee2f7a3e163dcab6b2646ad01b7b746d9 427396 subversion_1.14.0-3.debian.tar.xz +Homepage: http://subversion.apache.org/ +Dgit: 6ef306f777223c0d5c2eaab0586420ada61435f3 debian archive/debian/1.14.0-3 https://git.dgit.debian.org/subversion +Package-List: + libapache2-mod-svn deb httpd optional arch=any + libsvn-dev deb libdevel optional arch=any + libsvn-doc deb doc optional arch=all + libsvn-java deb java optional arch=any profile=!pkg.subversion.nojava + libsvn-perl deb perl optional arch=any + libsvn1 deb libs optional arch=any + python3-subversion deb python optional arch=any + ruby-svn deb ruby optional arch=any profile=!pkg.subversion.noruby + subversion deb vcs optional arch=any + subversion-tools deb vcs optional arch=any +Testsuite: autopkgtest +Testsuite-Triggers: apache2, wget +Directory: pool/main/s/subversion +Priority: source +Section: vcs + +Package: hg-git +Binary: mercurial-git +Version: 0.9.0-2 +Maintainer: Debian Python Team +Uploaders: Tristan Seligmann +Build-Depends: debhelper-compat (= 13), dh-python, git, python3-mercurial, openssh-client, python3, python3-dulwich (>= 0.20.6), python3-setuptools, unzip +Architecture: all +Standards-Version: 4.5.0 +Format: 3.0 (quilt) +Files: + 7dee1b877cf129c1f6ee618ebf690179 2090 hg-git_0.9.0-2.dsc + bcf30d513d8463332288aa93c1c67d3e 129138 hg-git_0.9.0.orig.tar.bz2 + 5674d6e2e8271150adf68b08833e4806 6996 hg-git_0.9.0-2.debian.tar.xz +Vcs-Browser: https://salsa.debian.org/python-team/packages/hg-git +Vcs-Git: https://salsa.debian.org/python-team/packages/hg-git.git +Checksums-Sha256: + a40beaef731c00a820d89918afedc1f01580d87f6e8c29e74903b1e108e38b27 2090 hg-git_0.9.0-2.dsc + eedd8773de76b21b47fd21a7e5c04c05c7ab0ecfc62a54bc947eb225b2c44424 129138 hg-git_0.9.0.orig.tar.bz2 + ded524f1688a248a0eefbd0cf9843daedf60001cc39bfbb9e89734742fa4a4d2 6996 hg-git_0.9.0-2.debian.tar.xz +Homepage: https://hg-git.github.io/ +Package-List: + mercurial-git deb vcs optional arch=all +Testsuite: autopkgtest +Testsuite-Triggers: git, openssh-client, unzip +Directory: pool/main/h/hg-git +Priority: source +Section: vcs diff --git a/swh/lister/debian/tests/data/Sources_buster b/swh/lister/debian/tests/data/Sources_buster new file mode 100644 index 0000000..7e96ba2 --- /dev/null +++ b/swh/lister/debian/tests/data/Sources_buster @@ -0,0 +1,78 @@ +Package: git +Binary: git, git-man, git-doc, git-cvs, git-svn, git-mediawiki, git-email, git-daemon-run, git-daemon-sysvinit, git-gui, gitk, git-el, gitweb, git-all +Version: 1:2.20.1-2+deb10u3 +Maintainer: Gerrit Pape +Uploaders: Jonathan Nieder , Anders Kaseorg +Build-Depends: libz-dev, gettext, libpcre2-dev | libpcre3-dev, libcurl4-gnutls-dev, libexpat1-dev, subversion, libsvn-perl, libyaml-perl, tcl, python, libhttp-date-perl | libtime-parsedate-perl, libcgi-pm-perl, liberror-perl, libmailtools-perl, cvs, cvsps, libdbd-sqlite3-perl, unzip, libio-pty-perl, debhelper (>= 9), dh-exec (>= 0.7), dh-apache2, dpkg-dev (>= 1.16.2~) +Build-Depends-Indep: asciidoc (>= 8.6.10), xmlto, docbook-xsl +Architecture: any all +Standards-Version: 4.3.0.1 +Format: 3.0 (quilt) +Files: + fcfb1e01b74dfa383f8171ae7d331de9 2923 git_2.20.1-2+deb10u3.dsc + 5fb4ff92b56ce3172b99c1c74c046c1a 5359872 git_2.20.1.orig.tar.xz + 3b629f9b0d2da6fa6ce5816478a57e09 646216 git_2.20.1-2+deb10u3.debian.tar.xz +Vcs-Browser: https://repo.or.cz/w/git/debian.git/ +Vcs-Git: https://repo.or.cz/r/git/debian.git/ +Checksums-Sha256: + 6322d0dbe9b867a6cd1cd75f95a4a20335faa2030c38688f460ddaaaacbd4d06 2923 git_2.20.1-2+deb10u3.dsc + 9d2e91e2faa2ea61ba0a70201d023b36f54d846314591a002c610ea2ab81c3e9 5359872 git_2.20.1.orig.tar.xz + 3c6e2f8495350bccd0981d579d4d1cac6b0e051e1f7ba8b1d22c842bd4cb3453 646216 git_2.20.1-2+deb10u3.debian.tar.xz +Homepage: https://git-scm.com/ +Package-List: + git deb vcs optional arch=any + git-all deb vcs optional arch=all + git-cvs deb vcs optional arch=all + git-daemon-run deb vcs optional arch=all + git-daemon-sysvinit deb vcs optional arch=all + git-doc deb doc optional arch=all + git-el deb vcs optional arch=all + git-email deb vcs optional arch=all + git-gui deb vcs optional arch=all + git-man deb doc optional arch=all + git-mediawiki deb vcs optional arch=all + git-svn deb vcs optional arch=all + gitk deb vcs optional arch=all + gitweb deb vcs optional arch=all +Directory: pool/main/g/git +Priority: source +Section: vcs + +Package: subversion +Binary: subversion, libsvn1, libsvn-dev, libsvn-doc, libapache2-mod-svn, python-subversion, subversion-tools, libsvn-java, libsvn-perl, ruby-svn +Version: 1.10.4-1+deb10u1 +Maintainer: James McCoy +Build-Depends: apache2-dev (>= 2.4.16), autoconf, bash-completion, chrpath, debhelper (>= 11~), default-jdk-headless (>= 2:1.6) [!hurd-i386 !hppa !sparc], dh-apache2, dh-python, doxygen, junit [!hurd-i386 !hppa !sparc], libapr1-dev, libaprutil1-dev, libdb5.3-dev, libdbus-1-dev, liblz4-dev (>= 0.0~r129), libkf5coreaddons-dev, libkf5i18n-dev, libkf5wallet-dev, libperl-dev, libsasl2-dev, libsecret-1-dev, libserf-dev (>= 1.3.9-4~), libsqlite3-dev (>= 3.8.7), libtool, libutf8proc-dev, perl, python-all-dev (>= 2.7), rename, ruby, ruby-dev, swig, zlib1g-dev +Build-Conflicts: libsvn-dev (<< 1.10~) +Architecture: any all +Standards-Version: 4.3.0 +Format: 3.0 (quilt) +Files: + 70b1d3c8ae91301a3f7766b8181d09c9 3428 subversion_1.10.4-1+deb10u1.dsc + fcfd1bcd95a8b44e6a6de3a97425aead 11347907 subversion_1.10.4.orig.tar.gz + 98e9c6902e6a18973b3d936657384a88 2107 subversion_1.10.4.orig.tar.gz.asc + a4a14bcff3cef49d0d9388356213f3e4 438024 subversion_1.10.4-1+deb10u1.debian.tar.xz +Vcs-Browser: https://salsa.debian.org/jamessan/subversion +Vcs-Git: https://salsa.debian.org/jamessan/subversion.git +Checksums-Sha256: + c9956fd5b850924dd123048b39195b3d591f55b9cbdf18d4d2a0f496f7decc72 3428 subversion_1.10.4-1+deb10u1.dsc + 354022a837596eb1b5676639ea8d73aa326fa8b2c610d8e1b39aeb7228921f4e 11347907 subversion_1.10.4.orig.tar.gz + bc6173c43ac837f875d9f2921e118c194455796b419769e155496cf084376428 2107 subversion_1.10.4.orig.tar.gz.asc + 1bc8900ef1b9d2af84827dab0fd0164e2058381be3bba0db6fd13cbc858c9b1e 438024 subversion_1.10.4-1+deb10u1.debian.tar.xz +Homepage: http://subversion.apache.org/ +Package-List: + libapache2-mod-svn deb httpd optional arch=any + libsvn-dev deb libdevel optional arch=any + libsvn-doc deb doc optional arch=all + libsvn-java deb java optional arch=any + libsvn-perl deb perl optional arch=any + libsvn1 deb libs optional arch=any + python-subversion deb python optional arch=any + ruby-svn deb ruby optional arch=any + subversion deb vcs optional arch=any + subversion-tools deb vcs optional arch=any +Testsuite: autopkgtest +Testsuite-Triggers: apache2, wget +Directory: pool/main/s/subversion +Priority: source +Section: vcs diff --git a/swh/lister/debian/tests/data/Sources_stretch b/swh/lister/debian/tests/data/Sources_stretch new file mode 100644 index 0000000..801496e --- /dev/null +++ b/swh/lister/debian/tests/data/Sources_stretch @@ -0,0 +1,113 @@ +Package: dh-elpa +Binary: dh-elpa +Version: 0.0.18 +Maintainer: Debian Emacs addons team +Uploaders: David Bremner +Build-Depends: debhelper (>= 9), emacs24-nox | emacs24 (>= 24~) | emacs24-lucid (>= 24~) +Architecture: all +Standards-Version: 3.9.6 +Format: 1.0 +Files: + 25beb4376110fe075460f4b7776d0349 1471 dh-elpa_0.0.18.dsc + dc0d3b42c1db80cac9817f43c171bfb3 10038 dh-elpa_0.0.18.tar.gz +Vcs-Browser: http://anonscm.debian.org/cgit/pkg-emacsen/pkg/dh-elpa.git/ +Vcs-Git: git://anonscm.debian.org/pkg-emacsen/pkg/dh-elpa.git +Checksums-Sha256: + 87fb2f13d4a8cdea0cec752cc9873eef1c92961655315d2f14d178f9b1b7fc43 1471 dh-elpa_0.0.18.dsc + 24e5be28cda286398db0018d9577493445c61a0602e239ca285a2981f1068b10 10038 dh-elpa_0.0.18.tar.gz +Package-List: + dh-elpa deb devel optional arch=all +Extra-Source-Only: yes +Directory: pool/main/d/dh-elpa +Priority: extra +Section: misc + +Package: dh-elpa +Binary: dh-elpa +Version: 0.0.19 +Maintainer: Debian Emacs addons team +Uploaders: David Bremner +Build-Depends: debhelper (>= 9), emacs24-nox | emacs24 (>= 24~) | emacs24-lucid (>= 24~) +Architecture: all +Standards-Version: 3.9.6 +Format: 1.0 +Files: + e4513c0f2112ba60031777ad0a65f9dc 1471 dh-elpa_0.0.19.dsc + ac70db483578ecac510612e1b894e53b 10291 dh-elpa_0.0.19.tar.gz +Vcs-Browser: http://anonscm.debian.org/cgit/pkg-emacsen/pkg/dh-elpa.git/ +Vcs-Git: git://anonscm.debian.org/pkg-emacsen/pkg/dh-elpa.git +Checksums-Sha256: + 796a96fad0b03eb589f47c44406f8d32e5b8881dce34c425f1c915650618235c 1471 dh-elpa_0.0.19.dsc + 4bb0a0ecdb75585e168a56a53c79e620b2da70584db9d29e136a3ae9f8a92a76 10291 dh-elpa_0.0.19.tar.gz +Package-List: + dh-elpa deb devel optional arch=all +Extra-Source-Only: yes +Directory: pool/main/d/dh-elpa +Priority: extra +Section: misc + +Package: dh-elpa +Binary: dh-elpa +Version: 0.0.20 +Maintainer: Debian Emacs addons team +Uploaders: David Bremner , Sean Whitton , +Build-Depends: debhelper (>= 9.20151004), emacs24-nox | emacs24 (>= 24~) | emacs24-lucid (>= 24~) +Architecture: all +Standards-Version: 3.9.8 +Format: 1.0 +Files: + 82455df65ccd88896cdc083541d29236 1526 dh-elpa_0.0.20.dsc + 4a7cc13b097e44228b5635c400e33202 12884 dh-elpa_0.0.20.tar.gz +Vcs-Browser: https://anonscm.debian.org/cgit/pkg-emacsen/pkg/dh-elpa.git/ +Vcs-Git: https://anonscm.debian.org/pkg-emacsen/pkg/dh-elpa.git +Checksums-Sha256: + 77c9761b1359c256ad25d4c7a826a27643a0094929a4cb3ac8cdaa0fcdb02d1b 1526 dh-elpa_0.0.20.dsc + 13e4c6ffaaa6cd793d19de677af470ac0edac098779627e9f8555644a7da42f0 12884 dh-elpa_0.0.20.tar.gz +Package-List: + dh-elpa deb devel optional arch=all +Extra-Source-Only: yes +Directory: pool/main/d/dh-elpa +Priority: extra +Section: misc + +Package: git +Binary: git, git-man, git-core, git-doc, git-arch, git-cvs, git-svn, git-mediawiki, git-email, git-daemon-run, git-daemon-sysvinit, git-gui, gitk, git-el, gitweb, git-all +Version: 1:2.11.0-3+deb9u7 +Maintainer: Gerrit Pape +Uploaders: Jonathan Nieder , Anders Kaseorg +Build-Depends: libz-dev, libpcre3-dev, gettext, libcurl4-gnutls-dev, libexpat1-dev, subversion, libsvn-perl, libyaml-perl, tcl, libhttp-date-perl | libtime-modules-perl, libcgi-pm-perl, python, cvs, cvsps, libdbd-sqlite3-perl, unzip, libio-pty-perl, debhelper (>= 9), dh-exec (>= 0.7), dh-apache2, dpkg-dev (>= 1.16.2~) +Build-Depends-Indep: asciidoc, xmlto, docbook-xsl +Architecture: any all +Standards-Version: 3.9.6.0 +Format: 3.0 (quilt) +Files: + e594aeada05ecb15253cc5768412ce3b 2944 git_2.11.0-3+deb9u7.dsc + dd4e3360e28aec5bb902fb34dd7fce3b 4197984 git_2.11.0.orig.tar.xz + e8d896e5307397f0e106e6a85c1b8682 610188 git_2.11.0-3+deb9u7.debian.tar.xz +Vcs-Browser: http://repo.or.cz/w/git/debian.git/ +Vcs-Git: https://repo.or.cz/r/git/debian.git/ +Checksums-Sha256: + 7f2be1b1709c216ad06590687cc8fc0ff6b55a6c3e0ad6ec32b2567ce10adec1 2944 git_2.11.0-3+deb9u7.dsc + 7e7e8d69d494892373b87007674be5820a4bc1ef596a0117d03ea3169119fd0b 4197984 git_2.11.0.orig.tar.xz + 3f54b7ea7b8cda477ddb559c63de063c5bd49d8ab772330c05c79ace546ce38d 610188 git_2.11.0-3+deb9u7.debian.tar.xz +Homepage: https://git-scm.com/ +Package-List: + git deb vcs optional arch=any + git-all deb vcs optional arch=all + git-arch deb vcs optional arch=all + git-core deb vcs optional arch=all + git-cvs deb vcs optional arch=all + git-daemon-run deb vcs optional arch=all + git-daemon-sysvinit deb vcs extra arch=all + git-doc deb doc optional arch=all + git-el deb vcs optional arch=all + git-email deb vcs optional arch=all + git-gui deb vcs optional arch=all + git-man deb doc optional arch=all + git-mediawiki deb vcs optional arch=all + git-svn deb vcs optional arch=all + gitk deb vcs optional arch=all + gitweb deb vcs optional arch=all +Directory: pool/main/g/git +Priority: source +Section: vcs diff --git a/swh/lister/debian/tests/data/http_deb.debian.org/debian__dists_stretch_contrib_source_Sources.xz b/swh/lister/debian/tests/data/http_deb.debian.org/debian__dists_stretch_contrib_source_Sources.xz deleted file mode 100644 index 865217416f3b5ad9458e61a0293151f0e619d8fa..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 44704 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 ETId+$`v3p{ diff --git a/swh/lister/debian/tests/data/http_deb.debian.org/debian__dists_stretch_main_source_Sources.xz b/swh/lister/debian/tests/data/http_deb.debian.org/debian__dists_stretch_main_source_Sources.xz deleted file mode 100644 index 15112e3d4719a1de87ca0158674dcdc51afe2ce6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3284 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 Soa586#Ao{g000001X)_9x>n8r diff --git a/swh/lister/debian/tests/test_init.py b/swh/lister/debian/tests/test_init.py deleted file mode 100644 index 3048613..0000000 --- a/swh/lister/debian/tests/test_init.py +++ /dev/null @@ -1,92 +0,0 @@ -# Copyright (C) 2019 The Software Heritage developers -# See the AUTHORS file at the top-level directory of this distribution -# License: GNU General Public License version 3, or any later version -# See top-level LICENSE file for more information - -import pytest - -from swh.lister.debian import debian_init -from swh.lister.debian.models import Area, Distribution - - -@pytest.fixture -def engine(session): - session.autoflush = False - return session.bind - - -def test_debian_init_step(engine, session): - distribution_name = "KaliLinux" - - distrib = ( - session.query(Distribution) - .filter(Distribution.name == distribution_name) - .one_or_none() - ) - assert distrib is None - - all_area = session.query(Area).all() - assert all_area == [] - - suites = ["wheezy", "jessie"] - components = ["main", "contrib"] - - debian_init( - engine, - distribution_name=distribution_name, - suites=suites, - components=components, - ) - distrib = ( - session.query(Distribution) - .filter(Distribution.name == distribution_name) - .one_or_none() - ) - - assert distrib is not None - assert distrib.name == distribution_name - assert distrib.type == "deb" - assert distrib.mirror_uri == "http://deb.debian.org/debian/" - - all_area = session.query(Area).all() - assert len(all_area) == 2 * 2, "2 suites * 2 components per suite" - - expected_area_names = [] - for suite in suites: - for component in components: - expected_area_names.append(f"{suite}/{component}") - - for area in all_area: - area.id = None - assert area.distribution == distrib - assert area.name in expected_area_names - - # check idempotency (on exact same call) - - debian_init( - engine, - distribution_name=distribution_name, - suites=suites, - components=components, - ) - - distribs = ( - session.query(Distribution).filter(Distribution.name == distribution_name).all() - ) - - assert len(distribs) == 1 - distrib = distribs[0] - - all_area = session.query(Area).all() - assert len(all_area) == 2 * 2, "2 suites * 2 components per suite" - - # Add a new suite - debian_init( - engine, - distribution_name=distribution_name, - suites=["lenny"], - components=components, - ) - - all_area = [a.name for a in session.query(Area).all()] - assert len(all_area) == (2 + 1) * 2, "3 suites * 2 components per suite" diff --git a/swh/lister/debian/tests/test_lister.py b/swh/lister/debian/tests/test_lister.py index 4fb4169..d4821a0 100644 --- a/swh/lister/debian/tests/test_lister.py +++ b/swh/lister/debian/tests/test_lister.py @@ -1,35 +1,201 @@ -# Copyright (C) 2019 The Software Heritage developers +# Copyright (C) 2019-2021 The Software Heritage developers # See the AUTHORS file at the top-level directory of this distribution # License: GNU General Public License version 3, or any later version # See top-level LICENSE file for more information -import logging +from collections import defaultdict +from pathlib import Path +from typing import Dict, List, Set, Tuple -logger = logging.getLogger(__name__) +from debian.deb822 import Sources +import pytest + +from swh.lister.debian.lister import ( + DebianLister, + DebianOrigin, + PkgName, + PkgVersion, + Suite, +) +from swh.scheduler.interface import SchedulerInterface + +# Those tests use sample debian Sources files whose content has been extracted +# from the real Sources files from stretch, buster and bullseye suite. +# They contain the follwowing package source info +# - stretch: +# * dh-elpa (versions: 0.0.18, 0.0.19, 0.0.20), +# * git (version: 1:2.11.0-3+deb9u7) +# - buster: +# * git (version: 1:2.20.1-2+deb10u3), +# * subversion (version: 1.10.4-1+deb10u1) +# - bullseye: +# * git (version: 1:2.29.2-1) +# * subversion (version: 1.14.0-3) +# * hg-git (version: 0.9.0-2) + +_mirror_url = "http://deb.debian.org/debian" +_suites = ["stretch", "buster", "bullseye"] +_components = ["main"] + +SourcesText = str -def test_lister_debian(lister_debian, datadir, requests_mock_datadir): - """Simple debian listing should create scheduled tasks +def _debian_sources_content(datadir: str, suite: Suite) -> SourcesText: + return Path(datadir, f"Sources_{suite}").read_text() + +@pytest.fixture +def debian_sources(datadir: str) -> Dict[Suite, SourcesText]: + return {suite: _debian_sources_content(datadir, suite) for suite in _suites} + + +# suite -> package name -> list of versions +DebianSuitePkgSrcInfo = Dict[Suite, Dict[PkgName, List[Sources]]] + + +def _init_test( + swh_scheduler: SchedulerInterface, + debian_sources: Dict[Suite, SourcesText], + requests_mock, +) -> Tuple[DebianLister, DebianSuitePkgSrcInfo]: + lister = DebianLister( + scheduler=swh_scheduler, + mirror_url=_mirror_url, + suites=list(debian_sources.keys()), + components=_components, + ) + + suite_pkg_info: DebianSuitePkgSrcInfo = {} + + for suite, sources in debian_sources.items(): + suite_pkg_info[suite] = defaultdict(list) + for pkg_src in Sources.iter_paragraphs(sources): + suite_pkg_info[suite][pkg_src["Package"]].append(pkg_src) + + for idx_url, compression in lister.debian_index_urls(suite, _components[0]): + if compression: + requests_mock.get(idx_url, status_code=404) + else: + requests_mock.get(idx_url, text=sources) + + return lister, suite_pkg_info + + +def _check_listed_origins( + swh_scheduler: SchedulerInterface, + lister: DebianLister, + suite_pkg_info: DebianSuitePkgSrcInfo, + lister_previous_state: Dict[PkgName, Set[PkgVersion]], +) -> Set[DebianOrigin]: + + scheduler_origins = swh_scheduler.get_listed_origins(lister.lister_obj.id).results + + origin_urls = set() + + # iterate on each debian suite for the main component + for suite, pkg_info in suite_pkg_info.items(): + # iterate on each package + for package_name, pkg_srcs in pkg_info.items(): + # iterate on each package version info + for pkg_src in pkg_srcs: + # build package version key + package_version_key = f"{suite}/{_components[0]}/{pkg_src['Version']}" + # if package or its version not previously listed, those info should + # have been sent to the scheduler database + if ( + package_name not in lister_previous_state + or package_version_key not in lister_previous_state[package_name] + ): + # build origin url + origin_url = lister.origin_url_for_package(package_name) + origin_urls.add(origin_url) + # get ListerOrigin object from scheduler database + filtered_origins = [ + scheduler_origin + for scheduler_origin in scheduler_origins + if scheduler_origin.url == origin_url + ] + + assert filtered_origins + # check the version info are available + assert ( + package_version_key + in filtered_origins[0].extra_loader_arguments["packages"] + ) + + # check listed package version is in lister state + assert package_name in lister.state.package_versions + assert ( + package_version_key + in lister.state.package_versions[package_name] + ) + return origin_urls + + +def test_lister_debian_all_suites( + swh_scheduler: SchedulerInterface, + debian_sources: Dict[Suite, SourcesText], + requests_mock, +): """ - # Run the lister - lister_debian.run() + Simulate a full listing of main component packages for all debian suites. + """ + lister, suite_pkg_info = _init_test(swh_scheduler, debian_sources, requests_mock) - r = lister_debian.scheduler.search_tasks(task_type="load-deb-package") - assert len(r) == 151 + stats = lister.run() - for row in r: - assert row["type"] == "load-deb-package" - # arguments check - args = row["arguments"]["args"] - assert len(args) == 0 + origin_urls = _check_listed_origins( + swh_scheduler, lister, suite_pkg_info, lister_previous_state={} + ) - # kwargs - kwargs = row["arguments"]["kwargs"] - assert set(kwargs.keys()) == {"url", "date", "packages"} + assert stats.pages == len(_suites) * len(_components) + assert stats.origins == len(origin_urls) - logger.debug("kwargs: %s", kwargs) - assert isinstance(kwargs["url"], str) + stats = lister.run() - assert row["policy"] == "oneshot" - assert row["priority"] is None + assert stats.pages == len(_suites) * len(_components) + assert stats.origins == 0 + + +@pytest.mark.parametrize( + "suites_params", + [[_suites[:1]], [_suites[:1], _suites[:2]], [_suites[:1], _suites[:2], _suites],], +) +def test_lister_debian_updated_packages( + swh_scheduler: SchedulerInterface, + debian_sources: Dict[Suite, SourcesText], + requests_mock, + suites_params: List[Suite], +): + """ + Simulate incremental listing of main component packages by adding new suite + to process between each listing operation. + """ + + lister_previous_state: Dict[PkgName, Set[PkgVersion]] = {} + + for idx, suites in enumerate(suites_params): + + sources = {suite: debian_sources[suite] for suite in suites} + + lister, suite_pkg_info = _init_test(swh_scheduler, sources, requests_mock) + + stats = lister.run() + + origin_urls = _check_listed_origins( + swh_scheduler, + lister, + suite_pkg_info, + lister_previous_state=lister_previous_state, + ) + + assert stats.pages == len(sources) + assert stats.origins == len(origin_urls) + + lister_previous_state = lister.state.package_versions + + # only new packages or packages with new versions should be listed + if len(suites) > 1 and idx < len(suites) - 1: + assert stats.origins == 0 + else: + assert stats.origins != 0 diff --git a/swh/lister/debian/tests/test_models.py b/swh/lister/debian/tests/test_models.py deleted file mode 100644 index 25cf995..0000000 --- a/swh/lister/debian/tests/test_models.py +++ /dev/null @@ -1,32 +0,0 @@ -# Copyright (C) 2019 The Software Heritage developers -# See the AUTHORS file at the top-level directory of this distribution -# License: GNU General Public License version 3, or any later version -# See top-level LICENSE file for more information - -import pytest - -from swh.lister.debian.models import Area, Distribution - - -def test_area_index_uris_deb(session): - d = Distribution( - name="Debian", type="deb", mirror_uri="http://deb.debian.org/debian" - ) - a = Area(distribution=d, name="unstable/main", active=True,) - session.add_all([d, a]) - session.commit() - - uris = list(a.index_uris()) - assert uris - - -def test_area_index_uris_rpm(session): - d = Distribution( - name="CentOS", type="rpm", mirror_uri="http://centos.mirrors.proxad.net/" - ) - a = Area(distribution=d, name="8", active=True,) - session.add_all([d, a]) - session.commit() - - with pytest.raises(NotImplementedError): - list(a.index_uris()) diff --git a/swh/lister/debian/tests/test_tasks.py b/swh/lister/debian/tests/test_tasks.py index dbf1136..0a1d30d 100644 --- a/swh/lister/debian/tests/test_tasks.py +++ b/swh/lister/debian/tests/test_tasks.py @@ -1,10 +1,12 @@ -# Copyright (C) 2019-2020 The Software Heritage developers +# Copyright (C) 2019-2021 The Software Heritage developers # See the AUTHORS file at the top-level directory of this distribution # License: GNU General Public License version 3, or any later version # See top-level LICENSE file for more information from unittest.mock import patch +from swh.lister.pattern import ListerStats + def test_ping(swh_scheduler_celery_app, swh_scheduler_celery_worker): res = swh_scheduler_celery_app.send_task("swh.lister.debian.tasks.ping") @@ -17,15 +19,25 @@ def test_ping(swh_scheduler_celery_app, swh_scheduler_celery_worker): @patch("swh.lister.debian.tasks.DebianLister") def test_lister(lister, swh_scheduler_celery_app, swh_scheduler_celery_worker): # setup the mocked DebianLister - lister.return_value = lister - lister.run.return_value = None + lister.from_configfile.return_value = lister + stats = ListerStats(pages=12, origins=35618) + lister.run.return_value = stats + + kwargs = dict( + mirror_url="http://www-ftp.lip6.fr/pub/linux/distributions/Ubuntu/archive/", + distribution="Ubuntu", + suites=["xenial", "bionic", "focal"], + components=["main", "multiverse", "restricted", "universe"], + ) res = swh_scheduler_celery_app.send_task( - "swh.lister.debian.tasks.DebianListerTask", ("stretch",) + "swh.lister.debian.tasks.DebianListerTask", kwargs=kwargs ) assert res res.wait() assert res.successful() - lister.assert_called_once_with(distribution="stretch") + lister.from_configfile.assert_called_once_with(**kwargs) lister.run.assert_called_once_with() + + assert res.result == stats.dict() diff --git a/swh/lister/debian/utils.py b/swh/lister/debian/utils.py deleted file mode 100644 index 777b401..0000000 --- a/swh/lister/debian/utils.py +++ /dev/null @@ -1,83 +0,0 @@ -# Copyright (C) 2017-2019 the Software Heritage developers -# License: GNU General Public License version 3, or any later version -# See top-level LICENSE file for more information - -import logging - -import click - -from swh.lister.debian.lister import DebianLister -from swh.lister.debian.models import Area, Distribution, SQLBase - - -@click.group() -@click.option("--verbose/--no-verbose", default=False) -@click.pass_context -def cli(ctx, verbose): - ctx.obj["lister"] = DebianLister() - if verbose: - loglevel = logging.DEBUG - logging.getLogger("sqlalchemy.engine").setLevel(logging.INFO) - else: - loglevel = logging.INFO - - logging.basicConfig( - format="%(asctime)s %(process)d %(levelname)s %(message)s", level=loglevel, - ) - - -@cli.command() -@click.pass_context -def create_schema(ctx): - """Create the schema from the models""" - SQLBase.metadata.create_all(ctx.obj["lister"].db_engine) - - -@cli.command() -@click.option("--name", help="The name of the distribution") -@click.option("--type", help="The type of distribution") -@click.option("--mirror-uri", help="The URL to the mirror of the distribution") -@click.option("--area", help="The areas for the distribution", multiple=True) -@click.pass_context -def create_distribution(ctx, name, type, mirror_uri, area): - to_add = [] - db_session = ctx.obj["lister"].db_session - d = ( - db_session.query(Distribution) - .filter(Distribution.name == name) - .filter(Distribution.type == type) - .one_or_none() - ) - - if not d: - d = Distribution(name=name, type=type, mirror_uri=mirror_uri) - to_add.append(d) - - for area_name in area: - a = None - if d.id: - a = ( - db_session.query(Area) - .filter(Area.distribution == d) - .filter(Area.name == area_name) - .one_or_none() - ) - - if not a: - a = Area(name=area_name, distribution=d) - to_add.append(a) - - db_session.add_all(to_add) - db_session.commit() - - -@cli.command() -@click.option("--name", help="The name of the distribution") -@click.pass_context -def list_distribution(ctx, name): - """List the distribution""" - ctx.obj["lister"].run(name) - - -if __name__ == "__main__": - cli(obj={})