diff --git a/Changelog.md b/Changelog.md index 15abe67e7..3593b10b9 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,3 +1,11 @@ +# v0.11.17 [unreleased] + +* [#1352](https://github.com/mbj/mutant/pull/1352) + + Add execution profiling via `--profile`. The output format is not stable + yet, still this profile is very useful to differentiate mutant load time from + target application load time. + # v0.11.16 2022-09-11 * [#1355](https://github.com/mbj/mutant/pull/1355) diff --git a/bin/mutant b/bin/mutant index a4cae880b..9a5449261 100755 --- a/bin/mutant +++ b/bin/mutant @@ -1,55 +1,64 @@ #!/usr/bin/env ruby # frozen_string_literal: true -trap('INT') do |status| - effective_status = status ? status + 128 : 128 - exit! effective_status -end +module Mutant + # Record executable timestamp + @executable_timestamp = Process.clock_gettime(Process::CLOCK_MONOTONIC) + + trap('INT') do |status| + effective_status = status ? status + 128 : 128 + exit! effective_status + end -require 'mutant' + require 'mutant' -Mutant::CLI.parse( - arguments: ARGV, - world: Mutant::WORLD -).either( - ->(message) { Mutant::WORLD.stderr.puts(message); Kernel.exit(false) }, - # rubocop:disable Metrics/BlockLength - lambda do |command| - status = + WORLD.record(:cli_parse) do + CLI.parse( + arguments: ARGV, + world: Mutant::WORLD + ) + end.either( + ->(message) { Mutant::WORLD.stderr.puts(message); Kernel.exit(false) }, + # rubocop:disable Metrics/BlockLength + lambda do |command| if command.zombie? - $stderr.puts('Running mutant zombified!') - Mutant::Zombifier.call( - namespace: :Zombie, - load_path: $LOAD_PATH, - kernel: Kernel, - pathname: Pathname, - require_highjack: Mutant::RequireHighjack - .public_method(:call) - .to_proc - .curry - .call(Kernel), - root_require: 'mutant', - includes: %w[ - adamantium - anima - concord - equalizer - mprelude - mutant - unparser - variable - ] - ) + command = WORLD.record(:zombify) do + $stderr.puts('Running mutant zombified!') + Zombifier.call( + namespace: :Zombie, + load_path: $LOAD_PATH, + kernel: Kernel, + pathname: Pathname, + require_highjack: RequireHighjack + .public_method(:call) + .to_proc + .curry + .call(Kernel), + root_require: 'mutant', + includes: %w[ + adamantium + anima + concord + equalizer + mprelude + mutant + unparser + variable + ] + ) - Zombie::Mutant::CLI.parse( - arguments: ARGV, - world: Zombie::Mutant::WORLD - ).from_right.call - else - command.call + Zombie::Mutant::CLI.parse( + arguments: ARGV, + world: Mutant::WORLD + ).from_right + end end - Kernel.exit(status) - end - # rubocop:enable Metrics/BlockLength -) + WORLD.record(:execute) { command.call }.tap do |status| + WORLD.recorder.print_profile(WORLD.stderr) if command.print_profile? + WORLD.kernel.exit(status) + end + end + # rubocop:enable Metrics/BlockLength + ) +end diff --git a/lib/mutant.rb b/lib/mutant.rb index 7cd054c34..5c1109bbe 100644 --- a/lib/mutant.rb +++ b/lib/mutant.rb @@ -1,32 +1,54 @@ # frozen_string_literal: true -require 'diff/lcs' -require 'diff/lcs/hunk' -require 'digest/sha1' -require 'etc' -require 'irb' -require 'json' -require 'open3' -require 'optparse' -require 'parser' -require 'parser/current' -require 'pathname' -require 'regexp_parser' -require 'set' -require 'singleton' -require 'sorbet-runtime' -require 'stringio' -require 'unparser' -require 'yaml' - -# This setting is done to make errors within the parallel -# reporter / execution visible in the main thread. -Thread.abort_on_exception = true - # Library namespace # # @api private +# rubocop:disable Metrics/ModuleLength module Mutant + # Boot timing infrastructure + get_timestamp = lambda do + Process.clock_gettime(Process::CLOCK_MONOTONIC) + end + + library_timestamp = get_timestamp.call + boot_events = [] + + record = lambda do |name, &block| + start = get_timestamp.call + + block.call.tap do + boot_events << [name, start, get_timestamp.call] + end + end + + record.call(:require_dependencies) do + %w[ + diff/lcs + diff/lcs/hunk + digest/sha1 + etc + irb + json + open3 + optparse + parser + parser/current + pathname + regexp_parser + securerandom + set + singleton + sorbet-runtime + stringio + unparser + yaml + ].each { |name| require(name) } + end + + # This setting is done to make errors within the parallel + # reporter / execution visible in the main thread. + Thread.abort_on_exception = true + AbstractType = Unparser::AbstractType Adamantium = Unparser::Adamantium Anima = Unparser::Anima @@ -43,211 +65,264 @@ module Mutant ENV_VARIABLE_KEY_VALUE_REGEXP = /\A(?#{env_key}+)=(?.*)\z/.freeze ENV_VARIABLE_KEY_REGEXP = /\A#{env_key}\z/.freeze -end # Mutant -require 'mutant/procto' -require 'mutant/transform' -require 'mutant/variable' -require 'mutant/bootstrap' -require 'mutant/version' -require 'mutant/env' -require 'mutant/pipe' -require 'mutant/util' -require 'mutant/registry' -require 'mutant/ast' -require 'mutant/ast/sexp' -require 'mutant/ast/types' -require 'mutant/ast/nodes' -require 'mutant/ast/named_children' -require 'mutant/ast/node_predicates' -require 'mutant/ast/find_metaclass_containing' -require 'mutant/ast/regexp' -require 'mutant/ast/regexp/transformer' -require 'mutant/ast/regexp/transformer/direct' -require 'mutant/ast/regexp/transformer/named_group' -require 'mutant/ast/regexp/transformer/options_group' -require 'mutant/ast/regexp/transformer/quantifier' -require 'mutant/ast/regexp/transformer/recursive' -require 'mutant/ast/regexp/transformer/root' -require 'mutant/ast/regexp/transformer/text' -require 'mutant/ast/meta' -require 'mutant/ast/meta/send' -require 'mutant/ast/meta/const' -require 'mutant/ast/meta/symbol' -require 'mutant/ast/meta/optarg' -require 'mutant/ast/meta/resbody' -require 'mutant/ast/pattern' -require 'mutant/ast/pattern/lexer' -require 'mutant/ast/pattern/parser' -require 'mutant/ast/pattern/source' -require 'mutant/ast/pattern/token' -require 'mutant/ast/structure' -require 'mutant/parser' -require 'mutant/isolation' -require 'mutant/isolation/exception' -require 'mutant/isolation/fork' -require 'mutant/isolation/none' -require 'mutant/parallel' -require 'mutant/parallel/driver' -require 'mutant/parallel/source' -require 'mutant/parallel/worker' -require 'mutant/require_highjack' -require 'mutant/mutation' -require 'mutant/mutation/config' -require 'mutant/mutator' -require 'mutant/mutator/util' -require 'mutant/mutator/util/array' -require 'mutant/mutator/util/symbol' -require 'mutant/mutator/node' -require 'mutant/mutator/node/generic' -require 'mutant/mutator/node/regexp' -require 'mutant/mutator/node/regexp/alternation_meta' -require 'mutant/mutator/node/regexp/beginning_of_line_anchor' -require 'mutant/mutator/node/regexp/capture_group' -require 'mutant/mutator/node/regexp/named_group' -require 'mutant/mutator/node/regexp/character_type' -require 'mutant/mutator/node/regexp/end_of_line_anchor' -require 'mutant/mutator/node/regexp/end_of_string_or_before_end_of_line_anchor' -require 'mutant/mutator/node/regexp/zero_or_more' -require 'mutant/mutator/node/literal' -require 'mutant/mutator/node/literal/boolean' -require 'mutant/mutator/node/literal/range' -require 'mutant/mutator/node/literal/symbol' -require 'mutant/mutator/node/literal/string' -require 'mutant/mutator/node/literal/integer' -require 'mutant/mutator/node/literal/float' -require 'mutant/mutator/node/literal/array' -require 'mutant/mutator/node/literal/hash' -require 'mutant/mutator/node/literal/regex' -require 'mutant/mutator/node/literal/nil' -require 'mutant/mutator/node/argument' -require 'mutant/mutator/node/arguments' -require 'mutant/mutator/node/begin' -require 'mutant/mutator/node/binary' -require 'mutant/mutator/node/const' -require 'mutant/mutator/node/dynamic_literal' -require 'mutant/mutator/node/kwbegin' -require 'mutant/mutator/node/named_value/access' -require 'mutant/mutator/node/named_value/constant_assignment' -require 'mutant/mutator/node/named_value/variable_assignment' -require 'mutant/mutator/node/next' -require 'mutant/mutator/node/break' -require 'mutant/mutator/node/noop' -require 'mutant/mutator/node/or_asgn' -require 'mutant/mutator/node/and_asgn' -require 'mutant/mutator/node/defined' -require 'mutant/mutator/node/op_asgn' -require 'mutant/mutator/node/conditional_loop' -require 'mutant/mutator/node/yield' -require 'mutant/mutator/node/super' -require 'mutant/mutator/node/zsuper' -require 'mutant/mutator/node/send' -require 'mutant/mutator/node/send/binary' -require 'mutant/mutator/node/send/conditional' -require 'mutant/mutator/node/send/attribute_assignment' -require 'mutant/mutator/node/when' -require 'mutant/mutator/node/class' -require 'mutant/mutator/node/sclass' -require 'mutant/mutator/node/define' -require 'mutant/mutator/node/mlhs' -require 'mutant/mutator/node/nthref' -require 'mutant/mutator/node/masgn' -require 'mutant/mutator/node/module' -require 'mutant/mutator/node/return' -require 'mutant/mutator/node/block' -require 'mutant/mutator/node/block_pass' -require 'mutant/mutator/node/if' -require 'mutant/mutator/node/case' -require 'mutant/mutator/node/splat' -require 'mutant/mutator/node/regopt' -require 'mutant/mutator/node/resbody' -require 'mutant/mutator/node/rescue' -require 'mutant/mutator/node/match_current_line' -require 'mutant/mutator/node/index' -require 'mutant/mutator/node/procarg_zero' -require 'mutant/mutator/node/kwargs' -require 'mutant/mutator/node/numblock' -require 'mutant/loader' -require 'mutant/context' -require 'mutant/scope' -require 'mutant/subject' -require 'mutant/subject/config' -require 'mutant/subject/method' -require 'mutant/subject/method/instance' -require 'mutant/subject/method/singleton' -require 'mutant/subject/method/metaclass' -require 'mutant/matcher' -require 'mutant/matcher/chain' -require 'mutant/matcher/config' -require 'mutant/matcher/descendants' -require 'mutant/matcher/filter' -require 'mutant/matcher/method' -require 'mutant/matcher/method/instance' -require 'mutant/matcher/method/metaclass' -require 'mutant/matcher/method/singleton' -require 'mutant/matcher/methods' -require 'mutant/matcher/namespace' -require 'mutant/matcher/null' -require 'mutant/matcher/scope' -require 'mutant/matcher/static' -require 'mutant/expression' -require 'mutant/expression/descendants' -require 'mutant/expression/method' -require 'mutant/expression/methods' -require 'mutant/expression/namespace' -require 'mutant/expression/parser' -require 'mutant/test' -require 'mutant/timer' -require 'mutant/integration' -require 'mutant/integration/null' -require 'mutant/selector' -require 'mutant/selector/expression' -require 'mutant/selector/null' -require 'mutant/world' -require 'mutant/hooks' -require 'mutant/config' -require 'mutant/config/coverage_criteria' -require 'mutant/cli' -require 'mutant/cli/command' -require 'mutant/cli/command/subscription' -require 'mutant/cli/command/environment' -require 'mutant/cli/command/environment/irb' -require 'mutant/cli/command/environment/run' -require 'mutant/cli/command/environment/show' -require 'mutant/cli/command/environment/subject' -require 'mutant/cli/command/environment/test' -require 'mutant/cli/command/util' -require 'mutant/cli/command/root' -require 'mutant/runner' -require 'mutant/runner/sink' -require 'mutant/result' -require 'mutant/reporter' -require 'mutant/reporter/null' -require 'mutant/reporter/sequence' -require 'mutant/reporter/cli' -require 'mutant/reporter/cli/printer' -require 'mutant/reporter/cli/printer/config' -require 'mutant/reporter/cli/printer/coverage_result' -require 'mutant/reporter/cli/printer/env' -require 'mutant/reporter/cli/printer/env_progress' -require 'mutant/reporter/cli/printer/env_result' -require 'mutant/reporter/cli/printer/isolation_result' -require 'mutant/reporter/cli/printer/mutation' -require 'mutant/reporter/cli/printer/mutation_result' -require 'mutant/reporter/cli/printer/status_progressive' -require 'mutant/reporter/cli/printer/subject_result' -require 'mutant/reporter/cli/format' -require 'mutant/repository' -require 'mutant/repository/diff' -require 'mutant/repository/diff/ranges' -require 'mutant/zombifier' -require 'mutant/range' -require 'mutant/license' -require 'mutant/license/subscription' -require 'mutant/license/subscription/opensource' -require 'mutant/license/subscription/commercial' + # rubocop:disable Metrics/BlockLength + record.call(:require_mutant_lib) do + require 'mutant/procto' + require 'mutant/transform' + require 'mutant/variable' + require 'mutant/bootstrap' + require 'mutant/version' + require 'mutant/env' + require 'mutant/pipe' + require 'mutant/util' + require 'mutant/registry' + require 'mutant/ast' + require 'mutant/ast/sexp' + require 'mutant/ast/types' + require 'mutant/ast/nodes' + require 'mutant/ast/named_children' + require 'mutant/ast/node_predicates' + require 'mutant/ast/find_metaclass_containing' + require 'mutant/ast/regexp' + require 'mutant/ast/regexp/transformer' + require 'mutant/ast/regexp/transformer/direct' + require 'mutant/ast/regexp/transformer/named_group' + require 'mutant/ast/regexp/transformer/options_group' + require 'mutant/ast/regexp/transformer/quantifier' + require 'mutant/ast/regexp/transformer/recursive' + require 'mutant/ast/regexp/transformer/root' + require 'mutant/ast/regexp/transformer/text' + require 'mutant/ast/meta' + require 'mutant/ast/meta/send' + require 'mutant/ast/meta/const' + require 'mutant/ast/meta/symbol' + require 'mutant/ast/meta/optarg' + require 'mutant/ast/meta/resbody' + require 'mutant/ast/pattern' + require 'mutant/ast/pattern/lexer' + require 'mutant/ast/pattern/parser' + require 'mutant/ast/pattern/source' + require 'mutant/ast/pattern/token' + require 'mutant/ast/structure' + require 'mutant/parser' + require 'mutant/isolation' + require 'mutant/isolation/exception' + require 'mutant/isolation/fork' + require 'mutant/isolation/none' + require 'mutant/parallel' + require 'mutant/parallel/driver' + require 'mutant/parallel/source' + require 'mutant/parallel/worker' + require 'mutant/require_highjack' + require 'mutant/mutation' + require 'mutant/mutation/config' + require 'mutant/mutator' + require 'mutant/mutator/util' + require 'mutant/mutator/util/array' + require 'mutant/mutator/util/symbol' + require 'mutant/mutator/node' + require 'mutant/mutator/node/generic' + require 'mutant/mutator/node/regexp' + require 'mutant/mutator/node/regexp/alternation_meta' + require 'mutant/mutator/node/regexp/beginning_of_line_anchor' + require 'mutant/mutator/node/regexp/capture_group' + require 'mutant/mutator/node/regexp/named_group' + require 'mutant/mutator/node/regexp/character_type' + require 'mutant/mutator/node/regexp/end_of_line_anchor' + require 'mutant/mutator/node/regexp/end_of_string_or_before_end_of_line_anchor' + require 'mutant/mutator/node/regexp/zero_or_more' + require 'mutant/mutator/node/literal' + require 'mutant/mutator/node/literal/boolean' + require 'mutant/mutator/node/literal/range' + require 'mutant/mutator/node/literal/symbol' + require 'mutant/mutator/node/literal/string' + require 'mutant/mutator/node/literal/integer' + require 'mutant/mutator/node/literal/float' + require 'mutant/mutator/node/literal/array' + require 'mutant/mutator/node/literal/hash' + require 'mutant/mutator/node/literal/regex' + require 'mutant/mutator/node/literal/nil' + require 'mutant/mutator/node/argument' + require 'mutant/mutator/node/arguments' + require 'mutant/mutator/node/begin' + require 'mutant/mutator/node/binary' + require 'mutant/mutator/node/const' + require 'mutant/mutator/node/dynamic_literal' + require 'mutant/mutator/node/kwbegin' + require 'mutant/mutator/node/named_value/access' + require 'mutant/mutator/node/named_value/constant_assignment' + require 'mutant/mutator/node/named_value/variable_assignment' + require 'mutant/mutator/node/next' + require 'mutant/mutator/node/break' + require 'mutant/mutator/node/noop' + require 'mutant/mutator/node/or_asgn' + require 'mutant/mutator/node/and_asgn' + require 'mutant/mutator/node/defined' + require 'mutant/mutator/node/op_asgn' + require 'mutant/mutator/node/conditional_loop' + require 'mutant/mutator/node/yield' + require 'mutant/mutator/node/super' + require 'mutant/mutator/node/zsuper' + require 'mutant/mutator/node/send' + require 'mutant/mutator/node/send/binary' + require 'mutant/mutator/node/send/conditional' + require 'mutant/mutator/node/send/attribute_assignment' + require 'mutant/mutator/node/when' + require 'mutant/mutator/node/class' + require 'mutant/mutator/node/sclass' + require 'mutant/mutator/node/define' + require 'mutant/mutator/node/mlhs' + require 'mutant/mutator/node/nthref' + require 'mutant/mutator/node/masgn' + require 'mutant/mutator/node/module' + require 'mutant/mutator/node/return' + require 'mutant/mutator/node/block' + require 'mutant/mutator/node/block_pass' + require 'mutant/mutator/node/if' + require 'mutant/mutator/node/case' + require 'mutant/mutator/node/splat' + require 'mutant/mutator/node/regopt' + require 'mutant/mutator/node/resbody' + require 'mutant/mutator/node/rescue' + require 'mutant/mutator/node/match_current_line' + require 'mutant/mutator/node/index' + require 'mutant/mutator/node/procarg_zero' + require 'mutant/mutator/node/kwargs' + require 'mutant/mutator/node/numblock' + require 'mutant/loader' + require 'mutant/context' + require 'mutant/scope' + require 'mutant/subject' + require 'mutant/subject/config' + require 'mutant/subject/method' + require 'mutant/subject/method/instance' + require 'mutant/subject/method/singleton' + require 'mutant/subject/method/metaclass' + require 'mutant/matcher' + require 'mutant/matcher/chain' + require 'mutant/matcher/config' + require 'mutant/matcher/descendants' + require 'mutant/matcher/filter' + require 'mutant/matcher/method' + require 'mutant/matcher/method/instance' + require 'mutant/matcher/method/metaclass' + require 'mutant/matcher/method/singleton' + require 'mutant/matcher/methods' + require 'mutant/matcher/namespace' + require 'mutant/matcher/null' + require 'mutant/matcher/scope' + require 'mutant/matcher/static' + require 'mutant/expression' + require 'mutant/expression/descendants' + require 'mutant/expression/method' + require 'mutant/expression/methods' + require 'mutant/expression/namespace' + require 'mutant/expression/parser' + require 'mutant/test' + require 'mutant/timer' + require 'mutant/integration' + require 'mutant/integration/null' + require 'mutant/selector' + require 'mutant/selector/expression' + require 'mutant/selector/null' + require 'mutant/world' + require 'mutant/hooks' + require 'mutant/config' + require 'mutant/config/coverage_criteria' + require 'mutant/cli' + require 'mutant/cli/command' + require 'mutant/cli/command/subscription' + require 'mutant/cli/command/environment' + require 'mutant/cli/command/environment/irb' + require 'mutant/cli/command/environment/run' + require 'mutant/cli/command/environment/show' + require 'mutant/cli/command/environment/subject' + require 'mutant/cli/command/environment/test' + require 'mutant/cli/command/util' + require 'mutant/cli/command/root' + require 'mutant/runner' + require 'mutant/runner/sink' + require 'mutant/result' + require 'mutant/reporter' + require 'mutant/reporter/null' + require 'mutant/reporter/sequence' + require 'mutant/reporter/cli' + require 'mutant/reporter/cli/printer' + require 'mutant/reporter/cli/printer/config' + require 'mutant/reporter/cli/printer/coverage_result' + require 'mutant/reporter/cli/printer/env' + require 'mutant/reporter/cli/printer/env_progress' + require 'mutant/reporter/cli/printer/env_result' + require 'mutant/reporter/cli/printer/isolation_result' + require 'mutant/reporter/cli/printer/mutation' + require 'mutant/reporter/cli/printer/mutation_result' + require 'mutant/reporter/cli/printer/status_progressive' + require 'mutant/reporter/cli/printer/subject_result' + require 'mutant/reporter/cli/format' + require 'mutant/repository' + require 'mutant/repository/diff' + require 'mutant/repository/diff/ranges' + require 'mutant/zombifier' + require 'mutant/range' + require 'mutant/license' + require 'mutant/license/subscription' + require 'mutant/license/subscription/opensource' + require 'mutant/license/subscription/commercial' + require 'mutant/segment' + require 'mutant/segment/recorder' + end + # rubocop:enable Metrics/BlockLength + + gen_id = SecureRandom.method(:uuid) + + # Transform boot events into segments + if instance_variable_defined?(:@executable_timestamp) + recording_start = @executable_timestamp + + executable_segment = + Segment.new( + id: gen_id.call, + name: :executable, + parent_id: nil, + timestamp_end: nil, + timestamp_start: @executable_timestamp + ) + + remove_instance_variable(:@executable_timestamp) + else + recording_start = library_timestamp + end + + library_segment = Segment.new( + id: gen_id.call, + name: :library, + parent_id: executable_segment&.id, + timestamp_end: nil, + timestamp_start: library_timestamp + ) + + boot_segments = boot_events.map do |name, timestamp_start, timestamp_end| + Segment.new( + id: gen_id.call, + name: name, + parent_id: library_segment.id, + timestamp_end: timestamp_end, + timestamp_start: timestamp_start + ) + end + + timer = Timer.new(Process) + + recorder = Segment::Recorder.new( + gen_id: gen_id, + root_id: (executable_segment || library_segment).id, + parent_id: library_segment.id, + recording_start: recording_start, + segments: [*executable_segment, library_segment, *boot_segments], + timer: timer + ) -module Mutant WORLD = World.new( condition_variable: ConditionVariable, environment_variables: ENV, @@ -264,10 +339,11 @@ module Mutant pathname: Pathname, process: Process, random: Random, + recorder: recorder, stderr: $stderr, stdout: $stdout, thread: Thread, - timer: Timer.new(Process) + timer: timer ) # Reopen class to initialize constant to avoid dep circle @@ -281,8 +357,8 @@ class Config Expression::Namespace::Exact, Expression::Namespace::Recursive ]), - fail_fast: false, environment_variables: EMPTY_HASH, + fail_fast: false, hooks: EMPTY_ARRAY, includes: EMPTY_ARRAY, integration: nil, @@ -308,3 +384,4 @@ def self.traverse(action, values) ) end end # Mutant +# rubocop:enable Metrics/ModuleLength diff --git a/lib/mutant/bootstrap.rb b/lib/mutant/bootstrap.rb index 16286455f..0f044460f 100644 --- a/lib/mutant/bootstrap.rb +++ b/lib/mutant/bootstrap.rb @@ -7,6 +7,8 @@ module Mutant # the impure world to produce an environment. # # env = config interpreted against the world + # + # rubocop:disable Metrics/ModuleLength module Bootstrap include Adamantium, Anima.new(:config, :parser, :world) @@ -30,66 +32,94 @@ module Bootstrap # # rubocop:disable Metrics/MethodLength def self.call(env) - env = load_hooks(env) - .tap(&method(:infect)) - .with(matchable_scopes: matchable_scopes(env)) - - subjects = start_subject(env, Matcher.from_config(env.config.matcher).call(env)) - - Integration.setup(env).fmap do |integration| - env.with( - integration: integration, - mutations: subjects.flat_map(&:mutations), - selector: Selector::Expression.new(integration), - subjects: subjects - ) + env.record(:bootstrap) do + env = load_hooks(env) + .tap(&method(:infect)) + .with(matchable_scopes: matchable_scopes(env)) + + matched_subjects = env.record(:subject_match) do + Matcher.from_config(env.config.matcher).call(env) + end + + selected_subjects = subject_select(env, matched_subjects) + + mutations = env.record(:mutation_generate) do + selected_subjects.flat_map(&:mutations) + end + + Integration.setup(env).fmap do |integration| + env.with( + integration: integration, + mutations: mutations, + selector: Selector::Expression.new(integration), + subjects: selected_subjects + ) + end end end # rubocop:enable Metrics/MethodLength def self.load_hooks(env) - env.with(hooks: Hooks.load_config(env.config)) + env.record(__method__) do + env.with(hooks: Hooks.load_config(env.config)) + end end private_class_method :load_hooks - def self.start_subject(env, subjects) - start_expressions = env.config.matcher.start_expressions + def self.subject_select(env, subjects) + env.record(__method__) do + start_expressions = env.config.matcher.start_expressions - return subjects if start_expressions.empty? + return subjects if start_expressions.empty? - subjects.drop_while do |subject| - start_expressions.none? do |expression| - expression.prefix?(subject.expression) + subjects.drop_while do |subject| + start_expressions.none? do |expression| + expression.prefix?(subject.expression) + end end end end - private_class_method :start_subject + private_class_method :subject_select + # rubocop:disable Metrics/AbcSize + # rubocop:disable Metrics/MethodLength def self.infect(env) - config, hooks, world = env.config, env.hooks, env.world + env.record(__method__) do + config, hooks, world = env.config, env.hooks, env.world - hooks.run(:env_infection_pre, env) + env.record(:hooks_env_infection_pre) do + hooks.run(:env_infection_pre, env) + end - config.environment_variables.each do |key, value| - world.environment_variables[key] = value - end + env.record(:require_target) do + config.environment_variables.each do |key, value| + world.environment_variables[key] = value + end - config.includes.each(&world.load_path.public_method(:<<)) - config.requires.each(&world.kernel.public_method(:require)) + config.includes.each(&world.load_path.public_method(:<<)) + config.requires.each(&world.kernel.public_method(:require)) + end - hooks.run(:env_infection_post, env) + env.record(:hooks_env_infection_post) do + hooks.run(:env_infection_post, env) + end + end end private_class_method :infect + # rubocop:enable Metrics/AbcSize + # rubocop:enable Metrics/MethodLength def self.matchable_scopes(env) - config = env.config + env.record(__method__) do + config = env.config - scopes = env.world.object_space.each_object(Module).each_with_object([]) do |scope, aggregate| - expression = expression(config.reporter, config.expression_parser, scope) || next - aggregate << Scope.new(scope, expression) - end + scopes = env.world.object_space.each_object(Module).each_with_object([]) do |scope, aggregate| + expression = expression(config.reporter, config.expression_parser, scope) || next + aggregate << Scope.new(scope, expression) + end - scopes.sort_by { |scope| scope.expression.syntax } + scopes.sort_by { |scope| scope.expression.syntax } + end end private_class_method :matchable_scopes @@ -132,4 +162,5 @@ def self.semantics_warning(reporter, format, options) end private_class_method :semantics_warning end # Bootstrap + # rubocop:enable Metrics/ModuleLength end # Mutant diff --git a/lib/mutant/cli/command.rb b/lib/mutant/cli/command.rb index 4ec0e69f0..fb278c7aa 100644 --- a/lib/mutant/cli/command.rb +++ b/lib/mutant/cli/command.rb @@ -4,7 +4,13 @@ module Mutant module CLI # rubocop:disable Metrics/ClassLength class Command - include AbstractType, Anima.new(:world, :main, :parent, :zombie) + include AbstractType, Anima.new( + :main, + :parent, + :print_profile, + :world, + :zombie + ) OPTIONS = [].freeze SUBCOMMANDS = [].freeze @@ -21,12 +27,13 @@ class OptionParser < ::OptionParser # @return [Command] # # rubocop:disable Metrics/ParameterLists - def self.parse(arguments:, parent: nil, world:, zombie: false) + def self.parse(arguments:, parent: nil, print_profile: false, world:, zombie: false) new( - main: nil, - parent: parent, - world: world, - zombie: zombie + main: nil, + parent: parent, + print_profile: print_profile, + world: world, + zombie: zombie ).__send__(:parse, arguments) end # rubocop:enable Metrics/ParameterLists @@ -59,7 +66,8 @@ def full_name [*parent&.full_name, self.class.command_name].join(' ') end - alias_method :zombie?, :zombie + alias_method :print_profile?, :print_profile + alias_method :zombie?, :zombie abstract_method :action @@ -136,6 +144,10 @@ def add_global_options(parser) capture_main { world.stdout.puts("mutant-#{VERSION}"); true } end + parser.on('--profile', 'Profile mutant execution') do + @print_profile = true + end + parser.on('--zombie', 'Run mutant zombified') do @zombie = true end @@ -176,10 +188,11 @@ def parse_subcommand(arguments) else find_command(command_name).bind do |command| command.parse( - arguments: arguments, - parent: self, - world: world, - zombie: zombie + arguments: arguments, + parent: self, + print_profile: print_profile, + world: world, + zombie: zombie ) end end diff --git a/lib/mutant/cli/command/environment.rb b/lib/mutant/cli/command/environment.rb index 190117359..a10194c26 100644 --- a/lib/mutant/cli/command/environment.rb +++ b/lib/mutant/cli/command/environment.rb @@ -27,8 +27,8 @@ def initialize(attributes) def bootstrap env = Env.empty(world, @config) - Config.load_config_file(env) - .fmap(&method(:expand)) + env + .record(:config) { Config.load_config_file(env).fmap(&method(:expand)) } .bind { Bootstrap.call(env.with(config: @config)) } end diff --git a/lib/mutant/env.rb b/lib/mutant/env.rb index f53adf47f..abc17dc68 100644 --- a/lib/mutant/env.rb +++ b/lib/mutant/env.rb @@ -134,6 +134,15 @@ def test_subject_ratio end memoize :test_subject_ratio + # Record segment + # + # @param [Symbol] name + # + # @return [self] + def record(name, &block) + world.record(name, &block) + end + private def run_mutation_tests(mutation, tests) diff --git a/lib/mutant/runner.rb b/lib/mutant/runner.rb index cbf2db760..8bf281206 100644 --- a/lib/mutant/runner.rb +++ b/lib/mutant/runner.rb @@ -15,15 +15,17 @@ def self.call(env) def self.run_mutation_analysis(env) reporter = reporter(env) - run_driver( - reporter, - Parallel.async(env.world, mutation_test_config(env)) - ).tap do |result| - reporter.report(result) - end + env + .record(:analysis) { run_driver(reporter, async_driver(env)) } + .tap { |result| env.record(:report) { reporter.report(result) } } end private_class_method :run_mutation_analysis + def self.async_driver(env) + Parallel.async(env.world, mutation_test_config(env)) + end + private_class_method :async_driver + def self.run_driver(reporter, driver) Signal.trap('INT') do driver.stop diff --git a/lib/mutant/segment.rb b/lib/mutant/segment.rb new file mode 100644 index 000000000..723d84b07 --- /dev/null +++ b/lib/mutant/segment.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +module Mutant + class Segment + include Adamantium, Anima.new( + :id, + :name, + :parent_id, + :timestamp_end, + :timestamp_start + ) + + def elapsed + timestamp_end - timestamp_start + end + + def offset_start(recording_start) + timestamp_start - recording_start + end + + def offset_end(recording_start) + timestamp_end - recording_start + end + end # Segment +end # Mutant diff --git a/lib/mutant/segment/recorder.rb b/lib/mutant/segment/recorder.rb new file mode 100644 index 000000000..ae486f7dd --- /dev/null +++ b/lib/mutant/segment/recorder.rb @@ -0,0 +1,124 @@ +# frozen_string_literal: true + +module Mutant + class Segment + class Recorder + include Anima.new( + :gen_id, + :parent_id, + :recording_start, + :root_id, + :segments, + :timer + ) + + private(*anima.attribute_names) + + # rubocop:disable Metrics/MethodLength + def record(name) + start = timer.now + parent_id = parent_id() + + @parent_id = id = gen_id.call + + yield.tap do + segments << Segment.new( + id: id, + name: name, + parent_id: parent_id, + timestamp_end: timer.now, + timestamp_start: start + ) + end + ensure + @parent_id = parent_id + end + # rubocop:enable Metrics/MethodLength + + def print_profile(io) + print_node(io, tree, 0) + end + + private + + class Node + include Adamantium, Anima.new(:value, :children) + end + private_constant :Node + + def tree + id_index = {} + parent_index = {} + + final_segments.each do |segment| + id_index[segment.id] = segment + + (parent_index[segment.parent_id] ||= []) << segment + end + + build_node( + value: id_index.fetch(root_id), + parent_index: parent_index + ) + end + + def final_segments + timestamp_end = timer.now + + segments.map do |segment| + if segment.timestamp_end + segment + else + segment.with(timestamp_end: timestamp_end) + end + end + end + + def build_node(value:, parent_index:) + Node.new( + value: value, + children: build_children( + parent_id: value.id, + parent_index: parent_index + ) + ) + end + + def build_children(parent_id:, parent_index:) + parent_index + .fetch(parent_id, EMPTY_ARRAY) + .map { |value| build_node(value: value, parent_index: parent_index) } + end + + def print_node(io, node, indent) + segment = node.value + + indent_str = ' ' * indent + + print_line(io, :offset_start, segment, indent_str) + + return unless node.children.any? + + node.children.each do |child| + print_node(io, child, indent.succ) + end + print_line(io, :offset_end, segment, indent_str) + end + + # rubocop:disable Metrics/ParameterLists + # rubocop:disable Style/FormatStringToken + def print_line(io, offset, segment, indent_str) + io.puts( + '%4.4f: (%4.4fs) %s %s' % [ + segment.public_send(offset, recording_start), + segment.elapsed, + indent_str, + segment.name + ] + ) + end + # rubocop:enable Metrics/ParameterLists + # rubocop:enable Style/FormatStringToken + end # Recorder + end # Segment +end # Mutant diff --git a/lib/mutant/world.rb b/lib/mutant/world.rb index 816e42d4d..2677735eb 100644 --- a/lib/mutant/world.rb +++ b/lib/mutant/world.rb @@ -19,6 +19,7 @@ class World :pathname, :process, :random, + :recorder, :stderr, :stdout, :thread, @@ -77,5 +78,9 @@ def deadline(allowed_time) Timer::Deadline::None.new end end + + def record(name, &block) + recorder.record(name, &block) + end end # World end # Mutant diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index e046af7ca..0d07aafcb 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -37,8 +37,29 @@ module Fixtures reporter: Mutant::Reporter::Null.new ) + id = 0 + + gen_id = -> { (id += 1).to_s } + + root_segment = Mutant::Segment.new( + id: 0, + name: :spec, + parent_id: nil, + timestamp_end: nil, + timestamp_start: 0 + ) + + recorder = Mutant::Segment::Recorder.new( + gen_id: gen_id, + root_id: root_segment.id, + parent_id: root_segment.id, + recording_start: 0, + segments: [root_segment], + timer: Mutant::WORLD.timer + ) + TEST_ENV = Mutant::Bootstrap - .call(Mutant::Env.empty(Mutant::WORLD, test_config)) + .call(Mutant::Env.empty(Mutant::WORLD.with(recorder: recorder), test_config)) .from_right end # Fixtures @@ -95,10 +116,11 @@ def fake_world open3: class_double(Open3), pathname: class_double(Pathname), process: class_double(Process), + random: class_double(Random), + recorder: instance_double(Mutant::Segment::Recorder), stderr: instance_double(IO), stdout: instance_double(IO), thread: class_double(Thread), - random: class_double(Random), timer: instance_double(Mutant::Timer) ) end diff --git a/spec/unit/mutant/bootstrap_spec.rb b/spec/unit/mutant/bootstrap_spec.rb index 1f9905385..1c77ab7cc 100644 --- a/spec/unit/mutant/bootstrap_spec.rb +++ b/spec/unit/mutant/bootstrap_spec.rb @@ -57,6 +57,7 @@ def require(_); end load_path: load_path, object_space: object_space, pathname: Pathname, + recorder: instance_double(Mutant::Segment::Recorder), timer: timer ) end @@ -71,17 +72,47 @@ def require(_); end let(:raw_expectations) do [ + { + receiver: world, + selector: :record, + arguments: [:bootstrap], + reaction: { yields: [] } + }, + { + receiver: world, + selector: :record, + arguments: [:load_hooks], + reaction: { yields: [] } + }, { receiver: Mutant::Hooks, selector: :load_config, arguments: [config], reaction: { return: hooks } }, + { + receiver: world, + selector: :record, + arguments: [:infect], + reaction: { yields: [] } + }, + { + receiver: world, + selector: :record, + arguments: [:hooks_env_infection_pre], + reaction: { yields: [] } + }, { receiver: hooks, selector: :run, arguments: [:env_infection_pre, env_initial] }, + { + receiver: world, + selector: :record, + arguments: [:require_target], + reaction: { yields: [] } + }, { receiver: world.environment_variables, selector: :[]=, @@ -107,11 +138,23 @@ def require(_); end selector: :require, arguments: %w[require-b] }, + { + receiver: world, + selector: :record, + arguments: [:hooks_env_infection_post], + reaction: { yields: [] } + }, { receiver: hooks, selector: :run, arguments: [:env_infection_post, env_initial] }, + { + receiver: world, + selector: :record, + arguments: [:matchable_scopes], + reaction: { yields: [] } + }, { receiver: object_space, selector: :each_object, @@ -119,6 +162,24 @@ def require(_); end reaction: { return: object_space_modules.each } }, *match_warnings, + { + receiver: world, + selector: :record, + arguments: [:subject_match], + reaction: { yields: [] } + }, + { + receiver: world, + selector: :record, + arguments: [:subject_select], + reaction: { yields: [] } + }, + { + receiver: world, + selector: :record, + arguments: [:mutation_generate], + reaction: { yields: [] } + }, { receiver: Mutant::Integration, selector: :setup, diff --git a/spec/unit/mutant/cli_spec.rb b/spec/unit/mutant/cli_spec.rb index 7bd2fa028..611fa5fc8 100644 --- a/spec/unit/mutant/cli_spec.rb +++ b/spec/unit/mutant/cli_spec.rb @@ -4,6 +4,7 @@ describe '.parse' do let(:env_config) { Mutant::Config::DEFAULT.with(jobs: 4) } let(:events) { [] } + let(:expected_print_profile) { false } let(:expected_zombie) { false } let(:kernel) { class_double(Kernel) } let(:stderr) { instance_double(IO, :stderr, tty?: false) } @@ -13,10 +14,11 @@ let(:world) do instance_double( Mutant::World, - kernel: kernel, - stderr: stderr, - stdout: stdout, - timer: timer + kernel: kernel, + recorder: instance_double(Mutant::Segment::Recorder), + stderr: stderr, + stdout: stdout, + timer: timer ) end @@ -60,6 +62,10 @@ def apply expect(apply.from_right.call).to be(expected_exit) end + it 'sets expected print_profile flag' do + expect(apply.from_right.print_profile?).to be(expected_print_profile) + end + it 'sets expected zombie flag' do expect(apply.from_right.zombie?).to be(expected_zombie) end @@ -82,7 +88,13 @@ def apply @test_klass = Class.new do - include Unparser::Anima.new(:arguments, :expected_exit, :expected_events, :expected_zombie) + include Unparser::Anima.new( + :arguments, + :expected_events, + :expected_exit, + :expected_print_profile, + :expected_zombie + ) end def self.make @@ -101,6 +113,7 @@ def self.main_body --help Print help --version Print mutants version + --profile Profile mutant execution --zombie Run mutant zombified Available subcommands: @@ -158,10 +171,11 @@ def self.main_body message = "#{main_body}\n" { - arguments: %w[--help], - expected_events: [[:stdout, :puts, message]], - expected_exit: true, - expected_zombie: false + arguments: %w[--help], + expected_events: [[:stdout, :puts, message]], + expected_exit: true, + expected_print_profile: false, + expected_zombie: false } end @@ -169,19 +183,21 @@ def self.main_body message = "#{main_body}\n" { - arguments: %w[--zombie --help], - expected_events: [[:stdout, :puts, message]], - expected_exit: true, - expected_zombie: true + arguments: %w[--zombie --help], + expected_events: [[:stdout, :puts, message]], + expected_exit: true, + expected_print_profile: false, + expected_zombie: true } end make do { - arguments: %w[--version], - expected_events: [[:stdout, :puts, "mutant-#{Mutant::VERSION}"]], - expected_exit: true, - expected_zombie: false + arguments: %w[--version], + expected_events: [[:stdout, :puts, "mutant-#{Mutant::VERSION}"]], + expected_exit: true, + expected_print_profile: false, + expected_zombie: false } end @@ -199,6 +215,7 @@ def self.main_body --help Print help --version Print mutants version + --profile Profile mutant execution --zombie Run mutant zombified Available subcommands: @@ -236,14 +253,16 @@ def self.main_body --help Print help --version Print mutants version + --profile Profile mutant execution --zombie Run mutant zombified MESSAGE { - arguments: %w[subscription show --help], - expected_events: [[:stdout, :puts, message]], - expected_exit: true, - expected_zombie: false + arguments: %w[subscription show --help], + expected_events: [[:stdout, :puts, message]], + expected_exit: true, + expected_print_profile: false, + expected_zombie: false } end @@ -259,6 +278,7 @@ def self.main_body --help Print help --version Print mutants version + --profile Profile mutant execution --zombie Run mutant zombified @@ -285,10 +305,11 @@ def self.main_body MESSAGE { - arguments: %w[run --help], - expected_events: [[:stdout, :puts, message]], - expected_exit: true, - expected_zombie: false + arguments: %w[run --help], + expected_events: [[:stdout, :puts, message]], + expected_exit: true, + expected_print_profile: false, + expected_zombie: false } end @@ -304,6 +325,7 @@ def self.main_body --help Print help --version Print mutants version + --profile Profile mutant execution --zombie Run mutant zombified @@ -330,10 +352,58 @@ def self.main_body MESSAGE { - arguments: %w[--zombie run --help], - expected_events: [[:stdout, :puts, message]], - expected_exit: true, - expected_zombie: true + arguments: %w[--profile run --help], + expected_events: [[:stdout, :puts, message]], + expected_exit: true, + expected_print_profile: true, + expected_zombie: false + } + end + + make do + message = <<~MESSAGE + usage: mutant run [options] + + Summary: Run code analysis + + mutant version: #{Mutant::VERSION} + + Global Options: + + --help Print help + --version Print mutants version + --profile Profile mutant execution + --zombie Run mutant zombified + + + Environment: + -I, --include DIRECTORY Add DIRECTORY to $LOAD_PATH + -r, --require NAME Require file with NAME + --env KEY=VALUE Set environment variable + + + Runner: + --fail-fast Fail fast + -j, --jobs NUMBER Number of kill jobs. Defaults to number of processors. + -t, --mutation-timeout NUMBER Per mutation analysis timeout + + + Integration: + --use INTEGRATION Use INTEGRATION to kill mutations + + + Matcher: + --ignore-subject EXPRESSION Ignore subjects that match EXPRESSION as prefix + --start-subject EXPRESSION Start mutation testing at a specific subject + --since REVISION Only select subjects touched since REVISION + MESSAGE + + { + arguments: %w[--zombie run --help], + expected_events: [[:stdout, :puts, message]], + expected_exit: true, + expected_print_profile: false, + expected_zombie: true } end @@ -349,6 +419,7 @@ def self.main_body --help Print help --version Print mutants version + --profile Profile mutant execution --zombie Run mutant zombified Available subcommands: @@ -357,10 +428,11 @@ def self.main_body MESSAGE { - arguments: %w[util --help], - expected_events: [[:stdout, :puts, message]], - expected_exit: true, - expected_zombie: false + arguments: %w[util --help], + expected_events: [[:stdout, :puts, message]], + expected_exit: true, + expected_print_profile: false, + expected_zombie: false } end @@ -376,6 +448,7 @@ def self.main_body --help Print help --version Print mutants version + --profile Profile mutant execution --zombie Run mutant zombified @@ -384,10 +457,11 @@ def self.main_body MESSAGE { - arguments: %w[util mutation --help], - expected_events: [[:stdout, :puts, message]], - expected_exit: true, - expected_zombie: false + arguments: %w[util mutation --help], + expected_events: [[:stdout, :puts, message]], + expected_exit: true, + expected_print_profile: false, + expected_zombie: false } end @@ -399,39 +473,42 @@ def self.main_body MESSAGE { - arguments: %w[util mutation -e true], - expected_events: [ + arguments: %w[util mutation -e true], + expected_events: [ [:stdout, :puts, ''], [:stdout, :write, message] ], - expected_exit: true, - expected_zombie: false + expected_exit: true, + expected_print_profile: false, + expected_zombie: false } end make do { - arguments: %w[util mutation -e true -i true], - expected_events: [ + arguments: %w[util mutation -e true -i true], + expected_events: [ [:stdout, :puts, ''] ], - expected_exit: true, - expected_zombie: false + expected_exit: true, + expected_print_profile: false, + expected_zombie: false } end make do { - arguments: %w[util mutation -e true -i foo], - expected_events: [ + arguments: %w[util mutation -e true -i foo], + expected_events: [ [:stderr, :puts, <<~'MESSAGE'.strip] Expected valid node type got: foo foo ^^^ MESSAGE ], - expected_exit: false, - expected_zombie: false + expected_exit: false, + expected_print_profile: false, + expected_zombie: false } end @@ -443,13 +520,14 @@ def self.main_body MESSAGE { - arguments: %w[util mutation test_app/simple.rb], - expected_events: [ + arguments: %w[util mutation test_app/simple.rb], + expected_events: [ [:stdout, :puts, 'file:test_app/simple.rb'], [:stdout, :write, message] ], - expected_exit: true, - expected_zombie: false + expected_exit: true, + expected_print_profile: false, + expected_zombie: false } end @@ -612,6 +690,11 @@ def self.main_body file_config_result end + allow(world).to receive(:record) do |name, &block| + events << [:record, name] + block.call + end + allow(Mutant::Bootstrap).to receive(:call) do |env| events << [:bootstrap, env.inspect] bootstrap_result @@ -650,6 +733,10 @@ def self.main_body let(:expected_events) do [ + %i[ + record + config + ], [ :load_config_file, Mutant::Env.empty(world, load_config_file_config).inspect @@ -676,6 +763,10 @@ def self.main_body let(:expected_events) do [ + %i[ + record + config + ], [ :load_config_file, Mutant::Env.empty(world, load_config_file_config).inspect @@ -716,6 +807,10 @@ def self.main_body let(:expected_events) do [ + %i[ + record + config + ], [ :load_config_file, Mutant::Env.empty(world, load_config_file_config).inspect @@ -767,6 +862,10 @@ def self.main_body let(:expected_events) do [ + %i[ + record + config + ], [ :load_config_file, Mutant::Env.empty(world, load_config_file_config).inspect @@ -814,6 +913,10 @@ def self.main_body let(:expected_events) do [ license_validation_event, + %i[ + record + config + ], [ :load_config_file, Mutant::Env.empty(world, load_config_file_config).inspect diff --git a/spec/unit/mutant/env_spec.rb b/spec/unit/mutant/env_spec.rb index f5e0279ca..1feb011d8 100644 --- a/spec/unit/mutant/env_spec.rb +++ b/spec/unit/mutant/env_spec.rb @@ -247,4 +247,25 @@ def apply ) end end + + describe '#record' do + before do + allow(subject.world.recorder).to receive(:record) do |name, &block| + events << [name, block.call] + end + end + + let(:block) { -> { :value } } + let(:events) { [] } + + def apply + subject.record(:test_segment, &block) + end + + it 'forwards calls to configured segment recorder' do + apply + + expect(events).to eql([%i[test_segment value]]) + end + end end diff --git a/spec/unit/mutant/runner_spec.rb b/spec/unit/mutant/runner_spec.rb index b7d6308a8..07ffe9978 100644 --- a/spec/unit/mutant/runner_spec.rb +++ b/spec/unit/mutant/runner_spec.rb @@ -68,6 +68,12 @@ def apply selector: :start, arguments: [env] }, + { + receiver: env, + selector: :record, + arguments: [:analysis], + reaction: { yields: [] } + }, { receiver: env, selector: :method, @@ -102,6 +108,12 @@ def apply arguments: [delay], reaction: { return: status_b } }, + { + receiver: env, + selector: :record, + arguments: [:report], + reaction: { yields: [] } + }, { receiver: reporter, selector: :report, @@ -127,6 +139,12 @@ def apply selector: :start, arguments: [env] }, + { + receiver: env, + selector: :record, + arguments: [:analysis], + reaction: { yields: [] } + }, { receiver: env, selector: :method, @@ -166,6 +184,12 @@ def apply arguments: [delay], reaction: { return: status_b } }, + { + receiver: env, + selector: :record, + arguments: [:report], + reaction: { yields: [] } + }, { receiver: reporter, selector: :report, diff --git a/spec/unit/mutant/segment/recorder_spec.rb b/spec/unit/mutant/segment/recorder_spec.rb new file mode 100644 index 000000000..29b0f694e --- /dev/null +++ b/spec/unit/mutant/segment/recorder_spec.rb @@ -0,0 +1,95 @@ +# frozen_string_literal: true + +RSpec.describe Mutant::Segment::Recorder do + subject do + described_class.new( + gen_id: gen_id, + parent_id: root_segment.id, + recording_start: 0.5, + root_id: root_segment.id, + segments: [root_segment], + timer: timer + ) + end + + let(:gen_id) { -> { @id += 1 } } + let(:io) { StringIO.new } + + let(:timer) do + instance_double(Mutant::Timer).tap do |timer| + allow(timer).to receive(:now) do + @timer += 1 + end + end + end + + let(:root_segment) do + Mutant::Segment.new( + id: 0, + name: 'root', + parent_id: nil, + timestamp_end: nil, + timestamp_start: 0.5 + ) + end + + before do + @id = 0 + @timer = 0 + end + + describe '#print_profile' do + def apply + add_segments + + subject.print_profile(io) + end + + shared_examples 'expected report' do + it 'returns expected report' do + apply + + io.rewind + + expect(io.read).to eql(expected_report) + end + end + + context 'just root segment' do + def add_segments; end + + let(:expected_report) do + <<~'REPORT' + 0.0000: (0.5000s) root + REPORT + end + + include_examples 'expected report' + end + + context 'root with a child' do + def add_segments + subject.record(:child0) do + subject.record(:child1) do + end + + subject.record(:child2) do + end + end + end + + let(:expected_report) do + <<~'REPORT' + 0.0000: (6.5000s) root + 0.5000: (5.0000s) child0 + 1.5000: (1.0000s) child1 + 3.5000: (1.0000s) child2 + 5.5000: (5.0000s) child0 + 6.5000: (6.5000s) root + REPORT + end + + include_examples 'expected report' + end + end +end diff --git a/spec/unit/mutant/segment_spec.rb b/spec/unit/mutant/segment_spec.rb new file mode 100644 index 000000000..093c05066 --- /dev/null +++ b/spec/unit/mutant/segment_spec.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +RSpec.describe Mutant::Segment do + let(:recording_start) { 10 } + + subject do + described_class.new( + id: SecureRandom.uuid, + name: :test_segment, + parent_id: nil, + timestamp_end: 13, + timestamp_start: 11 + ) + end + + describe '#elapsed' do + it 'returns expected value' do + expect(subject.elapsed).to eql(2) + end + end + + describe '#offset_end' do + it 'returns expected value' do + expect(subject.offset_end(recording_start)).to eql(3) + end + end + + describe '#offset_start' do + it 'returns expected value' do + expect(subject.offset_start(recording_start)).to eql(1) + end + end +end diff --git a/spec/unit/mutant/world_spec.rb b/spec/unit/mutant/world_spec.rb index 59cfb545d..a3c308c8b 100644 --- a/spec/unit/mutant/world_spec.rb +++ b/spec/unit/mutant/world_spec.rb @@ -23,6 +23,37 @@ def apply end end + describe '#record' do + subject do + super().with( + recorder: recorder + ) + end + + def apply + subject.record(name, &block) + end + + let(:block) { -> { result } } + let(:name) { :test_name } + let(:result) { instance_double(Object) } + + let(:recorder) do + instance_double(Mutant::Segment::Recorder) + end + + before do + allow(recorder).to receive(:record) do |observed_name, &block| + expect(observed_name).to be(name) + block.call + end + end + + it 'records segment' do + expect(apply).to be(result) + end + end + describe '#capture_stdout' do def apply subject.capture_stdout(command)