Skip to content

Commit c877c19

Browse files
meatball133kotp
andauthored
Impliment new template based generator (#1730)
* Impliment basic prototype as proof of concept * Add ci and more tests * Update gemfile.lock to include toml-rb * Fix rubocop and fix ci * Format files * EOL for every line in text file * Generator now executable and changes based on feedback * Fix interpreter name * Add missing name key in ci file * Fix execution path of ci scripts * Remove Crystal image refernce and fixes to ci * Bump rubocop version and add missing actions checkout * Test adding bundle install * Test uppdating gemfile * Change to using `bundle exec` * Make the generate script use the same rubocop config as the repo * Test rollback to rubocop 1.50 * Update readme to reflect recent changes * Split utils methods into its own module * Breakout helper method and exception class * Verify now creates a file in exercise directory to get same formatting as the normal creation. Other changes based on feedback * Changes based on feedback and add more tasks to rakefile * Update bin/generate Co-authored-by: Victor Goff <keeperotphones+github@gmail.com> * Changes based on feedback * Update tests to reflect rename of `skip?` * Update test names * Add EOL --------- Co-authored-by: Victor Goff <keeperotphones+github@gmail.com> Co-authored-by: KOTP <keeperotphones@gmail.com>
1 parent 45bbcd7 commit c877c19

File tree

16 files changed

+531
-15
lines changed

16 files changed

+531
-15
lines changed

.github/workflows/generator-tests.yml

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
name: GeneratorTests
2+
3+
on:
4+
workflow_dispatch:
5+
push:
6+
branches: [main]
7+
pull_request:
8+
branches: [main]
9+
10+
jobs:
11+
test-generator-templates:
12+
name: Check Generator Templates
13+
runs-on: ubuntu-22.04
14+
steps:
15+
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
16+
- name: Set up Ruby
17+
uses: ruby/setup-ruby@52753b7da854d5c07df37391a986c76ab4615999
18+
with:
19+
ruby-version: "3.3"
20+
bundler-cache: true
21+
- name: Verify templates
22+
run: bundle exec ./bin/generate --verify
23+
test-generator:
24+
name: Test Generator
25+
runs-on: ubuntu-22.04
26+
steps:
27+
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
28+
- name: Set up Ruby
29+
uses: ruby/setup-ruby@52753b7da854d5c07df37391a986c76ab4615999
30+
with:
31+
ruby-version: "3.3"
32+
bundler-cache: true
33+
- name: Run tests
34+
run: bundle exec rake test:generator

Gemfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ source 'https://rubygems.org'
33
gem 'base64'
44
gem 'minitest'
55
gem 'rake'
6+
gem 'toml-rb', require: false
67
gem 'mocha', require: false
78
gem 'rubocop', '~> 1.50.0', require: false
89
gem 'rubocop-minitest', require: false

Gemfile.lock

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,20 @@ GEM
33
specs:
44
ast (2.4.2)
55
base64 (0.2.0)
6+
citrus (3.0.2)
67
docile (1.4.0)
7-
json (2.7.2)
8+
json (2.8.1)
89
minitest (5.22.3)
910
mocha (2.1.0)
1011
ruby2_keywords (>= 0.0.5)
11-
parallel (1.24.0)
12-
parser (3.3.0.5)
12+
parallel (1.26.3)
13+
parser (3.3.6.0)
1314
ast (~> 2.4.1)
1415
racc
1516
racc (1.7.3)
1617
rainbow (3.1.1)
1718
rake (13.2.1)
18-
regexp_parser (2.9.0)
19+
regexp_parser (2.9.2)
1920
rexml (3.3.9)
2021
rubocop (1.50.2)
2122
json (~> 2.3)
@@ -27,8 +28,8 @@ GEM
2728
rubocop-ast (>= 1.28.0, < 2.0)
2829
ruby-progressbar (~> 1.7)
2930
unicode-display_width (>= 2.4.0, < 3.0)
30-
rubocop-ast (1.31.2)
31-
parser (>= 3.3.0.4)
31+
rubocop-ast (1.34.1)
32+
parser (>= 3.3.1.0)
3233
rubocop-minitest (0.34.5)
3334
rubocop (>= 1.39, < 2.0)
3435
rubocop-ast (>= 1.30.0, < 2.0)
@@ -42,7 +43,10 @@ GEM
4243
simplecov_json_formatter (~> 0.1)
4344
simplecov-html (0.12.3)
4445
simplecov_json_formatter (0.1.4)
45-
unicode-display_width (2.5.0)
46+
toml-rb (3.0.1)
47+
citrus (~> 3.0, > 3.0)
48+
racc (~> 1.7)
49+
unicode-display_width (2.6.0)
4650

4751
PLATFORMS
4852
ruby
@@ -57,6 +61,7 @@ DEPENDENCIES
5761
rubocop-minitest
5862
rubocop-rake
5963
simplecov
64+
toml-rb
6065

6166
BUNDLED WITH
6267
2.5.7

Rakefile

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,21 @@ task :rubocop do
1616
system('rubocop --display-cop-names')
1717
end
1818

19+
desc "Run generator for specefic exercise"
20+
task :generate, [:exercise] do |_t, argumments|
21+
system("./bin/generate --exercise #{argumments[:exercise]}")
22+
end
23+
24+
desc "Run generator for all exercises"
25+
task :generate_all do
26+
system("./bin/generate --all")
27+
end
28+
29+
desc "Verify templates for all exercises"
30+
task :verify do
31+
system("./bin/generate --verify")
32+
end
33+
1934
namespace :test do
2035
flags = ARGV.drop_while { |e| e != '--' }.drop(1).join(' ')
2136

@@ -25,5 +40,10 @@ namespace :test do
2540
task.pattern = 'test/**/*_test.rb'
2641
end
2742

43+
Rake::TestTask.new :generator do |task|
44+
task.options = flags
45+
task.pattern = 'generatorv2/test/**/*_test.rb'
46+
end
47+
2848
ExerciseTestTasks.new options: flags
2949
end

bin/generate

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
#!/usr/bin/env ruby
2+
require 'optparse'
3+
require 'tempfile'
4+
require_relative '../generatorv2/lib/generator'
5+
6+
# Helper methods
7+
def exercises
8+
Dir.entries('./exercises/practice')
9+
.select { |file| File.directory? File.join('./exercises/practice', file) }
10+
end
11+
12+
class VerificationError < StandardError
13+
MESSAGE = 'The result generated for %<exercise>s, does not match the current file'
14+
15+
def initialize(message = MESSAGE)
16+
super
17+
end
18+
end
19+
20+
# Parsing Code
21+
parser = OptionParser.new
22+
23+
parser.on('-v', '--version', 'Print the version') do
24+
puts File.read('./generatorv2/VERSION')
25+
end
26+
27+
parser.on('-h', '--help', 'Prints help') do
28+
puts parser
29+
end
30+
31+
parser.on('-a', '--all', 'Generate all exercises') do
32+
exercises.each do |exercise|
33+
if File.exist?("./exercises/practice/#{exercise}/.meta/test_template.erb")
34+
Generator.new(exercise).generate
35+
end
36+
end
37+
end
38+
39+
parser.on('--verify', 'Verify all exercises') do
40+
exercises.each do |exercise|
41+
if File.exist?("./exercises/practice/#{exercise}/.meta/test_template.erb")
42+
current_code = File.read("./exercises/practice/#{exercise}/#{exercise}_test.rb")
43+
f = File.new("./exercises/practice/#{exercise}/temp_test.rb", 'w+')
44+
Generator.new(exercise).generate(f.path)
45+
generated_code = f.read
46+
File.delete(f.path)
47+
fail VerificationError unless current_code == generated_code
48+
end
49+
rescue VerificationError => e
50+
STDERR.puts e.message % {exercise:}
51+
end
52+
end
53+
54+
parser.on('-e', '--exercise EXERCISE', 'The exercise to generate') do |exercise|
55+
Generator.new(exercise).generate
56+
end
57+
58+
parser.parse!
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
require 'minitest/autorun'
2+
require_relative 'acronym'
3+
4+
class AcronymTest < Minitest::Test
5+
<% json["cases"].each do |cases| %>
6+
def test_<%= underscore(cases["description"]) %>
7+
<%= skip? %>
8+
assert_equal '<%= cases["expected"] %>', <%= camel_case(json["exercise"]) %>.<%= underscore(cases["property"]) %>('<%= cases["input"]["phrase"] %>')
9+
end
10+
<% end %>
11+
end
12+
13+

exercises/practice/acronym/acronym_test.rb

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,37 +4,36 @@
44
class AcronymTest < Minitest::Test
55
def test_basic
66
# skip
7-
assert_equal "PNG", Acronym.abbreviate('Portable Network Graphics')
7+
assert_equal 'PNG', Acronym.abbreviate('Portable Network Graphics')
88
end
99

1010
def test_lowercase_words
1111
skip
12-
assert_equal "ROR", Acronym.abbreviate('Ruby on Rails')
12+
assert_equal 'ROR', Acronym.abbreviate('Ruby on Rails')
1313
end
1414

1515
def test_punctuation
1616
skip
17-
assert_equal "FIFO", Acronym.abbreviate('First In, First Out')
17+
assert_equal 'FIFO', Acronym.abbreviate('First In, First Out')
1818
end
1919

2020
def test_all_caps_word
2121
skip
22-
assert_equal "GIMP", Acronym.abbreviate('GNU Image Manipulation Program')
22+
assert_equal 'GIMP', Acronym.abbreviate('GNU Image Manipulation Program')
2323
end
2424

2525
def test_punctuation_without_whitespace
2626
skip
27-
assert_equal "CMOS", Acronym.abbreviate('Complementary metal-oxide semiconductor')
27+
assert_equal 'CMOS', Acronym.abbreviate('Complementary metal-oxide semiconductor')
2828
end
2929

3030
def test_very_long_abbreviation
3131
skip
32-
assert_equal "ROTFLSHTMDCOALM",
33-
Acronym.abbreviate('Rolling On The Floor Laughing So Hard That My Dogs Came Over And Licked Me')
32+
assert_equal 'ROTFLSHTMDCOALM', Acronym.abbreviate('Rolling On The Floor Laughing So Hard That My Dogs Came Over And Licked Me')
3433
end
3534

3635
def test_consecutive_delimiters
3736
skip
38-
assert_equal "SIMUFTA", Acronym.abbreviate('Something - I made up from thin air')
37+
assert_equal 'SIMUFTA', Acronym.abbreviate('Something - I made up from thin air')
3938
end
4039
end

generatorv2/README.md

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
# Generator
2+
3+
Last Updated: 2024/11/9
4+
5+
The generator is a powerful tool that can be used to generate tests for exercises based on the canonical data.
6+
The generator is written in Ruby and is located in the `bin` directory.
7+
8+
## How to use the generator
9+
10+
### Things to do before running the generator
11+
12+
Run `bundle install` to install the required libraries.
13+
Before running the generator you have to make sure a couple of files are in place.
14+
15+
1. `tests.toml` file
16+
17+
It is located under the `.meta` folder for each exercise.
18+
The toml file is used to configure which exercises are generated and which are not.
19+
Since the generator grabs all the data from the canonical data, so does this enable new tests that won't automatically be merged in.
20+
Instead so does new tests have to be added to the toml file before they show up in the test file.
21+
22+
If there is a test that isn't needed or something that doesn't fit Ruby you can remove it from the toml file.
23+
By writing after the test name `include = false` and it will be skipped when generating the test file.
24+
25+
2. `config.json` file, located in the root of the track
26+
27+
The generator makes sure that the exercise is in the config.json so you need to add it there before running the generator.
28+
29+
**NOTE:**
30+
You are **NOT** allowed to write `include = false` more than once after each UUID.
31+
Since that can lead to errors in the generator.
32+
33+
Bad way:
34+
35+
```toml
36+
[1e22cceb-c5e4-4562-9afe-aef07ad1eaf4]
37+
description = "basic"
38+
include = false
39+
include = false
40+
```
41+
42+
Good way:
43+
44+
```toml
45+
[1e22cceb-c5e4-4562-9afe-aef07ad1eaf4]
46+
description = "basic"
47+
include = false
48+
```
49+
50+
### Template
51+
52+
The generator uses a template file to generate the test file.
53+
The template is located under the `.meta` for each exercise.
54+
55+
This template has to be manually written for each exercise.
56+
The goal 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.
57+
58+
The template file is written in [Embedded Ruby(ERB)][erb].
59+
ERB enables you to write Ruby code inside of the template file.
60+
It also means that the templates can be highly customizable since you can write any Ruby code you want.
61+
62+
When writing the template file, it is recommended to look at already existing template files to get a better understanding of how it works.
63+
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.
64+
The modification is that the cases which are not included in the toml file will be removed from the data structure.
65+
66+
When writing the template so is it a special tool that can help with giving `# skip` and `skip` tags for tests.
67+
You simply have to call the `skip?` method.
68+
It will return either `# skip` or `skip` depending on if it is the first test case or not.
69+
70+
Here is an example:
71+
72+
```
73+
<%= skip? %>
74+
<%= skip? %>
75+
<%= skip? %>
76+
```
77+
78+
result:
79+
80+
```
81+
# skip
82+
skip
83+
skip
84+
```
85+
86+
### The Test Generator
87+
88+
If all the earlier steps are done you run the generator.
89+
To run the generator you need to have a working Ruby installation with the gems installed, via `bundle install`.
90+
The generator is located in the `bin` directory and is called `generator`.
91+
92+
To run the generator so do you have to be in the root directory and run the following command:
93+
94+
```shell
95+
bundle exec ./bin/generate -e <exercise-name>
96+
```
97+
98+
Where `<exercise-name>` is the same name as the exercise has in its directory.
99+
100+
For more commands and options, you can see this by running the command:
101+
102+
```shell
103+
bundle exec ./bin/generate --help
104+
```
105+
106+
### Errors and warnings
107+
108+
The generator will give you errors and warnings if something is wrong.
109+
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.
110+
It will also report an error if it can not read the `canonical-data.json` file.
111+
The generator will check that the generated file is formatted correctly, reporting an error if there is a problem.
112+
The file will still be generated even if the formatter reports errors, So that you can check the file and see what is wrong and fix it in the template.
113+
114+
[erb]: https://docs.ruby-lang.org/en/master/ERB.html
115+
[canonical data]: https://github.com/exercism/problem-specifications

generatorv2/VERSION

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
0.1.0

0 commit comments

Comments
 (0)