/**
* Class for storing and processing the information about how well testing
* for a certain `TestCase` went. That information is stored as
* a collection of `IssueSummary`s, that can be accessed all at once
* or by their context.
* `TestCaseSummary` must be initialized for some `TestCase` before it can
* be used for anything (unlike `IssueSummary`).
* 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 TestCaseSummary extends AcediaObject;
// Case for which this summary was initialized.
// `none` if it was not.
var private class ownerCase;
/**
*
* We will store issue summaries for different contexts separately.
* INVARIANT: any function that adds records to `contextRecords`
* must guarantee that:
* 1. No two distinct records will have the same `context`;
* 2. All the `IssueSummary`s in `issueSummaries` array have different
* issue descriptions.
* Comparisons of `string`s for two above conditions are case-insensitive.
*/
struct ContextRecord
{
var string context;
var array issueSummaries;
};
var private array contextRecords;
// String literals used for displaying array of test case summaries
var private const string indent;
var private const string reportHeader;
var private const string reportSuccessfulEnding;
var private const string reportUnsuccessfulEnding;
/**
* Initializes caller summary for given `TestCase` class.
* Can only be successfully done once, but will fail if
* passed a `none` reference.
*
* @param targetCase `TestCase` class for which this summary will be
* recording test results.
* @return `true` if initialization was successful and `false otherwise
* (either summary already initialized or passed reference is `none`).
*/
public final function bool Initialize(class targetCase)
{
if (ownerCase != none) return false;
if (targetCase == none) return false;
ownerCase = targetCase;
return true;
}
/**
* Returns index of a context record with a given description
* (`context`) in `contextRecords`.
* Creates one if missing. Never fails.
*
* @param context Context that desired record must match.
* @return Index of the context record that matches `context`.
* Returned index is always valid.
*/
private final function int TouchContext(string context)
{
local int i;
local ContextRecord newRecord;
// Try to find existing record with given context description
for (i = 0; i < contextRecords.length; i += 1)
{
if (context ~= contextRecords[i].context) {
return i;
}
}
// If there is none - make a new one
newRecord.context = context;
contextRecords[contextRecords.length] = newRecord;
return (contextRecords.length - 1);
}
/**
* Finds indices of a context record and an `IssueSummary` in
* a nested array that have matching `context`
* and `issueDescription`.
* Creates records and/or `IssueSummary` if missing. Never fails.
*
* @param context Context description that
* desired record must match.
* @param issueDescription Issue description that
* desired `IssueSummary`must match.
* @param recordIndex Index of the context record that matches
* `context` description will be recorded here.
* Returned value is always valid. Passed value is discarded.
* @param recordIndex Index of the `IssueSummary` that matches
* `issueDescription` description will be recorded here.
* Returned value is always valid. Passed value is discarded.
*/
private final function TouchIssue(
string context,
string issueDescription,
out int recordIndex,
out int issueIndex
)
{
local int i;
local array issueSummaries;
recordIndex = TouchContext(context);
issueSummaries = contextRecords[recordIndex].issueSummaries;
// Try to find existing issue summary with a given description
for (i = 0; i < issueSummaries.length; i += 1)
{
if (issueSummaries[i] == none) continue;
if (issueDescription ~= issueSummaries[i].GetDescription())
{
issueIndex = i;
return;
}
}
// If there is none - add a new one
issueIndex = issueSummaries.length;
issueSummaries[issueIndex] = new class'IssueSummary';
issueSummaries[issueIndex].SetIssue(ownerCase, context, issueDescription);
contextRecords[recordIndex].issueSummaries = issueSummaries;
}
/**
* Checks if caller summary was correctly initialized.
*
* @return `true` if summary was correctly initialized and `false` otherwise.
*/
public final function bool IsInitialized()
{
return (ownerCase != none);
}
/**
* Adds result of another test (success or not) to the records of this summary.
*
* @param context Context under which test was performed.
* @param issueDescription Description of issue,
* for which test was performed.
* @param success `true` if test was successful and had passed,
* `false` otherwise.
*/
public final function AddTestResult(
string context,
string issueDescription,
bool success
)
{
local int recordIndex, issueIndex;
TouchIssue(context, issueDescription, recordIndex, issueIndex);
contextRecords[recordIndex]
.issueSummaries[issueIndex]
.AddTestResult(success);
}
/**
* Returns all contexts, for which caller summary has any records of tests
* being performed.
*
* To check if particular context exists you can use `DoesContextExists()`.
*
* @return Array of `string`s, each representing one of the contexts,
* used in tests.
* Guarantees no duplicates (equality without accounting for case).
*/
public final function array GetContexts()
{
local int i;
local array result;
for (i = 0; i < contextRecords.length; i += 1) {
result[result.length] = contextRecords[i].context;
}
return result;
}
/**
* Checks if given context has any records about performing tests
* (whether they ended in success or a failure) under it.
*
* To get an array of all existing contexts use `GetContexts()`.
*
* @param context A context to check for existing in records.
* @return `true` if there was a record about a test being performed under
* a given context and `false` otherwise.
*/
public final function bool DoesContextExists(string context)
{
local int i;
for (i = 0; i < contextRecords.length; i += 1)
{
if (contextRecords[i].context ~= context) {
return true;
}
}
return false;
}
/**
* `IssueSummary`s for every issue that was tested and recorded in
* the caller `TestCaseSummary`.
*
* @return Array of `IssueSummary`s for every tested and recorded issue.
*/
public final function array GetIssueSummaries()
{
local int i, j;
local array recordedSummaries;
local array result;
for (i = 0; i < contextRecords.length; i += 1)
{
recordedSummaries = contextRecords[i].issueSummaries;
for (j = 0; j < recordedSummaries.length; j += 1) {
result[result.length] = recordedSummaries[j];
}
}
return result;
}
/**
* Returns `IssueSummary`s for every issue that was tested under
* a given context and recorded in caller `TestCaseSummary`.
*
* @param context Context under which issues of interest were tested.
* @return Array of `IssueSummary`s for every issue that was tested under
* given context.
*/
public final function array GetIssueSummariesForContext(
string context
)
{
local int i;
local array emptyResult;
for (i = 0; i < contextRecords.length; i += 1)
{
if (contextRecords[i].context ~= context) {
return contextRecords[i].issueSummaries;
}
}
return emptyResult;
}
// Counts total amount of tests performed under the contexts
// corresponding to `contextRecords[recordIndex]` record.
private final function int GetTotalTestsAmountForRecord(int recordIndex)
{
local int i;
local int result;
local array issueSummaries;
issueSummaries = contextRecords[recordIndex].issueSummaries;
result = 0;
for (i = 0; i < issueSummaries.length; i += 1)
{
if (issueSummaries[i] == none) continue;
result += issueSummaries[i].GetTotalTestsAmount();
}
return result;
}
/**
* Total amount of performed tests, recorded in caller `TestCaseSummary`.
*
* If you are interested in amount of test under a specific context, -
* use `GetTotalTestsAmountForContext()` instead.
*
* @return Total amount of performed tests.
*/
public final function int GetTotalTestsAmount()
{
local int i;
local int result;
for (i = 0; i < contextRecords.length; i += 1)
{
result += GetTotalTestsAmountForRecord(i);
}
return result;
}
/**
* Total amount of tests, performed under a context `context` and
* recorded in caller `TestCaseSummary`.
*
* If you are interested in total amount of test under all contexts, -
* use `GetTotalTestsAmount()` instead.
*
* @param context Context for which method must count amount of
* performed tests.
* @return Total amount of tests, performed under given context.
* If given context does not exist in records, - returns `-1`.
*/
public final function int GetTotalTestsAmountForContext(string context)
{
local int i;
for (i = 0; i < contextRecords.length; i += 1)
{
if (context ~= contextRecords[i].context) {
return GetTotalTestsAmountForRecord(i);
}
}
return -1;
}
// Counts total amount of successful tests performed under the contexts
// corresponding to `contextRecords[recordIndex]` record.
private final function int GetSuccessfulTestsAmountForRecord(int recordIndex)
{
local int i;
local int result;
local array issueSummaries;
issueSummaries = contextRecords[recordIndex].issueSummaries;
result = 0;
for (i = 0; i < issueSummaries.length; i += 1)
{
if (issueSummaries[i] == none) continue;
result += issueSummaries[i].GetSuccessfulTestsAmount();
}
return result;
}
/**
* Total amount of successfully performed tests,
* recorded in caller `TestCaseSummary`.
*
* If you are interested in amount of successful test under a specific context,
* - use `GetSuccessfulTestsAmountForContext()` instead.
*
* @return Total amount of successfully performed tests.
*/
public final function int GetSuccessfulTestsAmount()
{
local int i;
local int result;
for (i = 0; i < contextRecords.length; i += 1)
{
result += GetSuccessfulTestsAmountForRecord(i);
}
return result;
}
/**
* Total amount of tests, performed under a context `context` and
* recorded in caller `TestCaseSummary`.
*
* If you are interested in total amount of successful test under all contexts,
* - use `GetSuccessfulTestsAmount()` instead.
*
* @param context Context for which we method must count amount of
* successful tests.
* @return Total amount of successful tests, performed under given context.
* If given context does not exist in records, - returns `-1`.
*/
public final function int GetSuccessfulTestsAmountForContext(string context)
{
local int i;
for (i = 0; i < contextRecords.length; i += 1)
{
if (context ~= contextRecords[i].context) {
return GetSuccessfulTestsAmountForRecord(i);
}
}
return -1;
}
// Counts total amount of tests, failed under the contexts
// corresponding to `contextRecords[recordIndex]` record.
private final function int GetFailedTestsAmountForRecord(int recordIndex)
{
local int i;
local int result;
local array issueSummaries;
issueSummaries = contextRecords[recordIndex].issueSummaries;
result = 0;
for (i = 0; i < issueSummaries.length; i += 1)
{
if (issueSummaries[i] == none) continue;
result += issueSummaries[i].GetFailedTestsAmount();
}
return result;
}
/**
* Total amount of failed tests, recorded in caller `TestCaseSummary`.
*
* If you are interested in amount of failed test under a specific context, -
* use `GetFailedTestsAmountForContext()` instead.
*
* @return Total amount of failed tests.
*/
public final function int GetFailedTestsAmount()
{
local int i;
local int result;
for (i = 0; i < contextRecords.length; i += 1)
{
result += GetFailedTestsAmountForRecord(i);
}
return result;
}
/**
* Total amount of failed tests, performed under a context `context` and
* recorded in caller `TestCaseSummary`.
*
* If you are interested in total amount of failed test under all contexts, -
* use `GetFailedTestsAmount()` instead.
*
* @param context Context for which method must count amount of
* failed tests.
* @return Total amount of failed tests, performed under given context.
* If given context does not exist in records, - returns `-1`.
*/
public final function int GetFailedTestsAmountForContext(string context)
{
local int i;
for (i = 0; i < contextRecords.length; i += 1)
{
if (context ~= contextRecords[i].context) {
return GetFailedTestsAmountForRecord(i);
}
}
return -1;
}
/**
* Checks whether all tests recorded in this summary have passed.
*
* @return `true` if all tests have passed, `false` otherwise.
*/
public final function bool HasPassedAllTests()
{
return (GetFailedTestsAmount() <= 0);
}
/**
* Checks whether all tests, performed under given context and
* recorded in this summary, have passed.
*
* @return `true` if all tests under given context have passed,
* `false` otherwise.
* If given context does not exists - it did not fail any tests.
*/
public final function bool HasPassedAllTestsForContext(string context)
{
return (GetFailedTestsAmountForContext(context) <= 0);
}
/**
* Generates a text summary for a set of results, given as array of
* `TestCaseSummary`s (exactly how results are returned by `TestingService`).
*
* @param summaries `TestCase` summaries (obtained as a result of testing)
* that we want to display.
* @return Test representation of `summaries` as an array of
* formatted strings, where each string corresponds to it's own line.
*/
public final static function array GenerateStringSummary(
array summaries)
{
local int i;
local bool allTestsPassed;
local array result;
allTestsPassed = true;
result[0] = default.reportHeader;
for (i = 0; i < summaries.length; i += 1)
{
if (summaries[i] == none) continue;
summaries[i].AppendCaseSummary(result);
allTestsPassed = allTestsPassed && summaries[i].HasPassedAllTests();
}
if (allTestsPassed) {
result[result.length] = default.reportSuccessfulEnding;
}
else {
result[result.length] = default.reportUnsuccessfulEnding;
}
return result;
}
// Add text representation of caller `TestCase` to the existing array `result`.
private final function AppendCaseSummary(out array result)
{
local int i, j;
local array contexts;
local string testCaseAnnouncement;
local array issues;
if (ownerCase == none) return;
// Announce case
testCaseAnnouncement = "{$text_default Test case {$text_emphasis";
if (ownerCase.static.GetGroup() != "") {
testCaseAnnouncement @= "[" $ ownerCase.static.GetGroup() $ "]";
}
testCaseAnnouncement @= ownerCase.static.GetName() $ "}:}";
if (GetFailedTestsAmount() > 0) {
testCaseAnnouncement @= "{$text_failure failed}!";
}
else {
testCaseAnnouncement @= "{$text_ok passed}!";
}
result[result.length] = testCaseAnnouncement;
// Report failed tests
contexts = GetContexts();
for (i = 0;i < contexts.length; i += 1)
{
if (GetFailedTestsAmountForContext(contexts[i]) <= 0) continue;
result[result.length] = "{$text_warning " $ contexts[i] $ "}";
issues = GetIssueSummariesForContext(contexts[i]);
for (j = 0; j < issues.length; j += 1)
{
if (issues[j] == none) continue;
if (issues[j].GetFailedTestsAmount() <= 0) continue;
result[result.length] = indent $ issues[j].ToString();
}
}
}
defaultproperties
{
indent = " "
reportHeader = "{$text_default ############################## {$text_emphasis Test summary} ###############################}"
reportSuccessfulEnding = "{$text_default ########################### {$text_ok All tests have passed!} ############################}"
reportUnsuccessfulEnding = "{$text_default ########################## {$text_failure Some tests have failed :(} ###########################}"
}