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
1 change: 0 additions & 1 deletion bazel/py_itf_test.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
# *******************************************************************************
"""Bazel interface for running pytest"""

load("@itf_pip//:requirements.bzl", "requirement")
load("@rules_python//python:defs.bzl", "py_test")
load("@score_itf//bazel/rules:run_as_exec.bzl", "test_as_exec")

Expand Down
27 changes: 27 additions & 0 deletions score/itf/core/target/BUILD
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# *******************************************************************************
# 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
# *******************************************************************************

load("@itf_pip//:requirements.bzl", "requirement")
load("@rules_python//python:defs.bzl", "py_library")

py_library(
name = "target",
srcs = [
"__init__.py",
"target.py",
],
visibility = ["//visibility:public"],
deps = [
requirement("typing-extensions"),
],
)
14 changes: 14 additions & 0 deletions score/itf/core/target/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# *******************************************************************************
# 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
# *******************************************************************************

from .target import Target
102 changes: 102 additions & 0 deletions score/itf/core/target/target.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
# *******************************************************************************
# 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
# *******************************************************************************

from typing import Set, Optional, Any, Dict


class Target:
"""Base target interface for test implementations.

This class provides a common interface for targets that tests can implement against.
Targets support a capability-based system that allows tests to query and make
decisions based on available capabilities.

Example:
class MyTarget(Target):
def __init__(self):
super().__init__(capabilities={'ssh', 'file_transfer'})

def execute(self, command):
# Implementation specific logic
pass

# In a test:
if target.has_capability('ssh'):
target.execute('ls -la')
"""

def __init__(self, capabilities: Optional[Set[str]] = None):
"""Initialize the target with a set of capabilities.

Args:
capabilities: Set of capability identifiers supported by this target.
If None, an empty set is used.
"""
self._capabilities: Set[str] = capabilities if capabilities is not None else set()

def has_capability(self, capability: str) -> bool:
"""Check if the target supports a specific capability.

Args:
capability: The capability identifier to check.

Returns:
True if the capability is supported, False otherwise.
"""
return capability in self._capabilities

def has_all_capabilities(self, capabilities: Set[str]) -> bool:
"""Check if the target supports all of the specified capabilities.

Args:
capabilities: Set of capability identifiers to check.

Returns:
True if all capabilities are supported, False otherwise.
"""
return capabilities.issubset(self._capabilities)

def has_any_capability(self, capabilities: Set[str]) -> bool:
"""Check if the target supports any of the specified capabilities.

Args:
capabilities: Set of capability identifiers to check.

Returns:
True if at least one capability is supported, False otherwise.
"""
return bool(capabilities.intersection(self._capabilities))

def get_capabilities(self) -> Set[str]:
"""Get all capabilities supported by this target.

Returns:
Set of all capability identifiers.
"""
return self._capabilities.copy()

def add_capability(self, capability: str) -> None:
"""Add a capability to the target.

Args:
capability: The capability identifier to add.
"""
self._capabilities.add(capability)

def remove_capability(self, capability: str) -> None:
"""Remove a capability from the target.

Args:
capability: The capability identifier to remove.
"""
self._capabilities.discard(capability)
3 changes: 3 additions & 0 deletions score/itf/plugins/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ py_library(
srcs = ["core.py"],
visibility = ["//visibility:public"],
deps = [
"//score/itf/core/target",
requirement("pytest"),
],
)
Expand All @@ -27,6 +28,8 @@ py_library(
srcs = ["docker.py"],
visibility = ["//visibility:public"],
deps = [
":core",
"//score/itf/core/target",
requirement("docker"),
requirement("pytest"),
],
Expand Down
46 changes: 46 additions & 0 deletions score/itf/plugins/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@
#
# SPDX-License-Identifier: Apache-2.0
# *******************************************************************************

import pytest
import functools

from score.itf.core.target import Target


def pytest_addoption(parser):
Expand All @@ -32,10 +36,52 @@ def determine_target_scope(fixture_name, config):
return "function"


@pytest.fixture(scope=determine_target_scope)
def target_init():
"""Fixture to initialize the target.

Plugins need to implement this fixture to provide the actual target instance.
The scope of this fixture is determined by the --keep-target command line option.
"""
yield Target()


@pytest.fixture(scope=determine_target_scope)
def target(target_init):
"""Use automatic fixture resolution

Plugins need to define a pytest fixture 'target_init'
"""
yield target_init


def requires_capabilities(*capabilities):
"""Decorator to skip tests if target doesn't have all required capabilities.

Args:
*capabilities: Variable number of capability identifiers required for the test.

Example:
@requires_capabilities("exec", "ssh")
def test_remote_command(target):
target.exec_run("ls -la")
"""

def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
target = kwargs.get("target")
if target is None:
for arg in args:
if hasattr(arg, "has_all_capabilities"):
target = arg
break

if target and not target.has_all_capabilities(set(capabilities)):
pytest.skip(f"Target missing required capabilities: {capabilities}")

return func(*args, **kwargs)

return wrapper

return decorator
1 change: 1 addition & 0 deletions score/itf/plugins/dlt/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

from score.itf.core.utils.bunch import Bunch
from score.itf.plugins.core import determine_target_scope
from score.itf.plugins.dlt.dlt_receive import DltReceive, Protocol


def pytest_addoption(parser):
Expand Down
15 changes: 14 additions & 1 deletion score/itf/plugins/docker.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import pytest

from score.itf.plugins.core import determine_target_scope
from score.itf.plugins.core import Target


logger = logging.getLogger(__name__)
Expand All @@ -36,6 +37,18 @@ def pytest_addoption(parser):
)


DOCKER_CAPABILITIES = ["exec"]


class DockerTarget(Target):
def __init__(self, container, capabilities=DOCKER_CAPABILITIES):
super().__init__(capabilities=capabilities)
self.container = container

def __getattr__(self, name):
return getattr(self.container, name)


@pytest.fixture(scope=determine_target_scope)
def target_init(request):
docker_image_bootstrap = request.config.getoption("docker_image_bootstrap")
Expand All @@ -46,5 +59,5 @@ def target_init(request):
docker_image = request.config.getoption("docker_image")
client = pypi_docker.from_env()
container = client.containers.run(docker_image, "sleep infinity", detach=True, auto_remove=True, init=True)
yield container
yield DockerTarget(container)
container.stop(timeout=1)
12 changes: 12 additions & 0 deletions test/test_docker.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
# SPDX-License-Identifier: Apache-2.0
# *******************************************************************************

import score.itf


def check_command_exec(target, message):
exit_code, output = target.exec_run(f"echo -n {message}")
Expand All @@ -23,3 +25,13 @@ def test_docker_runs_1(target):

def test_docker_runs_2(target):
assert check_command_exec(target, "hello, world 1")


@score.itf.plugins.core.requires_capabilities("exec")
def test_docker_runs_for_exec_capability(target):
assert check_command_exec(target, "hello, world 1")


@score.itf.plugins.core.requires_capabilities("non-existing-capability")
def test_docker_skipped_for_non_existing_capability(target):
assert False, "This test should have been skipped due to missing capability"
8 changes: 8 additions & 0 deletions test/test_rules_are_working_correctly.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,11 @@
# *******************************************************************************
def test_local_fixture_has_correct_value(fixture42):
assert 42 == fixture42


def test_target_fixture_is_available(target):
assert target is not None


def test_target_fixture_has_no_capabilities_by_default(target):
assert target.get_capabilities() == set()