diff --git a/README.md b/README.md index 91554aec..0539539f 100644 --- a/README.md +++ b/README.md @@ -151,6 +151,10 @@ scenario. Make sure you introduce new default colors in `themes/default.py` for every new segment you create. Test your segment with this theme first. +You should add tests for your segment as best you are able. Unit and +integration tests are both welcome. Run your tests with the `nosetests` command +after install the requirements in `dev_requirements.txt`. + ### Themes The `themes` directory stores themes for your prompt, which are basically color diff --git a/circle.yml b/circle.yml new file mode 100644 index 00000000..7638a893 --- /dev/null +++ b/circle.yml @@ -0,0 +1,5 @@ +dependencies: + pre: + - sudo pip install -r dev_requirements.txt + - git config --global user.email "tester@example.com" + - git config --global user.name "Tester McGee" diff --git a/dev_requirements.txt b/dev_requirements.txt index a6786964..47a4588d 100644 --- a/dev_requirements.txt +++ b/dev_requirements.txt @@ -1,2 +1,3 @@ -nose -mock +nose>=1.3.7 +mock>=1.3.0 +sh>=1.11 diff --git a/install.py b/install.py index 9be4b712..bf36f41e 100755 --- a/install.py +++ b/install.py @@ -33,6 +33,10 @@ def load_source(srcfile): for segment in config.SEGMENTS: source += load_source(os.path.join(SEGMENTS_DIR, segment + '.py')) + # assumes each segment file will have a function called + # add_segment__[segment] that accepts the powerline object + source += 'add_{}_segment(powerline)\n'.format(segment) + source += 'sys.stdout.write(powerline.draw())\n' try: diff --git a/segments/__init__.py b/segments/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/segments/cwd.py b/segments/cwd.py index d5e3a791..3c80b701 100644 --- a/segments/cwd.py +++ b/segments/cwd.py @@ -28,7 +28,7 @@ def requires_special_home_display(name): return (name == '~' and Color.HOME_SPECIAL_DISPLAY) -def maybe_shorten_name(name): +def maybe_shorten_name(powerline, name): """If the user has asked for each directory name to be shortened, will return the name up to their specified length. Otherwise returns the full name.""" @@ -45,7 +45,7 @@ def get_fg_bg(name): return (Color.PATH_FG, Color.PATH_BG,) -def add_cwd_segment(): +def add_cwd_segment(powerline): cwd = powerline.cwd or os.getenv('PWD') if not py3: cwd = cwd.decode("utf-8") @@ -86,7 +86,5 @@ def add_cwd_segment(): separator = None separator_fg = None - powerline.append(' %s ' % maybe_shorten_name(name), fg, bg, + powerline.append(' %s ' % maybe_shorten_name(powerline, name), fg, bg, separator, separator_fg) - -add_cwd_segment() diff --git a/segments/exit_code.py b/segments/exit_code.py index 5c936156..e3b320ee 100644 --- a/segments/exit_code.py +++ b/segments/exit_code.py @@ -1,8 +1,6 @@ -def add_exit_code_segment(): +def add_exit_code_segment(powerline): if powerline.args.prev_error == 0: return fg = Color.CMD_FAILED_FG bg = Color.CMD_FAILED_BG powerline.append(' %s ' % str(powerline.args.prev_error), fg, bg) - -add_exit_code_segment() diff --git a/segments/fossil.py b/segments/fossil.py index bffc10f3..6c7818ec 100644 --- a/segments/fossil.py +++ b/segments/fossil.py @@ -12,7 +12,7 @@ def get_fossil_status(): return has_modified_files, has_untracked_files, has_missing_files -def add_fossil_segment(): +def _add_fossil_segment(powerline): subprocess.Popen(['fossil'], stdout=subprocess.PIPE).communicate()[0] branch = ''.join([i.replace('*','').strip() for i in os.popen("fossil branch 2> /dev/null").read().strip().split("\n") if i.startswith('*')]) if len(branch) == 0: @@ -32,9 +32,19 @@ def add_fossil_segment(): branch += (' ' + extra if extra != '' else '') powerline.append(' %s ' % branch, fg, bg) -try: - add_fossil_segment() -except OSError: - pass -except subprocess.CalledProcessError: - pass +def add_fossil_segment(powerline): + """Wraps _add_fossil_segment in exception handling.""" + + # FIXME This function was added when introducing a testing framework, + # during which the 'powerline' object was passed into the + # `add_[segment]_segment` functions instead of being a global variable. At + # that time it was unclear whether the below exceptions could actually be + # thrown. It would be preferable to find out whether they ever will. If so, + # write a comment explaining when. Otherwise remove. + + try: + _add_fossil_segment(powerline) + except OSError: + pass + except subprocess.CalledProcessError: + pass diff --git a/segments/git.py b/segments/git.py index cc0787bc..d33a9d39 100644 --- a/segments/git.py +++ b/segments/git.py @@ -1,5 +1,6 @@ import re import subprocess +import os GIT_SYMBOLS = { 'detached': u'\u2693', @@ -11,17 +12,24 @@ 'conflicted': u'\u273C' } -git_subprocess_env = { - # LANG is specified to ensure git always uses a language we are expecting. - # Otherwise we may be unable to parse the output. - "LANG": "C", +def get_PATH(): + """Normally gets the PATH from the OS. This function exists to enable + easily mocking the PATH in tests. + """ + return os.getenv("PATH") - # https://github.com/milkbikis/powerline-shell/pull/126 - "HOME": os.getenv("HOME"), +def git_subprocess_env(): + return { + # LANG is specified to ensure git always uses a language we are expecting. + # Otherwise we may be unable to parse the output. + "LANG": "C", - # https://github.com/milkbikis/powerline-shell/pull/153 - "PATH": os.getenv("PATH"), -} + # https://github.com/milkbikis/powerline-shell/pull/126 + "HOME": os.getenv("HOME"), + + # https://github.com/milkbikis/powerline-shell/pull/153 + "PATH": get_PATH(), + } def parse_git_branch_info(status): @@ -32,7 +40,7 @@ def parse_git_branch_info(status): def _get_git_detached_branch(): p = subprocess.Popen(['git', 'describe', '--tags', '--always'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, - env=git_subprocess_env) + env=git_subprocess_env()) detached_ref = p.communicate()[0].decode("utf-8").rstrip('\n') if p.returncode == 0: branch = u'{} {}'.format(GIT_SYMBOLS['detached'], detached_ref) @@ -62,10 +70,15 @@ def _n_or_empty(_dict, _key): return _dict[_key] if int(_dict[_key]) > 1 else u'' -def add_git_segment(): - p = subprocess.Popen(['git', 'status', '--porcelain', '-b'], - stdout=subprocess.PIPE, stderr=subprocess.PIPE, - env=git_subprocess_env) +def add_git_segment(powerline): + try: + p = subprocess.Popen(['git', 'status', '--porcelain', '-b'], + stdout=subprocess.PIPE, stderr=subprocess.PIPE, + env=git_subprocess_env()) + except OSError: + # Popen will throw an OSError if git is not found + return + pdata = p.communicate() if p.returncode != 0: return @@ -101,8 +114,3 @@ def _add(_dict, _key, fg, bg): _add(stats, 'notstaged', Color.GIT_NOTSTAGED_FG, Color.GIT_NOTSTAGED_BG) _add(stats, 'untracked', Color.GIT_UNTRACKED_FG, Color.GIT_UNTRACKED_BG) _add(stats, 'conflicted', Color.GIT_CONFLICTED_FG, Color.GIT_CONFLICTED_BG) - -try: - add_git_segment() -except (OSError, subprocess.CalledProcessError): - pass diff --git a/segments/hg.py b/segments/hg.py index 5a4f40dc..d5f1cf48 100644 --- a/segments/hg.py +++ b/segments/hg.py @@ -20,7 +20,7 @@ def get_hg_status(): has_modified_files = True return has_modified_files, has_untracked_files, has_missing_files -def add_hg_segment(): +def add_hg_segment(powerline): branch = os.popen('hg branch 2> /dev/null').read().rstrip() if len(branch) == 0: return False @@ -37,5 +37,3 @@ def add_hg_segment(): extra += '!' branch += (' ' + extra if extra != '' else '') return powerline.append(' %s ' % branch, fg, bg) - -add_hg_segment() diff --git a/segments/hostname.py b/segments/hostname.py index baf99191..609711e2 100644 --- a/segments/hostname.py +++ b/segments/hostname.py @@ -1,4 +1,4 @@ -def add_hostname_segment(): +def add_hostname_segment(powerline): if powerline.args.colorize_hostname: from lib.color_compliment import stringToHashToColorAndOpposite from lib.colortrans import rgb2short @@ -19,6 +19,3 @@ def add_hostname_segment(): host_prompt = ' %s ' % socket.gethostname().split('.')[0] powerline.append(host_prompt, Color.HOSTNAME_FG, Color.HOSTNAME_BG) - - -add_hostname_segment() diff --git a/segments/jobs.py b/segments/jobs.py index 5dde3e65..a6ff1808 100644 --- a/segments/jobs.py +++ b/segments/jobs.py @@ -2,7 +2,7 @@ import re import subprocess -def add_jobs_segment(): +def add_jobs_segment(powerline): pppid_proc = subprocess.Popen(['ps', '-p', str(os.getppid()), '-oppid='], stdout=subprocess.PIPE) pppid = pppid_proc.communicate()[0].decode("utf-8").strip() @@ -15,5 +15,3 @@ def add_jobs_segment(): if num_jobs > 0: powerline.append(' %d ' % num_jobs, Color.JOBS_FG, Color.JOBS_BG) - -add_jobs_segment() diff --git a/segments/node_version.py b/segments/node_version.py index 842e487c..466a0864 100644 --- a/segments/node_version.py +++ b/segments/node_version.py @@ -1,7 +1,7 @@ import subprocess -def add_node_version_segment(): +def add_node_version_segment(powerline): try: p1 = subprocess.Popen(["node", "--version"], stdout=subprocess.PIPE) version = p1.communicate()[0].decode("utf-8").rstrip() @@ -9,5 +9,3 @@ def add_node_version_segment(): powerline.append(version, 15, 18) except OSError: return - -add_node_version_segment() diff --git a/segments/php_version.py b/segments/php_version.py index ee174047..7b9b8aa3 100644 --- a/segments/php_version.py +++ b/segments/php_version.py @@ -1,7 +1,7 @@ import subprocess -def add_php_version_segment(): +def add_php_version_segment(powerline): try: output = subprocess.check_output(['php', '-r', 'echo PHP_VERSION;'], stderr=subprocess.STDOUT) if '-' in output: @@ -12,5 +12,3 @@ def add_php_version_segment(): powerline.append(version, 15, 4) except OSError: return - -add_php_version_segment() diff --git a/segments/read_only.py b/segments/read_only.py index bcb28f4c..efbe11ad 100644 --- a/segments/read_only.py +++ b/segments/read_only.py @@ -1,9 +1,7 @@ import os -def add_read_only_segment(): +def add_read_only_segment(powerline): cwd = powerline.cwd or os.getenv('PWD') if not os.access(cwd, os.W_OK): powerline.append(' %s ' % powerline.lock, Color.READONLY_FG, Color.READONLY_BG) - -add_read_only_segment() diff --git a/segments/root.py b/segments/root.py index 9cd5e8c2..49404ecf 100644 --- a/segments/root.py +++ b/segments/root.py @@ -1,4 +1,4 @@ -def add_root_indicator_segment(): +def add_root_segment(powerline): root_indicators = { 'bash': ' \\$ ', 'zsh': ' %# ', @@ -10,5 +10,3 @@ def add_root_indicator_segment(): fg = Color.CMD_FAILED_FG bg = Color.CMD_FAILED_BG powerline.append(root_indicators[powerline.args.shell], fg, bg) - -add_root_indicator_segment() diff --git a/segments/ruby_version.py b/segments/ruby_version.py index 82ba7f27..040f5334 100644 --- a/segments/ruby_version.py +++ b/segments/ruby_version.py @@ -1,7 +1,7 @@ import subprocess -def add_ruby_version_segment(): +def add_ruby_version_segment(powerline): try: p1 = subprocess.Popen(["ruby", "-v"], stdout=subprocess.PIPE) p2 = subprocess.Popen(["sed", "s/ (.*//"], stdin=p1.stdout, stdout=subprocess.PIPE) @@ -13,5 +13,3 @@ def add_ruby_version_segment(): powerline.append(version, 15, 1) except OSError: return - -add_ruby_version_segment() diff --git a/segments/set_term_title.py b/segments/set_term_title.py index 4d1d5cd8..e9954ae8 100644 --- a/segments/set_term_title.py +++ b/segments/set_term_title.py @@ -1,4 +1,4 @@ -def add_term_title_segment(): +def add_set_term_title_segment(powerline): term = os.getenv('TERM') if not (('xterm' in term) or ('rxvt' in term)): return @@ -13,5 +13,3 @@ def add_term_title_segment(): powerline.append(set_title, None, None, '') - -add_term_title_segment() diff --git a/segments/ssh.py b/segments/ssh.py index e90397a3..5361bc5e 100644 --- a/segments/ssh.py +++ b/segments/ssh.py @@ -1,8 +1,6 @@ import os -def add_ssh_segment(): +def add_ssh_segment(powerline): if os.getenv('SSH_CLIENT'): powerline.append(' %s ' % powerline.network, Color.SSH_FG, Color.SSH_BG) - -add_ssh_segment() diff --git a/segments/svn.py b/segments/svn.py index a378a87a..89168c01 100644 --- a/segments/svn.py +++ b/segments/svn.py @@ -1,6 +1,7 @@ import subprocess -def add_svn_segment(): + +def _add_svn_segment(powerline): is_svn = subprocess.Popen(['svn', 'status'], stdout=subprocess.PIPE, stderr=subprocess.PIPE) is_svn_output = is_svn.communicate()[1].decode("utf-8").strip() @@ -17,9 +18,20 @@ def add_svn_segment(): changes = output.strip() powerline.append(' %s ' % changes, Color.SVN_CHANGES_FG, Color.SVN_CHANGES_BG) -try: - add_svn_segment() -except OSError: - pass -except subprocess.CalledProcessError: - pass + +def add_svn_segment(powerline): + """Wraps _add_svn_segment in exception handling.""" + + # FIXME This function was added when introducing a testing framework, + # during which the 'powerline' object was passed into the + # `add_[segment]_segment` functions instead of being a global variable. At + # that time it was unclear whether the below exceptions could actually be + # thrown. It would be preferable to find out whether they ever will. If so, + # write a comment explaining when. Otherwise remove. + + try: + _add_svn_segment(powerline) + except OSError: + pass + except subprocess.CalledProcessError: + pass diff --git a/segments/time.py b/segments/time.py index 68431504..8c0313f6 100644 --- a/segments/time.py +++ b/segments/time.py @@ -1,4 +1,4 @@ -def add_time_segment(): +def add_time_segment(powerline): if powerline.args.shell == 'bash': time = ' \\t ' elif powerline.args.shell == 'zsh': @@ -8,5 +8,3 @@ def add_time_segment(): time = ' %s ' % time.strftime('%H:%M:%S') powerline.append(time, Color.HOSTNAME_FG, Color.HOSTNAME_BG) - -add_time_segment() diff --git a/segments/uptime.py b/segments/uptime.py index 8ea229f4..4c7711ce 100644 --- a/segments/uptime.py +++ b/segments/uptime.py @@ -1,15 +1,7 @@ import subprocess import re -# uptime output samples -# 1h: 00:00:00 up 1:00, 2 users, load average: 0,00, 0,00, 0,00 -# 10+h: 00:00:00 up 10:00, 2 users, load average: 0,00, 0,00, 0,00 -# 1+d: 00:00:00 up 1 days, 1:00, 2 users, load average: 0,00, 0,00, 0,00 -# 9+d: 00:00:00 up 12 days, 1:00, 2 users, load average: 0,00, 0,00, 0,00 -# -1h 00:00:00 up 120 days, 49 min, 2 users, load average: 0,00, 0,00, 0,00 -# mac: 00:00:00 up 23 3 day(s), 10:00, 2 users, load average: 0,00, 0,00, 0,00 - -def add_uptime_segment(): +def add_uptime_segment(powerline): try: output = subprocess.check_output(['uptime'], stderr=subprocess.STDOUT) raw_uptime = re.search('(?<=up).+(?=,\s+\d+\s+user)', output).group(0) @@ -22,5 +14,3 @@ def add_uptime_segment(): powerline.append(uptime, Color.CWD_FG, Color.PATH_BG) except OSError: return - -add_uptime_segment() diff --git a/segments/username.py b/segments/username.py index 4d8e5755..3d73a12c 100644 --- a/segments/username.py +++ b/segments/username.py @@ -1,5 +1,5 @@ -def add_username_segment(): +def add_username_segment(powerline): import os if powerline.args.shell == 'bash': user_prompt = ' \\u ' @@ -14,5 +14,3 @@ def add_username_segment(): bgcolor = Color.USERNAME_BG powerline.append(user_prompt, Color.USERNAME_FG, bgcolor) - -add_username_segment() diff --git a/segments/virtual_env.py b/segments/virtual_env.py index d8937e63..710efc69 100644 --- a/segments/virtual_env.py +++ b/segments/virtual_env.py @@ -1,6 +1,6 @@ import os -def add_virtual_env_segment(): +def add_virtual_env_segment(powerline): env = os.getenv('VIRTUAL_ENV') or os.getenv('CONDA_ENV_PATH') if env is None: return @@ -9,5 +9,3 @@ def add_virtual_env_segment(): bg = Color.VIRTUAL_ENV_BG fg = Color.VIRTUAL_ENV_FG powerline.append(' %s ' % env_name, fg, bg) - -add_virtual_env_segment() diff --git a/test/segments_test/git_test.py b/test/segments_test/git_test.py new file mode 100644 index 00000000..982289e3 --- /dev/null +++ b/test/segments_test/git_test.py @@ -0,0 +1,69 @@ +import unittest +import mock +import tempfile +import shutil +import sh +import segments.git as git + + +class GitTest(unittest.TestCase): + + def setUp(self): + self.powerline = mock.MagicMock() + git.Color = mock.MagicMock() + + self.dirname = tempfile.mkdtemp() + sh.cd(self.dirname) + sh.git("init", ".") + + def tearDown(self): + shutil.rmtree(self.dirname) + + def _add_and_commit(self, filename): + sh.touch(filename) + sh.git("add", filename) + sh.git("commit", "-m", "add file " + filename) + + def _new_branch(self, branch): + sh.git("checkout", "-b", branch) + + def _get_commit_hash(self): + return sh.git("rev-parse", "HEAD") + + @mock.patch('segments.git.get_PATH') + def test_git_not_installed(self, get_PATH): + get_PATH.return_value = "" # so git can't be found + git.add_git_segment(self.powerline) + self.assertEqual(self.powerline.append.call_count, 0) + + def test_non_git_directory(self): + shutil.rmtree(".git") + git.add_git_segment(self.powerline) + self.assertEqual(self.powerline.append.call_count, 0) + + def test_big_bang(self): + git.add_git_segment(self.powerline) + self.assertEqual(self.powerline.append.call_args[0][0], ' Big Bang ') + + def test_master_branch(self): + self._add_and_commit("foo") + git.add_git_segment(self.powerline) + self.assertEqual(self.powerline.append.call_args[0][0], ' master ') + + def test_different_branch(self): + self._add_and_commit("foo") + self._new_branch("bar") + git.add_git_segment(self.powerline) + self.assertEqual(self.powerline.append.call_args[0][0], ' bar ') + + def test_detached(self): + self._add_and_commit("foo") + commit_hash = self._get_commit_hash() + self._add_and_commit("bar") + sh.git("checkout", "HEAD^") + git.add_git_segment(self.powerline) + + # In detached mode, we output a unicode symbol and then the shortened + # commit hash. + self.assertIn(self.powerline.append.call_args[0][0].split()[1], + commit_hash) diff --git a/test/segments_test/uptime_test.py b/test/segments_test/uptime_test.py new file mode 100644 index 00000000..d2dacd2c --- /dev/null +++ b/test/segments_test/uptime_test.py @@ -0,0 +1,29 @@ +import unittest +import mock +import segments.uptime as uptime + +test_cases = { + # linux test cases + "00:00:00 up 1:00, 2 users, load average: 0,00, 0,00, 0,00": "1h", + "00:00:00 up 10:00, 2 users, load average: 0,00, 0,00, 0,00": "10h", + "00:00:00 up 1 days, 1:00, 2 users, load average: 0,00, 0,00, 0,00": "1d", + "00:00:00 up 12 days, 1:00, 2 users, load average: 0,00, 0,00, 0,00": "12d", + "00:00:00 up 120 days, 49 min, 2 users, load average: 0,00, 0,00, 0,00": "120d", + + # mac test cases + "00:00:00 up 23 3 day(s), 10:00, 2 users, load average: 0,00, 0,00, 0,00": "3d", +} + + +class UptimeTest(unittest.TestCase): + + def setUp(self): + self.powerline = mock.MagicMock() + uptime.Color = mock.MagicMock() + + @mock.patch('subprocess.check_output') + def test_all(self, check_output): + for stdout, result in test_cases.items(): + check_output.return_value = stdout + uptime.add_uptime_segment(self.powerline) + self.assertEqual(self.powerline.append.call_args[0][0].split()[0], result)