The Harvest #7 - Malleable Application Framework

Pete Hayman — 4th January, 2026

This post and pr #75 marks the completion of the great ECS-ification. The test runner was the final crate built in the hap-hazard 'c++ style oop' and now that it has been converted the beet repo is entirely ECS.

In celebration I'd like to dive a bit deeper into this specific refactor. Its all well and good to make grandious statements how ECS architecture is the bees knees, but these kinds of claims are best served alongside concrete examples.

Before: Seperate Runtimes

The very first thing the original runner does is fork behavior between native and wasm. These two environments are very different, for example a wasm app must first yield to the event loop in order to run async tests, and accomodating for this resulted in two completely different runtimes.

pub fn test_runner(tests: &[&TestDescAndFn]) {	
	#[cfg(target_arch = "wasm32")]
	test_runner_wasm(tests);
	#[cfg(not(target_arch = "wasm32"))]
	test_runner_native(tests);
}

After: Unified Runtime

Bevy has already solved a lot of the cross-platform differences for us, now the test runner is a regular bevy app. This is a shift from a unique and and bespoke implementation, to a standardized well trodden path, reducing bugs and maintenance costs.

pub fn test_runner(tests: &[&TestDescAndFn]) {
	App::new()
		.add_plugins(TestPlugin)
		.spawn(tests_bundle(tests))
		.run();
}

Before: Abstract Traits

Extensibility was always a goal for sweet, the original implementation was full of traits, best-effort attempts to accomodate for current and future needs. A lot of busy-work is involved in moving these strategy types around, and the traits inevitably become tangled as requirements shift.

struct TestHarness {
	suites: Vec<TestSuite>,
	runner: Box<dyn Runner>,
	case_logger: Box<dyn CaseLogger>,
	suite_logger: Box<dyn SuiteLogger>,
}

struct TestSuite {
	cases: Vec<TestCase>,
	runner: Box<dyn Runner>,
	case_logger: Box<dyn CaseLogger>,
	suite_logger: Box<dyn SuiteLogger>,
}

After: Flat System Architecture

The flat architecture of ECS systems is much easier to reason about. For instance the log_case_outcomes system is right there in plain sight, no longer buried under layers of traits and indirection. This also trivializes modding, to use a parallel runner we'd just replace the run_tests_series system.

impl Plugin for TestPlugin {
	fn build(&self, app: &mut App) {
		app.add_systems(
				Update,
				(
					log_suite_running,
					filter_tests,
					log_case_running,
					(run_tests_series, run_non_send_tests_series),
					trigger_timeouts,
					insert_suite_outcome,
					log_case_outcomes,
					log_suite_outcome,
					exit_on_suite_outcome,
				)
					.chain(),
			);
	}
}

Branding

In other news the repo has had a slight adjustment from last months rebranding:

A Malleable Application Framework

Mostly inspired by some recent reading I've been doing:

CodingItWrong - User-modifiable software

This is a great breakdown on two pioneers in malleable applications: smalltalk and hypercard.

InkAndSwitch - Malleable Software

The phrasing used in this essay is really growing on me. The term 'malleable' has this tangible quality I really like, and reflects well this idea of technology that can be bent into shape without breaking.

Maggie Appleton: Bare-foot developers

I think Maggie's concept of 'bare-foot developers' well articulates the semi-technical as a key audience for malleable applications.