build_randomization.rb 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481
  1. #!/usr/bin/env ruby
  2. # Licensed to Elasticsearch under one or more contributor
  3. # license agreements. See the NOTICE file distributed with
  4. # this work for additional information regarding copyright
  5. # ownership. Elasticsearch licenses this file to you under
  6. # the Apache License, Version 2.0 (the "License"); you may
  7. # 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,
  13. # software distributed under the License is distributed on
  14. # an 'AS IS' BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
  15. # either express or implied. See the License for the specific
  16. # language governing permissions and limitations under the License
  17. #
  18. # NAME
  19. # build_randomization.rb -- Generate property file for the JDK randomization test
  20. #
  21. # SYNOPSIS
  22. # build_randomization.rb [-d] [-l|t]
  23. #
  24. # DESCRIPTION
  25. # This script takes the randomization choices described in RANDOM_CHOICE and generates apporpriate JAVA property file 'prop.txt'
  26. # This property file also contain the appropriate JDK selection, randomized. JDK randomization is based on what is available on the Jenkins tools
  27. # directory. This script is used by Jenkins test system to conduct Elasticsearch server randomization testing.
  28. #
  29. # In hash RANDOM_CHOISES, the key of randomization hash maps to key of java property. The value of the hash describes the possible value of the randomization
  30. #
  31. # For example RANDOM_CHOICES = { 'es.node.mode' => {:choices => ['local', 'network'], :method => :get_random_one} } means
  32. # es.node.mode will be set to either 'local' or 'network', each with 50% of probability
  33. #
  34. # OPTIONS SUMMARY
  35. # The options are as follows:
  36. #
  37. # -d, --debug Increase logging verbosity for debugging purpose
  38. # -t, --test Run in test mode. The script will execute unit tests.
  39. # -l, --local Run in local mode. In this mode, directory structure will be created under current directory to mimick
  40. # Jenkins' server directory layout. This mode is mainly used for development.
  41. require 'enumerator'
  42. require 'getoptlong'
  43. require 'log4r'
  44. require 'optparse'
  45. require 'rubygems'
  46. require 'yaml'
  47. include Log4r
  48. RANDOM_CHOICES = {
  49. 'tests.jvm.argline' => [
  50. {:choices => ['-server'], :method => 'get_random_one'},
  51. {:choices => ['-XX:+UseConcMarkSweepGC', '-XX:+UseParallelGC', '-XX:+UseSerialGC', '-XX:+UseG1GC'], :method => 'get_random_one'},
  52. {:choices => ['-XX:+UseCompressedOops', '-XX:-UseCompressedOops'], :method => 'get_random_one'},
  53. {:choices => ['-XX:+AggressiveOpts'], :method => 'get_50_percent'}
  54. ],
  55. 'es.node.mode' => {:choices => ['local', 'network'], :method => 'get_random_one'},
  56. # bug forced to be false for now :test_nightly => { :method => :true_or_false},
  57. 'tests.nightly' => {:selections => false},
  58. 'tests.heap.size' => {:choices => [512, 1024], :method => :random_heap},
  59. 'tests.assertion.disabled'=> {:choices => 'org.elasticsearch', :method => 'get_10_percent'},
  60. 'tests.security.manager' => {:choices => [true, false], :method => 'get_90_percent'},
  61. }
  62. L = Logger.new 'test_randomizer'
  63. L.outputters = Outputter.stdout
  64. L.level = INFO
  65. C = {:local => false, :test => false}
  66. OptionParser.new do |opts|
  67. opts.banner = "Usage: build_ranodimzatin.rb [options]"
  68. opts.on("-d", "--debug", "Debug mode") do |d|
  69. L.level = DEBUG
  70. end
  71. opts.on("-l", "--local", "Run in local mode") do |l|
  72. C[:local] = true
  73. end
  74. opts.on("-t", "--test", "Run unit tests") do |t|
  75. C[:test] = true
  76. end
  77. end.parse!
  78. class Randomizer
  79. attr_accessor :data_array
  80. def initialize(data_array)
  81. @data_array = data_array
  82. end
  83. def true_or_false
  84. [true, false][rand(2)]
  85. end
  86. def random_heap
  87. inner_data_array = [data_array[0], data_array[1], data_array[0] + rand(data_array[1] - data_array[0])]
  88. "%sm" % inner_data_array[rand(inner_data_array.size)]
  89. end
  90. def get_random_with_distribution(mdata_array, distribution)
  91. L.debug "randomized distribution data %s" % YAML.dump(mdata_array)
  92. L.debug "randomized distribution distribution %s" % YAML.dump(distribution)
  93. carry = 0
  94. distribution_map = distribution.enum_for(:each_with_index).map { |x,i| pre_carry = carry ; carry += x; {i => x + pre_carry} }
  95. random_size = distribution_map.last.values.first
  96. selection = rand(random_size)
  97. #get the index that randomize choice mapped to
  98. choice = distribution_map.select do |x|
  99. x.values.first > selection #only keep the index with distribution value that is higher than the random generated number
  100. end.first.keys.first #first hash's first key is the index we want
  101. L.debug("randomized distribution choice %s" % mdata_array[choice])
  102. mdata_array[choice]
  103. end
  104. def get_random_one
  105. data_array[rand(data_array.size)]
  106. end
  107. def method_missing(meth, *args, &block)
  108. # trap randomization based on percentage
  109. if meth.to_s =~ /^get_(\d+)_percent/
  110. percentage = $1.to_i
  111. remain = 100 - percentage
  112. #data = args.first
  113. normalized_data = if(!data_array.kind_of?(Array))
  114. [data_array, nil]
  115. else
  116. data_array
  117. end
  118. get_random_with_distribution(normalized_data, [percentage, remain])
  119. else
  120. super
  121. end
  122. end
  123. end
  124. class JDKSelector
  125. attr_reader :directory, :jdk_list
  126. def initialize(directory)
  127. @directory = directory
  128. end
  129. # get selection of available JDKs from Jenkins automatic install directory
  130. def get_jdk
  131. @jdk_list = Dir.entries(directory).select do |x|
  132. x.chars.first == 'J'
  133. end.map do |y|
  134. File.join(directory, y)
  135. end
  136. self
  137. end
  138. def filter_java_6(files)
  139. files.select{ |i| File.basename(i).split(/[^0-9]/)[-1].to_i > 6 }
  140. end
  141. # do randomized selection from a given array
  142. def select_one(selection_array = nil)
  143. selection_array = filter_java_6(selection_array || @jdk_list)
  144. Randomizer.new(selection_array).get_random_one
  145. end
  146. def JDKSelector.generate_jdk_hash(jdk_choice)
  147. file_separator = if Gem.win_platform?
  148. File::ALT_SEPARATOR
  149. else
  150. File::SEPARATOR
  151. end
  152. {
  153. :PATH => [jdk_choice, 'bin'].join(file_separator) + File::PATH_SEPARATOR + ENV['PATH'],
  154. :JAVA_HOME => jdk_choice
  155. }
  156. end
  157. end
  158. #
  159. # Fix argument JDK selector
  160. #
  161. class FixedJDKSelector < JDKSelector
  162. def initialize(directory)
  163. @directory = [*directory] #selection of directories to pick from
  164. end
  165. def get_jdk
  166. #since JDK selection is already specified..jdk list is the @directory
  167. @jdk_list = @directory
  168. self
  169. end
  170. def select_one(selection_array = nil)
  171. #bypass filtering since this is not automatic
  172. selection_array ||= @jdk_list
  173. Randomizer.new(selection_array).get_random_one
  174. end
  175. end
  176. #
  177. # Property file writer
  178. #
  179. class PropertyWriter
  180. attr_reader :working_directory
  181. def initialize(mworking_directory)
  182. @working_directory = mworking_directory
  183. end
  184. # # pick first element out of array of hashes, generate write java property file
  185. def generate_property_file(data)
  186. directory = working_directory
  187. #array transformation
  188. content = data.to_a.map do |x|
  189. x.join('=')
  190. end.sort
  191. file_name = (ENV['BUILD_ID'] + ENV['BUILD_NUMBER']) || 'prop' rescue 'prop'
  192. file_name = file_name.split(File::SEPARATOR).first + '.txt'
  193. L.debug "Property file name is %s" % file_name
  194. File.open(File.join(directory, file_name), 'w') do |file|
  195. file.write(content.join("\n"))
  196. end
  197. end
  198. end
  199. #
  200. # Execute randomization logics
  201. #
  202. class RandomizedRunner
  203. attr_reader :random_choices, :jdk, :p_writer
  204. def initialize(mrandom_choices, mjdk, mwriter)
  205. @random_choices = mrandom_choices
  206. @jdk = mjdk
  207. @p_writer = mwriter
  208. end
  209. def generate_selections
  210. configuration = random_choices
  211. L.debug "Enter %s" % __method__
  212. L.debug "Configuration %s" % YAML.dump(configuration)
  213. generated = {}
  214. configuration.each do |k, v|
  215. if(v.kind_of?(Hash))
  216. if(v.has_key?(:method))
  217. randomizer = Randomizer.new(v[:choices])
  218. v[:selections] = randomizer.__send__(v[:method])
  219. end
  220. else
  221. v.each do |x|
  222. if(x.has_key?(:method))
  223. randomizer = Randomizer.new(x[:choices])
  224. x[:selections] = randomizer.__send__(x[:method])
  225. end
  226. end
  227. end
  228. end.each do |k, v|
  229. if(v.kind_of?(Array))
  230. selections = v.inject([]) do |sum, current_hash|
  231. sum.push(current_hash[:selections])
  232. end
  233. else
  234. selections = [v[:selections]] unless v[:selections].nil?
  235. end
  236. generated[k] = selections unless (selections.nil? || selections.size == 0)
  237. end
  238. L.debug "Generated selections %s" % YAML.dump(generated)
  239. generated
  240. end
  241. def get_env_matrix(jdk_selection, selections)
  242. L.debug "Enter %s" % __method__
  243. #normalization
  244. s = {}
  245. selections.each do |k, v|
  246. if(v.size > 1)
  247. s[k] = v.compact.join(' ') #this should be dependent on class of v[0] and perform reduce operation instead... good enough for now
  248. else
  249. s[k] = v.first
  250. end
  251. end
  252. j = JDKSelector.generate_jdk_hash(jdk_selection)
  253. # create build description line
  254. desc = {}
  255. # TODO: better error handling
  256. desc[:BUILD_DESC] = "%s,%s,heap[%s],%s%s%s%s" % [
  257. File.basename(j[:JAVA_HOME]),
  258. s['es.node.mode'],
  259. s['tests.heap.size'],
  260. s['tests.nightly'] ? 'nightly,':'',
  261. s['tests.jvm.argline'].gsub(/-XX:/,''),
  262. s.has_key?('tests.assertion.disabled')? ',assert off' : '',
  263. s['tests.security.manager'] ? ',sec manager on' : ''
  264. ]
  265. result = j.merge(s).merge(desc)
  266. L.debug(YAML.dump(result))
  267. result
  268. end
  269. def run!
  270. p_writer.generate_property_file(get_env_matrix(jdk, generate_selections))
  271. end
  272. end
  273. #
  274. # Main
  275. #
  276. unless(C[:test])
  277. # Check to see if this is running locally
  278. unless(C[:local])
  279. L.debug("Normal Mode")
  280. working_directory = ENV.fetch('WORKSPACE', (Gem.win_platform? ? Dir.pwd : '/var/tmp'))
  281. else
  282. L.debug("Local Mode")
  283. test_directory = 'tools/hudson.model.JDK/'
  284. unless(File.exist?(test_directory))
  285. L.info "running local mode, setting up running environment"
  286. L.info "properties are written to file prop.txt"
  287. FileUtils.mkpath "%sJDK6" % test_directory
  288. FileUtils.mkpath "%sJDK7" % test_directory
  289. end
  290. working_directory = Dir.pwd
  291. end
  292. # script support both window and linux
  293. # TODO: refactor into platform/machine dependent class structure
  294. jdk = if(Gem.win_platform?)
  295. #window mode jdk directories are fixed
  296. #TODO: better logic
  297. L.debug("Window Mode")
  298. if(File.directory?('y:\jdk7\7u55')) #old window system under ec2
  299. FixedJDKSelector.new('y:\jdk7\7u55')
  300. else #new metal window system
  301. FixedJDKSelector.new(['c:\PROGRA~1\JAVA\jdk1.8.0_05', 'c:\PROGRA~1\JAVA\jdk1.7.0_55'])
  302. end
  303. else
  304. #Jenkins sets pwd prior to execution
  305. L.debug("Linux Mode")
  306. JDKSelector.new(File.join(ENV['PWD'],'tools','hudson.model.JDK'))
  307. end
  308. runner = RandomizedRunner.new(RANDOM_CHOICES,
  309. jdk.get_jdk.select_one,
  310. PropertyWriter.new(working_directory))
  311. environment_matrix = runner.run!
  312. exit 0
  313. else
  314. require "test/unit"
  315. end
  316. #
  317. # Test
  318. #
  319. class TestJDKSelector < Test::Unit::TestCase
  320. L = Logger.new 'test'
  321. L.outputters = Outputter.stdout
  322. L.level = DEBUG
  323. def test_hash_generator
  324. jdk_choice = '/dummy/jdk7'
  325. generated = JDKSelector.generate_jdk_hash(jdk_choice)
  326. L.debug "Generated %s" % generated
  327. assert generated[:PATH].include?(jdk_choice), "PATH doesn't included choice"
  328. assert generated[:JAVA_HOME].include?(jdk_choice), "JAVA home doesn't include choice"
  329. end
  330. end
  331. class TestFixJDKSelector < Test::Unit::TestCase
  332. L = Logger.new 'test'
  333. L.outputters = Outputter.stdout
  334. L.level = DEBUG
  335. def test_initialize
  336. ['/home/dummy', ['/JDK7', '/home2'], ['home/dummy']].each do |x|
  337. test_object = FixedJDKSelector.new(x)
  338. assert_kind_of Array, test_object.directory
  339. assert_equal [*x], test_object.directory
  340. end
  341. end
  342. def test_select_one
  343. test_array = %w(one two three)
  344. test_object = FixedJDKSelector.new(test_array)
  345. assert test_array.include?(test_object.get_jdk.select_one)
  346. end
  347. def test_hash_generator
  348. jdk_choice = '/dummy/jdk7'
  349. generated = FixedJDKSelector.generate_jdk_hash(jdk_choice)
  350. L.debug "Generated %s" % generated
  351. assert generated[:PATH].include?(jdk_choice), "PATH doesn't included choice"
  352. assert generated[:JAVA_HOME].include?(jdk_choice), "JAVA home doesn't include choice"
  353. end
  354. end
  355. class TestPropertyWriter < Test::Unit::TestCase
  356. L = Logger.new 'test'
  357. L.outputters = Outputter.stdout
  358. L.level = DEBUG
  359. def test_initialize
  360. ['/home/dummy','/tmp'].each do |x|
  361. test_object = PropertyWriter.new(x)
  362. assert_kind_of String, test_object.working_directory
  363. assert_equal x, test_object.working_directory
  364. end
  365. end
  366. def test_generate_property
  367. test_file = '/tmp/prop.txt'
  368. File.delete(test_file) if File.exist?(test_file)
  369. test_object = PropertyWriter.new(File.dirname(test_file))
  370. # default prop.txt
  371. test_object.generate_property_file({:hi => 'there'})
  372. assert(File.exist?(test_file))
  373. File.open(test_file, 'r') do |properties_file|
  374. properties_file.read.each_line do |line|
  375. line.strip!
  376. assert_equal 'hi=there', line, "content %s is not hi=there" % line
  377. end
  378. end
  379. File.delete(test_file) if File.exist?(test_file)
  380. end
  381. end
  382. class DummyPropertyWriter < PropertyWriter
  383. def generate_property_file(data)
  384. L.debug "generating property file for %s" % YAML.dump(data)
  385. L.debug "on directory %s" % working_directory
  386. end
  387. end
  388. class TestRandomizedRunner < Test::Unit::TestCase
  389. def test_initialize
  390. test_object = RandomizedRunner.new(RANDOM_CHOICES, '/tmp/dummy/jdk', po = PropertyWriter.new('/tmp'))
  391. assert_equal RANDOM_CHOICES, test_object.random_choices
  392. assert_equal '/tmp/dummy/jdk', test_object.jdk
  393. assert_equal po, test_object.p_writer
  394. end
  395. def test_generate_selection_no_method
  396. test_object = RandomizedRunner.new({'tests.one' => {:selections => false }}, '/tmp/dummy/jdk', po = DummyPropertyWriter.new('/tmp'))
  397. selection = test_object.generate_selections
  398. assert_equal false, selection['tests.one'].first, 'randomization without selection method fails'
  399. end
  400. def test_generate_with_method
  401. test_object = RandomizedRunner.new({'es.node.mode' => {:choices => ['local', 'network'], :method => 'get_random_one'}},
  402. '/tmp/dummy/jdk', po = DummyPropertyWriter.new('/tmp'))
  403. selection = test_object.generate_selections
  404. assert ['local', 'network'].include?(selection['es.node.mode'].first), 'selection choice is not correct'
  405. end
  406. def test_get_env_matrix
  407. test_object = RandomizedRunner.new(RANDOM_CHOICES,
  408. '/tmp/dummy/jdk', po = DummyPropertyWriter.new('/tmp'))
  409. selection = test_object.generate_selections
  410. env_matrix = test_object.get_env_matrix('/tmp/dummy/jdk', selection)
  411. puts YAML.dump(env_matrix)
  412. assert_equal '/tmp/dummy/jdk', env_matrix[:JAVA_HOME]
  413. end
  414. end