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
12 changes: 9 additions & 3 deletions .github/workflows/proto-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ jobs:
fetch-depth: 0

- uses: bufbuild/buf-setup-action@v1
with:
version: "1.73.0"
github_token: ${{ secrets.GITHUB_TOKEN }}

- name: buf lint
run: buf lint proto
Expand All @@ -36,6 +39,9 @@ jobs:
- uses: actions/checkout@v4

- uses: bufbuild/buf-setup-action@v1
with:
version: "1.73.0"
github_token: ${{ secrets.GITHUB_TOKEN }}

- uses: actions/setup-go@v5
with:
Expand All @@ -47,12 +53,12 @@ jobs:

- name: Install Go plugins
run: |
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.36.11
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.6.2
echo "$(go env GOPATH)/bin" >> "$GITHUB_PATH"

- name: Install Python plugins
run: pip install grpcio-tools mypy-protobuf
run: pip install 'grpcio-tools==1.84.0' 'mypy-protobuf==5.1.0'

- name: Generate Go SDK
run: |
Expand Down
13 changes: 10 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,21 @@ $(VENV_PYTHON):
python3 -m venv $(VENV)

$(VENV_STAMP): $(VENV_PYTHON)
$(VENV_PYTHON) -m pip install --upgrade pip grpcio-tools mypy-protobuf
$(VENV_PYTHON) -m pip install --upgrade pip 'grpcio-tools==1.84.0' 'mypy-protobuf==5.1.0'
touch $(VENV_STAMP)

.PHONY: venv
venv: $(VENV_STAMP)

# Keep in sync with .github/workflows/proto-ci.yml. Unpinned @latest on
# CI vs an older local plugin rewrites the SDK and fails generate-check.
PROTOC_GEN_GO := google.golang.org/protobuf/cmd/protoc-gen-go@v1.36.11
PROTOC_GEN_GO_GRPC := google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.6.2

.PHONY: tools
tools: $(VENV_STAMP)
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
go install $(PROTOC_GEN_GO)
go install $(PROTOC_GEN_GO_GRPC)

.PHONY: lint
lint:
Expand All @@ -42,6 +47,8 @@ generate: generate-go generate-python

.PHONY: generate-go
generate-go:
go install $(PROTOC_GEN_GO)
go install $(PROTOC_GEN_GO_GRPC)
PATH="$(GOBIN):$$PATH" buf generate proto
./scripts/add-license-headers.sh sdk/go

Expand Down
2 changes: 1 addition & 1 deletion proto/hal/adapters/common/v1/common.proto
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ message HealthCheckResponse {
// Health of the physical hardware behind the adapter, as reported by the
// vendor's own driver/API for that device -- not just process liveness.
// Implementations query whatever the vendor exposes and translate it into
// an UP / DOWN / DEGRADED-style result, which may be informed by the
// an UP / DOWN / DEGRADED / IDLE-style result, which may be informed by the
// device's own lifecycle state enum.
message ResourceHealthCheckRequest {
string product_id = 1;
Expand Down
4 changes: 2 additions & 2 deletions sdk/go/hal/adapters/common/v1/common.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion sdk/go/hal/adapters/source/v1/source.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion sdk/go/hal/adapters/switch/v1/switch.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion sdk/go/hal/adapters/timetagger/v1/timetagger.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions test/conformance/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ These tests are used to verify the basic runtime validation of an adapters imple
- Adapters returns valid responses as per the HAL API specification
- All required fields are present and correctly formatted as per of the response structure
- Optional RPCs if implemented by the server are verified or skipped if unimplemented.
- Command RPCs (`Initialize`, `StartEmission`, `StartDataCollection`, …) skip on `FAILED_PRECONDITION` when the adapter refuses because of its current lifecycle state. Probe RPCs (`HealthCheck`, `ResourceHealthCheck`, `GetProductInfo`, `GetStatus`) must still succeed in any state.

These tests are designed to be run against a running adapter instance. They do not start the adapters themselves. Please see the instruction below for running the tests with a specific adapter instance.

Expand Down
4 changes: 3 additions & 1 deletion test/conformance/proto_constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,10 @@
RESOURCE_HEALTH_UP = "UP"
RESOURCE_HEALTH_DOWN = "DOWN"
RESOURCE_HEALTH_DEGRADED = "DEGRADED"
RESOURCE_HEALTH_IDLE = "IDLE"
VALID_RESOURCE_HEALTH_STATUSES = {
RESOURCE_HEALTH_UP,
RESOURCE_HEALTH_DOWN,
RESOURCE_HEALTH_DEGRADED
RESOURCE_HEALTH_DEGRADED,
RESOURCE_HEALTH_IDLE
}
11 changes: 6 additions & 5 deletions test/conformance/source/test_source.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from utils import (
validate_response_field,
validate_response_fields,
check_command_rpc,
check_optional_rpc,
)

Expand Down Expand Up @@ -151,7 +152,7 @@ def test_initialize(self, client, source_kind):
request = source_pb2.InitializeRequest(
**rpc_request(source_kind, "Initialize")
)
response = client.Initialize(request)
response = check_command_rpc(lambda: client.Initialize(request))
assert response is not None
validate_response_field(
response, "message", str, allow_empty=False
Expand All @@ -162,7 +163,7 @@ def test_deinitialize(self, client, source_kind):
request = source_pb2.DeinitializeRequest(
**rpc_request(source_kind, "Deinitialize")
)
response = client.Deinitialize(request)
response = check_command_rpc(lambda: client.Deinitialize(request))
assert response is not None
validate_response_field(
response, "message", str, allow_empty=False
Expand Down Expand Up @@ -201,7 +202,7 @@ def test_set_config(self, client, source_kind):
request = source_pb2.SetConfigRequest(
**rpc_request(source_kind, "SetConfig")
)
response = client.SetConfig(request)
response = check_command_rpc(lambda: client.SetConfig(request))
assert response is not None
validate_response_field(
response, "message", str, allow_empty=False
Expand All @@ -212,7 +213,7 @@ def test_start_emission(self, client, source_kind):
request = source_pb2.StartEmissionRequest(
**rpc_request(source_kind, "StartEmission")
)
response = client.StartEmission(request)
response = check_command_rpc(lambda: client.StartEmission(request))
assert response is not None
validate_response_field(
response, "message", str, allow_empty=False
Expand All @@ -223,7 +224,7 @@ def test_stop_emission(self, client, source_kind):
request = source_pb2.StopEmissionRequest(
**rpc_request(source_kind, "StopEmission")
)
response = client.StopEmission(request)
response = check_command_rpc(lambda: client.StopEmission(request))
assert response is not None
validate_response_field(
response, "message", str, allow_empty=False
Expand Down
11 changes: 6 additions & 5 deletions test/conformance/switch/test_switch.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from utils import (
validate_response_field,
validate_response_fields,
check_command_rpc,
check_optional_rpc,
)

Expand Down Expand Up @@ -152,7 +153,7 @@ def test_initialize(self, client, switch_kind):
request = switch_pb2.InitializeRequest(
**rpc_request(switch_kind, "Initialize")
)
response = client.Initialize(request)
response = check_command_rpc(lambda: client.Initialize(request))
assert response is not None
validate_response_field(
response, "message", str, allow_empty=False
Expand All @@ -163,7 +164,7 @@ def test_deinitialize(self, client, switch_kind):
request = switch_pb2.DeinitializeRequest(
**rpc_request(switch_kind, "Deinitialize")
)
response = client.Deinitialize(request)
response = check_command_rpc(lambda: client.Deinitialize(request))
assert response is not None
validate_response_field(
response, "message", str, allow_empty=False
Expand Down Expand Up @@ -201,7 +202,7 @@ def test_set_config(self, client, switch_kind):
request = switch_pb2.SetConfigRequest(
**rpc_request(switch_kind, "SetConfig")
)
response = client.SetConfig(request)
response = check_command_rpc(lambda: client.SetConfig(request))
assert response is not None
validate_response_field(
response, "message", str, allow_empty=False
Expand All @@ -212,7 +213,7 @@ def test_commit_connection(self, client, switch_kind):
request = switch_pb2.CommitConnectionRequest(
**rpc_request(switch_kind, "CommitConnection")
)
response = client.CommitConnection(request)
response = check_command_rpc(lambda: client.CommitConnection(request))
assert response is not None
validate_response_field(
response, "message", str, allow_empty=False
Expand All @@ -223,7 +224,7 @@ def test_clear_connections(self, client, switch_kind):
request = switch_pb2.ClearConnectionsRequest(
**rpc_request(switch_kind, "ClearConnections")
)
response = client.ClearConnections(request)
response = check_command_rpc(lambda: client.ClearConnections(request))
assert response is not None
validate_response_field(
response, "message", str, allow_empty=False
Expand Down
11 changes: 6 additions & 5 deletions test/conformance/timetagger/test_timetagger.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from utils import (
validate_response_field,
validate_response_fields,
check_command_rpc,
check_optional_rpc,
)

Expand Down Expand Up @@ -151,7 +152,7 @@ def test_initialize(self, client, timetagger_kind):
request = timetagger_pb2.InitializeRequest(
**rpc_request(timetagger_kind, "Initialize")
)
response = client.Initialize(request)
response = check_command_rpc(lambda: client.Initialize(request))
assert response is not None
validate_response_field(
response, "message", str, allow_empty=False
Expand All @@ -162,7 +163,7 @@ def test_deinitialize(self, client, timetagger_kind):
request = timetagger_pb2.DeinitializeRequest(
**rpc_request(timetagger_kind, "Deinitialize")
)
response = client.Deinitialize(request)
response = check_command_rpc(lambda: client.Deinitialize(request))
assert response is not None
validate_response_field(
response, "message", str, allow_empty=False
Expand Down Expand Up @@ -200,7 +201,7 @@ def test_set_config(self, client, timetagger_kind):
request = timetagger_pb2.SetConfigRequest(
**rpc_request(timetagger_kind, "SetConfig")
)
response = client.SetConfig(request)
response = check_command_rpc(lambda: client.SetConfig(request))
assert response is not None
validate_response_field(
response, "message", str, allow_empty=False
Expand All @@ -211,7 +212,7 @@ def test_start_data_collection(self, client, timetagger_kind):
request = timetagger_pb2.StartDataCollectionRequest(
**rpc_request(timetagger_kind, "StartDataCollection")
)
response = client.StartDataCollection(request)
response = check_command_rpc(lambda: client.StartDataCollection(request))
assert response is not None
validate_response_field(
response, "message", str, allow_empty=False
Expand All @@ -222,7 +223,7 @@ def test_stop_data_collection(self, client, timetagger_kind):
request = timetagger_pb2.StopDataCollectionRequest(
**rpc_request(timetagger_kind, "StopDataCollection")
)
response = client.StopDataCollection(request)
response = check_command_rpc(lambda: client.StopDataCollection(request))
assert response is not None
validate_response_field(
response, "message", str, allow_empty=False
Expand Down
58 changes: 57 additions & 1 deletion test/conformance/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

"""Utility functions for conformance tests"""
import grpc
import pytest


def is_unimplemented(error):
Expand All @@ -22,6 +23,56 @@ def is_unimplemented(error):
return isinstance(error, grpc.RpcError) and error.code() == grpc.StatusCode.UNIMPLEMENTED


def is_failed_precondition(error):
"""
Check if a gRPC error is FAILED_PRECONDITION status.

Args:
error: The exception to check

Returns:
bool: True if error is FAILED_PRECONDITION, False otherwise

Note:
Command RPCs may return FAILED_PRECONDITION when the adapter's
current lifecycle state does not allow the call. Conformance tests
skip rather than fail in that case.
"""
return (
isinstance(error, grpc.RpcError)
and error.code() == grpc.StatusCode.FAILED_PRECONDITION
)


def check_command_rpc(rpc_call):
"""
Call a required command RPC (Initialize, StartEmission, ...).

Args:
rpc_call: Callable that makes the RPC call

Returns:
The protobuf response on OK.

Raises:
grpc.RpcError: If the RPC fails with any status other than
FAILED_PRECONDITION.

Note:
These tests check the HAL wire contract, not adapter functionality.
FAILED_PRECONDITION means the adapter refused because of its own
state; skip so third-party adapters with different sequences still
pass. Probe RPCs (HealthCheck, ResourceHealthCheck, GetProductInfo,
GetStatus) must not use this helper — they must succeed in any state.
"""
try:
return rpc_call()
except grpc.RpcError as e:
if is_failed_precondition(e):
pytest.skip(e.details() or "FAILED_PRECONDITION")
raise


def check_optional_rpc(rpc_call):
"""
Helper to test optional RPCs that may return UNIMPLEMENTED.
Expand All @@ -36,17 +87,22 @@ def check_optional_rpc(rpc_call):

Raises:
grpc.RpcError: If RPC fails with any status other than UNIMPLEMENTED

or FAILED_PRECONDITION.

Note:
Optional RPCs in HAL API may return UNIMPLEMENTED. This helper treats
both successful responses and UNIMPLEMENTED as acceptable outcomes.
FAILED_PRECONDITION (implemented, but not allowed in the current
adapter state) skips the test.
"""
try:
response = rpc_call()
return True, response
except grpc.RpcError as e:
if is_unimplemented(e):
return False, None
if is_failed_precondition(e):
pytest.skip(e.details() or "FAILED_PRECONDITION")
raise


Expand Down
Loading