Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 37 additions & 8 deletions .github/workflows/coverage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,28 +7,57 @@ on:
branches: [main]

jobs:
build:
runs-on: ubuntu-latest
coverage:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ ubuntu-latest, macos-latest, windows-latest ]
python-version: [ "3.10", "3.11", "3.12", "3.13", "3.14" ]

env:
OS_NAME: ${{ matrix.os }}

steps:
- uses: actions/checkout@v4

- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
python-version: ${{ matrix.python-version }}

- name: Setup uv
uses: astral-sh/setup-uv@v6

- name: Install pytest
- name: Install tox
run: uv sync --group test

- name: Run coverage
run: uv run pytest --cov=. --cov-report=term-missing --cov-report=xml # generates coverage.xml
- name: Translate Python version to tox env
id: translate
shell: bash
run: |
ENVNAME="py${{ matrix.python-version }}"
ENVNAME="${ENVNAME//./}"
echo "envname=$ENVNAME" >> $GITHUB_OUTPUT

- name: Run tests and generate coverage reports
run: uv run tox -e ${{ steps.translate.outputs.envname }}

- name: Upload coverage to Coveralls
if: github.event_name == 'push' && github.ref == 'refs/heads/main'

uses: coverallsapp/github-action@v2
with:
format: cobertura
file: coverage.xml
parallel: true
file: reports/coverage-${{ matrix.os }}-${{ steps.translate.outputs.envname }}.xml

upload:
if: github.event_name == 'push' && github.ref == 'refs/heads/main'

needs: coverage
runs-on: ubuntu-latest
steps:
- name: Coveralls Finished
uses: coverallsapp/github-action@v2
with:
parallel-finished: true
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,8 @@ dist/
*.sqlite3
.coverage
coverage.xml
reports/

# tox
.tox
build/
1 change: 0 additions & 1 deletion cloudisk/db/models/space.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,6 @@ def create(self, name: str, protect: bool) -> SpaceModel:
try:
session.commit()
except IntegrityError:
pass
raise Space.AlreadyExists(f"Space '{name}' already exist")

session.refresh(space)
Expand Down
13 changes: 12 additions & 1 deletion cloudisk/fs/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,22 @@ def _try_link(src: Path, dst: Path) -> None:
Destination path to make symlink to.
"""
try:
os.symlink(src, dst, target_is_directory=True)
os.symlink(src, dst)
logger.info(f"Linked '{src}' -> '{dst}'")
except FileExistsError:
logger.info(f"Already linked: '{src}'")

# Raised on Windows when the user doesn't have Developer Mode on
# https://docs.python.org/3/library/os.html#os.symlink
except OSError as e: # pragma: no cover
if not os.name == "nt":
raise e

raise OSError(
"The symlink could not be created. "
"Make sure Developer Mode is on and try again."
) from e


def link_path(path: Path, recursive: bool = False) -> None:
"""
Expand Down
2 changes: 1 addition & 1 deletion cloudisk/fs/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ def ask_remove_path(path: Path) -> bool:
return ask_remove_dir(path)

raise Exception(
f"{path} already exists and is not a file or a directory. "
f"{path.as_posix()} already exists and is not a file or a directory. "
"Please, remove it first."
)

Expand Down
8 changes: 2 additions & 6 deletions cloudisk/tools/settings.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import importlib
import inspect
import os
import shutil
from functools import _lru_cache_wrapper, cache
from functools import cache
from pathlib import Path
from typing import Any

Expand Down Expand Up @@ -66,10 +65,7 @@ def set_default(self, **kwargs):

def clear_cache(self):
"""Clear cache for all functions and properties inside the instance."""
for _, attr in inspect.getmembers(self):
if inspect.ismethod(attr):
if isinstance(attr.__func__, _lru_cache_wrapper):
attr.__func__.cache_clear()
self.get.cache_clear()

@staticmethod
def build_module(path: Path):
Expand Down
27 changes: 27 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ test = [
"pytest>=8.4.2",
"pytest-asyncio>=1.3.0",
"pytest-cov>=7.0.0",
"tox>=4.34.1",
"tox-uv>=1.29.0",
]

build = [
Expand All @@ -56,6 +58,31 @@ build = [
[tool.pytest.ini_options]
testpaths = ["tests"]
python_files = ["test_*.py"]
markers = ["no_mock"]

[tool.tox]
env_list = [
"py310",
"py311",
"py312",
"py313",
"py314",
]

[tool.tox.env_run_base]
description = "Run test under {base_python}"

set_env.COV_PATH = { replace = "env", name = "COVERAGE_FILE", default = "reports/coverage-{env:OS_NAME:local}-{env_name}.xml" }

commands = [
["uv", "sync", "--group", "test", "--active", "--no-config"],
[
"pytest",
"--cov", ".",
"--cov-report", "term-missing",
"--cov-report", "xml:{env:COV_PATH}"
]
]

[tool.coverage.run]
branch = true
Expand Down
4 changes: 3 additions & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,6 @@ def fake_db(tmp_path, monkeypatch):

monkeypatch.setattr("cloudisk.http.dependencies.CLOUDISK_DB_PATH", tmp_db)

return tmp_db
yield tmp_db

context.engine.dispose()
24 changes: 23 additions & 1 deletion tests/fs/test_commands.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import os
import shutil
from pathlib import Path
from unittest.mock import patch

import pytest
Expand All @@ -15,7 +16,7 @@


@pytest.fixture(autouse=True)
def fake_root(tmp_path, monkeypatch):
def fake_root(tmp_path, monkeypatch) -> Path:
fake_path = tmp_path / "root"
fake_path.mkdir()

Expand Down Expand Up @@ -55,6 +56,7 @@ def test_try_link_ok(fake_root):

assert dst.is_symlink()
assert dst.resolve() == src
assert dst.resolve().is_dir()


def test_try_link_err(fake_root):
Expand All @@ -69,6 +71,23 @@ def test_try_link_err(fake_root):
assert dst.resolve() == src


def test_try_link_raises_OSError(fake_root):
src = fake_root / "src"
dst = fake_root / "dst"

src.mkdir()

with pytest.raises(OSError) as exc_info:
with patch.object(os, "symlink", side_effect=OSError):
_try_link(src, dst)

if os.name == "nt":
assert "Developer Mode" in str(exc_info.value)

assert not dst.is_symlink()
assert not dst.resolve() == src


def test_link_path_ok_not_recursive_path_is_file(tmp_path, fake_root):
file = tmp_path / "file1.txt"
file.write_text("Content")
Expand All @@ -80,6 +99,7 @@ def test_link_path_ok_not_recursive_path_is_file(tmp_path, fake_root):
assert link.exists()
assert link.is_symlink()
assert link.resolve() == file
assert link.resolve().is_file()


def test_link_path_ok_not_recursive_path_is_dir(tmp_path, fake_root):
Expand All @@ -96,6 +116,7 @@ def test_link_path_ok_not_recursive_path_is_dir(tmp_path, fake_root):
assert link.exists()
assert link.is_symlink()
assert link.resolve() == dir
assert link.resolve().is_dir()


def test_link_path_ok_recursive(tmp_path, fake_root):
Expand All @@ -112,6 +133,7 @@ def test_link_path_ok_recursive(tmp_path, fake_root):
assert link.exists()
assert link.is_symlink()
assert link.resolve() == file
assert link.resolve().is_file()


def test_link_path_err_path_doesnt_exist(fake_root):
Expand Down
2 changes: 1 addition & 1 deletion tests/fs/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ def test_remove_path_when_path(tmp_path: Path, mock_path_rmdir: MagicMock):
def test_remove_path_raises_exception(tmp_path: Path):
fake_path = tmp_path / "tmp_socket.sock"
exception_text = (
f"{fake_path} already exists and is not a file or a directory. "
f"{fake_path.as_posix()} already exists and is not a file or a directory. "
"Please, remove it first."
)

Expand Down
31 changes: 18 additions & 13 deletions tests/http/routers/test_files.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from pathlib import Path
from unittest.mock import patch

import pytest
Expand All @@ -14,7 +15,7 @@


@pytest.fixture(autouse=True)
def fake_root(tmp_path, monkeypatch):
def fake_root(tmp_path, monkeypatch) -> Path:
file1 = tmp_path / "file1.txt"
file1.write_text("Test 1")

Expand Down Expand Up @@ -130,13 +131,15 @@ def test_get_files_ok_no_available_paths(endpoint_download, endpoint_list, fake_

@pytest.mark.asyncio
async def test_list_files_500_path_is_file(fake_root):
path = fake_root / "file1.txt"

with pytest.raises(HTTPException) as exc_info:
await _list_files(fake_root / "file1.txt")
await _list_files(path)

response = exc_info.value

assert response.status_code == 500
assert "Errno 20" in response.detail
assert path.as_posix() in response.detail


def test_get_files_ok_with_file_path_small_file(endpoint_download, endpoint_list):
Expand All @@ -158,7 +161,9 @@ def test_get_files_err_404_path_doesnt_exist(endpoint_download, endpoint_list, f
assert response.status_code == 404

content = response.json()
assert content["detail"] == f"{fake_root / 'fake'} path does not exist"
expected_path = (fake_root / "fake").as_posix()

assert content["detail"] == f"{expected_path} path does not exist"

endpoint_download.assert_not_called()
endpoint_list.assert_called_once()
Expand Down Expand Up @@ -283,9 +288,9 @@ def test_upload_file_err_403_subpath(fake_root):
assert response.status_code == 403

content = response.json()
assert (
content["detail"] == f'You are not allowed to create {fake_root / "../file1.txt"}'
)
expected_path = (fake_root / "../file1.txt").as_posix()

assert content["detail"] == f"You are not allowed to create {expected_path}"


def test_delete_file_ok_file(fake_root):
Expand All @@ -297,7 +302,7 @@ def test_delete_file_ok_file(fake_root):
assert path.exists()

content = response.json()
assert content["message"] == f"{path} deleted correctly"
assert content["message"] == f"{path.as_posix()} deleted correctly"


def test_delete_file_ok_dir(fake_root):
Expand All @@ -309,7 +314,7 @@ def test_delete_file_ok_dir(fake_root):
assert path.exists()

content = response.json()
assert content["message"] == f"{path} deleted correctly"
assert content["message"] == f"{path.as_posix()} deleted correctly"


def test_delete_file_ok_link(fake_root):
Expand All @@ -321,7 +326,7 @@ def test_delete_file_ok_link(fake_root):
assert path.exists()

content = response.json()
assert content["message"] == f"{path} deleted correctly"
assert content["message"] == f"{path.as_posix()} deleted correctly"


def test_delete_file_err_403_subpath(fake_root):
Expand All @@ -333,7 +338,7 @@ def test_delete_file_err_403_subpath(fake_root):
assert not path.exists()

content = response.json()
assert content["detail"] == f"You are not allowed to delete {path}"
assert content["detail"] == f"You are not allowed to delete {path.as_posix()}"


def test_delete_file_err_404_path_doesnt_exist(fake_root):
Expand All @@ -345,7 +350,7 @@ def test_delete_file_err_404_path_doesnt_exist(fake_root):
assert not path.exists()

content = response.json()
assert content["detail"] == f"File at {path} not found"
assert content["detail"] == f"File at {path.as_posix()} not found"


def test_delete_file_err_500_path_not_removed(fake_root):
Expand All @@ -360,4 +365,4 @@ def test_delete_file_err_500_path_not_removed(fake_root):
assert path.exists()

content = response.json()
assert content["detail"] == f"{path} could not be deleted: not in database"
assert content["detail"] == f"{path.as_posix()} could not be deleted: not in database"
8 changes: 7 additions & 1 deletion tests/http/routers/test_root.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import os

from fastapi.testclient import TestClient

from cloudisk.http.config import app
Expand All @@ -9,7 +11,11 @@
def test_root():
response = client.get("/")

with open(CLOUDISK_STATIC / "index.html", "r", encoding="utf-8") as f:
with open(
CLOUDISK_STATIC / "index.html",
encoding="utf-8",
newline=os.linesep,
) as f:
expected = f.read()

assert response.status_code == 200
Expand Down
Loading