Skip to content

Commit

Permalink
Switch to nose2 for testing and mild doc cleanup
Browse files Browse the repository at this point in the history
The orignal Nose project is deprecated and vaguely unmaintained. Switch
to nose2, which is its successor.

Tag v0.15 and release.
  • Loading branch information
klausman committed Nov 1, 2021
1 parent c414071 commit 96ee3c4
Show file tree
Hide file tree
Showing 7 changed files with 75 additions and 48 deletions.
26 changes: 13 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Lib_users
# `Lib_users`

Lib_users is a Python script that goes through `/proc/*/maps` and finds all
`Lib_users` is a Python script that goes through `/proc/*/maps` and finds all
cases of libraries being mapped but marked as deleted. It then extracts the
programs name and arguments from `/proc/<pid>/cmdline`. This information is
presented to the user so that those processes can be restarted.
Expand All @@ -25,7 +25,7 @@ deleted (or rotated and compressed), but not told to reopen the file.

## Output formats

Lib_users supports two output formats/modes, human- and machine-readable:
`Lib_users` supports two output formats/modes, human- and machine-readable:

human-readable:

Expand Down Expand Up @@ -65,8 +65,8 @@ installation, there are no external dependencies.

If you want to run the test suite easily, install the Nose Python testing
framework. This is not needed for day-to-day operations. Running the tests with
Python 2.7 also requires the backported mock submodule of unittest
(https://github.com/jaraco/backports.unittest_mock)
Python 2.7 also requires the backported mock submodule of
[unittest](https://github.com/jaraco/backports.unittest_mock)

## Limitations

Expand All @@ -76,7 +76,7 @@ that have the same maps file structure as a Linux system).
If the script is not run as root, it can not display all processes on the
system that use deleted libs. In the spirit of graceful degradation, it will
then only display the information for processes it has access to. Usually this
is the list of processes owned by the user that runs lib_users. It will also
is the list of processes owned by the user that runs `lib_users`. It will also
output a warning to stderr that it could not read all map files.

The `-S` command line switch relies on systemdctl and its output. Therefore, it
Expand All @@ -85,16 +85,16 @@ that the output it produces is advisory and entirely reliant on systemd.

## False positives

Some programs open temporary files and immediately delete them. This is done
so they don't leave those files behind after a crash. As a consequence,
lib_users may report these programs. A notable example are programs that use
Some programs open temporary files and immediately delete them. This is done so
they don't leave those files behind after a crash. As a consequence,
`lib_users` may report these programs. A notable example are programs that use
liborc, the Oil Runtime Compiler. Here's an example, the media player
quodlibet:

```
$ lib_users
$ lib_users
2753 "/usr/bin/python2.7 /usr/bin/quodlibet"
$ grep deleted /proc/2753/maps
$ grep deleted /proc/2753/maps
7f409dd0b000-7f409dd1b000 rw-s 00000000 08:01 1179661 /tmp/orcexec.sqa9cE (deleted)
```

Expand All @@ -105,8 +105,8 @@ result in a different liborc tempfile show up as deleted.
Using the -s command line option will show you which deleted files are in use,
so you decide whether the listed process is a false positive.

Starting with lib_users 0.8, the `-i` and `-I` command line options can be used to
supply additional to-be-ignored patterns and static strings.
Starting with `lib_users` v0.8, the `-i` and `-I` command line options can be
used to supply additional to-be-ignored patterns and static strings.

## License

Expand Down
2 changes: 1 addition & 1 deletion fd_users.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
Warning: Some files could not be read. Note that fd_users has to be run as
root to get a full list of deleted in-use libraries.\n"""

__version__ = "0.14"
__version__ = "0.15"


def get_deleted_files(fddir, ign_patterns, ign_literals):
Expand Down
2 changes: 1 addition & 1 deletion lib_users.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
Warning: Some files could not be read. Note that lib_users has to be run as
root to get a full list of deleted in-use libraries.\n"""

__version__ = "0.14"
__version__ = "0.15"

# These are no true libs so don't make our process a deleted libs user
# The first set is patterns, i.e. they are compared using fnmatch()
Expand Down
69 changes: 40 additions & 29 deletions lib_users_util/test_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"""
Test suite for common
To be run through nose, not executed directly.
To be run through nose2, not executed directly.
"""
import os
import sys
Expand All @@ -28,6 +28,12 @@
EMPTYSET = frozenset()


class _mock_stdx(object):
"""A stand-in for sys.stdout/stderr"""

def write(self, *_, **_unused):
"""Discard everything"""

class _options(object):
"""Mock options object that mimicks the bare necessities"""

Expand All @@ -42,6 +48,18 @@ def __init__(self):

class TestGetProgargs(unittest.TestCase):

def setUp(self):
self._comm = common
self._orig_stderr = self._comm.sys.stderr
self._orig_stdout = self._comm.sys.stderr

self._comm.sys.stderr = _mock_stdx()
self._comm.sys.stdout = _mock_stdx()

def tearDown(self):
self._comm.sys.stderr = self._orig_stderr
self._comm.sys.stdout = self._orig_stdout

def test_progargs(self):
"""Test length of argv using string pid"""
m = unittest.mock.mock_open(read_data="x\x00b")
Expand All @@ -65,23 +83,19 @@ def test_fmt_human(self):
options = _options()
inp = {"argv1": (set(["1", "2"]), set(["l1", "l2"]))}
outp = '1,2 "argv1"'
print(common.fmt_human(inp, options))
self.assertEqual(common.fmt_human(inp, options), outp)

inp = {"argv1": (set(["1"]), set(["l1", "l2"]))}
outp = '1 "argv1"'
print(common.fmt_human(inp, options))
self.assertEqual(common.fmt_human(inp, options), outp)

# The space at the end of this argv should go away.
inp = {"argv1 argv2 ": (set(["1"]), set(["l1", "l2"]))}
outp = '1 "argv1 argv2"'
print(common.fmt_human(inp, options))
self.assertEqual(common.fmt_human(inp, options), outp)

inp = {}
outp = ''
print(common.fmt_human(inp, options))
self.assertEqual(common.fmt_human(inp, options), outp)

def test_fmt_human_with_libs(self):
Expand All @@ -90,18 +104,15 @@ def test_fmt_human_with_libs(self):
outp = '1,2 "argv1" uses l1,l2'
options = _options()
options.showitems = True
print(common.fmt_human(inp, options))
self.assertEqual(common.fmt_human(inp, options), outp)

inp = {"argv1": (set(["1"]), set(["l1"]))}
outp = '1 "argv1" uses l1'
print(common.fmt_human(inp, options))
self.assertEqual(common.fmt_human(inp, options), outp)

# The space at the end of this argv should go away.
inp = {"argv1 argv2 ": (set(["1"]), set(["l1", "l2"]))}
outp = '1 "argv1 argv2" uses l1,l2'
print(common.fmt_human(inp, options))
self.assertEqual(common.fmt_human(inp, options), outp)

inp = {}
Expand All @@ -113,28 +124,23 @@ def test_fmt_machine(self):
"""Test function for machine-readable output"""
inp = {"argv1": (set(["1", "2"]), set(["l1", "l2"]))}
outp = '1,2;l1,l2;argv1'
print(common.fmt_machine(inp))
self.assertEqual(common.fmt_machine(inp), outp)

inp = {"argv1": (set(["1"]), set(["l1", "l2"]))}
outp = '1;l1,l2;argv1'
print(common.fmt_machine(inp))
self.assertEqual(common.fmt_machine(inp), outp)

# The space at the end of this argv should go away.
inp = {"argv1 argv2 ": (set(["1"]), set(["l1", "l2"]))}
outp = '1;l1,l2;argv1 argv2'
print(common.fmt_machine(inp))
self.assertEqual(common.fmt_machine(inp), outp)

inp = {"argv1 argv2 ": (set(["1"]), set())}
outp = '1;;argv1 argv2'
print(common.fmt_machine(inp))
self.assertEqual(common.fmt_machine(inp), outp)

inp = {}
outp = ''
print(common.fmt_machine(inp))
self.assertEqual(common.fmt_machine(inp), outp)


Expand All @@ -144,18 +150,23 @@ class Testsystemdintegration(unittest.TestCase):

def setUp(self):
"""Set up golden data and save original function refs"""
self.comm = common
self._comm = common
self.query = {"/usr/bin/foo": (("1", "2", "3"), ("libbar", "libbaz"))}
self.golden = "1,2,3 belong to service.shmervice"
self._orig_query_systemctl = self.comm.query_systemctl
self._orig_Popen = self.comm.subprocess.Popen
self._orig_stderr = self.comm.sys.stderr
self._orig_query_systemctl = self._comm.query_systemctl
self._orig_Popen = self._comm.subprocess.Popen
self._orig_stderr = self._comm.sys.stderr
self._orig_stdout = self._comm.sys.stderr

self._comm.sys.stderr = _mock_stdx()
self._comm.sys.stdout = _mock_stdx()

def tearDown(self):
"""Restore mocked out functions"""
self.comm.query_systemctl = self._orig_query_systemctl
self.comm.subprocess.Popen = self._orig_Popen
self.comm.sys.stderr = self._orig_stderr
self._comm.query_systemctl = self._orig_query_systemctl
self._comm.subprocess.Popen = self._orig_Popen
self._comm.sys.stderr = self._orig_stderr
self._comm.sys.stdout = self._orig_stdout

def _mock_query_systemctl(self, _):
"""Mock out query_systemctl, always return "service.shmervice" """
Expand Down Expand Up @@ -193,39 +204,39 @@ def _mock_Popen_broken(self, *_, **_unused):

def test_get_services(self):
"""Test get_services"""
self.comm.query_systemctl = self._mock_query_systemctl
self._comm.query_systemctl = self._mock_query_systemctl
self.assertEqual(common.get_services(self.query), self.golden)

def test_get_services_with_broken_systemctl(self):
"""Test get_services with broken systctl"""
self.comm.query_systemctl = self._mock_query_systemctl_broken
self._comm.query_systemctl = self._mock_query_systemctl_broken
self.assertIn("Dummy Reason", common.get_services(self.query))

def test_query_systemctl(self):
"""Test test_query_systemctl with mocked Popen"""
self.comm.subprocess.Popen = self._mock_Popen
ret = self.comm.query_systemctl("1")
self._comm.subprocess.Popen = self._mock_Popen
ret = self._comm.query_systemctl("1")
self.assertEqual(ret, "sshd.service")

def test_query_systemctl_broken(self):
"""Test test_query_systemctl with mocked broken Popen"""
self.comm.subprocess.Popen = self._mock_Popen_broken
self._comm.subprocess.Popen = self._mock_Popen_broken
with self.assertRaises(OSError):
self.comm.query_systemctl("1")
self._comm.query_systemctl("1")

def test_format1(self):
"""Test "classic" output format of systemctl status"""
retval = self.comm.query_systemctl("1",
retval = self._comm.query_systemctl("1",
"sshd.service - OpenSSH Daemon")
self.assertEqual(retval, "sshd.service")

def test_format2(self):
"""Test first iteration output format of systemctl status"""
retval = self.comm.query_systemctl(
retval = self._comm.query_systemctl(
"1", "● sshd.service - OpenSSH Daemon")
self.assertEqual(retval, "sshd.service")

def test_no_match(self):
retval = self.comm.query_systemctl(
retval = self._comm.query_systemctl(
"1", "No unit for PID 1 is loaded.\nBlah")
self.assertEqual(retval, None)
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from distutils.core import setup

setup(name='lib_users',
version='0.14',
version='0.15',
description='Checks /proc for libraries and files being mapped/open '
'but marked as deleted',
author='Tobias Klausmann',
Expand Down
10 changes: 9 additions & 1 deletion test_fdusers.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""
Test suite for fd_users
To be run through nose, not executed directly.
To be run through nose2, not executed directly.
"""
# -*- coding: utf8 -*-
import locale
Expand Down Expand Up @@ -48,6 +48,11 @@ def setUp(self):
self.f_u = fd_users
self._orig_glob_glob = self.f_u.glob.glob
self._orig_os_readlink = self.f_u.os.readlink
self._orig_stderr = self.f_u.sys.stderr
self._orig_stdout = self.f_u.sys.stderr

self.f_u.sys.stderr = _mock_stdx()
self.f_u.sys.stdout = _mock_stdx()

def tearDown(self):
"""Restore mocked out functions"""
Expand Down Expand Up @@ -162,10 +167,13 @@ def setUp(self):
self._orig_get_deleted_files = self.f_u.get_deleted_files
self._orig_get_progargs = self.f_u.common.get_progargs
self._orig_stderr = self.f_u.sys.stderr
self._orig_stdout = self.f_u.sys.stderr

self.f_u.get_deleted_files = self._mock_get_deleted_files
self.f_u.common.get_progargs = self._mock_get_progargs

self.f_u.sys.stderr = _mock_stdx()
self.f_u.sys.stdout = _mock_stdx()

def tearDown(self):
"""Restore mocked out functions"""
Expand Down
12 changes: 10 additions & 2 deletions test_libusers.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""
Test suite for lib_users
To be run through nose, not executed directly.
To be run through nose2, not executed directly.
"""
# -*- coding: utf8 -*-
import sys
Expand Down Expand Up @@ -47,11 +47,14 @@ class Testlibusers(unittest.TestCase):
def setUp(self):
self.l_u = lib_users
self._orig_stderr = self.l_u.sys.stderr
self._orig_stdout = self.l_u.sys.stderr

self.l_u.sys.stderr = _mock_stdx()
self.l_u.sys.stdout = _mock_stdx()

def tearDown(self):
self.l_u.sys.stderr = self._orig_stderr
self.l_u.sys.stdout = self._orig_stdout

def test_nonlibs(self):
"""Test detection of mappings that aren't libs"""
Expand Down Expand Up @@ -179,6 +182,10 @@ def setUp(self):
self._orig_get_deleted_libs = self.l_u.get_deleted_libs
self._orig_get_progargs = self.l_u.common.get_progargs
self._orig_stderr = self.l_u.sys.stderr
self._orig_stdout = self.l_u.sys.stderr

self.l_u.sys.stderr = _mock_stdx()
self.l_u.sys.stdout = _mock_stdx()

self.l_u.get_deleted_libs = self._mock_get_deleted_libs
self.l_u.common.get_progargs = self._mock_get_progargs
Expand All @@ -187,8 +194,9 @@ def setUp(self):
def tearDown(self):
"""Restore mocked out functions"""
self.l_u.get_deleted_libs = self._orig_get_deleted_libs
self.l_u.get_progargs = self._orig_get_progargs
self.l_u.common.get_progargs = self._orig_get_progargs
self.l_u.sys.stderr = self._orig_stderr
self.l_u.sys.stdout = self._orig_stdout

def _mock_get_deleted_libs(*unused_args):
"""Mock out get_deleted_files, always returns set(["foo"])"""
Expand Down

0 comments on commit 96ee3c4

Please sign in to comment.