diff --git a/README.md b/README.md index f2c3a04fad..3a2a391bd7 100644 --- a/README.md +++ b/README.md @@ -114,54 +114,48 @@ the exercise, which lives in the x-common repository. This change will need to be submitted as a pull request to the x-common repository. This pull request needs to be merged before you can regenerate the exercise. -Changes that don't have to do directly with the test inputs and outputs, will either need to be -made to `exercises/$PROBLEM/example.tt` or `lib/$PROBLEM_cases.rb`. Then you can regenerate the -exercise with `bin/generate $PROBLEM`. +Changes that don't have to do directly with the test inputs and outputs, will +most likely be made to `lib/$PROBLEM_cases.rb` but may also be made to +`exercises/$PROBLEM/example.tt`. Then you can regenerate the exercise with +`bin/generate $PROBLEM`. #### Implementing a Generator -You will need to implement three files to create a generator: +You will need to implement two files and a directory to create a generator: -1. `exercises/$PROBLEM/example.tt` - the Erb template for the test file, `$PROBLEM_test.rb`. -1. `exercises/$PROBLEM/.meta/.version` - used to keep track of the version of the test files as the data changes. 1. `lib/$PROBLEM_cases.rb` - the logic for turning the data into tests. +1. `exercises/$PROBLEM/example.tt` - the Erb template for the test file, `$PROBLEM_test.rb`. +1. `exercises/$PROBLEM/.meta/` - metadata directory, currently contains version file You will not need to touch the top-level script, `bin/generate`. The `bin/generate` command relies on some common logic implemented in `lib/generator.rb`. You probably won't need to touch that, either. -The `lib/$PROBLEM_cases.rb` file should contain a small class that wraps the JSON for a single test case: +`lib/$PROBLEM_cases.rb` contains a derived class of `ExerciseCase` (in `lib/generator/exercise_cases.rb`) +which wraps the JSON for a single test case. The default version looks something like this: ``` -require 'exercise_cases' - -class ProblemNameCase < OpenStruct - def name - 'test_%s' % description.gsub(/[ -]/, '_') - end +class ProblemNameCase < ExerciseCase def workload # Example workload: - "assert #{expected.inspect}, Problem.call(#{input.inspect})" + "#{assert} Problem.call(#{input.inspect})" end - def skipped - index.zero? ? '# skip' : 'skip' - end end ``` -Instead of `ProblemName` use the name of the actual problem. This is important, since +Instead of `ProblemName` use the CamelCased name of the actual problem. This is important, since the generator script will infer the name of the class from the argument that is passed. -This class must implement the following methods: - -- `name` - Returns the name of the test (i.e `test_one_equals_one`) -- `workload` - Returns the main syntax for the test. This includes the assertion and any setup required for the test. This will vary depending on the test generator and its underlying implementation -- `skipped` - Returns skip syntax (i.e. `skip` or `# skip`) +This class must provide the methods used by `example.tt`. The base class provides methods +for the default template for everything except `workload`. -Beyond that, you can implement any helper methods that you need. +`workload` generates the code for the body of a test, including the assertion +and any setup required. The base class provides a variety of assertion and +helper methods. Beyond that, you can implement any helper methods that you need +as private methods in your derived class. Below this class, implement a small loop that will generate all the test cases by reading the `canonical-data.json` file, and looping through the test cases. @@ -198,7 +192,8 @@ end Finally, you need to create a text template, `example.tt`, as the bases for the test suite. -Start with the following boilerplate, and adjust as necessary: +Start with the following boilerplate, and adjust as necessary. Remember, however, to strive +to keep logic out of views. ``` #!/usr/bin/env ruby @@ -216,6 +211,7 @@ class ProblemNameTest < Minitest::Test <% end %> <%= IO.read(XRUBY_LIB + '/bookkeeping.md') %> + def test_bookkeeping skip assert_equal <%= version %>, BookKeeping::VERSION diff --git a/exercises/hamming/example.tt b/exercises/hamming/example.tt index eae5e6209a..2b477261f1 100644 --- a/exercises/hamming/example.tt +++ b/exercises/hamming/example.tt @@ -4,14 +4,16 @@ require 'minitest/autorun' require_relative 'hamming' # Common test data version: <%= abbreviated_commit_hash %> -class HammingTest < Minitest::Test<% test_cases.each do |test_case| %> +class HammingTest < Minitest::Test +<% test_cases.each do |test_case| %> def <%= test_case.name %> - <%= test_case.skipped %><% if test_case.raises_error? %> - assert_raises(ArgumentError) { <%= test_case.workload %> }<% else %> - assert_equal <%= test_case.expected %>, <%= test_case.workload %><% end %> + <%= test_case.skipped %> + <%= test_case.workload %> end + <% end %> <%= IO.read(XRUBY_LIB + '/bookkeeping.md') %> + def test_bookkeeping skip assert_equal <%= version %>, BookKeeping::VERSION diff --git a/exercises/hamming/hamming_test.rb b/exercises/hamming/hamming_test.rb index 06d9fead88..7053e045ec 100755 --- a/exercises/hamming/hamming_test.rb +++ b/exercises/hamming/hamming_test.rb @@ -3,9 +3,7 @@ require 'minitest/autorun' require_relative 'hamming' -# Test data version: -# deb225e Implement canonical dataset for scrabble-score problem (#255) - +# Common test data version: bb56dc7 class HammingTest < Minitest::Test def test_identical_strands # skip @@ -88,8 +86,9 @@ def test_disallow_second_strand_longer # not your solution. # # Define a constant named VERSION inside of the top level BookKeeping - # module. - # In your file, it will look like this: + # module, which may be placed near the end of your file. + # + # In your file, it will look like this: # # module BookKeeping # VERSION = 1 # Where the version number matches the one in the test. diff --git a/exercises/luhn/luhn_test.rb b/exercises/luhn/luhn_test.rb index 1b7828f669..40d3348d29 100755 --- a/exercises/luhn/luhn_test.rb +++ b/exercises/luhn/luhn_test.rb @@ -10,27 +10,27 @@ def test_single_digit_strings_can_not_be_valid refute Luhn.valid?("1") end - def test_A_single_zero_is_invalid + def test_a_single_zero_is_invalid skip refute Luhn.valid?("0") end - def test_a_simple_valid_SIN_that_remains_valid_if_reversed + def test_a_simple_valid_sin_that_remains_valid_if_reversed skip assert Luhn.valid?("059") end - def test_a_simple_valid_SIN_that_becomes_invalid_if_reversed + def test_a_simple_valid_sin_that_becomes_invalid_if_reversed skip assert Luhn.valid?("59") end - def test_a_valid_Canadian_SIN + def test_a_valid_canadian_sin skip assert Luhn.valid?("055 444 285") end - def test_invalid_Canadian_SIN + def test_invalid_canadian_sin skip refute Luhn.valid?("055 444 286") end diff --git a/exercises/ocr-numbers/ocr_numbers_test.rb b/exercises/ocr-numbers/ocr_numbers_test.rb index f1c19ef83b..67bec5b61d 100755 --- a/exercises/ocr-numbers/ocr_numbers_test.rb +++ b/exercises/ocr-numbers/ocr_numbers_test.rb @@ -85,7 +85,7 @@ def test_recognizes_string_of_decimal_numbers assert_equal "1234567890", OcrNumbers.convert(" _ _ _ _ _ _ _ _ \n | _| _||_||_ |_ ||_||_|| |\n ||_ _| | _||_| ||_| _||_|\n ") end - def test_numbers_separated_by_empty_lines_are_recognized__lines_are_joined_by_commas_ + def test_numbers_separated_by_empty_lines_are_recognized_lines_are_joined_by_commas skip assert_equal "123,456,789", OcrNumbers.convert(" _ _ \n | _| _|\n ||_ _|\n \n _ _ \n|_||_ |_ \n | _||_|\n \n _ _ _ \n ||_||_|\n ||_| _|\n ") end diff --git a/lib/generator/exercise_cases.rb b/lib/generator/exercise_cases.rb new file mode 100644 index 0000000000..f1f7271b25 --- /dev/null +++ b/lib/generator/exercise_cases.rb @@ -0,0 +1,43 @@ +require 'ostruct' +require 'json' + +class ExerciseCase < OpenStruct + using Generator::Underscore + + def name + 'test_%s' % description.underscore + end + + def skipped + index.zero? ? '# skip' : 'skip' + end + + protected + + # used in workload, for example, as + # "#{assert} Luhn.valid?(#{input.inspect})" + def assert + expected ? 'assert' : 'refute' + end + + # used in workload, for example, as + # assert_equal { "PigLatin.translate(#{input.inspect})" } + def assert_equal + "assert_equal #{expected.inspect}, #{yield}" + end + + # used in workload, for example, as + # if raises_error? + # assert_raises(ArgumentError) { test_case } + # else + # assert_equal { test_case } + # end + + def raises_error? + expected.to_i == -1 + end + + def assert_raises(error) + "assert_raises(#{error}) { #{yield} }" + end +end diff --git a/lib/generator/underscore.rb b/lib/generator/underscore.rb new file mode 100644 index 0000000000..46d5a94559 --- /dev/null +++ b/lib/generator/underscore.rb @@ -0,0 +1,9 @@ +module Generator + module Underscore + refine String do + def underscore + downcase.gsub(/[- ]/, '_').gsub(/[^\w?]/, '') + end + end + end +end diff --git a/lib/hamming_cases.rb b/lib/hamming_cases.rb index 1ad4f69cca..d6d8832831 100644 --- a/lib/hamming_cases.rb +++ b/lib/hamming_cases.rb @@ -1,21 +1,16 @@ -require 'exercise_cases' - -class HammingCase < OpenStruct - def name - 'test_%s' % description.gsub(/[ -]/, '_') - end - +class HammingCase < ExerciseCase def workload - "Hamming.compute('#{strand1}', '#{strand2}')" + if raises_error? + assert_raises(ArgumentError) { test_case } + else + assert_equal { test_case } + end end - def raises_error? - expected.to_i == -1 - end + private - def skipped - index.zero? && '# skip' || - 'skip' + def test_case + "Hamming.compute('#{strand1}', '#{strand2}')" end end diff --git a/lib/luhn_cases.rb b/lib/luhn_cases.rb index df608b4e23..377519f199 100644 --- a/lib/luhn_cases.rb +++ b/lib/luhn_cases.rb @@ -1,22 +1,6 @@ -require 'exercise_cases' - -class LuhnCase < OpenStruct - def name - 'test_%s' % description.tr('- ', '__') - end - +class LuhnCase < ExerciseCase def workload - %Q(#{assertion} Luhn.valid?(#{input.inspect})) - end - - def skipped - index.zero? ? '# skip' : 'skip' - end - - private - - def assertion - expected ? 'assert' : 'refute' + "#{assert} Luhn.valid?(#{input.inspect})" end end diff --git a/lib/ocr_numbers_cases.rb b/lib/ocr_numbers_cases.rb index 4fc94dd98c..fea8e4f2e8 100644 --- a/lib/ocr_numbers_cases.rb +++ b/lib/ocr_numbers_cases.rb @@ -1,22 +1,12 @@ -require 'exercise_cases' - -class OcrNumbersCase < OpenStruct - def name - 'test_%s' % description.downcase.tr('- .', '_') - end - +class OcrNumbersCase < ExerciseCase def workload - if expected == -1 - "assert_raises(ArgumentError) { #{test_case} }" + if raises_error? + assert_raises(ArgumentError) { test_case } else - "assert_equal #{expected.inspect}, #{test_case}" + assert_equal { test_case } end end - def skipped - index.zero? ? '# skip' : 'skip' - end - private def test_case diff --git a/lib/pig_latin_cases.rb b/lib/pig_latin_cases.rb index e256dfdefe..c6b9b701f6 100644 --- a/lib/pig_latin_cases.rb +++ b/lib/pig_latin_cases.rb @@ -1,16 +1,6 @@ -require 'exercise_cases' - -class PigLatinCase < OpenStruct - def name - 'test_%s' % description.tr('- ', '__') - end - +class PigLatinCase < ExerciseCase def workload - %Q(assert_equal #{expected.inspect}, PigLatin.translate(#{input.inspect})) - end - - def skipped - index.zero? ? '# skip' : 'skip' + assert_equal { "PigLatin.translate(#{input.inspect})" } end end diff --git a/test/generator/underscore_test.rb b/test/generator/underscore_test.rb new file mode 100644 index 0000000000..f5a753ea6e --- /dev/null +++ b/test/generator/underscore_test.rb @@ -0,0 +1,23 @@ +require_relative '../test_helper' + +module Generator + class UnderscoreTest < MiniTest::Test + using Underscore + + # at present, we're downcasing everything, including TLAs + def test_mixed_case + assert_equal 'A string with TLA'.underscore, 'a_string_with_tla' + end + + def test_hyphenated_text + assert_equal 'large distance in off-by-one strand'.underscore, 'large_distance_in_off_by_one_strand' + end + + def test_question_mark + assert_equal( + 'Unreadable but correctly sized inputs return ?'.underscore, + 'unreadable_but_correctly_sized_inputs_return_?' + ) + end + end +end