File: test_all_examples.py

package info (click to toggle)
python-traitsui 8.0.0-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 18,232 kB
  • sloc: python: 58,982; makefile: 113
file content (367 lines) | stat: -rw-r--r-- 11,264 bytes parent folder | download
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
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX
# All rights reserved.
#
# This software is provided without warranty under the terms of the BSD
# license included in LICENSE.txt and may be redistributed only under
# the conditions described in the aforementioned license. The license
# is also available online at http://www.enthought.com/licenses/BSD.txt
#
# Thanks for using Enthought open source!

""" Tests for demo and tutorial examples.
"""

import contextlib
import io
import os
import sys
import traceback
import unittest
from unittest import mock

import pkg_resources
from traits.api import HasTraits

from traitsui.tests._tools import (
    BaseTestMixin,
    is_qt,
    is_qt5,
    is_qt6,
    is_wx,
    process_cascade_events,
    requires_toolkit,
    ToolkitName,
)
from traitsui.testing.api import UITester

# This test file is not distributed nor is it in a package.
HERE = os.path.dirname(__file__)


class ExampleSearcher:
    """This object collects and reports example files to be tested."""

    def __init__(self, source_dirs):
        """
        Parameters
        ----------
        source_dirs : list of str
            List of directory paths from which Python files will be collected.
        """
        self.source_dirs = source_dirs
        self.files_may_be_skipped = {}

    def skip_file_if(self, filepath, condition, reason):
        """Mark a file to be skipped for a given condition.

        Parameters
        ----------
        filepath : str
            Path of the file which may be skipped from tests.
        condition: callable() -> bool
            The condition for skipping a file.
        reason : str
            Reason for skipping the file.
        """
        filepath = os.path.abspath(filepath)
        self.files_may_be_skipped[filepath] = (condition, reason)

    def is_skipped(self, filepath):
        """Return if the Python file should be skipped in test.

        Parameters
        ----------
        path : str
            Path to a file.

        Returns
        -------
        skipped : bool
            True if the file should be skipped.
        reason : str
            Reason why it should be skipped.
        """
        path = os.path.abspath(filepath)
        if path not in self.files_may_be_skipped:
            return False, ""
        condition, reason = self.files_may_be_skipped[path]
        return condition(), reason

    def validate(self):
        """Validate configuration. Currently this checks all files that may
        be skipped still exist.
        """
        for filepath in self.files_may_be_skipped:
            if not os.path.exists(filepath):
                raise RuntimeError("{} does not exist.".format(filepath))

    @staticmethod
    def _is_python_file(path):
        """Return true if the given path is (public) non-test Python file."""
        _, basename = os.path.split(path)
        _, ext = os.path.splitext(basename)
        return (
            ext == ".py"
            and not basename.startswith("_")
            and not basename.startswith("test_")
        )

    def get_python_files(self):
        """Report Python files to be tested or to be skipped.

        Returns
        -------
        accepted_files : list of str
            Python file paths to be tested.
        skipped_files : (filepath: str, reason: str)
            Skipped files. First item is the file path, second
            item is the reason why it is skipped.
        """
        accepted_files = []
        skipped_files = []
        for source_dir in self.source_dirs:
            for root, _, files in os.walk(source_dir):
                for filename in files:
                    path = os.path.abspath(os.path.join(root, filename))
                    if not self._is_python_file(path):
                        continue

                    skipped, reason = self.is_skipped(path)
                    if skipped:
                        skipped_files.append((path, reason))
                    else:
                        accepted_files.append(path)

        # Avoid arbitrary ordering from the OS
        return sorted(accepted_files), sorted(skipped_files)


# =============================================================================
# Configuration
# =============================================================================

# Tutorial files are not part of the package data
TUTORIALS = os.path.join(
    HERE,
    "..",
    "examples",
    "tutorials",
    "doc_examples",
    "examples",
)

# Demo files are part of the package data.
DEMO = pkg_resources.resource_filename("traitsui", "examples/demo")

#: Explicitly include folders from which example files should be found
#: recursively.
SOURCE_DIRS = [
    DEMO,
    TUTORIALS,
]

SEARCHER = ExampleSearcher(source_dirs=SOURCE_DIRS)
SEARCHER.skip_file_if(
    os.path.join(DEMO, "Advanced", "HDF5_tree_demo.py"),
    lambda: sys.platform == "darwin",
    "This example depends on PyTables which may be built to require CPUs with "
    "a specific AVX version that is not supported on a paricular OSX host.",
)
SEARCHER.skip_file_if(
    os.path.join(DEMO, "Advanced", "Table_editor_with_progress_column.py"),
    is_wx,
    "ProgressRenderer is not implemented in wx.",
)
SEARCHER.skip_file_if(
    os.path.join(DEMO, "Advanced", "Scrubber_editor_demo.py"),
    is_qt,
    "ScrubberEditor is not implemented in qt.",
)
SEARCHER.skip_file_if(
    os.path.join(DEMO, "Extras", "animated_GIF.py"),
    lambda: not is_wx(),
    "Only support wx",
)
SEARCHER.skip_file_if(
    os.path.join(DEMO, "Extras", "Tree_editor_with_TreeNodeRenderer.py"),
    lambda: not is_qt(),
    "Only support Qt",
)
SEARCHER.skip_file_if(
    os.path.join(DEMO, "Extras", "windows", "flash.py"),
    lambda: not is_wx(),
    "Only support wx",
)
SEARCHER.skip_file_if(
    os.path.join(DEMO, "Extras", "windows", "internet_explorer.py"),
    lambda: not is_wx(),
    "Only support wx",
)
SEARCHER.skip_file_if(
    os.path.join(TUTORIALS, "view_multi_object.py"),
    lambda: True,
    "Require wx and is blocking.",
)
SEARCHER.skip_file_if(
    os.path.join(TUTORIALS, "view_standalone.py"),
    lambda: True,
    "Require wx and is blocking.",
)

# Validate configuration.
SEARCHER.validate()


# =============================================================================
# Test run utility functions
# =============================================================================


def replaced_configure_traits(
    instance,
    filename=None,
    view=None,
    kind=None,
    edit=True,
    context=None,
    handler=None,
    id="",
    scrollable=None,
    **args,
):
    """Mocked configure_traits to launch then close the GUI."""
    ui_kwargs = dict(
        view=view,
        parent=None,
        kind="live",  # other options may block the test
        context=context,
        handler=handler,
        id=id,
        scrollable=scrollable,
        **args,
    )
    with UITester().create_ui(instance, ui_kwargs):
        pass


@contextlib.contextmanager
def replace_configure_traits():
    """Context manager to temporarily replace HasTraits.configure_traits
    with a mocked version such that GUI launched are closed soon after they
    are open.
    """
    original_func = HasTraits.configure_traits
    HasTraits.configure_traits = replaced_configure_traits
    try:
        yield
    finally:
        HasTraits.configure_traits = original_func


def run_file(file_path):
    """Execute a given Python file.

    Parameters
    ----------
    file_path : str
        File path to be tested.
    """
    with open(file_path, "r", encoding="utf-8") as f:
        content = f.read()

    globals = {
        "__name__": "__main__",
        "__file__": file_path,
    }
    with replace_configure_traits(), mock.patch(
        "sys.stdout", new_callable=io.StringIO
    ), mock.patch("sys.argv", [file_path]):
        # Mock stdout: Examples typically print educational information.
        # They are expected but they should not pollute test output.
        # Mock argv: Some example reads sys.argv to allow more arguments
        # But all examples should support being run without additional
        # arguments.
        exec(content, globals)


# =============================================================================
# load_tests protocol for unittest discover
# =============================================================================


def load_tests(loader, tests, pattern):
    """Implement load_tests protocol so that when unittest discover is run
    with this test module, the tests in the demo folder (not a package) are
    also loaded.

    See unittest documentation on load_tests
    """
    # Keep all the other loaded tests.
    suite = unittest.TestSuite()
    suite.addTests(tests)

    # Expand the test suite with tests from the examples, assuming
    # the test for ``group/script.py`` is placed in ``group/tests/`` directory.
    accepted_files, _ = SEARCHER.get_python_files()
    test_dirs = set(
        os.path.join(os.path.dirname(path), "tests") for path in accepted_files
    )
    test_dirs = set(path for path in test_dirs if os.path.exists(path))
    for dirpath in sorted(test_dirs):

        # Test files are scripts too and they demonstrate running the
        # tests. Mock the run side-effect when we load the test cases.
        with mock.patch.object(unittest.TextTestRunner, "run"):
            test_suite = unittest.TestLoader().discover(
                dirpath, pattern=pattern
            )
        if is_qt() or is_wx():
            suite.addTests(test_suite)

    return suite


# =============================================================================
# Test cases
# =============================================================================


@requires_toolkit([ToolkitName.qt, ToolkitName.wx])
class TestExample(BaseTestMixin, unittest.TestCase):
    def setUp(self):
        BaseTestMixin.setUp(self)

    def tearDown(self):
        BaseTestMixin.tearDown(self)

    def test_run(self):
        accepted_files, skipped_files = SEARCHER.get_python_files()

        for file_path in accepted_files:
            with self.subTest(file_path=file_path):
                try:
                    run_file(file_path)
                except Exception as exc:
                    message = "".join(
                        traceback.format_exception(*sys.exc_info())
                    )
                    self.fail(
                        "Executing {} failed with exception {}\n {}".format(
                            file_path, exc, message
                        )
                    )
                finally:
                    # Whatever failure, always flush the GUI event queue
                    # before running the next one.
                    process_cascade_events()

        # Report skipped files
        for file_path, reason in skipped_files:
            with self.subTest(file_path=file_path):
                # make up for unittest not reporting the parameter in skip
                # message.
                raise unittest.SkipTest(
                    "{reason} (File: {file_path})".format(
                        reason=reason, file_path=file_path
                    )
                )