class ScenarioRunner
TIMEOUT_SECONDS = 5
Result = Struct.new(:success, :output, :error, :duration, keyword_init: true) do
def success?
success
end
end
def self.run(source_file:, entry_class:, entry_method:, inputs: {})
new(source_file:, entry_class:, entry_method:, inputs:).call
end
def initialize(source_file:, entry_class:, entry_method:, inputs: {})
@source_file = source_file
@entry_class = entry_class
@entry_method = entry_method
@inputs = inputs
end
def call
sandbox = Module.new
source = File.read(@source_file)
sandbox.module_eval(source, @source_file)
klass = sandbox.const_get(@entry_class)
method = @entry_method.to_sym
start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
output = Timeout.timeout(TIMEOUT_SECONDS) do
if @inputs.any?
klass.public_send(method, **@inputs)
else
klass.public_send(method)
end
end
duration = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start
Result.new(success: true, output: output.to_s, duration: duration.round(4))
rescue Timeout::Error
Result.new(success: false, error: "Execution timed out after #{TIMEOUT_SECONDS} seconds")
rescue => e
Result.new(success: false, error: "#{e.class}: #{e.message}")
end
end