Skip to content

Instantly share code, notes, and snippets.

@meatherly
Last active September 17, 2024 17:53
Show Gist options
  • Save meatherly/e99a86b339b4a3efa53472b6a92197b6 to your computer and use it in GitHub Desktop.
Save meatherly/e99a86b339b4a3efa53472b6a92197b6 to your computer and use it in GitHub Desktop.
Rspec Metadata Modifier Script
#!/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