Skip to content

Commit

Permalink
Add parser round trip test to CI
Browse files Browse the repository at this point in the history
  • Loading branch information
mbj committed Feb 6, 2022
1 parent dd10945 commit 4fd4a7d
Show file tree
Hide file tree
Showing 3 changed files with 113 additions and 45 deletions.
16 changes: 16 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
Expand Down
4 changes: 4 additions & 0 deletions Changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
138 changes: 93 additions & 45 deletions test/run-parser-tests.rb → bin/parser-round-trip-test
100644 → 100755
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand All @@ -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

Expand All @@ -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

Expand All @@ -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

Expand Down Expand Up @@ -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
Expand All @@ -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(
'%3<number>d/%3<total>d: %-16<status>s %<name>s[%02<group_index>d] %<message>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
Expand Down Expand Up @@ -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

0 comments on commit 4fd4a7d

Please sign in to comment.