diff options
Diffstat (limited to 'lib/kernel/test/interactive_shell_SUITE.erl')
-rw-r--r-- | lib/kernel/test/interactive_shell_SUITE.erl | 2740 |
1 files changed, 1938 insertions, 802 deletions
diff --git a/lib/kernel/test/interactive_shell_SUITE.erl b/lib/kernel/test/interactive_shell_SUITE.erl index ec3887df1d..b413800661 100644 --- a/lib/kernel/test/interactive_shell_SUITE.erl +++ b/lib/kernel/test/interactive_shell_SUITE.erl @@ -1,7 +1,7 @@ %% %% %CopyrightBegin% %% -%% Copyright Ericsson AB 2007-2022. All Rights Reserved. +%% Copyright Ericsson AB 2007-2023. 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. @@ -19,6 +19,21 @@ %% -module(interactive_shell_SUITE). -include_lib("kernel/include/file.hrl"). +-include_lib("common_test/include/ct.hrl"). + +%% Things to add tests for: +%% - TERM=dumb +%% - Editing line > MAXSIZE (1 << 16) +%% - \t tests (use io:format("\t")) +%% - xn fix after Delete and Backspace +%% - octal_to_hex > 255 length (is this possible?) +%% 1222 0 : } else if (lastput == 0) { /* A multibyte UTF8 character */ +%% 1223 0 : for (i = 0; i < ubytes; ++i) { +%% 1224 0 : outc(ubuf[i]); +%% 1225 : } +%% 1226 : } else { +%% 1227 0 : outc(lastput); +%% - $TERM set to > 1024 long value -export([all/0, suite/0, groups/0, init_per_suite/1, end_per_suite/1, init_per_group/2, end_per_group/2, @@ -31,27 +46,45 @@ shell_history_custom/1, shell_history_custom_errors/1, job_control_remote_noshell/1,ctrl_keys/1, get_columns_and_rows_escript/1, - remsh_basic/1, remsh_longnames/1, remsh_no_epmd/1]). + shell_navigation/1, shell_multiline_navigation/1, shell_xnfix/1, shell_delete/1, + shell_transpose/1, shell_search/1, shell_insert/1, + shell_update_window/1, shell_small_window_multiline_navigation/1, shell_huge_input/1, + shell_invalid_unicode/1, shell_support_ansi_input/1, + shell_invalid_ansi/1, shell_suspend/1, shell_full_queue/1, + shell_unicode_wrap/1, shell_delete_unicode_wrap/1, + shell_delete_unicode_not_at_cursor_wrap/1, + shell_expand_location_above/1, + shell_expand_location_below/1, + shell_update_window_unicode_wrap/1, + shell_standard_error_nlcr/1, shell_clear/1, + remsh_basic/1, remsh_error/1, remsh_longnames/1, remsh_no_epmd/1, + remsh_expand_compatibility_25/1, remsh_expand_compatibility_later_version/1, + external_editor/1, external_editor_visual/1, + external_editor_unicode/1, shell_ignore_pager_commands/1]). -%% For spawn --export([toerl_server/3]). %% Exports for custom shell history module -export([load/0, add/1]). - +%% For custom prompt testing +-export([prompt/1]). +-record(tmux, {peer, node, name, orig_location }). suite() -> [{ct_hooks,[ts_install_cth]}, {timetrap,{minutes,3}}]. all() -> - [get_columns_and_rows_escript,get_columns_and_rows, - exit_initial, job_control_local, - job_control_remote, job_control_remote_noshell, - ctrl_keys, stop_during_init, wrap, - {group, shell_history}, - {group, remsh}]. + [{group, to_erl}, + {group, tty}]. groups() -> - [{shell_history, [], + [{to_erl,[], + [get_columns_and_rows_escript,get_columns_and_rows, + exit_initial, job_control_local, + job_control_remote, job_control_remote_noshell, + ctrl_keys, stop_during_init, wrap, + shell_invalid_ansi, + {group, shell_history}, + {group, remsh}]}, + {shell_history, [], [shell_history, shell_history_resize, shell_history_eaccess, @@ -65,26 +98,60 @@ groups() -> shell_history_custom_errors]}, {remsh, [], [remsh_basic, + remsh_error, remsh_longnames, - remsh_no_epmd]} + remsh_no_epmd, + remsh_expand_compatibility_25, + remsh_expand_compatibility_later_version]}, + {tty,[], + [{group,tty_unicode}, + {group,tty_latin1}, + shell_suspend, + shell_full_queue, + external_editor, + external_editor_visual, + shell_ignore_pager_commands + ]}, + {tty_unicode,[parallel], + [{group,tty_tests}, + shell_invalid_unicode, + external_editor_unicode + %% unicode wrapping does not work right yet + %% shell_unicode_wrap, + %% shell_delete_unicode_wrap, + %% shell_delete_unicode_not_at_cursor_wrap, + %% shell_update_window_unicode_wrap + ]}, + {tty_latin1,[],[{group,tty_tests}]}, + {tty_tests, [parallel], + [shell_navigation, shell_multiline_navigation, shell_xnfix, shell_delete, + shell_transpose, shell_search, shell_insert, + shell_update_window, shell_small_window_multiline_navigation, shell_huge_input, + shell_support_ansi_input, + shell_standard_error_nlcr, + shell_expand_location_above, + shell_expand_location_below, + shell_clear]} ]. init_per_suite(Config) -> - case get_progs() of - {error, Error} -> - {skip, Error}; - _ -> - Term = os:getenv("TERM", "dumb"), - os:putenv("TERM", "vt100"), - DefShell = get_default_shell(), - [{default_shell,DefShell},{term,Term}|Config] - end. + Term = os:getenv("TERM", "dumb"), + os:putenv("TERM", "vt100"), + [{term,Term}|Config]. end_per_suite(Config) -> Term = proplists:get_value(term,Config), os:putenv("TERM",Term), ok. +init_per_group(to_erl, Config) -> + case rtnode:get_progs() of + {error, Error} -> + {skip, Error}; + _ -> + DefShell = rtnode:get_default_shell(), + [{default_shell,DefShell}|Config] + end; init_per_group(remsh, Config) -> case proplists:get_value(default_shell, Config) of old -> {skip, "Not supported in old shell"}; @@ -95,13 +162,37 @@ init_per_group(shell_history, Config) -> old -> {skip, "Not supported in old shell"}; new -> Config end; +init_per_group(tty, Config) -> + case string:split(tmux("-V")," ") of + ["tmux",[Num,$.|_]] when Num >= $3, Num =< $9 -> + tmux("kill-session"), + "" = tmux("-u new-session -x 50 -y 60 -d"), + ["" = tmux(["set-environment '",Name,"' '",Value,"'"]) + || {Name,Value} <- os:env()], + Config; + ["tmux", Vsn] -> + {skip, "invalid tmux version " ++ Vsn ++ ". Need vsn 3 or later"}; + Error -> + {skip, "tmux not installed " ++ Error} + end; +init_per_group(Group, Config) when Group =:= tty_unicode; + Group =:= tty_latin1 -> + [Lang,_] = + string:split( + os:getenv("LC_ALL", + os:getenv("LC_CTYPE", + os:getenv("LANG","en_US.UTF-8"))),"."), + case Group of + tty_unicode -> + [{encoding, unicode},{env,[{"LC_ALL",Lang++".UTF-8"}]}|Config]; + tty_latin1 -> + % [{encoding, latin1},{env,[{"LC_ALL",Lang++".ISO-8859-1"}]}|Config], + {skip, "latin1 tests not implemented yet"} + end; init_per_group(sh_custom, Config) -> - %% Ensure that ERL_AFLAGS will not override the value of the - %% shell_history variable. - Name = interactive_shell_sh_custom, - Args = "-noshell -kernel shell_history not_overridden", - {ok, Node} = test_server:start_node(Name, slave, [{args,Args}]), - try erpc:call(Node, application, get_env, [kernel, shell_history], timeout(normal)) of + %% Ensure that ERL_AFLAGS will not override the value of the shell_history variable. + {ok, Peer, Node} = ?CT_PEER(["-noshell","-kernel","shell_history","not_overridden"]), + try erpc:call(Node, application, get_env, [kernel, shell_history], rtnode:timeout(normal)) of {ok, not_overridden} -> Config; _ -> @@ -112,23 +203,45 @@ init_per_group(sh_custom, Config) -> io:format("~p\n~p\n~p\n", [C,R,Stk]), {skip, "Unexpected error"} after - test_server:stop_node(Node) + peer:stop(Peer) end; init_per_group(_GroupName, Config) -> Config. +end_per_group(tty, _Config) -> + Windows = string:split(tmux("list-windows"), "\n", all), + lists:foreach( + fun(W) -> + case string:split(W, " ", all) of + ["0:" | _] -> ok; + [No, _Name | _] -> + "" = os:cmd(["tmux select-window -t ", string:split(No,":")]), + ct:log("~ts~n~ts",[W, os:cmd(lists:concat(["tmux capture-pane -p -e"]))]) + end + end, Windows), +% "" = os:cmd("tmux kill-session") + ok; end_per_group(_GroupName, Config) -> Config. -init_per_testcase(_Func, Config) -> - Config. - -end_per_testcase(_Case, _Config) -> - %% Terminate any connected nodes. They may disturb test cases that follow. - lists:foreach(fun(Node) -> - catch erpc:call(Node, erlang, halt, []) - end, nodes()), - ok. +init_per_testcase(Func, Config) -> + Path = [Func, + [proplists:get_value(name,P) || + P <- [proplists:get_value(tc_group_properties,Config,[])] ++ + proplists:get_value(tc_group_path,Config,[])]], + [{tc_path, lists:concat(lists:join("-",lists:flatten(Path)))} | Config]. + +end_per_testcase(_Case, Config) -> + GroupProperties = proplists:get_value(tc_group_properties, Config), + case (GroupProperties =/= false) andalso proplists:get_value(parallel, GroupProperties, false) of + true -> ok; + false -> + %% Terminate any connected nodes. They may disturb test cases that follow. + lists:foreach(fun(Node) -> + catch erpc:call(Node, erlang, halt, []) + end, nodes()), + ok + end. %%-define(DEBUG,1). -ifdef(DEBUG). @@ -155,7 +268,7 @@ run_unbuffer_escript(Rows, Columns, EScript, NoTermStdIn, NoTermStdOut) -> {true, true} -> io_lib:format(" > ~s < ~s ; cat ~s", [TmpFile, TmpFile, TmpFile]) end, Command = io_lib:format("unbuffer -p bash -c \"stty rows ~p; stty columns ~p; escript ~s ~s\"", - [Rows, Columns, EScript, CommandModifier]), + [Rows, Columns, EScript, CommandModifier]), %% io:format("Command: ~s ~n", [Command]), Out = os:cmd(Command), %% io:format("Out: ~p ~n", [Out]), @@ -203,50 +316,1396 @@ get_columns_and_rows(Config) when is_list(Config) -> ok. test_columns_and_rows(old, Args) -> - rtnode([{putline, ""}, - {putline, "2."}, - {expect, "2\r\n"}, - {putline, "io:columns()."}, - {expect, "{error,enotsup}\r\n"}, - {putline, "io:rows()."}, - {expect, "{error,enotsup}\r\n"} - ], [], [], Args), - - rtnode([{putline, ""}, - {putline, "2."}, - {expect, "2\r\n"}, - {putline, "io:columns()."}, - {expect, "{ok,90}\r\n"}, - {putline,"io:rows()."}, - {expect, "{ok,40}\r\n"}], - [], - "stty rows 40; stty columns 90; ", - Args); + rtnode:run( + [{putline, ""}, + {putline, "2."}, + {expect, "2\r\n"}, + {putline, "io:columns()."}, + {expect, "{error,enotsup}\r\n"}, + {putline, "io:rows()."}, + {expect, "{error,enotsup}\r\n"} + ], [], [], Args), + + rtnode:run( + [{putline, ""}, + {putline, "2."}, + {expect, "2\r\n"}, + {putline, "io:columns()."}, + {expect, "{ok,90}\r\n"}, + {putline,"io:rows()."}, + {expect, "{ok,40}\r\n"}], + [], + "stty rows 40; stty columns 90; ", + Args); test_columns_and_rows(new, _Args) -> - rtnode([{putline, ""}, - {expect, "1> $"}, - {putline, "2."}, - {expect, "\r\n2\r\n"}, - {expect, "> $"}, - {putline, "io:columns()."}, - {expect, "{ok,80}\r\n"}, - {expect, "> $"}, - {putline, "io:rows()."}, - {expect, "\r\n{ok,24}\r\n"} - ]), - - rtnode([{putline, ""}, - {expect, "1> $"}, - {putline, "2."}, - {expect, "\r\n2\r\n"}, - {expect, "> $"}, - {putline, "io:columns()."}, - {expect, "\r\n{ok,90}\r\n"}, - {expect, "> $"}, - {putline, "io:rows()."}, - {expect, "\r\n{ok,40}\r\n"}], - [], - "stty rows 40; stty columns 90; "). + rtnode:run( + [{putline, ""}, + {expect, "1> $"}, + {putline, "2."}, + {expect, "\r\n2\r\n"}, + {expect, "> $"}, + {putline, "io:columns()."}, + {expect, "{ok,80}\r\n"}, + {expect, "> $"}, + {putline, "io:rows()."}, + {expect, "\r\n{ok,24}\r\n"} + ]), + + rtnode:run( + [{putline, ""}, + {expect, "1> $"}, + {putline, "2."}, + {expect, "\r\n2\r\n"}, + {expect, "> $"}, + {putline, "io:columns()."}, + {expect, "\r\n{ok,90}\r\n"}, + {expect, "> $"}, + {putline, "io:rows()."}, + {expect, "\r\n{ok,40}\r\n"}], + [], + "stty rows 40; stty columns 90; "). + +shell_navigation(Config) -> + + Term = start_tty(Config), + + try + [begin + send_tty(Term,"{aaa,'b"++U++"b',ccc}"), + check_location(Term, {0, 0}), %% Check that cursor jump backward + check_content(Term, "{aaa,'b"++U++"b',ccc}$"), + timer:sleep(1000), %% Wait for cursor to jump back + check_location(Term, {0, width("{aaa,'b"++U++"b',ccc}")}), + send_tty(Term,"Home"), + check_location(Term, {0, 0}), + send_tty(Term,"End"), + check_location(Term, {0, width("{aaa,'b"++U++"b',ccc}")}), + send_tty(Term,"Left"), + check_location(Term, {0, width("{aaa,'b"++U++"b',ccc")}), + send_tty(Term,"C-Left"), + check_location(Term, {0, width("{aaa,'b"++U++"b',")}), + send_tty(Term,"C-Left"), + check_location(Term, {0, width("{aaa,")}), + send_tty(Term,"C-Right"), + check_location(Term, {0, width("{aaa,'b"++U++"b'")}), + send_tty(Term,"C-Left"), + check_location(Term, {0, width("{aaa,")}), + send_tty(Term,"C-Left"), + check_location(Term, {0, width("{")}), + send_tty(Term,"C-Left"), + check_location(Term, {0, 0}), + send_tty(Term,"C-E"), + check_location(Term, {0, width("{aaa,'b"++U++"b',ccc}")}), + send_tty(Term,"C-A"), + check_location(Term, {0, 0}), + send_tty(Term,"Enter") + end || U <- hard_unicode()], + ok + after + stop_tty(Term) + end. +shell_multiline_navigation(Config) -> + Term = start_tty(Config), + + try + [begin + check_location(Term, {0, 0}), + send_tty(Term,"{aaa,"), + check_location(Term, {0,width("{aaa,")}), + send_tty(Term,"\n'b"++U++"b',"), + check_location(Term, {0, width("'b"++U++"b',")}), + send_tty(Term,"\nccc}"), + check_location(Term, {-2, 0}), %% Check that cursor jump backward (blink) + timer:sleep(1000), %% Wait for cursor to jump back + check_location(Term, {0, width("ccc}")}), + send_tty(Term,"Home"), + check_location(Term, {0, 0}), + send_tty(Term,"End"), + check_location(Term, {0, width("ccc}")}), + send_tty(Term,"Left"), + check_location(Term, {0, width("ccc")}), + send_tty(Term,"C-Left"), + check_location(Term, {0, 0}), + send_tty(Term,"C-Left"), + check_location(Term, {-1, width("'b"++U++"b',")}), + send_tty(Term,"C-Left"), + check_location(Term, {-1, 0}), + %send_tty(Term,"C-Left"), + %check_location(Term, {-1, 0}), + %send_tty(Term,"C-Right"), + %check_location(Term, {-1, 1}), + send_tty(Term,"C-Right"), + check_location(Term, {-1, width("'b"++U++"b'")}), + send_tty(Term,"C-Up"), + check_location(Term, {-2, width("{aaa,")}), + send_tty(Term,"C-Down"), + send_tty(Term,"C-Down"), + check_location(Term, {0, width("ccc}")}), + send_tty(Term,"Left"), + send_tty(Term,"C-Up"), + check_location(Term, {-1, width("'b"++U)}), + send_tty(Term,"M-<"), + check_location(Term, {-2, 0}), + send_tty(Term,"M->"), + send_tty(Term,"Left"), + check_location(Term, {0,width("ccc")}), + send_tty(Term,"Enter"), + send_tty(Term,"Right"), + check_location(Term, {0,0}), + send_tty(Term,"C-h"), % Backspace + check_location(Term, {-1,width("ccc}")}), + send_tty(Term,"Left"), + send_tty(Term,"M-Enter"), + send_tty(Term,"Right"), + check_location(Term, {0,1}), + send_tty(Term,"M-c"), + check_location(Term, {-3,0}), + send_tty(Term,"{'"++U++"',\n\n\nworks}.\n") + end || U <- hard_unicode()], + ok + after + stop_tty(Term) + end. +shell_clear(Config) -> + + Term = start_tty(Config), + {Rows, _Cols} = get_window_size(Term), + + try + send_tty(Term,"foobar."), + send_tty(Term,"Enter"), + check_content(Term, "foobar"), + check_location(Term, {0, 0}), + send_tty(Term,"bazbat"), + check_location(Term, {0, 6}), + send_tty(Term,"C-L"), + check_location(Term, {-Rows+1, 6}), + check_content(Term, "bazbat") + after + stop_tty(Term) + end. + +shell_xnfix(Config) -> + + Term = start_tty(Config), + + {_Rows, Cols} = get_window_size(Term), + {_Row, Col} = get_location(Term), + + As = lists:duplicate(Cols - Col - 1,"a"), + + try + [begin + check_location(Term, {0, 0}), + send_tty(Term,As), + check_content(Term,[As,$$]), + check_location(Term, {0, Cols - Col - 1}), + send_tty(Term,"a"), + check_location(Term, {0, -Col}), + send_tty(Term,"aaa"), + check_location(Term, {0, -Col + 3}), + [send_tty(Term,"Left") || _ <- lists:seq(1,3 + width(U))], + send_tty(Term,U), + %% a{Cols-1}U\naaaaa + check_content(Term,[lists:duplicate(Cols - Col - 1 - width(U),$a), + U,"\n",lists:duplicate(3+width(U), $a),"$"]), + check_location(Term, {0, -Col}), + send_tty(Term,"Left"), + send_tty(Term,U), + %% a{Cols-1}U\nUaaaaa + check_content(Term,[lists:duplicate(Cols - Col - 1 - width(U),$a), + U,"\n",U,lists:duplicate(3+width(U), $a),"$"]), + check_location(Term, {0, -Col}), + %% send_tty(Term,"Left"), + %% send_tty(Term,"BSpace"), + %% a{Cols-2}U\nUaaaaa + %% check_content(Term,[lists:duplicate(Cols - Col - 2 - width(U),$a), + %% U,"\n",U,lists:duplicate(3+width(U), $a),"$"]), + %% send_tty(Term,"BSpace"), + %% check_content(Term,[lists:duplicate(Cols - Col - 1 - width(U),$a), + %% U,U,"\n",lists:duplicate(3+width(U), $a),"$"]), + %% send_tty(Term,"aa"), + %% check_content(Term,[lists:duplicate(Cols - Col - 2 - width(U),$a), + %% U,"a\n",U,lists:duplicate(3+width(U), $a),"$"]), + %% check_location(Term, {0, -Col}), + send_tty(Term,"C-K"), + check_location(Term, {0, -Col}), + send_tty(Term,"C-A"), + check_location(Term, {-1, 0}), + send_tty(Term,"C-E"), + check_location(Term, {0, -Col}), + send_tty(Term,"Enter"), + ok + end || U <- hard_unicode()] + after + stop_tty(Term) + end. + + +%% Characters that are larger than 2 wide need special handling when they +%% are at the end of the current line. +shell_unicode_wrap(Config) -> + + Term = start_tty(Config), + + {_Rows, Cols} = get_window_size(Term), + {_Row, Col} = get_location(Term), + + try + [begin + FirstLine = [U,lists:duplicate(Cols - Col - width(U)*2 + 1,"a")], + OtherLineA = [U,lists:duplicate(Cols - width(U) * 2+1,"a")], + OtherLineB = [U,lists:duplicate(Cols - width(U) * 2+1,"b")], + OtherLineC = [U,lists:duplicate(Cols - width(U) * 2+1,"c")], + OtherLineD = [U,lists:duplicate(Cols - width(U) * 2+1,"d")], + send_tty(Term,FirstLine), + check_content(Term, [FirstLine,$$]), + check_location(Term, {0, Cols - Col - width(U)+1}), + + send_tty(Term,OtherLineA), + check_content(Term, [OtherLineA,$$]), + check_location(Term, {0, Cols - Col - width(U)+1}), + + send_tty(Term,OtherLineB), + check_content(Term, [OtherLineB,$$]), + check_location(Term, {0, Cols - Col - width(U)+1}), + + send_tty(Term,OtherLineC), + check_content(Term, [OtherLineC,$$]), + check_location(Term, {0, Cols - Col - width(U)+1}), + + send_tty(Term,OtherLineD), + check_content(Term, [OtherLineD,$$]), + check_location(Term, {0, Cols - Col - width(U)+1}), + + send_tty(Term,"C-A"), + check_location(Term, {-4, 0}), %% Broken + send_tty(Term,"Right"), + check_location(Term, {-4, width(U)}), %% Broken + + send_tty(Term,"DC"), %% Broken + check_content(Term, ["a.*",U,"$"]), + check_content(Term, ["^b.*",U,"c$"]), + check_content(Term, ["^c.*",U,"dd$"]), + + send_tty(Term,"a"), + check_content(Term, [FirstLine,$$]), + check_content(Term, [OtherLineA,$$]), + check_content(Term, [OtherLineB,$$]), + check_content(Term, [OtherLineC,$$]), + check_content(Term, [OtherLineD,$$]), + + send_tty(Term,"Enter") + end || U <- hard_unicode()] + after + stop_tty(Term) + end. + +shell_delete(Config) -> + + Term = start_tty(Config), + + try + + [ begin + send_tty(Term,"a"), + check_content(Term, "> a$"), + check_location(Term, {0, 1}), + send_tty(Term,"BSpace"), + check_location(Term, {0, 0}), + check_content(Term, ">$"), + send_tty(Term,"a"), + send_tty(Term,U), + check_location(Term, {0, width([$a, U])}), + send_tty(Term,"a"), + send_tty(Term,U), + check_location(Term, {0, width([$a,U,$a,U])}), + check_content(Term, ["> a",U,$a,U,"$"]), + send_tty(Term,"Left"), + send_tty(Term,"Left"), + send_tty(Term,"BSpace"), + check_location(Term, {0, width([$a])}), + check_content(Term, ["> aa",U,"$"]), + send_tty(Term,U), + check_location(Term, {0, width([$a,U])}), + send_tty(Term,"Left"), + send_tty(Term,"DC"), + check_location(Term, {0, width([$a])}), + check_content(Term, ["> aa",U,"$"]), + send_tty(Term,"DC"), + send_tty(Term,"DC"), + check_content(Term, ["> a$"]), + send_tty(Term,"C-E"), + check_location(Term, {0, width([$a])}), + send_tty(Term,"BSpace"), + check_location(Term, {0, width([])}) + end || U <- hard_unicode()] + after + stop_tty(Term) + end. + +%% When deleting characters at the edge of the screen that are "large", +%% we need to take special care. +shell_delete_unicode_wrap(Config) -> + + Term = start_tty(Config), + + {_Rows, Cols} = get_window_size(Term), + {_Row, Col} = get_location(Term), + + try + [begin + send_tty(Term,lists:duplicate(Cols - Col,"a")), + check_content(Term,"> a*$"), + send_tty(Term,[U,U,"aaaaa"]), + check_content(Term,["\n",U,U,"aaaaa$"]), + [send_tty(Term,"Left") || _ <- lists:seq(1,5+2)], + check_location(Term,{0,-Col}), + send_tty(Term,"BSpace"), + check_content(Term,"> a* \n"), + check_location(Term,{-1,Cols - Col - 1}), + send_tty(Term,"BSpace"), + check_content(Term,["> a*",U,"\n"]), + check_location(Term,{-1,Cols - Col - 2}), + send_tty(Term,"BSpace"), + check_content(Term,["> a*",U," \n"]), + check_location(Term,{-1,Cols - Col - 3}), + send_tty(Term,"BSpace"), + check_content(Term,["> a*",U,U,"\n"]), + check_content(Term,["\naaaaa$"]), + check_location(Term,{-1,Cols - Col - 4}), + send_tty(Term,"BSpace"), + check_content(Term,["> a*",U,U,"a\n"]), + check_content(Term,["\naaaa$"]), + check_location(Term,{-1,Cols - Col - 5}), + send_tty(Term,"Enter") + end || U <- hard_unicode()] + after + stop_tty(Term) + end. + +%% When deleting characters and a "large" characters is changing line we need +%% to take extra care +shell_delete_unicode_not_at_cursor_wrap(Config) -> + + Term = start_tty(Config), + + {_Rows, Cols} = get_window_size(Term), + {_Row, Col} = get_location(Term), + + try + [begin + send_tty(Term,lists:duplicate(Cols - Col,"a")), + check_content(Term,"> a*$"), + send_tty(Term,["a",U,"aaaaa"]), + check_content(Term,["\na",U,"aaaaa$"]), + send_tty(Term,"C-A"), + send_tty(Term,"DC"), + check_content(Term,["\n",U,"aaaaa$"]), + send_tty(Term,"DC"), + check_content(Term,["\n",U,"aaaaa$"]), + check_content(Term,["> a* \n"]), + send_tty(Term,"DC"), + check_content(Term,["\naaaaa$"]), + check_content(Term,["> a*",U,"\n"]), + send_tty(Term,"DC"), + check_content(Term,["\naaaa$"]), + check_content(Term,["> a*",U,"a\n"]), + send_tty(Term,"Enter") + end || U <- hard_unicode()] + after + stop_tty(Term) + end. + +%% When deleting characters and a "large" characters is changing line we need +%% to take extra care +shell_update_window_unicode_wrap(Config) -> + + Term = start_tty(Config), + + {_Rows, Cols} = get_window_size(Term), + {_Row, Col} = get_location(Term), + + try + [begin + send_tty(Term,lists:duplicate(Cols - Col - width(U) + 1,"a")), + check_content(Term,"> a*$"), + send_tty(Term,[U,"aaaaa"]), + check_content(Term,["> a* ?\n",U,"aaaaa$"]), + tmux(["resize-window -t ",tty_name(Term)," -x ",Cols+1]), + check_content(Term,["> a*",U,"\naaaaa$"]), + tmux(["resize-window -t ",tty_name(Term)," -x ",Cols]), + check_content(Term,["> a* ?\n",U,"aaaaa$"]), + send_tty(Term,"Enter") + end || U <- hard_unicode()] + after + stop_tty(Term) + end. + +shell_transpose(Config) -> + + Term = start_tty(Config), + + Unicode = [[$a]] ++ hard_unicode(), + + try + [ + begin + send_tty(Term,"a"), + [send_tty(Term,[CP]) || CP <- U], + send_tty(Term,"b"), + [[send_tty(Term,[CP]) || CP <- U2] || U2 <- Unicode], + send_tty(Term,"cde"), + check_content(Term, ["a",U,"b",Unicode,"cde$"]), + check_location(Term, {0, width(["a",U,"b",Unicode,"cde"])}), + send_tty(Term,"Home"), + check_location(Term, {0, 0}), + send_tty(Term,"Right"), + send_tty(Term,"Right"), + check_location(Term, {0, 1+width([U])}), + send_tty(Term,"C-T"), + check_content(Term, ["ab",U,Unicode,"cde$"]), + send_tty(Term,"C-T"), + check_content(Term, ["ab",hd(Unicode),U,tl(Unicode),"cde$"]), + [send_tty(Term,"C-T") || _ <- lists:seq(1,length(Unicode)-1)], + check_content(Term, ["ab",Unicode,U,"cde$"]), + send_tty(Term,"C-T"), + check_content(Term, ["ab",Unicode,"c",U,"de$"]), + check_location(Term, {0, width(["ab",Unicode,"c",U])}), + send_tty(Term,"End"), + check_location(Term, {0, width(["ab",Unicode,"c",U,"de"])}), + send_tty(Term,"Left"), + send_tty(Term,"Left"), + send_tty(Term,"BSpace"), + check_content(Term, ["ab",Unicode,"cde$"]), + send_tty(Term,"End"), + send_tty(Term,"Enter") + end || U <- Unicode], + ok + after + stop_tty(Term), + ok + end. + +shell_search(C) -> + Term = start_tty(C), + + try + send_tty(Term,"a"), + send_tty(Term,"."), + send_tty(Term,"Enter"), + send_tty(Term,"'"), + send_tty(Term,"a"), + send_tty(Term,[16#1f600]), + send_tty(Term,"'"), + send_tty(Term,"."), + send_tty(Term,"Enter"), + check_location(Term, {0, 0}), + send_tty(Term,"C-r"), + check_content(Term, "search:\\s*\n\\s*'a😀'."), + send_tty(Term,"C-a"), + check_location(Term, {-1, width(C, "'a😀'.")}), + send_tty(Term,"Enter"), + send_tty(Term,"C-r"), + check_content(Term, "search:\\s*\n\\s*'a😀'."), + send_tty(Term,"a"), + check_content(Term, "search: a\\s*\n\\s*'a😀'."), + send_tty(Term,"C-r"), + check_content(Term, "search: a\\s*\n\\s*a."), + send_tty(Term,"BSpace"), + check_content(Term, "search:\\s*\n\\s*'a😀'."), + send_tty(Term,"BSpace"), + check_content(Term, "search:\\s*\n\\s*'a😀'."), + send_tty(Term,"M-c"), + check_location(Term, {-1, 0}), + ok + after + stop_tty(Term), + ok + end. + +shell_insert(Config) -> + Term = start_tty(Config), + + try + send_tty(Term,"abcdefghijklm"), + check_content(Term, "abcdefghijklm$"), + check_location(Term, {0, 13}), + send_tty(Term,"Home"), + send_tty(Term,"Right"), + send_tty(Term,"C-T"), + send_tty(Term,"C-T"), + send_tty(Term,"C-T"), + send_tty(Term,"C-T"), + check_content(Term, "bcdeafghijklm$"), + send_tty(Term,"End"), + send_tty(Term,"Left"), + send_tty(Term,"Left"), + send_tty(Term,"BSpace"), + check_content(Term, "bcdeafghijlm$"), + ok + after + stop_tty(Term) + end. + +shell_update_window(Config) -> + Term = start_tty(Config), + + Text = lists:flatten(["abcdefghijklmabcdefghijklm"]), + {_Row, Col} = get_location(Term), + + try + send_tty(Term,Text), + check_content(Term,Text), + check_location(Term, {0, width(Text)}), + tmux(["resize-window -t ",tty_name(Term)," -x ",width(Text)+Col+1]), + send_tty(Term,"a"), + check_location(Term, {0, -Col}), + send_tty(Term,"BSpace"), + tmux(["resize-window -t ",tty_name(Term)," -x ",width(Text)+Col]), + %% xnfix bug! at least in tmux... seems to work in iTerm as it does not + %% need xnfix when resizing + check_location(Term, {0, -Col}), + tmux(["resize-window -t ",tty_name(Term)," -x ",width(Text) div 2 + Col]), + check_location(Term, {0, -Col + width(Text) div 2}), + ok + after + stop_tty(Term) + end. +shell_small_window_multiline_navigation(Config) -> + Term0 = start_tty(Config), + tmux(["resize-window -t ",tty_name(Term0)," -x ",30, " -y ", 6]), + {Row, Col} = get_location(Term0), + Term = Term0#tmux{orig_location = {Row, Col}}, + Text = ("xbcdefghijklmabcdefghijklm\n"++ + "abcdefghijkl\n"++ + "abcdefghijklmabcdefghijklm\n"++ + "abcdefghijklmabcdefghijklx"), + try + send_tty(Term,Text), + check_location(Term, {0, -4}), + send_tty(Term,"Home"), + check_location(Term, {-1, 0}), + send_tty(Term, "C-Up"), + check_location(Term, {-2, 0}), + send_tty(Term, "C-Down"), + check_location(Term, {-1, 0}), + send_tty(Term, "Left"), + check_location(Term, {-1, -4}), + send_tty(Term, "Right"), + check_location(Term, {-1, 0}), + send_tty(Term, "\e[1;4A"), + check_location(Term, {-5, 0}), + check_content(Term,"xbc"), + send_tty(Term, "\e[1;4B"), + check_location(Term, {0, -4}), + check_content(Term,"klx"), + send_tty(Term, " sets:is_e\t"), + check_content(Term,"is_element"), + check_content(Term,"is_empty"), + check_location(Term, {-3, 6}), + send_tty(Term, "C-Up"), + send_tty(Term,"Home"), + check_location(Term, {-2, 0}), + send_tty(Term, "sets:is_e\t"), + check_content(Term,"is_element"), + check_content(Term,"is_empty"), + check_location(Term, {-4, 9}), + ok + after + stop_tty(Term) + end. +shell_huge_input(Config) -> + Term = start_tty(Config), + + ManyUnicode = lists:duplicate(100,hard_unicode()), + + try + send_tty(Term,ManyUnicode), + check_content(Term, hard_unicode_match(Config) ++ "$", + #{ replace => {"\n",""} }), + send_tty(Term,"Enter"), + ok + after + stop_tty(Term) + end. + +%% Test that the shell works when invalid utf-8 (aka latin1) is sent to it +shell_invalid_unicode(Config) -> + Term = start_tty(Config), + + InvalidUnicode = <<$å,$ä,$ö>>, %% åäö in latin1 + + try + send_tty(Term,hard_unicode()), + check_content(Term, hard_unicode() ++ "$"), + send_tty(Term,"Enter"), + check_content(Term, "illegal character"), + %% Send invalid utf-8 + send_stdin(Term,InvalidUnicode), + %% Check that the utf-8 was echoed + check_content(Term, "\\\\345\\\\344\\\\366$"), + send_tty(Term,"Enter"), + %% Check that the terminal entered "latin1" mode + send_tty(Term,"😀한."), + check_content(Term, "\\Q\\360\\237\\230\\200\\355\\225\\234.\\E$"), + send_tty(Term,"Enter"), + %% Check that we can reset the encoding to unicode + send_tty(Term,"io:setopts([{encoding,unicode}])."), + send_tty(Term,"Enter"), + check_content(Term, "\nok\n"), + send_tty(Term,"😀한"), + check_content(Term, "😀한$"), + ok + after + stop_tty(Term), + ok + end. + + +%% Test the we can handle ansi insert, navigation and delete +%% We currently can not so skip this test +shell_support_ansi_input(Config) -> + + Term = start_tty(Config), + + BoldText = "\e[;1m", + ClearText = "\e[0m", + + try + send_stdin(Term,["{",BoldText,"a😀b",ClearText,"}"]), + timer:sleep(1000), + try check_location(Term, {0, width("{1ma😀bm}")}) of + _ -> + throw({skip, "Do not support ansi input"}) + catch _:_ -> + ok + end, + check_location(Term, {0, width("{a😀b}")}), + check_content(fun() -> get_content(Term,"-e") end, + ["{", BoldText, "a😀b", ClearText, "}"]), + send_tty(Term,"Left"), + send_tty(Term,"Left"), + check_location(Term, {0, width("{a😀")}), + send_tty(Term,"C-Left"), + check_location(Term, {0, width("{")}), + send_tty(Term,"End"), + send_tty(Term,"BSpace"), + send_tty(Term,"BSpace"), + check_content(Term, ["{", BoldText, "a😀"]), + ok + after + stop_tty(Term), + ok + end. + +shell_expand_location_below(Config) -> + + Term = start_tty(Config), + + {Rows, _} = get_location(Term), + + NumFunctions = lists:seq(0, Rows*2), + FunctionName = "a_long_function_name", + + Module = lists:flatten( + ["-module(long_module).\n", + "-export([",lists:join($,,[io_lib:format("~s~p/0",[FunctionName,I]) || I <- NumFunctions]),"]).\n\n", + [io_lib:format("~s~p() -> ok.\n",[FunctionName,I]) || I <- NumFunctions]]), + + DoScan = fun F([]) -> + []; + F(Str) -> + {done,{ok,T,_},C} = erl_scan:tokens([],Str,0), + [ T | F(C) ] + end, + Forms = [ begin {ok,Y} = erl_parse:parse_form(X),Y end || X <- DoScan(Module) ], + {ok,_,Bin} = compile:forms(Forms, [debug_info]), + + try + tmux(["resize-window -t ",tty_name(Term)," -x 80"]), + + %% First check that basic completion works + send_stdin(Term, "escript:"), + send_stdin(Term, "\t"), + %% Cursor at correct place + check_location(Term, {-3, width("escript:")}), + %% Nothing after the start( completion + check_content(Term, "start\\($"), + + %% Check that completion is cleared when we type + send_stdin(Term, "s"), + check_location(Term, {-3, width("escript:s")}), + check_content(Term, "escript:s$"), + + %% Check that completion works when in the middle of a term + send_tty(Term, "Home"), + send_tty(Term, "["), + send_tty(Term, "End"), + send_tty(Term, ", test_after]"), + [send_tty(Term, "Left") || _ <- ", test_after]"], + send_stdin(Term, "\t"), + check_location(Term, {-3, width("[escript:s")}), + check_content(Term, "script_name\\([ ]+start\\($"), + send_tty(Term, "C-K"), + + %% Check that completion works when in the middle of a long term + send_tty(Term, ", "++ lists:duplicate(80*2, $a)++"]"), + [send_tty(Term, "Left") || _ <- ", "++ lists:duplicate(80*2, $a)++"]"], + send_stdin(Term, "\t"), + check_location(Term, {-4, width("[escript:s")}), + check_content(Term, "script_name\\([ ]+start\\($"), + send_tty(Term, "End"), + send_stdin(Term, ".\n"), + + %% Check that we behave as we should with very long completions + rpc(Term, fun() -> + {module, long_module} = code:load_binary(long_module, "long_module.beam", Bin) + end), + check_content(Term, "3>"), + send_stdin(Term, "long_module:" ++ FunctionName), + send_stdin(Term, "\t"), + %% Check that correct text is printed below expansion + check_content(Term, io_lib:format("Press tab to see all ~p expansions", + [length(NumFunctions)])), + send_stdin(Term, "\t"), + %% The expansion does not fit on screen, verify that + %% expand above mode is used + check_content(fun() -> get_content(Term, "-S -7") end, + "3> long_module:" ++ FunctionName ++ "\nfunctions"), + check_content(Term, "3> long_module:" ++ FunctionName ++ "$"), + + %% We resize the terminal to make everything fit and test that + %% expand below mode is used + tmux(["resize-window -t ", tty_name(Term), " -y ", integer_to_list(Rows+10)]), + timer:sleep(1000), %% Sleep to make sure window has resized + send_stdin(Term, "\t\t"), + check_content(Term, "3> long_module:" ++ FunctionName ++ "\nfunctions(\n|.)*a_long_function_name99\\($"), + ok + after + stop_tty(Term), + ok + end. + +shell_expand_location_above(Config) -> + + Term = start_tty([{args,["-stdlib","shell_expand_location","above"]}|Config]), + + try + tmux(["resize-window -t ",tty_name(Term)," -x 80"]), + send_stdin(Term, "escript:"), + send_stdin(Term, "\t"), + check_location(Term, {0, width("escript:")}), + check_content(Term, "start\\(\n"), + check_content(Term, "escript:$"), + send_stdin(Term, "s"), + send_stdin(Term, "\t"), + check_location(Term, {0, width("escript:s")}), + check_content(Term, "\nscript_name\\([ ]+start\\(\n"), + check_content(Term, "escript:s$"), + ok + after + stop_tty(Term), + ok + end. + +%% Test the we can handle invalid ansi escape chars. +%% tmux cannot handle this... so we test this using to_erl +shell_invalid_ansi(_Config) -> + + InvalidAnsiPrompt = + case proplists:get_value(encoding, io:getopts(user)) of + unicode -> + ["\e]94m",54620,44397,50612,47,51312,49440,47568,"\e]0m"]; + latin1 -> + ["\e]94minvalid_test\e]0m"] + end, + + rtnode:run( + [{eval, fun() -> application:set_env( + stdlib, shell_prompt_func_test, + fun() -> InvalidAnsiPrompt end) + end }, + {putline,"a."}, + {expect, "a[.]"}, + {expect, ["\\Q",InvalidAnsiPrompt,"\\E"]}], + "", "", + ["-pz",filename:dirname(code:which(?MODULE)), + "-connect_all","false", + "-kernel","logger_level","all", + "-kernel","shell_history","disabled", + "-kernel","prevent_overlapping_partitions","false", + "-eval","shell:prompt_func({interactive_shell_SUITE,prompt})." + ]). + +shell_ignore_pager_commands(Config) -> + Term = start_tty(Config), + case code:get_doc(file, #{sources=>[eep48]}) of + {error, _} -> {skip, "No documentation available"}; + _ -> + try + send_tty(Term, "h(file).\n"), + check_content(Term,"\\Qmore (y/n)? (y)\\E"), + send_tty(Term, "n\n"), + check_content(Term,"ok"), + send_tty(Term, "C-P"), + check_content(Term,"\\Qh(file).\\E"), + ok + after + stop_tty(Term), + ok + end + end. + +external_editor(Config) -> + case os:find_executable("nano") of + false -> {skip, "nano is not installed"}; + _ -> + Term = start_tty(Config), + try + tmux(["resize-window -t ",tty_name(Term)," -x 80"]), + send_tty(Term,"os:putenv(\"EDITOR\",\"nano\").\n"), + send_tty(Term, "\"some text with\nnewline in it\""), + check_content(Term,"3> \"some text with\\s*\n.+\\s*newline in it\""), + send_tty(Term, "C-O"), + check_content(Term,"GNU nano [\\d.]+"), + check_content(Term,"\"some text with\\s*\n\\s*newline in it\""), + send_tty(Term, "Right"), + send_tty(Term, "still"), + send_tty(Term, "Enter"), + send_tty(Term, "C-O"), %% save in nano + send_tty(Term, "Enter"), + send_tty(Term, "C-X"), %% quit in nano + check_content(Term,"3> \"still\\s*\n\\s*.+\\s*some text with\\s*\n.+\\s*newline in it\""), + send_tty(Term,".\n"), + check_content(Term,"\\Q\"still\\nsome text with\\nnewline in it\"\\E"), + ok + after + stop_tty(Term), + ok + end + end. + +external_editor_visual(Config) -> + case os:find_executable("vim") of + false -> + {skip, "vim is not installed"}; + _ -> + Term = start_tty(Config), + try + tmux(["resize-window -t ",tty_name(Term)," -x 80"]), + send_tty(Term,"os:putenv(\"EDITOR\",\"nano\").\n"), + check_content(Term, "3>"), + send_tty(Term,"os:putenv(\"VISUAL\",\"vim -u DEFAULTS -U NONE -i NONE\").\n"), + check_content(Term, "4>"), + send_tty(Term,"\"hello"), + send_tty(Term, "C-O"), %% Open vim + check_content(Term, "^\"hello"), + send_tty(Term, "$"), %% Set cursor at end + send_tty(Term, "a"), %% Enter insert mode at end + check_content(Term, "-- INSERT --"), + send_tty(Term, "\"."), + send_tty(Term,"Escape"), + send_tty(Term,":wq"), + send_tty(Term,"Enter"), + check_content(Term, "\"hello\"[.]$"), + send_tty(Term,"Enter"), + check_content(Term, "\"hello\""), + check_content(Term, "5>$") + after + stop_tty(Term), + ok + end + end. + +external_editor_unicode(Config) -> + NanoPath = os:find_executable("nano"), + case NanoPath of + false -> {skip, "nano is not installed"}; + _ -> + Term = start_tty(Config), + try + tmux(["resize-window -t ",tty_name(Term)," -x 80"]), + send_tty(Term,"os:putenv(\"EDITOR\",\"nano\").\n"), + send_tty(Term, hard_unicode()), + check_content(Term,"3> " ++ hard_unicode_match(Config)), + send_tty(Term, "C-O"), %% open external editor (nano) + check_content(Term,"GNU nano [\\d.]+"), + send_tty(Term, "still "), + check_content(Term,"\nstill "), + send_tty(Term, "C-O"), %% save in nano + send_tty(Term, "Enter"), + send_tty(Term, "C-X"), %% quit in nano + check_content(Term,"still "++hard_unicode_match(Config)), + ok + after + stop_tty(Term), + ok + end + end. + +%% There used to be a race condition when using shell:start_interactive where +%% the newline handling of standard_error did not change correctly to compensate +%% for the tty changing to canonical mode +shell_standard_error_nlcr(Config) -> + + [ + begin + Term = setup_tty([{env,[{"TERM",TERM}]},{args, ["-noshell"]} | Config]), + try + rpc(Term, io, format, [standard_error,"test~ntest~ntest", []]), + check_content(Term, "test\ntest\ntest$"), + rpc(Term, fun() -> shell:start_interactive(), + io:format(standard_error,"test~ntest~ntest", []) + end), + check_content(Term, "test\ntest\ntest(\n|.)*test\ntest\ntest") + after + stop_tty(Term) + end + end || TERM <- ["dumb",os:getenv("TERM")]]. + +%% We test that suspending of `erl` and then resuming restores the shell +shell_suspend(Config) -> + + Name = peer:random_name(proplists:get_value(tc_path,Config)), + %% In order to suspend `erl` we need it to run in a shell that has job control + %% so we start the peer within a tmux window instead of having it be the original + %% process. + os:cmd("tmux new-window -n " ++ Name ++ " -d -- bash --norc"), + + Peer = #{ name => Name, + post_process_args => + fun(["new-window","-n",_,"-d","--"|CmdAndArgs]) -> + FlatCmdAndArgs = + lists:join( + " ",[[$',A,$'] || A <- CmdAndArgs]), + ["send","-t",Name,lists:flatten(FlatCmdAndArgs),"Enter"] + end + }, + + + Term = start_tty([{peer, Peer}|Config]), + + try + send_tty(Term, hard_unicode()), + check_content(Term,["2> ",hard_unicode(),"$"]), + send_tty(Term, "C-Z"), + check_content(Term,"\\Q[1]+\\E\\s*Stopped"), + send_tty(Term, "fg"), + send_tty(Term, "Enter"), + send_tty(Term, "M-l"), + check_content(Term,["2> ",hard_unicode(),"$"]), + check_location(Term,{0,width(hard_unicode())}), + ok + after + stop_tty(Term), + ok + end. + +%% We test that suspending of `erl` and then resuming restores the shell +shell_full_queue(Config) -> + + [throw({skip,"Need unbuffered to run"}) || os:find_executable("unbuffered") =:= false], + + %% In order to fill the read buffer of the terminal we need to get a + %% bit creative. We first need to start erl in bash in order to be + %% able to get access to job control for suspended processes. + %% We then also wrap `erl` in `unbuffer -p` so that we can suspend + %% that program in order to block writing to stdout for a while. + + Name = peer:random_name(proplists:get_value(tc_path,Config)), + os:cmd("tmux new-window -n " ++ Name ++ " -d -- bash --norc"), + + Peer = #{ name => Name, + post_process_args => + fun(["new-window","-n",_,"-d","--"|CmdAndArgs]) -> + FlatCmdAndArgs = ["unbuffer -p "] ++ + lists:join( + " ",[[$',A,$'] || A <- CmdAndArgs]), + ["send","-t",Name,lists:flatten(FlatCmdAndArgs),"Enter"] + end + }, + + + Term = start_tty([{peer, Peer}|Config]), + + UnbufferedPid = os:cmd("ps -o ppid= -p " ++ rpc(Term,os,getpid,[])), + + WriteUntilStopped = + fun F(Char) -> + rpc(Term,io,format,[user,[Char],[]]), + put(bytes,get(bytes,0)+1), + receive + stop -> + rpc(Term,io,format,[user,[Char+1],[]]) + after 0 -> F(Char) + end + end, + + WaitUntilBlocked = + fun(Pid, Ref) -> + (fun F(Cnt) -> + receive + {'DOWN',Ref,_,_,_} = Down -> + ct:fail({io_format_did_not_block, Down}) + after 1000 -> + ok + end, + case process_info(Pid,dictionary) of + {dictionary,[{bytes,Cnt}]} -> + ct:log("Bytes until blocked: ~p~n",[Cnt]), + %% Add one extra byte as for + %% the current blocking call + Cnt + 1; + {dictionary,[{bytes,NewCnt}]} -> + F(NewCnt) + end + end)(0) + end, + + try + %% First test that we can suspend and then resume + os:cmd("kill -TSTP " ++ UnbufferedPid), + check_content(Term,"\\Q[1]+\\E\\s*Stopped"), + {Pid, Ref} = spawn_monitor(fun() -> WriteUntilStopped($a) end), + WaitUntilBlocked(Pid, Ref), + send_tty(Term, "fg"), + send_tty(Term, "Enter"), + Pid ! stop, + check_content(Term,"b$"), + + send_tty(Term, "."), + send_tty(Term, "Enter"), + + %% Then we test that all characters are written when system + %% is terminated just after writing + {ok,Cols} = rpc(Term,io,columns,[user]), + send_tty(Term, "Enter"), + os:cmd("kill -TSTP " ++ UnbufferedPid), + check_content(Term,"\\Q[1]+\\E\\s*Stopped"), + {Pid2, Ref2} = spawn_monitor(fun() -> WriteUntilStopped($c) end), + Bytes = WaitUntilBlocked(Pid2, Ref2) - 1, + stop_tty(Term), + send_tty(Term, "fg"), + send_tty(Term, "Enter"), + check_content( + fun() -> + tmux(["capture-pane -p -S - -E - -t ",tty_name(Term)]) + end, lists:flatten([lists:duplicate(Cols,$c) ++ "\n" || + _ <- lists:seq(1,(Bytes) div Cols)] + ++ [lists:duplicate((Bytes) rem Cols,$c)])), + ct:log("~ts",[tmux(["capture-pane -p -S - -E - -t ",tty_name(Term)])]), + ok + after + stop_tty(Term), + ok + end. + +get(Key,Default) -> + case get(Key) of + undefined -> + Default; + Value -> + Value + end. + +%% A list of unicode graphemes that are notoriously hard to render +hard_unicode() -> + ZWJ = + case os:type() of + %% macOS has very good rendering of ZWJ, + %% but the cursor does not agree with it.. + {unix, darwin} -> []; + _ -> [[16#1F91A,16#1F3FC]] % Hand with skintone 🤚🏼 + end, + [[16#1f600], % Smilie 😀 + "한", % Hangul + "Z̤͔ͧ̑̓","ä͖̭̈̇","lͮ̒ͫ","ǧ̗͚̚","o̙̔ͮ̇͐̇" %% Vertically stacked chars + %%"👩👩", % Zero width joiner + %%"👩👩👧👦" % Zero width joiner + | ZWJ]. + +hard_unicode_match(Config) -> + ["\\Q",[unicode_to_octet(Config, U) || U <- hard_unicode()],"\\E"]. + +unicode_to_octet(Config, U) -> + case ?config(encoding,Config) of + unicode -> U; + latin1 -> unicode_to_octet(U) + end. + +unicode_to_octet(U) -> + [if Byte >= 128 -> [$\\,integer_to_list(Byte,8)]; + true -> Byte + end || <<Byte>> <= unicode:characters_to_binary(U)]. + +unicode_to_hex(Config, U) -> + case ?config(encoding,Config) of + unicode -> U; + latin1 -> unicode_to_hex(U) + end. + +unicode_to_hex(U) when is_integer(U) -> + unicode_to_hex([U]); +unicode_to_hex(Us) -> + [if U < 128 -> U; + U < 512 -> ["\\",integer_to_list(U,8)]; + true -> ["\\x{",integer_to_list(U,16),"}"] + end || U <- Us]. + +width(C, Str) -> + case ?config(encoding, C) of + unicode -> width(Str); + latin1 -> width(unicode_to_octet(Str)) + end. +width(Str) -> + lists:sum( + [npwcwidth(CP) || CP <- lists:flatten(Str)]). + +npwcwidth(CP) -> + try prim_tty:npwcwidth(CP) + catch error:undef -> + if CP =:= 16#D55C -> + 2; %% 한 + CP =:= 16#1f91A -> + 2; %% hand + CP =:= 16#1F3Fc -> + 2; %% Skintone + CP =:= 16#1f600 -> + 2; %% smilie + true -> + case lists:member(CP, [775,776,780,785,786,787,788,791,793,794, + 804,813,848,852,854,858,871,875,878]) of + true -> + 0; + false -> + 1 + end + end + end. + +tmux([Cmd|_] = Command) when is_list(Cmd) -> + tmux(lists:concat(Command)); +tmux(Command) -> + string:trim(os:cmd(["tmux ",Command])). + +rpc(#tmux{ node = N }, Fun) -> + erpc:call(N, Fun). +rpc(#tmux{ node = N }, M, F, A) -> + erpc:call(N, M, F, A). + +%% Setup a TTY, but do not type anything in terminal +setup_tty(Config) -> + Name = maps:get(name,proplists:get_value(peer, Config, #{}), + peer:random_name(proplists:get_value(tc_path, Config))), + + Envs = lists:flatmap(fun({Key,Value}) -> + ["-env",Key,Value] + end, proplists:get_value(env,Config,[])), + + ExtraArgs = proplists:get_value(args,Config,[]), + + ExecArgs = case os:getenv("TMUX_DEBUG") of + "strace" -> + STraceLog = filename:join(proplists:get_value(priv_dir,Config), + Name++".strace"), + ct:pal("Link to strace: file://~ts", [STraceLog]), + [os:find_executable("strace"),"-f", + "-o",STraceLog, + "-e","trace=all", + "-e","read=0,1,2", + "-e","write=0,1,2" + ] ++ string:split(ct:get_progname()," ",all); + "rr" -> + [os:find_executable("cerl"),"-rr"]; + _ -> + string:split(ct:get_progname()," ",all) + end, + DefaultPeerArgs = #{ name => Name, + exec => + {os:find_executable("tmux"), + ["new-window","-n",Name,"-d","--"] ++ ExecArgs }, + + args => ["-pz",filename:dirname(code:which(?MODULE)), + "-connect_all","false", +% "-kernel","logger_level","all", + "-kernel","shell_history","disabled", + "-kernel","prevent_overlapping_partitions","false", + "-eval","shell:prompt_func({interactive_shell_SUITE,prompt})." + ] ++ Envs ++ ExtraArgs, + detached => false + }, + + {ok, Peer, Node} = + ?CT_PEER(maps:merge(proplists:get_value(peer,Config,#{}), + DefaultPeerArgs)), + + Self = self(), + + %% By default peer links with the starter. For these TCs we however only + %% want the peer to die if we die, so we create a "unidirection link" using + %% monitors. + spawn(fun() -> + TCRef = erlang:monitor(process, Self), + PeerRef = erlang:monitor(process, Peer), + receive + {'DOWN',TCRef,_,_,Reason} -> + exit(Peer, Reason); + {'DOWN',PeerRef,_,_,_} -> + ok + end + end), + unlink(Peer), + + "" = tmux(["set-option -t ",Name," remain-on-exit on"]), + + %% We start tracing on the remote node in order to help debugging + TraceLog = filename:join(proplists:get_value(priv_dir,Config),Name++".trace"), + ct:log("Link to trace: file://~ts",[TraceLog]), + + spawn(Node, + fun() -> + {ok, _} = dbg:tracer(file,TraceLog), + %% dbg:p(whereis(user_drv),[c,m]), + %% dbg:p(whereis(user_drv_writer),[c,m]), + %% dbg:p(whereis(user_drv_reader),[c,m]), + %% dbg:tp(user_drv,x), + %% dbg:tp(prim_tty,x), + %% dbg:tpl(prim_tty,write_nif,x), + %% dbg:tpl(prim_tty,read_nif,x), + monitor(process, Self), + receive _ -> ok end + end), + + #tmux{ peer = Peer, node = Node, name = Name }. + +%% Start a tty, setup custom prompt and set cursor at bottom +start_tty(Config) -> + + Term = setup_tty(Config), + + Prompt = fun() -> ["\e[94m",54620,44397,50612,47,51312,49440,47568,"\e[0m"] end, + erpc:call(Term#tmux.node, application, set_env, + [stdlib, shell_prompt_func_test, + proplists:get_value(shell_prompt_func_test, Config, Prompt)]), + + {Rows, _} = get_window_size(Term), + + %% We send a lot of newlines here in order for the number of rows + %% in the window to be max so that we can predict what the cursor + %% position is. + [send_tty(Term,"\n") || _ <- lists:seq(1, Rows)], + + %% We enter an 'a' here so that we can get the correct orig position + %% with an alternative prompt. + send_tty(Term,"a.\n"), + check_content(Term,"2>$"), + OrigLocation = get_location(Term), + Term#tmux{ orig_location = OrigLocation }. + +prompt(L) -> + N = proplists:get_value(history, L, 0), + Fun = application:get_env(stdlib, shell_prompt_func_test, + fun() -> atom_to_list(node()) end), + io_lib:format("(~ts)~w> ",[Fun(),N]). + +stop_tty(Term) -> + catch peer:stop(Term#tmux.peer), + ct:log("~ts",[get_content(Term, "-e")]), +% "" = tmux("kill-window -t " ++ Term#tmux.name), + ok. + +tty_name(Term) -> + Term#tmux.name. + +send_tty(Term, "Home") -> + %% https://stackoverflow.com/a/55616731 + send_tty(Term,"Escape"), + send_tty(Term,"OH"); +send_tty(Term, "End") -> + send_tty(Term,"Escape"), + send_tty(Term,"OF"); +send_tty(#tmux{ name = Name } = _Term,Value) -> + [Head | Quotes] = string:split(Value, "'", all), + "" = tmux("send -t " ++ Name ++ " '" ++ Head ++ "'"), + [begin + "" = tmux("send -t " ++ Name ++ " \"'\""), + "" = tmux("send -t " ++ Name ++ " '" ++ V ++ "'") + end || V <- Quotes]. + +%% We use send_stdin for testing of things that we cannot sent via +%% the tmux send command, such as invalid unicode +send_stdin(Term, Chars) when is_binary(Chars) -> + rpc(Term,erlang,display_string,[stdin,Chars]); +send_stdin(Term, Chars) -> + send_stdin(Term, iolist_to_binary(unicode:characters_to_binary(Chars))). + +check_location(Term, Where) -> + check_location(Term, Where, 5). +check_location(#tmux{ orig_location = {OrigRow, OrigCol} = Orig } = Term, + {AdjRow, AdjCol} = Where, Attempt) -> + NewLocation = get_location(Term), + case {OrigRow+AdjRow,OrigCol+AdjCol} of + NewLocation -> NewLocation; + _ when Attempt =:= 0 -> + {NewRow, NewCol} = NewLocation, + ct:fail({wrong_location, {expected,{AdjRow, AdjCol}}, + {got,{NewRow - OrigRow, NewCol - OrigCol}, + {NewLocation, Orig}}}); + _ -> + timer:sleep(50), + check_location(Term, Where, Attempt -1) + end. + +get_location(Term) -> + RowAndCol = tmux("display -pF '#{cursor_y} #{cursor_x}' -t "++Term#tmux.name), + [Row, Col] = string:lexemes(string:trim(RowAndCol,both)," "), + {list_to_integer(Row), list_to_integer(Col)}. + +get_window_size(Term) -> + RowAndCol = tmux("display -pF '#{window_height} #{window_width}' -t "++Term#tmux.name), + [Row, Col] = string:lexemes(string:trim(RowAndCol,both)," "), + {list_to_integer(Row), list_to_integer(Col)}. + +check_content(Term, Match) -> + check_content(Term, Match, #{}). +check_content(Term, Match, Opts) when is_map(Opts) -> + check_content(Term, Match, Opts, 5). +check_content(Term, Match, Opts, Attempt) -> + OrigContent = case Term of + #tmux{} -> get_content(Term); + Fun when is_function(Fun,0) -> Fun() + end, + Content = case maps:find(replace, Opts) of + {ok, {RE,Repl} } -> + re:replace(OrigContent, RE, Repl, [global]); + error -> + OrigContent + end, + case re:run(string:trim(Content, both), lists:flatten(Match), [unicode]) of + {match,_} -> + ok; + _ when Attempt =:= 0 -> + io:format("Failed to find '~ts' in ~n'~ts'~n", + [unicode:characters_to_binary(Match), Content]), + io:format("Failed to find '~w' in ~n'~w'~n", + [unicode:characters_to_binary(Match), Content]), + ct:fail(nomatch); + _ -> + timer:sleep(500), + check_content(Term, Match, Opts, Attempt - 1) + end. + +get_content(Term) -> + get_content(Term, ""). +get_content(#tmux{ name = Name }, Args) -> + Content = unicode:characters_to_binary(tmux("capture-pane -p " ++ Args ++ " -t " ++ Name)), + case string:split(Content,"a.\na") of + [_Ignore,C] -> + C; + [C] -> + C + end. %% Tests that exit of initial shell restarts shell. exit_initial(Config) when is_list(Config) -> @@ -260,36 +1719,38 @@ exit_initial(Config) when is_list(Config) -> ok. test_exit_initial(old) -> - rtnode([{putline, ""}, - {putline, "2."}, - {expect, "2\r\n"}, - {putline, "exit()."}, - {expect, "Eshell"}, - {putline, ""}, - {putline, "35."}, - {expect, "35\r\n"}], - [], [], ["-oldshell"]); + rtnode:run( + [{putline, ""}, + {putline, "2."}, + {expect, "2\r\n"}, + {putline, "exit()."}, + {expect, "Eshell"}, + {putline, ""}, + {putline, "35."}, + {expect, "35\r\n"}], + [], [], ["-oldshell"]); test_exit_initial(new) -> - rtnode([{putline, ""}, - {expect, "1> $"}, - {putline, "2."}, - {expect, "2"}, - {putline,"exit()."}, - {expect, "Eshell"}, - {expect, "1> $"}, - {putline, "35."}, - {expect, "35\r\n"}]). + rtnode:run( + [{putline, ""}, + {expect, "1> $"}, + {putline, "2."}, + {expect, "2"}, + {putline,"exit()."}, + {expect, "Eshell"}, + {expect, "1> $"}, + {putline, "35."}, + {expect, "35\r\n"}]). stop_during_init(Config) when is_list(Config) -> - {RunErl,_ToErl,Erl} = get_progs(), - case create_tempdir() of + {RunErl,_ToErl,[Erl|ErlArgs]} = rtnode:get_progs(), + case rtnode:create_tempdir() of {error, Reason} -> {skip, Reason}; Tempdir -> XArg = " -kernel shell_history enabled -s init stop", - start_runerl_command(RunErl, Tempdir, "\\\""++Erl++"\\\""++XArg), - Logs = rtnode_read_logs(Tempdir), - rtnode_dump_logs(Logs), + rtnode:start_runerl_command(RunErl, Tempdir, "\\\""++Erl++"\\\""++ErlArgs++XArg), + Logs = rtnode:read_logs(Tempdir), + rtnode:dump_logs(Logs), nomatch = binary:match(map_get("erlang.log.1", Logs), <<"*** ERROR: Shell process terminated! ***">>), ok @@ -312,16 +1773,16 @@ wrap(Config) when is_list(Config) -> case proplists:get_value(default_shell, Config) of new -> As = lists:duplicate(20,"a"), - rtnode([{putline, "io:columns()."}, - {expect, "{ok,20}\r\n"}, - {putline, ["io:format(\"~s\",[lists:duplicate(20,\"a\")])."]}, - {expect, As ++ " \b"}, - {putline, ["io:format(\"~s~n~s\",[lists:duplicate(20,\"a\"),lists:duplicate(20,\"a\")])."]}, - {expect, As ++ "\r\n" ++ As ++ " \b"} - ], - [], - "stty rows 40; stty columns 20; ", - [""]); + rtnode:run( + [{putline, "io:columns()."}, + {expect, "{ok,20}\r\n"}, + {putline, ["io:format(\"~s\",[lists:duplicate(20,\"a\")])."]}, + {expect, As ++ " \b"}, + {putline, ["io:format(\"~s~n~s\",[lists:duplicate(20,\"a\"),lists:duplicate(20,\"a\")])."]}, + {expect, As ++ "\r\n" ++ As ++ " \b"} + ], + [], + "stty rows 40; stty columns 20; "); _ -> ok end, @@ -336,58 +1797,71 @@ wrap(Config) when is_list(Config) -> %% commands. shell_history(Config) when is_list(Config) -> Path = shell_history_path(Config, "basic"), - rtnode([ - {putline, "echo1."}, - {expect, "echo1\r\n"}, - {putline, "echo2."}, - {expect, "echo2\r\n"}, - {putline, "echo3."}, - {expect, "echo3\r\n"}, - {putline, "echo4."}, - {expect, "echo4\r\n"}, - {putline, "echo5."}, - {expect, "echo5\r\n"} - ], [], [], " -kernel shell_history enabled " ++ - "-kernel shell_history_drop '[\\\"init:stop().\\\"]' " ++ - mk_sh_param(Path)), + rtnode:run( + [{putline, "echo1."}, + {expect, "echo1\r\n"}, + {putline, "echo2."}, + {expect, "echo2\r\n"}, + {putline, "echo3."}, + {expect, "echo3\r\n"}, + {putline, "echo4."}, + {expect, "echo4\r\n"}, + {putline, "echo5."}, + {expect, "echo5\r\n"} + ], [], [], mk_history_param(Path)), + receive after 1000 -> ok end, + rtnode:run( + [{sleep,100}, + {putline, ""}, + %% the init:stop that stopped the node is dropped + {putdata, [$\^p]}, {expect, "echo5[.]$"}, + {putdata, [$\n]}, + {expect, "echo5\r\n"}, + {putdata, [$\^p]}, {expect, "echo5[.]$"}, + {putdata, [$\^p]}, {expect, "echo4[.]$"}, + {putdata, [$\^p]}, {expect, "echo3[.]$"}, + {putdata, [$\^p]}, {expect, "echo2[.]$"}, + {putdata, [$\^n]}, {expect, "echo3[.]$"}, + {putdata, [$\^n]}, {expect, "echo4[.]$"}, + {putdata, [$\^b]}, {sleep,50}, %% the echo4. (cursor moved one left) + {putline, ["ECHO"]}, + {expect, "echo4ECHO\r\n"} + ], [], [], + mk_history_param(Path)), receive after 1000 -> ok end, - rtnode([ - {putline, ""}, - %% the init:stop that stopped the node is dropped - {putdata, [$\^p]}, {expect, "echo5[.]$"}, - {putdata, [$\n]}, - {expect, "echo5\r\n"}, - {putdata, [$\^p]}, {expect, "echo5[.]$"}, - {putdata, [$\^p]}, {expect, "echo4[.]$"}, - {putdata, [$\^p]}, {expect, "echo3[.]$"}, - {putdata, [$\^p]}, {expect, "echo2[.]$"}, - {putdata, [$\^n]}, {expect, "echo3[.]$"}, - {putdata, [$\^n]}, {expect, "echo4[.]$"}, - {putdata, [$\^b]}, {sleep,50}, %% the echo4. (cursor moved one left) - {putline, ["ECHO"]}, - {expect, "echo4ECHO\r\n"} - ], [], [], " -kernel shell_history enabled " ++ mk_sh_param(Path)), + + %% ignore input given after io:get_line, and after h(module) pager + rtnode:run( + [{putdata, "io:get_line(\"getline>\").\n"}, + {expect, "getline>"}, + {putline, "hej"}, {expect, "hej\r\n"}, + {putdata, [$\^p]}, {expect, "\\Qio:get_line(\"getline>\")\\E[.]$"} + ], [], [], + mk_history_param(Path)), ok. shell_history_resize(Config) -> Path = shell_history_path(Config, "resize"), - rtnode([ - {putline, "echo."}, - {expect, "echo\r\n"} - ], [], [], " -kernel shell_history_file_bytes 123456 " ++ - "-kernel shell_history enabled " ++ mk_sh_param(Path)), + rtnode:run( + [{putline, "echo."}, + {expect, "echo\r\n"}, + {putline, "echo2."}, + {expect, "echo2\r\n"} + ], [], [], ["-kernel","shell_history_file_bytes","123456"] ++ + mk_history_param(Path)), {ok, Logs} = - rtnode([ - {putline, ""}, - {putdata, [$\^p]}, {expect, "init:stop\\(\\)[.]$"}, - {putdata, [$\^p]}, {expect, "echo[.]$"}, - {putdata, [$\n]}, - {expect, "echo"} - ], [], [], " -kernel shell_history_file_bytes 654321 " ++ - "-kernel shell_history enabled " ++ mk_sh_param(Path)), - - rtnode_check_logs( + rtnode:run( + [{sleep,100}, + {putline, ""}, + {putdata, [$\^p]}, {expect, "echo2[.]$$"}, + {putdata, [$\^p]}, {expect, "echo[.]$"}, + {putdata, [$\n]}, + {expect, "echo"} + ], [], [], ["-kernel","shell_history_file_bytes","654321"] ++ + mk_history_param(Path)), + + rtnode:check_logs( "erlang.log.1", "The configured log history file size is different from the size " "of the log file on disk", Logs), @@ -405,24 +1879,24 @@ shell_history_eaccess(Config) -> %% Cannot create history log in folder {ok, Logs1} = - rtnode([ - {putline, "echo."}, - {expect, "echo\r\n"} - ], [], [], "-kernel shell_history enabled " ++ mk_sh_param(Path)), + rtnode:run( + [{putline, "echo."}, + {expect, "echo\r\n"} + ], [], [], mk_history_param(Path)), - rtnode_check_logs("erlang.log.1", "Error handling file", Logs1), + ct:pal("~p",[Logs1]), + rtnode:check_logs("erlang.log.1", "Error handling file", Logs1), %% shell_docs recursively creates the folder to store the %% logs. This test checks that erlang still starts if we %% cannot create the folders to the path. {ok, Logs2} = - rtnode([ - {putline, "echo."}, - {expect, "echo\r\n"} - ], [], [], "-kernel shell_history enabled " ++ - mk_sh_param(filename:join(Path,"logs"))), + rtnode:run( + [{putline, "echo."}, + {expect, "echo\r\n"} + ], [], [], mk_history_param(filename:join(Path,"logs"))), - rtnode_check_logs("erlang.log.1", "Error handling file", Logs2) + rtnode:check_logs("erlang.log.1", "Error handling file", Logs2) after file:write_file_info(Path, Info) @@ -436,15 +1910,15 @@ shell_history_repair(Config) -> shell_history_halt(Path), {ok, Logs} = - rtnode([ - {putline, ""}, - {putdata, [$\^p]}, {expect, "echo[.]$"}, - {putdata, [$\n]}, - {expect, "echo\r\n"} - ], [], [], "-kernel shell_history enabled " ++ mk_sh_param(Path)), + rtnode:run( + [{putline, ""}, + {putdata, [$\^p]}, {expect, "echo[.]$"}, + {putdata, [$\n]}, + {expect, "echo\r\n"} + ], [], [], mk_history_param(Path)), %% The regexp below checks that he string is NOT part of the log - rtnode_check_logs("erlang.log.1", + rtnode:check_logs("erlang.log.1", "The shell history log file was corrupted and was repaired", false, Logs), @@ -462,14 +1936,14 @@ shell_history_repair_corrupt(Config) -> ok = file:close(D), {ok, Logs} = - rtnode([ - {putline, ""}, - {putdata, [$\^p]}, {expect, "echo[.]$"}, - {putdata, [$\n]}, - {expect, "echo\r\n"} - ], [], [], "-kernel shell_history enabled " ++ mk_sh_param(Path)), - - rtnode_check_logs("erlang.log.1", + rtnode:run( + [{putline, ""}, + {putdata, [$\^p]}, {expect, "echo[.]$"}, + {putdata, [$\n]}, + {expect, "echo\r\n"} + ], [], [], mk_history_param(Path)), + + rtnode:check_logs("erlang.log.1", "The shell history log file was corrupted and was repaired.", Logs), ok. @@ -478,9 +1952,12 @@ shell_history_corrupt(Config) -> Path = shell_history_path(Config, "corrupt"), %% We initialize the shell history log with a known value. - rtnode([{putline, "echo."}, - {expect, "echo\r\n"} - ], [], [], "-kernel shell_history enabled " ++ mk_sh_param(Path)), + rtnode:run( + [{putline, "echo."}, + {expect, "echo\r\n"}, + {putline, "echo2."}, + {expect, "echo2\r\n"} + ], [], [], mk_history_param(Path)), %% We corrupt the disklog. {ok, D} = file:open(filename:join(Path,"erlang-shell-log.1"), [read, append]), @@ -488,98 +1965,104 @@ shell_history_corrupt(Config) -> ok = file:close(D), {ok, Logs} = - rtnode([ - {putline, ""}, - {putdata, [$\^p]}, {expect, "init:stop\\(\\)[.]$"}, - {putdata, [$\^p]}, {expect, "echo[.]$"}, - {putdata, [$\n]}, - {expect, "echo\r\n"} - ], [], [], "-kernel shell_history enabled " ++ mk_sh_param(Path)), - - rtnode_check_logs("erlang.log.1", "Invalid chunk in the file", Logs), + rtnode:run( + [{putline, ""}, + {putdata, [$\^p]}, {expect, "echo2[.]$"}, + {putdata, [$\^p]}, {expect, "echo[.]$"}, + {putdata, [$\n]}, + {expect, "echo\r\n"} + ], [], [], mk_history_param(Path)), + + rtnode:check_logs("erlang.log.1", "Invalid chunk in the file", Logs), ok. %% Stop the node without closing the log. shell_history_halt(Path) -> try - rtnode([ - {putline, "echo."}, - {expect, "echo\r\n"}, - {sleep, 2500}, % disk_log internal cache timer is 2000 ms - {putline, "halt(0)."} - ], [], [], "-kernel shell_history enabled " ++ mk_sh_param(Path)) + rtnode:run( + [{putline, "echo."}, + {expect, "echo\r\n"}, + {sleep, 2500}, % disk_log internal cache timer is 2000 ms + {putline, "halt(0)."}, + {expect, "\r\n"}, + {sleep, 1000} %% wait for node to terminate + ], [], [], mk_history_param(Path)) catch _:_ -> ok end. shell_history_path(Config, TestCase) -> - filename:join([proplists:get_value(priv_dir, Config), - "shell_history", TestCase]). + filename:join([proplists:get_value(priv_dir, Config), + "shell_history", TestCase]). -mk_sh_param(Path) -> - "-kernel shell_history_path '\\\"" ++ Path ++ "\\\"'". +mk_history_param(Path) -> + ["-kernel","shell_history","enabled", + "-kernel","shell_history_path","\"" ++ Path ++ "\"", + "-kernel","shell_history_drop","[\"init:stop().\"]" + ]. shell_history_custom(_Config) -> %% Up key: Ctrl + P = Cp=[$\^p] - rtnode([{expect, "1> $"}, - %% {putline, ""}, - {putdata, [$\^p]}, {expect, "0[.]"}, - {putdata, [$\n]}, - {expect, "0\r\n"}, - {putline, "echo."}, - {expect, "!echo\r\n"} % exclamation mark is printed by custom history module - ], [], [], " -kernel shell_history " ++ atom_to_list(?MODULE) ++ - " -pz " ++ filename:dirname(code:which(?MODULE))), + rtnode:run( + [{expect, "1> $"}, + %% {putline, ""}, + {putdata, [$\^p]}, {expect, "0[.]"}, + {putdata, [$\n]}, + {expect, "0\r\n"}, + {putline, "echo."}, + {expect, "!echo\r\n"} % exclamation mark is printed by custom history module + ], [], [], ["-kernel","shell_history",atom_to_list(?MODULE), + "-pz",filename:dirname(code:which(?MODULE))]), ok. shell_history_custom_errors(_Config) -> %% Check that we can start with a node with an undefined %% provider module. - rtnode([{expect, "1> $"}, - {putline, "echo."}, - {expect, "echo\r\n"} - ], [], [], " -kernel shell_history very_broken " ++ - " -pz " ++ filename:dirname(code:which(?MODULE))), + rtnode:run( + [{putline, "echo."}, + {expect, "echo\r\n"} + ], [], [], ["-kernel","shell_history","very_broken", + "-pz",filename:dirname(code:which(?MODULE))]), %% Check that we can start with a node with a provider module %% that crashes in load/0. - rtnode([ - {putline, "echo."}, - {expect, "echo\r\n"} - ], [], [], " -kernel shell_history " ++ atom_to_list(?MODULE) ++ - " -kernel provider_load crash" ++ - " -pz " ++ filename:dirname(code:which(?MODULE))), + rtnode:run( + [{putline, "echo."}, + {expect, "echo\r\n"} + ], [], [], ["-kernel","shell_history",atom_to_list(?MODULE), + "-kernel","provider_load","crash", + "-pz",filename:dirname(code:which(?MODULE))]), %% Check that we can start with a node with a provider module %% that return incorrect in load/0. - rtnode([ - {putline, "echo."}, - {expect, "echo\r\n"} - ], [], [], " -kernel shell_history " ++ atom_to_list(?MODULE) ++ - " -kernel provider_load badreturn" ++ - " -pz " ++ filename:dirname(code:which(?MODULE))), + rtnode:run( + [{putline, "echo."}, + {expect, "echo\r\n"} + ], [], [], ["-kernel","shell_history",atom_to_list(?MODULE), + "-kernel","provider_load","badreturn", + "-pz",filename:dirname(code:which(?MODULE))]), %% Check that we can start with a node with a provider module %% that crashes in load/0. - rtnode([ - {putline, "echo."}, - {expect, "Disabling shell history logging.\r\n"}, - {expect, "echo\r\n"} - ], [], [], " -kernel shell_history " ++ atom_to_list(?MODULE) ++ - " -kernel provider_add crash" ++ - " -pz " ++ filename:dirname(code:which(?MODULE))), + rtnode:run( + [{putline, "echo."}, + {expect, "(Disabling shell history logging.|echo)"}, + {expect, "(Disabling shell history logging.|echo)"} + ], [], [], ["-kernel","shell_history",atom_to_list(?MODULE), + "-kernel","provider_add","crash", + "-pz",filename:dirname(code:which(?MODULE))]), %% Check that we can start with a node with a provider module %% that return incorrect in load/0. - rtnode([ - {putline, "echo."}, - {expect, "It returned {error,badreturn}.\r\n"}, - {expect, "echo\r\n"} - ], [], [], " -kernel shell_history " ++ atom_to_list(?MODULE) ++ - " -kernel provider_add badreturn" ++ - " -pz " ++ filename:dirname(code:which(?MODULE))), + rtnode:run( + [{putline, "echo."}, + {expect, "It returned {error,badreturn}.\r\n"}, + {expect, "echo\r\n"} + ], [], [], ["-kernel","shell_history",atom_to_list(?MODULE), + "-kernel","provider_add","badreturn", + "-pz",filename:dirname(code:which(?MODULE))]), ok. @@ -613,20 +2096,60 @@ job_control_local(Config) when is_list(Config) -> {skip,"No new shell found"}; new -> %% New shell tests - rtnode([{putline, ""}, - {expect, "1> $"}, - {putline, "2."}, - {expect, "\r\n2\r\n"}, - {putline, "\^g"}, - {expect, ["--> $"]}, - {putline, "s"}, - {expect, ["--> $"]}, - {putline, "c"}, - {expect, ["\r\nEshell"]}, - {expect, ["1> $"]}, - {putline, "35."}, - {expect, "\r\n35\r\n2> $"}], - []), + rtnode:run( + [{putline, ""}, + {expect, "1> $"}, + {putline, "2."}, + {expect, "\r\n2\r\n"}, + {putline, "\^g"}, + {expect, "--> $"}, + {putline, "s"}, + {expect, "--> $"}, + {putline, "c"}, + {expect, "\r\nEshell"}, + {expect, "1> $"}, + {putline, "35."}, + {expect, "\r\n35\r\n"}, + {expect, "2> $"}, + {putline, "receive M -> M end.\r\n"}, + {putline, "\^g"}, + {expect, "--> $"}, + {putline, "i 3"}, + {expect, "Unknown job"}, + {expect, "--> $"}, + {putline, "i 2"}, + {expect, "--> $"}, + {putline, "c"}, + {expect, "[*][*] exception exit: killed"}, + {expect, "[23]>"}, + {putline, "\^g"}, + {expect, "--> $"}, + {putline, "k 3"}, + {expect, "Unknown job"}, + {expect, "--> $"}, + {putline, "k 2"}, + {expect, "--> $"}, + {putline, "k"}, + {expect, "Unknown job"}, + {expect, "--> $"}, + {putline, "c"}, + {expect, "Unknown job"}, + {expect, "--> $"}, + {putline, "i"}, + {expect, "Unknown job"}, + {expect, "--> $"}, + {putline, "?"}, + {expect, "this message"}, + {expect, "--> $"}, + {putline, "h"}, + {expect, "this message"}, + {expect, "--> $"}, + {putline, "c 1"}, + {expect, "\r\n"}, + {putline, "35."}, + {expect, "\r\n35\r\n"}, + {expect, "[23]> $"} + ]), ok end. @@ -636,11 +2159,12 @@ job_control_remote(Config) when is_list(Config) -> old -> {skip,"No new shell found"}; _ -> - NSNode = start_node(?FUNCTION_NAME, []), + {ok, Peer, NSNode} = ?CT_PEER(#{ args => ["-connect_all","false"], + peer_down => continue }), try test_remote_job_control(NSNode) after - test_server:stop_node(NSNode) + peer:stop(Peer) end end. @@ -651,48 +2175,69 @@ job_control_remote_noshell(Config) when is_list(Config) -> old -> {skip,"No new shell found"}; _ -> - NSNode = start_node(?FUNCTION_NAME, ["-noshell"]), + {ok, Peer, NSNode} = ?CT_PEER(#{ name => peer:random_name(test_remote_job_control), + args => ["-connect_all","false","-noshell"], + peer_down => continue }), try test_remote_job_control(NSNode) after - test_server:stop_node(NSNode) + peer:stop(Peer) end end. test_remote_job_control(Node) -> - RemNode = create_nodename(), + RemNode = peer:random_name(test_remote_job_control), Pid = spawn_link(Node, fun() -> receive die -> ok end - end), + end), PidStr = erpc:call(Node, erlang, pid_to_list, [Pid]), true = erpc:call(Node, erlang, register, [kalaskula,Pid]), PrintedNode = printed_atom(Node), CookieString = printed_atom(erlang:get_cookie()), - rtnode([{putline, ""}, - {putline, "erlang:get_cookie()."}, - {expect, "\r\n\\Q" ++ CookieString ++ "\\E"}, - {putdata, "\^g"}, - {expect, " --> $"}, - {putline, "r " ++ PrintedNode}, - {expect, "\r\n"}, - {putline, "c"}, - {expect, "\r\n"}, - {expect, "Eshell"}, - {expect, "\\Q(" ++ atom_to_list(Node) ++")1> \\E$"}, - {putline, "whereis(kalaskula)."}, - {expect, PidStr}, - {putline, "exit()."}, - {expect, "[*][*][*] Shell process terminated!"}, - {putdata, "\^g"}, - {expect, " --> $"}, - {putline, "c 1"}, - {expect, "\r\n"}, - {putline, ""}, - {expect, "\\Q("++RemNode++")\\E[12]> $"} - ], RemNode), + rtnode:run( + [{putline, ""}, + {putline, "erlang:get_cookie()."}, + {expect, "\r\n\\Q" ++ CookieString ++ "\\E"}, + {putdata, "\^g"}, + {expect, " --> $"}, + {putline, "r " ++ PrintedNode}, + {expect, "\r\n"}, + {putline, "j"}, + {expect, "1 {shell,start,\\[init]}"}, + {expect, "2[*] {\\Q"++PrintedNode++"\\E,shell,start,\\[]}"}, + {expect, " --> $"}, + {putline, "c"}, + {expect, "\r\n"}, + {expect, "Eshell"}, + {expect, "\\Q(" ++ atom_to_list(Node) ++")1> \\E$"}, + {putline, "whereis(kalaskula)."}, + {expect, PidStr}, + {putline, "kalaskula ! die."}, + {putline, "exit()."}, + {expect, "[*][*][*] Shell process terminated!"}, + {putdata, "\^g"}, + {expect, " --> $"}, + {putline, "j"}, + {expect, "1 {shell,start,\\[init]}"}, + {expect, " --> $"}, + {putline, "c"}, + {expect, "Unknown job"}, + {expect, " --> $"}, + {putline, "c 1"}, + {expect, "\\Q("++RemNode++"@\\E[^)]*\\)[12]> $"}, + {putdata, "\^g"}, + {expect, " --> $"}, + {putline, "j"}, + {expect, "1[*] {shell,start,\\[init]}"}, + {putline, "c"}, + {expect, "\\Q("++RemNode++"@\\E[^)]*\\)[123]> $"}, + {sleep, 100}, + {putline, "35."}, + {expect, "\\Q("++RemNode++"@\\E[^)]*\\)[123]> $"} + ], RemNode), Pid ! die, ok. @@ -703,20 +2248,21 @@ ctrl_keys(_Config) -> Cy = [$\^y], Home = [27,$O,$H], End = [27,$O,$F], - rtnode([{putline,""}, - {putline,"2."}, - {expect,"2"}, - {putline,"\"hello "++Cw++"world\"."}, % test <CTRL>+W - {expect,"\"world\""}, - {putline,"\"hello "++Cu++"\"world\"."}, % test <CTRL>+U - {expect,"\"world\""}, - {putline,"world\"."++Home++"\"hello "}, % test <HOME> - {expect,"\"hello world\""}, - {putline,"world"++Home++"\"hello "++End++"\"."}, % test <END> - {expect,"\"hello world\""}, - {putline,"\"hello world\""++Cu++Cy++"."}, - {expect,"\"hello world\""}] ++ - wordLeft() ++ wordRight(), []), + rtnode:run( + [{putline,""}, + {putline,"2."}, + {expect,"2"}, + {putline,"\"hello "++Cw++"world\"."}, % test <CTRL>+W + {expect,"\"world\""}, + {putline,"\"hello "++Cu++"\"world\"."}, % test <CTRL>+U + {expect,"\"world\""}, + {putline,"world\"."++Home++"\"hello "}, % test <HOME> + {expect,"\"hello world\""}, + {putline,"world"++Home++"\"hello "++End++"\"."}, % test <END> + {expect,"\"hello world\""}, + {putline,"\"hello world\""++Cu++Cy++"."}, + {expect,"\"hello world\""}] ++ + wordLeft() ++ wordRight()), ok. wordLeft() -> @@ -743,7 +2289,7 @@ wordRight(Chars) -> %% Test that -remsh works remsh_basic(Config) when is_list(Config) -> - TargetNode = start_node(?FUNCTION_NAME, []), + {ok, Peer, TargetNode} = ?CT_PEER(), TargetNodeStr = printed_atom(TargetNode), [_Name,Host] = string:split(atom_to_list(node()), "@"), @@ -756,19 +2302,30 @@ remsh_basic(Config) when is_list(Config) -> %% Test that remsh works with explicit -sname. HostNode = atom_to_list(?FUNCTION_NAME) ++ "_host", HostNodeStr = printed_atom(list_to_atom(HostNode ++ "@" ++ Host)), - rtnode(PreCmds ++ - [{putline,"nodes()."}, - {expect, "\\Q" ++ HostNodeStr ++ "\\E"}] ++ - PostCmds, - HostNode, [], "-remsh " ++ TargetNodeStr), + rtnode:run( + PreCmds ++ + [{putline,"nodes()."}, + {expect, "\\Q" ++ HostNodeStr ++ "\\E"}] ++ + PostCmds, + HostNode, " ", "-remsh " ++ TargetNodeStr), %% Test that remsh works without -sname. - rtnode(PreCmds ++ PostCmds, [], [], " -remsh " ++ TargetNodeStr), + rtnode:run(PreCmds ++ PostCmds, [], " ", "-remsh " ++ TargetNodeStr), - test_server:stop_node(TargetNode), + %% Test that if multiple remsh are given, we select the first + rtnode:run([{expect, "Multiple"}] ++ PreCmds ++ PostCmds, + [], " ", + "-remsh " ++ TargetNodeStr ++ " -remsh invalid_node"), + + peer:stop(Peer), ok. +%% Test that if we cannot connect to a node, we get a correct error +remsh_error(_Config) -> + "Could not connect to \"invalid_node\"\n" = + os:cmd(ct:get_progname() ++ " -remsh invalid_node"). + quit_hosting_node() -> %% Command sequence for entering a shell on the hosting node. [{putdata, "\^g"}, @@ -788,529 +2345,108 @@ remsh_longnames(Config) when is_list(Config) -> "@127.0.0.1"; _ -> "" end, - case rtstart(" -name " ++ atom_to_list(?FUNCTION_NAME)++Domain) of - {ok, _SRPid, STPid, SState} -> + Name = peer:random_name(?FUNCTION_NAME), + case rtnode:start(" -name " ++ Name ++ Domain) of + {ok, _SRPid, STPid, SNode, SState} -> try - {ok, _CRPid, CTPid, CState} = - rtstart("-name undefined" ++ Domain ++ - " -remsh " ++ atom_to_list(?FUNCTION_NAME)), + {ok, _CRPid, CTPid, CNode, CState} = + rtnode:start("-name undefined" ++ Domain ++ + " -remsh " ++ Name), try - ok = send_commands( + ok = rtnode:send_commands( + SNode, STPid, [{putline, ""}, {putline, "node()."}, - {expect, "\\Q" ++ atom_to_list(?FUNCTION_NAME) ++ "\\E"}], 1), - ok = send_commands( + {expect, "\\Q" ++ Name ++ "\\E"}], 1), + ok = rtnode:send_commands( + CNode, CTPid, [{putline, ""}, {putline, "node()."}, - {expect, "\\Q" ++ atom_to_list(?FUNCTION_NAME) ++ "\\E"} | quit_hosting_node()], 1) + {expect, "\\Q" ++ Name ++ "\\E"} | quit_hosting_node()], 1) after - rtnode_dump_logs(rtstop(CState)) + rtnode:dump_logs(rtnode:stop(CState)) end after - rtnode_dump_logs(rtstop(SState)) + rtnode:dump_logs(rtnode:stop(SState)) end; - Else -> + {skip, _} = Else -> Else end. %% Test that -remsh works without epmd. remsh_no_epmd(Config) when is_list(Config) -> - EPMD_ARGS = "-start_epmd false -erl_epmd_port 12345 ", - case rtstart([],"ERL_EPMD_PORT=12345 ", - EPMD_ARGS ++ " -sname " ++ atom_to_list(?FUNCTION_NAME)) of - {ok, _SRPid, STPid, SState} -> + EPMD_ARGS = "-start_epmd false -erl_epmd_port 12346 ", + Name = ?CT_PEER_NAME(), + case rtnode:start([],"ERL_EPMD_PORT=12345 ", + EPMD_ARGS ++ " -sname " ++ Name) of + {ok, _SRPid, STPid, SNode, SState} -> try - ok = send_commands( + ok = rtnode:send_commands( + SNode, STPid, [{putline, ""}, {putline, "node()."}, - {expect, "\\Q" ++ atom_to_list(?FUNCTION_NAME) ++ "\\E"}], 1), - {ok, _CRPid, CTPid, CState} = - rtstart([],"ERL_EPMD_PORT=12345 ", - EPMD_ARGS ++ " -remsh "++atom_to_list(?FUNCTION_NAME)), + {expect, "\\Q" ++ Name ++ "\\E"}], 1), + {ok, _CRPid, CTPid, CNode, CState} = + rtnode:start([],"ERL_EPMD_PORT=12345 ", + EPMD_ARGS ++ " -remsh "++Name), try - ok = send_commands( + ok = rtnode:send_commands( + CNode, CTPid, [{putline, ""}, {putline, "node()."}, - {expect, "\\Q" ++ atom_to_list(?FUNCTION_NAME) ++ "\\E"} | quit_hosting_node()], 1) + {expect, "\\Q" ++ Name ++ "\\E"} | quit_hosting_node()], 1) after - rtstop(CState) + rtnode:dump_logs(rtnode:stop(CState)) end after - rtstop(SState) + rtnode:dump_logs(rtnode:stop(SState)) end; - Else -> + {skip, _} = Else -> Else end. - -rtnode(C) -> - rtnode(C, []). - -rtnode(C, N) -> - rtnode(C, N, []). - -rtnode(Commands, Nodename, ErlPrefix) -> - rtnode(Commands, Nodename, ErlPrefix, []). - -rtnode(Commands, Nodename, ErlPrefix, Args) -> - case rtstart(Nodename, ErlPrefix, Args) of - {ok, _SPid, CPid, RTState} -> - Res = catch send_commands(CPid, Commands, 1), - Logs = rtstop(RTState), - case Res of - ok -> - rtnode_dump_logs(Logs), - ok; - _ -> - rtnode_dump_logs(Logs), - ok = Res - end, - {ok, Logs}; - Skip -> - Skip - end. - -rtstart(Args) -> - rtstart([], [], Args). - -rtstart(Nodename, ErlPrefix, Args) -> - case get_progs() of - {error,_Reason} -> - {skip,"No runerl present"}; - {RunErl,ToErl,Erl} -> - case create_tempdir() of - {error, Reason2} -> - {skip, Reason2}; - Tempdir -> - SPid = - start_runerl_node(RunErl,ErlPrefix++"\\\""++Erl++"\\\"", - Tempdir,Nodename,Args), - CPid = start_toerl_server(ToErl,Tempdir), - {ok, SPid, CPid, {CPid, SPid, ToErl, Tempdir}} - end - end. - -rtstop({CPid, SPid, ToErl, Tempdir}) -> - case stop_runerl_node(CPid) of - {error,_} -> - catch rtstop_try_harder(ToErl, Tempdir); - _ -> - ok - end, - wait_for_runerl_server(SPid), - Logs = rtnode_read_logs(Tempdir), - file:del_dir_r(Tempdir), - Logs. - -rtstop_try_harder(ToErl, Tempdir) -> - CPid = start_toerl_server(ToErl, Tempdir), - ok = send_commands(CPid, - [{putline,[7]}, - {expect, " --> $"}, - {putline, "s"}, - {putline, "c"}, - {putline, ""}], 1), - stop_runerl_node(CPid). - -timeout(longest) -> - timeout(long) + timeout(normal); -timeout(long) -> - 2 * timeout(normal); -timeout(short) -> - timeout(normal) div 10; -timeout(normal) -> - 10000 * test_server:timetrap_scale_factor(). - -start_node(Name, Args0) -> - PaDir = filename:dirname(code:which(?MODULE)), - Args1 = ["-pa",PaDir|Args0], - Args = lists:append(lists:join(" ", Args1)), - {ok, Node} = test_server:start_node(Name, slave, [{args,Args}]), - Node. - -send_commands(CPid, [{sleep, X}|T], N) -> - ?dbg({sleep, X}), - receive - after X -> - send_commands(CPid, T, N+1) - end; -send_commands(CPid, [{expect, Expect}|T], N) when is_list(Expect) -> - ?dbg(Exp), - case command(CPid, {expect, [Expect], timeout(normal)}) of - ok -> - send_commands(CPid, T, N + 1); - {expect_timeout, Got} -> - ct:pal("expect timed out waiting for ~p\ngot: ~p\n", [Expect,Got]), - {error, timeout}; - Other -> - Other - end; -send_commands(CPid, [{putline, Line}|T], N) -> - send_commands(CPid, [{putdata, Line ++ "\n"}|T], N); -send_commands(CPid, [{putdata, Data}|T], N) -> - ?dbg({putdata, Data}), - case command(CPid, {send_data, Data}) of - ok -> - send_commands(CPid, T, N+1); - Error -> - Error - end; -send_commands(_CPid, [], _) -> - ok. - -command(Pid, Req) -> - Timeout = timeout(longest), - Ref = erlang:monitor(process, Pid), - Pid ! {self(), Ref, Req}, - receive - {Ref, Reply} -> - erlang:demonitor(Ref, [flush]), - Reply; - {'DOWN', Ref, _, _, Reason} -> - {error, Reason} - after Timeout -> - io:format("timeout while executing ~p\n", [Req]), - {error, timeout} - end. - -wait_for_runerl_server(SPid) -> - Ref = erlang:monitor(process, SPid), - Timeout = timeout(long), - receive - {'DOWN', Ref, process, SPid, _Reason} -> - ok - after Timeout -> - {error, runerl_server_timeout} - end. - -stop_runerl_node(CPid) -> - Ref = erlang:monitor(process, CPid), - CPid ! {self(), kill_emulator}, - Timeout = timeout(longest), - receive - {'DOWN', Ref, process, CPid, noproc} -> - ok; - {'DOWN', Ref, process, CPid, normal} -> - ok; - {'DOWN', Ref, process, CPid, {error, Reason}} -> - {error, Reason} - after Timeout -> - {error, toerl_server_timeout} - end. - -get_progs() -> - try - do_get_progs() - catch - throw:Thrown -> - {error, Thrown} - end. - -do_get_progs() -> - case os:type() of - {unix,freebsd} -> - throw("Can't use run_erl on FreeBSD"); - {unix,openbsd} -> - throw("Can't use run_erl on OpenBSD"); - {unix,_} -> - RunErl = find_executable("run_erl"), - ToErl = find_executable("to_erl"), - Erl = find_executable("erl"), - {RunErl, ToErl, Erl}; - _ -> - throw("Not a Unix OS") - end. - -find_executable(Name) -> - case os:find_executable(Name) of - Prog when is_list(Prog) -> - Prog; - false -> - throw("Could not find " ++ Name) - end. - -create_tempdir() -> - create_tempdir(filename:join(["/tmp","rtnode"++os:getpid()]),$A). - -create_tempdir(Dir,X) when X > $Z, X < $a -> - create_tempdir(Dir,$a); -create_tempdir(Dir,X) when X > $z -> - Estr = lists:flatten( - io_lib:format("Unable to create ~s, reason eexist", - [Dir++[$z]])), - {error, Estr}; -create_tempdir(Dir0, Ch) -> - %% Expect fairly standard unix. - Dir = Dir0++[Ch], - case file:make_dir(Dir) of - {error, eexist} -> - create_tempdir(Dir0, Ch+1); - {error, Reason} -> - Estr = lists:flatten( - io_lib:format("Unable to create ~s, reason ~p", - [Dir,Reason])), - {error,Estr}; - ok -> - Dir - end. - -create_nodename() -> - create_nodename($A). - -create_nodename(X) when X > $Z, X < $a -> - create_nodename($a); -create_nodename(X) when X > $z -> - {error,out_of_nodenames}; -create_nodename(X) -> - NN = "rtnode"++os:getpid()++[X], - case file:read_file_info(filename:join(["/tmp",NN])) of - {error,enoent} -> - Host = lists:nth(2,string:tokens(atom_to_list(node()),"@")), - NN++"@"++Host; - _ -> - create_nodename(X+1) - end. - - -start_runerl_node(RunErl,Erl,Tempdir,Nodename,Args) -> - XArg = case Nodename of - [] -> - []; - _ -> - " -sname "++(if is_atom(Nodename) -> atom_to_list(Nodename); - true -> Nodename - end)++ - " -setcookie "++atom_to_list(erlang:get_cookie()) - end ++ " " ++ Args, - spawn(fun() -> start_runerl_command(RunErl, Tempdir, Erl++XArg) end). - -start_runerl_command(RunErl, Tempdir, Cmd) -> - FullCmd = "\""++RunErl++"\" "++Tempdir++"/ "++Tempdir++" \""++Cmd++"\"", - ct:pal("~ts",[FullCmd]), - os:cmd(FullCmd). - -start_toerl_server(ToErl,Tempdir) -> - Pid = spawn(?MODULE,toerl_server,[self(),ToErl,Tempdir]), - receive - {Pid,started} -> - Pid; - {Pid,error,Reason} -> - {error,Reason} - end. - -try_to_erl(_Command, 0) -> - {error, cannot_to_erl}; -try_to_erl(Command, N) -> - ?dbg({?LINE,N}), - Port = open_port({spawn, Command},[eof]), - Timeout = timeout(short) div 2, - receive - {Port, eof} -> - timer:sleep(Timeout), - try_to_erl(Command, N-1) - after Timeout -> - ?dbg(Port), - Port - end. - -toerl_server(Parent, ToErl, TempDir) -> - Port = try_to_erl("\""++ToErl++"\" "++TempDir++"/ 2>/dev/null", 8), - case Port of - P when is_port(P) -> - Parent ! {self(),started}; - {error,Other} -> - Parent ! {self(),error,Other}, - exit(Other) - end, - - State = #{port => Port, acc => [], kill_emulator_command => init_stop}, - case toerl_loop(State) of - normal -> - ok; - {error, Reason} -> - error_logger:error_msg("toerl_server exit with reason ~p~n", - [Reason]), - exit(Reason) - end. - -toerl_loop(#{port := Port} = State0) -> - ?dbg({toerl_loop, Port, map_get(acc, State0), - maps:get(match, State0, nomatch)}), - - State = handle_expect(State0), - - receive - {Port,{data,Data}} when is_port(Port) -> - ?dbg({?LINE,Port,{data,Data}}), - toerl_loop(State#{acc => map_get(acc, State) ++ Data}); - {Pid, Ref, {expect, Expect, Timeout}} -> - toerl_loop(init_expect(Pid, Ref, Expect, Timeout, State)); - {Pid, Ref, {send_data, Data}} -> - Port ! {self(), {command, Data}}, - Pid ! {Ref, ok}, - toerl_loop(State); - {_Pid, kill_emulator} -> - kill_emulator(State); - {timeout,Timer,expect_timeout} -> - toerl_loop(handle_expect_timeout(Timer, State)); - {Port, eof} -> - {error, unexpected_eof}; - Other -> - {error, {unexpected, Other}} - end. - -kill_emulator(#{port := Port}) -> - %% If the line happens to end in a ".", issuing "init:stop()." - %% will result in a syntax error. To avoid that, issue a "\n" - %% before "init:stop().". - Port ! {self(),{command, "\ninit:stop().\n"}}, - wait_for_eof(Port). - -wait_for_eof(Port) -> - receive - {Port,eof} -> - normal; - _Other -> - wait_for_eof(Port) - after - timeout(long) -> - {error, kill_timeout} - end. - -init_expect(Pid, Ref, ExpectList, Timeout, State) -> - try compile_expect(ExpectList) of - Expect -> - Exp = #{expect => Expect, - ref => Ref, - source => ExpectList, - timer => erlang:start_timer(Timeout, self(), expect_timeout), - from => Pid}, - State#{expect => Exp} - catch - Class:Reason:Stk -> - io:put_chars("Compilation of expect pattern failed:"), - io:format("~p\n", [ExpectList]), - io:put_chars(erl_error:format_exception(Class, Reason, Stk)), - exit(expect_pattern_error) - end. - -handle_expect(#{acc := Acc, expect := Exp} = State) -> - #{expect := Expect, from := Pid, ref := Ref} = Exp, - case Expect(Acc) of - nomatch -> - State; - {matched, Eaten, Result} -> - Pid ! {Ref, Result}, - finish_expect(Eaten, State) - end; -handle_expect(State) -> - State. - -handle_expect_timeout(Timer, State) -> - #{acc := Acc, expect := Exp} = State, - #{expect := Expect, timer := Timer, from := Pid, ref := Ref} = Exp, - case Expect({timeout, Acc}) of - nomatch -> - Result = {expect_timeout, Acc}, - Pid ! {Ref, Result}, - finish_expect(0, State); - {matched, Eaten, Result} -> - Pid ! {Ref, Result}, - finish_expect(Eaten, State) +remsh_expand_compatibility_25(Config) when is_list(Config) -> + {ok, _Peer, TargetNode} = ?CT_PEER(#{}), %% Create a vsn 26 node + NodeName = atom_to_list(TargetNode), %% compatibility + %% Start a node on vsn 25 but run the shell on vsn 26 + case rtnode:start(peer:random_name(), "stty columns 200; ERL_AFLAGS= ", "-remsh "++NodeName, [{release, "25"}|Config]) of + {ok, _SRPid, STPid, _, SState} -> + try + ok = rtnode:send_commands(undefined, + STPid, + [{putdata, "erlang:is_atom\t"}, + {expect, "\\Qerlang:is_atom(\\E"}| + quit_hosting_node()], 1) + after + Logs = rtnode:stop(SState), + rtnode:dump_logs(Logs) + end; + Else when element(1, Else) =/= ok -> Else end. - -finish_expect(Eaten, #{acc := Acc0, - expect := #{timer := Timer}}=State) -> - erlang:cancel_timer(Timer), - receive - {timeout,Timer,timeout} -> - ok - after 0 -> - ok - end, - Acc = lists:nthtail(Eaten, Acc0), - maps:remove(expect, State#{acc := Acc}). - -compile_expect([{timeout,Action}|T]) when is_function(Action, 1) -> - Next = compile_expect(T), - fun({timeout, _}=Tm) -> - {matched, 0, Action(Tm)}; - (Subject) -> - Next(Subject) - end; -compile_expect([{{re,RE0},Action}|T]) when is_binary(RE0), is_function(Action, 1) -> - {ok, RE} = re:compile(RE0), - Next = compile_expect(T), - fun({timeout, _}=Subject) -> - Next(Subject); - (Subject) -> - case re:run(Subject, RE, [{capture,first,index}]) of - nomatch -> - Next(Subject); - {match, [{Pos,Len}]} -> - Matched = binary:part(list_to_binary(Subject), Pos, Len), - {matched, Pos+Len, Action(Matched)} +remsh_expand_compatibility_later_version(Config) when is_list(Config) -> + PrivDir = proplists:get_value(priv_dir, Config), + case ?CT_PEER_REL([], "25", PrivDir) of + not_available -> {skip, "25 not available"}; + {ok, _Peer, TargetNode} -> + NodeName = atom_to_list(TargetNode), + %% Start a node on later version but run the shell on vsn 25 + case rtnode:start(peer:random_name(), "stty columns 200; ERL_AFLAGS= ", "-remsh " ++ NodeName, Config) of + {ok, _SRPid, STPid, _, SState} -> + try + ok = rtnode:send_commands(undefined, + STPid, + [{putdata, "erlang:is_atom\t"}, + {expect, "\\Qerlang:is_atom(\\E"}| + quit_hosting_node()], 1) + after + Logs = rtnode:stop(SState), + rtnode:dump_logs(Logs) + end; + Else when element(1, Else) =/= ok -> Else end - end; -compile_expect([RE|T]) when is_list(RE) -> - Ok = fun(_) -> ok end, - compile_expect([{{re,list_to_binary(RE)},Ok}|T]); -compile_expect([]) -> - fun(_) -> - nomatch - end. - -rtnode_check_logs(Logname, Pattern, Logs) -> -rtnode_check_logs(Logname, Pattern, true, Logs). -rtnode_check_logs(Logname, Pattern, Match, Logs) -> - case re:run(maps:get(Logname, Logs), Pattern) of - {match, [_]} when Match -> - ok; - nomatch when not Match -> - ok; - _ -> - rtnode_dump_logs(Logs), - ct:fail("~p not found in log ~ts",[Pattern, Logname]) - end. - -rtnode_dump_logs(Logs) -> - maps:foreach( - fun(File, Data) -> - ct:pal("~ts: ~ts",[File, Data]) - end, Logs). - -rtnode_read_logs(Tempdir) -> - {ok, LogFiles0} = file:list_dir(Tempdir), - - %% Make sure that we only read log files and not any named pipes. - LogFiles = [F || F <- LogFiles0, - case F of - "erlang.log" ++ _ -> true; - _ -> false - end], - - lists:foldl( - fun(File, Acc) -> - case file:read_file(filename:join(Tempdir, File)) of - {ok, Data} -> - Acc#{ File => Data }; - _ -> - Acc - end - end, #{}, LogFiles). - -get_default_shell() -> - try - rtnode([{putline,""}, - {putline, "is_pid(whereis(user_drv))."}, - {expect, "true\r\n"}], []), - new - catch _E:_R -> - ?dbg({_E,_R}), - old end. printed_atom(A) -> |