Skip to content
Open
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
5 changes: 5 additions & 0 deletions .bazelrc
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,8 @@ build:x86_64-qnx --credential_helper=*.qnx.com=%workspace%/tools/qnx_credential_

test:qemu-integration --config=x86_64-qnx
test:qemu-integration --run_under=//scripts:run_under_qemu

build --java_language_version=17
build --java_runtime_version=remotejdk_17
build --tool_java_language_version=17
build --tool_java_runtime_version=remotejdk_17
Comment thread
AlexanderLanin marked this conversation as resolved.
50 changes: 50 additions & 0 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# *******************************************************************************
# Copyright (c) 2026 Contributors to the Eclipse Foundation
#
# See the NOTICE file(s) distributed with this work for additional
# information regarding copyright ownership.
#
# This program and the accompanying materials are made available under the
# terms of the Apache License Version 2.0 which is available at
# https://www.apache.org/licenses/LICENSE-2.0
#
# SPDX-License-Identifier: Apache-2.0
# *******************************************************************************
name: Verify and Build Docs

permissions:
contents: read

on:
pull_request:
types: [opened, reopened, synchronize]
push:
branches:
- main
merge_group:
types: [checks_requested]
release:
Comment thread
AlexanderLanin marked this conversation as resolved.
types: [created]

jobs:
docs-verify:
Comment thread
clanghans marked this conversation as resolved.
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
uses: eclipse-score/cicd-workflows/.github/workflows/docs-verify.yml@c1c90b1a82a1fab0fc202979dde6686b2162d5a8 # v0.0.0
permissions:
pull-requests: write
contents: read
with:
bazel-docs-verify-target: "--lockfile_mode=error //:docs"

docs-build:
needs: [docs-verify]
if: github.event_name != 'pull_request'
uses: eclipse-score/cicd-workflows/.github/workflows/docs.yml@c1c90b1a82a1fab0fc202979dde6686b2162d5a8 # v0.0.0
permissions:
contents: write
pages: write
pull-requests: write
id-token: write
with:
bazel-target: "--lockfile_mode=error //:docs -- --github_user=${{ github.repository_owner }} --github_repo=${{ github.event.repository.name }}"
Comment thread
AlexanderLanin marked this conversation as resolved.
retention-days: 3
1 change: 1 addition & 0 deletions .github/workflows/itf.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
# SPDX-License-Identifier: Apache-2.0
# *******************************************************************************
on:
workflow_call:
pull_request:
types: [opened, reopened, synchronize]
merge_group:
Expand Down
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,9 @@ bazel-*
*__pycache__*
.venv/
*.egg-info/

# Sphinx build output
_build/

# Auto-generated by score_sphinx_bundle
docs/ubproject.toml
5 changes: 5 additions & 0 deletions BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
# *******************************************************************************
load("@rules_python//python:defs.bzl", "py_library")
load("@rules_python//python:pip.bzl", "compile_pip_requirements")
load("@score_docs_as_code//:docs.bzl", "docs")
load("@score_tooling//:defs.bzl", "copyright_checker")

compile_pip_requirements(
Expand Down Expand Up @@ -74,3 +75,7 @@ copyright_checker(
template = "@score_tooling//cr_checker/resources:templates",
visibility = ["//visibility:public"],
)

docs(
source_dir = "docs",
)
7 changes: 7 additions & 0 deletions MODULE.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,13 @@ bazel_dep(name = "score_tooling", version = "1.1.2")
bazel_dep(name = "score_bazel_platforms", version = "0.1.2")
bazel_dep(name = "platforms", version = "1.0.0")

###############################################################################
#
# Documentation dependencies
#
###############################################################################
bazel_dep(name = "score_docs_as_code", version = "4.0.1")

Comment thread
clanghans marked this conversation as resolved.
################################################################################
#
# Load DLT dependencies
Expand Down
163 changes: 163 additions & 0 deletions docs/concepts/architecture.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
..
# *******************************************************************************
# Copyright (c) 2026 Contributors to the Eclipse Foundation
#
# See the NOTICE file(s) distributed with this work for additional
# information regarding copyright ownership.
#
# This program and the accompanying materials are made available under the
# terms of the Apache License Version 2.0 which is available at
# https://www.apache.org/licenses/LICENSE-2.0
#
# SPDX-License-Identifier: Apache-2.0
# *******************************************************************************

.. _itf_architecture:

Architecture
============

This page explains the core design decisions in ITF: the target abstraction
layer, the capability system, the plugin lifecycle, and how ITF integrates
bidirectionally with Bazel.

.. plantuml:: itf_architecture.puml

Target abstraction layer
------------------------

The central concept in ITF is the ``Target``. A target represents the device
or environment under test — a Docker container, a QEMU virtual machine, or
real hardware. All target types expose the same interface, so test code does
not need to know which environment it runs on.

.. code-block:: python

class Target:
def execute(self, command): ...
def upload(self, local_path, remote_path): ...
def download(self, remote_path, local_path): ...
def restart(self): ...
def get_capabilities(self) -> Set[str]: ...

A test that calls ``target.execute("uname -a")`` runs unchanged against a
Docker container or a QEMU VM. The target type is determined at build time by
the ``plugins`` attribute on ``py_itf_test``, and at run time by the CLI
args (e.g. ``--docker-image``) that configure the chosen plugin.

Capability system
-----------------

Different target environments support different operations. A plain Docker
container supports ``exec`` and file transfer but not SSH or SFTP unless an
SSH server is installed. A QEMU VM provides SSH, SFTP, and network-level
operations.

Each ``Target`` subclass declares its capabilities, either by passing them
to the base constructor or by relying on ``Target.REQUIRED_CAPABILITIES``
(``exec``, ``file_transfer``, ``restart``), which is always merged in.
``DockerTarget`` uses only the required capabilities, so it passes no
extras:

.. code-block:: python

class DockerTarget(Target):
def __init__(self, container):
super().__init__() # capabilities come from REQUIRED_CAPABILITIES
self.container = container

Tests can be guarded against targets that lack required capabilities using the
``@requires_capabilities`` decorator:

.. code-block:: python

from score.itf.plugins.core import requires_capabilities

@requires_capabilities("ssh", "sftp")
def test_file_roundtrip(target):
...

If the active target does not provide all listed capabilities, pytest skips the
test with a clear message. This keeps test suites portable: the same file can
run against Docker for fast feedback and against a QEMU VM for full-system
integration, skipping tests that do not apply.

Tests can also query capabilities at runtime and branch accordingly:

.. code-block:: python

def test_adaptive(target):
if target.has_capability("ssh"):
with target.ssh() as ssh:
ssh.execute_command("echo hello")
else:
exit_code, _ = target.execute("echo hello")

Plugin lifecycle
----------------

Each plugin contributes a ``target_init`` pytest fixture. ITF's core plugin
Comment thread
clanghans marked this conversation as resolved.
calls this fixture to obtain the target instance, then wraps it in the
``target`` fixture that test functions receive.

The lifecycle for a single test is:

1. **Setup**: The plugin's ``target_init`` fixture starts the target
(spins up a container, boots a QEMU VM, connects to hardware).
2. **Test execution**: The test function receives the ``target`` fixture
and exercises the system under test.
3. **Teardown**: ``target_init`` tears down the target (stops the
container, shuts down the VM).

With ``--keep-target``, steps 1 and 3 run once per session instead of once
per test function. This is faster but means tests share target state, so it
should only be used when tests are designed to be order-independent.

**Plugin loading order is deterministic but should not be relied upon.**
The core plugin is always registered first. The remaining plugins are
registered with pytest in the exact order they are listed in
``py_itf_test.plugins``. While this order is stable, plugins are designed
to be independent of each other — no plugin should depend on another
plugin's initialisation having completed first.

Why a plugin-based design
--------------------------

Plugin-based design was chosen for three reasons:

**Separation of concerns.** Target management logic (starting containers,
booting VMs) is entirely isolated from test logic. A test that calls
``target.execute()`` has no dependency on Docker or QEMU APIs.

**Extensibility without forking.** Custom targets (real hardware, emulators,
cloud VMs) are added by implementing ``Target`` and ``target_init`` in a new
plugin. No changes to the ITF core are needed.

**Bazel-native composition.** Because plugins are declared as Bazel targets
with ``py_itf_plugin``, they carry their own Python libraries, data files,
and CLI args. Combining plugins — for example Docker + DLT — is as simple as
listing both labels in ``py_itf_test.plugins``. Bazel resolves transitive
dependencies automatically.

Bidirectional Bazel integration
---------------------------------

ITF integrates with Bazel in both directions:

**Build-time** (Bazel → ITF): The ``py_itf_test`` symbolic macro creates a
``py_test`` binary that bundles the test code and all plugin Python
libraries. Plugin CLI args (e.g. ``--docker-image``, paths from
``$(location ...)``) are resolved at analysis time and baked into a launcher
script. This means test hermetically carry their full dependency graph,
including container images or QEMU images referenced via Bazel labels.

**Run-time** (ITF → Bazel): ITF uses Bazel's runfiles mechanism to locate
data files at runtime. The ``$(location ...)`` substitution in ``args``
produces runfiles-relative paths that work regardless of where Bazel places
files in the output tree. Test results are reported via JUnit XML to
``$XML_OUTPUT_FILE``, integrating with Bazel's native test reporting and
caching.

This design means ITF tests participate fully in Bazel's incremental build
and caching: a test is only re-run if its source, its dependencies, or its
configuration changes.
25 changes: 25 additions & 0 deletions docs/concepts/index.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
..
# *******************************************************************************
# Copyright (c) 2026 Contributors to the Eclipse Foundation
#
# See the NOTICE file(s) distributed with this work for additional
# information regarding copyright ownership.
#
# This program and the accompanying materials are made available under the
# terms of the Apache License Version 2.0 which is available at
# https://www.apache.org/licenses/LICENSE-2.0
#
# SPDX-License-Identifier: Apache-2.0
# *******************************************************************************

.. _itf_concepts:

Concepts
========

Architecture, design decisions, and explanatory material for ITF.
Comment thread
clanghans marked this conversation as resolved.

.. toctree::
:maxdepth: 1

architecture
61 changes: 61 additions & 0 deletions docs/concepts/itf_architecture.puml
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
@startuml ITF Architecture
!pragma layout smetana
skinparam componentStyle rectangle
skinparam defaultFontName sans-serif
skinparam linetype ortho

' ── Test layer ────────────────────────────────────────────────────────────────
package "Test Code" {
component "test_*.py\n(pytest test function)" as TestFn
component "@requires_capabilities\n(optional guard)" as RC
}

' ── ITF Core ──────────────────────────────────────────────────────────────────
package "ITF Core (score.itf.plugins.core)" {
component "target fixture" as TargetFx
component "target_init fixture\n(plugin hook)" as TargetInit
}

' ── Target abstraction ────────────────────────────────────────────────────────
package "Target Abstraction" {
component "Target (base class)\n──────────────────\nexecute(cmd)\nupload(src, dst)\ndownload(src, dst)\nrestart()\nhas_capability(cap)\nget_capabilities() : Set[str]" as Target
}

' ── Plugin implementations ────────────────────────────────────────────────────
package "Plugins" {
component "Docker Plugin\n──────────\ncapabilities:\nexec | file_transfer\nrestart" as DockerPlugin

component "QEMU Plugin\n──────────\ncapabilities:\nssh | sftp | exec\nfile_transfer | restart" as QemuPlugin

component "DLT Plugin\n──────────\nDltWindow\ndlt_config fixture" as DltPlugin
}

' ── Infrastructure ────────────────────────────────────────────────────────────
package "Infrastructure" {
database "Docker Daemon" as DockerInfra
database "QEMU VM" as QemuInfra
}

' ── Bazel build layer ─────────────────────────────────────────────────────────
package "Bazel Build" {
component "py_itf_test\n(symbolic macro)" as PyItfTest
component "py_itf_plugin\n(rule)" as PyItfPlugin
}

' ── Relationships ─────────────────────────────────────────────────────────────
TestFn -down-> TargetFx : receives
TestFn .right.> RC : guarded by
TargetFx -down-> TargetInit : delegates to
TargetInit -down-> Target : yields instance of
DockerPlugin -up--|> Target : implements
QemuPlugin -up--|> Target : implements

DockerPlugin -down-> DockerInfra : manages lifecycle
QemuPlugin -down-> QemuInfra : manages lifecycle

TestFn ..> DltPlugin : optional\nDltWindow usage

PyItfTest -right-> PyItfPlugin : composes plugins
PyItfPlugin -up-> TargetInit : provides\ntarget_init fixture

@enduml
19 changes: 19 additions & 0 deletions docs/conf.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# *******************************************************************************
# Copyright (c) 2026 Contributors to the Eclipse Foundation
#
# See the NOTICE file(s) distributed with this work for additional
# information regarding copyright ownership.
#
# This program and the accompanying materials are made available under the
# terms of the Apache License Version 2.0 which is available at
# https://www.apache.org/licenses/LICENSE-2.0
#
# SPDX-License-Identifier: Apache-2.0
# *******************************************************************************

project = "Score ITF"
project_url = "https://github.com/eclipse-score/itf"

extensions = [
"score_sphinx_bundle",
]
Loading