Skip to content

Commit 193f4ab

Browse files
value + tests
Signed-off-by: Gabriele Ghio <gabriele.ghio@secomind.com>
1 parent 8012c5c commit 193f4ab

File tree

2 files changed

+313
-0
lines changed

2 files changed

+313
-0
lines changed
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
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.Mapping.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+
alias Astarte.Core.Mapping.ValueType
28+
29+
alias Astarte.Core.Generators.Mapping.ValueType, as: ValueTypeGenerator
30+
31+
alias Astarte.Common.Generators.DateTime, as: DateTimeGenerator
32+
alias Astarte.Common.Generators.Timestamp, as: TimestampGenerator
33+
34+
@doc """
35+
Generates a valid value based on interface
36+
"""
37+
@spec value(Interface.t()) :: StreamData.t(map())
38+
def value(%Interface{} = interface) when not is_struct(interface, StreamData) do
39+
interface |> constant() |> value()
40+
end
41+
42+
@spec value(StreamData.t(Interface.t())) :: StreamData.t(map())
43+
def value(gen), do: gen |> bind(&build_package/1)
44+
45+
defp build_package(%Interface{aggregation: :individual} = interface) do
46+
%Interface{mappings: mappings, aggregation: aggregation} = interface
47+
48+
gen all %Mapping{endpoint: endpoint, value_type: value_type} <- member_of(mappings),
49+
path <- endpoint_path(endpoint),
50+
value <- build_value(value_type) do
51+
%{path: path, value: value}
52+
end
53+
end
54+
55+
defp build_package(%Interface{aggregation: :object} = interface) do
56+
%Interface{mappings: [%Mapping{endpoint: endpoint} | _] = mappings, aggregation: aggregation} =
57+
interface
58+
59+
endpoint = endpoint |> String.split("/") |> Enum.drop(-1) |> Enum.join("/")
60+
61+
gen all path <- endpoint_path(endpoint),
62+
value <-
63+
mappings
64+
|> Enum.reduce(%{}, fn %Mapping{endpoint: endpoint, value_type: value_type}, acc ->
65+
Map.put(acc, endpoint_postfix(endpoint), build_value(value_type))
66+
end)
67+
|> optional_map() do
68+
%{path: path, value: value}
69+
end
70+
end
71+
72+
defp endpoint_path(endpoint) do
73+
endpoint
74+
|> String.split("/")
75+
|> Enum.map(&convert_token/1)
76+
|> fixed_list()
77+
|> map(&Enum.join(&1, "/"))
78+
end
79+
80+
defp endpoint_base(:individual, endpoint), do: endpoint
81+
82+
defp endpoint_base(:object, endpoint),
83+
do: endpoint_manipulate(endpoint, fn list -> Enum.drop(list, -1) end)
84+
85+
defp endpoint_postfix(endpoint), do: Regex.replace(~r/.*\//, endpoint, "")
86+
87+
defp build_value(value_type), do: ValueTypeGenerator.value_from_type(value_type)
88+
89+
# Utilities
90+
defp convert_token(token) do
91+
case(Mapping.is_placeholder?(token)) do
92+
true -> string(:alphanumeric, min_length: 1)
93+
false -> constant(token)
94+
end
95+
end
96+
97+
defp endpoint_manipulate(endpoint, manipulate_fn) do
98+
endpoint
99+
|> String.split("/")
100+
|> manipulate_fn.()
101+
|> Enum.join("/")
102+
end
103+
end
Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
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.Mapping.ValueTest do
20+
use ExUnit.Case, async: true
21+
use ExUnitProperties
22+
23+
alias Astarte.Core.Interface
24+
alias Astarte.Core.Mapping
25+
alias Astarte.Core.Mapping.EndpointsAutomaton
26+
27+
alias Astarte.Core.Generators.Interface, as: InterfaceGenerator
28+
alias Astarte.Core.Generators.Mapping, as: MappingGenerator
29+
alias Astarte.Core.Generators.Mapping.Value, as: ValueGenerator
30+
31+
@moduletag :value
32+
33+
# defp valid?(:individual, %{value: value})
34+
# when is_list(value) or is_float(value) or is_integer(value) or is_binary(value) or
35+
# is_boolean(value) or is_struct(value, DateTime),
36+
# do: true
37+
38+
# defp valid?(:object, %{value: value}) when is_map(value), do: true
39+
40+
# defp valid?(_, _), do: false
41+
42+
# @endpoint_param_regex ~r"%{[\w_][\w\d_]*}"
43+
# @endpoint_param_sub ~S"[\w_][\w\d_]*"
44+
45+
# defp build_regex(endpoint),
46+
# do:
47+
# @endpoint_param_regex
48+
# |> Regex.replace(endpoint, @endpoint_param_sub)
49+
# |> Regex.compile!()
50+
51+
# defp path_matches_endpoint?(path, endpoint) do
52+
# endpoint |> build_regex() |> Regex.match?(path)
53+
# end
54+
55+
# defp path_matches_endpoint?(:individual, path, endpoint),
56+
# do: path_matches_endpoint?(path, endpoint)
57+
58+
# defp path_matches_endpoint?(:object, path, endpoint),
59+
# do: path_matches_endpoint?(path, String.replace(endpoint, ~r"/[^/]+$", ""))
60+
61+
# defp type_array(:doublearray), do: :double
62+
# defp type_array(:integerarray), do: :integer
63+
# defp type_array(:longintegerarray), do: :longinteger
64+
# defp type_array(:booleanarray), do: :boolean
65+
# defp type_array(:stringarray), do: :string
66+
# defp type_array(:binaryblobarray), do: :binaryblob
67+
# defp type_array(:datetimearray), do: :datetime
68+
69+
# defp valid_value_for_value_type?(:double, value), do: is_float(value)
70+
71+
# defp valid_value_for_value_type?(:integer, value)
72+
# when is_integer(value) and value in -0x7FFFFFFF..0x7FFFFFFF,
73+
# do: true
74+
75+
# defp valid_value_for_value_type?(:boolean, value), do: is_boolean(value)
76+
77+
# defp valid_value_for_value_type?(:longinteger, value)
78+
# when is_integer(value) and value in -0x7FFFFFFFFFFFFFFF..0x7FFFFFFFFFFFFFFF,
79+
# do: true
80+
81+
# defp valid_value_for_value_type?(:string, value) when is_binary(value),
82+
# do: String.length(value) <= 65_535
83+
84+
# defp valid_value_for_value_type?(:binaryblob, value) do
85+
# case Base.decode64(value) do
86+
# {:ok, value} -> is_binary(value) and byte_size(value) <= 65_535
87+
# :error -> false
88+
# end
89+
# end
90+
91+
# defp valid_value_for_value_type?(:datetime, value) when is_integer(value), do: true
92+
93+
# defp valid_value_for_value_type?(:datetime, value) when is_binary(value) do
94+
# case DateTime.from_iso8601(value) do
95+
# {:ok, _datetime, _offset} -> true
96+
# {:error, _reason} -> false
97+
# end
98+
# end
99+
100+
# defp valid_value_for_value_type?(:datetime, _value), do: false
101+
102+
# defp valid_value_for_value_type?(array_type, value) when is_list(value) do
103+
# type = type_array(array_type)
104+
# Enum.all?(value, &valid_value_for_value_type?(type, &1))
105+
# end
106+
107+
# defp valid_value_for_path?(%Interface{} = interface, path, value) do
108+
# %Interface{mappings: mappings} = interface
109+
# {:ok, automaton} = EndpointsAutomaton.build(mappings)
110+
# {:ok, endpoint} = EndpointsAutomaton.resolve_path(path, automaton)
111+
112+
# %Mapping{value_type: value_type} =
113+
# mappings
114+
# |> Enum.find(&(&1.endpoint == endpoint)) || flunk("endpoint not found")
115+
116+
# valid_value_for_value_type?(value_type, value)
117+
# end
118+
119+
@doc false
120+
describe "value generator" do
121+
@describetag :success
122+
@describetag :ut
123+
124+
property "generates value based on interface" do
125+
gen = InterfaceGenerator.interface() |> ValueGenerator.value()
126+
127+
check all value <- gen do
128+
assert %{path: _path, value: _value} = value
129+
end
130+
end
131+
132+
@tag :temp
133+
property "generates value based on aggregation :object must have postfix different from other fields" do
134+
gen = InterfaceGenerator.interface(aggregation: :object) |> ValueGenerator.value()
135+
136+
check all %{path: path, value: value} <- gen do
137+
endpoint_postfix = path |> String.split("/") |> List.last()
138+
IO.inspect(endpoint_postfix)
139+
140+
for entry <- value do
141+
postfix = Map.keys(entry)
142+
IO.inspect(postfix)
143+
144+
refute postfix == endpoint_postfix
145+
end
146+
end
147+
end
148+
end
149+
150+
# @doc false
151+
# describe "value generator" do
152+
# @describetag :success
153+
# @describetag :ut
154+
155+
# property "generates value based on interface" do
156+
# check all value <- InterfaceGenerator.interface() |> ValueGenerator.value() do
157+
# assert %{path: _path, value: _value} = value
158+
# end
159+
# end
160+
161+
# property "generates valid value based on aggregation" do
162+
# check all aggregation <- one_of([:individual, :object]),
163+
# value <-
164+
# InterfaceGenerator.interface(aggregation: aggregation)
165+
# |> ValueGenerator.value() do
166+
# assert valid?(aggregation, value)
167+
# end
168+
# end
169+
170+
# property "generates values for the correct endpoint for :individual interfaces" do
171+
# check all interface <- InterfaceGenerator.interface(aggregation: :individual),
172+
# %{path: path, value: value} <- ValueGenerator.value(interface) do
173+
# assert valid_value_for_path?(interface, path, value)
174+
# end
175+
# end
176+
177+
# # property "check if path matches at least one endpoint considering aggregation" do
178+
# # check all aggregation <- one_of([:individual, :object]),
179+
# # interface_type <- InterfaceGenerator.type(),
180+
# # mappings <-
181+
# # MappingGenerator.mapping(interface_type: interface_type)
182+
# # |> list_of(min_length: 1, max_length: 10),
183+
# # %{path: path} <-
184+
# # InterfaceGenerator.interface(
185+
# # type: interface_type,
186+
# # aggregation: aggregation,
187+
# # mappings: mappings
188+
# # )
189+
# # |> ValueGenerator.value() do
190+
# # assert Enum.any?(mappings, fn %Mapping{endpoint: endpoint} ->
191+
# # path_matches_endpoint?(aggregation, path, endpoint)
192+
# # end)
193+
# # end
194+
# # end
195+
196+
# property "check field is present in object field (aggregation :object)" do
197+
# check all %{mappings: mappings} = interface <-
198+
# InterfaceGenerator.interface(aggregation: :object),
199+
# %{value: value} <- ValueGenerator.value(interface),
200+
# endpoints =
201+
# mappings
202+
# |> Enum.map(fn %Mapping{endpoint: endpoint} ->
203+
# Regex.replace(~r"^.*/", endpoint, "")
204+
# end),
205+
# fields = value |> Enum.map(fn {field, _} -> field end) do
206+
# assert Enum.all?(fields, &(&1 in endpoints))
207+
# end
208+
# end
209+
# end
210+
end

0 commit comments

Comments
 (0)