Enable gigacage on iOS
[WebKit-https.git] / Tools / Scripts / webkitruby / jsc-stress-test-writer-default.rb
1 # Copyright (C) 2013-2016 Apple Inc. All rights reserved.
2 #
3 # Redistribution and use in source and binary forms, with or without
4 # modification, are permitted provided that the following conditions
5 # are met:
6 #
7 # 1.  Redistributions of source code must retain the above copyright
8 #     notice, this list of conditions and the following disclaimer. 
9 # 2.  Redistributions in binary form must reproduce the above copyright
10 #     notice, this list of conditions and the following disclaimer in the
11 #     documentation and/or other materials provided with the distribution. 
12 #
13 # THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
14 # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
15 # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
16 # DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
17 # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
18 # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
19 # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
20 # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
21 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
22 # THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
23
24 def prefixCommand(prefix)
25     "awk " + Shellwords.shellescape("{ printf #{(prefix + ': ').inspect}; print }")
26 end
27
28 def redirectAndPrefixCommand(prefix)
29     prefixCommand(prefix) + " 2>&1"
30 end
31
32 def pipeAndPrefixCommand(outputFilename, prefix)
33     "tee " + Shellwords.shellescape(outputFilename.to_s) + " | " + prefixCommand(prefix)
34 end
35
36 # Output handler for tests that are expected to be silent.
37 def silentOutputHandler
38     Proc.new {
39         | name |
40         " | " + pipeAndPrefixCommand((Pathname("..") + (name + ".out")).to_s, name)
41     }
42 end
43
44 # Output handler for tests that are expected to produce meaningful output.
45 def noisyOutputHandler
46     Proc.new {
47         | name |
48         " | cat > " + Shellwords.shellescape((Pathname("..") + (name + ".out")).to_s)
49     }
50 end
51
52 # Error handler for tests that fail exactly when they return non-zero exit status.
53 # This is useful when a test is expected to fail.
54 def simpleErrorHandler
55     Proc.new {
56         | outp, plan |
57         outp.puts "if test -e #{plan.failFile}"
58         outp.puts "then"
59         outp.puts "    (echo ERROR: Unexpected exit code: `cat #{plan.failFile}`) | " + redirectAndPrefixCommand(plan.name)
60         outp.puts "    " + plan.failCommand
61         outp.puts "else"
62         outp.puts "    " + plan.successCommand
63         outp.puts "fi"
64     }
65 end
66
67 # Error handler for tests that fail exactly when they return zero exit status.
68 def expectedFailErrorHandler
69     Proc.new {
70         | outp, plan |
71         outp.puts "if test -e #{plan.failFile}"
72         outp.puts "then"
73         outp.puts "    " + plan.successCommand
74         outp.puts "else"
75         outp.puts "    (echo ERROR: Unexpected exit code: 0) | " + redirectAndPrefixCommand(plan.name)
76         outp.puts "    " + plan.failCommand
77         outp.puts "fi"
78     }
79 end
80
81 # Error handler for tests that fail exactly when they return non-zero exit status and produce
82 # lots of spew. This will echo that spew when the test fails.
83 def noisyErrorHandler
84     Proc.new {
85         | outp, plan |
86         outputFilename = Shellwords.shellescape((Pathname("..") + (plan.name + ".out")).to_s)
87     
88         outp.puts "if test -e #{plan.failFile}"
89         outp.puts "then"
90         outp.puts "    (cat #{outputFilename} && echo ERROR: Unexpected exit code: `cat #{plan.failFile}`) | " + redirectAndPrefixCommand(plan.name)
91         outp.puts "    " + plan.failCommand
92         outp.puts "else"
93         outp.puts "    " + plan.successCommand
94         outp.puts "fi"
95     }
96 end
97
98 # Error handler for tests that diff their output with some expectation.
99 def diffErrorHandler(expectedFilename)
100     Proc.new {
101         | outp, plan |
102         outputFilename = Shellwords.shellescape((Pathname("..") + (plan.name + ".out")).to_s)
103         diffFilename = Shellwords.shellescape((Pathname("..") + (plan.name + ".diff")).to_s)
104         
105         outp.puts "if test -e #{plan.failFile}"
106         outp.puts "then"
107         outp.puts "    (cat #{outputFilename} && echo ERROR: Unexpected exit code: `cat #{plan.failFile}`) | " + redirectAndPrefixCommand(plan.name)
108         outp.puts "    " + plan.failCommand
109         outp.puts "elif test -e ../#{Shellwords.shellescape(expectedFilename)}"
110         outp.puts "then"
111         outp.puts "    diff --strip-trailing-cr -u ../#{Shellwords.shellescape(expectedFilename)} #{outputFilename} > #{diffFilename}"
112         outp.puts "    if [ $? -eq 0 ]"
113         outp.puts "    then"
114         outp.puts "    " + plan.successCommand
115         outp.puts "    else"
116         outp.puts "        (echo \"DIFF FAILURE!\" && cat #{diffFilename}) | " + redirectAndPrefixCommand(plan.name)
117         outp.puts "        " + plan.failCommand
118         outp.puts "    fi"
119         outp.puts "else"
120         outp.puts "    (echo \"NO EXPECTATION!\" && cat #{outputFilename}) | " + redirectAndPrefixCommand(plan.name)
121         outp.puts "    " + plan.failCommand
122         outp.puts "fi"
123     }
124 end
125
126 # Error handler for tests that report error by saying "failed!". This is used by Mozilla
127 # tests.
128 def mozillaErrorHandler
129     Proc.new {
130         | outp, plan |
131         outputFilename = Shellwords.shellescape((Pathname("..") + (plan.name + ".out")).to_s)
132
133         outp.puts "if test -e #{plan.failFile}"
134         outp.puts "then"
135         outp.puts "    (cat #{outputFilename} && echo ERROR: Unexpected exit code: `cat #{plan.failFile}`) | " + redirectAndPrefixCommand(plan.name)
136         outp.puts "    " + plan.failCommand
137         outp.puts "elif grep -i -q failed! #{outputFilename}"
138         outp.puts "then"
139         outp.puts "    (echo Detected failures: && cat #{outputFilename}) | " + redirectAndPrefixCommand(plan.name)
140         outp.puts "    " + plan.failCommand
141         outp.puts "else"
142         outp.puts "    " + plan.successCommand
143         outp.puts "fi"
144     }
145 end
146
147 # Error handler for tests that report error by saying "failed!", and are expected to
148 # fail. This is used by Mozilla tests.
149 def mozillaFailErrorHandler
150     Proc.new {
151         | outp, plan |
152         outputFilename = Shellwords.shellescape((Pathname("..") + (plan.name + ".out")).to_s)
153
154         outp.puts "if test -e #{plan.failFile}"
155         outp.puts "then"
156         outp.puts "    " + plan.successCommand
157         outp.puts "elif grep -i -q failed! #{outputFilename}"
158         outp.puts "then"
159         outp.puts "    " + plan.successCommand
160         outp.puts "else"
161         outp.puts "    (echo NOTICE: You made this test pass, but it was expected to fail) | " + redirectAndPrefixCommand(plan.name)
162         outp.puts "    " + plan.failCommand
163         outp.puts "fi"
164     }
165 end
166
167 # Error handler for tests that report error by saying "failed!", and are expected to have
168 # an exit code of 3.
169 def mozillaExit3ErrorHandler
170     Proc.new {
171         | outp, plan |
172         outputFilename = Shellwords.shellescape((Pathname("..") + (plan.name + ".out")).to_s)
173
174         outp.puts "if test -e #{plan.failFile}"
175         outp.puts "then"
176         outp.puts "    if [ `cat #{plan.failFile}` -eq 3 ]"
177         outp.puts "    then"
178         outp.puts "        if grep -i -q failed! #{outputFilename}"
179         outp.puts "        then"
180         outp.puts "            (echo Detected failures: && cat #{outputFilename}) | " + redirectAndPrefixCommand(plan.name)
181         outp.puts "            " + plan.failCommand
182         outp.puts "        else"
183         outp.puts "            " + plan.successCommand
184         outp.puts "        fi"
185         outp.puts "    else"
186         outp.puts "        (cat #{outputFilename} && echo ERROR: Unexpected exit code: `cat #{plan.failFile}`) | " + redirectAndPrefixCommand(plan.name)
187         outp.puts "        " + plan.failCommand
188         outp.puts "    fi"
189         outp.puts "else"
190         outp.puts "    (cat #{outputFilename} && echo ERROR: Test expected to fail, but returned successfully) | " + redirectAndPrefixCommand(plan.name)
191         outp.puts "    " + plan.failCommand
192         outp.puts "fi"
193     }
194 end
195
196 # Error handler for tests that report success by saying "Passed" or error by saying "FAILED".
197 # This is used by Chakra tests.
198 def chakraPassFailErrorHandler
199     Proc.new {
200         | outp, plan |
201         outputFilename = Shellwords.shellescape((Pathname("..") + (plan.name + ".out")).to_s)
202
203         outp.puts "if test -e #{plan.failFile}"
204         outp.puts "then"
205         outp.puts "    (cat #{outputFilename} && echo ERROR: Unexpected exit code: `cat #{plan.failFile}`) | " + redirectAndPrefixCommand(plan.name)
206         outp.puts "    " + plan.failCommand
207         outp.puts "elif grep -i -q FAILED #{outputFilename}"
208         outp.puts "then"
209         outp.puts "    (echo Detected failures: && cat #{outputFilename}) | " + redirectAndPrefixCommand(plan.name)
210         outp.puts "    " + plan.failCommand
211         outp.puts "else"
212         outp.puts "    " + plan.successCommand
213         outp.puts "fi"
214     }
215 end
216
217 class Plan
218     attr_reader :directory, :arguments, :family, :name, :outputHandler, :errorHandler, :additionalEnv
219     attr_accessor :index
220     
221     def initialize(directory, arguments, family, name, outputHandler, errorHandler)
222         @directory = directory
223         @arguments = arguments
224         @family = family
225         @name = name
226         @outputHandler = outputHandler
227         @errorHandler = errorHandler
228         @isSlow = !!$runCommandOptions[:isSlow]
229         @additionalEnv = []
230     end
231     
232     def shellCommand
233         # It's important to remember that the test is actually run in a subshell, so if we change directory
234         # in the subshell when we return we will be in our original directory. This is nice because we don't
235         # have to bend over backwards to do things relative to the root.
236         script = "(cd ../#{Shellwords.shellescape(@directory.to_s)} && ("
237         ($envVars + additionalEnv).each { |var| script += "export " << var << "; " }
238         script += "\"$@\" " + escapeAll(@arguments) + "))"
239         return script
240     end
241     
242     def reproScriptCommand
243         # We have to find our way back to the .runner directory since that's where all of the relative
244         # paths assume they start out from.
245         script = "CURRENT_DIR=\"$( cd \"$( dirname \"${BASH_SOURCE[0]}\" )\" && pwd )\"\n"
246         script += "cd $CURRENT_DIR\n"
247         Pathname.new(@name).dirname.each_filename {
248             | pathComponent |
249             script += "cd ..\n"
250         }
251         script += "cd .runner\n"
252
253         script += "export DYLD_FRAMEWORK_PATH=$(cd #{$testingFrameworkPath.dirname}; pwd)\n"
254         script += "export JSCTEST_timeout=#{Shellwords.shellescape(ENV['JSCTEST_timeout'])}\n"
255         $envVars.each { |var| script += "export " << var << "\n" }
256         script += "#{shellCommand} || exit 1"
257         "echo #{Shellwords.shellescape(script)} > #{Shellwords.shellescape((Pathname.new("..") + @name).to_s)}"
258     end
259     
260     def failCommand
261         "echo FAIL: #{Shellwords.shellescape(@name)} ; touch #{failFile} ; " + reproScriptCommand
262     end
263     
264     def successCommand
265         if $progressMeter or $verbosity >= 2
266             "rm -f #{failFile} ; echo PASS: #{Shellwords.shellescape(@name)}"
267         else
268             "rm -f #{failFile}"
269         end
270     end
271     
272     def failFile
273         "test_fail_#{@index}"
274     end
275     
276     def writeRunScript(filename)
277         File.open(filename, "w") {
278             | outp |
279             outp.puts "echo Running #{Shellwords.shellescape(@name)}"
280             cmd  = "(" + shellCommand + " || (echo $? > #{failFile})) 2>&1 "
281             cmd += @outputHandler.call(@name)
282             if $verbosity >= 3
283                 outp.puts "echo #{Shellwords.shellescape(cmd)}"
284             end
285             outp.puts cmd
286             @errorHandler.call(outp, self)
287         }
288     end
289 end
290
291 def prepareShellTestRunner
292     FileUtils.cp SCRIPTS_PATH + "jsc-stress-test-helpers" + "shell-runner.sh", $runnerDir + "runscript"
293 end
294
295 def prepareMakeTestRunner
296     # The goals of our parallel test runner are scalability and simplicity. The
297     # simplicity part is particularly important. We don't want to have to have
298     # a full-time contributor just philosophising about parallel testing.
299     #
300     # As such, we just pass off all of the hard work to 'make'. This creates a
301     # dummy directory ("$outputDir/.runner") in which we create a dummy
302     # Makefile. The Makefile has an 'all' rule that depends on all of the tests.
303     # That is, for each test we know we will run, there is a rule in the
304     # Makefile and 'all' depends on it. Running 'make -j <whatever>' on this
305     # Makefile results in 'make' doing all of the hard work:
306     #
307     # - Load balancing just works. Most systems have a great load balancer in
308     #   'make'. If your system doesn't then just install a real 'make'.
309     #
310     # - Interruptions just work. For example Ctrl-C handling in 'make' is
311     #   exactly right. You don't have to worry about zombie processes.
312     #
313     # We then do some tricks to make failure detection work and to make this
314     # totally sound. If a test fails, we don't want the whole 'make' job to
315     # stop. We also don't have any facility for makefile-escaping of path names.
316     # We do have such a thing for shell-escaping, though. We fix both problems
317     # by having the actual work for each of the test rules be done in a shell
318     # script on the side. There is one such script per test. The script responds
319     # to failure by printing something on the console and then touching a
320     # failure file for that test, but then still returns 0. This makes 'make'
321     # continue past that failure and complete all the tests anyway.
322     #
323     # In the end, this script collects all of the failures by searching for
324     # files in the .runner directory whose name matches /^test_fail_/, where
325     # the thing after the 'fail_' is the test index. Those are the files that
326     # would be created by the test scripts if they detect failure. We're
327     # basically using the filesystem as a concurrent database of test failures.
328     # Even if two tests fail at the same time, since they're touching different
329     # files we won't miss any failures.
330     runIndices = []
331     $runlist.each {
332         | plan |
333         runIndices << plan.index
334     }
335     
336     File.open($runnerDir + "Makefile", "w") {
337         | outp |
338         outp.puts("all: " + runIndices.map{|v| "test_done_#{v}"}.join(' '))
339         runIndices.each {
340             | index |
341             plan = $runlist[index]
342             outp.puts "test_done_#{index}:"
343             outp.puts "\tsh test_script_#{plan.index}"
344         }
345     }
346 end
347
348 def prepareRubyTestRunner
349     File.open($runnerDir + "runscript", "w") {
350         | outp |
351         $runlist.each {
352             | plan |
353             outp.puts "print `sh test_script_#{plan.index} 2>&1`"
354         }
355     }
356 end
357
358 def testRunnerCommand
359     case $testRunnerType
360     when :shell
361         command = "sh runscript"
362     when :make
363         command = "make -j #{$numChildProcesses.to_s} -s -f Makefile"
364     when :ruby
365         command = "ruby runscript"
366     else
367         raise "Unknown test runner type: #{$testRunnerType.to_s}"
368     end
369     return command
370 end