Module: Ibex::CLICoverage

Defined in:
lib/ibex/cli/coverage.rb,
sig/ibex/cli/coverage.rbs

Overview

CLI collection, merge, and threshold checks for runtime coverage.

Instance Method Summary collapse

Instance Method Details

#atomic_write_coverage(path, source) ⇒ void

This method returns an undefined value.

RBS:

  • (String path, String source) -> void

Parameters:

  • path (String)
  • source (String)


151
152
153
154
155
156
157
158
159
160
161
162
163
164
# File 'lib/ibex/cli/coverage.rb', line 151

def atomic_write_coverage(path, source)
  target_path = File.symlink?(path) ? File.realpath(path) : path
  directory = File.dirname(File.expand_path(target_path))
  basename = File.basename(target_path)
  Tempfile.create([".#{basename}.", ".tmp"], directory) do |temporary|
    temporary.binmode
    temporary.write(source)
    temporary.flush
    temporary.fsync
    temporary.chmod(coverage_file_mode(target_path))
    temporary.close
    File.rename(temporary.path, target_path)
  end
end

#coverage_check_options(arguments) ⇒ coverage_check_options

RBS:

  • (Array[String] arguments) -> coverage_check_options

Parameters:

  • arguments (Array[String])

Returns:



98
99
100
101
102
103
104
105
106
107
108
109
110
111
# File 'lib/ibex/cli/coverage.rb', line 98

def coverage_check_options(arguments)
  settings = { paths: [], min_states: 0.0, min_productions: 0.0 } #: coverage_check_options
  parser = OptionParser.new do |options|
    options.banner = "Usage: ibex coverage check REPORT [thresholds]"
    options.on("--min-states=PERCENT", Float, "minimum visited states") do |value|
      settings[:min_states] = coverage_threshold(value, "min-states")
    end
    options.on("--min-productions=PERCENT", Float, "minimum reduced productions") do |value|
      settings[:min_productions] = coverage_threshold(value, "min-productions")
    end
  end
  settings[:paths] = parser.parse(arguments)
  settings
end

#coverage_file_mode(path) ⇒ Integer

RBS:

  • (String path) -> Integer

Parameters:

  • path (String)

Returns:

  • (Integer)


167
168
169
170
171
# File 'lib/ibex/cli/coverage.rb', line 167

def coverage_file_mode(path)
  return File.stat(path).mode & 0o777 if File.exist?(path)

  0o666 & ~File.umask
end

#coverage_output_options(arguments, command, operands) ⇒ coverage_output_options

RBS:

  • (Array[String] arguments, String command, String operands) -> coverage_output_options

Parameters:

  • arguments (Array[String])
  • command (String)
  • operands (String)

Returns:



87
88
89
90
91
92
93
94
95
# File 'lib/ibex/cli/coverage.rb', line 87

def coverage_output_options(arguments, command, operands)
  settings = { paths: [] } #: coverage_output_options
  parser = OptionParser.new do |options|
    options.banner = "Usage: ibex coverage #{command} #{operands} [-o FILE]"
    options.on("-o FILE", "--output=FILE", "write atomically to FILE") { |value| settings[:output] = value }
  end
  settings[:paths] = parser.parse(arguments)
  settings
end

#coverage_percentage(covered, total) ⇒ Float

RBS:

  • (Integer covered, Integer total) -> Float

Parameters:

  • covered (Integer)
  • total (Integer)

Returns:

  • (Float)


121
122
123
124
125
# File 'lib/ibex/cli/coverage.rb', line 121

def coverage_percentage(covered, total)
  return 100.0 if total.zero?

  covered.fdiv(total) * 100.0
end

#coverage_threshold(value, name) ⇒ Float

RBS:

  • (Float value, String name) -> Float

Parameters:

  • value (Float)
  • name (String)

Returns:

  • (Float)


114
115
116
117
118
# File 'lib/ibex/cli/coverage.rb', line 114

def coverage_threshold(value, name)
  return value if value.finite? && value >= 0.0 && value <= 100.0

  raise OptionParser::InvalidArgument, "--#{name} must be between 0 and 100"
end

#reject_coverage_output_collision(inputs, output) ⇒ void

This method returns an undefined value.

RBS:

  • (Array[String] inputs, String? output) -> void

Parameters:

  • inputs (Array[String])
  • output (String, nil)


135
136
137
138
139
140
# File 'lib/ibex/cli/coverage.rb', line 135

def reject_coverage_output_collision(inputs, output)
  return unless output
  return unless inputs.any? { |input| same_file_target?(input, output) }

  raise Ibex::Error, "(cli):1:1: coverage input and output paths must be distinct"
end

#run_coverage_check(arguments) ⇒ Integer

RBS:

  • (Array[String] arguments) -> Integer

Parameters:

  • arguments (Array[String])

Returns:

  • (Integer)


63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
# File 'lib/ibex/cli/coverage.rb', line 63

def run_coverage_check(arguments)
  settings = coverage_check_options(arguments)
  path = single_coverage_path(settings.fetch(:paths), "coverage check")
  report = Coverage::Report.load_file(path)
  state_percentage = coverage_percentage(report.state_hits.length, report.state_count)
  production_percentage = coverage_percentage(report.production_hits.length, report.production_count)
  passed = state_percentage >= settings.fetch(:min_states) &&
           production_percentage >= settings.fetch(:min_productions)
  summary = format(
    "coverage %<status>s: states %<states>.2f%% (%<state_hits>d/%<state_total>d), " \
    "productions %<productions>.2f%% (%<production_hits>d/%<production_total>d)",
    status: passed ? "ok" : "failed",
    states: state_percentage,
    state_hits: report.state_hits.length,
    state_total: report.state_count,
    productions: production_percentage,
    production_hits: report.production_hits.length,
    production_total: report.production_count
  )
  (passed ? @stdout : @stderr).puts(summary)
  passed ? 0 : 1
end

#run_coverage_collect(arguments) ⇒ Integer

RBS:

  • (Array[String] arguments) -> Integer

Parameters:

  • arguments (Array[String])

Returns:

  • (Integer)


38
39
40
41
42
43
44
45
# File 'lib/ibex/cli/coverage.rb', line 38

def run_coverage_collect(arguments)
  settings = coverage_output_options(arguments, "collect", "INPUT")
  input = single_coverage_path(settings.fetch(:paths), "coverage collect")
  reject_coverage_output_collision([input], settings[:output])
  report = Coverage::Collector.collect_file(input)
  write_coverage_report(report, settings[:output])
  0
end

#run_coverage_command(arguments) ⇒ Integer

RBS:

  • (Array[String] arguments) -> Integer

Parameters:

  • arguments (Array[String])

Returns:

  • (Integer)


26
27
28
29
30
31
32
33
34
35
# File 'lib/ibex/cli/coverage.rb', line 26

def run_coverage_command(arguments)
  operation = arguments.shift
  case operation
  when "collect" then run_coverage_collect(arguments)
  when "merge" then run_coverage_merge(arguments)
  when "check" then run_coverage_check(arguments)
  else
    raise Ibex::Error, "(cli):1:1: coverage requires collect, merge, or check"
  end
end

#run_coverage_merge(arguments) ⇒ Integer

RBS:

  • (Array[String] arguments) -> Integer

Parameters:

  • arguments (Array[String])

Returns:

  • (Integer)


48
49
50
51
52
53
54
55
56
57
58
59
60
# File 'lib/ibex/cli/coverage.rb', line 48

def run_coverage_merge(arguments)
  settings = coverage_output_options(arguments, "merge", "INPUT...")
  inputs = settings.fetch(:paths)
  raise Ibex::Error, "(cli):1:1: coverage merge requires at least one input" if inputs.empty?

  reject_coverage_output_collision(inputs, settings[:output])
  reports = inputs.map { |path| Coverage::Report.load_file(path) }
  report = Coverage::Report.merge(reports)
  write_coverage_report(report, settings[:output])
  0
rescue ArgumentError => e
  raise Ibex::Error, "(coverage):1:1: #{e.message}"
end

#single_coverage_path(arguments, command) ⇒ String

RBS:

  • (Array[String] arguments, String command) -> String

Parameters:

  • arguments (Array[String])
  • command (String)

Returns:

  • (String)


128
129
130
131
132
# File 'lib/ibex/cli/coverage.rb', line 128

def single_coverage_path(arguments, command)
  return arguments.first if arguments.length == 1

  raise Ibex::Error, "(cli):1:1: #{command} requires exactly one input"
end

#write_coverage_report(report, output) ⇒ void

This method returns an undefined value.

RBS:

  • (Coverage::Report report, String? output) -> void

Parameters:



143
144
145
146
147
148
# File 'lib/ibex/cli/coverage.rb', line 143

def write_coverage_report(report, output)
  source = report.to_json
  return @stdout.write(source) unless output

  atomic_write_coverage(output, source)
end