Repository: TheUncleKai/bbutils

Issue:
Add a simple execute wrapper
Add a simple execute wrapper with stdout/stdin catcher.

Comments:

Patch:
diff --git a/bbutil/__init__.py b/bbutil/__init__.py
index 0c14292..351809b 100644
--- a/bbutil/__init__.py
+++ b/bbutil/__init__.py
@@ -26,6 +26,7 @@ __all__ = [
     "worker",
 
     "data",
+    "execute",
     "file",
     "utils",
 
diff --git a/bbutil/execute.py b/bbutil/execute.py
new file mode 100644
index 0000000..d274556
--- /dev/null
+++ b/bbutil/execute.py
@@ -0,0 +1,147 @@
+#!/usr/bin/python3
+# coding=utf-8
+
+# Copyright (C) 2020, Siemens Healthcare Diagnostics Products GmbH
+# Licensed under the Siemens Inner Source License 1.2, see LICENSE.md.
+
+import subprocess
+
+from typing import Optional, List
+
+import bbutil
+
+__all__ = [
+    "Execute"
+]
+
+
+class Execute(object):
+
+    def __init__(self, **kwargs):
+        self.name: str = ""
+        self.desc: str = ""
+        self.commands: List[str] = []
+        self.messages: List[str] = []
+        self.returncode: int = 0
+        self.errors: Optional[List[str]] = None
+
+        self.stdout: Optional[subprocess.PIPE] = subprocess.PIPE
+        self.stderr: Optional[subprocess.PIPE] = subprocess.PIPE
+        self.stdin: Optional[subprocess.PIPE] = None
+        self.call_stdout = None
+        self.call_stderr = None
+
+        self.setup(**kwargs)
+        return
+
+    def show_command(self):
+        line = ""
+
+        for _item in self.commands:
+            if line == "":
+                line = _item
+            else:
+                line = "{0:s} {1:s}".format(line, _item)
+
+        bbutil.log.debug1(self.name, line)
+        return
+
+    @staticmethod
+    def _convert_line(data) -> str:
+        try:
+            new = data.decode(encoding='utf-8')
+        except UnicodeDecodeError:
+            return ""
+        new = new.replace("\r\n", "")
+        new = new.replace("\n", "")
+        return new
+
+    def setup(self, **kwargs):
+        item = kwargs.get("commands", None)
+        if item is not None:
+            self.commands = item
+
+        item = kwargs.get("name", None)
+        if item is not None:
+            self.name = item
+
+        item = kwargs.get("desc", None)
+        if item is not None:
+            self.desc = item
+
+        item = kwargs.get("stdout", None)
+        if item is not None:
+            self.stdout = item
+
+        item = kwargs.get("stderr", None)
+        if item is not None:
+            self.stderr = item
+
+        item = kwargs.get("stdin", None)
+        if item is not None:
+            self.stdin = item
+
+        item = kwargs.get("call_stdout", None)
+        if item is not None:
+            self.call_stdout = item
+
+        item = kwargs.get("call_stderr", None)
+        if item is not None:
+            self.call_stderr = item
+        return
+
+    def execute(self) -> bool:
+        self.show_command()
+
+        self.messages = []
+
+        if self.stdin is not None:
+            p = subprocess.Popen(self.commands, stdout=self.stdout, stderr=self.stderr, stdin=self.stdin)
+        else:
+            p = subprocess.Popen(self.commands, stdout=self.stdout, stderr=self.stderr)
+
+        #  parse output and wait for end
+        while True:
+            if p.stdout is not None:
+                for line in p.stdout:
+                    if line is None:
+                        continue
+                    data = self._convert_line(line)
+                    self.messages.append(data)
+
+                    if self.call_stdout is not None:
+                        self.call_stdout(data)
+
+            if p.stderr is not None:
+                for line in p.stderr:
+                    if line is None:
+                        continue
+                    data = self._convert_line(line)
+
+                    if self.errors is None:
+                        self.errors = []
+
+                    self.errors.append(data)
+
+                    if self.call_stderr is not None:
+                        self.call_stderr(data)
+
+            _poll = p.poll()
+            if _poll is not None:
+                break
+
+        self.returncode = p.returncode
+
+        if p.returncode != 0:
+            bbutil.log.clear()
+            bbutil.log.error("{0:s} failed!".format(self.desc))
+            bbutil.log.error("Process did end with error {0:d}".format(p.returncode))
+            if self.errors is not None:
+                for _line in self.errors:
+                    bbutil.log.error(_line)
+            else:
+                for _line in self.messages:
+                    bbutil.log.error(_line)
+            return False
+
+        return True


Test patch:
diff --git a/tests.json b/tests.json
index 233519e..1937c23 100644
--- a/tests.json
+++ b/tests.json
@@ -370,6 +370,18 @@
                 "test_worker_07",
                 "test_worker_08"
             ]
+        },
+        {
+            "id": "Execute",
+            "path": "tests.execute",
+            "classname": "TestExecute",
+            "tests": [
+                "test_setup_01",
+                "test_setup_02",
+                "test_setup_03",
+                "test_setup_04",
+                "test_setup_05"
+            ]
         }
     ]
 }
diff --git a/tests/__init__.py b/tests/__init__.py
index 614f1cb..b73dd09 100755
--- a/tests/__init__.py
+++ b/tests/__init__.py
@@ -23,6 +23,8 @@ __all__ = [
     "logging",
 
     "data",
+    "execute",
     "file",
-    "utils"
+    "utils",
+    "worker"
 ]
diff --git a/tests/execute.py b/tests/execute.py
new file mode 100644
index 0000000..d8c0bbd
--- /dev/null
+++ b/tests/execute.py
@@ -0,0 +1,138 @@
+#!/usr/bin/python3
+# -*- coding: utf-8 -*-
+#
+#    Licensed under the Apache License, Version 2.0 (the "License");
+#    you may not use this file except in compliance with the License.
+#    You may obtain a copy of the License at
+#
+#        http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS,
+#    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#    See the License for the specific language governing permissions and
+#    limitations under the License.
+#
+#    Copyright (C) 2017, Kai Raphahn <kai.raphahn@laburec.de>
+#
+
+import unittest
+
+import unittest.mock as mock
+
+from bbutil.execute import Execute
+
+from tests.helper import set_log
+from tests.helper.execute import CatchBacks, MockPopen1, MockPopen2, MockPopen3
+
+__all__ = [
+    "TestExecute"
+]
+
+
+oserror = OSError("Something strange did happen!")
+mock_oserror = mock.Mock(side_effect=oserror)
+mock_remove = mock.Mock()
+
+
+class TestExecute(unittest.TestCase):
+    """Testing class for locking module."""
+
+    def setUp(self):
+        set_log()
+        return
+
+    def test_setup_01(self):
+        _execute = Execute()
+
+        _commands = [
+            "/usr/bin/ls"
+        ]
+
+        _execute.setup(name="Test", desc="Print ls", commands=_commands)
+
+        _check = _execute.execute()
+        self.assertTrue(_check)
+        self.assertEqual(_execute.returncode, 0)
+        self.assertIsNone(_execute.errors)
+        self.assertGreater(len(_execute.messages), 1)
+        return
+
+    def test_setup_02(self):
+        _commands = [
+            "/usr/bin/ls"
+        ]
+
+        _execute = Execute(name="Test", desc="Print ls", commands=_commands)
+
+        _check = _execute.execute()
+        self.assertTrue(_check)
+        self.assertEqual(_execute.returncode, 0)
+        self.assertIsNone(_execute.errors)
+        self.assertGreater(len(_execute.messages), 1)
+        return
+
+    @mock.patch('subprocess.Popen', new=MockPopen1)
+    def test_setup_03(self):
+
+        _execute = Execute()
+        _commands = [
+            "/usr/bin/ls",
+            "-lA"
+        ]
+
+        _execute.setup(name="Test", desc="Print ls", commands=_commands, stdout="TEST", stderr="TEST", stdin="TEST")
+        _execute.show_command()
+
+        _check = _execute.execute()
+        self.assertTrue(_check)
+        self.assertEqual(_execute.returncode, 0)
+        self.assertIsNone(_execute.errors)
+        self.assertGreater(len(_execute.messages), 1)
+        return
+
+    @mock.patch('subprocess.Popen', new=MockPopen2)
+    def test_setup_04(self):
+
+        _callbacks = CatchBacks()
+
+        _execute = Execute()
+        _commands = [
+            "/usr/bin/ls"
+        ]
+
+        _execute.setup(name="Test", desc="Print ls", commands=_commands,
+                       call_stdout=_callbacks.add_stdout, call_stderr=_callbacks.add_stderr)
+
+        _check = _execute.execute()
+
+        self.assertFalse(_check)
+        self.assertEqual(_execute.returncode, 1)
+        self.assertIsNotNone(_execute.errors)
+        self.assertGreater(len(_execute.messages), 1)
+        self.assertEqual(len(_callbacks.stdout), 22)
+        self.assertEqual(len(_callbacks.stderr), 11)
+        return
+
+    @mock.patch('subprocess.Popen', new=MockPopen3)
+    def test_setup_05(self):
+
+        _callbacks = CatchBacks()
+
+        _execute = Execute()
+        _commands = [
+            "/usr/bin/ls"
+        ]
+
+        _execute.setup(name="Test", desc="Print ls", commands=_commands,
+                       call_stdout=_callbacks.add_stdout, call_stderr=_callbacks.add_stderr)
+
+        _check = _execute.execute()
+
+        self.assertFalse(_check)
+        self.assertEqual(_execute.returncode, 1)
+        self.assertIsNone(_execute.errors)
+        self.assertGreater(len(_execute.messages), 1)
+        self.assertEqual(len(_callbacks.stdout), 22)
+        self.assertEqual(len(_callbacks.stderr), 0)
+        return
diff --git a/tests/helper/__init__.py b/tests/helper/__init__.py
index 71630bf..c711b7c 100644
--- a/tests/helper/__init__.py
+++ b/tests/helper/__init__.py
@@ -26,6 +26,7 @@ from bbutil.utils import full_path
 
 __all__ = [
     "database",
+    "execute",
     "file",
     "sqlite",
     "table",
diff --git a/tests/helper/execute.py b/tests/helper/execute.py
new file mode 100644
index 0000000..97aa0a4
--- /dev/null
+++ b/tests/helper/execute.py
@@ -0,0 +1,138 @@
+#!/usr/bin/python3
+# -*- coding: utf-8 -*-
+#
+#    Licensed under the Apache License, Version 2.0 (the "License");
+#    you may not use this file except in compliance with the License.
+#    You may obtain a copy of the License at
+#
+#        http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS,
+#    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#    See the License for the specific language governing permissions and
+#    limitations under the License.
+#
+#    Copyright (C) 2017, Kai Raphahn <kai.raphahn@laburec.de>
+#
+
+from typing import Optional
+from unittest.mock import Mock
+
+__all__ = [
+    "CatchBacks",
+    "MockPopen1",
+    "MockPopen2",
+    "MockPopen3"
+]
+
+
+class CatchBacks(object):
+
+    def __init__(self):
+        self.stdout = []
+        self.stderr = []
+        return
+
+    def add_stdout(self, data):
+        if data is None:
+            return
+        self.stdout.append(data)
+        return
+
+    def add_stderr(self, data):
+        if data is None:
+            return
+        self.stderr.append(data)
+        return
+
+
+def get_stdout() -> list:
+
+    _line1 = "TEST"
+
+    _excec = UnicodeDecodeError('funnycodec', _line1.encode(), 1, 2, 'This is just a fake reason!')
+
+    _line2 = Mock()
+    _line2.decode = Mock(side_effect=_excec)
+
+    _stdout = [
+        _line1.encode(),
+        _line2,
+        None
+    ]
+    return _stdout
+
+
+def get_stderr() -> list:
+    _line1 = "ERROR!"
+
+    _stderr = [
+        _line1.encode(),
+        None
+    ]
+
+    return _stderr
+
+
+class MockPopen1(object):
+
+    def __init__(self, commands, stdout, stderr, stdin=None):
+
+        self.test_stdout = stdout
+        self.test_stderr = stderr
+        self.test_stdin = stdin
+        self._poll = 0
+        self.stdout = get_stdout()
+        self.stderr = []
+        self.returncode = 0
+        return
+
+    def poll(self) -> Optional[int]:
+        if self._poll == 10:
+            self._poll = 0
+            return 1
+        self._poll += 1
+        return None
+
+
+class MockPopen2(object):
+
+    def __init__(self, commands, stdout, stderr, stdin=None):
+
+        self.test_stdout = stdout
+        self.test_stderr = stderr
+        self.test_stdin = stdin
+        self._poll = 0
+        self.stdout = get_stdout()
+        self.stderr = get_stderr()
+        self.returncode = 1
+        return
+
+    def poll(self) -> Optional[int]:
+        if self._poll == 10:
+            self._poll = 0
+            return 1
+        self._poll += 1
+        return None
+
+
+class MockPopen3(object):
+
+    def __init__(self, commands, stdout, stderr, stdin=None):
+
+        self.test_stdout = stdout
+        self.test_stderr = stderr
+        self.test_stdin = stdin
+        self._poll = 0
+        self.stdout = get_stdout()
+        self.stderr = []
+        self.returncode = 1
+        return
+
+    def poll(self) -> Optional[int]:
+        if self._poll == 10:
+            self._poll = 0
+            return 1
+        self._poll += 1
+        return None