Skip to content
54 changes: 15 additions & 39 deletions lib/common_test/src/ct_doctest.erl
Original file line number Diff line number Diff line change
Expand Up @@ -278,13 +278,14 @@ if it were in a file. For example:
-module(my_module).
-export([foo/0]).
foo() ->
ok.
{?MODULE, ?FUNCTION_NAME, ?LINE}.
```

The module is then available for use in following prompts. For example:

```
1> my_module:foo().
{my_module, foo, 4}
```

### Edge cases
Expand Down Expand Up @@ -348,15 +349,17 @@ Options for doctest execution.
{missing_tests, [{atom(), arity()}]} |
{skip_tests, [moduledoc |
{function | type | callback, atom(), arity()}]} |
{verbose, boolean()}].
{verbose, boolean()} |
{compile_options, [compile:option()]}].

-record(options,
{ parser = fun parse_markdown_builtin/1 :: fun((unicode:unicode_binary()) -> term()),
skipped_blocks = false :: non_neg_integer() | false,
missing_tests = false :: [{atom(), arity()}] | false,
skip_tests = [] :: [moduledoc |
{function | type | callback, atom(), arity()}],
verbose = false :: boolean() }).
verbose = false :: boolean(),
compile_options = [] :: [compile:option()] }).

-doc #{equiv => module(Module, [])}.
-spec module(module()) ->
Expand Down Expand Up @@ -638,6 +641,8 @@ options(OptionsList) ->
Acc#options{ skip_tests = proplists:get_value(skip_tests, OptionsList) };
(verbose, Acc) ->
Acc#options{ verbose = proplists:get_value(verbose, OptionsList) };
(compile_options, Acc) ->
Acc#options{ compile_options = proplists:get_value(compile_options, OptionsList) };
(_Key, Acc) ->
Acc
end, #options{}, proplists:get_keys(OptionsList)).
Expand Down Expand Up @@ -752,54 +757,25 @@ run_test(Code, InitialBindings, Options) ->
end
end, InitialBindings, Tests),
[ok];
{match, [_Line_Number, _Prefix = <<"-module(">>, _Code]} ->
[compile_string(Code)];
{match, [_Line_Number, _Prefix = <<"-module(">>, ModContent]} ->
[ModName | _] = binary:split(ModContent, [<<")">>]),
[compile_string(Code, ModName, Options#options.compile_options)];
_ ->
[]
end
end.

compile_string(Code) ->

Toks =
case erl_scan:string(unicode:characters_to_list(Code),
0,
[text]) of
{ok, T, _} ->
T;
{error, {Line,Mod,Reason}, _} ->
Message = io_lib:format("unknown:~p: ~ts",[Line, Mod:format_error(Reason)]),
throw({error,#{ message => Message, context => Code}})
end,

Forms = parse_tokens(Code, Toks),

{attribute,_,module,ModuleName} = lists:keyfind(module, 3, Forms),

case compile:forms(Forms, [binary, return_errors, {source, atom_to_list(ModuleName) ++ ".erl"}]) of
compile_string(Code, ModName, CompileOptions) ->
FileName = unicode:characters_to_list(ModName) ++ ".erl",
case compile:string(Code, [binary, return_errors, {source, FileName} | CompileOptions]) of
{ok, Module, Binary} ->
{module, Module} = code:load_binary(Module, "nofile", Binary),
{module, Module} = code:load_binary(Module, FileName, Binary),
ok;
{error, Errors, Warnings} ->
Messages = [begin [{_, M}] = sys_messages:format_messages(File, "", Msgs, []), M end || {File, Msgs} <- Errors ++ Warnings],
throw({error,#{ message => Messages, context => Code}})
end.

parse_tokens(_Code, []) -> [];
parse_tokens(Code, Toks) ->
case lists:splitwith(fun(T) ->
element(1, T) =/= dot
end, Toks) of
{Ts, [{dot, _} = Dot | Rest]} ->
case erl_parse:parse_form(Ts ++ [Dot]) of
{ok, Forms} ->
[Forms | parse_tokens(Code, Rest)];
{error, {Line, Mod, Reason}} ->
Message = io_lib:format("unknown:~p: ~ts",[Line, Mod:format_error(Reason)]),
throw({error,#{ message => Message, context => Code}})
end
end.

check_prompt_numbers(Tests) ->
check_prompt_numbers(Tests, 1).

Expand Down
14 changes: 10 additions & 4 deletions lib/common_test/test/ct_doctest_SUITE.erl
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
-export([api_branches/1, module_result_modes/1,
docs_filtering_and_error_formatting/1, parser_prompt_parsing/1,
runtime_failure_matching/1, parse_rewrite_helpers/1, file_support/1,
external_parser/1, module/1,
external_parser/1, module/1, module_preprocessor/1,
type_and_callback_docs/1, verbose_option/1,
skipped_blocks_option/1, missing_tests_option/1,
skip_tests_option/1,
Expand All @@ -47,6 +47,7 @@ all() ->
file_support,
external_parser,
module,
module_preprocessor,
type_and_callback_docs,
verbose_option,
skipped_blocks_option,
Expand Down Expand Up @@ -141,12 +142,17 @@ file_support(Config) ->

module(_Config) ->

ExpectedSubstrings = ["unknown:2: unterminated atom starting with 'ok.\\n'",
"unknown:2: syntax error before: ok",
"test.erl:2: function f/0 undefined"],
ExpectedSubstrings = ["test.erl:3:11: unterminated atom starting with 'ok.",
"test.erl:3:7: syntax error before: ok",
"test.erl:3:2: function f/0 undefined"],

expect_error_count(ct_doctest_module_mod, [], 3, ExpectedSubstrings).

module_preprocessor(_Config) ->
%% Test that module code blocks support preprocessor features:
%% ?MODULE macro, -define macros, and -record definitions.
ok = ct_doctest:module(ct_doctest_module_preproc_mod).

external_parser(Config) ->
DataDir = ?config(data_dir, Config),
ParserOpt = {parser, fun ct_doctest_external_parser_mod:parse_doc/1},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
%%
%% %CopyrightBegin%
%%
%% SPDX-License-Identifier: Apache-2.0
%%
%% Copyright Ericsson AB 2026. 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(ct_doctest_module_preproc_mod).
-moduledoc """
Test that module code blocks in doctests support preprocessor features
like macros and records via epp.
""".

-export([uses_module_macro/0, uses_define/0, uses_record/0]).

-doc """
Module code block using ?MODULE macro:

```erlang
-module(macro_mod).
-export([name/0]).
name() -> ?MODULE.
```

```erlang
1> macro_mod:name().
macro_mod
```
""".
uses_module_macro() ->
ok.

-doc """
Module code block using -define macro:

```erlang
-module(define_mod).
-export([value/0]).
-define(VALUE, 42).
value() -> ?VALUE.
```

```erlang
1> define_mod:value().
42
```
""".
uses_define() ->
ok.

-doc """
Module code block using -record:

```erlang
-module(record_mod).
-export([new/2, name/1, age/1]).
-record(person, {name, age}).
new(Name, Age) -> #person{name = Name, age = Age}.
name(#person{name = N}) -> N.
age(#person{age = A}) -> A.
```

```erlang
1> P = record_mod:new(alice, 30).
2> record_mod:name(P).
alice
3> record_mod:age(P).
30
```
""".
uses_record() ->
ok.
Loading
Loading