From ee664acb356f8123f4f6b00b73c1e1cf0866c7fb Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Thu, 20 Oct 2022 09:40:42 +0000 Subject: Add latest changes from gitlab-org/gitlab@15-5-stable-ee --- .../lib/glfm/update_example_snapshots_spec.rb | 759 +++++++++++---------- spec/scripts/lib/glfm/update_specification_spec.rb | 169 ++++- ...rify_all_generated_files_are_up_to_date_spec.rb | 62 ++ spec/scripts/trigger-build_spec.rb | 2 - 4 files changed, 618 insertions(+), 374 deletions(-) create mode 100644 spec/scripts/lib/glfm/verify_all_generated_files_are_up_to_date_spec.rb (limited to 'spec/scripts') diff --git a/spec/scripts/lib/glfm/update_example_snapshots_spec.rb b/spec/scripts/lib/glfm/update_example_snapshots_spec.rb index f96936c0a6f..c97226c1a2d 100644 --- a/spec/scripts/lib/glfm/update_example_snapshots_spec.rb +++ b/spec/scripts/lib/glfm/update_example_snapshots_spec.rb @@ -2,8 +2,8 @@ require 'fast_spec_helper' require_relative '../../../../scripts/lib/glfm/update_example_snapshots' -# IMPORTANT NOTE: See https://docs.gitlab.com/ee/development/gitlab_flavored_markdown/specification_guide/ -# for details on the implementation and usage of the `update_example_snapshots` script being tested. +# IMPORTANT NOTE: See https://docs.gitlab.com/ee/development/gitlab_flavored_markdown/specification_guide/#update-example-snapshotsrb-script +# for details on the implementation and usage of the `update_example_snapshots.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. # @@ -16,17 +16,18 @@ require_relative '../../../../scripts/lib/glfm/update_example_snapshots' # which runs a jest test 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 `writing html.yml and prosemirror_json.yml` context is used -# to test these slow sub-processes, and it only contains a single example. +# However, only the `with full processing of static and WYSIWYG HTML` context is used +# to test these slow sub-processes, and it only contains two examples. # # All other tests currently in the file pass the `skip_static_and_wysiwyg: true` -# flag to `#process`, which skips the slow sub-processes. All of these tests +# flag to `#process`, which skips the slow sub-processes. 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. # # Also, the textual content of the individual fixture file entries is also crafted to help # indicate which scenarios which they are covering. +# rubocop:disable RSpec/MultipleMemoizedHelpers RSpec.describe Glfm::UpdateExampleSnapshots, '#process' do subject { described_class.new } @@ -34,9 +35,8 @@ RSpec.describe Glfm::UpdateExampleSnapshots, '#process' do let(:glfm_spec_txt_path) { described_class::GLFM_SPEC_TXT_PATH } let(:glfm_spec_txt_local_io) { StringIO.new(glfm_spec_txt_contents) } let(:glfm_example_status_yml_path) { described_class::GLFM_EXAMPLE_STATUS_YML_PATH } - let(:glfm_example_status_yml_io) { StringIO.new(glfm_example_status_yml_contents) } let(:glfm_example_metadata_yml_path) { described_class::GLFM_EXAMPLE_METADATA_YML_PATH } - let(:glfm_example_metadata_yml_io) { StringIO.new(glfm_example_metadata_yml_contents) } + let(:glfm_example_normalizations_yml_path) { described_class::GLFM_EXAMPLE_NORMALIZATIONS_YML_PATH } # Example Snapshot (ES) output files let(:es_examples_index_yml_path) { described_class::ES_EXAMPLES_INDEX_YML_PATH } @@ -285,10 +285,25 @@ RSpec.describe Glfm::UpdateExampleSnapshots, '#process' do YAML end + let(:test1) { '\1\2URI_PREFIX\4' } + + let(:glfm_example_normalizations_yml_contents) do + # NOTE: This heredoc identifier must be quoted because we are using control characters in the heredoc body. + # See https://stackoverflow.com/a/73831037/25192 + <<~'YAML' + --- + # If a config file entry starts with `00_`, it will be skipped for validation that it exists in `examples_index.yml` + 00_shared: + 00_uri: &00_uri + - regex: '(href|data-src)(=")(.*?)(test-file\.(png|zip)")' + replacement: '\1\2URI_PREFIX\4' + YAML + end + let(:es_html_yml_io_existing_contents) do <<~YAML --- - 00_00_00__obsolete_entry_to_be_deleted__001: + 01_00_00__obsolete_entry_to_be_deleted__001: canonical: | This entry is no longer exists in the spec.txt, so it will be deleted. static: |- @@ -315,7 +330,7 @@ RSpec.describe Glfm::UpdateExampleSnapshots, '#process' do let(:es_prosemirror_json_yml_io_existing_contents) do <<~YAML --- - 00_00_00__obsolete_entry_to_be_deleted__001: |- + 01_00_00__obsolete_entry_to_be_deleted__001: |- { "obsolete": "This entry is no longer exists in the spec.txt, and is not skipped, so it will be deleted." } @@ -356,9 +371,14 @@ RSpec.describe Glfm::UpdateExampleSnapshots, '#process' do # input files allow(File).to receive(:open).with(glfm_spec_txt_path) { glfm_spec_txt_local_io } - allow(File).to receive(:open).with(glfm_example_status_yml_path) { glfm_example_status_yml_io } + allow(File).to receive(:open).with(glfm_example_status_yml_path) do + StringIO.new(glfm_example_status_yml_contents) + end allow(File).to receive(:open).with(glfm_example_metadata_yml_path) do - glfm_example_metadata_yml_io + StringIO.new(glfm_example_metadata_yml_contents) + end + allow(File).to receive(:open).with(glfm_example_normalizations_yml_path) do + StringIO.new(glfm_example_normalizations_yml_contents) end # output files @@ -525,353 +545,404 @@ RSpec.describe Glfm::UpdateExampleSnapshots, '#process' do end end - # rubocop:disable RSpec/MultipleMemoizedHelpers - describe 'writing html.yml and prosemirror_json.yml' do - let(:es_html_yml_contents) { reread_io(es_html_yml_io) } - let(:es_prosemirror_json_yml_contents) { reread_io(es_prosemirror_json_yml_io) } + describe 'error handling when manually-curated input specification config files contain invalid example names:' do + let(:err_msg) do + /#{config_file}.*01_00_00__invalid__001.*does not have.*entry in.*#{described_class::ES_EXAMPLES_INDEX_YML_PATH}/m + end - # NOTE: This example_status.yml is crafted in conjunction with expected_html_yml_contents - # to test the behavior of the `skip_update_*` flags - let(:glfm_example_status_yml_contents) do + let(:invalid_example_name_file_contents) do <<~YAML --- - 02_01_00__inlines__strong__002: - # NOTE: 02_01_00__inlines__strong__002: is omitted from the existing prosemirror_json.yml file, and is also - # skipped here, to show that an example does not need to exist in order to be skipped. - # TODO: This should be changed to raise an error instead, to enforce that there cannot be orphaned - # entries in glfm_example_status.yml. This task is captured in - # https://gitlab.com/gitlab-org/gitlab/-/issues/361241#other-cleanup-tasks - skip_update_example_snapshot_prosemirror_json: "skipping because JSON isn't cool enough" - 03_01_00__first_gitlab_specific_section_with_examples__strong_but_with_two_asterisks__001: - skip_update_example_snapshot_html_static: "skipping because there's too much static" - 04_01_00__second_gitlab_specific_section_with_examples__strong_but_with_html__001: - skip_update_example_snapshot_html_wysiwyg: 'skipping because what you see is NOT what you get' - skip_update_example_snapshot_prosemirror_json: "skipping because JSON still isn't cool enough" - 05_01_00__third_gitlab_specific_section_with_skipped_examples__strong_but_skipped__001: - skip_update_example_snapshots: 'skipping this example because it is very bad' - 05_02_00__third_gitlab_specific_section_with_skipped_examples__strong_but_manually_modified_and_skipped__001: - skip_update_example_snapshots: 'skipping this example because we have manually modified it' + 01_00_00__invalid__001: + a: 1 YAML end - let(:expected_html_yml_contents) do - <<~YAML - --- - 02_01_00__inlines__strong__001: - canonical: | -

bold

- static: |- -

bold

- wysiwyg: |- -

bold

- 02_01_00__inlines__strong__002: - canonical: | -

bold with more text

- static: |- -

bold with more text

- wysiwyg: |- -

bold with more text

- 02_03_00__inlines__strikethrough_extension__001: - canonical: | -

Hi Hello, world!

- static: |- -

Hi Hello, world!

- wysiwyg: |- -

Hi Hello, world!

- 03_01_00__first_gitlab_specific_section_with_examples__strong_but_with_two_asterisks__001: - canonical: | -

bold

- wysiwyg: |- -

bold

- 03_02_01__first_gitlab_specific_section_with_examples__h2_which_contains_an_h3__example_in_an_h3__001: - canonical: | -

Example in an H3

- static: |- -

Example in an H3

- wysiwyg: |- -

Example in an H3

- 04_01_00__second_gitlab_specific_section_with_examples__strong_but_with_html__001: - canonical: | -

- bold -

- static: |- - - bold - - 05_02_00__third_gitlab_specific_section_with_skipped_examples__strong_but_manually_modified_and_skipped__001: - canonical: | -

This example will have its manually modified static HTML, WYSIWYG HTML, and ProseMirror JSON preserved

- static: |- -

This is the manually modified static HTML which will be preserved

- wysiwyg: |- -

This is the manually modified WYSIWYG HTML which will be preserved

- 06_01_00__api_request_overrides__group_upload_link__001: - canonical: | -

groups-test-file

- static: |- -

groups-test-file

- wysiwyg: |- -

groups-test-file

- 06_02_00__api_request_overrides__project_repo_link__001: - canonical: | -

projects-test-file

- static: |- -

projects-test-file

- wysiwyg: |- -

projects-test-file

- 06_03_00__api_request_overrides__project_snippet_ref__001: - canonical: | -

This project snippet ID reference IS filtered: $88888 - static: |- -

This project snippet ID reference IS filtered: $88888

- wysiwyg: |- -

This project snippet ID reference IS filtered: $88888

- 06_04_00__api_request_overrides__personal_snippet_ref__001: - canonical: | -

This personal snippet ID reference is NOT filtered: $99999

- static: |- -

This personal snippet ID reference is NOT filtered: $99999

- wysiwyg: |- -

This personal snippet ID reference is NOT filtered: $99999

- 06_05_00__api_request_overrides__project_wiki_link__001: - canonical: | -

project-wikis-test-file

- static: |- -

project-wikis-test-file

- wysiwyg: |- -

project-wikis-test-file

- YAML + context 'for glfm_example_status.yml' do + let(:config_file) { described_class::GLFM_EXAMPLE_STATUS_YML_PATH } + let(:glfm_example_status_yml_contents) { invalid_example_name_file_contents } + + it 'raises error' do + expect { subject.process(skip_static_and_wysiwyg: true) }.to raise_error(err_msg) + end end - let(:expected_prosemirror_json_contents) do - <<~YAML - --- - 02_01_00__inlines__strong__001: |- - { - "type": "doc", - "content": [ - { - "type": "paragraph", - "content": [ - { - "type": "text", - "marks": [ - { - "type": "bold" - } - ], - "text": "bold" - } - ] - } - ] - } - 02_03_00__inlines__strikethrough_extension__001: |- - { - "type": "doc", - "content": [ - { - "type": "paragraph", - "content": [ - { - "type": "text", - "marks": [ - { - "type": "strike" - } - ], - "text": "Hi" - }, - { - "type": "text", - "text": " Hello, world!" - } - ] - } - ] - } - 03_01_00__first_gitlab_specific_section_with_examples__strong_but_with_two_asterisks__001: |- - { - "type": "doc", - "content": [ - { - "type": "paragraph", - "content": [ - { - "type": "text", - "marks": [ - { - "type": "bold" - } - ], - "text": "bold" - } - ] - } - ] - } - 03_02_01__first_gitlab_specific_section_with_examples__h2_which_contains_an_h3__example_in_an_h3__001: |- - { - "type": "doc", - "content": [ - { - "type": "paragraph", - "content": [ - { - "type": "text", - "text": "Example in an H3" - } - ] - } - ] - } - 04_01_00__second_gitlab_specific_section_with_examples__strong_but_with_html__001: |- - { - "existing": "This entry is manually modified and preserved because skip_update_example_snapshot_prosemirror_json will be truthy" - } - 05_02_00__third_gitlab_specific_section_with_skipped_examples__strong_but_manually_modified_and_skipped__001: |- - { - "existing": "This entry is manually modified and preserved because skip_update_example_snapshots will be truthy" - } - 06_01_00__api_request_overrides__group_upload_link__001: |- - { - "type": "doc", - "content": [ - { - "type": "paragraph", - "content": [ - { - "type": "text", - "marks": [ - { - "type": "link", - "attrs": { - "href": "/uploads/groups-test-file", - "target": "_blank", - "class": null, - "title": null, - "canonicalSrc": "/uploads/groups-test-file", - "isReference": false - } - } - ], - "text": "groups-test-file" - } - ] - } - ] - } - 06_02_00__api_request_overrides__project_repo_link__001: |- - { - "type": "doc", - "content": [ - { - "type": "paragraph", - "content": [ - { - "type": "text", - "marks": [ - { - "type": "link", - "attrs": { - "href": "projects-test-file", - "target": "_blank", - "class": null, - "title": null, - "canonicalSrc": "projects-test-file", - "isReference": false - } - } - ], - "text": "projects-test-file" - } - ] - } - ] - } - 06_03_00__api_request_overrides__project_snippet_ref__001: |- - { - "type": "doc", - "content": [ - { - "type": "paragraph", - "content": [ - { - "type": "text", - "text": "This project snippet ID reference IS filtered: $88888" - } - ] - } - ] - } - 06_04_00__api_request_overrides__personal_snippet_ref__001: |- - { - "type": "doc", - "content": [ - { - "type": "paragraph", - "content": [ - { - "type": "text", - "text": "This personal snippet ID reference is NOT filtered: $99999" - } - ] - } - ] - } - 06_05_00__api_request_overrides__project_wiki_link__001: |- - { - "type": "doc", - "content": [ - { - "type": "paragraph", - "content": [ - { - "type": "text", - "marks": [ - { - "type": "link", - "attrs": { - "href": "project-wikis-test-file", - "target": "_blank", - "class": null, - "title": null, - "canonicalSrc": "project-wikis-test-file", - "isReference": false - } - } - ], - "text": "project-wikis-test-file" - } - ] - } - ] - } - YAML + context 'for glfm_example_metadata.yml' do + let(:config_file) { described_class::GLFM_EXAMPLE_METADATA_YML_PATH } + let(:glfm_example_metadata_yml_contents) { invalid_example_name_file_contents } + + it 'raises error' do + expect { subject.process(skip_static_and_wysiwyg: true) }.to raise_error(err_msg) + end end - before do - # NOTE: This is a necessary to avoid an `error Couldn't find an integrity file` error - # when invoking `yarn jest ...` on CI from within an RSpec job. It could be solved by - # adding `.yarn-install` to be included in the RSpec CI job, but that would be a performance - # hit to all RSpec jobs. We could also make a dedicate job just for this spec. However, - # since this is just a single script, those options may not be justified. - described_class.new.run_external_cmd('yarn install') if ENV['CI'] + context 'for glfm_example_normalizations.yml' do + let(:config_file) { described_class::GLFM_EXAMPLE_NORMALIZATIONS_YML_PATH } + let(:glfm_example_normalizations_yml_contents) { invalid_example_name_file_contents } + + it 'raises error' do + expect { subject.process(skip_static_and_wysiwyg: true) }.to raise_error(err_msg) + end end + end + + context 'with full processing of static and WYSIWYG HTML' do + before(:all) do + # NOTE: It is a necessary to do a `yarn install` in order to ensure that + # `scripts/lib/glfm/render_wysiwyg_html_and_json.js` can be invoked successfully + # on the CI job (which will not be set up for frontend specs since this is + # an RSpec spec), or if the current yarn dependencies are not installed locally. + described_class.new.run_external_cmd('yarn install --frozen-lockfile') + end + + describe 'manually-curated input specification config files' do + let(:glfm_example_status_yml_contents) { '' } + let(:glfm_example_metadata_yml_contents) { '' } + let(:glfm_example_normalizations_yml_contents) { '' } + + it 'can be empty' do + expect { subject.process }.not_to raise_error + end + end + + describe 'writing html.yml and prosemirror_json.yml' do + let(:es_html_yml_contents) { reread_io(es_html_yml_io) } + let(:es_prosemirror_json_yml_contents) { reread_io(es_prosemirror_json_yml_io) } + + # NOTE: This example_status.yml is crafted in conjunction with expected_html_yml_contents + # to test the behavior of the `skip_update_*` flags + let(:glfm_example_status_yml_contents) do + <<~YAML + --- + 02_01_00__inlines__strong__002: + # NOTE: 02_01_00__inlines__strong__002: is omitted from the existing prosemirror_json.yml file, and is also + # skipped here, to show that an example does not need to exist in order to be skipped. + # TODO: This should be changed to raise an error instead, to enforce that there cannot be orphaned + # entries in glfm_example_status.yml. This task is captured in + # https://gitlab.com/gitlab-org/gitlab/-/issues/361241#other-cleanup-tasks + skip_update_example_snapshot_prosemirror_json: "skipping because JSON isn't cool enough" + 03_01_00__first_gitlab_specific_section_with_examples__strong_but_with_two_asterisks__001: + skip_update_example_snapshot_html_static: "skipping because there's too much static" + 04_01_00__second_gitlab_specific_section_with_examples__strong_but_with_html__001: + skip_update_example_snapshot_html_wysiwyg: 'skipping because what you see is NOT what you get' + skip_update_example_snapshot_prosemirror_json: "skipping because JSON still isn't cool enough" + 05_01_00__third_gitlab_specific_section_with_skipped_examples__strong_but_skipped__001: + skip_update_example_snapshots: 'skipping this example because it is very bad' + 05_02_00__third_gitlab_specific_section_with_skipped_examples__strong_but_manually_modified_and_skipped__001: + skip_update_example_snapshots: 'skipping this example because we have manually modified it' + YAML + end + + let(:expected_html_yml_contents) do + <<~YAML + --- + 02_01_00__inlines__strong__001: + canonical: | +

bold

+ static: |- +

bold

+ wysiwyg: |- +

bold

+ 02_01_00__inlines__strong__002: + canonical: | +

bold with more text

+ static: |- +

bold with more text

+ wysiwyg: |- +

bold with more text

+ 02_03_00__inlines__strikethrough_extension__001: + canonical: | +

Hi Hello, world!

+ static: |- +

Hi Hello, world!

+ wysiwyg: |- +

Hi Hello, world!

+ 03_01_00__first_gitlab_specific_section_with_examples__strong_but_with_two_asterisks__001: + canonical: | +

bold

+ wysiwyg: |- +

bold

+ 03_02_01__first_gitlab_specific_section_with_examples__h2_which_contains_an_h3__example_in_an_h3__001: + canonical: | +

Example in an H3

+ static: |- +

Example in an H3

+ wysiwyg: |- +

Example in an H3

+ 04_01_00__second_gitlab_specific_section_with_examples__strong_but_with_html__001: + canonical: | +

+ bold +

+ static: |- + + bold + + 05_02_00__third_gitlab_specific_section_with_skipped_examples__strong_but_manually_modified_and_skipped__001: + canonical: | +

This example will have its manually modified static HTML, WYSIWYG HTML, and ProseMirror JSON preserved

+ static: |- +

This is the manually modified static HTML which will be preserved

+ wysiwyg: |- +

This is the manually modified WYSIWYG HTML which will be preserved

+ 06_01_00__api_request_overrides__group_upload_link__001: + canonical: | +

groups-test-file

+ static: |- +

groups-test-file

+ wysiwyg: |- +

groups-test-file

+ 06_02_00__api_request_overrides__project_repo_link__001: + canonical: | +

projects-test-file

+ static: |- +

projects-test-file

+ wysiwyg: |- +

projects-test-file

+ 06_03_00__api_request_overrides__project_snippet_ref__001: + canonical: | +

This project snippet ID reference IS filtered: $88888 + static: |- +

This project snippet ID reference IS filtered: $88888

+ wysiwyg: |- +

This project snippet ID reference IS filtered: $88888

+ 06_04_00__api_request_overrides__personal_snippet_ref__001: + canonical: | +

This personal snippet ID reference is NOT filtered: $99999

+ static: |- +

This personal snippet ID reference is NOT filtered: $99999

+ wysiwyg: |- +

This personal snippet ID reference is NOT filtered: $99999

+ 06_05_00__api_request_overrides__project_wiki_link__001: + canonical: | +

project-wikis-test-file

+ static: |- +

project-wikis-test-file

+ wysiwyg: |- +

project-wikis-test-file

+ YAML + end + + let(:expected_prosemirror_json_contents) do + <<~YAML + --- + 02_01_00__inlines__strong__001: |- + { + "type": "doc", + "content": [ + { + "type": "paragraph", + "content": [ + { + "type": "text", + "marks": [ + { + "type": "bold" + } + ], + "text": "bold" + } + ] + } + ] + } + 02_03_00__inlines__strikethrough_extension__001: |- + { + "type": "doc", + "content": [ + { + "type": "paragraph", + "content": [ + { + "type": "text", + "marks": [ + { + "type": "strike" + } + ], + "text": "Hi" + }, + { + "type": "text", + "text": " Hello, world!" + } + ] + } + ] + } + 03_01_00__first_gitlab_specific_section_with_examples__strong_but_with_two_asterisks__001: |- + { + "type": "doc", + "content": [ + { + "type": "paragraph", + "content": [ + { + "type": "text", + "marks": [ + { + "type": "bold" + } + ], + "text": "bold" + } + ] + } + ] + } + 03_02_01__first_gitlab_specific_section_with_examples__h2_which_contains_an_h3__example_in_an_h3__001: |- + { + "type": "doc", + "content": [ + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "Example in an H3" + } + ] + } + ] + } + 04_01_00__second_gitlab_specific_section_with_examples__strong_but_with_html__001: |- + { + "existing": "This entry is manually modified and preserved because skip_update_example_snapshot_prosemirror_json will be truthy" + } + 05_02_00__third_gitlab_specific_section_with_skipped_examples__strong_but_manually_modified_and_skipped__001: |- + { + "existing": "This entry is manually modified and preserved because skip_update_example_snapshots will be truthy" + } + 06_01_00__api_request_overrides__group_upload_link__001: |- + { + "type": "doc", + "content": [ + { + "type": "paragraph", + "content": [ + { + "type": "text", + "marks": [ + { + "type": "link", + "attrs": { + "href": "/uploads/groups-test-file", + "target": "_blank", + "class": null, + "title": null, + "canonicalSrc": "/uploads/groups-test-file", + "isReference": false + } + } + ], + "text": "groups-test-file" + } + ] + } + ] + } + 06_02_00__api_request_overrides__project_repo_link__001: |- + { + "type": "doc", + "content": [ + { + "type": "paragraph", + "content": [ + { + "type": "text", + "marks": [ + { + "type": "link", + "attrs": { + "href": "projects-test-file", + "target": "_blank", + "class": null, + "title": null, + "canonicalSrc": "projects-test-file", + "isReference": false + } + } + ], + "text": "projects-test-file" + } + ] + } + ] + } + 06_03_00__api_request_overrides__project_snippet_ref__001: |- + { + "type": "doc", + "content": [ + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "This project snippet ID reference IS filtered: $88888" + } + ] + } + ] + } + 06_04_00__api_request_overrides__personal_snippet_ref__001: |- + { + "type": "doc", + "content": [ + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "This personal snippet ID reference is NOT filtered: $99999" + } + ] + } + ] + } + 06_05_00__api_request_overrides__project_wiki_link__001: |- + { + "type": "doc", + "content": [ + { + "type": "paragraph", + "content": [ + { + "type": "text", + "marks": [ + { + "type": "link", + "attrs": { + "href": "project-wikis-test-file", + "target": "_blank", + "class": null, + "title": null, + "canonicalSrc": "project-wikis-test-file", + "isReference": false + } + } + ], + "text": "project-wikis-test-file" + } + ] + } + ] + } + YAML + end - # NOTE: Both `html.yml` and `prosemirror_json.yml` generation are tested in a single example, to - # avoid slower tests, because generating the static HTML is slow due to the need to invoke - # the rails environment. We could have separate sections, but this would require an extra flag - # to the `process` method to independently skip static vs. WYSIWYG, which is not worth the effort. - it 'writes the correct content', :unlimited_max_formatted_output_length do - # expectation that skipping message is only output once per example - expect(subject).to receive(:output).once.with(/reason.*skipping this example because it is very bad/i) + # NOTE: Both `html.yml` and `prosemirror_json.yml` generation are tested in a single example, to + # avoid slower tests, because generating the static HTML is slow due to the need to invoke + # the rails environment. We could have separate sections, but this would require an extra flag + # to the `process` method to independently skip static vs. WYSIWYG, which is not worth the effort. + it 'writes the correct content', :unlimited_max_formatted_output_length do + # expectation that skipping message is only output once per example + expect(subject).to receive(:output).once.with(/reason.*skipping this example because it is very bad/i) - subject.process + subject.process - expect(es_html_yml_contents).to eq(expected_html_yml_contents) - expect(es_prosemirror_json_yml_contents).to eq(expected_prosemirror_json_contents) + expect(es_html_yml_contents).to eq(expected_html_yml_contents) + expect(es_prosemirror_json_yml_contents).to eq(expected_prosemirror_json_contents) + end end end # rubocop:enable RSpec/MultipleMemoizedHelpers diff --git a/spec/scripts/lib/glfm/update_specification_spec.rb b/spec/scripts/lib/glfm/update_specification_spec.rb index 9fb671e0016..852b2b580e6 100644 --- a/spec/scripts/lib/glfm/update_specification_spec.rb +++ b/spec/scripts/lib/glfm/update_specification_spec.rb @@ -1,21 +1,53 @@ # 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_txt_path) { described_class::GHFM_SPEC_TXT_PATH } + 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_txt_path) { described_class::GLFM_INTRO_TXT_PATH } - let(:glfm_intro_txt_io) { StringIO.new(glfm_intro_txt_contents) } - let(:glfm_examples_txt_path) { described_class::GLFM_EXAMPLES_TXT_PATH } - let(:glfm_examples_txt_io) { StringIO.new(glfm_examples_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 @@ -52,7 +84,7 @@ RSpec.describe Glfm::UpdateSpecification, '#process' do MARKDOWN end - let(:glfm_intro_txt_contents) do + let(:glfm_intro_md_contents) do # language=Markdown <<~MARKDOWN # Introduction @@ -63,9 +95,17 @@ RSpec.describe Glfm::UpdateSpecification, '#process' do MARKDOWN end - let(:glfm_examples_txt_contents) do + let(:glfm_official_specification_examples_md_contents) do <<~MARKDOWN - # GitLab-Specific Section with Examples + # 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 @@ -73,44 +113,66 @@ RSpec.describe Glfm::UpdateSpecification, '#process' do before do # Mock default ENV var values - allow(ENV).to receive(:[]).with('UPDATE_GHFM_SPEC_TXT').and_return(nil) + 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. - allow(URI).to receive(:open).with(ghfm_spec_txt_uri) { ghfm_spec_txt_uri_io } - allow(File).to receive(:open).with(ghfm_spec_txt_path) { ghfm_spec_txt_local_io } - allow(File).to receive(:open).with(glfm_intro_txt_path) { glfm_intro_txt_io } - allow(File).to receive(:open).with(glfm_examples_txt_path) { glfm_examples_txt_io } + + # 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_TXT is not true (default)' do + context 'when UPDATE_GHFM_SPEC_MD is not true (default)' do it 'does not download' do - expect(URI).not_to receive(:open).with(ghfm_spec_txt_uri) + expect(URI).not_to receive(:parse).with(ghfm_spec_txt_uri) - subject.process + 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_TXT is true' do + 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_TXT').and_return('true') - allow(File).to receive(:open).with(ghfm_spec_txt_path, 'w') { ghfm_spec_txt_local_io } + 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 + subject.process(skip_spec_html_generation: true) expect(reread_io(ghfm_spec_txt_local_io)).to eq(ghfm_spec_txt_contents) end @@ -128,7 +190,9 @@ RSpec.describe Glfm::UpdateSpecification, '#process' do end it 'raises an error' do - expect { subject.process }.to raise_error /version mismatch.*expected.*29.*got.*30/i + expect do + subject.process(skip_spec_html_generation: true) + end.to raise_error /version mismatch.*expected.*29.*got.*30/i end end @@ -136,7 +200,7 @@ RSpec.describe Glfm::UpdateSpecification, '#process' do let(:ghfm_spec_txt_contents) { '' } it 'raises an error if lines cannot be read' do - expect { subject.process }.to raise_error /unable to read lines/i + expect { subject.process(skip_spec_html_generation: true) }.to raise_error /unable to read lines/i end end @@ -146,7 +210,7 @@ RSpec.describe Glfm::UpdateSpecification, '#process' do end it 'raises an error if file is blank' do - expect { subject.process }.to raise_error /unable to read string/i + expect { subject.process(skip_spec_html_generation: true) }.to raise_error /unable to read string/i end end end @@ -157,7 +221,7 @@ RSpec.describe Glfm::UpdateSpecification, '#process' do let(:glfm_contents) { reread_io(glfm_spec_txt_io) } before do - subject.process + subject.process(skip_spec_html_generation: true) end it 'replaces the header text with the GitLab version' do @@ -170,14 +234,18 @@ RSpec.describe Glfm::UpdateSpecification, '#process' do 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_txt_contents)}/m) + expect(glfm_contents).to match(/#{Regexp.escape(glfm_intro_md_contents)}/m) end - it 'inserts the GitLab examples sections before the appendix section' do + it 'inserts the GitLab official spec and internal extension examples sections before the appendix section' do expected = <<~MARKDOWN End of last GitHub examples section. - # GitLab-Specific Section with Examples + # Official Specification Section with Examples + + Some examples. + + # Internal Extension Section with Examples Some examples. @@ -189,6 +257,51 @@ RSpec.describe Glfm::UpdateSpecification, '#process' do 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 = <<~HTML +
+
title: GitLab Flavored Markdown (GLFM) Spec
+        version: alpha
+ +
+

+ Introduction

+

+ What is GitLab Flavored Markdown?

+

Intro text about GitLab Flavored Markdown.

+

+ Section with Examples

+

+ Strong

+
+
__bold__
+        .
+        <p><strong>bold</strong></p>
+ +
+

End of last GitHub examples section.

+

+ Official Specification Section with Examples

+

Some examples.

+

+ Internal Extension Section with Examples

+

Some examples.

+ +

+ Appendix

+

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) diff --git a/spec/scripts/lib/glfm/verify_all_generated_files_are_up_to_date_spec.rb b/spec/scripts/lib/glfm/verify_all_generated_files_are_up_to_date_spec.rb new file mode 100644 index 00000000000..fca037c9ff3 --- /dev/null +++ b/spec/scripts/lib/glfm/verify_all_generated_files_are_up_to_date_spec.rb @@ -0,0 +1,62 @@ +# frozen_string_literal: true +require 'fast_spec_helper' +require_relative '../../../../scripts/lib/glfm/verify_all_generated_files_are_up_to_date' + +# IMPORTANT NOTE: See https://docs.gitlab.com/ee/development/gitlab_flavored_markdown/specification_guide/#verify-all-generated-files-are-up-to-daterb-script +# for details on the implementation and usage of the `verify_all_generated_files_are_up_to_date.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. +RSpec.describe Glfm::VerifyAllGeneratedFilesAreUpToDate, '#process' do + subject { described_class.new } + + let(:output_path) { described_class::GLFM_SPEC_OUTPUT_PATH } + let(:snapshots_path) { described_class::EXAMPLE_SNAPSHOTS_PATH } + let(:verify_cmd) { "git status --porcelain #{output_path} #{snapshots_path}" } + + before do + # Prevent console output when running tests + allow(subject).to receive(:output) + end + + context 'when repo is dirty' do + before do + # Simulate a dirty repo + allow(subject).to receive(:run_external_cmd).with(verify_cmd).and_return(" M #{output_path}") + end + + it 'raises an error', :unlimited_max_formatted_output_length do + expect { subject.process }.to raise_error(/Cannot run.*uncommitted changes.*#{output_path}/m) + end + end + + context 'when repo is clean' do + before do + # Mock out all yarn install and script execution + allow(subject).to receive(:run_external_cmd).with('yarn install --frozen-lockfile') + allow(subject).to receive(:run_external_cmd).with(/update-specification.rb/) + allow(subject).to receive(:run_external_cmd).with(/update-example-snapshots.rb/) + end + + context 'when all generated files are up to date' do + before do + # Simulate a clean repo, then simulate no changes to generated files + allow(subject).to receive(:run_external_cmd).twice.with(verify_cmd).and_return('', '') + end + + it 'does not raise an error', :unlimited_max_formatted_output_length do + expect { subject.process }.not_to raise_error + end + end + + context 'when generated file(s) are not up to date' do + before do + # Simulate a clean repo, then simulate changes to generated files + allow(subject).to receive(:run_external_cmd).twice.with(verify_cmd).and_return('', "M #{snapshots_path}") + end + + it 'raises an error', :unlimited_max_formatted_output_length do + expect { subject.process }.to raise_error(/following files were modified.*#{snapshots_path}/m) + end + end + end +end diff --git a/spec/scripts/trigger-build_spec.rb b/spec/scripts/trigger-build_spec.rb index 46023d5823d..ac8e3c7797c 100644 --- a/spec/scripts/trigger-build_spec.rb +++ b/spec/scripts/trigger-build_spec.rb @@ -21,8 +21,6 @@ RSpec.describe Trigger do 'GITLAB_USER_NAME' => 'gitlab_user_name', 'GITLAB_USER_LOGIN' => 'gitlab_user_login', 'QA_IMAGE' => 'qa_image', - 'OMNIBUS_GITLAB_CACHE_UPDATE' => 'omnibus_gitlab_cache_update', - 'OMNIBUS_GITLAB_PROJECT_ACCESS_TOKEN' => nil, 'DOCS_PROJECT_API_TOKEN' => nil } end -- cgit v1.2.3