Repository: saltstack/salt
Pull request: Update schedule state module and remote module to report changes
### What does this PR do?
Updating the add, delete, modify, enable_job, and disable_job functions to return appropriate changes.  Moving all scheduler tests to pytest.

### What issues does this PR fix or reference?
Fixes: #59844 

### Merge requirements satisfied?
**[NOTICE] Bug fixes or features added to Salt require tests.**
<!-- Please review the [test documentation](https://docs.saltstack.com/en/master/topics/tutorials/writing_tests.html) for details on how to implement tests into Salt's test suite. -->
- [ ] Docs
- [x] Changelog - https://docs.saltstack.com/en/master/topics/development/changelog.html
- [x] Tests written/updated

### Commits signed with GPG?
Yes/No

Please review [Salt's Contributing Guide](https://docs.saltstack.com/en/master/topics/development/contributing.html) for best practices.

See GitHub's [page on GPG signing](https://help.github.com/articles/signing-commits-using-gpg/) for more information about signing commits with GPG.


Problem statement:
[BUG] schedule states do not report changes
**Description**
The `schedule.absent` state never reports changes, even if it does or would do something.
The `schedule.present` state only reports changes if it's going to change an existing job.

**Steps to Reproduce the behavior**
```yaml
# salt-call state.single schedule.present job1 function=test.ping seconds=3600 --state-output=full --state-verbose=true
local:
----------
          ID: job1
    Function: schedule.present
      Result: True
     Comment: Adding new job job1 to schedule
     Started: 16:16:22.962856
    Duration: 46.496 ms
     Changes:

Summary for local
------------
Succeeded: 1
Failed:    0
------------
Total states run:     1
Total run time:  46.496 ms
# salt-call state.single schedule.absent job1 test=true --state-output=full --state-verbose=true
local:
----------
          ID: job1
    Function: schedule.absent
      Result: True
     Comment: Job: job1 would be deleted from schedule.
     Started: 16:11:27.781563
    Duration: 16.764 ms
     Changes:

Summary for local
------------
Succeeded: 1
Failed:    0
------------
Total states run:     1
Total run time:  16.764 ms
#  salt-call state.single schedule.absent job1 --state-output=full --state-verbose=true
local:
----------
          ID: job1
    Function: schedule.absent
      Result: True
     Comment: Removed job job1 from schedule
     Started: 16:12:26.271653
    Duration: 43.652 ms
     Changes:

Summary for local
------------
Succeeded: 1
Failed:    0
------------
Total states run:     1
Total run time:  43.652 ms
# salt-call state.single schedule.absent job1 --state-output=full --state-verbose=true
local:
----------
          ID: job1
    Function: schedule.absent
      Result: True
     Comment: Job job1 not present in schedule
     Started: 16:12:40.613183
    Duration: 11.948 ms
     Changes:

Summary for local
------------
Succeeded: 1
Failed:    0
------------
Total states run:     1
Total run time:  11.948 ms
```

**Expected behavior**
State should report changes whenever they do (or in test mode, would do) something.

**Versions Report**
<details><summary>salt --versions-report</summary>
(Provided by running salt --versions-report. Please also mention any differences in master/minion versions.) 

```
Salt Version:
          Salt: 3002.5

Dependency Versions:
          cffi: Not Installed
      cherrypy: Not Installed
      dateutil: 2.7.3
     docker-py: Not Installed
         gitdb: Not Installed
     gitpython: Not Installed
        Jinja2: 2.10.1
       libgit2: 0.28.3
      M2Crypto: 0.31.0
          Mako: Not Installed
       msgpack: 0.6.2
  msgpack-pure: Not Installed
  mysql-python: Not Installed
     pycparser: Not Installed
      pycrypto: Not Installed
  pycryptodome: 3.6.1
        pygit2: 1.0.3
        Python: 3.8.5 (default, Jan 27 2021, 15:41:15)
  python-gnupg: 0.4.5
        PyYAML: 5.3.1
         PyZMQ: 18.1.1
         smmap: Not Installed
       timelib: Not Installed
       Tornado: 4.5.3
           ZMQ: 4.3.2

System Versions:
          dist: ubuntu 20.04 focal
        locale: utf-8
       machine: x86_64
       release: 5.4.0-67-generic
        system: Linux
       version: Ubuntu 20.04 focal
```
</details>

**Additional context**
The documentation for the `schedule.absent` `persist` parameter is also wrong.

Hints from issue discussion:
@OrangeDog Like this?

```yaml
$ sudo salt-call state.single schedule.present job1 function=test.ping seconds=3600 --state-output=full --state-verbose=true
local:
----------
          ID: job1
    Function: schedule.present
      Result: True
     Comment: Adding new job job1 to schedule
     Started: 15:20:17.968573
    Duration: 21.024 ms
     Changes:   
              ----------
              diff:
                  --- 
                  +++ 
                  @@ -0,0 +1,6 @@
                  +enabled:True
                  +function:test.ping
                  +jid_include:True
                  +maxrunning:1
                  +name:job1
                  +seconds:3600

Summary for local
------------
Succeeded: 1 (changed=1)
Failed:    0
------------
Total states run:     1
Total run time:  21.024 ms

$ sudo salt-call state.single schedule.absent job1 test=true --state-output=full --state-verbose=true
local:
----------
          ID: job1
    Function: schedule.absent
      Result: True
     Comment: Job: job1 would be deleted from schedule.
     Started: 15:20:39.117097
    Duration: 14.316 ms
     Changes:   
              ----------
              diff:
                  --- 
                  +++ 
                  @@ -1,6 +0,0 @@
                  -enabled:True
                  -function:test.ping
                  -jid_include:True
                  -maxrunning:1
                  -name:job1
                  -seconds:3600

Summary for local
------------
Succeeded: 1 (changed=1)
Failed:    0
------------
Total states run:     1
Total run time:  14.316 ms

$ sudo salt-call state.single schedule.absent job1 --state-output=full --state-verbose=true
local:
----------
          ID: job1
    Function: schedule.absent
      Result: True
     Comment: Removed job job1 from schedule
     Started: 15:21:02.569402
    Duration: 20.269 ms
     Changes:   
              ----------
              diff:
                  --- 
                  +++ 
                  @@ -1,6 +0,0 @@
                  -enabled:True
                  -function:test.ping
                  -jid_include:True
                  -maxrunning:1
                  -name:job1
                  -seconds:3600

Summary for local
------------
Succeeded: 1 (changed=1)
Failed:    0
------------
Total states run:     1
Total run time:  20.269 ms

$ sudo salt-call state.single schedule.absent job1 --state-output=full --state-verbose=true
local:
----------
          ID: job1
    Function: schedule.absent
      Result: True
     Comment: Job job1 not present in schedule
     Started: 15:21:31.523292
    Duration: 9.017 ms
     Changes:   

Summary for local
------------
Succeeded: 1
Failed:    0
------------
Total states run:     1
Total run time:   9.017 ms
```
@garethgreenaway that would work, though it's not clear what the line numbers are in reference to. It just has to a) return something and b) not be misleading. I was expecting more like

```json
{"job1": "added"}
```
```json
{"job1": "removed"}
```
One of the other ideas I was toying with was having _old_ and _new_ in the changes dictionary.

Commit message: increase sleep to 5 seconds.

Patch to review:
diff --git a/changelog/59844.fixed b/changelog/59844.fixed
new file mode 100644
index 000000000000..df568b1165a8
--- /dev/null
+++ b/changelog/59844.fixed
@@ -0,0 +1 @@
+Updating the add, delete, modify, enable_job, and disable_job functions to return appropriate changes.
diff --git a/salt/modules/schedule.py b/salt/modules/schedule.py
index b925ebe353e1..3c88d8f9d88d 100644
--- a/salt/modules/schedule.py
+++ b/salt/modules/schedule.py
@@ -8,7 +8,6 @@
 
 import copy as pycopy
 import datetime
-import difflib
 import logging
 import os
 
@@ -251,6 +250,7 @@ def delete(name, **kwargs):
     ret = {
         "comment": "Failed to delete job {} from schedule.".format(name),
         "result": False,
+        "changes": {},
     }
 
     if not name:
@@ -289,6 +289,7 @@ def delete(name, **kwargs):
                             ret["comment"] = "Deleted Job {} from schedule.".format(
                                 name
                             )
+                            ret["changes"][name] = "removed"
                         else:
                             ret[
                                 "comment"
@@ -440,6 +441,7 @@ def add(name, **kwargs):
     ret = {
         "comment": "Failed to add job {} to schedule.".format(name),
         "result": False,
+        "changes": {},
     }
 
     if name in list_(show_all=True, return_yaml=False):
@@ -501,6 +503,7 @@ def add(name, **kwargs):
                         if name in schedule:
                             ret["result"] = True
                             ret["comment"] = "Added job: {} to schedule.".format(name)
+                            ret["changes"][name] = "added"
                             return ret
         except KeyError:
             # Effectively a no-op, since we can't really return without an event system
@@ -553,11 +556,19 @@ def modify(name, **kwargs):
     if "function" not in kwargs:
         kwargs["function"] = _current.get("function")
 
+    # Remove the auto generated _seconds value
     if "_seconds" in _current:
         _current["seconds"] = _current["_seconds"]
         del _current["_seconds"]
 
-    _new = build_schedule_item(name, **kwargs)
+    # Copy _current _new, then update values from kwargs
+    _new = pycopy.deepcopy(_current)
+    _new.update(kwargs)
+
+    # Remove test from kwargs, it's not a valid schedule option
+    if "test" in _new:
+        del _new["test"]
+
     if "result" in _new and not _new["result"]:
         return _new
 
@@ -565,13 +576,10 @@ def modify(name, **kwargs):
         ret["comment"] = "Job {} in correct state".format(name)
         return ret
 
-    _current_lines = [
-        "{}:{}\n".format(key, value) for (key, value) in sorted(_current.items())
-    ]
-    _new_lines = ["{}:{}\n".format(key, value) for (key, value) in sorted(_new.items())]
-    _diff = difflib.unified_diff(_current_lines, _new_lines)
-
-    ret["changes"]["diff"] = "".join(_diff)
+    ret["changes"][name] = {
+        "old": salt.utils.odict.OrderedDict(_current),
+        "new": salt.utils.odict.OrderedDict(_new),
+    }
 
     if "test" in kwargs and kwargs["test"]:
         ret["comment"] = "Job: {} would be modified in schedule.".format(name)
@@ -653,7 +661,7 @@ def enable_job(name, **kwargs):
         salt '*' schedule.enable_job job1
     """
 
-    ret = {"comment": [], "result": True}
+    ret = {"comment": [], "result": True, "changes": {}}
 
     if not name:
         ret["comment"] = "Job name is required."
@@ -692,6 +700,7 @@ def enable_job(name, **kwargs):
                         if name in schedule and schedule[name]["enabled"]:
                             ret["result"] = True
                             ret["comment"] = "Enabled Job {} in schedule.".format(name)
+                            ret["changes"][name] = "enabled"
                         else:
                             ret["result"] = False
                             ret[
@@ -715,7 +724,7 @@ def disable_job(name, **kwargs):
         salt '*' schedule.disable_job job1
     """
 
-    ret = {"comment": [], "result": True}
+    ret = {"comment": [], "result": True, "changes": {}}
 
     if not name:
         ret["comment"] = "Job name is required."
@@ -754,6 +763,7 @@ def disable_job(name, **kwargs):
                         if name in schedule and not schedule[name]["enabled"]:
                             ret["result"] = True
                             ret["comment"] = "Disabled Job {} in schedule.".format(name)
+                            ret["changes"][name] = "disabled"
                         else:
                             ret["result"] = False
                             ret[
@@ -814,7 +824,7 @@ def enable(**kwargs):
         salt '*' schedule.enable
     """
 
-    ret = {"comment": [], "result": True}
+    ret = {"comment": [], "changes": {}, "result": True}
 
     if "test" in kwargs and kwargs["test"]:
         ret["comment"] = "Schedule would be enabled."
@@ -835,6 +845,7 @@ def enable(**kwargs):
                         if "enabled" in schedule and schedule["enabled"]:
                             ret["result"] = True
                             ret["comment"] = "Enabled schedule on minion."
+                            ret["changes"]["schedule"] = "enabled"
                         else:
                             ret["result"] = False
                             ret["comment"] = "Failed to enable schedule on minion."
@@ -856,7 +867,7 @@ def disable(**kwargs):
         salt '*' schedule.disable
     """
 
-    ret = {"comment": [], "result": True}
+    ret = {"comment": [], "changes": {}, "result": True}
 
     if "test" in kwargs and kwargs["test"]:
         ret["comment"] = "Schedule would be disabled."
@@ -877,6 +888,7 @@ def disable(**kwargs):
                         if "enabled" in schedule and not schedule["enabled"]:
                             ret["result"] = True
                             ret["comment"] = "Disabled schedule on minion."
+                            ret["changes"]["schedule"] = "disabled"
                         else:
                             ret["result"] = False
                             ret["comment"] = "Failed to disable schedule on minion."
diff --git a/salt/states/schedule.py b/salt/states/schedule.py
index cf7343af9830..b2eec5db07d6 100644
--- a/salt/states/schedule.py
+++ b/salt/states/schedule.py
@@ -1,4 +1,3 @@
-# -*- coding: utf-8 -*-
 """
 Management of the Salt scheduler
 ==============================================
@@ -97,7 +96,6 @@
     python-dateutil is installed on the minion.
 
 """
-from __future__ import absolute_import, print_function, unicode_literals
 
 
 def present(name, **kwargs):
@@ -229,7 +227,7 @@ def present(name, **kwargs):
                 new_item["enabled"] = True
 
         if new_item == current_schedule[name]:
-            ret["comment"].append("Job {0} in correct state".format(name))
+            ret["comment"].append("Job {} in correct state".format(name))
         else:
             if "test" in __opts__ and __opts__["test"]:
                 kwargs["test"] = True
@@ -243,7 +241,7 @@ def present(name, **kwargs):
                     ret["comment"] = result["comment"]
                     return ret
                 else:
-                    ret["comment"].append("Modifying job {0} in schedule".format(name))
+                    ret["comment"].append("Modifying job {} in schedule".format(name))
                     ret["changes"] = result["changes"]
     else:
         if "test" in __opts__ and __opts__["test"]:
@@ -257,7 +255,8 @@ def present(name, **kwargs):
                 ret["comment"] = result["comment"]
                 return ret
             else:
-                ret["comment"].append("Adding new job {0} to schedule".format(name))
+                ret["comment"].append("Adding new job {} to schedule".format(name))
+                ret["changes"] = result["changes"]
 
     ret["comment"] = "\n".join(ret["comment"])
     return ret
@@ -289,9 +288,10 @@ def absent(name, **kwargs):
                 ret["comment"] = result["comment"]
                 return ret
             else:
-                ret["comment"].append("Removed job {0} from schedule".format(name))
+                ret["comment"].append("Removed job {} from schedule".format(name))
+                ret["changes"] = result["changes"]
     else:
-        ret["comment"].append("Job {0} not present in schedule".format(name))
+        ret["comment"].append("Job {} not present in schedule".format(name))
 
     ret["comment"] = "\n".join(ret["comment"])
     return ret
@@ -321,12 +321,13 @@ def enabled(name, **kwargs):
             result = __salt__["schedule.enable_job"](name, **kwargs)
             if not result["result"]:
                 ret["result"] = result["result"]
+                ret["changes"] = result["changes"]
                 ret["comment"] = result["comment"]
                 return ret
             else:
-                ret["comment"].append("Enabled job {0} from schedule".format(name))
+                ret["comment"].append("Enabled job {} from schedule".format(name))
     else:
-        ret["comment"].append("Job {0} not present in schedule".format(name))
+        ret["comment"].append("Job {} not present in schedule".format(name))
 
     ret["comment"] = "\n".join(ret["comment"])
     return ret
@@ -359,9 +360,9 @@ def disabled(name, **kwargs):
                 ret["comment"] = result["comment"]
                 return ret
             else:
-                ret["comment"].append("Disabled job {0} from schedule".format(name))
+                ret["comment"].append("Disabled job {} from schedule".format(name))
     else:
-        ret["comment"].append("Job {0} not present in schedule".format(name))
+        ret["comment"].append("Job {} not present in schedule".format(name))
 
     ret["comment"] = "\n".join(ret["comment"])
     return ret
diff --git a/salt/utils/schedule.py b/salt/utils/schedule.py
index 661bd162edc8..f4b5d6c68208 100644
--- a/salt/utils/schedule.py
+++ b/salt/utils/schedule.py
@@ -950,7 +950,7 @@ def eval(self, now=None):
 
         """
 
-        log.trace("==== evaluating schedule now %s =====", now)
+        log.warning("==== evaluating schedule now %s =====", now)
 
         jids = []
         loop_interval = self.opts["loop_interval"]
diff --git a/tests/filename_map.yml b/tests/filename_map.yml
index 2ab584e825f6..49356384495b 100644
--- a/tests/filename_map.yml
+++ b/tests/filename_map.yml
@@ -237,13 +237,13 @@ salt/utils/docker/*:
   - unit.utils.test_dockermod
 
 salt/utils/schedule.py:
-  - unit.utils.scheduler.test_error
-  - unit.utils.scheduler.test_eval
-  - unit.utils.scheduler.test_postpone
-  - unit.utils.scheduler.test_skip
-  - unit.utils.scheduler.test_maxrunning
-  - unit.utils.scheduler.test_helpers
-  - unit.utils.scheduler.test_schedule
+  - pytests.unit.utils.scheduler.test_error
+  - pytests.unit.utils.scheduler.test_eval
+  - pytests.unit.utils.scheduler.test_postpone
+  - pytests.unit.utils.scheduler.test_skip
+  - pytests.unit.utils.scheduler.test_maxrunning
+  - pytests.unit.utils.scheduler.test_helpers
+  - pytests.unit.utils.scheduler.test_schedule
 
 salt/utils/vt.py:
   - integration.cli.test_custom_module
diff --git a/tests/pytests/unit/modules/test_schedule.py b/tests/pytests/unit/modules/test_schedule.py
new file mode 100644
index 000000000000..44ceedbd0d6e
--- /dev/null
+++ b/tests/pytests/unit/modules/test_schedule.py
@@ -0,0 +1,650 @@
+"""
+    :codeauthor: Jayesh Kariya <jayeshk@saltstack.com>
+"""
+
+import logging
+import os
+
+import pytest
+import salt.modules.schedule as schedule
+import salt.utils.odict
+from salt.utils.event import SaltEvent
+from tests.support.mock import MagicMock, patch
+
+log = logging.getLogger(__name__)
+
+JOB1 = {
+    "function": "test.ping",
+    "maxrunning": 1,
+    "name": "job1",
+    "jid_include": True,
+    "enabled": True,
+}
+
+
+@pytest.fixture
+def sock_dir():
+    with pytest.helpers.temp_directory() as tempdir:
+        sock_dir = os.path.join(tempdir, "test-socks")
+        return sock_dir
+
+
+@pytest.fixture
+def configure_loader_modules():
+    return {schedule: {}}
+
+
+# 'purge' function tests: 1
+
+
+@pytest.mark.slow_test
+def test_purge(sock_dir):
+    """
+    Test if it purge all the jobs currently scheduled on the minion.
+    """
+    with patch.dict(schedule.__opts__, {"schedule": {}, "sock_dir": sock_dir}):
+        mock = MagicMock(return_value=True)
+        with patch.dict(schedule.__salt__, {"event.fire": mock}):
+            _ret_value = {"complete": True, "schedule": {}}
+            with patch.object(SaltEvent, "get_event", return_value=_ret_value):
+                assert schedule.purge() == {
+                    "comment": ["Deleted job: schedule from schedule."],
+                    "result": True,
+                }
+
+
+# 'delete' function tests: 1
+
+
+@pytest.mark.slow_test
+def test_delete(sock_dir):
+    """
+    Test if it delete a job from the minion's schedule.
+    """
+    with patch.dict(schedule.__opts__, {"schedule": {}, "sock_dir": sock_dir}):
+        mock = MagicMock(return_value=True)
+        with patch.dict(schedule.__salt__, {"event.fire": mock}):
+            _ret_value = {"complete": True, "schedule": {}}
+            with patch.object(SaltEvent, "get_event", return_value=_ret_value):
+                assert schedule.delete("job1") == {
+                    "comment": "Job job1 does not exist.",
+                    "changes": {},
+                    "result": False,
+                }
+
+
+# 'build_schedule_item' function tests: 1
+
+
+def test_build_schedule_item(sock_dir):
+    """
+    Test if it build a schedule job.
+    """
+    comment = (
+        'Unable to use "seconds", "minutes", "hours", '
+        'or "days" with "when" or "cron" options.'
+    )
+    comment1 = 'Unable to use "when" and "cron" ' "options together.  Ignoring."
+    with patch.dict(schedule.__opts__, {"job1": {}}):
+        assert schedule.build_schedule_item("") == {
+            "comment": "Job name is required.",
+            "result": False,
+        }
+
+        assert schedule.build_schedule_item("job1", function="test.ping") == {
+            "function": "test.ping",
+            "maxrunning": 1,
+            "name": "job1",
+            "jid_include": True,
+            "enabled": True,
+        }
+
+        assert schedule.build_schedule_item(
+            "job1", function="test.ping", seconds=3600, when="2400"
+        ) == {"comment": comment, "result": False}
+
+        assert schedule.build_schedule_item(
+            "job1", function="test.ping", when="2400", cron="2"
+        ) == {"comment": comment1, "result": False}
+
+
+# 'build_schedule_item_invalid_when' function tests: 1
+
+
+def test_build_schedule_item_invalid_when(sock_dir):
+    """
+    Test if it build a schedule job.
+    """
+    comment = 'Schedule item garbage for "when" in invalid.'
+    with patch.dict(schedule.__opts__, {"job1": {}}):
+        assert schedule.build_schedule_item(
+            "job1", function="test.ping", when="garbage"
+        ) == {"comment": comment, "result": False}
+
+
+# 'add' function tests: 1
+
+
+@pytest.mark.slow_test
+def test_add(sock_dir):
+    """
+    Test if it add a job to the schedule.
+    """
+    comm1 = "Job job1 already exists in schedule."
+    comm2 = (
+        'Error: Unable to use "seconds", "minutes", "hours", '
+        'or "days" with "when" or "cron" options.'
+    )
+    comm3 = 'Unable to use "when" and "cron" options together.  Ignoring.'
+    comm4 = "Job: job2 would be added to schedule."
+    with patch.dict(
+        schedule.__opts__, {"schedule": {"job1": "salt"}, "sock_dir": sock_dir}
+    ):
+        mock = MagicMock(return_value=True)
+        with patch.dict(schedule.__salt__, {"event.fire": mock}):
+            _ret_value = {"complete": True, "schedule": {"job1": {"salt": "salt"}}}
+            with patch.object(SaltEvent, "get_event", return_value=_ret_value):
+                assert schedule.add("job1") == {
+                    "comment": comm1,
+                    "changes": {},
+                    "result": False,
+                }
+
+            _ret_value = {"complete": True, "schedule": {}}
+            with patch.object(SaltEvent, "get_event", return_value=_ret_value):
+                assert schedule.add(
+                    "job2", function="test.ping", seconds=3600, when="2400"
+                ) == {"comment": comm2, "changes": {}, "result": False}
+
+            _ret_value = {"complete": True, "schedule": {}}
+            with patch.object(SaltEvent, "get_event", return_value=_ret_value):
+                assert schedule.add(
+                    "job2", function="test.ping", when="2400", cron="2"
+                ) == {"comment": comm3, "changes": {}, "result": False}
+            _ret_value = {"complete": True, "schedule": {}}
+            with patch.object(SaltEvent, "get_event", return_value=_ret_value):
+                assert schedule.add("job2", function="test.ping", test=True) == {
+                    "comment": comm4,
+                    "changes": {},
+                    "result": True,
+                }
+
+
+# 'run_job' function tests: 1
+
+
+@pytest.mark.slow_test
+def test_run_job(sock_dir):
+    """
+    Test if it run a scheduled job on the minion immediately.
+    """
+    with patch.dict(
+        schedule.__opts__, {"schedule": {"job1": JOB1}, "sock_dir": sock_dir}
+    ):
+        mock = MagicMock(return_value=True)
+        with patch.dict(schedule.__salt__, {"event.fire": mock}):
+            _ret_value = {"complete": True, "schedule": {"job1": JOB1}}
+            with patch.object(SaltEvent, "get_event", return_value=_ret_value):
+                assert schedule.run_job("job1") == {
+                    "comment": "Scheduling Job job1 on minion.",
+                    "result": True,
+                }
+
+
+# 'enable_job' function tests: 1
+
+
+@pytest.mark.slow_test
+def test_enable_job(sock_dir):
+    """
+    Test if it enable a job in the minion's schedule.
+    """
+    with patch.dict(schedule.__opts__, {"schedule": {}, "sock_dir": sock_dir}):
+        mock = MagicMock(return_value=True)
+        with patch.dict(schedule.__salt__, {"event.fire": mock}):
+            _ret_value = {"complete": True, "schedule": {}}
+            with patch.object(SaltEvent, "get_event", return_value=_ret_value):
+                assert schedule.enable_job("job1") == {
+                    "comment": "Job job1 does not exist.",
+                    "changes": {},
+                    "result": False,
+                }
+
+
+# 'disable_job' function tests: 1
+
+
+@pytest.mark.slow_test
+def test_disable_job(sock_dir):
+    """
+    Test if it disable a job in the minion's schedule.
+    """
+    with patch.dict(schedule.__opts__, {"schedule": {}, "sock_dir": sock_dir}):
+        mock = MagicMock(return_value=True)
+        with patch.dict(schedule.__salt__, {"event.fire": mock}):
+            _ret_value = {"complete": True, "schedule": {}}
+            with patch.object(SaltEvent, "get_event", return_value=_ret_value):
+                assert schedule.disable_job("job1") == {
+                    "comment": "Job job1 does not exist.",
+                    "changes": {},
+                    "result": False,
+                }
+
+
+# 'save' function tests: 1
+
+
+@pytest.mark.slow_test
+def test_save(sock_dir):
+    """
+    Test if it save all scheduled jobs on the minion.
+    """
+    comm1 = "Schedule (non-pillar items) saved."
+    with patch.dict(
+        schedule.__opts__,
+        {"schedule": {}, "default_include": "/tmp", "sock_dir": sock_dir},
+    ):
+
+        mock = MagicMock(return_value=True)
+        with patch.dict(schedule.__salt__, {"event.fire": mock}):
+            _ret_value = {"complete": True, "schedule": {}}
+            with patch.object(SaltEvent, "get_event", return_value=_ret_value):
+                assert schedule.save() == {"comment": comm1, "result": True}
+
+
+# 'enable' function tests: 1
+
+
+def test_enable(sock_dir):
+    """
+    Test if it enable all scheduled jobs on the minion.
+    """
+    assert schedule.enable(test=True) == {
+        "comment": "Schedule would be enabled.",
+        "changes": {},
+        "result": True,
+    }
+
+
+# 'disable' function tests: 1
+
+
+def test_disable(sock_dir):
+    """
+    Test if it disable all scheduled jobs on the minion.
+    """
+    assert schedule.disable(test=True) == {
+        "comment": "Schedule would be disabled.",
+        "changes": {},
+        "result": True,
+    }
+
+
+# 'move' function tests: 1
+
+
+@pytest.mark.slow_test
+def test_move(sock_dir):
+    """
+    Test if it move scheduled job to another minion or minions.
+    """
+    comm1 = "no servers answered the published schedule.add command"
+    comm2 = "the following minions return False"
+    comm3 = "Moved Job job1 from schedule."
+    with patch.dict(
+        schedule.__opts__, {"schedule": {"job1": JOB1}, "sock_dir": sock_dir}
+    ):
+        mock = MagicMock(return_value=True)
+        with patch.dict(schedule.__salt__, {"event.fire": mock}):
+            _ret_value = {"complete": True, "schedule": {"job1": JOB1}}
+            with patch.object(SaltEvent, "get_event", return_value=_ret_value):
+                mock = MagicMock(return_value={})
+                with patch.dict(schedule.__salt__, {"publish.publish": mock}):
+                    assert schedule.move("job1", "minion1") == {
+                        "comment": comm1,
+                        "result": True,
+                    }
+
+                mock = MagicMock(return_value={"minion1": ""})
+                with patch.dict(schedule.__salt__, {"publish.publish": mock}):
+                    assert schedule.move("job1", "minion1") == {
+                        "comment": comm2,
+                        "minions": ["minion1"],
+                        "result": True,
+                    }
+
+                mock = MagicMock(return_value={"minion1": "job1"})
+                with patch.dict(schedule.__salt__, {"publish.publish": mock}):
+                    mock = MagicMock(return_value=True)
+                    with patch.dict(schedule.__salt__, {"event.fire": mock}):
+                        assert schedule.move("job1", "minion1") == {
+                            "comment": comm3,
+                            "minions": ["minion1"],
+                            "result": True,
+                        }
+
+                assert schedule.move("job3", "minion1") == {
+                    "comment": "Job job3 does not exist.",
+                    "result": False,
+                }
+
+    mock = MagicMock(side_effect=[{}, {"job1": {}}])
+    with patch.dict(schedule.__opts__, {"schedule": mock, "sock_dir": sock_dir}):
+        mock = MagicMock(return_value=True)
+        with patch.dict(schedule.__salt__, {"event.fire": mock}):
+            _ret_value = {"complete": True, "schedule": {"job1": JOB1}}
+            with patch.object(SaltEvent, "get_event", return_value=_ret_value):
+                with patch.dict(schedule.__pillar__, {"schedule": {"job1": JOB1}}):
+                    mock = MagicMock(return_value={})
+                    with patch.dict(schedule.__salt__, {"publish.publish": mock}):
+                        assert schedule.move("job1", "minion1") == {
+                            "comment": comm1,
+                            "result": True,
+                        }
+
+                    mock = MagicMock(return_value={"minion1": ""})
+                    with patch.dict(schedule.__salt__, {"publish.publish": mock}):
+                        assert schedule.move("job1", "minion1") == {
+                            "comment": comm2,
+                            "minions": ["minion1"],
+                            "result": True,
+                        }
+
+                    mock = MagicMock(return_value={"minion1": "job1"})
+                    with patch.dict(schedule.__salt__, {"publish.publish": mock}):
+                        mock = MagicMock(return_value=True)
+                        with patch.dict(schedule.__salt__, {"event.fire": mock}):
+                            assert schedule.move("job1", "minion1") == {
+                                "comment": comm3,
+                                "minions": ["minion1"],
+                                "result": True,
+                            }
+
+
+# 'copy' function tests: 1
+
+
+@pytest.mark.slow_test
+def test_copy(sock_dir):
+    """
+    Test if it copy scheduled job to another minion or minions.
+    """
+    comm1 = "no servers answered the published schedule.add command"
+    comm2 = "the following minions return False"
+    comm3 = "Copied Job job1 from schedule to minion(s)."
+    with patch.dict(
+        schedule.__opts__, {"schedule": {"job1": JOB1}, "sock_dir": sock_dir}
+    ):
+        mock = MagicMock(return_value=True)
+        with patch.dict(schedule.__salt__, {"event.fire": mock}):
+            _ret_value = {"complete": True, "schedule": {"job1": {"job1": JOB1}}}
+            with patch.object(SaltEvent, "get_event", return_value=_ret_value):
+                mock = MagicMock(return_value={})
+                with patch.dict(schedule.__salt__, {"publish.publish": mock}):
+                    assert schedule.copy("job1", "minion1") == {
+                        "comment": comm1,
+                        "result": True,
+                    }
+
+                mock = MagicMock(return_value={"minion1": ""})
+                with patch.dict(schedule.__salt__, {"publish.publish": mock}):
+                    assert schedule.copy("job1", "minion1") == {
+                        "comment": comm2,
+                        "minions": ["minion1"],
+                        "result": True,
+                    }
+
+                mock = MagicMock(return_value={"minion1": "job1"})
+                with patch.dict(schedule.__salt__, {"publish.publish": mock}):
+                    mock = MagicMock(return_value=True)
+                    with patch.dict(schedule.__salt__, {"event.fire": mock}):
+                        assert schedule.copy("job1", "minion1") == {
+                            "comment": comm3,
+                            "minions": ["minion1"],
+                            "result": True,
+                        }
+
+                assert schedule.copy("job3", "minion1") == {
+                    "comment": "Job job3 does not exist.",
+                    "result": False,
+                }
+
+    mock = MagicMock(side_effect=[{}, {"job1": {}}])
+    with patch.dict(schedule.__opts__, {"schedule": mock, "sock_dir": sock_dir}):
+        with patch.dict(schedule.__pillar__, {"schedule": {"job1": JOB1}}):
+            mock = MagicMock(return_value=True)
+            with patch.dict(schedule.__salt__, {"event.fire": mock}):
+                _ret_value = {
+                    "complete": True,
+                    "schedule": {"job1": {"job1": JOB1}},
+                }
+                with patch.object(SaltEvent, "get_event", return_value=_ret_value):
+
+                    mock = MagicMock(return_value={})
+                    with patch.dict(schedule.__salt__, {"publish.publish": mock}):
+                        assert schedule.copy("job1", "minion1") == {
+                            "comment": comm1,
+                            "result": True,
+                        }
+
+                    mock = MagicMock(return_value={"minion1": ""})
+                    with patch.dict(schedule.__salt__, {"publish.publish": mock}):
+                        assert schedule.copy("job1", "minion1") == {
+                            "comment": comm2,
+                            "minions": ["minion1"],
+                            "result": True,
+                        }
+
+                    mock = MagicMock(return_value={"minion1": "job1"})
+                    with patch.dict(schedule.__salt__, {"publish.publish": mock}):
+                        mock = MagicMock(return_value=True)
+                        with patch.dict(schedule.__salt__, {"event.fire": mock}):
+                            assert schedule.copy("job1", "minion1") == {
+                                "comment": comm3,
+                                "minions": ["minion1"],
+                                "result": True,
+                            }
+
+
+# 'modify' function tests: 1
+
+
+@pytest.mark.slow_test
+def test_modify(sock_dir):
+    """
+    Test if modifying job to the schedule.
+    """
+    current_job1 = {
+        "function": "salt",
+        "seconds": "3600",
+        "maxrunning": 1,
+        "name": "job1",
+        "enabled": True,
+        "jid_include": True,
+    }
+
+    new_job1 = {
+        "function": "salt",
+        "seconds": "60",
+        "maxrunning": 1,
+        "name": "job1",
+        "enabled": True,
+        "jid_include": True,
+    }
+
+    comm1 = "Modified job: job1 in schedule."
+    changes1 = {
+        "job1": {
+            "new": salt.utils.odict.OrderedDict(new_job1),
+            "old": salt.utils.odict.OrderedDict(current_job1),
+        }
+    }
+
+    new_job4 = {
+        "function": "test.version",
+        "seconds": "3600",
+        "maxrunning": 1,
+        "name": "job1",
+        "enabled": True,
+        "jid_include": True,
+    }
+
+    changes4 = {
+        "job1": {
+            "new": salt.utils.odict.OrderedDict(new_job4),
+            "old": salt.utils.odict.OrderedDict(current_job1),
+        }
+    }
+
+    expected1 = {"comment": comm1, "changes": changes1, "result": True}
+
+    comm2 = (
+        'Error: Unable to use "seconds", "minutes", "hours", '
+        'or "days" with "when" option.'
+    )
+    expected2 = {"comment": comm2, "changes": {}, "result": False}
+
+    comm3 = 'Unable to use "when" and "cron" options together.  Ignoring.'
+    expected3 = {"comment": comm3, "changes": {}, "result": False}
+
+    comm4 = "Job: job1 would be modified in schedule."
+    expected4 = {"comment": comm4, "changes": changes4, "result": True}
+
+    comm5 = "Job job2 does not exist in schedule."
+    expected5 = {"comment": comm5, "changes": {}, "result": False}
+
+    with patch.dict(
+        schedule.__opts__, {"schedule": {"job1": current_job1}, "sock_dir": sock_dir}
+    ):
+        mock = MagicMock(return_value=True)
+        with patch.dict(schedule.__salt__, {"event.fire": mock}):
+            _ret_value = {"complete": True, "schedule": {"job1": current_job1}}
+            with patch.object(SaltEvent, "get_event", return_value=_ret_value):
+                ret = schedule.modify("job1", seconds="60")
+                assert "job1" in ret["changes"]
+                assert "new" in ret["changes"]["job1"]
+                assert "old" in ret["changes"]["job1"]
+
+                for key in [
+                    "maxrunning",
+                    "function",
+                    "seconds",
+                    "jid_include",
+                    "name",
+                    "enabled",
+                ]:
+                    assert (
+                        ret["changes"]["job1"]["new"][key]
+                        == expected1["changes"]["job1"]["new"][key]
+                    )
+                    assert (
+                        ret["changes"]["job1"]["old"][key]
+                        == expected1["changes"]["job1"]["old"][key]
+                    )
+
+                assert ret["comment"] == expected1["comment"]
+                assert ret["result"] == expected1["result"]
+
+            _ret_value = {"complete": True, "schedule": {"job1": current_job1}}
+            with patch.object(SaltEvent, "get_event", return_value=_ret_value):
+                ret = schedule.modify(
+                    "job1", function="test.ping", seconds=3600, when="2400"
+                )
+                assert ret == expected2
+
+            _ret_value = {"complete": True, "schedule": {"job1": current_job1}}
+            with patch.object(SaltEvent, "get_event", return_value=_ret_value):
+                ret = schedule.modify(
+                    "job1", function="test.ping", when="2400", cron="2"
+                )
+                assert ret == expected3
+
+            _ret_value = {"complete": True, "schedule": {"job1": current_job1}}
+            with patch.object(SaltEvent, "get_event", return_value=_ret_value):
+                ret = schedule.modify("job1", function="test.version", test=True)
+
+                assert "job1" in ret["changes"]
+                assert "new" in ret["changes"]["job1"]
+                assert "old" in ret["changes"]["job1"]
+
+                for key in [
+                    "maxrunning",
+                    "function",
+                    "seconds",
+                    "jid_include",
+                    "name",
+                    "enabled",
+                ]:
+                    assert (
+                        ret["changes"]["job1"]["new"][key]
+                        == expected4["changes"]["job1"]["new"][key]
+                    )
+                    assert (
+                        ret["changes"]["job1"]["old"][key]
+                        == expected4["changes"]["job1"]["old"][key]
+                    )
+
+                assert ret["comment"] == expected4["comment"]
+                assert ret["result"] == expected4["result"]
+
+            _ret_value = {"complete": True, "schedule": {}}
+            with patch.object(SaltEvent, "get_event", return_value=_ret_value):
+                ret = schedule.modify("job2", function="test.version", test=True)
+                assert ret == expected5
+
+
+# 'is_enabled' function tests: 1
+
+
+def test_is_enabled(sock_dir):
+    """
+    Test is_enabled
+    """
+    job1 = {"function": "salt", "seconds": 3600}
+
+    comm1 = "Modified job: job1 in schedule."
+
+    mock_schedule = {"enabled": True, "job1": job1}
+
+    mock_lst = MagicMock(return_value=mock_schedule)
+
+    with patch.dict(
+        schedule.__opts__, {"schedule": {"job1": job1}, "sock_dir": sock_dir}
+    ):
+        mock = MagicMock(return_value=True)
+        with patch.dict(
+            schedule.__salt__, {"event.fire": mock, "schedule.list": mock_lst}
+        ):
+            _ret_value = {"complete": True, "schedule": {"job1": job1}}
+            with patch.object(SaltEvent, "get_event", return_value=_ret_value):
+                ret = schedule.is_enabled("job1")
+                assert ret == job1
+
+                ret = schedule.is_enabled()
+                assert ret
+
+
+# 'job_status' function tests: 1
+
+
+def test_job_status(sock_dir):
+    """
+    Test is_enabled
+    """
+    job1 = {"function": "salt", "seconds": 3600}
+
+    comm1 = "Modified job: job1 in schedule."
+
+    mock_schedule = {"enabled": True, "job1": job1}
+
+    mock_lst = MagicMock(return_value=mock_schedule)
+
+    with patch.dict(
+        schedule.__opts__, {"schedule": {"job1": job1}, "sock_dir": sock_dir}
+    ):
+        mock = MagicMock(return_value=True)
+        with patch.dict(schedule.__salt__, {"event.fire": mock}):
+            _ret_value = {"complete": True, "data": job1}
+            with patch.object(SaltEvent, "get_event", return_value=_ret_value):
+                ret = schedule.job_status("job1")
+                assert ret == job1
diff --git a/tests/pytests/unit/utils/scheduler/__init__.py b/tests/pytests/unit/utils/scheduler/__init__.py
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/tests/pytests/unit/utils/scheduler/conftest.py b/tests/pytests/unit/utils/scheduler/conftest.py
new file mode 100644
index 000000000000..7864e6c3cc60
--- /dev/null
+++ b/tests/pytests/unit/utils/scheduler/conftest.py
@@ -0,0 +1,73 @@
+"""
+    tests.unit.utils.scheduler.base
+    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+"""
+
+
+import copy
+import logging
+import os
+
+import pytest
+import salt.utils.platform
+import salt.utils.schedule
+from salt.modules.test import ping
+from salt.utils.process import SubprocessList
+from saltfactories.utils.processes import terminate_process
+from tests.support.mock import MagicMock, patch
+
+log = logging.getLogger(__name__)
+
+
+@pytest.fixture(scope="function")
+def setup_teardown_vars():
+    log.debug("=== running setup_teardown_vars ===")
+    try:
+
+        subprocess_list = SubprocessList()
+
+        with pytest.helpers.temp_directory() as tempdir:
+            root_dir = os.path.join(tempdir, "schedule-unit-tests")
+            sock_dir = os.path.join(root_dir, "test-socks")
+
+            default_config = salt.config.minion_config(None)
+            default_config["conf_dir"] = root_dir
+            default_config["root_dir"] = root_dir
+            default_config["sock_dir"] = sock_dir
+            default_config["pki_dir"] = os.path.join(root_dir, "pki")
+            default_config["cachedir"] = os.path.join(root_dir, "cache")
+
+        with patch("salt.utils.schedule.clean_proc_dir", MagicMock(return_value=None)):
+            functions = {"test.ping": ping}
+            schedule = salt.utils.schedule.Schedule(
+                copy.deepcopy(default_config),
+                functions,
+                returners={},
+                new_instance=True,
+            )
+
+        schedule.opts["loop_interval"] = 1
+        schedule._subprocess_list = subprocess_list
+
+        yield {"schedule": schedule}
+
+    finally:
+        processes = subprocess_list.processes
+        schedule.reset()
+        del schedule
+        for proc in processes:
+            if proc.is_alive():
+                terminate_process(proc.pid, kill_children=True, slow_stop=True)
+        subprocess_list.cleanup()
+        processes = subprocess_list.processes
+        if processes:
+            for proc in processes:
+                if proc.is_alive():
+                    terminate_process(proc.pid, kill_children=True, slow_stop=False)
+            subprocess_list.cleanup()
+        processes = subprocess_list.processes
+        if processes:
+            log.warning("Processes left running: %s", processes)
+
+        del default_config
+        del subprocess_list
diff --git a/tests/pytests/unit/utils/scheduler/test_error.py b/tests/pytests/unit/utils/scheduler/test_error.py
new file mode 100644
index 000000000000..7fb87481b764
--- /dev/null
+++ b/tests/pytests/unit/utils/scheduler/test_error.py
@@ -0,0 +1,220 @@
+import logging
+
+import pytest
+from tests.support.mock import MagicMock, patch
+
+try:
+    import dateutil.parser
+
+    HAS_DATEUTIL_PARSER = True
+except ImportError:
+    HAS_DATEUTIL_PARSER = False
+
+try:
+    import croniter  # pylint: disable=unused-import
+
+    HAS_CRONITER = True
+except ImportError:
+    HAS_CRONITER = False
+
+log = logging.getLogger(__name__)
+
+pytestmark = pytest.mark.skipif(
+    HAS_DATEUTIL_PARSER is False,
+    reason="The 'dateutil.parser' library is not available",
+)
+
+
+@pytest.mark.skipif(not HAS_CRONITER, reason="Cannot find croniter python module")
+def test_eval_cron_invalid(setup_teardown_vars):
+    """
+    verify that scheduled job runs
+    """
+    schedule = setup_teardown_vars["schedule"]
+    job = {"schedule": {"job1": {"function": "test.ping", "cron": "0 16 29 13 *"}}}
+
+    # Add the job to the scheduler
+    schedule.opts.update(job)
+
+    run_time = dateutil.parser.parse("11/29/2017 4:00pm")
+    with patch("croniter.croniter.get_next", MagicMock(return_value=run_time)):
+        schedule.eval(now=run_time)
+
+    ret = schedule.job_status("job1")
+    assert ret["_error"] == "Invalid cron string. Ignoring job job1."
+
+
+def test_eval_when_invalid_date(setup_teardown_vars):
+    """
+    verify that scheduled job does not run
+    and returns the right error
+    """
+    schedule = setup_teardown_vars["schedule"]
+    run_time = dateutil.parser.parse("11/29/2017 4:00pm")
+
+    job = {"schedule": {"job1": {"function": "test.ping", "when": "13/29/2017 1:00pm"}}}
+
+    # Add the job to the scheduler
+    schedule.opts.update(job)
+
+    # Evaluate 1 second before the run time
+    schedule.eval(now=run_time)
+    ret = schedule.job_status("job1")
+    assert ret["_error"] == "Invalid date string 13/29/2017 1:00pm. Ignoring job job1."
+
+
+def test_eval_whens_grain_not_dict(setup_teardown_vars):
+    """
+    verify that scheduled job does not run
+    and returns the right error
+    """
+    schedule = setup_teardown_vars["schedule"]
+    schedule.opts["grains"]["whens"] = {"tea time": "11/29/2017 12:00pm"}
+
+    run_time = dateutil.parser.parse("11/29/2017 4:00pm")
+
+    job = {"schedule": {"job1": {"function": "test.ping", "when": "tea time"}}}
+
+    schedule.opts["grains"]["whens"] = ["tea time"]
+
+    # Add the job to the scheduler
+    schedule.opts.update(job)
+
+    # Evaluate 1 second before the run time
+    schedule.eval(now=run_time)
+    ret = schedule.job_status("job1")
+    assert ret["_error"] == 'Grain "whens" must be a dict. Ignoring job job1.'
+
+
+def test_eval_once_invalid_datestring(setup_teardown_vars):
+    """
+    verify that scheduled job does not run
+    and returns the right error
+    """
+    schedule = setup_teardown_vars["schedule"]
+    job = {
+        "schedule": {"job1": {"function": "test.ping", "once": "2017-13-13T13:00:00"}}
+    }
+    run_time = dateutil.parser.parse("12/13/2017 1:00pm")
+
+    # Add the job to the scheduler
+    schedule.opts.update(job)
+
+    # Evaluate 1 second at the run time
+    schedule.eval(now=run_time)
+    ret = schedule.job_status("job1")
+    _expected = (
+        "Date string could not be parsed: "
+        "2017-13-13T13:00:00, %Y-%m-%dT%H:%M:%S. "
+        "Ignoring job job1."
+    )
+    assert ret["_error"] == _expected
+
+
+def test_eval_skip_during_range_invalid_date(setup_teardown_vars):
+    """
+    verify that scheduled job does not run
+    and returns the right error
+    """
+    schedule = setup_teardown_vars["schedule"]
+
+    job = {
+        "schedule": {
+            "job1": {
+                "function": "test.ping",
+                "hours": 1,
+                "skip_during_range": {"start": "1:00pm", "end": "25:00pm"},
+            }
+        }
+    }
+
+    # Add the job to the scheduler
+    schedule.opts.update(job)
+
+    # eval at 3:00pm to prime, simulate minion start up.
+    run_time = dateutil.parser.parse("11/29/2017 3:00pm")
+    schedule.eval(now=run_time)
+    ret = schedule.job_status("job1")
+
+    # eval at 4:00pm to prime
+    run_time = dateutil.parser.parse("11/29/2017 4:00pm")
+    schedule.eval(now=run_time)
+    ret = schedule.job_status("job1")
+    _expected = (
+        "Invalid date string for end in " "skip_during_range. Ignoring " "job job1."
+    )
+    assert ret["_error"] == _expected
+
+
+def test_eval_skip_during_range_end_before_start(setup_teardown_vars):
+    """
+    verify that scheduled job does not run
+    and returns the right error
+    """
+    schedule = setup_teardown_vars["schedule"]
+
+    job = {
+        "schedule": {
+            "job1": {
+                "function": "test.ping",
+                "hours": 1,
+                "skip_during_range": {"start": "1:00pm", "end": "12:00pm"},
+            }
+        }
+    }
+
+    # Add the job to the scheduler
+    schedule.opts.update(job)
+
+    # eval at 3:00pm to prime, simulate minion start up.
+    run_time = dateutil.parser.parse("11/29/2017 3:00pm")
+    schedule.eval(now=run_time)
+    ret = schedule.job_status("job1")
+
+    # eval at 4:00pm to prime
+    run_time = dateutil.parser.parse("11/29/2017 4:00pm")
+    schedule.eval(now=run_time)
+    ret = schedule.job_status("job1")
+    _expected = (
+        "schedule.handle_func: Invalid "
+        "range, end must be larger than "
+        "start. Ignoring job job1."
+    )
+    assert ret["_error"] == _expected
+
+
+def test_eval_skip_during_range_not_dict(setup_teardown_vars):
+    """
+    verify that scheduled job does not run
+    and returns the right error
+    """
+    schedule = setup_teardown_vars["schedule"]
+
+    job = {
+        "schedule": {
+            "job1": {
+                "function": "test.ping",
+                "hours": 1,
+                "skip_during_range": ["start", "1:00pm", "end", "12:00pm"],
+            }
+        }
+    }
+
+    # Add the job to the scheduler
+    schedule.opts.update(job)
+
+    # eval at 3:00pm to prime, simulate minion start up.
+    run_time = dateutil.parser.parse("11/29/2017 3:00pm")
+    schedule.eval(now=run_time)
+    ret = schedule.job_status("job1")
+
+    # eval at 4:00pm to prime
+    run_time = dateutil.parser.parse("11/29/2017 4:00pm")
+    schedule.eval(now=run_time)
+    ret = schedule.job_status("job1")
+    _expected = (
+        "schedule.handle_func: Invalid, "
+        "range must be specified as a "
+        "dictionary. Ignoring job job1."
+    )
+    assert ret["_error"] == _expected
diff --git a/tests/pytests/unit/utils/scheduler/test_eval.py b/tests/pytests/unit/utils/scheduler/test_eval.py
new file mode 100644
index 000000000000..2e834433cc75
--- /dev/null
+++ b/tests/pytests/unit/utils/scheduler/test_eval.py
@@ -0,0 +1,985 @@
+import datetime
+import logging
+import random
+import time
+
+import pytest
+import salt.utils.platform
+import salt.utils.schedule
+from tests.support.mock import MagicMock, patch
+from tests.support.unit import skipIf
+
+try:
+    import dateutil.parser
+
+    HAS_DATEUTIL_PARSER = True
+except ImportError:
+    HAS_DATEUTIL_PARSER = False
+
+
+try:
+    import croniter  # pylint: disable=unused-import
+
+    HAS_CRONITER = True
+except ImportError:
+    HAS_CRONITER = False
+
+log = logging.getLogger(__name__)
+
+pytestmark = [
+    pytest.mark.skipif(
+        HAS_DATEUTIL_PARSER is False,
+        reason="The 'dateutil.parser' library is not available",
+    ),
+    pytest.mark.windows_whitelisted,
+]
+
+
+@pytest.mark.slow_test
+def test_eval(setup_teardown_vars):
+    """
+    verify that scheduled job runs
+    """
+    schedule = setup_teardown_vars["schedule"]
+
+    job_name = "test_eval"
+    job = {
+        "schedule": {job_name: {"function": "test.ping", "when": "11/29/2017 4:00pm"}}
+    }
+    run_time2 = dateutil.parser.parse("11/29/2017 4:00pm")
+    run_time1 = run_time2 - datetime.timedelta(seconds=1)
+
+    # Add the job to the scheduler
+    schedule.opts.update(job)
+
+    # Evaluate 1 second before the run time
+    schedule.eval(now=run_time1)
+    ret = schedule.job_status(job_name)
+    assert "_last_run" not in ret
+
+    # Evaluate 1 second at the run time
+    schedule.eval(now=run_time2)
+    ret = schedule.job_status(job_name)
+    assert ret["_last_run"] == run_time2
+
+
+@pytest.mark.slow_test
+def test_eval_multiple_whens(setup_teardown_vars):
+    """
+    verify that scheduled job runs
+    """
+    schedule = setup_teardown_vars["schedule"]
+
+    job_name = "test_eval_multiple_whens"
+    job = {
+        "schedule": {
+            job_name: {
+                "function": "test.ping",
+                "when": ["11/29/2017 4:00pm", "11/29/2017 5:00pm"],
+            }
+        }
+    }
+    if salt.utils.platform.is_darwin():
+        job["schedule"][job_name]["dry_run"] = True
+
+    run_time1 = dateutil.parser.parse("11/29/2017 4:00pm")
+    run_time2 = dateutil.parser.parse("11/29/2017 5:00pm")
+
+    # Add the job to the scheduler
+    schedule.opts.update(job)
+
+    # Evaluate run time1
+    schedule.eval(now=run_time1)
+
+    ret = schedule.job_status(job_name)
+    assert ret["_last_run"] == run_time1
+
+    time.sleep(5)
+
+    # Evaluate run time2
+    schedule.eval(now=run_time2)
+    ret = schedule.job_status(job_name)
+
+    # Give the job a chance to finish
+    time.sleep(5)
+
+    log.info("=== in test_eval_multiple_whens - ret %s ===", ret)
+    assert ret["_last_run"] == run_time2
+
+
+@pytest.mark.slow_test
+def test_eval_whens(setup_teardown_vars):
+    """
+    verify that scheduled job runs
+    """
+    schedule = setup_teardown_vars["schedule"]
+
+    schedule.opts["grains"]["whens"] = {"tea time": "11/29/2017 12:00pm"}
+
+    job_name = "test_eval_whens"
+    job = {"schedule": {job_name: {"function": "test.ping", "when": "tea time"}}}
+    run_time = dateutil.parser.parse("11/29/2017 12:00pm")
+
+    # Add the job to the scheduler
+    schedule.opts.update(job)
+
+    # Evaluate run time1
+    schedule.eval(now=run_time)
+    ret = schedule.job_status(job_name)
+    assert ret["_last_run"] == run_time
+
+
+@pytest.mark.slow_test
+def test_eval_loop_interval(setup_teardown_vars):
+    """
+    verify that scheduled job runs
+    """
+    schedule = setup_teardown_vars["schedule"]
+
+    job_name = "test_eval_loop_interval"
+    job = {
+        "schedule": {job_name: {"function": "test.ping", "when": "11/29/2017 4:00pm"}}
+    }
+    # 30 second loop interval
+    LOOP_INTERVAL = random.randint(30, 59)
+    schedule.opts["loop_interval"] = LOOP_INTERVAL
+
+    run_time2 = dateutil.parser.parse("11/29/2017 4:00pm")
+
+    # Add the job to the scheduler
+    schedule.opts.update(job)
+
+    # Evaluate 1 second at the run time
+    schedule.eval(now=run_time2 + datetime.timedelta(seconds=LOOP_INTERVAL))
+
+    ret = schedule.job_status(job_name)
+    assert ret["_last_run"] == run_time2 + datetime.timedelta(seconds=LOOP_INTERVAL)
+
+
+@pytest.mark.slow_test
+def test_eval_multiple_whens_loop_interval(setup_teardown_vars):
+    """
+    verify that scheduled job runs
+    """
+    schedule = setup_teardown_vars["schedule"]
+
+    job_name = "test_eval_multiple_whens_loop_interval"
+    job = {
+        "schedule": {
+            job_name: {
+                "function": "test.ping",
+                "when": ["11/29/2017 4:00pm", "11/29/2017 5:00pm"],
+            }
+        }
+    }
+    if salt.utils.platform.is_darwin():
+        job["schedule"][job_name]["dry_run"] = True
+
+    # 30 second loop interval
+    LOOP_INTERVAL = random.randint(30, 59)
+    schedule.opts["loop_interval"] = LOOP_INTERVAL
+
+    run_time1 = dateutil.parser.parse("11/29/2017 4:00pm") + datetime.timedelta(
+        seconds=LOOP_INTERVAL
+    )
+    run_time2 = dateutil.parser.parse("11/29/2017 5:00pm") + datetime.timedelta(
+        seconds=LOOP_INTERVAL
+    )
+
+    # Add the job to the scheduler
+    schedule.opts.update(job)
+
+    # Evaluate 1 second at the run time
+    schedule.eval(now=run_time1)
+    ret = schedule.job_status(job_name)
+    assert ret["_last_run"] == run_time1
+
+    time.sleep(5)
+
+    # Evaluate 1 second at the run time
+    schedule.eval(now=run_time2)
+
+    # Give the job a chance to finish
+    time.sleep(5)
+
+    ret = schedule.job_status(job_name)
+    log.info("=== in test_eval_multiple_whens_loop_interval - ret %s ===", ret)
+    assert ret["_last_run"] == run_time2
+
+
+@pytest.mark.slow_test
+def test_eval_once(setup_teardown_vars):
+    """
+    verify that scheduled job runs
+    """
+    schedule = setup_teardown_vars["schedule"]
+
+    job_name = "test_once"
+    job = {
+        "schedule": {job_name: {"function": "test.ping", "once": "2017-12-13T13:00:00"}}
+    }
+    run_time = dateutil.parser.parse("12/13/2017 1:00pm")
+
+    # Add the job to the scheduler
+    schedule.opts["schedule"] = {}
+    schedule.opts.update(job)
+
+    # Evaluate 1 second at the run time
+    schedule.eval(now=run_time)
+    ret = schedule.job_status(job_name)
+    assert ret["_last_run"] == run_time
+
+
+@pytest.mark.slow_test
+def test_eval_once_loop_interval(setup_teardown_vars):
+    """
+    verify that scheduled job runs
+    """
+    schedule = setup_teardown_vars["schedule"]
+
+    job_name = "test_eval_once_loop_interval"
+    job = {
+        "schedule": {job_name: {"function": "test.ping", "once": "2017-12-13T13:00:00"}}
+    }
+    # Randomn second loop interval
+    LOOP_INTERVAL = random.randint(0, 59)
+    schedule.opts["loop_interval"] = LOOP_INTERVAL
+
+    # Run the job at the right plus LOOP_INTERVAL
+    run_time = dateutil.parser.parse("12/13/2017 1:00pm") + datetime.timedelta(
+        seconds=LOOP_INTERVAL
+    )
+
+    # Add the job to the scheduler
+    schedule.opts.update(job)
+
+    # Evaluate at the run time
+    schedule.eval(now=run_time)
+    ret = schedule.job_status(job_name)
+    assert ret["_last_run"] == run_time
+
+
+@skipIf(not HAS_CRONITER, "Cannot find croniter python module")
+def test_eval_cron(setup_teardown_vars):
+    """
+    verify that scheduled job runs
+    """
+    schedule = setup_teardown_vars["schedule"]
+
+    job_name = "test_eval_cron"
+    job = {"schedule": {job_name: {"function": "test.ping", "cron": "0 16 29 11 *"}}}
+
+    # Add the job to the scheduler
+    schedule.opts.update(job)
+
+    run_time = dateutil.parser.parse("11/29/2017 4:00pm")
+    with patch("croniter.croniter.get_next", MagicMock(return_value=run_time)):
+        schedule.eval(now=run_time)
+
+    ret = schedule.job_status(job_name)
+    assert ret["_last_run"] == run_time
+
+
+@skipIf(not HAS_CRONITER, "Cannot find croniter python module")
+def test_eval_cron_loop_interval(setup_teardown_vars):
+    """
+    verify that scheduled job runs
+    """
+    schedule = setup_teardown_vars["schedule"]
+
+    job_name = "test_eval_cron_loop_interval"
+    job = {"schedule": {job_name: {"function": "test.ping", "cron": "0 16 29 11 *"}}}
+    # Randomn second loop interval
+    LOOP_INTERVAL = random.randint(0, 59)
+    schedule.opts["loop_interval"] = LOOP_INTERVAL
+
+    # Add the job to the scheduler
+    schedule.opts.update(job)
+
+    run_time = dateutil.parser.parse("11/29/2017 4:00pm")
+    with patch("croniter.croniter.get_next", MagicMock(return_value=run_time)):
+        schedule.eval(now=run_time)
+
+    ret = schedule.job_status(job_name)
+    assert ret["_last_run"] == run_time
+
+
+@pytest.mark.slow_test
+def test_eval_until(setup_teardown_vars):
+    """
+    verify that scheduled job is skipped once the current
+    time reaches the specified until time
+    """
+    schedule = setup_teardown_vars["schedule"]
+
+    job_name = "test_eval_until"
+    job = {
+        "schedule": {
+            job_name: {
+                "function": "test.ping",
+                "hours": "1",
+                "until": "11/29/2017 5:00pm",
+            }
+        }
+    }
+
+    if salt.utils.platform.is_darwin():
+        job["schedule"][job_name]["dry_run"] = True
+
+    # Add job to schedule
+    schedule.delete_job("test_eval_until")
+    schedule.opts.update(job)
+
+    # eval at 2:00pm to prime, simulate minion start up.
+    run_time = dateutil.parser.parse("11/29/2017 2:00pm")
+    schedule.eval(now=run_time)
+    ret = schedule.job_status(job_name)
+
+    # eval at 3:00pm, will run.
+    run_time = dateutil.parser.parse("11/29/2017 3:00pm")
+    schedule.eval(now=run_time)
+    ret = schedule.job_status(job_name)
+    assert ret["_last_run"] == run_time
+
+    time.sleep(2)
+
+    # eval at 4:00pm, will run.
+    run_time = dateutil.parser.parse("11/29/2017 4:00pm")
+    schedule.eval(now=run_time)
+    ret = schedule.job_status(job_name)
+    assert ret["_last_run"] == run_time
+
+    time.sleep(2)
+
+    # eval at 5:00pm, will not run
+    run_time = dateutil.parser.parse("11/29/2017 5:00pm")
+    schedule.eval(now=run_time)
+    ret = schedule.job_status(job_name)
+    assert ret["_skip_reason"] == "until_passed"
+    assert ret["_skipped_time"] == run_time
+
+
+@pytest.mark.slow_test
+def test_eval_after(setup_teardown_vars):
+    """
+    verify that scheduled job is skipped until after the specified
+    time has been reached.
+    """
+    schedule = setup_teardown_vars["schedule"]
+
+    job_name = "test_eval_after"
+    job = {
+        "schedule": {
+            job_name: {
+                "function": "test.ping",
+                "hours": "1",
+                "after": "11/29/2017 5:00pm",
+            }
+        }
+    }
+
+    # Add job to schedule
+    schedule.opts.update(job)
+
+    # eval at 2:00pm to prime, simulate minion start up.
+    run_time = dateutil.parser.parse("11/29/2017 2:00pm")
+    schedule.eval(now=run_time)
+    ret = schedule.job_status(job_name)
+
+    # eval at 3:00pm, will not run.
+    run_time = dateutil.parser.parse("11/29/2017 3:00pm")
+    schedule.eval(now=run_time)
+    ret = schedule.job_status(job_name)
+    assert ret["_skip_reason"] == "after_not_passed"
+    assert ret["_skipped_time"] == run_time
+
+    # eval at 4:00pm, will not run.
+    run_time = dateutil.parser.parse("11/29/2017 4:00pm")
+    schedule.eval(now=run_time)
+    ret = schedule.job_status(job_name)
+    assert ret["_skip_reason"] == "after_not_passed"
+    assert ret["_skipped_time"] == run_time
+
+    # eval at 5:00pm, will not run
+    run_time = dateutil.parser.parse("11/29/2017 5:00pm")
+    schedule.eval(now=run_time)
+    ret = schedule.job_status(job_name)
+    assert ret["_skip_reason"] == "after_not_passed"
+    assert ret["_skipped_time"] == run_time
+
+    # eval at 6:00pm, will run
+    run_time = dateutil.parser.parse("11/29/2017 6:00pm")
+    schedule.eval(now=run_time)
+    ret = schedule.job_status(job_name)
+    assert ret["_last_run"] == run_time
+
+
+@pytest.mark.slow_test
+def test_eval_enabled(setup_teardown_vars):
+    """
+    verify that scheduled job does not run
+    """
+    schedule = setup_teardown_vars["schedule"]
+
+    job_name = "test_eval_enabled"
+    job = {
+        "schedule": {
+            "enabled": True,
+            job_name: {"function": "test.ping", "when": "11/29/2017 4:00pm"},
+        }
+    }
+    run_time1 = dateutil.parser.parse("11/29/2017 4:00pm")
+
+    # Add the job to the scheduler
+    schedule.opts.update(job)
+
+    # Evaluate 1 second at the run time
+    schedule.eval(now=run_time1)
+    ret = schedule.job_status(job_name)
+    assert ret["_last_run"] == run_time1
+
+
+@pytest.mark.slow_test
+def test_eval_enabled_key(setup_teardown_vars):
+    """
+    verify that scheduled job runs
+    when the enabled key is in place
+    https://github.com/saltstack/salt/issues/47695
+    """
+    schedule = setup_teardown_vars["schedule"]
+
+    job_name = "test_eval_enabled_key"
+    job = {
+        "schedule": {
+            "enabled": True,
+            job_name: {"function": "test.ping", "when": "11/29/2017 4:00pm"},
+        }
+    }
+    run_time2 = dateutil.parser.parse("11/29/2017 4:00pm")
+    run_time1 = run_time2 - datetime.timedelta(seconds=1)
+
+    # Add the job to the scheduler
+    schedule.opts.update(job)
+
+    # Evaluate 1 second before the run time
+    schedule.eval(now=run_time1)
+    ret = schedule.job_status("test_eval_enabled_key")
+    assert "_last_run" not in ret
+
+    # Evaluate 1 second at the run time
+    schedule.eval(now=run_time2)
+    ret = schedule.job_status("test_eval_enabled_key")
+    assert ret["_last_run"] == run_time2
+
+
+def test_eval_disabled(setup_teardown_vars):
+    """
+    verify that scheduled job does not run
+    """
+    schedule = setup_teardown_vars["schedule"]
+
+    job_name = "test_eval_disabled"
+    job = {
+        "schedule": {
+            "enabled": False,
+            job_name: {"function": "test.ping", "when": "11/29/2017 4:00pm"},
+        }
+    }
+    run_time1 = dateutil.parser.parse("11/29/2017 4:00pm")
+
+    # Add the job to the scheduler
+    schedule.opts.update(job)
+
+    # Evaluate 1 second at the run time
+    schedule.eval(now=run_time1)
+    ret = schedule.job_status(job_name)
+    assert "_last_run" not in ret
+    assert ret["_skip_reason"] == "disabled"
+
+    # Ensure job data still matches
+    assert ret == job["schedule"][job_name]
+
+
+def test_eval_global_disabled_job_enabled(setup_teardown_vars):
+    """
+    verify that scheduled job does not run
+    """
+    schedule = setup_teardown_vars["schedule"]
+
+    job_name = "test_eval_global_disabled"
+    job = {
+        "schedule": {
+            "enabled": False,
+            job_name: {
+                "function": "test.ping",
+                "when": "11/29/2017 4:00pm",
+                "enabled": True,
+            },
+        }
+    }
+    run_time1 = dateutil.parser.parse("11/29/2017 4:00pm")
+
+    # Add the job to the scheduler
+    schedule.opts.update(job)
+
+    # Evaluate 1 second at the run time
+    schedule.eval(now=run_time1)
+    ret = schedule.job_status(job_name)
+    assert "_last_run" not in ret
+    assert ret["_skip_reason"] == "disabled"
+
+    # Ensure job is still enabled
+    assert ret["enabled"]
+
+
+@pytest.mark.slow_test
+def test_eval_run_on_start(setup_teardown_vars):
+    """
+    verify that scheduled job is run when minion starts
+    """
+    schedule = setup_teardown_vars["schedule"]
+
+    job_name = "test_eval_run_on_start"
+    job = {
+        "schedule": {
+            job_name: {"function": "test.ping", "hours": "1", "run_on_start": True}
+        }
+    }
+
+    # Add job to schedule
+    schedule.opts.update(job)
+
+    # eval at 2:00pm, will run.
+    run_time = dateutil.parser.parse("11/29/2017 2:00pm")
+    schedule.eval(now=run_time)
+    ret = schedule.job_status(job_name)
+    assert ret["_last_run"] == run_time
+
+    # eval at 3:00pm, will run.
+    run_time = dateutil.parser.parse("11/29/2017 3:00pm")
+    schedule.eval(now=run_time)
+    ret = schedule.job_status(job_name)
+
+
+@pytest.mark.slow_test
+def test_eval_splay(setup_teardown_vars):
+    """
+    verify that scheduled job runs with splayed time
+    """
+    schedule = setup_teardown_vars["schedule"]
+
+    job_name = "job_eval_splay"
+    job = {
+        "schedule": {
+            job_name: {"function": "test.ping", "seconds": "30", "splay": "10"}
+        }
+    }
+
+    # Add job to schedule
+    schedule.opts.update(job)
+
+    with patch("random.randint", MagicMock(return_value=10)):
+        # eval at 2:00pm to prime, simulate minion start up.
+        run_time = dateutil.parser.parse("11/29/2017 2:00pm")
+        schedule.eval(now=run_time)
+        ret = schedule.job_status(job_name)
+
+        # eval at 2:00:40pm, will run.
+        run_time = dateutil.parser.parse("11/29/2017 2:00:40pm")
+        schedule.eval(now=run_time)
+        ret = schedule.job_status(job_name)
+        assert ret["_last_run"] == run_time
+
+
+@pytest.mark.slow_test
+def test_eval_splay_range(setup_teardown_vars):
+    """
+    verify that scheduled job runs with splayed time
+    """
+    schedule = setup_teardown_vars["schedule"]
+
+    job_name = "job_eval_splay_range"
+    job = {
+        "schedule": {
+            job_name: {
+                "function": "test.ping",
+                "seconds": "30",
+                "splay": {"start": 5, "end": 10},
+            }
+        }
+    }
+
+    # Add job to schedule
+    schedule.opts.update(job)
+
+    with patch("random.randint", MagicMock(return_value=10)):
+        # eval at 2:00pm to prime, simulate minion start up.
+        run_time = dateutil.parser.parse("11/29/2017 2:00pm")
+        schedule.eval(now=run_time)
+        ret = schedule.job_status(job_name)
+
+        # eval at 2:00:40pm, will run.
+        run_time = dateutil.parser.parse("11/29/2017 2:00:40pm")
+        schedule.eval(now=run_time)
+        ret = schedule.job_status(job_name)
+        assert ret["_last_run"] == run_time
+
+
+@pytest.mark.slow_test
+def test_eval_splay_global(setup_teardown_vars):
+    """
+    verify that scheduled job runs with splayed time
+    """
+    schedule = setup_teardown_vars["schedule"]
+
+    job_name = "job_eval_splay_global"
+    job = {
+        "schedule": {
+            "splay": {"start": 5, "end": 10},
+            job_name: {"function": "test.ping", "seconds": "30"},
+        }
+    }
+
+    # Add job to schedule
+    schedule.opts.update(job)
+
+    with patch("random.randint", MagicMock(return_value=10)):
+        # eval at 2:00pm to prime, simulate minion start up.
+        run_time = dateutil.parser.parse("11/29/2017 2:00pm")
+        schedule.eval(now=run_time)
+        ret = schedule.job_status(job_name)
+
+        # eval at 2:00:40pm, will run.
+        run_time = dateutil.parser.parse("11/29/2017 2:00:40pm")
+        schedule.eval(now=run_time)
+        ret = schedule.job_status(job_name)
+        assert ret["_last_run"] == run_time
+
+
+@pytest.mark.slow_test
+def test_eval_seconds(setup_teardown_vars):
+    """
+    verify that scheduled job run mutiple times with seconds
+    """
+    schedule = setup_teardown_vars["schedule"]
+
+    with patch.dict(schedule.opts, {"run_schedule_jobs_in_background": False}):
+        job_name = "job_eval_seconds"
+        job = {"schedule": {job_name: {"function": "test.ping", "seconds": "30"}}}
+
+        if salt.utils.platform.is_darwin():
+            job["schedule"][job_name]["dry_run"] = True
+
+        # Add job to schedule
+        schedule.opts.update(job)
+
+        # eval at 2:00pm to prime, simulate minion start up.
+        run_time = dateutil.parser.parse("11/29/2017 2:00pm")
+        next_run_time = run_time + datetime.timedelta(seconds=30)
+        jids = schedule.eval(now=run_time)
+        assert len(jids) == 0
+        ret = schedule.job_status(job_name)
+        assert ret["_next_fire_time"] == next_run_time
+
+        # eval at 2:00:01pm, will not run.
+        run_time = dateutil.parser.parse("11/29/2017 2:00:01pm")
+        jids = schedule.eval(now=run_time)
+        assert len(jids) == 0
+        ret = schedule.job_status(job_name)
+        assert "_last_run" not in ret
+        assert ret["_next_fire_time"] == next_run_time
+
+        # eval at 2:00:30pm, will run.
+        run_time = dateutil.parser.parse("11/29/2017 2:00:30pm")
+        next_run_time = run_time + datetime.timedelta(seconds=30)
+        jids = schedule.eval(now=run_time)
+        assert len(jids) == 1
+        ret = schedule.job_status(job_name)
+        assert ret["_last_run"] == run_time
+        assert ret["_next_fire_time"] == next_run_time
+
+        # eval at 2:01:00pm, will run.
+        run_time = dateutil.parser.parse("11/29/2017 2:01:00pm")
+        next_run_time = run_time + datetime.timedelta(seconds=30)
+        jids = schedule.eval(now=run_time)
+        assert len(jids) == 1
+        ret = schedule.job_status(job_name)
+        assert ret["_last_run"] == run_time
+        assert ret["_next_fire_time"] == next_run_time
+
+        # eval at 2:01:30pm, will run.
+        run_time = dateutil.parser.parse("11/29/2017 2:01:30pm")
+        next_run_time = run_time + datetime.timedelta(seconds=30)
+        jids = schedule.eval(now=run_time)
+        assert len(jids) == 1
+        ret = schedule.job_status(job_name)
+        assert ret["_last_run"] == run_time
+        assert ret["_next_fire_time"] == next_run_time
+
+
+@pytest.mark.slow_test
+def test_eval_minutes(setup_teardown_vars):
+    """
+    verify that scheduled job run mutiple times with minutes
+    """
+    schedule = setup_teardown_vars["schedule"]
+
+    with patch.dict(schedule.opts, {"run_schedule_jobs_in_background": False}):
+        job_name = "job_eval_minutes"
+        job = {"schedule": {job_name: {"function": "test.ping", "minutes": "30"}}}
+
+        if salt.utils.platform.is_darwin():
+            job["schedule"][job_name]["dry_run"] = True
+
+        # Add job to schedule
+        schedule.opts.update(job)
+
+        # eval at 2:00pm to prime, simulate minion start up.
+        run_time = dateutil.parser.parse("11/29/2017 2:00pm")
+        next_run_time = run_time + datetime.timedelta(minutes=30)
+        jids = schedule.eval(now=run_time)
+        assert len(jids) == 0
+        ret = schedule.job_status(job_name)
+        assert ret["_next_fire_time"] == next_run_time
+
+        # eval at 2:00:01pm, will not run.
+        run_time = dateutil.parser.parse("11/29/2017 2:00:01pm")
+        jids = schedule.eval(now=run_time)
+        assert len(jids) == 0
+        ret = schedule.job_status(job_name)
+        assert "_last_run" not in ret
+        assert ret["_next_fire_time"] == next_run_time
+
+        # eval at 2:30:00pm, will run.
+        run_time = dateutil.parser.parse("11/29/2017 2:30:00pm")
+        jids = schedule.eval(now=run_time)
+        assert len(jids) == 1
+        ret = schedule.job_status(job_name)
+        assert ret["_last_run"] == run_time
+
+        # eval at 3:00:00pm, will run.
+        run_time = dateutil.parser.parse("11/29/2017 3:00:00pm")
+        jids = schedule.eval(now=run_time)
+        assert len(jids) == 1
+        ret = schedule.job_status(job_name)
+        assert ret["_last_run"] == run_time
+
+        # eval at 3:30:00pm, will run.
+        run_time = dateutil.parser.parse("11/29/2017 3:30:00pm")
+        jids = schedule.eval(now=run_time)
+        assert len(jids) == 1
+        ret = schedule.job_status(job_name)
+        assert ret["_last_run"] == run_time
+
+
+@pytest.mark.slow_test
+def test_eval_hours(setup_teardown_vars):
+    """
+    verify that scheduled job run mutiple times with hours
+    """
+    schedule = setup_teardown_vars["schedule"]
+
+    with patch.dict(schedule.opts, {"run_schedule_jobs_in_background": False}):
+        job_name = "job_eval_hours"
+        job = {"schedule": {job_name: {"function": "test.ping", "hours": "2"}}}
+
+        if salt.utils.platform.is_darwin():
+            job["schedule"][job_name]["dry_run"] = True
+
+        # Add job to schedule
+        schedule.opts.update(job)
+
+        # eval at 2:00pm to prime, simulate minion start up.
+        run_time = dateutil.parser.parse("11/29/2017 2:00pm")
+        next_run_time = run_time + datetime.timedelta(hours=2)
+        jids = schedule.eval(now=run_time)
+        assert len(jids) == 0
+        ret = schedule.job_status(job_name)
+        assert ret["_next_fire_time"] == next_run_time
+
+        # eval at 2:00:01pm, will not run.
+        run_time = dateutil.parser.parse("11/29/2017 2:00:01pm")
+        jids = schedule.eval(now=run_time)
+        assert len(jids) == 0
+        ret = schedule.job_status(job_name)
+        assert "_last_run" not in ret
+        assert ret["_next_fire_time"] == next_run_time
+
+        # eval at 4:00:00pm, will run.
+        run_time = dateutil.parser.parse("11/29/2017 4:00:00pm")
+        jids = schedule.eval(now=run_time)
+        assert len(jids) == 1
+        ret = schedule.job_status(job_name)
+        assert ret["_last_run"] == run_time
+
+        # eval at 6:00:00pm, will run.
+        run_time = dateutil.parser.parse("11/29/2017 6:00:00pm")
+        jids = schedule.eval(now=run_time)
+        assert len(jids) == 1
+
+        ret = schedule.job_status(job_name)
+        assert ret["_last_run"] == run_time
+
+        # eval at 8:00:00pm, will run.
+        run_time = dateutil.parser.parse("11/29/2017 8:00:00pm")
+        pids = schedule.eval(now=run_time)
+        assert len(jids) == 1
+
+        ret = schedule.job_status(job_name)
+        assert ret["_last_run"] == run_time
+
+
+@pytest.mark.slow_test
+def test_eval_days(setup_teardown_vars):
+    """
+    verify that scheduled job run mutiple times with days
+    """
+    schedule = setup_teardown_vars["schedule"]
+
+    job_name = "job_eval_days"
+    job = {
+        "schedule": {job_name: {"function": "test.ping", "days": "2", "dry_run": True}}
+    }
+
+    if salt.utils.platform.is_darwin():
+        job["schedule"][job_name]["dry_run"] = True
+
+    # Add job to schedule
+    schedule.opts.update(job)
+
+    # eval at 11/23/2017 2:00pm to prime, simulate minion start up.
+    run_time = dateutil.parser.parse("11/23/2017 2:00pm")
+    next_run_time = run_time + datetime.timedelta(days=2)
+    schedule.eval(now=run_time)
+    ret = schedule.job_status(job_name)
+    assert ret["_next_fire_time"] == next_run_time
+
+    # eval at 11/25/2017 2:00:00pm, will run.
+    run_time = dateutil.parser.parse("11/25/2017 2:00:00pm")
+    next_run_time = run_time + datetime.timedelta(days=2)
+    schedule.eval(now=run_time)
+    ret = schedule.job_status(job_name)
+    assert ret["_last_run"] == run_time
+    assert ret["_next_fire_time"] == next_run_time
+
+    # eval at 11/26/2017 2:00:00pm, will not run.
+    run_time = dateutil.parser.parse("11/26/2017 2:00:00pm")
+    last_run_time = run_time - datetime.timedelta(days=1)
+    schedule.eval(now=run_time)
+    ret = schedule.job_status(job_name)
+    assert ret["_last_run"] == last_run_time
+    assert ret["_next_fire_time"] == next_run_time
+
+    time.sleep(2)
+
+    # eval at 11/27/2017 2:00:00pm, will run.
+    run_time = dateutil.parser.parse("11/27/2017 2:00:00pm")
+    next_run_time = run_time + datetime.timedelta(days=2)
+    schedule.eval(now=run_time)
+    ret = schedule.job_status(job_name)
+    assert ret["_last_run"] == run_time
+    assert ret["_next_fire_time"] == next_run_time
+
+    time.sleep(2)
+
+    # eval at 11/28/2017 2:00:00pm, will not run.
+    run_time = dateutil.parser.parse("11/28/2017 2:00:00pm")
+    last_run_time = run_time - datetime.timedelta(days=1)
+    schedule.eval(now=run_time)
+    ret = schedule.job_status(job_name)
+    assert ret["_last_run"] == last_run_time
+    assert ret["_next_fire_time"] == next_run_time
+
+    time.sleep(2)
+
+    # eval at 11/29/2017 2:00:00pm, will run.
+    run_time = dateutil.parser.parse("11/29/2017 2:00:00pm")
+    next_run_time = run_time + datetime.timedelta(days=2)
+    schedule.eval(now=run_time)
+    ret = schedule.job_status(job_name)
+    assert ret["_last_run"] == run_time
+    assert ret["_next_fire_time"] == next_run_time
+
+
+@pytest.mark.slow_test
+def test_eval_when_splay(setup_teardown_vars):
+    """
+    verify that scheduled job runs
+    """
+    schedule = setup_teardown_vars["schedule"]
+
+    job_name = "test_eval_when_splay"
+    splay = 300
+    job = {
+        "schedule": {
+            job_name: {
+                "function": "test.ping",
+                "when": "11/29/2017 4:00pm",
+                "splay": splay,
+            }
+        }
+    }
+    run_time1 = dateutil.parser.parse("11/29/2017 4:00pm")
+    run_time2 = run_time1 + datetime.timedelta(seconds=splay)
+    run_time3 = run_time2 + datetime.timedelta(seconds=1)
+
+    # Add the job to the scheduler
+    schedule.opts.update(job)
+
+    with patch("random.randint", MagicMock(return_value=splay)):
+        # Evaluate to prime
+        run_time = dateutil.parser.parse("11/29/2017 3:00pm")
+        schedule.eval(now=run_time)
+        ret = schedule.job_status(job_name)
+
+        # Evaluate at expected runtime1, should not run
+        schedule.eval(now=run_time1)
+        ret = schedule.job_status(job_name)
+        assert "_last_run" not in ret
+
+        # Evaluate at expected runtime2, should run
+        schedule.eval(now=run_time2)
+        ret = schedule.job_status(job_name)
+        assert ret["_last_run"] == run_time2
+
+        # Evaluate at expected runtime3, should not run
+        # _next_fire_time should be None
+        schedule.eval(now=run_time3)
+        ret = schedule.job_status(job_name)
+        assert ret["_last_run"] == run_time2
+        assert ret["_next_fire_time"] is None
+
+
+def test_eval_when_splay_in_past(setup_teardown_vars):
+    """
+    verify that scheduled job runs
+    """
+    schedule = setup_teardown_vars["schedule"]
+
+    job_name = "test_eval_when_splay_in_past"
+    splay = 300
+    job = {
+        "schedule": {
+            job_name: {
+                "function": "test.ping",
+                "when": ["11/29/2017 6:00am"],
+                "splay": splay,
+            }
+        }
+    }
+    run_time1 = dateutil.parser.parse("11/29/2017 4:00pm")
+
+    # Add the job to the scheduler
+    schedule.opts.update(job)
+
+    # Evaluate to prime
+    run_time = dateutil.parser.parse("11/29/2017 3:00pm")
+    schedule.eval(now=run_time)
+    ret = schedule.job_status(job_name)
+
+    # Evaluate at expected runtime1, should not run
+    # and _next_fire_time should be None
+    schedule.eval(now=run_time1)
+    ret = schedule.job_status(job_name)
+    assert "_last_run" not in ret
+    assert ret["_next_fire_time"] is None
diff --git a/tests/pytests/unit/utils/scheduler/test_helpers.py b/tests/pytests/unit/utils/scheduler/test_helpers.py
new file mode 100644
index 000000000000..1ed05835a987
--- /dev/null
+++ b/tests/pytests/unit/utils/scheduler/test_helpers.py
@@ -0,0 +1,32 @@
+import logging
+
+import pytest
+
+log = logging.getLogger(__name__)
+
+
+@pytest.fixture()
+def loop_interval(schedule):
+    schedule.opts["loop_interval"] = 1
+
+
+def test_get_schedule(setup_teardown_vars):
+    """
+    verify that the _get_schedule function works
+    when remove_hidden is True and schedule data
+    contains enabled key
+    """
+    schedule = setup_teardown_vars["schedule"]
+
+    job_name = "test_get_schedule"
+    job = {
+        "schedule": {
+            "enabled": True,
+            job_name: {"function": "test.ping", "seconds": 60},
+        }
+    }
+    # Add the job to the scheduler
+    schedule.opts.update(job)
+
+    ret = schedule._get_schedule(remove_hidden=True)
+    assert job["schedule"] == ret
diff --git a/tests/pytests/unit/utils/scheduler/test_maxrunning.py b/tests/pytests/unit/utils/scheduler/test_maxrunning.py
new file mode 100644
index 000000000000..777f62a27a2d
--- /dev/null
+++ b/tests/pytests/unit/utils/scheduler/test_maxrunning.py
@@ -0,0 +1,131 @@
+import logging
+
+import pytest
+from tests.support.mock import MagicMock, patch
+
+try:
+    import dateutil.parser as dateutil_parser
+
+    HAS_DATEUTIL_PARSER = True
+except ImportError:
+    HAS_DATEUTIL_PARSER = False
+
+
+log = logging.getLogger(__name__)
+
+pytestmark = [
+    pytest.mark.skipif(
+        HAS_DATEUTIL_PARSER is False,
+        reason="The 'dateutil.parser' library is not available",
+    ),
+    pytest.mark.windows_whitelisted,
+]
+
+
+def test_maxrunning_minion(setup_teardown_vars):
+    """
+    verify that scheduled job runs
+    """
+    schedule = setup_teardown_vars["schedule"]
+    schedule.opts["__role"] = "minion"
+
+    job = {
+        "schedule": {
+            "maxrunning_minion": {
+                "function": "test.ping",
+                "seconds": 10,
+                "maxrunning": 1,
+            }
+        }
+    }
+
+    job_data = {
+        "function": "test.ping",
+        "run": True,
+        "name": "maxrunning_minion",
+        "seconds": 10,
+        "_seconds": 10,
+        "jid_include": True,
+        "maxrunning": 1,
+    }
+
+    # Add the job to the scheduler
+    schedule.opts.update(job)
+
+    running_data = [
+        {
+            "fun_args": [],
+            "jid": "20181018165923360935",
+            "schedule": "maxrunning_minion",
+            "pid": 15338,
+            "fun": "test.ping",
+            "id": "host",
+        }
+    ]
+
+    run_time = dateutil_parser.parse("11/29/2017 4:00pm")
+
+    with patch("salt.utils.minion.running", MagicMock(return_value=running_data)):
+        with patch("salt.utils.process.os_is_running", MagicMock(return_value=True)):
+            ret = schedule._check_max_running(
+                "test.ping", job_data, schedule.opts, now=run_time
+            )
+    assert "_skip_reason" in ret
+    assert "maxrunning" == ret["_skip_reason"]
+    assert not ret["run"]
+
+
+def test_maxrunning_master(setup_teardown_vars):
+    """
+    verify that scheduled job runs
+    """
+    schedule = setup_teardown_vars["schedule"]
+    schedule.opts["__role"] = "master"
+
+    job = {
+        "schedule": {
+            "maxrunning_master": {
+                "function": "state.orch",
+                "args": ["test.orch_test"],
+                "minutes": 1,
+                "maxrunning": 1,
+            }
+        }
+    }
+
+    job_data = {
+        "function": "state.orch",
+        "fun_args": ["test.orch_test"],
+        "run": True,
+        "name": "maxrunning_master",
+        "minutes": 1,
+        "jid_include": True,
+        "maxrunning": 1,
+    }
+
+    # Add the job to the scheduler
+    schedule.opts.update(job)
+
+    running_data = [
+        {
+            "fun_args": ["test.orch_test"],
+            "jid": "20181018165923360935",
+            "schedule": "maxrunning_master",
+            "pid": 15338,
+            "fun": "state.orch",
+            "id": "host",
+        }
+    ]
+
+    run_time = dateutil_parser.parse("11/29/2017 4:00pm")
+
+    with patch(
+        "salt.utils.master.get_running_jobs", MagicMock(return_value=running_data)
+    ):
+        with patch("salt.utils.process.os_is_running", MagicMock(return_value=True)):
+            ret = schedule._check_max_running(
+                "state.orch", job_data, schedule.opts, now=run_time
+            )
+    assert "_skip_reason" in ret
+    assert "maxrunning" == ret["_skip_reason"]
+    assert not ret["run"]
diff --git a/tests/pytests/unit/utils/scheduler/test_postpone.py b/tests/pytests/unit/utils/scheduler/test_postpone.py
new file mode 100644
index 000000000000..780b8f2bb7b9
--- /dev/null
+++ b/tests/pytests/unit/utils/scheduler/test_postpone.py
@@ -0,0 +1,64 @@
+import datetime
+import logging
+
+import pytest
+
+try:
+    import dateutil.parser
+
+    HAS_DATEUTIL_PARSER = True
+except ImportError:
+    HAS_DATEUTIL_PARSER = False
+
+log = logging.getLogger(__name__)
+
+pytestmark = [
+    pytest.mark.skipif(
+        HAS_DATEUTIL_PARSER is False,
+        reason="The 'dateutil.parser' library is not available",
+    ),
+    pytest.mark.windows_whitelisted,
+]
+
+
+@pytest.mark.slow_test
+def test_postpone(setup_teardown_vars):
+    """
+    verify that scheduled job is postponed until the specified time.
+    """
+    schedule = setup_teardown_vars["schedule"]
+    job = {"schedule": {"job1": {"function": "test.ping", "when": "11/29/2017 4pm"}}}
+
+    # 11/29/2017 4pm
+    run_time = dateutil.parser.parse("11/29/2017 4:00pm")
+
+    # 5 minute delay
+    delay = 300
+
+    # Add job to schedule
+    schedule.opts.update(job)
+
+    # Postpone the job by 5 minutes
+    schedule.postpone_job(
+        "job1",
+        {
+            "time": run_time.strftime("%Y-%m-%dT%H:%M:%S"),
+            "new_time": (run_time + datetime.timedelta(seconds=delay)).strftime(
+                "%Y-%m-%dT%H:%M:%S"
+            ),
+        },
+    )
+    # Run at the original time
+    schedule.eval(now=run_time)
+    ret = schedule.job_status("job1")
+    assert "_last_run" not in ret
+
+    # Run 5 minutes later
+    schedule.eval(now=run_time + datetime.timedelta(seconds=delay))
+    ret = schedule.job_status("job1")
+    assert ret["_last_run"] == run_time + datetime.timedelta(seconds=delay)
+
+    # Run 6 minutes later
+    schedule.eval(now=run_time + datetime.timedelta(seconds=delay + 1))
+    ret = schedule.job_status("job1")
+    assert ret["_last_run"] == run_time + datetime.timedelta(seconds=delay)
diff --git a/tests/pytests/unit/utils/scheduler/test_run_job.py b/tests/pytests/unit/utils/scheduler/test_run_job.py
new file mode 100644
index 000000000000..9b2b420fa4df
--- /dev/null
+++ b/tests/pytests/unit/utils/scheduler/test_run_job.py
@@ -0,0 +1,20 @@
+import logging
+
+log = logging.getLogger(__name__)
+
+
+def test_run_job(setup_teardown_vars):
+    """
+    verify that scheduled job runs
+    """
+    schedule = setup_teardown_vars["schedule"]
+    job_name = "test_run_job"
+    job = {"schedule": {job_name: {"function": "test.ping"}}}
+    # Add the job to the scheduler
+    schedule.opts.update(job)
+
+    # Run job
+    schedule.run_job(job_name)
+    ret = schedule.job_status(job_name)
+    expected = {"function": "test.ping", "run": True, "name": "test_run_job"}
+    assert ret == expected
diff --git a/tests/pytests/unit/utils/scheduler/test_schedule.py b/tests/pytests/unit/utils/scheduler/test_schedule.py
new file mode 100644
index 000000000000..6f9463f507e9
--- /dev/null
+++ b/tests/pytests/unit/utils/scheduler/test_schedule.py
@@ -0,0 +1,508 @@
+"""
+    :codeauthor: Nicole Thomas <nicole@saltstack.com>
+"""
+
+import copy
+import datetime
+import logging
+
+import pytest
+import salt.config
+import salt.utils.schedule
+from salt.utils.schedule import Schedule
+from tests.support.mock import MagicMock, patch
+
+# pylint: disable=import-error,unused-import
+try:
+    import croniter
+
+    _CRON_SUPPORTED = True
+except ImportError:
+    _CRON_SUPPORTED = False
+# pylint: enable=import-error
+
+log = logging.getLogger(__name__)
+
+
+# pylint: disable=too-many-public-methods,invalid-name
+# delete_job tests
+@pytest.mark.slow_test
+def test_delete_job_exists(setup_teardown_vars):
+    """
+    Tests ensuring the job exists and deleting it
+    """
+    schedule = setup_teardown_vars["schedule"]
+    schedule.opts.update({"schedule": {"foo": "bar"}, "pillar": {}})
+    assert "foo" in schedule.opts["schedule"]
+
+    schedule.delete_job("foo")
+    assert "foo" not in schedule.opts["schedule"]
+
+
+@pytest.mark.slow_test
+def test_delete_job_in_pillar(setup_teardown_vars):
+    """
+    Tests ignoring deletion job from pillar
+    """
+    schedule = setup_teardown_vars["schedule"]
+    schedule.opts.update({"pillar": {"schedule": {"foo": "bar"}}, "schedule": {}})
+    assert "foo" in schedule.opts["pillar"]["schedule"]
+    schedule.delete_job("foo")
+
+    assert "foo" in schedule.opts["pillar"]["schedule"]
+
+
+@pytest.mark.slow_test
+def test_delete_job_intervals(setup_teardown_vars):
+    """
+    Tests removing job from intervals
+    """
+    schedule = setup_teardown_vars["schedule"]
+    schedule.opts.update({"pillar": {}, "schedule": {}})
+    schedule.intervals = {"foo": "bar"}
+    schedule.delete_job("foo")
+    assert "foo" not in schedule.intervals
+
+
+@pytest.mark.slow_test
+def test_delete_job_prefix(setup_teardown_vars):
+    """
+    Tests ensuring jobs exists and deleting them by prefix
+    """
+    schedule = setup_teardown_vars["schedule"]
+    schedule.opts.update(
+        {"schedule": {"foobar": "bar", "foobaz": "baz", "fooboo": "boo"}, "pillar": {}}
+    )
+    ret = copy.deepcopy(schedule.opts)
+    del ret["schedule"]["foobar"]
+    del ret["schedule"]["foobaz"]
+    schedule.delete_job_prefix("fooba")
+    assert schedule.opts == ret
+
+
+@pytest.mark.slow_test
+def test_delete_job_prefix_in_pillar(setup_teardown_vars):
+    """
+    Tests ignoring deletion jobs by prefix from pillar
+    """
+    schedule = setup_teardown_vars["schedule"]
+    schedule.opts.update(
+        {
+            "pillar": {"schedule": {"foobar": "bar", "foobaz": "baz", "fooboo": "boo"}},
+            "schedule": {},
+        }
+    )
+    ret = copy.deepcopy(schedule.opts)
+    schedule.delete_job_prefix("fooba")
+    assert schedule.opts == ret
+
+
+# add_job tests
+def test_add_job_data_not_dict(setup_teardown_vars):
+    """
+    Tests if data is a dictionary
+    """
+    schedule = setup_teardown_vars["schedule"]
+    data = "foo"
+    pytest.raises(ValueError, Schedule.add_job, schedule, data)
+
+
+def test_add_job_multiple_jobs(setup_teardown_vars):
+    """
+    Tests if more than one job is scheduled at a time
+    """
+    schedule = setup_teardown_vars["schedule"]
+    data = {"key1": "value1", "key2": "value2"}
+    pytest.raises(ValueError, Schedule.add_job, schedule, data)
+
+
+@pytest.mark.slow_test
+def test_add_job(setup_teardown_vars):
+    """
+    Tests adding a job to the schedule
+    """
+    schedule = setup_teardown_vars["schedule"]
+    data = {"foo": {"bar": "baz"}}
+    ret = copy.deepcopy(schedule.opts)
+    ret.update(
+        {
+            "schedule": {
+                "foo": {"bar": "baz", "enabled": True},
+                "hello": {"world": "peace", "enabled": True},
+            },
+            "pillar": {},
+        }
+    )
+    schedule.opts.update(
+        {"schedule": {"hello": {"world": "peace", "enabled": True}}, "pillar": {}}
+    )
+    Schedule.add_job(schedule, data)
+    assert schedule.opts == ret
+
+
+# enable_job tests
+@pytest.mark.slow_test
+def test_enable_job(setup_teardown_vars):
+    """
+    Tests enabling a job
+    """
+    schedule = setup_teardown_vars["schedule"]
+    schedule.opts.update({"schedule": {"name": {"enabled": "foo"}}})
+    Schedule.enable_job(schedule, "name")
+    assert schedule.opts["schedule"]["name"]["enabled"]
+
+
+@pytest.mark.slow_test
+def test_enable_job_pillar(setup_teardown_vars):
+    """
+    Tests ignoring enable a job from pillar
+    """
+    schedule = setup_teardown_vars["schedule"]
+    schedule.opts.update({"pillar": {"schedule": {"name": {"enabled": False}}}})
+    Schedule.enable_job(schedule, "name", persist=False)
+    assert not schedule.opts["pillar"]["schedule"]["name"]["enabled"]
+
+
+# disable_job tests
+@pytest.mark.slow_test
+def test_disable_job(setup_teardown_vars):
+    """
+    Tests disabling a job
+    """
+    schedule = setup_teardown_vars["schedule"]
+    schedule.opts.update({"schedule": {"name": {"enabled": "foo"}}, "pillar": {}})
+    Schedule.disable_job(schedule, "name")
+    assert not schedule.opts["schedule"]["name"]["enabled"]
+
+
+@pytest.mark.slow_test
+def test_disable_job_pillar(setup_teardown_vars):
+    """
+    Tests ignoring disable a job in pillar
+    """
+    schedule = setup_teardown_vars["schedule"]
+    schedule.opts.update(
+        {"pillar": {"schedule": {"name": {"enabled": True}}}, "schedule": {}}
+    )
+    Schedule.disable_job(schedule, "name", persist=False)
+    assert schedule.opts["pillar"]["schedule"]["name"]["enabled"]
+
+
+# modify_job tests
+@pytest.mark.slow_test
+def test_modify_job(setup_teardown_vars):
+    """
+    Tests modifying a job in the scheduler
+    """
+    schedule = setup_teardown_vars["schedule"]
+    schedule_dict = {"foo": "bar"}
+    schedule.opts.update({"schedule": {"name": "baz"}, "pillar": {}})
+    ret = copy.deepcopy(schedule.opts)
+    ret.update({"schedule": {"name": {"foo": "bar"}}})
+    Schedule.modify_job(schedule, "name", schedule_dict)
+    assert schedule.opts == ret
+
+
+def test_modify_job_not_exists(setup_teardown_vars):
+    """
+    Tests modifying a job in the scheduler if jobs not exists
+    """
+    schedule = setup_teardown_vars["schedule"]
+    schedule_dict = {"foo": "bar"}
+    schedule.opts.update({"schedule": {}, "pillar": {}})
+    ret = copy.deepcopy(schedule.opts)
+    ret.update({"schedule": {"name": {"foo": "bar"}}})
+    Schedule.modify_job(schedule, "name", schedule_dict)
+    assert schedule.opts == ret
+
+
+def test_modify_job_pillar(setup_teardown_vars):
+    """
+    Tests ignoring modification of job from pillar
+    """
+    schedule = setup_teardown_vars["schedule"]
+    schedule_dict = {"foo": "bar"}
+    schedule.opts.update({"schedule": {}, "pillar": {"schedule": {"name": "baz"}}})
+    ret = copy.deepcopy(schedule.opts)
+    Schedule.modify_job(schedule, "name", schedule_dict, persist=False)
+    assert schedule.opts == ret
+
+
+maxDiff = None
+
+
+# enable_schedule tests
+@pytest.mark.slow_test
+def test_enable_schedule(setup_teardown_vars):
+    """
+    Tests enabling the scheduler
+    """
+    schedule = setup_teardown_vars["schedule"]
+    with patch(
+        "salt.utils.schedule.Schedule.persist", MagicMock(return_value=None)
+    ) as persist_mock:
+        schedule.opts.update({"schedule": {"enabled": "foo"}, "pillar": {}})
+        Schedule.enable_schedule(schedule)
+        assert schedule.opts["schedule"]["enabled"]
+
+    persist_mock.assert_called()
+
+
+# disable_schedule tests
+@pytest.mark.slow_test
+def test_disable_schedule(setup_teardown_vars):
+    """
+    Tests disabling the scheduler
+    """
+    schedule = setup_teardown_vars["schedule"]
+    with patch(
+        "salt.utils.schedule.Schedule.persist", MagicMock(return_value=None)
+    ) as persist_mock:
+        schedule.opts.update({"schedule": {"enabled": "foo"}, "pillar": {}})
+        Schedule.disable_schedule(schedule)
+        assert not schedule.opts["schedule"]["enabled"]
+
+    persist_mock.assert_called()
+
+
+# reload tests
+def test_reload_update_schedule_key(setup_teardown_vars):
+    """
+    Tests reloading the schedule from saved schedule where both the
+    saved schedule and schedule.opts contain a schedule key
+    """
+    schedule = setup_teardown_vars["schedule"]
+    saved = {"schedule": {"foo": "bar"}}
+    ret = copy.deepcopy(schedule.opts)
+    ret.update({"schedule": {"foo": "bar", "hello": "world"}})
+    schedule.opts.update({"schedule": {"hello": "world"}})
+    Schedule.reload(schedule, saved)
+    assert schedule.opts == ret
+
+
+def test_reload_update_schedule_no_key(setup_teardown_vars):
+    """
+    Tests reloading the schedule from saved schedule that does not
+    contain a schedule key but schedule.opts does
+    """
+    schedule = setup_teardown_vars["schedule"]
+    saved = {"foo": "bar"}
+    ret = copy.deepcopy(schedule.opts)
+    ret.update({"schedule": {"foo": "bar", "hello": "world"}})
+    schedule.opts.update({"schedule": {"hello": "world"}})
+    Schedule.reload(schedule, saved)
+    assert schedule.opts == ret
+
+
+def test_reload_no_schedule_in_opts(setup_teardown_vars):
+    """
+    Tests reloading the schedule from saved schedule that does not
+    contain a schedule key and neither does schedule.opts
+    """
+    schedule = setup_teardown_vars["schedule"]
+    saved = {"foo": "bar"}
+    ret = copy.deepcopy(schedule.opts)
+    ret["schedule"] = {"foo": "bar"}
+    schedule.opts.pop("schedule", None)
+    Schedule.reload(schedule, saved)
+    assert schedule.opts == ret
+
+
+def test_reload_schedule_in_saved_but_not_opts(setup_teardown_vars):
+    """
+    Tests reloading the schedule from saved schedule that contains
+    a schedule key, but schedule.opts does not
+    """
+    schedule = setup_teardown_vars["schedule"]
+    saved = {"schedule": {"foo": "bar"}}
+    ret = copy.deepcopy(schedule.opts)
+    ret["schedule"] = {"foo": "bar"}
+    schedule.opts.pop("schedule", None)
+    Schedule.reload(schedule, saved)
+    assert schedule.opts == ret
+
+
+# eval tests
+def test_eval_schedule_is_not_dict(setup_teardown_vars):
+    """
+    Tests eval if the schedule is not a dictionary
+    """
+    schedule = setup_teardown_vars["schedule"]
+    schedule.opts.update({"schedule": "", "pillar": {"schedule": {}}})
+    pytest.raises(ValueError, Schedule.eval, schedule)
+
+
+def test_eval_schedule_is_not_dict_in_pillar(setup_teardown_vars):
+    """
+    Tests eval if the schedule from pillar is not a dictionary
+    """
+    schedule = setup_teardown_vars["schedule"]
+    schedule.opts.update({"schedule": {}, "pillar": {"schedule": ""}})
+    pytest.raises(ValueError, Schedule.eval, schedule)
+
+
+def test_eval_schedule_time(setup_teardown_vars):
+    """
+    Tests eval if the schedule setting time is in the future
+    """
+    schedule = setup_teardown_vars["schedule"]
+    schedule.opts.update({"pillar": {"schedule": {}}})
+    schedule.opts.update(
+        {"schedule": {"testjob": {"function": "test.true", "seconds": 60}}}
+    )
+    now = datetime.datetime.now()
+    schedule.eval()
+    assert schedule.opts["schedule"]["testjob"]["_next_fire_time"] > now
+
+
+def test_eval_schedule_time_eval(setup_teardown_vars):
+    """
+    Tests eval if the schedule setting time is in the future plus splay
+    """
+    schedule = setup_teardown_vars["schedule"]
+    schedule.opts.update({"pillar": {"schedule": {}}})
+    schedule.opts.update(
+        {"schedule": {"testjob": {"function": "test.true", "seconds": 60, "splay": 5}}}
+    )
+    now = datetime.datetime.now()
+    schedule.eval()
+    assert schedule.opts["schedule"]["testjob"]["_splay"] - now > datetime.timedelta(
+        seconds=60
+    )
+
+
+@pytest.mark.skipif(not _CRON_SUPPORTED, reason="croniter module not installed")
+def test_eval_schedule_cron(setup_teardown_vars):
+    """
+    Tests eval if the schedule is defined with cron expression
+    """
+    schedule = setup_teardown_vars["schedule"]
+    schedule.opts.update({"pillar": {"schedule": {}}})
+    schedule.opts.update(
+        {"schedule": {"testjob": {"function": "test.true", "cron": "* * * * *"}}}
+    )
+    now = datetime.datetime.now()
+    schedule.eval()
+    assert schedule.opts["schedule"]["testjob"]["_next_fire_time"] > now
+
+
+@pytest.mark.skipif(not _CRON_SUPPORTED, reason="croniter module not installed")
+def test_eval_schedule_cron_splay(setup_teardown_vars):
+    """
+    Tests eval if the schedule is defined with cron expression plus splay
+    """
+    schedule = setup_teardown_vars["schedule"]
+    schedule.opts.update({"pillar": {"schedule": {}}})
+    schedule.opts.update(
+        {
+            "schedule": {
+                "testjob": {"function": "test.true", "cron": "* * * * *", "splay": 5}
+            }
+        }
+    )
+    schedule.eval()
+    assert (
+        schedule.opts["schedule"]["testjob"]["_splay"]
+        > schedule.opts["schedule"]["testjob"]["_next_fire_time"]
+    )
+
+
+@pytest.mark.slow_test
+def test_handle_func_schedule_minion_blackout(setup_teardown_vars):
+    """
+    Tests eval if the schedule from pillar is not a dictionary
+    """
+    schedule = setup_teardown_vars["schedule"]
+    schedule.opts.update({"pillar": {"schedule": {}}})
+    schedule.opts.update({"grains": {"minion_blackout": True}})
+
+    schedule.opts.update(
+        {"schedule": {"testjob": {"function": "test.true", "seconds": 60}}}
+    )
+    data = {
+        "function": "test.true",
+        "_next_scheduled_fire_time": datetime.datetime(2018, 11, 21, 14, 9, 53, 903438),
+        "run": True,
+        "name": "testjob",
+        "seconds": 60,
+        "_splay": None,
+        "_seconds": 60,
+        "jid_include": True,
+        "maxrunning": 1,
+        "_next_fire_time": datetime.datetime(2018, 11, 21, 14, 8, 53, 903438),
+    }
+
+    with patch.object(salt.utils.schedule, "log") as log_mock:
+        with patch("salt.utils.process.daemonize"), patch("sys.platform", "linux2"):
+            schedule.handle_func(False, "test.ping", data)
+            assert log_mock.exception.called
+
+
+def test_handle_func_check_data(setup_teardown_vars):
+    """
+    Tests handle_func to ensure that __pub_fun_args is not
+    being duplicated in the value of kwargs in data.
+    """
+    schedule = setup_teardown_vars["schedule"]
+
+    data = {
+        "function": "test.arg",
+        "_next_scheduled_fire_time": datetime.datetime(2018, 11, 21, 14, 9, 53, 903438),
+        "run": True,
+        "args": ["arg1", "arg2"],
+        "kwargs": {"key1": "value1", "key2": "value2"},
+        "name": "testjob",
+        "seconds": 60,
+        "_splay": None,
+        "_seconds": 60,
+        "jid_include": True,
+        "maxrunning": 1,
+        "_next_fire_time": datetime.datetime(2018, 11, 21, 14, 8, 53, 903438),
+    }
+
+    with patch("salt.utils.process.daemonize"), patch("sys.platform", "linux2"):
+        with patch.object(schedule, "standalone", return_value=True):
+            # run handle_func once
+            schedule.handle_func(False, "test.arg", data)
+
+            # run handle_func and ensure __pub_fun_args
+            # is not in kwargs
+            schedule.handle_func(False, "test.arg", data)
+
+            assert "kwargs" in data
+            assert "__pub_fun_args" not in data["kwargs"]
+
+
+def test_handle_func_check_dicts(setup_teardown_vars):
+    """
+    Tests that utils, functions, and returners dicts are not
+    empty after handle_func has run on Windows.
+    """
+    schedule = setup_teardown_vars["schedule"]
+
+    data = {
+        "function": "test.arg",
+        "_next_scheduled_fire_time": datetime.datetime(2018, 11, 21, 14, 9, 53, 903438),
+        "run": True,
+        "args": ["arg1", "arg2"],
+        "kwargs": {"key1": "value1", "key2": "value2"},
+        "name": "testjob",
+        "seconds": 60,
+        "_splay": None,
+        "_seconds": 60,
+        "jid_include": True,
+        "maxrunning": 1,
+        "_next_fire_time": datetime.datetime(2018, 11, 21, 14, 8, 53, 903438),
+    }
+
+    with patch("salt.utils.process.daemonize"):
+        with patch.object(schedule, "standalone", return_value=True):
+            # simulate what happens before handle_func is called on Windows
+            schedule.functions = {}
+            schedule.returners = {}
+            schedule.utils = {}
+            schedule.handle_func(False, "test.arg", data)
+
+            assert {} != schedule.functions
+            assert {} != schedule.returners
+            assert {} != schedule.utils
diff --git a/tests/pytests/unit/utils/scheduler/test_skip.py b/tests/pytests/unit/utils/scheduler/test_skip.py
new file mode 100644
index 000000000000..e4f9d814cffc
--- /dev/null
+++ b/tests/pytests/unit/utils/scheduler/test_skip.py
@@ -0,0 +1,287 @@
+import logging
+
+import pytest
+
+try:
+    import dateutil.parser
+
+    HAS_DATEUTIL_PARSER = True
+except ImportError:
+    HAS_DATEUTIL_PARSER = False
+
+log = logging.getLogger(__name__)
+
+
+pytestmark = pytest.mark.skipif(
+    HAS_DATEUTIL_PARSER is False,
+    reason="The 'dateutil.parser' library is not available",
+)
+
+
+@pytest.mark.slow_test
+def test_skip(setup_teardown_vars):
+    """
+    verify that scheduled job is skipped at the specified time
+    """
+    schedule = setup_teardown_vars["schedule"]
+    job_name = "test_skip"
+    job = {
+        "schedule": {
+            job_name: {
+                "function": "test.ping",
+                "when": ["11/29/2017 4pm", "11/29/2017 5pm"],
+            }
+        }
+    }
+
+    # Add job to schedule
+    schedule.opts.update(job)
+
+    run_time = dateutil.parser.parse("11/29/2017 4:00pm")
+    schedule.skip_job(
+        job_name,
+        {
+            "time": run_time.strftime("%Y-%m-%dT%H:%M:%S"),
+            "time_fmt": "%Y-%m-%dT%H:%M:%S",
+        },
+    )
+
+    # Run 11/29/2017 at 4pm
+    schedule.eval(now=run_time)
+    ret = schedule.job_status(job_name)
+    assert "_last_run" not in ret
+    assert ret["_skip_reason"] == "skip_explicit"
+    assert ret["_skipped_time"] == run_time
+
+    # Run 11/29/2017 at 5pm
+    run_time = dateutil.parser.parse("11/29/2017 5:00pm")
+    schedule.eval(now=run_time)
+    ret = schedule.job_status(job_name)
+    assert ret["_last_run"] == run_time
+
+
+@pytest.mark.slow_test
+def test_skip_during_range(setup_teardown_vars):
+    """
+    verify that scheduled job is skipped during the specified range
+    """
+    schedule = setup_teardown_vars["schedule"]
+    job_name = "test_skip_during_range"
+    job = {
+        "schedule": {
+            job_name: {
+                "function": "test.ping",
+                "hours": "1",
+                "skip_during_range": {
+                    "start": "11/29/2017 2pm",
+                    "end": "11/29/2017 3pm",
+                },
+            }
+        }
+    }
+
+    # Add job to schedule
+    schedule.opts.update(job)
+
+    # eval at 1:30pm to prime.
+    run_time = dateutil.parser.parse("11/29/2017 1:30pm")
+    schedule.eval(now=run_time)
+    ret = schedule.job_status(job_name)
+
+    # eval at 2:30pm, will not run during range.
+    run_time = dateutil.parser.parse("11/29/2017 2:30pm")
+    schedule.eval(now=run_time)
+    ret = schedule.job_status(job_name)
+    assert "_last_run" not in ret
+    assert ret["_skip_reason"] == "in_skip_range"
+    assert ret["_skipped_time"] == run_time
+
+    # eval at 3:30pm, will run.
+    run_time = dateutil.parser.parse("11/29/2017 3:30pm")
+    schedule.eval(now=run_time)
+    ret = schedule.job_status(job_name)
+    assert ret["_last_run"] == run_time
+
+
+def test_skip_during_range_invalid_datestring(setup_teardown_vars):
+    """
+    verify that scheduled job is not not and returns the right error string
+    """
+    schedule = setup_teardown_vars["schedule"]
+    run_time = dateutil.parser.parse("11/29/2017 2:30pm")
+
+    job_name1 = "skip_during_range_invalid_datestring1"
+    job1 = {
+        "schedule": {
+            job_name1: {
+                "function": "test.ping",
+                "hours": "1",
+                "_next_fire_time": run_time,
+                "skip_during_range": {"start": "25pm", "end": "3pm"},
+            }
+        }
+    }
+
+    job_name2 = "skip_during_range_invalid_datestring2"
+    job2 = {
+        "schedule": {
+            job_name2: {
+                "function": "test.ping",
+                "hours": "1",
+                "_next_fire_time": run_time,
+                "skip_during_range": {"start": "2pm", "end": "25pm"},
+            }
+        }
+    }
+
+    # Add job1 to schedule
+    schedule.opts.update(job1)
+
+    # Eval
+    schedule.eval(now=run_time)
+
+    # Check the first job
+    ret = schedule.job_status(job_name1)
+    _expected = (
+        "Invalid date string for start in " "skip_during_range. Ignoring " "job {}."
+    ).format(job_name1)
+    assert ret["_error"] == _expected
+
+    # Clear out schedule
+    schedule.opts["schedule"] = {}
+
+    # Add job2 to schedule
+    schedule.opts.update(job2)
+
+    # Eval
+    schedule.eval(now=run_time)
+
+    # Check the second job
+    ret = schedule.job_status(job_name2)
+    _expected = (
+        "Invalid date string for end in " "skip_during_range. Ignoring " "job {}."
+    ).format(job_name2)
+    assert ret["_error"] == _expected
+
+
+@pytest.mark.slow_test
+def test_skip_during_range_global(setup_teardown_vars):
+    """
+    verify that scheduled job is skipped during the specified range
+    """
+    schedule = setup_teardown_vars["schedule"]
+    job_name = "skip_during_range_global"
+    job = {
+        "schedule": {
+            "skip_during_range": {
+                "start": "11/29/2017 2:00pm",
+                "end": "11/29/2017 3:00pm",
+            },
+            job_name: {"function": "test.ping", "hours": "1"},
+        }
+    }
+
+    # Add job to schedule
+    schedule.opts.update(job)
+
+    # eval at 1:30pm to prime.
+    run_time = dateutil.parser.parse("11/29/2017 1:30pm")
+    schedule.eval(now=run_time)
+    ret = schedule.job_status(job_name)
+
+    # eval at 2:30pm, will not run during range.
+    run_time = dateutil.parser.parse("11/29/2017 2:30pm")
+    schedule.eval(now=run_time)
+    ret = schedule.job_status(job_name)
+    assert "_last_run" not in ret
+    assert ret["_skip_reason"] == "in_skip_range"
+    assert ret["_skipped_time"] == run_time
+
+    # eval at 3:30pm, will run.
+    run_time = dateutil.parser.parse("11/29/2017 3:30pm")
+    schedule.eval(now=run_time)
+    ret = schedule.job_status(job_name)
+    assert ret["_last_run"] == run_time
+
+
+@pytest.mark.slow_test
+def test_run_after_skip_range(setup_teardown_vars):
+    """
+    verify that scheduled job is skipped during the specified range
+    """
+    schedule = setup_teardown_vars["schedule"]
+    job_name = "skip_run_after_skip_range"
+    job = {
+        "schedule": {
+            job_name: {
+                "function": "test.ping",
+                "when": "11/29/2017 2:30pm",
+                "run_after_skip_range": True,
+                "skip_during_range": {
+                    "start": "11/29/2017 2pm",
+                    "end": "11/29/2017 3pm",
+                },
+            }
+        }
+    }
+
+    # Add job to schedule
+    schedule.opts.update(job)
+
+    # eval at 2:30pm, will not run during range.
+    run_time = dateutil.parser.parse("11/29/2017 2:30pm")
+    schedule.eval(now=run_time)
+    ret = schedule.job_status(job_name)
+    assert "_last_run" not in ret
+    assert ret["_skip_reason"] == "in_skip_range"
+    assert ret["_skipped_time"] == run_time
+
+    # eval at 3:00:01pm, will run.
+    run_time = dateutil.parser.parse("11/29/2017 3:00:01pm")
+    schedule.eval(now=run_time)
+    ret = schedule.job_status(job_name)
+    assert ret["_last_run"] == run_time
+
+
+@pytest.mark.slow_test
+def test_run_seconds_skip(setup_teardown_vars):
+    """
+    verify that scheduled job is skipped during the specified range
+    """
+    schedule = setup_teardown_vars["schedule"]
+    job_name = "run_seconds_skip"
+    job = {"schedule": {job_name: {"function": "test.ping", "seconds": "10"}}}
+
+    # Add job to schedule
+    schedule.opts.update(job)
+
+    # eval at 2:00pm, to prime the scheduler
+    run_time = dateutil.parser.parse("11/29/2017 2:00pm")
+    schedule.eval(now=run_time)
+    ret = schedule.job_status(job_name)
+
+    # eval at 2:00:10pm
+    run_time = dateutil.parser.parse("11/29/2017 2:00:10pm")
+    schedule.eval(now=run_time)
+    ret = schedule.job_status(job_name)
+
+    # Skip at 2:00:20pm
+    run_time = dateutil.parser.parse("11/29/2017 2:00:20pm")
+    schedule.skip_job(
+        job_name,
+        {
+            "time": run_time.strftime("%Y-%m-%dT%H:%M:%S"),
+            "time_fmt": "%Y-%m-%dT%H:%M:%S",
+        },
+    )
+    schedule.eval(now=run_time)
+    ret = schedule.job_status(job_name)
+    assert "_next_fire_time" in ret
+    assert ret["_skip_reason"] == "skip_explicit"
+    assert ret["_skipped_time"] == run_time
+
+    # Run at 2:00:30pm
+    run_time = dateutil.parser.parse("11/29/2017 2:00:30pm")
+    schedule.eval(now=run_time)
+    ret = schedule.job_status(job_name)
+    assert "_last_run" in ret
diff --git a/tests/unit/modules/test_schedule.py b/tests/unit/modules/test_schedule.py
deleted file mode 100644
index c2b3ab6057e9..000000000000
--- a/tests/unit/modules/test_schedule.py
+++ /dev/null
@@ -1,588 +0,0 @@
-"""
-    :codeauthor: Jayesh Kariya <jayeshk@saltstack.com>
-"""
-
-import logging
-import os
-
-import pytest
-import salt.modules.schedule as schedule
-from salt.utils.event import SaltEvent
-from tests.support.mixins import LoaderModuleMockMixin
-from tests.support.mock import MagicMock, patch
-from tests.support.runtests import RUNTIME_VARS
-from tests.support.unit import TestCase
-
-log = logging.getLogger(__name__)
-
-JOB1 = {
-    "function": "test.ping",
-    "maxrunning": 1,
-    "name": "job1",
-    "jid_include": True,
-    "enabled": True,
-}
-
-
-class ScheduleTestCase(TestCase, LoaderModuleMockMixin):
-    """
-    Test cases for salt.modules.schedule
-    """
-
-    @classmethod
-    def setUpClass(cls):
-        cls.sock_dir = os.path.join(RUNTIME_VARS.TMP, "test-socks")
-
-    def setup_loader_modules(self):
-        return {schedule: {}}
-
-    # 'purge' function tests: 1
-
-    @pytest.mark.slow_test
-    def test_purge(self):
-        """
-        Test if it purge all the jobs currently scheduled on the minion.
-        """
-        with patch.dict(schedule.__opts__, {"schedule": {}, "sock_dir": self.sock_dir}):
-            mock = MagicMock(return_value=True)
-            with patch.dict(schedule.__salt__, {"event.fire": mock}):
-                _ret_value = {"complete": True, "schedule": {}}
-                with patch.object(SaltEvent, "get_event", return_value=_ret_value):
-                    self.assertDictEqual(
-                        schedule.purge(),
-                        {
-                            "comment": ["Deleted job: schedule from schedule."],
-                            "result": True,
-                        },
-                    )
-
-    # 'delete' function tests: 1
-
-    @pytest.mark.slow_test
-    def test_delete(self):
-        """
-        Test if it delete a job from the minion's schedule.
-        """
-        with patch.dict(schedule.__opts__, {"schedule": {}, "sock_dir": self.sock_dir}):
-            mock = MagicMock(return_value=True)
-            with patch.dict(schedule.__salt__, {"event.fire": mock}):
-                _ret_value = {"complete": True, "schedule": {}}
-                with patch.object(SaltEvent, "get_event", return_value=_ret_value):
-                    self.assertDictEqual(
-                        schedule.delete("job1"),
-                        {"comment": "Job job1 does not exist.", "result": False},
-                    )
-
-    # 'build_schedule_item' function tests: 1
-
-    def test_build_schedule_item(self):
-        """
-        Test if it build a schedule job.
-        """
-        comment = (
-            'Unable to use "seconds", "minutes", "hours", '
-            'or "days" with "when" or "cron" options.'
-        )
-        comment1 = 'Unable to use "when" and "cron" ' "options together.  Ignoring."
-        with patch.dict(schedule.__opts__, {"job1": {}}):
-            self.assertDictEqual(
-                schedule.build_schedule_item(""),
-                {"comment": "Job name is required.", "result": False},
-            )
-
-            self.assertDictEqual(
-                schedule.build_schedule_item("job1", function="test.ping"),
-                {
-                    "function": "test.ping",
-                    "maxrunning": 1,
-                    "name": "job1",
-                    "jid_include": True,
-                    "enabled": True,
-                },
-            )
-
-            self.assertDictEqual(
-                schedule.build_schedule_item(
-                    "job1", function="test.ping", seconds=3600, when="2400"
-                ),
-                {"comment": comment, "result": False},
-            )
-
-            self.assertDictEqual(
-                schedule.build_schedule_item(
-                    "job1", function="test.ping", when="2400", cron="2"
-                ),
-                {"comment": comment1, "result": False},
-            )
-
-    # 'build_schedule_item_invalid_when' function tests: 1
-
-    def test_build_schedule_item_invalid_when(self):
-        """
-        Test if it build a schedule job.
-        """
-        comment = 'Schedule item garbage for "when" in invalid.'
-        with patch.dict(schedule.__opts__, {"job1": {}}):
-            self.assertDictEqual(
-                schedule.build_schedule_item(
-                    "job1", function="test.ping", when="garbage"
-                ),
-                {"comment": comment, "result": False},
-            )
-
-    # 'add' function tests: 1
-
-    @pytest.mark.slow_test
-    def test_add(self):
-        """
-        Test if it add a job to the schedule.
-        """
-        comm1 = "Job job1 already exists in schedule."
-        comm2 = (
-            'Error: Unable to use "seconds", "minutes", "hours", '
-            'or "days" with "when" or "cron" options.'
-        )
-        comm3 = 'Unable to use "when" and "cron" options together.  Ignoring.'
-        comm4 = "Job: job2 would be added to schedule."
-        with patch.dict(
-            schedule.__opts__, {"schedule": {"job1": "salt"}, "sock_dir": self.sock_dir}
-        ):
-            mock = MagicMock(return_value=True)
-            with patch.dict(schedule.__salt__, {"event.fire": mock}):
-                _ret_value = {"complete": True, "schedule": {"job1": {"salt": "salt"}}}
-                with patch.object(SaltEvent, "get_event", return_value=_ret_value):
-                    self.assertDictEqual(
-                        schedule.add("job1"), {"comment": comm1, "result": False}
-                    )
-
-                _ret_value = {"complete": True, "schedule": {}}
-                with patch.object(SaltEvent, "get_event", return_value=_ret_value):
-                    self.assertDictEqual(
-                        schedule.add(
-                            "job2", function="test.ping", seconds=3600, when="2400"
-                        ),
-                        {"comment": comm2, "result": False},
-                    )
-
-                _ret_value = {"complete": True, "schedule": {}}
-                with patch.object(SaltEvent, "get_event", return_value=_ret_value):
-                    self.assertDictEqual(
-                        schedule.add(
-                            "job2", function="test.ping", when="2400", cron="2"
-                        ),
-                        {"comment": comm3, "result": False},
-                    )
-                _ret_value = {"complete": True, "schedule": {}}
-                with patch.object(SaltEvent, "get_event", return_value=_ret_value):
-                    self.assertDictEqual(
-                        schedule.add("job2", function="test.ping", test=True),
-                        {"comment": comm4, "result": True},
-                    )
-
-    # 'run_job' function tests: 1
-
-    @pytest.mark.slow_test
-    def test_run_job(self):
-        """
-        Test if it run a scheduled job on the minion immediately.
-        """
-        with patch.dict(
-            schedule.__opts__, {"schedule": {"job1": JOB1}, "sock_dir": self.sock_dir}
-        ):
-            mock = MagicMock(return_value=True)
-            with patch.dict(schedule.__salt__, {"event.fire": mock}):
-                _ret_value = {"complete": True, "schedule": {"job1": JOB1}}
-                with patch.object(SaltEvent, "get_event", return_value=_ret_value):
-                    self.assertDictEqual(
-                        schedule.run_job("job1"),
-                        {"comment": "Scheduling Job job1 on minion.", "result": True},
-                    )
-
-    # 'enable_job' function tests: 1
-
-    @pytest.mark.slow_test
-    def test_enable_job(self):
-        """
-        Test if it enable a job in the minion's schedule.
-        """
-        with patch.dict(schedule.__opts__, {"schedule": {}, "sock_dir": self.sock_dir}):
-            mock = MagicMock(return_value=True)
-            with patch.dict(schedule.__salt__, {"event.fire": mock}):
-                _ret_value = {"complete": True, "schedule": {}}
-                with patch.object(SaltEvent, "get_event", return_value=_ret_value):
-                    self.assertDictEqual(
-                        schedule.enable_job("job1"),
-                        {"comment": "Job job1 does not exist.", "result": False},
-                    )
-
-    # 'disable_job' function tests: 1
-
-    @pytest.mark.slow_test
-    def test_disable_job(self):
-        """
-        Test if it disable a job in the minion's schedule.
-        """
-        with patch.dict(schedule.__opts__, {"schedule": {}, "sock_dir": self.sock_dir}):
-            mock = MagicMock(return_value=True)
-            with patch.dict(schedule.__salt__, {"event.fire": mock}):
-                _ret_value = {"complete": True, "schedule": {}}
-                with patch.object(SaltEvent, "get_event", return_value=_ret_value):
-                    self.assertDictEqual(
-                        schedule.disable_job("job1"),
-                        {"comment": "Job job1 does not exist.", "result": False},
-                    )
-
-    # 'save' function tests: 1
-
-    @pytest.mark.slow_test
-    def test_save(self):
-        """
-        Test if it save all scheduled jobs on the minion.
-        """
-        comm1 = "Schedule (non-pillar items) saved."
-        with patch.dict(
-            schedule.__opts__,
-            {"schedule": {}, "default_include": "/tmp", "sock_dir": self.sock_dir},
-        ):
-
-            mock = MagicMock(return_value=True)
-            with patch.dict(schedule.__salt__, {"event.fire": mock}):
-                _ret_value = {"complete": True, "schedule": {}}
-                with patch.object(SaltEvent, "get_event", return_value=_ret_value):
-                    self.assertDictEqual(
-                        schedule.save(), {"comment": comm1, "result": True}
-                    )
-
-    # 'enable' function tests: 1
-
-    def test_enable(self):
-        """
-        Test if it enable all scheduled jobs on the minion.
-        """
-        self.assertDictEqual(
-            schedule.enable(test=True),
-            {"comment": "Schedule would be enabled.", "result": True},
-        )
-
-    # 'disable' function tests: 1
-
-    def test_disable(self):
-        """
-        Test if it disable all scheduled jobs on the minion.
-        """
-        self.assertDictEqual(
-            schedule.disable(test=True),
-            {"comment": "Schedule would be disabled.", "result": True},
-        )
-
-    # 'move' function tests: 1
-
-    @pytest.mark.slow_test
-    def test_move(self):
-        """
-        Test if it move scheduled job to another minion or minions.
-        """
-        comm1 = "no servers answered the published schedule.add command"
-        comm2 = "the following minions return False"
-        comm3 = "Moved Job job1 from schedule."
-        with patch.dict(
-            schedule.__opts__, {"schedule": {"job1": JOB1}, "sock_dir": self.sock_dir}
-        ):
-            mock = MagicMock(return_value=True)
-            with patch.dict(schedule.__salt__, {"event.fire": mock}):
-                _ret_value = {"complete": True, "schedule": {"job1": JOB1}}
-                with patch.object(SaltEvent, "get_event", return_value=_ret_value):
-                    mock = MagicMock(return_value={})
-                    with patch.dict(schedule.__salt__, {"publish.publish": mock}):
-                        self.assertDictEqual(
-                            schedule.move("job1", "minion1"),
-                            {"comment": comm1, "result": True},
-                        )
-
-                    mock = MagicMock(return_value={"minion1": ""})
-                    with patch.dict(schedule.__salt__, {"publish.publish": mock}):
-                        self.assertDictEqual(
-                            schedule.move("job1", "minion1"),
-                            {"comment": comm2, "minions": ["minion1"], "result": True},
-                        )
-
-                    mock = MagicMock(return_value={"minion1": "job1"})
-                    with patch.dict(schedule.__salt__, {"publish.publish": mock}):
-                        mock = MagicMock(return_value=True)
-                        with patch.dict(schedule.__salt__, {"event.fire": mock}):
-                            self.assertDictEqual(
-                                schedule.move("job1", "minion1"),
-                                {
-                                    "comment": comm3,
-                                    "minions": ["minion1"],
-                                    "result": True,
-                                },
-                            )
-
-                    self.assertDictEqual(
-                        schedule.move("job3", "minion1"),
-                        {"comment": "Job job3 does not exist.", "result": False},
-                    )
-
-        mock = MagicMock(side_effect=[{}, {"job1": {}}])
-        with patch.dict(
-            schedule.__opts__, {"schedule": mock, "sock_dir": self.sock_dir}
-        ):
-            mock = MagicMock(return_value=True)
-            with patch.dict(schedule.__salt__, {"event.fire": mock}):
-                _ret_value = {"complete": True, "schedule": {"job1": JOB1}}
-                with patch.object(SaltEvent, "get_event", return_value=_ret_value):
-                    with patch.dict(schedule.__pillar__, {"schedule": {"job1": JOB1}}):
-                        mock = MagicMock(return_value={})
-                        with patch.dict(schedule.__salt__, {"publish.publish": mock}):
-                            self.assertDictEqual(
-                                schedule.move("job1", "minion1"),
-                                {"comment": comm1, "result": True},
-                            )
-
-                        mock = MagicMock(return_value={"minion1": ""})
-                        with patch.dict(schedule.__salt__, {"publish.publish": mock}):
-                            self.assertDictEqual(
-                                schedule.move("job1", "minion1"),
-                                {
-                                    "comment": comm2,
-                                    "minions": ["minion1"],
-                                    "result": True,
-                                },
-                            )
-
-                        mock = MagicMock(return_value={"minion1": "job1"})
-                        with patch.dict(schedule.__salt__, {"publish.publish": mock}):
-                            mock = MagicMock(return_value=True)
-                            with patch.dict(schedule.__salt__, {"event.fire": mock}):
-                                self.assertDictEqual(
-                                    schedule.move("job1", "minion1"),
-                                    {
-                                        "comment": comm3,
-                                        "minions": ["minion1"],
-                                        "result": True,
-                                    },
-                                )
-
-    # 'copy' function tests: 1
-
-    @pytest.mark.slow_test
-    def test_copy(self):
-        """
-        Test if it copy scheduled job to another minion or minions.
-        """
-        comm1 = "no servers answered the published schedule.add command"
-        comm2 = "the following minions return False"
-        comm3 = "Copied Job job1 from schedule to minion(s)."
-        with patch.dict(
-            schedule.__opts__, {"schedule": {"job1": JOB1}, "sock_dir": self.sock_dir}
-        ):
-            mock = MagicMock(return_value=True)
-            with patch.dict(schedule.__salt__, {"event.fire": mock}):
-                _ret_value = {"complete": True, "schedule": {"job1": {"job1": JOB1}}}
-                with patch.object(SaltEvent, "get_event", return_value=_ret_value):
-                    mock = MagicMock(return_value={})
-                    with patch.dict(schedule.__salt__, {"publish.publish": mock}):
-                        self.assertDictEqual(
-                            schedule.copy("job1", "minion1"),
-                            {"comment": comm1, "result": True},
-                        )
-
-                    mock = MagicMock(return_value={"minion1": ""})
-                    with patch.dict(schedule.__salt__, {"publish.publish": mock}):
-                        self.assertDictEqual(
-                            schedule.copy("job1", "minion1"),
-                            {"comment": comm2, "minions": ["minion1"], "result": True},
-                        )
-
-                    mock = MagicMock(return_value={"minion1": "job1"})
-                    with patch.dict(schedule.__salt__, {"publish.publish": mock}):
-                        mock = MagicMock(return_value=True)
-                        with patch.dict(schedule.__salt__, {"event.fire": mock}):
-                            self.assertDictEqual(
-                                schedule.copy("job1", "minion1"),
-                                {
-                                    "comment": comm3,
-                                    "minions": ["minion1"],
-                                    "result": True,
-                                },
-                            )
-
-                    self.assertDictEqual(
-                        schedule.copy("job3", "minion1"),
-                        {"comment": "Job job3 does not exist.", "result": False},
-                    )
-
-        mock = MagicMock(side_effect=[{}, {"job1": {}}])
-        with patch.dict(
-            schedule.__opts__, {"schedule": mock, "sock_dir": self.sock_dir}
-        ):
-            with patch.dict(schedule.__pillar__, {"schedule": {"job1": JOB1}}):
-                mock = MagicMock(return_value=True)
-                with patch.dict(schedule.__salt__, {"event.fire": mock}):
-                    _ret_value = {
-                        "complete": True,
-                        "schedule": {"job1": {"job1": JOB1}},
-                    }
-                    with patch.object(SaltEvent, "get_event", return_value=_ret_value):
-
-                        mock = MagicMock(return_value={})
-                        with patch.dict(schedule.__salt__, {"publish.publish": mock}):
-                            self.assertDictEqual(
-                                schedule.copy("job1", "minion1"),
-                                {"comment": comm1, "result": True},
-                            )
-
-                        mock = MagicMock(return_value={"minion1": ""})
-                        with patch.dict(schedule.__salt__, {"publish.publish": mock}):
-                            self.assertDictEqual(
-                                schedule.copy("job1", "minion1"),
-                                {
-                                    "comment": comm2,
-                                    "minions": ["minion1"],
-                                    "result": True,
-                                },
-                            )
-
-                        mock = MagicMock(return_value={"minion1": "job1"})
-                        with patch.dict(schedule.__salt__, {"publish.publish": mock}):
-                            mock = MagicMock(return_value=True)
-                            with patch.dict(schedule.__salt__, {"event.fire": mock}):
-                                self.assertDictEqual(
-                                    schedule.copy("job1", "minion1"),
-                                    {
-                                        "comment": comm3,
-                                        "minions": ["minion1"],
-                                        "result": True,
-                                    },
-                                )
-
-    # 'modify' function tests: 1
-
-    @pytest.mark.slow_test
-    def test_modify(self):
-        """
-        Test if modifying job to the schedule.
-        """
-        job1 = {"function": "salt", "seconds": 3600}
-
-        comm1 = "Modified job: job1 in schedule."
-        diff1 = (
-            "--- \n+++ \n@@ -1,3 +1,6 @@\n "
-            "enabled:True\n function:salt\n"
-            "-seconds:3600\n+jid_include:True\n"
-            "+maxrunning:1\n+name:job1\n"
-            "+seconds:60\n"
-        )
-
-        diff4 = (
-            "--- \n+++ \n@@ -1,3 +1,5 @@\n "
-            "enabled:True\n-function:salt\n"
-            "-seconds:3600\n+function:test.version\n"
-            "+jid_include:True\n+maxrunning:1\n"
-            "+name:job1\n"
-        )
-
-        expected1 = {"comment": comm1, "changes": {"diff": diff1}, "result": True}
-
-        comm2 = (
-            'Error: Unable to use "seconds", "minutes", "hours", '
-            'or "days" with "when" option.'
-        )
-        expected2 = {"comment": comm2, "changes": {}, "result": False}
-
-        comm3 = 'Unable to use "when" and "cron" options together.  Ignoring.'
-        expected3 = {"comment": comm3, "changes": {}, "result": False}
-
-        comm4 = "Job: job1 would be modified in schedule."
-        expected4 = {"comment": comm4, "changes": {"diff": diff4}, "result": True}
-
-        comm5 = "Job job2 does not exist in schedule."
-        expected5 = {"comment": comm5, "changes": {}, "result": False}
-
-        with patch.dict(
-            schedule.__opts__, {"schedule": {"job1": job1}, "sock_dir": self.sock_dir}
-        ):
-            mock = MagicMock(return_value=True)
-            with patch.dict(schedule.__salt__, {"event.fire": mock}):
-                _ret_value = {"complete": True, "schedule": {"job1": job1}}
-                with patch.object(SaltEvent, "get_event", return_value=_ret_value):
-                    ret = schedule.modify("job1", seconds="60")
-                    self.assertDictEqual(ret, expected1)
-
-                _ret_value = {"complete": True, "schedule": {"job1": job1}}
-                with patch.object(SaltEvent, "get_event", return_value=_ret_value):
-                    ret = schedule.modify(
-                        "job1", function="test.ping", seconds=3600, when="2400"
-                    )
-                    self.assertDictEqual(ret, expected2)
-
-                _ret_value = {"complete": True, "schedule": {"job1": job1}}
-                with patch.object(SaltEvent, "get_event", return_value=_ret_value):
-                    ret = schedule.modify(
-                        "job1", function="test.ping", when="2400", cron="2"
-                    )
-                    self.assertDictEqual(ret, expected3)
-
-                _ret_value = {"complete": True, "schedule": {"job1": job1}}
-                with patch.object(SaltEvent, "get_event", return_value=_ret_value):
-                    ret = schedule.modify("job1", function="test.version", test=True)
-                    self.assertDictEqual(ret, expected4)
-
-                _ret_value = {"complete": True, "schedule": {}}
-                with patch.object(SaltEvent, "get_event", return_value=_ret_value):
-                    ret = schedule.modify("job2", function="test.version", test=True)
-                    self.assertDictEqual(ret, expected5)
-
-    # 'is_enabled' function tests: 1
-
-    def test_is_enabled(self):
-        """
-        Test is_enabled
-        """
-        job1 = {"function": "salt", "seconds": 3600}
-
-        comm1 = "Modified job: job1 in schedule."
-
-        mock_schedule = {"enabled": True, "job1": job1}
-
-        mock_lst = MagicMock(return_value=mock_schedule)
-
-        with patch.dict(
-            schedule.__opts__, {"schedule": {"job1": job1}, "sock_dir": self.sock_dir}
-        ):
-            mock = MagicMock(return_value=True)
-            with patch.dict(
-                schedule.__salt__, {"event.fire": mock, "schedule.list": mock_lst}
-            ):
-                _ret_value = {"complete": True, "schedule": {"job1": job1}}
-                with patch.object(SaltEvent, "get_event", return_value=_ret_value):
-                    ret = schedule.is_enabled("job1")
-                    self.assertDictEqual(ret, job1)
-
-                    ret = schedule.is_enabled()
-                    self.assertEqual(ret, True)
-
-    # 'job_status' function tests: 1
-
-    def test_job_status(self):
-        """
-        Test is_enabled
-        """
-        job1 = {"function": "salt", "seconds": 3600}
-
-        comm1 = "Modified job: job1 in schedule."
-
-        mock_schedule = {"enabled": True, "job1": job1}
-
-        mock_lst = MagicMock(return_value=mock_schedule)
-
-        with patch.dict(
-            schedule.__opts__, {"schedule": {"job1": job1}, "sock_dir": self.sock_dir}
-        ):
-            mock = MagicMock(return_value=True)
-            with patch.dict(schedule.__salt__, {"event.fire": mock}):
-                _ret_value = {"complete": True, "data": job1}
-                with patch.object(SaltEvent, "get_event", return_value=_ret_value):
-                    ret = schedule.job_status("job1")
-                    self.assertDictEqual(ret, job1)
diff --git a/tests/unit/test_module_names.py b/tests/unit/test_module_names.py
index 52ed9fb2751a..78843490709c 100644
--- a/tests/unit/test_module_names.py
+++ b/tests/unit/test_module_names.py
@@ -192,14 +192,6 @@ def test_module_name_source_match(self):
             "unit.test_simple",
             "unit.test_virtualname",
             "unit.test_zypp_plugins",
-            "unit.utils.scheduler.test_error",
-            "unit.utils.scheduler.test_eval",
-            "unit.utils.scheduler.test_helpers",
-            "unit.utils.scheduler.test_maxrunning",
-            "unit.utils.scheduler.test_postpone",
-            "unit.utils.scheduler.test_run_job",
-            "unit.utils.scheduler.test_schedule",
-            "unit.utils.scheduler.test_skip",
             "unit.auth.test_auth",
         )
         errors = []
diff --git a/tests/unit/utils/scheduler/__init__.py b/tests/unit/utils/scheduler/__init__.py
deleted file mode 100644
index 40a96afc6ff0..000000000000
--- a/tests/unit/utils/scheduler/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-# -*- coding: utf-8 -*-
diff --git a/tests/unit/utils/scheduler/base.py b/tests/unit/utils/scheduler/base.py
deleted file mode 100644
index 17e19646fd68..000000000000
--- a/tests/unit/utils/scheduler/base.py
+++ /dev/null
@@ -1,76 +0,0 @@
-"""
-    tests.unit.utils.scheduler.base
-    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-"""
-
-
-import copy
-import logging
-import os
-
-import salt.utils.platform
-import salt.utils.schedule
-from salt.modules.test import ping
-from salt.utils.process import SubprocessList
-from saltfactories.utils.processes import terminate_process
-from tests.support.mixins import SaltReturnAssertsMixin
-from tests.support.mock import MagicMock, patch
-from tests.support.runtests import RUNTIME_VARS
-from tests.support.unit import TestCase
-
-log = logging.getLogger(__name__)
-
-
-class SchedulerTestsBase(TestCase, SaltReturnAssertsMixin):
-    """
-    Validate the pkg module
-    """
-
-    @classmethod
-    def setUpClass(cls):
-        root_dir = os.path.join(RUNTIME_VARS.TMP, "schedule-unit-tests")
-        sock_dir = os.path.join(root_dir, "test-socks")
-
-        default_config = salt.config.minion_config(None)
-        default_config["conf_dir"] = root_dir
-        default_config["root_dir"] = root_dir
-        default_config["sock_dir"] = sock_dir
-        default_config["pki_dir"] = os.path.join(root_dir, "pki")
-        default_config["cachedir"] = os.path.join(root_dir, "cache")
-        cls.default_config = default_config
-        cls.subprocess_list = SubprocessList()
-
-    @classmethod
-    def tearDownClass(cls):
-        del cls.default_config
-        del cls.subprocess_list
-
-    def setUp(self):
-        with patch("salt.utils.schedule.clean_proc_dir", MagicMock(return_value=None)):
-            functions = {"test.ping": ping}
-            self.schedule = salt.utils.schedule.Schedule(
-                copy.deepcopy(self.default_config),
-                functions,
-                returners={},
-                new_instance=True,
-            )
-        self.schedule._subprocess_list = self.subprocess_list
-
-    def tearDown(self):
-        subprocess_list = self.subprocess_list
-        processes = subprocess_list.processes
-        self.schedule.reset()
-        del self.schedule
-        for proc in processes:
-            if proc.is_alive():
-                terminate_process(proc.pid, kill_children=True, slow_stop=True)
-        subprocess_list.cleanup()
-        processes = subprocess_list.processes
-        if processes:
-            for proc in processes:
-                if proc.is_alive():
-                    terminate_process(proc.pid, kill_children=True, slow_stop=False)
-            subprocess_list.cleanup()
-        processes = subprocess_list.processes
-        if processes:
-            log.warning("Processes left running: %s", processes)
diff --git a/tests/unit/utils/scheduler/test_error.py b/tests/unit/utils/scheduler/test_error.py
deleted file mode 100644
index 6428164daaa6..000000000000
--- a/tests/unit/utils/scheduler/test_error.py
+++ /dev/null
@@ -1,223 +0,0 @@
-# -*- coding: utf-8 -*-
-
-from __future__ import absolute_import
-
-import logging
-
-from tests.support.mock import MagicMock, patch
-from tests.support.unit import skipIf
-from tests.unit.utils.scheduler.base import SchedulerTestsBase
-
-try:
-    import dateutil.parser
-
-    HAS_DATEUTIL_PARSER = True
-except ImportError:
-    HAS_DATEUTIL_PARSER = False
-
-try:
-    import croniter  # pylint: disable=unused-import
-
-    HAS_CRONITER = True
-except ImportError:
-    HAS_CRONITER = False
-
-log = logging.getLogger(__name__)
-
-
-@skipIf(
-    HAS_DATEUTIL_PARSER is False, "The 'dateutil.parser' library is not available",
-)
-class SchedulerErrorTest(SchedulerTestsBase):
-    def setUp(self):
-        super(SchedulerErrorTest, self).setUp()
-        self.schedule.opts["loop_interval"] = 1
-
-        self.schedule.opts["grains"]["whens"] = {"tea time": "11/29/2017 12:00pm"}
-
-    @skipIf(not HAS_CRONITER, "Cannot find croniter python module")
-    def test_eval_cron_invalid(self):
-        """
-        verify that scheduled job runs
-        """
-        job = {"schedule": {"job1": {"function": "test.ping", "cron": "0 16 29 13 *"}}}
-
-        # Add the job to the scheduler
-        self.schedule.opts.update(job)
-
-        run_time = dateutil.parser.parse("11/29/2017 4:00pm")
-        with patch("croniter.croniter.get_next", MagicMock(return_value=run_time)):
-            self.schedule.eval(now=run_time)
-
-        ret = self.schedule.job_status("job1")
-        self.assertEqual(ret["_error"], "Invalid cron string. Ignoring job job1.")
-
-    def test_eval_when_invalid_date(self):
-        """
-        verify that scheduled job does not run
-        and returns the right error
-        """
-        run_time = dateutil.parser.parse("11/29/2017 4:00pm")
-
-        job = {
-            "schedule": {"job1": {"function": "test.ping", "when": "13/29/2017 1:00pm"}}
-        }
-
-        # Add the job to the scheduler
-        self.schedule.opts.update(job)
-
-        # Evaluate 1 second before the run time
-        self.schedule.eval(now=run_time)
-        ret = self.schedule.job_status("job1")
-        self.assertEqual(
-            ret["_error"], "Invalid date string 13/29/2017 1:00pm. Ignoring job job1."
-        )
-
-    def test_eval_whens_grain_not_dict(self):
-        """
-        verify that scheduled job does not run
-        and returns the right error
-        """
-        run_time = dateutil.parser.parse("11/29/2017 4:00pm")
-
-        job = {"schedule": {"job1": {"function": "test.ping", "when": "tea time"}}}
-
-        self.schedule.opts["grains"]["whens"] = ["tea time"]
-
-        # Add the job to the scheduler
-        self.schedule.opts.update(job)
-
-        # Evaluate 1 second before the run time
-        self.schedule.eval(now=run_time)
-        ret = self.schedule.job_status("job1")
-        self.assertEqual(
-            ret["_error"], 'Grain "whens" must be a dict. Ignoring job job1.'
-        )
-
-    def test_eval_once_invalid_datestring(self):
-        """
-        verify that scheduled job does not run
-        and returns the right error
-        """
-        job = {
-            "schedule": {
-                "job1": {"function": "test.ping", "once": "2017-13-13T13:00:00"}
-            }
-        }
-        run_time = dateutil.parser.parse("12/13/2017 1:00pm")
-
-        # Add the job to the scheduler
-        self.schedule.opts.update(job)
-
-        # Evaluate 1 second at the run time
-        self.schedule.eval(now=run_time)
-        ret = self.schedule.job_status("job1")
-        _expected = (
-            "Date string could not be parsed: "
-            "2017-13-13T13:00:00, %Y-%m-%dT%H:%M:%S. "
-            "Ignoring job job1."
-        )
-        self.assertEqual(ret["_error"], _expected)
-
-    def test_eval_skip_during_range_invalid_date(self):
-        """
-        verify that scheduled job does not run
-        and returns the right error
-        """
-
-        job = {
-            "schedule": {
-                "job1": {
-                    "function": "test.ping",
-                    "hours": 1,
-                    "skip_during_range": {"start": "1:00pm", "end": "25:00pm"},
-                }
-            }
-        }
-
-        # Add the job to the scheduler
-        self.schedule.opts.update(job)
-
-        # eval at 3:00pm to prime, simulate minion start up.
-        run_time = dateutil.parser.parse("11/29/2017 3:00pm")
-        self.schedule.eval(now=run_time)
-        ret = self.schedule.job_status("job1")
-
-        # eval at 4:00pm to prime
-        run_time = dateutil.parser.parse("11/29/2017 4:00pm")
-        self.schedule.eval(now=run_time)
-        ret = self.schedule.job_status("job1")
-        _expected = (
-            "Invalid date string for end in " "skip_during_range. Ignoring " "job job1."
-        )
-        self.assertEqual(ret["_error"], _expected)
-
-    def test_eval_skip_during_range_end_before_start(self):
-        """
-        verify that scheduled job does not run
-        and returns the right error
-        """
-
-        job = {
-            "schedule": {
-                "job1": {
-                    "function": "test.ping",
-                    "hours": 1,
-                    "skip_during_range": {"start": "1:00pm", "end": "12:00pm"},
-                }
-            }
-        }
-
-        # Add the job to the scheduler
-        self.schedule.opts.update(job)
-
-        # eval at 3:00pm to prime, simulate minion start up.
-        run_time = dateutil.parser.parse("11/29/2017 3:00pm")
-        self.schedule.eval(now=run_time)
-        ret = self.schedule.job_status("job1")
-
-        # eval at 4:00pm to prime
-        run_time = dateutil.parser.parse("11/29/2017 4:00pm")
-        self.schedule.eval(now=run_time)
-        ret = self.schedule.job_status("job1")
-        _expected = (
-            "schedule.handle_func: Invalid "
-            "range, end must be larger than "
-            "start. Ignoring job job1."
-        )
-        self.assertEqual(ret["_error"], _expected)
-
-    def test_eval_skip_during_range_not_dict(self):
-        """
-        verify that scheduled job does not run
-        and returns the right error
-        """
-
-        job = {
-            "schedule": {
-                "job1": {
-                    "function": "test.ping",
-                    "hours": 1,
-                    "skip_during_range": ["start", "1:00pm", "end", "12:00pm"],
-                }
-            }
-        }
-
-        # Add the job to the scheduler
-        self.schedule.opts.update(job)
-
-        # eval at 3:00pm to prime, simulate minion start up.
-        run_time = dateutil.parser.parse("11/29/2017 3:00pm")
-        self.schedule.eval(now=run_time)
-        ret = self.schedule.job_status("job1")
-
-        # eval at 4:00pm to prime
-        run_time = dateutil.parser.parse("11/29/2017 4:00pm")
-        self.schedule.eval(now=run_time)
-        ret = self.schedule.job_status("job1")
-        _expected = (
-            "schedule.handle_func: Invalid, "
-            "range must be specified as a "
-            "dictionary. Ignoring job job1."
-        )
-        self.assertEqual(ret["_error"], _expected)
diff --git a/tests/unit/utils/scheduler/test_eval.py b/tests/unit/utils/scheduler/test_eval.py
deleted file mode 100644
index 6bd823b4ee48..000000000000
--- a/tests/unit/utils/scheduler/test_eval.py
+++ /dev/null
@@ -1,921 +0,0 @@
-import datetime
-import logging
-import random
-import time
-
-import pytest
-import salt.utils.platform
-import salt.utils.schedule
-from tests.support.mock import MagicMock, patch
-from tests.support.unit import skipIf
-from tests.unit.utils.scheduler.base import SchedulerTestsBase
-
-try:
-    import dateutil.parser
-
-    HAS_DATEUTIL_PARSER = True
-except ImportError:
-    HAS_DATEUTIL_PARSER = False
-
-
-try:
-    import croniter  # pylint: disable=unused-import
-
-    HAS_CRONITER = True
-except ImportError:
-    HAS_CRONITER = False
-
-log = logging.getLogger(__name__)
-
-
-@skipIf(
-    HAS_DATEUTIL_PARSER is False, "The 'dateutil.parser' library is not available",
-)
-@pytest.mark.windows_whitelisted
-class SchedulerEvalTest(SchedulerTestsBase):
-    def setUp(self):
-        super().setUp()
-        self.schedule.opts["loop_interval"] = 1
-        self.schedule.opts["grains"]["whens"] = {"tea time": "11/29/2017 12:00pm"}
-
-    def tearDown(self):
-        self.schedule.reset()
-        super().tearDown()
-
-    @pytest.mark.slow_test
-    def test_eval(self):
-        """
-        verify that scheduled job runs
-        """
-        job_name = "test_eval"
-        job = {
-            "schedule": {
-                job_name: {"function": "test.ping", "when": "11/29/2017 4:00pm"}
-            }
-        }
-        run_time2 = dateutil.parser.parse("11/29/2017 4:00pm")
-        run_time1 = run_time2 - datetime.timedelta(seconds=1)
-
-        # Add the job to the scheduler
-        self.schedule.opts.update(job)
-
-        # Evaluate 1 second before the run time
-        self.schedule.eval(now=run_time1)
-        ret = self.schedule.job_status(job_name)
-        self.assertNotIn("_last_run", ret)
-
-        # Evaluate 1 second at the run time
-        self.schedule.eval(now=run_time2)
-        ret = self.schedule.job_status(job_name)
-        self.assertEqual(ret["_last_run"], run_time2)
-
-    @pytest.mark.slow_test
-    def test_eval_multiple_whens(self):
-        """
-        verify that scheduled job runs
-        """
-        job_name = "test_eval_multiple_whens"
-        job = {
-            "schedule": {
-                job_name: {
-                    "function": "test.ping",
-                    "when": ["11/29/2017 4:00pm", "11/29/2017 5:00pm"],
-                }
-            }
-        }
-        if salt.utils.platform.is_darwin():
-            job["schedule"][job_name]["dry_run"] = True
-
-        run_time1 = dateutil.parser.parse("11/29/2017 4:00pm")
-        run_time2 = dateutil.parser.parse("11/29/2017 5:00pm")
-
-        # Add the job to the scheduler
-        self.schedule.opts.update(job)
-
-        # Evaluate run time1
-        self.schedule.eval(now=run_time1)
-        ret = self.schedule.job_status(job_name)
-        self.assertEqual(ret["_last_run"], run_time1)
-
-        time.sleep(2)
-
-        # Evaluate run time2
-        self.schedule.eval(now=run_time2)
-        ret = self.schedule.job_status(job_name)
-        self.assertEqual(ret["_last_run"], run_time2)
-
-    @pytest.mark.slow_test
-    def test_eval_whens(self):
-        """
-        verify that scheduled job runs
-        """
-        job_name = "test_eval_whens"
-        job = {"schedule": {job_name: {"function": "test.ping", "when": "tea time"}}}
-        run_time = dateutil.parser.parse("11/29/2017 12:00pm")
-
-        # Add the job to the scheduler
-        self.schedule.opts.update(job)
-
-        # Evaluate run time1
-        self.schedule.eval(now=run_time)
-        ret = self.schedule.job_status(job_name)
-        self.assertEqual(ret["_last_run"], run_time)
-
-    @pytest.mark.slow_test
-    def test_eval_loop_interval(self):
-        """
-        verify that scheduled job runs
-        """
-        job_name = "test_eval_loop_interval"
-        job = {
-            "schedule": {
-                job_name: {"function": "test.ping", "when": "11/29/2017 4:00pm"}
-            }
-        }
-        # 30 second loop interval
-        LOOP_INTERVAL = random.randint(30, 59)
-        self.schedule.opts["loop_interval"] = LOOP_INTERVAL
-
-        run_time2 = dateutil.parser.parse("11/29/2017 4:00pm")
-
-        # Add the job to the scheduler
-        self.schedule.opts.update(job)
-
-        # Evaluate 1 second at the run time
-        self.schedule.eval(now=run_time2 + datetime.timedelta(seconds=LOOP_INTERVAL))
-
-        ret = self.schedule.job_status(job_name)
-        self.assertEqual(
-            ret["_last_run"], run_time2 + datetime.timedelta(seconds=LOOP_INTERVAL)
-        )
-
-    @pytest.mark.slow_test
-    def test_eval_multiple_whens_loop_interval(self):
-        """
-        verify that scheduled job runs
-        """
-        job_name = "test_eval_multiple_whens_loop_interval"
-        job = {
-            "schedule": {
-                job_name: {
-                    "function": "test.ping",
-                    "when": ["11/29/2017 4:00pm", "11/29/2017 5:00pm"],
-                }
-            }
-        }
-        if salt.utils.platform.is_darwin():
-            job["schedule"][job_name]["dry_run"] = True
-
-        # 30 second loop interval
-        LOOP_INTERVAL = random.randint(30, 59)
-        self.schedule.opts["loop_interval"] = LOOP_INTERVAL
-
-        run_time1 = dateutil.parser.parse("11/29/2017 4:00pm") + datetime.timedelta(
-            seconds=LOOP_INTERVAL
-        )
-        run_time2 = dateutil.parser.parse("11/29/2017 5:00pm") + datetime.timedelta(
-            seconds=LOOP_INTERVAL
-        )
-
-        # Add the job to the scheduler
-        self.schedule.opts.update(job)
-
-        # Evaluate 1 second at the run time
-        self.schedule.eval(now=run_time1)
-        ret = self.schedule.job_status(job_name)
-        self.assertEqual(ret["_last_run"], run_time1)
-
-        time.sleep(2)
-
-        # Evaluate 1 second at the run time
-        self.schedule.eval(now=run_time2)
-        ret = self.schedule.job_status(job_name)
-        self.assertEqual(ret["_last_run"], run_time2)
-
-    @pytest.mark.slow_test
-    def test_eval_once(self):
-        """
-        verify that scheduled job runs
-        """
-        job_name = "test_once"
-        job = {
-            "schedule": {
-                job_name: {"function": "test.ping", "once": "2017-12-13T13:00:00"}
-            }
-        }
-        run_time = dateutil.parser.parse("12/13/2017 1:00pm")
-
-        # Add the job to the scheduler
-        self.schedule.opts["schedule"] = {}
-        self.schedule.opts.update(job)
-
-        # Evaluate 1 second at the run time
-        self.schedule.eval(now=run_time)
-        ret = self.schedule.job_status(job_name)
-        self.assertEqual(ret["_last_run"], run_time)
-
-    @pytest.mark.slow_test
-    def test_eval_once_loop_interval(self):
-        """
-        verify that scheduled job runs
-        """
-        job_name = "test_eval_once_loop_interval"
-        job = {
-            "schedule": {
-                job_name: {"function": "test.ping", "once": "2017-12-13T13:00:00"}
-            }
-        }
-        # Randomn second loop interval
-        LOOP_INTERVAL = random.randint(0, 59)
-        self.schedule.opts["loop_interval"] = LOOP_INTERVAL
-
-        # Run the job at the right plus LOOP_INTERVAL
-        run_time = dateutil.parser.parse("12/13/2017 1:00pm") + datetime.timedelta(
-            seconds=LOOP_INTERVAL
-        )
-
-        # Add the job to the scheduler
-        self.schedule.opts.update(job)
-
-        # Evaluate at the run time
-        self.schedule.eval(now=run_time)
-        ret = self.schedule.job_status(job_name)
-        self.assertEqual(ret["_last_run"], run_time)
-
-    @skipIf(not HAS_CRONITER, "Cannot find croniter python module")
-    def test_eval_cron(self):
-        """
-        verify that scheduled job runs
-        """
-        job_name = "test_eval_cron"
-        job = {
-            "schedule": {job_name: {"function": "test.ping", "cron": "0 16 29 11 *"}}
-        }
-
-        # Add the job to the scheduler
-        self.schedule.opts.update(job)
-
-        run_time = dateutil.parser.parse("11/29/2017 4:00pm")
-        with patch("croniter.croniter.get_next", MagicMock(return_value=run_time)):
-            self.schedule.eval(now=run_time)
-
-        ret = self.schedule.job_status(job_name)
-        self.assertEqual(ret["_last_run"], run_time)
-
-    @skipIf(not HAS_CRONITER, "Cannot find croniter python module")
-    def test_eval_cron_loop_interval(self):
-        """
-        verify that scheduled job runs
-        """
-        job_name = "test_eval_cron_loop_interval"
-        job = {
-            "schedule": {job_name: {"function": "test.ping", "cron": "0 16 29 11 *"}}
-        }
-        # Randomn second loop interval
-        LOOP_INTERVAL = random.randint(0, 59)
-        self.schedule.opts["loop_interval"] = LOOP_INTERVAL
-
-        # Add the job to the scheduler
-        self.schedule.opts.update(job)
-
-        run_time = dateutil.parser.parse("11/29/2017 4:00pm")
-        with patch("croniter.croniter.get_next", MagicMock(return_value=run_time)):
-            self.schedule.eval(now=run_time)
-
-        ret = self.schedule.job_status(job_name)
-        self.assertEqual(ret["_last_run"], run_time)
-
-    @pytest.mark.slow_test
-    def test_eval_until(self):
-        """
-        verify that scheduled job is skipped once the current
-        time reaches the specified until time
-        """
-        job_name = "test_eval_until"
-        job = {
-            "schedule": {
-                job_name: {
-                    "function": "test.ping",
-                    "hours": "1",
-                    "until": "11/29/2017 5:00pm",
-                }
-            }
-        }
-
-        if salt.utils.platform.is_darwin():
-            job["schedule"][job_name]["dry_run"] = True
-
-        # Add job to schedule
-        self.schedule.delete_job("test_eval_until")
-        self.schedule.opts.update(job)
-
-        # eval at 2:00pm to prime, simulate minion start up.
-        run_time = dateutil.parser.parse("11/29/2017 2:00pm")
-        self.schedule.eval(now=run_time)
-        ret = self.schedule.job_status(job_name)
-
-        # eval at 3:00pm, will run.
-        run_time = dateutil.parser.parse("11/29/2017 3:00pm")
-        self.schedule.eval(now=run_time)
-        ret = self.schedule.job_status(job_name)
-        self.assertEqual(ret["_last_run"], run_time)
-
-        time.sleep(2)
-
-        # eval at 4:00pm, will run.
-        run_time = dateutil.parser.parse("11/29/2017 4:00pm")
-        self.schedule.eval(now=run_time)
-        ret = self.schedule.job_status(job_name)
-        self.assertEqual(ret["_last_run"], run_time)
-
-        time.sleep(2)
-
-        # eval at 5:00pm, will not run
-        run_time = dateutil.parser.parse("11/29/2017 5:00pm")
-        self.schedule.eval(now=run_time)
-        ret = self.schedule.job_status(job_name)
-        self.assertEqual(ret["_skip_reason"], "until_passed")
-        self.assertEqual(ret["_skipped_time"], run_time)
-
-    @pytest.mark.slow_test
-    def test_eval_after(self):
-        """
-        verify that scheduled job is skipped until after the specified
-        time has been reached.
-        """
-        job_name = "test_eval_after"
-        job = {
-            "schedule": {
-                job_name: {
-                    "function": "test.ping",
-                    "hours": "1",
-                    "after": "11/29/2017 5:00pm",
-                }
-            }
-        }
-
-        # Add job to schedule
-        self.schedule.opts.update(job)
-
-        # eval at 2:00pm to prime, simulate minion start up.
-        run_time = dateutil.parser.parse("11/29/2017 2:00pm")
-        self.schedule.eval(now=run_time)
-        ret = self.schedule.job_status(job_name)
-
-        # eval at 3:00pm, will not run.
-        run_time = dateutil.parser.parse("11/29/2017 3:00pm")
-        self.schedule.eval(now=run_time)
-        ret = self.schedule.job_status(job_name)
-        self.assertEqual(ret["_skip_reason"], "after_not_passed")
-        self.assertEqual(ret["_skipped_time"], run_time)
-
-        # eval at 4:00pm, will not run.
-        run_time = dateutil.parser.parse("11/29/2017 4:00pm")
-        self.schedule.eval(now=run_time)
-        ret = self.schedule.job_status(job_name)
-        self.assertEqual(ret["_skip_reason"], "after_not_passed")
-        self.assertEqual(ret["_skipped_time"], run_time)
-
-        # eval at 5:00pm, will not run
-        run_time = dateutil.parser.parse("11/29/2017 5:00pm")
-        self.schedule.eval(now=run_time)
-        ret = self.schedule.job_status(job_name)
-        self.assertEqual(ret["_skip_reason"], "after_not_passed")
-        self.assertEqual(ret["_skipped_time"], run_time)
-
-        # eval at 6:00pm, will run
-        run_time = dateutil.parser.parse("11/29/2017 6:00pm")
-        self.schedule.eval(now=run_time)
-        ret = self.schedule.job_status(job_name)
-        self.assertEqual(ret["_last_run"], run_time)
-
-    @pytest.mark.slow_test
-    def test_eval_enabled(self):
-        """
-        verify that scheduled job does not run
-        """
-        job_name = "test_eval_enabled"
-        job = {
-            "schedule": {
-                "enabled": True,
-                job_name: {"function": "test.ping", "when": "11/29/2017 4:00pm"},
-            }
-        }
-        run_time1 = dateutil.parser.parse("11/29/2017 4:00pm")
-
-        # Add the job to the scheduler
-        self.schedule.opts.update(job)
-
-        # Evaluate 1 second at the run time
-        self.schedule.eval(now=run_time1)
-        ret = self.schedule.job_status(job_name)
-        self.assertEqual(ret["_last_run"], run_time1)
-
-    @pytest.mark.slow_test
-    def test_eval_enabled_key(self):
-        """
-        verify that scheduled job runs
-        when the enabled key is in place
-        https://github.com/saltstack/salt/issues/47695
-        """
-        job_name = "test_eval_enabled_key"
-        job = {
-            "schedule": {
-                "enabled": True,
-                job_name: {"function": "test.ping", "when": "11/29/2017 4:00pm"},
-            }
-        }
-        run_time2 = dateutil.parser.parse("11/29/2017 4:00pm")
-        run_time1 = run_time2 - datetime.timedelta(seconds=1)
-
-        # Add the job to the scheduler
-        self.schedule.opts.update(job)
-
-        # Evaluate 1 second before the run time
-        self.schedule.eval(now=run_time1)
-        ret = self.schedule.job_status("test_eval_enabled_key")
-        self.assertNotIn("_last_run", ret)
-
-        # Evaluate 1 second at the run time
-        self.schedule.eval(now=run_time2)
-        ret = self.schedule.job_status("test_eval_enabled_key")
-        self.assertEqual(ret["_last_run"], run_time2)
-
-    def test_eval_disabled(self):
-        """
-        verify that scheduled job does not run
-        """
-        job_name = "test_eval_disabled"
-        job = {
-            "schedule": {
-                "enabled": False,
-                job_name: {"function": "test.ping", "when": "11/29/2017 4:00pm"},
-            }
-        }
-        run_time1 = dateutil.parser.parse("11/29/2017 4:00pm")
-
-        # Add the job to the scheduler
-        self.schedule.opts.update(job)
-
-        # Evaluate 1 second at the run time
-        self.schedule.eval(now=run_time1)
-        ret = self.schedule.job_status(job_name)
-        self.assertNotIn("_last_run", ret)
-        self.assertEqual(ret["_skip_reason"], "disabled")
-
-        # Ensure job data still matches
-        self.assertEqual(ret, job["schedule"][job_name])
-
-    def test_eval_global_disabled_job_enabled(self):
-        """
-        verify that scheduled job does not run
-        """
-        job_name = "test_eval_global_disabled"
-        job = {
-            "schedule": {
-                "enabled": False,
-                job_name: {
-                    "function": "test.ping",
-                    "when": "11/29/2017 4:00pm",
-                    "enabled": True,
-                },
-            }
-        }
-        run_time1 = dateutil.parser.parse("11/29/2017 4:00pm")
-
-        # Add the job to the scheduler
-        self.schedule.opts.update(job)
-
-        # Evaluate 1 second at the run time
-        self.schedule.eval(now=run_time1)
-        ret = self.schedule.job_status(job_name)
-        self.assertNotIn("_last_run", ret)
-        self.assertEqual(ret["_skip_reason"], "disabled")
-
-        # Ensure job is still enabled
-        self.assertEqual(ret["enabled"], True)
-
-    @pytest.mark.slow_test
-    def test_eval_run_on_start(self):
-        """
-        verify that scheduled job is run when minion starts
-        """
-        job_name = "test_eval_run_on_start"
-        job = {
-            "schedule": {
-                job_name: {"function": "test.ping", "hours": "1", "run_on_start": True}
-            }
-        }
-
-        # Add job to schedule
-        self.schedule.opts.update(job)
-
-        # eval at 2:00pm, will run.
-        run_time = dateutil.parser.parse("11/29/2017 2:00pm")
-        self.schedule.eval(now=run_time)
-        ret = self.schedule.job_status(job_name)
-        self.assertEqual(ret["_last_run"], run_time)
-
-        # eval at 3:00pm, will run.
-        run_time = dateutil.parser.parse("11/29/2017 3:00pm")
-        self.schedule.eval(now=run_time)
-        ret = self.schedule.job_status(job_name)
-
-    @pytest.mark.slow_test
-    def test_eval_splay(self):
-        """
-        verify that scheduled job runs with splayed time
-        """
-        job_name = "job_eval_splay"
-        job = {
-            "schedule": {
-                job_name: {"function": "test.ping", "seconds": "30", "splay": "10"}
-            }
-        }
-
-        # Add job to schedule
-        self.schedule.opts.update(job)
-
-        with patch("random.randint", MagicMock(return_value=10)):
-            # eval at 2:00pm to prime, simulate minion start up.
-            run_time = dateutil.parser.parse("11/29/2017 2:00pm")
-            self.schedule.eval(now=run_time)
-            ret = self.schedule.job_status(job_name)
-
-            # eval at 2:00:40pm, will run.
-            run_time = dateutil.parser.parse("11/29/2017 2:00:40pm")
-            self.schedule.eval(now=run_time)
-            ret = self.schedule.job_status(job_name)
-            self.assertEqual(ret["_last_run"], run_time)
-
-    @pytest.mark.slow_test
-    def test_eval_splay_range(self):
-        """
-        verify that scheduled job runs with splayed time
-        """
-        job_name = "job_eval_splay_range"
-        job = {
-            "schedule": {
-                job_name: {
-                    "function": "test.ping",
-                    "seconds": "30",
-                    "splay": {"start": 5, "end": 10},
-                }
-            }
-        }
-
-        # Add job to schedule
-        self.schedule.opts.update(job)
-
-        with patch("random.randint", MagicMock(return_value=10)):
-            # eval at 2:00pm to prime, simulate minion start up.
-            run_time = dateutil.parser.parse("11/29/2017 2:00pm")
-            self.schedule.eval(now=run_time)
-            ret = self.schedule.job_status(job_name)
-
-            # eval at 2:00:40pm, will run.
-            run_time = dateutil.parser.parse("11/29/2017 2:00:40pm")
-            self.schedule.eval(now=run_time)
-            ret = self.schedule.job_status(job_name)
-            self.assertEqual(ret["_last_run"], run_time)
-
-    @pytest.mark.slow_test
-    def test_eval_splay_global(self):
-        """
-        verify that scheduled job runs with splayed time
-        """
-        job_name = "job_eval_splay_global"
-        job = {
-            "schedule": {
-                "splay": {"start": 5, "end": 10},
-                job_name: {"function": "test.ping", "seconds": "30"},
-            }
-        }
-
-        # Add job to schedule
-        self.schedule.opts.update(job)
-
-        with patch("random.randint", MagicMock(return_value=10)):
-            # eval at 2:00pm to prime, simulate minion start up.
-            run_time = dateutil.parser.parse("11/29/2017 2:00pm")
-            self.schedule.eval(now=run_time)
-            ret = self.schedule.job_status(job_name)
-
-            # eval at 2:00:40pm, will run.
-            run_time = dateutil.parser.parse("11/29/2017 2:00:40pm")
-            self.schedule.eval(now=run_time)
-            ret = self.schedule.job_status(job_name)
-            self.assertEqual(ret["_last_run"], run_time)
-
-    @pytest.mark.slow_test
-    def test_eval_seconds(self):
-        """
-        verify that scheduled job run mutiple times with seconds
-        """
-        with patch.dict(self.schedule.opts, {"run_schedule_jobs_in_background": False}):
-            job_name = "job_eval_seconds"
-            job = {"schedule": {job_name: {"function": "test.ping", "seconds": "30"}}}
-
-            if salt.utils.platform.is_darwin():
-                job["schedule"][job_name]["dry_run"] = True
-
-            # Add job to schedule
-            self.schedule.opts.update(job)
-
-            # eval at 2:00pm to prime, simulate minion start up.
-            run_time = dateutil.parser.parse("11/29/2017 2:00pm")
-            next_run_time = run_time + datetime.timedelta(seconds=30)
-            jids = self.schedule.eval(now=run_time)
-            assert len(jids) == 0
-            ret = self.schedule.job_status(job_name)
-            self.assertEqual(ret["_next_fire_time"], next_run_time)
-
-            # eval at 2:00:01pm, will not run.
-            run_time = dateutil.parser.parse("11/29/2017 2:00:01pm")
-            jids = self.schedule.eval(now=run_time)
-            assert len(jids) == 0
-            ret = self.schedule.job_status(job_name)
-            self.assertNotIn("_last_run", ret)
-            self.assertEqual(ret["_next_fire_time"], next_run_time)
-
-            # eval at 2:00:30pm, will run.
-            run_time = dateutil.parser.parse("11/29/2017 2:00:30pm")
-            next_run_time = run_time + datetime.timedelta(seconds=30)
-            jids = self.schedule.eval(now=run_time)
-            assert len(jids) == 1
-            ret = self.schedule.job_status(job_name)
-            self.assertEqual(ret["_last_run"], run_time)
-            self.assertEqual(ret["_next_fire_time"], next_run_time)
-
-            # eval at 2:01:00pm, will run.
-            run_time = dateutil.parser.parse("11/29/2017 2:01:00pm")
-            next_run_time = run_time + datetime.timedelta(seconds=30)
-            jids = self.schedule.eval(now=run_time)
-            assert len(jids) == 1
-            ret = self.schedule.job_status(job_name)
-            self.assertEqual(ret["_last_run"], run_time)
-            self.assertEqual(ret["_next_fire_time"], next_run_time)
-
-            # eval at 2:01:30pm, will run.
-            run_time = dateutil.parser.parse("11/29/2017 2:01:30pm")
-            next_run_time = run_time + datetime.timedelta(seconds=30)
-            jids = self.schedule.eval(now=run_time)
-            assert len(jids) == 1
-            ret = self.schedule.job_status(job_name)
-            self.assertEqual(ret["_last_run"], run_time)
-            self.assertEqual(ret["_next_fire_time"], next_run_time)
-
-    @pytest.mark.slow_test
-    def test_eval_minutes(self):
-        """
-        verify that scheduled job run mutiple times with minutes
-        """
-        with patch.dict(self.schedule.opts, {"run_schedule_jobs_in_background": False}):
-            job_name = "job_eval_minutes"
-            job = {"schedule": {job_name: {"function": "test.ping", "minutes": "30"}}}
-
-            if salt.utils.platform.is_darwin():
-                job["schedule"][job_name]["dry_run"] = True
-
-            # Add job to schedule
-            self.schedule.opts.update(job)
-
-            # eval at 2:00pm to prime, simulate minion start up.
-            run_time = dateutil.parser.parse("11/29/2017 2:00pm")
-            next_run_time = run_time + datetime.timedelta(minutes=30)
-            jids = self.schedule.eval(now=run_time)
-            assert len(jids) == 0
-            ret = self.schedule.job_status(job_name)
-            self.assertEqual(ret["_next_fire_time"], next_run_time)
-
-            # eval at 2:00:01pm, will not run.
-            run_time = dateutil.parser.parse("11/29/2017 2:00:01pm")
-            jids = self.schedule.eval(now=run_time)
-            assert len(jids) == 0
-            ret = self.schedule.job_status(job_name)
-            self.assertNotIn("_last_run", ret)
-            self.assertEqual(ret["_next_fire_time"], next_run_time)
-
-            # eval at 2:30:00pm, will run.
-            run_time = dateutil.parser.parse("11/29/2017 2:30:00pm")
-            jids = self.schedule.eval(now=run_time)
-            assert len(jids) == 1
-            ret = self.schedule.job_status(job_name)
-            self.assertEqual(ret["_last_run"], run_time)
-
-            # eval at 3:00:00pm, will run.
-            run_time = dateutil.parser.parse("11/29/2017 3:00:00pm")
-            jids = self.schedule.eval(now=run_time)
-            assert len(jids) == 1
-            ret = self.schedule.job_status(job_name)
-            self.assertEqual(ret["_last_run"], run_time)
-
-            # eval at 3:30:00pm, will run.
-            run_time = dateutil.parser.parse("11/29/2017 3:30:00pm")
-            jids = self.schedule.eval(now=run_time)
-            assert len(jids) == 1
-            ret = self.schedule.job_status(job_name)
-            self.assertEqual(ret["_last_run"], run_time)
-
-    @pytest.mark.slow_test
-    def test_eval_hours(self):
-        """
-        verify that scheduled job run mutiple times with hours
-        """
-        with patch.dict(self.schedule.opts, {"run_schedule_jobs_in_background": False}):
-            job_name = "job_eval_hours"
-            job = {"schedule": {job_name: {"function": "test.ping", "hours": "2"}}}
-
-            if salt.utils.platform.is_darwin():
-                job["schedule"][job_name]["dry_run"] = True
-
-            # Add job to schedule
-            self.schedule.opts.update(job)
-
-            # eval at 2:00pm to prime, simulate minion start up.
-            run_time = dateutil.parser.parse("11/29/2017 2:00pm")
-            next_run_time = run_time + datetime.timedelta(hours=2)
-            jids = self.schedule.eval(now=run_time)
-            assert len(jids) == 0
-            ret = self.schedule.job_status(job_name)
-            self.assertEqual(ret["_next_fire_time"], next_run_time)
-
-            # eval at 2:00:01pm, will not run.
-            run_time = dateutil.parser.parse("11/29/2017 2:00:01pm")
-            jids = self.schedule.eval(now=run_time)
-            assert len(jids) == 0
-            ret = self.schedule.job_status(job_name)
-            self.assertNotIn("_last_run", ret)
-            self.assertEqual(ret["_next_fire_time"], next_run_time)
-
-            # eval at 4:00:00pm, will run.
-            run_time = dateutil.parser.parse("11/29/2017 4:00:00pm")
-            jids = self.schedule.eval(now=run_time)
-            assert len(jids) == 1
-            ret = self.schedule.job_status(job_name)
-            self.assertEqual(ret["_last_run"], run_time)
-
-            # eval at 6:00:00pm, will run.
-            run_time = dateutil.parser.parse("11/29/2017 6:00:00pm")
-            jids = self.schedule.eval(now=run_time)
-            assert len(jids) == 1
-
-            ret = self.schedule.job_status(job_name)
-            self.assertEqual(ret["_last_run"], run_time)
-
-            # eval at 8:00:00pm, will run.
-            run_time = dateutil.parser.parse("11/29/2017 8:00:00pm")
-            pids = self.schedule.eval(now=run_time)
-            assert len(jids) == 1
-
-            ret = self.schedule.job_status(job_name)
-            self.assertEqual(ret["_last_run"], run_time)
-
-    @pytest.mark.slow_test
-    def test_eval_days(self):
-        """
-        verify that scheduled job run mutiple times with days
-        """
-        job_name = "job_eval_days"
-        job = {
-            "schedule": {
-                job_name: {"function": "test.ping", "days": "2", "dry_run": True}
-            }
-        }
-
-        if salt.utils.platform.is_darwin():
-            job["schedule"][job_name]["dry_run"] = True
-
-        # Add job to schedule
-        self.schedule.opts.update(job)
-
-        # eval at 11/23/2017 2:00pm to prime, simulate minion start up.
-        run_time = dateutil.parser.parse("11/23/2017 2:00pm")
-        next_run_time = run_time + datetime.timedelta(days=2)
-        self.schedule.eval(now=run_time)
-        ret = self.schedule.job_status(job_name)
-        self.assertEqual(ret["_next_fire_time"], next_run_time)
-
-        # eval at 11/25/2017 2:00:00pm, will run.
-        run_time = dateutil.parser.parse("11/25/2017 2:00:00pm")
-        next_run_time = run_time + datetime.timedelta(days=2)
-        self.schedule.eval(now=run_time)
-        ret = self.schedule.job_status(job_name)
-        self.assertEqual(ret["_last_run"], run_time)
-        self.assertEqual(ret["_next_fire_time"], next_run_time)
-
-        # eval at 11/26/2017 2:00:00pm, will not run.
-        run_time = dateutil.parser.parse("11/26/2017 2:00:00pm")
-        last_run_time = run_time - datetime.timedelta(days=1)
-        self.schedule.eval(now=run_time)
-        ret = self.schedule.job_status(job_name)
-        self.assertEqual(ret["_last_run"], last_run_time)
-        self.assertEqual(ret["_next_fire_time"], next_run_time)
-
-        time.sleep(2)
-
-        # eval at 11/27/2017 2:00:00pm, will run.
-        run_time = dateutil.parser.parse("11/27/2017 2:00:00pm")
-        next_run_time = run_time + datetime.timedelta(days=2)
-        self.schedule.eval(now=run_time)
-        ret = self.schedule.job_status(job_name)
-        self.assertEqual(ret["_last_run"], run_time)
-        self.assertEqual(ret["_next_fire_time"], next_run_time)
-
-        time.sleep(2)
-
-        # eval at 11/28/2017 2:00:00pm, will not run.
-        run_time = dateutil.parser.parse("11/28/2017 2:00:00pm")
-        last_run_time = run_time - datetime.timedelta(days=1)
-        self.schedule.eval(now=run_time)
-        ret = self.schedule.job_status(job_name)
-        self.assertEqual(ret["_last_run"], last_run_time)
-        self.assertEqual(ret["_next_fire_time"], next_run_time)
-
-        time.sleep(2)
-
-        # eval at 11/29/2017 2:00:00pm, will run.
-        run_time = dateutil.parser.parse("11/29/2017 2:00:00pm")
-        next_run_time = run_time + datetime.timedelta(days=2)
-        self.schedule.eval(now=run_time)
-        ret = self.schedule.job_status(job_name)
-        self.assertEqual(ret["_last_run"], run_time)
-        self.assertEqual(ret["_next_fire_time"], next_run_time)
-
-    @pytest.mark.slow_test
-    def test_eval_when_splay(self):
-        """
-        verify that scheduled job runs
-        """
-        job_name = "test_eval_when_splay"
-        splay = 300
-        job = {
-            "schedule": {
-                job_name: {
-                    "function": "test.ping",
-                    "when": "11/29/2017 4:00pm",
-                    "splay": splay,
-                }
-            }
-        }
-        run_time1 = dateutil.parser.parse("11/29/2017 4:00pm")
-        run_time2 = run_time1 + datetime.timedelta(seconds=splay)
-        run_time3 = run_time2 + datetime.timedelta(seconds=1)
-
-        # Add the job to the scheduler
-        self.schedule.opts.update(job)
-
-        with patch("random.randint", MagicMock(return_value=splay)):
-            # Evaluate to prime
-            run_time = dateutil.parser.parse("11/29/2017 3:00pm")
-            self.schedule.eval(now=run_time)
-            ret = self.schedule.job_status(job_name)
-
-            # Evaluate at expected runtime1, should not run
-            self.schedule.eval(now=run_time1)
-            ret = self.schedule.job_status(job_name)
-            self.assertNotIn("_last_run", ret)
-
-            # Evaluate at expected runtime2, should run
-            self.schedule.eval(now=run_time2)
-            ret = self.schedule.job_status(job_name)
-            self.assertEqual(ret["_last_run"], run_time2)
-
-            # Evaluate at expected runtime3, should not run
-            # _next_fire_time should be None
-            self.schedule.eval(now=run_time3)
-            ret = self.schedule.job_status(job_name)
-            self.assertEqual(ret["_last_run"], run_time2)
-            self.assertEqual(ret["_next_fire_time"], None)
-
-    def test_eval_when_splay_in_past(self):
-        """
-        verify that scheduled job runs
-        """
-        job_name = "test_eval_when_splay_in_past"
-        splay = 300
-        job = {
-            "schedule": {
-                job_name: {
-                    "function": "test.ping",
-                    "when": ["11/29/2017 6:00am"],
-                    "splay": splay,
-                }
-            }
-        }
-        run_time1 = dateutil.parser.parse("11/29/2017 4:00pm")
-
-        # Add the job to the scheduler
-        self.schedule.opts.update(job)
-
-        # Evaluate to prime
-        run_time = dateutil.parser.parse("11/29/2017 3:00pm")
-        self.schedule.eval(now=run_time)
-        ret = self.schedule.job_status(job_name)
-
-        # Evaluate at expected runtime1, should not run
-        # and _next_fire_time should be None
-        self.schedule.eval(now=run_time1)
-        ret = self.schedule.job_status(job_name)
-        self.assertNotIn("_last_run", ret)
-        self.assertEqual(ret["_next_fire_time"], None)
diff --git a/tests/unit/utils/scheduler/test_helpers.py b/tests/unit/utils/scheduler/test_helpers.py
deleted file mode 100644
index cda8fdc4ce2e..000000000000
--- a/tests/unit/utils/scheduler/test_helpers.py
+++ /dev/null
@@ -1,38 +0,0 @@
-# -*- coding: utf-8 -*-
-
-from __future__ import absolute_import
-
-import logging
-
-from tests.unit.utils.scheduler.base import SchedulerTestsBase
-
-log = logging.getLogger(__name__)
-
-
-class SchedulerHelpersTest(SchedulerTestsBase):
-    """
-    Test scheduler helper functions
-    """
-
-    def setUp(self):
-        super(SchedulerHelpersTest, self).setUp()
-        self.schedule.opts["loop_interval"] = 1
-
-    def test_get_schedule(self):
-        """
-        verify that the _get_schedule function works
-        when remove_hidden is True and schedule data
-        contains enabled key
-        """
-        job_name = "test_get_schedule"
-        job = {
-            "schedule": {
-                "enabled": True,
-                job_name: {"function": "test.ping", "seconds": 60},
-            }
-        }
-        # Add the job to the scheduler
-        self.schedule.opts.update(job)
-
-        ret = self.schedule._get_schedule(remove_hidden=True)
-        self.assertEqual(job["schedule"], ret)
diff --git a/tests/unit/utils/scheduler/test_maxrunning.py b/tests/unit/utils/scheduler/test_maxrunning.py
deleted file mode 100644
index f9e884233ab4..000000000000
--- a/tests/unit/utils/scheduler/test_maxrunning.py
+++ /dev/null
@@ -1,130 +0,0 @@
-# -*- coding: utf-8 -*-
-
-from __future__ import absolute_import
-
-import logging
-
-import dateutil.parser as dateutil_parser
-from tests.support.mock import MagicMock, patch
-from tests.unit.utils.scheduler.base import SchedulerTestsBase
-
-log = logging.getLogger(__name__)
-
-
-class SchedulerMaxRunningTest(SchedulerTestsBase):
-    """
-    Validate the pkg module
-    """
-
-    def setUp(self):
-        super(SchedulerMaxRunningTest, self).setUp()
-        self.schedule.opts["loop_interval"] = 1
-
-    def test_maxrunning_minion(self):
-        """
-        verify that scheduled job runs
-        """
-        self.schedule.opts["__role"] = "minion"
-
-        job = {
-            "schedule": {
-                "maxrunning_minion": {
-                    "function": "test.ping",
-                    "seconds": 10,
-                    "maxrunning": 1,
-                }
-            }
-        }
-
-        job_data = {
-            "function": "test.ping",
-            "run": True,
-            "name": "maxrunning_minion",
-            "seconds": 10,
-            "_seconds": 10,
-            "jid_include": True,
-            "maxrunning": 1,
-        }
-
-        # Add the job to the scheduler
-        self.schedule.opts.update(job)
-
-        running_data = [
-            {
-                "fun_args": [],
-                "jid": "20181018165923360935",
-                "schedule": "maxrunning_minion",
-                "pid": 15338,
-                "fun": "test.ping",
-                "id": "host",
-            }
-        ]
-
-        run_time = dateutil_parser.parse("11/29/2017 4:00pm")
-
-        with patch("salt.utils.minion.running", MagicMock(return_value=running_data)):
-            with patch(
-                "salt.utils.process.os_is_running", MagicMock(return_value=True)
-            ):
-                ret = self.schedule._check_max_running(
-                    "test.ping", job_data, self.schedule.opts, now=run_time
-                )
-        self.assertIn("_skip_reason", ret)
-        self.assertEqual("maxrunning", ret["_skip_reason"])
-        self.assertEqual(False, ret["run"])
-
-    def test_maxrunning_master(self):
-        """
-        verify that scheduled job runs
-        """
-        self.schedule.opts["__role"] = "master"
-
-        job = {
-            "schedule": {
-                "maxrunning_master": {
-                    "function": "state.orch",
-                    "args": ["test.orch_test"],
-                    "minutes": 1,
-                    "maxrunning": 1,
-                }
-            }
-        }
-
-        job_data = {
-            "function": "state.orch",
-            "fun_args": ["test.orch_test"],
-            "run": True,
-            "name": "maxrunning_master",
-            "minutes": 1,
-            "jid_include": True,
-            "maxrunning": 1,
-        }
-
-        # Add the job to the scheduler
-        self.schedule.opts.update(job)
-
-        running_data = [
-            {
-                "fun_args": ["test.orch_test"],
-                "jid": "20181018165923360935",
-                "schedule": "maxrunning_master",
-                "pid": 15338,
-                "fun": "state.orch",
-                "id": "host",
-            }
-        ]
-
-        run_time = dateutil_parser.parse("11/29/2017 4:00pm")
-
-        with patch(
-            "salt.utils.master.get_running_jobs", MagicMock(return_value=running_data)
-        ):
-            with patch(
-                "salt.utils.process.os_is_running", MagicMock(return_value=True)
-            ):
-                ret = self.schedule._check_max_running(
-                    "state.orch", job_data, self.schedule.opts, now=run_time
-                )
-        self.assertIn("_skip_reason", ret)
-        self.assertEqual("maxrunning", ret["_skip_reason"])
-        self.assertEqual(False, ret["run"])
diff --git a/tests/unit/utils/scheduler/test_postpone.py b/tests/unit/utils/scheduler/test_postpone.py
deleted file mode 100644
index 5bd9d3797429..000000000000
--- a/tests/unit/utils/scheduler/test_postpone.py
+++ /dev/null
@@ -1,75 +0,0 @@
-import datetime
-import logging
-
-import pytest
-from tests.support.unit import skipIf
-from tests.unit.utils.scheduler.base import SchedulerTestsBase
-
-try:
-    import dateutil.parser
-
-    HAS_DATEUTIL_PARSER = True
-except ImportError:
-    HAS_DATEUTIL_PARSER = False
-
-log = logging.getLogger(__name__)
-
-
-@skipIf(
-    HAS_DATEUTIL_PARSER is False, "The 'dateutil.parser' library is not available",
-)
-class SchedulerPostponeTest(SchedulerTestsBase):
-    """
-    Validate the pkg module
-    """
-
-    def setUp(self):
-        super().setUp()
-        self.schedule.opts["loop_interval"] = 1
-
-    def tearDown(self):
-        self.schedule.reset()
-        super().tearDown()
-
-    @pytest.mark.slow_test
-    def test_postpone(self):
-        """
-        verify that scheduled job is postponed until the specified time.
-        """
-        job = {
-            "schedule": {"job1": {"function": "test.ping", "when": "11/29/2017 4pm"}}
-        }
-
-        # 11/29/2017 4pm
-        run_time = dateutil.parser.parse("11/29/2017 4:00pm")
-
-        # 5 minute delay
-        delay = 300
-
-        # Add job to schedule
-        self.schedule.opts.update(job)
-
-        # Postpone the job by 5 minutes
-        self.schedule.postpone_job(
-            "job1",
-            {
-                "time": run_time.strftime("%Y-%m-%dT%H:%M:%S"),
-                "new_time": (run_time + datetime.timedelta(seconds=delay)).strftime(
-                    "%Y-%m-%dT%H:%M:%S"
-                ),
-            },
-        )
-        # Run at the original time
-        self.schedule.eval(now=run_time)
-        ret = self.schedule.job_status("job1")
-        self.assertNotIn("_last_run", ret)
-
-        # Run 5 minutes later
-        self.schedule.eval(now=run_time + datetime.timedelta(seconds=delay))
-        ret = self.schedule.job_status("job1")
-        self.assertEqual(ret["_last_run"], run_time + datetime.timedelta(seconds=delay))
-
-        # Run 6 minutes later
-        self.schedule.eval(now=run_time + datetime.timedelta(seconds=delay + 1))
-        ret = self.schedule.job_status("job1")
-        self.assertEqual(ret["_last_run"], run_time + datetime.timedelta(seconds=delay))
diff --git a/tests/unit/utils/scheduler/test_run_job.py b/tests/unit/utils/scheduler/test_run_job.py
deleted file mode 100644
index c498c3f0133c..000000000000
--- a/tests/unit/utils/scheduler/test_run_job.py
+++ /dev/null
@@ -1,34 +0,0 @@
-# -*- coding: utf-8 -*-
-
-from __future__ import absolute_import
-
-import logging
-
-from tests.unit.utils.scheduler.base import SchedulerTestsBase
-
-log = logging.getLogger(__name__)
-
-
-class SchedulerRunJobTest(SchedulerTestsBase):
-    """
-    Validate the pkg module
-    """
-
-    def setUp(self):
-        super(SchedulerRunJobTest, self).setUp()
-        self.schedule.opts["loop_interval"] = 1
-
-    def test_run_job(self):
-        """
-        verify that scheduled job runs
-        """
-        job_name = "test_run_job"
-        job = {"schedule": {job_name: {"function": "test.ping"}}}
-        # Add the job to the scheduler
-        self.schedule.opts.update(job)
-
-        # Run job
-        self.schedule.run_job(job_name)
-        ret = self.schedule.job_status(job_name)
-        expected = {"function": "test.ping", "run": True, "name": "test_run_job"}
-        self.assertEqual(ret, expected)
diff --git a/tests/unit/utils/scheduler/test_schedule.py b/tests/unit/utils/scheduler/test_schedule.py
deleted file mode 100644
index 166c0970f6ee..000000000000
--- a/tests/unit/utils/scheduler/test_schedule.py
+++ /dev/null
@@ -1,495 +0,0 @@
-"""
-    :codeauthor: Nicole Thomas <nicole@saltstack.com>
-"""
-
-import copy
-import datetime
-import logging
-
-import pytest
-import salt.config
-import salt.utils.schedule
-from salt.utils.schedule import Schedule
-from tests.support.mock import MagicMock, patch
-from tests.support.unit import skipIf
-from tests.unit.utils.scheduler.base import SchedulerTestsBase
-
-# pylint: disable=import-error,unused-import
-try:
-    import croniter
-
-    _CRON_SUPPORTED = True
-except ImportError:
-    _CRON_SUPPORTED = False
-# pylint: enable=import-error
-
-log = logging.getLogger(__name__)
-
-
-# pylint: disable=too-many-public-methods,invalid-name
-class ScheduleTestCase(SchedulerTestsBase):
-    """
-    Unit tests for salt.utils.schedule module
-    """
-
-    # delete_job tests
-
-    @pytest.mark.slow_test
-    def test_delete_job_exists(self):
-        """
-        Tests ensuring the job exists and deleting it
-        """
-        self.schedule.opts.update({"schedule": {"foo": "bar"}, "pillar": {}})
-        self.assertIn("foo", self.schedule.opts["schedule"])
-        self.schedule.delete_job("foo")
-        self.assertNotIn("foo", self.schedule.opts["schedule"])
-
-    @pytest.mark.slow_test
-    def test_delete_job_in_pillar(self):
-        """
-        Tests ignoring deletion job from pillar
-        """
-        self.schedule.opts.update(
-            {"pillar": {"schedule": {"foo": "bar"}}, "schedule": {}}
-        )
-        self.assertIn("foo", self.schedule.opts["pillar"]["schedule"])
-        self.schedule.delete_job("foo")
-        self.assertIn("foo", self.schedule.opts["pillar"]["schedule"])
-
-    @pytest.mark.slow_test
-    def test_delete_job_intervals(self):
-        """
-        Tests removing job from intervals
-        """
-        self.schedule.opts.update({"pillar": {}, "schedule": {}})
-        self.schedule.intervals = {"foo": "bar"}
-        self.schedule.delete_job("foo")
-        self.assertNotIn("foo", self.schedule.intervals)
-
-    @pytest.mark.slow_test
-    def test_delete_job_prefix(self):
-        """
-        Tests ensuring jobs exists and deleting them by prefix
-        """
-        self.schedule.opts.update(
-            {
-                "schedule": {"foobar": "bar", "foobaz": "baz", "fooboo": "boo"},
-                "pillar": {},
-            }
-        )
-        ret = copy.deepcopy(self.schedule.opts)
-        del ret["schedule"]["foobar"]
-        del ret["schedule"]["foobaz"]
-        self.schedule.delete_job_prefix("fooba")
-        self.assertEqual(self.schedule.opts, ret)
-
-    @pytest.mark.slow_test
-    def test_delete_job_prefix_in_pillar(self):
-        """
-        Tests ignoring deletion jobs by prefix from pillar
-        """
-        self.schedule.opts.update(
-            {
-                "pillar": {
-                    "schedule": {"foobar": "bar", "foobaz": "baz", "fooboo": "boo"}
-                },
-                "schedule": {},
-            }
-        )
-        ret = copy.deepcopy(self.schedule.opts)
-        self.schedule.delete_job_prefix("fooba")
-        self.assertEqual(self.schedule.opts, ret)
-
-    # add_job tests
-
-    def test_add_job_data_not_dict(self):
-        """
-        Tests if data is a dictionary
-        """
-        data = "foo"
-        self.assertRaises(ValueError, Schedule.add_job, self.schedule, data)
-
-    def test_add_job_multiple_jobs(self):
-        """
-        Tests if more than one job is scheduled at a time
-        """
-        data = {"key1": "value1", "key2": "value2"}
-        self.assertRaises(ValueError, Schedule.add_job, self.schedule, data)
-
-    @pytest.mark.slow_test
-    def test_add_job(self):
-        """
-        Tests adding a job to the schedule
-        """
-        data = {"foo": {"bar": "baz"}}
-        ret = copy.deepcopy(self.schedule.opts)
-        ret.update(
-            {
-                "schedule": {
-                    "foo": {"bar": "baz", "enabled": True},
-                    "hello": {"world": "peace", "enabled": True},
-                },
-                "pillar": {},
-            }
-        )
-        self.schedule.opts.update(
-            {"schedule": {"hello": {"world": "peace", "enabled": True}}, "pillar": {}}
-        )
-        Schedule.add_job(self.schedule, data)
-        self.assertEqual(self.schedule.opts, ret)
-
-    # enable_job tests
-
-    @pytest.mark.slow_test
-    def test_enable_job(self):
-        """
-        Tests enabling a job
-        """
-        self.schedule.opts.update({"schedule": {"name": {"enabled": "foo"}}})
-        Schedule.enable_job(self.schedule, "name")
-        self.assertTrue(self.schedule.opts["schedule"]["name"]["enabled"])
-
-    @pytest.mark.slow_test
-    def test_enable_job_pillar(self):
-        """
-        Tests ignoring enable a job from pillar
-        """
-        self.schedule.opts.update(
-            {"pillar": {"schedule": {"name": {"enabled": False}}}}
-        )
-        Schedule.enable_job(self.schedule, "name", persist=False)
-        self.assertFalse(self.schedule.opts["pillar"]["schedule"]["name"]["enabled"])
-
-    # disable_job tests
-
-    @pytest.mark.slow_test
-    def test_disable_job(self):
-        """
-        Tests disabling a job
-        """
-        self.schedule.opts.update(
-            {"schedule": {"name": {"enabled": "foo"}}, "pillar": {}}
-        )
-        Schedule.disable_job(self.schedule, "name")
-        self.assertFalse(self.schedule.opts["schedule"]["name"]["enabled"])
-
-    @pytest.mark.slow_test
-    def test_disable_job_pillar(self):
-        """
-        Tests ignoring disable a job in pillar
-        """
-        self.schedule.opts.update(
-            {"pillar": {"schedule": {"name": {"enabled": True}}}, "schedule": {}}
-        )
-        Schedule.disable_job(self.schedule, "name", persist=False)
-        self.assertTrue(self.schedule.opts["pillar"]["schedule"]["name"]["enabled"])
-
-    # modify_job tests
-
-    @pytest.mark.slow_test
-    def test_modify_job(self):
-        """
-        Tests modifying a job in the scheduler
-        """
-        schedule = {"foo": "bar"}
-        self.schedule.opts.update({"schedule": {"name": "baz"}, "pillar": {}})
-        ret = copy.deepcopy(self.schedule.opts)
-        ret.update({"schedule": {"name": {"foo": "bar"}}})
-        Schedule.modify_job(self.schedule, "name", schedule)
-        self.assertEqual(self.schedule.opts, ret)
-
-    def test_modify_job_not_exists(self):
-        """
-        Tests modifying a job in the scheduler if jobs not exists
-        """
-        schedule = {"foo": "bar"}
-        self.schedule.opts.update({"schedule": {}, "pillar": {}})
-        ret = copy.deepcopy(self.schedule.opts)
-        ret.update({"schedule": {"name": {"foo": "bar"}}})
-        Schedule.modify_job(self.schedule, "name", schedule)
-        self.assertEqual(self.schedule.opts, ret)
-
-    def test_modify_job_pillar(self):
-        """
-        Tests ignoring modification of job from pillar
-        """
-        schedule = {"foo": "bar"}
-        self.schedule.opts.update(
-            {"schedule": {}, "pillar": {"schedule": {"name": "baz"}}}
-        )
-        ret = copy.deepcopy(self.schedule.opts)
-        Schedule.modify_job(self.schedule, "name", schedule, persist=False)
-        self.assertEqual(self.schedule.opts, ret)
-
-    maxDiff = None
-
-    # enable_schedule tests
-
-    @pytest.mark.slow_test
-    def test_enable_schedule(self):
-        """
-        Tests enabling the scheduler
-        """
-        with patch(
-            "salt.utils.schedule.Schedule.persist", MagicMock(return_value=None)
-        ) as persist_mock:
-            self.schedule.opts.update({"schedule": {"enabled": "foo"}, "pillar": {}})
-            Schedule.enable_schedule(self.schedule)
-            self.assertTrue(self.schedule.opts["schedule"]["enabled"])
-
-        persist_mock.assert_called()
-
-    # disable_schedule tests
-
-    @pytest.mark.slow_test
-    def test_disable_schedule(self):
-        """
-        Tests disabling the scheduler
-        """
-        with patch(
-            "salt.utils.schedule.Schedule.persist", MagicMock(return_value=None)
-        ) as persist_mock:
-            self.schedule.opts.update({"schedule": {"enabled": "foo"}, "pillar": {}})
-            Schedule.disable_schedule(self.schedule)
-            self.assertFalse(self.schedule.opts["schedule"]["enabled"])
-
-        persist_mock.assert_called()
-
-    # reload tests
-
-    def test_reload_update_schedule_key(self):
-        """
-        Tests reloading the schedule from saved schedule where both the
-        saved schedule and self.schedule.opts contain a schedule key
-        """
-        saved = {"schedule": {"foo": "bar"}}
-        ret = copy.deepcopy(self.schedule.opts)
-        ret.update({"schedule": {"foo": "bar", "hello": "world"}})
-        self.schedule.opts.update({"schedule": {"hello": "world"}})
-        Schedule.reload(self.schedule, saved)
-        self.assertEqual(self.schedule.opts, ret)
-
-    def test_reload_update_schedule_no_key(self):
-        """
-        Tests reloading the schedule from saved schedule that does not
-        contain a schedule key but self.schedule.opts does
-        """
-        saved = {"foo": "bar"}
-        ret = copy.deepcopy(self.schedule.opts)
-        ret.update({"schedule": {"foo": "bar", "hello": "world"}})
-        self.schedule.opts.update({"schedule": {"hello": "world"}})
-        Schedule.reload(self.schedule, saved)
-        self.assertEqual(self.schedule.opts, ret)
-
-    def test_reload_no_schedule_in_opts(self):
-        """
-        Tests reloading the schedule from saved schedule that does not
-        contain a schedule key and neither does self.schedule.opts
-        """
-        saved = {"foo": "bar"}
-        ret = copy.deepcopy(self.schedule.opts)
-        ret["schedule"] = {"foo": "bar"}
-        self.schedule.opts.pop("schedule", None)
-        Schedule.reload(self.schedule, saved)
-        self.assertEqual(self.schedule.opts, ret)
-
-    def test_reload_schedule_in_saved_but_not_opts(self):
-        """
-        Tests reloading the schedule from saved schedule that contains
-        a schedule key, but self.schedule.opts does not
-        """
-        saved = {"schedule": {"foo": "bar"}}
-        ret = copy.deepcopy(self.schedule.opts)
-        ret["schedule"] = {"foo": "bar"}
-        self.schedule.opts.pop("schedule", None)
-        Schedule.reload(self.schedule, saved)
-        self.assertEqual(self.schedule.opts, ret)
-
-    # eval tests
-
-    def test_eval_schedule_is_not_dict(self):
-        """
-        Tests eval if the schedule is not a dictionary
-        """
-        self.schedule.opts.update({"schedule": "", "pillar": {"schedule": {}}})
-        self.assertRaises(ValueError, Schedule.eval, self.schedule)
-
-    def test_eval_schedule_is_not_dict_in_pillar(self):
-        """
-        Tests eval if the schedule from pillar is not a dictionary
-        """
-        self.schedule.opts.update({"schedule": {}, "pillar": {"schedule": ""}})
-        self.assertRaises(ValueError, Schedule.eval, self.schedule)
-
-    def test_eval_schedule_time(self):
-        """
-        Tests eval if the schedule setting time is in the future
-        """
-        self.schedule.opts.update({"pillar": {"schedule": {}}})
-        self.schedule.opts.update(
-            {"schedule": {"testjob": {"function": "test.true", "seconds": 60}}}
-        )
-        now = datetime.datetime.now()
-        self.schedule.eval()
-        self.assertTrue(
-            self.schedule.opts["schedule"]["testjob"]["_next_fire_time"] > now
-        )
-
-    def test_eval_schedule_time_eval(self):
-        """
-        Tests eval if the schedule setting time is in the future plus splay
-        """
-        self.schedule.opts.update({"pillar": {"schedule": {}}})
-        self.schedule.opts.update(
-            {
-                "schedule": {
-                    "testjob": {"function": "test.true", "seconds": 60, "splay": 5}
-                }
-            }
-        )
-        now = datetime.datetime.now()
-        self.schedule.eval()
-        self.assertTrue(
-            self.schedule.opts["schedule"]["testjob"]["_splay"] - now
-            > datetime.timedelta(seconds=60)
-        )
-
-    @skipIf(not _CRON_SUPPORTED, "croniter module not installed")
-    def test_eval_schedule_cron(self):
-        """
-        Tests eval if the schedule is defined with cron expression
-        """
-        self.schedule.opts.update({"pillar": {"schedule": {}}})
-        self.schedule.opts.update(
-            {"schedule": {"testjob": {"function": "test.true", "cron": "* * * * *"}}}
-        )
-        now = datetime.datetime.now()
-        self.schedule.eval()
-        self.assertTrue(
-            self.schedule.opts["schedule"]["testjob"]["_next_fire_time"] > now
-        )
-
-    @skipIf(not _CRON_SUPPORTED, "croniter module not installed")
-    def test_eval_schedule_cron_splay(self):
-        """
-        Tests eval if the schedule is defined with cron expression plus splay
-        """
-        self.schedule.opts.update({"pillar": {"schedule": {}}})
-        self.schedule.opts.update(
-            {
-                "schedule": {
-                    "testjob": {
-                        "function": "test.true",
-                        "cron": "* * * * *",
-                        "splay": 5,
-                    }
-                }
-            }
-        )
-        self.schedule.eval()
-        self.assertTrue(
-            self.schedule.opts["schedule"]["testjob"]["_splay"]
-            > self.schedule.opts["schedule"]["testjob"]["_next_fire_time"]
-        )
-
-    @pytest.mark.slow_test
-    def test_handle_func_schedule_minion_blackout(self):
-        """
-        Tests eval if the schedule from pillar is not a dictionary
-        """
-        self.schedule.opts.update({"pillar": {"schedule": {}}})
-        self.schedule.opts.update({"grains": {"minion_blackout": True}})
-
-        self.schedule.opts.update(
-            {"schedule": {"testjob": {"function": "test.true", "seconds": 60}}}
-        )
-        data = {
-            "function": "test.true",
-            "_next_scheduled_fire_time": datetime.datetime(
-                2018, 11, 21, 14, 9, 53, 903438
-            ),
-            "run": True,
-            "name": "testjob",
-            "seconds": 60,
-            "_splay": None,
-            "_seconds": 60,
-            "jid_include": True,
-            "maxrunning": 1,
-            "_next_fire_time": datetime.datetime(2018, 11, 21, 14, 8, 53, 903438),
-        }
-
-        with patch.object(salt.utils.schedule, "log") as log_mock:
-            with patch("salt.utils.process.daemonize"), patch("sys.platform", "linux2"):
-                self.schedule.handle_func(False, "test.ping", data)
-                self.assertTrue(log_mock.exception.called)
-
-    def test_handle_func_check_data(self):
-        """
-        Tests handle_func to ensure that __pub_fun_args is not
-        being duplicated in the value of kwargs in data.
-        """
-
-        data = {
-            "function": "test.arg",
-            "_next_scheduled_fire_time": datetime.datetime(
-                2018, 11, 21, 14, 9, 53, 903438
-            ),
-            "run": True,
-            "args": ["arg1", "arg2"],
-            "kwargs": {"key1": "value1", "key2": "value2"},
-            "name": "testjob",
-            "seconds": 60,
-            "_splay": None,
-            "_seconds": 60,
-            "jid_include": True,
-            "maxrunning": 1,
-            "_next_fire_time": datetime.datetime(2018, 11, 21, 14, 8, 53, 903438),
-        }
-
-        with patch("salt.utils.process.daemonize"), patch("sys.platform", "linux2"):
-            with patch.object(self.schedule, "standalone", return_value=True):
-                # run handle_func once
-                self.schedule.handle_func(False, "test.arg", data)
-
-                # run handle_func and ensure __pub_fun_args
-                # is not in kwargs
-                self.schedule.handle_func(False, "test.arg", data)
-
-                self.assertIn("kwargs", data)
-                self.assertNotIn("__pub_fun_args", data["kwargs"])
-
-    # @skipIf(not salt.utils.platform.is_windows(), "Skip on Non-Windows systems")
-    def test_handle_func_check_dicts(self):
-        """
-        Tests that utils, functions, and returners dicts are not
-        empty after handle_func has run on Windows.
-        """
-
-        data = {
-            "function": "test.arg",
-            "_next_scheduled_fire_time": datetime.datetime(
-                2018, 11, 21, 14, 9, 53, 903438
-            ),
-            "run": True,
-            "args": ["arg1", "arg2"],
-            "kwargs": {"key1": "value1", "key2": "value2"},
-            "name": "testjob",
-            "seconds": 60,
-            "_splay": None,
-            "_seconds": 60,
-            "jid_include": True,
-            "maxrunning": 1,
-            "_next_fire_time": datetime.datetime(2018, 11, 21, 14, 8, 53, 903438),
-        }
-
-        with patch("salt.utils.process.daemonize"):
-            with patch.object(self.schedule, "standalone", return_value=True):
-                # simulate what happens before handle_func is called on Windows
-                self.schedule.functions = {}
-                self.schedule.returners = {}
-                self.schedule.utils = {}
-                self.schedule.handle_func(False, "test.arg", data)
-
-                self.assertNotEqual({}, self.schedule.functions)
-                self.assertNotEqual({}, self.schedule.returners)
-                self.assertNotEqual({}, self.schedule.utils)
diff --git a/tests/unit/utils/scheduler/test_skip.py b/tests/unit/utils/scheduler/test_skip.py
deleted file mode 100644
index 2886b12ad848..000000000000
--- a/tests/unit/utils/scheduler/test_skip.py
+++ /dev/null
@@ -1,288 +0,0 @@
-import logging
-
-import pytest
-from tests.support.unit import skipIf
-from tests.unit.utils.scheduler.base import SchedulerTestsBase
-
-try:
-    import dateutil.parser
-
-    HAS_DATEUTIL_PARSER = True
-except ImportError:
-    HAS_DATEUTIL_PARSER = False
-
-log = logging.getLogger(__name__)
-
-
-@skipIf(
-    HAS_DATEUTIL_PARSER is False, "The 'dateutil.parser' library is not available",
-)
-class SchedulerSkipTest(SchedulerTestsBase):
-    """
-    Validate the pkg module
-    """
-
-    def setUp(self):
-        super().setUp()
-        self.schedule.opts["loop_interval"] = 1
-
-    def tearDown(self):
-        self.schedule.reset()
-        super().tearDown()
-
-    @pytest.mark.slow_test
-    def test_skip(self):
-        """
-        verify that scheduled job is skipped at the specified time
-        """
-        job_name = "test_skip"
-        job = {
-            "schedule": {
-                job_name: {
-                    "function": "test.ping",
-                    "when": ["11/29/2017 4pm", "11/29/2017 5pm"],
-                }
-            }
-        }
-
-        # Add job to schedule
-        self.schedule.opts.update(job)
-
-        run_time = dateutil.parser.parse("11/29/2017 4:00pm")
-        self.schedule.skip_job(
-            job_name,
-            {
-                "time": run_time.strftime("%Y-%m-%dT%H:%M:%S"),
-                "time_fmt": "%Y-%m-%dT%H:%M:%S",
-            },
-        )
-
-        # Run 11/29/2017 at 4pm
-        self.schedule.eval(now=run_time)
-        ret = self.schedule.job_status(job_name)
-        self.assertNotIn("_last_run", ret)
-        self.assertEqual(ret["_skip_reason"], "skip_explicit")
-        self.assertEqual(ret["_skipped_time"], run_time)
-
-        # Run 11/29/2017 at 5pm
-        run_time = dateutil.parser.parse("11/29/2017 5:00pm")
-        self.schedule.eval(now=run_time)
-        ret = self.schedule.job_status(job_name)
-        self.assertEqual(ret["_last_run"], run_time)
-
-    @pytest.mark.slow_test
-    def test_skip_during_range(self):
-        """
-        verify that scheduled job is skipped during the specified range
-        """
-        job_name = "test_skip_during_range"
-        job = {
-            "schedule": {
-                job_name: {
-                    "function": "test.ping",
-                    "hours": "1",
-                    "skip_during_range": {
-                        "start": "11/29/2017 2pm",
-                        "end": "11/29/2017 3pm",
-                    },
-                }
-            }
-        }
-
-        # Add job to schedule
-        self.schedule.opts.update(job)
-
-        # eval at 1:30pm to prime.
-        run_time = dateutil.parser.parse("11/29/2017 1:30pm")
-        self.schedule.eval(now=run_time)
-        ret = self.schedule.job_status(job_name)
-
-        # eval at 2:30pm, will not run during range.
-        run_time = dateutil.parser.parse("11/29/2017 2:30pm")
-        self.schedule.eval(now=run_time)
-        ret = self.schedule.job_status(job_name)
-        self.assertNotIn("_last_run", ret)
-        self.assertEqual(ret["_skip_reason"], "in_skip_range")
-        self.assertEqual(ret["_skipped_time"], run_time)
-
-        # eval at 3:30pm, will run.
-        run_time = dateutil.parser.parse("11/29/2017 3:30pm")
-        self.schedule.eval(now=run_time)
-        ret = self.schedule.job_status(job_name)
-        self.assertEqual(ret["_last_run"], run_time)
-
-    def test_skip_during_range_invalid_datestring(self):
-        """
-        verify that scheduled job is not not and returns the right error string
-        """
-        run_time = dateutil.parser.parse("11/29/2017 2:30pm")
-
-        job_name1 = "skip_during_range_invalid_datestring1"
-        job1 = {
-            "schedule": {
-                job_name1: {
-                    "function": "test.ping",
-                    "hours": "1",
-                    "_next_fire_time": run_time,
-                    "skip_during_range": {"start": "25pm", "end": "3pm"},
-                }
-            }
-        }
-
-        job_name2 = "skip_during_range_invalid_datestring2"
-        job2 = {
-            "schedule": {
-                job_name2: {
-                    "function": "test.ping",
-                    "hours": "1",
-                    "_next_fire_time": run_time,
-                    "skip_during_range": {"start": "2pm", "end": "25pm"},
-                }
-            }
-        }
-
-        # Add job1 to schedule
-        self.schedule.opts.update(job1)
-
-        # Eval
-        self.schedule.eval(now=run_time)
-
-        # Check the first job
-        ret = self.schedule.job_status(job_name1)
-        _expected = (
-            "Invalid date string for start in " "skip_during_range. Ignoring " "job {}."
-        ).format(job_name1)
-        self.assertEqual(ret["_error"], _expected)
-
-        # Clear out schedule
-        self.schedule.opts["schedule"] = {}
-
-        # Add job2 to schedule
-        self.schedule.opts.update(job2)
-
-        # Eval
-        self.schedule.eval(now=run_time)
-
-        # Check the second job
-        ret = self.schedule.job_status(job_name2)
-        _expected = (
-            "Invalid date string for end in " "skip_during_range. Ignoring " "job {}."
-        ).format(job_name2)
-        self.assertEqual(ret["_error"], _expected)
-
-    @pytest.mark.slow_test
-    def test_skip_during_range_global(self):
-        """
-        verify that scheduled job is skipped during the specified range
-        """
-        job_name = "skip_during_range_global"
-        job = {
-            "schedule": {
-                "skip_during_range": {
-                    "start": "11/29/2017 2:00pm",
-                    "end": "11/29/2017 3:00pm",
-                },
-                job_name: {"function": "test.ping", "hours": "1"},
-            }
-        }
-
-        # Add job to schedule
-        self.schedule.opts.update(job)
-
-        # eval at 1:30pm to prime.
-        run_time = dateutil.parser.parse("11/29/2017 1:30pm")
-        self.schedule.eval(now=run_time)
-        ret = self.schedule.job_status(job_name)
-
-        # eval at 2:30pm, will not run during range.
-        run_time = dateutil.parser.parse("11/29/2017 2:30pm")
-        self.schedule.eval(now=run_time)
-        ret = self.schedule.job_status(job_name)
-        self.assertNotIn("_last_run", ret)
-        self.assertEqual(ret["_skip_reason"], "in_skip_range")
-        self.assertEqual(ret["_skipped_time"], run_time)
-
-        # eval at 3:30pm, will run.
-        run_time = dateutil.parser.parse("11/29/2017 3:30pm")
-        self.schedule.eval(now=run_time)
-        ret = self.schedule.job_status(job_name)
-        self.assertEqual(ret["_last_run"], run_time)
-
-    @pytest.mark.slow_test
-    def test_run_after_skip_range(self):
-        """
-        verify that scheduled job is skipped during the specified range
-        """
-        job_name = "skip_run_after_skip_range"
-        job = {
-            "schedule": {
-                job_name: {
-                    "function": "test.ping",
-                    "when": "11/29/2017 2:30pm",
-                    "run_after_skip_range": True,
-                    "skip_during_range": {
-                        "start": "11/29/2017 2pm",
-                        "end": "11/29/2017 3pm",
-                    },
-                }
-            }
-        }
-
-        # Add job to schedule
-        self.schedule.opts.update(job)
-
-        # eval at 2:30pm, will not run during range.
-        run_time = dateutil.parser.parse("11/29/2017 2:30pm")
-        self.schedule.eval(now=run_time)
-        ret = self.schedule.job_status(job_name)
-        self.assertNotIn("_last_run", ret)
-        self.assertEqual(ret["_skip_reason"], "in_skip_range")
-        self.assertEqual(ret["_skipped_time"], run_time)
-
-        # eval at 3:00:01pm, will run.
-        run_time = dateutil.parser.parse("11/29/2017 3:00:01pm")
-        self.schedule.eval(now=run_time)
-        ret = self.schedule.job_status(job_name)
-        self.assertEqual(ret["_last_run"], run_time)
-
-    @pytest.mark.slow_test
-    def test_run_seconds_skip(self):
-        """
-        verify that scheduled job is skipped during the specified range
-        """
-        job_name = "run_seconds_skip"
-        job = {"schedule": {job_name: {"function": "test.ping", "seconds": "10"}}}
-
-        # Add job to schedule
-        self.schedule.opts.update(job)
-
-        # eval at 2:00pm, to prime the scheduler
-        run_time = dateutil.parser.parse("11/29/2017 2:00pm")
-        self.schedule.eval(now=run_time)
-        ret = self.schedule.job_status(job_name)
-
-        # eval at 2:00:10pm
-        run_time = dateutil.parser.parse("11/29/2017 2:00:10pm")
-        self.schedule.eval(now=run_time)
-        ret = self.schedule.job_status(job_name)
-
-        # Skip at 2:00:20pm
-        run_time = dateutil.parser.parse("11/29/2017 2:00:20pm")
-        self.schedule.skip_job(
-            job_name,
-            {
-                "time": run_time.strftime("%Y-%m-%dT%H:%M:%S"),
-                "time_fmt": "%Y-%m-%dT%H:%M:%S",
-            },
-        )
-        self.schedule.eval(now=run_time)
-        ret = self.schedule.job_status(job_name)
-        self.assertIn("_next_fire_time", ret)
-        self.assertEqual(ret["_skip_reason"], "skip_explicit")
-        self.assertEqual(ret["_skipped_time"], run_time)
-
-        # Run at 2:00:30pm
-        run_time = dateutil.parser.parse("11/29/2017 2:00:30pm")
-        self.schedule.eval(now=run_time)
-        ret = self.schedule.job_status(job_name)
-        self.assertIn("_last_run", ret)


Code review comments:
diff_hunk: @@ -553,25 +556,30 @@ def modify(name, **kwargs):
     if "function" not in kwargs:
         kwargs["function"] = _current.get("function")
 
+    # Remove the auto generated _seconds value
     if "_seconds" in _current:
         _current["seconds"] = _current["_seconds"]
         del _current["_seconds"]
 
-    _new = build_schedule_item(name, **kwargs)
+    # Copy _current _new, then update values from kwargs
+    _new = pycopy.deepcopy(_current)
+    _new.update(kwargs)
+
+    # Remove test from kwargs, it's not a valid schedule option
+    if "test" in _new:
+        del _new["test"]
path: salt/modules/schedule.py
text: @user1:
```suggestion
    _new.pop("test", None)
```

diff_hunk: @@ -0,0 +1,650 @@
+"""
+    :codeauthor: Jayesh Kariya <jayeshk@saltstack.com>
+"""
+
+import logging
+import os
+
+import pytest
+import salt.modules.schedule as schedule
+import salt.utils.odict
+from salt.utils.event import SaltEvent
+from tests.support.mock import MagicMock, patch
+
+log = logging.getLogger(__name__)
+
+JOB1 = {
+    "function": "test.ping",
+    "maxrunning": 1,
+    "name": "job1",
+    "jid_include": True,
+    "enabled": True,
+}
+
+
+@pytest.fixture
+def sock_dir():
+    with pytest.helpers.temp_directory() as tempdir:
+        sock_dir = os.path.join(tempdir, "test-socks")
+        return sock_dir
path: tests/pytests/unit/modules/test_schedule.py
text: @user1:
```suggestion
def sock_dir(tmp_path):
    return str(tmp_path, "test-socks")
```

diff_hunk: @@ -0,0 +1,220 @@
+import logging
+
+import pytest
+from tests.support.mock import MagicMock, patch
+
+try:
+    import dateutil.parser
+
+    HAS_DATEUTIL_PARSER = True
+except ImportError:
+    HAS_DATEUTIL_PARSER = False
+
+try:
+    import croniter  # pylint: disable=unused-import
+
+    HAS_CRONITER = True
+except ImportError:
+    HAS_CRONITER = False
+
+log = logging.getLogger(__name__)
+
+pytestmark = pytest.mark.skipif(
+    HAS_DATEUTIL_PARSER is False,
+    reason="The 'dateutil.parser' library is not available",
+)
path: tests/pytests/unit/utils/scheduler/test_error.py
text: @user1:
```suggestion
pytestmark = [
    pytest.mark.skipif(
        HAS_DATEUTIL_PARSER is False,
        reason="The 'dateutil.parser' library is not available",
    ),
]
```

diff_hunk: @@ -0,0 +1,32 @@
+import logging
+
+import pytest
+
+log = logging.getLogger(__name__)
+
+
+@pytest.fixture()
+def loop_interval(schedule):
path: tests/pytests/unit/utils/scheduler/test_helpers.py
text: @user1:
Are you using this fixture at all?

diff_hunk: @@ -950,7 +950,7 @@ def eval(self, now=None):
 
         """
 
-        log.trace("==== evaluating schedule now %s =====", now)
+        log.warning("==== evaluating schedule now %s =====", now)
path: salt/utils/schedule.py
text: @user1:
Back to `trace`?

@author:
I changed this for debugging test runs in Jenkins where jobs were failing but not locally.  Once tests are passing again, I'll change it back.

@user1:
:+1:

diff_hunk: @@ -0,0 +1,508 @@
+"""
+    :codeauthor: Nicole Thomas <nicole@saltstack.com>
+"""
+
+import copy
+import datetime
+import logging
+
+import pytest
+import salt.config
+import salt.utils.schedule
+from salt.utils.schedule import Schedule
+from tests.support.mock import MagicMock, patch
+
+# pylint: disable=import-error,unused-import
+try:
+    import croniter
+
+    _CRON_SUPPORTED = True
+except ImportError:
+    _CRON_SUPPORTED = False
+# pylint: enable=import-error
+
+log = logging.getLogger(__name__)
+
+
+# pylint: disable=too-many-public-methods,invalid-name
+# delete_job tests
+@pytest.mark.slow_test
+def test_delete_job_exists(setup_teardown_vars):
+    """
+    Tests ensuring the job exists and deleting it
+    """
+    schedule = setup_teardown_vars["schedule"]
+    schedule.opts.update({"schedule": {"foo": "bar"}, "pillar": {}})
+    assert "foo" in schedule.opts["schedule"]
+
+    schedule.delete_job("foo")
+    assert "foo" not in schedule.opts["schedule"]
+
+
+@pytest.mark.slow_test
+def test_delete_job_in_pillar(setup_teardown_vars):
+    """
+    Tests ignoring deletion job from pillar
+    """
+    schedule = setup_teardown_vars["schedule"]
+    schedule.opts.update({"pillar": {"schedule": {"foo": "bar"}}, "schedule": {}})
+    assert "foo" in schedule.opts["pillar"]["schedule"]
+    schedule.delete_job("foo")
+
+    assert "foo" in schedule.opts["pillar"]["schedule"]
+
+
+@pytest.mark.slow_test
+def test_delete_job_intervals(setup_teardown_vars):
+    """
+    Tests removing job from intervals
+    """
+    schedule = setup_teardown_vars["schedule"]
+    schedule.opts.update({"pillar": {}, "schedule": {}})
+    schedule.intervals = {"foo": "bar"}
+    schedule.delete_job("foo")
+    assert "foo" not in schedule.intervals
+
+
+@pytest.mark.slow_test
+def test_delete_job_prefix(setup_teardown_vars):
+    """
+    Tests ensuring jobs exists and deleting them by prefix
+    """
+    schedule = setup_teardown_vars["schedule"]
+    schedule.opts.update(
+        {"schedule": {"foobar": "bar", "foobaz": "baz", "fooboo": "boo"}, "pillar": {}}
+    )
+    ret = copy.deepcopy(schedule.opts)
+    del ret["schedule"]["foobar"]
+    del ret["schedule"]["foobaz"]
+    schedule.delete_job_prefix("fooba")
+    assert schedule.opts == ret
+
+
+@pytest.mark.slow_test
+def test_delete_job_prefix_in_pillar(setup_teardown_vars):
+    """
+    Tests ignoring deletion jobs by prefix from pillar
+    """
+    schedule = setup_teardown_vars["schedule"]
+    schedule.opts.update(
+        {
+            "pillar": {"schedule": {"foobar": "bar", "foobaz": "baz", "fooboo": "boo"}},
+            "schedule": {},
+        }
+    )
+    ret = copy.deepcopy(schedule.opts)
+    schedule.delete_job_prefix("fooba")
+    assert schedule.opts == ret
+
+
+# add_job tests
+def test_add_job_data_not_dict(setup_teardown_vars):
+    """
+    Tests if data is a dictionary
+    """
+    schedule = setup_teardown_vars["schedule"]
+    data = "foo"
+    pytest.raises(ValueError, Schedule.add_job, schedule, data)
+
+
+def test_add_job_multiple_jobs(setup_teardown_vars):
+    """
+    Tests if more than one job is scheduled at a time
+    """
+    schedule = setup_teardown_vars["schedule"]
+    data = {"key1": "value1", "key2": "value2"}
+    pytest.raises(ValueError, Schedule.add_job, schedule, data)
+
+
+@pytest.mark.slow_test
+def test_add_job(setup_teardown_vars):
+    """
+    Tests adding a job to the schedule
+    """
+    schedule = setup_teardown_vars["schedule"]
+    data = {"foo": {"bar": "baz"}}
+    ret = copy.deepcopy(schedule.opts)
+    ret.update(
+        {
+            "schedule": {
+                "foo": {"bar": "baz", "enabled": True},
+                "hello": {"world": "peace", "enabled": True},
+            },
+            "pillar": {},
+        }
+    )
+    schedule.opts.update(
+        {"schedule": {"hello": {"world": "peace", "enabled": True}}, "pillar": {}}
+    )
+    Schedule.add_job(schedule, data)
+    assert schedule.opts == ret
+
+
+# enable_job tests
+@pytest.mark.slow_test
+def test_enable_job(setup_teardown_vars):
+    """
+    Tests enabling a job
+    """
+    schedule = setup_teardown_vars["schedule"]
+    schedule.opts.update({"schedule": {"name": {"enabled": "foo"}}})
+    Schedule.enable_job(schedule, "name")
+    assert schedule.opts["schedule"]["name"]["enabled"]
+
+
+@pytest.mark.slow_test
+def test_enable_job_pillar(setup_teardown_vars):
+    """
+    Tests ignoring enable a job from pillar
+    """
+    schedule = setup_teardown_vars["schedule"]
+    schedule.opts.update({"pillar": {"schedule": {"name": {"enabled": False}}}})
+    Schedule.enable_job(schedule, "name", persist=False)
+    assert not schedule.opts["pillar"]["schedule"]["name"]["enabled"]
+
+
+# disable_job tests
+@pytest.mark.slow_test
+def test_disable_job(setup_teardown_vars):
+    """
+    Tests disabling a job
+    """
+    schedule = setup_teardown_vars["schedule"]
+    schedule.opts.update({"schedule": {"name": {"enabled": "foo"}}, "pillar": {}})
+    Schedule.disable_job(schedule, "name")
+    assert not schedule.opts["schedule"]["name"]["enabled"]
+
+
+@pytest.mark.slow_test
+def test_disable_job_pillar(setup_teardown_vars):
+    """
+    Tests ignoring disable a job in pillar
+    """
+    schedule = setup_teardown_vars["schedule"]
+    schedule.opts.update(
+        {"pillar": {"schedule": {"name": {"enabled": True}}}, "schedule": {}}
+    )
+    Schedule.disable_job(schedule, "name", persist=False)
+    assert schedule.opts["pillar"]["schedule"]["name"]["enabled"]
+
+
+# modify_job tests
+@pytest.mark.slow_test
+def test_modify_job(setup_teardown_vars):
+    """
+    Tests modifying a job in the scheduler
+    """
+    schedule = setup_teardown_vars["schedule"]
+    schedule_dict = {"foo": "bar"}
+    schedule.opts.update({"schedule": {"name": "baz"}, "pillar": {}})
+    ret = copy.deepcopy(schedule.opts)
+    ret.update({"schedule": {"name": {"foo": "bar"}}})
+    Schedule.modify_job(schedule, "name", schedule_dict)
+    assert schedule.opts == ret
+
+
+def test_modify_job_not_exists(setup_teardown_vars):
+    """
+    Tests modifying a job in the scheduler if jobs not exists
+    """
+    schedule = setup_teardown_vars["schedule"]
+    schedule_dict = {"foo": "bar"}
+    schedule.opts.update({"schedule": {}, "pillar": {}})
+    ret = copy.deepcopy(schedule.opts)
+    ret.update({"schedule": {"name": {"foo": "bar"}}})
+    Schedule.modify_job(schedule, "name", schedule_dict)
+    assert schedule.opts == ret
+
+
+def test_modify_job_pillar(setup_teardown_vars):
+    """
+    Tests ignoring modification of job from pillar
+    """
+    schedule = setup_teardown_vars["schedule"]
+    schedule_dict = {"foo": "bar"}
+    schedule.opts.update({"schedule": {}, "pillar": {"schedule": {"name": "baz"}}})
+    ret = copy.deepcopy(schedule.opts)
+    Schedule.modify_job(schedule, "name", schedule_dict, persist=False)
+    assert schedule.opts == ret
+
+
+maxDiff = None
+
+
+# enable_schedule tests
+@pytest.mark.slow_test
+def test_enable_schedule(setup_teardown_vars):
+    """
+    Tests enabling the scheduler
+    """
+    schedule = setup_teardown_vars["schedule"]
+    with patch(
+        "salt.utils.schedule.Schedule.persist", MagicMock(return_value=None)
+    ) as persist_mock:
+        schedule.opts.update({"schedule": {"enabled": "foo"}, "pillar": {}})
+        Schedule.enable_schedule(schedule)
+        assert schedule.opts["schedule"]["enabled"]
+
+    persist_mock.assert_called()
+
+
+# disable_schedule tests
+@pytest.mark.slow_test
+def test_disable_schedule(setup_teardown_vars):
+    """
+    Tests disabling the scheduler
+    """
+    schedule = setup_teardown_vars["schedule"]
+    with patch(
+        "salt.utils.schedule.Schedule.persist", MagicMock(return_value=None)
+    ) as persist_mock:
+        schedule.opts.update({"schedule": {"enabled": "foo"}, "pillar": {}})
+        Schedule.disable_schedule(schedule)
+        assert not schedule.opts["schedule"]["enabled"]
+
+    persist_mock.assert_called()
+
+
+# reload tests
+def test_reload_update_schedule_key(setup_teardown_vars):
+    """
+    Tests reloading the schedule from saved schedule where both the
+    saved schedule and schedule.opts contain a schedule key
+    """
+    schedule = setup_teardown_vars["schedule"]
+    saved = {"schedule": {"foo": "bar"}}
+    ret = copy.deepcopy(schedule.opts)
+    ret.update({"schedule": {"foo": "bar", "hello": "world"}})
+    schedule.opts.update({"schedule": {"hello": "world"}})
+    Schedule.reload(schedule, saved)
+    assert schedule.opts == ret
+
+
+def test_reload_update_schedule_no_key(setup_teardown_vars):
+    """
+    Tests reloading the schedule from saved schedule that does not
+    contain a schedule key but schedule.opts does
+    """
+    schedule = setup_teardown_vars["schedule"]
+    saved = {"foo": "bar"}
+    ret = copy.deepcopy(schedule.opts)
+    ret.update({"schedule": {"foo": "bar", "hello": "world"}})
+    schedule.opts.update({"schedule": {"hello": "world"}})
+    Schedule.reload(schedule, saved)
+    assert schedule.opts == ret
+
+
+def test_reload_no_schedule_in_opts(setup_teardown_vars):
+    """
+    Tests reloading the schedule from saved schedule that does not
+    contain a schedule key and neither does schedule.opts
+    """
+    schedule = setup_teardown_vars["schedule"]
+    saved = {"foo": "bar"}
+    ret = copy.deepcopy(schedule.opts)
+    ret["schedule"] = {"foo": "bar"}
+    schedule.opts.pop("schedule", None)
+    Schedule.reload(schedule, saved)
+    assert schedule.opts == ret
+
+
+def test_reload_schedule_in_saved_but_not_opts(setup_teardown_vars):
+    """
+    Tests reloading the schedule from saved schedule that contains
+    a schedule key, but schedule.opts does not
+    """
+    schedule = setup_teardown_vars["schedule"]
+    saved = {"schedule": {"foo": "bar"}}
+    ret = copy.deepcopy(schedule.opts)
+    ret["schedule"] = {"foo": "bar"}
+    schedule.opts.pop("schedule", None)
+    Schedule.reload(schedule, saved)
+    assert schedule.opts == ret
+
+
+# eval tests
+def test_eval_schedule_is_not_dict(setup_teardown_vars):
+    """
+    Tests eval if the schedule is not a dictionary
+    """
+    schedule = setup_teardown_vars["schedule"]
+    schedule.opts.update({"schedule": "", "pillar": {"schedule": {}}})
+    pytest.raises(ValueError, Schedule.eval, schedule)
+
+
+def test_eval_schedule_is_not_dict_in_pillar(setup_teardown_vars):
+    """
+    Tests eval if the schedule from pillar is not a dictionary
+    """
+    schedule = setup_teardown_vars["schedule"]
+    schedule.opts.update({"schedule": {}, "pillar": {"schedule": ""}})
+    pytest.raises(ValueError, Schedule.eval, schedule)
+
+
+def test_eval_schedule_time(setup_teardown_vars):
+    """
+    Tests eval if the schedule setting time is in the future
+    """
+    schedule = setup_teardown_vars["schedule"]
+    schedule.opts.update({"pillar": {"schedule": {}}})
+    schedule.opts.update(
+        {"schedule": {"testjob": {"function": "test.true", "seconds": 60}}}
+    )
+    now = datetime.datetime.now()
+    schedule.eval()
+    assert schedule.opts["schedule"]["testjob"]["_next_fire_time"] > now
+
+
+def test_eval_schedule_time_eval(setup_teardown_vars):
+    """
+    Tests eval if the schedule setting time is in the future plus splay
+    """
+    schedule = setup_teardown_vars["schedule"]
+    schedule.opts.update({"pillar": {"schedule": {}}})
+    schedule.opts.update(
+        {"schedule": {"testjob": {"function": "test.true", "seconds": 60, "splay": 5}}}
+    )
+    now = datetime.datetime.now()
+    schedule.eval()
+    assert schedule.opts["schedule"]["testjob"]["_splay"] - now > datetime.timedelta(
+        seconds=60
+    )
+
+
+@pytest.mark.skipif(not _CRON_SUPPORTED, reason="croniter module not installed")
+def test_eval_schedule_cron(setup_teardown_vars):
+    """
+    Tests eval if the schedule is defined with cron expression
+    """
+    schedule = setup_teardown_vars["schedule"]
+    schedule.opts.update({"pillar": {"schedule": {}}})
+    schedule.opts.update(
+        {"schedule": {"testjob": {"function": "test.true", "cron": "* * * * *"}}}
+    )
+    now = datetime.datetime.now()
+    schedule.eval()
+    assert schedule.opts["schedule"]["testjob"]["_next_fire_time"] > now
+
+
+@pytest.mark.skipif(not _CRON_SUPPORTED, reason="croniter module not installed")
+def test_eval_schedule_cron_splay(setup_teardown_vars):
+    """
+    Tests eval if the schedule is defined with cron expression plus splay
+    """
+    schedule = setup_teardown_vars["schedule"]
+    schedule.opts.update({"pillar": {"schedule": {}}})
+    schedule.opts.update(
+        {
+            "schedule": {
+                "testjob": {"function": "test.true", "cron": "* * * * *", "splay": 5}
+            }
+        }
+    )
+    schedule.eval()
+    assert (
+        schedule.opts["schedule"]["testjob"]["_splay"]
+        > schedule.opts["schedule"]["testjob"]["_next_fire_time"]
+    )
+
+
+@pytest.mark.slow_test
+def test_handle_func_schedule_minion_blackout(setup_teardown_vars):
+    """
+    Tests eval if the schedule from pillar is not a dictionary
+    """
+    schedule = setup_teardown_vars["schedule"]
+    schedule.opts.update({"pillar": {"schedule": {}}})
+    schedule.opts.update({"grains": {"minion_blackout": True}})
+
+    schedule.opts.update(
+        {"schedule": {"testjob": {"function": "test.true", "seconds": 60}}}
+    )
+    data = {
+        "function": "test.true",
+        "_next_scheduled_fire_time": datetime.datetime(2018, 11, 21, 14, 9, 53, 903438),
+        "run": True,
+        "name": "testjob",
+        "seconds": 60,
+        "_splay": None,
+        "_seconds": 60,
+        "jid_include": True,
+        "maxrunning": 1,
+        "_next_fire_time": datetime.datetime(2018, 11, 21, 14, 8, 53, 903438),
+    }
+
+    with patch.object(salt.utils.schedule, "log") as log_mock:
+        with patch("salt.utils.process.daemonize"), patch("sys.platform", "linux2"):
+            schedule.handle_func(False, "test.ping", data)
+            assert log_mock.exception.called
+
+
+def test_handle_func_check_data(setup_teardown_vars):
+    """
+    Tests handle_func to ensure that __pub_fun_args is not
+    being duplicated in the value of kwargs in data.
+    """
+    schedule = setup_teardown_vars["schedule"]
+
+    data = {
+        "function": "test.arg",
+        "_next_scheduled_fire_time": datetime.datetime(2018, 11, 21, 14, 9, 53, 903438),
+        "run": True,
+        "args": ["arg1", "arg2"],
+        "kwargs": {"key1": "value1", "key2": "value2"},
+        "name": "testjob",
+        "seconds": 60,
+        "_splay": None,
+        "_seconds": 60,
+        "jid_include": True,
+        "maxrunning": 1,
+        "_next_fire_time": datetime.datetime(2018, 11, 21, 14, 8, 53, 903438),
+    }
+
+    with patch("salt.utils.process.daemonize"), patch("sys.platform", "linux2"):
+        with patch.object(schedule, "standalone", return_value=True):
+            # run handle_func once
+            schedule.handle_func(False, "test.arg", data)
+
+            # run handle_func and ensure __pub_fun_args
+            # is not in kwargs
+            schedule.handle_func(False, "test.arg", data)
+
+            assert "kwargs" in data
+            assert "__pub_fun_args" not in data["kwargs"]
+
+
+def test_handle_func_check_dicts(setup_teardown_vars):
+    """
+    Tests that utils, functions, and returners dicts are not
+    empty after handle_func has run on Windows.
+    """
+    schedule = setup_teardown_vars["schedule"]
+
+    data = {
+        "function": "test.arg",
+        "_next_scheduled_fire_time": datetime.datetime(2018, 11, 21, 14, 9, 53, 903438),
+        "run": True,
+        "args": ["arg1", "arg2"],
+        "kwargs": {"key1": "value1", "key2": "value2"},
+        "name": "testjob",
+        "seconds": 60,
+        "_splay": None,
+        "_seconds": 60,
+        "jid_include": True,
+        "maxrunning": 1,
+        "_next_fire_time": datetime.datetime(2018, 11, 21, 14, 8, 53, 903438),
+    }
+
+    with patch("salt.utils.process.daemonize"):
+        with patch.object(schedule, "standalone", return_value=True):
+            # simulate what happens before handle_func is called on Windows
+            schedule.functions = {}
+            schedule.returners = {}
+            schedule.utils = {}
+            schedule.handle_func(False, "test.arg", data)
+
+            assert {} != schedule.functions
+            assert {} != schedule.returners
+            assert {} != schedule.utils
path: tests/pytests/unit/utils/scheduler/test_schedule.py
text: @user1:
```suggestion
            assert schedule.functions != {}
            assert schedule.returners != {}
            assert schedule.utils != {}
```
assert what you get against what you expect.
In case of errors, the diff by pytest will make more sense.

diff_hunk: @@ -0,0 +1,650 @@
+"""
+    :codeauthor: Jayesh Kariya <jayeshk@saltstack.com>
+"""
+
+import logging
+import os
+
+import pytest
+import salt.modules.schedule as schedule
+import salt.utils.odict
+from salt.utils.event import SaltEvent
+from tests.support.mock import MagicMock, patch
+
+log = logging.getLogger(__name__)
+
+JOB1 = {
+    "function": "test.ping",
+    "maxrunning": 1,
+    "name": "job1",
+    "jid_include": True,
+    "enabled": True,
+}
path: tests/pytests/unit/modules/test_schedule.py
text: @user2:
```suggestion
@user1.fixture
def job1():
    return {
        "function": "test.ping",
        "maxrunning": 1,
        "name": "job1",
        "jid_include": True,
        "enabled": True,
    }
```