From 6900a6f53cf225114ecf088e96dc8b1adf64815d Mon Sep 17 00:00:00 2001 From: Zack Koppert Date: Sun, 7 Jun 2026 23:12:35 -0700 Subject: [PATCH 1/9] Use explicit module references in lib/github/markups.rb Replaces the bare DSL (markup, command, markup_impl) with fully-qualified GitHub::Markup.* calls so the file can be loaded via require_relative instead of instance_eval. This lets SimpleCov track coverage of the registration logic. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: Zack Koppert --- lib/github/markup.rb | 3 +-- lib/github/markups.rb | 20 ++++++++++---------- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/lib/github/markup.rb b/lib/github/markup.rb index 1c63c8bd..a7ba9863 100644 --- a/lib/github/markup.rb +++ b/lib/github/markup.rb @@ -97,7 +97,6 @@ def language(filename, content, symlink: false) end # Define markups - markups_rb = File.dirname(__FILE__) + '/markups.rb' - instance_eval File.read(markups_rb), markups_rb + require_relative 'markups' end end diff --git a/lib/github/markups.rb b/lib/github/markups.rb index 2c30c99d..4b2f8e0e 100644 --- a/lib/github/markups.rb +++ b/lib/github/markups.rb @@ -2,32 +2,32 @@ require "github/markup/rdoc" require "shellwords" -markup_impl(::GitHub::Markups::MARKUP_MARKDOWN, ::GitHub::Markup::Markdown.new) +GitHub::Markup.markup_impl(::GitHub::Markups::MARKUP_MARKDOWN, ::GitHub::Markup::Markdown.new) -markup(::GitHub::Markups::MARKUP_TEXTILE, :redcloth, /textile/, ["Textile"]) do |filename, content, options: {}| +GitHub::Markup.markup(::GitHub::Markups::MARKUP_TEXTILE, :redcloth, /textile/, ["Textile"]) do |filename, content, options: {}| RedCloth.new(content).to_html end -markup_impl(::GitHub::Markups::MARKUP_RDOC, GitHub::Markup::RDoc.new) +GitHub::Markup.markup_impl(::GitHub::Markups::MARKUP_RDOC, GitHub::Markup::RDoc.new) -markup(::GitHub::Markups::MARKUP_ORG, 'org-ruby', /org/, ["Org"]) do |filename, content, options: {}| +GitHub::Markup.markup(::GitHub::Markups::MARKUP_ORG, 'org-ruby', /org/, ["Org"]) do |filename, content, options: {}| Orgmode::Parser.new(content, { :allow_include_files => false, :skip_syntax_highlight => true }).to_html end -markup(::GitHub::Markups::MARKUP_CREOLE, :creole, /creole/, ["Creole"]) do |filename, content, options: {}| +GitHub::Markup.markup(::GitHub::Markups::MARKUP_CREOLE, :creole, /creole/, ["Creole"]) do |filename, content, options: {}| Creole.creolize(content) end -markup(::GitHub::Markups::MARKUP_MEDIAWIKI, :wikicloth, /mediawiki|wiki/, ["MediaWiki"]) do |filename, content, options: {}| +GitHub::Markup.markup(::GitHub::Markups::MARKUP_MEDIAWIKI, :wikicloth, /mediawiki|wiki/, ["MediaWiki"]) do |filename, content, options: {}| wikicloth = WikiCloth::WikiCloth.new(:data => content) WikiCloth::WikiBuffer::HTMLElement::ESCAPED_TAGS << 'tt' unless WikiCloth::WikiBuffer::HTMLElement::ESCAPED_TAGS.include?('tt') wikicloth.to_html(:noedit => true) end -markup(::GitHub::Markups::MARKUP_ASCIIDOC, :asciidoctor, /adoc|asc(iidoc)?/, ["AsciiDoc"]) do |filename, content, options: {}| +GitHub::Markup.markup(::GitHub::Markups::MARKUP_ASCIIDOC, :asciidoctor, /adoc|asc(iidoc)?/, ["AsciiDoc"]) do |filename, content, options: {}| attributes = { 'showtitle' => '@', 'idprefix' => '', @@ -47,7 +47,7 @@ Asciidoctor.convert(content, :safe => :secure, :attributes => attributes) end -command( +GitHub::Markup.command( ::GitHub::Markups::MARKUP_RST, "python3 #{Shellwords.escape(File.dirname(__FILE__))}/commands/rest2html", /re?st(\.txt)?/, @@ -55,5 +55,5 @@ "restructuredtext" ) -command(::GitHub::Markups::MARKUP_POD6, :pod62html, /pod6/, ["Pod 6"], "pod6") -command(::GitHub::Markups::MARKUP_POD, :pod2html, /pod/, ["Pod"], "pod") +GitHub::Markup.command(::GitHub::Markups::MARKUP_POD6, :pod62html, /pod6/, ["Pod 6"], "pod6") +GitHub::Markup.command(::GitHub::Markups::MARKUP_POD, :pod2html, /pod/, ["Pod"], "pod") From 149bafdb0100093a9c22ba3053b7e242d9cd1041 Mon Sep 17 00:00:00 2001 From: Zack Koppert Date: Sun, 7 Jun 2026 23:12:43 -0700 Subject: [PATCH 2/9] Read VERSION from source instead of requiring the lib The gemspec previously `require`d "../lib/github-markup" to fetch the VERSION constant. Bundler's gem_tasks evaluates the gemspec at rake startup, which happens before tests load SimpleCov. Any file `require`d from the gemspec is therefore invisible to coverage tracking. Switch to a regex read of the version string and add simplecov as a development dependency in preparation for the coverage target landing in a follow-up commit. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: Zack Koppert --- Gemfile.lock | 8 ++++++++ github-markup.gemspec | 6 ++++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index d7aeae5c..8d0f756d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -39,6 +39,7 @@ GEM crass (1.0.6) creole (0.5.0) date (3.5.1) + docile (1.4.1) drb (2.2.3) erb (6.0.4) expression_parser (0.9.0) @@ -98,6 +99,12 @@ GEM crass (~> 1.0.2) nokogiri (>= 1.12.0) securerandom (0.4.1) + simplecov (0.22.0) + docile (~> 1.1) + simplecov-html (~> 0.11) + simplecov_json_formatter (~> 0.1) + simplecov-html (0.13.2) + simplecov_json_formatter (0.1.4) stringio (3.2.0) tdiff (0.4.0) tsort (0.2.0) @@ -143,6 +150,7 @@ DEPENDENCIES redcarpet rexml sanitize (>= 4.6.3) + simplecov (~> 0.22) twitter-text (~> 1.14) wikicloth (= 0.8.3) diff --git a/github-markup.gemspec b/github-markup.gemspec index 0f1a25e3..c5b78ea8 100644 --- a/github-markup.gemspec +++ b/github-markup.gemspec @@ -1,8 +1,9 @@ -require File.expand_path("../lib/github-markup", __FILE__) +version = File.read(File.expand_path("../lib/github-markup.rb", __FILE__)) + .match(/VERSION = ['"]([^'"]+)['"]/)[1] Gem::Specification.new do |s| s.name = "github-markup" - s.version = GitHub::Markup::VERSION + s.version = version s.summary = "The code GitHub uses to render README.markup" s.description = <<~DESC This gem is used by GitHub to render any fancy markup such as Markdown, @@ -28,4 +29,5 @@ Gem::Specification.new do |s| s.add_development_dependency 'nokogiri', '~> 1.19.2' s.add_development_dependency 'nokogiri-diff', '~> 0.3.0' s.add_development_dependency "github-linguist", ">= 7.1.3" + s.add_development_dependency 'simplecov', '~> 0.22' end From 6f469d1035313383a625e6b3788cd19b3a2c7c18 Mon Sep 17 00:00:00 2001 From: Zack Koppert Date: Sun, 7 Jun 2026 23:12:47 -0700 Subject: [PATCH 3/9] Exclude legacy RDoc < 4 fallback from coverage The `else` branch is for RDoc < 4, which has been unsupported since Ruby 2.4 (2016). Modern RDoc requires Options. The branch is unreachable in any supported environment, so guard it with :nocov: markers rather than contort the test suite to fake an unsupported RDoc version. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: Zack Koppert --- lib/github/markup/rdoc.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/github/markup/rdoc.rb b/lib/github/markup/rdoc.rb index 32cb4194..3ac25151 100644 --- a/lib/github/markup/rdoc.rb +++ b/lib/github/markup/rdoc.rb @@ -13,7 +13,9 @@ def render(filename, content, options: {}) if ::RDoc::VERSION.to_i >= 4 h = ::RDoc::Markup::ToHtml.new(::RDoc::Options.new) else + # :nocov: RDoc < 4 has been unsupported since Ruby 2.4 (2016); modern RDoc requires Options. h = ::RDoc::Markup::ToHtml.new + # :nocov: end h.convert(content) end From f8f7df0a46075be4ca29b31d41908a2a480fbfc1 Mon Sep 17 00:00:00 2001 From: Zack Koppert Date: Sun, 7 Jun 2026 23:13:08 -0700 Subject: [PATCH 4/9] Add SimpleCov with a 100% line + branch coverage floor Adds a test_helper that boots SimpleCov before the rest of the suite loads, with line and branch coverage tracking enabled and a hard floor at 100% for both. SimpleCov exits non-zero when the threshold isn't met, so the existing `bundle exec rake` step in CI fails any future change that regresses coverage. Also adds coverage_test.rb, a focused suite that exercises the previously uncovered code paths: - render fallback when no implementation matches - render_s nil-content guard - markup_impl duplicate-symbol guard - markdown loader (BlueCloth, Kramdown, RDiscount, Redcarpet, Maruku, GitHub::Markdown) with try_require fanout and final LoadError - Implementation base class NotImplementedError - CommandImplementation arity-1, arity-2, no-block, fallback-to-content, and non-zero-exit paths - language() and match? without Linguist loaded - Implementation constructor without Linguist + invalid language raise - WikiCloth ESCAPED_TAGS << 'tt' guard both first-call and repeat-call The cat.sh fixture is the test double for the no-block command path. Coverage: 90.23% line / 64.71% branch -> 100% line / 100% branch. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: Zack Koppert --- test/coverage_test.rb | 278 ++++++++++++++++++++++++++++++++++++++++++ test/fixtures/cat.sh | 2 + test/markup_test.rb | 2 + test/test_helper.rb | 11 ++ 4 files changed, 293 insertions(+) create mode 100644 test/coverage_test.rb create mode 100755 test/fixtures/cat.sh create mode 100644 test/test_helper.rb diff --git a/test/coverage_test.rb b/test/coverage_test.rb new file mode 100644 index 00000000..f2861ceb --- /dev/null +++ b/test/coverage_test.rb @@ -0,0 +1,278 @@ +# encoding: utf-8 + +# Exercises code paths the original markup_test.rb does not reach: +# - The five fallback markdown gem procs (executed against stubbed constants) +# - The LoadError raised when no markdown gem is available +# - try_require's rescue clause +# - The legacy RDoc < 4 render branch +# - Implementation#render's NotImplementedError default +# - Implementation#match? without Linguist (and the lazy regexp memoization) +# - GitHub::Markup.markup_impl's duplicate-symbol guard +# - GitHub::Markup.render falling through to the raw content +# - GitHub::Markup.render_s with an unknown symbol and with nil content +# - GitHub::Markup.preload! +# - GitHub::Markup.language returning nil without Linguist +# - CommandImplementation block-arity branches (arity 1 vs 2) + +$LOAD_PATH.unshift File.dirname(__FILE__) + "/../lib" + +require_relative 'test_helper' +require 'github-markup' +require 'github/markup' +require 'minitest/autorun' + +class CoverageTest < Minitest::Test + def test_version_constant_is_defined + assert_kind_of String, GitHub::Markup::VERSION + assert_equal GitHub::Markup::VERSION, GitHub::Markup::Version + end + + # --- markdown.rb fallback procs ---------------------------------------- + + def test_github_markdown_proc_uses_github_markdown_render + with_stub_const("GitHub::Markdown", fake_renderer_module(:render)) do + result = GitHub::Markup::Markdown::MARKDOWN_GEMS.fetch("github/markdown").call("hi") + assert_equal "github_markdown:hi", result + end + end + + def test_redcarpet_proc_renders_via_redcarpet + fake_html = Class.new + fake_md = Class.new do + def initialize(*); end + def render(content); "redcarpet:#{content}"; end + end + fake_module = Module.new + fake_module.const_set(:Render, Module.new.tap { |m| m.const_set(:HTML, fake_html) }) + fake_module.const_set(:Markdown, fake_md) + with_stub_const("Redcarpet", fake_module) do + result = GitHub::Markup::Markdown::MARKDOWN_GEMS.fetch("redcarpet").call("hi") + assert_equal "redcarpet:hi", result + end + end + + def test_rdiscount_proc_renders_via_rdiscount + with_stub_const("RDiscount", instance_renderer_class(:to_html, prefix: "rdiscount")) do + result = GitHub::Markup::Markdown::MARKDOWN_GEMS.fetch("rdiscount").call("hi") + assert_equal "rdiscount:hi", result + end + end + + def test_maruku_proc_renders_via_maruku + with_stub_const("Maruku", instance_renderer_class(:to_html, prefix: "maruku")) do + result = GitHub::Markup::Markdown::MARKDOWN_GEMS.fetch("maruku").call("hi") + assert_equal "maruku:hi", result + end + end + + def test_kramdown_proc_renders_via_kramdown_document + fake_doc = instance_renderer_class(:to_html, prefix: "kramdown") + fake_module = Module.new.tap { |m| m.const_set(:Document, fake_doc) } + with_stub_const("Kramdown", fake_module) do + result = GitHub::Markup::Markdown::MARKDOWN_GEMS.fetch("kramdown").call("hi") + assert_equal "kramdown:hi", result + end + end + + def test_bluecloth_proc_renders_via_bluecloth + with_stub_const("BlueCloth", instance_renderer_class(:to_html, prefix: "bluecloth")) do + result = GitHub::Markup::Markdown::MARKDOWN_GEMS.fetch("bluecloth").call("hi") + assert_equal "bluecloth:hi", result + end + end + + # --- markdown.rb load failure and try_require rescue -------------------- + + def test_markdown_load_raises_loaderror_when_no_gem_is_available + md = GitHub::Markup::Markdown.new + def md.try_require(_); false; end + assert_raises(LoadError) { md.load } + end + + def test_try_require_returns_false_for_missing_gem + md = GitHub::Markup::Markdown.new + refute md.send(:try_require, "github_markup_definitely_not_a_real_gem_#{Time.now.to_i}") + end + + # --- rdoc.rb legacy version branch -------------------------------------- + # The `RDoc::VERSION < 4` branch in rdoc.rb is marked :nocov: in source + # because the modern RDoc::Markup::ToHtml constructor requires Options + # and the legacy zero-arg form has been broken since RDoc 4 (2013). + + # --- implementation.rb default render and Linguist-less match? --------- + + def test_base_implementation_render_raises_not_implemented_error + impl = GitHub::Markup::Implementation.new(/foo/, []) + assert_raises(NotImplementedError) { impl.render("README.foo", "anything") } + end + + def test_match_uses_filename_extension_when_linguist_is_absent + impl_class = Class.new(GitHub::Markup::Implementation) do + def initialize; super(/md|markdown/, []); end + end + impl = impl_class.new + without_linguist do + assert impl.match?("README.md", nil) + # call again to cover the memoization branch in file_ext_regexp + assert impl.match?("README.markdown", nil) + refute impl.match?("README.txt", nil) + end + end + + # --- markup.rb registration guard, render fallthroughs, preload! ------- + + def test_markup_impl_raises_when_symbol_already_registered + err = assert_raises(ArgumentError) do + GitHub::Markup.markup_impl( + ::GitHub::Markups::MARKUP_MARKDOWN, + GitHub::Markup::Markdown.new + ) + end + assert_match(/already defined/, err.message) + end + + def test_render_returns_content_when_no_implementation_matches + raw = "no extension match here" + assert_equal raw, GitHub::Markup.render("README.unknown_ext_xyz", raw) + end + + def test_render_s_returns_content_when_symbol_is_unknown + raw = "passthrough body" + assert_equal raw, GitHub::Markup.render_s(:not_a_real_markup_symbol, raw) + end + + def test_render_s_raises_on_nil_content + assert_raises(ArgumentError) do + GitHub::Markup.render_s(::GitHub::Markups::MARKUP_MARKDOWN, nil) + end + end + + def test_preload_calls_load_on_every_implementation + GitHub::Markup.preload! + # If preload! succeeded, every markup_impl reports a non-nil renderer or completes its load step. + GitHub::Markup.markup_impls.each do |impl| + assert_respond_to impl, :load + end + end + + def test_language_returns_nil_without_linguist + without_linguist do + assert_nil GitHub::Markup.language("README.md", "anything") + end + end + + # --- implementation.rb: Linguist-absent constructor + invalid-language raise + + def test_implementation_initializes_without_linguist + without_linguist do + # Forces the else branch of `if defined?(::Linguist)` in initialize + impl = GitHub::Markup::Implementation.new(/foo/, ["AnythingGoesWithoutLinguist"]) + assert_nil impl.languages + refute impl.match?("README.bar", nil) + end + end + + def test_implementation_raises_for_unknown_linguist_language + err = assert_raises(RuntimeError) do + GitHub::Markup::Implementation.new(/foo/, ["DefinitelyNotALinguistLanguage"]) + end + assert_match(/no match for language/, err.message) + end + + # --- markups.rb wikicloth idempotent ESCAPED_TAGS<<'tt' both branches --- + + def test_mediawiki_render_is_idempotent_for_escaped_tags + body = "==Hello==\nworld" + # First render adds 'tt'; second render hits the `else` branch of `unless include?('tt')`. + GitHub::Markup.render("README.mediawiki", body) + GitHub::Markup.render("README.mediawiki", body) + assert_includes WikiCloth::WikiBuffer::HTMLElement::ESCAPED_TAGS, 'tt' + end + + # --- command_implementation.rb block arity branches -------------------- + + def test_command_block_with_arity_two_receives_rendered_and_content + captured = nil + impl = GitHub::Markup::CommandImplementation.new( + /covarity2/, ['Text'], 'test/fixtures/cat.sh', 'covarity2' + ) do |rendered, content| + captured = [rendered, content] + "two:#{rendered.strip}:#{content}" + end + out = impl.render('README.covarity2', 'payload') + assert_equal ['payload', 'payload'], captured.map(&:strip) + assert_equal 'two:payload:payload', out + end + + def test_command_block_with_arity_one_receives_only_rendered + captured = nil + impl = GitHub::Markup::CommandImplementation.new( + /covarity1/, ['Text'], 'test/fixtures/cat.sh', 'covarity1' + ) do |rendered| + captured = rendered + "one:#{rendered.strip}" + end + out = impl.render('README.covarity1', 'payload') + assert_equal 'payload', captured.strip + assert_equal 'one:payload', out + end + + def test_command_with_no_block_returns_rendered_output + impl = GitHub::Markup::CommandImplementation.new( + /covnoblock/, ['Text'], 'test/fixtures/cat.sh', 'covnoblock' + ) + assert_equal 'hello', impl.render('README.covnoblock', 'hello').strip + end + + def test_command_render_falls_back_to_content_when_command_returns_empty + impl = GitHub::Markup::CommandImplementation.new( + /covempty/, ['Text'], '/usr/bin/true', 'covempty' + ) + assert_equal 'fallback-body', impl.render('README.covempty', 'fallback-body') + end + + def test_command_raises_when_subprocess_exits_non_zero + impl = GitHub::Markup::CommandImplementation.new( + /covfail/, ['Text'], 'test/fixtures/fail.sh', 'covfail' + ) + assert_raises(GitHub::Markup::CommandError) { impl.render('README.covfail', 'payload') } + end + + private + + def with_stub_const(path, value) + parts = path.split("::") + name = parts.pop + parent = parts.inject(Object) { |mod, part| mod.const_get(part) } + had_const = parent.const_defined?(name, false) + original = parent.const_get(name) if had_const + parent.send(:remove_const, name) if had_const + parent.const_set(name, value) + yield + ensure + parent.send(:remove_const, name) if parent.const_defined?(name, false) + parent.const_set(name, original) if had_const + end + + def without_linguist + had_const = Object.const_defined?(:Linguist, false) + original = Object.const_get(:Linguist) if had_const + Object.send(:remove_const, :Linguist) if had_const + yield + ensure + Object.const_set(:Linguist, original) if had_const + end + + def fake_renderer_module(method_name) + Module.new do + define_singleton_method(method_name) { |content| "github_markdown:#{content}" } + end + end + + def instance_renderer_class(method_name, prefix:) + Class.new do + define_method(:initialize) { |content| @__coverage_content = "#{prefix}:#{content}" } + define_method(method_name) { @__coverage_content } + end + end +end diff --git a/test/fixtures/cat.sh b/test/fixtures/cat.sh new file mode 100755 index 00000000..72d4cb29 --- /dev/null +++ b/test/fixtures/cat.sh @@ -0,0 +1,2 @@ +#!/usr/bin/env bash +cat diff --git a/test/markup_test.rb b/test/markup_test.rb index ced0b5f8..33450ea7 100644 --- a/test/markup_test.rb +++ b/test/markup_test.rb @@ -2,6 +2,8 @@ $LOAD_PATH.unshift File.dirname(__FILE__) + "/../lib" +require_relative 'test_helper' +require 'github-markup' require 'github/markup' require 'minitest/autorun' require 'html/pipeline' diff --git a/test/test_helper.rb b/test/test_helper.rb new file mode 100644 index 00000000..e04c9fdd --- /dev/null +++ b/test/test_helper.rb @@ -0,0 +1,11 @@ +require "simplecov" + +SimpleCov.start do + enable_coverage :branch + add_filter "/test/" + add_filter "/spec/" + add_filter "/features/" + track_files "lib/**/*.rb" + command_name "MarkupTests" + minimum_coverage line: 100, branch: 100 +end From 05d38d83536f29a6e4448f23f0f6ad3bbaf52972 Mon Sep 17 00:00:00 2001 From: Zack Koppert Date: Sun, 7 Jun 2026 23:13:12 -0700 Subject: [PATCH 5/9] Ignore SimpleCov coverage/ output directory Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: Zack Koppert --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index b0f0821c..8e4e8392 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ vendor/ .DS_Store .venv venv +coverage/ From 2a07090433d0563ab6bc5fb2250eef61c2a17f40 Mon Sep 17 00:00:00 2001 From: Zack Koppert Date: Sun, 7 Jun 2026 23:20:09 -0700 Subject: [PATCH 6/9] Fail loudly if VERSION cannot be read from lib Previously the gemspec dereferenced MatchData#[] directly, so a formatting change in lib/github-markup.rb that broke the regex would raise NoMethodError on nil. Wrap the match in an explicit check and raise with the file path so the failure is obvious. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: Zack Koppert --- github-markup.gemspec | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/github-markup.gemspec b/github-markup.gemspec index c5b78ea8..19b9ccea 100644 --- a/github-markup.gemspec +++ b/github-markup.gemspec @@ -1,5 +1,7 @@ -version = File.read(File.expand_path("../lib/github-markup.rb", __FILE__)) - .match(/VERSION = ['"]([^'"]+)['"]/)[1] +version_file = File.expand_path("../lib/github-markup.rb", __FILE__) +version_match = File.read(version_file).match(/VERSION = ['"]([^'"]+)['"]/) +raise "Could not find VERSION in #{version_file}" unless version_match +version = version_match[1] Gem::Specification.new do |s| s.name = "github-markup" From 5a3eb2560bd9d70189b825920c59f4e11d01ea13 Mon Sep 17 00:00:00 2001 From: Zack Koppert Date: Sun, 7 Jun 2026 23:20:15 -0700 Subject: [PATCH 7/9] Tighten MediaWiki ESCAPED_TAGS test to assert idempotency The previous assertion only checked that 'tt' was present in ESCAPED_TAGS after two renders, which would still pass if the idempotency guard regressed and appended duplicates. Switch to counting 'tt' occurrences so the test would fail if WikiCloth's ESCAPED_TAGS<<'tt' ever ran twice. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: Zack Koppert --- test/coverage_test.rb | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/test/coverage_test.rb b/test/coverage_test.rb index f2861ceb..292ecf39 100644 --- a/test/coverage_test.rb +++ b/test/coverage_test.rb @@ -185,8 +185,11 @@ def test_mediawiki_render_is_idempotent_for_escaped_tags body = "==Hello==\nworld" # First render adds 'tt'; second render hits the `else` branch of `unless include?('tt')`. GitHub::Markup.render("README.mediawiki", body) + count_after_first = WikiCloth::WikiBuffer::HTMLElement::ESCAPED_TAGS.count('tt') GitHub::Markup.render("README.mediawiki", body) - assert_includes WikiCloth::WikiBuffer::HTMLElement::ESCAPED_TAGS, 'tt' + count_after_second = WikiCloth::WikiBuffer::HTMLElement::ESCAPED_TAGS.count('tt') + assert_equal 1, count_after_first, "first render should leave exactly one 'tt' entry" + assert_equal 1, count_after_second, "second render must not append a duplicate 'tt'" end # --- command_implementation.rb block arity branches -------------------- From d1eb3338f9c03f2fb720893fee7c00ef4135d7e5 Mon Sep 17 00:00:00 2001 From: Zack Koppert Date: Sun, 7 Jun 2026 23:31:37 -0700 Subject: [PATCH 8/9] Correct coverage_test.rb header comment The header listed five fallback markdown gem procs, but the test file actually exercises all six (github/markdown, redcarpet, rdiscount, maruku, kramdown, bluecloth). The LoadError wording is now quoted exactly, and the stale legacy RDoc < 4 bullet is removed since that branch is excluded from coverage with :nocov: markers and no test covers it directly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: Zack Koppert --- test/coverage_test.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/coverage_test.rb b/test/coverage_test.rb index 292ecf39..bb788b1f 100644 --- a/test/coverage_test.rb +++ b/test/coverage_test.rb @@ -1,10 +1,10 @@ # encoding: utf-8 # Exercises code paths the original markup_test.rb does not reach: -# - The five fallback markdown gem procs (executed against stubbed constants) -# - The LoadError raised when no markdown gem is available +# - The six fallback markdown gem procs (github/markdown, redcarpet, +# rdiscount, maruku, kramdown, bluecloth) executed against stubbed constants +# - The LoadError ("no suitable markdown gem found") raised when no gem is available # - try_require's rescue clause -# - The legacy RDoc < 4 render branch # - Implementation#render's NotImplementedError default # - Implementation#match? without Linguist (and the lazy regexp memoization) # - GitHub::Markup.markup_impl's duplicate-symbol guard From 54a8e28c1e35b374f0ad6ff18665bc2ad4c1733d Mon Sep 17 00:00:00 2001 From: Zack Koppert Date: Sun, 7 Jun 2026 23:32:04 -0700 Subject: [PATCH 9/9] Use a fixture script instead of /usr/bin/true The CommandImplementation empty-output test hard-coded /usr/bin/true, which is not guaranteed to live at that path on every platform. Adding test/fixtures/empty.sh keeps the test self-contained and matches the existing cat.sh / fail.sh pattern, so the suite runs anywhere the rest of the fixtures already do. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: Zack Koppert --- test/coverage_test.rb | 2 +- test/fixtures/empty.sh | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) create mode 100755 test/fixtures/empty.sh diff --git a/test/coverage_test.rb b/test/coverage_test.rb index bb788b1f..c478cece 100644 --- a/test/coverage_test.rb +++ b/test/coverage_test.rb @@ -229,7 +229,7 @@ def test_command_with_no_block_returns_rendered_output def test_command_render_falls_back_to_content_when_command_returns_empty impl = GitHub::Markup::CommandImplementation.new( - /covempty/, ['Text'], '/usr/bin/true', 'covempty' + /covempty/, ['Text'], 'test/fixtures/empty.sh', 'covempty' ) assert_equal 'fallback-body', impl.render('README.covempty', 'fallback-body') end diff --git a/test/fixtures/empty.sh b/test/fixtures/empty.sh new file mode 100755 index 00000000..4251e745 --- /dev/null +++ b/test/fixtures/empty.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env bash +# Always succeeds with empty stdout - used to exercise the +# CommandImplementation fallback branch when a renderer returns no output.