spectracer 1.2.1 → 1.4.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 69f91e79d4902946517d095ed052d4f5780e7d6be4a433fea8c82bee46d54f78
4
- data.tar.gz: fe1abc2750f426935d646d8f3000f37b759f7a0ec7d1a24ae7ffb6fc19501a7f
3
+ metadata.gz: c01031e82f15d06eeef77717e0d31ec34a5bb73940323deb023c6bf3ec13eb13
4
+ data.tar.gz: 77395beec7e9113f050495f1ea036e8f019814514da08fb2a90654d25f0a6a10
5
5
  SHA512:
6
- metadata.gz: be6ecf26e9ebfc694d2df58b16fd8f86cf86a0ecc7d06cef002464cbc082d77833d6c64ecb03b3073564114d22dc0e3a1fe16068a598885dd852eaab346f617e
7
- data.tar.gz: 5d6aaa61ab88c7501cfce960aae3be010dc04f9202b741ff40a4035e3aea5359fe822ba96a1bccc26ee90f8d5f09e6148a4cd1bf8191abdec093850e6bb320c6
6
+ metadata.gz: 30dd806f8a0428ea5890a4f39b2c315ae198c740b27fa33c629f1be9b6279d28b71227afceb349994bf6dfea58e05e5b65fa574623d63131e5b338e51ba9a13e
7
+ data.tar.gz: 0741cbb623678895406c7a233124654c6b54f8a96743e3bb8ab05de3e4447cafc1b8fa4affc3ea0ded7466f0aa3a2b749048e9ff96a52982f09e7e8472969669
data/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.4.0] - 2026-02-04
4
+
5
+ ### Changed
6
+
7
+ - Remove `./` prefix from traced paths for bktec/zzglob compatibility
8
+ - zzglob's `path.Clean()` normalizes `./spec/` to `spec/`, causing pattern mismatches
9
+ - Paths are now stored without the `./` prefix (e.g., `spec/models/user_spec.rb`)
10
+ - Output is compatible with both `bktec --test-file-pattern` and `rspec --pattern`
11
+
12
+ ## [1.3.0] - 2026-02-02
13
+
14
+ ### Added
15
+
16
+ - GlobFactorizer to reduce redundant spec patterns before running tests
17
+ - Removes concrete files already matched by broader globs
18
+ - Eliminates narrower globs subsumed by broader ones
19
+ - Example: `{spec/user/**/*_spec.rb, spec/user/**/one_spec.rb}` → `{spec/user/**/*_spec.rb}`
20
+
3
21
  ## [1.2.0] - 2026-02-02
4
22
 
5
23
  ### Added
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Spectracer
4
+ module Core
5
+ class GlobFactorizer
6
+ def call(patterns)
7
+ return patterns.to_a if patterns.size <= 1
8
+
9
+ patterns_array = patterns.to_a.uniq
10
+ globs, files = patterns_array.partition { |p| glob_pattern?(p) }
11
+
12
+ # Remove files that are matched by any glob
13
+ remaining_files = files.reject do |file|
14
+ globs.any? { |glob| File.fnmatch?(glob, file, File::FNM_PATHNAME) }
15
+ end
16
+
17
+ # Remove globs subsumed by other globs (broader patterns win)
18
+ remaining_globs = globs.reject do |glob|
19
+ globs.any? { |other| other != glob && subsumes?(other, glob) }
20
+ end
21
+
22
+ remaining_globs + remaining_files
23
+ end
24
+
25
+ private
26
+
27
+ def glob_pattern?(pattern)
28
+ pattern.include?("*") || pattern.include?("?") || pattern.include?("[")
29
+ end
30
+
31
+ def subsumes?(broader, narrower)
32
+ return false if broader == narrower
33
+
34
+ # Convert the narrower glob to a concrete-ish path by replacing wildcards
35
+ # then check if the broader glob would match it
36
+ test_path = narrower
37
+ .gsub("**", "any/nested/path")
38
+ .gsub("*", "placeholder")
39
+
40
+ File.fnmatch?(broader, test_path, File::FNM_PATHNAME)
41
+ end
42
+ end
43
+ end
44
+ end
@@ -34,8 +34,8 @@ module Spectracer
34
34
  end
35
35
 
36
36
  def normalize(file_path, repo_root:)
37
- relative = file_path.sub(/\A#{Regexp.escape(repo_root)}/, ".")
38
- relative.start_with?("./") ? relative : "./#{relative}"
37
+ relative = file_path.sub(%r{\A#{Regexp.escape(repo_root)}/?}, "")
38
+ relative.sub(%r{\A\./}, "")
39
39
  end
40
40
 
41
41
  def strip_dot_prefix(path)
@@ -5,6 +5,10 @@ module Spectracer
5
5
  class SpecSelector
6
6
  Result = Struct.new(:specs, :file_to_specs_map, keyword_init: true)
7
7
 
8
+ def initialize(factorizer: GlobFactorizer.new)
9
+ @factorizer = factorizer
10
+ end
11
+
8
12
  def call(changed_files:, inverse_deps:, globs:, on_empty:)
9
13
  spec_set = Set.new
10
14
  file_to_specs = {}
@@ -12,12 +16,12 @@ module Spectracer
12
16
  changed_files.each do |file|
13
17
  matched_specs = []
14
18
 
15
- if file.end_with?("_spec.rb")
19
+ if file.end_with?("_spec.rb", "_test.rb")
16
20
  spec_set.add(file)
17
21
  matched_specs << file
18
22
  end
19
23
 
20
- file_key = file.start_with?("./") ? file : "./#{file}"
24
+ file_key = file.sub(%r{\A\./}, "")
21
25
  if (specs = inverse_deps[file_key])
22
26
  spec_set.merge(specs)
23
27
  matched_specs.concat(specs)
@@ -34,7 +38,7 @@ module Spectracer
34
38
  file_to_specs[file] = matched_specs.uniq.sort unless matched_specs.empty?
35
39
  end
36
40
 
37
- files = spec_set.to_a.sort
41
+ files = @factorizer.call(spec_set).sort
38
42
  specs_result = files.empty? ? on_empty : files.join(",")
39
43
 
40
44
  Result.new(specs: specs_result, file_to_specs_map: file_to_specs)
@@ -6,7 +6,8 @@ module Spectracer
6
6
  module IO
7
7
  class ConfigLoader
8
8
  FILE_PATH = ".spectracer.yml"
9
- DEFAULT_FILE_PATH = File.expand_path("../../spectracer.default.yml", __dir__)
9
+ DEFAULT_RSPEC_FILE_PATH = File.expand_path("../../spectracer.default.yml", __dir__)
10
+ DEFAULT_MINITEST_FILE_PATH = File.expand_path("../../spectracer.default.minitest.yml", __dir__)
10
11
 
11
12
  def initialize(logger: nil)
12
13
  @logger = logger
@@ -29,7 +30,17 @@ module Spectracer
29
30
  YAML.safe_load_file(FILE_PATH)
30
31
  else
31
32
  @logger&.debug("No configuration file found at #{FILE_PATH.inspect}. Using defaults.")
32
- YAML.safe_load_file(DEFAULT_FILE_PATH)
33
+ YAML.safe_load_file(default_file_path)
34
+ end
35
+ end
36
+
37
+ def default_file_path
38
+ if defined?(RSpec)
39
+ DEFAULT_RSPEC_FILE_PATH
40
+ elsif defined?(Minitest)
41
+ DEFAULT_MINITEST_FILE_PATH
42
+ else
43
+ DEFAULT_RSPEC_FILE_PATH
33
44
  end
34
45
  end
35
46
 
@@ -24,7 +24,9 @@ module Spectracer
24
24
 
25
25
  inverse_deps.each_value(&:sort!)
26
26
 
27
- @store.write(inverse_deps, @paths.collected_dependencies_file)
27
+ output_path = @paths.collected_dependencies_file
28
+ @logger&.debug("Writing collected dependencies to: #{output_path}")
29
+ @store.write(inverse_deps, output_path)
28
30
 
29
31
  nil
30
32
  end
@@ -10,12 +10,39 @@ namespace :spectracer do
10
10
  exit 0
11
11
  end
12
12
 
13
+ framework = detect_test_framework
14
+ default_file_path = case framework
15
+ when :minitest
16
+ Spectracer::IO::ConfigLoader::DEFAULT_MINITEST_FILE_PATH
17
+ else
18
+ Spectracer::IO::ConfigLoader::DEFAULT_RSPEC_FILE_PATH
19
+ end
20
+
21
+ warn "Detected test framework: #{framework}"
13
22
  warn "Creating '#{config_path}' file."
14
23
 
15
- default_content = File.read(Spectracer::IO::ConfigLoader::DEFAULT_FILE_PATH)
24
+ default_content = File.read(default_file_path)
16
25
  File.write(config_path, default_content)
17
26
  end
18
27
 
28
+ def detect_test_framework
29
+ gemfile_path = File.join(Dir.pwd, "Gemfile")
30
+ return :rspec unless File.exist?(gemfile_path)
31
+
32
+ gemfile_content = File.read(gemfile_path)
33
+
34
+ has_rspec = gemfile_content.match?(/['"]rspec['"]|['"]rspec-rails['"]/)
35
+ has_minitest = gemfile_content.match?(/['"]minitest['"]/) || Dir.exist?("test")
36
+
37
+ if has_rspec
38
+ :rspec
39
+ elsif has_minitest
40
+ :minitest
41
+ else
42
+ :rspec
43
+ end
44
+ end
45
+
19
46
  desc "Collect spec dependencies from tracing artifacts"
20
47
  task :collect_dependencies do
21
48
  logger = Spectracer::Logger.new(
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Spectracer
4
- VERSION = "1.2.1"
4
+ VERSION = "1.4.0"
5
5
  end
@@ -0,0 +1,16 @@
1
+ # This file was generated by the 'spectracer:install' Rake task.
2
+ # You can modify this file to customize the behavior of Spectracer.
3
+ # Test framework: Minitest
4
+ defaults:
5
+ all_specs_glob: "test/**/*_test.rb"
6
+ no_specs_glob: "{}"
7
+
8
+ # When no spec files are detected to run, Spectracer will run the specs defined in the 'on_empty_spec_set' key.
9
+ on_empty_spec_set: "{{no_specs_glob}}"
10
+
11
+ # If a file changes and it matches the glob, the corresponding pattern will be added to the list of spec files to run.
12
+ # These will take precedence over the 'on_empty_spec_set' key.
13
+ globs_matcher:
14
+ "Gemfile": "{{all_specs_glob}}"
15
+ "Gemfile.lock": "{{all_specs_glob}}"
16
+ "config/**": "{{all_specs_glob}}"
@@ -1,5 +1,6 @@
1
1
  # This file was generated by the 'spectracer:install' Rake task.
2
2
  # You can modify this file to customize the behavior of Spectracer.
3
+ # Test framework: RSpec
3
4
  defaults:
4
5
  all_specs_glob: "spec/**/*_spec.rb"
5
6
  no_specs_glob: "{}"
data/lib/spectracer.rb CHANGED
@@ -5,6 +5,7 @@ require_relative "spectracer/logger"
5
5
 
6
6
  require_relative "spectracer/core/paths"
7
7
  require_relative "spectracer/core/path_filter"
8
+ require_relative "spectracer/core/glob_factorizer"
8
9
  require_relative "spectracer/core/spec_selector"
9
10
 
10
11
  require_relative "spectracer/io/command_runner"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: spectracer
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.1
4
+ version: 1.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mitch Smith
@@ -36,8 +36,10 @@ files:
36
36
  - CHANGELOG.md
37
37
  - LICENSE.txt
38
38
  - README.md
39
+ - lib/spectracer.default.minitest.yml
39
40
  - lib/spectracer.default.yml
40
41
  - lib/spectracer.rb
42
+ - lib/spectracer/core/glob_factorizer.rb
41
43
  - lib/spectracer/core/path_filter.rb
42
44
  - lib/spectracer/core/paths.rb
43
45
  - lib/spectracer/core/spec_selector.rb