Skip to content

Commit 7d7f9d7

Browse files
value generator + tests
Signed-off-by: Gabriele Ghio <gabriele.ghio@secomind.com>
1 parent eacd23e commit 7d7f9d7

File tree

2 files changed

+226
-0
lines changed

2 files changed

+226
-0
lines changed
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
#
2+
# This file is part of Astarte.
3+
#
4+
# Copyright 2025 SECO Mind Srl
5+
#
6+
# Licensed under the Apache License, Version 2.0 (the "License");
7+
# you may not use this file except in compliance with the License.
8+
# You may obtain a copy of the License at
9+
#
10+
# http://www.apache.org/licenses/LICENSE-2.0
11+
#
12+
# Unless required by applicable law or agreed to in writing, software
13+
# distributed under the License is distributed on an "AS IS" BASIS,
14+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
# See the License for the specific language governing permissions and
16+
# limitations under the License.
17+
#
18+
19+
defmodule Astarte.Core.Generators.Value do
20+
@moduledoc """
21+
This module provides generators for any Value type.
22+
"""
23+
use ExUnitProperties
24+
25+
alias Astarte.Core.Interface
26+
alias Astarte.Core.Mapping
27+
28+
alias Astarte.Common.Generators.DateTime, as: DateTimeGenerator
29+
alias Astarte.Common.Generators.Timestamp, as: TimestampGenerator
30+
alias Astarte.Core.Generators.Mapping, as: MappingGenerator
31+
32+
@doc """
33+
Generates a valid value based on interface
34+
"""
35+
@spec value(Interface.t()) :: StreamData.t(map())
36+
def value(%Interface{} = interface) when not is_struct(interface, StreamData) do
37+
interface |> constant() |> value()
38+
end
39+
40+
@spec value(StreamData.t(Interface.t())) :: StreamData.t(map())
41+
def value(gen) do
42+
gen all %Interface{
43+
mappings: mappings,
44+
aggregation: aggregation
45+
} <- gen,
46+
%Mapping{
47+
endpoint: endpoint
48+
} <- member_of(mappings),
49+
endpoint = interface_endpoint(aggregation, endpoint),
50+
path <- endpoint_path(endpoint),
51+
value <- build_value(aggregation, mappings) do
52+
%{
53+
path: path,
54+
value: value
55+
}
56+
end
57+
end
58+
59+
defp interface_endpoint(:individual, endpoint), do: endpoint
60+
defp interface_endpoint(:object, endpoint), do: String.replace(endpoint, ~r"/[^/]+$", "")
61+
62+
defp endpoint_path(endpoint) do
63+
endpoint
64+
|> String.split("/")
65+
|> Enum.map(&convert_token/1)
66+
|> fixed_list()
67+
|> map(&Enum.join(&1, "/"))
68+
end
69+
70+
defp convert_token(token) do
71+
case(Mapping.is_placeholder?(token)) do
72+
true -> MappingGenerator.endpoint_segment()
73+
false -> constant(token)
74+
end
75+
end
76+
77+
defp build_value(:individual, [%Mapping{value_type: value_type} | _]) do
78+
value_from_type(value_type)
79+
end
80+
81+
defp build_value(:object, [%Mapping{} | _] = mappings) do
82+
mappings |> Map.new(&object_value/1) |> optional_map()
83+
end
84+
85+
defp type_array(:doublearray), do: :double
86+
defp type_array(:integerarray), do: :integer
87+
defp type_array(:longintegerarray), do: :longinteger
88+
defp type_array(:booleanarray), do: :boolean
89+
defp type_array(:stringarray), do: :string
90+
defp type_array(:binaryblobarray), do: :binaryblob
91+
defp type_array(:datetimearray), do: :datetime
92+
93+
defp value_from_type(:double), do: float()
94+
defp value_from_type(:integer), do: integer(-0x7FFFFFFF..0x7FFFFFFF)
95+
defp value_from_type(:boolean), do: boolean()
96+
defp value_from_type(:longinteger), do: integer(-0x7FFFFFFFFFFFFFFF..0x7FFFFFFFFFFFFFFF)
97+
defp value_from_type(:string), do: string(:utf8, max_length: 65_535)
98+
defp value_from_type(:binaryblob), do: map(binary(max_length: 65_535), &Base.encode64/1)
99+
100+
defp value_from_type(:datetime),
101+
do:
102+
one_of([
103+
TimestampGenerator.timestamp(),
104+
DateTimeGenerator.date_time() |> map(&DateTime.to_iso8601/1)
105+
])
106+
107+
defp value_from_type(array) when is_atom(array),
108+
do: type_array(array) |> value_from_type() |> list_of(max_length: 1023)
109+
110+
defp object_value(%Mapping{} = mapping) do
111+
%Mapping{endpoint: endpoint, value_type: value_type} = mapping
112+
{String.replace(endpoint, ~r"^.*/", ""), value_from_type(value_type)}
113+
end
114+
end
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
#
2+
# This file is part of Astarte.
3+
#
4+
# Copyright 2025 SECO Mind Srl
5+
#
6+
# Licensed under the Apache License, Version 2.0 (the "License");
7+
# you may not use this file except in compliance with the License.
8+
# You may obtain a copy of the License at
9+
#
10+
# http://www.apache.org/licenses/LICENSE-2.0
11+
#
12+
# Unless required by applicable law or agreed to in writing, software
13+
# distributed under the License is distributed on an "AS IS" BASIS,
14+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
# See the License for the specific language governing permissions and
16+
# limitations under the License.
17+
#
18+
19+
defmodule Astarte.Core.Generators.ValueTest do
20+
use ExUnit.Case, async: true
21+
use ExUnitProperties
22+
23+
alias Astarte.Core.Interface
24+
alias Astarte.Core.Mapping
25+
26+
alias Astarte.Core.Generators.Interface, as: InterfaceGenerator
27+
alias Astarte.Core.Generators.Mapping, as: MappingGenerator
28+
alias Astarte.Core.Generators.Value, as: ValueGenerator
29+
30+
@moduletag :value
31+
32+
defp valid?(:individual, %{value: value})
33+
when is_list(value) or is_float(value) or is_integer(value) or is_binary(value) or
34+
is_boolean(value) or is_struct(value, DateTime),
35+
do: true
36+
37+
defp valid?(:object, %{value: value}) when is_map(value), do: true
38+
39+
defp valid?(_, _), do: false
40+
41+
@endpoint_param_regex ~r"%{[\w_][\w\d_]*}"
42+
@endpoint_param_sub ~S"[\w_][\w\d_]*"
43+
defp build_regex(endpoint),
44+
do:
45+
@endpoint_param_regex
46+
|> Regex.replace(endpoint, @endpoint_param_sub)
47+
|> Regex.compile!()
48+
49+
defp path_matches_endpoint?(path, endpoint) do
50+
endpoint |> build_regex() |> Regex.match?(path)
51+
end
52+
53+
defp path_matches_endpoint?(:individual, path, endpoint),
54+
do: path_matches_endpoint?(path, endpoint)
55+
56+
defp path_matches_endpoint?(:object, path, endpoint),
57+
do: path_matches_endpoint?(path, String.replace(endpoint, ~r"/[^/]+$", ""))
58+
59+
@doc false
60+
describe "value generator" do
61+
@describetag :success
62+
@describetag :ut
63+
64+
property "generates value based on interface" do
65+
check all value <- InterfaceGenerator.interface() |> ValueGenerator.value() do
66+
assert %{path: _path, value: _value} = value
67+
end
68+
end
69+
70+
property "generates valid value based on aggregation" do
71+
check all aggregation <- one_of([:individual, :object]),
72+
value <-
73+
InterfaceGenerator.interface(aggregation: aggregation)
74+
|> ValueGenerator.value() do
75+
assert valid?(aggregation, value)
76+
end
77+
end
78+
79+
property "check if path matches at least one endpoint considering aggregation" do
80+
check all aggregation <- one_of([:individual, :object]),
81+
interface_type <- InterfaceGenerator.type(),
82+
mappings <-
83+
MappingGenerator.mapping(interface_type: interface_type)
84+
|> list_of(min_length: 1, max_length: 10),
85+
%{path: path} <-
86+
InterfaceGenerator.interface(
87+
type: interface_type,
88+
aggregation: aggregation,
89+
mappings: mappings
90+
)
91+
|> ValueGenerator.value() do
92+
assert Enum.any?(mappings, fn %Mapping{endpoint: endpoint} ->
93+
path_matches_endpoint?(aggregation, path, endpoint)
94+
end)
95+
end
96+
end
97+
98+
property "check field is present in object field (aggregation :object)" do
99+
check all %{mappings: mappings} = interface <-
100+
InterfaceGenerator.interface(aggregation: :object),
101+
%{value: value} <- ValueGenerator.value(interface),
102+
endpoints =
103+
mappings
104+
|> Enum.map(fn %Mapping{endpoint: endpoint} ->
105+
Regex.replace(~r"^.*/", endpoint, "")
106+
end),
107+
fields = value |> Enum.map(fn {field, _} -> field end) do
108+
assert Enum.all?(fields, &(&1 in endpoints))
109+
end
110+
end
111+
end
112+
end

0 commit comments

Comments
 (0)