teuthology 1.1.0__py3-none-any.whl → 1.2.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.
- scripts/describe.py +1 -0
- scripts/dispatcher.py +55 -26
- scripts/exporter.py +18 -0
- scripts/lock.py +1 -1
- scripts/node_cleanup.py +58 -0
- scripts/openstack.py +9 -9
- scripts/results.py +12 -11
- scripts/schedule.py +4 -0
- scripts/suite.py +57 -16
- scripts/supervisor.py +44 -0
- scripts/update_inventory.py +10 -4
- teuthology/__init__.py +24 -26
- teuthology/beanstalk.py +4 -3
- teuthology/config.py +16 -6
- teuthology/contextutil.py +18 -14
- teuthology/describe_tests.py +25 -18
- teuthology/dispatcher/__init__.py +210 -35
- teuthology/dispatcher/supervisor.py +140 -58
- teuthology/exceptions.py +43 -0
- teuthology/exporter.py +347 -0
- teuthology/kill.py +76 -81
- teuthology/lock/cli.py +3 -3
- teuthology/lock/ops.py +135 -61
- teuthology/lock/query.py +61 -44
- teuthology/ls.py +1 -1
- teuthology/misc.py +61 -75
- teuthology/nuke/__init__.py +12 -353
- teuthology/openstack/__init__.py +4 -3
- teuthology/openstack/openstack-centos-7.0-user-data.txt +1 -1
- teuthology/openstack/openstack-centos-7.1-user-data.txt +1 -1
- teuthology/openstack/openstack-centos-7.2-user-data.txt +1 -1
- teuthology/openstack/openstack-debian-8.0-user-data.txt +1 -1
- teuthology/openstack/openstack-opensuse-42.1-user-data.txt +1 -1
- teuthology/openstack/openstack-teuthology.cron +0 -1
- teuthology/orchestra/cluster.py +49 -7
- teuthology/orchestra/connection.py +16 -5
- teuthology/orchestra/console.py +111 -50
- teuthology/orchestra/daemon/cephadmunit.py +17 -4
- teuthology/orchestra/daemon/state.py +8 -1
- teuthology/orchestra/daemon/systemd.py +4 -4
- teuthology/orchestra/opsys.py +30 -11
- teuthology/orchestra/remote.py +405 -338
- teuthology/orchestra/run.py +3 -3
- teuthology/packaging.py +19 -16
- teuthology/provision/__init__.py +30 -10
- teuthology/provision/cloud/openstack.py +12 -6
- teuthology/provision/cloud/util.py +1 -2
- teuthology/provision/downburst.py +4 -3
- teuthology/provision/fog.py +68 -20
- teuthology/provision/openstack.py +5 -4
- teuthology/provision/pelagos.py +1 -1
- teuthology/repo_utils.py +43 -13
- teuthology/report.py +57 -35
- teuthology/results.py +5 -3
- teuthology/run.py +13 -14
- teuthology/run_tasks.py +27 -43
- teuthology/schedule.py +4 -3
- teuthology/scrape.py +28 -22
- teuthology/suite/__init__.py +74 -45
- teuthology/suite/build_matrix.py +34 -24
- teuthology/suite/fragment-merge.lua +105 -0
- teuthology/suite/matrix.py +31 -2
- teuthology/suite/merge.py +175 -0
- teuthology/suite/placeholder.py +6 -9
- teuthology/suite/run.py +175 -100
- teuthology/suite/util.py +64 -218
- teuthology/task/__init__.py +1 -1
- teuthology/task/ansible.py +101 -32
- teuthology/task/buildpackages.py +2 -2
- teuthology/task/ceph_ansible.py +13 -6
- teuthology/task/cephmetrics.py +2 -1
- teuthology/task/clock.py +33 -14
- teuthology/task/exec.py +18 -0
- teuthology/task/hadoop.py +2 -2
- teuthology/task/install/__init__.py +29 -7
- teuthology/task/install/bin/adjust-ulimits +16 -0
- teuthology/task/install/bin/daemon-helper +114 -0
- teuthology/task/install/bin/stdin-killer +263 -0
- teuthology/task/install/deb.py +1 -1
- teuthology/task/install/rpm.py +17 -5
- teuthology/task/install/util.py +3 -3
- teuthology/task/internal/__init__.py +41 -10
- teuthology/task/internal/edit_sudoers.sh +10 -0
- teuthology/task/internal/lock_machines.py +2 -9
- teuthology/task/internal/redhat.py +31 -1
- teuthology/task/internal/syslog.py +31 -8
- teuthology/task/kernel.py +152 -145
- teuthology/task/lockfile.py +1 -1
- teuthology/task/mpi.py +10 -10
- teuthology/task/pcp.py +1 -1
- teuthology/task/selinux.py +16 -8
- teuthology/task/ssh_keys.py +4 -4
- teuthology/task/tests/__init__.py +137 -77
- teuthology/task/tests/test_fetch_coredumps.py +116 -0
- teuthology/task/tests/test_run.py +4 -4
- teuthology/timer.py +3 -3
- teuthology/util/loggerfile.py +19 -0
- teuthology/util/scanner.py +159 -0
- teuthology/util/sentry.py +52 -0
- teuthology/util/time.py +52 -0
- teuthology-1.2.0.data/scripts/adjust-ulimits +16 -0
- teuthology-1.2.0.data/scripts/daemon-helper +114 -0
- teuthology-1.2.0.data/scripts/stdin-killer +263 -0
- teuthology-1.2.0.dist-info/METADATA +89 -0
- teuthology-1.2.0.dist-info/RECORD +174 -0
- {teuthology-1.1.0.dist-info → teuthology-1.2.0.dist-info}/WHEEL +1 -1
- {teuthology-1.1.0.dist-info → teuthology-1.2.0.dist-info}/entry_points.txt +3 -2
- scripts/nuke.py +0 -47
- scripts/worker.py +0 -37
- teuthology/nuke/actions.py +0 -456
- teuthology/openstack/test/__init__.py +0 -0
- teuthology/openstack/test/openstack-integration.py +0 -286
- teuthology/openstack/test/test_config.py +0 -35
- teuthology/openstack/test/test_openstack.py +0 -1695
- teuthology/orchestra/test/__init__.py +0 -0
- teuthology/orchestra/test/integration/__init__.py +0 -0
- teuthology/orchestra/test/integration/test_integration.py +0 -94
- teuthology/orchestra/test/test_cluster.py +0 -240
- teuthology/orchestra/test/test_connection.py +0 -106
- teuthology/orchestra/test/test_console.py +0 -217
- teuthology/orchestra/test/test_opsys.py +0 -404
- teuthology/orchestra/test/test_remote.py +0 -185
- teuthology/orchestra/test/test_run.py +0 -286
- teuthology/orchestra/test/test_systemd.py +0 -54
- teuthology/orchestra/test/util.py +0 -12
- teuthology/test/__init__.py +0 -0
- teuthology/test/fake_archive.py +0 -107
- teuthology/test/fake_fs.py +0 -92
- teuthology/test/integration/__init__.py +0 -0
- teuthology/test/integration/test_suite.py +0 -86
- teuthology/test/task/__init__.py +0 -205
- teuthology/test/task/test_ansible.py +0 -624
- teuthology/test/task/test_ceph_ansible.py +0 -176
- teuthology/test/task/test_console_log.py +0 -88
- teuthology/test/task/test_install.py +0 -337
- teuthology/test/task/test_internal.py +0 -57
- teuthology/test/task/test_kernel.py +0 -243
- teuthology/test/task/test_pcp.py +0 -379
- teuthology/test/task/test_selinux.py +0 -35
- teuthology/test/test_config.py +0 -189
- teuthology/test/test_contextutil.py +0 -68
- teuthology/test/test_describe_tests.py +0 -316
- teuthology/test/test_email_sleep_before_teardown.py +0 -81
- teuthology/test/test_exit.py +0 -97
- teuthology/test/test_get_distro.py +0 -47
- teuthology/test/test_get_distro_version.py +0 -47
- teuthology/test/test_get_multi_machine_types.py +0 -27
- teuthology/test/test_job_status.py +0 -60
- teuthology/test/test_ls.py +0 -48
- teuthology/test/test_misc.py +0 -391
- teuthology/test/test_nuke.py +0 -290
- teuthology/test/test_packaging.py +0 -763
- teuthology/test/test_parallel.py +0 -28
- teuthology/test/test_repo_utils.py +0 -225
- teuthology/test/test_report.py +0 -77
- teuthology/test/test_results.py +0 -155
- teuthology/test/test_run.py +0 -239
- teuthology/test/test_safepath.py +0 -55
- teuthology/test/test_schedule.py +0 -45
- teuthology/test/test_scrape.py +0 -167
- teuthology/test/test_timer.py +0 -80
- teuthology/test/test_vps_os_vers_parameter_checking.py +0 -84
- teuthology/test/test_worker.py +0 -303
- teuthology/worker.py +0 -354
- teuthology-1.1.0.dist-info/METADATA +0 -76
- teuthology-1.1.0.dist-info/RECORD +0 -213
- {teuthology-1.1.0.dist-info → teuthology-1.2.0.dist-info}/LICENSE +0 -0
- {teuthology-1.1.0.dist-info → teuthology-1.2.0.dist-info}/top_level.txt +0 -0
@@ -1,31 +1,28 @@
|
|
1
1
|
"""
|
2
|
-
This task
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
2
|
+
This task runs teuthology's unit tests and integration tests.
|
3
|
+
It can run in one of two modes: "py" or "cli". The latter executes py.test in a
|
4
|
+
separate process, whereas the former invokes it in the teuthology job's python
|
5
|
+
process.
|
6
|
+
If the running job has remotes available to it, it will attempt to run integration tests.
|
7
|
+
Note that this requires running in "py" mode - the default.
|
7
8
|
|
8
9
|
An example::
|
9
10
|
|
10
11
|
tasks
|
11
12
|
- tests:
|
12
|
-
|
13
13
|
"""
|
14
14
|
import logging
|
15
|
+
import os
|
16
|
+
import pathlib
|
17
|
+
import pexpect
|
15
18
|
import pytest
|
16
19
|
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
@pytest.fixture
|
22
|
-
def ctx():
|
23
|
-
return {}
|
20
|
+
from teuthology.job_status import set_status
|
21
|
+
from teuthology.task import Task
|
22
|
+
from teuthology.util.loggerfile import LoggerFile
|
24
23
|
|
25
24
|
|
26
|
-
|
27
|
-
def config():
|
28
|
-
return []
|
25
|
+
log = logging.getLogger(__name__)
|
29
26
|
|
30
27
|
|
31
28
|
class TeuthologyContextPlugin(object):
|
@@ -33,78 +30,141 @@ class TeuthologyContextPlugin(object):
|
|
33
30
|
self.ctx = ctx
|
34
31
|
self.config = config
|
35
32
|
self.failures = list()
|
33
|
+
self.stats = dict()
|
36
34
|
|
37
35
|
# this is pytest hook for generating tests with custom parameters
|
38
36
|
def pytest_generate_tests(self, metafunc):
|
39
37
|
# pass the teuthology ctx and config to each test method
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
def pytest_configure(self, config):
|
44
|
-
# removes the default pytest TerminalReporter
|
45
|
-
# this fixes failures with scheduled jobs; when run by a worker
|
46
|
-
# there is no terminal to report to and pytest dies
|
47
|
-
standard_reporter = config.pluginmanager.getplugin('terminalreporter')
|
48
|
-
config.pluginmanager.unregister(standard_reporter)
|
49
|
-
log.info("removing pytest terminal reporter")
|
38
|
+
if "ctx" in metafunc.fixturenames and \
|
39
|
+
"config" in metafunc.fixturenames:
|
40
|
+
metafunc.parametrize(["ctx", "config"], [(self.ctx, self.config),])
|
50
41
|
|
51
42
|
# log the outcome of each test
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
43
|
+
@pytest.hookimpl(hookwrapper=True)
|
44
|
+
def pytest_runtest_makereport(self, item: pytest.Item, call: pytest.CallInfo):
|
45
|
+
outcome = yield
|
46
|
+
report = outcome.get_result()
|
47
|
+
test_path = item.location[0]
|
48
|
+
line_no = item.location[1]
|
49
|
+
test_name = item.location[2]
|
50
|
+
name = f"{test_path}:{line_no}:{test_name}"
|
51
|
+
log_msg = f"{report.outcome.upper()} {name}"
|
52
|
+
outcome_str = report.outcome.lower()
|
53
|
+
self.stats.setdefault(outcome_str, 0)
|
54
|
+
self.stats[outcome_str] += 1
|
55
|
+
if outcome_str in ['passed', 'skipped']:
|
56
|
+
if call.when == 'call':
|
57
|
+
log.info(log_msg)
|
58
|
+
else:
|
59
|
+
log.info(f"----- {name} {call.when} -----")
|
60
|
+
else:
|
61
|
+
log_msg = f"{log_msg}:{call.when}"
|
62
|
+
if call.excinfo:
|
63
|
+
self.failures.append(name)
|
64
|
+
log_msg = f"{log_msg}: {call.excinfo.getrepr()}"
|
71
65
|
else:
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
66
|
+
self.failures.append(log_msg)
|
67
|
+
log.error(log_msg)
|
68
|
+
|
69
|
+
return
|
70
|
+
|
71
|
+
|
72
|
+
# https://docs.pytest.org/en/stable/reference/exit-codes.html
|
73
|
+
exit_codes = {
|
74
|
+
0: "All tests were collected and passed successfully",
|
75
|
+
1: "Tests were collected and run but some of the tests failed",
|
76
|
+
2: "Test execution was interrupted by the user",
|
77
|
+
3: "Internal error happened while executing tests",
|
78
|
+
4: "pytest command line usage error",
|
79
|
+
5: "No tests were collected",
|
80
|
+
}
|
81
|
+
|
82
|
+
|
83
|
+
class Tests(Task):
|
88
84
|
"""
|
89
85
|
Use pytest to recurse through this directory, finding any tests
|
90
86
|
and then executing them with the teuthology ctx and config args.
|
91
87
|
Your tests must follow standard pytest conventions to be discovered.
|
88
|
+
|
89
|
+
If config["mode"] == "py", (the default), it will be run in the job's process.
|
90
|
+
If config["mode"] == "cli" py.test will be invoked as a subprocess.
|
92
91
|
"""
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
92
|
+
base_args = ['-v', '--color=no']
|
93
|
+
|
94
|
+
def setup(self):
|
95
|
+
super().setup()
|
96
|
+
mode = self.config.get("mode", "py")
|
97
|
+
assert mode in ["py", "cli"], "mode must either be 'py' or 'cli'"
|
98
|
+
if mode == "cli":
|
99
|
+
# integration tests need ctx from this process, so we need to invoke
|
100
|
+
# pytest via python to be able to pass them
|
101
|
+
assert len(self.cluster.remotes) == 0, \
|
102
|
+
"Tests requiring remote nodes conflicts with CLI mode"
|
103
|
+
self.mode = mode
|
104
|
+
self.stats = dict()
|
105
|
+
self.orig_curdir = os.curdir
|
106
|
+
|
107
|
+
def begin(self):
|
108
|
+
super().begin()
|
109
|
+
try:
|
110
|
+
if self.mode == "py":
|
111
|
+
self.status, self.failures = self.run_py()
|
112
|
+
else:
|
113
|
+
self.status, self.failures = self.run_cli()
|
114
|
+
except Exception as e:
|
115
|
+
log.exception("Saw non-test failure!")
|
116
|
+
self.ctx.summary['failure_reason'] = str(e)
|
117
|
+
set_status(self.ctx.summary, "dead")
|
118
|
+
|
119
|
+
def end(self):
|
120
|
+
if os.curdir != self.orig_curdir:
|
121
|
+
os.chdir(self.orig_curdir)
|
122
|
+
if self.stats:
|
123
|
+
log.info(f"Stats: {self.stats}")
|
124
|
+
if self.status == 0:
|
106
125
|
log.info("OK. All tests passed!")
|
107
|
-
ctx.summary
|
126
|
+
set_status(self.ctx.summary, "pass")
|
108
127
|
else:
|
109
|
-
|
110
|
-
|
128
|
+
status_msg = str(self.status)
|
129
|
+
if self.status in exit_codes:
|
130
|
+
status_msg = f"{status_msg}: {exit_codes[self.status]}"
|
131
|
+
log.error(f"FAIL (exit code {status_msg})")
|
132
|
+
if self.failures:
|
133
|
+
msg = f"{len(self.failures)} Failures: {self.failures}"
|
134
|
+
self.ctx.summary['failure_reason'] = msg
|
135
|
+
log.error(msg)
|
136
|
+
set_status(self.ctx.summary, "fail")
|
137
|
+
super().end()
|
138
|
+
|
139
|
+
def run_cli(self):
|
140
|
+
pytest_args = self.base_args + ['./teuthology/test', './scripts']
|
141
|
+
if len(self.cluster.remotes):
|
142
|
+
pytest_args.append('./teuthology/task/tests')
|
143
|
+
self.log.info(f"pytest args: {pytest_args}")
|
144
|
+
cwd = str(pathlib.Path(__file__).parents[3])
|
145
|
+
log.info(f"pytest cwd: {cwd}")
|
146
|
+
_, status = pexpect.run(
|
147
|
+
"py.test " + " ".join(pytest_args),
|
148
|
+
cwd=cwd,
|
149
|
+
withexitstatus=True,
|
150
|
+
timeout=None,
|
151
|
+
logfile=LoggerFile(self.log, logging.INFO),
|
152
|
+
)
|
153
|
+
return status, []
|
154
|
+
|
155
|
+
def run_py(self):
|
156
|
+
pytest_args = self.base_args + ['--pyargs', 'teuthology', 'scripts']
|
157
|
+
if len(self.cluster.remotes):
|
158
|
+
pytest_args.append(__name__)
|
159
|
+
self.log.info(f"pytest args: {pytest_args}")
|
160
|
+
context_plugin = TeuthologyContextPlugin(self.ctx, self.config)
|
161
|
+
# the cwd needs to change so that FakeArchive can find files in this repo
|
162
|
+
os.chdir(str(pathlib.Path(__file__).parents[3]))
|
163
|
+
status = pytest.main(
|
164
|
+
args=pytest_args,
|
165
|
+
plugins=[context_plugin],
|
166
|
+
)
|
167
|
+
self.stats = context_plugin.stats
|
168
|
+
return status, context_plugin.failures
|
169
|
+
|
170
|
+
task = Tests
|
@@ -0,0 +1,116 @@
|
|
1
|
+
from teuthology.task.internal import fetch_binaries_for_coredumps
|
2
|
+
from unittest.mock import patch, Mock
|
3
|
+
import gzip
|
4
|
+
import os
|
5
|
+
|
6
|
+
class TestFetchCoreDumps(object):
|
7
|
+
class MockDecode(object):
|
8
|
+
def __init__(self, ret):
|
9
|
+
self.ret = ret
|
10
|
+
pass
|
11
|
+
|
12
|
+
def decode(self):
|
13
|
+
return self.ret
|
14
|
+
|
15
|
+
class MockPopen(object):
|
16
|
+
def __init__(self, ret):
|
17
|
+
self.ret = ret
|
18
|
+
|
19
|
+
def communicate(self, input=None):
|
20
|
+
return [TestFetchCoreDumps.MockDecode(self.ret)]
|
21
|
+
|
22
|
+
def setup_method(self):
|
23
|
+
self.the_function = fetch_binaries_for_coredumps
|
24
|
+
with gzip.open('file.gz', 'wb') as f:
|
25
|
+
f.write(b'Hello world!')
|
26
|
+
self.core_dump_path = "file.gz"
|
27
|
+
self.m_remote = Mock()
|
28
|
+
self.uncompressed_correct = self.MockPopen(
|
29
|
+
"ELF 64-bit LSB core file,"\
|
30
|
+
" x86-64, version 1 (SYSV), SVR4-style, from 'ceph_test_rados_api_io',"\
|
31
|
+
" real uid: 1194, effective uid: 1194, real gid: 1194,"\
|
32
|
+
" effective gid: 1194, execfn: '/usr/bin/ceph_test_rados_api_io', platform: 'x86_64'"
|
33
|
+
)
|
34
|
+
self.uncompressed_incorrect = self.MockPopen("ASCII text")
|
35
|
+
self.compressed_correct = self.MockPopen(
|
36
|
+
"gzip compressed data, was "\
|
37
|
+
"'correct.format.core', last modified: Wed Jun 29"\
|
38
|
+
" 19:55:29 2022, from Unix, original size modulo 2^32 3167080"
|
39
|
+
)
|
40
|
+
|
41
|
+
self.compressed_incorrect = self.MockPopen(
|
42
|
+
"gzip compressed data, was "\
|
43
|
+
"'incorrect.format.core', last modified: Wed Jun 29"\
|
44
|
+
" 19:56:56 2022, from Unix, original size modulo 2^32 11"
|
45
|
+
)
|
46
|
+
|
47
|
+
# Core is not compressed and file is in the correct format
|
48
|
+
@patch('teuthology.task.internal.subprocess.Popen')
|
49
|
+
@patch('teuthology.task.internal.os')
|
50
|
+
def test_uncompressed_correct_format(self, m_os, m_subproc_popen):
|
51
|
+
m_subproc_popen.side_effect = [
|
52
|
+
self.uncompressed_correct,
|
53
|
+
Exception("We shouldn't be hitting this!")
|
54
|
+
]
|
55
|
+
m_os.path.join.return_value = self.core_dump_path
|
56
|
+
m_os.path.sep = self.core_dump_path
|
57
|
+
m_os.path.isdir.return_value = True
|
58
|
+
m_os.path.dirname.return_value = self.core_dump_path
|
59
|
+
m_os.path.exists.return_value = True
|
60
|
+
m_os.listdir.return_value = [self.core_dump_path]
|
61
|
+
self.the_function(None, self.m_remote)
|
62
|
+
assert self.m_remote.get_file.called
|
63
|
+
|
64
|
+
# Core is not compressed and file is in the wrong format
|
65
|
+
@patch('teuthology.task.internal.subprocess.Popen')
|
66
|
+
@patch('teuthology.task.internal.os')
|
67
|
+
def test_uncompressed_incorrect_format(self, m_os, m_subproc_popen):
|
68
|
+
m_subproc_popen.side_effect = [
|
69
|
+
self.uncompressed_incorrect,
|
70
|
+
Exception("We shouldn't be hitting this!")
|
71
|
+
]
|
72
|
+
m_os.path.join.return_value = self.core_dump_path
|
73
|
+
m_os.path.sep = self.core_dump_path
|
74
|
+
m_os.path.isdir.return_value = True
|
75
|
+
m_os.path.dirname.return_value = self.core_dump_path
|
76
|
+
m_os.path.exists.return_value = True
|
77
|
+
m_os.listdir.return_value = [self.core_dump_path]
|
78
|
+
self.the_function(None, self.m_remote)
|
79
|
+
assert self.m_remote.get_file.called == False
|
80
|
+
|
81
|
+
# Core is compressed and file is in the correct format
|
82
|
+
@patch('teuthology.task.internal.subprocess.Popen')
|
83
|
+
@patch('teuthology.task.internal.os')
|
84
|
+
def test_compressed_correct_format(self, m_os, m_subproc_popen):
|
85
|
+
m_subproc_popen.side_effect = [
|
86
|
+
self.compressed_correct,
|
87
|
+
self.uncompressed_correct
|
88
|
+
]
|
89
|
+
m_os.path.join.return_value = self.core_dump_path
|
90
|
+
m_os.path.sep = self.core_dump_path
|
91
|
+
m_os.path.isdir.return_value = True
|
92
|
+
m_os.path.dirname.return_value = self.core_dump_path
|
93
|
+
m_os.path.exists.return_value = True
|
94
|
+
m_os.listdir.return_value = [self.core_dump_path]
|
95
|
+
self.the_function(None, self.m_remote)
|
96
|
+
assert self.m_remote.get_file.called
|
97
|
+
|
98
|
+
# Core is compressed and file is in the wrong format
|
99
|
+
@patch('teuthology.task.internal.subprocess.Popen')
|
100
|
+
@patch('teuthology.task.internal.os')
|
101
|
+
def test_compressed_incorrect_format(self, m_os, m_subproc_popen):
|
102
|
+
m_subproc_popen.side_effect = [
|
103
|
+
self.compressed_incorrect,
|
104
|
+
self.uncompressed_incorrect
|
105
|
+
]
|
106
|
+
m_os.path.join.return_value = self.core_dump_path
|
107
|
+
m_os.path.sep = self.core_dump_path
|
108
|
+
m_os.path.isdir.return_value = True
|
109
|
+
m_os.path.dirname.return_value = self.core_dump_path
|
110
|
+
m_os.path.exists.return_value = True
|
111
|
+
m_os.listdir.return_value = [self.core_dump_path]
|
112
|
+
self.the_function(None, self.m_remote)
|
113
|
+
assert self.m_remote.get_file.called == False
|
114
|
+
|
115
|
+
def teardown(self):
|
116
|
+
os.remove(self.core_dump_path)
|
@@ -1,7 +1,7 @@
|
|
1
1
|
import logging
|
2
2
|
import pytest
|
3
3
|
|
4
|
-
from
|
4
|
+
from io import StringIO
|
5
5
|
|
6
6
|
from teuthology.exceptions import CommandFailedError
|
7
7
|
|
@@ -17,7 +17,7 @@ class TestRun(object):
|
|
17
17
|
result = ""
|
18
18
|
try:
|
19
19
|
ctx.cluster.run(
|
20
|
-
args=["
|
20
|
+
args=["python3", "-c", "assert False"],
|
21
21
|
label="working as expected, nothing to see here"
|
22
22
|
)
|
23
23
|
except CommandFailedError as e:
|
@@ -28,13 +28,13 @@ class TestRun(object):
|
|
28
28
|
def test_command_failed_no_label(self, ctx, config):
|
29
29
|
with pytest.raises(CommandFailedError):
|
30
30
|
ctx.cluster.run(
|
31
|
-
args=["
|
31
|
+
args=["python3", "-c", "assert False"],
|
32
32
|
)
|
33
33
|
|
34
34
|
def test_command_success(self, ctx, config):
|
35
35
|
result = StringIO()
|
36
36
|
ctx.cluster.run(
|
37
|
-
args=["
|
37
|
+
args=["python3", "-c", "print('hi')"],
|
38
38
|
stdout=result
|
39
39
|
)
|
40
40
|
assert result.getvalue().strip() == "hi"
|
teuthology/timer.py
CHANGED
@@ -2,7 +2,7 @@ import logging
|
|
2
2
|
import time
|
3
3
|
import yaml
|
4
4
|
|
5
|
-
|
5
|
+
import datetime
|
6
6
|
|
7
7
|
log = logging.getLogger(__name__)
|
8
8
|
|
@@ -68,8 +68,8 @@ class Timer(object):
|
|
68
68
|
|
69
69
|
:param time: Time in seconds; like from time.time()
|
70
70
|
"""
|
71
|
-
_datetime = datetime.
|
72
|
-
return datetime.strftime(
|
71
|
+
_datetime = datetime.datetime.fromtimestamp(time, datetime.timezone.utc)
|
72
|
+
return datetime.datetime.strftime(
|
73
73
|
_datetime,
|
74
74
|
self.datetime_format,
|
75
75
|
)
|
@@ -0,0 +1,19 @@
|
|
1
|
+
import logging
|
2
|
+
|
3
|
+
class LoggerFile(object):
|
4
|
+
"""
|
5
|
+
A thin wrapper around a logging.Logger instance that provides a file-like
|
6
|
+
interface.
|
7
|
+
|
8
|
+
Used by Ansible.execute_playbook() when it calls pexpect.run()
|
9
|
+
"""
|
10
|
+
def __init__(self, logger: logging.Logger, level: int):
|
11
|
+
self.logger = logger
|
12
|
+
self.level = level
|
13
|
+
|
14
|
+
def write(self, string):
|
15
|
+
self.logger.log(self.level, string.decode('utf-8', 'ignore'))
|
16
|
+
|
17
|
+
def flush(self):
|
18
|
+
pass
|
19
|
+
|
@@ -0,0 +1,159 @@
|
|
1
|
+
import logging
|
2
|
+
import yaml
|
3
|
+
from typing import Optional, Tuple
|
4
|
+
from collections import defaultdict
|
5
|
+
from lxml import etree
|
6
|
+
|
7
|
+
log = logging.getLogger(__name__)
|
8
|
+
|
9
|
+
|
10
|
+
class Scanner():
|
11
|
+
def __init__(self, remote=None) -> None:
|
12
|
+
self.summary_data = []
|
13
|
+
self.remote = remote
|
14
|
+
|
15
|
+
def _parse(self, file_content) -> Tuple[str, dict]:
|
16
|
+
"""
|
17
|
+
This parses file_content and returns:
|
18
|
+
:returns: a message string
|
19
|
+
:returns: data dictionary with additional info
|
20
|
+
|
21
|
+
Just an abstract method in Scanner class,
|
22
|
+
to be defined in inherited classes.
|
23
|
+
"""
|
24
|
+
raise NotImplementedError
|
25
|
+
|
26
|
+
def scan_file(self, path: str) -> Optional[str]:
|
27
|
+
if not path:
|
28
|
+
return None
|
29
|
+
try:
|
30
|
+
file = self.remote._sftp_open_file(path, 'r')
|
31
|
+
file_content = file.read()
|
32
|
+
txt, data = self._parse(file_content)
|
33
|
+
if data:
|
34
|
+
data["file_path"] = path
|
35
|
+
self.summary_data += [data]
|
36
|
+
file.close()
|
37
|
+
return txt
|
38
|
+
except Exception as exc:
|
39
|
+
log.error(str(exc))
|
40
|
+
|
41
|
+
def scan_all_files(self, path_regex: str) -> [str]:
|
42
|
+
"""
|
43
|
+
Scans all files matching path_regex
|
44
|
+
and collect additional data in self.summary_data
|
45
|
+
|
46
|
+
:param path_regex: Regex string to find all the files which have to be scanned.
|
47
|
+
Example: /path/to/dir/*.xml
|
48
|
+
"""
|
49
|
+
(_, stdout, _) = self.remote.ssh.exec_command(f'ls -d {path_regex}', timeout=200)
|
50
|
+
|
51
|
+
files = stdout.read().decode().split('\n')
|
52
|
+
|
53
|
+
extracted_txts = []
|
54
|
+
for fpath in files:
|
55
|
+
txt = self.scan_file(fpath)
|
56
|
+
if txt:
|
57
|
+
extracted_txts += [txt]
|
58
|
+
return extracted_txts
|
59
|
+
|
60
|
+
def write_summary(self, yaml_path: str) -> None:
|
61
|
+
"""
|
62
|
+
Create yaml file locally
|
63
|
+
with self.summary_data.
|
64
|
+
"""
|
65
|
+
if self.summary_data and yaml_path:
|
66
|
+
with open(yaml_path, 'a') as f:
|
67
|
+
yaml.safe_dump(self.summary_data, f, default_flow_style=False)
|
68
|
+
else:
|
69
|
+
log.info("summary_data or yaml_file is empty!")
|
70
|
+
|
71
|
+
|
72
|
+
class UnitTestScanner(Scanner):
|
73
|
+
def __init__(self, remote=None) -> None:
|
74
|
+
super().__init__(remote)
|
75
|
+
|
76
|
+
def _parse(self, file_content: str) -> Tuple[Optional[str], Optional[dict]]:
|
77
|
+
xml_tree = etree.fromstring(file_content)
|
78
|
+
|
79
|
+
failed_testcases = xml_tree.xpath('.//failure/.. | .//error/..')
|
80
|
+
if len(failed_testcases) == 0:
|
81
|
+
return None, None
|
82
|
+
|
83
|
+
exception_txt = ""
|
84
|
+
error_data = defaultdict(list)
|
85
|
+
for testcase in failed_testcases:
|
86
|
+
testcase_name = testcase.get("name", "test-name")
|
87
|
+
testcase_suitename = testcase.get("classname", "suite-name")
|
88
|
+
for child in testcase:
|
89
|
+
if child.tag in ['failure', 'error']:
|
90
|
+
fault_kind = child.tag
|
91
|
+
reason = child.get('message', 'No message found in xml output, check logs.')
|
92
|
+
short_reason = (reason[:200].strip() + '...') if len(reason) > 200 else reason.strip()
|
93
|
+
error_data[testcase_suitename] += [{
|
94
|
+
"kind": fault_kind,
|
95
|
+
"testcase": testcase_name,
|
96
|
+
"message": reason,
|
97
|
+
}]
|
98
|
+
if not exception_txt:
|
99
|
+
exception_txt = f'{fault_kind.upper()}: Test `{testcase_name}` of `{testcase_suitename}`. Reason: {short_reason}.'
|
100
|
+
|
101
|
+
return exception_txt, { "failed_testsuites": dict(error_data), "num_of_failures": len(failed_testcases) }
|
102
|
+
|
103
|
+
@property
|
104
|
+
def num_of_total_failures(self):
|
105
|
+
total_failed_testcases = 0
|
106
|
+
if self.summary_data:
|
107
|
+
for file_data in self.summary_data:
|
108
|
+
failed_tests = file_data.get("num_of_failures", 0)
|
109
|
+
total_failed_testcases += failed_tests
|
110
|
+
return total_failed_testcases
|
111
|
+
|
112
|
+
def scan_and_write(self, path_regex: str, summary_path: str) -> Optional[str]:
|
113
|
+
"""
|
114
|
+
Scan all files matching 'path_regex'
|
115
|
+
and write summary in 'summary_path'.
|
116
|
+
"""
|
117
|
+
try:
|
118
|
+
errors = self.scan_all_files(path_regex)
|
119
|
+
self.write_summary(summary_path)
|
120
|
+
if errors:
|
121
|
+
count = self.num_of_total_failures
|
122
|
+
return f"(total {count} failed) " + errors[0]
|
123
|
+
except Exception as scanner_exc:
|
124
|
+
log.error(str(scanner_exc))
|
125
|
+
|
126
|
+
|
127
|
+
class ValgrindScanner(Scanner):
|
128
|
+
def __init__(self, remote=None) -> None:
|
129
|
+
super().__init__(remote)
|
130
|
+
|
131
|
+
def _parse(self, file_content: str) -> Tuple[Optional[str], Optional[dict]]:
|
132
|
+
xml_tree = etree.fromstring(file_content)
|
133
|
+
if xml_tree is None:
|
134
|
+
return None, None
|
135
|
+
|
136
|
+
error_tree = xml_tree.find('error')
|
137
|
+
if error_tree is None:
|
138
|
+
return None, None
|
139
|
+
|
140
|
+
error_data = {
|
141
|
+
"kind": error_tree.findtext("kind"),
|
142
|
+
"traceback": [],
|
143
|
+
}
|
144
|
+
for frame in error_tree.xpath("stack/frame"):
|
145
|
+
if len(error_data["traceback"]) >= 5:
|
146
|
+
break
|
147
|
+
curr_frame = {
|
148
|
+
"file": f"{frame.findtext('dir', '')}/{frame.findtext('file', '')}",
|
149
|
+
"line": frame.findtext("line", ''),
|
150
|
+
"function": frame.findtext("fn", ''),
|
151
|
+
}
|
152
|
+
error_data["traceback"].append(curr_frame)
|
153
|
+
|
154
|
+
traceback_functions = "\n".join(
|
155
|
+
frame.get("function", "N/A")
|
156
|
+
for frame in error_data["traceback"][:3]
|
157
|
+
)
|
158
|
+
exception_text = f"valgrind error: {error_data['kind']}\n{traceback_functions}"
|
159
|
+
return exception_text, error_data
|
@@ -0,0 +1,52 @@
|
|
1
|
+
import logging
|
2
|
+
import sentry_sdk
|
3
|
+
|
4
|
+
from copy import deepcopy
|
5
|
+
|
6
|
+
from teuthology.config import config as teuth_config
|
7
|
+
from teuthology.misc import get_http_log_path
|
8
|
+
|
9
|
+
log = logging.getLogger(__name__)
|
10
|
+
|
11
|
+
|
12
|
+
def report_error(job_config, exception, task_name=None):
|
13
|
+
if not teuth_config.sentry_dsn:
|
14
|
+
return None
|
15
|
+
sentry_sdk.init(teuth_config.sentry_dsn)
|
16
|
+
job_config = deepcopy(job_config)
|
17
|
+
|
18
|
+
tags = {
|
19
|
+
'task': task_name,
|
20
|
+
'owner': job_config.get("owner"),
|
21
|
+
}
|
22
|
+
optional_tags = ('teuthology_branch', 'branch', 'suite',
|
23
|
+
'machine_type', 'os_type', 'os_version')
|
24
|
+
for tag in optional_tags:
|
25
|
+
if tag in job_config:
|
26
|
+
tags[tag] = job_config[tag]
|
27
|
+
|
28
|
+
# Remove ssh keys from reported config
|
29
|
+
if 'targets' in job_config:
|
30
|
+
targets = job_config['targets']
|
31
|
+
for host in targets.keys():
|
32
|
+
targets[host] = '<redacted>'
|
33
|
+
|
34
|
+
job_id = job_config.get('job_id')
|
35
|
+
archive_path = job_config.get('archive_path')
|
36
|
+
extras = dict(config=job_config)
|
37
|
+
if job_id:
|
38
|
+
extras['logs'] = get_http_log_path(archive_path, job_id)
|
39
|
+
|
40
|
+
fingerprint = exception.fingerprint() if hasattr(exception, 'fingerprint') else None
|
41
|
+
exc_id = sentry_sdk.capture_exception(
|
42
|
+
error=exception,
|
43
|
+
tags=tags,
|
44
|
+
extras=extras,
|
45
|
+
fingerprint=fingerprint,
|
46
|
+
)
|
47
|
+
event_url = "{server}/?query={id}".format(
|
48
|
+
server=teuth_config.sentry_server.strip('/'), id=exc_id)
|
49
|
+
log.exception(" Sentry event: %s" % event_url)
|
50
|
+
return event_url
|
51
|
+
|
52
|
+
|