Merge branch 'RHH/master' into RHH/upcoming

This commit is contained in:
Eduardo Quezada 2023-07-16 01:33:30 -04:00
commit 786bf7752f
6 changed files with 156 additions and 42 deletions

View File

@ -4,9 +4,7 @@ on:
push: push:
branches: branches:
- master - master
- battle_engine - upcoming
- pokemon_expansion
- item_expansion
pull_request: pull_request:
jobs: jobs:

View File

@ -45,6 +45,13 @@ SECTIONS {
test/*.o(COMMON); test/*.o(COMMON);
*libc.a:sbrkr.o(COMMON); *libc.a:sbrkr.o(COMMON);
end = .; end = .;
/* .persistent starts at 0x3007F00 */
/* WARNING: This is the end of the IRQ stack, if there's too
* much data it WILL be overwritten. */
. = 0x7F00;
test/*.o(.persistent);
. = 0x8000; . = 0x8000;
} }

View File

@ -13,6 +13,7 @@ enum TestResult
TEST_RESULT_INVALID, TEST_RESULT_INVALID,
TEST_RESULT_ERROR, TEST_RESULT_ERROR,
TEST_RESULT_TIMEOUT, TEST_RESULT_TIMEOUT,
TEST_RESULT_CRASH,
TEST_RESULT_TODO, TEST_RESULT_TODO,
}; };
@ -38,8 +39,6 @@ struct TestRunnerState
{ {
u8 state; u8 state;
u8 exitCode; u8 exitCode;
s32 tests;
s32 passes;
const char *skipFilename; const char *skipFilename;
const struct Test *test; const struct Test *test;
u32 processCosts[MAX_PROCESSES]; u32 processCosts[MAX_PROCESSES];

View File

@ -1,7 +1,7 @@
/* Embedded DSL for automated black-box testing of battle mechanics. /* Embedded DSL for automated black-box testing of battle mechanics.
* *
* To run all the tests use: * To run all the tests use:
* make check * make check -j
* To run specific tests, e.g. Spikes ones, use: * To run specific tests, e.g. Spikes ones, use:
* make check TESTS='Spikes' * make check TESTS='Spikes'
* To build a ROM (pokemerald-test.elf) that can be opened in mgba to * To build a ROM (pokemerald-test.elf) that can be opened in mgba to

View File

@ -15,6 +15,16 @@ void CB2_TestRunner(void);
EWRAM_DATA struct TestRunnerState gTestRunnerState; EWRAM_DATA struct TestRunnerState gTestRunnerState;
EWRAM_DATA struct FunctionTestRunnerState *gFunctionTestRunnerState; EWRAM_DATA struct FunctionTestRunnerState *gFunctionTestRunnerState;
enum {
CURRENT_TEST_STATE_ESTIMATE,
CURRENT_TEST_STATE_RUN,
};
__attribute__((section(".persistent"))) static struct {
u32 address:28;
u32 state:1;
} sCurrentTest = {0};
void TestRunner_Battle(const struct Test *); void TestRunner_Battle(const struct Test *);
static bool32 MgbaOpen_(void); static bool32 MgbaOpen_(void);
@ -51,6 +61,47 @@ enum
STATE_EXIT, STATE_EXIT,
}; };
static u32 MinCostProcess(void)
{
u32 i;
u32 minCost, minCostProcess;
minCost = gTestRunnerState.processCosts[0];
minCostProcess = 0;
for (i = 1; i < gTestRunnerN; i++)
{
if (gTestRunnerState.processCosts[i] < minCost)
{
minCost = gTestRunnerState.processCosts[i];
minCostProcess = i;
}
}
return minCostProcess;
}
// Greedily assign tests to processes based on estimated cost.
// TODO: Make processCosts a min heap.
static u32 AssignCostToRunner(void)
{
u32 minCostProcess;
if (gTestRunnerState.test->runner == &gAssumptionsRunner)
return TRUE;
minCostProcess = MinCostProcess();
// XXX: If estimateCost returns only on some processes, or
// returns inconsistent results then processCosts will be
// inconsistent and some tests may not run.
if (gTestRunnerState.test->runner->estimateCost)
gTestRunnerState.processCosts[minCostProcess] += gTestRunnerState.test->runner->estimateCost(gTestRunnerState.test->data);
else
gTestRunnerState.processCosts[minCostProcess] += 1;
return minCostProcess;
}
void CB2_TestRunner(void) void CB2_TestRunner(void)
{ {
switch (gTestRunnerState.state) switch (gTestRunnerState.state)
@ -65,12 +116,43 @@ void CB2_TestRunner(void)
gIntrTable[7] = Intr_Timer2; gIntrTable[7] = Intr_Timer2;
gTestRunnerState.state = STATE_NEXT_TEST; // The current test restarted the ROM (e.g. by jumping to NULL).
if (sCurrentTest.address != 0)
{
gTestRunnerState.test = __start_tests;
while ((uintptr_t)gTestRunnerState.test != sCurrentTest.address)
{
AssignCostToRunner();
gTestRunnerState.test++;
}
if (sCurrentTest.state == CURRENT_TEST_STATE_ESTIMATE)
{
u32 runner = MinCostProcess();
gTestRunnerState.processCosts[runner] += 1;
if (runner == gTestRunnerI)
{
gTestRunnerState.state = STATE_REPORT_RESULT;
gTestRunnerState.result = TEST_RESULT_CRASH;
}
else
{
gTestRunnerState.state = STATE_NEXT_TEST;
}
}
else
{
gTestRunnerState.state = STATE_REPORT_RESULT;
gTestRunnerState.result = TEST_RESULT_CRASH;
}
}
else
{
gTestRunnerState.state = STATE_NEXT_TEST;
gTestRunnerState.test = __start_tests - 1;
}
gTestRunnerState.exitCode = 0; gTestRunnerState.exitCode = 0;
gTestRunnerState.tests = 0;
gTestRunnerState.passes = 0;
gTestRunnerState.skipFilename = NULL; gTestRunnerState.skipFilename = NULL;
gTestRunnerState.test = __start_tests - 1;
break; break;
case STATE_NEXT_TEST: case STATE_NEXT_TEST:
@ -108,40 +190,19 @@ void CB2_TestRunner(void)
return; return;
} }
// Greedily assign tests to processes based on estimated cost. sCurrentTest.address = (uintptr_t)gTestRunnerState.test;
// TODO: Make processCosts a min heap. sCurrentTest.state = CURRENT_TEST_STATE_ESTIMATE;
if (gTestRunnerState.test->runner != &gAssumptionsRunner)
{
u32 i;
u32 minCost, minCostProcess;
minCost = gTestRunnerState.processCosts[0];
minCostProcess = 0;
for (i = 1; i < gTestRunnerN; i++)
{
if (gTestRunnerState.processCosts[i] < minCost)
{
minCost = gTestRunnerState.processCosts[i];
minCostProcess = i;
}
}
if (minCostProcess == gTestRunnerI) if (AssignCostToRunner() == gTestRunnerI)
gTestRunnerState.state = STATE_RUN_TEST; gTestRunnerState.state = STATE_RUN_TEST;
else else
gTestRunnerState.state = STATE_NEXT_TEST; gTestRunnerState.state = STATE_NEXT_TEST;
// XXX: If estimateCost exits only on some processes then
// processCosts will be inconsistent.
if (gTestRunnerState.test->runner->estimateCost)
gTestRunnerState.processCosts[minCostProcess] += gTestRunnerState.test->runner->estimateCost(gTestRunnerState.test->data);
else
gTestRunnerState.processCosts[minCostProcess] += 1;
}
break; break;
case STATE_RUN_TEST: case STATE_RUN_TEST:
gTestRunnerState.state = STATE_REPORT_RESULT; gTestRunnerState.state = STATE_REPORT_RESULT;
sCurrentTest.state = CURRENT_TEST_STATE_RUN;
if (gTestRunnerState.test->runner->setUp) if (gTestRunnerState.test->runner->setUp)
gTestRunnerState.test->runner->setUp(gTestRunnerState.test->data); gTestRunnerState.test->runner->setUp(gTestRunnerState.test->data);
gTestRunnerState.test->runner->run(gTestRunnerState.test->data); gTestRunnerState.test->runner->run(gTestRunnerState.test->data);
@ -162,6 +223,15 @@ void CB2_TestRunner(void)
const struct MemBlock *block = head; const struct MemBlock *block = head;
do do
{ {
if (block->magic != MALLOC_SYSTEM_ID
|| !(EWRAM_START <= (uintptr_t)block->next && (uintptr_t)block->next < EWRAM_END)
|| (block->next <= block && block->next != head))
{
MgbaPrintf_("gHeap corrupted block at 0x%p", block);
gTestRunnerState.result = TEST_RESULT_ERROR;
break;
}
if (block->allocated) if (block->allocated)
{ {
const char *location = MemBlockLocation(block); const char *location = MemBlockLocation(block);
@ -186,11 +256,8 @@ void CB2_TestRunner(void)
const char *color; const char *color;
const char *result; const char *result;
gTestRunnerState.tests++;
if (gTestRunnerState.result == gTestRunnerState.expectedResult) if (gTestRunnerState.result == gTestRunnerState.expectedResult)
{ {
gTestRunnerState.passes++;
color = "\e[32m"; color = "\e[32m";
MgbaPrintf_(":N%s", gTestRunnerState.test->name); MgbaPrintf_(":N%s", gTestRunnerState.test->name);
} }
@ -243,6 +310,9 @@ void CB2_TestRunner(void)
case TEST_RESULT_TIMEOUT: case TEST_RESULT_TIMEOUT:
result = "TIMEOUT"; result = "TIMEOUT";
break; break;
case TEST_RESULT_CRASH:
result = "CRASH";
break;
default: default:
result = "UNKNOWN"; result = "UNKNOWN";
break; break;
@ -434,6 +504,7 @@ static s32 MgbaVPrintf_(const char *fmt, va_list va)
{ {
s32 i = 0; s32 i = 0;
s32 c, d; s32 c, d;
u32 p;
const char *s; const char *s;
while (*fmt) while (*fmt)
{ {
@ -467,6 +538,20 @@ static s32 MgbaVPrintf_(const char *fmt, va_list va)
i = MgbaPutchar_(i, buffer[--n]); i = MgbaPutchar_(i, buffer[--n]);
} }
break; break;
case 'p':
p = va_arg(va, unsigned);
{
s32 n;
for (n = 0; n < 7; n++)
{
unsigned nybble = (p >> (24 - (4*n))) & 0xF;
if (nybble <= 9)
i = MgbaPutchar_(i, '0' + nybble);
else
i = MgbaPutchar_(i, 'a' + nybble - 10);
}
}
break;
case 'q': case 'q':
d = va_arg(va, int); d = va_arg(va, int);
{ {

View File

@ -17,6 +17,7 @@
#include <fcntl.h> #include <fcntl.h>
#include <math.h> #include <math.h>
#include <poll.h> #include <poll.h>
#include <regex.h>
#include <signal.h> #include <signal.h>
#include <stdbool.h> #include <stdbool.h>
#include <stdio.h> #include <stdio.h>
@ -37,6 +38,8 @@
#define MAX_FAILED_TESTS_TO_LIST 100 #define MAX_FAILED_TESTS_TO_LIST 100
#define MAX_TEST_LIST_BUFFER_LENGTH 256 #define MAX_TEST_LIST_BUFFER_LENGTH 256
#define ARRAY_COUNT(arr) (sizeof((arr)) / sizeof((arr)[0]))
struct Runner struct Runner
{ {
pid_t pid; pid_t pid;
@ -248,7 +251,29 @@ int main(int argc, char *argv[])
exit(2); exit(2);
} }
nrunners = sysconf(_SC_NPROCESSORS_ONLN); nrunners = 1;
const char *makeflags = getenv("MAKEFLAGS");
if (makeflags)
{
int e;
regex_t preg;
regmatch_t pmatch[4];
if ((e = regcomp(&preg, "(^| )-j([0-9]*)($| )", REG_EXTENDED)) != 0)
{
char errbuf[256];
regerror(e, &preg, errbuf, sizeof(errbuf));
fprintf(stderr, "regcomp failed: '%s'\n", errbuf);
exit(2);
}
if (regexec(&preg, makeflags, ARRAY_COUNT(pmatch), pmatch, 0) != REG_NOMATCH)
{
if (pmatch[2].rm_so == pmatch[2].rm_eo)
nrunners = sysconf(_SC_NPROCESSORS_ONLN);
else
sscanf(makeflags + pmatch[2].rm_so, "%d", &nrunners);
}
regfree(&preg);
}
if (nrunners > MAX_PROCESSES) if (nrunners > MAX_PROCESSES)
nrunners = MAX_PROCESSES; nrunners = MAX_PROCESSES;
runners_digits = ceil(log10(nrunners)); runners_digits = ceil(log10(nrunners));