Basics of a working reporter plugin system

Signed-off-by: Clinton Wolfe <clintoncwolfe@gmail.com>
This commit is contained in:
Clinton Wolfe 2020-05-01 23:48:05 -04:00
parent 2e887a94af
commit 63d95b6b6b
11 changed files with 172 additions and 7 deletions

View file

@ -328,21 +328,35 @@ module Inspec
def validate_reporters!(reporters) def validate_reporters!(reporters)
return if reporters.nil? return if reporters.nil?
# TODO: move this into a reporter plugin type system # These "reporters" are actually RSpec Formatters.
valid_types = %w{ # json-rspec is our alias for RSpec's json formatter.
automate rspec_built_in_formatters = %w{
cli
documentation documentation
html html
json-rspec
progress
}
# These are true reporters, but have not been migrated to be plugins yet.
unmigrated_reporters = %w{
automate
cli
json json
json-automate json-automate
json-min json-min
json-rspec
junit junit
progress
yaml yaml
} }
# Additional reporters may be loaded via plugins. They will have already been detected at
# this point (see v2_loader.load_all in cli.rb) but they may not (and need not) be
# activated at this point. We only care about their existance their name, for validations sake.
plugin_reporters = Inspec::Plugin::V2::Registry.instance\
.find_activators(plugin_type: :reporter)\
.map(&:activator_name).map(&:to_s)
valid_types = rspec_built_in_formatters + unmigrated_reporters + plugin_reporters
reporters.each do |reporter_name, reporter_config| reporters.each do |reporter_name, reporter_config|
raise NotImplementedError, "'#{reporter_name}' is not a valid reporter type." unless valid_types.include?(reporter_name) raise NotImplementedError, "'#{reporter_name}' is not a valid reporter type." unless valid_types.include?(reporter_name)

View file

@ -0,0 +1,27 @@
module Inspec::Plugin::V2::PluginType
class Reporter < Inspec::Plugin::V2::PluginBase
register_plugin_type(:reporter)
attr_reader :run_data
def initialize(config)
@config = config
@run_data = config[:run_data]
@output = ""
end
def output(str, newline = true)
@output << str
@output << "\n" if newline
end
def rendered_output
@output
end
# each reporter must implement #render
def render
raise NotImplementedError, "#{self.class} must implement a `#render` method to format its output."
end
end
end

View file

@ -30,7 +30,10 @@ module Inspec::Reporters
when "yaml" when "yaml"
reporter = Inspec::Reporters::Yaml.new(config) reporter = Inspec::Reporters::Yaml.new(config)
else else
raise NotImplementedError, "'#{name}' is not a valid reporter type." # If we made it here, it must be a plugin, and we know it exists (because we validated it in config.rb)
activator = Inspec::Plugin::V2::Registry.instance.find_activator(plugin_type: :reporter, activator_name: name.to_sym)
activator.activate!
reporter = activator.implementation_class.new(config)
end end
# optional send_report method on reporter # optional send_report method on reporter

View file

@ -0,0 +1,10 @@
{
"plugins_config_version" : "1.0.0",
"plugins": [
{
"name": "inspec-reporter-test-fixture",
"installation_type": "path",
"installation_path": "test/fixtures/plugins/inspec-reporter-test-fixture/lib/inspec-reporter-test-fixture.rb"
}
]
}

View file

@ -0,0 +1,3 @@
# inspec-reporter-test-fixture
Reporter plugin used to test reporter plugin type in test/functional/plugins_test.rb

View file

@ -0,0 +1,4 @@
libdir = File.dirname(__FILE__)
$LOAD_PATH.unshift(libdir) unless $LOAD_PATH.include?(libdir)
require "inspec-reporter-test-fixture/plugin"

View file

@ -0,0 +1,13 @@
require "inspec-reporter-test-fixture/version"
module InspecPlugins
module ReporterTestFixture
class Plugin < ::Inspec.plugin(2)
plugin_name :'inspec-reporter-test-fixture'
reporter :"test-fixture" do
require "inspec-reporter-test-fixture/reporter"
InspecPlugins::ReporterTestFixture::ReporterImplementation
end
end
end
end

View file

@ -0,0 +1,25 @@
module InspecPlugins::ReporterTestFixture
class ReporterImplementation < Inspec.plugin(2, :reporter)
# The test reporter plugin returns a single line of output, like this:
# pXX:cYY:tZZ
# where XX is the count of profiles
# YY is the count of controls
# ZZ is the count of tests
def render
profile_count = run_data[:profiles].count
control_count = 0
test_count = 0
run_data[:profiles].each do |p|
controls = p[:controls] || []
control_count += controls.count
controls.each do |c|
tests = c[:results] || []
test_count += tests.count
end
end
output("p#{profile_count}c#{control_count}t#{test_count}",true)
end
end
end

View file

@ -0,0 +1,5 @@
module InspecPlugins
module ReporterTestFixture
VERSION = "0.1.0".freeze
end
end

View file

@ -146,6 +146,28 @@ describe "input plugins" do
end end
end end
#=========================================================================================#
# Reporter plugin type
#=========================================================================================#
describe "reporter plugins" do
# The test reporter plugin returns a single line of output, like this:
# pXX:cYY:tZZ
# where XX is the count of profiles
# YY is the count of controls
# ZZ is the count of tests
let(:env) { { INSPEC_CONFIG_DIR: "#{config_dir_path}/reporter_plugin" } }
# Test a flat profile - dependencies/profile_c is a simple one
describe "when using a custom reporter on a profile with one control" do
it "finds the single control" do
cmd = "exec #{profile_path}/dependencies/profile_c --reporter test-fixture"
run_result = run_inspec_process(cmd, env: env)
_(run_result.stderr).must_be_empty
_(run_result.stdout).must_include "p1c1t1"
end
end
end
#=========================================================================================# #=========================================================================================#
# inspec plugin command # inspec plugin command
#=========================================================================================# #=========================================================================================#

View file

@ -0,0 +1,39 @@
require "helper"
require "inspec/plugin/v2"
describe "Reporter plugin type" do
describe "when registering the plugin type superclass" do
it "returns the superclass when calling the global defintion method" do
klass = Inspec.plugin(2, :reporter)
_(klass).must_be_kind_of Class
_(klass).must_equal Inspec::Plugin::V2::PluginType::Reporter
end
it "returns the superclass when referenced by alias" do
klass = Inspec::Plugin::V2::PluginBase.base_class_for_type(:reporter)
_(klass).must_be_kind_of Class
_(klass).must_equal Inspec::Plugin::V2::PluginType::Reporter
end
it "registers an activation dsl method" do
klass = Inspec::Plugin::V2::PluginBase
_(klass).must_respond_to :reporter
end
end
describe "when examining the specific plugin type API" do
[
# API instance methods
:render, # pure virtual
:output, # helper
:rendered_output, # accessor
:run_data, # accessor
].each do |api_method|
it "should define a '#{api_method}' method in the superclass" do
klass = Inspec::Plugin::V2::PluginType::Reporter
_(klass.method_defined?(api_method)).must_equal true
end
end
end
end