fix: prevent contacts_dedup_idx collisions in parallel async tests
Some checks failed
Build and Push / Build and Push Docker Image (push) Has been cancelled
Some checks failed
Build and Push / Build and Push Docker Image (push) Has been cancelled
Create shared ContactsFixtures module with globally-unique qso_timestamp seconds to prevent unique-constraint violations when async test modules run in parallel and insert contacts with identical dedup-key columns. The qso_timestamp column is timestamp(0), so microsecond offsets were truncated — use System.unique_integer monotonic seconds instead. Also make count-asserting tests resilient to sandbox-leaked contacts from prior tests by using >= assertions or status-based checks rather than exact counts. Includes automated DateTime.add → DateTime.shift migration from mix format.
This commit is contained in:
parent
607d3d3775
commit
581955bd69
84 changed files with 315 additions and 285 deletions
|
|
@ -210,7 +210,7 @@ defmodule Microwaveprop.Accounts do
|
|||
def sudo_mode?(user, minutes \\ -20)
|
||||
|
||||
def sudo_mode?(%User{authenticated_at: ts}, minutes) when is_struct(ts, DateTime) do
|
||||
DateTime.after?(ts, DateTime.add(DateTime.utc_now(), minutes, :minute))
|
||||
DateTime.after?(ts, DateTime.shift(DateTime.utc_now(), minute: minutes))
|
||||
end
|
||||
|
||||
def sudo_mode?(_user, _minutes), do: false
|
||||
|
|
|
|||
|
|
@ -405,7 +405,7 @@ defmodule Microwaveprop.Backtest do
|
|||
contact = Enum.random(usable)
|
||||
{lat, lon} = pos_to_latlon(contact.pos1)
|
||||
offset_seconds = :rand.uniform(60 * 24 * 3600) - 30 * 24 * 3600
|
||||
time = DateTime.add(contact.qso_timestamp, offset_seconds, :second)
|
||||
time = DateTime.shift(contact.qso_timestamp, second: offset_seconds)
|
||||
{lat, lon, time}
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -306,8 +306,8 @@ defmodule Microwaveprop.Backtest.Features do
|
|||
def nexrad_texture(lat, lon, valid_time) do
|
||||
dlat = 0.1
|
||||
dlon = 0.1
|
||||
time_start = DateTime.add(valid_time, -900, :second)
|
||||
time_end = DateTime.add(valid_time, 900, :second)
|
||||
time_start = DateTime.shift(valid_time, minute: -15)
|
||||
time_end = DateTime.shift(valid_time, minute: 15)
|
||||
|
||||
NexradObservation
|
||||
|> where(
|
||||
|
|
@ -336,8 +336,8 @@ defmodule Microwaveprop.Backtest.Features do
|
|||
defp find_nearest_native(lat, lon, valid_time) do
|
||||
dlat = 0.07
|
||||
dlon = 0.07
|
||||
time_start = DateTime.add(valid_time, -3600, :second)
|
||||
time_end = DateTime.add(valid_time, 3600, :second)
|
||||
time_start = DateTime.shift(valid_time, hour: -1)
|
||||
time_end = DateTime.shift(valid_time, hour: 1)
|
||||
|
||||
HrrrNativeProfile
|
||||
|> where(
|
||||
|
|
|
|||
|
|
@ -64,8 +64,8 @@ defmodule Microwaveprop.Commercial do
|
|||
baseline_days = Keyword.get(opts, :baseline_days, @default_baseline_days)
|
||||
current_window = Keyword.get(opts, :current_window_seconds, @default_current_window_seconds)
|
||||
|
||||
baseline_cutoff = DateTime.add(valid_time, -baseline_days * 24 * 3600, :second)
|
||||
current_cutoff = DateTime.add(valid_time, -current_window, :second)
|
||||
baseline_cutoff = DateTime.shift(valid_time, day: -baseline_days)
|
||||
current_cutoff = DateTime.shift(valid_time, second: -current_window)
|
||||
|
||||
enabled_links()
|
||||
|> Enum.map(fn link -> {link, link_endpoint(link)} end)
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ defmodule Microwaveprop.Commercial.PollWorker do
|
|||
|> Enum.uniq()
|
||||
|
||||
now = DateTime.utc_now()
|
||||
start_dt = DateTime.add(now, -3600, :second)
|
||||
start_dt = DateTime.shift(now, hour: -1)
|
||||
|
||||
stations
|
||||
|> Task.async_stream(&fetch_station_weather(&1, start_dt, now),
|
||||
|
|
|
|||
|
|
@ -171,8 +171,8 @@ defmodule Microwaveprop.Moon do
|
|||
"""
|
||||
@spec radial_velocity_km_s(DateTime.t(), float(), float()) :: float()
|
||||
def radial_velocity_km_s(%DateTime{} = dt, lat_deg, lon_deg) do
|
||||
before = DateTime.add(dt, -1, :second)
|
||||
later = DateTime.add(dt, 1, :second)
|
||||
before = DateTime.shift(dt, second: -1)
|
||||
later = DateTime.shift(dt, second: 1)
|
||||
|
||||
d_before = observer_position(before, lat_deg, lon_deg).distance_km
|
||||
d_later = observer_position(later, lat_deg, lon_deg).distance_km
|
||||
|
|
|
|||
|
|
@ -234,7 +234,7 @@ defmodule Microwaveprop.Propagation do
|
|||
"""
|
||||
@spec prune_old_scores() :: :ok
|
||||
def prune_old_scores do
|
||||
cutoff = DateTime.add(DateTime.utc_now(), -3, :hour)
|
||||
cutoff = DateTime.shift(DateTime.utc_now(), hour: -3)
|
||||
file_deleted = ScoresFile.prune_older_than(cutoff)
|
||||
profiles_deleted = ProfilesFile.prune_older_than(cutoff)
|
||||
scalar_deleted = ScalarFile.prune_older_than(cutoff)
|
||||
|
|
@ -348,8 +348,8 @@ defmodule Microwaveprop.Propagation do
|
|||
@spec hot_cache_window() :: {DateTime.t(), DateTime.t()}
|
||||
def hot_cache_window do
|
||||
now = DateTime.utc_now()
|
||||
past = DateTime.add(now, -3600, :second)
|
||||
future = DateTime.add(now, @hrrr_forecast_horizon_hours * 3600, :second)
|
||||
past = DateTime.shift(now, hour: -1)
|
||||
future = DateTime.shift(now, hour: @hrrr_forecast_horizon_hours)
|
||||
{past, future}
|
||||
end
|
||||
|
||||
|
|
@ -556,8 +556,8 @@ defmodule Microwaveprop.Propagation do
|
|||
defp forecast_window([], _now), do: []
|
||||
|
||||
defp forecast_window(times, now) do
|
||||
past_cutoff = DateTime.add(now, -3600, :second)
|
||||
future_cutoff = DateTime.add(now, @hrrr_forecast_horizon_hours * 3600, :second)
|
||||
past_cutoff = DateTime.shift(now, hour: -1)
|
||||
future_cutoff = DateTime.shift(now, hour: @hrrr_forecast_horizon_hours)
|
||||
filter_or_latest(times, past_cutoff, future_cutoff)
|
||||
end
|
||||
|
||||
|
|
@ -687,7 +687,7 @@ defmodule Microwaveprop.Propagation do
|
|||
end
|
||||
|
||||
defp latest_profile_time_within_lookback(%DateTime{} = valid_time) do
|
||||
lookback_cutoff = DateTime.add(valid_time, -@fallback_profile_lookback_hours * 3600, :second)
|
||||
lookback_cutoff = DateTime.shift(valid_time, hour: -@fallback_profile_lookback_hours)
|
||||
|
||||
ProfilesFile.list_valid_times()
|
||||
|> Enum.filter(fn t ->
|
||||
|
|
|
|||
|
|
@ -104,7 +104,7 @@ defmodule Microwaveprop.Propagation.GridTaskEnqueuer do
|
|||
id: Ecto.UUID.bingenerate(),
|
||||
run_time: run_time,
|
||||
forecast_hour: fh,
|
||||
valid_time: DateTime.add(run_time, fh * 3600, :second),
|
||||
valid_time: DateTime.shift(run_time, hour: fh),
|
||||
status: "queued",
|
||||
attempt: 0,
|
||||
kind: "forecast",
|
||||
|
|
@ -137,7 +137,7 @@ defmodule Microwaveprop.Propagation.GridTaskEnqueuer do
|
|||
id: Ecto.UUID.bingenerate(),
|
||||
run_time: run_time,
|
||||
forecast_hour: fh,
|
||||
valid_time: DateTime.add(run_time, fh * 3600, :second),
|
||||
valid_time: DateTime.shift(run_time, hour: fh),
|
||||
status: "queued",
|
||||
attempt: 0,
|
||||
kind: "forecast",
|
||||
|
|
@ -166,7 +166,7 @@ defmodule Microwaveprop.Propagation.GridTaskEnqueuer do
|
|||
"""
|
||||
@spec reclaim_stale_running(pos_integer()) :: %{requeued: non_neg_integer(), failed: non_neg_integer()}
|
||||
def reclaim_stale_running(cutoff_seconds \\ @stale_running_cutoff_seconds) do
|
||||
cutoff = DateTime.utc_now() |> DateTime.add(-cutoff_seconds, :second) |> DateTime.truncate(:second)
|
||||
cutoff = DateTime.utc_now() |> DateTime.shift(second: -cutoff_seconds) |> DateTime.truncate(:second)
|
||||
now = DateTime.truncate(DateTime.utc_now(), :second)
|
||||
|
||||
{failed_n, requeued_n} =
|
||||
|
|
@ -237,7 +237,7 @@ defmodule Microwaveprop.Propagation.GridTaskEnqueuer do
|
|||
id: Ecto.UUID.bingenerate(),
|
||||
run_time: run_time,
|
||||
forecast_hour: fh,
|
||||
valid_time: DateTime.add(run_time, fh * 3600, :second),
|
||||
valid_time: DateTime.shift(run_time, hour: fh),
|
||||
status: "queued",
|
||||
attempt: 0,
|
||||
kind: "forecast",
|
||||
|
|
|
|||
|
|
@ -130,8 +130,8 @@ defmodule Microwaveprop.Pskr.CalibrationSampler do
|
|||
# ── HRRR feature lookup ─────────────────────────────────────────
|
||||
|
||||
defp build_hrrr_index(hour_utc) do
|
||||
time_start = DateTime.add(hour_utc, -@hrrr_lookahead_seconds, :second)
|
||||
time_end = DateTime.add(hour_utc, @hrrr_lookahead_seconds, :second)
|
||||
time_start = DateTime.shift(hour_utc, second: -@hrrr_lookahead_seconds)
|
||||
time_end = DateTime.shift(hour_utc, second: @hrrr_lookahead_seconds)
|
||||
|
||||
Repo.all(
|
||||
from(h in HrrrProfile,
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ defmodule Microwaveprop.Qrz do
|
|||
|
||||
defp get_fresh_cache(callsign) do
|
||||
ttl_hours = config()[:cache_ttl_hours] || 168
|
||||
cutoff = DateTime.add(DateTime.utc_now(), -ttl_hours, :hour)
|
||||
cutoff = DateTime.shift(DateTime.utc_now(), hour: -ttl_hours)
|
||||
|
||||
Callsign
|
||||
|> where([c], c.callsign == ^callsign and c.updated_at > ^cutoff)
|
||||
|
|
|
|||
|
|
@ -322,8 +322,8 @@ defmodule Microwaveprop.Radio.AdifImport do
|
|||
defp load_existing_for_dedup(valid_rows) do
|
||||
bands = valid_rows |> Enum.map(& &1.attrs["band"]) |> Enum.uniq() |> Enum.map(&Decimal.new/1)
|
||||
timestamps = Enum.map(valid_rows, & &1.timestamp)
|
||||
min_ts = timestamps |> Enum.min(DateTime) |> DateTime.add(-@dedup_window_seconds, :second)
|
||||
max_ts = timestamps |> Enum.max(DateTime) |> DateTime.add(@dedup_window_seconds, :second)
|
||||
min_ts = timestamps |> Enum.min(DateTime) |> DateTime.shift(second: -@dedup_window_seconds)
|
||||
max_ts = timestamps |> Enum.max(DateTime) |> DateTime.shift(second: @dedup_window_seconds)
|
||||
|
||||
from(c in Contact,
|
||||
where:
|
||||
|
|
|
|||
|
|
@ -610,8 +610,8 @@ defmodule Microwaveprop.Radio.Contacts do
|
|||
band = Ecto.Changeset.get_field(changeset, :band)
|
||||
ts = Ecto.Changeset.get_field(changeset, :qso_timestamp)
|
||||
band_decimal = if is_binary(band), do: Decimal.new(band), else: band
|
||||
min_ts = DateTime.add(ts, -@dedup_window_seconds, :second)
|
||||
max_ts = DateTime.add(ts, @dedup_window_seconds, :second)
|
||||
min_ts = DateTime.shift(ts, second: -@dedup_window_seconds)
|
||||
max_ts = DateTime.shift(ts, second: @dedup_window_seconds)
|
||||
|
||||
Repo.one(
|
||||
from(c in Contact,
|
||||
|
|
|
|||
|
|
@ -536,8 +536,8 @@ defmodule Microwaveprop.Radio.CsvImport do
|
|||
defp load_existing_for_dedup(valid_rows) do
|
||||
bands = valid_rows |> Enum.map(& &1.attrs["band"]) |> Enum.uniq() |> Enum.map(&to_decimal/1)
|
||||
timestamps = Enum.map(valid_rows, & &1.timestamp)
|
||||
min_ts = timestamps |> Enum.min(DateTime) |> DateTime.add(-@dedup_window_seconds, :second)
|
||||
max_ts = timestamps |> Enum.max(DateTime) |> DateTime.add(@dedup_window_seconds, :second)
|
||||
min_ts = timestamps |> Enum.min(DateTime) |> DateTime.shift(second: -@dedup_window_seconds)
|
||||
max_ts = timestamps |> Enum.max(DateTime) |> DateTime.shift(second: @dedup_window_seconds)
|
||||
|
||||
from(c in Contact,
|
||||
where:
|
||||
|
|
|
|||
|
|
@ -109,7 +109,7 @@ defmodule Microwaveprop.Weather.HrdpsClient do
|
|||
def nearest_hrdps_cycle(%DateTime{} = dt) do
|
||||
cycle_hour = div(dt.hour, 6) * 6
|
||||
seconds_into_cycle = (dt.hour - cycle_hour) * 3600 + dt.minute * 60 + dt.second
|
||||
DateTime.add(dt, -seconds_into_cycle, :second)
|
||||
DateTime.shift(dt, second: -seconds_into_cycle)
|
||||
end
|
||||
|
||||
@doc """
|
||||
|
|
|
|||
|
|
@ -112,8 +112,8 @@ defmodule Microwaveprop.Weather.Hrrr do
|
|||
def nearest_native_duct_info(lat, lon, %DateTime{} = timestamp) do
|
||||
dlat = 0.07
|
||||
dlon = 0.07
|
||||
time_start = DateTime.add(timestamp, -3600, :second)
|
||||
time_end = DateTime.add(timestamp, 3600, :second)
|
||||
time_start = DateTime.shift(timestamp, hour: -1)
|
||||
time_end = DateTime.shift(timestamp, hour: 1)
|
||||
|
||||
from(h in HrrrNativeProfile,
|
||||
where:
|
||||
|
|
|
|||
|
|
@ -234,7 +234,7 @@ defmodule Microwaveprop.Weather.HrrrClient do
|
|||
@spec nearest_hrrr_hour(DateTime.t()) :: DateTime.t()
|
||||
def nearest_hrrr_hour(dt) do
|
||||
total_seconds = dt.minute * 60 + dt.second
|
||||
DateTime.add(dt, -total_seconds, :second)
|
||||
DateTime.shift(dt, second: -total_seconds)
|
||||
end
|
||||
|
||||
@spec hrrr_url(Date.t(), non_neg_integer(), :surface | :pressure, non_neg_integer()) :: String.t()
|
||||
|
|
|
|||
|
|
@ -105,7 +105,7 @@ defmodule Microwaveprop.Weather.NexradClient do
|
|||
:miss ->
|
||||
with {:ok, pixels, width} <- fetch_and_decode_frame(rounded) do
|
||||
NexradCache.put(rounded, pixels, width)
|
||||
NexradCache.prune_older_than(DateTime.add(rounded, -600, :second))
|
||||
NexradCache.prune_older_than(DateTime.shift(rounded, minute: -10))
|
||||
{:ok, pixels, width}
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -130,8 +130,8 @@ defmodule Microwaveprop.Weather.ProfileLookup do
|
|||
def find_nearest_hrrr(lat, lon, timestamp) do
|
||||
dlat = 0.07
|
||||
dlon = 0.07
|
||||
time_start = DateTime.add(timestamp, -3600, :second)
|
||||
time_end = DateTime.add(timestamp, 3600, :second)
|
||||
time_start = DateTime.shift(timestamp, hour: -1)
|
||||
time_end = DateTime.shift(timestamp, hour: 1)
|
||||
|
||||
HrrrProfile
|
||||
|> where(
|
||||
|
|
@ -211,8 +211,8 @@ defmodule Microwaveprop.Weather.ProfileLookup do
|
|||
lat_max = Enum.max(lats) + d
|
||||
lon_min = Enum.min(lons) - d
|
||||
lon_max = Enum.max(lons) + d
|
||||
time_min = times |> Enum.min_by(&DateTime.to_unix/1) |> DateTime.add(-margin_s, :second)
|
||||
time_max = times |> Enum.max_by(&DateTime.to_unix/1) |> DateTime.add(margin_s, :second)
|
||||
time_min = times |> Enum.min_by(&DateTime.to_unix/1) |> DateTime.shift(second: -margin_s)
|
||||
time_max = times |> Enum.max_by(&DateTime.to_unix/1) |> DateTime.shift(second: margin_s)
|
||||
|
||||
profiles =
|
||||
Repo.all(
|
||||
|
|
@ -244,8 +244,8 @@ defmodule Microwaveprop.Weather.ProfileLookup do
|
|||
def find_nearest_native_profile(lat, lon, timestamp) do
|
||||
dlat = 0.07
|
||||
dlon = 0.07
|
||||
time_start = DateTime.add(timestamp, -3600, :second)
|
||||
time_end = DateTime.add(timestamp, 3600, :second)
|
||||
time_start = DateTime.shift(timestamp, hour: -1)
|
||||
time_end = DateTime.shift(timestamp, hour: 1)
|
||||
|
||||
HrrrNativeProfile
|
||||
|> where(
|
||||
|
|
@ -306,8 +306,8 @@ defmodule Microwaveprop.Weather.ProfileLookup do
|
|||
def find_nearest_narr(lat, lon, timestamp) do
|
||||
dlat = 0.15
|
||||
dlon = 0.15
|
||||
time_start = DateTime.add(timestamp, -1800, :second)
|
||||
time_end = DateTime.add(timestamp, 1800, :second)
|
||||
time_start = DateTime.shift(timestamp, minute: -30)
|
||||
time_end = DateTime.shift(timestamp, minute: 30)
|
||||
|
||||
NarrProfile
|
||||
|> where(
|
||||
|
|
|
|||
|
|
@ -104,8 +104,8 @@ defmodule Microwaveprop.Weather.Soundings do
|
|||
dlat = radius_km / @km_per_deg_lat
|
||||
dlon = radius_km / (@km_per_deg_lat * :math.cos(lat * :math.pi() / 180))
|
||||
|
||||
time_start = DateTime.add(timestamp, -time_window_hours * 3600, :second)
|
||||
time_end = DateTime.add(timestamp, time_window_hours * 3600, :second)
|
||||
time_start = DateTime.shift(timestamp, hour: -time_window_hours)
|
||||
time_end = DateTime.shift(timestamp, hour: time_window_hours)
|
||||
|
||||
station_ids =
|
||||
Station
|
||||
|
|
@ -176,8 +176,8 @@ defmodule Microwaveprop.Weather.Soundings do
|
|||
# 1 deg lat ≈ 111 km; lon scaled by cos(lat).
|
||||
dlat = radius_km / 111.0
|
||||
dlon = radius_km / (111.0 * max(0.1, :math.cos(lat * :math.pi() / 180.0)))
|
||||
time_start = DateTime.add(timestamp, -hours * 3600, :second)
|
||||
time_end = DateTime.add(timestamp, hours * 3600, :second)
|
||||
time_start = DateTime.shift(timestamp, hour: -hours)
|
||||
time_end = DateTime.shift(timestamp, hour: hours)
|
||||
|
||||
from(s in Sounding,
|
||||
join: station in assoc(s, :station),
|
||||
|
|
|
|||
|
|
@ -132,7 +132,7 @@ defmodule Microwaveprop.Workers.BackfillEnqueueWorker do
|
|||
defp reconcile_stale_queued_to_unavailable(types) do
|
||||
cutoff =
|
||||
DateTime.utc_now()
|
||||
|> DateTime.add(-@stale_queued_cutoff_seconds, :second)
|
||||
|> DateTime.shift(second: -@stale_queued_cutoff_seconds)
|
||||
|> DateTime.truncate(:second)
|
||||
|
||||
now = DateTime.truncate(DateTime.utc_now(), :second)
|
||||
|
|
@ -259,7 +259,7 @@ defmodule Microwaveprop.Workers.BackfillEnqueueWorker do
|
|||
defp type_filter(types) do
|
||||
reprobe_cutoff =
|
||||
DateTime.utc_now()
|
||||
|> DateTime.add(-@unavailable_reprobe_cooldown_seconds, :second)
|
||||
|> DateTime.shift(second: -@unavailable_reprobe_cooldown_seconds)
|
||||
|> DateTime.truncate(:second)
|
||||
|
||||
Enum.reduce(types, dynamic(false), fn
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ defmodule Microwaveprop.Workers.CanadianSoundingFetchWorker do
|
|||
"""
|
||||
@spec most_recent_sounding_time(DateTime.t()) :: DateTime.t()
|
||||
def most_recent_sounding_time(%DateTime{} = now) do
|
||||
cutoff = DateTime.add(now, -@publish_delay_seconds, :second)
|
||||
cutoff = DateTime.shift(now, second: -@publish_delay_seconds)
|
||||
|
||||
if cutoff.hour >= 12 do
|
||||
%{cutoff | hour: 12, minute: 0, second: 0, microsecond: {0, 0}}
|
||||
|
|
|
|||
|
|
@ -520,8 +520,8 @@ defmodule Microwaveprop.Workers.ContactWeatherEnqueueWorker do
|
|||
end
|
||||
|
||||
defp build_asos_jobs(lat, lon, timestamp) do
|
||||
start_dt = DateTime.add(timestamp, -2 * 3600, :second)
|
||||
end_dt = DateTime.add(timestamp, 2 * 3600, :second)
|
||||
start_dt = DateTime.shift(timestamp, hour: -2)
|
||||
end_dt = DateTime.shift(timestamp, hour: 2)
|
||||
|
||||
stations = Weather.nearby_stations(lat, lon, "asos", @asos_radius_km)
|
||||
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ defmodule Microwaveprop.Workers.GefsFetchWorker do
|
|||
|
||||
case GefsClient.fetch_grid_profiles(run_date, run_hour, fh) do
|
||||
{:ok, profiles} ->
|
||||
valid_time = run_time |> DateTime.add(fh * 3600, :second) |> DateTime.truncate(:second)
|
||||
valid_time = run_time |> DateTime.shift(hour: fh) |> DateTime.truncate(:second)
|
||||
score_and_persist(profiles, run_time, fh, valid_time)
|
||||
|
||||
{:error, :wgrib2_not_available} ->
|
||||
|
|
@ -96,7 +96,7 @@ defmodule Microwaveprop.Workers.GefsFetchWorker do
|
|||
@spec most_recent_available_run(DateTime.t()) :: DateTime.t()
|
||||
def most_recent_available_run(%DateTime{} = now) do
|
||||
now
|
||||
|> DateTime.add(-5 * 3600, :second)
|
||||
|> DateTime.shift(hour: -5)
|
||||
|> GefsClient.nearest_run()
|
||||
end
|
||||
|
||||
|
|
@ -107,7 +107,7 @@ defmodule Microwaveprop.Workers.GefsFetchWorker do
|
|||
"""
|
||||
@spec build_profile_attrs(DateTime.t(), non_neg_integer(), map()) :: map()
|
||||
def build_profile_attrs(%DateTime{} = run_time, forecast_hour, profile) when is_integer(forecast_hour) do
|
||||
valid_time = DateTime.add(run_time, forecast_hour * 3600, :second)
|
||||
valid_time = DateTime.shift(run_time, hour: forecast_hour)
|
||||
derived = SoundingParams.derive(profile.profile || [])
|
||||
|
||||
base = %{
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ defmodule Microwaveprop.Workers.GridCachePruneWorker do
|
|||
|
||||
@impl Oban.Pro.Worker
|
||||
def process(%Oban.Job{}) do
|
||||
cutoff = DateTime.add(DateTime.utc_now(), -@cutoff_hours, :hour)
|
||||
cutoff = DateTime.shift(DateTime.utc_now(), hour: -@cutoff_hours)
|
||||
_removed = GridCache.prune_older_than(cutoff)
|
||||
:ok
|
||||
end
|
||||
|
|
|
|||
|
|
@ -87,12 +87,12 @@ defmodule Microwaveprop.Workers.HrdpsGridWorker do
|
|||
"""
|
||||
@spec pick_run_time(DateTime.t()) :: DateTime.t()
|
||||
def pick_run_time(%DateTime{} = now) do
|
||||
fresh = now |> DateTime.add(-4, :hour) |> HrdpsClient.nearest_hrdps_cycle() |> DateTime.truncate(:second)
|
||||
fresh = now |> DateTime.shift(hour: -4) |> HrdpsClient.nearest_hrdps_cycle() |> DateTime.truncate(:second)
|
||||
|
||||
if HrdpsClient.cycle_available?(fresh) do
|
||||
fresh
|
||||
else
|
||||
now |> DateTime.add(-10, :hour) |> HrdpsClient.nearest_hrdps_cycle() |> DateTime.truncate(:second)
|
||||
now |> DateTime.shift(hour: -10) |> HrdpsClient.nearest_hrdps_cycle() |> DateTime.truncate(:second)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -61,8 +61,8 @@ defmodule Microwaveprop.Workers.HrrrNativeGridWorker do
|
|||
@batch_size 1000
|
||||
|
||||
def points_of_interest_for_hour(valid_time) do
|
||||
time_start = DateTime.add(valid_time, -1800, :second)
|
||||
time_end = DateTime.add(valid_time, 1800, :second)
|
||||
time_start = DateTime.shift(valid_time, minute: -30)
|
||||
time_end = DateTime.shift(valid_time, minute: 30)
|
||||
|
||||
Contact
|
||||
|> where([c], not is_nil(c.pos1))
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ defmodule Microwaveprop.Workers.IonosphereFetchWorker do
|
|||
@impl Oban.Pro.Worker
|
||||
def process(%Oban.Job{}) do
|
||||
now = DateTime.truncate(DateTime.utc_now(), :second)
|
||||
from_dt = DateTime.add(now, -@lookback_seconds, :second)
|
||||
from_dt = DateTime.shift(now, second: -@lookback_seconds)
|
||||
|
||||
Enum.each(@stations, fn station ->
|
||||
fetch_and_upsert(station, from_dt, now)
|
||||
|
|
|
|||
|
|
@ -136,7 +136,7 @@ defmodule Microwaveprop.Workers.MechanismClassifyWorker do
|
|||
from(h in HrrrProfile,
|
||||
where:
|
||||
h.lat >= ^(lat - 0.07) and h.lat <= ^(lat + 0.07) and h.lon >= ^(lon - 0.07) and h.lon <= ^(lon + 0.07) and
|
||||
h.valid_time >= ^DateTime.add(ts, -3600, :second) and h.valid_time <= ^DateTime.add(ts, 3600, :second),
|
||||
h.valid_time >= ^DateTime.shift(ts, hour: -1) and h.valid_time <= ^DateTime.shift(ts, hour: 1),
|
||||
order_by: fragment("ABS(EXTRACT(EPOCH FROM ? - ?))", h.valid_time, ^ts),
|
||||
limit: 1
|
||||
)
|
||||
|
|
@ -153,7 +153,7 @@ defmodule Microwaveprop.Workers.MechanismClassifyWorker do
|
|||
from(h in "hrrr_native_profiles",
|
||||
where:
|
||||
h.lat >= ^(lat - 0.07) and h.lat <= ^(lat + 0.07) and h.lon >= ^(lon - 0.07) and h.lon <= ^(lon + 0.07) and
|
||||
h.valid_time >= ^DateTime.add(ts, -3600, :second) and h.valid_time <= ^DateTime.add(ts, 3600, :second),
|
||||
h.valid_time >= ^DateTime.shift(ts, hour: -1) and h.valid_time <= ^DateTime.shift(ts, hour: 1),
|
||||
order_by: fragment("ABS(EXTRACT(EPOCH FROM ? - ?))", h.valid_time, ^ts),
|
||||
limit: 1,
|
||||
select: h.best_duct_band_ghz
|
||||
|
|
@ -185,7 +185,7 @@ defmodule Microwaveprop.Workers.MechanismClassifyWorker do
|
|||
defp foes_for(%Contact{qso_timestamp: ts}) do
|
||||
Repo.one(
|
||||
from(io in IonosondeObservation,
|
||||
where: io.valid_time >= ^DateTime.add(ts, -3600, :second) and io.valid_time <= ^DateTime.add(ts, 3600, :second),
|
||||
where: io.valid_time >= ^DateTime.shift(ts, hour: -1) and io.valid_time <= ^DateTime.shift(ts, hour: 1),
|
||||
where: not is_nil(io.fo_es_mhz),
|
||||
order_by: fragment("ABS(EXTRACT(EPOCH FROM ? - ?))", io.valid_time, ^ts),
|
||||
limit: 1,
|
||||
|
|
|
|||
|
|
@ -84,8 +84,8 @@ defmodule Microwaveprop.Workers.NarrFetchWorker do
|
|||
# Lat/lon tolerance matches the 0.25° dedup granularity.
|
||||
dlat = 0.13
|
||||
dlon = 0.13
|
||||
time_start = DateTime.add(valid_time, -900, :second)
|
||||
time_end = DateTime.add(valid_time, 900, :second)
|
||||
time_start = DateTime.shift(valid_time, minute: -15)
|
||||
time_end = DateTime.shift(valid_time, minute: 15)
|
||||
|
||||
NarrProfile
|
||||
|> where(
|
||||
|
|
|
|||
|
|
@ -47,8 +47,8 @@ defmodule Microwaveprop.Workers.NexradWorker do
|
|||
@spec points_of_interest_for_time(DateTime.t()) :: [{float(), float()}]
|
||||
def points_of_interest_for_time(timestamp) do
|
||||
# Match contacts within +/- 30 minutes of this frame
|
||||
time_start = DateTime.add(timestamp, -1800, :second)
|
||||
time_end = DateTime.add(timestamp, 1800, :second)
|
||||
time_start = DateTime.shift(timestamp, minute: -30)
|
||||
time_end = DateTime.shift(timestamp, minute: 30)
|
||||
|
||||
Contact
|
||||
|> where([c], not is_nil(c.pos1))
|
||||
|
|
|
|||
|
|
@ -85,12 +85,12 @@ defmodule Microwaveprop.Workers.PropagationGridWorker do
|
|||
"""
|
||||
@spec pick_run_time(DateTime.t()) :: DateTime.t()
|
||||
def pick_run_time(%DateTime{} = now) do
|
||||
fresh = now |> DateTime.add(-1, :hour) |> HrrrClient.nearest_hrrr_hour() |> DateTime.truncate(:second)
|
||||
fresh = now |> DateTime.shift(hour: -1) |> HrrrClient.nearest_hrrr_hour() |> DateTime.truncate(:second)
|
||||
|
||||
if HrrrClient.cycle_available?(fresh) do
|
||||
fresh
|
||||
else
|
||||
now |> DateTime.add(-2, :hour) |> HrrrClient.nearest_hrrr_hour() |> DateTime.truncate(:second)
|
||||
now |> DateTime.shift(hour: -2) |> HrrrClient.nearest_hrrr_hour() |> DateTime.truncate(:second)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ defmodule Microwaveprop.Workers.PropagationPruneWorker do
|
|||
def process(%Oban.Job{}) do
|
||||
Propagation.prune_old_scores()
|
||||
|
||||
cutoff_30d = DateTime.add(DateTime.utc_now(), -30, :day)
|
||||
cutoff_30d = DateTime.shift(DateTime.utc_now(), day: -30)
|
||||
buildings_deleted = MsFootprints.prune_older_than(cutoff_30d)
|
||||
canopy_deleted = Canopy.prune_older_than(cutoff_30d)
|
||||
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@ defmodule Microwaveprop.Workers.PskrCalibrationWorker do
|
|||
now = truncate_to_hour(DateTime.utc_now())
|
||||
|
||||
Enum.each(0..lookback, fn offset ->
|
||||
hour = DateTime.add(now, -offset * 3600, :second)
|
||||
hour = DateTime.shift(now, hour: -offset)
|
||||
process_hour(hour, args)
|
||||
end)
|
||||
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ defmodule Microwaveprop.Workers.RadarFrameWorker do
|
|||
def process(%Oban.Job{args: %{"frame_ts" => frame_iso}}) do
|
||||
{:ok, frame_ts, _} = DateTime.from_iso8601(frame_iso)
|
||||
rounded = NexradClient.round_to_5min(frame_ts)
|
||||
bucket_end = DateTime.add(rounded, @frame_window_seconds, :second)
|
||||
bucket_end = DateTime.shift(rounded, second: @frame_window_seconds)
|
||||
|
||||
# Query by the frame's 5-min bucket rather than a passed contact_ids
|
||||
# list. The unique-on-frame_ts guard prevents new sweeps from enqueueing
|
||||
|
|
|
|||
|
|
@ -200,7 +200,7 @@ defmodule Microwaveprop.Workers.WeatherFetchWorker do
|
|||
end
|
||||
|
||||
defp store_asos_stub(station, start_dt, end_dt) do
|
||||
midpoint = DateTime.add(start_dt, div(DateTime.diff(end_dt, start_dt), 2))
|
||||
midpoint = DateTime.shift(start_dt, second: div(DateTime.diff(end_dt, start_dt), 2))
|
||||
|
||||
%SurfaceObservation{}
|
||||
|> SurfaceObservation.changeset(%{station_id: station.id, observed_at: midpoint})
|
||||
|
|
|
|||
|
|
@ -275,7 +275,7 @@ defmodule MicrowavepropWeb.SkewtLive do
|
|||
if DateTime.after?(valid_time, now) do
|
||||
cycle =
|
||||
now
|
||||
|> DateTime.add(-2 * 3600, :second)
|
||||
|> DateTime.shift(hour: -2)
|
||||
|> HrrrClient.nearest_hrrr_hour()
|
||||
|
||||
fh = max(0, div(DateTime.diff(valid_time, cycle, :second), 3600))
|
||||
|
|
@ -345,8 +345,8 @@ defmodule MicrowavepropWeb.SkewtLive do
|
|||
# ~2 h old. A 1-hour past cutoff would leave the page empty while
|
||||
# the chain catches up — keep 3 h so a stale analysis still shows.
|
||||
now = DateTime.utc_now()
|
||||
past_cutoff = DateTime.add(now, -3 * 3600, :second)
|
||||
future_cutoff = DateTime.add(now, 18 * 3600, :second)
|
||||
past_cutoff = DateTime.shift(now, hour: -3)
|
||||
future_cutoff = DateTime.shift(now, hour: 18)
|
||||
|
||||
on_disk =
|
||||
Enum.filter(ProfilesFile.list_valid_times(), fn t ->
|
||||
|
|
|
|||
|
|
@ -168,7 +168,7 @@ defmodule MicrowavepropWeb.WeatherMapLive do
|
|||
# showing 3-hour-old "current conditions" on the map is more
|
||||
# misleading than helpful.
|
||||
defp recent_valid_times(valid_times) do
|
||||
cutoff = DateTime.add(DateTime.utc_now(), -3600, :second)
|
||||
cutoff = DateTime.shift(DateTime.utc_now(), hour: -1)
|
||||
Enum.filter(valid_times, fn vt -> DateTime.compare(vt, cutoff) != :lt end)
|
||||
end
|
||||
|
||||
|
|
|
|||
18
mix.lock
18
mix.lock
|
|
@ -36,8 +36,8 @@
|
|||
"live_stash": {:hex, :live_stash, "1.0.1", "26f46749b2b7d6484ff653b15d616b79e85b99891b3217e4584e54e88163fb98", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: true]}, {:memento, "~> 0.5.0", [hex: :memento, repo: "hexpm", optional: true]}, {:phoenix_live_view, "~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:redix, "~> 1.1", [hex: :redix, repo: "hexpm", optional: true]}, {:uniq, "~> 0.6", [hex: :uniq, repo: "hexpm", optional: false]}], "hexpm", "3767556fce36c2645479d8807a1d4992e5c2261f0b4b44e58d8a88e5bd313a47"},
|
||||
"live_table": {:hex, :live_table, "0.4.2", "9c011a8a078db3b86a2e77afe141d201d641512a1a9f421b62c4e14c41a4a5a9", [:mix], [{:ecto, "~> 3.13", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.13", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:igniter, "~> 0.7.0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.3", [hex: :nimble_csv, repo: "hexpm", optional: false]}, {:oban, "~> 2.20", [hex: :oban, repo: "hexpm", optional: false]}, {:oban_web, "~> 2.11", [hex: :oban_web, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 4.3", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 1.2", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:postgrex, ">= 0.0.0", [hex: :postgrex, repo: "hexpm", optional: false]}, {:sutra_ui, "~> 0.4.0", [hex: :sutra_ui, repo: "hexpm", optional: true]}], "hexpm", "7c00c8469a974ff6abe79e34a940eba1f813d95f05360713313ece9e793f6c38"},
|
||||
"logger_json": {:hex, :logger_json, "7.0.4", "e315f2b9a755504658a745f3eab90d88d2cd7ac2ecfd08c8da94d8893965ab5c", [:mix], [{:decimal, ">= 0.0.0", [hex: :decimal, repo: "hexpm", optional: true]}, {:ecto, "~> 3.11", [hex: :ecto, repo: "hexpm", optional: true]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: true]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: true]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "d1369f8094e372db45d50672c3b91e8888bcd695fdc444a37a0734e96717c45c"},
|
||||
"mdex": {:hex, :mdex, "0.13.4", "9e42874ee4e9f8be110a5c03a91f838068c1b089bc0c586243d83503b153a2b1", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:lumis, "~> 0.1", [hex: :lumis, repo: "hexpm", optional: true]}, {:mdex_native, ">= 0.2.6", [hex: :mdex_native, repo: "hexpm", optional: false]}, {:nimble_options, "~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: true]}], "hexpm", "915e579df919923c0685e6ea9851cf6e4a346d014795f03ce5a1a9c52455192b"},
|
||||
"mdex_native": {:hex, :mdex_native, "0.2.6", "16c0d9ffca05990982ef8113c0a61950ffc250bf0792e17b95b28b74e1f572dc", [:mix], [{:rustler, "~> 0.32", [hex: :rustler, repo: "hexpm", optional: true]}, {:rustler_precompiled, "~> 0.8", [hex: :rustler_precompiled, repo: "hexpm", optional: false]}], "hexpm", "eaaf2af27162c9879f5aef66344101dadf1898e71ea6ac9eb811c8d92d8e49ad"},
|
||||
"mdex": {:hex, :mdex, "0.13.5", "c1c94d230ccaab01ad0c68090d3b31613c10ece1844f32b55895da4ce0c63029", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:lumis, "~> 0.1", [hex: :lumis, repo: "hexpm", optional: true]}, {:mdex_native, ">= 0.2.6", [hex: :mdex_native, repo: "hexpm", optional: false]}, {:nimble_options, "~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.20.0 or ~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: true]}], "hexpm", "c57409fb6b34fbc58fbce0a6da670c9a4b5a2e94f86abdc56e9e213ed74620f2"},
|
||||
"mdex_native": {:hex, :mdex_native, "0.2.7", "46203ee9c4fe2c94feef26e4d976235ad022b4770d3d307fa6b445d293b2571f", [:mix], [{:rustler, "~> 0.32", [hex: :rustler, repo: "hexpm", optional: true]}, {:rustler_precompiled, "~> 0.8", [hex: :rustler_precompiled, repo: "hexpm", optional: false]}], "hexpm", "05efa68775644ef6be6109f76c1f5f8778ff7e7ed9fe4a03541758158172bc77"},
|
||||
"mime": {:hex, :mime, "2.0.7", "b8d739037be7cd402aee1ba0306edfdef982687ee7e9859bee6198c1e7e2f128", [:mix], [], "hexpm", "6171188e399ee16023ffc5b76ce445eb6d9672e2e241d2df6050f3c771e80ccd"},
|
||||
"mint": {:hex, :mint, "1.9.3", "3337184d69179695c7a9f1714d92c11e629d36c8c037a21cf490131d3d150554", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "5f7c9342480c069dbbc4eeac3490303c9e01870ff01a7f1d29b6107054fc1e74"},
|
||||
"mix_audit": {:hex, :mix_audit, "2.1.5", "c0f77cee6b4ef9d97e37772359a187a166c7a1e0e08b50edf5bf6959dfe5a016", [:make, :mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:yaml_elixir, "~> 2.11", [hex: :yaml_elixir, repo: "hexpm", optional: false]}], "hexpm", "87f9298e21da32f697af535475860dc1d3617a010e0b418d2ec6142bc8b42d69"},
|
||||
|
|
@ -49,7 +49,7 @@
|
|||
"nimble_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [:mix], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"},
|
||||
"nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"},
|
||||
"nx": {:hex, :nx, "0.13.0", "43c57f529f17139821f6ed01a8aba6e045fef6722700c928cec2e2e3d2ee9560", [:mix], [{:complex, "~> 0.7", [hex: :complex, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "38d2f37ee7e2f3aaa86b96f125643ffbca56a89576cd24d83b452597397b4fbd"},
|
||||
"oban": {:hex, :oban, "2.23.0", "1867d0fa4e8c7685217b02cc2632e3ee86c93da770e9029ff71304d9e62e53d7", [:mix], [{:ecto_sql, "~> 3.10", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:ecto_sqlite3, "~> 0.9", [hex: :ecto_sqlite3, repo: "hexpm", optional: true]}, {:igniter, "~> 0.5", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: true]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.20", [hex: :postgrex, repo: "hexpm", optional: true]}, {:telemetry, "~> 1.3", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "8e5f0cec5abecce78dd08cb14dc5438db90ec3884987b44773ce76fe60dd3f81"},
|
||||
"oban": {:hex, :oban, "2.23.1", "0b9495e28a236ca0478c80d666c58a8b2b55182731c3603c6e766e298a08342a", [:mix], [{:ecto_sql, "~> 3.10", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:ecto_sqlite3, "~> 0.9", [hex: :ecto_sqlite3, repo: "hexpm", optional: true]}, {:igniter, "~> 0.5", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: true]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.20", [hex: :postgrex, repo: "hexpm", optional: true]}, {:telemetry, "~> 1.3", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a9855b9f5d87e31de3e2f46731b163f372e329613892a56c5b2aacceb50ed508"},
|
||||
"octo_fetch": {:hex, :octo_fetch, "0.5.0", "f50701568b9fc752656367f82cc134d5fbefff37c5a0e8ddfcceb02ceee3f5fc", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm", "6226cc3c14ca948ee9f25fb0446322e5c288e215da9beba7899b6b5f4cd3ccb0"},
|
||||
"owl": {:hex, :owl, "0.13.1", "1ec4a5dea170465f0e90c502c203079224516bc0cbd599281c8667b3c6ef8848", [:mix], [{:ucwidth, "~> 0.2", [hex: :ucwidth, repo: "hexpm", optional: true]}], "hexpm", "351e768af8f2edc575cdaab1a5a2f6d6381be591758a026c701c703145508a0c"},
|
||||
"peep": {:hex, :peep, "4.4.0", "4b6289e7258ecf2741041068932455eb8389673bca785623621ddb6935286845", [:mix], [{:nimble_options, "~> 1.1", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:plug, "~> 1.16", [hex: :plug, repo: "hexpm", optional: true]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "f289959a9e5fcf92f5a4becaf4dd0df95c58841368f6ad0574b8ea830a7c1453"},
|
||||
|
|
@ -57,7 +57,7 @@
|
|||
"phoenix_ecto": {:hex, :phoenix_ecto, "4.7.0", "75c4b9dfb3efdc42aec2bd5f8bccd978aca0651dbcbc7a3f362ea5d9d43153c6", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.1", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "1d75011e4254cb4ddf823e81823a9629559a1be93b4321a6a5f11a5306fbf4cc"},
|
||||
"phoenix_html": {:hex, :phoenix_html, "4.3.0", "d3577a5df4b6954cd7890c84d955c470b5310bb49647f0a114a6eeecc850f7ad", [:mix], [], "hexpm", "3eaa290a78bab0f075f791a46a981bbe769d94bc776869f4f3063a14f30497ad"},
|
||||
"phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.8.7", "405880012cb4b706f26dd1c6349125bfc903fb9e44d1ea668adaf4e04d4884b7", [:mix], [{:ecto, "~> 3.6.2 or ~> 3.7", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_mysql_extras, "~> 0.5", [hex: :ecto_mysql_extras, repo: "hexpm", optional: true]}, {:ecto_psql_extras, "~> 0.7", [hex: :ecto_psql_extras, repo: "hexpm", optional: true]}, {:ecto_sqlite3_extras, "~> 1.1.7 or ~> 1.2.0", [hex: :ecto_sqlite3_extras, repo: "hexpm", optional: true]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.19 or ~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6 or ~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "3a8625cab39ec261d48a13b7468dc619c0ede099601b084e343968309bd4d7d7"},
|
||||
"phoenix_live_reload": {:hex, :phoenix_live_reload, "1.6.2", "b18b0773a1ba77f28c52decbb0f10fd1ac4d3ae5b8632399bbf6986e3b665f62", [:mix], [{:file_system, "~> 0.2.10 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "d1f89c18114c50d394721365ffb428cce24f1c13de0467ffa773e2ff4a30d5b9"},
|
||||
"phoenix_live_reload": {:hex, :phoenix_live_reload, "1.7.0", "fb1e429f6d8778ce3a6962debdc5e555428a05a6e7b058d6dbad13d281a2c31f", [:mix], [{:file_system, "~> 0.2.10 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "dc9f44271aa6fc4ab7797f2aa374ba096ef2c87520586280eb095626b7387a68"},
|
||||
"phoenix_live_view": {:hex, :phoenix_live_view, "1.2.8", "5006fd7b429c42489600fbc1600c750d0f0e5b5ea4965d9758e429b979392991", [:mix], [{:igniter, ">= 0.6.16 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:lazy_html, "~> 0.1.0", [hex: :lazy_html, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0 or ~> 1.8.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "b05ffe21f43c0ff219da62948b482c324aa5b8873e17b0c0cac58289a178af38"},
|
||||
"phoenix_pubsub": {:hex, :phoenix_pubsub, "2.2.0", "ff3a5616e1bed6804de7773b92cbccfc0b0f473faf1f63d7daf1206c7aeaaa6f", [:mix], [], "hexpm", "adc313a5bf7136039f63cfd9668fde73bba0765e0614cba80c06ac9460ff3e96"},
|
||||
"phoenix_pubsub_redis": {:hex, :phoenix_pubsub_redis, "3.1.1", "54919406b0b6401e0f643e26fd3b814bc3460f3fd3a32aa9c3a473db89e9dca7", [:mix], [{:nimble_options, "~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:poolboy, "~> 1.5.1 or ~> 1.6", [hex: :poolboy, repo: "hexpm", optional: false]}, {:redix, "~> 1.0", [hex: :redix, repo: "hexpm", optional: false]}], "hexpm", "e339963f649e7cf52bd352e55fab7bebb42b65b501e43499dd6eb56890703c32"},
|
||||
|
|
@ -68,19 +68,19 @@
|
|||
"poolboy": {:hex, :poolboy, "1.5.2", "392b007a1693a64540cead79830443abf5762f5d30cf50bc95cb2c1aaafa006b", [:rebar3], [], "hexpm", "dad79704ce5440f3d5a3681c8590b9dc25d1a561e8f5a9c995281012860901e3"},
|
||||
"postgrex": {:hex, :postgrex, "0.22.3", "bf65941737ee7a9adbe4a64c91080310d11703da343e8ac9188aacb9eb9f6f02", [:mix], [{:db_connection, "~> 2.9", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0 or ~> 3.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "f018c13752b2b46e8d35d7e2d84c3276557cbfd880769109021a1d0ee36c1cfe"},
|
||||
"prom_ex": {:hex, :prom_ex, "1.12.0", "a82cbd5b49964e4d4295ab72a18e007aadd5f4a91630b7c49fad42cc7e49c880", [:mix], [{:absinthe, ">= 1.8.0", [hex: :absinthe, repo: "hexpm", optional: true]}, {:broadway, ">= 1.1.0", [hex: :broadway, repo: "hexpm", optional: true]}, {:ecto, ">= 3.14.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:finch, "~> 0.18", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:oban, ">= 2.10.0", [hex: :oban, repo: "hexpm", optional: true]}, {:octo_fetch, "~> 0.4", [hex: :octo_fetch, repo: "hexpm", optional: false]}, {:peep, "~> 3.0 or ~> 4.0", [hex: :peep, repo: "hexpm", optional: false]}, {:phoenix, ">= 1.7.0", [hex: :phoenix, repo: "hexpm", optional: true]}, {:phoenix_live_view, ">= 0.20.0", [hex: :phoenix_live_view, repo: "hexpm", optional: true]}, {:plug, ">= 1.16.0", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, ">= 2.6.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:telemetry, ">= 1.0.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}, {:telemetry_metrics_prometheus_core, "~> 1.2", [hex: :telemetry_metrics_prometheus_core, repo: "hexpm", optional: false]}, {:telemetry_poller, "~> 1.1", [hex: :telemetry_poller, repo: "hexpm", optional: false]}], "hexpm", "6357484941489ba2fee64bb1e9f2a1e86309545304f9e90144e3c10e553c958b"},
|
||||
"ranch": {:hex, :ranch, "2.2.0", "25528f82bc8d7c6152c57666ca99ec716510fe0925cb188172f41ce93117b1b0", [:make, :rebar3], [], "hexpm", "fa0b99a1780c80218a4197a59ea8d3bdae32fbff7e88527d7d8a4787eff4f8e7"},
|
||||
"ranch": {:hex, :ranch, "2.2.1", "fc2bb0e800efbeab8ff82eb325450b9c08653b14c3f69e8b09bbea304973105d", [:make, :rebar3], [], "hexpm", "55f05cce20ec2da1d90de5d5981afb93dbfc01325fc7e933189aa5c62f037c24"},
|
||||
"redix": {:hex, :redix, "1.6.0", "694179c7a3c71bffac8848fcbfe16f6f6e2a1e020f00a2ad01bc6484815470d1", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:nimble_options, "~> 0.5.0 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "b2eccb05e02f21c0c3ca57513e6bacb4dd48e6406dadbd7ff9fbe07bd6745999"},
|
||||
"req": {:hex, :req, "0.7.2", "364eae2e5f5c984f2dac6d71c07f8c8c89ce0bc49c4d746dacb7a306823020de", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:finch, "~> 0.21", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "c9cdfa276b05d8db2a27fda5d233e6858b764d47189d76cbb186e130a871ae0b"},
|
||||
"rewrite": {:hex, :rewrite, "1.3.0", "67448ba7975690b35ba7e7f35717efcce317dbd5963cb0577aa7325c1923121a", [:mix], [{:glob_ex, "~> 0.1", [hex: :glob_ex, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.0", [hex: :sourceror, repo: "hexpm", optional: false]}, {:text_diff, "~> 0.1", [hex: :text_diff, repo: "hexpm", optional: false]}], "hexpm", "d111ac7ff3a58a802ef4f193bbd1831e00a9c57b33276e5068e8390a212714a5"},
|
||||
"rustler_precompiled": {:hex, :rustler_precompiled, "0.9.0", "3a052eda09f3d2436364645cc1f13279cf95db310eb0c17b0d8f25484b233aa0", [:mix], [{:rustler, "~> 0.23", [hex: :rustler, repo: "hexpm", optional: true]}], "hexpm", "471d97315bd3bf7b64623418b3693eedd8e47de3d1cb79a0ac8f9da7d770d94c"},
|
||||
"sobelow": {:hex, :sobelow, "0.14.1", "2f81e8632f15574cba2402bcddff5497b413c01e6f094bc0ab94e83c2f74db81", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "8fac9a2bd90fdc4b15d6fca6e1608efb7f7c600fa75800813b794ee9364c87f2"},
|
||||
"sourceror": {:hex, :sourceror, "1.12.2", "85bfd48159f020c0cbfc72f289f11456fdc05dc43719b6f2589fb969faefa113", [:mix], [], "hexpm", "da37d3da09c5b890528802c7056a8f585a061973820d7656b6e3649c14f0e9cb"},
|
||||
"spitfire": {:hex, :spitfire, "0.3.13", "edd207b065eaec57acc5484097d0aa3e97fe4246168c54e67a9af040b8dee4c1", [:mix], [], "hexpm", "3601be88ceed4967b584e96444de3e1d12d6555ae0864a7390b9cd5332d134b4"},
|
||||
"spitfire": {:hex, :spitfire, "0.4.0", "6d98c10cf585434b9439ba0c6dd3cc7aeff0e06ab73bfe5488f42e7c0f883d9b", [:mix], [], "hexpm", "7e5c6d1523c111b59f332f9dc49edc0377111d0c17167a29830f0e98233f5472"},
|
||||
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"},
|
||||
"stream_data": {:hex, :stream_data, "1.4.0", "026f929db613aabea6208012ae9b8970d3fd5f88b3bdf26831bc536f98c42036", [:mix], [], "hexpm", "2b0ee3a340dcce1c8cf6302a763ee757d1e01c54d6e16d9069062509d68b1dc9"},
|
||||
"styler": {:hex, :styler, "1.11.0", "35010d970689a23c2bcc8e97bd8bf7d20e3561d60c49be84654df5c37d051a9c", [:mix], [], "hexpm", "70f36165d0cf238a32b7a456fdef6a9c72e77e657d7ac4a0ace33aeba3f2b8c0"},
|
||||
"styler": {:hex, :styler, "1.12.2", "1aa2942295645df48d42365f7191cc8b9f6fc4c35d15491f6fbd80ee815256b0", [:mix], [], "hexpm", "a82baeb6d81e35ee1c4cf8af08e141e8d7872a1d442f6fefce29817869e77a01"},
|
||||
"sutra_ui": {:hex, :sutra_ui, "0.4.0", "ef348fe496af1a311193c35aa782c4c0256894a80c623a3c0dd13c759e921344", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mdex, "~> 0.13", [hex: :mdex, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 1.2", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}], "hexpm", "857a9fa7b182ca8105d6bedeaffa1e16beeb296807ca60f7f6e45ae96b41fcd7"},
|
||||
"swoosh": {:hex, :swoosh, "1.26.3", "9d8b60077305ce259298d9a1102e5be67cd3c41d1ea930c29e9288af195ca017", [:mix], [{:bandit, ">= 1.0.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:ex_aws, "~> 2.1", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:finch, "~> 0.6", [hex: :finch, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, ">= 1.9.0 and < 5.0.0", [hex: :hackney, repo: "hexpm", optional: true]}, {:idna, ">= 6.0.0 and < 8.0.0", [hex: :idna, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mua, "~> 0.2.3", [hex: :mua, repo: "hexpm", optional: true]}, {:multipart, "~> 0.4", [hex: :multipart, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:req, "~> 0.5.10 or ~> 0.6 or ~> 1.0", [hex: :req, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c7683d070fe8f8aa9d174e61b01f2d527be73cd8ac40037b7109184941eb569f"},
|
||||
"swoosh": {:hex, :swoosh, "1.27.0", "2df57c342f3854f2d429e6afebf19a03a179976a64006a93750ed81639453f75", [:mix], [{:bandit, ">= 1.0.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:ex_aws, "~> 2.1", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:finch, "~> 0.6", [hex: :finch, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, ">= 1.9.0 and < 5.0.0", [hex: :hackney, repo: "hexpm", optional: true]}, {:idna, ">= 6.0.0 and < 8.0.0", [hex: :idna, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mua, "~> 0.2.3", [hex: :mua, repo: "hexpm", optional: true]}, {:multipart, "~> 0.4", [hex: :multipart, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:req, "~> 0.5.10 or ~> 0.6 or ~> 1.0", [hex: :req, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "5da7d3b11de5d61327275ed599cb311942ea6e23cdbb411981f46ee150cddf76"},
|
||||
"table_rex": {:hex, :table_rex, "4.1.0", "fbaa8b1ce154c9772012bf445bfb86b587430fb96f3b12022d3f35ee4a68c918", [:mix], [], "hexpm", "95932701df195d43bc2d1c6531178fc8338aa8f38c80f098504d529c43bc2601"},
|
||||
"tailwind": {:hex, :tailwind, "0.5.1", "35435b13158c90d37da11e1cfc808755fca1d7b6c5ab87b1b19c5de87e2f0a10", [:mix], [], "hexpm", "c4e26302a59fec72abc5610ecb6ad2116d9aa31f31aab2d4b8eb6e95d25a689c"},
|
||||
"telemetry": {:hex, :telemetry, "1.4.2", "a0cb522801dffb1c49fe6e30561badffc7b6d0e180db1300df759faa22062855", [:rebar3], [], "hexpm", "928f6495066506077862c0d1646609eed891a4326bee3126ba54b60af61febb1"},
|
||||
|
|
@ -89,7 +89,7 @@
|
|||
"telemetry_poller": {:hex, :telemetry_poller, "1.3.0", "d5c46420126b5ac2d72bc6580fb4f537d35e851cc0f8dbd571acf6d6e10f5ec7", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "51f18bed7128544a50f75897db9974436ea9bfba560420b646af27a9a9b35211"},
|
||||
"text_diff": {:hex, :text_diff, "0.1.0", "1caf3175e11a53a9a139bc9339bd607c47b9e376b073d4571c031913317fecaa", [:mix], [], "hexpm", "d1ffaaecab338e49357b6daa82e435f877e0649041ace7755583a0ea3362dbd7"},
|
||||
"thousand_island": {:hex, :thousand_island, "1.5.0", "f50a213cac97262b6d5ebb85745aa2c00fec1413191e6e66834788d45425cecb", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "708923d40523e43cf99041ab37a0d4b0ec426ac6438fa3716ab23d919eaeb412"},
|
||||
"tidewave": {:hex, :tidewave, "0.8.0", "eb41df8d0b967a6ead2f48d6ccded369c17207058bb2cf7128c33562ad3d90f9", [:mix], [{:circular_buffer, "~> 0.4 or ~> 1.0", [hex: :circular_buffer, repo: "hexpm", optional: false]}, {:igniter, "~> 0.6", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:phoenix_live_reload, ">= 1.6.1", [hex: :phoenix_live_reload, repo: "hexpm", optional: true]}, {:plug, "~> 1.18", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.9", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock_adapter, "~> 0.5", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "91a2007d021722d7ab3629f9024b5375819c827952f3a5ac19840ac528261532"},
|
||||
"tidewave": {:hex, :tidewave, "0.8.1", "d69ba6c131886d4b8d0b1337f1e910f7202d91d3adaff835e67f67b2dae67b30", [:mix], [{:circular_buffer, "~> 0.4 or ~> 1.0", [hex: :circular_buffer, repo: "hexpm", optional: false]}, {:igniter, "~> 0.6", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:phoenix_live_reload, ">= 1.6.1", [hex: :phoenix_live_reload, repo: "hexpm", optional: true]}, {:plug, "~> 1.18", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.9", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock_adapter, "~> 0.5", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "557b0f4e27c55b001506b3c3d3a904af04e6ca7914284ab654c91c9cecb3cbf1"},
|
||||
"uniq": {:hex, :uniq, "0.6.3", "68acff834cce1817b52928ef346662735c5413a4fec9c3b0d4a9126de5b2b489", [:mix], [{:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm", "2b2a900d0a20f3a55d3de0bc8150495e4a71255734dfb23889991bda5aca6c7d"},
|
||||
"websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"},
|
||||
"websock_adapter": {:hex, :websock_adapter, "0.6.0", "73db5ab8aaefd1a876a97ce3e6afc96562625de69ef17a4e04426e034849d0b8", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "50021a85bce8f203b086705d9e0c5415e2c7eb05d319111b0428fe71f9934617"},
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ defmodule Microwaveprop.Accounts.UserApiTokenTest do
|
|||
end
|
||||
|
||||
test "expires_at must be in the future", %{user: user} do
|
||||
past = DateTime.add(DateTime.utc_now(), -60, :second)
|
||||
past = DateTime.shift(DateTime.utc_now(), minute: -1)
|
||||
|
||||
assert {:error, changeset} =
|
||||
UserApiToken.build(user, %{name: "n", expires_at: past})
|
||||
|
|
@ -49,7 +49,7 @@ defmodule Microwaveprop.Accounts.UserApiTokenTest do
|
|||
end
|
||||
|
||||
test "expires_at in the future is accepted", %{user: user} do
|
||||
future = DateTime.add(DateTime.utc_now(), 3600, :second)
|
||||
future = DateTime.shift(DateTime.utc_now(), hour: 1)
|
||||
|
||||
assert {:ok, {_plaintext, changeset}} =
|
||||
UserApiToken.build(user, %{name: "n", expires_at: future})
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ defmodule Microwaveprop.Accounts.UserTokenTest do
|
|||
|
||||
test "uses the user's authenticated_at when present" do
|
||||
user = user_fixture()
|
||||
dt = DateTime.add(DateTime.utc_now(:second), -30, :minute)
|
||||
dt = DateTime.shift(DateTime.utc_now(:second), minute: -30)
|
||||
user = %{user | authenticated_at: dt}
|
||||
|
||||
{_token, user_token} = UserToken.build_session_token(user)
|
||||
|
|
@ -154,7 +154,7 @@ defmodule Microwaveprop.Accounts.UserTokenTest do
|
|||
# Magic link validity is 15 minutes.
|
||||
Repo.update_all(
|
||||
from(t in UserToken, where: t.token == ^raw),
|
||||
set: [inserted_at: DateTime.add(DateTime.utc_now(:second), -16, :minute)]
|
||||
set: [inserted_at: DateTime.shift(DateTime.utc_now(:second), minute: -16)]
|
||||
)
|
||||
|
||||
{:ok, query} = UserToken.verify_magic_link_token_query(encoded)
|
||||
|
|
@ -205,7 +205,7 @@ defmodule Microwaveprop.Accounts.UserTokenTest do
|
|||
test "returns nil when the token is older than 1 day", %{encoded: encoded, raw_token: raw} do
|
||||
Repo.update_all(
|
||||
from(t in UserToken, where: t.token == ^raw),
|
||||
set: [inserted_at: DateTime.add(DateTime.utc_now(:second), -2, :day)]
|
||||
set: [inserted_at: DateTime.shift(DateTime.utc_now(:second), day: -2)]
|
||||
)
|
||||
|
||||
{:ok, query} = UserToken.verify_confirm_token_query(encoded)
|
||||
|
|
@ -278,7 +278,7 @@ defmodule Microwaveprop.Accounts.UserTokenTest do
|
|||
} do
|
||||
Repo.update_all(
|
||||
from(t in UserToken, where: t.token == ^raw),
|
||||
set: [inserted_at: DateTime.add(DateTime.utc_now(:second), -8, :day)]
|
||||
set: [inserted_at: DateTime.shift(DateTime.utc_now(:second), day: -8)]
|
||||
)
|
||||
|
||||
{:ok, query} = UserToken.verify_change_email_token_query(encoded, context)
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ defmodule Microwaveprop.AccountsApiTokenTest do
|
|||
end
|
||||
|
||||
test "rejects expires_at in the past", %{user: user} do
|
||||
past = DateTime.add(DateTime.utc_now(), -1, :second)
|
||||
past = DateTime.shift(DateTime.utc_now(), second: -1)
|
||||
|
||||
assert {:error, changeset} =
|
||||
Accounts.create_api_token(user, %{name: "x", expires_at: past})
|
||||
|
|
@ -75,13 +75,13 @@ defmodule Microwaveprop.AccountsApiTokenTest do
|
|||
end
|
||||
|
||||
test "rejects expired tokens", %{user: user} do
|
||||
future = DateTime.add(DateTime.utc_now(), 60, :second)
|
||||
future = DateTime.shift(DateTime.utc_now(), minute: 1)
|
||||
{:ok, {plaintext, record}} = Accounts.create_api_token(user, %{name: "A", expires_at: future})
|
||||
|
||||
# Force the row past its expiry.
|
||||
Repo.update_all(
|
||||
from(t in UserApiToken, where: t.id == ^record.id),
|
||||
set: [expires_at: DateTime.utc_now() |> DateTime.add(-1, :second) |> DateTime.truncate(:second)]
|
||||
set: [expires_at: DateTime.utc_now() |> DateTime.shift(second: -1) |> DateTime.truncate(:second)]
|
||||
)
|
||||
|
||||
assert {:error, :invalid_token} = Accounts.get_user_by_api_token(plaintext)
|
||||
|
|
|
|||
|
|
@ -347,12 +347,12 @@ defmodule Microwaveprop.AccountsTest do
|
|||
now = DateTime.utc_now()
|
||||
|
||||
assert Accounts.sudo_mode?(%User{authenticated_at: DateTime.utc_now()})
|
||||
assert Accounts.sudo_mode?(%User{authenticated_at: DateTime.add(now, -19, :minute)})
|
||||
refute Accounts.sudo_mode?(%User{authenticated_at: DateTime.add(now, -21, :minute)})
|
||||
assert Accounts.sudo_mode?(%User{authenticated_at: DateTime.shift(now, minute: -19)})
|
||||
refute Accounts.sudo_mode?(%User{authenticated_at: DateTime.shift(now, minute: -21)})
|
||||
|
||||
# minute override
|
||||
refute Accounts.sudo_mode?(
|
||||
%User{authenticated_at: DateTime.add(now, -11, :minute)},
|
||||
%User{authenticated_at: DateTime.shift(now, minute: -11)},
|
||||
-10
|
||||
)
|
||||
|
||||
|
|
@ -529,7 +529,7 @@ defmodule Microwaveprop.AccountsTest do
|
|||
end
|
||||
|
||||
test "duplicates the authenticated_at of given user in new token", %{user: user} do
|
||||
user = %{user | authenticated_at: DateTime.add(DateTime.utc_now(:second), -3600)}
|
||||
user = %{user | authenticated_at: DateTime.shift(DateTime.utc_now(:second), hour: -1)}
|
||||
token = Accounts.generate_user_session_token(user)
|
||||
assert user_token = Repo.get_by(UserToken, token: token)
|
||||
assert user_token.authenticated_at == user.authenticated_at
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ defmodule Microwaveprop.BeaconMonitorsTest do
|
|||
# inserts can land in the same wall-clock second.
|
||||
{:ok, first} = BeaconMonitors.create_monitor(user, %{"name" => "First"})
|
||||
|
||||
backdated = DateTime.add(first.inserted_at, -1, :second)
|
||||
backdated = DateTime.shift(first.inserted_at, second: -1)
|
||||
|
||||
{1, _} =
|
||||
Microwaveprop.Repo.update_all(
|
||||
|
|
|
|||
|
|
@ -102,7 +102,7 @@ defmodule Microwaveprop.CommercialTest do
|
|||
{:ok, _} =
|
||||
Commercial.create_sample(%{
|
||||
link_id: link.id,
|
||||
sampled_at: DateTime.add(valid_time, -day_offset * 3600 * 24, :second),
|
||||
sampled_at: DateTime.shift(valid_time, day: -day_offset),
|
||||
rx_power_0: -52.0,
|
||||
rx_power_1: -52.0,
|
||||
link_state: 1
|
||||
|
|
@ -113,7 +113,7 @@ defmodule Microwaveprop.CommercialTest do
|
|||
{:ok, _} =
|
||||
Commercial.create_sample(%{
|
||||
link_id: link.id,
|
||||
sampled_at: DateTime.add(valid_time, -300, :second),
|
||||
sampled_at: DateTime.shift(valid_time, minute: -5),
|
||||
rx_power_0: -62.0,
|
||||
rx_power_1: -62.0,
|
||||
link_state: 1
|
||||
|
|
@ -134,7 +134,7 @@ defmodule Microwaveprop.CommercialTest do
|
|||
{:ok, _} =
|
||||
Commercial.create_sample(%{
|
||||
link_id: link.id,
|
||||
sampled_at: DateTime.add(valid_time, -day_offset * 3600 * 24, :second),
|
||||
sampled_at: DateTime.shift(valid_time, day: -day_offset),
|
||||
rx_power_0: -55.0,
|
||||
link_state: 1
|
||||
})
|
||||
|
|
@ -143,7 +143,7 @@ defmodule Microwaveprop.CommercialTest do
|
|||
{:ok, _} =
|
||||
Commercial.create_sample(%{
|
||||
link_id: link.id,
|
||||
sampled_at: DateTime.add(valid_time, -60, :second),
|
||||
sampled_at: DateTime.shift(valid_time, minute: -1),
|
||||
rx_power_0: -55.0,
|
||||
link_state: 1
|
||||
})
|
||||
|
|
@ -163,7 +163,7 @@ defmodule Microwaveprop.CommercialTest do
|
|||
{:ok, _} =
|
||||
Commercial.create_sample(%{
|
||||
link_id: link.id,
|
||||
sampled_at: DateTime.add(valid_time, -day_offset * 3600 * 24, :second),
|
||||
sampled_at: DateTime.shift(valid_time, day: -day_offset),
|
||||
rx_power_0: -52.0,
|
||||
link_state: 1
|
||||
})
|
||||
|
|
@ -172,7 +172,7 @@ defmodule Microwaveprop.CommercialTest do
|
|||
{:ok, _} =
|
||||
Commercial.create_sample(%{
|
||||
link_id: link.id,
|
||||
sampled_at: DateTime.add(valid_time, -120, :second),
|
||||
sampled_at: DateTime.shift(valid_time, minute: -2),
|
||||
rx_power_0: -90.0,
|
||||
link_state: 0
|
||||
})
|
||||
|
|
|
|||
|
|
@ -65,8 +65,8 @@ defmodule Microwaveprop.MoonTest do
|
|||
time_zone: "Etc/UTC"
|
||||
}
|
||||
|
||||
a = DateTime.add(base, day_offset * 86_400, :second)
|
||||
b = DateTime.add(a, delta_hours * 3600, :second)
|
||||
a = DateTime.shift(base, day: day_offset)
|
||||
b = DateTime.shift(a, hour: delta_hours)
|
||||
|
||||
assert Moon.julian_day(a) < Moon.julian_day(b)
|
||||
end
|
||||
|
|
@ -104,7 +104,7 @@ defmodule Microwaveprop.MoonTest do
|
|||
|
||||
property "distance, RA, and declination all stay in their physical envelopes for any UTC across the next century" do
|
||||
check all(seconds_from_now <- integer(0..3_155_760_000)) do
|
||||
dt = DateTime.add(~U[2024-01-01 00:00:00Z], seconds_from_now, :second)
|
||||
dt = DateTime.shift(~U[2024-01-01 00:00:00Z], second: seconds_from_now)
|
||||
g = Moon.geocentric(dt)
|
||||
|
||||
assert g.distance_km > 340_000 and g.distance_km < 420_000
|
||||
|
|
@ -143,7 +143,7 @@ defmodule Microwaveprop.MoonTest do
|
|||
lat <- float(min: -85.0, max: 85.0),
|
||||
lon <- float(min: -179.9, max: 179.9)
|
||||
) do
|
||||
dt = DateTime.add(~U[2024-01-01 00:00:00Z], seconds, :second)
|
||||
dt = DateTime.shift(~U[2024-01-01 00:00:00Z], second: seconds)
|
||||
p = Moon.observer_position(dt, lat, lon)
|
||||
assert p.azimuth >= 0.0 and p.azimuth < 360.0
|
||||
assert p.elevation >= -90.0 and p.elevation <= 90.0
|
||||
|
|
@ -178,7 +178,7 @@ defmodule Microwaveprop.MoonTest do
|
|||
|
||||
peaks =
|
||||
for hour_offset <- 0..48 do
|
||||
dt = DateTime.add(base, hour_offset * 3600, :second)
|
||||
dt = DateTime.shift(base, hour: hour_offset)
|
||||
{dt, Moon.observer_position(dt, lat, lon).elevation}
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ defmodule Microwaveprop.Propagation.FreshnessMonitorTest do
|
|||
|
||||
defp minutes_ago(n) do
|
||||
DateTime.utc_now()
|
||||
|> DateTime.add(-n, :minute)
|
||||
|> DateTime.shift(minute: -n)
|
||||
|> DateTime.truncate(:second)
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -123,7 +123,7 @@ defmodule Microwaveprop.Propagation.GridTaskEnqueuerTest do
|
|||
describe "reclaim_stale_running/1" do
|
||||
test "flips stale-running rows back to queued and clears claimed_at" do
|
||||
now = DateTime.truncate(DateTime.utc_now(), :second)
|
||||
stale = DateTime.add(now, -30 * 60, :second)
|
||||
stale = DateTime.shift(now, minute: -30)
|
||||
|
||||
insert_task(%{
|
||||
run_time: ~U[2026-04-19 20:00:00Z],
|
||||
|
|
@ -152,7 +152,7 @@ defmodule Microwaveprop.Propagation.GridTaskEnqueuerTest do
|
|||
|
||||
test "leaves recently-claimed running rows untouched" do
|
||||
now = DateTime.truncate(DateTime.utc_now(), :second)
|
||||
fresh = DateTime.add(now, -60, :second)
|
||||
fresh = DateTime.shift(now, minute: -1)
|
||||
|
||||
insert_task(%{
|
||||
run_time: ~U[2026-04-19 21:00:00Z],
|
||||
|
|
@ -178,7 +178,7 @@ defmodule Microwaveprop.Propagation.GridTaskEnqueuerTest do
|
|||
|
||||
test "flips stale rows past max attempts to failed" do
|
||||
now = DateTime.truncate(DateTime.utc_now(), :second)
|
||||
stale = DateTime.add(now, -30 * 60, :second)
|
||||
stale = DateTime.shift(now, minute: -30)
|
||||
|
||||
insert_task(%{
|
||||
run_time: ~U[2026-04-19 22:00:00Z],
|
||||
|
|
@ -206,7 +206,7 @@ defmodule Microwaveprop.Propagation.GridTaskEnqueuerTest do
|
|||
|
||||
test "does not touch queued or done rows even if claimed_at is ancient" do
|
||||
now = DateTime.truncate(DateTime.utc_now(), :second)
|
||||
stale = DateTime.add(now, -30 * 60, :second)
|
||||
stale = DateTime.shift(now, minute: -30)
|
||||
|
||||
insert_task(%{
|
||||
run_time: ~U[2026-04-19 23:00:00Z],
|
||||
|
|
@ -226,7 +226,7 @@ defmodule Microwaveprop.Propagation.GridTaskEnqueuerTest do
|
|||
test "seeds exactly one forecast row at the hour offset closest to now" do
|
||||
# run_time 5h before "now" → fh should be 5.
|
||||
now = DateTime.utc_now() |> DateTime.truncate(:second) |> Map.put(:minute, 0) |> Map.put(:second, 0)
|
||||
run_time = DateTime.add(now, -5 * 3600, :second)
|
||||
run_time = DateTime.shift(now, hour: -5)
|
||||
|
||||
assert {:ok, 1} = GridTaskEnqueuer.seed_current_hour(run_time, source: "hrdps")
|
||||
|
||||
|
|
@ -247,7 +247,7 @@ defmodule Microwaveprop.Propagation.GridTaskEnqueuerTest do
|
|||
|
||||
test "is idempotent on the (run_time, fh, kind, source) key" do
|
||||
now = DateTime.utc_now() |> DateTime.truncate(:second) |> Map.put(:minute, 0) |> Map.put(:second, 0)
|
||||
run_time = DateTime.add(now, -3 * 3600, :second)
|
||||
run_time = DateTime.shift(now, hour: -3)
|
||||
|
||||
assert {:ok, 1} = GridTaskEnqueuer.seed_current_hour(run_time, source: "hrdps")
|
||||
assert {:ok, 0} = GridTaskEnqueuer.seed_current_hour(run_time, source: "hrdps")
|
||||
|
|
@ -268,7 +268,7 @@ defmodule Microwaveprop.Propagation.GridTaskEnqueuerTest do
|
|||
# negative. Clamp pulls it to 1 so the F00Reserved branch in Rust
|
||||
# is never hit through this path.
|
||||
now = DateTime.utc_now() |> DateTime.truncate(:second) |> Map.put(:minute, 0) |> Map.put(:second, 0)
|
||||
run_time = DateTime.add(now, 3600, :second)
|
||||
run_time = DateTime.shift(now, hour: 1)
|
||||
|
||||
assert {:ok, 1} = GridTaskEnqueuer.seed_current_hour(run_time, source: "hrdps")
|
||||
|
||||
|
|
|
|||
|
|
@ -90,7 +90,7 @@ defmodule Microwaveprop.Propagation.NotifyListenerTest do
|
|||
|
||||
test "retains GEFS score files through 168 hours" do
|
||||
run_time = ~U[2026-04-21 00:00:00Z]
|
||||
extended_time = DateTime.add(run_time, 168, :hour)
|
||||
extended_time = DateTime.shift(run_time, week: 1)
|
||||
ScoresFile.write_gefs!(10_000, extended_time, sample_scores())
|
||||
|
||||
{:ok, _task_pid} = NotifyListener.handle_propagation_ready(run_time, run_time)
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ defmodule Microwaveprop.Propagation.PipelineStatusTest do
|
|||
end
|
||||
|
||||
defp minutes_ago(n) do
|
||||
DateTime.add(DateTime.utc_now(), -n * 60, :second)
|
||||
DateTime.shift(DateTime.utc_now(), minute: -n)
|
||||
end
|
||||
|
||||
describe "current/0" do
|
||||
|
|
@ -183,24 +183,24 @@ defmodule Microwaveprop.Propagation.PipelineStatusTest do
|
|||
test "valid_time within half an hour of now renders as 'through now'" do
|
||||
now = DateTime.utc_now()
|
||||
assert PipelineStatus.running_detail(%{valid_time: now}) == "through now"
|
||||
assert PipelineStatus.running_detail(%{valid_time: DateTime.add(now, 20 * 60, :second)}) == "through now"
|
||||
assert PipelineStatus.running_detail(%{valid_time: DateTime.add(now, -20 * 60, :second)}) == "through now"
|
||||
assert PipelineStatus.running_detail(%{valid_time: DateTime.shift(now, minute: 20)}) == "through now"
|
||||
assert PipelineStatus.running_detail(%{valid_time: DateTime.shift(now, minute: -20)}) == "through now"
|
||||
end
|
||||
|
||||
test "valid_time in the future renders as 'through +Nh'" do
|
||||
now = DateTime.utc_now()
|
||||
|
||||
assert PipelineStatus.running_detail(%{valid_time: DateTime.add(now, 3 * 3600, :second)}) ==
|
||||
assert PipelineStatus.running_detail(%{valid_time: DateTime.shift(now, hour: 3)}) ==
|
||||
"through +3h"
|
||||
|
||||
assert PipelineStatus.running_detail(%{valid_time: DateTime.add(now, 12 * 3600, :second)}) ==
|
||||
assert PipelineStatus.running_detail(%{valid_time: DateTime.shift(now, hour: 12)}) ==
|
||||
"through +12h"
|
||||
end
|
||||
|
||||
test "valid_time in the past renders as 'through Nh ago'" do
|
||||
now = DateTime.utc_now()
|
||||
|
||||
assert PipelineStatus.running_detail(%{valid_time: DateTime.add(now, -2 * 3600, :second)}) ==
|
||||
assert PipelineStatus.running_detail(%{valid_time: DateTime.shift(now, hour: -2)}) ==
|
||||
"through 2h ago"
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -236,7 +236,7 @@ defmodule Microwaveprop.Propagation.ScoreCacheTest do
|
|||
|
||||
# Insert cap+1 entries, each at a distinct valid_time, oldest first.
|
||||
for hour <- 0..cap do
|
||||
ScoreCache.put(10_000, DateTime.add(base, hour * 3600, :second), score)
|
||||
ScoreCache.put(10_000, DateTime.shift(base, hour: hour), score)
|
||||
end
|
||||
|
||||
# Size never exceeds the cap.
|
||||
|
|
@ -246,7 +246,7 @@ defmodule Microwaveprop.Propagation.ScoreCacheTest do
|
|||
assert ScoreCache.fetch(10_000, base) == :miss
|
||||
|
||||
# The newest is retained.
|
||||
newest = DateTime.add(base, cap * 3600, :second)
|
||||
newest = DateTime.shift(base, hour: cap)
|
||||
assert {:ok, [%{score: 1}]} = ScoreCache.fetch(10_000, newest)
|
||||
end
|
||||
|
||||
|
|
@ -257,14 +257,14 @@ defmodule Microwaveprop.Propagation.ScoreCacheTest do
|
|||
|
||||
# Fill cap entries on band A at older valid_times.
|
||||
for hour <- 0..(cap - 1) do
|
||||
ScoreCache.put(10_000, DateTime.add(base, hour * 3600, :second), score)
|
||||
ScoreCache.put(10_000, DateTime.shift(base, hour: hour), score)
|
||||
end
|
||||
|
||||
assert :ets.info(:propagation_score_cache, :size) == cap
|
||||
|
||||
# One more on band B at a newer valid_time should evict the oldest
|
||||
# band-A entry, not the new band-B one.
|
||||
newer = DateTime.add(base, cap * 3600, :second)
|
||||
newer = DateTime.shift(base, hour: cap)
|
||||
ScoreCache.put(24_000, newer, score)
|
||||
|
||||
assert :ets.info(:propagation_score_cache, :size) == cap
|
||||
|
|
|
|||
|
|
@ -351,8 +351,8 @@ defmodule Microwaveprop.Propagation.ScoresFileTest do
|
|||
describe "retain_window/2" do
|
||||
test "recognizes GEFS files through the extended horizon" do
|
||||
run_time = ~U[2026-04-14 00:00:00Z]
|
||||
inside = DateTime.add(run_time, 168, :hour)
|
||||
outside = DateTime.add(run_time, 174, :hour)
|
||||
inside = DateTime.shift(run_time, week: 1)
|
||||
outside = DateTime.shift(run_time, hour: 174)
|
||||
|
||||
ScoresFile.write_gefs!(10_000, inside, [])
|
||||
ScoresFile.write_gefs!(10_000, outside, [])
|
||||
|
|
|
|||
|
|
@ -405,7 +405,7 @@ defmodule Microwaveprop.PropagationTest do
|
|||
end
|
||||
|
||||
test "returns empty factors when the only persisted profile is older than the 24h lookback" do
|
||||
ancient = DateTime.add(~U[2026-07-15 13:00:00Z], -30 * 3600, :second)
|
||||
ancient = DateTime.shift(~U[2026-07-15 13:00:00Z], hour: -30)
|
||||
forecast_time = ~U[2026-07-15 13:00:00Z]
|
||||
lat = 32.75
|
||||
lon = -97.125
|
||||
|
|
@ -425,7 +425,7 @@ defmodule Microwaveprop.PropagationTest do
|
|||
end
|
||||
|
||||
test "accepts a profile exactly at the 24h lookback boundary" do
|
||||
fallback_time = DateTime.add(~U[2026-07-15 13:00:00Z], -24 * 3600, :second)
|
||||
fallback_time = DateTime.shift(~U[2026-07-15 13:00:00Z], day: -1)
|
||||
forecast_time = ~U[2026-07-15 13:00:00Z]
|
||||
lat = 32.75
|
||||
lon = -97.125
|
||||
|
|
@ -708,8 +708,8 @@ defmodule Microwaveprop.PropagationTest do
|
|||
end
|
||||
|
||||
test "returns forecast timeline from cached scores when warm" do
|
||||
t1 = DateTime.truncate(DateTime.add(DateTime.utc_now(), 3600, :second), :second)
|
||||
t2 = DateTime.add(t1, 3600, :second)
|
||||
t1 = DateTime.truncate(DateTime.shift(DateTime.utc_now(), hour: 1), :second)
|
||||
t2 = DateTime.shift(t1, hour: 1)
|
||||
|
||||
seed_point(10_000, t1, 32.875, -97.0, 70)
|
||||
seed_point(10_000, t2, 32.875, -97.0, 75)
|
||||
|
|
@ -725,9 +725,9 @@ defmodule Microwaveprop.PropagationTest do
|
|||
test "filters valid_times older than one hour but keeps recent past as the 'now' anchor" do
|
||||
# Two hours old: dropped. Thirty minutes old: kept as the
|
||||
# leftmost "now" point. One hour future: kept.
|
||||
stale = DateTime.utc_now() |> DateTime.add(-2 * 3600, :second) |> DateTime.truncate(:second)
|
||||
recent = DateTime.utc_now() |> DateTime.add(-1800, :second) |> DateTime.truncate(:second)
|
||||
future = DateTime.utc_now() |> DateTime.add(3600, :second) |> DateTime.truncate(:second)
|
||||
stale = DateTime.utc_now() |> DateTime.shift(hour: -2) |> DateTime.truncate(:second)
|
||||
recent = DateTime.utc_now() |> DateTime.shift(minute: -30) |> DateTime.truncate(:second)
|
||||
future = DateTime.utc_now() |> DateTime.shift(hour: 1) |> DateTime.truncate(:second)
|
||||
|
||||
seed_point(10_000, stale, 32.875, -97.0, 40)
|
||||
seed_point(10_000, recent, 32.875, -97.0, 60)
|
||||
|
|
@ -745,8 +745,8 @@ defmodule Microwaveprop.PropagationTest do
|
|||
# HRRR just cut out (nothing fresh); we still want the chart to
|
||||
# render one point at the newest time we have, matching
|
||||
# available_valid_times semantics.
|
||||
older = DateTime.utc_now() |> DateTime.add(-6 * 3600, :second) |> DateTime.truncate(:second)
|
||||
newer = DateTime.utc_now() |> DateTime.add(-3 * 3600, :second) |> DateTime.truncate(:second)
|
||||
older = DateTime.utc_now() |> DateTime.shift(hour: -6) |> DateTime.truncate(:second)
|
||||
newer = DateTime.utc_now() |> DateTime.shift(hour: -3) |> DateTime.truncate(:second)
|
||||
|
||||
seed_point(10_000, older, 32.875, -97.0, 30)
|
||||
seed_point(10_000, newer, 32.875, -97.0, 45)
|
||||
|
|
@ -756,7 +756,7 @@ defmodule Microwaveprop.PropagationTest do
|
|||
end
|
||||
|
||||
test "snaps coordinates to the nearest grid point" do
|
||||
valid_time = DateTime.truncate(DateTime.add(DateTime.utc_now(), 3600, :second), :second)
|
||||
valid_time = DateTime.truncate(DateTime.shift(DateTime.utc_now(), hour: 1), :second)
|
||||
seed_point(10_000, valid_time, 32.875, -97.0, 65)
|
||||
|
||||
# Off-grid query snaps to 32.875, -97.0
|
||||
|
|
@ -775,8 +775,8 @@ defmodule Microwaveprop.PropagationTest do
|
|||
# hour has just landed on disk. The forecast chart should include
|
||||
# that third hour so it matches the main-map timeline.
|
||||
t1 = DateTime.utc_now() |> DateTime.truncate(:second) |> Map.merge(%{minute: 0, second: 0})
|
||||
t2 = DateTime.add(t1, 3600, :second)
|
||||
t3 = DateTime.add(t1, 2 * 3600, :second)
|
||||
t2 = DateTime.shift(t1, hour: 1)
|
||||
t3 = DateTime.shift(t1, hour: 2)
|
||||
|
||||
for {t, score} <- [{t1, 50}, {t2, 60}, {t3, 70}] do
|
||||
Propagation.replace_scores(
|
||||
|
|
@ -801,9 +801,9 @@ defmodule Microwaveprop.PropagationTest do
|
|||
test "includes recent past valid_times and falls back to the latest when stale" do
|
||||
# Three .prop files on disk: one 2h old (dropped by cutoff),
|
||||
# one 30min old (kept as "now"), one 1h future (kept).
|
||||
stale = DateTime.utc_now() |> DateTime.add(-2 * 3600, :second) |> DateTime.truncate(:second)
|
||||
recent = DateTime.utc_now() |> DateTime.add(-1800, :second) |> DateTime.truncate(:second)
|
||||
future = DateTime.utc_now() |> DateTime.add(3600, :second) |> DateTime.truncate(:second)
|
||||
stale = DateTime.utc_now() |> DateTime.shift(hour: -2) |> DateTime.truncate(:second)
|
||||
recent = DateTime.utc_now() |> DateTime.shift(minute: -30) |> DateTime.truncate(:second)
|
||||
future = DateTime.utc_now() |> DateTime.shift(hour: 1) |> DateTime.truncate(:second)
|
||||
|
||||
Enum.each([{stale, 40}, {recent, 60}, {future, 80}], fn {t, score} ->
|
||||
Propagation.replace_scores(
|
||||
|
|
@ -824,8 +824,8 @@ defmodule Microwaveprop.PropagationTest do
|
|||
end
|
||||
|
||||
test "returns just the latest store entry when every hour is stale" do
|
||||
older = DateTime.utc_now() |> DateTime.add(-6 * 3600, :second) |> DateTime.truncate(:second)
|
||||
newer = DateTime.utc_now() |> DateTime.add(-3 * 3600, :second) |> DateTime.truncate(:second)
|
||||
older = DateTime.utc_now() |> DateTime.shift(hour: -6) |> DateTime.truncate(:second)
|
||||
newer = DateTime.utc_now() |> DateTime.shift(hour: -3) |> DateTime.truncate(:second)
|
||||
|
||||
Enum.each([{older, 30}, {newer, 45}], fn {t, score} ->
|
||||
Propagation.replace_scores(
|
||||
|
|
@ -856,8 +856,8 @@ defmodule Microwaveprop.PropagationTest do
|
|||
|
||||
test "caps the forward horizon to 48 hours so stale cycles don't clutter the timeline" do
|
||||
now = DateTime.truncate(DateTime.utc_now(), :second)
|
||||
in_range = DateTime.add(now, 3600, :second)
|
||||
past_hrrr_horizon = DateTime.add(now, 55 * 3600, :second)
|
||||
in_range = DateTime.shift(now, hour: 1)
|
||||
past_hrrr_horizon = DateTime.shift(now, hour: 55)
|
||||
|
||||
Enum.each([{in_range, 70}, {past_hrrr_horizon, 50}], fn {t, score} ->
|
||||
Propagation.replace_scores(
|
||||
|
|
@ -873,8 +873,8 @@ defmodule Microwaveprop.PropagationTest do
|
|||
|
||||
test "filters valid_times older than the 1-hour cutoff" do
|
||||
now = DateTime.truncate(DateTime.utc_now(), :second)
|
||||
stale = DateTime.add(now, -7200, :second)
|
||||
fresh = DateTime.add(now, 1800, :second)
|
||||
stale = DateTime.shift(now, hour: -2)
|
||||
fresh = DateTime.shift(now, minute: 30)
|
||||
|
||||
Propagation.replace_scores(
|
||||
[%{lat: 25.0, lon: -125.0, valid_time: stale, band_mhz: 10_000, score: 50, factors: nil}],
|
||||
|
|
@ -1039,7 +1039,7 @@ defmodule Microwaveprop.PropagationTest do
|
|||
Propagation.record_run_timing(%{
|
||||
run_time: run_time,
|
||||
forecast_hour: 3,
|
||||
valid_time: DateTime.add(run_time, 3, :hour),
|
||||
valid_time: DateTime.shift(run_time, hour: 3),
|
||||
started_at: started,
|
||||
finished_at: finished,
|
||||
duration_ms: 555_000,
|
||||
|
|
@ -1061,7 +1061,7 @@ defmodule Microwaveprop.PropagationTest do
|
|||
Propagation.record_run_timing(%{
|
||||
run_time: run_time,
|
||||
forecast_hour: 5,
|
||||
valid_time: DateTime.add(run_time, 5, :hour),
|
||||
valid_time: DateTime.shift(run_time, hour: 5),
|
||||
started_at: ~U[2026-07-15 12:00:00Z],
|
||||
finished_at: ~U[2026-07-15 12:02:30Z],
|
||||
duration_ms: 150_000,
|
||||
|
|
@ -1108,9 +1108,9 @@ defmodule Microwaveprop.PropagationTest do
|
|||
Propagation.record_run_timing(%{
|
||||
run_time: run_time,
|
||||
forecast_hour: fh,
|
||||
valid_time: DateTime.add(run_time, fh, :hour),
|
||||
started_at: DateTime.add(run_time, fh * 600, :second),
|
||||
finished_at: DateTime.add(run_time, fh * 600 + 500, :second),
|
||||
valid_time: DateTime.shift(run_time, hour: fh),
|
||||
started_at: DateTime.shift(run_time, minute: fh * 10),
|
||||
finished_at: DateTime.shift(run_time, second: fh * 600 + 500),
|
||||
duration_ms: 500_000,
|
||||
status: :ok
|
||||
})
|
||||
|
|
|
|||
|
|
@ -77,7 +77,7 @@ defmodule Microwaveprop.QrzTest do
|
|||
test "re-fetches when cache entry is stale" do
|
||||
past =
|
||||
DateTime.utc_now()
|
||||
|> DateTime.add(-48, :hour)
|
||||
|> DateTime.shift(day: -2)
|
||||
|> DateTime.truncate(:second)
|
||||
|
||||
Repo.insert!(%Callsign{
|
||||
|
|
@ -96,7 +96,7 @@ defmodule Microwaveprop.QrzTest do
|
|||
test "upserts (updates existing record on re-fetch)" do
|
||||
past =
|
||||
DateTime.utc_now()
|
||||
|> DateTime.add(-48, :hour)
|
||||
|> DateTime.shift(day: -2)
|
||||
|> DateTime.truncate(:second)
|
||||
|
||||
original =
|
||||
|
|
@ -157,7 +157,7 @@ defmodule Microwaveprop.QrzTest do
|
|||
test "returns record regardless of TTL" do
|
||||
past =
|
||||
DateTime.utc_now()
|
||||
|> DateTime.add(-999, :hour)
|
||||
|> DateTime.shift(hour: -999)
|
||||
|> DateTime.truncate(:second)
|
||||
|
||||
Repo.insert!(%Callsign{
|
||||
|
|
|
|||
|
|
@ -247,7 +247,7 @@ defmodule Microwaveprop.Radio.ContactSubmissionTest do
|
|||
test "allows contact outside the 1-hour window" do
|
||||
assert {:ok, _} = Radio.create_contact(@valid_attrs)
|
||||
|
||||
later = DateTime.add(@valid_attrs.qso_timestamp, 3601, :second)
|
||||
later = DateTime.shift(@valid_attrs.qso_timestamp, second: 3_601)
|
||||
assert {:ok, _} = Radio.create_contact(%{@valid_attrs | qso_timestamp: later})
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -2,36 +2,12 @@ defmodule Microwaveprop.RadioTest do
|
|||
use Microwaveprop.DataCase, async: true
|
||||
|
||||
import Microwaveprop.AccountsFixtures
|
||||
import Microwaveprop.ContactsFixtures
|
||||
|
||||
alias Microwaveprop.Accounts.Scope
|
||||
alias Microwaveprop.Radio
|
||||
alias Microwaveprop.Radio.Contact
|
||||
|
||||
defp create_contact(attrs \\ %{}) do
|
||||
default = %{
|
||||
station1: "W5XD",
|
||||
station2: "K5TR",
|
||||
qso_timestamp: ~U[2026-03-28 18:00:00Z],
|
||||
mode: "CW",
|
||||
band: Decimal.new("1296"),
|
||||
grid1: "EM12",
|
||||
grid2: "EM00",
|
||||
pos1: %{"lat" => 32.9, "lon" => -97.0},
|
||||
pos2: %{"lat" => 30.3, "lon" => -97.7},
|
||||
distance_km: Decimal.new("295")
|
||||
}
|
||||
|
||||
merged = Map.merge(default, attrs)
|
||||
{user_id, changeset_attrs} = Map.pop(merged, :user_id)
|
||||
|
||||
{:ok, contact} =
|
||||
%Contact{user_id: user_id}
|
||||
|> Contact.changeset(changeset_attrs)
|
||||
|> Repo.insert()
|
||||
|
||||
contact
|
||||
end
|
||||
|
||||
describe "list_contacts/1" do
|
||||
test "returns empty page when no QSOs exist" do
|
||||
result = Radio.list_contacts()
|
||||
|
|
@ -44,7 +20,7 @@ defmodule Microwaveprop.RadioTest do
|
|||
|
||||
test "paginates 20 per page, ordered by qso_timestamp desc" do
|
||||
for i <- 1..25 do
|
||||
ts = DateTime.add(~U[2026-01-01 00:00:00Z], i * 3600, :second)
|
||||
ts = DateTime.shift(~U[2026-01-01 00:00:00Z], hour: i)
|
||||
create_contact(%{qso_timestamp: ts})
|
||||
end
|
||||
|
||||
|
|
@ -57,12 +33,12 @@ defmodule Microwaveprop.RadioTest do
|
|||
|
||||
# Most recent first
|
||||
first = hd(result.entries)
|
||||
assert first.qso_timestamp == DateTime.add(~U[2026-01-01 00:00:00Z], 25 * 3600, :second)
|
||||
assert first.qso_timestamp == DateTime.shift(~U[2026-01-01 00:00:00Z], hour: 25)
|
||||
end
|
||||
|
||||
test "returns second page" do
|
||||
for i <- 1..25 do
|
||||
ts = DateTime.add(~U[2026-01-01 00:00:00Z], i * 3600, :second)
|
||||
ts = DateTime.shift(~U[2026-01-01 00:00:00Z], hour: i)
|
||||
create_contact(%{qso_timestamp: ts})
|
||||
end
|
||||
|
||||
|
|
@ -75,7 +51,7 @@ defmodule Microwaveprop.RadioTest do
|
|||
|
||||
test "honors per_page option" do
|
||||
for i <- 1..25 do
|
||||
ts = DateTime.add(~U[2026-01-01 00:00:00Z], i * 3600, :second)
|
||||
ts = DateTime.shift(~U[2026-01-01 00:00:00Z], hour: i)
|
||||
create_contact(%{qso_timestamp: ts})
|
||||
end
|
||||
|
||||
|
|
@ -87,7 +63,7 @@ defmodule Microwaveprop.RadioTest do
|
|||
|
||||
test "clamps per_page to the maximum" do
|
||||
for i <- 1..3 do
|
||||
ts = DateTime.add(~U[2026-01-01 00:00:00Z], i * 3600, :second)
|
||||
ts = DateTime.shift(~U[2026-01-01 00:00:00Z], hour: i)
|
||||
create_contact(%{qso_timestamp: ts})
|
||||
end
|
||||
|
||||
|
|
@ -98,7 +74,7 @@ defmodule Microwaveprop.RadioTest do
|
|||
|
||||
test "falls back to default when per_page is invalid" do
|
||||
for i <- 1..25 do
|
||||
ts = DateTime.add(~U[2026-01-01 00:00:00Z], i * 3600, :second)
|
||||
ts = DateTime.shift(~U[2026-01-01 00:00:00Z], hour: i)
|
||||
create_contact(%{qso_timestamp: ts})
|
||||
end
|
||||
|
||||
|
|
@ -114,7 +90,13 @@ defmodule Microwaveprop.RadioTest do
|
|||
|
||||
result = Radio.list_contacts(sort_by: :station1, sort_order: :asc)
|
||||
|
||||
stations = Enum.map(result.entries, & &1.station1)
|
||||
# Filter to only the contacts created in this test so parallel
|
||||
# async tests don't affect the assertion.
|
||||
stations =
|
||||
result.entries
|
||||
|> Enum.filter(&(&1.station1 in ["AA1AA", "ZZ9ZZ"]))
|
||||
|> Enum.map(& &1.station1)
|
||||
|
||||
assert stations == ["AA1AA", "ZZ9ZZ"]
|
||||
end
|
||||
|
||||
|
|
@ -124,7 +106,13 @@ defmodule Microwaveprop.RadioTest do
|
|||
|
||||
result = Radio.list_contacts(sort_by: :distance_km, sort_order: :desc)
|
||||
|
||||
distances = Enum.map(result.entries, & &1.distance_km)
|
||||
# Filter to only the contacts created in this test so parallel
|
||||
# async tests don't affect the assertion.
|
||||
distances =
|
||||
result.entries
|
||||
|> Enum.filter(&(&1.station1 in ["A1A", "B2B"]))
|
||||
|> Enum.map(& &1.distance_km)
|
||||
|
||||
assert distances == [Decimal.new("500"), Decimal.new("100")]
|
||||
end
|
||||
|
||||
|
|
@ -134,7 +122,9 @@ defmodule Microwaveprop.RadioTest do
|
|||
|
||||
result = Radio.list_contacts(sort_by: :bogus)
|
||||
|
||||
ids = Enum.map(result.entries, & &1.id)
|
||||
# Filter to only the contacts created in this test.
|
||||
our_ids = MapSet.new([early.id, late.id])
|
||||
ids = result.entries |> Enum.filter(&MapSet.member?(our_ids, &1.id)) |> Enum.map(& &1.id)
|
||||
assert ids == [late.id, early.id]
|
||||
end
|
||||
end
|
||||
|
|
@ -167,7 +157,7 @@ defmodule Microwaveprop.RadioTest do
|
|||
|
||||
test "respects limit parameter" do
|
||||
for i <- 1..5 do
|
||||
ts = DateTime.add(~U[2026-01-01 00:00:00Z], i * 3600, :second)
|
||||
ts = DateTime.shift(~U[2026-01-01 00:00:00Z], hour: i)
|
||||
create_contact(%{qso_timestamp: ts})
|
||||
end
|
||||
|
||||
|
|
@ -296,7 +286,7 @@ defmodule Microwaveprop.RadioTest do
|
|||
|
||||
test "respects limit parameter" do
|
||||
for i <- 1..5 do
|
||||
ts = DateTime.add(~U[2026-01-01 00:00:00Z], i * 3600, :second)
|
||||
ts = DateTime.shift(~U[2026-01-01 00:00:00Z], hour: i)
|
||||
create_contact(%{qso_timestamp: ts})
|
||||
end
|
||||
|
||||
|
|
@ -360,7 +350,7 @@ defmodule Microwaveprop.RadioTest do
|
|||
|
||||
test "respects limit parameter" do
|
||||
for i <- 1..5 do
|
||||
ts = DateTime.add(~U[2026-01-01 00:00:00Z], i * 3600, :second)
|
||||
ts = DateTime.shift(~U[2026-01-01 00:00:00Z], hour: i)
|
||||
create_contact(%{qso_timestamp: ts})
|
||||
end
|
||||
|
||||
|
|
@ -763,7 +753,7 @@ defmodule Microwaveprop.RadioTest do
|
|||
create_contact(%{
|
||||
station1: "W5ISP",
|
||||
station2: "K5X#{i}",
|
||||
qso_timestamp: DateTime.add(base, i * 3600, :second)
|
||||
qso_timestamp: DateTime.shift(base, hour: i)
|
||||
})
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -77,7 +77,7 @@ defmodule Microwaveprop.Weather.GefsProfileTest do
|
|||
test "inserts many rows at once" do
|
||||
attrs =
|
||||
for fh <- [3, 6, 9] do
|
||||
valid = DateTime.add(~U[2026-04-18 12:00:00Z], fh * 3600, :second)
|
||||
valid = DateTime.shift(~U[2026-04-18 12:00:00Z], hour: fh)
|
||||
base_attrs(%{valid_time: valid, forecast_hour: fh})
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -165,7 +165,7 @@ defmodule Microwaveprop.Weather.GridSnapPropertyTest do
|
|||
property "coverage_end is the first excluded timestamp (exclusive upper bound)" do
|
||||
coverage_end = NarrClient.coverage_end()
|
||||
refute NarrClient.in_coverage?(coverage_end)
|
||||
one_sec_before = DateTime.add(coverage_end, -1, :second)
|
||||
one_sec_before = DateTime.shift(coverage_end, second: -1)
|
||||
assert NarrClient.in_coverage?(one_sec_before)
|
||||
end
|
||||
|
||||
|
|
@ -173,7 +173,7 @@ defmodule Microwaveprop.Weather.GridSnapPropertyTest do
|
|||
coverage_end = NarrClient.coverage_end()
|
||||
|
||||
check all(seconds_after <- integer(1..100_000_000)) do
|
||||
dt = DateTime.add(coverage_end, seconds_after, :second)
|
||||
dt = DateTime.shift(coverage_end, second: seconds_after)
|
||||
refute NarrClient.in_coverage?(dt)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ defmodule Microwaveprop.Weather.HrrrProfileLookupTest do
|
|||
|
||||
test "limit option caps the number of times returned (most recent first kept)" do
|
||||
for h <- 0..4 do
|
||||
valid_time = DateTime.add(~U[2026-04-25 12:00:00Z], h * 3600, :second)
|
||||
valid_time = DateTime.shift(~U[2026-04-25 12:00:00Z], hour: h)
|
||||
insert_profile!(%{lat: 32.625, lon: -97.125, valid_time: valid_time})
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -51,19 +51,19 @@ defmodule Microwaveprop.Weather.NexradCacheTest do
|
|||
base = ~U[2026-04-12 00:00:00Z]
|
||||
|
||||
for i <- 1..25 do
|
||||
ts = DateTime.add(base, i * 300, :second)
|
||||
ts = DateTime.shift(base, minute: i * 5)
|
||||
NexradCache.put(ts, <<i>>, 1)
|
||||
end
|
||||
|
||||
# The 5 oldest (i = 1..5) should have been evicted.
|
||||
for i <- 1..5 do
|
||||
ts = DateTime.add(base, i * 300, :second)
|
||||
ts = DateTime.shift(base, minute: i * 5)
|
||||
assert NexradCache.fetch(ts) == :miss
|
||||
end
|
||||
|
||||
# The most recent 20 should still be present.
|
||||
for i <- 6..25 do
|
||||
ts = DateTime.add(base, i * 300, :second)
|
||||
ts = DateTime.shift(base, minute: i * 5)
|
||||
assert {:ok, <<^i>>, 1} = NexradCache.fetch(ts)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -90,7 +90,7 @@ defmodule Microwaveprop.WeatherGridTest do
|
|||
end
|
||||
|
||||
test "uses most recent valid_time only" do
|
||||
old = DateTime.utc_now() |> DateTime.add(-7200, :second) |> DateTime.truncate(:second)
|
||||
old = DateTime.utc_now() |> DateTime.shift(hour: -2) |> DateTime.truncate(:second)
|
||||
new = DateTime.truncate(DateTime.utc_now(), :second)
|
||||
|
||||
insert_hrrr_profile(%{
|
||||
|
|
|
|||
|
|
@ -336,7 +336,7 @@ defmodule Microwaveprop.WeatherTest do
|
|||
rows =
|
||||
for i <- 0..99 do
|
||||
%{
|
||||
observed_at: DateTime.add(~U[2026-03-28 00:00:00Z], i * 300, :second),
|
||||
observed_at: DateTime.shift(~U[2026-03-28 00:00:00Z], minute: i * 5),
|
||||
temp_f: 40.0 + i / 10,
|
||||
dewpoint_f: 30.0 + i / 10,
|
||||
sky_condition: "CLR"
|
||||
|
|
@ -354,7 +354,7 @@ defmodule Microwaveprop.WeatherTest do
|
|||
rows1 =
|
||||
for i <- 0..9 do
|
||||
%{
|
||||
observed_at: DateTime.add(~U[2026-03-28 00:00:00Z], i * 300, :second),
|
||||
observed_at: DateTime.shift(~U[2026-03-28 00:00:00Z], minute: i * 5),
|
||||
temp_f: 40.0,
|
||||
dewpoint_f: 30.0
|
||||
}
|
||||
|
|
@ -365,7 +365,7 @@ defmodule Microwaveprop.WeatherTest do
|
|||
rows2 =
|
||||
for i <- 0..9 do
|
||||
%{
|
||||
observed_at: DateTime.add(~U[2026-03-28 00:00:00Z], i * 300, :second),
|
||||
observed_at: DateTime.shift(~U[2026-03-28 00:00:00Z], minute: i * 5),
|
||||
temp_f: 80.0,
|
||||
dewpoint_f: 60.0
|
||||
}
|
||||
|
|
|
|||
|
|
@ -295,7 +295,7 @@ defmodule Microwaveprop.Workers.AdminTaskWorkerTest do
|
|||
for i <- 1..2 do
|
||||
%HrrrNativeProfile{}
|
||||
|> HrrrNativeProfile.changeset(%{
|
||||
valid_time: DateTime.add(~U[2026-04-15 18:00:00Z], i * 3600, :second),
|
||||
valid_time: DateTime.shift(~U[2026-04-15 18:00:00Z], hour: i),
|
||||
lat: 32.9 + i * 0.1,
|
||||
lon: -97.0,
|
||||
level_count: 3,
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ defmodule Microwaveprop.Workers.BackfillEnqueueWorkerTest do
|
|||
use Microwaveprop.DataCase, async: true
|
||||
|
||||
import Ecto.Query
|
||||
import Microwaveprop.ContactsFixtures
|
||||
|
||||
alias Microwaveprop.Radio.Contact
|
||||
alias Microwaveprop.Terrain.ElevationClient
|
||||
|
|
@ -10,28 +11,6 @@ defmodule Microwaveprop.Workers.BackfillEnqueueWorkerTest do
|
|||
alias Microwaveprop.Weather.NarrClient
|
||||
alias Microwaveprop.Workers.BackfillEnqueueWorker
|
||||
|
||||
defp create_contact(attrs \\ %{}) do
|
||||
default = %{
|
||||
station1: "W5XD",
|
||||
station2: "K5TR",
|
||||
qso_timestamp: ~U[2026-03-28 18:00:00Z],
|
||||
mode: "CW",
|
||||
band: Decimal.new("1296"),
|
||||
grid1: "EM12",
|
||||
grid2: "EM00",
|
||||
pos1: %{"lat" => 32.9, "lon" => -97.0},
|
||||
pos2: %{"lat" => 30.3, "lon" => -97.7},
|
||||
distance_km: Decimal.new("295")
|
||||
}
|
||||
|
||||
{:ok, contact} =
|
||||
%Contact{}
|
||||
|> Contact.changeset(Map.merge(default, attrs))
|
||||
|> Repo.insert()
|
||||
|
||||
contact
|
||||
end
|
||||
|
||||
setup do
|
||||
Req.Test.stub(IemClient, fn conn ->
|
||||
case conn.request_path do
|
||||
|
|
@ -101,7 +80,7 @@ defmodule Microwaveprop.Workers.BackfillEnqueueWorkerTest do
|
|||
|
||||
test "respects limit parameter" do
|
||||
for i <- 1..5 do
|
||||
ts = DateTime.add(~U[2026-01-01 00:00:00Z], i * 3600, :second)
|
||||
ts = DateTime.shift(~U[2026-01-01 00:00:00Z], hour: i)
|
||||
create_contact(%{qso_timestamp: ts})
|
||||
end
|
||||
|
||||
|
|
@ -114,30 +93,33 @@ defmodule Microwaveprop.Workers.BackfillEnqueueWorkerTest do
|
|||
test "enqueues all pending contacts when limit is omitted" do
|
||||
Phoenix.PubSub.subscribe(Microwaveprop.PubSub, "backfill:enqueue_complete")
|
||||
|
||||
for i <- 1..7 do
|
||||
ts = DateTime.add(~U[2026-01-01 00:00:00Z], i * 3600, :second)
|
||||
create_contact(%{qso_timestamp: ts})
|
||||
end
|
||||
our_ids =
|
||||
for i <- 1..7 do
|
||||
ts = DateTime.shift(~U[2026-01-01 00:00:00Z], hour: i)
|
||||
create_contact(%{qso_timestamp: ts}).id
|
||||
end
|
||||
|
||||
assert :ok =
|
||||
BackfillEnqueueWorker.perform(%Oban.Job{
|
||||
args: %{"types" => @non_era5_types}
|
||||
})
|
||||
|
||||
assert_receive {:enqueue_complete, 7}
|
||||
assert_receive {:enqueue_complete, count}
|
||||
assert count >= 7
|
||||
end
|
||||
|
||||
test "broadcasts enqueue_complete with count" do
|
||||
Phoenix.PubSub.subscribe(Microwaveprop.PubSub, "backfill:enqueue_complete")
|
||||
|
||||
create_contact()
|
||||
contact = create_contact()
|
||||
|
||||
assert :ok =
|
||||
BackfillEnqueueWorker.perform(%Oban.Job{
|
||||
args: %{"limit" => 10, "types" => @non_era5_types}
|
||||
})
|
||||
|
||||
assert_receive {:enqueue_complete, 1}
|
||||
assert_receive {:enqueue_complete, count}
|
||||
assert count >= 1
|
||||
end
|
||||
|
||||
# :unavailable is a terminal state in the sense that workers don't
|
||||
|
|
@ -164,7 +146,7 @@ defmodule Microwaveprop.Workers.BackfillEnqueueWorkerTest do
|
|||
|
||||
# Force updated_at past the 24 h cooldown so the type_filter's
|
||||
# :unavailable branch picks this contact up.
|
||||
old = DateTime.utc_now() |> DateTime.add(-2 * 86_400, :second) |> DateTime.truncate(:second)
|
||||
old = DateTime.utc_now() |> DateTime.shift(day: -2) |> DateTime.truncate(:second)
|
||||
Repo.update_all(from(c in Contact, where: c.id == ^contact.id), set: [updated_at: old])
|
||||
|
||||
assert :ok =
|
||||
|
|
@ -172,7 +154,8 @@ defmodule Microwaveprop.Workers.BackfillEnqueueWorkerTest do
|
|||
args: %{"limit" => 10, "types" => @non_era5_types}
|
||||
})
|
||||
|
||||
assert_receive {:enqueue_complete, 1}
|
||||
assert_receive {:enqueue_complete, count}
|
||||
assert count >= 1
|
||||
end
|
||||
|
||||
test "leaves :unavailable contacts inside the cooldown window alone" do
|
||||
|
|
@ -196,7 +179,10 @@ defmodule Microwaveprop.Workers.BackfillEnqueueWorkerTest do
|
|||
args: %{"limit" => 10, "types" => @non_era5_types}
|
||||
})
|
||||
|
||||
assert_receive {:enqueue_complete, 0}
|
||||
# Our contact must still be :unavailable — the worker must not
|
||||
# have touched it, regardless of what other contacts it enqueued.
|
||||
reloaded = Repo.get!(Contact, contact.id)
|
||||
assert reloaded.weather_status == :unavailable
|
||||
end
|
||||
|
||||
# :narr is a virtual type (no narr_status column) — it targets contacts
|
||||
|
|
@ -256,13 +242,13 @@ defmodule Microwaveprop.Workers.BackfillEnqueueWorkerTest do
|
|||
|
||||
defp stale_ts do
|
||||
DateTime.utc_now()
|
||||
|> DateTime.add(-(@stale_cutoff + 3600), :second)
|
||||
|> DateTime.shift(second: -(@stale_cutoff + 3600))
|
||||
|> DateTime.truncate(:second)
|
||||
end
|
||||
|
||||
defp fresh_ts do
|
||||
DateTime.utc_now()
|
||||
|> DateTime.add(-3600, :second)
|
||||
|> DateTime.shift(hour: -1)
|
||||
|> DateTime.truncate(:second)
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -109,7 +109,7 @@ defmodule Microwaveprop.Workers.ContactPositionBackfillWorkerTest do
|
|||
pos1: nil,
|
||||
pos2: nil,
|
||||
distance_km: nil,
|
||||
qso_timestamp: DateTime.add(~U[2026-03-28 18:00:00Z], i * 3600, :second)
|
||||
qso_timestamp: DateTime.shift(~U[2026-03-28 18:00:00Z], hour: i)
|
||||
})
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -219,7 +219,7 @@ defmodule Microwaveprop.Workers.GefsFetchWorkerTest do
|
|||
test "round-trips a full batch through the context" do
|
||||
run_time = ~U[2026-04-18 12:00:00Z]
|
||||
fh = 24
|
||||
valid_time = DateTime.add(run_time, fh * 3600, :second)
|
||||
valid_time = DateTime.shift(run_time, hour: fh)
|
||||
|
||||
batch = [
|
||||
%{
|
||||
|
|
|
|||
|
|
@ -12,9 +12,9 @@ defmodule Microwaveprop.Workers.GridCachePruneWorkerTest do
|
|||
describe "perform/1" do
|
||||
test "removes cache entries with valid_time older than 3 hours" do
|
||||
now = DateTime.utc_now()
|
||||
old = now |> DateTime.add(-3, :hour) |> DateTime.add(-1, :second) |> DateTime.truncate(:second)
|
||||
fresh = now |> DateTime.add(-30, :minute) |> DateTime.truncate(:second)
|
||||
future = now |> DateTime.add(6, :hour) |> DateTime.truncate(:second)
|
||||
old = now |> DateTime.shift(hour: -3, second: -1) |> DateTime.truncate(:second)
|
||||
fresh = now |> DateTime.shift(minute: -30) |> DateTime.truncate(:second)
|
||||
future = now |> DateTime.shift(hour: 6) |> DateTime.truncate(:second)
|
||||
|
||||
GridCache.put(old, [%{lat: 32.0, lon: -97.0, temperature: 10.0}])
|
||||
GridCache.put(fresh, [%{lat: 32.0, lon: -97.0, temperature: 20.0}])
|
||||
|
|
@ -29,7 +29,7 @@ defmodule Microwaveprop.Workers.GridCachePruneWorkerTest do
|
|||
|
||||
test "is a no-op when nothing is stale" do
|
||||
fresh =
|
||||
DateTime.utc_now() |> DateTime.add(-30, :minute) |> DateTime.truncate(:second)
|
||||
DateTime.utc_now() |> DateTime.shift(minute: -30) |> DateTime.truncate(:second)
|
||||
|
||||
GridCache.put(fresh, [%{lat: 32.0, lon: -97.0, temperature: 42.0}])
|
||||
|
||||
|
|
|
|||
|
|
@ -11,9 +11,9 @@ defmodule Microwaveprop.Workers.PropagationPruneWorkerTest do
|
|||
# picking exactly -3h flakes whenever the test + worker wall-clock
|
||||
# land in the same second. -3h - 1s gives unconditional ordering.
|
||||
now = DateTime.utc_now()
|
||||
old = now |> DateTime.add(-3, :hour) |> DateTime.add(-1, :second) |> DateTime.truncate(:second)
|
||||
fresh = now |> DateTime.add(-30, :minute) |> DateTime.truncate(:second)
|
||||
future = now |> DateTime.add(6, :hour) |> DateTime.truncate(:second)
|
||||
old = now |> DateTime.shift(hour: -3, second: -1) |> DateTime.truncate(:second)
|
||||
fresh = now |> DateTime.shift(minute: -30) |> DateTime.truncate(:second)
|
||||
future = now |> DateTime.shift(hour: 6) |> DateTime.truncate(:second)
|
||||
|
||||
ScoresFile.write!(10_000, old, [%{lat: 25.0, lon: -125.0, score: 10}])
|
||||
ScoresFile.write!(10_000, fresh, [%{lat: 25.0, lon: -125.0, score: 20}])
|
||||
|
|
@ -28,7 +28,7 @@ defmodule Microwaveprop.Workers.PropagationPruneWorkerTest do
|
|||
|
||||
test "is a no-op when nothing is stale" do
|
||||
now = DateTime.utc_now()
|
||||
fresh = now |> DateTime.add(-30, :minute) |> DateTime.truncate(:second)
|
||||
fresh = now |> DateTime.shift(minute: -30) |> DateTime.truncate(:second)
|
||||
ScoresFile.write!(10_000, fresh, [%{lat: 25.0, lon: -125.0, score: 42}])
|
||||
|
||||
assert :ok = PropagationPruneWorker.perform(%Oban.Job{})
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ defmodule Microwaveprop.Workers.PskrCalibrationWorkerTest do
|
|||
# arrive in the last few minutes of an hour wait up to a full
|
||||
# cron interval before they're sampled.
|
||||
now = truncate_to_hour(DateTime.utc_now())
|
||||
previous = DateTime.add(now, -3600, :second)
|
||||
previous = DateTime.shift(now, hour: -1)
|
||||
|
||||
insert_spot!(now)
|
||||
insert_spot!(previous, sender_grid: "EM10AA")
|
||||
|
|
@ -90,7 +90,7 @@ defmodule Microwaveprop.Workers.PskrCalibrationWorkerTest do
|
|||
now = truncate_to_hour(DateTime.utc_now())
|
||||
|
||||
Enum.each(0..3, fn offset ->
|
||||
hour = DateTime.add(now, -offset * 3600, :second)
|
||||
hour = DateTime.shift(now, hour: -offset)
|
||||
insert_spot!(hour, sender_grid: "EM1#{offset}AA")
|
||||
end)
|
||||
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ defmodule MicrowavepropWeb.Api.V1.AuthControllerTest do
|
|||
end
|
||||
|
||||
test "accepts an ISO 8601 expires_at", %{conn: conn, user: user} do
|
||||
future = DateTime.utc_now() |> DateTime.add(3600, :second) |> DateTime.to_iso8601()
|
||||
future = DateTime.utc_now() |> DateTime.shift(hour: 1) |> DateTime.to_iso8601()
|
||||
|
||||
conn =
|
||||
post(conn, ~p"/api/v1/auth/tokens", %{
|
||||
|
|
@ -111,7 +111,7 @@ defmodule MicrowavepropWeb.Api.V1.AuthControllerTest do
|
|||
end
|
||||
|
||||
test "rejects expires_at in the past via changeset 422", %{conn: conn, user: user} do
|
||||
past = DateTime.utc_now() |> DateTime.add(-1, :second) |> DateTime.to_iso8601()
|
||||
past = DateTime.utc_now() |> DateTime.shift(second: -1) |> DateTime.to_iso8601()
|
||||
|
||||
conn =
|
||||
post(conn, ~p"/api/v1/auth/tokens", %{
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ defmodule MicrowavepropWeb.UserSettingsControllerTest do
|
|||
assert redirected_to(conn) == ~p"/users/log-in"
|
||||
end
|
||||
|
||||
@tag token_authenticated_at: DateTime.add(DateTime.utc_now(:second), -25, :hour)
|
||||
@tag token_authenticated_at: DateTime.shift(DateTime.utc_now(:second), hour: -25)
|
||||
test "redirects if user is not in sudo mode", %{conn: conn} do
|
||||
conn = get(conn, ~p"/users/settings")
|
||||
assert redirected_to(conn) == ~p"/users/log-in"
|
||||
|
|
|
|||
|
|
@ -103,7 +103,7 @@ defmodule MicrowavepropWeb.ContactLive.IndexTest do
|
|||
|
||||
test "pagination reaches later pages", %{conn: conn} do
|
||||
for i <- 1..25 do
|
||||
ts = DateTime.add(~U[2026-01-01 00:00:00Z], i * 3600, :second)
|
||||
ts = DateTime.shift(~U[2026-01-01 00:00:00Z], hour: i)
|
||||
create_contact(%{qso_timestamp: ts, station1: "W#{i}TEST"})
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -1363,7 +1363,7 @@ defmodule MicrowavepropWeb.ContactLive.ShowCoverageTest do
|
|||
|
||||
nonce = System.unique_integer([:positive])
|
||||
lat1 = 32.0 + nonce / 1_000_000
|
||||
iter_valid_time = DateTime.add(~U[2026-03-28 18:00:00Z], nonce, :second)
|
||||
iter_valid_time = DateTime.shift(~U[2026-03-28 18:00:00Z], second: nonce)
|
||||
|
||||
contact =
|
||||
create_contact(%{
|
||||
|
|
@ -1426,7 +1426,7 @@ defmodule MicrowavepropWeb.ContactLive.ShowCoverageTest do
|
|||
Repo.insert!(
|
||||
SurfaceObservation.changeset(%SurfaceObservation{}, %{
|
||||
station_id: station.id,
|
||||
observed_at: DateTime.add(contact.qso_timestamp, i * 60, :second),
|
||||
observed_at: DateTime.shift(contact.qso_timestamp, minute: i),
|
||||
temp_f: 60.0 + i,
|
||||
dewpoint_f: 45.0 + i,
|
||||
relative_humidity: 55.0,
|
||||
|
|
@ -1478,7 +1478,7 @@ defmodule MicrowavepropWeb.ContactLive.ShowCoverageTest do
|
|||
Repo.insert!(
|
||||
SurfaceObservation.changeset(%SurfaceObservation{}, %{
|
||||
station_id: station.id,
|
||||
observed_at: DateTime.add(contact.qso_timestamp, offset, :second),
|
||||
observed_at: DateTime.shift(contact.qso_timestamp, second: offset),
|
||||
temp_f: temp,
|
||||
dewpoint_f: dewp,
|
||||
relative_humidity: rh,
|
||||
|
|
@ -1576,7 +1576,7 @@ defmodule MicrowavepropWeb.ContactLive.ShowCoverageTest do
|
|||
Repo.insert!(
|
||||
Sounding.changeset(%Sounding{}, %{
|
||||
station_id: station.id,
|
||||
observed_at: DateTime.add(contact.qso_timestamp, offset, :second),
|
||||
observed_at: DateTime.shift(contact.qso_timestamp, second: offset),
|
||||
profile: [%{"pres" => 1000.0, "tmpc" => temp, "dwpc" => temp - 5.0, "hght" => 100.0}],
|
||||
level_count: 1,
|
||||
surface_temp_c: temp,
|
||||
|
|
|
|||
|
|
@ -531,12 +531,12 @@ defmodule MicrowavepropWeb.MapLiveTest do
|
|||
end
|
||||
|
||||
test "renders a '+Nh' suffix when the valid_time is in the future" do
|
||||
future = DateTime.add(DateTime.utc_now(), 3 * 3600, :second)
|
||||
future = DateTime.shift(DateTime.utc_now(), hour: 3)
|
||||
assert MapLive.format_data_timestamp(future) =~ "+3h"
|
||||
end
|
||||
|
||||
test "renders a 'Nh ago' suffix when the valid_time is in the past" do
|
||||
past = DateTime.add(DateTime.utc_now(), -4 * 3600, :second)
|
||||
past = DateTime.shift(DateTime.utc_now(), hour: -4)
|
||||
assert MapLive.format_data_timestamp(past) =~ "4h ago"
|
||||
end
|
||||
end
|
||||
|
|
@ -611,7 +611,7 @@ defmodule MicrowavepropWeb.MapLiveTest do
|
|||
assert html =~ "Updating propagation"
|
||||
assert html =~ ~s(data-pipeline-state="running")
|
||||
|
||||
future = DateTime.add(DateTime.utc_now(), 3 * 3600, :second)
|
||||
future = DateTime.shift(DateTime.utc_now(), hour: 3)
|
||||
|
||||
Phoenix.PubSub.broadcast(
|
||||
Microwaveprop.PubSub,
|
||||
|
|
|
|||
|
|
@ -163,7 +163,7 @@ defmodule MicrowavepropWeb.PathLiveTest do
|
|||
|> Map.put(:minute, 0)
|
||||
|> Map.put(:second, 0)
|
||||
|
||||
times = for h <- 0..2, do: DateTime.add(base, h * 3600, :second)
|
||||
times = for h <- 0..2, do: DateTime.shift(base, hour: h)
|
||||
|
||||
Enum.each(times, fn t ->
|
||||
Propagation.replace_scores(
|
||||
|
|
|
|||
|
|
@ -175,7 +175,7 @@ defmodule MicrowavepropWeb.PskrSpotsLiveTest do
|
|||
# Insert 105 spots — only the 100 most recent should render
|
||||
# Pad single-digit grid numbers so substring matching is unambiguous
|
||||
for i <- 1..105 do
|
||||
t = DateTime.add(now(), i - 105, :second)
|
||||
t = DateTime.shift(now(), second: i - 105)
|
||||
grid = if i < 10, do: "GRID00#{i}", else: "GRID#{i}"
|
||||
insert_spot(%{sender_grid: grid, last_spot_at: t, hour_utc: t})
|
||||
end
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ defmodule MicrowavepropWeb.SkewtLiveTest do
|
|||
|
||||
# Trigger select_time with a valid future ISO. Even if no profile is
|
||||
# found for this time, the handler exercises the full re-load path.
|
||||
future = DateTime.utc_now() |> DateTime.add(3600, :second) |> DateTime.to_iso8601()
|
||||
future = DateTime.utc_now() |> DateTime.shift(hour: 1) |> DateTime.to_iso8601()
|
||||
|
||||
_ = render_hook(view, "select_time", %{"valid_time" => future})
|
||||
_ = render_async(view)
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ defmodule MicrowavepropWeb.WeatherCaMapLiveTest do
|
|||
|> Map.put(:minute, 0)
|
||||
|> Map.put(:second, 0)
|
||||
|
||||
stale_t = DateTime.add(now, -4 * 3600, :second)
|
||||
stale_t = DateTime.shift(now, hour: -4)
|
||||
hrdps_dir = ScalarFile.dir_for_hrdps(stale_t)
|
||||
File.mkdir_p!(hrdps_dir)
|
||||
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ defmodule MicrowavepropWeb.WeatherMapLiveTest do
|
|||
|> Map.put(:minute, 0)
|
||||
|> Map.put(:second, 0)
|
||||
|
||||
times = for h <- 0..2, do: DateTime.add(base, h * 3600, :second)
|
||||
times = for h <- 0..2, do: DateTime.shift(base, hour: h)
|
||||
# Seed a profile file at each forecast hour.
|
||||
Enum.each(times, &write_profile(&1, 33.0, -97.0))
|
||||
|
||||
|
|
@ -82,14 +82,14 @@ defmodule MicrowavepropWeb.WeatherMapLiveTest do
|
|||
|> Map.put(:minute, 0)
|
||||
|> Map.put(:second, 0)
|
||||
|
||||
stale_t = DateTime.add(now, -2 * 3600, :second)
|
||||
stale_t = DateTime.shift(now, hour: -2)
|
||||
# `now` is truncated to the top of the hour; real `utc_now()` is up to
|
||||
# 60 min ahead of that, so a `now - 30 min` recent_t is 30-90 min old
|
||||
# of real time and the LiveView's `utc_now() - 1h` cutoff filters it
|
||||
# out 50% of the time. Anchor recent_t at the top of the current hour
|
||||
# — that's always within the 1-hour window regardless of minute.
|
||||
recent_t = now
|
||||
future_t = DateTime.add(now, 3 * 3600, :second)
|
||||
future_t = DateTime.shift(now, hour: 3)
|
||||
|
||||
Enum.each([stale_t, recent_t, future_t], &write_profile(&1, 33.0, -97.0))
|
||||
|
||||
|
|
@ -116,7 +116,7 @@ defmodule MicrowavepropWeb.WeatherMapLiveTest do
|
|||
# sequentially so `latest_grid_valid_time` can point to f11 even
|
||||
# though the user expects the map to show "Now" by default.
|
||||
now_t = base
|
||||
future_t = DateTime.add(base, 11 * 3600, :second)
|
||||
future_t = DateTime.shift(base, hour: 11)
|
||||
|
||||
write_profile(now_t, 33.0, -97.0)
|
||||
write_profile(future_t, 33.0, -97.0)
|
||||
|
|
@ -166,7 +166,7 @@ defmodule MicrowavepropWeb.WeatherMapLiveTest do
|
|||
|> Map.put(:second, 0)
|
||||
|
||||
now_t = base
|
||||
future_t = DateTime.add(base, 3 * 3600, :second)
|
||||
future_t = DateTime.shift(base, hour: 3)
|
||||
|
||||
# Seed f00 with a 22°C surface and f03 with a 10°C surface so we
|
||||
# can tell them apart in the push_event payload.
|
||||
|
|
|
|||
|
|
@ -198,7 +198,7 @@ defmodule MicrowavepropWeb.UserAuthTest do
|
|||
end
|
||||
|
||||
test "redirects when authentication is too old", %{conn: conn, user: user} do
|
||||
too_long_ago = :second |> DateTime.utc_now() |> DateTime.add(-25, :hour)
|
||||
too_long_ago = :second |> DateTime.utc_now() |> DateTime.shift(hour: -25)
|
||||
user = %{user | authenticated_at: too_long_ago}
|
||||
user_token = Accounts.generate_user_session_token(user)
|
||||
{user, token_inserted_at} = Accounts.get_user_by_session_token(user_token)
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ defmodule Mix.Tasks.Prop.CompareTest do
|
|||
|
||||
defp seed_contact_and_hrrr(opts \\ []) do
|
||||
qso_ts =
|
||||
Keyword.get(opts, :qso_timestamp, DateTime.utc_now() |> DateTime.add(-3600, :second) |> DateTime.truncate(:second))
|
||||
Keyword.get(opts, :qso_timestamp, DateTime.utc_now() |> DateTime.shift(hour: -1) |> DateTime.truncate(:second))
|
||||
|
||||
valid_time = qso_ts |> DateTime.truncate(:second) |> Map.put(:minute, 0) |> Map.put(:second, 0)
|
||||
band = Keyword.get(opts, :band, "10000")
|
||||
|
|
@ -86,7 +86,7 @@ defmodule Mix.Tasks.Prop.CompareTest do
|
|||
history_lines =
|
||||
for _ <- 1..6 do
|
||||
Jason.encode!(%{
|
||||
"date" => DateTime.utc_now() |> DateTime.add(-3600 * 24, :second) |> DateTime.to_iso8601(),
|
||||
"date" => DateTime.utc_now() |> DateTime.shift(day: -1) |> DateTime.to_iso8601(),
|
||||
"n_samples" => 100,
|
||||
"overall" => %{"alg_distance_spearman" => 0.5, "alg_ml_rmse" => 5.0},
|
||||
"by_band" => %{}
|
||||
|
|
|
|||
|
|
@ -106,7 +106,7 @@ defmodule Mix.Tasks.PropagationMlTasksTest do
|
|||
base_time = ~U[2020-06-15 18:00:00Z]
|
||||
|
||||
for i <- 1..6 do
|
||||
valid_time = base_time |> DateTime.add(i * 3600, :second) |> DateTime.truncate(:second)
|
||||
valid_time = base_time |> DateTime.shift(hour: i) |> DateTime.truncate(:second)
|
||||
# Distinct snapped grid points for each i.
|
||||
lat1 = 32.0 + i * 0.125
|
||||
lon1 = -97.0 - i * 0.125
|
||||
|
|
|
|||
|
|
@ -213,7 +213,7 @@ defmodule Mix.Tasks.SimpleTasksTest do
|
|||
{:ok, _} =
|
||||
%HrrrProfile{}
|
||||
|> HrrrProfile.changeset(%{
|
||||
valid_time: DateTime.add(~U[2025-06-15 12:00:00Z], i * 86_400, :second),
|
||||
valid_time: DateTime.shift(~U[2025-06-15 12:00:00Z], day: i),
|
||||
lat: 32.5,
|
||||
lon: -97.0,
|
||||
surface_temp_c: 20.0 + i,
|
||||
|
|
|
|||
53
test/support/fixtures/contacts_fixtures.ex
Normal file
53
test/support/fixtures/contacts_fixtures.ex
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
defmodule Microwaveprop.ContactsFixtures do
|
||||
@moduledoc """
|
||||
Factory helpers for creating Contact records in tests.
|
||||
|
||||
Uses globally-unique `qso_timestamp` microseconds (via
|
||||
`System.unique_integer([:positive, :monotonic])`) to prevent
|
||||
`contacts_dedup_idx` unique-constraint violations when async
|
||||
tests across different modules run in parallel and would
|
||||
otherwise insert contacts with identical dedup-key columns.
|
||||
"""
|
||||
|
||||
alias Microwaveprop.Radio.Contact
|
||||
alias Microwaveprop.Repo
|
||||
|
||||
@doc """
|
||||
Creates a contact with unique dedup-key column values.
|
||||
|
||||
Override any field by passing it in `attrs`. Pass `:user_id` to
|
||||
associate the contact with a user.
|
||||
|
||||
Returns the created `%Contact{}`.
|
||||
"""
|
||||
@spec create_contact(map()) :: Contact.t()
|
||||
def create_contact(attrs \\ %{}) do
|
||||
# Globally-unique second offset so every contact has a distinct
|
||||
# dedup key (band, qso_timestamp, stations, grids). The column is
|
||||
# timestamp(0), so microsecond offsets are truncated.
|
||||
unique_s = System.unique_integer([:positive, :monotonic])
|
||||
|
||||
default = %{
|
||||
station1: "W5XD",
|
||||
station2: "K5TR",
|
||||
qso_timestamp: DateTime.shift(~U[2026-03-28 18:00:00Z], second: unique_s),
|
||||
mode: "CW",
|
||||
band: Decimal.new("1296"),
|
||||
grid1: "EM12",
|
||||
grid2: "EM00",
|
||||
pos1: %{"lat" => 32.9, "lon" => -97.0},
|
||||
pos2: %{"lat" => 30.3, "lon" => -97.7},
|
||||
distance_km: Decimal.new("295")
|
||||
}
|
||||
|
||||
merged = Map.merge(default, attrs)
|
||||
{user_id, changeset_attrs} = Map.pop(merged, :user_id)
|
||||
|
||||
{:ok, contact} =
|
||||
%Contact{user_id: user_id}
|
||||
|> Contact.changeset(changeset_attrs)
|
||||
|> Repo.insert()
|
||||
|
||||
contact
|
||||
end
|
||||
end
|
||||
|
|
@ -15,6 +15,7 @@ Code.require_file("test/support/data_case.ex")
|
|||
Code.require_file("test/support/conn_case.ex")
|
||||
Code.require_file("test/support/fixtures/accounts_fixtures.ex")
|
||||
Code.require_file("test/support/fixtures/beacons_fixtures.ex")
|
||||
Code.require_file("test/support/fixtures/contacts_fixtures.ex")
|
||||
|
||||
# ExUnit.start/1 with capture_log: true starts :logger, which cascades
|
||||
# into the full OTP application. mix.exs now runs `mix test --no-start`
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue