|
| 1 | +#!/usr/bin/env ruby |
| 2 | +# frozen_string_literal: true |
| 3 | + |
| 4 | +# 문서의 코드 예제를 분석하여: |
| 5 | +# 1. spec 파일에 개별 예제 테스트 생성 |
| 6 | +# 2. 문서의 ExampleBadge 라인 번호 업데이트 |
| 7 | +# |
| 8 | +# 사용법: |
| 9 | +# ruby scripts/generate_example_tests.rb [--dry-run] |
| 10 | + |
| 11 | +require_relative "../lib/t_ruby/docs_example_extractor" |
| 12 | + |
| 13 | +class ExampleTestGenerator |
| 14 | + DOCS_ROOT = File.expand_path("../../t-ruby.github.io", __dir__) |
| 15 | + KO_DOCS = "#{DOCS_ROOT}/i18n/ko/docusaurus-plugin-content-docs/current" |
| 16 | + SPEC_ROOT = File.expand_path("../spec/docs_site/pages", __dir__) |
| 17 | + |
| 18 | + def initialize(dry_run: false) |
| 19 | + @dry_run = dry_run |
| 20 | + @extractor = TRuby::DocsExampleExtractor.new |
| 21 | + end |
| 22 | + |
| 23 | + def run |
| 24 | + doc_files = Dir.glob("#{KO_DOCS}/**/*.md") |
| 25 | + |
| 26 | + doc_files.each do |doc_path| |
| 27 | + process_document(doc_path) |
| 28 | + end |
| 29 | + end |
| 30 | + |
| 31 | + private |
| 32 | + |
| 33 | + def process_document(doc_path) |
| 34 | + relative_path = doc_path.sub("#{KO_DOCS}/", "") |
| 35 | + spec_relative = relative_path.sub(".md", "_spec.rb").gsub("-", "_") |
| 36 | + spec_path = "#{SPEC_ROOT}/#{spec_relative}" |
| 37 | + |
| 38 | + return unless File.exist?(spec_path) |
| 39 | + |
| 40 | + examples = @extractor.extract_from_file(doc_path) |
| 41 | + return if examples.empty? |
| 42 | + |
| 43 | + puts "Processing: #{relative_path} (#{examples.size} examples)" |
| 44 | + |
| 45 | + # 1. spec 파일 생성 |
| 46 | + spec_content = generate_spec_content(relative_path, examples) |
| 47 | + line_map = calculate_line_map(spec_content, examples) |
| 48 | + |
| 49 | + # 2. 문서 업데이트 |
| 50 | + doc_content = File.read(doc_path, encoding: "UTF-8") |
| 51 | + updated_doc = update_doc_badges(doc_content, examples, spec_relative, line_map) |
| 52 | + |
| 53 | + if @dry_run |
| 54 | + puts " [DRY RUN] Would update spec and doc" |
| 55 | + puts " Line map: #{line_map.inspect}" |
| 56 | + else |
| 57 | + File.write(spec_path, spec_content, encoding: "UTF-8") |
| 58 | + File.write(doc_path, updated_doc, encoding: "UTF-8") |
| 59 | + puts " Updated: #{spec_path}" |
| 60 | + puts " Updated: #{doc_path}" |
| 61 | + end |
| 62 | + end |
| 63 | + |
| 64 | + def generate_spec_content(relative_path, examples) |
| 65 | + doc_name = relative_path.sub(".md", "").split("/").last.gsub("-", " ").split.map(&:capitalize).join(" ") |
| 66 | + |
| 67 | + lines = [] |
| 68 | + lines << '# frozen_string_literal: true' |
| 69 | + lines << '' |
| 70 | + lines << 'require "spec_helper"' |
| 71 | + lines << 'require_relative "../support/shared_examples"' |
| 72 | + lines << '' |
| 73 | + lines << "RSpec.describe \"한글 문서: #{doc_name}\" do" |
| 74 | + lines << ' include_context "docs site paths"' |
| 75 | + lines << '' |
| 76 | + lines << " let(:relative_path) { \"#{relative_path}\" }" |
| 77 | + lines << ' let(:doc_path) { "#{ko_docs_root}/#{relative_path}" }' |
| 78 | + lines << ' let(:doc_content) { File.read(doc_path) }' |
| 79 | + lines << ' let(:extractor) { TRuby::DocsExampleExtractor.new }' |
| 80 | + lines << ' let(:verifier) { TRuby::DocsExampleVerifier.new }' |
| 81 | + lines << ' let(:examples) { extractor.extract_from_file(doc_path) }' |
| 82 | + lines << '' |
| 83 | + lines << ' before do' |
| 84 | + lines << ' skip "t-ruby.github.io not found" unless Dir.exist?(docs_site_root)' |
| 85 | + lines << ' skip "Document not found" unless File.exist?(doc_path)' |
| 86 | + lines << ' end' |
| 87 | + lines << '' |
| 88 | + lines << " it_behaves_like \"valid documentation page\", \"#{relative_path}\"" |
| 89 | + lines << '' |
| 90 | + lines << ' describe "코드 예제" do' |
| 91 | + |
| 92 | + examples.each_with_index do |example, index| |
| 93 | + lines << generate_example_test(example, index) |
| 94 | + end |
| 95 | + |
| 96 | + lines << ' end' |
| 97 | + lines << 'end' |
| 98 | + lines << '' |
| 99 | + |
| 100 | + lines.join("\n") |
| 101 | + end |
| 102 | + |
| 103 | + def generate_example_test(example, index) |
| 104 | + # 예제 코드의 첫 줄을 기반으로 설명 생성 |
| 105 | + first_line = example.code.lines.first&.strip || "코드" |
| 106 | + first_line = first_line[0..40] + "..." if first_line.length > 40 |
| 107 | + |
| 108 | + lang_name = case example.language |
| 109 | + when "trb" then "T-Ruby" |
| 110 | + when "ruby" then "Ruby" |
| 111 | + when "rbs" then "RBS" |
| 112 | + else example.language |
| 113 | + end |
| 114 | + |
| 115 | + lines = [] |
| 116 | + lines << '' |
| 117 | + lines << " # 예제 #{index + 1}: #{lang_name} (라인 #{example.line_number})" |
| 118 | + lines << " describe \"예제 #{index + 1}: #{lang_name} 코드\" do" |
| 119 | + lines << " let(:example) { examples[#{index}] }" |
| 120 | + lines << '' |
| 121 | + |
| 122 | + case example.language |
| 123 | + when "trb" |
| 124 | + lines << ' it "T-Ruby 코드가 파싱에 성공한다" do' |
| 125 | + lines << ' skip "예제를 찾을 수 없음" unless example' |
| 126 | + lines << ' parser = TRuby::Parser.new(example.code)' |
| 127 | + lines << ' expect { parser.parse }.not_to raise_error' |
| 128 | + lines << ' end' |
| 129 | + when "ruby" |
| 130 | + lines << ' it "유효한 Ruby 문법이다" do' |
| 131 | + lines << ' skip "예제를 찾을 수 없음" unless example' |
| 132 | + lines << ' expect { RubyVM::InstructionSequence.compile(example.code) }.not_to raise_error' |
| 133 | + lines << ' end' |
| 134 | + when "rbs" |
| 135 | + lines << ' it "RBS 형식이다" do' |
| 136 | + lines << ' skip "예제를 찾을 수 없음" unless example' |
| 137 | + lines << ' expect(example.code).to match(/\b(def|class|module|interface|type)\b/)' |
| 138 | + lines << ' end' |
| 139 | + end |
| 140 | + |
| 141 | + lines << ' end' |
| 142 | + lines.join("\n") |
| 143 | + end |
| 144 | + |
| 145 | + def calculate_line_map(spec_content, examples) |
| 146 | + # spec 파일에서 각 예제 테스트의 시작 라인 번호 계산 |
| 147 | + line_map = {} |
| 148 | + |
| 149 | + spec_content.lines.each_with_index do |line, index| |
| 150 | + if match = line.match(/# 예제 (\d+):/) |
| 151 | + example_index = match[1].to_i - 1 |
| 152 | + line_map[example_index] = index + 1 # 1-based line number |
| 153 | + end |
| 154 | + end |
| 155 | + |
| 156 | + line_map |
| 157 | + end |
| 158 | + |
| 159 | + def update_doc_badges(doc_content, examples, spec_file, line_map) |
| 160 | + lines = doc_content.lines |
| 161 | + result = [] |
| 162 | + example_index = 0 |
| 163 | + |
| 164 | + i = 0 |
| 165 | + while i < lines.size |
| 166 | + line = lines[i] |
| 167 | + |
| 168 | + # ExampleBadge 찾기 |
| 169 | + if line.include?("<ExampleBadge") |
| 170 | + # 다음 코드 블록의 예제 인덱스 찾기 |
| 171 | + test_line = line_map[example_index] || 21 |
| 172 | + |
| 173 | + # 라인 번호 업데이트 |
| 174 | + updated_badge = line.gsub(/line=\{\d+\}/, "line={#{test_line}}") |
| 175 | + result << updated_badge |
| 176 | + example_index += 1 |
| 177 | + else |
| 178 | + result << line |
| 179 | + end |
| 180 | + |
| 181 | + i += 1 |
| 182 | + end |
| 183 | + |
| 184 | + result.join |
| 185 | + end |
| 186 | +end |
| 187 | + |
| 188 | +if __FILE__ == $0 |
| 189 | + dry_run = ARGV.include?("--dry-run") |
| 190 | + ExampleTestGenerator.new(dry_run: dry_run).run |
| 191 | +end |
0 commit comments