diff --git a/.github/workflows/generator-tests.yml b/.github/workflows/generator-tests.yml new file mode 100644 index 0000000000..e377705844 --- /dev/null +++ b/.github/workflows/generator-tests.yml @@ -0,0 +1,33 @@ +name: GeneratorTests +on: + pull_request: + push: + branches: + - main + schedule: + # Weekly. + - cron: '0 0 * * 0' + +jobs: + test-generator-templates: + name: Test Generator + runs-on: ubuntu-22.04 + steps: + - name: Set up Ruby + uses: ruby/setup-ruby@52753b7da854d5c07df37391a986c76ab4615999 + with: + ruby-version: "3.3" + bundler-cache: true + run: ruby ./bin/generator.rb --verify + test-generator: + name: Check Generator Templates + runs-on: ubuntu-22.04 + container: + image: crystallang/crystal + steps: + - name: Set up Ruby + uses: ruby/setup-ruby@52753b7da854d5c07df37391a986c76ab4615999 + with: + ruby-version: "3.3" + bundler-cache: true + run: rake test:generator \ No newline at end of file diff --git a/Gemfile b/Gemfile index 6ec775621a..05b472f547 100644 --- a/Gemfile +++ b/Gemfile @@ -8,3 +8,4 @@ gem 'rubocop-minitest', require: false gem 'rubocop-rake', require: false gem 'simplecov', require: false gem 'racc', require: false +gem 'toml-rb', require: false diff --git a/Rakefile b/Rakefile index 0c18496369..fe00a092b5 100644 --- a/Rakefile +++ b/Rakefile @@ -25,5 +25,10 @@ namespace :test do task.pattern = 'test/**/*_test.rb' end + Rake::TestTask.new :generator do |task| + task.options = flags + task.pattern = 'generatorv2/test/**/*_test.rb' + end + ExerciseTestTasks.new options: flags end diff --git a/bin/generate.rb b/bin/generate.rb index 6beb27e0ad..824ec1e8b1 100644 --- a/bin/generate.rb +++ b/bin/generate.rb @@ -1,4 +1,5 @@ require 'optparse' +require 'tempfile' require_relative '../generatorv2/lib/generator' parser = OptionParser.new @@ -23,8 +24,13 @@ parser.on('--verify', 'Verify all exercises') do exercises = Dir.entries('./exercises/practice').select { |f| File.directory? File.join('./exercises/practice', f) } exercises.each do |exercise| - puts "Verifying #{exercise}" - system("ruby ./exercises/practice/#{exercise}/#{exercise}__test.rb") + if File.exist?("./exercises/practice/#{exercise}/.meta/test_template.erb") + current_code = File.read("./exercises/practice/#{exercise}/#{exercise}_test.rb") + f = Tempfile.create + Generator.new(exercise).generate(f.path) + generated_code = f.read + raise RuntimeError.new("The result generated for: #{exercise}, doesnt match the current file") if current_code != generated_code + end end end diff --git a/exercises/practice/acronym/acronym_test.rb b/exercises/practice/acronym/acronym_test.rb index 15c65a69b6..2acf62c9bb 100644 --- a/exercises/practice/acronym/acronym_test.rb +++ b/exercises/practice/acronym/acronym_test.rb @@ -30,7 +30,7 @@ def test_punctuation_without_whitespace def test_very_long_abbreviation skip assert_equal 'ROTFLSHTMDCOALM', - Acronym.abbreviate('Rolling On The Floor Laughing So Hard That My Dogs Came Over And Licked Me') + Acronym.abbreviate('Rolling On The Floor Laughing So Hard That My Dogs Came Over And Licked Me') end def test_consecutive_delimiters diff --git a/generatorv2/README.md b/generatorv2/README.md new file mode 100644 index 0000000000..4707dc43ef --- /dev/null +++ b/generatorv2/README.md @@ -0,0 +1,139 @@ +# Generator + +Last Updated: 2024/11/9 + +The generator is a powerful tool that can be used to generate tests for exercises based on the canonical data. +The generator is written in Ruby and is located in the `bin` directory. + +## How to use the generator + +### Things to do before running the generator + +Before running the generator you have to make sure a couple of files are in place. + +1. `tests.toml` file + +It is located under the `.meta` folder for each exercise. +The toml file is used to configure which exercises are generated and which are not. +Since the generator grabs all the data from the canonical data, so does this enable new tests that won't automatically be merged in. +Instead so does new tests have to be added to the toml file before they show up in the test file. + +If there is a test that isn't needed or something that doesn't fit Ruby you can remove it from the toml file. +By writing after the test name `include = false` and it will be skipped when generating the test file. + +2. `config.json` file, located in the root of the track + +The generator makes sure that the exercise is in the config.json so you need to add it there before running the generator. + +3. `spec` directory + +The generator will create a spec file for each exercise, so you need to make sure that the spec directory is in place. +Although there don't have to be any files in the directory, since the script will create one for you. +If it is a file already so will the generator overwrite it. + +#### Things to note + +The script which grabs info from the toml file is quite sensitive, writing the toml file in an incorrect way can brick the generator. + +Here are some examples of how you should **NOT** work with the toml file. + +Make sure that the uuid is the only thing inside of `[uuid]`, if there is, for example, an extra space so would that break it. +Here is an example + +```toml +# This would break it since it is an extra space between uuid and `]` +[1e22cceb-c5e4-4562-9afe-aef07ad1eaf4 ] +# This would break it since it is an extra space between uuid and `[` +[ 1e22cceb-c5e4-4562-9afe-aef07ad1eaf4] +``` + +The script won't care if you write `include = true` since if it sees the uuid it will always take it as long as `include = false` is not written. +The script will not work if anything is misspelled, although the part which gets `include = false` doesn't care if it gets an extra space or not. + +**NOTE:** +You are also **NOT** allowed to write `include = false` more than once after each uuid. +Since that can lead to errors in the generator. + +Bad way: + +```toml +[1e22cceb-c5e4-4562-9afe-aef07ad1eaf4] +description = "basic" +include = false +include = false +``` + +Good way: + +```toml +[1e22cceb-c5e4-4562-9afe-aef07ad1eaf4] +description = "basic" +include = false +``` + +### Template + +The generator uses a template file to generate the test file. +The template is located under the `.meta` for each exercise. + +This template has to be manually written for each exercise. +The goal although is to make it so that you only have to write the template once and then it will be able to be used to generate new tests. + +The template file is written in [Embedded Ruby(ERB)][erb]. +ERB enables you to write Ruby code inside of the template file. +It also means that the templates can be highly customizable since you can write any Ruby code you want. + +When writing the template file it is recommended to look at already existing template files to get a better understanding of how it works. +The template is getting a slightly modified version of the canonical data, so you can check out the [canonical data][canonical data] to see the data structure. +The modification is that the cases which are not included in the toml file will be removed from the data structure. + +When writing the template so is it a special tool that can help with giving `# skip` and `skip` tags for tests. +You simply have to call the `status` method. +It will return either `# skip` or `skip` depending on if it is the first test case or not. + +Here is an example: + +``` +<%= status()%> +<%= status()%> +<%= status()%> +``` + +result: + +``` +# skip +skip +skip +``` + +### The Test Generator + +If all the earlier steps are done so can you run the generator. +To run the generator you need to have a working Ruby installation and installed all gems in the Gemfile. +The generator is located in the `bin` directory and is called `generator.rb`. + +To run the generator so do you have to be in the root directory and run the following command: + +```shell +ruby ./bin/generator.rb -e +``` + +Where `` is the same name as the slug name which is located in the `config.json` file. + +For more commands so can you run the following command: + +```shell +ruby ./bin/generator.rb --help +``` + +### Errors and warnings + +The generator will give you errors and warnings if something is wrong. +That includes if the exercise is not in the `config.json` file, if the exercise is not in the toml file, or if the template file is missing. +Also if it has a problem getting the `canonical-data.json` file so will it give you an error. +The generator also uses a formatter which will give you errors if the generated file is not formatted correctly. +The file will still be generated even if formatter gives errors, therefore can you check the file and see what is wrong and fix it in the template. + +[erb]: https://docs.ruby-lang.org/en/master/ERB.html +[canonical data]: https://github.com/exercism/problem-specifications diff --git a/generatorv2/lib/generator.rb b/generatorv2/lib/generator.rb index 0dc2e59854..4f3fb15f2d 100644 --- a/generatorv2/lib/generator.rb +++ b/generatorv2/lib/generator.rb @@ -11,7 +11,7 @@ def initialize(exercise = nil) @exercise = exercise end - def generate + def generate(result_path = "./exercises/practice/#{@exercise}/#{@exercise}_test.rb") json = get_remote_files uuid = toml("./exercises/practice/#{@exercise}/.meta/tests.toml") additional_json(json) @@ -23,14 +23,14 @@ def generate result = template.result(binding) - File.write("./exercises/practice/#{@exercise}/#{@exercise}_test.rb", result) + File.write(result_path, result) cli = RuboCop::CLI.new - cli.run(['-x', "--force-default-config", "exercises/practice/#{@exercise}/#{@exercise}_test.rb"]) + cli.run(['-x', "--force-default-config", "-o", "/dev/null", result_path]) end def underscore(str) str.each_char.reduce("") do |acc, x| - acc << if x == ' ' + acc << if [' ', '-'].include?(x) '_' else x.downcase @@ -40,7 +40,7 @@ def underscore(str) end def camel_case(str) - str.split('_').map(&:capitalize).join + str.split(/[-_]/).map(&:capitalize).join end def status @@ -56,7 +56,7 @@ def toml(path = "./exercises/practice/#{@exercise}/.meta/tests.toml") uuid = TomlRB.load_file(path) uuid.filter do |_k, v| - !v.any? { |k, v| k == "include" && !v } + v.none? { |k, v| k == "include" && !v } end.map { |k, _v| k } end @@ -99,5 +99,3 @@ def remove_tests(uuid, json) end end end - -# Generator.new("acronym").get_remote_files diff --git a/generatorv2/test/toml_test.rb b/generatorv2/test/toml_test.rb index 7f3c69c458..8eff369c08 100644 --- a/generatorv2/test/toml_test.rb +++ b/generatorv2/test/toml_test.rb @@ -4,15 +4,15 @@ class GeneratorTest < Minitest::Test def test_importning_toml assert_equal %w[1e22cceb-c5e4-4562-9afe-aef07ad1eaf4 79ae3889-a5c0-4b01-baf0-232d31180c08 ec7000a7-3931-4a17-890e-33ca2073a548 32dd261c-0c92-469a-9c5c-b192e94a63b0 ae2ac9fa-a606-4d05-8244-3bcc4659c1d4 0e4b1e7c-1a6d-48fb-81a7-bf65eb9e69f9 6a078f49-c68d-4b7b-89af-33a1a98c28cc], - Generator.new("two_fer").toml("./test/misc/tests.toml") + Generator.new("two_fer").toml("generatorv2/test/misc/tests.toml") end def test_importing_toml_with_no_include assert_equal %w[1e22cceb-c5e4-4562-9afe-aef07ad1eaf4 79ae3889-a5c0-4b01-baf0-232d31180c08 ec7000a7-3931-4a17-890e-33ca2073a548 32dd261c-0c92-469a-9c5c-b192e94a63b0 ae2ac9fa-a606-4d05-8244-3bcc4659c1d4 6a078f49-c68d-4b7b-89af-33a1a98c28cc], - Generator.new("two_fer").toml("./test/misc/tests_no_include.toml") + Generator.new("two_fer").toml("generatorv2/test/misc/tests_no_include.toml") end def test_importing_toml_with_all_excluded - assert_empty Generator.new("two_fer").toml("./test/misc/tests_all_excluded.toml") + assert_empty Generator.new("two_fer").toml("generatorv2/test/misc/tests_all_excluded.toml") end end diff --git a/generatorv2/test/utils_test.rb b/generatorv2/test/utils_test.rb new file mode 100644 index 0000000000..7590322f42 --- /dev/null +++ b/generatorv2/test/utils_test.rb @@ -0,0 +1,36 @@ +require_relative '../lib/generator' +require 'minitest/autorun' + +class UtilTest < Minitest::Test + def test_camelize + assert_equal "Acronym", + Generator.new("acronym").camel_case("acronym") + end + + def test_camelize_with_two_words + assert_equal "TwoFer", + Generator.new("two-fer").camel_case("two-fer") + end + + def test_underscore + assert_equal "acronym", + Generator.new("acronym").underscore("acronym") + end + + def test_underscore_with_two_words + assert_equal "two_fer", + Generator.new("two-fer").underscore("two-fer") + end + + def test_status + assert_equal "# skip", + Generator.new("acronym").status + end + + def test_status_after_first + generator = Generator.new("acronym") + generator.status + assert_equal "skip", + generator.status + end +end