From 9e9f36ac9377349ca5f8374ea1b7828ecc0371b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Otto?= Date: Thu, 14 Nov 2024 00:01:11 +0100 Subject: [PATCH 1/7] Support Submodel/SSP-02 --- README.md | 5 ++- aas_test_engines/test_cases/v3_0/api.py | 58 ++++++++++++++++++++----- test/acceptance/server.py | 31 ++++++++++--- 3 files changed, 75 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 98fbe93..6191017 100644 --- a/README.md +++ b/README.md @@ -60,8 +60,9 @@ In case of API, the IDTA specifications define service specifications and profil | Name | Profile Identifier | Description | Support in test-engine | | :--- | :--- | :--- | :--- | | AAS Repository Read Profile | https://admin-shell.io/aas/API/3/0/AssetAdministrationShellRepositoryServiceSpecification/SSP-002 | Only read operations for the AAS Repository Service | ✅ | -| Submodel Repository Read Profile | https://admin-shell.io/aas/API/3/0/SubmodelServiceSpecification/SSP-002 | Only read operations for the Submodel Repository Service | ✅ | -| AAS Registry Read Profile | https://admin-shell.io/aas/API/3/0/AssetAdministrationShellRegistryServiceSpecification/SSP-002 | Only reads operations for AAS Registry Service | (✔️) | +| Submodel Repository Read Profile | https://admin-shell.io/aas/API/3/0/SubmodelRepositoryServiceSpecification/SSP-002 | Only read operations for the Submodel Repository Service | ✅ | +| Submodel Read Profile | https://admin-shell.io/aas/API/3/0/SubmodelServiceSpecification/SSP-002 | Only read operations for the Submodel Repository Service | ✅ | +| AAS Registry Read Profile | https://admin-shell.io/aas/API/3/0/AssetAdministrationShellRegistryServiceSpecification/SSP-002 | Only read operations for AAS Registry Service | (✔️) | For a detailed list of what is checked (and what is not), see [here](doc/api.md). diff --git a/aas_test_engines/test_cases/v3_0/api.py b/aas_test_engines/test_cases/v3_0/api.py index 0943be5..a8f6d32 100644 --- a/aas_test_engines/test_cases/v3_0/api.py +++ b/aas_test_engines/test_cases/v3_0/api.py @@ -485,7 +485,7 @@ def _get_json(response: requests.models.Response) -> dict: abort(AasTestResult(f"Cannot decode as JSON: {e}", Level.CRITICAL)) -def _invoke(request: Request, conf: ExecConf, positive_test) -> requests.models.Response: +def _execute(request: Request, conf: ExecConf, positive_test) -> requests.models.Response: prepared_request = request.build(conf.server).prepare() response = requests.Session().send(prepared_request, verify=conf.verify) write(f"Response: ({response.status_code}): {_shorten(response.content)}") @@ -500,9 +500,15 @@ def _invoke(request: Request, conf: ExecConf, positive_test) -> requests.models. return response +def _invoke(request: Request, conf: ExecConf, positive_test: bool) -> requests.models.Response: + with start(f"Invoke: {request.operation.method.upper()} {request.make_path()}"): + response = _execute(request, conf, positive_test) + return response + + def _invoke_and_decode(request: Request, conf: ExecConf, positive_test: bool) -> dict: with start(f"Invoke: {request.operation.method.upper()} {request.make_path()}"): - response = _invoke(request, conf, positive_test) + response = _execute(request, conf, positive_test) expected_responses = [] expected_responses += [i for i in request.operation.responses if i.code == response.status_code] expected_responses += [i for i in request.operation.responses if i.code is None] @@ -1073,6 +1079,8 @@ class SubmodelElementTestsBase(ApiTestSuite): 'File', ] + paths: Dict[str, List[str]] = {} + def _collect_submodel_elements(data: list, paths: Dict[str, List[str]], path_prefix: str): for i in data: @@ -1130,6 +1138,12 @@ def setup(self): class SubmodelElementBySubmodelSuite(SubmodelElementTestsBase): def setup(self): self.valid_values = {} + op = self.open_api.operations["GetAllSubmodelElements"] + request = generate_one_valid(op, self.sample_cache, self.valid_values) + data = _invoke_and_decode(request, self.conf, True) + elements = _lookup(data, ['result']) + _collect_submodel_elements(elements, self.paths, '') + self.valid_values['idShortPath'] = _first_iterator_item(self.paths.values())[0] class SubmodelElementBySuperpathSuite(SubmodelElementTestsBase): @@ -1271,6 +1285,11 @@ class GetSubmodelElementByPath_ValueOnly_SubmodelRepository(SubmodelElementBySub pass +@operation("GetSubmodelElementByPath-ValueOnly") +class GetSubmodelElementByPath(SubmodelElementBySubmodelSuite, GetSubmodelElementValueOnlyTests): + pass + + class GetSubmodelElementPathTests(SubmodelElementTestsBase): supported_submodel_elements = { 'SubmodelElementCollection', @@ -1316,6 +1335,11 @@ class GetSubmodelElementByPath_Path_SubmodelRepo(GetSubmodelElementPathTests, Su pass +@operation("GetSubmodelElementByPath-Path") +class GetSubmodelElementByPath(GetSubmodelElementPathTests, SubmodelElementBySubmodelSuite): + pass + + class GetSubmodelElementReferenceTests(SubmodelElementTestsBase): supported_submodel_elements = { 'SubmodelElementCollection', @@ -1359,6 +1383,11 @@ class GetSubmodelElementByPath_Path_SubmodelRepository(GetSubmodelElementReferen pass +@operation("GetSubmodelElementByPath-Reference") +class GetSubmodelElementByPath_Path_SubmodelRepository(GetSubmodelElementReferenceTests, SubmodelElementBySubmodelSuite): + pass + + class GetSubmodelElementMetadataTests(SubmodelElementTestsBase): supported_submodel_elements = { 'SubmodelElementCollection', @@ -1400,29 +1429,41 @@ class GetSubmodelElementByPath_Metadata_SubmodelRepository(GetSubmodelElementMet pass +@operation("GetSubmodelElementByPath-Metadata") +class GetSubmodelElementByPath_Metadata(GetSubmodelElementMetadataTests, SubmodelElementBySubmodelSuite): + pass + + class GetFileByPathTests(SubmodelElementTestsBase): def test_no_params(self): """ Invoke without params """ + valid_values = self.valid_values.copy() try: - self.valid_values['idShortPath'] = self.paths['File'] + valid_values['idShortPath'] = self.paths['File'][0] except KeyError: abort("No submodel element of type 'File' found, skipping test.") - request = generate_one_valid(self.operation, self.sample_cache, self.valid_values) + request = generate_one_valid(self.operation, self.sample_cache, valid_values) _invoke(request, self.conf, True) @operation("GetFileByPath_AasRepository") -class GetSubmodelElementByPath_Metadata_AasRepository(GetFileByPathTests, SubmodelElementByAasRepoSuite): +class GetFileByPath_AasRepository(GetFileByPathTests, SubmodelElementByAasRepoSuite): pass @operation("GetFileByPath_SubmodelRepo") -class GetSubmodelElementByPath_Metadata_SubmodelRepository(GetFileByPathTests, SubmodelElementBySubmodelRepoSuite): +class GetFileByPath_SubmodelRepo(GetFileByPathTests, SubmodelElementBySubmodelRepoSuite): pass + +@operation("GetFileByPath") +class GetFileByPath(GetFileByPathTests, SubmodelElementBySubmodelSuite): + pass + + # /serialization @@ -1568,13 +1609,8 @@ def test_contains_suite(self): @operation('PostSubmodelElementByPath') @operation('DeleteSubmodelElementByPath') @operation('PatchSubmodelElementByPath') -@operation('GetSubmodelElementByPath-Metadata') @operation('PatchSubmodelElementByPath-Metadata') -@operation('GetSubmodelElementByPath-ValueOnly') @operation('PatchSubmodelElementByPath-ValueOnly') -@operation('GetSubmodelElementByPath-Reference') -@operation('GetSubmodelElementByPath-Path') -@operation('GetFileByPath') @operation('PutFileByPath') @operation('DeleteFileByPath') @operation('InvokeOperation') diff --git a/test/acceptance/server.py b/test/acceptance/server.py index 65735f4..ff5d70d 100755 --- a/test/acceptance/server.py +++ b/test/acceptance/server.py @@ -70,24 +70,43 @@ def show_server_logs(container_id: str): class Params: url: str suite: str + valid_rejected: int + invalid_accepted: int + remove_path_prefix: str = '' + +# PYTHONPATH=. python -m aas_test_engines check_server http://localhost:8000/api/v3.0/shells/d3d3LmV4YW1wbGUuY29tL2lkcy9zbS84MTMyXzQxMDJfODA0Ml83NTYx/submodels/d3d3LmV4YW1wbGUuY29tL2lkcy9zbS84MTMyXzQxMDJfODA0Ml8xODYx SubmodelServiceSpecification/SSP-002 --no-verify --output html --remove-path-prefix /submodel > output.html params = [ - Params('/api/v3.0', "https://admin-shell.io/aas/API/3/0/AssetAdministrationShellRepositoryServiceSpecification/SSP-002"), - Params('/api/v3.0', "https://admin-shell.io/aas/API/3/0/SubmodelRepositoryServiceSpecification/SSP-002"), + Params( + '/api/v3.0', + "https://admin-shell.io/aas/API/3/0/AssetAdministrationShellRepositoryServiceSpecification/SSP-002", + 3, 0, + ), + Params( + '/api/v3.0', + "https://admin-shell.io/aas/API/3/0/SubmodelRepositoryServiceSpecification/SSP-002", + 3, 0, + ), + Params( + '/api/v3.0/shells/aHR0cDovL2N1c3RvbWVyLmNvbS9hYXMvOTE3NV83MDEzXzcwOTFfOTE2OA==/submodels/aHR0cDovL2k0MC5jdXN0b21lci5jb20vdHlwZS8xLzEvRjEzRTg1NzZGNjQ4ODM0Mg==', + "https://admin-shell.io/aas/API/3/0/SubmodelServiceSpecification/SSP-002", + 2, 2, + "/submodel", + ), # Params('/api/v3.0', "https://admin-shell.io/aas/API/3/0/AssetAdministrationShellServiceSpecification/SSP-002"), - # Params('/api/v3.0', "https://admin-shell.io/aas/API/3/0/SubmodelServiceSpecification/SSP-002"), ] for param in params: print("-"*10) print(f"Checking {param.suite}") conf = api.ExecConf( - server=f"{HOST}{param.url}" + server=f"{HOST}{param.url}", + remove_path_prefix=param.remove_path_prefix, ) result, mat = api.execute_tests(conf, param.suite) mat.print() # TODO # assert result.ok() - assert mat.valid_rejected == 3 # Constraint violations in ExampleMotor.aasx - assert mat.invalid_accepted == 0 + assert mat.valid_rejected == param.valid_rejected + assert mat.invalid_accepted == param.invalid_accepted From f19294582659527e142574ac0da8ecbc5d855bbb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Otto?= Date: Thu, 21 Nov 2024 12:03:50 +0100 Subject: [PATCH 2/7] Minor improvements (fixes #66) --- aas_test_engines/test_cases/v3_0/api.py | 30 +++++++++++++++++------ aas_test_engines/test_cases/v3_0/parse.py | 2 +- 2 files changed, 23 insertions(+), 9 deletions(-) diff --git a/aas_test_engines/test_cases/v3_0/api.py b/aas_test_engines/test_cases/v3_0/api.py index a8f6d32..a3d4650 100644 --- a/aas_test_engines/test_cases/v3_0/api.py +++ b/aas_test_engines/test_cases/v3_0/api.py @@ -19,6 +19,7 @@ import requests import base64 +import json from .model import AssetAdministrationShell, Environment from .parse import parse_and_check_json @@ -597,7 +598,7 @@ def setup(self): self.valid_id_short: str = _lookup(data, ['result', 0, 'idShort']) global_asset_id = _lookup(data, ['result', 0, 'assetInformation', 'globalAssetId'], None) if global_asset_id: - self.asset_id = {'name': 'globalAssetId', 'value': 'globalAssetId'} + self.asset_id = {'name': 'globalAssetId', 'value': global_asset_id} else: self.asset_id = _lookup(data, ['result', 0, 'specificAssetIds', 0]) self.cursor = _lookup(data, ['paging_metadata', 'cursor'], None) @@ -642,8 +643,10 @@ def test_filter_by_asset_id(self): """ Filter by assetId """ - request = generate_one_valid(self.operation, self.sample_cache, {'assetIds': self.asset_id}) - _invoke_and_decode(request, self.conf, True) + encoded_asset_id = base64.b64encode(json.dumps(self.asset_id).encode()).decode() + request = generate_one_valid(self.operation, self.sample_cache, {'assetIds': encoded_asset_id}) + data = _invoke_and_decode(request, self.conf, True) + _assert(len(data["result"]) != 0, "Returns non-empty list") @operation("GetAllAssetAdministrationShells") @@ -667,7 +670,12 @@ def test_filter_by_idshort(self): request = generate_one_valid(self.operation, self.sample_cache, {'idShort': self.valid_id_short}) _invoke_and_decode(request, self.conf, True) +##################################### +# AssetAdministrationShell Interface +##################################### + # /shells/ +# /aas @operation("GetAssetAdministrationShellById") @@ -727,6 +735,9 @@ def test_simple(self): request = generate_one_valid(self.operation, self.sample_cache, {'aasIdentifier': b64urlsafe(self.valid_id)}) _invoke_and_decode(request, self.conf, True) + # TODO: fetch by semantic id + # TODO: fetch by idShort + # TODO: two pagination tests # /shells//submodel-refs @@ -760,6 +771,7 @@ def test_pagination(self): data = _lookup(data, ['result']) _assert(len(data) == 1, 'Exactly one entry') + # TODO: set limit=1 and fetch only one # /shells//asset-information @@ -921,7 +933,7 @@ class GetSubmodel_Reference(SubmodelSuite, GetSubmodelRefsTests): # /shells//submodels//submodel-elements -class GetSubmodelElementTests(ApiTestSuite): +class GetSubmodelElementsTests(ApiTestSuite): def test_simple(self): """ Fetch all submodel elements @@ -969,19 +981,20 @@ def test_extent_without_blob_value(self): }) _invoke_and_decode(request, self.conf, True) + # TODO: two pagination tests are missing @operation("GetAllSubmodelElements_AasRepository") -class GetAllSubmodelElements_AasRepository(SubmodelByAasRepoSuite, GetSubmodelElementTests): +class GetAllSubmodelElements_AasRepository(SubmodelByAasRepoSuite, GetSubmodelElementsTests): pass @operation("GetAllSubmodelElements_SubmodelRepository") -class GetAllSubmodelElements_SubmodelRepository(SubmodelBySubmodelRepoSuite, GetSubmodelElementTests): +class GetAllSubmodelElements_SubmodelRepository(SubmodelBySubmodelRepoSuite, GetSubmodelElementsTests): pass @operation("GetAllSubmodelElements") -class GetAllSubmodelElements(SubmodelSuite, GetSubmodelElementTests): +class GetAllSubmodelElements(SubmodelSuite, GetSubmodelElementsTests): pass @@ -1146,7 +1159,7 @@ def setup(self): self.valid_values['idShortPath'] = _first_iterator_item(self.paths.values())[0] -class SubmodelElementBySuperpathSuite(SubmodelElementTestsBase): +class GetSubmodelElementTests(SubmodelElementTestsBase): supported_submodel_elements = { 'SubmodelElementCollection', 'SubmodelElementList', @@ -1501,6 +1514,7 @@ def test_include_concept_descriptions(self): if data: _assert('conceptDescriptions' in data, 'contains conceptDescriptions', Level.WARNING) + # TODO: invoke without params # /description diff --git a/aas_test_engines/test_cases/v3_0/parse.py b/aas_test_engines/test_cases/v3_0/parse.py index 01a6a88..e59b0b0 100644 --- a/aas_test_engines/test_cases/v3_0/parse.py +++ b/aas_test_engines/test_cases/v3_0/parse.py @@ -80,7 +80,7 @@ def __init__(self, raw_value: str): if self.pattern: if re.fullmatch(self.pattern, raw_value) is None: - raise ValueError("String does not match pattern") + raise ValueError(f"String '{raw_value}' does not match pattern") def __eq__(self, other: "StringFormattedValue") -> bool: return self.raw_value == other.raw_value From dc5fe369009a12d2892f7eb699e003ad520c5d2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Otto?= Date: Sun, 24 Nov 2024 02:06:16 +0100 Subject: [PATCH 3/7] Sync basyx_python --- .../servers/basyx_python/Dockerfile | 46 +++++++++--- .../servers/basyx_python/app/main.py | 2 +- .../servers/basyx_python/app/requirements.txt | 2 - .../servers/basyx_python/compose.yml | 2 +- .../servers/basyx_python/entrypoint.sh | 71 +++++++++++++++++++ .../basyx_python/nginx/body-buffer-size.conf | 6 -- .../basyx_python/nginx/cors-header.conf | 1 - .../servers/basyx_python/stop-supervisor.sh | 8 +++ .../servers/basyx_python/supervisord.ini | 27 +++++++ .../servers/basyx_python/uwsgi.ini | 9 +++ 10 files changed, 155 insertions(+), 19 deletions(-) delete mode 100644 bin/check_servers/servers/basyx_python/app/requirements.txt create mode 100644 bin/check_servers/servers/basyx_python/entrypoint.sh delete mode 100644 bin/check_servers/servers/basyx_python/nginx/body-buffer-size.conf delete mode 100644 bin/check_servers/servers/basyx_python/nginx/cors-header.conf create mode 100644 bin/check_servers/servers/basyx_python/stop-supervisor.sh create mode 100644 bin/check_servers/servers/basyx_python/supervisord.ini create mode 100644 bin/check_servers/servers/basyx_python/uwsgi.ini diff --git a/bin/check_servers/servers/basyx_python/Dockerfile b/bin/check_servers/servers/basyx_python/Dockerfile index 7baa7b4..6dc3c4c 100644 --- a/bin/check_servers/servers/basyx_python/Dockerfile +++ b/bin/check_servers/servers/basyx_python/Dockerfile @@ -1,15 +1,45 @@ -FROM tiangolo/uwsgi-nginx:python3.11 +FROM python:3.11-alpine -# Should match client_body_buffer_size defined in nginx/body-buffer-size.conf -ENV NGINX_MAX_UPLOAD 1M +LABEL org.label-schema.name="Eclipse BaSyx" \ + org.label-schema.version="1.0" \ + org.label-schema.description="Docker image for the basyx-python-sdk server application" \ + org.label-schema.maintainer="Eclipse BaSyx" +ENV PYTHONDONTWRITEBYTECODE=1 +ENV PYTHONUNBUFFERED=1 + +# If we have more dependencies for the server it would make sense +# to refactor uswgi to the pyproject.toml +RUN apk update && \ + apk add --no-cache nginx supervisor gcc musl-dev linux-headers python3-dev git bash && \ + pip install uwsgi && \ + pip install --no-cache-dir git+https://github.com/eclipse-basyx/basyx-python-sdk@main#subdirectory=sdk && \ + apk del git bash + + +COPY uwsgi.ini /etc/uwsgi/ +COPY supervisord.ini /etc/supervisor/conf.d/supervisord.ini +COPY stop-supervisor.sh /etc/supervisor/stop-supervisor.sh +RUN chmod +x /etc/supervisor/stop-supervisor.sh + +# Makes it possible to use a different configuration +ENV UWSGI_INI=/etc/uwsgi/uwsgi.ini # object stores aren't thread-safe yet # https://github.com/eclipse-basyx/basyx-python-sdk/issues/205 -ENV UWSGI_CHEAPER 0 -ENV UWSGI_PROCESSES 1 +ENV UWSGI_CHEAPER=0 +ENV UWSGI_PROCESSES=1 +ENV NGINX_MAX_UPLOAD=1M +ENV NGINX_WORKER_PROCESSES=1 +ENV LISTEN_PORT=80 +ENV CLIENT_BODY_BUFFER_SIZE=1M + +# Copy the entrypoint that will generate Nginx additional configs +COPY entrypoint.sh /entrypoint.sh +RUN chmod +x /entrypoint.sh -COPY app/requirements.txt /app -RUN pip install --no-cache-dir -r requirements.txt +ENTRYPOINT ["/entrypoint.sh"] COPY ./app /app -COPY ./nginx /etc/nginx/conf.d +WORKDIR /app + +CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/supervisord.ini"] diff --git a/bin/check_servers/servers/basyx_python/app/main.py b/bin/check_servers/servers/basyx_python/app/main.py index 6662c86..c502bfb 100644 --- a/bin/check_servers/servers/basyx_python/app/main.py +++ b/bin/check_servers/servers/basyx_python/app/main.py @@ -8,7 +8,7 @@ from basyx.aas.backend.local_file import LocalFileObjectStore from basyx.aas.adapter.http import WSGIApp -storage_path = os.getenv("STORAGE_PATH", "storage") +storage_path = os.getenv("STORAGE_PATH", "/storage") storage_type = os.getenv("STORAGE_TYPE", "LOCAL_FILE_READ_ONLY") base_path = os.getenv("API_BASE_PATH") diff --git a/bin/check_servers/servers/basyx_python/app/requirements.txt b/bin/check_servers/servers/basyx_python/app/requirements.txt deleted file mode 100644 index 65ed864..0000000 --- a/bin/check_servers/servers/basyx_python/app/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -Werkzeug -git+https://github.com/rwth-iat/basyx-python-sdk@main diff --git a/bin/check_servers/servers/basyx_python/compose.yml b/bin/check_servers/servers/basyx_python/compose.yml index 4acf66e..a0e7a79 100644 --- a/bin/check_servers/servers/basyx_python/compose.yml +++ b/bin/check_servers/servers/basyx_python/compose.yml @@ -4,4 +4,4 @@ services: ports: - 8000:80 volumes: - - ./../../test_data:/app/storage:ro + - ./../../test_data:/storage:ro diff --git a/bin/check_servers/servers/basyx_python/entrypoint.sh b/bin/check_servers/servers/basyx_python/entrypoint.sh new file mode 100644 index 0000000..7223944 --- /dev/null +++ b/bin/check_servers/servers/basyx_python/entrypoint.sh @@ -0,0 +1,71 @@ +#!/usr/bin/env sh +set -e + +# Get the maximum upload file size for Nginx, default to 0: unlimited +USE_NGINX_MAX_UPLOAD=${NGINX_MAX_UPLOAD:-0} + +# Get the number of workers for Nginx, default to 1 +USE_NGINX_WORKER_PROCESSES=${NGINX_WORKER_PROCESSES:-1} + +# Set the max number of connections per worker for Nginx, if requested +# Cannot exceed worker_rlimit_nofile, see NGINX_WORKER_OPEN_FILES below +NGINX_WORKER_CONNECTIONS=${NGINX_WORKER_CONNECTIONS:-1024} + +# Get the listen port for Nginx, default to 80 +USE_LISTEN_PORT=${LISTEN_PORT:-80} + +# Get the client_body_buffer_size for Nginx, default to 1M +USE_CLIENT_BODY_BUFFER_SIZE=${CLIENT_BODY_BUFFER_SIZE:-1M} + +# Create the conf.d directory if it doesn't exist +if [ ! -d /etc/nginx/conf.d ]; then + mkdir -p /etc/nginx/conf.d +fi + +if [ -f /app/nginx.conf ]; then + cp /app/nginx.conf /etc/nginx/nginx.conf +else + content='user nginx;\n' + # Set the number of worker processes in Nginx + content=$content"worker_processes ${USE_NGINX_WORKER_PROCESSES};\n" + content=$content'error_log /var/log/nginx/error.log warn;\n' + content=$content'pid /var/run/nginx.pid;\n' + content=$content'events {\n' + content=$content" worker_connections ${NGINX_WORKER_CONNECTIONS};\n" + content=$content'}\n' + content=$content'http {\n' + content=$content' include /etc/nginx/mime.types;\n' + content=$content' default_type application/octet-stream;\n' + content=$content' log_format main '"'\$remote_addr - \$remote_user [\$time_local] \"\$request\" '\n" + content=$content' '"'\$status \$body_bytes_sent \"\$http_referer\" '\n" + content=$content' '"'\"\$http_user_agent\" \"\$http_x_forwarded_for\"';\n" + content=$content' access_log /var/log/nginx/access.log main;\n' + content=$content' sendfile on;\n' + content=$content' keepalive_timeout 65;\n' + content=$content' include /etc/nginx/conf.d/*.conf;\n' + content=$content'}\n' + content=$content'daemon off;\n' + # Set the max number of open file descriptors for Nginx workers, if requested + if [ -n "${NGINX_WORKER_OPEN_FILES}" ] ; then + content=$content"worker_rlimit_nofile ${NGINX_WORKER_OPEN_FILES};\n" + fi + # Save generated /etc/nginx/nginx.conf + printf "$content" > /etc/nginx/nginx.conf + + content_server='server {\n' + content_server=$content_server" listen ${USE_LISTEN_PORT};\n" + content_server=$content_server' location / {\n' + content_server=$content_server' include uwsgi_params;\n' + content_server=$content_server' uwsgi_pass unix:///tmp/uwsgi.sock;\n' + content_server=$content_server' }\n' + content_server=$content_server'}\n' + # Save generated server /etc/nginx/conf.d/nginx.conf + printf "$content_server" > /etc/nginx/conf.d/nginx.conf + + # # Generate additional configuration + printf "client_max_body_size $USE_NGINX_MAX_UPLOAD;\n" > /etc/nginx/conf.d/upload.conf + printf "client_body_buffer_size $USE_CLIENT_BODY_BUFFER_SIZE;\n" > /etc/nginx/conf.d/body-buffer-size.conf + printf "add_header Access-Control-Allow-Origin *;\n" > /etc/nginx/conf.d/cors-header.conf +fi + +exec "$@" diff --git a/bin/check_servers/servers/basyx_python/nginx/body-buffer-size.conf b/bin/check_servers/servers/basyx_python/nginx/body-buffer-size.conf deleted file mode 100644 index 423e8cd..0000000 --- a/bin/check_servers/servers/basyx_python/nginx/body-buffer-size.conf +++ /dev/null @@ -1,6 +0,0 @@ -# While the Dockerfile sets client_max_body_size, it doesn't -# allow us to define client_body_buffer_size, which is 16k -# by default. This results in temporary buffer files being -# created, in case the request body exceeds this limit. Thus, -# we override it here to match client_max_body_size. -client_body_buffer_size 1M; diff --git a/bin/check_servers/servers/basyx_python/nginx/cors-header.conf b/bin/check_servers/servers/basyx_python/nginx/cors-header.conf deleted file mode 100644 index af78db3..0000000 --- a/bin/check_servers/servers/basyx_python/nginx/cors-header.conf +++ /dev/null @@ -1 +0,0 @@ -add_header Access-Control-Allow-Origin *; diff --git a/bin/check_servers/servers/basyx_python/stop-supervisor.sh b/bin/check_servers/servers/basyx_python/stop-supervisor.sh new file mode 100644 index 0000000..9a953c9 --- /dev/null +++ b/bin/check_servers/servers/basyx_python/stop-supervisor.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env sh + +printf "READY\n" + +while read line; do + echo "Processing Event: $line" >&2 + kill $PPID +done < /dev/stdin diff --git a/bin/check_servers/servers/basyx_python/supervisord.ini b/bin/check_servers/servers/basyx_python/supervisord.ini new file mode 100644 index 0000000..d73d980 --- /dev/null +++ b/bin/check_servers/servers/basyx_python/supervisord.ini @@ -0,0 +1,27 @@ +[supervisord] +nodaemon=true + +[program:uwsgi] +command=/usr/local/bin/uwsgi --ini /etc/uwsgi/uwsgi.ini +stdout_logfile=/dev/stdout +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/stderr +stderr_logfile_maxbytes=0 +startsecs = 0 +autorestart=false +# may make sense to have autorestart enabled in production + +[program:nginx] +command=/usr/sbin/nginx +stdout_logfile=/var/log/nginx.out.log +stdout_logfile_maxbytes=0 +stderr_logfile=/var/log/nginx.err.log +stderr_logfile_maxbytes=0 +stopsignal=QUIT +startsecs = 0 +autorestart=false +# may make sense to have autorestart enabled in production + +[eventlistener:quit_on_failure] +events=PROCESS_STATE_STOPPED,PROCESS_STATE_EXITED,PROCESS_STATE_FATAL +command=/etc/supervisor/stop-supervisor.sh diff --git a/bin/check_servers/servers/basyx_python/uwsgi.ini b/bin/check_servers/servers/basyx_python/uwsgi.ini new file mode 100644 index 0000000..9c54ae1 --- /dev/null +++ b/bin/check_servers/servers/basyx_python/uwsgi.ini @@ -0,0 +1,9 @@ +[uwsgi] +wsgi-file = /app/main.py +socket = /tmp/uwsgi.sock +chown-socket = nginx:nginx +chmod-socket = 664 +hook-master-start = unix_signal:15 gracefully_kill_them_all +need-app = true +die-on-term = true +show-config = false From ab4e3a7e57e522443d4a5c8ab656bae0d05dc0cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Otto?= Date: Sun, 24 Nov 2024 12:50:16 +0100 Subject: [PATCH 4/7] Html report: navigate using arrow keys --- aas_test_engines/data/template.html | 53 ++++++++++++++++++++++++++--- aas_test_engines/result.py | 8 ++--- 2 files changed, 53 insertions(+), 8 deletions(-) diff --git a/aas_test_engines/data/template.html b/aas_test_engines/data/template.html index b3a6778..44a9daf 100644 --- a/aas_test_engines/data/template.html +++ b/aas_test_engines/data/template.html @@ -65,6 +65,12 @@ color: red; font-weight: bolder; } + + .help { + position: absolute; + right:0; + top:0 + } @@ -72,6 +78,19 @@

Test Result

+
+ + + + + + + + + +
Expand one level
Collapse one level
+
+
@@ -82,15 +101,41 @@

Test Result

element.classList.toggle("caret-down"); } - var carets = document.getElementsByClassName("caret"); - var i; + function expand(element) { + element.parentElement.parentElement.querySelector(".sub-results").classList.add("visible"); + element.classList.add("caret-down"); + } + + function collapse(element) { + element.parentElement.parentElement.querySelector(".sub-results").classList.remove("visible"); + element.classList.remove("caret-down"); + } - for (i = 0; i < carets.length; i++) { - carets[i].addEventListener("click", function (event) { + for (const caret of document.getElementsByClassName("caret")) { + caret.addEventListener("click", function (event) { toggle(event.target); }); } + let level = 0 + document.addEventListener("keyup", (event => { + if (event.code == "ArrowRight") { + const carets = document.getElementsByClassName(`level-${level}`); + if(carets.length == 0) return; + for (const caret of document.getElementsByClassName(`level-${level}`)) { + expand(caret) + } + level++; + } else if (event.code == "ArrowLeft") { + if(level==0) return; + level--; + const carets = document.getElementsByClassName(`level-${level}`); + for (const caret of carets) { + collapse(caret) + } + } + })); + diff --git a/aas_test_engines/result.py b/aas_test_engines/result.py index 0669146..6507b37 100644 --- a/aas_test_engines/result.py +++ b/aas_test_engines/result.py @@ -54,7 +54,7 @@ def to_lines(self, indent=0, path=''): for sub_result in self.sub_results: yield from sub_result.to_lines(indent + 1) - def _to_html(self) -> str: + def _to_html(self, level: int) -> str: cls = { Level.INFO: 'info', Level.WARNING: 'warning', @@ -65,11 +65,11 @@ def _to_html(self) -> str: msg = html.escape(self.message) if self.sub_results: c = "" if self.ok() else "caret-down" - s += f'
{msg}
\n' + s += f'
{msg}
\n' c = "" if self.ok() else "visible" s += f'
\n' for sub_result in self.sub_results: - s += sub_result._to_html() + s += sub_result._to_html(level + 1) s += "
\n" else: s += f'
{msg}
\n' @@ -86,7 +86,7 @@ def to_html(self) -> str: """ with open(os.path.join(script_dir, 'data', 'template.html'), 'r') as f: content = f.read() - return content.replace("", self._to_html()) + return content.replace("", self._to_html(0)) def to_dict(self): return { From 0bd3d7dd47fc2e3c90b38cf2703ebfe3844e619a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Otto?= Date: Wed, 11 Dec 2024 15:17:31 +0100 Subject: [PATCH 5/7] Allow timezone offsets for lastUpdate and timestamp (fixes #79) --- aas_test_engines/test_cases/v3_0/api.yml | 4 ++-- aas_test_engines/test_cases/v3_0/model.py | 13 +++++++++++-- test/acceptance/file.py | 3 +++ test/acceptance/generate.py | 2 +- 4 files changed, 17 insertions(+), 5 deletions(-) diff --git a/aas_test_engines/test_cases/v3_0/api.yml b/aas_test_engines/test_cases/v3_0/api.yml index 2963370..153e3f5 100644 --- a/aas_test_engines/test_cases/v3_0/api.yml +++ b/aas_test_engines/test_cases/v3_0/api.yml @@ -15410,7 +15410,7 @@ components: text: type: string timestamp: - pattern: "^-?(([1-9][0-9][0-9][0-9]+)|(0[0-9][0-9][0-9]))-((0[1-9])|(1[0-2]))-((0[1-9])|([12][0-9])|(3[01]))T(((([01][0-9])|(2[0-3])):[0-5][0-9]:([0-5][0-9])(\\.[0-9]+)?)|24:00:00(\\.0+)?)(Z|\\+00:00|-00:00)$" + pattern: "^-?(([1-9][0-9][0-9][0-9]+)|(0[0-9][0-9][0-9]))-((0[1-9])|(1[0-2]))-((0[1-9])|([12][0-9])|(3[01]))T(((([01][0-9])|(2[0-3])):[0-5][0-9]:([0-5][0-9])(\\.[0-9]+)?)|24:00:00(\\.0+)?)(Z|\\+\\d\\d:\\d\\d|-\\d\\d:\\d\\d)$" type: string GetReferencesResult: allOf: @@ -15675,7 +15675,7 @@ components: messageBroker: $ref: '#/components/schemas/Reference' lastUpdate: - pattern: "^-?(([1-9][0-9][0-9][0-9]+)|(0[0-9][0-9][0-9]))-((0[1-9])|(1[0-2]))-((0[1-9])|([12][0-9])|(3[01]))T(((([01][0-9])|(2[0-3])):[0-5][0-9]:([0-5][0-9])(\\.[0-9]+)?)|24:00:00(\\.0+)?)(Z|\\+00:00|-00:00)$" + pattern: "^-?(([1-9][0-9][0-9][0-9]+)|(0[0-9][0-9][0-9]))-((0[1-9])|(1[0-2]))-((0[1-9])|([12][0-9])|(3[01]))T(((([01][0-9])|(2[0-3])):[0-5][0-9]:([0-5][0-9])(\\.[0-9]+)?)|24:00:00(\\.0+)?)(Z|\\+\\d\\d:\\d\\d|-\\d\\d:\\d\\d)$" type: string minInterval: pattern: "^-?P((([0-9]+Y([0-9]+M)?([0-9]+D)?|([0-9]+M)([0-9]+D)?|([0-9]+D))(T(([0-9]+H)([0-9]+M)?([0-9]+(\\.[0-9]+)?S)?|([0-9]+M)([0-9]+(\\.[0-9]+)?S)?|([0-9]+(\\.[0-9]+)?S)))?)|(T(([0-9]+H)([0-9]+M)?([0-9]+(\\.[0-9]+)?S)?|([0-9]+M)([0-9]+(\\.[0-9]+)?S)?|([0-9]+(\\.[0-9]+)?S))))$" diff --git a/aas_test_engines/test_cases/v3_0/model.py b/aas_test_engines/test_cases/v3_0/model.py index cb2083d..6b93c7b 100644 --- a/aas_test_engines/test_cases/v3_0/model.py +++ b/aas_test_engines/test_cases/v3_0/model.py @@ -3,7 +3,7 @@ from dataclasses import dataclass from typing import List, Optional, Set from enum import Enum -from .data_types import _is_bounded_integer, is_bcp_lang_string, DataTypeDefXsd, validate, is_xs_date_time_utc, is_bcp_47_for_english, is_any_uri +from .data_types import _is_bounded_integer, is_bcp_lang_string, DataTypeDefXsd, validate, is_xs_date_time_utc, is_bcp_47_for_english, is_any_uri, is_xs_date_time # TODO: AASd-021 # TODO: AASd-022 @@ -102,6 +102,15 @@ class ValueDataType(StringFormattedValue): pass +class DateTime(StringFormattedValue): + min_length = 1 + + def __init__(self, raw_value): + super().__init__(raw_value) + if not is_xs_date_time(self.raw_value): + raise ValueError("Not an xs:dateTime") + + class DateTimeUtc(StringFormattedValue): min_length = 1 @@ -744,7 +753,7 @@ class BasicEventElement(EventElement): state: StateOfEvent message_topic: Optional[MessageTopicString] message_broker: Optional[Reference] - last_update: Optional[DateTimeUtc] + last_update: Optional[DateTime] min_interval: Optional[Duration] max_interval: Optional[Duration] diff --git a/test/acceptance/file.py b/test/acceptance/file.py index 1229adc..33e8432 100755 --- a/test/acceptance/file.py +++ b/test/acceptance/file.py @@ -10,6 +10,8 @@ def is_blacklisted(path): 'Double/lowest.', 'Double/max.', 'Float/largest_normal.', + 'lastUpdate/date_time_without_zone', + 'lastUpdate/date_time_with_offset', ] for i in blacklist: if i in path: @@ -35,6 +37,7 @@ def run(dirname: str, check): if 'Expected' in path_in: valid_accepted += 1 else: + print(path_in) invalid_accepted += 1 else: if 'Expected' in path_in: diff --git a/test/acceptance/generate.py b/test/acceptance/generate.py index 31d74c5..863b0ff 100755 --- a/test/acceptance/generate.py +++ b/test/acceptance/generate.py @@ -67,7 +67,7 @@ mat_test_engines.print() # TODO: need to fix 7 issues in aas-core-python first -if mat_aas_core.valid_rejected > 20: +if mat_aas_core.valid_rejected > 43: print("Valid instances have been rejected!") exit(1) From d81e716c860c0ff41e9d8a69df83f4da1fa4682c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Otto?= Date: Wed, 11 Dec 2024 15:17:46 +0100 Subject: [PATCH 6/7] Update aasx test data --- bin/check_servers/test_data/TestDataWithThumbnail.aasx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bin/check_servers/test_data/TestDataWithThumbnail.aasx b/bin/check_servers/test_data/TestDataWithThumbnail.aasx index ce97baf..9b47345 100644 --- a/bin/check_servers/test_data/TestDataWithThumbnail.aasx +++ b/bin/check_servers/test_data/TestDataWithThumbnail.aasx @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:321d822ea6c608d31ae7562ee7f5183b7a7ecd0daa292f86c22ae3911307e256 -size 59831 +oid sha256:61ce8cb58a5805f5fc4ae9aa09748652a294c06ea10f8fcbb1840ed02431afac +size 59303 From 5d4fd5cd641c55720ecd1a26d22b53bc7f21b46b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Otto?= Date: Sun, 12 Jan 2025 14:29:54 +0100 Subject: [PATCH 7/7] Support AAS read profile --- README.md | 1 + aas_test_engines/test_cases/v3_0/api.py | 276 ++++++++++++++++------- aas_test_engines/test_cases/v3_0/api.yml | 10 + test/acceptance/server.py | 18 +- 4 files changed, 219 insertions(+), 86 deletions(-) diff --git a/README.md b/README.md index 6191017..4a1baa9 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,7 @@ In case of API, the IDTA specifications define service specifications and profil | Name | Profile Identifier | Description | Support in test-engine | | :--- | :--- | :--- | :--- | | AAS Repository Read Profile | https://admin-shell.io/aas/API/3/0/AssetAdministrationShellRepositoryServiceSpecification/SSP-002 | Only read operations for the AAS Repository Service | ✅ | +| AAS Read Profile | https://admin-shell.io/aas/API/3/0/AssetAdministrationShellServiceSpecification/SSP-002 | Only read operations for the AAS Service | ✅ | | Submodel Repository Read Profile | https://admin-shell.io/aas/API/3/0/SubmodelRepositoryServiceSpecification/SSP-002 | Only read operations for the Submodel Repository Service | ✅ | | Submodel Read Profile | https://admin-shell.io/aas/API/3/0/SubmodelServiceSpecification/SSP-002 | Only read operations for the Submodel Repository Service | ✅ | | AAS Registry Read Profile | https://admin-shell.io/aas/API/3/0/AssetAdministrationShellRegistryServiceSpecification/SSP-002 | Only read operations for AAS Registry Service | (✔️) | diff --git a/aas_test_engines/test_cases/v3_0/api.py b/aas_test_engines/test_cases/v3_0/api.py index a3d4650..02ebeab 100644 --- a/aas_test_engines/test_cases/v3_0/api.py +++ b/aas_test_engines/test_cases/v3_0/api.py @@ -646,7 +646,8 @@ def test_filter_by_asset_id(self): encoded_asset_id = base64.b64encode(json.dumps(self.asset_id).encode()).decode() request = generate_one_valid(self.operation, self.sample_cache, {'assetIds': encoded_asset_id}) data = _invoke_and_decode(request, self.conf, True) - _assert(len(data["result"]) != 0, "Returns non-empty list") + if data: + _assert(len(data["result"]) != 0, "Returns non-empty list") @operation("GetAllAssetAdministrationShells") @@ -674,46 +675,67 @@ def test_filter_by_idshort(self): # AssetAdministrationShell Interface ##################################### -# /shells/ -# /aas - -@operation("GetAssetAdministrationShellById") -class GetAasById(ApiTestSuite): +class AasByRepoSuite(ApiTestSuite): def setup(self): op = self.open_api.operations["GetAllAssetAdministrationShells"] request = generate_one_valid(op, self.sample_cache, {'limit': 1}) data = _invoke_and_decode(request, self.conf, True) self.valid_id: str = _lookup(data, ['result', 0, 'id']) + self.valid_values = {'aasIdentifier': b64urlsafe(self.valid_id)} + + +class AasTestSuite(ApiTestSuite): + def setup(self): + self.valid_values = {} + self.valid_id = None + + +# /shells/ +# /aas +class GetAasTests(ApiTestSuite): def test_get(self): """ - Fetch AAS by id + Fetch AAS """ - request = generate_one_valid(self.operation, self.sample_cache, {'aasIdentifier': b64urlsafe(self.valid_id)}) + request = generate_one_valid(self.operation, self.sample_cache, self.valid_values) data = _invoke_and_decode(request, self.conf, True) - data = _lookup(data, ['id']) - _assert(data == self.valid_id, 'Returned the correct one') + if self.valid_id and data: + data = _lookup(data, ['id']) + _assert(data == self.valid_id, 'Returned the correct one') -@operation('GetAssetAdministrationShellById-Reference_AasRepository') -class GetAasReferenceById(ApiTestSuite): - def setup(self): - op = self.open_api.operations["GetAllAssetAdministrationShells"] - request = generate_one_valid(op, self.sample_cache, {'limit': 1}) - data = _invoke_and_decode(request, self.conf, True) - self.valid_id: str = _lookup(data, ['result', 0, 'id']) - - def test_simple(self): +class GetAasRefTests(ApiTestSuite): + def test_get(self): """ - Fetch AAS reference by id + Fetch AAS reference """ - request = generate_one_valid(self.operation, self.sample_cache, {'aasIdentifier': b64urlsafe(self.valid_id)}) + request = generate_one_valid(self.operation, self.sample_cache, self.valid_values) data = _invoke_and_decode(request, self.conf, True) - data = _lookup(data, ['keys', 0, 'value']) - _assert(data == self.valid_id, 'Returned the correct one') + if self.valid_id and data: + data = _lookup(data, ['keys', 0, 'value']) + _assert(data == self.valid_id, 'Returned the correct one') + -# /shells//submodels +@operation('GetAssetAdministrationShell') +class GetAas(AasTestSuite, GetAasTests): + pass + + +@operation("GetAssetAdministrationShell-Reference") +class GetAasRef(AasTestSuite, GetAasRefTests): + pass + + +@operation("GetAssetAdministrationShellById") +class GetAasById(AasByRepoSuite, GetAasTests): + pass + + +@operation('GetAssetAdministrationShellById-Reference_AasRepository') +class GetAasReferenceById(AasByRepoSuite, GetAasRefTests): + pass @operation("GetAllSubmodels_AasRepository") @@ -740,24 +762,35 @@ def test_simple(self): # TODO: two pagination tests # /shells//submodel-refs +# /aas/submodel-refs -@operation("GetAllSubmodelReferences_AasRepository") -class GetAllSubmodelRefsTestSuite(ApiTestSuite): +class SubmodelRefsByAasTestSuite(ApiTestSuite): + def setup(self): + request = generate_one_valid(self.operation, self.sample_cache, {'limit': 1}) + data = _invoke_and_decode(request, self.conf, True) + self.cursor = _lookup(data, ['paging_metadata', 'cursor'], None) + self.valid_values = {} + + +class SubmodelRefsByRepoTestSuite(ApiTestSuite): def setup(self): op = self.open_api.operations["GetAllAssetAdministrationShells"] request = generate_one_valid(op, self.sample_cache, {'limit': 1}) data = _invoke_and_decode(request, self.conf, True) - self.valid_id = _lookup(data, ['result', 0, 'id']) - request = generate_one_valid(self.operation, self.sample_cache, {'aasIdentifier': b64urlsafe(self.valid_id), 'limit': 1}) + valid_id = _lookup(data, ['result', 0, 'id']) + request = generate_one_valid(self.operation, self.sample_cache, {'aasIdentifier': b64urlsafe(valid_id), 'limit': 1}) data = _invoke_and_decode(request, self.conf, True) self.cursor = _lookup(data, ['paging_metadata', 'cursor'], None) + self.valid_values = {'aasIdentifier': b64urlsafe(valid_id)} + +class GetAllSubmodelRefsTests(ApiTestSuite): def test_simple(self): """ Fetch by id """ - request = generate_one_valid(self.operation, self.sample_cache, {'aasIdentifier': b64urlsafe(self.valid_id)}) + request = generate_one_valid(self.operation, self.sample_cache, self.valid_values) _invoke_and_decode(request, self.conf, True) def test_pagination(self): @@ -766,54 +799,66 @@ def test_pagination(self): """ if self.cursor is None: abort(AasTestResult("Cannot check pagination, there must be at least 2 submodels", level=Level.WARNING)) - request = generate_one_valid(self.operation, self.sample_cache, {'aasIdentifier': b64urlsafe(self.valid_id), 'cursor': self.cursor, 'limit': 1}) + request = generate_one_valid(self.operation, self.sample_cache, {**self.valid_values, 'cursor': self.cursor, 'limit': 1}) data = _invoke_and_decode(request, self.conf, True) data = _lookup(data, ['result']) _assert(len(data) == 1, 'Exactly one entry') # TODO: set limit=1 and fetch only one -# /shells//asset-information +@operation('GetAllSubmodelReferences') +class GetAllSubmodelRefsTestSuite(GetAllSubmodelRefsTests, SubmodelRefsByAasTestSuite): + pass -class AssetInfoTestSuiteBase(ApiTestSuite): - def setup(self): - op = self.open_api.operations["GetAllAssetAdministrationShells"] - request = generate_one_valid(op, self.sample_cache, {'limit': 1}) - data = _invoke_and_decode(request, self.conf, True) - self.valid_id = _lookup(data, ['result', 0, 'id']) - self.valid_values = { - 'aasIdentifier': [b64urlsafe(self.valid_id)] - } +@operation("GetAllSubmodelReferences_AasRepository") +class GetAllSubmodelRefsBySuperpathTestSuite(GetAllSubmodelRefsTests, SubmodelRefsByRepoTestSuite): + pass -@operation("GetAssetInformation_AasRepository") -class AssetInfoBySuperPathSuite(AssetInfoTestSuiteBase): + +# /shells//asset-information +# /aas/asset-information + + +class AssetInfoTests(ApiTestSuite): def test_simple(self): """ Fetch by id """ - request = generate_one_valid(self.operation, self.sample_cache, {'aasIdentifier': b64urlsafe(self.valid_id)}) + request = generate_one_valid(self.operation, self.sample_cache, self.valid_values) _invoke_and_decode(request, self.conf, True) -@operation('GetThumbnail_AasRepository') -class AasThumbnailBySuperPathSuite(AssetInfoTestSuiteBase): - def setup(self): - op = self.open_api.operations["GetAllAssetAdministrationShells"] - request = generate_one_valid(op, self.sample_cache, {'limit': 1}) - data = _invoke_and_decode(request, self.conf, True) - self.valid_id: str = _lookup(data, ['result', 0, 'id']) +@operation("GetAssetInformation_AasRepository") +class AssetInfoBySuperPathSuite(AasByRepoSuite, AssetInfoTests): + pass + + +@operation("GetAssetInformation") +class AssetInfoBySuperPathSuite(AasTestSuite, AssetInfoTests): + pass + +class GetThumbnailTests(ApiTestSuite): def test_simple(self): """ - Fetch thumbnail by id + Fetch thumbnail """ - request = generate_one_valid(self.operation, self.sample_cache, { - 'aasIdentifier': b64urlsafe(self.valid_id), - }) + request = generate_one_valid(self.operation, self.sample_cache, self.valid_values) _invoke(request, self.conf, True) + +@operation('GetThumbnail_AasRepository') +class AasThumbnailBySuperPathSuite(AasByRepoSuite, GetThumbnailTests): + pass + + +@operation('GetThumbnail') +class AasThumbnailBySuperPathSuite(AasTestSuite, GetThumbnailTests): + pass + + ########################### # Submodel Interface ########################## @@ -832,6 +877,17 @@ def setup(self): } +class SubmodelByAasSuite(ApiTestSuite): + def setup(self): + op = self.open_api.operations["GetAssetAdministrationShell"] + request = generate_one_valid(op, self.sample_cache, {}) + data = _invoke_and_decode(request, self.conf, True) + self.valid_submodel_id: str = _lookup(data, ['submodels', 0, 'keys', 0, 'value']) + self.valid_values = { + 'submodelIdentifier': b64urlsafe(self.valid_submodel_id), + } + + class SubmodelBySubmodelRepoSuite(ApiTestSuite): def setup(self): op = self.open_api.operations["GetAllSubmodels"] @@ -866,10 +922,10 @@ def test_simple(self): # /shells//submodels/ +# /aas//submodel # /submodels/ # /submodel - class GetSubmodelTests(ApiTestSuite): def test_simple(self): """ @@ -888,6 +944,12 @@ class GetSubmodelById_AasRepository(SubmodelByAasRepoSuite, GetSubmodelTests): pass +@operation('GetSubmodel_AAS') +@operation('GetSubmodel-Metadata_AAS') +class GetSubmodelByAas(SubmodelByAasSuite, GetSubmodelTests): + pass + + @operation("GetSubmodelById") @operation("GetSubmodelById-Metadata") class GetSubmodelById(SubmodelBySubmodelRepoSuite, GetSubmodelTests): @@ -916,6 +978,13 @@ class GetSubmodelById_Reference_AasRepository(SubmodelByAasRepoSuite, GetSubmode pass +@operation('GetSubmodel-ValueOnly_AAS') +@operation('GetSubmodelMetadata-Reference_AAS') +@operation('GetSubmodel-Path_AAS') +class GetSubmodelById_Reference_Aas(SubmodelByAasSuite, GetSubmodelRefsTests): + pass + + @operation("GetSubmodelById-ValueOnly") @operation("GetSubmodelById-Reference") @operation("GetSubmodelById-Path") @@ -930,7 +999,9 @@ class GetSubmodel_Reference(SubmodelSuite, GetSubmodelRefsTests): pass # /shells//submodels//submodel-elements -# /shells//submodels//submodel-elements +# /aas/submodels//submodel-elements +# /submodels//submodel-elements +# /submodel/submodel-elements class GetSubmodelElementsTests(ApiTestSuite): @@ -983,11 +1054,17 @@ def test_extent_without_blob_value(self): # TODO: two pagination tests are missing + @operation("GetAllSubmodelElements_AasRepository") class GetAllSubmodelElements_AasRepository(SubmodelByAasRepoSuite, GetSubmodelElementsTests): pass +@operation("GetAllSubmodelElements_AAS") +class GetAllSubmodelElements_AasRepository(SubmodelByAasSuite, GetSubmodelElementsTests): + pass + + @operation("GetAllSubmodelElements_SubmodelRepository") class GetAllSubmodelElements_SubmodelRepository(SubmodelBySubmodelRepoSuite, GetSubmodelElementsTests): pass @@ -1034,6 +1111,13 @@ class GetAllSubmodelElements_ValueOnly_AasRepository(SubmodelByAasRepoSuite, Get pass +@operation("GetAllSubmodelElements-ValueOnly_AAS") +@operation("GetAllSubmodelElementsReference_AAS") +@operation("GetAllSubmodelElementsPath_AAS") +class GetAllSubmodelElements_ValueOnly_AasRepository(SubmodelByAasSuite, GetAllSubmodelElementsValueOnlyTests): + pass + + @operation("GetAllSubmodelElements-ValueOnly_SubmodelRepo") @operation("GetAllSubmodelElements-Reference_SubmodelRepo") @operation("GetAllSubmodelElements-Path_SubmodelRepo") @@ -1062,6 +1146,11 @@ class GetAllSubmodelElements_Metadata_AasRepository(SubmodelByAasRepoSuite, GetA pass +@operation("GetAllSubmodelElements-Metadata_AAS") +class GetAllSubmodelElements_Metadata_AasRepository(SubmodelByAasSuite, GetAllSubmodelElementRefsTests): + pass + + @operation("GetAllSubmodelElements-Metadata_SubmodelRepository") class GetAllSubmodelElements_Metadata_SubmodelRepository(SubmodelBySubmodelRepoSuite, GetAllSubmodelElementRefsTests): pass @@ -1073,6 +1162,9 @@ class GetAllSubmodelElements_Metadata(SubmodelSuite, GetAllSubmodelElementRefsTe # /shells//submodels//submodel-elements/ +# /aas/submodels//submodel-elements/ +# /submodels//submodel-elements/ +# /submodel/submodel-elements/ class SubmodelElementTestsBase(ApiTestSuite): all_submodel_elements = [ @@ -1129,6 +1221,25 @@ def setup(self): self.valid_values['idShortPath'] = _first_iterator_item(self.paths.values())[0] +class SubmodelElementByAasSuite(SubmodelElementTestsBase): + + def setup(self): + self.paths = {} + op = self.open_api.operations["GetAssetAdministrationShell"] + request = generate_one_valid(op, self.sample_cache, {}) + data = _invoke_and_decode(request, self.conf, True) + valid_submodel_id = _lookup(data, ['submodels', 0, 'keys', 0, 'value']) + self.valid_values = { + 'submodelIdentifier': b64urlsafe(valid_submodel_id), + } + op = self.open_api.operations["GetAllSubmodelElements_AAS"] + request = generate_one_valid(op, self.sample_cache, self.valid_values) + data = _invoke_and_decode(request, self.conf, True) + elements = _lookup(data, ['result']) + _collect_submodel_elements(elements, self.paths, '') + self.valid_values['idShortPath'] = _first_iterator_item(self.paths.values())[0] + + class SubmodelElementBySubmodelRepoSuite(SubmodelElementTestsBase): def setup(self): @@ -1222,6 +1333,11 @@ class GetSubmodelElementByPath_AasRepository(SubmodelElementByAasRepoSuite, GetS pass +@operation("GetSubmodelElementByPath_AAS") +class GetSubmodelElementByPath_Aas(SubmodelElementByAasSuite, GetSubmodelElementTests): + pass + + @operation("GetSubmodelElementByPath_SubmodelRepo") class GetSubmodelElementByPath_SubmodelRepository(SubmodelElementBySubmodelRepoSuite, GetSubmodelElementTests): pass @@ -1293,6 +1409,11 @@ class GetSubmodelElementByPath_ValueOnly_AasRepository(SubmodelElementByAasRepoS pass +@operation("GetSubmodelElementByPath-ValueOnly_AAS") +class GetSubmodelElementByPath_ValueOnly_AasRepository(SubmodelElementByAasSuite, GetSubmodelElementValueOnlyTests): + pass + + @operation("GetSubmodelElementByPath-ValueOnly_SubmodelRepo") class GetSubmodelElementByPath_ValueOnly_SubmodelRepository(SubmodelElementBySubmodelRepoSuite, GetSubmodelElementValueOnlyTests): pass @@ -1343,6 +1464,11 @@ class GetSubmodelElementByPath_Path_AasRepository(GetSubmodelElementPathTests, S pass +@operation("GetSubmodelElementByPath-Path_AAS") +class GetSubmodelElementByPath_Path_AasRepository(GetSubmodelElementPathTests, SubmodelElementByAasSuite): + pass + + @operation("GetSubmodelElementByPath-Path_SubmodelRepo") class GetSubmodelElementByPath_Path_SubmodelRepo(GetSubmodelElementPathTests, SubmodelElementBySubmodelRepoSuite): pass @@ -1391,6 +1517,11 @@ class GetSubmodelElementByPath_Path_AasRepository(GetSubmodelElementReferenceTes pass +@operation("GetSubmodelElementByPath-Reference_AAS") +class GetSubmodelElementByPath_Path_AasRepository(GetSubmodelElementReferenceTests, SubmodelElementByAasSuite): + pass + + @operation("GetSubmodelElementByPath-Reference_SubmodelRepo") class GetSubmodelElementByPath_Path_SubmodelRepository(GetSubmodelElementReferenceTests, SubmodelElementBySubmodelRepoSuite): pass @@ -1437,6 +1568,11 @@ class GetSubmodelElementByPath_Metadata_AasRepository(GetSubmodelElementMetadata pass +@operation("GetSubmodelElementByPath-Metadata_AAS") +class GetSubmodelElementByPath_Metadata_SubmodelRepository(GetSubmodelElementMetadataTests, SubmodelElementByAasSuite): + pass + + @operation("GetSubmodelElementByPath-Metadata_SubmodelRepo") class GetSubmodelElementByPath_Metadata_SubmodelRepository(GetSubmodelElementMetadataTests, SubmodelElementBySubmodelRepoSuite): pass @@ -1457,7 +1593,7 @@ def test_no_params(self): try: valid_values['idShortPath'] = self.paths['File'][0] except KeyError: - abort("No submodel element of type 'File' found, skipping test.") + abort(AasTestResult("No submodel element of type 'File' found, skipping test.", Level.WARNING)) request = generate_one_valid(self.operation, self.sample_cache, valid_values) _invoke(request, self.conf, True) @@ -1466,6 +1602,9 @@ def test_no_params(self): class GetFileByPath_AasRepository(GetFileByPathTests, SubmodelElementByAasRepoSuite): pass +@operation("GetFileByPath_AAS") +class GetFileByPath_AasRepository(GetFileByPathTests, SubmodelElementByAasSuite): + pass @operation("GetFileByPath_SubmodelRepo") class GetFileByPath_SubmodelRepo(GetFileByPathTests, SubmodelElementBySubmodelRepoSuite): @@ -1518,6 +1657,7 @@ def test_include_concept_descriptions(self): # /description + @operation("GetDescription") class GetDescriptionTestSuite(ApiTestSuite): def test_contains_suite(self): @@ -1560,49 +1700,27 @@ def test_contains_suite(self): @operation('GetOperationAsyncResult_AasRepository') @operation('GetOperationAsyncResult-ValueOnly_AasRepository') # /aas -@operation('GetAssetAdministrationShell') @operation('PutAssetAdministrationShell') -@operation('GetAssetAdministrationShell-Reference') -@operation('GetAssetInformation') @operation('PutAssetInformation') -@operation('GetThumbnail') @operation('PutThumbnail') @operation('DeleteThumbnail') # /aas/submodel-refs -@operation('GetAllSubmodelReferences') -@operation('GetAllSubmodelReferences') @operation('PostSubmodelReference') @operation('DeleteSubmodelReferenceById') # /aas/submodels/ -@operation('GetSubmodel_AAS') @operation('PutSubmodel_AAS') @operation('DeleteSubmodelById_AAS') @operation('PatchSubmodel_AAS') -@operation('GetSubmodel-Metadata_AAS') @operation('PatchSubmodelMetadata_AAS') -@operation('GetSubmodel-ValueOnly_AAS') @operation('PatchSubmodel-ValueOnly_AAS') -@operation('GetSubmodelMetadata-Reference_AAS') -@operation('GetSubmodel-Path_AAS') # /aas/submodels//submodel-elements -@operation('GetAllSubmodelElements_AAS') @operation('PostSubmodelElement_AAS') -@operation('GetAllSubmodelElements-Metadata_AAS') -@operation('GetAllSubmodelElements-ValueOnly_AAS') -@operation('GetAllSubmodelElementsReference_AAS') -@operation('GetAllSubmodelElementsPath_AAS') -@operation('GetSubmodelElementByPath_AAS') @operation('PutSubmodelElementByPath_AAS') @operation('PostSubmodelElementByPath_AAS') @operation('DeleteSubmodelElementByPath_AAS') @operation('PatchSubmodelElementValueByPath_AAS') -@operation('GetSubmodelElementByPath-Metadata_AAS') @operation('PatchSubmodelElementValueByPath-Metadata_AAS') -@operation('GetSubmodelElementByPath-ValueOnly_AAS') @operation('PatchSubmodelElementValueByPathValueOnly_AAS') -@operation('GetSubmodelElementByPath-Reference_AAS') -@operation('GetSubmodelElementByPath-Path_AAS') -@operation('GetFileByPath_AAS') @operation('PutFileByPath_AAS') @operation('DeleteFileByPath_AAS') @operation('InvokeOperationSync_AAS') diff --git a/aas_test_engines/test_cases/v3_0/api.yml b/aas_test_engines/test_cases/v3_0/api.yml index 153e3f5..0cd4628 100644 --- a/aas_test_engines/test_cases/v3_0/api.yml +++ b/aas_test_engines/test_cases/v3_0/api.yml @@ -2203,6 +2203,16 @@ paths: enum: - deep - core + - name: extent + in: query + description: Determines to which extent the resource is being serialized + required: false + schema: + type: string + default: deep + enum: + - deep + - core responses: "200": description: Requested submodel element in its ValueOnly representation diff --git a/test/acceptance/server.py b/test/acceptance/server.py index ff5d70d..0ce6996 100755 --- a/test/acceptance/server.py +++ b/test/acceptance/server.py @@ -74,27 +74,29 @@ class Params: invalid_accepted: int remove_path_prefix: str = '' -# PYTHONPATH=. python -m aas_test_engines check_server http://localhost:8000/api/v3.0/shells/d3d3LmV4YW1wbGUuY29tL2lkcy9zbS84MTMyXzQxMDJfODA0Ml83NTYx/submodels/d3d3LmV4YW1wbGUuY29tL2lkcy9zbS84MTMyXzQxMDJfODA0Ml8xODYx SubmodelServiceSpecification/SSP-002 --no-verify --output html --remove-path-prefix /submodel > output.html - - params = [ Params( '/api/v3.0', "https://admin-shell.io/aas/API/3/0/AssetAdministrationShellRepositoryServiceSpecification/SSP-002", - 3, 0, + 2, 0, ), Params( '/api/v3.0', "https://admin-shell.io/aas/API/3/0/SubmodelRepositoryServiceSpecification/SSP-002", - 3, 0, + 2, 0, ), Params( '/api/v3.0/shells/aHR0cDovL2N1c3RvbWVyLmNvbS9hYXMvOTE3NV83MDEzXzcwOTFfOTE2OA==/submodels/aHR0cDovL2k0MC5jdXN0b21lci5jb20vdHlwZS8xLzEvRjEzRTg1NzZGNjQ4ODM0Mg==', "https://admin-shell.io/aas/API/3/0/SubmodelServiceSpecification/SSP-002", - 2, 2, + 1, 2, "/submodel", ), - # Params('/api/v3.0', "https://admin-shell.io/aas/API/3/0/AssetAdministrationShellServiceSpecification/SSP-002"), + Params( + '/api/v3.0/shells/aHR0cDovL2N1c3RvbWVyLmNvbS9hYXMvOTE3NV83MDEzXzcwOTFfOTE2OA==/', + "https://admin-shell.io/aas/API/3/0/AssetAdministrationShellServiceSpecification/SSP-002", + 0, 2, + '/aas' + ), ] for param in params: @@ -106,6 +108,8 @@ class Params: ) result, mat = api.execute_tests(conf, param.suite) mat.print() + with open("output.html", "w") as f: + f.write(result.to_html()) # TODO # assert result.ok() assert mat.valid_rejected == param.valid_rejected