# -*- coding: utf-8 -*- from __future__ import absolute_import from __future__ import print_function from __future__ import unicode_literals import functools import os.path import re import shutil import sys import pexpect import pytest import aactivator from testing import make_venv_in_tempdir PS1 = 'TEST> ' INPUT = 'INPUT> ' def get_proc(shell, homedir): return pexpect.spawn( shell[0], list(shell[1:]), timeout=5, env={ 'COVERAGE_PROCESS_START': os.environ.get( 'COVERAGE_PROCESS_START', '', ), 'PS1': PS1, 'TOP': os.environ.get('TOP', ''), 'HOME': str(homedir), 'PATH': os.path.dirname(sys.executable) + os.defpath }, ) def expect_exact_better(proc, expected): """Uses raw strings like expect_exact, but starts looking from the start. """ # I'd put a $ on the end of this regex, but sometimes the buffer comes # to us too quickly for our assertions before = proc.before after = proc.after reg = '^' + re.escape(expected) reg = reg.replace('\n', '\r*\n') try: proc.expect(reg) except pexpect.TIMEOUT: # pragma: no cover message = ( 'Incorrect output.', '>>> Context:', before.decode('utf8') + after.decode('utf8'), '>>> Expected:', ' ' + expected.replace('\r', '').replace('\n', '\n '), '>>> Actual:', ' ' + proc.buffer.replace(b'\r', b'').replace(b'\n', b'\n ').decode('utf8'), ) message = '\n'.join(message) if sys.version_info < (3, 0): message = message.encode('utf8') raise AssertionError(message) def run_cmd(proc, line, output='', ps1=PS1): if ps1: expect_exact_better(proc, ps1) proc.sendline(line) expect_exact_better(proc, line + '\n' + output) run_input = functools.partial(run_cmd, ps1='') def parse_tests(tests): cmds = [] cmd_fn = None cmd = None output = '' for line in tests.splitlines(): if line.startswith((PS1, INPUT)): if cmd_fn is not None: cmds.append((cmd_fn, cmd, output)) cmd = None cmd_fn = None output = '' elif INPUT in line: if cmd_fn is not None: output += line[:line.index(INPUT)] cmds.append((cmd_fn, cmd, output)) cmd = None cmd_fn = None output = '' line = line[line.index(INPUT):] if line.startswith(PS1): cmd_fn = run_cmd cmd = line[len(PS1):] elif line.startswith(INPUT): cmd_fn = run_input cmd = line[len(INPUT):] else: output += line + '\n' if cmd_fn is not None: cmds.append((cmd_fn, cmd, output)) return cmds def shellquote(cmd): """transform a python command-list to a shell command-string""" from pipes import quote return ' '.join(quote(arg) for arg in cmd) def run_test(shell, tests, homedir): proc = get_proc(shell['cmd'], homedir) for test_fn, test, output in parse_tests(tests): test_fn(proc, test, output) @pytest.fixture(params=( { 'cmd': ('/bin/bash', '--noediting', '--norc', '-is'), 'errors': { 'pwd_missing': 'bash: cd: {tmpdir}/d: No such file or directory', }, }, { # -df is basically --norc # -V prevents a bizarre behavior where zsh prints lots of extra whitespace 'cmd': ('zsh', '-df', '-is', '-V', '+Z'), 'errors': { 'pwd_missing': 'cd: no such file or directory: {tmpdir}/d', }, }, )) def shell(request): return request.param def test_activates_when_cding_in(venv_path, shell, tmpdir): make_venv_in_tempdir(tmpdir) test = '''\ TEST> eval "$(aactivator init)" TEST> echo TEST> cd {venv_path} aactivator will source .activate.sh and .deactivate.sh at {venv_path}. Acceptable? (y)es (n)o (N)ever: INPUT> y aactivator will remember this: ~/.cache/aactivator/allowed aactivating... TEST> echo (aliased) TEST> cd / (aliased) deactivating... TEST> echo ''' test = test.format(venv_path=str(venv_path)) run_test(shell, test, tmpdir) def test_activates_when_cding_to_child_dir(venv_path, tmpdir, shell): make_venv_in_tempdir(tmpdir) test = '''\ TEST> eval "$(aactivator init)" TEST> echo TEST> cd {venv_path}/child-dir aactivator will source .activate.sh and .deactivate.sh at {venv_path}. Acceptable? (y)es (n)o (N)ever: INPUT> y aactivator will remember this: ~/.cache/aactivator/allowed aactivating... TEST> echo (aliased) TEST> cd / (aliased) deactivating... TEST> echo ''' test = test.format(venv_path=str(venv_path)) run_test(shell, test, tmpdir) def test_activates_subshell(venv_path, tmpdir, shell): make_venv_in_tempdir(tmpdir) test = '''\ TEST> eval "$(aactivator init)" TEST> echo TEST> cd {venv_path} aactivator will source .activate.sh and .deactivate.sh at {venv_path}. Acceptable? (y)es (n)o (N)ever: INPUT> y aactivator will remember this: ~/.cache/aactivator/allowed aactivating... TEST> echo (aliased) TEST> {shell} TEST> echo TEST> eval "$(aactivator init)" aactivating... TEST> echo 2 (aliased) 2 TEST> cd / (aliased) deactivating... TEST> echo 3 3 TEST> exit 2>/dev/null TEST> echo (aliased) TEST> cd / (aliased) deactivating... TEST> echo 5 5 ''' test = test.format( venv_path=str(venv_path), shell=shellquote(shell['cmd']), ) run_test(shell, test, tmpdir) def test_complains_when_not_activated(activate, venv_path, tmpdir, shell): make_venv_in_tempdir(tmpdir) activate.chmod(0o666) test = '''\ TEST> eval "$(aactivator init)" TEST> echo TEST> cd {venv_path} aactivator will source .activate.sh and .deactivate.sh at {venv_path}. Acceptable? (y)es (n)o (N)ever: INPUT> y aactivator will remember this: ~/.cache/aactivator/allowed aactivator: Cowardly refusing to source .activate.sh because writeable by others: .activate.sh TEST> echo aactivator: Cowardly refusing to source .activate.sh because writeable by others: .activate.sh TEST> cd / TEST> echo ''' test = test.format(venv_path=str(venv_path)) run_test(shell, test, tmpdir) def test_complains_parent_directory_insecure(venv_path, tmpdir, shell): make_venv_in_tempdir(tmpdir) venv_path.chmod(0o777) test = '''\ TEST> eval "$(aactivator init)" TEST> echo TEST> cd {venv_path} aactivator will source .activate.sh and .deactivate.sh at {venv_path}. Acceptable? (y)es (n)o (N)ever: INPUT> y aactivator will remember this: ~/.cache/aactivator/allowed aactivator: Cowardly refusing to source .activate.sh because writeable by others: . TEST> echo aactivator: Cowardly refusing to source .activate.sh because writeable by others: . TEST> cd / TEST> echo ''' test = test.format(venv_path=str(venv_path)) run_test(shell, test, tmpdir) def test_activate_but_no_deactivate(venv_path, tmpdir, deactivate, shell): make_venv_in_tempdir(tmpdir) deactivate.remove() test = '''\ TEST> eval "$(aactivator init)" TEST> echo TEST> cd {venv_path} aactivator will source .activate.sh and .deactivate.sh at {venv_path}. Acceptable? (y)es (n)o (N)ever: INPUT> y aactivator will remember this: ~/.cache/aactivator/allowed aactivating... TEST> echo (aliased) TEST> cd / (aliased) aactivator: Cannot deactivate. File missing: {deactivate} TEST> echo (aliased) ''' test = test.format( venv_path=str(venv_path), deactivate=str(deactivate), ) run_test(shell, test, tmpdir) def test_prompting_behavior(venv_path, tmpdir, shell): make_venv_in_tempdir(tmpdir) test = '''\ TEST> eval "$(aactivator init)" TEST> echo TEST> cd {venv_path} aactivator will source .activate.sh and .deactivate.sh at {venv_path}. Acceptable? (y)es (n)o (N)ever: INPUT> herpderp I didn't understand your response. Acceptable? (y)es (n)o (N)ever: INPUT> n TEST> echo TEST> echo TEST> echo TEST> cd / TEST> cd {venv_path} aactivator will source .activate.sh and .deactivate.sh at {venv_path}. Acceptable? (y)es (n)o (N)ever: INPUT> N aactivator will remember this: ~/.cache/aactivator/disallowed TEST> echo TEST> cd / TEST> echo ''' test = test.format(venv_path=str(venv_path)) run_test(shell, test, tmpdir) def test_pwd_goes_missing(tmpdir, shell): tmpdir.mkdir('d') make_venv_in_tempdir(tmpdir) test = '''\ TEST> eval "$(aactivator init)" TEST> echo TEST> cd {{tmpdir}}/d TEST> rm -rf $PWD TEST> cd $PWD {pwd_missing} TEST> echo TEST> echo ''' test = test.format( pwd_missing=shell['errors']['pwd_missing'], ).format( tmpdir=str(tmpdir), ) run_test(shell, test, tmpdir) def test_version_change(venv_path, tmpdir, shell): """If aactivator detects a version change, it will re-init and re-activate""" make_venv_in_tempdir(tmpdir) test = '''\ TEST> eval "$(aactivator init)" TEST> cd {venv_path} aactivator will source .activate.sh and .deactivate.sh at {venv_path}. Acceptable? (y)es (n)o (N)ever: INPUT> y aactivator will remember this: ~/.cache/aactivator/allowed aactivating... TEST> export AACTIVATOR_VERSION=0 aactivating... TEST> echo $AACTIVATOR_VERSION (aliased) {version} ''' test = test.format( venv_path=str(venv_path), version=aactivator.__version__, ) run_test(shell, test, tmpdir) def test_cd_dash(venv_path, tmpdir, shell): make_venv_in_tempdir(tmpdir) venv2 = make_venv_in_tempdir(tmpdir, 'venv2') test = '''\ TEST> eval "$(aactivator init)" TEST> cd {venv_path}/child-dir aactivator will source .activate.sh and .deactivate.sh at {venv_path}. Acceptable? (y)es (n)o (N)ever: INPUT> y aactivator will remember this: ~/.cache/aactivator/allowed aactivating... TEST> pwd {venv_path}/child-dir TEST> cd {venv2}/child-dir aactivator will source .activate.sh and .deactivate.sh at {venv2}. Acceptable? (y)es (n)o (N)ever: INPUT> y aactivator will remember this: ~/.cache/aactivator/allowed (aliased) deactivating... aactivating... TEST> cd - > /dev/null (aliased) deactivating... aactivating... TEST> pwd {venv_path}/child-dir TEST> cd - > /dev/null (aliased) deactivating... aactivating... TEST> pwd {venv2}/child-dir ''' test = test.format( venv_path=str(venv_path), venv2=str(venv2), ) run_test(shell, test, tmpdir) def test_aactivator_goes_missing_no_output(venv_path, shell, tmpdir): make_venv_in_tempdir(tmpdir) exe = tmpdir.join('exe').strpath src = os.path.join(os.path.dirname(sys.executable), 'aactivator') shutil.copy(src, exe) test = '''\ TEST> eval "$({exe} init)" TEST> rm {exe} TEST> echo TEST> cd {venv_path} TEST> echo ''' test = test.format(venv_path=str(venv_path), exe=exe) run_test(shell, test, tmpdir)