diff --git a/lib/generator.rb b/lib/generator.rb index ada326d026..d14497448f 100644 --- a/lib/generator.rb +++ b/lib/generator.rb @@ -15,7 +15,7 @@ def initialize(track:, metadata:) # Doesn't update the version information. class GenerateTests < ImplementationDelegator def call - create_tests_file + build_tests end end @@ -24,7 +24,7 @@ class UpdateVersionAndGenerateTests < ImplementationDelegator def call update_tests_version update_example_solution - create_tests_file + build_tests end end end diff --git a/lib/generator/command_line.rb b/lib/generator/command_line.rb index aff48ba98b..07f7b18fd2 100644 --- a/lib/generator/command_line.rb +++ b/lib/generator/command_line.rb @@ -16,7 +16,7 @@ def parse(args) attr_reader :paths def generators - implementations.map { |slug| generator(implementation(slug)) } + implementations.map { |slug| generator(implementation(exercise(slug))) } end def implementations @@ -35,13 +35,21 @@ def freeze? @options[:freeze] || @options[:all] end - def implementation(slug) + def exercise(slug) + Exercise.new(slug: slug) + end + + def implementation(exercise) LoggingImplementation.new( - implementation: Implementation.new(paths: paths, slug: slug), + implementation: Implementation.new(exercise: exercise, repository: repository(exercise)), logger: logger ) end + def repository(exercise) + Repository.new(paths: paths, exercise: exercise) + end + def logger logger = Logger.new($stdout) logger.formatter = proc { |_severity, _datetime, _progname, msg| "#{msg}\n" } diff --git a/lib/generator/exercise.rb b/lib/generator/exercise.rb new file mode 100644 index 0000000000..e858a12d1e --- /dev/null +++ b/lib/generator/exercise.rb @@ -0,0 +1,19 @@ +module Generator + class Exercise + using Generator::Underscore + + attr_reader :slug + + def initialize(slug:) + @slug = slug + end + + def name + @name ||= slug.underscore + end + + def case_class + slug.camel_case + 'Case' + end + end +end diff --git a/lib/generator/files/generator_cases.rb b/lib/generator/files/generator_cases.rb index b60d737a2a..6955b173f1 100644 --- a/lib/generator/files/generator_cases.rb +++ b/lib/generator/files/generator_cases.rb @@ -3,36 +3,15 @@ module Files module GeneratorCases class << self def available(track_path) - cases_filepaths(track_path).map { |filepath| slugify(filepath) }.sort - end - - def class_name(exercise_name_or_slug) - filename(exercise_name_or_slug).split('_').map(&:capitalize).join - end - - def source_filepath(track_path, slug) - path = meta_generator_path(track_path, slug) - filename = filename(slug) + '.rb' - File.join(path, filename) + filepaths(track_path).map do |filepath| + %r{#{track_path}/exercises/([-a-z]+)/}.match(filepath)[1] + end.sort end private - def cases_filepaths(track_path) - generator_glob = File.join(meta_generator_path(track_path, '*'), '*_case.rb') - Dir.glob(generator_glob, File::FNM_DOTMATCH) - end - - def slugify(filepath) - File.basename(filepath, '_case.rb').tr('_', '-') - end - - def filename(exercise_name_or_slug) - "#{exercise_name_or_slug.tr('-', '_')}_case" - end - - def meta_generator_path(track_path, slug) - File.join(track_path, 'exercises', slug, '.meta', 'generator') + def filepaths(track_path) + Dir.glob(File.join(track_path, 'exercises', '*', '.meta', 'generator')) end end end diff --git a/lib/generator/files/metadata_files.rb b/lib/generator/files/metadata_files.rb index 995b27a415..75e9db7f84 100644 --- a/lib/generator/files/metadata_files.rb +++ b/lib/generator/files/metadata_files.rb @@ -10,7 +10,7 @@ def canonical_data private def exercise_metadata_path - File.join(paths.metadata, 'exercises', slug) + File.join(paths.metadata, 'exercises', exercise.slug) end end diff --git a/lib/generator/files/track_files.rb b/lib/generator/files/track_files.rb index e7871b8cc4..a08032f280 100644 --- a/lib/generator/files/track_files.rb +++ b/lib/generator/files/track_files.rb @@ -15,6 +15,10 @@ def example_solution ExampleSolutionFile.new(filename: File.join(solutions_path, example_filename)) end + def test_case + TestCaseFile.new(filename: File.join(generator_path, case_filename)) + end + def tests_template TestsTemplateFile.new(filename: tests_template_absolute_filename) end @@ -22,7 +26,7 @@ def tests_template private def exercise_path - File.join(paths.track, 'exercises', slug) + File.join(paths.track, 'exercises', exercise.slug) end def meta_path @@ -33,8 +37,12 @@ def solutions_path File.join(meta_path, 'solutions') end + def generator_path + File.join(meta_path, 'generator') + end + def minitest_tests_filename - "#{exercise_name}_test.rb" + "#{exercise.name}_test.rb" end def version_filename @@ -42,7 +50,11 @@ def version_filename end def example_filename - "#{exercise_name}.rb" + "#{exercise.name}.rb" + end + + def case_filename + "#{exercise.name}_case.rb" end def tests_template_absolute_filename @@ -89,5 +101,8 @@ def generate(template:, values:) class TestsTemplateFile < Readable end + + class TestCaseFile < Readable + end end end diff --git a/lib/generator/implementation.rb b/lib/generator/implementation.rb index e94e62c80f..3732784e4b 100644 --- a/lib/generator/implementation.rb +++ b/lib/generator/implementation.rb @@ -1,17 +1,19 @@ require 'delegate' +require 'forwardable' module Generator class Implementation - include Files::TrackFiles - include Files::MetadataFiles include TemplateValuesFactory + extend Forwardable - def initialize(paths:, slug:) - @paths = paths - @slug = slug + def initialize(exercise:, repository:) + @exercise = exercise + @repository = repository end - attr_reader :paths, :slug + attr_reader :exercise, :repository + def_delegators :@repository, :tests_version, :example_solution, :minitest_tests, + :tests_template, :canonical_data, :test_case def version tests_version.to_i @@ -25,16 +27,12 @@ def update_example_solution example_solution.update_version(version) end - def create_tests_file + def build_tests minitest_tests.generate( template: tests_template.to_s, values: template_values ) end - - def exercise_name - @exercise_name ||= slug.tr('-', '_') - end end # This exists to give us a clue as to what we are delegating to. @@ -58,9 +56,9 @@ def update_example_solution @logger.debug "Updated version in example solution to #{version}" end - def create_tests_file - @implementation.create_tests_file - @logger.info "Generated #{slug} tests version #{version}" + def build_tests + @implementation.build_tests + @logger.info "Generated #{exercise.slug} tests version #{version}" end end end diff --git a/lib/generator/repository.rb b/lib/generator/repository.rb new file mode 100644 index 0000000000..84bf9986ed --- /dev/null +++ b/lib/generator/repository.rb @@ -0,0 +1,14 @@ +require 'delegate' + +module Generator + class Repository + include Files::TrackFiles + include Files::MetadataFiles + + def initialize(paths:, exercise:) + @paths = paths + @exercise = exercise + end + attr_reader :paths, :exercise + end +end diff --git a/lib/generator/template_values.rb b/lib/generator/template_values.rb index 74b298219a..0f8ea069ac 100644 --- a/lib/generator/template_values.rb +++ b/lib/generator/template_values.rb @@ -1,55 +1,63 @@ module Generator # Contains methods accessible to the ERB template class TemplateValues - attr_reader :abbreviated_commit_hash, :version, :exercise_name, :test_cases, :canonical_data_version + using Underscore - def initialize(abbreviated_commit_hash:, version:, exercise_name:, test_cases:, canonical_data_version: nil) - @abbreviated_commit_hash = abbreviated_commit_hash + attr_reader :exercise, :version, :canonical_data, :test_cases + + def initialize(exercise:, version:, canonical_data:, test_cases:) + @exercise = exercise @version = version - @exercise_name = exercise_name + @canonical_data = canonical_data @test_cases = test_cases - @canonical_data_version = canonical_data_version end def get_binding binding end + def abbreviated_commit_hash + canonical_data.abbreviated_commit_hash + end + + def canonical_data_version + canonical_data.version + end + + def exercise_name + exercise.name + end + def exercise_name_camel - exercise_name.split('_').map(&:capitalize).join + exercise.name.camel_case end end module TemplateValuesFactory def template_values TemplateValues.new( - abbreviated_commit_hash: canonical_data.abbreviated_commit_hash, - canonical_data_version: canonical_data.version, + exercise: exercise, version: version, - exercise_name: slug_underscore, + canonical_data: canonical_data, test_cases: extract ) end private - def slug_underscore - slug ? slug.tr('-_', '_') : '' - end - def extract load cases_load_name extractor.cases(canonical_data.to_s) end def extractor - CaseValues::Extractor.new( - case_class: Object.const_get(Files::GeneratorCases.class_name(slug)) - ) + CaseValues::Extractor.new( + case_class: Object.const_get(exercise.case_class) + ) end def cases_load_name - Files::GeneratorCases.source_filepath(paths.track, slug) + test_case.filename end end end diff --git a/lib/generator/underscore.rb b/lib/generator/underscore.rb index 46d5a94559..dbffba8e20 100644 --- a/lib/generator/underscore.rb +++ b/lib/generator/underscore.rb @@ -4,6 +4,10 @@ module Underscore def underscore downcase.gsub(/[- ]/, '_').gsub(/[^\w?]/, '') end + + def camel_case + underscore.split('_').map(&:capitalize).join + end end end end diff --git a/test/fixtures/xruby/lib/generator/test_template.erb b/test/fixtures/xruby/lib/generator/test_template.erb index 0e90df5d8b..1beb65b1f1 100644 --- a/test/fixtures/xruby/lib/generator/test_template.erb +++ b/test/fixtures/xruby/lib/generator/test_template.erb @@ -1,9 +1,9 @@ #!/usr/bin/env ruby require 'minitest/autorun' -require_relative 'acronym' +require_relative '<%= exercise_name %>' -# Common test data version: <%= abbreviated_commit_hash %> -class AcronymTest < Minitest::Test +# Common test data version: <%= canonical_data_version %> <%= abbreviated_commit_hash %> +class <%= exercise_name_camel %>Test < Minitest::Test <% test_cases.each_with_index do |test_case, idx| %> def <%= test_case.name %> <%= test_case.skipped(idx) %> diff --git a/test/generator/exercise_test.rb b/test/generator/exercise_test.rb new file mode 100644 index 0000000000..37ad555991 --- /dev/null +++ b/test/generator/exercise_test.rb @@ -0,0 +1,20 @@ +require_relative '../test_helper' + +module Generator + class ExerciseTest < Minitest::Test + def test_slug + exercise = Exercise.new(slug: 'alpha') + assert_equal 'alpha', exercise.slug + end + + def test_name + exercise = Exercise.new(slug: 'alpha-beta') + assert_equal 'alpha_beta', exercise.name + end + + def test_case_class + exercise = Exercise.new(slug: 'alpha-beta') + assert_equal 'AlphaBetaCase', exercise.case_class + end + end +end diff --git a/test/generator/files/generate_cases_test.rb b/test/generator/files/generate_cases_test.rb index ae5882673c..ccabcc1040 100644 --- a/test/generator/files/generate_cases_test.rb +++ b/test/generator/files/generate_cases_test.rb @@ -5,33 +5,22 @@ module Files class GeneratorCasesTest < Minitest::Test def test_available track_path = '/track' - fake_filenames = %w(/track/zzz/alpha_case.rb /track/aaa/hy_phen_ated_case.rb) - Dir.stub :glob, fake_filenames do + fake_filepaths = %w(/track/exercises/alpha/zzz /track/exercises/hy-phen-ated/yyy) + Dir.stub :glob, fake_filepaths do assert_equal %w(alpha hy-phen-ated), GeneratorCases.available(track_path) end end def test_available_calls_glob_with_the_right_arguments track_path = '/track' - expected_glob = "#{track_path}/exercises/*/.meta/generator/*_case.rb" + expected_glob = "#{track_path}/exercises/*/.meta/generator" mock_glob_call = Minitest::Mock.new - mock_glob_call.expect :call, [], [expected_glob, File::FNM_DOTMATCH] + mock_glob_call.expect :call, [], [expected_glob] Dir.stub :glob, mock_glob_call do GeneratorCases.available(track_path) end mock_glob_call.verify end - - def test_class_name - assert_equal 'TwoParterCase', GeneratorCases.class_name('two-parter') - end - - def test_source_filepath - track_path = '/track' - slug = 'slug' - expected_filename = track_path + '/exercises/slug/.meta/generator/slug_case.rb' - assert_equal expected_filename, GeneratorCases.source_filepath(track_path, slug) - end end end end diff --git a/test/generator/files/metadata_files_test.rb b/test/generator/files/metadata_files_test.rb index 82e3682f8c..3c3e205fa8 100644 --- a/test/generator/files/metadata_files_test.rb +++ b/test/generator/files/metadata_files_test.rb @@ -10,9 +10,9 @@ class MetadataFilesTest < Minitest::Test class TestMetadataFiles def initialize @paths = FixturePaths - @slug = 'alpha' + @exercise = Exercise.new(slug: 'alpha') end - attr_reader :paths, :slug + attr_reader :paths, :exercise include MetadataFiles end diff --git a/test/generator/files/track_files_test.rb b/test/generator/files/track_files_test.rb index 53c97e1de7..fec087afcb 100644 --- a/test/generator/files/track_files_test.rb +++ b/test/generator/files/track_files_test.rb @@ -11,10 +11,9 @@ class TrackFilesTest < Minitest::Test class TestTrackFiles def initialize @paths = FixturePaths - @slug = 'alpha-beta' - @exercise_name = 'alpha_beta' + @exercise = Exercise.new(slug: 'alpha-beta') end - attr_accessor :paths, :slug, :exercise_name + attr_reader :paths, :exercise include TrackFiles end @@ -34,6 +33,12 @@ def test_minitest_tests assert_instance_of MinitestTestsFile, subject.minitest_tests end + def test_test_case + subject = TestTrackFiles.new + expected_filename = FixturePaths.track + '/exercises/alpha-beta/.meta/generator/alpha_beta_case.rb' + assert_equal expected_filename, subject.test_case.filename + end + def test_tests_template subject = TestTrackFiles.new expected_filename = FixturePaths.track + '/exercises/alpha-beta/.meta/generator/test_template.erb' @@ -44,9 +49,9 @@ def test_tests_template class TestTrackFilesUseDefault def initialize @paths = FixturePaths - @slug = 'notemplate' + @exercise = Exercise.new(slug: 'notemplate') end - attr_reader :paths, :slug + attr_reader :paths, :exercise include TrackFiles end diff --git a/test/generator/implementation_test.rb b/test/generator/implementation_test.rb index 828db9615d..cf27fd0c1e 100644 --- a/test/generator/implementation_test.rb +++ b/test/generator/implementation_test.rb @@ -2,26 +2,30 @@ module Generator class ImplementationTest < Minitest::Test + # I would love to be mocking these tests, but Minitest::Mock does not play well with + # Forwardable. As we'll be refactoring out the includes in Repository soon, doesn't + # seem worth the fight. FixturePaths = Paths.new( metadata: 'test/fixtures/metadata', track: 'test/fixtures/xruby' ) def test_version - subject = Implementation.new(paths: FixturePaths, slug: 'alpha') + exercise = Minitest::Mock.new.expect :slug, 'alpha' + repository = Repository.new(paths: FixturePaths, exercise: exercise) + subject = Implementation.new(exercise: exercise, repository: repository) assert_equal 1, subject.version end - def test_slug - subject = Implementation.new(paths: FixturePaths, slug: 'alpha') - assert_equal 'alpha', subject.slug - end - def test_update_tests_version + # Minitest::Mock doesn't document how to allow the mocked method to be called twice + exercise = Exercise.new(slug: 'alpha') + repository = Repository.new(paths: FixturePaths, exercise: exercise) + subject = Implementation.new(exercise: exercise, repository: repository) + assert_equal 1, subject.tests_version.to_i + mock_file = Minitest::Mock.new.expect :write, '2'.length, [2] - subject = Implementation.new(paths: FixturePaths, slug: 'alpha') # Verify iniital condition from fixture file - assert_equal 1, subject.tests_version.to_i File.stub(:open, true, mock_file) do assert_equal 2, subject.update_tests_version end @@ -29,16 +33,23 @@ def test_update_tests_version end def test_update_example_solution + exercise = Exercise.new(slug: 'alpha') + repository = Repository.new(paths: FixturePaths, exercise: exercise) + subject = Implementation.new(exercise: exercise, repository: repository) + expected_content = "# This is the example\n\nclass BookKeeping\n VERSION = 1\nend\n" mock_file = Minitest::Mock.new.expect :write, expected_content.length, [expected_content] - subject = Implementation.new(paths: FixturePaths, slug: 'alpha') File.stub(:open, true, mock_file) do assert_equal expected_content, subject.update_example_solution end mock_file.verify end - def test_create_tests_file + def test_build_tests + exercise = Exercise.new(slug: 'alpha') + repository = Repository.new(paths: FixturePaths, exercise: exercise) + subject = Implementation.new(exercise: exercise, repository: repository) + # Q: Is the pain here caused by: # a) Implementation `including` everything rather than using composition? # b) Trying to verify the expected content. @@ -84,34 +95,29 @@ def test_bookkeeping end TESTS_FILE mock_file = Minitest::Mock.new.expect :write, expected_content.length, [expected_content] - subject = Implementation.new(paths: FixturePaths, slug: 'alpha') GitCommand.stub(:abbreviated_commit_hash, '123456789') do File.stub(:open, true, mock_file) do - assert_equal expected_content, subject.create_tests_file + assert_equal expected_content, subject.build_tests end end mock_file.verify # Don't pollute the namespace Object.send(:remove_const, :AlphaCase) end - - def test_exercise_name - subject = Implementation.new(paths: FixturePaths, slug: 'alpha-beta') - assert_equal 'alpha_beta', subject.exercise_name - end end class LoggingImplementationTest < Minitest::Test - def test_create_tests_file + def test_build_tests + exercise = Exercise.new(slug: 'alpha') mock_implementation = Minitest::Mock.new - mock_implementation.expect :create_tests_file, nil - mock_implementation.expect :slug, 'alpha' + mock_implementation.expect :build_tests, nil + mock_implementation.expect :exercise, exercise mock_implementation.expect :version, 2 mock_logger = Minitest::Mock.new mock_logger.expect :info, nil, ['Generated alpha tests version 2'] subject = LoggingImplementation.new(implementation: mock_implementation, logger: mock_logger) - subject.create_tests_file + subject.build_tests mock_implementation.verify end diff --git a/test/generator/template_values_test.rb b/test/generator/template_values_test.rb index ee179cc28b..38b69be700 100644 --- a/test/generator/template_values_test.rb +++ b/test/generator/template_values_test.rb @@ -4,40 +4,36 @@ module Generator class TestTemplateValuesTest < Minitest::Test def setup @arguments = { - abbreviated_commit_hash: nil, version: nil, exercise_name: nil, test_cases: nil + exercise: nil, version: nil, canonical_data: nil, test_cases: nil } end def test_abbreviated_commit_hash expected_abbreviated_commit_hash = '1234567' - subject = TemplateValues.new(@arguments.merge(abbreviated_commit_hash: expected_abbreviated_commit_hash)) + mock_canonical_data = Minitest::Mock.new.expect :abbreviated_commit_hash, expected_abbreviated_commit_hash + subject = TemplateValues.new(@arguments.merge(canonical_data: mock_canonical_data)) assert_equal expected_abbreviated_commit_hash, subject.abbreviated_commit_hash end - def test_version - expected_version = '1234567' - subject = TemplateValues.new(@arguments.merge(version: expected_version)) - assert_equal expected_version, subject.version + def test_canonical_data_version + expected_canonical_data_version = '0.1.0' + mock_canonical_data = Minitest::Mock.new.expect :version, expected_canonical_data_version + subject = TemplateValues.new(@arguments.merge(canonical_data: mock_canonical_data)) + assert_equal expected_canonical_data_version, subject.canonical_data_version end def test_exercise_name expected_exercise_name = 'alpha_beta' - subject = TemplateValues.new(@arguments.merge(exercise_name: expected_exercise_name)) + subject = TemplateValues.new(@arguments.merge(exercise: Exercise.new(slug: 'alpha-beta'))) assert_equal expected_exercise_name, subject.exercise_name end def test_exercise_name_camel expected_exercise_name_camel = 'AlphaBeta' - subject = TemplateValues.new(@arguments.merge(exercise_name: 'alpha_beta')) + subject = TemplateValues.new(@arguments.merge(exercise: Exercise.new(slug: 'alpha_beta'))) assert_equal expected_exercise_name_camel, subject.exercise_name_camel end - def test_test_cases - expected_test_cases = 'should be TemplateValues class' - subject = TemplateValues.new(@arguments.merge(test_cases: expected_test_cases)) - assert_equal expected_test_cases, subject.test_cases - end - def test_get_binding subject = TemplateValues.new(@arguments) assert_instance_of Binding, subject.get_binding @@ -46,34 +42,8 @@ def test_get_binding class TemplateValuesFactoryTest < Minitest::Test class TestTemplateValuesFactory - def slug - 'alpha' - end - - def version - 2 - end - - def canonical_data - mock_canonical_data = Minitest::Mock.new - mock_canonical_data.expect :abbreviated_commit_hash, nil - mock_canonical_data.expect :version, '1.2.3' - mock_canonical_data.expect :to_s, '{"cases":[]}' - mock_canonical_data - end - - def paths - mock_paths = Minitest::Mock.new - mock_paths.expect :track, 'test/fixtures/xruby' - mock_paths - end - - include TemplateValuesFactory - end - - class ClassBasedTestTemplateValuesFactory - def slug - 'beta' + def exercise + Exercise.new(slug: 'alpha') end def version @@ -88,20 +58,13 @@ def canonical_data mock_canonical_data end - def paths - mock_paths = Minitest::Mock.new - mock_paths.expect :track, 'test/fixtures/xruby' - mock_paths + def test_case + Minitest::Mock.new.expect :filename, 'test/fixtures/xruby/exercises/alpha/.meta/generator/alpha_case.rb' end include TemplateValuesFactory end - def test_template_values_from_class - subject = ClassBasedTestTemplateValuesFactory.new - assert_instance_of TemplateValues, subject.template_values - end - def test_template_values_loads_problem_case_classes subject = TestTemplateValuesFactory.new assert_instance_of TemplateValues, subject.template_values diff --git a/test/generator/underscore_test.rb b/test/generator/underscore_test.rb index f5a753ea6e..b5862d6643 100644 --- a/test/generator/underscore_test.rb +++ b/test/generator/underscore_test.rb @@ -19,5 +19,9 @@ def test_question_mark 'unreadable_but_correctly_sized_inputs_return_?' ) end + + def test_camel_case + assert_equal 'ASlug', 'a-slug'.camel_case + end end end diff --git a/test/generator_test.rb b/test/generator_test.rb index 2959a4cb87..b376960afa 100644 --- a/test/generator_test.rb +++ b/test/generator_test.rb @@ -6,7 +6,7 @@ def test_call mock_exercise = Minitest::Mock.new mock_exercise.expect :update_tests_version, nil mock_exercise.expect :update_example_solution, nil - mock_exercise.expect :create_tests_file, nil + mock_exercise.expect :build_tests, nil subject = UpdateVersionAndGenerateTests.new(mock_exercise) subject.call @@ -18,7 +18,7 @@ def test_call class UpdateVersionAndGenerateTestsFrozenVersionTest < Minitest::Test def test_call mock_exercise = Minitest::Mock.new - mock_exercise.expect :create_tests_file, nil + mock_exercise.expect :build_tests, nil subject = GenerateTests.new(mock_exercise) subject.call