Repository: quantum-elixir/quantum-core
Pull request #644 (2025-07-17 15:51:08)

Use crontab DateTime (TZ) Support


Patch:
diff --git a/lib/quantum/clock_broadcaster.ex b/lib/quantum/clock_broadcaster.ex
index d84e134..934559a 100644
--- a/lib/quantum/clock_broadcaster.ex
+++ b/lib/quantum/clock_broadcaster.ex
@@ -44,11 +44,11 @@ defmodule Quantum.ClockBroadcaster do
       |> storage.last_execution_date()
       |> case do
         :unknown -> start_time
-        date -> date
+        date -> DateTime.from_naive!(date, "Etc/UTC")
       end
-      |> NaiveDateTime.truncate(:second)
+      |> DateTime.truncate(:second)
       # Roll back one second since handle_tick will start at `now + 1`.
-      |> NaiveDateTime.add(-1, :second)
+      |> DateTime.add(-1, :second)
 
     :timer.send_interval(1000, :tick)
 
@@ -80,16 +80,16 @@ defmodule Quantum.ClockBroadcaster do
 
   defp handle_tick(%State{remaining_demand: remaining_demand, time: time} = state)
        when remaining_demand > 0 do
-    now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
+    now = DateTime.truncate(DateTime.utc_now(), :second)
 
     {events, new_time} =
       Enum.reduce_while(
         1..remaining_demand,
         {[], time},
         fn _, {list, time} = acc ->
-          new_time = NaiveDateTime.add(time, 1, :second)
+          new_time = DateTime.add(time, 1, :second)
 
-          case NaiveDateTime.compare(new_time, now) do
+          case DateTime.compare(new_time, now) do
             :lt ->
               {:cont, {[%Event{time: new_time, catch_up: true} | list], new_time}}
 
diff --git a/lib/quantum/clock_broadcaster/event.ex b/lib/quantum/clock_broadcaster/event.ex
index e16a2d9..49878d8 100644
--- a/lib/quantum/clock_broadcaster/event.ex
+++ b/lib/quantum/clock_broadcaster/event.ex
@@ -4,7 +4,7 @@ defmodule Quantum.ClockBroadcaster.Event do
   # Clock Event
 
   @type t :: %__MODULE__{
-          time: NaiveDateTime.t(),
+          time: DateTime.t(),
           catch_up: boolean()
         }
 
diff --git a/lib/quantum/clock_broadcaster/init_opts.ex b/lib/quantum/clock_broadcaster/init_opts.ex
index 66629de..8078f51 100644
--- a/lib/quantum/clock_broadcaster/init_opts.ex
+++ b/lib/quantum/clock_broadcaster/init_opts.ex
@@ -6,7 +6,7 @@ defmodule Quantum.ClockBroadcaster.InitOpts do
   alias Quantum.{Scheduler, Storage}
 
   @type t :: %__MODULE__{
-          start_time: NaiveDateTime.t(),
+          start_time: DateTime.t(),
           storage: Storage,
           scheduler: Scheduler,
           debug_logging: boolean()
diff --git a/lib/quantum/clock_broadcaster/start_opts.ex b/lib/quantum/clock_broadcaster/start_opts.ex
index b6150a0..0382d03 100644
--- a/lib/quantum/clock_broadcaster/start_opts.ex
+++ b/lib/quantum/clock_broadcaster/start_opts.ex
@@ -7,7 +7,7 @@ defmodule Quantum.ClockBroadcaster.StartOpts do
 
   @type t :: %__MODULE__{
           name: GenServer.server(),
-          start_time: NaiveDateTime.t(),
+          start_time: DateTime.t(),
           storage: Storage,
           scheduler: Scheduler,
           debug_logging: boolean()
diff --git a/lib/quantum/clock_broadcaster/state.ex b/lib/quantum/clock_broadcaster/state.ex
index 7ec1574..0f49306 100644
--- a/lib/quantum/clock_broadcaster/state.ex
+++ b/lib/quantum/clock_broadcaster/state.ex
@@ -5,7 +5,7 @@ defmodule Quantum.ClockBroadcaster.State do
 
   @type t :: %__MODULE__{
           debug_logging: boolean(),
-          time: NaiveDateTime.t(),
+          time: DateTime.t(),
           remaining_demand: non_neg_integer
         }
 
diff --git a/lib/quantum/date_library.ex b/lib/quantum/date_library.ex
deleted file mode 100644
index cdff2e5..0000000
--- a/lib/quantum/date_library.ex
+++ /dev/null
@@ -1,80 +0,0 @@
-defmodule Quantum.DateLibrary do
-  @moduledoc false
-
-  require Logger
-
-  alias Quantum.DateLibrary.{InvalidDateTimeForTimezoneError, InvalidTimezoneError}
-
-  # Convert Date to Utc
-  @spec to_utc!(NaiveDateTime.t(), :utc | String.t()) :: NaiveDateTime.t()
-
-  def to_utc!(date, :utc), do: date
-
-  def to_utc!(date, tz) when is_binary(tz) do
-    dt =
-      case DateTime.from_naive(date, tz) do
-        {:ok, dt} ->
-          dt
-
-        {:ambiguous, _, _} ->
-          raise InvalidDateTimeForTimezoneError
-
-        {:gap, _, _} ->
-          raise InvalidDateTimeForTimezoneError
-
-        {:error, :incompatible_calendars} ->
-          raise InvalidDateTimeForTimezoneError
-
-        {:error, :time_zone_not_found} ->
-          raise InvalidTimezoneError
-
-        {:error, :utc_only_time_zone_database} ->
-          Logger.warning("Timezone database not set up")
-          raise InvalidTimezoneError
-      end
-
-    case DateTime.shift_zone(dt, "Etc/UTC") do
-      {:ok, dt} ->
-        DateTime.to_naive(dt)
-
-      {:error, :utc_only_time_zone_database} ->
-        Logger.warning("Timezone database not set up")
-        raise InvalidTimezoneError
-    end
-  end
-
-  # Convert Date to TZ
-  @spec to_tz!(NaiveDateTime.t(), :utc | String.t()) :: NaiveDateTime.t()
-  def to_tz!(date, :utc), do: date
-
-  def to_tz!(date, tz) when is_binary(tz) do
-    result =
-      date
-      |> DateTime.from_naive!("Etc/UTC")
-      |> DateTime.shift_zone(tz)
-
-    case result do
-      {:ok, dt} ->
-        DateTime.to_naive(dt)
-
-      {:error, :time_zone_not_found} ->
-        raise InvalidTimezoneError
-
-      {:error, :utc_only_time_zone_database} ->
-        Logger.warning("Timezone database not set up")
-        raise InvalidTimezoneError
-    end
-  end
-
-  defmodule InvalidDateTimeForTimezoneError do
-    @moduledoc false
-
-    defexception message: "The requested time does not exist in the given timezone."
-  end
-
-  defmodule InvalidTimezoneError do
-    @moduledoc false
-
-    defexception message: "The requested timezone is invalid."
-  end
-end
diff --git a/lib/quantum/execution_broadcaster.ex b/lib/quantum/execution_broadcaster.ex
index 4a01954..3f4a5d9 100644
--- a/lib/quantum/execution_broadcaster.ex
+++ b/lib/quantum/execution_broadcaster.ex
@@ -12,12 +12,6 @@ defmodule Quantum.ExecutionBroadcaster do
 
   alias Quantum.ClockBroadcaster.Event, as: ClockEvent
 
-  alias Quantum.{
-    DateLibrary,
-    DateLibrary.InvalidDateTimeForTimezoneError,
-    DateLibrary.InvalidTimezoneError
-  }
-
   alias Quantum.ExecutionBroadcaster.Event, as: ExecuteEvent
   alias Quantum.ExecutionBroadcaster.InitOpts
   alias Quantum.ExecutionBroadcaster.State
@@ -202,7 +196,7 @@ defmodule Quantum.ExecutionBroadcaster do
          } = state,
          time
        ) do
-    case NaiveDateTime.compare(time, time_to_execute) do
+    case DateTime.compare(time, time_to_execute) do
       :gt ->
         raise "Jobs were skipped"
 
@@ -226,7 +220,7 @@ defmodule Quantum.ExecutionBroadcaster do
           jobs
           |> Enum.reduce(
             %{state | execution_timeline: tail},
-            &add_job_to_state(&1, &2, NaiveDateTime.add(time, 1, :second))
+            &add_job_to_state(&1, &2, DateTime.add(time, 1, :second))
           )
           |> sort_state
           |> execute_events_to_fire(time)
@@ -240,11 +234,17 @@ defmodule Quantum.ExecutionBroadcaster do
          state,
          time
        ) do
-    job
-    |> get_next_execution_time(time)
-    |> case do
-      {:ok, date} ->
-        add_to_state(state, time, date, job)
+    with {:ok, execution_date} <- get_next_execution_time(job, time) do
+      add_to_state(state, time, execution_date, job)
+    else
+      {:error, :time_zone_not_found} ->
+        Logger.error(
+          "Invalid Timezone #{inspect(timezone)} provided for job #{inspect(name)}.",
+          job: job,
+          error: :time_zone_not_found
+        )
+
+        state
 
       {:error, _} ->
         Logger.warning(fn ->
@@ -256,54 +256,32 @@ defmodule Quantum.ExecutionBroadcaster do
 
         state
     end
-  rescue
-    e in InvalidTimezoneError ->
-      Logger.error(
-        "Invalid Timezone #{inspect(timezone)} provided for job #{inspect(name)}.",
-        job: job,
-        error: e
-      )
-
-      state
   end
 
   defp get_next_execution_time(
-         %Job{schedule: schedule, timezone: timezone, name: name} = job,
+         %Job{schedule: schedule, timezone: :utc},
          time
        ) do
-    schedule
-    |> CrontabScheduler.get_next_run_date(DateLibrary.to_tz!(time, timezone))
-    |> case do
-      {:ok, date} ->
-        {:ok, DateLibrary.to_utc!(date, timezone)}
+    CrontabScheduler.get_next_run_date(schedule, time)
+  end
 
-      {:error, _} = error ->
-        error
+  defp get_next_execution_time(
+         %Job{schedule: schedule, timezone: timezone},
+         time
+       ) do
+    with {:ok, localized_time} <- DateTime.shift_zone(time, timezone),
+         {:ok, localized_execution_time} <-
+           CrontabScheduler.get_next_run_date(schedule, localized_time) do
+      DateTime.shift_zone(localized_execution_time, "Etc/UTC")
     end
-  rescue
-    _ in InvalidDateTimeForTimezoneError ->
-      next_time = NaiveDateTime.add(time, 60, :second)
-
-      Logger.warning(fn ->
-        """
-        Next execution time for job #{inspect(name)} is not a valid time.
-        Retrying with #{inspect(next_time)}
-        """
-      end)
-
-      get_next_execution_time(job, next_time)
   end
 
   defp sort_state(%State{execution_timeline: execution_timeline} = state) do
-    %{
-      state
-      | execution_timeline:
-          Enum.sort_by(execution_timeline, fn {date, _} -> NaiveDateTime.to_erl(date) end)
-    }
+    %{state | execution_timeline: Enum.sort_by(execution_timeline, &elem(&1, 0), DateTime)}
   end
 
   defp add_to_state(%State{execution_timeline: execution_timeline} = state, time, date, job) do
-    unless NaiveDateTime.compare(time, date) in [:lt, :eq] do
+    unless DateTime.compare(time, date) in [:lt, :eq] do
       raise Quantum.ExecutionBroadcaster.JobInPastError
     end
 
diff --git a/lib/quantum/execution_broadcaster/state.ex b/lib/quantum/execution_broadcaster/state.ex
index 20af38c..ed885b1 100644
--- a/lib/quantum/execution_broadcaster/state.ex
+++ b/lib/quantum/execution_broadcaster/state.ex
@@ -8,7 +8,7 @@ defmodule Quantum.ExecutionBroadcaster.State do
 
   @type t :: %__MODULE__{
           uninitialized_jobs: [Job.t()],
-          execution_timeline: [{NaiveDateTime.t(), [Job.t()]}],
+          execution_timeline: [{DateTime.t(), [Job.t()]}],
           storage: StorageAdapter,
           storage_pid: StorageAdapter.storage_pid(),
           scheduler: Quantum,
diff --git a/lib/quantum/storage.ex b/lib/quantum/storage.ex
index f88e9af..238d1b3 100644
--- a/lib/quantum/storage.ex
+++ b/lib/quantum/storage.ex
@@ -62,14 +62,14 @@ defmodule Quantum.Storage do
   Returns `:unknown` if the storage does not know the last execution time.
   In this case all jobs will be run at the next applicable date.
   """
-  @callback last_execution_date(storage_pid :: storage_pid) :: :unknown | NaiveDateTime.t()
+  @callback last_execution_date(storage_pid :: storage_pid) :: :unknown | DateTime.t()
 
   @doc """
   Update last execution time to given date.
   """
   @callback update_last_execution_date(
               storage_pid :: storage_pid,
-              last_execution_date :: NaiveDateTime.t()
+              last_execution_date :: DateTime.t()
             ) :: :ok
 
   @doc """
diff --git a/lib/quantum/supervisor.ex b/lib/quantum/supervisor.ex
index eb2a315..7aba015 100644
--- a/lib/quantum/supervisor.ex
+++ b/lib/quantum/supervisor.ex
@@ -46,7 +46,7 @@ defmodule Quantum.Supervisor do
         opts
         |> Map.take([:debug_logging, :storage, :scheduler])
         |> Map.put(:name, clock_broadcaster_name)
-        |> Map.put(:start_time, NaiveDateTime.utc_now())
+        |> Map.put(:start_time, DateTime.utc_now())
       )
 
     job_broadcaster_opts =
diff --git a/mix.exs b/mix.exs
index 4b09fd7..89c9624 100644
--- a/mix.exs
+++ b/mix.exs
@@ -13,7 +13,7 @@ defmodule Quantum.Mixfile do
       deps: deps(),
       description: "Cron-like job scheduler for Elixir.",
       docs: docs(),
-      elixir: "~> 1.12",
+      elixir: "~> 1.15",
       name: "Quantum",
       elixirc_paths: elixirc_paths(Mix.env()),
       package: package(),
@@ -100,7 +100,7 @@ defmodule Quantum.Mixfile do
 
   defp deps do
     [
-      {:crontab, "~> 1.1"},
+      {:crontab, "~> 1.2"},
       {:gen_stage, "~> 0.14 or ~> 1.0"},
       {:telemetry, "~> 0.4.3 or ~> 1.0"},
       {:tzdata, "~> 1.0", only: [:dev, :test]},


Test patch:
diff --git a/.github/workflows/part_test.yml b/.github/workflows/part_test.yml
index 2d59419..b82f109 100644
--- a/.github/workflows/part_test.yml
+++ b/.github/workflows/part_test.yml
@@ -7,6 +7,33 @@ env:
   BUILD_EMBEDDED: true
 
 jobs:
+  detectToolVersions:
+    name: "Detect Tool Versions"
+
+    runs-on: ubuntu-latest
+
+    outputs:
+      otpVersion: "${{ steps.toolVersions.outputs.OTP_VERSION }}"
+      elixirVersion: "${{ steps.toolVersions.outputs.ELIXIR_VERSION }}"
+
+    steps:
+      - name: Harden Runner
+        uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
+        with:
+          egress-policy: audit
+
+      - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+      - name: "Read .tool-versions"
+        id: toolVersions
+        run: |
+          OTP_VERSION="$(cat .tool-versions | grep erlang | cut -d' ' -f2-)"
+          echo OTP: $OTP_VERSION
+          echo "OTP_VERSION=${OTP_VERSION}" >> $GITHUB_OUTPUT
+
+          ELIXIR_VERSION="$(cat .tool-versions | grep elixir | cut -d' ' -f2-)"
+          echo Rebar: $ELIXIR_VERSION
+          echo "ELIXIR_VERSION=${ELIXIR_VERSION}" >> $GITHUB_OUTPUT
+
   format:
     name: Check Formatting
 
@@ -39,26 +66,32 @@ jobs:
       - run: mix format --check-formatted
 
   test:
-    name: Run Tests & Submit Coverage
+    name: Run Tests & Submit Coverage (${{ matrix.name }})
 
-    runs-on: ubuntu-latest
+    needs: ["detectToolVersions"]
+
+    runs-on: ${{ matrix.runs-on }}
 
     strategy:
       fail-fast: false
       matrix:
         include:
-          - otp: '24.3'
-            elixir: '1.12'
-          - otp: '25.2'
-            elixir: '1.13'
-          - otp: '25.2'
-            elixir: '1.14'
-          - otp: '26.1'
-            elixir: '1.15'
-          - otp: '26.1'
-            elixir: 'main'
-          - currentMainVersion: true
-            enable_coverage_export: 'true'
+          # Lowest Supported
+          - otp: "24.2"
+            elixir: "1.15"
+            runs-on: ubuntu-22.04
+            name: "lowest"
+          # Latest Supported
+          - otp: "${{ needs.detectToolVersions.outputs.otpVersion }}"
+            elixir: "${{ needs.detectToolVersions.outputs.elixirVersion }}"
+            runs-on: ubuntu-24.04
+            name: "latest"
+            enable_coverage_export: "true"
+          # Test Main
+          - otp: "${{ needs.detectToolVersions.outputs.otpVersion }}"
+            elixir: "main"
+            runs-on: ubuntu-24.04
+            name: "main"
 
     env:
       MIX_ENV: test
@@ -66,30 +99,23 @@ jobs:
     steps:
       - uses: actions/checkout@v4
       - uses: erlef/setup-elixir@v1
-        if: ${{ !matrix.currentMainVersion }}
-        id: setupBEAMDynamic
+        id: setupBEAM
         with:
           otp-version: ${{ matrix.otp }}
           elixir-version: ${{ matrix.elixir }}
-      - uses: erlef/setup-elixir@v1
-        if: ${{ matrix.currentMainVersion }}
-        id: setupBEAMCurrent
-        with:
-          version-file: '.tool-versions'
-          version-type: strict
       - uses: actions/cache@v4
         with:
           path: deps
-          key: deps-${{ runner.os }}-${{ steps.setupBEAMCurrent.outputs.otp-version }}${{ steps.setupBEAMDynamic.outputs.otp-version }}-${{ steps.setupBEAMCurrent.outputs.elixir-version }}${{ steps.setupBEAMDynamic.outputs.elixir-version }}-${{ hashFiles('mix.lock') }}
+          key: deps-${{ runner.os }}-${{ steps.setupBEAM.outputs.otp-version }}-${{ steps.setupBEAM.outputs.elixir-version }}-${{ hashFiles('mix.lock') }}
           restore-keys: |
-            deps-${{ runner.os }}-${{ steps.setupBEAMCurrent.outputs.otp-version }}${{ steps.setupBEAMDynamic.outputs.otp-version }}-${{ steps.setupBEAMCurrent.outputs.elixir-version }}${{ steps.setupBEAMDynamic.outputs.elixir-version }}-
+            deps-${{ runner.os }}-${{ steps.setupBEAM.outputs.otp-version }}-${{ steps.setupBEAM.outputs.elixir-version }}-
       - run: mix deps.get
       - uses: actions/cache@v4
         with:
           path: _build/test
-          key: compile-${{ env.MIX_ENV }}-${{ runner.os }}-${{ steps.setupBEAMCurrent.outputs.otp-version }}${{ steps.setupBEAMDynamic.outputs.otp-version }}-${{ steps.setupBEAMCurrent.outputs.elixir-version }}${{ steps.setupBEAMDynamic.outputs.elixir-version }}-${{ hashFiles('mix.lock') }}
+          key: compile-${{ env.MIX_ENV }}-${{ runner.os }}-${{ steps.setupBEAM.outputs.otp-version }}-${{ steps.setupBEAM.outputs.elixir-version }}-${{ hashFiles('mix.lock') }}
           restore-keys: |
-            compile-${{ env.MIX_ENV }}-${{ runner.os }}-${{ steps.setupBEAMCurrent.outputs.otp-version }}${{ steps.setupBEAMDynamic.outputs.otp-version }}-${{ steps.setupBEAMCurrent.outputs.elixir-version }}${{ steps.setupBEAMDynamic.outputs.elixir-version }}-
+            compile-${{ env.MIX_ENV }}-${{ runner.os }}-${{ steps.setupBEAM.outputs.otp-version }}-${{ steps.setupBEAM.outputs.elixir-version }}-
       - run: mix deps.compile
       - run: mix compile --warning-as-errors
       - run: mix coveralls.github
diff --git a/test/quantum/clock_broadcaster_test.exs b/test/quantum/clock_broadcaster_test.exs
index bea22b4..8d7c1d1 100644
--- a/test/quantum/clock_broadcaster_test.exs
+++ b/test/quantum/clock_broadcaster_test.exs
@@ -32,7 +32,7 @@ defmodule Quantum.ClockBroadcasterTest do
     |> stream_broadcaster!()
     |> Stream.take(@listen_events)
     |> Stream.each(fn event ->
-      send(self, {:event, event, NaiveDateTime.utc_now()})
+      send(self, {:event, event, DateTime.utc_now()})
     end)
     |> Enum.to_list()
 
@@ -46,7 +46,7 @@ defmodule Quantum.ClockBroadcasterTest do
   test "catches up fast and then one every second", %{test: test} do
     self = self()
 
-    start_time = NaiveDateTime.add(NaiveDateTime.utc_now(), -@listen_events, :second)
+    start_time = DateTime.add(DateTime.utc_now(), -@listen_events, :second)
 
     test
     |> stream_broadcaster!(start_time: start_time)
@@ -68,7 +68,7 @@ defmodule Quantum.ClockBroadcasterTest do
   test "should wait for future date until", %{test: test} do
     self = self()
 
-    start_time = NaiveDateTime.add(NaiveDateTime.utc_now(), 2, :second)
+    start_time = DateTime.add(DateTime.utc_now(), 2, :second)
 
     test
     |> stream_broadcaster!(start_time: start_time)
@@ -84,14 +84,14 @@ defmodule Quantum.ClockBroadcasterTest do
   end
 
   defp receive_send(pid, event) do
-    send(pid, {:event, event, NaiveDateTime.utc_now()})
+    send(pid, {:event, event, DateTime.utc_now()})
   end
 
   defp assert_live_event(message) do
     assert {:event,
             %Event{
               time:
-                %NaiveDateTime{
+                %DateTime{
                   year: year,
                   month: month,
                   day: day,
@@ -101,7 +101,7 @@ defmodule Quantum.ClockBroadcasterTest do
                 } = event_time,
               catch_up: false
             },
-            %NaiveDateTime{
+            %DateTime{
               year: year,
               month: month,
               day: day,
@@ -110,7 +110,7 @@ defmodule Quantum.ClockBroadcasterTest do
               second: second
             } = receive_time} = message
 
-    assert NaiveDateTime.diff(event_time, receive_time, :millisecond) < 100
+    assert DateTime.diff(event_time, receive_time, :millisecond) < 100
   end
 
   defp assert_catch_up_event(message) do
@@ -129,7 +129,7 @@ defmodule Quantum.ClockBroadcasterTest do
            [
              name: Module.concat(__MODULE__, test),
              debug_logging: false,
-             start_time: NaiveDateTime.utc_now(),
+             start_time: DateTime.utc_now(),
              storage: Quantum.Storage.Test,
              scheduler: NotNeeded
            ],
diff --git a/test/quantum/date_library_test.exs b/test/quantum/date_library_test.exs
deleted file mode 100644
index c50715c..0000000
--- a/test/quantum/date_library_test.exs
+++ /dev/null
@@ -1,41 +0,0 @@
-defmodule Quantum.DateLibraryTest do
-  use ExUnit.Case, async: true
-
-  alias Quantum.DateLibrary
-
-  describe "to_tz!/2" do
-    test "shifts zone" do
-      date = ~N[2015-01-01 01:00:00]
-      expected = ~N[2015-01-01 02:00:00]
-
-      assert DateLibrary.to_tz!(date, "Europe/Copenhagen") == expected
-    end
-
-    test "errors with invalid timezone" do
-      assert_raise Quantum.DateLibrary.InvalidTimezoneError, fn ->
-        DateLibrary.to_tz!(~N[2018-03-25 02:00:00], "Foobar")
-      end
-    end
-  end
-
-  describe "to_utc!/2" do
-    test "shifts zone" do
-      date = ~N[2015-01-01 02:00:00]
-      expected = ~N[2015-01-01 01:00:00]
-
-      assert DateLibrary.to_utc!(date, "Europe/Copenhagen") == expected
-    end
-
-    test "detects non-existent times" do
-      assert_raise Quantum.DateLibrary.InvalidDateTimeForTimezoneError, fn ->
-        DateLibrary.to_utc!(~N[2018-03-25 02:00:00], "Europe/Zurich")
-      end
-    end
-
-    test "errors with invalid timezone" do
-      assert_raise Quantum.DateLibrary.InvalidTimezoneError, fn ->
-        DateLibrary.to_utc!(~N[2018-03-25 02:00:00], "Foobar")
-      end
-    end
-  end
-end
diff --git a/test/quantum/execution_broadcaster_test.exs b/test/quantum/execution_broadcaster_test.exs
index 9f3ae28..20d8528 100644
--- a/test/quantum/execution_broadcaster_test.exs
+++ b/test/quantum/execution_broadcaster_test.exs
@@ -66,7 +66,7 @@ defmodule Quantum.ExecutionBroadcasterTest do
       # Some schedule that is valid but will not trigger the next 10 years
       non_reboot_job =
         TestScheduler.new_job()
-        |> Job.set_schedule(~e[* * * * * #{NaiveDateTime.utc_now().year + 1}])
+        |> Job.set_schedule(~e[* * * * * #{DateTime.utc_now().year + 1}])
 
       capture_log(fn ->
         TestProducer.send(producer, {:add, reboot_job})
@@ -94,13 +94,13 @@ defmodule Quantum.ExecutionBroadcasterTest do
         TestProducer.send(producer, {:add, job})
 
         spawn(fn ->
-          now = %{NaiveDateTime.utc_now() | microsecond: {0, 0}}
+          now = DateTime.truncate(DateTime.utc_now(), :second)
           TestProducer.send(producer, %ClockEvent{time: now, catch_up: false})
 
           Process.sleep(1_000)
 
           TestProducer.send(producer, %ClockEvent{
-            time: NaiveDateTime.add(now, 1, :second),
+            time: DateTime.add(now, 1, :second),
             catch_up: false
           })
         end)
@@ -118,10 +118,10 @@ defmodule Quantum.ExecutionBroadcasterTest do
 
       capture_log(fn ->
         TestProducer.send(producer, {:add, job})
-        now = %{NaiveDateTime.utc_now() | microsecond: {0, 0}}
+        now = DateTime.truncate(DateTime.utc_now(), :second)
         TestProducer.send(producer, %ClockEvent{time: now, catch_up: false})
 
-        assert_receive {:update_last_execution_date, %NaiveDateTime{}, _}, @max_timeout
+        assert_receive {:update_last_execution_date, %DateTime{}, _}, @max_timeout
 
         assert_receive {:received, %ExecuteEvent{job: ^job}}, @max_timeout
       end)
@@ -137,8 +137,8 @@ defmodule Quantum.ExecutionBroadcasterTest do
         TestProducer.send(producer, {:add, job})
 
         spawn(fn ->
-          now = %{NaiveDateTime.utc_now() | microsecond: {0, 0}}
-          add1 = NaiveDateTime.add(now, 1, :second)
+          now = DateTime.truncate(DateTime.utc_now(), :second)
+          add1 = DateTime.add(now, 1, :second)
           TestProducer.send(producer, %ClockEvent{time: now, catch_up: false})
           Process.sleep(1_000)
           TestProducer.send(producer, %ClockEvent{time: add1, catch_up: false})
@@ -158,7 +158,7 @@ defmodule Quantum.ExecutionBroadcasterTest do
       assert capture_log(fn ->
                TestProducer.send(producer, {:add, job})
 
-               now = %{NaiveDateTime.utc_now() | microsecond: {0, 0}}
+               now = DateTime.truncate(DateTime.utc_now(), :second)
                TestProducer.send(producer, %ClockEvent{time: now, catch_up: false})
 
                refute_receive {:received, %ExecuteEvent{job: ^job}}, @max_timeout
@@ -177,7 +177,7 @@ defmodule Quantum.ExecutionBroadcasterTest do
       assert capture_log(fn ->
                TestProducer.send(producer, {:add, job})
 
-               now = %{NaiveDateTime.utc_now() | microsecond: {0, 0}}
+               now = DateTime.truncate(DateTime.utc_now(), :second)
                TestProducer.send(producer, %ClockEvent{time: now, catch_up: false})
 
                refute_receive {:received, %ExecuteEvent{job: ^job}}, @max_timeout
@@ -197,7 +197,7 @@ defmodule Quantum.ExecutionBroadcasterTest do
       capture_log(fn ->
         TestProducer.send(producer, {:add, job})
 
-        now = %{NaiveDateTime.utc_now() | microsecond: {0, 0}}
+        now = DateTime.truncate(DateTime.utc_now(), :second)
         TestProducer.send(producer, %ClockEvent{time: now, catch_up: false})
 
         assert_receive {:received, %ExecuteEvent{job: ^job}}, @max_timeout
@@ -205,7 +205,7 @@ defmodule Quantum.ExecutionBroadcasterTest do
         TestProducer.send(producer, {:add, job_new})
 
         TestProducer.send(producer, %ClockEvent{
-          time: NaiveDateTime.add(now, 1, :second),
+          time: DateTime.add(now, 1, :second),
           catch_up: false
         })
 
@@ -226,7 +226,7 @@ defmodule Quantum.ExecutionBroadcasterTest do
         TestProducer.send(producer, {:add, job})
         TestProducer.send(producer, {:add, job_new})
 
-        now = %{NaiveDateTime.utc_now() | microsecond: {0, 0}}
+        now = DateTime.truncate(DateTime.utc_now(), :second)
         TestProducer.send(producer, %ClockEvent{time: now, catch_up: false})
 
         assert_receive {:received, %ExecuteEvent{job: ^job_new}}, @max_timeout
@@ -242,7 +242,7 @@ defmodule Quantum.ExecutionBroadcasterTest do
 
       capture_log(fn ->
         TestProducer.send(producer, {:add, job})
-        now = %{NaiveDateTime.utc_now() | microsecond: {0, 0}}
+        now = DateTime.truncate(DateTime.utc_now(), :second)
         TestProducer.send(producer, %ClockEvent{time: now, catch_up: false})
 
         assert_receive {:received, %ExecuteEvent{job: ^job}}, @max_timeout
@@ -250,7 +250,7 @@ defmodule Quantum.ExecutionBroadcasterTest do
         TestProducer.send(producer, {:remove, job.name})
 
         TestProducer.send(producer, %ClockEvent{
-          time: NaiveDateTime.add(now, 1, :second),
+          time: DateTime.add(now, 1, :second),
           catch_up: false
         })
 
@@ -266,7 +266,7 @@ defmodule Quantum.ExecutionBroadcasterTest do
       capture_log(fn ->
         TestProducer.send(producer, {:add, job})
 
-        now = %{NaiveDateTime.utc_now() | microsecond: {0, 0}}
+        now = DateTime.truncate(DateTime.utc_now(), :second)
         TestProducer.send(producer, %ClockEvent{time: now, catch_up: false})
 
         assert_receive {:received, %ExecuteEvent{job: ^job}}, @max_timeout
@@ -274,7 +274,7 @@ defmodule Quantum.ExecutionBroadcasterTest do
         TestProducer.send(producer, {:remove, make_ref()})
 
         TestProducer.send(producer, %ClockEvent{
-          time: NaiveDateTime.add(now, 1, :second),
+          time: DateTime.add(now, 1, :second),
           catch_up: false
         })
 
diff --git a/test/quantum/job_broadcaster_test.exs b/test/quantum/job_broadcaster_test.exs
index 834015b..28e33b3 100644
--- a/test/quantum/job_broadcaster_test.exs
+++ b/test/quantum/job_broadcaster_test.exs
@@ -581,8 +581,8 @@ defmodule Quantum.JobBroadcasterTest do
     end
   end
 
-  @tag jobs: :active
   describe "find_job" do
+    @tag jobs: :active
     test "finds correct one", %{broadcaster: broadcaster, active_job: active_job} do
       active_job_name = active_job.name
 
diff --git a/test/quantum/scheduler_test.exs b/test/quantum/scheduler_test.exs
index 1bf274a..1c30a93 100644
--- a/test/quantum/scheduler_test.exs
+++ b/test/quantum/scheduler_test.exs
@@ -78,8 +78,8 @@ defmodule Quantum.SchedulerTest do
     end
   end
 
-  @tag schedulers: [Scheduler]
   describe "add_job/2" do
+    @tag schedulers: [Scheduler]
     test "adding a job at run time" do
       spec = ~e[1 * * * *]
       fun = fn -> :ok end