Skip to content

Commit d45f7c0

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

File tree

2 files changed

+305
-0
lines changed

2 files changed

+305
-0
lines changed
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
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
44+
gen
45+
|> bind(&value_from_interface/1)
46+
47+
# gen all %Interface{
48+
# mappings: mappings,
49+
# aggregation: aggregation
50+
# } <- gen,
51+
# %Mapping{
52+
# endpoint: endpoint
53+
# } <- member_of(mappings),
54+
# endpoint = interface_endpoint(aggregation, endpoint),
55+
# path <- endpoint_path(endpoint),
56+
# value <- build_value(aggregation, mappings) do
57+
# %{
58+
# path: path,
59+
# value: value
60+
# }
61+
# end
62+
end
63+
64+
defp value_from_interface(%Interface{aggregation: :individual} = interface) do
65+
end
66+
67+
defp value_from_interface(%Interface{aggregation: :object} = interface) do
68+
end
69+
70+
defp interface_endpoint(:individual, endpoint), do: endpoint
71+
defp interface_endpoint(:object, endpoint), do: String.replace(endpoint, ~r"/[^/]+$", "")
72+
73+
defp endpoint_path(endpoint) do
74+
endpoint
75+
|> String.split("/")
76+
|> Enum.map(&convert_token/1)
77+
|> fixed_list()
78+
|> map(&Enum.join(&1, "/"))
79+
end
80+
81+
defp convert_token(token) do
82+
case(Mapping.is_placeholder?(token)) do
83+
true -> string(:alphanumeric, min_length: 1)
84+
false -> constant(token)
85+
end
86+
end
87+
88+
defp build_value(:individual, mappings) do
89+
%Mapping{value_type: value_type} = mappings |> Enum.at(0)
90+
value_from_type(value_type)
91+
end
92+
93+
defp build_value(:object, mappings) do
94+
mappings |> Map.new(&object_value/1) |> optional_map()
95+
end
96+
97+
defp type_array(:doublearray), do: :double
98+
defp type_array(:integerarray), do: :integer
99+
defp type_array(:longintegerarray), do: :longinteger
100+
defp type_array(:booleanarray), do: :boolean
101+
defp type_array(:stringarray), do: :string
102+
defp type_array(:binaryblobarray), do: :binaryblob
103+
defp type_array(:datetimearray), do: :datetime
104+
105+
defp value_from_type(:double), do: float()
106+
defp value_from_type(:integer), do: integer(-0x7FFFFFFF..0x7FFFFFFF)
107+
defp value_from_type(:boolean), do: boolean()
108+
defp value_from_type(:longinteger), do: integer(-0x7FFFFFFFFFFFFFFF..0x7FFFFFFFFFFFFFFF)
109+
defp value_from_type(:string), do: string(:utf8, max_length: 65_535)
110+
defp value_from_type(:binaryblob), do: map(binary(max_length: 65_535), &Base.encode64/1)
111+
112+
defp value_from_type(:datetime),
113+
do:
114+
one_of([
115+
TimestampGenerator.timestamp(),
116+
DateTimeGenerator.date_time() |> map(&DateTime.to_iso8601/1)
117+
])
118+
119+
defp value_from_type(array) when is_atom(array),
120+
do: type_array(array) |> value_from_type() |> list_of(max_length: 1023)
121+
122+
defp object_value(%Mapping{} = mapping) do
123+
%Mapping{endpoint: endpoint, value_type: value_type} = mapping
124+
{String.replace(endpoint, ~r"^.*/", ""), value_from_type(value_type)}
125+
end
126+
end
Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
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.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+
check all value <- InterfaceGenerator.interface() |> ValueGenerator.value() do
126+
assert %{path: _path, value: _value} = value
127+
end
128+
end
129+
130+
property "generates valid value based on aggregation" do
131+
check all aggregation <- one_of([:individual, :object]),
132+
value <-
133+
InterfaceGenerator.interface(aggregation: aggregation)
134+
|> ValueGenerator.value() do
135+
assert valid?(aggregation, value)
136+
end
137+
end
138+
139+
property "generates values for the correct endpoint for :individual interfaces" do
140+
check all interface <- InterfaceGenerator.interface(aggregation: :individual),
141+
%{path: path, value: value} <- ValueGenerator.value(interface) do
142+
assert valid_value_for_path?(interface, path, value)
143+
end
144+
end
145+
146+
# property "check if path matches at least one endpoint considering aggregation" do
147+
# check all aggregation <- one_of([:individual, :object]),
148+
# interface_type <- InterfaceGenerator.type(),
149+
# mappings <-
150+
# MappingGenerator.mapping(interface_type: interface_type)
151+
# |> list_of(min_length: 1, max_length: 10),
152+
# %{path: path} <-
153+
# InterfaceGenerator.interface(
154+
# type: interface_type,
155+
# aggregation: aggregation,
156+
# mappings: mappings
157+
# )
158+
# |> ValueGenerator.value() do
159+
# assert Enum.any?(mappings, fn %Mapping{endpoint: endpoint} ->
160+
# path_matches_endpoint?(aggregation, path, endpoint)
161+
# end)
162+
# end
163+
# end
164+
165+
property "check field is present in object field (aggregation :object)" do
166+
check all %{mappings: mappings} = interface <-
167+
InterfaceGenerator.interface(aggregation: :object),
168+
%{value: value} <- ValueGenerator.value(interface),
169+
endpoints =
170+
mappings
171+
|> Enum.map(fn %Mapping{endpoint: endpoint} ->
172+
Regex.replace(~r"^.*/", endpoint, "")
173+
end),
174+
fields = value |> Enum.map(fn {field, _} -> field end) do
175+
assert Enum.all?(fields, &(&1 in endpoints))
176+
end
177+
end
178+
end
179+
end

0 commit comments

Comments
 (0)