Clone in a terminal:
TestSupplement.vala
/*
    Copyright (C) 2012 Johan Mattsson

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

namespace Supplement {

class Test : Object {
	public Callback callback;
	public string name;
	
	public Test (Callback callback, string name) {
		this.callback = callback;
		this.name = name;
	}
}

/** Class for executing tests cases. */
class TestSupplement : GLib.Object {

	const int NOT_STARTED = 0;
	const int RUNNING = 1;
	const int PAUSED = 2;
	const int DONE = 3;
	
	static int state = NOT_STARTED;

	TestCases tests;
	
	unowned List<Test> test_cases;
	unowned List<Test?>? current_case = null;

	unowned List<Test> passed;
	unowned List<Test> failed;
	unowned List<Test> skipped;

	static TestSupplement? singleton = null;

	bool has_failed = false;
	bool has_skipped = false;
		
	static bool slow_test = false;
	
	string test_cases_to_run; // name of specific test case or all to run all test cases
	
	public TestSupplement () {
		assert (singleton == null);
		tests = new TestCases ();
		test_cases = tests.get_test_functions ();
		current_case = test_cases.first ();
		test_cases_to_run = "All";
		
		from_command_line ();
	}
	
	public static bool is_slow_test () {
		return slow_test;
	}
	
	public static void set_slow_test (bool s) {
		slow_test = s;
	}
	
	public static TestSupplement get_singleton () {
		if (singleton == null) {
			singleton = new TestSupplement ();
		}
		
		return (!) singleton;
	}

	private bool has_test_case (string s) {
		foreach (var t in test_cases) {
			if (t.name == s) return true;
		}	
		
		if (s == "" || s == "All") {
			print ("No specific tescase given run all test cases.\n");
			return true;
		}
		
		return false;
	}

	/** Run only test specified on the command line. */
	private void from_command_line () {
		string? stn = Supplement.get_argument ("--test");
	
		if (stn != null) {
			string st = (!) stn;
			
			if (!has_test_case (st)) {
				stderr.printf (@"Test case \"$st\" does not exist.\n");
				stderr.printf ("\nAvaliable test cases:\n");
				
				foreach (var t in test_cases) {
					stderr.printf (t.name);
					stderr.printf ("\n");
				}
				
				Process.exit(1);
			}

			if (st == "All" || st == "") {
				return;
			} else {
				stderr.printf  (@"Run only test case \"$st\" \n");
			}
			
			test_cases_to_run = st;
		}
	}

	public static void log (string? log_domain, LogLevelFlags log_levels, string message) {		
		Test t = (!)((!)get_singleton ().current_case).data;
		
		if (log_domain != null) {
			stderr.printf ("%s: \n", (!) log_domain);
		}
		
		stderr.printf ("Testcase \"%s\" failed because:\n", t.name);
		stderr.printf ("%s\n\n", message);	
		
		get_singleton ().has_failed = true;
		
		assert (!Supplement.fatal_wanings);
	}

	public static void @continue () {
		int s = AtomicInt.get (ref state);
		if (s == DONE) {
			singleton = null;
		}

		TestSupplement t = get_singleton ();
		
		LogLevelFlags levels = LogLevelFlags.LEVEL_ERROR | LogLevelFlags.LEVEL_CRITICAL | LogLevelFlags.LEVEL_WARNING;
		Log.set_handler (null, levels, log);
				
		AtomicInt.set (ref state, RUNNING);
		t.run_all_tests ();
	}

	public static bool is_running () {
		int r = AtomicInt.get (ref state);
		return (r == RUNNING);
	}

	public static void pause () {
		AtomicInt.set (ref state, PAUSED);
		Log.set_handler (null, LogLevelFlags.LEVEL_MASK, Log.default_handler);
	}

	private static void pad (int t) {
		for (int i = 0; i < t; i++) stdout.printf (" ");
	}

	public void print_result () {
		stdout.printf ("\n");
		stdout.printf ("Testcase results:\n");

		foreach (var t in skipped) {
			stdout.printf ("%s", t.name);
			pad (40 - t.name.char_count());
			stdout.printf ("Skipped\n");
		}
		
		if (skipped.length () > 0) {
			stdout.printf ("\n");
		}
		
		foreach (var t in passed) {
			stdout.printf ("%s", t.name);
			pad (40 - t.name.char_count());
			stdout.printf ("Passed\n");
		}
		
		foreach (var t in failed) {
			stdout.printf ("%s", t.name);
			pad (40 - t.name.char_count());
			stdout.printf ("Failed\n");
		}
		
		stdout.printf ("\n");
		
		stdout.printf ("Total %u test cases executed, %u passed and %u failed.\n", (passed.length () + failed.length ()), passed.length (), failed.length ());
	}

	/** Run tests in main loop. */
	public void run_all_tests () {
		var idle = new TimeoutSource (20);
		
		idle.set_callback(() => {
			int s = AtomicInt.get (ref state);
			
			if (s != RUNNING || current_case == null) {
				return false;
			}

			Test test = (!)((!) current_case).data;
					
			has_failed = false;
			has_skipped = false;

			if (test_cases_to_run != "All" && test_cases_to_run != test.name) {
				has_skipped = true;
			} else {
				test.callback ();
			}
			
			if (has_failed) {
				failed.append ((!) test);
				
				if (Supplement.has_argument ("--exit")) {
					print_result ();
					Process.exit (1);
				}
	
			} else if (has_skipped) {
				skipped.append ((!) test);
			} else {
				passed.append ((!) test);
			}

			if (unlikely (current_case == test_cases.last ())) {
				stdout.printf ("Finished running test suite.\n");			
				
				AtomicInt.set (ref state, DONE);
				Log.set_handler (null, LogLevelFlags.LEVEL_MASK, Log.default_handler);

				print_result ();
				
				if (Supplement.has_argument ("--exit")) {
					print_result ();
					Process.exit ((failed.length () == 0) ? 0 : 1);
				}
				return false;
			}
			
			current_case = ((!) current_case).next;
			
			return true;
		});
    
		idle.attach (null);
	}

}

}

Contact

Email: johan.mattsson.m@gmail.com
Mastodon: typo.social/@birdfont
GitHub: johanmattssonm
LinkedIn: johan-mattsson-2ab324112