mirror of
https://github.com/samsonjs/samhuri.net.git
synced 2026-04-12 11:45:53 +00:00
285 lines
7.5 KiB
Ruby
Executable file
285 lines
7.5 KiB
Ruby
Executable file
#!/usr/bin/env ruby
|
|
|
|
require 'optparse'
|
|
require 'fileutils'
|
|
require 'digest'
|
|
require 'json'
|
|
require 'nokogiri'
|
|
|
|
begin
|
|
require 'htmlbeautifier'
|
|
rescue LoadError
|
|
# Optional dependency used only for nicer diffs.
|
|
end
|
|
|
|
class OutputValidator
|
|
def initialize(swift_dir:, ruby_dir:, verbose:, ignore_patterns:, show_details:, dump_dir:)
|
|
@swift_dir = swift_dir
|
|
@ruby_dir = ruby_dir
|
|
@differences = []
|
|
@missing_in_ruby = []
|
|
@missing_in_swift = []
|
|
@identical_count = 0
|
|
@verbose = verbose
|
|
@ignore_patterns = ignore_patterns
|
|
@show_details = show_details
|
|
@dump_dir = dump_dir
|
|
prepare_dump_dirs if @dump_dir
|
|
end
|
|
|
|
def validate
|
|
puts "Comparing outputs:"
|
|
puts " Swift: #{@swift_dir}"
|
|
puts " Ruby: #{@ruby_dir}"
|
|
puts ""
|
|
|
|
swift_files = find_html_files(@swift_dir)
|
|
ruby_files = find_html_files(@ruby_dir)
|
|
|
|
puts "Found #{swift_files.length} Swift output files"
|
|
puts "Found #{ruby_files.length} Ruby output files"
|
|
puts ""
|
|
|
|
compare_files(swift_files, ruby_files)
|
|
print_summary
|
|
end
|
|
|
|
private
|
|
|
|
def find_html_files(dir)
|
|
Dir.glob(File.join(dir, '**', '*.{html,xml,json,css,js,png,jpg,jpeg,gif,svg,ico,woff,woff2,ttf,eot}'))
|
|
.map { |f| f.sub("#{dir}/", '') }
|
|
.sort
|
|
end
|
|
|
|
def compare_files(swift_files, ruby_files)
|
|
all_files = (swift_files + ruby_files).uniq.sort
|
|
|
|
all_files.each do |relative_path|
|
|
next if ignored?(relative_path)
|
|
|
|
swift_path = File.join(@swift_dir, relative_path)
|
|
ruby_path = File.join(@ruby_dir, relative_path)
|
|
|
|
if !File.exist?(swift_path)
|
|
@missing_in_swift << relative_path
|
|
elsif !File.exist?(ruby_path)
|
|
@missing_in_ruby << relative_path
|
|
else
|
|
compare_file_contents(relative_path, swift_path, ruby_path)
|
|
end
|
|
end
|
|
end
|
|
|
|
def compare_file_contents(relative_path, swift_path, ruby_path)
|
|
if binary_file?(relative_path)
|
|
swift_content = File.binread(swift_path)
|
|
ruby_content = File.binread(ruby_path)
|
|
else
|
|
swift_content = normalize_content(relative_path, File.read(swift_path))
|
|
ruby_content = normalize_content(relative_path, File.read(ruby_path))
|
|
dump_normalized(relative_path, swift_content, ruby_content) if @dump_dir
|
|
end
|
|
|
|
if swift_content == ruby_content
|
|
@identical_count += 1
|
|
puts "✓ #{relative_path}" if @verbose
|
|
else
|
|
@differences << {
|
|
path: relative_path,
|
|
swift_hash: Digest::SHA256.hexdigest(swift_content),
|
|
ruby_hash: Digest::SHA256.hexdigest(ruby_content),
|
|
swift_size: swift_content.length,
|
|
ruby_size: ruby_content.length
|
|
}
|
|
puts "✗ #{relative_path} (differs)"
|
|
end
|
|
end
|
|
|
|
def prepare_dump_dirs
|
|
FileUtils.rm_rf(@dump_dir)
|
|
FileUtils.mkdir_p(File.join(@dump_dir, 'swift'))
|
|
FileUtils.mkdir_p(File.join(@dump_dir, 'ruby'))
|
|
end
|
|
|
|
def dump_normalized(relative_path, swift_content, ruby_content)
|
|
write_normalized(File.join(@dump_dir, 'swift', relative_path), swift_content)
|
|
write_normalized(File.join(@dump_dir, 'ruby', relative_path), ruby_content)
|
|
end
|
|
|
|
def write_normalized(path, content)
|
|
FileUtils.mkdir_p(File.dirname(path))
|
|
File.write(path, content)
|
|
end
|
|
|
|
def ignored?(path)
|
|
return false if @ignore_patterns.empty?
|
|
|
|
@ignore_patterns.any? { |pattern| File.fnmatch?(pattern, path) }
|
|
end
|
|
|
|
def binary_file?(path)
|
|
ext = File.extname(path).downcase
|
|
%w[.png .jpg .jpeg .gif .svg .ico .woff .woff2 .ttf .eot].include?(ext)
|
|
end
|
|
|
|
def normalize_content(path, content)
|
|
normalized = case File.extname(path).downcase
|
|
when '.html', '.htm'
|
|
normalize_html_dom(content)
|
|
when '.xml'
|
|
normalize_xml_dom(content)
|
|
when '.json'
|
|
normalize_json(content)
|
|
else
|
|
normalize_text(content)
|
|
end
|
|
|
|
strip_dynamic_values(normalized)
|
|
end
|
|
|
|
def normalize_html_dom(content)
|
|
doc = Nokogiri::HTML5(content)
|
|
html = doc.to_html
|
|
if defined?(HtmlBeautifier) && HtmlBeautifier.respond_to?(:beautify)
|
|
html = HtmlBeautifier.beautify(html)
|
|
end
|
|
html
|
|
rescue StandardError
|
|
normalize_text(content)
|
|
end
|
|
|
|
def normalize_xml_dom(content)
|
|
doc = Nokogiri::XML(content) { |cfg| cfg.noblanks }
|
|
doc.to_xml(indent: 2)
|
|
rescue StandardError
|
|
normalize_text(content)
|
|
end
|
|
|
|
def normalize_json(content)
|
|
JSON.pretty_generate(JSON.parse(content))
|
|
rescue StandardError
|
|
normalize_text(content)
|
|
end
|
|
|
|
def normalize_text(content)
|
|
content.gsub(/\s+/, ' ').gsub(/>\s+</, '><').strip
|
|
end
|
|
|
|
def strip_dynamic_values(content)
|
|
content.gsub(/<lastBuildDate>.*?<\/lastBuildDate>/, '')
|
|
end
|
|
|
|
# Legacy helper retained for backwards compatibility if needed elsewhere.
|
|
def html_save_options
|
|
Nokogiri::XML::Node::SaveOptions::AS_XHTML |
|
|
Nokogiri::XML::Node::SaveOptions::NO_DECLARATION |
|
|
Nokogiri::XML::Node::SaveOptions::FORMAT
|
|
end
|
|
|
|
def xml_save_options
|
|
Nokogiri::XML::Node::SaveOptions::AS_XML |
|
|
Nokogiri::XML::Node::SaveOptions::NO_DECLARATION |
|
|
Nokogiri::XML::Node::SaveOptions::FORMAT
|
|
end
|
|
|
|
def print_summary
|
|
puts ""
|
|
puts "=" * 60
|
|
puts "VALIDATION SUMMARY"
|
|
puts "=" * 60
|
|
puts ""
|
|
puts "Identical files: #{@identical_count}"
|
|
puts "Different files: #{@differences.length}"
|
|
puts "Missing in Ruby: #{@missing_in_ruby.length}"
|
|
puts "Missing in Swift: #{@missing_in_swift.length}"
|
|
puts ""
|
|
|
|
if @differences.any?
|
|
puts "Files with differences:"
|
|
return unless @show_details
|
|
|
|
@differences.each do |diff|
|
|
puts " #{diff[:path]}"
|
|
puts " Swift: #{diff[:swift_size]} bytes (#{diff[:swift_hash][0..7]}...)"
|
|
puts " Ruby: #{diff[:ruby_size]} bytes (#{diff[:ruby_hash][0..7]}...)"
|
|
end
|
|
puts ""
|
|
end
|
|
|
|
if @missing_in_ruby.any?
|
|
puts "Missing in Ruby output:"
|
|
@missing_in_ruby.each { |path| puts " #{path}" }
|
|
puts ""
|
|
end
|
|
|
|
if @missing_in_swift.any?
|
|
puts "Missing in Swift output:"
|
|
@missing_in_swift.each { |path| puts " #{path}" }
|
|
puts ""
|
|
end
|
|
|
|
success = @differences.empty? && @missing_in_ruby.empty? && @missing_in_swift.empty?
|
|
puts success ? "✅ VALIDATION PASSED" : "❌ VALIDATION FAILED"
|
|
puts ""
|
|
|
|
exit(success ? 0 : 1)
|
|
end
|
|
end
|
|
|
|
options = {
|
|
verbose: false,
|
|
ignore_patterns: [],
|
|
show_details: false,
|
|
dump_dir: nil
|
|
}
|
|
|
|
parser = OptionParser.new do |opts|
|
|
opts.banner = "Usage: validate-output [options] SWIFT_DIR RUBY_DIR"
|
|
|
|
opts.on('-v', '--verbose', 'Print a ✓ line for every identical file') do
|
|
options[:verbose] = true
|
|
end
|
|
|
|
opts.on('-iPATTERN', '--ignore=PATTERN', 'Ignore files matching the glob (may be repeated)') do |pattern|
|
|
options[:ignore_patterns] << pattern
|
|
end
|
|
|
|
opts.on('--details', 'Include byte counts and hashes for differing files') do
|
|
options[:show_details] = true
|
|
end
|
|
|
|
opts.on('--dump-normalized=DIR', 'Write normalized Swift/Ruby files to DIR/{swift,ruby}') do |dir|
|
|
options[:dump_dir] = dir
|
|
end
|
|
end
|
|
|
|
parser.parse!
|
|
|
|
if ARGV.length != 2
|
|
puts parser
|
|
exit 1
|
|
end
|
|
|
|
swift_dir = ARGV[0]
|
|
ruby_dir = ARGV[1]
|
|
|
|
unless Dir.exist?(swift_dir)
|
|
puts "ERROR: Swift directory not found: #{swift_dir}"
|
|
exit 1
|
|
end
|
|
|
|
unless Dir.exist?(ruby_dir)
|
|
puts "ERROR: Ruby directory not found: #{ruby_dir}"
|
|
exit 1
|
|
end
|
|
|
|
validator = OutputValidator.new(
|
|
swift_dir: swift_dir,
|
|
ruby_dir: ruby_dir,
|
|
verbose: options[:verbose],
|
|
ignore_patterns: options[:ignore_patterns],
|
|
show_details: options[:show_details],
|
|
dump_dir: options[:dump_dir]
|
|
)
|
|
validator.validate
|