teuthology 1.0.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.
Files changed (172) hide show
  1. scripts/describe.py +1 -0
  2. scripts/dispatcher.py +62 -0
  3. scripts/exporter.py +18 -0
  4. scripts/lock.py +1 -1
  5. scripts/node_cleanup.py +58 -0
  6. scripts/openstack.py +9 -9
  7. scripts/results.py +12 -11
  8. scripts/run.py +4 -0
  9. scripts/schedule.py +4 -0
  10. scripts/suite.py +61 -16
  11. scripts/supervisor.py +44 -0
  12. scripts/update_inventory.py +10 -4
  13. scripts/wait.py +31 -0
  14. teuthology/__init__.py +24 -21
  15. teuthology/beanstalk.py +4 -3
  16. teuthology/config.py +17 -6
  17. teuthology/contextutil.py +18 -14
  18. teuthology/describe_tests.py +25 -18
  19. teuthology/dispatcher/__init__.py +365 -0
  20. teuthology/dispatcher/supervisor.py +374 -0
  21. teuthology/exceptions.py +54 -0
  22. teuthology/exporter.py +347 -0
  23. teuthology/kill.py +76 -75
  24. teuthology/lock/cli.py +16 -7
  25. teuthology/lock/ops.py +276 -70
  26. teuthology/lock/query.py +61 -44
  27. teuthology/ls.py +9 -18
  28. teuthology/misc.py +152 -137
  29. teuthology/nuke/__init__.py +12 -351
  30. teuthology/openstack/__init__.py +4 -3
  31. teuthology/openstack/openstack-centos-7.0-user-data.txt +1 -1
  32. teuthology/openstack/openstack-centos-7.1-user-data.txt +1 -1
  33. teuthology/openstack/openstack-centos-7.2-user-data.txt +1 -1
  34. teuthology/openstack/openstack-debian-8.0-user-data.txt +1 -1
  35. teuthology/openstack/openstack-opensuse-42.1-user-data.txt +1 -1
  36. teuthology/openstack/openstack-teuthology.cron +0 -1
  37. teuthology/orchestra/cluster.py +51 -9
  38. teuthology/orchestra/connection.py +23 -16
  39. teuthology/orchestra/console.py +111 -50
  40. teuthology/orchestra/daemon/cephadmunit.py +23 -5
  41. teuthology/orchestra/daemon/state.py +10 -3
  42. teuthology/orchestra/daemon/systemd.py +10 -8
  43. teuthology/orchestra/opsys.py +32 -11
  44. teuthology/orchestra/remote.py +369 -152
  45. teuthology/orchestra/run.py +21 -12
  46. teuthology/packaging.py +54 -15
  47. teuthology/provision/__init__.py +30 -10
  48. teuthology/provision/cloud/openstack.py +12 -6
  49. teuthology/provision/cloud/util.py +1 -2
  50. teuthology/provision/downburst.py +83 -29
  51. teuthology/provision/fog.py +68 -20
  52. teuthology/provision/openstack.py +5 -4
  53. teuthology/provision/pelagos.py +13 -5
  54. teuthology/repo_utils.py +91 -44
  55. teuthology/report.py +57 -35
  56. teuthology/results.py +5 -3
  57. teuthology/run.py +21 -15
  58. teuthology/run_tasks.py +114 -40
  59. teuthology/schedule.py +4 -3
  60. teuthology/scrape.py +28 -22
  61. teuthology/suite/__init__.py +75 -46
  62. teuthology/suite/build_matrix.py +34 -24
  63. teuthology/suite/fragment-merge.lua +105 -0
  64. teuthology/suite/matrix.py +31 -2
  65. teuthology/suite/merge.py +175 -0
  66. teuthology/suite/placeholder.py +8 -8
  67. teuthology/suite/run.py +204 -102
  68. teuthology/suite/util.py +67 -211
  69. teuthology/task/__init__.py +1 -1
  70. teuthology/task/ansible.py +101 -31
  71. teuthology/task/buildpackages.py +2 -2
  72. teuthology/task/ceph_ansible.py +13 -6
  73. teuthology/task/cephmetrics.py +2 -1
  74. teuthology/task/clock.py +33 -14
  75. teuthology/task/exec.py +18 -0
  76. teuthology/task/hadoop.py +2 -2
  77. teuthology/task/install/__init__.py +51 -22
  78. teuthology/task/install/bin/adjust-ulimits +16 -0
  79. teuthology/task/install/bin/daemon-helper +114 -0
  80. teuthology/task/install/bin/stdin-killer +263 -0
  81. teuthology/task/install/deb.py +24 -4
  82. teuthology/task/install/redhat.py +36 -32
  83. teuthology/task/install/rpm.py +41 -14
  84. teuthology/task/install/util.py +48 -22
  85. teuthology/task/internal/__init__.py +69 -11
  86. teuthology/task/internal/edit_sudoers.sh +10 -0
  87. teuthology/task/internal/lock_machines.py +3 -133
  88. teuthology/task/internal/redhat.py +48 -28
  89. teuthology/task/internal/syslog.py +31 -8
  90. teuthology/task/kernel.py +155 -147
  91. teuthology/task/lockfile.py +1 -1
  92. teuthology/task/mpi.py +10 -10
  93. teuthology/task/pcp.py +1 -1
  94. teuthology/task/selinux.py +17 -8
  95. teuthology/task/ssh_keys.py +6 -6
  96. teuthology/task/tests/__init__.py +137 -77
  97. teuthology/task/tests/test_fetch_coredumps.py +116 -0
  98. teuthology/task/tests/test_run.py +4 -4
  99. teuthology/timer.py +3 -3
  100. teuthology/util/loggerfile.py +19 -0
  101. teuthology/util/scanner.py +159 -0
  102. teuthology/util/sentry.py +52 -0
  103. teuthology/util/time.py +52 -0
  104. teuthology-1.2.0.data/scripts/adjust-ulimits +16 -0
  105. teuthology-1.2.0.data/scripts/daemon-helper +114 -0
  106. teuthology-1.2.0.data/scripts/stdin-killer +263 -0
  107. teuthology-1.2.0.dist-info/METADATA +89 -0
  108. teuthology-1.2.0.dist-info/RECORD +174 -0
  109. {teuthology-1.0.0.dist-info → teuthology-1.2.0.dist-info}/WHEEL +1 -1
  110. {teuthology-1.0.0.dist-info → teuthology-1.2.0.dist-info}/entry_points.txt +5 -2
  111. scripts/nuke.py +0 -45
  112. scripts/worker.py +0 -37
  113. teuthology/nuke/actions.py +0 -456
  114. teuthology/openstack/test/__init__.py +0 -0
  115. teuthology/openstack/test/openstack-integration.py +0 -286
  116. teuthology/openstack/test/test_config.py +0 -35
  117. teuthology/openstack/test/test_openstack.py +0 -1695
  118. teuthology/orchestra/test/__init__.py +0 -0
  119. teuthology/orchestra/test/integration/__init__.py +0 -0
  120. teuthology/orchestra/test/integration/test_integration.py +0 -94
  121. teuthology/orchestra/test/test_cluster.py +0 -240
  122. teuthology/orchestra/test/test_connection.py +0 -106
  123. teuthology/orchestra/test/test_console.py +0 -217
  124. teuthology/orchestra/test/test_opsys.py +0 -404
  125. teuthology/orchestra/test/test_remote.py +0 -185
  126. teuthology/orchestra/test/test_run.py +0 -286
  127. teuthology/orchestra/test/test_systemd.py +0 -54
  128. teuthology/orchestra/test/util.py +0 -12
  129. teuthology/sentry.py +0 -18
  130. teuthology/test/__init__.py +0 -0
  131. teuthology/test/fake_archive.py +0 -107
  132. teuthology/test/fake_fs.py +0 -92
  133. teuthology/test/integration/__init__.py +0 -0
  134. teuthology/test/integration/test_suite.py +0 -86
  135. teuthology/test/task/__init__.py +0 -205
  136. teuthology/test/task/test_ansible.py +0 -624
  137. teuthology/test/task/test_ceph_ansible.py +0 -176
  138. teuthology/test/task/test_console_log.py +0 -88
  139. teuthology/test/task/test_install.py +0 -337
  140. teuthology/test/task/test_internal.py +0 -57
  141. teuthology/test/task/test_kernel.py +0 -243
  142. teuthology/test/task/test_pcp.py +0 -379
  143. teuthology/test/task/test_selinux.py +0 -35
  144. teuthology/test/test_config.py +0 -189
  145. teuthology/test/test_contextutil.py +0 -68
  146. teuthology/test/test_describe_tests.py +0 -316
  147. teuthology/test/test_email_sleep_before_teardown.py +0 -81
  148. teuthology/test/test_exit.py +0 -97
  149. teuthology/test/test_get_distro.py +0 -47
  150. teuthology/test/test_get_distro_version.py +0 -47
  151. teuthology/test/test_get_multi_machine_types.py +0 -27
  152. teuthology/test/test_job_status.py +0 -60
  153. teuthology/test/test_ls.py +0 -48
  154. teuthology/test/test_misc.py +0 -368
  155. teuthology/test/test_nuke.py +0 -232
  156. teuthology/test/test_packaging.py +0 -763
  157. teuthology/test/test_parallel.py +0 -28
  158. teuthology/test/test_repo_utils.py +0 -204
  159. teuthology/test/test_report.py +0 -77
  160. teuthology/test/test_results.py +0 -155
  161. teuthology/test/test_run.py +0 -238
  162. teuthology/test/test_safepath.py +0 -55
  163. teuthology/test/test_schedule.py +0 -45
  164. teuthology/test/test_scrape.py +0 -167
  165. teuthology/test/test_timer.py +0 -80
  166. teuthology/test/test_vps_os_vers_parameter_checking.py +0 -84
  167. teuthology/test/test_worker.py +0 -303
  168. teuthology/worker.py +0 -339
  169. teuthology-1.0.0.dist-info/METADATA +0 -76
  170. teuthology-1.0.0.dist-info/RECORD +0 -210
  171. {teuthology-1.0.0.dist-info → teuthology-1.2.0.dist-info}/LICENSE +0 -0
  172. {teuthology-1.0.0.dist-info → teuthology-1.2.0.dist-info}/top_level.txt +0 -0
@@ -3,10 +3,10 @@
3
3
  Ssh-key key handlers and associated routines
4
4
  """
5
5
  import contextlib
6
+ import datetime
6
7
  import logging
7
8
  import paramiko
8
9
  import re
9
- from datetime import datetime
10
10
 
11
11
  from io import StringIO
12
12
  from teuthology import contextutil
@@ -21,7 +21,7 @@ def timestamp(format_='%Y-%m-%d_%H:%M:%S:%f'):
21
21
  """
22
22
  Return a UTC timestamp suitable for use in filenames
23
23
  """
24
- return datetime.utcnow().strftime(format_)
24
+ return datetime.datetime.now(datetime.timezone.utc).strftime(format_)
25
25
 
26
26
 
27
27
  def backup_file(remote, path, sudo=False):
@@ -54,7 +54,7 @@ def particular_ssh_key_test(line_to_test, ssh_key):
54
54
  """
55
55
  Check the validity of the ssh_key
56
56
  """
57
- match = re.match('[\w-]+ {key} \S+@\S+'.format(key=re.escape(ssh_key)), line_to_test)
57
+ match = re.match(r'[\w-]+ {key} \S+@\S+'.format(key=re.escape(ssh_key)), line_to_test)
58
58
 
59
59
  if match:
60
60
  return False
@@ -65,7 +65,7 @@ def ssh_keys_user_line_test(line_to_test, username ):
65
65
  """
66
66
  Check the validity of the username
67
67
  """
68
- match = re.match('[\w-]+ \S+ {username}@\S+'.format(username=username), line_to_test)
68
+ match = re.match(r'[\w-]+ \S+ {username}@\S+'.format(username=username), line_to_test)
69
69
 
70
70
  if match:
71
71
  return False
@@ -154,13 +154,13 @@ def push_keys_to_host(ctx, config, public_key, private_key):
154
154
  priv_key_data = '{priv_key}'.format(priv_key=private_key)
155
155
  misc.delete_file(remote, priv_key_file, force=True)
156
156
  # Hadoop requires that .ssh/id_rsa have permissions of '500'
157
- misc.create_file(remote, priv_key_file, priv_key_data, str(500))
157
+ remote.write_file(priv_key_file, priv_key_data, mode='0500')
158
158
 
159
159
  # then a private key
160
160
  pub_key_file = '/home/{user}/.ssh/id_rsa.pub'.format(user=username)
161
161
  pub_key_data = 'ssh-rsa {pub_key} {user_host}'.format(pub_key=public_key, user_host=str(remote))
162
162
  misc.delete_file(remote, pub_key_file, force=True)
163
- misc.create_file(remote, pub_key_file, pub_key_data)
163
+ remote.write_file(pub_key_file, pub_key_data)
164
164
 
165
165
  # add appropriate entries to the authorized_keys file for this host
166
166
  auth_keys_file = '/home/{user}/.ssh/authorized_keys'.format(
@@ -1,31 +1,28 @@
1
1
  """
2
- This task is used to integration test teuthology. Including this
3
- task in your yaml config will execute pytest which finds any tests in
4
- the current directory. Each test that is discovered will be passed the
5
- teuthology ctx and config args that each teuthology task usually gets.
6
- This allows the tests to operate against the cluster.
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
- log = logging.getLogger(__name__)
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
- @pytest.fixture
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
- metafunc.parametrize(["ctx", "config"], [(self.ctx, self.config),])
41
-
42
- @pytest.mark.trylast
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
- def pytest_runtest_makereport(self, __multicall__, item, call):
53
- report = __multicall__.execute()
54
-
55
- # after the test has been called, get it's report and log it
56
- if call.when == 'call':
57
- # item.location[0] is a slash delimeted path to the test file
58
- # being ran. We only want the portion after teuthology.task.tests
59
- test_path = item.location[0].replace("/", ".").split(".")
60
- test_path = ".".join(test_path[4:-1])
61
- # removes the string '[ctx0, config0]' after the test name
62
- test_name = item.location[2].split("[")[0]
63
- name = "{path}:{name}".format(path=test_path, name=test_name)
64
- if report.passed:
65
- log.info("{name} Passed".format(name=name))
66
- elif report.skipped:
67
- log.info("{name} {info}".format(
68
- name=name,
69
- info=call.excinfo.exconly()
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
- # TODO: figure out a way to log the traceback
73
- log.error("{name} Failed:\n {info}".format(
74
- name=name,
75
- info=call.excinfo.exconly()
76
- ))
77
- failure = "{name}: {err}".format(
78
- name=name,
79
- err=call.excinfo.exconly().replace("\n", "")
80
- )
81
- self.failures.append(failure)
82
- self.ctx.summary['failure_reason'] = self.failures
83
-
84
- return report
85
-
86
-
87
- def task(ctx, config):
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
- try:
94
- status = pytest.main(
95
- args=[
96
- '-q',
97
- '--pyargs', __name__
98
- ],
99
- plugins=[TeuthologyContextPlugin(ctx, config)]
100
- )
101
- except Exception:
102
- log.exception("Saw failure running pytest")
103
- ctx.summary["status"] = "dead"
104
- else:
105
- if status == 0:
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["status"] = "pass"
126
+ set_status(self.ctx.summary, "pass")
108
127
  else:
109
- log.error("FAIL. Saw test failures...")
110
- ctx.summary["status"] = "fail"
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 StringIO import StringIO
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=["python", "-c", "assert False"],
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=["python", "-c", "assert False"],
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=["python", "-c", "print('hi')"],
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
- from datetime import datetime
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.utcfromtimestamp(time)
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