summaryrefslogtreecommitdiffstats
path: root/util/testrunner/tests/tst_testrunner.py
blob: b2c100714d2634049fe7cf96c3812517664943b1 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
#!/usr/bin/env python3
# Copyright (C) 2021 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0


import sys
import os
import re
import subprocess

from subprocess import STDOUT, PIPE
from tempfile   import TemporaryDirectory, mkstemp

MY_NAME          = os.path.basename(__file__)
my_dir           = os.path.dirname(__file__)
testrunner       = os.path.join(my_dir, "..", "qt-testrunner.py")
mock_test        = os.path.join(my_dir, "qt_mock_test.py")
xml_log_template = os.path.join(my_dir, "qt_mock_test-log.xml")

with open(xml_log_template) as f:
    XML_TEMPLATE = f.read()


import unittest

def setUpModule():
    global TEMPDIR
    TEMPDIR = TemporaryDirectory(prefix="tst_testrunner-")

    filename = os.path.join(TEMPDIR.name, "file_1")
    print("setUpModule(): setting up temporary directory and env var"
          " QT_MOCK_TEST_STATE_FILE=" + filename + " and"
          " QT_MOCK_TEST_XML_TEMPLATE_FILE=" + xml_log_template)

    os.environ["QT_MOCK_TEST_STATE_FILE"]        = filename
    os.environ["QT_MOCK_TEST_XML_TEMPLATE_FILE"] = xml_log_template

def tearDownModule():
    print("\ntearDownModule(): Cleaning up temporary directory:",
          TEMPDIR.name)
    del os.environ["QT_MOCK_TEST_STATE_FILE"]
    TEMPDIR.cleanup()


# Helper to run a command and always capture output
def run(*args, **kwargs):
    if DEBUG:
        print("Running: ", args, flush=True)
    proc = subprocess.run(*args, stdout=PIPE, stderr=STDOUT, **kwargs)
    if DEBUG and proc.stdout:
        print(proc.stdout.decode(), flush=True)
    return proc

# Helper to run qt-testrunner.py with proper testing arguments.
def run_testrunner(xml_filename=None, extra_args=None, env=None):

    args = [ testrunner, mock_test ]
    if xml_filename:
        args += [ "--parse-xml-testlog", xml_filename ]
    if extra_args:
        args += extra_args

    return run(args, env=env)

# Write the XML_TEMPLATE to filename, replacing the templated results.
def write_xml_log(filename, failure=None):
    data = XML_TEMPLATE
    # Replace what was asked to fail with "fail"
    if type(failure) in (list, tuple):
        for template in failure:
            data = data.replace("{{"+template+"_result}}", "fail")
    elif type(failure) is str:
        data = data.replace("{{"+failure+"_result}}", "fail")
    # Replace the rest with "pass"
    data = re.sub(r"{{[^}]+}}", "pass", data)
    with open(filename, "w") as f:
        f.write(data)


# Test that qt_mock_test.py behaves well. This is necessary to properly
# test qt-testrunner.
class Test_qt_mock_test(unittest.TestCase):
    def setUp(self):
        state_file = os.environ["QT_MOCK_TEST_STATE_FILE"]
        if os.path.exists(state_file):
            os.remove(state_file)
    def test_always_pass(self):
        proc = run([mock_test, "always_pass"])
        self.assertEqual(proc.returncode, 0)
    def test_always_fail(self):
        proc = run([mock_test, "always_fail"])
        self.assertEqual(proc.returncode, 1)
    def test_fail_then_pass_2(self):
        proc = run([mock_test, "fail_then_pass:2"])
        self.assertEqual(proc.returncode, 1)
        proc = run([mock_test, "fail_then_pass:2"])
        self.assertEqual(proc.returncode, 1)
        proc = run([mock_test, "fail_then_pass:2"])
        self.assertEqual(proc.returncode, 0)
    def test_fail_then_pass_1(self):
        proc = run([mock_test, "fail_then_pass:1"])
        self.assertEqual(proc.returncode, 1)
        proc = run([mock_test, "fail_then_pass:1"])
        self.assertEqual(proc.returncode, 0)
    def test_fail_then_pass_many_tests(self):
        proc = run([mock_test, "fail_then_pass:1"])
        self.assertEqual(proc.returncode, 1)
        proc = run([mock_test, "fail_then_pass:2"])
        self.assertEqual(proc.returncode, 1)
        proc = run([mock_test, "fail_then_pass:1"])
        self.assertEqual(proc.returncode, 0)
        proc = run([mock_test, "fail_then_pass:2"])
        self.assertEqual(proc.returncode, 1)
        proc = run([mock_test, "fail_then_pass:2"])
        self.assertEqual(proc.returncode, 0)
    def test_xml_file_is_written(self):
        filename = os.path.join(TEMPDIR.name, "testlog.xml")
        proc = run([mock_test, "-o", filename+",xml"])
        self.assertEqual(proc.returncode, 0)
        self.assertTrue(os.path.exists(filename))
        self.assertGreater(os.path.getsize(filename), 0)
        os.remove(filename)

# Test regular invocations of qt-testrunner.
class Test_testrunner(unittest.TestCase):
    def setUp(self):
        state_file = os.environ["QT_MOCK_TEST_STATE_FILE"]
        if os.path.exists(state_file):
            os.remove(state_file)
        old_logfile = os.path.join(TEMPDIR.name, os.path.basename(mock_test) + ".xml")
        if os.path.exists(old_logfile):
            os.remove(old_logfile)
        self.env = dict()
        self.env["QT_MOCK_TEST_XML_TEMPLATE_FILE"] = os.environ["QT_MOCK_TEST_XML_TEMPLATE_FILE"]
        self.env["QT_MOCK_TEST_STATE_FILE"]        = state_file
        self.extra_args = [ "--log-dir", TEMPDIR.name ]
    def prepare_env(self, run_list=None):
        if run_list is not None:
            self.env['QT_MOCK_TEST_RUN_LIST'] = ",".join(run_list)
    def run2(self):
        return run_testrunner(extra_args=self.extra_args, env=self.env)
    def test_simple_invocation(self):
        # All tests pass.
        proc = self.run2()
        self.assertEqual(proc.returncode, 0)
    def test_always_pass(self):
        self.prepare_env(run_list=["always_pass"])
        proc = self.run2()
        self.assertEqual(proc.returncode, 0)
    def test_always_fail(self):
        self.prepare_env(run_list=["always_fail"])
        proc = self.run2()
        # TODO verify that re-runs==max_repeats
        self.assertEqual(proc.returncode, 2)
    def test_flaky_pass_1(self):
        self.prepare_env(run_list=["always_pass,fail_then_pass:1"])
        proc = self.run2()
        self.assertEqual(proc.returncode, 0)
    def test_flaky_pass_5(self):
        self.prepare_env(run_list=["always_pass,fail_then_pass:1,fail_then_pass:5"])
        proc = self.run2()
        self.assertEqual(proc.returncode, 0)
    def test_flaky_fail(self):
        self.prepare_env(run_list=["always_pass,fail_then_pass:6"])
        proc = self.run2()
        self.assertEqual(proc.returncode, 2)
    def test_flaky_pass_fail(self):
        self.prepare_env(run_list=["always_pass,fail_then_pass:1,fail_then_pass:6"])
        proc = self.run2()
        # TODO verify that one func was re-run and passed but the other failed.
        self.assertEqual(proc.returncode, 2)
    def test_initTestCase_fail_crash(self):
        self.prepare_env(run_list=["initTestCase,always_pass"])
        proc = self.run2()
        self.assertEqual(proc.returncode, 3)

    # If no XML file is found by qt-testrunner, it is usually considered a
    # CRASH and the whole test is re-run. Even when the return code is zero.
    # It is a PASS only if the test is not capable of XML output (see no_extra_args, TODO test it).
    def test_no_xml_log_written_pass_crash(self):
        del self.env["QT_MOCK_TEST_XML_TEMPLATE_FILE"]
        self.prepare_env(run_list=["always_pass"])
        proc = self.run2()
        self.assertEqual(proc.returncode, 3)
    # On the 2nd iteration of the full test, both of the tests pass.
    # Still it's a CRASH because no XML file was found.
    def test_no_xml_log_written_fail_then_pass_crash(self):
        del self.env["QT_MOCK_TEST_XML_TEMPLATE_FILE"]
        self.prepare_env(run_list=["always_pass,fail_then_pass:1"])
        proc = self.run2()
        # TODO verify that the whole test has run twice.
        self.assertEqual(proc.returncode, 3)
    # Even after 2 iterations of the full test we still get failures but no XML file,
    # and this is considered a CRASH.
    def test_no_xml_log_written_crash(self):
        del self.env["QT_MOCK_TEST_XML_TEMPLATE_FILE"]
        self.prepare_env(run_list=["fail_then_pass:2"])
        proc = self.run2()
        self.assertEqual(proc.returncode, 3)

    # If a test returns success but XML contains failures, it's a CRASH.
    def test_wrong_xml_log_written_1_crash(self):
        logfile = os.path.join(TEMPDIR.name, os.path.basename(mock_test) + ".xml")
        write_xml_log(logfile, failure="always_fail")
        del self.env["QT_MOCK_TEST_XML_TEMPLATE_FILE"]
        self.prepare_env(run_list=["always_pass"])
        proc = self.run2()
        self.assertEqual(proc.returncode, 3)
    # If a test returns failure but XML contains only pass, it's a CRASH.
    def test_wrong_xml_log_written_2_crash(self):
        logfile = os.path.join(TEMPDIR.name, os.path.basename(mock_test) + ".xml")
        write_xml_log(logfile)
        del self.env["QT_MOCK_TEST_XML_TEMPLATE_FILE"]
        self.prepare_env(run_list=["always_fail"])
        proc = self.run2()
        self.assertEqual(proc.returncode, 3)


# Test qt-testrunner script with an existing XML log file:
#   qt-testrunner.py qt_mock_test.py --parse-xml-testlog file.xml
# qt-testrunner should repeat the testcases that are logged as
# failures and fail or pass depending on how the testcases behave.
# Different XML files are generated for the following test cases.
#   + No failure logged.                  qt-testrunner should exit(0)
#   + The "always_pass" test has failed.  qt-testrunner should exit(0).
#   + The "always_fail" test has failed.  qt-testrunner should exit(2).
#   + The "always_crash" test has failed. qt-testrunner should exit(2).
#   + The "fail_then_pass:2" test failed. qt-testrunner should exit(0).
#   + The "fail_then_pass:5" test failed. qt-testrunner should exit(2).
#   + The "initTestCase" failed which is listed as NO_RERUN thus
#                                         qt-testrunner should exit(3).
class Test_testrunner_with_xml_logfile(unittest.TestCase):
    # Runs before every single test function, creating a unique temp file.
    def setUp(self):
        (_handle, self.xml_file) = mkstemp(
            suffix=".xml", prefix="qt_mock_test-log-",
            dir=TEMPDIR.name)
        if os.path.exists(os.environ["QT_MOCK_TEST_STATE_FILE"]):
            os.remove(os.environ["QT_MOCK_TEST_STATE_FILE"])
    def tearDown(self):
        os.remove(self.xml_file)
        del self.xml_file

    def test_no_failure(self):
        write_xml_log(self.xml_file, failure=None)
        proc = run_testrunner(self.xml_file)
        self.assertEqual(proc.returncode, 0)
    def test_always_pass_failed(self):
        write_xml_log(self.xml_file, failure="always_pass")
        proc = run_testrunner(self.xml_file)
        self.assertEqual(proc.returncode, 0)
    def test_always_pass_failed_max_repeats_0(self):
        write_xml_log(self.xml_file, failure="always_pass")
        proc = run_testrunner(self.xml_file,
                              extra_args=["--max-repeats", "0"])
        self.assertEqual(proc.returncode, 2)
    def test_always_fail_failed(self):
        write_xml_log(self.xml_file, failure="always_fail")
        proc = run_testrunner(self.xml_file)
        self.assertEqual(proc.returncode, 2)
        # Assert that one of the re-runs was in verbose mode
        matches = re.findall("VERBOSE RUN",
                             proc.stdout.decode())
        self.assertEqual(len(matches), 1)
        # Assert that the environment was altered too
        self.assertIn("QT_LOGGING_RULES", proc.stdout.decode())
    def test_always_crash_failed(self):
        write_xml_log(self.xml_file, failure="always_crash")
        proc = run_testrunner(self.xml_file)
        self.assertEqual(proc.returncode, 2)
    def test_fail_then_pass_2_failed(self):
        write_xml_log(self.xml_file, failure="fail_then_pass:2")
        proc = run_testrunner(self.xml_file)
        self.assertEqual(proc.returncode, 0)
    def test_fail_then_pass_5_failed(self):
        write_xml_log(self.xml_file, failure="fail_then_pass:5")
        proc = run_testrunner(self.xml_file)
        self.assertEqual(proc.returncode, 2)
    def test_with_two_failures(self):
        write_xml_log(self.xml_file,
                      failure=["always_pass", "fail_then_pass:2"])
        proc = run_testrunner(self.xml_file)
        self.assertEqual(proc.returncode, 0)
        # Check that test output is properly interleaved with qt-testrunner's logging.
        matches = re.findall(r"(PASS|FAIL!).*\n.*Test process exited with code",
                             proc.stdout.decode())
        self.assertEqual(len(matches), 4)
    def test_initTestCase_fail_crash(self):
        write_xml_log(self.xml_file, failure="initTestCase")
        proc = run_testrunner(self.xml_file)
        self.assertEqual(proc.returncode, 3)


if __name__ == "__main__":

    DEBUG = False
    if "--debug" in sys.argv:
        sys.argv.remove("--debug")
        DEBUG = True

    # We set failfast=True as we do not want the test suite to continue if the
    # tests of qt_mock_test failed. The next ones depend on it.
    unittest.main(failfast=True)