From 4fd4a7d5463b9413360fe1cc85364a3077390448 Mon Sep 17 00:00:00 2001 From: Markus Schirp Date: Sun, 6 Feb 2022 02:16:54 +0000 Subject: [PATCH] Add parser round trip test to CI --- .github/workflows/ci.yml | 16 ++ Changelog.md | 4 + .../parser-round-trip-test | 138 ++++++++++++------ 3 files changed, 113 insertions(+), 45 deletions(-) rename test/run-parser-tests.rb => bin/parser-round-trip-test (50%) mode change 100644 => 100755 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4195dd64..4e66dcd1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -63,6 +63,22 @@ jobs: bundler-cache: true ruby-version: ${{ matrix.ruby }} - run: bundle exec bin/corpus + ruby-parser-round-trip-tests: + name: Parser Round Trip Tests + runs-on: ${{ matrix.os }} + timeout-minutes: 10 + strategy: + fail-fast: false + matrix: + ruby: [ruby-2.6, ruby-2.7, ruby-3.0] + os: [macos-latest, ubuntu-latest] + steps: + - uses: actions/checkout@v2 + - uses: ruby/setup-ruby@v1 + with: + bundler-cache: true + ruby-version: ${{ matrix.ruby }} + - run: bundle exec ruby bin/parser-round-trip-test ruby-rubocop: name: Rubocop runs-on: ${{ matrix.os }} diff --git a/Changelog.md b/Changelog.md index 57254765..4c1b9bd7 100644 --- a/Changelog.md +++ b/Changelog.md @@ -4,6 +4,10 @@ * Fix emit of of `match_pattern` vs `match_pattern_p` +[#298](https://github.com/mbj/unparser/pull/298) + +* Add round trip tests dynamically derived from the `parser` gems test suite to CI + # v0.6.3 2022-01-16 [#290](https://github.com/mbj/unparser/pull/290) diff --git a/test/run-parser-tests.rb b/bin/parser-round-trip-test old mode 100644 new mode 100755 similarity index 50% rename from test/run-parser-tests.rb rename to bin/parser-round-trip-test index fb4b867a..9725a08f --- a/test/run-parser-tests.rb +++ b/bin/parser-round-trip-test @@ -1,19 +1,33 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + require 'unparser' -$: << __dir__ +# Hack to dynamically re-use the `parser` gems test suite on CI. +# The main idea is create a fake minitet runner to capture the +# signature of the examples encoded in the parsers test suite dynamically. +# +# This makes maintenance much more easier, especially on tracking new ruby +# syntax addtions. +# +# The API surface of the parser tests so far is low churn, while it may still +# make sense to provide the parser tests as an more easy to re-use data upstream. + +$LOAD_PATH << Pathname.new(__dir__).parent.join('test') -testBuilder = Class.new(Parser::Builders::Default) -testBuilder.modernize +test_builder = Class.new(Parser::Builders::Default) +test_builder.modernize -MODERN_ATTRIBUTES = testBuilder.instance_variables.map do |instance_variable| - attribute_name = instance_variable.to_s[1..-1].to_sym - [attribute_name, testBuilder.public_send(attribute_name)] -end.to_h +MODERN_ATTRIBUTES = test_builder.instance_variables.to_h do |instance_variable| + attribute_name = instance_variable.to_s[1..].to_sym + [attribute_name, test_builder.public_send(attribute_name)] +end +# Overwrite global scope method in the parser test suite def default_builder_attributes - MODERN_ATTRIBUTES.keys.map do |attribute_name| + MODERN_ATTRIBUTES.keys.to_h do |attribute_name| [attribute_name, Parser::Builders::Default.public_send(attribute_name)] - end.to_h + end end class Test @@ -26,13 +40,11 @@ class Test :rubies ) - TARGET_RUBIES = %w[2.6 2.7] - EXPECT_FAILURE = {}.freeze def legacy_attributes - default_builder_attributes.select do |attribute_name, value| - !MODERN_ATTRIBUTES.fetch(attribute_name).equal?(value) + default_builder_attributes.reject do |attribute_name, value| + MODERN_ATTRIBUTES.fetch(attribute_name).equal?(value) end.to_h end memoize :legacy_attributes @@ -43,7 +55,7 @@ def skip_reason elsif !allow_ruby? "Non targeted rubies: #{rubies.join(',')}" elsif validation.original_node.left? - "Test specifies a syntax error" + 'Test specifies a syntax error' end end @@ -56,18 +68,21 @@ def expect_failure? end def allow_ruby? - rubies.empty? || rubies.any?(TARGET_RUBIES.method(:include?)) + rubies.empty? || rubies.include?(RUBY_VERSION) end def right(value) Unparser::Either::Right.new(value) end + # rubocop:disable Metrics/AbcSize def validation identification = name.to_s + generated_source = Unparser.unparse_either(node) + .fmap { |string| string.dup.force_encoding(parser_source.encoding).freeze } - generated_node = generated_source.lmap{ |_value| }.bind do |source| + generated_node = generated_source.bind do |source| parse_either(source, identification) end @@ -79,11 +94,12 @@ def validation original_source: right(parser_source) ) end + # rubocop:enable Metrics/AbcSize memoize :validation def parser Unparser.parser.tap do |parser| - %w(foo bar baz).each(&parser.static_env.method(:declare)) + %w[foo bar baz].each(&parser.static_env.method(:declare)) end end @@ -115,7 +131,7 @@ def call def expect_failure if test.success? - fail format('Expected Failure', 'but got success') + message('Expected Failure', 'but got success') else print('Expected Failure') end @@ -126,22 +142,32 @@ def expect_success print('Success') else puts(test.validation.report) - fail format('Failure') + fail message('Failure') end end - def format(status, message = '') - '%3d/%3d: %-16s %s[%02d] %s' % [number, total, status, test.name, test.group_index, message] + def message(status, message = '') + format( + '%3d/%3d: %-16s %s[%02d] %s', + number: number, + total: total, + status: status, + name: test.name, + group_index: test.group_index, + message: message + ) end def print(status, message = '') - puts(format(status, message)) + puts(message(status, message)) end end module Minitest - class Test - end # Test + # Stub parent class + # rubocop:disable Lint/EmptyClass + class Test; end # Test + # rubocop:enable Lint/EmptyClass end # Minitest class Extractor @@ -183,46 +209,68 @@ def call(name) end end -require '../parser/test/parse_helper.rb' -require '../parser/test/test_parser.rb' +PARSER_PATH = Pathname.new('tmp/parser') + +unless PARSER_PATH.exist? + Kernel.system( + *%W[ + git + clone + https://github.com/whitequark/parser + #{PARSER_PATH} + ], + exception: true + ) +end + +Dir.chdir(PARSER_PATH) do + Kernel.system( + *%W[ + git + checkout + v#{Parser::VERSION} + ], + exception: true + ) + Kernel.system(*%w[git clean --force -d -X], exception: true) +end + +require "./#{PARSER_PATH}/test/parse_helper" +require "./#{PARSER_PATH}/test/test_parser" EXTRACTOR = Extractor.new module ParseHelper - def assert_diagnoses(*arguments) - end + def assert_diagnoses(*arguments); end def s(type, *children) Parser::AST::Node.new(type, children) end + # rubocop:disable Metrics/ParameterLists def assert_parses(node, parser_source, _diagnostics = nil, rubies = []) - EXTRACTOR.capture( - default_builder_attributes: default_builder_attributes, - node: node, - parser_source: parser_source, - rubies: rubies - ) + EXTRACTOR.capture( + default_builder_attributes: default_builder_attributes, + node: node, + parser_source: parser_source, + rubies: rubies + ) end + # rubocop:enable Metrics/ParameterLists - def test_clrf_line_endings(*arguments) - end + def test_clrf_line_endings(*arguments); end - def with_versions(*arguments) - end + def with_versions(*arguments); end - def assert_context(*arguments) - end + def assert_context(*arguments); end - def refute_diagnoses(*arguments) - end + def refute_diagnoses(*arguments); end - def assert_diagnoses_many(*arguments) - end + def assert_diagnoses_many(*arguments); end end TestParser.instance_methods.grep(/\Atest_/).each(&EXTRACTOR.method(:call)) EXTRACTOR.tests.sort_by(&:name).each_with_index do |test, index| - Execution.new(number: index.succ, total: EXTRACTOR.tests.length, test: test).call + Execution.new(number: index.succ, total: EXTRACTOR.tests.length, test: test).call end