Last active
September 17, 2024 17:53
-
-
Save meatherly/e99a86b339b4a3efa53472b6a92197b6 to your computer and use it in GitHub Desktop.
Rspec Metadata Modifier Script
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env ruby | |
require 'rspec' | |
require 'parser/current' | |
require 'unparser' | |
require 'optparse' | |
# This was generated by AI | |
# Install the required gems | |
# $ gem install parser unparser rspec | |
# Parse command-line arguments | |
options = {} | |
OptionParser.new do |opts| | |
opts.banner = 'Usage: modify_spec_metadata.rb PATH --tag TAG [options]' | |
opts.on('-t', '--tag TAG', | |
'Tag to add or modify metadata. Use a symbol (e.g. :on_string) or key-value pair (e.g. type:request)') do |v| | |
options[:tag] = v | |
end | |
opts.on('-r', '--remove-metadata', 'Remove existing metadata before adding new metadata') do |_v| | |
options[:remove_metadata] = true | |
end | |
end.parse! | |
# Ensure path and tag are provided | |
path = ARGV[0] | |
unless path && options[:tag] | |
puts 'Error: PATH and --tag are required.' | |
exit 1 | |
end | |
# Parse the tag option | |
def parse_tag(tag) | |
return { tag.to_sym => true } if tag.start_with?(':') | |
# Simple symbol tag | |
# Key-value pair | |
key, value = tag.split(':', 2) | |
key_sym = key.strip.to_sym | |
value_sym = value.strip.to_sym | |
{ key_sym => value_sym } | |
end | |
parsed_tag = parse_tag(options[:tag]) | |
# Set up RSpec filters if needed | |
RSpec.configure do |config| | |
config.filter_run_including parsed_tag.keys.first if options[:tag] | |
end | |
# Load RSpec examples from the given path | |
rspec_command = [path] | |
RSpec::Core::Runner.run(rspec_command) | |
# Collect all files with example groups | |
example_files = RSpec.world.example_groups.map(&:metadata).map { |m| m[:file_path] }.uniq | |
# AST manipulation class to modify describe blocks | |
class DescribeBlockModifier < Parser::TreeRewriter | |
def initialize(filename, remove_metadata, tag_data) | |
super() | |
@filename = filename | |
@remove_metadata = remove_metadata | |
@tag_data = tag_data | |
@in_outermost_describe = false | |
end | |
def on_send(node) | |
method_name = node.children[1] | |
return unless method_name == :describe && !@in_outermost_describe | |
@in_outermost_describe = true | |
# Section to modify metadata of the outermost describe block | |
##################################################################### | |
# Here, we either remove the existing metadata or merge new metadata. | |
# | |
# This section is controlled by the --remove-metadata flag. | |
# If the flag is present, we remove any existing metadata and add new metadata. | |
# If the flag is not present, we append new metadata to the existing metadata. | |
# Check if the describe block already has metadata | |
metadata_node = node.children.find { |n| n.type == :hash } | |
if @remove_metadata | |
# Remove existing metadata and add new metadata | |
if metadata_node | |
# Replace existing metadata with new metadata | |
replace(metadata_node.loc.expression, @tag_data.map { |k, v| "#{k}: #{v.inspect}" }.join(', ')) | |
else | |
# Add new metadata if none exists | |
insert_after(node.loc.expression, ", #{@tag_data.map { |k, v| "#{k}: #{v.inspect}" }.join(', ')}") | |
end | |
elsif metadata_node | |
# Merge new metadata into the existing metadata | |
existing_metadata = metadata_node.children.first.children.map { |k, v| [k, v] }.to_h | |
@tag_data.each do |key, value| | |
if existing_metadata.key?(key) | |
# Add value to existing key | |
if existing_metadata[key].is_a?(Array) | |
existing_metadata[key] << value unless existing_metadata[key].include?(value) | |
else | |
existing_metadata[key] = [existing_metadata[key], value].uniq | |
end | |
else | |
# Add new key-value pair | |
existing_metadata[key] = value | |
end | |
end | |
new_metadata = existing_metadata.map { |k, v| "#{k}: #{v.inspect}" }.join(', ') | |
replace(metadata_node.loc.expression, new_metadata) | |
# Modify existing metadata | |
else | |
# Add new metadata if none exists | |
insert_after(node.loc.expression, ", #{@tag_data.map { |k, v| "#{k}: #{v.inspect}" }.join(', ')}") | |
end | |
##################################################################### | |
end | |
def on_end(_node) | |
# Reset for the next file or the next outermost describe block | |
@in_outermost_describe = false | |
end | |
end | |
# Function to process each file and modify the outermost describe block | |
def process_file(file_path, remove_metadata, tag_data) | |
# Read the Ruby file | |
source_code = File.read(file_path) | |
# Parse the Ruby code into an AST | |
buffer = Parser::Source::Buffer.new(file_path) | |
buffer.source = source_code | |
parser = Parser::CurrentRuby.new | |
ast = parser.parse(buffer) | |
# Rewrite the AST | |
rewriter = DescribeBlockModifier.new(file_path, remove_metadata, tag_data) | |
new_source_code = rewriter.rewrite(buffer, ast) | |
# Write the modified AST back to the file if changes were made | |
if new_source_code != source_code | |
File.write(file_path, new_source_code) | |
puts "Modified: #{file_path}" | |
else | |
puts "No changes in: #{file_path}" | |
end | |
end | |
# Process all example files that RSpec loaded | |
example_files.each do |file| | |
process_file(file, options[:remove_metadata], parsed_tag) | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment