insightconnect-plugin-runtime 5.2.4__py3-none-any.whl → 5.3.1__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.
- insightconnect_plugin_runtime/api/endpoints.py +2 -0
- insightconnect_plugin_runtime/exceptions.py +10 -0
- {insightconnect_plugin_runtime-5.2.4.dist-info → insightconnect_plugin_runtime-5.3.1.dist-info}/METADATA +58 -12
- {insightconnect_plugin_runtime-5.2.4.dist-info → insightconnect_plugin_runtime-5.3.1.dist-info}/RECORD +9 -9
- {insightconnect_plugin_runtime-5.2.4.dist-info → insightconnect_plugin_runtime-5.3.1.dist-info}/WHEEL +1 -1
- tests/plugin/__init__.py +15 -8
- tests/plugin/hello_world/hello_world/komand_hello_world/triggers/hello_trigger/trigger.py +7 -1
- tests/plugin/hello_world/hello_world/komand_hello_world/triggers/throw_exception_trigger/trigger.py +2 -2
- {insightconnect_plugin_runtime-5.2.4.dist-info → insightconnect_plugin_runtime-5.3.1.dist-info}/top_level.txt +0 -0
|
@@ -157,11 +157,13 @@ class Endpoints:
|
|
|
157
157
|
500:
|
|
158
158
|
description: Unexpected error
|
|
159
159
|
"""
|
|
160
|
+
self.logger.info("Plugin task beginning execution...")
|
|
160
161
|
input_message = request.get_json(force=True)
|
|
161
162
|
self.logger.debug("Request input: %s", input_message)
|
|
162
163
|
Endpoints.validate_action_trigger_task_empty_input(input_message)
|
|
163
164
|
Endpoints.validate_action_trigger_task_name(input_message, name, "task")
|
|
164
165
|
output = self.run_action_trigger_task(input_message)
|
|
166
|
+
self.logger.info("Plugin task finished execution...")
|
|
165
167
|
return output
|
|
166
168
|
|
|
167
169
|
@legacy.route("/triggers/<string:name>/test", methods=["POST"])
|
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
# -*- coding: utf-8 -*-
|
|
2
|
+
import structlog
|
|
3
|
+
logger = structlog.get_logger("plugin")
|
|
4
|
+
|
|
5
|
+
|
|
2
6
|
class ClientException(Exception):
|
|
3
7
|
"""
|
|
4
8
|
An exception which marks an error made by the plugin invoker.
|
|
@@ -120,6 +124,12 @@ class ConnectionTestException(Exception):
|
|
|
120
124
|
|
|
121
125
|
self.data = str(data) if data else ""
|
|
122
126
|
|
|
127
|
+
# Safeguard to ensure the exception is logged across all plugins even if the plugin
|
|
128
|
+
# itself does not call `self.logger.error(<error info>)`
|
|
129
|
+
params = ["cause", "assistance", "data", "preset"]
|
|
130
|
+
info_log = ", ".join([f"{atr}='{getattr(self, atr)}'" for atr in params if getattr(self, atr)])
|
|
131
|
+
logger.error(f"Plugin exception instantiated. {info_log}")
|
|
132
|
+
|
|
123
133
|
def __str__(self):
|
|
124
134
|
if self.data:
|
|
125
135
|
return "Connection test failed!\n\n{cause} {assistance} Response was: {data}".format(
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: insightconnect-plugin-runtime
|
|
3
|
-
Version: 5.
|
|
3
|
+
Version: 5.3.1
|
|
4
4
|
Summary: InsightConnect Plugin Runtime
|
|
5
5
|
Home-page: https://github.com/rapid7/komand-plugin-sdk-python
|
|
6
6
|
Author: Rapid7 Integrations Alliance
|
|
@@ -15,11 +15,11 @@ Description-Content-Type: text/markdown
|
|
|
15
15
|
Requires-Dist: requests ==2.31.0
|
|
16
16
|
Requires-Dist: python-jsonschema-objects ==0.3.12
|
|
17
17
|
Requires-Dist: jsonschema ==3.2.0
|
|
18
|
-
Requires-Dist: certifi ==
|
|
18
|
+
Requires-Dist: certifi ==2023.7.22
|
|
19
19
|
Requires-Dist: Flask ==2.3.2
|
|
20
20
|
Requires-Dist: gunicorn ==20.0.4
|
|
21
|
-
Requires-Dist: greenlet
|
|
22
|
-
Requires-Dist: gevent ==
|
|
21
|
+
Requires-Dist: greenlet ==3.0.1
|
|
22
|
+
Requires-Dist: gevent ==23.9.1
|
|
23
23
|
Requires-Dist: marshmallow ==3.4.0
|
|
24
24
|
Requires-Dist: apispec ==3.2.0
|
|
25
25
|
Requires-Dist: apispec-webframeworks ==0.5.2
|
|
@@ -48,10 +48,10 @@ to get started.
|
|
|
48
48
|
|
|
49
49
|
## Development of the InsightConnect Plugin Runtime
|
|
50
50
|
|
|
51
|
-
The Python Runtime codebase is built to support Python 3.
|
|
51
|
+
The Python Runtime codebase is built to support Python 3.9.18 as of version 5.3.0. The following dependencies will need
|
|
52
52
|
to be installed when developing or testing the Plugin Runtime:
|
|
53
53
|
|
|
54
|
-
- Python 3.
|
|
54
|
+
- Python 3.9.18
|
|
55
55
|
- Docker
|
|
56
56
|
- make
|
|
57
57
|
- tox
|
|
@@ -67,19 +67,51 @@ version and activate it. Then build, install, and confirm the package has been i
|
|
|
67
67
|
> source venv/bin/activate
|
|
68
68
|
> pip install -e ./
|
|
69
69
|
> pip list | grep insightconnect-plugin-runtime
|
|
70
|
-
insightconnect-plugin-runtime
|
|
70
|
+
insightconnect-plugin-runtime 5.2.2
|
|
71
71
|
```
|
|
72
72
|
|
|
73
73
|
#### Building the InsightConnect Plugin Runtime Docker Images
|
|
74
74
|
|
|
75
|
-
Currently the `3
|
|
76
|
-
dockerfile for testing purposes, such as `3-
|
|
75
|
+
Currently the `python-3` dockerfile is used by default when building the docker image. If you want to specify another
|
|
76
|
+
dockerfile for testing purposes, such as `python-3-slim`, you can pass it as an argument.
|
|
77
77
|
|
|
78
78
|
```
|
|
79
|
-
make build-image DOCKERFILE=3-
|
|
79
|
+
make build-image DOCKERFILE=python-3-slim
|
|
80
80
|
```
|
|
81
81
|
|
|
82
|
-
This will overwrite the default `3
|
|
82
|
+
This will overwrite the default `python-3`, provided that it exists in the `dockerfiles` directory.
|
|
83
|
+
|
|
84
|
+
#### Building/troubleshooting local images (Mac M1)
|
|
85
|
+
|
|
86
|
+
Our images are built with OS/ARCH:linux/amd64 due to our ci.yml file specifying linux. Historically this has
|
|
87
|
+
not caused any issues but with an M1 Mac, docker will now use arm64. This is because docker builds the image to match the
|
|
88
|
+
distribution of the OS running the command. To ensure that building locally and on GH produces an image of the same
|
|
89
|
+
architecture the `--platform` specifier is passed in the dockerfile.
|
|
90
|
+
|
|
91
|
+
If you build a plugin locally we can see further issues that:
|
|
92
|
+
- the local image does match the current OS distro so docker continues to look remotely..
|
|
93
|
+
- docker can not find this (newly created) image remotely on docker registry (dev images should not be pushed to registry)
|
|
94
|
+
|
|
95
|
+
To overcome this we need to specify `platform=linux/amd64` in the plugin docker file. E.g. if building plugins/html then
|
|
96
|
+
this file should be updated `plugins/html/Dockerfile`.
|
|
97
|
+
*Note*: building this arch on mac M1 will cause the build to be slower.
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
Example error of local image not matching OS distro without passing the `--platform` flag:
|
|
101
|
+
```
|
|
102
|
+
=> ERROR [internal] load metadata for docker.io/rapid7/insightconnect-python-3-slim-plugin:latest 1.4s
|
|
103
|
+
------
|
|
104
|
+
> [internal] load metadata for docker.io/rapid7/insightconnect-python-3-slim-plugin:latest:
|
|
105
|
+
------
|
|
106
|
+
Dockerfile:1
|
|
107
|
+
--------------------
|
|
108
|
+
1 | >>> FROM rapid7/insightconnect-python-3-slim-plugin
|
|
109
|
+
2 | LABEL organization=rapid7
|
|
110
|
+
3 | LABEL sdk=python
|
|
111
|
+
--------------------
|
|
112
|
+
ERROR: failed to solve: rapid7/insightconnect-python-3-slim-plugin: pull access denied, repository does not exist or may require authorization: server message: insufficient_scope: authorization failed
|
|
113
|
+
make: *** [image] Error 1
|
|
114
|
+
```
|
|
83
115
|
|
|
84
116
|
### Testing Sample Plugin
|
|
85
117
|
The easiest way to test changes to the runtime is by running it locally against one of the [sample plugins](./samples)
|
|
@@ -145,9 +177,21 @@ Running all tests:
|
|
|
145
177
|
|
|
146
178
|
Running a specific test file:
|
|
147
179
|
```
|
|
148
|
-
> tox -e
|
|
180
|
+
> tox -e py39 -- tests/plugin/hello_world/tests/test_cli.py
|
|
149
181
|
```
|
|
150
182
|
|
|
183
|
+
## Plugin vs Slim Plugin Comparison
|
|
184
|
+
|
|
185
|
+
| | Plugin | Slim Plugin |
|
|
186
|
+
|:------------------|:-------:|:-----------:|
|
|
187
|
+
| Python Version | 3.9.18 | 3.9.18 |
|
|
188
|
+
| OS | Alpine | Bullseye |
|
|
189
|
+
| Package installer | apk | apt |
|
|
190
|
+
| Shell | /bin/sh | /bin/bash |
|
|
191
|
+
| Image Size | ~350MB | ~180MB |
|
|
192
|
+
|
|
193
|
+
Note that for the plugin image, we run `apk update` and `apk add ..` which leads to a longer build time.
|
|
194
|
+
|
|
151
195
|
## Release
|
|
152
196
|
|
|
153
197
|
To release a new version of the InsightConnect Python Plugin Runtime, the below steps must be followed:
|
|
@@ -168,6 +212,8 @@ after cloning this repository.
|
|
|
168
212
|
|
|
169
213
|
## Changelog
|
|
170
214
|
|
|
215
|
+
* 5.3.1 - New logging added to the beginning and end of a task | New logging when an exception is instantiated.
|
|
216
|
+
* 5.3.0 - Update base images to pull Python 3.9.18 | python packages bump | rename image to drop python minor version.
|
|
171
217
|
* 5.2.4 - Extended logging with OrgID and IntID
|
|
172
218
|
* 5.2.3 - Extended logging in AWSClient
|
|
173
219
|
* 5.2.2 - Fix longstanding bug where some error responses from a plugin could be in HTML format instead of JSON
|
|
@@ -3,7 +3,7 @@ insightconnect_plugin_runtime/action.py,sha256=8gsOONf7mzY83O3DNjCBIafk7C7acnf7m
|
|
|
3
3
|
insightconnect_plugin_runtime/cli.py,sha256=Pb-Janu-XfRlSXxPHh30OIquljWptrhhS51C3clJqh4,8939
|
|
4
4
|
insightconnect_plugin_runtime/connection.py,sha256=4bHHV2B0UFGsAtvLu1fiYQRwx7fissUakHPUyjLQO0E,2340
|
|
5
5
|
insightconnect_plugin_runtime/dispatcher.py,sha256=ru7njnyyWE1-oD-VbZJ-Z8tELwvDf69rM7Iezs4rbnw,1774
|
|
6
|
-
insightconnect_plugin_runtime/exceptions.py,sha256=
|
|
6
|
+
insightconnect_plugin_runtime/exceptions.py,sha256=rC74M9aCSrY7J-nq9ttsccLkNbHR0gbZ2SRvXlCgLx0,6507
|
|
7
7
|
insightconnect_plugin_runtime/helper.py,sha256=m5PxN04-NPXM1X10S2wwjqmiLvnNntd6TnwLoW4nnus,21108
|
|
8
8
|
insightconnect_plugin_runtime/metrics.py,sha256=hf_Aoufip_s4k4o8Gtzz90ymZthkaT2e5sXh5B4LcF0,3186
|
|
9
9
|
insightconnect_plugin_runtime/plugin.py,sha256=YEwxPHyIYsQcGAZYQZqh60POQOzbrtsZAcA5TcBmy8g,22708
|
|
@@ -15,7 +15,7 @@ insightconnect_plugin_runtime/trigger.py,sha256=Zq3cy68N3QxAGbNZKCID6CZF05Zi7YD2
|
|
|
15
15
|
insightconnect_plugin_runtime/util.py,sha256=lE6UVOYyzY9Ldyqpf91V24CvvdKjUu3WjGOSy957Sm0,8073
|
|
16
16
|
insightconnect_plugin_runtime/variables.py,sha256=7FjJGnU7KUR7m9o-_tRq7Q3KiaB1Pp0Apj1NGgOwrJk,3056
|
|
17
17
|
insightconnect_plugin_runtime/api/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
18
|
-
insightconnect_plugin_runtime/api/endpoints.py,sha256=
|
|
18
|
+
insightconnect_plugin_runtime/api/endpoints.py,sha256=jjKTFArNIOJkZw-L0QU_1yzRcKC6xyAC8ge86M6fQjQ,29099
|
|
19
19
|
insightconnect_plugin_runtime/api/schemas.py,sha256=jRmDrwLJTBl-iQOnyZkSwyJlCWg4eNjAnKfD9Eko4z0,2754
|
|
20
20
|
insightconnect_plugin_runtime/clients/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
21
21
|
insightconnect_plugin_runtime/clients/aws_client.py,sha256=3Ytx-uchkl7dTcwYKZPKvCJaWralEqZUZeireOCWlPo,20290
|
|
@@ -23,7 +23,7 @@ insightconnect_plugin_runtime/clients/oauth.py,sha256=bWtAGRMwdK4dw9vMPcw9usklyI
|
|
|
23
23
|
insightconnect_plugin_runtime/data/input_message_schema.json,sha256=7_BcHi6UOBiVWGrrJHHn5IoddteXjL7GOKETdO9T2DE,1770
|
|
24
24
|
insightconnect_plugin_runtime/data/output_message_schema.json,sha256=Qya6U-NR5MfOlw4V98VpQzGBVq75eGMUQhI-j3yxOHI,1137
|
|
25
25
|
tests/__init__.py,sha256=uKG3ssG-d51oNHQxKheE9Wzcusd7OW3ei6EYT3WxPnw,106
|
|
26
|
-
tests/plugin/__init__.py,sha256=
|
|
26
|
+
tests/plugin/__init__.py,sha256=ZTPmMxZoDAHcbFqyIQVFkeifTfun5f5r_hffPP17Y2w,2713
|
|
27
27
|
tests/plugin/hello_world/__init__.py,sha256=fzr7Uek0yDCKBchW3fgtv1IAp8JIlW335GwENk4-wPk,42
|
|
28
28
|
tests/plugin/hello_world/hello_world/__init__.py,sha256=rJm2tr4rZ2uR9AfjMyoQRntFM0JOf2SpHCfO7uWho2g,1018
|
|
29
29
|
tests/plugin/hello_world/hello_world/setup.py,sha256=-NAHjSoYB-qU4fZrs_0nHzN7OxkSJ7OHUvQE8SXA6eg,467
|
|
@@ -44,13 +44,13 @@ tests/plugin/hello_world/hello_world/komand_hello_world/connection/schema.py,sha
|
|
|
44
44
|
tests/plugin/hello_world/hello_world/komand_hello_world/triggers/__init__.py,sha256=6gwJkxwZLglGm_nIhMo96z5uM2ENAIHYxpJneuEDRrU,221
|
|
45
45
|
tests/plugin/hello_world/hello_world/komand_hello_world/triggers/hello_trigger/__init__.py,sha256=b8c55qKjWULUk5uhTvzSs246lm8VQxw0h4nvTyYqWuA,74
|
|
46
46
|
tests/plugin/hello_world/hello_world/komand_hello_world/triggers/hello_trigger/schema.py,sha256=NGOXa58BprKTZjyF4BNGPPnRnGnAqwVnZYNiIWElDZg,944
|
|
47
|
-
tests/plugin/hello_world/hello_world/komand_hello_world/triggers/hello_trigger/trigger.py,sha256=
|
|
47
|
+
tests/plugin/hello_world/hello_world/komand_hello_world/triggers/hello_trigger/trigger.py,sha256=PYPY8aW1h6mvQf61sE3ngopXm5SCOeRyhSm7-I1-Hr0,1148
|
|
48
48
|
tests/plugin/hello_world/hello_world/komand_hello_world/triggers/return_bad_json_trigger/__init__.py,sha256=PxfCcfOn24CwtojXbyI8qYTGhfv49kZMR5P30UdJhdg,82
|
|
49
49
|
tests/plugin/hello_world/hello_world/komand_hello_world/triggers/return_bad_json_trigger/schema.py,sha256=7M5gAmMzj_DWSZ982yu-J8yL6mjlCPld1nmNkx30_UE,960
|
|
50
50
|
tests/plugin/hello_world/hello_world/komand_hello_world/triggers/return_bad_json_trigger/trigger.py,sha256=1amZOxQiWaSdUwGjN-siTVSZw71ouUX4Xlp73Gh17yc,717
|
|
51
51
|
tests/plugin/hello_world/hello_world/komand_hello_world/triggers/throw_exception_trigger/__init__.py,sha256=gwdLBOIougxWEhHEQkOueHlWv0Ocq0EjzlfqwcfLucA,83
|
|
52
52
|
tests/plugin/hello_world/hello_world/komand_hello_world/triggers/throw_exception_trigger/schema.py,sha256=Y1FozH6fhm8apf_5yncc3mDT51Xqw_c-JzgB_x1sssU,962
|
|
53
|
-
tests/plugin/hello_world/hello_world/komand_hello_world/triggers/throw_exception_trigger/trigger.py,sha256=
|
|
53
|
+
tests/plugin/hello_world/hello_world/komand_hello_world/triggers/throw_exception_trigger/trigger.py,sha256=pydbyz09YdLzq_NrdvNW11Fhr53INn1NWa4TZ7L9DUw,696
|
|
54
54
|
tests/plugin/hello_world/hello_world/komand_hello_world/util/__init__.py,sha256=RAUlyeHr0LBBRVgwatrSBl7iAaUmrw8Z6BAZlCby_vY,40
|
|
55
55
|
tests/plugin/hello_world/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
56
56
|
tests/plugin/hello_world/tests/conftest.py,sha256=fM3XJmVDHMeYZA__yjXVocgx7kwgr5FRwcWsY1XsV70,1046
|
|
@@ -71,7 +71,7 @@ tests/unit/test_schema.py,sha256=swWZPRo_Q4M6VHte-srmxcV2wH-XS7pgmNRxpaL0Qrg,642
|
|
|
71
71
|
tests/unit/test_server_spec.py,sha256=7hmXdsY36nP0ENhijrolBInAMJwht2-Z-D6yhcg_zZI,578
|
|
72
72
|
tests/unit/test_trigger.py,sha256=E53mAUoVyponWu_4IQZ0IC1gQ9lakBnTn_9vKN2IZfg,1692
|
|
73
73
|
tests/unit/test_variables.py,sha256=OUEOqGYZA3Nd5oKk5GVY3hcrWKHpZpxysBJcO_v5gzs,291
|
|
74
|
-
insightconnect_plugin_runtime-5.
|
|
75
|
-
insightconnect_plugin_runtime-5.
|
|
76
|
-
insightconnect_plugin_runtime-5.
|
|
77
|
-
insightconnect_plugin_runtime-5.
|
|
74
|
+
insightconnect_plugin_runtime-5.3.1.dist-info/METADATA,sha256=nNNrHZvzCIPzoz3isVDx8T05UKQckWjbQOzz6Wp03mY,12026
|
|
75
|
+
insightconnect_plugin_runtime-5.3.1.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
|
|
76
|
+
insightconnect_plugin_runtime-5.3.1.dist-info/top_level.txt,sha256=AJtyJOpiFzHxsbHUICTcUKXyrGQ3tZxhrEHsPjJBvEA,36
|
|
77
|
+
insightconnect_plugin_runtime-5.3.1.dist-info/RECORD,,
|
tests/plugin/__init__.py
CHANGED
|
@@ -55,10 +55,16 @@ def run_trigger(input_file, output_file, plugin, expect_timeout=False):
|
|
|
55
55
|
plugin.triggers[trigger_name].dispatcher = capture
|
|
56
56
|
|
|
57
57
|
executor = thread.ThreadPoolExecutor()
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
58
|
+
|
|
59
|
+
if expect_timeout:
|
|
60
|
+
future = executor.submit(plugin.handle_step, input_message)
|
|
61
|
+
future_ans = future.exception()
|
|
62
|
+
done = future_ans.output
|
|
63
|
+
else:
|
|
64
|
+
executor.submit(plugin.handle_step, input_message)
|
|
65
|
+
future = executor.submit(capture.wait_for_caught_message)
|
|
66
|
+
out = futures.wait([future], timeout=10)
|
|
67
|
+
done = out.done
|
|
62
68
|
|
|
63
69
|
# Non-graceful shutdown
|
|
64
70
|
executor._threads.clear()
|
|
@@ -69,7 +75,8 @@ def run_trigger(input_file, output_file, plugin, expect_timeout=False):
|
|
|
69
75
|
return
|
|
70
76
|
raise Exception("Timeout")
|
|
71
77
|
|
|
72
|
-
output = capture.caught_message
|
|
78
|
+
# output = capture.caught_message
|
|
79
|
+
output = done if expect_timeout else capture.caught_message
|
|
73
80
|
|
|
74
81
|
if "body" in output and "log" in output["body"]:
|
|
75
82
|
output["body"]["log"] = ""
|
|
@@ -79,7 +86,7 @@ def run_trigger(input_file, output_file, plugin, expect_timeout=False):
|
|
|
79
86
|
|
|
80
87
|
if output != expected_output:
|
|
81
88
|
raise Exception(
|
|
82
|
-
"Actual output differs from expected output
|
|
83
|
-
|
|
84
|
-
|
|
89
|
+
f"Actual output differs from expected output/ \n"
|
|
90
|
+
f"Actual output: {output} \n"
|
|
91
|
+
f"Expected output: {expected_output}"
|
|
85
92
|
)
|
|
@@ -18,8 +18,14 @@ class HelloTrigger(insightconnect_plugin_runtime.Trigger):
|
|
|
18
18
|
"""Run the trigger"""
|
|
19
19
|
while True:
|
|
20
20
|
self.logger.info("I am the log")
|
|
21
|
-
|
|
21
|
+
resp = {"message": self.connection.greeting.format(params["name"])}
|
|
22
|
+
self.send(resp)
|
|
22
23
|
time.sleep(10)
|
|
24
|
+
# because this is a test we need to return otherwise the thread runs indefinitely to match how
|
|
25
|
+
# triggers run in production. Check using params 'test' so that if we build image of this plugin and
|
|
26
|
+
# test we will enter the loop as usual.
|
|
27
|
+
if params["test"]:
|
|
28
|
+
return resp
|
|
23
29
|
|
|
24
30
|
def test(self):
|
|
25
31
|
self.logger.info("This is a test")
|
tests/plugin/hello_world/hello_world/komand_hello_world/triggers/throw_exception_trigger/trigger.py
CHANGED
|
@@ -15,7 +15,7 @@ class ThrowExceptionTrigger(insightconnect_plugin_runtime.Trigger):
|
|
|
15
15
|
)
|
|
16
16
|
|
|
17
17
|
def run(self, params={}):
|
|
18
|
-
raise Exception("
|
|
18
|
+
raise Exception("Forced exception test")
|
|
19
19
|
|
|
20
20
|
def test(self, params={}):
|
|
21
|
-
raise Exception("
|
|
21
|
+
raise Exception("Forced exception test")
|
|
File without changes
|