diff --git a/CHANGES b/CHANGES index bf8dc80cba9..8207ea9fee9 100644 --- a/CHANGES +++ b/CHANGES @@ -7,6 +7,12 @@ Here you can find the recent changes to tmuxp. CURRENT ------- +- [tests]: New context manager for tests, ``temp_session``. +- [tests]: New testsuite, ``testsuite.test_utils`` for testing testsuite + tools. +- [config] [builder]: New command, ``before_script``, which is a file to + be executed with a return code. It can be a bash, perl, python etc. + script. - [docs]: :ref:`python_api_quickstart` per `Issue #56`_. .. _Issue #56: https://github.com/tony/tmuxp/issues/56 diff --git a/doc/examples.rst b/doc/examples.rst index 21875ea4913..f486af87673 100644 --- a/doc/examples.rst +++ b/doc/examples.rst @@ -246,6 +246,54 @@ JSON .. literalinclude:: ../.tmuxp.json :language: json +Run script before launch +------------------------ + +You can use ``before_script`` to run a script before the tmux session +starts building. + +It works by using the `Exit Status`_ code returned by a script. Your +script can be any type, including bash, python, ruby, etc. + +A successful script will exit with a status of ``0``. + +You can use this for things like bootstrapping ruby / python environments +for a project (or checking to verify their installation). + +Run a python script (and check for it's return code), the script is +*relative to the ``.tmuxp.yaml``'s root* (Windows and panes omitted in +this example): + +.. code-block:: yaml + + session_name: my session + before_script: bootstrap.py + # ... the rest of your config + +.. code-block:: json + + { + "session_name": "my session", + "before_script": "bootstrap.py" + } + +Run a shell script + check for return code on an absolute path. (Windows +and panes omitted in this example) + +.. code-block:: yaml + session_name: another example + before_script: /absolute/path/this.sh # abs path to shell script + # ... the rest of your config + +.. code-block:: json + + { + "session_name": "my session", + "before_script": "/absolute/path/this.sh" + } + +.. _Exit Status: http://tldp.org/LDP/abs/html/exit-status.html + Project configs --------------- diff --git a/tmuxp/testsuite/fixtures/script_complete.sh b/tmuxp/testsuite/fixtures/script_complete.sh new file mode 100755 index 00000000000..4d8f44b8c0b --- /dev/null +++ b/tmuxp/testsuite/fixtures/script_complete.sh @@ -0,0 +1,4 @@ +#!/bin/bash + +echo hello +echo $? # Exit status 0 returned because command executed successfully. diff --git a/tmuxp/testsuite/fixtures/script_failed.sh b/tmuxp/testsuite/fixtures/script_failed.sh new file mode 100755 index 00000000000..3940d9d530c --- /dev/null +++ b/tmuxp/testsuite/fixtures/script_failed.sh @@ -0,0 +1,4 @@ +#!/bin/bash + +exit 113 # Will return 113 to shell. + # To verify this, type "echo $?" after script terminates. diff --git a/tmuxp/testsuite/helpers.py b/tmuxp/testsuite/helpers.py index a14784df297..90ded313abb 100644 --- a/tmuxp/testsuite/helpers.py +++ b/tmuxp/testsuite/helpers.py @@ -10,13 +10,16 @@ with_statement, unicode_literals import time -from random import randint import logging +import contextlib + try: import unittest2 as unittest except ImportError: # Python 2.7 import unittest +from random import randint + from . import t from .. import Server, log, exc @@ -25,6 +28,28 @@ TEST_SESSION_PREFIX = 'tmuxp_' +def get_test_session_name(server, prefix='tmuxp_'): + while True: + session_name = prefix + str(randint(0, 9999999)) + if not t.has_session(session_name): + break + return session_name + + +@contextlib.contextmanager +def temp_session(server, session_name=None): + if not session_name: + session_name = get_test_session_name(server) + + session = server.new_session(session_name) + try: + yield session + finally: + if server.has_session(session_name): + session.kill_session() + return + + class TestCase(unittest.TestCase): """Base TestClass so we don't have to try: unittest2 every module. """ @@ -80,10 +105,7 @@ def bootstrap(self): ) ] - while True: - TEST_SESSION_NAME = TEST_SESSION_PREFIX + str(randint(0, 9999999)) - if not t.has_session(TEST_SESSION_NAME): - break + TEST_SESSION_NAME = get_test_session_name(server=t) try: session = t.new_session( @@ -113,4 +135,5 @@ def bootstrap(self): assert TEST_SESSION_NAME != 'tmuxp' self.TEST_SESSION_NAME = TEST_SESSION_NAME + self.server = t self.session = session diff --git a/tmuxp/testsuite/test_utils.py b/tmuxp/testsuite/test_utils.py new file mode 100644 index 00000000000..c4c1467d6dc --- /dev/null +++ b/tmuxp/testsuite/test_utils.py @@ -0,0 +1,44 @@ +# -*- coding: utf-8 -*- +"""Tests for tmuxp testsuite's helper and utility functions.""" + +from __future__ import absolute_import, division, print_function, \ + with_statement, unicode_literals + +from .helpers import get_test_session_name, temp_session, TestCase, \ + TmuxTestCase, unittest + + +class TempSession(TmuxTestCase): + + def test_kills_session(self): + server = self.server + session_name = get_test_session_name(server=server) + + with temp_session(server=server, session_name=session_name) as session: + result = server.has_session(session_name) + self.assertTrue(result) + + self.assertFalse(server.has_session(session_name)) + + def test_if_session_killed_before(self): + """Handles situation where session already closed within context""" + + server = self.server + session_name = get_test_session_name(server=server) + + with temp_session(server=server, session_name=session_name) as session: + + # an error or an exception within a temp_session kills the session + server.kill_session(session_name) + + result = server.has_session(session_name) + self.assertFalse(result) + + # really dead? + self.assertFalse(server.has_session(session_name)) + + +def suite(): + suite = unittest.TestSuite() + suite.addTest(unittest.makeSuite(TempSession)) + return suite diff --git a/tmuxp/testsuite/workspacebuilder.py b/tmuxp/testsuite/workspacebuilder.py index 40e8fd5efd9..1c00d9aff3a 100644 --- a/tmuxp/testsuite/workspacebuilder.py +++ b/tmuxp/testsuite/workspacebuilder.py @@ -13,6 +13,7 @@ import sys import logging import unittest +import subprocess import time import kaptan @@ -20,12 +21,13 @@ from .. import Window, config, exc from .._compat import text_type from ..workspacebuilder import WorkspaceBuilder -from .helpers import TmuxTestCase +from .helpers import TestCase, TmuxTestCase, temp_session logger = logging.getLogger(__name__) current_dir = os.path.abspath(os.path.dirname(__file__)) example_dir = os.path.abspath(os.path.join(current_dir, '..', '..', 'examples')) +fixtures_dir = os.path.abspath(os.path.join(current_dir, 'fixtures')) class TwoPaneTest(TmuxTestCase): @@ -339,7 +341,7 @@ def test_blank_pane_count(self): test_config = kaptan.Kaptan().import_config(self.yaml_config_file).get() test_config = config.expand(test_config) # for window in test_config['windows']: - # window['layout'] = 'tiled' + # window['layout'] = 'tiled' builder = WorkspaceBuilder(sconf=test_config) builder.build(session=self.session) @@ -527,8 +529,133 @@ def test_window_index(self): self.assertEqual(int(window['window_index']), expected_index) +class BeforeLoadScript(TmuxTestCase): + + config_script_not_exists = """ + session_name: sampleconfig + before_script: {fixtures_dir}/script_not_exists.sh + windows: + - panes: + - pane + """ + + config_script_fails = """ + session_name: sampleconfig + before_script: {fixtures_dir}/script_failed.sh + windows: + - panes: + - pane + """ + + config_script_completes = """ + session_name: sampleconfig + before_script: {fixtures_dir}/script_complete.sh + windows: + - panes: + - pane + """ + + def test_throw_error_if_retcode_false(self): + + sconfig = kaptan.Kaptan(handler='yaml') + yaml = self.config_script_fails.format( + fixtures_dir=fixtures_dir + ) + sconfig = sconfig.import_config(yaml).get() + sconfig = config.expand(sconfig) + sconfig = config.trickle(sconfig) + + builder = WorkspaceBuilder(sconf=sconfig) + + with temp_session(self.server) as sess: + session_name = sess.get('session_name') + + with self.assertRaises(subprocess.CalledProcessError): + builder.build(session=sess) + + result = self.server.has_session(session_name) + self.assertFalse( + result, + msg="Kills session if before_script exits with errcode" + ) + + def test_throw_error_if_file_not_exists(self): + + sconfig = kaptan.Kaptan(handler='yaml') + yaml = self.config_script_not_exists.format( + fixtures_dir=fixtures_dir + ) + sconfig = sconfig.import_config(yaml).get() + sconfig = config.expand(sconfig) + sconfig = config.trickle(sconfig) + + builder = WorkspaceBuilder(sconf=sconfig) + + with temp_session(self.server) as sess: + session_name = sess.get('session_name') + temp_session_exists = self.server.has_session(sess.get('session_name')) + self.assertTrue(temp_session_exists) + with self.assertRaisesRegexp( + (BeforeLoadScriptNotExists, OSError), + 'No such file or directory' + ): + builder.build(session=sess) + result = self.server.has_session(session_name) + self.assertFalse( + result, + msg="Kills session if before_script doesn't exist" + ) + + def test_true_if_test_passes(self): + + sconfig = kaptan.Kaptan(handler='yaml') + yaml = self.config_script_completes.format( + fixtures_dir=fixtures_dir + ) + sconfig = sconfig.import_config(yaml).get() + sconfig = config.expand(sconfig) + sconfig = config.trickle(sconfig) + + builder = WorkspaceBuilder(sconf=sconfig) + + with temp_session(self.session.server) as session: + builder.build(session=self.session) + + +from ..workspacebuilder import run_before_script, BeforeLoadScriptNotExists, \ + BeforeLoadScriptFailed + + +class RunBeforeScript(TestCase): + + def test_raise_BeforeLoadScriptNotExists_if_not_exists(self): + script_file = os.path.join(fixtures_dir, 'script_noexists.sh') + + with self.assertRaises(BeforeLoadScriptNotExists): + run_before_script(script_file) + + with self.assertRaises(OSError): + run_before_script(script_file) + + def test_raise_BeforeLoadScriptFailed_if_retcode(self): + script_file = os.path.join(fixtures_dir, 'script_failed.sh') + + with self.assertRaises(BeforeLoadScriptFailed): + run_before_script(script_file) + + with self.assertRaises(subprocess.CalledProcessError): + run_before_script(script_file) + + def test_return_stdout_if_exits_zero(self): + script_file = os.path.join(fixtures_dir, 'script_complete.sh') + + run_before_script(script_file) + + def suite(): suite = unittest.TestSuite() + suite.addTest(unittest.makeSuite(BeforeLoadScript)) + suite.addTest(unittest.makeSuite(RunBeforeScript)) suite.addTest(unittest.makeSuite(BlankPaneTest)) suite.addTest(unittest.makeSuite(FocusAndPaneIndexTest)) suite.addTest(unittest.makeSuite(PaneOrderingTest)) diff --git a/tmuxp/workspacebuilder.py b/tmuxp/workspacebuilder.py index aec1b652640..c3b761d37b5 100644 --- a/tmuxp/workspacebuilder.py +++ b/tmuxp/workspacebuilder.py @@ -11,12 +11,50 @@ import os import logging +import subprocess from . import exc, config, Window, Pane, Session, Server +from ._compat import PY2 logger = logging.getLogger(__name__) +class BeforeLoadScriptNotExists(OSError): + + def __init__(self, *args, **kwargs): + super(BeforeLoadScriptNotExists, self).__init__(*args, **kwargs) + + self.strerror = "before_script file '%s' doesn't exist." % self.strerror + + +class BeforeLoadScriptFailed(subprocess.CalledProcessError): + + def __init__(self, *args, **kwargs): + super(BeforeLoadScriptFailed, self).__init__(*args, **kwargs) + + def __unicode__(self): + return "before_script failed (%s): %s. Output: \n%s" % ( + self.returncode, self.cmd, self.output + ) + + if PY2: + def __str__(self): + return self.__unicode__().encode('utf-8') + + +def run_before_script(script_file): + """Function to wrap try/except for subprocess.check_call().""" + try: + return subprocess.check_call(script_file, stdout=subprocess.PIPE) + except subprocess.CalledProcessError as e: + raise BeforeLoadScriptFailed(e.returncode, e.cmd) + except OSError as e: + if e.errno == 2: + raise BeforeLoadScriptNotExists(e, script_file) + else: + raise(e) + + class WorkspaceBuilder(object): """Load workspace from session :py:obj:`dict`. @@ -127,11 +165,23 @@ def build(self, session=None): assert(len(self.sconf['session_name']) > 0) self.session = session + self.server = session.server + + self.server._list_sessions() + assert self.server.has_session(session.get('session_name')) + assert session.get('session_id') assert(isinstance(session, Session)) focus = None + if 'before_script' in self.sconf: + try: + run_before_script(self.sconf['before_script']) + except Exception as e: + self.session.kill_session() + raise(e) + for w, wconf in self.iter_create_windows(session): assert(isinstance(w, Window)) @@ -287,7 +337,7 @@ def freeze(session): pconf = {} pconf['shell_command'] = [] - if not 'start_directory' in wconf: + if 'start_directory' not in wconf: pconf['shell_command'].append( 'cd ' + p.get('pane_current_path') )