Empirical++: Power Tools Go Brrr...
Thanks to the software contributions of our WAVES participants and mentors, we’ve got an exciting slew of shiny new tools to show off. And you’ve got an exciting slew of shiny new things to learn about!
🔗 Prefab
Contributed by Sara and Matthew.
The idea of the prefab project is to provide prepackaged utilities for web apps (like a config panel) built out of Empirical’s web widgets with snazzy, mobile-friendly styling already applied.
You can find a showcase of prefab tools with code snippets about how to use them here.
Pick a prefab element and add it to your evo-algo web app! Maybe add a message explaining what’s going on inside of a bootstrap card? You could even try to put live stats about evolution like a counter of how many generations have elapsed in a different card!
For everything to look right, you’ll have to paste the boilerplate JS and CSS includes at the top of the demo page into your HTML file.
🔗 Installing Test Dependencies
Given the size and complexity of Empirical’s code base, how do we make sure that everything stays in working order? Tests! To get started working on an Empirical project, you’ll want to be familiar with how to run existing tests and how to write new ones.
First thing you’ve got to do is install a few dependencies
Luckily for you, we’ve got Empirical’s dependency install process automated as a make
rule in the base directory of the repo that should just work.
make install-test-dependencies
If that doesn’t just work, you can try hopping into our pre-built Docker container.
You should have better luck with make install-test-dependencies
in there.
sudo docker run -it devosoft/empirical:latest
If you’re installing docker on Ubuntu, be sure to run sudo apt-get install docker.io
not sudo apt-get install docker
.
For other platforms & more detailed instructions, see here.
🔗 Run All the Tests
To run our existing battery of native tests, you’ll just
cd tests
make
To run the tests for a particular source subdirectory, just cd
into the corresponding subdirectory in tests
and make
.
For example, to run the tests for base
cd tests/base
make
Those options probably take too long, though.
You’re more likely to find running the tests for a particular source file more useful.
To do that, cd
into its subdirectory and make test-BasenameWithoutExtension
.
For example, to test base/vector.hpp
you’d
cd tests/base
make test-vector
🔗 Anatomy of a Native Test
What’s actually going on inside these tests? Getting you to Catch the drift will REQUIRE popping one open for a closer look.
Hopefully, those puns will be very funny to you in a minute.
We use the Catch framework for our native tests.
Let’s take a look at tests/math/info_theory.cpp
.
#define CATCH_CONFIG_MAIN // Catch magic
#include "third-party/Catch/single_include/catch2/catch.hpp"
#include "emp/math/info_theory.hpp"
#include <sstream>
#include <string>
// Catch uses macro magic to string these TEST_CASE's together.
// You can provide several TEST_CASE's within the same file.
TEST_CASE("Test info_theory", "[tools]")
{
emp::vector<int> weights = { 100, 100, 200 };
// REQUIRE is a special Catch macro that must evaluate to true
// or the executable exits with an error code
REQUIRE( emp::Entropy(weights) == 1.5 );
}
Some portions of Empirical’s unit tests are kind of a hot mess and shouldn’t be used as a “"”good””” example of how to write unit tests.
Generally, try to have small, descriptive TEST_CASE
’s and be sure to include lots of inline comments that describe what you’re testing and why.
🔗 Adding a Native Test
Let’s add a header file inside of include/emp/math/
called dumb_add.hpp
.
#pragma once
namespace emp {
size_t dumb_add(const size_t first, const size_t second) {
if (second) return dumb_add(first+1, second-1);
else return first;
}
}
Now we need to test it!
Open up tests/math/dumb_add.cpp
as a new file and add some tests.
You’ll need to edit the first line of the Makefile
inside of tests/math/
(i.e., TEST_NAMES = ...
) to add dumb_add
.
🔗 Run the Web Tests
To run our existing battery of web tests, you’d
cd tests/web
make
To run the tests for a particular header, for example Widget
, just
cd tests/web
make test-web-Widget
There’s also DOM tests for prefab
in addition to web
inside tests/web
right now.
🔗 Anatomy of a Web Test
Let’s take a peek into tests/web/Widget.cpp
.
Includes… gotta have ‘em!
#include <functional>
#include <unordered_map>
#include "emp/base/assert.hpp"
#include "emp/web/_MochaTestRunner.hpp"
#include "emp/web/Document.hpp"
#include "emp/web/Element.hpp"
#include "emp/web/web.hpp"
Today, we’re going to be
paintingcoding a happy littletreestruct. There’s nothing wrong with having atreestruct as a friend.—
Bob RossMatthew Andres Moreno
This struct will inherit from BaseTest
, which handles much of the boilerplate for us behind the scenes.
struct Test_WidgetWrapWith : emp::web::BaseTest {
Step the first: set up some DOM state in our constructor.
// Construct the following HTML structure:
// <div id="wrapper2"> <!-- inserted with WrapWith -->
// <p id="parent">
// parent
// <div id="wrapper"> <!-- inserted with WrapWith -->
// wrapper
// <button id="child"></button>
// </div>
// </p>
// <br/><br/>
// </div>
Test_WidgetWrapWith()
: BaseTest({"emp_test_container"})
{
emp::web::Element parent("p", "parent");
parent << "parent";
Doc("emp_test_container") << parent;
emp::web::Button child(
[]() {
EM_ASM({
$("#child_button").attr("clicked", "yes");
});
},
"child"
);
child.SetAttr("clicked", "no");
child.SetAttr("id", "child_button");
parent << child;
emp::web::Div wrapper("wrapper");
wrapper << "wrapper";
child.WrapWith(wrapper);
parent.WrapWith(
emp::web::Div("wrapper2").SetCSS("background-color", "red")
).SetCSS("background-color", "blue");
Doc("emp_test_container").Div("wrapper2") << "<br/><br/>";
}
Step the second: make sure the DOM state ended up the way we expected it to.
Everything inside the EM_ASM
is actually JavaScript, not C++.
It’s built using the Mocha test framework and Chai BDD/TDD assertion library, so if you’re curious about the syntax kicking around or you want to learn even more fancy footwork take a peek over there.
void Describe() override {
// Test that the HTML components created in constructor are correct.
EM_ASM({
describe("Widget::WrapWith", function() {
describe("#wrapper2", function() {
it('should have parent #emp_test_container', function() {
const parent_id = $("#wrapper2").parent().attr("id");
chai.assert.equal(parent_id, "emp_test_container");
});
it('should have child #parent', function() {
var children = $("#wrapper2").children();
// Get ids of child
var child_ids = [];
for (i = 0; i < children.length; i++) {
child_ids.push(children[i].getAttribute("id"));
}
chai.assert.include(child_ids, "parent");
chai.assert.equal($("#wrapper2").children("#parent").length, 1);
});
});
describe("#parent", function() {
it('should have parent #wrapper2', function() {
const parent_id = $("#parent").parent().attr("id");
chai.assert.equal(parent_id, "wrapper2");
});
it('should have child #wrapper', function() {
chai.assert.equal($("#parent").children("#wrapper").length, 1);
});
});
describe("#wrapper", function() {
it('should have parent #parent', function() {
const parent_id = $("#wrapper").parent().attr("id");
chai.assert.equal(parent_id, "parent");
});
it('should have child #child_button', function() {
chai.assert.equal($("#wrapper").children("#child_button").length, 1);
});
});
describe("#child_button", function() {
it('should have parent #wrapper', function() {
const parent_id = $("#child_button").parent().attr("id");
chai.assert.equal(parent_id, "wrapper");
});
});
describe("button#child_button", function() {
it('should do stuff when clicked', function() {
const before = $("#child_button").attr("clicked");
chai.assert.equal(before, "no", "check initial clicked value");
$("#child_button").trigger( "click" );
const after = $("#child_button").attr("clicked");
chai.assert.equal(after, "yes", "check post-click clicked value");
});
});
});
});
}
Tie up our happy little struct.
};
All that’s left is to
- instantiate a
emp::web::MochaTestRunner
(ahoy, more boilerplate-hiding magics), - give it a vector of
div
’s that need to be in our HTML document before our happy little struct runs, - register our happy little struct with the test runner, and
- hit the go button.
emp::web::MochaTestRunner test_runner;
int main() {
// put a div with id emp_test_container into our HTML document
test_runner.Initialize({"emp_test_container"});
test_runner.AddTest<Test_WidgetWrapWith>("Test Widget::WrapWith");
test_runner.Run();
}
To run this test, you’ll use the make command make test-web-Widget
inside tests/web
.
🔗 Layin’ Down the Law
Github Actions yells at us when tests don’t pass.
CodeCov yells at us when we don’t write tests. You can use their web interface to check which code lines of your pull request haven’t been tested yet.