SERVER/YML: Add Unit test infrastructure

This commit is contained in:
MotoLegacy 2025-03-10 20:16:35 -07:00
parent 62228b72d8
commit 4ac4c8934c
17 changed files with 1084 additions and 9 deletions

18
.github/workflows/unit-tests.yml vendored Normal file
View file

@ -0,0 +1,18 @@
name: QuakeC Unit Tests
on: [pull_request]
jobs:
Unit-Tests:
name: Run Unit Tests
runs-on: ubuntu-latest
container:
image: ubuntu:24.10
options: --shm-size=8192m
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Wait for GitHub to keep up..
run: sleep 2s
shell: bash
- name: Run Unit Test Script
run: |
bash testing/run_unit_tests.sh

View file

@ -77,4 +77,13 @@ ai/zombie_core.qc
ai/crawler_core.qc
ai/dog_core.qc
utilities/map_compatibility.qc
#ifdef QUAKEC_TEST
tests/test_math.qc
tests/test_perksacola.qc
tests/test_weapons.qc
tests/test_misc_model.qc
tests/test_score.qc
tests/test_ai.qc
tests/test_module.qc
#endif
#endlist

View file

@ -423,7 +423,7 @@ void(entity who) makeCrawler;
void() spawnAllZombEnts;
void() set_z_health;
float() spawn_a_zombieA;
float gotdog;
float map_has_hellhounds;
float dogRound;
float dogWave;
float dog_round_count;

View file

@ -31,6 +31,8 @@ void() MBOX_Touch;
#define MBOX_SPAWNFLAG_NOTHERE 1
#define MBOX_SPAWNFLAG_NOLIGHT 2
float mystery_box_file_parsed;
//
// MBOX_UpdateGlowFrame()
// Updates the Glow model frame to match
@ -925,6 +927,9 @@ void(float mbox_file) MBOX_ParseMB2File =
//
void() MBOX_LoadData =
{
if (mystery_box_file_parsed == true)
return;
float mbox_file;
string file_path;
@ -935,6 +940,7 @@ void() MBOX_LoadData =
if (mbox_file != -1) {
MBOX_ParseMB2File(mbox_file);
fclose(mbox_file);
mystery_box_file_parsed = true;
return;
}
@ -945,6 +951,7 @@ void() MBOX_LoadData =
if (mbox_file != -1) {
MBOX_ParseMB1File(mbox_file);
fclose(mbox_file);
mystery_box_file_parsed = true;
return;
}
@ -960,6 +967,7 @@ void() MBOX_LoadData =
// Close the file pointer just in case.
fclose(mbox_file);
mystery_box_file_parsed = true;
}
void() mystery_box =

View file

@ -575,7 +575,7 @@ void() perk_double =
// Deadshot Daiquiri
void() perk_deadshot =
{
Perk_InitMachine("perk_deadshot", PERK_DEADSHOT_DEFAULT_MODEL, 2000, 2000,
Perk_InitMachine("perk_deadshot", PERK_DEADSHOT_DEFAULT_MODEL, 1500, 1500,
PERK_SPAWNFLAG_YELLOWLIGHT, 1000, 1000, true, true, 7, P_DEAD);
};
// naievil -- older maps compatability

View file

@ -135,7 +135,7 @@ void() StartFrame =
entity hellhound = find(world, classname, "spawn_dog");
if (hellhound != world)
gotdog = true;
map_has_hellhounds = true;
Compat_Init();
updateDogRound();

View file

@ -32,6 +32,12 @@ void() Spawns_Init;
void() SUB_UseTargets;
void(entity client) LastStand_UnlinkRevivee;
#ifdef QUAKEC_TEST
void() Test_RunAllTests;
#endif // QUAKEC_TEST
#define PLAYER_START_HEALTH 100
#define PLAYER_CROUCH_DIFFERENCE_HL 25
@ -275,6 +281,9 @@ float(float dir) checkMovement =
//
void(entity who, float value, float impacted_by_2x_points) Player_ChangeScore =
{
if (who.classname != "player")
return;
// We shouldn't ever allow a player in Last Stand to have
// their score modified.
if (who.downed)
@ -815,6 +824,13 @@ void() PlayerPostThink =
player_trace_time = time + 0.05;
}
#ifdef QUAKEC_TEST
if (time_before_gamestart != 0)
Test_RunAllTests();
#endif // QUAKEC_TEST
};
void() ClientKill = {};

View file

@ -138,7 +138,7 @@ void() EndRound =
rounds_change = 4;
SetUpdate(self, UT_ROUNDS_CHANGE, rounds_change, 0, 0);
if (gotdog && rounds == dogRound) {
if (map_has_hellhounds && rounds == dogRound) {
Rounds_PlayTransition("sounds/rounds/droundend.wav");
dogWave = false;
} else {
@ -190,7 +190,7 @@ void() NewRound =
}
if (rounds != 0)
{
if (gotdog && rounds == (dogRound - 1)) {
if (map_has_hellhounds && rounds == (dogRound - 1)) {
Rounds_PlayTransition("sounds/rounds/droundstart.wav");
dogWave = true;
dog_round_count++;
@ -203,7 +203,7 @@ void() NewRound =
}
// if we just had a dog round, set the next
if (gotdog && rounds == dogRound) {
if (map_has_hellhounds && rounds == dogRound) {
updateDogRound();
}
@ -252,7 +252,7 @@ void() NewRound =
spawn_delay = 0;
totalpowerups = 0;
if (rounds == dogRound && gotdog) {
if (rounds == dogRound && map_has_hellhounds) {
roundtype = 2;
localcmd("fog 200 525 54 55 60\n");
} else {

View file

@ -0,0 +1,48 @@
/*
server/tests/test_ai.qc
Unit tests for various AI behaviors
Copyright (C) 2021-2025 NZ:P Team
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 2
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, write to:
Free Software Foundation, Inc.
59 Temple Place - Suite 330
Boston, MA 02111-1307, USA
*/
float(float condition, string message) Test_Assert;
void(string message) Test_Skip;
//
// Test_AI_HellhoundsDetected()
// Validates that if a hellhound spawner is in the
// world that we have detected we can have Hellhound
// rounds.
// (https://github.com/nzp-team/nzportable/issues/623)
//
void() Test_AI_HellhoundsDetected =
{
entity hound = find(world, classname, "spawn_dog");
if (hound != world) {
Test_Assert((map_has_hellhounds == true), "Found Hellhound entities but map_has_hellhounds is false!");
Test_Assert((dogRound >= 0), "Found Hellhound entities but dogRound is zero!");
} else {
Test_Assert((map_has_hellhounds == false), "No Hellhound entities found but map_has_hellhounds is true!");
Test_Assert((dogRound == 0), "No Hellhound entities found but dogRound is not zero!");
}
};

View file

@ -0,0 +1,50 @@
/*
server/tests/test_math.qc
Unit test list for math functions. Mostly serves for testing the test infrastructure.
Copyright (C) 2021-2025 NZ:P Team
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 2
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, write to:
Free Software Foundation, Inc.
59 Temple Place - Suite 330
Boston, MA 02111-1307, USA
*/
float(float condition, string message) Test_Assert;
void(string message) Test_Skip;
//
// Test_Math_ClampReturnsMin()
// Asserts when sending a value less than min to clamp,
// min is returned.
//
void() Test_Math_ClampReturnsMin =
{
float clamp_value = clamp(0, 1, 100);
Test_Assert((clamp_value == 1), "Clamp returned incorrect value!");
};
//
// Test_Math_ClampReturnsMax()
// Asserts when sending a value greater than max to clamp,
// max is returned.
//
void() Test_Math_ClampReturnsMax =
{
float clamp_value = clamp(900, 1, 100);
Test_Assert((clamp_value == 100), "Clamp returned incorrect value!");
};

View file

@ -0,0 +1,56 @@
/*
server/tests/test_misc_model.qc
Unit tests for misc_model entity
Copyright (C) 2021-2025 NZ:P Team
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 2
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, write to:
Free Software Foundation, Inc.
59 Temple Place - Suite 330
Boston, MA 02111-1307, USA
*/
float(float condition, string message) Test_Assert;
void(string message) Test_Skip;
//
// Test_misc_model_ModelNotHiddenByDefault()
// Spawns a misc_model with properties and validates
// the model does not start hidden.
// (https://github.com/nzp-team/nzportable/issues/996)
//
void() Test_misc_model_ModelNotHiddenByDefault =
{
entity miscmodel = spawn();
entity old_self = self;
self = miscmodel;
self.last_frame = 154;
self.first_frame = 1;
self.frame = 0;
self.spawnflags = 6;
self.speed = 0.1;
self.model = "models/ai/zfull.mdl";
self.classname = "misc_model";
misc_model();
Test_Assert((self.model != ""), "Model property started hidden!");
self = old_self;
remove(miscmodel);
};

View file

@ -0,0 +1,155 @@
/*
server/tests/test_main.qc
Test running and validation infrastructure for QuakeC unit tests.
Copyright (C) 2021-2025 NZ:P Team
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 2
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, write to:
Free Software Foundation, Inc.
59 Temple Place - Suite 330
Boston, MA 02111-1307, USA
*/
#ifdef FTE
#ifdef QUAKEC_TEST
float total_tests_ran;
float total_tests_passed;
float total_tests_failed;
float total_tests_skipped;
float tests_starttime;
float tests_endtime;
float test_skipped_notify;
string skipped_message_buffer;
float test_failed_an_assert;
string failed_message_buffer;
float(float condition, string message) Test_Assert =
{
if (condition == false) {
failed_message_buffer = sprintf("%s[ERROR]: %s\n", failed_message_buffer, message);
test_failed_an_assert = true;
return false;
}
return true;
};
void(string message) Test_Skip =
{
test_skipped_notify = true;
skipped_message_buffer = sprintf("[SKIP]: %s\n", message);
}
void(void() test_func, string test_name) Test_Run =
{
print(sprintf("[INFO]: Running test [%s]..", test_name));
test_func();
if (test_skipped_notify == true) {
print("SKIP!\n");
total_tests_skipped++;
print(sprintf("%s", skipped_message_buffer));
skipped_message_buffer = "";
test_skipped_notify = false;
total_tests_ran--;
} else if (test_failed_an_assert == false) {
print("PASS!\n");
total_tests_passed++;
} else {
print("FAIL!\n");
total_tests_failed++;
print(sprintf("%s", failed_message_buffer));
failed_message_buffer = "";
test_failed_an_assert = false;
}
total_tests_ran++;
print("\n");
};
void() Test_PrintSummary =
{
print("====================== TEST SUMMARY ======================\n");
print(sprintf("* Passed: %d\n* Failed: %d\n* Skipped: %d\n* Total Ran: %d\n", total_tests_passed, total_tests_failed, total_tests_skipped, total_tests_ran));
print(sprintf("* %d%% Passing Grade\n", (total_tests_passed/total_tests_ran) * 100));
print(sprintf("* %fs Total Runtime\n", tests_endtime));
print("==================== END TEST SUMMARY ====================\n");
localcmd("quit\n");
};
//
// Test Table
// Add unit tests to this table for them to be
// executed during Pull Request testing.
//
var struct {
void() test_func;
string test_name;
} qc_unit_tests[] = {
{ Test_Math_ClampReturnsMin, "Test_Math_ClampReturnsMin" },
{ Test_Math_ClampReturnsMax, "Test_Math_ClampReturnsMax" },
{ Test_Perks_QuickRevive_ValidateFields, "Test_Perks_QuickRevive_ValidateFields" },
{ Test_Perks_JuggerNog_ValidateFields, "Test_Perks_JuggerNog_ValidateFields" },
{ Test_Perks_SpeedCola_ValidateFields, "Test_Perks_SpeedCola_ValidateFields" },
{ Test_Perks_DoubleTap_ValidateFields, "Test_Perks_DoubleTap_ValidateFields" },
{ Test_Perks_StaminUp_ValidateFields, "Test_Perks_StaminUp_ValidateFields" },
{ Test_Perks_PhDFlopper_ValidateFields, "Test_Perks_PhDFlopper_ValidateFields" },
{ Test_Perks_DeadshotDaiquiri_ValidateFields, "Test_Perks_DeadshotDaiquiri_ValidateFields" },
{ Test_Perks_Random_LegacyFields, "Test_Perks_Random_LegacyFields" },
{ Test_Perks_MuleKick_ValidateFields, "Test_Perks_MuleKick_ValidateFields" },
{ Test_Weapons_Gewehr_MagazineSize, "Test_Weapons_Gewehr_MagazineSize" },
{ Test_Weapons_BowieKnife_RemovedAfterRespawn, "Test_Weapons_BowieKnife_RemovedAfterRespawn" },
{ Test_Weapons_HoldTwoWeapons, "Test_Weapons_HoldTwoWeapons" },
{ Test_Weapons_HoldThreeWeapons, "Test_Weapons_HoldThreeWeapons" },
{ Test_Weapons_UpgradedSound, "Test_Weapons_UpgradedSound" },
{ Test_Weapons_ShotgunPenetration, "Test_Weapons_ShotgunPenetration" },
{ Test_Weapons_WunderWaffe_UpgradedReserve, "Test_Weapons_WunderWaffe_UpgradedReserve" },
{ Test_Weapons_WunderWaffe_FireAtBarrel, "Test_Weapons_WunderWaffe_FireAtBarrel" },
{ Test_WallWeapons_Message, "Test_WallWeapons_Message" },
{ Test_WallWeapons_ActivatesAllTargets, "Test_WallWeapons_ActivatesAllTargets" },
{ Test_Weapons_DualWield_ReloadLowReserveRightLeft, "Test_Weapons_DualWield_ReloadLowReserveRightLeft" },
{ Test_Weapons_DualWield_ReloadLowReserveLeftRight, "Test_Weapons_DualWield_ReloadLowReserveLeftRight" },
{ Test_misc_model_ModelNotHiddenByDefault, "Test_misc_model_ModelNotHiddenByDefault" },
{ Test_AddScore_NonClient, "Test_AddScore_NonClient" },
{ Test_AddScore_DamageTypes, "Test_AddScore_DamageTypes" },
{ Test_AddScore_MysteryBoxLeave, "Test_AddScore_MysteryBoxLeave" },
{ Test_AI_HellhoundsDetected, "Test_AI_HellhoundsDetected" }
};
void() Test_RunAllTests =
{
if (tests_starttime != 0)
return;
total_tests_ran = 0;
total_tests_passed = 0;
total_tests_failed = 0;
tests_starttime = gettime();
for(int i = 0; i < qc_unit_tests.length; i++) {
Test_Run(qc_unit_tests[i].test_func, qc_unit_tests[i].test_name);
}
tests_endtime = gettime();
Test_PrintSummary();
};
#endif // QUAKEC_TEST
#endif // FTE

View file

@ -0,0 +1,215 @@
/*
server/tests/test_perksacola.qc
Perk-A-Cola unit tests.
Copyright (C) 2021-2025 NZ:P Team
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 2
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, write to:
Free Software Foundation, Inc.
59 Temple Place - Suite 330
Boston, MA 02111-1307, USA
*/
float(float condition, string message) Test_Assert;
void(string message) Test_Skip;
//
// Test_Perks_QuickRevive_ValidatePrice()
// Spawns a Quick Revive machine and validates its
// default fields.
//
void() Test_Perks_QuickRevive_ValidateFields =
{
entity machine = spawn();
entity old_self = self;
self = machine;
perk_revive();
Test_Assert((self.model == PERK_QUICKREVIVE_DEFAULT_MODEL), "Perk has incorrect model!");
Test_Assert((self.cost == 500), "Perk has incorrect Solo cost!");
Test_Assert((self.cost2 == 1500), "Perk has incorrect Co-Op cost!");
self = old_self;
remove(machine);
};
//
// Test_Perks_JuggerNog_ValidatePrice()
// Spawns a Jugger-Nog machine and validates its
// default fields.
//
void() Test_Perks_JuggerNog_ValidateFields =
{
entity machine = spawn();
entity old_self = self;
self = machine;
perk_juggernog();
Test_Assert((self.model == PERK_JUGGERNOG_DEFAULT_MODEL), "Perk has incorrect model!");
Test_Assert((self.cost == 2500), "Perk has incorrect Solo cost!");
Test_Assert((self.cost2 == 2500), "Perk has incorrect Co-Op cost!");
self = old_self;
remove(machine);
};
//
// Test_Perks_SpeedCola_ValidatePrice()
// Spawns a Speed Cola machine and validates its
// default fields.
//
void() Test_Perks_SpeedCola_ValidateFields =
{
entity machine = spawn();
entity old_self = self;
self = machine;
perk_speed();
Test_Assert((self.model == PERK_SPEEDCOLA_DEFAULT_MODEL), "Perk has incorrect model!");
Test_Assert((self.cost == 3000), "Perk has incorrect Solo cost!");
Test_Assert((self.cost2 == 3000), "Perk has incorrect Co-Op cost!");
self = old_self;
remove(machine);
};
//
// Test_Perks_DoubleTap_ValidateFields()
// Spawns a Double Tap machine and validates its
// default fields.
//
void() Test_Perks_DoubleTap_ValidateFields =
{
entity machine = spawn();
entity old_self = self;
self = machine;
perk_double();
Test_Assert((self.model == PERK_DOUBLETAP_DEFAULT_MODEL), "Perk has incorrect model!");
Test_Assert((self.cost == 2000), "Perk has incorrect Solo cost!");
Test_Assert((self.cost2 == 2000), "Perk has incorrect Co-Op cost!");
self = old_self;
remove(machine);
};
//
// Test_Perks_StaminUp_ValidateFields()
// Spawns a Stamin-Up machine and validates its
// default fields.
//
void() Test_Perks_StaminUp_ValidateFields =
{
entity machine = spawn();
entity old_self = self;
self = machine;
perk_staminup();
Test_Assert((self.model == PERK_STAMINUP_DEFAULT_MODEL), "Perk has incorrect model!");
Test_Assert((self.cost == 2000), "Perk has incorrect Solo cost!");
Test_Assert((self.cost2 == 2000), "Perk has incorrect Co-Op cost!");
self = old_self;
remove(machine);
};
//
// Test_Perks_PhDFlopper_ValidateFields()
// Spawns a PhD Flopper machine and validates its
// default fields.
//
void() Test_Perks_PhDFlopper_ValidateFields =
{
entity machine = spawn();
entity old_self = self;
self = machine;
perk_flopper();
Test_Assert((self.model == PERK_PHDFLOPPER_DEFAULT_MODEL), "Perk has incorrect model!");
Test_Assert((self.cost == 2000), "Perk has incorrect Solo cost!");
Test_Assert((self.cost2 == 2000), "Perk has incorrect Co-Op cost!");
self = old_self;
remove(machine);
};
//
// Test_Perks_DeadshotDaiquiri_ValidateFields()
// Spawns a Deadshot Daiquiri machine and validates its
// default fields.
//
void() Test_Perks_DeadshotDaiquiri_ValidateFields =
{
entity machine = spawn();
entity old_self = self;
self = machine;
perk_deadshot();
Test_Assert((self.model == PERK_DEADSHOT_DEFAULT_MODEL), "Perk has incorrect model!");
Test_Assert((self.cost == 1500), "Perk has incorrect Solo cost!");
Test_Assert((self.cost2 == 1500), "Perk has incorrect Co-Op cost!");
self = old_self;
remove(machine);
};
//
// Test_Perks_MuleKick_ValidateFields()
// Spawns a Deadshot Daiquiri machine and validates its
// default fields.
//
void() Test_Perks_MuleKick_ValidateFields =
{
entity machine = spawn();
entity old_self = self;
self = machine;
perk_mule();
Test_Assert((self.model == PERK_MULEKICK_DEFAULT_MODEL), "Perk has incorrect model!");
Test_Assert((self.cost == 4000), "Perk has incorrect Solo cost!");
Test_Assert((self.cost2 == 4000), "Perk has incorrect Co-Op cost!");
self = old_self;
remove(machine);
};
//
// Test_Perks_Random_LegacyFields()
// Attempts to spawn a random Perk-A-Cola
// with the perk_random fields using
// legacy data.
// (https://github.com/nzp-team/nzportable/issues/664)
//
void() Test_Perks_Random_LegacyFields =
{
entity random = spawn();
entity old_self = self;
self = random;
self.spawnflags = 240;
perk_random();
self = old_self;
remove(random);
};

View file

@ -0,0 +1,113 @@
/*
server/tests/test_score.qc
Unit tests for score system
Copyright (C) 2021-2025 NZ:P Team
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 2
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, write to:
Free Software Foundation, Inc.
59 Temple Place - Suite 330
Boston, MA 02111-1307, USA
*/
float(float condition, string message) Test_Assert;
void(string message) Test_Skip;
//
// Test_AddScore_NonClient()
// Attempts to give points to a non-client.
// (https://github.com/nzp-team/nzportable/issues/783)
//
void() Test_AddScore_NonClient =
{
entity some_ent = spawn();
Player_AddScore(some_ent, 10, false);
remove(some_ent);
};
//
// Test_AddScore_DamageTypes()
// Asserts we are giving correct amount
// of score for every AI damage type.
// (https://github.com/nzp-team/nzportable/issues/776)
//
void() Test_AddScore_DamageTypes =
{
float styles[] = {
DMG_TYPE_HEADSHOT,
DMG_TYPE_MELEE,
DMG_TYPE_TESLA,
DMG_TYPE_FLAMETHROWER,
DMG_TYPE_GRENADE,
DMG_TYPE_EXPLOSIVE,
DMG_TYPE_LOWERTORSO,
DMG_TYPE_UPPERTORSO
};
float scores[] = {
DMG_SCORE_HEADSHOT,
DMG_SCORE_MELEE,
DMG_SCORE_GRENADE,
DMG_SCORE_GRENADE,
DMG_SCORE_GRENADE,
DMG_SCORE_EXPLOSIVE,
DMG_SCORE_LOWERTORSO,
DMG_SCORE_UPPERTORSO
};
for (float i = 0; i < styles.length; i++) {
float score_pre = self.points;
DieHandler(self, styles[i]);
float score_post = self.points;
float score_delta = score_post - score_pre;
Test_Assert((score_delta == scores[i]), sprintf("Unexpected score earned, expected [%d] but got [%d]!", scores[i], score_delta));
}
};
//
// Test_AddScore_MysteryBoxLeave()
// Asserts we get exact amount of points
// back when the Mystery Box attempts
// to leave.
// (https://github.com/nzp-team/nzportable/issues/775)
//
void() Test_AddScore_MysteryBoxLeave =
{
precache_sound("sounds/misc/giggle.wav");
float score_pre = self.points;
entity mystery_box_ent = spawn();
entity owner_ent = spawn();
entity old_self = self;
self = mystery_box_ent;
mystery_box();
self.owner = owner_ent;
self.owner.owner = old_self;
MBOX_PresentTeddy();
self = old_self;
float score_post = self.points;
Test_Assert((score_post - score_pre == mystery_box_cost), "Unexpected points earned!");
remove(mystery_box_ent);
remove(owner_ent);
};

View file

@ -0,0 +1,295 @@
/*
server/tests/test_weapons.qc
Weapon related unit tests.
Copyright (C) 2021-2025 NZ:P Team
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 2
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, write to:
Free Software Foundation, Inc.
59 Temple Place - Suite 330
Boston, MA 02111-1307, USA
*/
float(float condition, string message) Test_Assert;
void(string message) Test_Skip;
//
//
// Weapon System
//
//
//
// Test_Weapons_Gewehr_MagazineSize()
// Asserts the Gewehr and it's Pack-A-Punched
// form have the correct magazine sizes.
// (https://github.com/nzp-team/nzportable/issues/1102)
//
void() Test_Weapons_Gewehr_MagazineSize =
{
Test_Assert((getWeaponMag(W_GEWEHR) == 10), "Gewehr has incorrect magazine size!");
Test_Assert((getWeaponMag(W_COMPRESSOR) == 12), "Upgraded Gewehr has incorrect magazine size!");
};
//
// Test_Weapons_DualWield_ReloadLowReserveRightLeft()
// Asserts if we are attempting to reload a Dual Wield
// weapon while the reserve is too small to fill both
// mags, that we do not give out more ammo than intended.
// This triggers the Right side first, then Left.
// (https://github.com/nzp-team/nzportable/issues/1096)
//
void() Test_Weapons_DualWield_ReloadLowReserveRightLeft =
{
Weapon_RemoveWeapon(0);
Weapon_GiveWeapon(W_BIATCH, 0, 0);
self.weapons[0].weapon_magazine = 3;
self.weapons[0].weapon_magazine_left = 1;
self.weapons[0].weapon_reserve = 7;
W_Give_Ammo(S_RIGHT);
W_Give_Ammo(S_LEFT);
Test_Assert((self.weapons[0].weapon_magazine == 6), sprintf("Expected [6] for right magazine size, got [%d]!", self.weapons[0].weapon_magazine));
Test_Assert((self.weapons[0].weapon_magazine_left == 5), sprintf("Expected [5] for left magazine size, got [%d]!", self.weapons[0].weapon_magazine_left));
Test_Assert((self.weapons[0].weapon_reserve == 0), sprintf("Expected [0] for reserve size, got [%d]!", self.weapons[0].weapon_reserve));
};
//
// Test_Weapons_DualWield_ReloadLowReserveLeftRight()
// Asserts if we are attempting to reload a Dual Wield
// weapon while the reserve is too small to fill both
// mags, that we do not give out more ammo than intended.
// This triggers the Right side first, then Left.
// (https://github.com/nzp-team/nzportable/issues/1096)
//
void() Test_Weapons_DualWield_ReloadLowReserveLeftRight =
{
Weapon_RemoveWeapon(0);
Weapon_GiveWeapon(W_BIATCH, 0, 0);
self.weapons[0].weapon_magazine = 3;
self.weapons[0].weapon_magazine_left = 1;
self.weapons[0].weapon_reserve = 7;
W_Give_Ammo(S_LEFT);
W_Give_Ammo(S_RIGHT);
Test_Assert((self.weapons[0].weapon_magazine == 5), sprintf("Expected [5] for right magazine size, got [%d]!", self.weapons[0].weapon_magazine));
Test_Assert((self.weapons[0].weapon_magazine_left == 6), sprintf("Expected [6] for left magazine size, got [%d]!", self.weapons[0].weapon_magazine_left));
Test_Assert((self.weapons[0].weapon_reserve == 0), sprintf("Expected [0] for reserve size, got [%d]!", self.weapons[0].weapon_reserve));
};
//
// Test_Weapons_BowieKnife_RemovedAfterRespawn()
// Asserts that upon respawning a client loses the
// Bowie Knife.
// (https://github.com/nzp-team/nzportable/issues/922)
//
void() Test_Weapons_BowieKnife_RemovedAfterRespawn =
{
self.has_bowie_knife = true;
PlayerSpawn();
Test_Assert((self.has_bowie_knife == false), "Client still has Bowie Knife!");
};
//
// Test_Weapons_HoldTwoWeapons()
// Asserts we can hold more than just a single
// weapon.
// (https://github.com/nzp-team/nzportable/issues/786)
//
void() Test_Weapons_HoldTwoWeapons =
{
Weapon_GiveWeapon(W_COLT, 0, 0);
Weapon_GiveWeapon(W_GEWEHR, 0, 0);
Test_Assert((self.weapons[0].weapon_id != 0), "No weapon in slot zero!");
Test_Assert((self.weapons[1].weapon_id != 0), "No weapon in slot one!");
Weapon_RemoveWeapon(1);
};
//
// Test_Weapons_HoldThreeWeapons()
// Gives the client Mule Kick and asserts we can
// hold three weapons.
//
void() Test_Weapons_HoldThreeWeapons =
{
GivePerk(P_MULE);
Weapon_GiveWeapon(W_COLT, 0, 0);
Weapon_GiveWeapon(W_GEWEHR, 0, 0);
Weapon_GiveWeapon(W_KAR, 0, 0);
Test_Assert((self.weapons[0].weapon_id != 0), "No weapon in slot zero!");
Test_Assert((self.weapons[1].weapon_id != 0), "No weapon in slot one!");
Test_Assert((self.weapons[2].weapon_id != 0), "No weapon in slot two!");
Weapon_RemoveWeapon(1);
Weapon_RemoveWeapon(2);
self.perks = 0;
};
//
// Test_Weapons_UpgradedSound()
// Validates certain weapons are set to not
// play an upgraded sound.
//
void() Test_Weapons_UpgradedSound =
{
float weapon_list[] = { W_DG3, W_PORTER, W_FIW };
for (float i = 0; i < weapon_list.length; i++) {
Test_Assert((WepDef_DoesNotPlayUpgradedSound(weapon_list[i]) == true), sprintf("Weapon [%s] plays an upgraded sound!", GetWeaponName(weapon_list[i])));
}
return false;
};
//
// Test_Weapons_ShotgunPenetration()
// Validates shotgun penetration is set
// correctly.
// (https://github.com/nzp-team/nzportable/issues/652)
//
void() Test_Weapons_ShotgunPenetration =
{
float weapon_list[] = { W_DB, W_BORE, W_SAWNOFF, W_SNUFF, W_TRENCH, W_GUT };
for (float i = 0; i < weapon_list.length; i++) {
Test_Assert((getWeaponPenetration(weapon_list[i], 2) != 0), sprintf("Weapon [%s] not correctly penetrating!", GetWeaponName(weapon_list[i])));
}
};
//
// Test_Weapons_WunderWaffe_UpgradedReserve()
// Validates correct reserve ammo count for DG-3.
// (https://github.com/nzp-team/nzportable/issues/651)
//
void() Test_Weapons_WunderWaffe_UpgradedReserve =
{
Test_Assert(getWeaponAmmo(W_DG3) == 30, "Incorrect reserve ammo!");
};
//
// Test_Weapons_WunderWaffe_FireAtBarrel()
// Simulates firing the DG-2 at an explosive barrel
// and validates it does not count as a kill and
// does not award points.
// (https://github.com/nzp-team/nzportable/issues/584)
//
void() Test_Weapons_WunderWaffe_FireAtBarrel =
{
Test_Skip("Skipped while facing odd traceline behavior");
#if 0
float kills_pre = self.kills;
float score_pre = self.points;
makevectors(self.angles);
entity barrel = spawn();
entity old_self = self;
self = barrel;
setorigin(self, old_self.origin + (v_forward * 50) + '0 0 16');
explosive_barrel();
self = old_self;
Weapon_GiveWeapon(W_TESLA, 0, 0);
W_FireTesla();
float kills_post = self.kills;
float score_post = self.points;
Test_Assert((kills_pre == kills_post), "Explosive barrel counted as kill!");
Test_Assert((score_pre == score_post), "Explosive barrel earned score!");
#endif
};
//
//
// Wall Weapons
//
//
//
// Test_WallWeapons_Message()
// Asserts a written message field gets removed
// from a buy_weapon()
//
void() Test_WallWeapons_Message =
{
entity weapon = spawn();
entity old_self = self;
self = weapon;
self.message = "test";
buy_weapon();
Test_Assert((self.message == ""), "Message field was not removed!");
self = old_self;
remove(weapon);
};
//
// Test_WallWeapons_ActivatesAllTargets()
// Validates that when a buy_weapon entity
// uses its targets, all 8 are activated.
// (https://github.com/nzp-team/nzportable/issues/961)
//
void() Test_WallWeapons_ActivatesAllTargets =
{
float i;
entity weapon = spawn();
entity old_self = self;
entity targets[8];
for (i = 0; i < 8; i++) {
targets[i] = spawn();
targets[i].health = 2;
targets[i].targetname = sprintf("test_target_%d", i);
self = targets[i];
game_counter();
}
self = weapon;
self.target = "test_target_0";
self.target2 = "test_target_1";
self.target3 = "test_target_2";
self.target4 = "test_target_3";
self.target5 = "test_target_4";
self.target6 = "test_target_5";
self.target7 = "test_target_6";
self.target8 = "test_target_7";
buy_weapon();
SUB_UseTargets();
for(i = 0; i < 8; i++) {
Test_Assert((targets[i].frags == 1), sprintf("Target #%d did not increment!", i));
remove(targets[i]);
}
self = old_self;
remove(weapon);
};

84
testing/run_unit_tests.sh Normal file
View file

@ -0,0 +1,84 @@
#!/bin/bash
#
# Nazi Zombies: Portable
# QuakeC Unit test runner.
# ----
# This is intended to be used via a Docker
# container running ubuntu:24.10.
#
set -o errexit
# tzdata will try to display an interactive install prompt by
# default, so make sure we define our system as non-interactive.
export DEBIAN_FRONTEND=noninteractive DEBCONF_NONINTERACTIVE_SEEN=true
WORKING_DIRECTORY="/working"
OUTPUT_LOG="${WORKING_DIRECTORY}/run.log"
REPO_PWD=$(pwd)
function setup_container()
{
echo "[INFO]: Installing dependancies.."
apt update -y
apt install libsdl2-dev wget zip python3 python3-pip -y
wget https://raw.githubusercontent.com/nzp-team/QCHashTableGenerator/main/requirements.txt
pip install -r requirements.txt --break-system-packages
rm requirements.txt
mkdir -p "${WORKING_DIRECTORY}"
}
function download_nzp()
{
echo "[INFO]: Obtaining latest Nazi Zombies: Portable Linux x86_64 release.."
cd "${WORKING_DIRECTORY}"
wget https://github.com/nzp-team/nzportable/releases/download/nightly/nzportable-linux64.zip
mkdir nzportable-linux64
unzip nzportable-linux64.zip -d nzportable-linux64/
chmod +x nzportable-linux64/nzportable64-sdl
}
function build_quakec()
{
echo "[INFO]: Building QuakeC.."
cd "${REPO_PWD}/tools"
local cmd="./qc-compiler-gnu.sh --test-mode"
${cmd}
echo "[INFO]: Moving QuakeC to game download.."
cp "${REPO_PWD}/build/fte/qwprogs.dat" "${WORKING_DIRECTORY}/nzportable-linux64/nzp/"
}
function run_test()
{
local game_crashed="0"
touch "${OUTPUT_LOG}"
echo "[INFO]: Running unit tests.."
cd "${WORKING_DIRECTORY}/nzportable-linux64/"
local cmd="./nzportable64-sdl +map nzp_warehouse +vid_renderer headless"
${cmd} | tee "${OUTPUT_LOG}"
failed_count=$( cat "${OUTPUT_LOG}" | grep "* Failed: " | cut -c 11- || game_crashed="1" )
if [[ "${game_crashed}" -ne "0" ]]; then
echo "[ERROR]: Game crashed, no condump generated! Bailing!"
exit 1
fi
if [[ "${failed_count}" -ne "0" ]]; then
echo "[ERROR]: [${failed_count}] failures occurred while running unit tests!"
exit 1
fi
echo "[INFO]: UNIT TEST PASSED."
}
function main()
{
setup_container;
download_nzp;
build_quakec;
run_test;
}
main;

View file

@ -1,5 +1,5 @@
#!/usr/bin/env bash
set -e errexit
set -o errexit
SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &> /dev/null && pwd)
QUAKEC_ROOT=$(dirname "${SCRIPT_DIR}")
@ -13,6 +13,14 @@ if [[ "${OSTYPE}" == "darwin"* ]]; then
FTEQCC="fteqcc-cli-mac"
fi
# Arguments
while true; do
case "$1" in
-t | --test-mode ) TEST_FLAG="-DQUAKEC_TEST"; shift 1 ;;
* ) break ;;
esac
done
function setup()
{
cd "${QUAKEC_ROOT}"
@ -53,7 +61,7 @@ function main()
{
setup;
compile_progs "csqc" "FTE CSQC" "-DFTE -Wall"
compile_progs "ssqc" "FTE SSQC" "-O3 -DFTE -Wall"
compile_progs "ssqc" "FTE SSQC" "-O3 -DFTE ${TEST_FLAG} -Wall"
compile_progs "menu" "FTE MenuQC" "-O3 -DFTE -Wall"
compile_progs "ssqc" "Vril SSQC" "-O3 -Wall"
exit ${RC}