mite 2.5.2__tar.gz → 2.5.4__tar.gz
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.
- {mite-2.5.2 → mite-2.5.4}/Dockerfile.perftest +1 -1
- {mite-2.5.2 → mite-2.5.4}/PKG-INFO +2 -2
- mite-2.5.4/cd-scripts/cdBuild.sh +17 -0
- mite-2.5.2/cd-scripts/cdRelease.py → mite-2.5.4/cd-scripts/cdTagRelease.py +6 -0
- mite-2.5.2/cd-scripts/cdTagRelease.sh → mite-2.5.4/cd-scripts/lib.sh +17 -23
- mite-2.5.4/cd-scripts/manualRelease.sh +32 -0
- mite-2.5.4/cd-scripts/runTests.sh +15 -0
- mite-2.5.4/cd.yml +49 -0
- {mite-2.5.2 → mite-2.5.4}/dev-requirements.txt +1 -0
- {mite-2.5.2 → mite-2.5.4}/mite.egg-info/PKG-INFO +2 -2
- {mite-2.5.2 → mite-2.5.4}/mite.egg-info/SOURCES.txt +10 -3
- {mite-2.5.2 → mite-2.5.4}/mite.egg-info/requires.txt +1 -1
- {mite-2.5.2 → mite-2.5.4}/mite.egg-info/top_level.txt +1 -0
- mite-2.5.4/mite_db_datapool/README.md +45 -0
- mite-2.5.4/mite_db_datapool/__init__.py +8 -0
- mite-2.5.4/mite_db_datapool/db_to_pool.py +95 -0
- mite-2.5.4/mite_db_datapool/mite_db_example.py +39 -0
- {mite-2.5.2 → mite-2.5.4}/setup.cfg +1 -1
- mite-2.5.4/test/test_db_to_pool.py +155 -0
- {mite-2.5.2 → mite-2.5.4}/test-requirements.txt +4 -2
- mite-2.5.2/cd-scripts/runTests.sh +0 -31
- mite-2.5.2/cd.yml +0 -32
- {mite-2.5.2 → mite-2.5.4}/.dockerignore +0 -0
- {mite-2.5.2 → mite-2.5.4}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {mite-2.5.2 → mite-2.5.4}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {mite-2.5.2 → mite-2.5.4}/.github/pull_request_template.md +0 -0
- {mite-2.5.2 → mite-2.5.4}/.gitignore +0 -0
- {mite-2.5.2 → mite-2.5.4}/.isort.cfg +0 -0
- {mite-2.5.2 → mite-2.5.4}/.pre-commit-config.yaml +0 -0
- {mite-2.5.2 → mite-2.5.4}/.travis.yml +0 -0
- {mite-2.5.2 → mite-2.5.4}/CODE_OF_CONDUCT.md +0 -0
- {mite-2.5.2 → mite-2.5.4}/CONTRIBUTING.md +0 -0
- {mite-2.5.2 → mite-2.5.4}/DEV.md +0 -0
- {mite-2.5.2 → mite-2.5.4}/Dockerfile +0 -0
- {mite-2.5.2 → mite-2.5.4}/LICENSE +0 -0
- {mite-2.5.2 → mite-2.5.4}/MANIFEST.in +0 -0
- {mite-2.5.2 → mite-2.5.4}/README.md +0 -0
- /mite-2.5.2/cd-scripts/cdBuild-1.sh → /mite-2.5.4/cd-scripts/cdBuild-old.sh +0 -0
- {mite-2.5.2 → mite-2.5.4}/docker_compose.yml +0 -0
- {mite-2.5.2 → mite-2.5.4}/docker_compose_monitoring.yml +0 -0
- {mite-2.5.2 → mite-2.5.4}/docs/Makefile +0 -0
- {mite-2.5.2 → mite-2.5.4}/docs/components.rst +0 -0
- {mite-2.5.2 → mite-2.5.4}/docs/conf.py +0 -0
- {mite-2.5.2 → mite-2.5.4}/docs/config.rst +0 -0
- {mite-2.5.2 → mite-2.5.4}/docs/design-deployment.rst +0 -0
- {mite-2.5.2 → mite-2.5.4}/docs/index.rst +0 -0
- {mite-2.5.2 → mite-2.5.4}/docs/journeys.rst +0 -0
- {mite-2.5.2 → mite-2.5.4}/docs/notes.txt +0 -0
- {mite-2.5.2 → mite-2.5.4}/docs/prometheus.rst +0 -0
- {mite-2.5.2 → mite-2.5.4}/docs/stats.rst +0 -0
- {mite-2.5.2 → mite-2.5.4}/docs/volume-model.rst +0 -0
- {mite-2.5.2 → mite-2.5.4}/grafana/Dockerfile +0 -0
- {mite-2.5.2 → mite-2.5.4}/grafana/config.ini +0 -0
- {mite-2.5.2 → mite-2.5.4}/grafana/provisioning/dashboards/dashboards.yml +0 -0
- {mite-2.5.2 → mite-2.5.4}/grafana/provisioning/dashboards/grafana_dashboard_template.json +0 -0
- {mite-2.5.2 → mite-2.5.4}/grafana/provisioning/dashboards/mite_docker.json +0 -0
- {mite-2.5.2 → mite-2.5.4}/grafana/provisioning/datasources/datasources.yml +0 -0
- {mite-2.5.2 → mite-2.5.4}/local/Makefile +0 -0
- {mite-2.5.2 → mite-2.5.4}/local/README.md +0 -0
- {mite-2.5.2 → mite-2.5.4}/local/demo.py +0 -0
- {mite-2.5.2 → mite-2.5.4}/local/run_mite.sh +0 -0
- {mite-2.5.2 → mite-2.5.4}/mite/__init__.py +0 -0
- {mite-2.5.2 → mite-2.5.4}/mite/__main__.py +0 -0
- {mite-2.5.2 → mite-2.5.4}/mite/cli/__init__.py +0 -0
- {mite-2.5.2 → mite-2.5.4}/mite/cli/cat.py +0 -0
- {mite-2.5.2 → mite-2.5.4}/mite/cli/collector.py +0 -0
- {mite-2.5.2 → mite-2.5.4}/mite/cli/common.py +0 -0
- {mite-2.5.2 → mite-2.5.4}/mite/cli/duplicator.py +0 -0
- {mite-2.5.2 → mite-2.5.4}/mite/cli/receiver.py +0 -0
- {mite-2.5.2 → mite-2.5.4}/mite/cli/stats.py +0 -0
- {mite-2.5.2 → mite-2.5.4}/mite/cli/test.py +0 -0
- {mite-2.5.2 → mite-2.5.4}/mite/collector.py +0 -0
- {mite-2.5.2 → mite-2.5.4}/mite/config.py +0 -0
- {mite-2.5.2 → mite-2.5.4}/mite/context.py +0 -0
- {mite-2.5.2 → mite-2.5.4}/mite/controller.py +0 -0
- {mite-2.5.2 → mite-2.5.4}/mite/datapools.py +0 -0
- {mite-2.5.2 → mite-2.5.4}/mite/example.py +0 -0
- {mite-2.5.2 → mite-2.5.4}/mite/exceptions.py +0 -0
- {mite-2.5.2 → mite-2.5.4}/mite/har_to_mite.py +0 -0
- {mite-2.5.2 → mite-2.5.4}/mite/logoutput.py +0 -0
- {mite-2.5.2 → mite-2.5.4}/mite/nanomsg.py +0 -0
- {mite-2.5.2 → mite-2.5.4}/mite/recorder.py +0 -0
- {mite-2.5.2 → mite-2.5.4}/mite/runner.py +0 -0
- {mite-2.5.2 → mite-2.5.4}/mite/s3datapools.py +0 -0
- {mite-2.5.2 → mite-2.5.4}/mite/scenario.py +0 -0
- {mite-2.5.2 → mite-2.5.4}/mite/stats.py +0 -0
- {mite-2.5.2 → mite-2.5.4}/mite/test.py +0 -0
- {mite-2.5.2 → mite-2.5.4}/mite/utils.py +0 -0
- {mite-2.5.2 → mite-2.5.4}/mite/volume_model.py +0 -0
- {mite-2.5.2 → mite-2.5.4}/mite/web/__init__.py +0 -0
- {mite-2.5.2 → mite-2.5.4}/mite/web/prometheus.py +0 -0
- {mite-2.5.2 → mite-2.5.4}/mite/zmq.py +0 -0
- {mite-2.5.2 → mite-2.5.4}/mite.egg-info/dependency_links.txt +0 -0
- {mite-2.5.2 → mite-2.5.4}/mite.egg-info/entry_points.txt +0 -0
- {mite-2.5.2 → mite-2.5.4}/mite_amqp/__init__.py +0 -0
- {mite-2.5.2 → mite-2.5.4}/mite_browser/__init__.py +0 -0
- {mite-2.5.2 → mite-2.5.4}/mite_finagle/README.md +0 -0
- {mite-2.5.2 → mite-2.5.4}/mite_finagle/__init__.py +0 -0
- {mite-2.5.2 → mite-2.5.4}/mite_finagle/mux.py +0 -0
- {mite-2.5.2 → mite-2.5.4}/mite_finagle/stats.py +0 -0
- {mite-2.5.2 → mite-2.5.4}/mite_finagle/tests/__init__.py +0 -0
- {mite-2.5.2 → mite-2.5.4}/mite_finagle/tests/conftest.py +0 -0
- {mite-2.5.2 → mite-2.5.4}/mite_finagle/tests/foo_service/Foo-remote +0 -0
- {mite-2.5.2 → mite-2.5.4}/mite_finagle/tests/foo_service/Foo.py +0 -0
- {mite-2.5.2 → mite-2.5.4}/mite_finagle/tests/foo_service/__init__.py +0 -0
- {mite-2.5.2 → mite-2.5.4}/mite_finagle/tests/foo_service/constants.py +0 -0
- {mite-2.5.2 → mite-2.5.4}/mite_finagle/tests/foo_service/ttypes.py +0 -0
- {mite-2.5.2 → mite-2.5.4}/mite_finagle/tests/foo_service.thrift +0 -0
- {mite-2.5.2 → mite-2.5.4}/mite_finagle/tests/test_integration.py +0 -0
- {mite-2.5.2 → mite-2.5.4}/mite_finagle/tests/test_mux.py +0 -0
- {mite-2.5.2 → mite-2.5.4}/mite_finagle/tests/test_thrift.py +0 -0
- {mite-2.5.2 → mite-2.5.4}/mite_finagle/thrift.py +0 -0
- {mite-2.5.2 → mite-2.5.4}/mite_http/__init__.py +0 -0
- {mite-2.5.2 → mite-2.5.4}/mite_http/stats.py +0 -0
- {mite-2.5.2 → mite-2.5.4}/mite_kafka/__init__.py +0 -0
- {mite-2.5.2 → mite-2.5.4}/mite_kafka/mite_kafka_example.py +0 -0
- {mite-2.5.2 → mite-2.5.4}/mite_kafka/stats.py +0 -0
- {mite-2.5.2 → mite-2.5.4}/mite_selenium/__init__.py +0 -0
- {mite-2.5.2 → mite-2.5.4}/mite_selenium/stats.py +0 -0
- {mite-2.5.2 → mite-2.5.4}/mite_websocket/__init__.py +0 -0
- {mite-2.5.2 → mite-2.5.4}/prometheus/Dockerfile +0 -0
- {mite-2.5.2 → mite-2.5.4}/prometheus/prometheus_config_docker.yml +0 -0
- {mite-2.5.2 → mite-2.5.4}/prometheus/prometheus_config_template.yml +0 -0
- {mite-2.5.2 → mite-2.5.4}/pyproject.toml +0 -0
- {mite-2.5.2 → mite-2.5.4}/requirements.txt +0 -0
- {mite-2.5.2 → mite-2.5.4}/setup.py +0 -0
- {mite-2.5.2 → mite-2.5.4}/test/legacy.py +0 -0
- {mite-2.5.2 → mite-2.5.4}/test/mocks/mock_collector.py +0 -0
- {mite-2.5.2 → mite-2.5.4}/test/mocks/mock_context.py +0 -0
- {mite-2.5.2 → mite-2.5.4}/test/mocks/mock_direct_receiver.py +0 -0
- {mite-2.5.2 → mite-2.5.4}/test/mocks/mock_direct_runner_transport.py +0 -0
- {mite-2.5.2 → mite-2.5.4}/test/mocks/mock_recorder_msg_http.py +0 -0
- {mite-2.5.2 → mite-2.5.4}/test/mocks/mock_selenium.py +0 -0
- {mite-2.5.2 → mite-2.5.4}/test/mocks/mock_sender.py +0 -0
- {mite-2.5.2 → mite-2.5.4}/test/perf/.gitignore +0 -0
- {mite-2.5.2 → mite-2.5.4}/test/perf/http_server.py +0 -0
- {mite-2.5.2 → mite-2.5.4}/test/perf/http_server_aiohttp.py +0 -0
- {mite-2.5.2 → mite-2.5.4}/test/perf/mite_perftest.py +0 -0
- {mite-2.5.2 → mite-2.5.4}/test/perf/perftest.py +0 -0
- {mite-2.5.2 → mite-2.5.4}/test/perf/run-perftest.sh +0 -0
- {mite-2.5.2 → mite-2.5.4}/test/perf/test-requirements.txt +0 -0
- {mite-2.5.2 → mite-2.5.4}/test/test.har +0 -0
- {mite-2.5.2 → mite-2.5.4}/test/test_collector.py +0 -0
- {mite-2.5.2 → mite-2.5.4}/test/test_config.py +0 -0
- {mite-2.5.2 → mite-2.5.4}/test/test_context.py +0 -0
- {mite-2.5.2 → mite-2.5.4}/test/test_controller.py +0 -0
- {mite-2.5.2 → mite-2.5.4}/test/test_datapools.py +0 -0
- {mite-2.5.2 → mite-2.5.4}/test/test_har_to_mite.py +0 -0
- {mite-2.5.2 → mite-2.5.4}/test/test_logging.py +0 -0
- {mite-2.5.2 → mite-2.5.4}/test/test_mite_amqp.py +0 -0
- {mite-2.5.2 → mite-2.5.4}/test/test_mite_browser.py +0 -0
- {mite-2.5.2 → mite-2.5.4}/test/test_mite_http.py +0 -0
- {mite-2.5.2 → mite-2.5.4}/test/test_mite_kafka.py +0 -0
- {mite-2.5.2 → mite-2.5.4}/test/test_mite_utils.py +0 -0
- {mite-2.5.2 → mite-2.5.4}/test/test_mite_websockets.py +0 -0
- {mite-2.5.2 → mite-2.5.4}/test/test_prometheus_mite.py +0 -0
- {mite-2.5.2 → mite-2.5.4}/test/test_recorder.py +0 -0
- {mite-2.5.2 → mite-2.5.4}/test/test_runner.py +0 -0
- {mite-2.5.2 → mite-2.5.4}/test/test_runner_tracker.py +0 -0
- {mite-2.5.2 → mite-2.5.4}/test/test_scenario.py +0 -0
- {mite-2.5.2 → mite-2.5.4}/test/test_stats.py +0 -0
- {mite-2.5.2 → mite-2.5.4}/test/test_volume_model.py +0 -0
- {mite-2.5.2 → mite-2.5.4}/test/test_webdriver.py +0 -0
- {mite-2.5.2 → mite-2.5.4}/test/test_work_tracker.py +0 -0
- {mite-2.5.2 → mite-2.5.4}/test.har +0 -0
- {mite-2.5.2 → mite-2.5.4}/tox.ini +0 -0
|
@@ -4,7 +4,7 @@ COPY ./test/perf/test-requirements.txt /test-requirements.txt
|
|
|
4
4
|
|
|
5
5
|
RUN pip install -r test-requirements.txt
|
|
6
6
|
|
|
7
|
-
RUN pip install bs4 docopt flask msgpack-python nanomsg pyzmq uvloop
|
|
7
|
+
RUN pip install bs4 docopt flask msgpack-python nanomsg pyzmq uvloop==0.21.0
|
|
8
8
|
|
|
9
9
|
COPY . /package
|
|
10
10
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mite
|
|
3
|
-
Version: 2.5.
|
|
3
|
+
Version: 2.5.4
|
|
4
4
|
Summary: A Python Performance Testing Framework
|
|
5
5
|
Home-page: https://github.com/sky-uk/mite/
|
|
6
6
|
Classifier: License :: OSI Approved :: MIT License
|
|
@@ -21,7 +21,7 @@ Requires-Dist: nanomsg
|
|
|
21
21
|
Requires-Dist: prettytable
|
|
22
22
|
Requires-Dist: pyzmq
|
|
23
23
|
Requires-Dist: selenium<4
|
|
24
|
-
Requires-Dist: uvloop
|
|
24
|
+
Requires-Dist: uvloop==0.21.0
|
|
25
25
|
Requires-Dist: websockets
|
|
26
26
|
Provides-Extra: amqp
|
|
27
27
|
Requires-Dist: aio_pika; extra == "amqp"
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
|
|
3
|
+
source "$(dirname "${BASH_SOURCE[0]}")/lib.sh"
|
|
4
|
+
|
|
5
|
+
# Because of the settings of the environment where the pipeline runs, cdBuild.sh is used for both CI testing and manual releases as we can change the default scripts used.
|
|
6
|
+
# In a normal situation, we would have two separate scripts, one for CI and one for manual releases.
|
|
7
|
+
# Here we check the JOB_NAME variable to determine which kind of run it is and call the appropriate script.
|
|
8
|
+
|
|
9
|
+
# manual-release job will run the release script directly
|
|
10
|
+
# any other job (like miteci-release) will run the test script. Then the pipeline shape will take care of calling the release script if the tests pass for the CI job.
|
|
11
|
+
|
|
12
|
+
echo $JOB_NAME
|
|
13
|
+
|
|
14
|
+
if [[ "$JOB_NAME" =~ "mite-ci" ]]; then
|
|
15
|
+
echo "##### Job running as CI. Proceeding with the Test script. ######"
|
|
16
|
+
./cd-scripts/runTests.sh
|
|
17
|
+
fi
|
|
@@ -83,6 +83,12 @@ def create_and_push_tag(repo, tag):
|
|
|
83
83
|
|
|
84
84
|
def parse_pr(pr_number):
|
|
85
85
|
resp = requests.get(f"https://api.github.com/repos/sky-uk/mite/pulls/{pr_number}")
|
|
86
|
+
if resp.status_code != 200:
|
|
87
|
+
logger.error(
|
|
88
|
+
f"Request to GitHub not successfull: {resp.status_code} - {resp.text}"
|
|
89
|
+
)
|
|
90
|
+
sys.exit(1)
|
|
91
|
+
|
|
86
92
|
pr_message = resp.json()["body"]
|
|
87
93
|
|
|
88
94
|
matches = re.findall(
|
|
@@ -1,10 +1,25 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
2
|
|
|
3
|
+
function isOnMaster() {
|
|
4
|
+
current_revision=$(git rev-parse HEAD)
|
|
5
|
+
branch=$(git branch -r --contains $current_revision)
|
|
6
|
+
set +e
|
|
7
|
+
echo "$branch" | grep -q "origin/master$"
|
|
8
|
+
result=$?
|
|
9
|
+
set -e
|
|
10
|
+
return ${result}
|
|
11
|
+
}
|
|
12
|
+
|
|
3
13
|
function tagBuild() {
|
|
14
|
+
if [ -z "$1" ]; then
|
|
15
|
+
VERSION_INCREMENT_TYPE=""
|
|
16
|
+
else
|
|
17
|
+
VERSION_INCREMENT_TYPE="--${1}"
|
|
18
|
+
fi
|
|
4
19
|
git config user.email "mite@noreply.github.com"
|
|
5
20
|
git config user.name "Jenkins-CI"
|
|
6
21
|
pip3 install docopt GitPython packaging requests
|
|
7
|
-
python3 cd-scripts/
|
|
22
|
+
python3 cd-scripts/cdTagRelease.py ${VERSION_INCREMENT_TYPE}
|
|
8
23
|
if (( $? == 1)); then
|
|
9
24
|
VERSION_INCREMENT=false
|
|
10
25
|
fi
|
|
@@ -49,25 +64,4 @@ function initPypirc() {
|
|
|
49
64
|
echo -e "[pypi]" > ~/.pypirc
|
|
50
65
|
echo -e "username = __token__" >> ~/.pypirc
|
|
51
66
|
echo -e "password = $PYPI_TOKEN" >> ~/.pypirc
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
###### MAIN SECTION ######
|
|
55
|
-
|
|
56
|
-
echo "##### Starting tag process and check for version increment #####"
|
|
57
|
-
tagBuild
|
|
58
|
-
|
|
59
|
-
echo "##### Look for Acurl changes #####"
|
|
60
|
-
checkAcurl
|
|
61
|
-
|
|
62
|
-
echo "##### Build Linux-Wheels #####"
|
|
63
|
-
buildLinuxWheels
|
|
64
|
-
|
|
65
|
-
if [ "$VERSION_INCREMENT" = false ]; then
|
|
66
|
-
echo "Mite version not incremented, nothing to upload"
|
|
67
|
-
else
|
|
68
|
-
echo "##### Init .pypirc #####"
|
|
69
|
-
initPypirc
|
|
70
|
-
|
|
71
|
-
echo "##### Upload package #####"
|
|
72
|
-
python3 -m twine upload wheelhouse/*
|
|
73
|
-
fi
|
|
67
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
|
|
3
|
+
source "$(dirname "${BASH_SOURCE[0]}")/lib.sh"
|
|
4
|
+
|
|
5
|
+
VERSION_INCREMENT_TYPE=${1}
|
|
6
|
+
|
|
7
|
+
if isOnMaster; then
|
|
8
|
+
echo "##### Job running on MASTER. Proceeding with the Tag and Release script. ######"
|
|
9
|
+
echo "##### Starting tag process and check for version increment #####"
|
|
10
|
+
# We are passing over the VERSION_TYPE argument to tagBuild which will set the type of increment for the new version
|
|
11
|
+
# If not passed, cdRelease.py will use the GH PR to check which kind of increment it is supposed to be as VERSION_TYPE will be an empty string
|
|
12
|
+
tagBuild ${VERSION_INCREMENT_TYPE}
|
|
13
|
+
|
|
14
|
+
echo "##### Look for Acurl changes #####"
|
|
15
|
+
checkAcurl
|
|
16
|
+
|
|
17
|
+
echo "##### Build Linux-Wheels #####"
|
|
18
|
+
buildLinuxWheels
|
|
19
|
+
|
|
20
|
+
if [ "$VERSION_INCREMENT" = false ]; then
|
|
21
|
+
echo "Mite version not incremented, nothing to upload"
|
|
22
|
+
else
|
|
23
|
+
echo "##### Init .pypirc #####"
|
|
24
|
+
initPypirc
|
|
25
|
+
|
|
26
|
+
echo "##### Upload package #####"
|
|
27
|
+
python3 -m twine upload wheelhouse/*
|
|
28
|
+
fi
|
|
29
|
+
|
|
30
|
+
else
|
|
31
|
+
echo "Not going to build the mite image because the job is not running on the master branch"
|
|
32
|
+
fi
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
echo "##### Run pre-commit checks #####"
|
|
5
|
+
/home/jenkins/.local/bin/pre-commit run --origin HEAD --source origin/master
|
|
6
|
+
PRE_COMMIT_STATUS=$?
|
|
7
|
+
|
|
8
|
+
if [ $PRE_COMMIT_STATUS -ne 0 ]; then
|
|
9
|
+
git diff
|
|
10
|
+
fi
|
|
11
|
+
|
|
12
|
+
echo "##### Run tests with tox #####"
|
|
13
|
+
tox; TOX_EXIT_CODE=$?
|
|
14
|
+
[ "$TOX_EXIT_CODE" -eq 0 -a "$PRE_COMMIT_STATUS" -eq 0 ] || exit 1
|
|
15
|
+
echo "##### Pre-commit and Tests passed #####"
|
mite-2.5.4/cd.yml
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
version: 1
|
|
2
|
+
|
|
3
|
+
# Further ideas for jobs to run:
|
|
4
|
+
# - license check
|
|
5
|
+
# - make sure test coverage increases
|
|
6
|
+
# - merge all tox tests into one using pyenv
|
|
7
|
+
# And, once we're sure that the pipeline is working:
|
|
8
|
+
# - integration/performance tests
|
|
9
|
+
# Once we have docs:
|
|
10
|
+
# - documentation coverage
|
|
11
|
+
# - docs build (on master only)
|
|
12
|
+
|
|
13
|
+
context: sky-identity
|
|
14
|
+
triggering: multibranch
|
|
15
|
+
|
|
16
|
+
defaultNodes:
|
|
17
|
+
default: sky-identity-nonprod-mite-slave-buildkit
|
|
18
|
+
|
|
19
|
+
modules:
|
|
20
|
+
mite-ci:
|
|
21
|
+
pipelineShape: simple
|
|
22
|
+
directory: "/"
|
|
23
|
+
htmlReports:
|
|
24
|
+
- name: Coverage Report
|
|
25
|
+
directory: coverage_html_report
|
|
26
|
+
files: ["index.html"]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
mite-manual-release:
|
|
30
|
+
pipelineShape: simple
|
|
31
|
+
defaultTaskExecution: ./cd-scripts/manualRelease.sh
|
|
32
|
+
phaseLocks:
|
|
33
|
+
cdBuild:
|
|
34
|
+
defaultLockEnabled: false
|
|
35
|
+
parameters:
|
|
36
|
+
main:
|
|
37
|
+
- name: VERSION_INCREMENT_TYPE
|
|
38
|
+
type: choice
|
|
39
|
+
description: The version segment that you want increment for the release
|
|
40
|
+
options: [patch, minor, major]
|
|
41
|
+
|
|
42
|
+
# TODO find a work around for the issue with combining build and release in one pipeline
|
|
43
|
+
# mite-ci-and-release:
|
|
44
|
+
# pipelineShape: build-release
|
|
45
|
+
# directory: "/"
|
|
46
|
+
# htmlReports:
|
|
47
|
+
# - name: Coverage Report
|
|
48
|
+
# directory: coverage_html_report
|
|
49
|
+
# files: ["index.html"]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mite
|
|
3
|
-
Version: 2.5.
|
|
3
|
+
Version: 2.5.4
|
|
4
4
|
Summary: A Python Performance Testing Framework
|
|
5
5
|
Home-page: https://github.com/sky-uk/mite/
|
|
6
6
|
Classifier: License :: OSI Approved :: MIT License
|
|
@@ -21,7 +21,7 @@ Requires-Dist: nanomsg
|
|
|
21
21
|
Requires-Dist: prettytable
|
|
22
22
|
Requires-Dist: pyzmq
|
|
23
23
|
Requires-Dist: selenium<4
|
|
24
|
-
Requires-Dist: uvloop
|
|
24
|
+
Requires-Dist: uvloop==0.21.0
|
|
25
25
|
Requires-Dist: websockets
|
|
26
26
|
Provides-Extra: amqp
|
|
27
27
|
Requires-Dist: aio_pika; extra == "amqp"
|
|
@@ -25,9 +25,11 @@ tox.ini
|
|
|
25
25
|
.github/pull_request_template.md
|
|
26
26
|
.github/ISSUE_TEMPLATE/bug_report.md
|
|
27
27
|
.github/ISSUE_TEMPLATE/feature_request.md
|
|
28
|
-
cd-scripts/cdBuild-
|
|
29
|
-
cd-scripts/
|
|
30
|
-
cd-scripts/cdTagRelease.
|
|
28
|
+
cd-scripts/cdBuild-old.sh
|
|
29
|
+
cd-scripts/cdBuild.sh
|
|
30
|
+
cd-scripts/cdTagRelease.py
|
|
31
|
+
cd-scripts/lib.sh
|
|
32
|
+
cd-scripts/manualRelease.sh
|
|
31
33
|
cd-scripts/runTests.sh
|
|
32
34
|
docs/Makefile
|
|
33
35
|
docs/components.rst
|
|
@@ -89,6 +91,10 @@ mite/web/__init__.py
|
|
|
89
91
|
mite/web/prometheus.py
|
|
90
92
|
mite_amqp/__init__.py
|
|
91
93
|
mite_browser/__init__.py
|
|
94
|
+
mite_db_datapool/README.md
|
|
95
|
+
mite_db_datapool/__init__.py
|
|
96
|
+
mite_db_datapool/db_to_pool.py
|
|
97
|
+
mite_db_datapool/mite_db_example.py
|
|
92
98
|
mite_finagle/README.md
|
|
93
99
|
mite_finagle/__init__.py
|
|
94
100
|
mite_finagle/mux.py
|
|
@@ -123,6 +129,7 @@ test/test_config.py
|
|
|
123
129
|
test/test_context.py
|
|
124
130
|
test/test_controller.py
|
|
125
131
|
test/test_datapools.py
|
|
132
|
+
test/test_db_to_pool.py
|
|
126
133
|
test/test_har_to_mite.py
|
|
127
134
|
test/test_logging.py
|
|
128
135
|
test/test_mite_amqp.py
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Database -> DataPool for Mite
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
This module provides database-backed data pools for Mite testing scenarios. It allows you to load test data directly from databases and use it in your load testing journeys.
|
|
6
|
+
|
|
7
|
+
## Why SQLAlchemy?
|
|
8
|
+
|
|
9
|
+
We chose **SQLAlchemy** over **pyodbc** for several reasons:
|
|
10
|
+
|
|
11
|
+
- **Database Agnostic**: Works with SQLite, PostgreSQL, MySQL, SQL Server, Oracle, etc.
|
|
12
|
+
- **Connection Pooling**: Built-in connection management and pooling
|
|
13
|
+
- **Simpler API**: Clean, Pythonic interface for database operations
|
|
14
|
+
- **Better Error Handling**: More informative error messages
|
|
15
|
+
- **No Driver Dependencies**: SQLAlchemy handles driver selection automatically
|
|
16
|
+
- **URL-based Configuration**: Easy connection string format
|
|
17
|
+
|
|
18
|
+
## Files
|
|
19
|
+
|
|
20
|
+
### `db_to_pool.py`
|
|
21
|
+
Core implementation containing:
|
|
22
|
+
- `DBIterableDataPool`: Load data once, consume sequentially (burned data)
|
|
23
|
+
- `DBRecyclableIterableDataPool`: Load data once, reuse infinitely
|
|
24
|
+
- Memory protection with `max_size` parameter to prevent OOM with large datasets
|
|
25
|
+
|
|
26
|
+
### `mite_db_example.py`
|
|
27
|
+
Working example showing:
|
|
28
|
+
- SQLite database creation with test data
|
|
29
|
+
- DataPool setup and configuration
|
|
30
|
+
- Integration with Mite journey functions
|
|
31
|
+
- Basic load testing scenario
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
## Testing
|
|
35
|
+
|
|
36
|
+
Run the example:
|
|
37
|
+
```bash
|
|
38
|
+
# From mite directory
|
|
39
|
+
python mite_db_datapool/mite_db_example.py
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Run unit tests:
|
|
43
|
+
```bash
|
|
44
|
+
pytest test/test_db_to_pool.py -v
|
|
45
|
+
```
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from collections import deque
|
|
3
|
+
|
|
4
|
+
from sqlalchemy import text
|
|
5
|
+
|
|
6
|
+
from mite.datapools import DataPoolExhausted, DataPoolItem
|
|
7
|
+
|
|
8
|
+
logger = logging.getLogger(__name__)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class DBIterableDataPool:
|
|
12
|
+
def __init__(
|
|
13
|
+
self,
|
|
14
|
+
db_engine,
|
|
15
|
+
query,
|
|
16
|
+
preload_minimum=None,
|
|
17
|
+
max_size=None,
|
|
18
|
+
):
|
|
19
|
+
self.db_engine = db_engine
|
|
20
|
+
self.query = query
|
|
21
|
+
self.max_size = max_size
|
|
22
|
+
self._data = []
|
|
23
|
+
self.item_index = 0
|
|
24
|
+
self.db_exhausted = False
|
|
25
|
+
self.preload_minimum = preload_minimum
|
|
26
|
+
|
|
27
|
+
# Adjust preload_minimum if needed
|
|
28
|
+
if max_size and preload_minimum and preload_minimum > max_size:
|
|
29
|
+
self.preload_minimum = max_size
|
|
30
|
+
|
|
31
|
+
# Load initial data
|
|
32
|
+
if preload_minimum:
|
|
33
|
+
while len(self._data) < self.preload_minimum and not self.db_exhausted:
|
|
34
|
+
self.populate()
|
|
35
|
+
else:
|
|
36
|
+
while not self.db_exhausted:
|
|
37
|
+
self.populate()
|
|
38
|
+
|
|
39
|
+
def populate(self):
|
|
40
|
+
# Stop if at max_size
|
|
41
|
+
if self.max_size is not None and len(self._data) >= self.max_size:
|
|
42
|
+
return
|
|
43
|
+
|
|
44
|
+
with self.db_engine.connect() as conn:
|
|
45
|
+
result = conn.execute(text(self.query))
|
|
46
|
+
rows = result.fetchall()
|
|
47
|
+
|
|
48
|
+
if not rows:
|
|
49
|
+
self.db_exhausted = True
|
|
50
|
+
return
|
|
51
|
+
|
|
52
|
+
# Add rows up to max_size
|
|
53
|
+
rows_added = 0
|
|
54
|
+
for row in rows:
|
|
55
|
+
if self.max_size is not None and len(self._data) >= self.max_size:
|
|
56
|
+
break
|
|
57
|
+
self._data.append((self.item_index, dict(row._mapping)))
|
|
58
|
+
self.item_index += 1
|
|
59
|
+
rows_added += 1
|
|
60
|
+
|
|
61
|
+
# Only mark as exhausted if we processed all available rows
|
|
62
|
+
# If we only processed some rows due to max_size limit, don't mark as exhausted
|
|
63
|
+
if rows_added == len(rows):
|
|
64
|
+
self.db_exhausted = True
|
|
65
|
+
|
|
66
|
+
async def checkout(self, config):
|
|
67
|
+
try:
|
|
68
|
+
item_id, row = self._data.pop(0)
|
|
69
|
+
return DataPoolItem(item_id, row)
|
|
70
|
+
except IndexError as e:
|
|
71
|
+
raise DataPoolExhausted() from e
|
|
72
|
+
|
|
73
|
+
async def checkin(self, item_id):
|
|
74
|
+
pass
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class DBRecyclableIterableDataPool(DBIterableDataPool):
|
|
78
|
+
def __init__(self, *args, **kwargs):
|
|
79
|
+
super().__init__(*args, **kwargs)
|
|
80
|
+
self._available = deque(range(len(self._data)))
|
|
81
|
+
self._checked_out = set()
|
|
82
|
+
|
|
83
|
+
async def checkout(self, config):
|
|
84
|
+
if self._available:
|
|
85
|
+
item_id = self._available.popleft()
|
|
86
|
+
self._checked_out.add(item_id)
|
|
87
|
+
row = self._data[item_id][1]
|
|
88
|
+
return DataPoolItem(item_id, row)
|
|
89
|
+
else:
|
|
90
|
+
raise DataPoolExhausted("Recyclable iterable datapool was emptied!")
|
|
91
|
+
|
|
92
|
+
async def checkin(self, item_id):
|
|
93
|
+
if item_id in self._checked_out:
|
|
94
|
+
self._checked_out.remove(item_id)
|
|
95
|
+
self._available.append(item_id)
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import sqlite3
|
|
2
|
+
|
|
3
|
+
from db_to_pool import DBIterableDataPool
|
|
4
|
+
from sqlalchemy import create_engine
|
|
5
|
+
|
|
6
|
+
from mite import ensure_average_separation
|
|
7
|
+
|
|
8
|
+
# Step 1: Create a test SQLite database and table (local file)
|
|
9
|
+
DB_PATH = "example_test.db"
|
|
10
|
+
conn = sqlite3.connect(DB_PATH)
|
|
11
|
+
conn.execute("CREATE TABLE IF NOT EXISTS test_table (id INTEGER PRIMARY KEY, value TEXT)")
|
|
12
|
+
conn.execute("DELETE FROM test_table")
|
|
13
|
+
conn.execute("INSERT INTO test_table (value) VALUES ('mite1'), ('mite2'), ('mite3')")
|
|
14
|
+
conn.commit()
|
|
15
|
+
conn.close()
|
|
16
|
+
|
|
17
|
+
# Step 2: Set up SQLAlchemy engine (same file)
|
|
18
|
+
engine = create_engine(f"sqlite:///{DB_PATH}")
|
|
19
|
+
|
|
20
|
+
# Step 3: Test DBIterableDataPool
|
|
21
|
+
db_datapool = DBIterableDataPool(
|
|
22
|
+
engine, "SELECT * FROM test_table", preload_minimum=3, max_size=2
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
async def db_journey(ctx, item_id, row):
|
|
27
|
+
async with ensure_average_separation(1):
|
|
28
|
+
print("running journey", item_id, row)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def scenario():
|
|
32
|
+
return [["db_folder:db_journey", db_datapool, lambda s, e: 1]]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# Simple test to see it working
|
|
36
|
+
if __name__ == "__main__":
|
|
37
|
+
print(f"✓ DataPool created with {len(db_datapool._data)} items")
|
|
38
|
+
for i, (item_id, data) in enumerate(db_datapool._data):
|
|
39
|
+
print(f" Item {i}: {data}")
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import sqlite3
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
from sqlalchemy import create_engine
|
|
5
|
+
|
|
6
|
+
from mite_db_datapool.db_to_pool import (
|
|
7
|
+
DataPoolExhausted,
|
|
8
|
+
DBIterableDataPool,
|
|
9
|
+
DBRecyclableIterableDataPool,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@pytest.fixture
|
|
14
|
+
def setup_test_db():
|
|
15
|
+
"""Set up a test SQLite database and table"""
|
|
16
|
+
conn = sqlite3.connect("testdb.sqlite")
|
|
17
|
+
conn.execute(
|
|
18
|
+
"CREATE TABLE IF NOT EXISTS test_table (id INTEGER PRIMARY KEY, value TEXT)"
|
|
19
|
+
)
|
|
20
|
+
conn.execute("DELETE FROM test_table")
|
|
21
|
+
conn.execute("INSERT INTO test_table (value) VALUES ('foo'), ('bar'), ('baz')")
|
|
22
|
+
conn.commit()
|
|
23
|
+
conn.close()
|
|
24
|
+
|
|
25
|
+
engine = create_engine("sqlite:///testdb.sqlite")
|
|
26
|
+
yield engine
|
|
27
|
+
|
|
28
|
+
# Cleanup
|
|
29
|
+
import os
|
|
30
|
+
|
|
31
|
+
if os.path.exists("testdb.sqlite"):
|
|
32
|
+
os.remove("testdb.sqlite")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@pytest.fixture
|
|
36
|
+
def setup_large_test_db():
|
|
37
|
+
"""Set up a test SQLite database with more data for max_size testing"""
|
|
38
|
+
conn = sqlite3.connect("testdb_large.sqlite")
|
|
39
|
+
conn.execute(
|
|
40
|
+
"CREATE TABLE IF NOT EXISTS large_table (id INTEGER PRIMARY KEY, value TEXT)"
|
|
41
|
+
)
|
|
42
|
+
conn.execute("DELETE FROM large_table")
|
|
43
|
+
|
|
44
|
+
# Insert 100 rows for testing max_size functionality
|
|
45
|
+
values = [(f"item_{i}",) for i in range(100)]
|
|
46
|
+
conn.executemany("INSERT INTO large_table (value) VALUES (?)", values)
|
|
47
|
+
conn.commit()
|
|
48
|
+
conn.close()
|
|
49
|
+
|
|
50
|
+
engine = create_engine("sqlite:///testdb_large.sqlite")
|
|
51
|
+
yield engine
|
|
52
|
+
|
|
53
|
+
# Cleanup
|
|
54
|
+
import os
|
|
55
|
+
|
|
56
|
+
if os.path.exists("testdb_large.sqlite"):
|
|
57
|
+
os.remove("testdb_large.sqlite")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@pytest.mark.asyncio
|
|
61
|
+
async def test_db_iterable_datapool(setup_test_db):
|
|
62
|
+
"""Test basic DBIterableDataPool functionality"""
|
|
63
|
+
engine = setup_test_db
|
|
64
|
+
pool = DBIterableDataPool(engine, "SELECT * FROM test_table", preload_minimum=3)
|
|
65
|
+
|
|
66
|
+
checked_out_items = []
|
|
67
|
+
try:
|
|
68
|
+
while True:
|
|
69
|
+
item = await pool.checkout(config={})
|
|
70
|
+
checked_out_items.append(item)
|
|
71
|
+
except DataPoolExhausted:
|
|
72
|
+
pass
|
|
73
|
+
|
|
74
|
+
assert len(checked_out_items) == 3
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@pytest.mark.asyncio
|
|
78
|
+
async def test_db_recyclable_iterable_datapool(setup_test_db):
|
|
79
|
+
"""Test basic DBRecyclableIterableDataPool functionality"""
|
|
80
|
+
engine = setup_test_db
|
|
81
|
+
recycle_pool = DBRecyclableIterableDataPool(
|
|
82
|
+
engine, "SELECT * FROM test_table", preload_minimum=3
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
# Checkout items
|
|
86
|
+
items = []
|
|
87
|
+
for _ in range(3):
|
|
88
|
+
item = await recycle_pool.checkout(config={})
|
|
89
|
+
items.append(item.id)
|
|
90
|
+
|
|
91
|
+
# Check them back in
|
|
92
|
+
for item_id in items:
|
|
93
|
+
await recycle_pool.checkin(item_id)
|
|
94
|
+
|
|
95
|
+
# Try to checkout again - should work since items are recycled
|
|
96
|
+
recycled_items = []
|
|
97
|
+
for _ in range(3):
|
|
98
|
+
item = await recycle_pool.checkout(config={})
|
|
99
|
+
recycled_items.append(item.id)
|
|
100
|
+
|
|
101
|
+
assert len(recycled_items) == 3
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def test_max_size_limits_data_loading(setup_large_test_db):
|
|
105
|
+
"""Test that max_size prevents OOM with large datasets"""
|
|
106
|
+
engine = setup_large_test_db
|
|
107
|
+
|
|
108
|
+
pool = DBIterableDataPool(
|
|
109
|
+
engine,
|
|
110
|
+
"SELECT * FROM large_table",
|
|
111
|
+
preload_minimum=3, # Wants to load 3 items
|
|
112
|
+
max_size=2, # Limit to 2 items even though 100 are available
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
assert len(pool._data) == 2
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def test_infinite_loop_prevention(setup_large_test_db):
|
|
119
|
+
"""Test that preload_minimum > max_size doesn't cause infinite loop (critical fix)"""
|
|
120
|
+
import time
|
|
121
|
+
|
|
122
|
+
start_time = time.time()
|
|
123
|
+
|
|
124
|
+
pool = DBIterableDataPool(
|
|
125
|
+
setup_large_test_db,
|
|
126
|
+
"SELECT * FROM large_table",
|
|
127
|
+
preload_minimum=50, # Want 50
|
|
128
|
+
max_size=10, # Limited to 10
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
end_time = time.time()
|
|
132
|
+
|
|
133
|
+
assert end_time - start_time < 1.0 # Should complete quickly
|
|
134
|
+
assert pool.preload_minimum == 10 # Auto-adjusted
|
|
135
|
+
assert len(pool._data) == 10
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
@pytest.mark.asyncio
|
|
139
|
+
async def test_recyclable_with_max_size(setup_large_test_db):
|
|
140
|
+
"""Test recyclable pool respects max_size"""
|
|
141
|
+
engine = setup_large_test_db
|
|
142
|
+
|
|
143
|
+
recycle_pool = DBRecyclableIterableDataPool(
|
|
144
|
+
engine, "SELECT * FROM large_table", preload_minimum=3, max_size=2
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
assert len(recycle_pool._data) == 2
|
|
148
|
+
assert len(recycle_pool._available) == 2
|
|
149
|
+
|
|
150
|
+
# Test checkout/checkin cycle
|
|
151
|
+
item = await recycle_pool.checkout(config={})
|
|
152
|
+
assert len(recycle_pool._available) == 1
|
|
153
|
+
|
|
154
|
+
await recycle_pool.checkin(item.id)
|
|
155
|
+
assert len(recycle_pool._available) == 2
|
|
@@ -14,5 +14,7 @@ pytest-httpbin
|
|
|
14
14
|
pytest-httpserver
|
|
15
15
|
sanic
|
|
16
16
|
werkzeug==3.1.3
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
sqlalchemy
|
|
18
|
+
uvloop==0.21.0
|
|
19
|
+
setuptools>=78.1.1 # not directly required, pinned by Snyk to avoid a vulnerability
|
|
20
|
+
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
#!/bin/bash
|
|
2
|
-
|
|
3
|
-
function isOnMaster() {
|
|
4
|
-
current_revision=$(git rev-parse HEAD)
|
|
5
|
-
branch=$(git branch -r --contains $current_revision)
|
|
6
|
-
set +e
|
|
7
|
-
echo "$branch" | grep -q "origin/master$"
|
|
8
|
-
result=$?
|
|
9
|
-
set -e
|
|
10
|
-
return ${result}
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
echo "##### Run pre-commit checks #####"
|
|
15
|
-
/home/jenkins/.local/bin/pre-commit run --origin HEAD --source origin/master
|
|
16
|
-
PRE_COMMIT_STATUS=$?
|
|
17
|
-
|
|
18
|
-
if [ $PRE_COMMIT_STATUS -ne 0 ]; then
|
|
19
|
-
git diff
|
|
20
|
-
fi
|
|
21
|
-
|
|
22
|
-
echo "##### Run tests with tox #####"
|
|
23
|
-
tox; TOX_EXIT_CODE=$?
|
|
24
|
-
[ "$TOX_EXIT_CODE" -eq 0 -a "$PRE_COMMIT_STATUS" -eq 0 ] || exit 1
|
|
25
|
-
|
|
26
|
-
if isOnMaster ; then
|
|
27
|
-
echo "##### Job running on MASTER. Proceeding with the Tag and Release script. ######"
|
|
28
|
-
./cd-scripts/cdTagRelease.sh
|
|
29
|
-
else
|
|
30
|
-
echo "###### Job running on a Branch. Stopping here. ######"
|
|
31
|
-
fi
|