-
-
Notifications
You must be signed in to change notification settings - Fork 518
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Impliment new template based generator #1730
base: main
Are you sure you want to change the base?
Changes from 21 commits
77a991b
039911c
8e3afbe
d278534
5c27b41
b1f131f
7004988
067df32
653f629
4960f96
39bf79b
7fb52c5
e1c69f4
79b00d1
f806548
bbad039
b6d2e9e
98d267d
68d4671
76504b5
00af4d0
492b44b
f40f4c4
769dee3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
name: GeneratorTests | ||
|
||
on: | ||
workflow_dispatch: | ||
push: | ||
branches: [main] | ||
pull_request: | ||
branches: [main] | ||
|
||
jobs: | ||
test-generator-templates: | ||
name: Check Generator Templates | ||
runs-on: ubuntu-22.04 | ||
steps: | ||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 | ||
- name: Set up Ruby | ||
uses: ruby/setup-ruby@52753b7da854d5c07df37391a986c76ab4615999 | ||
with: | ||
ruby-version: "3.3" | ||
bundler-cache: true | ||
- name: Verify templates | ||
run: bundle exec ./bin/generate --verify | ||
test-generator: | ||
name: Test Generator | ||
runs-on: ubuntu-22.04 | ||
steps: | ||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 | ||
- name: Set up Ruby | ||
uses: ruby/setup-ruby@52753b7da854d5c07df37391a986c76ab4615999 | ||
with: | ||
ruby-version: "3.3" | ||
bundler-cache: true | ||
- name: Run tests | ||
run: bundle exec rake test:generator |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
#!/usr/bin/env ruby | ||
require 'optparse' | ||
require 'tempfile' | ||
require_relative '../generatorv2/lib/generator' | ||
|
||
parser = OptionParser.new | ||
|
||
parser.on('-v', '--version', 'Print the version') do | ||
puts '0.1.0' | ||
end | ||
|
||
parser.on('-h', '--help', 'Prints help') do | ||
puts parser | ||
end | ||
|
||
parser.on('-a', '--all', 'Generate all exercises') do | ||
exercises = Dir.entries('./exercises/practice').select { |f| File.directory? File.join('./exercises/practice', f) } | ||
exercises.each do |exercise| | ||
if File.exist?("./exercises/practice/#{exercise}/.meta/test_template.erb") | ||
Generator.new(exercise).generate | ||
end | ||
end | ||
end | ||
|
||
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| | ||
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 | ||
|
||
parser.on('-e', '--exercise EXERCISE', 'The exercise to generate') do |exercise| | ||
Generator.new(exercise).generate | ||
end | ||
|
||
parser.parse! |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
require 'minitest/autorun' | ||
require_relative 'acronym' | ||
|
||
class AcronymTest < Minitest::Test | ||
<% json["cases"].each do |cases| %> | ||
def test_<%= underscore(cases["description"]) %> | ||
<%= status() %> | ||
assert_equal '<%= cases["expected"] %>', <%= camel_case(json["exercise"]) %>.<%= underscore(cases["property"]) %>('<%= cases["input"]["phrase"] %>') | ||
end | ||
<% end %> | ||
end | ||
|
||
|
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,134 @@ | ||||||
# 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 | ||||||
|
||||||
meatball133 marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
Run `bundle install` to install the required libraries. | ||||||
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. | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
In case we move from TOML to another type of configuration file, this reference will not need to change in the documentation. |
||||||
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. | ||||||
|
||||||
#### 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. | ||||||
kotp marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
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. | ||||||
kotp marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
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 | ||||||
bundle exec ./bin/generate -e <exercise_slug> | ||||||
``` | ||||||
|
||||||
Where `<exercise_slug>` 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 | ||||||
bundle exec ./bin/generate --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 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
require 'toml-rb' | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What problems or solutions are we avoiding/gaining by using TOML as opposed to YAML (or even JSON)? This is the point at which TOML as a tool and dependency is being introduced, and its associated maintenance cost, so we should evaluate that. We should have a net positive effect by bringing this in. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The I wrote a manual parser for the Crystal generator (but it is likely not as good of an implementation) because I couldn't find any good shards (library). This library should be fine, the toml specification isn't changing, but we could have our own solution. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The goal is more or less with the toml files is that, if someone adds extra test cases or reimplements test cases for a certain exercise will that not affect the generation process until you sync the exercise. Meaning the ci won't fail in the majority of cases if not all exercises are synced. |
||
require 'net/http' | ||
require 'uri' | ||
require 'json' | ||
require 'erb' | ||
require 'rubocop' | ||
require_relative 'utils' | ||
|
||
class Generator | ||
include Utils | ||
|
||
def initialize(exercise = nil) | ||
@first = true | ||
@exercise = exercise | ||
end | ||
|
||
def generate(result_path = "./exercises/practice/#{@exercise}/#{@exercise}_test.rb") | ||
json = remote_files | ||
uuid = toml("./exercises/practice/#{@exercise}/.meta/tests.toml") | ||
additional_json(json) | ||
json["cases"] = remove_tests(uuid, json) | ||
status = proc { status } | ||
camel_case = proc { |str| camel_case(str) } | ||
underscore = proc { |str| underscore(str) } | ||
template = ERB.new File.read("./exercises/practice/#{@exercise}/.meta/test_template.erb") | ||
|
||
result = template.result(binding) | ||
|
||
File.write(result_path, result) | ||
cli = RuboCop::CLI.new | ||
cli.run(['-x', "-c", ".rubocop.yml", "-o", "/dev/null", result_path]) | ||
end | ||
|
||
def underscore(str) | ||
str.gsub(/[-\s]/, '_').downcase | ||
end | ||
|
||
def camel_case(str) | ||
str.split(/[-_]/).map(&:capitalize).join | ||
end | ||
|
||
def status | ||
if @first | ||
@first = false | ||
return "# skip" | ||
end | ||
"skip" | ||
end | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Typographical error (missing apostrophe) and preference of "positive conditional statement".
Prefer using
unless positive_conditional_statement
rather thanif negative
, this can keep all (or at least most) of our conditional statements positive.We should also consider if
RuntimeError
is the best error here. It could beVerificationError
instead, which would be more specific.We might also
fail
instead ofraise
for the communication that this is purposefully failed here, given the right conditions.