From a4a1c21cd7942fc820cde238c381c78292deebc9 Mon Sep 17 00:00:00 2001 From: Anton Tarasenko Date: Tue, 31 Mar 2020 13:28:20 +0700 Subject: [PATCH] Add basic unit test support Add class that can automatically perform defined tests on user request. Developers that wish to implemet unit tests for some functionality must extend that class (`TestCase`) and add it to the manifest, so that Acedia can read, register and later use it to perform tests. --- sources/Acedia.uc | 9 ++ sources/Manifest.uc | 3 + sources/TestCase.uc | 252 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 264 insertions(+) create mode 100644 sources/TestCase.uc diff --git a/sources/Acedia.uc b/sources/Acedia.uc index fe3cde6..3a7fe12 100644 --- a/sources/Acedia.uc +++ b/sources/Acedia.uc @@ -31,6 +31,9 @@ var private Acedia selfReference; // Array of predefined services that must be started along with Acedia mutator. var private array< class > systemServices; +// All unit tests loaded from all packages. +var private array< class > testCases; + static public final function Acedia GetInstance() { return default.selfReference; @@ -69,6 +72,12 @@ private final function LoadManifest(class manifestClass) manifestClass.default.features[i].static.EnableMe(); } } + // Load unit tests + for (i = 0; i < manifestClass.default.testCases.length; i += 1) + { + if (manifestClass.default.testCases[i] == none) continue; + testCases[testCases.length] = manifestClass.default.testCases[i]; + } } private final function LaunchServices() diff --git a/sources/Manifest.uc b/sources/Manifest.uc index 535e92a..26b21ca 100644 --- a/sources/Manifest.uc +++ b/sources/Manifest.uc @@ -26,6 +26,9 @@ // List of features in this manifest's package. var public const array< class > features; +// List of features in this manifest's package. +var public const array< class > testCases; + // Listeners listed here will be automatically activated. var public const array< class > requiredListeners; diff --git a/sources/TestCase.uc b/sources/TestCase.uc new file mode 100644 index 0000000..2512e30 --- /dev/null +++ b/sources/TestCase.uc @@ -0,0 +1,252 @@ +/** + * Base class aimed to contain sets of unit tests for various components of + * Acedia and it's features. + * Currently provides bare-bones testing functions that check boolean + * variables for true/false and objects for whether they're `none` or not. + * Tests: + * ~ can be grouped by their "context", + * describing what they are testing; + * ~ test (or several tests) can be assigned an error message, + * describing what exactly went wrong. + * Copyright 2020 Anton Tarasenko + *------------------------------------------------------------------------------ + * This file is part of Acedia. + * + * Acedia 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, version 3 of the License, or + * (at your option) any later version. + * + * Acedia 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 Acedia. If not, see . + */ +class TestCase extends Actor + abstract; + +// Name by which this set of unit tests can be referred to. +var protected const string caseName; + +// Information about how well testing went for a particular context, +// i.e. subsets of tests for a particular functionality. +struct ContextSummary +{ + // Text, human-readable description of the purpose of + // tests in this context. + var string description; + // `false` if at least one test failed, `true` otherwise. + var bool passed; + // How many test were performed. + var int testsPerformed; + // How many tests failed. + var int testsFailed; + // Error messages generated by failed tests. + var array errors; +}; + +// Collection of summaries for all contexts defined by the user so far. +struct Summary +{ + var bool passed; + var array contextSummaries; +}; + +// Has function for defining context (`Context()`) been called. +var private bool userDefinedContext; +// Were all tests performed? +var private bool finishedTests; +// Error message that will be generated if some test will fail now. +var private string currentErrorMessage; + +// Store complete summary here. +var private Summary currentSummary; +// For quick access store current context's summary here and update it in +// `currentSummary` once done. +var private ContextSummary activeContextSummary; + +// Call this function to define a context for subsequent test +// (until another call). +public final static function Context(string description) +{ + if ( default.userDefinedContext + || default.activeContextSummary.testsPerformed > 0) + { + UpdateContextSummary(default.activeContextSummary); + } + default.userDefinedContext = true; + default.activeContextSummary = GetContextSummary(description); + default.currentErrorMessage = ""; +} + +// Call this function to define an error message for tests that +// would fail after it. +// Message is reset by another call of `Issue()` or +// by changing the context via `Context()`. +public final static function Issue(string errorMessage) +{ + default.currentErrorMessage = errorMessage; +} + +// All tests to be performed can be placed in this function, +// along with appropriate calls to `Context()` and `Issue()`. +// For an example see class `TEST_JSON`. +protected static function TESTS(){} + +// Following functions provide simple test primitives, +public final static function TEST_ExpectTrue(bool result) +{ + RecordTestResult(result, default.currentErrorMessage); +} + +public final static function TEST_ExpectFalse(bool result) +{ + RecordTestResult(!result, default.currentErrorMessage); +} + +public final static function TEST_ExpectNone(Object object) +{ + RecordTestResult(object == none, default.currentErrorMessage); +} + +public final static function TEST_ExpectNotNone(Object object) +{ + RecordTestResult(object != none, default.currentErrorMessage); +} + +// Returns the summary of how testing went. +public final static function Summary GetSummary() +{ + return default.currentSummary; +} + +// Name by which this set of unit tests can be referred to. +public final static function string GetName() +{ + return default.caseName; +} + +// Creates brand new summary for context with a given description, +// marked as "passed" and zero tests done. +private final static function ContextSummary NewContextSummary +( + string description +) +{ + local ContextSummary newSummary; + newSummary.passed = true; + newSummary.description = description; + newSummary.testsPerformed = 0; + newSummary.testsFailed = 0; + newSummary.errors.length = 0; + return newSummary; +} + +// Returns index of summary with given description +// in our records (`currentSummary`). +// Return `-1` if there is no context with such description. +private final static function int GetContextSummaryIndex(string description) +{ + local int i; + for (i = 0; i < default.currentSummary.contextSummaries.length; i += 1) + { + if ( default.currentSummary.contextSummaries[i].description + ~= description) + { + return i; + } + } + return -1; +} +// Returns index summary with given description +// in our records. +// Return new context summary if there is no context with such description. +private final static function ContextSummary GetContextSummary +( + string description +) +{ + local int index; + if (default.activeContextSummary.description ~= description) + { + return default.activeContextSummary; + } + index = GetContextSummaryIndex(description); + if (index < 0) + { + return NewContextSummary(description); + } + + return default.currentSummary.contextSummaries[index]; +} + +// Rewrites summary (with the same name as a given summary) +// in `currentSummary` records. +// If there's no such record - adds a new one. +private final static function UpdateContextSummary +( + ContextSummary relevantSummary +) +{ + local int index; + index = GetContextSummaryIndex(relevantSummary.description); + if (index < 0) + { + index = default.currentSummary.contextSummaries.length; + } + default.currentSummary.contextSummaries[index] = relevantSummary; +} + +// Records (in current context summary) that another test was performed and +// succeeded/failed, along with given error message. +private final static function RecordTestResult +( + bool isSuccessful, + string errorMessage +) +{ + local int i; + local int errorsAmount; + if (default.finishedTests) return; + default.activeContextSummary.testsPerformed += 1; + if (isSuccessful) return; + + default.currentSummary.passed = false; + default.activeContextSummary.passed = false; + default.activeContextSummary.testsFailed += 1; + errorsAmount = default.activeContextSummary.errors.length; + for (i = 0; i < errorsAmount; i += 1) + { + if (default.activeContextSummary.errors[i] ~= errorMessage) + { + return; + } + } + default.activeContextSummary.errors[errorsAmount] = errorMessage; +} + +// Calling this function will perform unit tests defined in `TESTS()` +// function of this test case and will prepare the summary, +// obtainable through `GetSummary()` function. +// Returns `true` if all tests have successfully passed +// and `false` otherwise. +public final static function bool PerformTests() +{ + default.finishedTests = false; + default.userDefinedContext = false; + default.currentSummary.passed = true; + default.currentSummary.contextSummaries.length = 0; + default.activeContextSummary = NewContextSummary(""); + TESTS(); + UpdateContextSummary(default.activeContextSummary); + default.finishedTests = true; + return default.currentSummary.passed; +} + +defaultproperties +{ + caseName = "" +} \ No newline at end of file