WIP: LVS rerun feature

This commit is contained in:
Matthias Koefferlein 2019-08-25 21:55:48 +02:00
parent 515b68b76f
commit 441f946f43
16 changed files with 239 additions and 68 deletions

View File

@ -20,6 +20,9 @@ module DRC
cv = RBA::CellView::active
@generator = ""
@rdb_index = nil
@l2ndb_index = nil
@def_layout = cv && cv.layout
@def_cell = cv && cv.cell
@def_path = cv && cv.filename
@ -780,7 +783,7 @@ module DRC
cn || raise("No cell name specified - either the source was not specified before 'report' or there is no default source. In the latter case, specify a cell name as the third parameter of 'report'")
@output_rdb_cell = @output_rdb.create_cell(cn)
@output_rdb.generator = $0
@output_rdb.generator = self._generator
@output_rdb.top_cell_name = cn
@output_rdb.description = description
@ -1466,7 +1469,11 @@ CODE
# NOTE: to prevent the netter destroying the database, we need to take it
l2ndb = _take_data
l2ndb_index = view.add_l2ndb(l2ndb)
if self._l2ndb_index
l2ndb_index = view.replace_l2ndb(self._l2ndb_index, l2ndb)
else
l2ndb_index = view.add_l2ndb(l2ndb)
end
view.show_l2ndb(l2ndb_index, view.active_cellview_index)
end
@ -1551,6 +1558,30 @@ CODE
end
end
def _generator
@generator
end
def _generator=(g)
@generator = g
end
def _rdb_index
@rdb_index
end
def _rdb_index=(i)
@rdb_index = i
end
def _l2ndb_index
@l2ndb_index
end
def _l2ndb_index=(i)
@l2ndb_index = i
end
private
def _make_string(v)
@ -1717,7 +1748,7 @@ CODE
@layout_sources[name] = src
src
end
end
end

View File

@ -407,7 +407,7 @@ module DRC
@l2n = RBA::LayoutToNetlist::new(layout.top_cell.name, layout.dbu)
end
@l2n.generator = $0
@l2n.generator = @engine._generator
end

View File

@ -642,13 +642,15 @@ Class<tl::GlobPattern> decl_GlobPattern ("tl", "GlobPattern",
);
class Recipe_Impl
: public tl::Recipe
: public tl::Recipe, public gsi::ObjectBase
{
public:
Recipe_Impl (const std::string &name, const std::string &description)
: tl::Recipe (name, description)
{
// .. nothing yet ..
// makes the object owned by the C++ side (registrar). This way we don't need to keep a
// singleton instance.
keep ();
}
virtual tl::Variant execute (const std::map<std::string, tl::Variant> &params) const
@ -688,9 +690,11 @@ Class<Recipe_Impl> decl_Recipe_Impl ("tl", "Recipe",
gsi::method ("description", &Recipe_Impl::description,
"@brief Gets the description of the recipe."
) +
gsi::method ("make", &Recipe_Impl::make, gsi::arg ("generator"),
gsi::method ("make", &Recipe_Impl::make, gsi::arg ("generator"), gsi::arg ("add_params", std::map<std::string, tl::Variant> ()),
"@brief Executes the recipe given by the generator string.\n"
"The generator string is the one delivered with \\generator."
"The generator string is the one delivered with \\generator.\n"
"Additional parameters can be passed in \"add_params\". They have lower priority than the parameters "
"kept inside the generator string."
) +
gsi::method ("generator", &Recipe_Impl::generator, gsi::arg ("params"),
"@brief Delivers the generator string from the given parameters.\n"
@ -711,9 +715,9 @@ Class<Recipe_Impl> decl_Recipe_Impl ("tl", "Recipe",
"user requests a re-run of the DRC, the recipe will be called and \n"
"the implementation is supposed to deliver a new database.\n"
"\n"
"To register a recipe, reimplement tl::Recipe and create a singleton\n"
"To register a recipe, reimplement the Recipe class and create an\n"
"instance. To serialize a recipe, use \"generator\", to execute the\n"
"recipe, use \"make\". \n"
"recipe, use \"make\".\n"
"\n"
"Parameters are kept as a generic key/value map.\n"
"\n"

View File

@ -141,6 +141,9 @@
<iconset resource="../../lay/lay/layResources.qrc">
<normaloff>:/run.png</normaloff>:/run.png</iconset>
</property>
<property name="shortcut">
<string>F5</string>
</property>
<property name="autoRaise">
<bool>true</bool>
</property>

View File

@ -218,11 +218,17 @@ static db::LayoutVsSchematic *get_lvsdb (lay::LayoutView *view, unsigned int ind
return dynamic_cast<db::LayoutVsSchematic *> (db);
}
static void add_lvsdb (lay::LayoutView *view, db::LayoutVsSchematic *lvsdb)
static unsigned int add_lvsdb (lay::LayoutView *view, db::LayoutVsSchematic *lvsdb)
{
view->add_l2ndb (lvsdb);
return view->add_l2ndb (lvsdb);
}
static unsigned int replace_lvsdb (lay::LayoutView *view, unsigned int db_index, db::LayoutVsSchematic *lvsdb)
{
return view->replace_l2ndb (db_index, lvsdb);
}
// this binding returns a const pointer which is not converted into a copy by RBA
static lay::LayerPropertiesNodeRef insert_layer1 (lay::LayoutView *view, const lay::LayerPropertiesConstIterator &iter, const lay::LayerProperties &props)
{
@ -1499,6 +1505,15 @@ Class<lay::LayoutView> decl_LayoutView (QT_EXTERNAL_BASE (QWidget) "lay", "Layou
"\n"
"This method has been added in version 0.26."
) +
gsi::method ("replace_l2ndb", &lay::LayoutView::replace_l2ndb, gsi::arg ("db_index"), gsi::arg ("db"),
"@brief Replaces the database with the given index\n"
"\n"
"If the index is not valid, the database will be added to the view (see \\add_lvsdb).\n"
"\n"
"@return The index of the database within the view (see \\lvsdb)\n"
"\n"
"This method has been added in version 0.26."
) +
gsi::method_ext ("create_l2ndb", &create_l2ndb, gsi::arg ("name"),
"@brief Creates a new netlist database and returns the index of the new database\n"
"@param name The name of the new netlist database\n"
@ -1532,6 +1547,15 @@ Class<lay::LayoutView> decl_LayoutView (QT_EXTERNAL_BASE (QWidget) "lay", "Layou
"\n"
"This method has been added in version 0.26."
) +
gsi::method_ext ("replace_lvsdb", &replace_lvsdb, gsi::arg ("db_index"), gsi::arg ("db"),
"@brief Replaces the database with the given index\n"
"\n"
"If the index is not valid, the database will be added to the view (see \\add_lvsdb).\n"
"\n"
"@return The index of the database within the view (see \\lvsdb)\n"
"\n"
"This method has been added in version 0.26."
) +
gsi::method_ext ("create_lvsdb", &create_lvsdb, gsi::arg ("name"),
"@brief Creates a new netlist database and returns the index of the new database\n"
"@param name The name of the new netlist database\n"

View File

@ -7207,6 +7207,37 @@ LayoutView::add_l2ndb (db::LayoutToNetlist *l2ndb)
return (unsigned int)(m_l2ndbs.size () - 1);
}
unsigned int
LayoutView::replace_l2ndb (unsigned int db_index, db::LayoutToNetlist *l2ndb)
{
if (db_index < m_l2ndbs.size ()) {
std::string n = m_l2ndbs [db_index]->name ();
delete m_l2ndbs [db_index];
m_l2ndbs.erase (m_l2ndbs.begin () + db_index);
// keep old name if possible
if (l2ndb->name ().empty ()) {
l2ndb->set_name (n);
} else {
make_unique_name (l2ndb, m_l2ndbs.begin (), m_l2ndbs.end ());
}
m_l2ndbs.insert (m_l2ndbs.begin () + db_index, l2ndb);
// Mark this object as owned by us (for GSI)
l2ndb->keep ();
l2ndb_list_changed_event ();
return db_index;
} else {
return add_l2ndb (l2ndb);
}
}
db::LayoutToNetlist *
LayoutView::get_l2ndb (int index)
{

View File

@ -2340,6 +2340,17 @@ public:
*/
unsigned int add_l2ndb (db::LayoutToNetlist *l2ndb);
/**
* @brief Replaces a Netlist database
*
* The layout view will become owner of the database.
* If the index is not valid, the database will be added and the new index will be returned.
*
* @param db_index The index of the database to replace
* @param l2ndb The database to add
*/
unsigned int replace_l2ndb (unsigned int db_index, db::LayoutToNetlist *l2ndb);
/**
* @brief Get the netlist database by index
*

View File

@ -30,9 +30,9 @@
#include "layMarker.h"
#include "layNetInfoDialog.h"
#include "layNetExportDialog.h"
#include "lymMacro.h"
#include "tlProgress.h"
#include "tlExceptions.h"
#include "tlRecipe.h"
#include "dbLayoutToNetlist.h"
#include "dbNetlistDeviceClasses.h"
#include "dbCellMapping.h"
@ -600,13 +600,17 @@ NetlistBrowserPage::rerun_macro ()
{
if (! mp_database->generator ().empty ()) {
lym::Macro *generator = lym::MacroCollection::root ().find_macro (mp_database->generator ());
if (! generator) {
throw tl::Exception (tl::sprintf (tl::to_string (tr ("Cannot find generator script: %s")), mp_database->generator()));
} else {
generator->run ();
std::map<std::string, tl::Variant> add_pars;
for (unsigned int i = 0; i < mp_view->num_l2ndbs (); ++i) {
if (mp_view->get_l2ndb (i) == mp_database.get ()) {
add_pars["l2ndb_index"] = tl::Variant (int (i));
break;
}
}
tl::Recipe::make (mp_database->generator (), add_pars);
}
}
void
@ -767,7 +771,11 @@ NetlistBrowserPage::set_db (db::LayoutToNetlist *l2ndb)
rerun_button->setEnabled (mp_database.get () && ! mp_database->generator ().empty ());
if (rerun_button->isEnabled ()) {
rerun_button->setToolTip (tl::to_qstring (tl::to_string (tr ("Run ")) + mp_database->generator ()));
QString shortcut;
if (! rerun_button->shortcut ().isEmpty ()) {
shortcut = QString::fromUtf8 (" (%1)").arg (rerun_button->shortcut ().toString ());
}
rerun_button->setToolTip (tl::to_qstring (tl::to_string (tr ("Run ")) + mp_database->generator ()) + shortcut);
} else {
rerun_button->setToolTip (QString ());
}

View File

@ -61,7 +61,7 @@ module LVS
@lvs = RBA::LayoutVsSchematic::new(cell.name, layout.dbu)
end
@lvs.generator = $0
@lvs.generator = @engine._generator
@l2n = @lvs
@comparer = RBA::NetlistComparer::new

View File

@ -17,37 +17,39 @@
<text>
module LVS
def LVS.execute_lvs(_macro)
def self.execute_lvs(macro, generator, l2ndb_index = nil)
_timer = RBA::Timer::new
_timer.start
_lvs = LVSEngine::new
timer = RBA::Timer::new
timer.start
lvs = LVSEngine::new
lvs._l2ndb_index = l2ndb_index
lvs._generator = generator
begin
# Set a debugger scope so that our errors end up with the debugger set to the LVS's line
RBA::MacroExecutionContext::set_debugger_scope(_macro.path)
RBA::MacroExecutionContext::set_debugger_scope(macro.path)
# No verbosity set in lvs engine - we cannot use the engine's logger
RBA::Logger::verbosity &gt;= 10 &amp;&amp; RBA::Logger::info("Running #{_macro.path}")
_lvs.instance_eval(_macro.text, _macro.path)
RBA::Logger::verbosity &gt;= 10 &amp;&amp; RBA::Logger::info("Running #{macro.path}")
lvs.instance_eval(macro.text, macro.path)
# Remove the debugger scope
RBA::MacroExecutionContext::remove_debugger_scope
rescue =&gt; ex
_lvs.error("In #{_macro.path}: #{ex.to_s}")
lvs.error("In #{macro.path}: #{ex.to_s}")
RBA::MacroExecutionContext::ignore_next_exception
raise ex
ensure
# cleans up and creates layout and report views
_lvs._finish
lvs._finish
end
_timer.stop
_lvs.info("Total run time: #{'%.3f'%(_timer.sys+_timer.user)}s")
timer.stop
lvs.info("Total run time: #{'%.3f'%(timer.sys+timer.user)}s")
end
@ -55,7 +57,9 @@ module LVS
class LVSInterpreter &lt; RBA::MacroInterpreter
# Constructor
def initialize
def initialize(recipe)
@recipe = recipe
# Make the DSL use ruby syntax highlighting
self.syntax_scheme = "ruby"
@ -80,8 +84,7 @@ module LVS
# Implements the execute method
def execute(macro)
$0 = macro.path
LVS::execute_lvs(macro)
LVS::execute_lvs(macro, @recipe.generator("script" => macro.path))
end
end
@ -90,7 +93,9 @@ module LVS
class LVSPlainTextInterpreter &lt; RBA::MacroInterpreter
# Constructor
def initialize
def initialize(recipe)
@recipe = recipe
# Make the DSL use ruby syntax highlighting
self.syntax_scheme = "ruby"
@ -106,15 +111,41 @@ module LVS
# Implements the execute method
def execute(macro)
LVS::execute_lvs(macro)
LVS::execute_lvs(macro, @recipe.generator("script" => macro.path))
end
end
# A recipe implementation allowing the LVS run to be redone
class LVSRecipe &lt; RBA::Recipe
def initialize
super("lvs", "LVS recipe")
end
def execute(params)
script = params["script"]
if ! script
return
end
macro = RBA::Macro::macro_by_path(script)
macro || raise("Can't find LVS script #{script} - unable to re-run")
LVS::execute_lvs(macro, self.generator("script" => script), params["l2ndb_index"])
end
end
# Register the new interpreters
LVSInterpreter::new
LVSPlainTextInterpreter::new
# Register the recipe
lvs_recipe = LVSRecipe::new
# Register the new interpreters
LVSInterpreter::new(lvs_recipe)
LVSPlainTextInterpreter::new(lvs_recipe)
end
</text>
</klayout-macro>

View File

@ -73,9 +73,8 @@ public:
};
Class<gsi::MacroExecutionContext> decl_MacroExecutionContext ("lay", "MacroExecutionContext",
gsi::method ("set_debugger_scope", &gsi::MacroExecutionContext::set_debugger_scope,
gsi::method ("set_debugger_scope", &gsi::MacroExecutionContext::set_debugger_scope, gsi::arg ("filename"),
"@brief Sets a debugger scope (file level which shall appear in the debugger)\n"
"@args filename\n"
"If a debugger scope is set, back traces will be produced starting from that scope. "
"Setting a scope is useful for implementing DSL interpreters and giving a proper hint about "
"the original location of an error."
@ -277,9 +276,8 @@ Class<gsi::MacroInterpreter> decl_MacroInterpreter ("lay", "MacroInterpreter",
gsi::method ("NoDebugger", &const_NoDebugger,
"@brief Indicates no debugging for \\debugger_scheme\n"
) +
gsi::method ("register", &MacroInterpreter::register_gsi,
gsi::method ("register", &MacroInterpreter::register_gsi, gsi::arg ("name"),
"@brief Registers the macro interpreter\n"
"@args name\n"
"@param name The interpreter name. This is an arbitrary string which should be unique.\n"
"\n"
"Registration of the interpreter makes the object known to the system. After registration, macros whose interpreter "
@ -351,9 +349,8 @@ Class<gsi::MacroInterpreter> decl_MacroInterpreter ("lay", "MacroInterpreter",
"Before version 0.25 this attribute was a reimplementable method. It has been turned into an attribute for "
"performance reasons in version 0.25.\n"
) +
gsi::callback ("execute", &gsi::MacroInterpreter::execute, &gsi::MacroInterpreter::f_execute,
gsi::callback ("execute", &gsi::MacroInterpreter::execute, &gsi::MacroInterpreter::f_execute, gsi::arg ("macro"),
"@brief Gets called to execute a macro\n"
"@args macro\n"
"This method must be reimplemented to execute the macro. "
"The system will call this script when a macro with interpreter type 'dsl' and the "
"name of this interpreter is run."
@ -424,6 +421,11 @@ Class<gsi::MacroInterpreter> decl_MacroInterpreter ("lay", "MacroInterpreter",
"This class has been introduced in version 0.23.\n"
);
static lym::Macro *macro_by_path (const std::string &path)
{
return lym::MacroCollection::root ().find_macro (path);
}
Class<lym::Macro> decl_Macro ("lay", "Macro",
gsi::method ("path", &lym::Macro::path,
"@brief Gets the path of the macro\n"
@ -431,6 +433,13 @@ Class<lym::Macro> decl_Macro ("lay", "Macro",
"The path is the path where the macro is stored, starting with an abstract group identifier. "
"The path is used to identify the macro in the debugger for example."
) +
gsi::method ("macro_by_path", &macro_by_path, gsi::arg ("path"),
"@brief Finds the macro by installation path\n"
"\n"
"Returns nil if no macro with this path can be found.\n"
"\n"
"This method has been added in version 0.26."
) +
gsi::method ("name", &lym::Macro::name,
"@brief Gets the name of the macro\n"
"\n"
@ -443,9 +452,8 @@ Class<lym::Macro> decl_Macro ("lay", "Macro",
"the description text can have the format \"Group;;Description\". In that case, the macro "
"will appear in a group with title \"Group\"."
) +
gsi::method ("description=", &lym::Macro::set_description,
gsi::method ("description=", &lym::Macro::set_description, gsi::arg ("description"),
"@brief Sets the description text\n"
"@args description\n"
"@param description The description text.\n"
"See \\description for details.\n"
) +
@ -455,9 +463,8 @@ Class<lym::Macro> decl_Macro ("lay", "Macro",
"The prolog is executed before the actual code is executed. Interpretation depends on the "
"implementation of the DSL interpreter for DSL macros."
) +
gsi::method ("prolog=", &lym::Macro::set_prolog,
gsi::method ("prolog=", &lym::Macro::set_prolog, gsi::arg ("string"),
"@brief Sets the prolog\n"
"@args string\n"
"See \\prolog for details.\n"
) +
gsi::method ("epilog", &lym::Macro::epilog,
@ -466,9 +473,8 @@ Class<lym::Macro> decl_Macro ("lay", "Macro",
"The epilog is executed after the actual code is executed. Interpretation depends on the "
"implementation of the DSL interpreter for DSL macros."
) +
gsi::method ("epilog=", &lym::Macro::set_epilog,
gsi::method ("epilog=", &lym::Macro::set_epilog, gsi::arg ("string"),
"@brief Sets the epilog\n"
"@args string\n"
"See \\epilog for details.\n"
) +
gsi::method ("category", &lym::Macro::category,
@ -477,9 +483,8 @@ Class<lym::Macro> decl_Macro ("lay", "Macro",
"The category tags string indicates to which categories a macro will belong to. This string "
"is only used for templates currently and is a comma-separated list of category names."
) +
gsi::method ("category=", &lym::Macro::set_category,
gsi::method ("category=", &lym::Macro::set_category, gsi::arg ("string"),
"@brief Sets the category tags string\n"
"@args string\n"
"See \\category for details.\n"
) +
gsi::method ("text", &lym::Macro::text,
@ -488,17 +493,15 @@ Class<lym::Macro> decl_Macro ("lay", "Macro",
"The text is the code executed by the macro interpreter. "
"Depending on the DSL interpreter, the text can be any kind of code."
) +
gsi::method ("text=", &lym::Macro::set_text,
gsi::method ("text=", &lym::Macro::set_text, gsi::arg ("string"),
"@brief Sets the macro text\n"
"@args string\n"
"See \\text for details.\n"
) +
gsi::method ("show_in_menu?", &lym::Macro::show_in_menu,
"@brief Gets a value indicating whether the macro shall be shown in the menu\n"
) +
gsi::method ("show_in_menu=", &lym::Macro::set_show_in_menu,
gsi::method ("show_in_menu=", &lym::Macro::set_show_in_menu, gsi::arg ("flag"),
"@brief Sets a value indicating whether the macro shall be shown in the menu\n"
"@args flag\n"
) +
gsi::method ("group_name", &lym::Macro::group_name,
"@brief Gets the menu group name\n"
@ -506,9 +509,8 @@ Class<lym::Macro> decl_Macro ("lay", "Macro",
"If a group name is specified and \\show_in_menu? is true, the macro will appear in "
"a separate group (separated by a separator) together with other macros sharing the same group."
) +
gsi::method ("group_name=", &lym::Macro::set_group_name,
gsi::method ("group_name=", &lym::Macro::set_group_name, gsi::arg ("string"),
"@brief Sets the menu group name\n"
"@args string\n"
"See \\group_name for details.\n"
) +
gsi::method ("menu_path", &lym::Macro::menu_path,
@ -517,9 +519,8 @@ Class<lym::Macro> decl_Macro ("lay", "Macro",
"If a menu path is specified and \\show_in_menu? is true, the macro will appear in "
"the menu at the specified position."
) +
gsi::method ("menu_path=", &lym::Macro::set_menu_path,
gsi::method ("menu_path=", &lym::Macro::set_menu_path, gsi::arg ("string"),
"@brief Sets the menu path\n"
"@args string\n"
"See \\menu_path for details.\n"
),
"@brief A macro class\n"

View File

@ -350,8 +350,14 @@ struct writer<gsi::VectorType>
aa->write<void *> ((void *)0);
}
} else {
if (TYPE (arg) != T_ARRAY) {
throw tl::Exception (tl::sprintf (tl::to_string (tr ("Unexpected object type (expected array, got %s)")), rba_class_name (arg).c_str ()));
}
tl_assert (atype.inner () != 0);
aa->write<void *> ((void *)new RubyBasedVectorAdaptor (arg, atype.inner ()));
}
}
};
@ -370,10 +376,17 @@ struct writer<gsi::MapType>
} else {
aa->write<void *> ((void *)0);
}
} else {
if (TYPE (arg) != T_HASH) {
throw tl::Exception (tl::sprintf (tl::to_string (tr ("Unexpected object type (expected hash, got %s)")), rba_class_name (arg).c_str ()));
}
tl_assert (atype.inner () != 0);
tl_assert (atype.inner_k () != 0);
aa->write<void *> ((void *)new RubyBasedMapAdaptor (arg, atype.inner (), atype.inner_k ()));
}
}
};

View File

@ -51,7 +51,7 @@ std::string Recipe::generator (const std::map<std::string, tl::Variant> &params)
return g;
}
tl::Variant Recipe::make (const std::string &generator)
tl::Variant Recipe::make (const std::string &generator, const std::map<std::string, tl::Variant> &padd)
{
tl::Extractor ex (generator.c_str ());
@ -70,6 +70,10 @@ tl::Variant Recipe::make (const std::string &generator)
params.insert (std::make_pair (key, v));
}
for (std::map<std::string, tl::Variant>::const_iterator p = padd.begin (); p != padd.end (); ++p) {
params.insert (*p);
}
tl::Recipe *recipe_obj = 0;
for (tl::Registrar<tl::Recipe>::iterator r = tl::Registrar<tl::Recipe>::begin (); r != tl::Registrar<tl::Recipe>::end (); ++r) {
if (r->name () == recipe) {

View File

@ -104,8 +104,10 @@ public:
* @brief Executes the recipe from the generator
*
* Returns nil if the recipe can't be executed, e.g. because the recipe isn't known.
* Additional parameters can be passed in the second argument.
* They have lower priority than the parameters kept in the generator argument.
*/
static tl::Variant make (const std::string &generator);
static tl::Variant make (const std::string &generator, const std::map<std::string, tl::Variant> &params = std::map<std::string, tl::Variant> ());
/**
* @brief Recipe interface: executes the recipe with the given parameters

View File

@ -36,7 +36,8 @@ namespace {
{
int a = get_value (params, "A", 0);
double b = get_value (params, "B", 0.0);
return tl::Variant (b * a);
double c = get_value (params, "C", 1.0);
return tl::Variant (b * a * c);
}
};
@ -55,4 +56,9 @@ TEST(1)
tl::Variant res = tl::Recipe::make (g);
EXPECT_EQ (res.to_double (), 42.0);
std::map<std::string, tl::Variant> padd;
padd["C"] = tl::Variant(1.5);
res = tl::Recipe::make (g, padd);
EXPECT_EQ (res.to_double (), 63.0);
}

View File

@ -265,13 +265,14 @@ class Tl_TestClass < TestBase
class MyRecipe < RBA::Recipe
def initialize
super("test_recipe", "description")
super("rba_test_recipe", "description")
end
def execute(params)
a = params["A"] || 0
b = params["B"] || 0.0
b * a
c = params["C"] || 1.0
b * a * c
end
end
@ -285,12 +286,13 @@ class Tl_TestClass < TestBase
my_recipe = MyRecipe::new
my_recipe._create # makes debugging easier
assert_equal(my_recipe.name, "test_recipe")
assert_equal(my_recipe.name, "rba_test_recipe")
assert_equal(my_recipe.description, "description")
g = my_recipe.generator("A" => 6, "B" => 7.0)
assert_equal(g, "test_recipe: A=#6,B=##7")
assert_equal(g, "rba_test_recipe: A=#6,B=##7")
assert_equal("%g" % RBA::Recipe::make(g).to_s, "42")
assert_equal("%g" % RBA::Recipe::make(g, "C" => 1.5).to_s, "63")
my_recipe._destroy
my_recipe = nil