Make GHA tests parallel by using xdist (#32361)
* Add two no-op jobs named "all-prechecks" and "all" These are a suggestion from @tgamblin, they are stable named markers we can use from gitlab and possibly for required checks to make CI more resilient to refactors changing the names of specific checks. * Enable parallel testing using xdist for unit testing in CI * Normalize tmp paths to deal with macos * add -u flag compatibility to spack python As of now, it is accepted and ignored. The usage with xdist, where it is invoked specifically by `python -u spack python` which is then passed `-u` by xdist is the entire reason for doing this. It should never be used without explicitly passing -u to the executing python interpreter. * use spack python in xdist to support python 2 When running on python2, spack has many import cycles unless started through main. To allow that, this uses `spack python` as the interpreter, leveraging the `-u` support so xdist doesn't error out when it unconditionally requests unbuffered binary IO. * Use shutil.move to account for tmpdir being in a separate filesystem sometimes
This commit is contained in:
		
							
								
								
									
										1
									
								
								.flake8
									
									
									
									
									
								
							
							
						
						
									
										1
									
								
								.flake8
									
									
									
									
									
								
							| @@ -29,6 +29,7 @@ max-line-length = 99 | |||||||
| # | # | ||||||
| per-file-ignores = | per-file-ignores = | ||||||
|   var/spack/repos/*/package.py:F403,F405,F821 |   var/spack/repos/*/package.py:F403,F405,F821 | ||||||
|  |   *-ci-package.py:F403,F405,F821 | ||||||
|  |  | ||||||
| # exclude things we usually do not want linting for. | # exclude things we usually do not want linting for. | ||||||
| # These still get linted when passed explicitly, as when spack flake8 passes | # These still get linted when passed explicitly, as when spack flake8 passes | ||||||
|   | |||||||
							
								
								
									
										44
									
								
								.github/workflows/audit.yaml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								.github/workflows/audit.yaml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,44 @@ | |||||||
|  | name: audit | ||||||
|  |  | ||||||
|  | on: | ||||||
|  |   workflow_call: | ||||||
|  |     inputs: | ||||||
|  |       with_coverage: | ||||||
|  |         required: true | ||||||
|  |         type: string | ||||||
|  |       python_version: | ||||||
|  |         required: true | ||||||
|  |         type: string | ||||||
|  |  | ||||||
|  | concurrency: | ||||||
|  |   group: audit-${{inputs.python_version}}-${{github.ref}}-${{github.event.pull_request.number || github.run_number}} | ||||||
|  |   cancel-in-progress: true | ||||||
|  |  | ||||||
|  | jobs: | ||||||
|  |   # Run audits on all the packages in the built-in repository | ||||||
|  |   package-audits: | ||||||
|  |     runs-on: ubuntu-latest | ||||||
|  |     steps: | ||||||
|  |     - uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b # @v2 | ||||||
|  |     - uses: actions/setup-python@b55428b1882923874294fa556849718a1d7f2ca5 # @v2 | ||||||
|  |       with: | ||||||
|  |         python-version: ${{inputs.python_version}} | ||||||
|  |     - name: Install Python packages | ||||||
|  |       run: | | ||||||
|  |         pip install --upgrade pip six setuptools pytest codecov 'coverage[toml]<=6.2' | ||||||
|  |     - name: Package audits (with coverage) | ||||||
|  |       if: ${{ inputs.with_coverage == 'true' }} | ||||||
|  |       run: | | ||||||
|  |           . share/spack/setup-env.sh | ||||||
|  |           coverage run $(which spack) audit packages | ||||||
|  |           coverage combine | ||||||
|  |           coverage xml | ||||||
|  |     - name: Package audits (without coverage) | ||||||
|  |       if: ${{ inputs.with_coverage == 'false' }} | ||||||
|  |       run: | | ||||||
|  |           . share/spack/setup-env.sh | ||||||
|  |           $(which spack) audit packages | ||||||
|  |     - uses: codecov/codecov-action@81cd2dc8148241f03f5839d295e000b8f761e378 # @v2.1.0 | ||||||
|  |       if: ${{ inputs.with_coverage == 'true' }} | ||||||
|  |       with: | ||||||
|  |         flags: unittests,linux,audits | ||||||
							
								
								
									
										2
									
								
								.github/workflows/bootstrap.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/bootstrap.yml
									
									
									
									
										vendored
									
									
								
							| @@ -9,7 +9,7 @@ on: | |||||||
|     - cron: '16 2 * * *' |     - cron: '16 2 * * *' | ||||||
|  |  | ||||||
| concurrency: | concurrency: | ||||||
|   group: bootstrap-${{ github.workflow }}-${{ github.event.pull_request.number || github.run_number }} |   group: bootstrap-${{github.ref}}-${{github.event.pull_request.number || github.run_number}} | ||||||
|   cancel-in-progress: true |   cancel-in-progress: true | ||||||
|  |  | ||||||
| jobs: | jobs: | ||||||
|   | |||||||
							
								
								
									
										2
									
								
								.github/workflows/build-containers.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/build-containers.yml
									
									
									
									
										vendored
									
									
								
							| @@ -20,7 +20,7 @@ on: | |||||||
|     types: [published] |     types: [published] | ||||||
|  |  | ||||||
| concurrency: | concurrency: | ||||||
|   group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_number }} |   group: build_containers-${{github.ref}}-${{github.event.pull_request.number || github.run_number}} | ||||||
|   cancel-in-progress: true |   cancel-in-progress: true | ||||||
|  |  | ||||||
| jobs: | jobs: | ||||||
|   | |||||||
							
								
								
									
										25
									
								
								.github/workflows/ci.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										25
									
								
								.github/workflows/ci.yaml
									
									
									
									
										vendored
									
									
								
							| @@ -11,7 +11,7 @@ on: | |||||||
|       - releases/** |       - releases/** | ||||||
|  |  | ||||||
| concurrency: | concurrency: | ||||||
|   group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_number }} |   group: ci-${{github.ref}}-${{github.event.pull_request.number || github.run_number}} | ||||||
|   cancel-in-progress: true |   cancel-in-progress: true | ||||||
|  |  | ||||||
| jobs: | jobs: | ||||||
| @@ -20,6 +20,18 @@ jobs: | |||||||
|     uses: ./.github/workflows/valid-style.yml |     uses: ./.github/workflows/valid-style.yml | ||||||
|     with: |     with: | ||||||
|       with_coverage: ${{ needs.changes.outputs.with_coverage }} |       with_coverage: ${{ needs.changes.outputs.with_coverage }} | ||||||
|  |   audit-ancient-python: | ||||||
|  |     uses: ./.github/workflows/audit.yaml | ||||||
|  |     needs: [ changes ] | ||||||
|  |     with: | ||||||
|  |       with_coverage: ${{ needs.changes.outputs.with_coverage }} | ||||||
|  |       python_version: 2.7 | ||||||
|  |   all-prechecks: | ||||||
|  |     needs: [ prechecks ] | ||||||
|  |     runs-on: ubuntu-latest | ||||||
|  |     steps: | ||||||
|  |     - name: Success | ||||||
|  |       run: "true" | ||||||
|   # Check which files have been updated by the PR |   # Check which files have been updated by the PR | ||||||
|   changes: |   changes: | ||||||
|     runs-on: ubuntu-latest |     runs-on: ubuntu-latest | ||||||
| @@ -53,6 +65,7 @@ jobs: | |||||||
|             - 'share/spack/**' |             - 'share/spack/**' | ||||||
|             - '.github/workflows/bootstrap.yml' |             - '.github/workflows/bootstrap.yml' | ||||||
|             core: |             core: | ||||||
|  |             - '!lib/spack/docs/**' | ||||||
|             - './!(var/**)/**' |             - './!(var/**)/**' | ||||||
|             packages: |             packages: | ||||||
|             - 'var/**' |             - 'var/**' | ||||||
| @@ -79,7 +92,7 @@ jobs: | |||||||
|     needs: [ prechecks, changes ] |     needs: [ prechecks, changes ] | ||||||
|     uses: ./.github/workflows/bootstrap.yml |     uses: ./.github/workflows/bootstrap.yml | ||||||
|   unit-tests: |   unit-tests: | ||||||
|     if: ${{ github.repository == 'spack/spack' }} |     if: ${{ github.repository == 'spack/spack' && needs.changes.outputs.core == 'true' }} | ||||||
|     needs: [ prechecks, changes ] |     needs: [ prechecks, changes ] | ||||||
|     uses: ./.github/workflows/unit_tests.yaml |     uses: ./.github/workflows/unit_tests.yaml | ||||||
|     with: |     with: | ||||||
| @@ -87,7 +100,13 @@ jobs: | |||||||
|       packages: ${{ needs.changes.outputs.packages }} |       packages: ${{ needs.changes.outputs.packages }} | ||||||
|       with_coverage: ${{ needs.changes.outputs.with_coverage }} |       with_coverage: ${{ needs.changes.outputs.with_coverage }} | ||||||
|   windows: |   windows: | ||||||
|     if: ${{ github.repository == 'spack/spack' }} |     if: ${{ github.repository == 'spack/spack' && needs.changes.outputs.core == 'true' }} | ||||||
|     needs: [ prechecks ] |     needs: [ prechecks ] | ||||||
|     uses: ./.github/workflows/windows_python.yml |     uses: ./.github/workflows/windows_python.yml | ||||||
|  |   all: | ||||||
|  |     needs: [ windows, unit-tests, bootstrap, audit-ancient-python ] | ||||||
|  |     runs-on: ubuntu-latest | ||||||
|  |     steps: | ||||||
|  |     - name: Success | ||||||
|  |       run: "true" | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										33
									
								
								.github/workflows/unit_tests.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										33
									
								
								.github/workflows/unit_tests.yaml
									
									
									
									
										vendored
									
									
								
							| @@ -1,6 +1,7 @@ | |||||||
| name: unit tests | name: unit tests | ||||||
|  |  | ||||||
| on: | on: | ||||||
|  |   workflow_dispatch: | ||||||
|   workflow_call: |   workflow_call: | ||||||
|     inputs: |     inputs: | ||||||
|       core: |       core: | ||||||
| @@ -14,7 +15,7 @@ on: | |||||||
|         type: string |         type: string | ||||||
|  |  | ||||||
| concurrency: | concurrency: | ||||||
|   group: unit_tests-${{ github.workflow }}-${{ github.event.pull_request.number || github.run_number }} |   group: unit_tests-${{github.ref}}-${{github.event.pull_request.number || github.run_number}} | ||||||
|   cancel-in-progress: true |   cancel-in-progress: true | ||||||
|  |  | ||||||
| jobs: | jobs: | ||||||
| @@ -25,11 +26,26 @@ jobs: | |||||||
|       matrix: |       matrix: | ||||||
|         python-version: ['2.7', '3.6', '3.7', '3.8', '3.9', '3.10'] |         python-version: ['2.7', '3.6', '3.7', '3.8', '3.9', '3.10'] | ||||||
|         concretizer: ['clingo'] |         concretizer: ['clingo'] | ||||||
|  |         on_develop: | ||||||
|  |         - ${{ github.ref == 'refs/heads/develop' }} | ||||||
|         include: |         include: | ||||||
|         - python-version: 2.7 |         - python-version: 2.7 | ||||||
|           concretizer: original |           concretizer: original | ||||||
|         - python-version: 3.9 |           on_develop: false | ||||||
|  |         - python-version: '3.10' | ||||||
|           concretizer: original |           concretizer: original | ||||||
|  |           on_develop: false | ||||||
|  |         exclude: | ||||||
|  |         - python-version: '3.7' | ||||||
|  |           concretizer: 'clingo' | ||||||
|  |           on_develop: false | ||||||
|  |         - python-version: '3.8' | ||||||
|  |           concretizer: 'clingo' | ||||||
|  |           on_develop: false | ||||||
|  |         - python-version: '3.9' | ||||||
|  |           concretizer: 'clingo' | ||||||
|  |           on_develop: false | ||||||
|  |  | ||||||
|     steps: |     steps: | ||||||
|     - uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b # @v2 |     - uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b # @v2 | ||||||
|       with: |       with: | ||||||
| @@ -46,7 +62,7 @@ jobs: | |||||||
|               patchelf cmake bison libbison-dev kcov |               patchelf cmake bison libbison-dev kcov | ||||||
|     - name: Install Python packages |     - name: Install Python packages | ||||||
|       run: | |       run: | | ||||||
|           pip install --upgrade pip six setuptools pytest codecov "coverage[toml]<=6.2" |           pip install --upgrade pip six setuptools pytest codecov "coverage[toml]<=6.2" pytest-xdist | ||||||
|           # ensure style checks are not skipped in unit tests for python >= 3.6 |           # ensure style checks are not skipped in unit tests for python >= 3.6 | ||||||
|           # note that true/false (i.e., 1/0) are opposite in conditions in python and bash |           # note that true/false (i.e., 1/0) are opposite in conditions in python and bash | ||||||
|           if python -c 'import sys; sys.exit(not sys.version_info >= (3, 6))'; then |           if python -c 'import sys; sys.exit(not sys.version_info >= (3, 6))'; then | ||||||
| @@ -108,7 +124,7 @@ jobs: | |||||||
|           sudo apt-get install -y coreutils kcov csh zsh tcsh fish dash bash |           sudo apt-get install -y coreutils kcov csh zsh tcsh fish dash bash | ||||||
|     - name: Install Python packages |     - name: Install Python packages | ||||||
|       run: | |       run: | | ||||||
|           pip install --upgrade pip six setuptools pytest codecov coverage[toml]==6.2 |           pip install --upgrade pip six setuptools pytest codecov coverage[toml]==6.2 pytest-xdist | ||||||
|     - name: Setup git configuration |     - name: Setup git configuration | ||||||
|       run: | |       run: | | ||||||
|           # Need this for the git tests to succeed. |           # Need this for the git tests to succeed. | ||||||
| @@ -174,7 +190,7 @@ jobs: | |||||||
|               patchelf kcov |               patchelf kcov | ||||||
|     - name: Install Python packages |     - name: Install Python packages | ||||||
|       run: | |       run: | | ||||||
|           pip install --upgrade pip six setuptools pytest codecov coverage[toml]==6.2 clingo |           pip install --upgrade pip six setuptools pytest codecov coverage[toml]==6.2 clingo pytest-xdist | ||||||
|     - name: Setup git configuration |     - name: Setup git configuration | ||||||
|       run: | |       run: | | ||||||
|           # Need this for the git tests to succeed. |           # Need this for the git tests to succeed. | ||||||
| @@ -216,7 +232,7 @@ jobs: | |||||||
|     - name: Install Python packages |     - name: Install Python packages | ||||||
|       run: | |       run: | | ||||||
|           pip install --upgrade pip six setuptools |           pip install --upgrade pip six setuptools | ||||||
|           pip install --upgrade pytest codecov coverage[toml]==6.2 |           pip install --upgrade pytest codecov coverage[toml]==6.2 pytest-xdist | ||||||
|     - name: Setup Homebrew packages |     - name: Setup Homebrew packages | ||||||
|       run: | |       run: | | ||||||
|         brew install dash fish gcc gnupg2 kcov |         brew install dash fish gcc gnupg2 kcov | ||||||
| @@ -229,9 +245,10 @@ jobs: | |||||||
|         . share/spack/setup-env.sh |         . share/spack/setup-env.sh | ||||||
|         $(which spack) bootstrap untrust spack-install |         $(which spack) bootstrap untrust spack-install | ||||||
|         $(which spack) solve zlib |         $(which spack) solve zlib | ||||||
|  |         common_args=(--dist loadfile --tx '4*popen//python=./bin/spack-tmpconfig python -u ./bin/spack python' -x) | ||||||
|         if [ "${{ inputs.with_coverage }}" == "true" ] |         if [ "${{ inputs.with_coverage }}" == "true" ] | ||||||
|         then |         then | ||||||
|           coverage run $(which spack) unit-test -x |           coverage run $(which spack) unit-test "${common_args[@]}" | ||||||
|           coverage combine |           coverage combine | ||||||
|           coverage xml |           coverage xml | ||||||
|           # Delete the symlink going from ./lib/spack/docs/_spack_root back to |           # Delete the symlink going from ./lib/spack/docs/_spack_root back to | ||||||
| @@ -239,7 +256,7 @@ jobs: | |||||||
|           rm lib/spack/docs/_spack_root |           rm lib/spack/docs/_spack_root | ||||||
|         else |         else | ||||||
|           echo "ONLY PACKAGE RECIPES CHANGED [skipping coverage]" |           echo "ONLY PACKAGE RECIPES CHANGED [skipping coverage]" | ||||||
|           $(which spack) unit-test -x -m "not maybeslow" -k "test_all_virtual_packages_have_default_providers" |           $(which spack) unit-test "${common_args[@]}" -m "not maybeslow" -k "test_all_virtual_packages_have_default_providers" | ||||||
|         fi |         fi | ||||||
|     - uses: codecov/codecov-action@81cd2dc8148241f03f5839d295e000b8f761e378 # @v2.1.0 |     - uses: codecov/codecov-action@81cd2dc8148241f03f5839d295e000b8f761e378 # @v2.1.0 | ||||||
|       if: ${{ inputs.with_coverage == 'true' }} |       if: ${{ inputs.with_coverage == 'true' }} | ||||||
|   | |||||||
							
								
								
									
										32
									
								
								.github/workflows/valid-style.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										32
									
								
								.github/workflows/valid-style.yml
									
									
									
									
										vendored
									
									
								
							| @@ -8,7 +8,7 @@ on: | |||||||
|         type: string |         type: string | ||||||
|  |  | ||||||
| concurrency: | concurrency: | ||||||
|   group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_number }} |   group: style-${{github.ref}}-${{github.event.pull_request.number || github.run_number}} | ||||||
|   cancel-in-progress: true |   cancel-in-progress: true | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -53,30 +53,8 @@ jobs: | |||||||
|     - name: Run style tests |     - name: Run style tests | ||||||
|       run: | |       run: | | ||||||
|           share/spack/qa/run-style-tests |           share/spack/qa/run-style-tests | ||||||
|   # Run audits on all the packages in the built-in repository |   audit: | ||||||
|   package-audits: |     uses: ./.github/workflows/audit.yaml | ||||||
|     runs-on: ubuntu-latest |  | ||||||
|     steps: |  | ||||||
|     - uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b # @v2 |  | ||||||
|     - uses: actions/setup-python@b55428b1882923874294fa556849718a1d7f2ca5 # @v2 |  | ||||||
|     with: |     with: | ||||||
|         python-version: '3.10' |       with_coverage: ${{ inputs.with_coverage }} | ||||||
|     - name: Install Python packages |       python_version: '3.10' | ||||||
|       run: | |  | ||||||
|         pip install --upgrade pip six setuptools pytest codecov coverage[toml]==6.2 |  | ||||||
|     - name: Package audits (with coverage) |  | ||||||
|       if: ${{ inputs.with_coverage == 'true' }} |  | ||||||
|       run: | |  | ||||||
|           . share/spack/setup-env.sh |  | ||||||
|           coverage run $(which spack) audit packages |  | ||||||
|           coverage combine |  | ||||||
|           coverage xml |  | ||||||
|     - name: Package audits (without coverage) |  | ||||||
|       if: ${{ inputs.with_coverage == 'false' }} |  | ||||||
|       run: | |  | ||||||
|           . share/spack/setup-env.sh |  | ||||||
|           $(which spack) audit packages |  | ||||||
|     - uses: codecov/codecov-action@81cd2dc8148241f03f5839d295e000b8f761e378 # @v2.1.0 |  | ||||||
|       if: ${{ inputs.with_coverage == 'true' }} |  | ||||||
|       with: |  | ||||||
|         flags: unittests,linux,audits |  | ||||||
|   | |||||||
							
								
								
									
										2
									
								
								.github/workflows/windows_python.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/windows_python.yml
									
									
									
									
										vendored
									
									
								
							| @@ -4,7 +4,7 @@ on: | |||||||
|   workflow_call: |   workflow_call: | ||||||
|  |  | ||||||
| concurrency: | concurrency: | ||||||
|   group: windows-${{ github.workflow }}-${{ github.event.pull_request.number || github.run_number }} |   group: windows-${{github.ref}}-${{github.event.pull_request.number || github.run_number}} | ||||||
|   cancel-in-progress: true |   cancel-in-progress: true | ||||||
|  |  | ||||||
| defaults: | defaults: | ||||||
|   | |||||||
| @@ -7,7 +7,6 @@ export TMPDIR="${XDG_RUNTIME_DIR}" | |||||||
| export TMP_DIR="$(mktemp -d -t spack-test-XXXXX)" | export TMP_DIR="$(mktemp -d -t spack-test-XXXXX)" | ||||||
| clean_up() { | clean_up() { | ||||||
|     [[ -n "$TMPCONFIG_DEBUG" ]] && printf "cleaning up: $TMP_DIR\n" |     [[ -n "$TMPCONFIG_DEBUG" ]] && printf "cleaning up: $TMP_DIR\n" | ||||||
|     [[ -n "$TMPCONFIG_DEBUG" ]] && tree "$TMP_DIR" |  | ||||||
|     rm -rf "$TMP_DIR" |     rm -rf "$TMP_DIR" | ||||||
| } | } | ||||||
| trap clean_up EXIT | trap clean_up EXIT | ||||||
|   | |||||||
| @@ -228,8 +228,8 @@ def __init__(self, controller_function, minion_function): | |||||||
|         self.minion_function = minion_function |         self.minion_function = minion_function | ||||||
| 
 | 
 | ||||||
|         # these can be optionally set to change defaults |         # these can be optionally set to change defaults | ||||||
|         self.controller_timeout = 1 |         self.controller_timeout = 3 | ||||||
|         self.sleep_time = 0 |         self.sleep_time = 0.1 | ||||||
| 
 | 
 | ||||||
|     def start(self, **kwargs): |     def start(self, **kwargs): | ||||||
|         """Start the controller and minion processes. |         """Start the controller and minion processes. | ||||||
|   | |||||||
| @@ -30,6 +30,12 @@ def setup_parser(subparser): | |||||||
|         help="print the Python version number and exit", |         help="print the Python version number and exit", | ||||||
|     ) |     ) | ||||||
|     subparser.add_argument("-c", dest="python_command", help="command to execute") |     subparser.add_argument("-c", dest="python_command", help="command to execute") | ||||||
|  |     subparser.add_argument( | ||||||
|  |         "-u", | ||||||
|  |         dest="unbuffered", | ||||||
|  |         action="store_true", | ||||||
|  |         help="for compatibility with xdist, do not use without adding -u to the interpreter", | ||||||
|  |     ) | ||||||
|     subparser.add_argument( |     subparser.add_argument( | ||||||
|         "-i", |         "-i", | ||||||
|         dest="python_interpreter", |         dest="python_interpreter", | ||||||
|   | |||||||
| @@ -18,6 +18,7 @@ | |||||||
| import pstats | import pstats | ||||||
| import re | import re | ||||||
| import signal | import signal | ||||||
|  | import subprocess as sp | ||||||
| import sys | import sys | ||||||
| import traceback | import traceback | ||||||
| import warnings | import warnings | ||||||
| @@ -623,15 +624,19 @@ class SpackCommand(object): | |||||||
|     their output. |     their output. | ||||||
|     """ |     """ | ||||||
| 
 | 
 | ||||||
|     def __init__(self, command_name): |     def __init__(self, command_name, subprocess=False): | ||||||
|         """Create a new SpackCommand that invokes ``command_name`` when called. |         """Create a new SpackCommand that invokes ``command_name`` when called. | ||||||
| 
 | 
 | ||||||
|         Args: |         Args: | ||||||
|             command_name (str): name of the command to invoke |             command_name (str): name of the command to invoke | ||||||
|  |             subprocess (bool): whether to fork a subprocess or not. Currently not supported on | ||||||
|  |                 Windows, where it is always False. | ||||||
|         """ |         """ | ||||||
|         self.parser = make_argument_parser() |         self.parser = make_argument_parser() | ||||||
|         self.command = self.parser.add_command(command_name) |         self.command = self.parser.add_command(command_name) | ||||||
|         self.command_name = command_name |         self.command_name = command_name | ||||||
|  |         # TODO: figure out how to support this on windows | ||||||
|  |         self.subprocess = subprocess if sys.platform != "win32" else False | ||||||
| 
 | 
 | ||||||
|     def __call__(self, *argv, **kwargs): |     def __call__(self, *argv, **kwargs): | ||||||
|         """Invoke this SpackCommand. |         """Invoke this SpackCommand. | ||||||
| @@ -656,11 +661,21 @@ def __call__(self, *argv, **kwargs): | |||||||
|         self.error = None |         self.error = None | ||||||
| 
 | 
 | ||||||
|         prepend = kwargs["global_args"] if "global_args" in kwargs else [] |         prepend = kwargs["global_args"] if "global_args" in kwargs else [] | ||||||
| 
 |  | ||||||
|         args, unknown = self.parser.parse_known_args(prepend + [self.command_name] + list(argv)) |  | ||||||
| 
 |  | ||||||
|         fail_on_error = kwargs.get("fail_on_error", True) |         fail_on_error = kwargs.get("fail_on_error", True) | ||||||
| 
 | 
 | ||||||
|  |         if self.subprocess: | ||||||
|  |             p = sp.Popen( | ||||||
|  |                 [spack.paths.spack_script, self.command_name] + prepend + list(argv), | ||||||
|  |                 stdout=sp.PIPE, | ||||||
|  |                 stderr=sp.STDOUT, | ||||||
|  |             ) | ||||||
|  |             out, self.returncode = p.communicate() | ||||||
|  |             out = out.decode() | ||||||
|  |         else: | ||||||
|  |             args, unknown = self.parser.parse_known_args( | ||||||
|  |                 prepend + [self.command_name] + list(argv) | ||||||
|  |             ) | ||||||
|  | 
 | ||||||
|             out = StringIO() |             out = StringIO() | ||||||
|             try: |             try: | ||||||
|                 with log_output(out): |                 with log_output(out): | ||||||
| @@ -675,6 +690,7 @@ def __call__(self, *argv, **kwargs): | |||||||
|                 if fail_on_error: |                 if fail_on_error: | ||||||
|                     self._log_command_output(out) |                     self._log_command_output(out) | ||||||
|                     raise |                     raise | ||||||
|  |             out = out.getvalue() | ||||||
| 
 | 
 | ||||||
|         if fail_on_error and self.returncode not in (None, 0): |         if fail_on_error and self.returncode not in (None, 0): | ||||||
|             self._log_command_output(out) |             self._log_command_output(out) | ||||||
| @@ -683,7 +699,7 @@ def __call__(self, *argv, **kwargs): | |||||||
|                 % (self.returncode, self.command_name, ", ".join("'%s'" % a for a in argv)) |                 % (self.returncode, self.command_name, ", ".join("'%s'" % a for a in argv)) | ||||||
|             ) |             ) | ||||||
| 
 | 
 | ||||||
|         return out.getvalue() |         return out | ||||||
| 
 | 
 | ||||||
|     def _log_command_output(self, out): |     def _log_command_output(self, out): | ||||||
|         if tty.is_verbose(): |         if tty.is_verbose(): | ||||||
|   | |||||||
| @@ -484,6 +484,7 @@ def test_get_spec_filter_list(mutable_mock_env_path, config, mutable_mock_repo): | |||||||
|     assert affected_pkg_names == expected_affected_pkg_names |     assert affected_pkg_names == expected_affected_pkg_names | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | @pytest.mark.maybeslow | ||||||
| @pytest.mark.regression("29947") | @pytest.mark.regression("29947") | ||||||
| def test_affected_specs_on_first_concretization(mutable_mock_env_path, config): | def test_affected_specs_on_first_concretization(mutable_mock_env_path, config): | ||||||
|     e = ev.create("first_concretization") |     e = ev.create("first_concretization") | ||||||
|   | |||||||
| @@ -50,12 +50,14 @@ def test_checksum(arguments, expected, mock_packages, mock_stage): | |||||||
| 
 | 
 | ||||||
| @pytest.mark.skipif(sys.platform == "win32", reason="Not supported on Windows (yet)") | @pytest.mark.skipif(sys.platform == "win32", reason="Not supported on Windows (yet)") | ||||||
| def test_checksum_interactive(mock_packages, mock_fetch, mock_stage, monkeypatch): | def test_checksum_interactive(mock_packages, mock_fetch, mock_stage, monkeypatch): | ||||||
|  |     # TODO: mock_fetch doesn't actually work with stage, working around with ignoring | ||||||
|  |     # fail_on_error for now | ||||||
|     def _get_number(*args, **kwargs): |     def _get_number(*args, **kwargs): | ||||||
|         return 1 |         return 1 | ||||||
| 
 | 
 | ||||||
|     monkeypatch.setattr(tty, "get_number", _get_number) |     monkeypatch.setattr(tty, "get_number", _get_number) | ||||||
| 
 | 
 | ||||||
|     output = spack_checksum("preferred-test") |     output = spack_checksum("preferred-test", fail_on_error=False) | ||||||
|     assert "version of preferred-test" in output |     assert "version of preferred-test" in output | ||||||
|     assert "version(" in output |     assert "version(" in output | ||||||
| 
 | 
 | ||||||
|   | |||||||
| @@ -2192,10 +2192,8 @@ def fake_download_and_extract_artifacts(url, work_dir): | |||||||
| ) | ) | ||||||
| def test_ci_help(subcmd, capsys): | def test_ci_help(subcmd, capsys): | ||||||
|     """Make sure `spack ci` --help describes the (sub)command help.""" |     """Make sure `spack ci` --help describes the (sub)command help.""" | ||||||
|     with pytest.raises(SystemExit): |     out = spack.main.SpackCommand("ci", subprocess=True)(subcmd, "--help") | ||||||
|         ci_cmd(subcmd, "--help") |  | ||||||
| 
 | 
 | ||||||
|     out = str(capsys.readouterr()) |  | ||||||
|     usage = "usage: spack ci {0}{1}[".format(subcmd, " " if subcmd else "") |     usage = "usage: spack ci {0}{1}[".format(subcmd, " " if subcmd else "") | ||||||
|     assert usage in out |     assert usage in out | ||||||
| 
 | 
 | ||||||
|   | |||||||
| @@ -16,7 +16,7 @@ | |||||||
| import spack.paths | import spack.paths | ||||||
| from spack.cmd.commands import _positional_to_subroutine | from spack.cmd.commands import _positional_to_subroutine | ||||||
| 
 | 
 | ||||||
| commands = spack.main.SpackCommand("commands") | commands = spack.main.SpackCommand("commands", subprocess=True) | ||||||
| 
 | 
 | ||||||
| parser = spack.main.make_argument_parser() | parser = spack.main.make_argument_parser() | ||||||
| spack.main.add_all_commands(parser) | spack.main.add_all_commands(parser) | ||||||
| @@ -104,17 +104,18 @@ def test_rst_with_input_files(tmpdir): | |||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def test_rst_with_header(tmpdir): | def test_rst_with_header(tmpdir): | ||||||
|  |     local_commands = spack.main.SpackCommand("commands") | ||||||
|     fake_header = "this is a header!\n\n" |     fake_header = "this is a header!\n\n" | ||||||
| 
 | 
 | ||||||
|     filename = tmpdir.join("header.txt") |     filename = tmpdir.join("header.txt") | ||||||
|     with filename.open("w") as f: |     with filename.open("w") as f: | ||||||
|         f.write(fake_header) |         f.write(fake_header) | ||||||
| 
 | 
 | ||||||
|     out = commands("--format=rst", "--header", str(filename)) |     out = local_commands("--format=rst", "--header", str(filename)) | ||||||
|     assert out.startswith(fake_header) |     assert out.startswith(fake_header) | ||||||
| 
 | 
 | ||||||
|     with pytest.raises(spack.main.SpackCommandError): |     with pytest.raises(spack.main.SpackCommandError): | ||||||
|         commands("--format=rst", "--header", "asdfjhkf") |         local_commands("--format=rst", "--header", "asdfjhkf") | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def test_rst_update(tmpdir): | def test_rst_update(tmpdir): | ||||||
| @@ -207,13 +208,14 @@ def test_update_completion_arg(tmpdir, monkeypatch): | |||||||
| 
 | 
 | ||||||
|     monkeypatch.setattr(spack.cmd.commands, "update_completion_args", mock_args) |     monkeypatch.setattr(spack.cmd.commands, "update_completion_args", mock_args) | ||||||
| 
 | 
 | ||||||
|  |     local_commands = spack.main.SpackCommand("commands") | ||||||
|     # ensure things fail if --update-completion isn't specified alone |     # ensure things fail if --update-completion isn't specified alone | ||||||
|     with pytest.raises(spack.main.SpackCommandError): |     with pytest.raises(spack.main.SpackCommandError): | ||||||
|         commands("--update-completion", "-a") |         local_commands("--update-completion", "-a") | ||||||
| 
 | 
 | ||||||
|     # ensure arg is restored |     # ensure arg is restored | ||||||
|     assert "--update-completion" not in mock_bashfile.read() |     assert "--update-completion" not in mock_bashfile.read() | ||||||
|     commands("--update-completion") |     local_commands("--update-completion") | ||||||
|     assert "--update-completion" in mock_bashfile.read() |     assert "--update-completion" in mock_bashfile.read() | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|   | |||||||
| @@ -869,7 +869,7 @@ def test_env_with_included_config_var_path(packages_file): | |||||||
| 
 | 
 | ||||||
|     config_real_path = substitute_path_variables(config_var_path) |     config_real_path = substitute_path_variables(config_var_path) | ||||||
|     fs.mkdirp(os.path.dirname(config_real_path)) |     fs.mkdirp(os.path.dirname(config_real_path)) | ||||||
|     fs.rename(packages_file.strpath, config_real_path) |     shutil.move(packages_file.strpath, config_real_path) | ||||||
|     assert os.path.exists(config_real_path) |     assert os.path.exists(config_real_path) | ||||||
| 
 | 
 | ||||||
|     with e: |     with e: | ||||||
|   | |||||||
| @@ -11,7 +11,7 @@ | |||||||
| @pytest.mark.xfail | @pytest.mark.xfail | ||||||
| def test_reuse_after_help(): | def test_reuse_after_help(): | ||||||
|     """Test `spack help` can be called twice with the same SpackCommand.""" |     """Test `spack help` can be called twice with the same SpackCommand.""" | ||||||
|     help_cmd = SpackCommand("help") |     help_cmd = SpackCommand("help", subprocess=True) | ||||||
|     help_cmd() |     help_cmd() | ||||||
| 
 | 
 | ||||||
|     # This second invocation will somehow fail because the parser no |     # This second invocation will somehow fail because the parser no | ||||||
| @@ -30,14 +30,14 @@ def test_reuse_after_help(): | |||||||
| 
 | 
 | ||||||
| def test_help(): | def test_help(): | ||||||
|     """Sanity check the help command to make sure it works.""" |     """Sanity check the help command to make sure it works.""" | ||||||
|     help_cmd = SpackCommand("help") |     help_cmd = SpackCommand("help", subprocess=True) | ||||||
|     out = help_cmd() |     out = help_cmd() | ||||||
|     assert "These are common spack commands:" in out |     assert "These are common spack commands:" in out | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def test_help_all(): | def test_help_all(): | ||||||
|     """Test the spack help --all flag""" |     """Test the spack help --all flag""" | ||||||
|     help_cmd = SpackCommand("help") |     help_cmd = SpackCommand("help", subprocess=True) | ||||||
|     out = help_cmd("--all") |     out = help_cmd("--all") | ||||||
|     assert "Complete list of spack commands:" in out |     assert "Complete list of spack commands:" in out | ||||||
| 
 | 
 | ||||||
|   | |||||||
| @@ -10,7 +10,7 @@ | |||||||
| 
 | 
 | ||||||
| import pytest | import pytest | ||||||
| 
 | 
 | ||||||
| from llnl.util.filesystem import mkdirp | from llnl.util.filesystem import mkdirp, working_dir | ||||||
| 
 | 
 | ||||||
| import spack | import spack | ||||||
| from spack.util.executable import which | from spack.util.executable import which | ||||||
| @@ -40,10 +40,11 @@ def check_git_version(): | |||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @pytest.fixture(scope="function") | @pytest.fixture(scope="function") | ||||||
| def git_tmp_worktree(tmpdir): | def git_tmp_worktree(tmpdir, mock_git_version_info): | ||||||
|     """Create new worktree in a temporary folder and monkeypatch |     """Create new worktree in a temporary folder and monkeypatch | ||||||
|     spack.paths.prefix to point to it. |     spack.paths.prefix to point to it. | ||||||
|     """ |     """ | ||||||
|  |     with working_dir(mock_git_version_info[0]): | ||||||
|         # TODO: This is fragile and should be high priority for |         # TODO: This is fragile and should be high priority for | ||||||
|         # follow up fixes. 27021 |         # follow up fixes. 27021 | ||||||
|         # Path length is occasionally too long on Windows |         # Path length is occasionally too long on Windows | ||||||
|   | |||||||
| @@ -210,7 +210,7 @@ def test_setdefault_command(mutable_database, mutable_config): | |||||||
|     for k in preferred, other_spec: |     for k in preferred, other_spec: | ||||||
|         assert os.path.exists(writers[k].layout.filename) |         assert os.path.exists(writers[k].layout.filename) | ||||||
|     assert os.path.exists(link_name) and os.path.islink(link_name) |     assert os.path.exists(link_name) and os.path.islink(link_name) | ||||||
|     assert os.path.realpath(link_name) == writers[other_spec].layout.filename |     assert os.path.realpath(link_name) == os.path.realpath(writers[other_spec].layout.filename) | ||||||
| 
 | 
 | ||||||
|     # Reset the default to be the preferred spec |     # Reset the default to be the preferred spec | ||||||
|     module("lmod", "setdefault", preferred) |     module("lmod", "setdefault", preferred) | ||||||
| @@ -219,4 +219,4 @@ def test_setdefault_command(mutable_database, mutable_config): | |||||||
|     for k in preferred, other_spec: |     for k in preferred, other_spec: | ||||||
|         assert os.path.exists(writers[k].layout.filename) |         assert os.path.exists(writers[k].layout.filename) | ||||||
|     assert os.path.exists(link_name) and os.path.islink(link_name) |     assert os.path.exists(link_name) and os.path.islink(link_name) | ||||||
|     assert os.path.realpath(link_name) == writers[preferred].layout.filename |     assert os.path.realpath(link_name) == os.path.realpath(writers[preferred].layout.filename) | ||||||
|   | |||||||
| @@ -46,22 +46,22 @@ def has_develop_branch(): | |||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @pytest.fixture(scope="function") | @pytest.fixture(scope="function") | ||||||
| def flake8_package(): | def flake8_package(tmpdir): | ||||||
|     """Style only checks files that have been modified. This fixture makes a small |     """Style only checks files that have been modified. This fixture makes a small | ||||||
|     change to the ``flake8`` mock package, yields the filename, then undoes the |     change to the ``flake8`` mock package, yields the filename, then undoes the | ||||||
|     change on cleanup. |     change on cleanup. | ||||||
|     """ |     """ | ||||||
|     repo = spack.repo.Repo(spack.paths.mock_packages_path) |     repo = spack.repo.Repo(spack.paths.mock_packages_path) | ||||||
|     filename = repo.filename_for_package_name("flake8") |     filename = repo.filename_for_package_name("flake8") | ||||||
|     tmp = filename + ".tmp" |     rel_path = os.path.dirname(os.path.relpath(filename, spack.paths.prefix)) | ||||||
|  |     tmp = tmpdir / rel_path / "flake8-ci-package.py" | ||||||
|  |     tmp.ensure() | ||||||
|  |     tmp = str(tmp) | ||||||
| 
 | 
 | ||||||
|     try: |  | ||||||
|     shutil.copy(filename, tmp) |     shutil.copy(filename, tmp) | ||||||
|         package = FileFilter(filename) |     package = FileFilter(tmp) | ||||||
|     package.filter("state = 'unmodified'", "state = 'modified'", string=True) |     package.filter("state = 'unmodified'", "state = 'modified'", string=True) | ||||||
|         yield filename |     yield tmp | ||||||
|     finally: |  | ||||||
|         shutil.move(tmp, filename) |  | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @pytest.fixture | @pytest.fixture | ||||||
| @@ -71,9 +71,8 @@ def flake8_package_with_errors(scope="function"): | |||||||
|     filename = repo.filename_for_package_name("flake8") |     filename = repo.filename_for_package_name("flake8") | ||||||
|     tmp = filename + ".tmp" |     tmp = filename + ".tmp" | ||||||
| 
 | 
 | ||||||
|     try: |  | ||||||
|     shutil.copy(filename, tmp) |     shutil.copy(filename, tmp) | ||||||
|         package = FileFilter(filename) |     package = FileFilter(tmp) | ||||||
| 
 | 
 | ||||||
|     # this is a black error (quote style and spacing before/after operator) |     # this is a black error (quote style and spacing before/after operator) | ||||||
|     package.filter('state = "unmodified"', "state    =    'modified'", string=True) |     package.filter('state = "unmodified"', "state    =    'modified'", string=True) | ||||||
| @@ -82,9 +81,7 @@ def flake8_package_with_errors(scope="function"): | |||||||
|     package.filter( |     package.filter( | ||||||
|         "from spack.package import *", "from spack.package import *\nimport os", string=True |         "from spack.package import *", "from spack.package import *\nimport os", string=True | ||||||
|     ) |     ) | ||||||
|         yield filename |     yield tmp | ||||||
|     finally: |  | ||||||
|         shutil.move(tmp, filename) |  | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def test_changed_files_from_git_rev_base(tmpdir, capfd): | def test_changed_files_from_git_rev_base(tmpdir, capfd): | ||||||
| @@ -125,7 +122,7 @@ def test_changed_no_base(tmpdir, capfd): | |||||||
|         assert "This repository does not have a 'foobar'" in err |         assert "This repository does not have a 'foobar'" in err | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def test_changed_files_all_files(flake8_package): | def test_changed_files_all_files(): | ||||||
|     # it's hard to guarantee "all files", so do some sanity checks. |     # it's hard to guarantee "all files", so do some sanity checks. | ||||||
|     files = set( |     files = set( | ||||||
|         [ |         [ | ||||||
| @@ -139,13 +136,18 @@ def test_changed_files_all_files(flake8_package): | |||||||
| 
 | 
 | ||||||
|     # a builtin package |     # a builtin package | ||||||
|     zlib = spack.repo.path.get_pkg_class("zlib") |     zlib = spack.repo.path.get_pkg_class("zlib") | ||||||
|     assert zlib.module.__file__ in files |     zlib_file = zlib.module.__file__ | ||||||
|  |     if zlib_file.endswith("pyc"): | ||||||
|  |         zlib_file = zlib_file[:-1] | ||||||
|  |     assert zlib_file in files | ||||||
| 
 | 
 | ||||||
|     # a core spack file |     # a core spack file | ||||||
|     assert os.path.join(spack.paths.module_path, "spec.py") in files |     assert os.path.join(spack.paths.module_path, "spec.py") in files | ||||||
| 
 | 
 | ||||||
|     # a mock package |     # a mock package | ||||||
|     assert flake8_package in files |     repo = spack.repo.Repo(spack.paths.mock_packages_path) | ||||||
|  |     filename = repo.filename_for_package_name("flake8") | ||||||
|  |     assert filename in files | ||||||
| 
 | 
 | ||||||
|     # this test |     # this test | ||||||
|     assert __file__ in files |     assert __file__ in files | ||||||
|   | |||||||
| @@ -228,11 +228,14 @@ def test_missing_command(): | |||||||
|     ], |     ], | ||||||
|     ids=["no_stem", "vacuous", "leading_hyphen", "basic_good", "trailing_slash", "hyphenated"], |     ids=["no_stem", "vacuous", "leading_hyphen", "basic_good", "trailing_slash", "hyphenated"], | ||||||
| ) | ) | ||||||
| def test_extension_naming(extension_path, expected_exception, config): | def test_extension_naming(tmpdir, extension_path, expected_exception, config): | ||||||
|     """Ensure that we are correctly validating configured extension paths |     """Ensure that we are correctly validating configured extension paths | ||||||
|     for conformity with the rules: the basename should match |     for conformity with the rules: the basename should match | ||||||
|     ``spack-<name>``; <name> may have embedded hyphens but not begin with one. |     ``spack-<name>``; <name> may have embedded hyphens but not begin with one. | ||||||
|     """ |     """ | ||||||
|  |     # NOTE: if the directory is a valid extension directory name the "vacuous" test will | ||||||
|  |     # fail because it resolves to current working directory | ||||||
|  |     with tmpdir.as_cwd(): | ||||||
|         with spack.config.override("config:extensions", [extension_path]): |         with spack.config.override("config:extensions", [extension_path]): | ||||||
|             with pytest.raises(expected_exception): |             with pytest.raises(expected_exception): | ||||||
|                 spack.cmd.get_module("no-such-command") |                 spack.cmd.get_module("no-such-command") | ||||||
|   | |||||||
| @@ -10,6 +10,7 @@ | |||||||
| 
 | 
 | ||||||
| """ | """ | ||||||
| import code | import code | ||||||
|  | import io | ||||||
| import os | import os | ||||||
| import pdb | import pdb | ||||||
| import signal | import signal | ||||||
| @@ -53,7 +54,10 @@ class and use as a drop in for Pdb, although the syntax here is slightly differe | |||||||
|     the run of Spack.install, or any where else Spack spawns a child process. |     the run of Spack.install, or any where else Spack spawns a child process. | ||||||
|     """ |     """ | ||||||
| 
 | 
 | ||||||
|  |     try: | ||||||
|         _original_stdin_fd = sys.stdin.fileno() |         _original_stdin_fd = sys.stdin.fileno() | ||||||
|  |     except io.UnsupportedOperation: | ||||||
|  |         _original_stdin_fd = None | ||||||
|     _original_stdin = None |     _original_stdin = None | ||||||
| 
 | 
 | ||||||
|     def __init__(self, stdout_fd=None, stderr_fd=None): |     def __init__(self, stdout_fd=None, stderr_fd=None): | ||||||
|   | |||||||
| @@ -54,6 +54,11 @@ elif [[ "$SPACK_TEST_SOLVER" == "original" ]]; then | |||||||
|   export PYTEST_ADDOPTS='-m "not maybeslow"' |   export PYTEST_ADDOPTS='-m "not maybeslow"' | ||||||
| fi | fi | ||||||
|  |  | ||||||
|  | # Check if xdist is available | ||||||
|  | if python -m pytest --trace-config 2>&1 | grep xdist; then | ||||||
|  |   export PYTEST_ADDOPTS="$PYTEST_ADDOPTS --dist loadfile --tx '${SPACK_TEST_PARALLEL:=3}*popen//python=./bin/spack-tmpconfig python -u ./bin/spack python'" | ||||||
|  | fi | ||||||
|  |  | ||||||
| $coverage_run $(which spack) unit-test -x --verbose | $coverage_run $(which spack) unit-test -x --verbose | ||||||
|  |  | ||||||
| bash "$QA_DIR/test-env-cfg.sh" | bash "$QA_DIR/test-env-cfg.sh" | ||||||
|   | |||||||
| @@ -1568,7 +1568,7 @@ _spack_pydoc() { | |||||||
| _spack_python() { | _spack_python() { | ||||||
|     if $list_options |     if $list_options | ||||||
|     then |     then | ||||||
|         SPACK_COMPREPLY="-h --help -V --version -c -i -m --path" |         SPACK_COMPREPLY="-h --help -V --version -c -u -i -m --path" | ||||||
|     else |     else | ||||||
|         SPACK_COMPREPLY="" |         SPACK_COMPREPLY="" | ||||||
|     fi |     fi | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 Tom Scogland
					Tom Scogland