1%% Licensed under the Apache License, Version 2.0 (the "License"); you may 2%% not use this file except in compliance with the License. You may obtain 3%% a copy of the License at <http://www.apache.org/licenses/LICENSE-2.0> 4%% 5%% Unless required by applicable law or agreed to in writing, software 6%% distributed under the License is distributed on an "AS IS" BASIS, 7%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 8%% See the License for the specific language governing permissions and 9%% limitations under the License. 10%% 11%% Alternatively, you may use this file under the terms of the GNU Lesser 12%% General Public License (the "LGPL") as published by the Free Software 13%% Foundation; either version 2.1, or (at your option) any later version. 14%% If you wish to allow use of your version of this file only under the 15%% terms of the LGPL, you should delete the provisions above and replace 16%% them with the notice and other provisions required by the LGPL; see 17%% <http://www.gnu.org/licenses/>. If you do not delete the provisions 18%% above, a recipient may use your version of this file under the terms of 19%% either the Apache License or the LGPL. 20%% 21%% @author Richard Carlsson <carlsson.richard@gmail.com> 22%% @copyright 2006 Richard Carlsson 23%% @private 24%% @see eunit 25%% @doc Test runner process tree functions 26 27-module(eunit_proc). 28 29-include("eunit.hrl"). 30-include("eunit_internal.hrl"). 31 32-export([start/4]). 33 34%% This must be exported; see new_group_leader/1 for details. 35-export([group_leader_process/1]). 36 37-record(procstate, {ref, id, super, insulator, parent, order}). 38 39 40%% Spawns test process and returns the process Pid; sends {done, 41%% Reference, Pid} to caller when finished. See the function 42%% wait_for_task/2 for details about the need for the reference. 43%% 44%% The `Super' process receives a stream of status messages; see 45%% message_super/3 for details. 46 47start(Tests, Order, Super, Reference) 48 when is_pid(Super), is_reference(Reference) -> 49 St = #procstate{ref = Reference, 50 id = [], 51 super = Super, 52 order = Order}, 53 spawn_group(local, #group{tests = Tests}, St). 54 55 56%% Status messages sent to the supervisor process. (A supervisor does 57%% not have to act on these messages - it can e.g. just log them, or 58%% even discard them.) Each status message has the following form: 59%% 60%% {status, Id, Info} 61%% 62%% where Id identifies the item that the message pertains to, and the 63%% Info part can be one of: 64%% 65%% {progress, 'begin', {test | group, Data}} 66%% indicates that the item has been entered, and what type it is; 67%% Data is [{desc,binary()}, {source,Source}, {line,integer()}] for 68%% a test, and [{desc,binary()}, {spawn,SpawnType}, 69%% {order,OrderType}] for a group. 70%% 71%% {progress, 'end', {Status, Data}} 72%% Status = 'ok' | {error, Exception} | {skipped, Cause} | integer() 73%% Data = [{time,integer()}, {output,binary()}] 74%% 75%% where Time is measured in milliseconds and Output is the data 76%% written to the standard output stream during the test; if 77%% Status is {skipped, Cause}, then Cause is a term thrown from 78%% eunit_test:run_testfun/1. For a group item, the Status field is 79%% the number of immediate subitems of the group; this helps the 80%% collation of results. Failure for groups is always signalled 81%% through a cancel message, not through the Status field. 82%% 83%% {cancel, Descriptor} 84%% where Descriptor can be: 85%% timeout a timeout occurred 86%% {blame, Id} forced to terminate because of item `Id' 87%% {abort, Cause} the test or group failed to execute 88%% {exit, Reason} the test process terminated unexpectedly 89%% {startup, Reason} failed to start a remote test process 90%% 91%% where Cause is a term thrown from eunit_data:enter_context/4 or 92%% from eunit_data:iter_next/2, and Reason is an exit term from a 93%% crashed process 94%% 95%% Note that due to concurrent (and possibly distributed) execution, 96%% there are *no* strict ordering guarantees on the status messages, 97%% with one exception: a 'begin' message will always arrive before its 98%% corresponding 'end' message. 99 100message_super(Id, Info, St) -> 101 St#procstate.super ! {status, Id, Info}. 102 103 104%% @TODO implement synchronized mode for insulator/child execution 105 106%% Ideas for synchronized mode: 107%% 108%% * At each "program point", i.e., before entering a test, entering a 109%% group, or leaving a group, the child will synchronize with the 110%% insulator to make sure it is ok to proceed. 111%% 112%% * The insulator can receive controlling messages from higher up in 113%% the hierarchy, telling it to pause, resume, single-step, repeat, etc. 114%% 115%% * Synchronization on entering/leaving groups is necessary in order to 116%% get control over things such as subprocess creation/termination and 117%% setup/cleanup, making it possible to, e.g., repeat all the tests 118%% within a particular subprocess without terminating and restarting it, 119%% or repeating tests without repeating the setup/cleanup. 120%% 121%% * Some tests that depend on state will not be possible to repeat, but 122%% require a fresh context setup. There is nothing that can be done 123%% about this, and the many tests that are repeatable should not be 124%% punished because of it. The user must decide which level to restart. 125%% 126%% * Question: How propagate control messages down the hierarchy 127%% (preferably only to the correct insulator process)? An insulator does 128%% not currenctly know whether its child process has spawned subtasks. 129%% (The "supervisor" process does not know the Pids of the controlling 130%% insulator processes in the tree, and it probably should not be 131%% responsible for this anyway.) 132 133 134%% --------------------------------------------------------------------- 135%% Process tree primitives 136 137%% A "task" consists of an insulator process and a child process which 138%% handles the actual work. When the child terminates, the insulator 139%% process sends {done, Reference, self()} to the process which started 140%% the task (the "parent"). The child process is given a State record 141%% which contains the process id:s of the parent, the insulator, and the 142%% supervisor. 143 144%% @spec (Type, (#procstate{}) -> () -> term(), #procstate{}) -> pid() 145%% Type = local | {remote, Node::atom()} 146 147start_task(Type, Fun, St0) -> 148 St = St0#procstate{parent = self()}, 149 %% (note: the link here is mainly to propagate signals *downwards*, 150 %% so that the insulator can detect if the process that started the 151 %% task dies before the task is done) 152 F = fun () -> insulator_process(Type, Fun, St) end, 153 case Type of 154 local -> 155 %% we assume (at least for now) that local spawns can never 156 %% fail in such a way that the process does not start, so a 157 %% new local insulator does not need to synchronize here 158 spawn_link(F); 159 {remote, Node} -> 160 Pid = spawn_link(Node, F), 161 %% See below for the need for the {ok, Reference, Pid} 162 %% message. 163 Reference = St#procstate.ref, 164 Monitor = erlang:monitor(process, Pid), 165 %% (the DOWN message is guaranteed to arrive after any 166 %% messages sent by the process itself) 167 receive 168 {ok, Reference, Pid} -> 169 Pid; 170 {'DOWN', Monitor, process, Pid, Reason} -> 171 %% send messages as if the insulator process was 172 %% started, but terminated on its own accord 173 Msg = {startup, Reason}, 174 message_super(St#procstate.id, {cancel, Msg}, St), 175 self() ! {done, Reference, Pid} 176 end, 177 erlang:demonitor(Monitor, [flush]), 178 Pid 179 end. 180 181%% Relatively simple, and hopefully failure-proof insulator process 182%% (This is cleaner than temporarily setting up the caller to trap 183%% signals, and does not affect the caller's mailbox or other state.) 184%% 185%% We assume that nobody does a 'kill' on an insulator process - if that 186%% should happen, the test framework will hang since the insulator will 187%% never send a reply; see below for more. 188%% 189%% Note that even if the insulator process itself never fails, it is 190%% still possible that it does not start properly, if it is spawned 191%% remotely (e.g., if the remote node is down). Therefore, remote 192%% insulators must always immediately send an {ok, Reference, self()} 193%% message to the parent as soon as it is spawned. 194 195%% @spec (Type, Fun::() -> term(), St::#procstate{}) -> ok 196%% Type = local | {remote, Node::atom()} 197 198insulator_process(Type, Fun, St0) -> 199 process_flag(trap_exit, true), 200 Parent = St0#procstate.parent, 201 if Type =:= local -> ok; 202 true -> Parent ! {ok, St0#procstate.ref, self()} 203 end, 204 St = St0#procstate{insulator = self()}, 205 Child = spawn_link(fun () -> child_process(Fun(St), St) end), 206 insulator_wait(Child, Parent, [], St). 207 208%% Normally, child processes exit with the reason 'normal' even if the 209%% executed tests failed (by throwing exceptions), since the tests are 210%% executed within a try-block. Child processes can terminate abnormally 211%% by the following reasons: 212%% 1) an error in the processing of the test descriptors (a malformed 213%% descriptor, failure in a setup, cleanup or initialization, a 214%% missing module or function, or a failing generator function); 215%% 2) an internal error in the test running framework itself; 216%% 3) receiving a non-trapped error signal as a consequence of running 217%% test code. 218%% Those under point 1 are "expected errors", handled specially in the 219%% protocol, while the other two are unexpected errors. (Since alt. 3 220%% implies that the test neither reported success nor failure, it can 221%% never be considered "proper" behaviour of a test.) Abnormal 222%% termination is reported to the supervisor process but otherwise does 223%% not affect the insulator compared to normal termination. Child 224%% processes can also be killed abruptly by their insulators, in case of 225%% a timeout or if a parent process dies. 226%% 227%% The insulator is the group leader for the child process, and gets all 228%% of its standard I/O. The output is buffered and associated with the 229%% currently active test or group, and is sent along with the 'end' 230%% progress message when the test or group has finished. 231 232insulator_wait(Child, Parent, Buf, St) -> 233 receive 234 {child, Child, Id, {'begin', Type, Data}} -> 235 message_super(Id, {progress, 'begin', {Type, Data}}, St), 236 insulator_wait(Child, Parent, [[] | Buf], St); 237 {child, Child, Id, {'end', Status, Time}} -> 238 Data = [{time, Time}, {output, lists:reverse(hd(Buf))}], 239 message_super(Id, {progress, 'end', {Status, Data}}, St), 240 insulator_wait(Child, Parent, tl(Buf), St); 241 {child, Child, Id, {skipped, Reason}} -> 242 %% this happens when a subgroup fails to enter the context 243 message_super(Id, {cancel, {abort, Reason}}, St), 244 insulator_wait(Child, Parent, Buf, St); 245 {child, Child, Id, {abort, Cause}} -> 246 %% this happens when the child code threw an internal 247 %% eunit_abort; the child process has already exited 248 exit_messages(Id, {abort, Cause}, St), 249 %% no need to wait for the {'EXIT',Child,_} message 250 terminate_insulator(St); 251 {io_request, Child, ReplyAs, Req} -> 252 %% we only collect output from the child process itself, not 253 %% from secondary processes, otherwise we get race problems; 254 %% however, each test runs its personal group leader that 255 %% funnels all output - see the run_test() function 256 Buf1 = io_request(Child, ReplyAs, Req, hd(Buf)), 257 insulator_wait(Child, Parent, [Buf1 | tl(Buf)], St); 258 {io_request, From, ReplyAs, Req} when is_pid(From) -> 259 %% (this shouldn't happen anymore, but we keep it safe) 260 %% just ensure the sender gets a reply; ignore the data 261 io_request(From, ReplyAs, Req, []), 262 insulator_wait(Child, Parent, Buf, St); 263 {timeout, Child, Id} -> 264 exit_messages(Id, timeout, St), 265 kill_task(Child, St); 266 {'EXIT', Child, normal} -> 267 terminate_insulator(St); 268 {'EXIT', Child, Reason} -> 269 exit_messages(St#procstate.id, {exit, Reason}, St), 270 terminate_insulator(St); 271 {'EXIT', Parent, _} -> 272 %% make sure child processes are cleaned up recursively 273 kill_task(Child, St) 274 end. 275 276-spec kill_task(_, _) -> no_return(). 277 278kill_task(Child, St) -> 279 exit(Child, kill), 280 terminate_insulator(St). 281 282%% Unlinking before exit avoids polluting the parent process with exit 283%% signals from the insulator. The child process is already dead here. 284 285terminate_insulator(St) -> 286 %% messaging/unlinking is ok even if the parent is already dead 287 Parent = St#procstate.parent, 288 Parent ! {done, St#procstate.ref, self()}, 289 unlink(Parent), 290 exit(normal). 291 292%% send cancel messages for the Id of the "causing" item, and also for 293%% the Id of the insulator itself, if they are different 294exit_messages(Id, Cause, St) -> 295 %% the message for the most specific Id is always sent first 296 message_super(Id, {cancel, Cause}, St), 297 case St#procstate.id of 298 Id -> ok; 299 Id1 -> message_super(Id1, {cancel, {blame, Id}}, St) 300 end. 301 302%% Child processes send all messages via the insulator to ensure proper 303%% sequencing with timeouts and exit signals. 304 305message_insulator(Data, St) -> 306 St#procstate.insulator ! {child, self(), St#procstate.id, Data}. 307 308%% Timeout handling 309 310set_timeout(Time, St) -> 311 erlang:send_after(Time, St#procstate.insulator, 312 {timeout, self(), St#procstate.id}). 313 314clear_timeout(Ref) -> 315 erlang:cancel_timer(Ref). 316 317with_timeout(undefined, Default, F, St) -> 318 with_timeout(Default, F, St); 319with_timeout(Time, _Default, F, St) -> 320 with_timeout(Time, F, St). 321 322with_timeout(infinity, F, _St) -> 323 %% don't start timers unnecessarily 324 {T0, _} = statistics(wall_clock), 325 Value = F(), 326 {T1, _} = statistics(wall_clock), 327 {Value, T1 - T0}; 328with_timeout(Time, F, St) when is_integer(Time), Time > 16#FFFFffff -> 329 with_timeout(16#FFFFffff, F, St); 330with_timeout(Time, F, St) when is_integer(Time), Time < 0 -> 331 with_timeout(0, F, St); 332with_timeout(Time, F, St) when is_integer(Time) -> 333 Ref = set_timeout(Time, St), 334 {T0, _} = statistics(wall_clock), 335 try F() of 336 Value -> 337 %% we could also read the timer, but this is simpler 338 {T1, _} = statistics(wall_clock), 339 {Value, T1 - T0} 340 after 341 clear_timeout(Ref) 342 end. 343 344%% The normal behaviour of a child process is not to trap exit 345%% signals. The testing framework is not dependent on this, however, so 346%% the test code is allowed to enable signal trapping as it pleases. 347%% Note that I/O is redirected to the insulator process. 348 349%% @spec (() -> term(), #procstate{}) -> ok 350 351child_process(Fun, St) -> 352 group_leader(St#procstate.insulator, self()), 353 try Fun() of 354 _ -> ok 355 catch 356 %% the only "normal" way for a child process to bail out (e.g, 357 %% when not being able to parse the test descriptor) is to throw 358 %% an {eunit_abort, Reason} exception; any other exception will 359 %% be reported as an unexpected termination of the test 360 {eunit_abort, Cause} -> 361 message_insulator({abort, Cause}, St), 362 exit(aborted) 363 end. 364 365-ifdef(TEST). 366child_test_() -> 367 [{"test processes do not trap exit signals", 368 ?_assertMatch(false, process_flag(trap_exit, false))}]. 369-endif. 370 371%% @throws abortException() 372%% @type abortException() = {eunit_abort, Cause::term()} 373 374abort_task(Cause) -> 375 throw({eunit_abort, Cause}). 376 377%% Typically, the process that executes this code is not trapping 378%% signals, but it might be - it is outside of our control, since test 379%% code can enable or disable trapping at will. That we cannot rely on 380%% process links here, is why the insulator process of a task must be 381%% guaranteed to always send a reply before it terminates. 382%% 383%% The unique reference guarantees that we don't extract any message 384%% from the mailbox unless it belongs to the test framework (and not to 385%% the running tests) - it is not possible to use selective receive to 386%% match only messages that are tagged with some pid out of a 387%% dynamically varying set of pids. When the wait-loop terminates, no 388%% such message should remain in the mailbox. 389 390wait_for_task(Pid, St) -> 391 wait_for_tasks(sets:from_list([Pid]), St). 392 393wait_for_tasks(PidSet, St) -> 394 case sets:size(PidSet) of 395 0 -> 396 ok; 397 _ -> 398 %% (note that when we receive this message for some task, we 399 %% are guaranteed that the insulator process of the task has 400 %% already informed the supervisor about any anomalies) 401 Reference = St#procstate.ref, 402 receive 403 {done, Reference, Pid} -> 404 %% (if Pid is not in the set, del_element has no 405 %% effect, so this is always safe) 406 Rest = sets:del_element(Pid, PidSet), 407 wait_for_tasks(Rest, St) 408 end 409 end. 410 411%% --------------------------------------------------------------------- 412%% Separate testing process 413 414%% TODO: Ability to stop after N failures. 415%% TODO: Flow control, starting new job as soon as slot is available 416 417tests(T, St) -> 418 I = eunit_data:iter_init(T, St#procstate.id), 419 case St#procstate.order of 420 inorder -> tests_inorder(I, St); 421 inparallel -> tests_inparallel(I, 0, St); 422 {inparallel, N} when is_integer(N), N >= 0 -> 423 tests_inparallel(I, N, St) 424 end. 425 426set_id(I, St) -> 427 St#procstate{id = eunit_data:iter_id(I)}. 428 429tests_inorder(I, St) -> 430 tests_inorder(I, 0, St). 431 432tests_inorder(I, N, St) -> 433 case get_next_item(I) of 434 {T, I1} -> 435 handle_item(T, set_id(I1, St)), 436 tests_inorder(I1, N+1, St); 437 none -> 438 N % the return status of a group is the subtest count 439 end. 440 441tests_inparallel(I, K0, St) -> 442 tests_inparallel(I, 0, St, K0, K0, sets:new()). 443 444tests_inparallel(I, N, St, K, K0, Children) when K =< 0, K0 > 0 -> 445 wait_for_tasks(Children, St), 446 tests_inparallel(I, N, St, K0, K0, sets:new()); 447tests_inparallel(I, N, St, K, K0, Children) -> 448 case get_next_item(I) of 449 {T, I1} -> 450 Child = spawn_item(T, set_id(I1, St)), 451 tests_inparallel(I1, N+1, St, K - 1, K0, 452 sets:add_element(Child, Children)); 453 none -> 454 wait_for_tasks(Children, St), 455 N % the return status of a group is the subtest count 456 end. 457 458%% this starts a new separate task for an inparallel-item (which might 459%% be a group and in that case might cause yet another spawn in the 460%% handle_group() function, but it might also be just a single test) 461spawn_item(T, St0) -> 462 Fun = fun (St) -> 463 fun () -> handle_item(T, St) end 464 end, 465 %% inparallel-items are always spawned locally 466 start_task(local, Fun, St0). 467 468get_next_item(I) -> 469 try eunit_data:iter_next(I) 470 catch 471 Term -> abort_task(Term) 472 end. 473 474handle_item(T, St) -> 475 case T of 476 #test{} -> handle_test(T, St); 477 #group{} -> handle_group(T, St) 478 end. 479 480handle_test(T, St) -> 481 Data = [{desc, T#test.desc}, {source, T#test.location}, 482 {line, T#test.line}], 483 message_insulator({'begin', test, Data}, St), 484 485 %% each test case runs under a fresh group leader process 486 G0 = group_leader(), 487 Runner = self(), 488 G1 = new_group_leader(Runner), 489 group_leader(G1, self()), 490 491 %% run the actual test, handling timeouts and getting the total run 492 %% time of the test code (and nothing else) 493 {Status, Time} = with_timeout(T#test.timeout, ?DEFAULT_TEST_TIMEOUT, 494 fun () -> run_test(T) end, St), 495 496 %% restore group leader, get the collected output, and re-emit it so 497 %% that it all seems to come from this process, and always comes 498 %% before the 'end' message for this test 499 group_leader(G0, self()), 500 Output = group_leader_sync(G1), 501 io:put_chars(Output), 502 503 message_insulator({'end', Status, Time}, St), 504 ok. 505 506%% @spec (#test{}) -> ok | {error, eunit_lib:exception()} 507%% | {skipped, eunit_test:wrapperError()} 508 509run_test(#test{f = F}) -> 510 try eunit_test:run_testfun(F) of 511 {ok, _Value} -> 512 %% just discard the return value 513 ok; 514 {error, Exception} -> 515 {error, Exception} 516 catch 517 throw:WrapperError -> {skipped, WrapperError} 518 end. 519 520set_group_order(#group{order = undefined}, St) -> 521 St; 522set_group_order(#group{order = Order}, St) -> 523 St#procstate{order = Order}. 524 525handle_group(T, St0) -> 526 St = set_group_order(T, St0), 527 case T#group.spawn of 528 undefined -> 529 run_group(T, St); 530 Type -> 531 Child = spawn_group(Type, T, St), 532 wait_for_task(Child, St) 533 end. 534 535spawn_group(Type, T, St0) -> 536 Fun = fun (St) -> 537 fun () -> run_group(T, St) end 538 end, 539 start_task(Type, Fun, St0). 540 541run_group(T, St) -> 542 %% note that the setup/cleanup is outside the group timeout; if the 543 %% setup fails, we do not start any timers 544 Timeout = T#group.timeout, 545 Data = [{desc, T#group.desc}, {spawn, T#group.spawn}, 546 {order, T#group.order}], 547 message_insulator({'begin', group, Data}, St), 548 F = fun (G) -> enter_group(G, Timeout, St) end, 549 try with_context(T, F) of 550 {Status, Time} -> 551 message_insulator({'end', Status, Time}, St) 552 catch 553 %% a throw here can come from eunit_data:enter_context/4 or from 554 %% get_next_item/1; for context errors, report group as aborted, 555 %% but continue processing tests 556 {context_error, Why, Trace} -> 557 message_insulator({skipped, {Why, Trace}}, St) 558 end, 559 ok. 560 561enter_group(T, Timeout, St) -> 562 with_timeout(Timeout, ?DEFAULT_GROUP_TIMEOUT, 563 fun () -> tests(T, St) end, St). 564 565with_context(#group{context = undefined, tests = T}, F) -> 566 F(T); 567with_context(#group{context = #context{} = C, tests = I}, F) -> 568 eunit_data:enter_context(C, I, F). 569 570%% Group leader process for test cases - collects I/O output requests. 571 572new_group_leader(Runner) -> 573 %% We must use spawn/3 here (with explicit module and function 574 %% name), because the 'current function' status of the group leader 575 %% is used by the UNDER_EUNIT macro (in eunit.hrl). If we spawn 576 %% using a fun, the current function will be 'erlang:apply/2' during 577 %% early process startup, which will fool the macro. 578 spawn_link(?MODULE, group_leader_process, [Runner]). 579 580group_leader_process(Runner) -> 581 group_leader_loop(Runner, infinity, []). 582 583group_leader_loop(Runner, Wait, Buf) -> 584 receive 585 {io_request, From, ReplyAs, Req} -> 586 P = process_flag(priority, normal), 587 %% run this part under normal priority always 588 Buf1 = io_request(From, ReplyAs, Req, Buf), 589 process_flag(priority, P), 590 group_leader_loop(Runner, Wait, Buf1); 591 stop -> 592 %% quitting time: make a minimal pause, go low on priority, 593 %% set receive-timeout to zero and schedule out again 594 receive after 2 -> ok end, 595 process_flag(priority, low), 596 group_leader_loop(Runner, 0, Buf); 597 _ -> 598 %% discard any other messages 599 group_leader_loop(Runner, Wait, Buf) 600 after Wait -> 601 %% no more messages and nothing to wait for; we ought to 602 %% have collected all immediately pending output now 603 process_flag(priority, normal), 604 Runner ! {self(), lists:reverse(Buf)} 605 end. 606 607group_leader_sync(G) -> 608 G ! stop, 609 receive 610 {G, Buf} -> Buf 611 end. 612 613%% Implementation of buffering I/O for group leader processes. (Note that 614%% each batch of characters is just pushed on the buffer, so it needs to 615%% be reversed when it is flushed.) 616 617io_request(From, ReplyAs, Req, Buf) -> 618 {Reply, Buf1} = io_request(Req, Buf), 619 io_reply(From, ReplyAs, Reply), 620 Buf1. 621 622io_reply(From, ReplyAs, Reply) -> 623 From ! {io_reply, ReplyAs, Reply}. 624 625io_request({put_chars, Chars}, Buf) -> 626 {ok, [Chars | Buf]}; 627io_request({put_chars, M, F, As}, Buf) -> 628 try apply(M, F, As) of 629 Chars -> {ok, [Chars | Buf]} 630 catch 631 C:T:S -> {{error, {C,T,S}}, Buf} 632 end; 633io_request({put_chars, _Enc, Chars}, Buf) -> 634 io_request({put_chars, Chars}, Buf); 635io_request({put_chars, _Enc, Mod, Func, Args}, Buf) -> 636 io_request({put_chars, Mod, Func, Args}, Buf); 637io_request({get_chars, _Enc, _Prompt, _N}, Buf) -> 638 {eof, Buf}; 639io_request({get_chars, _Prompt, _N}, Buf) -> 640 {eof, Buf}; 641io_request({get_line, _Prompt}, Buf) -> 642 {eof, Buf}; 643io_request({get_line, _Enc, _Prompt}, Buf) -> 644 {eof, Buf}; 645io_request({get_until, _Prompt, _M, _F, _As}, Buf) -> 646 {eof, Buf}; 647io_request({get_until, _Enc, _Prompt, _M, _F, _As}, Buf) -> 648 {eof, Buf}; 649io_request({setopts, _Opts}, Buf) -> 650 {ok, Buf}; 651io_request(getopts, Buf) -> 652 {{error, enotsup}, Buf}; 653io_request({get_geometry,columns}, Buf) -> 654 {{error, enotsup}, Buf}; 655io_request({get_geometry,rows}, Buf) -> 656 {{error, enotsup}, Buf}; 657io_request({requests, Reqs}, Buf) -> 658 io_requests(Reqs, {ok, Buf}); 659io_request(_, Buf) -> 660 {{error, request}, Buf}. 661 662io_requests([R | Rs], {ok, Buf}) -> 663 io_requests(Rs, io_request(R, Buf)); 664io_requests(_, Result) -> 665 Result. 666 667-ifdef(TEST). 668io_error_test_() -> 669 [?_assertMatch({error, enotsup}, io:getopts()), 670 ?_assertMatch({error, enotsup}, io:columns()), 671 ?_assertMatch({error, enotsup}, io:rows())]. 672-endif. 673