lib/openstudio/extension/runner.rb in openstudio-extension-0.1.2 vs lib/openstudio/extension/runner.rb in openstudio-extension-0.1.3
- old
+ new
@@ -1,644 +1,667 @@
-
-# *******************************************************************************
-# OpenStudio(R), Copyright (c) 2008-2019, Alliance for Sustainable Energy, LLC.
-# All rights reserved.
-# Redistribution and use in source and binary forms, with or without
-# modification, are permitted provided that the following conditions are met:
-#
-# (1) Redistributions of source code must retain the above copyright notice,
-# this list of conditions and the following disclaimer.
-#
-# (2) Redistributions in binary form must reproduce the above copyright notice,
-# this list of conditions and the following disclaimer in the documentation
-# and/or other materials provided with the distribution.
-#
-# (3) Neither the name of the copyright holder nor the names of any contributors
-# may be used to endorse or promote products derived from this software without
-# specific prior written permission from the respective party.
-#
-# (4) Other than as required in clauses (1) and (2), distributions in any form
-# of modifications or other derivative works may not use the "OpenStudio"
-# trademark, "OS", "os", or any other confusingly similar designation without
-# specific prior written permission from Alliance for Sustainable Energy, LLC.
-#
-# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER(S) AND ANY CONTRIBUTORS
-# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
-# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
-# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER(S), ANY CONTRIBUTORS, THE
-# UNITED STATES GOVERNMENT, OR THE UNITED STATES DEPARTMENT OF ENERGY, NOR ANY OF
-# THEIR EMPLOYEES, BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
-# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
-# OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
-# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
-# STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
-# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-# *******************************************************************************
-
-require 'bundler'
-require 'fileutils'
-require 'json'
-require 'open3'
-require 'openstudio'
-require 'yaml'
-require 'fileutils'
-require 'parallel'
-
-module OpenStudio
- module Extension
- ##
- # The Runner class provides functionality to run various commands including calls to the OpenStudio CLI.
- #
- class Runner
- attr_reader :gemfile_path, :bundle_install_path
- ##
- # When initialized with a directory containing a Gemfile, the Runner will attempt to create a bundle
- # compatible with the OpenStudio CLI.
- ##
- # @param [String] dirname Directory to run commands in, defaults to Dir.pwd. If directory includes a Gemfile then create a local bundle.
- def initialize(dirname = Dir.pwd, bundle_without = [])
- # DLM: I am not sure if we want to use the main root directory to create these bundles
- # had the idea of passing in a Gemfile name/alias and path to Gemfile, then doing the bundle in ~/OpenStudio/#{alias} or something like that?
-
- puts "Initializing runner with dirname: '#{dirname}'"
- @dirname = File.absolute_path(dirname)
- @gemfile_path = File.join(@dirname, 'Gemfile')
- @bundle_install_path = File.join(@dirname, '.bundle/install/')
- @original_dir = Dir.pwd
-
- @bundle_without = bundle_without
- @bundle_without_string = bundle_without.join(' ')
- puts "@bundle_without_string = '#{@bundle_without_string}'"
-
- raise "#{@dirname} does not exist" if !File.exist?(@dirname)
- raise "#{@dirname} is not a directory" if !File.directory?(@dirname)
-
- if !File.exist?(@gemfile_path)
- # if there is no gemfile set these to nil
- @gemfile_path = nil
- @bundle_install_path = nil
- else
- # there is a gemfile, attempt to create a bundle
- original_dir = Dir.pwd
- begin
- # go to the directory with the gemfile
- Dir.chdir(@dirname)
-
- # test to see if bundle is installed
- check_bundle = run_command('bundle -v', get_clean_env)
- if !check_bundle
- raise "Failed to run command 'bundle -v', check that bundle is installed" if !File.exist?(@dirname)
- end
-
- # TODO: check that ruby version is correct
-
- # check existing config
- needs_config = true
- if File.exist?('./.bundle/config')
- puts 'config exists'
- needs_config = false
- config = YAML.load_file('./.bundle/config')
-
- if config['BUNDLE_PATH'] != @bundle_install_path
- needs_config = true
- end
-
- # if config['BUNDLE_WITHOUT'] != @bundle_without_string
- # needs_config = true
- # end
- end
-
- # check existing platform
- needs_platform = true
- if File.exist?('Gemfile.lock')
- puts 'Gemfile.lock exists'
- gemfile_lock = Bundler::LockfileParser.new(Bundler.read_file('Gemfile.lock'))
- if gemfile_lock.platforms.include?('ruby')
- # already been configured, might not be up to date
- needs_platform = false
- end
- end
-
- puts "needs_config = #{needs_config}"
- if needs_config
- run_command("bundle config --local path '#{@bundle_install_path}'", get_clean_env)
- # run_command("bundle config --local without '#{@bundle_without_string}'", get_clean_env)
- end
-
- puts "needs_platform = #{needs_platform}"
- if needs_platform
- run_command('bundle lock --add_platform ruby', get_clean_env)
- end
-
- needs_update = needs_config || needs_platform
- if !needs_update
- if !File.exist?('Gemfile.lock') || File.mtime(@gemfile_path) > File.mtime('Gemfile.lock')
- needs_update = true
- end
- end
-
- puts "needs_update = #{needs_update}"
- if needs_update
- run_command('bundle update', get_clean_env)
- end
- ensure
- Dir.chdir(@original_dir)
- end
- end
- end
-
- ##
- # Returns a hash of environment variables that can be merged with the current environment to prevent automatic bundle activation.
- #
- # DLM: should this be a module or class method?
- ##
- # @return [Hash]
- def get_clean_env
- # blank out bundler and gem path modifications, will be re-setup by new call
- new_env = {}
- new_env['BUNDLER_ORIG_MANPATH'] = nil
- new_env['BUNDLER_ORIG_PATH'] = nil
- new_env['BUNDLER_VERSION'] = nil
- new_env['BUNDLE_BIN_PATH'] = nil
- new_env['RUBYLIB'] = nil
- new_env['RUBYOPT'] = nil
-
- # DLM: preserve GEM_HOME and GEM_PATH set by current bundle because we are not supporting bundle
- # requires to ruby gems will work, will fail if we require a native gem
- new_env['GEM_PATH'] = nil
- new_env['GEM_HOME'] = nil
-
- # DLM: for now, ignore current bundle in case it has binary dependencies in it
- # bundle_gemfile = ENV['BUNDLE_GEMFILE']
- # bundle_path = ENV['BUNDLE_PATH']
- # if bundle_gemfile.nil? || bundle_path.nil?
- new_env['BUNDLE_GEMFILE'] = nil
- new_env['BUNDLE_PATH'] = nil
- new_env['BUNDLE_WITHOUT'] = nil
- # else
- # new_env['BUNDLE_GEMFILE'] = bundle_gemfile
- # new_env['BUNDLE_PATH'] = bundle_path
- # end
-
- return new_env
- end
-
- ##
- # Run a command after merging the current environment with env. Command is run in @dirname, returns to Dir.pwd after completion.
- # Returns true if the command completes successfully, false otherwise.
- # Standard Out, Standard Error, and Status Code are collected and printed, but not returned.
- ##
- # @return [Boolean]
- def run_command(command, env = {})
- result = false
- original_dir = Dir.pwd
- begin
- Dir.chdir(@dirname)
-
- # DLM: using popen3 here can result in deadlocks
- stdout_str, stderr_str, status = Open3.capture3(env, command)
- if status.success?
- # puts "Command completed successfully"
- # puts "stdout: #{stdout_str}"
- # puts "stderr: #{stderr_str}"
- # STDOUT.flush
- result = true
- else
- puts "Error running command: '#{command}'"
- puts "stdout: #{stdout_str}"
- puts "stderr: #{stderr_str}"
- STDOUT.flush
- result = false
- end
- ensure
- Dir.chdir(original_dir)
- return result
- end
-
- return result
- end
-
- ##
- # Get path to all measures found under measure dir.
- ##
- # @param [String] measures_dir Measures directory
- ##
- # @return [Array] returns path to all measure directories found under measure dir
- def get_measures_in_dir(measures_dir)
- measures = Dir.glob(File.join(measures_dir, '**/measure.rb'))
- if measures.empty?
- # also try nested 2-deep to support openstudio-measures
- measures = Dir.glob(File.join(measures_dir, '**/**/measure.rb'))
- end
-
- result = []
- measures.each { |m| result << File.dirname(m) }
- return result
- end
-
- ##
- # Get path to all measures dirs found under measure dir.
- ##
- # @param [String] measures_dir Measures directory
- ##
- # @return [Array] returns path to all directories containing measures found under measure dir
- def get_measure_dirs_in_dir(measures_dir)
- measures = Dir.glob(File.join(measures_dir, '**/measure.rb'))
- if measures.empty?
- # also try nested 2-deep to support openstudio-measures
- measures = Dir.glob(File.join(measures_dir, '**/**/measure.rb'))
- end
-
- result = []
- measures.each { |m| result << File.dirname(File.dirname(m)) }
-
- return result.uniq
- end
-
- ##
- # Run the OpenStudio CLI command to test measures on given directory
- # Returns true if the command completes successfully, false otherwise.
- # measures_dir configured in rake_task
- ##
- # @return [Boolean]
- def test_measures_with_cli(measures_dir)
- puts 'Testing measures with CLI system call'
- if measures_dir.nil? || measures_dir.empty?
- puts 'Measures dir is nil or empty'
- return true
- end
-
- puts "measures path: #{measures_dir}"
-
- cli = OpenStudio.getOpenStudioCLI
-
- the_call = ''
- if @gemfile_path
- if @bundle_without_string.empty?
- the_call = "#{cli} --verbose --bundle '#{@gemfile_path}' --bundle_path '#{@bundle_install_path}' measure -r '#{measures_dir}'"
- else
- the_call = "#{cli} --verbose --bundle '#{@gemfile_path}' --bundle_path '#{@bundle_install_path}' --bundle_without '#{@bundle_without_string}' measure -r '#{measures_dir}'"
- end
- else
- the_call = "#{cli} --verbose measure -r #{measures_dir}"
- end
-
- puts 'SYSTEM CALL:'
- puts the_call
- STDOUT.flush
- result = run_command(the_call, get_clean_env)
- puts "DONE, result = #{result}"
- STDOUT.flush
-
- return result
- end
-
- ##
- # Run the OpenStudio CLI command to update measures on given directory
- # Returns true if the command completes successfully, false otherwise.
- ##
- # @return [Boolean]
- def update_measures(measures_dir)
- puts 'Updating measures with CLI system call'
- if measures_dir.nil? || measures_dir.empty?
- puts 'Measures dir is nil or empty'
- return true
- end
-
- result = true
- # DLM: this is a temporary workaround to handle OpenStudio-Measures
- get_measure_dirs_in_dir(measures_dir).each do |measures_dir|
- puts "measures path: #{measures_dir}"
-
- cli = OpenStudio.getOpenStudioCLI
-
- the_call = ''
- if @gemfile_path
- if @bundle_without_string.empty?
- the_call = "#{cli} --verbose --bundle '#{@gemfile_path}' --bundle_path '#{@bundle_install_path}' measure -t '#{measures_dir}'"
- else
- the_call = "#{cli} --verbose --bundle '#{@gemfile_path}' --bundle_path '#{@bundle_install_path}' --bundle_without '#{@bundle_without_string}' measure -t '#{measures_dir}'"
- end
- else
- the_call = "#{cli} --verbose measure -t '#{measures_dir}'"
- end
-
- puts 'SYSTEM CALL:'
- puts the_call
- STDOUT.flush
- result &&= run_command(the_call, get_clean_env)
- puts "DONE, result = #{result}"
- STDOUT.flush
- end
-
- return result
- end
-
- ##
- # List measures in given directory
- # Returns true if the command completes successfully, false otherwise.
- ##
- # @return [Boolean]
-
- ##
- def list_measures(measures_dir)
- puts 'Listing measures'
- if measures_dir.nil? || measures_dir.empty?
- puts 'Measures dir is nil or empty'
- return true
- end
-
- puts "measures path: #{measures_dir}"
-
- # this is to accommodate a single measures dir (like most gems)
- # or a repo with multiple directories fo measures (like OpenStudio-measures)
- measures = Dir.glob(File.join(measures_dir, '**/measure.rb'))
- if measures.empty?
- # also try nested 2-deep to support openstudio-measures
- measures = Dir.glob(File.join(measures_dir, '**/**/measure.rb'))
- end
- puts "#{measures.length} MEASURES FOUND"
- measures.each do |measure|
- name = measure.split('/')[-2]
- puts name.to_s
- end
- end
-
- # Update measures by copying in the latest resource files from the Extension gem into
- # the measures' respective resources folders.
- # measures_dir and core_dir configured in rake_task
- # Returns true if the command completes successfully, false otherwise.
- #
- # @return [Boolean]
- def copy_core_files(measures_dir, core_dir)
- puts 'Copying measure resources'
- if measures_dir.nil? || measures_dir.empty?
- puts 'Measures dir is nil or empty'
- return true
- end
-
- result = false
- puts 'Copying updated resource files from extension core directory to individual measures.'
- puts 'Only files that have actually been changed will be listed.'
-
- # get all resource files in the core dir
- resource_files = Dir.glob(File.join(core_dir, '/*.*'))
-
- # this is to accommodate a single measures dir (like most gems)
- # or a repo with multiple directories fo measures (like OpenStudio-measures)
- measures = Dir.glob(File.join(measures_dir, '**/resources/*.rb'))
- if measures.empty?
- # also try nested 2-deep to support openstudio-measures
- measures = Dir.glob(File.join(measures_dir, '**/**/resources/*.rb'))
- end
-
- # Note: some older measures like AEDG use 'OsLib_SomeName' instead of 'os_lib_some_name'
- # this script isn't replacing those copies
-
- # loop through resource files
- resource_files.each do |resource_file|
- # loop through measure dirs looking for matching file
- measures.each do |measure|
- next unless File.basename(measure) == File.basename(resource_file)
- next if FileUtils.identical?(resource_file, File.path(measure))
- puts "Replacing #{measure} with #{resource_file}."
- FileUtils.cp(resource_file, File.path(measure))
- end
- end
- result = true
-
- return result
- end
-
- # Update measures by adding license file
- # measures_dir and doc_templates_dir configured in rake_task
- # Returns true if the command completes successfully, false otherwise.
- ##
- # @return [Boolean]
- def add_measure_license(measures_dir, doc_templates_dir)
- puts 'Adding measure licenses'
- if measures_dir.nil? || measures_dir.empty?
- puts 'Measures dir is nil or empty'
- return true
- elsif doc_templates_dir.nil? || doc_templates_dir.empty?
- puts 'Doc templates dir is nil or empty'
- return false
- end
-
- result = false
- license_file = File.join(doc_templates_dir, 'LICENSE.md')
- puts "License file path: #{license_file}"
-
- raise "License file not found '#{license_file}'" if !File.exist?(license_file)
-
- measures = Dir["#{measures_dir}/**/measure.rb"]
- if measures.empty?
- # also try nested 2-deep to support openstudio-measures
- measures = Dir["#{measures_dir}/**/**/measure.rb"]
- end
- measures.each do |measure|
- FileUtils.cp(license_file, "#{File.dirname(measure)}/LICENSE.md")
- end
- result = true
- return result
- end
-
- # Update measures by adding license file
- # measures_dir and doc_templates_dir configured in rake_task
- # Returns true if the command completes successfully, false otherwise.
- ##
- # @return [Boolean]
- def add_measure_readme(measures_dir, doc_templates_dir)
- puts 'Adding measure readmes'
- if measures_dir.nil? || measures_dir.empty?
- puts 'Measures dir is nil or empty'
- return true
- elsif doc_templates_dir.nil? || doc_templates_dir.empty?
- puts 'Measures files dir is nil or empty'
- return false
- end
-
- result = false
- readme_file = File.join(doc_templates_dir, 'README.md.erb')
- puts "Readme file path: #{readme_file}"
-
- raise "Readme file not found '#{readme_file}'" if !File.exist?(readme_file)
-
- measures = Dir["#{measures_dir}/**/measure.rb"]
- if measures.empty?
- # also try nested 2-deep to support openstudio-measures
- measures = Dir["#{measures_dir}/**/**/measure.rb"]
- end
- measures.each do |measure|
- next if File.exist?("#{File.dirname(measure)}/README.md.erb")
- next if File.exist?("#{File.dirname(measure)}/README.md")
- puts "adding template README to #{measure}"
- FileUtils.cp(readme_file, "#{File.dirname(measure)}/README.md.erb")
- end
- result = true
- return result
- end
-
- def update_copyright(root_dir, doc_templates_dir)
- if root_dir.nil? || root_dir.empty?
- puts 'Root dir is nil or empty'
- return false
- elsif doc_templates_dir.nil? || doc_templates_dir.empty?
- puts 'Doc templates dir is nil or empty'
- return false
- end
-
- if File.exist?(File.join(doc_templates_dir, 'LICENSE.md'))
- if File.exist?(File.join(root_dir, 'LICENSE.md'))
- puts 'updating LICENSE.md in root dir'
- FileUtils.cp(File.join(doc_templates_dir, 'LICENSE.md'), File.join(root_dir, 'LICENSE.md'))
- end
- end
-
- ruby_regex = /^\#\s?[\#\*]{12,}.*copyright.*?\#\s?[\#\*]{12,}\s*$/mi
- erb_regex = /^<%\s*\#\s?[\#\*]{12,}.*copyright.*?\#\s?[\#\*]{12,}\s*%>$/mi
- js_regex = /^\/\* @preserve.*copyright.*license.{2}\*\//mi
-
- filename = File.join(doc_templates_dir, 'copyright_ruby.txt')
- puts "Copyright file path: #{filename}"
- raise "Copyright file not found '#{filename}'" if !File.exist?(filename)
- file = File.open(filename, 'r')
- ruby_header_text = file.read
- file.close
- ruby_header_text.strip!
- ruby_header_text += "\n"
-
- filename = File.join(doc_templates_dir, 'copyright_erb.txt')
- puts "Copyright file path: #{filename}"
- raise "Copyright file not found '#{filename}'" if !File.exist?(filename)
- file = File.open(filename, 'r')
- erb_header_text = file.read
- file.close
- erb_header_text.strip!
- erb_header_text += "\n"
-
- filename = File.join(doc_templates_dir, 'copyright_js.txt')
- puts "Copyright file path: #{filename}"
- raise "Copyright file not found '#{filename}'" if !File.exist?(filename)
- file = File.open(filename, 'r')
- js_header_text = file.read
- file.close
- js_header_text.strip!
- js_header_text += "\n"
-
- raise 'bad copyright_ruby.txt' if ruby_header_text !~ ruby_regex
- raise 'bad copyright_erb.txt' if erb_header_text !~ erb_regex
- raise 'bad copyright_js.txt' if js_header_text !~ js_regex
-
- # look for .rb, .html.erb, and .js.erb
- paths = [
- { glob: "#{root_dir}/**/*.rb", license: ruby_header_text, regex: ruby_regex },
- { glob: "#{root_dir}/**/*.html.erb", license: erb_header_text, regex: erb_regex },
- { glob: "#{root_dir}/**/*.js.erb", license: js_header_text, regex: js_regex }
- ]
-
- puts "Encoding.default_external = #{Encoding.default_external}"
- puts "Encoding.default_internal = #{Encoding.default_internal}"
-
- paths.each do |path|
- Dir[path[:glob]].each do |file|
- puts "Updating license in file #{file}"
- f = File.read(file)
- if f =~ path[:regex]
- puts ' License found -- updating'
- File.open(file, 'w') { |write| write << f.gsub(path[:regex], path[:license]) }
- elsif f =~ /\(C\)/i || f =~ /\(Copyright\)/i
- puts ' File already has copyright -- skipping'
- else
- puts ' No license found -- adding'
- if f =~ /#!/
- puts ' CANNOT add license to file automatically, add it manually and it will update automatically in the future'
- next
- end
- File.open(file, 'w') { |write| write << f.insert(0, path[:license] + "\n") }
- end
- end
- end
- end
-
- ##
- # Run the OpenStudio CLI on an OSW. The OSW is configured to include measure and file locations for all loaded OpenStudio Extensions.
- ##
- # @param [String, Hash] in_osw If string this is the path to an OSW file on disk, if Hash it is loaded JSON with symbolized keys
- # @param [String] run_dir Directory to run the OSW in, will be created if does not exist
- ##
- # @return [Boolean] True if command succeeded, false otherwise # DLM: should this return path to out.osw instead?
- def run_osw(in_osw, run_dir)
- run_dir = File.absolute_path(run_dir)
-
- if in_osw.is_a?(String)
- in_osw_path = in_osw
- raise "'#{in_osw_path}' does not exist" if !File.exist?(in_osw_path)
-
- in_osw = {}
- File.open(in_osw_path, 'r') do |file|
- in_osw = JSON.parse(file.read, symbolize_names: true)
- end
- end
-
- osw = OpenStudio::Extension.configure_osw(in_osw)
- osw[:run_directory] = run_dir
-
- FileUtils.mkdir_p(run_dir)
-
- run_osw_path = File.join(run_dir, 'in.osw')
- File.open(run_osw_path, 'w') do |file|
- file.puts JSON.pretty_generate(osw)
- end
-
- cli = OpenStudio.getOpenStudioCLI
- out_log = run_osw_path + '.log'
- if Gem.win_platform?
- # out_log = "nul"
- else
- # out_log = "/dev/null"
- end
-
- the_call = ''
- if @gemfile_path
- if @bundle_without_string.empty?
- the_call = "#{cli} --verbose --bundle '#{@gemfile_path}' --bundle_path '#{@bundle_install_path}' run -w '#{run_osw_path}' 2>&1 > \"#{out_log}\""
- else
- the_call = "#{cli} --verbose --bundle '#{@gemfile_path}' --bundle_path '#{@bundle_install_path}' --bundle_without '#{@bundle_without_string}' run -w '#{run_osw_path}' 2>&1 > \"#{out_log}\""
- end
- else
- the_call = "#{cli} --verbose run -w '#{run_osw_path}' 2>&1 > \"#{out_log}\""
- end
-
- puts 'SYSTEM CALL:'
- puts the_call
- STDOUT.flush
- result = run_command(the_call, get_clean_env)
- puts "DONE, result = #{result}"
- STDOUT.flush
-
- # DLM: this does not always return false for failed CLI runs, consider checking for failed.job file as backup test
-
- return result
- end
-
- # run osws, return any failure messages
- def run_osws(osw_files, num_parallel = 1, max_to_run = Float::INFINITY)
- failures = []
-
- osw_files = osw_files.slice(0, [osw_files.size, max_to_run].min)
-
- Parallel.each(osw_files, in_threads: num_parallel) do |osw|
- # osw_files.each do |osw|
-
- result = run_osw(osw, File.dirname(osw))
-
- if !result
- failures << "Failed to run OSW '#{osw}'"
- end
- end
-
- return failures
- end
- end
- end
-end
+# *******************************************************************************
+# OpenStudio(R), Copyright (c) 2008-2019, Alliance for Sustainable Energy, LLC.
+# All rights reserved.
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#
+# (1) Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+#
+# (2) Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+#
+# (3) Neither the name of the copyright holder nor the names of any contributors
+# may be used to endorse or promote products derived from this software without
+# specific prior written permission from the respective party.
+#
+# (4) Other than as required in clauses (1) and (2), distributions in any form
+# of modifications or other derivative works may not use the "OpenStudio"
+# trademark, "OS", "os", or any other confusingly similar designation without
+# specific prior written permission from Alliance for Sustainable Energy, LLC.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER(S) AND ANY CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER(S), ANY CONTRIBUTORS, THE
+# UNITED STATES GOVERNMENT, OR THE UNITED STATES DEPARTMENT OF ENERGY, NOR ANY OF
+# THEIR EMPLOYEES, BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
+# OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+# STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+# *******************************************************************************
+
+require 'bundler'
+require 'fileutils'
+require 'json'
+require 'open3'
+require 'openstudio'
+require 'yaml'
+require 'fileutils'
+require 'parallel'
+
+module OpenStudio
+ module Extension
+ ##
+ # The Runner class provides functionality to run various commands including calls to the OpenStudio CLI.
+ #
+ class Runner
+ attr_reader :gemfile_path, :bundle_install_path, :options
+
+ ##
+ # When initialized with a directory containing a Gemfile, the Runner will attempt to create a bundle
+ # compatible with the OpenStudio CLI.
+ ##
+ # @param [String] dirname Directory to run commands in, defaults to Dir.pwd. If directory includes a Gemfile then create a local bundle.
+ # @param bundle_without [Hash] Hash describing the distribution of the variable.
+ # @param options [Hash] Hash describing options for running the simulation. These are the defaults for all runs unless overriden within the run_* methods.
+ # @option options [String] :max_datapoints Max number of datapoints to run
+ # @option options [String] :num_parallel Number of simulations to run in parallel at a time
+ # @option options [String] :run_simulations Set to true to run the simulations
+ # @option options [String] :verbose Set to true to receive extra information while running simulations
+ def initialize(dirname = Dir.pwd, bundle_without = [], options = {})
+ # DLM: I am not sure if we want to use the main root directory to create these bundles
+ # had the idea of passing in a Gemfile name/alias and path to Gemfile, then doing the bundle in ~/OpenStudio/#{alias} or something like that?
+
+ # if the dirname contains a runner.conf file, then use the config file to specify the parameters
+ if File.exist?(File.join(dirname, OpenStudio::Extension::RunnerConfig::FILENAME)) && !options
+ puts 'Using runner options from runner.conf file'
+ runner_config = OpenStudio::Extension::RunnerConfig.new(dirname)
+ @options = runner_config.options
+ else
+ # use the passed values or defaults overriden by passed options
+ @options = OpenStudio::Extension::RunnerConfig.default_config.merge(options)
+ end
+
+ puts "Initializing runner with dirname: '#{dirname}' and options: #{@options}"
+ @dirname = File.absolute_path(dirname)
+ @gemfile_path = File.join(@dirname, 'Gemfile')
+ @bundle_install_path = File.join(@dirname, '.bundle/install/')
+ @original_dir = Dir.pwd
+
+ @bundle_without = bundle_without || []
+ @bundle_without_string = @bundle_without.join(' ')
+ puts "@bundle_without_string = '#{@bundle_without_string}'"
+
+ raise "#{@dirname} does not exist" unless File.exist?(@dirname)
+ raise "#{@dirname} is not a directory" unless File.directory?(@dirname)
+
+ if !File.exist?(@gemfile_path)
+ # if there is no gemfile set these to nil
+ @gemfile_path = nil
+ @bundle_install_path = nil
+ else
+ # there is a gemfile, attempt to create a bundle
+ begin
+ # go to the directory with the gemfile
+ Dir.chdir(@dirname)
+
+ # test to see if bundle is installed
+ check_bundle = run_command('bundle -v', get_clean_env)
+ if !check_bundle
+ raise "Failed to run command 'bundle -v', check that bundle is installed" if !File.exist?(@dirname)
+ end
+
+ # TODO: check that ruby version is correct
+
+ # check existing config
+ needs_config = true
+ if File.exist?('./.bundle/config')
+ puts 'config exists'
+ needs_config = false
+ config = YAML.load_file('./.bundle/config')
+
+ if config['BUNDLE_PATH'] != @bundle_install_path
+ needs_config = true
+ end
+
+ # if config['BUNDLE_WITHOUT'] != @bundle_without_string
+ # needs_config = true
+ # end
+ end
+
+ # check existing platform
+ needs_platform = true
+ if File.exist?('Gemfile.lock')
+ puts 'Gemfile.lock exists'
+ gemfile_lock = Bundler::LockfileParser.new(Bundler.read_file('Gemfile.lock'))
+ if gemfile_lock.platforms.include?('ruby')
+ # already been configured, might not be up to date
+ needs_platform = false
+ end
+ end
+
+ puts "needs_config = #{needs_config}"
+ if needs_config
+ run_command("bundle config --local path '#{@bundle_install_path}'", get_clean_env)
+ # run_command("bundle config --local without '#{@bundle_without_string}'", get_clean_env)
+ end
+
+ puts "needs_platform = #{needs_platform}"
+ if needs_platform
+ run_command('bundle lock --add_platform ruby', get_clean_env)
+ end
+
+ needs_update = needs_config || needs_platform
+ if !needs_update
+ if !File.exist?('Gemfile.lock') || File.mtime(@gemfile_path) > File.mtime('Gemfile.lock')
+ needs_update = true
+ end
+ end
+
+ puts "needs_update = #{needs_update}"
+ if needs_update
+ run_command('bundle update', get_clean_env)
+ end
+ ensure
+ Dir.chdir(@original_dir)
+ end
+ end
+ end
+
+ ##
+ # Returns a hash of environment variables that can be merged with the current environment to prevent automatic bundle activation.
+ #
+ # DLM: should this be a module or class method?
+ ##
+ # @return [Hash]
+ def get_clean_env
+ # blank out bundler and gem path modifications, will be re-setup by new call
+ new_env = {}
+ new_env['BUNDLER_ORIG_MANPATH'] = nil
+ new_env['BUNDLER_ORIG_PATH'] = nil
+ new_env['BUNDLER_VERSION'] = nil
+ new_env['BUNDLE_BIN_PATH'] = nil
+ new_env['RUBYLIB'] = nil
+ new_env['RUBYOPT'] = nil
+
+ # DLM: preserve GEM_HOME and GEM_PATH set by current bundle because we are not supporting bundle
+ # requires to ruby gems will work, will fail if we require a native gem
+ new_env['GEM_PATH'] = nil
+ new_env['GEM_HOME'] = nil
+
+ # DLM: for now, ignore current bundle in case it has binary dependencies in it
+ # bundle_gemfile = ENV['BUNDLE_GEMFILE']
+ # bundle_path = ENV['BUNDLE_PATH']
+ # if bundle_gemfile.nil? || bundle_path.nil?
+ new_env['BUNDLE_GEMFILE'] = nil
+ new_env['BUNDLE_PATH'] = nil
+ new_env['BUNDLE_WITHOUT'] = nil
+ # else
+ # new_env['BUNDLE_GEMFILE'] = bundle_gemfile
+ # new_env['BUNDLE_PATH'] = bundle_path
+ # end
+
+ return new_env
+ end
+
+ ##
+ # Run a command after merging the current environment with env. Command is run in @dirname, returns to Dir.pwd after completion.
+ # Returns true if the command completes successfully, false otherwise.
+ # Standard Out, Standard Error, and Status Code are collected and printed, but not returned.
+ ##
+ # @return [Boolean]
+ def run_command(command, env = {})
+ result = false
+ original_dir = Dir.pwd
+ begin
+ Dir.chdir(@dirname)
+
+ # DLM: using popen3 here can result in deadlocks
+ stdout_str, stderr_str, status = Open3.capture3(env, command)
+ if status.success?
+ # puts "Command completed successfully"
+ # puts "stdout: #{stdout_str}"
+ # puts "stderr: #{stderr_str}"
+ # STDOUT.flush
+ result = true
+ else
+ puts "Error running command: '#{command}'"
+ puts "stdout: #{stdout_str}"
+ puts "stderr: #{stderr_str}"
+ STDOUT.flush
+ result = false
+ end
+ ensure
+ Dir.chdir(original_dir)
+ end
+
+ return result
+ end
+
+ ##
+ # Get path to all measures found under measure dir.
+ ##
+ # @param [String] measures_dir Measures directory
+ ##
+ # @return [Array] returns path to all measure directories found under measure dir
+ def get_measures_in_dir(measures_dir)
+ measures = Dir.glob(File.join(measures_dir, '**/measure.rb'))
+ if measures.empty?
+ # also try nested 2-deep to support openstudio-measures
+ measures = Dir.glob(File.join(measures_dir, '**/**/measure.rb'))
+ end
+
+ result = []
+ measures.each { |m| result << File.dirname(m) }
+ return result
+ end
+
+ ##
+ # Get path to all measures dirs found under measure dir.
+ ##
+ # @param [String] measures_dir Measures directory
+ ##
+ # @return [Array] returns path to all directories containing measures found under measure dir
+ def get_measure_dirs_in_dir(measures_dir)
+ measures = Dir.glob(File.join(measures_dir, '**/measure.rb'))
+ if measures.empty?
+ # also try nested 2-deep to support openstudio-measures
+ measures = Dir.glob(File.join(measures_dir, '**/**/measure.rb'))
+ end
+
+ result = []
+ measures.each { |m| result << File.dirname(File.dirname(m)) }
+
+ return result.uniq
+ end
+
+ ##
+ # Run the OpenStudio CLI command to test measures on given directory
+ # Returns true if the command completes successfully, false otherwise.
+ # measures_dir configured in rake_task
+ ##
+ # @return [Boolean]
+ def test_measures_with_cli(measures_dir)
+ puts 'Testing measures with CLI system call'
+ if measures_dir.nil? || measures_dir.empty?
+ puts 'Measures dir is nil or empty'
+ return true
+ end
+
+ puts "measures path: #{measures_dir}"
+
+ cli = OpenStudio.getOpenStudioCLI
+
+ the_call = ''
+ if @gemfile_path
+ if @bundle_without_string.empty?
+ the_call = "#{cli} --verbose --bundle '#{@gemfile_path}' --bundle_path '#{@bundle_install_path}' measure -r '#{measures_dir}'"
+ else
+ the_call = "#{cli} --verbose --bundle '#{@gemfile_path}' --bundle_path '#{@bundle_install_path}' --bundle_without '#{@bundle_without_string}' measure -r '#{measures_dir}'"
+ end
+ else
+ the_call = "#{cli} --verbose measure -r #{measures_dir}"
+ end
+
+ puts 'SYSTEM CALL:'
+ puts the_call
+ STDOUT.flush
+ result = run_command(the_call, get_clean_env)
+ puts "DONE, result = #{result}"
+ STDOUT.flush
+
+ return result
+ end
+
+ ##
+ # Run the OpenStudio CLI command to update measures on given directory
+ # Returns true if the command completes successfully, false otherwise.
+ ##
+ # @return [Boolean]
+ def update_measures(measures_dir)
+ puts 'Updating measures with CLI system call'
+ if measures_dir.nil? || measures_dir.empty?
+ puts 'Measures dir is nil or empty'
+ return true
+ end
+
+ result = true
+ # DLM: this is a temporary workaround to handle OpenStudio-Measures
+ get_measure_dirs_in_dir(measures_dir).each do |m_dir|
+ puts "measures path: #{m_dir}"
+
+ cli = OpenStudio.getOpenStudioCLI
+
+ the_call = ''
+ if @gemfile_path
+ if @bundle_without_string.empty?
+ the_call = "#{cli} --verbose --bundle '#{@gemfile_path}' --bundle_path '#{@bundle_install_path}' measure -t '#{m_dir}'"
+ else
+ the_call = "#{cli} --verbose --bundle '#{@gemfile_path}' --bundle_path '#{@bundle_install_path}' --bundle_without '#{@bundle_without_string}' measure -t '#{m_dir}'"
+ end
+ else
+ the_call = "#{cli} --verbose measure -t '#{m_dir}'"
+ end
+
+ puts 'SYSTEM CALL:'
+ puts the_call
+ STDOUT.flush
+ result &&= run_command(the_call, get_clean_env)
+ puts "DONE, result = #{result}"
+ STDOUT.flush
+ end
+
+ return result
+ end
+
+ ##
+ # List measures in given directory
+ # Returns true if the command completes successfully, false otherwise.
+ ##
+ # @return [Boolean]
+
+ ##
+ def list_measures(measures_dir)
+ puts 'Listing measures'
+ if measures_dir.nil? || measures_dir.empty?
+ puts 'Measures dir is nil or empty'
+ return true
+ end
+
+ puts "measures path: #{measures_dir}"
+
+ # this is to accommodate a single measures dir (like most gems)
+ # or a repo with multiple directories fo measures (like OpenStudio-measures)
+ measures = Dir.glob(File.join(measures_dir, '**/measure.rb'))
+ if measures.empty?
+ # also try nested 2-deep to support openstudio-measures
+ measures = Dir.glob(File.join(measures_dir, '**/**/measure.rb'))
+ end
+ puts "#{measures.length} MEASURES FOUND"
+ measures.each do |measure|
+ name = measure.split('/')[-2]
+ puts name.to_s
+ end
+ end
+
+ # Update measures by copying in the latest resource files from the Extension gem into
+ # the measures' respective resources folders.
+ # measures_dir and core_dir configured in rake_task
+ # Returns true if the command completes successfully, false otherwise.
+ #
+ # @return [Boolean]
+ def copy_core_files(measures_dir, core_dir)
+ puts 'Copying measure resources'
+ if measures_dir.nil? || measures_dir.empty?
+ puts 'Measures dir is nil or empty'
+ return true
+ end
+
+ result = false
+ puts 'Copying updated resource files from extension core directory to individual measures.'
+ puts 'Only files that have actually been changed will be listed.'
+
+ # get all resource files in the core dir
+ resource_files = Dir.glob(File.join(core_dir, '/*.*'))
+
+ # this is to accommodate a single measures dir (like most gems)
+ # or a repo with multiple directories fo measures (like OpenStudio-measures)
+ measures = Dir.glob(File.join(measures_dir, '**/resources/*.rb'))
+ if measures.empty?
+ # also try nested 2-deep to support openstudio-measures
+ measures = Dir.glob(File.join(measures_dir, '**/**/resources/*.rb'))
+ end
+
+ # Note: some older measures like AEDG use 'OsLib_SomeName' instead of 'os_lib_some_name'
+ # this script isn't replacing those copies
+
+ # loop through resource files
+ resource_files.each do |resource_file|
+ # loop through measure dirs looking for matching file
+ measures.each do |measure|
+ next unless File.basename(measure) == File.basename(resource_file)
+ next if FileUtils.identical?(resource_file, File.path(measure))
+
+ puts "Replacing #{measure} with #{resource_file}."
+ FileUtils.cp(resource_file, File.path(measure))
+ end
+ end
+ result = true
+
+ return result
+ end
+
+ # Update measures by adding license file
+ # measures_dir and doc_templates_dir configured in rake_task
+ # Returns true if the command completes successfully, false otherwise.
+ ##
+ # @return [Boolean]
+ def add_measure_license(measures_dir, doc_templates_dir)
+ puts 'Adding measure licenses'
+ if measures_dir.nil? || measures_dir.empty?
+ puts 'Measures dir is nil or empty'
+ return true
+ elsif doc_templates_dir.nil? || doc_templates_dir.empty?
+ puts 'Doc templates dir is nil or empty'
+ return false
+ end
+
+ result = false
+ license_file = File.join(doc_templates_dir, 'LICENSE.md')
+ puts "License file path: #{license_file}"
+
+ raise "License file not found '#{license_file}'" if !File.exist?(license_file)
+
+ measures = Dir["#{measures_dir}/**/measure.rb"]
+ if measures.empty?
+ # also try nested 2-deep to support openstudio-measures
+ measures = Dir["#{measures_dir}/**/**/measure.rb"]
+ end
+ measures.each do |measure|
+ FileUtils.cp(license_file, "#{File.dirname(measure)}/LICENSE.md")
+ end
+ result = true
+ return result
+ end
+
+ # Update measures by adding license file
+ # measures_dir and doc_templates_dir configured in rake_task
+ # Returns true if the command completes successfully, false otherwise.
+ ##
+ # @return [Boolean]
+ def add_measure_readme(measures_dir, doc_templates_dir)
+ puts 'Adding measure readmes'
+ if measures_dir.nil? || measures_dir.empty?
+ puts 'Measures dir is nil or empty'
+ return true
+ elsif doc_templates_dir.nil? || doc_templates_dir.empty?
+ puts 'Measures files dir is nil or empty'
+ return false
+ end
+
+ result = false
+ readme_file = File.join(doc_templates_dir, 'README.md.erb')
+ puts "Readme file path: #{readme_file}"
+
+ raise "Readme file not found '#{readme_file}'" if !File.exist?(readme_file)
+
+ measures = Dir["#{measures_dir}/**/measure.rb"]
+ if measures.empty?
+ # also try nested 2-deep to support openstudio-measures
+ measures = Dir["#{measures_dir}/**/**/measure.rb"]
+ end
+ measures.each do |measure|
+ next if File.exist?("#{File.dirname(measure)}/README.md.erb")
+ next if File.exist?("#{File.dirname(measure)}/README.md")
+
+ puts "adding template README to #{measure}"
+ FileUtils.cp(readme_file, "#{File.dirname(measure)}/README.md.erb")
+ end
+ result = true
+ return result
+ end
+
+ def update_copyright(root_dir, doc_templates_dir)
+ if root_dir.nil? || root_dir.empty?
+ puts 'Root dir is nil or empty'
+ return false
+ elsif doc_templates_dir.nil? || doc_templates_dir.empty?
+ puts 'Doc templates dir is nil or empty'
+ return false
+ end
+
+ if File.exist?(File.join(doc_templates_dir, 'LICENSE.md'))
+ if File.exist?(File.join(root_dir, 'LICENSE.md'))
+ puts 'updating LICENSE.md in root dir'
+ FileUtils.cp(File.join(doc_templates_dir, 'LICENSE.md'), File.join(root_dir, 'LICENSE.md'))
+ end
+ end
+
+ ruby_regex = /^\#\s?[\#\*]{12,}.*copyright.*?\#\s?[\#\*]{12,}\s*$/mi
+ erb_regex = /^<%\s*\#\s?[\#\*]{12,}.*copyright.*?\#\s?[\#\*]{12,}\s*%>$/mi
+ js_regex = /^\/\* @preserve.*copyright.*license.{2}\*\//mi
+
+ filename = File.join(doc_templates_dir, 'copyright_ruby.txt')
+ puts "Copyright file path: #{filename}"
+ raise "Copyright file not found '#{filename}'" if !File.exist?(filename)
+
+ file = File.open(filename, 'r')
+ ruby_header_text = file.read
+ file.close
+ ruby_header_text.strip!
+ ruby_header_text += "\n"
+
+ filename = File.join(doc_templates_dir, 'copyright_erb.txt')
+ puts "Copyright file path: #{filename}"
+ raise "Copyright file not found '#{filename}'" if !File.exist?(filename)
+
+ file = File.open(filename, 'r')
+ erb_header_text = file.read
+ file.close
+ erb_header_text.strip!
+ erb_header_text += "\n"
+
+ filename = File.join(doc_templates_dir, 'copyright_js.txt')
+ puts "Copyright file path: #{filename}"
+ raise "Copyright file not found '#{filename}'" if !File.exist?(filename)
+
+ file = File.open(filename, 'r')
+ js_header_text = file.read
+ file.close
+ js_header_text.strip!
+ js_header_text += "\n"
+
+ raise 'bad copyright_ruby.txt' if ruby_header_text !~ ruby_regex
+ raise 'bad copyright_erb.txt' if erb_header_text !~ erb_regex
+ raise 'bad copyright_js.txt' if js_header_text !~ js_regex
+
+ # look for .rb, .html.erb, and .js.erb
+ paths = [
+ { glob: "#{root_dir}/**/*.rb", license: ruby_header_text, regex: ruby_regex },
+ { glob: "#{root_dir}/**/*.html.erb", license: erb_header_text, regex: erb_regex },
+ { glob: "#{root_dir}/**/*.js.erb", license: js_header_text, regex: js_regex }
+ ]
+
+ puts "Encoding.default_external = #{Encoding.default_external}"
+ puts "Encoding.default_internal = #{Encoding.default_internal}"
+
+ paths.each do |path|
+ Dir[path[:glob]].each do |file|
+ puts "Updating license in file #{file}"
+ f = File.read(file)
+ if f =~ path[:regex]
+ puts ' License found -- updating'
+ File.open(file, 'w') { |write| write << f.gsub(path[:regex], path[:license]) }
+ elsif f =~ /\(C\)/i || f =~ /\(Copyright\)/i
+ puts ' File already has copyright -- skipping'
+ else
+ puts ' No license found -- adding'
+ if f =~ /#!/
+ puts ' CANNOT add license to file automatically, add it manually and it will update automatically in the future'
+ next
+ end
+ File.open(file, 'w') { |write| write << f.insert(0, path[:license] + "\n") }
+ end
+ end
+ end
+ end
+
+ ##
+ # Run the OpenStudio CLI on an OSW. The OSW is configured to include measure and file locations for all loaded OpenStudio Extensions.
+ ##
+ # @param [String, Hash] in_osw If string this is the path to an OSW file on disk, if Hash it is loaded JSON with symbolized keys
+ # @param [String] run_dir Directory to run the OSW in, will be created if does not exist
+ ##
+ # @return [Boolean] True if command succeeded, false otherwise # DLM: should this return path to out.osw instead?
+ def run_osw(in_osw, run_dir)
+ run_dir = File.absolute_path(run_dir)
+
+ if in_osw.is_a?(String)
+ in_osw_path = in_osw
+ raise "'#{in_osw_path}' does not exist" if !File.exist?(in_osw_path)
+
+ in_osw = {}
+ File.open(in_osw_path, 'r') do |file|
+ in_osw = JSON.parse(file.read, symbolize_names: true)
+ end
+ end
+
+ osw = OpenStudio::Extension.configure_osw(in_osw)
+ osw[:run_directory] = run_dir
+
+ FileUtils.mkdir_p(run_dir)
+
+ run_osw_path = File.join(run_dir, 'in.osw')
+ File.open(run_osw_path, 'w') do |file|
+ file.puts JSON.pretty_generate(osw)
+ end
+
+ if @options[:run_simulations]
+ cli = OpenStudio.getOpenStudioCLI
+ out_log = run_osw_path + '.log'
+ # if Gem.win_platform?
+ # # out_log = "nul"
+ # else
+ # # out_log = "/dev/null"
+ # end
+
+ the_call = ''
+ verbose_string = ''
+ if @options[:verbose]
+ verbose_string = ' --verbose'
+ end
+ if @gemfile_path
+ if @bundle_without_string.empty?
+ the_call = "#{cli}#{verbose_string} --bundle '#{@gemfile_path}' --bundle_path '#{@bundle_install_path}' run -w '#{run_osw_path}' 2>&1 > \"#{out_log}\""
+ else
+ the_call = "#{cli}#{verbose_string} --bundle '#{@gemfile_path}' --bundle_path '#{@bundle_install_path}' --bundle_without '#{@bundle_without_string}' run -w '#{run_osw_path}' 2>&1 > \"#{out_log}\""
+ end
+ else
+ the_call = "#{cli}#{verbose_string} run -w '#{run_osw_path}' 2>&1 > \"#{out_log}\""
+ end
+
+ puts 'SYSTEM CALL:'
+ puts the_call
+ STDOUT.flush
+ result = run_command(the_call, get_clean_env)
+ puts "DONE, result = #{result}"
+ STDOUT.flush
+ else
+ puts 'simulations are not performed, since to the @options[:run_simulations] is set to false'
+ end
+
+ # DLM: this does not always return false for failed CLI runs, consider checking for failed.job file as backup test
+
+ return result
+ end
+
+ # run osws, return any failure messages
+ def run_osws(osw_files, num_parallel = @options[:num_parallel], max_to_run = @options[:max_datapoints])
+ failures = []
+ osw_files = osw_files.slice(0, [osw_files.size, max_to_run].min)
+
+ Parallel.each(osw_files, in_threads: num_parallel) do |osw|
+ result = run_osw(osw, File.dirname(osw))
+ if !result
+ failures << "Failed to run OSW '#{osw}'"
+ end
+ end
+
+ return failures
+ end
+ end
+ end
+end