#!/usr/bin/env ruby # Licensed to Elasticsearch under one or more contributor # license agreements. See the NOTICE file distributed with # this work for additional information regarding copyright # ownership. Elasticsearch licenses this file to you 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 # # NAME # build_randomization.rb -- Generate property file for the JDK randomization test # # SYNOPSIS # build_randomization.rb [-d] [-l|t] # # DESCRIPTION # This script takes the randomization choices described in RANDOM_CHOICE and generates appropriate JAVA property file 'prop.txt' # This property file also contain the appropriate JDK selection, randomized. JDK randomization is based on what is available on the Jenkins tools # directory. This script is used by Jenkins test system to conduct Elasticsearch server randomization testing. # # In hash RANDOM_CHOICES, the key of randomization hash maps to key of java property. The value of the hash describes the possible value of the randomization # # For example RANDOM_CHOICES = { 'es.node.mode' => {:choices => ['local', 'network'], :method => :get_random_one} } means # es.node.mode will be set to either 'local' or 'network', each with 50% of probability # # OPTIONS SUMMARY # The options are as follows: # # -d, --debug Increase logging verbosity for debugging purpose # -t, --test Run in test mode. The script will execute unit tests. # -l, --local Run in local mode. In this mode, directory structure will be created under current directory to mimic # Jenkins' server directory layout. This mode is mainly used for development. require 'enumerator' require 'getoptlong' require 'log4r' require 'optparse' require 'rubygems' require 'yaml' include Log4r RANDOM_CHOICES = { 'tests.jvm.argline' => [ {:choices => ['-server'], :method => 'get_random_one'}, {:choices => ['-XX:+UseConcMarkSweepGC', '-XX:+UseParallelGC', '-XX:+UseSerialGC', '-XX:+UseG1GC'], :method => 'get_random_one'}, {:choices => ['-XX:+UseCompressedOops', '-XX:-UseCompressedOops'], :method => 'get_random_one'}, {:choices => ['-XX:+AggressiveOpts'], :method => 'get_50_percent'} ], 'es.node.mode' => {:choices => ['local', 'network'], :method => 'get_random_one'}, # bug forced to be false for now :test_nightly => { :method => :true_or_false}, 'tests.nightly' => {:selections => false}, 'tests.heap.size' => {:choices => [512, 1024], :method => :random_heap}, 'tests.assertion.disabled'=> {:choices => 'org.elasticsearch', :method => 'get_10_percent'}, 'tests.security.manager' => {:choices => [true, false], :method => 'get_90_percent'}, } L = Logger.new 'test_randomizer' L.outputters = Outputter.stdout L.level = INFO C = {:local => false, :test => false} OptionParser.new do |opts| opts.banner = "Usage: build_randomization.rb [options]" opts.on("-d", "--debug", "Debug mode") do |d| L.level = DEBUG end opts.on("-l", "--local", "Run in local mode") do |l| C[:local] = true end opts.on("-t", "--test", "Run unit tests") do |t| C[:test] = true end end.parse! class Randomizer attr_accessor :data_array def initialize(data_array) @data_array = data_array end def true_or_false [true, false][rand(2)] end def random_heap inner_data_array = [data_array[0], data_array[1], data_array[0] + rand(data_array[1] - data_array[0])] "%sm" % inner_data_array[rand(inner_data_array.size)] end def get_random_with_distribution(mdata_array, distribution) L.debug "randomized distribution data %s" % YAML.dump(mdata_array) L.debug "randomized distribution distribution %s" % YAML.dump(distribution) carry = 0 distribution_map = distribution.enum_for(:each_with_index).map { |x,i| pre_carry = carry ; carry += x; {i => x + pre_carry} } random_size = distribution_map.last.values.first selection = rand(random_size) #get the index that randomize choice mapped to choice = distribution_map.select do |x| x.values.first > selection #only keep the index with distribution value that is higher than the random generated number end.first.keys.first #first hash's first key is the index we want L.debug("randomized distribution choice %s" % mdata_array[choice]) mdata_array[choice] end def get_random_one data_array[rand(data_array.size)] end def method_missing(meth, *args, &block) # trap randomization based on percentage if meth.to_s =~ /^get_(\d+)_percent/ percentage = $1.to_i remain = 100 - percentage #data = args.first normalized_data = if(!data_array.kind_of?(Array)) [data_array, nil] else data_array end get_random_with_distribution(normalized_data, [percentage, remain]) else super end end end class JDKSelector attr_reader :directory, :jdk_list def initialize(directory) @directory = directory end # get selection of available JDKs from Jenkins automatic install directory def get_jdk @jdk_list = Dir.entries(directory).select do |x| x.chars.first == 'J' end.map do |y| File.join(directory, y) end self end def filter_java_6(files) files.select{ |i| File.basename(i).split(/[^0-9]/)[-1].to_i > 6 } end # do randomized selection from a given array def select_one(selection_array = nil) selection_array = filter_java_6(selection_array || @jdk_list) Randomizer.new(selection_array).get_random_one end def JDKSelector.generate_jdk_hash(jdk_choice) file_separator = if Gem.win_platform? File::ALT_SEPARATOR else File::SEPARATOR end { :PATH => [jdk_choice, 'bin'].join(file_separator) + File::PATH_SEPARATOR + ENV['PATH'], :JAVA_HOME => jdk_choice } end end # # Fix argument JDK selector # class FixedJDKSelector < JDKSelector def initialize(directory) @directory = [*directory] #selection of directories to pick from end def get_jdk #since JDK selection is already specified..jdk list is the @directory @jdk_list = @directory self end def select_one(selection_array = nil) #bypass filtering since this is not automatic selection_array ||= @jdk_list Randomizer.new(selection_array).get_random_one end end # # Property file writer # class PropertyWriter attr_reader :working_directory def initialize(mworking_directory) @working_directory = mworking_directory end # # pick first element out of array of hashes, generate write java property file def generate_property_file(data) directory = working_directory #array transformation content = data.to_a.map do |x| x.join('=') end.sort file_name = (ENV['BUILD_ID'] + ENV['BUILD_NUMBER']) || 'prop' rescue 'prop' file_name = file_name.split(File::SEPARATOR).first + '.txt' L.debug "Property file name is %s" % file_name File.open(File.join(directory, file_name), 'w') do |file| file.write(content.join("\n")) end end end # # Execute randomization logics # class RandomizedRunner attr_reader :random_choices, :jdk, :p_writer def initialize(mrandom_choices, mjdk, mwriter) @random_choices = mrandom_choices @jdk = mjdk @p_writer = mwriter end def generate_selections configuration = random_choices L.debug "Enter %s" % __method__ L.debug "Configuration %s" % YAML.dump(configuration) generated = {} configuration.each do |k, v| if(v.kind_of?(Hash)) if(v.has_key?(:method)) randomizer = Randomizer.new(v[:choices]) v[:selections] = randomizer.__send__(v[:method]) end else v.each do |x| if(x.has_key?(:method)) randomizer = Randomizer.new(x[:choices]) x[:selections] = randomizer.__send__(x[:method]) end end end end.each do |k, v| if(v.kind_of?(Array)) selections = v.inject([]) do |sum, current_hash| sum.push(current_hash[:selections]) end else selections = [v[:selections]] unless v[:selections].nil? end generated[k] = selections unless (selections.nil? || selections.size == 0) end L.debug "Generated selections %s" % YAML.dump(generated) generated end def get_env_matrix(jdk_selection, selections) L.debug "Enter %s" % __method__ #normalization s = {} selections.each do |k, v| if(v.size > 1) s[k] = v.compact.join(' ') #this should be dependent on class of v[0] and perform reduce operation instead... good enough for now else s[k] = v.first end end j = JDKSelector.generate_jdk_hash(jdk_selection) # create build description line desc = {} # TODO: better error handling desc[:BUILD_DESC] = "%s,%s,heap[%s],%s%s%s%s" % [ File.basename(j[:JAVA_HOME]), s['es.node.mode'], s['tests.heap.size'], s['tests.nightly'] ? 'nightly,':'', s['tests.jvm.argline'].gsub(/-XX:/,''), s.has_key?('tests.assertion.disabled')? ',assert off' : '', s['tests.security.manager'] ? ',sec manager on' : '' ] result = j.merge(s).merge(desc) L.debug(YAML.dump(result)) result end def run! p_writer.generate_property_file(get_env_matrix(jdk, generate_selections)) end end # # Main # unless(C[:test]) # Check to see if this is running locally unless(C[:local]) L.debug("Normal Mode") working_directory = ENV.fetch('WORKSPACE', (Gem.win_platform? ? Dir.pwd : '/var/tmp')) else L.debug("Local Mode") test_directory = 'tools/hudson.model.JDK/' unless(File.exist?(test_directory)) L.info "running local mode, setting up running environment" L.info "properties are written to file prop.txt" FileUtils.mkpath "%sJDK6" % test_directory FileUtils.mkpath "%sJDK7" % test_directory end working_directory = Dir.pwd end # script support both window and linux # TODO: refactor into platform/machine dependent class structure jdk = if(Gem.win_platform?) #window mode jdk directories are fixed #TODO: better logic L.debug("Window Mode") if(File.directory?('y:\jdk7\7u55')) #old window system under ec2 FixedJDKSelector.new('y:\jdk7\7u55') else #new metal window system FixedJDKSelector.new(['c:\PROGRA~1\JAVA\jdk1.8.0_05', 'c:\PROGRA~1\JAVA\jdk1.7.0_55']) end else #Jenkins sets pwd prior to execution L.debug("Linux Mode") JDKSelector.new(File.join(ENV['PWD'],'tools','hudson.model.JDK')) end runner = RandomizedRunner.new(RANDOM_CHOICES, jdk.get_jdk.select_one, PropertyWriter.new(working_directory)) environment_matrix = runner.run! exit 0 else require "test/unit" end # # Test # class TestJDKSelector < Test::Unit::TestCase L = Logger.new 'test' L.outputters = Outputter.stdout L.level = DEBUG def test_hash_generator jdk_choice = '/dummy/jdk7' generated = JDKSelector.generate_jdk_hash(jdk_choice) L.debug "Generated %s" % generated assert generated[:PATH].include?(jdk_choice), "PATH doesn't included choice" assert generated[:JAVA_HOME].include?(jdk_choice), "JAVA home doesn't include choice" end end class TestFixJDKSelector < Test::Unit::TestCase L = Logger.new 'test' L.outputters = Outputter.stdout L.level = DEBUG def test_initialize ['/home/dummy', ['/JDK7', '/home2'], ['home/dummy']].each do |x| test_object = FixedJDKSelector.new(x) assert_kind_of Array, test_object.directory assert_equal [*x], test_object.directory end end def test_select_one test_array = %w(one two three) test_object = FixedJDKSelector.new(test_array) assert test_array.include?(test_object.get_jdk.select_one) end def test_hash_generator jdk_choice = '/dummy/jdk7' generated = FixedJDKSelector.generate_jdk_hash(jdk_choice) L.debug "Generated %s" % generated assert generated[:PATH].include?(jdk_choice), "PATH doesn't included choice" assert generated[:JAVA_HOME].include?(jdk_choice), "JAVA home doesn't include choice" end end class TestPropertyWriter < Test::Unit::TestCase L = Logger.new 'test' L.outputters = Outputter.stdout L.level = DEBUG def test_initialize ['/home/dummy','/tmp'].each do |x| test_object = PropertyWriter.new(x) assert_kind_of String, test_object.working_directory assert_equal x, test_object.working_directory end end def test_generate_property test_file = '/tmp/prop.txt' File.delete(test_file) if File.exist?(test_file) test_object = PropertyWriter.new(File.dirname(test_file)) # default prop.txt test_object.generate_property_file({:hi => 'there'}) assert(File.exist?(test_file)) File.open(test_file, 'r') do |properties_file| properties_file.read.each_line do |line| line.strip! assert_equal 'hi=there', line, "content %s is not hi=there" % line end end File.delete(test_file) if File.exist?(test_file) end end class DummyPropertyWriter < PropertyWriter def generate_property_file(data) L.debug "generating property file for %s" % YAML.dump(data) L.debug "on directory %s" % working_directory end end class TestRandomizedRunner < Test::Unit::TestCase def test_initialize test_object = RandomizedRunner.new(RANDOM_CHOICES, '/tmp/dummy/jdk', po = PropertyWriter.new('/tmp')) assert_equal RANDOM_CHOICES, test_object.random_choices assert_equal '/tmp/dummy/jdk', test_object.jdk assert_equal po, test_object.p_writer end def test_generate_selection_no_method test_object = RandomizedRunner.new({'tests.one' => {:selections => false }}, '/tmp/dummy/jdk', po = DummyPropertyWriter.new('/tmp')) selection = test_object.generate_selections assert_equal false, selection['tests.one'].first, 'randomization without selection method fails' end def test_generate_with_method test_object = RandomizedRunner.new({'es.node.mode' => {:choices => ['local', 'network'], :method => 'get_random_one'}}, '/tmp/dummy/jdk', po = DummyPropertyWriter.new('/tmp')) selection = test_object.generate_selections assert ['local', 'network'].include?(selection['es.node.mode'].first), 'selection choice is not correct' end def test_get_env_matrix test_object = RandomizedRunner.new(RANDOM_CHOICES, '/tmp/dummy/jdk', po = DummyPropertyWriter.new('/tmp')) selection = test_object.generate_selections env_matrix = test_object.get_env_matrix('/tmp/dummy/jdk', selection) puts YAML.dump(env_matrix) assert_equal '/tmp/dummy/jdk', env_matrix[:JAVA_HOME] end end