feature_map 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/README.md +269 -0
- data/bin/featuremap +5 -0
- data/lib/feature_map/cli.rb +243 -0
- data/lib/feature_map/code_features/plugin.rb +79 -0
- data/lib/feature_map/code_features/plugins/identity.rb +39 -0
- data/lib/feature_map/code_features.rb +152 -0
- data/lib/feature_map/configuration.rb +43 -0
- data/lib/feature_map/constants.rb +11 -0
- data/lib/feature_map/mapper.rb +78 -0
- data/lib/feature_map/output_color.rb +42 -0
- data/lib/feature_map/private/assignment_mappers/directory_assignment.rb +150 -0
- data/lib/feature_map/private/assignment_mappers/feature_definition_assignment.rb +68 -0
- data/lib/feature_map/private/assignment_mappers/feature_globs.rb +138 -0
- data/lib/feature_map/private/assignment_mappers/file_annotations.rb +158 -0
- data/lib/feature_map/private/assignments_file.rb +190 -0
- data/lib/feature_map/private/code_cov.rb +96 -0
- data/lib/feature_map/private/cyclomatic_complexity_calculator.rb +46 -0
- data/lib/feature_map/private/docs/index.html +247 -0
- data/lib/feature_map/private/documentation_site.rb +128 -0
- data/lib/feature_map/private/extension_loader.rb +24 -0
- data/lib/feature_map/private/feature_assigner.rb +22 -0
- data/lib/feature_map/private/feature_metrics_calculator.rb +76 -0
- data/lib/feature_map/private/feature_plugins/assignment.rb +17 -0
- data/lib/feature_map/private/glob_cache.rb +80 -0
- data/lib/feature_map/private/lines_of_code_calculator.rb +49 -0
- data/lib/feature_map/private/metrics_file.rb +86 -0
- data/lib/feature_map/private/test_coverage_file.rb +97 -0
- data/lib/feature_map/private/test_pyramid_file.rb +151 -0
- data/lib/feature_map/private/todo_inspector.rb +57 -0
- data/lib/feature_map/private/validations/features_up_to_date.rb +78 -0
- data/lib/feature_map/private/validations/files_have_features.rb +45 -0
- data/lib/feature_map/private/validations/files_have_unique_features.rb +34 -0
- data/lib/feature_map/private.rb +204 -0
- data/lib/feature_map/validator.rb +29 -0
- data/lib/feature_map.rb +212 -0
- metadata +253 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: f1243d054f38c9a5520a3790190ad36cf49a447c4e931d7e5cc85cd1a958eb20
|
4
|
+
data.tar.gz: 275ed70329b4df76a18412719ee004c8626cb53f708905f5ff3413846fab34e2
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 62100534249d7ccc2ec67849ca92bcaf38dc55bd6d22a3d1aefb673b6a6222c16cddbb742af38f895832d758de85ffe75c6f93fccf2b4d6c292d344566b04089
|
7
|
+
data.tar.gz: e055eaa7af6fe4a6e27c9f29a38e1c2d1a687cbf0f5ba500541a0b2b21cbec9a78b29cc23c95128995eb81142b98cbeee3d01e4dc454084e956f8a748f0b8e95
|
data/README.md
ADDED
@@ -0,0 +1,269 @@
|
|
1
|
+
# FeatureMap
|
2
|
+
|
3
|
+
This gem helps identify and manage features within large Ruby and Rails applications. This gem works best in large, usually monolithic code bases for applications that incorporate a wide range of features with various dependencies.
|
4
|
+
|
5
|
+
## Getting started
|
6
|
+
|
7
|
+
To get started there's a few things you should do.
|
8
|
+
|
9
|
+
1) Create a `.feature_map/config.yml` file and declare where your files live. Here's a sample to start with:
|
10
|
+
```yml
|
11
|
+
assigned_globs:
|
12
|
+
- '{app,components,config,frontend,lib,packs,spec}/**/*.{rb,rake,js,jsx,ts,tsx}'
|
13
|
+
unassigned_globs:
|
14
|
+
- db/**/*
|
15
|
+
- app/services/some_file1.rb
|
16
|
+
- app/services/some_file2.rb
|
17
|
+
- frontend/javascripts/**/__generated__/**/*
|
18
|
+
```
|
19
|
+
You may find a more comprehensive example in this repository's `.feature_map/config.yml`.
|
20
|
+
|
21
|
+
2) Define the features of our your application. There are two methods for defining features:
|
22
|
+
* YAML Definitions: Each feature can be defined in a separate YAML file within the `.feature_map/definitions` directory. Here's an example, that would live at `.feature_map/definitions/onboarding.yml`:
|
23
|
+
```yml
|
24
|
+
name: Onboarding
|
25
|
+
description: Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation.
|
26
|
+
documentation_link: https://www.notion.so/onboarding-feature-abcd1234
|
27
|
+
```
|
28
|
+
* CSV Definitions: All features can be defined within a single CSV file located at `.feature_map/feature_definitions.csv`. Here's an example of what that file might look like:
|
29
|
+
```
|
30
|
+
# Comment explaining the purpose of this file and how it should be managed.
|
31
|
+
|
32
|
+
Name,Description,Documentation Link,Custom Attribute
|
33
|
+
Onboarding,"Lorem ipsum dolor sit amet, consectetur adipiscing elit.",https://www.notion.so/onboarding-feature-abcd1234,Test 123
|
34
|
+
User Management,"Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation",,
|
35
|
+
```
|
36
|
+
3) Declare feature assignments. You can do this at a directory level or at a file level. All of the files within the `assigned_globs` you declared in step 1 will need to have a feature assigned (or be opted out via `unassigned_globs`). See the next section for more detail.
|
37
|
+
4) Run validations when you commit, and/or in CI. If you run validations in CI, ensure that if your `assignments.yml` file gets changed, that gets pushed to the PR. A `metrics.yml` file will also be generated but we recommend NOT commiting that file because it changes very frequently.
|
38
|
+
|
39
|
+
## Usage: Assigning Features
|
40
|
+
|
41
|
+
There are multiple ways to assign the feature for a source file using this gem.
|
42
|
+
|
43
|
+
### Directory-Based Assignment
|
44
|
+
Directory based assignment allows for all files in that directory and all its sub-directories to be assigned to a single feature. To define this, add a `.feature` file inside that directory with the name of the feature as the contents of that file.
|
45
|
+
|
46
|
+
### File-Annotation Based Assignment
|
47
|
+
File annotations are a last resort if there is no clear home for your code. File annotations go at the top of your file, and look like this:
|
48
|
+
```ruby
|
49
|
+
# @feature Onboarding
|
50
|
+
```
|
51
|
+
|
52
|
+
### Glob-Based Assignment
|
53
|
+
In the YML configuration of a feature, you can set `assigned_globs` to be a glob of files assigned to this feature. For example, in `onboarding.yml`:
|
54
|
+
```yml
|
55
|
+
name: Onboarding
|
56
|
+
assigned_globs:
|
57
|
+
- app/services/stuff_for_onboarding/**/**
|
58
|
+
- app/controllers/other_stuff_for_onboarding/**/**
|
59
|
+
```
|
60
|
+
|
61
|
+
### Feature Definition File Assignment
|
62
|
+
By default any feature definition YML files, located in the `.feature_map/definitions' directory, are assigned to their corresponding feature.
|
63
|
+
|
64
|
+
The leading `.` in the path for these files results in them being quoted within the resulting `.feature_map/assignments` file. The following is an example of this content:
|
65
|
+
```yml
|
66
|
+
---
|
67
|
+
files:
|
68
|
+
".feature_map/definitions/bar.yml":
|
69
|
+
feature: Bar
|
70
|
+
mapper: Feature definition file assignment
|
71
|
+
features:
|
72
|
+
Bar:
|
73
|
+
- ".feature_map/definitions/bar.yml"
|
74
|
+
```
|
75
|
+
|
76
|
+
In cases when the feature assignments for these files is irrelevant, this behavior can be disabled by setting the `ignore_feature_definitions` key in the `.feature_map/config.yml` file to `true`.
|
77
|
+
|
78
|
+
### Custom Assignment
|
79
|
+
To enable custom assignment, you can inject your own custom classes into `feature_map`.
|
80
|
+
To do this, first create a class that adheres to the `FeatureMap::Mapper` and/or `FeatureMap::Validator` interface.
|
81
|
+
Then, in `.feature_map/config.yml`, you can require that file:
|
82
|
+
```yml
|
83
|
+
require:
|
84
|
+
- ./lib/my_extension.rb
|
85
|
+
```
|
86
|
+
|
87
|
+
Now, `bin/featuremap validate` will automatically include your new mapper and/or validator. See [`spec/lib/feature_map/private/extension_loader_spec.rb](spec/lib/feature_map/private/extension_loader_spec.rb) for an example of what this looks like.
|
88
|
+
|
89
|
+
## Usage: Reading FeatureMap
|
90
|
+
|
91
|
+
Check out [`lib/feature_map.rb`](https://github.com/Beyond-Finance/feature_map/blob/main/lib/feature_map.rb) to see the public API.
|
92
|
+
|
93
|
+
Check out [`feature_map_spec.rb`](https://github.com/Beyond-Finance/feature_map/blob/main/spec/lib/feature_map_spec.rb) to see examples of how the feature map utility is used.
|
94
|
+
|
95
|
+
### `for_file`
|
96
|
+
`FeatureMap.for_file`, given a relative path to a file returns a `CodeFeatures::Feature` if there is a feature assigned to the file, `nil` otherwise.
|
97
|
+
|
98
|
+
```ruby
|
99
|
+
FeatureMap.for_file('path/to/file/relative/to/application/root.rb')
|
100
|
+
```
|
101
|
+
|
102
|
+
Contributor note: If you are making updates to this method or the methods getting used here, please benchmark the performance of the new implementation against the current for both `for_files` and `for_file` (with 1, 100, 1000 files).
|
103
|
+
|
104
|
+
See `feature_map_spec.rb` for examples.
|
105
|
+
|
106
|
+
### `for_backtrace`
|
107
|
+
`FeatureMap.for_backtrace` can be given a backtrace and will either return `nil`, or a `CodeFeatures::Feature`.
|
108
|
+
|
109
|
+
```ruby
|
110
|
+
FeatureMap.for_backtrace(exception.backtrace)
|
111
|
+
```
|
112
|
+
|
113
|
+
This will go through the backtrace, and return the feature of the first files with a feature assignment associated with frames within the backtrace.
|
114
|
+
|
115
|
+
See `feature_map_spec.rb` for an example.
|
116
|
+
|
117
|
+
### `for_class`
|
118
|
+
|
119
|
+
`FeatureMap.for_class` can be given a class and will either return `nil`, or a `CodeFeatures::Feature`.
|
120
|
+
|
121
|
+
```ruby
|
122
|
+
FeatureMap.for_class(MyClass)
|
123
|
+
```
|
124
|
+
|
125
|
+
Under the hood, this finds the file where the class is defined and returns the featuer assigned to that file.
|
126
|
+
|
127
|
+
See `feature_map_spec.rb` for an example.
|
128
|
+
|
129
|
+
### `for_feature`
|
130
|
+
`FeatureMap.for_feature` can be used to generate a feature report for a single feature.
|
131
|
+
```ruby
|
132
|
+
FeatureMap.for_feature('Onboarding')
|
133
|
+
```
|
134
|
+
|
135
|
+
You can shovel this into a markdown file for easy viewing using the CLI:
|
136
|
+
```
|
137
|
+
bin/feature_map for_feature 'Onboarding' > tmp/onboarding_feature_report.md
|
138
|
+
```
|
139
|
+
|
140
|
+
## Usage: Generating Feature Assignment files
|
141
|
+
|
142
|
+
When you run `bin/featuremap validate`, the following files will automatically be generated:
|
143
|
+
* `.feature_map/assignments.yml`: Captures a mapping of files within a repository to their corresponding feature and a mapping of features to their corresponding files.
|
144
|
+
* `.feature_map/metrics.yml`: Captures a set of metrics rolled up at the feature level (i.e. computed over all files assigned to the feature).
|
145
|
+
|
146
|
+
## Usage: Generating Documentation
|
147
|
+
|
148
|
+
The feature map gem captures valuable insights about the features of your application (e.g. metrics like ABC size, lines of code, and cyclomatic complexity). To review this information locally, you can run `bin/featuremap docs` to produce a single, self contained HTML file that includes a fully functional documentation site with useful diagrams and details about the features of your application. This file is created within the `.feature_map/docs` directory and the `index.html` file can loaded in the browser of your choice by running `open .feature_map/docs/index.html`.
|
149
|
+
|
150
|
+
**Example screenshot**
|
151
|
+
![Feature Map Docs Dashboard](readme_assets/feature-map-docs-dashboard.png)
|
152
|
+
|
153
|
+
## Usage: Generating the Test Pyramid
|
154
|
+
|
155
|
+
The feature map gem supports reporting on the [test pyramid](https://martinfowler.com/bliki/TestPyramid.html) coverage of the application and its constituent features. It works, broadly, by: accepting test execution reports (e.g., rspec's `json` format) for unit, integration, and regression tests -- and then matches those tests to their corresponding features as follows:
|
156
|
+
- Unit: Uses tech stack conventions, e.g., rspec tests in `spec/models/user_spec.rb` are resolved to the `app/models/user.rb` implementation file, which may be matched with its feature annotation.
|
157
|
+
- Integration: Integration tests do not correspond to a single implementation file (though generally live within the same codebase), so they must be directly annotated with the feature they support.
|
158
|
+
- Regression: Regression tests do not correspond to a single implementation file, and may live in a given codebase or in a separate repository. They must both be tagged with the feature that they support, and a reference to the assignments of regression tests must be passed to the test pyramid generation command.
|
159
|
+
|
160
|
+
Once test pyramid data has been generated for a give project, it's automatically included in that project's doc site.
|
161
|
+
|
162
|
+
To generate the test pyramid data, run:
|
163
|
+
> bin/featuremap test_pyramid [unit_examples_file] [integration_examples_file] [regression_examples_file] [regression_assignments_file]
|
164
|
+
|
165
|
+
Note: FeatureMap currently only supports _examples files_ in rspec's `json` format (--format json), though support for others (e.g., jest's `--json`, or the JUnit XML format) may be added in the future.
|
166
|
+
|
167
|
+
## Usage: Collecting Test Coverage
|
168
|
+
|
169
|
+
When you run `bin/featuremap test_coverage`, the test coverage statistics the latest commit on the main branch will be pulled from [CodeCov](https://codecov.io/) and collected into a set of per-feature test coverage statistics. This feature level test coverage data is then captured in the `.feature_map/test-coverage.yml` file.
|
170
|
+
|
171
|
+
This command requires the following CodeCov account settings to be configured within the `.feature_map/config.yml` file:
|
172
|
+
|
173
|
+
```yml
|
174
|
+
code_cov:
|
175
|
+
service: github
|
176
|
+
owner: Acme-Org
|
177
|
+
repo: sample_app
|
178
|
+
```
|
179
|
+
|
180
|
+
See the [CodeCov API docs](https://docs.codecov.com/reference/repos_retrieve) for more information about the expected values for these configurations.
|
181
|
+
|
182
|
+
Test coverage statistics can be pulled for a specific commit (e.g. the latest commit on a feature branch) by including the full commit SHA as an argument at the end of this CLI command (e.g. `bin/featuremap test_coverage ae80a927654997be4f48d3dbcd1320083cf22eea`). Before running this command please check the [CodeCov dashboard](https://app.codecov.io/) for your application to ensure test coverage statistics have been reported for this commit.
|
183
|
+
|
184
|
+
### CodeCov API Token Generation
|
185
|
+
|
186
|
+
Running the `bin/featuremap test_coverage` requires an active CodeCov API access token to be specified in the `CODECOV_API_TOKEN` environment variable of your shell session. This token is used to retrieve coverage statistics from the CodeCov account configured in the `.feature_map/config.yml` file.
|
187
|
+
|
188
|
+
Use the following steps to generate a new CodeCov API token:
|
189
|
+
|
190
|
+
1. Log into your [CodeCov account](https://app.codecov.io/)
|
191
|
+
1. Click the "Settings" menu option in the profile dropdown menu in the top right corner of the screen
|
192
|
+
![CodeCov profile menu with Settings menu highlighted](readme_assets/codeCov-profileMenu.png)
|
193
|
+
1. Click the "Access" menu option from the left-hand navigation menu of the Settings page
|
194
|
+
![CodeCov Settings page navigation bar with Access menu highlighted](readme_assets/codeCov-settingsMenu.png)
|
195
|
+
1. Click the "Generate Token" button in the "API Tokens" section of the page
|
196
|
+
![CodeCov Access page with Generate Token button highlighted](readme_assets/codeCov-apiTokensTable.png)
|
197
|
+
1. Enter a descriptive name for the token (e.g. FeatureMap CLI) and click the "Generate Token" button
|
198
|
+
![CodeCov API token creation modal](readme_assets/codeCov-createTokenModal.png)
|
199
|
+
1. __IMPORTANT__: Copy the access token value presented on the screen and store it in a secure location (e.g. 1Password entry, BitWarden entry, etc)
|
200
|
+
![CodeCov newly created API token modal](readme_assets/codeCov-newTokenModal.png)
|
201
|
+
|
202
|
+
#### __OPTIONAL__: Store the token as an environment variable in your shell's environment:
|
203
|
+
**ZSH**
|
204
|
+
```shell
|
205
|
+
echo 'export CODECOV_API_TOKEN="YOUR_CODECOV_API_TOKEN"' >> ~/.zshrc
|
206
|
+
source ~/.zshrc
|
207
|
+
```
|
208
|
+
|
209
|
+
**Bash**
|
210
|
+
```shell
|
211
|
+
echo 'export CODECOV_API_TOKEN="YOUR_CODECOV_API_TOKEN"' >> ~/.bashrc
|
212
|
+
source ~/.bashrc
|
213
|
+
```
|
214
|
+
|
215
|
+
## Proper Configuration & Validation
|
216
|
+
|
217
|
+
FeatureMap comes with a validation function to ensure the following things are true:
|
218
|
+
|
219
|
+
1) Only one mechanism is defining the feature assignment for a file. That is -- you can't have a file annotation on a file assigned via glob-based assignment. This helps make feature assignment behavior more clear by avoiding concerns about precedence.
|
220
|
+
2) All features referenced as an assignment for any file is a valid feature (i.e. it's in the list of `CodeFeatures.all`).
|
221
|
+
3) All files have a feature assigned. You can specify in `unassigned_globs` to represent a TODO list of files to add feature assignments to.
|
222
|
+
* Teams using the [CodeOwnership](https://github.com/rubyatscale/code_ownership/tree/main) gem include a `require_assignment_for_teams` key in the `.feature_map/config.yml` file to have this validation to apply a specific list of team. This allows feature assignments to be rolled out in a gradual manner on a team-by-team basis. The `require_assignment_for_teams` configuration should contain a list of team names (i.e. the value from the `name` key in the associated `config/teams/*.yml` file) for the teams whose files will be included in this validation.
|
223
|
+
3) The `assignments.yml` file is up to date. This is automatically corrected and staged unless specified otherwise with `bin/featuremap validate --skip-autocorrect --skip-stage`. You can turn this validation off by setting `skip_features_validation: true` in `.feature_map/config.yml`.
|
224
|
+
|
225
|
+
FeatureMap also allows you to specify which globs and file extensions should be considered assignable.
|
226
|
+
|
227
|
+
Here is an example `.feature_map/config.yml`.
|
228
|
+
```yml
|
229
|
+
assigned_globs:
|
230
|
+
- '{app,components,config,frontend,lib,packs,spec}/**/*.{rb,rake,js,jsx,ts,tsx}'
|
231
|
+
unassigned_globs:
|
232
|
+
- db/**/*
|
233
|
+
- app/services/some_file1.rb
|
234
|
+
- app/services/some_file2.rb
|
235
|
+
- frontend/javascripts/**/__generated__/**/*
|
236
|
+
```
|
237
|
+
You can call the validation function with the Ruby API
|
238
|
+
```ruby
|
239
|
+
FeatureMap.validate!
|
240
|
+
```
|
241
|
+
or the CLI
|
242
|
+
```
|
243
|
+
bin/featuremap validate
|
244
|
+
```
|
245
|
+
|
246
|
+
## Development
|
247
|
+
|
248
|
+
Contributions are welcome and appreciated. Here's how to get started:
|
249
|
+
|
250
|
+
- clone repo: `$ git clone git@github.com:Beyond-Finance/feature_map.git`
|
251
|
+
- install dependencies: `$ bundle install`
|
252
|
+
- run tests: `$ bundle exec rspec`
|
253
|
+
- run Rubocop: `$ bundle exec rubocop`
|
254
|
+
- run Sorbet: `$ bundle exec srb tc`
|
255
|
+
|
256
|
+
That's it! Assuming you can complete all of these steps without any error or issues, you should be good to go.
|
257
|
+
|
258
|
+
#### Publication
|
259
|
+
|
260
|
+
When a new version of the gem is ready to be published, please follow these steps:
|
261
|
+
|
262
|
+
* Update `spec.version` value in the (feature_map.gemspec)[feature_map.gemspec] file.
|
263
|
+
* Assign a version to this release in accordance with [Semantic Versioning](https://semver.org/) based on the changes contained in this release.
|
264
|
+
* Create a new release tag in Github ([link](https://github.com/Beyond-Finance/feature_map/releases)) with a value that matches the new Gemspec version.
|
265
|
+
* Checkout the release tag in your local environment.
|
266
|
+
* Publish the new version of the gem to RubyGems ([docs](https://guides.rubygems.org/publishing/#publishing-to-rubygemsorg)), which largely consists of running the following commands:
|
267
|
+
* Build a new version of the gem: `gem build feature_map.gemspec`
|
268
|
+
* Authenticate with rubygems.org: `gem signin`
|
269
|
+
* Publish the new version of the gem: `gem push feature_map-[NEW_VERSION].gem`
|
data/bin/featuremap
ADDED
@@ -0,0 +1,243 @@
|
|
1
|
+
# typed: true
|
2
|
+
|
3
|
+
require 'optparse'
|
4
|
+
require 'pathname'
|
5
|
+
require 'fileutils'
|
6
|
+
require 'feature_map/output_color'
|
7
|
+
|
8
|
+
module FeatureMap
|
9
|
+
class Cli
|
10
|
+
def self.run!(argv)
|
11
|
+
command = argv.shift
|
12
|
+
if command == 'validate'
|
13
|
+
validate!(argv)
|
14
|
+
elsif command == 'docs'
|
15
|
+
docs!(argv)
|
16
|
+
elsif command == 'test_coverage'
|
17
|
+
test_coverage!(argv)
|
18
|
+
elsif command == 'test_pyramid'
|
19
|
+
test_pyramid!(argv)
|
20
|
+
elsif command == 'for_file'
|
21
|
+
for_file(argv)
|
22
|
+
elsif command == 'for_feature'
|
23
|
+
for_feature(argv)
|
24
|
+
elsif [nil, 'help'].include?(command)
|
25
|
+
puts <<~USAGE
|
26
|
+
Usage: bin/featuremap <subcommand>
|
27
|
+
|
28
|
+
Subcommands:
|
29
|
+
validate - run all validations
|
30
|
+
docs - generates feature documentation
|
31
|
+
test_coverage - generates per-feature test coverage statistics
|
32
|
+
test_pyramid - generates per-feature test pyramid (unit, integration, regression) statistics
|
33
|
+
for_file - find feature assignment for a single file
|
34
|
+
for_feature - find assignment information for a feature
|
35
|
+
help - display help information about feature_map
|
36
|
+
USAGE
|
37
|
+
else
|
38
|
+
puts "'#{command}' is not a feature_map command. See `bin/featuremap help`."
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def self.validate!(argv)
|
43
|
+
options = {}
|
44
|
+
|
45
|
+
parser = OptionParser.new do |opts|
|
46
|
+
opts.banner = 'Usage: bin/featuremap validate [options]'
|
47
|
+
|
48
|
+
opts.on('--skip-autocorrect', 'Skip automatically correcting any errors, such as the .feature_map/assignments.yml file') do
|
49
|
+
options[:skip_autocorrect] = true
|
50
|
+
end
|
51
|
+
|
52
|
+
opts.on('-d', '--diff', 'Only run validations with staged files') do
|
53
|
+
options[:diff] = true
|
54
|
+
end
|
55
|
+
|
56
|
+
opts.on('-s', '--skip-stage', 'Skips staging the .feature_map/assignments.yml file') do
|
57
|
+
options[:skip_stage] = true
|
58
|
+
end
|
59
|
+
|
60
|
+
opts.on('--help', 'Shows this prompt') do
|
61
|
+
puts opts
|
62
|
+
exit
|
63
|
+
end
|
64
|
+
end
|
65
|
+
args = parser.order!(argv)
|
66
|
+
parser.parse!(args)
|
67
|
+
|
68
|
+
files = if options[:diff]
|
69
|
+
ENV.fetch('FEATUREMAP_GIT_STAGED_FILES') { `git diff --staged --name-only` }.split("\n").select do |file|
|
70
|
+
File.exist?(file)
|
71
|
+
end
|
72
|
+
else
|
73
|
+
nil
|
74
|
+
end
|
75
|
+
|
76
|
+
FeatureMap.validate!(
|
77
|
+
files: files,
|
78
|
+
autocorrect: !options[:skip_autocorrect],
|
79
|
+
stage_changes: !options[:skip_stage]
|
80
|
+
)
|
81
|
+
|
82
|
+
puts OutputColor.green('FeatureMap validation complete.')
|
83
|
+
end
|
84
|
+
|
85
|
+
def self.docs!(argv)
|
86
|
+
options = {}
|
87
|
+
|
88
|
+
parser = OptionParser.new do |opts|
|
89
|
+
opts.banner = 'Usage: bin/featuremap docs [options] [target_commit_sha]'
|
90
|
+
|
91
|
+
opts.on('-s', '--skip-stage', 'Skips staging the .feature_map/assignments.yml file') do
|
92
|
+
options[:skip_stage] = true
|
93
|
+
end
|
94
|
+
|
95
|
+
opts.on('--skip-validate', 'Skip the execution of the validate command, using the existing feature output files') do
|
96
|
+
options[:skip_validate] = true
|
97
|
+
end
|
98
|
+
|
99
|
+
opts.on('--help', 'Shows this prompt') do
|
100
|
+
puts opts
|
101
|
+
exit
|
102
|
+
end
|
103
|
+
end
|
104
|
+
args = parser.order!(argv)
|
105
|
+
parser.parse!(args)
|
106
|
+
|
107
|
+
non_flag_args = argv.reject { |arg| arg.start_with?('--') }
|
108
|
+
custom_git_ref = non_flag_args[0]
|
109
|
+
|
110
|
+
FeatureMap.validate!(stage_changes: !options[:skip_stage]) unless options[:skip_validate]
|
111
|
+
|
112
|
+
FeatureMap.generate_docs!(custom_git_ref)
|
113
|
+
|
114
|
+
puts OutputColor.green('FeatureMap documentaiton site generated.')
|
115
|
+
puts 'Open .feature_map/docs/index.html in a browser to view the documentation site.'
|
116
|
+
end
|
117
|
+
|
118
|
+
def self.test_coverage!(argv)
|
119
|
+
parser = OptionParser.new do |opts|
|
120
|
+
opts.banner = <<~MSG
|
121
|
+
Usage: bin/featuremap test_coverage [options] [code_cov_commit_sha].
|
122
|
+
Note: Requires environment variable `CODECOV_API_TOKEN`.
|
123
|
+
MSG
|
124
|
+
|
125
|
+
opts.on('--help', 'Shows this prompt') do
|
126
|
+
puts opts
|
127
|
+
exit
|
128
|
+
end
|
129
|
+
end
|
130
|
+
args = parser.order!(argv)
|
131
|
+
parser.parse!(args)
|
132
|
+
non_flag_args = argv.reject { |arg| arg.start_with?('--') }
|
133
|
+
custom_commit_sha = non_flag_args[0]
|
134
|
+
|
135
|
+
code_cov_token = ENV.fetch('CODECOV_API_TOKEN', '')
|
136
|
+
raise 'Please specify a CodeCov API token in your environment as `CODECOV_API_TOKEN`' if code_cov_token.empty?
|
137
|
+
|
138
|
+
# If no commit SHA was providid in the CLI command args, use the most recent commit of the main branch in the upstream remote.
|
139
|
+
commit_sha = custom_commit_sha || `git log -1 --format=%H origin/main`.chomp
|
140
|
+
puts "Pulling test coverage statistics for commit #{commit_sha}"
|
141
|
+
|
142
|
+
FeatureMap.gather_test_coverage!(commit_sha, code_cov_token)
|
143
|
+
|
144
|
+
puts OutputColor.green('FeatureMap test coverage statistics collected.')
|
145
|
+
puts 'View the resulting test coverage for each feature in .feature_map/test-coverage.yml'
|
146
|
+
end
|
147
|
+
|
148
|
+
def self.test_pyramid!(argv)
|
149
|
+
parser = OptionParser.new do |opts|
|
150
|
+
opts.banner = <<~MSG
|
151
|
+
Usage: bin/featuremap test_pyramid [unit_path] [integration_path] [regression_path] [regression_assignments_path].
|
152
|
+
Paths should point to files containing json-formatted rspec test summaries.
|
153
|
+
These can be generated via rspec's `-f j` flag.
|
154
|
+
MSG
|
155
|
+
|
156
|
+
opts.on('--help', 'Shows this prompt') do
|
157
|
+
puts opts
|
158
|
+
exit
|
159
|
+
end
|
160
|
+
end
|
161
|
+
args = parser.order!(argv)
|
162
|
+
parser.parse!(args)
|
163
|
+
non_flag_args = argv.reject { |arg| arg.start_with?('--') }
|
164
|
+
|
165
|
+
file_paths = non_flag_args.first(4)
|
166
|
+
raise 'Please specify all of [unit_path] [integration_path] [regression_path] [regression_assignments_path]' if file_paths.compact.size != 4
|
167
|
+
|
168
|
+
unit_path, integration_path, regression_path, regression_assignments_path = file_paths
|
169
|
+
FeatureMap.generate_test_pyramid!(unit_path, integration_path, regression_path, regression_assignments_path)
|
170
|
+
end
|
171
|
+
|
172
|
+
# For now, this just returns feature assignment
|
173
|
+
# Later, this could also return feature assignment errors about that file.
|
174
|
+
def self.for_file(argv)
|
175
|
+
options = {}
|
176
|
+
|
177
|
+
# Long-term, we probably want to use something like `thor` so we don't have to implement logic
|
178
|
+
# like this. In the short-term, this is a simple way for us to use the built-in OptionParser
|
179
|
+
# while having an ergonomic CLI.
|
180
|
+
files = argv.reject { |arg| arg.start_with?('--') }
|
181
|
+
|
182
|
+
parser = OptionParser.new do |opts|
|
183
|
+
opts.banner = 'Usage: bin/featuremap for_file [options]'
|
184
|
+
|
185
|
+
opts.on('--json', 'Output as JSON') do
|
186
|
+
options[:json] = true
|
187
|
+
end
|
188
|
+
|
189
|
+
opts.on('--help', 'Shows this prompt') do
|
190
|
+
puts opts
|
191
|
+
exit
|
192
|
+
end
|
193
|
+
end
|
194
|
+
args = parser.order!(argv)
|
195
|
+
parser.parse!(args)
|
196
|
+
|
197
|
+
if files.count != 1
|
198
|
+
raise 'Please pass in one file. Use `bin/featuremap for_file --help` for more info'
|
199
|
+
end
|
200
|
+
|
201
|
+
feature = FeatureMap.for_file(files.first)
|
202
|
+
|
203
|
+
feature_name = feature&.name || 'Unassigned'
|
204
|
+
feature_yml = feature&.config_yml || 'Unassigned'
|
205
|
+
|
206
|
+
if options[:json]
|
207
|
+
json = {
|
208
|
+
feature_name: feature_name,
|
209
|
+
feature_yml: feature_yml
|
210
|
+
}
|
211
|
+
|
212
|
+
puts json.to_json
|
213
|
+
else
|
214
|
+
puts <<~MSG
|
215
|
+
Feature: #{feature_name}
|
216
|
+
Feature YML: #{feature_yml}
|
217
|
+
MSG
|
218
|
+
end
|
219
|
+
end
|
220
|
+
|
221
|
+
def self.for_feature(argv)
|
222
|
+
parser = OptionParser.new do |opts|
|
223
|
+
opts.banner = 'Usage: bin/featuremap for_feature \'Onboarding\''
|
224
|
+
|
225
|
+
opts.on('--help', 'Shows this prompt') do
|
226
|
+
puts opts
|
227
|
+
exit
|
228
|
+
end
|
229
|
+
end
|
230
|
+
features = argv.reject { |arg| arg.start_with?('--') }
|
231
|
+
args = parser.order!(argv)
|
232
|
+
parser.parse!(args)
|
233
|
+
|
234
|
+
if features.count != 1
|
235
|
+
raise 'Please pass in one feature. Use `bin/featuremap for_feature --help` for more info'
|
236
|
+
end
|
237
|
+
|
238
|
+
puts FeatureMap.for_feature(features.first)
|
239
|
+
end
|
240
|
+
|
241
|
+
private_class_method :validate!
|
242
|
+
end
|
243
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
# typed: strict
|
2
|
+
|
3
|
+
module FeatureMap
|
4
|
+
module CodeFeatures
|
5
|
+
# Plugins allow a client to add validation on custom keys in the feature YML.
|
6
|
+
# For now, only a single plugin is allowed to manage validation on a top-level key.
|
7
|
+
# In the future we can think of allowing plugins to be gracefully merged with each other.
|
8
|
+
class Plugin
|
9
|
+
extend T::Helpers
|
10
|
+
extend T::Sig
|
11
|
+
|
12
|
+
abstract!
|
13
|
+
|
14
|
+
sig { params(feature: Feature).void }
|
15
|
+
def initialize(feature)
|
16
|
+
@feature = feature
|
17
|
+
end
|
18
|
+
|
19
|
+
sig { params(base: T.untyped).void }
|
20
|
+
def self.inherited(base) # rubocop:disable Lint/MissingSuper
|
21
|
+
all_plugins << T.cast(base, T.class_of(Plugin))
|
22
|
+
end
|
23
|
+
|
24
|
+
sig { returns(T::Array[T.class_of(Plugin)]) }
|
25
|
+
def self.all_plugins
|
26
|
+
@all_plugins ||= T.let(@all_plugins, T.nilable(T::Array[T.class_of(Plugin)]))
|
27
|
+
@all_plugins ||= []
|
28
|
+
@all_plugins
|
29
|
+
end
|
30
|
+
|
31
|
+
sig { params(features: T::Array[Feature]).returns(T::Array[String]) }
|
32
|
+
def self.validation_errors(features)
|
33
|
+
[]
|
34
|
+
end
|
35
|
+
|
36
|
+
sig { params(feature: Feature).returns(T.attached_class) }
|
37
|
+
def self.for(feature)
|
38
|
+
register_feature(feature)
|
39
|
+
end
|
40
|
+
|
41
|
+
sig { params(feature: Feature, key: String).returns(String) }
|
42
|
+
def self.missing_key_error_message(feature, key)
|
43
|
+
"#{feature.name} is missing required key `#{key}`"
|
44
|
+
end
|
45
|
+
|
46
|
+
sig { returns(T::Hash[T.nilable(String), T::Hash[T.class_of(Plugin), Plugin]]) }
|
47
|
+
def self.registry
|
48
|
+
@registry ||= T.let(@registry, T.nilable(T::Hash[String, T::Hash[T.class_of(Plugin), Plugin]]))
|
49
|
+
@registry ||= {}
|
50
|
+
@registry
|
51
|
+
end
|
52
|
+
|
53
|
+
sig { params(feature: Feature).returns(T.attached_class) }
|
54
|
+
def self.register_feature(feature)
|
55
|
+
# We pull from the hash since `feature.name` uses the registry
|
56
|
+
feature_name = feature.raw_hash['name']
|
57
|
+
|
58
|
+
registry[feature_name] ||= {}
|
59
|
+
registry_for_feature = registry[feature_name] || {}
|
60
|
+
registry[feature_name] ||= {}
|
61
|
+
registry_for_feature[self] ||= new(feature)
|
62
|
+
T.unsafe(registry_for_feature[self])
|
63
|
+
end
|
64
|
+
|
65
|
+
sig { void }
|
66
|
+
def self.bust_caches!
|
67
|
+
all_plugins.each(&:clear_feature_registry!)
|
68
|
+
end
|
69
|
+
|
70
|
+
sig { void }
|
71
|
+
def self.clear_feature_registry!
|
72
|
+
@registry = nil
|
73
|
+
end
|
74
|
+
|
75
|
+
private_class_method :registry
|
76
|
+
private_class_method :register_feature
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# typed: true
|
2
|
+
|
3
|
+
module FeatureMap
|
4
|
+
module CodeFeatures
|
5
|
+
module Plugins
|
6
|
+
class Identity < Plugin
|
7
|
+
extend T::Sig
|
8
|
+
extend T::Helpers
|
9
|
+
|
10
|
+
IdentityStruct = Struct.new(:name)
|
11
|
+
|
12
|
+
sig { returns(IdentityStruct) }
|
13
|
+
def identity
|
14
|
+
IdentityStruct.new(
|
15
|
+
@feature.raw_hash['name']
|
16
|
+
)
|
17
|
+
end
|
18
|
+
|
19
|
+
sig { override.params(features: T::Array[CodeFeatures::Feature]).returns(T::Array[String]) }
|
20
|
+
def self.validation_errors(features)
|
21
|
+
errors = T.let([], T::Array[String])
|
22
|
+
|
23
|
+
uniq_set = Set.new
|
24
|
+
features.each do |feature|
|
25
|
+
for_feature = self.for(feature)
|
26
|
+
|
27
|
+
if !uniq_set.add?(for_feature.identity.name)
|
28
|
+
errors << "More than 1 definition for #{for_feature.identity.name} found"
|
29
|
+
end
|
30
|
+
|
31
|
+
errors << missing_key_error_message(feature, 'name') if for_feature.identity.name.nil?
|
32
|
+
end
|
33
|
+
|
34
|
+
errors
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|