Skip to content

Commit 8ad18ff

Browse files
committed
Implement TestML for Ruby
This implements all of the tests present in the other languages. It gets a couple of things wrong, that can be addressed in follow-up, specifically: * labels are right for a lot of cases, but there appears to be some kind of substitution that we are currently missing * it's not clear what "Plan" means, so turning it on does not do anything right now
1 parent 00ce1ac commit 8ad18ff

25 files changed

+312
-1083
lines changed

bin/testml-cli.bash

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,8 +137,9 @@ TestML supports the following runners:
137137
node-tap NodeJS w/ TAP
138138
perl-tap Perl w/ TAP
139139
python-tap Python (2 or 3) w/ TAP
140-
python-tap Python (2 or 3) w/ unittest
140+
python-unit Python (2 or 3) w/ unittest
141141
raku-tap Raku w/ TAP
142+
ruby-tap Ruby w/ TAP
142143
143144
Aliases:
144145
coffee Alias for coffee-mocha

src/ruby/bin/testml-ruby-tap

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ set -e -u -o pipefail
77
testml-run-file() {
88
# set -x
99
RUBYLIB=$TESTML_ROOT_LIB:$TESTML_LIB${RUBYLIB:+:$RUBYLIB} \
10-
${TESTML_LANG} -e "require 'testml/run/tap'; $TESTML_MODULE.run('$1')"
10+
${TESTML_LANG} -rtestml -e "$TESTML_MODULE.run('$1')"
1111
}
1212

1313
[[ ${TESTML_SOURCED-} ]] ||
@@ -17,7 +17,7 @@ source-testml-config
1717

1818
: "${TESTML_BIN:=testml-ruby-tap}"
1919
: "${TESTML_LANG:=ruby}"
20-
: "${TESTML_MODULE:=TestML::Run::TAP}"
21-
: "${TESTML_BRIDGE:=testml-bridge}"
20+
: "${TESTML_MODULE:=TestML}"
21+
: "${TESTML_BRIDGE:=testml/bridge}"
2222

2323
[[ $0 != "${BASH_SOURCE[0]}" ]] || testml-run "$@"

src/ruby/lib/testml.rb

Lines changed: 269 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,269 @@
1+
# frozen_string_literal: true
2+
3+
require "json"
4+
5+
module TestML
6+
def self.run(filepath)
7+
json = JSON.parse(File.read(filepath))
8+
raise "Unsupported TestML version: #{json["testml"]}" if json["testml"] != "0.3.0"
9+
10+
index = Index.new
11+
runner = Runner.new(index)
12+
13+
runner.run(json["code"], json["data"])
14+
puts "1..#{index}"
15+
end
16+
17+
class Index
18+
def initialize = @value = 0
19+
def succ! = @value = @value.succ
20+
def to_s = "#{@value}"
21+
end
22+
23+
Mine = Class.new
24+
NULL = Object.new
25+
NONE = Object.new
26+
FALSY = [nil, false, NONE, NULL].freeze
27+
28+
class Runner
29+
def initialize(index)
30+
@index = index
31+
@vars = {}
32+
@bridge = nil
33+
@label = nil
34+
@diff = false
35+
@plan = false
36+
end
37+
38+
def run(exprs, env)
39+
exprs.each { |expr| compile(expr, env) }
40+
end
41+
42+
private
43+
44+
def bridge
45+
@bridge ||=
46+
begin
47+
require ENV.fetch("TESTML_BRIDGE")
48+
TestML::Bridge.new
49+
end
50+
end
51+
52+
def join_label(parent, child)
53+
child.gsub(/(\A\+|\+\z|\{\+\})/, parent)
54+
end
55+
56+
def assert(positive, got, operator, want, label, env)
57+
if env.is_a?(Hash)
58+
label ||= env["Label"]
59+
label = join_label(label, @label) if @label
60+
end
61+
62+
label ||= ""
63+
label = label.gsub(/\{(\*?.+?)\}/) do
64+
substitution =
65+
case (value = $1)
66+
when "Got" then got
67+
when "Want" then want
68+
when /^\*(.+)$/ then env.fetch($1)
69+
else @vars.fetch(value)
70+
end
71+
72+
case substitution
73+
when String
74+
substitution.gsub("\n", "␤")
75+
when Array
76+
"[#{substitution.map(&:inspect).join(",")}]"
77+
else
78+
substitution.inspect
79+
end
80+
end
81+
82+
@index.succ!
83+
84+
result = got.public_send(operator, want)
85+
result = !result unless positive
86+
87+
if result
88+
puts "ok #{@index} - #{label}"
89+
else
90+
puts "not ok #{@index} - #{label}"
91+
92+
if @diff
93+
puts " got: '#{got.inspect}'"
94+
puts " expected: '#{want.inspect}'"
95+
end
96+
end
97+
end
98+
99+
def filters?(filters, env)
100+
filters.all? do |filter|
101+
case filter
102+
in /^\*(.+)$/ then env.key?($1)
103+
in /^\!\*(.+)$/ then !env.key?($1)
104+
end
105+
end
106+
end
107+
108+
def compile(expr, env)
109+
case expr
110+
111+
# functions
112+
in ["ArgV"] then ARGV
113+
in ["Bool", value] then !FALSY.include?(compile(value, env))
114+
in ["Block"] then env
115+
in ["Block", label] then env.find { |env| env["Label"] == label }
116+
in ["Blocks"] then env
117+
in ["Cat", *values] then values.map { |value| compile(value, env) }.join
118+
in ["Env"] then ENV
119+
in ["Error"] then StandardError.new
120+
in ["Error", message] then StandardError.new(message)
121+
in ["False"] then false
122+
in ["None"] then NONE
123+
in ["Null"] then NULL
124+
in ["Sum", *values] then values.sum { |value| compile(value, env) }
125+
in ["Throw", message] then raise StandardError, message
126+
in ["True"] then true
127+
128+
# assignments
129+
in ["=", "Label", value]
130+
@label = value
131+
in ["=", "Diff", value]
132+
@diff = compile(value, env)
133+
in ["=", "Plan", value]
134+
@plan = compile(value, env)
135+
in ["=", name, value]
136+
@vars[name] = compile(value, env)
137+
in ["||=", name, value]
138+
@vars[name] = compile(value, env) if FALSY.include?(@vars[name])
139+
140+
# statements
141+
in ["*", name]
142+
compile(env.fetch(name), env)
143+
in ["%<>", filters, ["=>", *] => function]
144+
env.each do |child_env|
145+
compile(function, child_env).call([], child_env) if filters?(filters, child_env)
146+
end
147+
in ["%<>", filters, expr]
148+
env.each do |child_env|
149+
if expr.length == 4
150+
child_env = { **child_env }
151+
child_env["Label"] = join_label(child_env["Label"], expr[3])
152+
end
153+
154+
compile(expr, child_env) if filters?(filters, child_env)
155+
end
156+
in ["<>", filters, ["=>", *] => function]
157+
compile(function, env).call([], env) if filters?(filters, env)
158+
in ["<>", filters, expr]
159+
compile(expr, env) if filters?(filters, env)
160+
in ["<>", filters, expr, label]
161+
compile(expr, env.merge("Label" => join_label(env["Label"], label))) if filters?(filters, env)
162+
in [".", receiver, *calls]
163+
compile_calls(receiver, calls, env)
164+
in ["%", inputs, function]
165+
callable = compile(function, env)
166+
compile(inputs, env).each { |input| callable.call([[input]], {}) }
167+
in ["&", expr]
168+
compile(expr, env).call([], env)
169+
in ["[]", receiver, index]
170+
compile(receiver, env).fetch(compile(index, env)) { NONE }
171+
in [":" | /\Ahash-lookup\z/i, receiver, key]
172+
compile(receiver, env).fetch(compile(key, env)) { NONE }
173+
174+
# assertions
175+
in [("==" | "!==") => operator, left, right, *labels]
176+
assert(operator == "==", compile(left, env), :==, compile(right, env), labels[0], env)
177+
in [("=~" | "!=~") => operator, left, right, *labels]
178+
left = compile(left, env)
179+
right = compile(right, env)
180+
181+
Array(right).each do |right|
182+
Array(left).each do |left|
183+
assert(operator == "=~", left, :match?, right, labels[0], env)
184+
end
185+
end
186+
in [("~~" | "!~~") => operator, left, right, *labels]
187+
left = compile(left, env)
188+
right = compile(right, env)
189+
190+
Array(right).each do |substring|
191+
assert(operator == "~~", left, :include?, substring, labels[0], env)
192+
end
193+
194+
# types
195+
in [Array => values] then values.map { |value| compile(value, env) }
196+
in [Hash => values] then values.transform_values { |value| compile(value, env) }
197+
in String then expr
198+
in Integer then expr
199+
in ["/", pattern] then Regexp.new(pattern)
200+
in ["\"", value] then value.gsub(/\{(.+?)\}/) { @vars.fetch($1) }
201+
in ["_"] then env["inputs"][0][0]
202+
in ["=>", params, exprs]
203+
runner = Runner.new(@index)
204+
exprs = compile_params(params).concat(exprs)
205+
->(inputs, env) { runner.run(exprs, env.merge("inputs" => inputs)) }
206+
in [String => name] if @vars.key?(name)
207+
@vars.fetch(name)
208+
in [String => name, *arguments] if @vars.key?(name)
209+
@vars.fetch(name).call([arguments.map { |argument| compile(argument, env) }], {})
210+
in [/\A[a-z]/ => name, *arguments]
211+
bridge.public_send(name.tr("-", "_"), *arguments.map { |argument| compile(argument, env) })
212+
end
213+
end
214+
215+
def compile_params(params)
216+
params.map.with_index { |param, index| ["=", param, ["[]", ["*", "inputs"], index]] }
217+
end
218+
219+
def compile_calls(callee, calls, env)
220+
calls.inject(-> { compile(callee, env) }) do |callee, call|
221+
-> {
222+
case call
223+
in ["Catch"]
224+
begin callee.call; rescue => error then error end
225+
in ["Count"]
226+
callee.call.count
227+
in ["Join", delimiter]
228+
callee.call.join(delimiter)
229+
in ["Lines"]
230+
callee.call.split("\n")
231+
in ["Msg"]
232+
callee.call.message
233+
in ["Split", delimiter]
234+
callee.call.split(delimiter)
235+
in ["Text"]
236+
callee.call.map { |line| "#{line}\n" }.join
237+
in ["Type"]
238+
compile_type(callee.call)
239+
in ["=>", params, exprs]
240+
Runner.new(@index).run(compile_params(params).concat(exprs), { "inputs" => [callee.call] })
241+
in [String => name, *arguments] if @vars.key?(name)
242+
@vars.fetch(name).call([[callee.call, *arguments]], {})
243+
in [/\A[a-z]/ => name, *arguments]
244+
bridge.public_send(name.tr("-", "_"), callee.call, *arguments.map { |argument| compile(argument, env) })
245+
else
246+
begin callee.call; rescue => error then raise end
247+
raise StandardError, "Unknown call: #{call.inspect}"
248+
end
249+
}
250+
end.call
251+
end
252+
253+
def compile_type(object)
254+
case object
255+
in Array then "list"
256+
in Hash then "hash"
257+
in Integer then "num"
258+
in Mine then "native"
259+
in NONE then "none"
260+
in NULL then "null"
261+
in Proc then "func"
262+
in Regexp then "regex"
263+
in StandardError then "error"
264+
in String then "str"
265+
in TrueClass | FalseClass then "bool"
266+
end
267+
end
268+
end
269+
end

src/ruby/lib/testml/bridge.rb

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,23 @@
1-
use strict; use warnings;
2-
package TestML::Bridge;
1+
module TestML
2+
class Bridge
3+
def add(left, right)
4+
left + right
5+
end
36

4-
sub new {
5-
my $class = shift;
7+
def cat(*values)
8+
values.join
9+
end
610

7-
bless {@_}, $class;
8-
}
11+
def get_env(key)
12+
ENV.fetch(key) { NONE }
13+
end
914

10-
1;
15+
def mine
16+
Mine.new
17+
end
18+
19+
def sub(left, right)
20+
left - right
21+
end
22+
end
23+
end

0 commit comments

Comments
 (0)