Skip to content

Commit

Permalink
Importable/exportable Minitest assertions, ShoesSpec runner (#399)
Browse files Browse the repository at this point in the history
* Importable/exportable Minitest assertions, ShoesSpec runner.

Refactor Changelog logic in constants.rb to work when not run from Scarpe root dir

* Remove Rubocop from default local test run

* Update lacci/lib/shoes/app.rb

Co-authored-by: Nick Schwaderer <[email protected]>

---------

Co-authored-by: Nick Schwaderer <[email protected]>
  • Loading branch information
noahgibbs and Schwad authored Oct 10, 2023
1 parent 6abd50f commit 0471e8a
Show file tree
Hide file tree
Showing 13 changed files with 297 additions and 33 deletions.
2 changes: 1 addition & 1 deletion Rakefile
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,4 @@ end

RuboCop::RakeTask.new

task default: [:test, :lacci_test, :component_test, :rubocop]
task default: [:test, :lacci_test, :component_test]
16 changes: 16 additions & 0 deletions lacci/lib/shoes-spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# frozen_string_literal: true

module Shoes
module Spec
def self.instance
@instance
end

def self.instance=(spec_inst)
if @instance && @instance != spec_inst
raise "Lacci can only use a single ShoesSpec implementation at one time!"
end
@instance = spec_inst
end
end
end
4 changes: 4 additions & 0 deletions lacci/lib/shoes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ module Kernel

require_relative "shoes/download"

if ENV["SHOES_SPEC_TEST"]
require_relative "shoes-spec"
end

# The module containing Shoes in all its glory.
# Shoes is a platform-independent GUI library, designed to create
# small visual applications in Ruby.
Expand Down
12 changes: 12 additions & 0 deletions lacci/lib/shoes/app.rb
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,18 @@ def initialize(
end
end

if ENV["SHOES_SPEC_TEST"]
require "scarpe/components/minitest_export_reporter"
Minitest::Reporters::ShoesExportReporter.activate!
test_code = File.read ENV["SHOES_SPEC_TEST"]
unless test_code.empty?
kwargs = {}
kwargs[:class_name] = ENV["SHOES_MINITEST_CLASS_NAME"] if ENV["SHOES_MINITEST_CLASS_NAME"]
kwargs[:test_name] = ENV["SHOES_MINITEST_METHOD_NAME"] if ENV["SHOES_MINITEST_METHOD_NAME"]
Shoes::Spec.instance.run_shoes_spec_test_code test_code, **kwargs
end
end

@app_code_body = app_code_body

# Try to de-dup as much as possible and not send repeat or multiple
Expand Down
43 changes: 25 additions & 18 deletions lib/scarpe/cats_cradle.rb
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,31 @@ def respond_to_missing?(method_name, include_private = false)
end
end

module CCHelpers
# What to do about TextDrawables? Link, code, em, strong?
# Also, wait, what's up with span? What *is* that?
Shoes::Drawable.drawable_classes.each do |drawable_class|
finder_name = drawable_class.dsl_name

define_method(finder_name) do |*args|
app = Shoes::App.instance

drawables = app.find_drawables_by(drawable_class, *args)
raise Scarpe::MultipleWidgetsFoundError, "Found more than one #{finder_name} matching #{args.inspect}!" if drawables.size > 1
raise Scarpe::NoWidgetsFoundError, "Found no #{finder_name} matching #{args.inspect}!" if drawables.empty?

CCProxy.new(drawables[0])
end
end
end

# This class defines the CatsCradle DSL. It also holds a "bag of fibers"
# with promises for when they should next resume.
class CCInstance
include Shoes::Log
include Scarpe::Test::EventedAssertions
include Scarpe::Test::Helpers
include Scarpe::Test::CCHelpers

def self.instance
@instance ||= CCInstance.new
Expand Down Expand Up @@ -136,22 +155,6 @@ def on_event(event, &block)
@waiting_fibers << { promise: event_promise(event), fiber: f }
end

# What to do about TextDrawables? Link, code, em, strong?
# Also, wait, what's up with span? What *is* that?
Shoes::Drawable.drawable_classes.each do |drawable_class|
finder_name = drawable_class.dsl_name

define_method(finder_name) do |*args|
app = Shoes::App.instance

drawables = app.find_drawables_by(drawable_class, *args)
raise Scarpe::MultipleDrawablesFoundError, "Found more than one #{finder_name} matching #{args.inspect}!" if drawables.size > 1
raise Scarpe::NoDrawablesFoundError, "Found no #{finder_name} matching #{args.inspect}!" if drawables.empty?

CCProxy.new(drawables[0])
end
end

def proxy_for(shoes_drawable)
CCProxy.new(shoes_drawable)
end
Expand Down Expand Up @@ -203,6 +206,10 @@ def test_finished(return_results: true)

::Shoes::DisplayService.dispatch_event("destroy", nil)
end

def test_finished_no_results
::Shoes::DisplayService.dispatch_event("destroy", nil)
end
end

# "Cat's Cradle" is a children's game where they interlace string between
Expand All @@ -219,10 +226,10 @@ def test_finished(return_results: true)
#
# Ruby Fiber basic docs: https://ruby-doc.org/core-3.0.0/Fiber.html
#
# This module is mixed into Shoes::App if we're running CatsCradle-based tests
# This module is mixed into a test object if we're running CatsCradle-based tests
module CatsCradle
def event_init
@cc_instance = CCInstance.instance
@cc_instance ||= CCInstance.instance
@cc_instance.event_init
end

Expand Down
2 changes: 2 additions & 0 deletions lib/scarpe/errors.rb
Original file line number Diff line number Diff line change
Expand Up @@ -74,4 +74,6 @@ class AppShutdownError < Scarpe::Error; end
class InvalidClassError < Scarpe::Error; end

class MissingClassError < Scarpe::Error; end

class MultipleShoesSpecRunsError < Scarpe::Error; end
end
2 changes: 1 addition & 1 deletion lib/scarpe/evented_assertions.rb
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ def return_assertion_data
end

# This does a final return of results. If it gets called
# multiple times, the test fails because that's not allowed.
# multiple times with different results, the test fails because that's not allowed.
#
# @param result_bool [Boolean] true if the results are success, false if failure
# @param msg [String] the message included with the results
Expand Down
59 changes: 59 additions & 0 deletions lib/scarpe/shoes_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# frozen_string_literal: true

require "minitest"
require "scarpe/cats_cradle" # Currently needed for CCHelpers

# Test framework code to allow Scarpe to execute Shoes-Spec test code.
# This will run inside the exe/scarpe child process, then send
# results back to the parent Minitest process.

module Scarpe::Test
# Cut down from Rails camelize
def self.camelize(string)
string = string.sub(/^[a-z\d]*/) { |match| match.capitalize }
string.gsub(/(?:_|(\/))([a-z\d]*)/) { "#{$1}#{$2.capitalize}" }
end

# Is it at all reasonable to define more than one test to run in the same Shoes run? Probably not.
# They'll leave in-memory residue.
def self.run_shoes_spec_test_code(code, class_name: "TestShoesSpecCode", test_name: "test_shoes_spec")
if @shoes_spec_init
raise MultipleShoesSpecRunsError, "Scarpe-Webview can only run a single Shoes spec per process!"
end
@shoes_spec_init = true

require_relative "cats_cradle"

# We want Minitest assertions available in the test code.
# But this will normally run in a subprocess. So we need
# to run Minitest tests and then export the results.

test_obj = Object.new
class << test_obj
include Scarpe::Test::CatsCradle
end
test_obj.instance_eval do
event_init

on_heartbeat do
Minitest.run ARGV

test_finished_no_results
end
end

test_class = Class.new(Scarpe::ShoesSpecTest)
Object.const_set(camelize(class_name), test_class)
test_name = "test_" + test_name unless test_name.start_with?("test_")
test_class.define_method(test_name) do
eval(code)
#test_obj.instance_variable_get(:@cc_instance).instance_eval(code)
end
end
end

# When running ShoesSpec tests, we create a parent class for all of them
# with the appropriate convenience methods and accessors.
class Scarpe::ShoesSpecTest < Minitest::Test
include Scarpe::Test::CCHelpers
end
5 changes: 5 additions & 0 deletions lib/scarpe/wv.rb
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,11 @@ class Drawable < Shoes::Linkable
loader = Scarpe::Components::SegmentedFileLoader.new
Shoes.add_file_loader loader

if ENV["SHOES_SPEC_TEST"]
require_relative "shoes_spec"
Shoes::Spec.instance = Scarpe::Test
end

require_relative "wv/web_wrangler"
require_relative "wv/control_interface"

Expand Down
18 changes: 6 additions & 12 deletions lib/scarpe/wv/app.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,13 @@ class App < Drawable
def initialize(properties)
super

# It's possible to provide a Ruby script by setting
# SCARPE_TEST_CONTROL to its file path. This can
# allow pre-setting test options or otherwise
# performing additional actions not written into
# the Shoes app itself.
#
# The control interface is what lets these files see
# events, specify overrides and so on.
# Scarpe's ControlInterface sets up event handlers
# for the display service that aren't sent to
# Lacci (Shoes). In general it's used for setup
# and additional control or testing, outside the
# Shoes app. This is how CatsCradle and Shoes-Spec
# set up testing, for instance.
@control_interface = ControlInterface.new
if ENV["SCARPE_TEST_CONTROL"]
require "scarpe/components/unit_test_helpers"
@control_interface.instance_eval File.read(ENV["SCARPE_TEST_CONTROL"])
end

# TODO: rename @view
@view = Scarpe::Webview::WebWrangler.new title: @title,
Expand Down
1 change: 0 additions & 1 deletion scarpe-components/lib/scarpe/components/calzini/slots.rb
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,6 @@ def spacing_styles_for_attr(attr, props, styles, with_options: true)
end

unless spacing_styles.empty?
#STDERR.puts "Props: #{props.inspect} Attr: #{attr} SpStyle: #{spacing_styles.inspect}"
return styles.merge(spacing_styles)
end

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# frozen_string_literal: true

# Have to require this to get DefaultReporter and the Minitest::Reporters namespace.
ENV["MINITEST_REPORTER"] = "ShoesExportReporter"
require "minitest/reporters"
require "json"
require "json/add/exception"

module Minitest
module Reporters
# To use this Scarpe component, you'll need minitest-reporters in your Gemfile,
# probably in the "test" group. You'll need to require and activate it to
# register it as Minitest's reporter:
#
# require "scarpe/components/minitest_export_reporter"
# Minitest::Reporters::ShoesExportReporter.activate!
#
# Select a destination to export JSON test results to:
#
# export SHOES_MINITEST_EXPORT_FILE=/tmp/shoes_test_export.json
#
# This class overrides the MINITEST_REPORTER environment variable when you call activate.
# If MINITEST_REPORTER isn't set then when you run via Vim, TextMate, RubyMine, etc,
# the reporter will be automatically overridden and print to console instead.
#
# Based on https://gist.github.com/davidwessman/09a13840a8a80080e3842ac3051714c7
class ShoesExportReporter < DefaultReporter
def self.activate!
unless ENV["SHOES_MINITEST_EXPORT_FILE"]
raise "ShoesExportReporter is available, but no export file was specified! Set SHOES_MINITEST_EXPORT_FILE!"
end

Minitest::Reporters.use!
end

def serialize_failures(failures)
failures.map do |fail|
case fail
when Minitest::UnexpectedError
["unexpected", fail.to_json, fail.error.to_json]
when Exception
["exception", fail.to_json]
else
raise "Not sure how to serialize failure object! #{fail.inspect}"
end
end
end

def report
super

results = tests.map do |result|
failures = serialize_failures result.failures
{
name: result.name,
klass: test_class(result),
assertions: result.assertions,
failures: failures,
time: result.time,
metadata: result.metadata,
source_location: (result.source_location rescue ["unknown", -1]),
}
end

out_file = ENV["SHOES_MINITEST_EXPORT_FILE"]
puts "Writing Minitest results to #{out_file.inspect}."
File.write(out_file, JSON.dump(results))
end
end
end
end
Loading

0 comments on commit 0471e8a

Please sign in to comment.