diff --git a/README.md b/README.md index b3db6a775..f315ba19b 100644 --- a/README.md +++ b/README.md @@ -67,6 +67,16 @@ pdk new defined_type defined_type_name PDK creates the new defined\_type manifest and a test file (as `defined_type_name_spec.rb`) in your module's `/spec/defines` directory. +### Generate a task + +To generate a task in your module, use the `pdk new task` command, specifying the name of your new task. + +1. From the command line, in your module's directory, run: +``` +pdk new task task_name +``` + +PDK creates the new task file and metadata. ### Validating a module diff --git a/lib/pdk/validators/base_validator.rb b/lib/pdk/validators/base_validator.rb index 7b893409f..f951d8e1e 100644 --- a/lib/pdk/validators/base_validator.rb +++ b/lib/pdk/validators/base_validator.rb @@ -42,13 +42,14 @@ def self.parse_targets(options) matched = targets.map { |target| if respond_to?(:pattern) if File.directory?(target) - target_list = Array[pattern].flatten.map { |p| Dir.glob(File.join(target, p)) } + pattern_glob = Array(pattern).map { |p| Dir.glob(File.join(PDK::Util.module_root, p)) } + target_list = pattern_glob.flatten.select { |file| File.fnmatch(File.join(File.expand_path(target), '*'), file) } skipped << target if target_list.flatten.empty? target_list elsif File.file?(target) - if target.eql? pattern + if Array(pattern).include? target target - elsif Array[pattern].flatten.map { |p| File.fnmatch(p, File.expand_path(target)) }.include? true + elsif Array(pattern).any? { |p| File.fnmatch(File.expand_path(p), File.expand_path(target)) } target else skipped << target diff --git a/lib/pdk/validators/metadata/metadata_json_lint.rb b/lib/pdk/validators/metadata/metadata_json_lint.rb index 234245be0..079e3b3ee 100644 --- a/lib/pdk/validators/metadata/metadata_json_lint.rb +++ b/lib/pdk/validators/metadata/metadata_json_lint.rb @@ -20,7 +20,7 @@ def self.cmd end def self.spinner_text(targets = []) - _('Checking metadata style (%{targets}).') % { + _('Checking module metadata style (%{targets}).') % { targets: PDK::Util.targets_relative_to_pwd(targets).join(' '), } end diff --git a/lib/pdk/validators/metadata/metadata_syntax.rb b/lib/pdk/validators/metadata/metadata_syntax.rb index 4d1902892..4cfe7aa09 100644 --- a/lib/pdk/validators/metadata/metadata_syntax.rb +++ b/lib/pdk/validators/metadata/metadata_syntax.rb @@ -11,12 +11,12 @@ def self.name end def self.pattern - 'metadata.json' + ['metadata.json', 'tasks/*.json'] end - def self.spinner_text(targets = []) + def self.spinner_text(_targets = []) _('Checking metadata syntax (%{targets}).') % { - targets: PDK::Util.targets_relative_to_pwd(targets).join(' '), + targets: pattern.join(' '), } end diff --git a/lib/pdk/validators/metadata/task_metadata_lint.rb b/lib/pdk/validators/metadata/task_metadata_lint.rb new file mode 100644 index 000000000..2aecc1ae8 --- /dev/null +++ b/lib/pdk/validators/metadata/task_metadata_lint.rb @@ -0,0 +1,149 @@ +require 'pdk' +require 'pdk/cli/exec' +require 'pdk/validators/base_validator' +require 'pdk/util' +require 'pathname' +require 'json-schema' + +module PDK + module Validate + class TaskMetadataLint < BaseValidator + FORGE_SCHEMA_URL = 'https://forgeapi.puppet.com/schemas/task.json'.freeze + + def self.name + 'task-metadata-lint' + end + + def self.pattern + 'tasks/*.json' + end + + def self.spinner_text(_targets = []) + _('Checking task metadata style (%{targets}).') % { + targets: pattern, + } + end + + def self.create_spinner(targets = [], options = {}) + return if PDK.logger.debug? + options = options.merge(PDK::CLI::Util.spinner_opts_for_platform) + + exec_group = options[:exec_group] + @spinner = if exec_group + exec_group.add_spinner(spinner_text(targets), options) + else + TTY::Spinner.new("[:spinner] #{spinner_text(targets)}", options) + end + @spinner.auto_spin + end + + def self.stop_spinner(exit_code) + if exit_code.zero? && @spinner + @spinner.success + elsif @spinner + @spinner.error + end + end + + def self.vendored_task_schema_path + @vendored_task_schema_path ||= File.join(PDK::Util.package_cachedir, 'task.json') + end + + def self.schema_file + schema = if PDK::Util.package_install? && File.exist?(vendored_task_schema_path) + File.read(vendored_task_schema_path) + else + download_schema_from_forge + end + + JSON.parse(schema) + rescue JSON::ParserError + raise PDK::CLI::FatalError, _('Failed to parse Task Metadata Schema file.') + end + + def self.download_schema_from_forge + PDK.logger.debug(_('Task Metadata Schema was not found in the cache. Now downloading from the forge.')) + require 'net/https' + require 'openssl' + + uri = URI.parse(FORGE_SCHEMA_URL) + http = Net::HTTP.new(uri.host, uri.port) + http.use_ssl = true + http.verify_mode = OpenSSL::SSL::VERIFY_NONE if Gem.win_platform? + request = Net::HTTP::Get.new(uri.request_uri) + response = http.request(request) + + raise PDK::CLI::FatalError, _('Unable to download Task Metadata Schema file. %{code}: %{message}.') % { code: response.code, message: response.message } unless response.code == '200' + + response.body + rescue StandardError => e + raise PDK::CLI::FatalError, _('Unable to download Task Metadata Schema file. Please check internet connectivity and retry this action. %{error}') % { error: e } + end + + def self.invoke(report, options = {}) + targets, skipped, invalid = parse_targets(options) + + process_skipped(report, skipped) + process_invalid(report, invalid) + + return 0 if targets.empty? + + return_val = 0 + create_spinner(targets, options) + + targets.each do |target| + unless File.readable?(target) + report.add_event( + file: target, + source: name, + state: :failure, + severity: 'error', + message: _('Could not be read.'), + ) + return_val = 1 + next + end + + begin + # Need to set the JSON Parser and State Generator to the Native one to be + # compatible with the multi_json adapter. + JSON.parser = JSON::Ext::Parser if defined?(JSON::Ext::Parser) + JSON.generator = JSON::Ext::Generator + + begin + errors = JSON::Validator.fully_validate(schema_file, File.read(target)) || [] + rescue JSON::Schema::SchemaError => e + raise PDK::CLI::FatalError, _('Unable to validate Task Metadata. %{error}.') % { error: e.message } + end + + if errors.empty? + report.add_event( + file: target, + source: name, + state: :passed, + severity: 'ok', + ) + else + errors.each do |error| + # strip off the trailing parts that aren't relevant + error = error.split('in schema').first if error.include? 'in schema' + + report.add_event( + file: target, + source: name, + state: :failure, + severity: 'error', + message: error, + ) + end + return_val = 1 + end + end + end + + stop_spinner(return_val) + return_val + end + end + end +end diff --git a/lib/pdk/validators/metadata_validator.rb b/lib/pdk/validators/metadata_validator.rb index 7496050a4..0bcc1af5a 100644 --- a/lib/pdk/validators/metadata_validator.rb +++ b/lib/pdk/validators/metadata_validator.rb @@ -3,6 +3,7 @@ require 'pdk/validators/base_validator' require 'pdk/validators/metadata/metadata_json_lint' require 'pdk/validators/metadata/metadata_syntax' +require 'pdk/validators/metadata/task_metadata_lint' module PDK module Validate @@ -12,7 +13,7 @@ def self.name end def self.metadata_validators - [MetadataSyntax, MetadataJSONLint] + [MetadataSyntax, MetadataJSONLint, TaskMetadataLint] end def self.invoke(report, options = {}) diff --git a/pdk.gemspec b/pdk.gemspec index 9f6e29959..90b20e48e 100644 --- a/pdk.gemspec +++ b/pdk.gemspec @@ -28,6 +28,7 @@ Gem::Specification.new do |spec| spec.add_runtime_dependency 'tty-spinner', '0.5.0' spec.add_runtime_dependency 'tty-prompt', '0.13.1' spec.add_runtime_dependency 'json_pure', '~> 2.1.0' + spec.add_runtime_dependency 'json-schema', '2.8.0' spec.add_runtime_dependency 'tty-which', '0.3.0' # Used in the pdk-module-template diff --git a/spec/acceptance/validate_all_spec.rb b/spec/acceptance/validate_all_spec.rb index 6f263d268..9d73c8dc0 100644 --- a/spec/acceptance/validate_all_spec.rb +++ b/spec/acceptance/validate_all_spec.rb @@ -20,8 +20,8 @@ class validate_all { } describe command('pdk validate') do its(:exit_status) { is_expected.to eq(0) } its(:stderr) { is_expected.to match(%r{Running all available validators}i) } - its(:stderr) { is_expected.to match(%r{Checking metadata syntax \(metadata\.json\)}i) } - its(:stderr) { is_expected.to match(%r{Checking metadata style \(metadata\.json\)}i) } + its(:stderr) { is_expected.to match(%r{Checking metadata syntax}i) } + its(:stderr) { is_expected.to match(%r{Checking module metadata style}i) } its(:stderr) { is_expected.to match(%r{Checking Puppet manifest syntax}i) } its(:stderr) { is_expected.to match(%r{Checking Puppet manifest style}i) } its(:stderr) { is_expected.to match(%r{Checking Ruby code style}i) } @@ -47,8 +47,8 @@ class validate_all { describe command('pdk validate') do its(:exit_status) { is_expected.not_to eq(0) } its(:stderr) { is_expected.to match(%r{Running all available validators}i) } - its(:stderr) { is_expected.to match(%r{Checking metadata syntax \(metadata\.json\)}i) } - its(:stderr) { is_expected.to match(%r{Checking metadata style \(metadata\.json\)}i) } + its(:stderr) { is_expected.to match(%r{Checking metadata syntax}i) } + its(:stderr) { is_expected.to match(%r{Checking module metadata style}i) } its(:stderr) { is_expected.to match(%r{Checking Puppet manifest syntax}i) } its(:stderr) { is_expected.to match(%r{Checking Ruby code style}i) } end @@ -57,8 +57,8 @@ class validate_all { its(:exit_status) { is_expected.not_to eq(0) } its(:stderr) { is_expected.to match(%r{Running all available validators}i) } its(:stderr) { is_expected.to match(%r{Validating module using \d+ threads}i) } - its(:stderr) { is_expected.to match(%r{Checking metadata syntax \(metadata\.json\)}i) } - its(:stderr) { is_expected.to match(%r{Checking metadata style \(metadata\.json\)}i) } + its(:stderr) { is_expected.to match(%r{Checking metadata syntax}i) } + its(:stderr) { is_expected.to match(%r{Checking module metadata style}i) } its(:stderr) { is_expected.to match(%r{Checking Puppet manifest syntax}i) } its(:stdout) { is_expected.to match(%r{Error:.*This Name has no effect}i) } its(:stdout) { is_expected.to match(%r{Error:.*This Type-Name has no effect}i) } @@ -69,8 +69,8 @@ class validate_all { describe command('pdk validate --format junit') do its(:exit_status) { is_expected.not_to eq(0) } its(:stderr) { is_expected.to match(%r{Running all available validators}i) } - its(:stderr) { is_expected.to match(%r{Checking metadata syntax \(metadata\.json\)}i) } - its(:stderr) { is_expected.to match(%r{Checking metadata style \(metadata\.json\)}i) } + its(:stderr) { is_expected.to match(%r{Checking metadata syntax}i) } + its(:stderr) { is_expected.to match(%r{Checking module metadata style}i) } its(:stderr) { is_expected.to match(%r{Checking Puppet manifest syntax}i) } its(:stderr) { is_expected.not_to match(%r{Checking Puppet manifest style}i) } its(:stderr) { is_expected.to match(%r{Checking Ruby code style}i) } @@ -124,11 +124,10 @@ class pdk_in_gemfile { } describe command('pdk validate') do its(:exit_status) { is_expected.to eq(0) } its(:stderr) { is_expected.to match(%r{Running all available validators}i) } - its(:stderr) { is_expected.to match(%r{Checking metadata syntax \(metadata\.json\)}i) } - its(:stderr) { is_expected.to match(%r{Checking metadata style \(metadata\.json\)}i) } + its(:stderr) { is_expected.to match(%r{Checking metadata syntax}i) } + its(:stderr) { is_expected.to match(%r{Checking module metadata style}i) } its(:stderr) { is_expected.to match(%r{Checking Puppet manifest syntax}i) } its(:stderr) { is_expected.to match(%r{Checking Ruby code style}i) } - its(:stdout) { is_expected.to eq('') } end end end diff --git a/spec/acceptance/validate_metadata_spec.rb b/spec/acceptance/validate_metadata_spec.rb index 443bb0f89..219a8b702 100644 --- a/spec/acceptance/validate_metadata_spec.rb +++ b/spec/acceptance/validate_metadata_spec.rb @@ -1,7 +1,10 @@ require 'spec_helper_acceptance' describe 'Running metadata validation' do - let(:spinner_text) { %r{checking metadata}i } + let(:junit_xsd) { File.join(RSpec.configuration.fixtures_path, 'JUnit.xsd') } + let(:metadata_syntax_spinner) { %r{checking metadata syntax}i } + let(:module_style_spinner) { %r{checking module metadata style}i } + let(:task_style_spinner) { %r{checking task metadata style}i } context 'with a metadata violation' do include_context 'in a new module', 'metadata_violation_module' @@ -19,12 +22,12 @@ describe command('pdk validate metadata') do its(:exit_status) { is_expected.not_to eq(0) } its(:stdout) { is_expected.to match(%r{^warning:.*metadata\.json:.+open ended dependency}) } - its(:stderr) { is_expected.to match(spinner_text) } + its(:stderr) { is_expected.to match(metadata_syntax_spinner) } end describe command('pdk validate metadata --format junit') do its(:exit_status) { is_expected.not_to eq(0) } - its(:stderr) { is_expected.to match(spinner_text) } + its(:stderr) { is_expected.to match(metadata_syntax_spinner) } it_behaves_like :it_generates_valid_junit_xml its(:stdout) do @@ -59,7 +62,7 @@ describe command('pdk validate metadata --format junit') do its(:exit_status) { is_expected.to eq(0) } - its(:stderr) { is_expected.to match(spinner_text) } + its(:stderr) { is_expected.to match(metadata_syntax_spinner) } its(:stdout) do is_expected.to have_xpath('/testsuites/testsuite[@name="metadata-json-lint"]/testcase').with_attributes( @@ -76,7 +79,7 @@ describe command('pdk validate metadata --format junit broken.json') do its(:exit_status) { is_expected.to eq(0) } - its(:stderr) { is_expected.not_to match(spinner_text) } + its(:stderr) { is_expected.not_to match(metadata_syntax_spinner) } its(:stdout) do is_expected.to have_xpath('/testsuites/testsuite[@name="metadata-json-lint"]').with_attributes( @@ -104,7 +107,7 @@ describe command('pdk validate metadata --format junit broken.json') do its(:exit_status) { is_expected.to eq(0) } - its(:stderr) { is_expected.not_to match(spinner_text) } + its(:stderr) { is_expected.not_to match(metadata_syntax_spinner) } its(:stdout) do is_expected.to have_junit_testsuite('metadata-json-lint').with_attributes( @@ -121,4 +124,79 @@ end end end + + context 'with valid task' do + include_context 'in a new module', 'foo' + + before(:all) do + File.open(File.join('tasks', 'valid.json'), 'w') do |f| + f.puts <<-EOS +{ + "puppet_task_version": 1, + "supports_noop": true, + "description": "A short description of this task" +} + EOS + end + end + + describe command('pdk validate metadata') do + its(:exit_status) { is_expected.to eq(0) } + its(:stderr) { is_expected.to match(metadata_syntax_spinner) } + its(:stderr) { is_expected.to match(module_style_spinner) } + its(:stderr) { is_expected.to match(task_style_spinner) } + end + + describe command('pdk validate metadata --format junit') do + its(:exit_status) { is_expected.to eq(0) } + its(:stderr) { is_expected.to match(metadata_syntax_spinner) } + its(:stderr) { is_expected.to match(module_style_spinner) } + its(:stderr) { is_expected.to match(task_style_spinner) } + its(:stdout) { is_expected.to pass_validation(junit_xsd) } + + its(:stdout) { is_expected.to have_junit_testsuite('task-metadata-lint') } + end + end + + context 'with invalid task' do + include_context 'in a new module', 'foo' + + before(:all) do + File.open(File.join('tasks', 'invalid.json'), 'w') do |f| + f.puts <<-EOS +{ + "puppet_task_version": 1, + "supports_noop": "true", + "description": "A short description of this task" +} + EOS + end + end + + describe command('pdk validate metadata') do + its(:exit_status) { is_expected.not_to eq(0) } + its(:stderr) { is_expected.to match(metadata_syntax_spinner) } + its(:stderr) { is_expected.to match(module_style_spinner) } + its(:stderr) { is_expected.to match(task_style_spinner) } + its(:stdout) { is_expected.to match(%r{The property '#/supports_noop' of type string did not match the following type: boolean}i) } + end + + describe command('pdk validate metadata --format junit') do + its(:exit_status) { is_expected.not_to eq(0) } + its(:stderr) { is_expected.to match(metadata_syntax_spinner) } + its(:stderr) { is_expected.to match(module_style_spinner) } + its(:stderr) { is_expected.to match(task_style_spinner) } + its(:stdout) { is_expected.to pass_validation(junit_xsd) } + + its(:stdout) do + is_expected.to have_junit_testcase.in_testsuite('task-metadata-lint').with_attributes( + 'classname' => 'task-metadata-lint', + 'name' => a_string_matching(%r{invalid.json}), + ).that_failed( + 'type' => a_string_matching(%r{error}i), + 'message' => a_string_matching(%r{The property '#/supports_noop' of type string did not match the following type: boolean}i), + ) + end + end + end end diff --git a/spec/support/it_accepts_metadata_json_targets.rb b/spec/support/it_accepts_metadata_json_targets.rb index f02da0e56..977f54353 100644 --- a/spec/support/it_accepts_metadata_json_targets.rb +++ b/spec/support/it_accepts_metadata_json_targets.rb @@ -3,21 +3,28 @@ subject(:parsed_targets) { described_class.parse_targets(targets: targets) } let(:module_metadata_json) { File.join(module_root, 'metadata.json') } - let(:glob_pattern) { File.join(module_root, described_class.pattern) } let(:globbed_files) { [] } + let(:glob_pattern) do + Array(described_class.pattern).flatten.map { |pattern| File.join(module_root, pattern) } + end before(:each) do - allow(Dir).to receive(:glob).with(glob_pattern).and_return(globbed_files) + glob_pattern.each do |pattern| + allow(Dir).to receive(:glob).with(pattern).and_return(globbed_files) + end end context 'when given no targets' do let(:targets) { [] } context 'and the module contains a metadata.json file' do - let(:globbed_files) { [module_metadata_json] } + before(:each) do + allow(Dir).to receive(:glob).with(module_metadata_json).and_return([module_metadata_json]) + allow(File).to receive(:expand_path).with(module_root).and_return(module_root) + end it 'returns the path to metadata.json in the module' do - expect(parsed_targets.first).to eq(globbed_files) + expect(parsed_targets.first).to eq([module_metadata_json]) end end @@ -44,30 +51,5 @@ expect(parsed_targets[2]).to eq([]) end end - - context 'when given a specific target directory' do - let(:targets) { [File.join('path', 'to', 'target', 'directory')] } - let(:glob_pattern) { File.join(targets.first, described_class.pattern) } - - before(:each) do - targets.each do |target| - allow(File).to receive(:directory?).with(target).and_return(true) - end - end - - context 'and the directory contains a metadata.json file' do - let(:globbed_files) { [File.join(targets.first, 'metadata.json')] } - - it 'returns the path to the metadata.json file in the target directory' do - expect(parsed_targets.first).to eq(globbed_files) - end - end - - context 'and the directory does not contain a metadata.json file' do - it 'returns no targets' do - expect(parsed_targets.first).to eq([]) - end - end - end end end diff --git a/spec/support/it_accepts_pp_targets.rb b/spec/support/it_accepts_pp_targets.rb index 461166954..7cad114eb 100644 --- a/spec/support/it_accepts_pp_targets.rb +++ b/spec/support/it_accepts_pp_targets.rb @@ -7,6 +7,7 @@ before(:each) do allow(Dir).to receive(:glob).with(glob_pattern).and_return(globbed_files) + allow(File).to receive(:expand_path).with(module_root).and_return(module_root) end context 'when given no targets' do @@ -33,12 +34,14 @@ end context 'when given specific target files' do - let(:targets) { ['manifest.pp', 'another.pp'] } + let(:targets) { ['manifests/manifest.pp', 'manifests/foo/another.pp'] } before(:each) do + allow(File).to receive(:expand_path).with(described_class.pattern).and_return(File.join(module_root, described_class.pattern)) targets.each do |target| allow(File).to receive(:directory?).with(target).and_return(false) allow(File).to receive(:file?).with(target).and_return(true) + allow(File).to receive(:expand_path).with(target).and_return(File.join(module_root, target)) end end @@ -46,28 +49,5 @@ expect(parsed_targets.first).to eq(targets) end end - - context 'when given a specific target directory' do - let(:targets) { [File.join('path', 'to', 'target', 'directory')] } - let(:glob_pattern) { File.join(targets.first, described_class.pattern) } - - before(:each) do - allow(File).to receive(:directory?).with(targets.first).and_return(true) - end - - context 'and the directory contains .pp files' do - let(:globbed_files) { [File.join(targets.first, 'test.pp')] } - - it 'returns the paths to the .pp files in the directory' do - expect(parsed_targets.first).to eq(globbed_files) - end - end - - context 'and the directory contains no .pp files' do - it 'returns no targets' do - expect(parsed_targets.first).to eq([]) - end - end - end end end diff --git a/spec/unit/pdk/validate/base_validator_spec.rb b/spec/unit/pdk/validate/base_validator_spec.rb index dcc515ed9..51f99d6c3 100644 --- a/spec/unit/pdk/validate/base_validator_spec.rb +++ b/spec/unit/pdk/validate/base_validator_spec.rb @@ -32,6 +32,7 @@ before(:each) do allow(File).to receive(:directory?).and_return(true) allow(Dir).to receive(:glob).with(glob_pattern).and_return(globbed_files) + allow(File).to receive(:expand_path).with(module_root).and_return(module_root) end it 'returns the module root' do @@ -41,23 +42,32 @@ context 'when given specific targets' do let(:targets) { ['target1.pp', 'target2/'] } + let(:glob_pattern) { File.join(module_root, described_class.pattern) } let(:globbed_target2) do [ - File.join('target2', 'target.pp'), + File.join(module_root, 'target2', 'target.pp'), ] end before(:each) do - allow(Dir).to receive(:glob).with(File.join('target2', described_class.pattern)).and_return(globbed_target2) + allow(Dir).to receive(:glob).with(glob_pattern).and_return(globbed_target2) allow(File).to receive(:directory?).with('target1.pp').and_return(false) allow(File).to receive(:directory?).with('target2/').and_return(true) allow(File).to receive(:file?).with('target1.pp').and_return(true) + + targets.map do |t| + allow(File).to receive(:expand_path).with(t).and_return(File.join(module_root, t)) + end + + Array[described_class.pattern].flatten.map do |p| + allow(File).to receive(:expand_path).with(p).and_return(File.join(module_root, p)) + end end it 'returns the targets' do - expect(target_files[0]).to eq(['target1.pp'].concat(globbed_target2)) - expect(target_files[1]).to be_empty + expect(target_files[0]).to eq(globbed_target2) + expect(target_files[1]).to eq(['target1.pp']) expect(target_files[2]).to be_empty end end @@ -70,7 +80,7 @@ end before(:each) do - allow(Dir).to receive(:glob).with(File.join('target3', described_class.pattern)).and_return(globbed_target2) + allow(Dir).to receive(:glob).with(File.join(module_root, described_class.pattern)).and_return(globbed_target2) allow(File).to receive(:directory?).with('target3/').and_return(true) end diff --git a/spec/unit/pdk/validate/metadata_json_lint_spec.rb b/spec/unit/pdk/validate/metadata_json_lint_spec.rb index 3fa52626c..c4f29751f 100644 --- a/spec/unit/pdk/validate/metadata_json_lint_spec.rb +++ b/spec/unit/pdk/validate/metadata_json_lint_spec.rb @@ -22,7 +22,7 @@ let(:targets) { ['foo/metadata.json'] } it 'includes the path to the target in the spinner text' do - expect(spinner_text).to match(%r{checking metadata style \(#{Regexp.escape(targets.first)}\)}i) + expect(spinner_text).to match(%r{checking module metadata style \(#{Regexp.escape(targets.first)}\)}i) end end @@ -41,7 +41,7 @@ end it 'includes the path to the target relative to the PWD in the spinner text' do - expect(spinner_text).to match(%r{checking metadata style \(metadata\.json\)}i) + expect(spinner_text).to match(%r{checking module metadata style \(metadata\.json\)}i) end end end diff --git a/spec/unit/pdk/validate/metadata_validator_spec.rb b/spec/unit/pdk/validate/metadata_validator_spec.rb index f651f1e5d..b6c4a2b8b 100644 --- a/spec/unit/pdk/validate/metadata_validator_spec.rb +++ b/spec/unit/pdk/validate/metadata_validator_spec.rb @@ -3,6 +3,10 @@ describe PDK::Validate::MetadataValidator do let(:report) { PDK::Report.new } + before(:each) do + allow(PDK::Util).to receive(:module_root).and_return('/path/to/module') + end + describe '.invoke' do subject(:return_value) { described_class.invoke(report, {}) } diff --git a/spec/unit/pdk/validate/rubocop_spec.rb b/spec/unit/pdk/validate/rubocop_spec.rb index 68e945008..f6578b644 100644 --- a/spec/unit/pdk/validate/rubocop_spec.rb +++ b/spec/unit/pdk/validate/rubocop_spec.rb @@ -17,14 +17,6 @@ end describe PDK::Validate::Rubocop do - let(:module_root) { File.join('path', 'to', 'test', 'module') } - let(:glob_pattern) { File.join(module_root, described_class.pattern) } - - before(:each) do - allow(PDK::Util).to receive(:module_root).and_return(module_root) - allow(File).to receive(:directory?).with(module_root).and_return(true) - end - it 'defines the base validator attributes' do expect(described_class).to have_attributes( name: 'rubocop', @@ -36,6 +28,17 @@ describe '.parse_targets' do subject(:target_files) { described_class.parse_targets(targets: targets) } + let(:module_root) { File.join('path', 'to', 'test', 'module') } + let(:pattern) { '**/**.rb' } + let(:glob_pattern) { File.join(module_root, described_class.pattern) } + + before(:each) do + allow(described_class).to receive(:pattern).and_return(pattern) + allow(PDK::Util).to receive(:module_root).and_return(module_root) + allow(File).to receive(:directory?).with(module_root).and_return(true) + allow(File).to receive(:expand_path).with(module_root).and_return(module_root) + end + context 'when given no targets' do let(:targets) { [] } @@ -59,19 +62,29 @@ let(:globbed_target2) do [ - File.join('target2', 'target.rb'), + File.join(module_root, 'target2', 'target.rb'), ] end before(:each) do - allow(Dir).to receive(:glob).with(File.join('target2', described_class.pattern)).and_return(globbed_target2) + allow(Dir).to receive(:glob).with(glob_pattern).and_return(globbed_target2) allow(File).to receive(:directory?).with('target1.rb').and_return(false) allow(File).to receive(:directory?).with('target2/').and_return(true) allow(File).to receive(:file?).with('target1.rb').and_return(true) + + targets.map do |t| + allow(File).to receive(:expand_path).with(t).and_return(File.join(module_root, t)) + end + + Array[pattern].flatten.map do |p| + allow(File).to receive(:expand_path).with(p).and_return(File.join(module_root, p)) + end end it 'returns the targets' do - expect(target_files.first).to eq(['target1.rb'].concat(globbed_target2)) + expect(target_files[0]).to eq(globbed_target2) + expect(target_files[1]).to eq(['target1.rb']) + expect(target_files[2]).to be_empty end end end @@ -107,7 +120,7 @@ let(:rubocop_report) { RuboCop::Formatter::JSONFormatter.new(nil) } let(:rubocop_json) { rubocop_report.output_hash.to_json } let(:report) { PDK::Report.new } - let(:test_file) { File.join(module_root, 'lib', 'test.rb') } + let(:test_file) { File.join('lib', 'test.rb') } def mock_offense(severity, message, cop_name, corrected, line, column) OpenStruct.new( @@ -209,8 +222,8 @@ def mock_offense(severity, message, cop_name, corrected, line, column) context 'when the rubocop output has information for multiple files' do let(:test_files) do { - File.join(module_root, 'spec', 'spec_helper.rb') => [], - File.join(module_root, 'lib', 'fail.rb') => [ + File.join('spec', 'spec_helper.rb') => [], + File.join('lib', 'fail.rb') => [ mock_offense('error', 'correctable error', 'Test/Cop', true, 1, 2), mock_offense('warning', 'uncorrectable thing', 'Test/Cop2', false, 3, 4), ], @@ -226,7 +239,7 @@ def mock_offense(severity, message, cop_name, corrected, line, column) it 'adds a passing event to the report for the file with no offenses' do expect(report).to receive(:add_event).with( - file: File.join(module_root, 'spec', 'spec_helper.rb'), + file: File.join('spec', 'spec_helper.rb'), source: described_class.name, state: :passed, severity: :ok, @@ -237,7 +250,7 @@ def mock_offense(severity, message, cop_name, corrected, line, column) it 'adds a corrected failure event to the report for the file with offenses' do expect(report).to receive(:add_event).with( - file: File.join(module_root, 'lib', 'fail.rb'), + file: File.join('lib', 'fail.rb'), source: described_class.name, state: :failure, severity: 'corrected', @@ -252,7 +265,7 @@ def mock_offense(severity, message, cop_name, corrected, line, column) it 'adds a failure event to the report for the file with offenses' do expect(report).to receive(:add_event).with( - file: File.join(module_root, 'lib', 'fail.rb'), + file: File.join('lib', 'fail.rb'), source: described_class.name, state: :failure, severity: 'warning', diff --git a/spec/unit/pdk/validate/task_metadata_lint_spec.rb b/spec/unit/pdk/validate/task_metadata_lint_spec.rb new file mode 100644 index 000000000..9f01cc955 --- /dev/null +++ b/spec/unit/pdk/validate/task_metadata_lint_spec.rb @@ -0,0 +1,173 @@ +require 'spec_helper' + +describe PDK::Validate::TaskMetadataLint do + let(:module_root) { File.join('path', 'to', 'test', 'module') } + let(:schema) do + { + 'title' => 'Puppet Task Metadata', + 'description' => 'The metadata format for Puppet Tasks', + 'type' => 'object', + 'properties' => { + 'description' => { + 'type' => 'string', + }, + 'version' => { + 'type' => 'integer', + }, + }, + } + end + + before(:each) do + allow(PDK::Util).to receive(:module_root).and_return(module_root) + allow(File).to receive(:directory?).with(module_root).and_return(true) + end + + it 'defines the base validator attributes' do + expect(described_class).to have_attributes( + name: 'task-metadata-lint', + ) + end + + describe '.spinner_text' do + subject(:spinner_text) { described_class.spinner_text(targets) } + + context 'when given a relative path to the target' do + let(:targets) { ['tasks/foo.json'] } + + it 'includes the path to the target in the spinner text' do + expect(spinner_text).to match(%r{checking task metadata style}i) + end + end + + context 'when given an absolute path to the target' do + let(:targets) do + if Gem.win_platform? + ['C:/path/to/module/tasks/foo.json'] + else + ['/path/to/module/tasks/foo.json'] + end + end + + before(:each) do + pwd = Gem.win_platform? ? 'C:/path/to/module' : '/path/to/module' + allow(Pathname).to receive(:pwd).and_return(Pathname.new(pwd)) + end + + it 'includes the path to the target relative to the PWD in the spinner text' do + expect(spinner_text).to match(%r{checking task metadata style}i) + end + end + end + + describe '.invoke' do + subject(:return_value) { described_class.invoke(report, targets: targets.map { |r| r[:name] }) } + + let(:report) { PDK::Report.new } + let(:targets) { [] } + + before(:each) do + allow(described_class).to receive(:schema_file).and_return(schema) + targets.each do |target| + allow(File).to receive(:directory?).with(target[:name]).and_return(target.fetch(:directory, false)) + allow(File).to receive(:file?).with(target[:name]).and_return(target.fetch(:file, true)) + allow(File).to receive(:readable?).with(target[:name]).and_return(target.fetch(:readable, true)) + allow(File).to receive(:read).with(target[:name]).and_return(target.fetch(:content, '')) + end + end + + context 'when a target is provided that is an unreadable file' do + let(:targets) do + [ + { name: 'tasks/unreadable.json', readable: false }, + ] + end + + it 'adds a failure event to the report' do + expect(report).to receive(:add_event).with( + file: targets.first[:name], + source: 'task-metadata-lint', + state: :failure, + severity: 'error', + message: 'Could not be read.', + ) + expect(return_value).to eq(1) + end + end + + context 'when a target is provided that contains valid JSON' do + let(:targets) do + [ + { + name: 'tasks/valid.json', + content: '{"description": "wow. so. valid.", "version": 1}', + }, + ] + end + + it 'adds a passing event to the report' do + expect(report).to receive(:add_event).with( + file: targets.first[:name], + source: 'task-metadata-lint', + state: :passed, + severity: 'ok', + ) + expect(return_value).to eq(0) + end + end + + context 'when a target is provided that contains invalid JSON' do + let(:targets) do + [ + { + name: 'tasks/invalid.json', + content: '{"description": "Invalid Metadata", "version": "definitely the wrong type"}', + }, + ] + end + + it 'adds a failure event to the report' do + expect(report).to receive(:add_event).with( + file: targets.first[:name], + source: 'task-metadata-lint', + state: :failure, + severity: 'error', + message: a_string_matching(%r{did not match the following type}i), + ) + expect(return_value).to eq(1) + end + end + + context 'when targets are provided that contain valid and invalid JSON' do + let(:targets) do + [ + { + name: 'tasks/invalid.json', + content: '{"description": "Invalid Metadata", "version": "definitely the wrong type"}', + }, + { + name: 'tasks/valid.json', + content: '{"description": "wow. so. valid.", "version": 1}', + }, + ] + end + + it 'adds events for all valid and invalid targets to the report' do + expect(report).to receive(:add_event).with( + file: 'tasks/invalid.json', + source: 'task-metadata-lint', + state: :failure, + severity: 'error', + message: a_string_matching(%r{did not match the following type}i), + ) + expect(report).to receive(:add_event).with( + file: 'tasks/valid.json', + source: 'task-metadata-lint', + state: :passed, + severity: 'ok', + ) + expect(return_value).to eq(1) + end + end + end +end