Class: Henitai::Result
- Inherits:
-
Object
- Object
- Henitai::Result
- Includes:
- UnparseHelper
- Defined in:
- lib/henitai/result.rb,
sig/henitai.rbs
Overview
Aggregates the outcome of a complete mutation testing run.
Provides metrics and the serialised Stryker mutation-testing-report-schema JSON payload. The schema version follows stryker-mutator/mutation-testing-elements.
Constant Summary collapse
- SCHEMA_VERSION =
"1.0"- DEFAULT_THRESHOLDS =
{ high: 80, low: 60 }.freeze
Instance Attribute Summary collapse
-
#finished_at ⇒ Time
readonly
Returns the value of attribute finished_at.
-
#git_sha ⇒ String?
readonly
Returns the value of attribute git_sha.
-
#mutants ⇒ Array[Mutant]
readonly
Returns the value of attribute mutants.
-
#session_id ⇒ String
readonly
Returns the value of attribute session_id.
-
#started_at ⇒ Time
readonly
Returns the value of attribute started_at.
-
#survivor_stats ⇒ Hash[Symbol, untyped]?
readonly
Returns the value of attribute survivor_stats.
-
#thresholds ⇒ Hash[Symbol, Integer]
readonly
Returns the value of attribute thresholds.
Instance Method Summary collapse
- #authoritative? ⇒ Boolean
- #base_schema ⇒ Hash[Symbol, untyped]
- #build_files_section ⇒ Hash[Symbol, untyped]
- #coverage_schema(mutant) ⇒ Hash[Symbol, untyped]
-
#detected ⇒ Integer
Detected = killed + timeout + runtime_error (alle Zustände die einen Fehler beweisen).
- #detected_in(list) ⇒ Integer
-
#duration ⇒ Float
Duration in seconds.
- #duration_for(mutant) ⇒ Integer?
- #equivalence_uncertainty ⇒ String?
-
#equivalent ⇒ Integer
Number of confirmed equivalent mutants (excluded from MS).
-
#executed_scoring_summary ⇒ Hash?
Scores over only the mutants actually executed this run — verdicts reused from the history store (
--incremental) excluded. -
#from_cache_for(mutant) ⇒ Boolean?
Vendored extension field (like stableId): marks verdicts reused from the history store so reports stay honest about what actually executed.
-
#initialize(mutants:, started_at:, finished_at:, thresholds: nil, partial_rerun: false, survivor_stats: nil, session_id: SecureRandom.uuid, git_sha: nil, source_provider: ->(_file) { "" }, authoritative: true) ⇒ Result
constructor
rubocop:disable Metrics/ParameterLists.
-
#killed ⇒ Integer
Number of killed mutants.
- #legacy_stable_id_for(mutant) ⇒ String?
- #line_column(mutant, prefix) ⇒ Hash[Symbol, Integer]
- #location_for(mutant) ⇒ Hash[Symbol, untyped]
- #mutant_to_schema(mutant) ⇒ Hash[Symbol, untyped]
-
#mutation_score ⇒ Float?
Mutation Score (MS) — Architektur-Formel aus Abschnitt 6.1:.
- #mutation_score_for(list) ⇒ Float?
-
#mutation_score_indicator ⇒ Float?
Mutation Score Indicator (MSI) — naive Berechnung ohne Äquivalenz-Bereinigung:.
- #mutation_score_indicator_for(list) ⇒ Float?
-
#partial_rerun? ⇒ Boolean
rubocop:enable Metrics/ParameterLists.
- #replacement_for(mutant) ⇒ String
-
#scoring_summary ⇒ Hash[Symbol, untyped]
Compact public summary for reporters.
-
#status_reason_for(mutant) ⇒ String?
Serialized as the schema's statusReason so directive reasons show up next to the Ignored status in the HTML report.
- #stryker_status(status) ⇒ String
-
#survived ⇒ Integer
Number of survived mutants.
-
#to_stryker_schema ⇒ Hash
Serialise to Stryker mutation-testing-report-schema JSON (schema 1.0).
- #unmatched_survivor_ids ⇒ Array[String]
Methods included from UnparseHelper
#fallback_source, #safe_unparse
Constructor Details
#initialize(mutants:, started_at:, finished_at:, thresholds: nil, partial_rerun: false, survivor_stats: nil, session_id: SecureRandom.uuid, git_sha: nil, source_provider: ->(_file) { "" }, authoritative: true) ⇒ Result
rubocop:disable Metrics/ParameterLists
32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
# File 'lib/henitai/result.rb', line 32 def initialize(mutants:, started_at:, finished_at:, thresholds: nil, partial_rerun: false, survivor_stats: nil, session_id: SecureRandom.uuid, git_sha: nil, source_provider: ->(_file) { "" }, authoritative: true) @mutants = mutants @started_at = started_at @finished_at = finished_at @thresholds = DEFAULT_THRESHOLDS.merge(thresholds || {}) @partial_rerun = partial_rerun @survivor_stats = survivor_stats @session_id = session_id @git_sha = git_sha @source_provider = source_provider @authoritative = end |
Instance Attribute Details
#finished_at ⇒ Time (readonly)
Returns the value of attribute finished_at.
18 19 20 |
# File 'lib/henitai/result.rb', line 18 def finished_at @finished_at end |
#git_sha ⇒ String? (readonly)
Returns the value of attribute git_sha.
18 19 20 |
# File 'lib/henitai/result.rb', line 18 def git_sha @git_sha end |
#mutants ⇒ Array[Mutant] (readonly)
Returns the value of attribute mutants.
18 19 20 |
# File 'lib/henitai/result.rb', line 18 def mutants @mutants end |
#session_id ⇒ String (readonly)
Returns the value of attribute session_id.
18 19 20 |
# File 'lib/henitai/result.rb', line 18 def session_id @session_id end |
#started_at ⇒ Time (readonly)
Returns the value of attribute started_at.
18 19 20 |
# File 'lib/henitai/result.rb', line 18 def started_at @started_at end |
#survivor_stats ⇒ Hash[Symbol, untyped]? (readonly)
Returns the value of attribute survivor_stats.
18 19 20 |
# File 'lib/henitai/result.rb', line 18 def survivor_stats @survivor_stats end |
#thresholds ⇒ Hash[Symbol, Integer] (readonly)
Returns the value of attribute thresholds.
18 19 20 |
# File 'lib/henitai/result.rb', line 18 def thresholds @thresholds end |
Instance Method Details
#authoritative? ⇒ Boolean
50 |
# File 'lib/henitai/result.rb', line 50 def = @authoritative |
#base_schema ⇒ Hash[Symbol, untyped]
158 159 160 161 162 163 164 165 |
# File 'lib/henitai/result.rb', line 158 def base_schema { # : Hash[Symbol, untyped] schemaVersion: SCHEMA_VERSION, sessionId: @session_id, thresholds: thresholds, files: build_files_section } end |
#build_files_section ⇒ Hash[Symbol, untyped]
173 174 175 176 177 178 179 180 181 |
# File 'lib/henitai/result.rb', line 173 def build_files_section mutants.group_by { |m| m.location[:file] }.transform_values do |file_mutants| { language: "ruby", source: @source_provider.call(file_mutants.first.location[:file]), mutants: file_mutants.map { |m| mutant_to_schema(m) } } end end |
#coverage_schema(mutant) ⇒ Hash[Symbol, untyped]
199 200 201 202 203 204 205 206 207 |
# File 'lib/henitai/result.rb', line 199 def coverage_schema(mutant) covered_by = Array(mutant.covered_by).compact return {} if covered_by.empty? { coveredBy: covered_by, testsCompleted: mutant.tests_completed || covered_by.size } end |
#detected ⇒ Integer
Detected = killed + timeout + runtime_error (alle Zustände die einen Fehler beweisen)
63 64 65 |
# File 'lib/henitai/result.rb', line 63 def detected detected_in(mutants) end |
#detected_in(list) ⇒ Integer
140 141 142 |
# File 'lib/henitai/result.rb', line 140 def detected_in(list) list.count { |m| %i[killed timeout runtime_error].include?(m.status) } end |
#duration ⇒ Float
Returns duration in seconds.
121 122 123 |
# File 'lib/henitai/result.rb', line 121 def duration finished_at - started_at end |
#duration_for(mutant) ⇒ Integer?
230 231 232 |
# File 'lib/henitai/result.rb', line 230 def duration_for(mutant) mutant.duration&.then { |d| (d * 1000).round } end |
#equivalence_uncertainty ⇒ String?
256 257 258 259 260 |
# File 'lib/henitai/result.rb', line 256 def equivalence_uncertainty return nil if mutation_score.nil? "~10-15% of live mutants" end |
#equivalent ⇒ Integer
Returns number of confirmed equivalent mutants (excluded from MS).
59 |
# File 'lib/henitai/result.rb', line 59 def equivalent = mutants.count(&:equivalent?) |
#executed_scoring_summary ⇒ Hash?
Scores over only the mutants actually executed this run — verdicts
reused from the history store (--incremental) excluded. nil when
nothing was reused: the combined scores already tell the whole story,
and reporters use nil to suppress the extra line.
98 99 100 101 102 103 104 105 106 |
# File 'lib/henitai/result.rb', line 98 def executed_scoring_summary executed = mutants.reject { |m| m.respond_to?(:from_cache?) && m.from_cache? } return nil if executed.size == mutants.size { mutation_score: mutation_score_for(executed), mutation_score_indicator: mutation_score_indicator_for(executed) } end |
#from_cache_for(mutant) ⇒ Boolean?
Vendored extension field (like stableId): marks verdicts reused from the history store so reports stay honest about what actually executed.
244 245 246 247 248 |
# File 'lib/henitai/result.rb', line 244 def from_cache_for(mutant) return nil unless mutant.respond_to?(:from_cache?) mutant.from_cache? || nil end |
#killed ⇒ Integer
Returns number of killed mutants.
53 |
# File 'lib/henitai/result.rb', line 53 def killed = mutants.count(&:killed?) |
#legacy_stable_id_for(mutant) ⇒ String?
250 251 252 253 254 |
# File 'lib/henitai/result.rb', line 250 def legacy_stable_id_for(mutant) return if mutant.respond_to?(:precomputed_stable_id) && mutant.precomputed_stable_id MutantIdentity.legacy_stable_id(mutant) end |
#line_column(mutant, prefix) ⇒ Hash[Symbol, Integer]
222 223 224 225 226 227 228 |
# File 'lib/henitai/result.rb', line 222 def line_column(mutant, prefix) # Stryker schema columns are 1-based; Parser locations are 0-based. { line: mutant.location.fetch(:"#{prefix}_line"), column: mutant.location.fetch(:"#{prefix}_col") + 1 } end |
#location_for(mutant) ⇒ Hash[Symbol, untyped]
215 216 217 218 219 220 |
# File 'lib/henitai/result.rb', line 215 def location_for(mutant) { start: line_column(mutant, :start), end: line_column(mutant, :end) } end |
#mutant_to_schema(mutant) ⇒ Hash[Symbol, untyped]
183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 |
# File 'lib/henitai/result.rb', line 183 def mutant_to_schema(mutant) { id: mutant.id, stableId: mutant.stable_id, legacyStableId: legacy_stable_id_for(mutant), mutatorName: mutant.operator, replacement: replacement_for(mutant), location: location_for(mutant), status: stryker_status(mutant.status), statusReason: status_reason_for(mutant), fromCache: from_cache_for(mutant), description: mutant.description, duration: duration_for(mutant) }.compact.merge(coverage_schema(mutant)) end |
#mutation_score ⇒ Float?
Mutation Score (MS) — Architektur-Formel aus Abschnitt 6.1:
MS = detected / (total − ignored − no_coverage − compile_error − equivalent)
Confirmed equivalent mutants werden aus BEIDEN Seiten der Gleichung entfernt: Sie sind weder im Zähler (nicht detektierbar) noch im Nenner (nicht testbar). Das ist der entscheidende Unterschied zum MSI.
76 77 78 |
# File 'lib/henitai/result.rb', line 76 def mutation_score mutation_score_for(mutants) end |
#mutation_score_for(list) ⇒ Float?
144 145 146 147 148 149 150 |
# File 'lib/henitai/result.rb', line 144 def mutation_score_for(list) excluded = %i[ignored no_coverage compile_error equivalent] valid = list.reject { |m| excluded.include?(m.status) } return nil if valid.empty? ((detected_in(list).to_f / valid.count) * 100.0).round(2).to_f end |
#mutation_score_indicator ⇒ Float?
Mutation Score Indicator (MSI) — naive Berechnung ohne Äquivalenz-Bereinigung:
MSI = killed / all_mutants
MSI ist immer ≤ MS. Der Unterschied kommuniziert die Äquivalenz-Unsicherheit. Beide Werte MÜSSEN im Report zusammen ausgewiesen werden (Anti-Pattern: nur MS).
88 89 90 |
# File 'lib/henitai/result.rb', line 88 def mutation_score_indicator mutation_score_indicator_for(mutants) end |
#mutation_score_indicator_for(list) ⇒ Float?
152 153 154 155 156 |
# File 'lib/henitai/result.rb', line 152 def mutation_score_indicator_for(list) return nil if list.empty? ((list.count(&:killed?).to_f / list.count) * 100.0).round(2).to_f end |
#partial_rerun? ⇒ Boolean
rubocop:enable Metrics/ParameterLists
49 |
# File 'lib/henitai/result.rb', line 49 def partial_rerun? = @partial_rerun |
#replacement_for(mutant) ⇒ String
209 210 211 212 213 |
# File 'lib/henitai/result.rb', line 209 def replacement_for(mutant) return mutant.mutated_source if mutant.respond_to?(:mutated_source) safe_unparse(mutant.mutated_node) end |
#scoring_summary ⇒ Hash[Symbol, untyped]
Compact public summary for reporters. The uncertainty note is intentionally qualitative: equivalent mutants are a known gray area, so the terminal report should communicate that uncertainty instead of pretending to be precise.
112 113 114 115 116 117 118 |
# File 'lib/henitai/result.rb', line 112 def scoring_summary { mutation_score: mutation_score, mutation_score_indicator: mutation_score_indicator, equivalence_uncertainty: equivalence_uncertainty } end |
#status_reason_for(mutant) ⇒ String?
Serialized as the schema's statusReason so directive reasons show up next to the Ignored status in the HTML report.
236 237 238 239 240 |
# File 'lib/henitai/result.rb', line 236 def status_reason_for(mutant) return nil unless mutant.respond_to?(:ignore_reason) mutant.ignore_reason end |
#stryker_status(status) ⇒ String
262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 |
# File 'lib/henitai/result.rb', line 262 def stryker_status(status) # :equivalent wird als "Ignored" serialisiert — das Stryker-Schema kennt keinen # Equivalent-Status. Die interne Unterscheidung (für MS vs. MSI) bleibt im Result-Objekt. { killed: "Killed", survived: "Survived", timeout: "Timeout", no_coverage: "NoCoverage", ignored: "Ignored", equivalent: "Ignored", compile_error: "CompileError", runtime_error: "RuntimeError", pending: "Pending" }.fetch(status, "Pending") end |
#survived ⇒ Integer
Returns number of survived mutants.
56 |
# File 'lib/henitai/result.rb', line 56 def survived = mutants.count(&:survived?) |
#to_stryker_schema ⇒ Hash
Serialise to Stryker mutation-testing-report-schema JSON (schema 1.0).
127 128 129 130 131 132 133 134 135 136 |
# File 'lib/henitai/result.rb', line 127 def to_stryker_schema schema = base_schema sha = @git_sha schema[:gitSha] = sha if sha return schema unless partial_rerun? schema[:partialRerun] = true schema[:unmatchedSurvivorIds] = unmatched_survivor_ids schema end |
#unmatched_survivor_ids ⇒ Array[String]
167 168 169 170 171 |
# File 'lib/henitai/result.rb', line 167 def unmatched_survivor_ids return survivor_stats.fetch(:unmatched_ids) if survivor_stats [] # : Array[String] end |