Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
96 commits
Select commit Hold shift + click to select a range
9ef20fc
feat: Models the core contract needs
Oct 2, 2026
771d48a
Feat: Protocols and Exceptions added for the core contract
Oct 2, 2026
5fe30c2
Feat: Private protocols for the BatchBackend
Oct 2, 2026
89d0bc9
feat: a fake backend to run unit tests against
Oct 6, 2026
af4c436
fix: some code cleanup in the helper functions
Oct 6, 2026
99f82ae
feat: setup for the unit test suite
Oct 6, 2026
e041acf
chore: style fix
Oct 6, 2026
d287307
chore: styling, unit test fixture
Oct 6, 2026
e830545
chore: single letter var `h` refering to hanlde changed to `handle`
Oct 6, 2026
be1e998
fix: unused parameter in `test_injected_failure_is_reported_per_job()…
Oct 6, 2026
2aeb589
chore: Making room in local fork for my documentation so i can easily…
Oct 7, 2026
5024ce9
fix: placed in the wrong folder :/
Oct 7, 2026
2c32012
chore: added testplan
Oct 7, 2026
3bf4210
feat: Models the core contract needs
Oct 2, 2026
0a6a9e1
Feat: Protocols and Exceptions added for the core contract
Oct 2, 2026
36cf7df
Feat: Private protocols for the BatchBackend
Oct 2, 2026
357050e
feat: a fake backend to run unit tests against
Oct 6, 2026
56bce86
fix: some code cleanup in the helper functions
Oct 6, 2026
5923545
feat: setup for the unit test suite
Oct 6, 2026
1f92acf
chore: style fix
Oct 6, 2026
f7cf50c
chore: styling, unit test fixture
Oct 6, 2026
bab3989
chore: single letter var `h` refering to hanlde changed to `handle`
Oct 6, 2026
eb3c644
fix: unused parameter in `test_injected_failure_is_reported_per_job()…
Oct 6, 2026
f727cbb
Merge branch 'feat/core-contract' of github.com:Alexander-Auer/interc…
Oct 7, 2026
feaf5e7
fix: made the OutputSpec optional as it is not suppoerted by all and …
Oct 7, 2026
46fd1a3
Feat: Core contract issue #16 (#1)
Alexander-Auer Oct 7, 2026
42dce34
fix: These documents should not exist in this way in the main repository
Oct 7, 2026
d67d380
fix: None check for the outputs in submissionspec
Oct 7, 2026
910b0b0
chore: trailing white spaces
Oct 7, 2026
a9fcca0
chore: fixed pre commit locally
Oct 7, 2026
74c40a7
chore: removed pdf, as they just take up space and no one will use th…
Oct 9, 2026
5cb1ee3
fix: JobHandle key is now a combination of the handle's fields instea…
Oct 19, 2026
b6af530
Merge branch 'main' into feat/core-contract
Alexander-Auer Oct 19, 2026
f90cdd4
feat: backend configuration and a closed registry
Alexander-Auer Oct 19, 2026
9dac6f5
test: expand unit test coverage to every logical case
Alexander-Auer Oct 20, 2026
0338aa2
feat: credential models and provider-based supply
Alexander-Auer Oct 20, 2026
ceed1df
Merge branch 'main' into feat/core-contract
Alexander-Auer Oct 23, 2026
d310fe8
test: trim the unit tests to logical cases that can happen
Oct 23, 2026
529fa75
Merge branch 'feat/core-contract' into stack/registry
Oct 23, 2026
5d94d0f
test: trim the registry and configuration tests to logical cases
Oct 23, 2026
60906ef
Merge branch 'stack/registry' into stack/test-expansion
Oct 23, 2026
ae6ca0a
test: trim the expanded tests to logical cases that can happen
Oct 23, 2026
359ece8
Merge branch 'stack/test-expansion' into stack/credential-models
Oct 23, 2026
f7e5eea
test: trim the credential tests to logical cases that can happen
Oct 23, 2026
a644ce6
docs: follow the Diátaxis structure for the user documentation
Oct 23, 2026
b6e172f
fix: address the review of the core contract
Oct 23, 2026
bc5619e
Merge branch 'feat/core-contract' into stack/registry
Oct 23, 2026
7391fbe
fix: no references to the ADRs in the registry and configuration
Oct 23, 2026
9c89415
Merge branch 'stack/registry' into stack/test-expansion
Oct 23, 2026
673cfa4
Merge branch 'stack/test-expansion' into stack/credential-models
Oct 23, 2026
ceb871e
fix: no references to the ADRs in the credential code
Oct 23, 2026
a0211b4
Merge DIRACGrid main into stack/registry (the core contract, squash-m…
Oct 26, 2026
5d10b5a
fix: apply the core contract review to the registry
Oct 26, 2026
7d86596
refactor: rename registry.backend() to get_backend()
Oct 26, 2026
b4fb7bd
refactor!: one public configuration model per backend kind
Oct 26, 2026
03efb2a
docs: generate the configuration reference from the models
Oct 26, 2026
2191feb
Merge branch 'stack/registry' into stack/test-expansion
Oct 26, 2026
929ea61
test: remove leftovers of the flat configuration model
Oct 26, 2026
ce1c811
Merge branch 'stack/test-expansion' into stack/credential-models
Oct 26, 2026
af2109f
fix: Rename FakeSettings to FakeConfig to stay in line with the revie…
Oct 26, 2026
23641f6
Merge remote-tracking branch 'origin/stack/registry' into stack/test-…
Oct 27, 2026
6dca5ef
test: keep the existing model tests where they are
Oct 27, 2026
d7e045f
Merge DIRACGrid main into stack/test-expansion (the registry, squash-…
Oct 27, 2026
b94e917
test: a configuration that is not a mapping is refused
Oct 27, 2026
66af4f2
Merge branch 'stack/test-expansion' into stack/credential-models
Oct 27, 2026
e55693f
Merge DIRACGrid main into stack/credential-models (the test expansion…
Oct 27, 2026
ca8d39f
fix: credential errors never repeat the value they were given
Oct 27, 2026
eccfbc6
fix: a provider's error text stays out of the AuthenticationError
Oct 27, 2026
8e2f76c
docs: document the credential fields in the API reference
Oct 27, 2026
2670cfb
docs: Google-style docstrings for the credential functions
Oct 27, 2026
d5f2216
Merge stack/htcondor-1-core: the shared HTCondor core
Nov 3, 2026
42f1a62
feat: an HTCondor-CE configuration can name its schedd
Nov 3, 2026
62b44b3
ci: run integration tests side by side and name what is skipped
Nov 3, 2026
25f97c9
fix: a local input that cannot be read is refused on submit
Nov 3, 2026
3198fa6
test: the htcondor integration stack
Nov 3, 2026
715810a
test: give each backend its own folder in the unit tests
Nov 3, 2026
815fc7f
feat: the HTCondor-CE backend's building blocks
Nov 3, 2026
98516ed
Merge stack/htcondor-2-integration-stack: the stack the suite runs on
Nov 3, 2026
082e2f6
feat: HTCondor submit descriptions in the shared core
Nov 3, 2026
3bee45d
feat: HTCondorCEBackend with submit and status
Nov 3, 2026
2f12e30
feat: kill and purge on the HTCondor-CE backend
Nov 3, 2026
c35d60f
test: the shared contract suite, on the htcondor stack
Nov 3, 2026
8c5128a
feat: HTCondor commands and the state map in the shared core
Nov 3, 2026
296cd9b
test: HTCondor-CE submit and status against simulated tools
Nov 3, 2026
b84ab11
test: purge releases a job, it does not have to forget it
Nov 3, 2026
5af5519
feat: collect output from the HTCondor-CE backend
Nov 3, 2026
fd8b2fe
docs: the shared suite replaces the phase 0 integration tests
Nov 3, 2026
89c3b94
fix: Removed the x509 proxy as it will not be used anymore, and also …
Nov 4, 2026
e3fc956
Merge branch 'main' into stack/credential-models
Alexander-Auer Nov 4, 2026
be3a762
fix: no X.509 proxy in HTCondor submit descriptions
Nov 4, 2026
34b0061
Merge branch 'stack/htcondor-1-core' into stack/htcondor-3-submit-status
Nov 4, 2026
1ab3f31
Merge remote-tracking branch 'origin/stack/credential-models' into st…
Nov 4, 2026
938b543
fix: the HTCondor-CE backend authenticates with one token
Nov 4, 2026
c85eda0
Merge branch 'stack/htcondor-3-submit-status' into stack/htcondor-4-k…
Nov 4, 2026
87fbce3
Merge branch 'stack/htcondor-4-kill-purge' into stack/htcondor-5-output
Nov 4, 2026
0405f83
Merge branch 'stack/htcondor-5-output' into stack/htcondor-6-contract…
Nov 4, 2026
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
7 changes: 5 additions & 2 deletions .github/workflows/integration.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ on:
- ".github/workflows/integration.yml"
- "tests/conftest.py"
- "pyproject.toml"
- "src/**"

permissions:
contents: read
Expand Down Expand Up @@ -66,10 +67,12 @@ jobs:
# TODO(#13/#14): ship the suite in the image and exec as an unprivileged user.
runner=(docker compose -f "$COMPOSE_FILE" exec -T backend pytest)
else
runner=(pixi run -e py313 pytest)
# Independent tests, run side by side (stacks.yml `workers`).
runner=(pixi run -e py313 pytest -n "${{ matrix.workers }}")
fi
set +e
"${runner[@]}" tests/integration -m "${{ matrix.markers }}" \
# -rs --no-fold-skipped: name every skipped test and why.
"${runner[@]}" tests/integration -m "${{ matrix.markers }}" -rs --no-fold-skipped \
--stack="${{ matrix.stack }}" --config="${{ matrix.config }}"
rc=$?
# Exit 5 = no tests selected; tolerated until the contract suite lands.
Expand Down
19 changes: 11 additions & 8 deletions docs/dev/explanations/integration-testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,17 @@ know which version caused the problem.

Some backends do not support every feature. The tests check for a feature in the same way that
users of the library do (`isinstance` against a `runtime_checkable` protocol), and skip the test
when the backend does not have it. The helper for this will be added in phase 2, together with the
first tests that use it.
when the backend does not have it. A test asks for a capability by taking its fixture
(`output_retriever`, `killable`, `purgeable`).

## Phases

- **Phase 0 (now).** The interCEde protocols do not exist yet. Each stack is tested with the
backend's own command-line tools (`arcsub`, `arcstat`, `arcget`, `condor_submit`, `sbatch`).
- **Phase 2.** One set of tests, written against the interCEde protocols, runs on every stack
(`test_submit.py`, `test_status.py`, `test_output.py`, `test_kill.py`). It replaces the phase 0
tests. Behaviour that only one backend has (for example HTCondor's one-time output download)
gets its own test and marker (`destructive_fetch`), not an `if` inside a shared test.
- **Phase 0.** Before the interCEde protocols existed, a stack was tested with the backend's own
command-line tools (`arcsub`, `arcstat`, `arcget`, `condor_submit`, `sbatch`).
- **Phase 2 (now).** One set of tests, written against the interCEde protocols, runs on every stack
(`test_submit.py`, `test_status.py`, `test_output.py`, `test_kill.py`, `test_purge.py`, and
`test_errors.py` for what a consumer sees when things go wrong). These tests carry the
`contract` marker, which selects them whatever a stack's `markers` say. A new backend writes no
tests of its own: its stack runs this suite. Behaviour that only one backend has (for example
HTCondor's one-time output download) gets its own test and marker (`destructive_fetch`), not an
`if` inside a shared test.
7 changes: 4 additions & 3 deletions docs/dev/how-to/add-an-integration-stack.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ You do not need to change any CI workflow. CI finds the stack in `stacks.yml`.

```sh
docker compose -f tests/integration/stacks/<id>/compose.yml up --wait
pixi run -e py313 pytest tests/integration -m "<markers>" --stack=<id> --config=basic
pixi run -e py313 pytest tests/integration -n 4 -m "<markers>" --stack=<id> --config=basic
pixi run -e py313 pytest tests/integration/test_manifest.py
```

Expand All @@ -35,8 +35,9 @@ You do not need to change any CI workflow. CI finds the stack in `stacks.yml`.
- [ ] The images use Docker, not Podman.
- [ ] `intercede-client.toml` is filled in: `id`, endpoint, queue, credential type and path.
- [ ] `pytest tests/integration/test_manifest.py` passes.
- [ ] At least one test runs for this stack (phase 0: a test that uses the backend's own
command-line tools). Without one, the CI job passes but tests nothing.
- [ ] The shared suite passes on this stack (`pytest tests/integration -m "<markers>" --stack=<id>`).
You do not write tests for a new backend: the stack runs the `contract` tests, skipping
those for capabilities the backend does not have.
- [ ] If your pull request had its own workflow for this stack, remove it. `integration.yml` runs
on every pull request that changes `tests/integration/`. It does not block merging yet
(#14), so keep your own workflow only if you need a check that blocks merging.
5 changes: 4 additions & 1 deletion docs/dev/how-to/run-integration-tests-locally.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,12 @@ CI runs the same commands as below, so you can reproduce any CI job on your mach
2. Run the tests. Copy `<markers>` from the stack's entry in `stacks.yml`:

```sh
pixi run -e py313 pytest tests/integration -m "<markers>" --stack=<id> --config=basic
pixi run -e py313 pytest tests/integration -n 4 -m "<markers>" --stack=<id> --config=basic
```

`-n 4` runs four tests at the same time, as CI does. Use the stack's `workers` value from
`stacks.yml` if it sets one, or leave `-n` out to run one test at a time while debugging.

For stacks with `exec_in_container: true` (the `local-*` stacks), run the tests inside the
container instead:

Expand Down
16 changes: 14 additions & 2 deletions docs/dev/reference/integration-stacks.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ tests/integration/
| `versions` | no | Backend versions to test, for example `[lts, latest]`. Default: `[latest]`. |
| `markers` | yes | `pytest -m` expression that selects the tests for this stack. |
| `exec_in_container` | no | `true`: run pytest inside the backend container. Default: `false`. |
| `workers` | no | How many tests run at the same time (pytest-xdist). Default: `4`. Set `1` for a backend that cannot take several jobs at once. |

## When a stack gets CI jobs

Expand Down Expand Up @@ -112,9 +113,20 @@ Defined in `tests/conftest.py` (options) and `tests/integration/conftest.py` (fi
| `stack` | fixture | The stack's entry from `stacks.yml`. |
| `stack_dir` | fixture | Folder with the stack's `compose.yml`. |
| `client_config` | fixture | The stack's `intercede-client.toml`, already parsed. |
| `refusals` | fixture | What `[refusals]` in `intercede-client.toml` says the backend cannot honour: `containers`, `url_inputs`, `url_outputs`. The shared suite checks each is refused at submit. |
| `credentials` | fixture | A credential provider holding what `[credentials]` points to (types: `token`). |
| `backend` | fixture | The backend `[backend]` describes, built by the registry and open. |
| `credential` | fixture | The credential `[credentials]` points to, read from the stack's credentials folder. |
| `rejected_credentials` | fixture | A provider holding a credential of the right kind that the resource rejects. |
| `unreachable_config` | fixture | `[backend]`, pointed at a closed port, as when the resource is down. |
| `output_retriever`, `killable`, `purgeable` | fixture | The backend as that capability; skips the test when the backend does not have it. |

If `intercede-client.toml` is missing, the test fails in CI (when `$CI` is set) and is skipped
locally. In CI, a job where every test is skipped would look green but test nothing.

Markers, defined in `pyproject.toml`: `remote`, `scheduler`, `arc`, `htcondor`, `slurm`, `ssh`,
`destructive_fetch`.
Markers, defined in `pyproject.toml`: `contract`, `remote`, `scheduler`, `arc`, `htcondor`,
`slurm`, `ssh`, `destructive_fetch`.

Tests marked `contract` are the shared suite and run on every stack: with `--stack`, they get the
markers that the stack's `markers` expression asks for, so `-m "<markers>"` selects them. The
other markers select tests for behaviour only some backends have.
8 changes: 8 additions & 0 deletions docs/user/reference/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,14 @@ Anything else, including every name starting with `_`, is internal.

::: intercede.registry

## Credentials

::: intercede.auth.models

::: intercede.auth.protocols

::: intercede.auth.providers

## Exceptions

::: intercede.exceptions
6 changes: 6 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ markers = [
"backend_only: restrict a test to the named backend fixture(s)",
"xfail_backend: strict xfail a test on the named backend fixture(s)",
# integration suite (selected per stack in tests/integration/stacks.yml)
"contract: the shared integration suite, run on every stack whatever its markers",
"remote: drives a remote CE backend (grid gateway: ARC, HTCondor-CE)",
"scheduler: drives a Transport x Scheduler composition (BatchBackend)",
"arc: requires an ARC CE",
Expand Down Expand Up @@ -144,6 +145,11 @@ py313 = { features = ["testing", "py313"] }
py314 = { features = ["testing", "py314"] }
docs = { features = ["docs"], solve-group = "default" }

[tool.pixi.feature.testing.dependencies]
# The HTCondor tools (condor_submit, condor_q, ...) the htcondor-ce backend runs;
# the integration suite drives the backend against a real CE with them.
htcondor-utils = "*"

[tool.pixi.feature.testing.pypi-dependencies]
intercede = { path = ".", editable = true }

Expand Down
11 changes: 11 additions & 0 deletions src/intercede/_backends/_htcondor/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
"""What interCEde knows about HTCondor, shared by every backend that drives it.

Plain functions and data only, with no dependency on any backend class: the
HTCondor-CE backend uses them directly, and the HTCondor scheduler of the
composed backend (issue #21) will use the same ones over its transport. They
share these functions, never a subtype.

- ``submit``: a submit description built from a ``SubmissionSpec``
- ``commands``: the argv of each HTCondor tool, and parsers for their output
- ``status``: the state mapping from job ads to ``JobStatus``
"""
117 changes: 117 additions & 0 deletions src/intercede/_backends/_htcondor/commands.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
"""The argv of each HTCondor tool, and parsers for what they print.

``remote`` is how a tool reaches its schedd: ``schedd_options(pool, name)``
for a remote schedd, as an HTCondor-CE is reached, or ``()`` for the schedd
of the host the tool runs on, as the composed backend reaches it.
"""

from __future__ import annotations

import re
from collections.abc import Sequence

from intercede.models import JobID

# Ids per command line, so a status poll of thousands of jobs stays bounded.
CHUNK = 100


def schedd_options(pool: str, name: str) -> list[str]:
"""Reach the schedd ``name`` through the collector ``pool`` (host:port)."""
return ["-pool", pool, "-name", name]


def submit(
description: str, pool: str | None = None, name: str | None = None
) -> list[str]:
"""Submit a description file and spool its inputs to the schedd.

Spooling copies the inputs at submit time, so nothing on the submitting
host is needed afterwards. With ``pool`` and ``name`` the schedd is a
remote one (``-remote`` implies spooling); without, it is the local one.
"""
if (pool is None) != (name is None):
raise ValueError("a remote schedd needs both pool and name")

command = ["condor_submit", "-terse"]

if pool is None or name is None:
command.append("-spool")
else:
command.extend(["-pool", pool, "-remote", name])

command.append(description)
return command


def query(
ids: Sequence[JobID], attributes: Sequence[str], remote: Sequence[str]
) -> list[str]:
"""Read the ads of jobs still in the queue."""
return ["condor_q", *remote, *ids, "-attributes", ",".join(attributes), "-json"]


def history(
ids: Sequence[JobID], attributes: Sequence[str], remote: Sequence[str]
) -> list[str]:
"""Read the ads of jobs that have left the queue."""
return [
"condor_history",
*remote,
*ids,
"-attributes",
",".join(attributes),
"-json",
]


def remove(ids: Sequence[JobID], remote: Sequence[str]) -> list[str]:
"""Remove jobs: cancels active ones, releases finished ones."""
return ["condor_rm", *remote, *ids]


def transfer_data(ids: Sequence[JobID], remote: Sequence[str]) -> list[str]:
"""Fetch the spooled output of jobs into the directory they were submitted from."""
return ["condor_transfer_data", *remote, *ids]


def chunks(ids: Sequence[JobID]) -> list[Sequence[JobID]]:
"""Split ids into groups of at most ``CHUNK``."""
return [ids[start : start + CHUNK] for start in range(0, len(ids), CHUNK)]


_TERSE = re.compile(
r"\s*(?P<cluster>\d+)\.(?P<first>\d+)"
r"\s*-\s*"
r"(?P<last_cluster>\d+)\.(?P<last>\d+)\s*"
)


def parse_submit(raw: str) -> list[JobID]:
"""Parse job IDs from condor_submit -terse output, e.g. '12.0 - 12.3'."""
lines = [line for line in raw.splitlines() if line.strip()]

if len(lines) != 1:
raise ValueError(f"unexpected condor_submit output: {raw.strip()!r}")

match = _TERSE.fullmatch(lines[0])
if match is None:
raise ValueError(f"unexpected condor_submit output: {raw.strip()!r}")

cluster = int(match["cluster"])
first = int(match["first"])
last_cluster = int(match["last_cluster"])
last = int(match["last"])

if cluster != last_cluster or last < first:
raise ValueError(f"unexpected condor_submit output: {raw.strip()!r}")

return [JobID(f"{cluster}.{proc}") for proc in range(first, last + 1)]


_REMOVED = re.compile(r"^Job (\d+\.\d+) marked for removal", re.MULTILINE)


def parse_remove(raw: str) -> set[JobID]:
"""Return the ids ``condor_rm`` reports as removed; any other id was not."""
return {JobID(job) for job in _REMOVED.findall(raw)}
Loading
Loading