1%%
2%% %CopyrightBegin%
3%%
4%% Copyright Ericsson AB 1997-2020. All Rights Reserved.
5%%
6%% Licensed under the Apache License, Version 2.0 (the "License");
7%% you may not use this file except in compliance with the License.
8%% You may obtain a copy of the License at
9%%
10%%     http://www.apache.org/licenses/LICENSE-2.0
11%%
12%% Unless required by applicable law or agreed to in writing, software
13%% distributed under the License is distributed on an "AS IS" BASIS,
14%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15%% See the License for the specific language governing permissions and
16%% limitations under the License.
17%%
18%% %CopyrightEnd%
19%%
20-module(erlc_SUITE).
21
22%% Tests the erlc command by compiling various types of files.
23
24-export([all/0, suite/0,groups/0,init_per_suite/1, end_per_suite/1,
25         init_per_group/2,end_per_group/2, compile_erl/1,
26         compile_yecc/1, compile_script/1,
27         compile_mib/1, good_citizen/1, deep_cwd/1, arg_overflow/1,
28         make_dep_options/1]).
29
30-include_lib("common_test/include/ct.hrl").
31
32suite() -> [{ct_hooks,[ts_install_cth]}].
33
34all() ->
35    [{group,with_server},{group,without_server}].
36
37groups() ->
38    Tests = tests(),
39    [{with_server,[],Tests},
40     {without_server,[],Tests}].
41
42tests() ->
43    [compile_erl, compile_yecc, compile_script, compile_mib,
44     good_citizen, deep_cwd, arg_overflow, make_dep_options].
45
46init_per_suite(Config) ->
47    Config.
48
49end_per_suite(_Config) ->
50    ok.
51
52init_per_group(with_server, Config) ->
53    os:putenv("ERLC_USE_SERVER", "yes"),
54    Config;
55init_per_group(without_server, Config) ->
56    os:putenv("ERLC_USE_SERVER", "no"),
57    Config;
58init_per_group(_, Config) ->
59    Config.
60
61end_per_group(_GroupName, Config) ->
62    os:unsetenv("ERLC_USE_SERVER"),
63    Config.
64
65%% Copy from erlc_SUITE_data/include/erl_test.hrl.
66
67-record(person, {name, shoe_size}).
68
69%% Tests that compiling Erlang source code works.
70
71compile_erl(Config) when is_list(Config) ->
72    {SrcDir, OutDir, Cmd} = get_cmd(Config),
73    FileName = filename:join(SrcDir, "erl_test_ok.erl"),
74
75    %% By default, warnings are now turned on.
76    run(Config, Cmd, FileName, "",
77        ["Warning: function foo/0 is unused\$", "_OK_"]),
78
79    %% Test that the compiled file is where it should be,
80    %% and that it is runnable.
81
82    {module, erl_test_ok} = code:load_abs(filename:join(OutDir, "erl_test_ok")),
83    42 = erl_test_ok:shoe_size(#person{shoe_size=42}),
84    code:purge(erl_test_ok),
85
86    %% Try disabling warnings.
87
88    run(Config, Cmd, FileName, "-W0", ["_OK_"]),
89
90    %% Try treating warnings as errors.
91
92    run(Config, Cmd, FileName, "-Werror",
93        ["compile: warnings being treated as errors\$",
94         "function foo/0 is unused\$", "_ERROR_"]),
95
96    %% Check a bad file.
97
98    BadFile = filename:join(SrcDir, "erl_test_bad.erl"),
99    run(Config, Cmd, BadFile, "", ["function non_existing/1 undefined\$",
100                                   "_ERROR_"]),
101
102    ok.
103
104%% Test that compiling yecc source code works.
105
106compile_yecc(Config) when is_list(Config) ->
107    {SrcDir, _, OutDir} = get_dirs(Config),
108    Cmd = erlc() ++ " -o" ++ OutDir ++ " ",
109    FileName = filename:join(SrcDir, "yecc_test_ok.yrl"),
110    run(Config, Cmd, FileName, "-W0", ["_OK_"]),
111    true = exists(filename:join(OutDir, "yecc_test_ok.erl")),
112
113    BadFile = filename:join(SrcDir, "yecc_test_bad.yrl"),
114    run(Config, Cmd, BadFile, "-W0",
115        ["rootsymbol form is not a nonterminal\$",
116         "undefined nonterminal: form\$",
117         "Nonterminals is missing\$",
118         "_ERROR_"]),
119    exists(filename:join(OutDir, "yecc_test_ok.erl")),
120    ok.
121
122%% Test that compiling start scripts works.
123
124compile_script(Config) when is_list(Config) ->
125    {SrcDir, OutDir, Cmd} = get_cmd(Config),
126    FileName = filename:join(SrcDir, "start_ok.script"),
127    run(Config, Cmd, FileName, "", ["_OK_"]),
128    true = exists(filename:join(OutDir, "start_ok.boot")),
129
130    BadFile = filename:join(SrcDir, "start_bad.script"),
131    run(Config, Cmd, BadFile, "", ["syntax error before:", "_ERROR_"]),
132    ok.
133
134%% Test that compiling SNMP mibs works.
135
136compile_mib(Config) when is_list(Config) ->
137    {SrcDir, OutDir, Cmd} = get_cmd(Config),
138    FileName = filename:join(SrcDir, "GOOD-MIB.mib"),
139    run(Config, Cmd, FileName, "", ["_OK_"]),
140    Output = filename:join(OutDir, "GOOD-MIB.bin"),
141    true = exists(Output),
142
143    %% Try -W option.
144
145    ok = file:delete(Output),
146    run(Config, Cmd, FileName, "-W",
147        ["_OK_"]),
148    true = exists(Output),
149
150    %% Try -W option and more verbose.
151
152    ok = file:delete(Output),
153    case test_server:os_type() of
154        {unix,_} ->
155            run(Config, Cmd, FileName, "-W +'{verbosity,info}'",
156                ["\\[GOOD-MIB[.]mib\\]\\[INF\\]: No accessfunction for 'sysDescr' => using default",
157                 "_OK_"]),
158            true = exists(Output),
159            ok = file:delete(Output);
160        _ -> ok				%Don't bother -- too much work.
161    end,
162
163    %% Try a bad file.
164
165    BadFile = filename:join(SrcDir, "BAD-MIB.mib"),
166    run(Config, Cmd, BadFile, "",
167        ["BAD-MIB.mib: 1: syntax error before: mibs\$",
168         "compilation_failed_ERROR_"]),
169
170    %% Make sure that no -I option works.
171
172    NewCmd = erlc() ++ " -o" ++ OutDir ++ " ",
173    run(Config, NewCmd, FileName, "", ["_OK_"]),
174    true = exists(Output),
175
176    ok.
177
178%% Checks that 'erlc' doesn't eat any input (important when called from a
179%% shell script with redirected input).
180good_citizen(Config) when is_list(Config) ->
181    case os:type() of
182        {unix, _} ->
183            PrivDir = proplists:get_value(priv_dir, Config),
184            Answer = filename:join(PrivDir, "answer"),
185            Script = filename:join(PrivDir, "test_script"),
186            Test = filename:join(PrivDir, "test.erl"),
187            S = ["#! /bin/sh\n", "erlc ", Test, "\n",
188                 "read reply\n", "echo $reply\n"],
189            ok = file:write_file(Script, S),
190            ok = file:write_file(Test, "-module(test).\n"),
191            Cmd = "echo y | sh " ++ Script ++ " > " ++ Answer,
192            os:cmd(Cmd),
193            {ok, Answer0} = file:read_file(Answer),
194            [$y|_] = binary_to_list(Answer0),
195            ok;
196        _ ->
197            {skip, "Unix specific"}
198    end.
199
200%% Make sure that compiling an Erlang module deep down in
201%% in a directory with more than 255 characters works.
202deep_cwd(Config) when is_list(Config) ->
203    case os:type() of
204        {unix, _} ->
205            PrivDir = proplists:get_value(priv_dir, Config),
206            deep_cwd_1(PrivDir);
207        _ ->
208            {skip, "Only a problem on Unix"}
209    end.
210
211deep_cwd_1(PrivDir) ->
212    DeepDir0 = filename:join(PrivDir, lists:duplicate(128, $a)),
213    DeepDir = filename:join(DeepDir0, lists:duplicate(128, $b)),
214    ok = filelib:ensure_dir(filename:join(DeepDir,"any_file")),
215    ok = file:set_cwd(DeepDir),
216    ok = file:write_file("test.erl", "-module(test).\n\n"),
217    io:format("~s\n", [os:cmd("erlc test.erl")]),
218    true = filelib:is_file("test.beam"),
219    ok.
220
221%% Test that a large number of command line switches does not
222%% overflow the argument buffer
223arg_overflow(Config) when is_list(Config) ->
224    {SrcDir, _OutDir, Cmd} = get_cmd(Config),
225    FileName = filename:join(SrcDir, "erl_test_ok.erl"),
226    %% Each -D option will be expanded to three arguments when
227    %% invoking 'erl'.
228    NumDOptions = num_d_options(),
229    Args = lists:flatten([ ["-D", integer_to_list(N, 36), "=1 "] ||
230                           N <- lists:seq(1, NumDOptions) ]),
231    run(Config, Cmd, FileName, Args,
232        ["Warning: function foo/0 is unused\$",
233         "_OK_"]),
234    ok.
235
236num_d_options() ->
237    case {os:type(),os:version()} of
238        {{win32,_},_} ->
239            %% The maximum size of a command line in the command
240            %% shell on Windows is 8191 characters.
241            %% Each -D option is expanded to "@dv NN 1", i.e.
242            %% 8 characters. (Numbers up to 1295 can be expressed
243            %% as two 36-base digits.)
244            1000;
245        {{unix,linux},Version} when Version < {2,6,23} ->
246            %% On some older 64-bit versions of Linux, the maximum number
247            %% of arguments is 16383.
248            %% See: http://www.in-ulm.de/~mascheck/various/argmax/
249            5440;
250        {{unix,darwin},{Major,_,_}} when Major >= 11 ->
251            %% "getconf ARG_MAX" still reports 262144 (as in previous
252            %% version of MacOS X), but the useful space seem to have
253            %% shrunk significantly (or possibly the number of arguments).
254            %% 7673
255            7500;
256        {_,_} ->
257            12000
258    end.
259
260erlc() ->
261    case os:find_executable("erlc") of
262        false ->
263            ct:fail("Can't find erlc");
264        Erlc ->
265            "\"" ++ Erlc ++ "\""
266    end.
267
268make_dep_options(Config) ->
269    {SrcDir,OutDir,Cmd} = get_cmd(Config),
270    FileName = filename:join(SrcDir, "erl_test_ok.erl"),
271    BeamFileName = filename:join(OutDir, "erl_test_ok.beam"),
272
273    DepRE = ["/erl_test_ok[.]beam: \\\\$",
274             "/system_test/erlc_SUITE_data/src/erl_test_ok[.]erl \\\\$",
275             "/system_test/erlc_SUITE_data/include/erl_test[.]hrl$",
276             "_OK_"],
277
278    DepRETarget =
279    ["^target: \\\\$",
280     "/system_test/erlc_SUITE_data/src/erl_test_ok[.]erl \\\\$",
281     "/system_test/erlc_SUITE_data/include/erl_test[.]hrl$",
282     "_OK_"],
283
284    DepREMP =
285    ["/erl_test_ok[.]beam: \\\\$",
286     "/system_test/erlc_SUITE_data/src/erl_test_ok[.]erl \\\\$",
287     "/system_test/erlc_SUITE_data/include/erl_test[.]hrl$",
288     [],
289     "/system_test/erlc_SUITE_data/include/erl_test.hrl:$",
290     "_OK_"],
291
292    DepREMissing =
293    ["/erl_test_missing_header[.]beam: \\\\$",
294     "/system_test/erlc_SUITE_data/src/erl_test_missing_header[.]erl \\\\$",
295     "/system_test/erlc_SUITE_data/include/erl_test[.]hrl \\\\$",
296     "missing.hrl$",
297     "_OK_"],
298
299    file:delete(BeamFileName),
300
301    %% Test plain -M
302    run(Config, Cmd, FileName, "-M", DepRE),
303    false = exists(BeamFileName),
304
305    %% Test -MF File
306    DepFile = filename:join(OutDir, "my.deps"),
307    run(Config, Cmd, FileName, "-MF "++DepFile, ["_OK_"]),
308    {ok,MFBin} = file:read_file(DepFile),
309    verify_result(binary_to_list(MFBin)++["_OK_"], DepRE),
310    false = exists(BeamFileName),
311
312    %% Test -MD
313    run(Config, Cmd, FileName, "-MD", ["_OK_"]),
314    MDFile = filename:join(OutDir, "erl_test_ok.Pbeam"),
315    {ok,MFBin} = file:read_file(MDFile),
316    file:delete(MDFile), %% used further down!
317    false = exists(BeamFileName),
318
319    %% Test -M -MT Target
320    run(Config, Cmd, FileName, "-M -MT target", DepRETarget),
321    false = exists(BeamFileName),
322
323    %% Test -MF File -MT Target
324    TargetDepFile = filename:join(OutDir, "target.deps"),
325    run(Config, Cmd, FileName, "-MF "++TargetDepFile++" -MT target",
326        ["_OK_"]),
327    {ok,TargetBin} = file:read_file(TargetDepFile),
328    verify_result(binary_to_list(TargetBin)++["_OK_"], DepRETarget),
329    file:delete(TargetDepFile),
330    false = exists(BeamFileName),
331
332    %% Test -MD -MT Target
333    run(Config, Cmd, FileName, "-MD -MT target", ["_OK_"]),
334    TargetMDFile = filename:join(OutDir, "erl_test_ok.Pbeam"),
335    {ok,TargetBin} = file:read_file(TargetMDFile),
336    file:delete(TargetDepFile),
337    false = exists(BeamFileName),
338
339    %% Test -M -MQ Target. (Note: Passing a $ on the command line
340    %% portably for Unix and Windows is tricky, so we will just test
341    %% that MQ works at all.)
342    run(Config, Cmd, FileName, "-M -MQ target", DepRETarget),
343    false = exists(BeamFileName),
344
345    %% Test -M -MP
346    run(Config, Cmd, FileName, "-M -MP", DepREMP),
347    false = exists(BeamFileName),
348
349    %% Test -M -MG
350    MissingHeader = filename:join(SrcDir, "erl_test_missing_header.erl"),
351    run(Config, Cmd, MissingHeader, "-M -MG", DepREMissing),
352    false = exists(BeamFileName),
353
354    %%
355    %% check the above variants with side-effect -MMD
356    %%
357
358    %% since compiler is run on the erlang code a warning will be
359    %% issued by the compiler, match that.
360    WarningRE = "/system_test/erlc_SUITE_data/src/erl_test_ok.erl:[0-9]+: "
361        "Warning: function foo/0 is unused$",
362    ErrorRE = "/system_test/erlc_SUITE_data/src/erl_test_missing_header.erl:"
363        "[0-9]+: can't find include file \"missing.hrl\"$",
364
365    DepRE_MMD = insert_before("_OK_", WarningRE, DepRE),
366    DepRETarget_MMD = insert_before("_OK_", WarningRE, DepRETarget),
367    DepREMP_MMD = insert_before("_OK_",WarningRE,DepREMP),
368    DepREMissing_MMD = (insert_before("_OK_",ErrorRE,DepREMissing)--
369                            ["_OK_"]) ++ ["_ERROR_"],
370    CompRE = [WarningRE,"_OK_"],
371
372
373    %% Test plain -MMD -M
374    run(Config, Cmd, FileName, "-MMD -M", DepRE_MMD),
375    true = exists(BeamFileName),
376    file:delete(BeamFileName),
377
378    %% Test -MMD -MF File
379    DepFile = filename:join(OutDir, "my.deps"),
380    run(Config, Cmd, FileName, "-MMD -MF "++DepFile, CompRE),
381    {ok,MFBin} = file:read_file(DepFile),
382    verify_result(binary_to_list(MFBin)++["_OK_"], DepRE),
383    true = exists(BeamFileName),
384    file:delete(BeamFileName),
385
386    %% Test -MMD -MD
387    run(Config, Cmd, FileName, "-MMD -MD", CompRE),
388    MDFile = filename:join(OutDir, "erl_test_ok.Pbeam"),
389    {ok,MFBin} = file:read_file(MDFile),
390    file:delete(MDFile), %% used further down!
391    true = exists(BeamFileName),
392    file:delete(BeamFileName),
393
394    %% Test -MMD -M -MT Target
395    run(Config, Cmd, FileName, "-MMD -M -MT target", DepRETarget_MMD),
396    true = exists(BeamFileName),
397    file:delete(BeamFileName),
398
399    %% Test -MMD -MF File -MT Target
400    TargetDepFile = filename:join(OutDir, "target.deps"),
401    run(Config, Cmd, FileName, "-MMD -MF "++TargetDepFile++" -MT target",
402        CompRE),
403    {ok,TargetBin} = file:read_file(TargetDepFile),
404    verify_result(binary_to_list(TargetBin)++["_OK_"], DepRETarget),
405    file:delete(TargetDepFile),
406    true = exists(BeamFileName),
407    file:delete(BeamFileName),
408
409    %% Test -MMD -MD -MT Target
410    run(Config, Cmd, FileName, "-MMD -MD -MT target", CompRE),
411    TargetMDFile = filename:join(OutDir, "erl_test_ok.Pbeam"),
412    {ok,TargetBin} = file:read_file(TargetMDFile),
413    file:delete(TargetDepFile),
414    true = exists(BeamFileName),
415    file:delete(BeamFileName),
416
417    %% Test -MMD -M -MQ Target. (Note: Passing a $ on the command line
418    %% portably for Unix and Windows is tricky, so we will just test
419    %% that MQ works at all.)
420    run(Config, Cmd, FileName, "-MMD -M -MQ target", DepRETarget_MMD),
421    true = exists(BeamFileName),
422    file:delete(BeamFileName),
423
424    %% Test -MMD -M -MP
425    run(Config, Cmd, FileName, "-MMD -M -MP", DepREMP_MMD),
426    true = exists(BeamFileName),
427    file:delete(BeamFileName),
428
429    %% Test -MMD -M -MG
430    MissingHeader = filename:join(SrcDir, "erl_test_missing_header.erl"),
431    run(Config, Cmd, MissingHeader, "-MMD -M -MG", DepREMissing_MMD),
432    false = exists(BeamFileName),
433    ok.
434
435%% Runs a command.
436
437run(Config, Cmd0, Name, Options, Expect) ->
438    Cmd = Cmd0 ++ " " ++ Options ++ " " ++ Name,
439    io:format("~ts", [Cmd]),
440    Result = run_command(Config, Cmd),
441    verify_result(Result, Expect).
442
443verify_result(Result, Expect) ->
444    Messages = split(Result, [], []),
445    io:format("Result: ~p", [Messages]),
446    io:format("Expected: ~p", [Expect]),
447    match_messages(Messages, Expect).
448
449%% insert What before Item, crash if Item is not found
450insert_before(Item, What, [Item|List]) ->
451    [What,Item|List];
452insert_before(Item, What, [Other|List]) ->
453    [Other|insert_before(Item, What, List)].
454
455split([$\n|Rest], Current, Lines) ->
456    split(Rest, [], [lists:reverse(Current)|Lines]);
457split([$\r|Rest], Current, Lines) ->
458    split(Rest, Current, Lines);
459split([Char|Rest], Current, Lines) ->
460    split(Rest, [Char|Current], Lines);
461split([], [], Lines) ->
462    lists:reverse(Lines);
463split([], Current, Lines) ->
464    split([], [], [lists:reverse(Current)|Lines]).
465
466match_messages([Msg|Rest1], [Regexp|Rest2]) ->
467    case re:run(Msg, Regexp, [{capture,none}, unicode]) of
468        match ->
469            ok;
470        nomatch ->
471            io:format("Not matching: ~s\n", [Msg]),
472            io:format("Regexp      : ~s\n", [Regexp]),
473            ct:fail(message_mismatch)
474    end,
475    match_messages(Rest1, Rest2);
476match_messages([], [Expect|Rest]) ->
477    ct:fail({too_few_messages, [Expect|Rest]});
478match_messages([Msg|Rest], []) ->
479    ct:fail({too_many_messages, [Msg|Rest]});
480match_messages([], []) ->
481    ok.
482
483get_cmd(Cfg) ->
484    {SrcDir, IncDir, OutDir} = get_dirs(Cfg),
485    Cmd = erlc() ++ " -I" ++ IncDir ++ " -o" ++ OutDir ++ " ",
486    {SrcDir, OutDir, Cmd}.
487
488get_dirs(Cfg) ->
489    DataDir = proplists:get_value(data_dir, Cfg),
490    PrivDir = proplists:get_value(priv_dir, Cfg),
491    SrcDir = filename:join(DataDir, "src"),
492    IncDir = filename:join(DataDir, "include"),
493    {SrcDir, IncDir, PrivDir}.
494
495exists(Name) ->
496    filelib:is_file(Name).
497
498%% Runs the command using os:cmd/1.
499%%
500%% Returns the output from the command (as a list of characters with
501%% embedded newlines).  The very last line will indicate the
502%% exit status of the command, where _OK_ means zero, and _ERROR_
503%% a non-zero exit status.
504
505run_command(Config, Cmd) ->
506    TmpDir = filename:join(proplists:get_value(priv_dir, Config), "tmp"),
507    file:make_dir(TmpDir),
508    {RunFile, Run, Script} = run_command(TmpDir, os:type(), Cmd),
509    ok = file:write_file(filename:join(TmpDir, RunFile), unicode:characters_to_binary(Script)),
510    os:cmd(Run).
511
512run_command(Dir, {win32, _}, Cmd) ->
513    BatchFile = filename:join(Dir, "run.bat"),
514    Run = re:replace(filename:rootname(BatchFile), "/", "\\",
515                     [global,{return,list}]),
516    {BatchFile,
517     Run,
518     ["@echo off\r\n",
519      "set ERLC_EMULATOR=", ct:get_progname(), "\r\n",
520      Cmd, "\r\n",
521      "if errorlevel 1 echo _ERROR_\r\n",
522      "if not errorlevel 1 echo _OK_\r\n"]};
523run_command(Dir, {unix, _}, Cmd) ->
524    Name = filename:join(Dir, "run"),
525    {Name,
526     "/bin/sh " ++ Name,
527     ["#!/bin/sh\n",
528      "ERLC_EMULATOR='", ct:get_progname(), "'\n",
529      "export ERLC_EMULATOR\n",
530      Cmd, "\n",
531      "case $? in\n",
532      "  0) echo '_OK_';;\n",
533      "  *) echo '_ERROR_';;\n",
534      "esac\n"]};
535run_command(_Dir, Other, _Cmd) ->
536    ct:fail("Don't know how to test exit code for ~p", [Other]).
537