Skip to content

Commit

Permalink
Merge pull request #572 from hilary/default_test_template
Browse files Browse the repository at this point in the history
generators: Default test template
  • Loading branch information
kotp committed Apr 28, 2017
2 parents fa8af11 + 9929c24 commit faa3286
Show file tree
Hide file tree
Showing 9 changed files with 205 additions and 111 deletions.
54 changes: 14 additions & 40 deletions README.md
Expand Up @@ -64,7 +64,7 @@ Note that flags which have an attached value, like above, must take the form

### Generated Test Suites

If you find an `example.tt` file in a problem directory, then the test suite is
If you find a `<problem_name>_cases.rb` file in `lib/`, then the test suite is
generated from shared data, which can be found in the exercise definition in the [x-common][]
repository.

Expand Down Expand Up @@ -94,7 +94,8 @@ bin/generate $PROBLEM

#### Changing a Generated Exercise

The `$PROBLEM/$PROBLEM_test.rb` will never be edited directly.
Do not edit `$PROBLEM/$PROBLEM_test.rb`. Any changes you make will be overwritten when
the file is generated the next time.

There are two reasons why a test suite might change:

Expand All @@ -114,23 +115,14 @@ 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
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
Changes that don't have to do directly with the test inputs and outputs should
be made to `lib/$PROBLEM_cases.rb`. Then you can regenerate the exercise with
`bin/generate $PROBLEM`.

#### Implementing a Generator

You will need to implement two files and a directory to create a generator:

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.
You will need to write code to produce the code that goes inside the test methods. Your
code will live in `lib/$PROBLEM_cases.rb`.

`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:
Expand All @@ -149,42 +141,24 @@ end
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 provide the methods used by `example.tt`. The base class provides methods
This class must provide the methods used by `lib/generator/test_template.erb`. The base class provides methods
for the default template for everything except `workload`.

`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. See below for more information about [the intention of workload](#workload-philosophy)

Finally, you need to create a text template, `example.tt`, as the bases for the test suite.
If you really must add additional logic to the view template, you can use a custom
template. Copy `lib/generator/test_template.erb` to your exercise directory, name it
`exercise.tt`, and customize.

Start with the following boilerplate, and adjust as necessary. Remember, however, to strive
to keep logic out of views.
You will not need to touch the top-level script, `bin/generate`.

```
#!/usr/bin/env ruby
gem 'minitest', '>= 5.0.0'
require 'minitest/autorun'
require_relative '$PROBLEM'
# Common test data version: <%= abbreviated_commit_hash %>
class ProblemNameTest < Minitest::Test
<% test_cases.each do |test_case| %>
def <%= test_case.name %>
<%= test_case.skipped %>
<%= test_case.workload %>
end
The `bin/generate` command relies on some common logic implemented in `lib/generator.rb`.
You won't need to touch that, either.

<% end %>
<%= IO.read(XRUBY_LIB + '/bookkeeping.md') %>

def test_bookkeeping
skip
assert_equal <%= version %>, BookKeeping::VERSION
end
end
```
### Workload philosophy.

Prioritize educational value over expert comprehension and make sure that
Expand Down
29 changes: 29 additions & 0 deletions lib/generator/files/generator_cases.rb
@@ -0,0 +1,29 @@
module Generator
module Files
module GeneratorCases
module_function

def available(track_path)
generator_glob = File.join(track_path, 'lib', '*_cases.rb')
Dir[generator_glob].sort.map { |filename| exercise_name(filename) }
end

def filename(exercise_name)
"#{exercise_name.tr('-', '_')}_cases"
end

def class_name(exercise_name)
filename(exercise_name)[0..-2].split('_').map(&:capitalize).join
end

def exercise_name(filename)
%r{([^/]*)_cases\.rb$}.match(filename).captures[0].tr('_', '-')
end

def load_filename(track_path, exercise_name)
path = File.join(track_path, 'lib')
"%s/%s.rb" % [ path, filename(exercise_name) ]
end
end
end
end
38 changes: 11 additions & 27 deletions lib/generator/files/track_files.rb
Expand Up @@ -2,32 +2,6 @@

module Generator
module Files
module GeneratorCases
module_function

def available(track_path)
generator_glob = File.join(track_path, 'lib', '*_cases.rb')
Dir[generator_glob].sort.map { |filename| exercise_name(filename) }
end

def filename(exercise_name)
"#{exercise_name.tr('-', '_')}_cases"
end

def class_name(exercise_name)
filename(exercise_name)[0..-2].split('_').map(&:capitalize).join
end

def exercise_name(filename)
%r{([^/]*)_cases\.rb$}.match(filename).captures[0].tr('_', '-')
end

def load_filename(track_path, exercise_name)
path = File.join(track_path, 'lib')
"%s/%s.rb" % [ path, filename(exercise_name) ]
end
end

module TrackFiles
include Exercise

Expand All @@ -44,7 +18,7 @@ def minitest_tests
end

def tests_template
TestsTemplateFile.new(filename: File.join(exercise_path, 'example.tt'))
TestsTemplateFile.new(filename: tests_template_filename)
end

private
Expand All @@ -53,6 +27,16 @@ def exercise_path
File.join(paths.track, 'exercises', exercise_name)
end

# this method contains a LOT of magic text
def tests_template_filename
File.exist?(track_tests_template_filename) ? track_tests_template_filename :
File.join(paths.track, 'lib', 'generator', 'test_template.erb')
end

def track_tests_template_filename
File.join(exercise_path, 'example.tt')
end

def minitest_tests_filename
"#{exercise_name.gsub(/[ -]/, '_')}_test.rb"
end
Expand Down
32 changes: 26 additions & 6 deletions lib/generator/template_values.rb
@@ -1,11 +1,14 @@
require 'forwardable'

module Generator
# Contains methods accessible to the ERB template
class TemplateValues
attr_reader :abbreviated_commit_hash, :version, :test_cases
attr_reader :metadata, :test_cases
extend Forwardable
def_delegators :@metadata, :abbreviated_commit_hash, :version, :exercise_name, :exercise_name_camel

def initialize(abbreviated_commit_hash:, version:, test_cases:)
@abbreviated_commit_hash = abbreviated_commit_hash
@version = version
def initialize(metadata:, test_cases:)
@metadata = metadata
@test_cases = test_cases
end

Expand All @@ -14,11 +17,28 @@ def get_binding
end
end

class TemplateMetadata
attr_reader :abbreviated_commit_hash, :version, :exercise_name

def initialize(abbreviated_commit_hash:, version:, exercise_name:)
@abbreviated_commit_hash = abbreviated_commit_hash
@version = version
@exercise_name = exercise_name ? exercise_name.tr('-_', '_') : ''
end

def exercise_name_camel
exercise_name.split(/[-_]/).map(&:capitalize).join
end
end

module TemplateValuesFactory
def template_values
TemplateValues.new(
abbreviated_commit_hash: canonical_data.abbreviated_commit_hash,
version: version,
metadata: TemplateMetadata.new(
abbreviated_commit_hash: canonical_data.abbreviated_commit_hash,
version: version,
exercise_name: exercise_name
),
test_cases: extract
)
end
Expand Down
21 changes: 21 additions & 0 deletions lib/generator/test_template.erb
@@ -0,0 +1,21 @@
#!/usr/bin/env ruby
gem 'minitest', '>= 5.0.0'
require 'minitest/autorun'
require_relative '<%= metadata.exercise_name %>'

# Common test data version: <%= metadata.abbreviated_commit_hash %>
class <%= metadata.exercise_name_camel %>Test < Minitest::Test
<% test_cases.each do |test_case| %>
def <%= test_case.name %>
<%= test_case.skipped %>
<%= test_case.workload %>
end
<% end %>
<%= IO.read(XRUBY_LIB + '/bookkeeping.md') %>

def test_bookkeeping
skip
assert_equal <%= metadata.version %>, BookKeeping::VERSION
end
end
21 changes: 21 additions & 0 deletions test/fixtures/xruby/lib/generator/test_template.erb
@@ -0,0 +1,21 @@
#!/usr/bin/env ruby
gem 'minitest', '>= 5.0.0'
require 'minitest/autorun'
require_relative '<%= metadata.exercise_name %>'

# Common test data version: <%= metadata.abbreviated_commit_hash %>
class <%= metadata.exercise_name_camel %>Test < Minitest::Test
<% test_cases.each do |test_case| %>
def <%= test_case.name %>
<%= test_case.skipped %>
<%= test_case.workload %>
end
<% end %>
<%= IO.read(XRUBY_LIB + '/bookkeeping.md') %>

def test_bookkeeping
skip
assert_equal <%= metadata.version %>, BookKeeping::VERSION
end
end
33 changes: 33 additions & 0 deletions test/generator/files/generate_cases_test.rb
@@ -0,0 +1,33 @@
require_relative '../../test_helper.rb'

module Generator
module Files
class GeneratorCasesTest < Minitest::Test
def test_no_cases_found
track_path = 'test/fixtures/nonexistant'
assert_equal [], GeneratorCases.available(track_path)
end

def test_cases_found
track_path = 'test/fixtures/xruby'
assert_equal %w(alpha beta), GeneratorCases.available(track_path).sort
end

def test_available_returns_exercise_names
track_path = 'test/fixtures/xruby'
Dir.stub :[], %w(/alpha_cases.rb hy_phen_ated_cases.rb) do
assert_equal %w(alpha hy-phen-ated), GeneratorCases.available(track_path)
end
end

def test_filename
exercise_name = 'two-parter'
assert_equal 'two_parter_cases', GeneratorCases.filename(exercise_name)
end

def test_class_name
assert_equal 'TwoParterCase', GeneratorCases.class_name('two-parter')
end
end
end
end
42 changes: 14 additions & 28 deletions test/generator/files/track_files_test.rb
Expand Up @@ -2,34 +2,6 @@

module Generator
module Files
class GeneratorCasesTest < Minitest::Test
def test_no_cases_found
track_path = 'test/fixtures/nonexistant'
assert_equal [], GeneratorCases.available(track_path)
end

def test_cases_found
track_path = 'test/fixtures/xruby'
assert_equal %w(alpha beta), GeneratorCases.available(track_path).sort
end

def test_available_returns_exercise_names
track_path = 'test/fixtures/xruby'
Dir.stub :[], %w(/alpha_cases.rb hy_phen_ated_cases.rb) do
assert_equal %w(alpha hy-phen-ated), GeneratorCases.available(track_path)
end
end

def test_filename
exercise_name = 'two-parter'
assert_equal 'two_parter_cases', GeneratorCases.filename(exercise_name)
end

def test_class_name
assert_equal 'TwoParterCase', GeneratorCases.class_name('two-parter')
end
end

class TrackFilesTest < Minitest::Test
FixturePaths = Paths.new(
metadata: 'test/fixtures/metadata',
Expand Down Expand Up @@ -64,6 +36,20 @@ def test_tests_template
subject = TestTrackFiles.new
assert_instance_of TestsTemplateFile, subject.tests_template
end

class TestTrackFilesUseDefault
def initialize
@paths = FixturePaths
@exercise_name = 'notemplate'
end
attr_reader :paths, :exercise_name
include TrackFiles
end

def test_default_tests_template
subject = TestTrackFiles.new
assert_instance_of TestsTemplateFile, subject.tests_template
end
end

class TestsVersionFileTest < Minitest::Test
Expand Down

0 comments on commit faa3286

Please sign in to comment.