Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 29 additions & 23 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,45 +125,47 @@ It's a module that returns a table.
These tests are not executed by the test runner.
- Examples: [`robot-name`][robot-name], [`custom-set`][custom-set]

##### Examples of custom assertions

- [`dnd-character`][dnd-character] -- `assert.between value, min, max`
- [`space-age`][space-age] -- `assert.approx_equal #{case.expected}, result`
- [`alphametics`][alphametics] -- `assert.has.same_kv result, expected`

#### Helper functions for formatting test cases

##### Functions in bin/generate-spec

These functions are exported from [`bin/generate-spec`][generate-spec-exported] for use in spec_generator modules

- `indent(text, level)` -- provide leading whitespace to the appropriate level.
- `quote(str)` -- add quotation marks to the string, single or double as appropriate.
- `is_empty(tbl)` -- predicate: is the table empty
- `is_json_null(value)` -- predicate: is the value `json.null` from dkjson
- `contains(tbl, value)` -- predicate: does the table contain the value (uses `==`)

##### Helper functions

We have a library of helper functions, useful for generating pretty tables mostly.

For example, to nicely format a list of words, or a list of strings over multiple lines, or to recursively format nested tables.
- to safely quote a word,
- to nicely format a list of words, or a list of strings over multiple lines,
- to recursively format nested tables.

**Look in [`lib/test_helpers.moon`][test-helpers].**
**Look in [`lib/spec_helpers.moon`][spec-helpers].**

Example:

```moonscript
import int_list, string_list from require 'test_helpers'
import indent, int_list, string_list from require 'spec_helpers'
...
{
generate_test: (case, level) ->
lines = {
"input = #{int_list case.input.numbers}"
"expected = #{string_list case.expected, level}"
...
"assert.are.same expected, someFunc input"
}
table.concat [indent line for line in *lines], "\n"
```

##### Custom Assertions

Custom assertions are stored in [`lib/spec_handlers/assertions.moon][assertions].
Currently there is only one:

- [`dnd-character`][dnd-character] -- `assert.is.between value, min, max`

I used to have some more, but the [luassert][luassert] library is quite complete:

- `assert.are.equal expected, actual` -- compare with `==`
- `assert.are.same expected, actual` -- deeply compare tables for same keys and values
- `assert.is.near expected, actual, epsilon` -- floats are approximately equal
- `assert.matches pattern, string [, init [, plain]]` -- pattern matching (with lua patterns)
- see the lua [string.find][string-find] and [string.match][string-match] docs.
- luassert uses string.match, or string.find if "plain" is true.

##### Comparing tables deeply

The `assert.are.same t1, t2` assertion is used to compare tables deeply.
Expand Down Expand Up @@ -242,9 +244,13 @@ Here, the value `4` was chosen to reflect the max depth of the expected value:
[exercise-list]: https://github.com/exercism/moonscript/issues/102
[gh-issues]: https://github.com/exercism/moonscript/issues
[gh-pulls]: https://github.com/exercism/moonscript/pulls
[luassert]: https://github.com/lunarmodules/luassert/blob/master/src/assertions.lua
[string-find]: https://www.lua.org/manual/5.4/manual.html#pdf-string.find
[string-match]: https://www.lua.org/manual/5.4/manual.html#pdf-string.match
[style]: ./STYLE.md
[generate-spec-exported]: ./bin/generate-spec#L51
[test-helpers]: ./lib/test_helpers.moon
[spec-helpers]: ./lib/spec_helpers.moon
[assertions]: ./lib/spec_helpers/assertions.moon
[space-age]: ./exercises/practice/space-age/.meta/spec_generator.moon
[alphametics]: ./exercises/practice/alphametics/.meta/spec_generator.moon
[dnd-character]: ./exercises/practice/dnd-character/.meta/spec_generator.moon
Expand Down
95 changes: 29 additions & 66 deletions bin/generate-spec
Original file line number Diff line number Diff line change
Expand Up @@ -3,76 +3,35 @@

require 'moonscript'
json = (require 'dkjson').use_lpeg!
path = require 'pl.path'
file = require 'pl.file'
-- import p from require 'moon'

local exercise_name, exercise_directory, spec_generator, included_tests -- forward declarations


file_exists = (path) ->
f = io.open path, 'r'
if f
f\close!
true
else
false


read_file = (path) ->
f = assert io.open path, 'r'
contents = f\read '*a'
f\close!
contents


write_file = (path, contents) ->
f = assert io.open path, 'w'
f\write contents
f\close!
package.moonpath = "./lib/?.moon;#{package.moonpath}"
import indent, quote, table_contains from require 'spec_helpers'

die = (msg) ->
io.stderr\write msg
os.exit 1

-- ----------------------------------------------------------
included_tests_from_toml = (path) ->
included = {}
local uuid, last_uuid
line_no = 0
local last_uuid

for line in io.lines(path)
line_no += 1
for uuid in line\gmatch('%[([%x%-]+)%]')
for line in io.lines path
for uuid in line\gmatch '%[([%x%-]+)%]'
last_uuid = uuid
included[uuid] = true

if line\match('^include%s*=%s*false')
if line\match '^include%s*=%s*false'
included[last_uuid] = nil

included

-- ----------------------------------------------------------
-- functions marked as global so spec_generators can see them
export indent, quote, is_json_null, is_empty, contains

indent = (text, level) ->
error 'Provide a level for `indent`', 2 if not level
string.rep(' ', level) .. text

quote = (str) ->
if str\find "'"
"\"#{str\gsub '"', '\\"'}\""
else
"'#{str}'"

is_empty = (t) -> not next t

-- the dkjson `json.null` value is an empty table
is_json_null = (value) -> type(value) == 'table' and is_empty(value)

-- a table contains a value
contains = (t, v) ->
for elem in *t
return true if elem == v
false

-- ----------------------------------------------------------

-- forward declarations
local exercise_name, exercise_directory, spec_generator, included_tests

test_cmd = 'it'

Expand All @@ -81,18 +40,21 @@ process = (node, level=0) ->
for exclusion in *(spec_generator.exclusions or {})
if node[exclusion.key]
if exclusion.op == 'contains'
return '' if contains node[exclusion.key], exclusion.value
return '' if table_contains node[exclusion.key], exclusion.value
else
return '' if node[exclusion.key] == exclusion.value

if node.cases
if not node.cases
test = "#{test_cmd} #{quote node.description}, ->\n#{spec_generator.generate_test(node, level + 1)}\n"
test_cmd = 'pending'
return indent test, level
else
output = {}

if node.description
table.insert output, indent("describe #{quote(node.description .. ':')}, ->", level)
else
table.insert output, indent("describe '#{exercise_name}:', ->", level)

if spec_generator.test_helpers
table.insert output, spec_generator.test_helpers

Expand All @@ -105,11 +67,6 @@ process = (node, level=0) ->

return table.concat output, '\n'

else -- no "cases" member
test = "#{test_cmd} #{quote node.description}, ->\n#{spec_generator.generate_test(node, level + 1)}\n"
test_cmd = 'pending'
return indent test, level

-- ----------------------------------------------------------
-- "main"
-- ----------------------------------------------------------
Expand All @@ -121,6 +78,12 @@ if snake_name == 'say'
snake_name = './say'

exercise_directory = 'exercises/practice/' .. exercise_name
if not path.exists(exercise_directory)
die "no such directory: #{exercise_directory}\n"

-- can't generate what's not there
if not path.exists("#{exercise_directory}/.meta/spec_generator.moon")
die "#{exercise_name} does not have a spec_generator module.\n"

canonical_data_url = "https://raw.githubusercontent.com/exercism/problem-specifications/main/exercises/#{exercise_name}/canonical-data.json"
canonical_data_path = "canonical-data/#{exercise_name}.json"
Expand All @@ -129,7 +92,7 @@ assert os.execute('mkdir -p "$(dirname "' .. canonical_data_path .. '")"')
assert os.execute('curl "' .. canonical_data_url .. '" -s -o "' .. canonical_data_path .. '"')

-- "json.null" ref: https://dkolf.de/dkjson-lua/documentation
canonical_data = json.decode read_file(canonical_data_path), 1, json.null
canonical_data = json.decode file.read(canonical_data_path, false), 1, json.null

tests_toml_path = exercise_directory .. '/.meta/tests.toml'
included_tests = included_tests_from_toml tests_toml_path
Expand All @@ -143,7 +106,7 @@ if spec_generator.module_name
elseif spec_generator.module_imports
spec = "import #{table.concat spec_generator.module_imports, ', '} from require '#{snake_name}'"
else
error 'spec_generator is missing both "module_name" and "module_imports"'
die 'spec_generator is missing both "module_name" and "module_imports"'

spec ..= "\n\n" .. process(canonical_data)

Expand All @@ -161,6 +124,6 @@ if spec_generator.bonus
spec ..= spec_generator.bonus

spec_path = exercise_directory .. '/' .. snake_name .. '_spec.moon'
write_file spec_path, spec
file.write spec_path, spec, false

print "Created #{spec_path}"
Empty file removed exercises/practice/.keep
Empty file.
2 changes: 2 additions & 0 deletions exercises/practice/acronym/.meta/spec_generator.moon
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import indent, quote from require 'spec_helpers'

{
module_imports: {'abbreviate'},
generate_test: (case, level) ->
Expand Down
2 changes: 2 additions & 0 deletions exercises/practice/affine-cipher/.meta/spec_generator.moon
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import indent, quote from require 'spec_helpers'

{
module_imports: {'encode', 'decode'},

Expand Down
2 changes: 1 addition & 1 deletion exercises/practice/all-your-base/.meta/spec_generator.moon
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import int_list from require 'test_helpers'
import indent, quote, int_list from require 'spec_helpers'

{
module_imports: {'rebase'},
Expand Down
2 changes: 1 addition & 1 deletion exercises/practice/allergies/.meta/spec_generator.moon
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import word_list from require 'test_helpers'
import indent, quote, word_list from require 'spec_helpers'

{
module_name: 'Allergies',
Expand Down
25 changes: 3 additions & 22 deletions exercises/practice/alphametics/.meta/spec_generator.moon
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import kv_table from require 'test_helpers'
import indent, quote, is_json_null, table_tostring_sortby_keys from require 'spec_helpers'

{
module_imports: {'solve'},
Expand All @@ -13,27 +13,8 @@ import kv_table from require 'test_helpers'
{
"puzzle = #{quote case.input.puzzle}",
"result = solve puzzle",
"expected = #{kv_table case.expected, level}",
"assert.is.same_kv result, expected"
"expected = #{table_tostring_sortby_keys case.expected, level}",
"assert.are.same result, expected"
}
table.concat [indent line, level for line in *lines], '\n'

test_helpers: [[
-- ----------------------------------------------------------
same_kv = (state, arguments) ->
actual = arguments[1]
return false if type(actual) != 'table'
expected = arguments[2]
size = (t) -> #[k for k, _ in pairs t]
return false if size(expected) != size(actual)
for k, v in pairs expected
return false if actual[k] != v
true

say = require 'say'
say\set 'assertion.same_kv.positive', 'Actual result\n%s\ndoes not have the same keys and values as expected\n%s'
say\set 'assertion.same_kv.negative', 'Actual result\n%s\nwas not supposed to be the same as expected\n%s'
assert\register 'assertion', 'same_kv', same_kv, 'assertion.same_kv.positive', 'assertion.same_kv.negative'
-- ----------------------------------------------------------
]]
}
Loading
Loading