spectracer 1.0.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 +7 -0
- data/AGENTS.md +93 -0
- data/CHANGELOG.md +17 -0
- data/LICENSE.txt +21 -0
- data/README.md +177 -0
- data/lib/spectacle.default.yml +15 -0
- data/lib/spectracer/core/paths.rb +46 -0
- data/lib/spectracer/core/spec_selector.rb +28 -0
- data/lib/spectracer/integrations/minitest.rb +55 -0
- data/lib/spectracer/integrations/railtie.rb +13 -0
- data/lib/spectracer/integrations/rspec.rb +30 -0
- data/lib/spectracer/io/command_runner.rb +25 -0
- data/lib/spectracer/io/config_loader.rb +56 -0
- data/lib/spectracer/io/dependency_store.rb +61 -0
- data/lib/spectracer/io/git_adapter.rb +58 -0
- data/lib/spectracer/logger.rb +38 -0
- data/lib/spectracer/orchestrators/dependency_collector.rb +54 -0
- data/lib/spectracer/orchestrators/dependency_tracer.rb +71 -0
- data/lib/spectracer/orchestrators/spec_run_determiner.rb +64 -0
- data/lib/spectracer/providers/git_changed_files.rb +40 -0
- data/lib/spectracer/providers/repository.rb +15 -0
- data/lib/spectracer/tasks/spectacle.rake +37 -0
- data/lib/spectracer/version.rb +5 -0
- data/lib/spectracer.rb +43 -0
- data/sig/spectracer.rbs +195 -0
- metadata +86 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: c55000e91497b314c04f6a3139416b257e63af8c16deb7c1d7ddbb657e4d2443
|
|
4
|
+
data.tar.gz: '028a4517c2bfc6fe8d0f3fa1341fb1f00b79852cc13999d38cc176edf3f4b92d'
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 5622bd3c3c4cd2a056711798b435b928a0bc927bd74c358a4afe6764903983682c68e0ce958a0ba3d5e793644a4ae206c19ead323ab7f1456df0eaf7c9546b9f
|
|
7
|
+
data.tar.gz: 46661f6f0882c81122d401a68424b927dd57bc3154628cf838fb33bf6ac642c34f8563cbbf1b594617c61193a79478d4e1c2a5df800f08ebd765495d5869c993
|
data/AGENTS.md
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# Spectracer - Agent Instructions
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
Spectracer is a Ruby gem that traces RSpec/Minitest dependencies and determines which specs to run based on git changes. It integrates with Buildkite CI for parallel test optimization.
|
|
6
|
+
|
|
7
|
+
## Commands
|
|
8
|
+
|
|
9
|
+
### Testing
|
|
10
|
+
```bash
|
|
11
|
+
bundle exec rspec # Run all tests
|
|
12
|
+
bundle exec rspec spec/core/ # Run specific directory
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
### Linting
|
|
16
|
+
```bash
|
|
17
|
+
bundle exec standardrb # Check style
|
|
18
|
+
bundle exec standardrb --fix # Auto-fix issues
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
### Full CI Check
|
|
22
|
+
```bash
|
|
23
|
+
bundle exec rake # Runs spec + standardrb
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### Build Gem
|
|
27
|
+
```bash
|
|
28
|
+
bundle exec rake build # Build .gem file
|
|
29
|
+
bundle exec rake install # Install locally
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Architecture
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
lib/spectracer/
|
|
36
|
+
├── core/ # Pure business logic (no I/O)
|
|
37
|
+
│ ├── paths.rb # Path computation from env
|
|
38
|
+
│ └── spec_selector.rb # Spec selection algorithm
|
|
39
|
+
├── io/ # I/O adapters (mockable)
|
|
40
|
+
│ ├── command_runner.rb # Shell command wrapper
|
|
41
|
+
│ ├── config_loader.rb # YAML config loading
|
|
42
|
+
│ ├── dependency_store.rb # Gzip JSON read/write
|
|
43
|
+
│ └── git_adapter.rb # Git gem wrapper
|
|
44
|
+
├── providers/ # Data providers
|
|
45
|
+
│ ├── git_changed_files.rb # Changed file detection
|
|
46
|
+
│ └── repository.rb # Repo root detection
|
|
47
|
+
├── orchestrators/ # Coordinate components
|
|
48
|
+
│ ├── dependency_collector.rb
|
|
49
|
+
│ ├── dependency_tracer.rb
|
|
50
|
+
│ └── spec_run_determiner.rb
|
|
51
|
+
├── integrations/ # Test framework hooks
|
|
52
|
+
│ ├── minitest.rb
|
|
53
|
+
│ ├── railtie.rb
|
|
54
|
+
│ └── rspec.rb
|
|
55
|
+
├── tasks/
|
|
56
|
+
│ └── spectracer.rake
|
|
57
|
+
├── logger.rb
|
|
58
|
+
└── version.rb
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Code Conventions
|
|
62
|
+
|
|
63
|
+
- All classes use dependency injection for testability
|
|
64
|
+
- Core classes are pure functions (no side effects)
|
|
65
|
+
- I/O classes wrap external dependencies and are mockable
|
|
66
|
+
- Use `frozen_string_literal: true` in all files
|
|
67
|
+
- Follow StandardRB style (double quotes, no trailing commas)
|
|
68
|
+
- RBS type signatures in `sig/spectracer.rbs`
|
|
69
|
+
|
|
70
|
+
## Key Design Decisions
|
|
71
|
+
|
|
72
|
+
1. **No ENV at require-time**: All environment variables read at runtime via injected `env` parameter
|
|
73
|
+
2. **Dependency injection**: All collaborators passed via `initialize` with sensible defaults
|
|
74
|
+
3. **Pure core logic**: `SpecSelector` has no I/O, fully testable with plain data
|
|
75
|
+
4. **Git gem over shelling out**: Uses `ruby-git` gem for git operations
|
|
76
|
+
|
|
77
|
+
## Testing Patterns
|
|
78
|
+
|
|
79
|
+
- Unit tests mock collaborators using `instance_double`
|
|
80
|
+
- Integration tests use real git operations
|
|
81
|
+
- Test files mirror source structure: `lib/spectracer/core/paths.rb` → `spec/core/paths_spec.rb`
|
|
82
|
+
|
|
83
|
+
## Environment Variables
|
|
84
|
+
|
|
85
|
+
| Variable | Purpose |
|
|
86
|
+
|----------|---------|
|
|
87
|
+
| `WITH_SPECTRACER_TRACING` | Enable dependency tracing (`"true"`) |
|
|
88
|
+
| `WITH_SPECTRACER_DEBUG` | Enable debug logging (`"true"`) |
|
|
89
|
+
| `SPECTRACER_TMP_DIRECTORY` | Override output directory |
|
|
90
|
+
| `BUILDKITE_BUILD_ID` | Buildkite build identifier |
|
|
91
|
+
| `BUILDKITE_JOB_ID` | Buildkite job identifier |
|
|
92
|
+
| `BUILDKITE_BRANCH` | Current branch name |
|
|
93
|
+
| `BUILDKITE_PIPELINE_DEFAULT_BRANCH` | Default branch (e.g., `main`) |
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [1.0.0] - 2026-01-31
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- Intelligent test selection based on git changes
|
|
8
|
+
- RSpec and Minitest integration via TracePoint
|
|
9
|
+
- Dependency tracing with gzip-compressed JSON storage
|
|
10
|
+
- Buildkite CI integration for parallel test optimization
|
|
11
|
+
- Git adapter for changed file detection
|
|
12
|
+
- Configurable via `.spectracer.yml`
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
|
|
16
|
+
- Switch TracePoint from `:line` to `:call` events for dramatically improved performance
|
|
17
|
+
- Renamed gem from `spectacle` to `spectracer`
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Mitch Smith
|
|
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
|
|
13
|
+
all 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
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
# Spectracer
|
|
2
|
+
|
|
3
|
+
> spectracer (noun): something exhibited to view as unusual, notable, or entertaining
|
|
4
|
+
|
|
5
|
+
Spectracer is a Ruby gem that intelligently determines which specs to run based on the files you've changed. By tracing dependencies during test execution, Spectracer builds a map of which source files are used by which specs, then uses this data to run only the relevant tests.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- **Dependency tracing** - Automatically tracks which files each spec depends on
|
|
10
|
+
- **Smart test selection** - Only runs specs affected by your changes
|
|
11
|
+
- **CI integration** - Built for Buildkite with parallel job support
|
|
12
|
+
- **Framework support** - Works with both RSpec and Minitest
|
|
13
|
+
- **Rails compatible** - Includes Railtie for automatic setup
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
Add Spectracer to your Gemfile:
|
|
18
|
+
|
|
19
|
+
```ruby
|
|
20
|
+
group :development, :test do
|
|
21
|
+
gem "spectracer", github: "mitchbne/spectracer", branch: "main"
|
|
22
|
+
end
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Then run:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
bundle install
|
|
29
|
+
bundle exec rake spectracer:install
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
This creates a `.spectracer.yml` configuration file in your project root.
|
|
33
|
+
|
|
34
|
+
## How It Works
|
|
35
|
+
|
|
36
|
+
Spectracer operates in three phases:
|
|
37
|
+
|
|
38
|
+
### 1. Tracing Phase
|
|
39
|
+
|
|
40
|
+
Run your full test suite with tracing enabled:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
WITH_SPECTRACER_TRACING=true bundle exec rspec
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
This uses Ruby's TracePoint to record which files each spec touches, outputting a gzipped JSON file mapping specs to their dependencies.
|
|
47
|
+
|
|
48
|
+
### 2. Collection Phase
|
|
49
|
+
|
|
50
|
+
After running specs across parallel jobs, collect all tracing artifacts:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
bundle exec rake spectracer:collect_dependencies
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
This combines individual trace files into a single inverse dependency map: for each source file, which specs depend on it.
|
|
57
|
+
|
|
58
|
+
### 3. Selection Phase
|
|
59
|
+
|
|
60
|
+
When running tests on a branch, determine which specs to run:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
SPECS=$(bundle exec rake spectracer:spec_determiner)
|
|
64
|
+
bundle exec rspec $SPECS
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Configuration
|
|
68
|
+
|
|
69
|
+
Create a `.spectracer.yml` file in your project root:
|
|
70
|
+
|
|
71
|
+
```yaml
|
|
72
|
+
defaults:
|
|
73
|
+
all_specs: "spec/**/*_spec.rb"
|
|
74
|
+
no_specs: ""
|
|
75
|
+
|
|
76
|
+
on_empty_spec_set: "{{all_specs}}"
|
|
77
|
+
|
|
78
|
+
globs_matcher:
|
|
79
|
+
"Gemfile": "{{all_specs}}"
|
|
80
|
+
"Gemfile.lock": "{{all_specs}}"
|
|
81
|
+
"db/schema.rb": "spec/models/**/*_spec.rb"
|
|
82
|
+
"config/routes.rb": "spec/routing/**/*_spec.rb"
|
|
83
|
+
"app/views/**/*": "spec/views/**/*_spec.rb,spec/features/**/*_spec.rb"
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Configuration Options
|
|
87
|
+
|
|
88
|
+
| Option | Description |
|
|
89
|
+
|--------|-------------|
|
|
90
|
+
| `defaults` | Named patterns that can be referenced in other options |
|
|
91
|
+
| `on_empty_spec_set` | Pattern to run when no specs are affected by changes |
|
|
92
|
+
| `globs_matcher` | Map of file globs to spec patterns to run when matched |
|
|
93
|
+
|
|
94
|
+
## Buildkite Integration
|
|
95
|
+
|
|
96
|
+
Example `pipeline.yml`:
|
|
97
|
+
|
|
98
|
+
```yaml
|
|
99
|
+
steps:
|
|
100
|
+
# Step 1: Run full suite with tracing (weekly/nightly)
|
|
101
|
+
- label: ":rspec: Full Suite with Tracing"
|
|
102
|
+
command: |
|
|
103
|
+
WITH_SPECTRACER_TRACING=true bundle exec rspec --format progress
|
|
104
|
+
artifact_paths:
|
|
105
|
+
- "tmp/spectracer/**/*"
|
|
106
|
+
branches: main
|
|
107
|
+
|
|
108
|
+
# Step 2: Collect dependencies
|
|
109
|
+
- label: ":package: Collect Dependencies"
|
|
110
|
+
command: |
|
|
111
|
+
buildkite-agent artifact download "tmp/spectracer/**/*" .
|
|
112
|
+
bundle exec rake spectracer:collect_dependencies
|
|
113
|
+
artifact_paths:
|
|
114
|
+
- "tmp/spectracer/dependencies.json.gz"
|
|
115
|
+
depends_on: "full-suite"
|
|
116
|
+
|
|
117
|
+
# Step 3: Run affected specs on feature branches
|
|
118
|
+
- label: ":rspec: Affected Specs"
|
|
119
|
+
command: |
|
|
120
|
+
buildkite-agent artifact download "tmp/spectracer/dependencies.json.gz" .
|
|
121
|
+
SPECS=$(bundle exec rake spectracer:spec_determiner)
|
|
122
|
+
bundle exec rspec $SPECS
|
|
123
|
+
branches: "!main"
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## RSpec Integration
|
|
127
|
+
|
|
128
|
+
Spectracer automatically configures RSpec when required. No additional setup needed:
|
|
129
|
+
|
|
130
|
+
```ruby
|
|
131
|
+
# Gemfile
|
|
132
|
+
gem "spectracer"
|
|
133
|
+
|
|
134
|
+
# That's it! Tracing is enabled via WITH_SPECTRACER_TRACING=true
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## Minitest Integration
|
|
138
|
+
|
|
139
|
+
Spectracer also supports Minitest:
|
|
140
|
+
|
|
141
|
+
```ruby
|
|
142
|
+
# test/test_helper.rb
|
|
143
|
+
require "spectracer"
|
|
144
|
+
|
|
145
|
+
# Tracing is automatically enabled when WITH_SPECTRACER_TRACING=true
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## Debug Mode
|
|
149
|
+
|
|
150
|
+
Enable debug logging to see what Spectracer is doing:
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
WITH_SPECTRACER_DEBUG=true bundle exec rake spectracer:spec_determiner
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## Development
|
|
157
|
+
|
|
158
|
+
After checking out the repo:
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
bin/setup # Install dependencies
|
|
162
|
+
bundle exec rake # Run tests + linting
|
|
163
|
+
bundle exec rspec # Run tests only
|
|
164
|
+
bundle exec standardrb # Run linting only
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
## Architecture
|
|
168
|
+
|
|
169
|
+
See [docs/architecture.md](docs/architecture.md) for detailed architecture documentation.
|
|
170
|
+
|
|
171
|
+
## Contributing
|
|
172
|
+
|
|
173
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/mitchbne/spectracer.
|
|
174
|
+
|
|
175
|
+
## License
|
|
176
|
+
|
|
177
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# This file was generated by the 'spectracer:install' Rake task.
|
|
2
|
+
# You can modify this file to customize the behavior of Spectracer.
|
|
3
|
+
defaults:
|
|
4
|
+
all_specs_glob: "spec/**/*_spec.rb"
|
|
5
|
+
no_specs_glob: "{}"
|
|
6
|
+
|
|
7
|
+
# When no spec files are detected to run, Spectracer will run the specs defined in the 'on_empty_spec_set' key.
|
|
8
|
+
on_empty_spec_set: "{{no_specs_glob}}"
|
|
9
|
+
|
|
10
|
+
# If a file changes and it matches the glob, the corresponding pattern will be added to the list of spec files to run.
|
|
11
|
+
# These will take precedence over the 'on_empty_spec_set' key.
|
|
12
|
+
globs_matcher:
|
|
13
|
+
"Gemfile": "{{all_specs_glob}}"
|
|
14
|
+
"Gemfile.lock": "{{all_specs_glob}}"
|
|
15
|
+
"config/**": "{{all_specs_glob}}"
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Spectracer
|
|
4
|
+
module Core
|
|
5
|
+
class Paths
|
|
6
|
+
DEFAULT_OUTPUT_DIR = "tmp/spectracer"
|
|
7
|
+
|
|
8
|
+
def initialize(env: ENV)
|
|
9
|
+
@env = env
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def build_id
|
|
13
|
+
@env["BUILDKITE_BUILD_ID"] || "local"
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def job_id
|
|
17
|
+
@env["BUILDKITE_JOB_ID"] || "local"
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def output_directory
|
|
21
|
+
@env["SPECTRACER_TMP_DIRECTORY"] || DEFAULT_OUTPUT_DIR
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def spec_artifact_output_file
|
|
25
|
+
File.join(output_directory, "tracing_output", build_id, "#{job_id}.json.gz")
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def spec_artifacts_download_glob
|
|
29
|
+
File.join(output_directory, "tracing_output", build_id, "*.json.gz")
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def collected_dependencies_file
|
|
33
|
+
File.join(output_directory, "dependencies.json.gz")
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def normalize(file_path, repo_root:)
|
|
37
|
+
relative = file_path.sub(/\A#{Regexp.escape(repo_root)}/, ".")
|
|
38
|
+
relative.start_with?("./") ? relative : "./#{relative}"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def strip_dot_prefix(path)
|
|
42
|
+
path.sub(%r{\A\./}, "")
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Spectracer
|
|
4
|
+
module Core
|
|
5
|
+
class SpecSelector
|
|
6
|
+
def call(changed_files:, inverse_deps:, globs:, on_empty:)
|
|
7
|
+
spec_set = Set.new
|
|
8
|
+
|
|
9
|
+
changed_files.each do |file|
|
|
10
|
+
spec_set.add(file) if file.end_with?("_spec.rb")
|
|
11
|
+
|
|
12
|
+
file_key = file.start_with?("./") ? file : "./#{file}"
|
|
13
|
+
if (specs = inverse_deps[file_key])
|
|
14
|
+
spec_set.merge(specs)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
globs.each do |glob, pattern|
|
|
18
|
+
normalized_glob = glob.sub(%r{\A\./}, "")
|
|
19
|
+
spec_set.add(pattern) if File.fnmatch?(normalized_glob, file)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
files = spec_set.to_a.sort
|
|
24
|
+
files.empty? ? on_empty : files.join(",")
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Spectracer
|
|
4
|
+
module Integrations
|
|
5
|
+
module Minitest
|
|
6
|
+
class << self
|
|
7
|
+
attr_accessor :tracer
|
|
8
|
+
|
|
9
|
+
def install!
|
|
10
|
+
return unless defined?(::Minitest)
|
|
11
|
+
return unless ENV["WITH_SPECTRACER_TRACING"] == "true"
|
|
12
|
+
|
|
13
|
+
logger = Spectracer::Logger.new(
|
|
14
|
+
enabled: ENV["WITH_SPECTRACER_DEBUG"] == "true",
|
|
15
|
+
level: :debug
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
self.tracer = Spectracer::Orchestrators::DependencyTracer.new(logger: logger)
|
|
19
|
+
|
|
20
|
+
::Minitest.singleton_class.prepend(RunPatch)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
module RunPatch
|
|
25
|
+
def run(args = [])
|
|
26
|
+
result = super
|
|
27
|
+
Spectracer::Integrations::Minitest.tracer&.write_output!
|
|
28
|
+
result
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
module TestCasePlugin
|
|
33
|
+
def before_setup
|
|
34
|
+
super
|
|
35
|
+
tracer = Spectracer::Integrations::Minitest.tracer
|
|
36
|
+
return unless tracer
|
|
37
|
+
|
|
38
|
+
file_path = method(name).source_location&.first
|
|
39
|
+
tracer.current_spec_file = file_path if file_path
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def run
|
|
43
|
+
tracer = Spectracer::Integrations::Minitest.tracer
|
|
44
|
+
return super unless tracer
|
|
45
|
+
|
|
46
|
+
tracer.with_tracing { super }
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
if defined?(::Minitest::Test) && ENV["WITH_SPECTRACER_TRACING"] == "true"
|
|
54
|
+
::Minitest::Test.prepend(Spectracer::Integrations::Minitest::TestCasePlugin)
|
|
55
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Spectracer
|
|
4
|
+
module Integrations
|
|
5
|
+
module RSpec
|
|
6
|
+
def self.install!
|
|
7
|
+
return unless defined?(::RSpec) && ::RSpec.respond_to?(:configure)
|
|
8
|
+
return unless ENV["WITH_SPECTRACER_TRACING"] == "true"
|
|
9
|
+
|
|
10
|
+
logger = Spectracer::Logger.new(
|
|
11
|
+
enabled: ENV["WITH_SPECTRACER_DEBUG"] == "true",
|
|
12
|
+
level: :debug
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
tracer = Spectracer::Orchestrators::DependencyTracer.new(logger: logger)
|
|
16
|
+
|
|
17
|
+
::RSpec.configure do |config|
|
|
18
|
+
config.around(:example) do |example|
|
|
19
|
+
tracer.current_spec_file = example.file_path
|
|
20
|
+
tracer.with_tracing { example.run }
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
config.after(:suite) do
|
|
24
|
+
tracer.write_output!
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "open3"
|
|
4
|
+
|
|
5
|
+
module Spectracer
|
|
6
|
+
module IO
|
|
7
|
+
class CommandRunner
|
|
8
|
+
def initialize(logger: nil)
|
|
9
|
+
@logger = logger
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def run(command)
|
|
13
|
+
@logger&.debug("Running command: '#{command}'")
|
|
14
|
+
|
|
15
|
+
stdout, stderr, status = Open3.capture3(command)
|
|
16
|
+
|
|
17
|
+
unless status.success?
|
|
18
|
+
@logger&.warn("Command failed with status #{status.exitstatus}: #{stderr}")
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
stdout.chomp
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "yaml"
|
|
4
|
+
|
|
5
|
+
module Spectracer
|
|
6
|
+
module IO
|
|
7
|
+
class ConfigLoader
|
|
8
|
+
FILE_PATH = ".spectracer.yml"
|
|
9
|
+
DEFAULT_FILE_PATH = File.expand_path("../../spectracer.default.yml", __dir__)
|
|
10
|
+
|
|
11
|
+
def initialize(logger: nil)
|
|
12
|
+
@logger = logger
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def load
|
|
16
|
+
raw_config = load_raw_config
|
|
17
|
+
return default_config if raw_config.nil?
|
|
18
|
+
|
|
19
|
+
parse_config(raw_config)
|
|
20
|
+
rescue => e
|
|
21
|
+
@logger&.error("Error loading configuration: #{e.message}")
|
|
22
|
+
default_config
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def load_raw_config
|
|
28
|
+
if File.exist?(FILE_PATH)
|
|
29
|
+
YAML.safe_load_file(FILE_PATH)
|
|
30
|
+
else
|
|
31
|
+
@logger&.debug("No configuration file found at #{FILE_PATH.inspect}. Using defaults.")
|
|
32
|
+
YAML.safe_load_file(DEFAULT_FILE_PATH)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def parse_config(raw)
|
|
37
|
+
defaults = raw.fetch("defaults", {})
|
|
38
|
+
on_empty = raw.fetch("on_empty_spec_set", "")
|
|
39
|
+
globs_matcher = raw.fetch("globs_matcher", {})
|
|
40
|
+
|
|
41
|
+
{
|
|
42
|
+
on_empty_spec_set: resolve_templates(on_empty, defaults),
|
|
43
|
+
globs: globs_matcher.transform_values { |v| resolve_templates(v, defaults) }
|
|
44
|
+
}
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def resolve_templates(template, defaults)
|
|
48
|
+
template.gsub(/\{\{(.*?)\}\}/) { defaults[$1] || "" }
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def default_config
|
|
52
|
+
{on_empty_spec_set: "", globs: {}}
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "json"
|
|
5
|
+
require "zlib"
|
|
6
|
+
|
|
7
|
+
module Spectracer
|
|
8
|
+
module IO
|
|
9
|
+
class DependencyStore
|
|
10
|
+
def initialize(logger: nil)
|
|
11
|
+
@logger = logger
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def read(file_path)
|
|
15
|
+
return {} unless File.exist?(file_path)
|
|
16
|
+
|
|
17
|
+
case File.extname(file_path)
|
|
18
|
+
when ".gz"
|
|
19
|
+
read_gzipped_json(file_path)
|
|
20
|
+
when ".json"
|
|
21
|
+
read_json(file_path)
|
|
22
|
+
else
|
|
23
|
+
@logger&.warn("Unknown file format: #{file_path}")
|
|
24
|
+
{}
|
|
25
|
+
end
|
|
26
|
+
rescue JSON::ParserError => e
|
|
27
|
+
@logger&.error("Failed to parse JSON file '#{file_path}': #{e.message}")
|
|
28
|
+
{}
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def write(data, file_path)
|
|
32
|
+
FileUtils.mkdir_p(File.dirname(file_path))
|
|
33
|
+
|
|
34
|
+
@logger&.debug("Writing to #{file_path}")
|
|
35
|
+
@logger&.debug(JSON.pretty_generate(data))
|
|
36
|
+
|
|
37
|
+
File.open(file_path, "wb") do |f|
|
|
38
|
+
Zlib::GzipWriter.wrap(f) do |gz|
|
|
39
|
+
gz.write(data.to_json)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def glob(pattern)
|
|
45
|
+
Dir.glob(pattern)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def read_gzipped_json(file_path)
|
|
51
|
+
Zlib::GzipReader.open(file_path) do |gz|
|
|
52
|
+
JSON.parse(gz.read)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def read_json(file_path)
|
|
57
|
+
JSON.parse(File.read(file_path))
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|