|
2 | 2 | # |
3 | 3 | # SPDX-License-Identifier: Apache-2.0 |
4 | 4 |
|
| 5 | +import contextlib |
| 6 | +import io |
5 | 7 | import os |
6 | 8 | import platform |
7 | 9 | import shutil |
|
13 | 15 |
|
14 | 16 | import pytest |
15 | 17 |
|
16 | | -import west.version |
17 | 18 | from west import configuration as config |
18 | 19 | from west.app import main |
19 | 20 |
|
20 | | -# Expected PYTHONPATH pointing to the current source directory |
21 | | -PYTHONPATH = os.fspath(Path(west.version.__file__).resolve().parents[1]) |
22 | | - |
23 | | -# West's main Python script, runnable as a command-line executable |
24 | | -WEST_MAIN = main.__file__ |
25 | | - |
26 | | -# installed west executable (only set if it is correct version) |
27 | | -WEST_EXE = shutil.which('west') |
28 | | -if WEST_EXE: |
29 | | - # ensure that installed west exe has expected version |
30 | | - expected = west.version.__version__ |
31 | | - actual = subprocess.check_output([WEST_EXE,'--version'], text=True) |
32 | | - if expected not in actual: |
33 | | - WEST_EXE = None |
34 | | - |
35 | 21 | GIT = shutil.which('git') |
36 | 22 |
|
37 | 23 | # Git capabilities are discovered at runtime in |
|
78 | 64 |
|
79 | 65 | WINDOWS = (platform.system() == 'Windows') |
80 | 66 |
|
| 67 | +# |
| 68 | +# Contextmanager |
| 69 | +# |
| 70 | + |
| 71 | +@contextlib.contextmanager |
| 72 | +def update_env(env: dict): |
| 73 | + """ |
| 74 | + Temporarily update the process environment variables. |
| 75 | + This context manager updates `os.environ` with the key-value pairs |
| 76 | + provided in the `env` dictionary for the duration of the `with` block. |
| 77 | + Any existing environment variables are preserved and restored when |
| 78 | + the block exits. |
| 79 | + """ |
| 80 | + env_bak = dict(os.environ) |
| 81 | + env_vars = {} |
| 82 | + for k,v in env.items(): |
| 83 | + env_vars[k] = str(v) if v else "" |
| 84 | + os.environ.update(env_vars) |
| 85 | + try: |
| 86 | + yield |
| 87 | + finally: |
| 88 | + os.environ.clear() |
| 89 | + os.environ.update(env_bak) |
| 90 | + |
| 91 | + |
| 92 | +@contextlib.contextmanager |
| 93 | +def chdir(path): |
| 94 | + """ |
| 95 | + Temporarily change the current working directory. |
| 96 | + This context manager changes the current working directory to `path` |
| 97 | + for the duration of the `with` block. After the block exits, the |
| 98 | + working directory is restored to its original value. |
| 99 | + """ |
| 100 | + oldpwd = os.getcwd() |
| 101 | + os.chdir(path) |
| 102 | + try: |
| 103 | + yield |
| 104 | + finally: |
| 105 | + os.chdir(oldpwd) |
| 106 | + |
| 107 | + |
81 | 108 | # |
82 | 109 | # Test fixtures |
83 | 110 | # |
@@ -334,52 +361,53 @@ def check_output(*args, **kwargs): |
334 | 361 | raise |
335 | 362 | return out_bytes.decode(sys.getdefaultencoding()) |
336 | 363 |
|
| 364 | + |
337 | 365 | def cmd(cmd, cwd=None, stderr=None, env=None): |
338 | 366 | # Run a west command in a directory (cwd defaults to os.getcwd()). |
339 | 367 | # |
340 | | - # This helper takes the command as a string. |
| 368 | + # This helper takes the command as a string or list. |
341 | 369 | # |
342 | | - # This helper relies on the test environment to ensure that the |
343 | | - # 'west' executable is a bootstrapper installed from the current |
344 | | - # west source code. If installed 'west' executable has correct version, |
345 | | - # it is preferred. Otherwise, fall back to running the local Python module. |
346 | | - # |
347 | | - # stdout from cmd is captured and returned. The command is run in |
348 | | - # a python subprocess so that program-level setup and teardown |
349 | | - # happen fresh. |
350 | | - |
351 | | - # If you have quoting issues: do NOT quote. It's not portable. |
352 | | - # Instead, pass `cmd` as a list. |
353 | | - |
354 | | - west_exe = WEST_EXE or WEST_MAIN |
355 | | - |
356 | | - cmd = [west_exe] + (cmd.split() if isinstance(cmd, str) else cmd) |
357 | | - print('running:', cmd) |
358 | | - |
359 | | - if env: |
360 | | - print('with non-default environment:') |
361 | | - for k in env: |
362 | | - if k not in os.environ or env[k] != os.environ[k]: |
363 | | - print(f'\t{k}={env[k]}') |
364 | | - for k in os.environ: |
365 | | - if k not in env: |
366 | | - print(f'\t{k}: deleted, was: {os.environ[k]}') |
367 | | - if cwd is not None: |
368 | | - cwd = os.fspath(cwd) |
369 | | - print(f'in {cwd}') |
370 | | - try: |
371 | | - return check_output(cmd, cwd=cwd, stderr=stderr, env=env) |
372 | | - except subprocess.CalledProcessError: |
373 | | - print('cmd: west:', west_exe, file=sys.stderr) |
374 | | - raise |
| 370 | + # This function executes the `main()` function with the given command as |
| 371 | + # arguments, capturing its stdout output while optionally modifying the working |
| 372 | + # directory and environment. |
| 373 | + cmd = (cmd.split() if isinstance(cmd, str) else cmd) |
| 374 | + # every member of cmd must be string |
| 375 | + cmd = [str(c) for c in cmd] |
| 376 | + |
| 377 | + stdout_buf = io.StringIO() |
| 378 | + with chdir(cwd or Path.cwd()): |
| 379 | + with update_env(env or {}): |
| 380 | + with contextlib.redirect_stdout(stdout_buf), \ |
| 381 | + contextlib.redirect_stderr(stderr or sys.stderr): |
| 382 | + try: |
| 383 | + main.main(cmd) |
| 384 | + except SystemExit as e: |
| 385 | + if e.code: |
| 386 | + raise e |
| 387 | + except Exception as e: |
| 388 | + print(f'Uncaught exception type {e}', file=sys.stderr) |
| 389 | + raise e |
| 390 | + return stdout_buf.getvalue() |
375 | 391 |
|
376 | 392 |
|
377 | 393 | def cmd_raises(cmd_str_or_list, expected_exception_type, cwd=None, env=None): |
378 | 394 | # Similar to 'cmd' but an expected exception is caught. |
379 | | - # Returns the output together with stderr data |
| 395 | + # Returns the stderr data. |
| 396 | + stderr_buf = io.StringIO() |
380 | 397 | with pytest.raises(expected_exception_type) as exc_info: |
381 | | - cmd(cmd_str_or_list, stderr=subprocess.STDOUT, cwd=cwd, env=env) |
382 | | - return exc_info.value.output.decode("utf-8") |
| 398 | + cmd(cmd_str_or_list, stderr=stderr_buf, cwd=cwd, env=env) |
| 399 | + return exc_info, stderr_buf.getvalue() |
| 400 | + |
| 401 | + |
| 402 | +def cmd_all_stdout(cmd, cwd=None, stderr=None, env=None): |
| 403 | + # This function is similar to `cmd()`, but runs the command in a separate |
| 404 | + # Python subprocess. This ensures that both Python-level stdout and any |
| 405 | + # subprocess output invoked by `main` are captured together in a single |
| 406 | + # string. It also ensures that program-level setup and teardown in `main` |
| 407 | + # happen fresh for each call. |
| 408 | + WEST_MAIN = main.__file__ |
| 409 | + cmd = (cmd.split() if isinstance(cmd, str) else cmd) |
| 410 | + return check_output([sys.executable, WEST_MAIN] + cmd, cwd=cwd) |
383 | 411 |
|
384 | 412 |
|
385 | 413 | def create_workspace(workspace_dir, and_git=True): |
|
0 commit comments