Permalink
Browse files

increase coverage of statem properties

  • Loading branch information...
1 parent 4e7c8a5 commit 4f1a36efdac05b79bb28a1ff68267258290508a4 @hyperthunk committed Nov 15, 2011
Showing with 215 additions and 53 deletions.
  1. +10 −8 include/xml_writer.hrl
  2. +0 −18 rebar.config
  3. +13 −0 release.config
  4. +127 −12 src/xml_writer.erl
  5. +15 −0 test.config
  6. +16 −0 test/test_helper.erl
  7. +34 −15 test/xml_writer_fsm_props.erl
View
@@ -25,19 +25,21 @@
-opaque writer() :: pid() | atom().
%% TODO: it seems there is no way to state that element_name must be non-empty!
--type element_name() :: iodata().
--type ns_name() :: iodata().
+-type namespace_prefix() :: binary() | string().
+-type namespace_uri() :: binary() | string().
+-type element_name() :: iodata().
+-type ns() :: iodata().
-record(stack_frame, {
- ns_name :: ns_name(),
- local_name :: element_name(),
- has_attributes :: boolean(),
- has_children :: boolean()
+ ns :: ns(),
+ local_name :: element_name(),
+ has_attributes :: boolean(),
+ has_children = false :: boolean()
}).
-record(writer, {
- ns = [] :: list(tuple(string(), string())),
- ns_stack = [] :: list(string()),
+ ns = [] :: list(tuple(namespace_prefix(), namespace_uri())),
+ ns_stack = [] :: list(namespace_uri()),
stack = [] :: [#stack_frame{}],
quote = <<"\"">> :: binary(),
prettyprint = false :: boolean(),
View
@@ -1,20 +1,2 @@
-{deps, [
- {hamcrest, ".*",
- {git, "https://github.com/hyperthunk/hamcrest-erlang.git"}},
- {proper, "1.0",
- {git, "http://github.com/manopapad/proper.git", "master"}},
- {rebar_dist_plugin, "0.0.6",
- {git, "git://github.com/hyperthunk/rebar_dist_plugin.git"}}
-]}.
-
-{cover_enabled, true}.
-{cover_print_enabled, true}.
% {erl_opts, [warnings_as_errors]}.
-
-{plugins, [rebar_dist_plugin]}.
-
-{dist, [
- {format, zip},
- {assembly, project}
-]}.
View
@@ -0,0 +1,13 @@
+
+{extend, "rebar.config"}.
+{deps, [
+ {rebar_dist_plugin, "0.0.6",
+ {git, "git://github.com/hyperthunk/rebar_dist_plugin.git"}}
+]}.
+
+{plugins, [rebar_dist_plugin]}.
+
+{dist, [
+ {format, zip},
+ {assembly, project}
+]}.
View
@@ -23,7 +23,10 @@
-behaviour(gen_fsm).
%% API exports
--export([new/1, new/2, close/1]).
+-export([file_writer/1, file_writer/2, file_writer/3]).
+-export([new/1, new/2, new/3, new/4]).
+-export([close/1]).
+-export([add_namespace/3]).
-export([write_value/2, start_element/2, end_element/1]).
%% gen_fsm state-name exports
@@ -39,10 +42,44 @@
-include("xml_writer.hrl").
+-define(OPEN_BRACE, <<"<">>).
+-define(FWD_SLASH, <<"/">>).
+-define(CLOSE_BRACE, <<">">>).
+-define(SPACE, <<" ">>).
+
+-define(OPEN_ELEM(N), [?OPEN_BRACE, N, ?CLOSE_BRACE]).
+-define(CLOSE_ELEM, [?FWD_SLASH, ?CLOSE_BRACE]).
+-define(CLOSING_ELEM(N), [?OPEN_BRACE, ?FWD_SLASH, N, ?CLOSE_BRACE]).
+
%%
%% API
%%
+file_writer(Path) ->
+ file_writer(Path, [binary, append]).
+
+file_writer(Path, Modes) ->
+ {ok, IoDevice} = file:open(Path, Modes),
+ new(fun(X) -> file:write(IoDevice, X) end,
+ fun(M, A, W) -> io:format(IoDevice, M, A), W end,
+ fun(_) -> file:close(IoDevice) end).
+
+file_writer(Name, Path, Modes) ->
+ {ok, IoDevice} = file:open(Path, Modes),
+ new(Name,
+ fun(X) -> file:write(IoDevice, X) end,
+ fun(M, A, W) -> io:format(IoDevice, M, A), W end,
+ fun(_) -> file:close(IoDevice) end).
+
+new(Name, WriteFun, FormatFun, CloseFun) ->
+ {ok, _Pid} = gen_fsm:start({local, Name}, ?MODULE,
+ [WriteFun, FormatFun, CloseFun], []),
+ Name.
+
+new(WriteFun, FormatFun, CloseFun) ->
+ {ok, Pid} = gen_fsm:start(?MODULE, [WriteFun, FormatFun, CloseFun], []),
+ Pid.
+
-spec new(name(), write_function()) -> writer().
new(Name, WriteFun) ->
{ok, _Pid} = gen_fsm:start({local, Name}, ?MODULE, [WriteFun], []),
@@ -53,10 +90,16 @@ new(WriteFun) ->
{ok, Pid} = gen_fsm:start(?MODULE, [WriteFun], []),
Pid.
-- spec close(writer()) -> term().
+-spec close(writer()) -> term().
close(Writer) ->
gen_fsm:sync_send_all_state_event(Writer, stop).
+%% namespace handling
+
+-spec add_namespace(writer(), namespace_prefix(), namespace_uri()) -> ok.
+add_namespace(Writer, NS, NSUri) ->
+ gen_fsm:sync_send_event(Writer, {add_namespace, NS, NSUri}).
+
-spec write_value(writer(), iodata()) -> 'ok' | {'error', term()}.
write_value(Writer, Value) ->
gen_fsm:sync_send_event(Writer, {write_value, Value}).
@@ -74,15 +117,26 @@ end_element(Writer) ->
%%
init([Writer]) ->
- {ok, waiting_for_input, #writer{ write=Writer }}.
-
-waiting_for_input({write_value, _}, From, W=#writer{ stack=[] }) ->
+ {ok, waiting_for_input, #writer{ write=Writer }};
+init([Writer, FormatFun, CloseFun]) ->
+ {ok, waiting_for_input,
+ #writer{ write = Writer,
+ format = FormatFun,
+ close = CloseFun}}.
+
+waiting_for_input({write_value, _}, _From, W=#writer{ stack=[] }) ->
{reply, {error, no_root_node}, waiting_for_input, W};
-waiting_for_input({start_element, ElementName},
- From, W=#writer{ stack=Stack }) ->
+waiting_for_input({start_element, ElementName}, From, W) ->
+ NewState = push(ElementName, W),
gen_fsm:reply(From, ok),
- {next_state, element_started,
- W#writer{ stack=[#stack_frame{ local_name=ElementName }|Stack] }}.
+ {next_state, element_started, NewState};
+waiting_for_input({add_namespace, NS, NSUri}, _From,
+ W=#writer{ ns_stack=NSStack, ns=NSMap }) ->
+ NewState = W#writer{
+ ns=lists:keystore(NS, 1, NSMap, {NS, NSUri}),
+ ns_stack=[NS|NSStack]
+ },
+ {reply, ok, waiting_for_input, NewState}.
element_started(end_element, _From, Writer) ->
{reply, ok, waiting_for_input, pop(Writer)}.
@@ -106,8 +160,69 @@ code_change(_OldVsn, StateName, StateData, _Extra) ->
%% Internal API
%%
+push(FR=#stack_frame{ns=ElemNS, local_name=ElemName},
+ W=#writer{ stack=S, ns=NSMap, ns_stack=NSStack }) ->
+ %% TODO: deal with nested nodes
+ write(start_elem(qname(ElemNS, ElemName), W), W),
+ %% TODO: move namespace handling out into another state
+ %% TODO: track namespace declarations and do not duplicate them.
+ case NSStack of
+ [] -> W;
+ [_|_] ->
+ XmlnsAtt =
+ [ setelement(1, lists:keyfind(NSEntry, 1, NSMap),
+ "xmlns:" ++ NSEntry) || NSEntry <- NSStack ],
+ write_attributes(XmlnsAtt, W)
+ end,
+ W#writer{ stack=[FR|S] };
+push(ElementName, W) ->
+ push(#stack_frame{ local_name=ElementName }, W).
+
+qname(undefined, Name) ->
+ Name;
+qname(NS, Name) ->
+ [NS, <<":">>, Name].
+
pop(Writer=#writer{ stack=[] }) ->
Writer;
-pop(Writer=#writer{ stack=[_|Rest] }) ->
- Writer#writer{ stack=Rest }.
-
+pop(W=#writer{ stack=[SF=#stack_frame{ns=NS, local_name=Name}|Stack] }) ->
+ case SF#stack_frame.has_children of
+ true ->
+ write(?CLOSING_ELEM(qname(NS, Name)), W);
+ false ->
+ write(?CLOSE_ELEM, W)
+ end,
+ W#writer{ stack=Stack }.
+
+write_attributes(AttributeList, Writer) ->
+ lists:foldl(fun({Name, Value}, XMLWriter) ->
+ write_attribute(Name, Value, XMLWriter)
+ end, Writer, AttributeList).
+
+write_attribute(Name, Value, Writer) ->
+ write_attribute(undefined, Name, Value, Writer).
+
+write_attribute(NS, Name, Value, Writer=#writer{ quote=Quot }) ->
+ write([?SPACE, qname(NS, Name), <<"=">>, Quot, Value, Quot], Writer).
+
+write(Data, Writer=#writer{ write=WriteFun }) when is_function(WriteFun, 1) ->
+ case WriteFun(encode(Data, Writer)) of
+ ok ->
+ Writer;
+ Error -> Error
+ %% exit(Error)
+ end.
+
+encode(Data, #writer{ encoding=undefined }) when is_atom(Data) ->
+ atom_to_binary(Data, utf8);
+encode(Data, #writer{ encoding=Encoding }) when is_atom(Data) ->
+ atom_to_binary(Data, Encoding);
+encode(Data, #writer{ encoding=_ }) -> %% when is_binary(Data) ->
+ Data. %% TODO: deal with in/out encoding requirements
+ %% HINT: maybe pass this in from things like Content Type/Disposition headers
+
+start_elem(NodeName, #writer{ prettyprint=false }) ->
+ [?OPEN_BRACE, NodeName];
+start_elem(NodeName, #writer{ stack=S, prettyprint=true, indent=I, newline=NL }) ->
+ %% TODO: consider a format string for this...
+ [NL, lists:duplicate(length(S), I), ?OPEN_BRACE, NodeName].
View
@@ -0,0 +1,15 @@
+
+{extend, "rebar.config"}.
+{deps, [
+ {hamcrest, ".*",
+ {git, "https://github.com/hyperthunk/hamcrest-erlang.git"}},
+ {proper, "1.0",
+ {git, "http://github.com/manopapad/proper.git", "master"}},
+ {proper_stdlib, ".*",
+ {git, "https://github.com/spawngrid/proper_stdlib.git"}}
+]}.
+
+{qc_opts, [long_result, {numtests, 500}]}.
+
+{cover_enabled, true}.
+{cover_print_enabled, true}.
View
@@ -22,6 +22,22 @@
-module(test_helper).
-compile(export_all).
+-include_lib("proper/include/proper.hrl").
+
+%%
+%% PropEr Types
+%%
+
+valid_latin_string() ->
+ non_empty(list(proper_stdgen:latin_char())).
+
+url() ->
+ ?LET({Host, Label}, {proper_stdgen:hostname(), proper_stdgen:label()},
+ "http://" ++ Host ++ "/" ++ Label).
+
+namespace_prefix() ->
+ ?SIZED(3, non_empty(list(proper_stdgen:latin_char()))).
+
%%
%% Utilities
%%
@@ -34,25 +34,38 @@
%% fsm transitions
%%
-waiting_for_input(#writer{}=W) ->
+waiting_for_input(_W) ->
+ %% TODO: loosen up the type constraints here....
[{history,
- {call, xml_writer, write_value, [?MODULE, binary()]}},
+ {call, xml_writer, add_namespace,
+ [?MODULE,
+ test_helper:namespace_prefix(),
+ test_helper:url()]}},
+ {history,
+ {call, xml_writer, write_value,
+ [?MODULE, test_helper:valid_latin_string()]}},
{element_started,
- {call, xml_writer, start_element, [?MODULE, binary()]}}].
+ {call, xml_writer, start_element,
+ [?MODULE, test_helper:valid_latin_string()]}}].
-element_started(W) ->
+element_started(_W) ->
[{waiting_for_input, {call, xml_writer, end_element, [?MODULE]}}].
initial_state() -> waiting_for_input.
initial_state_data() -> #writer{}.
+next_state_data(_, _, S,
+ _Result, {call, xml_writer, add_namespace, [_, NS, NSUri]}) ->
+ {_, _, _, S2} = xml_writer:waiting_for_input({add_namespace,
+ NS, NSUri}, undefined, S),
+ S2;
next_state_data(waiting_for_input, element_started, S,
- Result, {call, xml_writer, start_element, [_, Elem]}=Call) ->
+ _Result, {call, xml_writer, start_element, [_, Elem]}) ->
S#writer{ stack=[#stack_frame{ local_name=Elem }|S#writer.stack] };
-next_state_data(From, Target, StateData, Result, {call, _, _, _}=Call) ->
- ct:pal("From = ~p, Target = ~p, StateData = ~p, Result = ~p, Call = ~p~n",
- [From, Target, StateData, Result, Call]),
+next_state_data(_From, _Target, StateData, _Result, {call, _, _, _}) ->
+% ct:pal("From = ~p, Target = ~p, StateData = ~p, Result = ~p, Call = ~p~n",
+% [From, Target, StateData, Result, Call]),
StateData.
%precondition(waiting_for_input, _, W, {call, _, write_value, _}) ->
@@ -78,25 +91,31 @@ next_state_data(From, Target, StateData, Result, {call, _, _, _}=Call) ->
precondition(_From, _Target, _StateData, {call ,_,_,_}) ->
true.
+postcondition(waiting_for_input, _, _,
+ {call, _, add_namespace, _}, Result) ->
+% FoundNS = lists:keyfind(NS, 1, W#writer.ns),
+% error_logger:info_msg("FoundNS: ~p in ~p~n", [FoundNS, W]),
+% FoundNS == {NS, NSUri} andalso
+ Result =:= ok;
postcondition(waiting_for_input, _, #writer{stack=[]},
{call, _, write_value, _}, Result) ->
Result == {error, no_root_node};
postcondition(element_started, waiting_for_input,
- W, {call, _, _, _}=Call, Result) ->
-% ct:pal("W = ~p, Call = ~p, Result = ~p~n",
-% [W,Call, Result]),
+ W, {call, _, _, _}, _Result) ->
length(W#writer.stack) > 0;
postcondition(_, _, _, _, _) ->
true.
-%weight(_Today, _Tomorrow, {call,_,new_day,_}) -> 1;
-%weight(_Today, _Today, {call,_,hungry,_}) -> 3;
-%weight(_Today, _Today, {call,_,buy,_}) -> 2.
+weight(_OldState, _NewState, {call,_, add_namespace, _}) -> 5;
+weight(_, _, _) -> 1.
prop_all_state_transitions_are_valid() ->
?FORALL(Cmds, proper_fsm:commands(?MODULE),
begin
- Writer = xml_writer:new(?MODULE, fun io:format/2),
+ Path = filename:join([rebar_utils:get_cwd(), ".test",
+ atom_to_list(?MODULE)]),
+ filelib:ensure_dir(Path),
+ Writer = xml_writer:new(?MODULE, fun(_) -> ok end),
{History, State, Result} =
proper_fsm:run_commands(?MODULE, Cmds),
xml_writer:close(Writer),

0 comments on commit 4f1a36e

Please sign in to comment.