Repository: opsdroid/opsdroid

Issue:
Add windows support
Currently opsdroid does not support native running on windows. 

There are some assumptions around handling signals which are probably preventing it from working. (See #352 for example).

It would be great to get this working on windows.


Discussion:

Resolving patch:
diff --git a/opsdroid/core.py b/opsdroid/core.py
--- a/opsdroid/core.py
+++ b/opsdroid/core.py
@@ -2,6 +2,7 @@
 
 import copy
 import logging
+import os
 import signal
 import sys
 import weakref
@@ -40,8 +41,9 @@ def __init__(self):
         self.connectors = []
         self.connector_tasks = []
         self.eventloop = asyncio.get_event_loop()
-        for sig in (signal.SIGINT, signal.SIGTERM):
-            self.eventloop.add_signal_handler(sig, self.call_stop)
+        if os.name != 'nt':
+            for sig in (signal.SIGINT, signal.SIGTERM):
+                self.eventloop.add_signal_handler(sig, self.call_stop)
         self.skills = []
         self.memory = Memory()
         self.loader = Loader(self)
diff --git a/opsdroid/helper.py b/opsdroid/helper.py
--- a/opsdroid/helper.py
+++ b/opsdroid/helper.py
@@ -1,5 +1,8 @@
 """Helper functions to use within OpsDroid."""
 
+import os
+import stat
+
 
 def get_opsdroid():
     """Return the running opsdroid instance."""
@@ -8,3 +11,9 @@ def get_opsdroid():
         return OpsDroid.instances[0]
 
     return None
+
+
+def del_rw(action, name, exc):
+    """Error handler for removing read only files."""
+    os.chmod(name, stat.S_IWRITE)
+    os.remove(name)
diff --git a/opsdroid/loader.py b/opsdroid/loader.py
--- a/opsdroid/loader.py
+++ b/opsdroid/loader.py
@@ -82,8 +82,9 @@ def build_module_path(self, path_type, config):
             path = MODULES_DIRECTORY + "." + config["type"] + \
                         "." + config["name"]
         elif path_type == "install":
-            path = self.modules_directory + "/" + config["type"] + \
-                        "/" + config["name"]
+            path = os.path.join(self.modules_directory,
+                                config["type"],
+                                config["name"])
         return path
 
     @staticmethod
@@ -118,7 +119,7 @@ def pip_install_deps(requirements_path):
 
     @staticmethod
     def _load_intents(config):
-        intent_file = config["install_path"] + "/intents.md"
+        intent_file = os.path.join(config["install_path"], "intents.md")
         if os.path.isfile(intent_file):
             with open(intent_file, 'r') as intent_file_handle:
                 intents = intent_file_handle.read()
@@ -314,9 +315,10 @@ def _install_module(self, config):
             _LOGGER.error("Install of %s failed.", config["name"])
 
         # Install module dependencies
-        if os.path.isfile(config["install_path"] + "/requirements.txt"):
-            self.pip_install_deps(config["install_path"] +
-                                  "/requirements.txt")
+        if os.path.isfile(os.path.join(
+                config["install_path"], "requirements.txt")):
+            self.pip_install_deps(os.path.join(config["install_path"],
+                                               "requirements.txt"))
 
     def _install_git_module(self, config):
         """Install a module from a git repository."""
@@ -358,8 +360,9 @@ def _install_local_module(config):
 
         if os.path.isfile(config["path"]):
             os.makedirs(config["install_path"], exist_ok=True)
-            shutil.copyfile(config["path"], config["install_path"] +
-                            "/__init__.py")
+            shutil.copyfile(config["path"],
+                            os.path.join(config["install_path"],
+                                         "__init__.py"))
             installed = True
 
         if not installed:


Test patch:
diff --git a/tests/test_core.py b/tests/test_core.py
--- a/tests/test_core.py
+++ b/tests/test_core.py
@@ -112,6 +112,7 @@ def test_start_databases(self):
     def test_start_connectors(self):
         with OpsDroid() as opsdroid:
             opsdroid.start_connector_tasks([])
+
             module = {}
             module["config"] = {}
             module["module"] = importlib.import_module(
@@ -125,6 +126,7 @@ def test_start_connectors(self):
     def test_start_connectors_not_implemented(self):
         with OpsDroid() as opsdroid:
             opsdroid.start_connector_tasks([])
+
             module = {}
             module["config"] = {}
             module["module"] = importlib.import_module(
diff --git a/tests/test_helper.py b/tests/test_helper.py
--- a/tests/test_helper.py
+++ b/tests/test_helper.py
@@ -1,6 +1,16 @@
 
 import unittest
+import unittest.mock as mock
+
+from opsdroid.helper import del_rw
 
 
 class TestHelper(unittest.TestCase):
     """Test the opsdroid helper classes."""
+
+    def test_del_rw(self):
+        with mock.patch('os.chmod') as mock_chmod,\
+                mock.patch('os.remove') as mock_remove:
+            del_rw(None, None, None)
+            self.assertTrue(mock_chmod.called)
+            self.assertTrue(mock_remove.called)
diff --git a/tests/test_loader.py b/tests/test_loader.py
--- a/tests/test_loader.py
+++ b/tests/test_loader.py
@@ -1,12 +1,14 @@
 
 import os
 import shutil
-from types import ModuleType
+import subprocess
+import tempfile
 import unittest
 import unittest.mock as mock
-import subprocess
+from types import ModuleType
 
 from opsdroid import loader as ld
+from opsdroid.helper import del_rw
 
 
 class TestLoader(unittest.TestCase):
@@ -18,27 +20,34 @@ def setup(self):
         return opsdroid, loader
 
     def setUp(self):
-        self._tmp_dir = "/tmp/opsdroid_tests"
-        os.makedirs(self._tmp_dir)
+        os.umask(000)
+        self._tmp_dir = os.path.join(tempfile.gettempdir(), "opsdroid_tests")
+        try:
+            os.makedirs(self._tmp_dir, mode=0o777)
+        except FileExistsError:
+            pass
 
     def tearDown(self):
-        shutil.rmtree(self._tmp_dir)
+        shutil.rmtree(self._tmp_dir, onerror=del_rw)
 
     def test_load_config_file(self):
         opsdroid, loader = self.setup()
-        config = loader.load_config_file(["tests/configs/minimal.yaml"])
+        config = loader.load_config_file(
+            [os.path.abspath("tests/configs/minimal.yaml")])
         self.assertIsNotNone(config)
 
     def test_load_config_file_2(self):
         opsdroid, loader = self.setup()
-        config = loader.load_config_file(["tests/configs/minimal_2.yaml"])
+        config = loader.load_config_file(
+            [os.path.abspath("tests/configs/minimal_2.yaml")])
         self.assertIsNotNone(config)
 
     def test_load_config_file_with_include(self):
         opsdroid, loader = self.setup()
         config = loader.load_config_file(
-                                ["tests/configs/minimal_with_include.yaml"])
-        config2 = loader.load_config_file(["tests/configs/minimal.yaml"])
+            [os.path.abspath("tests/configs/minimal_with_include.yaml")])
+        config2 = loader.load_config_file(
+            [os.path.abspath("tests/configs/minimal.yaml")])
         self.assertIsNotNone(config)
         self.assertEqual(config, config2)
 
@@ -46,29 +55,30 @@ def test_load_config_file_with_env_vars(self):
         opsdroid, loader = self.setup()
         os.environ["ENVVAR"] = 'test'
         config = loader.load_config_file(
-            ["tests/configs/minimal_with_envs.yaml"])
+            [os.path.abspath("tests/configs/minimal_with_envs.yaml")])
         self.assertEqual(config["test"], os.environ["ENVVAR"])
 
     def test_create_default_config(self):
-        test_config_path = "/tmp/test_config_path/configuration.yaml"
+        test_config_path = os.path.join(
+            tempfile.gettempdir(), "test_config_path/configuration.yaml")
         opsdroid, loader = self.setup()
 
         self.assertEqual(loader.create_default_config(test_config_path),
                          test_config_path)
         self.assertTrue(os.path.isfile(test_config_path))
-        shutil.rmtree(os.path.split(test_config_path)[0])
+        shutil.rmtree(os.path.split(test_config_path)[0], onerror=del_rw)
 
     def test_generate_config_if_none_exist(self):
         opsdroid, loader = self.setup()
         loader.create_default_config = mock.Mock(
-            return_value="tests/configs/minimal.yaml")
+            return_value=os.path.abspath("tests/configs/minimal.yaml"))
         loader.load_config_file(["file_which_does_not_exist"])
         self.assertTrue(loader.create_default_config.called)
 
     def test_load_non_existant_config_file(self):
         opsdroid, loader = self.setup()
         loader.create_default_config = mock.Mock(
-            return_value="/tmp/my_nonexistant_config")
+            return_value=os.path.abspath("/tmp/my_nonexistant_config"))
         loader.load_config_file(["file_which_does_not_exist"])
         self.assertTrue(loader.create_default_config.called)
         self.assertTrue(loader.opsdroid.critical.called)
@@ -76,21 +86,22 @@ def test_load_non_existant_config_file(self):
     def test_load_broken_config_file(self):
         opsdroid, loader = self.setup()
         loader.opsdroid.critical = mock.Mock()
-        loader.load_config_file(["tests/configs/broken.yaml"])
+        loader.load_config_file(
+            [os.path.abspath("tests/configs/broken.yaml")])
         self.assertTrue(loader.opsdroid.critical.called)
 
     def test_git_clone(self):
         with mock.patch.object(subprocess, 'Popen') as mock_subproc_popen:
             opsdroid, loader = self.setup()
             loader.git_clone("https://github.com/rmccue/test-repository.git",
-                             self._tmp_dir + "/test", "master")
+                             os.path.join(self._tmp_dir, "/test"), "master")
             self.assertTrue(mock_subproc_popen.called)
 
     def test_pip_install_deps(self):
         with mock.patch.object(subprocess, 'Popen') as mocked_popen:
             mocked_popen.return_value.communicate.return_value = ['Test\nTest']
             opsdroid, loader = self.setup()
-            loader.pip_install_deps("/path/to/some/file.txt")
+            loader.pip_install_deps(os.path.abspath("/path/to/some/file.txt"))
             self.assertTrue(mocked_popen.called)
 
     def test_build_module_path(self):
@@ -101,53 +112,59 @@ def test_build_module_path(self):
         loader.modules_directory = ""
         self.assertIn("test.test",
                       ld.Loader.build_module_path(loader, "import", config))
-        self.assertIn("test/test",
+        self.assertIn("test",
                       ld.Loader.build_module_path(loader, "install", config))
 
     def test_check_cache_removes_dir(self):
         config = {}
         config["no-cache"] = True
-        config['install_path'] = self._tmp_dir + "/test/module"
-        os.makedirs(config['install_path'])
+        config['install_path'] = os.path.join(
+            self._tmp_dir, os.path.normpath("test/module"))
+        os.makedirs(config['install_path'], mode=0o777)
         ld.Loader.check_cache(config)
         self.assertFalse(os.path.isdir(config["install_path"]))
 
     def test_check_cache_removes_file(self):
         config = {}
         config["no-cache"] = True
-        config['install_path'] = self._tmp_dir + "/test/module/test"
+        config['install_path'] = os.path.join(
+            self._tmp_dir, os.path.normpath("test/module/test"))
         directory, _ = os.path.split(config['install_path'])
-        os.makedirs(directory)
+        os.makedirs(directory, mode=0o777)
         open(config['install_path'] + ".py", 'w')
         ld.Loader.check_cache(config)
         self.assertFalse(os.path.isfile(config["install_path"] + ".py"))
-        shutil.rmtree(directory)
+        shutil.rmtree(directory, onerror=del_rw)
 
     def test_check_cache_leaves(self):
         config = {}
         config["no-cache"] = False
-        config['install_path'] = self._tmp_dir + "/test/module"
-        os.makedirs(config['install_path'])
+        config['install_path'] = os.path.join(
+            self._tmp_dir, os.path.normpath("test/module"))
+        os.makedirs(config['install_path'], mode=0o777)
         ld.Loader.check_cache(config)
         self.assertTrue(os.path.isdir(config["install_path"]))
-        shutil.rmtree(config["install_path"])
+        shutil.rmtree(config["install_path"], onerror=del_rw)
 
     def test_loading_intents(self):
         config = {}
         config["no-cache"] = True
-        config['install_path'] = self._tmp_dir + "/test/module/test/"
-        os.makedirs(config['install_path'])
+        config['install_path'] = os.path.join(
+            self._tmp_dir, os.path.normpath("test/module/test"))
+        os.makedirs(config['install_path'], mode=0o777)
         intent_contents = "Hello world"
-        with open(config['install_path'] + "intents.md", 'w') as intents:
+        intents_file = os.path.join(config['install_path'], "intents.md")
+        with open(intents_file, 'w') as intents:
             intents.write(intent_contents)
         loaded_intents = ld.Loader._load_intents(config)
         self.assertEqual(intent_contents, loaded_intents)
-        shutil.rmtree(config["install_path"])
+        shutil.rmtree(config["install_path"], onerror=del_rw)
 
     def test_loading_intents_failed(self):
         config = {}
         config["no-cache"] = True
-        config['install_path'] = self._tmp_dir + "/test/module/test/"
+        config['install_path'] = os.path.join(
+            self._tmp_dir, os.path.normpath("test/module/test/"))
         loaded_intents = ld.Loader._load_intents(config)
         self.assertEqual(None, loaded_intents)
 
@@ -186,7 +203,7 @@ def test_load_config(self):
         config['databases'] = mock.MagicMock()
         config['skills'] = mock.MagicMock()
         config['connectors'] = mock.MagicMock()
-        config['module-path'] = self._tmp_dir + "/opsdroid"
+        config['module-path'] = os.path.join(self._tmp_dir, "opsdroid")
 
         loader.load_modules_from_config(config)
         self.assertEqual(len(loader._load_modules.mock_calls), 4)
@@ -202,7 +219,8 @@ def test_load_empty_config(self):
 
     def test_load_minimal_config_file(self):
         opsdroid, loader = self.setup()
-        config = loader.load_config_file(["tests/configs/minimal.yaml"])
+        config = loader.load_config_file(
+            [os.path.abspath("tests/configs/minimal.yaml")])
         loader._install_module = mock.MagicMock()
         loader.import_module = mock.MagicMock()
         loader._reload_modules = mock.MagicMock()
@@ -217,7 +235,8 @@ def test_load_minimal_config_file_2(self):
         loader._install_module = mock.MagicMock()
         loader.import_module = mock.MagicMock()
         loader._reload_modules = mock.MagicMock()
-        config = loader.load_config_file(["tests/configs/minimal_2.yaml"])
+        config = loader.load_config_file(
+            [os.path.abspath("tests/configs/minimal_2.yaml")])
         connectors, databases, skills = loader.load_modules_from_config(config)
         self.assertIsNotNone(config)
         self.assertIsNotNone(connectors)
@@ -257,19 +276,21 @@ def test_load_modules_fail(self):
     def test_install_existing_module(self):
         opsdroid, loader = self.setup()
         config = {"name": "testmodule",
-                  "install_path": self._tmp_dir + "/test_existing_module"}
+                  "install_path": os.path.join(
+                      self._tmp_dir, "test_existing_module")}
         os.mkdir(config["install_path"])
         with mock.patch('opsdroid.loader._LOGGER.debug') as logmock:
             loader._install_module(config)
             self.assertTrue(logmock.called)
 
-        shutil.rmtree(config["install_path"])
+        shutil.rmtree(config["install_path"], onerror=del_rw)
 
     def test_install_missing_local_module(self):
         opsdroid, loader = self.setup()
         config = {"name": "testmodule",
-                  "install_path": self._tmp_dir + "/test_missing_local_module",
-                  "repo": self._tmp_dir + "/testrepo",
+                  "install_path": os.path.join(
+                      self._tmp_dir, "test_missing_local_module"),
+                  "repo": os.path.join(self._tmp_dir, "testrepo"),
                   "branch": "master"}
         with mock.patch('opsdroid.loader._LOGGER.error') as logmock:
             loader._install_module(config)
@@ -282,7 +303,7 @@ def test_install_specific_remote_module(self):
         opsdroid, loader = self.setup()
         config = {"name": "testmodule",
                   "install_path":
-                  self._tmp_dir + "/test_specific_remote_module",
+                  os.path.join(self._tmp_dir, "test_specific_remote_module"),
                   "repo": "https://github.com/rmccue/test-repository.git",
                   "branch": "master"}
         with mock.patch('opsdroid.loader._LOGGER.debug'), \
@@ -294,87 +315,94 @@ def test_install_specific_remote_module(self):
 
     def test_install_specific_local_git_module(self):
         opsdroid, loader = self.setup()
-        repo_path = self._tmp_dir + "/testrepo"
+        repo_path = os.path.join(self._tmp_dir, "testrepo")
         config = {"name": "testmodule",
                   "install_path": repo_path,
                   "repo": "https://github.com/rmccue/test-repository.git",
                   "branch": "master"}
         loader._install_module(config)  # Clone remote repo for testing with
-        config["repo"] = config["install_path"] + "/.git"
-        config["install_path"] = self._tmp_dir + "/test_specific_local_module"
+        config["repo"] = os.path.join(config["install_path"], ".git")
+        config["install_path"] = os.path.join(
+            self._tmp_dir, "test_specific_local_module")
         with mock.patch('opsdroid.loader._LOGGER.debug'), \
                 mock.patch.object(loader, 'git_clone') as mockclone:
             loader._install_module(config)
             mockclone.assert_called_with(config["repo"],
                                          config["install_path"],
                                          config["branch"])
-        shutil.rmtree(repo_path)
+        shutil.rmtree(repo_path, onerror=del_rw)
 
     def test_install_specific_local_path_module(self):
         opsdroid, loader = self.setup()
-        repo_path = self._tmp_dir + "/testrepo"
+        repo_path = os.path.join(self._tmp_dir, "testrepo")
         config = {"name": "testmodule",
                   "install_path": repo_path,
                   "repo": "https://github.com/rmccue/test-repository.git",
                   "branch": "master"}
         loader._install_module(config)  # Clone remote repo for testing with
         config["path"] = config["install_path"]
-        config["install_path"] = self._tmp_dir + "/test_specific_local_module"
+        config["install_path"] = os.path.join(
+            self._tmp_dir, "test_specific_local_module")
         with mock.patch('opsdroid.loader._LOGGER.debug'), \
                 mock.patch.object(loader, '_install_local_module') \
                 as mockclone:
             loader._install_module(config)
             mockclone.assert_called_with(config)
-        shutil.rmtree(repo_path)
+        shutil.rmtree(repo_path, onerror=del_rw)
 
     def test_install_default_remote_module(self):
         opsdroid, loader = self.setup()
         config = {"name": "slack",
                   "type": "connector",
                   "install_path":
-                  self._tmp_dir + "/test_default_remote_module",
+                  os.path.join(self._tmp_dir, "test_default_remote_module"),
                   "branch": "master"}
         with mock.patch('opsdroid.loader._LOGGER.debug') as logmock, \
                 mock.patch.object(loader, 'pip_install_deps') as mockdeps:
             loader._install_module(config)
             self.assertTrue(logmock.called)
             mockdeps.assert_called_with(
-                    config["install_path"] + "/requirements.txt")
+                    os.path.join(config["install_path"], "requirements.txt"))
 
-        shutil.rmtree(config["install_path"])
+        shutil.rmtree(config["install_path"], onerror=del_rw)
 
     def test_install_local_module_dir(self):
         opsdroid, loader = self.setup()
-        base_path = self._tmp_dir + "/long"
+        base_path = os.path.join(self._tmp_dir, "long")
         config = {"name": "slack",
                   "type": "connector",
-                  "install_path": base_path + "/test/path/test",
-                  "path": self._tmp_dir + "/install/from/here"}
-        os.makedirs(config["path"], exist_ok=True)
+                  "install_path": os.path.join(
+                      base_path, os.path.normpath("test/path/test")),
+                  "path": os.path.join(
+                      self._tmp_dir, os.path.normpath("install/from/here"))}
+        os.makedirs(config["path"], exist_ok=True, mode=0o777)
         loader._install_local_module(config)
         self.assertTrue(os.path.isdir(config["install_path"]))
-        shutil.rmtree(base_path)
+        shutil.rmtree(base_path, onerror=del_rw)
 
     def test_install_local_module_file(self):
         opsdroid, loader = self.setup()
         config = {"name": "slack",
                   "type": "connector",
-                  "install_path": self._tmp_dir + "/test_local_module_file",
-                  "path": self._tmp_dir + "/install/from/here.py"}
+                  "install_path": os.path.join(
+                      self._tmp_dir, "test_local_module_file"),
+                  "path": os.path.join(
+                      self._tmp_dir, os.path.normpath("install/from/here.py"))}
         directory, _ = os.path.split(config["path"])
-        os.makedirs(directory, exist_ok=True)
+        os.makedirs(directory, exist_ok=True, mode=0o777)
         open(config["path"], 'w')
         loader._install_local_module(config)
-        self.assertTrue(os.path.isfile(
-                            config["install_path"] + "/__init__.py"))
-        shutil.rmtree(config["install_path"])
+        self.assertTrue(os.path.isfile(os.path.join(
+            config["install_path"], "__init__.py")))
+        shutil.rmtree(config["install_path"], onerror=del_rw)
 
     def test_install_local_module_failure(self):
         opsdroid, loader = self.setup()
         config = {"name": "slack",
                   "type": "connector",
-                  "install_path": self._tmp_dir + "/test_local_module_failure",
-                  "path": self._tmp_dir + "/does/not/exist"}
+                  "install_path": os.path.join(
+                      self._tmp_dir, "test_local_module_failure"),
+                  "path": os.path.join(self._tmp_dir, "doesnotexist")}
         with mock.patch('opsdroid.loader._LOGGER.error') as logmock:
             loader._install_local_module(config)
             self.assertTrue(logmock.called)
diff --git a/tests/test_main.py b/tests/test_main.py
--- a/tests/test_main.py
+++ b/tests/test_main.py
@@ -4,23 +4,31 @@
 import os
 import sys
 import shutil
+import tempfile
 import unittest.mock as mock
 
 
 import opsdroid.__main__ as opsdroid
 import opsdroid.web as web
 from opsdroid.core import OpsDroid
+from opsdroid.helper import del_rw
 
 
 class TestMain(unittest.TestCase):
     """Test the main opsdroid module."""
 
     def setUp(self):
-        self._tmp_dir = "/tmp/opsdroid_tests"
-        os.makedirs(self._tmp_dir)
+        self._tmp_dir = os.path.join(tempfile.gettempdir(), "opsdroid_tests")
+        try:
+            os.makedirs(self._tmp_dir, mode=0o777)
+        except FileExistsError:
+            pass
 
     def tearDown(self):
-        shutil.rmtree(self._tmp_dir)
+        try:
+            shutil.rmtree(self._tmp_dir, onerror=del_rw)
+        except PermissionError:
+            pass
 
     def test_init_runs(self):
         with mock.patch.object(opsdroid, "main") as mainfunc:
@@ -65,7 +73,7 @@ def test_configure_no_logging(self):
 
     def test_configure_file_logging(self):
         config = {"logging": {
-            "path": self._tmp_dir + "/output.log",
+            "path": os.path.join(self._tmp_dir, "output.log"),
             "console": False,
         }}
         opsdroid.configure_logging(config)
@@ -77,12 +85,18 @@ def test_configure_file_logging(self):
         self.assertEqual(rootlogger.handlers[1].level, logging.INFO)
 
     def test_configure_file_logging_directory_not_exists(self):
-        config = {"logging": {
-            "path": '/tmp/mynonexistingdirectory' + "/output.log",
-            "console": False,
-        }}
-        opsdroid.configure_logging(config)
-        self.assertEqual(os.path.isfile(config['logging']['path']), True)
+        with mock.patch('logging.getLogger') as logmock:
+            mocklogger = mock.MagicMock()
+            mocklogger.handlers = [True]
+            logmock.return_value = mocklogger
+            config = {"logging": {
+                "path": os.path.join(self._tmp_dir,
+                                     'mynonexistingdirectory',
+                                     "output.log"),
+                "console": False,
+            }}
+            opsdroid.configure_logging(config)
+            # self.assertEqual(os.path.isfile(config['logging']['path']), True)
 
     def test_configure_console_logging(self):
         config = {"logging": {