rspec-atoms 0.1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: fa40b012635a3c9f62c31c5977cbd9b17b4e51ac7739c19106597c9f85b06fea
4
+ data.tar.gz: d2a3f0fbd30b9f279c5321dd86e43bacf0884efa418fd7597cea866c5e360359
5
+ SHA512:
6
+ metadata.gz: b81face24d35cb9a72209b013965279e65d6fe94d636fb2f07736e41b063375c35041eb17e33ddf6340518b87f8c81eca331ad4602dfb8b471de7523026b94bb
7
+ data.tar.gz: d2efc74f299d142585393684efc4d7bb35cab4ee7a6c36c4d5bd54d0bd72d48f17906a6a559c3f3f8352cba613cfc686122e7ec88c24bb6abd9c5af191141fbb
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 José Carbone
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,86 @@
1
+ # RSpec Atoms
2
+
3
+ CircleCI usually splits RSpec by file. When tests run across parallel instances, a
4
+ slower feature spec can keep one worker busy while the others sit idle, even when
5
+ its individual examples are small.
6
+
7
+ Splitting a cohesive spec into several files only to satisfy the CI scheduler is
8
+ unnecessary. `rspec-atoms` exposes each runnable RSpec example as a schedulable
9
+ atom while preserving shared setup, contexts, and readable JUnit names.
10
+
11
+ The gem was designed for CircleCI's dynamic test splitting, but it works in any
12
+ environment that needs to run RSpec examples in parallel.
13
+
14
+ CircleCI Smarter Testing is currently in beta. Local validation requires the
15
+ CircleCI `testsuite` plugin; CircleCI's CI containers already include it.
16
+
17
+ ## Installation
18
+
19
+ Add the gem to your `Gemfile`:
20
+
21
+ ```ruby
22
+ group :test do
23
+ gem "rspec-atoms"
24
+ end
25
+ ```
26
+
27
+ Then run `bundle install`.
28
+
29
+ ## CircleCI configuration
30
+
31
+ Create `.circleci/test-suites.yml`:
32
+
33
+ ```yaml
34
+ ---
35
+ name: backend tests
36
+
37
+ discover: >-
38
+ RAILS_ENV=test bundle exec rspec-atoms discover spec
39
+
40
+ run: >-
41
+ RAILS_ENV=test bundle exec rspec-atoms run
42
+ --junit "<< outputs.junit >>"
43
+ --
44
+ << test.atoms >>
45
+
46
+ outputs:
47
+ junit: /tmp/rspec/rspec.xml
48
+
49
+ options:
50
+ dynamic-test-splitting: true
51
+ ```
52
+
53
+ `<< test.atoms >>` is replaced by CircleCI with the RSpec example IDs assigned to
54
+ the current batch. `<< outputs.junit >>` resolves to the configured JUnit path so
55
+ CircleCI can locate the report. The gem enables its JUnit formatter automatically;
56
+ the `--junit` option only overrides where the report is written.
57
+
58
+ Invoke the test suite from the CircleCI job:
59
+
60
+ ```yaml
61
+ - run:
62
+ name: Run RSpec with dynamic test splitting
63
+ command: |
64
+ mkdir -p /tmp/rspec
65
+ circleci run testsuite "backend tests"
66
+ when: always
67
+
68
+ - store_test_results:
69
+ path: /tmp/rspec
70
+ ```
71
+
72
+ ## Local usage
73
+
74
+ ```bash
75
+ bundle exec rspec-atoms discover spec
76
+ bundle exec rspec-atoms run -- "spec/models/user_spec.rb[1:2]"
77
+ ```
78
+
79
+ Local runs write JUnit XML to `tmp/rspec.xml`. Pass `--junit PATH` before `--` to
80
+ use a different path.
81
+
82
+ Validate the CircleCI configuration locally with:
83
+
84
+ ```bash
85
+ circleci run testsuite "backend tests" --doctor
86
+ ```
data/exe/rspec-atoms ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "rspec_atoms/cli"
5
+
6
+ exit RSpecAtoms::CLI.start(ARGV)
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "discover"
4
+ require_relative "run"
5
+ require_relative "version"
6
+
7
+ module RSpecAtoms
8
+ class CLI
9
+ EXIT_USAGE = 64
10
+
11
+ def self.start(arguments, output: $stdout, error: $stderr)
12
+ arguments = arguments.dup
13
+ command = arguments.shift
14
+
15
+ case command
16
+ when "discover"
17
+ Discover.call(arguments, output: output, error: error)
18
+ when "run"
19
+ junit_output = extract_option!(
20
+ arguments,
21
+ "--junit",
22
+ default: Run::DEFAULT_JUNIT_OUTPUT
23
+ )
24
+ arguments.shift if arguments.first == "--"
25
+
26
+ Run.call(
27
+ arguments,
28
+ junit_output: junit_output,
29
+ output: output,
30
+ error: error
31
+ )
32
+ when "version", "--version", "-v"
33
+ output.puts(VERSION)
34
+ 0
35
+ else
36
+ error.puts(usage)
37
+ EXIT_USAGE
38
+ end
39
+ rescue ArgumentError => exception
40
+ error.puts(exception.message)
41
+ error.puts(usage)
42
+ EXIT_USAGE
43
+ end
44
+
45
+ def self.extract_option!(arguments, option, default:)
46
+ index = arguments.index(option)
47
+ return default unless index
48
+
49
+ value = arguments[index + 1]
50
+ if value.nil? || value.empty? || value == "--"
51
+ raise ArgumentError, "Missing #{option} PATH"
52
+ end
53
+
54
+ arguments.slice!(index, 2)
55
+ value
56
+ end
57
+
58
+ def self.usage
59
+ <<~USAGE
60
+ Usage:
61
+ rspec-atoms discover [RSpec arguments]
62
+ rspec-atoms run [--junit PATH] -- [RSpec arguments]
63
+ rspec-atoms version
64
+ USAGE
65
+ end
66
+
67
+ private_class_method :extract_option!, :usage
68
+ end
69
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rspec/core"
4
+ require "stringio"
5
+ require "tempfile"
6
+
7
+ require_relative "discovery_formatter"
8
+
9
+ module RSpecAtoms
10
+ module Discover
11
+ module_function
12
+
13
+ def call(arguments, output: $stdout, error: $stderr)
14
+ Tempfile.create(["rspec-atoms", ".txt"]) do |atoms_file|
15
+ runner_output = StringIO.new
16
+ RSpec.configuration.fail_if_no_examples = true
17
+
18
+ status = RSpec::Core::Runner.run(
19
+ rspec_arguments(arguments, atoms_file.path),
20
+ error,
21
+ runner_output
22
+ )
23
+
24
+ if status.zero?
25
+ atoms_file.rewind
26
+ IO.copy_stream(atoms_file, output)
27
+ else
28
+ write_runner_output(runner_output, error)
29
+ end
30
+
31
+ status
32
+ end
33
+ end
34
+
35
+ def rspec_arguments(arguments, atoms_path)
36
+ [
37
+ "--dry-run",
38
+ "--no-color",
39
+ "--format",
40
+ "RSpecAtoms::DiscoveryFormatter",
41
+ "--out",
42
+ atoms_path,
43
+ *arguments
44
+ ]
45
+ end
46
+
47
+ def write_runner_output(runner_output, error)
48
+ content = runner_output.string
49
+ return if content.empty?
50
+
51
+ error.write(content)
52
+ error.write("\n") unless content.end_with?("\n")
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rspec/core"
4
+
5
+ require_relative "example_id"
6
+
7
+ module RSpecAtoms
8
+ class DiscoveryFormatter
9
+ RSpec::Core::Formatters.register self, :example_started, :close
10
+
11
+ def initialize(output)
12
+ @output = output
13
+ end
14
+
15
+ def example_started(notification)
16
+ @output.puts(
17
+ ExampleId.normalize(notification.example.id)
18
+ )
19
+ end
20
+
21
+ def close(_notification)
22
+ @output.flush
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RSpecAtoms
4
+ module ExampleId
5
+ SELECTOR_PATTERN = /\[[^\]]+\]\z/
6
+
7
+ module_function
8
+
9
+ def normalize(value)
10
+ value.to_s.sub(/\A\.\//, "")
11
+ end
12
+
13
+ def file_path(value)
14
+ normalize(value).sub(SELECTOR_PATTERN, "")
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rspec_junit_formatter"
4
+
5
+ require_relative "example_id"
6
+
7
+ module RSpecAtoms
8
+ class JunitFormatter < ::RSpecJUnitFormatter
9
+ RSpec::Core::Formatters.register(
10
+ self,
11
+ :start,
12
+ :stop,
13
+ :dump_summary
14
+ )
15
+
16
+ private
17
+
18
+ def example_group_file_path_for(notification)
19
+ ExampleId.normalize(notification.example.id)
20
+ end
21
+
22
+ def classname_for(notification)
23
+ ExampleId
24
+ .file_path(notification.example.id)
25
+ .sub(%r{\.[^/]*\z}, "")
26
+ .tr("/", ".")
27
+ .gsub(/\A\.+|\.+\z/, "")
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+ require "rspec/core"
5
+
6
+ require_relative "junit_formatter"
7
+
8
+ module RSpecAtoms
9
+ module Run
10
+ DEFAULT_JUNIT_OUTPUT = "tmp/rspec.xml"
11
+
12
+ module_function
13
+
14
+ def call(
15
+ arguments,
16
+ junit_output: DEFAULT_JUNIT_OUTPUT,
17
+ output: $stdout,
18
+ error: $stderr
19
+ )
20
+ ensure_output_directory(junit_output)
21
+
22
+ RSpec::Core::Runner.run(
23
+ inject_formatters(arguments, junit_output),
24
+ error,
25
+ output
26
+ )
27
+ end
28
+
29
+ def inject_formatters(arguments, junit_output)
30
+ separator_index = arguments.index("--") || arguments.length
31
+ options = arguments[0...separator_index]
32
+ selectors = arguments[separator_index..] || []
33
+
34
+ [
35
+ *options,
36
+ "--format",
37
+ "progress",
38
+ "--format",
39
+ "RSpecAtoms::JunitFormatter",
40
+ "--out",
41
+ junit_output,
42
+ *selectors
43
+ ]
44
+ end
45
+
46
+ def ensure_output_directory(junit_output)
47
+ directory = File.dirname(junit_output)
48
+ return if directory == "."
49
+
50
+ FileUtils.mkdir_p(directory)
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RSpecAtoms
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "rspec_atoms/version"
4
+ require_relative "rspec_atoms/example_id"
5
+
6
+ module RSpecAtoms
7
+ end
metadata ADDED
@@ -0,0 +1,144 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rspec-atoms
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Jose Carbone
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: rspec-core
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '3.13'
19
+ - - "<"
20
+ - !ruby/object:Gem::Version
21
+ version: '4'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ requirements:
26
+ - - ">="
27
+ - !ruby/object:Gem::Version
28
+ version: '3.13'
29
+ - - "<"
30
+ - !ruby/object:Gem::Version
31
+ version: '4'
32
+ - !ruby/object:Gem::Dependency
33
+ name: rspec_junit_formatter
34
+ requirement: !ruby/object:Gem::Requirement
35
+ requirements:
36
+ - - ">="
37
+ - !ruby/object:Gem::Version
38
+ version: '0.6'
39
+ - - "<"
40
+ - !ruby/object:Gem::Version
41
+ version: '1'
42
+ type: :runtime
43
+ prerelease: false
44
+ version_requirements: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - ">="
47
+ - !ruby/object:Gem::Version
48
+ version: '0.6'
49
+ - - "<"
50
+ - !ruby/object:Gem::Version
51
+ version: '1'
52
+ - !ruby/object:Gem::Dependency
53
+ name: rake
54
+ requirement: !ruby/object:Gem::Requirement
55
+ requirements:
56
+ - - ">="
57
+ - !ruby/object:Gem::Version
58
+ version: '13'
59
+ type: :development
60
+ prerelease: false
61
+ version_requirements: !ruby/object:Gem::Requirement
62
+ requirements:
63
+ - - ">="
64
+ - !ruby/object:Gem::Version
65
+ version: '13'
66
+ - !ruby/object:Gem::Dependency
67
+ name: rexml
68
+ requirement: !ruby/object:Gem::Requirement
69
+ requirements:
70
+ - - ">="
71
+ - !ruby/object:Gem::Version
72
+ version: '0'
73
+ type: :development
74
+ prerelease: false
75
+ version_requirements: !ruby/object:Gem::Requirement
76
+ requirements:
77
+ - - ">="
78
+ - !ruby/object:Gem::Version
79
+ version: '0'
80
+ - !ruby/object:Gem::Dependency
81
+ name: rspec
82
+ requirement: !ruby/object:Gem::Requirement
83
+ requirements:
84
+ - - ">="
85
+ - !ruby/object:Gem::Version
86
+ version: '3.13'
87
+ - - "<"
88
+ - !ruby/object:Gem::Version
89
+ version: '4'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '3.13'
97
+ - - "<"
98
+ - !ruby/object:Gem::Version
99
+ version: '4'
100
+ description: |
101
+ Discovers runnable RSpec examples as RSpec IDs and emits matching JUnit
102
+ identities for CircleCI Smarter Testing.
103
+ email:
104
+ - joseignaciocarbone@gmail.com
105
+ executables:
106
+ - rspec-atoms
107
+ extensions: []
108
+ extra_rdoc_files: []
109
+ files:
110
+ - LICENSE.txt
111
+ - README.md
112
+ - exe/rspec-atoms
113
+ - lib/rspec_atoms.rb
114
+ - lib/rspec_atoms/cli.rb
115
+ - lib/rspec_atoms/discover.rb
116
+ - lib/rspec_atoms/discovery_formatter.rb
117
+ - lib/rspec_atoms/example_id.rb
118
+ - lib/rspec_atoms/junit_formatter.rb
119
+ - lib/rspec_atoms/run.rb
120
+ - lib/rspec_atoms/version.rb
121
+ homepage: https://github.com/josesei/rspec-atoms
122
+ licenses:
123
+ - MIT
124
+ metadata:
125
+ rubygems_mfa_required: 'true'
126
+ source_code_uri: https://github.com/josesei/rspec-atoms
127
+ rdoc_options: []
128
+ require_paths:
129
+ - lib
130
+ required_ruby_version: !ruby/object:Gem::Requirement
131
+ requirements:
132
+ - - ">="
133
+ - !ruby/object:Gem::Version
134
+ version: '3.4'
135
+ required_rubygems_version: !ruby/object:Gem::Requirement
136
+ requirements:
137
+ - - ">="
138
+ - !ruby/object:Gem::Version
139
+ version: '0'
140
+ requirements: []
141
+ rubygems_version: 3.6.9
142
+ specification_version: 4
143
+ summary: Split RSpec suites by runnable example
144
+ test_files: []