diff --git a/README.md b/README.md index 89400aa..b877728 100644 --- a/README.md +++ b/README.md @@ -141,16 +141,22 @@ gem install package-audit package-audit --group staging --group production [DIR] ``` -* To show how risk is calculated for the above report run: +* To produce the same report in a CSV format run: ```bash - package-audit risk + package-audit --format csv ``` -* To produce the same report in a CSV format run: +* To produce the same report in a Markdown format run: + + ```bash + package-audit --format md + ``` + +* To show how risk is calculated for the above report run: ```bash - package-audit --csv + package-audit risk ``` #### For a list of all commands and their options run: diff --git a/lib/package/audit/cli.rb b/lib/package/audit/cli.rb index 8642aa9..b325593 100644 --- a/lib/package/audit/cli.rb +++ b/lib/package/audit/cli.rb @@ -1,5 +1,6 @@ require_relative 'const/file' require_relative 'const/time' +require_relative 'enum/format' require_relative 'enum/option' require_relative 'services/command_parser' require_relative 'util//risk_legend' @@ -25,12 +26,12 @@ class CLI < Thor class_option Enum::Option::INCLUDE_IGNORED, type: :boolean, default: false, desc: 'Include packages ignored by a configuration file' - class_option Enum::Option::CSV, - type: :boolean, default: false, - desc: 'Output reports using comma separated values (CSV)' + class_option Enum::Option::FORMAT, + aliases: '-f', banner: Enum::Format.all.join('|'), type: :string, + desc: 'Output reports using a different format (e.g. CSV or Markdown)' class_option Enum::Option::CSV_EXCLUDE_HEADERS, type: :boolean, default: false, - desc: "Hide headers when using the --#{Enum::Option::CSV} option" + desc: "Hide headers when using the #{Enum::Format::CSV} format" map '-v' => :version map '--version' => :version diff --git a/lib/package/audit/enum/format.rb b/lib/package/audit/enum/format.rb new file mode 100644 index 0000000..5a8018a --- /dev/null +++ b/lib/package/audit/enum/format.rb @@ -0,0 +1,14 @@ +module Package + module Audit + module Enum + module Format + CSV = 'csv' + MARKDOWN = 'md' + + def self.all + constants.map { |key| const_get(key) }.sort + end + end + end + end +end diff --git a/lib/package/audit/enum/option.rb b/lib/package/audit/enum/option.rb index bcd7741..2464360 100644 --- a/lib/package/audit/enum/option.rb +++ b/lib/package/audit/enum/option.rb @@ -3,7 +3,7 @@ module Audit module Enum module Option CONFIG = 'config' - CSV = 'csv' + FORMAT = 'format' CSV_EXCLUDE_HEADERS = 'exclude-headers' GROUP = 'group' INCLUDE_IGNORED = 'include-ignored' diff --git a/lib/package/audit/enum/technology.rb b/lib/package/audit/enum/technology.rb index 24b0f90..8d2998f 100644 --- a/lib/package/audit/enum/technology.rb +++ b/lib/package/audit/enum/technology.rb @@ -6,7 +6,7 @@ module Technology RUBY = 'ruby' def self.all - constants.map { |key| Enum::Technology.const_get(key) }.sort + constants.map { |key| const_get(key) }.sort end end end diff --git a/lib/package/audit/models/package.rb b/lib/package/audit/models/package.rb index 10f68e3..d099589 100644 --- a/lib/package/audit/models/package.rb +++ b/lib/package/audit/models/package.rb @@ -41,7 +41,7 @@ def risk? end def group_list - @groups.join('|') + @groups.join(' ') end def vulnerabilities_grouped diff --git a/lib/package/audit/services/command_parser.rb b/lib/package/audit/services/command_parser.rb index 63c29fa..46bcb64 100644 --- a/lib/package/audit/services/command_parser.rb +++ b/lib/package/audit/services/command_parser.rb @@ -18,9 +18,10 @@ def initialize(dir, options, report) @dir = dir @options = options @report = report - @config = parse_config_file + @config = parse_config_file! @groups = @options[Enum::Option::GROUP] - @technologies = parse_technologies + @technologies = parse_technologies! + validate_format! @spinner = Util::Spinner.new('Evaluating packages and their dependencies...') end @@ -71,13 +72,13 @@ def process_technologies # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticCo def print_results(technology, pkgs, ignored_pkgs) PackagePrinter.new(@options, pkgs).print(Const::Fields::DEFAULT) - print_summary(technology, pkgs, ignored_pkgs) unless @options[Enum::Option::CSV] - print_disclaimer(technology) unless @options[Enum::Option::CSV] || pkgs.empty? + print_summary(technology, pkgs, ignored_pkgs) unless @options[Enum::Option::FORMAT] == Enum::Format::CSV + print_disclaimer(technology) unless @options[Enum::Option::FORMAT] == Enum::Format::CSV || pkgs.empty? end def print_summary(technology, pkgs, ignored_pkgs) if @report == Enum::Report::ALL - Util::SummaryPrinter.statistics(technology, @report, pkgs, ignored_pkgs) + Util::SummaryPrinter.statistics(@options[Enum::Option::FORMAT], technology, @report, pkgs, ignored_pkgs) else Util::SummaryPrinter.total(technology, @report, pkgs, ignored_pkgs) end @@ -103,7 +104,7 @@ def learn_more_command(technology) end end - def parse_config_file + def parse_config_file! if @options[Enum::Option::CONFIG].nil? YAML.load_file("#{@dir}/#{Const::File::CONFIG}") if File.exist? "#{@dir}/#{Const::File::CONFIG}" elsif File.exist? @options[Enum::Option::CONFIG] @@ -113,7 +114,13 @@ def parse_config_file end end - def parse_technologies + def validate_format! + format = @options[Enum::Option::FORMAT] + raise ArgumentError, "Invalid format: #{format}, should be one of [#{Enum::Format.all.join('|')}]" unless + @options[Enum::Option::FORMAT].nil? || Enum::Format.all.include?(format) + end + + def parse_technologies! technology_validator = Technology::Validator.new(@dir) @options[Enum::Option::TECHNOLOGY]&.each { |technology| technology_validator.validate! technology } @options[Enum::Option::TECHNOLOGY] || Technology::Detector.new(@dir).detect diff --git a/lib/package/audit/services/package_printer.rb b/lib/package/audit/services/package_printer.rb index 1744a76..249ffbd 100644 --- a/lib/package/audit/services/package_printer.rb +++ b/lib/package/audit/services/package_printer.rb @@ -21,8 +21,11 @@ def print(fields) check_fields(fields) return if @pkgs.empty? - if @options[Enum::Option::CSV] + case @options[Enum::Option::FORMAT] + when Enum::Format::CSV csv(fields, exclude_headers: @options[Enum::Option::CSV_EXCLUDE_HEADERS]) + when Enum::Format::MARKDOWN + markdown(fields) else pretty(fields) end @@ -39,72 +42,78 @@ def check_fields(fields) "Available fields names are: #{Const::Fields::DEFAULT}." end - def pretty(fields = Const::Fields::DEFAULT) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity - # find the maximum length of each field across all the packages so we know how many - # characters of horizontal space to allocate for each field when printing - fields.each do |key| - instance_variable_set :"@max_#{key}", Const::Fields::HEADERS[key].length - @pkgs.each do |gem| - curr_field_length = case key - when :vulnerabilities - gem.vulnerabilities_grouped.length - when :groups - gem.group_list.length - else - gem.send(key)&.gsub(BASH_FORMATTING_REGEX, '')&.length || 0 - end - max_field_length = instance_variable_get :"@max_#{key}" - instance_variable_set :"@max_#{key}", [curr_field_length, max_field_length].max - end - end - - line_length = fields.sum { |key| instance_variable_get :"@max_#{key}" } + - (COLUMN_GAP * (fields.length - 1)) + def pretty(fields = Const::Fields::DEFAULT) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength + max_widths = get_field_max_widths(fields) + header = fields.map.with_index do |field, index| + Const::Fields::HEADERS[field].gsub(BASH_FORMATTING_REGEX, '').ljust(max_widths[index]) + end.join(' ' * COLUMN_GAP) + separator = max_widths.map { |width| '=' * width }.join('=' * COLUMN_GAP) - puts '=' * line_length - puts fields.map { |key| - Const::Fields::HEADERS[key].gsub(BASH_FORMATTING_REGEX, '').ljust(instance_variable_get(:"@max_#{key}")) - }.join(' ' * COLUMN_GAP) - puts '=' * line_length + puts separator + puts header + puts separator @pkgs.each do |pkg| - puts fields.map { |key| - val = pkg.send(key) || '' - val = case key - when :groups - pkg.group_list - when :risk_type - Formatter::Risk.new(pkg.risk_type).format - when :version - Formatter::Version.new(pkg.version, pkg.latest_version).format - when :vulnerabilities - Formatter::Vulnerability.new(pkg.vulnerabilities).format - when :latest_version_date - Formatter::VersionDate.new(pkg.latest_version_date).format - else - val - end - + puts fields.map.with_index { |key, index| + val = get_field_value(pkg, key) formatting_length = val.length - val.gsub(BASH_FORMATTING_REGEX, '').length - val.ljust(instance_variable_get(:"@max_#{key}") + formatting_length) + val.ljust(max_widths[index] + formatting_length) }.join(' ' * COLUMN_GAP) end end - def csv(fields, exclude_headers: false) - value_fields = fields.map do |field| - case field - when :groups - :group_list - when :vulnerabilities - :vulnerabilities_grouped - else - field + def csv(fields = Const::Fields::DEFAULT, exclude_headers: false) + puts fields.join(',') unless exclude_headers + @pkgs.map do |pkg| + puts fields.map { |field| get_field_value(pkg, field) }.join(',').gsub(BASH_FORMATTING_REGEX, '') + end + end + + def markdown(fields = Const::Fields::DEFAULT) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength + max_widths = get_field_max_widths(fields) + header = fields.map.with_index do |field, index| + Const::Fields::HEADERS[field].gsub(BASH_FORMATTING_REGEX, '').ljust(max_widths[index]) + end.join(' | ') + separator = max_widths.map { |width| ":#{'-' * width}" }.join('-|') + + puts "| #{header} |" + puts "|#{separator}-|" + + @pkgs.each do |pkg| + row = fields.map.with_index do |key, index| + val = get_field_value(pkg, key) + formatting_length = val.length - val.gsub(BASH_FORMATTING_REGEX, '').length + val.ljust(max_widths[index] + formatting_length) end + puts "| #{row.join(' | ')} |" end + end - puts fields.join(',') unless exclude_headers - @pkgs.map { |gem| puts gem.to_csv(value_fields) } + def get_field_max_widths(fields) + # Calculate the maximum width for each column, including header titles and content + fields.map do |field| + [@pkgs.map do |pkg| + value = get_field_value(pkg, field).to_s.gsub(BASH_FORMATTING_REGEX, '').length + value + end.max, Const::Fields::HEADERS[field].gsub(BASH_FORMATTING_REGEX, '').length].max + end + end + + def get_field_value(pkg, field) # rubocop:disable Metrics/MethodLength + case field + when :groups + pkg.group_list + when :risk_type + Formatter::Risk.new(pkg.risk_type).format + when :version + Formatter::Version.new(pkg.version, pkg.latest_version).format + when :vulnerabilities + Formatter::Vulnerability.new(pkg.vulnerabilities).format + when :latest_version_date + Formatter::VersionDate.new(pkg.latest_version_date).format + else + pkg.send(field) || '' + end end end end diff --git a/lib/package/audit/util/summary_printer.rb b/lib/package/audit/util/summary_printer.rb index 7468b84..84bbb1a 100644 --- a/lib/package/audit/util/summary_printer.rb +++ b/lib/package/audit/util/summary_printer.rb @@ -33,9 +33,9 @@ def self.total(technology, report, pkgs, ignored_pkgs) end end - def self.statistics(technology, report, pkgs, ignored_pkgs) + def self.statistics(format, technology, report, pkgs, ignored_pkgs) stats = calculate_statistics(pkgs, ignored_pkgs) - display_results(technology, report, pkgs, ignored_pkgs, stats) + display_results(format, technology, report, pkgs, ignored_pkgs, stats) end private_class_method def self.calculate_statistics(pkgs, ignored_pkgs) @@ -56,12 +56,16 @@ def self.statistics(technology, report, pkgs, ignored_pkgs) pkgs.count(&status) end - private_class_method def self.display_results(technology, report, pkgs, ignored_pkgs, stats) + private_class_method def self.display_results(format, technology, report, pkgs, ignored_pkgs, stats) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Metrics/ParameterLists if pkgs.any? - puts status_message(stats) + print status_message(stats) + print Util::BashColor.cyan(' \\') if format == Enum::Format::MARKDOWN + puts total(technology, report, pkgs, ignored_pkgs) elsif ignored_pkgs.any? - puts status_message(stats) + print status_message(stats) + print Util::BashColor.cyan(' \\') if format == Enum::Format::MARKDOWN + puts puts Util::BashColor.green("There are no deprecated, outdated or vulnerable #{technology} " \ "packages (#{ignored_pkgs.length} ignored)!\n") else diff --git a/sig/package/audit/enum/format.rbs b/sig/package/audit/enum/format.rbs new file mode 100644 index 0000000..1df0d34 --- /dev/null +++ b/sig/package/audit/enum/format.rbs @@ -0,0 +1,12 @@ +module Package + module Audit + module Enum + module Format + CSV: String + MARKDOWN: String + + def self.all: -> Array[String] + end + end + end +end diff --git a/sig/package/audit/enum/option.rbs b/sig/package/audit/enum/option.rbs index 2277240..f86e5e4 100644 --- a/sig/package/audit/enum/option.rbs +++ b/sig/package/audit/enum/option.rbs @@ -3,7 +3,7 @@ module Package module Enum module Option CONFIG: String - CSV: String + FORMAT: String CSV_EXCLUDE_HEADERS: String GROUP: String INCLUDE_IGNORED: String diff --git a/sig/package/audit/services/command_parser.rbs b/sig/package/audit/services/command_parser.rbs index be629ce..3687373 100644 --- a/sig/package/audit/services/command_parser.rbs +++ b/sig/package/audit/services/command_parser.rbs @@ -17,10 +17,12 @@ module Package def learn_more_command: (String) -> String? - def parse_config_file: -> Hash[String, untyped]? + def parse_config_file!: -> Hash[String, untyped]? def parse_technologies: -> Array[String] + def parse_technologies!: -> Array[String] + def print_disclaimer: (String) -> void def print_results: (String, Array[Package], Array[Package]) -> void @@ -28,6 +30,8 @@ module Package def print_summary: (String, Array[Package], Array[Package]) -> void def process_technologies: -> int + + def validate_format!: -> void end end end diff --git a/sig/package/audit/services/package_printer.rbs b/sig/package/audit/services/package_printer.rbs index e89c40d..cfde4e5 100644 --- a/sig/package/audit/services/package_printer.rbs +++ b/sig/package/audit/services/package_printer.rbs @@ -18,7 +18,13 @@ module Package def csv: (Array[Symbol], ?exclude_headers: bool) -> void - def pretty: (?Array[Symbol]) -> void + def get_field_max_widths: (Array[Symbol]) -> Array[Integer] + + def get_field_value: (Package, Symbol) -> String + + def markdown: (Array[Symbol]) -> void + + def pretty: (Array[Symbol]) -> void end end end diff --git a/sig/package/audit/util/summary_printer.rbs b/sig/package/audit/util/summary_printer.rbs index c82badd..59ddd62 100644 --- a/sig/package/audit/util/summary_printer.rbs +++ b/sig/package/audit/util/summary_printer.rbs @@ -4,13 +4,15 @@ module Package module SummaryPrinter def self.all: -> void + def self.calculate_statistics: (Array[Package], Array[Package]) -> Hash[Symbol, Integer] + def self.count_status: (Array[Package], Symbol) -> Integer def self.deprecated: -> void - def self.display_results: (String, Symbol, Array[Package], Array[Package], Hash[Symbol, Integer]) -> void + def self.display_results: (String, String, Symbol, Array[Package], Array[Package], Hash[Symbol, Integer]) -> void - def self.statistics: (String, Symbol, Array[Package], Array[Package]) -> void + def self.statistics: (String, String, Symbol, Array[Package], Array[Package]) -> void def self.status_message: (Hash[Symbol, Integer]) -> String diff --git a/steep_expectations.yml b/steep_expectations.yml index 1e12ca9..40e4a78 100644 --- a/steep_expectations.yml +++ b/steep_expectations.yml @@ -145,19 +145,23 @@ severity: WARNING message: 'Cannot find the declaration of constant: `Bundler`' code: Ruby::UnknownConstant -- file: lib/package/audit/util/summary_printer.rb +- file: lib/package/audit/services/package_printer.rb diagnostics: - range: start: - line: 37 - character: 18 + line: 94 + character: 19 end: - line: 37 - character: 38 - severity: ERROR - message: Type `singleton(::Package::Audit::Util::SummaryPrinter)` does not have - method `calculate_statistics` - code: Ruby::NoMethod + line: 99 + character: 11 + severity: WARNING + message: |- + Cannot allow block body have type `(::Integer | nil)` because declared as type `::Integer` + (::Integer | nil) <: ::Integer + nil <: ::Integer + code: Ruby::BlockBodyTypeMismatch +- file: lib/package/audit/util/summary_printer.rb + diagnostics: - range: start: line: 56 diff --git a/test/package/audit/test_cli.rb b/test/package/audit/test_cli.rb new file mode 100644 index 0000000..291d9fd --- /dev/null +++ b/test/package/audit/test_cli.rb @@ -0,0 +1,89 @@ +require 'test_helper' +require_relative '../../../lib/package/audit/enum/format' +require_relative '../../../lib/package/audit/enum/technology' +require_relative '../../../lib/package/audit/util/risk_legend' +require_relative '../../../lib/package/audit/version' +require 'bundler' + +module Package + module Audit + class Cli < Minitest::Test + def test_that_it_version_command_works + output = `bundle exec package-audit --version` + + assert_match VERSION, output + end + + def test_that_it_risk_command_works + output = `bundle exec package-audit risk` + + stdout = StringIO.new + $stdout = stdout + Util::RiskLegend.print + $stdout = STDOUT + + assert_equal stdout.string, output + end + + def test_that_help_command_works + output = `bundle exec package-audit help` + + assert_match 'Commands:', output + end + + def test_that_unknown_commands_give_an_appropriate_error + output = `bundle exec package-audit invalid` + + assert_match '"invalid" is not a valid directory', output + end + + def test_that_that_config_option_works + output = `bundle exec package-audit test/files/gemfile/empty --config test/files/config/.package-audit.yml` + + assert_match 'There are no deprecated, outdated or vulnerable ruby packages!', output + end + + def test_that_that_config_option_alias_works + output = `bundle exec package-audit test/files/gemfile/empty -c test/files/config/.package-audit.yml` + + assert_match 'There are no deprecated, outdated or vulnerable ruby packages!', output + end + + def test_that_that_config_option_returns_an_appropriate_error + output = `bundle exec package-audit test/files/gemfile/empty -c test/files/config/invalid` + + assert_match 'Configuration file not found: test/files/config/invalid', output + end + + def test_that_that_include_ignored_option_works + output = `bundle exec package-audit test/files/gemfile/empty --include-ignored` + + assert_match 'There are no deprecated, outdated or vulnerable ruby packages!', output + end + + def test_that_that_exclude_headers_option_works + output = `bundle exec package-audit test/files/gemfile/empty --exclude-headers` + + assert_match 'There are no deprecated, outdated or vulnerable ruby packages!', output + end + + def test_that_that_format_option_works + output = `bundle exec package-audit test/files/gemfile/empty --format invalid` + + assert_match "Invalid format: invalid, should be one of [#{Enum::Format.all.join('|')}]", output + end + + def test_that_that_format_option_alias_works + output = `bundle exec package-audit test/files/gemfile/empty -f invalid` + + assert_match "Invalid format: invalid, should be one of [#{Enum::Format.all.join('|')}]", output + end + + def test_that_that_technology_option_works + output = `bundle exec package-audit test/files/gemfile/empty --technology invalid` + + assert_match "\"invalid\" is not a supported technology, use one of #{Enum::Technology.all}", output + end + end + end +end diff --git a/test/package/audit/test_package.rb b/test/package/audit/test_package.rb index 342b139..a4ad131 100644 --- a/test/package/audit/test_package.rb +++ b/test/package/audit/test_package.rb @@ -20,7 +20,7 @@ def test_that_the_full_name_field_works_as_expected end def test_that_the_group_list_is_properly_delimited - assert_equal @package.groups.join('|'), @package.group_list + assert_equal @package.groups.join(' '), @package.group_list end def test_that_grouped_vulnerabilities_are_formatted_correctly diff --git a/test/package/audit/test_printer.rb b/test/package/audit/test_printer.rb index 14da76f..8fd88eb 100644 --- a/test/package/audit/test_printer.rb +++ b/test/package/audit/test_printer.rb @@ -2,6 +2,7 @@ require 'stringio' require_relative '../../../lib/package/audit/const/fields' +require_relative '../../../lib/package/audit/enum/format' require_relative '../../../lib/package/audit/util/bash_color' require_relative '../../../lib/package/audit/services/package_printer' require_relative '../../../lib/package/audit/formatter/vulnerability' @@ -41,19 +42,36 @@ def test_that_an_error_is_shown_for_invalid_fields end end - def test_that_the_dependencies_are_displayed_correctly + def test_that_the_dependencies_are_displayed_correctly # rubocop:disable Metrics/AbcSize PackagePrinter.new({}, @gems).print(%i[name version latest_version latest_version_date]) lines = @output.string.split("\n") assert_equal 6, lines.length + assert_equal '========================================', lines[0] + assert_equal 'Package Version Latest Latest Date', lines[1] + assert_equal '========================================', lines[2] assert_equal "fileutils 1.#{Util::BashColor.yellow('5.0')} 1.7.1 #{@today} ", lines[3] assert_equal "puma 5.1.1 5.1.1 #{Util::BashColor.yellow('2020-12-10')} ", lines[4] assert_equal "rails #{Util::BashColor.orange('6.0.0')} 7.0.4.3 #{@today} ", lines[5] end + def test_that_the_dependencies_are_displayed_correctly_in_markdown # rubocop:disable Metrics/AbcSize + options = {Enum::Option::FORMAT => Enum::Format::MARKDOWN} + PackagePrinter.new(options, @gems).print(%i[name version latest_version latest_version_date]) + lines = @output.string.split("\n") + + assert_equal 5, lines.length + assert_equal '| Package | Version | Latest | Latest Date |', lines[0] + assert_equal '|:----------|:--------|:--------|:------------|', lines[1] + assert_equal "| fileutils | 1.#{Util::BashColor.yellow('5.0')} | 1.7.1 | #{@today} |", lines[2] + assert_equal "| puma | 5.1.1 | 5.1.1 | #{Util::BashColor.yellow('2020-12-10')} |", lines[3] + assert_equal "| rails | #{Util::BashColor.orange('6.0.0')} | 7.0.4.3 | #{@today} |", lines[4] + end + def test_that_the_dependencies_are_displayed_correctly_in_csv_with_headers # rubocop:disable Metrics/AbcSize + options = {Enum::Option::FORMAT => Enum::Format::CSV} columns = %i[name version latest_version groups] - PackagePrinter.new({ 'csv' => true}, @gems).print(columns) + PackagePrinter.new(options, @gems).print(columns) lines = @output.string.split("\n") assert_equal @gems.length + 1, lines.length @@ -72,10 +90,11 @@ def test_that_vulnerabilities_are_displayed_correctly end def test_that_vulnerabilities_are_displayed_correctly_in_csv - PackagePrinter.new({ 'csv' => true}, [@rails]).print(%i[vulnerabilities]) + options = {Enum::Option::FORMAT => Enum::Format::CSV} + PackagePrinter.new(options, [@rails]).print(%i[vulnerabilities]) lines = @output.string.split("\n") - assert_equal 'medium(1)|high(2)', lines[1] + assert_equal 'medium(1) high(2)', lines[1] end end end diff --git a/test/package/test_audit.rb b/test/package/test_audit.rb index ac0a3bb..4638143 100644 --- a/test/package/test_audit.rb +++ b/test/package/test_audit.rb @@ -1,5 +1,4 @@ require 'test_helper' -require_relative '../../lib/package/audit/util/risk_legend' require_relative '../../lib/package/audit/util/summary_printer' require_relative '../../lib/package/audit/version' @@ -11,23 +10,6 @@ def test_that_it_has_a_version_number refute_nil Package::Audit::VERSION end - def test_that_it_returns_a_version_number - output = `bundle exec package-audit --version` - - assert_match Package::Audit::VERSION, output - end - - def test_that_it_prints_risk_information - output = `bundle exec package-audit risk` - - stdout = StringIO.new - $stdout = stdout - Package::Audit::Util::RiskLegend.print - $stdout = STDOUT - - assert_equal stdout.string, output - end - def test_that_there_is_a_success_message_when_report_is_empty output = `bundle exec package-audit test/files/gemfile/empty`