diff --git a/.github/workflows/validate.yaml b/.github/workflows/validate.yaml index b496b8256..9f65e91a7 100644 --- a/.github/workflows/validate.yaml +++ b/.github/workflows/validate.yaml @@ -52,6 +52,10 @@ jobs: os: ubuntu-latest TOX_EXTRA_COMMAND: "flake8 --exit-zero rdflib" TOXENV_SUFFIX: "-docs" + PREPARATION: "sudo apt-get install -y firejail" + extensive-tests: true + TOX_TEST_HARNESS: "firejail --net=none --" + TOX_PYTEST_EXTRA_ARGS: "-m 'not webtest'" - python-version: "3.11" os: ubuntu-latest TOXENV_SUFFIX: "-docs" @@ -82,11 +86,15 @@ jobs: uses: arduino/setup-task@v1 with: repo-token: ${{ secrets.GITHUB_TOKEN }} + - name: Run preparation + if: ${{ matrix.PREPARATION }} + shell: bash + run: | + ${{ matrix.PREPARATION }} - name: Run validation shell: bash run: | task \ - TOX_EXTRA_COMMAND="${{ matrix.TOX_EXTRA_COMMAND }}" \ OS=${{ matrix.os }} \ MATRIX_SUFFIX=${{ matrix.suffix }} \ EXTENSIVE=${{ matrix.extensive-tests || 'false' }} \ @@ -96,6 +104,9 @@ jobs: gha:validate env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TOX_PYTEST_EXTRA_ARGS: ${{ matrix.TOX_PYTEST_EXTRA_ARGS }} + TOX_TEST_HARNESS: ${{ matrix.TOX_TEST_HARNESS }} + TOX_EXTRA_COMMAND: ${{ matrix.TOX_EXTRA_COMMAND }} - uses: actions/upload-artifact@v3 if: ${{ (success() || failure()) }} with: diff --git a/Taskfile.yml b/Taskfile.yml index feb7624c2..b2febc570 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -98,7 +98,6 @@ tasks: - echo "TOXENV=${TOXENV}" - | {{if .TOX_PYTEST_ARGS}}TOX_PYTEST_ARGS={{shellQuote .TOX_PYTEST_ARGS}}{{end}} \ - {{if .TOX_EXTRA_COMMAND}}TOX_EXTRA_COMMAND={{shellQuote .TOX_EXTRA_COMMAND}}{{end}} \ {{if .TOX_JUNIT_XML_PREFIX}}TOX_JUNIT_XML_PREFIX={{shellQuote .TOX_JUNIT_XML_PREFIX}}{{end}} \ {{if .COVERAGE_FILE}}COVERAGE_FILE={{shellQuote .COVERAGE_FILE}}{{end}} \ {{.TEST_HARNESS}} \ @@ -359,6 +358,12 @@ tasks: poetry run mypy --show-error-context --show-error-codes -p rdflib poetry run sphinx-build -T -W -b html -d docs/_build/doctree docs docs/_build/html poetry run pytest + + test:no_internet: + desc: Run tests without internet access + cmds: + - | + {{.TEST_HARNESS}}{{.RUN_PREFIX}} firejail --net=none -- pytest -m "not webtest" {{.CLI_ARGS}} _rimraf: # This task is a utility task for recursively removing directories, it is # similar to rm -rf but not identical and it should work wherever there is diff --git a/pyproject.toml b/pyproject.toml index 2b3577a72..bbacb6806 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -155,6 +155,7 @@ addopts = [ "--ignore=rdflib/extras/external_graph_libs.py", "--ignore-glob=docs/*.py", "--doctest-glob=docs/*.rst", + "--strict-markers", ] doctest_optionflags = "ALLOW_UNICODE" filterwarnings = [ @@ -163,6 +164,9 @@ filterwarnings = [ # The below warning is a consequence of how pytest detects fixtures and how DefinedNamespace behaves when an undefined attribute is being accessed. "ignore:Code. _pytestfixturefunction is not defined in namespace .*:UserWarning", ] +markers = [ + "webtest: mark a test as using the internet", +] # log_cli = true # log_cli_level = "DEBUG" log_format = "%(asctime)s.%(msecs)03d %(levelname)-8s %(name)-12s %(filename)s:%(lineno)s:%(funcName)s %(message)s" diff --git a/test/conftest.py b/test/conftest.py index 98fe47385..2f61c9fe3 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -5,10 +5,19 @@ pytest.register_assert_rewrite("test.utils") +from pathlib import Path # noqa: E402 from test.utils.audit import AuditHookDispatcher # noqa: E402 from test.utils.http import ctx_http_server # noqa: E402 from test.utils.httpfileserver import HTTPFileServer # noqa: E402 -from typing import Generator, Optional # noqa: E402 +from typing import ( # noqa: E402 + Collection, + Dict, + Generator, + Iterable, + Optional, + Tuple, + Union, +) from rdflib import Graph @@ -67,3 +76,31 @@ def audit_hook_dispatcher() -> Generator[Optional[AuditHookDispatcher], None, No def exit_stack() -> Generator[ExitStack, None, None]: with ExitStack() as stack: yield stack + + +EXTRA_MARKERS: Dict[ + Tuple[Optional[str], str], Collection[Union[pytest.MarkDecorator, str]] +] = { + ("rdflib/__init__.py", "rdflib"): [pytest.mark.webtest], + ("rdflib/term.py", "rdflib.term.Literal.normalize"): [pytest.mark.webtest], + ("rdflib/extras/infixowl.py", "rdflib.extras.infixowl"): [pytest.mark.webtest], +} + + +PROJECT_ROOT = Path(__file__).parent.parent + + +@pytest.hookimpl(tryfirst=True) +def pytest_collection_modifyitems(items: Iterable[pytest.Item]): + for item in items: + parent_name = ( + str(Path(item.parent.module.__file__).relative_to(PROJECT_ROOT)) + if item.parent is not None + and isinstance(item.parent, pytest.Module) + and item.parent.module is not None + else None + ) + if (parent_name, item.name) in EXTRA_MARKERS: + extra_markers = EXTRA_MARKERS[(parent_name, item.name)] + for extra_marker in extra_markers: + item.add_marker(extra_marker) diff --git a/test/jsonld/test_onedotone.py b/test/jsonld/test_onedotone.py index bfb30ef8e..4c555d1ec 100644 --- a/test/jsonld/test_onedotone.py +++ b/test/jsonld/test_onedotone.py @@ -231,6 +231,10 @@ def global_state(): chdir(old_cwd) +@pytest.mark.webtest +# TODO: apply webtest marker to individual tests +# Marking this whole function as webtest is too broad, as many tests don't +# require the web, but making it narrower requires more refactoring. @pytest.mark.parametrize( "rdf_test_uri, func, suite_base, cat, num, inputpath, expectedpath, context, options", get_test_suite_cases(), diff --git a/test/test_examples.py b/test/test_examples.py index d21d7cc00..9a85de6e2 100644 --- a/test/test_examples.py +++ b/test/test_examples.py @@ -19,6 +19,7 @@ def generate_example_cases() -> Iterable[ParameterSet]: yield pytest.param(example_file, id=f"{example_file.relative_to(EXAMPLES_DIR)}") +@pytest.mark.webtest @pytest.mark.parametrize(["example_file"], generate_example_cases()) def test_example(example_file: Path) -> None: """ diff --git a/test/test_extras/test_infixowl/test_basic.py b/test/test_extras/test_infixowl/test_basic.py index 139238ba8..af9545499 100644 --- a/test/test_extras/test_infixowl/test_basic.py +++ b/test/test_extras/test_infixowl/test_basic.py @@ -1,5 +1,7 @@ from test.data import context0 +import pytest + from rdflib import OWL, Graph, Literal, Namespace from rdflib.extras.infixowl import ( Class, @@ -79,6 +81,7 @@ def test_infixowl_serialization(): ) +@pytest.mark.webtest def test_infix_owl_example1(): g = Graph(identifier=context0) g.bind("ex", EXNS) diff --git a/test/test_extras/test_infixowl/test_context.py b/test/test_extras/test_infixowl/test_context.py index 927785b27..50365ee32 100644 --- a/test/test_extras/test_infixowl/test_context.py +++ b/test/test_extras/test_infixowl/test_context.py @@ -28,6 +28,7 @@ def graph(): del g +@pytest.mark.webtest def test_context(graph): # Now we have an empty graph, we can construct OWL classes in it # using the Python classes defined in this module diff --git a/test/test_sparql/test_service.py b/test/test_sparql/test_service.py index 284565f7e..d83ac32e0 100644 --- a/test/test_sparql/test_service.py +++ b/test/test_sparql/test_service.py @@ -25,6 +25,7 @@ from rdflib.term import BNode, Identifier +@pytest.mark.webtest def test_service(): g = Graph() q = """select ?sameAs ?dbpComment @@ -47,6 +48,7 @@ def test_service(): assert len(r) == 2 +@pytest.mark.webtest def test_service_with_bind(): g = Graph() q = """select ?sameAs ?dbpComment ?subject @@ -69,6 +71,7 @@ def test_service_with_bind(): assert len(r) == 3 +@pytest.mark.webtest def test_service_with_bound_solutions(): g = Graph() g.update( @@ -104,6 +107,7 @@ def test_service_with_bound_solutions(): assert len(r) == 3 +@pytest.mark.webtest def test_service_with_values(): g = Graph() q = """select ?sameAs ?dbpComment ?subject @@ -126,6 +130,7 @@ def test_service_with_values(): assert len(r) == 3 +@pytest.mark.webtest def test_service_with_implicit_select(): g = Graph() q = """select ?s ?p ?o @@ -142,6 +147,7 @@ def test_service_with_implicit_select(): assert len(r) == 3 +@pytest.mark.webtest def test_service_with_implicit_select_and_prefix(): g = Graph() q = """prefix ex: @@ -159,6 +165,7 @@ def test_service_with_implicit_select_and_prefix(): assert len(r) == 3 +@pytest.mark.webtest def test_service_with_implicit_select_and_base(): g = Graph() q = """base @@ -176,6 +183,7 @@ def test_service_with_implicit_select_and_base(): assert len(r) == 3 +@pytest.mark.webtest def test_service_with_implicit_select_and_allcaps(): g = Graph() q = """SELECT ?s @@ -199,6 +207,7 @@ def freeze_bindings( return frozenset(result) +@pytest.mark.webtest def test_simple_not_null(): """Test service returns simple literals not as NULL. @@ -216,6 +225,7 @@ def test_simple_not_null(): assert results.bindings[0].get(Variable("o")) == Literal("c") +@pytest.mark.webtest def test_service_node_types(): """Test if SERVICE properly returns different types of nodes: - URI; diff --git a/tox.ini b/tox.ini index a5b058cb4..73834fc08 100644 --- a/tox.ini +++ b/tox.ini @@ -24,7 +24,7 @@ commands_pre = commands = {env:TOX_EXTRA_COMMAND:} {env:TOX_MYPY_COMMAND:poetry run python -m mypy --show-error-context --show-error-codes --junit-xml=test_reports/{env:TOX_JUNIT_XML_PREFIX:}mypy-junit.xml} - {posargs:poetry run pytest -ra --tb=native {env:TOX_PYTEST_ARGS:--junit-xml=test_reports/{env:TOX_JUNIT_XML_PREFIX:}pytest-junit.xml --cov --cov-report=}} + {posargs:poetry run {env:TOX_TEST_HARNESS:} pytest -ra --tb=native {env:TOX_PYTEST_ARGS:--junit-xml=test_reports/{env:TOX_JUNIT_XML_PREFIX:}pytest-junit.xml --cov --cov-report=} {env:TOX_PYTEST_EXTRA_ARGS:}} docs: poetry run sphinx-build -T -W -b html -d {envdir}/doctree docs docs/_build/html [testenv:covreport]