Skip to content

Commit

Permalink
Updated tests to run the ESR more like the real world
Browse files Browse the repository at this point in the history
Also added a few extra checks and testability things to the ESR
  • Loading branch information
t-persson committed Feb 9, 2024
1 parent 94acc81 commit 4e319f3
Show file tree
Hide file tree
Showing 9 changed files with 617 additions and 360 deletions.
18 changes: 13 additions & 5 deletions projects/etos_suite_runner/src/etos_suite_runner/esr.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,12 +102,13 @@ def _release_environment(self) -> None:
if not status:
self.logger.error(message)

def run_suites(self, triggered: EiffelActivityTriggeredEvent) -> None:
def run_suites(self, triggered: EiffelActivityTriggeredEvent) -> list[str]:
"""Start up a suite runner handling multiple suites that execute within test runners.
Will only start the test activity if there's a 'slot' available.
:param triggered: Activity triggered.
:return: List of main suite IDs
"""
context = triggered.meta.event_id
self.etos.config.set("context", context)
Expand All @@ -125,12 +126,15 @@ def run_suites(self, triggered: EiffelActivityTriggeredEvent) -> None:

try:
self.logger.info("Get test environment.")
threading.Thread(target=self._request_environment, args=(ids,), daemon=True).start()
threading.Thread(
target=self._request_environment, args=(ids.copy(),), daemon=True
).start()

self.etos.events.send_activity_started(triggered, {"CONTEXT": context})

self.logger.info("Starting ESR.")
runner.start_suites_and_wait()
return ids
except EnvironmentProviderException:
self.logger.info("Release test environment.")
self._release_environment()
Expand All @@ -143,8 +147,11 @@ def verify_input() -> None:
assert os.getenv("SOURCE_HOST"), "SOURCE_HOST environment variable not provided."
assert os.getenv("TERCC"), "TERCC environment variable not provided."

def run(self) -> None:
"""Run the ESR main loop."""
def run(self) -> list[str]:
"""Run the ESR main loop.
:return: List of test suites (main suites) that were started.
"""
tercc_id = None
try:
tercc_id = self.params.tercc.meta.event_id
Expand Down Expand Up @@ -186,10 +193,11 @@ def run(self) -> None:
raise

try:
self.run_suites(triggered)
ids = self.run_suites(triggered)
self.etos.events.send_activity_finished(
triggered, {"conclusion": "SUCCESSFUL"}, {"CONTEXT": context}
)
return ids
except Exception as exception: # pylint:disable=broad-except
reason = str(exception)
self.logger.exception(
Expand Down
4 changes: 4 additions & 0 deletions projects/etos_suite_runner/src/etos_suite_runner/lib/suite.py
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,10 @@ def start(self) -> None:
"Environment received. Starting up a sub suite", extra={"user_log": True}
)
sub_suite_definition = self._download_sub_suite(sub_suite_environment)
if sub_suite_definition is None:
raise EnvironmentProviderException(
"URL to sub suite is missing", self.etos.config.get("task_id")
)
sub_suite_definition["id"] = sub_suite_environment["meta"]["id"]
sub_suite = SubSuite(
self.etos, sub_suite_definition, self.suite["test_suite_started_id"]
Expand Down
1 change: 1 addition & 0 deletions projects/etos_suite_runner/test-requirements.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
mock
pytest
pytest-cov
mongomock
124 changes: 124 additions & 0 deletions projects/etos_suite_runner/tests/library/fake_database.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
# Copyright Axis Communications AB.
#
# For a full list of individual contributors, please see the commit history.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Fake database library helpers."""
from queue import Queue
from threading import Event, RLock, Timer
from typing import Any, Iterator, Optional

from etcd3gw.lease import Lease

# pylint:disable=unused-argument


class FakeDatabase:
"""A fake database that follows the etcd client."""

lock = RLock()

def __init__(self):
"""Initialize fake reader and writer."""
self.db_dict = {}
self.expire = []
self.leases = {}
self.watchers = []

def __call__(self):
"""Database instantiation faker."""
return self

def watch(self, path: str, range_end: Optional[str] = None) -> (Event, Iterator[dict]):
"""Watch for changes of a path."""
canceled = Event()
queue = Queue()

def cancel():
canceled.set()
queue.put(None)

def iterator():
self.watchers.append(queue)
try:
while not canceled.is_set():
event = queue.get()
if event is None:
canceled.set()
if not canceled.is_set():
yield event
finally:
self.watchers.remove(queue)

return iterator(), cancel

def __event(self, event: dict) -> None:
"""Send an event to all watchers."""
for watcher in self.watchers:
watcher.put_nowait(event)

def put(self, item: str, value: Any, lease: Optional[Lease] = None) -> None:
"""Put an item into database."""
with self.lock:
self.db_dict.setdefault(item, [])
if lease is not None:
self.leases[item] = lease
timer = Timer(self.expire[lease.id], self.delete, args=(item,))
timer.daemon = True
timer.start()
self.db_dict[item].append(str(value).encode())
self.__event({"kv": {"key": item.encode(), "value": str(value).encode()}})

def lease(self, ttl=30) -> Lease:
"""Create a lease."""
with self.lock:
self.expire.append(ttl)
# ttl is unused since we do not actually make the post request that the regular
# etcd client does. First argument to `Lease` is the ID that was returned by the
# etcd server.
return Lease(len(self.expire) - 1)

def get(self, path: str) -> list[bytes]:
"""Get an item from database."""
if isinstance(path, bytes):
path = path.decode()
with self.lock:
return list(reversed(self.db_dict.get(path, [])))

def get_prefix(self, prefix: str) -> list[tuple[bytes, dict]]:
"""Get items based on prefix."""
if isinstance(prefix, bytes):
prefix = prefix.decode()
paths = []
with self.lock:
for key, value in self.db_dict.items():
if key.startswith(prefix):
paths.append((value[-1], {"key": key.encode()}))
return paths

def delete(self, path: str) -> None:
"""Delete a single item."""
with self.lock:
del self.db_dict[path]
if self.leases.get(path):
self.expire.pop(self.leases.get(path).id)
del self.leases[path]
self.__event({"kv": {"key": path.encode()}, "type": "DELETE"})

def delete_prefix(self, prefix: str) -> None:
"""Delete items based on prefix."""
with self.lock:
db_dict = self.db_dict.copy()
for key in db_dict:
if key.startswith(prefix):
self.delete(key)
Loading

0 comments on commit 4e319f3

Please sign in to comment.