Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

(SDK-312) Add option --parallel to pdk test unit #154

Merged
merged 1 commit into from
Aug 2, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions lib/pdk/cli/exec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,7 @@ def execute!
stdout: @stdout.read,
stderr: @stderr.read,
exit_code: @process.exit_code,
duration: @duration,
}

return process_data
Expand All @@ -183,6 +184,7 @@ def execute!
def run_process!
command_string = argv.join(' ')
PDK.logger.debug(_("Executing '%{command}'") % { command: command_string })
start_time = Time.now
begin
@process.start
rescue ChildProcess::LaunchError => e
Expand All @@ -199,6 +201,7 @@ def run_process!
# Wait indfinitely if no timeout set.
@process.wait
end
@duration = Time.now - start_time
end
end

Expand Down
1 change: 1 addition & 0 deletions lib/pdk/cli/test/unit.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ module PDK::CLI
summary _('Run unit tests.')

flag nil, :list, _('list all available unit tests and their descriptions')
flag nil, :parallel, _('run unit tests in parallel'), hidden: true

option nil, :tests, _('a comma-separated list of tests to run'), argument: :required, default: '' do |values|
PDK::CLI::Util::OptionValidator.comma_separated_list?(values)
Expand Down
60 changes: 53 additions & 7 deletions lib/pdk/tests/unit.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@
module PDK
module Test
class Unit
def self.cmd(_tests)
def self.cmd(_tests, opts = {})
# TODO: test selection
[File.join(PDK::Util.module_root, 'bin', 'rake'), 'spec']
rake_task = opts.key?(:parallel) ? 'parallel_spec' : 'spec'
[File.join(PDK::Util.module_root, 'bin', 'rake'), rake_task]
end

def self.invoke(report, options = {})
Expand All @@ -17,12 +18,13 @@ def self.invoke(report, options = {})

tests = options.fetch(:tests)

cmd_argv = cmd(tests)
cmd_argv = cmd(tests, options)
cmd_argv.unshift('ruby') if Gem.win_platform?

command = PDK::CLI::Exec::Command.new(*cmd_argv).tap do |c|
c.context = :module
c.add_spinner('Running unit tests')
spinner_msg = options.key?(:parallel) ? _('Running unit tests in parallel') : _('Running unit tests')
c.add_spinner(spinner_msg)
c.environment['CI_SPEC_OPTIONS'] = '--format j'
end

Expand All @@ -33,17 +35,24 @@ def self.invoke(report, options = {})
# TODO: cleanup rspec and/or beaker output
# Iterate through possible JSON documents until we find one that is valid.
json_result = nil
json_result = [] if options.key?(:parallel)

result[:stdout].scan(%r{\{(?:[^{}]|(?:\g<0>))*\}}x) do |str|
begin
json_result = JSON.parse(str)
break
if options.key?(:parallel)
json_result.push(JSON.parse(str))
else
json_result = JSON.parse(str)
break
end
rescue JSON::ParserError
next
end
end

raise PDK::CLI::FatalError, _('Unit test output did not contain a valid JSON result: %{output}') % { output: result[:stdout] } unless json_result
raise PDK::CLI::FatalError, _('Unit test output did not contain a valid JSON result: %{output}') % { output: result[:stdout] } if json_result.nil? || json_result.empty?

json_result = merge_json_results(json_result, result[:duration]) if options.key?(:parallel)

parse_output(report, json_result)

Expand Down Expand Up @@ -99,6 +108,43 @@ def self.parse_output(report, json_data)
}
end

def self.merge_json_results(json_data, duration)
merged_json_result = {}

# Merge messages
message_set = Set.new
json_data.each do |json|
next unless json['messages']
message_set |= json['messages']
end
merged_json_result['messages'] = message_set.to_a

# Merge examples
all_examples = []
json_data.each do |json|
next unless json['examples']
all_examples.concat json['examples']
end
merged_json_result['examples'] = all_examples

# Merge summaries
summary_hash = {
'duration' => duration,
'example_count' => 0,
'failure_count' => 0,
'pending_count' => 0,
}
json_data.each do |json|
next unless json['summary']
summary_hash['example_count'] += json['summary']['example_count']
summary_hash['failure_count'] += json['summary']['failure_count']
summary_hash['pending_count'] += json['summary']['pending_count']
end
merged_json_result['summary'] = summary_hash

merged_json_result
end

# @return array of { :id, :full_description }
def self.list
PDK::Util::Bundler.ensure_bundle!
Expand Down
182 changes: 182 additions & 0 deletions spec/unit/pdk/test/unit_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,186 @@
it 'has an invoke method' do
expect(described_class.methods(false)).to include(:invoke)
end

describe '.merge_json_results' do
let(:duration) { 55 }
let(:results) do
[{ 'messages' => ['message 1', 'message 2'],
'examples' => %w[example example example],
'summary' => {
'example_count' => 40,
'failure_count' => 7,
'pending_count' => 12,
'duration' => 30,
} },
{
'messages' => ['message 2', 'message 3'],
'examples' => %w[example example example],
'summary' => {
'example_count' => 30,
'failure_count' => 4,
'pending_count' => 6,
'duration' => 40,
},
}]
end

it 'successfully combines information' do
json_result = described_class.merge_json_results(results, duration)

expect(json_result['messages'].count).to eq(3)
expect(json_result['examples'].count).to eq(6)
expect(json_result['summary']['example_count']).to eq(70)
expect(json_result['summary']['failure_count']).to eq(11)
expect(json_result['summary']['pending_count']).to eq(18)
end

it 'assigns given duration to the result' do
json_result = described_class.merge_json_results(results, duration)

expect(json_result['summary']['duration']).to eq(duration)
end
end

describe '.cmd' do
before(:each) do
allow(PDK::Util).to receive(:module_root).and_return('/path/to/module')
end

context 'when run with parallel option' do
it 'uses the parallel_spec rake task' do
cmd = described_class.cmd(nil, parallel: true)

expect(cmd.join('/')).to match(%r{bin/rake/parallel_spec})
end
end

context 'when run without the parallel option' do
it 'uses the spec rake task' do
cmd = described_class.cmd(nil)

expect(cmd.join('/')).to match(%r{bin/rake/spec})
end
end
end

describe '.parse_output' do
let(:report) { PDK::Report.new }

before(:each) do
allow(report).to receive(:add_event).with(%r{message \d})
end

context 'with messages' do
let(:json) { { 'messages' => ['message 1', 'message 2'] } }

it 'prints the messages to stderr' do
expect($stderr).to receive(:puts).twice
described_class.parse_output(report, json)
end
end

context 'with summary' do
let(:json) do
{ 'summary' => {
'example_count' => 30,
'duration' => 32,
'failure_count' => 2,
'pending_count' => 6,
} }
end

it 'prints the summary to stderr' do
expect($stderr).to receive(:puts).once.with(%r{Evaluated 30 tests in 32 seconds})

described_class.parse_output(report, json)
end
end
end

# Allow any_instance stubbing of Commands
# rubocop:disable RSpec/AnyInstance
describe '.list' do
before(:each) do
allow(PDK::Util::Bundler).to receive(:ensure_bundle!)
allow(PDK::Util::Bundler).to receive(:ensure_binstubs!)
allow(PDK::Util).to receive(:module_root).and_return('/path/to/module')
allow_any_instance_of(PDK::CLI::Exec::Command).to receive(:execute!).and_return(stdout: rspec_json_output)
end

context 'with examples' do
let(:rspec_json_output) do
'{
"examples": [
{ "id": "./path/to/test[1:1:1]",
"full_description": "a bunch of useful descriptive words",
"description": "descriptive words" }
]
}'
end

it 'returns the id and full_description from the rspec output' do
expected_result = [
{
id: './path/to/test[1:1:1]',
full_description: 'a bunch of useful descriptive words',
},
]

expect(described_class.list).to eq(expected_result)
end
end

context 'without examples' do
let(:rspec_json_output) do
'{
"messages": [
"No examples found."
],
"examples": []
}'
end

it 'returns an empty array' do
expect(described_class.list).to eq([])
end
end
end

describe '.invoke' do
let(:report) { PDK::Report.new }
let(:rspec_json_output) do
'{
"examples":
[
{
"id": "./spec/fixtures/modules/testmod/spec/classes/testmod_spec.rb[1:3:1]",
"status": "passed",
"pending_message": null
}
],
"summary": {
"duration": 0.295112,
"example_count": 1,
"failure_count": 0,
"pending_count": 0
}
}'
end

before(:each) do
allow(PDK::Util::Bundler).to receive(:ensure_bundle!)
allow(PDK::Util::Bundler).to receive(:ensure_binstubs!)
allow(PDK::Util).to receive(:module_root).and_return('/path/to/module')
allow_any_instance_of(PDK::CLI::Exec::Command).to receive(:execute!).and_return(stdout: rspec_json_output, exit_code: -1)
allow(described_class).to receive(:parse_output)
end

it 'executes and parses output' do
expect(described_class).to receive(:parse_output).once
exit_code = described_class.invoke(report, tests: 'a_test_spec.rb')
expect(exit_code).to eq(-1)
end
end
# rubocop:enable RSpec/AnyInstance
end