use super::*;
const CONTEXT: &str = r#"defmodule Booking.Insights do
@moduledoc """
Insights.
"""
alias Booking.Repo
@default_rate 19
@doc """
Arrange an insight.
"""
@spec arrange(map(), map()) :: {:ok, map()}
def arrange(insight, user) do
insight
|> check(user)
|> record(:arranged)
end
def revoke(insight, user), do: record(check(insight, user), :revoked)
def total(insight), do: Enum.sum(insight.lines) * rate()
def list, do: Repo.all(Booking.Insight)
defp check(insight, user) do
if allowed?(user), do: insight, else: raise("no")
end
defp allowed?(%{role: :admin}), do: true
defp allowed?(_), do: false
defp record(insight, state) do
Repo.insert!(%{insight | state: state})
end
defp rate, do: @default_rate
end
"#;
fn line_of(needle: &str) -> usize {
CONTEXT
.lines()
.position(|l| l.contains(needle))
.unwrap_or_else(|| panic!("no line with {needle:?}"))
+ 1
}
fn changed(needles: &[&str]) -> Result<Vec<String>, usize> {
let lines: Vec<usize> = needles.iter().map(|n| line_of(n)).collect();
changed_functions(CONTEXT, &lines).map(|names| names.into_iter().collect())
}
#[test]
fn functions_with_their_lines() {
let found = functions(CONTEXT);
let span = |name: &str| {
let f = found.iter().find(|f| f.name == name).unwrap();
(f.public, f.first_line, f.last_line)
};
assert_eq!(
span("arrange"),
(true, line_of("@doc"), line_of("|> record(:arranged)") + 1)
);
assert_eq!(span("revoke").2, line_of("def revoke"));
assert_eq!(
span("check"),
(false, line_of("defp check"), line_of("defp check") + 2)
);
assert_eq!(found.iter().filter(|f| f.name == "allowed?").count(), 2);
let names: Vec<&str> = found.iter().map(|f| f.name.as_str()).collect();
assert_eq!(
names,
[
"arrange", "revoke", "total", "list", "check", "allowed?", "allowed?", "record", "rate"
]
);
}
#[test]
fn a_change_inside_a_public_function_affects_it_alone() {
assert_eq!(changed(&["|> check(user)"]).unwrap(), ["arrange"]);
assert_eq!(changed(&["def list"]).unwrap(), ["list"]);
assert_eq!(changed(&["Arrange an insight."]).unwrap(), ["arrange"]);
assert_eq!(changed(&["@spec arrange"]).unwrap(), ["arrange"]);
}
#[test]
fn a_change_to_a_private_function_affects_the_public_ones_that_reach_it() {
assert_eq!(changed(&["Repo.insert!"]).unwrap(), ["arrange", "revoke"]);
assert_eq!(changed(&["role: :admin"]).unwrap(), ["arrange", "revoke"]);
assert_eq!(changed(&["defp rate"]).unwrap(), ["total"]);
}
#[test]
fn a_change_that_may_concern_every_function_cannot_be_narrowed() {
assert_eq!(changed(&["defmodule Booking.Insights"]), Err(1));
let with_use = "defmodule A do\n use GenServer\n import Ecto.Query\n defstruct [:id]\n\n def f, do: 1\nend\n";
assert_eq!(changed_functions(with_use, &[2]), Err(2));
assert_eq!(changed_functions(with_use, &[3]), Err(3));
assert_eq!(changed_functions(with_use, &[4]), Err(4));
assert_eq!(changed_functions(with_use, &[6, 4, 3]), Err(4));
}
#[test]
fn naming_what_the_functions_use_changes_no_function() {
assert_eq!(
changed(&["alias Booking.Repo"]).unwrap(),
Vec::<String>::new()
);
assert_eq!(
changed(&["def list", "alias Booking.Repo"]).unwrap(),
["list"]
);
let grouped = "defmodule A do\n alias B.{\n C,\n D\n }\n require Logger\n\n def f, do: C.x()\nend\n";
for line in 2..=6 {
assert_eq!(
changed_functions(grouped, &[line]).unwrap().len(),
0,
"line {line} should change no function"
);
}
}
#[test]
fn a_module_attribute_changes_the_functions_that_read_it() {
assert_eq!(changed(&["@default_rate 19"]).unwrap(), ["total"]);
let source = "defmodule A do\n @moduledoc false\n @type t :: map()\n @timeout 5_000\n @timeouts %{a: 1}\n @unused :x\n @behaviour Runner\n\n def wait, do: sleep(@timeout)\n def all, do: @timeouts\n def other, do: :ok\nend\n";
assert_eq!(changed_functions(source, &[2, 3]).unwrap().len(), 0);
let names = |lines: &[usize]| {
changed_functions(source, lines).map(|n| n.into_iter().collect::<Vec<_>>())
};
assert_eq!(names(&[4]).unwrap(), ["wait"]);
assert_eq!(names(&[5]).unwrap(), ["all"]);
assert_eq!(names(&[6]), Err(6));
assert_eq!(names(&[7]), Err(7));
}
#[test]
fn a_change_to_comments_or_the_module_doc_affects_nothing() {
assert_eq!(changed(&["Insights."]).unwrap(), Vec::<String>::new());
assert_eq!(changed_functions(CONTEXT, &[]).unwrap().len(), 0);
assert_eq!(changed_functions(CONTEXT, &[9_999]).unwrap().len(), 0);
}
#[test]
fn a_local_call_is_told_from_a_remote_one_and_from_a_longer_name() {
assert!(calls_local("x |> record(:a)", "record"));
assert!(calls_local("Enum.map(xs, &record/1)", "record"));
assert!(!calls_local("Other.record(x)", "record"));
assert!(!calls_local("prerecord(x)", "record"));
assert!(!calls_local("recorded(x)", "record"));
assert!(!calls_local("record = 1", "record"));
assert!(!calls_local("%{record: 1}", "record"));
}
const COMPONENTS: &str = r#"defmodule Web.Components do
use Phoenix.Component
attr :label, :string, required: true
attr :rest, :global,
include: ~w(disabled form)
slot :inner_block
slot :col do
attr :title, :string
end
def button(assigns) do
render(assigns)
end
attr(:kind, :atom, default: :info)
def badge(assigns), do: render(assigns)
def fetch(id) do
load(id)
rescue
error -> handle(error)
after
cleanup()
end
def after_fetch, do: :ok
end
"#;
fn component_line(needle: &str) -> usize {
COMPONENTS.lines().position(|l| l.contains(needle)).unwrap() + 1
}
fn changed_component(needle: &str) -> Result<Vec<String>, usize> {
changed_functions(COMPONENTS, &[component_line(needle)]).map(|n| n.into_iter().collect())
}
#[test]
fn the_assigns_a_component_declares_are_part_of_it() {
for needle in [
"attr :label",
"include: ~w(disabled form)",
"slot :inner_block",
"slot :col do",
"attr :title",
] {
assert_eq!(changed_component(needle).unwrap(), ["button"], "{needle}");
}
assert_eq!(changed_component("attr(:kind").unwrap(), ["badge"]);
assert_eq!(changed_component("use Phoenix.Component"), Err(2));
}
#[test]
fn a_rescue_at_the_indentation_of_a_function_is_part_of_it() {
for needle in ["load(id)", "rescue", "handle(error)", "cleanup()"] {
assert_eq!(changed_component(needle).unwrap(), ["fetch"], "{needle}");
}
assert_eq!(
changed_component("def after_fetch").unwrap(),
["after_fetch"]
);
let fetch = functions(COMPONENTS)
.into_iter()
.find(|f| f.name == "fetch")
.unwrap();
assert_eq!(fetch.first_line, component_line("def fetch"));
assert_eq!(fetch.last_line, component_line("cleanup()") + 1);
}
#[test]
fn a_declaration_above_does_not_reach_past_another_function() {
let source = "defmodule A do\n attr :x, :string\n\n def one(assigns),\n do: assigns\n\n def two(assigns), do: assigns\nend\n";
let spans: Vec<(String, usize, usize)> = functions(source)
.into_iter()
.map(|f| (f.name, f.first_line, f.last_line))
.collect();
assert_eq!(
spans,
[("one".to_string(), 2, 5), ("two".to_string(), 7, 7)]
);
}