Anonymous avatar Anonymous committed 5ccf25b

copy riak/apps/mochiweb to external mochiweb repo

Comments (0)

Files changed (45)

-
 all:
-	./rebar compile
+	(cd src;$(MAKE) all)
 
 edoc:
-	erl -noshell -pa ebin \
-	    -eval "edoc:application(mochiweb, \".\", [{dir, \"doc\"}])" \
-	    -s init stop
+	(cd src;$(MAKE) edoc)
 
 test:
-	erl -noshell -pa ebin -s mochiweb test -s init stop
+	(cd src;$(MAKE) test)
 
 clean:
-	./rebar clean
+	(cd src;$(MAKE) clean)
+
+clean_plt:
+	(cd src;$(MAKE) clean_plt)
+
+dialyzer:
+	(cd src;$(MAKE) dialyzer)

Empty file added.

ebin/mochiweb.app

 {application, mochiweb,
  [{description, "MochiMedia Web Server"},
-  {vsn, "0.01"},
+  {vsn, "0.02"},
   {modules, [
+        mochiglobal,            
         mochihex,
         mochijson,
         mochijson2,
         mochiweb_app,
         mochiweb_charref,
         mochiweb_cookies,
+        mochiweb_cover,
         mochiweb_echo,
         mochiweb_headers,
         mochiweb_html,
         mochiweb_http,
+    	mochiweb_mime,
         mochiweb_multipart,
         mochiweb_request,
         mochiweb_response,

examples/keepalive/keepalive.erl

--module(keepalive).
-
-%% your web app can push data to clients using a technique called comet long
-%% polling.  browsers make a request and your server waits to send a
-%% response until data is available.  see wikipedia for a better explanation:
-%% http://en.wikipedia.org/wiki/Comet_(programming)#Ajax_with_long_polling
-%%
-%% since the majority of your http handlers will be idle at any given moment,
-%% you might consider making them hibernate while they wait for more data from
-%% another process.  however, since the execution stack is discarded when a
-%% process hibernates, the handler would usually terminate after your response
-%% code runs.  this means http keep alives wouldn't work; the handler process
-%% would terminate after each response and close its socket rather than
-%% returning to the big @mochiweb_http@ loop and processing another request.
-%%
-%% however, if mochiweb exposes a continuation that encapsulates the return to
-%% the top of the big loop in @mochiweb_http@, we can call that after the
-%% response.  if you do that then control flow returns to the proper place,
-%% and keep alives work like they would if you hadn't hibernated.
-
--export([ start/1, loop/1
-        ]).
-
-%% internal export (so hibernate can reach it)
--export([ resume/3
-        ]).
-
--define(LOOP, {?MODULE, loop}).
-
-start(Options = [{port, _Port}]) ->
-    mochiweb_http:start([{name, ?MODULE}, {loop, ?LOOP} | Options]).
-
-loop(Req) ->
-    Path = Req:get(path),
-    case string:tokens(Path, "/") of
-        ["longpoll" | RestOfPath] ->
-            %% the "reentry" is a continuation -- what @mochiweb_http@
-            %% needs to do to start its loop back at the top
-            Reentry = mochiweb_http:reentry(?LOOP),
-
-            %% here we could send a message to some other process and hope
-            %% to get an interesting message back after a while.  for
-            %% simplicity let's just send ourselves a message after a few
-            %% seconds
-            erlang:send_after(2000, self(), "honk honk"),
-
-            %% since we expect to wait for a long time before getting a
-            %% reply, let's hibernate.  memory usage will be minimized, so
-            %% we won't be wasting memory just sitting in a @receive@
-            proc_lib:hibernate(?MODULE, resume, [Req, RestOfPath, Reentry]),
-
-            %% we'll never reach this point, and this function @loop/1@
-            %% won't ever return control to @mochiweb_http@.  luckily
-            %% @resume/3@ will take care of that.
-            io:format("not gonna happen~n", []);
-
-        _ ->
-            ok(Req, io_lib:format("some other page: ~p", [Path]))
-    end,
-
-    io:format("restarting loop normally in ~p~n", [Path]),
-    ok.
-
-%% this is the function that's called when a message arrives.
-resume(Req, RestOfPath, Reentry) ->
-    receive
-        Msg ->
-            Text = io_lib:format("wake up message: ~p~nrest of path: ~p", [Msg, RestOfPath]),
-            ok(Req, Text)
-    end,
-
-    %% if we didn't call @Reentry@ here then the function would finish and the
-    %% process would exit.  calling @Reentry@ takes care of returning control
-    %% to @mochiweb_http@
-    io:format("reentering loop via continuation in ~p~n", [Req:get(path)]),
-    Reentry(Req).
-
-ok(Req, Response) ->
-    Req:ok({_ContentType = "text/plain",
-            _Headers = [],
-            Response}).

priv/skel/Makefile

 all:
 	(cd src;$(MAKE))
 
+edoc:
+	(cd src;$(MAKE) edoc)
+
+test:
+	(cd src;$(MAKE) test)
+
 clean:
 	(cd src;$(MAKE) clean)
+
+clean_plt:
+	(cd src;$(MAKE) clean_plt)
+
+dialyzer:
+	(cd src;$(MAKE) dialyzer)

priv/skel/src/Makefile

 include ../support/include.mk
 
+APPLICATION=skel
+DOC_OPTS={dir,\"../doc\"}
+TEST_PLT=$(TEST_DIR)/dialyzer_plt
+
 all: $(EBIN_FILES)
 
 debug:
 
 clean:
 	rm -rf $(EBIN_FILES)
+
+edoc:
+	$(ERL) -noshell -pa ../ebin \
+		-eval "edoc:application($(APPLICATION), \".\", [$(DOC_OPTS)])" \
+		-s init stop
+
+test: $(EBIN_FILES)
+	mkdir -p $(TEST_DIR);
+	@../support/run_tests.escript $(EBIN_DIR) | tee $(TEST_DIR)/test.log
+
+$(TEST_PLT):
+	mkdir -p $(TEST_DIR)
+	cp $(DIALYZER_PLT) $(TEST_PLT)
+	dialyzer --plt $(TEST_PLT) --add_to_plt -r ../deps/*/ebin
+
+clean_plt:
+	rm $(TEST_PLT)
+
+dialyzer: $(TEST_PLT)
+	dialyzer --src --plt $(TEST_PLT) -DNOTEST -DDIALYZER -c ../src | tee $(TEST_DIR)/dialyzer.log

priv/skel/src/skel.app

 {application, skel,
  [{description, "skel"},
-  {vsn, "0.01"},
+  {vsn, "0.9"},
   {modules, [
     skel,
     skel_app,

priv/skel/src/skel.erl

         {error, {already_started, App}} ->
             ok
     end.
-        
+
 %% @spec start() -> ok
 %% @doc Start the skel server.
 start() ->

priv/skel/src/skel_app.erl

 -author('author <author@example.com>').
 
 -behaviour(application).
--export([start/2,stop/1]).
+-export([start/2, stop/1]).
 
 
 %% @spec start(_Type, _StartArgs) -> ServerRet
 %% @doc application stop callback for skel.
 stop(_State) ->
     ok.
+
+
+%%
+%% Tests
+%%
+-include_lib("eunit/include/eunit.hrl").
+-ifdef(TEST).
+-endif.

priv/skel/src/skel_deps.erl

                 end
         end,
     ordsets:from_list(lists:foldl(F, [], code:get_path())).
-    
+
 %% @spec new_siblings(Module) -> [Dir]
 %% @doc Find new siblings paths relative to Module that aren't already on the
 %%      code path.
                            ordsets:is_element(
                              filename:basename(filename:dirname(X)),
                              Existing) =:= false],
-    lists:filter(fun filelib:is_dir/1, 
+    lists:filter(fun filelib:is_dir/1,
                  lists:append([[filename:join([X, "ebin"]),
                                 filename:join([X, "include"])] ||
                                   X <- Siblings])).
-        
+
 
 %% @spec ensure(Module) -> ok
 %% @doc Ensure that all ebin and include paths for dependencies
 %%      Equivalent to local_path(Components, ?MODULE).
 local_path(Components) ->
     local_path(Components, ?MODULE).
+
+
+%%
+%% Tests
+%%
+-include_lib("eunit/include/eunit.hrl").
+-ifdef(TEST).
+-endif.

priv/skel/src/skel_sup.erl

 %% @spec init([]) -> SupervisorTree
 %% @doc supervisor callback.
 init([]) ->
-    Ip = case os:getenv("MOCHIWEB_IP") of false -> "0.0.0.0"; Any -> Any end,   
+    Ip = case os:getenv("MOCHIWEB_IP") of false -> "0.0.0.0"; Any -> Any end,
     WebConfig = [
          {ip, Ip},
                  {port, 8000},
 
     Processes = [Web],
     {ok, {{one_for_one, 10, 10}, Processes}}.
+
+
+%%
+%% Tests
+%%
+-include_lib("eunit/include/eunit.hrl").
+-ifdef(TEST).
+-endif.

priv/skel/src/skel_web.erl

 
 get_option(Option, Options) ->
     {proplists:get_value(Option, Options), proplists:delete(Option, Options)}.
+
+
+%%
+%% Tests
+%%
+-include_lib("eunit/include/eunit.hrl").
+-ifdef(TEST).
+-endif.

priv/skel/start-dev.sh

 #!/bin/sh
 cd `dirname $0`
+make
 exec erl -pa $PWD/ebin $PWD/deps/*/ebin -boot start_sasl -s reloader -s skel

priv/skel/support/include.mk

-## -*- makefile -*-
-
-######################################################################
-## Erlang
-
-ERL := erl
-ERLC := $(ERL)c
-
-INCLUDE_DIRS := ../include $(wildcard ../deps/*/include)
-EBIN_DIRS := $(wildcard ../deps/*/ebin)
-ERLC_FLAGS := -W $(INCLUDE_DIRS:../%=-I ../%) $(EBIN_DIRS:%=-pa %)
-
-ifndef no_debug_info
-  ERLC_FLAGS += +debug_info
-endif
-
-ifdef debug
-  ERLC_FLAGS += -Ddebug
-endif
-
-EBIN_DIR := ../ebin
-DOC_DIR  := ../doc
-EMULATOR := beam
-
-ERL_SOURCES := $(wildcard *.erl)
-ERL_HEADERS := $(wildcard *.hrl) $(wildcard ../include/*.hrl)
-ERL_OBJECTS := $(ERL_SOURCES:%.erl=$(EBIN_DIR)/%.$(EMULATOR))
-ERL_DOCUMENTS := $(ERL_SOURCES:%.erl=$(DOC_DIR)/%.html)
-ERL_OBJECTS_LOCAL := $(ERL_SOURCES:%.erl=./%.$(EMULATOR))
-APP_FILES := $(wildcard *.app)
-EBIN_FILES = $(ERL_OBJECTS) $(ERL_DOCUMENTS) $(APP_FILES:%.app=../ebin/%.app)
-EBIN_FILES_NO_DOCS = $(ERL_OBJECTS) $(APP_FILES:%.app=../ebin/%.app)
-MODULES = $(ERL_SOURCES:%.erl=%)
-
-../ebin/%.app: %.app
-	cp $< $@
-
-$(EBIN_DIR)/%.$(EMULATOR): %.erl
-	$(ERLC) $(ERLC_FLAGS) -o $(EBIN_DIR) $<
-
-./%.$(EMULATOR): %.erl
-	$(ERLC) $(ERLC_FLAGS) -o . $<
-
-$(DOC_DIR)/%.html: %.erl
-	$(ERL) -noshell -run edoc file $< -run init stop
-	mv *.html $(DOC_DIR)
+{erl_opts, [debug_info, fail_on_warning]}.
+
+
+include ../support/include.mk
+
+APPLICATION=mochiweb
+DOC_OPTS={dir,\"../doc\"}
+TEST_PLT=$(TEST_DIR)/dialyzer_plt
+
+all: $(EBIN_FILES)
+
+debug:
+	$(MAKE) DEBUG=-DDEBUG
+
+clean:
+	rm -rf $(EBIN_FILES)
+
+edoc:
+	$(ERL) -noshell -pa ../ebin \
+		-eval "edoc:application($(APPLICATION), \".\", [$(DOC_OPTS)])" \
+		-s init stop
+
+test: $(EBIN_FILES)
+	mkdir -p $(TEST_DIR);
+	@../support/run_tests.escript $(EBIN_DIR) | tee $(TEST_DIR)/test.log
+
+$(TEST_PLT):
+	mkdir -p $(TEST_DIR)
+	cp $(DIALYZER_PLT) $(TEST_PLT)
+	dialyzer --plt $(TEST_PLT) --add_to_plt
+
+clean_plt:
+	rm $(TEST_PLT)
+
+dialyzer: $(TEST_PLT)
+	dialyzer --src --plt $(TEST_PLT) -DNOTEST -DDIALYZER -c ../src | tee $(TEST_DIR)/dialyzer.log
 -export([tokenize/1, format/3, get_field/3, format_field/3]).
 -export([bformat/2, bformat/3]).
 -export([f/2, f/3]).
--export([test/0]).
 
 -record(conversion, {length, precision, ctype, align, fill_char, sign}).
 
 bformat(Format, Args, Module) ->
     iolist_to_binary(format(Format, Args, Module)).
 
-%% @spec test() -> ok
-%% @doc Run tests.
-test() ->
-    ok = test_tokenize(),
-    ok = test_format(),
-    ok = test_std(),
-    ok = test_records(),
-    ok.
-
 %% Internal API
 
 add_raw("", Acc) ->
 parse_std_conversion([Type], Acc) ->
     parse_std_conversion("", Acc#conversion{ctype=ctype(Type)}).
 
-test_tokenize() ->
+
+%%
+%% Tests
+%%
+-include_lib("eunit/include/eunit.hrl").
+-ifdef(TEST).
+
+tokenize_test() ->
     {?MODULE, [{raw, "ABC"}]} = tokenize("ABC"),
     {?MODULE, [{format, {"0", "", ""}}]} = tokenize("{0}"),
     {?MODULE, [{raw, "ABC"}, {format, {"1", "", ""}}, {raw, "DEF"}]} =
         tokenize("ABC{1}DEF"),
     ok.
 
-test_format() ->
+format_test() ->
     <<"  -4">> = bformat("{0:4}", [-4]),
     <<"   4">> = bformat("{0:4}", [4]),
     <<"   4">> = bformat("{0:{0}}", [4]),
                                {{2008,5,4}, {4, 2, 2}}),
     ok.
 
-test_std() ->
+std_test() ->
     M = mochifmt_std:new(),
     <<"01">> = bformat("{0}{1}", [0, 1], M),
     ok.
 
-test_records() ->
+records_test() ->
     M = mochifmt_records:new([{conversion, record_info(fields, conversion)}]),
     R = #conversion{length=long, precision=hard, sign=peace},
     long = M:get_value("length", R),
     <<"long hard">> = bformat("{length} {precision}", R, M),
     <<"long hard">> = bformat("{0.length} {0.precision}", [R], M),
     ok.
+
+-endif.

src/mochifmt_records.erl

     Index;
 get_rec_index(Atom, [_ | Rest], Index) ->
     get_rec_index(Atom, Rest, 1 + Index).
+
+
+%%
+%% Tests
+%%
+-include_lib("eunit/include/eunit.hrl").
+-ifdef(TEST).
+-endif.

src/mochifmt_std.erl

 
 format_field(Arg, Format) ->
     mochifmt:format_field(Arg, Format, THIS).
+
+%%
+%% Tests
+%%
+-include_lib("eunit/include/eunit.hrl").
+-ifdef(TEST).
+-endif.

src/mochiglobal.erl

+%% @author Bob Ippolito <bob@mochimedia.com>
+%% @copyright 2010 Mochi Media, Inc.
+%% @doc Abuse module constant pools as a "read-only shared heap" (since erts 5.6)
+%%      <a href="http://www.erlang.org/pipermail/erlang-questions/2009-March/042503.html">[1]</a>.
+-module(mochiglobal).
+-author("Bob Ippolito <bob@mochimedia.com>").
+-export([get/1, get/2, put/2, delete/1]).
+
+-spec get(atom()) -> any() | undefined.
+%% @equiv get(K, undefined)
+get(K) ->
+    get(K, undefined).
+
+-spec get(atom(), T) -> any() | T.
+%% @doc Get the term for K or return Default.
+get(K, Default) ->
+    get(K, Default, key_to_module(K)).
+
+get(_K, Default, Mod) ->
+    try Mod:term()
+    catch error:undef ->
+            Default
+    end.
+
+-spec put(atom(), any()) -> ok.
+%% @doc Store term V at K, replaces an existing term if present.
+put(K, V) ->
+    put(K, V, key_to_module(K)).
+
+put(_K, V, Mod) ->
+    Bin = compile(Mod, V),
+    code:purge(Mod),
+    code:load_binary(Mod, atom_to_list(Mod) ++ ".erl", Bin),
+    ok.
+
+-spec delete(atom()) -> boolean().
+%% @doc Delete term stored at K, no-op if non-existent.
+delete(K) ->
+    delete(K, key_to_module(K)).
+
+delete(_K, Mod) ->
+    code:purge(Mod),
+    code:delete(Mod).
+
+-spec key_to_module(atom()) -> atom().
+key_to_module(K) ->
+    list_to_atom("mochiglobal:" ++ atom_to_list(K)).
+
+-spec compile(atom(), any()) -> binary().
+compile(Module, T) ->
+    {ok, Module, Bin} = compile:forms(forms(Module, T),
+                                      [verbose, report_errors]),
+    Bin.
+
+-spec forms(atom(), any()) -> [erl_syntax:syntaxTree()].
+forms(Module, T) ->
+    [erl_syntax:revert(X) || X <- term_to_abstract(Module, term, T)].
+
+-spec term_to_abstract(atom(), atom(), any()) -> [erl_syntax:syntaxTree()].
+term_to_abstract(Module, Getter, T) ->
+    [%% -module(Module).
+     erl_syntax:attribute(
+       erl_syntax:atom(module),
+       [erl_syntax:atom(Module)]),
+     %% -export([Getter/0]).
+     erl_syntax:attribute(
+       erl_syntax:atom(export),
+       [erl_syntax:list(
+         [erl_syntax:arity_qualifier(
+            erl_syntax:atom(Getter),
+            erl_syntax:integer(0))])]),
+     %% Getter() -> T.
+     erl_syntax:function(
+       erl_syntax:atom(Getter),
+       [erl_syntax:clause([], none, [erl_syntax:abstract(T)])])].
+
+%%
+%% Tests
+%%
+-include_lib("eunit/include/eunit.hrl").
+-ifdef(TEST).
+get_put_delete_test() ->
+    K = '$$test$$mochiglobal',
+    delete(K),
+    ?assertEqual(
+       bar,
+       get(K, bar)),
+    try
+        ?MODULE:put(K, baz),
+        ?assertEqual(
+           baz,
+           get(K, bar)),
+        ?MODULE:put(K, wibble),
+        ?assertEqual(
+           wibble,
+           ?MODULE:get(K))
+    after
+        delete(K)
+    end,
+    ?assertEqual(
+       bar,
+       get(K, bar)),
+    ?assertEqual(
+       undefined,
+       ?MODULE:get(K)),
+    ok.
+-endif.
 -module(mochihex).
 -author('bob@mochimedia.com').
 
--export([test/0, to_hex/1, to_bin/1, to_int/1, dehex/1, hexdigit/1]).
+-export([to_hex/1, to_bin/1, to_int/1, dehex/1, hexdigit/1]).
 
 %% @type iolist() = [char() | binary() | iolist()]
 %% @type iodata() = iolist() | binary()
 hexdigit(C) when C =< 15 ->
     C + $a - 10.
 
-%% @spec test() -> ok
-%% @doc Test this module.
-test() ->
-    "ff000ff1" = to_hex([255, 0, 15, 241]),
-    <<255, 0, 15, 241>> = to_bin("ff000ff1"),
-    16#ff000ff1 = to_int("ff000ff1"),
-    "ff000ff1" = to_hex(16#ff000ff1),
-    ok.
-
-
 %% Internal API
 
 to_hex(<<>>, Acc) ->
 to_bin([C1, C2 | Rest], Acc) ->
     to_bin(Rest, [(dehex(C1) bsl 4) bor dehex(C2) | Acc]).
 
+
+
+%%
+%% Tests
+%%
+-include_lib("eunit/include/eunit.hrl").
+-ifdef(TEST).
+
+to_hex_test() ->
+    "ff000ff1" = to_hex([255, 0, 15, 241]),
+    "ff000ff1" = to_hex(16#ff000ff1),
+    "0" = to_hex(16#0),
+    ok.
+
+to_bin_test() ->
+    <<255, 0, 15, 241>> = to_bin("ff000ff1"),
+    <<255, 0, 10, 161>> = to_bin("Ff000aA1"),
+    ok.
+
+to_int_test() ->
+    16#ff000ff1 = to_int("ff000ff1"),
+    16#ff000aa1 = to_int("FF000Aa1"),
+    16#0 = to_int("0"),
+    ok.
+
+-endif.

src/mochijson.erl

 -export([decoder/1, decode/1]).
 -export([binary_encoder/1, binary_encode/1]).
 -export([binary_decoder/1, binary_decode/1]).
--export([test/0]).
 
 % This is a macro to placate syntax highlighters..
 -define(Q, $\").
 binary_decode(S) ->
     mochijson2:decode(S).
 
-test() ->
-    test_all(),
-    mochijson2:test().
-
 %% Internal API
 
 parse_encoder_options([], State) ->
             {{const, list_to_float(Float)}, Rest, S1}
     end.
 
+
+%%
+%% Tests
+%%
+-include_lib("eunit/include/eunit.hrl").
+-ifdef(TEST).
+
 %% testing constructs borrowed from the Yaws JSON implementation.
 
 %% Create an object from a list of Key/Value pairs.
 equiv_list([V1 | L1], [V2 | L2]) ->
     equiv(V1, V2) andalso equiv_list(L1, L2).
 
-test_all() ->
-    test_issue33(),
+e2j_vec_test() ->
     test_one(e2j_test_vec(utf8), 1).
 
-test_issue33() ->
+issue33_test() ->
     %% http://code.google.com/p/mochiweb/issues/detail?id=33
     Js = {struct, [{"key", [194, 163]}]},
     Encoder = encoder([{input_encoding, utf8}]),
     {{array, [-123, "foo", obj_from_list([{"bar", {array, []}}]), null]},
      "[-123,\"foo\",{\"bar\":[]},null]"}
     ].
+
+-endif.

src/mochijson2.erl

 -author('bob@mochimedia.com').
 -export([encoder/1, encode/1]).
 -export([decoder/1, decode/1]).
--export([test/0]).
 
 % This is a macro to placate syntax highlighters..
 -define(Q, $\").
 %% @type json_number() = integer() | float()
 %% @type json_array() = [json_term()]
 %% @type json_object() = {struct, [{json_string(), json_term()}]}
+%% @type json_iolist() = {json, iolist()}
 %% @type json_term() = json_string() | json_number() | json_array() |
-%%                     json_object()
+%%                     json_object() | json_iolist()
 
 -record(encoder, {handler=null,
                   utf8=false}).
 decode(S) ->
     json_decode(S, #decoder{}).
 
-test() ->
-    test_all().
-
 %% Internal API
 
 parse_encoder_options([], State) ->
     json_encode_array(Array, State);
 json_encode({struct, Props}, State) when is_list(Props) ->
     json_encode_proplist(Props, State);
+json_encode({json, IoList}, _State) ->
+    IoList;
 json_encode(Bad, #encoder{handler=null}) ->
     exit({json_encode, {bad_term, Bad}});
 json_encode(Bad, State=#encoder{handler=Handler}) ->
             false;
         $\t ->
             false;
-        C when C >= 0, C < $\s; C >= 16#7f, C =< 16#10FFFF ->
+        C when C >= 0, C < $\s; C >= 16#7f ->
             false;
         C when C < 16#7f ->
-            json_bin_is_safe(Rest);
-        _ ->
-            false
+            json_bin_is_safe(Rest)
     end.
 
 json_encode_string_unicode([], _State, Acc) ->
             trim = S#decoder.state,
             {eof, S}
     end.
+%%
+%% Tests
+%%
+-include_lib("eunit/include/eunit.hrl").
+-ifdef(TEST).
+
 
 %% testing constructs borrowed from the Yaws JSON implementation.
 
     {struct, []}.
 
 is_obj({struct, Props}) ->
-    F = fun ({K, _}) when is_binary(K) ->
-                true;
-            (_) ->
-                false
-        end,    
+    F = fun ({K, _}) when is_binary(K) -> true end,
     lists:all(F, Props).
 
 obj_from_list(Props) ->
     Obj = {struct, Props},
-    case is_obj(Obj) of
-        true -> Obj;
-        false -> exit({json_bad_object, Obj})
-    end.
+    ?assert(is_obj(Obj)),
+    Obj.
 
 %% Test for equivalence of Erlang terms.
 %% Due to arbitrary order of construction, equivalent objects might
     equiv_list(L1, L2);
 equiv(N1, N2) when is_number(N1), is_number(N2) -> N1 == N2;
 equiv(B1, B2) when is_binary(B1), is_binary(B2) -> B1 == B2;
-equiv(true, true) -> true;
-equiv(false, false) -> true;
-equiv(null, null) -> true.
+equiv(A, A) when A =:= true orelse A =:= false orelse A =:= null -> true.
 
 %% Object representation and traversal order is unknown.
 %% Use the sledgehammer and sort property lists.
 equiv_list([V1 | L1], [V2 | L2]) ->
     equiv(V1, V2) andalso equiv_list(L1, L2).
 
-test_all() ->
+decode_test() ->
     [1199344435545.0, 1] = decode(<<"[1199344435545.0,1]">>),
-    <<16#F0,16#9D,16#9C,16#95>> = decode([34,"\\ud835","\\udf15",34]),
-    test_encoder_utf8(),
-    test_input_validation(),
+    <<16#F0,16#9D,16#9C,16#95>> = decode([34,"\\ud835","\\udf15",34]).
+
+e2j_vec_test() ->
     test_one(e2j_test_vec(utf8), 1).
 
 test_one([], _N) ->
      {[], "[]"},
      {[[]], "[[]]"},
      {[1, <<"foo">>], "[1,\"foo\"]"},
-     
+
      %% json array in a json object
      {obj_from_list([{<<"foo">>, [123]}]),
       "{\"foo\":[123]}"},
-     
+
      %% json object in a json object
      {obj_from_list([{<<"foo">>, obj_from_list([{<<"bar">>, true}])}]),
       "{\"foo\":{\"bar\":true}}"},
-     
+
      %% fold evaluation order
      {obj_from_list([{<<"foo">>, []},
                      {<<"bar">>, obj_from_list([{<<"baz">>, true}])},
                      {<<"alice">>, <<"bob">>}]),
       "{\"foo\":[],\"bar\":{\"baz\":true},\"alice\":\"bob\"}"},
-     
+
      %% json object in a json array
      {[-123, <<"foo">>, obj_from_list([{<<"bar">>, []}]), null],
       "[-123,\"foo\",{\"bar\":[]},null]"}
     ].
 
 %% test utf8 encoding
-test_encoder_utf8() ->
+encoder_utf8_test() ->
     %% safe conversion case (default)
-    [34,"\\u0001","\\u0442","\\u0435","\\u0441","\\u0442",34] = 
+    [34,"\\u0001","\\u0442","\\u0435","\\u0441","\\u0442",34] =
         encode(<<1,"\321\202\320\265\321\201\321\202">>),
 
     %% raw utf8 output (optional)
     [34,"\\u0001",[209,130],[208,181],[209,129],[209,130],34] =
         Enc(<<1,"\321\202\320\265\321\201\321\202">>).
 
-test_input_validation() ->
+input_validation_test() ->
     Good = [
-        {16#00A3, <<?Q, 16#C2, 16#A3, ?Q>>}, % pound
-        {16#20AC, <<?Q, 16#E2, 16#82, 16#AC, ?Q>>}, % euro
-        {16#10196, <<?Q, 16#F0, 16#90, 16#86, 16#96, ?Q>>} % denarius
+        {16#00A3, <<?Q, 16#C2, 16#A3, ?Q>>}, %% pound
+        {16#20AC, <<?Q, 16#E2, 16#82, 16#AC, ?Q>>}, %% euro
+        {16#10196, <<?Q, 16#F0, 16#90, 16#86, 16#96, ?Q>>} %% denarius
     ],
     lists:foreach(fun({CodePoint, UTF8}) ->
         Expect = list_to_binary(xmerl_ucs:to_utf8(CodePoint)),
         Expect = decode(UTF8)
     end, Good),
-    
+
     Bad = [
-        % 2nd, 3rd, or 4th byte of a multi-byte sequence w/o leading byte
+        %% 2nd, 3rd, or 4th byte of a multi-byte sequence w/o leading byte
         <<?Q, 16#80, ?Q>>,
-        % missing continuations, last byte in each should be 80-BF
+        %% missing continuations, last byte in each should be 80-BF
         <<?Q, 16#C2, 16#7F, ?Q>>,
         <<?Q, 16#E0, 16#80,16#7F, ?Q>>,
         <<?Q, 16#F0, 16#80, 16#80, 16#7F, ?Q>>,
-        % we don't support code points > 10FFFF per RFC 3629
+        %% we don't support code points > 10FFFF per RFC 3629
         <<?Q, 16#F5, 16#80, 16#80, 16#80, ?Q>>
     ],
-    lists:foreach(fun(X) ->
-        ok = try decode(X) catch invalid_utf8 -> ok end
-    end, Bad).
+    lists:foreach(
+      fun(X) ->
+              ok = try decode(X) catch invalid_utf8 -> ok end,
+              %% could be {ucs,{bad_utf8_character_code}} or
+              %%          {json_encode,{bad_char,_}}
+              {'EXIT', _} = (catch encode(X))
+      end, Bad).
+
+inline_json_test() ->
+    ?assertEqual(<<"\"iodata iodata\"">>,
+                 iolist_to_binary(
+                   encode({json, [<<"\"iodata">>, " iodata\""]}))),
+    ?assertEqual({struct, [{<<"key">>, <<"iodata iodata">>}]},
+                 decode(
+                   encode({struct,
+                           [{key, {json, [<<"\"iodata">>, " iodata\""]}}]}))),
+    ok.
+
+big_unicode_test() ->
+    UTF8Seq = list_to_binary(xmerl_ucs:to_utf8(16#0001d120)),
+    ?assertEqual(
+       <<"\"\\ud834\\udd20\"">>,
+       iolist_to_binary(encode(UTF8Seq))),
+    ?assertEqual(
+       UTF8Seq,
+       decode(iolist_to_binary(encode(UTF8Seq)))),
+    ok.
+
+custom_decoder_test() ->
+    ?assertEqual(
+       {struct, [{<<"key">>, <<"value">>}]},
+       (decoder([]))("{\"key\": \"value\"}")),
+    F = fun ({struct, [{<<"key">>, <<"value">>}]}) -> win end,
+    ?assertEqual(
+       win,
+       (decoder([{object_hook, F}]))("{\"key\": \"value\"}")),
+    ok.
+
+atom_test() ->
+    %% JSON native atoms
+    [begin
+         ?assertEqual(A, decode(atom_to_list(A))),
+         ?assertEqual(iolist_to_binary(atom_to_list(A)),
+                      iolist_to_binary(encode(A)))
+     end || A <- [true, false, null]],
+    %% Atom to string
+    ?assertEqual(
+       <<"\"foo\"">>,
+       iolist_to_binary(encode(foo))),
+    ?assertEqual(
+       <<"\"\\ud834\\udd20\"">>,
+       iolist_to_binary(encode(list_to_atom(xmerl_ucs:to_utf8(16#0001d120))))),
+    ok.
+
+key_encode_test() ->
+    %% Some forms are accepted as keys that would not be strings in other
+    %% cases
+    ?assertEqual(
+       <<"{\"foo\":1}">>,
+       iolist_to_binary(encode({struct, [{foo, 1}]}))),
+    ?assertEqual(
+       <<"{\"foo\":1}">>,
+       iolist_to_binary(encode({struct, [{<<"foo">>, 1}]}))),
+    ?assertEqual(
+       <<"{\"foo\":1}">>,
+       iolist_to_binary(encode({struct, [{"foo", 1}]}))),
+    ?assertEqual(
+       <<"{\"\\ud834\\udd20\":1}">>,
+       iolist_to_binary(
+         encode({struct, [{[16#0001d120], 1}]}))),
+    ?assertEqual(
+       <<"{\"1\":1}">>,
+       iolist_to_binary(encode({struct, [{1, 1}]}))),
+    ok.
+
+unsafe_chars_test() ->
+    Chars = "\"\\\b\f\n\r\t",
+    [begin
+         ?assertEqual(false, json_string_is_safe([C])),
+         ?assertEqual(false, json_bin_is_safe(<<C>>)),
+         ?assertEqual(<<C>>, decode(encode(<<C>>)))
+     end || C <- Chars],
+    ?assertEqual(
+       false,
+       json_string_is_safe([16#0001d120])),
+    ?assertEqual(
+       false,
+       json_bin_is_safe(list_to_binary(xmerl_ucs:to_utf8(16#0001d120)))),
+    ?assertEqual(
+       [16#0001d120],
+       xmerl_ucs:from_utf8(
+         binary_to_list(
+           decode(encode(list_to_atom(xmerl_ucs:to_utf8(16#0001d120))))))),
+    ?assertEqual(
+       false,
+       json_string_is_safe([16#110000])),
+    ?assertEqual(
+       false,
+       json_bin_is_safe(list_to_binary(xmerl_ucs:to_utf8([16#110000])))),
+    %% solidus can be escaped but isn't unsafe by default
+    ?assertEqual(
+       <<"/">>,
+       decode(<<"\"\\/\"">>)),
+    ok.
+
+int_test() ->
+    ?assertEqual(0, decode("0")),
+    ?assertEqual(1, decode("1")),
+    ?assertEqual(11, decode("11")),
+    ok.
+
+float_fallback_test() ->
+    ?assertEqual(<<"-2147483649.0">>, iolist_to_binary(encode(-2147483649))),
+    ?assertEqual(<<"2147483648.0">>, iolist_to_binary(encode(2147483648))),
+    ok.
+
+handler_test() ->
+    ?assertEqual(
+       {'EXIT',{json_encode,{bad_term,{}}}},
+       catch encode({})),
+    F = fun ({}) -> [] end,
+    ?assertEqual(
+       <<"[]">>,
+       iolist_to_binary((encoder([{handler, F}]))({}))),
+    ok.
+
+-endif.
 
 -module(mochinum).
 -author("Bob Ippolito <bob@mochimedia.com>").
--export([digits/1, frexp/1, int_pow/2, int_ceil/1, test/0]).
+-export([digits/1, frexp/1, int_pow/2, int_ceil/1]).
 
 %% IEEE 754 Float exponent bias
 -define(FLOAT_BIAS, 1022).
         _ ->
             R
     end.
-    
+
 %% @spec frexp(F::float()) -> {Frac::float(), Exp::float()}
 %% @doc  Return the fractional and exponent part of an IEEE 754 double,
 %%       equivalent to the libc function of the same name.
     case Exp >= 0 of
         true ->
             BExp = 1 bsl Exp,
-            case (Frac /= ?BIG_POW) of
+            case (Frac =/= ?BIG_POW) of
                 true ->
                     scale((Frac * BExp * 2), 2, BExp, BExp,
                           Round, Round, Float);
                           Round, Round, Float)
             end;
         false ->
-            case (Exp == ?MIN_EXP) orelse (Frac /= ?BIG_POW) of
+            case (Exp =:= ?MIN_EXP) orelse (Frac =/= ?BIG_POW) of
                 true ->
                     scale((Frac * 2), 1 bsl (1 - Exp), 1, 1,
                           Round, Round, Float);
             end
     end.
 
-unpack(Float) ->    
+unpack(Float) ->
     <<Sign:1, Exp:11, Frac:52>> = <<Float:64/float>>,
     {Sign, Exp, Frac}.
 
     log2floor(Int bsr 1, 1 + N).
 
 
-test() ->
-    ok = test_frexp(),
-    ok = test_int_ceil(),
-    ok = test_int_pow(),
-    ok = test_digits(),
-    ok.
+%%
+%% Tests
+%%
+-include_lib("eunit/include/eunit.hrl").
+-ifdef(TEST).
 
-test_int_ceil() ->
+int_ceil_test() ->
     1 = int_ceil(0.0001),
     0 = int_ceil(0.0),
     1 = int_ceil(0.99),
     -1 = int_ceil(-1.5),
     -2 = int_ceil(-2.0),
     ok.
-    
-test_int_pow() ->
+
+int_pow_test() ->
     1 = int_pow(1, 1),
     1 = int_pow(1, 0),
     1 = int_pow(10, 0),
     100 = int_pow(10, 2),
     1000 = int_pow(10, 3),
     ok.
-    
-test_digits() ->
-    "0" = digits(0),
-    "0.0" = digits(0.0),
-    "1.0" = digits(1.0),
-    "-1.0" = digits(-1.0),
-    "0.1" = digits(0.1),
-    "0.01" = digits(0.01),
-    "0.001" = digits(0.001),
-    ok.
 
-test_frexp() ->
+%% XXX arg 02/12/10
+%% disabled pending resolution of http://code.google.com/p/mochiweb/issues/detail?id=63
+%digits_test_disabled() ->
+%    ?assertEqual("0",
+%                 digits(0)),
+%    ?assertEqual("0.0",
+%                 digits(0.0)),
+%    ?assertEqual("1.0",
+%                 digits(1.0)),
+%    ?assertEqual("-1.0",
+%                 digits(-1.0)),
+%    ?assertEqual("0.1",
+%                 digits(0.1)),
+%    ?assertEqual("0.01",
+%                 digits(0.01)),
+%    ?assertEqual("0.001",
+%                 digits(0.001)),
+%    ?assertEqual("1.0e+6",
+%                 digits(1000000.0)),
+%    ?assertEqual("0.5",
+%                 digits(0.5)),
+%    ?assertEqual("4503599627370496.0",
+%                 digits(4503599627370496.0)),
+%    %% small denormalized number
+%    %% 4.94065645841246544177e-324
+%    <<SmallDenorm/float>> = <<0,0,0,0,0,0,0,1>>,
+%    ?assertEqual("4.9406564584124654e-324",
+%                 digits(SmallDenorm)),
+%    ?assertEqual(SmallDenorm,
+%                 list_to_float(digits(SmallDenorm))),
+%    %% large denormalized number
+%    %% 2.22507385850720088902e-308
+%    <<BigDenorm/float>> = <<0,15,255,255,255,255,255,255>>,
+%    ?assertEqual("2.225073858507201e-308",
+%                 digits(BigDenorm)),
+%    ?assertEqual(BigDenorm,
+%                 list_to_float(digits(BigDenorm))),
+%    %% small normalized number
+%    %% 2.22507385850720138309e-308
+%    <<SmallNorm/float>> = <<0,16,0,0,0,0,0,0>>,
+%    ?assertEqual("2.2250738585072014e-308",
+%                 digits(SmallNorm)),
+%    ?assertEqual(SmallNorm,
+%                 list_to_float(digits(SmallNorm))),
+%    %% large normalized number
+%    %% 1.79769313486231570815e+308
+%    <<LargeNorm/float>> = <<127,239,255,255,255,255,255,255>>,
+%    ?assertEqual("1.7976931348623157e+308",
+%                 digits(LargeNorm)),
+%    ?assertEqual(LargeNorm,
+%                 list_to_float(digits(LargeNorm))),
+%    ok.
+
+frexp_test() ->
     %% zero
     {0.0, 0} = frexp(0.0),
     %% one
     <<LargeNorm/float>> = <<127,239,255,255,255,255,255,255>>,
     {0.99999999999999989, 1024} = frexp(LargeNorm),
     ok.
+
+-endif.
+{application, mochiweb,
+ [{description, "MochiMedia Web Server"},
+  {vsn, "0.02"},
+  {modules, [
+        mochiglobal,            
+        mochihex,
+        mochijson,
+        mochijson2,
+        mochinum,
+        mochiweb,
+        mochiweb_app,
+        mochiweb_charref,
+        mochiweb_cookies,
+        mochiweb_cover,
+        mochiweb_echo,
+        mochiweb_headers,
+        mochiweb_html,
+        mochiweb_http,
+    	mochiweb_mime,
+        mochiweb_multipart,
+        mochiweb_request,
+        mochiweb_response,
+        mochiweb_skel,
+        mochiweb_socket_server,
+        mochiweb_sup,
+        mochiweb_util,
+        reloader,
+        mochifmt,
+        mochifmt_std,
+        mochifmt_records
+	    ]},
+  {registered, []},
+  {mod, {mochiweb_app, []}},
+  {env, []},
+  {applications, [kernel, stdlib]}]}.
 -export([start/0, stop/0]).
 -export([new_request/1, new_response/1]).
 -export([all_loaded/0, all_loaded/1, reload/0]).
--export([test/0]).
 
 %% @spec start() -> ok
 %% @doc Start the MochiWeb server.
     application:stop(crypto),
     Res.
 
-%% @spec test() -> ok
-%% @doc Run all of the tests for MochiWeb.
-test() ->
-    mochiweb_util:test(),
-    mochiweb_headers:test(),
-    mochiweb_cookies:test(),
-    mochihex:test(),
-    mochinum:test(),
-    mochijson:test(),
-    mochiweb_charref:test(),
-    mochiweb_html:test(),
-    mochifmt:test(),
-    test_request(),
-    ok.
-
 reload() ->
     [c:l(Module) || Module <- all_loaded()].
 
 
 %% Internal API
 
-test_request() ->
-    R = mochiweb_request:new(z, z, "/foo/bar/baz%20wibble+quux?qs=2", z, []),
-    "/foo/bar/baz wibble quux" = R:get(path),
-    ok.
-
 ensure_started(App) ->
     case application:start(App) of
         ok ->
         {error, {already_started, App}} ->
             ok
     end.
+
+
+%%
+%% Tests
+%%
+-include_lib("eunit/include/eunit.hrl").
+-ifdef(TEST).
+
+request_test() ->
+    R = mochiweb_request:new(z, z, "/foo/bar/baz%20wibble+quux?qs=2", z, []),
+    "/foo/bar/baz wibble quux" = R:get(path),
+    ok.
+
+-endif.

src/mochiweb_app.erl

 %% @doc application stop callback for mochiweb.
 stop(_State) ->
     ok.
+
+%%
+%% Tests
+%%
+-include_lib("eunit/include/eunit.hrl").
+-ifdef(TEST).
+-endif.

src/mochiweb_charref.erl

 
 %% @doc Converts HTML 4 charrefs and entities to codepoints.
 -module(mochiweb_charref).
--export([charref/1, test/0]).
+-export([charref/1]).
 
 %% External API.
 
     end;
 charref(L) ->
     entity(L).
-    
-%% @spec test() -> ok
-%% @doc Run tests for mochiweb_charref.
-test() ->
-    1234 = charref("#1234"),
-    255 = charref("#xfF"),
-    255 = charref("#XFf"),
-    38 = charref("amp"),
-    undefined = charref("not_an_entity"),
-    ok.
 
 %% Internal API.
 
 entity("euro") -> 8364;
 entity(_) -> undefined.
 
+
+%%
+%% Tests
+%%
+-include_lib("eunit/include/eunit.hrl").
+-ifdef(TEST).
+
+exhaustive_entity_test() ->
+    T = mochiweb_cover:clause_lookup_table(?MODULE, entity),
+    [?assertEqual(V, entity(K)) || {K, V} <- T].
+
+charref_test() ->
+    1234 = charref("#1234"),
+    255 = charref("#xfF"),
+    255 = charref(<<"#XFf">>),
+    38 = charref("amp"),
+    38 = charref(<<"amp">>),
+    undefined = charref("not_an_entity"),
+    undefined = charref("#not_an_entity"),
+    undefined = charref("#xnot_an_entity"),
+    ok.
+
+-endif.

src/mochiweb_cookies.erl

 %% @doc HTTP Cookie parsing and generating (RFC 2109, RFC 2965).
 
 -module(mochiweb_cookies).
--export([parse_cookie/1, cookie/3, cookie/2, test/0]).
+-export([parse_cookie/1, cookie/3, cookie/2]).
 
 -define(QUOTE, $\").
 
 cookie(Key, Value) ->
     cookie(Key, Value, []).
 
-%% @spec cookie(Key::string(), Value::string(), Options::[Option]) -> header() 
-%% where Option = {max_age, integer()} | {local_time, {date(), time()}} 
+%% @spec cookie(Key::string(), Value::string(), Options::[Option]) -> header()
+%% where Option = {max_age, integer()} | {local_time, {date(), time()}}
 %%                | {domain, string()} | {path, string()}
 %%                | {secure, true | false} | {http_only, true | false}
 %%
 %% @spec parse_cookie(string()) -> [{K::string(), V::string()}]
 %% @doc Parse the contents of a Cookie header field, ignoring cookie
 %% attributes, and return a simple property list.
-parse_cookie("") -> 
+parse_cookie("") ->
     [];
-parse_cookie(Cookie) -> 
+parse_cookie(Cookie) ->
     parse_cookie(Cookie, []).
 
-%% @spec test() -> ok
-%% @doc Run tests for mochiweb_cookies.
-test() ->
-    parse_cookie_test(),
-    cookie_test(),
-    ok.
-
 %% Internal API
 
 parse_cookie([], Acc) ->
-    lists:reverse(Acc); 
-parse_cookie(String, Acc) -> 
+    lists:reverse(Acc);
+parse_cookie(String, Acc) ->
     {{Token, Value}, Rest} = read_pair(String),
     Acc1 = case Token of
                "" ->
     read_quoted(Rest, [Any | Acc]);
 read_quoted([C | Rest], Acc) ->
     read_quoted(Rest, [C | Acc]).
-    
+
 skip_whitespace(String) ->
     F = fun (C) -> ?IS_WHITESPACE(C) end,
     lists:dropwhile(F, String).
     F = fun (C) -> not ?IS_SEPARATOR(C) end,
     lists:splitwith(F, String).
 
-skip_past_separator([]) ->    
+skip_past_separator([]) ->
     [];
 skip_past_separator([$; | Rest]) ->
     Rest;
 skip_past_separator([_ | Rest]) ->
     skip_past_separator(Rest).
 
-parse_cookie_test() ->
-    %% RFC example
-    C1 = "$Version=\"1\"; Customer=\"WILE_E_COYOTE\"; $Path=\"/acme\"; 
-    Part_Number=\"Rocket_Launcher_0001\"; $Path=\"/acme\";
-    Shipping=\"FedEx\"; $Path=\"/acme\"",
-    [
-     {"Customer","WILE_E_COYOTE"},
-     {"Part_Number","Rocket_Launcher_0001"},
-     {"Shipping","FedEx"}
-    ] = parse_cookie(C1),
-    %% Potential edge cases
-    [{"foo", "x"}] = parse_cookie("foo=\"\\x\""),
-    [] = parse_cookie("="),
-    [{"foo", ""}, {"bar", ""}] = parse_cookie("  foo ; bar  "),
-    [{"foo", ""}, {"bar", ""}] = parse_cookie("foo=;bar="),
-    [{"foo", "\";"}, {"bar", ""}] = parse_cookie("foo = \"\\\";\";bar "),
-    [{"foo", "\";bar"}] = parse_cookie("foo=\"\\\";bar").
-
 any_to_list(V) when is_list(V) ->
     V;
 any_to_list(V) when is_atom(V) ->
 any_to_list(V) when is_integer(V) ->
     integer_to_list(V).
 
+%%
+%% Tests
+%%
+-include_lib("eunit/include/eunit.hrl").
+-ifdef(TEST).
+
+quote_test() ->
+    %% ?assertError eunit macro is not compatible with coverage module
+    try quote(":wq")
+    catch error:{cookie_quoting_required, ":wq"} -> ok
+    end,
+    ?assertEqual(
+       "foo",
+       quote(foo)),
+    ok.
+
+parse_cookie_test() ->
+    %% RFC example
+    C1 = "$Version=\"1\"; Customer=\"WILE_E_COYOTE\"; $Path=\"/acme\";
+    Part_Number=\"Rocket_Launcher_0001\"; $Path=\"/acme\";
+    Shipping=\"FedEx\"; $Path=\"/acme\"",
+    ?assertEqual(
+       [{"Customer","WILE_E_COYOTE"},
+        {"Part_Number","Rocket_Launcher_0001"},
+        {"Shipping","FedEx"}],
+       parse_cookie(C1)),
+    %% Potential edge cases
+    ?assertEqual(
+       [{"foo", "x"}],
+       parse_cookie("foo=\"\\x\"")),
+    ?assertEqual(
+       [],
+       parse_cookie("=")),
+    ?assertEqual(
+       [{"foo", ""}, {"bar", ""}],
+       parse_cookie("  foo ; bar  ")),
+    ?assertEqual(
+       [{"foo", ""}, {"bar", ""}],
+       parse_cookie("foo=;bar=")),
+    ?assertEqual(
+       [{"foo", "\";"}, {"bar", ""}],
+       parse_cookie("foo = \"\\\";\";bar ")),
+    ?assertEqual(
+       [{"foo", "\";bar"}],
+       parse_cookie("foo=\"\\\";bar")),
+    ?assertEqual(
+       [],
+       parse_cookie([])),
+    ?assertEqual(
+       [{"foo", "bar"}, {"baz", "wibble"}],
+       parse_cookie("foo=bar , baz=wibble ")),
+    ok.
+
+domain_test() ->
+    ?assertEqual(
+       {"Set-Cookie",
+        "Customer=WILE_E_COYOTE; "
+        "Version=1; "
+        "Domain=acme.com; "
+        "HttpOnly"},
+       cookie("Customer", "WILE_E_COYOTE",
+              [{http_only, true}, {domain, "acme.com"}])),
+    ok.
+
+local_time_test() ->
+    {"Set-Cookie", S} = cookie("Customer", "WILE_E_COYOTE",
+                               [{max_age, 111}, {secure, true}]),
+    ?assertMatch(
+       ["Customer=WILE_E_COYOTE",
+        " Version=1",
+        " Expires=" ++ _,
+        " Max-Age=111",
+        " Secure"],
+       string:tokens(S, ";")),
+    ok.
 
 cookie_test() ->
     C1 = {"Set-Cookie",
     C1 = cookie(<<"Customer">>, <<"WILE_E_COYOTE">>, [{path, <<"/acme">>}]),
 
     {"Set-Cookie","=NoKey; Version=1"} = cookie("", "NoKey", []),
-        
-        LocalTime = calendar:universal_time_to_local_time({{2007, 5, 15}, {13, 45, 33}}), 
+    {"Set-Cookie","=NoKey; Version=1"} = cookie("", "NoKey"),
+    LocalTime = calendar:universal_time_to_local_time({{2007, 5, 15}, {13, 45, 33}}),
     C2 = {"Set-Cookie",
           "Customer=WILE_E_COYOTE; "
           "Version=1; "
     C3 = cookie("Customer", "WILE_E_COYOTE",
                 [{max_age, 86417}, {local_time, LocalTime}]),
     ok.
+
+-endif.

src/mochiweb_cover.erl

+%% @author Bob Ippolito <bob@mochimedia.com>
+%% @copyright 2010 Mochi Media, Inc.
+
+%% @doc Workarounds for various cover deficiencies.
+-module(mochiweb_cover).
+-export([get_beam/1, get_abstract_code/1,
+         get_clauses/2, clause_lookup_table/1]).
+-export([clause_lookup_table/2]).
+
+%% Internal
+
+get_beam(Module) ->
+    {Module, Beam, _Path} = code:get_object_code(Module),
+    Beam.
+
+get_abstract_code(Beam) ->
+    {ok, {_Module,
+          [{abstract_code,
+            {raw_abstract_v1, L}}]}} = beam_lib:chunks(Beam, [abstract_code]),
+    L.
+
+get_clauses(Function, Code) ->
+    [L] = [Clauses || {function, _, FName, _, Clauses}
+                          <- Code, FName =:= Function],
+    L.
+
+clause_lookup_table(Module, Function) ->
+    clause_lookup_table(
+      get_clauses(Function,
+                  get_abstract_code(get_beam(Module)))).
+
+clause_lookup_table(Clauses) ->
+    lists:foldr(fun clause_fold/2, [], Clauses).
+
+clause_fold({clause, _,
+             [InTerm],
+             _Guards=[],
+             [OutTerm]},
+            Acc) ->
+    try [{erl_parse:normalise(InTerm), erl_parse:normalise(OutTerm)} | Acc]
+    catch error:_ -> Acc
+    end;
+clause_fold(_, Acc) ->
+    Acc.
+
+%%
+%% Tests
+%%
+-include_lib("eunit/include/eunit.hrl").
+-ifdef(TEST).
+foo_table(a) -> b;
+foo_table("a") -> <<"b">>;
+foo_table(123) -> {4, 3, 2};
+foo_table([list]) -> [];
+foo_table([list1, list2]) -> [list1, list2, list3];
+foo_table(ignored) -> some, code, ignored;
+foo_table(Var) -> Var.
+
+foo_table_test() ->
+    T = clause_lookup_table(?MODULE, foo_table),
+    [?assertEqual(V, foo_table(K)) || {K, V} <- T].
+
+clause_lookup_table_test() ->
+    ?assertEqual(b, foo_table(a)),
+    ?assertEqual(ignored, foo_table(ignored)),
+    ?assertEqual('Var', foo_table('Var')),
+    ?assertEqual(
+       [{a, b},
+        {"a", <<"b">>},
+        {123, {4, 3, 2}},
+        {[list], []},
+        {[list1, list2], [list1, list2, list3]}],
+       clause_lookup_table(?MODULE, foo_table)).
+
+-endif.

src/mochiweb_echo.erl

 
 stop() ->
     mochiweb_socket_server:stop(?MODULE).
-    
+
 start() ->
     mochiweb_socket_server:start([{name, ?MODULE},
                                   {port, 6789},
         _Other ->
             exit(normal)
     end.
+
+%%
+%% Tests
+%%
+-include_lib("eunit/include/eunit.hrl").
+-ifdef(TEST).
+-endif.

src/mochiweb_headers.erl

 -export([delete_any/2, get_primary_value/2]).
 -export([default/3, enter_from_list/2, default_from_list/2]).
 -export([to_list/1, make/1]).
--export([test/0]).
+-export([from_binary/1]).
 
 %% @type headers().
 %% @type key() = atom() | binary() | string().
 %% @type value() = atom() | binary() | string() | integer().
 
-%% @spec test() -> ok
-%% @doc Run tests for this module.
-test() ->
-    H = ?MODULE:make([{hdr, foo}, {"Hdr", "bar"}, {'Hdr', 2}]),
-    [{hdr, "foo, bar, 2"}] = ?MODULE:to_list(H), 
-    H1 = ?MODULE:insert(taco, grande, H),
-    [{hdr, "foo, bar, 2"}, {taco, "grande"}] = ?MODULE:to_list(H1),
-    H2 = ?MODULE:make([{"Set-Cookie", "foo"}]),
-    [{"Set-Cookie", "foo"}] = ?MODULE:to_list(H2),
-    H3 = ?MODULE:insert("Set-Cookie", "bar", H2),
-    [{"Set-Cookie", "foo"}, {"Set-Cookie", "bar"}] = ?MODULE:to_list(H3),
-    "foo, bar" = ?MODULE:get_value("set-cookie", H3),
-    {value, {"Set-Cookie", "foo, bar"}} = ?MODULE:lookup("set-cookie", H3),
-    undefined = ?MODULE:get_value("shibby", H3),
-    none = ?MODULE:lookup("shibby", H3),
-    H4 = ?MODULE:insert("content-type",
-                        "application/x-www-form-urlencoded; charset=utf8",
-                        H3),
-    "application/x-www-form-urlencoded" = ?MODULE:get_primary_value(
-                                             "content-type", H4),
-    H4 = ?MODULE:delete_any("nonexistent-header", H4),
-    H3 = ?MODULE:delete_any("content-type", H4),
-    ok.
-
 %% @spec empty() -> headers()
 %% @doc Create an empty headers structure.
 empty() ->
 make(T) when is_tuple(T) ->
     T.
 
+%% @spec from_binary(iolist()) -> headers()
+%% @doc Transforms a raw HTTP header into a mochiweb headers structure.
+%%
+%%      The given raw HTTP header can be one of the following:
+%%
+%%      1) A string or a binary representing a full HTTP header ending with
+%%         double CRLF.
+%%         Examples:
+%%         ```
+%%         "Content-Length: 47\r\nContent-Type: text/plain\r\n\r\n"
+%%         <<"Content-Length: 47\r\nContent-Type: text/plain\r\n\r\n">>'''
+%%
+%%      2) A list of binaries or strings where each element represents a raw
+%%         HTTP header line ending with a single CRLF.
+%%         Examples:
+%%         ```
+%%         [<<"Content-Length: 47\r\n">>, <<"Content-Type: text/plain\r\n">>]
+%%         ["Content-Length: 47\r\n", "Content-Type: text/plain\r\n"]
+%%         ["Content-Length: 47\r\n", <<"Content-Type: text/plain\r\n">>]'''
+%%
+from_binary(RawHttpHeader) when is_binary(RawHttpHeader) ->
+    from_binary(RawHttpHeader, []);
+from_binary(RawHttpHeaderList) ->
+    from_binary(list_to_binary([RawHttpHeaderList, "\r\n"])).
+
+from_binary(RawHttpHeader, Acc) ->
+    case erlang:decode_packet(httph, RawHttpHeader, []) of
+        {ok, {http_header, _, H, _, V}, Rest} ->
+            from_binary(Rest, [{H, V} | Acc]);
+        _ ->
+            make(Acc)
+    end.
+
 %% @spec from_list([{key(), value()}]) -> headers()
 %% @doc Construct a headers() from the given list.
 from_list(List) ->
 
 %% @spec to_list(headers()) -> [{key(), string()}]
 %% @doc Return the contents of the headers. The keys will be the exact key
-%%      that was first inserted (e.g. may be an atom or binary, case is 
+%%      that was first inserted (e.g. may be an atom or binary, case is
 %%      preserved).
 to_list(T) ->
     F = fun ({K, {array, L}}, Acc) ->
 any_to_list(V) when is_integer(V) ->
     integer_to_list(V).
 
+%%
+%% Tests.
+%%
+-include_lib("eunit/include/eunit.hrl").
+-ifdef(TEST).
 
+make_test() ->
+    Identity = make([{hdr, foo}]),
+    ?assertEqual(
+       Identity,
+       make(Identity)).
+
+enter_from_list_test() ->
+    H = make([{hdr, foo}]),
+    ?assertEqual(
+       [{baz, "wibble"}, {hdr, "foo"}],
+       to_list(enter_from_list([{baz, wibble}], H))),
+    ?assertEqual(
+       [{hdr, "bar"}],
+       to_list(enter_from_list([{hdr, bar}], H))),
+    ok.
+
+default_from_list_test() ->
+    H = make([{hdr, foo}]),
+    ?assertEqual(
+       [{baz, "wibble"}, {hdr, "foo"}],
+       to_list(default_from_list([{baz, wibble}], H))),
+    ?assertEqual(
+       [{hdr, "foo"}],
+       to_list(default_from_list([{hdr, bar}], H))),
+    ok.
+
+get_primary_value_test() ->
+    H = make([{hdr, foo}, {baz, <<"wibble;taco">>}]),
+    ?assertEqual(
+       "foo",
+       get_primary_value(hdr, H)),
+    ?assertEqual(
+       undefined,
+       get_primary_value(bar, H)),
+    ?assertEqual(
+       "wibble",
+       get_primary_value(<<"baz">>, H)),
+    ok.
+
+set_cookie_test() ->
+    H = make([{"set-cookie", foo}, {"set-cookie", bar}, {"set-cookie", baz}]),
+    ?assertEqual(
+       [{"set-cookie", "foo"}, {"set-cookie", "bar"}, {"set-cookie", "baz"}],
+       to_list(H)),
+    ok.
+
+headers_test() ->
+    H = ?MODULE:make([{hdr, foo}, {"Hdr", "bar"}, {'Hdr', 2}]),
+    [{hdr, "foo, bar, 2"}] = ?MODULE:to_list(H),
+    H1 = ?MODULE:insert(taco, grande, H),
+    [{hdr, "foo, bar, 2"}, {taco, "grande"}] = ?MODULE:to_list(H1),
+    H2 = ?MODULE:make([{"Set-Cookie", "foo"}]),
+    [{"Set-Cookie", "foo"}] = ?MODULE:to_list(H2),
+    H3 = ?MODULE:insert("Set-Cookie", "bar", H2),
+    [{"Set-Cookie", "foo"}, {"Set-Cookie", "bar"}] = ?MODULE:to_list(H3),
+    "foo, bar" = ?MODULE:get_value("set-cookie", H3),
+    {value, {"Set-Cookie", "foo, bar"}} = ?MODULE:lookup("set-cookie", H3),
+    undefined = ?MODULE:get_value("shibby", H3),
+    none = ?MODULE:lookup("shibby", H3),
+    H4 = ?MODULE:insert("content-type",
+                        "application/x-www-form-urlencoded; charset=utf8",
+                        H3),
+    "application/x-www-form-urlencoded" = ?MODULE:get_primary_value(
+                                             "content-type", H4),
+    H4 = ?MODULE:delete_any("nonexistent-header", H4),
+    H3 = ?MODULE:delete_any("content-type", H4),
+    HB = <<"Content-Length: 47\r\nContent-Type: text/plain\r\n\r\n">>,
+    H_HB = ?MODULE:from_binary(HB),
+    H_HB = ?MODULE:from_binary(binary_to_list(HB)),
+    "47" = ?MODULE:get_value("Content-Length", H_HB),
+    "text/plain" = ?MODULE:get_value("Content-Type", H_HB),
+    L_H_HB = ?MODULE:to_list(H_HB),
+    2 = length(L_H_HB),
+    true = lists:member({'Content-Length', "47"}, L_H_HB),
+    true = lists:member({'Content-Type', "text/plain"}, L_H_HB),
+    HL = [ <<"Content-Length: 47\r\n">>, <<"Content-Type: text/plain\r\n">> ],
+    HL2 = [ "Content-Length: 47\r\n", <<"Content-Type: text/plain\r\n">> ],
+    HL3 = [ <<"Content-Length: 47\r\n">>, "Content-Type: text/plain\r\n" ],
+    H_HL = ?MODULE:from_binary(HL),
+    H_HL = ?MODULE:from_binary(HL2),
+    H_HL = ?MODULE:from_binary(HL3),
+    "47" = ?MODULE:get_value("Content-Length", H_HL),
+    "text/plain" = ?MODULE:get_value("Content-Type", H_HL),
+    L_H_HL = ?MODULE:to_list(H_HL),
+    2 = length(L_H_HL),
+    true = lists:member({'Content-Length', "47"}, L_H_HL),
+    true = lists:member({'Content-Type', "text/plain"}, L_H_HL),
+    [] = ?MODULE:to_list(?MODULE:from_binary(<<>>)),
+    [] = ?MODULE:to_list(?MODULE:from_binary(<<"">>)),
+    [] = ?MODULE:to_list(?MODULE:from_binary(<<"\r\n">>)),
+    [] = ?MODULE:to_list(?MODULE:from_binary(<<"\r\n\r\n">>)),
+    [] = ?MODULE:to_list(?MODULE:from_binary("")),
+    [] = ?MODULE:to_list(?MODULE:from_binary([<<>>])),
+    [] = ?MODULE:to_list(?MODULE:from_binary([<<"">>])),
+    [] = ?MODULE:to_list(?MODULE:from_binary([<<"\r\n">>])),
+    [] = ?MODULE:to_list(?MODULE:from_binary([<<"\r\n\r\n">>])),
+    ok.
+
+-endif.

src/mochiweb_html.erl

 %% @doc Loosely tokenizes and generates parse trees for HTML 4.
 -module(mochiweb_html).
 -export([tokens/1, parse/1, parse_tokens/1, to_tokens/1, escape/1,
-         escape_attr/1, to_html/1, test/0]).
+         escape_attr/1, to_html/1]).
 
-% This is a macro to placate syntax highlighters..
+%% This is a macro to placate syntax highlighters..
 -define(QUOTE, $\").
 -define(SQUOTE, $\').
 -define(ADV_COL(S, N),
 escape_attr(F) when is_float(F) ->
     escape_attr(mochinum:digits(F), []).
 
-%% @spec test() -> ok
-%% @doc Run tests for mochiweb_html.
-test() ->
-    test_destack(),
-    test_tokens(),
-    test_tokens2(),
-    test_parse(),
-    test_parse2(),
-    test_parse_tokens(),
-    test_escape(),
-    test_escape_attr(),
-    test_to_html(),
-    ok.
-
-
-%% Internal API
-
-test_to_html() ->
-    Expect = <<"<html><head><title>hey!</title></head><body><p class=\"foo\">what's up<br /></p><div>sucka</div><!-- comment! --></body></html>">>,
-    Expect = iolist_to_binary(
-               to_html({html, [],
-                        [{<<"head">>, [],
-                          [{title, <<"hey!">>}]},
-                         {body, [],
-                          [{p, [{class, foo}], [<<"what's">>, <<" up">>, {br}]},
-                           {'div', <<"sucka">>},
-                           {comment, <<" comment! ">>}]}]})),
-    Expect1 = <<"<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">">>,
-    Expect1 = iolist_to_binary(
-                to_html({doctype,
-                         [<<"html">>, <<"PUBLIC">>,
-                          <<"-//W3C//DTD XHTML 1.0 Transitional//EN">>,
-                          <<"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">>]})),
-    ok.
 to_html([], Acc) ->
     lists:reverse(Acc);
 to_html([{'=', Content} | Rest], Acc) ->
                   [[<<" ">>, escape(K), <<"=\"">>,
                     escape_attr(V), <<"\"">>] | Acc]).
 
-test_escape() ->
-    <<"&amp;quot;\"word &lt;&lt;up!&amp;quot;">> =
-        escape(<<"&quot;\"word <<up!&quot;">>),
-    ok.
-
-test_escape_attr() ->
-    <<"&amp;quot;&quot;word &lt;&lt;up!&amp;quot;">> =
-        escape_attr(<<"&quot;\"word <<up!&quot;">>),
-    ok.
-
 escape([], Acc) ->
     list_to_binary(lists:reverse(Acc));
 escape("<" ++ Rest, Acc) ->
     Tag = to_tag(Tag0),
     to_tokens([{Tag, R1} | Rest], [{data, B, false} | Acc]).
 
-test_tokens() ->
-    [{start_tag, <<"foo">>, [{<<"bar">>, <<"baz">>},
-                             {<<"wibble">>, <<"wibble">>},
-                             {<<"alice">>, <<"bob">>}], true}] =
-        tokens(<<"<foo bar=baz wibble='wibble' alice=\"bob\"/>">>),
-    [{start_tag, <<"foo">>, [{<<"bar">>, <<"baz">>},
-                             {<<"wibble">>, <<"wibble">>},
-                             {<<"alice">>, <<"bob">>}], true}] =
-        tokens(<<"<foo bar=baz wibble='wibble' alice=bob/>">>),
-    [{comment, <<"[if lt IE 7]>\n<style type=\"text/css\">\n.no_ie { display: none; }\n</style>\n<![endif]">>}] =
-        tokens(<<"<!--[if lt IE 7]>\n<style type=\"text/css\">\n.no_ie { display: none; }\n</style>\n<![endif]-->">>),
-    [{start_tag, <<"script">>, [{<<"type">>, <<"text/javascript">>}], false},
-     {data, <<" A= B <= C ">>, false},
-     {end_tag, <<"script">>}] =
-        tokens(<<"<script type=\"text/javascript\"> A= B <= C </script>">>),
-    [{start_tag, <<"script">>, [{<<"type">>, <<"text/javascript">>}], false},
-     {data, <<" A= B <= C ">>, false},
-     {end_tag, <<"script">>}] =
-        tokens(<<"<script type =\"text/javascript\"> A= B <= C </script>">>),
-    [{start_tag, <<"script">>, [{<<"type">>, <<"text/javascript">>}], false},
-     {data, <<" A= B <= C ">>, false},
-     {end_tag, <<"script">>}] =
-        tokens(<<"<script type = \"text/javascript\"> A= B <= C </script>">>),
-    [{start_tag, <<"script">>, [{<<"type">>, <<"text/javascript">>}], false},
-     {data, <<" A= B <= C ">>, false},
-     {end_tag, <<"script">>}] =
-        tokens(<<"<script type= \"text/javascript\"> A= B <= C </script>">>),
-    [{start_tag, <<"textarea">>, [], false},
-     {data, <<"<html></body>">>, false},
-     {end_tag, <<"textarea">>}] =
-        tokens(<<"<textarea><html></body></textarea>">>),
-    ok.
-
 tokens(B, S=#decoder{offset=O}, Acc) ->
     case B of
         <<_:O/binary>> ->
             tokenize_data(B, S)
     end.
 
-test_parse() ->
-    D0 = <<"<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01//EN\" \"http://www.w3.org/TR/html4/strict.dtd\">
-<html>
- <head>
-   <meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\">
-   <title>Foo</title>
-   <link rel=\"stylesheet\" type=\"text/css\" href=\"/static/rel/dojo/resources/dojo.css\" media=\"screen\">
-   <link rel=\"stylesheet\" type=\"text/css\" href=\"/static/foo.css\" media=\"screen\">
-   <!--[if lt IE 7]>
-   <style type=\"text/css\">
-     .no_ie { display: none; }
-   </style>
-   <![endif]-->
-   <link rel=\"icon\" href=\"/static/images/favicon.ico\" type=\"image/x-icon\">
-   <link rel=\"shortcut icon\" href=\"/static/images/favicon.ico\" type=\"image/x-icon\">
- </head>
- <body id=\"home\" class=\"tundra\"><![CDATA[&lt;<this<!-- is -->CDATA>&gt;]]></body>
-</html>">>,
-    Expect = {<<"html">>, [],
-              [{<<"head">>, [],
-                [{<<"meta">>,
-                  [{<<"http-equiv">>,<<"Content-Type">>},
-                   {<<"content">>,<<"text/html; charset=UTF-8">>}],
-                  []},
-                 {<<"title">>,[],[<<"Foo">>]},
-                 {<<"link">>,
-                  [{<<"rel">>,<<"stylesheet">>},
-                   {<<"type">>,<<"text/css">>},
-                   {<<"href">>,<<"/static/rel/dojo/resources/dojo.css">>},
-                   {<<"media">>,<<"screen">>}],
-                  []},
-                 {<<"link">>,
-                  [{<<"rel">>,<<"stylesheet">>},
-                   {<<"type">>,<<"text/css">>},
-                   {<<"href">>,<<"/static/foo.css">>},
-                   {<<"media">>,<<"screen">>}],
-                  []},
-                 {comment,<<"[if lt IE 7]>\n   <style type=\"text/css\">\n     .no_ie { display: none; }\n   </style>\n   <![endif]">>},
-                 {<<"link">>,
-                  [{<<"rel">>,<<"icon">>},
-                   {<<"href">>,<<"/static/images/favicon.ico">>},
-                   {<<"type">>,<<"image/x-icon">>}],
-                  []},
-                 {<<"link">>,
-                  [{<<"rel">>,<<"shortcut icon">>},
-                   {<<"href">>,<<"/static/images/favicon.ico">>},
-                   {<<"type">>,<<"image/x-icon">>}],
-                  []}]},
-               {<<"body">>,
-                [{<<"id">>,<<"home">>},
-                 {<<"class">>,<<"tundra">>}],
-                [<<"&lt;<this<!-- is -->CDATA>&gt;">>]}]},
-    Expect = parse(D0),
-    ok.
-
-test_tokens2() ->
-    D0 = <<"<channel><title>from __future__ import *</title><link>http://bob.pythonmac.org</link><description>Bob's Rants</description></channel>">>,
-    Expect = [{start_tag,<<"channel">>,[],false},
-              {start_tag,<<"title">>,[],false},
-              {data,<<"from __future__ import *">>,false},
-              {end_tag,<<"title">>},
-              {start_tag,<<"link">>,[],true},
-              {data,<<"http://bob.pythonmac.org">>,false},
-              {end_tag,<<"link">>},
-              {start_tag,<<"description">>,[],false},
-              {data,<<"Bob's Rants">>,false},
-              {end_tag,<<"description">>},
-              {end_tag,<<"channel">>}],
-    Expect = tokens(D0),
-    ok.
-
-test_parse2() ->
-    D0 = <<"<channel><title>from __future__ import *</title><link>http://bob.pythonmac.org<br>foo</link><description>Bob's Rants</description></channel>">>,
-    Expect = {<<"channel">>,[],
-              [{<<"title">>,[],[<<"from __future__ import *">>]},
-               {<<"link">>,[],[
-                               <<"http://bob.pythonmac.org">>,
-                               {<<"br">>,[],[]},
-                               <<"foo">>]},
-               {<<"description">>,[],[<<"Bob's Rants">>]}]},
-    Expect = parse(D0),
-    ok.
-
-test_parse_tokens() ->
-    D0 = [{doctype,[<<"HTML">>,<<"PUBLIC">>,<<"-//W3C//DTD HTML 4.01 Transitional//EN">>]},
-          {data,<<"\n">>,true},
-          {start_tag,<<"html">>,[],false}],
-    {<<"html">>, [], []} = parse_tokens(D0),
-    D1 = D0 ++ [{end_tag, <<"html">>}],
-    {<<"html">>, [], []} = parse_tokens(D1),
-    D2 = D0 ++ [{start_tag, <<"body">>, [], false}],
-    {<<"html">>, [], [{<<"body">>, [], []}]} = parse_tokens(D2),
-    D3 = D0 ++ [{start_tag, <<"head">>, [], false},
-                {end_tag, <<"head">>},
-                {start_tag, <<"body">>, [], false}],
-    {<<"html">>, [], [{<<"head">>, [], []}, {<<"body">>, [], []}]} = parse_tokens(D3),
-    D4 = D3 ++ [{data,<<"\n">>,true},
-                {start_tag,<<"div">>,[{<<"class">>,<<"a">>}],false},
-                {start_tag,<<"a">>,[{<<"name">>,<<"#anchor">>}],false},
-                {end_tag,<<"a">>},
-                {end_tag,<<"div">>},
-                {start_tag,<<"div">>,[{<<"class">>,<<"b">>}],false},
-                {start_tag,<<"div">>,[{<<"class">>,<<"c">>}],false},
-                {end_tag,<<"div">>},
-                {end_tag,<<"div">>}],
-    {<<"html">>, [],
-     [{<<"head">>, [], []},
-      {<<"body">>, [],
-       [{<<"div">>, [{<<"class">>, <<"a">>}], [{<<"a">>, [{<<"name">>, <<"#anchor">>}], []}]},
-        {<<"div">>, [{<<"class">>, <<"b">>}], [{<<"div">>, [{<<"class">>, <<"c">>}], []}]}
-       ]}]} = parse_tokens(D4),
-    D5 = [{start_tag,<<"html">>,[],false},
-          {data,<<"\n">>,true},
-          {data,<<"boo">>,false},
-          {data,<<"hoo">>,false},
-          {data,<<"\n">>,true},
-          {end_tag,<<"html">>}],
-    {<<"html">>, [], [<<"\nboohoo\n">>]} = parse_tokens(D5),
-    D6 = [{start_tag,<<"html">>,[],false},
-          {data,<<"\n">>,true},
-          {data,<<"\n">>,true},
-          {end_tag,<<"html">>}],
-    {<<"html">>, [], []} = parse_tokens(D6),
-    D7 = [{start_tag,<<"html">>,[],false},
-          {start_tag,<<"ul">>,[],false},
-          {start_tag,<<"li">>,[],false},
-          {data,<<"word">>,false},
-          {start_tag,<<"li">>,[],false},
-          {data,<<"up">>,false},
-          {end_tag,<<"li">>},
-          {start_tag,<<"li">>,[],false},
-          {data,<<"fdsa">>,false},
-          {start_tag,<<"br">>,[],true},
-          {data,<<"asdf">>,false},
-          {end_tag,<<"ul">>},
-          {end_tag,<<"html">>}],
-    {<<"html">>, [],
-     [{<<"ul">>, [],
-       [{<<"li">>, [], [<<"word">>]},
-        {<<"li">>, [], [<<"up">>]},
-        {<<"li">>, [], [<<"fdsa">>,{<<"br">>, [], []}, <<"asdf">>]}]}]} = parse_tokens(D7),
-    ok.
-
 tree_data([{data, Data, Whitespace} | Rest], AllWhitespace, Acc) ->
     tree_data(Rest, (Whitespace andalso AllWhitespace), [Data | Acc]);
 tree_data(Rest, AllWhitespace, Acc) ->
     tree(Rest, append_stack_child(T, S));
 tree(L=[{data, _Data, _Whitespace} | _], S) ->
     case tree_data(L, true, []) of
-        {_, true, Rest} -> 
+        {_, true, Rest} ->
             tree(Rest, S);
         {Data, false, Rest} ->
             tree(Rest, append_stack_child(Data, S))
-    end.
+    end;
+tree([{doctype, _} | Rest], Stack) ->
+    tree(Rest, Stack).
 
 norm({Tag, Attrs}) ->
     {norm(Tag), [{norm(K), iolist_to_binary(V)} || {K, V} <- Attrs], []};
 norm(Tag) ->
     list_to_binary(string:to_lower(Tag)).
 
-test_destack() ->
-    {<<"a">>, [], []} =
-        destack([{<<"a">>, [], []}]),
-    {<<"a">>, [], [{<<"b">>, [], []}]} =
-        destack([{<<"b">>, [], []}, {<<"a">>, [], []}]),
-    {<<"a">>, [], [{<<"b">>, [], [{<<"c">>, [], []}]}]} =
-     destack([{<<"c">>, [], []}, {<<"b">>, [], []}, {<<"a">>, [], []}]),
-    [{<<"a">>, [], [{<<"b">>, [], [{<<"c">>, [], []}]}]}] =
-     destack(<<"b">>,
-             [{<<"c">>, [], []}, {<<"b">>, [], []}, {<<"a">>, [], []}]),
-    [{<<"b">>, [], [{<<"c">>, [], []}]}, {<<"a">>, [], []}] =
-     destack(<<"c">>,
-             [{<<"c">>, [], []}, {<<"b">>, [], []},{<<"a">>, [], []}]),
-    ok.
-
 stack(T1={TN, _, _}, Stack=[{TN, _, _} | _Rest])
   when TN =:= <<"li">> orelse TN =:= <<"option">> ->
     [T1 | destack(TN, Stack)];
         <<_:Start/binary, Raw/binary>> ->
             {{data, Raw, false}, S}
     end.
+
+
+%%
+%% Tests
+%%
+-include_lib("eunit/include/eunit.hrl").