Skip to content

Commit 1e7a4f5

Browse files
generate edge cases more frequently
1 parent ed6048f commit 1e7a4f5

6 files changed

Lines changed: 104 additions & 19 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ All notable changes to this project will be documented in this file.
44

55
## Unreleased
66

7+
## 0.0.12 [2026-06-28]
8+
- Generate better extreme cases for "number", "integer" and "string" types
9+
710
## 0.0.11 [2026-06-26]
811
- Add `:string_kind` option to `RockSolid.from_schema/2`
912

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ Add to your list of dependencies
1010
```elixir
1111
def deps do
1212
[
13-
{:rock_solid, "~> 0.0.11", only: :test}
13+
{:rock_solid, "~> 0.0.12", only: :test}
1414
]
1515
end
1616
```

lib/rock_solid.ex

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ defmodule RockSolid do
6666

6767
@opts_schema [
6868
resolver: [type: :mod_arg, required: true],
69-
string_kind: [type: :atom, default: :printable]
69+
string_kind: [type: :atom, default: nil]
7070
]
7171

7272
@doc """
@@ -76,7 +76,8 @@ defmodule RockSolid do
7676
7777
- `:resolver` - Either a module or a tuple {module, args} that implements the
7878
`RockSolid.Resolution.Resolver` behaviour. Defaults to [`DummyResolver`](`RockSolid.Resolution.Resolvers.DummyResolver`).
79-
- `:string_kind` - The kind of strings to generate. See `StreamData.string/2`. Defaults to `:printable`
79+
- `:string_kind` - The kind of strings to generate. See `StreamData.string/2`. Defaults to
80+
generating `:utf8` strings
8081
"""
8182
def from_schema(json_schema, opts \\ []) do
8283
opts = opts |> parse_resolver() |> NimbleOptions.validate!(@opts_schema)

lib/rock_solid/strategy.ex

Lines changed: 88 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -54,21 +54,20 @@ defmodule RockSolid.Strategy do
5454
end
5555

5656
defp gen(%{"type" => "integer"} = schema, _) do
57-
[min: Schemas.Number.min_value(schema), max: Schemas.Number.max_value(schema)]
58-
|> Keyword.filter(fn {_, v} -> not is_nil(v) end)
59-
|> MoreStreamData.more_integer()
57+
StreamData.frequency([
58+
{90, regular_integer(schema)},
59+
{5, close_to_max(schema)},
60+
{5, close_to_min(schema)}
61+
])
6062
|> filter_value(schema["exclusiveMinimum"])
6163
|> filter_value(schema["exclusiveMaximum"])
6264
end
6365

64-
defp gen(%{"type" => "number"} = schema, _) do
65-
[
66-
min: Schemas.Number.min_value(schema),
67-
max: Schemas.Number.max_value(schema),
68-
exclude_min?: Map.has_key?(schema, "exclusiveMinimum"),
69-
exclude_max?: Map.has_key?(schema, "exclusiveMaximum")
70-
]
71-
|> MoreStreamData.more_float()
66+
defp gen(%{"type" => "number"} = schema, opts) do
67+
StreamData.frequency([
68+
{50, gen(float_to_int(schema), opts)},
69+
{50, gen_float(schema)}
70+
])
7271
end
7372

7473
# pattern always takes priority above format, because format is not stanarized
@@ -91,10 +90,17 @@ defmodule RockSolid.Strategy do
9190
end
9291

9392
defp gen(%{"type" => "string"} = schema, opts) do
94-
StreamData.string(
95-
Keyword.fetch!(opts, :string_kind),
96-
to_keyword(schema, min_length: "minLength", max_length: "maxLength")
97-
)
93+
string_opts = to_keyword(schema, min_length: "minLength", max_length: "maxLength")
94+
95+
case Keyword.fetch!(opts, :string_kind) do
96+
nil ->
97+
[:utf8, :printable, :ascii]
98+
|> Enum.map(fn codepoints -> {1, StreamData.string(codepoints, string_opts)} end)
99+
|> StreamData.frequency()
100+
101+
kind ->
102+
StreamData.string(kind, string_opts)
103+
end
98104
|> filter_min_length(schema["minLength"])
99105
end
100106

@@ -503,4 +509,71 @@ defmodule RockSolid.Strategy do
503509
end
504510
end)
505511
end
512+
513+
defp regular_integer(schema) do
514+
[min: Schemas.Number.min_value(schema), max: Schemas.Number.max_value(schema)]
515+
|> Keyword.filter(fn {_, v} -> not is_nil(v) end)
516+
|> MoreStreamData.more_integer()
517+
end
518+
519+
defp close_to_max(schema) do
520+
max = Schemas.Number.max_value(schema) || 2_147_483_647
521+
min = Schemas.Number.min_value(schema) || -2_147_483_648
522+
523+
StreamData.integer(0..(max - min))
524+
|> StreamData.map(fn val -> max - val end)
525+
end
526+
527+
defp close_to_min(schema) do
528+
max = Schemas.Number.max_value(schema) || 2_147_483_647
529+
min = Schemas.Number.min_value(schema) || -2_147_483_648
530+
531+
StreamData.integer(0..(max - min))
532+
|> StreamData.map(fn val -> val + min end)
533+
end
534+
535+
defp gen_float(schema) do
536+
StreamData.frequency([
537+
{90, regular_float(schema)},
538+
{5, close_to_max_float(schema)},
539+
{5, close_to_min_float(schema)}
540+
])
541+
end
542+
543+
defp regular_float(schema) do
544+
[
545+
min: Schemas.Number.min_value(schema),
546+
max: Schemas.Number.max_value(schema),
547+
exclude_min?: Map.has_key?(schema, "exclusiveMinimum"),
548+
exclude_max?: Map.has_key?(schema, "exclusiveMaximum")
549+
]
550+
|> MoreStreamData.more_float()
551+
end
552+
553+
defp close_to_max_float(schema) do
554+
max = Schemas.Number.max_value(schema) || 8.94e307
555+
min = Schemas.Number.min_value(schema) || -8.94e307
556+
557+
StreamData.float(min: 0, max: max - min)
558+
|> StreamData.map(fn val -> max - val end)
559+
end
560+
561+
defp close_to_min_float(schema) do
562+
max = Schemas.Number.max_value(schema) || 8.94e307
563+
min = Schemas.Number.min_value(schema) || -8.94e307
564+
565+
StreamData.float(min: 0, max: max - min)
566+
|> StreamData.map(fn val -> val + min end)
567+
end
568+
569+
defp float_to_int(%{"type" => "number"} = schema) do
570+
["minimum", "maximum", "exclusiveMinimum", "exclusiveMaximum"]
571+
|> Enum.reduce(Map.put(schema, "type", "integer"), fn key, acc_schema ->
572+
if Map.has_key?(acc_schema, key) do
573+
Map.update!(acc_schema, key, &trunc/1)
574+
else
575+
acc_schema
576+
end
577+
end)
578+
end
506579
end

mix.exs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ defmodule RockSolid.MixProject do
22
use Mix.Project
33

44
@source_url "https://github.com/IgnacioGoldchluk/rock_solid"
5-
@version "0.0.11"
5+
@version "0.0.12"
66

77
def project do
88
[

test/rock_solid/strategy_test.exs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,14 @@ defmodule RockSolid.StrategyTest do
55
alias RockSolid.Schemas.Vocabulary
66

77
describe "from_json_schema/1" do
8+
property "number generates both integers and floats" do
9+
schema = %{"type" => "number", "minimum" => -1.0, "maximum" => 250.5}
10+
11+
check all num <- RockSolid.from_schema(schema) do
12+
assert num >= -1.0 and num <= 250.5
13+
end
14+
end
15+
816
property "generates strings of the specified codepoints" do
917
schema = %{
1018
"type" => "object",

0 commit comments

Comments
 (0)