%%
%% %CopyrightBegin%
%%
%% Copyright Ericsson AB 1997-2021. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%%     http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%
%% %CopyrightEnd%
%%
-module(erlc_SUITE).

%% Tests the erlc command by compiling various types of files.

-export([all/0, suite/0,groups/0,init_per_suite/1, end_per_suite/1, 
         init_per_group/2,end_per_group/2, compile_erl/1,
         compile_yecc/1, compile_script/1,
         compile_mib/1, good_citizen/1, deep_cwd/1, arg_overflow/1,
         make_dep_options/1]).

-include_lib("common_test/include/ct.hrl").

suite() -> [{ct_hooks,[ts_install_cth]}].

all() -> 
    [{group,with_server},{group,without_server}].

groups() ->
    Tests = tests(),
    [{with_server,[],Tests},
     {without_server,[],Tests}].

tests() ->
    [compile_erl, compile_yecc, compile_script, compile_mib,
     good_citizen, deep_cwd, arg_overflow, make_dep_options].

init_per_suite(Config) ->
    Config.

end_per_suite(_Config) ->
    ok.

init_per_group(with_server, Config) ->
    os:putenv("ERLC_USE_SERVER", "yes"),
    Config;
init_per_group(without_server, Config) ->
    os:putenv("ERLC_USE_SERVER", "no"),
    Config;
init_per_group(_, Config) ->
    Config.

end_per_group(_GroupName, Config) ->
    os:unsetenv("ERLC_USE_SERVER"),
    Config.

%% Copy from erlc_SUITE_data/include/erl_test.hrl.

-record(person, {name, shoe_size}).

%% Tests that compiling Erlang source code works.

compile_erl(Config) when is_list(Config) ->
    {SrcDir, OutDir, Cmd0} = get_cmd(Config),
    Cmd = Cmd0 ++ " +brief",
    FileName = filename:join(SrcDir, "erl_test_ok.erl"),

    %% By default, warnings are now turned on.
    run(Config, Cmd, FileName, "",
        ["Warning: function foo/0 is unused\$", "_OK_"]),

    %% Test that the compiled file is where it should be,
    %% and that it is runnable.

    {module, erl_test_ok} = code:load_abs(filename:join(OutDir, "erl_test_ok")),
    42 = erl_test_ok:shoe_size(#person{shoe_size=42}),
    code:purge(erl_test_ok),

    %% Try disabling warnings.

    run(Config, Cmd, FileName, "-W0", ["_OK_"]),

    %% Try treating warnings as errors.

    run(Config, Cmd, FileName, "-Werror",
        ["compile: warnings being treated as errors\$",
         "function foo/0 is unused\$", "_ERROR_"]),

    %% Check a bad file.

    BadFile = filename:join(SrcDir, "erl_test_bad.erl"),
    run(Config, Cmd, BadFile, "", ["function non_existing/1 undefined\$",
                                   "_ERROR_"]),
    ok.

%% Test that compiling yecc source code works.

compile_yecc(Config) when is_list(Config) ->
    {SrcDir, _, OutDir} = get_dirs(Config),
    Cmd = erlc() ++ " -o" ++ OutDir ++ " ",
    FileName = filename:join(SrcDir, "yecc_test_ok.yrl"),
    run(Config, Cmd, FileName, "-W0", ["_OK_"]),
    true = exists(filename:join(OutDir, "yecc_test_ok.erl")),

    BadFile = filename:join(SrcDir, "yecc_test_bad.yrl"),
    run(Config, Cmd, BadFile, "-W0",
        ["Nonterminals is missing\$",
         "rootsymbol form is not a nonterminal\$",
         "undefined nonterminal: form\$",
         "_ERROR_"]),
    exists(filename:join(OutDir, "yecc_test_ok.erl")),
    ok.

%% Test that compiling start scripts works.

compile_script(Config) when is_list(Config) ->
    {SrcDir, OutDir, Cmd} = get_cmd(Config),
    FileName = filename:join(SrcDir, "start_ok.script"),
    run(Config, Cmd, FileName, "", ["_OK_"]),
    true = exists(filename:join(OutDir, "start_ok.boot")),

    BadFile = filename:join(SrcDir, "start_bad.script"),
    run(Config, Cmd, BadFile, "", ["syntax error before:", "_ERROR_"]),
    ok.

%% Test that compiling SNMP mibs works.

compile_mib(Config) when is_list(Config) ->
    {SrcDir, OutDir, Cmd} = get_cmd(Config),
    FileName = filename:join(SrcDir, "GOOD-MIB.mib"),
    run(Config, Cmd, FileName, "", ["_OK_"]),
    Output = filename:join(OutDir, "GOOD-MIB.bin"),
    true = exists(Output),

    %% Try -W option.

    ok = file:delete(Output),
    run(Config, Cmd, FileName, "-W",
        ["_OK_"]),
    true = exists(Output),

    %% Try -W option and more verbose.

    ok = file:delete(Output),
    case test_server:os_type() of
        {unix,_} ->
            run(Config, Cmd, FileName, "-W +'{verbosity,info}'",
                ["\\[GOOD-MIB[.]mib\\]\\[INF\\]: No accessfunction for 'sysDescr' => using default",
                 "_OK_"]),
            true = exists(Output),
            ok = file:delete(Output);
        _ -> ok				%Don't bother -- too much work.
    end,

    %% Try a bad file.

    BadFile = filename:join(SrcDir, "BAD-MIB.mib"),
    run(Config, Cmd, BadFile, "",
        ["BAD-MIB.mib: 1: syntax error before: mibs\$",
         "compilation_failed_ERROR_"]),

    %% Make sure that no -I option works.

    NewCmd = erlc() ++ " -o" ++ OutDir ++ " ",
    run(Config, NewCmd, FileName, "", ["_OK_"]),
    true = exists(Output),

    ok.

%% Checks that 'erlc' doesn't eat any input (important when called from a
%% shell script with redirected input).
good_citizen(Config) when is_list(Config) ->
    case os:type() of
        {unix, _} ->
            PrivDir = proplists:get_value(priv_dir, Config),
            Answer = filename:join(PrivDir, "answer"),
            Script = filename:join(PrivDir, "test_script"),
            Test = filename:join(PrivDir, "test.erl"),
            S = ["#! /bin/sh\n", "erlc ", Test, "\n",
                 "read reply\n", "echo $reply\n"],
            ok = file:write_file(Script, S),
            ok = file:write_file(Test, "-module(test).\n"),
            Cmd = "echo y | sh " ++ Script ++ " > " ++ Answer,
            os:cmd(Cmd),
            {ok, Answer0} = file:read_file(Answer),
            [$y|_] = binary_to_list(Answer0),
            ok;
        _ ->
            {skip, "Unix specific"}
    end.

%% Make sure that compiling an Erlang module deep down in
%% in a directory with more than 255 characters works.
deep_cwd(Config) when is_list(Config) ->
    case os:type() of
        {unix, _} ->
            PrivDir = proplists:get_value(priv_dir, Config),
            deep_cwd_1(PrivDir);
        _ ->
            {skip, "Only a problem on Unix"}
    end.

deep_cwd_1(PrivDir) ->
    DeepDir0 = filename:join(PrivDir, lists:duplicate(128, $a)),
    DeepDir = filename:join(DeepDir0, lists:duplicate(128, $b)),
    ok = filelib:ensure_dir(filename:join(DeepDir,"any_file")),
    ok = file:set_cwd(DeepDir),
    ok = file:write_file("test.erl", "-module(test).\n\n"),
    io:format("~s\n", [os:cmd("erlc test.erl")]),
    true = filelib:is_file("test.beam"),
    ok.

%% Test that a large number of command line switches does not
%% overflow the argument buffer
arg_overflow(Config) when is_list(Config) ->
    {SrcDir, _OutDir, Cmd0} = get_cmd(Config),
    Cmd = Cmd0 ++ " +brief",
    FileName = filename:join(SrcDir, "erl_test_ok.erl"),
    %% Each -D option will be expanded to three arguments when
    %% invoking 'erl'.
    NumDOptions = num_d_options(),
    Args = lists:flatten([ ["-D", integer_to_list(N, 36), "=1 "] ||
                           N <- lists:seq(1, NumDOptions) ]),
    run(Config, Cmd, FileName, Args,
        ["Warning: function foo/0 is unused\$",
         "_OK_"]),
    ok.

num_d_options() ->
    case {os:type(),os:version()} of
        {{win32,_},_} ->
            %% The maximum size of a command line in the command
            %% shell on Windows is 8191 characters.
            %% Each -D option is expanded to "@dv NN 1", i.e.
            %% 8 characters. (Numbers up to 1295 can be expressed
            %% as two 36-base digits.)
            1000;
        {{unix,linux},Version} when Version < {2,6,23} ->
            %% On some older 64-bit versions of Linux, the maximum number
            %% of arguments is 16383.
            %% See: http://www.in-ulm.de/~mascheck/various/argmax/
            5440;
        {{unix,darwin},{Major,_,_}} when Major >= 11 ->
            %% "getconf ARG_MAX" still reports 262144 (as in previous
            %% version of MacOS X), but the useful space seem to have
            %% shrunk significantly (or possibly the number of arguments).
            %% 7673
            7500;
        {_,_} ->
            12000
    end.

erlc() ->
    case os:find_executable("erlc") of
        false ->
            ct:fail("Can't find erlc");
        Erlc ->
            "\"" ++ Erlc ++ "\""
    end.

make_dep_options(Config) ->
    {SrcDir,OutDir,Cmd0} = get_cmd(Config),
    Cmd = Cmd0 ++ " +brief",
    FileName = filename:join(SrcDir, "erl_test_ok.erl"),
    BeamFileName = filename:join(OutDir, "erl_test_ok.beam"),

    DepRE = ["/erl_test_ok[.]beam: \\\\$",
             "/erlc_SUITE_data/src/erl_test_ok[.]erl \\\\$",
             "/erlc_SUITE_data/include/erl_test[.]hrl$",
             "_OK_"],

    DepRETarget =
    ["^target: \\\\$",
     "/erlc_SUITE_data/src/erl_test_ok[.]erl \\\\$",
     "/erlc_SUITE_data/include/erl_test[.]hrl$",
     "_OK_"],

    DepREMP =
    ["/erl_test_ok[.]beam: \\\\$",
     "/erlc_SUITE_data/src/erl_test_ok[.]erl \\\\$",
     "/erlc_SUITE_data/include/erl_test[.]hrl$",
     [],
     "/erlc_SUITE_data/include/erl_test.hrl:$",
     "_OK_"],

    DepREMissing =
    ["/erl_test_missing_header[.]beam: \\\\$",
     "/erlc_SUITE_data/src/erl_test_missing_header[.]erl \\\\$",
     "/erlc_SUITE_data/include/erl_test[.]hrl \\\\$",
     "missing.hrl$",
     "_OK_"],

    file:delete(BeamFileName),

    %% Test plain -M
    run(Config, Cmd, FileName, "-M", DepRE),
    false = exists(BeamFileName),

    %% Test -MF File
    DepFile = filename:join(OutDir, "my.deps"),
    run(Config, Cmd, FileName, "-MF "++DepFile, ["_OK_"]),
    {ok,MFBin} = file:read_file(DepFile),
    verify_result(binary_to_list(MFBin)++["_OK_"], DepRE),
    false = exists(BeamFileName),

    %% Test -MD
    run(Config, Cmd, FileName, "-MD", ["_OK_"]),
    MDFile = filename:join(OutDir, "erl_test_ok.Pbeam"),
    {ok,MFBin} = file:read_file(MDFile),
    file:delete(MDFile), %% used further down!
    false = exists(BeamFileName),

    %% Test -M -MT Target
    run(Config, Cmd, FileName, "-M -MT target", DepRETarget),
    false = exists(BeamFileName),

    %% Test -MF File -MT Target
    TargetDepFile = filename:join(OutDir, "target.deps"),
    run(Config, Cmd, FileName, "-MF "++TargetDepFile++" -MT target",
        ["_OK_"]),
    {ok,TargetBin} = file:read_file(TargetDepFile),
    verify_result(binary_to_list(TargetBin)++["_OK_"], DepRETarget),
    file:delete(TargetDepFile),
    false = exists(BeamFileName),

    %% Test -MD -MT Target
    run(Config, Cmd, FileName, "-MD -MT target", ["_OK_"]),
    TargetMDFile = filename:join(OutDir, "erl_test_ok.Pbeam"),
    {ok,TargetBin} = file:read_file(TargetMDFile),
    file:delete(TargetDepFile),
    false = exists(BeamFileName),

    %% Test -M -MQ Target. (Note: Passing a $ on the command line
    %% portably for Unix and Windows is tricky, so we will just test
    %% that MQ works at all.)
    run(Config, Cmd, FileName, "-M -MQ target", DepRETarget),
    false = exists(BeamFileName),

    %% Test -M -MP
    run(Config, Cmd, FileName, "-M -MP", DepREMP),
    false = exists(BeamFileName),

    %% Test -M -MG
    MissingHeader = filename:join(SrcDir, "erl_test_missing_header.erl"),
    run(Config, Cmd, MissingHeader, "-M -MG", DepREMissing),
    false = exists(BeamFileName),

    %%
    %% check the above variants with side-effect -MMD
    %%

    %% since compiler is run on the erlang code a warning will be
    %% issued by the compiler, match that.
    WarningRE =
         "/erlc_SUITE_data/src/erl_test_ok.erl:[0-9]+:[0-9]+: "
        "Warning: function foo/0 is unused$",
    ErrorRE = "/erlc_SUITE_data/src/erl_test_missing_header.erl:"
        "[0-9]+:[0-9]+: can't find include file \"missing.hrl\"$",

    DepRE_MMD = insert_before("_OK_", WarningRE, DepRE),
    DepRETarget_MMD = insert_before("_OK_", WarningRE, DepRETarget),
    DepREMP_MMD = insert_before("_OK_",WarningRE,DepREMP),
    DepREMissing_MMD = (insert_before("_OK_",ErrorRE,DepREMissing)--
                            ["_OK_"]) ++ ["_ERROR_"],
    CompRE = [WarningRE,"_OK_"],


    %% Test plain -MMD -M
    run(Config, Cmd, FileName, "-MMD -M", DepRE_MMD),
    true = exists(BeamFileName),
    file:delete(BeamFileName),

    %% Test -MMD -MF File
    DepFile = filename:join(OutDir, "my.deps"),
    run(Config, Cmd, FileName, "-MMD -MF "++DepFile, CompRE),
    {ok,MFBin} = file:read_file(DepFile),
    verify_result(binary_to_list(MFBin)++["_OK_"], DepRE),
    true = exists(BeamFileName),
    file:delete(BeamFileName),

    %% Test -MMD -MD
    run(Config, Cmd, FileName, "-MMD -MD", CompRE),
    MDFile = filename:join(OutDir, "erl_test_ok.Pbeam"),
    {ok,MFBin} = file:read_file(MDFile),
    file:delete(MDFile), %% used further down!
    true = exists(BeamFileName),
    file:delete(BeamFileName),

    %% Test -MMD -M -MT Target
    run(Config, Cmd, FileName, "-MMD -M -MT target", DepRETarget_MMD),
    true = exists(BeamFileName),
    file:delete(BeamFileName),

    %% Test -MMD -MF File -MT Target
    TargetDepFile = filename:join(OutDir, "target.deps"),
    run(Config, Cmd, FileName, "-MMD -MF "++TargetDepFile++" -MT target",
        CompRE),
    {ok,TargetBin} = file:read_file(TargetDepFile),
    verify_result(binary_to_list(TargetBin)++["_OK_"], DepRETarget),
    file:delete(TargetDepFile),
    true = exists(BeamFileName),
    file:delete(BeamFileName),

    %% Test -MMD -MD -MT Target
    run(Config, Cmd, FileName, "-MMD -MD -MT target", CompRE),
    TargetMDFile = filename:join(OutDir, "erl_test_ok.Pbeam"),
    {ok,TargetBin} = file:read_file(TargetMDFile),
    file:delete(TargetDepFile),
    true = exists(BeamFileName),
    file:delete(BeamFileName),

    %% Test -MMD -M -MQ Target. (Note: Passing a $ on the command line
    %% portably for Unix and Windows is tricky, so we will just test
    %% that MQ works at all.)
    run(Config, Cmd, FileName, "-MMD -M -MQ target", DepRETarget_MMD),
    true = exists(BeamFileName),
    file:delete(BeamFileName),

    %% Test -MMD -M -MP
    run(Config, Cmd, FileName, "-MMD -M -MP", DepREMP_MMD),
    true = exists(BeamFileName),
    file:delete(BeamFileName),

    %% Test -MMD -M -MG
    MissingHeader = filename:join(SrcDir, "erl_test_missing_header.erl"),
    run(Config, Cmd, MissingHeader, "-MMD -M -MG", DepREMissing_MMD),
    false = exists(BeamFileName),
    ok.

%% Runs a command.

run(Config, Cmd0, Name, Options, Expect) ->
    Cmd = Cmd0 ++ " " ++ Options ++ " " ++ Name,
    io:format("~ts", [Cmd]),
    Result = run_command(Config, Cmd),
    verify_result(Result, Expect).

verify_result(Result, Expect) ->
    Messages = split(Result, [], []),
    io:format("Result: ~p", [Messages]),
    io:format("Expected: ~p", [Expect]),
    match_messages(Messages, Expect).

%% insert What before Item, crash if Item is not found
insert_before(Item, What, [Item|List]) ->
    [What,Item|List];
insert_before(Item, What, [Other|List]) ->
    [Other|insert_before(Item, What, List)].

split([$\n|Rest], Current, Lines) ->
    split(Rest, [], [lists:reverse(Current)|Lines]);
split([$\r|Rest], Current, Lines) ->
    split(Rest, Current, Lines);
split([Char|Rest], Current, Lines) ->
    split(Rest, [Char|Current], Lines);
split([], [], Lines) ->
    lists:reverse(Lines);
split([], Current, Lines) ->
    split([], [], [lists:reverse(Current)|Lines]).

match_messages([Msg|Rest1], [Regexp|Rest2]) ->
    case re:run(Msg, Regexp, [{capture,none}, unicode]) of
        match ->
            ok;
        nomatch ->
            io:format("Not matching: ~s\n", [Msg]),
            io:format("Regexp      : ~s\n", [Regexp]),
            ct:fail(message_mismatch)
    end,
    match_messages(Rest1, Rest2);
match_messages([], [Expect|Rest]) ->
    ct:fail({too_few_messages, [Expect|Rest]});
match_messages([Msg|Rest], []) ->
    ct:fail({too_many_messages, [Msg|Rest]});
match_messages([], []) ->
    ok.

get_cmd(Cfg) ->
    {SrcDir, IncDir, OutDir} = get_dirs(Cfg),
    Cmd = erlc() ++ " -I" ++ IncDir ++ " -o" ++ OutDir ++ " ",
    {SrcDir, OutDir, Cmd}.

get_dirs(Cfg) ->
    DataDir = proplists:get_value(data_dir, Cfg),
    PrivDir = proplists:get_value(priv_dir, Cfg),
    SrcDir = filename:join(DataDir, "src"),
    IncDir = filename:join(DataDir, "include"),
    {SrcDir, IncDir, PrivDir}.

exists(Name) ->
    filelib:is_file(Name).

%% Runs the command using os:cmd/1.
%%
%% Returns the output from the command (as a list of characters with
%% embedded newlines).  The very last line will indicate the
%% exit status of the command, where _OK_ means zero, and _ERROR_
%% a non-zero exit status.

run_command(Config, Cmd) ->
    TmpDir = filename:join(proplists:get_value(priv_dir, Config), "tmp"),
    file:make_dir(TmpDir),
    {RunFile, Run, Script} = run_command(TmpDir, os:type(), Cmd),
    ok = file:write_file(filename:join(TmpDir, RunFile), unicode:characters_to_binary(Script)),
    os:cmd(Run).

run_command(Dir, {win32, _}, Cmd) ->
    BatchFile = filename:join(Dir, "run.bat"),
    Run = re:replace(filename:rootname(BatchFile), "/", "\\",
                     [global,{return,list}]),
    {BatchFile,
     Run,
     ["@echo off\r\n",
      "set ERLC_EMULATOR=", ct:get_progname(), "\r\n",
      Cmd, "\r\n",
      "if errorlevel 1 echo _ERROR_\r\n",
      "if not errorlevel 1 echo _OK_\r\n"]};
run_command(Dir, {unix, _}, Cmd) ->
    Name = filename:join(Dir, "run"),
    {Name,
     "/bin/sh " ++ Name,
     ["#!/bin/sh\n",
      "ERLC_EMULATOR='", ct:get_progname(), "'\n",
      "export ERLC_EMULATOR\n",
      Cmd, "\n",
      "case $? in\n",
      "  0) echo '_OK_';;\n",
      "  *) echo '_ERROR_';;\n",
      "esac\n"]};
run_command(_Dir, Other, _Cmd) ->
    ct:fail("Don't know how to test exit code for ~p", [Other]).
