proper-testing / proper

PropEr: a QuickCheck-inspired property-based testing tool for Erlang
http://proper-testing.github.io
GNU General Public License v3.0
882 stars 167 forks source link

Limiting the size/complexity of the commands generated by proper_fsm #315

Open rvangraan opened 11 months ago

rvangraan commented 11 months ago

Hi there,

I'm working on tests that rely on proper_fsm. I'm noticing that proper_fsm:commands(?MODULE) generates truly gigantic command sequences. I'm using a lot of inferred state, i.e. delayed symbolic evaluations to effect state and it seems that sometimes these things becoming recursive. With that I mean code like this, that uses symbolic functions to calculate state:

...
next_state_data(try_connect_client, _, S, Result, {call,_,connect,_}) ->
    S#{
        client_state => {call, ?MODULE, derive_client_state, [Result]}
    };
next_state_data(client_connected, _, S, Result, {call,_,tcp_send,[_, Payload]}) ->
    S#{
        payloads_sent => {call, ?MODULE, derive_add_payload_if_sent, [S, Result, Payload]},
        payload_counter => {call, ?MODULE, derive_payload_counter, [S, Result]}     
    };
next_state_data(client_connected, _, S, _Result, {call,_,disconnect,_}) ->
    S#{client_state => disconnected}.

Some of the more extreme examples (That don't blow up):

Cmd len: 26 size: 92,736,416 bytes

If I sample a few of the outputs, you'll see the delayed evaluation calls:

{ok,{forall,[{set,{var,1},
                  {call,gen_tcp_test_proxy,resume,[{var,'PID'}]}},
             {set,{var,2},
                  {call,gen_tcp_test_proxy,connect,[{var,'PID'}]}},
             {set,{var,3},
                  {call,gen_tcp_test_proxy,tcp_send,
                        [{var,'PID'},
                         {call,gen_tcp_server_layer_tests,payload,
                               [#{payload_counter => 1,lower_layer_state => offline,
                                  client_state =>
                                      {call,gen_tcp_server_layer_tests,derive_client_state,
                                            [{var,2}]},
                                  stack_state => offline}]}]}},
             {set,{var,4},
                  {call,gen_tcp_test_proxy,disconnect,[{var,'PID'}]}},
             {set,{var,5},
                  {call,gen_tcp_test_proxy,connect,[{var,'PID'}]}},
             {set,{var,6},
                  {call,gen_tcp_test_proxy,disconnect,[{var,'PID'}]}},
             {set,{var,7},
                  {call,gen_tcp_test_proxy,signal_from_bottom,
                        [{var,'PID'},layer_up]}},
             {set,{var,8},
                  {call,gen_tcp_test_proxy,connect,[{var,'PID'}]}}],

This code works fine for shorter sequences, but the generator produces gigantic examples, that literally causes my beam VM to run out of memory.

So is there a way to limit the size of these things?

Here is a (bigger) example. There is nothing wrong with this test case, it does what it's supposed to. The issue is that the generator produces much, much bigger ones.

{ok,{forall,[{set,{var,1},
                  {call,gen_tcp_test_proxy,resume,[{var,'PID'}]}},
             {set,{var,2},
                  {call,gen_tcp_test_proxy,connect,[{var,'PID'}]}},
             {set,{var,3},
                  {call,gen_tcp_test_proxy,tcp_send,
                        [{var,'PID'},
                         {call,gen_tcp_server_layer_tests,payload,
                               [#{payload_counter => 1,lower_layer_state => offline,
                                  client_state =>
                                      {call,gen_tcp_server_layer_tests,derive_client_state,
                                            [{var,2}]},
                                  stack_state => offline}]}]}},
             {set,{var,4},
                  {call,gen_tcp_test_proxy,tcp_send,
                        [{var,'PID'},
                         {call,gen_tcp_server_layer_tests,payload,
                               [#{payloads_sent =>
                                      {call,gen_tcp_server_layer_tests,derive_add_payload_if_sent,
                                            [#{payload_counter => 1,lower_layer_state => offline,
                                               client_state =>
                                                   {call,gen_tcp_server_layer_tests,derive_client_state,
                                                         [{var,2}]},
                                               stack_state => offline},
                                             {var,3},
                                             {call,gen_tcp_server_layer_tests,payload,
                                                   [#{payload_counter => 1,lower_layer_state => offline,
                                                      client_state =>
                                                          {call,gen_tcp_server_layer_tests,derive_client_state,
                                                                [{var,2}]},
                                                      stack_state => offline}]}]},
                                  payload_counter =>
                                      {call,gen_tcp_server_layer_tests,derive_payload_counter,
                                            [#{payload_counter => 1,lower_layer_state => offline,
                                               client_state =>
                                                   {call,gen_tcp_server_layer_tests,derive_client_state,
                                                         [{var,2}]},
                                               stack_state => offline},
                                             {var,3}]},
                                  lower_layer_state => offline,
                                  client_state =>
                                      {call,gen_tcp_server_layer_tests,derive_client_state,
                                            [{var,2}]},
                                  stack_state => offline}]}]}},
             {set,{var,5},
                  {call,gen_tcp_test_proxy,tcp_send,
                        [{var,'PID'},
                         {call,gen_tcp_server_layer_tests,payload,
                               [#{payloads_sent =>
                                      {call,gen_tcp_server_layer_tests,derive_add_payload_if_sent,
                                            [#{payloads_sent =>
                                                   {call,gen_tcp_server_layer_tests,derive_add_payload_if_sent,
                                                         [#{payload_counter => 1,lower_layer_state => offline,
                                                            client_state =>
                                                                {call,gen_tcp_server_layer_tests,derive_client_state,
                                                                      [{var,2}]},
                                                            stack_state => offline},
                                                          {var,3},
                                                          {call,gen_tcp_server_layer_tests,payload,
                                                                [#{payload_counter => 1,lower_layer_state => offline,
                                                                   client_state =>
                                                                       {call,gen_tcp_server_layer_tests,derive_client_state,
                                                                             [{var,2}]},
                                                                   stack_state => offline}]}]},
                                               payload_counter =>
                                                   {call,gen_tcp_server_layer_tests,derive_payload_counter,
                                                         [#{payload_counter => 1,lower_layer_state => offline,
                                                            client_state =>
                                                                {call,gen_tcp_server_layer_tests,derive_client_state,
                                                                      [{var,2}]},
                                                            stack_state => offline},
                                                          {var,3}]},
                                               lower_layer_state => offline,
                                               client_state =>
                                                   {call,gen_tcp_server_layer_tests,derive_client_state,
                                                         [{var,2}]},
                                               stack_state => offline},
                                             {var,4},
                                             {call,gen_tcp_server_layer_tests,payload,
                                                   [#{payloads_sent =>
                                                          {call,gen_tcp_server_layer_tests,derive_add_payload_if_sent,
                                                                [#{payload_counter => 1,lower_layer_state => offline,
                                                                   client_state =>
                                                                       {call,gen_tcp_server_layer_tests,derive_client_state,
                                                                             [{var,2}]},
                                                                   stack_state => offline},
                                                                 {var,3},
                                                                 {call,gen_tcp_server_layer_tests,payload,
                                                                       [#{payload_counter => 1,lower_layer_state => offline,
                                                                          client_state =>
                                                                              {call,gen_tcp_server_layer_tests,derive_client_state,
                                                                                    [{var,2}]},
                                                                          stack_state => offline}]}]},
                                                      payload_counter =>
                                                          {call,gen_tcp_server_layer_tests,derive_payload_counter,
                                                                [#{payload_counter => 1,lower_layer_state => offline,
                                                                   client_state =>
                                                                       {call,gen_tcp_server_layer_tests,derive_client_state,
                                                                             [{var,2}]},
                                                                   stack_state => offline},
                                                                 {var,3}]},
                                                      lower_layer_state => offline,
                                                      client_state =>
                                                          {call,gen_tcp_server_layer_tests,derive_client_state,
                                                                [{var,2}]},
                                                      stack_state => offline}]}]},
                                  payload_counter =>
                                      {call,gen_tcp_server_layer_tests,derive_payload_counter,
                                            [#{payloads_sent =>
                                                   {call,gen_tcp_server_layer_tests,derive_add_payload_if_sent,
                                                         [#{payload_counter => 1,lower_layer_state => offline,
                                                            client_state =>
                                                                {call,gen_tcp_server_layer_tests,derive_client_state,
                                                                      [{var,2}]},
                                                            stack_state => offline},
                                                          {var,3},
                                                          {call,gen_tcp_server_layer_tests,payload,
                                                                [#{payload_counter => 1,lower_layer_state => offline,
                                                                   client_state =>
                                                                       {call,gen_tcp_server_layer_tests,derive_client_state,
                                                                             [{var,2}]},
                                                                   stack_state => offline}]}]},
                                               payload_counter =>
                                                   {call,gen_tcp_server_layer_tests,derive_payload_counter,
                                                         [#{payload_counter => 1,lower_layer_state => offline,
                                                            client_state =>
                                                                {call,gen_tcp_server_layer_tests,derive_client_state,
                                                                      [{var,2}]},
                                                            stack_state => offline},
                                                          {var,3}]},
                                               lower_layer_state => offline,
                                               client_state =>
                                                   {call,gen_tcp_server_layer_tests,derive_client_state,
                                                         [{var,2}]},
                                               stack_state => offline},
                                             {var,4}]},
                                  lower_layer_state => offline,
                                  client_state =>
                                      {call,gen_tcp_server_layer_tests,derive_client_state,
                                            [{var,2}]},
                                  stack_state => offline}]}]}},
             {set,{var,6},
                  {call,gen_tcp_test_proxy,disconnect,[{var,'PID'}]}},
             {set,{var,7},
                  {call,gen_tcp_test_proxy,signal_from_bottom,
                        [{var,'PID'},layer_up]}}],
            #Fun<gen_tcp_server_layer_tests.2.77195010>}}

Thanks!

R

rvangraan commented 11 months ago

Update: I've implemented a work-around that successfully avoids the massive test cases:

precondition(_, _,S = #{}, {call, _, Call, _}) when Call =:= tcp_send orelse Call =:= connect ->
    Size = size(term_to_binary(S)),
    case Size > ?MEGABYTE(?MAX_DEEP_STATE_SIZE_MB) of
        true ->
            io:format("\nState size: ~p exceeds maximum threshold of ~pMb attempting call ~300p. Skipping", [Size, ?MAX_DEEP_STATE_SIZE_MB,Call]);
        false ->
            ok
    end,
    Size < ?MEGABYTE(?MAX_DEEP_STATE_SIZE_MB);
precondition(_, _,_S = #{}, _call) ->
    true.

Basically, the calls that result in deep state mutations have been limited by a precondition that checks the size of the symbolic state.