etos-test-runner 3.7.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,43 @@
1
+ # Copyright 2020 Axis Communications AB.
2
+ #
3
+ # For a full list of individual contributors, please see the commit history.
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+ """ETOS test runner module."""
17
+ import os
18
+ import logging
19
+ from importlib.metadata import version, PackageNotFoundError
20
+ from etos_lib.logging.logger import setup_logging
21
+ from etos_test_runner.lib.decrypt import decrypt
22
+
23
+ try:
24
+ VERSION = version("etos_test_runner")
25
+ except PackageNotFoundError:
26
+ VERSION = "Unknown"
27
+
28
+ if os.getenv("ETOS_ENCRYPTION_KEY"):
29
+ os.environ["ETOS_RABBITMQ_PASSWORD"] = decrypt(
30
+ os.environ["ETOS_RABBITMQ_PASSWORD"], os.getenv("ETOS_ENCRYPTION_KEY")
31
+ )
32
+
33
+ DEV = os.getenv("DEV", "false").lower() == "true"
34
+ ENVIRONMENT = "development" if DEV else "production"
35
+ setup_logging("ETOS Test Runner", VERSION, ENVIRONMENT)
36
+
37
+ # JSONTas would print all passwords as they are decrypted,
38
+ # which is not safe, so we disable propagation on the loggers.
39
+ # Propagation needs to be set to 0 instead of disabling the
40
+ # logger or setting the loglevel higher because of how the
41
+ # etos library sets up logging.
42
+ logging.getLogger("JSONTas").propagate = 0
43
+ logging.getLogger("Dataset").propagate = 0
@@ -0,0 +1,209 @@
1
+ #!/usr/bin/env python
2
+ # Copyright Axis Communications AB.
3
+ #
4
+ # For a full list of individual contributors, please see the commit history.
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+ # -*- coding: utf-8 -*-
18
+ """ETOS test runner module."""
19
+ import sys
20
+ import logging
21
+ import os
22
+ import signal
23
+ import importlib
24
+ import pkgutil
25
+ from typing import Optional, Union
26
+ from pprint import pprint
27
+ from collections import OrderedDict
28
+
29
+ from etos_lib import ETOS
30
+ from etos_lib.logging.logger import FORMAT_CONFIG
31
+ from jsontas.jsontas import JsonTas
32
+
33
+ from etos_test_runner.lib.testrunner import TestRunner
34
+ from etos_test_runner.lib.iut import Iut
35
+ from etos_test_runner.lib.custom_dataset import CustomDataset
36
+ from etos_test_runner.lib.decrypt import Decrypt, decrypt
37
+
38
+
39
+ # Remove spam from pika.
40
+ logging.getLogger("pika").setLevel(logging.WARNING)
41
+
42
+ _LOGGER = logging.getLogger(__name__)
43
+
44
+
45
+ class ETR:
46
+ """ETOS Test Runner."""
47
+
48
+ context = None
49
+
50
+ def __init__(self) -> None:
51
+ """Initialize ETOS library and start eiffel publisher."""
52
+ self.etos = ETOS("ETOS Test Runner", os.getenv("HOSTNAME"), "ETOS Test Runner")
53
+ if os.getenv("ETOS_ENCRYPTION_KEY"):
54
+ os.environ["RABBITMQ_PASSWORD"] = decrypt(
55
+ os.environ["RABBITMQ_PASSWORD"], os.getenv("ETOS_ENCRYPTION_KEY")
56
+ )
57
+
58
+ self.etos.config.rabbitmq_publisher_from_environment()
59
+ self.etos.config.set("etos_rabbitmq_password", os.environ.get("ETOS_RABBITMQ_PASSWORD"))
60
+ # ETR will print the entire environment just before executing.
61
+ # Hide the password.
62
+ os.environ["RABBITMQ_PASSWORD"] = "*********"
63
+ os.environ["ETOS_RABBITMQ_PASSWORD"] = "*********"
64
+
65
+ self.etos.start_publisher()
66
+ self.environment_id = os.getenv("ENVIRONMENT_ID")
67
+
68
+ signal.signal(signal.SIGTERM, self.graceful_shutdown)
69
+
70
+ @staticmethod
71
+ def graceful_shutdown(*_) -> None:
72
+ """Catch sigterm."""
73
+ raise Exception("ETR has been terminated.") # pylint:disable=broad-exception-raised
74
+
75
+ def download_and_load(self, sub_suite_url: str) -> None:
76
+ """Download and load test json.
77
+
78
+ :param sub_suite_url: URL to where the sub suite information exists.
79
+ """
80
+ response = self.etos.http.get(sub_suite_url)
81
+ json_config = response.json(object_pairs_hook=OrderedDict)
82
+ dataset = CustomDataset()
83
+ dataset.add("decrypt", Decrypt)
84
+ config = JsonTas(dataset).run(json_config)
85
+
86
+ # ETR will print the entire environment just before executing.
87
+ # Hide the encryption key.
88
+ if os.getenv("ETOS_ENCRYPTION_KEY"):
89
+ os.environ["ETOS_ENCRYPTION_KEY"] = "*********"
90
+
91
+ self.etos.config.set("test_config", config)
92
+ self.etos.config.set("context", config.get("context"))
93
+ self.etos.config.set("artifact", config.get("artifact"))
94
+ self.etos.config.set("main_suite_id", config.get("test_suite_started_id"))
95
+ self.etos.config.set("suite_id", config.get("suite_id"))
96
+
97
+ def _run_tests(self) -> Union[int, dict]:
98
+ """Run tests in ETOS test runner.
99
+
100
+ :return: Results of test runner execution.
101
+ """
102
+ iut = Iut(self.etos.config.get("test_config").get("iut"))
103
+ test_runner = TestRunner(iut, self.etos)
104
+ return test_runner.execute()
105
+
106
+ def load_plugins(self) -> None:
107
+ """Load plugins from environment using the name etr_."""
108
+ disable_plugins = os.getenv("DISABLE_PLUGINS")
109
+ disabled_plugins = []
110
+ if disable_plugins:
111
+ disabled_plugins = disable_plugins.split(",")
112
+
113
+ discovered_plugins = {
114
+ name: importlib.import_module(name)
115
+ for _, name, _ in pkgutil.iter_modules()
116
+ if name.startswith("etr_") and name not in disabled_plugins
117
+ }
118
+ plugins = []
119
+ for name, module in discovered_plugins.items():
120
+ _LOGGER.info("Loading plugin: %r", name)
121
+ if not hasattr(module, "ETRPlugin"):
122
+ raise AttributeError(f"{name} does not have an ETRPlugin class!")
123
+ plugins.append(module.ETRPlugin(self.etos))
124
+ self.etos.config.set("plugins", plugins)
125
+
126
+ def get_sub_suite_url(self, environment_id: str) -> Optional[str]:
127
+ """Get sub suite from ETOS environment defined event.
128
+
129
+ :param environment_id: ID of th environment defined event.
130
+ :return: URL for sub suite.
131
+ """
132
+ query = (
133
+ """
134
+ {
135
+ environmentDefined(search: "{'meta.id': '%s'}") {
136
+ edges {
137
+ node {
138
+ data {
139
+ uri
140
+ }
141
+ }
142
+ }
143
+ }
144
+ }
145
+ """
146
+ % environment_id
147
+ )
148
+ # Timeout can be configured using ETOS_DEFAULT_WAIT_TIMEOUT environment variable
149
+ # Default timeout is 60s.
150
+ wait_generator = self.etos.utils.wait(self.etos.graphql.execute, query=query)
151
+ for response in wait_generator:
152
+ if response:
153
+ try:
154
+ _, environment_defined = next(
155
+ self.etos.graphql.search_for_nodes(response, "environmentDefined")
156
+ )
157
+ except StopIteration:
158
+ continue
159
+ return environment_defined["data"]["uri"]
160
+ return None
161
+
162
+ def run_etr(self) -> Union[int, dict]:
163
+ """Send activity events and run ETR.
164
+
165
+ :return: Result of testrunner execution.
166
+ """
167
+ _LOGGER.info("Starting ETR.")
168
+ sub_suite_url = self.get_sub_suite_url(self.environment_id)
169
+ if sub_suite_url is None:
170
+ raise TimeoutError(
171
+ f"Could not get sub suite environment event with id {self.environment_id!r}"
172
+ )
173
+ self.download_and_load(sub_suite_url)
174
+ FORMAT_CONFIG.identifier = self.etos.config.get("suite_id")
175
+ self.load_plugins()
176
+ try:
177
+ activity_name = self.etos.config.get("test_config").get("name")
178
+ triggered = self.etos.events.send_activity_triggered(activity_name)
179
+ self.etos.events.send_activity_started(triggered)
180
+ result = self._run_tests()
181
+ except Exception as exc: # pylint:disable=broad-except
182
+ self.etos.events.send_activity_finished(
183
+ triggered, {"conclusion": "FAILED", "description": str(exc)}
184
+ )
185
+ raise
186
+ self.etos.events.send_activity_finished(triggered, {"conclusion": "SUCCESSFUL"})
187
+ _LOGGER.info("ETR finished.")
188
+ return result
189
+
190
+
191
+ def main() -> None:
192
+ """Start ETR."""
193
+ etr = ETR()
194
+ result = etr.run_etr()
195
+ if isinstance(result, dict):
196
+ pprint(result)
197
+ _LOGGER.info("Done. Exiting")
198
+ _LOGGER.info(result)
199
+ sys.exit(result)
200
+
201
+
202
+ def run():
203
+ """Entry point to ETR."""
204
+ logging.basicConfig(level=logging.INFO, stream=sys.stdout)
205
+ main()
206
+
207
+
208
+ if __name__ == "__main__":
209
+ run()
@@ -0,0 +1,16 @@
1
+ # Copyright 2020 Axis Communications AB.
2
+ #
3
+ # For a full list of individual contributors, please see the commit history.
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+ """ETOS test runner common helpers."""
@@ -0,0 +1,16 @@
1
+ #!/bin/bash
2
+ # Copyright 2020 Axis Communications AB.
3
+ #
4
+ # For a full list of individual contributors, please see the commit history.
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
@@ -0,0 +1,37 @@
1
+ # Copyright Axis Communications AB.
2
+ #
3
+ # For a full list of individual contributors, please see the commit history.
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+ """Custom dataset module."""
17
+ from jsontas.dataset import Dataset
18
+
19
+
20
+ class CustomDataset(Dataset):
21
+ """Custom dataset for ETR to decrypt secrets.
22
+
23
+ This custom dataset removes all default JsonTas datastructures
24
+ as we are going to run JsonTas on the sub suite information
25
+ retrieved from the environment provider.
26
+ This sub suite information is quite large and if we keep the
27
+ default datastructures the ETR would be susceptible to remote
28
+ code execution. This custom dataset shall only be used when
29
+ decrypting secrets.
30
+ """
31
+
32
+ def __init__(self):
33
+ """Initialize an empty dataset."""
34
+ super().__init__()
35
+ # pylint:disable=unused-private-member
36
+ # It is used by the parent class.
37
+ self.__dataset = {}
@@ -0,0 +1,47 @@
1
+ # Copyright Axis Communications AB.
2
+ #
3
+ # For a full list of individual contributors, please see the commit history.
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+ """JSONTas decrypt string data structure module."""
17
+ import os
18
+ from cryptography.fernet import Fernet
19
+ from jsontas.data_structures.datastructure import DataStructure
20
+
21
+ # pylint:disable=too-few-public-methods
22
+
23
+
24
+ def decrypt(value, key):
25
+ """Decrypt a string.
26
+
27
+ :param value: Data to decrypt.
28
+ :type value: str
29
+ :param key: Encryption key to decrypt data with.
30
+ :type key: str
31
+ :return: Decrypted data.
32
+ :rtype: str
33
+ """
34
+ return Fernet(key).decrypt(value).decode()
35
+
36
+
37
+ class Decrypt(DataStructure):
38
+ """Decrypt an encrypted string."""
39
+
40
+ def execute(self):
41
+ """Execute datastructure.
42
+
43
+ :return: Name of key. None, to tel JSONTas to not override key name, and decrypted value.
44
+ """
45
+ key = os.getenv("ETOS_ENCRYPTION_KEY")
46
+ assert key is not None, "ETOS_ENCRYPTION_KEY environment variable must be set"
47
+ return None, decrypt(self.data.get("value"), key)
@@ -0,0 +1,16 @@
1
+ #!/bin/bash
2
+ # Copyright 2020 Axis Communications AB.
3
+ #
4
+ # For a full list of individual contributors, please see the commit history.
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
@@ -0,0 +1,62 @@
1
+ # Copyright Axis Communications AB.
2
+ #
3
+ # For a full list of individual contributors, please see the commit history.
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+ # -*- coding: utf-8 -*-
17
+ """ETOS internal message bus module."""
18
+ import os
19
+ from etos_lib import ETOS
20
+ from etos_lib.logging.log_publisher import RabbitMQLogPublisher
21
+
22
+
23
+ class EventPublisher:
24
+ """EventPublisher helps in sending events to the internal ETOS message bus."""
25
+
26
+ disabled = False
27
+
28
+ def __init__(self, etos: ETOS):
29
+ """Set up, but do not start, the RabbitMQ publisher."""
30
+ if os.getenv("DISABLE_EVENT_PUBLISHING", "false").lower() == "true":
31
+ self.disabled = True
32
+ publisher = etos.config.get("event_publisher")
33
+ if self.disabled is False and publisher is None:
34
+ config = etos.config.etos_rabbitmq_publisher_data()
35
+ # This password should already be decrypted when setting up the logging.
36
+ config["password"] = etos.config.get("etos_rabbitmq_password")
37
+ publisher = RabbitMQLogPublisher(**config, routing_key=None)
38
+ etos.config.set("event_publisher", publisher)
39
+ self.publisher = publisher
40
+ self.identifier = etos.config.get("suite_id")
41
+
42
+ def __del__(self):
43
+ """Close the RabbitMQ publisher."""
44
+ self.close()
45
+
46
+ def close(self):
47
+ """Close the RabbitMQ publisher if it is started."""
48
+ if self.publisher is not None and self.publisher.is_alive():
49
+ self.publisher.wait_for_unpublished_events()
50
+ self.publisher.close()
51
+ self.publisher.wait_close()
52
+
53
+ def publish(self, event: dict):
54
+ """Publish an event to the ETOS internal message bus."""
55
+ if self.disabled:
56
+ return
57
+ if self.publisher is None:
58
+ return
59
+ if not self.publisher.running:
60
+ self.publisher.start()
61
+ routing_key = f"{self.identifier}.event.{event.get('event')}"
62
+ self.publisher.send_event(event, routing_key=routing_key)