Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Initial import. *Almost* all tests pass (see below)

Failing test: Bound Ruby methods don't accept arguments, yet.

Pending tests:
  Tomato conversions from Ruby to JS to Ruby should handle objects (how to do it? JSON?)
  Tomato conversions from Ruby to JS to Ruby should handle classes (how to do it? Prototype?)
  Tomato conversions from Ruby to JS to Ruby should handle modules (how to do it? Prototype?)
  Tomato conversions from Ruby to JS to Ruby should handle regexps (api support)
  Tomato conversions from Ruby to JS to Ruby should handle structs (TODO)
  Tomato conversions from Ruby to JS to Ruby should handle bignums (TODO)
  Tomato should build an anonymous Ruby object to mirror anonymous JS objects (TODO)
  Tomato should convert JS regexps to Ruby ones (api support)
  • Loading branch information...
commit b0f8e165cf36fa103f5842433e39ef0419b794ab 1 parent 592c801
@sinisterchipmunk authored
View
6 .gitignore
@@ -19,3 +19,9 @@ rdoc
pkg
## PROJECT::SPECIFIC
+
+.idea
+*.o
+*.log
+*.bundle
+*.so
View
120 README.rdoc
@@ -1,6 +1,124 @@
= tomato
-Description goes here.
+Leverages Google's V8 JavaScript library to interface Ruby code with JavaScript code.
+
+== Examples
+
+=== Instantiation
+
+ require 'tomato'
+ tomato = Tomato.new
+
+=== Running JavaScript code
+
+When JS code is executed, it'll do its thing internally and then return the result:
+
+ tomato.run("(1+1);") # => 2
+
+When JS code encounters an error of some kind, it'll get raised in Ruby:
+
+ tomato.run("throw 'error';")
+
+ # Produces:
+ #
+ # Tomato::Error: (dynamic):error
+ # throw 'error';
+ # ^
+ #
+ # from (irb):3:in `run'
+ # from (irb):3
+ # from /Users/colin/.rvm/rubies/ruby-1.9.1-p378/bin/irb:17:in `<main>'
+
+You can bind Ruby methods to JavaScript, as well. CURRENTLY only Tomato instance methods are supported. This will be
+extended in the near future to include any method on any object. Current implementation of method binding looks like
+this:
+
+ tomato.bind_method(:inspect)
+ tomato.run("inspect();") #=> "#<Tomato>"
+
+For now (until a better method binding is implemented) you can add methods. You can do this before OR after binding it:
+
+ tomato.bind_method(:say_hello)
+ def tomato.say_hello
+ puts "Hi there!"
+ end
+ tomato.run("say_hello();")
+
+ # Produces:
+ #
+ # Hi there!
+ # => nil
+
+You can also catch Ruby errors in JavaScript:
+
+ tomato.bind_method(:raise_err)
+ def tomato.raise_err
+ raise ArgumentError, "Not what I meant!"
+ end
+ tomato.run("try { raise_err(); } catch(e) {}"); #=> Nothing, because the error was caught.
+
+Or let them bubble up to Ruby:
+
+ tomato.bind_method(:raise_err)
+ def tomato.raise_err
+ raise ArgumentError, "Not what I meant!"
+ end
+ begin
+ tomato.run("raise_err();");
+ rescue ArgumentError => err
+ puts "Error: #{err.message}"
+ end
+
+ # Produces:
+ #
+ # Error: Not what I meant!
+ # => nil
+
+That's all I've implemented so far, but I think it looks pretty cool.
+
+=== Object Types
+
+Some objects in Ruby don't exist in JS. So far the ones I've implemented are Hashes and Symbols.
+
+==== Symbols
+
+A Symbol is converted to an object in JS with a "symbol" attribute and a "toString()" method. That same object, if
+returned to Ruby, is converted back to a Symbol.
+
+
+==== Hashes
+
+A Hash is converted to an object of some form or another. It's not an Array. I haven't added much to the JS side of
+this object yet, so I don't know if it's even usable in JS. But it does, at least, return to Ruby as a Hash. So you
+can have a method that returns a Hash, call the method from JS, return that object to Ruby, and get the hash. Dunno
+if that's useful, but it's an implementation detail that had to be looked at regardless. Hashes still have a lot of
+development ahead of them.
+
+== Requirements
+
+ * Ruby. I'm testing on 1.9.1.
+ * V8. You'll need to modify ext/tomato/extconf.rb to point to your V8 libraries and headers. You can download
+ V8 from http://code.google.com/apis/v8/ -- you'll need to build it yourself.
+
+=== Regarding V8
+
+I haven't figured out the best way to approach this yet. I think I'll end up bundling a version of V8 with the gem
+when release time comes, so that I can ensure compatibility. If the API changes, my code breaks. You know how it goes.
+For now you'll have to download it separately and build it manually, however. So if you're on Windows... good luck.
+
+== Performance
+
+Too early to tell. Benchmarking will be done as I get closer to a real release.
+
+Since V8's JavaScript code is compiled into byte code, this MAY (untested) have a positive impact on performance
+in some applications. Just translate your code to JavaScript and let V8 compile it to byte code. I'm hoping for some
+cool results.
+
+== IMPORTANT
+
+I haven't even added a version number yet because it's WAAAY too early for that. This is some pretty cool code IMO,
+but keep in mind if you want to check it out that it's still development code (not even pre-Alpha). I do hope to
+release it as a real gem after it's reached a sufficient level of functionality.
== Note on Patches/Pull Requests
View
31 Rakefile
@@ -1,5 +1,6 @@
require 'rubygems'
require 'rake'
+require 'spec/rake/spectask'
begin
require 'jeweler'
@@ -10,7 +11,7 @@ begin
gem.email = "sinisterchipmunk@gmail.com"
gem.homepage = "http://github.com/sinisterchipmunk/tomato"
gem.authors = ["Colin MacKenzie IV"]
- gem.add_development_dependency "thoughtbot-shoulda", ">= 0"
+ gem.add_development_dependency "rspec", ">= 1.3.0"
# gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
end
Jeweler::GemcutterTasks.new
@@ -18,11 +19,11 @@ rescue LoadError
puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
end
-require 'rake/testtask'
-Rake::TestTask.new(:test) do |test|
- test.libs << 'lib' << 'test'
- test.pattern = 'test/**/test_*.rb'
+Spec::Rake::SpecTask.new(:test) do |test|
+ test.libs << 'lib'
+ test.pattern = 'spec/**/*_spec.rb'
test.verbose = true
+ test.spec_opts << ['-c']
end
begin
@@ -38,7 +39,25 @@ rescue LoadError
end
end
-task :test => :check_dependencies
+namespace :make do
+ desc "Clean binaries"
+ task :clean do
+ chdir(File.expand_path("../ext/tomato", __FILE__)) do
+ raise "Clean failed" unless system("make clean")
+ end
+ end
+
+ desc "Build binaries"
+ task :build do
+ chdir(File.expand_path("../ext/tomato", __FILE__)) do
+ unless system("gcc -MM *.cpp > depend") && system("ruby extconf.rb") && system("make all")
+ raise "Build failed"
+ end
+ end
+ end
+end
+
+task :test => ['make:build', :check_dependencies]
task :default => :test
View
17 ext/tomato/IMPORTANT
@@ -0,0 +1,17 @@
+This library does a lot of arbitrary mixing of C and C++. That's inherently dangerous. Following is one VERY IMPORTANT
+consideration to bear in mind regarding C++ objects with nontrivial destructors (i.e. V8!):
+
+http://blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-talk/100535
+
+> Second, ruby's exception handling via setjmp/longjmp effectively means
+> you should never construct a C++ object with a nontrivial destructor
+> on the stack. If ruby longjmps out of your code, your destructors
+> will not be called.
+
+It's worse than that; if ruby longjmps over the destruction of an
+automatic object, the program has undefined behavior. And if a C++
+exception ever leaves C++ code and goes into Ruby code, the result will
+also be undefined.
+
+C99 programmers also have to be careful; longjmping over the destruction
+of a variable-length array can result in a memory leak.
View
187 ext/tomato/Makefile
@@ -0,0 +1,187 @@
+
+SHELL = /bin/sh
+
+#### Start of system configuration section. ####
+
+srcdir = .
+topdir = /Users/colin/.rvm/rubies/ruby-1.9.1-p378/include/ruby-1.9.1
+hdrdir = /Users/colin/.rvm/rubies/ruby-1.9.1-p378/include/ruby-1.9.1
+arch_hdrdir = /Users/colin/.rvm/rubies/ruby-1.9.1-p378/include/ruby-1.9.1/$(arch)
+VPATH = $(srcdir):$(arch_hdrdir)/ruby:$(hdrdir)/ruby
+prefix = $(DESTDIR)/Users/colin/.rvm/rubies/ruby-1.9.1-p378
+exec_prefix = $(prefix)
+vendorhdrdir = $(rubyhdrdir)/vendor_ruby
+sitehdrdir = $(rubyhdrdir)/site_ruby
+rubyhdrdir = $(includedir)/$(RUBY_INSTALL_NAME)-$(ruby_version)
+vendordir = $(libdir)/$(RUBY_INSTALL_NAME)/vendor_ruby
+sitedir = $(libdir)/$(RUBY_INSTALL_NAME)/site_ruby
+mandir = $(datarootdir)/man
+localedir = $(datarootdir)/locale
+libdir = $(exec_prefix)/lib
+psdir = $(docdir)
+pdfdir = $(docdir)
+dvidir = $(docdir)
+htmldir = $(docdir)
+infodir = $(datarootdir)/info
+docdir = $(datarootdir)/doc/$(PACKAGE)
+oldincludedir = $(DESTDIR)/usr/include
+includedir = $(prefix)/include
+localstatedir = $(prefix)/var
+sharedstatedir = $(prefix)/com
+sysconfdir = $(prefix)/etc
+datadir = $(datarootdir)
+datarootdir = $(prefix)/share
+libexecdir = $(exec_prefix)/libexec
+sbindir = $(exec_prefix)/sbin
+bindir = $(exec_prefix)/bin
+rubylibdir = $(libdir)/$(ruby_install_name)/$(ruby_version)
+archdir = $(rubylibdir)/$(arch)
+sitelibdir = $(sitedir)/$(ruby_version)
+sitearchdir = $(sitelibdir)/$(sitearch)
+vendorlibdir = $(vendordir)/$(ruby_version)
+vendorarchdir = $(vendorlibdir)/$(sitearch)
+
+CC = gcc
+CXX = g++
+LIBRUBY = $(LIBRUBY_SO)
+LIBRUBY_A = lib$(RUBY_SO_NAME)-static.a
+LIBRUBYARG_SHARED = -l$(RUBY_SO_NAME)
+LIBRUBYARG_STATIC = -l$(RUBY_SO_NAME)-static
+OUTFLAG = -o
+COUTFLAG = -o
+
+RUBY_EXTCONF_H =
+cflags = $(optflags) $(debugflags) $(warnflags)
+optflags = -O2
+debugflags = -g
+warnflags = -Wall -Wno-parentheses
+CFLAGS = -fno-common $(cflags) -fno-common -pipe -fno-common
+INCFLAGS = -I. -I$(arch_hdrdir) -I$(hdrdir)/ruby/backward -I$(hdrdir) -I$(srcdir)
+DEFS =
+CPPFLAGS = -DHAVE_V8_H -D_XOPEN_SOURCE -D_DARWIN_C_SOURCE $(DEFS) $(cppflags) -I/Users/colin/projects/gems/v8/v8-read-only/include
+CXXFLAGS = $(CFLAGS) $(cxxflags)
+ldflags = -L. -L/usr/local/lib -L/Users/colin/projects/gems/v8/v8-read-only -lv8
+dldflags =
+archflag =
+DLDFLAGS = $(ldflags) $(dldflags) $(archflag)
+LDSHARED = cc -dynamic -bundle -undefined suppress -flat_namespace
+LDSHAREDXX = $(LDSHARED)
+AR = ar
+EXEEXT =
+
+RUBY_INSTALL_NAME = ruby
+RUBY_SO_NAME = ruby
+arch = i386-darwin10.4.0
+sitearch = i386-darwin10.4.0
+ruby_version = 1.9.1
+ruby = /Users/colin/.rvm/rubies/ruby-1.9.1-p378/bin/ruby
+RUBY = $(ruby)
+RM = rm -f
+RM_RF = $(RUBY) -run -e rm -- -rf
+RMDIRS = $(RUBY) -run -e rmdir -- -p
+MAKEDIRS = mkdir -p
+INSTALL = /usr/bin/install -c
+INSTALL_PROG = $(INSTALL) -m 0755
+INSTALL_DATA = $(INSTALL) -m 644
+COPY = cp
+
+#### End of system configuration section. ####
+
+preload =
+
+libpath = . $(libdir)
+LIBPATH = -L. -L$(libdir)
+DEFFILE =
+
+CLEANFILES = mkmf.log
+DISTCLEANFILES =
+DISTCLEANDIRS =
+
+extout =
+extout_prefix =
+target_prefix = /tomato
+LOCAL_LIBS =
+LIBS = $(LIBRUBYARG_SHARED) -lv8 -lpthread -ldl -lobjc -lstdc++
+SRCS = binding_methods.cpp conversions_to_js.cpp conversions_to_rb.cpp errors.cpp tomato.cpp v8.cpp
+OBJS = binding_methods.o conversions_to_js.o conversions_to_rb.o errors.o tomato.o v8.o
+TARGET = tomato
+DLLIB = $(TARGET).bundle
+EXTSTATIC =
+STATIC_LIB =
+
+BINDIR = $(bindir)
+RUBYCOMMONDIR = $(sitedir)$(target_prefix)
+RUBYLIBDIR = $(sitelibdir)$(target_prefix)
+RUBYARCHDIR = $(sitearchdir)$(target_prefix)
+HDRDIR = $(rubyhdrdir)/ruby$(target_prefix)
+ARCHHDRDIR = $(rubyhdrdir)/$(arch)/ruby$(target_prefix)
+
+TARGET_SO = $(DLLIB)
+CLEANLIBS = $(TARGET).bundle
+CLEANOBJS = *.o *.bak
+
+all: $(DLLIB)
+static: $(STATIC_LIB)
+
+clean-rb-default::
+clean-rb::
+clean-so::
+clean: clean-so clean-rb-default clean-rb
+ @-$(RM) $(CLEANLIBS) $(CLEANOBJS) $(CLEANFILES)
+
+distclean-rb-default::
+distclean-rb::
+distclean-so::
+distclean: clean distclean-so distclean-rb-default distclean-rb
+ @-$(RM) Makefile $(RUBY_EXTCONF_H) conftest.* mkmf.log
+ @-$(RM) core ruby$(EXEEXT) *~ $(DISTCLEANFILES)
+ @-$(RMDIRS) $(DISTCLEANDIRS)
+
+realclean: distclean
+install: install-so install-rb
+
+install-so: $(RUBYARCHDIR)
+install-so: $(RUBYARCHDIR)/$(DLLIB)
+$(RUBYARCHDIR)/$(DLLIB): $(DLLIB)
+ $(INSTALL_PROG) $(DLLIB) $(RUBYARCHDIR)
+install-rb: pre-install-rb install-rb-default
+install-rb-default: pre-install-rb-default
+pre-install-rb: Makefile
+pre-install-rb-default: Makefile
+$(RUBYARCHDIR):
+ $(MAKEDIRS) $@
+
+site-install: site-install-so site-install-rb
+site-install-so: install-so
+site-install-rb: install-rb
+
+.SUFFIXES: .c .m .cc .cxx .cpp .C .o
+
+.cc.o:
+ $(CXX) $(INCFLAGS) $(CPPFLAGS) $(CXXFLAGS) $(COUTFLAG)$@ -c $<
+
+.cxx.o:
+ $(CXX) $(INCFLAGS) $(CPPFLAGS) $(CXXFLAGS) $(COUTFLAG)$@ -c $<
+
+.cpp.o:
+ $(CXX) $(INCFLAGS) $(CPPFLAGS) $(CXXFLAGS) $(COUTFLAG)$@ -c $<
+
+.C.o:
+ $(CXX) $(INCFLAGS) $(CPPFLAGS) $(CXXFLAGS) $(COUTFLAG)$@ -c $<
+
+.c.o:
+ $(CC) $(INCFLAGS) $(CPPFLAGS) $(CFLAGS) $(COUTFLAG)$@ -c $<
+
+$(DLLIB): $(OBJS) Makefile
+ @-$(RM) $(@)
+ $(LDSHAREDXX) -o $@ $(OBJS) $(LIBPATH) $(DLDFLAGS) $(LOCAL_LIBS) $(LIBS)
+
+
+
+###
+binding_methods.o: binding_methods.cpp tomato.h
+conversions_to_js.o: conversions_to_js.cpp tomato.h
+conversions_to_rb.o: conversions_to_rb.cpp tomato.h
+errors.o: errors.cpp tomato.h
+tomato.o: tomato.cpp tomato.h
+v8.o: v8.cpp tomato.h
View
84 ext/tomato/binding_methods.cpp
@@ -0,0 +1,84 @@
+#include "tomato.h"
+
+static VALUE bound_method_call(VALUE args);
+static Handle<Value> bound_method(const Arguments& args);
+static int store_rb_message(const Arguments &args, V8Tomato **out_tomato, VALUE *out_receiver, ID *out_method_id);
+
+
+Handle<Value> bound_method(const Arguments& args)
+{
+ // we get the method ID and then call it, so that any C++ destructors that need to fire before
+ // we do so, can.
+ VALUE receiver;
+ ID rb_method_id;
+ V8Tomato *tomato;
+ int code = store_rb_message(args, &tomato, &receiver, &rb_method_id);
+ if (code == -1)
+ return ThrowException(String::New("Error: _tomato is not an object (BUG: please report)"));
+
+ VALUE rbargs = rb_ary_new2(2);
+ rb_ary_store(rbargs, 0, receiver);
+ rb_ary_store(rbargs, 1, ID2SYM(rb_method_id));
+
+ int error;
+ VALUE result = rb_protect(bound_method_call, rbargs, &error);
+ if(error)
+ {
+ return ThrowException(js_error_from(rb_gv_get("$!")));
+ }
+
+ return js_value_of(tomato, result);
+}
+
+static VALUE bound_method_call(VALUE args)
+{
+ VALUE receiver = rb_ary_entry(args, 0);
+ VALUE rb_method_id = SYM2ID(rb_ary_entry(args, 1));
+
+ VALUE result = rb_funcall(receiver, rb_method_id, 0);
+
+ return result;
+}
+
+int store_rb_message(const Arguments &args, V8Tomato **out_tomato, VALUE *out_receiver, ID *out_method_id)
+{
+ Handle<Function> function = args.Callee();
+ Handle<String> _tomato = String::New("_tomato");
+ if (!function->Get(_tomato)->IsExternal())
+ return -1;
+
+ V8Tomato *tomato = (V8Tomato *)Local<External>::Cast(function->Get(_tomato))->Value();
+ VALUE receiver = tomato->rb_instance;
+ String::Utf8Value method_name(function->GetName());
+
+ *out_tomato = tomato;
+ *out_method_id = rb_intern(ToCString(method_name));
+ *out_receiver = receiver;
+ return 0;
+}
+
+VALUE fTomato_bind_method(int argc, VALUE *argv, VALUE self)
+{
+ if (argc != 1) rb_raise(rb_eArgError, "Expected name of method");
+ const char *method_name = rb_id2name(rb_to_id(argv[0]));
+
+ V8Tomato *tomato;
+ Data_Get_Struct(self, V8Tomato, tomato);
+
+ HandleScope handle_scope;
+ Context::Scope context_scope(tomato->context);
+ Handle<Value> value = tomato->context->Global();
+ if (value->IsObject())
+ {
+ Handle<Object> object = Handle<Object>::Cast(value);
+ Handle<Value> proto_value = object->GetPrototype();
+ Handle<Function> function = FunctionTemplate::New(bound_method)->GetFunction();
+
+ function->Set(String::New("_tomato"), External::New(tomato));
+ function->SetName(String::New(method_name));
+
+ object->Set(String::New(method_name), function);
+ return Qtrue;
+ }
+ return Qfalse;
+}
View
177 ext/tomato/conversions_to_js.cpp
@@ -0,0 +1,177 @@
+#include "tomato.h"
+
+static Handle<Value> js_array_from(V8Tomato *tomato, VALUE value);
+static Handle<Value> js_hash_from(V8Tomato *tomato, VALUE value);
+static Handle<Value> js_symbol_to_string(const Arguments& args);
+static Handle<Value> js_symbol_from(VALUE value);
+
+Handle<Value> js_value_of(V8Tomato *tomato, VALUE value)
+{
+ switch(TYPE(value))
+ {
+ case T_NIL : return Null();
+ //case T_OBJECT :
+ //case T_CLASS :
+ //case T_MODULE :
+ case T_FLOAT : return Number::New(NUM2DBL(value));
+ case T_STRING : return String::New(StringValuePtr(value));
+// case T_REGEXP :
+ case T_ARRAY : return js_array_from(tomato, value);
+ case T_HASH : return js_hash_from(tomato, value);
+ //case T_STRUCT :
+ case T_BIGNUM : return Number::New(NUM2LONG(value));
+ case T_FIXNUM : return Int32::New(FIX2INT(value));
+ //case T_COMPLEX :
+ //case T_RATIONAL:
+ //case T_FILE :
+ case T_TRUE : return True();
+ case T_FALSE : return False();
+ //case T_DATA :
+ case T_SYMBOL :
+ if (SYM2ID(value) == rb_intern("undefined")) return Undefined();
+ else return js_symbol_from(value);
+ //return String::New(rb_id2name(SYM2ID(value)));
+ };
+ return inspect_rb(value);
+}
+
+Handle<Value> js_array_from(V8Tomato *tomato, VALUE value)
+{
+ Handle<Array> array;
+ int size, i;
+ VALUE *ptr;
+
+ switch(TYPE(value))
+ {
+ case T_ARRAY:
+ size = RARRAY_LEN(value);
+ ptr = RARRAY_PTR(value);
+ array = Array::New(size);
+ for (i = 0; i < size; i++)
+ array->Set(i, js_value_of(tomato, *(ptr+i)));
+ break;
+ default:
+ return ThrowException(String::New("Could not construct JS array: unexpected Ruby type (BUG: please report)"));
+ }
+ return array;
+}
+
+static Handle<Value> js_symbol_from(VALUE value)
+{
+ ID id = SYM2ID(value);
+ Handle<Object> symbol = Object::New();
+ symbol->Set(String::New("_tomato_symbol"), Boolean::New(true), DontEnum);
+ symbol->Set(String::New("symbol"), String::New(rb_id2name(id)));
+
+ // make it transparently convert to string
+ Handle<Function> function = FunctionTemplate::New(js_symbol_to_string)->GetFunction();
+ function->SetName(String::New("toString"));
+ function->Set(String::New("symbol"), symbol, DontEnum);
+ symbol->Set(String::New("toString"), function);
+ return symbol;
+}
+
+static Handle<Value> js_symbol_to_string(const Arguments& args)
+{
+ Handle<Function> function = args.Callee();
+ Handle<Object> symbol = Handle<Object>::Cast(function->Get(String::New("symbol")));
+ if (!symbol->IsObject())
+ return ThrowException(String::New("Could not convert symbol to string: symbol handle missing (BUG: please report)"));
+ return symbol->Get(String::New("symbol"));
+}
+
+Handle<Value> js_hash_from(V8Tomato *tomato, VALUE value)
+{
+ VALUE rb_keys = rb_funcall(value, rb_intern("keys"), 0);
+ VALUE rb_values = rb_funcall(value, rb_intern("values"), 0);
+ VALUE *keys = RARRAY_PTR(rb_keys),
+ *values = RARRAY_PTR(rb_values);
+
+ int size = RARRAY_LEN(rb_keys), i;
+
+ Handle<Object> js_hash = Object::New();
+ Handle<Array> js_keys = Array::New(size);
+ Handle<Array> js_values = Array::New(size);
+
+ for (i = 0; i < size; i++)
+ {
+ js_keys->Set(i, js_value_of(tomato, *(keys+i)));
+ js_values->Set(i, js_value_of(tomato, *(values+i)));
+ }
+
+ js_hash->Set(String::New("_tomato_hash_keys"), js_keys, DontEnum);
+ js_hash->Set(String::New("_tomato_hash_values"), js_values, DontEnum);
+ js_hash->Set(String::New("_tomato_hash"), Boolean::New(true), DontEnum);
+
+ return js_hash;
+}
+
+Handle<Value> inspect_rb(VALUE value)
+{
+ VALUE string = rb_funcall(value, rb_intern("inspect"), 0);
+ return String::New(StringValuePtr(string));
+}
+
+/*
+Handle<Value> v8_value_of(V8Tomato *tomato, VALUE object)
+{
+
+ switch(TYPE(object))
+ {
+ case T_NIL :
+ case T_OBJECT :
+ //case T_CLASS :
+ //case T_MODULE :
+ case T_FLOAT :
+ case T_STRING :
+ case T_REGEXP :
+ case T_ARRAY :
+ case T_HASH :
+ //case T_STRUCT :
+ case T_BIGNUM :
+ case T_FIXNUM :
+ //case T_COMPLEX :
+ //case T_RATIONAL:
+ //case T_FILE :
+ case T_TRUE :
+ case T_FALSE :
+ //case T_DATA :
+ case T_SYMBOL :
+ symbol_from(object)
+ break;
+ default: string_from(object);
+ };
+}
+*/
+/*
+ if (result->IsUndefined()) { return ID2SYM(rb_intern("undefined")); }
+ if (result->IsNull()) { return Qnil; }
+ if (result->IsTrue()) { return Qtrue; }
+ if (result->IsFalse()) { return Qfalse; }
+ if (result->IsString()) { return ruby_string_from(result); }
+ if (result->IsFunction()) { return ruby_string_from(result); }
+ if (result->IsArray()) { Handle<Array> array = Handle<Array>::Cast(result); return ruby_array_from(tomato, array); }
+ if (result->IsNumber()) { return ruby_numeric_from(result); }
+ if (result->IsDate()) { return ruby_date_from(result); }
+
+
+ if (result->IsObject())
+ {
+ Handle<Value> json = tomato->context->Global()->Get(String::New("JSON"));
+ if (json->IsObject())
+ {
+ Handle<Value> stringify = Handle<Object>::Cast(json)->Get(String::New("stringify"));
+ if (stringify->IsFunction())
+ {
+ String::Utf8Value str(Handle<Function>::Cast(stringify)->Call(
+ Handle<Object>::Cast(json),
+ 1,
+ &result
+ ));
+ return rb_str_new2(ToCString(str));
+ }
+ }
+ }
+ return Qnil;
+}
+*/
View
124 ext/tomato/conversions_to_rb.cpp
@@ -0,0 +1,124 @@
+#include "tomato.h"
+
+static VALUE ruby_array_from(V8Tomato *tomato, Handle<Array> result);
+static VALUE ruby_numeric_from(const Handle<Value> &number);
+static VALUE ruby_date_from(const Handle<Value> &date);
+static VALUE ruby_string_from(const Handle<Value> &value);
+static VALUE ruby_symbol_from(const Handle<Object> &value);
+static VALUE ruby_object_from(V8Tomato *tomato, Handle<Value> result);
+static VALUE ruby_hash_from(V8Tomato *tomato, const Handle<Object> &object);
+
+VALUE ruby_value_of(V8Tomato *tomato, Handle<Value> result)
+{
+ if (result->IsUndefined()) { return ID2SYM(rb_intern("undefined")); }
+ if (result->IsNull()) { return Qnil; }
+ if (result->IsBoolean())
+ {
+ if (result->IsTrue()) { return Qtrue; }
+ else if (result->IsFalse()) { return Qfalse; }
+ }
+ if (result->IsString()) { return ruby_string_from(result); }
+ if (result->IsFunction()) { return ruby_string_from(result); }
+ if (result->IsArray()) { Handle<Array> array = Handle<Array>::Cast(result); return ruby_array_from(tomato, array); }
+ if (result->IsNumber()) { return ruby_numeric_from(result); }
+ if (result->IsDate()) { return ruby_date_from(result); }
+ /* TODO: RegExp support: To patch or not to patch? */
+ if (result->IsObject()) { return ruby_object_from(tomato, result); }
+
+ return Qnil;
+}
+
+
+/* First checks for any special cases set up internally (i.e. Hash). If all else fails, returns the JSON for this
+ object. */
+static VALUE ruby_object_from(V8Tomato *tomato, Handle<Value> result)
+{
+ if (result->IsObject())
+ {
+ Handle<Object> object = Handle<Object>::Cast(result);
+
+ if (object->Get(String::New("_tomato_hash"))->IsTrue())
+ return ruby_hash_from(tomato, object);
+ if (object->Get(String::New("_tomato_symbol"))->IsTrue())
+ return ruby_symbol_from(object);
+ }
+
+ /* Call Javascript's JSON.stringify(object) method. If that can't be done for any reason, return nil. */
+ Handle<Value> json = tomato->context->Global()->Get(String::New("JSON"));
+ if (json->IsObject())
+ {
+ Handle<Value> stringify = Handle<Object>::Cast(json)->Get(String::New("stringify"));
+ if (stringify->IsFunction())
+ {
+ String::Utf8Value str(Handle<Function>::Cast(stringify)->Call(
+ Handle<Object>::Cast(json),
+ 1,
+ &result
+ ));
+ /* TODO translate the result to Ruby! */
+ return rb_str_new2(ToCString(str));
+ }
+ }
+ return ID2SYM(rb_intern("unknown"));
+}
+
+static VALUE ruby_hash_from(V8Tomato *tomato, const Handle<Object> &object)
+{
+ VALUE hash = rb_hash_new();
+
+ Handle<Array> keys = Handle<Array>::Cast(object->Get(String::New("_tomato_hash_keys")));
+ Handle<Array> values = Handle<Array>::Cast(object->Get(String::New("_tomato_hash_values")));
+
+ int length = keys->Length();
+
+ for (int i = 0; i < length; i++)
+ {
+ rb_hash_aset(hash, ruby_value_of(tomato, keys->Get(i)), ruby_value_of(tomato, values->Get(i)));
+ }
+
+ return hash;
+}
+
+static VALUE ruby_symbol_from(const Handle<Object> &value)
+{
+ String::Utf8Value symbol_value(value->Get(String::New("symbol")));
+ return ID2SYM(rb_intern(ToCString(symbol_value)));
+}
+
+static VALUE ruby_string_from(const Handle<Value> &value)
+{
+ String::Utf8Value string_value(value);
+ return rb_str_new2(ToCString(string_value));
+}
+
+static VALUE ruby_date_from(const Handle<Value> &value)
+{
+ const Handle<Date> date = Handle<Date>::Cast(value);
+ return rb_funcall(rb_cTime, rb_intern("at"), 1, DBL2NUM(date->NumberValue() / 1000.0));
+}
+
+static VALUE ruby_array_from(V8Tomato *tomato, Handle<Array> array)
+{
+ unsigned int length = array->Length();
+ VALUE rbarr = rb_ary_new2(length);
+ for (unsigned int i = 0; i < length; i++)
+ rb_ary_push(rbarr, ruby_value_of(tomato, array->Get(Integer::New(i))));
+ return rbarr;
+}
+
+static VALUE ruby_numeric_from(const Handle<Value> &number)
+{
+ if (number->IsInt32()) { return INT2FIX(number->Int32Value()); }
+ if (number->IsUint32()) { return INT2FIX(number->Uint32Value()); }
+ /* TODO FIXME: There's no way to know if we're facing a Uint64. It works as a Float but user may not expect a Float... */
+ //if (number->IsInt64()) { return INT2NUM(number->IntegerValue()); }
+ return DBL2NUM(number->NumberValue());
+}
+
+void inspect_js(V8Tomato *tomato, Handle<Value> obj)
+{
+ VALUE rbObj = ruby_value_of(tomato, obj);
+ VALUE rbStr = rb_funcall(rbObj, rb_intern("inspect"), 0);
+ printf("%s\n", StringValuePtr(rbStr));
+}
+
View
6 ext/tomato/depend
@@ -0,0 +1,6 @@
+binding_methods.o: binding_methods.cpp tomato.h
+conversions_to_js.o: conversions_to_js.cpp tomato.h
+conversions_to_rb.o: conversions_to_rb.cpp tomato.h
+errors.o: errors.cpp tomato.h
+tomato.o: tomato.cpp tomato.h
+v8.o: v8.cpp tomato.h
View
73 ext/tomato/errors.cpp
@@ -0,0 +1,73 @@
+#include "tomato.h"
+
+VALUE eTransError = Qnil;
+
+Local<Object> js_error_from(VALUE ruby_error)
+{
+ VALUE message = rb_funcall(ruby_error, rb_intern("to_s"), 0);
+ Local<Object> js_error = Local<Object>::Cast(Exception::Error(String::New(StringValuePtr(message))));
+ js_error->Set(String::New("_is_ruby_error"), Boolean::New(true), DontEnum);
+ return js_error;
+}
+
+void err_init(void)
+{
+ eTransError = rb_define_class_under(cTomato, "TranslatableError", cTomatoError);
+}
+
+
+/* The following was adapted from samples/shell.cc in v8 project. */
+void raise_error(TryCatch *try_catch)
+{
+ HandleScope handle_scope;
+ String::Utf8Value exception(try_catch->Exception());
+ Handle<Message> message = try_catch->Message();
+
+ /* check for Ruby error; if it's an error, reraise $!. TODO: See $! can ever be a different error... */
+ Handle<Value> is_ruby_error = Local<Object>::Cast(try_catch->Exception())->Get(String::New("_is_ruby_error"));
+ if (is_ruby_error->IsBoolean() && is_ruby_error->IsTrue())
+ {
+ throw rb_gv_get("$!");
+ }
+
+ std::string errmsg;
+ if (message.IsEmpty()) {
+ // V8 didn't provide any extra information about this error; just
+ // print the exception.
+ errmsg += ToCString(exception);
+ errmsg += "\n";
+ } else {
+ // Print (filename):(line number): (message).
+ String::Utf8Value filename(message->GetScriptResourceName());
+ int linenum = message->GetLineNumber();
+ errmsg += ToCString(filename);
+ errmsg += ":";
+ errmsg += linenum;
+ errmsg += ToCString(exception);
+ errmsg += "\n";
+
+ // Print line of source code.
+ String::Utf8Value sourceline(message->GetSourceLine());
+ errmsg += ToCString(sourceline);
+ errmsg += "\n";
+
+ // Print wavy underline (GetUnderline is deprecated).
+ int start = message->GetStartColumn();
+ for (int i = 0; i < start; i++) {
+ errmsg += " ";
+ }
+ int end = message->GetEndColumn();
+ for (int i = start; i < end; i++) {
+ errmsg += "^";
+ }
+ errmsg += "\n";
+ String::Utf8Value stack_trace(try_catch->StackTrace());
+ if (stack_trace.length() > 0) {
+ const char* stack_trace_string = ToCString(stack_trace);
+ errmsg += stack_trace_string;
+ errmsg += "\n";
+ }
+ }
+
+ throw errmsg;
+}
View
24 ext/tomato/extconf.rb
@@ -0,0 +1,24 @@
+#require 'mkmf-rice'
+#
+##$LIBS << " -lstdc++"
+#$CPPFLAGS << " -I/Users/colin/projects/gems/v8/v8-read-only/include"
+#$LDFLAGS << " -L/Users/colin/projects/gems/v8/v8-read-only -lv8"
+#
+#have_header("v8.h")
+#have_library("v8")#, "v8::Handle")
+#
+##$LDFLAGS = "-lv8"
+#create_makefile("tomato/tomato")
+#
+
+require 'mkmf'
+
+$LIBS << " -lstdc++"
+$CPPFLAGS << " -I/Users/colin/projects/gems/v8/v8-read-only/include"
+$LDFLAGS << " -L/Users/colin/projects/gems/v8/v8-read-only -lv8"
+
+have_header("v8.h")
+have_library("v8")#, "v8::Handle")
+
+#$LDFLAGS = "-lv8"
+create_makefile("tomato/tomato")
View
117 ext/tomato/tomato.cpp
@@ -0,0 +1,117 @@
+#include "tomato.h"
+
+VALUE cTomato = Qnil;
+VALUE cTomatoError = Qnil;
+VALUE rb_cTime = Qnil;
+
+static VALUE fTomato_run(int argc, VALUE *argv, VALUE self);
+static VALUE fTomato_version(VALUE self);
+static VALUE fTomato_allocate(VALUE klass);
+
+static VALUE fTomato_execute(VALUE self, const char *javascript, const char *filename);
+
+static void tomato_mark(V8Tomato *tomato);
+static void tomato_free(V8Tomato *tomato);
+
+
+extern "C"
+void Init_tomato(void)
+{
+ /* Look up the DateTime class */
+ rb_cTime = rb_const_get(rb_cObject, rb_intern("Time"));
+
+ /* define the Tomato class */
+ cTomato = rb_define_class("Tomato", rb_cObject);
+
+ /* definte the Tomato::Error class */
+ cTomatoError = rb_define_class_under(cTomato, "Error", rb_eRuntimeError);
+
+ /* object allocation method, which will initialize our V8 context */
+ rb_define_alloc_func(cTomato, (ruby_method_1 *)&fTomato_allocate);
+
+ /* instance method "run" accepts a String argument */
+ rb_define_method(cTomato, "run", (ruby_method_vararg *)&fTomato_run, -1);
+
+ /* instance method "version" */
+ rb_define_method(cTomato, "version", (ruby_method_vararg *)&fTomato_version, 0);
+
+ /* instance method "bind_method" */
+ rb_define_method(cTomato, "_bind_method", (ruby_method_vararg *)&fTomato_bind_method, -1);
+
+ /* init error-specific junk */
+ err_init();
+}
+
+
+static VALUE fTomato_allocate(VALUE klass)
+{
+ V8Tomato *tomato = new V8Tomato;
+ VALUE instance = Data_Wrap_Struct(klass, tomato_mark, tomato_free, tomato);
+
+ HandleScope handle_scope;
+ Handle<ObjectTemplate> global = ObjectTemplate::New();
+ tomato->context = Context::New(NULL, global);
+ tomato->rb_instance = instance;
+
+ return instance;
+}
+
+static void tomato_mark(V8Tomato *tomato) { }
+static void tomato_free(V8Tomato *tomato)
+{
+ tomato->context.Dispose();
+ delete tomato;
+}
+
+static VALUE fTomato_version(VALUE self)
+{
+ return rb_str_new2(V8::GetVersion());
+}
+
+static VALUE fTomato_run(int argc, VALUE *argv, VALUE self)
+{
+ if (argc == 0)
+ {
+ rb_raise(rb_eArgError, "expected at least 1 argument: the JavaScript to be executed");
+ return Qnil;
+ }
+ else if (argc > 2)
+ {
+ rb_raise(rb_eArgError, "expected at most 2 arguments: the JavaScript to be executed, and an optional file name");
+ return Qnil;
+ }
+
+ char *javascript = StringValuePtr(argv[0]);
+ char *filename;
+ if (argc == 2)
+ filename = StringValuePtr(argv[1]);
+ else
+ filename = (char *)"(dynamic)";
+
+ /* We have to do things this way because rb_raise does a longjmp, which causes C++ destructors not to fire.
+ V8 is somewhat reliant on those destructors, so that's a Bad Thing. */
+ try
+ {
+ VALUE return_value = fTomato_execute(self, javascript, filename);
+ return return_value;
+ }
+ catch (std::string const &exception_message)
+ {
+ rb_raise(cTomatoError, "%s", exception_message.c_str());
+ }
+ catch (VALUE const &rbErr)
+ {
+ rb_exc_raise(rbErr);
+ }
+}
+
+static VALUE fTomato_execute(VALUE self, const char *javascript, const char *filename)
+{
+ V8Tomato *tomato;
+ Data_Get_Struct(self, V8Tomato, tomato);
+ Context::Scope context_scope(tomato->context);
+ HandleScope handle_scope;
+ Handle<String> source_code = String::New(javascript);
+ Handle<String> name = String::New(filename);
+ return execute(tomato, source_code, name);
+}
View
45 ext/tomato/tomato.h
@@ -0,0 +1,45 @@
+#ifndef TOMATO_H
+#define TOMATO_H
+
+#include <string>
+#include <ruby.h>
+#include <v8.h>
+
+using namespace v8;
+
+typedef VALUE (ruby_method_vararg)(...);
+typedef VALUE (ruby_method_1)(VALUE);
+
+typedef struct {
+ Persistent<Context> context;
+ VALUE rb_instance;
+} V8Tomato;
+
+// Extracts a C string from a V8 Utf8Value.
+#define ToCString(value) (*value ? *value : "<string conversion failed>")
+
+/* in tomato.cpp */
+extern VALUE cTomato;
+extern VALUE cTomatoError;
+extern VALUE rb_cTime;
+
+/* in conversions_to_rb.cpp */
+extern VALUE ruby_value_of(V8Tomato *tomato, Handle<Value> result);
+extern void inspect_js(V8Tomato *tomato, Handle<Value> obj);
+
+/* in conversions_to_js.cpp */
+extern Handle<Value> js_value_of(V8Tomato *tomato, VALUE value);
+extern Handle<Value> inspect_rb(VALUE value);
+
+/* in errors.cpp */
+extern void raise_error(TryCatch *try_catch);
+extern Local<Object> js_error_from(VALUE ruby_error);
+extern void err_init(void);
+
+/* in v8.cpp */
+extern VALUE execute(V8Tomato *tomato, Handle<String> source, Handle<Value> name);
+
+/* in binding_methods.cpp */
+extern VALUE fTomato_bind_method(int argc, VALUE *argv, VALUE self);
+
+#endif//TOMATO_H
View
27 ext/tomato/v8.cpp
@@ -0,0 +1,27 @@
+#include "tomato.h"
+
+// Executes a string within the current v8 context.
+VALUE execute(V8Tomato *tomato, Handle<String> source, Handle<Value> name)
+{
+ HandleScope handle_scope;
+ TryCatch try_catch;
+ Handle<Script> script = Script::Compile(source, name);
+ if (script.IsEmpty())
+ {
+ raise_error(&try_catch);
+ return Qnil;
+ }
+ else
+ {
+ Handle<Value> result = script->Run();
+ if (result.IsEmpty())
+ {
+ raise_error(&try_catch);
+ return Qnil;
+ }
+ else
+ {
+ return ruby_value_of(tomato, result);
+ }
+ }
+}
View
12 lib/tomato.rb
@@ -0,0 +1,12 @@
+$LOAD_PATH << File.expand_path("../../ext/tomato", __FILE__)
+require 'tomato.so'
+
+class Tomato
+ def bind_method(method_name)
+ _bind_method(method_name)
+ end
+
+ def inspect
+ "#<Tomato>"
+ end
+end
View
16 spec/lib/bound_methods_spec.rb
@@ -0,0 +1,16 @@
+require 'spec_helper'
+
+describe "Tomato bound methods" do
+ subject { Tomato.new }
+
+ it "should map Ruby methods to JavaScript methods" do
+ subject.bind_method(:inspect).should == true
+ proc { subject.run("(inspect());") }.should_not raise_error
+ end
+
+ it "should accept arguments" do
+ subject.bind_method(:echo)
+ def subject.echo(i); i; end
+ subject.run("echo(1);").should == 1
+ end
+end
View
114 spec/lib/conversions_spec.rb
@@ -0,0 +1,114 @@
+require 'spec_helper'
+
+describe "Tomato conversions" do
+ context "from Ruby to JS to Ruby" do
+ subject { Tomato.new }
+
+ def handle(value)
+ $test_value = value
+ def subject.a_method; $test_value; end
+ subject.bind_method(:a_method)
+ subject.run("(a_method());")
+ end
+
+ it "should add a toString function to ruby symbols" do
+ def subject.a_method; return :symbol; end
+ subject.bind_method(:a_method)
+ subject.run("a_method().toString();").should == "symbol"
+ end
+
+ it "should handle exceptions" do
+ def subject.a_method; raise ArgumentError, "err"; end
+ subject.bind_method(:a_method)
+ proc { subject.run("(a_method());") }.should raise_error(ArgumentError)
+ end
+
+ it "should handle nil" do
+ handle(nil).should == nil
+ end
+
+ it "should handle objects" do
+ pending "how to do it? JSON?"
+ end
+
+ it "should handle classes" do
+ pending "how to do it? Prototype?"
+ end
+
+ it "should handle modules" do
+ pending "how to do it? Prototype?"
+ end
+
+ it "should handle floats" do
+ handle(1.05).should == 1.05
+ end
+
+ it "should handle strings" do
+ handle("hello").should == "hello"
+ end
+
+ it "should handle regexps" do
+ pending "api support"
+ handle(/./).should == /./
+ end
+
+ it "should handle arrays" do
+ handle([1, 2, 3]).should == [1, 2, 3]
+ end
+
+ it "should handle hashes" do
+ handle({:a => 1}).should == {:a => 1}
+ end
+
+ it "should handle structs" do
+ pending
+ end
+
+ it "should handle bignums" do
+ pending
+ end
+
+ it "should handle fixnum" do
+ handle(1).should == 1
+ end
+
+ it "should handle true" do
+ handle(true).should == true
+ end
+
+ it "should handle false" do
+ handle(false).should == false
+ end
+
+ it "should handle undefined" do
+ handle(:undefined).should == :undefined
+ end
+
+ it "should handle symbols" do
+ handle(:symbol).should == :symbol
+ end
+=begin
+ case T_NIL :
+ case T_OBJECT :
+ //case T_CLASS :
+ //case T_MODULE :
+ case T_FLOAT :
+ case T_STRING :
+ case T_REGEXP :
+ case T_ARRAY :
+ case T_HASH :
+ //case T_STRUCT :
+ case T_BIGNUM :
+ case T_FIXNUM :
+ //case T_COMPLEX :
+ //case T_RATIONAL:
+ //case T_FILE :
+ case T_TRUE :
+ case T_FALSE :
+ //case T_DATA :
+ case T_SYMBOL :
+ symbol_from(object)
+ break;
+=end
+ end
+end
View
92 spec/lib/tomato_spec.rb
@@ -0,0 +1,92 @@
+require 'spec_helper'
+
+describe Tomato do
+ subject { Tomato.new }
+
+ it "should have the right V8 version" do
+ subject.version.should == "2.2.18"
+ end
+
+ it "should build an anonymous Ruby object to mirror anonymous JS objects" do
+ pending
+ end
+
+ it "should convert JS regexps to Ruby ones" do
+ pending "api support"
+ subject.run("/./").should == /./
+ end
+
+ it "should raise ArgumentError given too few arguments to #run" do
+ proc { Tomato.new.run }.should raise_error(ArgumentError)
+ end
+
+ it "should raise ArgumentError given too many arguments to #run" do
+ proc { Tomato.new.run(1, 2, 3) }.should raise_error(ArgumentError)
+ end
+
+ it "should raise an error calling a missing method" do
+ proc { subject.run("(nothing());") }.should raise_error(Tomato::Error)
+ end
+
+ it "should raise an error given bad syntax" do
+ proc { subject.run("1a = 3b5;") }.should raise_error(Tomato::Error)
+ end
+
+ it 'should execute basic javascript' do
+ # Basically, this demonstrates that initialization of the engine and actually *running* crap works.
+ # Additionally, "(1+1);" is valid JavaScript so should not raise any other form of error.
+ proc { subject.run("(1+1);").to_s.should == '2' }.should_not raise_error
+ end
+
+ it 'should return nil if result is null' do
+ subject.run("(null);").should be_nil
+ end
+
+ it "should return :undefined if result is undefined" do
+ subject.run("({}.x);").should == :undefined
+ end
+
+ it "should return true if result is true" do
+ subject.run("(true);").should == true
+ end
+
+ it "should return false if result is true" do
+ subject.run("(false);").should == false
+ end
+
+ it "should return a string if the result is a string" do
+ subject.run("('hi');").should == "hi"
+ end
+
+ it "should return a string if the result is a function" do
+ subject.run("(function() { });").should == "function () { }"
+ end
+
+ it "should return an array if the result is an array" do
+ subject.run("([1, 2, 3]);").should == [1, 2, 3]
+ end
+
+ it "should return a Fixnum if the result is an integer" do
+ subject.run("(1);").should == 1
+ subject.run("(1);").should be_kind_of(Fixnum)
+ end
+
+ it "should return a Float if the result is a float or double" do
+ subject.run("(1.05);").should == 1.05
+ subject.run("(1.05);").should be_kind_of(Float)
+ end
+
+ it "should return a Date if the result is a date" do
+ # JavaScript doesn't measure time in microseconds like ruby does, so we have to convert to milliseconds and lose
+ # some precision in the process.
+ time_in_millis = (Time.now.to_f * 1000.0).to_i
+ code = "(new Date(#{time_in_millis}));"
+ (subject.run(code).to_f * 1000.0).to_i.should == time_in_millis
+ end
+
+ it "should handle 64 bit integers" do
+ subject.run("(9223372036854775806);").should == 9223372036854775806
+# /* TODO FIXME: There's no way to know if we're facing a Uint64. It works as a Float but user may not expect a Float... */
+# subject.run("(9223372036854775806);").should_not be_kind_of(Float) # and should be a Bignum, if we're feeling picky.
+ end
+end
View
1  spec/spec_helper.rb
@@ -0,0 +1 @@
+require File.expand_path("../../lib/tomato", __FILE__)
View
10 test/helper.rb
@@ -1,10 +0,0 @@
-require 'rubygems'
-require 'test/unit'
-require 'shoulda'
-
-$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
-$LOAD_PATH.unshift(File.dirname(__FILE__))
-require 'tomato'
-
-class Test::Unit::TestCase
-end
View
7 test/test_tomato.rb
@@ -1,7 +0,0 @@
-require 'helper'
-
-class TestTomato < Test::Unit::TestCase
- should "probably rename this file and start testing for real" do
- flunk "hey buddy, you should probably rename this file and start testing for real"
- end
-end
Please sign in to comment.
Something went wrong with that request. Please try again.