fix: fix flaky test
This commit is contained in:
@@ -15,6 +15,11 @@ defmodule BDS.Generation.Validation do
|
|||||||
|
|
||||||
alias BDS.Slug
|
alias BDS.Slug
|
||||||
|
|
||||||
|
# POSIX mtimes from File.stat/2 have second granularity while generation
|
||||||
|
# timestamps are milliseconds, so ordering within one second is unknowable;
|
||||||
|
# only treat a source file as newer when it is beyond this tolerance.
|
||||||
|
@mtime_granularity_tolerance_ms 1_000
|
||||||
|
|
||||||
@spec generated_file_updated_at_map([map()]) :: map()
|
@spec generated_file_updated_at_map([map()]) :: map()
|
||||||
def generated_file_updated_at_map(generated_files) do
|
def generated_file_updated_at_map(generated_files) do
|
||||||
Map.new(generated_files, &{&1.relative_path, &1.updated_at})
|
Map.new(generated_files, &{&1.relative_path, &1.updated_at})
|
||||||
@@ -139,7 +144,8 @@ defmodule BDS.Generation.Validation do
|
|||||||
effective_generated_at_ms =
|
effective_generated_at_ms =
|
||||||
max(mtime_ms(html_stat), check.generated_updated_at_ms || 0)
|
max(mtime_ms(html_stat), check.generated_updated_at_ms || 0)
|
||||||
|
|
||||||
if mtime_ms(post_stat) > effective_generated_at_ms do
|
if mtime_ms(post_stat) >
|
||||||
|
effective_generated_at_ms + @mtime_granularity_tolerance_ms do
|
||||||
MapSet.put(acc, normalized_url_path)
|
MapSet.put(acc, normalized_url_path)
|
||||||
else
|
else
|
||||||
acc
|
acc
|
||||||
|
|||||||
97
test/bds/generation/validation_test.exs
Normal file
97
test/bds/generation/validation_test.exs
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
defmodule BDS.Generation.ValidationTest do
|
||||||
|
use ExUnit.Case, async: true
|
||||||
|
|
||||||
|
alias BDS.Generation.Validation
|
||||||
|
|
||||||
|
@base_url "https://example.com/blog"
|
||||||
|
@post_url_path "/2026/06/01/sample-post"
|
||||||
|
|
||||||
|
setup do
|
||||||
|
temp_dir =
|
||||||
|
Path.join(System.tmp_dir!(), "bds-validation-#{System.unique_integer([:positive])}")
|
||||||
|
|
||||||
|
html_dir = Path.join(temp_dir, "html")
|
||||||
|
post_html_path = Path.join([html_dir, "2026", "06", "01", "sample-post", "index.html"])
|
||||||
|
source_path = Path.join(temp_dir, "sample-post.md")
|
||||||
|
|
||||||
|
File.mkdir_p!(Path.dirname(post_html_path))
|
||||||
|
File.write!(Path.join(html_dir, "index.html"), "<html>home</html>")
|
||||||
|
File.write!(post_html_path, "<html>post</html>")
|
||||||
|
File.write!(source_path, "Sample body")
|
||||||
|
on_exit(fn -> File.rm_rf(temp_dir) end)
|
||||||
|
|
||||||
|
%{html_dir: html_dir, post_html_path: post_html_path, source_path: source_path}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp compare(html_dir, source_path, generated_updated_at_ms) do
|
||||||
|
sitemap_xml =
|
||||||
|
Enum.join([
|
||||||
|
~s(<?xml version="1.0" encoding="UTF-8"?>),
|
||||||
|
~s(<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">),
|
||||||
|
~s(<url><loc>#{@base_url}/</loc></url>),
|
||||||
|
~s(<url><loc>#{@base_url}#{@post_url_path}/</loc></url>),
|
||||||
|
~s(</urlset>)
|
||||||
|
])
|
||||||
|
|
||||||
|
Validation.compare_sitemap_to_html(%{
|
||||||
|
sitemap_xml: sitemap_xml,
|
||||||
|
base_url: @base_url,
|
||||||
|
html_dir: html_dir,
|
||||||
|
on_progress: nil,
|
||||||
|
post_timestamp_checks: [
|
||||||
|
%{
|
||||||
|
post_url_path: @post_url_path,
|
||||||
|
post_file_path: source_path,
|
||||||
|
generated_updated_at_ms: generated_updated_at_ms
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
test "does not report a post as updated when its source mtime second adjoins the generation timestamp",
|
||||||
|
%{html_dir: html_dir, post_html_path: post_html_path, source_path: source_path} do
|
||||||
|
source_mtime = System.os_time(:second)
|
||||||
|
|
||||||
|
File.touch!(source_path, source_mtime)
|
||||||
|
File.touch!(post_html_path, source_mtime - 5)
|
||||||
|
File.touch!(Path.join(html_dir, "index.html"), source_mtime - 5)
|
||||||
|
|
||||||
|
generated_updated_at_ms = source_mtime * 1000 - 100
|
||||||
|
|
||||||
|
result = compare(html_dir, source_path, generated_updated_at_ms)
|
||||||
|
|
||||||
|
assert result.missing_url_paths == []
|
||||||
|
assert result.extra_url_paths == []
|
||||||
|
assert result.updated_post_url_paths == []
|
||||||
|
end
|
||||||
|
|
||||||
|
test "does not report a post as updated when its source mtime is in the same second as the generation timestamp",
|
||||||
|
%{html_dir: html_dir, post_html_path: post_html_path, source_path: source_path} do
|
||||||
|
source_mtime = System.os_time(:second)
|
||||||
|
|
||||||
|
File.touch!(source_path, source_mtime)
|
||||||
|
File.touch!(post_html_path, source_mtime - 5)
|
||||||
|
File.touch!(Path.join(html_dir, "index.html"), source_mtime - 5)
|
||||||
|
|
||||||
|
generated_updated_at_ms = source_mtime * 1000 + 500
|
||||||
|
|
||||||
|
result = compare(html_dir, source_path, generated_updated_at_ms)
|
||||||
|
|
||||||
|
assert result.updated_post_url_paths == []
|
||||||
|
end
|
||||||
|
|
||||||
|
test "reports a post as updated when its source mtime is beyond the granularity tolerance",
|
||||||
|
%{html_dir: html_dir, post_html_path: post_html_path, source_path: source_path} do
|
||||||
|
source_mtime = System.os_time(:second)
|
||||||
|
|
||||||
|
File.touch!(source_path, source_mtime)
|
||||||
|
File.touch!(post_html_path, source_mtime - 5)
|
||||||
|
File.touch!(Path.join(html_dir, "index.html"), source_mtime - 5)
|
||||||
|
|
||||||
|
generated_updated_at_ms = (source_mtime - 5) * 1000
|
||||||
|
|
||||||
|
result = compare(html_dir, source_path, generated_updated_at_ms)
|
||||||
|
|
||||||
|
assert result.updated_post_url_paths == [@post_url_path]
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -890,8 +890,8 @@ defmodule BDS.GenerationTest do
|
|||||||
source_path = Path.join([temp_dir, published_post.file_path])
|
source_path = Path.join([temp_dir, published_post.file_path])
|
||||||
extra_path = Path.join([temp_dir, "html", "obsolete", "index.html"])
|
extra_path = Path.join([temp_dir, "html", "obsolete", "index.html"])
|
||||||
|
|
||||||
|
backdate_generated_outputs(project.id, temp_dir)
|
||||||
File.rm!(post_file_path)
|
File.rm!(post_file_path)
|
||||||
Process.sleep(1200)
|
|
||||||
File.write!(source_path, File.read!(source_path) <> "\n")
|
File.write!(source_path, File.read!(source_path) <> "\n")
|
||||||
File.mkdir_p!(Path.dirname(extra_path))
|
File.mkdir_p!(Path.dirname(extra_path))
|
||||||
File.write!(extra_path, "<html>obsolete</html>")
|
File.write!(extra_path, "<html>obsolete</html>")
|
||||||
@@ -973,12 +973,12 @@ defmodule BDS.GenerationTest do
|
|||||||
updated_post_source_path = Path.join([temp_dir, published_updated_post.file_path])
|
updated_post_source_path = Path.join([temp_dir, published_updated_post.file_path])
|
||||||
extra_route_path = Path.join([temp_dir, "html", "obsolete", "deep", "index.html"])
|
extra_route_path = Path.join([temp_dir, "html", "obsolete", "deep", "index.html"])
|
||||||
|
|
||||||
|
backdate_generated_outputs(project.id, temp_dir)
|
||||||
File.rm!(sitemap_path)
|
File.rm!(sitemap_path)
|
||||||
File.rm!(missing_post_html_path)
|
File.rm!(missing_post_html_path)
|
||||||
File.mkdir_p!(Path.dirname(extra_route_path))
|
File.mkdir_p!(Path.dirname(extra_route_path))
|
||||||
File.write!(extra_route_path, "<html>obsolete</html>")
|
File.write!(extra_route_path, "<html>obsolete</html>")
|
||||||
|
|
||||||
Process.sleep(1200)
|
|
||||||
File.write!(updated_post_source_path, File.read!(updated_post_source_path) <> "\n")
|
File.write!(updated_post_source_path, File.read!(updated_post_source_path) <> "\n")
|
||||||
|
|
||||||
assert {:ok, report} = BDS.Generation.validate_site(project.id)
|
assert {:ok, report} = BDS.Generation.validate_site(project.id)
|
||||||
@@ -1317,9 +1317,9 @@ defmodule BDS.GenerationTest do
|
|||||||
post_html_path = Path.join([temp_dir, "html", post_path])
|
post_html_path = Path.join([temp_dir, "html", post_path])
|
||||||
post_source_path = Path.join([temp_dir, published_post.file_path])
|
post_source_path = Path.join([temp_dir, published_post.file_path])
|
||||||
|
|
||||||
|
backdate_generated_outputs(project.id, temp_dir)
|
||||||
before_stat = File.stat!(post_html_path)
|
before_stat = File.stat!(post_html_path)
|
||||||
|
|
||||||
Process.sleep(1200)
|
|
||||||
File.write!(post_source_path, File.read!(post_source_path) <> "\n")
|
File.write!(post_source_path, File.read!(post_source_path) <> "\n")
|
||||||
|
|
||||||
assert {:ok, report} = BDS.Generation.validate_site(project.id)
|
assert {:ok, report} = BDS.Generation.validate_site(project.id)
|
||||||
@@ -1340,6 +1340,23 @@ defmodule BDS.GenerationTest do
|
|||||||
assert clean_report.updated_post_url_paths == []
|
assert clean_report.updated_post_url_paths == []
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Pushes generation timestamps and html mtimes far into the past so a
|
||||||
|
# subsequent source-file write is newer by more than the second-granularity
|
||||||
|
# mtime tolerance, without sleeping across real second boundaries.
|
||||||
|
defp backdate_generated_outputs(project_id, temp_dir) do
|
||||||
|
past_posix = System.os_time(:second) - 120
|
||||||
|
|
||||||
|
Repo.update_all(
|
||||||
|
from(g in BDS.Generation.GeneratedFileHash, where: g.project_id == ^project_id),
|
||||||
|
set: [updated_at: past_posix * 1000]
|
||||||
|
)
|
||||||
|
|
||||||
|
[temp_dir, "html", "**"]
|
||||||
|
|> Path.join()
|
||||||
|
|> Path.wildcard()
|
||||||
|
|> Enum.each(&File.touch!(&1, past_posix))
|
||||||
|
end
|
||||||
|
|
||||||
defp relative_path_to_url_path(relative_path) do
|
defp relative_path_to_url_path(relative_path) do
|
||||||
cleaned =
|
cleaned =
|
||||||
relative_path
|
relative_path
|
||||||
|
|||||||
Reference in New Issue
Block a user