igalic

igalic

How to create a custom StreamData Generator

Hi folks,

I’m trying to transform our tests for AuthToken (a wrapper around JOSE) to ExUnitProperties. But I’m kinda failing at the most basic task of transforming a simple function into a generator. What I’ve done is this:

  defp stream_authtoken_keys(_seed, _range) do
    Stream.repeatedly(&AuthToken.generate_key/0)
  end

  defp gen_authtoken_key() do
    %StreamData{generator: &stream_authtoken_keys/2}
  end

However, when trying to use it…

  describe "keys" do
    property "generate_key/0 returns a valid AES128 key" do
      check all authtoken_key <- gen_authtoken_key()  do
       {:ok, key} = authtoken_key
        assert byte_size(key) == 16
      end
    end
  end

I get the following error:

  1) property keys generate_key/0 returns a valid AES128 key (AuthTokenTest)
     test/authtoken_test.exs:23
     ** (MatchError) no match of right hand side value: #Function<51.91433161/2 in Stream.repeatedly/1>
     code: check all authtoken_key <- gen_authtoken_key()  do
     stacktrace:
       (stream_data) lib/stream_data.ex:203: StreamData.bind_filter/5
       (stream_data) lib/stream_data.ex:346: anonymous fn/5 in StreamData.bind_filter/3
       (stream_data) lib/stream_data.ex:203: StreamData.check_all/6
       test/authtoken_test.exs:24: (test)

Marked As Solved

igalic

igalic

After posting this in elixir (on irc & discord) I got a very decent reply that works out pretty fine, so I’m gonna close this topic for now with it!

First off, trying to generate %StreamData{} myself, is kinda bad form, since it’s basically a private struct…
We looked for alternatives, and found StreamData.constant/0. However, on its own, it always generates the same value.

Which makes a certain amount of sense when you think about how it’s called:

iex(1)> x = StreamData.constant(AuthToken.generate_key) # this is now evaluated, we're done here
%StreamData{generator: #Function<16.32994346/2 in StreamData.constant/1>}
iex(2)> x |> Enum.take(2)                              
[
  ok: <<109, 179, 255, 84, 16, 160, 239, 132, 198, 157, 218, 254, 13, 160, 196, 137>>,
  ok: <<109, 179, 255, 84, 16, 160, 239, 132, 198, 157, 218, 254, 13, 160, 196, 137>>
]
iex(3)>

The clue came from @michalmuskala:

You usually construct the opaque type one way or the other, you rarely construct it directly. For example the MapSet struct is opaque, but you can generate it by doing something like map(list(), &MapSet.new/1)

and the code we now have from @ericmj

  defp gen_authtoken_key() do
    StreamData.map(StreamData.list_of(StreamData.constant(:unused_tick)), fn _ ->
      AuthToken.generate_key()
    end)
  end

what we’re doing here is using StreamData.constant/1 as a sort of clock generator, that drives the call of AuthToken.generate_key/0… and it’s nice start, but we’re needlessly generating a list_of which we then map
So let’s update this with all responses below!

  defp gen_authtoken_key() do
    StreamData.unshrinkable(
      StreamData.bind(StreamData.constant(:unused), fn _ ->
        StreamData.constant(AuthToken.generate_key())
      end)
    )
  end

This is much cleaner! Since we use bind/2, which is essentially made for this… better see what’s going on, we can use pipes to extract the flow:

  defp gen_authtoken_key() do
    gen_ticker = fn _ ->
      StreamData.constant(AuthToken.generate_key())
    end

    StreamData.constant(:unused)
    |> StreamData.bind(gen_ticker)
    |> StreamData.unshrinkable()
  end

We feed a stream of :unused into gen_ticker lambda, just so it will be constantly called. Since our call to AuthToken.generate_key/0 is wrapped in said lambda, it will be executed on every call, rather than on ge_ticker’s declaration. Finally, we pass the so generated %StreamData{} into unshrinkable/0 (because it is) before returning it.

Also Liked

whatyouhide

whatyouhide

Elixir Core Team

Seeing this now after Add StreamData.repeatedly to pass in a function as your generator by aforward · Pull Request #104 · whatyouhide/stream_data · GitHub has been opened. Just wanted to mention here too that I also think that a generic way to convert streams to generators would be nice but there is a big limitation: generators are stateless while streams aren’t. Streams are enumerated in order and there is no way for generators to know at which index in the stream it’s at.

@ericmj also your solution can be even simpler as bind + constant is just map:

def repeatedly(fun) do
  StreamData.map(StreamData.constant(nil), fn _ -> fun.() end)
end
ericmj

ericmj

Elixir Core Team

I found a (maybe) cleaner way of doing it that doesn’t involve generating a list:

defp gen_authtoken_key() do
  StreamData.bind(StreamData.constant(:unused), fn _ ->
    StreamData.constant(AuthToken.generate_key())
  end)
end

I haven’t tested this so I can’t verify that it works.

EDIT: @whatyouhide has a more concise solution below: How to create a custom StreamData Generator - #12 by whatyouhide

LostKobrakai

LostKobrakai

It might also be good to make the generator unshrinkable, because shrinking doesn’t seem to make sense in that context.

Where Next?

Popular in Questions Top

_russellb
I want to try my hand at web scraping. What tools/libraries do I need to use. I’m hoping to turn this into something professional so don’...
New
New
9mm
I am constructing a JSON object (map) and I need to conditionally set a field. I’m trying to write proper elixir-way code… and I’m at a l...
New
Harrisonl
We have an ECS cluster with 4 services, where each task joins a single cluster, via discovery ECS discovery service. Currently when I de...
New
senggen
Erlang/OTP 25 [erts-13.2.2] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] 15:22:35.803 [error] gen_event {lager_file_backend...
New
tduccuong
Hi, is there any work on GUI with Elixir, that is similar to Electron/Javascript? My idea is to bundle Phoenix and BEAM into a single se...
New
jononomo
I am trying to figure out how Mix knows whether the environment is test, dev, or prod – where is this set? Thanks.
New
sergio_101
I am VERY much an elixir newbie. I have taken one elixir course and one phoenix course on Udemy. During that course, I saw the instructor...
New
fayddelight
I tried installing elixir 1.11.2 erlang 23.3.4 via asdf in my zsh shell. Enabled the versions locally and globally. When I list them ...
New
WestKeys
Currently suffering from paralysis by [HTTP client] analysis. This is rather unusual in Elixirland as there tends to be consensus on the ...
New

Other popular topics Top

msaraiva
Surface is an experimental library built on top of Phoenix LiveView and its new LiveComponent API that aims to provide a more declarative...
564 43622 214
New
chrismccord
This release brings a number of exciting features, including integration with the new Phoenix LiveDashboard and Phoenix LiveView. There h...
New
vrod
I am using the Starship cross-shell prompt – it seems pretty nice, but I get some errors: [WARN] - (starship::utils): Executing command ...
New
freewebwithme
Using vs code and installed ElixirLS: support and debugger. And I got an error popped up on start up says Failed to run ‘elixir’ comma...
New
RisingFromAshes
I’ve read in another post that it may be possible with a router helper - but I couldn’t find an appropriate one, and tbh, I’m still just ...
New
fayddelight
I tried installing elixir 1.11.2 erlang 23.3.4 via asdf in my zsh shell. Enabled the versions locally and globally. When I list them ...
New
saif
Hello everyone, Long time lurker first time poster here. I’ve recently begun working on Elixir full-time again! :raised_hands: It’s been...
New
Brian
What is the proper way to load a module from a file in to IEX? In the python world, doing something like this pretty standard: from ....
New
svb
Hi! Currently I want to submit a form by pressing the Enter key. However, since my input field is of type “textarea” this is just adds a...
New
lanycrost
Hi everyone! I need implement if…else if…else condition from my elixir code, and anymore of this control flow structures not work proper...
New

We're in Beta

About us Mission Statement