# frozen_string_literal: true require 'fast_spec_helper' require_relative '../../../../scripts/lib/glfm/update_specification' require_relative '../../../support/helpers/next_instance_of' # IMPORTANT NOTE: See https://docs.gitlab.com/ee/development/gitlab_flavored_markdown/specification_guide/#update-specificationrb-script # for details on the implementation and usage of the `update_specification.rb` script being tested. # This developers guide contains diagrams and documentation of the script, # including explanations and examples of all files it reads and writes. # # Note that this test is not structured in a traditional way, with multiple examples # to cover all different scenarios. Instead, the content of the stubbed test fixture # files are crafted to cover multiple scenarios with in a single example run. # # This is because the invocation of the full script is slow, because it executes # a subshell for processing, which runs a full Rails environment. # This results in each full run of the script taking between 30-60 seconds. # The majority of this is spent loading the Rails environment. # # However, only the `with generation of spec.html` context is used # to test this slow sub-process, and it only contains one example. # # All other tests currently in the file pass the `skip_spec_html_generation: true` # flag to `#process`, which skips the slow sub-process. All of these other tests # should run in sub-second time when the Spring pre-loader is used. This allows # logic which is not directly related to the slow sub-processes to be TDD'd with a # very rapid feedback cycle. RSpec.describe Glfm::UpdateSpecification, '#process' do include NextInstanceOf subject { described_class.new } let(:ghfm_spec_txt_uri) { described_class::GHFM_SPEC_TXT_URI } let(:ghfm_spec_txt_uri_parsed) { instance_double(URI::HTTPS, :ghfm_spec_txt_uri_parsed) } let(:ghfm_spec_txt_uri_io) { StringIO.new(ghfm_spec_txt_contents) } let(:ghfm_spec_md_path) { described_class::GHFM_SPEC_MD_PATH } let(:ghfm_spec_txt_local_io) { StringIO.new(ghfm_spec_txt_contents) } let(:glfm_intro_md_path) { described_class::GLFM_INTRO_MD_PATH } let(:glfm_intro_md_io) { StringIO.new(glfm_intro_md_contents) } let(:glfm_official_specification_examples_md_path) { described_class::GLFM_OFFICIAL_SPECIFICATION_EXAMPLES_MD_PATH } let(:glfm_official_specification_examples_md_io) { StringIO.new(glfm_official_specification_examples_md_contents) } let(:glfm_internal_extension_examples_md_path) { described_class::GLFM_INTERNAL_EXTENSION_EXAMPLES_MD_PATH } let(:glfm_internal_extension_examples_md_io) { StringIO.new(glfm_internal_extension_examples_md_contents) } let(:glfm_spec_txt_path) { described_class::GLFM_SPEC_TXT_PATH } let(:glfm_spec_txt_io) { StringIO.new } let(:glfm_spec_html_path) { described_class::GLFM_SPEC_HTML_PATH } let(:glfm_spec_html_io) { StringIO.new } let(:markdown_tempfile_io) { StringIO.new } let(:ghfm_spec_txt_contents) do <<~MARKDOWN --- title: GitHub Flavored Markdown Spec version: 0.29 date: '2019-04-06' license: '[CC-BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/)' ... # Introduction ## What is GitHub Flavored Markdown? It's like GLFM, but with an H. # Section with Examples ## Strong ```````````````````````````````` example __bold__ .
bold
```````````````````````````````` End of last GitHub examples section. # Appendix Appendix text. MARKDOWN end let(:glfm_intro_md_contents) do # language=Markdown <<~MARKDOWN # Introduction ## What is GitLab Flavored Markdown? Intro text about GitLab Flavored Markdown. MARKDOWN end let(:glfm_official_specification_examples_md_contents) do <<~MARKDOWN # Official Specification Section with Examples Some examples. MARKDOWN end let(:glfm_internal_extension_examples_md_contents) do <<~MARKDOWN # Internal Extension Section with Examples Some examples. MARKDOWN end before do # Mock default ENV var values allow(ENV).to receive(:[]).with('UPDATE_GHFM_SPEC_MD').and_return(nil) allow(ENV).to receive(:[]).and_call_original # We mock out the URI and local file IO objects with real StringIO, instead of just mock # objects. This gives better and more realistic coverage, while still avoiding # actual network and filesystem I/O during the spec run. # input files allow(URI).to receive(:parse).with(ghfm_spec_txt_uri).and_return(ghfm_spec_txt_uri_parsed) allow(ghfm_spec_txt_uri_parsed).to receive(:open).and_return(ghfm_spec_txt_uri_io) allow(File).to receive(:open).with(ghfm_spec_md_path) { ghfm_spec_txt_local_io } allow(File).to receive(:open).with(glfm_intro_md_path) { glfm_intro_md_io } allow(File).to receive(:open).with(glfm_official_specification_examples_md_path) do glfm_official_specification_examples_md_io end allow(File).to receive(:open).with(glfm_internal_extension_examples_md_path) do glfm_internal_extension_examples_md_io end # output files allow(File).to receive(:open).with(glfm_spec_txt_path, 'w') { glfm_spec_txt_io } allow(File).to receive(:open).with(glfm_spec_html_path, 'w') { glfm_spec_html_io } # Allow normal opening of Tempfile files created during script execution. tempfile_basenames = [ described_class::MARKDOWN_TEMPFILE_BASENAME[0], described_class::STATIC_HTML_TEMPFILE_BASENAME[0] ].join('|') # NOTE: This approach with a single regex seems to be the only way this can work. If you # attempt to have multiple `allow...and_call_original` with `any_args`, the mocked # parameter matching will fail to match the second one. tempfiles_regex = /(#{tempfile_basenames})/ allow(File).to receive(:open).with(tempfiles_regex, any_args).and_call_original # Prevent console output when running tests allow(subject).to receive(:output) end describe 'retrieving latest GHFM spec.txt' do context 'when UPDATE_GHFM_SPEC_MD is not true (default)' do it 'does not download' do expect(URI).not_to receive(:parse).with(ghfm_spec_txt_uri) subject.process(skip_spec_html_generation: true) expect(reread_io(ghfm_spec_txt_local_io)).to eq(ghfm_spec_txt_contents) end end context 'when UPDATE_GHFM_SPEC_MD is true' do let(:ghfm_spec_txt_local_io) { StringIO.new } before do allow(ENV).to receive(:[]).with('UPDATE_GHFM_SPEC_MD').and_return('true') allow(File).to receive(:open).with(ghfm_spec_md_path, 'w') { ghfm_spec_txt_local_io } end context 'with success' do it 'downloads and saves' do subject.process(skip_spec_html_generation: true) expect(reread_io(ghfm_spec_txt_local_io)).to eq(ghfm_spec_txt_contents) end end context 'with error handling' do context 'with a version mismatch' do let(:ghfm_spec_txt_contents) do <<~MARKDOWN --- title: GitHub Flavored Markdown Spec version: 0.30 ... MARKDOWN end it 'raises an error' do expect do subject.process(skip_spec_html_generation: true) end.to raise_error /version mismatch.*expected.*29.*got.*30/i end end context 'with a failed read of file lines' do let(:ghfm_spec_txt_contents) { '' } it 'raises an error if lines cannot be read' do expect { subject.process(skip_spec_html_generation: true) }.to raise_error /unable to read lines/i end end context 'with a failed re-read of file string' do before do allow(ghfm_spec_txt_uri_io).to receive(:read).and_return(nil) end it 'raises an error if file is blank' do expect { subject.process(skip_spec_html_generation: true) }.to raise_error /unable to read string/i end end end end end describe 'writing GLFM spec.txt' do let(:glfm_contents) { reread_io(glfm_spec_txt_io) } before do subject.process(skip_spec_html_generation: true) end it 'replaces the header text with the GitLab version' do expect(glfm_contents).not_to match(/GitHub Flavored Markdown Spec/m) expect(glfm_contents).not_to match(/^version: \d\.\d/m) expect(glfm_contents).not_to match(/^date: /m) expect(glfm_contents).not_to match(/^license: /m) expect(glfm_contents).to match(/#{Regexp.escape(described_class::GLFM_SPEC_TXT_HEADER)}\n/mo) end it 'replaces the intro section with the GitLab version' do expect(glfm_contents).not_to match(/What is GitHub Flavored Markdown/m) expect(glfm_contents).to match(/#{Regexp.escape(glfm_intro_md_contents)}/m) end it 'inserts the GitLab official spec and internal extension examples sections before the appendix section' do expected = <<~MARKDOWN End of last GitHub examples section. # Official Specification Section with Examples Some examples. # Internal Extension Section with Examples Some examples. # Appendix MARKDOWN expect(glfm_contents).to match(/#{Regexp.escape(expected)}/m) end end describe 'writing GLFM spec.html' do let(:glfm_contents) { reread_io(glfm_spec_html_io) } before do subject.process end it 'renders HTML from spec.txt', :unlimited_max_formatted_output_length do expected = <<~HTMLtitle: GitLab Flavored Markdown (GLFM) Spec
version: alpha
Intro text about GitLab Flavored Markdown.
__bold__
.
<p><strong>bold</strong></p>
End of last GitHub examples section.
Some examples.
Some examples.
Appendix text.
HTML expect(glfm_contents).to be == expected end end def reread_io(io) # Reset the io StringIO to the beginning position of the buffer io.seek(0) io.read end end