Skip to content

Commit 419309c

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 419309c

25 files changed

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

src/ruby/lib/testml/bridge.rb

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +0,0 @@
1-
use strict; use warnings;
2-
package TestML::Bridge;
3-
4-
sub new {
5-
my $class = shift;
6-
7-
bless {@_}, $class;
8-
}
9-
10-
1;

0 commit comments

Comments
 (0)