From 18060dd02a1092938d187a429d44dbeacaab0d6d Mon Sep 17 00:00:00 2001 From: Marti Bolivar Date: Fri, 4 Jan 2019 14:41:24 -0700 Subject: [PATCH] tests: use tox and overhaul project testing To properly test the project commands, it would be best to have a fresh west bootstrapper package created and installed on PATH, so it could be used to run commands exactly as they'd happen if we package and ship the working tree. To make that easier, add a dependency on tox and use it for testing: https://tox.readthedocs.io/en/latest/ From now on, we'll test west by running 'tox' from the repository root. This has several advantages over running pytest directly: - "Just run tox": there are no longer any differences in test invocation between POSIX OSes and Windows. - tox creates an sdist package of the current tree using our setup.py and installs it into a new virtual environment, then runs tests there. This removes interference from other packages installed on the host (like released bootstrappers that are also installed) - we get to run multiple shell commands in order, should that ever be needed, in our test procedures in a way that won't affect users With that done, we can re-work the multirepo command testing to invoke the bootstrapper in the virtual environment, adding various tests and filling in longstanding testing gaps by adding increased checking of the results (currently, much of the testing just checks whether commands do or do not error out, which isn't enough). These changes were made with a view towards the upcoming changes which are planned before releasing west "into the wild": the test case code should be mostly the same before and after the changes, so this serves as a good baseline against regressions introduced by those upcoming changes. Signed-off-by: Marti Bolivar [wip] debugging shippable results Signed-off-by: Marti Bolivar [wip] just test one py3 shutil.which west is picking up a 3.4 version in the 3.6 test, oddly Signed-off-by: Marti Bolivar --- .gitignore | 1 + .shippable.yml | 7 +- README.rst | 20 +- tests/west/project/test_project.py | 990 +++++++++++++++++-------- tox.ini | 16 + tests_requirements.txt => tox_deps.txt | 0 6 files changed, 706 insertions(+), 328 deletions(-) create mode 100644 tox.ini rename tests_requirements.txt => tox_deps.txt (100%) diff --git a/.gitignore b/.gitignore index 1a0a59a..fc7e785 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ __pycache__/ .pytest_cache/ .eggs/ shippable/ +.tox/ diff --git a/.shippable.yml b/.shippable.yml index ef84ffb..114f5cc 100644 --- a/.shippable.yml +++ b/.shippable.yml @@ -9,9 +9,6 @@ python: build: ci: - - python setup.py bdist_wheel - - pip install dist/west*.whl - - pip install -r tests_requirements.txt + - pip install tox - mkdir -p shippable/{testresults,codecoverage} - - PYTHONPATH=src pytest --junitxml=shippable/testresults/nosetests.xml tests - - PYTHONPATH=src pytest --cov=west --cov-report=xml:shippable/codecoverage/coverage.xml tests + - tox -- --junitxml=$PWD/shippable/testresults/nosetests.xml --cov=west --cov-report=xml:$PWD/shippable/codecoverage/coverage.xml tests diff --git a/README.rst b/README.rst index b09f63c..6312d72 100644 --- a/README.rst +++ b/README.rst @@ -16,7 +16,7 @@ a directory of your choosing:: mkdir zephyrproject && cd zephyrproject west init - west fetch + west clone What just happened: @@ -27,9 +27,9 @@ What just happened: one supported by the bootstrapper itself; all other commands are implemented in the west source repository it clones. -- ``west fetch`` clones the repositories in the manifest, creating +- ``west clone`` clones the repositories in the manifest, creating working trees in the installation directory. In this case, the - bootstrapper notices the command (``fetch``) is not ``init``, and + bootstrapper notices the command (``clone``) is not ``init``, and delegates handling to the "main" west implementation in the source repository it cloned in the previous step. @@ -63,17 +63,19 @@ command with ``west -h``. For example:: Test Suite ---------- -To run the test suite, run this from the west repository:: +Before running tests, install tox:: - pip3 install -r tests_requirements.txt + # macOS, Windows + pip3 install tox -Then, in a Bash shell:: + # Linux + pip3 install --user tox - PYTHONPATH=src py.test +Then, to run the test suite locally:: -On Windows:: + tox - cmd /C "set PYTHONPATH=/path/to/west/src && py.test" +See the tox configuration file, tox.ini, for more details. Hacking on West --------------- diff --git a/tests/west/project/test_project.py b/tests/west/project/test_project.py index 1d57cd7..269f76b 100644 --- a/tests/west/project/test_project.py +++ b/tests/west/project/test_project.py @@ -1,106 +1,559 @@ -import argparse import os +from os.path import dirname import shlex import shutil import subprocess import sys +import textwrap +from unittest.mock import patch import pytest -from west import config -from west.commands import project -import west._bootstrap.main as bootstrap - -# Where the projects are cloned to -NET_TOOLS_PATH = 'net-tools' -KCONFIGLIB_PATH = 'sub/Kconfiglib' +import west._bootstrap.main GIT = shutil.which('git') - -COMMAND_OBJECTS = ( - project.List(), - project.Fetch(), - project.Pull(), - project.Rebase(), - project.Branch(), - project.Checkout(), - project.Diff(), - project.Status(), - project.Update(), - project.ForAll(), -) +# Assumes this file is west/tests/west/project/test_project.py, returns +# path to toplevel 'west' +THIS_WEST = os.path.abspath(dirname(dirname(dirname(dirname(__file__))))) -def cmd(cmd): - # We assume the manifest is in manifest.yml when tests are run. - cmd += ' -m manifest.yml' +# +# Test fixtures +# - # cmd() takes the command as a string, which is less clunky to work with. - # Split it according to shell rules. - split_cmd = shlex.split(cmd) - command_name = split_cmd[0] +@pytest.fixture +def repos_tmpdir(tmpdir): + '''Fixture for tmpdir with "remote" repositories, manifest, and west. - for command_object in COMMAND_OBJECTS: - # Find the WestCommand object that implements the command - if command_object.name == command_name: - # Use it to parse the arguments - parser = argparse.ArgumentParser() - command_object.do_add_parser(parser.add_subparsers()) + These can then be used to bootstrap an installation and run + project-related commands on it with predictable results. - # Pass the parsed arguments and unknown arguments to run it - command_object.do_run(*parser.parse_known_args(split_cmd)) - break - else: - assert False, "unknown command " + command_name + Switches directory to, and returns, the top level tmpdir -- NOT + the subdirectory containing the repositories themselves. + + Initializes placeholder upstream repositories in /remote-repos/ + with the following contents: + + repos/ + ├── west (branch: master) + │ └── (contains this west's worktree contents) + ├── manifest (branch: master) + │ └── default.yml + ├── Kconfiglib (branch: zephyr) + │ └── kconfiglib.py + ├── net-tools (branch: master) + │ └── qemu-script.sh + └── zephyr (branch: master) + ├── CODEOWNERS + ├── include + │ └── header.h + └── subsys + └── bluetooth + └── code.c + + The contents of default.yml are: + + west: + url: file:///west + manifest: + defaults: + remote: test-local + remotes: + - name: test-local + url-base: file:///remote-repos + projects: + - name: Kconfiglib + revision: zephyr + path: subdir/Kconfiglib + - name: net-tools + clone_depth: 1 + - name: zephyr + + ''' + rr = tmpdir.mkdir('repos') # "remote" repositories + rp = {} # individual repository paths under rr + + # Mirror this west tree into a "remote" west repository under rr. + wdst = rr.join('west') + mirror_west_repo(wdst) + rp['west'] = str(wdst) + + # Create the other repositories. + for repo in 'manifest', 'net-tools', 'Kconfiglib', 'zephyr': + path = str(rr.join(repo)) + rp[repo] = path + create_repo(path) + + # Initialize the manifest repository. + add_commit(rp['manifest'], 'test manifest', + files={'default.yml': textwrap.dedent('''\ + west: + url: file://{west} + manifest: + defaults: + remote: test-local + + remotes: + - name: test-local + url-base: file://{rr} + + projects: + - name: Kconfiglib + revision: zephyr + path: subdir/Kconfiglib + - name: net-tools + - name: zephyr + '''.format(west=rp['west'], rr=str(rr)))}) + + # Initialize the Kconfiglib repository. + subprocess.check_call([GIT, 'checkout', '-b', 'zephyr'], + cwd=rp['Kconfiglib']) + add_commit(rp['Kconfiglib'], 'test kconfiglib commit', + files={'kconfiglib.py': 'print("hello world kconfiglib")\n'}) + + # Initialize the net-tools repository. + add_commit(rp['net-tools'], 'test net-tools commit', + files={'qemu-script.sh': 'echo hello world net-tools\n'}) + + # Initialize the zephyr repository. + add_commit(rp['zephyr'], 'test zephyr commit', + files={'CODEOWNERS': '', + 'include/header.h': '#pragma once\n', + 'subsys/bluetooth/code.c': 'void foo(void) {}\n'}) + + # Switch to and return the top-level temporary directory. + # + # This can be used to populate a west installation alongside. + tmpdir.chdir() + return tmpdir @pytest.fixture -def clean_west_topdir(tmpdir): - # Initialize some placeholder upstream repositories, in remote-repos/ - remote_repos_dir = tmpdir.mkdir('remote-repos') - for project in 'net-tools', 'Kconfiglib', 'manifest', 'west': - path = str(remote_repos_dir.join(project)) - create_repo(path) - add_commit(path, 'initial') - if project == 'Kconfiglib': - subprocess.check_call([GIT, 'branch', 'zephyr'], cwd=path) +def west_init_tmpdir(repos_tmpdir): + '''Fixture for a tmpdir with 'remote' repositories and 'west init' run. - # Create west/.west_topdir, to mark this directory as a West installation, - # and a manifest.yml pointing to the repositories we created above - tmpdir.join('west', '.west_topdir').ensure() - tmpdir.join('manifest.yml').write(''' -manifest: - defaults: - remote: repos - revision: master + Uses the remote repositories from the repos_tmpdir fixture to + create a west installation using the system bootstrapper's init + command -- and thus the test environment must install the + bootstrapper from the current west source code tree under test. - remotes: - - name: repos - url-base: file://{} + The contents of the west installation aren't checked at all. + This is left up to the test cases. - projects: - - name: net-tools - - name: Kconfiglib - revision: zephyr - path: sub/Kconfiglib -'''.format(remote_repos_dir)) + The directory that 'west init' created is returned as a + py.path.local, with the current working directory set there.''' + west_tmpdir = repos_tmpdir.join('west_installation') + cmd('init -m "{}" "{}"'.format(str(repos_tmpdir.join('repos', 'manifest')), + str(west_tmpdir))) + west_tmpdir.chdir() + return west_tmpdir - # Switch to the top-level West installation directory - tmpdir.chdir() - return tmpdir +@pytest.fixture +def west_clone_tmpdir(west_init_tmpdir): + '''Like west_init_tmpdir, but also runs west clone.''' + cmd('clone', cwd=str(west_init_tmpdir)) + return west_init_tmpdir +# +# Test cases +# + +def test_installation(west_clone_tmpdir): + # Basic test that west_clone_tmpdir bootstrapped correctly. This + # is a basic test of west init and west clone. + + # Make sure the expected files and directories exist in the right + # places. + wct = west_clone_tmpdir + assert wct.check(dir=1) + assert wct.join('subdir', 'Kconfiglib').check(dir=1) + assert wct.join('subdir', 'Kconfiglib', '.git').check(dir=1) + assert wct.join('subdir', 'Kconfiglib', 'kconfiglib.py').check(file=1) + assert wct.join('net-tools').check(dir=1) + assert wct.join('net-tools', '.git').check(dir=1) + assert wct.join('net-tools', 'qemu-script.sh').check(file=1) + assert wct.join('zephyr').check(dir=1) + assert wct.join('zephyr', '.git').check(dir=1) + assert wct.join('zephyr', 'CODEOWNERS').check(file=1) + assert wct.join('zephyr', 'include', 'header.h').check(file=1) + assert wct.join('zephyr', 'subsys', 'bluetooth', 'code.c').check(file=1) + + +def test_list(west_clone_tmpdir): + # Projects shall be listed in the order they appear in the manifest. + # Check the behavior for some format arguments of interest as well. + actual = cmd('list -f "{name} {revision} {path} {cloned} {clone_depth}"') + expected = ['Kconfiglib zephyr {} (cloned) None'.format( + os.path.join('subdir', 'Kconfiglib')), + 'net-tools master net-tools (cloned) None', + 'zephyr master zephyr (cloned) None'] + assert actual.splitlines() == expected + + +def test_fetch_nonexistent(west_clone_tmpdir): + # Fetch a non-existent project. This should fail. + + with pytest.raises(subprocess.CalledProcessError): + cmd('fetch non-existent') + + +def test_fetch_one(west_clone_tmpdir): + # Run update_helper() with intermediate command 'west fetch net-tools'. + # + # Verify the following: + # + # - local net-tools manifest-rev changes + # - local net-tools HEAD does not change + # - local kconfiglib manifest-rev and HEAD don't change + + (nt_mr_0, nt_mr_1, + nt_head_0, nt_head_1, + kl_mr_0, kl_mr_1, + kl_head_0, kl_head_1) = update_helper(west_clone_tmpdir, + 'fetch net-tools') + + assert nt_mr_0 != nt_mr_1, 'failed to update net-tools manifest-rev' + assert nt_head_0 == nt_head_1, 'undesired change to net-tools HEAD' + assert kl_mr_0 == kl_mr_1, 'undesired change to kconfiglib manifest-rev' + assert kl_head_0 == kl_head_1, 'undesired change to kconfiglib HEAD' + + +def test_fetch_all(west_clone_tmpdir): + # Run update_helper() with intermediate command 'west fetch'. + # + # Verify the following: + # + # - local net-tools manifest-rev changes + # - local net-tools HEAD does not change + # - local kconfiglib manifest-rev changes + # - local kconfiglib HEAD does not change + + (nt_mr_0, nt_mr_1, + nt_head_0, nt_head_1, + kl_mr_0, kl_mr_1, + kl_head_0, kl_head_1) = update_helper(west_clone_tmpdir, 'fetch') + + assert nt_mr_0 != nt_mr_1, 'failed to update net-tools manifest-rev' + assert nt_head_0 == nt_head_1, 'undesired change to net-tools HEAD' + assert kl_mr_0 != kl_mr_1, 'failed to update kconfiglib manifest-rev' + assert kl_head_0 == kl_head_1, 'undesired change to kconfiglib HEAD' + + +def test_fetch_one_init(west_init_tmpdir): + # 'west fetch' can be used to clone a project from a directory + # which only has had 'west init' run in it. The resulting + # installation should know about all available projects, but only + # have cloned the one that was explicitly fetched. + cmd('fetch net-tools') + actual = cmd('list -f "{name} {cloned}"') + expected = ['Kconfiglib (not cloned)', + 'net-tools (cloned)', + 'zephyr (not cloned)'] + assert actual.splitlines() == expected + + +def test_fetch_all_init(west_init_tmpdir): + # Similarly, 'west fetch' can be used to clone all the projects + # from a directory which has only had init run on it. + cmd('fetch') + actual = cmd('list -f "{name} {cloned}"') + expected = ['Kconfiglib (cloned)', + 'net-tools (cloned)', + 'zephyr (cloned)'] + assert actual.splitlines() == expected + + +def test_pull_nonexistent(west_clone_tmpdir): + # Pull a non-existent project. This should fail. + + with pytest.raises(subprocess.CalledProcessError): + cmd('pull non-existent') + + +def test_pull_one(west_clone_tmpdir): + # Run update_helper() with intermediate command 'west pull net-tools'. + # + # Verify the following: + # + # - local net-tools manifest-rev and HEAD change + # - local kconfiglib manifest-rev and HEAD don't change + + (nt_mr_0, nt_mr_1, + nt_head_0, nt_head_1, + kl_mr_0, kl_mr_1, + kl_head_0, kl_head_1) = update_helper(west_clone_tmpdir, 'pull net-tools') + + assert nt_mr_0 != nt_mr_1, 'failed to update net-tools manifest-rev' + assert nt_head_0 != nt_head_1, 'failed to update net-tools HEAD' + assert kl_mr_0 == kl_mr_1, 'undesired change to kconfiglib manifest-rev' + assert kl_head_0 == kl_head_1, 'undesired change to kconfiglib HEAD' + + +def test_pull_all(west_clone_tmpdir): + # Run update_helper() with intermediate command 'west pull'. + # + # Verify the following: + # + # - local net-tools manifest-rev and HEAD change + # - local kconfiglib manifest-rev and HEAD change + + (nt_mr_0, nt_mr_1, + nt_head_0, nt_head_1, + kl_mr_0, kl_mr_1, + kl_head_0, kl_head_1) = update_helper(west_clone_tmpdir, 'pull') + + assert nt_mr_0 != nt_mr_1, 'failed to update net-tools manifest-rev' + assert nt_head_0 != nt_head_1, 'failed to update net-tools HEAD' + assert kl_mr_0 != kl_mr_1, 'failed to update kconfiglib manifest-rev' + assert kl_head_0 != kl_head_1, 'failed to update kconfiglib HEAD' + + +def test_pull_one_init(west_init_tmpdir): + # 'west pull' can be used to clone a project from a directory + # which only has had 'west init' run in it. The resulting + # installation should know about all available projects, but only + # have cloned the one that was explicitly pulled. + cmd('pull net-tools') + actual = cmd('list -f "{name} {cloned}"') + expected = ['Kconfiglib (not cloned)', + 'net-tools (cloned)', + 'zephyr (not cloned)'] + assert actual.splitlines() == expected + + +def test_pull_all_init(west_init_tmpdir): + # Similarly, 'west pull' can be used to clone all the projects + # from a directory which has only had init run on it. + cmd('pull') + actual = cmd('list -f "{name} {cloned}"') + expected = ['Kconfiglib (cloned)', + 'net-tools (cloned)', + 'zephyr (cloned)'] + assert actual.splitlines() == expected + + +def test_rebase(west_init_tmpdir): + # Basic check that rebase commands either don't or do return + # errors in expected cases. + # + # FIXME: these need to test the actual rebase itself, by adding + # local and remote commits before rebasing, then checking that + # local commits actually are rebased onto the new remote ones. + + # Clone just one project + cmd('clone net-tools') + + # Piggyback a check that just that project got cloned + assert west_init_tmpdir.join('subdir', 'Kconfiglib').check(exists=0) + + # Rebase the project (non-cloned project should be silently skipped) + cmd('rebase') + + # Rebase the project again, naming it explicitly + cmd('rebase net-tools') + + # Try rebasing a project that hasn't been cloned + with pytest.raises(subprocess.CalledProcessError): + cmd('rebase Kconfiglib') + + # Clone the other project + cmd('pull Kconfiglib') + + # It can be rebased now. + cmd('rebase Kconfiglib') + + +def test_branches(west_init_tmpdir): + # Basic check that rebase commands either don't or do return + # errors in expected cases. + # + # FIXME: these need to verify the branches are actually checked + # out or not as expected. Output of each command should be checked + # as needed for correctness also. + + # Missing branch name + with pytest.raises(subprocess.CalledProcessError): + cmd('checkout') + + # Clone just one project + cmd('clone net-tools') + + # Branch foo does not exist and -b is not given + with pytest.raises(subprocess.CalledProcessError): + cmd('checkout foo') + + # Branch bar does not exist but -b is given + cmd('checkout -b bar net-tools') + + # Create and check out foo branch, with and without naming the + # project. + cmd('branch foo') + cmd('checkout foo') + cmd('checkout foo net-tools') + + # Kconfiglib isn't cloned yet, so branches can't be checked out + with pytest.raises(subprocess.CalledProcessError): + cmd('checkout foo Kconfiglib') + + # Clone the other project and retry: it still doesn't have the branch + cmd('fetch --no-update Kconfiglib') + with pytest.raises(subprocess.CalledProcessError): + cmd('checkout foo Kconfiglib') + + # Create a differently-named branch + cmd('branch baz Kconfiglib') + + # That branch shouldn't exist in the other project + with pytest.raises(subprocess.CalledProcessError): + cmd('checkout baz net-tools') + + # It should be possible to check out each branch even though they only + # exists in one project + cmd('checkout foo') + cmd('checkout bar') + + # List all branches and the projects they appear in + cmd('branch') + + +def test_diff(west_init_tmpdir): + # FIXME: Check output + + # Diff with no projects cloned shouldn't fail + + cmd('diff') + + # Neither should it fail after fetching one or both projects + + cmd('clone net-tools') + cmd('diff') + + cmd('clone Kconfiglib') + cmd('diff --cached') # Pass a custom flag too + + +def test_status(west_init_tmpdir): + # FIXME: Check output + + # Status with no projects cloned shouldn't fail + + cmd('status') + + # Neither should it fail after fetching one or both projects + + cmd('clone net-tools') + cmd('status') + + cmd('clone Kconfiglib') + cmd('status --long') # Pass a custom flag too + + +def test_forall(west_init_tmpdir): + # FIXME: Check output + # The 'echo' command is available in both 'shell' and 'batch' + + # 'forall' with no projects cloned shouldn't fail + + cmd("forall -c 'echo *'") + + # Neither should it fail after cloning one or both projects + + cmd('clone net-tools') + cmd("forall -c 'echo *'") + + cmd('clone Kconfiglib') + cmd("forall -c 'echo *'") + + +def test_update(west_init_tmpdir): + # Test the 'west update' command. It calls through to the same backend + # functions that are used for automatic updates and 'west init' + # reinitialization. + + # Clone the net-tools repository + cmd('clone net-tools') + + net_tools_prev = head_subject('net-tools') + west_prev = head_subject('west/west') + manifest_prev = head_subject('west/manifest') + + # Add commits to the local repos. We need to reconfigure + # explicitly as these are clones, and west doesn't handle that for + # us. + for path in 'west/manifest', 'west/west', 'net-tools': + add_commit(path, 'test-update-local', reconfigure=True) + + # Check that resetting the manifest repository removes the local commit + cmd('update --reset-manifest') + assert head_subject('west/manifest') == manifest_prev + assert head_subject('west/west') == 'test-update-local' # Unaffected + assert head_subject('net-tools') == 'test-update-local' # Unaffected + + # Check that resetting the west repository removes the local commit + cmd('update --reset-west') + assert head_subject('west/west') == west_prev + assert head_subject('net-tools') == 'test-update-local' # Unaffected + + # Check that resetting projects removes the local commit + cmd('update --reset-projects') + assert head_subject('net-tools') == net_tools_prev + + # Add commits to the upstream special repos + remotes = west_init_tmpdir.join('..', 'repos') + for r in remotes.join('manifest'), remotes.join('west'): + add_commit(str(r), 'test-update-upstream') + + # Check that updating the manifest repository gets the upstream commit + cmd('update --update-manifest') + assert head_subject('west/manifest') == 'test-update-upstream' + assert head_subject('west/west') == west_prev # Unaffected + + +def test_init_again(west_init_tmpdir): + # Test that 'west init' on an initialized tmpdir errors out + + with pytest.raises(subprocess.CalledProcessError): + cmd('init') + + +@patch('west._bootstrap.main.wrap') +def test_reinit(west_init_tmpdir): + # Basic test of how reinit works in the bootstrapper. + # + # FIXME: actually verify the intended reinit operation, e.g. by + # changing remote manifest branch, reiniting, and checking the + # local repositories. + + # Test that the bootstrap script reinits with the expected + # --reset-* flags. + wrap = west._bootstrap.main.wrap + for init_args, wrap_args in ( + (['-m', 'foo'], ['update', '--reset-manifest', '--reset-projects', + '--reset-west']), + (['--mr', 'foo'], ['update', '--reset-manifest', '--reset-projects', + '--reset-west'])): + + west._bootstrap.main.init(init_args) + assert wrap.called_once_with(*wrap_args) + wrap.reset_mock() + + +# +# Helper functions used by the test cases and fixtures. +# + def create_repo(path): # Initializes a Git repository in 'path', and adds an initial commit to it subprocess.check_call([GIT, 'init', path]) + + config_repo(path) add_commit(path, 'initial') -def add_commit(path, msg): - # Adds an empty commit with message 'msg' to the repo in 'path' - +def config_repo(path): # Set name and email. This avoids a "Please tell me who you are" error when # there's no global default. subprocess.check_call([GIT, 'config', 'user.name', 'West Test'], cwd=path) @@ -108,6 +561,33 @@ def add_commit(path, msg): 'west-test@example.com'], cwd=path) + +def add_commit(repo, msg, files=None, reconfigure=True): + # Adds a commit with message 'msg' to the repo in 'repo' + # + # If 'files' is given, it must be a dictionary mapping files to + # edit to the contents they should contain in the new + # commit. Otherwise, the commit will be empty. + # + # If 'reconfigure' is True, the user.name and user.email git + # configuration variables will be set in 'repo' using config_repo(). + + if reconfigure: + config_repo(repo) + + # Edit any files as specified by the user and add them to the index. + if files: + for path, contents in files.items(): + dirname, basename = os.path.dirname(path), os.path.basename(path) + fulldir = os.path.join(repo, dirname) + if not os.path.isdir(fulldir): + # Allow any errors (like trying to create a directory + # where a file already exists) to propagate up. + os.makedirs(fulldir) + with open(os.path.join(fulldir, basename), 'w') as f: + f.write(contents) + subprocess.check_call([GIT, 'add', path], cwd=repo) + # The extra '--no-xxx' flags are for convenience when testing # on developer workstations, which may have global git # configuration to sign commits, etc. @@ -116,229 +596,92 @@ def add_commit(path, msg): # intervention or fail in environments where Git isn't # configured. subprocess.check_call( - [GIT, 'commit', '--allow-empty', '-m', msg, '--no-verify', - '--no-gpg-sign', '--no-post-rewrite'], cwd=path) - - -def test_list(clean_west_topdir): - # TODO: Check output - cmd('list') - - -def test_fetch(clean_west_topdir): - # Clone all projects - cmd('fetch --no-update') - - # Check that they got cloned - assert os.path.isdir(NET_TOOLS_PATH) - assert os.path.isdir(KCONFIGLIB_PATH) - - # Non-existent project - with pytest.raises(SystemExit): - cmd('fetch --no-update non-existent') - - # Update a specific project - cmd('fetch --no-update net-tools') - - -def test_pull(clean_west_topdir): - # Clone all projects - cmd('pull --no-update') - - # Check that they got cloned - assert os.path.isdir(NET_TOOLS_PATH) - assert os.path.isdir(KCONFIGLIB_PATH) - - # Non-existent project - with pytest.raises(SystemExit): - cmd('pull --no-update non-existent') - - # Update a specific project - cmd('pull --no-update net-tools') - - -def test_rebase(clean_west_topdir): - # Clone just one project - cmd('fetch --no-update net-tools') - - # Piggyback a check that just that project got cloned - assert not os.path.exists(KCONFIGLIB_PATH) - - # Rebase the project (non-cloned project should be silently skipped) - cmd('rebase') - - # Rebase the project again, naming it explicitly - cmd('rebase net-tools') - - # Try rebasing a project that hasn't been cloned - with pytest.raises(SystemExit): - cmd('pull --no-update rebase Kconfiglib') - - # Clone the other project - cmd('pull --no-update Kconfiglib') - - # Will rebase both projects now - cmd('rebase') - - -def test_branches(clean_west_topdir): - # Missing branch name - with pytest.raises(SystemExit): - cmd('checkout') - - - # Clone just one project - cmd('fetch --no-update net-tools') - - # Create a branch in the cloned project - cmd('branch foo') - - # Check out the branch - cmd('checkout foo') - - # Check out the branch again, naming the project explicitly - cmd('checkout foo net-tools') - - # Try checking out a branch that doesn't exist in any project - with pytest.raises(SystemExit): - cmd('checkout nonexistent') - - # Try checking out a branch in a non-cloned project - with pytest.raises(SystemExit): - cmd('checkout foo Kconfiglib') - - # Clone the other project - cmd('fetch --no-update Kconfiglib') - - # It still doesn't have the branch - with pytest.raises(SystemExit): - cmd('checkout foo Kconfiglib') - - # Create a differently-named branch it - cmd('branch bar Kconfiglib') - - # That branch shouldn't exist in the other project - with pytest.raises(SystemExit): - cmd('checkout bar net-tools') - - # It should be possible to check out each branch even though they only - # exists in one project - cmd('checkout foo') - cmd('checkout bar') - - # List all branches and the projects they appear in (TODO: Check output) - cmd('branch') - - -def test_diff(clean_west_topdir): - # TODO: Check output - - # Diff with no projects cloned shouldn't fail - - cmd('diff') - - # Neither should it fail after fetching one or both projects - - cmd('fetch --no-update net-tools') - cmd('diff') - - cmd('fetch --no-update Kconfiglib') - cmd('diff --cached') # Pass a custom flag too - - -def test_status(clean_west_topdir): - # TODO: Check output - - # Status with no projects cloned shouldn't fail - - cmd('status') - - # Neither should it fail after fetching one or both projects - - cmd('fetch --no-update net-tools') - cmd('status') - - cmd('fetch --no-update Kconfiglib') - cmd('status --long') # Pass a custom flag too - - -def test_forall(clean_west_topdir): - # TODO: Check output - # The 'echo' command is available in both 'shell' and 'batch' - - # 'forall' with no projects cloned shouldn't fail - - cmd("forall -c 'echo *'") - - # Neither should it fail after fetching one or both projects - - cmd('fetch --no-update net-tools') - cmd("forall -c 'echo *'") - - cmd('fetch --no-update Kconfiglib') - cmd("forall -c 'echo *'") - - -def test_update(clean_west_topdir): - # Test the 'west update' command. It calls through to the same backend - # functions that are used for automatic updates and 'west init' - # reinitialization. - - # Create placeholder local repos - create_repo('west/manifest') - create_repo('west/west') - - # Create a simple configuration file. Git requires absolute paths for local - # repositories. - clean_west_topdir.join('west/config').write(''' -[manifest] -remote = {0}/remote-repos/manifest -revision = master -'''.format(clean_west_topdir)) - - config.read_config() - - # modify the manifest to point to another west - clean_west_topdir.join('manifest.yml').write(''' -west: - url: file://{}/remote-repos/west - revision: master -'''.format(clean_west_topdir), 'a') - - # Fetch the net-tools repository - cmd('fetch --no-update net-tools') - - # Add commits to the local repos - for path in 'west/manifest', 'west/west', NET_TOOLS_PATH: - add_commit(path, 'local') - - # Check that resetting the manifest repository removes the local commit - cmd('update --reset-manifest') - assert head_subject('west/manifest') == 'initial' - assert head_subject('west/west') == 'local' # Unaffected - assert head_subject(NET_TOOLS_PATH) == 'local' # Unaffected - - # Check that resetting the west repository removes the local commit - cmd('update --reset-west') - assert head_subject('west/west') == 'initial' - assert head_subject(NET_TOOLS_PATH) == 'local' # Unaffected - - # Check that resetting projects removes the local commit - cmd('update --reset-projects') - assert head_subject(NET_TOOLS_PATH) == 'initial' - - # Add commits to the upstream special repos - for path in 'remote-repos/manifest', 'remote-repos/west': - add_commit(path, 'upstream') - - # Check that updating the manifest repository gets the upstream commit - cmd('update --update-manifest') - assert head_subject('west/manifest') == 'upstream' - assert head_subject('west/west') == 'initial' # Unaffected - - # Check that updating the West repository triggers a restart - with pytest.raises(project.WestUpdated): - cmd('update --update-west') + [GIT, 'commit', '-a', '--allow-empty', '-m', msg, '--no-verify', + '--no-gpg-sign', '--no-post-rewrite'], cwd=repo) + + +def check_output(*args, **kwargs): + # Like subprocess.check_output, but returns a string in the + # default encoding instead of a byte array. + try: + out_bytes = subprocess.check_output(*args, **kwargs) + except subprocess.CalledProcessError as e: + print('*** check_output: nonzero return code', e.returncode, + file=sys.stderr) + print('cwd =', os.getcwd(), 'args =', args, + 'kwargs =', kwargs, file=sys.stderr) + print('subprocess output:', file=sys.stderr) + print(e.output.decode(), file=sys.stderr) + raise + return out_bytes.decode(sys.getdefaultencoding()) + + +def mirror_west_repo(dst): + # Create a west repository in dst which mirrors the exact state of + # the current tree, except ignored files. + # + # This is done in a simple way: + # + # 1. recursively copy THIS_WEST there (except .git and ignored files) + # 2. init a new git repository there + # 3. add the entire tree, and commit + # + # (We can't just clone THIS_WEST because we want to allow + # developers to test their working trees without having to make a + # commit -- remember, 'west init' clones the remote.) + wut = str(dst) # "west under test" + + # Copy the west working tree, except ignored files. + def ignore(directory, files): + # Get newline separated list of ignored files, as a string. + try: + ignored = check_output([GIT, 'check-ignore'] + files, + cwd=directory) + except subprocess.CalledProcessError as e: + # From the manpage: return values 0 and 1 respectively + # mean that some and no argument files were ignored. These + # are both OK. Treat other return values as errors. + if e.returncode not in (0, 1): + raise + else: + ignored = e.output.decode(sys.getdefaultencoding()) + + # Convert ignored to a set of file names as strings. + ignored = set(ignored.splitlines()) + + # Also ignore the .git directory itself. + if '.git' in files: + ignored.add('.git') + + return ignored + shutil.copytree(THIS_WEST, wut, ignore=ignore) + + # Create a fresh .git and commit existing directory tree. + create_repo(wut) + subprocess.check_call([GIT, 'add', '-A'], cwd=wut) + add_commit(wut, 'west under test') + + +def cmd(cmd, cwd=None): + # Run a west command in a directory (cwd defaults to os.getcwd()). + # + # This helper takes the command as a string, which is less clunky + # to work with than a list. It is split according to shell rules + # before being run. + # + # This helper relies on the test environment to ensure that the + # 'west' executable is a bootstrapper installed from the current + # west source code. + # + # stdout from cmd is captured and returned. The command is run in + # a python subprocess so that program-level setup and teardown + # happen fresh. + + try: + return check_output(shlex.split('west ' + cmd), cwd=cwd) + except subprocess.CalledProcessError: + print('cmd: west:', shutil.which('west'), file=sys.stderr) + raise def head_subject(path): @@ -348,28 +691,47 @@ def head_subject(path): cwd=path).decode().rstrip() -def test_bootstrap_reinit(clean_west_topdir, monkeypatch): - # Test that the bootstrap script calls 'west' with the expected --reset-* - # flags flags when reinitializing +def update_helper(west_tmpdir, command): + # Helper command for causing a change in two remote repositories, + # then running a project command on the west installation. + # + # Adds a commit to both of the kconfiglib and net-tools projects + # remotes, then run `command`. + # + # Captures the 'manifest-rev' and HEAD SHAs in both repositories + # before and after running the command, returning them in a tuple + # like this: + # + # (net-tools-manifest-rev-before, + # net-tools-manifest-rev-after, + # net-tools-HEAD-before, + # net-tools-HEAD-after, + # kconfiglib-manifest-rev-before, + # kconfiglib-manifest-rev-after, + # kconfiglib-HEAD-before, + # kconfiglib-HEAD-after) - def save_wrap_args(args): - # Saves bootstrap.wrap() arguments into wrap_args - nonlocal wrap_args - wrap_args = args + nt_remote = str(west_tmpdir.join('..', 'repos', 'net-tools')) + nt_local = str(west_tmpdir.join('net-tools')) + kl_remote = str(west_tmpdir.join('..', 'repos', 'Kconfiglib')) + kl_local = str(west_tmpdir.join('subdir', 'Kconfiglib')) - monkeypatch.setattr(bootstrap, 'wrap', save_wrap_args) + nt_mr_0 = check_output([GIT, 'rev-parse', 'manifest-rev'], cwd=nt_local) + kl_mr_0 = check_output([GIT, 'rev-parse', 'manifest-rev'], cwd=kl_local) + nt_head_0 = check_output([GIT, 'rev-parse', 'HEAD'], cwd=nt_local) + kl_head_0 = check_output([GIT, 'rev-parse', 'HEAD'], cwd=kl_local) - with pytest.raises(SystemExit): - bootstrap.init([]) # West already initialized + add_commit(nt_remote, 'another net-tools commit') + add_commit(kl_remote, 'another kconfiglib commit') - for init_args, west_args in ( - (['-m', 'foo'], ['update', '--reset-manifest', '--reset-projects', - '--reset-west']), - (['--mr', 'foo'], ['update', '--reset-manifest', '--reset-projects', - '--reset-west'])): + cmd(command) - # Reset wrap_args before each test so that it ends up as [] if wrap() - # isn't called (for the --no-reset case) - wrap_args = [] - bootstrap.init(init_args) - assert wrap_args == west_args + nt_mr_1 = check_output([GIT, 'rev-parse', 'manifest-rev'], cwd=nt_local) + kl_mr_1 = check_output([GIT, 'rev-parse', 'manifest-rev'], cwd=kl_local) + nt_head_1 = check_output([GIT, 'rev-parse', 'HEAD'], cwd=nt_local) + kl_head_1 = check_output([GIT, 'rev-parse', 'HEAD'], cwd=kl_local) + + return (nt_mr_0, nt_mr_1, + nt_head_0, nt_head_1, + kl_mr_0, kl_mr_1, + kl_head_0, kl_head_1) diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..af0e181 --- /dev/null +++ b/tox.ini @@ -0,0 +1,16 @@ +[tox] +envlist=py3-{posix,windows} +skip_missing_interpreters=true + +[testenv] +deps = -rtox_deps.txt +platform = posix: (linux|macos) + windows: win32 +whitelist_externals = + py.test +# Tests which import west modules directly from src need this PYTHONPATH +# available. The sdist which tox builds and installs only contains the +# bootstrapper modules. +setenv = PYTHONPATH={toxinidir}/src +commands = + py.test {posargs:tests} diff --git a/tests_requirements.txt b/tox_deps.txt similarity index 100% rename from tests_requirements.txt rename to tox_deps.txt