diff --git a/erts/emulator/test/atomics_SUITE.erl b/erts/emulator/test/atomics_SUITE.erl index be39246a4b93..d3560d5c9699 100644 --- a/erts/emulator/test/atomics_SUITE.erl +++ b/erts/emulator/test/atomics_SUITE.erl @@ -22,7 +22,7 @@ -module(atomics_SUITE). -export([suite/0, all/0, signed/1, unsigned/1, bad/1, signed_limits/1, unsigned_limits/1, - error_info/1]). + error_info/1, doctests/1]). -include_lib("common_test/include/ct.hrl"). @@ -30,7 +30,7 @@ suite() -> [{ct_hooks,[ts_install_cth]}]. all() -> [signed, unsigned, bad, signed_limits, unsigned_limits, - error_info]. + error_info, doctests]. signed(Config) when is_list(Config) -> Size = 10, @@ -157,6 +157,9 @@ max_atomic_sz() -> end end. +doctests(_Config) -> + ct_doctest:module(atomics). + error_info(_Config) -> Atomics = atomics:new(10, []), Huge = 1 bsl 64, diff --git a/erts/emulator/test/counters_SUITE.erl b/erts/emulator/test/counters_SUITE.erl index ab3b2184ad2c..f921e44c4aaf 100644 --- a/erts/emulator/test/counters_SUITE.erl +++ b/erts/emulator/test/counters_SUITE.erl @@ -25,12 +25,12 @@ -export([suite/0, all/0]). -export([basic/1, bad/1, limits/1, indep/1, write_concurrency/1, - error_info/1]). + error_info/1, doctests/1]). suite() -> [{ct_hooks,[ts_install_cth]}]. all() -> - [basic, bad, limits, indep, write_concurrency, error_info]. + [basic, bad, limits, indep, write_concurrency, error_info, doctests]. basic(Config) when is_list(Config) -> Size = 10, @@ -240,6 +240,9 @@ rand_log64() -> 2 -> Uint end. +doctests(_Config) -> + ct_doctest:module(counters). + error_info(_Config) -> Counters = counters:new(10, [write_concurrency]), Huge = 1 bsl 64, diff --git a/erts/emulator/test/persistent_term_SUITE.erl b/erts/emulator/test/persistent_term_SUITE.erl index 019a429f6f2f..532bb153a93c 100644 --- a/erts/emulator/test/persistent_term_SUITE.erl +++ b/erts/emulator/test/persistent_term_SUITE.erl @@ -39,7 +39,8 @@ shared_magic_ref/1, non_message_signal/1, get_put_colliding_bucket/1, - gc_binary_orig/1]). + gc_binary_orig/1, + doctests/1]). %% -export([test_init_restart_cmd/1]). @@ -60,7 +61,8 @@ all() -> shared_magic_ref, non_message_signal, get_put_colliding_bucket, - gc_binary_orig]. + gc_binary_orig, + doctests]. init_per_suite(Config) -> erts_debug:set_internal_state(available_internal_state, true), @@ -1266,3 +1268,11 @@ gpcb_updater(CollidesWith) -> persistent_term:erase(CollidesWith), persistent_term:put(CollidesWith, unexpected), gpcb_updater(CollidesWith). + +doctests(_Config) -> + Keys = [scheduler_ets, my_key, foo, bar, {my_app, config}], + try + ct_doctest:module(persistent_term) + after + [persistent_term:erase(K) || K <- Keys] + end. diff --git a/erts/preloaded/src/atomics.erl b/erts/preloaded/src/atomics.erl index 729ab6d36555..64cc8f7ee43b 100644 --- a/erts/preloaded/src/atomics.erl +++ b/erts/preloaded/src/atomics.erl @@ -41,6 +41,41 @@ access. The atomics are organized into arrays with the following semantics: can read the new value of B and then read the old value of A. - Indexes into atomic arrays are one-based. An atomic array of arity N contains N atomics with index from 1 to N. + +## Examples + +```erlang +1> Ref = atomics:new(3, [{signed, true}]). +2> atomics:put(Ref, 1, 10). +ok +3> atomics:get(Ref, 1). +10 +4> atomics:add(Ref, 1, 5). +ok +5> atomics:get(Ref, 1). +15 +6> atomics:exchange(Ref, 1, 42). +15 +7> atomics:compare_exchange(Ref, 1, 42, 100). +ok +8> atomics:get(Ref, 1). +100 +9> atomics:info(Ref). +#{size => 3, max => ..., min => ..., memory => ...} +``` + +Atomics can be updated concurrently from multiple processes: + +```erlang +1> Ref = atomics:new(1, []). +2> Self = self(). +3> N = 1000. +1000 +4> Pids = [spawn(fun() -> atomics:add(Ref, 1, 1), Self ! self() end) || _ <- lists:seq(1, N)]. +5> [receive Pid -> ok end || Pid <- Pids]. +6> atomics:get(Ref, 1). +1000 +``` """. -moduledoc(#{since => "OTP 21.2"}). @@ -77,6 +112,14 @@ Argument `Opts` is a list of the following possible options: Atomics are not tied to the current process and are automatically garbage collected when they are no longer referenced. + +## Examples + +```erlang +1> atomics:new(5, []). +2> atomics:new(5, [{signed, false}]). +3> atomics:new(5, [{signed, true}]). +``` """. -doc(#{since => <<"OTP 21.2">>}). -spec new(Arity, Opts) -> atomics_ref() when @@ -105,7 +148,23 @@ encode_opts([], Acc) -> encode_opts(_, _) -> throw(badopt). --doc "Set atomic to `Value`.". +-doc """ +Set atomic to `Value`. + +## Examples + +```erlang +1> Ref = atomics:new(1, []). +2> atomics:put(Ref, 1, 42). +ok +3> atomics:get(Ref, 1). +42 +4> atomics:put(Ref, 1, -10). +ok +5> atomics:get(Ref, 1). +-10 +``` +""". -doc(#{since => <<"OTP 21.2">>}). -spec put(Ref, Ix, Value) -> ok when Ref :: atomics_ref(), @@ -114,7 +173,21 @@ encode_opts(_, _) -> put(_Ref, _Ix, _Value) -> erlang:nif_error(undef). --doc "Read atomic value.". +-doc """ +Read atomic value. + +## Examples + +```erlang +1> Ref = atomics:new(2, []). +2> atomics:get(Ref, 1). +0 +3> atomics:put(Ref, 1, 100). +ok +4> atomics:get(Ref, 1). +100 +``` +""". -doc(#{since => <<"OTP 21.2">>}). -spec get(Ref, Ix) -> integer() when Ref :: atomics_ref(), @@ -122,7 +195,25 @@ put(_Ref, _Ix, _Value) -> get(_Ref, _Ix) -> erlang:nif_error(undef). --doc "Add `Incr` to atomic.". +-doc """ +Add `Incr` to atomic. + +## Examples + +```erlang +1> Ref = atomics:new(1, []). +2> atomics:add(Ref, 1, 5). +ok +3> atomics:add(Ref, 1, 3). +ok +4> atomics:get(Ref, 1). +8 +5> atomics:add(Ref, 1, -2). +ok +6> atomics:get(Ref, 1). +6 +``` +""". -doc(#{since => <<"OTP 21.2">>}). -spec add(Ref, Ix, Incr) -> ok when Ref :: atomics_ref(), @@ -131,7 +222,21 @@ get(_Ref, _Ix) -> add(_Ref, _Ix, _Incr) -> erlang:nif_error(undef). --doc "Atomically add `Incr` to atomic and return the result.". +-doc """ +Atomically add `Incr` to atomic and return the result. + +## Examples + +```erlang +1> Ref = atomics:new(1, []). +2> atomics:add_get(Ref, 1, 5). +5 +3> atomics:add_get(Ref, 1, 10). +15 +4> atomics:add_get(Ref, 1, -3). +12 +``` +""". -doc(#{since => <<"OTP 21.2">>}). -spec add_get(Ref, Ix, Incr) -> integer() when Ref :: atomics_ref(), @@ -140,7 +245,21 @@ add(_Ref, _Ix, _Incr) -> add_get(_Ref, _Ix, _Incr) -> erlang:nif_error(undef). --doc "Subtract `Decr` from atomic.". +-doc """ +Subtract `Decr` from atomic. + +## Examples + +```erlang +1> Ref = atomics:new(1, []). +2> atomics:put(Ref, 1, 10). +ok +3> atomics:sub(Ref, 1, 3). +ok +4> atomics:get(Ref, 1). +7 +``` +""". -doc(#{since => <<"OTP 21.2">>}). -spec sub(Ref, Ix, Decr) -> ok when Ref :: atomics_ref(), @@ -154,7 +273,21 @@ sub(Ref, Ix, Decr) -> error_with_info(Error, [Ref, Ix, Decr]) end. --doc "Atomically subtract `Decr` from atomic and return the result.". +-doc """ +Atomically subtract `Decr` from atomic and return the result. + +## Examples + +```erlang +1> Ref = atomics:new(1, []). +2> atomics:put(Ref, 1, 20). +ok +3> atomics:sub_get(Ref, 1, 5). +15 +4> atomics:sub_get(Ref, 1, 10). +5 +``` +""". -doc(#{since => <<"OTP 21.2">>}). -spec sub_get(Ref, Ix, Decr) -> integer() when Ref :: atomics_ref(), @@ -170,6 +303,18 @@ sub_get(Ref, Ix, Decr) -> -doc """ Atomically replace the value of the atomic with `Desired` and return the previous value. + +## Examples + +```erlang +1> Ref = atomics:new(1, []). +2> atomics:put(Ref, 1, 42). +ok +3> atomics:exchange(Ref, 1, 100). +42 +4> atomics:get(Ref, 1). +100 +``` """. -doc(#{since => <<"OTP 21.2">>}). -spec exchange(Ref, Ix, Desired) -> integer() when @@ -185,6 +330,22 @@ atomic to `Desired`. Return `ok` if `Desired` was written. Return the actual atomic value if not equal to `Expected`. + +## Examples + +```erlang +1> Ref = atomics:new(1, []). +2> atomics:put(Ref, 1, 42). +ok +3> atomics:compare_exchange(Ref, 1, 42, 100). +ok +4> atomics:get(Ref, 1). +100 +5> atomics:compare_exchange(Ref, 1, 42, 200). +100 +6> atomics:get(Ref, 1). +100 +``` """. -doc(#{since => <<"OTP 21.2">>}). -spec compare_exchange(Ref, Ix, Expected, Desired) -> ok | integer() when @@ -204,6 +365,14 @@ The map has the following keys: - **`max`** - The highest possible value an atomic in this array can hold. - **`min`** - The lowest possible value an atomic in this array can hold. - **`memory`** - Approximate memory consumption for the array in bytes. + +## Examples + +```erlang +1> Ref = atomics:new(10, []). +2> atomics:info(Ref). +#{size => 10, max => ..., min => ..., memory => ...} +``` """. -doc(#{since => <<"OTP 21.2">>}). -spec info(Ref) -> Info when diff --git a/erts/preloaded/src/counters.erl b/erts/preloaded/src/counters.erl index 06c77fc89dd5..f432026bc23f 100644 --- a/erts/preloaded/src/counters.erl +++ b/erts/preloaded/src/counters.erl @@ -43,6 +43,39 @@ organized into arrays with the following semantics: inconsistencies. See `new/2`. - Indexes into counter arrays are one-based. A counter array of size N contains N counters with index from 1 to N. + +## Examples + +```erlang +1> Ref = counters:new(3, []). +2> counters:add(Ref, 1, 10). +ok +3> counters:get(Ref, 1). +10 +4> counters:sub(Ref, 1, 3). +ok +5> counters:get(Ref, 1). +7 +6> counters:put(Ref, 1, 42). +ok +7> counters:get(Ref, 1). +42 +8> counters:info(Ref). +#{size => 3, memory => ...} +``` + +Counters can be updated concurrently from multiple processes: + +```erlang +1> Ref = counters:new(1, []). +2> Self = self(). +3> N = 1000. +1000 +4> Pids = [spawn(fun() -> counters:add(Ref, 1, 1), Self ! self() end) || _ <- lists:seq(1, N)]. +5> [receive Pid -> ok end || Pid <- Pids]. +6> counters:get(Ref, 1). +1000 +``` """. -moduledoc(#{since => "OTP 21.2"}). @@ -91,6 +124,19 @@ Argument `Opts` is a list of the following possible options: Counters are not tied to the current process and are automatically garbage collected when they are no longer referenced. + +## Examples + +```erlang +1> counters:new(5, []). +2> counters:new(5, [write_concurrency]). +3> counters:new(5, [atomics]). +4> counters:new(5, [atomics, write_concurrency]). +** exception error: bad argument + in function counters:new/2 + called as counters:new(5,[atomics,write_concurrency]) + *** argument 2: invalid option in list +``` """. -doc(#{since => <<"OTP 21.2">>}). -spec new(Size, Opts) -> counters_ref() when @@ -119,7 +165,21 @@ new(Size, Options) -> end. --doc "Read counter value.". +-doc """ +Read counter value. + +## Examples + +```erlang +1> Ref = counters:new(2, []). +2> counters:get(Ref, 1). +0 +3> counters:add(Ref, 1, 100). +ok +4> counters:get(Ref, 1). +100 +``` +""". -doc(#{since => <<"OTP 21.2">>}). -spec get(Ref, Ix) -> integer() when Ref :: counters_ref(), @@ -140,7 +200,36 @@ get(Ref, Ix) -> end. --doc "Add `Incr` to counter at index `Ix`.". +-doc """ +Add `Incr` to counter at index `Ix`. + +## Examples + +```erlang +1> Ref = counters:new(1, []). +2> counters:add(Ref, 1, 5). +ok +3> counters:add(Ref, 1, 3). +ok +4> counters:get(Ref, 1). +8 +5> counters:add(Ref, 1, -10). +ok +6> counters:get(Ref, 1). +-2 +7> counters:add(Ref, 1, 1 bsl 63 - 1). %% Add max positive signed 64 bit integer. +ok +8> counters:get(Ref, 1). +9223372036854775805 +9> counters:add(Ref, 1, 4). %% Add 4 to cause overflow. +ok +10> counters:get(Ref, 1). %% Counter wraps around at overflow. +-9223372036854775807 +11> counters:add(Ref, 1, 1 bsl 64). +** exception error: bad argument + in function counters:add/3 +``` +""". -doc(#{since => <<"OTP 21.2">>}). -spec add(Ref, Ix, Incr) -> ok when Ref :: counters_ref(), @@ -162,7 +251,25 @@ add(Ref, Ix, Incr) -> end. --doc "Subtract `Decr` from counter at index `Ix`.". +-doc """ +Subtract `Decr` from counter at index `Ix`. + +## Examples + +```erlang +1> Ref = counters:new(1, []). +2> counters:put(Ref, 1, 10). +ok +3> counters:sub(Ref, 1, 3). +ok +4> counters:get(Ref, 1). +7 +5> counters:sub(Ref, 1, 7). +ok +6> counters:get(Ref, 1). +0 +``` +""". -doc(#{since => <<"OTP 21.2">>}). -spec sub(Ref, Ix, Decr) -> ok when Ref :: counters_ref(), @@ -193,8 +300,22 @@ Write `Value` to counter at index `Ix`. > Despite its name, the `write_concurrency` optimization does not improve `put`. > A call to `put` is a relatively heavy operation compared to the very > lightweight and scalable [`add`](`add/3`) and [`sub`](`sub/3`). The cost for a -> `put` with `write_concurrency` is like a [`get` ](`get/2`)plus a `put` without +> `put` with `write_concurrency` is like a [`get` ](`get/2`) plus a `put` without > `write_concurrency`. + +## Examples + +```erlang +1> Ref = counters:new(1, []). +2> counters:put(Ref, 1, 42). +ok +3> counters:get(Ref, 1). +42 +4> counters:put(Ref, 1, -10). +ok +5> counters:get(Ref, 1). +-10 +``` """. -doc(#{since => <<"OTP 21.2">>}). -spec put(Ref, Ix, Value) -> ok when @@ -224,6 +345,14 @@ The map has the following keys (at least): - **`size`** - The number of counters in the array. - **`memory`** - Approximate memory consumption for the array in bytes. + +## Examples + +```erlang +1> Ref = counters:new(10, []). +2> counters:info(Ref). +#{size => 10, memory => ...} +``` """. -doc(#{since => <<"OTP 21.2">>}). -spec info(Ref) -> Info when diff --git a/erts/preloaded/src/persistent_term.erl b/erts/preloaded/src/persistent_term.erl index e886c8498474..a8aadb727588 100644 --- a/erts/preloaded/src/persistent_term.erl +++ b/erts/preloaded/src/persistent_term.erl @@ -83,7 +83,7 @@ Here is an example how the reserved virtual address space for literals can be raised to 2 GB (2048 MB): ```text - erl +MIscs 2048 +$ erl +MIscs 2048 ``` ## Best Practices for Using Persistent Terms @@ -124,17 +124,35 @@ the system less responsive N times longer than deleting a single persistent term. Therefore, terms that are to be updated at the same time should be collected into a larger term, for example, a map or a tuple. -## Example +## Examples The following example shows how lock contention for ETS tables can be minimized by having one ETS table for each scheduler. The table identifiers for the ETS tables are stored as a single persistent term: ```erlang - %% There is one ETS table for each scheduler. +-module(scheduler_ets). +-export([init/0, update_counter/2]). + +init() -> + Tids = list_to_tuple( + [ets:new(table, [public]) || + _ <- lists:seq(1, erlang:system_info(schedulers))]), + persistent_term:put(?MODULE, Tids). + +update_counter(Key, Incr) -> Sid = erlang:system_info(scheduler_id), Tid = element(Sid, persistent_term:get(?MODULE)), - ets:update_counter(Tid, Key, 1). + ets:update_counter(Tid, Key, Incr, {Key, 0}). +``` + +```erlang +1> scheduler_ets:init(). +ok +2> scheduler_ets:update_counter(my_counter, 1). +1 +3> scheduler_ets:update_counter(my_counter, 5). +6 ``` """. -moduledoc(#{since => "OTP 21.2"}). @@ -155,6 +173,17 @@ and `false` if there was no persistent term associated with the key. If there existed a previous persistent term associated with key `Key`, a global GC has been initiated when [`erase/1`](`erase/1`) returns. See [Description](`m:persistent_term`). + +## Examples + +```erlang +1> persistent_term:put(my_key, hello). +ok +2> persistent_term:erase(my_key). +true +3> persistent_term:erase(my_key). +false +``` """. -doc(#{since => <<"OTP 21.2">>}). -spec erase(Key) -> Result when @@ -168,6 +197,20 @@ Retrieve the keys and values for all persistent terms. The keys will be copied to the heap for the process calling `get/0`, but the values will not. + +## Examples + +```erlang +1> persistent_term:put(foo, 1). +ok +2> persistent_term:put(bar, 2). +ok +3> List = persistent_term:get(). +4> {foo, 1} = lists:keyfind(foo, 1, List). +{foo,1} +5> {bar, 2} = lists:keyfind(bar, 1, List). +{bar,2} +``` """. -doc(#{since => <<"OTP 21.2">>}). -spec get() -> List when @@ -187,6 +230,22 @@ the key `Key`. If the calling process holds on to the value of the persistent term and the persistent term is deleted in the future, the term will be copied to the process. + +## Examples + +```erlang +1> persistent_term:put(my_key, #{data => [1, 2, 3]}). +ok +2> persistent_term:get(my_key). +#{data => [1,2,3]} +3> persistent_term:erase(my_key). +true +4> persistent_term:get(my_key). +** exception error: bad argument + in function persistent_term:get/1 + called as persistent_term:get(my_key) + *** argument 1: no persistent term stored with this key +``` """. -doc(#{since => <<"OTP 21.2">>}). -spec get(Key) -> Value when @@ -206,6 +265,17 @@ This function returns `Default` if no term has been stored with the key `Key`. If the calling process holds on to the value of the persistent term and the persistent term is deleted in the future, the term will be copied to the process. + +## Examples + +```erlang +1> persistent_term:put(my_key, hello). +ok +2> persistent_term:get(my_key, default). +hello +3> persistent_term:get(nonexistent_key, default). +default +``` """. -doc(#{since => <<"OTP 21.3">>}). -spec get(Key, Default) -> Value when @@ -224,6 +294,13 @@ The map has the following keys: - **`memory`** - The total amount of memory (measured in bytes) used by all persistent terms. + +## Examples + +```erlang +1> persistent_term:info(). +#{count => ..., memory => ...} +``` """. -doc(#{since => <<"OTP 21.2">>}). -spec info() -> Info when @@ -243,6 +320,15 @@ If the value `Value` is equal to the value previously stored for the key, If there existed a previous persistent term associated with key `Key`, a global GC has been initiated when [`put/2`](`put/2`) returns. See [Description](`m:persistent_term`). + +## Examples + +```erlang +1> persistent_term:put({my_app, config}, #{timeout => 5000, retries => 3}). +ok +2> persistent_term:get({my_app, config}). +#{retries => 3,timeout => 5000} +``` """. -doc(#{since => <<"OTP 21.2">>}). -spec put(Key, Value) -> 'ok' when @@ -260,6 +346,19 @@ If the value `Value` is equal to the value previously stored for the key, If there existed a previous persistent term associated with key `Key`, the function fails with a `badarg` exception. + +## Examples + +```erlang +1> persistent_term:put_new(my_key, first). +ok +2> persistent_term:put_new(my_key, second). +** exception error: bad argument + in function persistent_term:put_new/2 + called as persistent_term:put_new(my_key,second) +3> persistent_term:get(my_key). +first +``` """. -doc(#{since => <<"OTP 28.4">>}). -spec put_new(Key, Value) -> 'ok' when diff --git a/lib/common_test/src/ct_doctest.erl b/lib/common_test/src/ct_doctest.erl index da30c21201eb..e7645832647a 100644 --- a/lib/common_test/src/ct_doctest.erl +++ b/lib/common_test/src/ct_doctest.erl @@ -313,7 +313,14 @@ Options for doctest execution. """. -type options() :: [{parser, fun((unicode:unicode_binary()) -> [unicode:unicode_binary()] | {error, term()}) } | {skipped_blocks, non_neg_integer() | false} | - {verbose, boolean()}]. + {verbose, boolean()} | + {compile_options, [compile:option()]}]. + +-record(options, + { parser = fun parse_markdown_builtin/1, %:: fun((unicode:unicode_binary()) -> [unicode:unicode_binary()] | {error, term()}), + skipped_blocks = false :: non_neg_integer() | false, + verbose = false :: boolean(), + compile_options = [] :: [compile:option()] }). -doc #{equiv => module(Module, [])}. -spec module(module()) -> @@ -344,21 +351,19 @@ Use `Bindings` to provide prebound variables for a specific doc entry. Use See `t:options/0` for available options. """. --spec module(module(), Bindings, options()) -> +-spec module(Module :: module(), Bindings, Options :: options()) -> ok | {comment, string()} | {error, term()} | no_return() when KFA :: {Kind :: function | type | callback, atom(), arity()}, Bindings :: [{KFA | moduledoc, erl_eval:binding_struct()}]. -module(Module, Bindings, Options) -> - HasParserKey = proplists:is_defined(parser, Options), - ParserFun = options_parser(Options), - ExpectedSkipped = options_skipped_blocks(Options), - Verbose = options_verbose(Options), +module(Module, Bindings, OptionsList) -> + Options = options(OptionsList), + HasParserKey = proplists:is_defined(parser, OptionsList), case code:get_doc(Module) of {ok, #docs_v1{ format = ~"text/markdown" } = Docs} when not HasParserKey -> - run_module_docs(Docs, Bindings, ParserFun, ExpectedSkipped, Verbose); + run_module_docs(Docs, Bindings, Options); {ok, #docs_v1{} = Docs} when HasParserKey -> - run_module_docs(Docs, Bindings, ParserFun, ExpectedSkipped, Verbose); + run_module_docs(Docs, Bindings, Options); {ok, _} -> {error, unsupported_format}; Else -> @@ -392,19 +397,17 @@ code blocks to be tested. See `t:options/0` for available options. """. --spec file(file:filename(), Bindings :: [{atom(), term()}], options()) -> +-spec file(File :: file:filename(), Bindings :: [{atom(), term()}], Options :: options()) -> ok | {comment, string()} | {error, term()} | no_return(). -file(File, Bindings, Options) -> - ParserFun = options_parser(Options), - ExpectedSkipped = options_skipped_blocks(Options), - Verbose = options_verbose(Options), +file(File, Bindings, OptionsList) -> + Options = options(OptionsList), case file:read_file(File) of {ok, Content} -> try - Blocks = inspect(parse(Content, ParserFun)), + Blocks = inspect(parse(Content, Options#options.parser)), {_RunResult, Skipped} = run_blocks(Blocks, Bindings, - {file, File}, Verbose), - ensure_skipped_blocks(ExpectedSkipped, Skipped), + {file, File}, Options), + ensure_skipped_blocks(Options#options.skipped_blocks, Skipped), ok catch throw:{error, Error} -> @@ -419,11 +422,11 @@ file(File, Bindings, Options) -> end. run_module_docs(#docs_v1{ docs = Docs, module_doc = MD }, - Bindings, ParserFun, ExpectedSkipped, Verbose) -> - MDRes = parse_and_run(moduledoc, MD, Bindings, ParserFun, Verbose), + Bindings, Options) -> + MDRes = parse_and_run(moduledoc, MD, Bindings, Options), Res = lists:append( - [parse_and_run(KFA, EntryDocs, Bindings, ParserFun, Verbose) || + [parse_and_run(KFA, EntryDocs, Bindings, Options) || {KFA, _Anno, _Sig, EntryDocs, _Meta} <- Docs, is_map(EntryDocs)]), Errors = @@ -433,10 +436,10 @@ run_module_docs(#docs_v1{ docs = Docs, module_doc = MD }, case length(Errors) of 0 -> Skipped = lists:sum([Count || {_, _, Count} <- MDRes ++ Res]), - verbose_log(Verbose, + verbose_log(Options, "module complete; total skipped blocks: ~p (expected ~p)", - [Skipped, ExpectedSkipped]), - ensure_skipped_blocks(ExpectedSkipped, Skipped), + [Skipped, Options#options.skipped_blocks]), + ensure_skipped_blocks(Options#options.skipped_blocks, Skipped), NoTests = lists:sort([io_lib:format(" ~p/~p\n", [F,A]) || {{function,F,A},[],_} <- Res]), case length(NoTests) of @@ -475,17 +478,17 @@ format_error_context(#{ message := Message, context := Context }) -> format_error_context(#{ message := Message }) -> io_lib:format("~ts~n", [string:trim(Message)]). -parse_and_run(_, hidden, _, _, _) -> []; -parse_and_run(_, none, _, _, _) -> []; -parse_and_run(KFA, #{} = Ds, Bindings, ParserFun, Verbose) -> - [do_parse_and_run(KFA, D, Bindings, ParserFun, Verbose) || _ := D <- Ds]. +parse_and_run(_, hidden, _, _) -> []; +parse_and_run(_, none, _, _) -> []; +parse_and_run(KFA, #{} = Ds, Bindings, Options) -> + [do_parse_and_run(KFA, D, Bindings, Options) || _ := D <- Ds]. -do_parse_and_run(KFA, Docs, Bindings, ParserFun, Verbose) -> +do_parse_and_run(KFA, Docs, Bindings, Options) -> try InitialBindings = proplists:get_value(KFA, Bindings, []), - Blocks = inspect(parse(Docs, ParserFun)), + Blocks = inspect(parse(Docs, Options#options.parser)), {RunResult, Skipped} = run_blocks(Blocks, InitialBindings, - {module, KFA}, Verbose), + {module, KFA}, Options), {KFA, RunResult, Skipped} catch throw:{error,_}=Error -> @@ -495,42 +498,42 @@ do_parse_and_run(KFA, Docs, Bindings, ParserFun, Verbose) -> erlang:raise(C, R, ST) end. -run_blocks(Blocks, Bindings, Context, Verbose) -> +run_blocks(Blocks, Bindings, Context, Options) -> {_Index, Result} = lists:foldl(fun(Test, {Index, {Acc, Skipped}}) -> {Result0, NewSkipped} = test_block(Test, Bindings, - Context, Index, Skipped, Verbose), + Context, Index, Skipped, Options), {Index + 1, {Acc ++ Result0, NewSkipped}} end, {1, {[], 0}}, Blocks), Result. -test_block(Code, Bindings, Context, Index, Skipped, Verbose) when is_binary(Code) -> +test_block(Code, Bindings, Context, Index, Skipped, Options) when is_binary(Code) -> ContextLabel = context_label(Context), FirstLines = first_lines(Code), - verbose_log(Verbose, "running block ~p in ~ts:~n~ts", + verbose_log(Options, "running block ~p in ~ts:~n~ts", [Index, ContextLabel, Code]), - try run_test(Code, Bindings, Verbose) of + try run_test(Code, Bindings, Options) of [] -> - verbose_log(Verbose, "skipped block ~p in ~ts (no runnable prompt, ~p skipped): ~ts", + verbose_log(Options, "skipped block ~p in ~ts (no runnable prompt, ~p skipped): ~ts", [Index, ContextLabel, Skipped + 1, FirstLines]), {[], Skipped + 1}; Result -> - verbose_log(Verbose, "passed block ~p in ~ts", [Index, ContextLabel]), + verbose_log(Options, "passed block ~p in ~ts", [Index, ContextLabel]), {Result, Skipped} catch throw:{error, ErrorContext} = Error -> - verbose_log(Verbose, + verbose_log(Options, "failed block ~p in ~ts:~n~ts~n", [Index, ContextLabel, format_error_context(ErrorContext)]), throw(Error); C:R:ST -> - verbose_log(Verbose, + verbose_log(Options, "failed block ~p in ~ts with ~p:~tp~nblock snippet:~n~ts", [Index, ContextLabel, C, R, Code]), erlang:raise(C, R, ST) end; -test_block(Other, _Bindings, _Context, _Index, _Skipped, _Verbose) -> +test_block(Other, _Bindings, _Context, _Index, _Skipped, _Options) -> throw({error, {invalid_code_block, Other}}). context_label({module, moduledoc}) -> @@ -546,12 +549,12 @@ first_lines(Code) -> Lines = string:split(Code, "\n", all), lists:join($\n, lists:sublist(Lines, 5)). -verbose_log(false, _Fmt, _Args) -> - ok; -verbose_log(true, Fmt, Args) -> +verbose_log(#options{ verbose = true }, Fmt, Args) -> Str = io_lib:format("ct_doctest(verbose): " ++ Fmt, Args), [First | Rest] = string:split(string:trim(Str), "\n", all), - io:put_chars([First, [["\n ", Line] || Line <- Rest], "\n"]). + io:put_chars([First, [["\n ", Line] || Line <- Rest], "\n"]); +verbose_log(_, _, _) -> + ok. parse(Content, ParserFun) -> validate_code_blocks(run_parser(ParserFun, Content)). @@ -576,9 +579,19 @@ validate_code_block(Block) when is_binary(Block) -> validate_code_block(Other) -> throw({error, #{ message => io_lib:format("Invalid code block: ~p.", [Other]) }}). -options_parser(Options) -> - proplists:get_value(parser, Options, - fun parse_markdown_builtin/1). +options(OptionsList) -> + lists:foldl(fun + (parser, Acc) -> + Acc#options{ parser = proplists:get_value(parser, OptionsList) }; + (skipped_blocks, Acc) -> + Acc#options{ skipped_blocks = proplists:get_value(skipped_blocks, 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)). parse_markdown_builtin(Markdown) -> extract_erlang_code_blocks(inspect(shell_docs_markdown:parse_md(Markdown))). @@ -599,12 +612,6 @@ extract_erlang_code_blocks({_Tag, _Attrs, Content}) -> extract_erlang_code_blocks(_Other) -> []. -options_skipped_blocks(Options) -> - proplists:get_value(skipped_blocks, Options, false). - -options_verbose(Options) -> - proplists:get_value(verbose, Options, false) =:= true. - ensure_skipped_blocks(false, _Actual) -> ok; ensure_skipped_blocks(Expected, Actual) when is_integer(Expected), Expected >= 0 -> @@ -618,7 +625,7 @@ ensure_skipped_blocks(Expected, Actual) when is_integer(Expected), Expected >= 0 -define(RE_CAPTURE, ~B"(?:(?'line_number'[0-9]+)(?'prefix'>\s)|(?'prefix'\-module\())?(?'content'.*)"). -define(RE_OPTIONS, [{capture, [line_number, prefix, content], binary}, dupnames, unicode]). -run_test(Code, InitialBindings, Verbose) -> +run_test(Code, InitialBindings, Options) -> Lines = string:split(Code, "\n", all), CollapsedComments = [re:replace(Line, ~B"^\s*%.*$", <<"">>, [global, unicode]) || Line <- Lines], case lists:search(fun(Line) -> @@ -633,60 +640,31 @@ run_test(Code, InitialBindings, Verbose) -> Tests = inspect(parse_tests(ReLines, [], 1)), check_prompt_numbers(Tests), _ = lists:foldl(fun(Test, Bindings) -> - try run_tests(Test, Bindings, Verbose) + try run_tests(Test, Bindings, Options) catch throw:{error, Error} -> throw({error, Error#{ test => Test}}) 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). @@ -726,19 +704,19 @@ parse_match([{match, [<<>>, <<>>, <<" ", _/binary>> = More]} | T], Acc) -> parse_match(Rest, Acc) -> {Acc, Rest}. -run_tests({test, _Index, Test0, Match0}, Bindings, Verbose) -> +run_tests({test, _Index, Test0, Match0}, Bindings, Options) -> Test1 = unicode:characters_to_list(Test0), Test = string:trim(string:trim(Test1), trailing, "."), case Match0 of [<<"** ", _/binary>> | _] -> Match = unicode:characters_to_list(Match0), - run_failing(Test, Match, Bindings, Verbose); + run_failing(Test, Match, Bindings, Options); _ -> - run_successful(Test, Match0, Bindings, Verbose) + run_successful(Test, Match0, Bindings, Options) end. -run_successful(Test, Match, Bindings, Verbose) -> - verbose_log(Verbose, "Running: ~ts = ~ts", [Match, Test]), +run_successful(Test, Match, Bindings, Options) -> + verbose_log(Options, "Running: ~ts = ~ts", [Match, Test]), Ast = parse_exprs(Test, Match), try {value, _Res, NewBindings} = inspect(erl_eval:exprs(Ast, Bindings)), @@ -747,8 +725,8 @@ run_successful(Test, Match, Bindings, Verbose) -> throw({error,#{ message => format_exception(C, R, ST) }}) end. -run_failing(Test, Match, Bindings, Verbose) -> - verbose_log(Verbose, "Running: ~ts", [Test]), +run_failing(Test, Match, Bindings, Options) -> + verbose_log(Options, "Running: ~ts", [Test]), Ast = parse_exprs(Test, "_"), try inspect(erl_eval:exprs(Ast, Bindings)) of {value, Res, _} -> diff --git a/lib/common_test/test/ct_doctest_SUITE.erl b/lib/common_test/test/ct_doctest_SUITE.erl index 66cde049df72..0d8926865525 100644 --- a/lib/common_test/test/ct_doctest_SUITE.erl +++ b/lib/common_test/test/ct_doctest_SUITE.erl @@ -28,7 +28,8 @@ -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, type_and_callback_docs/1, verbose_option/1, + external_parser/1, module/1, module_preprocessor/1, + type_and_callback_docs/1, verbose_option/1, skipped_blocks_option/1, integration_smoke/1]). @@ -45,6 +46,7 @@ all() -> file_support, external_parser, module, + module_preprocessor, type_and_callback_docs, verbose_option, skipped_blocks_option, @@ -128,12 +130,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}, diff --git a/lib/common_test/test/ct_doctest_SUITE_data/ct_doctest_module_preproc_mod.erl b/lib/common_test/test/ct_doctest_SUITE_data/ct_doctest_module_preproc_mod.erl new file mode 100644 index 000000000000..7bb2ce20dd60 --- /dev/null +++ b/lib/common_test/test/ct_doctest_SUITE_data/ct_doctest_module_preproc_mod.erl @@ -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. diff --git a/lib/compiler/src/compile.erl b/lib/compiler/src/compile.erl index f532df2329ad..49051dd1c5b2 100644 --- a/lib/compiler/src/compile.erl +++ b/lib/compiler/src/compile.erl @@ -205,6 +205,7 @@ source code. %% High-level interface. -export([file/1,file/2,noenv_file/2,format_error/1]). -export([forms/1,forms/2,noenv_forms/2]). +-export([string/1,string/2,noenv_string/2]). -export([output_generated/1,noenv_output_generated/1]). -export([options/0]). -export([env_compiler_options/0]). @@ -931,15 +932,12 @@ file(File, Opts) when is_list(Opts) -> file(File, Opt) -> file(File, [Opt|?DEFAULT_OPTIONS]). --doc """ -Is the same as -[`forms(Forms, [verbose,report_errors,report_warnings])`](`forms/2`). -""". +-doc #{ equiv => forms(Forms, ?DEFAULT_OPTIONS) }. -spec forms(forms()) -> CompRet :: comp_ret(). forms(Forms) -> forms(Forms, ?DEFAULT_OPTIONS). --doc """ +-doc """" Analogous to [`file/1`](`file/1`), but takes a list of forms (in either Erlang abstract or Core Erlang format representation) as first argument. @@ -947,7 +945,38 @@ Option `binary` is implicit, that is, no object code file is produced. For options that normally produce a listing file, such as 'E', the internal format for that compiler pass (an Erlang term, usually not a binary) is returned instead of a binary. -""". + +## Examples + +```erlang +1> Program = """ + -module(test). + -export([foo/0]). + foo() -> bar. + """. +2> ParseForms = + fun + Parse("", Loc) -> + []; + Parse(String, Loc) -> + case erl_scan:tokens("", String, Loc) of + {done, {ok, Tokens, NewLoc}, Cont} -> + {ok, Form} = erl_parse:parse_form(Tokens), + [Form | Parse(Cont, NewLoc)] + end + end. +3> Forms = ParseForms(Program ++ "\n", {1,1}). +[{attribute,{1,2},module,test}, + {attribute,{2,2},export,[{foo,0}]}, + {function,{3,1}, + foo,0, + [{clause,{3,1},[],[],[{atom,{3,10},bar}]}]}] +4> compile:forms(Forms). +{ok,test, + <<70,79,82,49,0,0,1,196,66,69,65,77,65,116,85,56,0,0,0, + 52,255,255,255,250,64,116,...>>} +``` +"""". -spec forms(Forms :: forms(), Options :: [option()] | option()) -> CompRet :: comp_ret(). forms(Forms, Opts) when is_list(Opts) -> @@ -999,6 +1028,57 @@ noenv_forms(Forms, Opts) when is_list(Opts) -> noenv_forms(Forms, Opt) when is_atom(Opt) -> noenv_forms(Forms, [Opt|?DEFAULT_OPTIONS]). +-doc #{ equiv => string(String, ?DEFAULT_OPTIONS) }. +-doc #{ since => <<"OTP @OTP-1234567@">> }. +-spec string(String :: unicode:chardata()) -> CompRet :: comp_ret(). +string(String) -> string(String, ?DEFAULT_OPTIONS). + +-doc """ +Compiles an Erlang source code string. + +Analogous to [`file/1`](`file/1`), but takes a `t:unicode:chardata/0` containing +Erlang source code as first argument. The source code is run through the +Erlang preprocessor (`epp`) just as when compiling a file, so directives +such as `-include`, `-define`, and `-ifdef` are supported. + +Option `binary` is implicit, that is, no object code file is +produced. For options that normally produce a listing file, such as +'E', the internal format for that compiler pass (an Erlang term, +usually not a binary) is returned instead of a binary. + +The `{include_path_open, Fun}` option can be used to provide a custom function +for opening include files (see `epp:open/1`), allowing compilation +entirely in memory without touching the file system. + +## Examples + +```erlang +1> {ok, foo, Bin} = compile:string("-module(foo). -export([bar/0]). bar() -> ok."). +{ok,foo,<<...>>} +2> code:load_binary(foo, "foo.erl", Bin). +{module, foo} +3> foo:bar(). +ok +``` +""". +-doc #{ since => <<"OTP @OTP-1234567@">> }. +-spec string(String :: unicode:chardata(), Options :: [option()] | option()) -> + CompRet :: comp_ret(). +string(String, Opts) when is_list(Opts) -> + do_compile({string,String}, [binary|Opts++env_default_opts()]); +string(String, Opt) when is_atom(Opt) -> + string(String, [Opt|?DEFAULT_OPTIONS]). + +-doc #{ equiv => noenv_string(String, []) }. +-doc #{ since => <<"OTP @OTP-1234567@">> }. +-spec noenv_string(String :: unicode:chardata(), Options :: [option()] | option()) -> + comp_ret(). + +noenv_string(String, Opts) when is_list(Opts) -> + do_compile({string,String}, [binary|Opts]); +noenv_string(String, Opt) when is_atom(Opt) -> + noenv_string(String, [Opt|?DEFAULT_OPTIONS]). + -doc """ Works like `output_generated/1`, except that the environment variable `ERL_COMPILER_OPTIONS` is not consulted. @@ -1250,6 +1330,14 @@ internal({forms,Forms}, Opts0) -> strip_columns(Forms) end, internal_comp(Ps, NewForms, Source, "", Compile); +internal({string,String}, Opts0) -> + Bin = unicode:characters_to_binary(String), + {ok, Fd} = file:open(Bin, [ram, read, binary, cooked]), + try + do_parse_string(Fd, Opts0) + after + file:close(Fd) + end; internal({file,File}, Opts) -> {Ext,Ps} = passes(file, Opts), Compile = build_compile(Opts), @@ -1953,6 +2041,71 @@ parse_module(_Code, St) -> Ret end. +do_parse_string(Fd, Opts0) -> + StartLocation = case with_columns(Opts0) of + true -> + {1,1}; + false -> + 1 + end, + case erl_features:init_parse_state(Opts0, fun erl_scan:f_reserved_word/1) of + {ok, {Features, ResWordFun}} -> + PathOpenOpt = case proplists:get_value(include_path_open, Opts0) of + undefined -> []; + PathOpenFun -> [{include_path_open, PathOpenFun}] + end, + Name = proplists:get_value(source, Opts0, "string"), + EppOpts = [{fd, Fd}, + {name, Name}, + {includes, ["." | inc_paths(Opts0)]}, + {macros, pre_defs(Opts0)}, + {default_encoding, utf8}, + {location, StartLocation}, + {reserved_word_fun, ResWordFun}, + {features, Features}, + extra | + PathOpenOpt ++ + case member(check_ssa, Opts0) of + true -> + [{compiler_internal, [ssa_checks]}]; + false -> + [] + end], + {ok, Epp, Extra0} = epp:open(EppOpts), + try + Forms0 = epp:parse_file(Epp), + Epp ! {get_features, self()}, + UsedFtrs = receive {features, X} -> X end, + Extra = [{features, UsedFtrs} | Extra0], + Encoding = proplists:get_value(encoding, Extra), + {_, Ps} = passes(forms, Opts0), + Source = proplists:get_value(source, Opts0, source_from_forms(Forms0)), + Opts1 = proplists:delete(source, Opts0), + Compile0 = build_compile(Opts1), + St0 = metadata_add_features(UsedFtrs, Compile0), + Opts2 = [{features, UsedFtrs} | St0#compile.options], + St1 = St0#compile{encoding=Encoding, options=Opts2}, + Forms = case with_columns(Opts2 ++ compile_options(Forms0)) of + true -> + Forms0; + false -> + strip_columns(Forms0) + end, + internal_comp(Ps, Forms, Source, "", St1) + after + epp:close(Epp) + end; + {error, {Mod, Reason}} -> + Source = proplists:get_value(source, Opts0, "string"), + Es = [{Source, [{none, Mod, Reason}]}], + {error, {errors, Es, []}} + end. + +source_from_forms([{attribute,_,module,Mod}|_]) -> + atom_to_list(Mod) ++ ".erl"; +source_from_forms([_|T]) -> source_from_forms(T); +source_from_forms([]) -> "string". + deterministic_filename(#compile{ifile=File,options=Opts}) -> SourceName0 = proplists:get_value(source, Opts, File), case member(deterministic, Opts) of diff --git a/lib/compiler/test/compile_SUITE.erl b/lib/compiler/test/compile_SUITE.erl index 7e448dd8ef20..8bd3c01e56f5 100644 --- a/lib/compiler/test/compile_SUITE.erl +++ b/lib/compiler/test/compile_SUITE.erl @@ -26,11 +26,11 @@ -include_lib("common_test/include/ct.hrl"). -include_lib("stdlib/include/erl_compile.hrl"). --export([all/0, suite/0,groups/0,init_per_suite/1, end_per_suite/1, +-export([all/0, suite/0,groups/0,init_per_suite/1, end_per_suite/1, init_per_group/2,end_per_group/2, app_test/1,appup_test/1,bigE_roundtrip/1, debug_info/4, custom_debug_info/1, custom_compile_info/1, - file_1/1, forms_2/1, module_mismatch/1, outdir/1, + file_1/1, forms_2/1, string_1/1, module_mismatch/1, outdir/1, binary/1, makedep/1, cond_and_ifdef/1, listings/1, listings_big/1, other_output/1, encrypted_abstr/1, strict_record/1, utf8_atoms/1, utf8_functions/1, extra_chunks/1, @@ -44,7 +44,7 @@ compile_attribute/1, message_printing/1, other_options/1, transforms/1, erl_compile_api/1, types_pp/1, bs_init_writable/1, annotations_pp/1, option_order/1, - sys_coverage/1 + sys_coverage/1, doctests/1 ]). suite() -> [{ct_hooks,[ts_install_cth]}]. @@ -55,7 +55,7 @@ suite() -> [{ct_hooks,[ts_install_cth]}]. all() -> [app_test, appup_test, bigE_roundtrip, file_1, - forms_2, module_mismatch, outdir, + forms_2, string_1, module_mismatch, outdir, binary, makedep, cond_and_ifdef, listings, listings_big, other_output, encrypted_abstr, tuple_calls, strict_record, utf8_atoms, utf8_functions, extra_chunks, @@ -67,7 +67,7 @@ all() -> deterministic_docs, compile_attribute, message_printing, other_options, transforms, erl_compile_api, types_pp, bs_init_writable, annotations_pp, - option_order, sys_coverage]. + option_order, sys_coverage, doctests]. groups() -> []. @@ -321,6 +321,169 @@ forms_compile_and_load(Code, Opts) -> false = code:purge(simple), ok. +string_1(_Config) -> + %% Basic compilation from string. + {ok,foo,BinFoo} = compile:string("-module(foo). -export([bar/0]). bar() -> hello."), + {module,foo} = code:load_binary(foo, "foo.erl", BinFoo), + hello = foo:bar(), + true = code:delete(foo), + false = code:purge(foo), + + %% Binary input. + {ok,binmod,BinMod} = compile:string(<<"-module(binmod). -export([g/0]). g() -> ok.">>), + {module,binmod} = code:load_binary(binmod, "binmod.erl", BinMod), + ok = binmod:g(), + true = code:delete(binmod), + false = code:purge(binmod), + + %% Preprocessor: macros with -define. + {ok,macmod,BinMac} = compile:string( + "-module(macmod). -export([f/0]).\n" + "-define(X, 42).\n" + "f() -> ?X.\n"), + {module,macmod} = code:load_binary(macmod, "macmod.erl", BinMac), + 42 = macmod:f(), + true = code:delete(macmod), + false = code:purge(macmod), + + %% Preprocessor: records. + {ok,recmod,BinRec} = compile:string( + "-module(recmod). -export([new/0]).\n" + "-record(point, {x = 0, y = 0}).\n" + "new() -> #point{x = 1, y = 2}.\n"), + {module,recmod} = code:load_binary(recmod, "recmod.erl", BinRec), + {point,1,2} = recmod:new(), + true = code:delete(recmod), + false = code:purge(recmod), + + %% Preprocessor: -ifdef/-endif. + {ok,ifmod,BinIf} = compile:string( + "-module(ifmod). -export([f/0]).\n" + "-ifdef(NOTDEFINED).\n" + "f() -> wrong.\n" + "-else.\n" + "f() -> right.\n" + "-endif.\n"), + {module,ifmod} = code:load_binary(ifmod, "ifmod.erl", BinIf), + right = ifmod:f(), + true = code:delete(ifmod), + false = code:purge(ifmod), + + %% Preprocessor: predefined macros via options. + {ok,defmod,BinDef} = compile:string( + "-module(defmod). -export([f/0]).\n" + "f() -> ?MY_VALUE.\n", + [{d,'MY_VALUE',99}]), + {module,defmod} = code:load_binary(defmod, "defmod.erl", BinDef), + 99 = defmod:f(), + true = code:delete(defmod), + false = code:purge(defmod), + + %% Preprocessor: ?MODULE. + {ok,modmac,BinModMac} = compile:string( + "-module(modmac). -export([name/0]).\n" + "name() -> ?MODULE.\n"), + {module,modmac} = code:load_binary(modmac, "modmac.erl", BinModMac), + modmac = modmac:name(), + true = code:delete(modmac), + false = code:purge(modmac), + + %% Source option. + {ok,srcmod,BinSrc} = compile:string( + "-module(srcmod). -export([f/0]). f() -> ok.", + [{source,"my_source.erl"}]), + {module,srcmod} = code:load_binary(srcmod, "srcmod.erl", BinSrc), + Info = srcmod:module_info(compile), + SrcInfo = proplists:get_value(source, Info), + true = lists:suffix("my_source.erl", SrcInfo), + true = code:delete(srcmod), + false = code:purge(srcmod), + + %% Syntax error returns error. + error = compile:string("-module(bad). f( ->"), + + %% Syntax error with return_errors option. + {error,[{"string",[{_,_,_}|_]}],_} = + compile:string("-module(bad). f( ->", [return_errors]), + + %% Source option affects error filenames. + {error,[{"bad.erl",[{_,_,_}|_]}],_} = + compile:string("-module(bad). f( ->", + [return_errors, {source, "bad.erl"}]), + + %% Cover: option not in a list (undocumented feature). + {ok,smod,_} = compile:string("-module(smod). -export([f/0]). f() -> ok.", binary), + + %% noenv_string/2. + {ok,nmod,_} = compile:noenv_string( + "-module(nmod). -export([f/0]). f() -> ok.", []), + + %% include_path_open: include from in-memory files. + Headers = #{ + "my_header.hrl" => + <<"-record(point, {x, y}).\n" + "-define(ORIGIN, #point{x=0, y=0}).\n">> + }, + PathOpen = fun(_Path, Name, _Modes) -> + case maps:find(Name, Headers) of + {ok, Content} -> + {ok, Fd} = file:open(Content, [ram, read, binary, cooked]), + {ok, Fd, Name}; + error -> + {error, enoent} + end + end, + {ok,incmod,BinInc} = compile:string( + "-module(incmod).\n" + "-include(\"my_header.hrl\").\n" + "-export([f/0]).\n" + "f() -> ?ORIGIN.\n", + [{include_path_open, PathOpen}]), + {module,incmod} = code:load_binary(incmod, "incmod.erl", BinInc), + {point,0,0} = incmod:f(), + true = code:delete(incmod), + false = code:purge(incmod), + + %% include_path_open: nested includes from in-memory files. + Headers2 = #{ + "types.hrl" => + <<"-type my_int() :: integer().\n">>, + "all.hrl" => + <<"-include(\"types.hrl\").\n" + "-define(DEFAULT, 0).\n">> + }, + PathOpen2 = fun(_Path, Name, _Modes) -> + case maps:find(Name, Headers2) of + {ok, Content} -> + {ok, Fd} = file:open(Content, [ram, read, binary, cooked]), + {ok, Fd, Name}; + error -> + {error, enoent} + end + end, + {ok,nestmod,BinNest} = compile:string( + "-module(nestmod).\n" + "-include(\"all.hrl\").\n" + "-export([f/0]).\n" + "-spec f() -> my_int().\n" + "f() -> ?DEFAULT.\n", + [{include_path_open, PathOpen2}]), + {module,nestmod} = code:load_binary(nestmod, "nestmod.erl", BinNest), + 0 = nestmod:f(), + true = code:delete(nestmod), + false = code:purge(nestmod), + + %% include_path_open: missing include returns error. + PathOpenNone = fun(_Path, _Name, _Modes) -> {error, enoent} end, + {error, _, _} = compile:string( + "-module(missinc).\n" + "-include(\"nonexistent.hrl\").\n" + "-export([f/0]).\n" + "f() -> ok.\n", + [{include_path_open, PathOpenNone}, return_errors]), + + ok. + module_mismatch(Config) when is_list(Config) -> DataDir = proplists:get_value(data_dir, Config), File = filename:join(DataDir, "wrong_module_name.erl"), @@ -2362,6 +2525,17 @@ sys_coverage_2(DataDir) -> ok. +doctests(_Config) -> + PathOpen = fun(Path, Filename, Modes) -> + case file:path_open(Path, Filename, Modes) of + {error, enoent} -> + {ok, FD} = file:open(~"", [ram, cooked | Modes]), + {ok, FD, filename:basename(Filename)}; + Else -> Else + end + end, + ct_doctest:module(compile, [], [{compile_options, [{include_path_open, PathOpen}]}]). + %%% %%% Utilities. %%% diff --git a/lib/kernel/src/file.erl b/lib/kernel/src/file.erl index b006a6a9156c..6add4f5b660b 100644 --- a/lib/kernel/src/file.erl +++ b/lib/kernel/src/file.erl @@ -347,6 +347,14 @@ runtime libraries of most C compilers. Returns the filename encoding mode. If it is `latin1`, the system translates no filenames. If it is `utf8`, filenames are converted back and forth to the native filename encoding (usually UTF-8, but UTF-16 on Windows). + +## Examples + +```erlang +1> Enc = file:native_name_encoding(). +2> is_atom(Enc). +true +``` """. -doc(#{since => <<"OTP R14B01">>}). -spec native_name_encoding() -> latin1 | utf8. @@ -363,6 +371,17 @@ native_name_encoding() -> -doc """ Given the error reason returned by any function in this module, returns a descriptive string of the error in English. + +## Examples + +```erlang +1> file:format_error(enoent). +"no such file or directory" +2> file:format_error(eacces). +"permission denied" +3> file:format_error(eisdir). +"illegal operation on a directory" +``` """. -spec format_error(Reason) -> Chars when Reason :: posix() | badarg | terminated | system_limit @@ -405,6 +424,12 @@ A typical error reason: - **`eacces`** - Missing read permission for one of the parents of the current directory. + +## Examples + +```erlang +1> {ok, Dir} = file:get_cwd(). +``` """. -spec get_cwd() -> {ok, Dir} | {error, Reason} when Dir :: filename(), @@ -429,6 +454,7 @@ Typical error reasons: - **`eacces`** - The drive does not exist. - **`einval`** - The format of `Drive` is invalid. + """. -spec get_cwd(Drive) -> {ok, Dir} | {error, Reason} when Drive :: string(), @@ -466,6 +492,16 @@ Typical error reasons are: > > In a future release, a bad type for argument `Dir` will probably generate an > exception. + +## Examples + +```erlang +1> {ok, Original} = file:get_cwd(). +2> ok = file:set_cwd(Dir). +3> {ok, _} = file:get_cwd(). +4> file:set_cwd(Original). +ok +``` """. -spec set_cwd(Dir) -> ok | {error, Reason} when Dir :: name() | EncodedBinary, @@ -507,6 +543,17 @@ Typical error reasons: > > In a future release, a bad type for argument `Filename` will probably generate > an exception. + +## Examples + +```erlang +1> File = filename:join(Dir, "delete_example.txt"). +2> ok = file:write_file(File, "temporary data"). +3> file:delete(File). +ok +4> file:delete(File). +{error,enoent} +``` """. -doc(#{since => <<"OTP 24.0">>}). -spec delete(Filename, Opts) -> ok | {error, Reason} when @@ -560,6 +607,20 @@ Typical error reasons: - **`enotdir`** - `Source` is a directory, but `Destination` is not. - **`exdev`** - `Source` and `Destination` are on different file systems. + +## Examples + +```erlang +1> Src = filename:join(Dir, "rename_src.txt"). +2> Dst = filename:join(Dir, "rename_dst.txt"). +3> ok = file:write_file(Src, "data"). +4> file:rename(Src, Dst). +ok +5> file:read_file(Dst). +{ok,<<"data">>} +6> file:read_file(Src). +{error,enoent} +``` """. -spec rename(Source, Destination) -> ok | {error, Reason} when Source :: name_all(), @@ -586,6 +647,16 @@ Typical error reasons: - **`enotdir`** - A component of `Dir` is not a directory. On some platforms, `enoent` is returned instead. + +## Examples + +```erlang +1> NewDir = filename:join(Dir, "my_new_dir"). +2> file:make_dir(NewDir). +ok +3> file:make_dir(NewDir). +{error,eexist} +``` """. -spec make_dir(Dir) -> ok | {error, Reason} when Dir :: name_all(), @@ -612,6 +683,15 @@ Typical error reasons: - **`einval`** - Attempt to delete the current directory. On some platforms, `eacces` is returned instead. + +## Examples + +```erlang +1> EmptyDir = filename:join(Dir, "empty_dir"). +2> ok = file:make_dir(EmptyDir). +3> file:del_dir(EmptyDir). +ok +``` """. -spec del_dir(Dir) -> ok | {error, Reason} when Dir :: name_all(), @@ -629,6 +709,18 @@ first recursively deleted. Returns: - **`{error, posix()}`** - An error occurred when accessing or deleting `File`. If some file or directory under `File` could not be deleted, `File` cannot be deleted as it is non-empty, and `{error, eexist}` is returned. + +## Examples + +```erlang +1> Parent = filename:join(Dir, "parent"). +2> ok = file:make_dir(Parent). +3> ok = file:write_file(filename:join(Parent, "a.txt"), ""). +4> ok = file:make_dir(filename:join(Parent, "sub")). +5> ok = file:write_file(filename:join([Parent, "sub", "b.txt"]), ""). +6> file:del_dir_r(Parent). +ok +``` """. -doc(#{since => <<"OTP 23.0">>}). -spec del_dir_r(File) -> ok | {error, Reason} when @@ -775,6 +867,20 @@ Typical error reasons: - **`enotdir`** - A component of the filename is not a directory. On some platforms, `enoent` is returned instead. + +## Examples + +```erlang +1> File = filename:join(Dir, "info_example.txt"). +2> ok = file:write_file(File, <<"Hello!">>). +3> {ok, Info} = file:read_file_info(File). +4> element(2, Info). +6 +5> element(3, Info). +regular +6> element(4, Info). +read_write +``` """. -doc(#{since => <<"OTP R15B">>}). -spec read_file_info(File, Opts) -> {ok, FileInfo} | {error, Reason} when @@ -833,6 +939,7 @@ If `Name` is not a symbolic link, this function returns the same result as [`read_file_info/1`](`read_file_info/1`). On platforms that do not support symbolic links, this function is always equivalent to [`read_file_info/1`](`read_file_info/1`). + """. -doc(#{since => <<"OTP R15B">>}). -spec read_link_info(Name, Opts) -> {ok, FileInfo} | {error, Reason} when @@ -870,6 +977,7 @@ Typical error reasons: - **`enoent`** - The file does not exist. - **`enotsup`** - Symbolic links are not supported on this platform. + """. -spec read_link(Name) -> {ok, Filename} | {error, Reason} when Name :: name_all(), @@ -893,6 +1001,7 @@ Typical error reasons: - **`enoent`** - The file does not exist. - **`enotsup`** - Symbolic links are not supported on this platform. + """. -doc(#{since => <<"OTP R16B">>}). -spec read_link_all(Name) -> {ok, Filename} | {error, Reason} when @@ -994,6 +1103,7 @@ Typical error reasons: - **`enotdir`** - A component of the filename is not a directory. On some platforms, `enoent` is returned instead. + """. -doc(#{since => <<"OTP R15B">>}). -spec write_file_info(Filename, FileInfo, Opts) -> ok | {error, Reason} when @@ -1031,6 +1141,18 @@ Typical error reasons: - **`{no_translation, Filename}`** - `Filename` is a `t:binary/0` with characters coded in ISO Latin-1 and the VM was started with parameter `+fnue`. + +## Examples + +```erlang +1> SubDir = filename:join(Dir, "listdir_test"). +2> ok = file:make_dir(SubDir). +3> ok = file:write_file(filename:join(SubDir, "a.txt"), ""). +4> ok = file:write_file(filename:join(SubDir, "b.txt"), ""). +5> {ok, Files} = file:list_dir(SubDir). +6> lists:sort(Files). +["a.txt","b.txt"] +``` """. -spec list_dir(Dir) -> {ok, Filenames} | {error, Reason} when Dir :: name_all(), @@ -1054,6 +1176,17 @@ Typical error reasons: parent directories. - **`enoent`** - The directory does not exist. + +## Examples + +```erlang +1> SubDir = filename:join(Dir, "listdirall_test"). +2> ok = file:make_dir(SubDir). +3> ok = file:write_file(filename:join(SubDir, "a.txt"), ""). +4> {ok, Files} = file:list_dir_all(SubDir). +5> lists:sort(Files). +["a.txt"] +``` """. -doc(#{since => <<"OTP R16B">>}). -spec list_dir_all(Dir) -> {ok, Filenames} | {error, Reason} when @@ -1092,6 +1225,17 @@ Typical error reasons: platforms, `enoent` is returned instead. - **`enomem`** - There is not enough memory for the contents of the file. + +## Examples + +```erlang +1> File = filename:join(Dir, "read_example.txt"). +2> ok = file:write_file(File, <<"Hello!">>). +3> file:read_file(File). +{ok,<<"Hello!">>} +4> file:read_file("nonexistent_file"). +{error,enoent} +``` """. -doc(#{since => <<"OTP 27.0">>}). -spec read_file(Filename, Opts) -> {ok, Binary} | {error, Reason} when @@ -1128,6 +1272,18 @@ Typical error reasons: - **`eexist`** - `New` already exists. - **`enotsup`** - Hard links are not supported on this platform. + +## Examples + +```erlang +1> File = filename:join(Dir, "hardlink_example.txt"). +2> ok = file:write_file(File, <<"data">>). +3> Link = filename:join(Dir, "hardlink"). +4> file:make_link(File, Link). +ok +5> file:read_file(Link). +{ok,<<"data">>} +``` """. -spec make_link(Existing, New) -> ok | {error, Reason} when Existing :: name_all(), @@ -1155,6 +1311,7 @@ Typical error reasons: - **`eperm`** - User does not have privileges to create symbolic links (`SeCreateSymbolicLinkPrivilege` on Windows). + """. -spec make_symlink(Existing, New) -> ok | {error, Reason} when Existing :: name_all(), @@ -1182,6 +1339,20 @@ Typical error reasons: parent directories. - **`eisdir`** - The named file is a directory. + +## Examples + +```erlang +1> File = filename:join(Dir, "write_example.txt"). +2> file:write_file(File, "Hello, world!"). +ok +3> file:read_file(File). +{ok,<<"Hello, world!">>} +4> file:write_file(File, [<<"line 1\n">>, <<"line 2\n">>]). +ok +5> file:read_file(File). +{ok,<<"line 1\nline 2\n">>} +``` """. -spec write_file(Filename, Bytes) -> ok | {error, Reason} when Filename :: name_all(), @@ -1199,6 +1370,18 @@ write_file(Name, Bin) -> Same as [`write_file/2`](`write_file/2`), but takes a third argument `Modes`, a list of possible modes, see `open/2`. The mode flags `binary` and `write` are implicit, so they are not to be used. + +## Examples + +Append to an existing file: + +```erlang +1> File = filename:join(Dir, "write_file3_example.txt"). +2> ok = file:write_file(File, <<"Hello">>). +3> ok = file:write_file(File, <<", world!">>, [append]). +4> file:read_file(File). +{ok,<<"Hello, world!">>} +``` """. -spec write_file(Filename, Bytes, Modes) -> ok | {error, Reason} when Filename :: name_all(), @@ -1408,8 +1591,15 @@ more of the following options: This option is not allowed on `raw` files. -- **`ram`** - `File` must be `t:iodata/0`. Returns an `t:fd/0`, which lets - module `file` operate on the data in-memory as if it is a file. +- **`ram`**{: #ram } - `File` must be `t:iodata/0`. Returns an `t:fd/0` (or a `t:io_server/0` + if combined with `cooked`), which lets module `file` operate on the data in-memory + as if it is a file. + +- **`cooked`** - Only meaningful together with `ram`. When specified, the + ram file is wrapped in a pid-based I/O server, making it compatible with the + `m:io` module (for example, `io:get_line/2`, `io:scan_erl_form/3`). + + Since OTP @OTP-123456789@ - **`sync`** - On platforms supporting it, enables the POSIX `O_SYNC` synchronous I/O flag or its platform-dependent equivalent (for example, @@ -1467,6 +1657,20 @@ Typical error reasons: - **`enospc`** - There is no space left on the device (if `write` access was specified). + +## Examples + +```erlang +1> File = filename:join(Dir, "open_example.txt"). +2> {ok, Fd} = file:open(File, [write]). +3> ok = file:write(Fd, "Hello, world!"). +4> ok = file:close(Fd). +5> {ok, Fd2} = file:open(File, [read, binary]). +6> file:read(Fd2, 13). +{ok,<<"Hello, world!">>} +7> file:close(Fd2). +ok +``` """. -spec open(File, Modes) -> {ok, IoDevice} | {error, Reason} when File :: Filename | iodata(), @@ -1490,7 +1694,15 @@ open(Item, ModeList) when is_list(ModeList) -> {true, false} -> raw_file_io:open(file_name(Item), ModeList); {false, true} -> - ram_file:open(Item, ModeList); + case lists:member(cooked, ModeList) of + true -> + file_io_server:start_handle( + self(), + fun(_, _) -> ram_file:open(Item, [binary | ModeList -- [cooked]]) end, + ModeList -- [ram, cooked]); + false -> + ram_file:open(Item, ModeList) + end; {true, true} -> erlang:error(badarg, [Item, ModeList]) end; @@ -1511,6 +1723,16 @@ some severe errors such as out of memory. Notice that if option `delayed_write` was used when opening the file, [`close/1`](`close/1`) can return an old write error and not even try to close the file. See `open/2`. + +## Examples + +```erlang +1> File = filename:join(Dir, "close_example.txt"). +2> {ok, Fd} = file:open(File, [write]). +3> ok = file:write(Fd, "data"). +4> file:close(Fd). +ok +``` """. -spec close(IoDevice) -> ok | {error, Reason} when IoDevice :: io_device(), @@ -1537,6 +1759,18 @@ data in a specific pattern in the future, thus allowing the operating system to perform appropriate optimizations. On some platforms, this function might have no effect. + +## Examples + +```erlang +1> File = filename:join(Dir, "advise_example.txt"). +2> ok = file:write_file(File, <<"data">>). +3> {ok, Fd} = file:open(File, [read]). +4> file:advise(Fd, 0, 0, sequential). +ok +5> file:close(Fd). +ok +``` """. -doc(#{since => <<"OTP R14B">>}). -spec advise(IoDevice, Offset, Length, Advise) -> ok | {error, Reason} when @@ -1603,6 +1837,22 @@ Typical error reasons: - **`{no_translation, unicode, latin1}`** - The file is opened with another `encoding` than `latin1` and the data in the file cannot be translated to the byte-oriented data that this function returns. + +## Examples + +```erlang +1> File = filename:join(Dir, "read_example2.txt"). +2> ok = file:write_file(File, <<"Hello, world!">>). +3> {ok, Fd} = file:open(File, [read, binary]). +4> file:read(Fd, 5). +{ok,<<"Hello">>} +5> file:read(Fd, 100). +{ok,<<", world!">>} +6> file:read(Fd, 1). +eof +7> file:close(Fd). +ok +``` """. -spec read(IoDevice, Number) -> {ok, Data} | eof | {error, Reason} when IoDevice :: io_device() | io:device(), @@ -1666,6 +1916,24 @@ Typical error reasons: - **`{no_translation, unicode, latin1}`** - The file is opened with another `encoding` than `latin1` and the data on the file cannot be translated to the byte-oriented data that this function returns. + +## Examples + +```erlang +1> File = filename:join(Dir, "readline_example.txt"). +2> ok = file:write_file(File, <<"line 1\nline 2\nline 3">>). +3> {ok, Fd} = file:open(File, [read, binary]). +4> file:read_line(Fd). +{ok,<<"line 1\n">>} +5> file:read_line(Fd). +{ok,<<"line 2\n">>} +6> file:read_line(Fd). +{ok,<<"line 3">>} +7> file:read_line(Fd). +eof +8> file:close(Fd). +ok +``` """. -spec read_line(IoDevice) -> {ok, Data} | eof | {error, Reason} when IoDevice :: io_device() | io:device(), @@ -1697,6 +1965,18 @@ requested position is beyond end of file. As the position is specified as a byte-offset, take special caution when working with files where `encoding` is set to something else than `latin1`, as not every byte position is a valid character boundary on such a file. + +## Examples + +```erlang +1> File = filename:join(Dir, "pread2_example.txt"). +2> ok = file:write_file(File, <<"HelloWorld">>). +3> {ok, Fd} = file:open(File, [read, binary]). +4> file:pread(Fd, [{0, 5}, {5, 5}]). +{ok,[<<"Hello">>,<<"World">>]} +5> file:close(Fd). +ok +``` """. -spec pread(IoDevice, LocNums) -> {ok, DataL} | eof | {error, Reason} when IoDevice :: io_device(), @@ -1738,6 +2018,20 @@ and unchanged for `ram` mode. As the position is specified as a byte-offset, take special caution when working with files where `encoding` is set to something else than `latin1`, as not every byte position is a valid character boundary on such a file. + +## Examples + +```erlang +1> File = filename:join(Dir, "pread_example.txt"). +2> ok = file:write_file(File, <<"Hello, world!">>). +3> {ok, Fd} = file:open(File, [read, binary]). +4> file:pread(Fd, 0, 5). +{ok,<<"Hello">>} +5> file:pread(Fd, 7, 5). +{ok,<<"world">>} +6> file:close(Fd). +ok +``` """. -spec pread(IoDevice, Location, Number) -> {ok, Data} | eof | {error, Reason} when @@ -1771,6 +2065,20 @@ Typical error reasons: - **`ebadf`** - The file is not opened for writing. - **`enospc`** - No space is left on the device. + +## Examples + +```erlang +1> File = filename:join(Dir, "write_example2.txt"). +2> {ok, Fd} = file:open(File, [write]). +3> file:write(Fd, "Hello!"). +ok +4> file:write(Fd, [<<" More">>, <<" data">>]). +ok +5> ok = file:close(Fd). +6> file:read_file(File). +{ok,<<"Hello! More data">>} +``` """. -spec write(IoDevice, Bytes) -> ok | {error, Reason} when IoDevice :: io_device() | io:device(), @@ -1798,6 +2106,20 @@ the failure. When positioning in a file with other `encoding` than `latin1`, caution must be taken to set the position on a correct character boundary. For details, see `position/2`. + +## Examples + +```erlang +1> File = filename:join(Dir, "pwrite2_example.txt"). +2> ok = file:write_file(File, <<"HelloWorld">>). +3> {ok, Fd} = file:open(File, [write, read, binary]). +4> file:pwrite(Fd, [{0, <<"HELLO">>}, {5, <<"WORLD">>}]). +ok +5> file:pread(Fd, 0, 10). +{ok,<<"HELLOWORLD">>} +6> file:close(Fd). +ok +``` """. -spec pwrite(IoDevice, LocBytes) -> ok | {error, {N, Reason}} when IoDevice :: io_device(), @@ -1836,6 +2158,20 @@ and unchanged for `ram` mode. When positioning in a file with other `encoding` than `latin1`, caution must be taken to set the position on a correct character boundary. For details, see `position/2`. + +## Examples + +```erlang +1> File = filename:join(Dir, "pwrite_example.txt"). +2> {ok, Fd} = file:open(File, [write, read, binary]). +3> ok = file:write(Fd, <<"Hello, world!">>). +4> file:pwrite(Fd, 7, <<"Erlang!">>). +ok +5> file:pread(Fd, 0, 14). +{ok,<<"Hello, Erlang!">>} +6> file:close(Fd). +ok +``` """. -spec pwrite(IoDevice, Location, Bytes) -> ok | {error, Reason} when IoDevice :: io_device(), @@ -1866,6 +2202,17 @@ newly written data and another one to update the modification time stored in the Available only in some POSIX systems, this call results in a call to `fsync()`, or has no effect in systems not providing the `fdatasync()` syscall. + +## Examples + +```erlang +1> File = filename:join(Dir, "datasync_example.txt"). +2> {ok, Fd} = file:open(File, [write]). +3> ok = file:write(Fd, "important data"). +4> ok = file:datasync(Fd). +5> file:close(Fd). +ok +``` """. -doc(#{since => <<"OTP R14B">>}). -spec datasync(IoDevice) -> ok | {error, Reason} when @@ -1887,6 +2234,17 @@ effect. A typical error reason is: - **`enospc`** - Not enough space left to write the file. + +## Examples + +```erlang +1> File = filename:join(Dir, "sync_example.txt"). +2> {ok, Fd} = file:open(File, [write]). +3> ok = file:write(Fd, "important data"). +4> ok = file:sync(Fd). +5> file:close(Fd). +ok +``` """. -spec sync(IoDevice) -> ok | {error, Reason} when IoDevice :: io_device(), @@ -1927,6 +2285,24 @@ A typical error reason is: - **`einval`** - Either `Location` is illegal, or it is evaluated to a negative offset in the file. Notice that if the resulting position is a negative value, the result is an error, and after the call the file position is undefined. + +## Examples + +```erlang +1> File = filename:join(Dir, "position_example.txt"). +2> ok = file:write_file(File, <<"Hello, world!">>). +3> {ok, Fd} = file:open(File, [read, binary]). +4> file:position(Fd, {bof, 7}). +{ok,7} +5> file:read(Fd, 5). +{ok,<<"world">>} +6> file:position(Fd, bof). +{ok,0} +7> file:read(Fd, 5). +{ok,<<"Hello">>} +8> file:close(Fd). +ok +``` """. -spec position(IoDevice, Location) -> {ok, NewPosition} | {error, Reason} when IoDevice :: io_device(), @@ -1944,6 +2320,20 @@ position(_, _) -> -doc """ Truncates the file referenced by `IoDevice` at the current position. Returns `ok` if successful, otherwise `{error, Reason}`. + +## Examples + +```erlang +1> File = filename:join(Dir, "truncate_example.txt"). +2> ok = file:write_file(File, <<"Hello, world!">>). +3> {ok, Fd} = file:open(File, [read, write, binary]). +4> {ok, 5} = file:position(Fd, 5). +5> file:truncate(Fd). +ok +6> ok = file:close(Fd). +7> file:read_file(File). +{ok,<<"Hello">>} +``` """. -spec truncate(IoDevice) -> ok | {error, Reason} when IoDevice :: io_device(), @@ -1992,6 +2382,18 @@ source. If the operation fails, `{error, Reason}` is returned. Typical error reasons: as for `open/2` if a file had to be opened, and as for `read/2` and `write/2`. + +## Examples + +```erlang +1> Src = filename:join(Dir, "copy_src.txt"). +2> Dst = filename:join(Dir, "copy_dst.txt"). +3> ok = file:write_file(Src, <<"Hello, world!">>). +4> file:copy(Src, Dst). +{ok,13} +5> file:read_file(Dst). +{ok,<<"Hello, world!">>} +``` """. -spec copy(Source, Destination, ByteCount) -> {ok, BytesCopied} | {error, Reason} when @@ -2246,15 +2648,12 @@ following: Erlang terms in the file. To convert the three-element tuple to an English description of the error, use `format_error/1`. -_Example:_ - -```text -f.txt: {person, "kalle", 25}. - {person, "pelle", 30}. -``` +## Examples ```erlang -1> file:consult("f.txt"). +1> File = filename:join(Dir, "terms.txt"). +2> ok = file:write_file(File, "{person, \"kalle\", 25}.\n{person, \"pelle\", 30}.\n"). +3> file:consult(File). {ok,[{person,"kalle",25},{person,"pelle",30}]} ``` @@ -2299,6 +2698,15 @@ Returns one of the following: The encoding of `Filename` can be set by a comment as described in [`epp`](`m:epp#encoding`). + +## Examples + +```erlang +1> ok = file:write_file(filename:join(Dir, "config.terms"), "{key, value}.\n"). +2> {ok, Terms, _FullName} = file:path_consult([Dir], "config.terms"). +3> Terms. +[{key,value}] +``` """. -spec path_consult(Path, Filename) -> {ok, Terms, FullName} | {error, Reason} when Path :: [Dir], @@ -2343,6 +2751,15 @@ Returns one of the following: The encoding of `Filename` can be set by a comment, as described in [`epp`](`m:epp#encoding`). + +## Examples + +```erlang +1> File = filename:join(Dir, "eval_example.erl"). +2> ok = file:write_file(File, "io:format(\"hello\\n\").\n"). +3> file:eval(File). +ok +``` """. -spec eval(Filename) -> ok | {error, Reason} when Filename :: name_all(), @@ -2356,6 +2773,16 @@ eval(File) -> The same as [`eval/1`](`eval/1`), but the variable bindings `Bindings` are used in the evaluation. For information about the variable bindings, see `m:erl_eval`. + +## Examples + +```erlang +1> File = filename:join(Dir, "eval2_example.erl"). +2> ok = file:write_file(File, "X + 1.\n"). +3> Bs = erl_eval:add_binding('X', 10, erl_eval:new_bindings()). +4> file:eval(File, Bs). +ok +``` """. -spec eval(Filename, Bindings) -> ok | {error, Reason} when Filename :: name_all(), @@ -2398,6 +2825,13 @@ Returns one of the following: The encoding of `Filename` can be set by a comment as described in [`epp`](`m:epp#encoding`). + +## Examples + +```erlang +1> ok = file:write_file(filename:join(Dir, "path_eval.erl"), "1 + 1.\n"). +2> {ok, _FullName} = file:path_eval([Dir], "path_eval.erl"). +``` """. -spec path_eval(Path, Filename) -> {ok, FullName} | {error, Reason} when Path :: [Dir :: name_all()], @@ -2452,6 +2886,15 @@ Returns one of the following: The encoding of `Filename` can be set by a comment as described in [`epp`](`m:epp#encoding`). + +## Examples + +```erlang +1> File = filename:join(Dir, "script_example.erl"). +2> ok = file:write_file(File, "lists:seq(1, 5).\n"). +3> file:script(File). +{ok,[1,2,3,4,5]} +``` """. -spec script(Filename) -> {ok, Value} | {error, Reason} when Filename :: name_all(), @@ -2465,6 +2908,16 @@ script(File) -> -doc """ The same as [`script/1`](`script/1`) but the variable bindings `Bindings` are used in the evaluation. See `m:erl_eval` about variable bindings. + +## Examples + +```erlang +1> File = filename:join(Dir, "script2_example.erl"). +2> ok = file:write_file(File, "X * 2.\n"). +3> Bs = erl_eval:add_binding('X', 21, erl_eval:new_bindings()). +4> file:script(File, Bs). +{ok,42} +``` """. -spec script(Filename, Bindings) -> {ok, Value} | {error, Reason} when Filename :: name_all(), @@ -2506,6 +2959,13 @@ Returns one of the following: The encoding of `Filename` can be set by a comment as described in [`epp`](`m:epp#encoding`). + +## Examples + +```erlang +1> ok = file:write_file(filename:join(Dir, "path_script.erl"), "1 + 2.\n"). +2> {ok, 3, _FullName} = file:path_script([Dir], "path_script.erl"). +``` """. -spec path_script(Path, Filename) -> {ok, Value, FullName} | {error, Reason} when @@ -2522,6 +2982,14 @@ path_script(Path, File) -> -doc """ The same as [`path_script/2`](`path_script/2`) but the variable bindings `Bindings` are used in the evaluation. See `m:erl_eval` about variable bindings. + +## Examples + +```erlang +1> ok = file:write_file(filename:join(Dir, "path_script3.erl"), "X + Y.\n"). +2> Bs = erl_eval:add_binding('Y', 3, erl_eval:add_binding('X', 4, erl_eval:new_bindings())). +3> {ok, 7, _FullName} = file:path_script([Dir], "path_script3.erl", Bs). +``` """. -spec path_script(Path, Filename, Bindings) -> {ok, Value, FullName} | {error, Reason} when @@ -2571,6 +3039,17 @@ Returns one of the following: `Path`. - **`{error, atom()}`** - The file cannot be opened. + +## Examples + +```erlang +1> ok = file:write_file(filename:join(Dir, "path_open.txt"), <<"data">>). +2> {ok, Fd, _FullName} = file:path_open([Dir], "path_open.txt", [read]). +3> file:close(Fd). +ok +4> file:path_open([Dir], "nonexistent.txt", [read]). +{error,enoent} +``` """. -spec path_open(Path, Filename, Modes) -> {ok, IoDevice, FullName} | {error, Reason} when @@ -2599,7 +3078,18 @@ path_open(PathList, Name, Mode) -> end end. --doc "Changes permissions of a file. See `write_file_info/2`.". +-doc """ +Changes permissions of a file. See `write_file_info/2`. + +## Examples + +```erlang +1> File = filename:join(Dir, "chmod_example.txt"). +2> ok = file:write_file(File, <<"data">>). +3> file:change_mode(File, 8#00644). +ok +``` +""". -doc(#{since => <<"OTP R14B">>}). -spec change_mode(Filename, Mode) -> ok | {error, Reason} when Filename :: name_all(), @@ -2641,7 +3131,21 @@ change_group(Name, GroupId) when is_integer(GroupId) -> write_file_info(Name, #file_info{gid=GroupId}). --doc "Changes the modification and access times of a file. See `write_file_info/2`.". +-doc """ +Changes the modification and access times of a file. See `write_file_info/2`. + +## Examples + +```erlang +1> File = filename:join(Dir, "ctime_example.txt"). +2> ok = file:write_file(File, <<"data">>). +3> file:change_time(File, {{2024, 1, 1}, {0, 0, 0}}). +ok +4> {ok, Info} = file:read_file_info(File). +5> element(6, Info). +{{2024,1,1},{0,0,0}} +``` +""". -spec change_time(Filename, Mtime) -> ok | {error, Reason} when Filename :: name_all(), Mtime :: date_time(), @@ -2655,6 +3159,17 @@ change_time(Name, {{Y, M, D}, {H, Min, Sec}}=Time) -doc """ Changes the modification and last access times of a file. See `write_file_info/2`. + +## Examples + +```erlang +1> File = filename:join(Dir, "ctime3_example.txt"). +2> ok = file:write_file(File, <<"data">>). +3> Atime = {{2024, 6, 15}, {12, 0, 0}}. +4> Mtime = {{2024, 6, 15}, {13, 0, 0}}. +5> file:change_time(File, Atime, Mtime). +ok +``` """. -spec change_time(Filename, Atime, Mtime) -> ok | {error, Reason} when Filename :: name_all(), diff --git a/lib/kernel/src/file_io_server.erl b/lib/kernel/src/file_io_server.erl index 956fcd7d60bb..89555fd20965 100644 --- a/lib/kernel/src/file_io_server.erl +++ b/lib/kernel/src/file_io_server.erl @@ -28,6 +28,7 @@ -export([format_error/1]). -export([start/3, start_link/3]). +-export([start_handle/3, start_link_handle/3]). -export([count_and_find/3]). @@ -60,27 +61,36 @@ start_link(Owner, FileName, ModeList) when is_pid(Owner), (is_list(FileName) orelse is_binary(FileName)), is_list(ModeList) -> do_start(spawn_link, Owner, FileName, ModeList). +start_handle(Owner, OpenFun, ModeList) + when is_pid(Owner), is_function(OpenFun, 2), is_list(ModeList) -> + do_start_handle(spawn, Owner, OpenFun, ModeList). + +start_link_handle(Owner, OpenFun, ModeList) + when is_pid(Owner), is_function(OpenFun, 2), is_list(ModeList) -> + do_start_handle(spawn_link, Owner, OpenFun, ModeList). + %%%----------------------------------------------------------------- %%% Server starter, dispatcher and helpers do_start(Spawn, Owner, FileName, ModeList) -> + OpenFun = fun(ReadMode, Opts0) -> + Opts = maybe_add_read_ahead(ReadMode, Opts0), + raw_file_io:open(FileName, [raw | Opts]) + end, + do_start_handle(Spawn, Owner, OpenFun, ModeList). + +do_start_handle(Spawn, Owner, OpenFun, ModeList) -> Self = self(), Ref = make_ref(), Utag = erlang:dt_spread_tag(true), - Pid = + Pid = erlang:Spawn( fun() -> erlang:dt_restore_tag(Utag), - %% process_flag(trap_exit, true), case parse_options(ModeList) of - {ReadMode, UnicodeMode, Opts0} -> - Opts = maybe_add_read_ahead(ReadMode, Opts0), - case raw_file_io:open(FileName, [raw | Opts]) of - {error, Reason} = Error -> - Self ! {Ref, Error}, - exit(Reason); + {ReadMode, UnicodeMode, Opts} -> + case OpenFun(ReadMode, Opts) of {ok, Handle} -> - %% XXX must I handle R6 nodes here? M = erlang:monitor(process, Owner), Self ! {Ref, ok}, server_loop( @@ -89,7 +99,10 @@ do_start(Spawn, Owner, FileName, ModeList) -> mref = M, buf = <<>>, read_mode = ReadMode, - unic = UnicodeMode}) + unic = UnicodeMode}); + {error, Reason} = Error -> + Self ! {Ref, Error}, + exit(Reason) end; {error,Reason1} = Error1 -> Self ! {Ref, Error1}, diff --git a/lib/kernel/src/ram_file.erl b/lib/kernel/src/ram_file.erl index 43a8cc4028f9..dd9b7eda133f 100644 --- a/lib/kernel/src/ram_file.erl +++ b/lib/kernel/src/ram_file.erl @@ -26,8 +26,8 @@ %% Generic file contents operations -export([open/2, close/1]). --export([write/2, read/2, copy/3, - pread/2, pread/3, pwrite/2, pwrite/3, +-export([write/2, read/2, read_line/1, copy/3, + pread/2, pread/3, pwrite/2, pwrite/3, position/2, truncate/1, datasync/1, sync/1]). %% Specialized file operations @@ -143,7 +143,38 @@ read(#file_descriptor{module = ?MODULE, data = Port}, Sz) {error, einval} end. -write(#file_descriptor{module = ?MODULE, data = Port}, Bytes) -> +read_line(#file_descriptor{module = ?MODULE} = Fd) -> + read_line(Fd, []). + +read_line(Fd, Acc) -> + case read(Fd, 256) of + {ok, Data} -> + case binary:match(Data, <<"\n">>) of + {Pos, 1} -> + %% Check for \r\n: replace with just \n. + {Before, After} = + case Pos > 0 andalso binary:at(Data, Pos - 1) =:= $\r of + true -> + {binary:part(Data, 0, Pos - 1), binary:part(Data, Pos + 1, byte_size(Data) - Pos - 1)}; + false -> + {binary:part(Data, 0, Pos), binary:part(Data, Pos + 1, byte_size(Data) - Pos - 1)} + end, + %% Seek back the unread portion after the \n. + case byte_size(After) of + 0 -> ok; + N -> {ok, _} = position(Fd, {cur, -N}), ok + end, + {ok, iolist_to_binary(lists:reverse(Acc, [Before, <<"\n">>]))}; + nomatch -> + read_line(Fd, [Data | Acc]) + end; + eof when Acc =:= [] -> + eof; + eof -> + {ok, iolist_to_binary(lists:reverse(Acc))} + end. + +write(#file_descriptor{module = ?MODULE, data = Port}, Bytes) -> case call_port(Port, [?RAM_FILE_WRITE | Bytes]) of {ok, _Sz} -> ok; diff --git a/lib/kernel/test/file_SUITE.erl b/lib/kernel/test/file_SUITE.erl index 1cfa5bfbdb06..ad2ce592db04 100644 --- a/lib/kernel/test/file_SUITE.erl +++ b/lib/kernel/test/file_SUITE.erl @@ -45,10 +45,11 @@ -module(?FILE_SUITE). --export([all/0, suite/0,groups/0,init_per_suite/1, end_per_suite/1, +-export([all/0, suite/0,groups/0,init_per_suite/1, end_per_suite/1, init_per_group/2,end_per_group/2, init_per_testcase/2, end_per_testcase/2, - read_write_file/1, names/1]). + read_write_file/1, names/1, + doctests/1]). -export([cur_dir_0/1, cur_dir_1/1, make_del_dir/1, make_del_dir_r/1, list_dir/1,list_dir_error/1, untranslatable_names/1, untranslatable_names_error/1, @@ -56,7 +57,8 @@ -export([close/1, consult1/1, path_consult/1, delete/1]). -export([ eval1/1, path_eval/1, script1/1, path_script/1, open1/1, - old_modes/1, new_modes/1, path_open/1, open_errors/1]). + old_modes/1, new_modes/1, path_open/1, open_errors/1, + cooked_ram/1]). -export([ file_info_basic_file/1, file_info_basic_directory/1, file_info_bad/1, file_info_times/1, file_write_file_info/1, file_wfi_helpers/1]). @@ -140,7 +142,8 @@ all() -> ipread, interleaved_read_write, otp_5814, otp_10852, large_file, large_write, read_line_1, read_line_2, read_line_3, read_line_4, standard_io, old_io_protocol, - unicode_mode, {group, bench} + unicode_mode, {group, bench}, + doctests ]. groups() -> @@ -154,7 +157,7 @@ groups() -> {open, [], [open1, old_modes, new_modes, path_open, close, access, read_write, pread_write, append, open_errors, - exclusive]}, + exclusive, cooked_ram]}, {pos, [], [pos1, pos2, pos3]}, {file_info, [], [file_info_basic_file, file_info_basic_directory, @@ -1318,6 +1321,51 @@ exclusive(Config) when is_list(Config) -> ok = ?FILE_MODULE:close(Fd), ok. +%% Test cooked option for ram files. +%% The cooked option wraps a ram file in a pid-based I/O server, +%% making it usable with the io module (unlike a plain ram file +%% which returns a raw file descriptor). +cooked_ram(_Config) -> + Data = <<"hello, world\nline two\n">>, + + %% Basic read via io module. + {ok, Fd1} = ?FILE_MODULE:open(Data, [ram, read, binary, cooked]), + true = is_pid(Fd1), + <<"hello, world\n">> = io:get_line(Fd1, ''), + <<"line two\n">> = io:get_line(Fd1, ''), + eof = io:get_line(Fd1, ''), + ok = ?FILE_MODULE:close(Fd1), + + %% Write and read back. + {ok, Fd2} = ?FILE_MODULE:open(<<>>, [ram, write, read, binary, cooked]), + true = is_pid(Fd2), + ok = io:put_chars(Fd2, "test data"), + {ok, 0} = ?FILE_MODULE:position(Fd2, bof), + {ok, <<"test data">>} = ?FILE_MODULE:read(Fd2, 100), + ok = ?FILE_MODULE:close(Fd2), + + %% io:scan_erl_form works (needed for epp). + ErlData = <<"-module(test).\n">>, + {ok, Fd3} = ?FILE_MODULE:open(ErlData, [ram, read, binary, cooked]), + true = is_pid(Fd3), + {ok, Tokens, _} = io:scan_erl_form(Fd3, '', 1), + true = is_list(Tokens), + ok = ?FILE_MODULE:close(Fd3), + + %% Without cooked, ram files return a raw fd (not a pid). + {ok, RawFd} = ?FILE_MODULE:open(Data, [ram, read, binary]), + false = is_pid(RawFd), + ok = ?FILE_MODULE:close(RawFd), + + %% io:setopts and io:getopts work on cooked ram files. + {ok, Fd4} = ?FILE_MODULE:open(Data, [ram, read, binary, cooked]), + Opts = io:getopts(Fd4), + true = is_list(Opts), + ok = ?FILE_MODULE:close(Fd4), + + [] = flush(), + ok. + %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% @@ -4921,3 +4969,53 @@ memsize() -> Available -> Available end. + +doctests(Config) -> + Dir = filename:join(proplists:get_value(priv_dir, Config), "doctests"), + ok = file:make_dir(Dir), + Bindings = #{'Dir' => Dir}, + FunsNeedingDir = + [{function, delete, 2}, + {function, rename, 2}, + {function, set_cwd, 1}, + {function, make_dir, 1}, + {function, del_dir, 1}, + {function, del_dir_r, 1}, + {function, list_dir, 1}, + {function, list_dir_all, 1}, + {function, read_file, 2}, + {function, read_file_info, 2}, + {function, write_file, 2}, + {function, write_file, 3}, + {function, make_link, 2}, + {function, open, 2}, + {function, close, 1}, + {function, advise, 4}, + {function, read, 2}, + {function, write, 2}, + {function, read_line, 1}, + {function, pread, 2}, + {function, pread, 3}, + {function, pwrite, 2}, + {function, pwrite, 3}, + {function, datasync, 1}, + {function, sync, 1}, + {function, position, 2}, + {function, truncate, 1}, + {function, consult, 1}, + {function, path_consult, 2}, + {function, eval, 1}, + {function, eval, 2}, + {function, path_eval, 2}, + {function, script, 1}, + {function, script, 2}, + {function, path_script, 2}, + {function, path_script, 3}, + {function, path_open, 3}, + {function, change_mode, 2}, + {function, change_time, 2}, + {function, change_time, 3}, + {function, copy, 3}], + ct_doctest:module(file, + [{KFA, Bindings} || KFA <- FunsNeedingDir], + []). diff --git a/lib/stdlib/src/epp.erl b/lib/stdlib/src/epp.erl index a230ce81b709..18c5f5bf1863 100644 --- a/lib/stdlib/src/epp.erl +++ b/lib/stdlib/src/epp.erl @@ -89,6 +89,101 @@ Module:format_error(ErrorDescriptor) -type epp_handle() :: pid(). -type source_encoding() :: latin1 | utf8. +-doc """ +The `t:option/0` are the options that can be used to customize the preprocessing. + +* **`{default_encoding, DefEncoding}`** - sets the default encoding of the file. The default encoding is + used if no valid encoding is found in the file. The valid encodings are `latin1` and `utf8`, + where the case of the characters can be chosen freely. If unset, it defaults to `utf8`. + +* **`{includes, IncludePath}`** - sets the include path for the file. The include path is used to resolve + `-include`, `-include_lib` directives. + +* **`{source_name, SourceName}`** - sets the file name of the implicit -file() attributes inserted + during preprocessing. If unset it will default to the name of the opened file. + +* **`{macros, PredefMacros}`** - sets the predefined `t:macros/0` for the file. `PredefMacros` is a list of + macros that are defined before preprocessing starts. + +* **`{deterministic, Enabled}`** - if set to `true`, will reduce the file name of the + implicit -file() attributes inserted during preprocessing to only the basename of the path. + +* **`{extra, Enabled}`** - if set to `true`, the return value is `{ok, Epp, Extra}` instead of `{ok, Epp}`, + where `Extra` contains which encoding was detected from the file. + +* **`{location, StartLocation}`** - sets the initial location of the file. The option `location` is forwarded + to the Erlang token scanner, see [`erl_scan:tokens/3,4`](`erl_scan:tokens/3`). For example: + + ```erlang + 1> file:read_file("example.erl"). + {ok, <<"-module(example).\n\n-export([foo/0]).\n\nfoo() -> ?MODULE.", ...>>} + 2> epp:parse_file("example.erl", [{location, {1, 1}}]). + {ok,[{attribute,{1,1},file,{"example.erl",1}}, + {attribute,{1,2},module,example}, + {attribute,{3,2},export,[{foo,0}]}, + {function,{5,1}, + foo,0, + [{clause,{5,1},[],[],[{atom,{5,11},example}]}]}, + {eof,{28,1}}]} + ``` + +* **`{fd, FileDescriptor}`** - use an already opened file descriptor to read from instead of a file name. + The file descriptor is expected to be an `t:file:io_server/0`. This enables in-memory preprocessing + using [ram files](`m:file#ram`), where the main source file can be served from memory without touching + disk. For example: + + See `parse_file/2` for an example of how to use this option. + +* **`{include_path_open, Fun}`** - provide a custom function for opening include files. The function has the + same signature as `file:path_open/3` and is used when resolving `-include`, `-include_lib`, and + `-doc {file, ...}` directives. This enables fully in-memory preprocessing using [ram files](`m:file#ram`). + For example: + + ```erlang + 1> {ok, IoDevice} = file:open(<<"-module(foo).\n-include(\"bar.hrl\").">>, + [read, binary, ram, cooked]). + 2> IncludeFileFun = fun + (_Path, "bar.hrl", Modes) -> + {ok, RamFD} = file:open(<<"-export([bar/0]).\nbar() ->\n?MODULE.">>, + [read, binary, ram, cooked | Modes]), + {ok, RamFD, "bar.hrl"}; + (Path, Name, Modes) -> + file:path_open(Path, Name, Modes) + end. + 3> {ok, EPP} = epp:parse_file("example.erl", + [{fd, IoDevice}, {include_path_open, IncludeFileFun}]). + {ok,[{attribute,1,file,{"example.erl",1}}, + {attribute,1,module,foo}, + {attribute,1,file,{"bar.hrl",1}}, + {attribute,1,export,[{bar,0}]}, + {function,2,bar,0,[{clause,2,[],[],[{atom,3,foo}]}]}, + {attribute,2,file,{"example.erl",2}}, + {eof,2}]} + ``` + + Since OTP @OTP-12345678@ +* **`{compiler_internal,term()}`** - forwarded to the Erlang token + scanner, see [`{compiler_internal,term()}`](`m:erl_scan#compiler_interal`) in `erl_scan:string/3`. +""". +-type option() :: + {default_encoding, DefEncoding :: source_encoding()} | + {includes, IncludePath :: [ DirectoryName :: file:name()]} | + {macros, PredefMacros :: macros()} | + {source_name, SourceName :: file:name()} | + {deterministic, boolean()} | + {location, StartLocation :: erl_anno:location()} | + {reserved_word_fun, Fun :: fun((atom()) -> boolean())} | + {features, [Feature :: atom()]} | + {fd, OpenedFile :: file:io_server()} | + {include_path_open, fun((Path :: [file:name_all()], + FileName :: file:name_all(), + Modes :: [file:mode()]) -> + {ok, IoDevice :: file:io_device(), + FullName :: file:filename_all()} | + {error, term()})} | + 'extra' | + {'compiler_internal', [term()]}. + -type ifdef() :: 'ifdef' | 'ifndef' | 'if' | 'else'. -type name() :: atom(). @@ -129,7 +224,8 @@ Module:format_error(ErrorDescriptor) features = [] :: [atom()], else_reserved = false :: boolean(), fname = [] :: function_name_type(), - deterministic = false :: boolean() + deterministic = false :: boolean(), + include_path_open = fun file:path_open/3 :: fun() }). %% open(Options) @@ -145,7 +241,7 @@ Module:format_error(ErrorDescriptor) %% parse_file(FileName, IncludePath, PreDefMacros) %% macro_defs(Epp) --doc "Equivalent to `epp:open([{name, FileName}, {includes, IncludePath}])`.". +-doc #{ equiv => open([{name, FileName}, {includes, IncludePath}]) }. -spec open(FileName, IncludePath) -> {'ok', Epp} | {'error', ErrorDescriptor} when FileName :: file:name(), @@ -156,10 +252,7 @@ Module:format_error(ErrorDescriptor) open(Name, Path) -> open(Name, Path, []). --doc """ -Equivalent to -`epp:open([{name, FileName}, {includes, IncludePath}, {macros, PredefMacros}])`. -""". +-doc #{ equiv => open([{name, FileName}, {includes, IncludePath}, {macros, PredefMacros}]) }. -spec open(FileName, IncludePath, PredefMacros) -> {'ok', Epp} | {'error', ErrorDescriptor} when FileName :: file:name(), @@ -174,36 +267,37 @@ open(Name, Path, Pdm) -> -doc """ Opens a file for preprocessing. -If you want to change the file name of the implicit -file() attributes inserted -during preprocessing, you can do with `{source_name, SourceName}`. If unset it -will default to the name of the opened file. +The function is used to start parsing of an Erlang source file. To get the forms from the file +use `scan_erl_form/1` or `parse_erl_form/1`. When finished, the `m:epp` server should be closed with +`close/1`. Use this function if you want to scan or parse a file incrementally. To scan or parse +a whole file at once, use `scan_file/2` and `parse_file/3` respectively. -Setting `{deterministic, Enabled}` will additionally reduce the file name of the -implicit -file() attributes inserted during preprocessing to only the basename -of the path. +When using `open/1` the option `name` must always be specified and is the name of the file to +preprocess. This name is used in error messages and in the implicit `-file()` attributes +generated during preprocessing. -If `extra` is specified in `Options`, the return value is `{ok, Epp, Extra}` -instead of `{ok, Epp}`. +See `t:option/0` for the other available options and what they do. -The option `location` is forwarded to the Erlang token scanner, see -[`erl_scan:tokens/3,4`](`erl_scan:tokens/3`). +## Examples + +```erlang +1> file:read_file("example.erl"). +{ok, <<"-module(example).\n", ...>>} +2> {ok, EPP} = epp:open("example.erl", []). +3> epp:scan_erl_form(EPP). +{ok,[{'-',1},{atom,1,file},{'(',1},{string,1,"example.erl"}, + {',',1},{integer,1,1},{')',1},{dot,1}]} +4> epp:parse_erl_form(EPP). +{ok,{attribute,1,module,example}} +5> epp:close(EPP). +ok +``` -The `{compiler_internal,term()}` option is forwarded to the Erlang token -scanner, see [`{compiler_internal,term()}`](`m:erl_scan#compiler_interal`). """. -doc(#{since => <<"OTP 17.0">>}). -spec open(Options) -> {'ok', Epp} | {'ok', Epp, Extra} | {'error', ErrorDescriptor} when - Options :: [{'default_encoding', DefEncoding :: source_encoding()} | - {'includes', IncludePath :: [DirectoryName :: file:name()]} | - {'source_name', SourceName :: file:name()} | - {'deterministic', Enabled :: boolean()} | - {'macros', PredefMacros :: macros()} | - {'name',FileName :: file:name()} | - {'location',StartLocation :: erl_anno:location()} | - {'fd',FileDescriptor :: file:io_device()} | - 'extra' | - {'compiler_internal', [term()]}], + Options :: [option() | {'name',FileName :: file:name()}], Epp :: epp_handle(), Extra :: [{'encoding', source_encoding() | 'none'}], ErrorDescriptor :: term(). @@ -229,8 +323,7 @@ open(Options) -> end. -doc "Closes the preprocessing of a file.". --spec close(Epp) -> 'ok' when - Epp :: epp_handle(). +-spec close(Epp :: epp_handle()) -> 'ok'. close(Epp) -> %% Make sure that close is synchronous as a courtesy to test @@ -242,7 +335,9 @@ close(Epp) -> -doc """ Returns the raw tokens of the next Erlang form from the opened Erlang source -file. A tuple `{eof, Line}` is returned at the end of the file. The first form +file. + +A tuple `{eof, Line}` is returned at the end of the file. The first form corresponds to an implicit attribute `-file(File,1).`, where `File` is the file name. """. @@ -260,8 +355,9 @@ scan_erl_form(Epp) -> epp_request(Epp, scan_erl_form). -doc """ -Returns the next Erlang form from the opened Erlang source file. Tuple -`{eof, Location}` is returned at the end of the file. The first form corresponds +Returns the next Erlang form from the opened Erlang source file. + +Tuple `{eof, Location}` is returned at the end of the file. The first form corresponds to an implicit attribute `-file(File,1).`, where `File` is the file name. """. -spec parse_erl_form(Epp) -> @@ -365,18 +461,30 @@ format_error_1(E) -> file:format_error(E). -doc """ Preprocesses an Erlang source file returning a list of the lists of raw tokens -of each form. Notice that the tuple `{eof, Line}` returned at the end of the -file is included as a "form", and any failures to scan a form are included in -the list as tuples `{error, ErrorInfo}`. +of each form. + +Notice that the tuple `{eof, Line}` returned at the end of the file is included +as a "form", and any failures to scan a form are included in the list as tuples +`{error, ErrorInfo}`. + +For details on what each `Option` does, see `t:option/0`. + +## Examples + +``` +1> file:read_file("example.erl"). +{ok, <<"-module(example).\n\n-export([foo/0]).\n\nfoo() -> ?MODULE.", ...>>} +2> epp:scan_file("example.erl", []). +{ok,[[{'-',1},{atom,1,file},{'(',1},{string,1,"example.erl"}, + {',',1},{integer,1,1},{')',1},{dot,1}], ...], + [{encoding,none}]} +``` """. -doc(#{since => <<"OTP 24.0">>}). -spec scan_file(FileName, Options) -> {'ok', [Form], Extra} | {error, OpenError} when FileName :: file:name(), - Options :: [{'includes', IncludePath :: [DirectoryName :: file:name()]} | - {'source_name', SourceName :: file:name()} | - {'macros', PredefMacros :: macros()} | - {'default_encoding', DefEncoding :: source_encoding()}], + Options :: [option()], Form :: erl_scan:tokens() | {'error', ErrorInfo} | {'eof', Loc}, Loc :: erl_anno:location(), ErrorInfo :: erl_scan:error_info(), @@ -404,10 +512,7 @@ scan_file(Epp) -> [{eof,Location}] end. --doc """ -Equivalent to -`epp:parse_file(FileName, [{includes, IncludePath}, {macros, PredefMacros}])`. -""". +-doc #{ equiv => parse_file(Filename, [{includes, IncludePath}, {macros, PredefMacros}]) }. -spec parse_file(FileName, IncludePath, PredefMacros) -> {'ok', [Form]} | {error, OpenError} when FileName :: file:name(), @@ -424,36 +529,46 @@ parse_file(Ifile, Path, Predefs) -> parse_file(Ifile, [{includes, Path}, {macros, Predefs}]). -doc """ -Preprocesses and parses an Erlang source file. Notice that tuple -`{eof, Location}` returned at the end of the file is included as a "form". +Preprocesses and parses an Erlang source file. + +Notice that tuple `{eof, Location}` returned at the end of the file is included as +a "form". -If you want to change the file name of the implicit -file() attributes inserted -during preprocessing, you can do with `{source_name, SourceName}`. If unset it -will default to the name of the opened file. +See `t:option/0` for the available options and what they do. -If `extra` is specified in `Options`, the return value is `{ok, [Form], Extra}` -instead of `{ok, [Form]}`. +## Examples -The option `location` is forwarded to the Erlang token scanner, see -[`erl_scan:tokens/3,4`](`erl_scan:tokens/3`). +Parse a file: -The `{compiler_internal,term()}` option is forwarded to the Erlang token -scanner, see [`{compiler_internal,term()}`](`m:erl_scan#compiler_interal`). +```erlang +1> file:read_file("example.erl"). +{ok, <<"-module(example).\n\n-export([foo/0]).\n\nfoo() -> ?MODULE.", ...>>} +2> epp:parse_file("example.erl", []). +{ok,[{attribute,1,file,{"example.erl",1}}, + {attribute,1,module,example}, + {attribute,3,export,[{foo,0}]}, + {function,5,foo,0,[{clause,5,[],[],[{atom,5,example}]}]}, + {eof,28}]} +``` + +Parse a ram file: + +```erlang +1> {ok, IoDevice} = file:open(<<"-module(foo).\n-export([bar/0]).\nbar() ->\n?MODULE.">>, + [read, binary, ram, cooked]). +2> epp:parse_file("foo.erl", [{fd, IoDevice}]). +{ok,[{attribute,1,file,{"foo.erl",1}}, + {attribute,1,module,foo}, + {attribute,2,export,[{bar,0}]}, + {function,3,bar,0,[{clause,3,[],[],[{atom,4,foo}]}]}, + {eof,4}]} +``` """. -doc(#{since => <<"OTP 17.0">>}). -spec parse_file(FileName, Options) -> {'ok', [Form]} | {'ok', [Form], Extra} | {error, OpenError} when FileName :: file:name(), - Options :: [{'includes', IncludePath :: [DirectoryName :: file:name()]} | - {'source_name', SourceName :: file:name()} | - {'macros', PredefMacros :: macros()} | - {'default_encoding', DefEncoding :: source_encoding()} | - {'location',StartLocation :: erl_anno:location()} | - {'reserved_word_fun', Fun :: fun((atom()) -> boolean())} | - {'features', [Feature :: atom()]} | - {'deterministic', boolean()} | - 'extra' | - {'compiler_internal', [term()]}], + Options :: [option()], Form :: erl_parse:abstract_form() | {'error', ErrorInfo} | {'eof',Location}, @@ -827,6 +942,8 @@ init_server(Pid, FileName, Options, St0) -> AtLocation = proplists:get_value(location, Options, 1), Deterministic = proplists:get_value(deterministic, Options, false), + PathOpen = proplists:get_value(include_path_open, Options, + fun file:path_open/3), St = St0#epp{delta=0, name=SourceName, name2=SourceName, path=Path, location=AtLocation, macs=Ms1, default_encoding=DefEncoding, @@ -839,7 +956,8 @@ init_server(Pid, FileName, Options, St0) -> end, features = Features, else_reserved = ResWordFun('else'), - deterministic = Deterministic}, + deterministic = Deterministic, + include_path_open = PathOpen}, From = wait_request(St), Anno = erl_anno:new(AtLocation), enter_file_reply(From, file_name(SourceName), Anno, @@ -993,7 +1111,7 @@ enter_file(_NewName, Inc, From, St) epp_reply(From, {error,{loc(Inc),epp,{depth,"include"}}}), wait_req_scan(St); enter_file(NewName, Inc, From, St) -> - case file:path_open(St#epp.path, NewName, [read]) of + case (St#epp.include_path_open)(St#epp.path, NewName, [read]) of {ok,NewF,Pname} -> Loc = start_loc(St#epp.location), wait_req_scan(enter_file2(NewF, Pname, From, St, Loc)); @@ -1014,7 +1132,8 @@ enter_file2(NewF, Pname, From, St0, AtLocation) -> erl_scan_opts = ScanOpts, else_reserved = ElseReserved, features = Ftrs, - deterministic = Deterministic} = St0, + deterministic = Deterministic, + include_path_open = PathOpen} = St0, Ms = Ms0#{'FILE':={none,[{string,Anno,source_name(St0,Pname)}]}}, %% update the head of the include path to be the directory of the new %% source file, so that an included file can always include other files @@ -1031,7 +1150,8 @@ enter_file2(NewF, Pname, From, St0, AtLocation) -> erl_scan_opts = ScanOpts, else_reserved = ElseReserved, default_encoding=DefEncoding, - deterministic=Deterministic}. + deterministic=Deterministic, + include_path_open=PathOpen}. enter_file_reply(From, Name, LocationAnno, AtLocation, Where, Deterministic) -> Anno0 = erl_anno:new(AtLocation), @@ -1214,7 +1334,7 @@ scan_filedoc_content({string, _A, DocFilename}, Dot, {atom,DocLoc,Doc}, From, #epp{name = CurrentFilename} = St) -> %% The head of the path is the dir where the current file is Cwd = hd(St#epp.path), - case file:path_open([Cwd], DocFilename, [read, binary]) of + case (St#epp.include_path_open)([Cwd], DocFilename, [read, binary]) of {ok, NewF, Pname} -> case file:read_file_info(NewF) of {ok, #file_info{ size = Sz }} -> @@ -1506,14 +1626,15 @@ scan_include_lib1([{'(',_Alp},{string,_Af,NewName0}=N,{')',_Arp},{dot,_Ad}], _Inc, From, St) -> NewName = expand_var(NewName0), Loc = start_loc(St#epp.location), - case file:path_open(St#epp.path, NewName, [read]) of + case (St#epp.include_path_open)(St#epp.path, NewName, [read]) of {ok,NewF,Pname} -> wait_req_scan(enter_file2(NewF, Pname, From, St, Loc)); {error,_E1} -> case expand_lib_dir(NewName) of {ok,Header} -> - case file:open(Header, [read]) of - {ok,NewF} -> + {ok, Cwd} = file:get_cwd(), + case (St#epp.include_path_open)([Cwd], Header, [read]) of + {ok,NewF,_} -> wait_req_scan(enter_file2(NewF, Header, From, St, Loc)); {error,_E2} -> diff --git a/lib/stdlib/test/epp_SUITE.erl b/lib/stdlib/test/epp_SUITE.erl index 911001015ef9..4dc904289357 100644 --- a/lib/stdlib/test/epp_SUITE.erl +++ b/lib/stdlib/test/epp_SUITE.erl @@ -36,7 +36,9 @@ gh_8268/1, moduledoc_include/1, stringify/1, - fun_type_arg/1 + fun_type_arg/1, + include_path_open/1, + doctests/1 ]). -export([epp_parse_erl_form/2]). @@ -84,7 +86,9 @@ all() -> gh_8268, moduledoc_include, stringify, - fun_type_arg]. + fun_type_arg, + include_path_open, + doctests]. groups() -> [{upcase_mac, [], [upcase_mac_1, upcase_mac_2]}, @@ -2334,6 +2338,126 @@ run_test(Config, Test0, Opts0) -> Reply end. +%% Test the include_path_open option for epp using ram files. +include_path_open(_Config) -> + %% Test 1: Basic ram file preprocessing with macros + Code1 = <<"-module(ram_test).\n" + "-export([f/0]).\n" + "-define(HELLO, hello).\n" + "f() -> {?MODULE, ?HELLO}.\n">>, + Forms1 = epp_open_ram(Code1, []), + {attribute,_,module,ram_test} = lists:keyfind(module, 3, Forms1), + {ok, ram_test, Bin1} = compile:forms(Forms1), + {module, ram_test} = code:load_binary(ram_test, "ram_test.erl", Bin1), + {ram_test, hello} = ram_test:f(), + true = code:delete(ram_test), + code:purge(ram_test), + + %% Test 2: Ram file with -include resolved via include_path_open + HrlContent = <<"-record(point, {x, y}).\n" + "-define(ORIGIN, #point{x=0, y=0}).\n">>, + Code2 = <<"-module(ram_inc).\n" + "-include(\"my_header.hrl\").\n" + "-export([origin/0]).\n" + "origin() -> ?ORIGIN.\n">>, + Files2 = #{"my_header.hrl" => HrlContent}, + PathOpen2 = ram_include_path_open(Files2), + Forms2 = epp_open_ram(Code2, [{include_path_open, PathOpen2}]), + {attribute,_,module,ram_inc} = lists:keyfind(module, 3, Forms2), + {ok, ram_inc, Bin2} = compile:forms(Forms2), + {module, ram_inc} = code:load_binary(ram_inc, "ram_inc.erl", Bin2), + {point, 0, 0} = ram_inc:origin(), + true = code:delete(ram_inc), + code:purge(ram_inc), + + %% Test 3: Nested includes via ram files + Hrl3a = <<"-define(A, a_value).\n">>, + Hrl3b = <<"-include(\"a.hrl\").\n" + "-define(B, {?A, b_value}).\n">>, + Code3 = <<"-module(ram_nested).\n" + "-include(\"b.hrl\").\n" + "-export([t/0]).\n" + "t() -> ?B.\n">>, + Files3 = #{"a.hrl" => Hrl3a, "b.hrl" => Hrl3b}, + PathOpen3 = ram_include_path_open(Files3), + Forms3 = epp_open_ram(Code3, [{include_path_open, PathOpen3}]), + {ok, ram_nested, Bin3} = compile:forms(Forms3), + {module, ram_nested} = code:load_binary(ram_nested, "ram_nested.erl", Bin3), + {a_value, b_value} = ram_nested:t(), + true = code:delete(ram_nested), + code:purge(ram_nested), + + %% Test 4: include_lib resolved via include_path_open + HrlLib = <<"-define(LIB_VAL, from_lib).\n">>, + Code4 = <<"-module(ram_lib).\n" + "-include_lib(\"myapp/include/lib.hrl\").\n" + "-export([t/0]).\n" + "t() -> ?LIB_VAL.\n">>, + Files4 = #{"myapp/include/lib.hrl" => HrlLib}, + PathOpen4 = ram_include_path_open(Files4), + Forms4 = epp_open_ram(Code4, [{include_path_open, PathOpen4}]), + {ok, ram_lib, Bin4} = compile:forms(Forms4), + {module, ram_lib} = code:load_binary(ram_lib, "ram_lib.erl", Bin4), + from_lib = ram_lib:t(), + true = code:delete(ram_lib), + code:purge(ram_lib), + + %% Test 5: Missing include via include_path_open gives proper error + Code5 = <<"-module(ram_missing).\n" + "-include(\"missing.hrl\").\n" + "-export([t/0]).\n" + "t() -> ok.\n">>, + PathOpen5 = ram_include_path_open(#{}), + Forms5 = epp_open_ram(Code5, [{include_path_open, PathOpen5}]), + {error, {2, epp, {include, file, "missing.hrl"}}} = + lists:keyfind(error, 1, Forms5), + + ok. + +%% Helper: open a binary as a cooked ram file and preprocess with epp. +epp_open_ram(Bin, ExtraOpts) -> + {ok, Fd} = file:open(Bin, [ram, read, binary, cooked]), + try + {ok, Epp} = epp:open([{fd, Fd}, {name, "ram_file.erl"}, + {location, 1} | ExtraOpts]), + try + collect_epp_forms(Epp) + after + epp:close(Epp) + end + after + file:close(Fd) + end. + +%% Helper: create a include_path_open function backed by in-memory files. +ram_include_path_open(Files) -> + fun(Path, Name, Modes) -> + %% Try Name directly (for absolute paths and include_lib), + %% then try each path prefix. + Candidates = [Name | [filename:join(Dir, Name) || Dir <- Path]], + ram_include_path_open_try(Candidates, Files, Modes) + end. + +ram_include_path_open_try([], _Files, _Modes) -> + {error, enoent}; +ram_include_path_open_try([Candidate | Rest], Files, Modes) -> + case maps:find(Candidate, Files) of + {ok, Content} -> + {ok, Fd} = file:open(Content, [ram, read, binary, cooked]), + {ok, Fd, Candidate}; + error -> + ram_include_path_open_try(Rest, Files, Modes) + end. + +doctests(Config) -> + {ok, Cwd} = file:get_cwd(), + try + ok = file:set_cwd(proplists:get_value(data_dir, Config)), + ct_doctest:module(epp) + after + file:set_cwd(Cwd) + end. + fail() -> ct:fail(failed). diff --git a/lib/stdlib/test/epp_SUITE_data/example.erl b/lib/stdlib/test/epp_SUITE_data/example.erl new file mode 100644 index 000000000000..3d5d1ca5a380 --- /dev/null +++ b/lib/stdlib/test/epp_SUITE_data/example.erl @@ -0,0 +1,27 @@ +-module(example). + +-export([foo/0]). + +foo() -> ?MODULE. + +%% +%% %CopyrightBegin% +%% +%% SPDX-License-Identifier: Apache-2.0 +%% +%% Copyright Ericsson AB 2018-2025. 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% +%%