diff --git a/swh/fuse/fs/artifact.py b/swh/fuse/fs/artifact.py --- a/swh/fuse/fs/artifact.py +++ b/swh/fuse/fs/artifact.py @@ -3,15 +3,17 @@ # License: GNU General Public License version 3, or any later version # See top-level LICENSE file for more information -from typing import Any, AsyncIterator +from dataclasses import dataclass +from pathlib import Path +from typing import Any, AsyncIterator, List from swh.fuse.fs.entry import EntryMode, FuseEntry -from swh.model.identifiers import CONTENT, DIRECTORY, SWHID - -# Avoid cycling import -Fuse = "Fuse" +from swh.fuse.fs.symlink import SymlinkEntry +from swh.model.from_disk import DentryPerms +from swh.model.identifiers import CONTENT, DIRECTORY, REVISION, SWHID +@dataclass class ArtifactEntry(FuseEntry): """ FUSE virtual entry for a Software Heritage Artifact @@ -20,21 +22,8 @@ prefetch: optional prefetched metadata used to set entry attributes """ - def __init__( - self, name: str, mode: int, fuse: Fuse, swhid: SWHID, prefetch: Any = None - ): - super().__init__(name, mode, fuse) - self.swhid = swhid - self.prefetch = prefetch - - -def typify( - name: str, mode: int, fuse: Fuse, swhid: SWHID, prefetch: Any = None -) -> ArtifactEntry: - """ Create an artifact entry corresponding to the given artifact type """ - - getters = {CONTENT: Content, DIRECTORY: Directory} - return getters[swhid.object_type](name, mode, fuse, swhid, prefetch) + swhid: SWHID + prefetch: Any = None class Content(ArtifactEntry): @@ -48,14 +37,16 @@ directory, the permissions of the `archive/SWHID` file will be arbitrary and not meaningful (e.g., `0x644`). """ - async def content(self) -> bytes: - return await self.fuse.get_blob(self.swhid) + async def get_content(self) -> bytes: + data = await self.fuse.get_blob(self.swhid) + self.prefetch["length"] = len(data) + return data - async def length(self) -> int: - # When listing entries from a directory, the API already gave us information + async def size(self) -> int: if self.prefetch: return self.prefetch["length"] - return len(await self.content()) + else: + return len(await self.get_content()) async def __aiter__(self): raise ValueError("Cannot iterate over a content type artifact") @@ -73,21 +64,115 @@ So it is possible that, in the context of a directory, a file is presented as writable, whereas actually writing to it will fail with `EPERM`. """ - async def __aiter__(self) -> AsyncIterator[ArtifactEntry]: + async def __aiter__(self) -> AsyncIterator[FuseEntry]: metadata = await self.fuse.get_metadata(self.swhid) for entry in metadata: - yield typify( - name=entry["name"], - # Use default read-only permissions for directories, and - # archived permissions for contents - mode=( - entry["perms"] - if entry["target"].object_type == CONTENT - else int(EntryMode.RDONLY_DIR) - ), - fuse=self.fuse, - swhid=entry["target"], - # The directory API has extra info we can use to set attributes - # without additional Software Heritage API call - prefetch=entry, + name = entry["name"] + swhid = entry["target"] + mode = ( + # Archived permissions for directories are always set to + # 0o040000 so use a read-only permission instead + int(EntryMode.RDONLY_DIR) + if swhid.object_type == DIRECTORY + else entry["perms"] + ) + + # 1. Symlinks + if mode == DentryPerms.symlink: + yield self.create_child( + SymlinkEntry, + name=name, + # Symlink target is stored in the blob content + target=await self.fuse.get_blob(swhid), + ) + # 2. Submodules + elif swhid.object_type == REVISION: + # Make sure the revision metadata is fetched and create a + # symlink to distinguish it with regular directories + await self.fuse.get_metadata(swhid) + yield self.create_child( + SymlinkEntry, + name=name, + target=Path(self.get_relative_root_path(), f"archive/{swhid}"), + ) + # 3. Regular entries (directories, contents) + else: + yield self.create_child( + OBJTYPE_GETTERS[swhid.object_type], + name=name, + mode=mode, + swhid=swhid, + # The directory API has extra info we can use to set + # attributes without additional Software Heritage API call + prefetch=entry, + ) + + +class Revision(ArtifactEntry): + """ Software Heritage revision artifact. + + Revision (AKA commit) nodes are represented on the file-system as + directories with the following entries: + + - `root`: source tree at the time of the commit, as a symlink pointing into + `archive/`, to a SWHID of type `dir` + - `parents/` (note the plural): a virtual directory containing entries named + `1`, `2`, `3`, etc., one for each parent commit. Each of these entry is a + symlink pointing into `archive/`, to the SWHID file for the given parent + commit + - `parent` (note the singular): present if and only if the current commit + has at least one parent commit (which is the most common case). When + present it is a symlink pointing into `parents/1/` + - `meta.json`: metadata for the current node, as a symlink pointing to the + relevant `meta/.json` file """ + + async def __aiter__(self) -> AsyncIterator[FuseEntry]: + metadata = await self.fuse.get_metadata(self.swhid) + directory = metadata["directory"] + parents = metadata["parents"] + + # Make sure all necessary metadatas are fetched + await self.fuse.get_metadata(directory) + for parent in parents: + await self.fuse.get_metadata(parent["id"]) + + root_path = self.get_relative_root_path() + + yield self.create_child( + SymlinkEntry, name="root", target=Path(root_path, f"archive/{directory}"), + ) + yield self.create_child( + SymlinkEntry, + name="meta.json", + target=Path(root_path, f"meta/{self.swhid}.json"), + ) + yield self.create_child( + RevisionParents, + name="parents", + mode=int(EntryMode.RDONLY_DIR), + parents=[x["id"] for x in parents], + ) + + if len(parents) >= 1: + yield self.create_child( + SymlinkEntry, name="parent", target="parents/1/", ) + + +@dataclass +class RevisionParents(FuseEntry): + """ Revision virtual `parents/` directory """ + + parents: List[SWHID] + + async def __aiter__(self) -> AsyncIterator[FuseEntry]: + root_path = self.get_relative_root_path() + for i, parent in enumerate(self.parents): + yield self.create_child( + SymlinkEntry, + name=str(i + 1), + target=Path(root_path, f"archive/{parent}"), + ) + + +OBJTYPE_GETTERS = {CONTENT: Content, DIRECTORY: Directory, REVISION: Revision} diff --git a/swh/fuse/fs/entry.py b/swh/fuse/fs/entry.py --- a/swh/fuse/fs/entry.py +++ b/swh/fuse/fs/entry.py @@ -3,8 +3,13 @@ # License: GNU General Public License version 3, or any later version # See top-level LICENSE file for more information +from __future__ import annotations + +from dataclasses import dataclass, field from enum import IntEnum -from stat import S_IFDIR, S_IFREG +from pathlib import Path +from stat import S_IFDIR, S_IFLNK, S_IFREG +from typing import Any, AsyncIterator, Union # Avoid cycling import Fuse = "Fuse" @@ -20,8 +25,10 @@ RDONLY_FILE = S_IFREG | 0o444 RDONLY_DIR = S_IFDIR | 0o555 + SYMLINK = S_IFLNK | 0o444 +@dataclass class FuseEntry: """ Main wrapper class to manipulate virtual FUSE entries @@ -32,17 +39,37 @@ inode: unique integer identifying the entry """ - def __init__(self, name: str, mode: int, fuse: Fuse): - self.name = name - self.mode = mode - self.fuse = fuse - self.inode = fuse._alloc_inode(self) + name: str + mode: int + depth: int + fuse: Fuse + inode: int = field(init=False) - async def length(self) -> int: - return 0 + def __post_init__(self): + self.inode = self.fuse._alloc_inode(self) + + async def get_content(self) -> bytes: + """ Return the content of a file entry """ - async def content(self): return None - async def __aiter__(self): + async def size(self) -> int: + """ Return the size of a file entry """ + + return 0 + + async def __aiter__(self) -> AsyncIterator[FuseEntry]: + """ Return the child entries of a directory entry """ + + yield None + + def get_target(self) -> Union[str, bytes, Path]: + """ Return the path target of a symlink entry """ + return None + + def get_relative_root_path(self) -> str: + return "../" * (self.depth - 1) + + def create_child(self, constructor: Any, **kwargs) -> FuseEntry: + return constructor(depth=self.depth + 1, fuse=self.fuse, **kwargs) diff --git a/swh/fuse/fs/mountpoint.py b/swh/fuse/fs/mountpoint.py --- a/swh/fuse/fs/mountpoint.py +++ b/swh/fuse/fs/mountpoint.py @@ -3,34 +3,35 @@ # License: GNU General Public License version 3, or any later version # See top-level LICENSE file for more information +from dataclasses import dataclass, field import json from typing import AsyncIterator -from swh.fuse.fs.artifact import typify +from swh.fuse.fs.artifact import OBJTYPE_GETTERS from swh.fuse.fs.entry import EntryMode, FuseEntry from swh.model.identifiers import CONTENT, SWHID -# Avoid cycling import -Fuse = "Fuse" - +@dataclass class Root(FuseEntry): """ The FUSE mountpoint, consisting of the archive/ and meta/ directories """ - def __init__(self, fuse: Fuse): - super().__init__(name="root", mode=int(EntryMode.RDONLY_DIR), fuse=fuse) + name: str = field(init=False, default=None) + mode: int = field(init=False, default=int(EntryMode.RDONLY_DIR)) + depth: int = field(init=False, default=1) async def __aiter__(self) -> AsyncIterator[FuseEntry]: - for entry in [ArchiveDir(self.fuse), MetaDir(self.fuse)]: - yield entry + yield self.create_child(ArchiveDir) + yield self.create_child(MetaDir) +@dataclass class ArchiveDir(FuseEntry): """ The archive/ directory is lazily populated with one entry per accessed SWHID, having actual SWHIDs as names """ - def __init__(self, fuse: Fuse): - super().__init__(name="archive", mode=int(EntryMode.RDONLY_DIR), fuse=fuse) + name: str = field(init=False, default="archive") + mode: int = field(init=False, default=int(EntryMode.RDONLY_DIR)) async def __aiter__(self) -> AsyncIterator[FuseEntry]: async for swhid in self.fuse.cache.get_cached_swhids(): @@ -38,9 +39,15 @@ mode = EntryMode.RDONLY_FILE else: mode = EntryMode.RDONLY_DIR - yield typify(str(swhid), int(mode), self.fuse, swhid) + yield self.create_child( + OBJTYPE_GETTERS[swhid.object_type], + name=str(swhid), + mode=int(mode), + swhid=swhid, + ) +@dataclass class MetaDir(FuseEntry): """ The meta/ directory contains one SWHID.json file for each SWHID entry under archive/. The JSON file contain all available meta information about @@ -49,29 +56,31 @@ branches) the JSON file will contain a complete version with all pages merged together. """ - def __init__(self, fuse: Fuse): - super().__init__(name="meta", mode=int(EntryMode.RDONLY_DIR), fuse=fuse) + name: str = field(init=False, default="meta") + mode: int = field(init=False, default=int(EntryMode.RDONLY_DIR)) async def __aiter__(self) -> AsyncIterator[FuseEntry]: async for swhid in self.fuse.cache.get_cached_swhids(): - yield MetaEntry(swhid, self.fuse) + yield self.create_child( + MetaEntry, + name=f"{swhid}.json", + mode=int(EntryMode.RDONLY_FILE), + swhid=swhid, + ) +@dataclass class MetaEntry(FuseEntry): """ An entry from the meta/ directory, containing for each accessed SWHID a corresponding SWHID.json file with all the metadata from the Software Heritage archive. """ - def __init__(self, swhid: SWHID, fuse: Fuse): - super().__init__( - name=str(swhid) + ".json", mode=int(EntryMode.RDONLY_FILE), fuse=fuse - ) - self.swhid = swhid + swhid: SWHID - async def content(self) -> bytes: + async def get_content(self) -> bytes: # Get raw JSON metadata from API (un-typified) metadata = await self.fuse.cache.metadata.get(self.swhid, typify=False) return json.dumps(metadata).encode() - async def length(self) -> int: - return len(await self.content()) + async def size(self) -> int: + return len(await self.get_content()) diff --git a/swh/fuse/fs/symlink.py b/swh/fuse/fs/symlink.py new file mode 100644 --- /dev/null +++ b/swh/fuse/fs/symlink.py @@ -0,0 +1,28 @@ +# Copyright (C) 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 + +from dataclasses import dataclass, field +from pathlib import Path +from typing import Union + +from swh.fuse.fs.entry import EntryMode, FuseEntry + + +@dataclass +class SymlinkEntry(FuseEntry): + """ FUSE virtual entry for symlinks + + Attributes: + target: path to symlink target + """ + + mode: int = field(init=False, default=int(EntryMode.SYMLINK)) + target: Union[str, bytes, Path] + + async def size(self) -> int: + return len(str(self.target)) + + def get_target(self) -> Union[str, bytes, Path]: + return self.target diff --git a/swh/fuse/fuse.py b/swh/fuse/fuse.py --- a/swh/fuse/fuse.py +++ b/swh/fuse/fuse.py @@ -119,7 +119,7 @@ attrs.st_uid = self.uid attrs.st_ino = entry.inode attrs.st_mode = entry.mode - attrs.st_size = await entry.length() + attrs.st_size = await entry.size() return attrs async def getattr( @@ -174,7 +174,7 @@ inode = fh entry = self.inode2entry(inode) - data = await entry.content() + data = await entry.get_content() return data[offset : offset + length] async def lookup( @@ -193,6 +193,10 @@ logging.error(f"Unknown name during lookup: '{name}'") raise pyfuse3.FUSEError(errno.ENOENT) + async def readlink(self, inode: int, _ctx: pyfuse3.RequestContext) -> bytes: + entry = self.inode2entry(inode) + return os.fsencode(entry.get_target()) + async def main(swhids: List[SWHID], root_path: Path, conf: Dict[str, Any]) -> None: """ swh-fuse CLI entry-point """ diff --git a/swh/fuse/tests/api_data.py b/swh/fuse/tests/api_data.py --- a/swh/fuse/tests/api_data.py +++ b/swh/fuse/tests/api_data.py @@ -3,12 +3,90 @@ # fmt: off API_URL = 'https://invalid-test-only.archive.softwareheritage.org/api/1' -ROOT_SWHID = 'swh:1:dir:9eb62ef7dd283f7385e7d31af6344d9feedd25de' -ROOT_URL = 'directory/9eb62ef7dd283f7385e7d31af6344d9feedd25de/' +ROOTREV_SWHID = 'swh:1:rev:d012a7190fc1fd72ed48911e77ca97ba4521bccd' +ROOTDIR_SWHID = 'swh:1:dir:9eb62ef7dd283f7385e7d31af6344d9feedd25de' +ROOTREV_URL = 'revision/d012a7190fc1fd72ed48911e77ca97ba4521bccd/' +ROOTREV_PARENT_URL = 'revision/cb95712138ec5e480db5160b41172bbc6f6494cc/' +ROOTDIR_URL = 'directory/9eb62ef7dd283f7385e7d31af6344d9feedd25de/' README_URL = 'content/sha1_git:669ac7c32292798644b21dbb5a0dc657125f444d/' README_RAW_URL = 'content/sha1_git:669ac7c32292798644b21dbb5a0dc657125f444d/raw/' MOCK_ARCHIVE = { + 'revision/d012a7190fc1fd72ed48911e77ca97ba4521bccd/': # NoQA: E501 + r"""{ + "author": { + "email": "torvalds@linux-foundation.org", + "fullname": "Linus Torvalds ", + "name": "Linus Torvalds" + }, + "committer": { + "email": "torvalds@linux-foundation.org", + "fullname": "Linus Torvalds ", + "name": "Linus Torvalds" + }, + "committer_date": "2020-08-23T14:08:43-07:00", + "date": "2020-08-23T14:08:43-07:00", + "directory": "9eb62ef7dd283f7385e7d31af6344d9feedd25de", + "directory_url": "https://archive.softwareheritage.org/api/1/directory/9eb62ef7dd283f7385e7d31af6344d9feedd25de/", + "extra_headers": [], + "history_url": "https://archive.softwareheritage.org/api/1/revision/d012a7190fc1fd72ed48911e77ca97ba4521bccd/log/", + "id": "d012a7190fc1fd72ed48911e77ca97ba4521bccd", + "merge": false, + "message": "Linux 5.9-rc2\n", + "metadata": {}, + "parents": [ + { + "id": "cb95712138ec5e480db5160b41172bbc6f6494cc", + "url": "https://archive.softwareheritage.org/api/1/revision/cb95712138ec5e480db5160b41172bbc6f6494cc/" + } + ], + "synthetic": false, + "type": "git", + "url": "https://archive.softwareheritage.org/api/1/revision/d012a7190fc1fd72ed48911e77ca97ba4521bccd/" +} +""", # NoQA: E501 + 'revision/cb95712138ec5e480db5160b41172bbc6f6494cc/': # NoQA: E501 + r"""{ + "author": { + "email": "torvalds@linux-foundation.org", + "fullname": "Linus Torvalds ", + "name": "Linus Torvalds" + }, + "committer": { + "email": "torvalds@linux-foundation.org", + "fullname": "Linus Torvalds ", + "name": "Linus Torvalds" + }, + "committer_date": "2020-08-23T11:37:23-07:00", + "date": "2020-08-23T11:37:23-07:00", + "directory": "4fa3b43d90ce69b46916cc3fd3ea1d15de70443d", + "directory_url": "https://archive.softwareheritage.org/api/1/directory/4fa3b43d90ce69b46916cc3fd3ea1d15de70443d/", + "extra_headers": [ + [ + "mergetag", + "object 64ef8f2c4791940d7f3945507b6a45c20d959260\ntype commit\ntag powerpc-5.9-3\ntagger Michael Ellerman 1598185676 +1000\n\npowerpc fixes for 5.9 #3\n\nAdd perf support for emitting extended registers for power10.\n\nA fix for CPU hotplug on pseries, where on large/loaded systems we may not wait\nlong enough for the CPU to be offlined, leading to crashes.\n\nAddition of a raw cputable entry for Power10, which is not required to boot, but\nis required to make our PMU setup work correctly in guests.\n\nThree fixes for the recent changes on 32-bit Book3S to move modules into their\nown segment for strict RWX.\n\nA fix for a recent change in our powernv PCI code that could lead to crashes.\n\nA change to our perf interrupt accounting to avoid soft lockups when using some\nevents, found by syzkaller.\n\nA change in the way we handle power loss events from the hypervisor on pseries.\nWe no longer immediately shut down if we're told we're running on a UPS.\n\nA few other minor fixes.\n\nThanks to:\n Alexey Kardashevskiy, Andreas Schwab, Aneesh Kumar K.V, Anju T Sudhakar,\n Athira Rajeev, Christophe Leroy, Frederic Barrat, Greg Kurz, Kajol Jain,\n Madhavan Srinivasan, Michael Neuling, Michael Roth, Nageswara R Sastry, Oliver\n O'Halloran, Thiago Jung Bauermann, Vaidyanathan Srinivasan, Vasant Hegde.\n-----BEGIN PGP SIGNATURE-----\n\niQJHBAABCAAxFiEEJFGtCPCthwEv2Y/bUevqPMjhpYAFAl9CYMwTHG1wZUBlbGxl\ncm1hbi5pZC5hdQAKCRBR6+o8yOGlgC/wEACljEVnfHzUObmIgqn9Ru3JlfEI6Hlk\nts7kajCgS/I/bV6DoDMZ8rlZX87QFOwiBkNM1I+vGHSLAuzsmFAnbFPyxw/idxpQ\nXUoNy8OCvbbzCPzChYdiU0PxW2h2i+QxkmktlWSN1SAPudJUWvoPS2Y4+sC4zksk\nB4B6tbW2DT8TFO1kKeZsU9r2t+EH5KwlIOi+uxbH8d76lJINKkBNSnjzMytl7drM\nTZx/HWr8+s/WJo1787x6bv8gxs5tV9b4vIKt2YZNTY2kvYsEDE+fBR1XfCAneXMw\nASYnZV+/xCLIUpRF6DI4RAShLBT/Sfiy1yMTndZgfqAgquokFosszNx2zrk0IzCd\nAgqX93YGbGz/H72W3Y/B0W9+74XyO/u2D9zhNpkCRMpdcsM5MbvOQrQA5Ustu47E\nav5MOaF/nNCd8J+OC4Qjgt5VFb/s0h4FdtrwT80srOa2U6Of9cD/T6xAfOszSJ96\ncWdSb5qhn5wuD9pP32KjwdmWBiUw38/gnRGKpRlOVzyHL/GKZijyaBbWBlkoEmty\n0nbjWW/IVfsOb5Weuiybg541h/QOVuOkb2pOvPClITiH83MY/AciDJ+auo4M//hW\nhaKz9IgV/KctmzDE+v9d0BD8sGmW03YUcQAPdRufI0eGXijDLcnHeuk2B3Nu84Pq\n8mtev+VQ+T6cZA==\n=sdJ1\n-----END PGP SIGNATURE-----" + ] + ], + "history_url": "https://archive.softwareheritage.org/api/1/revision/cb95712138ec5e480db5160b41172bbc6f6494cc/log/", + "id": "cb95712138ec5e480db5160b41172bbc6f6494cc", + "merge": true, + "message": "Merge tag 'powerpc-5.9-3' of git://git.kernel.org/pub/scm/linux/kernel/git/powerpc/linux\n\nPull powerpc fixes from Michael Ellerman:\n\n - Add perf support for emitting extended registers for power10.\n\n - A fix for CPU hotplug on pseries, where on large/loaded systems we\n may not wait long enough for the CPU to be offlined, leading to\n crashes.\n\n - Addition of a raw cputable entry for Power10, which is not required\n to boot, but is required to make our PMU setup work correctly in\n guests.\n\n - Three fixes for the recent changes on 32-bit Book3S to move modules\n into their own segment for strict RWX.\n\n - A fix for a recent change in our powernv PCI code that could lead to\n crashes.\n\n - A change to our perf interrupt accounting to avoid soft lockups when\n using some events, found by syzkaller.\n\n - A change in the way we handle power loss events from the hypervisor\n on pseries. We no longer immediately shut down if we're told we're\n running on a UPS.\n\n - A few other minor fixes.\n\nThanks to Alexey Kardashevskiy, Andreas Schwab, Aneesh Kumar K.V, Anju T\nSudhakar, Athira Rajeev, Christophe Leroy, Frederic Barrat, Greg Kurz,\nKajol Jain, Madhavan Srinivasan, Michael Neuling, Michael Roth,\nNageswara R Sastry, Oliver O'Halloran, Thiago Jung Bauermann,\nVaidyanathan Srinivasan, Vasant Hegde.\n\n* tag 'powerpc-5.9-3' of git://git.kernel.org/pub/scm/linux/kernel/git/powerpc/linux:\n powerpc/perf/hv-24x7: Move cpumask file to top folder of hv-24x7 driver\n powerpc/32s: Fix module loading failure when VMALLOC_END is over 0xf0000000\n powerpc/pseries: Do not initiate shutdown when system is running on UPS\n powerpc/perf: Fix soft lockups due to missed interrupt accounting\n powerpc/powernv/pci: Fix possible crash when releasing DMA resources\n powerpc/pseries/hotplug-cpu: wait indefinitely for vCPU death\n powerpc/32s: Fix is_module_segment() when MODULES_VADDR is defined\n powerpc/kasan: Fix KASAN_SHADOW_START on BOOK3S_32\n powerpc/fixmap: Fix the size of the early debug area\n powerpc/pkeys: Fix build error with PPC_MEM_KEYS disabled\n powerpc/kernel: Cleanup machine check function declarations\n powerpc: Add POWER10 raw mode cputable entry\n powerpc/perf: Add extended regs support for power10 platform\n powerpc/perf: Add support for outputting extended regs in perf intr_regs\n powerpc: Fix P10 PVR revision in /proc/cpuinfo for SMT4 cores\n", + "metadata": {}, + "parents": [ + { + "id": "550c2129d93d5eb198835ac83c05ef672e8c491c", + "url": "https://archive.softwareheritage.org/api/1/revision/550c2129d93d5eb198835ac83c05ef672e8c491c/" + }, + { + "id": "64ef8f2c4791940d7f3945507b6a45c20d959260", + "url": "https://archive.softwareheritage.org/api/1/revision/64ef8f2c4791940d7f3945507b6a45c20d959260/" + } + ], + "synthetic": false, + "type": "git", + "url": "https://archive.softwareheritage.org/api/1/revision/cb95712138ec5e480db5160b41172bbc6f6494cc/" +} +""", # NoQA: E501 'directory/9eb62ef7dd283f7385e7d31af6344d9feedd25de/': # NoQA: E501 r"""[ { diff --git a/swh/fuse/tests/conftest.py b/swh/fuse/tests/conftest.py --- a/swh/fuse/tests/conftest.py +++ b/swh/fuse/tests/conftest.py @@ -16,7 +16,7 @@ from swh.fuse import cli -from .api_data import API_URL, MOCK_ARCHIVE, ROOT_SWHID +from .api_data import API_URL, MOCK_ARCHIVE, ROOTDIR_SWHID, ROOTREV_SWHID @pytest.fixture @@ -44,7 +44,14 @@ config_path.write_text(yaml.dump(config)) CliRunner().invoke( cli.mount, - args=[mntdir, ROOT_SWHID, "--foreground", "--config-file", config_path], + args=[ + mntdir, + ROOTDIR_SWHID, + ROOTREV_SWHID, + "--foreground", + "--config-file", + config_path, + ], ) fuse = Process(target=fuse_process, args=[tmpdir, tmpfile]) diff --git a/swh/fuse/tests/gen-api-data.py b/swh/fuse/tests/gen-api-data.py --- a/swh/fuse/tests/gen-api-data.py +++ b/swh/fuse/tests/gen-api-data.py @@ -13,13 +13,18 @@ API_URL_test = "https://invalid-test-only.archive.softwareheritage.org/api/1" # Use the Linux kernel as a testing repository -ROOT_HASH = "9eb62ef7dd283f7385e7d31af6344d9feedd25de" +ROOTREV_HASH = "d012a7190fc1fd72ed48911e77ca97ba4521bccd" +ROOTREV_PARENT_HASH = "cb95712138ec5e480db5160b41172bbc6f6494cc" +ROOTDIR_HASH = "9eb62ef7dd283f7385e7d31af6344d9feedd25de" README_HASH = "669ac7c32292798644b21dbb5a0dc657125f444d" -ROOT_SWHID = f"swh:1:dir:{ROOT_HASH}" +ROOTREV_SWHID = f"swh:1:rev:{ROOTREV_HASH}" +ROOTDIR_SWHID = f"swh:1:dir:{ROOTDIR_HASH}" urls = { - "ROOT": f"directory/{ROOT_HASH}/", + "ROOTREV": f"revision/{ROOTREV_HASH}/", + "ROOTREV_PARENT": f"revision/{ROOTREV_PARENT_HASH}/", + "ROOTDIR": f"directory/{ROOTDIR_HASH}/", "README": f"content/sha1_git:{README_HASH}/", "README_RAW": f"content/sha1_git:{README_HASH}/raw/", } @@ -30,7 +35,8 @@ print("") print(f"API_URL = '{API_URL_test}'") -print(f"ROOT_SWHID = '{ROOT_SWHID}'") +print(f"ROOTREV_SWHID = '{ROOTREV_SWHID}'") +print(f"ROOTDIR_SWHID = '{ROOTDIR_SWHID}'") for name, url in urls.items(): print(f"{name}_URL = '{url}'") print("") diff --git a/swh/fuse/tests/test_cli.py b/swh/fuse/tests/test_cli.py --- a/swh/fuse/tests/test_cli.py +++ b/swh/fuse/tests/test_cli.py @@ -5,13 +5,13 @@ from pathlib import Path -from .api_data import ROOT_SWHID +from .api_data import ROOTDIR_SWHID def test_mountpoint(fuse_mntdir): archive_dir = Path(fuse_mntdir, "archive") meta_dir = Path(fuse_mntdir, "meta") - swhid_dir = Path(fuse_mntdir, "archive", ROOT_SWHID) + swhid_dir = Path(fuse_mntdir, "archive", ROOTDIR_SWHID) assert archive_dir.is_dir() assert meta_dir.is_dir() assert swhid_dir.is_dir() diff --git a/swh/fuse/tests/test_content.py b/swh/fuse/tests/test_content.py --- a/swh/fuse/tests/test_content.py +++ b/swh/fuse/tests/test_content.py @@ -1,15 +1,15 @@ from pathlib import Path -from .api_data import MOCK_ARCHIVE, README_RAW_URL, ROOT_SWHID +from .api_data import MOCK_ARCHIVE, README_RAW_URL, ROOTDIR_SWHID def test_file_exists(fuse_mntdir): - readme_path = Path(fuse_mntdir, "archive", ROOT_SWHID, "README") + readme_path = Path(fuse_mntdir, "archive", ROOTDIR_SWHID, "README") assert readme_path.is_file() def test_cat_file(fuse_mntdir): - readme_path = Path(fuse_mntdir, "archive", ROOT_SWHID, "README") + readme_path = Path(fuse_mntdir, "archive", ROOTDIR_SWHID, "README") expected = MOCK_ARCHIVE[README_RAW_URL] with open(readme_path, "r") as f: actual = f.read() diff --git a/swh/fuse/tests/test_directory.py b/swh/fuse/tests/test_directory.py --- a/swh/fuse/tests/test_directory.py +++ b/swh/fuse/tests/test_directory.py @@ -2,13 +2,17 @@ from os import listdir from pathlib import Path -from .api_data import MOCK_ARCHIVE, ROOT_SWHID, ROOT_URL +from .api_data import MOCK_ARCHIVE, ROOTDIR_SWHID, ROOTDIR_URL -def test_ls_root_swhid(fuse_mntdir): - root_resp = json.loads(MOCK_ARCHIVE[ROOT_URL]) - expected = [entry["name"] for entry in root_resp] +def get_rootdir_entries(): + rootdir_resp = json.loads(MOCK_ARCHIVE[ROOTDIR_URL]) + return [entry["name"] for entry in rootdir_resp] - swhid_dir = Path(fuse_mntdir, "archive", ROOT_SWHID) - actual = listdir(swhid_dir) + +def test_ls_rootdir(fuse_mntdir): + expected = get_rootdir_entries() + + rootdir_path = Path(fuse_mntdir, "archive", ROOTDIR_SWHID) + actual = listdir(rootdir_path) assert actual == expected diff --git a/swh/fuse/tests/test_revision.py b/swh/fuse/tests/test_revision.py new file mode 100644 --- /dev/null +++ b/swh/fuse/tests/test_revision.py @@ -0,0 +1,22 @@ +from os import listdir +from pathlib import Path + +from swh.fuse.tests.test_directory import get_rootdir_entries + +from .api_data import ROOTREV_SWHID + + +def test_symlinks_exist(fuse_mntdir): + rootrev_dir = Path(fuse_mntdir, "archive", ROOTREV_SWHID) + assert Path(rootrev_dir, "root").is_symlink() + assert Path(rootrev_dir, "parent").is_symlink() + assert Path(rootrev_dir, "parents").is_dir() + assert Path(rootrev_dir, "meta.json").is_symlink() + + +def test_ls_rootdir(fuse_mntdir): + expected = get_rootdir_entries() + + rootdir_path = Path(fuse_mntdir, "archive", ROOTREV_SWHID, "root") + actual = listdir(rootdir_path) + assert actual == expected