klenod-test 0.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 469fd8edab3de5dd48965dbb967cde185bc2e0b90f4081b9c7581adff7ee8287
4
+ data.tar.gz: e5f5c39e24cb625f210d083741749218f16dca6603901f5aa71dc3aeb8303ff1
5
+ SHA512:
6
+ metadata.gz: 4de48c343c46d765d52b0883c18b82d8599bd5a8d4eb3cad2068a01752fe4a3ed950c6491f96bcf8fe6c33a3bbf109f7475a4fb2e54b6b2ec710f1fad0f38db6
7
+ data.tar.gz: 3ab7371fe5dd99eb681ead786a718c8b07bdd5febd073900d0763babc8a249233680ce61b5778327acfba7d836c44938ab233ea4791bd3a184cab4f4279140b6
data/README.md ADDED
@@ -0,0 +1,98 @@
1
+ # klenod-test
2
+
3
+ `klenod-test` discovers and runs application tests without choosing a test
4
+ framework. Add its plugin to the application's build configuration:
5
+
6
+ ```ruby
7
+ plugins [
8
+ Klenod::Test::Plugin.new,
9
+ Klenod::Build::Plugins::RubyPlugin.new
10
+ ]
11
+ ```
12
+
13
+ The plugin finds `*.test.rb` files in deterministic order and prevents
14
+ application modules and other tests from importing them. Tests can import normal
15
+ application modules. `Klenod::Test::Suite` indexes each test's eager and lazy
16
+ dependency closure without evaluating application code.
17
+
18
+ The runner can run the full suite once or watch the module graph and rerun only
19
+ tests related to a change. Install the `klenod` meta-gem and run it from an
20
+ application directory:
21
+
22
+ ```sh
23
+ bundle exec klenod test --run
24
+ bundle exec klenod test --watch
25
+ ```
26
+
27
+ Without an option, the command watches unless `CI` is set.
28
+
29
+ The command searches the current directory and its parents for
30
+ `klenod.test.rb`. The file supplies the application-specific context and test
31
+ framework adapter:
32
+
33
+ ```ruby
34
+ context do
35
+ path = File.expand_path("klenod.config.rb", __dir__)
36
+ Klenod::Build::ConfigLoader.load(path).context
37
+ end
38
+
39
+ execute do |context, test_paths|
40
+ # Register and run the selected test modules, then return an exit status.
41
+ end
42
+
43
+ format_error do |error, context|
44
+ # Optionally format collection or evaluation errors.
45
+ end
46
+ ```
47
+
48
+ The runner does not choose a testing framework. Applications provide a context
49
+ factory and an execution callback. The same runner is also available as a Ruby
50
+ API:
51
+
52
+ ```ruby
53
+ runner = Klenod::Test::Runner.new(
54
+ context: -> { build_config.context },
55
+ execute: ->(context, test_paths) { run_tests(context, test_paths) },
56
+ watch: true
57
+ )
58
+
59
+ exit runner.call
60
+ ```
61
+
62
+ Each batch runs in a fresh worker process. The execution callback receives that
63
+ worker's context and sorted source-relative test paths, then returns an integer
64
+ exit status. By default, the runner starts the current Ruby program with
65
+ `--worker -- <test paths>`. A runner created by that program recognizes those
66
+ arguments automatically and executes the callback instead of starting another
67
+ worker. Pass `worker_command` and `worker_paths` explicitly when embedding the
68
+ runner in a command with its own argument handling.
69
+
70
+ ## Coverage
71
+
72
+ Run the complete test suite once under Covered:
73
+
74
+ ```sh
75
+ bundle exec klenod coverage
76
+ bundle exec klenod coverage --report partial --minimum 90
77
+ ```
78
+
79
+ Coverage defaults to the brief report with no required minimum. Configure both
80
+ defaults in `klenod.test.rb`:
81
+
82
+ ```ruby
83
+ coverage report: :brief, minimum: 90
84
+ ```
85
+
86
+ Command-line values override the configuration. Reports may be `brief`,
87
+ `partial`, `full`, `markdown`, or `quiet`. The minimum is an overall percentage
88
+ from 0 through 100; falling below it returns a failing status.
89
+
90
+ Coverage includes evaluated application Ruby and source-mapped modules. Klenod
91
+ maps generated execution lines back to the original Haml, Markdown, or other
92
+ plugin source. It excludes test modules, gem and virtual modules, and generated
93
+ wrappers for data files. Source files which no test evaluates are not synthesized
94
+ into the report.
95
+
96
+ Collection wraps the application's existing `execute` callback, so it does not
97
+ depend on Minitest, RSpec, or another test framework. Frameworks that start
98
+ independent subprocesses need their own subprocess coverage integration.
@@ -0,0 +1,146 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rbconfig"
4
+
5
+ require "klenod/build/cli"
6
+
7
+ require_relative "config"
8
+ require_relative "coverage"
9
+ require_relative "runner"
10
+
11
+ module Klenod
12
+ module Test
13
+ module CLI
14
+ class Command < Samovar::Command
15
+ self.description = "Run and watch application tests."
16
+
17
+ options do
18
+ option "--run", "Run all tests once."
19
+ option "--watch", "Run all tests, then watch for changes."
20
+ option "--worker", "Run selected test files in a worker process."
21
+ end
22
+
23
+ split :test_paths, "Test paths passed to the worker."
24
+
25
+ def call
26
+ config_path = ConfigLoader.find
27
+ unless config_path
28
+ output.puts "Could not find klenod.test.rb"
29
+ return 1
30
+ end
31
+
32
+ config = ConfigLoader.load(config_path)
33
+ Dir.chdir(config.base_dir) do
34
+ Klenod::Test::Runner.new(
35
+ context: config.context,
36
+ execute: config.execute,
37
+ **runner_options,
38
+ worker_command: worker_command,
39
+ output:,
40
+ format_error: config.format_error
41
+ ).call
42
+ end
43
+ rescue ConfigError => error
44
+ output.puts error.message
45
+ 1
46
+ end
47
+
48
+ private
49
+
50
+ def runner_options
51
+ selected = [@options[:run] && false, @options[:watch] && true].compact
52
+ raise ConfigError, "Choose either --run or --watch" if selected.length > 1
53
+
54
+ paths = Array(@test_paths)
55
+ if @options[:worker]
56
+ raise ConfigError, "--worker cannot be combined with --run or --watch" unless selected.empty?
57
+
58
+ {worker_paths: paths}
59
+ elsif paths.empty?
60
+ selected.empty? ? {} : {watch: selected.first}
61
+ else
62
+ raise ConfigError, "Test paths are only accepted with --worker"
63
+ end
64
+ end
65
+
66
+ def worker_command
67
+ [RbConfig.ruby, Gem.bin_path("klenod", "klenod"), "test"]
68
+ end
69
+ end
70
+
71
+ class CoverageCommand < Samovar::Command
72
+ self.description = "Run the full application test suite with coverage."
73
+
74
+ options do
75
+ option "--report <name>", "Coverage report: brief, partial, full, markdown, or quiet."
76
+ option "--minimum <percent>", "Fail below this overall coverage percentage."
77
+ option "--worker", "Run selected test files in a coverage worker process."
78
+ end
79
+
80
+ split :test_paths, "Test paths passed to the worker."
81
+
82
+ def call
83
+ config_path = ConfigLoader.find
84
+ unless config_path
85
+ output.puts "Could not find klenod.test.rb"
86
+ return 1
87
+ end
88
+
89
+ config = ConfigLoader.load(config_path)
90
+ coverage_config = coverage_config(config.coverage)
91
+ Dir.chdir(config.base_dir) do
92
+ Klenod::Test::Runner.new(
93
+ context: config.context,
94
+ execute: coverage_execute(config, coverage_config),
95
+ watch: false,
96
+ worker_paths: worker_paths,
97
+ spawn_empty: true,
98
+ worker_command: worker_command,
99
+ output:,
100
+ format_error: config.format_error
101
+ ).call
102
+ end
103
+ rescue ConfigError => error
104
+ output.puts error.message
105
+ 1
106
+ end
107
+
108
+ private
109
+
110
+ def coverage_config(config)
111
+ CoverageConfig.build(
112
+ report: @options[:report] || config.report,
113
+ minimum: @options[:minimum] || config.minimum
114
+ )
115
+ end
116
+
117
+ def coverage_execute(config, coverage_config)
118
+ lambda do |context, test_paths|
119
+ plugin = context.graph.plugins.find { it.is_a?(Klenod::Test::Plugin) }
120
+ raise ArgumentError, "The Klenod context must include Klenod::Test::Plugin" unless plugin
121
+
122
+ CoverageRunner.new(context:, plugin:, config: coverage_config, output:).call do
123
+ config.execute.call(context, test_paths)
124
+ end
125
+ end
126
+ end
127
+
128
+ def worker_paths
129
+ paths = Array(@test_paths)
130
+ return paths if @options[:worker]
131
+
132
+ raise ConfigError, "Test paths are only accepted with --worker" unless paths.empty?
133
+
134
+ nil
135
+ end
136
+
137
+ def worker_command
138
+ command = [RbConfig.ruby, Gem.bin_path("klenod", "klenod"), "coverage"]
139
+ command.concat(["--report", @options[:report]]) if @options[:report]
140
+ command.concat(["--minimum", @options[:minimum]]) if @options[:minimum]
141
+ command
142
+ end
143
+ end
144
+ end
145
+ end
146
+ end
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Klenod
4
+ module Test
5
+ class ConfigError < ArgumentError; end
6
+
7
+ COVERAGE_REPORTS = %i[brief partial full markdown quiet].freeze
8
+
9
+ CoverageConfig = Data.define(:report, :minimum) do
10
+ def self.build(report: :brief, minimum: nil)
11
+ report = report.to_s.downcase.to_sym
12
+ unless COVERAGE_REPORTS.include?(report)
13
+ raise ConfigError, "Unknown coverage report #{report.inspect}; choose one of: #{COVERAGE_REPORTS.join(", ")}"
14
+ end
15
+
16
+ new(report, normalize_minimum(minimum))
17
+ end
18
+
19
+ def self.normalize_minimum(minimum)
20
+ return unless minimum
21
+
22
+ value = begin
23
+ Float(minimum)
24
+ rescue ArgumentError, TypeError
25
+ raise ConfigError, "Coverage minimum must be a number between 0 and 100"
26
+ end
27
+ return value if value.between?(0, 100)
28
+
29
+ raise ConfigError, "Coverage minimum must be between 0 and 100"
30
+ end
31
+ end
32
+
33
+ Config = Data.define(:base_dir, :context, :execute, :format_error, :coverage)
34
+
35
+ class ConfigBuilder
36
+ def initialize(path)
37
+ @path = File.expand_path(path)
38
+ end
39
+
40
+ def context(&block)
41
+ @context = block
42
+ end
43
+
44
+ def execute(&block)
45
+ @execute = block
46
+ end
47
+
48
+ def format_error(&block)
49
+ @format_error = block
50
+ end
51
+
52
+ def coverage(report: :brief, minimum: nil)
53
+ @coverage = CoverageConfig.build(report:, minimum:)
54
+ end
55
+
56
+ def config
57
+ missing = []
58
+ missing << "context" unless @context
59
+ missing << "execute" unless @execute
60
+ raise ConfigError, "#{@path}: missing #{missing.join(" and ")}" unless missing.empty?
61
+
62
+ Config.new(
63
+ base_dir: File.dirname(@path),
64
+ context: @context,
65
+ execute: @execute,
66
+ format_error: @format_error,
67
+ coverage: @coverage || CoverageConfig.build
68
+ )
69
+ end
70
+ end
71
+
72
+ module ConfigLoader
73
+ CONFIG_FILE = "klenod.test.rb"
74
+
75
+ module_function
76
+
77
+ def find(start_dir = Dir.pwd)
78
+ dir = File.expand_path(start_dir)
79
+
80
+ loop do
81
+ path = File.join(dir, CONFIG_FILE)
82
+ return path if File.file?(path)
83
+
84
+ parent = File.dirname(dir)
85
+ return nil if parent == dir
86
+
87
+ dir = parent
88
+ end
89
+ end
90
+
91
+ def load(path)
92
+ builder = ConfigBuilder.new(path)
93
+ builder.instance_eval(File.read(path), path, 1)
94
+ builder.config
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "covered"
4
+ require "covered/markdown_summary"
5
+ require "covered/statistics"
6
+
7
+ require_relative "config"
8
+
9
+ module Klenod
10
+ module Test
11
+ class CoverageResult < Covered::Wrapper
12
+ def initialize(output, context:, plugin:)
13
+ super(output)
14
+ @modules_by_path = context.graph.mods.to_h do |module_id, mod|
15
+ [File.expand_path(mod.eval_path), [module_id, mod]]
16
+ end
17
+ @plugin = plugin
18
+ end
19
+
20
+ def each
21
+ return enum_for unless block_given?
22
+
23
+ super do |coverage|
24
+ mapped = map(coverage)
25
+ yield mapped if mapped
26
+ end
27
+ end
28
+
29
+ private
30
+
31
+ attr_reader :modules_by_path, :plugin
32
+
33
+ def map(coverage)
34
+ module_id, mod = modules_by_path[File.expand_path(coverage.path)]
35
+ return unless module_id&.scheme == :app
36
+ return if plugin.test_module_id?(module_id)
37
+
38
+ if mod.source_map
39
+ map_source(coverage, mod.source_map)
40
+ elsif module_id.extname == ".rb"
41
+ coverage
42
+ end
43
+ end
44
+
45
+ def map_source(coverage, source_map)
46
+ counts = Array.new(source_map.input.lines.count + 1)
47
+ input_line = nil
48
+
49
+ coverage.counts.each_with_index do |count, output_line|
50
+ input_line = source_map.marks_by_output_line[output_line]&.line || input_line
51
+ next if count.nil?
52
+ next unless input_line
53
+
54
+ counts[input_line] = [counts[input_line] || 0, count].max
55
+ end
56
+
57
+ return unless counts.any? { |count| !count.nil? }
58
+
59
+ Covered::Coverage.new(Covered::Source.for(coverage.path), counts)
60
+ end
61
+ end
62
+
63
+ class CoverageRunner
64
+ REPORTS = {
65
+ brief: Covered::BriefSummary,
66
+ partial: Covered::PartialSummary,
67
+ full: Covered::FullSummary,
68
+ markdown: Covered::MarkdownSummary,
69
+ quiet: Covered::Quiet
70
+ }.freeze
71
+
72
+ def initialize(context:, plugin:, config:, output: $stdout)
73
+ @context = context
74
+ @plugin = plugin
75
+ @config = config
76
+ @output = output
77
+ end
78
+
79
+ def call
80
+ policy = Covered::Policy.new
81
+ policy.root(context.graph.source_dir.to_s)
82
+ policy.start
83
+ status = begin
84
+ Integer(yield)
85
+ ensure
86
+ policy.finish
87
+ end
88
+
89
+ result = CoverageResult.new(policy, context:, plugin:)
90
+ REPORTS.fetch(config.report).new.call(result, output)
91
+ validate_minimum(result, status)
92
+ end
93
+
94
+ private
95
+
96
+ attr_reader :context, :plugin, :config, :output
97
+
98
+ def validate_minimum(result, status)
99
+ return status unless config.minimum
100
+
101
+ statistics = Covered::Statistics.new
102
+ result.each { |coverage| statistics << coverage }
103
+ statistics.validate!(config.minimum / 100.0)
104
+ status
105
+ rescue Covered::CoverageError => error
106
+ output.puts
107
+ output.puts error.message
108
+ status.zero? ? 1 : status
109
+ end
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "klenod/build/errors"
4
+ require "klenod/build/module_id"
5
+ require "klenod/build/plugin"
6
+
7
+ module Klenod
8
+ module Test
9
+ class ImportError < Klenod::Build::ResolveError; end
10
+
11
+ class Plugin < Klenod::Build::Plugin
12
+ DEFAULT_PATTERN = "**/*.test.rb"
13
+
14
+ def initialize(pattern: DEFAULT_PATTERN)
15
+ @pattern = pattern.to_s
16
+ end
17
+
18
+ attr_reader :pattern
19
+
20
+ def discover(source_dir:)
21
+ root = Pathname.new(source_dir).expand_path
22
+
23
+ root
24
+ .glob(pattern)
25
+ .select(&:file?)
26
+ .map { |path| Klenod::Build::ModuleId.new(path.relative_path_from(root).to_s.tr("\\", "/"), nil) }
27
+ .sort_by(&:to_s)
28
+ end
29
+
30
+ def test_module_id?(module_id)
31
+ module_id.scheme == :app && File.fnmatch?(pattern, module_id.relative_path, File::FNM_PATHNAME | File::FNM_EXTGLOB)
32
+ end
33
+
34
+ def finalize(module_id, result, resolved_dependencies, _dependency_records, _context)
35
+ imported_test = resolved_dependencies.find { |dependency| test_module_id?(dependency.module_id) }
36
+ return result unless imported_test
37
+
38
+ error = ImportError.new("Test file #{imported_test.module_id.path.inspect} cannot be imported")
39
+ raise error.with_resolution_context(dependency: imported_test.dependency, importer_id: module_id)
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,166 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rbconfig"
4
+
5
+ require "async/process"
6
+ require "klenod/build"
7
+
8
+ require_relative "plugin"
9
+ require_relative "suite"
10
+
11
+ module Klenod
12
+ module Test
13
+ class Runner
14
+ WORKER_ARGUMENT = "--worker"
15
+
16
+ def self.worker_paths_from(arguments)
17
+ arguments = Array(arguments)
18
+ return unless arguments.first == WORKER_ARGUMENT
19
+ raise ArgumentError, "Expected -- after #{WORKER_ARGUMENT}" unless arguments[1] == "--"
20
+
21
+ arguments.drop(2)
22
+ end
23
+
24
+ def initialize(
25
+ context:,
26
+ execute:,
27
+ watch: nil,
28
+ worker_paths: Runner.worker_paths_from(ARGV),
29
+ spawn_empty: false,
30
+ worker_command: [RbConfig.ruby, $PROGRAM_NAME],
31
+ process: Async::Process,
32
+ output: $stdout,
33
+ error_output: $stderr,
34
+ env: ENV,
35
+ format_error: nil
36
+ )
37
+ @context_factory = context
38
+ @execute = execute
39
+ @watch = watch.nil? ? !env.key?("CI") : watch
40
+ @worker_paths = worker_paths&.dup
41
+ @spawn_empty = spawn_empty
42
+ @worker_command = Array(worker_command)
43
+ @process = process
44
+ @output = output
45
+ @error_output = error_output
46
+ @env = env
47
+ @format_error = format_error || ->(error, _context) { error.full_message }
48
+ @last_status = 0
49
+ @worker_mutex = Mutex.new
50
+ end
51
+
52
+ def call
53
+ return run_worker(worker_paths) if worker_paths
54
+
55
+ context = context_factory.call
56
+ plugin = test_plugin(context)
57
+ suite = Klenod::Test::Suite.new(context:, plugin:)
58
+ selection = suite.collect
59
+ return run_in_worker(selection.test_paths) unless watch
60
+
61
+ run_watch(context, suite, selection)
62
+ rescue ArgumentError => error
63
+ error_output.puts error.message
64
+ 1
65
+ end
66
+
67
+ private
68
+
69
+ attr_reader :context_factory, :execute, :watch, :worker_paths, :worker_command,
70
+ :spawn_empty, :process, :output, :error_output, :env, :format_error, :last_status
71
+
72
+ def run_watch(context, suite, selection)
73
+ watcher = build_watcher(context)
74
+ context.on_update do |event|
75
+ related = suite.update(event)
76
+ log_removed(related.removed_test_paths)
77
+ run_in_worker(related.test_paths) unless related.test_paths.empty?
78
+ end
79
+
80
+ begin
81
+ watcher.start
82
+ run_in_worker(selection.test_paths)
83
+ wait_for_changes
84
+ rescue Interrupt
85
+ output.puts
86
+ ensure
87
+ watcher.stop
88
+ end
89
+
90
+ last_status
91
+ end
92
+
93
+ def run_in_worker(test_paths)
94
+ @worker_mutex.synchronize do
95
+ clear_screen
96
+ report_test_paths(test_paths)
97
+ @last_status = (test_paths.empty? && !spawn_empty) ? 0 : worker_status(test_paths)
98
+ report_watch_status if watch
99
+ last_status
100
+ end
101
+ end
102
+
103
+ def worker_status(test_paths)
104
+ status = process.spawn(*worker_command, WORKER_ARGUMENT, "--", *test_paths)
105
+ status.success? ? 0 : (status.exitstatus || 1)
106
+ end
107
+
108
+ def run_worker(test_paths)
109
+ context = context_factory.call
110
+ Integer(execute.call(context, test_paths))
111
+ rescue => error
112
+ error_output.puts format_error.call(error, context)
113
+ 1
114
+ end
115
+
116
+ def test_plugin(context)
117
+ context.graph.plugins.find do |candidate|
118
+ candidate.is_a?(Klenod::Test::Plugin)
119
+ end || raise(ArgumentError, "The Klenod context must include Klenod::Test::Plugin")
120
+ end
121
+
122
+ def build_watcher(context)
123
+ Klenod::Build::Watcher.new(source_dir: context.graph.source_dir, context:)
124
+ end
125
+
126
+ def wait_for_changes
127
+ loop { sleep }
128
+ end
129
+
130
+ def log_removed(paths)
131
+ paths.each { |path| output.puts "Removed #{path}" }
132
+ end
133
+
134
+ def clear_screen
135
+ return unless watch && output.respond_to?(:tty?) && output.tty?
136
+
137
+ output.print "\e[2J\e[H"
138
+ end
139
+
140
+ def report_test_paths(test_paths)
141
+ count = test_paths.length
142
+ output.puts "#{color(:run, " RUN ")} #{count} test #{(count == 1) ? "file" : "files"}"
143
+ output.puts
144
+ test_paths.each { |path| output.puts " #{path}" }
145
+ output.puts
146
+ output.flush
147
+ end
148
+
149
+ def report_watch_status
150
+ status = last_status.zero? ? :success : :failure
151
+ label = last_status.zero? ? " PASS " : " FAIL "
152
+ output.puts
153
+ output.puts "#{color(status, label)} Watching for file changes..."
154
+ output.flush
155
+ end
156
+
157
+ def color(name, value)
158
+ return value if env.key?("NO_COLOR")
159
+ return value unless output.respond_to?(:tty?) && output.tty?
160
+
161
+ codes = {run: "\e[1;34m", success: "\e[1;32m", failure: "\e[1;31m"}
162
+ "#{codes.fetch(name)}#{value}\e[0m"
163
+ end
164
+ end
165
+ end
166
+ end
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Klenod
4
+ module Test
5
+ Selection = Data.define(:test_paths, :removed_test_paths) do
6
+ def empty?
7
+ test_paths.empty? && removed_test_paths.empty?
8
+ end
9
+ end
10
+
11
+ class Suite
12
+ def initialize(context:, plugin:)
13
+ @context = context
14
+ @plugin = plugin
15
+ @dependency_ids = {}
16
+ @failed_test_ids = Set.new
17
+ end
18
+
19
+ def collect
20
+ test_ids = discover
21
+ test_ids.each { |test_id| index(test_id) }
22
+ selection(test_ids)
23
+ end
24
+
25
+ def update(event)
26
+ previous_test_ids = @dependency_ids.keys.to_set
27
+ current_test_ids = discover.to_set
28
+ removed_test_ids = previous_test_ids - current_test_ids
29
+ added_test_ids = current_test_ids - previous_test_ids
30
+ affected_module_ids = update_module_ids(event)
31
+ affected_test_ids =
32
+ @dependency_ids.filter_map do |test_id, dependency_ids|
33
+ test_id if current_test_ids.include?(test_id) && dependency_ids.intersect?(affected_module_ids)
34
+ end
35
+
36
+ test_ids = (added_test_ids + affected_test_ids + @failed_test_ids).select { |test_id| current_test_ids.include?(test_id) }
37
+ removed_test_ids.each { |test_id| remove(test_id) }
38
+ test_ids.each { |test_id| index(test_id) }
39
+
40
+ selection(test_ids, removed_test_ids)
41
+ end
42
+
43
+ private
44
+
45
+ attr_reader :context, :plugin
46
+
47
+ def discover
48
+ plugin.discover(source_dir: context.graph.source_dir)
49
+ end
50
+
51
+ def index(test_id)
52
+ dependency_ids = Set[test_id]
53
+ @dependency_ids[test_id] = dependency_ids
54
+ collect_dependencies(test_id, dependency_ids)
55
+ @failed_test_ids.delete(test_id)
56
+ rescue
57
+ @failed_test_ids << test_id
58
+ end
59
+
60
+ def collect_dependencies(module_id, dependency_ids)
61
+ record = context.graph.records[module_id] || context.graph.collect_module(module_id)
62
+
63
+ record.resolved_dependencies.each do |dependency|
64
+ dependency_id = dependency.module_id
65
+ next unless dependency_ids.add?(dependency_id)
66
+
67
+ collect_dependencies(dependency_id, dependency_ids)
68
+ end
69
+ end
70
+
71
+ def remove(test_id)
72
+ @dependency_ids.delete(test_id)
73
+ @failed_test_ids.delete(test_id)
74
+ end
75
+
76
+ def update_module_ids(event)
77
+ result = event.result
78
+ Set.new(
79
+ result.changed_module_ids +
80
+ result.removed_module_ids +
81
+ result.reloaded_module_ids +
82
+ result.reevaluated_module_ids
83
+ )
84
+ end
85
+
86
+ def selection(test_ids, removed_test_ids = [])
87
+ Selection.new(
88
+ test_ids.map(&:relative_path).sort.freeze,
89
+ removed_test_ids.map(&:relative_path).sort.freeze
90
+ )
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Klenod
4
+ module Test
5
+ VERSION = "0.0.5"
6
+ end
7
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "test/version"
4
+ require_relative "test/config"
5
+ require_relative "test/plugin"
6
+ require_relative "test/suite"
7
+ require_relative "test/runner"
8
+ require_relative "test/coverage"
9
+
10
+ module Klenod
11
+ module Test
12
+ end
13
+ end
metadata ADDED
@@ -0,0 +1,94 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: klenod-test
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.5
5
+ platform: ruby
6
+ authors:
7
+ - Andrés Alin
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: klenod-build
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - '='
17
+ - !ruby/object:Gem::Version
18
+ version: 0.0.5
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - '='
24
+ - !ruby/object:Gem::Version
25
+ version: 0.0.5
26
+ - !ruby/object:Gem::Dependency
27
+ name: async-process
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '1.4'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '1.4'
40
+ - !ruby/object:Gem::Dependency
41
+ name: covered
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '0.30'
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '0.30'
54
+ description: Framework-independent test selection, watch runs, and coverage for Klenod
55
+ applications.
56
+ email:
57
+ - andreas.alin@gmail.com
58
+ executables: []
59
+ extensions: []
60
+ extra_rdoc_files: []
61
+ files:
62
+ - README.md
63
+ - lib/klenod/test.rb
64
+ - lib/klenod/test/cli.rb
65
+ - lib/klenod/test/config.rb
66
+ - lib/klenod/test/coverage.rb
67
+ - lib/klenod/test/plugin.rb
68
+ - lib/klenod/test/runner.rb
69
+ - lib/klenod/test/suite.rb
70
+ - lib/klenod/test/version.rb
71
+ homepage: https://github.com/aalin/klenod
72
+ licenses:
73
+ - MIT
74
+ metadata:
75
+ homepage_uri: https://github.com/aalin/klenod
76
+ source_code_uri: https://github.com/aalin/klenod/tree/main/gems/klenod-test
77
+ rdoc_options: []
78
+ require_paths:
79
+ - lib
80
+ required_ruby_version: !ruby/object:Gem::Requirement
81
+ requirements:
82
+ - - ">="
83
+ - !ruby/object:Gem::Version
84
+ version: 4.0.6
85
+ required_rubygems_version: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ requirements: []
91
+ rubygems_version: 4.0.16
92
+ specification_version: 4
93
+ summary: Test runner for Klenod applications.
94
+ test_files: []