summaryrefslogtreecommitdiff
path: root/lib/stdlib/test/shell_docs_SUITE.erl
diff options
context:
space:
mode:
Diffstat (limited to 'lib/stdlib/test/shell_docs_SUITE.erl')
-rw-r--r--lib/stdlib/test/shell_docs_SUITE.erl237
1 files changed, 237 insertions, 0 deletions
diff --git a/lib/stdlib/test/shell_docs_SUITE.erl b/lib/stdlib/test/shell_docs_SUITE.erl
new file mode 100644
index 0000000000..222aec5e0b
--- /dev/null
+++ b/lib/stdlib/test/shell_docs_SUITE.erl
@@ -0,0 +1,237 @@
+%%
+%% %CopyrightBegin%
+%%
+%% Copyright Ericsson AB 2020. All Rights Reserved.
+%%
+%% Licensed under the Apache License, Version 2.0 (the "License");
+%% you may not use this file except in compliance with the License.
+%% You may obtain a copy of the License at
+%%
+%% http://www.apache.org/licenses/LICENSE-2.0
+%%
+%% Unless required by applicable law or agreed to in writing, software
+%% distributed under the License is distributed on an "AS IS" BASIS,
+%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+%% See the License for the specific language governing permissions and
+%% limitations under the License.
+%%
+%% %CopyrightEnd%
+%%
+-module(shell_docs_SUITE).
+-export([all/0, suite/0, groups/0, init_per_suite/1, end_per_suite/1,
+ init_per_group/2, end_per_group/2]).
+
+-export([render/1, links/1, normalize/1, render_prop/1]).
+
+-export([render_all/1]).
+
+-include_lib("kernel/include/eep48.hrl").
+-include_lib("stdlib/include/assert.hrl").
+
+suite() ->
+ [{timetrap,{minutes,10}}].
+
+all() ->
+ [render, links, normalize, {group, prop}].
+
+groups() ->
+ [{prop,[],[render_prop]}].
+
+%% Include a spec here in order to test that specs of undocumented functions
+%% is rendered correctly.
+-spec init_per_suite(Config1) -> Config2 when
+ Config1 :: list({atom(),term()}),
+ Config2 :: list({atom(),term()}).
+init_per_suite(Config) ->
+ {ok, ?MODULE} = c:c(?MODULE,[debug_info]),
+ Config.
+
+end_per_suite(_Config) ->
+ ok.
+
+init_per_group(prop, Config) ->
+ ct_property_test:init_per_suite(Config);
+init_per_group(_GroupName, Config) ->
+ Config.
+
+end_per_group(_GroupName, Config) ->
+ Config.
+
+render(_Config) ->
+ docsmap(
+ fun(Mod, #docs_v1{ docs = Docs } = D) ->
+ lists:foreach(
+ fun(Config) ->
+ try
+ shell_docs:render(Mod, D, Config),
+ shell_docs:render_type(Mod, D, Config),
+ shell_docs:render_callback(Mod, D, Config),
+ [try
+ shell_docs:render(Mod, F, A, D, Config)
+ catch _E:R:ST ->
+ io:format("Failed to render ~p:~p/~p~n~p:~p~n~p~n",
+ [Mod,F,A,R,ST,shell_docs:get_doc(Mod,F,A)]),
+ erlang:raise(error,R,ST)
+ end || {F,A} <- Mod:module_info(exports)],
+ [try
+ shell_docs:render_type(Mod, T, A, D, Config)
+ catch _E:R:ST ->
+ io:format("Failed to render type ~p:~p/~p~n~p:~p~n~p~n",
+ [Mod,T,A,R,ST,shell_docs:get_type_doc(Mod,T,A)]),
+ erlang:raise(error,R,ST)
+ end || {{type,T,A},_,_,_,_} <- Docs],
+ [try
+ shell_docs:render_callback(Mod, T, A, D, Config)
+ catch _E:R:ST ->
+ io:format("Failed to render callback ~p:~p/~p~n~p:~p~n~p~n",
+ [Mod,T,A,R,ST,shell_docs:get_callback_doc(Mod,T,A)]),
+ erlang:raise(error,R,ST)
+ end || {{callback,T,A},_,_,_,_} <- Docs]
+ catch throw:R:ST ->
+ io:format("Failed to render ~p~n~p:~p~n",[Mod,R,ST]),
+ exit(R)
+ end
+ end, [#{},
+ #{ ansi => false },
+ #{ ansi => true },
+ #{ columns => 5 },
+ #{ columns => 150 },
+ #{ encoding => unicode},
+ #{ encoding => latin1}])
+ end),
+ ok.
+
+render_prop(Config) ->
+% dbg:tracer(),dbg:p(all,c),dbg:tpl(shell_docs_prop,[]),
+ ct_property_test:quickcheck(
+ shell_docs_prop:prop_render(),Config).
+
+links(_Config) ->
+ docsmap(
+ fun(Mod, #docs_v1{ module_doc = MDoc, docs = Docs }) ->
+ try
+ [check_links(Mod, maps:get(<<"en">>,MDoc)) || MDoc =/= none, MDoc =/= hidden]
+ catch _E1:R1:ST1 ->
+ io:format("Failed to render ~p~n~p:~p~n~p~n",
+ [Mod,R1,ST1,MDoc]),
+ erlang:raise(error,R1,ST1)
+ end,
+ [try
+ check_links(Mod, D)
+ catch _E:R:ST ->
+ io:format("Failed to render ~p:~p~n~p:~p~n~p~n",
+ [Mod,Kind,R,ST,D]),
+ erlang:raise(error,R,ST)
+ end || {Kind,_Anno,_Sig,#{ <<"en">> := D },_MD} <- Docs]
+ end).
+
+check_links(Mod, [{a,Attr,C}|T]) ->
+ case proplists:get_value(rel,Attr) of
+ <<"https://erlang.org/doc/link/seemfa">> ->
+ case string:lexemes(proplists:get_value(href, Attr),":#/") of
+ [_App,TargetMod,Func,Arity] ->
+ case code:get_doc(b2a(TargetMod)) of
+ {ok, _ModDoc} ->
+ case shell_docs:get_doc(b2a(TargetMod),
+ b2a(Func),
+ binary_to_integer(Arity)) of
+ [] -> throw({could_not_find,b2a(TargetMod),
+ b2a(Func),
+ binary_to_integer(Arity)});
+ _Else -> ok
+ end;
+ _Else ->
+ throw({could_not_find, TargetMod, Attr})
+ end;
+ _Callback ->
+ ok
+ end;
+ _OtherlinkType ->
+ ok
+ end,
+ check_links(Mod, C),
+ check_links(Mod, T);
+check_links(Mod, [{_,_,C}|T]) ->
+ check_links(Mod, C),
+ check_links(Mod, T);
+check_links(Mod, [C|T]) when is_binary(C) ->
+ check_links(Mod, T);
+check_links(_, []) ->
+ ok.
+
+normalize(_Config) ->
+ ?assertMatch(
+ [{p,[],[{em,[],[<<"a ">>,{code,[],[<<"b ">>]},<<"c">>]}]}],
+ shell_docs:normalize([{p,[],[{em,[],[<<" a ">>,{code,[],[<<" b ">>]},<<" c">>]}]}])
+ ),
+ ?assertMatch(
+ [{'div',[],[<<"!">>]}],
+ shell_docs:normalize([{'div',[],[{code,[],[<<" ">>,{i,[],[<<" ">>]}]},<<" !">>]}])
+ ),
+ ok.
+
+%% Special binary_to_atom that deals with <<"'and'">>
+b2a(Bin) ->
+ case erl_scan:string(binary_to_list(Bin)) of
+ {ok,[{atom,_,A}],_} -> A;
+ {ok,[{A,_}],_} -> A
+ end.
+
+%% Testing functions
+render_all(Dir) ->
+ file:make_dir(Dir),
+ docsmap(
+ fun(Mod, #docs_v1{ docs = Docs } = D) ->
+ SMod = atom_to_list(Mod),
+ file:write_file(filename:join(Dir,SMod ++ ".txt"),
+ unicode:characters_to_binary(shell_docs:render(Mod, D))),
+ file:write_file(filename:join(Dir,SMod ++ "_type.txt"),
+ unicode:characters_to_binary(shell_docs:render_type(Mod, D))),
+ file:write_file(filename:join(Dir,SMod ++ "_cb.txt"),
+ unicode:characters_to_binary(shell_docs:render_callback(Mod, D))),
+ lists:foreach(
+ fun({{function,Name,Arity},_Anno,_Sig,_Doc,_Meta}) ->
+ FName = SMod ++ "_"++atom_to_list(Name)++"_"++integer_to_list(Arity)++"_func.txt",
+ ok = file:write_file(filename:join(Dir,re:replace(FName,"[/:]","_",
+ [global,{return,list}])),
+ unicode:characters_to_binary(shell_docs:render(Mod, Name, Arity, D)));
+ ({{type,Name,Arity},_Anno,_Sig,_Doc,_Meta}) ->
+ FName = SMod ++ "_"++atom_to_list(Name)++"_"++integer_to_list(Arity)++"_type.txt",
+ ok = file:write_file(filename:join(Dir,re:replace(FName,"[/:]","_",
+ [global,{return,list}])),
+ unicode:characters_to_binary(shell_docs:render_type(Mod, Name, Arity, D)));
+ ({{callback,Name,Arity},_Anno,_Sig,_Doc,_Meta}) ->
+ FName = SMod ++ "_"++atom_to_list(Name)++"_"++integer_to_list(Arity)++"_cb.txt",
+ file:write_file(filename:join(Dir,re:replace(FName,"[/:]","_",
+ [global,{return,list}])),
+ unicode:characters_to_binary(shell_docs:render_callback(Mod, Name, Arity, D)))
+ end, Docs)
+ end).
+
+docsmap(Fun) ->
+ lists:map(
+ fun F({Mod,_,_}) ->
+ F(Mod);
+ F(Mod) when is_list(Mod) ->
+ F(list_to_atom(Mod));
+ F(Mod) ->
+ case code:get_doc(Mod) of
+ {error, missing} ->
+ ok;
+ {error, cover_compiled} ->
+ ok;
+ {error, E} when E =:= eperm; E =:= eacces ->
+ %% This can happen in BSD's for some reason...
+ ok;
+ {error, eisdir} ->
+ %% Uhm?
+ ok;
+ {ok, Docs} ->
+ try
+ Fun(Mod, Docs)
+ catch E:R:ST ->
+ io:format("Failed to render ~p~n~p:~p:~p~n",[Mod,E,R,ST]),
+ erlang:raise(E,R,ST)
+ end
+ end
+ end, code:all_available()).